@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,439 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import {
5
+ FileChangeSchema,
6
+ FileContentSchema,
7
+ type FileChange,
8
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
9
+ import {
10
+ FileChangeCaptureLevel,
11
+ FileChangeType,
12
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
13
+ import type { UseFileChangeContentReturn } from "../../execution/useFileChangeContent";
14
+
15
+ // Drive FileChangeDiff's content deterministically (it is rendered by the diff
16
+ // pane) — same seam FileChangesView's own tests mock.
17
+ let mockFileChangeContent: UseFileChangeContentReturn;
18
+ vi.mock("../../execution/useFileChangeContent", () => ({
19
+ useFileChangeContent: () => mockFileChangeContent,
20
+ }));
21
+
22
+ const { FileViewer } = await import("../FileViewer");
23
+
24
+ afterEach(cleanup);
25
+ beforeEach(() => {
26
+ mockFileChangeContent = {
27
+ beforeText: "alpha\n",
28
+ afterText: "beta\n",
29
+ isBinary: false,
30
+ isLoading: false,
31
+ error: null,
32
+ isTruncated: false,
33
+ downloadUrl: null,
34
+ };
35
+ });
36
+ import type { WorkspaceEntry } from "../useWorkspaceEntries";
37
+ import type {
38
+ WorkspaceFileContent,
39
+ WorkspaceFileReader,
40
+ } from "../WorkspaceFileReader";
41
+
42
+ function wholeFileChange(path: string): FileChange {
43
+ return create(FileChangeSchema, {
44
+ path,
45
+ changeType: FileChangeType.MODIFY,
46
+ captureLevel: FileChangeCaptureLevel.WHOLE_FILE,
47
+ before: create(FileContentSchema, { body: { case: "inline", value: "alpha\n" } }),
48
+ after: create(FileContentSchema, { body: { case: "inline", value: "beta\n" } }),
49
+ });
50
+ }
51
+
52
+ function deleteChange(path: string): FileChange {
53
+ return create(FileChangeSchema, {
54
+ path,
55
+ changeType: FileChangeType.DELETE,
56
+ captureLevel: FileChangeCaptureLevel.WHOLE_FILE,
57
+ before: create(FileContentSchema, { body: { case: "inline", value: "alpha\n" } }),
58
+ });
59
+ }
60
+
61
+ function gitEntry(id = "e1"): WorkspaceEntry {
62
+ return {
63
+ id,
64
+ name: "acme/repo",
65
+ type: "git",
66
+ gitUrl: "https://github.com/acme/repo",
67
+ gitBranch: "main",
68
+ };
69
+ }
70
+
71
+ const ENTRIES = [gitEntry()];
72
+
73
+ function readerFor(result: WorkspaceFileContent | null): WorkspaceFileReader {
74
+ return vi.fn(async () => result);
75
+ }
76
+
77
+ function text(t: string): WorkspaceFileContent {
78
+ return { text: t, isBinary: false, size: t.length, encoding: "utf-8" };
79
+ }
80
+
81
+ describe("FileViewer", () => {
82
+ it("shows the unsupported state when no reader is injected", () => {
83
+ render(
84
+ <FileViewer
85
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
86
+ entries={ENTRIES}
87
+ reader={undefined}
88
+ />,
89
+ );
90
+ expect(screen.getByText(/Preview isn.t available/i)).toBeTruthy();
91
+ });
92
+
93
+ it("shows a loading skeleton while the read is in flight", () => {
94
+ const reader: WorkspaceFileReader = () => new Promise(() => {});
95
+ render(
96
+ <FileViewer
97
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
98
+ entries={ENTRIES}
99
+ reader={reader}
100
+ />,
101
+ );
102
+ expect(screen.getByLabelText("Loading file")).toBeTruthy();
103
+ });
104
+
105
+ it("renders displayable text via the shared renderer", async () => {
106
+ render(
107
+ <FileViewer
108
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
109
+ entries={ENTRIES}
110
+ reader={readerFor(text("const answer = 42;"))}
111
+ />,
112
+ );
113
+ await waitFor(() =>
114
+ expect(screen.getByText("const answer = 42;")).toBeTruthy(),
115
+ );
116
+ });
117
+
118
+ it("shows the binary state for binary content", async () => {
119
+ render(
120
+ <FileViewer
121
+ selectedFile={{ entryId: "e1", path: "logo.png" }}
122
+ entries={ENTRIES}
123
+ reader={readerFor({ text: null, isBinary: true, size: 2048, encoding: "unknown" })}
124
+ />,
125
+ );
126
+ await waitFor(() => expect(screen.getByText(/Binary file/i)).toBeTruthy());
127
+ });
128
+
129
+ it("shows the too-large state for a size-capped read with no text", async () => {
130
+ render(
131
+ <FileViewer
132
+ selectedFile={{ entryId: "e1", path: "big.log" }}
133
+ entries={ENTRIES}
134
+ reader={readerFor({
135
+ text: null,
136
+ isBinary: false,
137
+ size: 20 * 1024 * 1024,
138
+ encoding: "none",
139
+ truncated: true,
140
+ })}
141
+ />,
142
+ );
143
+ await waitFor(() =>
144
+ expect(screen.getByText(/too large to preview/i)).toBeTruthy(),
145
+ );
146
+ });
147
+
148
+ it("shows the undecodable state when text is null without truncation or binary", async () => {
149
+ render(
150
+ <FileViewer
151
+ selectedFile={{ entryId: "e1", path: "weird.dat" }}
152
+ entries={ENTRIES}
153
+ reader={readerFor({ text: null, isBinary: false, size: 5, encoding: "unknown" })}
154
+ />,
155
+ );
156
+ await waitFor(() =>
157
+ expect(screen.getByText(/can.t be displayed as text/i)).toBeTruthy(),
158
+ );
159
+ });
160
+
161
+ it("shows the error state with a working Retry", async () => {
162
+ let attempt = 0;
163
+ const reader: WorkspaceFileReader = vi.fn(async () => {
164
+ attempt += 1;
165
+ if (attempt === 1) throw new Error("network down");
166
+ return text("recovered");
167
+ });
168
+ render(
169
+ <FileViewer
170
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
171
+ entries={ENTRIES}
172
+ reader={reader}
173
+ />,
174
+ );
175
+
176
+ await waitFor(() => expect(screen.getByText("network down")).toBeTruthy());
177
+
178
+ fireEvent.click(screen.getByText("Retry"));
179
+
180
+ await waitFor(() => expect(screen.getByText("recovered")).toBeTruthy());
181
+ expect(reader).toHaveBeenCalledTimes(2);
182
+ });
183
+
184
+ it("shows the empty state when the owning entry has been removed", () => {
185
+ render(
186
+ <FileViewer
187
+ selectedFile={{ entryId: "gone", path: "src/a.ts" }}
188
+ entries={ENTRIES}
189
+ reader={readerFor(text("x"))}
190
+ />,
191
+ );
192
+ expect(screen.getByText(/no longer in the workspace/i)).toBeTruthy();
193
+ });
194
+
195
+ it("renders the basename and directory in the header", async () => {
196
+ render(
197
+ <FileViewer
198
+ selectedFile={{ entryId: "e1", path: "src/deep/module.ts" }}
199
+ entries={ENTRIES}
200
+ reader={readerFor(text("ok"))}
201
+ />,
202
+ );
203
+ expect(screen.getByText("module.ts")).toBeTruthy();
204
+ expect(screen.getByText("src/deep")).toBeTruthy();
205
+ });
206
+
207
+ it("invokes onClose from the close button", async () => {
208
+ const onClose = vi.fn();
209
+ render(
210
+ <FileViewer
211
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
212
+ entries={ENTRIES}
213
+ reader={readerFor(text("ok"))}
214
+ onClose={onClose}
215
+ />,
216
+ );
217
+ fireEvent.click(screen.getByLabelText("Close file viewer"));
218
+ expect(onClose).toHaveBeenCalledTimes(1);
219
+ });
220
+
221
+ // --- no `change` prop: unchanged browse-only behavior (regression) --------
222
+
223
+ it("shows no view toggle when no change is provided", async () => {
224
+ render(
225
+ <FileViewer
226
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
227
+ entries={ENTRIES}
228
+ reader={readerFor(text("plain content"))}
229
+ />,
230
+ );
231
+ await waitFor(() => expect(screen.getByText("plain content")).toBeTruthy());
232
+ expect(screen.queryByRole("radiogroup", { name: "File view" })).toBeNull();
233
+ expect(screen.queryByText(/Live — may differ/i)).toBeNull();
234
+ });
235
+ });
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // Diff mode (Slice 4) — a changed file defaults to its authoritative diff
239
+ // ---------------------------------------------------------------------------
240
+
241
+ describe("FileViewer — diff mode", () => {
242
+ it("defaults a changed file to the Diff view", async () => {
243
+ render(
244
+ <FileViewer
245
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
246
+ entries={ENTRIES}
247
+ reader={readerFor(text("live content"))}
248
+ change={wholeFileChange("src/a.ts")}
249
+ />,
250
+ );
251
+
252
+ const group = screen.getByRole("radiogroup", { name: "File view" });
253
+ const diffRadio = screen.getByRole("radio", { name: "Diff" });
254
+ expect(diffRadio.getAttribute("aria-checked")).toBe("true");
255
+ expect(group).toBeTruthy();
256
+ // The diff (added side) renders; the live file body does not.
257
+ await waitFor(() => expect(screen.getByText("beta")).toBeTruthy());
258
+ expect(screen.queryByText("live content")).toBeNull();
259
+ // Refresh is meaningless for an immutable captured diff.
260
+ expect(screen.queryByLabelText("Reload a.ts")).toBeNull();
261
+ });
262
+
263
+ it("toggles to the live File view, showing the live caption and content", async () => {
264
+ render(
265
+ <FileViewer
266
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
267
+ entries={ENTRIES}
268
+ reader={readerFor(text("live content"))}
269
+ change={wholeFileChange("src/a.ts")}
270
+ />,
271
+ );
272
+
273
+ fireEvent.click(screen.getByRole("radio", { name: "File" }));
274
+
275
+ await waitFor(() => expect(screen.getByText("live content")).toBeTruthy());
276
+ expect(screen.getByText(/Live — may differ from the reviewed change/i)).toBeTruthy();
277
+ expect(screen.getByRole("radio", { name: "File" }).getAttribute("aria-checked")).toBe("true");
278
+ // Refresh returns in the live view.
279
+ expect(screen.getByLabelText("Reload a.ts")).toBeTruthy();
280
+ });
281
+
282
+ it("omits the File option when no reader supports the substrate (diff only)", async () => {
283
+ render(
284
+ <FileViewer
285
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
286
+ entries={ENTRIES}
287
+ reader={undefined}
288
+ change={wholeFileChange("src/a.ts")}
289
+ />,
290
+ );
291
+ await waitFor(() => expect(screen.getByText("beta")).toBeTruthy());
292
+ expect(screen.queryByRole("radiogroup", { name: "File view" })).toBeNull();
293
+ });
294
+
295
+ it("omits the File option for a DELETE change (no current bytes)", async () => {
296
+ render(
297
+ <FileViewer
298
+ selectedFile={{ entryId: "e1", path: "src/gone.ts" }}
299
+ entries={ENTRIES}
300
+ reader={readerFor(text("should not be reachable"))}
301
+ change={deleteChange("src/gone.ts")}
302
+ />,
303
+ );
304
+ await waitFor(() => expect(screen.getByText("alpha")).toBeTruthy());
305
+ expect(screen.queryByRole("radiogroup", { name: "File view" })).toBeNull();
306
+ });
307
+
308
+ it("supports arrow-key roving between Diff and File (radiogroup a11y)", async () => {
309
+ render(
310
+ <FileViewer
311
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
312
+ entries={ENTRIES}
313
+ reader={readerFor(text("live content"))}
314
+ change={wholeFileChange("src/a.ts")}
315
+ />,
316
+ );
317
+
318
+ const diffRadio = screen.getByRole("radio", { name: "Diff" });
319
+ fireEvent.keyDown(diffRadio, { key: "ArrowRight" });
320
+ await waitFor(() =>
321
+ expect(screen.getByRole("radio", { name: "File" }).getAttribute("aria-checked")).toBe("true"),
322
+ );
323
+
324
+ fireEvent.keyDown(screen.getByRole("radio", { name: "File" }), { key: "ArrowLeft" });
325
+ await waitFor(() =>
326
+ expect(screen.getByRole("radio", { name: "Diff" }).getAttribute("aria-checked")).toBe("true"),
327
+ );
328
+ });
329
+
330
+ it("moves focus with selection when roving Diff|File", async () => {
331
+ render(
332
+ <FileViewer
333
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
334
+ entries={ENTRIES}
335
+ reader={readerFor(text("live content"))}
336
+ change={wholeFileChange("src/a.ts")}
337
+ />,
338
+ );
339
+
340
+ const diffRadio = screen.getByRole("radio", { name: "Diff" });
341
+ diffRadio.focus();
342
+ fireEvent.keyDown(diffRadio, { key: "ArrowRight" });
343
+ await waitFor(() =>
344
+ expect(document.activeElement).toBe(screen.getByRole("radio", { name: "File" })),
345
+ );
346
+ });
347
+ });
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // Reveal (jump-to-line) — a line target opens File view and scrolls to it
351
+ // ---------------------------------------------------------------------------
352
+
353
+ describe("FileViewer — reveal (jump-to-line)", () => {
354
+ let scrollIntoView: ReturnType<typeof vi.fn>;
355
+ beforeEach(() => {
356
+ scrollIntoView = vi.fn();
357
+ Element.prototype.scrollIntoView =
358
+ scrollIntoView as unknown as typeof Element.prototype.scrollIntoView;
359
+ });
360
+
361
+ it("opens a changed file in the live File view when a reveal is present (DR-1)", async () => {
362
+ render(
363
+ <FileViewer
364
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
365
+ entries={ENTRIES}
366
+ reader={readerFor(text("live content"))}
367
+ change={wholeFileChange("src/a.ts")}
368
+ reveal={{ line: 1, nonce: 1 }}
369
+ />,
370
+ );
371
+
372
+ // File view wins over the diff-default, but the Diff toggle stays available.
373
+ expect(screen.getByRole("radio", { name: "File" }).getAttribute("aria-checked")).toBe("true");
374
+ expect(screen.getByRole("radio", { name: "Diff" })).toBeTruthy();
375
+ await waitFor(() => expect(screen.getByText("live content")).toBeTruthy());
376
+ });
377
+
378
+ it("highlights the revealed line and scrolls to it", async () => {
379
+ const { container } = render(
380
+ <FileViewer
381
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
382
+ entries={ENTRIES}
383
+ reader={readerFor(text("one\ntwo\nthree"))}
384
+ reveal={{ line: 2, nonce: 1 }}
385
+ />,
386
+ );
387
+ await waitFor(() => expect(container.querySelector('[data-line="2"]')).not.toBeNull());
388
+ expect(container.querySelector('[data-line="2"]')?.className).toContain(
389
+ "bg-primary-subtle",
390
+ );
391
+ expect(scrollIntoView).toHaveBeenCalled();
392
+ });
393
+
394
+ it("re-forces the File view when a new reveal nonce arrives after a manual toggle", async () => {
395
+ const { rerender } = render(
396
+ <FileViewer
397
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
398
+ entries={ENTRIES}
399
+ reader={readerFor(text("live content"))}
400
+ change={wholeFileChange("src/a.ts")}
401
+ reveal={{ line: 1, nonce: 1 }}
402
+ />,
403
+ );
404
+ // User flips to Diff.
405
+ fireEvent.click(screen.getByRole("radio", { name: "Diff" }));
406
+ await waitFor(() =>
407
+ expect(screen.getByRole("radio", { name: "Diff" }).getAttribute("aria-checked")).toBe("true"),
408
+ );
409
+
410
+ // A second search hit in the same (already-mounted) file bumps the nonce.
411
+ rerender(
412
+ <FileViewer
413
+ selectedFile={{ entryId: "e1", path: "src/a.ts" }}
414
+ entries={ENTRIES}
415
+ reader={readerFor(text("live content"))}
416
+ change={wholeFileChange("src/a.ts")}
417
+ reveal={{ line: 1, nonce: 2 }}
418
+ />,
419
+ );
420
+ await waitFor(() =>
421
+ expect(screen.getByRole("radio", { name: "File" }).getAttribute("aria-checked")).toBe("true"),
422
+ );
423
+ });
424
+
425
+ it("is a no-op for a DELETE change (no live bytes to anchor)", async () => {
426
+ render(
427
+ <FileViewer
428
+ selectedFile={{ entryId: "e1", path: "src/gone.ts" }}
429
+ entries={ENTRIES}
430
+ reader={readerFor(text("unused"))}
431
+ change={deleteChange("src/gone.ts")}
432
+ reveal={{ line: 1, nonce: 1 }}
433
+ />,
434
+ );
435
+ // Falls back to the diff (deleted side); no view toggle, no crash.
436
+ await waitFor(() => expect(screen.getByText("alpha")).toBeTruthy());
437
+ expect(screen.queryByRole("radiogroup", { name: "File view" })).toBeNull();
438
+ });
439
+ });
@@ -0,0 +1,229 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { render, screen, fireEvent, cleanup } from "@testing-library/react";
3
+ import { WorkspaceContentSearch } from "../WorkspaceContentSearch";
4
+ import type { WorkspaceEntry } from "../useWorkspaceEntries";
5
+ import type {
6
+ WorkspaceContentMatch,
7
+ WorkspaceContentSearchResult,
8
+ WorkspaceContentSearcher,
9
+ } from "../WorkspaceContentSearcher";
10
+
11
+ let entryIdCounter = 0;
12
+
13
+ function makeEntry(overrides?: Partial<WorkspaceEntry>): WorkspaceEntry {
14
+ return {
15
+ id: `ws-${++entryIdCounter}`,
16
+ name: "acme/api",
17
+ type: "local",
18
+ localPath: "/repo",
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ function match(path: string, line: number, preview: string): WorkspaceContentMatch {
24
+ return { path, line, preview };
25
+ }
26
+
27
+ function ok(
28
+ matches: WorkspaceContentMatch[],
29
+ truncated = false,
30
+ ): WorkspaceContentSearchResult {
31
+ return { matches, truncated };
32
+ }
33
+
34
+ function searcherReturning(result: WorkspaceContentSearchResult): WorkspaceContentSearcher {
35
+ return vi.fn(async () => result);
36
+ }
37
+
38
+ function typeQuery(value: string) {
39
+ const input = screen.getByRole("combobox");
40
+ fireEvent.change(input, { target: { value } });
41
+ return input;
42
+ }
43
+
44
+ afterEach(cleanup);
45
+
46
+ describe("WorkspaceContentSearch", () => {
47
+ it("shows an unavailable state and disables input when no searcher is injected", () => {
48
+ render(
49
+ <WorkspaceContentSearch entries={[makeEntry()]} searcher={undefined} onOpenFile={vi.fn()} />,
50
+ );
51
+ expect(screen.getByText(/text search isn.t available here/i)).toBeTruthy();
52
+ expect(screen.getByRole("combobox")).toHaveProperty("disabled", true);
53
+ });
54
+
55
+ it("shows a hint when the query is empty", () => {
56
+ render(
57
+ <WorkspaceContentSearch
58
+ entries={[makeEntry()]}
59
+ searcher={searcherReturning(ok([]))}
60
+ onOpenFile={vi.fn()}
61
+ />,
62
+ );
63
+ expect(screen.getByText(/search file contents across the whole workspace/i)).toBeTruthy();
64
+ });
65
+
66
+ it("shows a min-length hint below the threshold and does not search", () => {
67
+ const searcher = searcherReturning(ok([match("a.ts", 1, "x")]));
68
+ render(
69
+ <WorkspaceContentSearch entries={[makeEntry()]} searcher={searcher} onOpenFile={vi.fn()} />,
70
+ );
71
+ typeQuery("a");
72
+ // Assert the visible message, not the parallel sr-only live-region copy.
73
+ expect(
74
+ screen.getByText(/type at least 2 characters/i, { ignore: ".sr-only" }),
75
+ ).toBeTruthy();
76
+ expect(searcher).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it("renders a file header with line rows and opens the file on click (single entry)", async () => {
80
+ const entry = makeEntry();
81
+ const onOpenFile = vi.fn();
82
+ render(
83
+ <WorkspaceContentSearch
84
+ entries={[entry]}
85
+ searcher={searcherReturning(
86
+ ok([match("src/app.ts", 3, "const foo = 1"), match("src/app.ts", 7, "return foo")]),
87
+ )}
88
+ onOpenFile={onOpenFile}
89
+ />,
90
+ );
91
+
92
+ typeQuery("foo");
93
+ const options = await screen.findAllByRole("option");
94
+ expect(options).toHaveLength(2);
95
+ // File header (basename) present; single entry → no entry header.
96
+ expect(screen.getByText("app.ts")).toBeTruthy();
97
+ expect(screen.queryByText("acme/api")).toBeNull();
98
+
99
+ // Clicking a line hit opens via the shared onOpenFile seam (the same path a
100
+ // tree/filename click uses — this is what makes a changed file honor the
101
+ // FileViewer diff-default; see FileViewer diff-mode suite).
102
+ fireEvent.click(options[0]);
103
+ // The hit's line rides along so the viewer jumps to it (DR-1/DR-2).
104
+ expect(onOpenFile).toHaveBeenCalledWith(entry.id, "src/app.ts", { line: 3 });
105
+ });
106
+
107
+ it("shows an entry header when more than one entry has hits", async () => {
108
+ const a = makeEntry({ name: "acme/api" });
109
+ const b = makeEntry({ name: "acme/web" });
110
+ const searcher: WorkspaceContentSearcher = vi.fn(async (e) =>
111
+ e.id === a.id ? ok([match("a.ts", 1, "foo")]) : ok([match("b.ts", 2, "foo")]),
112
+ );
113
+ render(<WorkspaceContentSearch entries={[a, b]} searcher={searcher} onOpenFile={vi.fn()} />);
114
+
115
+ typeQuery("foo");
116
+ await screen.findAllByRole("option");
117
+ expect(screen.getByText("acme/api")).toBeTruthy();
118
+ expect(screen.getByText("acme/web")).toBeTruthy();
119
+ });
120
+
121
+ it("highlights every occurrence of the query in the preview", async () => {
122
+ const { container } = render(
123
+ <WorkspaceContentSearch
124
+ entries={[makeEntry()]}
125
+ searcher={searcherReturning(ok([match("a.ts", 1, "foo and foo")]))}
126
+ onOpenFile={vi.fn()}
127
+ />,
128
+ );
129
+ typeQuery("foo");
130
+ await screen.findByRole("option");
131
+ const highlights = container.querySelectorAll(".font-semibold");
132
+ expect(Array.from(highlights).map((el) => el.textContent)).toEqual(["foo", "foo"]);
133
+ });
134
+
135
+ it("shows a no-match state", async () => {
136
+ render(
137
+ <WorkspaceContentSearch
138
+ entries={[makeEntry()]}
139
+ searcher={searcherReturning(ok([]))}
140
+ onOpenFile={vi.fn()}
141
+ />,
142
+ );
143
+ typeQuery("zzz");
144
+ expect(
145
+ await screen.findByText(/no files containing/i, { ignore: ".sr-only" }),
146
+ ).toBeTruthy();
147
+ });
148
+
149
+ it("moves virtual focus with ArrowDown and opens on Enter", async () => {
150
+ const entry = makeEntry();
151
+ const onOpenFile = vi.fn();
152
+ render(
153
+ <WorkspaceContentSearch
154
+ entries={[entry]}
155
+ searcher={searcherReturning(
156
+ ok([match("a.ts", 1, "foo"), match("a.ts", 2, "foo")]),
157
+ )}
158
+ onOpenFile={onOpenFile}
159
+ />,
160
+ );
161
+
162
+ const input = typeQuery("foo");
163
+ await screen.findAllByRole("option");
164
+
165
+ fireEvent.keyDown(input, { key: "ArrowDown" });
166
+ const active = input.getAttribute("aria-activedescendant");
167
+ expect(active).toBeTruthy();
168
+ expect(document.getElementById(active!)?.getAttribute("aria-selected")).toBe("true");
169
+
170
+ fireEvent.keyDown(input, { key: "Enter" });
171
+ expect(onOpenFile).toHaveBeenCalledWith(entry.id, "a.ts", { line: 1 });
172
+ });
173
+
174
+ it("caps rendered rows and reports the total", async () => {
175
+ const many = Array.from({ length: 250 }, (_, i) => match("a.ts", i + 1, "foo"));
176
+ render(
177
+ <WorkspaceContentSearch
178
+ entries={[makeEntry()]}
179
+ searcher={searcherReturning(ok(many))}
180
+ onOpenFile={vi.fn()}
181
+ />,
182
+ );
183
+ typeQuery("foo");
184
+ await screen.findAllByRole("option");
185
+ expect(screen.getAllByRole("option")).toHaveLength(200);
186
+ expect(
187
+ screen.getByText(/showing the first 200 of 250 matches/i, {
188
+ ignore: ".sr-only",
189
+ }),
190
+ ).toBeTruthy();
191
+ });
192
+
193
+ it("announces result status in a polite live region", async () => {
194
+ // Content matching is backend-delegated, so the searcher must vary by query.
195
+ const searcher: WorkspaceContentSearcher = vi.fn(async (_e, query) =>
196
+ query === "foo" ? ok([match("a.ts", 1, "foo")]) : ok([]),
197
+ );
198
+ render(
199
+ <WorkspaceContentSearch
200
+ entries={[makeEntry()]}
201
+ searcher={searcher}
202
+ onOpenFile={vi.fn()}
203
+ />,
204
+ );
205
+ const status = screen.getByRole("status");
206
+ expect(status.getAttribute("aria-live")).toBe("polite");
207
+
208
+ typeQuery("foo");
209
+ await screen.findByRole("option");
210
+ expect(status.textContent).toMatch(/match/i);
211
+
212
+ typeQuery("zzz");
213
+ await screen.findByText(/no files containing/i, { ignore: ".sr-only" });
214
+ expect(status.textContent).toMatch(/no files containing/i);
215
+ });
216
+
217
+ it("renders a truncation notice when a search was capped", async () => {
218
+ render(
219
+ <WorkspaceContentSearch
220
+ entries={[makeEntry()]}
221
+ searcher={searcherReturning(ok([match("a.ts", 1, "foo")], true))}
222
+ onOpenFile={vi.fn()}
223
+ />,
224
+ );
225
+ typeQuery("foo");
226
+ await screen.findByRole("option");
227
+ expect(screen.getByText(/more matches exist than shown/i)).toBeTruthy();
228
+ });
229
+ });
@@ -146,6 +146,25 @@ describe("WorkspaceEditor initialPanel", () => {
146
146
  expect(screen.getByText("new-repo")).toBeTruthy();
147
147
  });
148
148
 
149
+ it("labels the manual git inputs for assistive tech (no OAuth connection)", () => {
150
+ // With no gitHubConnection, the panel falls back to manual URL/branch entry.
151
+ render(
152
+ <WorkspaceEditor
153
+ workspace={createMockWorkspace()}
154
+ enableGitHub
155
+ enableLocal={false}
156
+ initialPanel="github"
157
+ />,
158
+ );
159
+ // Accessible names come from aria-label, not the disappearing placeholder.
160
+ expect(
161
+ screen.getByRole("textbox", { name: "Git repository URL" }),
162
+ ).toBeTruthy();
163
+ expect(
164
+ screen.getByRole("textbox", { name: "Branch (optional)" }),
165
+ ).toBeTruthy();
166
+ });
167
+
149
168
  it("shows loading state when GitHub is checking connection with initialPanel='github'", () => {
150
169
  const connection = createMockGitHubConnection({ isLoading: true });
151
170