@jetcrab/pi-desk 1.1.18-dev → 1.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/docs/plugins.md +82 -2
  2. package/next.config.ts +1 -0
  3. package/package.json +4 -3
  4. package/src/app/page.tsx +6 -1
  5. package/src/app/project-preview/page.tsx +10 -0
  6. package/src/client/l1_entry/pages/l1-home-page.tsx +30 -0
  7. package/src/client/l1_entry/pages/l1-project-preview-page.tsx +33 -0
  8. package/src/client/l1_entry/providers/l1-app-providers.tsx +18 -15
  9. package/src/client/l1_entry/providers/l1-app-runtime-provider.tsx +2 -2
  10. package/src/client/l2_biz/auth/l2-login-biz.ts +3 -0
  11. package/src/client/l2_biz/auth/l2-login.tsx +2 -3
  12. package/src/client/l2_biz/plugin-host/l2-plugin-capability-details.tsx +6 -1
  13. package/src/client/l2_biz/plugin-host/l2-plugin-contribution-host.tsx +71 -10
  14. package/src/client/l2_biz/plugin-host/l2-plugin-management-controls.tsx +9 -0
  15. package/src/client/l2_biz/plugin-host/l2-plugin-session-header-layout.ts +18 -0
  16. package/src/client/l2_biz/plugin-host/l2-plugin-session-header.tsx +247 -0
  17. package/src/client/l2_biz/plugin-host/l2-plugin-widgets.tsx +50 -0
  18. package/src/client/l2_biz/plugin-host/views/l2-plugin-session-header-action.tsx +112 -0
  19. package/src/client/l2_biz/project-preview/hooks/l2-use-project-picker.ts +76 -0
  20. package/src/client/l2_biz/project-preview/hooks/l2-use-project-preview.ts +214 -0
  21. package/src/client/l2_biz/project-preview/l2-project-preview-biz.ts +49 -4
  22. package/src/client/l2_biz/project-preview/l2-project-preview-dialog.tsx +42 -450
  23. package/src/client/l2_biz/project-preview/l2-project-preview-page.tsx +61 -0
  24. package/src/client/l2_biz/project-preview/l2-project-preview.tsx +342 -0
  25. package/src/client/l2_biz/project-preview/runtime/l2-project-history-runtime.ts +9 -3
  26. package/src/client/l2_biz/project-preview/views/l2-project-picker.tsx +168 -0
  27. package/src/client/l2_biz/settings/hooks/l2-use-model-settings.ts +34 -31
  28. package/src/client/l2_biz/settings/views/l2-model-replacement-preview.tsx +7 -1
  29. package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-runtime.ts +1 -1
  30. package/src/client/l2_biz/workbench/file-workspace/l2-project-preview-biz.ts +4 -3
  31. package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-reload.ts +42 -0
  32. package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-viewport.ts +28 -21
  33. package/src/client/l2_biz/workbench/l2-work-session-columns.module.css +32 -0
  34. package/src/client/l2_biz/workbench/l2-work-session-columns.tsx +55 -16
  35. package/src/client/l2_biz/workbench/l2-work-session-sidebar.tsx +38 -9
  36. package/src/client/l2_biz/workbench/l2-workbench-chat-composer.tsx +12 -1
  37. package/src/client/l2_biz/workbench/l2-workbench-chat.ts +45 -0
  38. package/src/client/l2_biz/workbench/l2-workbench-initialization-notice.tsx +8 -3
  39. package/src/client/l2_biz/workbench/l2-workbench-plugin-slots.ts +13 -0
  40. package/src/client/l2_biz/workbench/l2-workbench.tsx +19 -2
  41. package/src/client/l3_modules/conversation/l3-conversation-markdown.tsx +10 -1
  42. package/src/client/l3_modules/conversation/l3-conversation-viewport.tsx +12 -6
  43. package/src/client/l3_modules/project-files/l3-markdown-preview.module.css +1 -1
  44. package/src/client/l3_modules/project-files/l3-markdown-preview.tsx +10 -1
  45. package/src/client/l3_modules/project-files/l3-project-document-git.ts +4 -3
  46. package/src/client/l3_modules/project-files/l3-project-document-view.tsx +6 -1
  47. package/src/client/l3_modules/project-files/l3-project-file-browser.tsx +171 -229
  48. package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +133 -70
  49. package/src/client/l3_modules/project-files/l3-project-reader.ts +26 -37
  50. package/src/client/l3_modules/project-files/views/l3-project-search-controls.tsx +201 -0
  51. package/src/client/l3_modules/project-files/views/l3-project-search-results.tsx +333 -0
  52. package/src/client/l4_foundation/lib/l4-client-id.tsx +11 -0
  53. package/src/client/l4_foundation/plugin-host/l4-plugin-host-runtime.ts +316 -35
  54. package/src/client/l4_foundation/ui/code/l4-code-preview.tsx +47 -3
  55. package/src/client/l4_foundation/ui/markdown/l4-markdown-remark.ts +89 -0
  56. package/src/client/l4_foundation/ui/markdown/l4-markdown.module.css +58 -0
  57. package/src/client/l4_foundation/ui/markdown/l4-markdown.tsx +108 -0
  58. package/src/common/l2_biz/auth/l2-auth-return-to.ts +41 -0
  59. package/src/common/l2_biz/model-settings/l2-model-settings-contract.ts +5 -1
  60. package/src/common/l2_biz/plugin/l2-plugin-management-messages.ts +4 -0
  61. package/src/common/l2_biz/project-preview/l2-project-history-messages.ts +28 -0
  62. package/src/common/l2_biz/project-preview/l2-project-preview-navigation.ts +5 -0
  63. package/src/common/l2_biz/settings/l2-settings-messages.ts +4 -3
  64. package/src/common/l2_biz/work-session/l2-work-session-file-contract.ts +4 -3
  65. package/src/common/l2_biz/workbench/l2-workbench-locale-messages.ts +8 -0
  66. package/src/common/l3_modules/project-files/l3-project-files-contract.ts +2 -3
  67. package/src/common/l3_modules/project-files/l3-project-files-locale-messages.ts +56 -10
  68. package/src/common/l4_foundation/file/l4-project-file-contract.ts +51 -1
  69. package/src/common/l4_foundation/locale/l4-host-error-messages.ts +11 -0
  70. package/src/common/l4_foundation/locale/l4-locale-messages.ts +16 -0
  71. package/src/instrumentation.ts +0 -2
  72. package/src/server/l1_entry/api/l1-project-read-routes.ts +6 -4
  73. package/src/server/l1_entry/api/l1-project-route.ts +9 -8
  74. package/src/server/l1_entry/api/l1-work-session-file-error.ts +23 -0
  75. package/src/server/l1_entry/node/l1-server.ts +8 -4
  76. package/src/server/l2_biz/plugin-management/l2-plugin-management.ts +0 -3
  77. package/src/server/l2_biz/work-session/l2-work-session-chat-runtime.ts +30 -58
  78. package/src/server/l2_biz/work-session/l2-work-session-manage.ts +0 -4
  79. package/src/server/l4_foundation/file/l4-project-file-index.ts +276 -0
  80. package/src/server/l4_foundation/file/l4-project-search.ts +399 -0
  81. package/src/server/l4_foundation/file/l4-ripgrep-search.ts +192 -0
  82. package/src/server/l4_foundation/file/l4-work-session-project-file.ts +8 -332
  83. package/src/server/l4_foundation/model-settings/l4-model-settings-store.ts +29 -20
  84. package/src/server/l4_foundation/model-settings/l4-model-settings-types.ts +1 -1
  85. package/src/server/l4_foundation/pi/l4-pi-chat-worker.ts +22 -5
  86. package/src/server/l4_foundation/pi/l4-pi-package-maintenance.ts +0 -6
  87. package/src/server/l4_foundation/pi/l4-pi-plugin-capability-probe.ts +1 -3
  88. package/src/server/l4_foundation/pi/l4-pi-plugin-owner-runtime.ts +133 -13
  89. package/src/server/l4_foundation/pi/l4-pi-plugin-probe.ts +1 -3
  90. package/src/server/l4_foundation/pi/l4-pi-runner-process.ts +2 -2
  91. package/src/server/l4_foundation/pi/l4-pi-session-initialization.ts +9 -0
  92. package/src/server/l4_foundation/process/l4-process-tree.js +23 -4
  93. package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
  94. package/temp/build/pi-desk/release/.next/app-path-routes-manifest.json +3 -2
  95. package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
  96. package/temp/build/pi-desk/release/.next/prerender-manifest.json +3 -3
  97. package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +1 -2
  98. package/temp/build/pi-desk/release/.next/required-server-files.js +4 -3
  99. package/temp/build/pi-desk/release/.next/required-server-files.json +4 -3
  100. package/temp/build/pi-desk/release/.next/routes-manifest.json +6 -0
  101. package/temp/build/pi-desk/release/.next/server/app/_global-error/page.js +1 -1
  102. package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  103. package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
  104. package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +3 -3
  105. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +3 -3
  106. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +3 -3
  107. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  108. package/temp/build/pi-desk/release/.next/server/app/_not-found/page.js +1 -1
  109. package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  110. package/temp/build/pi-desk/release/.next/server/app/api/auth/login/route.js +1 -1
  111. package/temp/build/pi-desk/release/.next/server/app/api/auth/logout/route.js +1 -1
  112. package/temp/build/pi-desk/release/.next/server/app/api/auth-settings/get/route.js +1 -1
  113. package/temp/build/pi-desk/release/.next/server/app/api/auth-settings/replace/route.js +1 -1
  114. package/temp/build/pi-desk/release/.next/server/app/api/capability-modes/get/route.js +1 -1
  115. package/temp/build/pi-desk/release/.next/server/app/api/capability-modes/replace/route.js +1 -1
  116. package/temp/build/pi-desk/release/.next/server/app/api/chat/context/get/route.js +1 -1
  117. package/temp/build/pi-desk/release/.next/server/app/api/chat/images/get/route.js +1 -1
  118. package/temp/build/pi-desk/release/.next/server/app/api/chat/messages/get/route.js +1 -1
  119. package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
  120. package/temp/build/pi-desk/release/.next/server/app/api/mcp-check/get/route.js +1 -1
  121. package/temp/build/pi-desk/release/.next/server/app/api/mcp-settings/del/route.js +1 -1
  122. package/temp/build/pi-desk/release/.next/server/app/api/mcp-settings/get/route.js +1 -1
  123. package/temp/build/pi-desk/release/.next/server/app/api/mcp-settings/replace/route.js +1 -1
  124. package/temp/build/pi-desk/release/.next/server/app/api/model-catalog/list/route.js +1 -1
  125. package/temp/build/pi-desk/release/.next/server/app/api/model-settings/get/route.js +1 -1
  126. package/temp/build/pi-desk/release/.next/server/app/api/model-settings/preview/route.js +1 -1
  127. package/temp/build/pi-desk/release/.next/server/app/api/model-settings/replace/route.js +1 -1
  128. package/temp/build/pi-desk/release/.next/server/app/api/pi/directories/list/route.js +1 -1
  129. package/temp/build/pi-desk/release/.next/server/app/api/pi/directory-entries/list/route.js +1 -1
  130. package/temp/build/pi-desk/release/.next/server/app/api/pi/directory-ignores/list/route.js +1 -1
  131. package/temp/build/pi-desk/release/.next/server/app/api/pi/directory-ignores/replace/route.js +1 -1
  132. package/temp/build/pi-desk/release/.next/server/app/api/pi/models/list/route.js +1 -1
  133. package/temp/build/pi-desk/release/.next/server/app/api/pi/session-user-messages/list/route.js +1 -1
  134. package/temp/build/pi-desk/release/.next/server/app/api/pi/sessions/list/route.js +1 -1
  135. package/temp/build/pi-desk/release/.next/server/app/api/plugin-catalog/get/route.js +1 -1
  136. package/temp/build/pi-desk/release/.next/server/app/api/plugin-catalog/search/route.js +1 -1
  137. package/temp/build/pi-desk/release/.next/server/app/api/plugin-catalog/settings-get/route.js +1 -1
  138. package/temp/build/pi-desk/release/.next/server/app/api/plugin-catalog/settings-replace/route.js +1 -1
  139. package/temp/build/pi-desk/release/.next/server/app/api/plugins/add/route.js +1 -1
  140. package/temp/build/pi-desk/release/.next/server/app/api/plugins/apply/route.js +1 -1
  141. package/temp/build/pi-desk/release/.next/server/app/api/plugins/batch/route.js +1 -1
  142. package/temp/build/pi-desk/release/.next/server/app/api/plugins/browser-resources/[resourceGroup]/[...resourcePath]/route.js +1 -1
  143. package/temp/build/pi-desk/release/.next/server/app/api/plugins/del/route.js +1 -1
  144. package/temp/build/pi-desk/release/.next/server/app/api/plugins/enabled-set/route.js +1 -1
  145. package/temp/build/pi-desk/release/.next/server/app/api/plugins/get/route.js +1 -1
  146. package/temp/build/pi-desk/release/.next/server/app/api/plugins/host-runtime/[runtimeVersion]/[...resourcePath]/route.js +1 -1
  147. package/temp/build/pi-desk/release/.next/server/app/api/plugins/list/route.js +1 -1
  148. package/temp/build/pi-desk/release/.next/server/app/api/plugins/reload/route.js +1 -1
  149. package/temp/build/pi-desk/release/.next/server/app/api/plugins/update/route.js +1 -1
  150. package/temp/build/pi-desk/release/.next/server/app/api/project-files/get/route.js +2 -1
  151. package/temp/build/pi-desk/release/.next/server/app/api/project-files/list/route.js +2 -1
  152. package/temp/build/pi-desk/release/.next/server/app/api/project-files/reveal/route.js +1 -1
  153. package/temp/build/pi-desk/release/.next/server/app/api/project-files/search/route.js +2 -1
  154. package/temp/build/pi-desk/release/.next/server/app/api/project-git/get/route.js +2 -1
  155. package/temp/build/pi-desk/release/.next/server/app/api/project-git-branches/list/route.js +2 -1
  156. package/temp/build/pi-desk/release/.next/server/app/api/project-git-changes/list/route.js +2 -1
  157. package/temp/build/pi-desk/release/.next/server/app/api/project-git-diff/get/route.js +2 -1
  158. package/temp/build/pi-desk/release/.next/server/app/api/project-git-heads/get/route.js +2 -1
  159. package/temp/build/pi-desk/release/.next/server/app/api/project-git-log/list/route.js +2 -1
  160. package/temp/build/pi-desk/release/.next/server/app/api/project-model-default/get/route.js +1 -1
  161. package/temp/build/pi-desk/release/.next/server/app/api/project-model-default/replace/route.js +1 -1
  162. package/temp/build/pi-desk/release/.next/server/app/api/skill-files/get/route.js +1 -1
  163. package/temp/build/pi-desk/release/.next/server/app/api/skill-files/list/route.js +1 -1
  164. package/temp/build/pi-desk/release/.next/server/app/api/skill-files/replace/route.js +1 -1
  165. package/temp/build/pi-desk/release/.next/server/app/api/skills/list/route.js +1 -1
  166. package/temp/build/pi-desk/release/.next/server/app/api/task-center/images/get/route.js +1 -1
  167. package/temp/build/pi-desk/release/.next/server/app/api/task-center/messages/get/route.js +1 -1
  168. package/temp/build/pi-desk/release/.next/server/app/api/work-session-git-branch/add/route.js +1 -1
  169. package/temp/build/pi-desk/release/.next/server/app/api/work-session-git-branch/replace/route.js +1 -1
  170. package/temp/build/pi-desk/release/.next/server/app/api/work-session-tree/get/route.js +1 -1
  171. package/temp/build/pi-desk/release/.next/server/app/api/work-session-tree-entry/get/route.js +1 -1
  172. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/add/route.js +1 -1
  173. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/branch/route.js +1 -1
  174. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/del/route.js +1 -1
  175. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/rename/route.js +1 -1
  176. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/replace/route.js +1 -1
  177. package/temp/build/pi-desk/release/.next/server/app/api/work-sessions/sort/route.js +1 -1
  178. package/temp/build/pi-desk/release/.next/server/app/favicon.ico/route.js +1 -1
  179. package/temp/build/pi-desk/release/.next/server/app/login/page.js +1 -1
  180. package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
  181. package/temp/build/pi-desk/release/.next/server/app/page.js +72 -72
  182. package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
  183. package/temp/build/pi-desk/release/.next/server/app/project-preview/page.js +1 -0
  184. package/temp/build/pi-desk/release/.next/server/app/project-preview/page_client-reference-manifest.js +1 -0
  185. package/temp/build/pi-desk/release/.next/server/app-paths-manifest.json +3 -2
  186. package/temp/build/pi-desk/release/.next/server/chunks/10912.js +2 -2
  187. package/temp/build/pi-desk/release/.next/server/chunks/20460.js +15 -0
  188. package/temp/build/pi-desk/release/.next/server/chunks/23054.js +1 -1
  189. package/temp/build/pi-desk/release/.next/server/chunks/25406.js +10 -10
  190. package/temp/build/pi-desk/release/.next/server/chunks/35985.js +3 -17
  191. package/temp/build/pi-desk/release/.next/server/chunks/36361.js +1 -1
  192. package/temp/build/pi-desk/release/.next/server/chunks/48180.js +1 -1
  193. package/temp/build/pi-desk/release/.next/server/chunks/50520.js +2 -4
  194. package/temp/build/pi-desk/release/.next/server/chunks/56830.js +235 -0
  195. package/temp/build/pi-desk/release/.next/server/chunks/6917.js +1 -1
  196. package/temp/build/pi-desk/release/.next/server/chunks/70780.js +1 -0
  197. package/temp/build/pi-desk/release/.next/server/chunks/74528.js +1 -1
  198. package/temp/build/pi-desk/release/.next/server/chunks/78262.js +1 -1
  199. package/temp/build/pi-desk/release/.next/server/chunks/79686.js +62 -0
  200. package/temp/build/pi-desk/release/.next/server/chunks/80780.js +1 -0
  201. package/temp/build/pi-desk/release/.next/server/chunks/81363.js +1 -1
  202. package/temp/build/pi-desk/release/.next/server/chunks/82499.js +4 -4
  203. package/temp/build/pi-desk/release/.next/server/chunks/92386.js +8 -8
  204. package/temp/build/pi-desk/release/.next/server/instrumentation.js +1 -1
  205. package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
  206. package/temp/build/pi-desk/release/.next/server/middleware-react-loadable-manifest.js +1 -1
  207. package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
  208. package/temp/build/pi-desk/release/.next/server/server-reference-manifest.json +1 -1
  209. package/temp/build/pi-desk/release/.next/static/8Yd33M0zPi4tbRn4xCaRY/_buildManifest.js +1 -0
  210. package/temp/build/pi-desk/release/.next/static/chunks/11937-1d0a9822f2c7995b.js +1 -0
  211. package/temp/build/pi-desk/release/.next/static/chunks/14825-5579aed34a631ad7.js +62 -0
  212. package/temp/build/pi-desk/release/.next/static/chunks/15302-9c807afc6c60de0c.js +1 -0
  213. package/temp/build/pi-desk/release/.next/static/chunks/19991.8aa2e4b6df15fad6.js +1 -0
  214. package/temp/build/pi-desk/release/.next/static/chunks/22092-6d1bfbefa825ec03.js +1 -0
  215. package/temp/build/pi-desk/release/.next/static/chunks/37313-d5970e2d0c270d22.js +238 -0
  216. package/temp/build/pi-desk/release/.next/static/chunks/{49516-cc4dcd84d64ab4fe.js → 51474-8dc714cb76b2e60a.js} +2 -2
  217. package/temp/build/pi-desk/release/.next/static/chunks/57235-03ba1044f22b6c84.js +1 -0
  218. package/temp/build/pi-desk/release/.next/static/chunks/79267-7d21d7cdbd7cfca1.js +1 -0
  219. package/temp/build/pi-desk/release/.next/static/chunks/82371-f7ba4a07a5dbcf34.js +1 -0
  220. package/temp/build/pi-desk/release/.next/static/chunks/9038-03144ed96d613f75.js +15 -0
  221. package/temp/build/pi-desk/release/.next/static/chunks/app/{error-0296a3b20cd1bf00.js → error-216b14b5da089ed3.js} +1 -1
  222. package/temp/build/pi-desk/release/.next/static/chunks/app/layout-20718f1503910291.js +1 -0
  223. package/temp/build/pi-desk/release/.next/static/chunks/app/login/page-0d3d4827802108e5.js +1 -0
  224. package/temp/build/pi-desk/release/.next/static/chunks/app/{not-found-2aab8b33ebbf3257.js → not-found-720cdf6a606b821a.js} +1 -1
  225. package/temp/build/pi-desk/release/.next/static/chunks/app/page-6ebc1c877f6025dd.js +158 -0
  226. package/temp/build/pi-desk/release/.next/static/chunks/app/project-preview/page-d70ee4e9409b970a.js +1 -0
  227. package/temp/build/pi-desk/release/.next/static/chunks/webpack-b60891a214e0925d.js +1 -0
  228. package/temp/build/pi-desk/release/.next/static/css/1e780a4936aa6ba3.css +1 -0
  229. package/temp/build/pi-desk/release/.next/static/css/6dbae2922f88ca31.css +1 -0
  230. package/temp/build/pi-desk/release/.next/static/css/f646ca6c07245c4c.css +1 -0
  231. package/temp/build/pi-desk/release/.next/trace-build +1 -1
  232. package/temp/build/pi-desk/release/.next/server/chunks/33658.js +0 -296
  233. package/temp/build/pi-desk/release/.next/server/chunks/8584.js +0 -2
  234. package/temp/build/pi-desk/release/.next/static/chunks/31087-ba426d09d6f68bfd.js +0 -1
  235. package/temp/build/pi-desk/release/.next/static/chunks/3814-35cfab0ce3aa7c41.js +0 -1
  236. package/temp/build/pi-desk/release/.next/static/chunks/39654-b5fedd762ea0312e.js +0 -238
  237. package/temp/build/pi-desk/release/.next/static/chunks/70669.07f3670bfb7d6dd2.js +0 -1
  238. package/temp/build/pi-desk/release/.next/static/chunks/74220-795a2e7185bd66cb.js +0 -1
  239. package/temp/build/pi-desk/release/.next/static/chunks/75130-d2ca74894aff8085.js +0 -13
  240. package/temp/build/pi-desk/release/.next/static/chunks/78923-17a9c337b29dcecd.js +0 -1
  241. package/temp/build/pi-desk/release/.next/static/chunks/81881-f11dbe7dc0d04ed9.js +0 -1
  242. package/temp/build/pi-desk/release/.next/static/chunks/96995-8f8313ea2ac3abd7.js +0 -62
  243. package/temp/build/pi-desk/release/.next/static/chunks/app/layout-c0e1c5cef8620cf8.js +0 -3
  244. package/temp/build/pi-desk/release/.next/static/chunks/app/login/page-84892b06243891f0.js +0 -1
  245. package/temp/build/pi-desk/release/.next/static/chunks/app/page-a984139e99586041.js +0 -158
  246. package/temp/build/pi-desk/release/.next/static/chunks/webpack-40a782fc8a3535b9.js +0 -1
  247. package/temp/build/pi-desk/release/.next/static/css/6427d02b6edf406f.css +0 -1
  248. package/temp/build/pi-desk/release/.next/static/css/79e06f05df9edff0.css +0 -1
  249. package/temp/build/pi-desk/release/.next/static/n2LrXQgq8UkbH_AzhR3fw/_buildManifest.js +0 -1
  250. /package/temp/build/pi-desk/release/.next/static/{n2LrXQgq8UkbH_AzhR3fw → 8Yd33M0zPi4tbRn4xCaRY}/_ssgManifest.js +0 -0
