mirror of
https://github.com/microsoft/vscode.git
synced 2026-10-06 16:39:08 +01:00
Merge branch 'main' into agents/issue-reporter-shared-anonymous
This commit is contained in:
429 files changed
+16272
-2192
No files matched your search
@@ -1,6 +1,6 @@
|
||||
disturl="https://electronjs.org/headers"
|
||||
target="43.7.5"
|
||||
ms_build_id="15523606"
|
||||
target="43.7.7"
|
||||
ms_build_id="15553055"
|
||||
runtime="electron"
|
||||
ignore-scripts=false
|
||||
build_from_source="true"
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
7ca9d9e9654f66e63d3117b868f5407605af53ca6115bcf9f0490a68568afc14 *chromedriver-v43.7.5-darwin-arm64.zip
|
||||
0133b3e40b192e9ff3fa1e80773a25e591b8638a95aa50e33b773a6222c12493 *chromedriver-v43.7.5-darwin-x64.zip
|
||||
f85f88fe6cf1be1e55312c1302d1f3d7d0f319dfe4e7e610f9fbf3f0b682018f *chromedriver-v43.7.5-linux-arm64.zip
|
||||
97f4939ba6d58954673cbda3c14ff5c0b00e5a4b7e70a1b166d0313da91a8a5f *chromedriver-v43.7.5-linux-armv7l.zip
|
||||
854d5396d3abfb91284a9e66e6cb962a7e274aad28cd86ff6d5c22492bfc06d7 *chromedriver-v43.7.5-linux-x64.zip
|
||||
fd1cc7a02fc96fabb01482a87807d9e6dd7fd70df5b97be5e5704422d751d063 *chromedriver-v43.7.5-mas-arm64.zip
|
||||
713387926080fab188c99674548653a144f4e3a0890924f60b81e99b2fff33b7 *chromedriver-v43.7.5-mas-x64.zip
|
||||
423f1210316104b5daaf5ffee8b7e33c9f537e52b38b8f75d3bc1b9021ea47bd *chromedriver-v43.7.5-win32-arm64.zip
|
||||
b868501754d37212d561bc5298caf99d38e9c6d9eef8902263a6f6d3eab5e569 *chromedriver-v43.7.5-win32-ia32.zip
|
||||
9f5d50e29f3cfb13b8afd5cc711ae3219cdd76d52d2dcc86991221d2b7342a39 *chromedriver-v43.7.5-win32-x64.zip
|
||||
c94a7f38ff9c34b6ef2d4861ac74b6015ce93b92814721ae1e0e7fc7ef75bc6c *electron-api.json
|
||||
9236a0cc6e74086761f31edf26e053751d9f69e0ca69fec8a2cba3e7034c74fc *electron-v43.7.5-darwin-arm64-dsym-snapshot.zip
|
||||
6bf01e64c0aa457c7725ddaa8abaec50a7e06a3d2f325795934e3a6370f83db7 *electron-v43.7.5-darwin-arm64-dsym.tar.xz
|
||||
9486b06f8aa6b20b082e0fa8741141a463de96b25a2b362a81f192d1a823c996 *electron-v43.7.5-darwin-arm64-symbols.zip
|
||||
4ddb8717f3c9ade6c5b8a86f0f2b9648067108de93f9a1e7fd6a9ffcf06692ac *electron-v43.7.5-darwin-arm64.zip
|
||||
9236a0cc6e74086761f31edf26e053751d9f69e0ca69fec8a2cba3e7034c74fc *electron-v43.7.5-darwin-x64-dsym-snapshot.zip
|
||||
25c9e7e63ce1b904a2bc7a49f4d1845b4ab4fb461f84a4520d1ec78c72c3d37a *electron-v43.7.5-darwin-x64-dsym.tar.xz
|
||||
9cd6fc5cf09edfed1ba29cf6b72ce929dda71aecc584f9823a93196bcda25ed9 *electron-v43.7.5-darwin-x64-symbols.zip
|
||||
fec7945208ade18b15c9f386c29bdf973f3cb7f779df9bc5e1a04f9de8a146ce *electron-v43.7.5-darwin-x64.zip
|
||||
ec18f1ed85958b6bb0e7285a15fba7da3f546c5919e477a108150f86936e3988 *electron-v43.7.5-linux-arm64-debug.zip
|
||||
5893561c6452225dd6dc8eb8bde572e6d675b5cdcdbba6e0d3c9ad11a95df564 *electron-v43.7.5-linux-arm64-symbols.zip
|
||||
0e8017c56a243deed2510467e5de4be17a2c0634eb017429851ad026551e28ec *electron-v43.7.5-linux-arm64.zip
|
||||
5224ccd7679443680c5a51f9ccaf2ede12b940a39191261dd9011eec6d0231d3 *electron-v43.7.5-linux-armv7l-debug.zip
|
||||
0f8d9b40fde9a6dbc211f99a3caae6f0367afb636e1770f985eaba5993598cf3 *electron-v43.7.5-linux-armv7l-symbols.zip
|
||||
1079cbaf38af752402c29e1a888a9f044403baf784693de3147c7c29097a4157 *electron-v43.7.5-linux-armv7l.zip
|
||||
6ed81092b68add4ec6fb375615f14bdb4a6f5c598242f7800431c7b4d11f7645 *electron-v43.7.5-linux-x64-debug.zip
|
||||
829e3e9c97c08cc61126c43885798325ce1feffcfdb0c882c5489db46a9fb079 *electron-v43.7.5-linux-x64-symbols.zip
|
||||
dce41d6d9a36a2854c5fa1a97f90bc7a8ac8285cf2a76fe3f997c7a02432b364 *electron-v43.7.5-linux-x64.zip
|
||||
9236a0cc6e74086761f31edf26e053751d9f69e0ca69fec8a2cba3e7034c74fc *electron-v43.7.5-mas-arm64-dsym-snapshot.zip
|
||||
abf2db07595e11531aa2cb4c572b6d5fbe81705878b382d962d00b2262f810c7 *electron-v43.7.5-mas-arm64-dsym.tar.xz
|
||||
8e2cde372c0a5f5245c334343741a94ad840e86a698f1ec943f259e39ba3f552 *electron-v43.7.5-mas-arm64-symbols.zip
|
||||
bebd05a60252b9a4f6c83202e3a5b435ef8a489ff6de110fa76aa52a7903dd3c *electron-v43.7.5-mas-arm64.zip
|
||||
9236a0cc6e74086761f31edf26e053751d9f69e0ca69fec8a2cba3e7034c74fc *electron-v43.7.5-mas-x64-dsym-snapshot.zip
|
||||
1bbd11786d0ef8b3ba36c49e6831f4948f6fddc6a1bba1696aa2dce6724c6d8d *electron-v43.7.5-mas-x64-dsym.tar.xz
|
||||
7c6c3971bcc89d4caa4a6f16a7244aac7f3ae300e86fe65820347f5e46987f9f *electron-v43.7.5-mas-x64-symbols.zip
|
||||
5502585a5c698e92a5472b8fbeeaf6aa757f70d5f1a42f5592dff889c6b8ee37 *electron-v43.7.5-mas-x64.zip
|
||||
012133e592664f7b1fd74a42b2f4b9e0e14ec5510ec65a079cd80d8add2eda5e *electron-v43.7.5-win32-arm64-pdb.zip
|
||||
245111a9bbf1f1acf99a4c0212a7cf1226eea5cb4bcf20326802e9b5b50e2e4c *electron-v43.7.5-win32-arm64-symbols.zip
|
||||
90386280bc7e4ac5d451e43e26a7c76ed1c8bcdc0206ec50762c7e4f09c59c28 *electron-v43.7.5-win32-arm64-toolchain-profile.zip
|
||||
996f250e86611b2e5245b8ac09a0375f24d6e343292799a6c9bf7334391fd9c9 *electron-v43.7.5-win32-arm64.zip
|
||||
cfd279ac0110c4b128627eaffed83e0d3dc3b70d6d8d6a54ca5254bef5a79d15 *electron-v43.7.5-win32-ia32-pdb.zip
|
||||
1b52b17efc00fab5317d191e0b871fd8e324983dcddfcc6829c867e1baf5dc9f *electron-v43.7.5-win32-ia32-symbols.zip
|
||||
90386280bc7e4ac5d451e43e26a7c76ed1c8bcdc0206ec50762c7e4f09c59c28 *electron-v43.7.5-win32-ia32-toolchain-profile.zip
|
||||
86897584b0b36a7a55280a33bcd2040115d3651b7ea753f23ab5097286947482 *electron-v43.7.5-win32-ia32.zip
|
||||
c7531d071950a50b3d8b55ec403bdcbfdd9a8ba62f91a18a88de7a638084525a *electron-v43.7.5-win32-x64-pdb.zip
|
||||
1891dfd0d60dc76bbf83601a354c866642ce1fdf5b9a2063666990fa99424799 *electron-v43.7.5-win32-x64-symbols.zip
|
||||
90386280bc7e4ac5d451e43e26a7c76ed1c8bcdc0206ec50762c7e4f09c59c28 *electron-v43.7.5-win32-x64-toolchain-profile.zip
|
||||
7acfa0646793f912ff983c8db8c3a145dc18ee40fe3d11a01840fd59cb76e5a2 *electron-v43.7.5-win32-x64.zip
|
||||
81e05df0600cb229dbdb080e5200ddf3532d0c4b5a668e2cba5bc5683f805098 *electron.d.ts
|
||||
c3f967e6e0fcada1d2dd24b08f190b02aaf243a014531901e82b70417193c48b *ffmpeg-v43.7.5-darwin-arm64.zip
|
||||
a6860ea4f0ec3954ae2686ae2ff2fd72cb4793f7229fe0d0c9c8efd0487e7297 *ffmpeg-v43.7.5-darwin-x64.zip
|
||||
ed1d9f6af263524c00ff955f50bf289e19c881625384ff421c773ef2aeb8a82d *ffmpeg-v43.7.5-linux-arm64.zip
|
||||
001847bd19549781850fd6f69941a54f92356ed26bca02892e9642637215668d *ffmpeg-v43.7.5-linux-armv7l.zip
|
||||
6638429b92e6d213b78114923585526aff88dbe25f91ace7f2bd5228fac08de5 *ffmpeg-v43.7.5-linux-x64.zip
|
||||
5290184f135527f4b58636e14869688a9fad6249cfe9f3608c9046e06efc6660 *ffmpeg-v43.7.5-mas-arm64.zip
|
||||
785ededd318692376a61ed854a95fe9103ad2f7616f64b5e3b96ce34eaecdad1 *ffmpeg-v43.7.5-mas-x64.zip
|
||||
1bbd64356da38ad2c8b9dd254046b0491e07d72e27952ee7f49331191a26bdb1 *ffmpeg-v43.7.5-win32-arm64.zip
|
||||
7e9044d58cf7695e5549f73a4d0a54a7b2e24af930ff69195765c890346361f2 *ffmpeg-v43.7.5-win32-ia32.zip
|
||||
b1b2597a18e68c70d0f65c46cdd0f0e10cb847e227e3e34cdbe6a6eee0e663c0 *ffmpeg-v43.7.5-win32-x64.zip
|
||||
47b695db7f9dedebbdeec47070622da4312574f38e8ca8d77ef94c827a522a2f *hunspell_dictionaries.zip
|
||||
198993602d37429e707e494f781eea876de3e9fd85d016c01b1f7432a747b418 *libcxx-objects-v43.7.5-linux-arm64.zip
|
||||
80b6639adb398b42622ec0644a3a9c9e3f696309825537a817d42cf92c0a4f2e *libcxx-objects-v43.7.5-linux-armv7l.zip
|
||||
dcb37cf5b0b1941e0304379dfea3587a3ba39167d8edca9e2ab749f54349586d *libcxx-objects-v43.7.5-linux-x64.zip
|
||||
88eeecbe3a86626bc8b9677d764e831b13589449126b8b5aa84cb291e38f220e *libcxx_headers.zip
|
||||
88a69e434f5a001028891091b32535b7b4e95c7764e4a7597112467042615d9b *libcxxabi_headers.zip
|
||||
cdf1bb3bfa3c2a95ad463f9abd0cb3c94f39b9e5eb317eced9715d6496829c34 *mksnapshot-v43.7.5-darwin-arm64.zip
|
||||
08e2460962d8729e8fa36bf30758d943ef4059f99e2d41dfd329193b51f94960 *mksnapshot-v43.7.5-darwin-x64.zip
|
||||
d4544b339553ba5722aeaa53c22345183ae6d041e6330de0b0f3ef21d9af2046 *mksnapshot-v43.7.5-linux-arm64-x64.zip
|
||||
ab060e6272e2e536732921eccef686560268b02edf832d63c3cada562cb423e7 *mksnapshot-v43.7.5-linux-armv7l-x64.zip
|
||||
1a271d78fbb4922c02043538d53a4f87fe4e1c8bd58e7a78a29aeffb2cb742f2 *mksnapshot-v43.7.5-linux-x64.zip
|
||||
42e489bd90f30581941143a2285861c69316bd3a603320bbe32ef217b8650adc *mksnapshot-v43.7.5-mas-arm64.zip
|
||||
c2082d5d7f90e866760d2a6424bab260b3c757cc00884e1f00d87cad6d53a453 *mksnapshot-v43.7.5-mas-x64.zip
|
||||
5665055067d7f53d459f0c63884648eb7dcfbf64b417b8c0b0738b7133b97812 *mksnapshot-v43.7.5-win32-arm64-x64.zip
|
||||
7c882bd39f45cc9e1112f7b949b443305e959fad57292a9f9b97c3e45d502ce8 *mksnapshot-v43.7.5-win32-ia32.zip
|
||||
bff2442315ad1482db15f6230f7f98308a0b758ab46b0b6f53ff0ace715e96ce *mksnapshot-v43.7.5-win32-x64.zip
|
||||
b9b7d9ff132e9655a574f03edfb01add8d43a8b0d3a79320c226f6aa99c922ea *chromedriver-v43.7.7-darwin-arm64.zip
|
||||
f3b26877642739e6c668e6368daada33bb1dfcf4d5fec34143195e184e984569 *chromedriver-v43.7.7-darwin-x64.zip
|
||||
091707f83e4191f36848152d664c80c255572dfe36a042250ca94037c06959d2 *chromedriver-v43.7.7-linux-arm64.zip
|
||||
9c3dc509c12709522a22e3c4eb3270907050391fe05d89f68ab5ea689f9aa5b2 *chromedriver-v43.7.7-linux-armv7l.zip
|
||||
7ea6157ec70b065ef6a7f77fc1822dc6441f5702b0f99660e26cfca1897ecfa4 *chromedriver-v43.7.7-linux-x64.zip
|
||||
cad78ce6d3399821fde0cc5701109334409aace9b2c82ab8f4c0d3fffa152906 *chromedriver-v43.7.7-mas-arm64.zip
|
||||
42221fd4d828376802e644178717f1b0626bd1188d45199e958e17ab520ff9ee *chromedriver-v43.7.7-mas-x64.zip
|
||||
d2c67dad42cfdcf17fce6eac8bba81502c4b2d9cd90cc16f189b1da71dde8ca3 *chromedriver-v43.7.7-win32-arm64.zip
|
||||
79aedd7837ac32ec19f98677901def62d5cdbbe63d90fe5f25ca131f8609a843 *chromedriver-v43.7.7-win32-ia32.zip
|
||||
8b81ca5a4add14593548f5b4042a028e4c50758d61c5a244741ca6d07be3c95d *chromedriver-v43.7.7-win32-x64.zip
|
||||
aabd65bdb7a4fbd80c114af429392aa5a126382883c7c2e8f3ee11acf2e4ce08 *electron-api.json
|
||||
649e6c16d4650ec4f3037daa285b9f8e72b91742b43810bba80ca5e8bfb8cfe7 *electron-v43.7.7-darwin-arm64-dsym-snapshot.zip
|
||||
941f6adb5eb6dbce25db81158a8f19fd5b4b24bcc97ea5986bb901f822b93a54 *electron-v43.7.7-darwin-arm64-dsym.tar.xz
|
||||
34f7acfef1ff41f81b63895f010b866c1f39ac22e0d700445eb6e9ee595276c4 *electron-v43.7.7-darwin-arm64-symbols.zip
|
||||
9327d8ba5bc9e279d1a2f7da90235301c65a2e80eb4ad3bc5610d28d483340f9 *electron-v43.7.7-darwin-arm64.zip
|
||||
649e6c16d4650ec4f3037daa285b9f8e72b91742b43810bba80ca5e8bfb8cfe7 *electron-v43.7.7-darwin-x64-dsym-snapshot.zip
|
||||
2a0fc941390038cad45ef580cf65ee333f61f6e4504b9433447ed8fa3585e234 *electron-v43.7.7-darwin-x64-dsym.tar.xz
|
||||
6f003f93738843419063adba7dbac56ac1dcfc79dcbeee39d700c335d34d831a *electron-v43.7.7-darwin-x64-symbols.zip
|
||||
cbed66567d55db4a2bffad0bb6ee9795ca0037ad241039f7473fe75680d10905 *electron-v43.7.7-darwin-x64.zip
|
||||
ba871fb65de9fbfd71f9aaafc1ac278434f497ee921e9f27fee9e0a5000e9eef *electron-v43.7.7-linux-arm64-debug.zip
|
||||
d13b1f995845a5a138c35100944dc1264b98be3dd5540253640a66fc388d233d *electron-v43.7.7-linux-arm64-symbols.zip
|
||||
16071038a9677d0f00b11d3d0f0b7b4ea3250987727aa73aa563b87789f2d5ab *electron-v43.7.7-linux-arm64.zip
|
||||
bb2295977fc6d33adce9f62def5dfaa19de0e1bd29e5bbd366e0cc35d1a57a09 *electron-v43.7.7-linux-armv7l-debug.zip
|
||||
07c0739157f9674827c7d55976016972cff70e0876d68287bdf24fca54d778bd *electron-v43.7.7-linux-armv7l-symbols.zip
|
||||
088c2817c1b5928c5bea706cc60aa05ea328e6d1d8bd549fda75388a3552482a *electron-v43.7.7-linux-armv7l.zip
|
||||
0f20d65a6f671b1966de12d3f71c2d96a1a65e7ba8c1d534b60bc08c7f82b950 *electron-v43.7.7-linux-x64-debug.zip
|
||||
a5fb797b973514622f37154d5088132705ce86641f05e6b1e858269a86961868 *electron-v43.7.7-linux-x64-symbols.zip
|
||||
4d0a48398c444258dbcf2f5f83b49ca5bc53583130f354e0c299dad5b22b5271 *electron-v43.7.7-linux-x64.zip
|
||||
649e6c16d4650ec4f3037daa285b9f8e72b91742b43810bba80ca5e8bfb8cfe7 *electron-v43.7.7-mas-arm64-dsym-snapshot.zip
|
||||
dc850f562fbf669db1d9940d458c7ba9790392cff18d95ebaec1fe94a8d583ff *electron-v43.7.7-mas-arm64-dsym.tar.xz
|
||||
61cf1a0dc10498aff1d1eefa9a5b96350cba32734bdc9f7f40724f9aa17d03d9 *electron-v43.7.7-mas-arm64-symbols.zip
|
||||
987eebfdc29638b9066e5484b0dcb59e7096627edebad722c7649f8472f2c8a0 *electron-v43.7.7-mas-arm64.zip
|
||||
649e6c16d4650ec4f3037daa285b9f8e72b91742b43810bba80ca5e8bfb8cfe7 *electron-v43.7.7-mas-x64-dsym-snapshot.zip
|
||||
8caa427ef3e1dd3f1832a3b782ace268999afcf011540fe05a62c957917487ea *electron-v43.7.7-mas-x64-dsym.tar.xz
|
||||
91f67bf1b2deedf84355d98095a6060de69fb64c01caf223a35e0edef38aee55 *electron-v43.7.7-mas-x64-symbols.zip
|
||||
f5eaad26bda0bc133f8d0af0f3a77d2e16adf2572306bec611424c3bab876acf *electron-v43.7.7-mas-x64.zip
|
||||
b7aa250996ba4aadcbb115e951c6a46f19f246ee6178cffc6b115088189fc1d5 *electron-v43.7.7-win32-arm64-pdb.zip
|
||||
e63955e73f754a249db46a2c312fd904b6405018650d22b8a16037a9c10270e0 *electron-v43.7.7-win32-arm64-symbols.zip
|
||||
90386280bc7e4ac5d451e43e26a7c76ed1c8bcdc0206ec50762c7e4f09c59c28 *electron-v43.7.7-win32-arm64-toolchain-profile.zip
|
||||
d3751b5046350eee0cfd88e7709697c0ef05db93bb3435195c2d0cec50f0ad1f *electron-v43.7.7-win32-arm64.zip
|
||||
2e8d41b27d3c2394bfeb4ec00904d7049c8a7ccaa105e678179a983cdfb319db *electron-v43.7.7-win32-ia32-pdb.zip
|
||||
67ea48dcfdcca2f7b854fbe3689d56fe047ca9a2aee0808422ef313e0bc4b52a *electron-v43.7.7-win32-ia32-symbols.zip
|
||||
90386280bc7e4ac5d451e43e26a7c76ed1c8bcdc0206ec50762c7e4f09c59c28 *electron-v43.7.7-win32-ia32-toolchain-profile.zip
|
||||
a170eeedf4a216b3b0672b151294e7aef0b4fdc15ae5c207ea3f9f37c4a14c42 *electron-v43.7.7-win32-ia32.zip
|
||||
3b65b14be1594bba502283ae02e9af54b55b1894f3f6be961c608c9e80a74179 *electron-v43.7.7-win32-x64-pdb.zip
|
||||
37d27ae6956d20fde7fa1218453ad9514c106f17cf30634c452302a7453f189f *electron-v43.7.7-win32-x64-symbols.zip
|
||||
90386280bc7e4ac5d451e43e26a7c76ed1c8bcdc0206ec50762c7e4f09c59c28 *electron-v43.7.7-win32-x64-toolchain-profile.zip
|
||||
97dcb75065444ef031b9b6ea814ccd2109b97934fffb0c503a555d4737ca79cc *electron-v43.7.7-win32-x64.zip
|
||||
d78a11ef458b68672c35957f0252396a1fa19e73a8ca3b9178e2e75cd090453d *electron.d.ts
|
||||
9cd0907e7c7c652cc6f519c8e7d75cee26bed06394bb1d51fabdb9d3ada33c67 *ffmpeg-v43.7.7-darwin-arm64.zip
|
||||
fc2ecfa9bb7ab0131e073f138923ee58f01b88941e366a0be1bad821ed5386bd *ffmpeg-v43.7.7-darwin-x64.zip
|
||||
ed1d9f6af263524c00ff955f50bf289e19c881625384ff421c773ef2aeb8a82d *ffmpeg-v43.7.7-linux-arm64.zip
|
||||
001847bd19549781850fd6f69941a54f92356ed26bca02892e9642637215668d *ffmpeg-v43.7.7-linux-armv7l.zip
|
||||
6638429b92e6d213b78114923585526aff88dbe25f91ace7f2bd5228fac08de5 *ffmpeg-v43.7.7-linux-x64.zip
|
||||
cbbd298f4548e53200d6b0321bea708963e94acc699059721642183301752109 *ffmpeg-v43.7.7-mas-arm64.zip
|
||||
7943078d8e3ee924f1cad1354a29aa3aceaf0b7b7414df3a71924ea9c8906223 *ffmpeg-v43.7.7-mas-x64.zip
|
||||
c8ec52d6bbda2789c678211a76ffc6eaf1dc5faebe1269fc4d6aafa7fbc5510f *ffmpeg-v43.7.7-win32-arm64.zip
|
||||
37f7bf4b42615e880f971d8714f2b1f4629c06bd8bbd73c8993a8713f8d3628f *ffmpeg-v43.7.7-win32-ia32.zip
|
||||
df14da964d36f36c50030004bb5e93a3b5fac9e4ddd6dd4f0e4ecdd49456ee4f *ffmpeg-v43.7.7-win32-x64.zip
|
||||
1987f0c54ac21514090440f4dc981245cae34b71e95ddbeffd5bcb5da9a4982e *hunspell_dictionaries.zip
|
||||
cd2a8fc7973a60a60f1612d87528e07eb047fad61106f240f4e39bffe9b66969 *libcxx-objects-v43.7.7-linux-arm64.zip
|
||||
76dc80ad01bf2f0da658d5e58e256bcbcfc5b5e03d5ce25c3a43b9073d0e2a4f *libcxx-objects-v43.7.7-linux-armv7l.zip
|
||||
5df32a0f3b4e11575d619dfc30bd614238a616f87303142b37bcf62b6206047f *libcxx-objects-v43.7.7-linux-x64.zip
|
||||
f318b7480e1e9b6149cfa83dedee6027a381f4dc7a4b99af89dddbab4f6d0261 *libcxx_headers.zip
|
||||
600988a1000006cb10d8b88727c6877dfe3e0cbcb49baee317551d8131593c0c *libcxxabi_headers.zip
|
||||
080d43c0521b53dc9fffe91b17f9592518230aa0c819bac3e7a6aff484126895 *mksnapshot-v43.7.7-darwin-arm64.zip
|
||||
3a4cf3ca11eb4f98e66e9ef111f7be82d5d517851ef0882d71bf25ef3f5e4adb *mksnapshot-v43.7.7-darwin-x64.zip
|
||||
052aa030567fe51301f4ba7c52098583868296f0ea91fa1a78158f763bec1332 *mksnapshot-v43.7.7-linux-arm64-x64.zip
|
||||
de81d082e286d93e6c2baa6a2ce4b5ccf36dbf5fab512c1a8274e4ad1d6ba660 *mksnapshot-v43.7.7-linux-armv7l-x64.zip
|
||||
1a222e37532fb300237eedfc6eae2a836f9b8ccf988af64507c88178a9ad3a41 *mksnapshot-v43.7.7-linux-x64.zip
|
||||
4aaa40fcf9c283f9c3106523fcaff9301dac65b7d36434aa1970def061c82a20 *mksnapshot-v43.7.7-mas-arm64.zip
|
||||
2a46e79869eaaee734f7f70ef5650bdba3b72cce26a201c8f1a87e15364efd16 *mksnapshot-v43.7.7-mas-x64.zip
|
||||
8de088472502ed3a26b5a56b016849add2cc694eb2acc76e599d582c675ca8e1 *mksnapshot-v43.7.7-win32-arm64-x64.zip
|
||||
e73d7a9ada185a6182bdf6872f6c5a7ae22f18a1f7efdb9e4fcb7b21b21b0c53 *mksnapshot-v43.7.7-win32-ia32.zip
|
||||
4878c2cf511a54a3d38cab480b0f3a2699f84e0d0bdf80b7f300cfbe867ca8b5 *mksnapshot-v43.7.7-win32-x64.zip
|
||||
@@ -56,42 +56,6 @@
|
||||
"status": "enforced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "chat.agent.allowedNetworkDomains",
|
||||
"name": "ChatAgentAllowedNetworkDomains",
|
||||
"category": "InteractiveSession",
|
||||
"minimumVersion": "1.116",
|
||||
"localization": {
|
||||
"description": {
|
||||
"key": "chat.agent.allowedNetworkDomains",
|
||||
"value": "Allowed domains for network access by agent tools (fetch tool, integrated browser). Applies when `#chat.agent.networkFilter#` or `#chat.agent.sandbox.enabled#` is enabled. When `#chat.agent.sandbox.network.allowNetwork#` is enabled, all domains are allowed. Supports wildcards like `*.example.com`. When both allowed and denied lists are empty, all domains are blocked. Denied domains (see `#chat.agent.deniedNetworkDomains#`) take precedence.\n\nChanges may not take full effect until VS Code is restarted."
|
||||
}
|
||||
},
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"included": true,
|
||||
"agentHost": {
|
||||
"status": "partial"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "chat.agent.deniedNetworkDomains",
|
||||
"name": "ChatAgentDeniedNetworkDomains",
|
||||
"category": "InteractiveSession",
|
||||
"minimumVersion": "1.116",
|
||||
"localization": {
|
||||
"description": {
|
||||
"key": "chat.agent.deniedNetworkDomains",
|
||||
"value": "Denied domains for network access by agent tools (fetch tool, integrated browser). Applies when `#chat.agent.networkFilter#` or `#chat.agent.sandbox.enabled#` is enabled. This does not apply when `#chat.agent.sandbox.network.allowNetwork#` is enabled. Takes precedence over `#chat.agent.allowedNetworkDomains#`. Supports wildcards like `*.example.com`.\n\nChanges may not take full effect until VS Code is restarted."
|
||||
}
|
||||
},
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"included": true,
|
||||
"agentHost": {
|
||||
"status": "partial"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "chat.agent.enabled",
|
||||
"name": "ChatAgentMode",
|
||||
@@ -118,7 +82,7 @@
|
||||
"localization": {
|
||||
"description": {
|
||||
"key": "chat.agent.networkFilter",
|
||||
"value": "When enabled, network access by agent tools (fetch tool, integrated browser) is restricted according to `#chat.agent.allowedNetworkDomains#` and `#chat.agent.deniedNetworkDomains#`. Domain filtering is also applied to those tools when `#chat.agent.sandbox.enabled#` is enabled.\n\nChanges may not take full effect until VS Code is restarted."
|
||||
"value": "When enabled, network access by agent tools (fetch tool, integrated browser) is restricted according to `#chat.agent.sandbox.network.allowedDomains#` and `#chat.agent.sandbox.network.deniedDomains#`. Domain filtering is also applied to those tools when `#chat.agent.sandbox.enabled#` is enabled.\n\nChanges may not take full effect until VS Code is restarted."
|
||||
}
|
||||
},
|
||||
"type": "boolean",
|
||||
@@ -178,6 +142,24 @@
|
||||
"status": "notApplicable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "chat.agent.sandbox.network.allowedDomains",
|
||||
"name": "ChatAgentAllowedNetworkDomains",
|
||||
"category": "InteractiveSession",
|
||||
"minimumVersion": "1.116",
|
||||
"localization": {
|
||||
"description": {
|
||||
"key": "chat.agent.allowedNetworkDomains",
|
||||
"value": "Allowed domains for network access by agent tools (fetch tool, integrated browser) when `#chat.agent.networkFilter#` is enabled. In Copilot Agent Host sessions with sandboxing (`#chat.agent.sandbox.enabled#`) enabled, also restricts the integrated browser when outbound network access (`#chat.agent.sandbox.network.allowNetwork#`) is allowed; an empty allow list adds no sandbox restriction. This list does not enable outbound access. Supports wildcards like `*.example.com`. Outside sandboxing, when both allowed and denied lists are empty, all domains are blocked. Denied domains (see `#chat.agent.sandbox.network.deniedDomains#`) take precedence.\n\nChanges may not take full effect until VS Code is restarted."
|
||||
}
|
||||
},
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"included": true,
|
||||
"agentHost": {
|
||||
"status": "partial"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "chat.agent.sandbox.network.allowNetwork",
|
||||
"name": "ChatAgentSandboxAllowNetwork",
|
||||
@@ -196,6 +178,24 @@
|
||||
"status": "notApplicable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "chat.agent.sandbox.network.deniedDomains",
|
||||
"name": "ChatAgentDeniedNetworkDomains",
|
||||
"category": "InteractiveSession",
|
||||
"minimumVersion": "1.116",
|
||||
"localization": {
|
||||
"description": {
|
||||
"key": "chat.agent.deniedNetworkDomains",
|
||||
"value": "Denied domains for network access by agent tools (fetch tool, integrated browser) when `#chat.agent.networkFilter#` is enabled. Also applies to the integrated browser in sandboxed Copilot Agent Host sessions (`#chat.agent.sandbox.enabled#`); allowing outbound network access (`#chat.agent.sandbox.network.allowNetwork#`) does not bypass this list. Takes precedence over `#chat.agent.sandbox.network.allowedDomains#`. Supports wildcards like `*.example.com`.\n\nChanges may not take full effect until VS Code is restarted."
|
||||
}
|
||||
},
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"included": true,
|
||||
"agentHost": {
|
||||
"status": "partial"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "chat.agentHost.claudeAgent.enabled",
|
||||
"name": "Claude3PIntegration",
|
||||
|
||||
+3
-3
@@ -529,13 +529,13 @@
|
||||
"git": {
|
||||
"name": "electron",
|
||||
"repositoryUrl": "https://github.com/electron/electron",
|
||||
"commitHash": "5338ed09bfdcf1bbd2e6bab722c2620efe8c4790",
|
||||
"tag": "43.7.5"
|
||||
"commitHash": "ddb762f50fbd70384d0385302697e60ada549024",
|
||||
"tag": "43.7.7"
|
||||
}
|
||||
},
|
||||
"isOnlyProductionDependency": true,
|
||||
"license": "MIT",
|
||||
"version": "43.7.5"
|
||||
"version": "43.7.7"
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
|
||||
@@ -80,7 +80,12 @@ pub async fn agent_stop(ctx: CommandContext, args: AgentStopArgs) -> Result<i32,
|
||||
"Agent host returned an invalid active turn start timestamp",
|
||||
)
|
||||
})?;
|
||||
let duration = Timestamp::now().as_millisecond() - started_at.as_millisecond();
|
||||
|
||||
// Defensive programming: Prevent negative duration values caused by clock skew
|
||||
let now_ms = Timestamp::now().as_millisecond();
|
||||
let started_ms = started_at.as_millisecond();
|
||||
let duration = std::cmp::max(0, now_ms - started_ms);
|
||||
|
||||
let turn_id = active_turn.id;
|
||||
|
||||
debug!(ctx.log, "Cancelling turn {} on {}", turn_id, chat_uri);
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
name: migrate-customizations
|
||||
description: Guide a safe, reversible migration of VS Code local agent customizations when launched from the Agent Customizations Migrations page with VS Code-generated migration context.
|
||||
argument-hint: Launch from Agent Customizations > Migrations > Migrate with Agent
|
||||
user-invocable: true
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# Migrate Agent Customizations
|
||||
|
||||
Guide the user through migrating the inventory included in the invoking prompt. The source locations and valid target folders in that prompt were evaluated for the selected harness. Treat them as authoritative: do not substitute hardcoded destination paths or migrate to a folder that is not listed.
|
||||
|
||||
## Required Migration Context
|
||||
|
||||
Before doing anything else, verify that the invoking prompt includes all of the following VS Code-generated values:
|
||||
|
||||
- Selected harness
|
||||
- Migration telemetry flow
|
||||
- Recovery bundle folder and filesystem path
|
||||
- Customizations that need migration
|
||||
- Valid target folders reported by the selected harness
|
||||
|
||||
If any value is missing, do not inspect, copy, move, convert, or delete customizations. Explain that this workflow must be started from **Agent Customizations > Migrations** by selecting **Migrate with Agent**, then stop. Do not ask the user to manually reconstruct or paste the missing context.
|
||||
|
||||
Read [migration techniques](./references/migration-techniques.md) before proposing changes.
|
||||
|
||||
## Safety Contract
|
||||
|
||||
1. Never modify a customization before creating a recovery bundle.
|
||||
2. Use the recovery bundle folder supplied in the invoking prompt. VS Code created and is watching this folder; do not move it or substitute another location.
|
||||
3. In the recovery bundle, create:
|
||||
- `migration-log.md`, containing the selected harness, scope, timestamps, source and destination paths, commands or tools used, validation results, and every user decision.
|
||||
- `backups/`, containing a byte-for-byte copy of every file before its first modification or deletion.
|
||||
- `restore.md`, containing exact steps for restoring the backups and reverting newly created files.
|
||||
- `migration-results.json`, containing only the privacy-safe aggregate schema described in Finish. VS Code watches this file for the final migration outcome.
|
||||
4. Append to the log after every attempted operation, including failures and rollbacks.
|
||||
5. Approval to migrate a group authorizes destination writes only; it does not authorize deleting source files, removing source MCP entries, or clearing location settings. Preserve sources by default. After the destination is validated, ask separately for explicit approval for each kind of cleanup. If runtime validation is not possible, explain what was and was not verified, keep the source, and ask whether to defer cleanup.
|
||||
6. Stop on an unexpected source change, destination conflict, invalid configuration, failed backup, or failed validation. Explain the problem instead of guessing.
|
||||
7. Never include customization names, paths, contents, MCP configuration, or other user data in telemetry.
|
||||
8. Classify each inventory item exactly once using its supplied category and scope. Do not offer the same item again in another group; if groups overlap, explain the overlap and use the item's inventory category to decide where it is handled.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Choose Scope
|
||||
|
||||
Ask whether to migrate:
|
||||
|
||||
- user customizations,
|
||||
- workspace customizations, or
|
||||
- both.
|
||||
|
||||
Do not act on an unselected scope.
|
||||
|
||||
If workspace customizations are selected, ask whether the user wants a pull request. If they do, keep workspace changes focused, include the validation evidence and migration rationale in the pull request, and do not include a recovery bundle stored outside the repository.
|
||||
|
||||
### 2. Review the Plan
|
||||
|
||||
Classify the supplied inventory by its reported category and scope, then handle applicable items in this order:
|
||||
|
||||
1. prompt files that should become skills;
|
||||
2. VS Code profile user-data agents, instructions, and skills;
|
||||
3. MCP servers;
|
||||
4. workspace or user customizations at locations defined by settings `chat.agentSkillsLocations`, `chat.instructionsFilesLocations`, and `chat.agentFilesLocations`.
|
||||
|
||||
The groups are mutually exclusive for an inventory item. In particular, a file reported under `configuredLocations` belongs in group 4, not group 2, even if its type is agent, instruction, or skill. If a previously skipped item would otherwise reappear in a later group, do not ask about it again unless the user requests reconsideration.
|
||||
|
||||
Before each group:
|
||||
|
||||
- explain why the migration is useful;
|
||||
- list the source and harness-reported destination locations;
|
||||
- describe changes that cannot be preserved;
|
||||
- state explicitly that approval covers destination writes only and that source cleanup will require a separate approval;
|
||||
- ask for confirmation to proceed with that group.
|
||||
|
||||
Work one group at a time. Do not request approval for all writes at once.
|
||||
|
||||
### 3. Migrate Prompt Files to Skills
|
||||
|
||||
Explain the compatibility reason for converting each prompt to a skill. Convert each selected prompt into a skill directory with a `SKILL.md`, preserving supported name, description, argument guidance, invocation semantics, and body content. Add `disable-model-invocation: true` when needed to preserve an explicit-invocation-only prompt. Record unsupported frontmatter in the log for review; do not silently drop behavior.
|
||||
|
||||
Validate that each skill has valid frontmatter, a meaningful description, and a folder name that matches the skill name. Distinguish structural validation from runtime validation: test discovery and representative invocation in the selected destination harness when possible. A file inspection or a test against a different harness is not proof that the destination harness loads the skill. If runtime validation requires a new session or is otherwise unavailable, say so, leave the source in place, and defer cleanup unless the user explicitly chooses otherwise after hearing the limitation.
|
||||
|
||||
### 4. Migrate User Data
|
||||
|
||||
Explain that Agent Host doesn't read the VS Code profile user data folder. It only reads the harness's user folders. Copy selected agents, instructions, and skills to a compatible listed destination without silently changing their contents.
|
||||
Keep user-scope items in user scope. Explain that approval to copy does not authorize deleting the VS Code source or changing sync/location settings; ask for those separately after validation.
|
||||
|
||||
### 5. Migrate MCP Servers
|
||||
|
||||
Explain that moving MCP configuration lets the selected Agent Host load the server directly. Review every server separately, including destination conflicts and properties the destination format cannot preserve. Call out that a disabled user server might become enabled after migration.
|
||||
|
||||
Write and statically validate the destination before considering source cleanup. Leave unselected, unsupported, and conflicting servers unchanged. Test that migrated servers are discovered and can start in the selected destination harness; spawning a command directly can verify process behavior, but does not by itself prove that the harness discovers or loads the configuration. If destination-harness testing is unavailable, report that limitation and keep source entries.
|
||||
|
||||
Approval for this group authorizes destination writes only. After successful destination validation, ask separately whether to remove each migrated or already-equivalent source entry. Do not remove a source entry merely because its destination entry was written or because the user approved the group.
|
||||
|
||||
### 6. Customizations at Custom Locations
|
||||
|
||||
Files in custom locations defined by `chat.agentSkillsLocations`, `chat.instructionsFilesLocations`, and `chat.agentFilesLocations` can continue to work when the selected harness is connected to VS Code. Moving them to a harness-reported standard location may improve portability or consistency, but is not automatically required for compatibility. Explain this trade-off before asking whether to migrate.
|
||||
|
||||
Handle only items reported as `configuredLocations` and not already handled in another group. Do not re-offer files the user explicitly skipped. Treat moving a file, deleting its old copy, and clearing or changing the location setting as separate actions; obtain explicit approval for each cleanup action after validating the destination.
|
||||
|
||||
### 7. Finish
|
||||
|
||||
Before the final summary, write `migration-results.json` in the recovery bundle:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"migrationFlowId": "<flow from the invoking prompt>",
|
||||
"cancelled": false,
|
||||
"results": [
|
||||
{
|
||||
"category": "promptFiles",
|
||||
"scope": "workspace",
|
||||
"customizationType": "skill",
|
||||
"outcome": "migrated",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Aggregate results by:
|
||||
|
||||
- category: `promptFiles`, `userData`, `configuredLocations`, or `mcpServers`;
|
||||
- scope: `user` or `workspace`;
|
||||
- customization type: `agent`, `instructions`, `skill`, or `mcpServer`;
|
||||
- outcome: `migrated`, `skipped`, or `failed`.
|
||||
|
||||
The total reported for each category, scope, and customization type must not exceed the inventory supplied by VS Code. The result file must never include names, paths, contents, configuration values, error messages, or other user data.
|
||||
|
||||
Write the validated JSON to the exact recovery bundle supplied by VS Code. Do not write a result file when no migration telemetry flow was supplied.
|
||||
|
||||
If the user stops the workflow, set `cancelled` to `true`, record completed work with its actual outcome, mark the remaining inventory as `skipped`, and still validate and write the result file before the final cancellation summary.
|
||||
|
||||
Summarize:
|
||||
|
||||
- migrated, skipped, and failed items;
|
||||
- created, modified, deleted, and retained files;
|
||||
- validation performed;
|
||||
- cleanup still pending;
|
||||
- recovery bundle and restoration instructions;
|
||||
- pull request URL when one was requested.
|
||||
|
||||
Keep the recovery bundle until the user confirms the migrated customizations work.
|
||||
|
||||
For a requested pull request, verify the final diff contains only approved workspace changes, exclude the recovery bundle, and include the migration rationale and actual validation performed. Do not claim runtime validation when only static checks or direct process tests were possible.
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
# Agent Customization Migration Techniques
|
||||
|
||||
Use these techniques together with the source inventory and target folders supplied by VS Code. Harness-reported destinations take precedence over examples in general documentation.
|
||||
|
||||
## General Technique
|
||||
|
||||
- Back up every source before writing.
|
||||
- Group approval authorizes destination writes only. Preserve each source by default; ask for explicit cleanup approval separately, after destination validation.
|
||||
- Keep user and workspace scope unchanged unless the user explicitly chooses otherwise.
|
||||
- Classify each inventory item once by its supplied category and scope. Do not offer a skipped item again in another group unless the user asks to reconsider it.
|
||||
- In a multi-root workspace, keep each migrated customization in the corresponding workspace root.
|
||||
- Verify discovery and representative behavior in the destination environment. Remote and Dev Container user folders belong to that environment.
|
||||
- Migrated and original copies do not synchronize. Remove the original only after validation.
|
||||
- Separate structural checks (file shape, frontmatter, or valid JSON) from runtime checks in the selected harness. A direct server-process test is useful evidence about that process, but does not prove the harness discovers or loads its configuration. If destination-harness testing is unavailable, report the limitation and keep the source unless the user explicitly chooses otherwise after being informed.
|
||||
|
||||
## Prompt Files to Skills
|
||||
|
||||
Agent Host harnesses do not load `*.prompt.md` files. Convert a prompt to `<skill-folder>/SKILL.md`.
|
||||
|
||||
- Preserve supported `name`, `description`, and argument guidance.
|
||||
- Preserve invocation semantics. Add `disable-model-invocation: true` when needed to retain an explicit-invocation-only prompt.
|
||||
- Choose a valid, unique skill folder name.
|
||||
- Record unsupported prompt frontmatter rather than silently discarding it.
|
||||
- Keep the prompt body focused as a reusable workflow.
|
||||
- Validate the skill's frontmatter and invoke it with a representative request.
|
||||
- Do not treat structural validation as proof of runtime discovery. Test the skill in the selected destination harness when possible; if testing requires a new session, defer it and keep the prompt until the user has been told runtime validation is pending.
|
||||
|
||||
## User Data Locations
|
||||
|
||||
VS Code profile user data are Local-only. Move agents, instructions, and skills to a destination reported by the selected harness.
|
||||
|
||||
- Preserve file type, name, and content unless a compatibility issue requires an explained change.
|
||||
- Keep workspace files in their originating repository root.
|
||||
- Explain that harness user folders might not participate in VS Code Settings Sync.
|
||||
- Treat copying a file, deleting the source, and clearing or changing a location setting as separate decisions. Group migration approval authorizes the copy only.
|
||||
- `chat.promptFilesLocations` entries are prompt-to-skill migrations, not ordinary location moves.
|
||||
|
||||
## MCP Servers
|
||||
|
||||
Migrate each MCP server independently.
|
||||
|
||||
- Review source and destination JSON before writing.
|
||||
- Detect duplicate server names and non-equivalent destination entries.
|
||||
- Explain how every source property and variable will migrate by using the table below.
|
||||
- A disabled user server might become enabled at the destination; review enablement before starting a session.
|
||||
- Write and statically validate the destination entry before runtime testing or considering source cleanup.
|
||||
- Leave unsupported, invalid, conflicting, and unselected servers unchanged.
|
||||
- Do not remove a source entry solely because an equivalent destination entry already exists. After successful validation, ask separately before removing each migrated or redundant source entry.
|
||||
|
||||
| Source configuration | Migration result | What to do |
|
||||
|----------------------|------------------|------------|
|
||||
| Standard `command`, `args`, `env`, `url`, and `headers` values | Migrate automatically and add `tools: ["*"]` | Inspect the server's tools. Replace `*` with the specific tools the agent should invoke when access to every server tool is not appropriate. |
|
||||
| `${workspaceFolder}`, `${workspaceRoot}`, `${workspaceFolderBasename}`, `${workspaceRootFolderName}`, `${cwd}`, or `${pathSeparator}` | Resolve the variable and write its current value | Review the resulting value before sharing the destination file or using it on another machine. |
|
||||
| `gallery`, `version`, `dev`, or `sandboxEnabled` | Migrate while removing these properties | Explain the removal and decide how to handle updates, development behavior, or sandboxing after migration. |
|
||||
| `${input:...}`, `${config:...}`, `${command:...}`, or other interactive VS Code variables | Do not migrate automatically | Reconfigure the value for the destination harness. Never copy a resolved secret into the MCP file. |
|
||||
| `${env:NAME}` | Do not migrate automatically | Use `$NAME`, `${NAME}`, or `${NAME:-default}`, and define the variable in the Agent Host environment. |
|
||||
| `cwd` | Do not migrate automatically | Add `cwd` manually only when the server requires it, and verify the path on the Agent Host machine. |
|
||||
| `envFile` | Do not migrate automatically | Export the required variables in the Agent Host environment and reference them from `env`. Never copy secret values into the MCP file. |
|
||||
| SSE transport | Do not migrate automatically | Use `type: "sse"` only when the server does not support Streamable HTTP. SSE is deprecated. |
|
||||
| A VS Code `oauth` object | Do not migrate automatically | Remove the nested object to use OAuth discovery, or translate supported client settings to the flat destination OAuth fields. Authenticate when prompted. |
|
||||
| Environment variables with `null` values | Do not migrate automatically | Remove the entry or provide a supported value. |
|
||||
| Additional VS Code-specific properties | Do not migrate automatically | Remove or replace the unsupported property before retrying. |
|
||||
| A different server with the same name in the destination or another workspace root | Stop without changing the source | Rename or remove the conflicting server, and then retry. |
|
||||
|
||||
Input variables require manual configuration. VS Code can prompt for values such as API keys through `${input:api-key}`, but the destination MCP format does not use that input flow. Ask the user how the value should be supplied securely in the Agent Host environment.
|
||||
|
||||
## Custom Locations
|
||||
|
||||
Agents, skill, and instruction files located in custom locations defined by settings `chat.agentSkillsLocations`, `chat.instructionsFilesLocations`, and `chat.agentFilesLocations` can continue to work when the selected harness is connected to VS Code. Moving them to a harness-reported standard location may improve portability or consistency, but is not automatically required for compatibility. Explain the trade-off rather than presenting the move as mandatory.
|
||||
|
||||
Handle only items reported under `configuredLocations` and not already handled under another inventory category. Do not re-ask about files the user explicitly skipped. Treat copying/moving a file, deleting its old copy, and clearing or changing the location setting as separate actions requiring separate approval.
|
||||
|
||||
## Verification
|
||||
|
||||
After each migration group:
|
||||
|
||||
1. Confirm the item appears in the Agent Customizations editor for the selected harness.
|
||||
2. Start a session with that harness and run a representative task.
|
||||
3. Confirm MCP servers start and expose the expected tools.
|
||||
4. Review version-control changes for workspace migrations.
|
||||
5. Report structural validation separately from runtime validation. If the selected harness cannot be exercised in this session, mark runtime validation as pending and keep source files and entries.
|
||||
6. Only after successful destination validation, ask for explicit approval before removing old files, MCP entries, or settings. Approval to migrate is not approval to clean up.
|
||||
Generated
+15
-15
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "copilot-chat",
|
||||
"version": "0.69.0",
|
||||
"version": "0.70.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "copilot-chat",
|
||||
"version": "0.69.0",
|
||||
"version": "0.70.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"dependencies": {
|
||||
@@ -143,7 +143,7 @@
|
||||
"engines": {
|
||||
"node": ">=22.14.0",
|
||||
"npm": ">=9.0.0",
|
||||
"vscode": "^1.141.0"
|
||||
"vscode": "^1.142.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
@@ -266,9 +266,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-client/node_modules/@typespec/ts-http-runtime": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz",
|
||||
"integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==",
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz",
|
||||
"integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -277,7 +277,7 @@
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http-compat": {
|
||||
@@ -387,9 +387,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-util/node_modules/@typespec/ts-http-runtime": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz",
|
||||
"integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==",
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz",
|
||||
"integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
@@ -397,7 +397,7 @@
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity": {
|
||||
@@ -520,9 +520,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/logger/node_modules/@typespec/ts-http-runtime": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz",
|
||||
"integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==",
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz",
|
||||
"integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
@@ -530,7 +530,7 @@
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-browser": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "copilot-chat",
|
||||
"displayName": "GitHub Copilot",
|
||||
"description": "AI chat features powered by Copilot",
|
||||
"version": "0.69.0",
|
||||
"version": "0.70.0",
|
||||
"build": "1",
|
||||
"completionsCoreVersion": "1.378.1799",
|
||||
"internalLargeStorageAriaKey": "ec712b3202c5462fb6877acae7f1f9d7-c19ad55e-3e3c-4f99-984b-827f6d95bd9e-6917",
|
||||
@@ -22,7 +22,7 @@
|
||||
"icon": "assets/copilot.png",
|
||||
"pricing": "Trial",
|
||||
"engines": {
|
||||
"vscode": "^1.141.0",
|
||||
"vscode": "^1.142.0",
|
||||
"npm": ">=9.0.0",
|
||||
"node": ">=22.14.0"
|
||||
},
|
||||
@@ -6866,6 +6866,14 @@
|
||||
"copilotcli"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "./assets/prompts/skills/migrate-customizations/SKILL.md",
|
||||
"sessionTypes": [
|
||||
"copilotcli",
|
||||
"claude",
|
||||
"codex"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "./assets/prompts/skills/init/SKILL.md",
|
||||
"sessionTypes": [
|
||||
|
||||
+2
-2
@@ -826,8 +826,8 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
|
||||
const fileSystemSetting = rawFileSystemSetting && typeof rawFileSystemSetting === 'object'
|
||||
? rawFileSystemSetting as IAgentSandboxFileSystemSettings
|
||||
: undefined;
|
||||
const allowedHosts = readStringArraySetting(this.configurationService, 'chat.agent.allowedNetworkDomains');
|
||||
const blockedHosts = readStringArraySetting(this.configurationService, 'chat.agent.deniedNetworkDomains');
|
||||
const allowedHosts = readStringArraySetting(this.configurationService, 'chat.agent.sandbox.network.allowedDomains');
|
||||
const blockedHosts = readStringArraySetting(this.configurationService, 'chat.agent.sandbox.network.deniedDomains');
|
||||
return buildSandboxConfigForCLI(process.platform, sandboxSetting, fileSystemSetting, { allowedHosts, blockedHosts });
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ export class ApiEmbeddingsIndex implements IApiEmbeddingsIndex {
|
||||
) {
|
||||
const cacheVersion = sanitizeVSCodeVersion(envService.getEditorInfo().version);
|
||||
this.embeddingsCache = useRemoteCache ?
|
||||
instantiationService.createInstance(RemoteEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'api', cacheVersion, EmbeddingType.text3small_512, RemoteCacheType.Api) :
|
||||
instantiationService.createInstance(LocalEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'api', cacheVersion, EmbeddingType.text3small_512);
|
||||
instantiationService.createInstance(RemoteEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'api', cacheVersion, EmbeddingType.metis_1024_I16_Binary, RemoteCacheType.Api) :
|
||||
instantiationService.createInstance(LocalEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'api', cacheVersion, EmbeddingType.metis_1024_I16_Binary);
|
||||
}
|
||||
|
||||
async updateIndex(): Promise<void> {
|
||||
@@ -95,7 +95,7 @@ export class VSCodeAPIContextElement extends PromptElement<VSCodeAPIContextProps
|
||||
return [];
|
||||
}
|
||||
|
||||
const embeddingResult = await this.embeddingsComputer.computeEmbeddings(EmbeddingType.text3small_512, [this.props.query], {}, new TelemetryCorrelationId('VSCodeAPIContextElement::getSnippets'), token);
|
||||
const embeddingResult = await this.embeddingsComputer.computeEmbeddings(EmbeddingType.metis_1024_I16_Binary, [this.props.query], { inputType: 'query' }, new TelemetryCorrelationId('VSCodeAPIContextElement::getSnippets'), token);
|
||||
if (embeddingResult.values.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AutoChatEndpoint } from '../../../platform/endpoint/node/autoChatEndpoi
|
||||
import { IAutomodeService } from '../../../platform/endpoint/node/automodeService';
|
||||
import { CopilotChatEndpoint, CopilotUtilityChatEndpoint, CopilotUtilitySmallChatEndpoint } from '../../../platform/endpoint/node/copilotChatEndpoint';
|
||||
import { EmbeddingEndpoint } from '../../../platform/endpoint/node/embeddingsEndpoint';
|
||||
import { IModelMetadataFetcher, ModelMetadataFetcher } from '../../../platform/endpoint/node/modelMetadataFetcher';
|
||||
import { EmbeddingsModelFamily, IModelMetadataFetcher, ModelMetadataFetcher } from '../../../platform/endpoint/node/modelMetadataFetcher';
|
||||
import { ExtensionContributedChatEndpoint } from '../../../platform/endpoint/vscode-node/extChatEndpoint';
|
||||
import { ILogService } from '../../../platform/log/common/logService';
|
||||
import { IChatEndpoint, IEmbeddingsEndpoint } from '../../../platform/networking/common/networking';
|
||||
@@ -20,6 +20,10 @@ import { Emitter, Event } from '../../../util/vs/base/common/event';
|
||||
import { Disposable } from '../../../util/vs/base/common/lifecycle';
|
||||
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
|
||||
|
||||
const embeddingsModelFamilies: Record<EmbeddingsEndpointFamily, EmbeddingsModelFamily> = {
|
||||
text3small: 'text-embedding-3-small',
|
||||
metis: 'metis',
|
||||
};
|
||||
|
||||
// Keep in sync with `BYOKUtilityModelDefault` in `src/vs/workbench/contrib/chat/common/constants.ts` and the `chat.byokUtilityModelDefault` enum in `chat.shared.contribution.ts`.
|
||||
const enum BYOKUtilityModelDefault {
|
||||
@@ -353,14 +357,14 @@ export class ProductionEndpointProvider extends Disposable implements IEndpointP
|
||||
|
||||
async getEmbeddingsEndpoint(family?: EmbeddingsEndpointFamily): Promise<IEmbeddingsEndpoint> {
|
||||
this._logService.trace(`Resolving embedding model`);
|
||||
const modelMetadata = await this._modelFetcher.getEmbeddingsModel('text-embedding-3-small');
|
||||
const modelMetadata = await this._modelFetcher.getEmbeddingsModel(embeddingsModelFamilies[family ?? 'metis']);
|
||||
const model = await this.getOrCreateEmbeddingEndpointInstance(modelMetadata);
|
||||
this._logService.trace(`Resolved embedding model`);
|
||||
return model;
|
||||
}
|
||||
|
||||
private async getOrCreateEmbeddingEndpointInstance(modelMetadata: IEmbeddingModelInformation): Promise<IEmbeddingsEndpoint> {
|
||||
const modelId = 'text-embedding-3-small';
|
||||
const modelId = modelMetadata.id;
|
||||
let embeddingEndpoint = this._embeddingEndpoints.get(modelId);
|
||||
if (!embeddingEndpoint) {
|
||||
embeddingEndpoint = this._instantiationService.createInstance(EmbeddingEndpoint, modelMetadata);
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export class SettingsEditorSearchServiceImpl implements ISettingsEditorSearchSer
|
||||
// Start searching for embedding results.
|
||||
let embeddingResult: Embeddings;
|
||||
try {
|
||||
embeddingResult = await this.embeddingsComputer.computeEmbeddings(EmbeddingType.text3small_512, [query], {}, new TelemetryCorrelationId('SettingsEditorSearchServiceImpl::provideSettingsSearchResults'), token);
|
||||
embeddingResult = await this.embeddingsComputer.computeEmbeddings(EmbeddingType.metis_1024_I16_Binary, [query], { inputType: 'query' }, new TelemetryCorrelationId('SettingsEditorSearchServiceImpl::provideSettingsSearchResults'), token);
|
||||
} catch {
|
||||
this.reportEmptyEmbeddingsResult(query, progress);
|
||||
if (!options.embeddingsOnly) {
|
||||
|
||||
@@ -105,7 +105,7 @@ export class NewWorkspacePrompt extends PromptElement<NewWorkspacePromptProps, N
|
||||
}
|
||||
else if (instruction.intent === 'Project') {
|
||||
if (this.props.useTemplates) {
|
||||
const result = await this.embeddingsComputer.computeEmbeddings(EmbeddingType.text3small_512, [instruction.question], {}, undefined);
|
||||
const result = await this.embeddingsComputer.computeEmbeddings(EmbeddingType.metis_1024_I16_Binary, [instruction.question], { inputType: 'query' }, undefined);
|
||||
if (result.values.length === 0) {
|
||||
return { intent: instruction };
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ export class VscodePrompt extends PromptElement<VscodePromptProps, VscodePromptS
|
||||
return { settings: [], commands: [], query: userQuery };
|
||||
}
|
||||
|
||||
const embeddingResult = await this.embeddingsComputer.computeEmbeddings(EmbeddingType.text3small_512, [userQuery], {}, undefined);
|
||||
const embeddingResult = await this.embeddingsComputer.computeEmbeddings(EmbeddingType.metis_1024_I16_Binary, [userQuery], { inputType: 'query' }, undefined);
|
||||
if (token.isCancellationRequested) {
|
||||
return { settings: [], commands: [], query: userQuery };
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ class FakeModelMetadataFetcher implements IModelMetadataFetcher {
|
||||
};
|
||||
}
|
||||
|
||||
async getEmbeddingsModel(): Promise<IEmbeddingModelInformation> {
|
||||
async getEmbeddingsModel(family: 'text-embedding-3-small' | 'metis'): Promise<IEmbeddingModelInformation> {
|
||||
return {
|
||||
id: 'text-embedding-3-small',
|
||||
id: family,
|
||||
name: 'fake-name',
|
||||
vendor: 'fake-vendor',
|
||||
version: 'fake-version',
|
||||
@@ -68,7 +68,7 @@ class FakeModelMetadataFetcher implements IModelMetadataFetcher {
|
||||
capabilities: {
|
||||
type: 'embeddings',
|
||||
tokenizer: TokenizerType.O200K,
|
||||
family: 'text-embedding-3-small',
|
||||
family,
|
||||
limits: { max_inputs: 256 }
|
||||
}
|
||||
};
|
||||
@@ -102,6 +102,26 @@ suite('Endpoint Class Test', function () {
|
||||
|
||||
assert.strictEqual(endpoint.model, 'gpt-5.4-nano');
|
||||
});
|
||||
|
||||
test('resolves embedding endpoints by family with Metis as the default', async function () {
|
||||
const defaultEndpoint = await endpointProvider.getEmbeddingsEndpoint();
|
||||
const metisEndpoint = await endpointProvider.getEmbeddingsEndpoint('metis');
|
||||
const text3smallEndpoint = await endpointProvider.getEmbeddingsEndpoint('text3small');
|
||||
|
||||
assert.deepStrictEqual({
|
||||
defaultFamily: defaultEndpoint.family,
|
||||
metisFamily: metisEndpoint.family,
|
||||
text3smallFamily: text3smallEndpoint.family,
|
||||
reusesMetisEndpoint: defaultEndpoint === metisEndpoint,
|
||||
keepsFamiliesSeparate: metisEndpoint !== text3smallEndpoint,
|
||||
}, {
|
||||
defaultFamily: 'metis',
|
||||
metisFamily: 'metis',
|
||||
text3smallFamily: 'text-embedding-3-small',
|
||||
reusesMetisEndpoint: true,
|
||||
keepsFamiliesSeparate: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class CopilotMatchableModelMetadataFetcher implements IModelMetadataFetcher {
|
||||
@@ -122,9 +142,9 @@ class CopilotMatchableModelMetadataFetcher implements IModelMetadataFetcher {
|
||||
async getChatModelFromCapiFamily(family: string): Promise<IChatModelInformation> {
|
||||
return makeChatModel(family);
|
||||
}
|
||||
async getEmbeddingsModel(): Promise<IEmbeddingModelInformation> {
|
||||
async getEmbeddingsModel(family: 'text-embedding-3-small' | 'metis'): Promise<IEmbeddingModelInformation> {
|
||||
return {
|
||||
id: 'text-embedding-3-small',
|
||||
id: family,
|
||||
name: 'fake-name',
|
||||
vendor: 'fake-vendor',
|
||||
version: 'fake-version',
|
||||
@@ -134,7 +154,7 @@ class CopilotMatchableModelMetadataFetcher implements IModelMetadataFetcher {
|
||||
capabilities: {
|
||||
type: 'embeddings',
|
||||
tokenizer: TokenizerType.O200K,
|
||||
family: 'text-embedding-3-small',
|
||||
family,
|
||||
limits: { max_inputs: 256 }
|
||||
}
|
||||
};
|
||||
|
||||
+17
-12
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Embedding, EmbeddingType } from '../../../../platform/embeddings/common/embeddingsComputer';
|
||||
import { Embedding, EmbeddingType, EmbeddingVector } from '../../../../platform/embeddings/common/embeddingsComputer';
|
||||
import { EmbeddingCacheType, IEmbeddingsCache, RemoteCacheType, RemoteEmbeddingsCache } from '../../../../platform/embeddings/common/embeddingsIndex';
|
||||
import { IEnvService } from '../../../../platform/env/common/envService';
|
||||
import { ILogService } from '../../../../platform/log/common/logService';
|
||||
@@ -11,7 +11,11 @@ import { sanitizeVSCodeVersion } from '../../../../util/common/vscodeVersion';
|
||||
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
|
||||
import { IToolEmbeddingsCache } from './toolEmbeddingsComputer';
|
||||
|
||||
export const EMBEDDING_TYPE_FOR_TOOL_GROUPING = EmbeddingType.text3small_512;
|
||||
export const EMBEDDING_TYPE_FOR_TOOL_GROUPING = EmbeddingType.metis_1024_I16_Binary;
|
||||
|
||||
type ToolEmbeddingCacheData =
|
||||
| Record<string, { readonly embedding: EmbeddingVector }>
|
||||
| readonly { readonly key: string; readonly embedding: EmbeddingVector }[];
|
||||
|
||||
export class PreComputedToolEmbeddingsCache implements IToolEmbeddingsCache {
|
||||
private readonly cache: IEmbeddingsCache;
|
||||
@@ -44,18 +48,21 @@ export class PreComputedToolEmbeddingsCache implements IToolEmbeddingsCache {
|
||||
|
||||
private async _loadEmbeddings() {
|
||||
try {
|
||||
const embeddingsData = await this.cache.getCache();
|
||||
const embeddingsData = await this.cache.getCache<ToolEmbeddingCacheData>();
|
||||
const embeddingsMap = new Map<string, Embedding>();
|
||||
|
||||
if (embeddingsData) {
|
||||
for (const [key, embeddingVector] of Object.entries(embeddingsData)) {
|
||||
if (embeddingVector === undefined) {
|
||||
this._logService.warn(`Tool embedding missing for key: ${key}`);
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(embeddingsData)) {
|
||||
for (const entry of embeddingsData) {
|
||||
embeddingsMap.set(entry.key, {
|
||||
type: this.embeddingType,
|
||||
value: entry.embedding
|
||||
});
|
||||
}
|
||||
} else if (embeddingsData) {
|
||||
for (const [key, entry] of Object.entries(embeddingsData)) {
|
||||
embeddingsMap.set(key, {
|
||||
type: this.embeddingType,
|
||||
value: embeddingVector.embedding
|
||||
value: entry.embedding
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -67,5 +74,3 @@ export class PreComputedToolEmbeddingsCache implements IToolEmbeddingsCache {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -116,7 +116,7 @@ export class ToolEmbeddingsComputer implements IToolEmbeddingsComputer {
|
||||
return [];
|
||||
}
|
||||
|
||||
const queryEmbedding = await this._embeddingsComputer.computeEmbeddings(this._embeddingType, [query], {}, new TelemetryCorrelationId('ToolEmbeddingsComputer::searchToolsByQuery'), token);
|
||||
const queryEmbedding = await this._embeddingsComputer.computeEmbeddings(this._embeddingType, [query], { inputType: 'query' }, new TelemetryCorrelationId('ToolEmbeddingsComputer::searchToolsByQuery'), token);
|
||||
if (!queryEmbedding || queryEmbedding.values.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -184,7 +184,7 @@ export class ToolEmbeddingsComputer implements IToolEmbeddingsComputer {
|
||||
|
||||
const toolNames = tools.map(t => t.name + '\n\n' + t.description);
|
||||
const start = new StopWatch();
|
||||
const embeddings = await this._embeddingsComputer.computeEmbeddings(this._embeddingType, toolNames, {}, new TelemetryCorrelationId('ToolEmbeddingsComputer::computeEmbeddingsForTools'), token);
|
||||
const embeddings = await this._embeddingsComputer.computeEmbeddings(this._embeddingType, toolNames, { inputType: 'document' }, new TelemetryCorrelationId('ToolEmbeddingsComputer::computeEmbeddingsForTools'), token);
|
||||
this._logService.trace(`[virtual-tools] Computed embeddings for ${toolNames.length} tools in ${start.elapsed()}ms`);
|
||||
|
||||
if (embeddings?.values.length === 0 || embeddings?.values.length !== toolNames.length) {
|
||||
|
||||
@@ -351,7 +351,7 @@ export class VirtualToolGrouper implements IToolCategorization {
|
||||
}
|
||||
|
||||
// compute the embeddings for the query
|
||||
const queryEmbedding = await this.embeddingsComputer.computeEmbeddings(EMBEDDING_TYPE_FOR_TOOL_GROUPING, [query], {}, new TelemetryCorrelationId('VirtualToolGrouper::_getPredictedTools'), token);
|
||||
const queryEmbedding = await this.embeddingsComputer.computeEmbeddings(EMBEDDING_TYPE_FOR_TOOL_GROUPING, [query], { inputType: 'query' }, new TelemetryCorrelationId('VirtualToolGrouper::_getPredictedTools'), token);
|
||||
if (!queryEmbedding || queryEmbedding.values.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { EmbeddingType, EmbeddingVector } from '../../../../../platform/embeddings/common/embeddingsComputer';
|
||||
import { IEmbeddingsCache } from '../../../../../platform/embeddings/common/embeddingsIndex';
|
||||
import { NullEnvService } from '../../../../../platform/env/common/nullEnvService';
|
||||
import { TestLogService } from '../../../../../platform/testing/common/testLogService';
|
||||
import { mock } from '../../../../../util/common/test/simpleMock';
|
||||
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
|
||||
import { PreComputedToolEmbeddingsCache } from '../../../common/virtualTools/preComputedToolEmbeddingsCache';
|
||||
|
||||
function createCache(data: readonly { readonly key: string; readonly embedding: EmbeddingVector }[] | Record<string, { readonly embedding: EmbeddingVector }>): PreComputedToolEmbeddingsCache {
|
||||
const embeddingsCache = new class extends mock<IEmbeddingsCache>() {
|
||||
override readonly embeddingType = EmbeddingType.metis_1024_I16_Binary;
|
||||
|
||||
override async getCache<T>(): Promise<T> {
|
||||
return data as T;
|
||||
}
|
||||
}();
|
||||
const instantiationService = new class extends mock<IInstantiationService>() { }();
|
||||
instantiationService.createInstance = (() => embeddingsCache) as IInstantiationService['createInstance'];
|
||||
return new PreComputedToolEmbeddingsCache(new TestLogService(), instantiationService, NullEnvService.Instance);
|
||||
}
|
||||
|
||||
describe('PreComputedToolEmbeddingsCache', () => {
|
||||
test('loads published array entries by tool name', async () => {
|
||||
const cache = createCache([{ key: 'click_element', embedding: [1, 0] }]);
|
||||
|
||||
await cache.initialize();
|
||||
|
||||
expect(cache.get({ name: 'click_element' })).toEqual({
|
||||
type: EmbeddingType.metis_1024_I16_Binary,
|
||||
value: [1, 0],
|
||||
});
|
||||
});
|
||||
|
||||
test('loads legacy object entries by tool name', async () => {
|
||||
const cache = createCache({ click_element: { embedding: [1, 0] } });
|
||||
|
||||
await cache.initialize();
|
||||
|
||||
expect(cache.get({ name: 'click_element' })).toEqual({
|
||||
type: EmbeddingType.metis_1024_I16_Binary,
|
||||
value: [1, 0],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,7 @@ export enum RemoteCacheType {
|
||||
// These values are the blob storage container names where we publish computed embeddings
|
||||
enum RemoteEmbeddingsContainer {
|
||||
TEXT3SMALL = 'text-3-small',
|
||||
METIS_1024_I16_BINARY = 'metis-1024-I16-Binary'
|
||||
METIS_1024_I16_BINARY = 'metis-1024-i16-binary'
|
||||
}
|
||||
|
||||
function embeddingsModelToRemoteContainer(embeddingType: EmbeddingType): RemoteEmbeddingsContainer {
|
||||
@@ -150,8 +150,8 @@ class EmbeddingsCache {
|
||||
await this.fileSystemService.stat(this.cacheStorageUri);
|
||||
await this.fileSystemService.delete(cacheFile, { useTrash: false });
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
throw new Error(`Cache file ${cacheFile} does not exist`);
|
||||
if (e.code !== 'ENOENT' && e.code !== 'FileNotFound') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@ export class LocalEmbeddingsCache implements IEmbeddingsCache {
|
||||
this._embeddingsCache = instantiationService.createInstance(
|
||||
EmbeddingsCache,
|
||||
cacheType,
|
||||
cacheKey,
|
||||
`${cacheKey}-${embeddingType.id}`,
|
||||
cacheVersion
|
||||
);
|
||||
}
|
||||
@@ -235,13 +235,14 @@ export class RemoteEmbeddingsCache implements IEmbeddingsCache {
|
||||
@IFetcherService protected readonly fetcherService: IFetcherService,
|
||||
@IInstantiationService instantiationService: IInstantiationService
|
||||
) {
|
||||
const modelCacheKey = `${cacheKey}-${embeddingType.id}`;
|
||||
this.embeddingsCache = instantiationService.createInstance(
|
||||
EmbeddingsCache,
|
||||
cacheType,
|
||||
cacheKey,
|
||||
modelCacheKey,
|
||||
cacheVersion
|
||||
);
|
||||
this.remoteCacheVersionKey = `${cacheKey}-version-remote`;
|
||||
this.remoteCacheVersionKey = `${modelCacheKey}-version-remote`;
|
||||
}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { IEmbeddingsEndpoint, postRequest } from '../../networking/common/networ
|
||||
import { GenAiAttr, GenAiOperationName, GenAiProviderName } from '../../otel/common/genAiAttributes';
|
||||
import { IOTelService, SpanKind, SpanStatusCode } from '../../otel/common/otelService';
|
||||
import { ITelemetryService } from '../../telemetry/common/telemetry';
|
||||
import { ComputeEmbeddingsOptions, Embedding, EmbeddingType, EmbeddingTypeInfo, EmbeddingVector, Embeddings, IEmbeddingsComputer, getWellKnownEmbeddingTypeInfo } from './embeddingsComputer';
|
||||
import { ComputeEmbeddingsOptions, Embedding, EmbeddingInputType, EmbeddingType, EmbeddingTypeInfo, EmbeddingVector, Embeddings, IEmbeddingsComputer, LEGACY_EMBEDDING_MODEL_ID, getWellKnownEmbeddingTypeInfo } from './embeddingsComputer';
|
||||
|
||||
interface CAPIEmbeddingResults {
|
||||
readonly type: 'success';
|
||||
@@ -72,7 +72,7 @@ export class RemoteEmbeddingsComputer implements IEmbeddingsComputer {
|
||||
// Determine endpoint type: use CAPI for no-auth users, otherwise use GitHub
|
||||
const copilotToken = await this._authService.getCopilotToken();
|
||||
if (copilotToken.isNoAuthUser) {
|
||||
const embeddings = await this.computeCAPIEmbeddings(inputs, options, cancellationToken);
|
||||
const embeddings = await this.computeCAPIEmbeddings(embeddingType, inputs, options, cancellationToken);
|
||||
return embeddings ?? { type: embeddingType, values: [] };
|
||||
}
|
||||
|
||||
@@ -168,19 +168,21 @@ export class RemoteEmbeddingsComputer implements IEmbeddingsComputer {
|
||||
}
|
||||
|
||||
private async computeCAPIEmbeddings(
|
||||
embeddingType: EmbeddingType,
|
||||
inputs: readonly string[],
|
||||
options?: ComputeEmbeddingsOptions,
|
||||
cancellationToken?: CancellationToken,
|
||||
) {
|
||||
const typeInfo = getWellKnownEmbeddingTypeInfo(EmbeddingType.text3small_512);
|
||||
const typeInfo = getWellKnownEmbeddingTypeInfo(embeddingType);
|
||||
if (!typeInfo) {
|
||||
throw new Error(`Embeddings type info not found: ${EmbeddingType.text3small_512}`);
|
||||
throw new Error(`Embeddings type info not found: ${embeddingType}`);
|
||||
}
|
||||
const endpoint = await this._endpointProvider.getEmbeddingsEndpoint('text3small');
|
||||
const endpointFamily = typeInfo.model === LEGACY_EMBEDDING_MODEL_ID.Metis_I16_Binary ? 'metis' : 'text3small';
|
||||
const endpoint = await this._endpointProvider.getEmbeddingsEndpoint(endpointFamily);
|
||||
const batchSize = endpoint.maxBatchSize;
|
||||
// Open AI seems to allow 1 less than max tokens for the model requests. So if the max tokens is 8192, we can only send 8191 tokens.
|
||||
const maxTokens = endpoint.modelMaxPromptTokens - 1;
|
||||
return this.fetchResponseWithBatches(typeInfo, endpoint, inputs, cancellationToken, maxTokens, batchSize);
|
||||
return this.fetchResponseWithBatches(embeddingType, typeInfo, endpoint, inputs, options?.inputType ?? 'document', cancellationToken, maxTokens, batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,9 +193,11 @@ export class RemoteEmbeddingsComputer implements IEmbeddingsComputer {
|
||||
* @returns The embeddings
|
||||
*/
|
||||
private async fetchResponseWithBatches(
|
||||
embeddingType: EmbeddingType,
|
||||
type: EmbeddingTypeInfo,
|
||||
endpoint: IEmbeddingsEndpoint,
|
||||
inputs: readonly string[],
|
||||
inputType: EmbeddingInputType,
|
||||
cancellationToken: CancellationToken | undefined,
|
||||
maxTokens: number,
|
||||
batchSize: number,
|
||||
@@ -218,7 +222,7 @@ export class RemoteEmbeddingsComputer implements IEmbeddingsComputer {
|
||||
return;
|
||||
}
|
||||
|
||||
const r = await this.rawEmbeddingsFetchWithTelemetry(type, endpoint, generateUuid(), currentBatch, cancellationToken);
|
||||
const r = await this.rawEmbeddingsFetchWithTelemetry(type, endpoint, generateUuid(), currentBatch, inputType, cancellationToken);
|
||||
if (r.type === 'failed') {
|
||||
throw new Error('Embeddings request failed ' + r.reason);
|
||||
}
|
||||
@@ -241,7 +245,7 @@ export class RemoteEmbeddingsComputer implements IEmbeddingsComputer {
|
||||
if (embeddings.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return { type: EmbeddingType.text3small_512, values: embeddings.map((value): Embedding => ({ type: EmbeddingType.text3small_512, value })) };
|
||||
return { type: embeddingType, values: embeddings.map((value): Embedding => ({ type: embeddingType, value })) };
|
||||
}
|
||||
|
||||
private async rawEmbeddingsFetchWithTelemetry(
|
||||
@@ -249,10 +253,11 @@ export class RemoteEmbeddingsComputer implements IEmbeddingsComputer {
|
||||
endpoint: IEmbeddingsEndpoint,
|
||||
requestId: string,
|
||||
inputs: readonly string[],
|
||||
inputType: EmbeddingInputType,
|
||||
cancellationToken: CancellationToken | undefined
|
||||
) {
|
||||
const startTime = Date.now();
|
||||
const rawRequest = await this.rawEmbeddingsFetch(type, endpoint, requestId, inputs, cancellationToken);
|
||||
const rawRequest = await this.rawEmbeddingsFetch(type, endpoint, requestId, inputs, inputType, cancellationToken);
|
||||
if (rawRequest.type === 'failed') {
|
||||
this._telemetryService.sendMSFTTelemetryErrorEvent('embedding.error', {
|
||||
type: rawRequest.type,
|
||||
@@ -281,12 +286,18 @@ export class RemoteEmbeddingsComputer implements IEmbeddingsComputer {
|
||||
endpoint: IEmbeddingsEndpoint,
|
||||
requestId: string,
|
||||
inputs: readonly string[],
|
||||
inputType: EmbeddingInputType,
|
||||
cancellationToken: CancellationToken | undefined
|
||||
): Promise<CAPIEmbeddingResults | CAPIEmbeddingError> {
|
||||
try {
|
||||
const token = await this._authService.getCopilotToken();
|
||||
|
||||
const body = { input: inputs, model: type.model, dimensions: type.dimensions };
|
||||
const body = {
|
||||
input: inputs,
|
||||
input_type: inputType,
|
||||
model: type.model,
|
||||
dimensions: type.dimensions
|
||||
};
|
||||
endpoint.interceptBody?.(body);
|
||||
const response = await this._instantiationService.invokeFunction(postRequest, {
|
||||
endpointOrUrl: endpoint,
|
||||
|
||||
@@ -108,7 +108,7 @@ abstract class RelatedInformationProviderEmbeddingsIndex<V extends { key: string
|
||||
return [];
|
||||
}
|
||||
const startOfEmbeddingRequest = Date.now();
|
||||
const embeddingResult = await this.embeddingsComputer.computeEmbeddings(EmbeddingType.text3small_512, [query], {}, new TelemetryCorrelationId('RelatedInformationProviderEmbeddingsIndex::provideRelatedInformation'), token);
|
||||
const embeddingResult = await this.embeddingsComputer.computeEmbeddings(EmbeddingType.metis_1024_I16_Binary, [query], { inputType: 'query' }, new TelemetryCorrelationId('RelatedInformationProviderEmbeddingsIndex::provideRelatedInformation'), token);
|
||||
this._logService.debug(`Related Information: Remote similarly request took ${Date.now() - startOfEmbeddingRequest}ms`);
|
||||
if (token.isCancellationRequested) {
|
||||
// return an array of 0s the same length as comparisons
|
||||
@@ -127,7 +127,7 @@ abstract class RelatedInformationProviderEmbeddingsIndex<V extends { key: string
|
||||
break;
|
||||
}
|
||||
if (item.embedding) {
|
||||
const score = distance(embeddingResult.values[0], { value: item.embedding, type: EmbeddingType.text3small_512 }).value;
|
||||
const score = distance(embeddingResult.values[0], { value: item.embedding, type: EmbeddingType.metis_1024_I16_Binary }).value;
|
||||
if (score > this.relatedInformationConfig.threshold) {
|
||||
results.push(this.toRelatedInformation(item, score));
|
||||
}
|
||||
@@ -161,13 +161,13 @@ class CommandIdIndex extends RelatedInformationProviderEmbeddingsIndex<CommandLi
|
||||
) {
|
||||
super(
|
||||
'CommandIdIndex',
|
||||
EmbeddingType.text3small_512,
|
||||
EmbeddingType.metis_1024_I16_Binary,
|
||||
'commandEmbeddings',
|
||||
embeddingsFetcher,
|
||||
embeddingscache,
|
||||
{
|
||||
type: RelatedInformationType.CommandInformation,
|
||||
threshold: /* min threshold of 0 for text-3-small*/ 0,
|
||||
threshold: 0,
|
||||
maxResults: 100,
|
||||
},
|
||||
logService,
|
||||
@@ -222,13 +222,13 @@ class SettingsIndex extends RelatedInformationProviderEmbeddingsIndex<SettingLis
|
||||
) {
|
||||
super(
|
||||
'SettingsIndex',
|
||||
EmbeddingType.text3small_512,
|
||||
EmbeddingType.metis_1024_I16_Binary,
|
||||
'settingEmbeddings',
|
||||
embeddingsFetcher,
|
||||
embeddingsCache,
|
||||
{
|
||||
type: RelatedInformationType.SettingInformation,
|
||||
threshold: /* min threshold of 0 for text-3-small*/ 0,
|
||||
threshold: 0,
|
||||
maxResults: 100,
|
||||
},
|
||||
logService,
|
||||
@@ -291,11 +291,11 @@ export class VSCodeCombinedIndexImpl implements ICombinedEmbeddingIndex {
|
||||
) {
|
||||
// Local embeddings cache version is locked to 1.98
|
||||
const settingsEmbeddingsCache = useRemoteCache ?
|
||||
instantiationService.createInstance(RemoteEmbeddingsExtensionCache, EmbeddingCacheType.GLOBAL, 'settingEmbeddings', sanitizeVSCodeVersion(envService.getEditorInfo().version), EmbeddingType.text3small_512, RemoteCacheType.Settings) :
|
||||
instantiationService.createInstance(LocalEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'settingEmbeddings', '1.98', EmbeddingType.text3small_512);
|
||||
instantiationService.createInstance(RemoteEmbeddingsExtensionCache, EmbeddingCacheType.GLOBAL, 'settingEmbeddings', sanitizeVSCodeVersion(envService.getEditorInfo().version), EmbeddingType.metis_1024_I16_Binary, RemoteCacheType.Settings) :
|
||||
instantiationService.createInstance(LocalEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'settingEmbeddings', '1.98', EmbeddingType.metis_1024_I16_Binary);
|
||||
const commandsEmbeddingsCache = useRemoteCache ?
|
||||
instantiationService.createInstance(RemoteEmbeddingsExtensionCache, EmbeddingCacheType.GLOBAL, 'commandEmbeddings', sanitizeVSCodeVersion(envService.getEditorInfo().version), EmbeddingType.text3small_512, RemoteCacheType.Commands) :
|
||||
instantiationService.createInstance(LocalEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'commandEmbeddings', '1.98', EmbeddingType.text3small_512);
|
||||
instantiationService.createInstance(RemoteEmbeddingsExtensionCache, EmbeddingCacheType.GLOBAL, 'commandEmbeddings', sanitizeVSCodeVersion(envService.getEditorInfo().version), EmbeddingType.metis_1024_I16_Binary, RemoteCacheType.Commands) :
|
||||
instantiationService.createInstance(LocalEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'commandEmbeddings', '1.98', EmbeddingType.metis_1024_I16_Binary);
|
||||
|
||||
this.settingsIndex = instantiationService.createInstance(SettingsIndex, settingsEmbeddingsCache);
|
||||
this.commandIdIndex = instantiationService.createInstance(CommandIdIndex, commandsEmbeddingsCache);
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { createTestExtendedTokenInfo, CopilotToken } from '../../../authentication/common/copilotToken';
|
||||
import { IAuthenticationService } from '../../../authentication/common/authentication';
|
||||
import { IEndpointProvider } from '../../../endpoint/common/endpointProvider';
|
||||
import { NullEnvService } from '../../../env/common/nullEnvService';
|
||||
import { IEmbeddingsEndpoint } from '../../../networking/common/networking';
|
||||
import { NoopOTelService } from '../../../otel/common/noopOtelService';
|
||||
import { resolveOTelConfig } from '../../../otel/common/otelConfig';
|
||||
import { NullTelemetryService } from '../../../telemetry/common/nullTelemetryService';
|
||||
import { TestLogService } from '../../../testing/common/testLogService';
|
||||
import { mock } from '../../../../util/common/test/simpleMock';
|
||||
import { ITokenizer } from '../../../../util/common/tokenizer';
|
||||
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
|
||||
import { EmbeddingType, LEGACY_EMBEDDING_MODEL_ID } from '../../common/embeddingsComputer';
|
||||
import { RemoteEmbeddingsComputer } from '../../common/remoteEmbeddingsComputer';
|
||||
|
||||
class TestAuthenticationService extends mock<IAuthenticationService>() {
|
||||
override readonly hasCopilotTokenSource = true;
|
||||
|
||||
override async getCopilotToken(): Promise<CopilotToken> {
|
||||
return new CopilotToken(createTestExtendedTokenInfo({ sku: 'no_auth_limited_copilot' }));
|
||||
}
|
||||
}
|
||||
|
||||
class TestTokenizer extends mock<ITokenizer>() {
|
||||
override async tokenLength(): Promise<number> {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
class TestEmbeddingsEndpoint extends mock<IEmbeddingsEndpoint>() {
|
||||
override readonly maxBatchSize = 100;
|
||||
override readonly modelMaxPromptTokens = 8192;
|
||||
|
||||
override acquireTokenizer(): ITokenizer {
|
||||
return new TestTokenizer();
|
||||
}
|
||||
}
|
||||
|
||||
class TestEndpointProvider extends mock<IEndpointProvider>() {
|
||||
readonly requestedFamilies: Array<string | undefined> = [];
|
||||
|
||||
override async getEmbeddingsEndpoint(family?: 'text3small' | 'metis'): Promise<IEmbeddingsEndpoint> {
|
||||
this.requestedFamilies.push(family);
|
||||
return new TestEmbeddingsEndpoint();
|
||||
}
|
||||
}
|
||||
|
||||
describe('RemoteEmbeddingsComputer', () => {
|
||||
function createComputer(endpointProvider: IEndpointProvider): RemoteEmbeddingsComputer {
|
||||
return new RemoteEmbeddingsComputer(
|
||||
new TestAuthenticationService(),
|
||||
NullEnvService.Instance,
|
||||
new TestLogService(),
|
||||
new NullTelemetryService(),
|
||||
endpointProvider,
|
||||
new class extends mock<IInstantiationService>() { }(),
|
||||
new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '1.0.0', sessionId: 'test' })),
|
||||
);
|
||||
}
|
||||
|
||||
test('uses the Metis CAPI endpoint for Metis embeddings', async () => {
|
||||
const endpointProvider = new TestEndpointProvider();
|
||||
const computer = createComputer(endpointProvider);
|
||||
const fetch = vi.spyOn(computer, 'rawEmbeddingsFetch').mockResolvedValue({
|
||||
type: 'success',
|
||||
embeddings: [[1, 0]],
|
||||
});
|
||||
|
||||
const result = await computer.computeEmbeddings(EmbeddingType.metis_1024_I16_Binary, ['input'], { inputType: 'query' });
|
||||
|
||||
expect({
|
||||
requestedFamilies: endpointProvider.requestedFamilies,
|
||||
requestedModel: fetch.mock.calls[0][0].model,
|
||||
inputType: fetch.mock.calls[0][4],
|
||||
resultType: result.type.id,
|
||||
valueTypes: result.values.map(value => value.type.id),
|
||||
}).toEqual({
|
||||
requestedFamilies: ['metis'],
|
||||
requestedModel: LEGACY_EMBEDDING_MODEL_ID.Metis_I16_Binary,
|
||||
inputType: 'query',
|
||||
resultType: EmbeddingType.metis_1024_I16_Binary.id,
|
||||
valueTypes: [EmbeddingType.metis_1024_I16_Binary.id],
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves the text3small endpoint for text3small embeddings', async () => {
|
||||
const endpointProvider = new TestEndpointProvider();
|
||||
const computer = createComputer(endpointProvider);
|
||||
const fetch = vi.spyOn(computer, 'rawEmbeddingsFetch').mockResolvedValue({
|
||||
type: 'success',
|
||||
embeddings: [[1, 0]],
|
||||
});
|
||||
|
||||
const result = await computer.computeEmbeddings(EmbeddingType.text3small_512, ['input']);
|
||||
|
||||
expect({
|
||||
requestedFamilies: endpointProvider.requestedFamilies,
|
||||
requestedModel: fetch.mock.calls[0][0].model,
|
||||
inputType: fetch.mock.calls[0][4],
|
||||
resultType: result.type.id,
|
||||
}).toEqual({
|
||||
requestedFamilies: ['text3small'],
|
||||
requestedModel: LEGACY_EMBEDDING_MODEL_ID.TEXT3SMALL,
|
||||
inputType: 'document',
|
||||
resultType: EmbeddingType.text3small_512.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,8 @@ import { IExperimentationService } from '../../telemetry/common/nullExperimentat
|
||||
import { getModelCapabilityOverride } from '../common/chatModelCapabilities';
|
||||
import { IChatModelInformation, ICompletionModelInformation, IEmbeddingModelInformation, IModelAPIResponse, isChatModelInformation, isCompletionModelInformation, isEmbeddingModelInformation } from '../common/endpointProvider';
|
||||
|
||||
export type EmbeddingsModelFamily = 'text-embedding-3-small' | 'metis';
|
||||
|
||||
export interface IModelMetadataFetcher {
|
||||
|
||||
/**
|
||||
@@ -67,7 +69,7 @@ export interface IModelMetadataFetcher {
|
||||
* Retrieves an embeddings model by its family name
|
||||
* @param family The family of the model to fetch
|
||||
*/
|
||||
getEmbeddingsModel(family: 'text-embedding-3-small'): Promise<IEmbeddingModelInformation>;
|
||||
getEmbeddingsModel(family: EmbeddingsModelFamily): Promise<IEmbeddingModelInformation>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,7 +221,7 @@ export class ModelMetadataFetcher extends Disposable implements IModelMetadataFe
|
||||
return resolvedModel;
|
||||
}
|
||||
|
||||
public async getEmbeddingsModel(family: 'text-embedding-3-small'): Promise<IEmbeddingModelInformation> {
|
||||
public async getEmbeddingsModel(family: EmbeddingsModelFamily): Promise<IEmbeddingModelInformation> {
|
||||
await this._taskSingler.getOrCreate(ModelMetadataFetcher.ALL_MODEL_KEY, this._fetchModels.bind(this));
|
||||
const resolvedModel = this._familyMap.get(family)?.[0];
|
||||
if (!resolvedModel || !isEmbeddingModelInformation(resolvedModel)) {
|
||||
|
||||
+2
-2
@@ -37,8 +37,8 @@ export class ProjectTemplatesIndex implements IProjectTemplatesIndex {
|
||||
) {
|
||||
const cacheVersion = sanitizeVSCodeVersion(envService.getEditorInfo().version);
|
||||
this.embeddingsCache = useRemoteCache ?
|
||||
instantiationService.createInstance(RemoteEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'projectTemplateEmbeddings', cacheVersion, EmbeddingType.text3small_512, RemoteCacheType.ProjectTemplates)
|
||||
: instantiationService.createInstance(LocalEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'projectTemplateEmbeddings', cacheVersion, EmbeddingType.text3small_512);
|
||||
instantiationService.createInstance(RemoteEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'projectTemplateEmbeddings', cacheVersion, EmbeddingType.metis_1024_I16_Binary, RemoteCacheType.ProjectTemplates)
|
||||
: instantiationService.createInstance(LocalEmbeddingsCache, EmbeddingCacheType.GLOBAL, 'projectTemplateEmbeddings', cacheVersion, EmbeddingType.metis_1024_I16_Binary);
|
||||
}
|
||||
|
||||
async updateIndex(): Promise<void> {
|
||||
|
||||
@@ -60,4 +60,47 @@ suite('built-in skills', () => {
|
||||
expect(skill).not.toContain('extensions_manage({');
|
||||
expect(skill).not.toContain('run `/extensions`');
|
||||
});
|
||||
|
||||
test('contributes the customization migration skill to Agent Host sessions', () => {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(copilotRoot, 'package.json'), 'utf-8')) as ICopilotPackage;
|
||||
const contributions = manifest.contributes?.chatSkills?.filter(skill => skill.path.endsWith('/migrate-customizations/SKILL.md'));
|
||||
const skill = fs.readFileSync(
|
||||
path.join(copilotRoot, 'assets', 'prompts', 'skills', 'migrate-customizations', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(skill).not.toContain('report_customization_migration');
|
||||
|
||||
expect({
|
||||
contributions,
|
||||
requiredContent: [
|
||||
'name: migrate-customizations',
|
||||
'user-invocable: true',
|
||||
'disable-model-invocation: true',
|
||||
'Agent Customizations > Migrations',
|
||||
'Do not ask the user to manually reconstruct or paste the missing context',
|
||||
'./references/migration-techniques.md',
|
||||
'migration-log.md',
|
||||
'restore.md',
|
||||
'pull request',
|
||||
'migration-results.json',
|
||||
].filter(anchor => skill.includes(anchor)),
|
||||
}).toEqual({
|
||||
contributions: [{
|
||||
path: './assets/prompts/skills/migrate-customizations/SKILL.md',
|
||||
sessionTypes: ['copilotcli', 'claude', 'codex'],
|
||||
}],
|
||||
requiredContent: [
|
||||
'name: migrate-customizations',
|
||||
'user-invocable: true',
|
||||
'disable-model-invocation: true',
|
||||
'Agent Customizations > Migrations',
|
||||
'Do not ask the user to manually reconstruct or paste the missing context',
|
||||
'./references/migration-techniques.md',
|
||||
'migration-log.md',
|
||||
'restore.md',
|
||||
'pull request',
|
||||
'migration-results.json',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -979,13 +979,18 @@ const coAuthorRegex = /^Co-authored-by:\s*(.+?)\s*<([^>]+)>\s*$/gim;
|
||||
|
||||
export function parseCoAuthors(message: string): CoAuthor[] {
|
||||
const coAuthors: CoAuthor[] = [];
|
||||
const emails = new Set<string>();
|
||||
let match;
|
||||
|
||||
coAuthorRegex.lastIndex = 0;
|
||||
while ((match = coAuthorRegex.exec(message)) !== null) {
|
||||
const name = match[1].trim();
|
||||
const email = match[2].trim();
|
||||
if (name && email) {
|
||||
|
||||
// Squash merge commit messages repeat the trailers of each squashed commit
|
||||
const emailKey = email.toLowerCase();
|
||||
if (name && email && !emails.has(emailKey)) {
|
||||
emails.add(emailKey);
|
||||
coAuthors.push({ name, email });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,6 +642,32 @@ suite('git', () => {
|
||||
[{ name: 'Jane Doe', email: 'jane@example.com' }]
|
||||
);
|
||||
});
|
||||
|
||||
test('duplicate co-authors (squash merge)', function () {
|
||||
assert.deepStrictEqual(
|
||||
parseCoAuthors([
|
||||
'Fix bug (#123)',
|
||||
'',
|
||||
'* First change',
|
||||
'',
|
||||
'Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>',
|
||||
'',
|
||||
'* Second change',
|
||||
'',
|
||||
'Co-authored-by: Jane Doe <jane@example.com>',
|
||||
'Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>',
|
||||
'',
|
||||
'---------',
|
||||
'',
|
||||
'Co-authored-by: Copilot <223556219+copilot@users.noreply.github.com>',
|
||||
'Co-authored-by: Jane <JANE@example.com>'
|
||||
].join('\n')),
|
||||
[
|
||||
{ name: 'Copilot', email: '223556219+Copilot@users.noreply.github.com' },
|
||||
{ name: 'Jane Doe', email: 'jane@example.com' }
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('splitInChunks', () => {
|
||||
|
||||
@@ -168,10 +168,11 @@ function extractTextContent(result: vscode.LanguageModelToolResult): string {
|
||||
});
|
||||
const browserConfig = vscode.workspace.getConfiguration('workbench.browser');
|
||||
const agentConfig = vscode.workspace.getConfiguration('chat.agent');
|
||||
const agentNetworkConfig = vscode.workspace.getConfiguration('chat.agent.sandbox.network');
|
||||
|
||||
try {
|
||||
await agentConfig.update('allowedNetworkDomains', ['*'], vscode.ConfigurationTarget.Global);
|
||||
await agentConfig.update('deniedNetworkDomains', ['localhost'], vscode.ConfigurationTarget.Global);
|
||||
await agentNetworkConfig.update('allowedDomains', ['*'], vscode.ConfigurationTarget.Global);
|
||||
await agentNetworkConfig.update('deniedDomains', ['localhost'], vscode.ConfigurationTarget.Global);
|
||||
await agentConfig.update('networkFilter', true, vscode.ConfigurationTarget.Global);
|
||||
|
||||
await browserConfig.update('dataStorage', 'global', vscode.ConfigurationTarget.Global);
|
||||
@@ -233,8 +234,8 @@ function extractTextContent(result: vscode.LanguageModelToolResult): string {
|
||||
} finally {
|
||||
await browserConfig.update('dataStorage', undefined, vscode.ConfigurationTarget.Global);
|
||||
await agentConfig.update('networkFilter', undefined, vscode.ConfigurationTarget.Global);
|
||||
await agentConfig.update('allowedNetworkDomains', undefined, vscode.ConfigurationTarget.Global);
|
||||
await agentConfig.update('deniedNetworkDomains', undefined, vscode.ConfigurationTarget.Global);
|
||||
await agentNetworkConfig.update('allowedDomains', undefined, vscode.ConfigurationTarget.Global);
|
||||
await agentNetworkConfig.update('deniedDomains', undefined, vscode.ConfigurationTarget.Global);
|
||||
await Promise.all(vscode.window.browserTabs.map(tab => tab.close()));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close(error => error ? reject(error) : resolve());
|
||||
|
||||
@@ -319,7 +319,7 @@ function extractTextContent(result: vscode.LanguageModelToolResult): string {
|
||||
this.timeout(60000);
|
||||
|
||||
const configuration = vscode.workspace.getConfiguration();
|
||||
await configuration.update('chat.agent.allowedNetworkDomains', ['example.com'], vscode.ConfigurationTarget.Global);
|
||||
await configuration.update('chat.agent.sandbox.network.allowedDomains', ['example.com'], vscode.ConfigurationTarget.Global);
|
||||
try {
|
||||
const output = await invokeRunInTerminal('curl -s --max-time 5 https://example.com');
|
||||
const trimmed = output.trim();
|
||||
@@ -329,7 +329,7 @@ function extractTextContent(result: vscode.LanguageModelToolResult): string {
|
||||
];
|
||||
assert.ok(acceptable.some(value => trimmed.includes(value) || trimmed === value), `Unexpected output: ${JSON.stringify(trimmed)}`);
|
||||
} finally {
|
||||
await configuration.update('chat.agent.allowedNetworkDomains', undefined, vscode.ConfigurationTarget.Global);
|
||||
await configuration.update('chat.agent.sandbox.network.allowedDomains', undefined, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Generated
+38
-38
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.141.0",
|
||||
"version": "1.142.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.141.0",
|
||||
"version": "1.142.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.82.0",
|
||||
"@devcontainers/cli": "0.88.0",
|
||||
"@github/copilot-sdk": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk": "1.0.17-preview.4",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@microsoft/dev-tunnels-connections": "^1.3.41",
|
||||
@@ -1170,9 +1170,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-sdk": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-XYoM7Jv+KOw28i+SbZ8hddpkSNjFOeBGZX7aGl+dRWzRFGOUlS4wpmaH33I3EvywfXRj48CO/6/0FabX98fk7Q==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-tt5pyDLUjiaxJSMCU2HRDLRSFOA4mIFpI6+360EC0jqNDmhlaeJy88YwO6zDO77D21l8lGt8INBj+IYMbPb30Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"koffi": "3.2.1",
|
||||
@@ -1183,20 +1183,20 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@github/copilot-sdk-darwin-arm64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-darwin-x64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-linux-arm64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-linux-x64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-linuxmusl-arm64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-linuxmusl-x64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-win32-arm64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-win32-x64": "1.0.17-preview.0"
|
||||
"@github/copilot-sdk-darwin-arm64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-darwin-x64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-linux-arm64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-linux-x64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-linuxmusl-arm64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-linuxmusl-x64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-win32-arm64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-win32-x64": "1.0.17-preview.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-darwin-arm64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-darwin-arm64/-/copilot-sdk-darwin-arm64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-p7J5iCeq6ntw4GD42AK7Kmck9jyhVjKZFFH2W8G8B76fopYSBnvMX+LxdfLDkvOHb0OMY91rG0KA9EImZLkiiA==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-darwin-arm64/-/copilot-sdk-darwin-arm64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-cufab0CWxFH0wwIw1/dlPNx4gFP/PJoL4+tUQ/d6lZ+SWX4/2VQgRJCQ97qe9Nqql3YWDikiJTTHXQ55xkmmRA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1207,9 +1207,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-darwin-x64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-darwin-x64/-/copilot-sdk-darwin-x64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-5whrJOGWrfDZz97VH2TavhWPppNbf9E19IcGtF2DhxBCdWklLWhBfSDCMzrWvpiUnukHSJA925sKQCoThJDKZg==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-darwin-x64/-/copilot-sdk-darwin-x64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-4kj7nU3oXCFFm24d6FijBkuiLTCBtfiI8qEHsDcSxfgPdPnwV+9osT/EAZRKPTh3DYGwqTO7i+bIHEHgogd+Wg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1220,9 +1220,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-linux-arm64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linux-arm64/-/copilot-sdk-linux-arm64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-Dm1Q+/SIU4V1/+VBI2zUfTHcrKBAhuMY204WjuVxyry9BBdrAe98XuZuaGv8+ztbWOsIQ/Vhdcbj7PkEBHetrQ==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linux-arm64/-/copilot-sdk-linux-arm64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-aOHcV5YiFGhtxh+H3qhOuMzktyhwuepGl/jWkWu2vTLy2oztpMnxmoRgwJaj9NdNQoiM8PO13YjbFhbb3TGaOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1236,9 +1236,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-linux-x64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linux-x64/-/copilot-sdk-linux-x64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-UG37BcakUmUh5C7Av1ab2smsFJiitev9mWH3vohaAaCYujj/Zb4CRkTVezVA8KLkkdnq67R7AOQSvTFArb0NoA==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linux-x64/-/copilot-sdk-linux-x64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-W98/SrLJXVk5xn8GgvUxNs8wHM10nPe5Ff3fU9vKb4Kb0aS2KyxoAhC4eoNn8VRB2XDW2wFTBvUJAutCiWGL+Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1252,9 +1252,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-linuxmusl-arm64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linuxmusl-arm64/-/copilot-sdk-linuxmusl-arm64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-DgwS8F4IAra48ML6jWprk1HeJAANf7FdjCRIViVAuhaVLvdjbPi/7pYTQGLtwTcxN63O4cG+/1mPKf2roz83YA==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linuxmusl-arm64/-/copilot-sdk-linuxmusl-arm64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-hym02Fv/JuqjmmMqs4E+XFEgx8nzIpIdMLSr5CFpMLWVVKQYBhY8x6r+03qXhfutUWlcoL5f8gK/pdOR23k9jw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1268,9 +1268,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-linuxmusl-x64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linuxmusl-x64/-/copilot-sdk-linuxmusl-x64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-9T2ik5qK5akLqVhIyvGVicjcumKGOivvuLaAPYU+210MV+t70u9BGQ0a3Vy3tlgDPGHos/js72ZL2RUo6hmFZg==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linuxmusl-x64/-/copilot-sdk-linuxmusl-x64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-IhjdLXtUSV8lSZiWsciGHD2iwv3nS2pAwLMywfgB1GozouBDIfWZJlbTUX5Jqv4JeZjE7lpvD5e+Sc11HL6kQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1284,9 +1284,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-win32-arm64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-win32-arm64/-/copilot-sdk-win32-arm64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-7nSjNxKXQWCEsf+iDQMEW8dr8q4SkQAimemb0U03fqD3jj7JA6Z/XvmAIFb/Cf+RWqTLexHw0O3HjFs9vF5MNQ==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-win32-arm64/-/copilot-sdk-win32-arm64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-MxJJiT2DoDXi3a6dPY/DwMbJPweK75ywlIIbK6Agnof055euC4st3bKMUXz8Nuq4pWzXU3HEWFUVWnNr/63TNw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1297,9 +1297,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-win32-x64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-win32-x64/-/copilot-sdk-win32-x64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-4pa51WL3KZgouFrODCngtmGw3yavDIIEwiK+cGAA7jH45uFu5EpPnuVHlpFIrAU/fa80ZSbiX5KkiigP+EpLow==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-win32-x64/-/copilot-sdk-win32-x64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-NGC8k/WHw6lP6O6h/nJiw3lBaaaMjb1hu+urr+Wpa9EP9a/PFL4rWXpxixgQT4qym7EAVY+EE1vSTA8jCi7cAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.141.0",
|
||||
"distro": "a620eb1a35f4f1994abbd67502c0b2c3b8e73ee0",
|
||||
"version": "1.142.0",
|
||||
"distro": "a0e7521b2a1e3289e4118fad155277b200aebd35",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
},
|
||||
@@ -13,7 +13,7 @@
|
||||
"test": "node -e \"console.error('Run a test script from the scripts folder, for example: ./scripts/test.sh --run <file>.'); process.exit(1)\"",
|
||||
"test-browser": "npx playwright install && node test/unit/browser/index.js",
|
||||
"test-browser-no-install": "node test/unit/browser/index.js",
|
||||
"test-node": "mocha test/unit/node/index.js --delay --ui=tdd --timeout=5000 --exit",
|
||||
"test-node": "mocha test/unit/node/index.js --delay --ui=tdd --exit",
|
||||
"test-extension": "vscode-test",
|
||||
"test-build-scripts": "cd build && npm run test",
|
||||
"test-agent-host-e2e": "node scripts/test-agent-host-e2e.ts",
|
||||
@@ -102,7 +102,7 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.82.0",
|
||||
"@devcontainers/cli": "0.88.0",
|
||||
"@github/copilot-sdk": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk": "1.0.17-preview.4",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@microsoft/dev-tunnels-connections": "^1.3.41",
|
||||
@@ -325,5 +325,5 @@
|
||||
"@vscode/windows-ca-certs@0.3.4": true,
|
||||
"@vscode/windows-process-tree@0.8.0": true
|
||||
},
|
||||
"copilotRuntimeVersion": "1.0.92-0"
|
||||
"copilotRuntimeVersion": "1.0.92-4"
|
||||
}
|
||||
Generated
+36
-36
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@devcontainers/cli": "0.88.0",
|
||||
"@github/copilot-sdk": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk": "1.0.17-preview.4",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@microsoft/mxc-sdk": "0.9.0",
|
||||
@@ -73,9 +73,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-sdk": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-XYoM7Jv+KOw28i+SbZ8hddpkSNjFOeBGZX7aGl+dRWzRFGOUlS4wpmaH33I3EvywfXRj48CO/6/0FabX98fk7Q==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-tt5pyDLUjiaxJSMCU2HRDLRSFOA4mIFpI6+360EC0jqNDmhlaeJy88YwO6zDO77D21l8lGt8INBj+IYMbPb30Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"koffi": "3.2.1",
|
||||
@@ -86,20 +86,20 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@github/copilot-sdk-darwin-arm64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-darwin-x64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-linux-arm64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-linux-x64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-linuxmusl-arm64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-linuxmusl-x64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-win32-arm64": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk-win32-x64": "1.0.17-preview.0"
|
||||
"@github/copilot-sdk-darwin-arm64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-darwin-x64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-linux-arm64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-linux-x64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-linuxmusl-arm64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-linuxmusl-x64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-win32-arm64": "1.0.17-preview.4",
|
||||
"@github/copilot-sdk-win32-x64": "1.0.17-preview.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-darwin-arm64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-darwin-arm64/-/copilot-sdk-darwin-arm64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-p7J5iCeq6ntw4GD42AK7Kmck9jyhVjKZFFH2W8G8B76fopYSBnvMX+LxdfLDkvOHb0OMY91rG0KA9EImZLkiiA==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-darwin-arm64/-/copilot-sdk-darwin-arm64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-cufab0CWxFH0wwIw1/dlPNx4gFP/PJoL4+tUQ/d6lZ+SWX4/2VQgRJCQ97qe9Nqql3YWDikiJTTHXQ55xkmmRA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -110,9 +110,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-darwin-x64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-darwin-x64/-/copilot-sdk-darwin-x64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-5whrJOGWrfDZz97VH2TavhWPppNbf9E19IcGtF2DhxBCdWklLWhBfSDCMzrWvpiUnukHSJA925sKQCoThJDKZg==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-darwin-x64/-/copilot-sdk-darwin-x64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-4kj7nU3oXCFFm24d6FijBkuiLTCBtfiI8qEHsDcSxfgPdPnwV+9osT/EAZRKPTh3DYGwqTO7i+bIHEHgogd+Wg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -123,9 +123,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-linux-arm64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linux-arm64/-/copilot-sdk-linux-arm64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-Dm1Q+/SIU4V1/+VBI2zUfTHcrKBAhuMY204WjuVxyry9BBdrAe98XuZuaGv8+ztbWOsIQ/Vhdcbj7PkEBHetrQ==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linux-arm64/-/copilot-sdk-linux-arm64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-aOHcV5YiFGhtxh+H3qhOuMzktyhwuepGl/jWkWu2vTLy2oztpMnxmoRgwJaj9NdNQoiM8PO13YjbFhbb3TGaOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -139,9 +139,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-linux-x64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linux-x64/-/copilot-sdk-linux-x64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-UG37BcakUmUh5C7Av1ab2smsFJiitev9mWH3vohaAaCYujj/Zb4CRkTVezVA8KLkkdnq67R7AOQSvTFArb0NoA==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linux-x64/-/copilot-sdk-linux-x64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-W98/SrLJXVk5xn8GgvUxNs8wHM10nPe5Ff3fU9vKb4Kb0aS2KyxoAhC4eoNn8VRB2XDW2wFTBvUJAutCiWGL+Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -155,9 +155,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-linuxmusl-arm64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linuxmusl-arm64/-/copilot-sdk-linuxmusl-arm64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-DgwS8F4IAra48ML6jWprk1HeJAANf7FdjCRIViVAuhaVLvdjbPi/7pYTQGLtwTcxN63O4cG+/1mPKf2roz83YA==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linuxmusl-arm64/-/copilot-sdk-linuxmusl-arm64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-hym02Fv/JuqjmmMqs4E+XFEgx8nzIpIdMLSr5CFpMLWVVKQYBhY8x6r+03qXhfutUWlcoL5f8gK/pdOR23k9jw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -171,9 +171,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-linuxmusl-x64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linuxmusl-x64/-/copilot-sdk-linuxmusl-x64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-9T2ik5qK5akLqVhIyvGVicjcumKGOivvuLaAPYU+210MV+t70u9BGQ0a3Vy3tlgDPGHos/js72ZL2RUo6hmFZg==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-linuxmusl-x64/-/copilot-sdk-linuxmusl-x64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-IhjdLXtUSV8lSZiWsciGHD2iwv3nS2pAwLMywfgB1GozouBDIfWZJlbTUX5Jqv4JeZjE7lpvD5e+Sc11HL6kQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -187,9 +187,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-win32-arm64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-win32-arm64/-/copilot-sdk-win32-arm64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-7nSjNxKXQWCEsf+iDQMEW8dr8q4SkQAimemb0U03fqD3jj7JA6Z/XvmAIFb/Cf+RWqTLexHw0O3HjFs9vF5MNQ==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-win32-arm64/-/copilot-sdk-win32-arm64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-MxJJiT2DoDXi3a6dPY/DwMbJPweK75ywlIIbK6Agnof055euC4st3bKMUXz8Nuq4pWzXU3HEWFUVWnNr/63TNw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -200,9 +200,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@github/copilot-sdk-win32-x64": {
|
||||
"version": "1.0.17-preview.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-win32-x64/-/copilot-sdk-win32-x64-1.0.17-preview.0.tgz",
|
||||
"integrity": "sha512-4pa51WL3KZgouFrODCngtmGw3yavDIIEwiK+cGAA7jH45uFu5EpPnuVHlpFIrAU/fa80ZSbiX5KkiigP+EpLow==",
|
||||
"version": "1.0.17-preview.4",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk-win32-x64/-/copilot-sdk-win32-x64-1.0.17-preview.4.tgz",
|
||||
"integrity": "sha512-NGC8k/WHw6lP6O6h/nJiw3lBaaaMjb1hu+urr+Wpa9EP9a/PFL4rWXpxixgQT4qym7EAVY+EE1vSTA8jCi7cAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@devcontainers/cli": "0.88.0",
|
||||
"@github/copilot-sdk": "1.0.17-preview.0",
|
||||
"@github/copilot-sdk": "1.0.17-preview.4",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@microsoft/mxc-sdk": "0.9.0",
|
||||
@@ -78,5 +78,5 @@
|
||||
"@vscode/windows-ca-certs@0.3.4": true,
|
||||
"@vscode/windows-process-tree@0.8.0": true
|
||||
},
|
||||
"copilotRuntimeVersion": "1.0.92-0"
|
||||
"copilotRuntimeVersion": "1.0.92-4"
|
||||
}
|
||||
@@ -1293,7 +1293,7 @@ export class Emitter<T> {
|
||||
let stack: Stacktrace | undefined;
|
||||
if (this._leakWarningThreshold !== undefined && this._size >= Math.ceil(this._leakWarningThreshold * 0.2)) {
|
||||
const leakageMon = this._getLeakageMonitor();
|
||||
if (leakageMon) {
|
||||
if (leakageMon && this._size + 1 >= leakageMon.threshold) {
|
||||
// check and record this emitter for potential leakage
|
||||
contained.stack = Stacktrace.create();
|
||||
removeMonitor = leakageMon.check(contained.stack, this._size + 1);
|
||||
|
||||
@@ -17,7 +17,7 @@ export function popup(items: IContextMenuItem[], options?: IPopupOptions, onHide
|
||||
const itemId = args[0] as number;
|
||||
const context = args[1] as IContextMenuEvent;
|
||||
const item = processedItems[itemId];
|
||||
item.click?.(context);
|
||||
item?.click?.(context);
|
||||
};
|
||||
|
||||
ipcRenderer.once(onClickChannel, onClickChannelHandler);
|
||||
@@ -27,8 +27,6 @@ export function popup(items: IContextMenuItem[], options?: IPopupOptions, onHide
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.removeListener(onClickChannel, onClickChannelHandler);
|
||||
|
||||
onHide?.();
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ export function registerContextMenuListener(): void {
|
||||
// It turns out that the menu gets GC'ed if not referenced anymore
|
||||
// As such we drag it into this scope so that it is not being GC'ed
|
||||
if (menu) {
|
||||
// Complete the one-shot click listener even when no item was selected.
|
||||
event.sender.send(onClickChannel, -1);
|
||||
event.sender.send(CONTEXT_MENU_CLOSE_CHANNEL, contextMenuId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import { MessageController } from '../../message/browser/messageController.js';
|
||||
import { PreferredPasteConfiguration } from './copyPasteContribution.js';
|
||||
import { DefaultTextPasteOrDropEditProvider } from './defaultProviders.js';
|
||||
import { createCombinedWorkspaceEdit, sortEditsByYieldTo } from './edit.js';
|
||||
import { PasteEditSession } from './pasteEditSession.js';
|
||||
import { PostEditWidgetManager } from './postEditWidget.js';
|
||||
|
||||
export const changePasteTypeCommandId = 'editor.changePasteType';
|
||||
@@ -60,11 +61,6 @@ type PasteEditWithProvider = DocumentPasteEdit & {
|
||||
};
|
||||
|
||||
|
||||
interface DocumentPasteWithProviderEditsSession {
|
||||
edits: readonly PasteEditWithProvider[];
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export type PastePreference =
|
||||
| { readonly only: HierarchicalKind }
|
||||
| { readonly preferences: readonly HierarchicalKind[] }
|
||||
@@ -339,7 +335,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
const cts = disposables.add(new CancellationTokenSource(pToken));
|
||||
disposables.add(editorStateCts.token.onCancellationRequested(() => cts.cancel()));
|
||||
const editorStateListener = disposables.add(editorStateCts.token.onCancellationRequested(() => cts.cancel()));
|
||||
|
||||
const token = cts.token;
|
||||
try {
|
||||
@@ -359,8 +355,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
triggerKind: DocumentPasteTriggerKind.Automatic,
|
||||
};
|
||||
|
||||
const editSession = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, token);
|
||||
disposables.add(editSession);
|
||||
const editSession = disposables.add(await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, token));
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
@@ -372,7 +367,9 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
|
||||
if (editSession.edits.length) {
|
||||
const canShowWidget = editor.getOption(EditorOption.pasteAs).showPasteSelector === 'afterPaste';
|
||||
return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: this.getInitialActiveEditIndex(model, editSession.edits), allEdits: editSession.edits }, canShowWidget, async (edit, resolveToken) => {
|
||||
// Applying the paste changes editor state itself. The widget manager handles cancellation during resolve.
|
||||
disposables.delete(editorStateListener);
|
||||
return await this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: this.getInitialActiveEditIndex(model, editSession.edits), allEdits: editSession.edits }, canShowWidget, async (edit, resolveToken) => {
|
||||
if (!edit.provider.resolveDocumentPasteEdit) {
|
||||
return edit;
|
||||
}
|
||||
@@ -388,7 +385,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
edit.additionalEdit = resolved.additionalEdit;
|
||||
}
|
||||
return edit;
|
||||
}, token);
|
||||
}, token, editSession);
|
||||
}
|
||||
|
||||
await this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent);
|
||||
@@ -443,28 +440,26 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
triggerKind: DocumentPasteTriggerKind.PasteAs,
|
||||
only: preference && 'only' in preference ? preference.only : undefined,
|
||||
};
|
||||
let editSession = disposables.add(await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token));
|
||||
const editSession = disposables.add(await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token));
|
||||
if (tokenSource.token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out any edits that don't match the requested kind
|
||||
let edits = editSession.edits;
|
||||
if (preference) {
|
||||
editSession = {
|
||||
edits: editSession.edits.filter(edit => {
|
||||
if ('only' in preference) {
|
||||
return preference.only.contains(edit.kind);
|
||||
} else if ('preferences' in preference) {
|
||||
return preference.preferences.some(preference => preference.contains(edit.kind));
|
||||
} else {
|
||||
return preference.providerId === edit.provider.id;
|
||||
}
|
||||
}),
|
||||
dispose: editSession.dispose
|
||||
};
|
||||
edits = edits.filter(edit => {
|
||||
if ('only' in preference) {
|
||||
return preference.only.contains(edit.kind);
|
||||
} else if ('preferences' in preference) {
|
||||
return preference.preferences.some(preference => preference.contains(edit.kind));
|
||||
} else {
|
||||
return preference.providerId === edit.provider.id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!editSession.edits.length) {
|
||||
if (!edits.length) {
|
||||
if (preference) {
|
||||
this.showPasteAsNoEditMessage(selections, preference);
|
||||
}
|
||||
@@ -473,7 +468,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
|
||||
let pickedEdit: DocumentPasteEdit | undefined;
|
||||
if (preference) {
|
||||
pickedEdit = editSession.edits.at(0);
|
||||
pickedEdit = edits.at(0);
|
||||
} else {
|
||||
type ItemWithEdit = IQuickPickItem & { edit?: DocumentPasteEdit };
|
||||
const configureDefaultItem: ItemWithEdit = {
|
||||
@@ -484,7 +479,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
|
||||
const selected = await this._quickInputService.pick<ItemWithEdit>(
|
||||
[
|
||||
...editSession.edits.map((edit): ItemWithEdit => ({
|
||||
...edits.map((edit): ItemWithEdit => ({
|
||||
label: edit.title,
|
||||
description: edit.kind?.value,
|
||||
edit,
|
||||
@@ -596,7 +591,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
}
|
||||
}
|
||||
|
||||
private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise<DocumentPasteWithProviderEditsSession> {
|
||||
private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise<PasteEditSession<PasteEditWithProvider>> {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
const results = await raceCancellation(
|
||||
@@ -618,10 +613,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
const edits = coalesce(results ?? []).flat().filter(edit => {
|
||||
return !context.only || context.only.contains(edit.kind);
|
||||
});
|
||||
return {
|
||||
edits: sortEditsByYieldTo(edits),
|
||||
dispose: () => disposables.dispose()
|
||||
};
|
||||
return new PasteEditSession(sortEditsByYieldTo(edits), disposables);
|
||||
}
|
||||
|
||||
private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken, clipboardEvent: ClipboardEvent | undefined) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { DocumentPasteEdit } from '../../../common/languages.js';
|
||||
|
||||
/** Owns cached paste edits until they are disposed or transferred to another session. */
|
||||
export class PasteEditSession<T extends DocumentPasteEdit = DocumentPasteEdit> extends Disposable {
|
||||
private readonly _disposables = this._register(new MutableDisposable<IDisposable>());
|
||||
|
||||
constructor(readonly edits: readonly T[], disposables: IDisposable | undefined) {
|
||||
super();
|
||||
this._disposables.value = disposables;
|
||||
}
|
||||
|
||||
/** Transfers the edits to a new owner. Disposing this session will no longer release them. */
|
||||
take(): PasteEditSession<T> {
|
||||
return new PasteEditSession(this.edits, this._disposables.clearAndLeak());
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import { DocumentDropEdit, DocumentPasteEdit } from '../../../common/languages.j
|
||||
import { TrackedRangeStickiness } from '../../../common/model.js';
|
||||
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from '../../editorState/browser/editorState.js';
|
||||
import { createCombinedWorkspaceEdit } from './edit.js';
|
||||
import { PasteEditSession } from './pasteEditSession.js';
|
||||
import './postEditWidget.css';
|
||||
|
||||
|
||||
@@ -47,6 +48,8 @@ class PostEditWidget<T extends DocumentPasteEdit | DocumentDropEdit> extends Dis
|
||||
readonly allowEditorOverflow = true;
|
||||
readonly suppressMouseDown = true;
|
||||
|
||||
private readonly _editSession: PasteEditSession | undefined;
|
||||
|
||||
private domNode!: HTMLElement;
|
||||
private button!: Button;
|
||||
|
||||
@@ -59,14 +62,16 @@ class PostEditWidget<T extends DocumentPasteEdit | DocumentDropEdit> extends Dis
|
||||
private readonly showCommand: ShowCommand,
|
||||
private readonly range: Range,
|
||||
private readonly edits: EditSet<T>,
|
||||
private readonly onSelectNewEdit: (editIndex: number) => void,
|
||||
private readonly onSelectNewEdit: (editIndex: number, editSession: PasteEditSession | undefined) => void,
|
||||
private readonly additionalActions: readonly IAction[],
|
||||
editSessionOwner: PasteEditSession | undefined,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._editSession = editSessionOwner ? this._register(editSessionOwner.take()) : undefined;
|
||||
this.create();
|
||||
|
||||
this.visibleContext = visibleContext.bindTo(contextKeyService);
|
||||
@@ -140,7 +145,8 @@ class PostEditWidget<T extends DocumentPasteEdit | DocumentDropEdit> extends Dis
|
||||
|
||||
const i = this.edits.allEdits.findIndex(edit => edit === item);
|
||||
if (i !== this.edits.activeEditIndex) {
|
||||
return this.onSelectNewEdit(i);
|
||||
// Transfer the edits before undo disposes this widget.
|
||||
return this.onSelectNewEdit(i, this._editSession?.take());
|
||||
}
|
||||
},
|
||||
}, anchor, this.editor.getDomNode() ?? undefined, this.additionalActions);
|
||||
@@ -169,7 +175,7 @@ export class PostEditWidgetManager<T extends DocumentPasteEdit | DocumentDropEdi
|
||||
)(() => this.clear()));
|
||||
}
|
||||
|
||||
public async applyEditAndShowIfNeeded(ranges: readonly Range[], edits: EditSet<T>, canShowWidget: boolean, resolve: (edit: T, token: CancellationToken) => Promise<T>, token: CancellationToken) {
|
||||
public async applyEditAndShowIfNeeded(ranges: readonly Range[], edits: EditSet<T>, canShowWidget: boolean, resolve: (edit: T, token: CancellationToken) => Promise<T>, token: CancellationToken, editSessionOwner?: PasteEditSession) {
|
||||
if (!ranges.length || !this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
@@ -180,14 +186,19 @@ export class PostEditWidgetManager<T extends DocumentPasteEdit | DocumentDropEdi
|
||||
return;
|
||||
}
|
||||
|
||||
const onDidSelectEdit = async (newEditIndex: number) => {
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
const onDidSelectEdit = async (newEditIndex: number, editSession: PasteEditSession | undefined) => {
|
||||
try {
|
||||
this.clear();
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
await model.undo();
|
||||
this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, resolve, token);
|
||||
await model.undo();
|
||||
await this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, resolve, token, editSession);
|
||||
} finally {
|
||||
editSession?.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (e: Error, message: string) => {
|
||||
@@ -197,7 +208,7 @@ export class PostEditWidgetManager<T extends DocumentPasteEdit | DocumentDropEdi
|
||||
|
||||
this._notificationService.error(message);
|
||||
if (canShowWidget) {
|
||||
this.show(ranges[0], edits, onDidSelectEdit);
|
||||
this.show(ranges[0], edits, onDidSelectEdit, editSessionOwner);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -241,15 +252,15 @@ export class PostEditWidgetManager<T extends DocumentPasteEdit | DocumentDropEdi
|
||||
}
|
||||
|
||||
if (canShowWidget && editResult.isApplied && edits.allEdits.length > 1) {
|
||||
this.show(editRange ?? primaryRange, edits, onDidSelectEdit);
|
||||
this.show(editRange ?? primaryRange, edits, onDidSelectEdit, editSessionOwner);
|
||||
}
|
||||
}
|
||||
|
||||
public show(range: Range, edits: EditSet<T>, onDidSelectEdit: (newIndex: number) => void) {
|
||||
public show(range: Range, edits: EditSet<T>, onDidSelectEdit: (newIndex: number, editSession: PasteEditSession | undefined) => void, editSessionOwner?: PasteEditSession) {
|
||||
this.clear();
|
||||
|
||||
if (this._editor.hasModel()) {
|
||||
this._currentWidget.value = this._instantiationService.createInstance(PostEditWidget<T>, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit, this._getAdditionalActions());
|
||||
this._currentWidget.value = this._instantiationService.createInstance(PostEditWidget<T>, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit, this._getAdditionalActions(), editSessionOwner);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 sinon from 'sinon';
|
||||
import { DeferredPromise } from '../../../../../base/common/async.js';
|
||||
import { createStringDataTransferItem, VSDataTransfer } from '../../../../../base/common/dataTransfer.js';
|
||||
import { HierarchicalKind } from '../../../../../base/common/hierarchicalKind.js';
|
||||
import { mock } from '../../../../../base/test/common/mock.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';
|
||||
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IProgressService } from '../../../../../platform/progress/common/progress.js';
|
||||
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
|
||||
import { IBulkEditService, ResourceEdit, ResourceTextEdit } from '../../../../browser/services/bulkEditService.js';
|
||||
import { DocumentPasteEditProvider, WorkspaceEdit } from '../../../../common/languages.js';
|
||||
import { withAsyncTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
|
||||
import { SnippetParser } from '../../../snippet/browser/snippetParser.js';
|
||||
import { CopyPasteController } from '../../browser/copyPasteController.js';
|
||||
|
||||
suite('CopyPasteController - paste edit session', () => {
|
||||
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
for (const endInteraction of ['dismiss', 'cursor', 'content', 'model', 'dispose', 'noSelector', 'singleEdit', 'cancelReplacement', 'failedUndo'] as const) {
|
||||
let name = `resolves alternatives after the initial paste and releases edits on ${endInteraction}`;
|
||||
switch (endInteraction) {
|
||||
case 'noSelector': name = 'releases edits after paste when the selector is disabled'; break;
|
||||
case 'singleEdit': name = 'releases edits after paste when there are no alternatives'; break;
|
||||
case 'cancelReplacement': name = 'releases transferred edits when alternative resolution is cancelled'; break;
|
||||
case 'failedUndo': name = 'releases transferred edits when undo fails'; break;
|
||||
}
|
||||
test(name, async () => {
|
||||
await withAsyncTestCodeEditor('', { pasteAs: { enabled: true, showPasteSelector: endInteraction === 'noSelector' ? 'never' : 'afterPaste' } }, async (editor, _viewModel, instantiationService) => {
|
||||
instantiationService.stub(createDecorator<{ add(): () => void }>('IEditorCancelService'), { add: () => () => { } });
|
||||
let selectEdit: ((index: number) => Promise<void>) | undefined;
|
||||
instantiationService.stub(IActionWidgetService, new class extends mock<IActionWidgetService>() {
|
||||
override show: IActionWidgetService['show'] = (_user, _preview, items, delegate) => {
|
||||
selectEdit = async index => { await delegate.onSelect(items[index].item!); };
|
||||
};
|
||||
override hide() { }
|
||||
});
|
||||
instantiationService.stub(IBulkEditService, new class extends mock<IBulkEditService>() {
|
||||
override async apply(edit: ResourceEdit[] | WorkspaceEdit) {
|
||||
const textEdits = (Array.isArray(edit) ? edit : edit.edits).map(edit => {
|
||||
assert.ok(ResourceTextEdit.is(edit));
|
||||
return { range: edit.textEdit.range, text: new SnippetParser().parse(edit.textEdit.text).toString() };
|
||||
});
|
||||
editor.getModel().pushStackElement();
|
||||
editor.getModel().pushEditOperations(editor.getSelections(), textEdits, () => null);
|
||||
editor.getModel().pushStackElement();
|
||||
return { isApplied: true, ariaSummary: '' };
|
||||
}
|
||||
});
|
||||
instantiationService.stub(IQuickInputService, new class extends mock<IQuickInputService>() { });
|
||||
instantiationService.stub(IProgressService, new class extends mock<IProgressService>() { });
|
||||
|
||||
let released = 0;
|
||||
let resolves = 0;
|
||||
const resolveStarted = new DeferredPromise<void>();
|
||||
const finishResolve = new DeferredPromise<void>();
|
||||
const plainKind = new HierarchicalKind('text.plain');
|
||||
const provider: DocumentPasteEditProvider = {
|
||||
copyMimeTypes: [],
|
||||
pasteMimeTypes: ['text/plain'],
|
||||
providedPasteEditKinds: [plainKind],
|
||||
async provideDocumentPasteEdits() {
|
||||
return {
|
||||
edits: [
|
||||
{ title: 'Resolve test', insertText: 'UNRESOLVED', kind: new HierarchicalKind('test'), yieldTo: [{ kind: plainKind }] },
|
||||
{ title: 'Plain text', insertText: 'PLAIN', kind: plainKind }
|
||||
].slice(endInteraction === 'singleEdit' ? 1 : 0),
|
||||
dispose: () => { ++released; }
|
||||
};
|
||||
},
|
||||
async resolveDocumentPasteEdit(edit) {
|
||||
assert.strictEqual(released, 0, 'Provider edits must still be cached during resolve');
|
||||
++resolves;
|
||||
if (endInteraction === 'cancelReplacement' && edit.title === 'Resolve test') {
|
||||
resolveStarted.complete();
|
||||
await finishResolve.p;
|
||||
}
|
||||
return { ...edit, insertText: edit.title === 'Resolve test' ? 'RESOLVED' : edit.insertText };
|
||||
}
|
||||
};
|
||||
const controller = disposables.add(instantiationService.createInstance(CopyPasteController, editor));
|
||||
const dataTransfer = new VSDataTransfer();
|
||||
dataTransfer.append('text/plain', createStringDataTransferItem('PLAIN'));
|
||||
// The test editor has no clipboard view, so start the inline paste directly.
|
||||
// eslint-disable-next-line local/code-no-bracket-notation-for-identifiers
|
||||
controller['doPasteInline']([provider], editor.getSelections(), dataTransfer, undefined, undefined);
|
||||
await controller.finishedPaste();
|
||||
if (endInteraction === 'noSelector' || endInteraction === 'singleEdit') {
|
||||
assert.deepStrictEqual({ text: editor.getValue(), released, resolves }, { text: 'PLAIN', released: 1, resolves: 1 });
|
||||
return;
|
||||
}
|
||||
assert.deepStrictEqual({ text: editor.getValue(), released, resolves }, { text: 'PLAIN', released: 0, resolves: 1 });
|
||||
|
||||
controller.changePasteType();
|
||||
assert.ok(selectEdit);
|
||||
if (endInteraction === 'failedUndo') {
|
||||
const undo = sinon.stub(editor.getModel(), 'undo').rejects(new Error('Undo failed'));
|
||||
try {
|
||||
await assert.rejects(selectEdit(1), /Undo failed/);
|
||||
} finally {
|
||||
undo.restore();
|
||||
}
|
||||
controller.clearWidgets();
|
||||
assert.strictEqual(released, 1);
|
||||
return;
|
||||
}
|
||||
if (endInteraction === 'cancelReplacement') {
|
||||
const selection = selectEdit(1);
|
||||
await resolveStarted.p;
|
||||
editor.getModel().setValue('changed');
|
||||
finishResolve.complete();
|
||||
await selection;
|
||||
controller.clearWidgets();
|
||||
assert.deepStrictEqual({ text: editor.getValue(), released }, { text: 'changed', released: 1 });
|
||||
return;
|
||||
}
|
||||
await selectEdit(1);
|
||||
assert.deepStrictEqual({ text: editor.getValue(), released, resolves }, { text: 'RESOLVED', released: 0, resolves: 2 });
|
||||
|
||||
controller.changePasteType();
|
||||
await selectEdit(0);
|
||||
assert.deepStrictEqual({ text: editor.getValue(), released, resolves }, { text: 'PLAIN', released: 0, resolves: 3 });
|
||||
|
||||
switch (endInteraction) {
|
||||
case 'dismiss': controller.clearWidgets(); break;
|
||||
case 'cursor': editor.setPosition({ lineNumber: 1, column: 2 }); break;
|
||||
case 'content': editor.getModel().setValue('changed'); break;
|
||||
case 'model': editor.setModel(null); break;
|
||||
case 'dispose': controller.dispose(); break;
|
||||
}
|
||||
controller.clearWidgets();
|
||||
assert.strictEqual(released, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -32,7 +32,7 @@ import { AGENT_HOST_SCHEME, agentHostAuthority, createAgentHostResourceUriMapper
|
||||
import { AgentHostResourceIdentity, AgentHostResourcePermissionError, IAgentHostResourceService, LOCAL_AGENT_HOST_RESOURCE_IDENTITY } from '../common/agentHostResourceService.js';
|
||||
import type { ClientNotificationMap, CommandMap, JsonRpcErrorResponse, JsonRpcRequest, JsonRpcResponse } from '../common/state/protocol/messages.js';
|
||||
import { ActionType, type ActionEnvelope, type ChatAction, type ClientAnnotationsAction, type ClientAutomationAction, type ClientAutomationRunAction, type ClientChangesetAction, type INotification, type IRootConfigChangedAction, type SessionAction, type TerminalAction } from '../common/state/sessionActions.js';
|
||||
import { MessageAttachmentKind, SessionSummary, ROOT_STATE_URI, StateComponents, isAhpRootChannel, isDefaultChatUri, isSessionStatusArchived, type ClientPluginCustomization, type Message, type RootState } from '../common/state/sessionState.js';
|
||||
import { MessageAttachmentKind, SessionSummary, ROOT_STATE_URI, StateComponents, isAhpRootChannel, isDefaultChatUri, isSessionStatusArchived, isSessionStatusRead, type ClientPluginCustomization, type Message, type RootState } from '../common/state/sessionState.js';
|
||||
import { normalizeLegacyActionEnvelope } from '../common/state/legacyProtocolCompatibility.js';
|
||||
import { SUPPORTED_PROTOCOL_VERSIONS } from '../common/state/protocol/version/registry.js';
|
||||
import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, ProtocolError, ReconnectResultType, type ProtocolMessage, type IStateSnapshot } from '../common/state/sessionProtocol.js';
|
||||
@@ -1852,6 +1852,7 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
|
||||
origin: chat.origin,
|
||||
...(chat.interactivity !== undefined ? { interactivity: chat.interactivity } : {}),
|
||||
...(isSessionStatusArchived(chat.status) || chat.archived === true ? { archived: true } : {}),
|
||||
...(chat.status !== undefined ? { isRead: isSessionStatusRead(chat.status) } : {}),
|
||||
...(chat.changes !== undefined ? { changes: chat.changes } : {}),
|
||||
})),
|
||||
// Carry durable host provenance for sessions first materialized from a listing.
|
||||
|
||||
@@ -193,6 +193,8 @@ export interface IAgentSessionChatMetadata {
|
||||
readonly origin?: ChatOrigin;
|
||||
readonly interactivity?: ChatInteractivity;
|
||||
readonly archived?: boolean;
|
||||
/** Exact chat read state when known; absence means the provider did not supply it. */
|
||||
readonly isRead?: boolean;
|
||||
readonly changes?: ChangesSummary;
|
||||
}
|
||||
|
||||
@@ -1044,6 +1046,9 @@ export interface IAgentToolPendingConfirmationSignal {
|
||||
|
||||
export type AgentSubagentTaskModelSource = 'task_argument' | 'subagent_configuration' | 'custom_agent_definition' | 'unset';
|
||||
|
||||
/** `task` is a delegated subagent; `fusionPhase` is a presentation-only chat for one HydraFusion phase of the parent turn. */
|
||||
export type AgentSubagentKind = 'task' | 'fusionPhase';
|
||||
|
||||
/**
|
||||
* A subagent was spawned by a tool call. The host creates a child session
|
||||
* silently and routes subsequent inner-tool events to it.
|
||||
@@ -1059,6 +1064,10 @@ export interface IAgentSubagentStartedSignal {
|
||||
readonly agentDisplayName: string;
|
||||
readonly agentDescription?: string;
|
||||
readonly taskModelSource?: AgentSubagentTaskModelSource;
|
||||
/** Absent means `task`. */
|
||||
readonly subagentKind?: AgentSubagentKind;
|
||||
/** For telemetry, when the chat reports no usage of its own. */
|
||||
readonly model?: string;
|
||||
/**
|
||||
* The spawning Task tool's short (typically 3-5 word) `description`
|
||||
* input, e.g. "Review package.json structure". Distinct from
|
||||
|
||||
@@ -8,6 +8,18 @@ import { type AgentProvider } from './agent.js';
|
||||
const LOCAL_AGENT_HOST_SESSION_TYPE_PREFIX = 'agent-host-';
|
||||
const REMOTE_AGENT_HOST_SESSION_TYPE_PREFIX = 'remote-';
|
||||
|
||||
/** Identifies the Copilot agent family without changing the host's provider ID or implying runtime capabilities. */
|
||||
export function isCopilotAgentHostProvider(provider: AgentProvider | undefined): boolean {
|
||||
return provider === 'copilotcli' || provider === 'copilot';
|
||||
}
|
||||
|
||||
/** Identifies local and remote Copilot agent-host session types, preserving their existing URI schemes. */
|
||||
export function isCopilotAgentHostSessionType(sessionType: string): boolean {
|
||||
const provider = parseAgentHostHarness(sessionType);
|
||||
return provider !== undefined && isCopilotAgentHostProvider(provider)
|
||||
&& (!isRemoteAgentHostSessionType(sessionType) || parseRemoteAgentHostSessionTypeAuthority(sessionType, provider) !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the unique per-connection identifier for a remote agent host.
|
||||
*
|
||||
|
||||
@@ -988,6 +988,7 @@ export interface IAgentService {
|
||||
* with {@link unsubscribe} when the subscription is released. When
|
||||
* provided, `isActive` is checked before registering the subscriber so a
|
||||
* request cancelled during asynchronous resolution cannot pin the resource.
|
||||
* Callers must distinguish lost subscription ownership from a missing resource.
|
||||
*/
|
||||
subscribe(resource: URI, clientId: string, isActive?: () => boolean): Promise<IStateSnapshot>;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { RemoteAgentHostsEnabledSettingId } from './remoteAgentHostService.js';
|
||||
import { IReplayedTaskHistory } from './taskEventReplay.js';
|
||||
import { SessionStatus } from './state/sessionState.js';
|
||||
import { SessionModelInfo } from './state/protocol/state.js';
|
||||
|
||||
/** Configuration key gating the cloud-sandbox connection path. Disabled by default. */
|
||||
export const CloudSandboxEnabledSettingId = 'chat.agentHost.cloudSandbox.enabled';
|
||||
@@ -122,6 +123,8 @@ export interface ICloudSandboxDiscoveredSession {
|
||||
readonly updatedAt?: string;
|
||||
/** Last reported activity; this does not establish environment availability or session flags. */
|
||||
readonly status?: SessionStatus;
|
||||
/** Mission Control's archive state, independent of the sandbox's AHP session flags. */
|
||||
readonly isArchived?: boolean;
|
||||
}
|
||||
|
||||
/** Build the synthesized remote-agent-host address for a sandbox environment. */
|
||||
@@ -233,6 +236,11 @@ export interface ICloudSandboxConnectionRequest {
|
||||
|
||||
export const ICloudSandboxApiService = createDecorator<ICloudSandboxApiService>('cloudSandboxApiService');
|
||||
|
||||
export interface ICloudSandboxModelCatalog {
|
||||
readonly models: readonly SessionModelInfo[];
|
||||
readonly defaultModel?: string;
|
||||
}
|
||||
|
||||
/** Account identity and control-plane APIs for sandbox credentials, discovery, and persisted history. */
|
||||
export interface ICloudSandboxApiService {
|
||||
readonly _serviceBrand: undefined;
|
||||
@@ -243,6 +251,9 @@ export interface ICloudSandboxApiService {
|
||||
/** Resolves an opaque, credential-free account key using the same identity as task requests. */
|
||||
getAccountKey(): Promise<string | undefined>;
|
||||
|
||||
/** Account-authorized cloud model catalog, available before a sandbox is provisioned. */
|
||||
listModels(token: CancellationToken): Promise<ICloudSandboxModelCatalog>;
|
||||
|
||||
/**
|
||||
* Mint a fresh client Web PubSub connection token for a new logical connection. May resolve to a
|
||||
* "waking" result the caller should retry.
|
||||
@@ -277,6 +288,9 @@ export interface ICloudSandboxApiService {
|
||||
/** Rename the Mission Control task without waking the sandbox. */
|
||||
renameTask(taskId: string, title: string, token: CancellationToken): Promise<void>;
|
||||
|
||||
/** Archive or unarchive the Mission Control task without waking the sandbox. */
|
||||
setTaskArchived(taskId: string, archived: boolean, token: CancellationToken): Promise<void>;
|
||||
|
||||
/**
|
||||
* Read a task's persisted AHP history and fold it back into session and chat state. Served by
|
||||
* Mission Control's mirror, so it works without the sandbox. `undefined` when there is none.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export type { ISlashCommandResource } from './vscode/agentSlashCommandOutputMeta.js';
|
||||
export { readSlashCommandResource, toSlashCommandResourceMeta } from './vscode/agentSlashCommandOutputMeta.js';
|
||||
@@ -13,6 +13,7 @@ export interface ICloudSandboxProject {
|
||||
readonly path: string;
|
||||
readonly git: boolean;
|
||||
readonly status: 'ready' | 'cloning' | 'failed';
|
||||
readonly progress?: number;
|
||||
readonly remoteUrl?: string;
|
||||
readonly error?: string;
|
||||
}
|
||||
@@ -37,6 +38,9 @@ function readProject(value: unknown): ICloudSandboxProject | undefined {
|
||||
path: value.path,
|
||||
git: value.git,
|
||||
status,
|
||||
progress: status === 'cloning' && typeof value.progress === 'number' && Number.isFinite(value.progress)
|
||||
? Math.min(100, Math.max(0, value.progress))
|
||||
: undefined,
|
||||
remoteUrl: typeof value.remoteUrl === 'string' ? value.remoteUrl : undefined,
|
||||
error: typeof value.error === 'string' ? value.error : undefined,
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export type { McpServerSource } from './vscode/mcpCustomizationMeta.js';
|
||||
export { readMcpServerSource, withMcpServerSourceMeta } from './vscode/mcpCustomizationMeta.js';
|
||||
export { readMcpServerDisplayName, readMcpServerSource, withMcpServerDisplayNameMeta, withMcpServerSourceMeta } from './vscode/mcpCustomizationMeta.js';
|
||||
@@ -16,20 +16,30 @@ export interface ISessionSandboxPolicy {
|
||||
readonly allowDevToolAccess?: boolean;
|
||||
readonly sandboxMcpServers?: boolean;
|
||||
readonly sandboxLspServers?: boolean;
|
||||
readonly authenticateGit?: boolean;
|
||||
readonly authenticateGh?: boolean;
|
||||
readonly failClosed?: boolean;
|
||||
}
|
||||
|
||||
/** Missing or unsupported metadata carries no assertion about the host's policy. */
|
||||
/** Reads trusted host policy metadata; missing or non-object metadata carries no policy assertion. */
|
||||
export function readSessionSandboxPolicy(source: { readonly _meta?: Record<string, unknown> } | undefined): ISessionSandboxPolicy | undefined {
|
||||
const value = source?._meta?.[sandboxPolicyKey];
|
||||
if (!isObject(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const { enabled, allowBypass, allowOutbound, allowLocalNetwork, allowDevToolAccess, sandboxMcpServers, sandboxLspServers, failClosed } = value as Record<string, unknown>;
|
||||
if (typeof enabled !== 'boolean' || (allowBypass !== undefined && typeof allowBypass !== 'boolean') || (allowOutbound !== undefined && typeof allowOutbound !== 'boolean') || (allowLocalNetwork !== undefined && typeof allowLocalNetwork !== 'boolean') || (allowDevToolAccess !== undefined && typeof allowDevToolAccess !== 'boolean') || (sandboxMcpServers !== undefined && typeof sandboxMcpServers !== 'boolean') || (sandboxLspServers !== undefined && typeof sandboxLspServers !== 'boolean') || (failClosed !== undefined && typeof failClosed !== 'boolean')) {
|
||||
return undefined;
|
||||
}
|
||||
return { enabled, ...(allowBypass !== undefined ? { allowBypass } : {}), ...(allowOutbound !== undefined ? { allowOutbound } : {}), ...(allowLocalNetwork !== undefined ? { allowLocalNetwork } : {}), ...(allowDevToolAccess !== undefined ? { allowDevToolAccess } : {}), ...(sandboxMcpServers !== undefined ? { sandboxMcpServers } : {}), ...(sandboxLspServers !== undefined ? { sandboxLspServers } : {}), ...(failClosed !== undefined ? { failClosed } : {}) };
|
||||
const { enabled, allowBypass, allowOutbound, allowLocalNetwork, allowDevToolAccess, sandboxMcpServers, sandboxLspServers, authenticateGit, authenticateGh, failClosed } = value as ISessionSandboxPolicy;
|
||||
return {
|
||||
enabled,
|
||||
...(allowBypass !== undefined ? { allowBypass } : {}),
|
||||
...(allowOutbound !== undefined ? { allowOutbound } : {}),
|
||||
...(allowLocalNetwork !== undefined ? { allowLocalNetwork } : {}),
|
||||
...(allowDevToolAccess !== undefined ? { allowDevToolAccess } : {}),
|
||||
...(sandboxMcpServers !== undefined ? { sandboxMcpServers } : {}),
|
||||
...(sandboxLspServers !== undefined ? { sandboxLspServers } : {}),
|
||||
...(authenticateGit !== undefined ? { authenticateGit } : {}),
|
||||
...(authenticateGh !== undefined ? { authenticateGh } : {}),
|
||||
...(failClosed !== undefined ? { failClosed } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function withSessionSandboxPolicy(meta: Record<string, unknown> | undefined, policy: ISessionSandboxPolicy | undefined): Record<string, unknown> {
|
||||
@@ -43,6 +53,8 @@ export function withSessionSandboxPolicy(meta: Record<string, unknown> | undefin
|
||||
...(policy.allowDevToolAccess !== undefined ? { allowDevToolAccess: policy.allowDevToolAccess } : {}),
|
||||
...(policy.sandboxMcpServers !== undefined ? { sandboxMcpServers: policy.sandboxMcpServers } : {}),
|
||||
...(policy.sandboxLspServers !== undefined ? { sandboxLspServers: policy.sandboxLspServers } : {}),
|
||||
...(policy.authenticateGit !== undefined ? { authenticateGit: policy.authenticateGit } : {}),
|
||||
...(policy.authenticateGh !== undefined ? { authenticateGh: policy.authenticateGh } : {}),
|
||||
...(policy.failClosed !== undefined ? { failClosed: policy.failClosed } : {}),
|
||||
} : undefined,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isObject } from '../../../../../base/common/types.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
|
||||
const slashCommandResourceKey = 'vscode.slashCommandResource';
|
||||
|
||||
export interface ISlashCommandResource {
|
||||
readonly resource: URI;
|
||||
readonly preview: boolean;
|
||||
}
|
||||
|
||||
/** Reads an optional resource to open for a user-invoked slash command. */
|
||||
export function readSlashCommandResource(source: { readonly _meta?: Record<string, unknown> }): ISlashCommandResource | undefined {
|
||||
const value = source._meta?.[slashCommandResourceKey];
|
||||
if (!isObject(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const { resource, preview } = value as Record<string, unknown>;
|
||||
if (typeof resource !== 'string' || typeof preview !== 'boolean') {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return { resource: URI.parse(resource, true), preview };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function toSlashCommandResourceMeta(resource: URI, preview: boolean): Record<string, unknown> {
|
||||
return { [slashCommandResourceKey]: { resource: resource.toString(), preview } };
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import type { Mutable } from '../../../../../base/common/types.js';
|
||||
import { hasAgentMetadata } from '../metadata.js';
|
||||
import { ISandboxNetworkRestrictions, isSandboxNetworkRestrictions } from '../../../../sandbox/common/sandboxSettingsResolutionHelper.js';
|
||||
|
||||
/** Anything carrying a tool call's `_meta` bag (persisted state or wire actions). */
|
||||
interface IHasToolCallMeta {
|
||||
@@ -18,6 +19,8 @@ interface IHasToolCallMeta {
|
||||
* wrong-typed values.
|
||||
*/
|
||||
export interface IToolCallMeta {
|
||||
/** Trusted Copilot host snapshot for integrated-browser client tools. Absent for other tools and harnesses. */
|
||||
readonly 'vscode.copilotSandboxNetworkRestrictions'?: ISandboxNetworkRestrictions;
|
||||
readonly 'agentHost.sandboxBypass'?: boolean;
|
||||
/**
|
||||
* VS Code rendering hint. `terminal` routes the call to the command/output
|
||||
@@ -62,6 +65,7 @@ export type AgentFusionPhaseStatus = typeof fusionPhaseStatuses[number];
|
||||
const knownFusionPhaseStatuses: ReadonlySet<string> = new Set(fusionPhaseStatuses);
|
||||
|
||||
const toolCallMetaKeys = [
|
||||
'vscode.copilotSandboxNetworkRestrictions',
|
||||
'agentHost.sandboxBypass', 'toolKind', 'language', 'subagentDescription', 'subagentAgentName', 'subagentChatUri',
|
||||
'mcpServerName', 'mcpToolName', 'autoApproveBySetting', 'autoApproveRuleResolvable', 'toolSearchCandidates', 'progressMessage', 'fusionPhase',
|
||||
] as const;
|
||||
@@ -167,6 +171,10 @@ export function readToolCallMeta(source: IHasToolCallMeta): IToolCallMeta {
|
||||
return {};
|
||||
}
|
||||
const result: Mutable<IToolCallMeta> = {};
|
||||
const networkRestrictions = meta['vscode.copilotSandboxNetworkRestrictions'];
|
||||
if (isSandboxNetworkRestrictions(networkRestrictions)) {
|
||||
result['vscode.copilotSandboxNetworkRestrictions'] = networkRestrictions;
|
||||
}
|
||||
if (typeof meta['agentHost.sandboxBypass'] === 'boolean') { result['agentHost.sandboxBypass'] = meta['agentHost.sandboxBypass']; }
|
||||
if (isToolKind(meta.toolKind)) { result.toolKind = meta.toolKind; }
|
||||
if (typeof meta.language === 'string') { result.language = meta.language; }
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import type { McpServerCustomization } from '../../state/protocol/state.js';
|
||||
|
||||
const sourceKey = 'agentHost.mcpServerSource';
|
||||
const displayNameKey = 'vscode.mcpServerDisplayName';
|
||||
const maxDisplayNameLength = 512;
|
||||
|
||||
export type McpServerSource =
|
||||
| 'user' // Defined in user-level configuration.
|
||||
@@ -36,3 +38,22 @@ export function withMcpServerSourceMeta(meta: Record<string, unknown> | undefine
|
||||
}
|
||||
return { ...(meta ?? {}), [sourceKey]: source };
|
||||
}
|
||||
|
||||
/** Reads an optional Connector catalog name without changing the MCP server's runtime identity. */
|
||||
export function readMcpServerDisplayName(customization: McpServerCustomization | undefined): string | undefined {
|
||||
return normalizeMcpServerDisplayName(customization?._meta?.[displayNameKey]);
|
||||
}
|
||||
|
||||
/** Records an optional Connector catalog name while preserving every other metadata entry. */
|
||||
export function withMcpServerDisplayNameMeta(meta: Record<string, unknown> | undefined, displayName: string | undefined): Record<string, unknown> | undefined {
|
||||
const normalized = normalizeMcpServerDisplayName(displayName);
|
||||
return normalized === undefined ? meta : { ...(meta ?? {}), [displayNameKey]: normalized };
|
||||
}
|
||||
|
||||
function normalizeMcpServerDisplayName(value: unknown): string | undefined {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 && trimmed.length <= maxDisplayNameLength ? trimmed : undefined;
|
||||
}
|
||||
@@ -32,6 +32,8 @@ export const enum AgentHostSandboxKey {
|
||||
AllowUnsandboxedCommands = 'allowUnsandboxedCommands',
|
||||
SandboxMcpServers = 'sandboxMcpServers',
|
||||
SandboxLspServers = 'sandboxLspServers',
|
||||
AuthenticateGit = 'authenticateGit',
|
||||
AuthenticateGh = 'authenticateGh',
|
||||
AllowDevToolAccess = 'allowDevToolAccess',
|
||||
UserConfiguredPaths = 'fileSystem.userConfiguredPaths',
|
||||
LinuxFileSystem = 'fileSystem.linux',
|
||||
@@ -50,6 +52,8 @@ export type ISandboxConfigValue = Partial<{
|
||||
[AgentHostSandboxKey.AllowUnsandboxedCommands]: boolean;
|
||||
[AgentHostSandboxKey.SandboxMcpServers]: boolean;
|
||||
[AgentHostSandboxKey.SandboxLspServers]: boolean;
|
||||
[AgentHostSandboxKey.AuthenticateGit]: boolean;
|
||||
[AgentHostSandboxKey.AuthenticateGh]: boolean;
|
||||
[AgentHostSandboxKey.AllowDevToolAccess]: boolean;
|
||||
[AgentHostSandboxKey.UserConfiguredPaths]: IAgentSandboxUserConfiguredPaths;
|
||||
[AgentHostSandboxKey.LinuxFileSystem]: IAgentSandboxFileSystemSetting;
|
||||
@@ -97,6 +101,14 @@ export const sandboxConfigSchema = createSchema({
|
||||
type: 'boolean',
|
||||
title: localize('agentHost.config.sandbox.sandboxLspServers.title', "Sandbox LSP Servers"),
|
||||
},
|
||||
[AgentHostSandboxKey.AuthenticateGit]: {
|
||||
type: 'boolean',
|
||||
title: localize('agentHost.config.sandbox.authenticateGit.title', "Authenticate git"),
|
||||
},
|
||||
[AgentHostSandboxKey.AuthenticateGh]: {
|
||||
type: 'boolean',
|
||||
title: localize('agentHost.config.sandbox.authenticateGh.title', "Authenticate gh"),
|
||||
},
|
||||
[AgentHostSandboxKey.AllowDevToolAccess]: {
|
||||
type: 'boolean',
|
||||
title: localize('agentHost.config.sandbox.allowDevToolAccess.title', "Allow Dev Tool Access"),
|
||||
@@ -166,6 +178,8 @@ export const sandboxSettingIdToAgentHostKey: Readonly<Record<string, AgentHostSa
|
||||
[AgentSandboxSettingId.AgentSandboxAllowUnsandboxedCommands]: AgentHostSandboxKey.AllowUnsandboxedCommands,
|
||||
[AgentSandboxSettingId.AgentSandboxMcpServers]: AgentHostSandboxKey.SandboxMcpServers,
|
||||
[AgentSandboxSettingId.AgentSandboxLspServers]: AgentHostSandboxKey.SandboxLspServers,
|
||||
[AgentSandboxSettingId.AgentSandboxAuthenticateGit]: AgentHostSandboxKey.AuthenticateGit,
|
||||
[AgentSandboxSettingId.AgentSandboxAuthenticateGh]: AgentHostSandboxKey.AuthenticateGh,
|
||||
[AgentSandboxSettingId.AgentSandboxAllowDevToolAccess]: AgentHostSandboxKey.AllowDevToolAccess,
|
||||
[AgentSandboxSettingId.AgentSandboxUserConfiguredPaths]: AgentHostSandboxKey.UserConfiguredPaths,
|
||||
[AgentSandboxSettingId.AgentSandboxLinuxFileSystem]: AgentHostSandboxKey.LinuxFileSystem,
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
type ChangesetState,
|
||||
type ChatState,
|
||||
type ChatSummary,
|
||||
type ChatOrigin,
|
||||
type CanvasState,
|
||||
type ErrorInfo,
|
||||
type ErrorResponsePart,
|
||||
@@ -2029,12 +2030,14 @@ export const AH_META_AUTO_ARCHIVED_AT_DB_KEY = 'agentHost.autoArchivedAt';
|
||||
export const AH_META_IS_DONE_DB_KEY = 'isDone';
|
||||
|
||||
/**
|
||||
* Session-database metadata key recording whether a session has been read. This is
|
||||
* the only durable representation of read state; the in-memory truth is
|
||||
* {@link SessionStatus.IsRead}. The host owns it — no agent SDK tracks read state.
|
||||
* Session-database metadata key recording the session aggregate read state. The
|
||||
* in-memory truth is {@link SessionStatus.IsRead}; chat state is stored separately.
|
||||
*/
|
||||
export const AH_META_IS_READ_DB_KEY = 'isRead';
|
||||
|
||||
/** Session-database metadata key recording the default chat's independent read state. */
|
||||
export const AH_META_DEFAULT_CHAT_IS_READ_DB_KEY = 'defaultChatIsRead';
|
||||
|
||||
/** Returns `status` with `flag` set or cleared. */
|
||||
export function withSessionStatusFlag(status: SessionStatus, flag: SessionStatus, set: boolean): SessionStatus {
|
||||
return set ? (status | flag) : (status & ~flag);
|
||||
@@ -2045,6 +2048,16 @@ export function isSessionStatusRead(status: SessionStatus | undefined): boolean
|
||||
return status !== undefined && (status & SessionStatus.IsRead) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a chat participates in the containing session's aggregate read state.
|
||||
* Tool/subagent/hidden chats retain exact per-chat state without making the session unread.
|
||||
*/
|
||||
export function isChatInSessionReadAggregate(resource: ProtocolURI, origin?: ChatOrigin, interactivity?: ChatInteractivity): boolean {
|
||||
return origin?.kind !== ChatOriginKind.Tool
|
||||
&& interactivity !== ChatInteractivity.Hidden
|
||||
&& !isSubagentChatUri(resource);
|
||||
}
|
||||
|
||||
/** Whether the {@link SessionStatus.IsArchived} flag bit is set. */
|
||||
export function isSessionStatusArchived(status: SessionStatus | undefined): boolean {
|
||||
return status !== undefined && (status & SessionStatus.IsArchived) !== 0;
|
||||
|
||||
@@ -14,21 +14,16 @@
|
||||
// shape. The envelopes are folded by the same `sessionReducer` / `chatReducer` the live
|
||||
// subscriptions use, so a replayed session and a live one cannot drift.
|
||||
|
||||
import { equals } from '../../../base/common/objects.js';
|
||||
import { ChunkEnvelope, Reassembler } from './webPubSub/chunking.js';
|
||||
import { ActionEnvelope, ActionType, StateAction } from './state/protocol/common/actions.js';
|
||||
import { SessionDefaultChatChangedAction } from './state/protocol/channels-session/actions.js';
|
||||
import { chatReducer } from './state/protocol/channels-chat/reducer.js';
|
||||
import { ChatState } from './state/protocol/channels-chat/state.js';
|
||||
import { ChatOriginKind, ChatState } from './state/protocol/channels-chat/state.js';
|
||||
import { sessionReducer } from './state/protocol/channels-session/reducer.js';
|
||||
import { SessionLifecycle, SessionState, SessionStatus } from './state/protocol/channels-session/state.js';
|
||||
import { ChatAction, SessionAction } from './state/sessionActions.js';
|
||||
|
||||
/**
|
||||
* Highest transport sequence number that may legitimately appear *below* the expected next
|
||||
* sequence. Mission Control re-hosts a dormant session on a fresh mirror process whose transport
|
||||
* sequence restarts at 0 or 1 while `/events` continues with only the new actions; anything
|
||||
* further back is a genuine gap.
|
||||
*/
|
||||
/** Highest starting sequence allowed for a new mirror epoch after exact duplicates are removed. */
|
||||
const MAX_RESTART_EPOCH_INITIAL_SEQUENCE = 1;
|
||||
|
||||
/** A persisted history that could not be decoded. Distinct from a transport/HTTP failure. */
|
||||
@@ -48,11 +43,8 @@ export interface IReplayedSession {
|
||||
/** Folded chat-channel state, keyed by chat channel URI. */
|
||||
readonly chats: ReadonlyMap<string, ChatState>;
|
||||
/**
|
||||
* Channel of the session's default chat, as the recorded history named it.
|
||||
*
|
||||
* Resolved from the history rather than derived locally: the host writes whatever channel
|
||||
* convention it uses (today `<session>/chat`), which need not match the URI a client would
|
||||
* build for the same chat.
|
||||
* The host-announced default chat, or a sole recorded chat consistent with all recorded catalogue evidence.
|
||||
* History without either retains the legacy `<session>/chat` fallback.
|
||||
*/
|
||||
readonly defaultChat: string;
|
||||
/** Timestamp of the last persisted event, ISO 8601. */
|
||||
@@ -75,6 +67,7 @@ export interface IReplayedTaskHistory {
|
||||
/** Per-session accumulator used while decoding the transport layer. */
|
||||
interface ISessionReplayState {
|
||||
readonly envelopes: ActionEnvelope[];
|
||||
readonly eventsBySeq: Map<number, Record<string, unknown>>;
|
||||
modifiedAt: string;
|
||||
nextSeq: number;
|
||||
reassembler: Reassembler;
|
||||
@@ -154,7 +147,8 @@ function seedChatState(chatChannel: string, modifiedAt: string): ChatState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the persisted transport layer into ordered envelopes, grouped by session.
|
||||
* Decode the persisted transport layer into ordered envelopes, grouped by session, ignoring exact
|
||||
* duplicate records within each mirror epoch.
|
||||
*
|
||||
* Throws on a genuine sequence gap or a corrupt record — a history that cannot be trusted must
|
||||
* not be shown as if it were complete.
|
||||
@@ -176,10 +170,13 @@ function decodeEvents(events: readonly unknown[]): Map<string, ISessionReplaySta
|
||||
|
||||
let entry = sessions.get(session);
|
||||
if (!entry) {
|
||||
entry = { envelopes: [], modifiedAt: at, nextSeq: seq, reassembler: new Reassembler(), abandonedChunkGroup: false };
|
||||
entry = { envelopes: [], eventsBySeq: new Map(), modifiedAt: at, nextSeq: seq, reassembler: new Reassembler(), abandonedChunkGroup: false };
|
||||
sessions.set(session, entry);
|
||||
}
|
||||
entry.modifiedAt = at;
|
||||
// Persisted batches can overlap; never fold an already-seen delta or chunk twice.
|
||||
if (equals(entry.eventsBySeq.get(seq), value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startsRestartEpoch = seq !== entry.nextSeq && seq < entry.nextSeq && seq <= MAX_RESTART_EPOCH_INITIAL_SEQUENCE;
|
||||
if (seq !== entry.nextSeq && !startsRestartEpoch) {
|
||||
@@ -192,9 +189,12 @@ function decodeEvents(events: readonly unknown[]): Map<string, ISessionReplaySta
|
||||
// buffered at the restart is an action the previous epoch never finished emitting, so
|
||||
// remember it — replacing the reassembler is what would otherwise lose that fact.
|
||||
entry.abandonedChunkGroup ||= entry.reassembler.inFlightGroupCount > 0;
|
||||
entry.eventsBySeq.clear();
|
||||
entry.nextSeq = seq;
|
||||
entry.reassembler = new Reassembler();
|
||||
}
|
||||
entry.eventsBySeq.set(seq, value);
|
||||
entry.modifiedAt = at;
|
||||
entry.nextSeq += 1;
|
||||
|
||||
let reassembled: unknown;
|
||||
@@ -227,9 +227,7 @@ function decodeEvents(events: readonly unknown[]): Map<string, ISessionReplaySta
|
||||
/**
|
||||
* Fold one session's envelopes into session state plus a chat state per chat channel.
|
||||
*
|
||||
* Routed by **action type**, not channel scheme: recorded channels are whatever the host wrote
|
||||
* (today `<session>/chat`, not the `ahp-chat://` URI a client builds), so matching on a scheme would
|
||||
* silently drop every chat action. The live subscriptions route the same way.
|
||||
* Routed by action type, not channel scheme, so host-defined chat URIs are preserved.
|
||||
*
|
||||
* A session may own several peer chats, so each chat channel found in the history gets its own fold
|
||||
* — discovered from the envelopes rather than assumed, which keeps forked and peer chats intact.
|
||||
@@ -237,19 +235,18 @@ function decodeEvents(events: readonly unknown[]): Map<string, ISessionReplaySta
|
||||
function foldSession(session: string, entry: ISessionReplayState): IReplayedSession {
|
||||
let state = seedSessionState();
|
||||
const chats = new Map<string, ChatState>();
|
||||
// The host announces its default chat via `session/defaultChatChanged`; until then the
|
||||
// deterministic `<session>/chat` is the convention it writes.
|
||||
let defaultChat = `${session}/chat`;
|
||||
let hasChatCatalogueEvidence = false;
|
||||
|
||||
for (const envelope of entry.envelopes) {
|
||||
const channel = envelope.channel;
|
||||
const action: StateAction = envelope.action;
|
||||
|
||||
if (action.type.startsWith('session/') && channel === session) {
|
||||
hasChatCatalogueEvidence ||= action.type === ActionType.SessionChatAdded
|
||||
|| action.type === ActionType.SessionChatRemoved
|
||||
|| action.type === ActionType.SessionChatUpdated
|
||||
|| action.type === ActionType.SessionChatsReordered;
|
||||
state = sessionReducer(state, action as SessionAction);
|
||||
if (action.type === ActionType.SessionDefaultChatChanged) {
|
||||
defaultChat = (action as SessionDefaultChatChangedAction).defaultChat || `${session}/chat`;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (action.type.startsWith('chat/')) {
|
||||
@@ -260,8 +257,13 @@ function foldSession(session: string, entry: ISessionReplayState): IReplayedSess
|
||||
// are intentionally skipped.
|
||||
}
|
||||
|
||||
// A session whose history never announced its chats still owns a default chat, so surface an
|
||||
// empty one rather than a session that appears to have no conversation at all.
|
||||
const [recordedChat] = chats.keys();
|
||||
const unambiguousChat = chats.size === 1
|
||||
&& (!hasChatCatalogueEvidence || state.chats.length === 1)
|
||||
&& state.chats.every(chat =>
|
||||
chat.resource === recordedChat && (!chat.origin || chat.origin.kind === ChatOriginKind.User))
|
||||
? recordedChat : undefined;
|
||||
const defaultChat = state.defaultChat || unambiguousChat || `${session}/chat`;
|
||||
if (!chats.has(defaultChat)) {
|
||||
chats.set(defaultChat, seedChatState(defaultChat, entry.modifiedAt));
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ export class AgentHostCatalogListReader {
|
||||
origin: fromCatalogChatOrigin(chat.origin),
|
||||
...(chat.interactivity !== undefined ? { interactivity: chat.interactivity } : {}),
|
||||
...(chat.archived === true ? { archived: true } : {}),
|
||||
...(chat.isRead !== undefined ? { isRead: chat.isRead } : {}),
|
||||
...(chat.changes !== undefined ? { changes: chat.changes } : {}),
|
||||
})),
|
||||
...(meta !== undefined ? { _meta: meta } : {}),
|
||||
|
||||
@@ -15,7 +15,7 @@ import { SESSION_META_ARTIFACTS_KEY } from '../common/sessionArtifacts.js';
|
||||
import { ChatInteractivity } from '../common/state/protocol/channels-chat/state.js';
|
||||
import { SESSION_META_CREATED_BY_SESSION_KEY, SESSION_META_EHCLI_ADOPTABLE_KEY, SESSION_META_EHCLI_ADOPTED_KEY, SESSION_META_FOLDER_PICKER_KEY, SESSION_META_GIT_DATA_KEY, SESSION_META_GIT_KEY, SESSION_META_GITHUB_DATA_KEY, SESSION_META_GITHUB_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, SESSION_META_WORKSPACELESS_KEY } from '../common/state/sessionState.js';
|
||||
|
||||
export const AGENT_HOST_CATALOG_PAYLOAD_VERSION = 1;
|
||||
export const AGENT_HOST_CATALOG_PAYLOAD_VERSION = 2;
|
||||
export const AGENT_HOST_CATALOG_GITHUB_REFERENCE_LIMIT = 10;
|
||||
export const AGENT_HOST_CATALOG_ARTIFACT_LIMIT = 100;
|
||||
export const AGENT_HOST_CATALOG_CHILD_LIMIT = 1000;
|
||||
@@ -385,6 +385,7 @@ const chatValidator = plainObject(vObj({
|
||||
origin: vOptionalProp(jsonValue()),
|
||||
interactivity: vOptionalProp(vEnum(ChatInteractivity.Full, ChatInteractivity.ReadOnly, ChatInteractivity.Hidden)),
|
||||
archived: vOptionalProp(vBoolean()),
|
||||
isRead: vOptionalProp(vBoolean()),
|
||||
inheritedTurnId: vOptionalProp(boundedString(AGENT_HOST_CATALOG_JSON_STRING_LENGTH_LIMIT)),
|
||||
workingDirectories: vOptionalProp(workingDirectoriesValidator),
|
||||
changes: vOptionalProp(agentHostCatalogChangesValidator),
|
||||
|
||||
@@ -12,7 +12,7 @@ import { parseSessionArtifacts, readSessionArtifacts, SESSION_META_ARTIFACTS_KEY
|
||||
import { getChatChangesSummaryMetadataKey, META_CHANGES_SUMMARY } from '../common/agentHostChangesetService.js';
|
||||
import { META_GIT_DATA_STATE, META_GIT_STATE, META_GITHUB_DATA_STATE, META_GITHUB_STATE, META_SOURCE_CONTROL_STATE } from '../common/agentHostGitStateService.js';
|
||||
import { ChangesSummary, ChatInteractivity, ChatOrigin, ChatOriginKind } from '../common/state/protocol/state.js';
|
||||
import { AH_META_CREATED_BY_SESSION_DB_KEY, AH_META_EHCLI_ADOPTED_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, AH_META_IS_READ_DB_KEY, AH_META_WORKSPACELESS_DB_KEY, ISessionGitHubState, ISessionGitState, ISessionSourceControlState, parseSessionCreationReference, parseSessionFolderPickerDecision, parseSessionMultiRootMetadata, readSessionCreationReference, readSessionEhcliAdoptable, readSessionEhcliAdopted, readSessionExternal, readSessionFolderPickerDecision, parseSessionGitHubData, parseSessionGitHubState, readSessionGitData, readSessionGitHubData, readSessionGitState, withMigratedSessionGitHubState, readSessionMultiRootMetadata, readSessionSourceControlState, readSessionWorkspaceless, SESSION_META_CREATED_BY_SESSION_KEY, SESSION_META_EHCLI_ADOPTABLE_KEY, SESSION_META_EHCLI_ADOPTED_KEY, SESSION_META_FOLDER_PICKER_KEY, SESSION_META_GIT_DATA_KEY, SESSION_META_GIT_KEY, SESSION_META_GITHUB_DATA_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, SESSION_META_WORKSPACELESS_KEY, SessionStatus, SessionSummary } from '../common/state/sessionState.js';
|
||||
import { AH_META_CREATED_BY_SESSION_DB_KEY, AH_META_DEFAULT_CHAT_IS_READ_DB_KEY, AH_META_EHCLI_ADOPTED_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, AH_META_IS_READ_DB_KEY, AH_META_WORKSPACELESS_DB_KEY, ISessionGitHubState, ISessionGitState, ISessionSourceControlState, isChatInSessionReadAggregate, parseSessionCreationReference, parseSessionFolderPickerDecision, parseSessionMultiRootMetadata, readSessionCreationReference, readSessionEhcliAdoptable, readSessionEhcliAdopted, readSessionExternal, readSessionFolderPickerDecision, parseSessionGitHubData, parseSessionGitHubState, readSessionGitData, readSessionGitHubData, readSessionGitState, withMigratedSessionGitHubState, readSessionMultiRootMetadata, readSessionSourceControlState, readSessionWorkspaceless, SESSION_META_CREATED_BY_SESSION_KEY, SESSION_META_EHCLI_ADOPTABLE_KEY, SESSION_META_EHCLI_ADOPTED_KEY, SESSION_META_FOLDER_PICKER_KEY, SESSION_META_GIT_DATA_KEY, SESSION_META_GIT_KEY, SESSION_META_GITHUB_DATA_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, SESSION_META_WORKSPACELESS_KEY, SessionStatus, SessionSummary } from '../common/state/sessionState.js';
|
||||
import { AGENT_HOST_CATALOG_JSON_STRING_LENGTH_LIMIT, AGENT_HOST_CATALOG_TITLE_LENGTH_LIMIT, AgentHostCatalogData, AgentHostCatalogJsonValue, AgentHostCatalogMetadata, agentHostCatalogChangesValidator, agentHostCatalogGitDataValidator, agentHostCatalogGitValidator } from './agentHostCatalogProjection.js';
|
||||
import { IAgentHostCatalogSyncRequest } from './agentHostCatalogSyncService.js';
|
||||
import { AGENT_HOST_TITLE_SOURCE_AUTO, AgentHostTitleSource, customChatTitleMetadataKey, customChatTitleSourceMetadataKey, SESSION_ARTIFACTS_KEY, SESSION_CUSTOM_TITLE_KEY, SESSION_CUSTOM_TITLE_SOURCE_KEY } from './shared/persistSessionMetadata.js';
|
||||
@@ -35,6 +35,7 @@ export interface ICatalogSourceState {
|
||||
readonly origin?: ChatOrigin;
|
||||
readonly interactivity?: ChatInteractivity;
|
||||
readonly archived?: boolean;
|
||||
readonly isRead?: boolean;
|
||||
readonly inheritedTurnId?: string;
|
||||
readonly workingDirectories?: readonly string[];
|
||||
readonly changes?: ChangesSummary;
|
||||
@@ -85,6 +86,7 @@ const sessionMetadata = {
|
||||
title: stringSessionMetadataKey(SESSION_CUSTOM_TITLE_KEY),
|
||||
titleSource: stringSessionMetadataKey(SESSION_CUSTOM_TITLE_SOURCE_KEY),
|
||||
isRead: parsedSessionMetadataKey(AH_META_IS_READ_DB_KEY, value => value === 'true'),
|
||||
defaultChatIsRead: parsedSessionMetadataKey(AH_META_DEFAULT_CHAT_IS_READ_DB_KEY, value => value === 'true'),
|
||||
isArchived: parsedSessionMetadataKey(AH_META_IS_ARCHIVED_DB_KEY, value => value === 'true'),
|
||||
isDone: parsedSessionMetadataKey(AH_META_IS_DONE_DB_KEY, value => value === 'true'),
|
||||
creationReference: parsedSessionMetadataKey(AH_META_CREATED_BY_SESSION_DB_KEY, parseSessionCreationReference),
|
||||
@@ -200,9 +202,18 @@ export class AgentHostCatalogSourceResolver {
|
||||
? persistedWorkspaceless
|
||||
: readSessionWorkspaceless(state.meta) || persistedWorkspaceless;
|
||||
const stateIsRead = (state.status & SessionStatus.IsRead) !== 0;
|
||||
const isRead = preferPersistedMetadata && metadata[AH_META_IS_READ_DB_KEY] !== undefined
|
||||
const persistedSessionIsRead = preferPersistedMetadata && metadata[AH_META_IS_READ_DB_KEY] !== undefined
|
||||
? sessionMetadata.isRead.read(metadata) ?? false
|
||||
: stateIsRead;
|
||||
const chatIsRead = (chat: ICatalogSourceState['chats'][number]): boolean | undefined => preferPersistedMetadata
|
||||
&& chat.kind === 'default'
|
||||
&& sessionMetadata.defaultChatIsRead.has(metadata)
|
||||
? sessionMetadata.defaultChatIsRead.read(metadata)
|
||||
: chat.isRead;
|
||||
const aggregateChats = state.chats.filter(chat => isChatInSessionReadAggregate(chat.uri, chat.origin, chat.interactivity));
|
||||
const isRead = aggregateChats.length === 1 && aggregateChats[0].isRead !== undefined
|
||||
? chatIsRead(aggregateChats[0]) ?? persistedSessionIsRead
|
||||
: persistedSessionIsRead && !aggregateChats.some(chat => chatIsRead(chat) === false);
|
||||
const persistedArchived = sessionMetadata.isArchived.read(metadata) ?? sessionMetadata.isDone.read(metadata);
|
||||
const isArchived = preferPersistedMetadata && persistedArchived !== undefined
|
||||
? persistedArchived
|
||||
@@ -261,6 +272,7 @@ export class AgentHostCatalogSourceResolver {
|
||||
// temporary restriction must not become permanent after a restart.
|
||||
const interactivity = chat.interactivity === ChatInteractivity.ReadOnly && readChatInputState({ _meta: state.meta }, chat.uri)
|
||||
? ChatInteractivity.Full : chat.interactivity;
|
||||
const isRead = chatIsRead(chat);
|
||||
const persistedChatChanges = readPersistedChanges(metadata[getChatChangesSummaryMetadataKey(chat.uri)]);
|
||||
const chatChanges = preferPersistedMetadata
|
||||
? persistedChatChanges ?? chat.changes
|
||||
@@ -274,6 +286,7 @@ export class AgentHostCatalogSourceResolver {
|
||||
origin: toCatalogChatOrigin(chat.origin),
|
||||
...(interactivity !== undefined ? { interactivity } : {}),
|
||||
...(chat.archived === true ? { archived: true } : {}),
|
||||
...(isRead !== undefined ? { isRead } : {}),
|
||||
...(chat.inheritedTurnId !== undefined ? { inheritedTurnId: chat.inheritedTurnId } : {}),
|
||||
...(chat.workingDirectories !== undefined ? { workingDirectories: chat.workingDirectories } : {}),
|
||||
...(chatChanges !== undefined ? { changes: chatChanges } : {}),
|
||||
|
||||
@@ -100,6 +100,7 @@ export interface IAgentHostDatabaseSessionV2 extends IAgentHostDatabaseSessionV2
|
||||
export interface IAgentHostDatabaseSessionChat {
|
||||
readonly chat: string;
|
||||
readonly order: number;
|
||||
readonly isRead?: boolean;
|
||||
readonly archived?: boolean;
|
||||
readonly providerData?: string;
|
||||
readonly origin?: string;
|
||||
@@ -281,6 +282,9 @@ const sessionChatCatalogSchemaSql = [
|
||||
].join(';\n');
|
||||
|
||||
const CHAT_ARCHIVE_MIGRATION_VERSION = 12;
|
||||
const CHAT_READ_MIGRATION_VERSION = 13;
|
||||
const chatArchiveMigrationSql = 'ALTER TABLE session_chats ADD COLUMN archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0, 1))';
|
||||
const chatReadMigrationSql = 'ALTER TABLE session_chats ADD COLUMN is_read INTEGER CHECK (is_read IN (0, 1))';
|
||||
|
||||
const migrations = [
|
||||
{
|
||||
@@ -328,23 +332,52 @@ const migrations = [
|
||||
// Versions 6 through 11 were used by pre-release catalog schemas and are
|
||||
// normalized above, so new migrations resume at 12.
|
||||
version: CHAT_ARCHIVE_MIGRATION_VERSION,
|
||||
sql: 'ALTER TABLE session_chats ADD COLUMN archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0, 1))',
|
||||
sql: chatArchiveMigrationSql,
|
||||
},
|
||||
{
|
||||
version: CHAT_READ_MIGRATION_VERSION,
|
||||
sql: chatReadMigrationSql,
|
||||
},
|
||||
] as const;
|
||||
|
||||
async function normalizePreReleaseCatalogSchema(database: Database, currentVersion: number): Promise<number> {
|
||||
if (currentVersion < 4 || currentVersion > 11 || !await get(database, `SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'sessions_v2'`, [])) {
|
||||
if (currentVersion < 4 || !await get(database, `SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'sessions_v2'`, [])) {
|
||||
return currentVersion;
|
||||
}
|
||||
const hasFinalCatalog = await get(database, `SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'session_chat_catalogs'`, [])
|
||||
&& await get(database, `SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'session_chats'`, []);
|
||||
if (hasFinalCatalog && currentVersion >= 5 && currentVersion < CHAT_ARCHIVE_MIGRATION_VERSION) {
|
||||
if (hasFinalCatalog && currentVersion >= 5) {
|
||||
const chatColumns = await all(database, 'PRAGMA table_info(session_chats)', []);
|
||||
if (chatColumns.some(column => column.name === 'archived')) {
|
||||
const hasArchived = chatColumns.some(column => column.name === 'archived');
|
||||
const hasRead = chatColumns.some(column => column.name === 'is_read');
|
||||
if (currentVersion >= CHAT_READ_MIGRATION_VERSION) {
|
||||
// Temporary compatibility for development profiles shared by worktrees
|
||||
// whose pre-release schema versions can advance independently.
|
||||
if (!hasArchived) {
|
||||
await exec(database, chatArchiveMigrationSql);
|
||||
}
|
||||
if (!hasRead) {
|
||||
await exec(database, chatReadMigrationSql);
|
||||
}
|
||||
return currentVersion;
|
||||
}
|
||||
if (hasArchived && hasRead) {
|
||||
await exec(database, `PRAGMA user_version = ${CHAT_READ_MIGRATION_VERSION}`);
|
||||
return CHAT_READ_MIGRATION_VERSION;
|
||||
}
|
||||
if (hasRead) {
|
||||
await exec(database, chatArchiveMigrationSql);
|
||||
await exec(database, `PRAGMA user_version = ${CHAT_READ_MIGRATION_VERSION}`);
|
||||
return CHAT_READ_MIGRATION_VERSION;
|
||||
}
|
||||
if (hasArchived) {
|
||||
await exec(database, `PRAGMA user_version = ${CHAT_ARCHIVE_MIGRATION_VERSION}`);
|
||||
return CHAT_ARCHIVE_MIGRATION_VERSION;
|
||||
}
|
||||
}
|
||||
if (currentVersion >= CHAT_READ_MIGRATION_VERSION) {
|
||||
return currentVersion;
|
||||
}
|
||||
const isPreReleaseVersion11 = currentVersion === 11;
|
||||
if (hasFinalCatalog && currentVersion >= 5 && !isPreReleaseVersion11) {
|
||||
return currentVersion;
|
||||
@@ -1290,6 +1323,7 @@ export class AgentHostDatabase implements IAgentHostDatabase {
|
||||
(SELECT value FROM metadata WHERE key = ?) AS legacy_mirrored_payload,
|
||||
chat.chat_uri,
|
||||
chat.chat_order,
|
||||
chat.is_read,
|
||||
chat.archived,
|
||||
chat.provider_data,
|
||||
chat.origin,
|
||||
@@ -1309,6 +1343,7 @@ export class AgentHostDatabase implements IAgentHostDatabase {
|
||||
chats: rows.filter(row => row.chat_uri !== null).map(row => ({
|
||||
chat: row.chat_uri as string,
|
||||
order: row.chat_order as number,
|
||||
...(row.is_read === null ? {} : { isRead: row.is_read === 1 }),
|
||||
...(row.archived === 1 ? { archived: true } : {}),
|
||||
...(row.provider_data === null ? {} : { providerData: row.provider_data as string }),
|
||||
...(row.origin === null ? {} : { origin: row.origin as string }),
|
||||
@@ -1375,11 +1410,12 @@ export class AgentHostDatabase implements IAgentHostDatabase {
|
||||
for (let offset = 0; offset < chats.length; offset += SESSION_CHAT_INSERT_BATCH_SIZE) {
|
||||
const batch = chats.slice(offset, offset + SESSION_CHAT_INSERT_BATCH_SIZE);
|
||||
await run(database, `INSERT INTO session_chats (
|
||||
session_uri, chat_uri, chat_order, archived, provider_data, origin, inherited_turn_id
|
||||
) VALUES ${batch.map(() => '(?, ?, ?, ?, ?, ?, ?)').join(', ')}`, batch.flatMap(chat => [
|
||||
session_uri, chat_uri, chat_order, is_read, archived, provider_data, origin, inherited_turn_id
|
||||
) VALUES ${batch.map(() => '(?, ?, ?, ?, ?, ?, ?, ?)').join(', ')}`, batch.flatMap(chat => [
|
||||
session,
|
||||
chat.chat,
|
||||
chat.order,
|
||||
chat.isRead === undefined ? null : chat.isRead ? 1 : 0,
|
||||
chat.archived === true ? 1 : 0,
|
||||
chat.providerData ?? null,
|
||||
chat.origin ?? null,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { StopWatch } from '../../../base/common/stopwatch.js';
|
||||
import type { IAgentTelemetryContext } from '../common/agent.js';
|
||||
import type { AgentSubagentKind, IAgentTelemetryContext } from '../common/agent.js';
|
||||
import type { IAgentHostClientTelemetryContext } from '../common/agentHostTelemetry.js';
|
||||
import { ChatInputRequestPurpose, readChatInputRequestPurpose } from '../common/meta/agentChatInputRequestMeta.js';
|
||||
import type { ChatInputCompletedAction } from '../common/state/sessionActions.js';
|
||||
@@ -31,6 +31,7 @@ export class AgentHostInputRequestTracker {
|
||||
private readonly _stopWatchFactory: () => Pick<StopWatch, 'elapsed'> = () => StopWatch.create(true),
|
||||
private readonly _getClientContext: (session: string, turnId: string) => IAgentHostClientTelemetryContext | undefined = () => undefined,
|
||||
private readonly _getTelemetryContext: (session: string, turnId: string) => IAgentTelemetryContext | undefined = () => undefined,
|
||||
private readonly _getSubagentKind: (session: string, turnId: string) => AgentSubagentKind | undefined = () => undefined,
|
||||
) { }
|
||||
|
||||
inputRequested(provider: string, session: string, turnId: string, request: ChatInputRequest): void {
|
||||
@@ -85,6 +86,7 @@ export class AgentHostInputRequestTracker {
|
||||
telemetryContext: this._getTelemetryContext(timing.session, timing.turnId),
|
||||
provider: timing.provider,
|
||||
session: timing.session,
|
||||
subagentKind: this._getSubagentKind(timing.session, timing.turnId),
|
||||
requestId: timing.turnId,
|
||||
questionCount: questions.length,
|
||||
answeredCount,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ILogService } from '../../log/common/log.js';
|
||||
import { ISessionDataService } from '../common/sessionDataService.js';
|
||||
import type { AgentHostCatalogDatabaseReference } from './agentHostCatalogSyncService.js';
|
||||
import { ChatOrigin } from '../common/state/protocol/state.js';
|
||||
import { buildChatUri, isDefaultChatUri, parseRequiredSessionUriFromChatUri } from '../common/state/sessionState.js';
|
||||
import { AH_META_IS_READ_DB_KEY, buildChatUri, isDefaultChatUri, parseRequiredSessionUriFromChatUri } from '../common/state/sessionState.js';
|
||||
import { fromCatalogChatOrigin, toSerializableJsonValue } from './agentHostCatalogSourceResolver.js';
|
||||
import { AGENT_HOST_CATALOG_CHILD_LIMIT } from './agentHostCatalogProjection.js';
|
||||
import { IAgentHostDatabase } from './agentHostDatabase.js';
|
||||
@@ -31,11 +31,13 @@ export const IAgentHostPeerChatPersistenceService = createDecorator<IAgentHostPe
|
||||
|
||||
export interface IAgentHostPeerChatPersistenceService {
|
||||
readonly _serviceBrand: undefined;
|
||||
setRead(session: URI, chat: URI, isRead: boolean): Promise<void>;
|
||||
setArchived(session: URI, chat: URI, archived: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IPersistedPeerChat {
|
||||
readonly uri: string;
|
||||
readonly isRead?: boolean;
|
||||
readonly archived?: boolean;
|
||||
readonly providerData?: string;
|
||||
readonly origin?: ChatOrigin;
|
||||
@@ -389,6 +391,7 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
const next = entries.filter(entry => entry.uri !== chatUri);
|
||||
next.push({
|
||||
uri: chatUri,
|
||||
...(existing?.isRead !== undefined ? { isRead: existing.isRead } : {}),
|
||||
...(existing?.archived ? { archived: true } : {}),
|
||||
...(providerData !== undefined ? { providerData } : {}),
|
||||
...(effectiveOrigin !== undefined ? { origin: effectiveOrigin } : {}),
|
||||
@@ -406,6 +409,7 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
const next = entries.filter(entry => entry.uri !== chatUri);
|
||||
next.push({
|
||||
uri: chatUri,
|
||||
...(existing?.isRead !== undefined ? { isRead: existing.isRead } : {}),
|
||||
...(existing?.archived ? { archived: true } : {}),
|
||||
...(existing?.providerData !== undefined ? { providerData: existing.providerData } : {}),
|
||||
...(existing?.origin !== undefined ? { origin: existing.origin } : {}),
|
||||
@@ -424,6 +428,14 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
: entry));
|
||||
}
|
||||
|
||||
setRead(session: URI, chat: URI, isRead: boolean): Promise<void> {
|
||||
const chatUri = chat.toString();
|
||||
return this._enqueueWrite(session, entries => entries.map(entry =>
|
||||
entry.uri === chatUri
|
||||
? { ...entry, isRead }
|
||||
: entry));
|
||||
}
|
||||
|
||||
remove(session: URI, chat: URI): Promise<void> {
|
||||
const chatUri = chat.toString();
|
||||
return this._enqueueWrite(session, entries => entries.filter(entry => entry.uri !== chatUri));
|
||||
@@ -557,6 +569,7 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
private _catalogRows(entries: readonly IPersistedPeerChat[]): Array<{
|
||||
readonly chat: string;
|
||||
readonly order: number;
|
||||
readonly isRead?: boolean;
|
||||
readonly archived?: boolean;
|
||||
readonly providerData?: string;
|
||||
readonly origin?: string;
|
||||
@@ -565,6 +578,7 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
return entries.map((entry, order) => ({
|
||||
chat: entry.uri,
|
||||
order,
|
||||
...(entry.isRead !== undefined ? { isRead: entry.isRead } : {}),
|
||||
...(entry.archived === true ? { archived: true } : {}),
|
||||
...(entry.providerData !== undefined ? { providerData: entry.providerData } : {}),
|
||||
...(entry.origin !== undefined ? { origin: this._stringifyOrigin(entry.origin) } : {}),
|
||||
@@ -735,11 +749,15 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
}
|
||||
try {
|
||||
const metadata = await ref.object.getMetadataObject({
|
||||
[AH_META_IS_READ_DB_KEY]: true,
|
||||
[CHAT_PROVIDER_DATA_METADATA_KEY]: true,
|
||||
[CHAT_ORIGIN_METADATA_KEY]: true,
|
||||
[CHAT_INHERITED_TURN_METADATA_KEY]: true,
|
||||
[CHAT_WORKING_DIRECTORIES_METADATA_KEY]: true,
|
||||
});
|
||||
const isRead = metadata[AH_META_IS_READ_DB_KEY] !== undefined
|
||||
? metadata[AH_META_IS_READ_DB_KEY] === 'true'
|
||||
: entry.isRead;
|
||||
const origin = metadata[CHAT_ORIGIN_METADATA_KEY]
|
||||
? this._parseOrigin(metadata[CHAT_ORIGIN_METADATA_KEY])
|
||||
: metadata[CHAT_ORIGIN_METADATA_KEY] === '' ? undefined : entry.origin;
|
||||
@@ -748,6 +766,7 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
: metadata[CHAT_WORKING_DIRECTORIES_METADATA_KEY] === '' ? undefined : entry.workingDirectories;
|
||||
return {
|
||||
uri: entry.uri,
|
||||
...(isRead !== undefined ? { isRead } : {}),
|
||||
...(metadata[CHAT_PROVIDER_DATA_METADATA_KEY] !== undefined
|
||||
? metadata[CHAT_PROVIDER_DATA_METADATA_KEY] ? { providerData: metadata[CHAT_PROVIDER_DATA_METADATA_KEY] } : {}
|
||||
: entry.providerData !== undefined ? { providerData: entry.providerData } : {}),
|
||||
@@ -793,6 +812,7 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
const ref = this._sessionDataService.openDatabase(URI.parse(entry.uri));
|
||||
try {
|
||||
await ref.object.setMetadataValues({
|
||||
...(entry.isRead !== undefined ? { [AH_META_IS_READ_DB_KEY]: entry.isRead ? 'true' : '' } : {}),
|
||||
[CHAT_PROVIDER_DATA_METADATA_KEY]: entry.providerData ?? '',
|
||||
[CHAT_ORIGIN_METADATA_KEY]: entry.origin === undefined ? '' : this._stringifyOrigin(entry.origin),
|
||||
[CHAT_INHERITED_TURN_METADATA_KEY]: entry.inheritedTurnId ?? '',
|
||||
@@ -826,6 +846,7 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
|
||||
private _entriesFromCatalog(chats: readonly {
|
||||
readonly chat: string;
|
||||
readonly isRead?: boolean;
|
||||
readonly providerData?: string;
|
||||
readonly origin?: string;
|
||||
readonly inheritedTurnId?: string;
|
||||
@@ -833,6 +854,7 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
}[]): IPersistedPeerChat[] {
|
||||
return chats.map(chat => ({
|
||||
uri: chat.chat,
|
||||
...(chat.isRead !== undefined ? { isRead: chat.isRead } : {}),
|
||||
...(chat.archived ? { archived: true } : {}),
|
||||
...(chat.providerData !== undefined ? { providerData: chat.providerData } : {}),
|
||||
...(chat.origin !== undefined ? { origin: this._parseOrigin(chat.origin) } : {}),
|
||||
@@ -889,6 +911,10 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
this._logService.warn(`[AgentService] Skipping peer-chat catalog entry ${index} with invalid archived state`);
|
||||
continue;
|
||||
}
|
||||
if (value.isRead !== undefined && typeof value.isRead !== 'boolean') {
|
||||
this._logService.warn(`[AgentService] Skipping peer-chat catalog entry ${index} with invalid read state`);
|
||||
continue;
|
||||
}
|
||||
const originValue = toSerializableJsonValue(value.origin);
|
||||
const origin = fromCatalogChatOrigin(originValue);
|
||||
if (value.origin !== undefined && !origin) {
|
||||
@@ -897,6 +923,7 @@ export class AgentHostPeerChatStore implements IAgentHostPeerChatPersistenceServ
|
||||
seen.add(value.uri);
|
||||
result.push({
|
||||
uri: value.uri,
|
||||
...(typeof value.isRead === 'boolean' ? { isRead: value.isRead } : {}),
|
||||
...(value.archived === true ? { archived: true } : {}),
|
||||
...(typeof value.providerData === 'string' ? { providerData: value.providerData } : {}),
|
||||
...(origin ? { origin } : {}),
|
||||
|
||||
@@ -13,7 +13,7 @@ import { TelemetryLevel } from '../../telemetry/common/telemetry.js';
|
||||
import { ActionType, ActionEnvelope, ActionOrigin, INotification, IRootConfigChangedAction, SessionAction, ChatAction, RootAction, StateAction, TerminalAction, ChangesetAction, ClientChangesetAction, AnnotationsAction, ClientAnnotationsAction, isRootAction, isSessionAction, isChatAction, isChangesetAction, isAnnotationsAction, isAutomationAction, isAutomationRunAction, isPassiveSessionMetadataAction, type AuthRequiredParams, type ClientAutomationAction, type ClientAutomationRunAction, type ProgressParams, type SessionSummaryChangedParams, type SessionSummaryChanges } from '../common/state/sessionActions.js';
|
||||
import type { IStateSnapshot } from '../common/state/sessionProtocol.js';
|
||||
import { rootReducer, sessionReducer, chatReducer, changesetReducer, annotationsReducer, automationReducer, automationRunReducer } from '../common/state/sessionReducers.js';
|
||||
import { createRootState, createSessionState, createChatState, createDefaultChatSummary, chatSummaryFromState, buildDefaultChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseSubagentSessionUri, isAhpChatChannel, isAhpAutomationCatalogChannel, isAhpAutomationRunChannel, isDefaultChatUri, isSessionStatusArchived, mergeSessionWithDefaultChat, isAhpRootChannel, readSessionExternal, SessionLifecycle, withHostBuildInfo, withSessionStatusFlag, type AutomationState, type AutomationRunState, type Changeset, type ChangesetState, type AnnotationsState, type ChatState, type ChatSummary, type Customization, type ISessionWithDefaultChat, type Message, type RootState, type SessionConfigState, type SessionMeta, type SessionState, type SessionSummary, type Turn, type URI, ROOT_STATE_URI, ChangesetStatus, ChatOriginKind, IHostBuildInfo, SessionStatus, ResponsePartKind, ToolCallStatus } from '../common/state/sessionState.js';
|
||||
import { createRootState, createSessionState, createChatState, createDefaultChatSummary, chatSummaryFromState, buildDefaultChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseSubagentSessionUri, isAhpChatChannel, isAhpAutomationCatalogChannel, isAhpAutomationRunChannel, isChatInSessionReadAggregate, isDefaultChatUri, isSessionStatusArchived, isSessionStatusRead, mergeSessionWithDefaultChat, isAhpRootChannel, readSessionExternal, SessionLifecycle, withHostBuildInfo, withSessionStatusFlag, type AutomationState, type AutomationRunState, type Changeset, type ChangesetState, type AnnotationsState, type ChatState, type ChatSummary, type Customization, type ISessionWithDefaultChat, type Message, type RootState, type SessionConfigState, type SessionMeta, type SessionState, type SessionSummary, type Turn, type URI, ROOT_STATE_URI, ChangesetStatus, ChatOriginKind, IHostBuildInfo, SessionStatus, ResponsePartKind, ToolCallStatus } from '../common/state/sessionState.js';
|
||||
import { AgentHostTelemetryLevelConfigKey, IPermissionsValue, platformRootSchema, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js';
|
||||
import { SessionConfigKey } from '../common/sessionConfigKeys.js';
|
||||
import { parseChangesetUri } from '../common/changesetUri.js';
|
||||
@@ -544,6 +544,7 @@ export class AgentHostStateManager extends Disposable {
|
||||
&& chat.title === other.title
|
||||
&& equals(chat.origin, other.origin)
|
||||
&& chat.interactivity === other.interactivity
|
||||
&& chat.status === other.status
|
||||
&& structuralEquals(chat.changes, other.changes);
|
||||
});
|
||||
}
|
||||
@@ -1074,6 +1075,16 @@ export class AgentHostStateManager extends Disposable {
|
||||
this._summaryNotifier.applyAnnouncedChanges(session, { status });
|
||||
}
|
||||
|
||||
/** Updates passive status metadata and optional exact chat read state for an unloaded session. */
|
||||
setSurfacedSessionStatusAndChats(session: string, status: SessionStatus, chats?: SessionSummary['chats']): void {
|
||||
if (!this._sessionStates.has(session) && this._summaryNotifier.getAnnounced(session)) {
|
||||
this._summaryNotifier.applyAnnouncedChanges(session, {
|
||||
status,
|
||||
...(chats ? { chats } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Publishes or unpublishes a live session summary without changing its session state. */
|
||||
setSessionSummaryPublished(session: string, published: boolean): void {
|
||||
if (published) {
|
||||
@@ -1171,7 +1182,7 @@ export class AgentHostStateManager extends Disposable {
|
||||
* summary (e.g. adoptable-legacy), a `sessionSummaryChanged` delta is emitted
|
||||
* so clients update the entry in place instead of dropping it.
|
||||
*/
|
||||
restoreSession(summary: SessionSummary, turns: Turn[], options?: { readonly draft?: Message; readonly defaultChatTitle?: string; readonly defaultChatModifiedAt?: string; readonly defaultChatWorkingDirectories?: readonly string[] }): SessionState {
|
||||
restoreSession(summary: SessionSummary, turns: Turn[], options?: { readonly draft?: Message; readonly defaultChatTitle?: string; readonly defaultChatModifiedAt?: string; readonly defaultChatWorkingDirectories?: readonly string[]; readonly defaultChatIsRead?: boolean }): SessionState {
|
||||
const key = summary.resource;
|
||||
const existing = this._sessionStates.get(key);
|
||||
if (existing) {
|
||||
@@ -1185,7 +1196,7 @@ export class AgentHostStateManager extends Disposable {
|
||||
};
|
||||
const entry = this._newEntry(state, summary, SessionUse.Used);
|
||||
this._sessionStates.set(key, entry);
|
||||
this._ensureDefaultChat(key, summary, turns, options?.draft, options?.defaultChatTitle, options?.defaultChatModifiedAt, options?.defaultChatWorkingDirectories);
|
||||
this._ensureDefaultChat(key, summary, turns, options?.draft, options?.defaultChatTitle, options?.defaultChatModifiedAt, options?.defaultChatWorkingDirectories, options?.defaultChatIsRead);
|
||||
for (const chat of summary.chats ?? []) {
|
||||
if (chat.resource === state.defaultChat || isDefaultChatUri(chat.resource)) {
|
||||
continue;
|
||||
@@ -1194,9 +1205,15 @@ export class AgentHostStateManager extends Disposable {
|
||||
title: chat.title,
|
||||
origin: chat.origin,
|
||||
interactivity: chat.interactivity,
|
||||
archived: chat.status === undefined ? undefined : isSessionStatusArchived(chat.status),
|
||||
isRead: chat.status === undefined ? undefined : isSessionStatusRead(chat.status),
|
||||
changes: chat.changes,
|
||||
});
|
||||
}
|
||||
const restoredState = entry.state;
|
||||
if (restoredState.chats.some(chat => isChatInSessionReadAggregate(chat.resource, chat.origin, chat.interactivity) && !isSessionStatusRead(chat.status))) {
|
||||
restoredState.status = withSessionStatusFlag(restoredState.status, SessionStatus.IsRead, false);
|
||||
}
|
||||
// A session that was previously surfaced (e.g. announced as an
|
||||
// adoptable-legacy session) is already known to clients with a different
|
||||
// summary. Emit the delta so they update the entry in place — clearing the
|
||||
@@ -1211,7 +1228,7 @@ export class AgentHostStateManager extends Disposable {
|
||||
|
||||
this._logService.trace(`[AgentHostStateManager] Restored session: ${key} (${turns.length} turns)`);
|
||||
|
||||
return state;
|
||||
return restoredState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1226,7 +1243,7 @@ export class AgentHostStateManager extends Disposable {
|
||||
* at creation/restore time, so the snapshot a client later receives on
|
||||
* subscribe already reflects the default chat.
|
||||
*/
|
||||
private _ensureDefaultChat(sessionKey: string, summary: SessionSummary, turns?: Turn[], draft?: Message, defaultChatTitle?: string, modifiedAt?: string, workingDirectories?: readonly string[]): void {
|
||||
private _ensureDefaultChat(sessionKey: string, summary: SessionSummary, turns?: Turn[], draft?: Message, defaultChatTitle?: string, modifiedAt?: string, workingDirectories?: readonly string[], isRead?: boolean): void {
|
||||
const chatUri = buildDefaultChatUri(sessionKey);
|
||||
const changes = summary.chats?.find(chat => chat.resource === chatUri)?.changes;
|
||||
// Empty title means "inherit the session title"; a persisted independent
|
||||
@@ -1238,6 +1255,9 @@ export class AgentHostStateManager extends Disposable {
|
||||
...(workingDirectories !== undefined ? { workingDirectories: [...workingDirectories] } : {}),
|
||||
...(changes !== undefined ? { changes } : {}),
|
||||
};
|
||||
if (isRead !== undefined) {
|
||||
chatSummary.status = withSessionStatusFlag(chatSummary.status, SessionStatus.IsRead, isRead);
|
||||
}
|
||||
this._chatEntries.set(chatUri, {
|
||||
session: sessionKey,
|
||||
summary: chatSummary,
|
||||
@@ -1297,7 +1317,7 @@ export class AgentHostStateManager extends Disposable {
|
||||
const chatSummary: ChatSummary = {
|
||||
...createDefaultChatSummary(this._toSummary(session, entry), chatUri),
|
||||
title: options?.title ?? '',
|
||||
status: SessionStatus.Idle,
|
||||
status: SessionStatus.Idle | SessionStatus.IsRead,
|
||||
...(options?.origin ? { origin: options.origin } : {}),
|
||||
interactivity: options?.interactivity,
|
||||
...(options?.workingDirectories !== undefined ? { workingDirectories: [...options.workingDirectories] } : {}),
|
||||
@@ -1319,7 +1339,7 @@ export class AgentHostStateManager extends Disposable {
|
||||
* creating conversation state. The state-manager-owned resolver installs a
|
||||
* complete state only through {@link resolveChatState}.
|
||||
*/
|
||||
registerRestoredChatSummary(session: URI, chatUri: URI, options: { readonly title?: string; readonly modifiedAt?: string; readonly origin?: ChatOrigin; readonly interactivity?: ChatInteractivity; readonly archived?: boolean; readonly draft?: Message; readonly providerData?: string; readonly inheritedTurnId?: string; readonly workingDirectories?: readonly string[]; readonly changes?: ChangesSummary; readonly resolver?: RestoredChatResolver }): ChatSummary | undefined {
|
||||
registerRestoredChatSummary(session: URI, chatUri: URI, options: { readonly title?: string; readonly modifiedAt?: string; readonly origin?: ChatOrigin; readonly interactivity?: ChatInteractivity; readonly archived?: boolean; readonly isRead?: boolean; readonly draft?: Message; readonly providerData?: string; readonly inheritedTurnId?: string; readonly workingDirectories?: readonly string[]; readonly changes?: ChangesSummary; readonly resolver?: RestoredChatResolver }): ChatSummary | undefined {
|
||||
const entry = this._sessionStates.get(session);
|
||||
if (!entry) {
|
||||
this._logService.warn(`[AgentHostStateManager] registerRestoredChatSummary for unknown session: ${session}`);
|
||||
@@ -1330,12 +1350,20 @@ export class AgentHostStateManager extends Disposable {
|
||||
if (existing) {
|
||||
const existingEntry = this._chatEntries.get(chatUri);
|
||||
if (existingEntry && !existingEntry.state && options.resolver) {
|
||||
let status = existing.status ?? SessionStatus.Idle;
|
||||
const hasStatusChange = options.archived !== undefined || options.isRead !== undefined;
|
||||
if (options.archived !== undefined) {
|
||||
status = withSessionStatusFlag(status, SessionStatus.IsArchived, options.archived);
|
||||
}
|
||||
if (options.isRead !== undefined) {
|
||||
status = withSessionStatusFlag(status, SessionStatus.IsRead, options.isRead);
|
||||
}
|
||||
const summary: ChatSummary = {
|
||||
...existing,
|
||||
...(options.modifiedAt !== undefined ? { modifiedAt: options.modifiedAt } : {}),
|
||||
...(options.origin !== undefined ? { origin: options.origin } : {}),
|
||||
interactivity: options.interactivity ?? existing.interactivity,
|
||||
...(options.archived !== undefined ? { status: withSessionStatusFlag(existing.status ?? SessionStatus.Idle, SessionStatus.IsArchived, options.archived) } : {}),
|
||||
...(hasStatusChange ? { status } : {}),
|
||||
...(options.workingDirectories !== undefined ? { workingDirectories: [...options.workingDirectories] } : {}),
|
||||
...(existing.changes === undefined && options.changes !== undefined ? { changes: options.changes } : {}),
|
||||
};
|
||||
@@ -1354,7 +1382,11 @@ export class AgentHostStateManager extends Disposable {
|
||||
...createDefaultChatSummary(this._toSummary(session, entry), chatUri),
|
||||
title: options.title ?? '',
|
||||
...(options.modifiedAt !== undefined ? { modifiedAt: options.modifiedAt } : {}),
|
||||
status: withSessionStatusFlag(SessionStatus.Idle, SessionStatus.IsArchived, options.archived === true),
|
||||
status: withSessionStatusFlag(
|
||||
withSessionStatusFlag(SessionStatus.Idle, SessionStatus.IsArchived, options.archived === true),
|
||||
SessionStatus.IsRead,
|
||||
options.isRead ?? true,
|
||||
),
|
||||
// A persisted catalog entry with no recorded origin is a plain
|
||||
// user-created chat; keep the default rather than restoring it
|
||||
// without provenance.
|
||||
@@ -1909,6 +1941,17 @@ export class AgentHostStateManager extends Disposable {
|
||||
config: preserveProviderBackedRootConfigValues(this._rootState, action.config),
|
||||
};
|
||||
}
|
||||
if (action.type === ActionType.SessionIsReadChanged && action.isRead) {
|
||||
const state = this._sessionStates.get(channel)?.state;
|
||||
const aggregateChats = state?.chats.filter(chat => isChatInSessionReadAggregate(chat.resource, chat.origin, chat.interactivity)) ?? [];
|
||||
if (aggregateChats.length > 1 && aggregateChats.some(chat => !isSessionStatusRead(chat.status))) {
|
||||
action = { ...action, isRead: false };
|
||||
} else if (aggregateChats.length === 1 && !isSessionStatusRead(aggregateChats[0].status)) {
|
||||
this.dispatchServerAction(aggregateChats[0].resource, { type: ActionType.ChatIsReadChanged, isRead: true });
|
||||
}
|
||||
}
|
||||
let sessionToMarkUnread: URI | undefined;
|
||||
let singleChatToSynchronize: { readonly chat: URI; readonly isRead: boolean } | undefined;
|
||||
// Apply to state
|
||||
if (isRootAction(action)) {
|
||||
// `RootConfigChanged` can be a true no-op: the reducer merges/replaces
|
||||
@@ -1941,6 +1984,13 @@ export class AgentHostStateManager extends Disposable {
|
||||
const summaryChanged = !this._summaryFieldsEqual(previousState, newState);
|
||||
entry.state = newState;
|
||||
this._synchronizeChatEntries(key, newState.chats);
|
||||
const aggregateChats = newState.chats.filter(chat => isChatInSessionReadAggregate(chat.resource, chat.origin, chat.interactivity));
|
||||
if (sessionAction.type === ActionType.SessionIsReadChanged && !sessionAction.isRead && aggregateChats.length === 1) {
|
||||
const chat = aggregateChats[0];
|
||||
if (isSessionStatusRead(chat.status)) {
|
||||
singleChatToSynchronize = { chat: chat.resource, isRead: false };
|
||||
}
|
||||
}
|
||||
|
||||
if (previousState.title !== newState.title) {
|
||||
this._onDidChangeSessionTitle.fire({ session: key, title: newState.title });
|
||||
@@ -1986,6 +2036,12 @@ export class AgentHostStateManager extends Disposable {
|
||||
this._pruneChatCanvases(channel, new Set(newChat.canvases?.map(canvas => canvas.resource) ?? []));
|
||||
}
|
||||
this._onChatStateChanged(sessionKey, channel, chat, newChat);
|
||||
if (chatAction.type === ActionType.ChatIsReadChanged && !chatAction.isRead && isChatInSessionReadAggregate(channel, chatEntry.summary.origin, chatEntry.summary.interactivity)) {
|
||||
const session = this._sessionStates.get(sessionKey)?.state;
|
||||
if (session && isSessionStatusRead(session.status)) {
|
||||
sessionToMarkUnread = sessionKey;
|
||||
}
|
||||
}
|
||||
resultingState = newChat;
|
||||
} else {
|
||||
this._logService.warn(`[AgentHostStateManager] Action for unknown chat: ${channel}, type=${action.type}`);
|
||||
@@ -2070,6 +2126,12 @@ export class AgentHostStateManager extends Disposable {
|
||||
|
||||
this._logService.trace(`[AgentHostStateManager] Emitting envelope: seq=${envelope.serverSeq}, channel=${envelope.channel}, type=${action.type}${origin ? `, origin=${origin.clientId}:${origin.clientSeq}` : ''}`);
|
||||
this._onDidEmitEnvelope.fire(envelope);
|
||||
if (singleChatToSynchronize) {
|
||||
this.dispatchServerAction(singleChatToSynchronize.chat, { type: ActionType.ChatIsReadChanged, isRead: singleChatToSynchronize.isRead });
|
||||
}
|
||||
if (sessionToMarkUnread) {
|
||||
this.dispatchServerAction(sessionToMarkUnread, { type: ActionType.SessionIsReadChanged, isRead: false });
|
||||
}
|
||||
|
||||
return resultingState;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
import { TelemetryTrustedValue } from '../../telemetry/common/telemetryUtils.js';
|
||||
import { hash } from '../../../base/common/hash.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { AgentSession, type AgentSubagentTaskModelSource, type AgentTurnProviderCallState, type AgentTurnProviderSessionState, type IAgentTurnDiagnosticSnapshot, type IAgentTokenUsageSummary, type IAgentTelemetryContext } from '../common/agent.js';
|
||||
import { AgentSession, type AgentSubagentKind, type AgentSubagentTaskModelSource, type AgentTurnProviderCallState, type AgentTurnProviderSessionState, type IAgentTurnDiagnosticSnapshot, type IAgentTokenUsageSummary, type IAgentTelemetryContext } from '../common/agent.js';
|
||||
import { isReasoningEffortLevel } from '../common/reasoningEffort.js';
|
||||
import type { SessionMode } from '../common/agentHostSchema.js';
|
||||
import { getTelemetryChatSessionId } from '../common/agentTelemetryCorrelation.js';
|
||||
@@ -80,6 +80,10 @@ export interface IAgentHostEventTelemetry extends IAgentHostInitiatorTelemetry,
|
||||
|
||||
export type IAgentHostEventClassification = IAgentHostInitiatorClassification & IAgentHostCopilotSkuClassification;
|
||||
|
||||
export type IAgentHostSubagentKindClassification = {
|
||||
subagentKind?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What the subagent chat represents, set only when isSubagentSession is true: task for a delegated subagent, or fusionPhase for a presentation-only HydraFusion phase chat that shows part of the parent turn.' };
|
||||
};
|
||||
|
||||
export interface IAgentHostExecutionModeChangedEvent extends IAgentHostEventTelemetry {
|
||||
provider: string;
|
||||
agentSessionId: string;
|
||||
@@ -254,6 +258,7 @@ export interface IAgentHostTurnCompletedEvent extends IAgentHostEventTelemetry,
|
||||
agentSessionId: string;
|
||||
chatSessionId: string;
|
||||
isSubagentSession: boolean;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
turnId: string;
|
||||
parentTurnId: string | undefined;
|
||||
parentToolCallId: string | undefined;
|
||||
@@ -301,7 +306,7 @@ export interface IAgentHostTurnCompletedEvent extends IAgentHostEventTelemetry,
|
||||
modelCallCount: number;
|
||||
}
|
||||
|
||||
export type IAgentHostTurnCompletedClassification = IAgentHostEventClassification & CodexAccountTelemetryClassification & {
|
||||
export type IAgentHostTurnCompletedClassification = IAgentHostEventClassification & CodexAccountTelemetryClassification & IAgentHostSubagentKindClassification & {
|
||||
hostRootTurnOrdinal?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'One-based root-turn ordinal over the agent host process lifetime, captured at turn start and excluding subagent turns.' };
|
||||
hostProcessAgeMs?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Agent host process age in milliseconds captured at root turn start, not completion.' };
|
||||
titleGenerationStrategy?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The effective persisted automatic title generation strategy captured before sending the root turn: activeAgent, utility, or deferred.' };
|
||||
@@ -363,6 +368,7 @@ export interface IAgentHostTurnFailedEvent extends IAgentHostEventTelemetry {
|
||||
agentSessionId: string;
|
||||
chatSessionId: string;
|
||||
isSubagentSession: boolean;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
turnId: string;
|
||||
messageOriginKind: AgentHostMessageOriginTelemetryKind | undefined;
|
||||
failureStage: AgentHostTurnFailureStage;
|
||||
@@ -375,7 +381,7 @@ export interface IAgentHostTurnFailedEvent extends IAgentHostEventTelemetry {
|
||||
callstack: string | undefined;
|
||||
}
|
||||
|
||||
export type IAgentHostTurnFailedClassification = IAgentHostEventClassification & {
|
||||
export type IAgentHostTurnFailedClassification = IAgentHostEventClassification & IAgentHostSubagentKindClassification & {
|
||||
provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the failed agent host turn.' };
|
||||
agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' };
|
||||
chatSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The chat identifier within the agent host session.' };
|
||||
@@ -409,6 +415,7 @@ export interface IAgentHostTurnCompletedReport extends IAgentHostTurnAttributedR
|
||||
titleGenerationStrategy?: AutomaticTitleGenerationStrategy;
|
||||
provider: string;
|
||||
session: string;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
turnId: string;
|
||||
parentTurnId: string | undefined;
|
||||
parentToolCallId: string | undefined;
|
||||
@@ -452,6 +459,7 @@ interface IRequestTokenUsageEvent extends IAgentHostEventTelemetry, Omit<IAgentT
|
||||
parentTurnId?: string;
|
||||
parentToolCallId?: string;
|
||||
isSubagentSession: boolean;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
model: string | TelemetryTrustedValue<string>;
|
||||
selectedModel?: string | TelemetryTrustedValue<string>;
|
||||
result: AgentHostTurnResult;
|
||||
@@ -461,7 +469,7 @@ interface IRequestTokenUsageEvent extends IAgentHostEventTelemetry, Omit<IAgentT
|
||||
reasoningEffortSource: 'sdkReported';
|
||||
}
|
||||
|
||||
type RequestTokenUsageClassification = IAgentHostEventClassification & {
|
||||
type RequestTokenUsageClassification = IAgentHostEventClassification & IAgentHostSubagentKindClassification & {
|
||||
provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Agent provider.' };
|
||||
agentSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Opaque owning session identifier.' };
|
||||
chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Opaque owning chat identifier.' };
|
||||
@@ -563,6 +571,7 @@ export interface IAgentHostTurnHungEvent extends IAgentHostEventTelemetry, Parti
|
||||
agentSessionId: string;
|
||||
chatSessionId: string;
|
||||
isSubagentSession: boolean;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
turnId: string;
|
||||
messageOriginKind: AgentHostMessageOriginTelemetryKind | undefined;
|
||||
hangReason: AgentHostTurnHangReason;
|
||||
@@ -586,7 +595,7 @@ export interface IAgentHostTurnHungEvent extends IAgentHostEventTelemetry, Parti
|
||||
permissionLevel: string | undefined;
|
||||
}
|
||||
|
||||
export type IAgentHostTurnHungClassification = IAgentHostEventClassification & CodexAccountTelemetryClassification & {
|
||||
export type IAgentHostTurnHungClassification = IAgentHostEventClassification & CodexAccountTelemetryClassification & IAgentHostSubagentKindClassification & {
|
||||
provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the hung agent host turn.' };
|
||||
agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' };
|
||||
chatSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The chat identifier within the agent host session.' };
|
||||
@@ -620,6 +629,7 @@ export interface IAgentHostTurnHungReport extends IAgentHostTurnAttributedReport
|
||||
providerTelemetryContext?: IAgentProviderTurnTelemetryContext;
|
||||
provider: string;
|
||||
session: string;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
turnId: string;
|
||||
messageOriginKind: AgentHostMessageOriginTelemetryKind | undefined;
|
||||
hangReason: AgentHostTurnHangReason;
|
||||
@@ -646,6 +656,7 @@ export interface IAgentHostHungTurnCompletedEvent extends IAgentHostEventTelemet
|
||||
agentSessionId: string;
|
||||
chatSessionId: string;
|
||||
isSubagentSession: boolean;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
turnId: string;
|
||||
messageOriginKind: AgentHostMessageOriginTelemetryKind | undefined;
|
||||
hangReason: AgentHostTurnHangReason;
|
||||
@@ -655,7 +666,7 @@ export interface IAgentHostHungTurnCompletedEvent extends IAgentHostEventTelemet
|
||||
timeAfterHangMs: number;
|
||||
}
|
||||
|
||||
export type IAgentHostHungTurnCompletedClassification = IAgentHostEventClassification & {
|
||||
export type IAgentHostHungTurnCompletedClassification = IAgentHostEventClassification & IAgentHostSubagentKindClassification & {
|
||||
provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the recovered agent host turn.' };
|
||||
agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' };
|
||||
chatSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The chat identifier within the agent host session.' };
|
||||
@@ -674,6 +685,7 @@ export type IAgentHostHungTurnCompletedClassification = IAgentHostEventClassific
|
||||
export interface IAgentHostHungTurnCompletedReport extends IAgentHostTurnAttributedReport {
|
||||
provider: string;
|
||||
session: string;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
turnId: string;
|
||||
messageOriginKind: AgentHostMessageOriginTelemetryKind | undefined;
|
||||
hangReason: AgentHostTurnHangReason;
|
||||
@@ -694,6 +706,7 @@ type IAgentHostLanguageModelToolInvokedClassification = LanguageModelToolInvoked
|
||||
export interface IAgentHostToolInvokedReport extends IAgentHostTurnAttributedReport {
|
||||
provider: string;
|
||||
session: string;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
turnId: string;
|
||||
toolId: string;
|
||||
toolSourceKind: string;
|
||||
@@ -713,12 +726,13 @@ export type IAgentHostToolInvokedEvent = LanguageModelToolInvokedEvent & IAgentH
|
||||
agentSessionId: string;
|
||||
chatSessionId: string;
|
||||
isSubagentSession: boolean;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
errorCode: string | undefined;
|
||||
msg: string | undefined;
|
||||
mcpSourceKind?: McpServerSource;
|
||||
};
|
||||
|
||||
export type IAgentHostToolInvokedClassification = Omit<LanguageModelToolInvokedClassification, 'provider' | 'chatSessionId' | 'owner' | 'comment'> & IAgentHostEventClassification & {
|
||||
export type IAgentHostToolInvokedClassification = Omit<LanguageModelToolInvokedClassification, 'provider' | 'chatSessionId' | 'owner' | 'comment'> & IAgentHostEventClassification & IAgentHostSubagentKindClassification & {
|
||||
provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The Agent Host provider that invoked the tool.' };
|
||||
agentSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The Agent Host session identifier.' };
|
||||
chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat identifier within the Agent Host session.' };
|
||||
@@ -742,9 +756,10 @@ export interface IAgentHostAskQuestionsToolInvokedEvent extends IAgentHostEventT
|
||||
provider: string;
|
||||
agentSessionId: string;
|
||||
isSubagentSession: boolean;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
}
|
||||
|
||||
export type IAgentHostAskQuestionsToolInvokedClassification = IAgentHostEventClassification & {
|
||||
export type IAgentHostAskQuestionsToolInvokedClassification = IAgentHostEventClassification & IAgentHostSubagentKindClassification & {
|
||||
requestId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the current request turn.' };
|
||||
questionCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The total number of questions asked' };
|
||||
answeredCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of questions that were answered' };
|
||||
@@ -763,6 +778,7 @@ export type IAgentHostAskQuestionsToolInvokedClassification = IAgentHostEventCla
|
||||
export interface IAgentHostAskQuestionsToolInvokedReport extends IAgentHostTurnAttributedReport {
|
||||
provider: string;
|
||||
session: string;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
requestId: string;
|
||||
questionCount: number;
|
||||
answeredCount: number;
|
||||
@@ -945,6 +961,7 @@ export interface IAgentHostToolCallStalledEvent extends IAgentHostEventTelemetry
|
||||
provider: string;
|
||||
agentSessionId: string;
|
||||
isSubagentSession: boolean;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication;
|
||||
toolCallId: string;
|
||||
toolId: string;
|
||||
@@ -955,7 +972,7 @@ export interface IAgentHostToolCallStalledEvent extends IAgentHostEventTelemetry
|
||||
|
||||
export type AgentHostExecutorClientConnectionState = 'connected' | 'disconnected' | 'unknown';
|
||||
|
||||
export type IAgentHostToolCallStalledClassification = IAgentHostEventClassification & {
|
||||
export type IAgentHostToolCallStalledClassification = IAgentHostEventClassification & IAgentHostSubagentKindClassification & {
|
||||
provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the stalled agent host tool call.' };
|
||||
agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' };
|
||||
isSubagentSession: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the stalled tool call belongs to a subagent session.' };
|
||||
@@ -972,6 +989,7 @@ export type IAgentHostToolCallStalledClassification = IAgentHostEventClassificat
|
||||
export interface IAgentHostToolCallStalledReport extends IAgentHostTurnAttributedReport {
|
||||
provider: string;
|
||||
session: string;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication;
|
||||
toolCallId: string;
|
||||
toolId: string;
|
||||
@@ -984,6 +1002,7 @@ export interface IAgentHostStalledToolCallCompletedEvent extends IAgentHostEvent
|
||||
provider: string;
|
||||
agentSessionId: string;
|
||||
isSubagentSession: boolean;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication;
|
||||
toolCallId: string;
|
||||
toolId: string;
|
||||
@@ -994,7 +1013,7 @@ export interface IAgentHostStalledToolCallCompletedEvent extends IAgentHostEvent
|
||||
timeAfterStallMs: number;
|
||||
}
|
||||
|
||||
export type IAgentHostStalledToolCallCompletedClassification = IAgentHostEventClassification & {
|
||||
export type IAgentHostStalledToolCallCompletedClassification = IAgentHostEventClassification & IAgentHostSubagentKindClassification & {
|
||||
provider: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The provider handling the completed agent host tool call.' };
|
||||
agentSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The agent host session identifier.' };
|
||||
isSubagentSession: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the completed tool call belongs to a subagent session.' };
|
||||
@@ -1013,6 +1032,7 @@ export type IAgentHostStalledToolCallCompletedClassification = IAgentHostEventCl
|
||||
export interface IAgentHostStalledToolCallCompletedReport extends IAgentHostTurnAttributedReport {
|
||||
provider: string;
|
||||
session: string;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
blockerKind: SessionInputRequestKind.ToolConfirmation | SessionInputRequestKind.ToolClientExecution | SessionInputRequestKind.ToolAuthentication;
|
||||
toolCallId: string;
|
||||
toolId: string;
|
||||
@@ -1081,6 +1101,10 @@ export function toInitiatorTelemetry(clientContext: IAgentHostClientTelemetryCon
|
||||
|
||||
export const IAgentHostTelemetryReporter = createDecorator<AgentHostTelemetryReporter>('agentHostTelemetryReporter');
|
||||
|
||||
export function toSubagentKindTelemetry(isSubagent: boolean, kind: AgentSubagentKind | undefined): { subagentKind?: AgentSubagentKind } {
|
||||
return isSubagent ? { subagentKind: kind ?? 'task' } : {};
|
||||
}
|
||||
|
||||
export class AgentHostTelemetryReporter {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
@@ -1479,8 +1503,9 @@ export class AgentHostTelemetryReporter {
|
||||
restricted.sendInternalMSFTTelemetryEventForContext(context, 'request.repoInfo', internalMultiplexedProperties, measurements);
|
||||
}
|
||||
|
||||
requestTokenUsage(report: { clientContext: IAgentHostClientTelemetryContext; telemetryContext?: IAgentTelemetryContext; provider: string; session: string; requestId: string; parentTurnId?: string; parentToolCallId?: string; selectedModel?: string; selectedModelTelemetryKind?: AgentHostModelTelemetryKind; modelTelemetryKind?: AgentHostModelTelemetryKind; result: AgentHostTurnResult; summary: IAgentTokenUsageSummary }): void {
|
||||
requestTokenUsage(report: { clientContext: IAgentHostClientTelemetryContext; telemetryContext?: IAgentTelemetryContext; provider: string; session: string; requestId: string; parentTurnId?: string; parentToolCallId?: string; subagentKind?: AgentSubagentKind; selectedModel?: string; selectedModelTelemetryKind?: AgentHostModelTelemetryKind; modelTelemetryKind?: AgentHostModelTelemetryKind; result: AgentHostTurnResult; summary: IAgentTokenUsageSummary }): void {
|
||||
const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session;
|
||||
const isSubagent = isSubagentChatUri(report.session) || isSubagentSession(session);
|
||||
const { model: usageModel, reasoningEffort, ...summary } = report.summary;
|
||||
const model = usageModel === 'auto' ? 'unknown' : toTelemetryModel(usageModel, report.modelTelemetryKind) ?? 'unknown';
|
||||
const selectedModel = toTelemetryModel(report.selectedModel, report.selectedModelTelemetryKind);
|
||||
@@ -1495,7 +1520,8 @@ export class AgentHostTelemetryReporter {
|
||||
requestId: report.requestId,
|
||||
parentTurnId: report.parentTurnId,
|
||||
parentToolCallId: report.parentToolCallId,
|
||||
isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session),
|
||||
isSubagentSession: isSubagent,
|
||||
...toSubagentKindTelemetry(isSubagent, report.subagentKind),
|
||||
model,
|
||||
...(selectedModel !== undefined ? { selectedModel } : {}),
|
||||
...(effort !== undefined ? { reasoningEffort: effort } : {}),
|
||||
@@ -1511,6 +1537,7 @@ export class AgentHostTelemetryReporter {
|
||||
const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session;
|
||||
const chatSessionId = getTelemetryChatSessionId(report.session);
|
||||
const isSubagent = isSubagentChatUri(report.session) || isSubagentSession(session);
|
||||
const subagentKindTelemetry = toSubagentKindTelemetry(isSubagent, report.subagentKind);
|
||||
const model = toTelemetryModel(report.model, report.modelTelemetryKind);
|
||||
const providerStages = toProviderStageMeasurements(report.providerStageDurationsMs);
|
||||
this._otelService?.emitTurnTiming({
|
||||
@@ -1546,6 +1573,7 @@ export class AgentHostTelemetryReporter {
|
||||
agentSessionId: AgentSession.id(session),
|
||||
chatSessionId,
|
||||
isSubagentSession: isSubagent,
|
||||
...subagentKindTelemetry,
|
||||
turnId: report.turnId,
|
||||
parentTurnId: report.parentTurnId,
|
||||
parentToolCallId: report.parentToolCallId,
|
||||
@@ -1592,6 +1620,7 @@ export class AgentHostTelemetryReporter {
|
||||
agentSessionId: AgentSession.id(session),
|
||||
chatSessionId,
|
||||
isSubagentSession: isSubagent,
|
||||
...subagentKindTelemetry,
|
||||
turnId: report.turnId,
|
||||
messageOriginKind: report.messageOriginKind,
|
||||
failureStage: report.failure.stage,
|
||||
@@ -1613,6 +1642,7 @@ export class AgentHostTelemetryReporter {
|
||||
*/
|
||||
turnHung(report: IAgentHostTurnHungReport): void {
|
||||
const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session;
|
||||
const isSubagent = isSubagentChatUri(report.session) || isSubagentSession(session);
|
||||
this._telemetryService.publicLog2<IAgentHostTurnHungEvent, IAgentHostTurnHungClassification>('agentHost.turnHung', {
|
||||
...toInitiatorTelemetry(report.clientContext),
|
||||
...report.telemetryContext,
|
||||
@@ -1620,7 +1650,8 @@ export class AgentHostTelemetryReporter {
|
||||
provider: report.provider,
|
||||
agentSessionId: AgentSession.id(session),
|
||||
chatSessionId: getTelemetryChatSessionId(report.session),
|
||||
isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session),
|
||||
isSubagentSession: isSubagent,
|
||||
...toSubagentKindTelemetry(isSubagent, report.subagentKind),
|
||||
turnId: report.turnId,
|
||||
messageOriginKind: report.messageOriginKind,
|
||||
hangReason: report.hangReason,
|
||||
@@ -1650,13 +1681,15 @@ export class AgentHostTelemetryReporter {
|
||||
/** Paired recovery event for a turn previously reported by {@link turnHung}. */
|
||||
hungTurnCompleted(report: IAgentHostHungTurnCompletedReport): void {
|
||||
const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session;
|
||||
const isSubagent = isSubagentChatUri(report.session) || isSubagentSession(session);
|
||||
this._telemetryService.publicLog2<IAgentHostHungTurnCompletedEvent, IAgentHostHungTurnCompletedClassification>('agentHost.hungTurnCompleted', {
|
||||
...toInitiatorTelemetry(report.clientContext),
|
||||
...report.telemetryContext,
|
||||
provider: report.provider,
|
||||
agentSessionId: AgentSession.id(session),
|
||||
chatSessionId: getTelemetryChatSessionId(report.session),
|
||||
isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session),
|
||||
isSubagentSession: isSubagent,
|
||||
...toSubagentKindTelemetry(isSubagent, report.subagentKind),
|
||||
turnId: report.turnId,
|
||||
messageOriginKind: report.messageOriginKind,
|
||||
hangReason: report.hangReason,
|
||||
@@ -1672,6 +1705,7 @@ export class AgentHostTelemetryReporter {
|
||||
// previously emitted by `CopilotAgentSession`). Action signals are keyed
|
||||
// by their chat-channel URI, so normalize it back to the session URI.
|
||||
const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session;
|
||||
const isSubagent = isSubagentChatUri(report.session) || isSubagentSession(session);
|
||||
this._telemetryService.publicLog2<IAgentHostLanguageModelToolInvokedEvent, IAgentHostLanguageModelToolInvokedClassification>('languageModelToolInvoked', {
|
||||
...toInitiatorTelemetry(report.clientContext),
|
||||
...report.telemetryContext,
|
||||
@@ -1694,7 +1728,8 @@ export class AgentHostTelemetryReporter {
|
||||
result: report.result,
|
||||
agentSessionId: AgentSession.id(session),
|
||||
chatSessionId: getTelemetryChatSessionId(report.session),
|
||||
isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session),
|
||||
isSubagentSession: isSubagent,
|
||||
...toSubagentKindTelemetry(isSubagent, report.subagentKind),
|
||||
toolId: report.toolId,
|
||||
toolExtensionId: undefined,
|
||||
toolSourceKind: report.toolSourceKind,
|
||||
@@ -1713,6 +1748,7 @@ export class AgentHostTelemetryReporter {
|
||||
|
||||
askQuestionsToolInvoked(report: IAgentHostAskQuestionsToolInvokedReport): void {
|
||||
const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session;
|
||||
const isSubagent = isSubagentChatUri(report.session) || isSubagentSession(session);
|
||||
this._telemetryService.publicLog2<IAgentHostAskQuestionsToolInvokedEvent, IAgentHostAskQuestionsToolInvokedClassification>('askQuestionsToolInvoked', {
|
||||
...toInitiatorTelemetry(report.clientContext),
|
||||
...report.telemetryContext,
|
||||
@@ -1726,18 +1762,21 @@ export class AgentHostTelemetryReporter {
|
||||
duration: report.duration,
|
||||
provider: report.provider,
|
||||
agentSessionId: AgentSession.id(session),
|
||||
isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session),
|
||||
isSubagentSession: isSubagent,
|
||||
...toSubagentKindTelemetry(isSubagent, report.subagentKind),
|
||||
});
|
||||
}
|
||||
|
||||
toolCallStalled(report: IAgentHostToolCallStalledReport): void {
|
||||
const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session;
|
||||
const isSubagent = isSubagentChatUri(report.session) || isSubagentSession(session);
|
||||
this._telemetryService.publicLog2<IAgentHostToolCallStalledEvent, IAgentHostToolCallStalledClassification>('agentHost.toolCallStalled', {
|
||||
...toInitiatorTelemetry(report.clientContext),
|
||||
...report.telemetryContext,
|
||||
provider: report.provider,
|
||||
agentSessionId: AgentSession.id(session),
|
||||
isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session),
|
||||
isSubagentSession: isSubagent,
|
||||
...toSubagentKindTelemetry(isSubagent, report.subagentKind),
|
||||
blockerKind: report.blockerKind,
|
||||
toolCallId: report.toolCallId,
|
||||
toolId: report.toolId,
|
||||
@@ -1749,12 +1788,14 @@ export class AgentHostTelemetryReporter {
|
||||
|
||||
stalledToolCallCompleted(report: IAgentHostStalledToolCallCompletedReport): void {
|
||||
const session = isAhpChatChannel(report.session) ? parseRequiredSessionUriFromChatUri(report.session) : report.session;
|
||||
const isSubagent = isSubagentChatUri(report.session) || isSubagentSession(session);
|
||||
this._telemetryService.publicLog2<IAgentHostStalledToolCallCompletedEvent, IAgentHostStalledToolCallCompletedClassification>('agentHost.stalledToolCallCompleted', {
|
||||
...toInitiatorTelemetry(report.clientContext),
|
||||
...report.telemetryContext,
|
||||
provider: report.provider,
|
||||
agentSessionId: AgentSession.id(session),
|
||||
isSubagentSession: isSubagentChatUri(report.session) || isSubagentSession(session),
|
||||
isSubagentSession: isSubagent,
|
||||
...toSubagentKindTelemetry(isSubagent, report.subagentKind),
|
||||
blockerKind: report.blockerKind,
|
||||
toolCallId: report.toolCallId,
|
||||
toolId: report.toolId,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
|
||||
import { StopWatch } from '../../../base/common/stopwatch.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import type { IAgentHostClientTelemetryContext } from '../common/agentHostTelemetry.js';
|
||||
import type { IAgentTelemetryContext } from '../common/agent.js';
|
||||
import type { AgentSubagentKind, IAgentTelemetryContext } from '../common/agent.js';
|
||||
import type { McpServerSource } from '../common/meta/mcpCustomizationMeta.js';
|
||||
import { SessionInputRequestKind, type SessionToolAuthenticationRequest, type SessionToolClientExecutionRequest, type SessionToolConfirmationRequest } from '../common/state/protocol/state.js';
|
||||
import { type ToolCallContributor, type ToolCallResult } from '../common/state/sessionState.js';
|
||||
@@ -56,6 +56,7 @@ interface IToolCallTiming {
|
||||
modelResolvedFromUsage: boolean;
|
||||
readonly clientContext: IAgentHostClientTelemetryContext | undefined;
|
||||
readonly telemetryContext: IAgentTelemetryContext | undefined;
|
||||
readonly subagentKind: AgentSubagentKind | undefined;
|
||||
}
|
||||
|
||||
interface IStalledToolCall {
|
||||
@@ -114,6 +115,7 @@ export class AgentHostToolCallTracker extends Disposable {
|
||||
modelResolvedFromUsage: resolvedModel !== undefined,
|
||||
clientContext: this._turnTracker.getClientTelemetryContext(session, turnId),
|
||||
telemetryContext: this._turnTracker.getTelemetryContext(session, turnId),
|
||||
subagentKind: this._turnTracker.getSubagentKind(session, turnId),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,6 +176,7 @@ export class AgentHostToolCallTracker extends Disposable {
|
||||
telemetryContext: timing.telemetryContext,
|
||||
provider: timing.provider,
|
||||
session: timing.session,
|
||||
subagentKind: timing.subagentKind,
|
||||
turnId: timing.turnId,
|
||||
toolId: timing.toolId,
|
||||
toolSourceKind: timing.toolSourceKind,
|
||||
@@ -203,6 +206,7 @@ export class AgentHostToolCallTracker extends Disposable {
|
||||
telemetryContext: timing.telemetryContext,
|
||||
provider: timing.provider,
|
||||
session: timing.session,
|
||||
subagentKind: timing.subagentKind,
|
||||
blockerKind: stalled.blockerKind,
|
||||
toolCallId,
|
||||
toolId: timing.toolId,
|
||||
@@ -233,6 +237,7 @@ export class AgentHostToolCallTracker extends Disposable {
|
||||
telemetryContext: this._toolCalls.get(toolCallKey)?.telemetryContext,
|
||||
provider,
|
||||
session,
|
||||
subagentKind: this._toolCalls.get(toolCallKey)?.subagentKind,
|
||||
blockerKind: request.kind,
|
||||
toolCallId: request.toolCall.toolCallId,
|
||||
toolId: request.toolCall.toolName,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Disposable, DisposableMap, toDisposable } from '../../../base/common/li
|
||||
import { StopWatch } from '../../../base/common/stopwatch.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import type { AgentModelCallFinishedOutcome, AgentSubagentTaskModelSource, IAgent, IAgentTelemetryContext, IAgentTokenUsageSummary, IAgentTurnDiagnosticSnapshot, IAgentTurnTokenUsage } from '../common/agent.js';
|
||||
import type { AgentModelCallFinishedOutcome, AgentSubagentKind, AgentSubagentTaskModelSource, IAgent, IAgentTelemetryContext, IAgentTokenUsageSummary, IAgentTurnDiagnosticSnapshot, IAgentTurnTokenUsage } from '../common/agent.js';
|
||||
import type { SessionMode } from '../common/agentHostSchema.js';
|
||||
import { createUnknownAgentHostClientTelemetryContext, type AgentHostProviderSendStage, type IAgentHostClientTelemetryContext, type IAgentProviderSendStageRecorder, type IAgentProviderTurnTelemetryContext } from '../common/agentHostTelemetry.js';
|
||||
import { AgentHostClientType } from '../common/agentHostClientInfo.js';
|
||||
@@ -82,6 +82,7 @@ interface ITurnTiming {
|
||||
/** Who produced the message that started the turn, when known. */
|
||||
readonly messageOriginKind: AgentHostMessageOriginTelemetryKind | undefined;
|
||||
readonly subagentTaskModelSource: AgentSubagentTaskModelSource | undefined;
|
||||
readonly subagentKind: AgentSubagentKind | undefined;
|
||||
readonly clientContext: IAgentHostClientTelemetryContext;
|
||||
telemetryContext: IAgentTelemetryContext | undefined;
|
||||
readonly providerTelemetryContext: IAgentProviderTurnTelemetryContext | undefined;
|
||||
@@ -209,7 +210,7 @@ export class AgentHostTurnTracker extends Disposable {
|
||||
}));
|
||||
}
|
||||
|
||||
turnStarted(agent: IAgent, session: string, turnId: string, model: string | undefined, modelTelemetryKind: AgentHostModelTelemetryKind | undefined, modelSelectionKind: 'default' | 'auto' | 'explicit', permissionLevel: string | undefined, interactionMode: SessionMode | undefined, clientContext = createUnknownAgentHostClientTelemetryContext(AgentHostClientType.Unknown), initiatorClientId?: string, parentTurnId?: string, parentToolCallId?: string, messageOriginKind?: AgentHostMessageOriginTelemetryKind, subagentTaskModelSource?: AgentSubagentTaskModelSource, providerChat = URI.parse(session)): void {
|
||||
turnStarted(agent: IAgent, session: string, turnId: string, model: string | undefined, modelTelemetryKind: AgentHostModelTelemetryKind | undefined, modelSelectionKind: 'default' | 'auto' | 'explicit', permissionLevel: string | undefined, interactionMode: SessionMode | undefined, clientContext = createUnknownAgentHostClientTelemetryContext(AgentHostClientType.Unknown), initiatorClientId?: string, parentTurnId?: string, parentToolCallId?: string, messageOriginKind?: AgentHostMessageOriginTelemetryKind, subagentTaskModelSource?: AgentSubagentTaskModelSource, providerChat = URI.parse(session), subagentKind?: AgentSubagentKind): void {
|
||||
const key = this._key(session, turnId);
|
||||
let rootTiming = this._rootTurnTimings.get(key);
|
||||
const isNewRootTurn = !parentTurnId && !isSubagentChatUri(session) && !isSubagentSession(parseChatUri(session)?.session ?? session) && !rootTiming;
|
||||
@@ -239,6 +240,7 @@ export class AgentHostTurnTracker extends Disposable {
|
||||
interactionMode,
|
||||
messageOriginKind,
|
||||
subagentTaskModelSource,
|
||||
subagentKind,
|
||||
clientContext,
|
||||
telemetryContext: agent.getTelemetryContext?.(),
|
||||
providerTelemetryContext: captureProviderTurnTelemetryContext(agent),
|
||||
@@ -643,6 +645,10 @@ export class AgentHostTurnTracker extends Disposable {
|
||||
return this._turnTimings.get(this._key(session, turnId))?.providerTelemetryContext;
|
||||
}
|
||||
|
||||
getSubagentKind(session: string, turnId: string): AgentSubagentKind | undefined {
|
||||
return this._turnTimings.get(this._key(session, turnId))?.subagentKind;
|
||||
}
|
||||
|
||||
getMessageOriginKind(session: string, turnId: string): AgentHostMessageOriginTelemetryKind | undefined {
|
||||
return this._turnTimings.get(this._key(session, turnId))?.messageOriginKind;
|
||||
}
|
||||
@@ -719,6 +725,7 @@ export class AgentHostTurnTracker extends Disposable {
|
||||
interactionMode: timing.interactionMode,
|
||||
messageOriginKind: timing.messageOriginKind,
|
||||
subagentTaskModelSource: timing.subagentTaskModelSource,
|
||||
subagentKind: timing.subagentKind,
|
||||
failure,
|
||||
isMultiRoot: workspace?.isMultiRoot ?? false,
|
||||
folderCount: workspace?.folderCount ?? 0,
|
||||
@@ -739,6 +746,7 @@ export class AgentHostTurnTracker extends Disposable {
|
||||
provider: timing.agent.id,
|
||||
session: timing.session,
|
||||
turnId,
|
||||
subagentKind: timing.subagentKind,
|
||||
messageOriginKind: timing.messageOriginKind,
|
||||
hangReason: timing.lastHangReason,
|
||||
result,
|
||||
@@ -752,7 +760,7 @@ export class AgentHostTurnTracker extends Disposable {
|
||||
this._reporter.requestTokenUsage({
|
||||
clientContext: timing.clientContext, provider: timing.agent.id,
|
||||
telemetryContext: timing.telemetryContext,
|
||||
session, requestId: turnId, parentTurnId: timing.parentTurnId, parentToolCallId: timing.parentToolCallId,
|
||||
session, requestId: turnId, parentTurnId: timing.parentTurnId, parentToolCallId: timing.parentToolCallId, subagentKind: timing.subagentKind,
|
||||
selectedModel: timing.selectedModel, selectedModelTelemetryKind: timing.selectedModelTelemetryKind,
|
||||
modelTelemetryKind: summary.model ? getModelTelemetryContext(timing.agent, summary.model).modelTelemetryKind : undefined,
|
||||
result, summary,
|
||||
@@ -847,6 +855,7 @@ export class AgentHostTurnTracker extends Disposable {
|
||||
provider: timing.agent.id,
|
||||
session: timing.session,
|
||||
turnId: timing.turnId,
|
||||
subagentKind: timing.subagentKind,
|
||||
messageOriginKind: timing.messageOriginKind,
|
||||
hangReason,
|
||||
hadAnyProgress: timing.lastActivityKind !== TURN_ACTIVITY_NONE,
|
||||
|
||||
@@ -41,7 +41,7 @@ import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } f
|
||||
import { AhpErrorCodes, AHP_SESSION_NOT_FOUND, ContentEncoding, JSON_RPC_INTERNAL_ERROR, ProtocolError, ResourceChangeType, ResourceType, ResourceWriteMode, type CreateResourceWatchParams, type CreateResourceWatchResult, type DirectoryEntry, type ResourceCopyParams, type ResourceCopyResult, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMkdirParams, type ResourceMkdirResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceResolveParams, type ResourceResolveResult, type ResourceWatchState, type ResourceWriteParams, type ResourceWriteResult, type IStateSnapshot } from '../common/state/sessionProtocol.js';
|
||||
import { ChangesSummary, ChatInteractivity, ChatOriginKind, MessageAttachmentKind, PendingMessageKind, TerminalClaimKind, TerminalLifecycleStatus, type Annotation, type AnnotationEntry, type AnnotationOrigin, type AnnotationsState, type ChatOrigin, type ChatState, type Customization, type Message, type MessageAttachment, type MessageResourceAttachment, type TerminalState, type TextRange, type ToolResultTerminalContent } from '../common/state/protocol/state.js';
|
||||
import type { ChatPendingMessageSetAction, ChatTurnStartedAction, SessionConfigChangedAction } from '../common/state/protocol/actions.js';
|
||||
import { isAhpAutomationCatalogChannel, isAhpAutomationRunChannel, ISessionGitState, MessageKind, ResponsePartKind, SESSION_META_GITHUB_KEY, SESSION_META_GIT_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, AH_META_AUTO_ARCHIVED_AT_DB_KEY, AH_META_CREATED_BY_SESSION_DB_KEY, readSessionCreationReference, readSessionComparisonMetadata, readSessionSpawnDepth, withSessionSpawnDepth, withSessionCreationReference, parseSessionCreationReference, SessionLifecycle, SessionStatus, ToolCallStatus, ToolResultContentType, TurnState, AH_META_HAS_WORKSPACE_TRANSITIONS_DB_KEY, AH_META_WORKSPACE_CONVERSION_QUARANTINED_DB_KEY, AH_META_WORKSPACELESS_DB_KEY, AH_META_EHCLI_ADOPTED_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, AH_META_IS_READ_DB_KEY, buildChatUri, buildDefaultChatUri, buildResourceWatchChannelUri, buildSubagentChatUri, buildSubagentSessionUriPrefix, chatStorageUri, getErrorResponsePart, isAhpChatChannel, isChatReadOnly, isDefaultChatUri, isSessionStatusArchived, isSubagentChatUri, isSubagentSession, needsSessionGitStateRefresh, parseChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseResourceWatchChannelUri, parseSessionGitData, parseSessionMultiRootMetadata, parseSubagentSessionUri, readSessionExternal, readSessionGitHubState, readSessionGitState, readSessionMultiRootMetadata, readSessionSourceControlState, readSessionWorkspaceless, withSessionExternal, withSessionGitData, withSessionGitHubState, withSessionGitState, withSessionHasWorkspaceTransitions, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionStatusFlag, withSessionWorkspaceless, withSessionEhcliAdopted, withSessionEhcliLastMigratedTurn, AH_META_EHCLI_LAST_TURN_DB_KEY, withSessionFolderPickerDecision, readSessionFolderPickerDecision, parseSessionFolderPickerDecision, SESSION_META_FOLDER_PICKER_KEY, getAllSessionRelatedPullRequestUrls, readSessionEhcliAdoptable, readSessionGitHubData, parseSessionGitHubData, parseSessionGitHubState, readSessionGitHubStateInput, withMigratedSessionGitHubState, withReplacedFolderGitHubState, SESSION_META_GITHUB_DATA_KEY, withWorkingDirectoryKey, withWorkingDirectoryScopeId, type ISessionSourceControlState, type SessionConfigState, type SessionSummary, type SessionSummaryMeta, type ToolResultSubagentContent, type Turn } from '../common/state/sessionState.js';
|
||||
import { isAhpAutomationCatalogChannel, isAhpAutomationRunChannel, ISessionGitState, MessageKind, ResponsePartKind, SESSION_META_GITHUB_KEY, SESSION_META_GIT_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, AH_META_AUTO_ARCHIVED_AT_DB_KEY, AH_META_CREATED_BY_SESSION_DB_KEY, readSessionCreationReference, readSessionComparisonMetadata, readSessionSpawnDepth, withSessionSpawnDepth, withSessionCreationReference, parseSessionCreationReference, SessionLifecycle, SessionStatus, ToolCallStatus, ToolResultContentType, TurnState, AH_META_HAS_WORKSPACE_TRANSITIONS_DB_KEY, AH_META_WORKSPACE_CONVERSION_QUARANTINED_DB_KEY, AH_META_WORKSPACELESS_DB_KEY, AH_META_EHCLI_ADOPTED_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, AH_META_IS_READ_DB_KEY, AH_META_DEFAULT_CHAT_IS_READ_DB_KEY, buildChatUri, buildDefaultChatUri, buildResourceWatchChannelUri, buildSubagentChatUri, buildSubagentSessionUriPrefix, chatStorageUri, getErrorResponsePart, isAhpChatChannel, isChatInSessionReadAggregate, isChatReadOnly, isDefaultChatUri, isSessionStatusArchived, isSessionStatusRead, isSubagentChatUri, isSubagentSession, needsSessionGitStateRefresh, parseChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseResourceWatchChannelUri, parseSessionGitData, parseSessionMultiRootMetadata, parseSubagentSessionUri, readSessionExternal, readSessionGitHubState, readSessionGitState, readSessionMultiRootMetadata, readSessionSourceControlState, readSessionWorkspaceless, withSessionExternal, withSessionGitData, withSessionGitHubState, withSessionGitState, withSessionHasWorkspaceTransitions, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionStatusFlag, withSessionWorkspaceless, withSessionEhcliAdopted, withSessionEhcliLastMigratedTurn, AH_META_EHCLI_LAST_TURN_DB_KEY, withSessionFolderPickerDecision, readSessionFolderPickerDecision, parseSessionFolderPickerDecision, SESSION_META_FOLDER_PICKER_KEY, getAllSessionRelatedPullRequestUrls, readSessionEhcliAdoptable, readSessionGitHubData, parseSessionGitHubData, parseSessionGitHubState, readSessionGitHubStateInput, withMigratedSessionGitHubState, withReplacedFolderGitHubState, SESSION_META_GITHUB_DATA_KEY, withWorkingDirectoryKey, withWorkingDirectoryScopeId, type ISessionSourceControlState, type SessionConfigState, type SessionSummary, type SessionSummaryMeta, type ToolResultSubagentContent, type Turn } from '../common/state/sessionState.js';
|
||||
import { readToolCallMeta } from '../common/meta/agentToolCallMeta.js';
|
||||
import { isHostSnapshotAttachment, toHostSnapshotAttachmentMeta } from '../common/meta/agentSnapshotAttachmentMeta.js';
|
||||
import { readEphemeralSessionMeta, withEphemeralSessionMeta } from '../common/meta/agentEphemeralSessionMeta.js';
|
||||
@@ -153,6 +153,7 @@ interface IPassiveSessionMetadataUpdate {
|
||||
readonly key: string;
|
||||
readonly flag: SessionStatus;
|
||||
readonly set: boolean;
|
||||
readonly requestedSet: boolean;
|
||||
}
|
||||
|
||||
interface IBackgroundPassiveSessionMetadataWrite {
|
||||
@@ -408,6 +409,7 @@ interface ICatalogChat {
|
||||
readonly origin?: ChatOrigin;
|
||||
readonly interactivity?: ChatInteractivity;
|
||||
readonly archived?: boolean;
|
||||
readonly isRead?: boolean;
|
||||
readonly inheritedTurnId?: string;
|
||||
readonly workingDirectories?: readonly string[];
|
||||
readonly changes?: ChangesSummary;
|
||||
@@ -2190,7 +2192,11 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
kind: summary.defaultChat === chat.resource || isDefaultChatUri(chat.resource) ? 'default' : 'peer',
|
||||
origin: chat.origin,
|
||||
...(chat.interactivity !== undefined ? { interactivity: chat.interactivity } : {}),
|
||||
...(isSessionStatusArchived(chat.status) || chat.archived === true ? { archived: true } : {}),
|
||||
...(chat.status !== undefined ? {
|
||||
archived: isSessionStatusArchived(chat.status),
|
||||
isRead: isSessionStatusRead(chat.status),
|
||||
} : {}),
|
||||
...(chat.archived === true ? { archived: true } : {}),
|
||||
...(chat.changes !== undefined ? { changes: chat.changes } : {}),
|
||||
}));
|
||||
}
|
||||
@@ -2367,6 +2373,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
uri: buildDefaultChatUri(registered.session),
|
||||
kind: 'default',
|
||||
title: metadata.summary,
|
||||
isRead: metadata.chats?.find(chat => chat.kind === 'default')?.isRead,
|
||||
...(defaultChatWorkingDirectories !== undefined ? { workingDirectories: defaultChatWorkingDirectories } : {}),
|
||||
},
|
||||
...peers.map(peer => ({
|
||||
@@ -2374,6 +2381,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
kind: 'peer' as const,
|
||||
origin: peer.origin,
|
||||
archived: peer.archived,
|
||||
isRead: peer.isRead,
|
||||
inheritedTurnId: peer.inheritedTurnId,
|
||||
workingDirectories: peer.workingDirectories,
|
||||
})),
|
||||
@@ -2447,6 +2455,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
origin: chat.origin,
|
||||
...(chat.interactivity !== undefined ? { interactivity: chat.interactivity } : {}),
|
||||
...(isSessionStatusArchived(chat.status) && state.defaultChat !== chat.resource && !isDefaultChatUri(chat.resource) ? { archived: true } : {}),
|
||||
isRead: isSessionStatusRead(chat.status),
|
||||
inheritedTurnId: this._stateManager.getChatInheritedTurnId(chat.resource),
|
||||
workingDirectories: chat.workingDirectories,
|
||||
changes: chat.changes,
|
||||
@@ -3200,12 +3209,14 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
uri: buildDefaultChatUri(metadata.session),
|
||||
kind: 'default',
|
||||
title: metadata.summary,
|
||||
isRead: metadata.chats?.find(chat => chat.kind === 'default')?.isRead,
|
||||
},
|
||||
...peers.map(peer => ({
|
||||
uri: peer.uri,
|
||||
kind: 'peer' as const,
|
||||
origin: peer.origin,
|
||||
archived: peer.archived,
|
||||
isRead: peer.isRead,
|
||||
inheritedTurnId: peer.inheritedTurnId,
|
||||
})),
|
||||
],
|
||||
@@ -3449,6 +3460,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
kind: 'peer' as const,
|
||||
origin: peer.origin,
|
||||
archived: peer.archived,
|
||||
isRead: peer.isRead,
|
||||
inheritedTurnId: peer.inheritedTurnId,
|
||||
workingDirectories: peer.workingDirectories,
|
||||
})),
|
||||
@@ -6843,21 +6855,35 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
* first, so an absent surfaced summary can only mean "unknown".
|
||||
*/
|
||||
private async _applyPassiveSessionMetadata(session: string, action: IIsArchivedChangedAction | IIsReadChangedAction): Promise<boolean> {
|
||||
if (!this._stateManager.getSurfacedSessionSummary(session)) {
|
||||
const summary = this._stateManager.getSurfacedSessionSummary(session);
|
||||
if (!summary) {
|
||||
return false;
|
||||
}
|
||||
const sessionUri = URI.parse(session);
|
||||
const [key, flag, set] = action.type === ActionType.SessionIsArchivedChanged
|
||||
const [key, flag, requestedSet] = action.type === ActionType.SessionIsArchivedChanged
|
||||
? [AH_META_IS_ARCHIVED_DB_KEY, SessionStatus.IsArchived, action.isArchived] as const
|
||||
: [AH_META_IS_READ_DB_KEY, SessionStatus.IsRead, action.isRead] as const;
|
||||
const aggregateChats = action.type === ActionType.SessionIsReadChanged
|
||||
? summary.chats?.filter(chat => isChatInSessionReadAggregate(chat.resource, chat.origin, chat.interactivity)) ?? []
|
||||
: [];
|
||||
const set = requestedSet && aggregateChats.length > 1 && aggregateChats.some(chat => chat.status !== undefined && !isSessionStatusRead(chat.status))
|
||||
? false
|
||||
: requestedSet;
|
||||
const soleChat = aggregateChats.length === 1 ? aggregateChats[0] : undefined;
|
||||
await persistSessionMetadataValues(this._sessionDataService, session, {
|
||||
[key]: set ? 'true' : '',
|
||||
...(soleChat && isDefaultChatUri(soleChat.resource) ? { [AH_META_DEFAULT_CHAT_IS_READ_DB_KEY]: set ? 'true' : '' } : {}),
|
||||
...(action.type === ActionType.SessionIsArchivedChanged && !action.isArchived ? { [AH_META_AUTO_ARCHIVED_AT_DB_KEY]: '' } : {}),
|
||||
});
|
||||
this._invalidateSessionList();
|
||||
this._stateManager.setSurfacedSessionStatusFlag(session, flag, set);
|
||||
const chats = soleChat
|
||||
? summary.chats?.map(chat => chat.resource === soleChat.resource
|
||||
? { ...chat, status: withSessionStatusFlag(chat.status ?? summary.status, SessionStatus.IsRead, set) }
|
||||
: chat)
|
||||
: undefined;
|
||||
this._stateManager.setSurfacedSessionStatusAndChats(session, withSessionStatusFlag(summary.status, flag, set), chats);
|
||||
const payloadDirty = this._markCatalogPayloadDirty(session);
|
||||
this._queuePassiveSessionMetadataSynchronization(sessionUri, { key, flag, set });
|
||||
this._queuePassiveSessionMetadataSynchronization(sessionUri, { key, flag, set, requestedSet });
|
||||
await payloadDirty;
|
||||
return true;
|
||||
}
|
||||
@@ -6947,9 +6973,34 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
let data = request.data;
|
||||
const legacyMetadata = { ...request.legacyMetadata };
|
||||
for (const update of updates) {
|
||||
if (update.flag === SessionStatus.IsRead) {
|
||||
const aggregateChats = data.chats.filter(chat => {
|
||||
const isToolChat = typeof chat.origin === 'object'
|
||||
&& chat.origin !== null
|
||||
&& !Array.isArray(chat.origin)
|
||||
&& chat.origin.kind === ChatOriginKind.Tool;
|
||||
return !isToolChat && isChatInSessionReadAggregate(chat.uri, undefined, chat.interactivity);
|
||||
});
|
||||
const set = update.requestedSet && aggregateChats.length > 1 && aggregateChats.some(chat => chat.isRead === false)
|
||||
? false
|
||||
: update.set;
|
||||
const soleChat = aggregateChats.length === 1 ? aggregateChats[0] : undefined;
|
||||
data = {
|
||||
...data,
|
||||
isRead: set,
|
||||
...(soleChat ? {
|
||||
chats: data.chats.map(chat => chat.uri === soleChat.uri ? { ...chat, isRead: set } : chat),
|
||||
} : {}),
|
||||
};
|
||||
legacyMetadata[update.key] = set ? 'true' : '';
|
||||
if (soleChat?.kind === 'default') {
|
||||
legacyMetadata[AH_META_DEFAULT_CHAT_IS_READ_DB_KEY] = set ? 'true' : '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
data = {
|
||||
...data,
|
||||
...(update.flag === SessionStatus.IsArchived ? { isArchived: update.set } : { isRead: update.set }),
|
||||
isArchived: update.set,
|
||||
};
|
||||
legacyMetadata[update.key] = update.set ? 'true' : '';
|
||||
}
|
||||
@@ -7481,12 +7532,18 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
|
||||
private _dispatchActionNow(channel: string, sessionChannel: string, action: SessionAction | ChatAction | TerminalAction | ClientChangesetAction | ClientAnnotationsAction | IRootConfigChangedAction, clientId: string, clientSeq: number, clientContext: IAgentHostClientTelemetryContext): void {
|
||||
const origin = { clientId, clientSeq };
|
||||
const chatState = action.type === ActionType.ChatIsArchivedChanged ? this._stateManager.getChatState(channel) : undefined;
|
||||
const chatState = action.type === ActionType.ChatIsArchivedChanged || action.type === ActionType.ChatIsReadChanged
|
||||
? this._stateManager.getChatState(channel)
|
||||
: undefined;
|
||||
if (action.type === ActionType.ChatIsArchivedChanged
|
||||
&& (isDefaultChatUri(channel) || !chatState || chatState.origin?.kind === ChatOriginKind.Tool || chatState.origin?.kind === ChatOriginKind.SideChat)) {
|
||||
this._stateManager.rejectClientAction(channel, action, origin, 'Only a known independently manageable non-default chat can be archived.');
|
||||
return;
|
||||
}
|
||||
if (action.type === ActionType.ChatIsReadChanged && !chatState) {
|
||||
this._stateManager.rejectClientAction(channel, action, origin, 'Only a known chat can change its read state.');
|
||||
return;
|
||||
}
|
||||
if (action.type === ActionType.SessionIsArchivedChanged && !action.isArchived && this._sessionResidency.isBeingDisposed(sessionChannel)) {
|
||||
this._stateManager.rejectClientAction(channel, action, origin, 'Cannot unarchive a session while it is being deleted.');
|
||||
return;
|
||||
@@ -8291,6 +8348,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
// Check for persisted metadata in the session database
|
||||
let title = meta.summary ?? 'Session';
|
||||
let isRead: boolean | undefined;
|
||||
let defaultChatIsRead: boolean | undefined;
|
||||
let isArchived: boolean | undefined;
|
||||
let persistedConfigValues: Record<string, unknown> | undefined;
|
||||
let changes: ChangesSummary | undefined;
|
||||
@@ -8308,6 +8366,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
const m = await db.object.getMetadataObject({
|
||||
customTitle: true,
|
||||
[AH_META_IS_READ_DB_KEY]: true,
|
||||
[AH_META_DEFAULT_CHAT_IS_READ_DB_KEY]: true,
|
||||
[AH_META_IS_ARCHIVED_DB_KEY]: true,
|
||||
[AH_META_IS_DONE_DB_KEY]: true,
|
||||
configValues: true,
|
||||
@@ -8335,6 +8394,9 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
if (m[AH_META_IS_READ_DB_KEY] !== undefined) {
|
||||
isRead = m[AH_META_IS_READ_DB_KEY] === 'true';
|
||||
}
|
||||
if (m[AH_META_DEFAULT_CHAT_IS_READ_DB_KEY] !== undefined) {
|
||||
defaultChatIsRead = m[AH_META_DEFAULT_CHAT_IS_READ_DB_KEY] === 'true';
|
||||
}
|
||||
const persistedArchived = m[AH_META_IS_ARCHIVED_DB_KEY] ?? m[AH_META_IS_DONE_DB_KEY];
|
||||
if (persistedArchived !== undefined) {
|
||||
isArchived = persistedArchived === 'true';
|
||||
@@ -8450,7 +8512,9 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
}
|
||||
if (adoptionListVisible?.isRead !== undefined) {
|
||||
isRead = adoptionListVisible.isRead;
|
||||
defaultChatIsRead ??= adoptionListVisible.isRead;
|
||||
}
|
||||
defaultChatIsRead ??= isRead;
|
||||
|
||||
let turns: readonly Turn[];
|
||||
try {
|
||||
@@ -8532,6 +8596,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
defaultChatTitle,
|
||||
defaultChatModifiedAt,
|
||||
defaultChatWorkingDirectories,
|
||||
defaultChatIsRead,
|
||||
});
|
||||
if (adoptionListVisible) {
|
||||
const adoptionMetadata: Record<string, string> = {};
|
||||
@@ -8541,6 +8606,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
}
|
||||
if (adoptionListVisible.isRead !== undefined) {
|
||||
adoptionMetadata[AH_META_IS_READ_DB_KEY] = adoptionListVisible.isRead ? 'true' : '';
|
||||
adoptionMetadata[AH_META_DEFAULT_CHAT_IS_READ_DB_KEY] = adoptionListVisible.isRead ? 'true' : '';
|
||||
}
|
||||
await this._persistListVisibleSessionState(session, adoptionMetadata);
|
||||
}
|
||||
@@ -8654,8 +8720,9 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
...(chat.workingDirectories !== undefined ? { workingDirectories: chat.workingDirectories } : {}),
|
||||
})));
|
||||
}
|
||||
const entriesWithReadState = entries.map(entry => entry.isRead === undefined ? { ...entry, isRead: true } : entry);
|
||||
const cachedWorkingDirectories = new Map(cached?.filter(chat => chat.kind === 'peer' && chat.workingDirectories !== undefined).map(chat => [chat.uri, chat.workingDirectories]));
|
||||
const enrichedEntries = entries.map(entry => entry.workingDirectories === undefined && cachedWorkingDirectories.get(entry.uri) !== undefined
|
||||
const enrichedEntries = entriesWithReadState.map(entry => entry.workingDirectories === undefined && cachedWorkingDirectories.get(entry.uri) !== undefined
|
||||
? { ...entry, workingDirectories: cachedWorkingDirectories.get(entry.uri) }
|
||||
: entry);
|
||||
const toolChatUris = new Set([
|
||||
@@ -8739,6 +8806,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
if (cached?.some(chat => chat.kind === 'peer')) {
|
||||
const projectedPeers: IPersistedPeerChat[] = cached.filter(chat => chat.kind === 'peer').map(chat => ({
|
||||
uri: chat.uri,
|
||||
isRead: true,
|
||||
...(chat.origin !== undefined ? { origin: chat.origin } : {}),
|
||||
...(chat.archived === true ? { archived: true } : {}),
|
||||
...(chat.inheritedTurnId !== undefined ? { inheritedTurnId: chat.inheritedTurnId } : {}),
|
||||
@@ -8771,6 +8839,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
}
|
||||
const entries: IPersistedPeerChat[] = legacy.map(chat => ({
|
||||
uri: chat.uri.toString(),
|
||||
isRead: true,
|
||||
...(chat.providerData !== undefined ? { providerData: chat.providerData } : {}),
|
||||
}));
|
||||
return this._peerChatStore.initialize(session, entries, database);
|
||||
@@ -8806,6 +8875,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
inheritedTurnId: entry.inheritedTurnId,
|
||||
workingDirectories: entry.workingDirectories ?? cachedChat?.workingDirectories,
|
||||
archived: entry.archived,
|
||||
isRead: entry.isRead,
|
||||
changes: cachedChat?.changes,
|
||||
};
|
||||
}));
|
||||
@@ -8813,7 +8883,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
const { chatUri, title, draft, providerData, origin, interactivity, inheritedTurnId, workingDirectories, archived, changes } = item;
|
||||
const { chatUri, title, draft, providerData, origin, interactivity, inheritedTurnId, workingDirectories, archived, isRead, changes } = item;
|
||||
if (this._stateManager.getChatState(chatUri.toString())) {
|
||||
continue;
|
||||
}
|
||||
@@ -8826,6 +8896,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
inheritedTurnId,
|
||||
workingDirectories,
|
||||
archived,
|
||||
isRead,
|
||||
changes,
|
||||
resolver: currentProviderData => this._materializeRestoredPeerChat(session, chatUri, currentProviderData),
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ import { IAgentHostChatContributions, type ISendTurnMessageOptions } from '../co
|
||||
import { AgentHostClientType } from '../common/agentHostClientInfo.js';
|
||||
import { isRenameChatTool } from '../common/serverToolNames.js';
|
||||
import { AgentHostLaunchKind, createUnknownAgentHostClientTelemetryContext, type IAgentHostClientTelemetryContext } from '../common/agentHostTelemetry.js';
|
||||
import { AgentSession, AgentSignal, IAgent, IAgentChatContext, IAgentToolPendingConfirmationSignal, type AgentSubagentTaskModelSource, type IAgentModelCallCompletedSignal, type IAgentModelCallFinishedSignal } from '../common/agent.js';
|
||||
import { AgentSession, AgentSignal, IAgent, IAgentChatContext, IAgentToolPendingConfirmationSignal, type AgentSubagentKind, type AgentSubagentTaskModelSource, type IAgentModelCallCompletedSignal, type IAgentModelCallFinishedSignal, type IAgentSubagentStartedSignal } from '../common/agent.js';
|
||||
import { isPresentationOnlyToolCall, readToolCallMeta, toToolCallMeta } from '../common/meta/agentToolCallMeta.js';
|
||||
import { isAgentMergeMessage } from '../common/meta/agentMergeMessageMeta.js';
|
||||
import { readAgentPermissionResponseMeta } from '../common/meta/agentPermissionResponseMeta.js';
|
||||
@@ -172,6 +172,8 @@ interface ISubagentSessionRef {
|
||||
readonly chatUri: ProtocolURI;
|
||||
readonly turnStopWatch: StopWatch;
|
||||
readonly taskModelSource: AgentSubagentTaskModelSource | undefined;
|
||||
readonly subagentKind: AgentSubagentKind | undefined;
|
||||
readonly model: string | undefined;
|
||||
}
|
||||
|
||||
interface ISubagentParentTurnTelemetryContext {
|
||||
@@ -311,6 +313,7 @@ export class AgentSideEffects extends Disposable {
|
||||
undefined,
|
||||
(session, turnId) => this._turnTracker.getClientTelemetryContext(session, turnId),
|
||||
(session, turnId) => this._turnTracker.getTelemetryContext(session, turnId),
|
||||
(session, turnId) => this._turnTracker.getSubagentKind(session, turnId),
|
||||
);
|
||||
this._permissionManager = this._register(this._instantiationService.createInstance(SessionPermissionManager, this._stateManager, {}));
|
||||
this._register(this._stateManager.onDidSnapshotDefaultChatTitle(event => this._persistDefaultChatTitleSnapshot(event.session, event.chat, event.title)));
|
||||
@@ -674,7 +677,7 @@ export class AgentSideEffects extends Disposable {
|
||||
return;
|
||||
}
|
||||
if (signal.kind === 'subagent_started') {
|
||||
this._handleSubagentStarted(signal.chat.toString(), signal.toolCallId, signal.agentName, signal.agentDisplayName, signal.agentDescription, signal.taskPrompt, signal.parentToolCallId, signal.taskModelSource);
|
||||
this._handleSubagentStarted(signal);
|
||||
this._drainPendingSubagentSignals(signal.chat.toString(), signal.toolCallId);
|
||||
return;
|
||||
}
|
||||
@@ -923,10 +926,8 @@ export class AgentSideEffects extends Disposable {
|
||||
if (!isSubagentChatUri(sessionKey)) {
|
||||
this._turnTracker.updateBilledNanoAiu(sessionKey, action.turnId, usageMeta.copilotUsage?.totalNanoAiu);
|
||||
}
|
||||
if (action.usage.model && agent) {
|
||||
const modelContext = getModelTelemetryContext(agent, action.usage.model);
|
||||
this._turnTracker.updateModel(sessionKey, action.turnId, modelContext.model, modelContext.modelTelemetryKind);
|
||||
this._toolCallTracker.updateTurnModel(sessionKey, action.turnId, modelContext.model, modelContext.modelTelemetryKind);
|
||||
if (agent) {
|
||||
this._updateTurnModel(agent, sessionKey, action.turnId, action.usage.model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1171,25 +1172,18 @@ export class AgentSideEffects extends Disposable {
|
||||
* before this runs, so this only drives the turn/tracking/parent content
|
||||
* — it does not add the chat.
|
||||
*
|
||||
* `chatURI` is always the agent's top-level chat: the subagent is
|
||||
* `signal.chat` is always the agent's top-level chat: the subagent is
|
||||
* registered (and inner events routed) under it because inner-tool
|
||||
* signals carry the top-level chat as their resource. `spawningToolParentId`,
|
||||
* signals carry the top-level chat as their resource. `signal.parentToolCallId`,
|
||||
* when set, is the tool call one level up from the spawning `toolCallId`
|
||||
* — the tool call in whose (subagent) chat the spawning tool lives — and
|
||||
* is used to route the discovery content block to that immediate parent
|
||||
* chat. Since subagent chats are flat (keyed off the root session), this
|
||||
* one-hop reference resolves the parent chat at any nesting depth.
|
||||
*/
|
||||
private _handleSubagentStarted(
|
||||
chatURI: ProtocolURI,
|
||||
toolCallId: string,
|
||||
agentName: string,
|
||||
agentDisplayName: string,
|
||||
agentDescription?: string,
|
||||
taskPrompt?: string,
|
||||
spawningToolParentId?: string,
|
||||
taskModelSource?: AgentSubagentTaskModelSource,
|
||||
): void {
|
||||
private _handleSubagentStarted(signal: IAgentSubagentStartedSignal): void {
|
||||
const chatURI = signal.chat.toString();
|
||||
const { toolCallId, agentName, agentDisplayName, agentDescription, taskPrompt, parentToolCallId: spawningToolParentId, taskModelSource, subagentKind, model } = signal;
|
||||
const parentSessionUri = parseRequiredSessionUriFromChatUri(chatURI);
|
||||
const subagentChatUri = buildSubagentChatUri(parentSessionUri, toolCallId);
|
||||
const immediateParentChatUri = spawningToolParentId
|
||||
@@ -1199,6 +1193,9 @@ export class AgentSideEffects extends Disposable {
|
||||
|
||||
const existing = this._subagentChats.get(chatURI, toolCallId);
|
||||
if (existing) {
|
||||
if (model && model !== existing.model) {
|
||||
this._subagentChats.set({ ...existing, model }, chatURI, toolCallId);
|
||||
}
|
||||
this._resumeSubagentSession(chatURI, toolCallId, taskPrompt ? { text: taskPrompt, origin: { kind: MessageKind.User } } : undefined, immediateParentChatUri);
|
||||
return;
|
||||
}
|
||||
@@ -1219,11 +1216,12 @@ export class AgentSideEffects extends Disposable {
|
||||
const agent = this._options.getAgent(parentSessionUri);
|
||||
if (agent) {
|
||||
const interactionMode = getConfiguredSessionMode(this._stateManager.getSessionState(parentSessionUri)?.config);
|
||||
this._turnTracker.turnStarted(agent, subagentChatUri, turnId, undefined, undefined, 'default', undefined, interactionMode, parentClientContext, initiatorClientId, correlatedParentTurnId, toolCallId, messageOriginKind, taskModelSource, URI.parse(chatURI));
|
||||
this._turnTracker.turnStarted(agent, subagentChatUri, turnId, undefined, undefined, 'default', undefined, interactionMode, parentClientContext, initiatorClientId, correlatedParentTurnId, toolCallId, messageOriginKind, taskModelSource, URI.parse(chatURI), subagentKind);
|
||||
this._turnTracker.setCurrentStage(subagentChatUri, turnId, 'provider');
|
||||
this._updateTurnModel(agent, subagentChatUri, turnId, model);
|
||||
}
|
||||
|
||||
this._subagentChats.set({ parentChatUri: chatURI, immediateParentChatUri, toolCallId, sessionUri: parentSessionUri, chatUri: subagentChatUri, turnStopWatch: StopWatch.create(false), taskModelSource }, chatURI, toolCallId);
|
||||
this._subagentChats.set({ parentChatUri: chatURI, immediateParentChatUri, toolCallId, sessionUri: parentSessionUri, chatUri: subagentChatUri, turnStopWatch: StopWatch.create(false), taskModelSource, subagentKind, model }, chatURI, toolCallId);
|
||||
|
||||
// Dispatch the discovery content on the spawning tool call's own chat; the top-level chat is a no-op when nested.
|
||||
if (parentTurnId) {
|
||||
@@ -1295,14 +1293,25 @@ export class AgentSideEffects extends Disposable {
|
||||
const agent = this._options.getAgent(subagent.sessionUri);
|
||||
if (agent) {
|
||||
const interactionMode = getConfiguredSessionMode(this._stateManager.getSessionState(subagent.sessionUri)?.config);
|
||||
this._turnTracker.turnStarted(agent, subagent.chatUri, turnId, undefined, undefined, 'default', undefined, interactionMode, parentClientContext, initiatorClientId, correlatedParentTurnId, toolCallId, messageOriginKind, subagent.taskModelSource, URI.parse(parentChatURI));
|
||||
this._turnTracker.turnStarted(agent, subagent.chatUri, turnId, undefined, undefined, 'default', undefined, interactionMode, parentClientContext, initiatorClientId, correlatedParentTurnId, toolCallId, messageOriginKind, subagent.taskModelSource, URI.parse(parentChatURI), subagent.subagentKind);
|
||||
this._turnTracker.setCurrentStage(subagent.chatUri, turnId, 'provider');
|
||||
this._updateTurnModel(agent, subagent.chatUri, turnId, subagent.model);
|
||||
}
|
||||
this._subagentChats.set({ ...subagent, immediateParentChatUri: correlatedParentChatUri, turnStopWatch: StopWatch.create(false) }, parentChatURI, toolCallId);
|
||||
}
|
||||
|
||||
private _updateTurnModel(agent: IAgent, chat: ProtocolURI, turnId: string, model: string | undefined): void {
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
const modelContext = getModelTelemetryContext(agent, model);
|
||||
this._turnTracker.updateModel(chat, turnId, modelContext.model, modelContext.modelTelemetryKind);
|
||||
this._toolCallTracker.updateTurnModel(chat, turnId, modelContext.model, modelContext.modelTelemetryKind);
|
||||
}
|
||||
|
||||
private _getSubagentParentTurnTelemetryContext(immediateParentChatUri: ProtocolURI | undefined, fallbackParentChatUri: ProtocolURI): ISubagentParentTurnTelemetryContext {
|
||||
const parentChatUri = immediateParentChatUri ?? fallbackParentChatUri;
|
||||
immediateParentChatUri = immediateParentChatUri && this._turnOwningChat(immediateParentChatUri);
|
||||
const parentChatUri = immediateParentChatUri ?? this._turnOwningChat(fallbackParentChatUri);
|
||||
const parentTurnId = this._stateManager.getActiveTurnId(parentChatUri);
|
||||
const parentSessionUri = parseRequiredSessionUriFromChatUri(parentChatUri);
|
||||
const parentMessageOriginKind = parentTurnId ? this._turnTracker.getMessageOriginKind(parentChatUri, parentTurnId) : undefined;
|
||||
@@ -1317,6 +1326,19 @@ export class AgentSideEffects extends Disposable {
|
||||
};
|
||||
}
|
||||
|
||||
/** A Fusion phase chat only presents part of its parent's turn, so that parent owns the work done in it. */
|
||||
private _turnOwningChat(chatUri: ProtocolURI): ProtocolURI {
|
||||
if (!isSubagentChatUri(chatUri)) {
|
||||
return chatUri;
|
||||
}
|
||||
for (const subagent of this._subagentChats.values()) {
|
||||
if (subagent.chatUri === chatUri && subagent.subagentKind === 'fusionPhase') {
|
||||
return this._turnOwningChat(subagent.immediateParentChatUri ?? subagent.parentChatUri);
|
||||
}
|
||||
}
|
||||
return chatUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels child executions, retaining reusable routing only when requested by a parent turn cancellation.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AdditionalWorktreeLifecycleContribution } from './additionalWorktreeLif
|
||||
import { ChatArchiveContribution } from './chatArchive/chatArchiveContribution.js';
|
||||
import { ChatDraftContribution } from './chatDraft/chatDraftContribution.js';
|
||||
import { ChatInputContribution } from './chatInput/chatInputContribution.js';
|
||||
import { ChatReadContribution } from './chatRead/chatReadContribution.js';
|
||||
import { ChatSurfaceContribution } from './chatSurface/chatSurfaceContribution.js';
|
||||
import { CheckpointAndChangesetContribution } from './checkpointAndChangeset/checkpointAndChangesetContribution.js';
|
||||
import { ExternalSessionAdoptionContribution } from './externalSessionAdoption/externalSessionAdoptionContribution.js';
|
||||
@@ -63,5 +64,6 @@ export function registerBuiltInChatContributions(
|
||||
registrations.add(contributions.registerContribution(SessionFlagsContribution));
|
||||
registrations.add(contributions.registerContribution(AdditionalWorktreeLifecycleContribution));
|
||||
registrations.add(contributions.registerContribution(ChatArchiveContribution));
|
||||
registrations.add(contributions.registerContribution(ChatReadContribution));
|
||||
return registrations;
|
||||
}
|
||||
+6
-1
@@ -8,7 +8,8 @@ import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ILogService } from '../../../../log/common/log.js';
|
||||
import type { IAgentHostChatContribution, IAgentHostChatContributionContext, IDispatchedAction } from '../../../common/agentHostChatContributionsService.js';
|
||||
import { ActionType } from '../../../common/state/sessionActions.js';
|
||||
import { isAhpChatChannel, isDefaultChatUri, parseRequiredSessionUriFromChatUri } from '../../../common/state/sessionState.js';
|
||||
import { isAhpChatChannel, isDefaultChatUri, isSessionStatusRead, parseRequiredSessionUriFromChatUri } from '../../../common/state/sessionState.js';
|
||||
import { AgentHostStateManager, IAgentHostStateManager } from '../../agentHostStateManager.js';
|
||||
import { IAgentHostPeerChatPersistenceService } from '../../agentHostPeerChatStore.js';
|
||||
|
||||
/**
|
||||
@@ -22,6 +23,7 @@ export class ChatArchiveContribution extends Disposable implements IAgentHostCha
|
||||
|
||||
constructor(
|
||||
protected readonly _context: IAgentHostChatContributionContext,
|
||||
@IAgentHostStateManager private readonly _stateManager: AgentHostStateManager,
|
||||
@IAgentHostPeerChatPersistenceService private readonly _peerChatPersistenceService: IAgentHostPeerChatPersistenceService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
) {
|
||||
@@ -37,6 +39,9 @@ export class ChatArchiveContribution extends Disposable implements IAgentHostCha
|
||||
}
|
||||
const session = URI.parse(parseRequiredSessionUriFromChatUri(dispatched.channel));
|
||||
const chat = URI.parse(dispatched.channel);
|
||||
if (dispatched.action.isArchived && !isSessionStatusRead(this._stateManager.getChatState(dispatched.channel)?.status)) {
|
||||
this._stateManager.dispatchServerAction(dispatched.channel, { type: ActionType.ChatIsReadChanged, isRead: true });
|
||||
}
|
||||
void this._peerChatPersistenceService.setArchived(session, chat, dispatched.action.isArchived).catch(error => {
|
||||
this._logService.error(error, `[ChatArchiveContribution] Failed to persist archived state for ${dispatched.channel}`);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ILogService } from '../../../../log/common/log.js';
|
||||
import { ISessionDataService } from '../../../common/sessionDataService.js';
|
||||
import type { IAgentHostChatContribution, IAgentHostChatContributionContext, IDispatchedAction } from '../../../common/agentHostChatContributionsService.js';
|
||||
import { ActionType } from '../../../common/state/sessionActions.js';
|
||||
import { AH_META_DEFAULT_CHAT_IS_READ_DB_KEY, isAhpChatChannel, isDefaultChatUri, parseRequiredSessionUriFromChatUri } from '../../../common/state/sessionState.js';
|
||||
import { IAgentHostPeerChatPersistenceService } from '../../agentHostPeerChatStore.js';
|
||||
import { persistSessionMetadata } from '../../shared/persistSessionMetadata.js';
|
||||
|
||||
/** Persists accepted chat read-state changes. */
|
||||
export class ChatReadContribution extends Disposable implements IAgentHostChatContribution {
|
||||
|
||||
static readonly id = 'chatRead';
|
||||
readonly order = 760;
|
||||
|
||||
constructor(
|
||||
protected readonly _context: IAgentHostChatContributionContext,
|
||||
@IAgentHostPeerChatPersistenceService private readonly _peerChatPersistenceService: IAgentHostPeerChatPersistenceService,
|
||||
@ISessionDataService private readonly _sessionDataService: ISessionDataService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
onDidDispatchAction(dispatched: IDispatchedAction): void {
|
||||
if (dispatched.rejectionReason !== undefined
|
||||
|| dispatched.action.type !== ActionType.ChatIsReadChanged
|
||||
|| !isAhpChatChannel(dispatched.channel)) {
|
||||
return;
|
||||
}
|
||||
const sessionResource = parseRequiredSessionUriFromChatUri(dispatched.channel);
|
||||
if (isDefaultChatUri(dispatched.channel)) {
|
||||
persistSessionMetadata(this._sessionDataService, this._logService, sessionResource, AH_META_DEFAULT_CHAT_IS_READ_DB_KEY, dispatched.action.isRead ? 'true' : '');
|
||||
return;
|
||||
}
|
||||
const session = URI.parse(sessionResource);
|
||||
const chat = URI.parse(dispatched.channel);
|
||||
void this._peerChatPersistenceService.setRead(session, chat, dispatched.action.isRead).catch(error => {
|
||||
this._logService.error(error, `[ChatReadContribution] Failed to persist read state for ${dispatched.channel}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
+12
-4
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { ActionType } from '../../../common/state/sessionActions.js';
|
||||
import { SessionStatus } from '../../../common/state/sessionState.js';
|
||||
import { isAhpChatChannel, isChatInSessionReadAggregate, SessionStatus } from '../../../common/state/sessionState.js';
|
||||
import type { IAgentHostChatContribution, IAgentHostChatContributionContext, ITurnEnd } from '../../../common/agentHostChatContributionsService.js';
|
||||
import { AgentHostStateManager, IAgentHostStateManager } from '../../agentHostStateManager.js';
|
||||
|
||||
@@ -29,9 +29,17 @@ export class MarkUnreadContribution extends Disposable implements IAgentHostChat
|
||||
if (turn.reason.kind === 'localCommand' || turn.reason.kind === 'rejected') {
|
||||
return;
|
||||
}
|
||||
// Route subagent turns to their owning session too (a background subagent
|
||||
// can complete after the parent turn). Each client keeps its active session
|
||||
// read; marking it unread is idempotent.
|
||||
const session = this._stateManager.getSessionState(turn.session);
|
||||
const chatSummary = isAhpChatChannel(turn.channel)
|
||||
? session?.chats.find(chat => chat.resource === turn.channel)
|
||||
: undefined;
|
||||
const isKnownChat = !!chatSummary;
|
||||
if (isKnownChat) {
|
||||
this._stateManager.dispatchServerAction(turn.channel, { type: ActionType.ChatIsReadChanged, isRead: false });
|
||||
}
|
||||
if (!isChatInSessionReadAggregate(turn.channel, chatSummary?.origin, chatSummary?.interactivity)) {
|
||||
return;
|
||||
}
|
||||
const status = this._stateManager.getSessionSummary(turn.session)?.status ?? 0;
|
||||
if (!(status & SessionStatus.IsRead)) {
|
||||
return;
|
||||
|
||||
@@ -44,6 +44,7 @@ import { ChatInputRequestPurpose, withChatInputRequestPurpose } from '../../comm
|
||||
import { AgentSystemNotificationKind, AgentSystemNotificationSeverity, toAgentSystemNotificationMeta } from '../../common/meta/agentSystemNotificationMeta.js';
|
||||
import { readCopilotShellAttachment, toCopilotBackgroundShellMeta } from '../../common/meta/copilotBackgroundWorkMeta.js';
|
||||
import { getSessionSandboxConfig } from '../sessionSandbox.js';
|
||||
import { getCopilotBrowserSandboxNetworkRestrictions } from './copilotSandboxPolicy.js';
|
||||
import { AgentHostAutoApprovePolicyRestrictedConfigKey, AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostAutoReplyAnswer, AgentHostAutoReplyEnabledConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, AgentHostMcpToolRoutingEnabledConfigKey, platformRootSchema, platformSessionSchema } from '../../common/agentHostSchema.js';
|
||||
import { createUnknownAgentHostClientTelemetryContext, type IAgentHostClientTelemetryContext, type IAgentProviderSendStageRecorder } from '../../common/agentHostTelemetry.js';
|
||||
import { AgentSession, AgentSignal, AgentWorkingDirectoryChangedError, AuthenticateParams, IMcpNotification, subagentChatTitle, type AgentSubagentTaskModelSource, type AgentTurnProviderCallState, type IAgentPendingMessageSender, type IAgentPermissionResponseContext, type IAgentTelemetryContext, type IAgentToolPendingConfirmationSignal, type IAgentTurnDiagnosticSnapshot, type IAgentTurnTokenUsage } from '../../common/agent.js';
|
||||
@@ -113,6 +114,7 @@ import { CopilotMcpToolRoutingCache, getMcpRoutingCacheKey, type ICopilotMcpRout
|
||||
import { getCopilotCustomizationCommandHandler } from './copilotCustomizationCommandDisplay.js';
|
||||
import { renderCopilotSlashCommandOutput, type RuntimeSlashCommandInfo } from './copilotSlashCommand.js';
|
||||
import { CopilotSandboxPolicyDisplay } from './copilotSandboxPolicyDisplay.js';
|
||||
import { toSlashCommandResourceMeta } from '../../common/meta/agentSlashCommandOutputMeta.js';
|
||||
import { createCopilotFailureCorrelation, reportCopilotModelCallFailure, reportCopilotSdkSessionError } from './copilotFailureTelemetry.js';
|
||||
import { reportCopilotTodoStoreOperation } from './copilotTodoStoreTelemetry.js';
|
||||
import { ModelCallTurnCorrelation } from './modelCallTurnCorrelation.js';
|
||||
@@ -174,6 +176,10 @@ interface IPendingMcpAuthRequest {
|
||||
readonly toolCalls: IMcpAuthToolCall[];
|
||||
}
|
||||
|
||||
type SubagentTurnOutcome =
|
||||
| { readonly kind: 'failed'; readonly message: string; readonly durationMs: number | undefined }
|
||||
| { readonly kind: 'cancelled'; readonly durationMs: number | undefined };
|
||||
|
||||
interface IMcpAuthToolCall {
|
||||
readonly turnId: string;
|
||||
readonly toolCallId: string;
|
||||
@@ -947,6 +953,7 @@ export class CopilotAgentSession extends Disposable {
|
||||
private readonly _autoModeResolvedByToolCallId = new Map<string, NonNullable<UsageInfoMeta['autoModeResolved']>>();
|
||||
private readonly _activeSubagentAgentIds = new Set<string>();
|
||||
private readonly _subagentTaskCompletionSchedulers = this._register(new DisposableMap<string, RunOnceScheduler>());
|
||||
private readonly _pendingSubagentLifecycleCompletions = new Map<string, { readonly toolCallId: string; readonly activityRevision: number | undefined; readonly outcome?: SubagentTurnOutcome }>();
|
||||
private readonly _subagentTaskCompletionDelay: number;
|
||||
/** Bumped when a child starts a model round so a task snapshot taken earlier cannot complete it; entries are dropped on completion. */
|
||||
private readonly _subagentActivityRevisions = new Map<string, number>();
|
||||
@@ -1275,6 +1282,7 @@ export class CopilotAgentSession extends Disposable {
|
||||
/** One-shot SDK callbacks, keyed by request id; answering one delivers a token but does not confirm acceptance. */
|
||||
private readonly _pendingMcpAuthRequests = new PendingRequestRegistry<McpAuthResult | null | undefined, IPendingMcpAuthRequest>();
|
||||
private _requiresConnectorConfigurationRefresh = false;
|
||||
private readonly _mcpServerDisplayNames = new Map<string, string>();
|
||||
/**
|
||||
* Retains challenge metadata and its latest callback id so token delivery can report Starting.
|
||||
* Connected and needs-auth statuses remain the final lifecycle authority.
|
||||
@@ -1830,6 +1838,7 @@ export class CopilotAgentSession extends Disposable {
|
||||
private _completeSubagentTurn(agentId: string | undefined, toolCallId?: string): void {
|
||||
if (agentId) {
|
||||
this._subagentTaskCompletionSchedulers.deleteAndDispose(agentId);
|
||||
this._pendingSubagentLifecycleCompletions.delete(agentId);
|
||||
this._subagentActivityRevisions.delete(agentId);
|
||||
if (!this._activeSubagentAgentIds.delete(agentId)) {
|
||||
return;
|
||||
@@ -1869,18 +1878,7 @@ export class CopilotAgentSession extends Disposable {
|
||||
this._autoModeResolvedByToolCallId.delete(parentToolCallId);
|
||||
}
|
||||
|
||||
private _scheduleSubagentTurnCompletion(agentId: string, toolCallId?: string): void {
|
||||
if (!this._activeSubagentAgentIds.has(agentId)) {
|
||||
return;
|
||||
}
|
||||
const parentToolCallId = toolCallId ?? this._parentToolCallIdsByAgentId.get(agentId);
|
||||
if (!parentToolCallId) {
|
||||
return;
|
||||
}
|
||||
if (this._subagentTaskCompletionDelay <= 0) {
|
||||
this._completeSubagentTurn(agentId, parentToolCallId);
|
||||
return;
|
||||
}
|
||||
private _getOrCreateSubagentCompletionScheduler(agentId: string): RunOnceScheduler {
|
||||
let scheduler = this._subagentTaskCompletionSchedulers.get(agentId);
|
||||
if (!scheduler) {
|
||||
const confirmation = new RunOnceScheduler(() => {
|
||||
@@ -1900,9 +1898,84 @@ export class CopilotAgentSession extends Disposable {
|
||||
scheduler = confirmation;
|
||||
this._subagentTaskCompletionSchedulers.set(agentId, scheduler);
|
||||
}
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
private _scheduleSubagentTurnCompletion(agentId: string, toolCallId?: string, outcome?: SubagentTurnOutcome): void {
|
||||
if (!this._activeSubagentAgentIds.has(agentId)) {
|
||||
return;
|
||||
}
|
||||
const parentToolCallId = toolCallId ?? this._parentToolCallIdsByAgentId.get(agentId);
|
||||
if (!parentToolCallId) {
|
||||
return;
|
||||
}
|
||||
if (this._subagentTaskCompletionDelay <= 0) {
|
||||
this._settleSubagentTurn(agentId, parentToolCallId, outcome);
|
||||
return;
|
||||
}
|
||||
const scheduler = this._getOrCreateSubagentCompletionScheduler(agentId);
|
||||
scheduler.schedule();
|
||||
}
|
||||
|
||||
private _scheduleSubagentLifecycleCompletion(agentId: string | undefined, toolCallId: string, outcome?: SubagentTurnOutcome): void {
|
||||
const mappedAgentId = agentId ?? [...this._parentToolCallIdsByAgentId].find(([, parentToolCallId]) => parentToolCallId === toolCallId)?.[0];
|
||||
if (!mappedAgentId) {
|
||||
if (this._rootTurnIdBySubagentToolCallId.has(toolCallId)) {
|
||||
this._emitSubagentOutcome(toolCallId, outcome);
|
||||
this._completeSubagentTurn(undefined, toolCallId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this._activeSubagentAgentIds.has(mappedAgentId)) {
|
||||
return;
|
||||
}
|
||||
this._pendingSubagentLifecycleCompletions.set(mappedAgentId, {
|
||||
toolCallId,
|
||||
activityRevision: this._subagentActivityRevisions.get(mappedAgentId),
|
||||
outcome,
|
||||
});
|
||||
this._getOrCreateSubagentCompletionScheduler(mappedAgentId).schedule();
|
||||
}
|
||||
|
||||
/** Completes an active child turn, first ending it as failed or cancelled when it did not succeed. */
|
||||
private _settleSubagentTurn(agentId: string, toolCallId: string, outcome: SubagentTurnOutcome | undefined): void {
|
||||
if (this._activeSubagentAgentIds.has(agentId)) {
|
||||
this._emitSubagentOutcome(toolCallId, outcome);
|
||||
}
|
||||
this._completeSubagentTurn(agentId, toolCallId);
|
||||
}
|
||||
|
||||
/** Task status is authoritative; a pending lifecycle event only supplies details the status lacks. */
|
||||
private _taskOutcome(task: Extract<Awaited<ReturnType<CopilotSession['rpc']['tasks']['list']>>['tasks'][number], { type: 'agent' }>): SubagentTurnOutcome | undefined {
|
||||
const pending = this._pendingSubagentLifecycleCompletions.get(task.id)?.outcome;
|
||||
switch (task.status) {
|
||||
case 'failed':
|
||||
return pending?.kind === 'failed' ? pending : { kind: 'failed', message: task.error ?? localize('copilotAgent.subagentFailed', "Subagent failed."), durationMs: task.activeTimeMs };
|
||||
case 'cancelled':
|
||||
return pending?.kind === 'cancelled' ? pending : { kind: 'cancelled', durationMs: task.activeTimeMs };
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Ends the child turn as failed or cancelled; the completion that follows then finds no active turn. */
|
||||
private _emitSubagentOutcome(toolCallId: string, outcome: SubagentTurnOutcome | undefined): void {
|
||||
if (outcome?.kind === 'failed') {
|
||||
this._emitAction({
|
||||
type: ActionType.ChatError,
|
||||
turnId: this._turnId,
|
||||
duration: outcome.durationMs ?? 0,
|
||||
part: createErrorResponsePart({ errorType: 'subagentFailed', message: outcome.message }),
|
||||
}, toolCallId);
|
||||
} else if (outcome?.kind === 'cancelled') {
|
||||
this._emitAction({
|
||||
type: ActionType.ChatTurnCancelled,
|
||||
turnId: this._turnId,
|
||||
duration: outcome.durationMs ?? 0,
|
||||
}, toolCallId);
|
||||
}
|
||||
}
|
||||
|
||||
private _observeTokenUsage(parentToolCallId: string | undefined, eventId: string, model: string | undefined, scope: 'direct-model' | 'compaction', tokens: UsageContext, reasoningEffort?: string, apiCallId?: string): void {
|
||||
const recordId = apiCallId ? `api:${apiCallId}` : eventId ? `event:${eventId}` : undefined;
|
||||
const identity = recordId ? `${scope}\0${recordId}` : undefined;
|
||||
@@ -1960,10 +2033,12 @@ export class CopilotAgentSession extends Disposable {
|
||||
return false;
|
||||
}
|
||||
this._publishBackgroundWork(tasks.tasks);
|
||||
const agentTaskIds = new Set<string>();
|
||||
for (const task of tasks.tasks) {
|
||||
if (task.type !== 'agent') {
|
||||
continue;
|
||||
}
|
||||
agentTaskIds.add(task.id);
|
||||
const displayName = subagentChatTitle(task.description, task.displayName);
|
||||
if (displayName && !this._subagentDisplayNamesByAgentId.get(task.id)?.trim()) {
|
||||
this._subagentDisplayNamesByAgentId.set(task.id, displayName);
|
||||
@@ -1974,14 +2049,29 @@ export class CopilotAgentSession extends Disposable {
|
||||
if (task.status === 'idle' || task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
|
||||
const scheduler = this._subagentTaskCompletionSchedulers.get(task.id);
|
||||
if (scheduler && !scheduler.isScheduled()) {
|
||||
this._completeSubagentTurn(task.id, task.toolCallId);
|
||||
this._settleSubagentTurn(task.id, task.toolCallId, this._taskOutcome(task));
|
||||
} else if (!scheduler) {
|
||||
this._scheduleSubagentTurnCompletion(task.id, task.toolCallId);
|
||||
this._scheduleSubagentTurnCompletion(task.id, task.toolCallId, this._taskOutcome(task));
|
||||
}
|
||||
} else {
|
||||
this._pendingSubagentLifecycleCompletions.delete(task.id);
|
||||
this._subagentTaskCompletionSchedulers.deleteAndDispose(task.id);
|
||||
}
|
||||
}
|
||||
for (const [agentId, pending] of this._pendingSubagentLifecycleCompletions) {
|
||||
if (agentTaskIds.has(agentId)) {
|
||||
continue;
|
||||
}
|
||||
if (pending.activityRevision !== this._subagentActivityRevisions.get(agentId)) {
|
||||
this._pendingSubagentLifecycleCompletions.delete(agentId);
|
||||
this._subagentTaskCompletionSchedulers.deleteAndDispose(agentId);
|
||||
continue;
|
||||
}
|
||||
const scheduler = this._subagentTaskCompletionSchedulers.get(agentId);
|
||||
if (scheduler && !scheduler.isScheduled()) {
|
||||
this._settleSubagentTurn(agentId, pending.toolCallId, pending.outcome);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -2131,7 +2221,13 @@ export class CopilotAgentSession extends Disposable {
|
||||
private _createToolCallMeta(toolName: string, parameters: Record<string, unknown> | undefined): Mutable<IToolCallMeta> {
|
||||
const toolKind = getToolKind(toolName, parameters);
|
||||
const subagentMeta = toolKind === 'subagent' ? getSubagentMetadata(parameters) : undefined;
|
||||
const clientToolName = this._clientToolName(toolName);
|
||||
const networkRestrictions = this._clientToolNames.has(clientToolName)
|
||||
? getCopilotBrowserSandboxNetworkRestrictions(this._configurationService, this._ownerSessionUri.toString(), clientToolName) : undefined;
|
||||
return {
|
||||
...(networkRestrictions ? {
|
||||
'vscode.copilotSandboxNetworkRestrictions': networkRestrictions,
|
||||
} : {}),
|
||||
toolKind,
|
||||
language: toolKind === 'terminal' ? getShellLanguage(toolName) : undefined,
|
||||
subagentDescription: subagentMeta?.description,
|
||||
@@ -2503,6 +2599,19 @@ export class CopilotAgentSession extends Disposable {
|
||||
}, parentToolCallId, trustedRootTurn);
|
||||
}
|
||||
|
||||
private _emitSlashCommandOutput(content: string, meta?: Record<string, unknown>): void {
|
||||
const turn = this._currentTurn.value;
|
||||
if (!meta || !turn) {
|
||||
this._emitMarkdownDelta(content, undefined, true);
|
||||
return;
|
||||
}
|
||||
this._emitAction({
|
||||
type: ActionType.ChatResponsePart,
|
||||
turnId: turn.id,
|
||||
part: { kind: ResponsePartKind.SystemNotification, content: { markdown: content }, _meta: meta },
|
||||
}, undefined, true);
|
||||
}
|
||||
|
||||
/** Emits a reasoning delta, similar to {@link _emitMarkdownDelta} but for reasoning parts. */
|
||||
private _emitReasoningDelta(content: string, parentToolCallId?: string): void {
|
||||
if (parentToolCallId === undefined && this._shouldDropLateRootTurnEvent('assistant.reasoning_delta')) {
|
||||
@@ -3105,6 +3214,7 @@ export class CopilotAgentSession extends Disposable {
|
||||
handleExitPlanModeRequest: this._guarded((request, invocation) => this._handleExitPlanModeRequest(request, invocation), { approved: false } satisfies CopilotExitPlanModeResponse, 'exit-plan-mode'),
|
||||
handleUserInputRequest: this._guarded((request, invocation) => this._handleUserInputRequest(request, invocation), { answer: '', wasFreeform: true } satisfies UserInputResponse, 'user-input'),
|
||||
handleElicitationRequest: this._guarded(context => this._handleElicitationRequest(context), { action: 'cancel' } satisfies ElicitationResult, 'elicitation'),
|
||||
setMcpServerDisplayNames: displayNames => this._setMcpServerDisplayNames(displayNames),
|
||||
handleMcpAuthRequest: this._guarded(request => this._handleMcpAuthRequest(request), { kind: 'cancelled' } satisfies McpAuthResult, 'mcp-auth'),
|
||||
requestUnsandboxedCommandConfirmation: this._guarded(request => this._requestUnsandboxedCommandConfirmation(request), false, 'unsandboxed-command-confirmation'),
|
||||
createClientSdkTools: toolSearchActive => this._createClientSdkTools(toolSearchActive),
|
||||
@@ -3116,6 +3226,17 @@ export class CopilotAgentSession extends Disposable {
|
||||
};
|
||||
}
|
||||
|
||||
private _setMcpServerDisplayNames(displayNames: ReadonlyMap<string, string>): void {
|
||||
this._mcpServerDisplayNames.clear();
|
||||
for (const [serverName, displayName] of displayNames) {
|
||||
this._mcpServerDisplayNames.set(serverName, displayName);
|
||||
const state = this._mcpCustomizations.stateForServer(serverName);
|
||||
if (state) {
|
||||
this._mcpCustomizations.applyOne({ name: serverName, displayName, state });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolves only matching, currently pending SDK authentication callbacks. */
|
||||
async resolveMcpAuthentication(params: AuthenticateParams): Promise<boolean> {
|
||||
let resolved = false;
|
||||
@@ -3199,6 +3320,7 @@ export class CopilotAgentSession extends Disposable {
|
||||
});
|
||||
this._mcpCustomizations.applyOne({
|
||||
name: request.serverName,
|
||||
displayName: this._mcpServerDisplayNames.get(request.serverName),
|
||||
state: {
|
||||
kind: McpServerStatus.AuthRequired,
|
||||
...auth,
|
||||
@@ -3720,14 +3842,17 @@ export class CopilotAgentSession extends Disposable {
|
||||
}
|
||||
const output = await runtimeSlashCommand.getOutput?.(slashCommand.rest, result);
|
||||
const renderedOutput = output ? renderCopilotSlashCommandOutput(output) : undefined;
|
||||
const outputMeta = output?.kind === 'link' && output.openInEditor
|
||||
? toSlashCommandResourceMeta(output.resource, output.preview === true)
|
||||
: undefined;
|
||||
switch (result.kind) {
|
||||
case 'text':
|
||||
this._emitMarkdownDelta(renderedOutput ?? (result.markdown === true ? result.text : escapeMarkdownSyntaxTokens(result.text)), undefined, true);
|
||||
this._emitSlashCommandOutput(renderedOutput ?? (result.markdown === true ? result.text : escapeMarkdownSyntaxTokens(result.text)), outputMeta);
|
||||
break;
|
||||
case 'completed': {
|
||||
const message = renderedOutput ?? result.message;
|
||||
if (message) {
|
||||
this._emitMarkdownDelta(message, undefined, true);
|
||||
this._emitSlashCommandOutput(message, outputMeta);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -6666,7 +6791,7 @@ export class CopilotAgentSession extends Disposable {
|
||||
const telemetrySession = parentToolCallId
|
||||
? URI.parse(buildSubagentSessionUri(this._storageUri.toString(), parentToolCallId))
|
||||
: this.resourceUri;
|
||||
reportCopilotTodoStoreOperation(this._telemetryService, telemetrySession, e.data.toolCallId, tracked.toolName, tracked.parameters, this._currentTurn.value?.clientContext);
|
||||
reportCopilotTodoStoreOperation(this._telemetryService, telemetrySession, e.data.toolCallId, tracked.toolName, tracked.parameters, this._currentTurn.value?.clientContext, parentToolCallId && this._fusionPhaseLabels.has(parentToolCallId) ? 'fusionPhase' : undefined);
|
||||
}
|
||||
this._logService.info(`[Copilot:${sessionId}] Tool completed: ${e.data.toolCallId}`);
|
||||
this._reportToolApprovalIfNoPermission(e.data.toolCallId);
|
||||
@@ -7042,6 +7167,14 @@ export class CopilotAgentSession extends Disposable {
|
||||
this._updateSubagentModel(e.data.toolCallId, e.data.model);
|
||||
}));
|
||||
|
||||
this._register(wrapper.onSubagentCompleted(e => {
|
||||
this._scheduleSubagentLifecycleCompletion(e.agentId, e.data.toolCallId, e.data.cancelled ? { kind: 'cancelled', durationMs: e.data.durationMs } : undefined);
|
||||
}));
|
||||
|
||||
this._register(wrapper.onSubagentFailed(e => {
|
||||
this._scheduleSubagentLifecycleCompletion(e.agentId, e.data.toolCallId, { kind: 'failed', message: e.data.error, durationMs: e.data.durationMs });
|
||||
}));
|
||||
|
||||
this._register(wrapper.onSubagentConfigured(e => {
|
||||
const parentToolCallId = this._parentToolCallIdForSubagentEvent(e);
|
||||
if (!parentToolCallId) {
|
||||
@@ -7825,6 +7958,7 @@ export class CopilotAgentSession extends Disposable {
|
||||
: {};
|
||||
return {
|
||||
name: server.name,
|
||||
displayName: this._mcpServerDisplayNames.get(server.name),
|
||||
state: this._translateSdkMcpStatus(server.name, server.status, server.error, hasPendingAuthentication),
|
||||
...(server.status === 'pending' && !hasPendingAuthentication ? { allowAuthRequiredToStarting: true } : {}),
|
||||
enabled: server.status !== 'disabled' && server.status !== 'not_configured',
|
||||
@@ -8202,6 +8336,8 @@ export class CopilotAgentSession extends Disposable {
|
||||
agentName: COPILOT_FUSION_PHASE_AGENT_NAME,
|
||||
agentDisplayName: label,
|
||||
taskDescription: label,
|
||||
subagentKind: 'fusionPhase',
|
||||
model: this._fusionProgress.getPhaseModel(toolCallId),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8418,6 +8554,13 @@ export class CopilotAgentSession extends Disposable {
|
||||
this._logService.trace(`[Copilot:${sessionId}] Unhandled SDK event: ${safeStringify(loggedEvent)}`);
|
||||
}));
|
||||
|
||||
this._register(wrapper.onIndexedSearch(e => {
|
||||
const data = e.data.kind === 'startup' || e.data.kind === 'server_error'
|
||||
? (({ errorMessage: _, ...data }) => data)(e.data)
|
||||
: e.data;
|
||||
this._logService.trace(`[Copilot:${sessionId}] session.indexed_search: ${safeStringify(data)}`);
|
||||
}));
|
||||
|
||||
this._register(wrapper.onExtensionsLoaded(() => {
|
||||
if (wrapper.canvasRuntimeEnabled) {
|
||||
this._canvasProjectionReady = true;
|
||||
@@ -8606,6 +8749,7 @@ export class CopilotAgentSession extends Disposable {
|
||||
this._register(wrapper.onTurnStart(e => {
|
||||
if (e.agentId) {
|
||||
this._subagentActivityRevisions.set(e.agentId, (this._subagentActivityRevisions.get(e.agentId) ?? 0) + 1);
|
||||
this._pendingSubagentLifecycleCompletions.delete(e.agentId);
|
||||
this._subagentTaskCompletionSchedulers.deleteAndDispose(e.agentId);
|
||||
}
|
||||
const turn = this._currentTurn.value;
|
||||
|
||||
@@ -149,6 +149,11 @@ export class CopilotFusionProgress {
|
||||
return this._phase ? getFusionPhaseToolCallId(this._phase.fusionId, this._phase.phaseId) : undefined;
|
||||
}
|
||||
|
||||
getPhaseModel(toolCallId: string): string | undefined {
|
||||
const toolCall = this._phaseTools.get(toolCallId);
|
||||
return toolCall ? readToolCallMeta(toolCall).fusionPhase?.model : undefined;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._milestones.clear();
|
||||
this._finishedFusions.clear();
|
||||
|
||||
@@ -209,6 +209,7 @@ type ModelCallTurnCorrelatedClassification = {
|
||||
"arguments": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Telemetry-safe tool arguments; unsafe values are hashed by the runtime." },
|
||||
"result_type": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Coarse tool result outcome, either SUCCESS or FAILURE." },
|
||||
"invoke_outcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Fine-grained invocation outcome: success, error, cancelled, or disabledByUser." },
|
||||
"search_engine": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Search implementation used by the grep tool, either ripgrep or tgrep." },
|
||||
"model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Model that requested the tool call." },
|
||||
"tool_call_id": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Identifier for this tool call, stable across correlated events." },
|
||||
"turn_id": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Identifier for the agent loop turn the tool was invoked in." },
|
||||
@@ -224,12 +225,55 @@ type ModelCallTurnCorrelatedClassification = {
|
||||
"interaction_id": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Identifier that correlates events in an interaction." },
|
||||
"engagement_id": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Identifier that correlates events in an engagement." },
|
||||
"duration_ms": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Wall-clock duration of the tool execution in milliseconds.", "isMeasurement": true },
|
||||
"result_token_count": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Token count of the model-facing tool result, measured with the model tokenizer. Present only for successful results.", "isMeasurement": true },
|
||||
"result_token_count": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Local runtime estimate of the tokens in the tool's explicit model-facing text output (o200k_base scaled by a per-model multiplier); not an exact model-tokenizer count or billed usage. Reported only for successful text results up to 20 KiB without binary or structured-reference content; absence does not mean zero.", "isMeasurement": true },
|
||||
"binary_result_count": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of binary result parts returned by the tool.", "isMeasurement": true },
|
||||
"binary_result_total_bytes": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Total byte size of binary result parts returned by the tool.", "isMeasurement": true }
|
||||
}
|
||||
*/
|
||||
|
||||
/* __GDPR__
|
||||
"copilotSdk/tgrep_startup": {
|
||||
"owner": "bhavyaus",
|
||||
"comment": "Reports the outcome and performance of an indexed-search startup attempt in the Copilot CLI runtime.",
|
||||
"${include}": [ "${CopilotSdkForwardedTelemetry}" ],
|
||||
"outcome": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Bounded outcome of the indexed-search startup attempt." },
|
||||
"forced_by_env": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether indexed search was explicitly enabled through the environment." },
|
||||
"warm_start": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the startup attempted to wait for index readiness." },
|
||||
"disabled_reason": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Bounded configuration, policy, or workspace condition that disabled indexed search." },
|
||||
"eligible": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the repository met the automatic indexing file-count threshold." },
|
||||
"error_message": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "comment": "Indexed-search startup failure details, emitted only as restricted telemetry." },
|
||||
"file_count": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of text files counted in the repository.", "isMeasurement": true },
|
||||
"startup_duration_ms": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Wall-clock duration of indexed-search startup in milliseconds.", "isMeasurement": true }
|
||||
}
|
||||
*/
|
||||
|
||||
/* __GDPR__
|
||||
"copilotSdk/tgrep_server_error": {
|
||||
"owner": "bhavyaus",
|
||||
"comment": "Reports failures of the Copilot CLI indexed-search server.",
|
||||
"${include}": [ "${CopilotSdkForwardedTelemetry}" ],
|
||||
"error_type": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Bounded category of the indexed-search server failure." },
|
||||
"error_message": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "comment": "Indexed-search server failure details, emitted only as restricted telemetry." },
|
||||
"exit_code": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Indexed-search server process exit code when available.", "isMeasurement": true }
|
||||
}
|
||||
*/
|
||||
|
||||
/* __GDPR__
|
||||
"copilotSdk/tgrep_incremental_indexing": {
|
||||
"owner": "bhavyaus",
|
||||
"comment": "Reports aggregate file counts and performance for incremental Copilot CLI indexed-search updates.",
|
||||
"${include}": [ "${CopilotSdkForwardedTelemetry}" ],
|
||||
"phase": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether workspace changes were detected or the incremental index update completed." },
|
||||
"changed_file_count": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of modified files detected during the incremental update.", "isMeasurement": true },
|
||||
"added_file_count": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of added files detected during the incremental update.", "isMeasurement": true },
|
||||
"deleted_file_count": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of deleted files detected during the incremental update.", "isMeasurement": true },
|
||||
"total_change_count": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Total number of changes detected during the incremental update.", "isMeasurement": true },
|
||||
"walk_duration_ms": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Workspace scan duration in milliseconds.", "isMeasurement": true },
|
||||
"update_duration_ms": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Index update duration in milliseconds.", "isMeasurement": true },
|
||||
"total_duration_ms": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Total incremental indexing duration in milliseconds.", "isMeasurement": true }
|
||||
}
|
||||
*/
|
||||
|
||||
/* __GDPR__
|
||||
"copilotSdk/hydrafusion_route": {
|
||||
"owner": "amunger",
|
||||
|
||||
@@ -45,8 +45,9 @@ export class CopilotSandboxDiagnostics extends Disposable {
|
||||
const network = config.userPolicy?.network;
|
||||
reasons = support.capabilities.filter(capability => !capability.supported && (
|
||||
capability.name === 'shell'
|
||||
|| capability.name === 'filesystem_enumeration'
|
||||
|| capability.name === 'network' && network?.allowOutbound === true
|
||||
|| capability.name === 'network_filtering' && network?.proxy !== undefined
|
||||
|| capability.name === 'network_filtering'
|
||||
|| capability.name === 'denied_paths' && !!config.userPolicy?.filesystem?.deniedPaths?.length
|
||||
)).map(capability => capability.reason || localize('copilot.sandbox.capabilityUnavailable', "The sandbox capability '{0}' is unavailable on this host.", capability.name));
|
||||
}
|
||||
|
||||
@@ -4,14 +4,34 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { SessionEvent } from '@github/copilot-sdk';
|
||||
import { browserChatToolReferenceNames } from '../../../browserView/common/browserChatToolReferenceNames.js';
|
||||
import type { ILogService } from '../../../log/common/log.js';
|
||||
import type { ISessionSandboxPolicy } from '../sessionSandbox.js';
|
||||
import { ISandboxNetworkRestrictions, SandboxSettingsResolutionHelper } from '../../../sandbox/common/sandboxSettingsResolutionHelper.js';
|
||||
import type { IAgentConfigurationService } from '../agentConfigurationService.js';
|
||||
import { getSessionSandboxConfig, type ISessionSandboxPolicy } from '../sessionSandbox.js';
|
||||
|
||||
/** Resolves session network restrictions only for Copilot's integrated-browser client tools. */
|
||||
export function getCopilotBrowserSandboxNetworkRestrictions(configuration: IAgentConfigurationService, session: string, clientToolName: string): ISandboxNetworkRestrictions | undefined {
|
||||
if (clientToolName !== 'list_browser_pages' && !browserChatToolReferenceNames.some(name => name === clientToolName)) {
|
||||
return undefined;
|
||||
}
|
||||
const sandbox = getSessionSandboxConfig(configuration, session);
|
||||
const network = SandboxSettingsResolutionHelper.getNetworkRestrictions(sandbox.enabled, sandbox.allowNetwork);
|
||||
return {
|
||||
sandboxEnabled: network.sandboxEnabled,
|
||||
allowNetwork: network.allowNetwork,
|
||||
allowedDomains: sandbox.allowedNetworkDomains ?? [],
|
||||
deniedDomains: sandbox.deniedNetworkDomains ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/** Projects only resolved boolean sandbox fields; composition and validation remain runtime-owned. */
|
||||
export function projectCopilotSandboxPolicy(data: Extract<SessionEvent, { type: 'session.managed_settings_resolved' }>['data'], sessionId: string, logService: ILogService): ISessionSandboxPolicy {
|
||||
const settings = data.settings;
|
||||
const sandboxValue = settings && typeof settings === 'object' && !Array.isArray(settings) ? settings.sandbox : undefined;
|
||||
const sandbox = sandboxValue && typeof sandboxValue === 'object' && !Array.isArray(sandboxValue) ? sandboxValue : undefined;
|
||||
const authValue = sandbox?.auth;
|
||||
const auth = authValue && typeof authValue === 'object' && !Array.isArray(authValue) ? authValue : undefined;
|
||||
const userPolicyValue = sandbox?.userPolicy;
|
||||
const userPolicy = userPolicyValue && typeof userPolicyValue === 'object' && !Array.isArray(userPolicyValue) ? userPolicyValue : undefined;
|
||||
const networkValue = userPolicy?.network;
|
||||
@@ -29,6 +49,8 @@ export function projectCopilotSandboxPolicy(data: Extract<SessionEvent, { type:
|
||||
...(typeof sandbox?.allowDevToolAccess === 'boolean' ? { allowDevToolAccess: sandbox.allowDevToolAccess } : {}),
|
||||
...(typeof sandbox?.sandboxMcpServers === 'boolean' ? { sandboxMcpServers: sandbox.sandboxMcpServers } : {}),
|
||||
...(typeof sandbox?.sandboxLspServers === 'boolean' ? { sandboxLspServers: sandbox.sandboxLspServers } : {}),
|
||||
...(typeof auth?.git === 'boolean' ? { authenticateGit: auth.git } : {}),
|
||||
...(typeof auth?.gh === 'boolean' ? { authenticateGh: auth.gh } : {}),
|
||||
...(sandboxFailClosed ? { failClosed: true } : {}),
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import { VSBuffer } from '../../../../base/common/buffer.js';
|
||||
import { appendEscapedMarkdownInlineCode, escapeMarkdownSyntaxTokens, MarkdownString } from '../../../../base/common/htmlContent.js';
|
||||
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { IFileService } from '../../../files/common/files.js';
|
||||
import { ILogService } from '../../../log/common/log.js';
|
||||
@@ -16,12 +15,12 @@ import type { CopilotSlashCommandOutput, CopilotSlashCommandResult, ICopilotSlas
|
||||
|
||||
export const copilotSandboxPolicyCommand: RuntimeSlashCommandInfo = {
|
||||
name: 'sandbox',
|
||||
description: localize('copilotSlashCommand.sandboxPolicy', "Show the effective sandbox policy for this session"),
|
||||
description: localize('copilotSlashCommand.sandboxPolicy', "Open the latest effective sandbox policy for this session in the editor"),
|
||||
kind: 'builtin',
|
||||
allowDuringAgentExecution: true,
|
||||
input: {
|
||||
hint: '',
|
||||
choices: [{ name: 'policy', description: localize('copilotSlashCommand.sandboxPolicy', "Show the effective sandbox policy for this session") }],
|
||||
choices: [{ name: 'policy', description: localize('copilotSlashCommand.sandboxPolicy', "Open the latest effective sandbox policy for this session in the editor") }],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -49,7 +48,7 @@ export class CopilotSandboxPolicyDisplay {
|
||||
throw new Error(localize('copilotSlashCommand.sandboxPolicyUnavailable', "The SDK did not return a sandbox policy."));
|
||||
}
|
||||
const markdown = formatSandboxPolicyMarkdown(text, result.kind !== 'text' || result.markdown === true);
|
||||
const resource = URI.joinPath(this._sessionDataService.getSessionDataDir(this._storageUri), 'diagnostics', generateUuid(), 'sandbox-policy.md');
|
||||
const resource = URI.joinPath(this._sessionDataService.getSessionDataDir(this._storageUri), 'diagnostics', 'sandbox-policy.md');
|
||||
try {
|
||||
await this._fileService.writeFile(resource, VSBuffer.fromString(markdown));
|
||||
} catch (err) {
|
||||
@@ -61,6 +60,7 @@ export class CopilotSandboxPolicyDisplay {
|
||||
resource,
|
||||
label: localize('copilotSlashCommand.openSandboxPolicy', "Open Sandbox Policy"),
|
||||
preview: true,
|
||||
openInEditor: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,7 @@ export interface ICopilotSessionRuntime {
|
||||
handleExitPlanModeRequest(request: ExitPlanModeRequest, invocation: { sessionId: string }): Promise<ExitPlanModeResult>;
|
||||
handleUserInputRequest(request: UserInputRequest, invocation: UserInputInvocation): Promise<UserInputResponse>;
|
||||
handleElicitationRequest(context: ElicitationContext): Promise<ElicitationResult>;
|
||||
setMcpServerDisplayNames(displayNames: ReadonlyMap<string, string>): void;
|
||||
handleMcpAuthRequest(request: McpAuthRequest, context: McpAuthContext): Promise<McpAuthResponse>;
|
||||
requestUnsandboxedCommandConfirmation(request: IUnsandboxedCommandConfirmationRequest): Promise<boolean>;
|
||||
handlePreToolUse(input: PreToolUseHookInput): Promise<PreToolUseHookOutput>;
|
||||
@@ -706,7 +707,7 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher {
|
||||
}
|
||||
};
|
||||
if (plan.kind === 'create') {
|
||||
return this._createSession(plan, config, sandboxConfig);
|
||||
return this._createSession(plan, config, sandboxConfig, runtime);
|
||||
}
|
||||
|
||||
let fallbackPlan = plan;
|
||||
@@ -714,7 +715,7 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher {
|
||||
const session = AgentSession.uri('copilotcli', plan.sessionId);
|
||||
try {
|
||||
const raw = await this._resumeSession(session, plan, config);
|
||||
return this._finalizeSession(raw, sandboxConfig, plan, plan.fallback.model?.id, config);
|
||||
return this._finalizeSession(raw, sandboxConfig, plan, plan.fallback.model?.id, config, runtime);
|
||||
} catch (err) {
|
||||
let resumeError = err;
|
||||
const errCode = getCopilotSdkErrorCode(resumeError);
|
||||
@@ -726,7 +727,7 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher {
|
||||
this._logService.warn(`[Copilot:${plan.sessionId}] Stored custom agent '${plan.resolvedAgentName}' was not found; retrying resume without a custom agent`);
|
||||
try {
|
||||
const raw = await this._resumeSession(session, fallbackPlan, fallbackConfig);
|
||||
return this._finalizeSession(raw, sandboxConfig, fallbackPlan, fallbackPlan.fallback.model?.id, fallbackConfig);
|
||||
return this._finalizeSession(raw, sandboxConfig, fallbackPlan, fallbackPlan.fallback.model?.id, fallbackConfig, runtime);
|
||||
} catch (retryErr) {
|
||||
resumeError = retryErr;
|
||||
this._logService.warn(`[Copilot:${plan.sessionId}] SDK resumeSession without custom agent failed: code=${getCopilotSdkErrorCode(retryErr)}, message=${getErrorMessage(retryErr)}`);
|
||||
@@ -747,7 +748,7 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher {
|
||||
model: fallbackPlan.fallback.model,
|
||||
longContextWindow: fallbackPlan.fallback.longContextWindow,
|
||||
freeLongContext: fallbackPlan.fallback.freeLongContext,
|
||||
}, fallbackConfig, sandboxConfig);
|
||||
}, fallbackConfig, sandboxConfig, runtime);
|
||||
this._sessionOpenTelemetry.sdkResumeFallbackCreated(session);
|
||||
this._logService.info(`[Copilot:${plan.sessionId}] Fallback createSession succeeded`);
|
||||
return wrapper;
|
||||
@@ -776,7 +777,7 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher {
|
||||
return this._otelService.withTraceContext(this._otelService.getSessionTraceContext(sessionId, sessionUri), fn);
|
||||
}
|
||||
|
||||
private async _createSession(plan: ICopilotCreateSessionLaunchPlan, config: ResumeSessionConfig, sandboxConfig: (session: CopilotSessionWrapper['session']) => Promise<void>): Promise<CopilotSessionWrapper> {
|
||||
private async _createSession(plan: ICopilotCreateSessionLaunchPlan, config: ResumeSessionConfig, sandboxConfig: (session: CopilotSessionWrapper['session']) => Promise<void>, runtime: ICopilotSessionRuntime): Promise<CopilotSessionWrapper> {
|
||||
const raw = await this._withTraceContext(plan.sessionId, () => plan.client.createSession({
|
||||
...config,
|
||||
sessionId: plan.sessionId,
|
||||
@@ -787,15 +788,15 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher {
|
||||
...(plan.resolvedAgentName ? { agent: plan.resolvedAgentName } : {}),
|
||||
workingDirectory: plan.workingDirectory?.fsPath,
|
||||
}));
|
||||
return this._finalizeSession(raw, sandboxConfig, plan, plan.model?.id, config);
|
||||
return this._finalizeSession(raw, sandboxConfig, plan, plan.model?.id, config, runtime);
|
||||
}
|
||||
|
||||
private async _finalizeSession(raw: CopilotSessionWrapper['session'], sandboxConfig: (session: CopilotSessionWrapper['session']) => Promise<void>, plan: CopilotSessionLaunchPlan, modelId: string | undefined, config: ResumeSessionConfig): Promise<CopilotSessionWrapper> {
|
||||
private async _finalizeSession(raw: CopilotSessionWrapper['session'], sandboxConfig: (session: CopilotSessionWrapper['session']) => Promise<void>, plan: CopilotSessionLaunchPlan, modelId: string | undefined, config: ResumeSessionConfig, runtime: ICopilotSessionRuntime): Promise<CopilotSessionWrapper> {
|
||||
plan.stageRecorder?.mark('finalize');
|
||||
try {
|
||||
await this._applyScriptSafety(raw, plan.sessionId);
|
||||
await sandboxConfig(raw);
|
||||
await this._reconcileCopilotConnectors(raw, plan);
|
||||
runtime.setMcpServerDisplayNames(await this._reconcileCopilotConnectors(raw, plan));
|
||||
} catch (err) {
|
||||
// Nothing owns `raw` until it is wrapped below, so a fail-closed launch has
|
||||
// to disconnect it here or the runtime keeps an orphaned session alive.
|
||||
@@ -809,15 +810,16 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher {
|
||||
return new CopilotSessionWrapper(raw, config.requestCanvasRenderer === true, { providers: config.providers, models: config.models }, this._logService);
|
||||
}
|
||||
|
||||
private async _reconcileCopilotConnectors(session: CopilotSessionWrapper['session'], plan: CopilotSessionLaunchPlan): Promise<void> {
|
||||
private async _reconcileCopilotConnectors(session: CopilotSessionWrapper['session'], plan: CopilotSessionLaunchPlan): Promise<ReadonlyMap<string, string>> {
|
||||
const displayNames = new Map<string, string>();
|
||||
if (this._configurationService.getRootValue(platformRootSchema, AgentHostMcpConnectorsEnabledConfigKey) !== true) {
|
||||
return;
|
||||
return displayNames;
|
||||
}
|
||||
try {
|
||||
const capabilities = await session.rpc.connectors.getCapabilities();
|
||||
if (capabilities.availability !== 'enabled') {
|
||||
this._logService.info(`[Copilot:${plan.sessionId}] Connector MCP reconciliation unavailable: ${capabilities.availability}`);
|
||||
return;
|
||||
return displayNames;
|
||||
}
|
||||
const token = plan.githubCredentials.token;
|
||||
const auth = await session.rpc.gitHubAuth.getStatus();
|
||||
@@ -837,13 +839,18 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher {
|
||||
}
|
||||
if (!account?.selectionId) {
|
||||
this._logService.warn(`[Copilot:${plan.sessionId}] Connector MCP reconciliation skipped because the session account could not be resolved`);
|
||||
return;
|
||||
return displayNames;
|
||||
}
|
||||
const status = await session.rpc.connectors.reconcile({ accountId: account.selectionId, refreshCatalog: true });
|
||||
const connectorDisplayNames = new Map(status.catalog?.connectors.map(connector => [connector.name, connector.displayName.trim() || connector.name]));
|
||||
for (const server of status.runtimeServers) {
|
||||
displayNames.set(server.runtimeServerId, connectorDisplayNames.get(server.connectorName) ?? server.connectorName);
|
||||
}
|
||||
this._logService.info(`[Copilot:${plan.sessionId}] Reconciled ${status.runtimeServers.length} connector MCP server(s) through the Copilot runtime`);
|
||||
} catch (error) {
|
||||
this._logService.warn(`[Copilot:${plan.sessionId}] Connector MCP reconciliation failed; continuing without connector tools: ${getErrorMessage(error)}`);
|
||||
}
|
||||
return displayNames;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -230,6 +230,11 @@ export class CopilotSessionWrapper extends Disposable {
|
||||
return this._onSessionInfo ??= this._sdkEvent('session.info');
|
||||
}
|
||||
|
||||
private _onIndexedSearch: Event<SessionEventPayload<'session.indexed_search'>> | undefined;
|
||||
get onIndexedSearch(): Event<SessionEventPayload<'session.indexed_search'>> {
|
||||
return this._onIndexedSearch ??= this._sdkEvent('session.indexed_search');
|
||||
}
|
||||
|
||||
private _onSessionWarning: Event<SessionEventPayload<'session.warning'>> | undefined;
|
||||
get onSessionWarning(): Event<SessionEventPayload<'session.warning'>> {
|
||||
return this._onSessionWarning ??= this._sdkEvent('session.warning');
|
||||
|
||||
@@ -13,7 +13,7 @@ export type CopilotSlashCommandResult = Awaited<ReturnType<CopilotSession['rpc']
|
||||
|
||||
export type CopilotSlashCommandOutput =
|
||||
| { readonly kind: 'text'; readonly text: string; readonly markdown?: boolean }
|
||||
| { readonly kind: 'link'; readonly resource: URI; readonly label: string; readonly preview?: boolean };
|
||||
| { readonly kind: 'link'; readonly resource: URI; readonly label: string; readonly preview?: boolean; readonly openInEditor?: boolean };
|
||||
|
||||
/** Host-side behavior attached to a resolved command, separate from SDK catalog metadata. */
|
||||
export interface ICopilotSlashCommandHandler {
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import type { ITelemetryService } from '../../../telemetry/common/telemetry.js';
|
||||
import { AgentSession } from '../../common/agent.js';
|
||||
import { AgentSession, type AgentSubagentKind } from '../../common/agent.js';
|
||||
import type { IAgentHostClientTelemetryContext } from '../../common/agentHostTelemetry.js';
|
||||
import { isSubagentSession } from '../../common/state/sessionState.js';
|
||||
import { toInitiatorTelemetry, type IAgentHostEventClassification, type IAgentHostEventTelemetry } from '../agentHostTelemetryReporter.js';
|
||||
import { toInitiatorTelemetry, toSubagentKindTelemetry, type IAgentHostEventClassification, type IAgentHostEventTelemetry, type IAgentHostSubagentKindClassification } from '../agentHostTelemetryReporter.js';
|
||||
|
||||
type TodoStoreOperation = 'read' | 'write' | 'mixed';
|
||||
type TodoStoreTarget = 'todos' | 'todo_deps' | 'both';
|
||||
@@ -20,9 +20,10 @@ type TodoStoreOperationEvent = IAgentHostEventTelemetry & {
|
||||
provider: string;
|
||||
agentSessionId: string;
|
||||
isSubagentSession: boolean;
|
||||
subagentKind?: AgentSubagentKind;
|
||||
};
|
||||
|
||||
type TodoStoreOperationClassification = IAgentHostEventClassification & {
|
||||
type TodoStoreOperationClassification = IAgentHostEventClassification & IAgentHostSubagentKindClassification & {
|
||||
operation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the SQL operation read from, wrote to, or both read from and wrote to todo storage.' };
|
||||
target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the SQL operation referenced todo items, todo dependencies, or both.' };
|
||||
toolCallId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the SQL tool call, used to correlate with generic tool telemetry.' };
|
||||
@@ -43,19 +44,21 @@ interface ISqlToken {
|
||||
readonly kind: 'identifier' | 'punctuation';
|
||||
}
|
||||
|
||||
export function reportCopilotTodoStoreOperation(telemetryService: ITelemetryService, session: URI, toolCallId: string, toolName: string, toolInput: Readonly<Record<string, unknown>> | undefined, clientContext?: IAgentHostClientTelemetryContext): void {
|
||||
export function reportCopilotTodoStoreOperation(telemetryService: ITelemetryService, session: URI, toolCallId: string, toolName: string, toolInput: Readonly<Record<string, unknown>> | undefined, clientContext?: IAgentHostClientTelemetryContext, subagentKind?: AgentSubagentKind): void {
|
||||
const operation = getCopilotTodoStoreOperationData(toolName, toolInput);
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSubagent = isSubagentSession(session);
|
||||
telemetryService.publicLog2<TodoStoreOperationEvent, TodoStoreOperationClassification>('todoStoreOperation', {
|
||||
...toInitiatorTelemetry(clientContext),
|
||||
...operation,
|
||||
toolCallId,
|
||||
provider: session.scheme,
|
||||
agentSessionId: AgentSession.id(session),
|
||||
isSubagentSession: isSubagentSession(session),
|
||||
isSubagentSession: isSubagent,
|
||||
...toSubagentKindTelemetry(isSubagent, subagentKind),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ export interface SandboxSeatbeltPolicy {
|
||||
*
|
||||
* All platforms share enablement and user-configured paths; legacy per-OS paths are ignored.
|
||||
* Optional toggles are forwarded only when supplied; absent values use runtime defaults.
|
||||
* Credential authentication defaults to enabled and uses the effective Copilot-only preferences.
|
||||
*
|
||||
* `extraReadonlyPaths` grants read access to session attachments and generated
|
||||
* shell init scripts when the effective sandbox is applied before each turn.
|
||||
@@ -168,8 +169,8 @@ export function buildSandboxConfigForSdk(
|
||||
...(allowDevToolAccess !== undefined ? { allowDevToolAccess } : {}),
|
||||
...(allowBypass !== undefined ? { allowBypass } : {}),
|
||||
auth: {
|
||||
git: true,
|
||||
gh: true,
|
||||
git: sandbox?.[AgentHostSandboxKey.AuthenticateGit] ?? true,
|
||||
gh: sandbox?.[AgentHostSandboxKey.AuthenticateGh] ?? true,
|
||||
},
|
||||
userPolicy: {
|
||||
...(denied.size || readonly.size || readwrite.size ? {
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
type SubscribeResult,
|
||||
type ListSessionsResult,
|
||||
} from '../common/state/sessionProtocol.js';
|
||||
import { isAhpAutomationCatalogChannel, isAhpResourceWatchChannel, ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, buildDefaultChatUri, isAhpChatChannel, parseChatUri, parseRequiredSessionUriFromChatUri, type ISessionWithDefaultChat, type SessionState } from '../common/state/sessionState.js';
|
||||
import { isAhpAutomationCatalogChannel, isAhpResourceWatchChannel, ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, buildDefaultChatUri, isAhpChatChannel, parseChatUri, parseRequiredSessionUriFromChatUri, withSessionStatusFlag, type ISessionWithDefaultChat, type SessionState } from '../common/state/sessionState.js';
|
||||
import type { IProtocolServer, IProtocolTransport } from '../common/state/sessionTransport.js';
|
||||
import { IAgentHostManagedSettingsService } from './agentHostManagedSettingsService.js';
|
||||
import { AgentHostStateManager } from './agentHostStateManager.js';
|
||||
@@ -118,6 +118,13 @@ function jsonRpcErrorFrom(id: number, err: unknown): JsonRpcResponse {
|
||||
return jsonRpcError(id, JSON_RPC_INTERNAL_ERROR, message);
|
||||
}
|
||||
|
||||
/** AHP has no cancellation-specific code, but a cancelled request still needs an error response. */
|
||||
class SubscriptionCancelledError extends ProtocolError {
|
||||
constructor(channel: string) {
|
||||
super(JSON_RPC_INTERNAL_ERROR, `Subscription cancelled: ${channel}`);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldLogFailedRequest(method: string, params: unknown, err: unknown): boolean {
|
||||
if (!(err instanceof ProtocolError) || err.code !== AhpErrorCodes.NotFound || !isFileResourceRead(method, params)) {
|
||||
return true;
|
||||
@@ -1613,7 +1620,11 @@ export class ProtocolServerHandler extends Disposable implements IAgentHostClien
|
||||
// is JSON over the wire, so narrowing at this boundary is safe.
|
||||
return { snapshot: snapshot as SubscribeResult['snapshot'] };
|
||||
} catch (err) {
|
||||
if (!pendingSubscription.active && client.subscriptions.get(classified.uri) === pendingSubscription) {
|
||||
// Losing request ownership cancels the subscription, not the resource.
|
||||
if (client.subscriptions.get(classified.uri) !== pendingSubscription) {
|
||||
throw new SubscriptionCancelledError(params.channel);
|
||||
}
|
||||
if (!pendingSubscription.active) {
|
||||
client.subscriptions.delete(classified.uri);
|
||||
}
|
||||
if (err instanceof ProtocolError) {
|
||||
@@ -1734,6 +1745,13 @@ export class ProtocolServerHandler extends Disposable implements IAgentHostClien
|
||||
title: chat.summary ?? '',
|
||||
origin: chat.origin,
|
||||
...(chat.interactivity !== undefined ? { interactivity: chat.interactivity } : {}),
|
||||
...(chat.isRead !== undefined ? {
|
||||
status: withSessionStatusFlag(
|
||||
withSessionStatusFlag(SessionStatus.Idle, SessionStatus.IsArchived, chat.archived === true),
|
||||
SessionStatus.IsRead,
|
||||
chat.isRead,
|
||||
),
|
||||
} : {}),
|
||||
...(chat.archived === true ? { archived: true } : {}),
|
||||
...(chat.changes !== undefined ? { changes: chat.changes } : {}),
|
||||
})),
|
||||
@@ -1886,7 +1904,9 @@ export class ProtocolServerHandler extends Disposable implements IAgentHostClien
|
||||
this._logService.trace(`[ProtocolServer] Request '${method}' id=${id} succeeded`);
|
||||
client.transport.send(jsonRpcSuccess(id, result ?? null));
|
||||
}).catch(err => {
|
||||
if (shouldLogFailedRequest(method, params, err)) {
|
||||
if (err instanceof SubscriptionCancelledError) {
|
||||
this._logService.trace(`[ProtocolServer] Request '${method}' id=${id} cancelled`, err.message);
|
||||
} else if (shouldLogFailedRequest(method, params, err)) {
|
||||
this._logService.error(`[ProtocolServer] Request '${method}' failed`, err);
|
||||
}
|
||||
client.transport.send(jsonRpcErrorFrom(id, err));
|
||||
|
||||
@@ -38,7 +38,7 @@ export function getSessionSandboxConfig(configuration: IAgentConfigurationServic
|
||||
}
|
||||
|
||||
/** Resolves the owning session's toggle overrides without changing global settings. */
|
||||
export function getSessionSandboxOverrides(configuration: IAgentConfigurationService, session: string): Pick<ISandboxConfigValue, AgentHostSandboxKey.Enabled | AgentHostSandboxKey.AllowUnsandboxedCommands | AgentHostSandboxKey.AllowNetwork | AgentHostSandboxKey.AllowLocalNetwork | AgentHostSandboxKey.AllowDevToolAccess | AgentHostSandboxKey.SandboxMcpServers | AgentHostSandboxKey.SandboxLspServers> {
|
||||
export function getSessionSandboxOverrides(configuration: IAgentConfigurationService, session: string): Pick<ISandboxConfigValue, AgentHostSandboxKey.Enabled | AgentHostSandboxKey.AllowUnsandboxedCommands | AgentHostSandboxKey.AllowNetwork | AgentHostSandboxKey.AllowLocalNetwork | AgentHostSandboxKey.AllowDevToolAccess | AgentHostSandboxKey.SandboxMcpServers | AgentHostSandboxKey.SandboxLspServers | AgentHostSandboxKey.AuthenticateGit | AgentHostSandboxKey.AuthenticateGh> {
|
||||
session = resolveAgentHostSession(URI.parse(session)).toString();
|
||||
const raw = configuration.getSessionConfigValues(session)?.[SessionConfigKey.SandboxEnabled];
|
||||
const selection = platformSessionSchema.validate(SessionConfigKey.SandboxEnabled, raw) ? raw : undefined;
|
||||
@@ -54,6 +54,8 @@ export function getSessionSandboxOverrides(configuration: IAgentConfigurationSer
|
||||
const allowDevToolAccess = SandboxSettingsResolutionHelper.resolveAllowAccess(undefined, policy?.allowDevToolAccess);
|
||||
const sandboxMcpServers = SandboxSettingsResolutionHelper.resolveSandboxServers(undefined, policy?.sandboxMcpServers);
|
||||
const sandboxLspServers = SandboxSettingsResolutionHelper.resolveSandboxServers(undefined, policy?.sandboxLspServers);
|
||||
const authenticateGit = SandboxSettingsResolutionHelper.resolveAllowAccess(undefined, policy?.authenticateGit);
|
||||
const authenticateGh = SandboxSettingsResolutionHelper.resolveAllowAccess(undefined, policy?.authenticateGh);
|
||||
return {
|
||||
...(enabled !== undefined ? {
|
||||
[AgentHostSandboxKey.Enabled]: enabled,
|
||||
@@ -64,5 +66,7 @@ export function getSessionSandboxOverrides(configuration: IAgentConfigurationSer
|
||||
...(allowDevToolAccess !== undefined ? { [AgentHostSandboxKey.AllowDevToolAccess]: allowDevToolAccess } : {}),
|
||||
...(sandboxMcpServers !== undefined ? { [AgentHostSandboxKey.SandboxMcpServers]: sandboxMcpServers } : {}),
|
||||
...(sandboxLspServers !== undefined ? { [AgentHostSandboxKey.SandboxLspServers]: sandboxLspServers } : {}),
|
||||
...(authenticateGit !== undefined ? { [AgentHostSandboxKey.AuthenticateGit]: authenticateGit } : {}),
|
||||
...(authenticateGh !== undefined ? { [AgentHostSandboxKey.AuthenticateGh]: authenticateGh } : {}),
|
||||
};
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { URI } from '../../../../base/common/uri.js';
|
||||
import { AgentSession } from '../../common/agent.js';
|
||||
import { ActionType } from '../../common/state/protocol/common/actions.js';
|
||||
import { isCustomizationEnabled } from '../../common/customizationEnablement.js';
|
||||
import { McpServerSource, readMcpServerSource, withMcpServerSourceMeta } from '../../common/meta/mcpCustomizationMeta.js';
|
||||
import { McpServerSource, readMcpServerDisplayName, readMcpServerSource, withMcpServerDisplayNameMeta, withMcpServerSourceMeta } from '../../common/meta/mcpCustomizationMeta.js';
|
||||
import { CustomizationLoadStatus, CustomizationType, McpServerStatus, type AhpMcpUiHostCapabilities, type Customization, type CustomizationEnablement, type McpServerCustomization, type McpServerState } from '../../common/state/protocol/channels-session/state.js';
|
||||
import { DEFAULT_MCP_APP, DEFAULT_MCP_APP_CAPABILITIES } from '../../common/state/protocol/mcpAppDefaults.js';
|
||||
import { parseChatUri } from '../../common/state/sessionState.js';
|
||||
@@ -26,6 +26,8 @@ import { AgentHostStateManager, IAgentHostStateManager } from '../agentHostState
|
||||
export interface ISdkMcpServer {
|
||||
/** Server name (used both as the customization name and the channel suffix). */
|
||||
readonly name: string;
|
||||
/** Optional Connector catalog name that does not participate in runtime identity. */
|
||||
readonly displayName?: string;
|
||||
readonly source?: McpServerSource;
|
||||
/** Configuration file URI. Omitted on lifecycle updates; null clears a previously known source. */
|
||||
readonly sourceUri?: string | null;
|
||||
@@ -131,6 +133,7 @@ export interface IMcpCustomizationControllerOptions {
|
||||
|
||||
interface ILiveEntry {
|
||||
readonly serverName: string;
|
||||
readonly displayName?: string;
|
||||
readonly state: McpServerState;
|
||||
readonly enabled: boolean;
|
||||
readonly publishedId: string;
|
||||
@@ -223,7 +226,7 @@ export class McpCustomizationController extends Disposable {
|
||||
if (entry.topLevelId === undefined) {
|
||||
continue;
|
||||
}
|
||||
out.push(this._buildTopLevel(entry.topLevelId, entry.serverName, entry.state, entry.enabled, readMcpServerSource(entry.topLevelCustomization), entry.topLevelCustomization?.uri));
|
||||
out.push(this._buildTopLevel(entry.topLevelId, entry.serverName, entry.displayName, entry.state, entry.enabled, readMcpServerSource(entry.topLevelCustomization), entry.topLevelCustomization?.uri));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -359,6 +362,7 @@ export class McpCustomizationController extends Disposable {
|
||||
const previous = this._live.get().get(server.name);
|
||||
const state = this._stateForUpdate(previous?.state, server.state, server.allowAuthRequiredToStarting === true);
|
||||
const enabled = server.enabled ?? previous?.enabled ?? true;
|
||||
const displayName = server.displayName ?? previous?.displayName;
|
||||
// Once promoted to a top-level entry, stay top-level for the
|
||||
// session — flipping back to a child mid-stream would orphan the
|
||||
// previously-published top-level id.
|
||||
@@ -372,8 +376,8 @@ export class McpCustomizationController extends Disposable {
|
||||
const childContainerId = published?.childContainerId ?? (retainChildIdentity ? previous?.childContainerId : undefined);
|
||||
if (childId !== undefined) {
|
||||
const stateChanged = force || previous?.topLevelId !== undefined || previous?.publishedId !== childId || previous?.childContainerId !== childContainerId || !equals(previous?.state, state);
|
||||
if (stateChanged || previous?.enabled !== enabled) {
|
||||
this._setLiveEntry(server.name, { serverName: server.name, state, enabled, publishedId: childId, topLevelId: undefined, childContainerId }, tx);
|
||||
if (stateChanged || previous?.enabled !== enabled || previous?.displayName !== displayName) {
|
||||
this._setLiveEntry(server.name, { serverName: server.name, displayName, state, enabled, publishedId: childId, topLevelId: undefined, childContainerId }, tx);
|
||||
}
|
||||
if (!stateChanged) {
|
||||
return;
|
||||
@@ -390,10 +394,11 @@ export class McpCustomizationController extends Disposable {
|
||||
}
|
||||
const source = server.source ?? readMcpServerSource(previous?.topLevelCustomization);
|
||||
const sourceUri = server.sourceUri !== undefined ? server.sourceUri : previous?.topLevelCustomization?.uri;
|
||||
const customization = this._buildTopLevel(topLevelId, server.name, state, enabled, source, sourceUri);
|
||||
const customization = this._buildTopLevel(topLevelId, server.name, displayName, state, enabled, source, sourceUri);
|
||||
const resolvedDisplayName = readMcpServerDisplayName(customization);
|
||||
const customizationChanged = force || previous?.topLevelId !== topLevelId || !equals(previous?.topLevelCustomization, customization);
|
||||
if (customizationChanged || previous?.enabled !== enabled) {
|
||||
this._setLiveEntry(server.name, { serverName: server.name, state, enabled, publishedId: topLevelId, topLevelId, topLevelCustomization: customization }, tx);
|
||||
this._setLiveEntry(server.name, { serverName: server.name, displayName: resolvedDisplayName, state, enabled, publishedId: topLevelId, topLevelId, topLevelCustomization: customization }, tx);
|
||||
}
|
||||
if (!customizationChanged) {
|
||||
return;
|
||||
@@ -511,7 +516,7 @@ export class McpCustomizationController extends Disposable {
|
||||
return buildMcpChannel(this._chatUri, serverName);
|
||||
}
|
||||
|
||||
private _buildTopLevel(id: string, serverName: string, state: McpServerState, enabled: boolean, source?: McpServerSource, sourceUri?: string | null): McpServerCustomization {
|
||||
private _buildTopLevel(id: string, serverName: string, displayName: string | undefined, state: McpServerState, enabled: boolean, source?: McpServerSource, sourceUri?: string | null): McpServerCustomization {
|
||||
const channel = this._buildChannel(serverName, state);
|
||||
const owningPluginUri = this.pluginMcpServerSources?.get(serverName);
|
||||
// Per AHP spec, `mcpApp` is a static capability declaration —
|
||||
@@ -525,7 +530,7 @@ export class McpCustomizationController extends Disposable {
|
||||
const existing = getMcpServerCustomizations(this._stateManager.getSessionState(this._sessionUri.toString())?.customizations ?? [])
|
||||
.find(customization => customization.id === id);
|
||||
// `SessionCustomizationUpdated` replaces the whole customization, so keep opaque entries owned by others.
|
||||
const meta = withMcpServerSourceMeta(existing?._meta, source);
|
||||
const meta = withMcpServerDisplayNameMeta(withMcpServerSourceMeta(existing?._meta, source), displayName);
|
||||
const uri = (sourceUri === undefined ? existing?.uri : sourceUri) ?? this._mintTopLevelId(serverName);
|
||||
const customization: McpServerCustomization = {
|
||||
type: CustomizationType.McpServer,
|
||||
|
||||
@@ -5,11 +5,36 @@
|
||||
|
||||
import assert from 'assert';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
||||
import { findRemoteAgentHostSessionTypeAuthority, isRemoteAgentHostSessionType, parseAgentHostHarness, parseRemoteAgentHostHarness, parseRemoteAgentHostSessionTypeAuthority, remoteAgentHostSessionTypeAuthorityPrefix, remoteAgentHostSessionTypeId } from '../../common/agentHostSessionType.js';
|
||||
import { findRemoteAgentHostSessionTypeAuthority, isCopilotAgentHostProvider, isCopilotAgentHostSessionType, isRemoteAgentHostSessionType, parseAgentHostHarness, parseRemoteAgentHostHarness, parseRemoteAgentHostSessionTypeAuthority, remoteAgentHostSessionTypeAuthorityPrefix, remoteAgentHostSessionTypeId } from '../../common/agentHostSessionType.js';
|
||||
|
||||
suite('agentHostSessionType', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('recognizes both Copilot provider IDs without matching other providers', () => {
|
||||
assert.deepStrictEqual(
|
||||
['copilotcli', 'copilot', 'claude', 'codex', 'copilotcloud', undefined].map(isCopilotAgentHostProvider),
|
||||
[true, true, false, false, false, false],
|
||||
);
|
||||
});
|
||||
|
||||
test('recognizes local and remote Copilot session types without changing their schemes', () => {
|
||||
assert.deepStrictEqual([
|
||||
'agent-host-copilotcli',
|
||||
'agent-host-copilot',
|
||||
'remote-dev-box-copilotcli',
|
||||
'remote-cloudsandbox_environment-one-copilot',
|
||||
'agent-host-claude',
|
||||
'remote-dev-box-codex',
|
||||
'copilotcli',
|
||||
'copilot',
|
||||
'copilotcloud',
|
||||
'remote--copilot',
|
||||
'remote-dev-box-copilot-other',
|
||||
].map(isCopilotAgentHostSessionType), [
|
||||
true, true, true, true, false, false, false, false, false, false, false,
|
||||
]);
|
||||
});
|
||||
|
||||
test('remoteAgentHostSessionTypeId pins the wire format', () => {
|
||||
assert.deepStrictEqual([
|
||||
remoteAgentHostSessionTypeId('foo', 'copilot'),
|
||||
|
||||
@@ -9,10 +9,11 @@ import { type AgentFusionPhaseStatus, isPresentationOnlyToolCall, readToolCallMe
|
||||
import { AgentSystemNotificationKind, type AgentFusionProgressStatus, readAgentSystemNotificationMeta, toAgentSystemNotificationMeta } from '../../common/meta/agentSystemNotificationMeta.js';
|
||||
import { readEphemeralSessionMeta, withEphemeralSessionMeta } from '../../common/meta/agentEphemeralSessionMeta.js';
|
||||
import { readSessionSandboxPolicy, withSessionSandboxPolicy } from '../../common/meta/agentSandboxPolicyMeta.js';
|
||||
import { readSlashCommandResource, toSlashCommandResourceMeta } from '../../common/meta/agentSlashCommandOutputMeta.js';
|
||||
import { readChatInputState, withChatInputState } from '../../common/meta/agentHostChatInputState.js';
|
||||
import { createEditorInlineChatInstruction, createTerminalChatInstruction, readChatSurfaceMeta, withChatSurfaceMeta } from '../../common/meta/agentChatSurfaceMeta.js';
|
||||
import { readAgentCustomizationMeta, toAgentCustomizationMeta } from '../../common/meta/agentCustomizationMeta.js';
|
||||
import { readMcpServerSource, withMcpServerSourceMeta } from '../../common/meta/mcpCustomizationMeta.js';
|
||||
import { readMcpServerDisplayName, readMcpServerSource, withMcpServerDisplayNameMeta, withMcpServerSourceMeta } from '../../common/meta/mcpCustomizationMeta.js';
|
||||
import { getCommandArgumentHint, getCompletionAction, readCompletionAttachmentMeta, toCommandCompletionAttachmentMeta, toSkillCompletionAttachmentMeta } from '../../common/meta/agentCompletionAttachmentMeta.js';
|
||||
import { CustomizationType, MessageAttachmentKind, ToolCallStatus, hasReportedUsage, readSessionComparisonMetadata, readUsageInfoMeta, withSessionComparisonMetadata, type AgentCustomization, type ClientPluginCustomization, type ToolCallState, type UsageInfo } from '../../common/state/sessionState.js';
|
||||
import { McpServerStatus, type McpServerCustomization, type SessionModelInfo, type SimpleMessageAttachment } from '../../common/state/protocol/state.js';
|
||||
@@ -40,6 +41,31 @@ suite('Agent host _meta readers', () => {
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('Copilot network restrictions round trip and reject malformed or absent metadata', () => {
|
||||
const restrictions = { sandboxEnabled: true, allowNetwork: false, allowedDomains: ['example.com'], deniedDomains: [] };
|
||||
const key = 'vscode.copilotSandboxNetworkRestrictions';
|
||||
assert.deepStrictEqual([
|
||||
readToolCallMeta(toolCall(toToolCallMeta({ [key]: restrictions })))[key],
|
||||
readToolCallMeta(toolCall(undefined))[key],
|
||||
readToolCallMeta(toolCall({ [key]: { ...restrictions, sandboxEnabled: 'true' } }))[key],
|
||||
readToolCallMeta(toolCall({ [key]: { ...restrictions, deniedDomains: [5] } }))[key],
|
||||
], [restrictions, undefined, undefined, undefined]);
|
||||
});
|
||||
|
||||
test('validates slash command resource metadata', () => {
|
||||
const resource = URI.parse('agenthost-content:///reports/sandbox-policy.md');
|
||||
assert.deepStrictEqual([
|
||||
readSlashCommandResource({ _meta: toSlashCommandResourceMeta(resource, true) }),
|
||||
...[undefined, null, [], {}, { resource: 1, preview: true }, { resource: resource.toString(), preview: 'true' }, { resource: '/reports/policy.md', preview: true }]
|
||||
.map(value => readSlashCommandResource({ _meta: { 'vscode.slashCommandResource': value } })),
|
||||
readSlashCommandResource({}),
|
||||
].map(value => value ? { resource: value.resource.toString(), preview: value.preview } : undefined), [
|
||||
{ resource: resource.toString(), preview: true },
|
||||
undefined, undefined, undefined, undefined, undefined, undefined, undefined,
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
|
||||
suite('session sandbox policy', () => {
|
||||
const policies = [
|
||||
undefined,
|
||||
@@ -111,8 +137,8 @@ suite('Agent host _meta readers', () => {
|
||||
})?.role, 'judge');
|
||||
});
|
||||
|
||||
test('validates MCP configuration sources and merges them into open metadata', () => {
|
||||
const read = (meta: Record<string, unknown> | undefined) => readMcpServerSource({
|
||||
test('validates MCP presentation metadata and preserves opaque entries', () => {
|
||||
const server = (meta: Record<string, unknown> | undefined) => ({
|
||||
type: CustomizationType.McpServer,
|
||||
id: 'server',
|
||||
uri: 'mcp-top-level:server',
|
||||
@@ -121,18 +147,21 @@ suite('Agent host _meta readers', () => {
|
||||
_meta: meta,
|
||||
} satisfies McpServerCustomization);
|
||||
const opaque = { 'test.opaque': 'kept' };
|
||||
const merged = withMcpServerDisplayNameMeta(withMcpServerSourceMeta(opaque, 'user'), ' Mail ');
|
||||
|
||||
assert.deepStrictEqual({
|
||||
sources: [
|
||||
...(['user', 'workspace', 'plugin', 'builtin', 'managed'] as const).map(source => read(withMcpServerSourceMeta(undefined, source))),
|
||||
...[undefined, 'unknown', 1, {}, ['user']].map(source => read({ 'agentHost.mcpServerSource': source })),
|
||||
read(undefined),
|
||||
...(['user', 'workspace', 'plugin', 'builtin', 'managed'] as const).map(source => readMcpServerSource(server(withMcpServerSourceMeta(undefined, source)))),
|
||||
...[undefined, 'unknown', 1, {}, ['user']].map(source => readMcpServerSource(server({ 'agentHost.mcpServerSource': source }))),
|
||||
readMcpServerSource(server(undefined)),
|
||||
],
|
||||
replaced: withMcpServerSourceMeta(withMcpServerSourceMeta(opaque, 'user'), 'workspace'),
|
||||
displayNames: [' Mail ', '', 'x'.repeat(513), 1].map(displayName => readMcpServerDisplayName(server({ 'vscode.mcpServerDisplayName': displayName }))),
|
||||
merged,
|
||||
unchanged: withMcpServerSourceMeta(opaque, undefined) === opaque,
|
||||
}, {
|
||||
sources: ['user', 'workspace', 'plugin', 'builtin', 'managed', undefined, undefined, undefined, undefined, undefined, undefined],
|
||||
replaced: { 'test.opaque': 'kept', 'agentHost.mcpServerSource': 'workspace' },
|
||||
displayNames: ['Mail', undefined, undefined, undefined],
|
||||
merged: { 'test.opaque': 'kept', 'agentHost.mcpServerSource': 'user', 'vscode.mcpServerDisplayName': 'Mail' },
|
||||
unchanged: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
||||
import { readCloudSandboxCloneResult, readCloudSandboxProjects } from '../../common/meta/cloudSandboxProjectMeta.js';
|
||||
|
||||
suite('CloudSandboxProjectMeta', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('reads finite clone percentages, clamps their range, and ignores progress after cloning', () => {
|
||||
const project = { id: 'checkout', path: '/checkout', git: true };
|
||||
assert.deepStrictEqual({
|
||||
cloning: [undefined, 0, 58, 100, -1, 101, NaN, Infinity, '58'].map(progress =>
|
||||
readCloudSandboxCloneResult({ project: { ...project, status: 'cloning', progress } })?.progress),
|
||||
otherStates: [undefined, 'ready', 'failed'].map(status =>
|
||||
readCloudSandboxCloneResult({ project: { ...project, status, progress: 58 } })?.progress),
|
||||
root: readCloudSandboxProjects({
|
||||
agents: [],
|
||||
_meta: { 'copilot.projectManagement': { available: true } },
|
||||
config: {
|
||||
schema: { type: 'object', properties: {} },
|
||||
values: { copilot: { projects: [{ ...project, status: 'cloning', progress: 58 }] } },
|
||||
},
|
||||
})?.map(project => ({ status: project.status, progress: project.progress })),
|
||||
}, {
|
||||
cloning: [undefined, 0, 58, 100, 0, 100, undefined, undefined, undefined],
|
||||
otherStates: [undefined, undefined, undefined],
|
||||
root: [{ status: 'cloning', progress: 58 }],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,11 +11,7 @@ import { parseTaskEventsResponse, replayTaskAhpEvents, TaskEventReplayError } fr
|
||||
const SESSION_A = 'ahp-session:/aaaaaaaa-0000-4000-8000-000000000001';
|
||||
const SESSION_B = 'ahp-session:/bbbbbbbb-0000-4000-8000-000000000002';
|
||||
|
||||
/**
|
||||
* The default chat channel as Mission Control's recorded frames name it. Deliberately NOT
|
||||
* `buildDefaultChatUri`: the host writes `<session>/chat`, while a client builds
|
||||
* `ahp-chat://default/<base64>` for the same chat. Replay must accept what was recorded.
|
||||
*/
|
||||
/** The legacy default chat convention; replay must also accept arbitrary host-provided channels. */
|
||||
function defaultChat(sessionId: string): string {
|
||||
return `${sessionId}/chat`;
|
||||
}
|
||||
@@ -181,6 +177,86 @@ suite('Task event replay', () => {
|
||||
['t1', 't2', 't3']);
|
||||
});
|
||||
|
||||
test('ignores overlapping batches and interleaved duplicate deltas', () => {
|
||||
const chat = defaultChat(SESSION_A);
|
||||
const events = [
|
||||
event(SESSION_A, 1926, chat, turnStarted('t1', 'hello')),
|
||||
event(SESSION_A, 1927, chat, { type: 'chat/responsePart', turnId: 't1', part: { kind: 'markdown', id: 'p1', content: '' } }),
|
||||
event(SESSION_A, 1928, chat, { type: 'chat/delta', turnId: 't1', partId: 'p1', content: 'first ' }),
|
||||
event(SESSION_A, 1929, chat, { type: 'chat/delta', turnId: 't1', partId: 'p1', content: 'second ' }),
|
||||
event(SESSION_A, 1930, chat, { type: 'chat/delta', turnId: 't1', partId: 'p1', content: 'third' }),
|
||||
event(SESSION_A, 1931, chat, turnComplete('t1')),
|
||||
];
|
||||
const history = replayTaskAhpEvents([
|
||||
...events.slice(0, 4),
|
||||
...structuredClone(events.slice(2, 4)),
|
||||
events[4],
|
||||
structuredClone(events[2]),
|
||||
events[5],
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
truncated: history?.truncated,
|
||||
turns: history?.sessions[0].chats.get(chat)?.turns.map(t => ({ id: t.id, responseParts: t.responseParts })),
|
||||
},
|
||||
{ truncated: false, turns: [{ id: 't1', responseParts: [{ kind: 'markdown', id: 'p1', content: 'first second third' }] }] });
|
||||
});
|
||||
|
||||
test('ignores duplicate chunks without disrupting an in-flight group', () => {
|
||||
const chat = defaultChat(SESSION_A);
|
||||
const chunks = chunkedEvents(SESSION_A, 0, chat, turnStarted('t1', 'chunked'), 3);
|
||||
const history = replayTaskAhpEvents([
|
||||
chunks[0],
|
||||
structuredClone(chunks[0]),
|
||||
chunks[1],
|
||||
structuredClone(chunks[0]),
|
||||
chunks[2],
|
||||
...structuredClone(chunks),
|
||||
event(SESSION_A, 3, chat, turnComplete('t1')),
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
{ truncated: history?.truncated, turns: history?.sessions[0].chats.get(chat)?.turns.map(t => t.id) },
|
||||
{ truncated: false, turns: ['t1'] });
|
||||
});
|
||||
|
||||
test('does not rewind modifiedAt when skipping an older duplicate', () => {
|
||||
const first = event(SESSION_A, 2, SESSION_A, titleChanged('first'));
|
||||
const later = { ...event(SESSION_A, 3, SESSION_A, titleChanged('later')), at: '2026-08-04T13:00:00.000Z' };
|
||||
const history = replayTaskAhpEvents([first, later, structuredClone(first)]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
history?.sessions.map(s => ({ title: s.state.title, modifiedAt: s.modifiedAt })),
|
||||
[{ title: 'later', modifiedAt: later.at }]);
|
||||
});
|
||||
|
||||
test('clears duplicate tracking at a mirror restart', () => {
|
||||
const repeated = event(SESSION_A, 2, SESSION_A, titleChanged('same in both epochs'));
|
||||
const history = replayTaskAhpEvents([
|
||||
event(SESSION_A, 1, SESSION_A, titleChanged('first epoch')),
|
||||
repeated,
|
||||
event(SESSION_A, 3, SESSION_A, titleChanged('before restart')),
|
||||
event(SESSION_A, 1, SESSION_A, titleChanged('second epoch')),
|
||||
structuredClone(repeated),
|
||||
event(SESSION_A, 3, SESSION_A, titleChanged('after restart')),
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
{ truncated: history?.truncated, title: history?.sessions[0].state.title },
|
||||
{ truncated: false, title: 'after restart' });
|
||||
});
|
||||
|
||||
test('rejects conflicting records instead of treating them as duplicates', () => {
|
||||
const first = event(SESSION_A, 2, SESSION_A, titleChanged('first'));
|
||||
for (const conflicting of [
|
||||
event(SESSION_A, 2, SESSION_A, titleChanged('different payload')),
|
||||
{ ...first, at: '2026-08-04T13:00:00.000Z' },
|
||||
]) {
|
||||
assert.throws(() => replayTaskAhpEvents([first, conflicting]), TaskEventReplayError);
|
||||
}
|
||||
});
|
||||
|
||||
test('reports truncation from an epoch whose reassembler was replaced by a restart', () => {
|
||||
// The restart installs a fresh reassembler, so the incomplete group from the first epoch is
|
||||
// no longer buffered — the loss has to be remembered or the transcript reads as whole.
|
||||
@@ -239,6 +315,142 @@ suite('Task event replay', () => {
|
||||
{ defaultChat: announced, turns: ['t1'] });
|
||||
});
|
||||
|
||||
test('uses the only recorded chat when the default was not announced', () => {
|
||||
const chat = 'ahp-chat:/90b9344c160a544093d7a3ebf4089e3f';
|
||||
const history = replayTaskAhpEvents(completedTurn(SESSION_A, 0, chat, 't1', 'hello'));
|
||||
const session = history?.sessions[0];
|
||||
|
||||
assert.deepStrictEqual({
|
||||
defaultChat: session?.defaultChat,
|
||||
chats: [...(session?.chats.keys() ?? [])],
|
||||
turns: session?.chats.get(session.defaultChat)?.turns.map(turn => turn.id),
|
||||
}, { defaultChat: chat, chats: [chat], turns: ['t1'] });
|
||||
});
|
||||
|
||||
for (const origin of [
|
||||
{ kind: 'tool', chat: 'ahp-chat:/main', toolCallId: 'tool-1' },
|
||||
{ kind: 'sideChat', chat: 'ahp-chat:/main', turnId: 't0' },
|
||||
{ kind: 'fork', chat: 'ahp-chat:/main', turnId: 't0' },
|
||||
]) {
|
||||
test(`does not infer the default from a sole recorded ${origin.kind} chat`, () => {
|
||||
const peer = 'ahp-chat:/peer';
|
||||
const history = replayTaskAhpEvents([
|
||||
event(SESSION_A, 0, SESSION_A, {
|
||||
type: 'session/chatAdded',
|
||||
summary: { resource: peer, title: '', status: 1, modifiedAt: '2026-08-04T12:00:00.000Z', origin },
|
||||
}),
|
||||
...completedTurn(SESSION_A, 1, peer, 't1', 'peer conversation'),
|
||||
]);
|
||||
const session = history?.sessions[0];
|
||||
|
||||
assert.deepStrictEqual({
|
||||
defaultChat: session?.defaultChat,
|
||||
turns: session?.chats.get(session.defaultChat)?.turns,
|
||||
peerTurns: session?.chats.get(peer)?.turns.map(turn => turn.id),
|
||||
}, { defaultChat: defaultChat(SESSION_A), turns: [], peerTurns: ['t1'] });
|
||||
});
|
||||
}
|
||||
|
||||
test('does not infer a default when the catalogue advertises another chat', () => {
|
||||
const history = replayTaskAhpEvents([
|
||||
event(SESSION_A, 0, SESSION_A, {
|
||||
type: 'session/chatAdded',
|
||||
summary: { resource: 'ahp-chat:/main', title: '', status: 1, modifiedAt: '2026-08-04T12:00:00.000Z', origin: { kind: 'user' } },
|
||||
}),
|
||||
...completedTurn(SESSION_A, 1, 'ahp-chat:/peer', 't1', 'peer conversation'),
|
||||
]);
|
||||
const session = history?.sessions[0];
|
||||
|
||||
assert.deepStrictEqual({
|
||||
defaultChat: session?.defaultChat,
|
||||
turns: session?.chats.get(session.defaultChat)?.turns,
|
||||
}, { defaultChat: defaultChat(SESSION_A), turns: [] });
|
||||
});
|
||||
|
||||
for (const kind of ['user', 'tool', 'sideChat', 'fork']) {
|
||||
test(`does not infer the default from a removed ${kind} chat`, () => {
|
||||
const chat = 'ahp-chat:/removed';
|
||||
const history = replayTaskAhpEvents([
|
||||
event(SESSION_A, 0, SESSION_A, {
|
||||
type: 'session/chatAdded',
|
||||
summary: {
|
||||
resource: chat, title: '', status: 1, modifiedAt: '2026-08-04T12:00:00.000Z',
|
||||
origin: { kind, chat: 'ahp-chat:/main', turnId: 't0', toolCallId: 'tool-1' },
|
||||
},
|
||||
}),
|
||||
...completedTurn(SESSION_A, 1, chat, 't1', 'removed conversation'),
|
||||
event(SESSION_A, 3, SESSION_A, { type: 'session/chatRemoved', chat }),
|
||||
]);
|
||||
const session = history?.sessions[0];
|
||||
|
||||
assert.deepStrictEqual({
|
||||
catalogue: session?.state.chats,
|
||||
defaultChat: session?.defaultChat,
|
||||
turns: session?.chats.get(session.defaultChat)?.turns,
|
||||
}, { catalogue: [], defaultChat: defaultChat(SESSION_A), turns: [] });
|
||||
});
|
||||
}
|
||||
|
||||
test('honours a chat removal even when its addition was not recorded', () => {
|
||||
const chat = 'ahp-chat:/removed';
|
||||
const history = replayTaskAhpEvents([
|
||||
...completedTurn(SESSION_A, 0, chat, 't1', 'removed conversation'),
|
||||
event(SESSION_A, 2, SESSION_A, { type: 'session/chatRemoved', chat }),
|
||||
]);
|
||||
const session = history?.sessions[0];
|
||||
|
||||
assert.deepStrictEqual({
|
||||
defaultChat: session?.defaultChat,
|
||||
turns: session?.chats.get(session.defaultChat)?.turns,
|
||||
}, { defaultChat: defaultChat(SESSION_A), turns: [] });
|
||||
});
|
||||
|
||||
test('uses the sole recorded user chat advertised in the catalogue', () => {
|
||||
const chat = 'ahp-chat:/main';
|
||||
const history = replayTaskAhpEvents([
|
||||
event(SESSION_A, 0, SESSION_A, {
|
||||
type: 'session/chatAdded',
|
||||
summary: { resource: chat, title: '', status: 1, modifiedAt: '2026-08-04T12:00:00.000Z', origin: { kind: 'user' } },
|
||||
}),
|
||||
...completedTurn(SESSION_A, 1, chat, 't1', 'main conversation'),
|
||||
]);
|
||||
const session = history?.sessions[0];
|
||||
|
||||
assert.deepStrictEqual({
|
||||
defaultChat: session?.defaultChat,
|
||||
turns: session?.chats.get(session.defaultChat)?.turns.map(turn => turn.id),
|
||||
}, { defaultChat: chat, turns: ['t1'] });
|
||||
});
|
||||
|
||||
test('does not replace an announced empty default with a peer chat', () => {
|
||||
const announced = 'ahp-chat:/main';
|
||||
const peer = 'ahp-chat:/peer';
|
||||
const history = replayTaskAhpEvents([
|
||||
event(SESSION_A, 0, SESSION_A, { type: 'session/defaultChatChanged', defaultChat: announced }),
|
||||
...completedTurn(SESSION_A, 1, peer, 't1', 'peer conversation'),
|
||||
]);
|
||||
const session = history?.sessions[0];
|
||||
|
||||
assert.deepStrictEqual({
|
||||
defaultChat: session?.defaultChat,
|
||||
turns: session?.chats.get(session.defaultChat)?.turns,
|
||||
peerTurns: session?.chats.get(peer)?.turns.map(turn => turn.id),
|
||||
}, { defaultChat: announced, turns: [], peerTurns: ['t1'] });
|
||||
});
|
||||
|
||||
test('preserves separate chats when the recorded default is ambiguous', () => {
|
||||
const history = replayTaskAhpEvents([
|
||||
...completedTurn(SESSION_A, 0, 'ahp-chat:/first', 't1', 'first conversation'),
|
||||
...completedTurn(SESSION_A, 2, 'ahp-chat:/second', 't2', 'second conversation'),
|
||||
]);
|
||||
const session = history?.sessions[0];
|
||||
|
||||
assert.deepStrictEqual({
|
||||
defaultChat: session?.defaultChat,
|
||||
turns: [...(session?.chats.values() ?? [])].map(chat => chat.turns.map(turn => turn.id)),
|
||||
}, { defaultChat: defaultChat(SESSION_A), turns: [['t1'], ['t2'], []] });
|
||||
});
|
||||
|
||||
test('surfaces an empty default chat for a session with no chat history', () => {
|
||||
const history = replayTaskAhpEvents([event(SESSION_A, 0, SESSION_A, titleChanged('no chat yet'))]);
|
||||
|
||||
|
||||
@@ -713,7 +713,7 @@ suite('AgentHostProtocolClient', () => {
|
||||
workingDirectories: [URI.file('/home/user/.copilot/chats/quick-1').toString()],
|
||||
chats: [
|
||||
{ resource: 'agent-chat://copilotcli/quick-1/default', title: 'Quick Chat' },
|
||||
{ resource: 'agent-chat://copilotcli/quick-1/peer', title: 'Peer Chat', archived: true, interactivity: ChatInteractivity.Hidden },
|
||||
{ resource: 'agent-chat://copilotcli/quick-1/peer', title: 'Peer Chat', status: SessionStatus.Idle | SessionStatus.IsArchived, interactivity: ChatInteractivity.Hidden },
|
||||
],
|
||||
defaultChat: 'agent-chat://copilotcli/quick-1/default',
|
||||
_meta: withSessionWorkspaceless(undefined, true),
|
||||
@@ -733,7 +733,7 @@ suite('AgentHostProtocolClient', () => {
|
||||
workingDirectories: [toAgentHostUri(URI.file('/home/user/.copilot/chats/quick-1'), agentHostAuthority('test.example:1234'))],
|
||||
chats: [
|
||||
{ chat: 'agent-chat://copilotcli/quick-1/default', summary: 'Quick Chat', kind: 'default', origin: undefined },
|
||||
{ chat: 'agent-chat://copilotcli/quick-1/peer', summary: 'Peer Chat', kind: 'peer', origin: undefined, interactivity: ChatInteractivity.Hidden, archived: true },
|
||||
{ chat: 'agent-chat://copilotcli/quick-1/peer', summary: 'Peer Chat', kind: 'peer', origin: undefined, interactivity: ChatInteractivity.Hidden, archived: true, isRead: false },
|
||||
],
|
||||
}]);
|
||||
});
|
||||
|
||||
@@ -97,8 +97,8 @@ suite('AgentHostCatalogListReader', () => {
|
||||
[SESSION_META_EHCLI_ADOPTABLE_KEY]: true,
|
||||
},
|
||||
chats: [
|
||||
{ uri: `${session.toString()}/chat/default`, order: 0, kind: 'default', summary: 'Catalog title', titleSource: 'user' },
|
||||
{ uri: `${session.toString()}/chat/peer`, order: 1, kind: 'peer', summary: 'Peer title', titleSource: 'agent', origin: { kind: 'fork', chat: `${session.toString()}/chat/default`, turnId: 'turn-1' }, interactivity: ChatInteractivity.Hidden, archived: true },
|
||||
{ uri: `${session.toString()}/chat/default`, order: 0, kind: 'default', summary: 'Catalog title', titleSource: 'user', isRead: true },
|
||||
{ uri: `${session.toString()}/chat/peer`, order: 1, kind: 'peer', summary: 'Peer title', titleSource: 'agent', origin: { kind: 'fork', chat: `${session.toString()}/chat/default`, turnId: 'turn-1' }, interactivity: ChatInteractivity.Hidden, archived: true, isRead: false },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -191,6 +191,7 @@ suite('AgentHostCatalogListReader', () => {
|
||||
origin: chat.origin,
|
||||
...(chat.interactivity !== undefined ? { interactivity: chat.interactivity } : {}),
|
||||
...(chat.archived === true ? { archived: true } : {}),
|
||||
...(chat.isRead !== undefined ? { isRead: chat.isRead } : {}),
|
||||
})),
|
||||
catalogChats: data.chats,
|
||||
});
|
||||
|
||||
@@ -146,7 +146,7 @@ suite('AgentHostCatalogProjection', () => {
|
||||
ok: true,
|
||||
value: { data: typedData, payload: encoded.payload },
|
||||
},
|
||||
payload: '{"data":{"_meta":{"agentHost/createdBySession":{"chat":"agent-chat://test/parent/default","session":"agent-session://test/parent","turnId":"turn-1"},"agentHost/sessionArtifacts":[{"id":"artifact-1","isArtifact":true,"label":"Catalog payload","link":"https://github.com/microsoft/vscode/pull/1","type":"pullRequest"}],"ehcliAdoptable":true,"ehcliAdopted":true,"git":{"branchName":"feature/catalog","hasGitHubRemote":true,"incomingChanges":2},"github":{"issueUrls":["https://github.com/microsoft/vscode/issues/2"],"owner":"microsoft","pullRequestUrls":["https://github.com/microsoft/vscode/pull/1"],"repo":"vscode"},"multiRoot":{"workspaceFile":"file:///workspace/project.code-workspace"},"vscode.folderPicker":{"hidden":true,"primary":"file:///workspace"},"vscode.sourceControl":{"latestOutcome":"merge","merge":{"commit":"0123456789abcdef"}},"workspaceless":true},"changes":{"additions":12,"deletions":4,"files":2},"chats":[{"kind":"default","order":0,"origin":{"kind":"default","metadata":{"a":1,"b":2}},"summary":"Main","titleSource":"auto","uri":"agent-chat://test/session/default"},{"archived":true,"kind":"peer","order":1,"origin":{"kind":"subagent"},"summary":"Peer","titleSource":"agent","uri":"agent-chat://test/session/peer"}],"isArchived":false,"isChatBacking":false,"isRead":true,"modifiedTime":1720000000000,"project":{"displayName":"workspace","uri":"file:///workspace"},"summary":"Implement opaque catalog payload","titleSource":"user","workingDirectories":["file:///workspace","file:///workspace/secondary"]},"payloadVersion":1}',
|
||||
payload: '{"data":{"_meta":{"agentHost/createdBySession":{"chat":"agent-chat://test/parent/default","session":"agent-session://test/parent","turnId":"turn-1"},"agentHost/sessionArtifacts":[{"id":"artifact-1","isArtifact":true,"label":"Catalog payload","link":"https://github.com/microsoft/vscode/pull/1","type":"pullRequest"}],"ehcliAdoptable":true,"ehcliAdopted":true,"git":{"branchName":"feature/catalog","hasGitHubRemote":true,"incomingChanges":2},"github":{"issueUrls":["https://github.com/microsoft/vscode/issues/2"],"owner":"microsoft","pullRequestUrls":["https://github.com/microsoft/vscode/pull/1"],"repo":"vscode"},"multiRoot":{"workspaceFile":"file:///workspace/project.code-workspace"},"vscode.folderPicker":{"hidden":true,"primary":"file:///workspace"},"vscode.sourceControl":{"latestOutcome":"merge","merge":{"commit":"0123456789abcdef"}},"workspaceless":true},"changes":{"additions":12,"deletions":4,"files":2},"chats":[{"kind":"default","order":0,"origin":{"kind":"default","metadata":{"a":1,"b":2}},"summary":"Main","titleSource":"auto","uri":"agent-chat://test/session/default"},{"archived":true,"kind":"peer","order":1,"origin":{"kind":"subagent"},"summary":"Peer","titleSource":"agent","uri":"agent-chat://test/session/peer"}],"isArchived":false,"isChatBacking":false,"isRead":true,"modifiedTime":1720000000000,"project":{"displayName":"workspace","uri":"file:///workspace"},"summary":"Implement opaque catalog payload","titleSource":"user","workingDirectories":["file:///workspace","file:///workspace/secondary"]},"payloadVersion":2}',
|
||||
hash: hashAgentHostCatalogPayload(encoded.payload),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import { readRemoteSessionOrigin, REMOTE_SESSION_ORIGIN_METADATA_KEY, withRemote
|
||||
import { readSessionInitiator, SESSION_INITIATOR_METADATA_KEY, withSessionInitiator } from '../../common/meta/agentSessionInitiatorMeta.js';
|
||||
import { parseSessionArtifacts, SessionArtifactType, SESSION_META_ARTIFACTS_KEY, withSessionArtifacts } from '../../common/sessionArtifacts.js';
|
||||
import { ChatInteractivity, ChatOriginKind } from '../../common/state/protocol/state.js';
|
||||
import { AH_META_CREATED_BY_SESSION_DB_KEY, AH_META_EHCLI_ADOPTED_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_READ_DB_KEY, AH_META_WORKSPACELESS_DB_KEY, SESSION_META_CREATED_BY_SESSION_KEY, SESSION_META_EHCLI_ADOPTABLE_KEY, SESSION_META_EHCLI_ADOPTED_KEY, SESSION_META_FOLDER_PICKER_KEY, SESSION_META_GIT_DATA_KEY, SESSION_META_GIT_KEY, SESSION_META_GITHUB_DATA_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, SESSION_META_WORKSPACELESS_KEY, SessionSourceControlOutcome, SessionStatus, withSessionCreationReference, withSessionEhcliAdoptable, withSessionFolderPickerDecision, withSessionGitHubState, withSessionGitState, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionWorkspaceless } from '../../common/state/sessionState.js';
|
||||
import { AH_META_CREATED_BY_SESSION_DB_KEY, AH_META_DEFAULT_CHAT_IS_READ_DB_KEY, AH_META_EHCLI_ADOPTED_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_READ_DB_KEY, AH_META_WORKSPACELESS_DB_KEY, SESSION_META_CREATED_BY_SESSION_KEY, SESSION_META_EHCLI_ADOPTABLE_KEY, SESSION_META_EHCLI_ADOPTED_KEY, SESSION_META_FOLDER_PICKER_KEY, SESSION_META_GIT_DATA_KEY, SESSION_META_GIT_KEY, SESSION_META_GITHUB_DATA_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, SESSION_META_WORKSPACELESS_KEY, SessionSourceControlOutcome, SessionStatus, withSessionCreationReference, withSessionEhcliAdoptable, withSessionFolderPickerDecision, withSessionGitHubState, withSessionGitState, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionWorkspaceless } from '../../common/state/sessionState.js';
|
||||
import { AGENT_HOST_CATALOG_TITLE_LENGTH_LIMIT, encodeAgentHostCatalogPayload } from '../../node/agentHostCatalogProjection.js';
|
||||
import { AgentHostCatalogSourceResolver, CHAT_BACKING_METADATA_KEY, ICatalogSourceState } from '../../node/agentHostCatalogSourceResolver.js';
|
||||
import { customChatTitleMetadataKey, customChatTitleSourceMetadataKey, SESSION_ARTIFACTS_KEY, SESSION_CUSTOM_TITLE_KEY, SESSION_CUSTOM_TITLE_SOURCE_KEY } from '../../node/shared/persistSessionMetadata.js';
|
||||
@@ -176,6 +176,68 @@ suite('AgentHostCatalogSourceResolver', () => {
|
||||
}, { interactivity: [ChatInteractivity.Full, ChatInteractivity.ReadOnly], archived: [true, undefined], input: undefined });
|
||||
});
|
||||
|
||||
test('uses persisted default chat read state only during persistence-first reconciliation', async () => {
|
||||
const state: ICatalogSourceState = {
|
||||
...sourceState(),
|
||||
chats: [
|
||||
{ ...sourceState().chats[0], isRead: true },
|
||||
{ uri: `${chat}/peer`, kind: 'peer', isRead: false },
|
||||
],
|
||||
};
|
||||
const resolver = createResolver({
|
||||
[AH_META_IS_READ_DB_KEY]: 'true',
|
||||
[AH_META_DEFAULT_CHAT_IS_READ_DB_KEY]: '',
|
||||
});
|
||||
|
||||
const persisted = await resolver.buildCatalogSyncRequest(session, state, {}, true);
|
||||
const live = await resolver.buildCatalogSyncRequest(session, state, {}, false);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
persistedSession: persisted.data.isRead,
|
||||
liveSession: live.data.isRead,
|
||||
persisted: persisted.data.chats.map(entry => entry.isRead),
|
||||
live: live.data.chats.map(entry => entry.isRead),
|
||||
}, {
|
||||
persistedSession: false,
|
||||
liveSession: false,
|
||||
persisted: [false, false],
|
||||
live: [true, false],
|
||||
});
|
||||
});
|
||||
|
||||
test('excludes unread tool chats from session read reconciliation', async () => {
|
||||
const state: ICatalogSourceState = {
|
||||
...sourceState(),
|
||||
status: SessionStatus.IsRead,
|
||||
chats: [
|
||||
{ ...sourceState().chats[0], isRead: true },
|
||||
{
|
||||
uri: `${chat}/tool`,
|
||||
kind: 'peer',
|
||||
isRead: false,
|
||||
origin: { kind: ChatOriginKind.Tool, chat, toolCallId: 'tool-call' },
|
||||
},
|
||||
],
|
||||
};
|
||||
const resolver = createResolver({
|
||||
[AH_META_IS_READ_DB_KEY]: 'true',
|
||||
[AH_META_DEFAULT_CHAT_IS_READ_DB_KEY]: 'true',
|
||||
});
|
||||
|
||||
const persisted = await resolver.buildCatalogSyncRequest(session, state, {}, true);
|
||||
const live = await resolver.buildCatalogSyncRequest(session, state, {}, false);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
persistedSession: persisted.data.isRead,
|
||||
liveSession: live.data.isRead,
|
||||
chats: live.data.chats.map(entry => entry.isRead),
|
||||
}, {
|
||||
persistedSession: true,
|
||||
liveSession: true,
|
||||
chats: [true, false],
|
||||
});
|
||||
});
|
||||
|
||||
test('projects remote origins from live and persisted state without losing exact chat or depth', async () => {
|
||||
const live = { session: 'remote-host-copilotcli:/parent', chat: 'remote-host-copilotcli:/parent#peer', depth: 2 };
|
||||
const persisted = { session: 'agent-host-copilotcli:/origin', chat: 'agent-host-copilotcli:/origin#original', depth: 3 };
|
||||
|
||||
@@ -14,6 +14,7 @@ import { join } from '../../../../base/common/path.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
||||
import { AgentHostDatabase, IAgentHostDatabase, IAgentHostDatabaseSessionV2Envelope } from '../../node/agentHostDatabase.js';
|
||||
import { AGENT_HOST_CATALOG_PAYLOAD_VERSION } from '../../node/agentHostCatalogProjection.js';
|
||||
|
||||
function openDatabase(path: string): Promise<Database> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -39,7 +40,7 @@ function close(database: Database): Promise<void> {
|
||||
|
||||
function createPayload(session: string, sourceRevision: number, isChatBacking = false): string {
|
||||
return stableStringify({
|
||||
payloadVersion: 1,
|
||||
payloadVersion: AGENT_HOST_CATALOG_PAYLOAD_VERSION,
|
||||
data: {
|
||||
modifiedTime: 100 + sourceRevision,
|
||||
summary: `Title ${sourceRevision}`,
|
||||
@@ -69,7 +70,7 @@ function createEnvelope(
|
||||
session,
|
||||
sessionGeneration,
|
||||
sourceRevision,
|
||||
payloadVersion: 1,
|
||||
payloadVersion: AGENT_HOST_CATALOG_PAYLOAD_VERSION,
|
||||
payloadHash: createHash('sha256').update(payload, 'utf8').digest('hex'),
|
||||
verified: true,
|
||||
payload,
|
||||
@@ -235,7 +236,7 @@ suite('AgentHostDatabase sessions_v2', () => {
|
||||
sessionV2ForeignKeys,
|
||||
sessionChatColumns: sessionChatColumns.map(row => row.name),
|
||||
}, {
|
||||
version: [{ user_version: 12 }],
|
||||
version: [{ user_version: 13 }],
|
||||
tables: ['metadata', 'session_chat_catalogs', 'session_chats', 'sessions', 'sessions_v2'],
|
||||
sessionColumns: ['session_uri', 'provider', 'start_time', 'external', 'registration_source', 'modified_time'],
|
||||
sessionV2Columns: [
|
||||
@@ -243,7 +244,7 @@ suite('AgentHostDatabase sessions_v2', () => {
|
||||
'source_revision', 'payload_version', 'payload_hash', 'verified', 'payload', 'is_chat_backing', 'modified_time',
|
||||
],
|
||||
sessionV2ForeignKeys: [],
|
||||
sessionChatColumns: ['session_uri', 'chat_uri', 'chat_order', 'provider_data', 'origin', 'inherited_turn_id', 'archived'],
|
||||
sessionChatColumns: ['session_uri', 'chat_uri', 'chat_order', 'provider_data', 'origin', 'inherited_turn_id', 'archived', 'is_read'],
|
||||
});
|
||||
|
||||
} finally {
|
||||
@@ -262,7 +263,7 @@ suite('AgentHostDatabase sessions_v2', () => {
|
||||
|
||||
const before = await database.getSessionChatCatalog(session);
|
||||
const firstResult = await database.replaceSessionChatCatalog(session, [
|
||||
{ chat: 'ahp-chat://first', order: 0, providerData: 'first', origin: '{"kind":"user"}' },
|
||||
{ chat: 'ahp-chat://first', order: 0, isRead: false, providerData: 'first', origin: '{"kind":"user"}' },
|
||||
{ chat: 'ahp-chat://second', order: 1, inheritedTurnId: 'turn-1' },
|
||||
], undefined);
|
||||
if (firstResult.status !== 'applied') {
|
||||
@@ -302,7 +303,7 @@ suite('AgentHostDatabase sessions_v2', () => {
|
||||
revision: 1,
|
||||
legacyMirroredRevision: 0,
|
||||
chats: [
|
||||
{ chat: 'ahp-chat://first', order: 0, providerData: 'first', origin: '{"kind":"user"}' },
|
||||
{ chat: 'ahp-chat://first', order: 0, isRead: false, providerData: 'first', origin: '{"kind":"user"}' },
|
||||
{ chat: 'ahp-chat://second', order: 1, inheritedTurnId: 'turn-1' },
|
||||
],
|
||||
},
|
||||
@@ -534,7 +535,7 @@ suite('AgentHostDatabase sessions_v2', () => {
|
||||
directCurrent: await upgraded.getSessionV2Registration(direct),
|
||||
}, {
|
||||
version,
|
||||
schemaVersion: [{ user_version: 12 }],
|
||||
schemaVersion: [{ user_version: 13 }],
|
||||
foreignKeys: [],
|
||||
published: undefined,
|
||||
directLegacy: undefined,
|
||||
@@ -581,7 +582,7 @@ suite('AgentHostDatabase sessions_v2', () => {
|
||||
version: await all(rawDatabase, 'PRAGMA user_version'),
|
||||
tables: (await all(rawDatabase, `SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name`)).map(row => row.name),
|
||||
}, {
|
||||
version: [{ user_version: 12 }],
|
||||
version: [{ user_version: 13 }],
|
||||
tables: ['metadata', 'session_chat_catalogs', 'session_chats', 'sessions', 'sessions_v2'],
|
||||
});
|
||||
} finally {
|
||||
@@ -674,7 +675,45 @@ suite('AgentHostDatabase sessions_v2', () => {
|
||||
legacyMirroredRevision: 0,
|
||||
chats: [{ chat: 'ahp-chat://peer', order: 0, providerData: 'peer' }],
|
||||
},
|
||||
version: [{ user_version: 12 }],
|
||||
version: [{ user_version: 13 }],
|
||||
});
|
||||
});
|
||||
|
||||
test('repairs missing chat columns without lowering a future schema version', async () => {
|
||||
const path = join(temporaryDirectory!, 'agent-host-future-v17.db');
|
||||
database = new AgentHostDatabase(path);
|
||||
await database.registerSessionV2('session://future-v17', { provider: 'copilot', startTime: 1, source: 'explicit' }, { checkTombstone: false });
|
||||
await database.replaceSessionChatCatalog('session://future-v17', [
|
||||
{ chat: 'ahp-chat://peer', order: 0, providerData: 'peer' },
|
||||
], undefined);
|
||||
await database.close();
|
||||
database = undefined;
|
||||
|
||||
const futureDatabase = await openDatabase(path);
|
||||
await exec(futureDatabase, `ALTER TABLE session_chats DROP COLUMN is_read;
|
||||
ALTER TABLE session_chats ADD COLUMN parent_chat TEXT;
|
||||
ALTER TABLE session_chats ADD COLUMN storage_resource TEXT;
|
||||
PRAGMA user_version = 17`);
|
||||
await close(futureDatabase);
|
||||
|
||||
database = new AgentHostDatabase(path);
|
||||
const catalog = await database.getSessionChatCatalog('session://future-v17');
|
||||
await database.close();
|
||||
database = undefined;
|
||||
|
||||
const repairedDatabase = await openDatabase(path);
|
||||
const version = await all(repairedDatabase, 'PRAGMA user_version');
|
||||
const columns = (await all(repairedDatabase, 'PRAGMA table_info(session_chats)')).map(row => row.name);
|
||||
await close(repairedDatabase);
|
||||
|
||||
assert.deepStrictEqual({ catalog, version, columns }, {
|
||||
catalog: {
|
||||
revision: 1,
|
||||
legacyMirroredRevision: 0,
|
||||
chats: [{ chat: 'ahp-chat://peer', order: 0, providerData: 'peer' }],
|
||||
},
|
||||
version: [{ user_version: 17 }],
|
||||
columns: ['session_uri', 'chat_uri', 'chat_order', 'provider_data', 'origin', 'inherited_turn_id', 'archived', 'parent_chat', 'storage_resource', 'is_read'],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -712,7 +751,7 @@ suite('AgentHostDatabase sessions_v2', () => {
|
||||
external: false,
|
||||
source: 'explicit',
|
||||
},
|
||||
version: [{ user_version: 12 }],
|
||||
version: [{ user_version: 13 }],
|
||||
marker: [{ name: 'future_v6_marker' }],
|
||||
});
|
||||
}).timeout(10_000);
|
||||
@@ -913,7 +952,7 @@ suite('AgentHostDatabase sessions_v2', () => {
|
||||
/payloadHash must match payload/,
|
||||
);
|
||||
await assert.rejects(
|
||||
database.upsertSessionV2(createEnvelope(session, 'generation-1', 3, { payload: '{"payloadVersion":1,"data":{}}' }), 'generation-1'),
|
||||
database.upsertSessionV2(createEnvelope(session, 'generation-1', 3, { payload: `{"payloadVersion":${AGENT_HOST_CATALOG_PAYLOAD_VERSION},"data":{}}` }), 'generation-1'),
|
||||
/Catalog payload is invalid/,
|
||||
);
|
||||
await assert.rejects(
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import assert from 'assert';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
||||
import { ITelemetryService, TelemetryLevel } from '../../../telemetry/common/telemetry.js';
|
||||
import { AgentSession, type IAgentTelemetryContext } from '../../common/agent.js';
|
||||
import { AgentSession, type AgentSubagentKind, type IAgentTelemetryContext } from '../../common/agent.js';
|
||||
import { AgentHostClientType } from '../../common/agentHostClientInfo.js';
|
||||
import { AgentHostClientConnectionKind, AgentHostLaunchKind, AgentHostTransportKind, type IAgentHostClientTelemetryContext } from '../../common/agentHostTelemetry.js';
|
||||
import { ChatInputRequestPurpose, withChatInputRequestPurpose } from '../../common/meta/agentChatInputRequestMeta.js';
|
||||
@@ -43,11 +43,11 @@ suite('AgentHostInputRequestTracker', () => {
|
||||
const rootChat = buildDefaultChatUri(rootSession);
|
||||
const subagentChat = buildSubagentChatUri(rootSession, 'subagent-tool');
|
||||
|
||||
function createTracker(clientContext?: IAgentHostClientTelemetryContext, telemetryContext?: IAgentTelemetryContext): { telemetry: CapturingTelemetryService; tracker: AgentHostInputRequestTracker } {
|
||||
function createTracker(clientContext?: IAgentHostClientTelemetryContext, telemetryContext?: IAgentTelemetryContext, getSubagentKind?: (session: string) => AgentSubagentKind | undefined): { telemetry: CapturingTelemetryService; tracker: AgentHostInputRequestTracker } {
|
||||
const telemetry = new CapturingTelemetryService();
|
||||
return {
|
||||
telemetry,
|
||||
tracker: new AgentHostInputRequestTracker(new AgentHostTelemetryReporter(telemetry), () => ({ elapsed: () => 25 }), () => clientContext, () => telemetryContext),
|
||||
tracker: new AgentHostInputRequestTracker(new AgentHostTelemetryReporter(telemetry), () => ({ elapsed: () => 25 }), () => clientContext, () => telemetryContext, getSubagentKind),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,16 +222,22 @@ suite('AgentHostInputRequestTracker', () => {
|
||||
assert.deepStrictEqual(telemetry.events, []);
|
||||
});
|
||||
|
||||
test('emits subagent identifiers', () => {
|
||||
const { telemetry, tracker } = createTracker();
|
||||
test('emits subagent identifiers and what the subagent chat represents', () => {
|
||||
const phaseChat = buildSubagentChatUri(rootSession, 'fusion:fusion-1:phase-1');
|
||||
const { telemetry, tracker } = createTracker(undefined, undefined, session => session === phaseChat ? 'fusionPhase' : undefined);
|
||||
const request: ChatInputRequest = withChatInputRequestPurpose({ id: 'request-1', questions: [] }, ChatInputRequestPurpose.AskUser);
|
||||
|
||||
tracker.inputRequested('mock', subagentChat, 'turn-1', request);
|
||||
tracker.inputCompleted(subagentChat, accept(request.id), completedState(subagentChat, 'turn-1', request));
|
||||
for (const chat of [subagentChat, phaseChat]) {
|
||||
tracker.inputRequested('mock', chat, 'turn-1', request);
|
||||
tracker.inputCompleted(chat, accept(request.id), completedState(chat, 'turn-1', request));
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(telemetry.events.map(event => {
|
||||
const data = event.data as { agentSessionId: string; isSubagentSession: boolean };
|
||||
return { agentSessionId: data.agentSessionId, isSubagentSession: data.isSubagentSession };
|
||||
}), [{ agentSessionId: 'root-session', isSubagentSession: true }]);
|
||||
const data = event.data as { agentSessionId: string; isSubagentSession: boolean; subagentKind?: AgentSubagentKind };
|
||||
return { agentSessionId: data.agentSessionId, isSubagentSession: data.isSubagentSession, subagentKind: data.subagentKind };
|
||||
}), [
|
||||
{ agentSessionId: 'root-session', isSubagentSession: true, subagentKind: 'task' },
|
||||
{ agentSessionId: 'root-session', isSubagentSession: true, subagentKind: 'fusionPhase' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
||||
import { NullLogService } from '../../../log/common/log.js';
|
||||
import { ChatOriginKind } from '../../common/state/protocol/state.js';
|
||||
import { buildChatUri, buildDefaultChatUri } from '../../common/state/sessionState.js';
|
||||
import { AH_META_IS_READ_DB_KEY, buildChatUri, buildDefaultChatUri } from '../../common/state/sessionState.js';
|
||||
import { AGENT_HOST_CATALOG_CHILD_LIMIT } from '../../node/agentHostCatalogProjection.js';
|
||||
import { AgentHostDatabase } from '../../node/agentHostDatabase.js';
|
||||
import { AgentHostPeerChatStore, CHAT_ORIGIN_METADATA_KEY, CHAT_PROVIDER_DATA_METADATA_KEY, PEER_CHATS_METADATA_KEY } from '../../node/agentHostPeerChatStore.js';
|
||||
@@ -929,6 +929,30 @@ suite('AgentHostPeerChatStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('persists read state across updates and restores', async () => {
|
||||
const { store, databaseFor } = createPerResourceStore();
|
||||
await store.upsert(session, first, 'old', origin, 'inherited-turn');
|
||||
await store.setRead(session, first, false);
|
||||
await store.upsert(session, first, 'refreshed');
|
||||
const unread = await store.tryRead(session);
|
||||
const centralUnread = (await orchestrator.getSessionChatCatalog(session.toString()))?.chats.find(chat => chat.chat === first.toString())?.isRead;
|
||||
const compatibilityUnread = await databaseFor(first).getMetadata(AH_META_IS_READ_DB_KEY);
|
||||
|
||||
await store.setRead(session, first, true);
|
||||
const read = await store.tryRead(session);
|
||||
const centralRead = (await orchestrator.getSessionChatCatalog(session.toString()))?.chats.find(chat => chat.chat === first.toString())?.isRead;
|
||||
const compatibilityRead = await databaseFor(first).getMetadata(AH_META_IS_READ_DB_KEY);
|
||||
|
||||
assert.deepStrictEqual({ unread, centralUnread, compatibilityUnread, read, centralRead, compatibilityRead }, {
|
||||
unread: [{ uri: first.toString(), isRead: false, providerData: 'refreshed', origin, inheritedTurnId: 'inherited-turn' }],
|
||||
centralUnread: false,
|
||||
compatibilityUnread: '',
|
||||
read: [{ uri: first.toString(), isRead: true, providerData: 'refreshed', origin, inheritedTurnId: 'inherited-turn' }],
|
||||
centralRead: true,
|
||||
compatibilityRead: 'true',
|
||||
});
|
||||
});
|
||||
|
||||
test('persists and reads the explicit empty sentinel', async () => {
|
||||
const database = new TestSessionDatabase();
|
||||
const store = createStore(database);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c
|
||||
import { runWithFakedTimers } from '../../../../base/test/common/timeTravelScheduler.js';
|
||||
import { NullLogService } from '../../../log/common/log.js';
|
||||
import { ActionType, NotificationType, type ActionEnvelope, type INotification } from '../../common/state/sessionActions.js';
|
||||
import { ChangesetStatus, ChatInputQuestionKind, ChatInputResponseKind, ChatInteractivity, ChatOriginKind, MessageKind, SessionSummary, ResponsePartKind, ROOT_STATE_URI, SessionLifecycle, SessionStatus, TurnState, buildChatUri, buildDefaultChatUri, buildSubagentSessionUri, buildSubagentSessionUriPrefix, createErrorResponsePart, isSubagentSession, mergeSessionWithDefaultChat, parseSubagentSessionUri, readHostBuildInfo, readSessionEhcliAdoptable, withSessionEhcliAdoptable, type ChatState, type MarkdownResponsePart, type SessionState, type Turn } from '../../common/state/sessionState.js';
|
||||
import { ChangesetStatus, ChatInputQuestionKind, ChatInputResponseKind, ChatInteractivity, ChatOriginKind, MessageKind, SessionSummary, ResponsePartKind, ROOT_STATE_URI, SessionLifecycle, SessionStatus, TurnState, buildChatUri, buildDefaultChatUri, buildSubagentSessionUri, buildSubagentSessionUriPrefix, createErrorResponsePart, isSessionStatusRead, isSubagentSession, mergeSessionWithDefaultChat, parseSubagentSessionUri, readHostBuildInfo, readSessionEhcliAdoptable, withSessionEhcliAdoptable, type ChatState, type MarkdownResponsePart, type SessionState, type Turn } from '../../common/state/sessionState.js';
|
||||
import { type SessionSummaryChangedParams } from '../../common/state/protocol/notifications.js';
|
||||
import { BackgroundWorkKind, type BackgroundShellWork } from '../../common/state/protocol/channels-chat/state.js';
|
||||
import { AgentHostStateManager } from '../../node/agentHostStateManager.js';
|
||||
@@ -232,6 +232,123 @@ suite('AgentHostStateManager', () => {
|
||||
assert.strictEqual(envelopes[0].origin, undefined);
|
||||
});
|
||||
|
||||
test('does not mark a multi-chat session read while any chat is unread', () => {
|
||||
manager.createSession(makeSessionSummary());
|
||||
const peer = buildChatUri(sessionUri, 'peer');
|
||||
manager.addChat(sessionUri, peer);
|
||||
manager.dispatchServerAction(peer, { type: ActionType.ChatIsReadChanged, isRead: false });
|
||||
|
||||
manager.dispatchServerAction(sessionUri, { type: ActionType.SessionIsReadChanged, isRead: true });
|
||||
|
||||
const state = manager.getSessionState(sessionUri)!;
|
||||
assert.deepStrictEqual({
|
||||
sessionIsRead: !!(state.status & SessionStatus.IsRead),
|
||||
defaultChatIsRead: !!(manager.getChatState(sessionChatUri)!.status & SessionStatus.IsRead),
|
||||
peerIsRead: !!(manager.getChatState(peer)!.status & SessionStatus.IsRead),
|
||||
catalog: state.chats.map(chat => ({
|
||||
resource: chat.resource,
|
||||
isRead: !!(chat.status & SessionStatus.IsRead),
|
||||
})),
|
||||
}, {
|
||||
sessionIsRead: false,
|
||||
defaultChatIsRead: false,
|
||||
peerIsRead: false,
|
||||
catalog: [
|
||||
{ resource: sessionChatUri, isRead: false },
|
||||
{ resource: peer, isRead: false },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('marking a chat unread also marks its session unread', () => {
|
||||
manager.createSession(makeSessionSummary());
|
||||
const peer = buildChatUri(sessionUri, 'peer');
|
||||
manager.addChat(sessionUri, peer);
|
||||
manager.dispatchServerAction(sessionChatUri, { type: ActionType.ChatIsReadChanged, isRead: true });
|
||||
manager.dispatchServerAction(peer, { type: ActionType.ChatIsReadChanged, isRead: true });
|
||||
manager.dispatchServerAction(sessionUri, { type: ActionType.SessionIsReadChanged, isRead: true });
|
||||
|
||||
manager.dispatchServerAction(peer, { type: ActionType.ChatIsReadChanged, isRead: false });
|
||||
|
||||
assert.deepStrictEqual({
|
||||
sessionIsRead: isSessionStatusRead(manager.getSessionState(sessionUri)!.status),
|
||||
defaultChatIsRead: isSessionStatusRead(manager.getChatState(sessionChatUri)!.status),
|
||||
peerIsRead: isSessionStatusRead(manager.getChatState(peer)!.status),
|
||||
}, {
|
||||
sessionIsRead: false,
|
||||
defaultChatIsRead: true,
|
||||
peerIsRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('tool chats retain exact read state without affecting the session aggregate', () => {
|
||||
manager.createSession(makeSessionSummary());
|
||||
manager.dispatchServerAction(sessionUri, { type: ActionType.SessionIsReadChanged, isRead: true });
|
||||
const tool = buildChatUri(sessionUri, 'tool');
|
||||
manager.addChat(sessionUri, tool, {
|
||||
origin: { kind: ChatOriginKind.Tool, chat: sessionChatUri, toolCallId: 'tool-call' },
|
||||
});
|
||||
|
||||
manager.dispatchServerAction(tool, { type: ActionType.ChatIsReadChanged, isRead: false });
|
||||
const afterToolUnread = {
|
||||
session: isSessionStatusRead(manager.getSessionState(sessionUri)!.status),
|
||||
defaultChat: isSessionStatusRead(manager.getChatState(sessionChatUri)!.status),
|
||||
tool: isSessionStatusRead(manager.getChatState(tool)!.status),
|
||||
};
|
||||
manager.dispatchServerAction(sessionUri, { type: ActionType.SessionIsReadChanged, isRead: false });
|
||||
const afterSessionUnread = {
|
||||
session: isSessionStatusRead(manager.getSessionState(sessionUri)!.status),
|
||||
defaultChat: isSessionStatusRead(manager.getChatState(sessionChatUri)!.status),
|
||||
tool: isSessionStatusRead(manager.getChatState(tool)!.status),
|
||||
};
|
||||
|
||||
assert.deepStrictEqual({ afterToolUnread, afterSessionUnread }, {
|
||||
afterToolUnread: { session: true, defaultChat: true, tool: false },
|
||||
afterSessionUnread: { session: false, defaultChat: false, tool: false },
|
||||
});
|
||||
});
|
||||
|
||||
test('hidden chats retain exact read state without affecting the session aggregate', () => {
|
||||
manager.createSession(makeSessionSummary());
|
||||
manager.dispatchServerAction(sessionUri, { type: ActionType.SessionIsReadChanged, isRead: true });
|
||||
const hidden = buildChatUri(sessionUri, 'hidden');
|
||||
manager.addChat(sessionUri, hidden, { interactivity: ChatInteractivity.Hidden });
|
||||
|
||||
manager.dispatchServerAction(hidden, { type: ActionType.ChatIsReadChanged, isRead: false });
|
||||
|
||||
assert.deepStrictEqual({
|
||||
session: isSessionStatusRead(manager.getSessionState(sessionUri)!.status),
|
||||
defaultChat: isSessionStatusRead(manager.getChatState(sessionChatUri)!.status),
|
||||
hidden: isSessionStatusRead(manager.getChatState(hidden)!.status),
|
||||
}, {
|
||||
session: true,
|
||||
defaultChat: true,
|
||||
hidden: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('session read state synchronizes its sole default chat', () => {
|
||||
manager.createSession(makeSessionSummary());
|
||||
|
||||
manager.dispatchServerAction(sessionUri, { type: ActionType.SessionIsReadChanged, isRead: true });
|
||||
const afterRead = {
|
||||
session: isSessionStatusRead(manager.getSessionState(sessionUri)!.status),
|
||||
chat: isSessionStatusRead(manager.getChatState(sessionChatUri)!.status),
|
||||
};
|
||||
manager.dispatchServerAction(sessionUri, { type: ActionType.SessionIsReadChanged, isRead: false });
|
||||
|
||||
assert.deepStrictEqual({
|
||||
afterRead,
|
||||
afterUnread: {
|
||||
session: isSessionStatusRead(manager.getSessionState(sessionUri)!.status),
|
||||
chat: isSessionStatusRead(manager.getChatState(sessionChatUri)!.status),
|
||||
},
|
||||
}, {
|
||||
afterRead: { session: true, chat: true },
|
||||
afterUnread: { session: false, chat: false },
|
||||
});
|
||||
});
|
||||
|
||||
test('emits session title changes and suppresses no-op assignments', () => {
|
||||
manager.createSession(makeSessionSummary());
|
||||
|
||||
@@ -755,6 +872,44 @@ suite('AgentHostStateManager', () => {
|
||||
assert.strictEqual((chatState?.turns[0].responseParts[0] as MarkdownResponsePart).content, 'world');
|
||||
});
|
||||
|
||||
test('restoreSession marks an inconsistent multi-chat aggregate unread', () => {
|
||||
const peer = buildChatUri(sessionUri, 'peer');
|
||||
const state = manager.restoreSession({
|
||||
...makeSessionSummary(),
|
||||
status: SessionStatus.Idle | SessionStatus.IsRead,
|
||||
chats: [{ resource: peer, title: 'Peer', status: SessionStatus.Idle | SessionStatus.IsRead }],
|
||||
}, [], { defaultChatIsRead: false });
|
||||
|
||||
assert.deepStrictEqual({
|
||||
sessionIsRead: isSessionStatusRead(state.status),
|
||||
defaultChatIsRead: isSessionStatusRead(manager.getChatState(sessionChatUri)!.status),
|
||||
peerIsRead: isSessionStatusRead(state.chats.find(chat => chat.resource === peer)!.status),
|
||||
}, {
|
||||
sessionIsRead: false,
|
||||
defaultChatIsRead: false,
|
||||
peerIsRead: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('restoreSession excludes hidden unread chats from the session aggregate', () => {
|
||||
const hidden = buildChatUri(sessionUri, 'hidden');
|
||||
const state = manager.restoreSession({
|
||||
...makeSessionSummary(),
|
||||
status: SessionStatus.Idle | SessionStatus.IsRead,
|
||||
chats: [{ resource: hidden, title: 'Hidden', interactivity: ChatInteractivity.Hidden, status: SessionStatus.Idle }],
|
||||
}, [], { defaultChatIsRead: true });
|
||||
|
||||
assert.deepStrictEqual({
|
||||
sessionIsRead: isSessionStatusRead(state.status),
|
||||
defaultChatIsRead: isSessionStatusRead(manager.getChatState(sessionChatUri)!.status),
|
||||
hiddenIsRead: isSessionStatusRead(state.chats.find(chat => chat.resource === hidden)!.status),
|
||||
}, {
|
||||
sessionIsRead: true,
|
||||
defaultChatIsRead: true,
|
||||
hiddenIsRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('restoreSession returns existing state for duplicate session', () => {
|
||||
const existing = manager.createSession(makeSessionSummary());
|
||||
|
||||
@@ -933,6 +1088,42 @@ suite('AgentHostStateManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('emits sessionSummaryChanged when compact chat status changes', () => {
|
||||
return runWithFakedTimers({ useFakeTimers: true }, async () => {
|
||||
const chat = buildChatUri(sessionUri, 'compact-status');
|
||||
manager.createSession(makeSessionSummary());
|
||||
manager.dispatchServerAction(sessionUri, { type: ActionType.SessionReady });
|
||||
manager.addChat(sessionUri, chat, { title: 'Peer' });
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
|
||||
const notifications: INotification[] = [];
|
||||
disposables.add(manager.onDidEmitNotification(notification => notifications.push(notification)));
|
||||
manager.dispatchServerAction(chat, { type: ActionType.ChatIsArchivedChanged, isArchived: true });
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
|
||||
const changes = notifications
|
||||
.filter(notification => notification.type === NotificationType.SessionSummaryChanged)
|
||||
.map(notification => notification.changes);
|
||||
assert.deepStrictEqual(changes, [{
|
||||
chats: [
|
||||
{
|
||||
resource: buildDefaultChatUri(sessionUri),
|
||||
title: 'Test',
|
||||
origin: { kind: MessageKind.User },
|
||||
status: SessionStatus.Idle,
|
||||
},
|
||||
{
|
||||
resource: chat,
|
||||
title: 'Peer',
|
||||
origin: { kind: MessageKind.User },
|
||||
status: SessionStatus.Idle | SessionStatus.IsRead | SessionStatus.IsArchived,
|
||||
archived: true,
|
||||
},
|
||||
],
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
test('coalesces multiple summary changes into one notification', () => {
|
||||
return runWithFakedTimers({ useFakeTimers: true }, async () => {
|
||||
manager.createSession(makeSessionSummary());
|
||||
@@ -1249,7 +1440,7 @@ suite('AgentHostStateManager', () => {
|
||||
chatResources: [buildDefaultChatUri(sessionUri), peerChat].sort(),
|
||||
summaryChats: [
|
||||
{ resource: buildDefaultChatUri(sessionUri), title: 'Test', origin: { kind: MessageKind.User }, status: SessionStatus.Idle },
|
||||
{ resource: peerChat, title: 'Peer', origin: { kind: MessageKind.User }, status: SessionStatus.Idle },
|
||||
{ resource: peerChat, title: 'Peer', origin: { kind: MessageKind.User }, status: SessionStatus.Idle | SessionStatus.IsRead },
|
||||
],
|
||||
defaultChat: buildDefaultChatUri(sessionUri),
|
||||
peerTurns: 0,
|
||||
@@ -1569,14 +1760,14 @@ suite('AgentHostStateManager', () => {
|
||||
resource: peerChat,
|
||||
title: 'Peer',
|
||||
origin: { kind: ChatOriginKind.User },
|
||||
status: SessionStatus.Idle | SessionStatus.IsArchived,
|
||||
status: SessionStatus.Idle | SessionStatus.IsRead | SessionStatus.IsArchived,
|
||||
archived: true,
|
||||
},
|
||||
sessionArchived: false,
|
||||
action: {
|
||||
type: ActionType.SessionChatUpdated,
|
||||
chat: peerChat,
|
||||
changes: { status: SessionStatus.Idle | SessionStatus.IsArchived, activity: undefined },
|
||||
changes: { status: SessionStatus.Idle | SessionStatus.IsRead | SessionStatus.IsArchived, activity: undefined },
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -2184,7 +2375,7 @@ suite('AgentHostStateManager', () => {
|
||||
...makeSessionSummary(),
|
||||
chats: [
|
||||
{ resource: defaultChat, title: '' },
|
||||
{ resource: peerChat, title: 'Peer', status: SessionStatus.Idle },
|
||||
{ resource: peerChat, title: 'Peer', status: SessionStatus.Idle | SessionStatus.IsRead },
|
||||
],
|
||||
defaultChat,
|
||||
};
|
||||
@@ -2197,7 +2388,7 @@ suite('AgentHostStateManager', () => {
|
||||
|
||||
assert.strictEqual(
|
||||
manager.getSessionState(sessionUri)?.chats.find(chat => chat.resource === peerChat)?.status,
|
||||
SessionStatus.Idle | SessionStatus.IsArchived,
|
||||
SessionStatus.Idle | SessionStatus.IsRead | SessionStatus.IsArchived,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2410,9 +2601,10 @@ suite('AgentHostStateManager', () => {
|
||||
resource: chat.resource,
|
||||
title: chat.title,
|
||||
interactivity: chat.interactivity,
|
||||
status: chat.status,
|
||||
})), [
|
||||
{ resource: sessionChatUri, title: 'Test', interactivity: undefined },
|
||||
{ resource: peerChat, title: 'Hidden peer', interactivity: ChatInteractivity.Hidden },
|
||||
{ resource: sessionChatUri, title: 'Test', interactivity: undefined, status: SessionStatus.Idle },
|
||||
{ resource: peerChat, title: 'Hidden peer', interactivity: ChatInteractivity.Hidden, status: SessionStatus.Idle | SessionStatus.IsRead },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import { getTelemetryChatSessionId } from '../../common/agentTelemetryCorrelatio
|
||||
import { withMcpServerSourceMeta } from '../../common/meta/mcpCustomizationMeta.js';
|
||||
import { CustomizationType, McpServerStatus, SessionInputRequestKind, type McpServerCustomization } from '../../common/state/protocol/state.js';
|
||||
import { ActionType, type ChatAction } from '../../common/state/sessionActions.js';
|
||||
import { buildDefaultChatUri, MessageKind, SessionStatus, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, type ToolCallContributor, type ToolCallResult } from '../../common/state/sessionState.js';
|
||||
import { buildDefaultChatUri, buildSubagentChatUri, MessageKind, SessionStatus, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, type ToolCallContributor, type ToolCallResult } from '../../common/state/sessionState.js';
|
||||
import { IAgentHostCheckpointService, NULL_CHECKPOINT_SERVICE } from '../../common/agentHostCheckpointService.js';
|
||||
import { IAgentHostChatContributions } from '../../common/agentHostChatContributionsService.js';
|
||||
import { IAgentHostTerminalManager } from '../../node/agentHostTerminalManager.js';
|
||||
@@ -292,6 +292,7 @@ suite('AgentSideEffects — tool call telemetry', () => {
|
||||
[IAgentHostClientConnectionService, clientConnectionService],
|
||||
[IAgentHostPeerChatPersistenceService, {
|
||||
_serviceBrand: undefined,
|
||||
setRead: async () => { },
|
||||
setArchived: async () => { },
|
||||
}],
|
||||
[ISessionWorkspaceConversionService, {
|
||||
@@ -392,6 +393,30 @@ suite('AgentSideEffects — tool call telemetry', () => {
|
||||
}]);
|
||||
});
|
||||
|
||||
test('classifies tool calls in subagent chats by subagent kind and reports the phase model', () => {
|
||||
setupSession();
|
||||
agent.setModels([{ provider: 'mock', id: 'gpt-5.5', name: 'GPT 5.5', supportsVision: false }]);
|
||||
startTurn('turn-1');
|
||||
const runSubagentTool = (toolCallId: string, phase?: { subagentKind: 'fusionPhase'; model: string }) => {
|
||||
const chatUri = buildSubagentChatUri(sessionUri, toolCallId);
|
||||
stateManager.addChat(sessionKey, chatUri);
|
||||
agent.fireProgress({ kind: 'subagent_started', chat: URI.parse(defaultChatUri), toolCallId, agentName: 'agent', agentDisplayName: 'Agent', ...phase });
|
||||
const turnId = stateManager.getActiveTurnId(chatUri) ?? 'missing-subagent-turn';
|
||||
const fireOnChat = (action: ChatAction) => agent.fireProgress({ kind: 'action', resource: URI.parse(chatUri), action });
|
||||
fireOnChat({ type: ActionType.ChatToolCallStart, turnId, toolCallId: `${toolCallId}-view`, toolName: 'view', displayName: 'view' });
|
||||
fireOnChat({ type: ActionType.ChatToolCallComplete, turnId, toolCallId: `${toolCallId}-view`, result: { success: true, pastTenseMessage: 'viewed' } });
|
||||
agent.fireProgress({ kind: 'subagent_completed', chat: URI.parse(defaultChatUri), toolCallId });
|
||||
};
|
||||
runSubagentTool('call-task');
|
||||
runSubagentTool('fusion:fusion-1:phase-1', { subagentKind: 'fusionPhase', model: 'gpt-5.5' });
|
||||
completeTurn('turn-1');
|
||||
|
||||
assert.deepStrictEqual(agentHostToolEvents().map(({ data }) => ({ toolCallId: data.toolCallId, isSubagentSession: data.isSubagentSession, subagentKind: data.subagentKind, model: data.model })), [
|
||||
{ toolCallId: 'call-task-view', isSubagentSession: true, subagentKind: 'task', model: undefined },
|
||||
{ toolCallId: 'fusion:fusion-1:phase-1-view', isSubagentSession: true, subagentKind: 'fusionPhase', model: { trusted: true, value: 'gpt-5.5' } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('attributes tool telemetry to the initiating turn client', () => {
|
||||
setupSession();
|
||||
const clientContext: IAgentHostClientTelemetryContext = {
|
||||
|
||||
Loaded 100 of 429 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user