@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
@@ -0,0 +1,399 @@
1
+ import 'server-only'
2
+
3
+ import { lstat, open, realpath, stat } from 'node:fs/promises'
4
+ import { basename, resolve } from 'node:path'
5
+ import {
6
+ L4_PROJECT_CONTENT_SEARCH_MAX_RESULTS,
7
+ L4_PROJECT_FILE_SEARCH_MAX_RESULTS,
8
+ L4_PROJECT_SEARCH_TEXT_MAX_LENGTH,
9
+ L4_PROJECT_TEXT_FILE_MAX_BYTES,
10
+ L4ProjectFilePathSchema,
11
+ L4ProjectSearchInputSchema,
12
+ type L4ProjectContentMatch,
13
+ type L4ProjectFileMatch,
14
+ type L4ProjectFileSearch,
15
+ type L4ProjectSearchInput,
16
+ type L4ProjectSearchRange
17
+ } from '@common/l4_foundation/file/l4-project-file-contract'
18
+ import { isL4PathInsideRoot } from './l4-file-path'
19
+ import { getSearchIndex } from './l4-project-file-index'
20
+ import { L4WorkSessionProjectFileError } from './l4-work-session-project-file'
21
+ import {
22
+ checkSearchActive,
23
+ L4ProjectSearchError,
24
+ ripgrepPatternArgs,
25
+ ripgrepText,
26
+ runRipgrep,
27
+ type RipgrepBudget,
28
+ type RipgrepMatch
29
+ } from './l4-ripgrep-search'
30
+
31
+ export { L4ProjectSearchError } from './l4-ripgrep-search'
32
+
33
+ // 预留 rg 可执行路径、选项及 Windows 的参数转义开销。
34
+ const MAX_BATCH_ARGUMENT_UNITS = 16_000
35
+ const TEXT_VALIDATION_CONCURRENCY = 16
36
+
37
+ function plainFileMatch(
38
+ path: string,
39
+ input: L4ProjectSearchInput
40
+ ): { match: L4ProjectFileMatch; score: number } | null {
41
+ // 词尾 Σ 等转换依赖上下文,匹配值必须整串折叠。
42
+ const value = input.caseSensitive ? path : path.toLocaleLowerCase()
43
+ const query = input.caseSensitive ? input.query : input.query.toLocaleLowerCase()
44
+ const name = basename(value)
45
+ const nameOffset = value.length - name.length
46
+ let score: number
47
+ let positions: number[]
48
+ if (name === query) {
49
+ score = 0
50
+ positions = Array.from({ length: query.length }, (_, index) => nameOffset + index)
51
+ } else if (name.startsWith(query)) {
52
+ score = 100 + name.length - query.length
53
+ positions = Array.from({ length: query.length }, (_, index) => nameOffset + index)
54
+ } else if (name.includes(query)) {
55
+ const start = name.indexOf(query)
56
+ score = 1_000 + start * 4 + name.length
57
+ positions = Array.from({ length: query.length }, (_, index) => nameOffset + start + index)
58
+ } else if (value.includes(query)) {
59
+ const start = value.indexOf(query)
60
+ score = 10_000 + start * 4 + value.length
61
+ positions = Array.from({ length: query.length }, (_, index) => start + index)
62
+ } else {
63
+ positions = []
64
+ let next = 0
65
+ for (let index = 0; index < value.length && next < query.length; index += 1) {
66
+ if (value[index] === query[next]) {
67
+ positions.push(index)
68
+ next += 1
69
+ }
70
+ }
71
+ if (next !== query.length) return null
72
+ const gaps = positions.at(-1)! - positions[0]! + 1 - query.length
73
+ score = 100_000 + positions[0]! * 4 + gaps + value.length
74
+ }
75
+ const offsets: Array<{ start: number; end: number }> = []
76
+ let offset = 0
77
+ for (const character of path) {
78
+ const folded = input.caseSensitive ? character : character.toLocaleLowerCase()
79
+ for (let index = 0; index < folded.length; index += 1) {
80
+ offsets.push({ start: offset, end: offset + character.length })
81
+ }
82
+ offset += character.length
83
+ }
84
+ const ranges: L4ProjectSearchRange[] = []
85
+ for (const position of positions) {
86
+ const range = offsets[position]!
87
+ const previous = ranges.at(-1)
88
+ if (previous && previous.end >= range.start) previous.end = Math.max(previous.end, range.end)
89
+ else ranges.push({ ...range })
90
+ }
91
+ return { match: { path, ranges }, score }
92
+ }
93
+
94
+ function utf16Ranges(text: string, match: RipgrepMatch): L4ProjectSearchRange[] {
95
+ const bytes = Buffer.from(ripgrepText(match.lines))
96
+ // rg 的命中有序且不重叠,逐段转换避免反复解码整段行前缀。
97
+ let byteOffset = 0
98
+ let utf16Offset = 0
99
+ const convert = (offset: number): number => {
100
+ utf16Offset += bytes.subarray(byteOffset, offset).toString('utf8').length
101
+ byteOffset = offset
102
+ return Math.min(text.length, utf16Offset)
103
+ }
104
+ return match.submatches.map(({ start, end }) => ({ start: convert(start), end: convert(end) }))
105
+ }
106
+
107
+ function contentMatch(path: string, match: RipgrepMatch): L4ProjectContentMatch {
108
+ const text = ripgrepText(match.lines).replace(/\r?\n$/, '')
109
+ const ranges = utf16Ranges(text, match)
110
+ let start = Math.max(0, (ranges[0]?.start ?? 0) - 200)
111
+ let end = Math.min(text.length, start + L4_PROJECT_SEARCH_TEXT_MAX_LENGTH)
112
+ if (start > 0 && /[\uDC00-\uDFFF]/.test(text[start]!) && /[\uD800-\uDBFF]/.test(text[start - 1]!))
113
+ start += 1
114
+ if (
115
+ end < text.length &&
116
+ /[\uD800-\uDBFF]/.test(text[end - 1]!) &&
117
+ /[\uDC00-\uDFFF]/.test(text[end]!)
118
+ )
119
+ end -= 1
120
+ return {
121
+ path,
122
+ line: match.line_number,
123
+ text: text.slice(start, end),
124
+ ranges: ranges
125
+ .filter((range) => range.end >= start && range.start <= end)
126
+ .map((range) => ({
127
+ start: Math.max(start, range.start) - start,
128
+ end: Math.min(end, range.end) - start
129
+ }))
130
+ }
131
+ }
132
+
133
+ async function isSearchableDirectory(
134
+ root: string,
135
+ path: string,
136
+ checks: Map<string, Promise<boolean>>
137
+ ): Promise<boolean> {
138
+ if (!path) return true
139
+ const cached = checks.get(path)
140
+ if (cached) return cached
141
+ const check = (async (): Promise<boolean> => {
142
+ const slash = path.lastIndexOf('/')
143
+ if (!(await isSearchableDirectory(root, slash < 0 ? '' : path.slice(0, slash), checks)))
144
+ return false
145
+ const info = await lstat(resolve(root, path))
146
+ return info.isDirectory() && !info.isSymbolicLink()
147
+ })()
148
+ checks.set(path, check)
149
+ return check
150
+ }
151
+
152
+ async function isSearchableText(
153
+ root: string,
154
+ path: string,
155
+ signal: AbortSignal | undefined,
156
+ budget: RipgrepBudget,
157
+ directoryChecks: Map<string, Promise<boolean>>
158
+ ): Promise<boolean> {
159
+ try {
160
+ checkSearchActive(signal, budget)
161
+ const slash = path.lastIndexOf('/')
162
+ if (
163
+ !(await isSearchableDirectory(root, slash < 0 ? '' : path.slice(0, slash), directoryChecks))
164
+ )
165
+ return false
166
+ checkSearchActive(signal, budget)
167
+ const target = resolve(root, path)
168
+ const info = await lstat(target)
169
+ if (!info.isFile() || info.isSymbolicLink() || info.size > L4_PROJECT_TEXT_FILE_MAX_BYTES)
170
+ return false
171
+ if (!isL4PathInsideRoot(root, await realpath(target))) return false
172
+ checkSearchActive(signal, budget)
173
+ const handle = await open(target, 'r')
174
+ try {
175
+ const decoder = new TextDecoder('utf-8', { fatal: true })
176
+ const buffer = Buffer.alloc(64 * 1024)
177
+ let total = 0
178
+ for (;;) {
179
+ checkSearchActive(signal, budget)
180
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null)
181
+ if (bytesRead === 0) break
182
+ total += bytesRead
183
+ if (total > L4_PROJECT_TEXT_FILE_MAX_BYTES) return false
184
+ const bytes = buffer.subarray(0, bytesRead)
185
+ if (bytes.includes(0)) return false
186
+ try {
187
+ decoder.decode(bytes, { stream: true })
188
+ } catch {
189
+ return false
190
+ }
191
+ }
192
+ try {
193
+ decoder.decode()
194
+ } catch {
195
+ return false
196
+ }
197
+ return true
198
+ } finally {
199
+ await handle.close()
200
+ }
201
+ } catch (error) {
202
+ checkSearchActive(signal, budget)
203
+ if (
204
+ error instanceof Error &&
205
+ 'code' in error &&
206
+ (error.code === 'ENOENT' || error.code === 'ENOTDIR')
207
+ )
208
+ return false
209
+ throw new L4ProjectSearchError('read-failed', `无法读取搜索文件:${path}`)
210
+ }
211
+ }
212
+
213
+ async function waitForIndex(
214
+ root: string,
215
+ signal: AbortSignal | undefined,
216
+ budget: RipgrepBudget
217
+ ): ReturnType<typeof getSearchIndex> {
218
+ checkSearchActive(signal, budget)
219
+ return new Promise((resolveIndex, rejectIndex) => {
220
+ const cleanup = (): void => {
221
+ clearTimeout(timer)
222
+ signal?.removeEventListener('abort', abort)
223
+ }
224
+ const abort = (): void => {
225
+ cleanup()
226
+ rejectIndex(signal?.reason ?? new DOMException('搜索已取消', 'AbortError'))
227
+ }
228
+ const timer = setTimeout(
229
+ () => {
230
+ cleanup()
231
+ rejectIndex(new L4ProjectSearchError('timeout', '项目搜索超时,请缩小搜索范围'))
232
+ },
233
+ Math.max(1, budget.deadline - Date.now())
234
+ )
235
+ signal?.addEventListener('abort', abort, { once: true })
236
+ // 只取消本次等待,不中止其他调用者共用的索引构建。
237
+ getSearchIndex(root).then(resolveIndex, rejectIndex).finally(cleanup)
238
+ })
239
+ }
240
+
241
+ export async function searchL4ProjectFiles(
242
+ cwd: string,
243
+ input: L4ProjectSearchInput,
244
+ signal?: AbortSignal
245
+ ): Promise<L4ProjectFileSearch> {
246
+ const started = Date.now()
247
+ const budget: RipgrepBudget = { deadline: started + 10_000, outputBytes: 0 }
248
+ let stage = 'index'
249
+ try {
250
+ checkSearchActive(signal, budget)
251
+ // HTTP 以外的调用者也遵守公共搜索条件,实际搜索词不 trim。
252
+ if (!L4ProjectSearchInputSchema.safeParse(input).success)
253
+ throw new L4ProjectSearchError('invalid-pattern', '搜索条件无效')
254
+ let root: string
255
+ try {
256
+ root = await realpath(cwd)
257
+ if (!(await stat(root)).isDirectory())
258
+ throw new L4WorkSessionProjectFileError('not-directory', '项目根路径不是目录')
259
+ } catch (error) {
260
+ if (error instanceof L4WorkSessionProjectFileError) throw error
261
+ throw new L4WorkSessionProjectFileError('not-found', '项目目录不存在')
262
+ }
263
+ const index = await waitForIndex(root, signal, budget)
264
+ checkSearchActive(signal, budget)
265
+ const paths = index.files.filter((path) => L4ProjectFilePathSchema.safeParse(path).success)
266
+ const result: L4ProjectFileSearch = {
267
+ fileMatches: [],
268
+ contentMatches: [],
269
+ truncated: index.truncated
270
+ }
271
+ const args = ripgrepPatternArgs(input)
272
+ if (input.scope !== 'content') {
273
+ stage = 'files'
274
+ if (input.regex || input.wholeWord) {
275
+ const candidates = new Set(paths)
276
+ const truncated = await runRipgrep(
277
+ root,
278
+ [...args, '--null-data', '-'],
279
+ budget,
280
+ signal,
281
+ (match) => {
282
+ const path = ripgrepText(match.lines).replace(/\0$/, '')
283
+ if (!candidates.has(path)) return true
284
+ if (result.fileMatches.length >= L4_PROJECT_FILE_SEARCH_MAX_RESULTS) return false
285
+ result.fileMatches.push({ path, ranges: utf16Ranges(path, match) })
286
+ return true
287
+ },
288
+ (function* (): Generator<string> {
289
+ for (const path of paths) yield `${path}\0`
290
+ })()
291
+ )
292
+ result.truncated ||= truncated
293
+ } else {
294
+ const matches = paths
295
+ .flatMap((path) => {
296
+ const match = plainFileMatch(path, input)
297
+ return match ? [match] : []
298
+ })
299
+ .sort(
300
+ (left, right) =>
301
+ left.score - right.score || left.match.path.localeCompare(right.match.path)
302
+ )
303
+ result.fileMatches = matches
304
+ .slice(0, L4_PROJECT_FILE_SEARCH_MAX_RESULTS)
305
+ .map(({ match }) => match)
306
+ result.truncated ||= matches.length > L4_PROJECT_FILE_SEARCH_MAX_RESULTS
307
+ }
308
+ }
309
+ // 输出预算已经耗尽时,不再启动正文批次;文件结果截断不影响正文分支。
310
+ if (input.scope !== 'files' && budget.outputBytes <= 4 * 1024 * 1024) {
311
+ stage = 'content'
312
+ let batch: string[] = []
313
+ let units = 0
314
+ let searched = false
315
+ let stopped = false
316
+ // 只在本次请求复用目录检查;不保存正文或跨请求的校验结论。
317
+ const directoryChecks = new Map<string, Promise<boolean>>()
318
+ const flush = async (): Promise<void> => {
319
+ const validPaths: string[] = []
320
+ for (let offset = 0; offset < batch.length; offset += TEXT_VALIDATION_CONCURRENCY) {
321
+ checkSearchActive(signal, budget)
322
+ const paths = batch.slice(offset, offset + TEXT_VALIDATION_CONCURRENCY)
323
+ // 等待同组全部释放文件句柄后再抛错,不让取消留下后台校验。
324
+ const checked = await Promise.allSettled(
325
+ paths.map((path) => isSearchableText(root, path, signal, budget, directoryChecks))
326
+ )
327
+ for (const [index, entry] of checked.entries()) {
328
+ if (entry.status === 'rejected') throw entry.reason
329
+ if (entry.value) validPaths.push(paths[index]!)
330
+ }
331
+ }
332
+ batch = []
333
+ units = 0
334
+ if (validPaths.length === 0) return
335
+ searched = true
336
+ const candidates = new Set(validPaths)
337
+ stopped = await runRipgrep(
338
+ root,
339
+ [
340
+ ...args,
341
+ '--no-ignore',
342
+ '--hidden',
343
+ '--no-follow',
344
+ '--no-mmap',
345
+ '--threads',
346
+ '1',
347
+ '--encoding',
348
+ 'utf-8',
349
+ '--max-filesize',
350
+ String(L4_PROJECT_TEXT_FILE_MAX_BYTES),
351
+ '--',
352
+ ...validPaths.map((path) => `./${path}`)
353
+ ],
354
+ budget,
355
+ signal,
356
+ (match) => {
357
+ const path = ripgrepText(match.path).replace(/^\.\//, '').replaceAll('\\', '/')
358
+ if (!candidates.has(path) || !L4ProjectFilePathSchema.safeParse(path).success)
359
+ return true
360
+ if (result.contentMatches.length >= L4_PROJECT_CONTENT_SEARCH_MAX_RESULTS) return false
361
+ result.contentMatches.push(contentMatch(path, match))
362
+ return true
363
+ }
364
+ )
365
+ result.truncated ||= stopped
366
+ }
367
+ for (const path of paths) {
368
+ checkSearchActive(signal, budget)
369
+ const cost = Math.max(path.length, Buffer.byteLength(path)) * 2 + 8
370
+ if (units + cost > MAX_BATCH_ARGUMENT_UNITS) await flush()
371
+ if (stopped) break
372
+ batch.push(path)
373
+ units += cost
374
+ }
375
+ if (!stopped) await flush()
376
+ // 空项目同样交给 Rust 引擎验证正则语法。
377
+ if (!searched && input.regex && input.scope === 'content') {
378
+ const truncated = await runRipgrep(root, [...args, '-'], budget, signal, () => true)
379
+ result.truncated ||= truncated
380
+ }
381
+ result.contentMatches.sort(
382
+ (left, right) => left.path.localeCompare(right.path) || left.line - right.line
383
+ )
384
+ }
385
+ checkSearchActive(signal, budget)
386
+ return result
387
+ } catch (error) {
388
+ if (!signal?.aborted && !(error instanceof Error && error.name === 'AbortError')) {
389
+ console.warn('[Pi Desk][Project Search] 项目搜索失败', {
390
+ cwd,
391
+ scope: input.scope,
392
+ stage,
393
+ elapsedMs: Date.now() - started,
394
+ reason: error instanceof Error ? error.message : String(error)
395
+ })
396
+ }
397
+ throw error
398
+ }
399
+ }
@@ -0,0 +1,192 @@
1
+ import 'server-only'
2
+
3
+ import { spawn } from 'node:child_process'
4
+ import { Readable } from 'node:stream'
5
+ import type { L4ProjectSearchInput } from '@common/l4_foundation/file/l4-project-file-contract'
6
+
7
+ const MAX_OUTPUT_BYTES = 4 * 1024 * 1024
8
+ const MAX_RECORD_BYTES = 1024 * 1024
9
+ const MAX_STDERR_BYTES = 64 * 1024
10
+
11
+ export class L4ProjectSearchError extends Error {
12
+ constructor(
13
+ readonly kind: 'invalid-pattern' | 'unavailable' | 'timeout' | 'read-failed',
14
+ message: string
15
+ ) {
16
+ super(message)
17
+ this.name = 'L4ProjectSearchError'
18
+ }
19
+ }
20
+
21
+ export interface RipgrepMatch {
22
+ path: { text?: string; bytes?: string }
23
+ lines: { text?: string; bytes?: string }
24
+ line_number: number
25
+ submatches: Array<{ start: number; end: number }>
26
+ }
27
+
28
+ export interface RipgrepBudget {
29
+ deadline: number
30
+ outputBytes: number
31
+ }
32
+
33
+ export function checkSearchActive(signal: AbortSignal | undefined, budget: RipgrepBudget): void {
34
+ signal?.throwIfAborted()
35
+ if (Date.now() >= budget.deadline) {
36
+ throw new L4ProjectSearchError('timeout', '项目搜索超时,请缩小搜索范围')
37
+ }
38
+ }
39
+
40
+ export function ripgrepPatternArgs(input: L4ProjectSearchInput): string[] {
41
+ return [
42
+ '--no-config',
43
+ '--json',
44
+ input.caseSensitive ? '--case-sensitive' : '--ignore-case',
45
+ ...(input.regex ? [] : ['--fixed-strings']),
46
+ ...(input.wholeWord ? ['--word-regexp'] : []),
47
+ '-e',
48
+ input.query
49
+ ]
50
+ }
51
+
52
+ async function resolveRipgrep(): Promise<string> {
53
+ try {
54
+ // ESM 导入让 Next 保留外部依赖;require 会导致开发编译器继续解析平台二进制。
55
+ return (await import('@vscode/ripgrep')).rgPath
56
+ } catch {
57
+ throw new L4ProjectSearchError('unavailable', '项目搜索引擎不可用,请重新安装完整运行依赖')
58
+ }
59
+ }
60
+
61
+ export async function runRipgrep(
62
+ cwd: string,
63
+ args: string[],
64
+ budget: RipgrepBudget,
65
+ signal: AbortSignal | undefined,
66
+ onMatch: (match: RipgrepMatch) => boolean,
67
+ stdin?: Iterable<string>
68
+ ): Promise<boolean> {
69
+ checkSearchActive(signal, budget)
70
+ const executable = await resolveRipgrep()
71
+ checkSearchActive(signal, budget)
72
+ return new Promise<boolean>((resolveRun, rejectRun) => {
73
+ const child = spawn(executable, args, { cwd, shell: false, windowsHide: true, stdio: 'pipe' })
74
+ let pending = Buffer.alloc(0)
75
+ let stderr = Buffer.alloc(0)
76
+ let truncated = false
77
+ let failure: unknown
78
+ let stopped = false
79
+ const source = stdin ? Readable.from(stdin) : null
80
+
81
+ const stop = (): void => {
82
+ stopped = true
83
+ source?.unpipe(child.stdin)
84
+ source?.destroy()
85
+ child.stdin.destroy()
86
+ child.kill()
87
+ }
88
+ const abort = (): void => {
89
+ failure = signal?.reason ?? new DOMException('搜索已取消', 'AbortError')
90
+ stop()
91
+ }
92
+ const timer = setTimeout(
93
+ () => {
94
+ failure = new L4ProjectSearchError('timeout', '项目搜索超时,请缩小搜索范围')
95
+ stop()
96
+ },
97
+ Math.max(1, budget.deadline - Date.now())
98
+ )
99
+
100
+ const consume = (line: Buffer): void => {
101
+ if (line.length === 0) return
102
+ try {
103
+ const event = JSON.parse(line.toString('utf8')) as { type: string; data: RipgrepMatch }
104
+ if (event.type === 'match' && !onMatch(event.data)) {
105
+ truncated = true
106
+ stop()
107
+ }
108
+ } catch (error) {
109
+ failure =
110
+ error instanceof L4ProjectSearchError
111
+ ? error
112
+ : new L4ProjectSearchError('read-failed', '无法解析项目搜索结果')
113
+ stop()
114
+ }
115
+ }
116
+ child.stdout.on('data', (chunk: Buffer) => {
117
+ if (stopped) return
118
+ const remaining = MAX_OUTPUT_BYTES - budget.outputBytes
119
+ budget.outputBytes += chunk.length
120
+ const accepted = chunk.subarray(0, Math.max(0, remaining))
121
+ pending = Buffer.concat([pending, accepted])
122
+ let end: number
123
+ while (!stopped && (end = pending.indexOf(10)) >= 0) {
124
+ if (end > MAX_RECORD_BYTES) {
125
+ budget.outputBytes = MAX_OUTPUT_BYTES + 1
126
+ truncated = true
127
+ stop()
128
+ break
129
+ }
130
+ consume(pending.subarray(0, end))
131
+ pending = pending.subarray(end + 1)
132
+ }
133
+ if (
134
+ !stopped &&
135
+ (pending.length > MAX_RECORD_BYTES || budget.outputBytes > MAX_OUTPUT_BYTES)
136
+ ) {
137
+ budget.outputBytes = MAX_OUTPUT_BYTES + 1
138
+ truncated = true
139
+ stop()
140
+ }
141
+ if (stopped) pending = Buffer.alloc(0)
142
+ })
143
+ child.stderr.on('data', (chunk: Buffer) => {
144
+ const remaining = MAX_STDERR_BYTES - stderr.length
145
+ if (remaining > 0) stderr = Buffer.concat([stderr, chunk.subarray(0, remaining)])
146
+ })
147
+ child.stdin.on('error', (error: NodeJS.ErrnoException) => {
148
+ // rg 可因非法正则先关闭 stdin,真实原因由 close 的退出码与 stderr 决定。
149
+ if (error.code !== 'EPIPE' && error.code !== 'ERR_STREAM_DESTROYED' && !stopped) {
150
+ failure = new L4ProjectSearchError('read-failed', '无法向项目搜索引擎传递文件列表')
151
+ stop()
152
+ }
153
+ })
154
+ child.once('error', () => {
155
+ failure = new L4ProjectSearchError('unavailable', '项目搜索引擎无法启动,请检查运行依赖')
156
+ })
157
+ child.once('close', (code) => {
158
+ clearTimeout(timer)
159
+ signal?.removeEventListener('abort', abort)
160
+ source?.unpipe(child.stdin)
161
+ source?.destroy()
162
+ child.stdin.destroy()
163
+ if (!stopped && pending.length > 0) consume(pending)
164
+ pending = Buffer.alloc(0)
165
+ const diagnostic = stderr.toString('utf8')
166
+ stderr = Buffer.alloc(0)
167
+ if (failure) rejectRun(failure)
168
+ else if (truncated || code === 0 || code === 1) resolveRun(truncated)
169
+ else if (
170
+ /regex parse error|regex syntax error|error parsing.*regex|compiled regex exceeds size limit/i.test(
171
+ diagnostic
172
+ )
173
+ ) {
174
+ rejectRun(new L4ProjectSearchError('invalid-pattern', '正则表达式无效或包含不支持的语法'))
175
+ } else
176
+ rejectRun(
177
+ new L4ProjectSearchError(
178
+ 'read-failed',
179
+ `项目搜索读取失败:${diagnostic.trim() || `退出码 ${code}`}`
180
+ )
181
+ )
182
+ })
183
+ signal?.addEventListener('abort', abort, { once: true })
184
+ if (signal?.aborted) abort()
185
+ else if (source) source.pipe(child.stdin)
186
+ else child.stdin.end()
187
+ })
188
+ }
189
+
190
+ export function ripgrepText(value: { text?: string; bytes?: string }): string {
191
+ return value.text ?? Buffer.from(value.bytes ?? '', 'base64').toString('utf8')
192
+ }