@@ -40,6 +40,14 @@ export const L4_LOCALE_MESSAGES = {
40
40
  authenticationRequired: 'Please sign in',
41
41
  authUnavailable: 'Sign-in service is not ready',
42
42
  shuttingDown: 'The service is shutting down',
43
+ markdownPending: 'Not completed',
44
+ markdownCompleted: 'Completed',
45
+ markdownProgress: 'In progress',
46
+ markdownNote: 'Note',
47
+ markdownTip: 'Tip',
48
+ markdownImportant: 'Important',
49
+ markdownWarning: 'Warning',
50
+ markdownCaution: 'Caution',
43
51
  codeReadOnly: 'Read-only code file',
44
52
  codeLoading: 'Loading code viewer…',
45
53
  codeFailed: 'Could not load code viewer. Reopen the file.',
@@ -118,6 +126,14 @@ export const L4_LOCALE_MESSAGES = {
118
126
  authenticationRequired: '请先登录',
119
127
  authUnavailable: '登录认证尚未就绪',
120
128
  shuttingDown: '服务正在关闭',
129
+ markdownPending: '未完成',
130
+ markdownCompleted: '已完成',
131
+ markdownProgress: '进行中',
132
+ markdownNote: '说明',
133
+ markdownTip: '提示',
134
+ markdownImportant: '重要',
135
+ markdownWarning: '警告',
136
+ markdownCaution: '注意',
121
137
  codeReadOnly: '只读代码文件',
122
138
  codeLoading: '正在加载代码查看器…',
123
139
  codeFailed: '代码查看器加载失败,请重新打开文件。',
@@ -33,7 +33,6 @@ export async function register(): Promise<void> {
33
33
  })
34
34
  const manage = getL2WorkSessionManage()
35
35
  initializeL2PluginManagement({
36
- refreshPluginMessages: () => manage.refreshPluginMessages(),
37
36
  runPluginRestart: (operation) => manage.runPluginRestart(operation)
38
37
  })
39
38
  registerL1WebAuthRuntime(getL1WebAuthRuntime())
@@ -70,7 +69,6 @@ export async function register(): Promise<void> {
70
69
  setTimeout(() => {
71
70
  void pluginRuntime.initialize().then(
72
71
  () => {
73
- manage.refreshPluginMessages()
74
72
  console.info('[Pi Desk][Instrumentation] 插件加载尝试完成,基础聊天不依赖插件就绪')
75
73
  },
76
74
  (error: unknown) => {
@@ -2,9 +2,9 @@ import 'server-only'
2
2
  import * as contracts from '@common/l3_modules/project-files/l3-project-files-contract'
3
3
  import {
4
4
  getL4WorkSessionProjectFile,
5
- listL4WorkSessionProjectFiles,
6
- searchL4WorkSessionProjectFiles
5
+ listL4WorkSessionProjectFiles
7
6
  } from '@server/l4_foundation/file/l4-work-session-project-file'
7
+ import { searchL4ProjectFiles } from '@server/l4_foundation/file/l4-project-search'
8
8
  import {
9
9
  getL4WorkSessionGitHeads,
10
10
  getL4WorkSessionGitSnapshot
@@ -20,8 +20,10 @@ import { createL1ProjectRoute } from './l1-project-route'
20
20
  export const projectFileList = createL1ProjectRoute(contracts.L3ProjectFileList, (input) =>
21
21
  listL4WorkSessionProjectFiles(input.cwd, input.path)
22
22
  )
23
- export const projectFileSearch = createL1ProjectRoute(contracts.L3ProjectFileSearch, (input) =>
24
- searchL4WorkSessionProjectFiles(input.cwd, input.query)
23
+ export const projectFileSearch = createL1ProjectRoute(
24
+ contracts.L3ProjectFileSearch,
25
+ ({ cwd, query, scope, caseSensitive, wholeWord, regex }, signal) =>
26
+ searchL4ProjectFiles(cwd, { query, scope, caseSensitive, wholeWord, regex }, signal)
25
27
  )
26
28
  export const projectFileGet = createL1ProjectRoute(contracts.L3ProjectFileGet, (input) =>
27
29
  getL4WorkSessionProjectFile(input.cwd, input.path, input.imagePreviewMode)
@@ -13,7 +13,7 @@ import { createL1WorkSessionGitErrorResponse } from './l1-work-session-git-error
13
13
 
14
14
  export function createL1ProjectRoute<I extends contracts.L3ProjectReadContext, O>(
15
15
  contract: { path: string; request: z.ZodType<I>; response: z.ZodType<O> },
16
- execute: (input: I) => Promise<O>
16
+ execute: (input: I, signal: AbortSignal) => Promise<O>
17
17
  ): (request: Request) => Promise<Response> {
18
18
  return async (request: Request): Promise<Response> => {
19
19
  let payload: unknown
@@ -49,19 +49,20 @@ export function createL1ProjectRoute<I extends contracts.L3ProjectReadContext, O
49
49
  input.cwd = context.cwd
50
50
  isCurrent = context.isCurrent
51
51
  }
52
- const result = await execute(input)
52
+ const result = await execute(input, request.signal)
53
53
  if (isCurrent && !isCurrent())
54
54
  return createL4ApiErrorResponse(409, '工作会话项目已变化', {
55
55
  key: 'errors:projectChanged'
56
56
  })
57
57
  return createL4ApiSuccessResponse(contract.response.parse(result))
58
58
  } catch (error) {
59
- console.warn('[Pi Desk][Project] 项目请求失败', {
60
- path: contract.path,
61
- cwd: input.cwd,
62
- workId: input.workId,
63
- error: error instanceof Error ? error.message : String(error)
64
- })
59
+ if (!request.signal.aborted)
60
+ console.warn('[Pi Desk][Project] 项目请求失败', {
61
+ path: contract.path,
62
+ cwd: input.cwd,
63
+ workId: input.workId,
64
+ error: error instanceof Error ? error.message : String(error)
65
+ })
65
66
  if (getL4WorkSessionGitErrorKind(error) === 'read-failed') {
66
67
  return createL4ApiErrorResponse(
67
68
  500,
@@ -1,6 +1,7 @@
1
1
  import 'server-only'
2
2
 
3
3
  import { getL4WorkSessionProjectFileErrorKind } from '@server/l4_foundation/file/l4-work-session-project-file'
4
+ import { L4ProjectSearchError } from '@server/l4_foundation/file/l4-project-search'
4
5
  import { createL4ApiErrorResponse } from '@server/l4_foundation/http/l4-api-response'
5
6
 
6
7
  export function createL1WorkSessionFileErrorResponse(error: unknown): Response | null {
@@ -11,6 +12,28 @@ export function createL1WorkSessionFileErrorResponse(error: unknown): Response |
11
12
  return createL4ApiErrorResponse(409, '工作会话项目已变化', { key: 'errors:projectChanged' })
12
13
  }
13
14
 
15
+ if (error instanceof Error && error.name === 'AbortError') {
16
+ return createL4ApiErrorResponse(499, '项目搜索已取消', { key: 'errors:projectSearchCancelled' })
17
+ }
18
+ if (error instanceof L4ProjectSearchError) {
19
+ switch (error.kind) {
20
+ case 'invalid-pattern':
21
+ return createL4ApiErrorResponse(400, error.message, {
22
+ key: 'errors:projectSearchPatternInvalid'
23
+ })
24
+ case 'unavailable':
25
+ return createL4ApiErrorResponse(503, error.message, {
26
+ key: 'errors:projectSearchUnavailable'
27
+ })
28
+ case 'timeout':
29
+ return createL4ApiErrorResponse(504, error.message, { key: 'errors:projectSearchTimeout' })
30
+ case 'read-failed':
31
+ return createL4ApiErrorResponse(500, error.message, {
32
+ key: 'errors:projectSearchReadFailed'
33
+ })
34
+ }
35
+ }
36
+
14
37
  switch (getL4WorkSessionProjectFileErrorKind(error)) {
15
38
  case 'not-found':
16
39
  return createL4ApiErrorResponse(404, '文件或目录不存在', { key: 'errors:fileMissing' })
@@ -1,6 +1,7 @@
1
1
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
2
2
  import type { Duplex } from 'node:stream'
3
3
  import { URL } from 'node:url'
4
+ import { L2BuildLoginHref, L2ResolveAuthReturnTo } from '@common/l2_biz/auth/l2-auth-return-to'
4
5
  import {
5
6
  L4_APP_WEBSOCKET_ENDPOINT,
6
7
  L4_APP_WEBSOCKET_PROTOCOL,
@@ -97,15 +98,16 @@ function allowHttpRequest(
97
98
  response: ServerResponse,
98
99
  hostname: string
99
100
  ): boolean {
100
- let pathname: string
101
+ let requestUrl: URL
101
102
  try {
102
- pathname = new URL(request.url ?? '/', `http://${request.headers.host ?? hostname}`).pathname
103
+ requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? hostname}`)
103
104
  } catch {
104
105
  response.statusCode = 400
105
106
  response.end('Bad Request')
106
107
  return false
107
108
  }
108
109
 
110
+ const pathname = requestUrl.pathname
109
111
  if (isPublicHttpPath(pathname) && pathname !== LOGIN_PAGE_PATH) return true
110
112
 
111
113
  const authRuntime = getL1WebAuthRuntimeBridge()
@@ -119,9 +121,11 @@ function allowHttpRequest(
119
121
  }
120
122
  if (pathname === LOGIN_PAGE_PATH) {
121
123
  if (!authenticated) return true
124
+ const returnTo = L2ResolveAuthReturnTo(requestUrl.searchParams.get('returnTo'))
122
125
  response.statusCode = 302
123
126
  response.setHeader('Cache-Control', 'no-store')
124
- response.setHeader('Location', '/')
127
+ // 保留已校验的原路径,避免将 /.//host 规范化为协议相对 Location。
128
+ response.setHeader('Location', encodeURI(returnTo).replace(/%25/g, '%'))
125
129
  response.end()
126
130
  return false
127
131
  }
@@ -146,7 +150,7 @@ function allowHttpRequest(
146
150
 
147
151
  response.statusCode = 302
148
152
  response.setHeader('Cache-Control', 'no-store')
149
- response.setHeader('Location', LOGIN_PAGE_PATH)
153
+ response.setHeader('Location', L2BuildLoginHref(`${requestUrl.pathname}${requestUrl.search}`))
150
154
  response.end()
151
155
  return false
152
156
  }
@@ -112,7 +112,6 @@ interface L2PluginPreparedMutation {
112
112
  }
113
113
 
114
114
  export interface L2PluginManagementWorkSessions {
115
- refreshPluginMessages: () => void
116
115
  runPluginRestart: <T>(operation: () => Promise<T>) => Promise<T>
117
116
  }
118
117
 
@@ -621,7 +620,6 @@ export class L2PluginManagement {
621
620
  if (change.current?.path) clearL4PiPluginNativeDiagnostics(change.current.path)
622
621
  if (change.previous?.path) clearL4PiPluginNativeDiagnostics(change.previous.path)
623
622
  this.capabilityCache = null
624
- this.workSessions.refreshPluginMessages()
625
623
  this.operations.delete(change.source)
626
624
  } catch (error) {
627
625
  this.setOperation(change.source, 'failed', errorMessage(error))
@@ -981,7 +979,6 @@ export class L2PluginManagement {
981
979
  this.updates.clear(operationSource)
982
980
  this.updates.clear(source)
983
981
  this.capabilityCache = null
984
- this.workSessions.refreshPluginMessages()
985
982
  },
986
983
  this.lifetime.signal,
987
984
  waiting
@@ -108,7 +108,6 @@ interface L2WorkSessionChatRecord {
108
108
  source: L2ChatSource
109
109
  state: L2ChatSourceState
110
110
  sourceEventVersion: number
111
- forceFullSync: boolean
112
111
  rawTemporaryMessages: Map<string, L4PiChatMessage>
113
112
  metadata: L2WorkSessionChatMetadata
114
113
  mainRunning: boolean
@@ -302,20 +301,6 @@ export class L2WorkSessionChatRuntime {
302
301
  return isCurrent() ? { cwd, isCurrent } : null
303
302
  }
304
303
 
305
- refreshPluginMessages(): void {
306
- for (const record of this.recordsByWorkId.values()) {
307
- if (record.state.runtime.presentationMode === 'basic') continue
308
- try {
309
- this.reprojectRecord(record, record.state.runtime.presentationMode)
310
- } catch (error) {
311
- console.error('[Pi Desk][WorkSessionChatRuntime] 插件消息刷新失败,保留已有聊天', {
312
- workId: record.source.workId,
313
- message: error instanceof Error ? error.message : String(error)
314
- })
315
- }
316
- }
317
- }
318
-
319
304
  setPresentation(source: L2ChatSource, mode: L2ChatMode): Promise<void> {
320
305
  const record = this.currentRecord(source)
321
306
  return this.enqueueCommand(record, async () => {
@@ -363,7 +348,6 @@ export class L2WorkSessionChatRuntime {
363
348
  temporaryMessages,
364
349
  runtime: { ...record.state.runtime, presentationMode: mode }
365
350
  }
366
- record.forceFullSync = true
367
351
  record.sourceEventVersion += 1
368
352
  this.publish({
369
353
  type: 'source_event',
@@ -380,16 +364,14 @@ export class L2WorkSessionChatRuntime {
380
364
 
381
365
  private async syncRecordRuntime(record: L2WorkSessionChatRecord): Promise<void> {
382
366
  const source = record.source
383
- const runtime = L2ChatRuntimeSchema.parse({
384
- ...(await getL4PiWorkSessionRuntime(source.sessionId).readChatRuntime()),
367
+ const runtime = await getL4PiWorkSessionRuntime(source.sessionId).readChatRuntime()
368
+ const nextRuntime = L2ChatRuntimeSchema.parse({
369
+ ...runtime,
385
370
  presentationMode: record.state.runtime.presentationMode
386
371
  })
387
372
  if (!this.isCurrentSource(source)) return
388
- if (!sameRuntime(record.state.runtime, runtime))
389
- this.applySourceEvent(source, { type: 'runtime_update', runtime })
390
- if (runtime.extensionMode === 'basic' && record.state.runtime.presentationMode !== 'basic') {
391
- this.reprojectRecord(record, 'basic')
392
- }
373
+ if (!sameRuntime(record.state.runtime, nextRuntime))
374
+ this.applySourceEvent(source, { type: 'runtime_update', runtime: nextRuntime })
393
375
  }
394
376
 
395
377
  createSync(sourceInput: L2ChatSource, cursor: L2ChatCursor): L2PreparedChatSourceSync {
@@ -413,24 +395,23 @@ export class L2WorkSessionChatRuntime {
413
395
  })
414
396
  }
415
397
 
416
- const event: L2ChatSessionSyncEvent =
417
- cursor !== null && cursorMatches && !record.forceFullSync
418
- ? {
419
- type: 'session_sync',
420
- mode: 'incremental',
421
- baseCursor: cursor,
422
- messages: messages.slice(cursor.index + 1).map(withoutL3ConversationMessageDetail),
423
- temporaryMessages,
424
- runtime
425
- }
426
- : {
427
- type: 'session_sync',
428
- mode: 'full',
429
- baseCursor: null,
430
- messages: messages.map(withoutL3ConversationMessageDetail),
431
- temporaryMessages,
432
- runtime
433
- }
398
+ const event: L2ChatSessionSyncEvent = cursorMatches
399
+ ? {
400
+ type: 'session_sync',
401
+ mode: 'incremental',
402
+ baseCursor: cursor,
403
+ messages: messages.slice(cursor.index + 1).map(withoutL3ConversationMessageDetail),
404
+ temporaryMessages,
405
+ runtime
406
+ }
407
+ : {
408
+ type: 'session_sync',
409
+ mode: 'full',
410
+ baseCursor: null,
411
+ messages: messages.map(withoutL3ConversationMessageDetail),
412
+ temporaryMessages,
413
+ runtime
414
+ }
434
415
 
435
416
  return { event, watermark: record.sourceEventVersion }
436
417
  }
@@ -548,12 +529,6 @@ export class L2WorkSessionChatRuntime {
548
529
  } finally {
549
530
  if (this.recordsByWorkId.get(source.workId) === record) {
550
531
  await this.syncRecordRuntime(record)
551
- if (
552
- record.state.runtime.extensionMode === 'basic' &&
553
- record.state.runtime.presentationMode !== 'basic'
554
- ) {
555
- this.reprojectRecord(record, 'basic')
556
- }
557
532
  }
558
533
  }
559
534
  })
@@ -685,7 +660,6 @@ export class L2WorkSessionChatRuntime {
685
660
  source,
686
661
  state,
687
662
  sourceEventVersion: 0,
688
- forceFullSync: isL4PiDeskSafeMode(),
689
663
  rawTemporaryMessages: new Map(),
690
664
  metadata: {
691
665
  sessionTitle: snapshot.sessionName ?? firstUserTitle,
@@ -714,6 +688,9 @@ export class L2WorkSessionChatRuntime {
714
688
  if (this.recordsByWorkId.get(record.source.workId) !== record) return
715
689
  const previousMetadata = record.metadata
716
690
  const previousStatus = this.statusForRecord(record)
691
+ const basic =
692
+ record.state.runtime.extensionMode === 'basic' ||
693
+ record.state.runtime.presentationMode === 'basic'
717
694
 
718
695
  switch (event.type) {
719
696
  case 'message_start':
@@ -726,7 +703,7 @@ export class L2WorkSessionChatRuntime {
726
703
  record.source,
727
704
  false,
728
705
  'temporary',
729
- record.state.runtime.presentationMode === 'basic'
706
+ basic
730
707
  )
731
708
  })
732
709
  return
@@ -744,7 +721,7 @@ export class L2WorkSessionChatRuntime {
744
721
  record.source,
745
722
  false,
746
723
  'update',
747
- record.state.runtime.presentationMode === 'basic'
724
+ basic
748
725
  )
749
726
  )
750
727
  if (update) this.applySourceEvent(record.source, update)
@@ -761,7 +738,7 @@ export class L2WorkSessionChatRuntime {
761
738
  record.source,
762
739
  false,
763
740
  'final',
764
- record.state.runtime.presentationMode === 'basic'
741
+ basic
765
742
  )
766
743
  const update = buildL3ConversationMessageUpdate(current, publicFinal)
767
744
  if (update) this.applySourceEvent(record.source, update)
@@ -801,12 +778,6 @@ export class L2WorkSessionChatRuntime {
801
778
  if (!sameRuntime(record.state.runtime, runtime)) {
802
779
  this.applySourceEvent(record.source, { type: 'runtime_update', runtime })
803
780
  }
804
- if (
805
- runtime.extensionMode === 'basic' &&
806
- record.state.runtime.presentationMode !== 'basic'
807
- ) {
808
- this.reprojectRecord(record, 'basic')
809
- }
810
781
  return
811
782
  }
812
783
  case 'agent_started':
@@ -861,7 +832,8 @@ export class L2WorkSessionChatRuntime {
861
832
  record.source,
862
833
  true,
863
834
  'final',
864
- record.state.runtime.presentationMode === 'basic'
835
+ record.state.runtime.extensionMode === 'basic' ||
836
+ record.state.runtime.presentationMode === 'basic'
865
837
  )
866
838
  record.state = { ...record.state, temporaryMessages }
867
839
  }
@@ -250,10 +250,6 @@ export class WorkSessionManage {
250
250
  })
251
251
  }
252
252
 
253
- refreshPluginMessages(): void {
254
- this.chat.refreshPluginMessages()
255
- }
256
-
257
253
  runPluginRestart<T>(operation: () => Promise<T>): Promise<T> {
258
254
  return this.runExclusive(async () => {
259
255
  await this.ensureHydrated()
@@ -0,0 +1,276 @@
1
+ import 'server-only'
2
+
3
+ import { execFile } from 'node:child_process'
4
+ import { readdir, realpath } from 'node:fs/promises'
5
+ import { basename, relative, resolve, sep } from 'node:path'
6
+ import { promisify } from 'node:util'
7
+ import { listL4WorkSessionGitRepositoryRoots } from '@server/l4_foundation/git/l4-work-session-git'
8
+ import { getL4FileSystemKey as fileSystemKey } from './l4-file-path'
9
+
10
+ const execFileAsync = promisify(execFile)
11
+ const SEARCH_CACHE_TTL_MS = 30_000
12
+ const SEARCH_CACHE_MAX_CWDS = 20
13
+ const SEARCH_INDEX_MAX_FILES = 50_000
14
+
15
+ export const IGNORED_NAMES = new Set([
16
+ '.git',
17
+ 'node_modules',
18
+ '.next',
19
+ 'dist',
20
+ 'build',
21
+ 'coverage',
22
+ '.turbo'
23
+ ])
24
+
25
+ interface SearchIndexCacheEntry {
26
+ files: string[]
27
+ truncated: boolean
28
+ expiresAt: number
29
+ }
30
+
31
+ const searchIndexCache = new Map<string, SearchIndexCacheEntry>()
32
+ const pendingSearchIndexes = new Map<string, Promise<SearchIndexCacheEntry>>()
33
+
34
+ function joinProtocolPath(parent: string, name: string): string {
35
+ return parent ? `${parent}/${name}` : name
36
+ }
37
+
38
+ export function isSupportedProtocolName(name: string): boolean {
39
+ return name.length > 0 && !name.includes('/') && !name.includes('\\') && !name.includes('\0')
40
+ }
41
+
42
+ function pathContainsIgnoredSegment(path: string): boolean {
43
+ return path.split('/').some((segment) => IGNORED_NAMES.has(segment))
44
+ }
45
+
46
+ export function invalidateSearchIndex(root: string): void {
47
+ const key = fileSystemKey(root)
48
+ searchIndexCache.delete(key)
49
+ pendingSearchIndexes.delete(key)
50
+ }
51
+
52
+ const GIT_SEARCH_OPTIONS = {
53
+ timeout: 10_000,
54
+ maxBuffer: 64 * 1024 * 1024,
55
+ env: { ...process.env, LC_ALL: 'C', GIT_OPTIONAL_LOCKS: '0' }
56
+ }
57
+
58
+ async function listFilesWithContainingGit(
59
+ root: string
60
+ ): Promise<{ files: string[]; truncated: boolean } | null> {
61
+ try {
62
+ const { stdout: topLevelOutput } = await execFileAsync(
63
+ 'git',
64
+ ['-C', root, 'rev-parse', '--show-toplevel'],
65
+ GIT_SEARCH_OPTIONS
66
+ )
67
+ const topLevel = await realpath(topLevelOutput.trim())
68
+ const prefix = relative(topLevel, root).split(sep).join('/')
69
+ const { stdout } = await execFileAsync(
70
+ 'git',
71
+ [
72
+ '-C',
73
+ topLevel,
74
+ 'ls-files',
75
+ '--full-name',
76
+ '--cached',
77
+ '--others',
78
+ '--exclude-standard',
79
+ '-z',
80
+ '--',
81
+ prefix || '.'
82
+ ],
83
+ GIT_SEARCH_OPTIONS
84
+ )
85
+ const prefixWithSlash = prefix ? `${prefix}/` : ''
86
+ const all = stdout
87
+ .split('\0')
88
+ .flatMap((path) => {
89
+ if (!path) return []
90
+ const projectPath = prefixWithSlash
91
+ ? path.startsWith(prefixWithSlash)
92
+ ? path.slice(prefixWithSlash.length)
93
+ : ''
94
+ : path
95
+ return projectPath ? [projectPath] : []
96
+ })
97
+ .filter(
98
+ (path) =>
99
+ isSupportedProtocolName(basename(path)) &&
100
+ !path.includes('\\') &&
101
+ !pathContainsIgnoredSegment(path)
102
+ )
103
+ return {
104
+ files: all.slice(0, SEARCH_INDEX_MAX_FILES),
105
+ truncated: all.length > SEARCH_INDEX_MAX_FILES
106
+ }
107
+ } catch {
108
+ return null
109
+ }
110
+ }
111
+
112
+ function isPathInsideRepository(path: string, repositoryRoot: string): boolean {
113
+ return path === repositoryRoot || path.startsWith(`${repositoryRoot}/`)
114
+ }
115
+
116
+ async function listFilesInRepository(
117
+ projectRoot: string,
118
+ repositoryRoot: string,
119
+ nestedRepositoryRoots: readonly string[]
120
+ ): Promise<{ files: string[]; truncated: boolean } | null> {
121
+ const absoluteRoot = repositoryRoot
122
+ ? resolve(projectRoot, ...repositoryRoot.split('/'))
123
+ : projectRoot
124
+ try {
125
+ const { stdout } = await execFileAsync(
126
+ 'git',
127
+ [
128
+ '-C',
129
+ absoluteRoot,
130
+ 'ls-files',
131
+ '--full-name',
132
+ '--cached',
133
+ '--others',
134
+ '--exclude-standard',
135
+ '-z'
136
+ ],
137
+ GIT_SEARCH_OPTIONS
138
+ )
139
+ const files = stdout.split('\0').flatMap((path) => {
140
+ if (!path || path.includes('\\') || pathContainsIgnoredSegment(path)) return []
141
+ const projectPath = joinProtocolPath(repositoryRoot, path)
142
+ if (
143
+ nestedRepositoryRoots.some((nestedRoot) => isPathInsideRepository(projectPath, nestedRoot))
144
+ ) {
145
+ return []
146
+ }
147
+ return isSupportedProtocolName(basename(projectPath)) ? [projectPath] : []
148
+ })
149
+ return {
150
+ files: files.slice(0, SEARCH_INDEX_MAX_FILES),
151
+ truncated: files.length > SEARCH_INDEX_MAX_FILES
152
+ }
153
+ } catch {
154
+ return null
155
+ }
156
+ }
157
+
158
+ async function listFilesWithWalk(
159
+ root: string,
160
+ excludedRepositoryRoots: readonly string[] = []
161
+ ): Promise<{ files: string[]; truncated: boolean }> {
162
+ const files: string[] = []
163
+ const queue: Array<{ absolute: string; relative: string }> = [{ absolute: root, relative: '' }]
164
+ let queueIndex = 0
165
+
166
+ while (queueIndex < queue.length) {
167
+ const current = queue[queueIndex]!
168
+ queueIndex += 1
169
+ let dirents
170
+ try {
171
+ dirents = await readdir(current.absolute, { withFileTypes: true })
172
+ } catch {
173
+ continue
174
+ }
175
+
176
+ for (const entry of dirents) {
177
+ if (IGNORED_NAMES.has(entry.name) || !isSupportedProtocolName(entry.name)) continue
178
+ const childRelative = joinProtocolPath(current.relative, entry.name)
179
+ if (entry.isDirectory()) {
180
+ if (excludedRepositoryRoots.includes(childRelative)) continue
181
+ queue.push({ absolute: resolve(current.absolute, entry.name), relative: childRelative })
182
+ continue
183
+ }
184
+ if (!entry.isFile()) continue
185
+ files.push(childRelative)
186
+ if (files.length >= SEARCH_INDEX_MAX_FILES) return { files, truncated: true }
187
+ }
188
+ }
189
+
190
+ return { files, truncated: false }
191
+ }
192
+
193
+ async function listFilesWithRepositoryRoots(
194
+ projectRoot: string,
195
+ repositoryRoots: readonly string[]
196
+ ): Promise<{ files: string[]; truncated: boolean }> {
197
+ const files = new Set<string>()
198
+ let truncated = false
199
+ const hasProjectRootRepository = repositoryRoots.includes('')
200
+
201
+ if (!hasProjectRootRepository) {
202
+ const workspaceFiles = await listFilesWithWalk(projectRoot, repositoryRoots.filter(Boolean))
203
+ for (const path of workspaceFiles.files) files.add(path)
204
+ truncated ||= workspaceFiles.truncated
205
+ }
206
+
207
+ for (const repositoryRoot of repositoryRoots) {
208
+ const nestedRoots = repositoryRoots.filter(
209
+ (candidate) =>
210
+ candidate !== repositoryRoot &&
211
+ (repositoryRoot === '' || candidate.startsWith(`${repositoryRoot}/`))
212
+ )
213
+ let listing = await listFilesInRepository(projectRoot, repositoryRoot, nestedRoots)
214
+ if (!listing) {
215
+ const absoluteRoot = repositoryRoot
216
+ ? resolve(projectRoot, ...repositoryRoot.split('/'))
217
+ : projectRoot
218
+ const nestedRelativeRoots = nestedRoots.map((root) =>
219
+ repositoryRoot ? root.slice(repositoryRoot.length + 1) : root
220
+ )
221
+ const fallback = await listFilesWithWalk(absoluteRoot, nestedRelativeRoots)
222
+ listing = {
223
+ files: fallback.files.map((path) => joinProtocolPath(repositoryRoot, path)),
224
+ truncated: fallback.truncated
225
+ }
226
+ }
227
+ for (const path of listing.files) {
228
+ files.add(path)
229
+ if (files.size >= SEARCH_INDEX_MAX_FILES) {
230
+ return { files: [...files], truncated: true }
231
+ }
232
+ }
233
+ truncated ||= listing.truncated
234
+ }
235
+
236
+ return { files: [...files], truncated }
237
+ }
238
+
239
+ export async function getSearchIndex(root: string): Promise<SearchIndexCacheEntry> {
240
+ const key = fileSystemKey(root)
241
+ const now = Date.now()
242
+ const cached = searchIndexCache.get(key)
243
+ if (cached && cached.expiresAt > now) return cached
244
+
245
+ const active = pendingSearchIndexes.get(key)
246
+ if (active) return active
247
+
248
+ const load: Promise<SearchIndexCacheEntry> = Promise.resolve().then(async () => {
249
+ const repositoryRoots = await listL4WorkSessionGitRepositoryRoots(root)
250
+ const listing =
251
+ repositoryRoots.length > 0
252
+ ? await listFilesWithRepositoryRoots(root, repositoryRoots)
253
+ : ((await listFilesWithContainingGit(root)) ?? (await listFilesWithWalk(root)))
254
+ const next: SearchIndexCacheEntry = {
255
+ ...listing,
256
+ expiresAt: now + SEARCH_CACHE_TTL_MS
257
+ }
258
+ if (pendingSearchIndexes.get(key) === load) {
259
+ for (const [cacheKey, entry] of searchIndexCache) {
260
+ if (entry.expiresAt <= Date.now()) searchIndexCache.delete(cacheKey)
261
+ }
262
+ while (searchIndexCache.size >= SEARCH_CACHE_MAX_CWDS) {
263
+ const oldestKey = searchIndexCache.keys().next().value as string
264
+ searchIndexCache.delete(oldestKey)
265
+ }
266
+ searchIndexCache.set(key, next)
267
+ }
268
+ return next
269
+ })
270
+ pendingSearchIndexes.set(key, load)
271
+ try {
272
+ return await load
273
+ } finally {
274
+ if (pendingSearchIndexes.get(key) === load) pendingSearchIndexes.delete(key)
275
+ }
276
+ }