@stigmer/react 3.1.1 → 3.1.2

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 (426) hide show
  1. package/composer/SessionComposer.d.ts +24 -1
  2. package/composer/SessionComposer.d.ts.map +1 -1
  3. package/composer/SessionComposer.js +19 -8
  4. package/composer/SessionComposer.js.map +1 -1
  5. package/execution/ArtifactContentRenderer.d.ts +11 -1
  6. package/execution/ArtifactContentRenderer.d.ts.map +1 -1
  7. package/execution/ArtifactContentRenderer.js +39 -15
  8. package/execution/ArtifactContentRenderer.js.map +1 -1
  9. package/execution/FilePathContext.d.ts +8 -1
  10. package/execution/FilePathContext.d.ts.map +1 -1
  11. package/execution/FilePathContext.js.map +1 -1
  12. package/execution/FilePathLink.d.ts.map +1 -1
  13. package/execution/FilePathLink.js +21 -6
  14. package/execution/FilePathLink.js.map +1 -1
  15. package/execution/MessageEntry.d.ts +29 -0
  16. package/execution/MessageEntry.d.ts.map +1 -1
  17. package/execution/MessageEntry.js +29 -5
  18. package/execution/MessageEntry.js.map +1 -1
  19. package/execution/MessageThread.d.ts +76 -13
  20. package/execution/MessageThread.d.ts.map +1 -1
  21. package/execution/MessageThread.js +178 -24
  22. package/execution/MessageThread.js.map +1 -1
  23. package/execution/PlanArtifactCard.d.ts +27 -18
  24. package/execution/PlanArtifactCard.d.ts.map +1 -1
  25. package/execution/PlanArtifactCard.js +13 -23
  26. package/execution/PlanArtifactCard.js.map +1 -1
  27. package/execution/PlanDocumentMessage.d.ts +32 -0
  28. package/execution/PlanDocumentMessage.d.ts.map +1 -0
  29. package/execution/PlanDocumentMessage.js +37 -0
  30. package/execution/PlanDocumentMessage.js.map +1 -0
  31. package/execution/PlanStreamingCard.d.ts +45 -0
  32. package/execution/PlanStreamingCard.d.ts.map +1 -0
  33. package/execution/PlanStreamingCard.js +35 -0
  34. package/execution/PlanStreamingCard.js.map +1 -0
  35. package/execution/index.d.ts +5 -1
  36. package/execution/index.d.ts.map +1 -1
  37. package/execution/index.js +2 -0
  38. package/execution/index.js.map +1 -1
  39. package/execution/useCreateAgentExecution.d.ts +10 -0
  40. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  41. package/execution/useCreateAgentExecution.js +5 -1
  42. package/execution/useCreateAgentExecution.js.map +1 -1
  43. package/github/decodeGitHubContent.d.ts +29 -0
  44. package/github/decodeGitHubContent.d.ts.map +1 -0
  45. package/github/decodeGitHubContent.js +72 -0
  46. package/github/decodeGitHubContent.js.map +1 -0
  47. package/github/index.d.ts +1 -0
  48. package/github/index.d.ts.map +1 -1
  49. package/github/index.js +1 -0
  50. package/github/index.js.map +1 -1
  51. package/github/useGitHubFileReader.d.ts +26 -0
  52. package/github/useGitHubFileReader.d.ts.map +1 -0
  53. package/github/useGitHubFileReader.js +106 -0
  54. package/github/useGitHubFileReader.js.map +1 -0
  55. package/github/useGitHubTreeLister.d.ts.map +1 -1
  56. package/github/useGitHubTreeLister.js +6 -2
  57. package/github/useGitHubTreeLister.js.map +1 -1
  58. package/index.d.ts +9 -9
  59. package/index.d.ts.map +1 -1
  60. package/index.js +5 -5
  61. package/index.js.map +1 -1
  62. package/internal/ResizableSplit.d.ts +72 -22
  63. package/internal/ResizableSplit.d.ts.map +1 -1
  64. package/internal/ResizableSplit.js +96 -38
  65. package/internal/ResizableSplit.js.map +1 -1
  66. package/internal/VirtualizedThread.d.ts +5 -3
  67. package/internal/VirtualizedThread.d.ts.map +1 -1
  68. package/internal/VirtualizedThread.js +6 -4
  69. package/internal/VirtualizedThread.js.map +1 -1
  70. package/internal/file-icons/index.d.ts +17 -0
  71. package/internal/file-icons/index.d.ts.map +1 -0
  72. package/internal/file-icons/index.js +60 -0
  73. package/internal/file-icons/index.js.map +1 -0
  74. package/internal/file-tree/FileTreeNode.d.ts +18 -1
  75. package/internal/file-tree/FileTreeNode.d.ts.map +1 -1
  76. package/internal/file-tree/FileTreeNode.js +11 -2
  77. package/internal/file-tree/FileTreeNode.js.map +1 -1
  78. package/internal/markdown-components.d.ts +38 -1
  79. package/internal/markdown-components.d.ts.map +1 -1
  80. package/internal/markdown-components.js +86 -11
  81. package/internal/markdown-components.js.map +1 -1
  82. package/{workflow → internal}/motion-preference.d.ts +7 -4
  83. package/internal/motion-preference.d.ts.map +1 -0
  84. package/{workflow → internal}/motion-preference.js +7 -4
  85. package/internal/motion-preference.js.map +1 -0
  86. package/internal/store/index.d.ts +33 -0
  87. package/internal/store/index.d.ts.map +1 -1
  88. package/internal/store/index.js +59 -0
  89. package/internal/store/index.js.map +1 -1
  90. package/internal/store/workspace-editors-store.d.ts +145 -0
  91. package/internal/store/workspace-editors-store.d.ts.map +1 -0
  92. package/internal/store/workspace-editors-store.js +208 -0
  93. package/internal/store/workspace-editors-store.js.map +1 -0
  94. package/internal/store/workspace-file-selection-store.d.ts +38 -0
  95. package/internal/store/workspace-file-selection-store.d.ts.map +1 -0
  96. package/internal/store/workspace-file-selection-store.js +62 -0
  97. package/internal/store/workspace-file-selection-store.js.map +1 -0
  98. package/internal/useRevealLine.d.ts +49 -0
  99. package/internal/useRevealLine.d.ts.map +1 -0
  100. package/internal/useRevealLine.js +43 -0
  101. package/internal/useRevealLine.js.map +1 -0
  102. package/library/detect-plan-artifact.d.ts +21 -0
  103. package/library/detect-plan-artifact.d.ts.map +1 -1
  104. package/library/detect-plan-artifact.js +20 -0
  105. package/library/detect-plan-artifact.js.map +1 -1
  106. package/library/detect-streaming-plan.d.ts +45 -0
  107. package/library/detect-streaming-plan.d.ts.map +1 -0
  108. package/library/detect-streaming-plan.js +83 -0
  109. package/library/detect-streaming-plan.js.map +1 -0
  110. package/library/index.d.ts +4 -1
  111. package/library/index.d.ts.map +1 -1
  112. package/library/index.js +2 -1
  113. package/library/index.js.map +1 -1
  114. package/package.json +4 -4
  115. package/session/NewSessionViewer.d.ts +19 -4
  116. package/session/NewSessionViewer.d.ts.map +1 -1
  117. package/session/NewSessionViewer.js +52 -25
  118. package/session/NewSessionViewer.js.map +1 -1
  119. package/session/PlanEditor.d.ts +55 -0
  120. package/session/PlanEditor.d.ts.map +1 -0
  121. package/session/PlanEditor.js +88 -0
  122. package/session/PlanEditor.js.map +1 -0
  123. package/session/PlanStreamingDocument.d.ts +30 -0
  124. package/session/PlanStreamingDocument.d.ts.map +1 -0
  125. package/session/PlanStreamingDocument.js +37 -0
  126. package/session/PlanStreamingDocument.js.map +1 -0
  127. package/session/SessionPanelChip.d.ts +26 -0
  128. package/session/SessionPanelChip.d.ts.map +1 -0
  129. package/session/SessionPanelChip.js +40 -0
  130. package/session/SessionPanelChip.js.map +1 -0
  131. package/session/SessionViewer.d.ts +34 -7
  132. package/session/SessionViewer.d.ts.map +1 -1
  133. package/session/SessionViewer.js +315 -33
  134. package/session/SessionViewer.js.map +1 -1
  135. package/session/{inspector → facets}/ArtifactsTab.d.ts +10 -2
  136. package/session/facets/ArtifactsTab.d.ts.map +1 -0
  137. package/session/{inspector → facets}/ArtifactsTab.js +8 -3
  138. package/session/facets/ArtifactsTab.js.map +1 -0
  139. package/session/{inspector → facets}/ChangesTab.d.ts +3 -3
  140. package/session/facets/ChangesTab.d.ts.map +1 -0
  141. package/session/{inspector → facets}/ChangesTab.js +3 -3
  142. package/session/facets/ChangesTab.js.map +1 -0
  143. package/session/facets/InspectTab.d.ts.map +1 -0
  144. package/session/facets/InspectTab.js.map +1 -0
  145. package/session/{inspector → facets}/SetupTab.d.ts +11 -2
  146. package/session/facets/SetupTab.d.ts.map +1 -0
  147. package/session/{inspector → facets}/SetupTab.js +4 -3
  148. package/session/facets/SetupTab.js.map +1 -0
  149. package/session/{inspector → facets}/UsageTab.d.ts +1 -1
  150. package/session/facets/UsageTab.d.ts.map +1 -0
  151. package/session/{inspector → facets}/UsageTab.js +1 -1
  152. package/session/facets/UsageTab.js.map +1 -0
  153. package/session/{inspector → facets}/index.d.ts +0 -6
  154. package/session/facets/index.d.ts.map +1 -0
  155. package/session/{inspector → facets}/index.js +0 -3
  156. package/session/facets/index.js.map +1 -0
  157. package/session/index.d.ts +11 -2
  158. package/session/index.d.ts.map +1 -1
  159. package/session/index.js +15 -2
  160. package/session/index.js.map +1 -1
  161. package/session/plan-document.d.ts +9 -0
  162. package/session/plan-document.d.ts.map +1 -0
  163. package/session/plan-document.js +17 -0
  164. package/session/plan-document.js.map +1 -0
  165. package/session/useOpenFileChange.d.ts +20 -0
  166. package/session/useOpenFileChange.d.ts.map +1 -0
  167. package/session/useOpenFileChange.js +31 -0
  168. package/session/useOpenFileChange.js.map +1 -0
  169. package/session/usePlanDraft.d.ts +52 -0
  170. package/session/usePlanDraft.d.ts.map +1 -0
  171. package/session/usePlanDraft.js +63 -0
  172. package/session/usePlanDraft.js.map +1 -0
  173. package/session/useSessionConversation.d.ts +6 -0
  174. package/session/useSessionConversation.d.ts.map +1 -1
  175. package/session/useSessionConversation.js +1 -0
  176. package/session/useSessionConversation.js.map +1 -1
  177. package/session/useSessionPageFlow.d.ts.map +1 -1
  178. package/session/useSessionPageFlow.js +1 -0
  179. package/session/useSessionPageFlow.js.map +1 -1
  180. package/session/useSessionPanel.d.ts +120 -0
  181. package/session/useSessionPanel.d.ts.map +1 -0
  182. package/session/useSessionPanel.js +130 -0
  183. package/session/useSessionPanel.js.map +1 -0
  184. package/session/useSessionRailViews.d.ts +45 -0
  185. package/session/useSessionRailViews.d.ts.map +1 -0
  186. package/session/useSessionRailViews.js +105 -0
  187. package/session/useSessionRailViews.js.map +1 -0
  188. package/src/composer/SessionComposer.tsx +52 -10
  189. package/src/composer/__tests__/SessionComposer-contract.test.tsx +58 -4
  190. package/src/execution/ArtifactContentRenderer.tsx +101 -24
  191. package/src/execution/FilePathContext.tsx +8 -1
  192. package/src/execution/FilePathLink.tsx +22 -6
  193. package/src/execution/MessageEntry.tsx +111 -1
  194. package/src/execution/MessageThread.tsx +291 -36
  195. package/src/execution/PlanArtifactCard.tsx +63 -31
  196. package/src/execution/PlanDocumentMessage.tsx +96 -0
  197. package/src/execution/PlanStreamingCard.tsx +137 -0
  198. package/src/execution/__tests__/ArtifactContentRenderer.test.tsx +80 -1
  199. package/src/execution/__tests__/FilePathLink.test.tsx +87 -0
  200. package/src/execution/__tests__/MessageThread.test.tsx +101 -0
  201. package/src/execution/__tests__/PlanArtifactCard.test.tsx +66 -12
  202. package/src/execution/__tests__/PlanDocumentMessage.test.tsx +55 -0
  203. package/src/execution/__tests__/PlanStreamingCard.test.tsx +49 -0
  204. package/src/execution/__tests__/buildThreadItems-plan-completion.test.ts +504 -22
  205. package/src/execution/__tests__/message-entry.test.tsx +48 -0
  206. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +85 -0
  207. package/src/execution/index.ts +7 -1
  208. package/src/execution/useCreateAgentExecution.ts +16 -1
  209. package/src/github/__tests__/decodeGitHubContent.test.ts +102 -0
  210. package/src/github/__tests__/useGitHubFileReader.test.ts +183 -0
  211. package/src/github/decodeGitHubContent.ts +83 -0
  212. package/src/github/index.ts +1 -0
  213. package/src/github/useGitHubFileReader.ts +152 -0
  214. package/src/github/useGitHubTreeLister.ts +6 -2
  215. package/src/index.ts +63 -9
  216. package/src/internal/ResizableSplit.tsx +184 -51
  217. package/src/internal/VirtualizedThread.tsx +12 -4
  218. package/src/internal/__tests__/ResizableSplit.test.tsx +125 -3
  219. package/src/internal/__tests__/markdown-components.test.tsx +68 -0
  220. package/src/internal/__tests__/useRevealLine.test.tsx +97 -0
  221. package/src/internal/file-icons/__tests__/file-icons.test.tsx +35 -0
  222. package/src/internal/file-icons/index.tsx +137 -0
  223. package/src/internal/file-tree/FileTreeNode.tsx +43 -3
  224. package/src/internal/file-tree/__tests__/FileTreeNode-explorer.test.tsx +86 -0
  225. package/src/internal/markdown-components.tsx +116 -11
  226. package/src/{workflow → internal}/motion-preference.ts +7 -4
  227. package/src/internal/store/__tests__/workspace-editors-store.test.ts +216 -0
  228. package/src/internal/store/__tests__/workspace-file-selection-store.test.ts +118 -0
  229. package/src/internal/store/index.ts +94 -0
  230. package/src/internal/store/workspace-editors-store.ts +315 -0
  231. package/src/internal/store/workspace-file-selection-store.ts +88 -0
  232. package/src/internal/useRevealLine.ts +81 -0
  233. package/src/library/__tests__/detect-plan-artifact.test.ts +48 -0
  234. package/src/library/__tests__/detect-streaming-plan.test.ts +234 -0
  235. package/src/library/detect-plan-artifact.ts +35 -0
  236. package/src/library/detect-streaming-plan.ts +108 -0
  237. package/src/library/index.ts +5 -0
  238. package/src/session/NewSessionViewer.tsx +123 -55
  239. package/src/session/PlanEditor.tsx +361 -0
  240. package/src/session/PlanStreamingDocument.tsx +97 -0
  241. package/src/session/SessionPanelChip.tsx +103 -0
  242. package/src/session/SessionViewer.tsx +617 -90
  243. package/src/session/__tests__/NewSessionViewer.test.tsx +148 -0
  244. package/src/session/__tests__/PlanEditor.test.tsx +217 -0
  245. package/src/session/__tests__/PlanStreamingDocument.test.tsx +50 -0
  246. package/src/session/__tests__/SessionPanelChip.test.tsx +42 -0
  247. package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +9 -4
  248. package/src/session/__tests__/SessionViewer-transcriptFileOpen.test.tsx +205 -0
  249. package/src/session/__tests__/audienceWiring.test.tsx +55 -19
  250. package/src/session/__tests__/useOpenFileChange.test.tsx +82 -0
  251. package/src/session/__tests__/usePlanDraft.test.ts +102 -0
  252. package/src/session/__tests__/useSessionPanel.test.ts +298 -0
  253. package/src/session/__tests__/useSessionRailViews.test.tsx +124 -0
  254. package/src/session/{inspector → facets}/ArtifactsTab.tsx +27 -5
  255. package/src/session/{inspector → facets}/ChangesTab.tsx +3 -3
  256. package/src/session/{inspector → facets}/SetupTab.tsx +13 -1
  257. package/src/session/{inspector → facets}/UsageTab.tsx +1 -1
  258. package/src/session/{inspector → facets}/__tests__/ArtifactsTab.test.tsx +35 -1
  259. package/src/session/facets/__tests__/SetupTab.test.tsx +44 -0
  260. package/src/session/{inspector → facets}/index.ts +0 -16
  261. package/src/session/index.ts +30 -13
  262. package/src/session/plan-document.ts +19 -0
  263. package/src/session/useOpenFileChange.ts +53 -0
  264. package/src/session/usePlanDraft.ts +109 -0
  265. package/src/session/useSessionConversation.ts +7 -0
  266. package/src/session/useSessionPageFlow.ts +1 -0
  267. package/src/session/useSessionPanel.ts +288 -0
  268. package/src/session/useSessionRailViews.tsx +200 -0
  269. package/src/workflow/WorkflowExecutionGraph.tsx +1 -1
  270. package/src/workflow/index.ts +1 -1
  271. package/src/workflow/useFollowExecution.ts +1 -1
  272. package/src/workspace/EditorTabs.tsx +145 -0
  273. package/src/workspace/ExplorerTree.tsx +247 -0
  274. package/src/workspace/FileViewer.tsx +516 -0
  275. package/src/workspace/WorkspaceContentSearch.tsx +478 -0
  276. package/src/workspace/WorkspaceContentSearcher.ts +70 -0
  277. package/src/workspace/WorkspaceFileLister.ts +11 -0
  278. package/src/workspace/WorkspaceFileReader.ts +57 -0
  279. package/src/workspace/WorkspaceFileSearch.tsx +429 -0
  280. package/src/workspace/WorkspaceSurface.tsx +781 -0
  281. package/src/workspace/__tests__/EditorTabs.test.tsx +100 -0
  282. package/src/workspace/__tests__/ExplorerTree.test.tsx +152 -0
  283. package/src/workspace/__tests__/FileViewer.test.tsx +421 -0
  284. package/src/workspace/__tests__/WorkspaceContentSearch.test.tsx +196 -0
  285. package/src/workspace/__tests__/WorkspaceFileSearch.test.tsx +206 -0
  286. package/src/workspace/__tests__/WorkspaceSurface.test.tsx +348 -0
  287. package/src/workspace/__tests__/findChangeForSelection.test.ts +132 -0
  288. package/src/workspace/__tests__/matchContentPreview.test.ts +55 -0
  289. package/src/workspace/__tests__/matchWorkspaceFiles.test.ts +104 -0
  290. package/src/workspace/__tests__/resolveWorkspaceFileSelection.test.ts +135 -0
  291. package/src/workspace/__tests__/useWorkspaceContentSearch.test.tsx +196 -0
  292. package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +190 -0
  293. package/src/workspace/__tests__/useWorkspaceFileSearch.test.tsx +162 -0
  294. package/src/workspace/findChangeForSelection.ts +61 -0
  295. package/src/workspace/index.ts +56 -2
  296. package/src/workspace/matchContentPreview.ts +51 -0
  297. package/src/workspace/matchWorkspaceFiles.ts +86 -0
  298. package/src/workspace/resolveWorkspaceFileSelection.ts +119 -0
  299. package/src/workspace/useWorkspaceContentSearch.ts +228 -0
  300. package/src/workspace/useWorkspaceFileContent.ts +129 -0
  301. package/src/workspace/useWorkspaceFileSearch.ts +184 -0
  302. package/src/workspace/useWorkspaceFiles.ts +27 -27
  303. package/src/workspace/workspaceListingCache.ts +93 -0
  304. package/styles.css +1 -1
  305. package/workflow/WorkflowExecutionGraph.js +1 -1
  306. package/workflow/WorkflowExecutionGraph.js.map +1 -1
  307. package/workflow/index.d.ts +1 -1
  308. package/workflow/index.d.ts.map +1 -1
  309. package/workflow/index.js +1 -1
  310. package/workflow/index.js.map +1 -1
  311. package/workflow/useFollowExecution.js +1 -1
  312. package/workflow/useFollowExecution.js.map +1 -1
  313. package/workspace/EditorTabs.d.ts +29 -0
  314. package/workspace/EditorTabs.d.ts.map +1 -0
  315. package/workspace/EditorTabs.js +66 -0
  316. package/workspace/EditorTabs.js.map +1 -0
  317. package/workspace/ExplorerTree.d.ts +32 -0
  318. package/workspace/ExplorerTree.d.ts.map +1 -0
  319. package/workspace/ExplorerTree.js +45 -0
  320. package/workspace/ExplorerTree.js.map +1 -0
  321. package/workspace/FileViewer.d.ts +93 -0
  322. package/workspace/FileViewer.d.ts.map +1 -0
  323. package/workspace/FileViewer.js +182 -0
  324. package/workspace/FileViewer.js.map +1 -0
  325. package/workspace/WorkspaceContentSearch.d.ts +41 -0
  326. package/workspace/WorkspaceContentSearch.d.ts.map +1 -0
  327. package/workspace/WorkspaceContentSearch.js +195 -0
  328. package/workspace/WorkspaceContentSearch.js.map +1 -0
  329. package/workspace/WorkspaceContentSearcher.d.ts +59 -0
  330. package/workspace/WorkspaceContentSearcher.d.ts.map +1 -0
  331. package/workspace/WorkspaceContentSearcher.js +7 -0
  332. package/workspace/WorkspaceContentSearcher.js.map +1 -0
  333. package/workspace/WorkspaceFileLister.d.ts +11 -0
  334. package/workspace/WorkspaceFileLister.d.ts.map +1 -1
  335. package/workspace/WorkspaceFileReader.d.ts +48 -0
  336. package/workspace/WorkspaceFileReader.d.ts.map +1 -0
  337. package/workspace/WorkspaceFileReader.js +11 -0
  338. package/workspace/WorkspaceFileReader.js.map +1 -0
  339. package/workspace/WorkspaceFileSearch.d.ts +32 -0
  340. package/workspace/WorkspaceFileSearch.d.ts.map +1 -0
  341. package/workspace/WorkspaceFileSearch.js +155 -0
  342. package/workspace/WorkspaceFileSearch.js.map +1 -0
  343. package/workspace/WorkspaceSurface.d.ts +151 -0
  344. package/workspace/WorkspaceSurface.d.ts.map +1 -0
  345. package/workspace/WorkspaceSurface.js +184 -0
  346. package/workspace/WorkspaceSurface.js.map +1 -0
  347. package/workspace/findChangeForSelection.d.ts +19 -0
  348. package/workspace/findChangeForSelection.d.ts.map +1 -0
  349. package/workspace/findChangeForSelection.js +46 -0
  350. package/workspace/findChangeForSelection.js.map +1 -0
  351. package/workspace/index.d.ts +24 -2
  352. package/workspace/index.d.ts.map +1 -1
  353. package/workspace/index.js +13 -1
  354. package/workspace/index.js.map +1 -1
  355. package/workspace/matchContentPreview.d.ts +21 -0
  356. package/workspace/matchContentPreview.d.ts.map +1 -0
  357. package/workspace/matchContentPreview.js +36 -0
  358. package/workspace/matchContentPreview.js.map +1 -0
  359. package/workspace/matchWorkspaceFiles.d.ts +30 -0
  360. package/workspace/matchWorkspaceFiles.d.ts.map +1 -0
  361. package/workspace/matchWorkspaceFiles.js +58 -0
  362. package/workspace/matchWorkspaceFiles.js.map +1 -0
  363. package/workspace/resolveWorkspaceFileSelection.d.ts +19 -0
  364. package/workspace/resolveWorkspaceFileSelection.d.ts.map +1 -0
  365. package/workspace/resolveWorkspaceFileSelection.js +115 -0
  366. package/workspace/resolveWorkspaceFileSelection.js.map +1 -0
  367. package/workspace/useWorkspaceContentSearch.d.ts +69 -0
  368. package/workspace/useWorkspaceContentSearch.d.ts.map +1 -0
  369. package/workspace/useWorkspaceContentSearch.js +154 -0
  370. package/workspace/useWorkspaceContentSearch.js.map +1 -0
  371. package/workspace/useWorkspaceFileContent.d.ts +73 -0
  372. package/workspace/useWorkspaceFileContent.d.ts.map +1 -0
  373. package/workspace/useWorkspaceFileContent.js +57 -0
  374. package/workspace/useWorkspaceFileContent.js.map +1 -0
  375. package/workspace/useWorkspaceFileSearch.d.ts +61 -0
  376. package/workspace/useWorkspaceFileSearch.d.ts.map +1 -0
  377. package/workspace/useWorkspaceFileSearch.js +119 -0
  378. package/workspace/useWorkspaceFileSearch.js.map +1 -0
  379. package/workspace/useWorkspaceFiles.d.ts +13 -5
  380. package/workspace/useWorkspaceFiles.d.ts.map +1 -1
  381. package/workspace/useWorkspaceFiles.js +18 -19
  382. package/workspace/useWorkspaceFiles.js.map +1 -1
  383. package/workspace/workspaceListingCache.d.ts +47 -0
  384. package/workspace/workspaceListingCache.d.ts.map +1 -0
  385. package/workspace/workspaceListingCache.js +55 -0
  386. package/workspace/workspaceListingCache.js.map +1 -0
  387. package/session/inspector/ArtifactsTab.d.ts.map +0 -1
  388. package/session/inspector/ArtifactsTab.js.map +0 -1
  389. package/session/inspector/ChangesTab.d.ts.map +0 -1
  390. package/session/inspector/ChangesTab.js.map +0 -1
  391. package/session/inspector/InspectTab.d.ts.map +0 -1
  392. package/session/inspector/InspectTab.js.map +0 -1
  393. package/session/inspector/SessionInspector.d.ts +0 -64
  394. package/session/inspector/SessionInspector.d.ts.map +0 -1
  395. package/session/inspector/SessionInspector.js +0 -53
  396. package/session/inspector/SessionInspector.js.map +0 -1
  397. package/session/inspector/SetupTab.d.ts.map +0 -1
  398. package/session/inspector/SetupTab.js.map +0 -1
  399. package/session/inspector/UsageTab.d.ts.map +0 -1
  400. package/session/inspector/UsageTab.js.map +0 -1
  401. package/session/inspector/WorkspaceTab.d.ts +0 -35
  402. package/session/inspector/WorkspaceTab.d.ts.map +0 -1
  403. package/session/inspector/WorkspaceTab.js +0 -75
  404. package/session/inspector/WorkspaceTab.js.map +0 -1
  405. package/session/inspector/index.d.ts.map +0 -1
  406. package/session/inspector/index.js.map +0 -1
  407. package/session/inspector/useSessionInspector.d.ts +0 -49
  408. package/session/inspector/useSessionInspector.d.ts.map +0 -1
  409. package/session/inspector/useSessionInspector.js +0 -122
  410. package/session/inspector/useSessionInspector.js.map +0 -1
  411. package/src/session/inspector/SessionInspector.tsx +0 -162
  412. package/src/session/inspector/WorkspaceTab.tsx +0 -245
  413. package/src/session/inspector/__tests__/useSessionInspector.test.ts +0 -251
  414. package/src/session/inspector/useSessionInspector.ts +0 -178
  415. package/src/workspace/WorkspaceEntryFiles.tsx +0 -196
  416. package/workflow/motion-preference.d.ts.map +0 -1
  417. package/workflow/motion-preference.js.map +0 -1
  418. package/workspace/WorkspaceEntryFiles.d.ts +0 -22
  419. package/workspace/WorkspaceEntryFiles.d.ts.map +0 -1
  420. package/workspace/WorkspaceEntryFiles.js +0 -58
  421. package/workspace/WorkspaceEntryFiles.js.map +0 -1
  422. /package/session/{inspector → facets}/InspectTab.d.ts +0 -0
  423. /package/session/{inspector → facets}/InspectTab.js +0 -0
  424. /package/src/{workflow → internal}/__tests__/motion-preference.test.ts +0 -0
  425. /package/src/session/{inspector → facets}/InspectTab.tsx +0 -0
  426. /package/src/session/{inspector → facets}/__tests__/ChangesTab.test.tsx +0 -0
