mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-13 15:35:20 +01:00
Merge branch 'main' into dev/dmitriv/finalize-button-location-api
This commit is contained in:
Vendored
-2
@@ -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,
|
||||
|
||||
@@ -1924,7 +1924,15 @@ export class Repository {
|
||||
async stage(path: string, data: Uint8Array): Promise<void> {
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Generated
+48
-48
@@ -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/*"
|
||||
|
||||
+10
-10
@@ -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",
|
||||
|
||||
Generated
+48
-48
@@ -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/*"
|
||||
|
||||
+10
-10
@@ -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",
|
||||
|
||||
Generated
+44
-44
@@ -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/*"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() { }
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string> {
|
||||
export async function fetchAuthorizationServerMetadata(
|
||||
authorizationServer: string,
|
||||
options: IFetchAuthorizationServerMetadataOptions = {}
|
||||
): Promise<IAuthorizationServerMetadata> {
|
||||
): 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)
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<number, IDecorationStyleCacheEntry>();
|
||||
private readonly _cacheByStyle = new NKeyMap<IDecorationStyleCacheEntry, [number, number, string]>();
|
||||
private readonly _cacheByStyle = new NKeyMap<IDecorationStyleCacheEntry, [number, number, string, number, string, number]>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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<IBaseFileStat> { }
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<IMcpAuthSetupTelemetry, McpAuthSetupClassification>('mcp/authSetup', data);
|
||||
}
|
||||
|
||||
private async loginPrompt(mcpLabel: string, providerLabel: string, recreatingSession: boolean): Promise<boolean> {
|
||||
const message = recreatingSession
|
||||
? nls.localize('confirmRelogin', "The MCP Server Definition '{0}' wants you to authenticate to {1}.", mcpLabel, providerLabel)
|
||||
|
||||
@@ -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<string | undefined>;
|
||||
$getTokenForProviderId(id: number, providerId: string, scopes: string[], options?: IMcpAuthenticationOptions): Promise<string | undefined>;
|
||||
$logMcpAuthSetup(data: IMcpAuthSetupTelemetry): void;
|
||||
}
|
||||
|
||||
export interface MainThreadDataChannelsShape extends IDisposable {
|
||||
|
||||
@@ -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<AuthMetadata> {
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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()})`);
|
||||
|
||||
@@ -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<void> {
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IQuickAccessRegistry>(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);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<IPickerQuickAccessItem> {
|
||||
|
||||
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<IPickerQuickAccessItem | IQuickPickSeparator> = [];
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+34
-13
@@ -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);
|
||||
}
|
||||
|
||||
+80
-28
@@ -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<string, unknown> | undefined) ?? {};
|
||||
let newValue: Record<string, unknown>;
|
||||
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<string, unknown> | undefined) ?? {};
|
||||
if (isObject(oldValue)) {
|
||||
const newValue: Record<string, unknown> = { ...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<string, unknown> | undefined) ?? {};
|
||||
if (isObject(oldValue)) {
|
||||
const newValue: Record<string, unknown> = { ...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;
|
||||
|
||||
@@ -1555,7 +1555,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
|
||||
}, context, templateData);
|
||||
|
||||
if (thinkingPart instanceof ChatThinkingContentPart) {
|
||||
thinkingPart.appendItem(part?.domNode, toolInvocation.toolId, toolInvocation);
|
||||
thinkingPart.appendItem(part?.domNode, toolInvocation.toolId, toolInvocation, templateData.value);
|
||||
thinkingPart.addDisposable(part);
|
||||
thinkingPart.addDisposable(thinkingPart.onDidChangeHeight(() => {
|
||||
this.updateItemHeight(templateData);
|
||||
@@ -1567,7 +1567,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
|
||||
|
||||
if (this.shouldPinPart(toolInvocation, context.element)) {
|
||||
if (lastThinking && part?.domNode && toolInvocation.presentation !== 'hidden') {
|
||||
lastThinking.appendItem(part?.domNode, toolInvocation.toolId, toolInvocation);
|
||||
lastThinking.appendItem(part?.domNode, toolInvocation.toolId, toolInvocation, templateData.value);
|
||||
lastThinking.addDisposable(part);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -305,6 +305,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
this.logService.debug('ChatWidget#setViewModel: no viewModel');
|
||||
}
|
||||
|
||||
this.currentRequest = undefined;
|
||||
this._onDidChangeViewModel.fire({ previousSessionResource, currentSessionResource: this._viewModel?.sessionResource });
|
||||
}
|
||||
|
||||
@@ -1642,7 +1643,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
this.inputContainer = dom.$('.chat-edit-input-container');
|
||||
rowContainer.appendChild(this.inputContainer);
|
||||
this.createInput(this.inputContainer);
|
||||
this.input.setChatMode(this.inputPart.currentModeKind);
|
||||
this.input.setChatMode(this.inputPart.currentModeObs.get().id);
|
||||
} else {
|
||||
this.inputPart.element.classList.add('editing');
|
||||
}
|
||||
@@ -1708,7 +1709,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
const isInput = this.configurationService.getValue<string>('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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, Set<number>>();
|
||||
|
||||
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();
|
||||
|
||||
+2
-1
@@ -56,7 +56,8 @@ class ExtendedTestFileService extends TestFileService {
|
||||
mtime: 0,
|
||||
ctime: 0,
|
||||
readonly: false,
|
||||
locked: false
|
||||
locked: false,
|
||||
executable: false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3070,6 +3070,8 @@ class SCMTreeDataSource extends Disposable implements IAsyncDataSource<ISCMViewS
|
||||
return result;
|
||||
} else if (isSCMInput(element)) {
|
||||
return element.repository;
|
||||
} else if (isSCMActionButton(element)) {
|
||||
return element.repository;
|
||||
} else if (isSCMResourceGroup(element)) {
|
||||
const repository = this.scmViewService.visibleRepositories.find(r => r.provider === element.provider);
|
||||
if (!repository) {
|
||||
@@ -3077,6 +3079,8 @@ class SCMTreeDataSource extends Disposable implements IAsyncDataSource<ISCMViewS
|
||||
}
|
||||
|
||||
return repository;
|
||||
} else if (isSCMRepository(element)) {
|
||||
return this.scmViewService;
|
||||
} else {
|
||||
throw new Error('Unexpected call to getParent');
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ abstract class DetachedTerminalMirror extends Disposable {
|
||||
container.classList.add('chat-terminal-output-terminal');
|
||||
const needsAttach = this._attachedContainer !== container || container.firstChild === null;
|
||||
if (needsAttach) {
|
||||
terminal.attachToElement(container);
|
||||
terminal.attachToElement(container, { enableGpu: false });
|
||||
this._attachedContainer = container;
|
||||
}
|
||||
return terminal;
|
||||
|
||||
@@ -217,6 +217,21 @@ export interface ITerminalChatService {
|
||||
* @returns True if the session has auto approval enabled
|
||||
*/
|
||||
hasChatSessionAutoApproval(chatSessionId: string): boolean;
|
||||
|
||||
/**
|
||||
* Add a session-scoped auto-approve rule.
|
||||
* @param chatSessionId The chat session ID to associate the rule with
|
||||
* @param key The rule key (command or regex pattern)
|
||||
* @param value The rule value (approval boolean or object with approve and matchCommandLine)
|
||||
*/
|
||||
addSessionAutoApproveRule(chatSessionId: string, key: string, value: boolean | { approve: boolean; matchCommandLine?: boolean }): void;
|
||||
|
||||
/**
|
||||
* Get all session-scoped auto-approve rules for a specific chat session.
|
||||
* @param chatSessionId The chat session ID to get rules for
|
||||
* @returns A record of all session-scoped auto-approve rules for the session
|
||||
*/
|
||||
getSessionAutoApproveRules(chatSessionId: string): Readonly<Record<string, boolean | { approve: boolean; matchCommandLine?: boolean }>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,8 +572,10 @@ const terminalConfiguration: IStringDictionary<IConfigurationPropertySchema> = {
|
||||
[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}#\``),
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
.monaco-workbench .terminal-inline-chat .interactive-session {
|
||||
margin: initial;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.monaco-workbench .terminal-inline-chat.hide {
|
||||
|
||||
@@ -53,6 +53,13 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ
|
||||
*/
|
||||
private readonly _sessionAutoApprovalEnabled = new Set<string>();
|
||||
|
||||
/**
|
||||
* 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<chatSessionId, Record<ruleKey, ruleValue>>
|
||||
*/
|
||||
private readonly _sessionAutoApproveRules = new Map<string, Record<string, boolean | { approve: boolean; matchCommandLine?: boolean }>>();
|
||||
|
||||
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<Record<string, boolean | { approve: boolean; matchCommandLine?: boolean }>> {
|
||||
return this._sessionAutoApproveRules.get(chatSessionId) ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
+107
-17
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
+27
-7
@@ -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<IToolTerminal> {
|
||||
const instance = await this._createCopilotTerminal(shellOrProfile);
|
||||
async createTerminal(shellOrProfile: string | ITerminalProfile, os: OperatingSystem, token: CancellationToken): Promise<IToolTerminal> {
|
||||
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<string, string> = {
|
||||
// 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)) {
|
||||
|
||||
+104
-23
@@ -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<ICommandApprovalResultWithReason> {
|
||||
// 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);
|
||||
|
||||
+232
@@ -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 <script>
|
||||
// npm run-script <script>
|
||||
/^(?<command>npm)\s+(?:run(?:-script)?)\s+(?<scriptName>[^\s&|;]+)/i,
|
||||
// npm test, npm start, npm stop, npm restart (shorthand commands)
|
||||
// See https://docs.npmjs.com/cli/v10/commands/npm-run-script
|
||||
/^(?<command>npm)\s+(?<scriptName>test|start|stop|restart)\b/i,
|
||||
// yarn <script>
|
||||
// yarn run <script>
|
||||
/^(?<command>yarn)\s+(?:run\s+)?(?<scriptName>[^\s&|;]+)/i,
|
||||
// pnpm <script>
|
||||
// pnpm run <script>
|
||||
/^(?<command>pnpm)\s+(?:run\s+)?(?<scriptName>[^\s&|;]+)/i,
|
||||
];
|
||||
|
||||
/**
|
||||
* Yarn built-in commands that should not be treated as script names.
|
||||
* Note: 'test' is omitted since it's commonly a user script, and 'yarn test'
|
||||
* is often used to run the 'test' script from package.json.
|
||||
*/
|
||||
const yarnBuiltinCommands = new Set([
|
||||
'add', 'audit', 'autoclean', 'bin', 'cache', 'check', 'config',
|
||||
'create', 'dedupe', 'dlx', 'exec', 'explain', 'generate-lock-entry',
|
||||
'global', 'help', 'import', 'info', 'init', 'install', 'licenses',
|
||||
'link', 'list', 'login', 'logout', 'node', 'outdated', 'owner',
|
||||
'pack', 'patch', 'patch-commit', 'plugin', 'policies', 'publish',
|
||||
'rebuild', 'remove', 'run', 'search', 'set', 'stage', 'tag', 'team',
|
||||
'unlink', 'unplug', 'up', 'upgrade', 'upgrade-interactive',
|
||||
'version', 'versions', 'why', 'workspace', 'workspaces',
|
||||
]);
|
||||
|
||||
/**
|
||||
* pnpm built-in commands that should not be treated as script names.
|
||||
* Note: 'test' is omitted since it's commonly a user script, and 'pnpm test'
|
||||
* is often used to run the 'test' script from package.json.
|
||||
*/
|
||||
const pnpmBuiltinCommands = new Set([
|
||||
'add', 'audit', 'bin', 'config', 'dedupe', 'deploy', 'dlx', 'doctor',
|
||||
'env', 'exec', 'fetch', 'import', 'init', 'install', 'install-test',
|
||||
'licenses', 'link', 'list', 'ln', 'ls', 'outdated', 'pack', 'patch',
|
||||
'patch-commit', 'patch-remove', 'prune', 'publish', 'rb', 'rebuild',
|
||||
'remove', 'rm', 'root', 'run', 'server', 'setup', 'store',
|
||||
'un', 'uninstall', 'unlink', 'up', 'update', 'why',
|
||||
]);
|
||||
|
||||
interface IPackageJsonScripts {
|
||||
uri: URI;
|
||||
scripts: Set<string>;
|
||||
}
|
||||
|
||||
export interface INpmScriptAutoApproveResult {
|
||||
isAutoApproved: boolean;
|
||||
scriptName?: string;
|
||||
autoApproveInfo?: IMarkdownString;
|
||||
}
|
||||
|
||||
export class NpmScriptAutoApprover extends Disposable {
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a single command is an npm/yarn/pnpm script that exists in package.json.
|
||||
* Returns auto-approve result if the command is a valid script.
|
||||
*/
|
||||
async isCommandAutoApproved(command: string, cwd: URI | undefined): Promise<INpmScriptAutoApproveResult> {
|
||||
// Check if the feature is enabled
|
||||
const isNpmScriptAutoApproveEnabled = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoApproveWorkspaceNpmScripts) === true;
|
||||
if (!isNpmScriptAutoApproveEnabled) {
|
||||
return { isAutoApproved: false };
|
||||
}
|
||||
|
||||
// Extract script name from the command
|
||||
const scriptName = this._extractScriptName(command);
|
||||
if (!scriptName) {
|
||||
return { isAutoApproved: false };
|
||||
}
|
||||
|
||||
// Find and parse package.json
|
||||
const packageJsonScripts = await this._getPackageJsonScripts(cwd);
|
||||
if (!packageJsonScripts) {
|
||||
return { isAutoApproved: false };
|
||||
}
|
||||
|
||||
// Check if script exists in package.json
|
||||
if (!packageJsonScripts.scripts.has(scriptName)) {
|
||||
return { isAutoApproved: false };
|
||||
}
|
||||
|
||||
// Script exists - auto approve
|
||||
return {
|
||||
isAutoApproved: true,
|
||||
scriptName,
|
||||
autoApproveInfo: new MarkdownString(
|
||||
localize('autoApprove.npmScript', 'Auto approved as {0} is defined in package.json', `\`${scriptName}\``)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts script name from an npm/yarn/pnpm run command.
|
||||
*/
|
||||
private _extractScriptName(command: string): string | undefined {
|
||||
const trimmedCommand = command.trim();
|
||||
|
||||
for (const pattern of npmRunPatterns) {
|
||||
const match = trimmedCommand.match(pattern);
|
||||
if (match?.groups?.scriptName) {
|
||||
const { command: pkgManager, scriptName } = match.groups;
|
||||
|
||||
// Check if this is a yarn/pnpm shorthand that matches a built-in command
|
||||
if (pkgManager.toLowerCase() === 'yarn' && yarnBuiltinCommands.has(scriptName.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
if (pkgManager.toLowerCase() === 'pnpm' && pnpmBuiltinCommands.has(scriptName.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return scriptName;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URI is within any workspace folder.
|
||||
*/
|
||||
private _isWithinWorkspace(uri: URI): boolean {
|
||||
const workspaceFolders = this._workspaceContextService.getWorkspace().folders;
|
||||
return workspaceFolders.some((folder: IWorkspaceFolder) => this._uriIdentityService.extUri.isEqualOrParent(uri, folder.uri));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and parses package.json to get the scripts section.
|
||||
* Only looks within the workspace for security.
|
||||
*/
|
||||
private async _getPackageJsonScripts(cwd: URI | undefined): Promise<IPackageJsonScripts | undefined> {
|
||||
// Only look in cwd if it's within the workspace
|
||||
if (!cwd || !this._isWithinWorkspace(cwd)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const packageJsonUri = URI.joinPath(cwd, 'package.json');
|
||||
const scripts = await this._readPackageJsonScripts(packageJsonUri);
|
||||
if (scripts) {
|
||||
return { uri: packageJsonUri, scripts };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and parses the scripts section from a package.json file.
|
||||
*/
|
||||
private async _readPackageJsonScripts(packageJsonUri: URI): Promise<Set<string> | undefined> {
|
||||
try {
|
||||
const exists = await this._fileService.exists(packageJsonUri);
|
||||
if (!exists) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const content = await this._fileService.readFile(packageJsonUri);
|
||||
const text = content.value.toString();
|
||||
|
||||
return this._parsePackageJsonScripts(text);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the scripts section from package.json content using jsonc-parser.
|
||||
*/
|
||||
private _parsePackageJsonScripts(content: string): Set<string> | undefined {
|
||||
const scripts = new Set<string>();
|
||||
let inScripts = false;
|
||||
let level = 0;
|
||||
|
||||
const visitor: JSONVisitor = {
|
||||
onError() {
|
||||
// Ignore parse errors
|
||||
},
|
||||
onObjectBegin() {
|
||||
level++;
|
||||
},
|
||||
onObjectEnd() {
|
||||
if (inScripts && level === 2) {
|
||||
inScripts = false;
|
||||
}
|
||||
level--;
|
||||
},
|
||||
onObjectProperty(property: string) {
|
||||
if (level === 1 && property === 'scripts') {
|
||||
inScripts = true;
|
||||
} else if (inScripts && level === 2) {
|
||||
scripts.add(property);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
visit(content, visitor);
|
||||
|
||||
return scripts.size > 0 ? scripts : undefined;
|
||||
}
|
||||
}
|
||||
+23
@@ -7,9 +7,32 @@ import type { IMarkdownString } from '../../../../../../../base/common/htmlConte
|
||||
import type { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import type { OperatingSystem } from '../../../../../../../base/common/platform.js';
|
||||
import type { URI } from '../../../../../../../base/common/uri.js';
|
||||
import type { ConfigurationTarget } from '../../../../../../../platform/configuration/common/configuration.js';
|
||||
import type { ToolConfirmationAction } from '../../../../../chat/common/tools/languageModelToolsService.js';
|
||||
import type { INpmScriptAutoApproveResult } from './autoApprove/npmScriptAutoApprover.js';
|
||||
import type { TreeSitterCommandParserLanguage } from '../../treeSitterCommandParser.js';
|
||||
|
||||
export interface IAutoApproveRule {
|
||||
regex: RegExp;
|
||||
regexCaseInsensitive: RegExp;
|
||||
sourceText: string;
|
||||
sourceTarget: ConfigurationTarget | 'session';
|
||||
isDefaultRule: boolean;
|
||||
}
|
||||
|
||||
export interface INpmScriptAutoApproveRule {
|
||||
type: 'npmScript';
|
||||
npmScriptResult: INpmScriptAutoApproveResult;
|
||||
}
|
||||
|
||||
export function isAutoApproveRule(rule: IAutoApproveRule | INpmScriptAutoApproveRule | undefined): rule is IAutoApproveRule {
|
||||
return !!rule && 'sourceText' in rule;
|
||||
}
|
||||
|
||||
export function isNpmScriptAutoApproveRule(rule: IAutoApproveRule | INpmScriptAutoApproveRule | undefined): rule is INpmScriptAutoApproveRule {
|
||||
return !!rule && 'type' in rule && rule.type === 'npmScript';
|
||||
}
|
||||
|
||||
export interface ICommandLineAnalyzer extends IDisposable {
|
||||
analyze(options: ICommandLineAnalyzerOptions): Promise<ICommandLineAnalyzerResult>;
|
||||
}
|
||||
|
||||
+50
-18
@@ -8,7 +8,7 @@ import { createCommandUri, MarkdownString, type IMarkdownString } from '../../..
|
||||
import { Disposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import type { SingleOrMany } from '../../../../../../../base/common/types.js';
|
||||
import { localize } from '../../../../../../../nls.js';
|
||||
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js';
|
||||
import { ConfigurationTarget, IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITerminalChatService } from '../../../../../terminal/browser/terminal.js';
|
||||
import { IStorageService, StorageScope } from '../../../../../../../platform/storage/common/storage.js';
|
||||
@@ -16,12 +16,12 @@ import { TerminalToolConfirmationStorageKeys } from '../../../../../chat/browser
|
||||
import { ChatConfiguration } from '../../../../../chat/common/constants.js';
|
||||
import type { ToolConfirmationAction } from '../../../../../chat/common/tools/languageModelToolsService.js';
|
||||
import { TerminalChatAgentToolsSettingId } from '../../../common/terminalChatAgentToolsConfiguration.js';
|
||||
import { CommandLineAutoApprover, type IAutoApproveRule, type ICommandApprovalResult, type ICommandApprovalResultWithReason } from '../../commandLineAutoApprover.js';
|
||||
import { dedupeRules, generateAutoApproveActions, isPowerShell } from '../../runInTerminalHelpers.js';
|
||||
import type { RunInTerminalToolTelemetry } from '../../runInTerminalToolTelemetry.js';
|
||||
import { type TreeSitterCommandParser } from '../../treeSitterCommandParser.js';
|
||||
import type { ICommandLineAnalyzer, ICommandLineAnalyzerOptions, ICommandLineAnalyzerResult } from './commandLineAnalyzer.js';
|
||||
import { type ICommandLineAnalyzer, type ICommandLineAnalyzerOptions, type ICommandLineAnalyzerResult, type IAutoApproveRule, isAutoApproveRule, isNpmScriptAutoApproveRule } from './commandLineAnalyzer.js';
|
||||
import { TerminalChatCommandId } from '../../../../chat/browser/terminalChat.js';
|
||||
import { CommandLineAutoApprover, type ICommandApprovalResultWithReason } from './autoApprove/commandLineAutoApprover.js';
|
||||
|
||||
const promptInjectionWarningCommandsLower = [
|
||||
'curl',
|
||||
@@ -67,9 +67,11 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma
|
||||
};
|
||||
}
|
||||
|
||||
const trimmedCommandLine = options.commandLine.trimStart();
|
||||
|
||||
let subCommands: string[] | undefined;
|
||||
try {
|
||||
subCommands = await this._treeSitterCommandParser.extractSubCommands(options.treeSitterLanguage, options.commandLine);
|
||||
subCommands = await this._treeSitterCommandParser.extractSubCommands(options.treeSitterLanguage, trimmedCommandLine);
|
||||
this._log(`Parsed sub-commands via ${options.treeSitterLanguage} grammar`, subCommands);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -87,8 +89,8 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma
|
||||
};
|
||||
}
|
||||
|
||||
const subCommandResults = subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, options.shell, options.os));
|
||||
const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(options.commandLine);
|
||||
const subCommandResults = await Promise.all(subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, options.shell, options.os, options.cwd, options.chatSessionId)));
|
||||
const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(trimmedCommandLine, options.chatSessionId);
|
||||
const autoApproveReasons: string[] = [
|
||||
...subCommandResults.map(e => e.reason),
|
||||
commandLineResult.reason,
|
||||
@@ -102,26 +104,26 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma
|
||||
if (deniedSubCommandResult) {
|
||||
this._log('Sub-command DENIED auto approval');
|
||||
isDenied = true;
|
||||
autoApproveDefault = deniedSubCommandResult.rule?.isDefaultRule;
|
||||
autoApproveDefault = isAutoApproveRule(deniedSubCommandResult.rule) ? deniedSubCommandResult.rule.isDefaultRule : undefined;
|
||||
autoApproveReason = 'subCommand';
|
||||
} else if (commandLineResult.result === 'denied') {
|
||||
this._log('Command line DENIED auto approval');
|
||||
isDenied = true;
|
||||
autoApproveDefault = commandLineResult.rule?.isDefaultRule;
|
||||
autoApproveDefault = isAutoApproveRule(commandLineResult.rule) ? commandLineResult.rule.isDefaultRule : undefined;
|
||||
autoApproveReason = 'commandLine';
|
||||
} else {
|
||||
if (subCommandResults.every(e => e.result === 'approved')) {
|
||||
this._log('All sub-commands auto-approved');
|
||||
autoApproveReason = 'subCommand';
|
||||
isAutoApproved = true;
|
||||
autoApproveDefault = subCommandResults.every(e => e.rule?.isDefaultRule);
|
||||
autoApproveReason = 'subCommand';
|
||||
autoApproveDefault = subCommandResults.every(e => isAutoApproveRule(e.rule) && e.rule.isDefaultRule);
|
||||
} else {
|
||||
this._log('All sub-commands NOT auto-approved');
|
||||
if (commandLineResult.result === 'approved') {
|
||||
this._log('Command line auto-approved');
|
||||
autoApproveReason = 'commandLine';
|
||||
isAutoApproved = true;
|
||||
autoApproveDefault = commandLineResult.rule?.isDefaultRule;
|
||||
autoApproveDefault = isAutoApproveRule(commandLineResult.rule) ? commandLineResult.rule.isDefaultRule : undefined;
|
||||
} else {
|
||||
this._log('Command line NOT auto-approved');
|
||||
}
|
||||
@@ -169,7 +171,7 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma
|
||||
}
|
||||
|
||||
if (!isAutoApproved && isAutoApproveEnabled) {
|
||||
customActions = generateAutoApproveActions(options.commandLine, subCommands, { subCommandResults, commandLineResult });
|
||||
customActions = generateAutoApproveActions(trimmedCommandLine, subCommands, { subCommandResults, commandLineResult });
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -189,11 +191,36 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma
|
||||
subCommandResults: ICommandApprovalResultWithReason[],
|
||||
commandLineResult: ICommandApprovalResultWithReason,
|
||||
): IMarkdownString | undefined {
|
||||
const formatRuleLinks = (result: SingleOrMany<{ result: ICommandApprovalResult; rule?: IAutoApproveRule; reason: string }>): string => {
|
||||
return asArray(result).map(e => {
|
||||
const settingsUri = createCommandUri(TerminalChatCommandId.OpenTerminalSettingsLink, e.rule!.sourceTarget);
|
||||
return `[\`${e.rule!.sourceText}\`](${settingsUri.toString()} "${localize('ruleTooltip', 'View rule in settings')}")`;
|
||||
}).join(', ');
|
||||
const formatRuleLinks = (result: SingleOrMany<ICommandApprovalResultWithReason>): string => {
|
||||
return asArray(result)
|
||||
.filter((e): e is ICommandApprovalResultWithReason & { rule: IAutoApproveRule } =>
|
||||
isAutoApproveRule(e.rule))
|
||||
.map(e => {
|
||||
// Session rules cannot be actioned currently so no link
|
||||
if (e.rule.sourceTarget === 'session') {
|
||||
return localize('autoApproveRule.sessionIndicator', '{0} (session)', `\`${e.rule.sourceText}\``);
|
||||
}
|
||||
const settingsUri = createCommandUri(TerminalChatCommandId.OpenTerminalSettingsLink, e.rule.sourceTarget);
|
||||
const tooltip = localize('ruleTooltip', 'View rule in settings');
|
||||
let label = e.rule.sourceText;
|
||||
switch (e.rule?.sourceTarget) {
|
||||
case ConfigurationTarget.DEFAULT:
|
||||
label = `${label} (default)`;
|
||||
break;
|
||||
case ConfigurationTarget.USER:
|
||||
case ConfigurationTarget.USER_LOCAL:
|
||||
label = `${label} (user)`;
|
||||
break;
|
||||
case ConfigurationTarget.USER_REMOTE:
|
||||
label = `${label} (remote)`;
|
||||
break;
|
||||
case ConfigurationTarget.WORKSPACE:
|
||||
case ConfigurationTarget.WORKSPACE_FOLDER:
|
||||
label = `${label} (workspace)`;
|
||||
break;
|
||||
}
|
||||
return `[\`${label}\`](${settingsUri.toString()} "${tooltip}")`;
|
||||
}).join(', ');
|
||||
};
|
||||
|
||||
const mdTrustSettings = {
|
||||
@@ -212,12 +239,17 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma
|
||||
if (isAutoApproved) {
|
||||
switch (autoApproveReason) {
|
||||
case 'commandLine': {
|
||||
if (commandLineResult.rule) {
|
||||
if (isAutoApproveRule(commandLineResult.rule)) {
|
||||
return new MarkdownString(localize('autoApprove.rule', 'Auto approved by rule {0}', formatRuleLinks(commandLineResult)), mdTrustSettings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'subCommand': {
|
||||
// Check if approval came from npm script
|
||||
const npmScriptApproval = subCommandResults.find(e => isNpmScriptAutoApproveRule(e.rule));
|
||||
if (npmScriptApproval && isNpmScriptAutoApproveRule(npmScriptApproval.rule) && npmScriptApproval.rule.npmScriptResult.autoApproveInfo) {
|
||||
return npmScriptApproval.rule.npmScriptResult.autoApproveInfo;
|
||||
}
|
||||
const uniqueRules = dedupeRules(subCommandResults);
|
||||
if (uniqueRules.length === 1) {
|
||||
return new MarkdownString(localize('autoApprove.rule', 'Auto approved by rule {0}', formatRuleLinks(uniqueRules)), mdTrustSettings);
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js';
|
||||
import { isBash, isZsh } from '../../runInTerminalHelpers.js';
|
||||
import { TerminalChatAgentToolsSettingId } from '../../../common/terminalChatAgentToolsConfiguration.js';
|
||||
import type { ICommandLineRewriter, ICommandLineRewriterOptions, ICommandLineRewriterResult } from './commandLineRewriter.js';
|
||||
|
||||
/**
|
||||
* Rewriter that prepends a space to commands to prevent them from being added to shell history for
|
||||
* certain shells. This depends on $VSCODE_PREVENT_SHELL_HISTORY being handled in shell integration
|
||||
* scripts to set `HISTCONTROL=ignorespace` (bash) or `HIST_IGNORE_SPACE` (zsh) env vars. The
|
||||
* prepended space is harmless so we don't try to remove it if shell integration isn't functional.
|
||||
*/
|
||||
export class CommandLinePreventHistoryRewriter extends Disposable implements ICommandLineRewriter {
|
||||
constructor(
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
rewrite(options: ICommandLineRewriterOptions): ICommandLineRewriterResult | undefined {
|
||||
const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true;
|
||||
if (!preventShellHistory) {
|
||||
return undefined;
|
||||
}
|
||||
// Only bash and zsh use space prefix to exclude from history
|
||||
if (isBash(options.shell, options.os) || isZsh(options.shell, options.os)) {
|
||||
return {
|
||||
rewritten: ` ${options.commandLine}`,
|
||||
reasoning: 'Prepended with a space to exclude from shell history'
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
+19
-7
@@ -47,6 +47,7 @@ import { IPollingResult, OutputMonitorState } from './monitoring/types.js';
|
||||
import { LocalChatSessionUri } from '../../../../chat/common/model/chatUri.js';
|
||||
import type { ICommandLineRewriter } from './commandLineRewriter/commandLineRewriter.js';
|
||||
import { CommandLineCdPrefixRewriter } from './commandLineRewriter/commandLineCdPrefixRewriter.js';
|
||||
import { CommandLinePreventHistoryRewriter } from './commandLineRewriter/commandLinePreventHistoryRewriter.js';
|
||||
import { CommandLinePwshChainOperatorRewriter } from './commandLineRewriter/commandLinePwshChainOperatorRewriter.js';
|
||||
import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js';
|
||||
import { IHistoryService } from '../../../../../services/history/common/history.js';
|
||||
@@ -312,6 +313,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
this._commandLineRewriters = [
|
||||
this._register(this._instantiationService.createInstance(CommandLineCdPrefixRewriter)),
|
||||
this._register(this._instantiationService.createInstance(CommandLinePwshChainOperatorRewriter, this._treeSitterCommandParser)),
|
||||
this._register(this._instantiationService.createInstance(CommandLinePreventHistoryRewriter)),
|
||||
];
|
||||
this._commandLineAnalyzers = [
|
||||
this._register(this._instantiationService.createInstance(CommandLineFileWriteAnalyzer, this._treeSitterCommandParser, (message, args) => this._logService.info(`RunInTerminalTool#CommandLineFileWriteAnalyzer: ${message}`, args))),
|
||||
@@ -457,9 +459,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
shellType = 'pwsh';
|
||||
}
|
||||
|
||||
const isFinalAutoApproved = (
|
||||
// Is the setting enabled and the user has opted-in
|
||||
isAutoApproveAllowed &&
|
||||
// Check if the command would be auto-approved based on rules (ignoring warning state)
|
||||
const wouldBeAutoApproved = (
|
||||
// Does at least one analyzer auto approve
|
||||
commandLineAnalyzerResults.some(e => e.isAutoApproved) &&
|
||||
// No analyzer denies auto approval
|
||||
@@ -468,7 +469,16 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
analyzersIsAutoApproveAllowed
|
||||
);
|
||||
|
||||
if (isFinalAutoApproved) {
|
||||
const isFinalAutoApproved = (
|
||||
// Is the setting enabled and the user has opted-in
|
||||
isAutoApproveAllowed &&
|
||||
// Would be auto-approved based on rules
|
||||
wouldBeAutoApproved
|
||||
);
|
||||
|
||||
// Pass autoApproveInfo if command would be auto-approved (even if warning not yet accepted)
|
||||
// This allows the confirmation widget to auto-approve after user accepts the warning
|
||||
if (isFinalAutoApproved || (isAutoApproveEnabled && wouldBeAutoApproved)) {
|
||||
toolSpecificData.autoApproveInfo = commandLineAnalyzerResults.find(e => e.autoApproveInfo)?.autoApproveInfo;
|
||||
}
|
||||
|
||||
@@ -792,7 +802,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
private async _initBackgroundTerminal(chatSessionId: string, termId: string, terminalToolSessionId: string | undefined, token: CancellationToken): Promise<IToolTerminal> {
|
||||
this._logService.debug(`RunInTerminalTool: Creating background terminal with ID=${termId}`);
|
||||
const profile = await this._profileFetcher.getCopilotProfile();
|
||||
const toolTerminal = await this._terminalToolCreator.createTerminal(profile, token);
|
||||
const os = await this._osBackend;
|
||||
const toolTerminal = await this._terminalToolCreator.createTerminal(profile, os, token);
|
||||
this._terminalChatService.registerTerminalInstanceWithToolSession(terminalToolSessionId, toolTerminal.instance);
|
||||
this._terminalChatService.registerTerminalInstanceWithChatSession(chatSessionId, toolTerminal.instance);
|
||||
this._registerInputListener(toolTerminal);
|
||||
@@ -814,7 +825,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
return cachedTerminal;
|
||||
}
|
||||
const profile = await this._profileFetcher.getCopilotProfile();
|
||||
const toolTerminal = await this._terminalToolCreator.createTerminal(profile, token);
|
||||
const os = await this._osBackend;
|
||||
const toolTerminal = await this._terminalToolCreator.createTerminal(profile, os, token);
|
||||
this._terminalChatService.registerTerminalInstanceWithToolSession(terminalToolSessionId, toolTerminal.instance);
|
||||
this._terminalChatService.registerTerminalInstanceWithChatSession(chatSessionId, toolTerminal.instance);
|
||||
this._registerInputListener(toolTerminal);
|
||||
@@ -957,7 +969,7 @@ class BackgroundTerminalExecution extends Disposable {
|
||||
private readonly _xterm: XtermTerminal,
|
||||
private readonly _commandLine: string,
|
||||
readonly sessionId: string,
|
||||
commandId?: string
|
||||
commandId?: string,
|
||||
) {
|
||||
super();
|
||||
|
||||
|
||||
+32
-7
@@ -15,11 +15,13 @@ import { PolicyCategory } from '../../../../../base/common/policy.js';
|
||||
export const enum TerminalChatAgentToolsSettingId {
|
||||
EnableAutoApprove = 'chat.tools.terminal.enableAutoApprove',
|
||||
AutoApprove = 'chat.tools.terminal.autoApprove',
|
||||
AutoApproveWorkspaceNpmScripts = 'chat.tools.terminal.autoApproveWorkspaceNpmScripts',
|
||||
IgnoreDefaultAutoApproveRules = 'chat.tools.terminal.ignoreDefaultAutoApproveRules',
|
||||
BlockDetectedFileWrites = 'chat.tools.terminal.blockDetectedFileWrites',
|
||||
ShellIntegrationTimeout = 'chat.tools.terminal.shellIntegrationTimeout',
|
||||
AutoReplyToPrompts = 'chat.tools.terminal.autoReplyToPrompts',
|
||||
OutputLocation = 'chat.tools.terminal.outputLocation',
|
||||
PreventShellHistory = 'chat.tools.terminal.preventShellHistory',
|
||||
|
||||
TerminalProfileLinux = 'chat.tools.terminal.terminalProfile.linux',
|
||||
TerminalProfileMacOs = 'chat.tools.terminal.terminalProfile.osx',
|
||||
@@ -195,22 +197,24 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
|
||||
//
|
||||
// Safe and common sub-commands
|
||||
|
||||
'git status': true,
|
||||
'git log': true,
|
||||
'git show': true,
|
||||
'git diff': true,
|
||||
// Note: These patterns support `-C <path>` and `--no-pager` immediately after `git`
|
||||
'/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+status\\b/': true,
|
||||
'/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+log\\b/': true,
|
||||
'/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+show\\b/': true,
|
||||
'/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+diff\\b/': true,
|
||||
'/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+ls-files\\b/': true,
|
||||
|
||||
// git grep
|
||||
// - `--open-files-in-pager`: This is the configured pager, so no risk of code execution
|
||||
// - See notes on `grep`
|
||||
'git grep': true,
|
||||
'/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+grep\\b/': true,
|
||||
|
||||
// git branch
|
||||
// - `-d`, `-D`, `--delete`: Prevent branch deletion
|
||||
// - `-m`, `-M`: Prevent branch renaming
|
||||
// - `--force`: Generally dangerous
|
||||
'git branch': true,
|
||||
'/^git branch\\b.*-(d|D|m|M|-delete|-force)\\b/': false,
|
||||
'/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+branch\\b/': true,
|
||||
'/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+branch\\b.*-(d|D|m|M|-delete|-force)\\b/': false,
|
||||
|
||||
// #endregion
|
||||
|
||||
@@ -223,6 +227,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
|
||||
'Get-Location': true,
|
||||
'Write-Host': true,
|
||||
'Write-Output': true,
|
||||
'Out-String': true,
|
||||
'Split-Path': true,
|
||||
'Join-Path': true,
|
||||
'Start-Sleep': true,
|
||||
@@ -355,6 +360,15 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
|
||||
tags: ['experimental'],
|
||||
markdownDescription: localize('ignoreDefaultAutoApproveRules.description', "Whether to ignore the built-in default auto-approve rules used by the run in terminal tool as defined in {0}. When this setting is enabled, the run in terminal tool will ignore any rule that comes from the default set but still follow rules defined in the user, remote and workspace settings. Use this setting at your own risk; the default auto-approve rules are designed to protect you against running dangerous commands.", `\`#${TerminalChatAgentToolsSettingId.AutoApprove}#\``),
|
||||
},
|
||||
[TerminalChatAgentToolsSettingId.AutoApproveWorkspaceNpmScripts]: {
|
||||
restricted: true,
|
||||
type: 'boolean',
|
||||
// In order to use agent mode the workspace must be trusted, this plus the fact that
|
||||
// modifying package.json is protected means this is safe to enable by default.
|
||||
default: true,
|
||||
tags: ['experimental'],
|
||||
markdownDescription: localize('autoApproveWorkspaceNpmScripts.description', "Whether to automatically approve npm, yarn, and pnpm run commands when the script is defined in a workspace package.json file. Since the workspace is trusted, scripts defined in package.json are considered safe to run without explicit approval."),
|
||||
},
|
||||
[TerminalChatAgentToolsSettingId.BlockDetectedFileWrites]: {
|
||||
type: 'string',
|
||||
enum: ['never', 'outsideWorkspace', 'all'],
|
||||
@@ -445,6 +459,17 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
|
||||
experiment: {
|
||||
mode: 'auto'
|
||||
}
|
||||
},
|
||||
[TerminalChatAgentToolsSettingId.PreventShellHistory]: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
markdownDescription: [
|
||||
localize('preventShellHistory.description', "Whether to exclude commands run by the terminal tool from the shell history. See below for the supported shells and the method used for each:"),
|
||||
`- \`bash\`: ${localize('preventShellHistory.description.bash', "Sets `HISTCONTROL=ignorespace` and prepends the command with space")}`,
|
||||
`- \`zsh\`: ${localize('preventShellHistory.description.zsh', "Sets `HIST_IGNORE_SPACE` option and prepends the command with space")}`,
|
||||
`- \`fish\`: ${localize('preventShellHistory.description.fish', "Sets `fish_private_mode` to prevent any command from entering history")}`,
|
||||
`- \`pwsh\`: ${localize('preventShellHistory.description.pwsh', "Sets a custom history handler via PSReadLine's `AddToHistoryHandler` to prevent any command from entering history")}`,
|
||||
].join('\n'),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+352
-293
File diff suppressed because it is too large
Load Diff
+188
-11
@@ -4,11 +4,12 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ok, strictEqual } from 'assert';
|
||||
import { TRUNCATION_MESSAGE, dedupeRules, isPowerShell, sanitizeTerminalOutput, truncateOutputKeepingTail } from '../../browser/runInTerminalHelpers.js';
|
||||
import { generateAutoApproveActions, TRUNCATION_MESSAGE, dedupeRules, isPowerShell, sanitizeTerminalOutput, truncateOutputKeepingTail } from '../../browser/runInTerminalHelpers.js';
|
||||
import { OperatingSystem } from '../../../../../../base/common/platform.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
import { ConfigurationTarget } from '../../../../../../platform/configuration/common/configuration.js';
|
||||
import type { IAutoApproveRule, ICommandApprovalResultWithReason } from '../../browser/commandLineAutoApprover.js';
|
||||
import type { ICommandApprovalResultWithReason } from '../../browser/tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.js';
|
||||
import { isAutoApproveRule, type IAutoApproveRule } from '../../browser/tools/commandLineAnalyzer/commandLineAnalyzer.js';
|
||||
|
||||
suite('isPowerShell', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
@@ -186,6 +187,10 @@ suite('dedupeRules', () => {
|
||||
};
|
||||
}
|
||||
|
||||
function getSourceText(result: ICommandApprovalResultWithReason): string | undefined {
|
||||
return isAutoApproveRule(result.rule) ? result.rule.sourceText : undefined;
|
||||
}
|
||||
|
||||
test('should return empty array for empty input', () => {
|
||||
const result = dedupeRules([]);
|
||||
strictEqual(result.length, 0);
|
||||
@@ -197,8 +202,8 @@ suite('dedupeRules', () => {
|
||||
createMockResult('approved', 'approved by ls rule', createMockRule('ls'))
|
||||
]);
|
||||
strictEqual(result.length, 2);
|
||||
strictEqual(result[0].rule?.sourceText, 'echo');
|
||||
strictEqual(result[1].rule?.sourceText, 'ls');
|
||||
strictEqual(getSourceText(result[0]), 'echo');
|
||||
strictEqual(getSourceText(result[1]), 'ls');
|
||||
});
|
||||
|
||||
test('should deduplicate rules with same sourceText', () => {
|
||||
@@ -208,8 +213,8 @@ suite('dedupeRules', () => {
|
||||
createMockResult('approved', 'approved by ls rule', createMockRule('ls'))
|
||||
]);
|
||||
strictEqual(result.length, 2);
|
||||
strictEqual(result[0].rule?.sourceText, 'echo');
|
||||
strictEqual(result[1].rule?.sourceText, 'ls');
|
||||
strictEqual(getSourceText(result[0]), 'echo');
|
||||
strictEqual(getSourceText(result[1]), 'ls');
|
||||
});
|
||||
|
||||
test('should preserve first occurrence when deduplicating', () => {
|
||||
@@ -228,7 +233,7 @@ suite('dedupeRules', () => {
|
||||
createMockResult('denied', 'denied without rule')
|
||||
]);
|
||||
strictEqual(result.length, 1);
|
||||
strictEqual(result[0].rule?.sourceText, 'echo');
|
||||
strictEqual(getSourceText(result[0]), 'echo');
|
||||
});
|
||||
|
||||
test('should handle mix of rules and no-rule results with duplicates', () => {
|
||||
@@ -240,8 +245,8 @@ suite('dedupeRules', () => {
|
||||
createMockResult('denied', 'denied without rule')
|
||||
]);
|
||||
strictEqual(result.length, 2);
|
||||
strictEqual(result[0].rule?.sourceText, 'echo');
|
||||
strictEqual(result[1].rule?.sourceText, 'ls');
|
||||
strictEqual(getSourceText(result[0]), 'echo');
|
||||
strictEqual(getSourceText(result[1]), 'ls');
|
||||
});
|
||||
|
||||
test('should handle multiple duplicates of same rule', () => {
|
||||
@@ -252,9 +257,9 @@ suite('dedupeRules', () => {
|
||||
createMockResult('approved', 'git rule', createMockRule('git'))
|
||||
]);
|
||||
strictEqual(result.length, 2);
|
||||
strictEqual(result[0].rule?.sourceText, 'npm');
|
||||
strictEqual(getSourceText(result[0]), 'npm');
|
||||
strictEqual(result[0].reason, 'npm rule 1');
|
||||
strictEqual(result[1].rule?.sourceText, 'git');
|
||||
strictEqual(getSourceText(result[1]), 'git');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -287,3 +292,175 @@ suite('sanitizeTerminalOutput', () => {
|
||||
ok(result.endsWith('line'));
|
||||
});
|
||||
});
|
||||
|
||||
suite('generateAutoApproveActions', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
function createMockRule(sourceText: string): IAutoApproveRule {
|
||||
// Escape special regex characters for test purposes to prevent regex errors
|
||||
const escapedText = sourceText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return {
|
||||
regex: new RegExp(escapedText),
|
||||
regexCaseInsensitive: new RegExp(escapedText, 'i'),
|
||||
sourceText,
|
||||
sourceTarget: ConfigurationTarget.USER,
|
||||
isDefaultRule: false
|
||||
};
|
||||
}
|
||||
|
||||
function createMockResult(result: 'approved' | 'denied' | 'noMatch', reason: string, rule?: IAutoApproveRule): ICommandApprovalResultWithReason {
|
||||
return {
|
||||
result,
|
||||
reason,
|
||||
rule
|
||||
};
|
||||
}
|
||||
|
||||
test('should suggest mvn test when command is mvn test', () => {
|
||||
const commandLine = 'mvn test';
|
||||
const subCommands = ['mvn test'];
|
||||
const autoApproveResult = {
|
||||
subCommandResults: [createMockResult('noMatch', 'not approved')],
|
||||
commandLineResult: createMockResult('noMatch', 'not approved')
|
||||
};
|
||||
|
||||
const actions = generateAutoApproveActions(commandLine, subCommands, autoApproveResult);
|
||||
const subCommandAction = actions.find(action => action.label.includes('mvn test'));
|
||||
ok(subCommandAction, 'Should suggest mvn test approval');
|
||||
});
|
||||
|
||||
test('should suggest mvn -DskipIT test when flags appear before subcommand', () => {
|
||||
const commandLine = 'mvn -DskipIT test';
|
||||
const subCommands = ['mvn -DskipIT test'];
|
||||
const autoApproveResult = {
|
||||
subCommandResults: [createMockResult('noMatch', 'not approved')],
|
||||
commandLineResult: createMockResult('noMatch', 'not approved')
|
||||
};
|
||||
|
||||
const actions = generateAutoApproveActions(commandLine, subCommands, autoApproveResult);
|
||||
const subCommandAction = actions.find(action => action.label.includes('mvn -DskipIT test'));
|
||||
ok(subCommandAction, 'Should suggest mvn -DskipIT test approval (including flags)');
|
||||
});
|
||||
|
||||
test('should suggest mvn -X -DskipIT test when multiple flags appear before subcommand', () => {
|
||||
const commandLine = 'mvn -X -DskipIT test';
|
||||
const subCommands = ['mvn -X -DskipIT test'];
|
||||
const autoApproveResult = {
|
||||
subCommandResults: [createMockResult('noMatch', 'not approved')],
|
||||
commandLineResult: createMockResult('noMatch', 'not approved')
|
||||
};
|
||||
|
||||
const actions = generateAutoApproveActions(commandLine, subCommands, autoApproveResult);
|
||||
const subCommandAction = actions.find(action => action.label.includes('mvn -X -DskipIT test'));
|
||||
ok(subCommandAction, 'Should suggest mvn -X -DskipIT test approval with multiple flags');
|
||||
});
|
||||
|
||||
test('should suggest gradle --info build when flags appear before subcommand', () => {
|
||||
const commandLine = 'gradle --info build';
|
||||
const subCommands = ['gradle --info build'];
|
||||
const autoApproveResult = {
|
||||
subCommandResults: [createMockResult('noMatch', 'not approved')],
|
||||
commandLineResult: createMockResult('noMatch', 'not approved')
|
||||
};
|
||||
|
||||
const actions = generateAutoApproveActions(commandLine, subCommands, autoApproveResult);
|
||||
const subCommandAction = actions.find(action => action.label.includes('gradle --info build'));
|
||||
ok(subCommandAction, 'Should suggest gradle --info build approval');
|
||||
});
|
||||
|
||||
test('should suggest npm --silent run test when flags appear before subcommand', () => {
|
||||
const commandLine = 'npm --silent run test';
|
||||
const subCommands = ['npm --silent run test'];
|
||||
const autoApproveResult = {
|
||||
subCommandResults: [createMockResult('noMatch', 'not approved')],
|
||||
commandLineResult: createMockResult('noMatch', 'not approved')
|
||||
};
|
||||
|
||||
const actions = generateAutoApproveActions(commandLine, subCommands, autoApproveResult);
|
||||
const subCommandAction = actions.find(action => action.label.includes('npm --silent run test'));
|
||||
ok(subCommandAction, 'Should suggest npm --silent run test approval (sub-sub-command with flags)');
|
||||
});
|
||||
|
||||
test('should suggest npm --silent run --verbose test when flags appear between subcommands', () => {
|
||||
const commandLine = 'npm --silent run --verbose test';
|
||||
const subCommands = ['npm --silent run --verbose test'];
|
||||
const autoApproveResult = {
|
||||
subCommandResults: [createMockResult('noMatch', 'not approved')],
|
||||
commandLineResult: createMockResult('noMatch', 'not approved')
|
||||
};
|
||||
|
||||
const actions = generateAutoApproveActions(commandLine, subCommands, autoApproveResult);
|
||||
const subCommandAction = actions.find(action => action.label.includes('npm --silent run --verbose test'));
|
||||
ok(subCommandAction, 'Should suggest npm --silent run --verbose test with flags between subcommands');
|
||||
});
|
||||
|
||||
test('should not suggest approval when only flags and no subcommand', () => {
|
||||
const commandLine = 'mvn -X -DskipIT';
|
||||
const subCommands = ['mvn -X -DskipIT'];
|
||||
const autoApproveResult = {
|
||||
subCommandResults: [createMockResult('noMatch', 'not approved')],
|
||||
commandLineResult: createMockResult('noMatch', 'not approved')
|
||||
};
|
||||
|
||||
const actions = generateAutoApproveActions(commandLine, subCommands, autoApproveResult);
|
||||
const subCommandAction = actions.find(action => action.label.includes('Always Allow Command:') && action.label.includes('mvn'));
|
||||
strictEqual(subCommandAction, undefined, 'Should not suggest mvn approval when no subcommand found');
|
||||
});
|
||||
|
||||
test('should suggest exact command line when subcommand cannot be extracted', () => {
|
||||
const commandLine = 'mvn -X -DskipIT';
|
||||
const subCommands = ['mvn -X -DskipIT'];
|
||||
const autoApproveResult = {
|
||||
subCommandResults: [createMockResult('noMatch', 'not approved')],
|
||||
commandLineResult: createMockResult('noMatch', 'not approved')
|
||||
};
|
||||
|
||||
const actions = generateAutoApproveActions(commandLine, subCommands, autoApproveResult);
|
||||
const exactCommandAction = actions.find(action => action.label.includes('Always Allow Exact Command Line'));
|
||||
ok(exactCommandAction, 'Should suggest exact command line approval');
|
||||
});
|
||||
|
||||
test('should handle multiple subcommands with flags', () => {
|
||||
const commandLine = 'mvn -DskipIT test && gradle --info build';
|
||||
const subCommands = ['mvn -DskipIT test', 'gradle --info build'];
|
||||
const autoApproveResult = {
|
||||
subCommandResults: [
|
||||
createMockResult('noMatch', 'not approved'),
|
||||
createMockResult('noMatch', 'not approved')
|
||||
],
|
||||
commandLineResult: createMockResult('noMatch', 'not approved')
|
||||
};
|
||||
|
||||
const actions = generateAutoApproveActions(commandLine, subCommands, autoApproveResult);
|
||||
const subCommandAction = actions.find(action =>
|
||||
action.label.includes('mvn -DskipIT test') && action.label.includes('gradle --info build')
|
||||
);
|
||||
ok(subCommandAction, 'Should suggest both mvn -DskipIT test and gradle --info build');
|
||||
});
|
||||
|
||||
test('should not suggest when commands are denied', () => {
|
||||
const commandLine = 'mvn -DskipIT test';
|
||||
const subCommands = ['mvn -DskipIT test'];
|
||||
const autoApproveResult = {
|
||||
subCommandResults: [createMockResult('denied', 'denied by rule', createMockRule('mvn test'))],
|
||||
commandLineResult: createMockResult('noMatch', 'not approved')
|
||||
};
|
||||
|
||||
const actions = generateAutoApproveActions(commandLine, subCommands, autoApproveResult);
|
||||
const subCommandAction = actions.find(action => action.label.includes('Always Allow Command:'));
|
||||
strictEqual(subCommandAction, undefined, 'Should not suggest approval for denied commands');
|
||||
});
|
||||
|
||||
test('should not suggest when commands are already approved', () => {
|
||||
const commandLine = 'mvn -DskipIT test';
|
||||
const subCommands = ['mvn -DskipIT test'];
|
||||
const autoApproveResult = {
|
||||
subCommandResults: [createMockResult('approved', 'approved by rule', createMockRule('mvn test'))],
|
||||
commandLineResult: createMockResult('noMatch', 'not approved')
|
||||
};
|
||||
|
||||
const actions = generateAutoApproveActions(commandLine, subCommands, autoApproveResult);
|
||||
const subCommandAction = actions.find(action => action.label.includes('mvn -DskipIT test') && action.label.includes('Always Allow Command:'));
|
||||
strictEqual(subCommandAction, undefined, 'Should not suggest approval for already approved commands');
|
||||
});
|
||||
});
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { strictEqual } from 'assert';
|
||||
import { VSBuffer } from '../../../../../../../base/common/buffer.js';
|
||||
import { Schemas } from '../../../../../../../base/common/network.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js';
|
||||
import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { FileService } from '../../../../../../../platform/files/common/fileService.js';
|
||||
import { IFileService } from '../../../../../../../platform/files/common/files.js';
|
||||
import { InMemoryFileSystemProvider } from '../../../../../../../platform/files/common/inMemoryFilesystemProvider.js';
|
||||
import type { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { NullLogService } from '../../../../../../../platform/log/common/log.js';
|
||||
import { IWorkspaceContextService, toWorkspaceFolder } from '../../../../../../../platform/workspace/common/workspace.js';
|
||||
import { Workspace } from '../../../../../../../platform/workspace/test/common/testWorkspace.js';
|
||||
import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js';
|
||||
import { TestIPCFileSystemProvider } from '../../../../../../test/electron-browser/workbenchTestServices.js';
|
||||
import { TestContextService } from '../../../../../../test/common/workbenchTestServices.js';
|
||||
import { TerminalChatAgentToolsSettingId } from '../../../common/terminalChatAgentToolsConfiguration.js';
|
||||
import { NpmScriptAutoApprover } from '../../../browser/tools/commandLineAnalyzer/autoApprove/npmScriptAutoApprover.js';
|
||||
|
||||
suite('NpmScriptAutoApprover', () => {
|
||||
const store = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
let instantiationService: TestInstantiationService;
|
||||
let approver: NpmScriptAutoApprover;
|
||||
let configurationService: TestConfigurationService;
|
||||
let workspaceContextService: TestContextService;
|
||||
let fileService: FileService;
|
||||
let fileSystemProvider: InMemoryFileSystemProvider;
|
||||
|
||||
// Use inMemory scheme to avoid platform-specific path issues
|
||||
const cwd = URI.from({ scheme: Schemas.inMemory, path: '/workspace/project' });
|
||||
|
||||
setup(async () => {
|
||||
fileService = store.add(new FileService(new NullLogService()));
|
||||
|
||||
// Register file: scheme provider for tree-sitter WASM grammar loading
|
||||
store.add(fileService.registerProvider(Schemas.file, new TestIPCFileSystemProvider()));
|
||||
|
||||
// Register inMemory: scheme provider for test package.json files
|
||||
fileSystemProvider = store.add(new InMemoryFileSystemProvider());
|
||||
store.add(fileService.registerProvider(Schemas.inMemory, fileSystemProvider));
|
||||
|
||||
// Create workspace directory structure
|
||||
await fileService.createFolder(cwd);
|
||||
|
||||
configurationService = new TestConfigurationService();
|
||||
workspaceContextService = new TestContextService();
|
||||
|
||||
instantiationService = workbenchInstantiationService({
|
||||
fileService: () => fileService,
|
||||
configurationService: () => configurationService
|
||||
}, store);
|
||||
|
||||
instantiationService.stub(IWorkspaceContextService, workspaceContextService);
|
||||
instantiationService.stub(IFileService, fileService);
|
||||
|
||||
approver = store.add(instantiationService.createInstance(NpmScriptAutoApprover));
|
||||
|
||||
// Enable npm script auto-approve by default for tests
|
||||
configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.AutoApproveWorkspaceNpmScripts, true);
|
||||
|
||||
// Setup workspace
|
||||
const workspace = new Workspace('test', [toWorkspaceFolder(cwd)]);
|
||||
workspaceContextService.setWorkspace(workspace);
|
||||
});
|
||||
|
||||
async function writePackageJson(uri: URI, scripts: Record<string, string>) {
|
||||
const packageJson = {
|
||||
name: 'test-project',
|
||||
version: '1.0.0',
|
||||
scripts
|
||||
};
|
||||
await fileService.writeFile(uri, VSBuffer.fromString(JSON.stringify(packageJson, null, 2)));
|
||||
}
|
||||
|
||||
async function t(command: string, scripts: Record<string, string>, expectedAutoApproved: boolean) {
|
||||
const packageJsonUri = URI.joinPath(cwd, 'package.json');
|
||||
await writePackageJson(packageJsonUri, scripts);
|
||||
|
||||
const result = await approver.isCommandAutoApproved(command, cwd);
|
||||
strictEqual(result.isAutoApproved, expectedAutoApproved, `Expected isAutoApproved to be ${expectedAutoApproved} for: ${command}`);
|
||||
}
|
||||
|
||||
suite('npm run commands', () => {
|
||||
test('npm run build - script exists', () => t('npm run build', { build: 'tsc' }, true));
|
||||
test('npm run test - script exists', () => t('npm run test', { test: 'jest' }, true));
|
||||
test('npm run dev - script exists', () => t('npm run dev', { dev: 'vite' }, true));
|
||||
test('npm run start - script exists', () => t('npm run start', { start: 'node index.js' }, true));
|
||||
test('npm run lint - script exists', () => t('npm run lint', { lint: 'eslint .' }, true));
|
||||
test('npm run-script build - script exists', () => t('npm run-script build', { build: 'tsc' }, true));
|
||||
|
||||
// npm shorthand commands (npm test, npm start, npm stop, npm restart)
|
||||
test('npm test - shorthand script exists', () => t('npm test', { test: 'jest' }, true));
|
||||
test('npm start - shorthand script exists', () => t('npm start', { start: 'node index.js' }, true));
|
||||
test('npm stop - shorthand script exists', () => t('npm stop', { stop: 'pkill node' }, true));
|
||||
test('npm restart - shorthand script exists', () => t('npm restart', { restart: 'npm stop && npm start' }, true));
|
||||
test('npm test - shorthand script does not exist', () => t('npm test', { build: 'tsc' }, false));
|
||||
test('npm test -- --watch - shorthand with args', () => t('npm test -- --watch', { test: 'jest' }, true));
|
||||
test('npm startevil - word boundary prevents match', () => t('npm startevil', { start: 'node index.js', startevil: 'evil' }, false));
|
||||
test('npm install - built-in command, not a script', () => t('npm install', { install: 'echo should not match' }, false));
|
||||
|
||||
// Scripts with colons (namespaced scripts)
|
||||
test('npm run build:prod - script with colon exists', () => t('npm run build:prod', { 'build:prod': 'tsc --build' }, true));
|
||||
test('npm run test:unit - script with colon exists', () => t('npm run test:unit', { 'test:unit': 'jest --testPathPattern=unit' }, true));
|
||||
test('npm run lint:fix - script with colon exists', () => t('npm run lint:fix', { 'lint:fix': 'eslint . --fix' }, true));
|
||||
|
||||
test('npm run missing - script does not exist', () => t('npm run missing', { build: 'tsc' }, false));
|
||||
test('npm run build - no scripts section', async () => {
|
||||
const packageJsonUri = URI.joinPath(cwd, 'package.json');
|
||||
await fileService.writeFile(packageJsonUri, VSBuffer.fromString(JSON.stringify({ name: 'test' })));
|
||||
|
||||
const result = await approver.isCommandAutoApproved('npm run build', cwd);
|
||||
strictEqual(result.isAutoApproved, false);
|
||||
});
|
||||
});
|
||||
|
||||
suite('yarn commands', () => {
|
||||
test('yarn run build - script exists', () => t('yarn run build', { build: 'tsc' }, true));
|
||||
test('yarn run test - script exists', () => t('yarn run test', { test: 'jest' }, true));
|
||||
|
||||
// Yarn shorthand (yarn <script>)
|
||||
test('yarn build - script exists (shorthand)', () => t('yarn build', { build: 'tsc' }, true));
|
||||
test('yarn test - script exists (shorthand)', () => t('yarn test', { test: 'jest' }, true));
|
||||
|
||||
// Yarn built-in commands should not match
|
||||
test('yarn install - built-in command, not a script', () => t('yarn install', { install: 'echo should not match' }, false));
|
||||
test('yarn add - built-in command, not a script', () => t('yarn add lodash', { add: 'echo should not match' }, false));
|
||||
|
||||
test('yarn run missing - script does not exist', () => t('yarn run missing', { build: 'tsc' }, false));
|
||||
});
|
||||
|
||||
suite('pnpm commands', () => {
|
||||
test('pnpm run build - script exists', () => t('pnpm run build', { build: 'tsc' }, true));
|
||||
test('pnpm run test - script exists', () => t('pnpm run test', { test: 'jest' }, true));
|
||||
|
||||
// pnpm shorthand (pnpm <script>)
|
||||
test('pnpm build - script exists (shorthand)', () => t('pnpm build', { build: 'tsc' }, true));
|
||||
test('pnpm test - script exists (shorthand)', () => t('pnpm test', { test: 'jest' }, true));
|
||||
|
||||
// pnpm built-in commands should not match
|
||||
test('pnpm install - built-in command, not a script', () => t('pnpm install', { install: 'echo should not match' }, false));
|
||||
test('pnpm add - built-in command, not a script', () => t('pnpm add lodash', { add: 'echo should not match' }, false));
|
||||
|
||||
test('pnpm run missing - script does not exist', () => t('pnpm run missing', { build: 'tsc' }, false));
|
||||
});
|
||||
|
||||
suite('no package.json', () => {
|
||||
test('npm run build - no package.json file', async () => {
|
||||
const result = await approver.isCommandAutoApproved('npm run build', URI.from({ scheme: Schemas.inMemory, path: '/nonexistent/path' }));
|
||||
strictEqual(result.isAutoApproved, false);
|
||||
});
|
||||
});
|
||||
|
||||
suite('non-npm commands', () => {
|
||||
test('git status - not an npm command', () => t('git status', { build: 'tsc' }, false));
|
||||
test('ls -la - not an npm command', () => t('ls -la', { build: 'tsc' }, false));
|
||||
test('echo hello - not an npm command', () => t('echo hello', { build: 'tsc' }, false));
|
||||
});
|
||||
|
||||
suite('auto-approve disabled', () => {
|
||||
test('npm run build - npm script auto-approve setting disabled', async () => {
|
||||
configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.AutoApproveWorkspaceNpmScripts, false);
|
||||
|
||||
const packageJsonUri = URI.joinPath(cwd, 'package.json');
|
||||
await writePackageJson(packageJsonUri, { build: 'tsc' });
|
||||
|
||||
const result = await approver.isCommandAutoApproved('npm run build', cwd);
|
||||
strictEqual(result.isAutoApproved, false);
|
||||
});
|
||||
});
|
||||
|
||||
suite('autoApproveInfo message', () => {
|
||||
test('single script - message contains script name', async () => {
|
||||
const packageJsonUri = URI.joinPath(cwd, 'package.json');
|
||||
await writePackageJson(packageJsonUri, { build: 'tsc' });
|
||||
|
||||
const result = await approver.isCommandAutoApproved('npm run build', cwd);
|
||||
strictEqual(result.isAutoApproved, true);
|
||||
strictEqual(result.scriptName, 'build', 'Should return script name');
|
||||
strictEqual(result.autoApproveInfo?.value.includes('build'), true, 'Should mention script name');
|
||||
strictEqual(result.autoApproveInfo?.value.includes('package.json'), true, 'Should mention package.json');
|
||||
});
|
||||
});
|
||||
|
||||
suite('workspace folder security', () => {
|
||||
test('cwd outside workspace - does not auto-approve', async () => {
|
||||
// Create package.json outside workspace
|
||||
const outsideCwd = URI.from({ scheme: Schemas.inMemory, path: '/outside/project' });
|
||||
await fileService.createFolder(outsideCwd);
|
||||
const outsidePackageJsonUri = URI.joinPath(outsideCwd, 'package.json');
|
||||
await writePackageJson(outsidePackageJsonUri, { build: 'tsc' });
|
||||
|
||||
const result = await approver.isCommandAutoApproved('npm run build', outsideCwd);
|
||||
strictEqual(result.isAutoApproved, false, 'Should not auto-approve when cwd is outside workspace');
|
||||
});
|
||||
});
|
||||
});
|
||||
+156
-46
@@ -10,7 +10,7 @@ import { Emitter } from '../../../../../../base/common/event.js';
|
||||
import { Schemas } from '../../../../../../base/common/network.js';
|
||||
import { isLinux, isWindows, OperatingSystem } from '../../../../../../base/common/platform.js';
|
||||
import { count } from '../../../../../../base/common/strings.js';
|
||||
import type { SingleOrMany } from '../../../../../../base/common/types.js';
|
||||
import { hasKey, type SingleOrMany } from '../../../../../../base/common/types.js';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
import { ITreeSitterLibraryService } from '../../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js';
|
||||
@@ -82,6 +82,9 @@ suite('RunInTerminalTool', () => {
|
||||
fileService: () => fileService,
|
||||
}, store);
|
||||
|
||||
instantiationService.stub(IChatService, {
|
||||
onDidDisposeSession: chatServiceDisposeEmitter.event
|
||||
});
|
||||
instantiationService.stub(ITerminalChatService, store.add(instantiationService.createInstance(TerminalChatService)));
|
||||
instantiationService.stub(IWorkspaceContextService, workspaceContextService);
|
||||
instantiationService.stub(IHistoryService, {
|
||||
@@ -101,9 +104,6 @@ suite('RunInTerminalTool', () => {
|
||||
onDidDisposeInstance: terminalServiceDisposeEmitter.event,
|
||||
setNextCommandId: async () => { }
|
||||
});
|
||||
instantiationService.stub(IChatService, {
|
||||
onDidDisposeSession: chatServiceDisposeEmitter.event
|
||||
});
|
||||
instantiationService.stub(ITerminalProfileResolverService, {
|
||||
getDefaultProfile: async () => ({ path: 'bash' } as ITerminalProfile)
|
||||
});
|
||||
@@ -227,6 +227,7 @@ suite('RunInTerminalTool', () => {
|
||||
'Get-Location',
|
||||
'Write-Host "Hello"',
|
||||
'Write-Output "Test"',
|
||||
'Out-String',
|
||||
'Split-Path C:\\Users\\test',
|
||||
'Join-Path C:\\Users test',
|
||||
'Start-Sleep 2',
|
||||
@@ -493,7 +494,9 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
suite('prepareToolInvocation - custom actions for dropdown', () => {
|
||||
|
||||
function assertDropdownActions(result: IPreparedToolInvocation | undefined, items: ({ subCommand: SingleOrMany<string> } | 'commandLine' | '---' | 'configure' | 'sessionApproval')[]) {
|
||||
type ActionItemType = { subCommand: SingleOrMany<string>; scope: 'session' | 'workspace' | 'user' } | { commandLine: true; scope: 'session' | 'workspace' | 'user' } | '---' | 'configure' | 'sessionApproval';
|
||||
|
||||
function assertDropdownActions(result: IPreparedToolInvocation | undefined, items: ActionItemType[]) {
|
||||
const actions = result?.confirmationMessages?.terminalCustomActions!;
|
||||
ok(actions, 'Expected custom actions to be defined');
|
||||
|
||||
@@ -511,16 +514,21 @@ suite('RunInTerminalTool', () => {
|
||||
} else if (item === 'sessionApproval') {
|
||||
strictEqual(action.label, 'Allow All Commands in this Session');
|
||||
strictEqual(action.data.type, 'sessionApproval');
|
||||
} else if (item === 'commandLine') {
|
||||
strictEqual(action.label, 'Always Allow Exact Command Line');
|
||||
} else if (hasKey(item, { commandLine: true })) {
|
||||
const expectedLabel = item.scope === 'session' ? 'Allow Exact Command Line in this Session'
|
||||
: item.scope === 'workspace' ? 'Allow Exact Command Line in this Workspace'
|
||||
: 'Always Allow Exact Command Line';
|
||||
strictEqual(action.label, expectedLabel);
|
||||
strictEqual(action.data.type, 'newRule');
|
||||
ok(!Array.isArray(action.data.rule), 'Expected rule to be an object');
|
||||
} else {
|
||||
if (Array.isArray(item.subCommand)) {
|
||||
strictEqual(action.label, `Always Allow Commands: ${item.subCommand.join(', ')}`);
|
||||
} else {
|
||||
strictEqual(action.label, `Always Allow Command: ${item.subCommand}`);
|
||||
}
|
||||
const subCommandLabel = Array.isArray(item.subCommand)
|
||||
? `Commands ${item.subCommand.map(e => `\`${e} \u2026\``).join(', ')}`
|
||||
: `\`${item.subCommand} \u2026\``;
|
||||
const expectedLabel = item.scope === 'session' ? `Allow ${subCommandLabel} in this Session`
|
||||
: item.scope === 'workspace' ? `Allow ${subCommandLabel} in this Workspace`
|
||||
: `Always Allow ${subCommandLabel}`;
|
||||
strictEqual(action.label, expectedLabel);
|
||||
strictEqual(action.data.type, 'newRule');
|
||||
ok(Array.isArray(action.data.rule), 'Expected rule to be an array');
|
||||
}
|
||||
@@ -539,8 +547,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'npm run build' },
|
||||
'commandLine',
|
||||
{ subCommand: 'npm run build', scope: 'session' },
|
||||
{ subCommand: 'npm run build', scope: 'workspace' },
|
||||
{ subCommand: 'npm run build', scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -556,7 +569,10 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'foo' },
|
||||
{ subCommand: 'foo', scope: 'session' },
|
||||
{ subCommand: 'foo', scope: 'workspace' },
|
||||
{ subCommand: 'foo', scope: 'user' },
|
||||
'---',
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -601,8 +617,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: ['npm install', 'npm run build'] },
|
||||
'commandLine',
|
||||
{ subCommand: ['npm install', 'npm run build'], scope: 'session' },
|
||||
{ subCommand: ['npm install', 'npm run build'], scope: 'workspace' },
|
||||
{ subCommand: ['npm install', 'npm run build'], scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -621,8 +642,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'foo' },
|
||||
'commandLine',
|
||||
{ subCommand: 'foo', scope: 'session' },
|
||||
{ subCommand: 'foo', scope: 'workspace' },
|
||||
{ subCommand: 'foo', scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -655,8 +681,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: ['foo', 'bar'] },
|
||||
'commandLine',
|
||||
{ subCommand: ['foo', 'bar'], scope: 'session' },
|
||||
{ subCommand: ['foo', 'bar'], scope: 'workspace' },
|
||||
{ subCommand: ['foo', 'bar'], scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -672,8 +703,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'git status' },
|
||||
'commandLine',
|
||||
{ subCommand: 'git status', scope: 'session' },
|
||||
{ subCommand: 'git status', scope: 'workspace' },
|
||||
{ subCommand: 'git status', scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -689,8 +725,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'npm test' },
|
||||
'commandLine',
|
||||
{ subCommand: 'npm test', scope: 'session' },
|
||||
{ subCommand: 'npm test', scope: 'workspace' },
|
||||
{ subCommand: 'npm test', scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -706,8 +747,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'npm run build' },
|
||||
'commandLine',
|
||||
{ subCommand: 'npm run build', scope: 'session' },
|
||||
{ subCommand: 'npm run build', scope: 'workspace' },
|
||||
{ subCommand: 'npm run build', scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -723,8 +769,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'yarn run test' },
|
||||
'commandLine',
|
||||
{ subCommand: 'yarn run test', scope: 'session' },
|
||||
{ subCommand: 'yarn run test', scope: 'workspace' },
|
||||
{ subCommand: 'yarn run test', scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -740,8 +791,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'foo' },
|
||||
'commandLine',
|
||||
{ subCommand: 'foo', scope: 'session' },
|
||||
{ subCommand: 'foo', scope: 'workspace' },
|
||||
{ subCommand: 'foo', scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -757,8 +813,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'npm run abc' },
|
||||
'commandLine',
|
||||
{ subCommand: 'npm run abc', scope: 'session' },
|
||||
{ subCommand: 'npm run abc', scope: 'workspace' },
|
||||
{ subCommand: 'npm run abc', scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -774,8 +835,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: ['npm run build', 'git status'] },
|
||||
'commandLine',
|
||||
{ subCommand: ['npm run build', 'git status'], scope: 'session' },
|
||||
{ subCommand: ['npm run build', 'git status'], scope: 'workspace' },
|
||||
{ subCommand: ['npm run build', 'git status'], scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -791,8 +857,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: ['git push', 'echo'] },
|
||||
'commandLine',
|
||||
{ subCommand: ['git push', 'echo'], scope: 'session' },
|
||||
{ subCommand: ['git push', 'echo'], scope: 'workspace' },
|
||||
{ subCommand: ['git push', 'echo'], scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -808,8 +879,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: ['git status', 'git log'] },
|
||||
'commandLine',
|
||||
{ subCommand: ['git status', 'git log'], scope: 'session' },
|
||||
{ subCommand: ['git status', 'git log'], scope: 'workspace' },
|
||||
{ subCommand: ['git status', 'git log'], scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -825,8 +901,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'foo' },
|
||||
'commandLine',
|
||||
{ subCommand: 'foo', scope: 'session' },
|
||||
{ subCommand: 'foo', scope: 'workspace' },
|
||||
{ subCommand: 'foo', scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -856,8 +937,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'npm test' },
|
||||
'commandLine',
|
||||
{ subCommand: 'npm test', scope: 'session' },
|
||||
{ subCommand: 'npm test', scope: 'workspace' },
|
||||
{ subCommand: 'npm test', scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -873,8 +959,13 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'foo' },
|
||||
'commandLine',
|
||||
{ subCommand: 'foo', scope: 'session' },
|
||||
{ subCommand: 'foo', scope: 'workspace' },
|
||||
{ subCommand: 'foo', scope: 'user' },
|
||||
'---',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -890,7 +981,9 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
assertConfirmationRequired(result);
|
||||
assertDropdownActions(result, [
|
||||
'commandLine',
|
||||
{ commandLine: true, scope: 'session' },
|
||||
{ commandLine: true, scope: 'workspace' },
|
||||
{ commandLine: true, scope: 'user' },
|
||||
'---',
|
||||
'sessionApproval',
|
||||
'---',
|
||||
@@ -1018,6 +1111,23 @@ suite('RunInTerminalTool', () => {
|
||||
assertConfirmationRequired(await executeToolTest({ command: 'echo hello world' }), 'Run `bash` command?');
|
||||
});
|
||||
|
||||
test('should include autoApproveInfo when command would be auto-approved but warning not accepted', async () => {
|
||||
setConfig(TerminalChatAgentToolsSettingId.EnableAutoApprove, true);
|
||||
setAutoApprove({
|
||||
echo: true
|
||||
});
|
||||
|
||||
clearAutoApproveWarningAcceptedState();
|
||||
|
||||
const result = await executeToolTest({ command: 'echo hello world' });
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
|
||||
// autoApproveInfo should be set so the confirmation widget knows to auto-approve
|
||||
// after the user accepts the warning modal
|
||||
const terminalData = result!.toolSpecificData as IChatTerminalToolInvocationData;
|
||||
ok(terminalData.autoApproveInfo, 'autoApproveInfo should be set for commands that would be auto-approved');
|
||||
});
|
||||
|
||||
test('should auto-approve commands when both auto-approve enabled and warning accepted', async () => {
|
||||
setConfig(TerminalChatAgentToolsSettingId.EnableAutoApprove, true);
|
||||
setAutoApprove({
|
||||
|
||||
@@ -484,10 +484,11 @@ suite('Terminal history', () => {
|
||||
|
||||
if (!isWindows) {
|
||||
suite('local', () => {
|
||||
let originalEnvValues: { HOME: string | undefined };
|
||||
let originalEnvValues: { HOME: string | undefined; XDG_DATA_HOME: string | undefined };
|
||||
setup(() => {
|
||||
originalEnvValues = { HOME: env['HOME'] };
|
||||
originalEnvValues = { HOME: env['HOME'], XDG_DATA_HOME: env['XDG_DATA_HOME'] };
|
||||
env['HOME'] = '/home/user';
|
||||
delete env['XDG_DATA_HOME'];
|
||||
remoteConnection = { remoteAuthority: 'some-remote' };
|
||||
fileScheme = Schemas.vscodeRemote;
|
||||
filePath = '/home/user/.local/share/fish/fish_history';
|
||||
@@ -498,6 +499,11 @@ suite('Terminal history', () => {
|
||||
} else {
|
||||
env['HOME'] = originalEnvValues['HOME'];
|
||||
}
|
||||
if (originalEnvValues['XDG_DATA_HOME'] === undefined) {
|
||||
delete env['XDG_DATA_HOME'];
|
||||
} else {
|
||||
env['XDG_DATA_HOME'] = originalEnvValues['XDG_DATA_HOME'];
|
||||
}
|
||||
});
|
||||
test('current OS', async () => {
|
||||
filePath = '/home/user/.local/share/fish/fish_history';
|
||||
@@ -528,10 +534,11 @@ suite('Terminal history', () => {
|
||||
});
|
||||
}
|
||||
suite('remote', () => {
|
||||
let originalEnvValues: { HOME: string | undefined };
|
||||
let originalEnvValues: { HOME: string | undefined; XDG_DATA_HOME: string | undefined };
|
||||
setup(() => {
|
||||
originalEnvValues = { HOME: env['HOME'] };
|
||||
originalEnvValues = { HOME: env['HOME'], XDG_DATA_HOME: env['XDG_DATA_HOME'] };
|
||||
env['HOME'] = '/home/user';
|
||||
delete env['XDG_DATA_HOME'];
|
||||
remoteConnection = { remoteAuthority: 'some-remote' };
|
||||
fileScheme = Schemas.vscodeRemote;
|
||||
filePath = '/home/user/.local/share/fish/fish_history';
|
||||
@@ -542,6 +549,11 @@ suite('Terminal history', () => {
|
||||
} else {
|
||||
env['HOME'] = originalEnvValues['HOME'];
|
||||
}
|
||||
if (originalEnvValues['XDG_DATA_HOME'] === undefined) {
|
||||
delete env['XDG_DATA_HOME'];
|
||||
} else {
|
||||
env['XDG_DATA_HOME'] = originalEnvValues['XDG_DATA_HOME'];
|
||||
}
|
||||
});
|
||||
test('Windows', async () => {
|
||||
remoteEnvironment = { os: OperatingSystem.Windows };
|
||||
|
||||
+9
-8
@@ -50,7 +50,7 @@ export class TerminalStickyScrollOverlay extends Disposable {
|
||||
|
||||
private readonly _xtermAddonLoader = new XtermAddonImporter();
|
||||
private _serializeAddon?: SerializeAddonType;
|
||||
private _webglAddon?: WebglAddonType;
|
||||
private readonly _webglAddon: MutableDisposable<WebglAddonType> = this._register(new MutableDisposable());
|
||||
private _webglAddonCustomGlyphs?: boolean;
|
||||
private _ligaturesAddon?: LigaturesAddonType;
|
||||
|
||||
@@ -497,18 +497,19 @@ export class TerminalStickyScrollOverlay extends Disposable {
|
||||
|
||||
@throttle(0)
|
||||
private async _refreshGpuAcceleration() {
|
||||
if (this._shouldLoadWebgl() && (!this._webglAddon || this._webglAddonCustomGlyphs !== this._terminalConfigurationService.config.customGlyphs)) {
|
||||
if (this._shouldLoadWebgl() && (!this._webglAddon.value || this._webglAddonCustomGlyphs !== this._terminalConfigurationService.config.customGlyphs)) {
|
||||
const WebglAddon = await this._xtermAddonLoader.importAddon('webgl');
|
||||
if (this._store.isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._webglAddon = this._register(new WebglAddon({
|
||||
// Dispose of existing addon before creating a new one to avoid leaking WebGL contexts
|
||||
this._webglAddon.value = new WebglAddon({
|
||||
customGlyphs: this._terminalConfigurationService.config.customGlyphs
|
||||
}));
|
||||
this._stickyScrollOverlay?.loadAddon(this._webglAddon);
|
||||
} else if (!this._shouldLoadWebgl() && this._webglAddon) {
|
||||
this._webglAddon.dispose();
|
||||
this._webglAddon = undefined;
|
||||
});
|
||||
this._webglAddonCustomGlyphs = this._terminalConfigurationService.config.customGlyphs;
|
||||
this._stickyScrollOverlay?.loadAddon(this._webglAddon.value);
|
||||
} else if (!this._shouldLoadWebgl() && this._webglAddon.value) {
|
||||
this._webglAddon.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+60
-3
@@ -269,6 +269,11 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
|
||||
const resourceCompletions: ITerminalCompletion[] = [];
|
||||
const cursorPrefix = promptValue.substring(0, cursorPosition);
|
||||
|
||||
// Determine if we're completing the command (first word) vs an argument
|
||||
// We're in command position if there are no unescaped spaces before cursor
|
||||
const wordsBeforeCursor = cursorPrefix.split(/(?<!\\) /);
|
||||
const isCommandPosition = wordsBeforeCursor.length <= 1 && !cursorPrefix.endsWith(' ');
|
||||
|
||||
// TODO: Leverage Fig's tokens array here?
|
||||
// The last word (or argument). When the cursor is following a space it will be the empty
|
||||
// string
|
||||
@@ -309,12 +314,55 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
|
||||
|
||||
|
||||
// Determine the current folder being shown
|
||||
let lastWordFolderResource: URI | string | undefined;
|
||||
const lastWordFolderHasDotPrefix = !!lastWordFolder.match(/^\.\.?[\\\/]/);
|
||||
const lastWordFolderHasTildePrefix = !!lastWordFolder.match(/^~[\\\/]?/);
|
||||
const isAbsolutePath = getIsAbsolutePath(shellType, resourceOptions.pathSeparator, lastWordFolder, useWindowsStylePath);
|
||||
const type = lastWordFolderHasTildePrefix ? 'tilde' : isAbsolutePath ? 'absolute' : 'relative';
|
||||
const cwd = URI.revive(resourceOptions.cwd);
|
||||
let lastWordFolderResource: URI | string | undefined;
|
||||
if (type === 'relative' && lastWordFolder.length > 0) {
|
||||
// If the typed folder matches the tail of cwd (common when the extension already
|
||||
// resolved the path, such as `./src/vs/`), reuse cwd to avoid duplicating segments.
|
||||
const normalizedFolder = (useWindowsStylePath ? lastWordFolder.replaceAll('\\', '/') : lastWordFolder).replaceAll('\\ ', ' ');
|
||||
const hasDotPrefix = normalizedFolder.startsWith('./');
|
||||
if (hasDotPrefix) {
|
||||
const stripped = normalizedFolder.replace(/^\.\/+/, '').replace(/\/+$/, '');
|
||||
if (stripped) {
|
||||
const cwdParts = cwd.path.replace(/\/+$/, '').split('/');
|
||||
const strippedParts = stripped.split('/');
|
||||
const tailMatches = strippedParts.length <= cwdParts.length && strippedParts.every((part, idx) => cwdParts[cwdParts.length - strippedParts.length + idx] === part);
|
||||
if (tailMatches) {
|
||||
try {
|
||||
await this._fileService.stat(cwd);
|
||||
lastWordFolderResource = cwd;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise resolve the folder relative to cwd.
|
||||
if (!lastWordFolderResource) {
|
||||
const folderToResolve = URI.joinPath(cwd, normalizedFolder);
|
||||
try {
|
||||
await this._fileService.stat(folderToResolve);
|
||||
lastWordFolderResource = folderToResolve;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
} else if (type === 'relative') {
|
||||
lastWordFolderResource = cwd;
|
||||
}
|
||||
if (type === 'relative' && !lastWordFolderResource) {
|
||||
try {
|
||||
await this._fileService.stat(cwd);
|
||||
lastWordFolderResource = cwd;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'tilde': {
|
||||
@@ -340,7 +388,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
|
||||
break;
|
||||
}
|
||||
case 'relative': {
|
||||
lastWordFolderResource = cwd;
|
||||
lastWordFolderResource ??= cwd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -363,7 +411,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
|
||||
return resourceCompletions;
|
||||
}
|
||||
|
||||
const stat = await this._fileService.resolve(lastWordFolderResource, { resolveSingleChildDescendants: true });
|
||||
const stat = await this._fileService.resolve(lastWordFolderResource, {
|
||||
resolveMetadata: true,
|
||||
resolveSingleChildDescendants: true
|
||||
});
|
||||
if (!stat?.children) {
|
||||
return;
|
||||
}
|
||||
@@ -425,6 +476,12 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
|
||||
kind = TerminalCompletionItemKind.Folder;
|
||||
}
|
||||
} else if (showFiles && child.isFile) {
|
||||
// When completing the command (first word) on Unix, only show executable files
|
||||
if (isCommandPosition && !useWindowsStylePath) {
|
||||
if (!child.executable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (child.isSymbolicLink) {
|
||||
kind = TerminalCompletionItemKind.SymbolicLinkFile;
|
||||
} else {
|
||||
|
||||
+58
-12
@@ -96,7 +96,7 @@ suite('TerminalCompletionService', () => {
|
||||
let configurationService: TestConfigurationService;
|
||||
let capabilities: TerminalCapabilityStore;
|
||||
let validResources: URI[];
|
||||
let childResources: { resource: URI; isFile?: boolean; isDirectory?: boolean; isSymbolicLink?: boolean }[];
|
||||
let childResources: { resource: URI; isFile?: boolean; isDirectory?: boolean; isSymbolicLink?: boolean; executable?: boolean }[];
|
||||
let terminalCompletionService: TerminalCompletionService;
|
||||
const provider = 'testProvider';
|
||||
|
||||
@@ -104,17 +104,22 @@ suite('TerminalCompletionService', () => {
|
||||
instantiationService = workbenchInstantiationService({
|
||||
pathService: () => new TestPathService(URI.file(homeDir ?? '/')),
|
||||
}, store);
|
||||
const normalizePath = (path: string) => path === '/' ? path : path.replace(/\/+$/, '');
|
||||
const doesResourceExist = (resource: URI) => validResources.some(e => normalizePath(e.path) === normalizePath(resource.path)) || childResources.some(e => normalizePath(e.resource.path) === normalizePath(resource.path));
|
||||
configurationService = new TestConfigurationService();
|
||||
instantiationService.stub(ITerminalLogService, new NullLogService());
|
||||
instantiationService.stub(IConfigurationService, configurationService);
|
||||
instantiationService.stub(IFileService, {
|
||||
async stat(resource) {
|
||||
if (!validResources.map(e => e.path).includes(resource.path)) {
|
||||
if (!doesResourceExist(resource)) {
|
||||
throw new Error('Doesn\'t exist');
|
||||
}
|
||||
return createFileStat(resource);
|
||||
},
|
||||
async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata> {
|
||||
if (!doesResourceExist(resource)) {
|
||||
throw new Error('Doesn\'t exist');
|
||||
}
|
||||
const children = childResources.filter(child => {
|
||||
const childFsPath = child.resource.path.replace(/\/$/, '');
|
||||
const parentFsPath = resource.path.replace(/\/$/, '');
|
||||
@@ -227,10 +232,10 @@ suite('TerminalCompletionService', () => {
|
||||
setup(() => {
|
||||
validResources = [URI.parse('file:///test')];
|
||||
childResources = [
|
||||
{ resource: URI.parse('file:///test/.hiddenFile'), isFile: true },
|
||||
{ resource: URI.parse('file:///test/.hiddenFile'), isFile: true, executable: true },
|
||||
{ resource: URI.parse('file:///test/.hiddenFolder/'), isDirectory: true },
|
||||
{ resource: URI.parse('file:///test/folder1/'), isDirectory: true },
|
||||
{ resource: URI.parse('file:///test/file1.txt'), isFile: true },
|
||||
{ resource: URI.parse('file:///test/file1.txt'), isFile: true, executable: true },
|
||||
];
|
||||
});
|
||||
|
||||
@@ -302,7 +307,7 @@ suite('TerminalCompletionService', () => {
|
||||
childResources = [
|
||||
{ resource: URI.parse('file:///home/vscode'), isDirectory: true },
|
||||
{ resource: URI.parse('file:///home/vscode/foo'), isDirectory: true },
|
||||
{ resource: URI.parse('file:///home/vscode/bar.txt'), isFile: true },
|
||||
{ resource: URI.parse('file:///home/vscode/bar.txt'), isFile: true, executable: true },
|
||||
];
|
||||
});
|
||||
|
||||
@@ -484,6 +489,22 @@ suite('TerminalCompletionService', () => {
|
||||
], { replacementRange: [0, 2] });
|
||||
});
|
||||
|
||||
test('should not return completions when relative folder prefix does not exist', async () => {
|
||||
const resourceOptions: TerminalCompletionResourceOptions = {
|
||||
cwd: URI.parse('file:///test'),
|
||||
showDirectories: true,
|
||||
pathSeparator
|
||||
};
|
||||
validResources = [URI.parse('file:///test')];
|
||||
childResources = [
|
||||
{ resource: URI.parse('file:///test/src/'), isDirectory: true },
|
||||
{ resource: URI.parse('file:///test/vs/'), isDirectory: true }
|
||||
];
|
||||
const result = await terminalCompletionService.resolveResources(resourceOptions, 's/', 2, provider, capabilities);
|
||||
|
||||
assert.strictEqual(result, undefined);
|
||||
});
|
||||
|
||||
test('./| should handle large directories with many results gracefully', async () => {
|
||||
const resourceOptions: TerminalCompletionResourceOptions = {
|
||||
cwd: URI.parse('file:///test'),
|
||||
@@ -524,6 +545,28 @@ suite('TerminalCompletionService', () => {
|
||||
{ label: './../', detail: '/' }
|
||||
], { replacementRange: [1, 10] });
|
||||
});
|
||||
test('should resolve nested folder when name matches cwd basename', async () => {
|
||||
const resourceOptions: TerminalCompletionResourceOptions = {
|
||||
cwd: URI.parse('file:///test'),
|
||||
showDirectories: true,
|
||||
pathSeparator
|
||||
};
|
||||
validResources = [
|
||||
URI.parse('file:///test'),
|
||||
URI.parse('file:///test/test'),
|
||||
];
|
||||
childResources = [
|
||||
{ resource: URI.parse('file:///test/test/'), isDirectory: true },
|
||||
{ resource: URI.parse('file:///test/test/inner/'), isDirectory: true }
|
||||
];
|
||||
const result = await terminalCompletionService.resolveResources(resourceOptions, 'test/', 5, provider, capabilities);
|
||||
|
||||
assertCompletions(result, [
|
||||
{ label: './test/', detail: '/test/test/' },
|
||||
{ label: './test/inner/', detail: '/test/test/inner/' },
|
||||
{ label: './test/../', detail: '/' }
|
||||
], { replacementRange: [0, 5] });
|
||||
});
|
||||
test('test/| should normalize current and parent folders', async () => {
|
||||
const resourceOptions: TerminalCompletionResourceOptions = {
|
||||
cwd: URI.parse('file:///test'),
|
||||
@@ -539,14 +582,14 @@ suite('TerminalCompletionService', () => {
|
||||
{ resource: URI.parse('file:///test/folder1/'), isDirectory: true },
|
||||
{ resource: URI.parse('file:///test/folder2/'), isDirectory: true }
|
||||
];
|
||||
const result = await terminalCompletionService.resolveResources(resourceOptions, 'test/', 5, provider, capabilities);
|
||||
const result = await terminalCompletionService.resolveResources(resourceOptions, './test/', 7, provider, capabilities);
|
||||
|
||||
assertCompletions(result, [
|
||||
{ label: './test/', detail: '/test/' },
|
||||
{ label: './test/folder1/', detail: '/test/folder1/' },
|
||||
{ label: './test/folder2/', detail: '/test/folder2/' },
|
||||
{ label: './test/../', detail: '/' }
|
||||
], { replacementRange: [0, 5] });
|
||||
], { replacementRange: [0, 7] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -554,7 +597,10 @@ suite('TerminalCompletionService', () => {
|
||||
let shellEnvDetection: ShellEnvDetectionCapability;
|
||||
|
||||
setup(() => {
|
||||
validResources = [URI.parse('file:///test')];
|
||||
validResources = [
|
||||
URI.parse('file:///test'),
|
||||
URI.parse('file:///cdpath_value')
|
||||
];
|
||||
childResources = [
|
||||
{ resource: URI.parse('file:///cdpath_value/folder1/'), isDirectory: true },
|
||||
{ resource: URI.parse('file:///cdpath_value/file1.txt'), isFile: true },
|
||||
@@ -666,7 +712,7 @@ suite('TerminalCompletionService', () => {
|
||||
];
|
||||
childResources = [
|
||||
{ resource: URI.file('C:\\Users\\foo\\bar'), isDirectory: true, isFile: false },
|
||||
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true }
|
||||
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true, executable: true }
|
||||
];
|
||||
const result = await terminalCompletionService.resolveResources(resourceOptions, 'C:/Users/foo/', 13, provider, capabilities, WindowsShellType.GitBash);
|
||||
assertCompletions(result, [
|
||||
@@ -689,7 +735,7 @@ suite('TerminalCompletionService', () => {
|
||||
];
|
||||
childResources = [
|
||||
{ resource: URI.file('C:\\Users\\foo\\bar'), isDirectory: true },
|
||||
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true }
|
||||
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true, executable: true }
|
||||
];
|
||||
const result = await terminalCompletionService.resolveResources(resourceOptions, './', 2, provider, capabilities, WindowsShellType.GitBash);
|
||||
assertCompletions(result, [
|
||||
@@ -714,7 +760,7 @@ suite('TerminalCompletionService', () => {
|
||||
];
|
||||
childResources = [
|
||||
{ resource: URI.file('C:\\Users\\foo\\bar'), isDirectory: true },
|
||||
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true }
|
||||
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true, executable: true }
|
||||
];
|
||||
const result = await terminalCompletionService.resolveResources(resourceOptions, '/c/Users/foo/', 13, provider, capabilities, WindowsShellType.GitBash);
|
||||
assertCompletions(result, [
|
||||
@@ -768,7 +814,7 @@ suite('TerminalCompletionService', () => {
|
||||
{ resource: URI.parse('file:///test/[folder1]/'), isDirectory: true },
|
||||
{ resource: URI.parse('file:///test/folder 2/'), isDirectory: true },
|
||||
{ resource: URI.parse('file:///test/!special$chars&/'), isDirectory: true },
|
||||
{ resource: URI.parse('file:///test/!special$chars2&'), isFile: true }
|
||||
{ resource: URI.parse('file:///test/!special$chars2&'), isFile: true, executable: true }
|
||||
];
|
||||
const result = await terminalCompletionService.resolveResources(resourceOptions, '', 0, provider, capabilities);
|
||||
|
||||
|
||||
@@ -2319,6 +2319,7 @@ suite('EditorService', () => {
|
||||
isSymbolicLink: false,
|
||||
readonly: false,
|
||||
locked: false,
|
||||
executable: false,
|
||||
children: undefined
|
||||
}));
|
||||
await activeEditorChangePromise;
|
||||
|
||||
@@ -512,6 +512,7 @@ suite('HistoryService', function () {
|
||||
name: 'other.txt',
|
||||
readonly: false,
|
||||
locked: false,
|
||||
executable: false,
|
||||
size: 0,
|
||||
resource: toResource.call(this, '/path/other.txt'),
|
||||
children: undefined
|
||||
|
||||
@@ -109,7 +109,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
}
|
||||
|
||||
readonly defaultKeybindingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/keybindings.json' });
|
||||
private readonly defaultSettingsRawResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/defaultSettings.json' });
|
||||
private readonly defaultSettingsRawResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/defaultSettings.jsonc' });
|
||||
|
||||
get userSettingsResource(): URI {
|
||||
return this.userDataProfileService.currentProfile.settingsResource;
|
||||
|
||||
@@ -371,7 +371,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
value: buffer,
|
||||
encoding: preferredEncoding.encoding,
|
||||
readonly: false,
|
||||
locked: false
|
||||
locked: false,
|
||||
executable: false
|
||||
}, true /* dirty (resolved from buffer) */, options);
|
||||
}
|
||||
|
||||
@@ -419,7 +420,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
value: await createTextBufferFactoryFromStream(await this.textFileService.getDecodedStream(this.resource, backup.value, { encoding: UTF8 })),
|
||||
encoding,
|
||||
readonly: false,
|
||||
locked: false
|
||||
locked: false,
|
||||
executable: false
|
||||
}, true /* dirty (resolved from backup) */, options);
|
||||
|
||||
// Restore orphaned flag based on state
|
||||
@@ -517,6 +519,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
etag: content.etag,
|
||||
readonly: content.readonly,
|
||||
locked: content.locked,
|
||||
executable: false,
|
||||
isFile: true,
|
||||
isDirectory: false,
|
||||
isSymbolicLink: false,
|
||||
|
||||
@@ -527,7 +527,8 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
|
||||
etag,
|
||||
value: buffer,
|
||||
readonly: false,
|
||||
locked: false
|
||||
locked: false,
|
||||
executable: false
|
||||
}, true /* dirty (resolved from buffer) */);
|
||||
}
|
||||
|
||||
@@ -568,7 +569,8 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
|
||||
etag: backup.meta ? backup.meta.etag : ETAG_DISABLED, // etag disabled if unknown!
|
||||
value: backup.value,
|
||||
readonly: false,
|
||||
locked: false
|
||||
locked: false,
|
||||
executable: false
|
||||
}, true /* dirty (resolved from backup) */);
|
||||
|
||||
// Restore orphaned flag based on state
|
||||
@@ -664,6 +666,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
|
||||
etag: content.etag,
|
||||
readonly: content.readonly,
|
||||
locked: content.locked,
|
||||
executable: false,
|
||||
isFile: true,
|
||||
isDirectory: false,
|
||||
isSymbolicLink: false,
|
||||
|
||||
@@ -118,6 +118,7 @@ export class TestStoredFileWorkingCopyModelWithCustomSave extends TestStoredFile
|
||||
isSymbolicLink: false,
|
||||
readonly: false,
|
||||
locked: false,
|
||||
executable: false,
|
||||
children: undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -487,7 +487,8 @@ export class TestTextFileService extends BrowserTextFileService {
|
||||
value: await createTextBufferFactoryFromStream(content.value),
|
||||
size: 10,
|
||||
readonly: false,
|
||||
locked: false
|
||||
locked: false,
|
||||
executable: false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy {
|
||||
}
|
||||
}
|
||||
|
||||
export function createFileStat(resource: URI, readonly = false, isFile?: boolean, isDirectory?: boolean, isSymbolicLink?: boolean, children?: { resource: URI; isFile?: boolean; isDirectory?: boolean; isSymbolicLink?: boolean }[] | undefined): IFileStatWithMetadata {
|
||||
export function createFileStat(resource: URI, readonly = false, isFile?: boolean, isDirectory?: boolean, isSymbolicLink?: boolean, children?: { resource: URI; isFile?: boolean; isDirectory?: boolean; isSymbolicLink?: boolean; executable?: boolean }[] | undefined, executable?: boolean): IFileStatWithMetadata {
|
||||
return {
|
||||
resource,
|
||||
etag: Date.now().toString(),
|
||||
@@ -257,8 +257,9 @@ export function createFileStat(resource: URI, readonly = false, isFile?: boolean
|
||||
isSymbolicLink: isSymbolicLink ?? false,
|
||||
readonly,
|
||||
locked: false,
|
||||
executable: executable ?? false,
|
||||
name: basename(resource),
|
||||
children: children?.map(c => createFileStat(c.resource, false, c.isFile, c.isDirectory, c.isSymbolicLink)),
|
||||
children: children?.map(c => createFileStat(c.resource, false, c.isFile, c.isDirectory, c.isSymbolicLink, undefined, c.executable)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user