diff --git a/.npmrc b/.npmrc index b07eade64d573..2c7c6c1b58489 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="39.6.0" -ms_build_id="13330601" +target="39.6.1" +ms_build_id="13369494" runtime="electron" ignore-scripts=false build_from_source="true" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 3df57a48a97d2..90e662c384deb 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -1a1bb622d9788793310458b7bf9eedcea8347da9556dd1d7661b757c15ebfdd5 *chromedriver-v39.6.0-darwin-arm64.zip -c84565c127adeca567ca69e85bbd8f387fff1f83c09e69f6f851528f5602dc4e *chromedriver-v39.6.0-darwin-x64.zip -f50df11f99a2e3df84560d5331608cd0a9d7a147a1490f25edfd8a95531918a2 *chromedriver-v39.6.0-linux-arm64.zip -a571fd25e33f3b3bded91506732a688319d93eb652e959bb19a09cd3f67f9e5f *chromedriver-v39.6.0-linux-armv7l.zip -2a50751190bbfe07984f7d8cbf2f12c257a4c132a36922a78c4e320169b8f498 *chromedriver-v39.6.0-linux-x64.zip -cf6034c20b727c48a6f44bb87b1ec89fd4189f56200a32cd39cedaab3f19e007 *chromedriver-v39.6.0-mas-arm64.zip -d2107db701c41fa5f3aaa04c279275ac4dcffde4542c032c806939acd8c6cd6c *chromedriver-v39.6.0-mas-x64.zip -1593ed5550fa11c549fd4ff5baea5cb7806548bff15b79340343ac24a86d6de3 *chromedriver-v39.6.0-win32-arm64.zip -deee89cbeed935a57551294fbc59f6a346b76769e27dd78a59a35a82ae3037d9 *chromedriver-v39.6.0-win32-ia32.zip -f88a23ebc246ed2a506d6d172eb9ffbb4c9d285103285a735e359268fcd08895 *chromedriver-v39.6.0-win32-x64.zip -2e1ec8568f4fda21dc4bb7231cdb0427fa31bb03c4bc39f8aa36659894f2d23e *electron-api.json -03e743428685b44beeab9aa51bad7437387dc2ce299b94745ed8fb0923dd9a07 *electron-v39.6.0-darwin-arm64-dsym-snapshot.zip -723d64530286ebd58539bc29deb65e9334ae8450a714b075d369013b4bbfdce0 *electron-v39.6.0-darwin-arm64-dsym.zip -8f529fbbed8c386f3485614fa059ea9408ebe17d3f0c793269ea52ef3efdf8df *electron-v39.6.0-darwin-arm64-symbols.zip -dace1f9e5c49f4f63f32341f8b0fb7f16b8cf07ce5fcb17abcc0b33782966b8c *electron-v39.6.0-darwin-arm64.zip -e2425514469c4382be374e676edff6779ef98ca1c679b1500337fa58aa863e98 *electron-v39.6.0-darwin-x64-dsym-snapshot.zip -877e72afd7d8695e8a4420a74765d45c30fad30606d3dbab07a0e88fe600e3f6 *electron-v39.6.0-darwin-x64-dsym.zip -ae958c150c6fe76fc7989a28ddb6104851f15d2e24bd32fe60f51e308954a816 *electron-v39.6.0-darwin-x64-symbols.zip -bed88dac3ac28249a020397d83f3f61871c7eaea2099d5bf6b1e92878cb14f19 *electron-v39.6.0-darwin-x64.zip -a86e9470d6084611f38849c9f9b3311584393fa81b55d0bbf7e284a649b729cf *electron-v39.6.0-linux-arm64-debug.zip -e7d7aec3873a6d2f2c9fe406a27a8668910f8b4fdf55a36b5302d9db3ec390db *electron-v39.6.0-linux-arm64-symbols.zip -d6ded47a49046eb031800cf70f2b5d763ccac11dac64e70a874c62aaa115ccba *electron-v39.6.0-linux-arm64.zip -2bf6a75c9f3c2400698c325e48c9b6444d108e4d76544fb130d04605002ae084 *electron-v39.6.0-linux-armv7l-debug.zip -421d02c8a063602b22e4f16a2614fe6cc13e07f9d4ead309fe40aeac296fe951 *electron-v39.6.0-linux-armv7l-symbols.zip -ee34896d1317f1572ed4f3ed8eb1719f599f250d442fc6afb6ec40091c4f4cdc *electron-v39.6.0-linux-armv7l.zip -233f55caae4514144310928248a96bd3a3ce7ac6dc1ff99e7531737a579793b1 *electron-v39.6.0-linux-x64-debug.zip -eca69e741b00ce141b9c2e6e63c1f77cd834a85aa095385f032fdb58d3154fff *electron-v39.6.0-linux-x64-symbols.zip -94bf4bee48f3c657edffd4556abbe62556ca8225cbb4528d62eb858233a3c34b *electron-v39.6.0-linux-x64.zip -6dfebeb760627df74c65ff8da7088fb77e0ae222cab5590fea4cdd37c060ea06 *electron-v39.6.0-mas-arm64-dsym-snapshot.zip -b327d41507546799451a684b6061caed10f1c16ee39a7e686aac71187f8b7afe *electron-v39.6.0-mas-arm64-dsym.zip -02a56a9c3c3522ebc653f03ad88be9a2f46594c730a767a28e7322ddb7a789b7 *electron-v39.6.0-mas-arm64-symbols.zip -2fe93cd39521371bb5722c358feebadc5e79d79628b07a79a00a9d918e261de4 *electron-v39.6.0-mas-arm64.zip -f25ddc8a9b2b699d6d9e54fdf66220514e387ae36e45efeb4d8217b1462503f6 *electron-v39.6.0-mas-x64-dsym-snapshot.zip -6732026b6a3728bea928af0c5928bf82d565eebeb3f5dc5b6991639d27e7c457 *electron-v39.6.0-mas-x64-dsym.zip -5260dabf5b0fc369e0f69d3286fbcce9d67bc65e3364e17f7bb13dd49e320422 *electron-v39.6.0-mas-x64-symbols.zip -905f7cf95270afa92972b6c9242fc50c0afd65ffd475a81ded6033588f27a613 *electron-v39.6.0-mas-x64.zip -9204c9844e89f5ca0b32a8347cf9141d8dcb66671906e299afa06004f464d9b0 *electron-v39.6.0-win32-arm64-pdb.zip -6778c54d8cf7a0d305e4334501c3b877daf4737197187120ac18064f4e093b23 *electron-v39.6.0-win32-arm64-symbols.zip -efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.0-win32-arm64-toolchain-profile.zip -22b96aca4cf8f7823b98e3b20b6131e521e0100c5cd03ab76f106eefbd0399cf *electron-v39.6.0-win32-arm64.zip -f5b69c8c1c9349a1f3b4309fb3fa1cf6326953e0807d2063fc27ba9f1400232e *electron-v39.6.0-win32-ia32-pdb.zip -1d6e103869acdeb0330b26ee08089667e0b5afc506efcd7021ba761ed8b786b5 *electron-v39.6.0-win32-ia32-symbols.zip -efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.0-win32-ia32-toolchain-profile.zip -2b30e5bc923fff1443e2a4d1971cb9b26f61bd6a454cfbb991042457bab4d623 *electron-v39.6.0-win32-ia32.zip -5f93924c317206a2a4800628854e44e68662a9c40b3457c9e72690d6fff884d3 *electron-v39.6.0-win32-x64-pdb.zip -eab07439f0a21210cd560c1169c04ea5e23c6fe0ab65bd60cffce2b9f69fd36e *electron-v39.6.0-win32-x64-symbols.zip -efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.0-win32-x64-toolchain-profile.zip -e8eee36be3bb85ba6fd8fcd26cf3a264bc946ac0717762c64e168896695c8e34 *electron-v39.6.0-win32-x64.zip -2e84c606e40c7bab5530e4c83bbf3a24c28143b0a768dafa5ecf78b18d889297 *electron.d.ts -27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.6.0-darwin-arm64.zip -321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.6.0-darwin-x64.zip -52ae6eccbdb4a9403a6c3eb46b356a28940ec25958b6b9181fb2f38e612e40ed *ffmpeg-v39.6.0-linux-arm64.zip -622cb781fb1e3b9617e7e60c36384427f7b0d9b5ad888e9bc356a83b050e13f1 *ffmpeg-v39.6.0-linux-armv7l.zip -ba441851788008362f013bf2983b22b0042af8df31bf90123328f928cc067492 *ffmpeg-v39.6.0-linux-x64.zip -27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.6.0-mas-arm64.zip -321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.6.0-mas-x64.zip -2a358c2dbeeb259c0b6a18057b52ffb0109de69112086cb2ce02f3a79bd70cee *ffmpeg-v39.6.0-win32-arm64.zip -4555510880a7b8dff5d5d0520f641665c62494689782adbed67fa0e24b45ae67 *ffmpeg-v39.6.0-win32-ia32.zip -091ab3c97d5a1cda1e04c6bd263a2c07ea63ed7ec3fd06600af6d7e23bbbbe15 *ffmpeg-v39.6.0-win32-x64.zip -650fb5fbc7e6cc27e5caeb016f72aba756469772bbfdfb3ec0b229f973d8ad46 *hunspell_dictionaries.zip -669ef1bf8ed0f6378e67f4f8bc23d2907d7cc1db7369dbdf468e164f4ef49365 *libcxx-objects-v39.6.0-linux-arm64.zip -996d81ad796524246144e15e22ffef75faff055a102c49021d70b03f039c3541 *libcxx-objects-v39.6.0-linux-armv7l.zip -1ffb610613c11169640fa76e4790137034a0deb3b48e2aef51a01c9b96b7700a *libcxx-objects-v39.6.0-linux-x64.zip -6dd8db57473992367c7914b50d06cae3a1b713cc09ceebecfcd4107df333e759 *libcxx_headers.zip -e5c18f813cc64a7d3b0404ee9adeb9cbb49e7ee5e1054b62c71fa7d1a448ad1b *libcxxabi_headers.zip -7f58d6e1d8c75b990f7d2259de8d0896414d0f2cff2f0fe4e5c7f8037d8fe879 *mksnapshot-v39.6.0-darwin-arm64.zip -be1178e4aa1f4910ba2b8f35b5655e12182657b9e32d509b47f0b2db033f0ac5 *mksnapshot-v39.6.0-darwin-x64.zip -5e36a594067fea08bb3d7bcd60873c3e240ebcee2208bcebfbc9f77d3075cc0d *mksnapshot-v39.6.0-linux-arm64-x64.zip -2db9196d2af0148ebb7b6f1f597f46a535b7af482f95739bd1ced78e1ebf39e7 *mksnapshot-v39.6.0-linux-armv7l-x64.zip -cd673e0a908fc950e0b4246e2b099018a8ee879d12a62973a01cb7de522f5bcf *mksnapshot-v39.6.0-linux-x64.zip -0749d8735a1fd8c666862cd7020b81317c45203d01319c9be089d1e750cb2c15 *mksnapshot-v39.6.0-mas-arm64.zip -81ae98e064485f8c6c69cd6c875ee72666c0cc801a8549620d382c2d0cea3b5c *mksnapshot-v39.6.0-mas-x64.zip -2e44f75df797922e7c8bad61a1b41fed14b070a54257a6a751892b2b8b9dfe29 *mksnapshot-v39.6.0-win32-arm64-x64.zip -fb5d73a8bf4b8db80f61b7073aa8458b5c46cce5c2a4b23591e851c6fcbd0144 *mksnapshot-v39.6.0-win32-ia32.zip -118ae88dbcd6b260cfa370e46ccfb0ab00af5efbf59495aaeea56a2831f604b2 *mksnapshot-v39.6.0-win32-x64.zip +71cee744399edac3516b0b56d6565b015a70abda53785501b94e7efe68efb556 *chromedriver-v39.6.1-darwin-arm64.zip +2284eb7a536b6001d2b53b57a69d8b397f7245044c6055515268a472e780e9b3 *chromedriver-v39.6.1-darwin-x64.zip +d64a85751e3b779ce9d4e88ab0e25db4970594f7c2b4995a21d5bfce8d6b63f0 *chromedriver-v39.6.1-linux-arm64.zip +83e938f9b5c19785d32f510a4ef5eab9ac6e63d40944b0e759a5f85867a02b72 *chromedriver-v39.6.1-linux-armv7l.zip +b1cd220f1c71edd4aeb57910cbf63bdf8862c62a1c3270ae7af5a4bd2098fe6f *chromedriver-v39.6.1-linux-x64.zip +c63e4ea9a0bdb883d2a6369919262a7ff3f9b432dfdcd32c827debbabfa6e8e4 *chromedriver-v39.6.1-mas-arm64.zip +d073ed5147b5adf3aefc88c5ee24c0998a8930849d10f886a1e553b99ed001b9 *chromedriver-v39.6.1-mas-x64.zip +7d475b4bbddb9a6b2ab1fd7793cbe5fd2a3b3e8ee291b804ff1773d3a6432482 *chromedriver-v39.6.1-win32-arm64.zip +ff7175e1953a604a3d033eac76c0be6c0c6cadce014459c23186b4ce35ee632b *chromedriver-v39.6.1-win32-ia32.zip +2c3591abbc4a11ea9508d02eb9e9152eba7ef693746696723239cac8db4faac1 *chromedriver-v39.6.1-win32-x64.zip +c97d31018c4f3229555607f71e14ddf44ecf78b684055dce548ae5117b4fd284 *electron-api.json +9777c57ec393cb9961500f645166b3f05a9e1ddd8472474c3c6ae9f8224cc029 *electron-v39.6.1-darwin-arm64-dsym-snapshot.zip +005d1a2b75c7e022eda48ecb5b8026e7556b9e12ad6bbd3de39b555a299edbd9 *electron-v39.6.1-darwin-arm64-dsym.zip +fbd66751111a813295d29cfb152d67610d6fa3d606712f649c809883d087aae2 *electron-v39.6.1-darwin-arm64-symbols.zip +287d3bbff9709e37abb9a8c2780e6227f99a165c066dc870a104e173ef4bfe95 *electron-v39.6.1-darwin-arm64.zip +1dee7165235ead83950170805f84be03d6f89a4224e74ed9d570cd7393c2e9d3 *electron-v39.6.1-darwin-x64-dsym-snapshot.zip +e8f1c0b4d61272f95db0fa43a83b181ca449c31665012bb4d65b7320d3b450e1 *electron-v39.6.1-darwin-x64-dsym.zip +c04988ad2b72293fc5b26dba1c291cb3eb755dee9e1d274ec0553c35b306b339 *electron-v39.6.1-darwin-x64-symbols.zip +a9d801eaa52cdfbcb0e238c77b264ef04dc3831e4ff960b666f9bd414cdcac27 *electron-v39.6.1-darwin-x64.zip +43803eeeb2c85c8c122e7f2b036d577fdc761b469cfe503beffed96f6896dfbc *electron-v39.6.1-linux-arm64-debug.zip +9bd95e9fbdf836c0bce62a9e071e54a544e10d3509753e09a71e9ad8c1b74ff7 *electron-v39.6.1-linux-arm64-symbols.zip +fb5f0d71b908f9e49e845cda014ddcafa0637bbf21d811ad30ac799cb453d0a9 *electron-v39.6.1-linux-arm64.zip +9451c34d1608030b841018ce5df2af4319c70e43528e8033c2c469b836a4e15f *electron-v39.6.1-linux-armv7l-debug.zip +bfe9d26d2070ee4f330be8f89de9886ae121efffe6740f8af068b6c65be091eb *electron-v39.6.1-linux-armv7l-symbols.zip +6346f457f12ac728eacb63a782873438580291b853d7c2f387387da363cd021e *electron-v39.6.1-linux-armv7l.zip +d8b0d22e49cf1d7af1318608c791564637a6055584cae558f94a4c6c48422fee *electron-v39.6.1-linux-x64-debug.zip +1e25737611d32a47aec6941d803f9affad90713d9fac695c926806f8d852ca03 *electron-v39.6.1-linux-x64-symbols.zip +2c8ce4905bfeba655df1f8528981361225a3c8f2eb3e9fd1df1f7c2a6e0a03fa *electron-v39.6.1-linux-x64.zip +ef108445cada7fd68a1ec43037e5dd5c542ebe9f579ef59b4c2f4ee33dcf2d2f *electron-v39.6.1-mas-arm64-dsym-snapshot.zip +1139730349a8ab01f0c28f0c1066b58d79b815c6c99d88c19e9effe2e0f0ec37 *electron-v39.6.1-mas-arm64-dsym.zip +08bb61490eca6157ef5e6fff36fe0fc194994446fcab3c2a779190b45d14c206 *electron-v39.6.1-mas-arm64-symbols.zip +5c1e6db65a37e9810e88e172328b18bc7f6fa30f6edf865d600dbd3f399a9dd2 *electron-v39.6.1-mas-arm64.zip +88879e5383ee0066d138587f47ee1eacce18d1494fdfef851f448621fb69de50 *electron-v39.6.1-mas-x64-dsym-snapshot.zip +3125230a1c02498adbd404852eef94040114532990d2a84ced6f30f9fe58c0d1 *electron-v39.6.1-mas-x64-dsym.zip +4a2d0deaa795d2488d3a708ddbf887d6d8421256a03ae0945ec7c8cca383874d *electron-v39.6.1-mas-x64-symbols.zip +c1f9e7faef99ab132bdaf9cef3c773ca92a32683c7ba787d00abcfdbd87c99e2 *electron-v39.6.1-mas-x64.zip +ed8ac6af71ad036a8b3b725db1f45ac92ea346f6c7d6f8f9a9e6af89fcb98405 *electron-v39.6.1-win32-arm64-pdb.zip +66f0e675d11cf174960d0e700678dbbb92b993b52f058f1eb34613d3b66705e5 *electron-v39.6.1-win32-arm64-symbols.zip +efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.1-win32-arm64-toolchain-profile.zip +41e1368fe8ea044cc872c5da3d747ea9ab1576386b99580a55a8e14c7375dec9 *electron-v39.6.1-win32-arm64.zip +8079d11539a2924eb07c05639d711ab12273e4d333aaeb3d6ae346bf7c5e45f6 *electron-v39.6.1-win32-ia32-pdb.zip +b22f3898fbb0e49f8c0506bd45b4f3510a0a0fdd947c0a62114bbc709c061eb5 *electron-v39.6.1-win32-ia32-symbols.zip +efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.1-win32-ia32-toolchain-profile.zip +bb5563fc6ce349271e2bee3456a15445afd3af742c4d315b44ffd868d0211bfc *electron-v39.6.1-win32-ia32.zip +4af89e83c7c56c9418f83f8f534133bc664287bbde49b066607b45217f5acfa9 *electron-v39.6.1-win32-x64-pdb.zip +d6eda384bd6ebf6e39173b0363490f920fba92ecb8fdd53b3061080c72806c12 *electron-v39.6.1-win32-x64-symbols.zip +efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.1-win32-x64-toolchain-profile.zip +87ddfa6c8f2c1178582dd79af76da6b267d09dcbb52cc59d40d30c01c39f49f0 *electron-v39.6.1-win32-x64.zip +2ebffb8530e804319443d49288343c982c08cbae0aac314f70b9859cb2722f42 *electron.d.ts +27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.6.1-darwin-arm64.zip +321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.6.1-darwin-x64.zip +52ae6eccbdb4a9403a6c3eb46b356a28940ec25958b6b9181fb2f38e612e40ed *ffmpeg-v39.6.1-linux-arm64.zip +622cb781fb1e3b9617e7e60c36384427f7b0d9b5ad888e9bc356a83b050e13f1 *ffmpeg-v39.6.1-linux-armv7l.zip +ba441851788008362f013bf2983b22b0042af8df31bf90123328f928cc067492 *ffmpeg-v39.6.1-linux-x64.zip +27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.6.1-mas-arm64.zip +321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.6.1-mas-x64.zip +2a358c2dbeeb259c0b6a18057b52ffb0109de69112086cb2ce02f3a79bd70cee *ffmpeg-v39.6.1-win32-arm64.zip +4555510880a7b8dff5d5d0520f641665c62494689782adbed67fa0e24b45ae67 *ffmpeg-v39.6.1-win32-ia32.zip +091ab3c97d5a1cda1e04c6bd263a2c07ea63ed7ec3fd06600af6d7e23bbbbe15 *ffmpeg-v39.6.1-win32-x64.zip +c98648b15b337bc6a6443ba009a46cfc696d657032445078a2475e5bc835cae9 *hunspell_dictionaries.zip +7ac45cdcc0b3f46c92a23689aacae8f5d341d71bf61f7fbaf9254087840e0a8d *libcxx-objects-v39.6.1-linux-arm64.zip +901825379fba3261a94e9967f6b3759366d4c0e59a3e1a2e0c787e3e3f725ebb *libcxx-objects-v39.6.1-linux-armv7l.zip +832d867351c2a0ae6adc02efdd24bdf44173a011f07009f489df80e8a5ccc483 *libcxx-objects-v39.6.1-linux-x64.zip +9d6081a9fc90693d7bd54bb0bdef147d9c37bd7a179a1eea63018570ee7813f0 *libcxx_headers.zip +23be04f992ee37ce8f29ffb9e53bd5bf461c50a91b6eddedaecc9f20b8967937 *libcxxabi_headers.zip +beccf9d63fc88de743a5497626096e886a70c631b08bc3e3a924a134bc32c9dc *mksnapshot-v39.6.1-darwin-arm64.zip +40757d33f3e939efbc126b5c9a52cfc1e102098295bcd4c5ccdd588fb03fd918 *mksnapshot-v39.6.1-darwin-x64.zip +68a16dd164cd6fcd2cc44fa530a60ac17061c5d3bbe4a95dc534fb8de74c06ee *mksnapshot-v39.6.1-linux-arm64-x64.zip +5b6fee728d9a54afd8cd11b8c153c32c8ff1a7cdbe2473e4bb4edf771610b118 *mksnapshot-v39.6.1-linux-armv7l-x64.zip +1653b0c1d087058be4f16f1b5877decfeb0705b3732f9428eadff003ffc0f166 *mksnapshot-v39.6.1-linux-x64.zip +0a1d2064c80cc60bc44d7b4609fb83f1870bc66de5fbc2d4907f6baf0bc6e0a0 *mksnapshot-v39.6.1-mas-arm64.zip +6294fc74584a6b20fb3cdd6fcff4c644234ce0471896728b9652ecac00b577f6 *mksnapshot-v39.6.1-mas-x64.zip +d997856786ba5a72a9fddbdc13a085979a613a2c94a343286f0e136d2f3d84c5 *mksnapshot-v39.6.1-win32-arm64-x64.zip +76dcbe1c3cf7d36bee79ecb7f8c02a9b75167c48983ecd8df047da5793d6248c *mksnapshot-v39.6.1-win32-ia32.zip +4c9f0bd54704a6333f5268645625b65cd87d61aa8c6d52482d9d39cc1f85808a *mksnapshot-v39.6.1-win32-x64.zip diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 2a8b923395756..25a360094868e 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -31,6 +31,7 @@ import minimist from 'minimist'; import { compileBuildWithoutManglingTask, compileBuildWithManglingTask } from './gulpfile.compile.ts'; import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask } from './gulpfile.extensions.ts'; import { copyCodiconsTask } from './lib/compilation.ts'; +import type { EmbeddedProductInfo } from './lib/embeddedType.ts'; import { useEsbuildTranspile } from './buildConfig.ts'; import { promisify } from 'util'; import globCallback from 'glob'; @@ -388,7 +389,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const isInsiderOrExploration = quality === 'insider' || quality === 'exploration'; const embedded = isInsiderOrExploration - ? (product as typeof product & { embedded?: { nameShort: string; nameLong: string; applicationName: string; dataFolderName: string; darwinBundleIdentifier: string; urlProtocol: string } }).embedded + ? (product as typeof product & { embedded?: EmbeddedProductInfo }).embedded : undefined; const packageSubJsonStream = isInsiderOrExploration @@ -403,12 +404,9 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const productSubJsonStream = embedded ? gulp.src(['product.json'], { base: '.' }) .pipe(jsonEditor((json: Record) => { - json.nameShort = embedded.nameShort; - json.nameLong = embedded.nameLong; - json.applicationName = embedded.applicationName; - json.dataFolderName = embedded.dataFolderName; - json.darwinBundleIdentifier = embedded.darwinBundleIdentifier; - json.urlProtocol = embedded.urlProtocol; + Object.keys(embedded).forEach(key => { + json[key] = embedded[key as keyof EmbeddedProductInfo]; + }); return json; })) .pipe(rename('product.sub.json')) @@ -499,6 +497,9 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d 'resources/win32/code_70x70.png', 'resources/win32/code_150x150.png' ], { base: '.' })); + if (embedded) { + all = es.merge(all, gulp.src('resources/win32/sessions.ico', { base: '.' })); + } } else if (platform === 'linux') { const policyDest = gulp.src('.build/policies/linux/**', { base: '.build/policies/linux' }) .pipe(rename(f => f.dirname = `policies/${f.dirname}`)); @@ -522,6 +523,8 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d darwinMiniAppName: embedded.nameShort, darwinMiniAppBundleIdentifier: embedded.darwinBundleIdentifier, darwinMiniAppIcon: 'resources/darwin/sessions.icns', + win32ProxyAppName: embedded.nameShort, + win32ProxyIcon: 'resources/win32/sessions.ico', } : {}) }; @@ -530,7 +533,13 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d .pipe(util.fixWin32DirectoryPermissions()) .pipe(filter(['**', '!**/.github/**'], { dot: true })) // https://github.com/microsoft/vscode/issues/116523 .pipe(electron(electronConfig)) - .pipe(filter(['**', '!LICENSE', '!version'], { dot: true })); + .pipe(filter([ + '**', + '!LICENSE', + '!version', + ...(platform === 'darwin' && !isInsiderOrExploration ? ['!**/Contents/Applications'] : []), + ...(platform === 'win32' && !isInsiderOrExploration ? ['!**/electron_proxy.exe'] : []), + ], { dot: true })); if (platform === 'linux') { result = es.merge(result, gulp.src('resources/completions/bash/code', { base: '.' }) diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts index d04e7f1f0e7d3..0f81323c98db2 100644 --- a/build/gulpfile.vscode.win32.ts +++ b/build/gulpfile.vscode.win32.ts @@ -14,6 +14,7 @@ import product from '../product.json' with { type: 'json' }; import { getVersion } from './lib/getVersion.ts'; import * as task from './lib/task.ts'; import * as util from './lib/util.ts'; +import type { EmbeddedProductInfo } from './lib/embeddedType.ts'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); @@ -112,6 +113,17 @@ function buildWin32Setup(arch: string, target: string): task.CallbackTask { Quality: quality }; + const isInsiderOrExploration = quality === 'insider' || quality === 'exploration'; + const embedded = isInsiderOrExploration + ? (product as typeof product & { embedded?: EmbeddedProductInfo }).embedded + : undefined; + + if (embedded) { + definitions['ProxyExeBasename'] = embedded.nameShort; + definitions['ProxyAppUserId'] = embedded.win32AppUserModelId; + definitions['ProxyNameLong'] = embedded.nameLong; + } + if (quality === 'stable' || quality === 'insider') { definitions['AppxPackage'] = `${quality === 'stable' ? 'code' : 'code_insider'}_${arch}.appx`; definitions['AppxPackageDll'] = `${quality === 'stable' ? 'code' : 'code_insider'}_explorer_command_${arch}.dll`; diff --git a/build/lib/embeddedType.ts b/build/lib/embeddedType.ts new file mode 100644 index 0000000000000..4b3075f4a7165 --- /dev/null +++ b/build/lib/embeddedType.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export type EmbeddedProductInfo = { + nameShort: string; + nameLong: string; + applicationName: string; + dataFolderName: string; + darwinBundleIdentifier: string; + urlProtocol: string; + win32AppUserModelId: string; + win32MutexName: string; + win32RegValueName: string; + win32NameVersion: string; + win32VersionedUpdate: boolean; +}; diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 5d00be88ea8e7..9e2eea3b858ec 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -645,6 +645,8 @@ "--vscode-searchEditor-findMatchBorder", "--vscode-searchEditor-textInputBorder", "--vscode-selection-background", + "--vscode-sessionsUpdateButton-downloadedBackground", + "--vscode-sessionsUpdateButton-downloadingBackground", "--vscode-settings-checkboxBackground", "--vscode-settings-checkboxBorder", "--vscode-settings-checkboxForeground", diff --git a/build/win32/code.iss b/build/win32/code.iss index f7091b28e5597..935f17dbe4150 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -95,9 +95,12 @@ Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{ Name: "{app}"; AfterInstall: DisableAppDirInheritance [Files] -Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\policies,\policies\*,\appx,\appx\*,\resources\app\product.json,\{#ExeBasename}.exe,\{#ExeBasename}.VisualElementsManifest.xml,\bin,\bin\*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\policies,\policies\*,\appx,\appx\*,\resources\app\product.json,\{#ExeBasename}.exe,{#ifdef ProxyExeBasename}\{#ProxyExeBasename}.exe,{#endif}\{#ExeBasename}.VisualElementsManifest.xml,\bin,\bin\*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#ExeBasename}.exe"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetExeBasename}"; Flags: ignoreversion Source: "{#ExeBasename}.VisualElementsManifest.xml"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetVisualElementsManifest}"; Flags: ignoreversion +#ifdef ProxyExeBasename +Source: "{#ProxyExeBasename}.exe"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetProxyExeBasename}"; Flags: ignoreversion +#endif Source: "tools\*"; DestDir: "{app}\{#VersionedResourcesFolder}\tools"; Flags: ignoreversion Source: "policies\*"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\policies"; Flags: ignoreversion skipifsourcedoesntexist Source: "bin\{#TunnelApplicationName}.exe"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirTunnelApplicationFilename}"; Flags: ignoreversion skipifsourcedoesntexist @@ -113,6 +116,11 @@ Source: "appx\{#AppxPackageDll}"; DestDir: "{code:GetDestDir}\{#VersionedResourc Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{group}\{#NameLong}.lnk')) Name: "{autodesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{autodesktop}\{#NameLong}.lnk')) Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}.lnk')) +#ifdef ProxyExeBasename +Name: "{group}\{#ProxyExeBasename}"; Filename: "{app}\{#ProxyExeBasename}.exe"; AppUserModelID: "{#ProxyAppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{group}\{#ProxyExeBasename}.lnk')) +Name: "{autodesktop}\{#ProxyNameLong}"; Filename: "{app}\{#ProxyExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#ProxyAppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{autodesktop}\{#ProxyNameLong}.lnk')) +Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#ProxyNameLong}"; Filename: "{app}\{#ProxyExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#ProxyAppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#ProxyNameLong}.lnk')) +#endif [Run] Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunAfterUpdate @@ -1562,6 +1570,16 @@ begin Result := ExpandConstant('{#ExeBasename}.exe'); end; +#ifdef ProxyExeBasename +function GetProxyExeBasename(Value: string): string; +begin + if IsBackgroundUpdate() and IsVersionedUpdate() then + Result := ExpandConstant('new_{#ProxyExeBasename}.exe') + else + Result := ExpandConstant('{#ProxyExeBasename}.exe'); +end; +#endif + function GetBinDirTunnelApplicationFilename(Value: string): string; begin if IsBackgroundUpdate() and IsVersionedUpdate() then diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index c3c4a0cd2bcb8..424e997bde5b9 100644 Binary files a/build/win32/inno_updater.exe and b/build/win32/inno_updater.exe differ diff --git a/cgmanifest.json b/cgmanifest.json index 21554434500a7..45f578c1b9767 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -529,13 +529,13 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "a229dbf7a56336b847b34dfff1bac79afc311eee", - "tag": "39.6.0" + "commitHash": "2ffb9e1e05c55975112ff6b23037af0eeda79142", + "tag": "39.6.1" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "39.6.0" + "version": "39.6.1" }, { "component": { diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index ee51bb61214e2..8d3f082600d84 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -84,7 +84,7 @@ "sideBarSectionHeader.foreground": "#bfbfbf", "sideBarSectionHeader.border": "#2A2B2CFF", "titleBar.activeBackground": "#191A1B", - "titleBar.activeForeground": "#bfbfbf", + "titleBar.activeForeground": "#8C8C8C", "titleBar.inactiveBackground": "#191A1B", "titleBar.inactiveForeground": "#8C8C8C", "titleBar.border": "#2A2B2CFF", @@ -110,11 +110,11 @@ "editorCursor.foreground": "#BBBEBF", "editor.selectionBackground": "#27678280", "editor.inactiveSelectionBackground": "#27678260", - "editor.selectionHighlightBackground": "#27678260", - "editor.wordHighlightBackground": "#27678250", - "editor.wordHighlightStrongBackground": "#27678280", - "editor.findMatchBackground": "#27678290", - "editor.findMatchHighlightBackground": "#27678280", + "editor.selectionHighlightBackground": "#27678260", + "editor.wordHighlightBackground": "#27678250", + "editor.wordHighlightStrongBackground": "#27678280", + "editor.findMatchBackground": "#27678290", + "editor.findMatchHighlightBackground": "#27678280", "editor.findRangeHighlightBackground": "#242526", "editor.hoverHighlightBackground": "#242526", "editor.lineHighlightBackground": "#242526", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index b86ef612b20c3..f11ffef290574 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -93,7 +93,7 @@ "sideBarSectionHeader.foreground": "#202020", "sideBarSectionHeader.border": "#F2F3F4FF", "titleBar.activeBackground": "#FAFAFD", - "titleBar.activeForeground": "#424242", + "titleBar.activeForeground": "#606060", "titleBar.inactiveBackground": "#FAFAFD", "titleBar.inactiveForeground": "#606060", "titleBar.border": "#F2F3F4FF", diff --git a/package-lock.json b/package-lock.json index bf94454e49551..587596d6666fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,7 +86,7 @@ "@typescript/native-preview": "^7.0.0-dev.20260130", "@vscode/component-explorer": "^0.1.1-12", "@vscode/component-explorer-cli": "^0.1.1-8", - "@vscode/gulp-electron": "https://github.com/microsoft/vscode-gulp-electron.git#405e3df0e4e9c37fcf549cbe6f5cef8d5ba5ddff", + "@vscode/gulp-electron": "1.40.0", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", "@vscode/test-cli": "^0.0.6", @@ -103,7 +103,7 @@ "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", - "electron": "39.6.0", + "electron": "39.6.1", "eslint": "^9.36.0", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -3115,9 +3115,9 @@ } }, "node_modules/@vscode/gulp-electron": { - "version": "1.38.2", - "resolved": "git+ssh://git@github.com/microsoft/vscode-gulp-electron.git#405e3df0e4e9c37fcf549cbe6f5cef8d5ba5ddff", - "integrity": "sha512-SHFKIq0Gr8WOeVn9QOACkbxX5lsaj96Ux2npBHSb/a7S6ykyDD0Im1i+xCT96WimWLRQV0X20sK9IFli8I2Mkg==", + "version": "1.40.0", + "resolved": "git+ssh://git@github.com/microsoft/vscode-gulp-electron.git#580228be384d7942b39aca6466b5a5050e4744a2", + "integrity": "sha512-EfQqw/kFmqiUgBv7WXx3wIrtz9cujAgX2uKQzTq517MbVjlpg7BIAjNC4Iq/wVB4Vgpl/ZGB7/XuSN7LsaLdlA==", "dev": true, "license": "MIT", "dependencies": { @@ -6824,9 +6824,9 @@ "dev": true }, "node_modules/electron": { - "version": "39.6.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-39.6.0.tgz", - "integrity": "sha512-KQK3sJ6JCyymY3HQxV0N/bVBQwKQETRW0N/+OYcrL9H6tZhpmTSaZY3qSxcruWrPIuouvoiP3Vk/JKUpw05ZIw==", + "version": "39.6.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-39.6.1.tgz", + "integrity": "sha512-pgmTbWnT3rP+eo3EolO5EdNw5f7/x/0S7vP+eXC8Zyp2sWGjP4+kmo1RyeAYCChwIRWJFKQ2rQVl/ZkqwK6O2Q==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/package.json b/package.json index f0de36f12a677..cb5b8b6a963e4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.110.0", - "distro": "2d57806cfc4400f602f114b4bfc0fb17ce7e1a32", + "distro": "fdfb55bde654559f3a3baea839e9797107b4e92e", "author": { "name": "Microsoft Corporation" }, @@ -154,7 +154,7 @@ "@typescript/native-preview": "^7.0.0-dev.20260130", "@vscode/component-explorer": "^0.1.1-12", "@vscode/component-explorer-cli": "^0.1.1-8", - "@vscode/gulp-electron": "https://github.com/microsoft/vscode-gulp-electron.git#405e3df0e4e9c37fcf549cbe6f5cef8d5ba5ddff", + "@vscode/gulp-electron": "1.40.0", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", "@vscode/test-cli": "^0.0.6", @@ -171,7 +171,7 @@ "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", - "electron": "39.6.0", + "electron": "39.6.1", "eslint": "^9.36.0", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -249,4 +249,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} \ No newline at end of file +} diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 23de97f6d2395..842c676692495 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -70,7 +70,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau protected override async initialize(): Promise { if ((process as INodeProcess).isEmbeddedApp) { this.setState(State.Disabled(DisablementReason.EmbeddedApp)); - this.logService.info('update#ctor - updates are disabled for embedded app'); + this.logService.info('update#ctor - updates are disabled from embedded app'); return; } diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 257edf1af198a..25535f2125222 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -33,6 +33,7 @@ import { asJson, IRequestService } from '../../request/common/request.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js'; import { AbstractUpdateService, createUpdateURL, getUpdateRequestHeaders, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js'; +import { INodeProcess } from '../../../base/common/platform.js'; interface IAvailableUpdate { packagePath: string; @@ -98,6 +99,12 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } protected override async initialize(): Promise { + if ((process as INodeProcess).isEmbeddedApp) { + this.setState(State.Disabled(DisablementReason.EmbeddedApp)); + this.logService.info('update#ctor - updates are disabled from embedded app'); + return; + } + if (this.productService.win32VersionedUpdate) { const cachePath = await this.cachePath; app.setPath('appUpdate', cachePath); diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 50b1321e83e30..75bcee0c678ad 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -7,7 +7,7 @@ import electron, { Display, Rectangle } from 'electron'; import { Color } from '../../../base/common/color.js'; import { Event } from '../../../base/common/event.js'; import { join } from '../../../base/common/path.js'; -import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; +import { INodeProcess, IProcessEnvironment, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; @@ -177,8 +177,15 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt if (isLinux) { options.icon = join(environmentMainService.appRoot, 'resources/linux/code.png'); // always on Linux - } else if (isWindows && !environmentMainService.isBuilt) { - options.icon = join(environmentMainService.appRoot, 'resources/win32/code_150x150.png'); // only when running out of sources on Windows + } else if (isWindows) { + if (!environmentMainService.isBuilt) { + options.icon = join(environmentMainService.appRoot, 'resources/win32/code_150x150.png'); // only when running out of sources on Windows + } else if ((process as INodeProcess).isEmbeddedApp) { + // For sub app the proxy executable acts as a launcher to the main executable whose + // icon will be used when creating windows if the following override is not set. + // This avoids sharing icon with the main application. + options.icon = join(environmentMainService.appRoot, 'resources/win32/sessions.ico'); + } } if (isMacintosh) { diff --git a/src/vs/sessions/AI_CUSTOMIZATIONS.md b/src/vs/sessions/AI_CUSTOMIZATIONS.md index 604626bc35ced..eb342a816ad34 100644 --- a/src/vs/sessions/AI_CUSTOMIZATIONS.md +++ b/src/vs/sessions/AI_CUSTOMIZATIONS.md @@ -56,7 +56,7 @@ The `IAICustomizationWorkspaceService` interface controls per-window behavior: |----------|-------------|----------| | `managementSections` | All sections except Models | Same | | `visibleStorageSources` | workspace, user, extension, plugin | workspace, user only | -| `preferManualCreation` | `false` (opens file externally) | `true` (embedded editor) | +| `preferManualCreation` | `false` (AI generation primary) | `true` (file creation primary) | | `activeProjectRoot` | First workspace folder | Active session worktree | ## Key Services @@ -71,3 +71,11 @@ Browser compatibility is required — no Node.js APIs. ## Feature Gating All commands and UI respect `ChatContextKeys.enabled` and the `chat.customizationsMenu.enabled` setting. + +## Settings + +Settings use the `chat.customizationsMenu.` namespace: + +| Setting | Default | Description | +|---------|---------|-------------| +| `chat.customizationsMenu.enabled` | `true` | Show the Chat Customizations editor in the Command Palette | diff --git a/src/vs/sessions/common/theme.ts b/src/vs/sessions/common/theme.ts index 782b1be6ab59b..2f928d12aecbb 100644 --- a/src/vs/sessions/common/theme.ts +++ b/src/vs/sessions/common/theme.ts @@ -7,6 +7,7 @@ import { localize } from '../../nls.js'; import { registerColor, transparent } from '../../platform/theme/common/colorUtils.js'; import { contrastBorder, iconForeground } from '../../platform/theme/common/colorRegistry.js'; import { Color } from '../../base/common/color.js'; +import { buttonBackground } from '../../platform/theme/common/colors/inputColors.js'; import { SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND } from '../../workbench/common/theme.js'; // Sessions sidebar background color @@ -55,3 +56,16 @@ export const agentFeedbackInputWidgetBorder = registerColor( { dark: transparent(iconForeground, 0.8), light: transparent(iconForeground, 0.8), hcDark: contrastBorder, hcLight: contrastBorder }, localize('agentFeedbackInputWidget.border', 'Border color of the agent feedback input widget shown in the editor.') ); + +// Sessions update button colors +export const sessionsUpdateButtonDownloadingBackground = registerColor( + 'sessionsUpdateButton.downloadingBackground', + transparent(buttonBackground, 0.4), + localize('sessionsUpdateButton.downloadingBackground', 'Background color of the update button to show download progress in the agent sessions window.') +); + +export const sessionsUpdateButtonDownloadedBackground = registerColor( + 'sessionsUpdateButton.downloadedBackground', + transparent(buttonBackground, 0.7), + localize('sessionsUpdateButton.downloadedBackground', 'Background color of the update button when download is complete in the agent sessions window.') +); diff --git a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts index cbff23edc4c94..2cd913c75f647 100644 --- a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts +++ b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts @@ -23,7 +23,9 @@ import { IAction } from '../../../../base/common/actions.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; +import { Downloading, IUpdateService, State, StateType } from '../../../../platform/update/common/update.js'; +import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; +import { sessionsUpdateButtonDownloadingBackground, sessionsUpdateButtonDownloadedBackground } from '../../../common/theme.js'; // --- Account Menu Items --- // const AccountMenu = new MenuId('SessionsAccountMenu'); @@ -160,7 +162,7 @@ class AccountWidget extends ActionViewItem { } } -class UpdateWidget extends ActionViewItem { +export class UpdateWidget extends ActionViewItem { private updateButton: Button | undefined; private readonly viewItemDisposables = this._register(new DisposableStore()); @@ -222,9 +224,51 @@ class UpdateWidget extends ActionViewItem { if (this.isUpdatePending() && !this.isUpdateReady()) { this.updateButton.enabled = false; this.updateButton.label = `$(${Codicon.loading.id}~spin) ${this.getUpdateProgressMessage(state.type)}`; + this.updateDownloadProgress(state); } else { this.updateButton.enabled = true; this.updateButton.label = `$(${Codicon.debugRestart.id}) ${localize('update', "Update")}`; + + const el = this.updateButton.element; + if (state.type === StateType.Ready) { + const color = asCssVariable(sessionsUpdateButtonDownloadedBackground); + el.style.backgroundImage = `linear-gradient(to right, ${color} 100%, transparent 100%)`; + } else { + // Ensure non-update states (e.g. Idle, Disabled, Uninitialized) do not look like a completed download + el.style.backgroundImage = ''; + } + } + } + + private updateDownloadProgress(state: State): void { + if (!this.updateButton) { + return; + } + + const el = this.updateButton.element; + + if (state.type === StateType.Downloading) { + const { downloadedBytes, totalBytes } = state as Downloading; + if (downloadedBytes !== undefined && totalBytes && totalBytes > 0) { + const percent = Math.min(100, Math.round((downloadedBytes / totalBytes) * 100)); + const color = asCssVariable(sessionsUpdateButtonDownloadingBackground); + el.style.backgroundImage = `linear-gradient(to right, ${color} ${percent}%, transparent ${percent}%)`; + } else { + // Indeterminate: show a subtle pulsing background + const color = asCssVariable(sessionsUpdateButtonDownloadingBackground); + el.style.backgroundImage = `linear-gradient(to right, ${color} 0%, transparent 100%)`; + } + } else if (state.type === StateType.Downloaded) { + const color = asCssVariable(sessionsUpdateButtonDownloadedBackground); + el.style.backgroundImage = `linear-gradient(to right, ${color} 100%, transparent 100%)`; + } else { + this.clearDownloadProgress(); + } + } + + private clearDownloadProgress(): void { + if (this.updateButton) { + this.updateButton.element.style.backgroundImage = ''; } } diff --git a/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts b/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts new file mode 100644 index 0000000000000..a67edb78b11b7 --- /dev/null +++ b/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action } from '../../../../../base/common/actions.js'; +import { Emitter } from '../../../../../base/common/event.js'; +import { IUpdateService, State } from '../../../../../platform/update/common/update.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; +import { UpdateWidget } from '../../browser/account.contribution.js'; + +// Ensure color registrations are loaded +import '../../../../common/theme.js'; +import '../../../../../platform/theme/common/colors/inputColors.js'; + +// Import the CSS +import '../../../../browser/media/sidebarActionButton.css'; +import '../../browser/media/accountWidget.css'; + +const mockUpdate = { version: '1.0.0' }; + +function createMockUpdateService(state: State): IUpdateService { + const onStateChange = new Emitter(); + const service: IUpdateService = { + _serviceBrand: undefined, + state, + onStateChange: onStateChange.event, + checkForUpdates: async () => { }, + downloadUpdate: async () => { }, + applyUpdate: async () => { }, + quitAndInstall: async () => { }, + isLatestVersion: async () => true, + _applySpecificUpdate: async () => { }, + setInternalOrg: async () => { }, + }; + return service; +} + +function renderUpdateWidget(ctx: ComponentFixtureContext, state: State): void { + ctx.container.style.padding = '16px'; + ctx.container.style.width = '300px'; + ctx.container.style.backgroundColor = 'var(--vscode-sideBar-background)'; + + const mockService = createMockUpdateService(state); + + const instantiationService = createEditorServices(ctx.disposableStore, { + colorTheme: ctx.theme, + additionalServices: (reg) => { + reg.defineInstance(IUpdateService, mockService); + }, + }); + + const action = ctx.disposableStore.add(new Action('sessions.action.updateWidget', 'Sessions Update')); + const widget = instantiationService.createInstance(UpdateWidget, action, {}); + ctx.disposableStore.add(widget); + widget.render(ctx.container); +} + +export default defineThemedFixtureGroup({ + Ready: defineComponentFixture({ + render: (ctx) => renderUpdateWidget(ctx, State.Ready(mockUpdate, true, false)), + }), + + CheckingForUpdates: defineComponentFixture({ + render: (ctx) => renderUpdateWidget(ctx, State.CheckingForUpdates(true)), + }), + + AvailableForDownload: defineComponentFixture({ + render: (ctx) => renderUpdateWidget(ctx, State.AvailableForDownload(mockUpdate)), + }), + + Downloading0Percent: defineComponentFixture({ + render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 0, 100_000_000)), + }), + + Downloading30Percent: defineComponentFixture({ + render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 30_000_000, 100_000_000)), + }), + + Downloading65Percent: defineComponentFixture({ + render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 65_000_000, 100_000_000)), + }), + + Downloading100Percent: defineComponentFixture({ + render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 100_000_000, 100_000_000)), + }), + + DownloadingIndeterminate: defineComponentFixture({ + render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false)), + }), + + Downloaded: defineComponentFixture({ + render: (ctx) => renderUpdateWidget(ctx, State.Downloaded(mockUpdate, true, false)), + }), + + Updating: defineComponentFixture({ + render: (ctx) => renderUpdateWidget(ctx, State.Updating(mockUpdate)), + }), + + Overwriting: defineComponentFixture({ + render: (ctx) => renderUpdateWidget(ctx, State.Overwriting(mockUpdate, true)), + }), +}); diff --git a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts index 02b43d147dbbb..bfb415f522f33 100644 --- a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts +++ b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts @@ -9,6 +9,7 @@ import { IAICustomizationWorkspaceService, AICustomizationManagementSection } fr import { PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { CustomizationCreatorService } from '../../../../workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; @@ -22,10 +23,14 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization readonly activeProjectRoot: IObservable; + readonly excludedUserFileRoots: readonly URI[]; + constructor( @ISessionsManagementService private readonly sessionsService: ISessionsManagementService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, ) { + this.excludedUserFileRoots = [userDataProfilesService.defaultProfile.promptsHome]; this.activeProjectRoot = derived(reader => { const session = this.sessionsService.activeSession.read(reader); return session?.worktree ?? session?.repository; @@ -43,7 +48,8 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization AICustomizationManagementSection.Instructions, AICustomizationManagementSection.Prompts, AICustomizationManagementSection.Hooks, - AICustomizationManagementSection.McpServers, + // TODO: Re-enable MCP Servers once CLI MCP configuration is unified with VS Code + // AICustomizationManagementSection.McpServers, ]; readonly visibleStorageSources: readonly PromptsStorage[] = [ diff --git a/src/vs/sessions/contrib/chat/browser/slashCommands.ts b/src/vs/sessions/contrib/chat/browser/slashCommands.ts index 361c7e736a330..a2b6d2dc343dc 100644 --- a/src/vs/sessions/contrib/chat/browser/slashCommands.ts +++ b/src/vs/sessions/contrib/chat/browser/slashCommands.ts @@ -15,13 +15,24 @@ import { Position } from '../../../../editor/common/core/position.js'; import { Range } from '../../../../editor/common/core/range.js'; import { getWordAtText } from '../../../../editor/common/core/wordHelper.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { inputPlaceholderForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { localize } from '../../../../nls.js'; import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../../../workbench/contrib/chat/common/widget/chatColors.js'; import { AICustomizationManagementCommands, AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; +/** + * Static command ID used by completion items to trigger immediate slash command execution, + * mirroring the pattern of core's `ChatSubmitAction` for `executeImmediately` commands. + */ +export const SESSIONS_EXECUTE_SLASH_COMMAND_ID = 'sessions.chat.executeSlashCommand'; + +CommandsRegistry.registerCommand(SESSIONS_EXECUTE_SLASH_COMMAND_ID, (_, handler: SlashCommandHandler, slashCommandStr: string) => { + handler.tryExecuteSlashCommand(slashCommandStr); + handler.clearInput(); +}); + /** * Minimal slash command descriptor for the sessions new-chat widget. * Self-contained copy of the essential fields from core's `IChatSlashData` @@ -60,6 +71,10 @@ export class SlashCommandHandler extends Disposable { this._registerDecorations(); } + clearInput(): void { + this._editor.getModel()?.setValue(''); + } + /** * Attempts to parse and execute a slash command from the input. * Returns `true` if a command was handled. @@ -119,13 +134,6 @@ export class SlashCommandHandler extends Disposable { executeImmediately: true, execute: openSection(AICustomizationManagementSection.Hooks), }); - this._slashCommands.push({ - command: 'mcp', - detail: localize('slashCommand.mcp', "View and manage MCP servers"), - sortText: 'z3_mcp', - executeImmediately: true, - execute: openSection(AICustomizationManagementSection.McpServers), - }); } private _registerDecorations(): void { @@ -219,11 +227,12 @@ export class SlashCommandHandler extends Disposable { const withSlash = `/${c.command}`; return { label: withSlash, - insertText: `${withSlash} `, + insertText: c.executeImmediately ? '' : `${withSlash} `, detail: c.detail, range, sortText: c.sortText ?? 'a'.repeat(i + 1), kind: CompletionItemKind.Text, + command: c.executeImmediately ? { id: SESSIONS_EXECUTE_SLASH_COMMAND_ID, title: withSlash, arguments: [this, withSlash] } : undefined, }; }) }; diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 4ed1b598aa25e..bbd83462075ee 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -30,6 +30,8 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'github.copilot.chat.languageContext.typescript.enabled': true, 'github.copilot.chat.cli.mcp.enabled': true, + 'chat.customizationsMenu.userStoragePath': '~/.copilot', + 'inlineChat.affordance': 'editor', 'inlineChat.renderMode': 'hover', diff --git a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts index 5b528d0d4cc1f..ce46e82314f23 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { URI } from '../../../../base/common/uri.js'; +import { isEqualOrParent } from '../../../../base/common/resources.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { IPromptsService, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; @@ -33,38 +35,53 @@ export function getSourceCountsTotal(counts: ISourceCounts, workspaceService: IA return total; } -export async function getPromptSourceCounts(promptsService: IPromptsService, promptType: PromptsType): Promise { +/** + * Returns true if the URI should be excluded based on excluded user file roots. + */ +function isExcludedUserFile(uri: URI, excludedRoots: readonly URI[]): boolean { + return excludedRoots.some(root => isEqualOrParent(uri, root)); +} + +export async function getPromptSourceCounts(promptsService: IPromptsService, promptType: PromptsType, excludedUserFileRoots: readonly URI[] = []): Promise { const [workspaceItems, userItems, extensionItems] = await Promise.all([ promptsService.listPromptFilesForStorage(promptType, PromptsStorage.local, CancellationToken.None), promptsService.listPromptFilesForStorage(promptType, PromptsStorage.user, CancellationToken.None), promptsService.listPromptFilesForStorage(promptType, PromptsStorage.extension, CancellationToken.None), ]); + const filteredUserItems = excludedUserFileRoots.length > 0 + ? userItems.filter(item => !isExcludedUserFile(item.uri, excludedUserFileRoots)) + : userItems; return { workspace: workspaceItems.length, - user: userItems.length, + user: filteredUserItems.length, extension: extensionItems.length, }; } -export async function getSkillSourceCounts(promptsService: IPromptsService): Promise { +export async function getSkillSourceCounts(promptsService: IPromptsService, excludedUserFileRoots: readonly URI[] = []): Promise { const skills = await promptsService.findAgentSkills(CancellationToken.None); if (!skills || skills.length === 0) { return { workspace: 0, user: 0, extension: 0 }; } + const userSkills = skills.filter(s => s.storage === PromptsStorage.user); + const filteredUserSkills = excludedUserFileRoots.length > 0 + ? userSkills.filter(s => !isExcludedUserFile(s.uri, excludedUserFileRoots)) + : userSkills; return { workspace: skills.filter(s => s.storage === PromptsStorage.local).length, - user: skills.filter(s => s.storage === PromptsStorage.user).length, + user: filteredUserSkills.length, extension: skills.filter(s => s.storage === PromptsStorage.extension).length, }; } export async function getCustomizationTotalCount(promptsService: IPromptsService, mcpService: IMcpService, workspaceService: IAICustomizationWorkspaceService): Promise { + const excluded = workspaceService.excludedUserFileRoots; const [agentCounts, skillCounts, instructionCounts, promptCounts, hookCounts] = await Promise.all([ - getPromptSourceCounts(promptsService, PromptsType.agent), - getSkillSourceCounts(promptsService), - getPromptSourceCounts(promptsService, PromptsType.instructions), - getPromptSourceCounts(promptsService, PromptsType.prompt), - getPromptSourceCounts(promptsService, PromptsType.hook), + getPromptSourceCounts(promptsService, PromptsType.agent, excluded), + getSkillSourceCounts(promptsService, excluded), + getPromptSourceCounts(promptsService, PromptsType.instructions, excluded), + getPromptSourceCounts(promptsService, PromptsType.prompt, excluded), + getPromptSourceCounts(promptsService, PromptsType.hook, excluded), ]); return getSourceCountsTotal(agentCounts, workspaceService) diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts index 041463a5977bc..23398d75287f9 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts @@ -5,7 +5,6 @@ import '../../../browser/media/sidebarActionButton.css'; import './media/customizationsToolbar.css'; -import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize, localize2 } from '../../../../nls.js'; @@ -34,12 +33,14 @@ import { getPromptSourceCounts, getSkillSourceCounts, getSourceCountsTotal, ISou import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { URI } from '../../../../base/common/uri.js'; + interface ICustomizationItemConfig { readonly id: string; readonly label: string; readonly icon: ThemeIcon; readonly section: AICustomizationManagementSection; - readonly getSourceCounts?: (promptsService: IPromptsService) => Promise; + readonly getSourceCounts?: (promptsService: IPromptsService, excludedUserFileRoots: readonly URI[]) => Promise; readonly getCount?: (languageModelsService: ILanguageModelsService, mcpService: IMcpService) => Promise; } @@ -49,43 +50,37 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ label: localize('agents', "Agents"), icon: agentIcon, section: AICustomizationManagementSection.Agents, - getSourceCounts: (ps) => getPromptSourceCounts(ps, PromptsType.agent), + getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.agent, ex), }, { id: 'sessions.customization.skills', label: localize('skills', "Skills"), icon: skillIcon, section: AICustomizationManagementSection.Skills, - getSourceCounts: (ps) => getSkillSourceCounts(ps), + getSourceCounts: (ps, ex) => getSkillSourceCounts(ps, ex), }, { id: 'sessions.customization.instructions', label: localize('instructions', "Instructions"), icon: instructionsIcon, section: AICustomizationManagementSection.Instructions, - getSourceCounts: (ps) => getPromptSourceCounts(ps, PromptsType.instructions), + getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.instructions, ex), }, { id: 'sessions.customization.prompts', label: localize('prompts', "Prompts"), icon: promptIcon, section: AICustomizationManagementSection.Prompts, - getSourceCounts: (ps) => getPromptSourceCounts(ps, PromptsType.prompt), + getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.prompt, ex), }, { id: 'sessions.customization.hooks', label: localize('hooks', "Hooks"), icon: hookIcon, section: AICustomizationManagementSection.Hooks, - getSourceCounts: (ps) => getPromptSourceCounts(ps, PromptsType.hook), - }, - { - id: 'sessions.customization.mcpServers', - label: localize('mcpServers', "MCP Servers"), - icon: Codicon.server, - section: AICustomizationManagementSection.McpServers, - getCount: (_lm, mcp) => Promise.resolve(mcp.servers.get().length), + getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.hook, ex), }, + // TODO: Re-enable MCP Servers once CLI MCP configuration is unified with VS Code ]; /** @@ -167,7 +162,7 @@ class CustomizationLinkViewItem extends ActionViewItem { } if (this._config.getSourceCounts) { - const counts = await this._config.getSourceCounts(this._promptsService); + const counts = await this._config.getSourceCounts(this._promptsService, this._workspaceService.excludedUserFileRoots); this._renderSourceCounts(this._countContainer, counts); } else if (this._config.getCount) { const count = await this._config.getCount(this._languageModelsService, this._mcpService); diff --git a/src/vs/sessions/copilot-customizations-spec.md b/src/vs/sessions/copilot-customizations-spec.md new file mode 100644 index 0000000000000..2b39296618014 --- /dev/null +++ b/src/vs/sessions/copilot-customizations-spec.md @@ -0,0 +1,356 @@ +# Copilot Agent Runtime — Customization Surface Spec + +> **Purpose:** Definitive reference for every customization mechanism that affects agent behavior when a user sends a message. Intended for building a UI that collects all customizations into a single view. +> +> **Source:** `github/copilot-agent-runtime` codebase as of 2026-02-25. + +> Some information has been removed by the human compiling this spec, scoping to what is deemed most relevant for the sessions window implementation. For the full details, see the source code (for maintainers likely checked out side-by-side). + +--- + +## Overview + +When a user sends a message, the agent assembles its behavior from **10 customization categories**, each discovered from well-known file paths, environment variables, or runtime APIs. This document enumerates every source, file pattern, and merge rule. + +--- + +## 1. Instructions + +System-prompt additions that shape how the agent responds. Multiple sources are discovered and merged in priority order. + +### 1.1 Repo-Level Instruction Files + +Each pattern is defined in `src/helpers/repo-helpers.ts` → `instructionPatterns`: + +| Convention | File Pattern | Notes | +|------------|-------------|-------| +| Copilot | `{repo}/.github/copilot-instructions.md` | Primary repo instructions | +| Codex / OpenAI | `{repo}/AGENTS.md` | OpenAI model convention | +| Claude / Anthropic | `{repo}/CLAUDE.md` | Claude model convention | +| Claude (alt) | `{repo}/.claude/CLAUDE.md` | Secondary Claude location | +| Gemini / Google | `{repo}/GEMINI.md` | Gemini model convention | + +### 1.2 VSCode-Style Instruction Files + +Glob-matched instruction files with metadata (applyTo patterns, description). + +| Scope | File Pattern | Code Reference | +|-------|-------------|----------------| +| Repo | `{repo}/.github/instructions/**/*.instructions.md` | `readVSCodeInstructions()` | +| User | `~/.copilot/instructions/**/*.instructions.md` | `readUserCopilotInstructions()` | + +### 1.3 User-Level Instructions + +| Scope | File Pattern | Code Reference | +|-------|-------------|----------------| +| User global | `~/.copilot/copilot-instructions.md` | `hasHomeCopilotInstructions()` | + +### 1.4 CWD-Specific Instructions + +When the working directory differs from the repo root, the same instruction patterns are re-checked relative to `{cwd}`: + +- `{cwd}/.github/copilot-instructions.md` +- `{cwd}/CLAUDE.md`, `{cwd}/.claude/CLAUDE.md` +- `{cwd}/AGENTS.md` +- `{cwd}/GEMINI.md` + +### 1.5 Nested / Child Instructions + +Breadth-first traversal from `{cwd}` up to **2 levels deep** (`CHILD_INSTRUCTIONS_MAX_DEPTH = 2`), scanning all instruction patterns in subdirectories. + +**Ignored directories:** `node_modules`, `.git`, `vendor`, `dist`, `build`, `.next`, `.nuxt`, `out`, `coverage` (plus `.gitignore` patterns when available). + +Feature-gated via `enableChildInstructions` option. + +### 1.6 Additional Sources + +| Source | Mechanism | +|--------|-----------| +| Env var | `COPILOT_CUSTOM_INSTRUCTIONS_DIRS` — comma-separated list of additional directories to scan | +| Organization | `RuntimeContext.organizationCustomInstructions` — injected at runtime via API (not file-based) | + +### 1.7 Merge Order + +Instructions are concatenated in this order (all additive): + +1. User global (`~/.copilot/copilot-instructions.md`) +2. Repo-level instruction files (all patterns above) +3. VSCode-style instruction files (repo, then user) +4. CWD-specific overrides (when cwd ≠ repo root) +5. Child/nested instructions +6. Organization instructions (API-injected) + +Duplicate content is deduplicated by file content hash. + +--- + +## 2. Skills + +Reusable prompt-based capabilities exposed as `/skill-name` slash commands. + +### 2.1 Discovery Paths + +| Source | File Pattern | Code Reference | +|--------|-------------|----------------| +| Repo (Copilot) | `{repo}/.github/skills/*/SKILL.md` | `loader.ts` collectProjectDirs | +| Repo (Agents) | `{repo}/.agents/skills/*/SKILL.md` | `loader.ts` collectProjectDirs | +| Repo (Claude) | `{repo}/.claude/skills/*/SKILL.md` | `loader.ts` collectProjectDirs | +| User (Copilot) | `~/.copilot/skills/*/SKILL.md` | `loader.ts` personalDirs | +| User (Claude) | `~/.claude/skills/*/SKILL.md` | `loader.ts` personalDirs | +| Env var | Dirs listed in `COPILOT_SKILLS_DIRS` (comma-separated) | `loader.ts` | +| Plugins | `{pluginRoot}/skills/*/SKILL.md` | `skills.ts` | + +### 2.2 File Structure + +Each skill is a directory containing a `SKILL.md` file with YAML frontmatter: + +``` +.github/skills/ + my-skill/ + SKILL.md ← markdown with frontmatter +``` + +Or a flat `SKILL.md` directly in the skills directory (single-skill mode). + +### 2.3 Frontmatter Schema + +```yaml +--- +name: skill-name # Optional; derived from folder name if absent +description: "What this skill does" # Optional; derived from first 3 lines of body +allowed-tools: grep,view # Comma-separated tool whitelist (optional) +user-invocable: true # Whether user can invoke via slash command (default: true) +disable-model-invocation: false # Whether model can invoke autonomously (default: false) +--- + +Skill prompt content here... +``` + +--- + +## 3. Commands + +A variant of skills, loaded from `.claude/commands/` only. + +| Source | File Pattern | Code Reference | +|--------|-------------|----------------| +| Project | `{repo}/.claude/commands/*.md` | `loader.ts` getCommandDirectories | +| User | `~/.claude/commands/*.md` | `loader.ts` getCommandDirectories | + +**Note:** Commands use only the `.claude/` convention — not `.github/` or `.agents/`. + +Any `.md` file in the directory is treated as a command. Same frontmatter schema as skills. Treated internally as skills with `isCommand: true`. Skills take priority over commands on name conflicts. + +--- + +## 4. Custom Agents + +Sub-agent definitions available via the task tool or direct user selection. + +### 4.1 Discovery Paths + +| Source | File Pattern | Code Reference | +|--------|-------------|----------------| +| Repo (Copilot) | `{repo}/.github/agents/*.md`, `*.agent.md` | `useCustomAgents.ts` | +| Repo (Claude) | `{repo}/.claude/agents/*.md`, `*.agent.md` | `useCustomAgents.ts` | +| User (Copilot) | `~/.github/agents/*.md`, `*.agent.md` | `useCustomAgents.ts` | +| User (Claude) | `~/.claude/agents/*.md`, `*.agent.md` | `useCustomAgents.ts` | +| Plugins | `{pluginRoot}/agents/*.md`, `*.agent.md` | `agent-loader.ts` | +| Builtin | `src/agents/definitions/*.agent.yaml` | YAML agent loader | + +### 4.2 Priority Rules + +- `*.agent.md` takes precedence over `*.md` when both exist for the same base name. +- `.github/agents/` sources have higher priority than `.claude/agents/`. + +### 4.3 Frontmatter Schema + +```yaml +--- +name: agent-name +displayName: "Human-Readable Name" +description: "What this agent does" +tools: ["*"] # or ["tool1", "tool2"] — required +model: claude-sonnet-4-20250514 # Optional model override +disableModelInvocation: false # Cannot be auto-invoked as a tool +userInvocable: true # User can select it +mcp-servers: # Inline MCP server config (optional) + server-name: + command: "npx" + args: ["@some/mcp-server"] +--- + +Agent system prompt content here... +Supports {{cwd}} placeholder. +``` + +--- + +## 5. MCP Servers + +Model Context Protocol servers that expose additional tools and resources. + +### 5.1 Config Sources (merge order, last wins) + +| Priority | Source | File Pattern | Code Reference | +|----------|--------|-------------|----------------| +| 1 (lowest) | User | `~/.copilot/mcp-config.json` | `mcp-config.ts` | +| 2 | Workspace | `{cwd}/.mcp.json` | `mcpConfigMerger.ts` | +| 3 | VSCode | `{cwd}/.vscode/mcp.json` | `vsCodeWorkspaceMcpConfig.ts` | +| 4 | Plugins | `{pluginRoot}/.mcp.json`, `{pluginRoot}/.github/mcp.json` | `mcp-loader.ts` | +| 5 | Windows ODR | Registry `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Mcp` | `odrMcpRegistry.ts` | +| 6 (highest) | CLI flag | `--additional-mcp-config ` | `mcpConfigMerger.ts` | + +### 5.2 Config Schema + +```json +{ + "mcpServers": { + "server-name": { + "type": "local | http | sse", + "command": "path/to/server", + "args": ["--flag"], + "cwd": "/optional/working/dir", + "env": { + "KEY": "$ENV_VAR", + "URL": "https://${HOST}:${PORT}", + "WITH_DEFAULT": "${VAR:-fallback}" + }, + "url": "https://remote-server/endpoint", + "headers": { "Authorization": "Bearer ${TOKEN}" }, + "tools": ["*"], + "timeout": 30000, + "filterMapping": "hidden_characters | markdown | none", + "displayName": "My Server", + "oauthClientId": "client-id", + "oauthPublicClient": false + } + } +} +``` + +**Environment variable expansion:** `$VAR`, `${VAR}`, `${VAR:-default}` are all supported in `env`, `args`, `url`, and `headers` fields. + +--- + +## 6. Hooks + +Scripts that execute at specific agent lifecycle events, with the ability to approve/deny/modify behavior. + +### 6.1 Config Sources + +| Source | File Pattern | Code Reference | +|--------|-------------|----------------| +| Config dirs | `{configDir}/**/*.json` | `hookConfigLoader.ts` | +| Plugins | `{pluginRoot}/hooks.json` | `hooks.ts` | +| Plugins (alt) | `{pluginRoot}/hooks/hooks.json` | `hooks.ts` | +| Plugin manifest | Inline in `plugin.json` → `hooks` field (object) | `hooks.ts` | + +### 6.2 Hook Events + +| Event | Trigger | Can Modify? | +|-------|---------|-------------| +| `sessionStart` | Session begins | No (informational) | +| `sessionEnd` | Session ends | No (informational) | +| `userPromptSubmitted` | User sends a message | Yes (can modify prompt) | +| `preToolUse` | Before tool execution | Yes (allow / deny / modify args) | +| `postToolUse` | After tool execution | Yes (can modify result) | +| `errorOccurred` | Error happens | Yes (retry / skip / abort) | +| `agentStop` | Main agent finishes | Yes (can force continuation) | +| `subagentStop` | Sub-agent completes | Yes (can force continuation) | + +### 6.3 Config Schema + +```json +{ + "version": 1, + "hooks": { + "preToolUse": [ + { + "type": "command", + "command": "bash", + "args": ["-c", "echo checking"], + "cwd": "/optional/cwd", + "env": { "KEY": "value" }, + "timeout": 30000 + } + ] + } +} +``` + +--- + +## 7. Plugins + +Bundles that install combinations of skills, agents, hooks, and MCP servers. + +### 7.1 Plugin Manifest Locations + +Within a plugin repository, the manifest is searched at: + +| File Pattern | Code Reference | +|-------------|----------------| +| `plugin.json` | `marketplace-loader.ts` PLUGIN_JSON_PATHS | +| `.github/plugin/plugin.json` | `marketplace-loader.ts` PLUGIN_JSON_PATHS | +| `.claude-plugin/plugin.json` | `marketplace-loader.ts` PLUGIN_JSON_PATHS | + + +Each field (`skills`, `agents`, `hooks`) can be a string path, array of paths, or (for hooks) an inline object. + +--- + +## Appendix A: XDG Base Directory Compliance + +All `~/.copilot/` paths respect XDG overrides: + +| Type | Default | XDG Override | +|------|---------|-------------| +| Config files | `~/.copilot/` | `$XDG_CONFIG_HOME/.copilot/` | +| State/cache | `~/.copilot/` | `$XDG_STATE_HOME/.copilot/` | + +The base directory name is always `.copilot` (`APP_DIRECTORY` in `path-helpers.ts`). + +--- + +## Appendix B: Complete Discovery Summary + +``` +Message received + │ + ├─ Feature flags resolved + │ ├─ Tier defaults + │ ├─ config.json → feature_flags.enabled + │ └─ Env vars (COPILOT_CLI_ENABLED_FEATURE_FLAGS, individual) + │ + ├─ System prompt assembled + │ ├─ Base agent prompt + │ ├─ User instructions ~/.copilot/copilot-instructions.md + │ ├─ Repo instructions .github/copilot-instructions.md, AGENTS.md, CLAUDE.md, GEMINI.md + │ ├─ VSCode instructions .github/instructions/**/*.instructions.md + │ ├─ CWD instructions (when cwd ≠ repo root) + │ ├─ Child instructions (depth=2 traversal) + │ └─ Org instructions (API-injected) + │ + ├─ Tools assembled + │ ├─ Built-in tools + │ ├─ MCP servers ~/.copilot/mcp-config.json + .mcp.json + .vscode/mcp.json + plugins + │ └─ Content exclusion (org API restrictions applied) + │ + ├─ Skills listed .github/skills/ + .agents/skills/ + .claude/skills/ + personal + plugins + ├─ Commands listed .claude/commands/ + personal + ├─ Custom agents listed .github/agents/ + .claude/agents/ + personal + plugins + │ + ├─ userPromptSubmitted hooks fire + │ + ├─ Model selected config.json → model, agent override, or default + │ + ├─ For each tool call: + │ ├─ preToolUse hooks (allow / deny / modify) + │ ├─ Permission check + │ ├─ Firewall policy + │ ├─ Tool executes + │ └─ postToolUse hooks (modify result) + │ + └─ Session telemetry emitted +``` diff --git a/src/vs/workbench/api/browser/mainThreadChatDebug.ts b/src/vs/workbench/api/browser/mainThreadChatDebug.ts index d36be25864758..82594dcb038d3 100644 --- a/src/vs/workbench/api/browser/mainThreadChatDebug.ts +++ b/src/vs/workbench/api/browser/mainThreadChatDebug.ts @@ -83,6 +83,7 @@ export class MainThreadChatDebug extends Disposable implements MainThreadChatDeb ...base, kind: 'modelTurn', model: dto.model, + requestName: dto.requestName, inputTokens: dto.inputTokens, outputTokens: dto.outputTokens, totalTokens: dto.totalTokens, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3fc7572a3a99c..8abfea0b8d317 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1415,6 +1415,7 @@ export interface IChatDebugToolCallEventDto extends IChatDebugEventCommonDto { export interface IChatDebugModelTurnEventDto extends IChatDebugEventCommonDto { readonly kind: 'modelTurn'; readonly model?: string; + readonly requestName?: string; readonly inputTokens?: number; readonly outputTokens?: number; readonly totalTokens?: number; diff --git a/src/vs/workbench/api/common/extHostChatDebug.ts b/src/vs/workbench/api/common/extHostChatDebug.ts index 4790d2141a5e0..04125f3e551aa 100644 --- a/src/vs/workbench/api/common/extHostChatDebug.ts +++ b/src/vs/workbench/api/common/extHostChatDebug.ts @@ -145,6 +145,7 @@ export class ExtHostChatDebug extends Disposable implements ExtHostChatDebugShap ...base, kind: 'modelTurn', model: e.model, + requestName: e.requestName, inputTokens: e.inputTokens, outputTokens: e.outputTokens, totalTokens: e.totalTokens, @@ -197,13 +198,14 @@ export class ExtHostChatDebug extends Disposable implements ExtHostChatDebugShap }; } default: { - // Final fallback: treat as generic const generic = event as vscode.ChatDebugGenericEvent; + const rawName = generic.name; + const rawDetails = generic.details; return { ...base, kind: 'generic', - name: generic.name ?? '', - details: generic.details, + name: typeof rawName === 'string' ? rawName : '', + details: typeof rawDetails === 'string' ? rawDetails : undefined, level: generic.level ?? 1, category: generic.category, }; diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index 89b50a46a6d00..0cd292267d5f8 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -717,7 +717,7 @@ export class BrowserEditor extends EditorPane { */ async showFind(): Promise { // Get selected text from the browser view to pre-populate the search box. - const selectedText = await this._model?.getSelectedText(); + const selectedText = (await this._model?.getSelectedText())?.trim(); // Only use the selected text if it doesn't contain newlines (single line selection) const textToReveal = selectedText && !/[\r\n]/.test(selectedText) ? selectedText : undefined; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts index f5d589353f4e6..40827f04060d7 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts @@ -7,6 +7,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { h } from '../../../../../base/browser/dom.js'; import { Disposable, IDisposable, markAsSingleton } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; +import { isAbsolute } from '../../../../../base/common/path.js'; import { basename } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -25,8 +26,9 @@ import { IKeybindingService } from '../../../../../platform/keybinding/common/ke import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import { ResourceContextKey } from '../../../../common/contextkeys.js'; +import { IsSessionsWindowContext, ResourceContextKey } from '../../../../common/contextkeys.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; import { IChatAgentService } from '../../common/participants/chatAgents.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; @@ -36,12 +38,72 @@ import { ChatSendResult, IChatService } from '../../common/chatService/chatServi import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js'; -import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; -import { IChatWidget, IChatWidgetService } from '../chat.js'; +import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; +import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js'; +import { IChatWidget, IChatWidgetService, isIChatViewViewContext } from '../chat.js'; import { ctxHasEditorModification } from '../chatEditing/chatEditingEditorContextKeys.js'; import { CHAT_SETUP_ACTION_ID } from './chatActions.js'; import { PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js'; +/** + * Extracts the "owner/repo" name-with-owner from a git remote URL. + * Supports HTTPS (https://github.com/owner/repo.git) and SSH (git@github.com:owner/repo.git) formats. + */ +function extractNwoFromRemoteUrl(remoteUrl: string): string | undefined { + const match = remoteUrl.match(/(?:github\.com)[/:](?[^/]+)\/(?[^/.]+)/); + if (match?.groups) { + return `${match.groups.owner}/${match.groups.repo}`; + } + return undefined; +} + +/** + * Resolves GitHub NWO from a local git repository path by reading `.git/config`. + * Handles both regular repos and git worktrees. + */ +async function resolveGitRemoteNwo(repoPath: string, fileService: IFileService): Promise { + try { + const gitPath = `${repoPath}/.git`; + const gitUri = URI.file(gitPath); + + let configUri: URI; + try { + const stat = await fileService.stat(gitUri); + if (stat.isDirectory) { + // Regular git repo + configUri = URI.file(`${gitPath}/config`); + } else { + // Git worktree — .git is a file with "gitdir: " + const gitFile = await fileService.readFile(gitUri); + const gitDir = gitFile.value.toString().trim().replace(/^gitdir:\s*/, ''); + // Resolve relative paths + const resolvedGitDir = gitDir.startsWith('/') + ? gitDir + : `${repoPath}/${gitDir}`; + // The config is in the common dir (parent of worktree git dirs) + // e.g., gitdir points to /repo/.git/worktrees/name, config is at /repo/.git/config + const commonDir = resolvedGitDir.replace(/\/worktrees\/[^/]+$/, ''); + configUri = URI.file(`${commonDir}/config`); + } + } catch { + // .git doesn't exist + return undefined; + } + + const content = await fileService.readFile(configUri); + const configText = content.value.toString(); + + // Parse remote "origin" URL from git config + const remoteMatch = configText.match(/\[remote\s+"origin"\][^[]*url\s*=\s*(.+)/m); + if (remoteMatch?.[1]) { + return extractNwoFromRemoteUrl(remoteMatch[1].trim()); + } + } catch { + // File not found or not readable + } + return undefined; +} + export const enum ActionLocation { ChatWidget = 'chatWidget', Editor = 'editor' @@ -212,6 +274,98 @@ export class CreateRemoteAgentJobAction { commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${continuationTarget.type}`); } + /** + * Extracts the GitHub "owner/repo" NWO from the source session by checking + * multiple data sources: chat model repoData, session metadata, and session options. + */ + private async extractRepoNwoFromSession(agentSessionsService: IAgentSessionsService, chatSessionsService: IChatSessionsService, fileService: IFileService, sessionResource: URI, chatModel: ChatModel): Promise { + // 1. Try chat model's repoData (populated when local git repo exists) + const repoData = chatModel.repoData; + if (repoData?.remoteUrl) { + const nwo = extractNwoFromRemoteUrl(repoData.remoteUrl); + if (nwo) { + return nwo; + } + } + + // 2. Try agent session metadata (populated by session providers) + const agentSession = agentSessionsService.getSession(sessionResource); + if (agentSession?.metadata) { + const metadata = agentSession.metadata; + + // Cloud sessions set name/owner in metadata + const owner = metadata.owner as string | undefined; + const name = metadata.name as string | undefined; + if (owner && name) { + return `${owner}/${name}`; + } + + // Background sessions may set repositoryNwo directly + const repositoryNwo = metadata.repositoryNwo as string | undefined; + if (repositoryNwo?.includes('/')) { + return repositoryNwo; + } + + // Background sessions may set repositoryUrl + const repositoryUrl = metadata.repositoryUrl as string | undefined; + if (repositoryUrl) { + const nwo = extractNwoFromRemoteUrl(repositoryUrl); + if (nwo) { + return nwo; + } + } + + // Background sessions set workingDirectoryPath — resolve git remote from it + const workingDir = (metadata.workingDirectoryPath ?? metadata.repositoryPath ?? metadata.worktreePath) as string | undefined; + if (workingDir) { + const nwo = await resolveGitRemoteNwo(workingDir, fileService); + if (nwo) { + return nwo; + } + } + } + + // 3. Try session options (repository picker selection) + // Cloud sessions use 'repositories', sessions window uses 'repository' + for (const optionId of ['repositories', 'repository']) { + const repoOption = chatSessionsService.getSessionOption(sessionResource, optionId); + if (repoOption) { + const optionValue = typeof repoOption === 'string' ? repoOption : (repoOption as { id: string }).id; + if (optionValue) { + // Check if it's already a "owner/repo" NWO (exactly two segments) + const segments = optionValue.split('/').filter(Boolean); + if (segments.length === 2) { + return optionValue; + } + // Try extracting NWO from a URL + const nwo = extractNwoFromRemoteUrl(optionValue); + if (nwo) { + return nwo; + } + // Try parsing as URI (e.g. github-remote-file://github/owner/repo/...) + try { + const uri = URI.parse(optionValue); + if (uri.authority === 'github') { + const parts = uri.path.split('/').filter(Boolean); + if (parts.length >= 2) { + return `${parts[0]}/${parts[1]}`; + } + } + } catch { /* ignore */ } + // Local filesystem path — resolve git remote + if (isAbsolute(optionValue)) { + const nwoFromGit = await resolveGitRemoteNwo(optionValue, fileService); + if (nwoFromGit) { + return nwoFromGit; + } + } + } + } + } + + return undefined; + } + async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint, _widget?: IChatWidget) { const contextKeyService = accessor.get(IContextKeyService); const commandService = accessor.get(ICommandService); @@ -219,6 +373,9 @@ export class CreateRemoteAgentJobAction { const chatAgentService = accessor.get(IChatAgentService); const chatService = accessor.get(IChatService); const editorService = accessor.get(IEditorService); + const agentSessionsService = accessor.get(IAgentSessionsService); + const chatSessionsService = accessor.get(IChatSessionsService); + const fileService = accessor.get(IFileService); const remoteJobCreatingKey = ChatContextKeys.remoteJobCreating.bindTo(contextKeyService); @@ -273,10 +430,53 @@ export class CreateRemoteAgentJobAction { } } + const continuationTargetType = continuationTarget.type; + + // When source and target session types differ in the sessions window, + // open a new session of the target type with the prompt and context + // instead of sending to the current (incompatible) session resource. + const isSessionsWindow = IsSessionsWindowContext.getValue(contextKeyService); + const sourceProvider = getAgentSessionProvider(sessionResource); + if (isSessionsWindow && sourceProvider && sourceProvider !== continuationTargetType) { + const isSidebar = isIChatViewViewContext(widget.viewContext); + const actionId = isSidebar + ? `workbench.action.chat.openNewSessionSidebar.${continuationTargetType}` + : `${NEW_CHAT_SESSION_ACTION_ID}.${continuationTargetType}`; + + // Build conversation transcript from the source session to preserve context. + // Truncate to avoid exceeding token limits of the target model. + const maxTranscriptLength = 20_000; + let transcript = chatRequests.map(req => { + const userMsg = `User: ${req.message.text}`; + const respMsg = req.response?.response ? `Assistant: ${req.response.response.getMarkdown()}` : ''; + return respMsg ? `${userMsg}\n${respMsg}` : userMsg; + }).join('\n\n'); + if (transcript.length > maxTranscriptLength) { + transcript = transcript.substring(transcript.length - maxTranscriptLength); + } + + const delegationPrompt = transcript + ? `The following is the conversation history from a previous ${getAgentSessionProviderName(sourceProvider)} session. Continue working on it.\n\n${transcript}\n\nUser: ${userPrompt}` + : userPrompt; + + // Extract repository info from the source session to pass to the target session + const initialSessionOptions: { optionId: string; value: string }[] = []; + const repoNwo = await this.extractRepoNwoFromSession(agentSessionsService, chatSessionsService, fileService, sessionResource, chatModel); + if (repoNwo) { + initialSessionOptions.push({ optionId: 'repositories', value: repoNwo }); + } + + await commandService.executeCommand(actionId, { + prompt: delegationPrompt, + attachedContext: attachedContext.asArray(), + initialSessionOptions: initialSessionOptions.length > 0 ? initialSessionOptions : undefined, + }); + return; + } + const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat); const instantiationService = accessor.get(IInstantiationService); const requestParser = instantiationService.createInstance(ChatRequestParser); - const continuationTargetType = continuationTarget.type; // Add the request to the model first const parsedRequest = requestParser.parseChatRequest(sessionResource, userPrompt, ChatAgentLocation.Chat); @@ -300,7 +500,7 @@ export class CreateRemoteAgentJobAction { await widget.handleDelegationExitIfNeeded(defaultAgent, sendResult.data.agent); } } catch (e) { - console.error('Error creating remote coding agent job', e); + console.error('[Delegation] Error creating remote coding agent job', e); throw e; } finally { remoteJobCreatingKey.set(false); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 7e505aa90765c..9701a79f06436 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -807,7 +807,7 @@ class SendToNewChatAction extends Action2 { // Cancel any in-progress request before clearing if (widget.viewModel) { - chatService.cancelCurrentRequestForSession(widget.viewModel.sessionResource); + chatService.cancelCurrentRequestForSession(widget.viewModel.sessionResource, 'newSessionAction'); } if (widget.viewModel?.model) { @@ -886,7 +886,7 @@ export class CancelAction extends Action2 { const chatService = accessor.get(IChatService); if (widget.viewModel) { - chatService.cancelCurrentRequestForSession(widget.viewModel.sessionResource); + chatService.cancelCurrentRequestForSession(widget.viewModel.sessionResource, 'cancelAction'); } else { telemetryService.publicLog2(ChatStopCancellationNoopEventName, { source: 'cancelAction', diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts index a8d166e68f7d2..2db5e67e5d3c3 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts @@ -213,7 +213,7 @@ export class ChatSendPendingImmediatelyAction extends Action2 { ]; chatService.setPendingRequests(context.sessionResource, reordered); - chatService.cancelCurrentRequestForSession(context.sessionResource); + chatService.cancelCurrentRequestForSession(context.sessionResource, 'queueRunNext'); chatService.processPendingRequests(context.sessionResource); } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 8edec5cccc335..84a06b2877e8e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -139,7 +139,7 @@ .agent-session-title-row { line-height: 17px; - padding-bottom: 6px; + padding-bottom: 4px; } .agent-session-details-row { diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 5be1a768214db..5f52802bda9cb 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -9,7 +9,7 @@ import { Disposable, DisposableStore } from '../../../../../base/common/lifecycl import { Emitter, Event } from '../../../../../base/common/event.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { autorun } from '../../../../../base/common/observable.js'; -import { basename, dirname } from '../../../../../base/common/resources.js'; +import { basename, dirname, isEqualOrParent } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { Codicon } from '../../../../../base/common/codicons.js'; @@ -769,8 +769,12 @@ export class AICustomizationListWidget extends Disposable { } } else if (promptType === PromptsType.prompt) { // Use getPromptSlashCommands which has parsed name/description from frontmatter + // Filter out skills since they have their own section const commands = await this.promptsService.getPromptSlashCommands(CancellationToken.None); for (const command of commands) { + if (command.promptPath.type === PromptsType.skill) { + continue; + } const filename = basename(command.promptPath.uri); items.push({ id: command.promptPath.uri.toString(), @@ -816,8 +820,29 @@ export class AICustomizationListWidget extends Disposable { }); } } else { - // For instructions, fetch once and group by storage - const allItems = await this.promptsService.listPromptFiles(promptType, CancellationToken.None); + // For instructions, fetch prompt files and group by storage + const promptFiles = await this.promptsService.listPromptFiles(promptType, CancellationToken.None); + const allItems: IPromptPath[] = [...promptFiles]; + + // Also include agent instruction files (AGENTS.md, CLAUDE.md, copilot-instructions.md) + if (promptType === PromptsType.instructions) { + const agentInstructions = await this.promptsService.listAgentInstructions(CancellationToken.None, undefined); + const workspaceFolderUris = this.workspaceContextService.getWorkspace().folders.map(f => f.uri); + const activeRoot = this.workspaceService.getActiveProjectRoot(); + if (activeRoot) { + workspaceFolderUris.push(activeRoot); + } + for (const file of agentInstructions) { + const isWorkspaceFile = workspaceFolderUris.some(root => isEqualOrParent(file.uri, root)); + allItems.push({ + uri: file.uri, + storage: isWorkspaceFile ? PromptsStorage.local : PromptsStorage.user, + type: PromptsType.instructions, + name: basename(file.uri), + }); + } + } + const workspaceItems = allItems.filter(item => item.storage === PromptsStorage.local); const userItems = allItems.filter(item => item.storage === PromptsStorage.user); const extensionItems = allItems.filter(item => item.storage === PromptsStorage.extension); @@ -844,6 +869,16 @@ export class AICustomizationListWidget extends Disposable { items.push(...pluginItems.map(mapToListItem)); } + // Filter out files under excluded user roots + const excludedRoots = this.workspaceService.excludedUserFileRoots; + if (excludedRoots.length > 0) { + for (let i = items.length - 1; i >= 0; i--) { + if (items[i].storage === PromptsStorage.user && excludedRoots.some(root => isEqualOrParent(items[i].uri, root))) { + items.splice(i, 1); + } + } + } + // Sort items by name items.sort((a, b) => a.name.localeCompare(b.name)); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 33d40453b83e0..8095e29d8a6c2 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -7,6 +7,7 @@ import './media/aiCustomizationManagement.css'; import * as DOM from '../../../../../base/browser/dom.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { DisposableStore, IReference, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Delayer } from '../../../../../base/common/async.js'; import { Event } from '../../../../../base/common/event.js'; import { autorun } from '../../../../../base/common/observable.js'; import { Orientation, Sizing, SplitView } from '../../../../../base/browser/ui/splitview/splitview.js'; @@ -53,13 +54,14 @@ import { showConfigureHooksQuickPick } from '../promptSyntax/hookActions.js'; import { resolveWorkspaceTargetDirectory, resolveUserTargetDirectory } from './customizationCreatorService.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ILayoutService } from '../../../../../platform/layout/browser/layoutService.js'; import { getSimpleEditorOptions } from '../../../codeEditor/browser/simpleEditorOptions.js'; import { IWorkingCopyService } from '../../../../services/workingCopy/common/workingCopyService.js'; +import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; +import { IPathService } from '../../../../services/path/common/pathService.js'; import { McpServerEditorInput } from '../../../mcp/browser/mcpServerEditorInput.js'; import { McpServerEditor } from '../../../mcp/browser/mcpServerEditor.js'; import { IWorkbenchMcpServer } from '../../../mcp/common/mcpTypes.js'; @@ -140,7 +142,7 @@ export class AICustomizationManagementEditor extends EditorPane { private modelsContentContainer: HTMLElement | undefined; private modelsFooterElement: HTMLElement | undefined; - // Embedded editor state (sessions only — preferManualCreation) + // Embedded editor state private editorContentContainer: HTMLElement | undefined; private embeddedEditor: CodeEditorWidget | undefined; private editorItemNameElement!: HTMLElement; @@ -176,31 +178,30 @@ export class AICustomizationManagementEditor extends EditorPane { @IOpenerService private readonly openerService: IOpenerService, @ICommandService private readonly commandService: ICommandService, @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, - @IEditorService private readonly editorService: IEditorService, @IPromptsService private readonly promptsService: IPromptsService, @ITextModelService private readonly textModelService: ITextModelService, @IConfigurationService private readonly configurationService: IConfigurationService, @ILayoutService private readonly layoutService: ILayoutService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @ITextFileService private readonly textFileService: ITextFileService, + @IPathService private readonly pathService: IPathService, ) { super(AICustomizationManagementEditor.ID, group, telemetryService, themeService, storageService); this.inEditorContextKey = CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_EDITOR.bindTo(contextKeyService); this.sectionContextKey = CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_SECTION.bindTo(contextKeyService); - // Track workspace changes for embedded editor (sessions only) - if (this.workspaceService.preferManualCreation) { - this._register(autorun(reader => { - this.workspaceService.activeProjectRoot.read(reader); - if (this.viewMode === 'editor') { - this.currentEditingProjectRoot = this.workspaceService.getActiveProjectRoot(); - } - })); - this._register(toDisposable(() => { - this.currentModelRef?.dispose(); - this.currentModelRef = undefined; - })); - } + // Track workspace changes for embedded editor + this._register(autorun(reader => { + this.workspaceService.activeProjectRoot.read(reader); + if (this.viewMode === 'editor') { + this.currentEditingProjectRoot = this.workspaceService.getActiveProjectRoot(); + } + })); + this._register(toDisposable(() => { + this.currentModelRef?.dispose(); + this.currentModelRef = undefined; + })); // Build sections from the workspace service configuration const sectionInfo: Record = { @@ -359,13 +360,9 @@ export class AICustomizationManagementEditor extends EditorPane { // Handle item selection this.editorDisposables.add(this.listWidget.onDidSelectItem(item => { - if (this.workspaceService.preferManualCreation) { - const isWorkspaceFile = item.storage === PromptsStorage.local; - const isReadOnly = item.storage === PromptsStorage.extension || item.storage === PromptsStorage.plugin; - this.showEmbeddedEditor(item.uri, item.name, isWorkspaceFile, isReadOnly); - } else { - this.editorService.openEditor({ resource: item.uri }); - } + const isWorkspaceFile = item.storage === PromptsStorage.local; + const isReadOnly = item.storage === PromptsStorage.extension || item.storage === PromptsStorage.plugin; + this.showEmbeddedEditor(item.uri, item.name, isWorkspaceFile, isReadOnly); })); // Handle create actions - AI-guided creation @@ -412,11 +409,9 @@ export class AICustomizationManagementEditor extends EditorPane { })); } - // Embedded editor container (sessions only) - if (this.workspaceService.preferManualCreation) { - this.editorContentContainer = DOM.append(contentInner, $('.editor-content-container')); - this.createEmbeddedEditor(); - } + // Embedded editor container + this.editorContentContainer = DOM.append(contentInner, $('.editor-content-container')); + this.createEmbeddedEditor(); // Set initial visibility based on selected section this.updateContentVisibility(); @@ -496,27 +491,22 @@ export class AICustomizationManagementEditor extends EditorPane { * Creates a new customization using the AI-guided flow. */ private async createNewItemWithAI(type: PromptsType): Promise { - this.close(); + if (this.input) { + this.group.closeEditor(this.input); + } await this.workspaceService.generateCustomization(type); } /** - * Creates a new prompt file and opens it. - * In sessions (preferManualCreation), uses the embedded editor with commit-on-close. - * In core, opens in a regular editor tab. + * Creates a new prompt file and opens it in the embedded editor. */ private async createNewItemManual(type: PromptsType, target: 'workspace' | 'user'): Promise { - const useEmbeddedEditor = this.workspaceService.preferManualCreation; if (type === PromptsType.hook) { const isWorkspace = target === 'workspace'; await this.instantiationService.invokeFunction(showConfigureHooksQuickPick, { openEditor: async (resource) => { - if (useEmbeddedEditor) { - await this.showEmbeddedEditor(resource, basename(resource), isWorkspace); - } else { - await this.editorService.openEditor({ resource }); - } + await this.showEmbeddedEditor(resource, basename(resource), isWorkspace); return; }, }); @@ -525,20 +515,15 @@ export class AICustomizationManagementEditor extends EditorPane { const targetDir = target === 'workspace' ? resolveWorkspaceTargetDirectory(this.workspaceService, type) - : await resolveUserTargetDirectory(this.promptsService, type); + : await resolveUserTargetDirectory(this.promptsService, type, this.configurationService, this.pathService); const options: INewPromptOptions = { targetFolder: targetDir, targetStorage: target === 'user' ? PromptsStorage.user : PromptsStorage.local, openFile: async (uri) => { - if (useEmbeddedEditor) { - const isWorkspace = target === 'workspace'; - await this.showEmbeddedEditor(uri, basename(uri), isWorkspace); - return this.embeddedEditor; - } else { - await this.editorService.openEditor({ resource: uri }); - return undefined; - } + const isWorkspace = target === 'workspace'; + await this.showEmbeddedEditor(uri, basename(uri), isWorkspace); + return this.embeddedEditor; }, }; @@ -626,13 +611,7 @@ export class AICustomizationManagementEditor extends EditorPane { void this.listWidget.refresh(); } - private close(): void { - if (!this.workspaceService.preferManualCreation && this.input) { - this.group.closeEditor(this.input); - } - } - - //#region Embedded Editor (sessions only) + //#region Embedded Editor private createEmbeddedEditor(): void { if (!this.editorContentContainer) { @@ -701,10 +680,21 @@ export class AICustomizationManagementEditor extends EditorPane { this.embeddedEditor!.focus(); this.editorModelChangeDisposables.clear(); + const saveDelayer = this.editorModelChangeDisposables.add(new Delayer(500)); this.editorModelChangeDisposables.add(ref.object.textEditorModel.onDidChangeContent(() => { this.editorSaveIndicator.className = 'editor-save-indicator visible'; this.editorSaveIndicator.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); this.editorSaveIndicator.title = localize('saving', "Saving..."); + saveDelayer.trigger(async () => { + try { + await this.textFileService.save(uri); + } catch (error) { + console.error('Failed to save AI customization file:', error); + this.editorSaveIndicator.className = 'editor-save-indicator visible error'; + this.editorSaveIndicator.classList.add(...ThemeIcon.asClassNameArray(Codicon.error)); + this.editorSaveIndicator.title = localize('saveFailed', "Save Failed"); + } + }); })); this.editorModelChangeDisposables.add(this.workingCopyService.onDidSave(e => { if (isEqual(e.workingCopy.resource, uri)) { diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts index 8c2cdc5228452..4f4828681261b 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts @@ -62,6 +62,8 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic readonly preferManualCreation = false; + readonly excludedUserFileRoots: readonly URI[] = []; + async commitFiles(_projectRoot: URI, _fileUris: URI[]): Promise { // No-op in core VS Code. } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts index e8188abf63b04..00d0721d35bcf 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts @@ -6,13 +6,15 @@ import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; import { IChatWidgetService } from '../chat.js'; import { IChatService } from '../../common/chatService/chatService.js'; -import { ChatModeKind } from '../../common/constants.js'; +import { ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { getPromptFileDefaultLocations } from '../../common/promptSyntax/config/promptFileLocations.js'; import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { URI } from '../../../../../base/common/uri.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IPathService } from '../../../../services/path/common/pathService.js'; import { localize } from '../../../../../nls.js'; /** @@ -32,6 +34,8 @@ export class CustomizationCreatorService { @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, @IPromptsService private readonly promptsService: IPromptsService, @IQuickInputService private readonly quickInputService: IQuickInputService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IPathService private readonly pathService: IPathService, ) { } async createWithAI(type: PromptsType): Promise { @@ -99,7 +103,7 @@ export class CustomizationCreatorService { * Resolves the user-level directory for a new customization file. */ async resolveUserDirectory(type: PromptsType): Promise { - return resolveUserTargetDirectory(this.promptsService, type); + return resolveUserTargetDirectory(this.promptsService, type, this.configurationService, this.pathService); } } @@ -121,13 +125,41 @@ export function resolveWorkspaceTargetDirectory(workspaceService: IAICustomizati /** * Resolves the user-level directory for a new customization file. + * If chat.customizations.userStoragePath is set, uses that as the base + * with a type-appropriate subfolder. Otherwise falls back to the default + * source folders from IPromptsService. */ -export async function resolveUserTargetDirectory(promptsService: IPromptsService, type: PromptsType): Promise { +export async function resolveUserTargetDirectory( + promptsService: IPromptsService, + type: PromptsType, + configurationService?: IConfigurationService, + pathService?: IPathService, +): Promise { + const overridePath = configurationService?.getValue(ChatConfiguration.ChatCustomizationUserStoragePath); + const subfolder = getUserStorageSubfolder(type); + if (overridePath && pathService && subfolder) { + const userHome = await pathService.userHome(); + const resolved = overridePath.startsWith('~/') + ? URI.joinPath(userHome, overridePath.slice(2)) + : URI.file(overridePath); + return URI.joinPath(resolved, subfolder); + } const folders = await promptsService.getSourceFolders(type); const userFolder = folders.find(f => f.storage === PromptsStorage.user); return userFolder?.uri; } +/** + * Returns the subdirectory name for user-level storage of a given prompt type. + */ +function getUserStorageSubfolder(type: PromptsType): string | undefined { + switch (type) { + case PromptsType.instructions: return 'instructions'; + case PromptsType.skill: return 'skills'; + default: return undefined; + } +} + //#region Agent Instructions /** diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 37f7907162471..d8b6675e6cf0c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1245,6 +1245,12 @@ configurationRegistry.registerConfiguration({ tags: ['preview'], description: nls.localize('chat.aiCustomizationMenu.enabled', "Controls whether the Chat Customizations editor is available in the Command Palette. When disabled, the Chat Customizations editor and related commands are hidden."), default: true, + }, + [ChatConfiguration.ChatCustomizationUserStoragePath]: { + type: 'string', + tags: ['experimental'], + description: nls.localize('chat.customizationsMenu.userStoragePath', "Experimental: This setting is temporary and should not be relied on. Override the base directory for user-level customization files. When set, new user customizations are created here instead of the VS Code profile folder."), + default: '', } } }); diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEventList.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEventList.ts index 7ae85ffa0f88b..9797275555bd2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEventList.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEventList.ts @@ -12,6 +12,14 @@ import { safeIntl } from '../../../../../base/common/date.js'; const $ = DOM.$; +/** Coerce a value to a string, returning a fallback for null/undefined/non-strings. */ +function safeStr(value: string | undefined | null, fallback: string = ''): string { + if (value === null || value === undefined || typeof value !== 'string') { + return fallback; + } + return value; +} + const dateFormatter = safeIntl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', @@ -32,30 +40,31 @@ function renderEventToTemplate(element: IChatDebugEvent, templateData: IChatDebu switch (element.kind) { case 'toolCall': - templateData.name.textContent = element.toolName; - templateData.details.textContent = element.result ?? ''; + templateData.name.textContent = safeStr(element.toolName, localize('chatDebug.unknownEvent', "(unknown)")); + templateData.details.textContent = safeStr(element.result); break; case 'modelTurn': - templateData.name.textContent = element.model ?? localize('chatDebug.modelTurn', "Model Turn"); - templateData.details.textContent = element.totalTokens !== undefined - ? localize('chatDebug.tokens', "{0} tokens", element.totalTokens) - : ''; + templateData.name.textContent = safeStr(element.model) || localize('chatDebug.modelTurn', "Model Turn"); + templateData.details.textContent = [ + safeStr(element.requestName), + element.totalTokens !== undefined ? localize('chatDebug.tokens', "{0} tokens", element.totalTokens) : '', + ].filter(Boolean).join(' \u00b7 '); break; case 'generic': - templateData.name.textContent = element.name; - templateData.details.textContent = element.details ?? ''; + templateData.name.textContent = safeStr(element.name, localize('chatDebug.unknownEvent', "(unknown)")); + templateData.details.textContent = safeStr(element.details); break; case 'subagentInvocation': - templateData.name.textContent = element.agentName; - templateData.details.textContent = element.description ?? (element.status ?? ''); + templateData.name.textContent = safeStr(element.agentName, localize('chatDebug.unknownEvent', "(unknown)")); + templateData.details.textContent = safeStr(element.description) || safeStr(element.status); break; case 'userMessage': templateData.name.textContent = localize('chatDebug.userMessage', "User Message"); - templateData.details.textContent = element.message; + templateData.details.textContent = safeStr(element.message); break; case 'agentResponse': templateData.name.textContent = localize('chatDebug.agentResponse', "Agent Response"); - templateData.details.textContent = element.message; + templateData.details.textContent = safeStr(element.message); break; } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts index 2a82fb2fd227c..8f25da718a1c1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts @@ -119,7 +119,7 @@ export function registerFilterMenuItems( registerToggle(CHAT_DEBUG_CMD_TOGGLE_TOOL_CALL, localize('chatDebug.filter.toolCall', "Tool Calls"), CHAT_DEBUG_KIND_TOOL_CALL, '1_kind', () => state.filterKindToolCall, v => { state.filterKindToolCall = v; }, kindToolCallKey); registerToggle(CHAT_DEBUG_CMD_TOGGLE_MODEL_TURN, localize('chatDebug.filter.modelTurn', "Model Turns"), CHAT_DEBUG_KIND_MODEL_TURN, '1_kind', () => state.filterKindModelTurn, v => { state.filterKindModelTurn = v; }, kindModelTurnKey); - registerToggle(CHAT_DEBUG_CMD_TOGGLE_PROMPT_DISCOVERY, localize('chatDebug.filter.promptDiscovery', "Prompt Discovery"), CHAT_DEBUG_KIND_PROMPT_DISCOVERY, '1_kind', () => state.filterKindPromptDiscovery, v => { state.filterKindPromptDiscovery = v; }, kindPromptDiscoveryKey); + registerToggle(CHAT_DEBUG_CMD_TOGGLE_PROMPT_DISCOVERY, localize('chatDebug.filter.promptDiscovery', "Chat Customization"), CHAT_DEBUG_KIND_PROMPT_DISCOVERY, '1_kind', () => state.filterKindPromptDiscovery, v => { state.filterKindPromptDiscovery = v; }, kindPromptDiscoveryKey); registerToggle(CHAT_DEBUG_CMD_TOGGLE_SUBAGENT, localize('chatDebug.filter.subagent', "Subagent Invocations"), CHAT_DEBUG_KIND_SUBAGENT, '1_kind', () => state.filterKindSubagent, v => { state.filterKindSubagent = v; }, kindSubagentKey); return store; diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts index 6cfe024552c64..528e3a5d2915d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts @@ -549,6 +549,9 @@ function getEventSublabel(event: IChatDebugEvent, effectiveKind?: IChatDebugEven switch (kind) { case 'modelTurn': { const parts: string[] = []; + if (event.kind === 'modelTurn' && event.requestName) { + parts.push(event.requestName); + } if (event.kind === 'modelTurn' && event.totalTokens) { parts.push(localize('tokenCount', "{0} tokens", event.totalTokens)); } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts index 5f177aa9e21b0..20a12c032c801 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts @@ -98,7 +98,11 @@ export class ChatDebugHomeView extends Disposable { DOM.append(item, $(`span${ThemeIcon.asCSSSelector(Codicon.comment)}`)); const titleSpan = DOM.append(item, $('span.chat-debug-home-session-item-title')); - const isShimmering = isUUID(sessionTitle); + // Only show shimmer when the title is a UUID AND the model is not + // yet loaded. A live session with no requests yet has an empty title but its model exists — show a + // placeholder instead of an indefinite spinner. + const hasLiveModel = !!this.chatService.getSession(sessionResource); + const isShimmering = isUUID(sessionTitle) && !hasLiveModel; if (isShimmering) { titleSpan.classList.add('chat-debug-home-session-item-shimmer'); item.disabled = true; diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 538cf28732f06..f0161aec65010 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -1038,8 +1038,8 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } let session: IChatSession; - if (sessionResource.path.startsWith('/untitled')) { - const newSessionOptions = this.getNewSessionOptionsForSessionType(resolvedType); + const newSessionOptions = this.getNewSessionOptionsForSessionType(resolvedType); + if (sessionResource.path.startsWith('/untitled') && newSessionOptions) { session = { sessionResource: sessionResource, onWillDispose: Event.None, @@ -1259,6 +1259,7 @@ export enum ChatSessionPosition { type NewChatSessionSendOptions = { readonly prompt: string; readonly attachedContext?: IChatRequestVariableEntry[]; + readonly initialSessionOptions?: ReadonlyArray<{ optionId: string; value: string | { id: string; name: string } }>; }; export type NewChatSessionOpenOptions = { @@ -1322,6 +1323,17 @@ async function openChatSession(accessor: ServicesAccessor, openOptions: NewChatS // Send initial prompt if provided if (chatSendOptions) { try { + // Set initial session options on the model before sending the request, + // so that the contributed session provider can read them. + if (chatSendOptions.initialSessionOptions) { + const model = chatService.getSession(resource); + if (model?.contributedChatSession) { + model.setContributedChatSession({ + ...model.contributedChatSession, + initialSessionOptions: chatSendOptions.initialSessionOptions, + }); + } + } await chatService.sendRequest(resource, chatSendOptions.prompt, { agentIdSilent: openOptions.type, attachedContext: chatSendOptions.attachedContext }); } catch (e) { logService.error(`Failed to send initial request to '${openOptions.type}' chat session with contextOptions: ${JSON.stringify(chatSendOptions)}`, e); diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index f6d6f20dc9e4c..0ccf0f81f3ab1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -6,7 +6,6 @@ import { timeout } from '../../../../base/common/async.js'; import { MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { URI } from '../../../../base/common/uri.js'; import * as nls from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -22,7 +21,6 @@ import { ACTION_ID_NEW_CHAT } from './actions/chatActions.js'; import { ChatSubmitAction, OpenModePickerAction, OpenModelPickerAction } from './actions/chatExecuteActions.js'; import { ConfigureToolsAction } from './actions/chatToolActions.js'; import { IAgentSessionsService } from './agentSessions/agentSessionsService.js'; -import { IChatWidgetService } from './chat.js'; import { CONFIGURE_INSTRUCTIONS_ACTION_ID } from './promptSyntax/attachInstructionsAction.js'; import { showConfigureHooksQuickPick } from './promptSyntax/hookActions.js'; import { CONFIGURE_PROMPTS_ACTION_ID } from './promptSyntax/runPromptAction.js'; @@ -38,7 +36,6 @@ export class ChatSlashCommandsContribution extends Disposable { @IChatSlashCommandService slashCommandService: IChatSlashCommandService, @ICommandService commandService: ICommandService, @IChatAgentService chatAgentService: IChatAgentService, - @IChatWidgetService chatWidgetService: IChatWidgetService, @IInstantiationService instantiationService: IInstantiationService, @IAgentSessionsService agentSessionsService: IAgentSessionsService, @IChatService chatService: IChatService, @@ -161,15 +158,19 @@ export class ChatSlashCommandsContribution extends Disposable { chatService.setChatSessionTitle(sessionResource, title); } })); - const enableAutoApprove = async (): Promise => { + const handleEnableAutoApprove = async () => { const inspection = configurationService.inspect(ChatConfiguration.GlobalAutoApprove); if (inspection.policyValue !== undefined) { if (inspection.policyValue === true) { - // Global auto-approve is already enabled by policy; nothing more to do. - return true; + notificationService.info(nls.localize('autoApprove.alreadyEnabled', "Global auto-approve is already enabled.")); + return; } - notificationService.warn(nls.localize('autoApprove.policyManaged', "Global auto-approve is managed by your organization policy. Contact your administrator to change this setting.")); - return false; + notificationService.warn(nls.localize('autoApprove.policyBlocked', "Global auto-approve is managed by your organization policy. Contact your administrator to change this setting.")); + return; + } + if (configurationService.getValue(ChatConfiguration.GlobalAutoApprove)) { + notificationService.info(nls.localize('autoApprove.alreadyEnabled', "Global auto-approve is already enabled.")); + return; } const alreadyOptedIn = storageService.getBoolean('chat.tools.global.autoApprove.optIn', StorageScope.APPLICATION, false); if (!alreadyOptedIn) { @@ -185,61 +186,62 @@ export class ChatSlashCommandsContribution extends Disposable { } }); if (result.result !== true) { - return false; + return; } storageService.store('chat.tools.global.autoApprove.optIn', true, StorageScope.APPLICATION, StorageTarget.USER); } await configurationService.updateValue(ChatConfiguration.GlobalAutoApprove, true); - return true; + notificationService.info(nls.localize('autoApprove.enabled', "Global auto-approve enabled — all tool calls will be approved automatically")); }; - const handleAutoApprove = async (prompt: string, _progress: unknown, _history: unknown, _location: unknown, sessionResource: URI) => { - const trimmed = prompt.trim(); - if (trimmed) { - // /autoApprove — prompt to enable, then submit the request - if (await enableAutoApprove()) { - const widget = chatWidgetService.getWidgetBySessionResource(sessionResource); - if (widget) { - widget.acceptInput(trimmed); - } - } else { - // Restore the prompt so the user doesn't lose their input - const widget = chatWidgetService.getWidgetBySessionResource(sessionResource); - if (widget) { - widget.setInput(trimmed); - } - } - } else { - // /autoApprove — toggle - const isEnabled = configurationService.getValue(ChatConfiguration.GlobalAutoApprove); - if (isEnabled) { - const inspection = configurationService.inspect(ChatConfiguration.GlobalAutoApprove); - if (inspection.policyValue !== undefined) { - notificationService.warn(nls.localize('autoApprove.policyManaged', "Global auto-approve is managed by your organization policy. Contact your administrator to change this setting.")); - return; - } - await configurationService.updateValue(ChatConfiguration.GlobalAutoApprove, false); - notificationService.info(nls.localize('autoApprove.disabled', "Global auto-approve disabled — tools will require approval")); - } else { - await enableAutoApprove(); + const handleDisableAutoApprove = async () => { + const inspection = configurationService.inspect(ChatConfiguration.GlobalAutoApprove); + if (inspection.policyValue !== undefined) { + if (inspection.policyValue === false) { + notificationService.info(nls.localize('autoApprove.alreadyDisabled', "Global auto-approve is already disabled.")); + return; } + notificationService.warn(nls.localize('autoApprove.policyBlocked', "Global auto-approve is managed by your organization policy. Contact your administrator to change this setting.")); + return; } + if (!configurationService.getValue(ChatConfiguration.GlobalAutoApprove)) { + notificationService.info(nls.localize('autoApprove.alreadyDisabled', "Global auto-approve is already disabled.")); + return; + } + await configurationService.updateValue(ChatConfiguration.GlobalAutoApprove, false); + notificationService.info(nls.localize('autoApprove.disabled', "Global auto-approve disabled — tools will require approval")); }; this._store.add(slashCommandService.registerSlashCommand({ command: 'autoApprove', - detail: nls.localize('autoApprove', "Toggle global auto-approval of all tool calls (alias: /yolo)"), + detail: nls.localize('autoApprove', "Enable global auto-approval of all tool calls"), sortText: 'z1_autoApprove', - executeImmediately: false, + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat] + }, handleEnableAutoApprove)); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'disableAutoApprove', + detail: nls.localize('disableAutoApprove', "Disable global auto-approval of all tool calls"), + sortText: 'z1_disableAutoApprove', + executeImmediately: true, silent: true, locations: [ChatAgentLocation.Chat] - }, handleAutoApprove)); + }, handleDisableAutoApprove)); this._store.add(slashCommandService.registerSlashCommand({ command: 'yolo', - detail: nls.localize('yolo', "Toggle global auto-approval of all tool calls (alias: /autoApprove)"), + detail: nls.localize('yolo', "Enable global auto-approval of all tool calls"), sortText: 'z1_yolo', - executeImmediately: false, + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat] + }, handleEnableAutoApprove)); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'disableYolo', + detail: nls.localize('disableYolo', "Disable global auto-approval of all tool calls"), + sortText: 'z1_disableYolo', + executeImmediately: true, silent: true, locations: [ChatAgentLocation.Chat] - }, handleAutoApprove)); + }, handleDisableAutoApprove)); this._store.add(slashCommandService.registerSlashCommand({ command: 'help', detail: '', diff --git a/src/vs/workbench/contrib/chat/browser/promptsDebugContribution.ts b/src/vs/workbench/contrib/chat/browser/promptsDebugContribution.ts index 9620797fe5efe..964f68d2ac022 100644 --- a/src/vs/workbench/contrib/chat/browser/promptsDebugContribution.ts +++ b/src/vs/workbench/contrib/chat/browser/promptsDebugContribution.ts @@ -7,7 +7,6 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IChatDebugResolvedEventContent, IChatDebugService } from '../common/chatDebugService.js'; -import { LocalChatSessionUri } from '../common/model/chatUri.js'; import { IPromptDiscoveryInfo, IPromptsService } from '../common/promptSyntax/service/promptsService.js'; /** @@ -53,7 +52,7 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo } chatDebugService.log( - LocalChatSessionUri.forSession(entry.sessionId), + entry.sessionResource, entry.name, entry.details, undefined, diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatCodeBlockPill.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatCodeBlockPill.css index c546f8192a2be..d459decaef8a6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatCodeBlockPill.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatCodeBlockPill.css @@ -4,14 +4,13 @@ *--------------------------------------------------------------------------------------------*/ .interactive-item-container.interactive-response .value .chat-markdown-part.rendered-markdown .code:has(.chat-codeblock-pill-container) { - margin-bottom: 8px; + margin-bottom: 16px; } .chat-markdown-part.rendered-markdown .code .chat-codeblock-pill-container { display: flex; align-items: center; gap: 5px; - margin: 0 0 6px 0px; font-size: var(--vscode-chat-font-size-body-s); color: var(--vscode-descriptionForeground); @@ -29,7 +28,7 @@ color: var(--vscode-icon-foreground) !important; &::before { - font-size: var(--vscode-chat-font-size-body-s); + font-size: 12px; } } @@ -39,6 +38,8 @@ .show-checkmarks & .codicon.codicon-check { display: inline-flex; + padding-left: 2px; + margin-left: 0.2em; } .status-label { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css index 179a2b06dc892..24d64e65bb7a6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css @@ -37,6 +37,9 @@ padding-top: 6px; } -.chat-used-context.chat-hook-outcome-warning .chat-used-context-label .monaco-button { - gap: 4px; +.chat-used-context.chat-hook-outcome-warning, +.chat-used-context.chat-hook-outcome-blocked { + .chat-used-context-label .monaco-button { + gap: 4px; + } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css index 1097c935dcb64..1702eaf25b327 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css @@ -132,18 +132,15 @@ margin: 0 0 2px 6px; } - .codicon:not(.chat-thinking-icon) { - display: none; + /* todo: ideally not !important, but the competing css has 14 specificity */ + .codicon:not(.chat-thinking-icon){ + display: none !important; } .chat-collapsible-hover-chevron.codicon { display: inline-flex; } - .chat-used-context.show-checkmarks .chat-used-context-label .monaco-icon-button > .codicon:first-child:not(.chat-collapsible-hover-chevron) { - display: inline-block; - } - .show-checkmarks .chat-confirmation-widget-title > .codicon:first-child:not(.chat-collapsible-hover-chevron) { display: inline-block; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 3238bb06704a9..02c2121487809 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -20,6 +20,7 @@ import { Iterable } from '../../../../../base/common/iterator.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, thenIfNotDisposed } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; +import { IsSessionsWindowContext } from '../../../../common/contextkeys.js'; import { filter } from '../../../../../base/common/objects.js'; import { autorun, derived, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; import { basename, extUri, isEqual } from '../../../../../base/common/resources.js'; @@ -188,7 +189,7 @@ const supportsAllAttachments: Required = { supportsPromptAttachments: true, }; -const DISCLAIMER = localize('chatDisclaimer', "AI responses may be inaccurate."); +const DISCLAIMER = localize('chatDisclaimer', "AI responses may be inaccurate"); export class ChatWidget extends Disposable implements IChatWidget { @@ -1543,8 +1544,9 @@ export class ChatWidget extends Disposable implements IChatWidget { } private async archiveLocalParentSession(sessionResource: URI): Promise { - if (sessionResource.scheme !== Schemas.vscodeLocalChatSession) { - this.logService.debug(`[Delegation] archiveLocalParentSession: skipping, scheme=${sessionResource.scheme} is not vscodeLocalChatSession`); + // In the regular workbench, only archive local chat sessions. + // In the sessions window, allow archiving any session type after delegation. + if (sessionResource.scheme !== Schemas.vscodeLocalChatSession && !IsSessionsWindowContext.getValue(this.contextKeyService)) { return; } @@ -2393,7 +2395,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.chatService.removePendingRequest(this.viewModel.sessionResource, editingRequestId); options.queue ??= editingPendingRequest; } else { - this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource); + this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource, 'acceptInput-editing'); options.queue = undefined; } @@ -2413,7 +2415,7 @@ export class ChatWidget extends Disposable implements IChatWidget { options.queue ??= ChatRequestQueueKind.Queued; } if (model.requestNeedsInput.get() && !model.getPendingRequests().length) { - this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource); + this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource, 'acceptInput-needsInput'); options.queue ??= ChatRequestQueueKind.Queued; } if (requestInProgress) { @@ -2826,8 +2828,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this.logService.debug(`ChatWidget#_autoAttachInstructions: prompt files are always enabled`); const enabledTools = this.input.currentModeKind === ChatModeKind.Agent ? this.input.selectedToolsModel.userSelectedTools.get() : undefined; const enabledSubAgents = this.input.currentModeKind === ChatModeKind.Agent ? this.input.currentModeObs.get().agents?.get() : undefined; - const sessionId = this._viewModel?.model.sessionId; - const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, this.input.currentModeKind, enabledTools, enabledSubAgents, sessionId); + const sessionResource = this._viewModel?.model.sessionResource; + const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, this.input.currentModeKind, enabledTools, enabledSubAgents, sessionResource); await computer.collect(attachedContext, CancellationToken.None); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts index 9e5218f2afbf1..f7d921a424561 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts @@ -7,9 +7,20 @@ import { IAction } from '../../../../../../base/common/actions.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { URI } from '../../../../../../base/common/uri.js'; import { localize } from '../../../../../../nls.js'; +import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; import { IActionWidgetDropdownAction } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { IsSessionsWindowContext } from '../../../../../common/contextkeys.js'; +import { IChatSessionsService } from '../../../common/chatSessionsService.js'; import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; import { AgentSessionProviders, getAgentCanContinueIn, getAgentSessionProvider, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; +import { ISessionTypePickerDelegate } from '../../chat.js'; +import { IChatInputPickerOptions } from './chatInputPickerActionItem.js'; import { ISessionTypeItem, SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; /** @@ -17,6 +28,26 @@ import { ISessionTypeItem, SessionTypePickerActionItem } from './sessionTargetPi * This picker allows switching to remote execution providers when the session is not empty. */ export class DelegationSessionPickerActionItem extends SessionTypePickerActionItem { + + private readonly _isSessionsWindow: boolean; + + constructor( + action: MenuItemAction, + chatSessionPosition: 'sidebar' | 'editor', + delegate: ISessionTypePickerDelegate, + pickerOptions: IChatInputPickerOptions, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, + @IChatSessionsService chatSessionsService: IChatSessionsService, + @ICommandService commandService: ICommandService, + @IOpenerService openerService: IOpenerService, + @ITelemetryService telemetryService: ITelemetryService, + ) { + super(action, chatSessionPosition, delegate, pickerOptions, actionWidgetService, keybindingService, contextKeyService, chatSessionsService, commandService, openerService, telemetryService); + this._isSessionsWindow = IsSessionsWindowContext.getValue(contextKeyService) === true; + } + protected override _run(sessionTypeItem: ISessionTypeItem): void { if (this.delegate.setPendingDelegationTarget) { this.delegate.setPendingDelegationTarget(sessionTypeItem.type); @@ -38,11 +69,17 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt const allContributions = this.chatSessionsService.getAllChatSessionContributions(); const contribution = allContributions.find(contribution => getAgentSessionProvider(contribution.type) === type); - if (this.delegate.getActiveSessionProvider() !== AgentSessionProviders.Local) { - return false; // Can only delegate when active session is local + // In core VS Code, only allow delegation from local sessions. + // In the sessions window, only allow delegation from background sessions (not cloud). + const activeProvider = this.delegate.getActiveSessionProvider(); + if (!this._isSessionsWindow && activeProvider !== AgentSessionProviders.Local) { + return false; + } + if (this._isSessionsWindow && activeProvider !== AgentSessionProviders.Background) { + return false; } - if (contribution && !contribution.canDelegate && this.delegate.getActiveSessionProvider() !== type /* Allow switching back to active type */) { + if (contribution && !contribution.canDelegate && activeProvider !== type /* Allow switching back to active type */) { return false; } @@ -50,6 +87,11 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt } protected override _isVisible(type: AgentSessionProviders): boolean { + // In the sessions window, only show Background and Cloud targets + if (this._isSessionsWindow && type === AgentSessionProviders.Local) { + return false; + } + if (this.delegate.getActiveSessionProvider() === type) { return true; // Always show active session type } @@ -79,6 +121,9 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt } protected override _getAdditionalActions(): IActionWidgetDropdownAction[] { + if (this._isSessionsWindow) { + return []; + } return [{ id: 'newChatSession', class: undefined, diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 35e910c2c37a7..16d1bd39b4df0 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -282,7 +282,11 @@ } .interactive-item-container > .value .chat-used-context { - margin-bottom: 10px; + margin-bottom: 16px; +} + +.interactive-item-container > .value .chat-used-context .chat-used-context-list { + margin-bottom: 0; } .interactive-item-container .value .rendered-markdown:not(:has(.chat-extensions-content-part)) { @@ -2313,6 +2317,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .show-checkmarks .progress-container > .codicon.codicon-check, .progress-container.show-checkmarks > .codicon.codicon-check { display: inline-flex; + margin-left: 4px; } .interactive-item-container .chat-command-button { diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css index 225d4422d4d8b..4c9933dc63793 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css @@ -53,11 +53,13 @@ div.chat-welcome-view { padding: 12px; & > .chat-welcome-view-icon { - min-height: 48px; + min-height: 40px; } & > .chat-welcome-view-icon .codicon { + color: var(--vscode-descriptionForeground); font-size: 40px; + margin-bottom: 24px; } & > .chat-welcome-view-icon.custom-icon { @@ -84,7 +86,9 @@ div.chat-welcome-view { } & > .chat-welcome-view-title { - font-size: 24px; + font-size: 13px; + font-weight: 600; + color: var(--vscode-foreground); margin-top: 5px; text-align: center; line-height: normal; @@ -94,10 +98,11 @@ div.chat-welcome-view { & > .chat-welcome-view-message { position: relative; text-align: center; - max-width: 100%; + max-width: 280px; padding: 0 20px; - margin: 0 auto; - color: var(--vscode-foreground); + margin: 8px auto 0; + color: var(--vscode-descriptionForeground); + font-size: 12px; a { color: var(--vscode-textLink-foreground); @@ -110,8 +115,7 @@ div.chat-welcome-view { } p { - margin-top: 8px; - margin-bottom: 8px; + margin: 0; } } @@ -153,11 +157,16 @@ div.chat-welcome-view { } & > .chat-welcome-view-disclaimer { - color: var(--vscode-foreground); + color: var(--vscode-input-placeholderForeground); text-align: center; - margin: -16px auto; - max-width: 400px; + margin: 0; + max-width: 256px; padding: 0 12px; + font-size: 12px; + + p { + margin: 0; + } a { color: var(--vscode-textLink-foreground); @@ -190,9 +199,7 @@ div.chat-welcome-view { position: absolute; top: 8px; left: 16px; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.5px; + font-size: 12px; color: var(--vscode-descriptionForeground); margin: 0; text-align: left; @@ -202,8 +209,8 @@ div.chat-welcome-view { display: flex; align-items: center; gap: 6px; - height: 24px; - padding: 0 8px; + height: 20px; + padding: 0 6px; border-radius: 4px; background-color: var(--vscode-editorWidget-background); cursor: pointer; @@ -214,13 +221,13 @@ div.chat-welcome-view { flex: 0 0 auto; & > .chat-welcome-view-suggested-prompt-title { - font-size: 13px; + font-size: 12px; color: var(--vscode-editorWidget-foreground); white-space: nowrap; } & > .chat-welcome-view-suggested-prompt-description { - font-size: 13px; + font-size: 11px; color: var(--vscode-descriptionForeground); overflow: hidden; text-overflow: ellipsis; diff --git a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts index 642afcefb4f5c..c404cb281ab1d 100644 --- a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts @@ -52,6 +52,12 @@ export interface IAICustomizationWorkspaceService { */ readonly visibleStorageSources: readonly PromptsStorage[]; + /** + * URI roots to exclude from user-level file listings. + * Files under these roots are hidden from the customization list. + */ + readonly excludedUserFileRoots: readonly URI[]; + /** * Whether the primary creation action should create a file directly */ diff --git a/src/vs/workbench/contrib/chat/common/chatDebugService.ts b/src/vs/workbench/contrib/chat/common/chatDebugService.ts index a4028bf9823b0..a50c09b18a111 100644 --- a/src/vs/workbench/contrib/chat/common/chatDebugService.ts +++ b/src/vs/workbench/contrib/chat/common/chatDebugService.ts @@ -48,6 +48,7 @@ export interface IChatDebugToolCallEvent extends IChatDebugEventCommon { export interface IChatDebugModelTurnEvent extends IChatDebugEventCommon { readonly kind: 'modelTurn'; readonly model?: string; + readonly requestName?: string; readonly inputTokens?: number; readonly outputTokens?: number; readonly totalTokens?: number; diff --git a/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts index f9787ed7978c7..cce684e6d5b81 100644 --- a/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts @@ -10,6 +10,7 @@ import { Disposable, IDisposable, toDisposable } from '../../../../base/common/l import { ResourceMap } from '../../../../base/common/map.js'; import { URI } from '../../../../base/common/uri.js'; import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugLogProvider, IChatDebugResolvedEventContent, IChatDebugService } from './chatDebugService.js'; +import { LocalChatSessionUri } from './model/chatUri.js'; export class ChatDebugServiceImpl extends Disposable implements IChatDebugService { declare readonly _serviceBrand: undefined; @@ -33,6 +34,9 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic activeSessionResource: URI | undefined; log(sessionResource: URI, name: string, details?: string, level: ChatDebugLogLevel = ChatDebugLogLevel.Info, options?: { id?: string; category?: string; parentEventId?: string }): void { + if (!LocalChatSessionUri.isLocalSession(sessionResource)) { + return; + } this.addEvent({ kind: 'generic', id: options?.id, @@ -118,6 +122,10 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic } async invokeProviders(sessionResource: URI): Promise { + if (!LocalChatSessionUri.isLocalSession(sessionResource)) { + return; + } + // Cancel only the previous invocation for THIS session, not others. // Each session has its own pipeline so events from multiple sessions // can be streamed concurrently. diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index bdfb80a30cd18..c2edad1d45c52 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -1409,7 +1409,7 @@ export interface IChatService { resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise; adoptRequest(sessionResource: URI, request: IChatRequestModel): Promise; removeRequest(sessionResource: URI, requestId: string): Promise; - cancelCurrentRequestForSession(sessionResource: URI, isInlineChat?: boolean): void; + cancelCurrentRequestForSession(sessionResource: URI, source?: string): void; /** * Sets yieldRequested on the active request for the given session. */ @@ -1485,7 +1485,7 @@ export interface IChatSessionStartOptions { export const ChatStopCancellationNoopEventName = 'chat.stopCancellationNoop'; export type ChatStopCancellationNoopEvent = { - source: 'cancelAction' | 'chatService'; + source: string; reason: 'noWidget' | 'noViewModel' | 'noPendingRequest' | 'requestAlreadyCanceled' | 'requestIdUnavailable'; requestInProgress: 'true' | 'false' | 'unknown'; pendingRequests: number; @@ -1504,7 +1504,7 @@ export const ChatPendingRequestChangeEventName = 'chat.pendingRequestChange'; export type ChatPendingRequestChangeEvent = { action: 'add' | 'remove' | 'notCancelable'; - source: 'sendRequest' | 'sendRequestComplete' | 'removeRequest' | 'cancelRequest' | 'adoptRequest' | 'remoteSession'; + source: string; }; export type ChatPendingRequestChangeClassification = { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index d92c02ee69576..57a993bb914d0 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -954,7 +954,7 @@ export class ChatService extends Disposable implements IChatService { let collectedHooks: IChatRequestHooks | undefined; let hasDisabledClaudeHooks = false; try { - const hooksInfo = await this.promptsService.getHooks(token, model.sessionId); + const hooksInfo = await this.promptsService.getHooks(token, model.sessionResource); if (hooksInfo) { collectedHooks = hooksInfo.hooks; hasDisabledClaudeHooks = hooksInfo.hasDisabledClaudeHooks; @@ -1434,28 +1434,26 @@ export class ChatService extends Disposable implements IChatService { request.response?.complete(); } - cancelCurrentRequestForSession(sessionResource: URI, isInlineChat?: boolean): void { + cancelCurrentRequestForSession(sessionResource: URI, source?: string): void { this.trace('cancelCurrentRequestForSession', `session: ${sessionResource}`); const pendingRequest = this._pendingRequests.get(sessionResource); if (!pendingRequest) { const model = this._sessionModels.get(sessionResource); const requestInProgress = model?.requestInProgress.get(); const pendingRequestsCount = model?.getPendingRequests().length ?? 0; - if (!isInlineChat) { - this.telemetryService.publicLog2(ChatStopCancellationNoopEventName, { - source: 'chatService', - reason: 'noPendingRequest', - requestInProgress: requestInProgress === undefined ? 'unknown' : requestInProgress ? 'true' : 'false', - pendingRequests: pendingRequestsCount, - }); - this.info('cancelCurrentRequestForSession', `No pending request was found for session ${sessionResource}. requestInProgress=${requestInProgress ?? 'unknown'}, pendingRequests=${pendingRequestsCount}`); - } + this.telemetryService.publicLog2(ChatStopCancellationNoopEventName, { + source: source ?? 'chatService', + reason: 'noPendingRequest', + requestInProgress: requestInProgress === undefined ? 'unknown' : requestInProgress ? 'true' : 'false', + pendingRequests: pendingRequestsCount, + }); + this.info('cancelCurrentRequestForSession', `No pending request was found for session ${sessionResource}. requestInProgress=${requestInProgress ?? 'unknown'}, pendingRequests=${pendingRequestsCount}`); return; } pendingRequest.cancel(); this._pendingRequests.deleteAndDispose(sessionResource); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'cancelRequest' }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: source ?? 'cancelRequest' }); } setYieldRequested(sessionResource: URI): void { diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index d7ce75b61bd31..aa9dcb530a70e 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -56,6 +56,7 @@ export enum ChatConfiguration { ExplainChangesEnabled = 'chat.editing.explainChanges.enabled', GrowthNotificationEnabled = 'chat.growthNotification.enabled', ChatCustomizationMenuEnabled = 'chat.customizationsMenu.enabled', + ChatCustomizationUserStoragePath = 'chat.customizationsMenu.userStoragePath', } /** diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts index 1b413761b4b09..c22a9c0166b51 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts @@ -31,6 +31,11 @@ export interface IAgentPluginSkill { readonly name: string; } +export interface IAgentPluginAgent { + readonly uri: URI; + readonly name: string; +} + export interface IAgentPluginMcpServerDefinition { readonly name: string; readonly configuration: IMcpServerConfiguration; @@ -43,6 +48,7 @@ export interface IAgentPlugin { readonly hooks: IObservable; readonly commands: IObservable; readonly skills: IObservable; + readonly agents: IObservable; readonly mcpServerDefinitions: IObservable; /** Set when the plugin was installed from a marketplace repository. */ readonly fromMarketplace?: IMarketplacePlugin; diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index 27d1dcc37e16c..a6107f388f4b5 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -8,6 +8,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { parse as parseJSONC } from '../../../../../base/common/json.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; +import { cloneAndChange } from '../../../../../base/common/objects.js'; import { autorun, derived, IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; import { posix, @@ -17,8 +18,10 @@ import { basename, extname, joinPath } from '../../../../../base/common/resources.js'; +import { escapeRegExpCharacters } from '../../../../../base/common/strings.js'; +import { Mutable } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; -import { IConfigurationService, ConfigurationTarget, getConfigValueInTarget } from '../../../../../platform/configuration/common/configuration.js'; +import { ConfigurationTarget, getConfigValueInTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; @@ -27,15 +30,12 @@ import { observableConfigValue } from '../../../../../platform/observable/common import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IPathService } from '../../../../services/path/common/pathService.js'; import { ChatConfiguration } from '../constants.js'; -import { parseCopilotHooks } from '../promptSyntax/hookCompatibility.js'; import { parseClaudeHooks } from '../promptSyntax/hookClaudeCompat.js'; -import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginCommand, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from './agentPluginService.js'; -import { escapeRegExpCharacters } from '../../../../../base/common/strings.js'; +import { parseCopilotHooks } from '../promptSyntax/hookCompatibility.js'; +import { IHookCommand } from '../promptSyntax/hookSchema.js'; +import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from './agentPluginService.js'; import { IPluginInstallService } from './pluginInstallService.js'; import { IMarketplacePlugin, IPluginMarketplaceService } from './pluginMarketplaceService.js'; -import { Mutable } from '../../../../../base/common/types.js'; -import { IHookCommand } from '../promptSyntax/hookSchema.js'; -import { cloneAndChange } from '../../../../../base/common/objects.js'; const COMMAND_FILE_SUFFIX = '.md'; @@ -444,12 +444,14 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent const store = new DisposableStore(); const commands = observableValue('agentPluginCommands', []); const skills = observableValue('agentPluginSkills', []); + const agents = observableValue('agentPluginAgents', []); const hooks = observableValue('agentPluginHooks', []); const mcpServerDefinitions = observableValue('agentPluginMcpServerDefinitions', []); const enabled = observableValue('agentPluginEnabled', initialEnabled); const commandsDir = joinPath(uri, 'commands'); const skillsDir = joinPath(uri, 'skills'); + const agentsDir = joinPath(uri, 'agents'); const commandsScheduler = store.add(new RunOnceScheduler(async () => { commands.set(await this._readCommands(uri), undefined); @@ -457,6 +459,9 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent const skillsScheduler = store.add(new RunOnceScheduler(async () => { skills.set(await this._readSkills(uri), undefined); }, 200)); + const agentsScheduler = store.add(new RunOnceScheduler(async () => { + agents.set(await this._readAgents(uri), undefined); + }, 200)); const hooksScheduler = store.add(new RunOnceScheduler(async () => { hooks.set(await this._readHooks(uri, adapter), undefined); }, 200)); @@ -472,6 +477,9 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent if (e.affects(skillsDir)) { skillsScheduler.schedule(); } + if (e.affects(agentsDir)) { + agentsScheduler.schedule(); + } if (adapter.hookWatchPaths.some(path => e.affects(joinPath(uri, path)))) { hooksScheduler.schedule(); } @@ -483,6 +491,7 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent commandsScheduler.schedule(); skillsScheduler.schedule(); + agentsScheduler.schedule(); hooksScheduler.schedule(); mcpScheduler.schedule(); @@ -495,6 +504,7 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent hooks, commands, skills, + agents, mcpServerDefinitions, fromMarketplace, }; @@ -695,6 +705,37 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent return skills; } + private async _readAgents(uri: URI): Promise { + const agentsDir = joinPath(uri, 'agents'); + let stat; + try { + stat = await this._fileService.resolve(agentsDir); + } catch { + return []; + } + + if (!stat.isDirectory || !stat.children) { + return []; + } + + const agents: IAgentPluginAgent[] = []; + for (const child of stat.children) { + if (!child.isFile || extname(child.resource).toLowerCase() !== COMMAND_FILE_SUFFIX) { + continue; + } + + const name = basename(child.resource).slice(0, -COMMAND_FILE_SUFFIX.length); + + agents.push({ + uri: child.resource, + name, + }); + } + + agents.sort((a, b) => a.name.localeCompare(b.name)); + return agents; + } + private async _readCommands(uri: URI): Promise { const commandsDir = joinPath(uri, 'commands'); let stat; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 62b01c294e54c..16b5459a50c81 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -63,7 +63,7 @@ export class ComputeAutomaticInstructions { private readonly _modeKind: ChatModeKind, private readonly _enabledTools: UserSelectedTools | undefined, private readonly _enabledSubagents: (readonly string[]) | undefined, - private readonly _sessionId: string | undefined, + private readonly _sessionResource: URI | undefined, @IPromptsService private readonly _promptsService: IPromptsService, @ILogService public readonly _logService: ILogService, @ILabelService private readonly _labelService: ILabelService, @@ -93,7 +93,7 @@ export class ComputeAutomaticInstructions { public async collect(variables: ChatRequestVariableSet, token: CancellationToken): Promise { - const instructionFiles = await this._promptsService.getInstructionFiles(token, this._sessionId); + const instructionFiles = await this._promptsService.getInstructionFiles(token, this._sessionResource); this._logService.trace(`[InstructionsContextComputer] ${instructionFiles.length} instruction files available.`); @@ -354,7 +354,7 @@ export class ComputeAutomaticInstructions { entries.push('', '', ''); // add trailing newline } - const agentSkills = await this._promptsService.findAgentSkills(token, this._sessionId); + const agentSkills = await this._promptsService.findAgentSkills(token, this._sessionResource); // Filter out skills with disableModelInvocation=true (they can only be triggered manually via /name) const modelInvocableSkills = agentSkills?.filter(skill => !skill.disableModelInvocation); if (modelInvocableSkills && modelInvocableSkills.length > 0) { @@ -400,7 +400,7 @@ export class ComputeAutomaticInstructions { return (agent: ICustomAgent) => subagents.includes(agent.name); } })(); - const agents = await this._promptsService.getCustomAgents(token, this._sessionId); + const agents = await this._promptsService.getCustomAgents(token, this._sessionResource); if (agents.length > 0) { entries.push(''); entries.push('Here is a list of agents that can be used when running a subagent.'); 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 a2ca1942cf04f..73f48099a1396 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -22,7 +22,7 @@ import { IResolvedPromptSourceFolder } from '../config/promptFileLocations.js'; * A debug bridge (e.g. contribution) can listen and forward these to IChatDebugService. */ export interface IPromptDiscoveryLogEntry { - readonly sessionId: string; + readonly sessionResource: URI; readonly name: string; readonly details?: string; readonly category?: string; @@ -410,9 +410,9 @@ export interface IPromptsService extends IDisposable { /** * Returns a prompt command if the command name is valid. - * @param sessionId Optional session ID to scope debug logging to a specific session. + * @param sessionResource Optional session resource to scope debug logging to a specific session. */ - getPromptSlashCommands(token: CancellationToken, sessionId?: string): Promise; + getPromptSlashCommands(token: CancellationToken, sessionResource?: URI): Promise; /** * Returns the prompt command name for the given URI. @@ -426,9 +426,9 @@ export interface IPromptsService extends IDisposable { /** * Finds all available custom agents - * @param sessionId Optional session ID to scope debug logging to a specific session. + * @param sessionResource Optional session resource to scope debug logging to a specific session. */ - getCustomAgents(token: CancellationToken, sessionId?: string): Promise; + getCustomAgents(token: CancellationToken, sessionResource?: URI): Promise; /** * Parses the provided URI @@ -486,30 +486,30 @@ export interface IPromptsService extends IDisposable { /** * Gets list of agent skills files. - * @param sessionId Optional session ID to scope debug logging to a specific session. + * @param sessionResource Optional session resource to scope debug logging to a specific session. */ - findAgentSkills(token: CancellationToken, sessionId?: string): Promise; + findAgentSkills(token: CancellationToken, sessionResource?: URI): Promise; /** * Gets detailed discovery information for a prompt type. * This includes all files found and their load/skip status with reasons. * Used for diagnostics and config-info displays. - * @param sessionId Optional session ID to scope debug logging to a specific session. + * @param sessionResource Optional session resource to scope debug logging to a specific session. */ - getPromptDiscoveryInfo(type: PromptsType, token: CancellationToken, sessionId?: string): Promise; + getPromptDiscoveryInfo(type: PromptsType, token: CancellationToken, sessionResource?: URI): Promise; /** * Gets all hooks collected from hooks.json files. * The result is cached and invalidated when hook files change. - * @param sessionId Optional session ID to scope debug logging to a specific session. + * @param sessionResource Optional session resource to scope debug logging to a specific session. */ - getHooks(token: CancellationToken, sessionId?: string): Promise; + getHooks(token: CancellationToken, sessionResource?: URI): Promise; /** * Gets all instruction files, logging discovery info to the debug log. - * @param sessionId Optional session ID to scope debug logging to a specific session. + * @param sessionResource Optional session resource to scope debug logging to a specific session. */ - getInstructionFiles(token: CancellationToken, sessionId?: string): Promise; + getInstructionFiles(token: CancellationToken, sessionResource?: URI): Promise; /** * Fired when a discovery-related log entry is produced. 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 38866f18fb46f..15e82a428608e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -231,6 +231,10 @@ export class PromptsService extends Disposable implements IPromptsService { PromptsType.skill, (plugin, reader) => plugin.skills.read(reader), )); + this._register(this.watchPluginPromptFilesForType( + PromptsType.agent, + (plugin, reader) => plugin.agents.read(reader), + )); this._register(autorun(reader => { const plugins = this.agentPluginService.plugins.read(reader); @@ -570,7 +574,7 @@ export class PromptsService extends Disposable implements IPromptsService { return this.cachedSlashCommands.onDidChange; } - public async getPromptSlashCommands(token: CancellationToken, sessionId?: string): Promise { + public async getPromptSlashCommands(token: CancellationToken, sessionResource?: URI): Promise { return await this.cachedSlashCommands.get(token); } @@ -644,17 +648,17 @@ export class PromptsService extends Disposable implements IPromptsService { return this.cachedCustomAgents.onDidChange; } - public async getCustomAgents(token: CancellationToken, sessionId?: string): Promise { + public async getCustomAgents(token: CancellationToken, sessionResource?: URI): Promise { const sw = StopWatch.create(); const result = await this.cachedCustomAgents.get(token); - if (sessionId) { + if (sessionResource) { const elapsed = sw.elapsed(); const discoveryInfo = await this.getAgentDiscoveryInfo(token); const details = result.length === 1 ? localize("promptsService.resolvedAgent", "Resolved {0} agent in {1}ms", result.length, elapsed.toFixed(1)) : localize("promptsService.resolvedAgents", "Resolved {0} agents in {1}ms", result.length, elapsed.toFixed(1)); this._onDidLogDiscovery.fire({ - sessionId, + sessionResource, name: localize("promptsService.loadAgents", "Load Agents"), details, discoveryInfo, @@ -1038,7 +1042,7 @@ export class PromptsService extends Disposable implements IPromptsService { return this.cachedSkills.onDidChange; } - public async findAgentSkills(token: CancellationToken, sessionId?: string): Promise { + public async findAgentSkills(token: CancellationToken, sessionResource?: URI): Promise { const useAgentSkills = this.configurationService.getValue(PromptsConfig.USE_AGENT_SKILLS); if (!useAgentSkills) { return undefined; @@ -1046,14 +1050,14 @@ export class PromptsService extends Disposable implements IPromptsService { const sw = StopWatch.create(); const result = await this.cachedSkills.get(token); - if (sessionId) { + if (sessionResource) { const elapsed = sw.elapsed(); const discoveryInfo = await this.getSkillDiscoveryInfo(token); const details = result.length === 1 ? localize("promptsService.resolvedSkill", "Resolved {0} skill in {1}ms", result.length, elapsed.toFixed(1)) : localize("promptsService.resolvedSkills", "Resolved {0} skills in {1}ms", result.length, elapsed.toFixed(1)); this._onDidLogDiscovery.fire({ - sessionId, + sessionResource, name: localize("promptsService.loadSkills", "Load Skills"), details, discoveryInfo, @@ -1166,10 +1170,10 @@ export class PromptsService extends Disposable implements IPromptsService { return result; } - public async getHooks(token: CancellationToken, sessionId?: string): Promise { + public async getHooks(token: CancellationToken, sessionResource?: URI): Promise { const sw = StopWatch.create(); const result = await this.cachedHooks.get(token); - if (sessionId) { + if (sessionResource) { const elapsed = sw.elapsed(); const hookCount = result ? Object.values(result.hooks).reduce((sum, arr) => sum + arr.length, 0) : 0; const discoveryInfo = await this.getHookDiscoveryInfo(token); @@ -1177,7 +1181,7 @@ export class PromptsService extends Disposable implements IPromptsService { ? localize("promptsService.resolvedHook", "Resolved {0} hook in {1}ms", hookCount, elapsed.toFixed(1)) : localize("promptsService.resolvedHooks", "Resolved {0} hooks in {1}ms", hookCount, elapsed.toFixed(1)); this._onDidLogDiscovery.fire({ - sessionId, + sessionResource, name: localize("promptsService.loadHooks", "Load Hooks"), details, discoveryInfo, @@ -1187,17 +1191,17 @@ export class PromptsService extends Disposable implements IPromptsService { return result; } - public async getInstructionFiles(token: CancellationToken, sessionId?: string): Promise { + public async getInstructionFiles(token: CancellationToken, sessionResource?: URI): Promise { const sw = StopWatch.create(); const result = await this.listPromptFiles(PromptsType.instructions, token); - if (sessionId) { + if (sessionResource) { const elapsed = sw.elapsed(); const discoveryInfo = await this.getInstructionsDiscoveryInfo(token); const details = result.length === 1 ? localize("promptsService.resolvedInstruction", "Resolved {0} instruction in {1}ms", result.length, elapsed.toFixed(1)) : localize("promptsService.resolvedInstructions", "Resolved {0} instructions in {1}ms", result.length, elapsed.toFixed(1)); this._onDidLogDiscovery.fire({ - sessionId, + sessionResource, name: localize("promptsService.loadInstructions", "Load Instructions"), details, discoveryInfo, @@ -1300,10 +1304,10 @@ export class PromptsService extends Disposable implements IPromptsService { return { hooks: result, hasDisabledClaudeHooks }; } - public async getPromptDiscoveryInfo(type: PromptsType, token: CancellationToken, sessionId?: string): Promise { - if (sessionId) { + public async getPromptDiscoveryInfo(type: PromptsType, token: CancellationToken, sessionResource?: URI): Promise { + if (sessionResource) { this._onDidLogDiscovery.fire({ - sessionId, + sessionResource, name: localize("promptsService.discoveryStart", "Discovery {0} (Start)", type), category: 'discovery', }); @@ -1334,7 +1338,7 @@ export class PromptsService extends Disposable implements IPromptsService { result = { ...result, sourceFolders }; } - if (sessionId) { + if (sessionResource) { const details = localize( "promptsService.discoveryResult", "{0} loaded, {1} skipped", @@ -1342,7 +1346,7 @@ export class PromptsService extends Disposable implements IPromptsService { skippedCount, ); this._onDidLogDiscovery.fire({ - sessionId, + sessionResource, name: localize("promptsService.discoveryEnd", "Discovery {0} (End)", type), details, discoveryInfo: result, diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index fe31a470b1d6e..bf441cf3d5e49 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -20,7 +20,6 @@ import { IChatProgress, IChatService } from '../../chatService/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../constants.js'; import { ILanguageModelsService } from '../../languageModels.js'; import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js'; -import { chatSessionResourceToId } from '../../model/chatUri.js'; import { IChatAgentRequest, IChatAgentService } from '../../participants/chatAgents.js'; import { ComputeAutomaticInstructions } from '../../promptSyntax/computeAutomaticInstructions.js'; import { IChatRequestHooks } from '../../promptSyntax/hookSchema.js'; @@ -245,14 +244,13 @@ export class RunSubagentTool extends Disposable implements IToolImpl { } const variableSet = new ChatRequestVariableSet(); - const sessionId = chatSessionResourceToId(invocation.context.sessionResource); - const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, ChatModeKind.Agent, modeTools, undefined, sessionId); // agents can not call subagents + const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, ChatModeKind.Agent, modeTools, undefined, invocation.context.sessionResource); // agents can not call subagents await computer.collect(variableSet, token); // Collect hooks from hook .json files let collectedHooks: IChatRequestHooks | undefined; try { - const info = await this.promptsService.getHooks(token, chatSessionResourceToId(invocation.context.sessionResource)); + const info = await this.promptsService.getHooks(token, invocation.context.sessionResource); collectedHooks = info?.hooks; } catch (error) { this.logService.warn('[ChatService] Failed to collect hooks:', error); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts index 8113041814474..b15974182e42f 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts @@ -150,7 +150,7 @@ class MockChatService implements IChatService { throw new Error('Method not implemented.'); } - cancelCurrentRequestForSession(_sessionResource: URI): void { } + cancelCurrentRequestForSession(_sessionResource: URI, _source?: string): void { } setYieldRequested(_sessionResource: URI): void { } diff --git a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/customizationCreatorService.test.ts b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/customizationCreatorService.test.ts new file mode 100644 index 0000000000000..5e27539c76710 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/customizationCreatorService.test.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; +import { IPromptsService, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; +import { resolveUserTargetDirectory } from '../../../browser/aiCustomization/customizationCreatorService.js'; +import { ChatConfiguration } from '../../../common/constants.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IPathService } from '../../../../../services/path/common/pathService.js'; + +suite('customizationCreatorService', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const userHome = URI.file('/home/user'); + + function createMockPathService(): Pick { + return { + userHome: ((options?: { preferLocal: boolean }) => { + if (options?.preferLocal) { + return userHome; + } + return Promise.resolve(userHome); + }) as IPathService['userHome'], + }; + } + + function createMockConfigService(overridePath: string): Pick { + return { + getValue: (key: string) => { + if (key === ChatConfiguration.ChatCustomizationUserStoragePath) { + return overridePath; + } + return undefined; + }, + } as Pick; + } + + function createMockPromptsService(userFolderUri?: URI): Pick { + return { + getSourceFolders: () => Promise.resolve( + userFolderUri + ? [{ uri: userFolderUri, storage: PromptsStorage.user, type: PromptsType.instructions }] + : [] + ), + } as Pick; + } + + suite('resolveUserTargetDirectory', () => { + + test('with override path and tilde for instructions', async () => { + const result = await resolveUserTargetDirectory( + createMockPromptsService() as IPromptsService, + PromptsType.instructions, + createMockConfigService('~/.copilot') as IConfigurationService, + createMockPathService() as IPathService, + ); + assert.strictEqual(result?.path, '/home/user/.copilot/instructions'); + }); + + test('with override path and tilde for skills', async () => { + const result = await resolveUserTargetDirectory( + createMockPromptsService() as IPromptsService, + PromptsType.skill, + createMockConfigService('~/.copilot') as IConfigurationService, + createMockPathService() as IPathService, + ); + assert.strictEqual(result?.path, '/home/user/.copilot/skills'); + }); + + test('override path is ignored for prompts (no CLI discovery path)', async () => { + const fallbackUri = URI.file('/home/user/.vscode/prompts'); + const result = await resolveUserTargetDirectory( + createMockPromptsService(fallbackUri) as IPromptsService, + PromptsType.prompt, + createMockConfigService('~/.copilot') as IConfigurationService, + createMockPathService() as IPathService, + ); + // Should fall through to getSourceFolders, not use the override + assert.strictEqual(result?.path, '/home/user/.vscode/prompts'); + }); + + test('override path is ignored for agents (no CLI convention)', async () => { + const fallbackUri = URI.file('/home/user/.vscode/prompts'); + const result = await resolveUserTargetDirectory( + createMockPromptsService(fallbackUri) as IPromptsService, + PromptsType.agent, + createMockConfigService('~/.copilot') as IConfigurationService, + createMockPathService() as IPathService, + ); + // Should fall through to getSourceFolders, not use the override + assert.strictEqual(result?.path, '/home/user/.vscode/prompts'); + }); + + test('override path is ignored for hooks (no CLI convention)', async () => { + const result = await resolveUserTargetDirectory( + createMockPromptsService() as IPromptsService, + PromptsType.hook, + createMockConfigService('~/.copilot') as IConfigurationService, + createMockPathService() as IPathService, + ); + // No user folder for hooks, should return undefined + assert.strictEqual(result, undefined); + }); + + test('falls back to getSourceFolders when no override is set', async () => { + const fallbackUri = URI.file('/home/user/.vscode/prompts'); + const result = await resolveUserTargetDirectory( + createMockPromptsService(fallbackUri) as IPromptsService, + PromptsType.instructions, + createMockConfigService('') as IConfigurationService, + createMockPathService() as IPathService, + ); + assert.strictEqual(result?.path, '/home/user/.vscode/prompts'); + }); + + test('falls back to getSourceFolders when no config service provided', async () => { + const fallbackUri = URI.file('/home/user/.vscode/prompts'); + const result = await resolveUserTargetDirectory( + createMockPromptsService(fallbackUri) as IPromptsService, + PromptsType.instructions, + ); + assert.strictEqual(result?.path, '/home/user/.vscode/prompts'); + }); + + test('with absolute override path (no tilde)', async () => { + const result = await resolveUserTargetDirectory( + createMockPromptsService() as IPromptsService, + PromptsType.instructions, + createMockConfigService('/custom/path') as IConfigurationService, + createMockPathService() as IPathService, + ); + assert.strictEqual(result?.path, '/custom/path/instructions'); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptsDebugContribution.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptsDebugContribution.test.ts index 2233f9cd99294..f5045a72ed8d1 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptsDebugContribution.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptsDebugContribution.test.ts @@ -10,6 +10,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/tes import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugGenericEvent, IChatDebugService } from '../../common/chatDebugService.js'; import { ChatDebugServiceImpl } from '../../common/chatDebugServiceImpl.js'; +import { LocalChatSessionUri } from '../../common/model/chatUri.js'; import { PromptsDebugContribution } from '../../browser/promptsDebugContribution.js'; import { IPromptDiscoveryLogEntry, IPromptDiscoveryInfo, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; @@ -38,7 +39,7 @@ suite('PromptsDebugContribution', () => { disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e))); promptsOnDidLogDiscovery.fire({ - sessionId: 'session-1', + sessionResource: LocalChatSessionUri.forSession('session-1'), name: 'Load Instructions', details: 'Resolved 3 instructions in 12.5ms', category: 'discovery', @@ -76,7 +77,7 @@ suite('PromptsDebugContribution', () => { }; promptsOnDidLogDiscovery.fire({ - sessionId: 'session-1', + sessionResource: LocalChatSessionUri.forSession('session-1'), name: 'Discovery End', details: '1 loaded, 0 skipped', category: 'discovery', @@ -114,7 +115,7 @@ suite('PromptsDebugContribution', () => { disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e))); promptsOnDidLogDiscovery.fire({ - sessionId: 'session-1', + sessionResource: LocalChatSessionUri.forSession('session-1'), name: 'Discovery Start', category: 'discovery', }); @@ -149,7 +150,7 @@ suite('PromptsDebugContribution', () => { }; promptsOnDidLogDiscovery.fire({ - sessionId: 'session-1', + sessionResource: LocalChatSessionUri.forSession('session-1'), name: 'Discovery End', discoveryInfo, }); @@ -172,7 +173,7 @@ suite('PromptsDebugContribution', () => { disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e))); promptsOnDidLogDiscovery.fire({ - sessionId: 'session-1', + sessionResource: LocalChatSessionUri.forSession('session-1'), name: 'Test', }); diff --git a/src/vs/workbench/contrib/chat/test/common/chatDebugServiceImpl.test.ts b/src/vs/workbench/contrib/chat/test/common/chatDebugServiceImpl.test.ts index a0df375b4c3d8..5a247eaea99d8 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatDebugServiceImpl.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatDebugServiceImpl.test.ts @@ -9,6 +9,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugGenericEvent, IChatDebugLogProvider, IChatDebugModelTurnEvent, IChatDebugResolvedEventContent, IChatDebugToolCallEvent } from '../../common/chatDebugService.js'; import { ChatDebugServiceImpl } from '../../common/chatDebugServiceImpl.js'; +import { LocalChatSessionUri } from '../../common/model/chatUri.js'; suite('ChatDebugServiceImpl', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -17,9 +18,10 @@ suite('ChatDebugServiceImpl', () => { const session1 = URI.parse('vscode-chat-session://local/session-1'); const session2 = URI.parse('vscode-chat-session://local/session-2'); - const sessionA = URI.parse('vscode-chat-session://local/a'); - const sessionB = URI.parse('vscode-chat-session://local/b'); + const sessionA = LocalChatSessionUri.forSession('a'); + const sessionB = LocalChatSessionUri.forSession('b'); const sessionGeneric = URI.parse('vscode-chat-session://local/session'); + const nonLocalSession = URI.parse('vscode-chat-session://remote-provider/session-1'); setup(() => { service = disposables.add(new ChatDebugServiceImpl()); @@ -145,6 +147,16 @@ suite('ChatDebugServiceImpl', () => { assert.strictEqual(event.category, 'testing'); assert.strictEqual(event.parentEventId, 'parent-1'); }); + + test('should not log events for non-local sessions', () => { + const firedEvents: IChatDebugEvent[] = []; + disposables.add(service.onDidAddEvent(e => firedEvents.push(e))); + + service.log(nonLocalSession, 'should-be-skipped', 'details'); + + assert.strictEqual(firedEvents.length, 0); + assert.strictEqual(service.getEvents(nonLocalSession).length, 0); + }); }); suite('getSessionResources', () => { @@ -319,6 +331,29 @@ suite('ChatDebugServiceImpl', () => { assert.strictEqual(tokenA.isCancellationRequested, false, 'session-a token should not be cancelled'); }); + test('should not invoke providers for non-local sessions', async () => { + let providerCalled = false; + + const provider: IChatDebugLogProvider = { + provideChatDebugLog: async () => { + providerCalled = true; + return [{ + kind: 'generic', + sessionResource: nonLocalSession, + created: new Date(), + name: 'should-not-appear', + level: ChatDebugLogLevel.Info, + }]; + }, + }; + + disposables.add(service.registerProvider(provider)); + await service.invokeProviders(nonLocalSession); + + assert.strictEqual(providerCalled, false); + assert.strictEqual(service.getEvents(nonLocalSession).length, 0); + }); + test('newly registered provider should be invoked for active sessions', async () => { // Start an invocation before the provider is registered const firstProvider: IChatDebugLogProvider = { diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts index df44c9f3fd461..54579be64cb42 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -89,7 +89,7 @@ export class MockChatService implements IChatService { removeRequest(sessionResource: URI, requestId: string): Promise { throw new Error('Method not implemented.'); } - cancelCurrentRequestForSession(sessionResource: URI): void { + cancelCurrentRequestForSession(sessionResource: URI, source?: string): void { throw new Error('Method not implemented.'); } setYieldRequested(sessionResource: URI): void { 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 d222b92eb6817..e01bc0089f014 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 @@ -31,7 +31,7 @@ export class MockPromptsService implements IPromptsService { this._onDidChangeCustomChatModes.fire(); } - async getCustomAgents(token: CancellationToken, sessionId?: string): Promise { + async getCustomAgents(token: CancellationToken, sessionResource?: URI): Promise { return this._customModes; } @@ -50,7 +50,7 @@ export class MockPromptsService implements IPromptsService { resolvePromptSlashCommand(command: string, _token: CancellationToken): Promise { throw new Error('Not implemented'); } get onDidChangeSlashCommands(): Event { throw new Error('Not implemented'); } // eslint-disable-next-line @typescript-eslint/no-explicit-any - getPromptSlashCommands(_token: CancellationToken, _sessionId?: string): Promise { throw new Error('Not implemented'); } + getPromptSlashCommands(_token: CancellationToken, _sessionResource?: URI): Promise { throw new Error('Not implemented'); } getPromptSlashCommandName(uri: URI, _token: CancellationToken): Promise { throw new Error('Not implemented'); } // eslint-disable-next-line @typescript-eslint/no-explicit-any parse(_uri: URI, _type: any, _token: CancellationToken): Promise { throw new Error('Not implemented'); } @@ -65,11 +65,11 @@ export class MockPromptsService implements IPromptsService { getDisabledPromptFiles(type: PromptsType): ResourceSet { throw new Error('Method not implemented.'); } setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void { throw new Error('Method not implemented.'); } registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } - findAgentSkills(token: CancellationToken, sessionId?: string): Promise { throw new Error('Method not implemented.'); } + findAgentSkills(token: CancellationToken, sessionResource?: URI): Promise { throw new Error('Method not implemented.'); } // eslint-disable-next-line @typescript-eslint/no-explicit-any - getPromptDiscoveryInfo(_type: any, _token: CancellationToken, _sessionId?: string): Promise { throw new Error('Method not implemented.'); } + getPromptDiscoveryInfo(_type: any, _token: CancellationToken, _sessionResource?: URI): Promise { throw new Error('Method not implemented.'); } // eslint-disable-next-line @typescript-eslint/no-explicit-any - getHooks(_token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - getInstructionFiles(_token: CancellationToken, _sessionId?: string): Promise { throw new Error('Method not implemented.'); } + getHooks(_token: CancellationToken, _sessionResource?: URI): Promise { throw new Error('Method not implemented.'); } + getInstructionFiles(_token: CancellationToken, _sessionResource?: URI): Promise { throw new Error('Method not implemented.'); } dispose(): void { } } 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 5a784486c3200..dc2fb6d67b9c7 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 @@ -53,7 +53,7 @@ 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'; -import { IAgentPlugin, IAgentPluginCommand, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from '../../../../common/plugins/agentPluginService.js'; +import { IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from '../../../../common/plugins/agentPluginService.js'; suite('PromptsService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -3444,6 +3444,7 @@ suite('PromptsService', () => { const hooks = observableValue('testPluginHooks', initialHooks); const commands = observableValue('testPluginCommands', []); const skills = observableValue('testPluginSkills', []); + const agents = observableValue('testPluginAgents', []); const mcpServerDefinitions = observableValue('testPluginMcpServerDefinitions', []); return { @@ -3454,6 +3455,7 @@ suite('PromptsService', () => { hooks, commands, skills, + agents, mcpServerDefinitions, }, hooks, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index c13f86866fa9f..ad180ba141c7b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -604,7 +604,7 @@ export class InlineChatController implements IEditorContribution { if (!session) { return; } - this._chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource); + this._chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject'); await session.editingSession.reject(); session.dispose(); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 97d99bf403bda..4a008a59b0f9d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -86,7 +86,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const store = new DisposableStore(); store.add(toDisposable(() => { - this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource, true); + this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession'); chatModel.editingSession?.reject(); this._sessions.delete(uri); this._onDidChangeSessions.fire(this); diff --git a/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts b/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts index e759a24cec497..7a5e0d28c0529 100644 --- a/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts +++ b/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts @@ -26,7 +26,7 @@ suite('McpStdioStateHandler', () => { processId: new Promise((resolve) => { child.on('spawn', () => resolve(child.pid!)); }), - output: new Promise((resolve) => { + output: new Promise((resolve, reject) => { let output = ''; child.stderr.setEncoding('utf-8').on('data', (data) => { output += data.toString(); @@ -34,6 +34,7 @@ suite('McpStdioStateHandler', () => { child.stdout.setEncoding('utf-8').on('data', (data) => { output += data.toString(); }); + child.on('error', reject); child.on('close', () => resolve(output)); }), }; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 606acf234a1b4..2be6c5d9e96c5 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -412,7 +412,7 @@ export class TerminalChatWidget extends Disposable { if (!model?.sessionResource) { return; } - this._chatService.cancelCurrentRequestForSession(model?.sessionResource, true); + this._chatService.cancelCurrentRequestForSession(model?.sessionResource, 'terminalChat'); } async viewInChat(): Promise {