diff --git a/.vscode/sessions.json b/.vscode/sessions.json new file mode 100644 index 0000000000000..539b14b5dd49e --- /dev/null +++ b/.vscode/sessions.json @@ -0,0 +1,28 @@ +{ + "scripts": [ + { + "name": "run Windows", + "command": "code.bat" + }, + { + "name": "run macOS", + "command": "code.sh" + }, + { + "name": "run Linux", + "command": "code.sh" + }, + { + "name": "run tests Windows", + "command": "test.bat" + }, + { + "name": "run tests macOS", + "command": "test.sh" + }, + { + "name": "run tests Linux", + "command": "test.sh" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8d0d6552d05ec..03fe5fb263fc0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -26,7 +26,7 @@ "message": 4 }, "background": { - "beginsPattern": "Starting transpilation...", + "beginsPattern": "Starting transpilation\\.\\.\\.", "endsPattern": "Finished transpilation with" } } diff --git a/build/hygiene.ts b/build/hygiene.ts index 8778907f13f63..936b0cbe63063 100644 --- a/build/hygiene.ts +++ b/build/hygiene.ts @@ -70,7 +70,7 @@ export function hygiene(some: NodeJS.ReadWriteStream | string[] | undefined, run } // Please do not add symbols that resemble ASCII letters! // eslint-disable-next-line no-misleading-character-class - const m = /([^\t\n\r\x20-\x7E⊃⊇✔︎✓🎯🧪✍️⚠️🛑🔴🚗🚙🚕🎉✨❗⇧⌥⌘×÷¦⋯…↑↓→→←↔⟷·•●◆▼⟪⟫┌└├⏎↩√φ]+)/g.exec(line); + const m = /([^\t\n\r\x20-\x7E⊃⊇✔︎✓🎯🧪✍️⚠️🛑🔴🚗🚙🚕🎉✨❗⇧⌥⌘×÷¦⋯…↑↓→→←↔⟷—·•●◆▼⟪⟫┌└├⏎↩√φ]+)/g.exec(line); if (m) { console.error( file.relative + `(${i + 1},${m.index + 1}): Unexpected unicode character: "${m[0]}" (charCode: ${m[0].charCodeAt(0)}). To suppress, use // allow-any-unicode-next-line` diff --git a/build/package-lock.json b/build/package-lock.json index 36b85902e22d6..5cf0189d3e690 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -53,7 +53,7 @@ "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", - "dmg-builder": "26.5.0", + "dmg-builder": "26.8.1", "esbuild": "0.27.2", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", @@ -813,14 +813,13 @@ } }, "node_modules/@electron/rebuild": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.1.tgz", - "integrity": "sha512-iMGXb6Ib7H/Q3v+BKZJoETgF9g6KMNZVbsO4b7Dmpgb5qTFqyFTzqW9F3TOSHdybv2vKYKzSS9OiZL+dcJb+1Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", "dev": true, "license": "MIT", "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", @@ -831,7 +830,7 @@ "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", - "tar": "^6.0.5", + "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { @@ -841,142 +840,6 @@ "node": ">=22.12.0" } }, - "node_modules/@electron/rebuild/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@electron/rebuild/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@electron/rebuild/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/rebuild/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@electron/rebuild/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/rebuild/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@electron/rebuild/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@electron/rebuild/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@electron/rebuild/node_modules/node-abi": { "version": "4.26.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", @@ -991,9 +854,9 @@ } }, "node_modules/@electron/rebuild/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -1003,38 +866,6 @@ "node": ">=10" } }, - "node_modules/@electron/rebuild/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@electron/universal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", @@ -1792,9 +1623,9 @@ } }, "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -3136,23 +2967,24 @@ "license": "MIT" }, "node_modules/app-builder-lib": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.5.0.tgz", - "integrity": "sha512-iRRiJhM0uFMauDeIuv8ESHZSn+LESbdDEuHi7rKdeETjrvBObecXnWJx1f3vs3KtoGcd3hCk1zURKypyvZOtFQ==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", "dev": true, "license": "MIT", "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", - "@electron/rebuild": "4.0.1", + "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", - "builder-util": "26.4.1", + "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", @@ -3160,7 +2992,7 @@ "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", - "electron-publish": "26.4.1", + "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", @@ -3170,9 +3002,10 @@ "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", + "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", - "tar": "7.5.3", + "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" @@ -3181,8 +3014,55 @@ "node": ">=14.0.0" }, "peerDependencies": { - "dmg-builder": "26.5.0", - "electron-builder-squirrel-windows": "26.5.0" + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "node_modules/app-builder-lib/node_modules/@electron/osx-sign": { @@ -3227,6 +3107,29 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/app-builder-lib/node_modules/balanced-match": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/app-builder-lib/node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/app-builder-lib/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -3242,14 +3145,37 @@ "node": ">=12" } }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/app-builder-lib/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/app-builder-lib/node_modules/js-yaml": { @@ -3265,27 +3191,14 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/app-builder-lib/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", + "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { "node": "20 || >=22" @@ -3295,9 +3208,9 @@ } }, "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -3307,16 +3220,6 @@ "node": ">=10" } }, - "node_modules/app-builder-lib/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/app-builder-lib/node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -3398,6 +3301,13 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -3603,9 +3513,9 @@ "license": "MIT" }, "node_modules/builder-util": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.4.1.tgz", - "integrity": "sha512-FlgH43XZ50w3UtS1RVGDWOz8v9qMXPC7upMtKMtBEnYdt1OVoS61NYhKm/4x+cIaWqJTXua0+VVPI+fSPGXNIw==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", "dev": true, "license": "MIT", "dependencies": { @@ -3822,6 +3732,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -4595,14 +4506,14 @@ } }, "node_modules/dmg-builder": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.5.0.tgz", - "integrity": "sha512-AyOCzpS1TCxDkSWxAzpfw5l7jBX4C8jKCucmT/6y6/24H5VKSHpjcVJD0W8o5BrFi+skC7Z7+F4aNyHmvn4AAw==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.5.0", - "builder-util": "26.4.1", + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" @@ -4883,17 +4794,17 @@ } }, "node_modules/electron-publish": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.4.1.tgz", - "integrity": "sha512-nByal9K5Ar3BNJUfCSglXltpKUhJqpwivNpKVHnkwxTET9LKl+NxoojpGF1dSXVFcoBKVm+OhsVa28ZsoshEPA==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", "dev": true, "license": "MIT", "dependencies": { "@types/fs-extra": "^9.0.11", - "builder-util": "26.4.1", + "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", - "form-data": "^4.0.0", + "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" @@ -5509,9 +5420,9 @@ "dev": true }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -6328,13 +6239,6 @@ "node": ">=10" } }, - "node_modules/jake/node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -7007,19 +6911,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -7110,9 +7001,9 @@ } }, "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -7160,19 +7051,19 @@ } }, "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -7903,6 +7794,25 @@ "node": ">=10" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -9056,10 +8966,9 @@ } }, "node_modules/tar": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.3.tgz", - "integrity": "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/build/package.json b/build/package.json index e889b6ac54d7b..0b18b8daff84b 100644 --- a/build/package.json +++ b/build/package.json @@ -47,7 +47,7 @@ "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", - "dmg-builder": "26.5.0", + "dmg-builder": "26.8.1", "esbuild": "0.27.2", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index ec6b840db4708..af89d698f9c12 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -30,7 +30,7 @@ /* Stealth Shadows - shadow-based depth for UI elements, controlled by workbench.stealthShadows.enabled */ /* Activity Bar */ -.monaco-workbench .part.activitybar { +.monaco-workbench.vs .part.activitybar { z-index: 50; position: relative; } @@ -44,55 +44,54 @@ } /* Sidebar */ -.monaco-workbench .part.sidebar { +.monaco-workbench.vs .part.sidebar { box-shadow: var(--shadow-md); z-index: 40; position: relative; } -.monaco-workbench.sidebar-right .part.sidebar { +.monaco-workbench.sidebar-right.vs .part.sidebar { box-shadow: var(--shadow-md); } -.monaco-workbench .part.auxiliarybar { +.monaco-workbench.vs .part.auxiliarybar { box-shadow: var(--shadow-md); z-index: 35; position: relative; } /* Ensure iframe containers in pane-body render above sidebar z-index */ -.monaco-workbench > div[data-keybinding-context] { +.monaco-workbench.vs > div[data-keybinding-context] { z-index: 50 !important; } /* Ensure in-editor pane iframes render below sidebar z-index */ -.monaco-workbench > div[data-parent-flow-to-element-id] { +.monaco-workbench.vs > div[data-parent-flow-to-element-id] { z-index: 0 !important; } /* Ensure webview containers render above sidebar z-index */ -.monaco-workbench .part.sidebar .webview, -.monaco-workbench .part.sidebar .webview-container, -.monaco-workbench .part.auxiliarybar .webview, -.monaco-workbench .part.auxiliarybar .webview-container { +.monaco-workbench.vs .part.sidebar .webview, +.monaco-workbench.vs .part.sidebar .webview-container, +.monaco-workbench.vs .part.auxiliarybar .webview, +.monaco-workbench.vs .part.auxiliarybar .webview-container { position: relative; z-index: 50; transform: translateZ(0); } /* Panel */ -.monaco-workbench .part.panel { +.monaco-workbench.vs .part.panel { box-shadow: var(--shadow-md); - /* z-index: 35; */ position: relative; } -.monaco-workbench.panel-position-left .part.panel { +.monaco-workbench.panel-position-left.vs .part.panel { box-shadow: var(--shadow-md); } -.monaco-workbench.panel-position-right .part.panel { +.monaco-workbench.panel-position-right.vs .part.panel { box-shadow: var(--shadow-md); } @@ -101,42 +100,40 @@ } /* Sashes - ensure they extend full height and are above other panels */ -.monaco-workbench .monaco-sash { +.monaco-workbench.vs .monaco-sash { z-index: 35; } -.monaco-workbench .monaco-sash.vertical { +.monaco-workbench.vs .monaco-sash.vertical { z-index: 40; } -.monaco-workbench .monaco-sash.vertical:nth-child(2) { +.monaco-workbench.vs .monaco-sash.vertical:nth-child(2) { z-index: 45; } -.monaco-workbench .monaco-sash.horizontal { +.monaco-workbench.vs .monaco-sash.horizontal { z-index: 35; } /* Editor */ -.monaco-workbench .part.editor { +.monaco-workbench.vs .part.editor { position: relative; } -.monaco-workbench .part.editor > .content .editor-group-container > .title { +.monaco-workbench.vs .part.editor > .content .editor-group-container > .title { box-shadow: none; position: relative; z-index: 10; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { +.monaco-workbench.vs .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { box-shadow: inset var(--shadow-active-tab); position: relative; z-index: 5; - border-radius: 0; - border-top: none !important; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { +.monaco-workbench.vs .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { box-shadow: var(--shadow-sm); } @@ -154,14 +151,6 @@ box-shadow: var(--shadow-md); } -.monaco-workbench .part.titlebar.inactive { - background: var(--vscode-titleBar-inactiveBackground) !important; - - & > * { - opacity: 0.3; - } -} - /* Quick Input (Command Palette) */ .monaco-workbench .quick-input-widget { box-shadow: var(--shadow-xl) !important; @@ -211,22 +200,20 @@ display: flex; align-items: center; font-size: 11px; - padding: 0 4px 1px 4px; + padding: 0 4px; border-radius: var(--vscode-cornerRadius-small) !important; - background: color-mix(in srgb, var(--vscode-badge-background) 70%, transparent) !important; - color: var(--vscode-badge-foreground) !important; + background: transparent !important; + color: var(--vscode-descriptionForeground) !important; + border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; margin-right: 8px; } -.monaco-workbench.vs-dark .quick-input-list .quick-input-list-entry .quick-input-list-separator { - background: color-mix(in srgb, var(--vscode-badge-background) 50%, transparent) !important; -} - .monaco-workbench .monaco-list-row.focused .quick-input-list-entry .quick-input-list-separator, .monaco-workbench .monaco-list-row.selected .quick-input-list-entry .quick-input-list-separator, .monaco-workbench .monaco-list-row:hover .quick-input-list-entry .quick-input-list-separator { background: transparent !important; color: inherit !important; + border: none !important; padding: 0; } @@ -250,10 +237,13 @@ /* Chat Widget */ .monaco-workbench .interactive-session .chat-input-container { - box-shadow: inset var(--shadow-sm); border-radius: var(--radius-lg); } +.monaco-workbench.vs .interactive-session .chat-input-container { + box-shadow: inset var(--shadow-sm); +} + .monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor, .monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { border-radius: var(--radius-lg) var(--radius-lg) 0 0; @@ -448,11 +438,9 @@ .monaco-workbench .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { border-radius: var(--radius-sm); - background: color-mix(in srgb, var(--vscode-badge-background) 70%, transparent) !important; -} - -.monaco-workbench.vs-dark .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { - background: color-mix(in srgb, var(--vscode-badge-background) 50%, transparent) !important; + background: transparent !important; + color: var(--vscode-descriptionForeground) !important; + border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; } /* Welcome Tiles */ diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 021ad016e0222..3e5b309b82643 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -203,6 +203,7 @@ export interface IChatFileContribution { readonly path: string; readonly name?: string; readonly description?: string; + readonly when?: string; } export interface IExtensionContributions { diff --git a/src/vs/platform/update/common/update.config.contribution.ts b/src/vs/platform/update/common/update.config.contribution.ts index 5d1a419f8f2b3..93dd62df97ec6 100644 --- a/src/vs/platform/update/common/update.config.contribution.ts +++ b/src/vs/platform/update/common/update.config.contribution.ts @@ -67,8 +67,8 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true, scope: ConfigurationScope.APPLICATION, - title: localize('enableWindowsBackgroundUpdatesTitle', "Enable Background Updates on Windows"), - description: localize('enableWindowsBackgroundUpdates', "Enable to download and install new VS Code versions in the background on Windows."), + title: localize('enableWindowsBackgroundUpdatesTitle', "Enable Background Updates"), + description: localize('enableWindowsBackgroundUpdates', "Enable to download and install new VS Code versions in the background."), included: isWindows && !isWeb }, 'update.showReleaseNotes': { diff --git a/src/vs/sessions/browser/media/sidebarActionButton.css b/src/vs/sessions/browser/media/sidebarActionButton.css index 3d9b3a393fa2f..f170a6ed4fd0f 100644 --- a/src/vs/sessions/browser/media/sidebarActionButton.css +++ b/src/vs/sessions/browser/media/sidebarActionButton.css @@ -19,8 +19,7 @@ border: none; padding: 4px 8px; margin: 0; - font-size: 11px; - font-weight: 500; + font-size: 12px; height: auto; white-space: nowrap; overflow: hidden; diff --git a/src/vs/sessions/browser/parts/media/titlebarpart.css b/src/vs/sessions/browser/parts/media/titlebarpart.css index b005dbe50de41..22273655103c7 100644 --- a/src/vs/sessions/browser/parts/media/titlebarpart.css +++ b/src/vs/sessions/browser/parts/media/titlebarpart.css @@ -38,3 +38,8 @@ .agent-sessions-workbench:not(.nosidebar) .part.titlebar > .titlebar-container > .titlebar-left > .left-toolbar-container { display: none !important; } + +/* macOS native: the spacer uses window-controls-container but should not block dragging */ +.agent-sessions-workbench.mac .part.titlebar .window-controls-container { + -webkit-app-region: drag; +} diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts index 5b4028dda8e7b..82acbdb1c5be3 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import './agentFeedbackEditorInputContribution.js'; +import './agentFeedbackGlyphMarginContribution.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { AgentFeedbackService, IAgentFeedbackService } from './agentFeedbackService.js'; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts index bcb3a079ad4be..7e17e30caf3d3 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts @@ -145,13 +145,13 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements return; } - const match = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); - if (!match) { + const sessionResource = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); + if (!sessionResource) { this._hide(); return; } - this._sessionResource = match.sessionResource; + this._sessionResource = sessionResource; this._show(); } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts index 30c75a7556dfd..56cb43ad9347d 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts @@ -21,8 +21,10 @@ import { IAgentFeedbackService } from './agentFeedbackService.js'; import { navigateNextFeedbackActionId, navigatePreviousFeedbackActionId, navigationBearingFakeActionId, submitFeedbackActionId } from './agentFeedbackEditorActions.js'; import { assertType } from '../../../../base/common/types.js'; import { localize } from '../../../../nls.js'; -import { getActiveResourceCandidates } from './agentFeedbackEditorUtils.js'; +import { getActiveResourceCandidates, getSessionForResource } from './agentFeedbackEditorUtils.js'; import { Menus } from '../../../browser/menus.js'; +import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; +import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; class AgentFeedbackActionViewItem extends ActionViewItem { @@ -140,7 +142,9 @@ class AgentFeedbackOverlayController { container: HTMLElement, group: IEditorGroup, @IAgentFeedbackService agentFeedbackService: IAgentFeedbackService, + @IAgentSessionsService agentSessionsService: IAgentSessionsService, @IInstantiationService instaService: IInstantiationService, + @IChatEditingService chatEditingService: IChatEditingService, ) { this._domNode.classList.add('agent-feedback-editor-overlay'); this._domNode.style.position = 'absolute'; @@ -176,18 +180,16 @@ class AgentFeedbackOverlayController { activeSignal.read(r); const candidates = getActiveResourceCandidates(group.activeEditorPane?.input); - let shouldShow = false; - let navigationBearings = { activeIdx: -1, totalCount: 0 }; + let navigationBearings = undefined; for (const candidate of candidates) { - const sessionResource = agentFeedbackService.getMostRecentSessionForResource(candidate); + const sessionResource = getSessionForResource(candidate, chatEditingService, agentSessionsService); if (sessionResource && agentFeedbackService.getFeedback(sessionResource).length > 0) { - shouldShow = true; navigationBearings = agentFeedbackService.getNavigationBearing(sessionResource); break; } } - if (!shouldShow) { + if (!navigationBearings) { hide(); return; } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts index c4fc944e1bb6b..bb42a4ff24241 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts @@ -9,11 +9,6 @@ import { IChatEditingService } from '../../../../workbench/contrib/chat/common/e import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { agentSessionContainsResource, editingEntriesContainResource } from '../../../../workbench/contrib/chat/browser/sessionResourceMatching.js'; -export interface ISessionResourceMatch { - readonly sessionResource: URI; - readonly resourceUri: URI; -} - /** * Find the session that contains the given resource by checking editing sessions and agent sessions. */ @@ -21,16 +16,16 @@ export function getSessionForResource( resourceUri: URI, chatEditingService: IChatEditingService, agentSessionsService: IAgentSessionsService, -): ISessionResourceMatch | undefined { +): URI | undefined { for (const editingSession of chatEditingService.editingSessionsObs.get()) { if (editingEntriesContainResource(editingSession.entries.get(), resourceUri)) { - return { sessionResource: editingSession.chatSessionResource, resourceUri }; + return editingSession.chatSessionResource; } } for (const session of agentSessionsService.model.sessions) { if (agentSessionContainsResource(session, resourceUri)) { - return { sessionResource: session.resource, resourceUri }; + return session.resource; } } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts new file mode 100644 index 0000000000000..6436e692fc2a8 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/agentFeedbackGlyphMargin.css'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; +import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; +import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; +import { GlyphMarginLane, IModelDeltaDecoration, TrackedRangeStickiness } from '../../../../editor/common/model.js'; +import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IAgentFeedbackService } from './agentFeedbackService.js'; +import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; +import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { getSessionForResource } from './agentFeedbackEditorUtils.js'; +import { Selection } from '../../../../editor/common/core/selection.js'; + +const GLYPH_MARGIN_LANE = GlyphMarginLane.Left; + +const feedbackGlyphDecoration = ModelDecorationOptions.register({ + description: 'agent-feedback-glyph', + glyphMarginClassName: `${ThemeIcon.asClassName(Codicon.comment)} agent-feedback-glyph`, + glyphMargin: { position: GLYPH_MARGIN_LANE }, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, +}); + +const addFeedbackHintDecoration = ModelDecorationOptions.register({ + description: 'agent-feedback-add-hint', + glyphMarginClassName: `${ThemeIcon.asClassName(Codicon.add)} agent-feedback-add-hint`, + glyphMargin: { position: GLYPH_MARGIN_LANE }, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, +}); + +export class AgentFeedbackGlyphMarginContribution extends Disposable implements IEditorContribution { + + static readonly ID = 'agentFeedback.glyphMarginContribution'; + + private readonly _feedbackDecorations; + + private _hintDecorationId: string | null = null; + private _hintLine = -1; + private _sessionResource: URI | undefined; + private _feedbackLines = new Set(); + + constructor( + private readonly _editor: ICodeEditor, + @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, + ) { + super(); + + this._feedbackDecorations = this._editor.createDecorationsCollection(); + + this._store.add(this._agentFeedbackService.onDidChangeFeedback(() => this._updateFeedbackDecorations())); + this._store.add(this._editor.onDidChangeModel(() => this._onModelChanged())); + this._store.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onMouseMove(e))); + this._store.add(this._editor.onMouseLeave(() => this._updateHintDecoration(-1))); + this._store.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onMouseDown(e))); + + this._resolveSession(); + this._updateFeedbackDecorations(); + } + + private _onModelChanged(): void { + this._updateHintDecoration(-1); + this._resolveSession(); + this._updateFeedbackDecorations(); + } + + private _resolveSession(): void { + const model = this._editor.getModel(); + if (!model) { + this._sessionResource = undefined; + return; + } + this._sessionResource = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); + } + + private _updateFeedbackDecorations(): void { + if (!this._sessionResource) { + this._feedbackDecorations.clear(); + this._feedbackLines.clear(); + return; + } + + const feedbackItems = this._agentFeedbackService.getFeedback(this._sessionResource); + const decorations: IModelDeltaDecoration[] = []; + const lines = new Set(); + + for (const item of feedbackItems) { + const model = this._editor.getModel(); + if (!model || item.resourceUri.toString() !== model.uri.toString()) { + continue; + } + + const line = item.range.startLineNumber; + lines.add(line); + decorations.push({ + range: new Range(line, 1, line, 1), + options: feedbackGlyphDecoration, + }); + } + + this._feedbackLines = lines; + this._feedbackDecorations.set(decorations); + } + + private _onMouseMove(e: IEditorMouseEvent): void { + if (!this._sessionResource) { + this._updateHintDecoration(-1); + return; + } + + if (e.target.position + && e.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN + && !e.target.detail.isAfterLines + && !this._feedbackLines.has(e.target.position.lineNumber) + ) { + this._updateHintDecoration(e.target.position.lineNumber); + } else { + this._updateHintDecoration(-1); + } + } + + private _updateHintDecoration(line: number): void { + if (line === this._hintLine) { + return; + } + + this._hintLine = line; + this._editor.changeDecorations(accessor => { + if (this._hintDecorationId) { + accessor.removeDecoration(this._hintDecorationId); + this._hintDecorationId = null; + } + if (line !== -1) { + this._hintDecorationId = accessor.addDecoration( + new Range(line, 1, line, 1), + addFeedbackHintDecoration, + ); + } + }); + } + + private _onMouseDown(e: IEditorMouseEvent): void { + if (!e.target.position + || e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN + || e.target.detail.isAfterLines + || !this._sessionResource + ) { + return; + } + + const lineNumber = e.target.position.lineNumber; + + // Lines with existing feedback - do nothing + if (this._feedbackLines.has(lineNumber)) { + return; + } + + // Select the line content and focus the editor + const model = this._editor.getModel(); + if (!model) { + return; + } + + const startColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); + const endColumn = model.getLineLastNonWhitespaceColumn(lineNumber); + if (startColumn === 0 || endColumn === 0) { + // Empty line - select the whole line range + this._editor.setSelection(new Selection(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber))); + } else { + this._editor.setSelection(new Selection(lineNumber, startColumn, lineNumber, endColumn)); + } + this._editor.focus(); + } + + override dispose(): void { + this._feedbackDecorations.clear(); + this._updateHintDecoration(-1); + super.dispose(); + } +} + +registerEditorContribution(AgentFeedbackGlyphMarginContribution.ID, AgentFeedbackGlyphMarginContribution, EditorContributionInstantiation.Eventually); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts index 19f5baf47c227..701495fc188b9 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts @@ -7,15 +7,8 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { Comment, CommentThread, CommentThreadCollapsibleState, CommentThreadState, CommentInput } from '../../../../editor/common/languages.js'; -import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { ICommentController, ICommentInfo, ICommentService, INotebookCommentInfo } from '../../../../workbench/contrib/comments/browser/commentService.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { generateUuid } from '../../../../base/common/uuid.js'; -import { registerAction2, Action2, MenuId } from '../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { localize } from '../../../../nls.js'; import { isEqual } from '../../../../base/common/resources.js'; import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; @@ -90,8 +83,6 @@ export interface IAgentFeedbackService { // --- Implementation ----------------------------------------------------------- -const AGENT_FEEDBACK_OWNER = 'agentFeedbackController'; -const AGENT_FEEDBACK_CONTEXT_VALUE = 'agentFeedback'; const AGENT_FEEDBACK_ATTACHMENT_ID_PREFIX = 'agentFeedback:'; export class AgentFeedbackService extends Disposable implements IAgentFeedbackService { @@ -109,11 +100,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe private _sessionUpdatedSequence = 0; private readonly _navigationAnchorBySession = new Map(); - private _controllerRegistered = false; - private _nextThreadHandle = 1; - constructor( - @ICommentService private readonly _commentService: ICommentService, @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @@ -153,81 +140,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe })); } - private _ensureController(): void { - if (this._controllerRegistered) { - return; - } - this._controllerRegistered = true; - - const self = this; - - const controller: ICommentController = { - id: AGENT_FEEDBACK_OWNER, - label: 'Agent Feedback', - features: {}, - contextValue: AGENT_FEEDBACK_CONTEXT_VALUE, - owner: AGENT_FEEDBACK_OWNER, - activeComment: undefined, - createCommentThreadTemplate: async () => { }, - updateCommentThreadTemplate: async () => { }, - deleteCommentThreadMain: () => { }, - toggleReaction: async () => { }, - getDocumentComments: async (resource: URI, _token: CancellationToken): Promise> => { - // Return threads for this resource from all sessions - const threads: CommentThread[] = []; - for (const [, sessionFeedback] of self._feedbackBySession) { - for (const f of sessionFeedback) { - if (f.resourceUri.toString() === resource.toString()) { - threads.push(self._createThread(f)); - } - } - } - return { - threads, - commentingRanges: { ranges: [], resource, fileComments: false }, - uniqueOwner: AGENT_FEEDBACK_OWNER, - }; - }, - getNotebookComments: async (_resource: URI, _token: CancellationToken): Promise => { - return { threads: [], uniqueOwner: AGENT_FEEDBACK_OWNER }; - }, - setActiveCommentAndThread: async () => { }, - }; - - this._commentService.registerCommentController(AGENT_FEEDBACK_OWNER, controller); - this._store.add({ dispose: () => this._commentService.unregisterCommentController(AGENT_FEEDBACK_OWNER) }); - - // Register delete action for our feedback threads - this._store.add(registerAction2(class extends Action2 { - constructor() { - super({ - id: 'agentFeedback.deleteThread', - title: localize('agentFeedback.delete', "Delete Feedback"), - icon: Codicon.trash, - menu: { - id: MenuId.CommentThreadTitle, - when: ContextKeyExpr.equals('commentController', AGENT_FEEDBACK_CONTEXT_VALUE), - group: 'navigation', - } - }); - } - run(accessor: ServicesAccessor, ...args: unknown[]): void { - const agentFeedbackService = accessor.get(IAgentFeedbackService); - const arg = args[0] as { thread?: { threadId?: string }; threadId?: string } | undefined; - const thread = arg?.thread ?? arg; - if (thread?.threadId) { - const sessionResource = self._findSessionForFeedback(thread.threadId); - if (sessionResource) { - agentFeedbackService.removeFeedback(sessionResource, thread.threadId); - } - } - } - })); - } - addFeedback(sessionResource: URI, resourceUri: URI, range: IRange, text: string): IAgentFeedback { - this._ensureController(); - const key = sessionResource.toString(); let feedbackItems = this._feedbackBySession.get(key); if (!feedbackItems) { @@ -246,7 +159,6 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe this._sessionUpdatedOrder.set(key, ++this._sessionUpdatedSequence); this._onDidChangeNavigation.fire(sessionResource); - this._syncThreads(sessionResource); this._onDidChangeFeedback.fire({ sessionResource, feedbackItems }); return feedback; @@ -261,9 +173,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe const idx = feedbackItems.findIndex(f => f.id === feedbackId); if (idx >= 0) { - const removed = feedbackItems[idx]; feedbackItems.splice(idx, 1); - this._activeThreadIds.delete(feedbackId); if (this._navigationAnchorBySession.get(key) === feedbackId) { this._navigationAnchorBySession.delete(key); this._onDidChangeNavigation.fire(sessionResource); @@ -274,34 +184,10 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe this._sessionUpdatedOrder.delete(key); } - // Fire updateComments with the thread in removed[] so the editor - // controller's onDidUpdateCommentThreads handler removes the zone widget - const thread = this._createThread(removed); - thread.isDisposed = true; - this._commentService.updateComments(AGENT_FEEDBACK_OWNER, { - added: [], - removed: [thread], - changed: [], - pending: [], - }); - this._onDidChangeFeedback.fire({ sessionResource, feedbackItems }); } } - /** - * Find which session a feedback item belongs to by its ID. - */ - _findSessionForFeedback(feedbackId: string): URI | undefined { - for (const [, feedbackItems] of this._feedbackBySession) { - const item = feedbackItems.find(f => f.id === feedbackId); - if (item) { - return item.sessionResource; - } - } - return undefined; - } - getFeedback(sessionResource: URI): readonly IAgentFeedback[] { return this._feedbackBySession.get(sessionResource.toString()) ?? []; } @@ -393,178 +279,10 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe clearFeedback(sessionResource: URI): void { const key = sessionResource.toString(); - const feedbackItems = this._feedbackBySession.get(key); - if (feedbackItems && feedbackItems.length > 0) { - const removedThreads = feedbackItems.map(f => { - this._activeThreadIds.delete(f.id); - const thread = this._createThread(f); - thread.isDisposed = true; - return thread; - }); - - this._commentService.updateComments(AGENT_FEEDBACK_OWNER, { - added: [], - removed: removedThreads, - changed: [], - pending: [], - }); - } this._feedbackBySession.delete(key); this._sessionUpdatedOrder.delete(key); this._navigationAnchorBySession.delete(key); this._onDidChangeNavigation.fire(sessionResource); this._onDidChangeFeedback.fire({ sessionResource, feedbackItems: [] }); } - - /** Threads currently known to the comment service, keyed by feedback id */ - private readonly _activeThreadIds = new Set(); - - /** - * Sync feedback threads to the ICommentService using updateComments for - * incremental add/remove, which the editor controller listens to. - */ - private _syncThreads(_sessionResource: URI): void { - // Collect all current feedback IDs - const currentIds = new Set(); - const allFeedback: IAgentFeedback[] = []; - for (const [, sessionFeedback] of this._feedbackBySession) { - for (const f of sessionFeedback) { - currentIds.add(f.id); - allFeedback.push(f); - } - } - - // Determine added and removed - const added: CommentThread[] = []; - const removed: CommentThread[] = []; - - for (const f of allFeedback) { - if (!this._activeThreadIds.has(f.id)) { - added.push(this._createThread(f)); - } - } - - for (const id of this._activeThreadIds) { - if (!currentIds.has(id)) { - // Create a minimal thread just for removal (needs threadId and resource) - removed.push(this._createRemovedThread(id)); - } - } - - // Update tracking - this._activeThreadIds.clear(); - for (const id of currentIds) { - this._activeThreadIds.add(id); - } - - if (added.length || removed.length) { - this._commentService.updateComments(AGENT_FEEDBACK_OWNER, { - added, - removed, - changed: [], - pending: [], - }); - } - } - - private _createRemovedThread(feedbackId: string): CommentThread { - const noopEvent = Event.None; - return { - isDocumentCommentThread(): this is CommentThread { return true; }, - commentThreadHandle: -1, - controllerHandle: 0, - threadId: feedbackId, - resource: null, - range: undefined, - label: undefined, - contextValue: undefined, - comments: undefined, - onDidChangeComments: noopEvent, - collapsibleState: CommentThreadCollapsibleState.Collapsed, - initialCollapsibleState: CommentThreadCollapsibleState.Collapsed, - onDidChangeInitialCollapsibleState: noopEvent, - state: undefined, - applicability: undefined, - canReply: false, - input: undefined, - onDidChangeInput: noopEvent, - onDidChangeLabel: noopEvent, - onDidChangeCollapsibleState: noopEvent, - onDidChangeState: noopEvent, - onDidChangeCanReply: noopEvent, - isDisposed: true, - isTemplate: false, - }; - } - - private _createThread(feedback: IAgentFeedback): CommentThread { - const handle = this._nextThreadHandle++; - - const threadComment: Comment = { - uniqueIdInThread: 1, - body: feedback.text, - userName: 'You', - }; - - return new AgentFeedbackThread(handle, feedback.id, feedback.resourceUri.toString(), feedback.range, [threadComment]); - } -} - -/** - * A CommentThread implementation with proper emitters so the editor - * comment controller can react to state changes (collapse/expand). - */ -class AgentFeedbackThread implements CommentThread { - - private readonly _onDidChangeComments = new Emitter(); - readonly onDidChangeComments = this._onDidChangeComments.event; - - private readonly _onDidChangeCollapsibleState = new Emitter(); - readonly onDidChangeCollapsibleState = this._onDidChangeCollapsibleState.event; - - private readonly _onDidChangeInitialCollapsibleState = new Emitter(); - readonly onDidChangeInitialCollapsibleState = this._onDidChangeInitialCollapsibleState.event; - - private readonly _onDidChangeInput = new Emitter(); - readonly onDidChangeInput = this._onDidChangeInput.event; - - private readonly _onDidChangeLabel = new Emitter(); - readonly onDidChangeLabel = this._onDidChangeLabel.event; - - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState = this._onDidChangeState.event; - - private readonly _onDidChangeCanReply = new Emitter(); - readonly onDidChangeCanReply = this._onDidChangeCanReply.event; - - readonly controllerHandle = 0; - readonly label = undefined; - readonly contextValue = undefined; - readonly applicability = undefined; - readonly input = undefined; - readonly isTemplate = false; - - private _collapsibleState = CommentThreadCollapsibleState.Collapsed; - get collapsibleState(): CommentThreadCollapsibleState { return this._collapsibleState; } - set collapsibleState(value: CommentThreadCollapsibleState) { - this._collapsibleState = value; - this._onDidChangeCollapsibleState.fire(value); - } - - readonly initialCollapsibleState = CommentThreadCollapsibleState.Collapsed; - readonly state = CommentThreadState.Unresolved; - readonly canReply = false; - isDisposed = false; - - constructor( - readonly commentThreadHandle: number, - readonly threadId: string, - readonly resource: string, - readonly range: IRange, - readonly comments: readonly Comment[], - ) { } - - isDocumentCommentThread(): this is CommentThread { - return true; - } } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css new file mode 100644 index 0000000000000..16aac443e4802 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-glyph, +.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-add-hint { + border-radius: 3px; + display: flex !important; + align-items: center; + justify-content: center; +} + +.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-glyph { + background-color: var(--vscode-editorGutter-commentGlyphForeground, var(--vscode-icon-foreground)); + color: var(--vscode-editor-background); +} + +.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-add-hint { + background-color: var(--vscode-toolbar-hoverBackground); + opacity: 0.7; +} + +.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-add-hint:hover { + opacity: 1; +} diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts index 4571e7b0e0eca..caaf09e7ca242 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts +++ b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts @@ -48,7 +48,6 @@ import { SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH, CONTENT_MIN_WIDTH, - getActiveSessionRoot, } from './aiCustomizationManagement.js'; import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon } from '../../aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.js'; import { ChatModelsWidget } from '../../../../workbench/contrib/chat/browser/chatManagement/chatModelsWidget.js'; @@ -58,9 +57,7 @@ import { INewPromptOptions, NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, import { showConfigureHooksQuickPick } from '../../../../workbench/contrib/chat/browser/promptSyntax/hookActions.js'; import { CustomizationCreatorService } from './customizationCreatorService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; +import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { IWorkingCopyService } from '../../../../workbench/services/workingCopy/common/workingCopyService.js'; const $ = DOM.$; @@ -148,7 +145,7 @@ export class AICustomizationManagementEditor extends EditorPane { private editorSaveIndicator!: HTMLElement; private readonly editorModelChangeDisposables = this._register(new DisposableStore()); private currentEditingUri: URI | undefined; - private currentWorktreeUri: URI | undefined; + private currentActiveSession: IActiveSessionItem | undefined; private currentEditingIsWorktree = false; private currentModelRef: IReference | undefined; private viewMode: 'list' | 'editor' = 'list'; @@ -177,7 +174,6 @@ export class AICustomizationManagementEditor extends EditorPane { @ILayoutService private readonly layoutService: ILayoutService, @ICommandService private readonly commandService: ICommandService, @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, - @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, ) { super(AICustomizationManagementEditor.ID, group, telemetryService, themeService, storageService); @@ -192,7 +188,7 @@ export class AICustomizationManagementEditor extends EditorPane { if (this.viewMode !== 'editor' || !this.currentEditingIsWorktree) { return; } - this.currentWorktreeUri = getActiveSessionRoot(this.activeSessionService); + this.currentActiveSession = this.activeSessionService.getActiveSession() ?? undefined; })); // Safety disposal for the embedded editor model reference @@ -547,8 +543,7 @@ export class AICustomizationManagementEditor extends EditorPane { this.editorItemPathElement.textContent = basename(uri); // Track worktree URI for auto-commit on close - const worktreeDir = getActiveSessionRoot(this.activeSessionService); - this.currentWorktreeUri = isWorktreeFile ? worktreeDir : undefined; + this.currentActiveSession = isWorktreeFile ? this.activeSessionService.getActiveSession() ?? undefined : undefined; this.currentEditingIsWorktree = isWorktreeFile; // Update visibility @@ -593,16 +588,16 @@ export class AICustomizationManagementEditor extends EditorPane { private goBackToList(): void { // Auto-commit worktree files when leaving the embedded editor const fileUri = this.currentEditingUri; - const worktreeUri = this.currentWorktreeUri; - if (fileUri && worktreeUri) { - this.commitWorktreeFile(worktreeUri, fileUri); + const session = this.currentActiveSession; + if (fileUri && session) { + this.commitWorktreeFile(session, fileUri); } // Dispose model reference this.currentModelRef?.dispose(); this.currentModelRef = undefined; this.currentEditingUri = undefined; - this.currentWorktreeUri = undefined; + this.currentActiveSession = undefined; this.currentEditingIsWorktree = false; this.editorModelChangeDisposables.clear(); this.clearSaveIndicator(); @@ -786,12 +781,8 @@ export class AICustomizationManagementEditor extends EditorPane { /** * Commits a worktree file via the extension and refreshes the Changes view. */ - private async commitWorktreeFile(worktreeUri: URI, fileUri: URI): Promise { - await this.commandService.executeCommand( - 'github.copilot.cli.sessions.commitToWorktree', - { worktreeUri, fileUri } - ); - await this.agentSessionsService.model.resolve(AgentSessionProviders.Background); + private async commitWorktreeFile(session: IActiveSessionItem, fileUri: URI): Promise { + await this.activeSessionService.commitWorktreeFiles(session, [fileUri]); this.refreshList(); } diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index b17a408300976..a00c358a7b9cf 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -26,6 +26,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { AgenticPromptsService } from './promptsService.js'; import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { ISessionsConfigurationService, SessionsConfigurationService } from './sessionsConfigurationService.js'; import { ChatViewContainerId, ChatViewId } from '../../../../workbench/contrib/chat/browser/chat.js'; import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; import { NewChatViewPane, SessionsViewId } from './newChatViewPane.js'; @@ -213,3 +214,4 @@ registerWorkbenchContribution2(RunScriptContribution.ID, RunScriptContribution, // register services registerSingleton(IPromptsService, AgenticPromptsService, InstantiationType.Delayed); +registerSingleton(ISessionsConfigurationService, SessionsConfigurationService, InstantiationType.Delayed); diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 61fd3a1cd3dbb..a8c69daa78eb9 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -12,7 +12,6 @@ import { Separator, toAction } from '../../../../base/common/actions.js'; import { Radio } from '../../../../base/browser/ui/radio/radio.js'; import { DropdownMenuActionViewItem } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable, observableValue } from '../../../../base/common/observable.js'; @@ -237,6 +236,7 @@ class NewChatWidget extends Disposable { @IFileDialogService private readonly fileDialogService: IFileDialogService, @IWorkspacesService private readonly workspacesService: IWorkspacesService, @IStorageService private readonly storageService: IStorageService, + @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, ) { super(); this._contextAttachments = this._register(this.instantiationService.createInstance(NewChatContextAttachments)); @@ -383,8 +383,7 @@ class NewChatWidget extends Disposable { }); this._pendingSessionResources.set(target, this._pendingSessionResource); - // Create the session in the extension so that session options can be stored - this.chatSessionsService.getOrCreateChatSession(this._pendingSessionResource, CancellationToken.None) + this.sessionsManagementService.createNewPendingSession(this._pendingSessionResource,) .catch((err) => this.logService.trace('Failed to create pending session:', err)); } @@ -1189,6 +1188,9 @@ export class NewChatViewPane extends ViewPane { override setVisible(visible: boolean): void { super.setVisible(visible); this._widget?.setVisible(visible); + if (visible) { + this._widget?.focusInput(); + } } } diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index 6138f48d98e69..914bfe0396189 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -5,22 +5,20 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { autorun, derived, IObservable, observableSignal } from '../../../../base/common/observable.js'; +import { autorun, derived, IObservable } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { localize, localize2 } from '../../../../nls.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { Menus } from '../../../browser/menus.js'; +import { ISessionsConfigurationService, ISessionScript } from './sessionsConfigurationService.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -// Storage keys -const STORAGE_KEY_DEFAULT_RUN_ACTION = 'workbench.agentSessions.defaultRunAction'; - // Menu IDs - exported for use in auxiliary bar part export const RunScriptDropdownMenuId = MenuId.for('AgentSessionsRunScriptDropdown'); @@ -28,15 +26,9 @@ export const RunScriptDropdownMenuId = MenuId.for('AgentSessionsRunScriptDropdow const RUN_SCRIPT_ACTION_ID = 'workbench.action.agentSessions.runScript'; const CONFIGURE_DEFAULT_RUN_ACTION_ID = 'workbench.action.agentSessions.configureDefaultRunAction'; -// Types for stored default action -interface IStoredRunAction { - readonly name: string; - readonly command: string; -} - interface IRunScriptActionContext { - readonly storageKey: string; - readonly action: IStoredRunAction | undefined; + readonly session: IActiveSessionItem; + readonly scripts: readonly ISessionScript[]; readonly cwd: URI; } @@ -49,149 +41,141 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr static readonly ID = 'workbench.contrib.agentSessions.runScript'; private readonly _activeRunState: IObservable; - private readonly _updateSignal = observableSignal(this); constructor( - @IStorageService private readonly _storageService: IStorageService, @ITerminalService private readonly _terminalService: ITerminalService, - @ISessionsManagementService activeSessionService: ISessionsManagementService, + @ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService, @IQuickInputService private readonly _quickInputService: IQuickInputService, + @ISessionsConfigurationService private readonly _sessionsConfigService: ISessionsConfigurationService, ) { super(); this._activeRunState = derived(this, reader => { - const activeSession = activeSessionService.activeSession.read(reader); - if (!activeSession || !activeSession.repository) { + const activeSession = this._activeSessionService.activeSession.read(reader); + const cwd = activeSession?.worktree ?? activeSession?.repository; + if (!activeSession || !cwd) { return undefined; } - this._updateSignal.read(reader); - const storageKey = `${STORAGE_KEY_DEFAULT_RUN_ACTION}.${activeSession.repository.toString()}`; - const action = this._getStoredDefaultAction(storageKey); - - return { - storageKey, - action, - cwd: activeSession.worktree ?? activeSession.repository - }; + const scripts = this._sessionsConfigService.getScripts(activeSession).read(reader); + return { session: activeSession, scripts, cwd }; }); this._registerActions(); } - private _getStoredDefaultAction(storageKey: string): IStoredRunAction | undefined { - const stored = this._storageService.get(storageKey, StorageScope.WORKSPACE); - if (stored) { - try { - const parsed = JSON.parse(stored); - if (typeof parsed?.name === 'string' && typeof parsed?.command === 'string') { - return parsed; - } - } catch { - return undefined; - } - } - return undefined; - } - - private _setStoredDefaultAction(storageKey: string, action: IStoredRunAction): void { - this._storageService.store(storageKey, JSON.stringify(action), StorageScope.WORKSPACE, StorageTarget.MACHINE); - this._updateSignal.trigger(undefined); - } - private _registerActions(): void { const that = this; - // Main play action this._register(autorun(reader => { const activeSession = this._activeRunState.read(reader); if (!activeSession) { return; } - const title = activeSession.action ? activeSession.action.name : localize('runScriptNoAction', "Run Script"); - const tooltip = activeSession.action ? - localize('runScriptTooltip', "Run '{0}' in terminal", activeSession.action.name) - : localize('runScriptTooltipNoAction', "Configure run action"); - - reader.store.add(registerAction2(class extends Action2 { - constructor() { - super({ - id: RUN_SCRIPT_ACTION_ID, - title: title, - tooltip: tooltip, - icon: Codicon.play, - category: localize2('agentSessions', 'Agent Sessions'), - menu: [{ - id: RunScriptDropdownMenuId, - group: 'navigation', - order: 0, - }] - }); - } + const { scripts, cwd, session } = activeSession; + const configureScriptPrecondition = session.worktree ? ContextKeyExpr.true() : ContextKeyExpr.false(); + + if (scripts.length === 0) { + // No scripts configured - show a "Run Script" button that opens the configure quick pick + reader.store.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: RUN_SCRIPT_ACTION_ID, + title: localize('runScriptNoAction', "Run Script"), + tooltip: localize('runScriptTooltipNoAction', "Configure run action"), + icon: Codicon.play, + category: localize2('agentSessions', 'Agent Sessions'), + precondition: configureScriptPrecondition, + menu: [{ + id: RunScriptDropdownMenuId, + when: configureScriptPrecondition, + group: 'navigation', + order: 0, + }] + }); + } - async run(): Promise { - if (activeSession.action) { - await that._runScript(activeSession.cwd, activeSession.action); - } else { - // Open quick pick to configure run action - await that._showConfigureQuickPick(activeSession); + async run(): Promise { + await that._showConfigureQuickPick(session, cwd); } + })); + } else { + // Register an action for each script + for (let i = 0; i < scripts.length; i++) { + const script = scripts[i]; + const actionId = `${RUN_SCRIPT_ACTION_ID}.${i}`; + + reader.store.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: actionId, + title: script.name, + tooltip: localize('runScriptTooltip', "Run '{0}' in terminal", script.name), + icon: Codicon.play, + category: localize2('agentSessions', 'Agent Sessions'), + menu: [{ + id: RunScriptDropdownMenuId, + group: '0_scripts', + order: i, + }] + }); + } + + async run(): Promise { + await that._runScript(cwd, script); + } + })); } - })); + } - // Configure run action (shown in dropdown) + // Configure run action (always shown in dropdown) reader.store.add(registerAction2(class extends Action2 { constructor() { super({ id: CONFIGURE_DEFAULT_RUN_ACTION_ID, - title: localize2('configureDefaultRunAction', "Configure Run Action..."), + title: localize2('configureDefaultRunAction', "Add Run Script..."), category: localize2('agentSessions', 'Agent Sessions'), - icon: Codicon.play, + icon: Codicon.add, + precondition: configureScriptPrecondition, menu: [{ id: RunScriptDropdownMenuId, - group: '0_configure', + group: '1_configure', order: 0 }] }); } async run(): Promise { - await that._showConfigureQuickPick(activeSession); + await that._showConfigureQuickPick(session, cwd); } })); })); } - private async _showConfigureQuickPick(activeSession: IRunScriptActionContext): Promise { - - // Show input box for command + private async _showConfigureQuickPick(session: IActiveSessionItem, cwd: URI): Promise { const command = await this._quickInputService.input({ placeHolder: localize('enterCommandPlaceholder', "Enter command (e.g., npm run dev)"), prompt: localize('enterCommandPrompt', "This command will be run in the integrated terminal") }); if (command) { - const storedAction: IStoredRunAction = { - name: command, - command - }; - this._setStoredDefaultAction(activeSession.storageKey, storedAction); - await this._runScript(activeSession.cwd, storedAction); + const script: ISessionScript = { name: command, command }; + await this._sessionsConfigService.addScript(script, session); + await this._runScript(cwd, script); } } - private async _runScript(cwd: URI, action: IStoredRunAction): Promise { - // Create a new terminal and run the command + private async _runScript(cwd: URI, script: ISessionScript): Promise { const terminal = await this._terminalService.createTerminal({ location: TerminalLocation.Panel, config: { - name: action.name + name: script.name }, cwd }); - terminal.sendText(action.command, true); + terminal.sendText(script.command, true); await this._terminalService.revealTerminal(terminal); } } diff --git a/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts new file mode 100644 index 0000000000000..f2ac0049198c3 --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, IObservable, observableSignal, observableValue } from '../../../../base/common/observable.js'; +import { joinPath } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; + +const SESSIONS_CONFIG_RELATIVE = '.vscode/sessions.json'; + +export interface ISessionScript { + readonly name: string; + readonly command: string; +} + +function isISessionScript(s: unknown): s is ISessionScript { + return typeof s === 'object' && s !== null && + typeof (s as ISessionScript).name === 'string' && + typeof (s as ISessionScript).command === 'string'; +} + +export interface ISessionsConfigurationService { + readonly _serviceBrand: undefined; + + /** + * Observable list of scripts for the active session. + * Automatically reloads when the active session changes or the file is modified. + */ + getScripts(session: IActiveSessionItem): IObservable; + + /** Append a script to the session's config file. */ + addScript(script: ISessionScript, session: IActiveSessionItem): Promise; + + /** Remove a script from the session's config file. */ + removeScript(script: ISessionScript, session: IActiveSessionItem): Promise; +} + +export const ISessionsConfigurationService = createDecorator('sessionsConfigurationService'); + +export class SessionsConfigurationService extends Disposable implements ISessionsConfigurationService { + + declare readonly _serviceBrand: undefined; + + private readonly _scripts = observableValue(this, []); + private readonly _refreshSignal = observableSignal(this); + + constructor( + @IFileService private readonly _fileService: IFileService, + @ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + + // Watch active session changes + file changes, load scripts reactively + this._register(autorun(reader => { + const activeSession = this._activeSessionService.activeSession.read(reader); + this._refreshSignal.read(reader); + + if (!activeSession) { + this._scripts.set([], undefined); + return; + } + + const configUri = this._getConfigFileUri(activeSession); + if (!configUri) { + this._scripts.set([], undefined); + return; + } + + // Watch the file for external changes + reader.store.add(this._fileService.watch(configUri)); + reader.store.add(this._fileService.onDidFilesChange(e => { + if (e.contains(configUri)) { + this._refreshSignal.trigger(undefined); + } + })); + + // Read the file (async, updates _scripts when done) + this._readScripts(configUri).then( + scripts => this._scripts.set(scripts, undefined), + () => this._scripts.set([], undefined), + ); + })); + } + + getScripts(_session: IActiveSessionItem): IObservable { + return this._scripts; + } + + async addScript(script: ISessionScript, session: IActiveSessionItem): Promise { + const uri = this._getConfigFileUri(session); + if (!uri) { + return; + } + + const current = await this._readScripts(uri); + const updated = [...current, script]; + await this._writeScripts(uri, updated, session); + } + + async removeScript(script: ISessionScript, session: IActiveSessionItem): Promise { + const uri = this._getConfigFileUri(session); + if (!uri) { + return; + } + + const current = await this._readScripts(uri); + const updated = current.filter(s => s.name !== script.name || s.command !== script.command); + await this._writeScripts(uri, updated, session); + } + + private _getConfigFileUri(session: IActiveSessionItem): URI | undefined { + const root = session.worktree ?? session.repository; + if (!root) { + return undefined; + } + return joinPath(root, SESSIONS_CONFIG_RELATIVE); + } + + private async _readScripts(uri: URI): Promise { + try { + const content = await this._fileService.readFile(uri); + const parsed = JSON.parse(content.value.toString()); + if (parsed && Array.isArray(parsed.scripts)) { + return parsed.scripts.filter(isISessionScript); + } + } catch { + // File doesn't exist or is malformed - return empty + } + return []; + } + + private async _writeScripts(uri: URI, scripts: readonly ISessionScript[], session: IActiveSessionItem): Promise { + const data = JSON.stringify({ scripts }, null, '\t'); + await this._fileService.writeFile(uri, VSBuffer.fromString(data)); + this._logService.trace(`[SessionsConfigurationService] Wrote ${scripts.length} script(s) to ${uri.toString()}`); + + await this._activeSessionService.commitWorktreeFiles(session, [uri]); + this._refreshSignal.trigger(undefined); + } +} diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 9a5d00df68597..4da1e012c757b 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -19,11 +19,13 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'files.autoSave': 'afterDelay', + 'git.autofetch': true, 'git.showProgress': false, 'github.copilot.chat.claudeCode.enabled': true, 'github.copilot.chat.cli.branchSupport.enabled': true, 'github.copilot.chat.languageContext.typescript.enabled': true, + 'github.copilot.chat.cli.mcp.enabled': true, 'inlineChat.affordance': 'editor', 'inlineChat.renderMode': 'hover', @@ -33,7 +35,11 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'workbench.startupEditor': 'none', 'workbench.tips.enabled': false, 'workbench.layoutControl.type': 'toggles', - 'workbench.editor.allowOpenInModalEditor': false + 'workbench.editor.allowOpenInModalEditor': false, + 'window.menuStyle': 'custom', + 'window.dialogStyle': 'custom', + + 'terminal.integrated.initialHint': false }, donotCache: true }]); diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css index b054dfd8f0d9b..1666be701275e 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css @@ -46,6 +46,8 @@ display: flex; align-items: center; gap: 4px; + padding-top: 10px; + padding-right: 12px; -webkit-user-select: none; user-select: none; } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index b25676606aa52..86a963db393c8 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -20,9 +20,12 @@ import { ChatAgentLocation } from '../../../../workbench/contrib/chat/common/con import { IAgentSession, isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { LocalChatSessionUri } from '../../../../workbench/contrib/chat/common/model/chatUri.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; +import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; +import { localize } from '../../../../nls.js'; export const IsNewChatSessionContext = new RawContextKey('isNewChatSession', true); @@ -73,11 +76,22 @@ export interface ISessionsManagementService { */ openNewSession(): void; + /** + * Create a new session and set it as active, without opening a chat view. + */ + createNewPendingSession(pendingSessionResource: URI): Promise; + /** * Open a new session, apply options, and send the initial request. * This is the main entry point for the new-chat welcome widget. */ sendRequestForNewSession(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap, folderUri?: URI): Promise; + + /** + * Commit files in a worktree and refresh the agent sessions model + * so the Changes view reflects the update. + */ + commitWorktreeFiles(session: IActiveSessionItem, fileUris: URI[]): Promise; } export const ISessionsManagementService = createDecorator('sessionsManagementService'); @@ -104,6 +118,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, @IViewsService private readonly viewsService: IViewsService, + @ICommandService private readonly commandService: ICommandService, ) { super(); @@ -224,6 +239,23 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } } + async createNewPendingSession(pendingSessionResource: URI): Promise { + const chatsSession = await this.chatSessionsService.getOrCreateChatSession(pendingSessionResource, CancellationToken.None); + const chatSessionItem: IChatSessionItem = { + resource: chatsSession.sessionResource, + label: localize('sessionsManagement.newPendingAgentSessionLabel', 'Pending Session'), + timing: { + created: Date.now(), + lastRequestStarted: undefined, + lastRequestEnded: undefined, + } + }; + const repository = this.getRepositoryFromSessionOption(chatsSession.sessionResource); + const activeSessionItem = { ...chatSessionItem, repository, worktree: undefined }; + this._activeSession.set(activeSessionItem, undefined); + return activeSessionItem; + } + /** * Open an existing agent session - set it as active and reveal it. */ @@ -392,6 +424,20 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this._activeSession.set(activeSessionItem, undefined); } + async commitWorktreeFiles(session: IActiveSessionItem, fileUris: URI[]): Promise { + const worktreeUri = session.worktree; + if (!worktreeUri) { + throw new Error('Cannot commit worktree files: active session has no associated worktree'); + } + for (const fileUri of fileUris) { + await this.commandService.executeCommand( + 'github.copilot.cli.sessions.commitToWorktree', + { worktreeUri, fileUri } + ); + } + await this.agentSessionsService.model.resolve(AgentSessionProviders.Background); + } + private loadLastSelectedSession(): URI | undefined { const cached = this.storageService.get(LAST_SELECTED_SESSION_KEY, StorageScope.WORKSPACE); if (!cached) { diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 81f0aa6849afb..a1e45eb3d0cb6 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -11,7 +11,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { autorun } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -44,7 +44,6 @@ import { getCustomizationTotalCount } from './customizationCounts.js'; const $ = DOM.$; export const SessionsViewId = 'agentic.workbench.view.sessionsView'; const SessionsViewFilterSubMenu = new MenuId('AgentSessionsViewFilterSubMenu'); -const SessionsViewHeaderMenu = new MenuId('AgentSessionsViewHeaderMenu'); const CUSTOMIZATIONS_COLLAPSED_KEY = 'agentSessions.customizationsCollapsed'; @@ -88,18 +87,15 @@ export class AgenticSessionsViewPane extends ViewPane { private createControls(parent: HTMLElement): void { const sessionsContainer = DOM.append(parent, $('.agent-sessions-container')); + // Sessions Filter (actions go to view title bar via menu registration) + const sessionsFilter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, { + filterMenuId: SessionsViewFilterSubMenu, + groupResults: () => AgentSessionsGrouping.Date + })); + // Sessions section (top, fills available space) const sessionsSection = DOM.append(sessionsContainer, $('.agent-sessions-section')); - // Sessions header with title and toolbar actions - const sessionsHeader = DOM.append(sessionsSection, $('.agent-sessions-header')); - const headerText = DOM.append(sessionsHeader, $('span')); - headerText.textContent = localize('sessions', "SESSIONS"); - const headerToolbarContainer = DOM.append(sessionsHeader, $('.agent-sessions-header-toolbar')); - this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, headerToolbarContainer, SessionsViewHeaderMenu, { - menuOptions: { shouldForwardArgs: true }, - })); - // Sessions content container const sessionsContent = DOM.append(sessionsSection, $('.agent-sessions-content')); @@ -116,12 +112,6 @@ export class AgenticSessionsViewPane extends ViewPane { keybindingHint.textContent = keybinding.getLabel() ?? ''; } - // Sessions filter: contributes filter actions via SessionsViewFilterSubMenu; actions are rendered in the sessions header toolbar (SessionsViewHeaderMenu) - const sessionsFilter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, { - filterMenuId: SessionsViewFilterSubMenu, - groupResults: () => AgentSessionsGrouping.Date - })); - // Sessions Control this.sessionsControlContainer = DOM.append(sessionsContent, $('.agent-sessions-control-container')); const sessionsControl = this.sessionsControl = this._register(this.instantiationService.createInstance(AgentSessionsControl, this.sessionsControlContainer, { @@ -297,12 +287,13 @@ KeybindingsRegistry.registerKeybindingRule({ primary: KeyMod.CtrlCmd | KeyCode.KeyN, }); -MenuRegistry.appendMenuItem(SessionsViewHeaderMenu, { +MenuRegistry.appendMenuItem(MenuId.ViewTitle, { submenu: SessionsViewFilterSubMenu, title: localize2('filterAgentSessions', "Filter Agent Sessions"), group: 'navigation', order: 3, icon: Codicon.filter, + when: ContextKeyExpr.equals('view', SessionsViewId) } satisfies ISubmenuItem); registerAction2(class RefreshAgentSessionsViewerAction extends Action2 { @@ -311,11 +302,8 @@ registerAction2(class RefreshAgentSessionsViewerAction extends Action2 { id: 'sessionsView.refresh', title: localize2('refresh', "Refresh Agent Sessions"), icon: Codicon.refresh, - menu: [{ - id: SessionsViewHeaderMenu, - group: 'navigation', - order: 1, - }], + f1: true, + category: localize2('sessionsViewCategory', "Agent Sessions"), }); } override run(accessor: ServicesAccessor) { @@ -333,9 +321,10 @@ registerAction2(class FindAgentSessionInViewerAction extends Action2 { title: localize2('find', "Find Agent Session"), icon: Codicon.search, menu: [{ - id: SessionsViewHeaderMenu, + id: MenuId.ViewTitle, group: 'navigation', order: 2, + when: ContextKeyExpr.equals('view', SessionsViewId), }] }); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 33aeea43820a5..d565aa7634c67 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2987,8 +2987,13 @@ export namespace ChatToolInvocationPart { language: data.language }; } else if ('commandLine' in data && 'language' in data) { + const presentationOverrides = data.presentationOverrides && typeof data.presentationOverrides.commandLine === 'string' ? { + commandLine: data.presentationOverrides.commandLine, + language: data.presentationOverrides.language + } : undefined; const result: IChatTerminalToolInvocationData = { kind: 'terminal', + presentationOverrides, commandLine: data.commandLine, language: data.language, terminalCommandOutput: typeof data.output?.text === 'string' ? { diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 982f5a620df98..454939c8afc4a 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -146,6 +146,10 @@ visibility: hidden; } +.monaco-workbench .part.titlebar.inactive > * { + opacity: 0.3; +} + .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label, .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item.monaco-dropdown-with-primary .action-label { color: var(--vscode-titleBar-activeForeground); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index 29d0f93534ec1..1cc7ae425003e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -366,7 +366,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart if (terminalToolsInThinking && !requiresConfirmation) { this._isInThinkingContainer = true; - this.domNode = this._createCollapsibleWrapper(progressPart.domNode, command, toolInvocation, context); + this.domNode = this._createCollapsibleWrapper(progressPart.domNode, displayCommand, toolInvocation, context); } else { this.domNode = progressPart.domNode; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index b429dfde84a39..8cc57f3238fd4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -214,11 +214,11 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { const agentMode = modes.builtin.find(mode => mode.id === ChatMode.Agent.id); const otherBuiltinModes = modes.builtin.filter(mode => { - return mode.id !== ChatMode.Agent.id && shouldShowBuiltInMode(mode, assignments.get()); + return mode.id !== ChatMode.Agent.id && shouldShowBuiltInMode(mode, assignments.get(), agentModeDisabledViaPolicy); }); const filteredCustomModes = modes.custom.filter(mode => { if (isModeConsideredBuiltIn(mode, this._productService)) { - return shouldShowBuiltInMode(mode, assignments.get()); + return shouldShowBuiltInMode(mode, assignments.get(), agentModeDisabledViaPolicy); } return true; }); @@ -335,19 +335,24 @@ function isModeConsideredBuiltIn(mode: IChatMode, productService: IProductServic return !isOrganizationPromptFile(modeUri, mode.source.extensionId, productService); } -function shouldShowBuiltInMode(mode: IChatMode, assignments: { showOldAskMode: boolean }): boolean { - // The built-in "Edit" mode is deprecated, but still supported for older conversations. - if (mode.id === ChatMode.Edit.id) { - return false; +function shouldShowBuiltInMode(mode: IChatMode, assignments: { showOldAskMode: boolean }, agentModeDisabledViaPolicy: boolean): boolean { + // The built-in "Edit" mode is deprecated, but still supported for older conversations and agent disablement. + if (mode.id === ChatMode.Edit.id || mode.name.get().toLowerCase() === 'edit') { + if (mode.id === ChatMode.Edit.id) { + return agentModeDisabledViaPolicy; + } else { + return !agentModeDisabledViaPolicy; + } } - // The "Ask" mode is a special case - we want to show either the old or new version based on the assignment, but not both + // The "Ask" mode is a special case - we want to show either the old or new version based on the assignment or agent disablement, but not both // We still support the old "Ask" mode for conversations that already use it. - if (mode.id === ChatMode.Ask.id) { - return assignments.showOldAskMode; - } - if (mode.name.get().toLowerCase() === 'ask') { - return !assignments.showOldAskMode; + if (mode.id === ChatMode.Ask.id || mode.name.get().toLowerCase() === 'ask') { + if (mode.id === ChatMode.Ask.id) { + return assignments.showOldAskMode || agentModeDisabledViaPolicy; + } else { + return !(assignments.showOldAskMode || agentModeDisabledViaPolicy); + } } return true; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts index f2d6dd208104b..763de5e22c7b9 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts @@ -18,11 +18,13 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; interface IRawChatFileContribution { readonly path: string; readonly name?: string; readonly description?: string; + readonly when?: string; } enum ChatContributionPoint { @@ -65,6 +67,10 @@ function registerChatFilesExtensionPoint(point: ChatContributionPoint) { description: localize('chatContribution.property.description', '(Optional) Description of the entry.'), deprecationMessage: localize('chatContribution.property.description.deprecated', 'Specify "description" in the prompt file itself instead.'), type: 'string' + }, + when: { + description: localize('chatContribution.property.when', '(Optional) A condition which must be true to enable this entry.'), + type: 'string' } } } @@ -122,8 +128,12 @@ export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribut ext.collector.error(localize('extension.invalid.path', "Extension '{0}' {1} entry '{2}' resolves outside the extension.", ext.description.identifier.value, contributionPoint, raw.path)); continue; } + if (raw.when && !ContextKeyExpr.deserialize(raw.when)) { + ext.collector.error(localize('extension.invalid.when', "Extension '{0}' {1} entry '{2}' has an invalid when clause: '{3}'.", ext.description.identifier.value, contributionPoint, raw.path, raw.when)); + continue; + } try { - const d = this.promptsService.registerContributedFile(type, fileUri, ext.description, raw.name, raw.description); + const d = this.promptsService.registerContributedFile(type, fileUri, ext.description, raw.name, raw.description, raw.when); this.registrations.set(key(ext.description.identifier, type, raw.path), d); } catch (e) { const msg = e instanceof Error ? e.message : String(e); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index c94c7a1069657..1ba51c3703aed 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -101,6 +101,7 @@ export interface IExtensionPromptPath extends IPromptPathBase { readonly source: ExtensionAgentSourceType; readonly name?: string; readonly description?: string; + readonly when?: string; } export interface ILocalPromptPath extends IPromptPathBase { readonly storage: PromptsStorage.local; @@ -391,7 +392,7 @@ export interface IPromptsService extends IDisposable { * Internal: register a contributed file. Returns a disposable that removes the contribution. * Not intended for extension authors; used by contribution point handler. */ - registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined): IDisposable; + registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined, when?: string): IDisposable; getPromptLocationLabel(promptPath: IPromptPath): string; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 912a103fe5845..ded88d9804ab3 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -40,6 +40,7 @@ import { HookSourceFormat, getHookSourceFormat, parseHooksFromFile } from '../ho import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IPathService } from '../../../../../services/path/common/pathService.js'; import { getTarget, mapClaudeModels, mapClaudeTools } from '../languageProviders/promptValidator.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; /** * Error thrown when a skill file is missing the required name attribute. @@ -133,6 +134,13 @@ export class PromptsService extends Disposable implements IPromptsService { [PromptsType.hook]: new ResourceMap>(), }; + /** + * Context keys referenced by contributed file `when` clauses. + */ + private readonly _contributedWhenKeys = new Set(); + private readonly _contributedWhenClauses = new Map(); + private readonly _onDidContributedWhenChange = this._register(new Emitter()); + constructor( @ILogService public readonly logger: ILogService, @ILabelService private readonly labelService: ILabelService, @@ -147,6 +155,7 @@ export class PromptsService extends Disposable implements IPromptsService { @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IPathService private readonly pathService: IPathService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); @@ -155,10 +164,19 @@ export class PromptsService extends Disposable implements IPromptsService { this.cachedParsedPromptFromModels.delete(model.uri); })); + this._register(this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(this._contributedWhenKeys)) { + for (const type of Object.keys(this.cachedFileLocations) as PromptsType[]) { + this.cachedFileLocations[type] = undefined; + } + this._onDidContributedWhenChange.fire(); + } + })); + const modelChangeEvent = this._register(new ModelChangeTracker(this.modelService)).onDidPromptChange; this.cachedCustomAgents = this._register(new CachedPromise( (token) => this.computeCustomAgents(token), - () => Event.any(this.getFileLocatorEvent(PromptsType.agent), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.agent)) + () => Event.any(this.getFileLocatorEvent(PromptsType.agent), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.agent), this._onDidContributedWhenChange.event) )); this.cachedSlashCommands = this._register(new CachedPromise( @@ -167,12 +185,13 @@ export class PromptsService extends Disposable implements IPromptsService { this.getFileLocatorEvent(PromptsType.prompt), this.getFileLocatorEvent(PromptsType.skill), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.prompt), - Event.filter(modelChangeEvent, e => e.promptType === PromptsType.skill)), + Event.filter(modelChangeEvent, e => e.promptType === PromptsType.skill), + this._onDidContributedWhenChange.event), )); this.cachedSkills = this._register(new CachedPromise( (token) => this.computeAgentSkills(token), - () => Event.any(this.getFileLocatorEvent(PromptsType.skill), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.skill)) + () => Event.any(this.getFileLocatorEvent(PromptsType.skill), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.skill), this._onDidContributedWhenChange.event) )); this.cachedHooks = this._register(new CachedPromise( @@ -388,7 +407,18 @@ export class PromptsService extends Disposable implements IPromptsService { const settledResults = await Promise.allSettled(this.contributedFiles[type].values()); const contributedFiles = settledResults .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map(result => result.value); + .map(result => result.value) + .filter(file => { + if (!file.when) { + return true; + } + const expr = ContextKeyExpr.deserialize(file.when); + if (!expr) { + this.logger.warn(`[getExtensionPromptFiles] Ignoring contributed prompt file with invalid when clause: ${file.when}`); + return false; + } + return this.contextKeyService.contextMatchesRules(expr); + }); const activationEvent = this.getProviderActivationEvent(type); if (!activationEvent) { @@ -631,7 +661,7 @@ export class PromptsService extends Disposable implements IPromptsService { return new PromptFileParser().parse(uri, fileContent.value.toString()); } - public registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name?: string, description?: string) { + public registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name?: string, description?: string, when?: string) { const bucket = this.contributedFiles[type]; if (bucket.has(uri)) { // keep first registration per extension (handler filters duplicates per extension already) @@ -657,11 +687,15 @@ export class PromptsService extends Disposable implements IPromptsService { const msg = e instanceof Error ? e.message : String(e); this.logger.error(`[registerContributedFile] Failed to make prompt file readonly: ${uri}`, msg); } - return { uri, name, description, storage: PromptsStorage.extension, type, extension, source: ExtensionAgentSourceType.contribution } satisfies IExtensionPromptPath; + return { uri, name, description, when, storage: PromptsStorage.extension, type, extension, source: ExtensionAgentSourceType.contribution } satisfies IExtensionPromptPath; })(); bucket.set(uri, entryPromise); + if (when) { + this._contributedWhenClauses.set(`${type}/${uri.toString()}`, when); + } const flushCachesIfRequired = () => { + this._updateContributedWhenKeys(); this.cachedFileLocations[type] = undefined; switch (type) { case PromptsType.agent: @@ -680,11 +714,22 @@ export class PromptsService extends Disposable implements IPromptsService { return { dispose: () => { bucket.delete(uri); + this._contributedWhenClauses.delete(`${type}/${uri.toString()}`); flushCachesIfRequired(); } }; } + private _updateContributedWhenKeys(): void { + this._contributedWhenKeys.clear(); + for (const whenClause of this._contributedWhenClauses.values()) { + const expr = ContextKeyExpr.deserialize(whenClause); + for (const key of expr?.keys() ?? []) { + this._contributedWhenKeys.add(key); + } + } + } + getPromptLocationLabel(promptPath: IPromptPath): string { switch (promptPath.storage) { case PromptsStorage.local: return this.labelService.getUriLabel(dirname(promptPath.uri), { relative: true }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts index 2d008a2567f07..abde6d1f8453d 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts @@ -44,6 +44,8 @@ import { IRemoteAgentService } from '../../../../../../workbench/services/remote import { basename } from '../../../../../../base/common/resources.js'; import { match } from '../../../../../../base/common/glob.js'; import { ChatModeKind } from '../../../common/constants.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; suite('ComputeAutomaticInstructions', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -175,6 +177,8 @@ suite('ComputeAutomaticInstructions', () => { getEnvironment: () => Promise.resolve(null), }); + instaService.stub(IContextKeyService, new MockContextKeyService()); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts index ab269f0e48597..525d116ff6520 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts @@ -54,7 +54,7 @@ export class MockPromptsService implements IPromptsService { // eslint-disable-next-line @typescript-eslint/no-explicit-any parseNew(_uri: URI, _token: CancellationToken): Promise { throw new Error('Not implemented'); } getParsedPromptFile(textModel: ITextModel): ParsedPromptFile { throw new Error('Not implemented'); } - registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined): IDisposable { throw new Error('Not implemented'); } + registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined, when?: string): IDisposable { throw new Error('Not implemented'); } getPromptLocationLabel(promptPath: IPromptPath): string { throw new Error('Not implemented'); } listNestedAgentMDs(token: CancellationToken): Promise { throw new Error('Not implemented'); } listAgentInstructions(token: CancellationToken): Promise { throw new Error('Not implemented'); } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index dd8f81d4dd3a8..e3c508205ac3f 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import * as sinon from 'sinon'; import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; -import { Event } from '../../../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../../../base/common/event.js'; import { match } from '../../../../../../../base/common/glob.js'; import { ResourceSet } from '../../../../../../../base/common/map.js'; import { Schemas } from '../../../../../../../base/common/network.js'; @@ -50,6 +50,8 @@ import { IExtensionService } from '../../../../../../services/extensions/common/ import { IRemoteAgentService } from '../../../../../../services/remote/common/remoteAgentService.js'; import { ChatModeKind } from '../../../../common/constants.js'; import { HookType } from '../../../../common/promptSyntax/hookSchema.js'; +import { IContextKeyService, IContextKeyChangeEvent } from '../../../../../../../platform/contextkey/common/contextkey.js'; +import { MockContextKeyService } from '../../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; suite('PromptsService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -158,6 +160,8 @@ suite('PromptsService', () => { getEnvironment: () => Promise.resolve(null), }); + instaService.stub(IContextKeyService, new MockContextKeyService()); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); @@ -1910,6 +1914,50 @@ suite('PromptsService', () => { registered1.dispose(); registered2.dispose(); }); + + test('Contributed file with when clause is filtered by context key', async () => { + const uri = URI.parse('file://extensions/my-extension/conditional.instructions.md'); + const extension = {} as IExtensionDescription; + + // Create a mock context key service that we can control + let matchResult = false; + const contextKeyChangeEmitter = disposables.add(new Emitter()); + const testContextKeyService = new class extends MockContextKeyService { + override contextMatchesRules(): boolean { + return matchResult; + } + override get onDidChangeContext() { + return contextKeyChangeEmitter.event; + } + }(); + instaService.stub(IContextKeyService, testContextKeyService); + const testService = disposables.add(instaService.createInstance(PromptsService)); + + const registered = testService.registerContributedFile( + PromptsType.instructions, uri, extension, + 'Conditional Instructions', 'Only when enabled', 'myFeature.enabled', + ); + + // When clause is false - should be filtered out + const before = await testService.listPromptFiles(PromptsType.instructions, CancellationToken.None); + assert.strictEqual(before.length, 0, 'Should be filtered out when context key is false'); + + // Change context to make when clause true + matchResult = true; + contextKeyChangeEmitter.fire({ + affectsSome: (keys) => keys.has('myFeature.enabled'), + allKeysContainedIn: () => false, + }); + + const after = await testService.listPromptFiles(PromptsType.instructions, CancellationToken.None); + assert.strictEqual(after.length, 1, 'Should be included when context key is true'); + assert.strictEqual(after[0].uri.toString(), uri.toString()); + + registered.dispose(); + + // Restore original stub + instaService.stub(IContextKeyService, new MockContextKeyService()); + }); }); test('Instructions provider', async () => { diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts index 2a60ae28108aa..2a2ae1cc5d6bf 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { reverseOrder, compareBy, numberComparator, sumBy } from '../../../../../base/common/arrays.js'; -import { IntervalTimer, TimeoutTimer } from '../../../../../base/common/async.js'; +import { IntervalTimer } from '../../../../../base/common/async.js'; import { toDisposable, Disposable } from '../../../../../base/common/lifecycle.js'; import { mapObservableArrayCached, derived, IObservable, observableSignal, runOnChange, autorun } from '../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -18,7 +18,7 @@ import { sumByCategory } from '../helpers/utils.js'; import { ScmAdapter, ScmRepoAdapter } from './scmAdapter.js'; import { IRandomService } from '../randomService.js'; -type EditTelemetryMode = 'longterm' | '5minWindow' | '20minFocusWindow'; +type EditTelemetryMode = 'longterm' | '10minFocusWindow' | '20minFocusWindow'; type EditTelemetryTrigger = '10hours' | 'hashChange' | 'branchChange' | 'closed' | 'time'; export class EditSourceTrackingImpl extends Disposable { @@ -112,7 +112,7 @@ class TrackedDocumentInfo extends Disposable { this._store.add(this._instantiationService.createInstance(EditTelemetryReportEditArcForChatOrInlineChatSender, _doc.documentWithAnnotations, this._repo)); this._store.add(this._instantiationService.createInstance(CreateSuggestionIdForChatOrInlineChatCaller, _doc.documentWithAnnotations)); - // Wall-clock time based 5-minute window tracker + // Focus time based 10-minute window tracker const resetSignal = observableSignal('resetSignal'); this.windowedTracker = derived((reader) => { @@ -123,17 +123,17 @@ class TrackedDocumentInfo extends Disposable { } resetSignal.read(reader); - // Reset after 5 minutes of wall-clock time - reader.store.add(new TimeoutTimer(() => { + // Reset after 10 minutes of accumulated focus time + reader.store.add(this._userAttentionService.fireAfterGivenFocusTimePassed(10 * 60 * 1000, () => { resetSignal.trigger(undefined); - }, 5 * 60 * 1000)); + })); const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined)); const startFocusTime = this._userAttentionService.totalFocusTimeMs; const startTime = Date.now(); reader.store.add(toDisposable(async () => { // send windowed document telemetry - this.sendTelemetry('5minWindow', 'time', t, this._userAttentionService.totalFocusTimeMs - startFocusTime, Date.now() - startTime); + this.sendTelemetry('10minFocusWindow', 'time', t, this._userAttentionService.totalFocusTimeMs - startFocusTime, Date.now() - startTime); t.dispose(); })); @@ -217,9 +217,9 @@ class TrackedDocumentInfo extends Disposable { totalModifiedCount: number; }, { owner: 'hediet'; - comment: 'Provides detailed character count breakdown for individual edit sources (typing, paste, inline completions, NES, etc.) within a session. Reports the top 10-30 sources per session with granular metadata including extension IDs and model IDs for AI edits. Sessions are scoped to either 5-minute wall-clock time windows, 20-minute focus time windows for visible documents, or longer periods ending on branch changes, commits, or 10-hour intervals. Focus time is computed as the accumulated time where VS Code has focus and there was recent user activity (within the last minute). This event complements editSources.stats by providing source-specific details. @sentToGitHub'; + comment: 'Provides detailed character count breakdown for individual edit sources (typing, paste, inline completions, NES, etc.) within a session. Reports the top 10-30 sources per session with granular metadata including extension IDs and model IDs for AI edits. Sessions are scoped to either 10-minute or 20-minute focus time windows for visible documents, or longer periods ending on branch changes, commits, or 10-hour intervals. Focus time is computed as the accumulated time where VS Code has focus and there was recent user activity (within the last minute). This event complements editSources.stats by providing source-specific details. @sentToGitHub'; - mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Describes the session mode. Is either \'longterm\', \'5minWindow\', or \'20minFocusWindow\'.' }; + mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Describes the session mode. Is either \'longterm\', \'10minFocusWindow\', or \'20minFocusWindow\'.' }; sourceKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A description of the source of the edit.' }; sourceKeyCleaned: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the edit with some properties (such as extensionId, extensionVersion and modelId) removed.' }; @@ -275,9 +275,9 @@ class TrackedDocumentInfo extends Disposable { trigger: EditTelemetryTrigger; }, { owner: 'hediet'; - comment: 'Aggregates character counts by edit source category (user typing, AI completions, NES, IDE actions, external changes) for each editing session. Sessions represent units of work and end when documents close, branches change, commits occur, or time limits are reached (5 minutes of wall-clock time, 20 minutes of focus time for visible documents, or 10 hours otherwise). Focus time is computed as accumulated 1-minute blocks where VS Code has focus and there was recent user activity. Tracks both total characters inserted and characters remaining at session end to measure retention. This high-level summary complements editSources.details which provides granular per-source breakdowns. @sentToGitHub'; + comment: 'Aggregates character counts by edit source category (user typing, AI completions, NES, IDE actions, external changes) for each editing session. Sessions represent units of work and end when documents close, branches change, commits occur, or time limits are reached (10 or 20 minutes of focus time for visible documents, or 10 hours otherwise). Focus time is computed as accumulated 1-minute blocks where VS Code has focus and there was recent user activity. Tracks both total characters inserted and characters remaining at session end to measure retention. This high-level summary complements editSources.details which provides granular per-source breakdowns. @sentToGitHub'; - mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm, 5minWindow, or 20minFocusWindow' }; + mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm, 10minFocusWindow, or 20minFocusWindow' }; languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' }; statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' }; diff --git a/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts b/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts index 8c6cf1052645f..5ac15f260deb4 100644 --- a/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts +++ b/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts @@ -111,11 +111,11 @@ function fib(n) { '00:11:010 editTelemetry.codeSuggested: {\"eventId\":\"evt-5c9c6fe7-b219-4ff8-aaa7-ab2b355b21c0\",\"suggestionId\":\"sgt-74379122-0452-4e26-9c38-9d62f1e7ae73\",\"presentation\":\"highlightedEdit\",\"feature\":\"sideBarChat\",\"languageId\":\"plaintext\",\"editCharsInserted\":19,\"editCharsDeleted\":20,\"editLinesInserted\":1,\"editLinesDeleted\":1,\"modelId\":{\"isTrustedTelemetryValue\":true}}', '01:01:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e\",\"didBranchChange\":0,\"timeDelayMs\":60000,\"originalCharCount\":37,\"originalLineCount\":1,\"originalDeletedLineCount\":0,\"arc\":16,\"currentLineCount\":1,\"currentDeletedLineCount\":0}', '01:11:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"1eb8a394-2489-41c2-851b-6a79432fc6bc\",\"didBranchChange\":0,\"timeDelayMs\":60000,\"originalCharCount\":19,\"originalLineCount\":1,\"originalDeletedLineCount\":1,\"arc\":19,\"currentLineCount\":1,\"currentDeletedLineCount\":1}', - '05:00:000 editTelemetry.editSources.details: {\"mode\":\"5minWindow\",\"sourceKey\":\"source:Chat.applyEdits\",\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":35,\"deltaModifiedCount\":56,\"totalModifiedCount\":39}', - '05:00:000 editTelemetry.editSources.details: {\"mode\":\"5minWindow\",\"sourceKey\":\"source:cursor-kind:type\",\"sourceKeyCleaned\":\"source:cursor-kind:type\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":4,\"deltaModifiedCount\":4,\"totalModifiedCount\":39}', - '05:00:000 editTelemetry.editSources.stats: {\"mode\":\"5minWindow\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"nesModifiedCount\":0,\"inlineCompletionsCopilotModifiedCount\":0,\"inlineCompletionsNESModifiedCount\":0,\"otherAIModifiedCount\":35,\"unknownModifiedCount\":0,\"userModifiedCount\":4,\"ideModifiedCount\":0,\"totalModifiedCharacters\":39,\"externalModifiedCount\":0,\"isTrackedByGit\":0,\"focusTime\":250010,\"actualTime\":300000,\"trigger\":\"time\"}', '05:01:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e\",\"didBranchChange\":0,\"timeDelayMs\":300000,\"originalCharCount\":37,\"originalLineCount\":1,\"originalDeletedLineCount\":0,\"arc\":16,\"currentLineCount\":1,\"currentDeletedLineCount\":0}', '05:11:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"1eb8a394-2489-41c2-851b-6a79432fc6bc\",\"didBranchChange\":0,\"timeDelayMs\":300000,\"originalCharCount\":19,\"originalLineCount\":1,\"originalDeletedLineCount\":1,\"arc\":19,\"currentLineCount\":1,\"currentDeletedLineCount\":1}', + '12:00:000 editTelemetry.editSources.details: {\"mode\":\"10minFocusWindow\",\"sourceKey\":\"source:Chat.applyEdits\",\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":35,\"deltaModifiedCount\":56,\"totalModifiedCount\":39}', + '12:00:000 editTelemetry.editSources.details: {\"mode\":\"10minFocusWindow\",\"sourceKey\":\"source:cursor-kind:type\",\"sourceKeyCleaned\":\"source:cursor-kind:type\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":4,\"deltaModifiedCount\":4,\"totalModifiedCount\":39}', + '12:00:000 editTelemetry.editSources.stats: {\"mode\":\"10minFocusWindow\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"nesModifiedCount\":0,\"inlineCompletionsCopilotModifiedCount\":0,\"inlineCompletionsNESModifiedCount\":0,\"otherAIModifiedCount\":35,\"unknownModifiedCount\":0,\"userModifiedCount\":4,\"ideModifiedCount\":0,\"totalModifiedCharacters\":39,\"externalModifiedCount\":0,\"isTrackedByGit\":0,\"focusTime\":600000,\"actualTime\":720000,\"trigger\":\"time\"}', '22:00:000 editTelemetry.editSources.details: {\"mode\":\"20minFocusWindow\",\"sourceKey\":\"source:Chat.applyEdits\",\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"modifiedCount\":35,\"deltaModifiedCount\":56,\"totalModifiedCount\":39}', '22:00:000 editTelemetry.editSources.details: {\"mode\":\"20minFocusWindow\",\"sourceKey\":\"source:cursor-kind:type\",\"sourceKeyCleaned\":\"source:cursor-kind:type\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"modifiedCount\":4,\"deltaModifiedCount\":4,\"totalModifiedCount\":39}', '22:00:000 editTelemetry.editSources.stats: {\"mode\":\"20minFocusWindow\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"nesModifiedCount\":0,\"inlineCompletionsCopilotModifiedCount\":0,\"inlineCompletionsNESModifiedCount\":0,\"otherAIModifiedCount\":35,\"unknownModifiedCount\":0,\"userModifiedCount\":4,\"ideModifiedCount\":0,\"totalModifiedCharacters\":39,\"externalModifiedCount\":0,\"isTrackedByGit\":0,\"focusTime\":1200000,\"actualTime\":1320000,\"trigger\":\"time\"}' diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index ce4653f5cfbea..f99d40fbd0fb5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -6,10 +6,10 @@ import './inlineChatDefaultModel.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import { IMenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IMenuItem, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { InlineChatController } from './inlineChatController.js'; import * as InlineChatActions from './inlineChatActions.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS, ACTION_START } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; @@ -23,8 +23,6 @@ import { localize } from '../../../../nls.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; -import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { Codicon } from '../../../../base/common/codicons.js'; registerEditorContribution(InlineChatController.ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors @@ -86,25 +84,17 @@ const cancelActionMenuItem: IMenuItem = { MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, cancelActionMenuItem); -// --- InlineChatEditorAffordance menu --- -MenuRegistry.appendMenuItem(MenuId.InlineChatEditorAffordance, { - group: '0_chat', - order: 1, - command: { - id: ACTION_START, - title: localize('editCode', "Ask for Edits"), - shortTitle: localize('editCodeShort', "Ask for Edits"), - icon: Codicon.sparkle, - }, - when: EditorContextKeys.hasNonEmptySelection, -}); // --- actions --- registerAction2(InlineChatActions.StartSessionAction); +registerAction2(InlineChatActions.AskInChatAction); registerAction2(InlineChatActions.FocusInlineChat); registerAction2(InlineChatActions.SubmitInlineChatInputAction); +registerAction2(InlineChatActions.SubmitToChatAction); +registerAction2(InlineChatActions.AttachToChatAction); +registerAction2(InlineChatActions.HideInlineChatInputAction); const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 1b1a639631153..39bbc646a0099 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,10 +11,10 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED, CTX_HOVER_MODE, CTX_INLINE_CHAT_INPUT_HAS_TEXT } from '../common/inlineChat.js'; +import { ACTION_ACCEPT_CHANGES, ACTION_ASK_IN_CHAT, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED, CTX_HOVER_MODE, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, InlineChatConfigKeys } from '../common/inlineChat.js'; import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js'; import { localize, localize2 } from '../../../../nls.js'; -import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; +import { Action2, IAction2Options, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; @@ -25,6 +25,13 @@ import { CommandsRegistry } from '../../../../platform/commands/common/commands. import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IChatEditingService } from '../../chat/common/editing/chatEditingService.js'; +import { IChatWidgetService } from '../../chat/browser/chat.js'; +import { IAgentFeedbackVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { basename } from '../../../../base/common/resources.js'; +import { ChatRequestQueueKind } from '../../chat/common/chatService/chatService.js'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); @@ -59,7 +66,7 @@ export class StartSessionAction extends Action2 { shortTitle: localize2('runShort', 'Inline Chat'), category: AbstractInlineChatAction.category, f1: true, - precondition: inlineChatContextKey, + precondition: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), keybinding: { when: EditorContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib, @@ -103,6 +110,8 @@ export class StartSessionAction extends Action2 { private async _runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) { + const configServce = accessor.get(IConfigurationService); + const ctrl = InlineChatController.get(editor); if (!ctrl) { return; @@ -117,10 +126,36 @@ export class StartSessionAction extends Action2 { if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { options = arg; } - await InlineChatController.get(editor)?.run({ ...options }); + + // use hover overlay to ask for input + if (!options?.message && configServce.getValue(InlineChatConfigKeys.RenderMode) === 'hover') { + const selection = editor.getSelection(); + const placeholder = selection && !selection.isEmpty() + ? localize('placeholderWithSelection', "Describe how to change this") + : localize('placeholderNoSelection', "Describe what to generate"); + // show menu and RETURN because the menu is re-entrant + await ctrl.inputOverlayWidget.showMenuAtSelection(placeholder); + return; + } + + await ctrl?.run({ ...options }); } } +// --- InlineChatEditorAffordance menu --- + +MenuRegistry.appendMenuItem(MenuId.InlineChatEditorAffordance, { + group: '0_chat', + order: 1, + when: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasNonEmptySelection, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + command: { + id: ACTION_START, + title: localize('editCode', "Ask for Edits"), + shortTitle: localize('editCodeShort', "Ask for Edits"), + icon: Codicon.sparkle, + } +}); + export class FocusInlineChat extends EditorAction2 { constructor() { @@ -334,11 +369,17 @@ export class SubmitInlineChatInputAction extends AbstractInlineChatAction { id: 'inlineChat.submitInput', title: localize2('submitInput', "Send"), icon: Codicon.send, - precondition: CTX_INLINE_CHAT_INPUT_HAS_TEXT, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + weight: KeybindingWeight.EditorCore + 10, + primary: KeyCode.Enter + }, menu: [{ id: MenuId.InlineChatInput, group: '0_main', order: 1, + when: CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate() }] }); } @@ -351,3 +392,206 @@ export class SubmitInlineChatInputAction extends AbstractInlineChatAction { } } } + +export class HideInlineChatInputAction extends AbstractInlineChatAction { + + constructor() { + super({ + id: 'inlineChat.hideInput', + title: localize2('hideInput', "Hide Input"), + precondition: CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, + keybinding: { + when: CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, + weight: KeybindingWeight.EditorCore + 10, + primary: KeyCode.Escape + } + }); + } + + override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: unknown[]): void { + ctrl.inputWidget.hide(); + } +} + + +export class AskInChatAction extends EditorAction2 { + + constructor() { + super({ + id: ACTION_ASK_IN_CHAT, + title: localize2('askInChat', 'Ask in Chat'), + category: AbstractInlineChatAction.category, + f1: true, + precondition: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + keybinding: { + when: EditorContextKeys.focus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyI + }, + icon: Codicon.chatSparkle, + menu: [{ + id: MenuId.EditorContext, + group: '1_chat', + order: 3, + when: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT) + }, { + id: MenuId.InlineChatEditorAffordance, + group: '0_chat', + order: 1, + when: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT) + }] + }); + } + + override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor) { + const chatEditingService = accessor.get(IChatEditingService); + const ctrl = InlineChatController.get(editor); + if (!ctrl || !editor.hasModel()) { + return; + } + const entry = chatEditingService.editingSessionsObs.get().find(value => value.getEntry(editor.getModel().uri)); + if (entry) { + ctrl.inputOverlayWidget.showMenuAtSelection(localize('placeholderAskInChat', "Describe how to proceed in Chat")); + } + } +} + +export class SubmitToChatAction extends AbstractInlineChatAction { + + + constructor() { + super({ + id: 'inlineChat.submitToChat', + title: localize2('submitToChat', "Send to Chat"), + icon: Codicon.arrowUp, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + weight: KeybindingWeight.EditorCore + 10, + primary: KeyCode.Enter + }, + menu: [{ + id: MenuId.InlineChatInput, + group: '0_main', + order: 1, + when: CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, + alt: { + id: AttachToChatAction.Id, + title: localize2('attachToChat', "Attach to Chat"), + icon: Codicon.attach + } + }] + }); + } + + override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor): Promise { + const chatEditingService = accessor.get(IChatEditingService); + const chatWidgetService = accessor.get(IChatWidgetService); + if (!editor.hasModel()) { + return; + } + + const value = ctrl.inputWidget.value; + ctrl.inputWidget.hide(); + if (!value) { + return; + } + + const session = chatEditingService.editingSessionsObs.get().find(s => s.getEntry(editor.getModel().uri)); + if (!session) { + return; + } + + const widget = await chatWidgetService.openSession(session.chatSessionResource); + if (!widget) { + return; + } + + const selection = editor.getSelection(); + if (selection && !selection.isEmpty()) { + await widget.attachmentModel.addFile(editor.getModel().uri, selection); + } + await widget.acceptInput(value, { queue: ChatRequestQueueKind.Queued }); + } +} + +export class AttachToChatAction extends AbstractInlineChatAction { + + static readonly Id = 'inlineChat.attachToChat'; + + constructor() { + super({ + id: AttachToChatAction.Id, + title: localize2('attachToChat', "Attach to Chat"), + icon: Codicon.attach, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + weight: KeybindingWeight.EditorCore + 10, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + secondary: [KeyMod.Alt | KeyCode.Enter] + }, + }); + } + + override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor): Promise { + const chatEditingService = accessor.get(IChatEditingService); + const chatWidgetService = accessor.get(IChatWidgetService); + if (!editor.hasModel()) { + return; + } + + const value = ctrl.inputWidget.value; + const selection = editor.getSelection(); + ctrl.inputWidget.hide(); + if (!value || !selection || selection.isEmpty()) { + return; + } + + const session = chatEditingService.editingSessionsObs.get().find(s => s.getEntry(editor.getModel().uri)); + if (!session) { + return; + } + + const widget = await chatWidgetService.openSession(session.chatSessionResource); + if (!widget) { + return; + } + + const uri = editor.getModel().uri; + const selectedText = editor.getModel().getValueInRange(selection); + const fileName = basename(uri); + const lineRef = selection.startLineNumber === selection.endLineNumber + ? `${selection.startLineNumber}` + : `${selection.startLineNumber}-${selection.endLineNumber}`; + + const feedbackValue = [ + ``, + ``, + selectedText, + ``, + ``, + value, + ``, + `` + ].join('\n'); + + const feedbackId = generateUuid(); + const entry: IAgentFeedbackVariableEntry = { + kind: 'agentFeedback', + id: `inlineChat.feedback.${feedbackId}`, + name: localize('attachToChat.name', "{0}:{1}", fileName, lineRef), + icon: Codicon.comment, + sessionResource: session.chatSessionResource, + feedbackItems: [{ + id: feedbackId, + text: value, + resourceUri: uri, + range: selection, + }], + value: feedbackValue, + }; + + widget.attachmentModel.addContext(entry); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index ea0933b7e448c..4e3cf4c660003 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -44,7 +44,7 @@ export class InlineChatAffordance extends Disposable { readonly #editor: ICodeEditor; readonly #inputWidget: InlineChatInputWidget; readonly #instantiationService: IInstantiationService; - readonly #menuData = observableValue<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>(this, undefined); + readonly #menuData = observableValue<{ rect: DOMRect; above: boolean; lineNumber: number; placeholder: string } | undefined>(this, undefined); constructor( editor: ICodeEditor, @@ -118,7 +118,6 @@ export class InlineChatAffordance extends Disposable { InlineChatGutterAffordance, editorObs, derived(r => affordance.read(r) === 'gutter' ? selectionData.read(r) : undefined), - this.#menuData )); const editorAffordance = this.#instantiationService.createInstance( @@ -157,7 +156,7 @@ export class InlineChatAffordance extends Disposable { const left = data.rect.left - editorRect.left; // Show the overlay widget - this.#inputWidget.show(data.lineNumber, left, data.above); + this.#inputWidget.show(data.lineNumber, left, data.above, data.placeholder); })); this._store.add(autorun(r => { @@ -168,7 +167,7 @@ export class InlineChatAffordance extends Disposable { })); } - async showMenuAtSelection() { + async showMenuAtSelection(placeholder: string): Promise { assertType(this.#editor.hasModel()); const direction = this.#editor.getSelection().getDirection(); @@ -182,7 +181,8 @@ export class InlineChatAffordance extends Disposable { this.#menuData.set({ rect: new DOMRect(x, y, 0, scrolledPosition.height), above: direction === SelectionDirection.RTL, - lineNumber: position.lineNumber + lineNumber: position.lineNumber, + placeholder }, undefined); await waitForState(this.#inputWidget.position, pos => pos === null); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 79335bf93cd9a..6415bfcfe49de 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -39,7 +39,7 @@ import { ISharedWebContentExtractorService } from '../../../../platform/webConte import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { IChatAttachmentResolveService } from '../../chat/browser/attachments/chatAttachmentResolveService.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js'; -import { ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; +import { IChatEditingService, ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; import { ChatModel } from '../../chat/common/model/chatModel.js'; import { ChatMode } from '../../chat/common/chatModes.js'; import { IChatService } from '../../chat/common/chatService/chatService.js'; @@ -51,14 +51,13 @@ import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../. import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; -import { CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; import { InlineChatAffordance } from './inlineChatAffordance.js'; import { InlineChatInputWidget, InlineChatSessionOverlayWidget } from './inlineChatOverlayWidget.js'; import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; - export abstract class InlineChatRunOptions { initialSelection?: ISelection; @@ -117,7 +116,7 @@ export class InlineChatController implements IEditorContribution { private readonly _isActiveController = observableValue(this, false); private readonly _renderMode: IObservable<'zone' | 'hover'>; private readonly _zone: Lazy; - private readonly _gutterIndicator: InlineChatAffordance; + readonly inputOverlayWidget: InlineChatAffordance; private readonly _inputWidget: InlineChatInputWidget; private readonly _currentSession: IObservable; @@ -149,17 +148,42 @@ export class InlineChatController implements IEditorContribution { @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, @ILanguageModelsService private readonly _languageModelService: ILanguageModelsService, @ILogService private readonly _logService: ILogService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, ) { const editorObs = observableCodeEditor(_editor); - const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); + const ctxFileBelongsToChat = CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.bindTo(contextKeyService); const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService); + // Track whether the current editor's file is being edited by any chat editing session + this._store.add(autorun(r => { + const model = editorObs.model.read(r); + if (!model) { + ctxFileBelongsToChat.set(false); + return; + } + const sessions = this._chatEditingService.editingSessionsObs.read(r); + let hasEdits = false; + for (const session of sessions) { + const entries = session.entries.read(r); + for (const entry of entries) { + if (isEqual(entry.modifiedURI, model.uri)) { + hasEdits = true; + break; + } + } + if (hasEdits) { + break; + } + } + ctxFileBelongsToChat.set(hasEdits); + })); + const overlayWidget = this._inputWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs)); const sessionOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatSessionOverlayWidget, editorObs)); - this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); + this.inputOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); this._zone = new Lazy(() => { @@ -231,8 +255,6 @@ export class InlineChatController implements IEditorContribution { return result; }); - - const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessionService.onDidChangeSessions); this._currentSession = derived(r => { @@ -474,18 +496,10 @@ export class InlineChatController implements IEditorContribution { existingSession.dispose(); } - // use hover overlay to ask for input - if (!arg?.message && this._configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'hover') { - // show menu and RETURN because the menu is re-entrant - await this._gutterIndicator.showMenuAtSelection(); - return true; - } - this._isActiveController.set(true, undefined); const session = this._inlineChatSessionService.createSession(this._editor); - // Store for tracking model changes during this session const sessionStore = new DisposableStore(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index 519ae158e7f68..1123978c2e85b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -28,7 +28,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { ACTION_START } from '../common/inlineChat.js'; +import { ACTION_START, ACTION_ASK_IN_CHAT } from '../common/inlineChat.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; class QuickFixActionViewItem extends MenuEntryActionViewItem { @@ -105,7 +105,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { } } -class InlineChatStartActionViewItem extends MenuEntryActionViewItem { +class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { private readonly _kbLabel: string | undefined; @@ -150,7 +150,7 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi private readonly _onDidRunAction = this._store.add(new Emitter()); readonly onDidRunAction: Event = this._onDidRunAction.event; - readonly allowEditorOverflow = false; + readonly allowEditorOverflow = true; readonly suppressMouseDown = false; constructor( @@ -173,8 +173,8 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi if (action instanceof MenuItemAction && action.id === quickFixCommandId) { return instantiationService.createInstance(QuickFixActionViewItem, action, this._editor); } - if (action instanceof MenuItemAction && action.id === ACTION_START) { - return instantiationService.createInstance(InlineChatStartActionViewItem, action); + if (action instanceof MenuItemAction && (action.id === ACTION_START || action.id === ACTION_ASK_IN_CHAT)) { + return instantiationService.createInstance(LabelWithKeybindingActionViewItem, action); } return undefined; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts index 19d67a29e9503..3d82cec90ec04 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -5,11 +5,11 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { autorun, constObservable, derived, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { constObservable, derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { ObservableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; -import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { Selection } from '../../../../editor/common/core/selection.js'; import { InlineCompletionCommand } from '../../../../editor/common/languages.js'; import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.js'; @@ -30,9 +30,8 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { readonly onDidRunAction: Event = this._onDidRunAction.event; constructor( - private readonly _myEditorObs: ObservableCodeEditor, + myEditorObs: ObservableCodeEditor, selection: IObservable, - private readonly _hover: ISettableObservable<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>, @IKeybindingService _keybindingService: IKeybindingService, @IHoverService hoverService: HoverService, @IInstantiationService instantiationService: IInstantiationService, @@ -46,7 +45,7 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { const menu = menuService.createMenu(MenuId.InlineChatEditorAffordance, contextKeyService); const menuObs = observableFromEvent(menu.onDidChange, () => menu.getActions({ renderShortTitle: false })); - const codeActionController = CodeActionController.get(_myEditorObs.editor); + const codeActionController = CodeActionController.get(myEditorObs.editor); const lightBulbObs = codeActionController?.lightBulbState; const data = derived(r => { @@ -93,7 +92,7 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { return new InlineEditsGutterIndicatorData( gutterMenuData, lineRange, - new SimpleInlineSuggestModel(() => { }, () => this._doShowHover()), + new SimpleInlineSuggestModel(() => { }, () => { }), undefined, // altAction { icon } ); @@ -102,36 +101,13 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { const focusIsInMenu = observableValue({}, false); super( - _myEditorObs, data, constObservable(InlineEditTabAction.Inactive), constObservable(0), constObservable(false), focusIsInMenu, + myEditorObs, data, constObservable(InlineEditTabAction.Inactive), constObservable(0), constObservable(false), focusIsInMenu, hoverService, instantiationService, accessibilityService, themeService, userInteractionService ); this._store.add(menu); - this._store.add(autorun(r => { - const element = _hover.read(r); - this._hoverVisible.set(!!element, undefined); - })); this._store.add(this.onDidCloseWithCommand(commandId => this._onDidRunAction.fire(commandId))); } - - private _doShowHover(): void { - if (this._hoverVisible.get()) { - return; - } - - // Use the icon element from the base class as anchor - const iconElement = this._iconRef.element; - if (!iconElement) { - this._hover.set(undefined, undefined); - return; - } - - const selection = this._myEditorObs.cursorSelection.get(); - const direction = selection?.getDirection() ?? SelectionDirection.LTR; - const lineNumber = selection?.getPosition().lineNumber ?? 1; - this._hover.set({ rect: iconElement.getBoundingClientRect(), above: direction === SelectionDirection.RTL, lineNumber }, undefined); - } - } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 844b7dae00309..1d27ddb7da5dd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -28,9 +28,8 @@ import { getFlatActionBarActions } from '../../../../platform/actions/browser/me import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ChatEditingAcceptRejectActionViewItem } from '../../chat/browser/chatEditing/chatEditingEditorOverlay.js'; -import { CTX_INLINE_CHAT_INPUT_HAS_TEXT } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED } from '../common/inlineChat.js'; import { StickyScrollController } from '../../../../editor/contrib/stickyScroll/browser/stickyScrollController.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -53,7 +52,6 @@ export class InlineChatInputWidget extends Disposable { private readonly _position = observableValue(this, null); readonly position: IObservable = this._position; - private readonly _showStore = this._store.add(new DisposableStore()); private readonly _stickyScrollHeight: IObservable; private readonly _layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>; @@ -65,7 +63,6 @@ export class InlineChatInputWidget extends Disposable { constructor( private readonly _editorObs: ObservableCodeEditor, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @ICommandService private readonly _commandService: ICommandService, @IMenuService private readonly _menuService: IMenuService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, @@ -153,24 +150,14 @@ export class InlineChatInputWidget extends Disposable { this._store.add(resizeObserver); this._store.add(resizeObserver.observe(toolbar.getElement())); - // Compute min and max widget width based on editor content width - const maxWidgetWidth = derived(r => { - const layoutInfo = this._editorObs.layoutInfo.read(r); - return Math.max(0, Math.round(layoutInfo.contentWidth * 0.70)); - }); - const minWidgetWidth = derived(r => { - const layoutInfo = this._editorObs.layoutInfo.read(r); - return Math.max(0, Math.round(layoutInfo.contentWidth * 0.33)); - }); - const contentWidth = observableFromEvent(this, this._input.onDidChangeModelContent, () => this._input.getContentWidth()); const contentHeight = observableFromEvent(this, this._input.onDidContentSizeChange, () => this._input.getContentHeight()); this._layoutData = derived(r => { const editorPad = 6; const totalWidth = contentWidth.read(r) + editorPad + toolbarWidth.read(r); - const minWidth = minWidgetWidth.read(r); - const maxWidth = maxWidgetWidth.read(r); + const minWidth = 220; + const maxWidth = 600; const clampedWidth = this._input.getOption(EditorOption.wordWrap) === 'on' ? maxWidth : Math.max(minWidth, Math.min(totalWidth, maxWidth)); @@ -210,17 +197,6 @@ export class InlineChatInputWidget extends Disposable { this._toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0); })); - // Update placeholder based on selection state - this._store.add(autorun(r => { - const selection = this._editorObs.cursorSelection.read(r); - const hasSelection = selection && !selection.isEmpty(); - const placeholderText = hasSelection - ? localize('placeholderWithSelection', "Describe how to change this") - : localize('placeholderNoSelection', "Describe what to generate"); - - this._input.updateOptions({ placeholder: placeholderText }); - })); - // Track input text for context key and adjust width based on content const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(this._contextKeyService); @@ -229,21 +205,15 @@ export class InlineChatInputWidget extends Disposable { })); this._store.add(toDisposable(() => inputHasText.reset())); - // Handle Enter key to submit, ArrowDown to move to actions + // Track focus state + const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(this._contextKeyService); + this._store.add(this._input.onDidFocusEditorText(() => inputWidgetFocused.set(true))); + this._store.add(this._input.onDidBlurEditorText(() => inputWidgetFocused.set(false))); + this._store.add(toDisposable(() => inputWidgetFocused.reset())); + + // Handle key events: ArrowDown to move to actions this._store.add(this._input.onKeyDown(e => { - if (e.keyCode === KeyCode.Enter && !e.shiftKey) { - e.preventDefault(); - e.stopPropagation(); - this._commandService.executeCommand('inlineChat.submitInput'); - } else if (e.keyCode === KeyCode.Escape) { - // Hide overlay if input is empty - const value = this._input.getModel().getValue() ?? ''; - if (!value) { - e.preventDefault(); - e.stopPropagation(); - this.hide(); - } - } else if (e.keyCode === KeyCode.DownArrow && !actionBar.isEmpty()) { + if (e.keyCode === KeyCode.DownArrow && !actionBar.isEmpty()) { const model = this._input.getModel(); const position = this._input.getPosition(); if (position && position.lineNumber === model.getLineCount()) { @@ -282,11 +252,11 @@ export class InlineChatInputWidget extends Disposable { * @param left Left offset relative to editor * @param anchorAbove Whether to anchor above the position (widget grows upward) */ - show(lineNumber: number, left: number, anchorAbove: boolean): void { + show(lineNumber: number, left: number, anchorAbove: boolean, placeholder: string): void { this._showStore.clear(); // Clear input state - this._input.updateOptions({ wordWrap: 'off' }); + this._input.updateOptions({ wordWrap: 'off', placeholder }); this._input.getModel().setValue(''); // Store anchor info for scroll updates diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css index b50a69f8752d0..37d4268342add 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -15,6 +15,7 @@ min-height: var(--vscode-inline-chat-affordance-height); line-height: var(--vscode-inline-chat-affordance-height); border: 1px solid var(--vscode-input-border, transparent); + z-index: 100; } .inline-chat-content-widget .action-label.codicon.codicon-light-bulb, diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index 294867b3056ea..9acc9ecf81703 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -10,7 +10,7 @@ border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); border-radius: 8px; box-shadow: 0 2px 8px var(--vscode-widget-shadow); - z-index: 10000; + z-index: 100; } .inline-chat-gutter-menu .input { diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 99591004f8210..28c824b62beaf 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -109,6 +109,7 @@ export const CTX_INLINE_CHAT_EDITING = new RawContextKey('inlineChatEdi export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey('inlineChatResponseFocused', false, localize('inlineChatResponseFocused', "Whether the interactive widget's response is focused")); export const CTX_INLINE_CHAT_EMPTY = new RawContextKey('inlineChatEmpty', false, localize('inlineChatEmpty', "Whether the interactive editor input is empty")); export const CTX_INLINE_CHAT_INPUT_HAS_TEXT = new RawContextKey('inlineChatInputHasText', false, localize('inlineChatInputHasText', "Whether the inline chat input widget has text")); +export const CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED = new RawContextKey('inlineChatInputWidgetFocused', false, localize('inlineChatInputWidgetFocused', "Whether the inline chat input widget editor is focused")); export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('inlineChatInnerCursorFirst', false, localize('inlineChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line")); export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); @@ -117,6 +118,7 @@ export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlin export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineChatChangeShowsDiff', false, localize('inlineChatChangeShowsDiff', "Whether the current change showing a diff")); export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('inlineChatRequestInProgress', false, localize('inlineChatRequestInProgress', "Whether an inline chat request is currently in progress")); export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey('inlineChatResponseType', InlineChatResponseType.None, localize('inlineChatResponseTypes', "What type was the responses have been receieved, nothing yet, just messages, or messaged and local edits")); +export const CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT = new RawContextKey('inlineChatFileBelongsToChat', false, localize('inlineChatFileBelongsToChat', "Whether the current file belongs to a chat editing session")); export const CTX_INLINE_CHAT_V1_ENABLED = ContextKeyExpr.or( ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CTX_INLINE_CHAT_HAS_NOTEBOOK_INLINE) @@ -132,6 +134,7 @@ export const CTX_HOVER_MODE = ContextKeyExpr.equals('config.inlineChat.renderMod // --- (selected) action identifier export const ACTION_START = 'inlineChat.start'; +export const ACTION_ASK_IN_CHAT = 'inlineChat.askInChat'; export const ACTION_ACCEPT_CHANGES = 'inlineChat.acceptChanges'; export const ACTION_DISCARD_CHANGES = 'inlineChat.discardHunkChange'; export const ACTION_REGENERATE_RESPONSE = 'inlineChat.regenerate'; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index b23503fa2328f..f122a55550a83 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -223,6 +223,16 @@ declare module 'vscode' { }; language: string; + /** + * Overrides for how the command is presented in the UI. + * For example, when a `cd && ` prefix is detected, + * the presentation can show only the actual command. + */ + presentationOverrides?: { + commandLine: string; + language?: string; + }; + /** * Terminal command output. Displayed when the terminal is no longer available. */