@stigmer/react 3.1.1 → 3.1.3

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 (483) hide show
  1. package/composer/SessionComposer.d.ts +25 -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/ArtifactPreviewModal.d.ts +5 -5
  10. package/execution/ArtifactPreviewModal.js +4 -4
  11. package/execution/ArtifactPreviewModal.js.map +1 -1
  12. package/execution/FileChangeProgressBar.d.ts +47 -0
  13. package/execution/FileChangeProgressBar.d.ts.map +1 -0
  14. package/execution/FileChangeProgressBar.js +49 -0
  15. package/execution/FileChangeProgressBar.js.map +1 -0
  16. package/execution/FileChangesView.d.ts.map +1 -1
  17. package/execution/FileChangesView.js +19 -4
  18. package/execution/FileChangesView.js.map +1 -1
  19. package/execution/FilePathContext.d.ts +8 -1
  20. package/execution/FilePathContext.d.ts.map +1 -1
  21. package/execution/FilePathContext.js.map +1 -1
  22. package/execution/FilePathLink.d.ts.map +1 -1
  23. package/execution/FilePathLink.js +21 -6
  24. package/execution/FilePathLink.js.map +1 -1
  25. package/execution/FileReviewAtoms.d.ts +27 -0
  26. package/execution/FileReviewAtoms.d.ts.map +1 -0
  27. package/execution/FileReviewAtoms.js +64 -0
  28. package/execution/FileReviewAtoms.js.map +1 -0
  29. package/execution/FileReviewCard.d.ts +2 -2
  30. package/execution/FileReviewCard.d.ts.map +1 -1
  31. package/execution/FileReviewCard.js +32 -62
  32. package/execution/FileReviewCard.js.map +1 -1
  33. package/execution/MessageEntry.d.ts +20 -0
  34. package/execution/MessageEntry.d.ts.map +1 -1
  35. package/execution/MessageEntry.js +19 -5
  36. package/execution/MessageEntry.js.map +1 -1
  37. package/execution/MessageThread.d.ts +75 -13
  38. package/execution/MessageThread.d.ts.map +1 -1
  39. package/execution/MessageThread.js +195 -27
  40. package/execution/MessageThread.js.map +1 -1
  41. package/execution/PlanArtifactCard.d.ts +36 -21
  42. package/execution/PlanArtifactCard.d.ts.map +1 -1
  43. package/execution/PlanArtifactCard.js +39 -25
  44. package/execution/PlanArtifactCard.js.map +1 -1
  45. package/execution/PlanCompletionCard.d.ts +4 -4
  46. package/execution/PlanCompletionCard.d.ts.map +1 -1
  47. package/execution/PlanCompletionCard.js +4 -4
  48. package/execution/PlanCompletionCard.js.map +1 -1
  49. package/execution/PlanDocumentMessage.d.ts +32 -0
  50. package/execution/PlanDocumentMessage.d.ts.map +1 -0
  51. package/execution/PlanDocumentMessage.js +37 -0
  52. package/execution/PlanDocumentMessage.js.map +1 -0
  53. package/execution/PlanStreamingCard.d.ts +50 -0
  54. package/execution/PlanStreamingCard.d.ts.map +1 -0
  55. package/execution/PlanStreamingCard.js +40 -0
  56. package/execution/PlanStreamingCard.js.map +1 -0
  57. package/execution/index.d.ts +9 -1
  58. package/execution/index.d.ts.map +1 -1
  59. package/execution/index.js +4 -0
  60. package/execution/index.js.map +1 -1
  61. package/execution/use-build-from-plan-hotkey.d.ts +1 -1
  62. package/execution/use-build-from-plan-hotkey.js +1 -1
  63. package/execution/useArtifactCopy.d.ts +51 -0
  64. package/execution/useArtifactCopy.d.ts.map +1 -0
  65. package/execution/useArtifactCopy.js +75 -0
  66. package/execution/useArtifactCopy.js.map +1 -0
  67. package/execution/useArtifactDownload.d.ts.map +1 -1
  68. package/execution/useArtifactDownload.js +17 -5
  69. package/execution/useArtifactDownload.js.map +1 -1
  70. package/execution/useCreateAgentExecution.d.ts +12 -0
  71. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  72. package/execution/useCreateAgentExecution.js +5 -1
  73. package/execution/useCreateAgentExecution.js.map +1 -1
  74. package/github/decodeGitHubContent.d.ts +29 -0
  75. package/github/decodeGitHubContent.d.ts.map +1 -0
  76. package/github/decodeGitHubContent.js +72 -0
  77. package/github/decodeGitHubContent.js.map +1 -0
  78. package/github/index.d.ts +1 -0
  79. package/github/index.d.ts.map +1 -1
  80. package/github/index.js +1 -0
  81. package/github/index.js.map +1 -1
  82. package/github/useGitHubFileReader.d.ts +26 -0
  83. package/github/useGitHubFileReader.d.ts.map +1 -0
  84. package/github/useGitHubFileReader.js +106 -0
  85. package/github/useGitHubFileReader.js.map +1 -0
  86. package/github/useGitHubTreeLister.d.ts.map +1 -1
  87. package/github/useGitHubTreeLister.js +6 -2
  88. package/github/useGitHubTreeLister.js.map +1 -1
  89. package/index.d.ts +9 -9
  90. package/index.d.ts.map +1 -1
  91. package/index.js +5 -5
  92. package/index.js.map +1 -1
  93. package/internal/ResizableSplit.d.ts +72 -22
  94. package/internal/ResizableSplit.d.ts.map +1 -1
  95. package/internal/ResizableSplit.js +96 -38
  96. package/internal/ResizableSplit.js.map +1 -1
  97. package/internal/VirtualizedThread.d.ts +5 -3
  98. package/internal/VirtualizedThread.d.ts.map +1 -1
  99. package/internal/VirtualizedThread.js +6 -4
  100. package/internal/VirtualizedThread.js.map +1 -1
  101. package/internal/file-icons/index.d.ts +17 -0
  102. package/internal/file-icons/index.d.ts.map +1 -0
  103. package/internal/file-icons/index.js +60 -0
  104. package/internal/file-icons/index.js.map +1 -0
  105. package/internal/file-tree/FileTreeNode.d.ts +18 -1
  106. package/internal/file-tree/FileTreeNode.d.ts.map +1 -1
  107. package/internal/file-tree/FileTreeNode.js +11 -2
  108. package/internal/file-tree/FileTreeNode.js.map +1 -1
  109. package/internal/markdown-components.d.ts +38 -1
  110. package/internal/markdown-components.d.ts.map +1 -1
  111. package/internal/markdown-components.js +86 -11
  112. package/internal/markdown-components.js.map +1 -1
  113. package/{workflow → internal}/motion-preference.d.ts +7 -4
  114. package/internal/motion-preference.d.ts.map +1 -0
  115. package/{workflow → internal}/motion-preference.js +7 -4
  116. package/internal/motion-preference.js.map +1 -0
  117. package/internal/store/index.d.ts +33 -0
  118. package/internal/store/index.d.ts.map +1 -1
  119. package/internal/store/index.js +59 -0
  120. package/internal/store/index.js.map +1 -1
  121. package/internal/store/workspace-editors-store.d.ts +145 -0
  122. package/internal/store/workspace-editors-store.d.ts.map +1 -0
  123. package/internal/store/workspace-editors-store.js +208 -0
  124. package/internal/store/workspace-editors-store.js.map +1 -0
  125. package/internal/store/workspace-file-selection-store.d.ts +38 -0
  126. package/internal/store/workspace-file-selection-store.d.ts.map +1 -0
  127. package/internal/store/workspace-file-selection-store.js +62 -0
  128. package/internal/store/workspace-file-selection-store.js.map +1 -0
  129. package/internal/useRevealLine.d.ts +49 -0
  130. package/internal/useRevealLine.d.ts.map +1 -0
  131. package/internal/useRevealLine.js +43 -0
  132. package/internal/useRevealLine.js.map +1 -0
  133. package/library/detect-plan-artifact.d.ts +43 -4
  134. package/library/detect-plan-artifact.d.ts.map +1 -1
  135. package/library/detect-plan-artifact.js +45 -5
  136. package/library/detect-plan-artifact.js.map +1 -1
  137. package/library/detect-streaming-plan.d.ts +45 -0
  138. package/library/detect-streaming-plan.d.ts.map +1 -0
  139. package/library/detect-streaming-plan.js +83 -0
  140. package/library/detect-streaming-plan.js.map +1 -0
  141. package/library/index.d.ts +4 -1
  142. package/library/index.d.ts.map +1 -1
  143. package/library/index.js +2 -1
  144. package/library/index.js.map +1 -1
  145. package/package.json +4 -4
  146. package/session/NewSessionViewer.d.ts +19 -4
  147. package/session/NewSessionViewer.d.ts.map +1 -1
  148. package/session/NewSessionViewer.js +52 -25
  149. package/session/NewSessionViewer.js.map +1 -1
  150. package/session/PlanEditor.d.ts +55 -0
  151. package/session/PlanEditor.d.ts.map +1 -0
  152. package/session/PlanEditor.js +88 -0
  153. package/session/PlanEditor.js.map +1 -0
  154. package/session/PlanStreamingDocument.d.ts +30 -0
  155. package/session/PlanStreamingDocument.d.ts.map +1 -0
  156. package/session/PlanStreamingDocument.js +37 -0
  157. package/session/PlanStreamingDocument.js.map +1 -0
  158. package/session/SessionPanelChip.d.ts +26 -0
  159. package/session/SessionPanelChip.d.ts.map +1 -0
  160. package/session/SessionPanelChip.js +40 -0
  161. package/session/SessionPanelChip.js.map +1 -0
  162. package/session/SessionViewer.d.ts +34 -7
  163. package/session/SessionViewer.d.ts.map +1 -1
  164. package/session/SessionViewer.js +334 -33
  165. package/session/SessionViewer.js.map +1 -1
  166. package/session/{inspector → facets}/ArtifactsTab.d.ts +10 -2
  167. package/session/facets/ArtifactsTab.d.ts.map +1 -0
  168. package/session/{inspector → facets}/ArtifactsTab.js +8 -3
  169. package/session/facets/ArtifactsTab.js.map +1 -0
  170. package/session/{inspector → facets}/ChangesTab.d.ts +3 -3
  171. package/session/facets/ChangesTab.d.ts.map +1 -0
  172. package/session/{inspector → facets}/ChangesTab.js +3 -3
  173. package/session/facets/ChangesTab.js.map +1 -0
  174. package/session/facets/InspectTab.d.ts.map +1 -0
  175. package/session/facets/InspectTab.js.map +1 -0
  176. package/session/{inspector → facets}/SetupTab.d.ts +11 -2
  177. package/session/facets/SetupTab.d.ts.map +1 -0
  178. package/session/{inspector → facets}/SetupTab.js +4 -3
  179. package/session/facets/SetupTab.js.map +1 -0
  180. package/session/{inspector → facets}/UsageTab.d.ts +1 -1
  181. package/session/facets/UsageTab.d.ts.map +1 -0
  182. package/session/{inspector → facets}/UsageTab.js +1 -1
  183. package/session/facets/UsageTab.js.map +1 -0
  184. package/session/{inspector → facets}/index.d.ts +0 -6
  185. package/session/facets/index.d.ts.map +1 -0
  186. package/session/{inspector → facets}/index.js +0 -3
  187. package/session/facets/index.js.map +1 -0
  188. package/session/index.d.ts +11 -2
  189. package/session/index.d.ts.map +1 -1
  190. package/session/index.js +15 -2
  191. package/session/index.js.map +1 -1
  192. package/session/plan-document.d.ts +21 -0
  193. package/session/plan-document.d.ts.map +1 -0
  194. package/session/plan-document.js +28 -0
  195. package/session/plan-document.js.map +1 -0
  196. package/session/useOpenFileChange.d.ts +20 -0
  197. package/session/useOpenFileChange.d.ts.map +1 -0
  198. package/session/useOpenFileChange.js +31 -0
  199. package/session/useOpenFileChange.js.map +1 -0
  200. package/session/usePlanDraft.d.ts +52 -0
  201. package/session/usePlanDraft.d.ts.map +1 -0
  202. package/session/usePlanDraft.js +63 -0
  203. package/session/usePlanDraft.js.map +1 -0
  204. package/session/useSessionConversation.d.ts +14 -1
  205. package/session/useSessionConversation.d.ts.map +1 -1
  206. package/session/useSessionConversation.js +20 -2
  207. package/session/useSessionConversation.js.map +1 -1
  208. package/session/useSessionPageFlow.d.ts.map +1 -1
  209. package/session/useSessionPageFlow.js +1 -0
  210. package/session/useSessionPageFlow.js.map +1 -1
  211. package/session/useSessionPanel.d.ts +120 -0
  212. package/session/useSessionPanel.d.ts.map +1 -0
  213. package/session/useSessionPanel.js +130 -0
  214. package/session/useSessionPanel.js.map +1 -0
  215. package/session/useSessionRailViews.d.ts +45 -0
  216. package/session/useSessionRailViews.d.ts.map +1 -0
  217. package/session/useSessionRailViews.js +105 -0
  218. package/session/useSessionRailViews.js.map +1 -0
  219. package/src/composer/SessionComposer.tsx +53 -10
  220. package/src/composer/__tests__/SessionComposer-contract.test.tsx +58 -4
  221. package/src/execution/ArtifactContentRenderer.tsx +101 -24
  222. package/src/execution/ArtifactPreviewModal.tsx +9 -9
  223. package/src/execution/FileChangeProgressBar.tsx +121 -0
  224. package/src/execution/FileChangesView.tsx +18 -4
  225. package/src/execution/FilePathContext.tsx +8 -1
  226. package/src/execution/FilePathLink.tsx +22 -6
  227. package/src/execution/FileReviewAtoms.tsx +89 -0
  228. package/src/execution/FileReviewCard.tsx +57 -90
  229. package/src/execution/MessageEntry.tsx +49 -1
  230. package/src/execution/MessageThread.tsx +308 -39
  231. package/src/execution/PlanArtifactCard.tsx +174 -64
  232. package/src/execution/PlanCompletionCard.tsx +5 -5
  233. package/src/execution/PlanDocumentMessage.tsx +96 -0
  234. package/src/execution/PlanStreamingCard.tsx +141 -0
  235. package/src/execution/__tests__/ArtifactContentRenderer.test.tsx +80 -1
  236. package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +6 -6
  237. package/src/execution/__tests__/FileChangeProgressBar.test.tsx +125 -0
  238. package/src/execution/__tests__/FilePathLink.test.tsx +87 -0
  239. package/src/execution/__tests__/FileReviewCard.test.tsx +66 -7
  240. package/src/execution/__tests__/MessageThread.test.tsx +102 -1
  241. package/src/execution/__tests__/PlanArtifactCard.test.tsx +114 -26
  242. package/src/execution/__tests__/PlanCompletionCard.test.tsx +2 -2
  243. package/src/execution/__tests__/PlanDocumentMessage.test.tsx +55 -0
  244. package/src/execution/__tests__/PlanStreamingCard.test.tsx +56 -0
  245. package/src/execution/__tests__/buildThreadItems-plan-completion.test.ts +600 -22
  246. package/src/execution/__tests__/message-entry.test.tsx +17 -0
  247. package/src/execution/__tests__/useArtifactCopy.test.tsx +90 -0
  248. package/src/execution/__tests__/useArtifactDownload.test.tsx +2 -0
  249. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +85 -0
  250. package/src/execution/index.ts +13 -1
  251. package/src/execution/use-build-from-plan-hotkey.ts +1 -1
  252. package/src/execution/useArtifactCopy.ts +109 -0
  253. package/src/execution/useArtifactDownload.ts +17 -5
  254. package/src/execution/useCreateAgentExecution.ts +18 -1
  255. package/src/github/__tests__/decodeGitHubContent.test.ts +102 -0
  256. package/src/github/__tests__/useGitHubFileReader.test.ts +183 -0
  257. package/src/github/decodeGitHubContent.ts +83 -0
  258. package/src/github/index.ts +1 -0
  259. package/src/github/useGitHubFileReader.ts +152 -0
  260. package/src/github/useGitHubTreeLister.ts +6 -2
  261. package/src/index.ts +69 -9
  262. package/src/internal/ResizableSplit.tsx +184 -51
  263. package/src/internal/VirtualizedThread.tsx +12 -4
  264. package/src/internal/__tests__/ResizableSplit.test.tsx +125 -3
  265. package/src/internal/__tests__/markdown-components.test.tsx +68 -0
  266. package/src/internal/__tests__/useRevealLine.test.tsx +97 -0
  267. package/src/internal/file-icons/__tests__/file-icons.test.tsx +35 -0
  268. package/src/internal/file-icons/index.tsx +137 -0
  269. package/src/internal/file-tree/FileTreeNode.tsx +56 -5
  270. package/src/internal/file-tree/__tests__/FileTreeNode-explorer.test.tsx +86 -0
  271. package/src/internal/markdown-components.tsx +116 -11
  272. package/src/{workflow → internal}/motion-preference.ts +7 -4
  273. package/src/internal/store/__tests__/workspace-editors-store.test.ts +216 -0
  274. package/src/internal/store/__tests__/workspace-file-selection-store.test.ts +118 -0
  275. package/src/internal/store/index.ts +94 -0
  276. package/src/internal/store/workspace-editors-store.ts +315 -0
  277. package/src/internal/store/workspace-file-selection-store.ts +88 -0
  278. package/src/internal/useRevealLine.ts +81 -0
  279. package/src/library/__tests__/detect-plan-artifact.test.ts +69 -0
  280. package/src/library/__tests__/detect-streaming-plan.test.ts +234 -0
  281. package/src/library/detect-plan-artifact.ts +62 -5
  282. package/src/library/detect-streaming-plan.ts +108 -0
  283. package/src/library/index.ts +7 -0
  284. package/src/session/NewSessionViewer.tsx +123 -55
  285. package/src/session/PlanEditor.tsx +361 -0
  286. package/src/session/PlanStreamingDocument.tsx +97 -0
  287. package/src/session/SessionPanelChip.tsx +103 -0
  288. package/src/session/SessionViewer.tsx +655 -96
  289. package/src/session/__tests__/NewSessionViewer.test.tsx +148 -0
  290. package/src/session/__tests__/PlanEditor.test.tsx +217 -0
  291. package/src/session/__tests__/PlanStreamingDocument.test.tsx +50 -0
  292. package/src/session/__tests__/SessionPanelChip.test.tsx +42 -0
  293. package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +66 -4
  294. package/src/session/__tests__/SessionViewer-transcriptFileOpen.test.tsx +205 -0
  295. package/src/session/__tests__/audienceWiring.test.tsx +55 -19
  296. package/src/session/__tests__/useOpenFileChange.test.tsx +82 -0
  297. package/src/session/__tests__/usePlanDraft.test.ts +102 -0
  298. package/src/session/__tests__/useSessionConversation.test.tsx +44 -0
  299. package/src/session/__tests__/useSessionPanel.test.ts +298 -0
  300. package/src/session/__tests__/useSessionRailViews.test.tsx +124 -0
  301. package/src/session/{inspector → facets}/ArtifactsTab.tsx +27 -5
  302. package/src/session/{inspector → facets}/ChangesTab.tsx +3 -3
  303. package/src/session/{inspector → facets}/SetupTab.tsx +13 -1
  304. package/src/session/{inspector → facets}/UsageTab.tsx +1 -1
  305. package/src/session/{inspector → facets}/__tests__/ArtifactsTab.test.tsx +42 -8
  306. package/src/session/facets/__tests__/SetupTab.test.tsx +44 -0
  307. package/src/session/{inspector → facets}/index.ts +0 -16
  308. package/src/session/index.ts +30 -13
  309. package/src/session/plan-document.ts +30 -0
  310. package/src/session/useOpenFileChange.ts +53 -0
  311. package/src/session/usePlanDraft.ts +109 -0
  312. package/src/session/useSessionConversation.ts +38 -3
  313. package/src/session/useSessionPageFlow.ts +1 -0
  314. package/src/session/useSessionPanel.ts +288 -0
  315. package/src/session/useSessionRailViews.tsx +200 -0
  316. package/src/workflow/WorkflowExecutionGraph.tsx +1 -1
  317. package/src/workflow/index.ts +1 -1
  318. package/src/workflow/useFollowExecution.ts +1 -1
  319. package/src/workspace/EditorTabs.tsx +174 -0
  320. package/src/workspace/ExplorerTree.tsx +253 -0
  321. package/src/workspace/FileViewer.tsx +537 -0
  322. package/src/workspace/WorkspaceContentSearch.tsx +502 -0
  323. package/src/workspace/WorkspaceContentSearcher.ts +70 -0
  324. package/src/workspace/WorkspaceEditor.tsx +19 -16
  325. package/src/workspace/WorkspaceFileLister.ts +11 -0
  326. package/src/workspace/WorkspaceFileReader.ts +57 -0
  327. package/src/workspace/WorkspaceFileSearch.tsx +452 -0
  328. package/src/workspace/WorkspaceSurface.tsx +839 -0
  329. package/src/workspace/__tests__/EditorTabs.test.tsx +102 -0
  330. package/src/workspace/__tests__/ExplorerTree.test.tsx +204 -0
  331. package/src/workspace/__tests__/FileViewer.test.tsx +439 -0
  332. package/src/workspace/__tests__/WorkspaceContentSearch.test.tsx +229 -0
  333. package/src/workspace/__tests__/WorkspaceEditor-initialPanel.test.tsx +19 -0
  334. package/src/workspace/__tests__/WorkspaceFileSearch.test.tsx +227 -0
  335. package/src/workspace/__tests__/WorkspaceSurface.test.tsx +418 -0
  336. package/src/workspace/__tests__/a11y/harness.tsx +140 -0
  337. package/src/workspace/__tests__/a11y/mock-capabilities.ts +138 -0
  338. package/src/workspace/__tests__/a11y/workspace-editor.a11y.test.tsx +65 -0
  339. package/src/workspace/__tests__/a11y/workspace-explorer-search.a11y.test.tsx +112 -0
  340. package/src/workspace/__tests__/a11y/workspace-viewer-tabs.a11y.test.tsx +69 -0
  341. package/src/workspace/__tests__/findChangeForSelection.test.ts +132 -0
  342. package/src/workspace/__tests__/matchContentPreview.test.ts +55 -0
  343. package/src/workspace/__tests__/matchWorkspaceFiles.test.ts +104 -0
  344. package/src/workspace/__tests__/resolveWorkspaceFileSelection.test.ts +135 -0
  345. package/src/workspace/__tests__/useWorkspaceContentSearch.test.tsx +196 -0
  346. package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +190 -0
  347. package/src/workspace/__tests__/useWorkspaceFileSearch.test.tsx +162 -0
  348. package/src/workspace/findChangeForSelection.ts +61 -0
  349. package/src/workspace/index.ts +56 -2
  350. package/src/workspace/matchContentPreview.ts +51 -0
  351. package/src/workspace/matchWorkspaceFiles.ts +86 -0
  352. package/src/workspace/resolveWorkspaceFileSelection.ts +119 -0
  353. package/src/workspace/useWorkspaceContentSearch.ts +228 -0
  354. package/src/workspace/useWorkspaceFileContent.ts +129 -0
  355. package/src/workspace/useWorkspaceFileSearch.ts +184 -0
  356. package/src/workspace/useWorkspaceFiles.ts +27 -27
  357. package/src/workspace/workspaceListingCache.ts +93 -0
  358. package/styles.css +1 -1
  359. package/workflow/WorkflowExecutionGraph.js +1 -1
  360. package/workflow/WorkflowExecutionGraph.js.map +1 -1
  361. package/workflow/index.d.ts +1 -1
  362. package/workflow/index.d.ts.map +1 -1
  363. package/workflow/index.js +1 -1
  364. package/workflow/index.js.map +1 -1
  365. package/workflow/useFollowExecution.js +1 -1
  366. package/workflow/useFollowExecution.js.map +1 -1
  367. package/workspace/EditorTabs.d.ts +47 -0
  368. package/workspace/EditorTabs.d.ts.map +1 -0
  369. package/workspace/EditorTabs.js +78 -0
  370. package/workspace/EditorTabs.js.map +1 -0
  371. package/workspace/ExplorerTree.d.ts +32 -0
  372. package/workspace/ExplorerTree.d.ts.map +1 -0
  373. package/workspace/ExplorerTree.js +49 -0
  374. package/workspace/ExplorerTree.js.map +1 -0
  375. package/workspace/FileViewer.d.ts +93 -0
  376. package/workspace/FileViewer.d.ts.map +1 -0
  377. package/workspace/FileViewer.js +193 -0
  378. package/workspace/FileViewer.js.map +1 -0
  379. package/workspace/WorkspaceContentSearch.d.ts +41 -0
  380. package/workspace/WorkspaceContentSearch.d.ts.map +1 -0
  381. package/workspace/WorkspaceContentSearch.js +216 -0
  382. package/workspace/WorkspaceContentSearch.js.map +1 -0
  383. package/workspace/WorkspaceContentSearcher.d.ts +59 -0
  384. package/workspace/WorkspaceContentSearcher.d.ts.map +1 -0
  385. package/workspace/WorkspaceContentSearcher.js +7 -0
  386. package/workspace/WorkspaceContentSearcher.js.map +1 -0
  387. package/workspace/WorkspaceEditor.d.ts.map +1 -1
  388. package/workspace/WorkspaceEditor.js +8 -10
  389. package/workspace/WorkspaceEditor.js.map +1 -1
  390. package/workspace/WorkspaceFileLister.d.ts +11 -0
  391. package/workspace/WorkspaceFileLister.d.ts.map +1 -1
  392. package/workspace/WorkspaceFileReader.d.ts +48 -0
  393. package/workspace/WorkspaceFileReader.d.ts.map +1 -0
  394. package/workspace/WorkspaceFileReader.js +11 -0
  395. package/workspace/WorkspaceFileReader.js.map +1 -0
  396. package/workspace/WorkspaceFileSearch.d.ts +32 -0
  397. package/workspace/WorkspaceFileSearch.d.ts.map +1 -0
  398. package/workspace/WorkspaceFileSearch.js +175 -0
  399. package/workspace/WorkspaceFileSearch.js.map +1 -0
  400. package/workspace/WorkspaceSurface.d.ts +151 -0
  401. package/workspace/WorkspaceSurface.d.ts.map +1 -0
  402. package/workspace/WorkspaceSurface.js +220 -0
  403. package/workspace/WorkspaceSurface.js.map +1 -0
  404. package/workspace/findChangeForSelection.d.ts +19 -0
  405. package/workspace/findChangeForSelection.d.ts.map +1 -0
  406. package/workspace/findChangeForSelection.js +46 -0
  407. package/workspace/findChangeForSelection.js.map +1 -0
  408. package/workspace/index.d.ts +24 -2
  409. package/workspace/index.d.ts.map +1 -1
  410. package/workspace/index.js +13 -1
  411. package/workspace/index.js.map +1 -1
  412. package/workspace/matchContentPreview.d.ts +21 -0
  413. package/workspace/matchContentPreview.d.ts.map +1 -0
  414. package/workspace/matchContentPreview.js +36 -0
  415. package/workspace/matchContentPreview.js.map +1 -0
  416. package/workspace/matchWorkspaceFiles.d.ts +30 -0
  417. package/workspace/matchWorkspaceFiles.d.ts.map +1 -0
  418. package/workspace/matchWorkspaceFiles.js +58 -0
  419. package/workspace/matchWorkspaceFiles.js.map +1 -0
  420. package/workspace/resolveWorkspaceFileSelection.d.ts +19 -0
  421. package/workspace/resolveWorkspaceFileSelection.d.ts.map +1 -0
  422. package/workspace/resolveWorkspaceFileSelection.js +115 -0
  423. package/workspace/resolveWorkspaceFileSelection.js.map +1 -0
  424. package/workspace/useWorkspaceContentSearch.d.ts +69 -0
  425. package/workspace/useWorkspaceContentSearch.d.ts.map +1 -0
  426. package/workspace/useWorkspaceContentSearch.js +154 -0
  427. package/workspace/useWorkspaceContentSearch.js.map +1 -0
  428. package/workspace/useWorkspaceFileContent.d.ts +73 -0
  429. package/workspace/useWorkspaceFileContent.d.ts.map +1 -0
  430. package/workspace/useWorkspaceFileContent.js +57 -0
  431. package/workspace/useWorkspaceFileContent.js.map +1 -0
  432. package/workspace/useWorkspaceFileSearch.d.ts +61 -0
  433. package/workspace/useWorkspaceFileSearch.d.ts.map +1 -0
  434. package/workspace/useWorkspaceFileSearch.js +119 -0
  435. package/workspace/useWorkspaceFileSearch.js.map +1 -0
  436. package/workspace/useWorkspaceFiles.d.ts +13 -5
  437. package/workspace/useWorkspaceFiles.d.ts.map +1 -1
  438. package/workspace/useWorkspaceFiles.js +18 -19
  439. package/workspace/useWorkspaceFiles.js.map +1 -1
  440. package/workspace/workspaceListingCache.d.ts +47 -0
  441. package/workspace/workspaceListingCache.d.ts.map +1 -0
  442. package/workspace/workspaceListingCache.js +55 -0
  443. package/workspace/workspaceListingCache.js.map +1 -0
  444. package/session/inspector/ArtifactsTab.d.ts.map +0 -1
  445. package/session/inspector/ArtifactsTab.js.map +0 -1
  446. package/session/inspector/ChangesTab.d.ts.map +0 -1
  447. package/session/inspector/ChangesTab.js.map +0 -1
  448. package/session/inspector/InspectTab.d.ts.map +0 -1
  449. package/session/inspector/InspectTab.js.map +0 -1
  450. package/session/inspector/SessionInspector.d.ts +0 -64
  451. package/session/inspector/SessionInspector.d.ts.map +0 -1
  452. package/session/inspector/SessionInspector.js +0 -53
  453. package/session/inspector/SessionInspector.js.map +0 -1
  454. package/session/inspector/SetupTab.d.ts.map +0 -1
  455. package/session/inspector/SetupTab.js.map +0 -1
  456. package/session/inspector/UsageTab.d.ts.map +0 -1
  457. package/session/inspector/UsageTab.js.map +0 -1
  458. package/session/inspector/WorkspaceTab.d.ts +0 -35
  459. package/session/inspector/WorkspaceTab.d.ts.map +0 -1
  460. package/session/inspector/WorkspaceTab.js +0 -75
  461. package/session/inspector/WorkspaceTab.js.map +0 -1
  462. package/session/inspector/index.d.ts.map +0 -1
  463. package/session/inspector/index.js.map +0 -1
  464. package/session/inspector/useSessionInspector.d.ts +0 -49
  465. package/session/inspector/useSessionInspector.d.ts.map +0 -1
  466. package/session/inspector/useSessionInspector.js +0 -122
  467. package/session/inspector/useSessionInspector.js.map +0 -1
  468. package/src/session/inspector/SessionInspector.tsx +0 -162
  469. package/src/session/inspector/WorkspaceTab.tsx +0 -245
  470. package/src/session/inspector/__tests__/useSessionInspector.test.ts +0 -251
  471. package/src/session/inspector/useSessionInspector.ts +0 -178
  472. package/src/workspace/WorkspaceEntryFiles.tsx +0 -196
  473. package/workflow/motion-preference.d.ts.map +0 -1
  474. package/workflow/motion-preference.js.map +0 -1
  475. package/workspace/WorkspaceEntryFiles.d.ts +0 -22
  476. package/workspace/WorkspaceEntryFiles.d.ts.map +0 -1
  477. package/workspace/WorkspaceEntryFiles.js +0 -58
  478. package/workspace/WorkspaceEntryFiles.js.map +0 -1
  479. /package/session/{inspector → facets}/InspectTab.d.ts +0 -0
  480. /package/session/{inspector → facets}/InspectTab.js +0 -0
  481. /package/src/{workflow → internal}/__tests__/motion-preference.test.ts +0 -0
  482. /package/src/session/{inspector → facets}/InspectTab.tsx +0 -0
  483. /package/src/session/{inspector → facets}/__tests__/ChangesTab.test.tsx +0 -0
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { findHighlightRanges } from "../matchContentPreview";
3
+
4
+ describe("findHighlightRanges", () => {
5
+ it("returns no ranges for an empty or whitespace query", () => {
6
+ expect(findHighlightRanges("hello world", "")).toEqual([]);
7
+ expect(findHighlightRanges("hello world", " ")).toEqual([]);
8
+ });
9
+
10
+ it("returns no ranges when the query is absent", () => {
11
+ expect(findHighlightRanges("hello world", "xyz")).toEqual([]);
12
+ });
13
+
14
+ it("finds a single occurrence", () => {
15
+ expect(findHighlightRanges("hello world", "world")).toEqual([
16
+ { start: 6, end: 11 },
17
+ ]);
18
+ });
19
+
20
+ it("finds every occurrence on the line", () => {
21
+ expect(findHighlightRanges("foo bar foo baz foo", "foo")).toEqual([
22
+ { start: 0, end: 3 },
23
+ { start: 8, end: 11 },
24
+ { start: 16, end: 19 },
25
+ ]);
26
+ });
27
+
28
+ it("matches case-insensitively but reports offsets into the original text", () => {
29
+ expect(findHighlightRanges("TODO todo ToDo", "todo")).toEqual([
30
+ { start: 0, end: 4 },
31
+ { start: 5, end: 9 },
32
+ { start: 10, end: 14 },
33
+ ]);
34
+ });
35
+
36
+ it("does not produce overlapping ranges (advances past each match)", () => {
37
+ // "aa" in "aaaa" → [0,2) and [2,4), not [1,3).
38
+ expect(findHighlightRanges("aaaa", "aa")).toEqual([
39
+ { start: 0, end: 2 },
40
+ { start: 2, end: 4 },
41
+ ]);
42
+ });
43
+
44
+ it("trims the query before matching", () => {
45
+ expect(findHighlightRanges("hello world", " world ")).toEqual([
46
+ { start: 6, end: 11 },
47
+ ]);
48
+ });
49
+
50
+ it("handles multibyte text with correct code-unit offsets", () => {
51
+ const text = "日本 needle 世界";
52
+ const [range] = findHighlightRanges(text, "needle");
53
+ expect(text.slice(range.start, range.end)).toBe("needle");
54
+ });
55
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { matchWorkspaceFiles } from "../matchWorkspaceFiles";
3
+ import type { WorkspaceFileEntry } from "../WorkspaceFileLister";
4
+
5
+ function file(path: string): WorkspaceFileEntry {
6
+ return { path, isDirectory: false };
7
+ }
8
+
9
+ describe("matchWorkspaceFiles", () => {
10
+ it("returns [] for an empty or whitespace query", () => {
11
+ const files = [file("src/index.ts")];
12
+ expect(matchWorkspaceFiles(files, "")).toEqual([]);
13
+ expect(matchWorkspaceFiles(files, " ")).toEqual([]);
14
+ });
15
+
16
+ it("matches case-insensitively on the full relative path", () => {
17
+ const files = [file("src/Components/Button.tsx")];
18
+ const result = matchWorkspaceFiles(files, "button");
19
+ expect(result).toHaveLength(1);
20
+ expect(result[0].path).toBe("src/Components/Button.tsx");
21
+ });
22
+
23
+ it("finds files by a cross-segment path substring", () => {
24
+ const files = [file("src/utils/helper.ts"), file("README.md")];
25
+ const result = matchWorkspaceFiles(files, "utils/helper");
26
+ expect(result.map((m) => m.path)).toEqual(["src/utils/helper.ts"]);
27
+ });
28
+
29
+ it("excludes directories", () => {
30
+ const files: WorkspaceFileEntry[] = [
31
+ { path: "src", isDirectory: true },
32
+ { path: "src/index.ts", isDirectory: false },
33
+ ];
34
+ const result = matchWorkspaceFiles(files, "src");
35
+ expect(result.map((m) => m.path)).toEqual(["src/index.ts"]);
36
+ });
37
+
38
+ it("excludes advisory notice entries even if the query matches the message", () => {
39
+ const files: WorkspaceFileEntry[] = [
40
+ { path: "src/tree.ts", isDirectory: false },
41
+ { path: "... (tree truncated by GitHub — repository has too many files)", isDirectory: false, notice: true },
42
+ ];
43
+ const result = matchWorkspaceFiles(files, "tree");
44
+ expect(result.map((m) => m.path)).toEqual(["src/tree.ts"]);
45
+ });
46
+
47
+ it("returns [] when nothing matches", () => {
48
+ expect(matchWorkspaceFiles([file("src/index.ts")], "zzz")).toEqual([]);
49
+ });
50
+
51
+ it("reports the contiguous highlight range as code-unit offsets", () => {
52
+ const [m] = matchWorkspaceFiles([file("src/index.ts")], "index");
53
+ expect(m.matchStart).toBe(4);
54
+ expect(m.matchEnd).toBe(9);
55
+ expect("src/index.ts".slice(m.matchStart, m.matchEnd)).toBe("index");
56
+ });
57
+
58
+ it("ranks a basename hit above a path-only hit", () => {
59
+ const files = [
60
+ file("app/main.ts"), // "app" is only in the dir segment (path-only)
61
+ file("src/app.ts"), // "app" is the basename
62
+ ];
63
+ const result = matchWorkspaceFiles(files, "app");
64
+ expect(result[0].path).toBe("src/app.ts");
65
+ expect(result[1].path).toBe("app/main.ts");
66
+ });
67
+
68
+ it("ranks an earlier match offset first among basename hits", () => {
69
+ const files = [file("src/index.ts"), file("index.ts")];
70
+ const result = matchWorkspaceFiles(files, "index");
71
+ // "index.ts" matches at offset 0; "src/index.ts" at offset 4.
72
+ expect(result.map((m) => m.path)).toEqual(["index.ts", "src/index.ts"]);
73
+ });
74
+
75
+ it("breaks ties by shorter path then lexicographically", () => {
76
+ const files = [
77
+ file("b/thing.ts"),
78
+ file("a/thing.ts"),
79
+ file("thing.ts"),
80
+ ];
81
+ const result = matchWorkspaceFiles(files, "thing");
82
+ // "thing.ts" (offset 0) first; the two offset-2 hits tie on length → lexicographic.
83
+ expect(result.map((m) => m.path)).toEqual([
84
+ "thing.ts",
85
+ "a/thing.ts",
86
+ "b/thing.ts",
87
+ ]);
88
+ });
89
+
90
+ it("handles unicode paths and slices the range consistently", () => {
91
+ const files = [file("src/café/résumé.ts")];
92
+ const [m] = matchWorkspaceFiles(files, "résumé");
93
+ expect(m).toBeDefined();
94
+ expect("src/café/résumé.ts".slice(m.matchStart, m.matchEnd)).toBe("résumé");
95
+ });
96
+
97
+ it("is deterministic regardless of input order", () => {
98
+ const a = [file("z.ts"), file("a.ts"), file("m.ts")];
99
+ const b = [file("a.ts"), file("m.ts"), file("z.ts")];
100
+ const qa = matchWorkspaceFiles(a, ".ts").map((m) => m.path);
101
+ const qb = matchWorkspaceFiles(b, ".ts").map((m) => m.path);
102
+ expect(qa).toEqual(qb);
103
+ });
104
+ });
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveWorkspaceFileSelection } from "../resolveWorkspaceFileSelection";
3
+ import type { WorkspaceEntry } from "../useWorkspaceEntries";
4
+
5
+ function git(id: string, name: string): WorkspaceEntry {
6
+ return { id, name, type: "git", gitUrl: `https://github.com/${name}.git`, gitBranch: "main" };
7
+ }
8
+
9
+ function local(id: string, name: string, localPath: string): WorkspaceEntry {
10
+ return { id, name, type: "local", localPath };
11
+ }
12
+
13
+ const DAYTONA = "/home/daytona/workspace";
14
+
15
+ describe("resolveWorkspaceFileSelection", () => {
16
+ // --- single git entry (cloud): repo cloned at the workspace root ----------
17
+
18
+ it("returns a repo-relative path unchanged for a single git entry (identity)", () => {
19
+ const entries = [git("e1", "org/repo")];
20
+ expect(resolveWorkspaceFileSelection("src/main.go", entries)).toEqual({
21
+ entryId: "e1",
22
+ path: "src/main.go",
23
+ });
24
+ });
25
+
26
+ it("strips the sandbox workspace root from a cloud-absolute path", () => {
27
+ const entries = [git("e1", "org/repo")];
28
+ expect(
29
+ resolveWorkspaceFileSelection(`${DAYTONA}/src/main.go`, entries, DAYTONA),
30
+ ).toEqual({ entryId: "e1", path: "src/main.go" });
31
+ });
32
+
33
+ it("normalizes a leading slash", () => {
34
+ const entries = [git("e1", "org/repo")];
35
+ expect(resolveWorkspaceFileSelection("/src/main.go", entries)).toEqual({
36
+ entryId: "e1",
37
+ path: "src/main.go",
38
+ });
39
+ });
40
+
41
+ // --- multiple git entries: each cloned into <root>/<entry.name>/ ----------
42
+
43
+ it("strips the subdir (entry.name) prefix for a multi-entry git session", () => {
44
+ const entries = [git("a", "org/repoA"), git("b", "org/repoB")];
45
+ expect(resolveWorkspaceFileSelection("org/repoB/src/main.go", entries)).toEqual({
46
+ entryId: "b",
47
+ path: "src/main.go",
48
+ });
49
+ });
50
+
51
+ it("strips both the sandbox root and the subdir for a multi-entry absolute path", () => {
52
+ const entries = [git("a", "org/repoA"), git("b", "org/repoB")];
53
+ expect(
54
+ resolveWorkspaceFileSelection(`${DAYTONA}/org/repoA/x.ts`, entries, DAYTONA),
55
+ ).toEqual({ entryId: "a", path: "x.ts" });
56
+ });
57
+
58
+ it("returns null for a multi-entry path that matches no entry (never mis-attribute)", () => {
59
+ const entries = [git("a", "org/repoA"), git("b", "org/repoB")];
60
+ expect(resolveWorkspaceFileSelection("unrelated/x.ts", entries)).toBeNull();
61
+ });
62
+
63
+ // --- local entries (desktop): anchor on the absolute localPath -----------
64
+
65
+ it("strips the absolute localPath prefix for a local entry", () => {
66
+ const entries = [local("e1", "app", "/Users/dev/app")];
67
+ expect(
68
+ resolveWorkspaceFileSelection("/Users/dev/app/src/main.go", entries),
69
+ ).toEqual({ entryId: "e1", path: "src/main.go" });
70
+ });
71
+
72
+ it("accepts an already-root-relative path for a single local entry", () => {
73
+ const entries = [local("e1", "app", "/Users/dev/app")];
74
+ expect(resolveWorkspaceFileSelection("src/main.go", entries)).toEqual({
75
+ entryId: "e1",
76
+ path: "src/main.go",
77
+ });
78
+ });
79
+
80
+ it("normalizes Windows backslash paths for a local entry", () => {
81
+ const entries = [local("e1", "app", "C:\\Users\\dev\\app")];
82
+ expect(
83
+ resolveWorkspaceFileSelection("C:\\Users\\dev\\app\\src\\main.go", entries),
84
+ ).toEqual({ entryId: "e1", path: "src/main.go" });
85
+ });
86
+
87
+ // --- platform mount, roots, and empties -> null (keep copy fallback) ------
88
+
89
+ it("returns null for a .stigmer/ platform path", () => {
90
+ const entries = [git("e1", "org/repo")];
91
+ expect(resolveWorkspaceFileSelection(".stigmer/skills/x.md", entries)).toBeNull();
92
+ });
93
+
94
+ it("returns null for an absolute .stigmer/ path under the sandbox root", () => {
95
+ const entries = [git("e1", "org/repo")];
96
+ expect(
97
+ resolveWorkspaceFileSelection(`${DAYTONA}/.stigmer/skills/x`, entries, DAYTONA),
98
+ ).toBeNull();
99
+ });
100
+
101
+ it("returns null for the bare workspace root", () => {
102
+ const entries = [git("e1", "org/repo")];
103
+ expect(resolveWorkspaceFileSelection(DAYTONA, entries, DAYTONA)).toBeNull();
104
+ expect(resolveWorkspaceFileSelection(".", entries)).toBeNull();
105
+ });
106
+
107
+ it("returns null when the path targets a local entry's own root directory", () => {
108
+ const entries = [local("e1", "app", "/Users/dev/app")];
109
+ expect(resolveWorkspaceFileSelection("/Users/dev/app", entries)).toBeNull();
110
+ });
111
+
112
+ it("returns null for an empty path", () => {
113
+ expect(resolveWorkspaceFileSelection("", [git("e1", "org/repo")])).toBeNull();
114
+ });
115
+
116
+ it("returns null when there are no workspace entries", () => {
117
+ expect(resolveWorkspaceFileSelection("src/main.go", [])).toBeNull();
118
+ });
119
+
120
+ // --- cross-check invariant: transcript-open == tree-open ------------------
121
+
122
+ it("produces the same repo-relative path a file-tree click would (single entry)", () => {
123
+ // The GitHub Trees lister emits repo-root-relative node paths; a tree click
124
+ // opens { entryId, path: "src/app/page.tsx" }. A transcript click on the
125
+ // same file must resolve to an identical selection so both hit one fetch.
126
+ const entries = [git("e1", "org/repo")];
127
+ const treeSelection = { entryId: "e1", path: "src/app/page.tsx" };
128
+ expect(resolveWorkspaceFileSelection("src/app/page.tsx", entries)).toEqual(
129
+ treeSelection,
130
+ );
131
+ expect(
132
+ resolveWorkspaceFileSelection(`${DAYTONA}/src/app/page.tsx`, entries, DAYTONA),
133
+ ).toEqual(treeSelection);
134
+ });
135
+ });
@@ -0,0 +1,196 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { renderHook, waitFor, cleanup } from "@testing-library/react";
3
+ import { useWorkspaceContentSearch } from "../useWorkspaceContentSearch";
4
+ import type { WorkspaceEntry } from "../useWorkspaceEntries";
5
+ import type {
6
+ WorkspaceContentMatch,
7
+ WorkspaceContentSearchResult,
8
+ WorkspaceContentSearcher,
9
+ } from "../WorkspaceContentSearcher";
10
+
11
+ // Real timers with `debounceMs: 0` (mirroring the filename-search hook suite):
12
+ // the debounce collapses to a microtask so `waitFor` drives the async settle
13
+ // deterministically. Fake timers + `advanceTimersByTimeAsync` interact badly
14
+ // with the deferred-promise refetch assertions below (microtask blowup), and
15
+ // the behaviors under test — min-length gating, aggregation, error isolation,
16
+ // refetch-keeps-results, and the stale-generation guard — don't need wall-clock
17
+ // timing to verify.
18
+
19
+ let entryIdCounter = 0;
20
+
21
+ function makeEntry(overrides?: Partial<WorkspaceEntry>): WorkspaceEntry {
22
+ return {
23
+ id: `ws-${++entryIdCounter}`,
24
+ name: "acme/api",
25
+ type: "local",
26
+ localPath: "/repo",
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ function match(path: string, line: number, preview: string): WorkspaceContentMatch {
32
+ return { path, line, preview };
33
+ }
34
+
35
+ function result(
36
+ matches: WorkspaceContentMatch[],
37
+ truncated = false,
38
+ ): WorkspaceContentSearchResult {
39
+ return { matches, truncated };
40
+ }
41
+
42
+ /** A manually-resolvable searcher result, for observing in-flight states. */
43
+ function deferred() {
44
+ let resolve!: (r: WorkspaceContentSearchResult) => void;
45
+ const promise = new Promise<WorkspaceContentSearchResult>((res) => {
46
+ resolve = res;
47
+ });
48
+ return { promise, resolve };
49
+ }
50
+
51
+ afterEach(cleanup);
52
+
53
+ describe("useWorkspaceContentSearch", () => {
54
+ it("is unsupported and never searches when no searcher is injected", () => {
55
+ const entries = [makeEntry()];
56
+ const { result: r } = renderHook(() =>
57
+ useWorkspaceContentSearch({
58
+ entries,
59
+ searcher: undefined,
60
+ query: "foo",
61
+ debounceMs: 0,
62
+ }),
63
+ );
64
+ expect(r.current.isUnsupported).toBe(true);
65
+ expect(r.current.groups).toEqual([]);
66
+ });
67
+
68
+ it("does not search below the minimum query length", async () => {
69
+ const entries = [makeEntry()];
70
+ const searcher = vi.fn(async () => result([])) as unknown as WorkspaceContentSearcher;
71
+ const { result: r } = renderHook(() =>
72
+ useWorkspaceContentSearch({ entries, searcher, query: "a", debounceMs: 0 }),
73
+ );
74
+ // Give the debounce a tick to (not) fire.
75
+ await new Promise((res) => setTimeout(res, 20));
76
+ expect(searcher).not.toHaveBeenCalled();
77
+ expect(r.current.groups).toEqual([]);
78
+ expect(r.current.isUnsupported).toBe(false);
79
+ });
80
+
81
+ it("aggregates per-entry line matches", async () => {
82
+ const a = makeEntry();
83
+ const b = makeEntry();
84
+ const searcher: WorkspaceContentSearcher = vi.fn(async (e) =>
85
+ e.id === a.id
86
+ ? result([match("a.ts", 1, "foo one"), match("a.ts", 4, "foo two")])
87
+ : result([match("b.ts", 2, "foo three")]),
88
+ );
89
+
90
+ const { result: r } = renderHook(() =>
91
+ useWorkspaceContentSearch({ entries: [a, b], searcher, query: "foo", debounceMs: 0 }),
92
+ );
93
+ await waitFor(() => expect(r.current.isLoading).toBe(false));
94
+
95
+ expect(r.current.totalMatches).toBe(3);
96
+ expect(
97
+ r.current.groups.map((g) => ({ id: g.entry.id, lines: g.matches.map((m) => m.line) })),
98
+ ).toEqual([
99
+ { id: a.id, lines: [1, 4] },
100
+ { id: b.id, lines: [2] },
101
+ ]);
102
+ });
103
+
104
+ it("isolates a per-entry error while other entries still resolve", async () => {
105
+ const good = makeEntry();
106
+ const bad = makeEntry();
107
+ const searcher: WorkspaceContentSearcher = vi.fn(async (e) => {
108
+ if (e.id === bad.id) throw new Error("disk error");
109
+ return result([match("a.ts", 1, "foo")]);
110
+ });
111
+
112
+ const { result: r } = renderHook(() =>
113
+ useWorkspaceContentSearch({ entries: [good, bad], searcher, query: "foo", debounceMs: 0 }),
114
+ );
115
+ await waitFor(() => expect(r.current.isLoading).toBe(false));
116
+
117
+ expect(r.current.groups.find((g) => g.entry.id === good.id)?.matches).toHaveLength(1);
118
+ expect(r.current.groups.find((g) => g.entry.id === bad.id)?.error?.message).toBe("disk error");
119
+ expect(r.current.error?.message).toBe("disk error");
120
+ });
121
+
122
+ it("reports isUnsupported when every entry returns null", async () => {
123
+ const entries = [makeEntry()];
124
+ const searcher: WorkspaceContentSearcher = vi.fn(async () => null);
125
+ const { result: r } = renderHook(() =>
126
+ useWorkspaceContentSearch({ entries, searcher, query: "foo", debounceMs: 0 }),
127
+ );
128
+ await waitFor(() => expect(r.current.isUnsupported).toBe(true));
129
+ expect(r.current.groups).toEqual([]);
130
+ });
131
+
132
+ it("surfaces a per-entry truncation flag", async () => {
133
+ const entries = [makeEntry()];
134
+ const searcher: WorkspaceContentSearcher = vi.fn(async () =>
135
+ result([match("a.ts", 1, "foo")], true),
136
+ );
137
+ const { result: r } = renderHook(() =>
138
+ useWorkspaceContentSearch({ entries, searcher, query: "foo", debounceMs: 0 }),
139
+ );
140
+ await waitFor(() => expect(r.current.isLoading).toBe(false));
141
+ expect(r.current.groups[0].truncated).toBe(true);
142
+ });
143
+
144
+ it("keeps prior results visible while a new query loads (isRefetching, no blank flash)", async () => {
145
+ const entry = makeEntry();
146
+ const second = deferred();
147
+ const searcher = vi
148
+ .fn()
149
+ .mockResolvedValueOnce(result([match("a.ts", 1, "foo")]))
150
+ .mockReturnValueOnce(second.promise) as unknown as WorkspaceContentSearcher;
151
+
152
+ const { result: r, rerender } = renderHook(
153
+ ({ q }) => useWorkspaceContentSearch({ entries: [entry], searcher, query: q, debounceMs: 0 }),
154
+ { initialProps: { q: "foo" } },
155
+ );
156
+
157
+ await waitFor(() => expect(r.current.groups[0]?.matches).toHaveLength(1));
158
+ expect(r.current.isLoading).toBe(false);
159
+
160
+ // New query → the second (pending) search starts; old results stay visible.
161
+ rerender({ q: "bar" });
162
+ await waitFor(() => expect(r.current.isRefetching).toBe(true));
163
+ expect(r.current.isLoading).toBe(false);
164
+ expect(r.current.groups[0].matches).toHaveLength(1);
165
+
166
+ // Resolve the second search → new results replace the old ones.
167
+ second.resolve(result([match("b.ts", 2, "bar"), match("c.ts", 3, "bar")]));
168
+ await waitFor(() => expect(r.current.isRefetching).toBe(false));
169
+ expect(r.current.totalMatches).toBe(2);
170
+ });
171
+
172
+ it("ignores a stale in-flight search when the query changes again", async () => {
173
+ const entry = makeEntry();
174
+ const first = deferred();
175
+ const searcher = vi
176
+ .fn()
177
+ .mockReturnValueOnce(first.promise)
178
+ .mockResolvedValueOnce(result([match("new.ts", 1, "bar")])) as unknown as WorkspaceContentSearcher;
179
+
180
+ const { result: r, rerender } = renderHook(
181
+ ({ q }) => useWorkspaceContentSearch({ entries: [entry], searcher, query: q, debounceMs: 0 }),
182
+ { initialProps: { q: "foo" } },
183
+ );
184
+
185
+ // Supersede the first (still pending) query; the second resolves.
186
+ rerender({ q: "bar" });
187
+ await waitFor(() =>
188
+ expect(r.current.groups.map((g) => g.matches.map((m) => m.path))).toEqual([["new.ts"]]),
189
+ );
190
+
191
+ // The stale first now resolves — it must not clobber the fresher result.
192
+ first.resolve(result([match("stale.ts", 9, "foo")]));
193
+ await new Promise((res) => setTimeout(res, 20));
194
+ expect(r.current.groups.map((g) => g.matches.map((m) => m.path))).toEqual([["new.ts"]]);
195
+ });
196
+ });
@@ -0,0 +1,190 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { renderHook, act, waitFor } from "@testing-library/react";
3
+ import { useWorkspaceFileContent } from "../useWorkspaceFileContent";
4
+ import type { WorkspaceEntry } from "../useWorkspaceEntries";
5
+ import type {
6
+ WorkspaceFileContent,
7
+ WorkspaceFileReader,
8
+ } from "../WorkspaceFileReader";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Fixtures
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function gitEntry(id = "e1"): WorkspaceEntry {
15
+ return {
16
+ id,
17
+ name: "acme/repo",
18
+ type: "git",
19
+ gitUrl: "https://github.com/acme/repo",
20
+ gitBranch: "main",
21
+ };
22
+ }
23
+
24
+ function textContent(text: string): WorkspaceFileContent {
25
+ return { text, isBinary: false, size: text.length, encoding: "utf-8" };
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // useWorkspaceFileContent
30
+ // ---------------------------------------------------------------------------
31
+
32
+ describe("useWorkspaceFileContent", () => {
33
+ it("resolves file content on success and is not unsupported", async () => {
34
+ const reader: WorkspaceFileReader = vi.fn(async () => textContent("hello"));
35
+ const { result } = renderHook(() =>
36
+ useWorkspaceFileContent({ entry: gitEntry(), path: "a.ts", reader }),
37
+ );
38
+
39
+ expect(result.current.isLoading).toBe(true);
40
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
41
+
42
+ expect(result.current.content?.text).toBe("hello");
43
+ expect(result.current.isUnsupported).toBe(false);
44
+ expect(result.current.error).toBeNull();
45
+ });
46
+
47
+ it("reports isUnsupported when the reader resolves null", async () => {
48
+ const reader: WorkspaceFileReader = vi.fn(async () => null);
49
+ const { result } = renderHook(() =>
50
+ useWorkspaceFileContent({ entry: gitEntry(), path: "a.ts", reader }),
51
+ );
52
+
53
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
54
+
55
+ expect(result.current.content).toBeNull();
56
+ expect(result.current.isUnsupported).toBe(true);
57
+ expect(result.current.error).toBeNull();
58
+ });
59
+
60
+ it("surfaces a reader throw as error, not unsupported", async () => {
61
+ const reader: WorkspaceFileReader = vi.fn(async () => {
62
+ throw new Error("boom");
63
+ });
64
+ const { result } = renderHook(() =>
65
+ useWorkspaceFileContent({ entry: gitEntry(), path: "a.ts", reader }),
66
+ );
67
+
68
+ await waitFor(() => expect(result.current.error).not.toBeNull());
69
+
70
+ expect(result.current.error?.message).toBe("boom");
71
+ expect(result.current.isUnsupported).toBe(false);
72
+ expect(result.current.content).toBeNull();
73
+ });
74
+
75
+ it("stays idle and unsupported when no reader is injected", () => {
76
+ const { result } = renderHook(() =>
77
+ useWorkspaceFileContent({ entry: gitEntry(), path: "a.ts", reader: undefined }),
78
+ );
79
+
80
+ expect(result.current.isLoading).toBe(false);
81
+ expect(result.current.isUnsupported).toBe(true);
82
+ expect(result.current.content).toBeNull();
83
+ });
84
+
85
+ it("does not treat a binary file (present object, text null) as unsupported", async () => {
86
+ const binary: WorkspaceFileContent = {
87
+ text: null,
88
+ isBinary: true,
89
+ size: 10,
90
+ encoding: "unknown",
91
+ };
92
+ const reader: WorkspaceFileReader = vi.fn(async () => binary);
93
+ const { result } = renderHook(() =>
94
+ useWorkspaceFileContent({ entry: gitEntry(), path: "img.png", reader }),
95
+ );
96
+
97
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
98
+
99
+ expect(result.current.content).toEqual(binary);
100
+ expect(result.current.isUnsupported).toBe(false);
101
+ });
102
+
103
+ it("stays idle and never calls the reader when no file is selected", () => {
104
+ const reader = vi.fn(async () => textContent("x")) as WorkspaceFileReader;
105
+ const { result } = renderHook(() =>
106
+ useWorkspaceFileContent({ entry: null, path: null, reader }),
107
+ );
108
+
109
+ expect(result.current.isLoading).toBe(false);
110
+ expect(result.current.isUnsupported).toBe(false);
111
+ expect(reader).not.toHaveBeenCalled();
112
+ });
113
+
114
+ it("re-reads on refetch()", async () => {
115
+ let n = 0;
116
+ const reader: WorkspaceFileReader = vi.fn(async () => textContent(`v${++n}`));
117
+ const { result } = renderHook(() =>
118
+ useWorkspaceFileContent({ entry: gitEntry(), path: "a.ts", reader }),
119
+ );
120
+
121
+ await waitFor(() => expect(result.current.content?.text).toBe("v1"));
122
+
123
+ act(() => {
124
+ result.current.refetch();
125
+ });
126
+
127
+ await waitFor(() => expect(result.current.content?.text).toBe("v2"));
128
+ expect(reader).toHaveBeenCalledTimes(2);
129
+ });
130
+
131
+ it("re-reads when the path changes", async () => {
132
+ const reader: WorkspaceFileReader = vi.fn(async (_e, p) =>
133
+ textContent(`content:${p}`),
134
+ );
135
+ const { result, rerender } = renderHook(
136
+ (props: { path: string }) =>
137
+ useWorkspaceFileContent({ entry: gitEntry(), path: props.path, reader }),
138
+ { initialProps: { path: "a.ts" } },
139
+ );
140
+
141
+ await waitFor(() => expect(result.current.content?.text).toBe("content:a.ts"));
142
+
143
+ rerender({ path: "b.ts" });
144
+
145
+ await waitFor(() => expect(result.current.content?.text).toBe("content:b.ts"));
146
+ });
147
+
148
+ it("ignores a stale slow read when the path changes mid-flight", async () => {
149
+ const deferred = new Map<string, (v: WorkspaceFileContent) => void>();
150
+ const reader: WorkspaceFileReader = (_e, p) =>
151
+ new Promise((resolve) => deferred.set(p, resolve));
152
+
153
+ const { result, rerender } = renderHook(
154
+ (props: { path: string }) =>
155
+ useWorkspaceFileContent({ entry: gitEntry(), path: props.path, reader }),
156
+ { initialProps: { path: "slow.ts" } },
157
+ );
158
+
159
+ // Switch to a second file before the first resolves.
160
+ rerender({ path: "fast.ts" });
161
+
162
+ // Resolve the current (fast) read first.
163
+ await act(async () => {
164
+ deferred.get("fast.ts")!(textContent("FAST"));
165
+ });
166
+ await waitFor(() => expect(result.current.content?.text).toBe("FAST"));
167
+
168
+ // The stale (slow) read resolves later — it must NOT overwrite FAST.
169
+ await act(async () => {
170
+ deferred.get("slow.ts")!(textContent("SLOW"));
171
+ });
172
+ expect(result.current.content?.text).toBe("FAST");
173
+ });
174
+
175
+ it("goes idle when the entry is removed (entry -> null)", async () => {
176
+ const reader: WorkspaceFileReader = vi.fn(async () => textContent("x"));
177
+ const { result, rerender } = renderHook(
178
+ (props: { entry: WorkspaceEntry | null }) =>
179
+ useWorkspaceFileContent({ entry: props.entry, path: "a.ts", reader }),
180
+ { initialProps: { entry: gitEntry() as WorkspaceEntry | null } },
181
+ );
182
+
183
+ await waitFor(() => expect(result.current.content?.text).toBe("x"));
184
+
185
+ rerender({ entry: null });
186
+
187
+ await waitFor(() => expect(result.current.content).toBeNull());
188
+ expect(result.current.isLoading).toBe(false);
189
+ });
190
+ });