@@ -0,0 +1,61 @@
1
+ // Correlate an open workspace-file selection with the session file change that
2
+ // touched it, so the read-only Viewer can default a changed file to its diff
3
+ // (Slice 4). This is the change-side peer of `resolveWorkspaceFileSelection`.
4
+ //
5
+ // The hard part is again PATH FORM, and — critically — ENTRY IDENTITY. A
6
+ // `FileChange` carries a capture-side path (`path` is workspace-root-relative,
7
+ // falling back to `absolutePath`), while the selection is the reader's
8
+ // `{ entryId, path }`. Matching by a bare `/`-boundary suffix (as the
9
+ // presentation-only `changeForRowPath` does) is deliberately NOT used here: in
10
+ // a multi-repo session it would happily match `repoA/src/a.ts` to a change in
11
+ // `repoB/src/a.ts` and render the wrong file's diff.
12
+ //
13
+ // Instead we route each change's path through the SAME correlation-grade
14
+ // resolver (`resolveWorkspaceFileSelection`) the tree and transcript use to
15
+ // open files, with the SAME `entries` and `sandboxWorkspaceRoot`. A change is
16
+ // the open file's change iff it resolves to the exact `{ entryId, path }`
17
+ // currently selected. Single-entry sessions match exactly; ambiguous
18
+ // multi-entry / absolute paths degrade to "no match" (never a wrong match),
19
+ // inheriting DD-08's honesty. One resolver, one truth.
20
+
21
+ import type { FileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
22
+ import type { SelectedWorkspaceFile } from "../internal/store/workspace-file-selection-store.js";
23
+ import { resolveWorkspaceFileSelection } from "./resolveWorkspaceFileSelection.js";
24
+ import type { WorkspaceEntry } from "./useWorkspaceEntries.js";
25
+
26
+ /**
27
+ * The session {@link FileChange} that touched the currently-open file, or
28
+ * `null` when the open file was not changed this session (or cannot be
29
+ * correlated with certainty).
30
+ *
31
+ * @param selection - The open file, keyed by `entryId` + repo/root-relative `path`.
32
+ * @param fileChanges - Net-per-file session changes (from `useSessionFileChanges`).
33
+ * @param entries - The client workspace entries — the SAME list the tree and
34
+ * transcript resolve against (`flow.workspace.entries`), so the returned
35
+ * `entryId` is directly comparable. Do not pass the proto `workspaceEntries`.
36
+ * @param sandboxWorkspaceRoot - The sandbox root for git sessions (strips
37
+ * cloud-absolute change paths), identical to the transcript resolver's input.
38
+ * @returns The matching change, or `null`.
39
+ */
40
+ export function findChangeForSelection(
41
+ selection: SelectedWorkspaceFile,
42
+ fileChanges: readonly FileChange[],
43
+ entries: readonly WorkspaceEntry[],
44
+ sandboxWorkspaceRoot?: string,
45
+ ): FileChange | null {
46
+ for (const change of fileChanges) {
47
+ const resolved = resolveWorkspaceFileSelection(
48
+ change.path || change.absolutePath,
49
+ entries,
50
+ sandboxWorkspaceRoot,
51
+ );
52
+ if (
53
+ resolved !== null &&
54
+ resolved.entryId === selection.entryId &&
55
+ resolved.path === selection.path
56
+ ) {
57
+ return change;
58
+ }
59
+ }
60
+ return null;
61
+ }
@@ -7,13 +7,67 @@ export type {
7
7
  WorkspaceFileEntry,
8
8
  WorkspaceFileLister,
9
9
  } from "./WorkspaceFileLister.js";
10
+ export { MAX_WORKSPACE_FILE_READ_BYTES } from "./WorkspaceFileReader.js";
11
+ export type {
12
+ WorkspaceFileContent,
13
+ WorkspaceFileReader,
14
+ } from "./WorkspaceFileReader.js";
10
15
  export { useWorkspaceFiles } from "./useWorkspaceFiles.js";
11
16
  export type {
12
17
  UseWorkspaceFilesOptions,
13
18
  UseWorkspaceFilesReturn,
14
19
  } from "./useWorkspaceFiles.js";
15
- export { WorkspaceEntryFiles } from "./WorkspaceEntryFiles.js";
16
- export type { WorkspaceEntryFilesProps } from "./WorkspaceEntryFiles.js";
20
+ export { useWorkspaceFileContent } from "./useWorkspaceFileContent.js";
21
+ export type {
22
+ UseWorkspaceFileContentOptions,
23
+ UseWorkspaceFileContentReturn,
24
+ } from "./useWorkspaceFileContent.js";
25
+ export { useWorkspaceFileSearch } from "./useWorkspaceFileSearch.js";
26
+ export type {
27
+ UseWorkspaceFileSearchOptions,
28
+ UseWorkspaceFileSearchReturn,
29
+ WorkspaceFileSearchGroup,
30
+ } from "./useWorkspaceFileSearch.js";
31
+ // The match type is part of the public search return; the matcher itself stays @internal.
32
+ export type { WorkspaceFileMatch } from "./matchWorkspaceFiles.js";
33
+ export { WorkspaceFileSearch } from "./WorkspaceFileSearch.js";
34
+ export type { WorkspaceFileSearchProps } from "./WorkspaceFileSearch.js";
35
+ export type {
36
+ WorkspaceContentMatch,
37
+ WorkspaceContentSearchResult,
38
+ WorkspaceContentSearcher,
39
+ } from "./WorkspaceContentSearcher.js";
40
+ export { useWorkspaceContentSearch } from "./useWorkspaceContentSearch.js";
41
+ export type {
42
+ UseWorkspaceContentSearchOptions,
43
+ UseWorkspaceContentSearchReturn,
44
+ WorkspaceContentSearchGroup,
45
+ } from "./useWorkspaceContentSearch.js";
46
+ export { WorkspaceContentSearch } from "./WorkspaceContentSearch.js";
47
+ export type { WorkspaceContentSearchProps } from "./WorkspaceContentSearch.js";
48
+ // The highlight helper (findHighlightRanges) stays @internal, like matchWorkspaceFiles.
49
+ export { FileViewer } from "./FileViewer.js";
50
+ export type { FileViewerProps, FileViewerHandle } from "./FileViewer.js";
51
+ export { WorkspaceSurface } from "./WorkspaceSurface.js";
52
+ export type {
53
+ WorkspaceSurfaceProps,
54
+ SurfaceRailView,
55
+ SurfaceVirtualDocument,
56
+ } from "./WorkspaceSurface.js";
57
+ // Virtual-document identity helpers: hosts mint collision-proof entry ids for
58
+ // SurfaceVirtualDocument tabs and detect them in file-only code paths.
59
+ export { virtualEntryId, isVirtualEntryId } from "../internal/store/index.js";
60
+ export { ExplorerTree } from "./ExplorerTree.js";
61
+ export type { ExplorerTreeProps } from "./ExplorerTree.js";
62
+ // The open-editor tab shape + open-file options are part of the public
63
+ // WorkspaceSurface API.
64
+ export type { OpenEditor, OpenFileOptions } from "../internal/store/index.js";
65
+ // The jump-to-line reveal target rides on FileViewer / WorkspaceSurface /
66
+ // ArtifactContentRenderer props; export the shape (the hook stays @internal).
67
+ export type { RevealTarget } from "../internal/useRevealLine.js";
68
+ // The viewer's selection shape is part of the public FileViewer API; re-export
69
+ // the type (not the store) so consumers can construct/inspect a selection.
70
+ export type { SelectedWorkspaceFile } from "../internal/store/workspace-file-selection-store.js";
17
71
  export { WorkspaceEditor } from "./WorkspaceEditor.js";
18
72
  export type { WorkspaceEditorProps } from "./WorkspaceEditor.js";
19
73
  export { WorkspaceSummary } from "./WorkspaceSummary.js";
@@ -0,0 +1,51 @@
1
+ // Pure highlight helper behind content search. Kept separate from the hook and
2
+ // component so the offset math is exhaustively unit-testable in isolation (no
3
+ // React, no async).
4
+ //
5
+ // The backend returns each hit's `preview` line but no column offset (a
6
+ // cross-language byte/UTF-16 offset would be brittle — see WorkspaceContentMatch).
7
+ // Since matching is case-insensitive substring, the client re-derives the
8
+ // highlight span(s) from the preview text itself, identically to how
9
+ // `matchWorkspaceFiles` derives the filename highlight range. `@internal`.
10
+
11
+ /**
12
+ * A half-open `[start, end)` range of code-unit offsets into a preview string,
13
+ * marking one occurrence of the query to emphasize.
14
+ */
15
+ export interface HighlightRange {
16
+ /** Inclusive start offset within the preview. */
17
+ readonly start: number;
18
+ /** Exclusive end offset within the preview. */
19
+ readonly end: number;
20
+ }
21
+
22
+ /**
23
+ * Finds every case-insensitive occurrence of `query` within `preview`,
24
+ * best-effort for rendering emphasis.
25
+ *
26
+ * - Empty/whitespace query → `[]` (nothing to highlight).
27
+ * - Overlapping matches cannot occur (search advances past each match end).
28
+ * - Offsets are JS string code units, so a consumer highlights with
29
+ * `preview.slice(start, end)`.
30
+ */
31
+ export function findHighlightRanges(
32
+ preview: string,
33
+ query: string,
34
+ ): HighlightRange[] {
35
+ const needle = query.trim().toLowerCase();
36
+ if (needle.length === 0) return [];
37
+
38
+ const haystack = preview.toLowerCase();
39
+ const ranges: HighlightRange[] = [];
40
+
41
+ let from = 0;
42
+ for (;;) {
43
+ const start = haystack.indexOf(needle, from);
44
+ if (start === -1) break;
45
+ const end = start + needle.length;
46
+ ranges.push({ start, end });
47
+ from = end;
48
+ }
49
+
50
+ return ranges;
51
+ }
@@ -0,0 +1,86 @@
1
+ // Pure ranking function behind workspace search. Kept separate from the hook and
2
+ // component so the matching/ranking rules are exhaustively unit-testable in
3
+ // isolation (no React, no async).
4
+ //
5
+ // Match semantics intentionally mirror `internal/file-tree/filterFileTree` —
6
+ // case-insensitive substring — but widened from a node's basename to the full
7
+ // relative path, so `foo/bar` is findable and directory context is searchable
8
+ // (DD-10). The trivial predicate is not extracted into a shared micro-module; a
9
+ // cross-reference comment is the lower-coupling, honest choice.
10
+
11
+ import type { WorkspaceFileEntry } from "./WorkspaceFileLister.js";
12
+
13
+ /**
14
+ * A workspace file that matched a search query.
15
+ *
16
+ * `matchStart`/`matchEnd` are code-unit offsets into `path` (a half-open
17
+ * `[start, end)` range) for the single contiguous substring hit, so a consumer
18
+ * can highlight exactly the matched span with `path.slice(...)`.
19
+ */
20
+ export interface WorkspaceFileMatch {
21
+ /** Repo-relative (git) or root-relative (local) path of the matched file. */
22
+ readonly path: string;
23
+ /** Inclusive start offset of the matched substring within `path`. */
24
+ readonly matchStart: number;
25
+ /** Exclusive end offset of the matched substring within `path`. */
26
+ readonly matchEnd: number;
27
+ }
28
+
29
+ /** A match plus the ranking signal used only for sorting. */
30
+ interface RankedMatch extends WorkspaceFileMatch {
31
+ /** `true` when the hit falls within the basename (after the last `/`). */
32
+ readonly isBasenameHit: boolean;
33
+ }
34
+
35
+ /**
36
+ * Ranks a flat listing against a query, returning matched files best-first.
37
+ *
38
+ * - Case-insensitive substring on the full relative path.
39
+ * - Directories and advisory `notice` entries are never matched (a defensive
40
+ * guard even though the shared cache already strips notices — DD-11).
41
+ * - Empty/whitespace query returns `[]` (search shows a hint, not everything).
42
+ *
43
+ * Ranking, in order: a basename hit outranks a path-only hit; then an earlier
44
+ * match offset; then a shorter path; then lexicographic path for a stable,
45
+ * deterministic order.
46
+ */
47
+ export function matchWorkspaceFiles(
48
+ files: readonly WorkspaceFileEntry[],
49
+ query: string,
50
+ ): WorkspaceFileMatch[] {
51
+ const trimmed = query.trim();
52
+ if (trimmed.length === 0) return [];
53
+
54
+ const needle = trimmed.toLowerCase();
55
+ const ranked: RankedMatch[] = [];
56
+
57
+ for (const file of files) {
58
+ if (file.isDirectory || file.notice) continue;
59
+
60
+ const matchStart = file.path.toLowerCase().indexOf(needle);
61
+ if (matchStart === -1) continue;
62
+
63
+ const lastSlash = file.path.lastIndexOf("/");
64
+ ranked.push({
65
+ path: file.path,
66
+ matchStart,
67
+ matchEnd: matchStart + needle.length,
68
+ isBasenameHit: matchStart >= lastSlash + 1,
69
+ });
70
+ }
71
+
72
+ ranked.sort(compareMatches);
73
+
74
+ return ranked.map(({ path, matchStart, matchEnd }) => ({
75
+ path,
76
+ matchStart,
77
+ matchEnd,
78
+ }));
79
+ }
80
+
81
+ function compareMatches(a: RankedMatch, b: RankedMatch): number {
82
+ if (a.isBasenameHit !== b.isBasenameHit) return a.isBasenameHit ? -1 : 1;
83
+ if (a.matchStart !== b.matchStart) return a.matchStart - b.matchStart;
84
+ if (a.path.length !== b.path.length) return a.path.length - b.path.length;
85
+ return a.path.localeCompare(b.path);
86
+ }
@@ -0,0 +1,119 @@
1
+ // Resolve a transcript tool-call file path to an openable workspace-file
2
+ // selection. This is the seam that lets a click on a path in the message thread
3
+ // route to the same read-only Viewer a file-tree click drives (Slice 3).
4
+ //
5
+ // The hard part is NOT identity ("which entry") but PATH FORM. The reader and
6
+ // lister contracts are strict: `WorkspaceFileReader` / the GitHub Trees lister
7
+ // speak repo-root-relative (git) or root-relative (local) paths — the exact
8
+ // strings a file-tree node carries. Transcript tool-call paths are not
9
+ // guaranteed to be in that form: depending on harness and workspace layout they
10
+ // arrive workspace-relative, subdir-prefixed, or absolute (see
11
+ // `execution/file-review-status.ts` `changeForRowPath`, which documents the
12
+ // same variance and treats path matching as presentation-only).
13
+ //
14
+ // Rather than fuzzy-match, this resolver normalizes deterministically using only
15
+ // anchors the client already holds, grounded in how the runner provisions the
16
+ // workspace (`backend/.../workspace/provisioner.ts`):
17
+ // - Single entry -> cloned at the workspace root, so a (humanized) tool-call
18
+ // path is already repo/root-relative. Identity transform.
19
+ // - Multiple git -> each cloned into `<root>/<entry.name>/`, so the path is
20
+ // prefixed by the subdir name; strip it.
21
+ // - Local (desktop) -> the native reader joins `entry.localPath` + a
22
+ // root-relative path; local tool paths may be absolute, so
23
+ // strip the known `entry.localPath` prefix.
24
+ // - Cloud absolute -> strip the known `sandboxWorkspaceRoot` first.
25
+ // Anything that does not resolve to a definite relative path returns `null` so
26
+ // the caller keeps its existing copy/GitHub-link behavior — a wrong file can
27
+ // never open. Extending this to arbitrary/absolute paths belongs to a later
28
+ // slice that resolves against the authoritative file-tree index.
29
+
30
+ import { classifyPath } from "../execution/file-path-resolver.js";
31
+ import type { SelectedWorkspaceFile } from "../internal/store/workspace-file-selection-store.js";
32
+ import type { WorkspaceEntry } from "./useWorkspaceEntries.js";
33
+
34
+ /** Forward-slash-normalize and drop any leading slashes for prefix comparison. */
35
+ function normalize(path: string): string {
36
+ return path.replace(/\\/g, "/").replace(/^\/+/, "");
37
+ }
38
+
39
+ /**
40
+ * Returns the portion of `path` beneath `anchor`, or `null` when `path` does
41
+ * not sit under `anchor`. An exact match (the anchor itself, a directory) yields
42
+ * `""` — the caller rejects it, since a directory is not a viewable file.
43
+ */
44
+ function relativeTo(path: string, anchor: string): string | null {
45
+ if (!anchor) return null;
46
+ if (path === anchor) return "";
47
+ if (path.startsWith(anchor + "/")) return path.slice(anchor.length + 1);
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * Resolves a transcript tool-call file path to the {@link SelectedWorkspaceFile}
53
+ * that opens it in the read-only Viewer, or `null` when it cannot be resolved to
54
+ * a definite repo/root-relative file (caller keeps copy/GitHub-link fallback).
55
+ *
56
+ * `entries` are the client-side workspace entries (`useWorkspaceEntries`), the
57
+ * same list the Viewer resolves against, so the returned `entryId` is directly
58
+ * usable. `sandboxWorkspaceRoot` (when the session has git-repo entries, e.g.
59
+ * `/home/daytona/workspace`) is stripped first so cloud-absolute paths reduce to
60
+ * workspace-relative before per-entry anchoring.
61
+ *
62
+ * The result `path` is guaranteed to match what the file tree / lister emits for
63
+ * the same file, so opening from the transcript and from the tree produce an
64
+ * identical selection (and hit the same content fetch/cache).
65
+ */
66
+ export function resolveWorkspaceFileSelection(
67
+ path: string,
68
+ entries: readonly WorkspaceEntry[],
69
+ sandboxWorkspaceRoot?: string,
70
+ ): SelectedWorkspaceFile | null {
71
+ if (!path || entries.length === 0) return null;
72
+
73
+ // 1. Reduce a cloud-absolute path to workspace-relative using the known root.
74
+ let work = normalize(path);
75
+ if (sandboxWorkspaceRoot) {
76
+ const root = normalize(sandboxWorkspaceRoot);
77
+ const relToRoot = relativeTo(work, root);
78
+ if (relToRoot !== null) work = relToRoot;
79
+ }
80
+ // The bare workspace root ("" after stripping, or the humanized ".") is not a
81
+ // file.
82
+ if (!work || work === ".") return null;
83
+
84
+ // 2. The platform mount (`.stigmer/…`) is a virtual namespace, not a workspace
85
+ // file — checked after the root strip so an absolute `<root>/.stigmer/…`
86
+ // is caught too. Keep copy behavior for it.
87
+ if (classifyPath(work).kind === "platform") return null;
88
+
89
+ // 3. Anchor on the exact per-entry roots the client holds. Local entries
90
+ // anchor on their absolute `localPath`; multi-entry git repos anchor on the
91
+ // provisioning subdir (`entry.name`). A single git entry is cloned at the
92
+ // root, so it falls through to step 4.
93
+ const isMulti = entries.length > 1;
94
+ for (const entry of entries) {
95
+ let anchor: string | null = null;
96
+ if (entry.type === "local" && entry.localPath) {
97
+ anchor = normalize(entry.localPath);
98
+ } else if (entry.type === "git" && isMulti) {
99
+ anchor = normalize(entry.name);
100
+ }
101
+ if (anchor === null) continue;
102
+
103
+ const rel = relativeTo(work, anchor);
104
+ // Exact match is the entry's own root directory, not a viewable file — stop
105
+ // rather than fall through to the single-entry fallback (which would return
106
+ // the raw root path).
107
+ if (rel === "") return null;
108
+ if (rel) return { entryId: entry.id, path: rel };
109
+ }
110
+
111
+ // 4. A single entry whose anchor did not apply (e.g. a git repo at the root, or
112
+ // a local path already root-relative): the path is already repo/root-relative.
113
+ if (entries.length === 1) {
114
+ return { entryId: entries[0].id, path: work };
115
+ }
116
+
117
+ // 5. Multi-entry with no anchor match: refuse to guess which entry owns it.
118
+ return null;
119
+ }
@@ -0,0 +1,228 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { toError } from "../internal/toError.js";
5
+ import type { WorkspaceEntry } from "./useWorkspaceEntries.js";
6
+ import type {
7
+ WorkspaceContentMatch,
8
+ WorkspaceContentSearcher,
9
+ } from "./WorkspaceContentSearcher.js";
10
+
11
+ /** Default debounce for content-search query changes — matches `useResourceSearch`. */
12
+ const DEFAULT_DEBOUNCE_MS = 300;
13
+
14
+ /**
15
+ * Minimum query length before a disk search fires. Content search hits the
16
+ * filesystem on every query (unlike the cached filename search), so a
17
+ * single-character query would scan the whole tree for a near-useless result.
18
+ * Two characters is a gentle, documented floor — not an arbitrary limit.
19
+ */
20
+ const DEFAULT_MIN_LENGTH = 2;
21
+
22
+ /** Options for {@link useWorkspaceContentSearch}. */
23
+ export interface UseWorkspaceContentSearchOptions {
24
+ /** All workspace entries to search across. */
25
+ readonly entries: readonly WorkspaceEntry[];
26
+ /** Platform-injected content searcher. `undefined` → content search is unavailable. */
27
+ readonly searcher: WorkspaceContentSearcher | undefined;
28
+ /** Current search query. Trimmed; below {@link minLength} yields no search. */
29
+ readonly query: string;
30
+ /** Debounce delay for query changes in milliseconds. @default 300 */
31
+ readonly debounceMs?: number;
32
+ /** Minimum trimmed query length before a search runs. @default 2 */
33
+ readonly minLength?: number;
34
+ }
35
+
36
+ /** Content-search hits (and per-entry status) for one workspace entry. */
37
+ export interface WorkspaceContentSearchGroup {
38
+ /** The workspace entry these matches belong to. */
39
+ readonly entry: WorkspaceEntry;
40
+ /** Line-level matches within this entry, backend-ordered (path, then line). */
41
+ readonly matches: readonly WorkspaceContentMatch[];
42
+ /** `true` when this entry's search was capped (more matches exist than shown). */
43
+ readonly truncated: boolean;
44
+ /** Search error for this entry, if any (isolated — other entries still resolve). */
45
+ readonly error: Error | null;
46
+ }
47
+
48
+ /** Return value of {@link useWorkspaceContentSearch}. */
49
+ export interface UseWorkspaceContentSearchReturn {
50
+ /** Entries with something to show (matches, an error, or a truncation notice). */
51
+ readonly groups: readonly WorkspaceContentSearchGroup[];
52
+ /** Total matches across all groups (for a "showing N of M" affordance). */
53
+ readonly totalMatches: number;
54
+ /** `true` during the first search of a query with no prior results to show. */
55
+ readonly isLoading: boolean;
56
+ /** `true` while a new query loads *over* previously-shown results (no blank flash). */
57
+ readonly isRefetching: boolean;
58
+ /** `true` when no substrate can be searched (no searcher, or every entry unsupported). */
59
+ readonly isUnsupported: boolean;
60
+ /** First per-entry error, for a top-level fallback message. `null` when none. */
61
+ readonly error: Error | null;
62
+ }
63
+
64
+ /** Settled per-entry result held between query changes (never a "loading" state). */
65
+ type EntryResult =
66
+ | { readonly status: "ready"; readonly matches: readonly WorkspaceContentMatch[]; readonly truncated: boolean }
67
+ | { readonly status: "error"; readonly error: Error }
68
+ | { readonly status: "unsupported" };
69
+
70
+ const EMPTY_GROUPS: readonly WorkspaceContentSearchGroup[] = [];
71
+
72
+ /**
73
+ * Behavior hook powering workspace-wide content (text) search.
74
+ *
75
+ * The text-grep sibling of
76
+ * {@link useWorkspaceFileSearch}: it keeps
77
+ * that hook's per-entry aggregation, error isolation, and generation guard, and
78
+ * adds the platform's canonical debounce idiom from `useResourceSearch`
79
+ * (`query`/`debouncedQuery` + `setTimeout`). The key difference is that the
80
+ * debounced query is part of the *fetch* — each keystroke hits disk via the
81
+ * injected {@link WorkspaceContentSearcher}, uncached — so a `minLength` guard
82
+ * and a debounce protect the filesystem.
83
+ *
84
+ * Prior results stay visible while a new debounced query loads (`isRefetching`
85
+ * true, results unchanged); only the very first search with nothing to show
86
+ * flips `isLoading`. This avoids a per-keystroke blank flash the cached filename
87
+ * search never had.
88
+ *
89
+ * @example
90
+ * ```tsx
91
+ * const { groups, totalMatches, isRefetching } = useWorkspaceContentSearch({
92
+ * entries: workspace.entries,
93
+ * searcher: workspaceContentSearcher,
94
+ * query,
95
+ * });
96
+ * ```
97
+ */
98
+ export function useWorkspaceContentSearch({
99
+ entries,
100
+ searcher,
101
+ query,
102
+ debounceMs = DEFAULT_DEBOUNCE_MS,
103
+ minLength = DEFAULT_MIN_LENGTH,
104
+ }: UseWorkspaceContentSearchOptions): UseWorkspaceContentSearchReturn {
105
+ // Settled results per entry, keyed by entry id. Retained across query changes
106
+ // and only swapped wholesale when a new generation fully resolves — so a
107
+ // reload never blanks what the user is already reading.
108
+ const [results, setResults] = useState<Record<string, EntryResult>>({});
109
+ const [pending, setPending] = useState(false);
110
+
111
+ // Debounce the query into the fetch key (the `useResourceSearch` idiom).
112
+ const [debouncedQuery, setDebouncedQuery] = useState(query);
113
+ useEffect(() => {
114
+ const timer = setTimeout(() => setDebouncedQuery(query), debounceMs);
115
+ return () => clearTimeout(timer);
116
+ }, [query, debounceMs]);
117
+
118
+ // Latest entries read inside the effect without widening its dependency to the
119
+ // (possibly per-render) array identity — the effect keys on the id set instead.
120
+ const entriesRef = useRef(entries);
121
+ entriesRef.current = entries;
122
+ const entriesKey = entries.map((entry) => entry.id).join("\u0000");
123
+
124
+ // Ignore resolutions from a superseded run (id set, searcher, or query changed).
125
+ const generationRef = useRef(0);
126
+
127
+ const effectiveQuery = debouncedQuery.trim();
128
+ const belowMinLength = effectiveQuery.length < minLength;
129
+
130
+ useEffect(() => {
131
+ // No searcher or a too-short query: clear results, nothing in flight. (An
132
+ // in-flight generation is abandoned via the bumped ref.)
133
+ if (!searcher || belowMinLength) {
134
+ generationRef.current += 1;
135
+ setResults({});
136
+ setPending(false);
137
+ return;
138
+ }
139
+
140
+ const currentEntries = entriesRef.current;
141
+ const generation = ++generationRef.current;
142
+ setPending(true);
143
+
144
+ Promise.all(
145
+ currentEntries.map(async (entry): Promise<[string, EntryResult]> => {
146
+ try {
147
+ const result = await searcher(entry, effectiveQuery);
148
+ if (result === null) return [entry.id, { status: "unsupported" }];
149
+ return [
150
+ entry.id,
151
+ { status: "ready", matches: result.matches, truncated: result.truncated },
152
+ ];
153
+ } catch (err) {
154
+ return [entry.id, { status: "error", error: toError(err) }];
155
+ }
156
+ }),
157
+ ).then((settled) => {
158
+ // A newer generation started while this batch was in flight — drop it so
159
+ // stale results never overwrite fresher ones (or clobber a cleared state).
160
+ if (generationRef.current !== generation) return;
161
+ const next: Record<string, EntryResult> = {};
162
+ for (const [id, res] of settled) next[id] = res;
163
+ setResults(next);
164
+ setPending(false);
165
+ });
166
+ }, [entriesKey, searcher, effectiveQuery, belowMinLength]);
167
+
168
+ return useMemo<UseWorkspaceContentSearchReturn>(() => {
169
+ if (!searcher) {
170
+ return {
171
+ groups: EMPTY_GROUPS,
172
+ totalMatches: 0,
173
+ isLoading: false,
174
+ isRefetching: false,
175
+ isUnsupported: true,
176
+ error: null,
177
+ };
178
+ }
179
+
180
+ const groups: WorkspaceContentSearchGroup[] = [];
181
+ let totalMatches = 0;
182
+ let firstError: Error | null = null;
183
+ let unsupportedCount = 0;
184
+ let settledCount = 0;
185
+
186
+ for (const entry of entries) {
187
+ const state = results[entry.id];
188
+ if (!state) continue;
189
+ settledCount += 1;
190
+
191
+ if (state.status === "unsupported") {
192
+ unsupportedCount += 1;
193
+ continue;
194
+ }
195
+ if (state.status === "error") {
196
+ if (!firstError) firstError = state.error;
197
+ groups.push({ entry, matches: [], truncated: false, error: state.error });
198
+ continue;
199
+ }
200
+
201
+ totalMatches += state.matches.length;
202
+ // Worth showing only when it carries matches or a truncation notice.
203
+ if (state.matches.length > 0 || state.truncated) {
204
+ groups.push({
205
+ entry,
206
+ matches: state.matches,
207
+ truncated: state.truncated,
208
+ error: null,
209
+ });
210
+ }
211
+ }
212
+
213
+ const hasPriorResults = settledCount > 0;
214
+ const isUnsupported =
215
+ entries.length > 0 && unsupportedCount === entries.length;
216
+
217
+ return {
218
+ groups,
219
+ totalMatches,
220
+ // First search (nothing settled yet) blanks; a reload over existing
221
+ // results keeps them visible and signals `isRefetching` instead.
222
+ isLoading: pending && !hasPriorResults,
223
+ isRefetching: pending && hasPriorResults,
224
+ isUnsupported,
225
+ error: firstError,
226
+ };
227
+ }, [entries, results, pending, searcher]);
228
+ }