diff --git a/.vscode/settings.json b/.vscode/settings.json index 9ac42bf24a4..bec2efbe491 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,8 +2,6 @@ // --- Chat --- "inlineChat.enableV2": true, "chat.tools.terminal.autoApprove": { - "/^npm (test|lint|run compile)\\b/": true, - "/^npx tsc\\b.*--noEmit/": true, "scripts/test.bat": true, "scripts/test.sh": true, "scripts/test-integration.bat": true, diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 3fb80e0ee98..f17163f9f55 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1924,7 +1924,15 @@ export class Repository { async stage(path: string, data: Uint8Array): Promise { const relativePath = this.sanitizeRelativePath(path); const child = this.stream(['hash-object', '--stdin', '-w', '--path', relativePath], { stdio: [null, null, null] }); - child.stdin!.end(data); + + if (!child.stdin) { + throw new GitError({ + message: 'Failed to spawn git process', + exitCode: -1 + }); + } + + child.stdin.end(data); const { exitCode, stdout } = await exec(child); const hash = stdout.toString('utf8'); @@ -2684,19 +2692,23 @@ export class Repository { if (limit !== 0 && parser.status.length > limit) { child.removeListener('close', onClose); - child.stdout!.removeListener('data', onStdoutData); + child.stdout?.removeListener('data', onStdoutData); child.kill(); c({ status: parser.status.slice(0, limit), statusLength: parser.status.length, didHitLimit: true }); } }; - child.stdout!.setEncoding('utf8'); - child.stdout!.on('data', onStdoutData); + if (child.stdout) { + child.stdout.setEncoding('utf8'); + child.stdout.on('data', onStdoutData); + } const stderrData: string[] = []; - child.stderr!.setEncoding('utf8'); - child.stderr!.on('data', raw => stderrData.push(raw as string)); + if (child.stderr) { + child.stderr.setEncoding('utf8'); + child.stderr.on('data', raw => stderrData.push(raw as string)); + } child.on('error', cpErrorHandler(e)); child.on('close', onClose); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 7977887182f..0e733707674 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -2347,7 +2347,15 @@ export class Repository implements Disposable { // https://git-scm.com/docs/git-check-ignore#git-check-ignore--z const child = this.repository.stream(['check-ignore', '-v', '-z', '--stdin'], { stdio: [null, null, null] }); - child.stdin!.end(filePaths.join('\0'), 'utf8'); + + if (!child.stdin) { + return reject(new GitError({ + message: 'Failed to spawn git process', + exitCode: -1 + })); + } + + child.stdin.end(filePaths.join('\0'), 'utf8'); const onExit = (exitCode: number) => { if (exitCode === 1) { @@ -2369,12 +2377,16 @@ export class Repository implements Disposable { data += raw; }; - child.stdout!.setEncoding('utf8'); - child.stdout!.on('data', onStdoutData); + if (child.stdout) { + child.stdout.setEncoding('utf8'); + child.stdout.on('data', onStdoutData); + } let stderr: string = ''; - child.stderr!.setEncoding('utf8'); - child.stderr!.on('data', raw => stderr += raw); + if (child.stderr) { + child.stderr.setEncoding('utf8'); + child.stderr.on('data', raw => stderr += raw); + } child.on('error', reject); child.on('exit', onExit); diff --git a/package-lock.json b/package-lock.json index bbaf75e91da..d5d62a00450 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.56", - "@xterm/addon-image": "^0.10.0-beta.56", - "@xterm/addon-ligatures": "^0.11.0-beta.56", - "@xterm/addon-progress": "^0.3.0-beta.56", - "@xterm/addon-search": "^0.17.0-beta.56", - "@xterm/addon-serialize": "^0.15.0-beta.56", - "@xterm/addon-unicode11": "^0.10.0-beta.56", - "@xterm/addon-webgl": "^0.20.0-beta.55", - "@xterm/headless": "^6.1.0-beta.56", - "@xterm/xterm": "^6.1.0-beta.56", + "@xterm/addon-clipboard": "^0.3.0-beta.77", + "@xterm/addon-image": "^0.10.0-beta.77", + "@xterm/addon-ligatures": "^0.11.0-beta.77", + "@xterm/addon-progress": "^0.3.0-beta.77", + "@xterm/addon-search": "^0.17.0-beta.77", + "@xterm/addon-serialize": "^0.15.0-beta.77", + "@xterm/addon-unicode11": "^0.10.0-beta.77", + "@xterm/addon-webgl": "^0.20.0-beta.76", + "@xterm/headless": "^6.1.0-beta.77", + "@xterm/xterm": "^6.1.0-beta.77", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -3511,30 +3511,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.56.tgz", - "integrity": "sha512-NHwA74cC+WAXJkcF/pQXHZFalGdhcUXefmatRJkRjiRGAwjn+JZYDATEnkkdgiGEWagjegalyRSBh+++hogvtQ==", + "version": "0.3.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.77.tgz", + "integrity": "sha512-wI5PTmsdBWEsXY/odiFwBnLHCDD5bTYI9/ix4hphuAU/E6DS9T4vQPxv5BvW9i/QJ9T1/FRPtzR54xwbJ4+eag==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.56.tgz", - "integrity": "sha512-ERc6kHXjGRbn20aqvrROx2xx+Mc5uVWpQnesMJuncnsDodEDTiunEyaG2VdBpEnlDW0xfnDA358ZQHnqzRE3SA==", + "version": "0.10.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.77.tgz", + "integrity": "sha512-85lYTKFgkXpMVwfSiFpdgXp4cNREr8BAIImFbnovZcmFdqDQn4QoiOywORXYos6on52oeFAyuPVEEa/GMHuHZw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.56.tgz", - "integrity": "sha512-nRVvfaJYAdxd2chk8aEYdWPJvcBo/GojeH8LRV2cuJYWUBmw7wc9USktE+K56WKEAYPPmy+l3RY9WGZrjnf/PQ==", + "version": "0.11.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.77.tgz", + "integrity": "sha512-NlmHh5D4vu1fkIXvpuTQAsVTBaZh1B55UgltnSusLiLe42jrCcU8LP+1WRrgUKnsiobZ2Q5xtrV+hiDvUro09Q==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3544,67 +3544,67 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.56.tgz", - "integrity": "sha512-AgCCqziSDuszTrUdobG5HAN1kTRrMI7AWWWajSkTPKLYUMsD9grDSYKHDSUPHz/HC19AYaOZATgu4dFZ0zcOhw==", + "version": "0.3.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.77.tgz", + "integrity": "sha512-duzs9Y/BSn5RyOsKdQEqQ6oeSSXlM+jxGak3KzxWwUt0b20b7UA+lHbcj0aUqTekER1oblCJf4PgJXMMH85+hw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.56.tgz", - "integrity": "sha512-VQOIBrFUaUaM/ChqriNJW1k6TmgxsbeAppURsYV78+g2qEv5YtQKGDyEeR0yDb+UxF1t3bYeDaiOvg2AbFK9Yw==", + "version": "0.17.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.77.tgz", + "integrity": "sha512-GZ/B/ox/JwQTkuak6lba3JvqvEW1GaJ1p/e+WdYlyC343H7rxkls1VvA+y4KajFw+8WWYSIJn6KtvfNH+IKLww==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.56.tgz", - "integrity": "sha512-nn7HB0A5e6pZAGjmgE+Uur3GNtGM29XdAjm+vsaACaYlNwyMsYt2wBf/wklHH9TTReKLsag/RWawglrtMXP7HQ==", + "version": "0.15.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.77.tgz", + "integrity": "sha512-wNzJvUr2j/6p7VqBnhHSHhnimih7TE00RCPQtNo5JnWE1g7dx1ftDvojEW4X7g+Hz/TFCSCJ/JxNaps7W7Ymhw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.56.tgz", - "integrity": "sha512-6mMqs/5xYD5zMFKtMvHy5hhMHzHt4TXeoYE59G42Ac2w6vi7PNEAaEWYNd4Y4/OcR6cuIFLvzj/nlrXmxrBpqg==", + "version": "0.10.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.77.tgz", + "integrity": "sha512-T1XgoL1ML5fE0+nSb6bmIPHUN6Xw8X+7iQhhFa32MWDRiSqzSFYxWTs+f/zPCxXopKoL2GZEt9BXItaXMP4apQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.55", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.55.tgz", - "integrity": "sha512-F/QMnRffPSmT9SaKl01dVNDHhjQov0msxKyWQrcgGbkyGOlIHDnHn11rmgtWrD63w/zyLa3DFcLKx++kbPV99g==", + "version": "0.20.0-beta.76", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.76.tgz", + "integrity": "sha512-7E9eBZMe8zn8pJPrnKuJfTpm3s55mJpch6h7TnqvTHzHBVM0eTMSTz/s7CfMPzEKWQ/i4JIfnIRuWXys6UF2qA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.56.tgz", - "integrity": "sha512-9PfYeTlJHxuAA2MiaNRj/aEmGE7RL4GxO0rZKudkPaG2koy/mcLsrP5yI+jSt3ylXkzYpHVEBKz4rY9cHZlOWQ==", + "version": "6.1.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.77.tgz", + "integrity": "sha512-meZRyQMlOIAzVYBj9QzOp7lBNPSOCQmec/c5kbcJeeWjbF89OOh1LO7775QUr0oUB0mBYdAEGNxQlRH8fL0L3Q==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.56.tgz", - "integrity": "sha512-lpgFqA8R6UWsULseccXMN5FUEKe43i/9SC2+BDROIup6ZVhqDkyCfWf4AqRaoaF1swD4yhe5opdMsnrRryCFUQ==", + "version": "6.1.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.77.tgz", + "integrity": "sha512-aAZhkdX+9VENtrbs02RoG+7OAK/G5+GOfumFSJrj7dFHrkSezDmJAHNwnU0eCn23/x1IXJ6xpWCchW1MVbfn3w==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/package.json b/package.json index 42dd73d992a..9307ce7934b 100644 --- a/package.json +++ b/package.json @@ -89,16 +89,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.56", - "@xterm/addon-image": "^0.10.0-beta.56", - "@xterm/addon-ligatures": "^0.11.0-beta.56", - "@xterm/addon-progress": "^0.3.0-beta.56", - "@xterm/addon-search": "^0.17.0-beta.56", - "@xterm/addon-serialize": "^0.15.0-beta.56", - "@xterm/addon-unicode11": "^0.10.0-beta.56", - "@xterm/addon-webgl": "^0.20.0-beta.55", - "@xterm/headless": "^6.1.0-beta.56", - "@xterm/xterm": "^6.1.0-beta.56", + "@xterm/addon-clipboard": "^0.3.0-beta.77", + "@xterm/addon-image": "^0.10.0-beta.77", + "@xterm/addon-ligatures": "^0.11.0-beta.77", + "@xterm/addon-progress": "^0.3.0-beta.77", + "@xterm/addon-search": "^0.17.0-beta.77", + "@xterm/addon-serialize": "^0.15.0-beta.77", + "@xterm/addon-unicode11": "^0.10.0-beta.77", + "@xterm/addon-webgl": "^0.20.0-beta.76", + "@xterm/headless": "^6.1.0-beta.77", + "@xterm/xterm": "^6.1.0-beta.77", "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 946d9286ffe..b711e3f9937 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -20,16 +20,16 @@ "@vscode/watcher": "bpasero/watcher#8ecffb4a57df24ac3e6946aa095b9b1f14f8bba9", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.3.0-beta.56", - "@xterm/addon-image": "^0.10.0-beta.56", - "@xterm/addon-ligatures": "^0.11.0-beta.56", - "@xterm/addon-progress": "^0.3.0-beta.56", - "@xterm/addon-search": "^0.17.0-beta.56", - "@xterm/addon-serialize": "^0.15.0-beta.56", - "@xterm/addon-unicode11": "^0.10.0-beta.56", - "@xterm/addon-webgl": "^0.20.0-beta.55", - "@xterm/headless": "^6.1.0-beta.56", - "@xterm/xterm": "^6.1.0-beta.56", + "@xterm/addon-clipboard": "^0.3.0-beta.77", + "@xterm/addon-image": "^0.10.0-beta.77", + "@xterm/addon-ligatures": "^0.11.0-beta.77", + "@xterm/addon-progress": "^0.3.0-beta.77", + "@xterm/addon-search": "^0.17.0-beta.77", + "@xterm/addon-serialize": "^0.15.0-beta.77", + "@xterm/addon-unicode11": "^0.10.0-beta.77", + "@xterm/addon-webgl": "^0.20.0-beta.76", + "@xterm/headless": "^6.1.0-beta.77", + "@xterm/xterm": "^6.1.0-beta.77", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -243,30 +243,30 @@ "license": "MIT" }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.56.tgz", - "integrity": "sha512-NHwA74cC+WAXJkcF/pQXHZFalGdhcUXefmatRJkRjiRGAwjn+JZYDATEnkkdgiGEWagjegalyRSBh+++hogvtQ==", + "version": "0.3.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.77.tgz", + "integrity": "sha512-wI5PTmsdBWEsXY/odiFwBnLHCDD5bTYI9/ix4hphuAU/E6DS9T4vQPxv5BvW9i/QJ9T1/FRPtzR54xwbJ4+eag==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.56.tgz", - "integrity": "sha512-ERc6kHXjGRbn20aqvrROx2xx+Mc5uVWpQnesMJuncnsDodEDTiunEyaG2VdBpEnlDW0xfnDA358ZQHnqzRE3SA==", + "version": "0.10.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.77.tgz", + "integrity": "sha512-85lYTKFgkXpMVwfSiFpdgXp4cNREr8BAIImFbnovZcmFdqDQn4QoiOywORXYos6on52oeFAyuPVEEa/GMHuHZw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.56.tgz", - "integrity": "sha512-nRVvfaJYAdxd2chk8aEYdWPJvcBo/GojeH8LRV2cuJYWUBmw7wc9USktE+K56WKEAYPPmy+l3RY9WGZrjnf/PQ==", + "version": "0.11.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.77.tgz", + "integrity": "sha512-NlmHh5D4vu1fkIXvpuTQAsVTBaZh1B55UgltnSusLiLe42jrCcU8LP+1WRrgUKnsiobZ2Q5xtrV+hiDvUro09Q==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -276,67 +276,67 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.56.tgz", - "integrity": "sha512-AgCCqziSDuszTrUdobG5HAN1kTRrMI7AWWWajSkTPKLYUMsD9grDSYKHDSUPHz/HC19AYaOZATgu4dFZ0zcOhw==", + "version": "0.3.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.77.tgz", + "integrity": "sha512-duzs9Y/BSn5RyOsKdQEqQ6oeSSXlM+jxGak3KzxWwUt0b20b7UA+lHbcj0aUqTekER1oblCJf4PgJXMMH85+hw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.56.tgz", - "integrity": "sha512-VQOIBrFUaUaM/ChqriNJW1k6TmgxsbeAppURsYV78+g2qEv5YtQKGDyEeR0yDb+UxF1t3bYeDaiOvg2AbFK9Yw==", + "version": "0.17.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.77.tgz", + "integrity": "sha512-GZ/B/ox/JwQTkuak6lba3JvqvEW1GaJ1p/e+WdYlyC343H7rxkls1VvA+y4KajFw+8WWYSIJn6KtvfNH+IKLww==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.56.tgz", - "integrity": "sha512-nn7HB0A5e6pZAGjmgE+Uur3GNtGM29XdAjm+vsaACaYlNwyMsYt2wBf/wklHH9TTReKLsag/RWawglrtMXP7HQ==", + "version": "0.15.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.77.tgz", + "integrity": "sha512-wNzJvUr2j/6p7VqBnhHSHhnimih7TE00RCPQtNo5JnWE1g7dx1ftDvojEW4X7g+Hz/TFCSCJ/JxNaps7W7Ymhw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.56.tgz", - "integrity": "sha512-6mMqs/5xYD5zMFKtMvHy5hhMHzHt4TXeoYE59G42Ac2w6vi7PNEAaEWYNd4Y4/OcR6cuIFLvzj/nlrXmxrBpqg==", + "version": "0.10.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.77.tgz", + "integrity": "sha512-T1XgoL1ML5fE0+nSb6bmIPHUN6Xw8X+7iQhhFa32MWDRiSqzSFYxWTs+f/zPCxXopKoL2GZEt9BXItaXMP4apQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.55", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.55.tgz", - "integrity": "sha512-F/QMnRffPSmT9SaKl01dVNDHhjQov0msxKyWQrcgGbkyGOlIHDnHn11rmgtWrD63w/zyLa3DFcLKx++kbPV99g==", + "version": "0.20.0-beta.76", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.76.tgz", + "integrity": "sha512-7E9eBZMe8zn8pJPrnKuJfTpm3s55mJpch6h7TnqvTHzHBVM0eTMSTz/s7CfMPzEKWQ/i4JIfnIRuWXys6UF2qA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.56.tgz", - "integrity": "sha512-9PfYeTlJHxuAA2MiaNRj/aEmGE7RL4GxO0rZKudkPaG2koy/mcLsrP5yI+jSt3ylXkzYpHVEBKz4rY9cHZlOWQ==", + "version": "6.1.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.77.tgz", + "integrity": "sha512-meZRyQMlOIAzVYBj9QzOp7lBNPSOCQmec/c5kbcJeeWjbF89OOh1LO7775QUr0oUB0mBYdAEGNxQlRH8fL0L3Q==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.56.tgz", - "integrity": "sha512-lpgFqA8R6UWsULseccXMN5FUEKe43i/9SC2+BDROIup6ZVhqDkyCfWf4AqRaoaF1swD4yhe5opdMsnrRryCFUQ==", + "version": "6.1.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.77.tgz", + "integrity": "sha512-aAZhkdX+9VENtrbs02RoG+7OAK/G5+GOfumFSJrj7dFHrkSezDmJAHNwnU0eCn23/x1IXJ6xpWCchW1MVbfn3w==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/package.json b/remote/package.json index a540f0e0f67..b914e0c2897 100644 --- a/remote/package.json +++ b/remote/package.json @@ -15,16 +15,16 @@ "@vscode/watcher": "bpasero/watcher#8ecffb4a57df24ac3e6946aa095b9b1f14f8bba9", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.3.0-beta.56", - "@xterm/addon-image": "^0.10.0-beta.56", - "@xterm/addon-ligatures": "^0.11.0-beta.56", - "@xterm/addon-progress": "^0.3.0-beta.56", - "@xterm/addon-search": "^0.17.0-beta.56", - "@xterm/addon-serialize": "^0.15.0-beta.56", - "@xterm/addon-unicode11": "^0.10.0-beta.56", - "@xterm/addon-webgl": "^0.20.0-beta.55", - "@xterm/headless": "^6.1.0-beta.56", - "@xterm/xterm": "^6.1.0-beta.56", + "@xterm/addon-clipboard": "^0.3.0-beta.77", + "@xterm/addon-image": "^0.10.0-beta.77", + "@xterm/addon-ligatures": "^0.11.0-beta.77", + "@xterm/addon-progress": "^0.3.0-beta.77", + "@xterm/addon-search": "^0.17.0-beta.77", + "@xterm/addon-serialize": "^0.15.0-beta.77", + "@xterm/addon-unicode11": "^0.10.0-beta.77", + "@xterm/addon-webgl": "^0.20.0-beta.76", + "@xterm/headless": "^6.1.0-beta.77", + "@xterm/xterm": "^6.1.0-beta.77", "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 0b655405886..5d3e7a4c603 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.56", - "@xterm/addon-image": "^0.10.0-beta.56", - "@xterm/addon-ligatures": "^0.11.0-beta.56", - "@xterm/addon-progress": "^0.3.0-beta.56", - "@xterm/addon-search": "^0.17.0-beta.56", - "@xterm/addon-serialize": "^0.15.0-beta.56", - "@xterm/addon-unicode11": "^0.10.0-beta.56", - "@xterm/addon-webgl": "^0.20.0-beta.55", - "@xterm/xterm": "^6.1.0-beta.56", + "@xterm/addon-clipboard": "^0.3.0-beta.77", + "@xterm/addon-image": "^0.10.0-beta.77", + "@xterm/addon-ligatures": "^0.11.0-beta.77", + "@xterm/addon-progress": "^0.3.0-beta.77", + "@xterm/addon-search": "^0.17.0-beta.77", + "@xterm/addon-serialize": "^0.15.0-beta.77", + "@xterm/addon-unicode11": "^0.10.0-beta.77", + "@xterm/addon-webgl": "^0.20.0-beta.76", + "@xterm/xterm": "^6.1.0-beta.77", "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.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.56.tgz", - "integrity": "sha512-NHwA74cC+WAXJkcF/pQXHZFalGdhcUXefmatRJkRjiRGAwjn+JZYDATEnkkdgiGEWagjegalyRSBh+++hogvtQ==", + "version": "0.3.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.77.tgz", + "integrity": "sha512-wI5PTmsdBWEsXY/odiFwBnLHCDD5bTYI9/ix4hphuAU/E6DS9T4vQPxv5BvW9i/QJ9T1/FRPtzR54xwbJ4+eag==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.56.tgz", - "integrity": "sha512-ERc6kHXjGRbn20aqvrROx2xx+Mc5uVWpQnesMJuncnsDodEDTiunEyaG2VdBpEnlDW0xfnDA358ZQHnqzRE3SA==", + "version": "0.10.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.77.tgz", + "integrity": "sha512-85lYTKFgkXpMVwfSiFpdgXp4cNREr8BAIImFbnovZcmFdqDQn4QoiOywORXYos6on52oeFAyuPVEEa/GMHuHZw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.56.tgz", - "integrity": "sha512-nRVvfaJYAdxd2chk8aEYdWPJvcBo/GojeH8LRV2cuJYWUBmw7wc9USktE+K56WKEAYPPmy+l3RY9WGZrjnf/PQ==", + "version": "0.11.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.77.tgz", + "integrity": "sha512-NlmHh5D4vu1fkIXvpuTQAsVTBaZh1B55UgltnSusLiLe42jrCcU8LP+1WRrgUKnsiobZ2Q5xtrV+hiDvUro09Q==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -125,58 +125,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.56.tgz", - "integrity": "sha512-AgCCqziSDuszTrUdobG5HAN1kTRrMI7AWWWajSkTPKLYUMsD9grDSYKHDSUPHz/HC19AYaOZATgu4dFZ0zcOhw==", + "version": "0.3.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.77.tgz", + "integrity": "sha512-duzs9Y/BSn5RyOsKdQEqQ6oeSSXlM+jxGak3KzxWwUt0b20b7UA+lHbcj0aUqTekER1oblCJf4PgJXMMH85+hw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.56.tgz", - "integrity": "sha512-VQOIBrFUaUaM/ChqriNJW1k6TmgxsbeAppURsYV78+g2qEv5YtQKGDyEeR0yDb+UxF1t3bYeDaiOvg2AbFK9Yw==", + "version": "0.17.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.77.tgz", + "integrity": "sha512-GZ/B/ox/JwQTkuak6lba3JvqvEW1GaJ1p/e+WdYlyC343H7rxkls1VvA+y4KajFw+8WWYSIJn6KtvfNH+IKLww==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.56.tgz", - "integrity": "sha512-nn7HB0A5e6pZAGjmgE+Uur3GNtGM29XdAjm+vsaACaYlNwyMsYt2wBf/wklHH9TTReKLsag/RWawglrtMXP7HQ==", + "version": "0.15.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.77.tgz", + "integrity": "sha512-wNzJvUr2j/6p7VqBnhHSHhnimih7TE00RCPQtNo5JnWE1g7dx1ftDvojEW4X7g+Hz/TFCSCJ/JxNaps7W7Ymhw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.56.tgz", - "integrity": "sha512-6mMqs/5xYD5zMFKtMvHy5hhMHzHt4TXeoYE59G42Ac2w6vi7PNEAaEWYNd4Y4/OcR6cuIFLvzj/nlrXmxrBpqg==", + "version": "0.10.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.77.tgz", + "integrity": "sha512-T1XgoL1ML5fE0+nSb6bmIPHUN6Xw8X+7iQhhFa32MWDRiSqzSFYxWTs+f/zPCxXopKoL2GZEt9BXItaXMP4apQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.55", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.55.tgz", - "integrity": "sha512-F/QMnRffPSmT9SaKl01dVNDHhjQov0msxKyWQrcgGbkyGOlIHDnHn11rmgtWrD63w/zyLa3DFcLKx++kbPV99g==", + "version": "0.20.0-beta.76", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.76.tgz", + "integrity": "sha512-7E9eBZMe8zn8pJPrnKuJfTpm3s55mJpch6h7TnqvTHzHBVM0eTMSTz/s7CfMPzEKWQ/i4JIfnIRuWXys6UF2qA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.56" + "@xterm/xterm": "^6.1.0-beta.77" } }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.56", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.56.tgz", - "integrity": "sha512-lpgFqA8R6UWsULseccXMN5FUEKe43i/9SC2+BDROIup6ZVhqDkyCfWf4AqRaoaF1swD4yhe5opdMsnrRryCFUQ==", + "version": "6.1.0-beta.77", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.77.tgz", + "integrity": "sha512-aAZhkdX+9VENtrbs02RoG+7OAK/G5+GOfumFSJrj7dFHrkSezDmJAHNwnU0eCn23/x1IXJ6xpWCchW1MVbfn3w==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/web/package.json b/remote/web/package.json index 7b75777c51f..12d9d507321 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.56", - "@xterm/addon-image": "^0.10.0-beta.56", - "@xterm/addon-ligatures": "^0.11.0-beta.56", - "@xterm/addon-progress": "^0.3.0-beta.56", - "@xterm/addon-search": "^0.17.0-beta.56", - "@xterm/addon-serialize": "^0.15.0-beta.56", - "@xterm/addon-unicode11": "^0.10.0-beta.56", - "@xterm/addon-webgl": "^0.20.0-beta.55", - "@xterm/xterm": "^6.1.0-beta.56", + "@xterm/addon-clipboard": "^0.3.0-beta.77", + "@xterm/addon-image": "^0.10.0-beta.77", + "@xterm/addon-ligatures": "^0.11.0-beta.77", + "@xterm/addon-progress": "^0.3.0-beta.77", + "@xterm/addon-search": "^0.17.0-beta.77", + "@xterm/addon-serialize": "^0.15.0-beta.77", + "@xterm/addon-unicode11": "^0.10.0-beta.77", + "@xterm/addon-webgl": "^0.20.0-beta.76", + "@xterm/xterm": "^6.1.0-beta.77", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index 9660e763095..6d3e3f2b3db 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -228,7 +228,7 @@ export class Separator implements IAction { readonly tooltip: string = ''; readonly class: string = 'separator'; readonly enabled: boolean = false; - readonly checked: boolean = false; + readonly checked: undefined = undefined; async run() { } } diff --git a/src/vs/base/common/oauth.ts b/src/vs/base/common/oauth.ts index 37235fe1179..ea5a3cbbe35 100644 --- a/src/vs/base/common/oauth.ts +++ b/src/vs/base/common/oauth.ts @@ -1112,7 +1112,7 @@ export async function fetchResourceMetadata( targetResource: string, resourceMetadataUrl: string | undefined, options: IFetchResourceMetadataOptions = {} -): Promise<{ metadata: IAuthorizationProtectedResourceMetadata; errors: Error[] }> { +): Promise<{ metadata: IAuthorizationProtectedResourceMetadata; discoveryUrl: string; errors: Error[] }> { const { sameOriginHeaders = {}, fetch: fetchImpl = fetch @@ -1164,7 +1164,7 @@ export async function fetchResourceMetadata( if (resourceMetadataUrl) { try { const metadata = await fetchPrm(resourceMetadataUrl, targetResource); - return { metadata, errors }; + return { metadata, discoveryUrl: resourceMetadataUrl, errors }; } catch (e) { errors.push(e instanceof Error ? e : new Error(String(e))); } @@ -1178,7 +1178,7 @@ export async function fetchResourceMetadata( const pathAppendedUrl = `${rootUrl}${targetResourceUrlObj.pathname}`; try { const metadata = await fetchPrm(pathAppendedUrl, targetResource); - return { metadata, errors }; + return { metadata, discoveryUrl: pathAppendedUrl, errors }; } catch (e) { errors.push(e instanceof Error ? e : new Error(String(e))); } @@ -1187,7 +1187,7 @@ export async function fetchResourceMetadata( // Finally, try root discovery try { const metadata = await fetchPrm(rootUrl, targetResourceUrlObj.origin); - return { metadata, errors }; + return { metadata, discoveryUrl: rootUrl, errors }; } catch (e) { errors.push(e instanceof Error ? e : new Error(String(e))); } @@ -1261,7 +1261,7 @@ async function getErrText(res: CommonResponse): Promise { export async function fetchAuthorizationServerMetadata( authorizationServer: string, options: IFetchAuthorizationServerMetadataOptions = {} -): Promise { +): Promise<{ metadata: IAuthorizationServerMetadata; discoveryUrl: string; errors: Error[] }> { const { additionalHeaders = {}, fetch: fetchImpl = fetch @@ -1301,7 +1301,7 @@ export async function fetchAuthorizationServerMetadata( const pathToFetch = new URL(AUTH_SERVER_METADATA_DISCOVERY_PATH, authorizationServer).toString() + extraPath; let metadata = await doFetch(pathToFetch); if (metadata) { - return metadata; + return { metadata, discoveryUrl: pathToFetch, errors }; } // Try fetching the OpenID Connect Discovery with path insertion. @@ -1310,7 +1310,7 @@ export async function fetchAuthorizationServerMetadata( const openidPathInsertionUrl = new URL(OPENID_CONNECT_DISCOVERY_PATH, authorizationServer).toString() + extraPath; metadata = await doFetch(openidPathInsertionUrl); if (metadata) { - return metadata; + return { metadata, discoveryUrl: openidPathInsertionUrl, errors }; } // Try fetching the other discovery URL. For the openid metadata discovery @@ -1321,7 +1321,7 @@ export async function fetchAuthorizationServerMetadata( : authorizationServer + OPENID_CONNECT_DISCOVERY_PATH; metadata = await doFetch(openidPathAdditionUrl); if (metadata) { - return metadata; + return { metadata, discoveryUrl: openidPathAdditionUrl, errors }; } // If we've tried all URLs and none worked, throw the error(s) diff --git a/src/vs/base/test/common/oauth.test.ts b/src/vs/base/test/common/oauth.test.ts index eac87812098..c554a8b7bdd 100644 --- a/src/vs/base/test/common/oauth.test.ts +++ b/src/vs/base/test/common/oauth.test.ts @@ -880,6 +880,8 @@ suite('OAuth', () => { ); assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); assert.strictEqual(fetchStub.firstCall.args[0], resourceMetadataUrl); assert.strictEqual(fetchStub.firstCall.args[1].method, 'GET'); @@ -903,12 +905,13 @@ suite('OAuth', () => { text: async () => JSON.stringify(expectedMetadata) }); - await fetchResourceMetadata( + const result = await fetchResourceMetadata( targetResource, resourceMetadataUrl, { fetch: fetchStub, sameOriginHeaders } ); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['Accept'], 'application/json'); assert.strictEqual(headers['X-Test-Header'], 'test-value'); @@ -931,12 +934,13 @@ suite('OAuth', () => { text: async () => JSON.stringify(expectedMetadata) }); - await fetchResourceMetadata( + const result = await fetchResourceMetadata( targetResource, resourceMetadataUrl, { fetch: fetchStub, sameOriginHeaders } ); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['Accept'], 'application/json'); assert.strictEqual(headers['X-Test-Header'], undefined); @@ -1024,6 +1028,7 @@ suite('OAuth', () => { // URL normalization should handle hostname case differences const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }); assert.deepStrictEqual(result.metadata, metadata); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); }); test('should throw error when response is not valid resource metadata', async () => { @@ -1116,6 +1121,7 @@ suite('OAuth', () => { const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl); assert.deepStrictEqual(result.metadata, metadata); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); assert.strictEqual(globalFetchStub.callCount, 1); }); @@ -1135,12 +1141,13 @@ suite('OAuth', () => { text: async () => JSON.stringify(metadata) }); - await fetchResourceMetadata( + const result = await fetchResourceMetadata( targetResource, resourceMetadataUrl, { fetch: fetchStub, sameOriginHeaders } ); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); // Different ports mean different origins const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['X-Test-Header'], undefined); @@ -1162,12 +1169,13 @@ suite('OAuth', () => { text: async () => JSON.stringify(metadata) }); - await fetchResourceMetadata( + const result = await fetchResourceMetadata( targetResource, resourceMetadataUrl, { fetch: fetchStub, sameOriginHeaders } ); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); // Different protocols mean different origins const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['X-Test-Header'], undefined); @@ -1219,6 +1227,7 @@ suite('OAuth', () => { ); assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource/api/v1'); assert.strictEqual(fetchStub.callCount, 1); // Should try path-appended version first assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1'); @@ -1251,6 +1260,8 @@ suite('OAuth', () => { ); assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource'); + assert.strictEqual(result.errors.length, 1); assert.strictEqual(fetchStub.callCount, 2); // First attempt with path assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1'); @@ -1299,6 +1310,7 @@ suite('OAuth', () => { ); assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource'); assert.strictEqual(fetchStub.callCount, 1); // Both URLs should be the same when path is / assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource'); @@ -1320,12 +1332,13 @@ suite('OAuth', () => { text: async () => JSON.stringify(expectedMetadata) }); - await fetchResourceMetadata( + const result = await fetchResourceMetadata( targetResource, undefined, { fetch: fetchStub, sameOriginHeaders } ); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource/api'); const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['Accept'], 'application/json'); assert.strictEqual(headers['X-Test-Header'], 'test-value'); @@ -1355,6 +1368,9 @@ suite('OAuth', () => { ); assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource'); + assert.strictEqual(result.errors.length, 1); + assert.ok(/Network connection failed/.test(result.errors[0].message)); assert.strictEqual(fetchStub.callCount, 2); // First attempt with path should have thrown error assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1'); @@ -1532,6 +1548,8 @@ suite('OAuth', () => { const result = await fetchResourceMetadata(targetResource, undefined, { fetch: fetchStub }); assert.deepStrictEqual(result.metadata, invalidMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource'); + assert.strictEqual(result.errors.length, 1); assert.strictEqual(fetchStub.callCount, 2); // Verify both URLs were tried assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1'); @@ -1560,6 +1578,7 @@ suite('OAuth', () => { ); assert.deepStrictEqual(result.metadata, validMetadata); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); assert.strictEqual(fetchStub.callCount, 1); assert.strictEqual(fetchStub.firstCall.args[0], resourceMetadataUrl); }); @@ -1583,6 +1602,8 @@ suite('OAuth', () => { // Should succeed on root discovery fallback const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }); assert.deepStrictEqual(result.metadata, invalidMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource'); + assert.ok(result.errors.length >= 1); // Should have tried explicit URL, path-appended, then succeeded on root assert.ok(fetchStub.callCount >= 2); }); @@ -1639,7 +1660,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); // Should try OAuth discovery with path insertion: https://auth.example.com/.well-known/oauth-authorization-server/tenant assert.strictEqual(fetchStub.firstCall.args[0], 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); @@ -1672,7 +1695,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/openid-configuration/tenant'); + assert.strictEqual(result.errors.length, 1); assert.strictEqual(fetchStub.callCount, 2); // First attempt: OAuth discovery assert.strictEqual(fetchStub.firstCall.args[0], 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); @@ -1713,7 +1738,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/tenant/.well-known/openid-configuration'); + assert.strictEqual(result.errors.length, 2); assert.strictEqual(fetchStub.callCount, 3); // First attempt: OAuth discovery assert.strictEqual(fetchStub.firstCall.args[0], 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); @@ -1741,7 +1768,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); // For root URLs, no extra path is added assert.strictEqual(fetchStub.firstCall.args[0], 'https://auth.example.com/.well-known/oauth-authorization-server'); @@ -1765,7 +1794,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server/tenant/'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); }); @@ -1787,8 +1818,9 @@ suite('OAuth', () => { statusText: 'OK' }); - await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub, additionalHeaders }); + const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub, additionalHeaders }); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['X-Custom-Header'], 'custom-value'); assert.strictEqual(headers['Authorization'], 'Bearer token123'); @@ -1847,7 +1879,8 @@ suite('OAuth', () => { // Should succeed on second attempt const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.errors.length, 1); assert.strictEqual(fetchStub.callCount, 2); }); @@ -1944,7 +1977,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(globalFetchStub.callCount, 1); }); @@ -1966,7 +2001,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.errors.length, 1); + assert.ok(/Network error/.test(result.errors[0].message)); // Should have tried two endpoints assert.strictEqual(fetchStub.callCount, 2); }); @@ -2022,7 +2059,8 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.errors.length, 2); // Should have tried all three endpoints assert.strictEqual(fetchStub.callCount, 3); }); @@ -2082,7 +2120,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/tenant/.well-known/openid-configuration'); + assert.strictEqual(result.errors.length, 2); assert.strictEqual(fetchStub.callCount, 3); // Third attempt should correctly handle trailing slash (not double-slash) assert.strictEqual(fetchStub.thirdCall.args[0], 'https://auth.example.com/tenant/.well-known/openid-configuration'); @@ -2104,7 +2144,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server/tenant/org/sub'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); // Should correctly insert well-known path with nested paths assert.strictEqual(fetchStub.firstCall.args[0], 'https://auth.example.com/.well-known/oauth-authorization-server/tenant/org/sub'); @@ -2162,7 +2204,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, validMetadata); + assert.deepStrictEqual(result.metadata, validMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); }); @@ -2182,7 +2226,10 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + // Query parameters are not included in the discovery URL (only pathname is extracted) + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); }); @@ -2200,8 +2247,9 @@ suite('OAuth', () => { statusText: 'OK' }); - await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub, additionalHeaders: {} }); + const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub, additionalHeaders: {} }); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server'); const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['Accept'], 'application/json'); }); diff --git a/src/vs/editor/browser/coreCommands.ts b/src/vs/editor/browser/coreCommands.ts index a1d6137f875..9331eea48ff 100644 --- a/src/vs/editor/browser/coreCommands.ts +++ b/src/vs/editor/browser/coreCommands.ts @@ -1358,11 +1358,13 @@ export namespace CoreNavigationCommands { if (args.revealCursor) { // must ensure cursor is in new visible range const desiredVisibleViewRange = viewModel.getCompletelyVisibleViewRangeAtScrollTop(desiredScrollTop); + const paddedRange = viewModel.getViewRangeWithCursorPadding(desiredVisibleViewRange); + viewModel.setCursorStates( source, CursorChangeReason.Explicit, [ - CursorMoveCommands.findPositionInViewportIfOutside(viewModel, viewModel.getPrimaryCursorState(), desiredVisibleViewRange, args.select) + CursorMoveCommands.findPositionInViewportIfOutside(viewModel, viewModel.getPrimaryCursorState(), paddedRange, args.select) ] ); } diff --git a/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts index 43dbd85c87a..91afd694b1f 100644 --- a/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, getActiveDocument } from '../../../../base/browser/dom.js'; +import { $, getActiveDocument, getActiveWindow } from '../../../../base/browser/dom.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import './media/decorationCssRuleExtractor.css'; @@ -81,4 +81,14 @@ export class DecorationCssRuleExtractor extends Disposable { return rules; } + + /** + * Resolves a CSS variable to its computed value using the container element. + */ + resolveCssVariable(canvas: HTMLCanvasElement, variableName: string): string { + canvas.appendChild(this._container); + const result = getActiveWindow().getComputedStyle(this._container).getPropertyValue(variableName).trim(); + canvas.removeChild(this._container); + return result; + } } diff --git a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts index 1b1c07df163..9a2eea56e1a 100644 --- a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts +++ b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -18,6 +18,18 @@ export interface IDecorationStyleSet { * A number between 0 and 1 representing the opacity of the text. */ opacity: number | undefined; + /** + * Whether the text should be rendered with a strikethrough. + */ + strikethrough: boolean | undefined; + /** + * The thickness of the strikethrough line in pixels (CSS pixels, not device pixels). + */ + strikethroughThickness: number | undefined; + /** + * A 32-bit number representing the strikethrough color. + */ + strikethroughColor: number | undefined; } export interface IDecorationStyleCacheEntry extends IDecorationStyleSet { @@ -32,29 +44,49 @@ export class DecorationStyleCache { private _nextId = 1; private readonly _cacheById = new Map(); - private readonly _cacheByStyle = new NKeyMap(); + private readonly _cacheByStyle = new NKeyMap(); getOrCreateEntry( color: number | undefined, bold: boolean | undefined, - opacity: number | undefined + opacity: number | undefined, + strikethrough: boolean | undefined, + strikethroughThickness: number | undefined, + strikethroughColor: number | undefined ): number { - if (color === undefined && bold === undefined && opacity === undefined) { + if (color === undefined && bold === undefined && opacity === undefined && strikethrough === undefined && strikethroughThickness === undefined && strikethroughColor === undefined) { return 0; } - const result = this._cacheByStyle.get(color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); + const result = this._cacheByStyle.get( + color ?? 0, + bold ? 1 : 0, + opacity === undefined ? '' : opacity.toFixed(2), + strikethrough ? 1 : 0, + strikethroughThickness === undefined ? '' : strikethroughThickness.toFixed(2), + strikethroughColor ?? 0 + ); if (result) { return result.id; } const id = this._nextId++; - const entry = { + const entry: IDecorationStyleCacheEntry = { id, color, bold, opacity, + strikethrough, + strikethroughThickness, + strikethroughColor, }; this._cacheById.set(id, entry); - this._cacheByStyle.set(entry, color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); + this._cacheByStyle.set(entry, + color ?? 0, + bold ? 1 : 0, + opacity === undefined ? '' : opacity.toFixed(2), + strikethrough ? 1 : 0, + strikethroughThickness === undefined ? '' : strikethroughThickness.toFixed(2), + strikethroughColor ?? 0 + ); return id; } diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index cc41c8dcaa4..7b74ec8830c 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -115,7 +115,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // The sub-pixel x offset is the fractional part of the x pixel coordinate of the cell, this // is used to improve the spacing between rendered characters. - const xSubPixelXOffset = (tokenMetadata & 0b1111) / 10; + const subPixelXOffset = (tokenMetadata & 0b1111) / 10; const bgId = TokenMetadata.getBackground(tokenMetadata); const bg = colorMap[bgId]; @@ -145,26 +145,54 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { fontSb.appendString(`${devicePixelFontSize}px ${this.fontFamily}`); this._ctx.font = fontSb.build(); - // TODO: Support FontStyle.Strikethrough and FontStyle.Underline text decorations, these - // need to be drawn manually to the canvas. See xterm.js for "dodging" the text for - // underlines. + // TODO: Support FontStyle.Underline text decorations, these need to be drawn manually to + // the canvas. See xterm.js for "dodging" the text for underlines. const originX = devicePixelFontSize; const originY = devicePixelFontSize; + + // Apply text color if (decorationStyleSet?.color !== undefined) { this._ctx.fillStyle = `#${decorationStyleSet.color.toString(16).padStart(8, '0')}`; } else { this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(tokenMetadata)]; } - this._ctx.textBaseline = 'top'; + // Apply opacity if (decorationStyleSet?.opacity !== undefined) { this._ctx.globalAlpha = decorationStyleSet.opacity; } - this._ctx.fillText(chars, originX + xSubPixelXOffset, originY); + // The glyph baseline is top, meaning it's drawn at the top-left of the + // cell. Add `TextMetrics.alphabeticBaseline` to the drawn position to + // get the alphabetic baseline. + this._ctx.textBaseline = 'top'; + + // Draw the text + this._ctx.fillText(chars, originX + subPixelXOffset, originY); + + // Draw strikethrough + if (decorationStyleSet?.strikethrough) { + // TODO: This position could be refined further by checking + // TextMetrics of lowercase letters. + // Position strikethrough at approximately the vertical center of + // lowercase letters. + const strikethroughY = Math.round(originY - this._textMetrics.alphabeticBaseline * 0.65); + const lineWidth = decorationStyleSet?.strikethroughThickness !== undefined + ? Math.round(decorationStyleSet.strikethroughThickness * this.devicePixelRatio) + : Math.max(1, Math.floor(devicePixelFontSize / 10)); + // Apply strikethrough color if specified + if (decorationStyleSet?.strikethroughColor !== undefined) { + this._ctx.fillStyle = `#${decorationStyleSet.strikethroughColor.toString(16).padStart(8, '0')}`; + } + // Intentionally do not apply the sub pixel x offset to + // strikethrough to ensure successive glyphs form a contiguous line. + this._ctx.fillRect(originX, strikethroughY - Math.floor(lineWidth / 2), Math.ceil(this._textMetrics.width), lineWidth); + } + this._ctx.restore(); + // Extract the image data and clear the background color const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); if (this._antiAliasing === 'subpixel') { const bgR = parseInt(bg.substring(1, 3), 16); @@ -173,7 +201,10 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._clearColor(imageData, bgR, bgG, bgB); this._ctx.putImageData(imageData, 0, 0); } + + // Find the bounding box this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); + // const offset = { // x: textMetrics.actualBoundingBoxLeft, // y: textMetrics.actualBoundingBoxAscent diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index 9588c78c49c..f0ae8bba97f 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -257,6 +257,9 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { let decorationStyleSetBold: boolean | undefined; let decorationStyleSetColor: number | undefined; let decorationStyleSetOpacity: number | undefined; + let decorationStyleSetStrikethrough: boolean | undefined; + let decorationStyleSetStrikethroughThickness: number | undefined; + let decorationStyleSetStrikethroughColor: number | undefined; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; @@ -375,6 +378,9 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { decorationStyleSetColor = undefined; decorationStyleSetBold = undefined; decorationStyleSetOpacity = undefined; + decorationStyleSetStrikethrough = undefined; + decorationStyleSetStrikethroughThickness = undefined; + decorationStyleSetStrikethroughColor = undefined; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -419,6 +425,36 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { decorationStyleSetOpacity = parsedValue; break; } + case 'text-decoration': + case 'text-decoration-line': { + if (value === 'line-through') { + decorationStyleSetStrikethrough = true; + } + break; + } + case 'text-decoration-thickness': { + const match = value.match(/^(\d+(?:\.\d+)?)px$/); + if (match) { + decorationStyleSetStrikethroughThickness = parseFloat(match[1]); + } + break; + } + case 'text-decoration-color': { + let colorValue = value; + const varMatch = value.match(/^var\((--[^,]+),\s*(?:initial|inherit)\)$/); + if (varMatch) { + colorValue = ViewGpuContext.decorationCssRuleExtractor.resolveCssVariable(this._viewGpuContext.canvas.domNode, varMatch[1]); + } + const parsedColor = Color.Format.CSS.parse(colorValue); + if (parsedColor) { + decorationStyleSetStrikethroughColor = parsedColor.toNumber32Bit(); + } + break; + } + case 'text-decoration-style': { + // These are validated in canRender and use default behavior + break; + } default: throw new BugIndicatingError('Unexpected inline decoration style'); } } @@ -443,7 +479,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { continue; } - const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness, decorationStyleSetStrikethroughColor); glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); absoluteOffsetY = Math.round( diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 9dbfa48a53f..446fef8cbbc 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -209,6 +209,9 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { let decorationStyleSetBold: boolean | undefined; let decorationStyleSetColor: number | undefined; let decorationStyleSetOpacity: number | undefined; + let decorationStyleSetStrikethrough: boolean | undefined; + let decorationStyleSetStrikethroughThickness: number | undefined; + let decorationStyleSetStrikethroughColor: number | undefined; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; @@ -278,6 +281,9 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { decorationStyleSetColor = undefined; decorationStyleSetBold = undefined; decorationStyleSetOpacity = undefined; + decorationStyleSetStrikethrough = undefined; + decorationStyleSetStrikethroughThickness = undefined; + decorationStyleSetStrikethroughColor = undefined; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -322,6 +328,36 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { decorationStyleSetOpacity = parsedValue; break; } + case 'text-decoration': + case 'text-decoration-line': { + if (value === 'line-through') { + decorationStyleSetStrikethrough = true; + } + break; + } + case 'text-decoration-thickness': { + const match = value.match(/^(\d+(?:\.\d+)?)px$/); + if (match) { + decorationStyleSetStrikethroughThickness = parseFloat(match[1]); + } + break; + } + case 'text-decoration-color': { + let colorValue = value; + const varMatch = value.match(/^var\((--[^,]+),\s*(?:initial|inherit)\)$/); + if (varMatch) { + colorValue = ViewGpuContext.decorationCssRuleExtractor.resolveCssVariable(this._viewGpuContext.canvas.domNode, varMatch[1]); + } + const parsedColor = Color.Format.CSS.parse(colorValue); + if (parsedColor) { + decorationStyleSetStrikethroughColor = parsedColor.toNumber32Bit(); + } + break; + } + case 'text-decoration-style': { + // These are validated in canRender and use default behavior + break; + } default: throw new BugIndicatingError('Unexpected inline decoration style'); } } @@ -346,7 +382,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { continue; } - const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness, decorationStyleSetStrikethroughColor); glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); absoluteOffsetY = Math.round( diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index bab5b2f9408..e3f24380846 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -6,6 +6,7 @@ import * as nls from '../../../nls.js'; import { addDisposableListener, getActiveWindow } from '../../../base/browser/dom.js'; import { createFastDomNode, type FastDomNode } from '../../../base/browser/fastDomNode.js'; +import { Color } from '../../../base/common/color.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; @@ -255,6 +256,11 @@ const gpuSupportedDecorationCssRules = [ 'color', 'font-weight', 'opacity', + 'text-decoration', + 'text-decoration-color', + 'text-decoration-line', + 'text-decoration-style', + 'text-decoration-thickness', ]; function supportsCssRule(rule: string, style: CSSStyleDeclaration) { @@ -263,6 +269,31 @@ function supportsCssRule(rule: string, style: CSSStyleDeclaration) { } // Check for values that aren't supported switch (rule) { + case 'text-decoration': + case 'text-decoration-line': { + const value = style.getPropertyValue(rule); + // Only line-through is supported currently + return value === 'line-through'; + } + case 'text-decoration-color': { + const value = style.getPropertyValue(rule); + // Support var(--something, initial/inherit) which falls back to currentcolor + if (/^var\(--[^,]+,\s*(?:initial|inherit)\)$/.test(value)) { + return true; + } + // Support parsed color values + return Color.Format.CSS.parse(value) !== null; + } + case 'text-decoration-style': { + const value = style.getPropertyValue(rule); + // Only 'initial' (solid) is supported + return value === 'initial'; + } + case 'text-decoration-thickness': { + const value = style.getPropertyValue(rule); + // Only pixel values and 'initial' are supported + return value === 'initial' || /^\d+(\.\d+)?px$/.test(value); + } default: return true; } } diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 646d41f0f98..4c1aeff7482 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -57,6 +57,7 @@ export interface IViewModel extends ICursorSimpleModel, ISimpleModel { getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): MinimapLinesRenderingData; getCompletelyVisibleViewRange(): Range; getCompletelyVisibleViewRangeAtScrollTop(scrollTop: number): Range; + getViewRangeWithCursorPadding(viewRange: Range): Range; getHiddenAreas(): Range[]; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 284bbc487a4..3fab2ddee2e 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -693,6 +693,32 @@ export class ViewModel extends Disposable implements IViewModel { ); } + /** + * Applies `cursorSurroundingLines` and `stickyScroll` padding to the given view range. + */ + public getViewRangeWithCursorPadding(viewRange: Range): Range { + const options = this._configuration.options; + const cursorSurroundingLines = options.get(EditorOption.cursorSurroundingLines); + const stickyScroll = options.get(EditorOption.stickyScroll); + + let { startLineNumber, endLineNumber } = viewRange; + const padding = Math.min( + Math.max(cursorSurroundingLines, stickyScroll.enabled ? stickyScroll.maxLineCount : 0), + Math.floor((endLineNumber - startLineNumber + 1) / 2)); + + startLineNumber += padding; + endLineNumber -= Math.max(0, padding - 1); + + if (padding === 0 || startLineNumber > endLineNumber) { + return viewRange; + } + + return new Range( + startLineNumber, this.getLineMinColumn(startLineNumber), + endLineNumber, this.getLineMaxColumn(endLineNumber) + ); + } + public saveState(): IViewState { const compatViewState = this.viewLayout.saveState(); diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index a5f6dc5023a..b775ea6cf10 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -260,6 +260,7 @@ export class FileService extends Disposable implements IFileService { size: stat.size, readonly: Boolean((stat.permissions ?? 0) & FilePermission.Readonly) || Boolean(provider.capabilities & FileSystemProviderCapabilities.Readonly), locked: Boolean((stat.permissions ?? 0) & FilePermission.Locked), + executable: Boolean((stat.permissions ?? 0) & FilePermission.Executable), etag: etag({ mtime: stat.mtime, size: stat.size }), children: undefined }; diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 57e5c964484..cb3b59e0ba9 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -473,7 +473,13 @@ export enum FilePermission { * to edit the contents and ask the user upon saving to * remove the lock. */ - Locked = 2 + Locked = 2, + + /** + * File is executable. Relevant for Unix-like systems where + * the executable bit determines if a file can be run. + */ + Executable = 4 } export interface IStat { @@ -1247,6 +1253,12 @@ export interface IBaseFileStat { * remove the lock. */ readonly locked?: boolean; + + /** + * File is executable. Relevant for Unix-like systems where + * the executable bit determines if a file can be run. + */ + readonly executable?: boolean; } export interface IBaseFileStatWithMetadata extends Required { } @@ -1287,6 +1299,7 @@ export interface IFileStatWithMetadata extends IFileStat, IBaseFileStatWithMetad readonly size: number; readonly readonly: boolean; readonly locked: boolean; + readonly executable: boolean; readonly children: IFileStatWithMetadata[] | undefined; } diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 3e550845b9c..f29c4742883 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Stats, promises } from 'fs'; +import { Stats, constants, promises } from 'fs'; import { Barrier, retry } from '../../../base/common/async.js'; import { ResourceMap } from '../../../base/common/map.js'; import { VSBuffer } from '../../../base/common/buffer.js'; @@ -73,12 +73,24 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple try { const { stat, symbolicLink } = await SymlinkSupport.stat(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly + let permissions: FilePermission | undefined = undefined; + if ((stat.mode & 0o200) === 0) { + permissions = FilePermission.Locked; + } + if ( + stat.mode & constants.S_IXUSR || + stat.mode & constants.S_IXGRP || + stat.mode & constants.S_IXOTH + ) { + permissions = (permissions ?? 0) | FilePermission.Executable; + } + return { type: this.toType(stat, symbolicLink), ctime: stat.birthtime.getTime(), // intentionally not using ctime here, we want the creation time mtime: stat.mtime.getTime(), size: stat.size, - permissions: (stat.mode & 0o200) === 0 ? FilePermission.Locked : undefined + permissions }; } catch (error) { throw this.toFileSystemProviderError(error); diff --git a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts index 17ba631493c..6f9622409e1 100644 --- a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts +++ b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts @@ -496,6 +496,21 @@ flakySuite('Disk File Service', function () { assert.ok(result.ctime > 0); }); + // The executable bit does not exist on Windows so use a condition not skip + if (!isWindows) { + test('stat - executable', async () => { + const nonExecutable = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/executable/non_executable'); + let resolved = await service.stat(nonExecutable); + assert.strictEqual(resolved.isFile, true); + assert.strictEqual(resolved.executable, false); + + const executable = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/executable/executable'); + resolved = await service.stat(executable); + assert.strictEqual(resolved.isFile, true); + assert.strictEqual(resolved.executable, true); + }); + } + test('deleteFile (non recursive)', async () => { return testDeleteFile(false, false); }); diff --git a/src/vs/platform/files/test/node/fixtures/executable/executable b/src/vs/platform/files/test/node/fixtures/executable/executable new file mode 100755 index 00000000000..e69de29bb2d diff --git a/src/vs/platform/files/test/node/fixtures/executable/non_executable b/src/vs/platform/files/test/node/fixtures/executable/non_executable new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/vs/platform/quickinput/browser/quickInputActions.ts b/src/vs/platform/quickinput/browser/quickInputActions.ts index ed9817fd70a..a536022fb22 100644 --- a/src/vs/platform/quickinput/browser/quickInputActions.ts +++ b/src/vs/platform/quickinput/browser/quickInputActions.ts @@ -260,6 +260,14 @@ registerQuickInputCommandAndKeybindingRule( registerQuickPickCommandAndKeybindingRule( { id: 'quickInput.toggleCheckbox', + when: ContextKeyExpr.and( + inQuickInputContext, + ContextKeyExpr.or( + ContextKeyExpr.equals(quickInputTypeContextKeyValue, QuickInputType.QuickPick), + ContextKeyExpr.equals(quickInputTypeContextKeyValue, QuickInputType.QuickTree), + ), + InputFocusedContext.negate() + ), primary: KeyCode.Space, handler: accessor => { const quickInputService = accessor.get(IQuickInputService); diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 03a6f35b428..f319304b434 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -167,7 +167,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess // Delay resizes to avoid conpty not respecting very early resize calls if (isWindows) { if (useConpty && cols === 0 && rows === 0 && this.shellLaunchConfig.executable?.endsWith('Git\\bin\\bash.exe')) { - this._delayedResizer = new DelayedResizer(); + this._delayedResizer = this._register(new DelayedResizer()); this._register(this._delayedResizer.onTrigger(dimensions => { this._delayedResizer?.dispose(); this._delayedResizer = undefined; @@ -177,11 +177,11 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess })); } // WindowsShellHelper is used to fetch the process title and shell type - this.onProcessReady(e => { + this._register(this.onProcessReady(e => { this._windowsShellHelper = this._register(new WindowsShellHelper(e.pid)); this._register(this._windowsShellHelper.onShellTypeChanged(e => this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: e }))); this._register(this._windowsShellHelper.onShellNameChanged(e => this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: e }))); - }); + })); } this._register(toDisposable(() => { if (this._titleInterval) { diff --git a/src/vs/workbench/api/browser/mainThreadMcp.ts b/src/vs/workbench/api/browser/mainThreadMcp.ts index dbf2b34e2e0..3a6dd7a9b4c 100644 --- a/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -16,6 +16,7 @@ import { ContextKeyExpr, IContextKeyService } from '../../../platform/contextkey import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { LogLevel } from '../../../platform/log/common/log.js'; +import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; import { IMcpMessageTransport, IMcpRegistry } from '../../contrib/mcp/common/mcpRegistryTypes.js'; import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js'; import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js'; @@ -28,7 +29,7 @@ import { ExtensionHostKind, extensionHostKindToString } from '../../services/ext import { IExtensionService } from '../../services/extensions/common/extensions.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { Proxied } from '../../services/extensions/common/proxyIdentifier.js'; -import { ExtHostContext, ExtHostMcpShape, IMcpAuthenticationDetails, IMcpAuthenticationOptions, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js'; +import { ExtHostContext, ExtHostMcpShape, IMcpAuthenticationDetails, IMcpAuthenticationOptions, IMcpAuthSetupTelemetry, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js'; @extHostNamedCustomer(MainContext.MainThreadMcp) export class MainThreadMcp extends Disposable implements MainThreadMcpShape { @@ -55,6 +56,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { @IDynamicAuthenticationProviderStorageService private readonly _dynamicAuthenticationProviderStorageService: IDynamicAuthenticationProviderStorageService, @IExtensionService private readonly _extensionService: IExtensionService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { super(); this._register(_authenticationService.onDidChangeSessions(e => this._onDidChangeAuthSessions(e.providerId, e.label))); @@ -355,6 +357,16 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { } } + $logMcpAuthSetup(data: IMcpAuthSetupTelemetry): void { + type McpAuthSetupClassification = { + owner: 'TylerLeonhardt'; + comment: 'Tracks how MCP OAuth authentication setup was discovered and configured'; + resourceMetadataSource: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How resource metadata was discovered (header, wellKnown, or none)' }; + serverMetadataSource: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How authorization server metadata was discovered (resourceMetadata, wellKnown, or default)' }; + }; + this._telemetryService.publicLog2('mcp/authSetup', data); + } + private async loginPrompt(mcpLabel: string, providerLabel: string, recreatingSession: boolean): Promise { const message = recreatingSession ? nls.localize('confirmRelogin', "The MCP Server Definition '{0}' wants you to authenticate to {1}.", mcpLabel, providerLabel) diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9927978d42f..4a46bbaa532 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3156,6 +3156,23 @@ export interface IMcpAuthenticationOptions { forceNewRegistration?: boolean; } +export const enum McpAuthResourceMetadataSource { + Header = 'header', + WellKnown = 'wellKnown', + None = 'none', +} + +export const enum McpAuthServerMetadataSource { + ResourceMetadata = 'resourceMetadata', + WellKnown = 'wellKnown', + Default = 'default', +} + +export interface IMcpAuthSetupTelemetry { + resourceMetadataSource: McpAuthResourceMetadataSource; + serverMetadataSource: McpAuthServerMetadataSource; +} + export interface MainThreadMcpShape { $onDidChangeState(id: number, state: McpConnectionState): void; $onDidPublishLog(id: number, level: LogLevel, log: string): void; @@ -3164,6 +3181,7 @@ export interface MainThreadMcpShape { $deleteMcpCollection(collectionId: string): void; $getTokenFromServerMetadata(id: number, authDetails: IMcpAuthenticationDetails, options?: IMcpAuthenticationOptions): Promise; $getTokenForProviderId(id: number, providerId: string, scopes: string[], options?: IMcpAuthenticationOptions): Promise; + $logMcpAuthSetup(data: IMcpAuthSetupTelemetry): void; } export interface MainThreadDataChannelsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index 1610473b34c..6a450d0c8c3 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -21,7 +21,7 @@ import { StorageScope } from '../../../platform/storage/common/storage.js'; import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerStaticMetadata, McpServerStaticToolAvailability, McpServerTransportHTTP, McpServerTransportType, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js'; import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; -import { ExtHostMcpShape, IMcpAuthenticationDetails, IStartMcpOptions, MainContext, MainThreadMcpShape } from './extHost.protocol.js'; +import { ExtHostMcpShape, IMcpAuthenticationDetails, IMcpAuthSetupTelemetry, IStartMcpOptions, MainContext, MainThreadMcpShape, McpAuthResourceMetadataSource, McpAuthServerMetadataSource } from './extHost.protocol.js'; import { IExtHostInitDataService } from './extHostInitDataService.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import * as Convert from './extHostTypeConverters.js'; @@ -701,6 +701,7 @@ export class McpHTTPHandle extends Disposable { fetch: (url, init) => this._fetch(url, init as MinimalRequestInit), log: (level, message) => this._log(level, message) }); + this._proxy.$logMcpAuthSetup(this._authMetadata.telemetry); await this._addAuthHeader(headers); if (headers['Authorization']) { // Update the headers in the init object @@ -722,7 +723,7 @@ export class McpHTTPHandle extends Disposable { // If we have an Authorization header and still get an auth error, we should retry with a new auth registration if (headers['Authorization'] && isAuthStatusCode(res.status)) { const errorText = await this._getErrText(res); - this._log(LogLevel.Debug, `Received ${res.status} status with Authorization header, retrying with new auth registration. Error details: ${errorText || 'no additional details'}`); + this._log(LogLevel.Info, `Received ${res.status} status with Authorization header, retrying with new auth registration. Error details: ${errorText || 'no additional details'}`); await this._addAuthHeader(headers, true); res = await doFetch(); } @@ -839,6 +840,8 @@ export interface IAuthMetadata { readonly serverMetadata: IAuthorizationServerMetadata; readonly resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined; readonly scopes: string[] | undefined; + /** Telemetry data about how auth metadata was discovered */ + readonly telemetry: IMcpAuthSetupTelemetry; /** * Updates the scopes based on the WWW-Authenticate header in the response. @@ -860,6 +863,7 @@ class AuthMetadata implements IAuthMetadata { public readonly serverMetadata: IAuthorizationServerMetadata, public readonly resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined, scopes: string[] | undefined, + public readonly telemetry: IMcpAuthSetupTelemetry, private readonly _log: AuthMetadataLogger, ) { this._scopes = scopes; @@ -872,7 +876,7 @@ class AuthMetadata implements IAuthMetadata { update(response: CommonResponse): boolean { const scopesChallenge = this._parseScopesFromResponse(response); if (!scopesMatch(scopesChallenge, this._scopes)) { - this._log(LogLevel.Debug, `Scopes changed from ${JSON.stringify(this._scopes)} to ${JSON.stringify(scopesChallenge)}, updating`); + this._log(LogLevel.Info, `Scopes changed from ${JSON.stringify(this._scopes)} to ${JSON.stringify(scopesChallenge)}, updating`); this._scopes = scopesChallenge; return true; } @@ -890,7 +894,7 @@ class AuthMetadata implements IAuthMetadata { if (challenge.scheme === 'Bearer' && challenge.params['scope']) { const scopes = challenge.params['scope'].split(AUTH_SCOPE_SEPARATOR).filter(s => s.trim().length); if (scopes.length) { - this._log(LogLevel.Debug, `Found scope challenge in WWW-Authenticate header: ${challenge.params['scope']}`); + this._log(LogLevel.Info, `Found scope challenge in WWW-Authenticate header: ${challenge.params['scope']}`); return scopes; } } @@ -932,6 +936,10 @@ export async function createAuthMetadata( ): Promise { const { launchHeaders, fetch, log } = options; + // Track discovery sources for telemetry + let resourceMetadataSource = McpAuthResourceMetadataSource.None; + let serverMetadataSource: McpAuthServerMetadataSource | undefined; + // Parse the WWW-Authenticate header for resource_metadata and scope challenges const { resourceMetadataChallenge, scopesChallenge: scopesChallengeFromHeader } = parseWWWAuthenticateHeaderForChallenges(originalResponse, log); @@ -941,7 +949,7 @@ export async function createAuthMetadata( let scopesChallenge = scopesChallengeFromHeader; try { - const { metadata, errors } = await fetchResourceMetadata(mcpUrl, resourceMetadataChallenge, { + const { metadata, discoveryUrl, errors } = await fetchResourceMetadata(mcpUrl, resourceMetadataChallenge, { sameOriginHeaders: { ...Object.fromEntries(launchHeaders), 'MCP-Protocol-Version': MCP.LATEST_PROTOCOL_VERSION @@ -951,10 +959,20 @@ export async function createAuthMetadata( for (const err of errors) { log(LogLevel.Warning, `Error fetching resource metadata: ${err}`); } + log(LogLevel.Info, `Discovered resource metadata at ${discoveryUrl}`); + + // Determine if resource metadata came from header or well-known + resourceMetadataSource = resourceMetadataChallenge ? McpAuthResourceMetadataSource.Header : McpAuthResourceMetadataSource.WellKnown; + // TODO:@TylerLeonhardt support multiple authorization servers // Consider using one that has an auth provider first, over the dynamic flow serverMetadataUrl = metadata.authorization_servers?.[0]; - log(LogLevel.Debug, `Using auth server metadata url: ${serverMetadataUrl}`); + if (!serverMetadataUrl) { + log(LogLevel.Warning, `No authorization_servers found in resource metadata ${discoveryUrl} - Is this resource metadata configured correctly?`); + } else { + log(LogLevel.Info, `Using auth server metadata url: ${serverMetadataUrl}`); + serverMetadataSource = McpAuthServerMetadataSource.ResourceMetadata; + } scopesChallenge ??= metadata.scopes_supported; resource = metadata; } catch (e) { @@ -977,16 +995,25 @@ export async function createAuthMetadata( try { log(LogLevel.Debug, `Fetching auth server metadata for: ${serverMetadataUrl} ...`); - const serverMetadataResponse = await fetchAuthorizationServerMetadata(serverMetadataUrl, { + const { metadata, discoveryUrl, errors } = await fetchAuthorizationServerMetadata(serverMetadataUrl, { additionalHeaders, fetch: (url, init) => fetch(url, init as MinimalRequestInit) }); - log(LogLevel.Info, 'Populated auth metadata'); + for (const err of errors) { + log(LogLevel.Warning, `Error fetching authorization server metadata: ${err}`); + } + log(LogLevel.Info, `Discovered authorization server metadata at ${discoveryUrl}`); + + // If serverMetadataSource is not yet defined, it means we fell back to baseUrl + // and successfully fetched from well-known + serverMetadataSource ??= McpAuthServerMetadataSource.WellKnown; + return new AuthMetadata( URI.parse(serverMetadataUrl), - serverMetadataResponse, + metadata, resource, scopesChallenge, + { resourceMetadataSource, serverMetadataSource }, log ); } catch (e) { @@ -1001,6 +1028,7 @@ export async function createAuthMetadata( defaultMetadata, resource, scopesChallenge, + { resourceMetadataSource, serverMetadataSource: McpAuthServerMetadataSource.Default }, log ); } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index e6ef77dbc0d..09904ec3d08 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -385,8 +385,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { size: stat.size, readonly: Boolean((stat.permissions ?? 0) & files.FilePermission.Readonly) || !this._extHostFileSystem.value.isWritableFileSystem(uri.scheme), locked: Boolean((stat.permissions ?? 0) & files.FilePermission.Locked), - etag: files.etag({ mtime: stat.mtime, size: stat.size }), - children: undefined + executable: Boolean((stat.permissions ?? 0) & files.FilePermission.Executable), + etag: files.etag({ mtime: stat.mtime, size: stat.size }) }; this.trace(`exit saveNotebook(versionId: ${versionId}, ${uri.toString()})`); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts index 40e3fae63c5..5785c66df3c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts @@ -8,6 +8,7 @@ import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions import { localize2 } from '../../../../../nls.js'; import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatService } from '../../common/chatService/chatService.js'; import { IChatWidgetService } from '../chat.js'; @@ -15,6 +16,7 @@ import { IChatWidgetService } from '../chat.js'; export function registerChatDeveloperActions() { registerAction2(LogChatInputHistoryAction); registerAction2(LogChatIndexAction); + registerAction2(InspectChatModelAction); } class LogChatInputHistoryAction extends Action2 { @@ -56,3 +58,57 @@ class LogChatIndexAction extends Action2 { chatService.logChatIndex(); } } + +class InspectChatModelAction extends Action2 { + static readonly ID = 'workbench.action.chat.inspectChatModel'; + + constructor() { + super({ + id: InspectChatModelAction.ID, + title: localize2('workbench.action.chat.inspectChatModel.label', "Inspect Chat Model"), + icon: Codicon.inspect, + category: Categories.Developer, + f1: true, + precondition: ChatContextKeys.enabled + }); + } + + override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const chatWidgetService = accessor.get(IChatWidgetService); + const editorService = accessor.get(IEditorService); + const widget = chatWidgetService.lastFocusedWidget; + + if (!widget?.viewModel) { + return; + } + + const model = widget.viewModel.model; + const modelData = model.toJSON(); + + // Build markdown output with latest response at the top + let output = '# Chat Model Inspection\n\n'; + + // Show latest response first if it exists + const requests = modelData.requests; + if (requests && requests.length > 0) { + const latestRequest = requests[requests.length - 1]; + if (latestRequest.response) { + output += '## Latest Response\n\n'; + output += '```json\n' + JSON.stringify(latestRequest.response, null, 2) + '\n```\n\n'; + } + } + + // Show full model data + output += '## Full Chat Model\n\n'; + output += '```json\n' + JSON.stringify(modelData, null, 2) + '\n```\n'; + + await editorService.openEditor({ + resource: undefined, + contents: output, + languageId: 'markdown', + options: { + pinned: true + } + }); + } +} 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 2f7bb4548be..5baf0428100 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts @@ -4,9 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../../base/common/codicons.js'; -import { localize2 } from '../../../../../nls.js'; +import { localize, localize2 } from '../../../../../nls.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'; +import { Extensions as QuickAccessExtensions, IQuickAccessRegistry } from '../../../../../platform/quickinput/common/quickAccess.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { AgentSessionsViewerOrientation, AgentSessionsViewerPosition } from './agentSessions.js'; import { IAgentSessionsService, AgentSessionsService } from './agentSessionsService.js'; @@ -14,6 +16,7 @@ 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, ToggleChatViewSessionsAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction } from './agentSessionsActions.js'; +import { AgentSessionsQuickAccessProvider, AGENT_SESSIONS_QUICK_ACCESS_PREFIX } from './agentSessionsQuickAccess.js'; //#region Actions and Menus @@ -145,6 +148,21 @@ MenuRegistry.appendMenuItem(MenuId.ChatViewSessionTitleToolbar, { //#endregion +//#region Quick Access + +Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ + ctor: AgentSessionsQuickAccessProvider, + prefix: AGENT_SESSIONS_QUICK_ACCESS_PREFIX, + contextKey: 'inAgentSessionsPicker', + placeholder: localize('agentSessionsQuickAccessPlaceholder', "Search agent sessions by name"), + helpEntries: [{ + description: localize('agentSessionsQuickAccessHelp', "Show All Agent Sessions"), + commandId: 'workbench.action.chat.history', + }] +}); + +//#endregion + //#region Workbench Contributions registerWorkbenchContribution2(LocalAgentsSessionsProvider.ID, LocalAgentsSessionsProvider, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts index 32a675f8fc5..cd91ba6fbdb 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts @@ -22,26 +22,46 @@ interface ISessionPickItem extends IQuickPickItem { readonly session: IAgentSession; } -const archiveButton: IQuickInputButton = { +export const archiveButton: IQuickInputButton = { iconClass: ThemeIcon.asClassName(Codicon.archive), tooltip: localize('archiveSession', "Archive") }; -const unarchiveButton: IQuickInputButton = { +export const unarchiveButton: IQuickInputButton = { iconClass: ThemeIcon.asClassName(Codicon.inbox), tooltip: localize('unarchiveSession', "Unarchive") }; -const renameButton: IQuickInputButton = { +export const renameButton: IQuickInputButton = { iconClass: ThemeIcon.asClassName(Codicon.edit), tooltip: localize('renameSession', "Rename") }; -const deleteButton: IQuickInputButton = { +export const deleteButton: IQuickInputButton = { iconClass: ThemeIcon.asClassName(Codicon.trash), tooltip: localize('deleteSession', "Delete") }; +export function getSessionDescription(session: IAgentSession): string { + const descriptionText = typeof session.description === 'string' ? session.description : session.description ? renderAsPlaintext(session.description) : undefined; + const timeAgo = fromNow(session.timing.endTime || session.timing.startTime); + const descriptionParts = [descriptionText, session.providerLabel, timeAgo].filter(part => !!part); + + return descriptionParts.join(' • '); +} + +export function getSessionButtons(session: IAgentSession): IQuickInputButton[] { + const buttons: IQuickInputButton[] = []; + + if (isLocalAgentSessionItem(session)) { + buttons.push(renameButton); + buttons.push(deleteButton); + } + buttons.push(session.isArchived() ? unarchiveButton : archiveButton); + + return buttons; +} + export class AgentSessionsPicker { private readonly sorter = new AgentSessionsSorter(); @@ -68,7 +88,7 @@ export class AgentSessionsPicker { sideBySide: e.inBackground, editorOptions: { preserveFocus: e.inBackground, - pinned: false + pinned: e.inBackground } }); } @@ -122,17 +142,8 @@ export class AgentSessionsPicker { } private toPickItem(session: IAgentSession): ISessionPickItem { - const descriptionText = typeof session.description === 'string' ? session.description : session.description ? renderAsPlaintext(session.description) : undefined; - const timeAgo = fromNow(session.timing.endTime || session.timing.startTime); - const descriptionParts = [descriptionText, session.providerLabel, timeAgo].filter(part => !!part); - const description = descriptionParts.join(' • '); - - const buttons: IQuickInputButton[] = []; - if (isLocalAgentSessionItem(session)) { - buttons.push(renameButton); - buttons.push(deleteButton); - } - buttons.push(session.isArchived() ? unarchiveButton : archiveButton); + const description = getSessionDescription(session); + const buttons = getSessionButtons(session); return { id: session.resource.toString(), diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsQuickAccess.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsQuickAccess.ts new file mode 100644 index 00000000000..9c33a0dc6ad --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsQuickAccess.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; +import { PickerQuickAccessProvider, IPickerQuickAccessItem, TriggerAction } from '../../../../../platform/quickinput/browser/pickerQuickAccess.js'; +import { localize } from '../../../../../nls.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IMatch, matchesFuzzy } from '../../../../../base/common/filters.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { IAgentSessionsService } from './agentSessionsService.js'; +import { AgentSessionsSorter, groupAgentSessions } from './agentSessionsViewer.js'; +import { IAgentSession } from './agentSessionsModel.js'; +import { openSession } from './agentSessionsOpener.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { AGENT_SESSION_DELETE_ACTION_ID, AGENT_SESSION_RENAME_ACTION_ID } from './agentSessions.js'; +import { archiveButton, deleteButton, getSessionButtons, getSessionDescription, renameButton, unarchiveButton } from './agentSessionsPicker.js'; + +export const AGENT_SESSIONS_QUICK_ACCESS_PREFIX = 'agent '; + +export class AgentSessionsQuickAccessProvider extends PickerQuickAccessProvider { + + private readonly sorter = new AgentSessionsSorter(); + + constructor( + @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService, + ) { + super(AGENT_SESSIONS_QUICK_ACCESS_PREFIX, { + canAcceptInBackground: true, + noResultsPick: { + label: localize('noAgentSessionResults', "No matching agent sessions") + } + }); + } + + protected async _getPicks(filter: string): Promise<(IQuickPickSeparator | IPickerQuickAccessItem)[]> { + const picks: Array = []; + + const sessions = this.agentSessionsService.model.sessions.sort(this.sorter.compare.bind(this.sorter)); + const groupedSessions = groupAgentSessions(sessions); + + for (const group of groupedSessions.values()) { + if (group.sessions.length > 0) { + picks.push({ type: 'separator', label: group.label }); + + for (const session of group.sessions) { + const highlights = matchesFuzzy(filter, session.label, true); + if (highlights) { + picks.push(this.toPickItem(session, highlights)); + } + } + } + } + + return picks; + } + + private toPickItem(session: IAgentSession, highlights: IMatch[]): IPickerQuickAccessItem { + const description = getSessionDescription(session); + const buttons = getSessionButtons(session); + + return { + label: session.label, + description, + highlights: { label: highlights }, + iconClass: ThemeIcon.asClassName(session.icon), + buttons, + trigger: async (buttonIndex) => { + const button = buttons[buttonIndex]; + switch (button) { + case renameButton: + await this.commandService.executeCommand(AGENT_SESSION_RENAME_ACTION_ID, session); + return TriggerAction.REFRESH_PICKER; + case deleteButton: + await this.commandService.executeCommand(AGENT_SESSION_DELETE_ACTION_ID, session); + return TriggerAction.REFRESH_PICKER; + case archiveButton: + case unarchiveButton: { + const newArchivedState = !session.isArchived(); + session.setArchived(newArchivedState); + return TriggerAction.REFRESH_PICKER; + } + default: + return TriggerAction.NO_ACTION; + } + }, + accept: (keyMods: IKeyMods, event: IQuickPickDidAcceptEvent) => { + this.instantiationService.invokeFunction(openSession, session, { + sideBySide: event.inBackground, + editorOptions: { + preserveFocus: event.inBackground, + pinned: event.inBackground + } + }); + } + }; + } +} 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 4d40891ff35..953318f44dc 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, clearNode } from '../../../../../../base/browser/dom.js'; +import { $, clearNode, hide } from '../../../../../../base/browser/dom.js'; import { IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js'; import { IChatContentPartRenderContext, IChatContentPart } from './chatContentParts.js'; import { IChatRendererContent } from '../../../common/model/chatViewModel.js'; @@ -94,8 +94,8 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen private toolInvocationCount: number = 0; private streamingCompleted: boolean = false; private isActive: boolean = true; - private currentToolCallLabel: string | undefined; private toolInvocations: (IChatToolInvocation | IChatToolInvocationSerialized)[] = []; + private singleToolItemInfo: { element: HTMLElement; originalParent: HTMLElement; originalNextSibling: Node | null } | undefined; constructor( content: IChatThinkingPart, @@ -356,11 +356,9 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen return; } - // case where we only have one dropdown in the thinking container and no thinking parts - if (this.toolInvocationCount === 1 && this.extractedTitles.length === 1 && this.currentToolCallLabel && this.currentThinkingValue.trim() === '') { - const title = this.currentToolCallLabel; - this.currentTitle = title; - this.setTitleWithWidgets(new MarkdownString(title), this.instantiationService, this.chatMarkdownAnchorService, this.chatContentMarkdownRenderer); + // case where we only have one tool in the thinking container and no thinking parts, we want to move it back to its original position + if (this.toolInvocationCount === 1 && this.currentThinkingValue.trim() === '' && this.singleToolItemInfo) { + this.restoreSingleToolToOriginalPosition(); return; } @@ -455,6 +453,23 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.setFallbackTitle(); } + private restoreSingleToolToOriginalPosition(): void { + if (!this.singleToolItemInfo) { + return; + } + + const { element, originalParent, originalNextSibling } = this.singleToolItemInfo; + + if (originalNextSibling && originalNextSibling.parentNode === originalParent) { + originalParent.insertBefore(element, originalNextSibling); + } else { + originalParent.appendChild(element); + } + + hide(this.domNode); + this.singleToolItemInfo = undefined; + } + private setFallbackTitle(): void { const finalLabel = this.toolInvocationCount > 0 ? localize('chat.thinking.finished.withTools', 'Finished thinking and invoked {0} tool{1}', this.toolInvocationCount, this.toolInvocationCount === 1 ? '' : 's') @@ -472,7 +487,18 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.updateDropdownClickability(); } - public appendItem(content: HTMLElement, toolInvocationId?: string, toolInvocation?: IChatToolInvocation | IChatToolInvocationSerialized): void { + public appendItem(content: HTMLElement, toolInvocationId?: string, toolInvocation?: IChatToolInvocation | IChatToolInvocationSerialized, originalParent?: HTMLElement): void { + // save the first tool item info for potential restoration later + if (this.toolInvocationCount === 0 && originalParent) { + this.singleToolItemInfo = { + element: content, + originalParent, + originalNextSibling: this.domNode + }; + } else { + this.singleToolItemInfo = undefined; + } + const itemWrapper = $('.chat-thinking-tool-wrapper'); const icon = toolInvocationId ? getToolInvocationIcon(toolInvocationId) : Codicon.tools; const iconElement = createThinkingIcon(icon); @@ -490,11 +516,6 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } else { toolCallLabel = `Invoked \`${toolInvocationId}\``; } - - if (toolInvocation?.pastTenseMessage) { - this.currentToolCallLabel = typeof toolInvocation.pastTenseMessage === 'string' ? toolInvocation.pastTenseMessage : toolInvocation.pastTenseMessage.value; - } - if (toolInvocation) { this.toolInvocations.push(toolInvocation); } 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 977c707d471..a09f4f28c9a 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 @@ -55,6 +55,7 @@ export interface ITerminalNewAutoApproveRule { approve: boolean; matchCommandLine?: boolean; }; + scope: 'session' | 'workspace' | 'user'; } export type TerminalNewAutoApproveButtonData = ( @@ -231,13 +232,13 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS const optedIn = await this._showAutoApproveWarning(); if (optedIn) { this.storageService.store(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, true, StorageScope.APPLICATION, StorageTarget.USER); - // This is good to auto approve immediately - if (!terminalCustomActions) { + // If this command would have been auto-approved, approve immediately + if (terminalData.autoApproveInfo) { toolConfirmKind = ToolConfirmKind.UserAction; } // If this would not have been auto approved, enable the options and // do not complete - else { + else if (terminalCustomActions) { for (const action of terminalCustomActions) { if (!(action instanceof Separator)) { action.disabled = false; @@ -258,28 +259,65 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS } case 'newRule': { const newRules = asArray(data.rule); - const inspect = this.configurationService.inspect(TerminalContribSettingId.AutoApprove); - const oldValue = (inspect.user?.value as Record | undefined) ?? {}; - let newValue: Record; - if (isObject(oldValue)) { - newValue = { ...oldValue }; - for (const newRule of newRules) { - newValue[newRule.key] = newRule.value; - } - } else { - this.preferencesService.openSettings({ - jsonEditor: true, - target: ConfigurationTarget.USER, - revealSetting: { - key: TerminalContribSettingId.AutoApprove - }, - }); - throw new ErrorNoTelemetry(`Cannot add new rule, existing setting is unexpected format`); + + // Group rules by scope + const sessionRules = newRules.filter(r => r.scope === 'session'); + const workspaceRules = newRules.filter(r => r.scope === 'workspace'); + const userRules = newRules.filter(r => r.scope === 'user'); + + // Handle session-scoped rules (temporary, in-memory only) + const chatSessionId = this.context.element.sessionId; + for (const rule of sessionRules) { + this.terminalChatService.addSessionAutoApproveRule(chatSessionId, rule.key, rule.value); } - await this.configurationService.updateValue(TerminalContribSettingId.AutoApprove, newValue, ConfigurationTarget.USER); - function formatRuleLinks(newRules: ITerminalNewAutoApproveRule[]): string { - return newRules.map(e => { - const settingsUri = createCommandUri(TerminalContribCommandId.OpenTerminalSettingsLink, ConfigurationTarget.USER); + + // Handle workspace-scoped rules + if (workspaceRules.length > 0) { + const inspect = this.configurationService.inspect(TerminalContribSettingId.AutoApprove); + const oldValue = (inspect.workspaceValue as Record | undefined) ?? {}; + if (isObject(oldValue)) { + const newValue: Record = { ...oldValue }; + for (const rule of workspaceRules) { + newValue[rule.key] = rule.value; + } + await this.configurationService.updateValue(TerminalContribSettingId.AutoApprove, newValue, ConfigurationTarget.WORKSPACE); + } else { + this.preferencesService.openSettings({ + jsonEditor: true, + target: ConfigurationTarget.WORKSPACE, + revealSetting: { key: TerminalContribSettingId.AutoApprove }, + }); + throw new ErrorNoTelemetry(`Cannot add new rule, existing workspace setting is unexpected format`); + } + } + + // Handle user-scoped rules + if (userRules.length > 0) { + const inspect = this.configurationService.inspect(TerminalContribSettingId.AutoApprove); + const oldValue = (inspect.userValue as Record | undefined) ?? {}; + if (isObject(oldValue)) { + const newValue: Record = { ...oldValue }; + for (const rule of userRules) { + newValue[rule.key] = rule.value; + } + await this.configurationService.updateValue(TerminalContribSettingId.AutoApprove, newValue, ConfigurationTarget.USER); + } else { + this.preferencesService.openSettings({ + jsonEditor: true, + target: ConfigurationTarget.USER, + revealSetting: { key: TerminalContribSettingId.AutoApprove }, + }); + throw new ErrorNoTelemetry(`Cannot add new rule, existing setting is unexpected format`); + } + } + + function formatRuleLinks(rules: ITerminalNewAutoApproveRule[], scope: 'session' | 'workspace' | 'user'): string { + return rules.map(e => { + if (scope === 'session') { + return `\`${e.key}\``; + } + const target = scope === 'workspace' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER; + const settingsUri = createCommandUri(TerminalContribCommandId.OpenTerminalSettingsLink, target); return `[\`${e.key}\`](${settingsUri.toString()} "${localize('ruleTooltip', 'View rule in settings')}")`; }).join(', '); } @@ -288,10 +326,24 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS enabledCommands: [TerminalContribCommandId.OpenTerminalSettingsLink] } }; - if (newRules.length === 1) { - terminalData.autoApproveInfo = new MarkdownString(localize('newRule', 'Auto approve rule {0} added', formatRuleLinks(newRules)), mdTrustSettings); - } else if (newRules.length > 1) { - terminalData.autoApproveInfo = new MarkdownString(localize('newRule.plural', 'Auto approve rules {0} added', formatRuleLinks(newRules)), mdTrustSettings); + const parts: string[] = []; + if (sessionRules.length > 0) { + parts.push(sessionRules.length === 1 + ? localize('newRule.session', 'Session rule {0} added', formatRuleLinks(sessionRules, 'session')) + : localize('newRule.session.plural', 'Session rules {0} added', formatRuleLinks(sessionRules, 'session'))); + } + if (workspaceRules.length > 0) { + parts.push(workspaceRules.length === 1 + ? localize('newRule.workspace', 'Workspace rule {0} added', formatRuleLinks(workspaceRules, 'workspace')) + : localize('newRule.workspace.plural', 'Workspace rules {0} added', formatRuleLinks(workspaceRules, 'workspace'))); + } + if (userRules.length > 0) { + parts.push(userRules.length === 1 + ? localize('newRule.user', 'User rule {0} added', formatRuleLinks(userRules, 'user')) + : localize('newRule.user.plural', 'User rules {0} added', formatRuleLinks(userRules, 'user'))); + } + if (parts.length > 0) { + terminalData.autoApproveInfo = new MarkdownString(parts.join(', '), mdTrustSettings); } toolConfirmKind = ToolConfirmKind.UserAction; break; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 10343133fd5..89820667cf5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1555,7 +1555,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { this.updateItemHeight(templateData); @@ -1567,7 +1567,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('chat.editRequests') === 'input'; if (!isInput) { - this.inputPart.setChatMode(this.input.currentModeKind); + this.inputPart.setChatMode(this.input.currentModeObs.get().id); const currentModel = this.input.selectedLanguageModel; if (currentModel) { this.inputPart.switchModel(currentModel.metadata); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts index 82725f4a25b..2deb9859f16 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts @@ -231,6 +231,10 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService this._widgets.push(newWidget); this._onDidAddWidget.fire(newWidget); + if (!this._lastFocusedWidget) { + this.setLastFocusedWidget(newWidget); + } + return combinedDisposable( newWidget.onDidFocus(() => this.setLastFocusedWidget(newWidget)), newWidget.onDidChangeViewModel(({ previousSessionResource, currentSessionResource }) => { @@ -245,7 +249,12 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService } }); }), - toDisposable(() => this._widgets.splice(this._widgets.indexOf(newWidget), 1)) + toDisposable(() => { + this._widgets.splice(this._widgets.indexOf(newWidget), 1); + if (this._lastFocusedWidget === newWidget) { + this.setLastFocusedWidget(undefined); + } + }) ); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts index 8efd46da9e1..4f73ea56374 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts @@ -38,7 +38,7 @@ import { INotificationService } from '../../../../../../../platform/notification import { Registry } from '../../../../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../../../common/contributions.js'; -import { EditorsOrder } from '../../../../../../common/editor.js'; +import { EditorsOrder, isDiffEditorInput } from '../../../../../../common/editor.js'; import { IEditorService } from '../../../../../../services/editor/common/editorService.js'; import { IHistoryService } from '../../../../../../services/history/common/history.js'; import { LifecyclePhase } from '../../../../../../services/lifecycle/common/lifecycle.js'; @@ -974,21 +974,22 @@ class BuiltinDynamicCompletions extends Disposable { // HISTORY // always take the last N items for (const [i, item] of this.historyService.getHistory().entries()) { - if (!item.resource || seen.has(item.resource) || !this.instantiationService.invokeFunction(accessor => isSupportedChatFileScheme(accessor, item.resource!.scheme))) { + const resource = isDiffEditorInput(item) ? item.modified.resource : item.resource; + if (!resource || seen.has(resource) || !this.instantiationService.invokeFunction(accessor => isSupportedChatFileScheme(accessor, resource.scheme))) { // ignore editors without a resource continue; } if (pattern) { // use pattern if available - const basename = this.labelService.getUriBasenameLabel(item.resource).toLowerCase(); + const basename = this.labelService.getUriBasenameLabel(resource).toLowerCase(); if (!isPatternInWord(pattern, 0, pattern.length, basename, 0, basename.length)) { continue; } } - seen.add(item.resource); - const newLen = result.suggestions.push(makeCompletionItem(item.resource, FileKind.FILE, i === 0 ? localize('activeFile', 'Active file') : undefined, i === 0)); + seen.add(resource); + const newLen = result.suggestions.push(makeCompletionItem(resource, FileKind.FILE, i === 0 ? localize('activeFile', 'Active file') : undefined, i === 0)); if (newLen - len >= 5) { break; } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts index 86799310b0e..708621ded2b 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts @@ -5,7 +5,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Disposable, MutableDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../../base/common/network.js'; import { isEqual } from '../../../../../../base/common/resources.js'; import { truncate } from '../../../../../../base/common/strings.js'; @@ -17,27 +17,21 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ import { registerIcon } from '../../../../../../platform/theme/common/iconRegistry.js'; import { EditorInputCapabilities, IEditorIdentifier, IEditorSerializer, IUntypedEditorInput, Verbosity } from '../../../../../common/editor.js'; import { EditorInput, IEditorCloseHandler } from '../../../../../common/editor/editorInput.js'; -import { IChatEditingSession, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; -import { IChatModel } from '../../../common/model/chatModel.js'; import { IChatModelReference, IChatService } from '../../../common/chatService/chatService.js'; import { IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; -import { LocalChatSessionUri, getChatSessionType } from '../../../common/model/chatUri.js'; import { ChatAgentLocation, ChatEditorTitleMaxLength } from '../../../common/constants.js'; +import { IChatEditingSession, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; +import { IChatModel } from '../../../common/model/chatModel.js'; +import { LocalChatSessionUri, getChatSessionType } from '../../../common/model/chatUri.js'; import { IClearEditingSessionConfirmationOptions } from '../../actions/chatActions.js'; import type { IChatEditorOptions } from './chatEditor.js'; const ChatEditorIcon = registerIcon('chat-editor-label-icon', Codicon.chatSparkle, nls.localize('chatEditorLabelIcon', 'Icon of the chat editor label.')); export class ChatEditorInput extends EditorInput implements IEditorCloseHandler { - /** Maps input name strings to sets of active editor counts */ - static readonly countsInUseMap = new Map>(); - static readonly TypeID: string = 'workbench.input.chatSession'; static readonly EditorID: string = 'workbench.editor.chatSession'; - private readonly inputCount: number; - private readonly inputName: string; - private _sessionResource: URI | undefined; /** @@ -47,7 +41,6 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler */ public get sessionResource(): URI | undefined { return this._sessionResource; } - private hasCustomTitle: boolean = false; private didTransferOutEditingSession = false; private cachedIcon: ThemeIcon | URI | undefined; @@ -61,15 +54,6 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler return ChatEditorUri.getNewEditorUri(); } - private static getNextCount(inputName: string): number { - let count = 0; - while (ChatEditorInput.countsInUseMap.get(inputName)?.has(count)) { - count++; - } - - return count; - } - constructor( readonly resource: URI, readonly options: IChatEditorOptions, @@ -93,36 +77,6 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler } else { this._sessionResource = resource; } - - // Check if we already have a custom title for this session - const hasExistingCustomTitle = this._sessionResource && ( - this.chatService.getSessionTitle(this._sessionResource)?.trim() - ); - - this.hasCustomTitle = Boolean(hasExistingCustomTitle); - - // Input counts are unique to the displayed fallback title - this.inputName = options.title?.fallback ?? ''; - if (!ChatEditorInput.countsInUseMap.has(this.inputName)) { - ChatEditorInput.countsInUseMap.set(this.inputName, new Set()); - } - - // Only allocate a count if we don't already have a custom title - if (!this.hasCustomTitle) { - this.inputCount = ChatEditorInput.getNextCount(this.inputName); - ChatEditorInput.countsInUseMap.get(this.inputName)?.add(this.inputCount); - this._register(toDisposable(() => { - // Only remove if we haven't already removed it due to custom title - if (!this.hasCustomTitle) { - ChatEditorInput.countsInUseMap.get(this.inputName)?.delete(this.inputCount); - if (ChatEditorInput.countsInUseMap.get(this.inputName)?.size === 0) { - ChatEditorInput.countsInUseMap.delete(this.inputName); - } - } - })); - } else { - this.inputCount = 0; // Not used when we have a custom title - } } override closeHandler = this; @@ -195,9 +149,7 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler } // Fall back to default naming pattern - const inputCountSuffix = (this.inputCount > 0 ? ` ${this.inputCount + 1}` : ''); - const defaultName = this.options.title?.fallback ?? nls.localize('chatEditorName', "Chat"); - return defaultName + inputCountSuffix; + return this.options.title?.fallback ?? nls.localize('chatEditorName', "Chat"); } override getTitle(verbosity?: Verbosity): string { @@ -277,14 +229,6 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler this._sessionResource = this.model.sessionResource; this._register(this.model.onDidChange((e) => { - // When a custom title is set, we no longer need the numeric count - if (e && e.kind === 'setCustomTitle' && !this.hasCustomTitle) { - this.hasCustomTitle = true; - ChatEditorInput.countsInUseMap.get(this.inputName)?.delete(this.inputCount); - if (ChatEditorInput.countsInUseMap.get(this.inputName)?.size === 0) { - ChatEditorInput.countsInUseMap.delete(this.inputName); - } - } // Invalidate icon cache when label changes this.cachedIcon = undefined; this._onDidChangeLabel.fire(); 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 9a93089dcce..efcb9f37fe1 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 @@ -56,7 +56,8 @@ class ExtendedTestFileService extends TestFileService { mtime: 0, ctime: 0, readonly: false, - locked: false + locked: false, + executable: false }; } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index e9977c33a87..d854fcdb68c 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -3070,6 +3070,8 @@ class SCMTreeDataSource extends Disposable implements IAsyncDataSource r.provider === element.provider); if (!repository) { @@ -3077,6 +3079,8 @@ class SCMTreeDataSource extends Disposable implements IAsyncDataSource>; } /** diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index b3d32492841..1e6a52f9e04 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -592,13 +592,12 @@ export class TerminalTabbedView extends Disposable { // be focused. So wait for connection to finish, then focus. const previousActiveElement = this._tabListElement.ownerDocument.activeElement; if (previousActiveElement) { - // TODO: Improve lifecycle management this event should be disposed after first fire - this._register(this._terminalService.onDidChangeConnectionState(() => { + const listener = this._register(Event.once(this._terminalService.onDidChangeConnectionState)(() => { // Only focus the terminal if the activeElement has not changed since focus() was called - // TODO: Hack if (dom.isActiveElement(previousActiveElement)) { this._focus(); } + this._store.delete(listener); })); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 628e1313ba5..9a745727415 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -337,13 +337,12 @@ export class TerminalViewPane extends ViewPane { // be focused. So wait for connection to finish, then focus. const previousActiveElement = this.element.ownerDocument.activeElement; if (previousActiveElement) { - // TODO: Improve lifecycle management this event should be disposed after first fire - this._register(this._terminalService.onDidChangeConnectionState(() => { + const listener = this._register(Event.once(this._terminalService.onDidChangeConnectionState)(() => { // Only focus the terminal if the activeElement has not changed since focus() was called - // TODO: Hack if (previousActiveElement && dom.isActiveElement(previousActiveElement)) { this._terminalGroupService.showPanel(true); } + this._store.delete(listener); })); } } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 9c6fc347a7c..d1c470cb600 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -803,6 +803,10 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach if (!this.raw.element || this._webglAddon && this._webglAddonCustomGlyphs === this._terminalConfigurationService.config.customGlyphs) { return; } + + // Dispose of existing addon before creating a new one to avoid leaking WebGL contexts + this._disposeOfWebglRenderer(); + this._webglAddonCustomGlyphs = this._terminalConfigurationService.config.customGlyphs; const Addon = await this._xtermAddonLoader.importAddon('webgl'); @@ -892,6 +896,9 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach } private _disposeOfWebglRenderer(): void { + if (!this._webglAddon) { + return; + } try { this._webglAddon?.dispose(); } catch { @@ -1001,6 +1008,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach override dispose(): void { this._anyTerminalFocusContextKey.reset(); this._anyFocusedTerminalHasSelection.reset(); + this._disposeOfWebglRenderer(); this._onDidDispose.fire(); super.dispose(); } diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index 8ec742e124f..87e3a63fe0a 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -62,6 +62,12 @@ if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then builtin return fi +# Prevent AI-executed commands from polluting shell history +if [ "${VSCODE_PREVENT_SHELL_HISTORY:-}" = "1" ]; then + export HISTCONTROL="ignorespace" + builtin unset VSCODE_PREVENT_SHELL_HISTORY +fi + # Apply EnvironmentVariableCollections if needed if [ -n "${VSCODE_ENV_REPLACE:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_REPLACE" diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index f32b74f1927..5389bd95b12 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -98,6 +98,12 @@ if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then builtin return fi +# Prevent AI-executed commands from polluting shell history +if [ "${VSCODE_PREVENT_SHELL_HISTORY:-}" = "1" ]; then + builtin setopt HIST_IGNORE_SPACE + builtin unset VSCODE_PREVENT_SHELL_HISTORY +fi + # The property (P) and command (E) codes embed values which require escaping. # Backslashes are doubled. Non-alphanumeric characters are converted to escaped hex. __vsc_escape_value() { diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish index 51e0d3a7d6e..0e0b6798c16 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish @@ -23,6 +23,13 @@ or exit set --global VSCODE_SHELL_INTEGRATION 1 set --global __vscode_shell_env_reporting $VSCODE_SHELL_ENV_REPORTING set -e VSCODE_SHELL_ENV_REPORTING + +# Prevent AI-executed commands from polluting shell history +if test "$VSCODE_PREVENT_SHELL_HISTORY" = "1" + set -g fish_private_mode 1 + set -e VSCODE_PREVENT_SHELL_HISTORY +end + set -g envVarsToReport if test -n "$__vscode_shell_env_reporting" set envVarsToReport (string split "," "$__vscode_shell_env_reporting") diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index 3c37defc7b2..e89f6ec24c4 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -259,4 +259,13 @@ function Set-MappedKeyHandlers { if ($Global:__VSCodeState.HasPSReadLine) { Set-MappedKeyHandlers + + # Prevent AI-executed commands from polluting shell history + if ($env:VSCODE_PREVENT_SHELL_HISTORY -eq "1") { + Set-PSReadLineOption -AddToHistoryHandler { + param([string]$line) + return $false + } + $env:VSCODE_PREVENT_SHELL_HISTORY = $null + } } diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 6d9963d17c3..82345c4b09c 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -572,8 +572,10 @@ const terminalConfiguration: IStringDictionary = { [TerminalSettingId.CustomGlyphs]: { markdownDescription: localize('terminal.integrated.customGlyphs', "Whether to draw custom glyphs instead of using the font for the following unicode ranges:\n\n{0}\n\nThis will typically result in better rendering with continuous lines, even when line height and letter spacing is used. This feature only works when {1} is enabled.", [ '- Box Drawing (U+2500-U+257F)', + '- Block Elements (U+2580-U+259F)', '- Braille Patterns (U+2800-U+28FF)', '- Powerline Symbols (U+E0A0-U+E0D4, Private Use Area)', + '- Progress Indicators (U+EE00-U+EE0B, Private Use Area)', '- Git Branch Symbols (U+F5D0-U+F60D, Private Use Area)', '- Symbols for Legacy Computing (U+1FB00-U+1FBFF)' ].join('\n'), `\`#${TerminalSettingId.GpuAcceleration}#\``), diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index bf5b51e3d49..7144fed24e3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -31,6 +31,7 @@ .monaco-workbench .terminal-inline-chat .interactive-session { margin: initial; + max-width: unset; } .monaco-workbench .terminal-inline-chat.hide { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts index 653772adfc3..9d9fa542fc4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts @@ -53,6 +53,13 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ */ private readonly _sessionAutoApprovalEnabled = new Set(); + /** + * Tracks session-scoped auto-approve rules per chat session. These are temporary rules that + * last only for the duration of the chat session (not persisted to disk). + * Map> + */ + private readonly _sessionAutoApproveRules = new Map>(); + constructor( @ILogService private readonly _logService: ILogService, @ITerminalService private readonly _terminalService: ITerminalService, @@ -66,6 +73,16 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ this._hasHiddenToolTerminalContext = TerminalChatContextKeys.hasHiddenChatTerminals.bindTo(this._contextKeyService); this._restoreFromStorage(); + + // Clear session auto-approve rules when chat sessions end + this._register(this._chatService.onDidDisposeSession(e => { + for (const resource of e.sessionResource) { + const sessionId = LocalChatSessionUri.parseLocalSessionId(resource); + if (sessionId) { + this._sessionAutoApproveRules.delete(sessionId); + } + } + })); } registerTerminalInstanceWithToolSession(terminalToolSessionId: string | undefined, instance: ITerminalInstance): void { @@ -313,4 +330,17 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ hasChatSessionAutoApproval(chatSessionId: string): boolean { return this._sessionAutoApprovalEnabled.has(chatSessionId); } + + addSessionAutoApproveRule(chatSessionId: string, key: string, value: boolean | { approve: boolean; matchCommandLine?: boolean }): void { + let sessionRules = this._sessionAutoApproveRules.get(chatSessionId); + if (!sessionRules) { + sessionRules = {}; + this._sessionAutoApproveRules.set(chatSessionId, sessionRules); + } + sessionRules[key] = value; + } + + getSessionAutoApproveRules(chatSessionId: string): Readonly> { + return this._sessionAutoApproveRules.get(chatSessionId) ?? {}; + } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index fbbc47ec2b7..81acecc6588 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -11,7 +11,8 @@ import { escapeRegExpCharacters, removeAnsiEscapeCodes } from '../../../../../ba import { localize } from '../../../../../nls.js'; import type { TerminalNewAutoApproveButtonData } from '../../../chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; import type { ToolConfirmationAction } from '../../../chat/common/tools/languageModelToolsService.js'; -import type { ICommandApprovalResultWithReason } from './commandLineAutoApprover.js'; +import type { ICommandApprovalResultWithReason } from './tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.js'; +import { isAutoApproveRule } from './tools/commandLineAnalyzer/commandLineAnalyzer.js'; export function isPowerShell(envShell: string, os: OperatingSystem): boolean { if (os === OperatingSystem.Windows) { @@ -32,6 +33,13 @@ export function isZsh(envShell: string, os: OperatingSystem): boolean { return /^zsh$/.test(pathPosix.basename(envShell)); } +export function isBash(envShell: string, os: OperatingSystem): boolean { + if (os === OperatingSystem.Windows) { + return /^bash(?:\.exe)?$/i.test(pathWin32.basename(envShell)); + } + return /^bash$/.test(pathPosix.basename(envShell)); +} + export function isFish(envShell: string, os: OperatingSystem): boolean { if (os === OperatingSystem.Windows) { return /^fish(?:\.exe)?$/i.test(pathWin32.basename(envShell)); @@ -105,27 +113,50 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str // instead of `foo`) const commandsWithSubSubCommands = new Set(['npm run', 'yarn run']); + // Helper function to find the first non-flag argument after a given index + const findNextNonFlagArg = (parts: string[], startIndex: number): number | undefined => { + for (let i = startIndex; i < parts.length; i++) { + if (!parts[i].startsWith('-')) { + return i; + } + } + return undefined; + }; + // For each unapproved sub-command (within the overall command line), decide whether to // suggest new rules for the command, a sub-command, a sub-command of a sub-command or to // not suggest at all. + // + // This includes support for detecting flags between the commands, so `mvn -DskipIT test a` + // would suggest `mvn -DskipIT test` as that's more useful than only suggesting the exact + // command line. const subCommandsToSuggest = Array.from(new Set(coalesce(unapprovedSubCommands.map(command => { const parts = command.trim().split(/\s+/); const baseCommand = parts[0].toLowerCase(); - const baseSubCommand = parts.length > 1 ? `${parts[0]} ${parts[1]}`.toLowerCase() : ''; // Security check: Never suggest auto-approval for dangerous interpreter commands if (neverAutoApproveCommands.has(baseCommand)) { return undefined; } - if (commandsWithSubSubCommands.has(baseSubCommand)) { - if (parts.length >= 3 && !parts[2].startsWith('-')) { - return `${parts[0]} ${parts[1]} ${parts[2]}`; - } - return undefined; - } else if (commandsWithSubcommands.has(baseCommand)) { - if (parts.length >= 2 && !parts[1].startsWith('-')) { - return `${parts[0]} ${parts[1]}`; + if (commandsWithSubcommands.has(baseCommand)) { + // Find the first non-flag argument after the command + const subCommandIndex = findNextNonFlagArg(parts, 1); + if (subCommandIndex !== undefined) { + // Check if this is a sub-sub-command case + const baseSubCommand = `${parts[0]} ${parts[subCommandIndex]}`.toLowerCase(); + if (commandsWithSubSubCommands.has(baseSubCommand)) { + // Look for the second non-flag argument after the first subcommand + const subSubCommandIndex = findNextNonFlagArg(parts, subCommandIndex + 1); + if (subSubCommandIndex !== undefined) { + // Include everything from command to sub-sub-command (including flags) + return parts.slice(0, subSubCommandIndex + 1).join(' '); + } + return undefined; + } else { + // Include everything from command to subcommand (including flags) + return parts.slice(0, subCommandIndex + 1).join(' '); + } } return undefined; } else { @@ -136,22 +167,48 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str if (subCommandsToSuggest.length > 0) { let subCommandLabel: string; if (subCommandsToSuggest.length === 1) { - subCommandLabel = localize('autoApprove.baseCommandSingle', 'Always Allow Command: {0}', subCommandsToSuggest[0]); + subCommandLabel = `\`${subCommandsToSuggest[0]} \u2026\``; } else { - const commandSeparated = subCommandsToSuggest.join(', '); - subCommandLabel = localize('autoApprove.baseCommand', 'Always Allow Commands: {0}', commandSeparated); + subCommandLabel = `Commands ${subCommandsToSuggest.map(e => `\`${e} \u2026\``).join(', ')}`; } actions.push({ - label: subCommandLabel, + label: `Allow ${subCommandLabel} in this Session`, data: { type: 'newRule', rule: subCommandsToSuggest.map(key => ({ key, - value: true + value: true, + scope: 'session' })) } satisfies TerminalNewAutoApproveButtonData }); + actions.push({ + label: `Allow ${subCommandLabel} in this Workspace`, + data: { + type: 'newRule', + rule: subCommandsToSuggest.map(key => ({ + key, + value: true, + scope: 'workspace' + })) + } satisfies TerminalNewAutoApproveButtonData + }); + actions.push({ + label: `Always Allow ${subCommandLabel}`, + data: { + type: 'newRule', + rule: subCommandsToSuggest.map(key => ({ + key, + value: true, + scope: 'user' + })) + } satisfies TerminalNewAutoApproveButtonData + }); + } + + if (actions.length > 0) { + actions.push(new Separator()); } // Allow exact command line, don't do this if it's just the first sub-command's first @@ -162,6 +219,34 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str !commandsWithSubcommands.has(commandLine) && !commandsWithSubSubCommands.has(commandLine) ) { + actions.push({ + label: localize('autoApprove.exactCommand1', 'Allow Exact Command Line in this Session'), + data: { + type: 'newRule', + rule: { + key: `/^${escapeRegExpCharacters(commandLine)}$/`, + value: { + approve: true, + matchCommandLine: true + }, + scope: 'session' + } + } satisfies TerminalNewAutoApproveButtonData + }); + actions.push({ + label: localize('autoApprove.exactCommand2', 'Allow Exact Command Line in this Workspace'), + data: { + type: 'newRule', + rule: { + key: `/^${escapeRegExpCharacters(commandLine)}$/`, + value: { + approve: true, + matchCommandLine: true + }, + scope: 'workspace' + } + } satisfies TerminalNewAutoApproveButtonData + }); actions.push({ label: localize('autoApprove.exactCommand', 'Always Allow Exact Command Line'), data: { @@ -171,7 +256,8 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str value: { approve: true, matchCommandLine: true - } + }, + scope: 'user' } } satisfies TerminalNewAutoApproveButtonData }); @@ -207,6 +293,10 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str export function dedupeRules(rules: ICommandApprovalResultWithReason[]): ICommandApprovalResultWithReason[] { return rules.filter((result, index, array) => { - return result.rule && array.findIndex(r => r.rule && r.rule.sourceText === result.rule!.sourceText) === index; + if (!isAutoApproveRule(result.rule)) { + return false; + } + const sourceText = result.rule.sourceText; + return array.findIndex(r => isAutoApproveRule(r.rule) && r.rule.sourceText === sourceText) === index; }); } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts index b18ccbcf0a6..bb0ea29770e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts @@ -9,6 +9,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { CancellationError } from '../../../../../base/common/errors.js'; import { Event } from '../../../../../base/common/event.js'; import { DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { OperatingSystem } from '../../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { hasKey, isNumber, isObject, isString } from '../../../../../base/common/types.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -17,6 +18,8 @@ import { PromptInputState } from '../../../../../platform/terminal/common/capabi import { ITerminalLogService, ITerminalProfile, TerminalSettingId, type IShellLaunchConfig } from '../../../../../platform/terminal/common/terminal.js'; import { ITerminalService, type ITerminalInstance } from '../../../terminal/browser/terminal.js'; import { getShellIntegrationTimeout } from '../../../terminal/common/terminalEnvironment.js'; +import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js'; +import { isBash, isFish, isPowerShell, isZsh } from './runInTerminalHelpers.js'; const enum ShellLaunchType { Unknown = 0, @@ -50,8 +53,8 @@ export class ToolTerminalCreator { ) { } - async createTerminal(shellOrProfile: string | ITerminalProfile, token: CancellationToken): Promise { - const instance = await this._createCopilotTerminal(shellOrProfile); + async createTerminal(shellOrProfile: string | ITerminalProfile, os: OperatingSystem, token: CancellationToken): Promise { + const instance = await this._createCopilotTerminal(shellOrProfile, os); const toolTerminal: IToolTerminal = { instance, shellIntegrationQuality: ShellIntegrationQuality.None, @@ -138,15 +141,32 @@ export class ToolTerminalCreator { } } - private _createCopilotTerminal(shellOrProfile: string | ITerminalProfile) { + private _createCopilotTerminal(shellOrProfile: string | ITerminalProfile, os: OperatingSystem) { + const shellPath = isString(shellOrProfile) ? shellOrProfile : shellOrProfile.path; + + const env: Record = { + // Avoid making `git diff` interactive when called from copilot + GIT_PAGER: 'cat', + }; + + const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true; + if (preventShellHistory) { + // Check if the shell supports history exclusion via shell integration scripts + if ( + isBash(shellPath, os) || + isZsh(shellPath, os) || + isFish(shellPath, os) || + isPowerShell(shellPath, os) + ) { + env['VSCODE_PREVENT_SHELL_HISTORY'] = '1'; + } + } + const config: IShellLaunchConfig = { icon: ThemeIcon.fromId(Codicon.chatSparkle.id), hideFromUser: true, forcePersist: true, - env: { - // Avoid making `git diff` interactive when called from copilot - GIT_PAGER: 'cat', - } + env, }; if (isString(shellOrProfile)) { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.ts similarity index 66% rename from src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts rename to src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.ts index fd0be4041a1..effcd105dbc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.ts @@ -3,27 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import type { OperatingSystem } from '../../../../../base/common/platform.js'; -import { escapeRegExpCharacters, regExpLeadsToEndlessLoop } from '../../../../../base/common/strings.js'; -import { isObject } from '../../../../../base/common/types.js'; -import { structuralEquals } from '../../../../../base/common/equals.js'; -import { ConfigurationTarget, IConfigurationService, type IConfigurationValue } from '../../../../../platform/configuration/common/configuration.js'; -import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js'; -import { isPowerShell } from './runInTerminalHelpers.js'; - -export interface IAutoApproveRule { - regex: RegExp; - regexCaseInsensitive: RegExp; - sourceText: string; - sourceTarget: ConfigurationTarget; - isDefaultRule: boolean; -} +import { structuralEquals } from '../../../../../../../../base/common/equals.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import type { OperatingSystem } from '../../../../../../../../base/common/platform.js'; +import { escapeRegExpCharacters, regExpLeadsToEndlessLoop } from '../../../../../../../../base/common/strings.js'; +import { isObject } from '../../../../../../../../base/common/types.js'; +import type { URI } from '../../../../../../../../base/common/uri.js'; +import { ConfigurationTarget, IConfigurationService, type IConfigurationValue } from '../../../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../../../platform/instantiation/common/instantiation.js'; +import { ITerminalChatService } from '../../../../../../terminal/browser/terminal.js'; +import { TerminalChatAgentToolsSettingId } from '../../../../common/terminalChatAgentToolsConfiguration.js'; +import { isPowerShell } from '../../../runInTerminalHelpers.js'; +import type { IAutoApproveRule, INpmScriptAutoApproveRule } from '../commandLineAnalyzer.js'; +import { NpmScriptAutoApprover } from './npmScriptAutoApprover.js'; export interface ICommandApprovalResultWithReason { result: ICommandApprovalResult; reason: string; - rule?: IAutoApproveRule; + rule?: IAutoApproveRule | INpmScriptAutoApproveRule; } export type ICommandApprovalResult = 'approved' | 'denied' | 'noMatch'; @@ -36,11 +33,15 @@ export class CommandLineAutoApprover extends Disposable { private _allowListRules: IAutoApproveRule[] = []; private _allowListCommandLineRules: IAutoApproveRule[] = []; private _denyListCommandLineRules: IAutoApproveRule[] = []; + private readonly _npmScriptAutoApprover: NpmScriptAutoApprover; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @ITerminalChatService private readonly _terminalChatService: ITerminalChatService, ) { super(); + this._npmScriptAutoApprover = this._register(instantiationService.createInstance(NpmScriptAutoApprover)); this.updateConfiguration(); this._register(this._configurationService.onDidChangeConfiguration(e => { if ( @@ -76,7 +77,7 @@ export class CommandLineAutoApprover extends Disposable { this._denyListCommandLineRules = denyListCommandLineRules; } - isCommandAutoApproved(command: string, shell: string, os: OperatingSystem): ICommandApprovalResultWithReason { + async isCommandAutoApproved(command: string, shell: string, os: OperatingSystem, cwd: URI | undefined, chatSessionId?: string): Promise { // Check if the command has a transient environment variable assignment prefix which we // always deny for now as it can easily lead to execute other commands if (transientEnvVarRegex.test(command)) { @@ -86,7 +87,7 @@ export class CommandLineAutoApprover extends Disposable { }; } - // Check the deny list to see if this command requires explicit approval + // Check the config deny list to see if this command requires explicit approval for (const rule of this._denyListRules) { if (this._commandMatchesRule(rule, command, shell, os)) { return { @@ -97,7 +98,18 @@ export class CommandLineAutoApprover extends Disposable { } } - // Check the allow list to see if the command is allowed to run without explicit approval + // Check session allow rules (session deny rules can't exist) + for (const rule of this._getSessionRules(chatSessionId).allowListRules) { + if (this._commandMatchesRule(rule, command, shell, os)) { + return { + result: 'approved', + rule, + reason: `Command '${command}' is approved by session allow list rule: ${rule.sourceText}` + }; + } + } + + // Check the config allow list to see if the command is allowed to run without explicit approval for (const rule of this._allowListRules) { if (this._commandMatchesRule(rule, command, shell, os)) { return { @@ -108,6 +120,16 @@ export class CommandLineAutoApprover extends Disposable { } } + // Check if this is an npm/yarn/pnpm script defined in package.json + const npmScriptResult = await this._npmScriptAutoApprover.isCommandAutoApproved(command, cwd); + if (npmScriptResult.isAutoApproved) { + return { + result: 'approved', + rule: { type: 'npmScript', npmScriptResult }, + reason: `Command '${command}' is approved as npm script '${npmScriptResult.scriptName}' is defined in package.json` + }; + } + // TODO: LLM-based auto-approval https://github.com/microsoft/vscode/issues/253267 // Fallback is always to require approval @@ -117,8 +139,8 @@ export class CommandLineAutoApprover extends Disposable { }; } - isCommandLineAutoApproved(commandLine: string): ICommandApprovalResultWithReason { - // Check the deny list first to see if this command line requires explicit approval + isCommandLineAutoApproved(commandLine: string, chatSessionId?: string): ICommandApprovalResultWithReason { + // Check the config deny list first to see if this command line requires explicit approval for (const rule of this._denyListCommandLineRules) { if (rule.regex.test(commandLine)) { return { @@ -129,7 +151,18 @@ export class CommandLineAutoApprover extends Disposable { } } - // Check if the full command line matches any of the allow list command line regexes + // Check session allow list (session deny rules can't exist) + for (const rule of this._getSessionRules(chatSessionId).allowListCommandLineRules) { + if (rule.regex.test(commandLine)) { + return { + result: 'approved', + rule, + reason: `Command line '${commandLine}' is approved by session allow list rule: ${rule.sourceText}` + }; + } + } + + // Check if the full command line matches any of the config allow list command line regexes for (const rule of this._allowListCommandLineRules) { if (rule.regex.test(commandLine)) { return { @@ -145,6 +178,54 @@ export class CommandLineAutoApprover extends Disposable { }; } + private _getSessionRules(chatSessionId?: string): { + denyListRules: IAutoApproveRule[]; + allowListRules: IAutoApproveRule[]; + allowListCommandLineRules: IAutoApproveRule[]; + denyListCommandLineRules: IAutoApproveRule[]; + } { + const denyListRules: IAutoApproveRule[] = []; + const allowListRules: IAutoApproveRule[] = []; + const allowListCommandLineRules: IAutoApproveRule[] = []; + const denyListCommandLineRules: IAutoApproveRule[] = []; + + if (!chatSessionId) { + return { denyListRules, allowListRules, allowListCommandLineRules, denyListCommandLineRules }; + } + + const sessionRulesConfig = this._terminalChatService.getSessionAutoApproveRules(chatSessionId); + for (const [key, value] of Object.entries(sessionRulesConfig)) { + if (typeof value === 'boolean') { + const { regex, regexCaseInsensitive } = this._convertAutoApproveEntryToRegex(key); + if (value === true) { + allowListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } else if (value === false) { + denyListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } + } else if (typeof value === 'object' && value !== null) { + const objectValue = value as { approve?: boolean; matchCommandLine?: boolean }; + if (typeof objectValue.approve === 'boolean') { + const { regex, regexCaseInsensitive } = this._convertAutoApproveEntryToRegex(key); + if (objectValue.approve === true) { + if (objectValue.matchCommandLine === true) { + allowListCommandLineRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } else { + allowListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } + } else if (objectValue.approve === false) { + if (objectValue.matchCommandLine === true) { + denyListCommandLineRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } else { + denyListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } + } + } + } + } + + return { denyListRules, allowListRules, allowListCommandLineRules, denyListCommandLineRules }; + } + private _commandMatchesRule(rule: IAutoApproveRule, command: string, shell: string, os: OperatingSystem): boolean { const isPwsh = isPowerShell(shell, os); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/npmScriptAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/npmScriptAutoApprover.ts new file mode 100644 index 00000000000..57da0c5439c --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/npmScriptAutoApprover.ts @@ -0,0 +1,232 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownString, type IMarkdownString } from '../../../../../../../../base/common/htmlContent.js'; +import { visit, type JSONVisitor } from '../../../../../../../../base/common/json.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../../../../base/common/uri.js'; +import { IUriIdentityService } from '../../../../../../../../platform/uriIdentity/common/uriIdentity.js'; +import { localize } from '../../../../../../../../nls.js'; +import { IConfigurationService } from '../../../../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../../../../platform/files/common/files.js'; +import { IWorkspaceContextService, type IWorkspaceFolder } from '../../../../../../../../platform/workspace/common/workspace.js'; +import { TerminalChatAgentToolsSettingId } from '../../../../common/terminalChatAgentToolsConfiguration.js'; + +/** + * Regex patterns to match npm/yarn/pnpm run commands and extract the script name. + * Uses named capture groups: 'command' for the package manager, 'scriptName' for the script. + */ +const npmRunPatterns = [ + // npm run