@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,227 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { render, screen, fireEvent, cleanup } from "@testing-library/react";
3
+ import { WorkspaceFileSearch } from "../WorkspaceFileSearch";
4
+ import { __clearWorkspaceListingCache } from "../workspaceListingCache";
5
+ import type { WorkspaceEntry } from "../useWorkspaceEntries";
6
+ import type { WorkspaceFileEntry, WorkspaceFileLister } from "../WorkspaceFileLister";
7
+
8
+ let entryIdCounter = 0;
9
+
10
+ function makeEntry(overrides?: Partial<WorkspaceEntry>): WorkspaceEntry {
11
+ return {
12
+ id: `ws-${++entryIdCounter}`,
13
+ name: "acme/api",
14
+ type: "git",
15
+ gitUrl: "https://github.com/acme/api",
16
+ gitBranch: "main",
17
+ ...overrides,
18
+ };
19
+ }
20
+
21
+ const FILES: WorkspaceFileEntry[] = [
22
+ { path: "src/index.ts", isDirectory: false },
23
+ { path: "src/button.tsx", isDirectory: false },
24
+ { path: "README.md", isDirectory: false },
25
+ ];
26
+
27
+ function typeQuery(value: string) {
28
+ const input = screen.getByRole("combobox");
29
+ fireEvent.change(input, { target: { value } });
30
+ return input;
31
+ }
32
+
33
+ describe("WorkspaceFileSearch", () => {
34
+ beforeEach(() => {
35
+ __clearWorkspaceListingCache();
36
+ });
37
+ afterEach(cleanup);
38
+
39
+ it("shows an unavailable state and disables input when no lister is injected", () => {
40
+ render(
41
+ <WorkspaceFileSearch
42
+ entries={[makeEntry()]}
43
+ lister={undefined}
44
+ onOpenFile={vi.fn()}
45
+ />,
46
+ );
47
+ expect(screen.getByText(/search isn.t available here/i)).toBeTruthy();
48
+ expect(screen.getByRole("combobox")).toHaveProperty("disabled", true);
49
+ });
50
+
51
+ it("shows a hint when the query is empty", () => {
52
+ render(
53
+ <WorkspaceFileSearch
54
+ entries={[makeEntry()]}
55
+ lister={vi.fn(async () => FILES)}
56
+ onOpenFile={vi.fn()}
57
+ />,
58
+ );
59
+ expect(
60
+ screen.getByText(/search files by name across the whole workspace/i),
61
+ ).toBeTruthy();
62
+ });
63
+
64
+ it("renders a flat result list and opens a file on click (single entry)", async () => {
65
+ const entry = makeEntry();
66
+ const onOpenFile = vi.fn();
67
+ render(
68
+ <WorkspaceFileSearch
69
+ entries={[entry]}
70
+ lister={vi.fn(async () => FILES)}
71
+ onOpenFile={onOpenFile}
72
+ />,
73
+ );
74
+
75
+ typeQuery("button");
76
+ const option = await screen.findByRole("option");
77
+ fireEvent.click(option);
78
+
79
+ expect(onOpenFile).toHaveBeenCalledWith(entry.id, "src/button.tsx");
80
+ // Single entry → no group header.
81
+ expect(screen.queryByText("acme/api")).toBeNull();
82
+ });
83
+
84
+ it("groups results by entry when more than one entry matches", async () => {
85
+ const a = makeEntry({ name: "acme/api" });
86
+ const b = makeEntry({ name: "acme/web" });
87
+ const lister: WorkspaceFileLister = vi.fn(async (e) =>
88
+ e.id === a.id
89
+ ? [{ path: "src/button.tsx", isDirectory: false }]
90
+ : [{ path: "lib/button.ts", isDirectory: false }],
91
+ );
92
+ render(
93
+ <WorkspaceFileSearch entries={[a, b]} lister={lister} onOpenFile={vi.fn()} />,
94
+ );
95
+
96
+ typeQuery("button");
97
+ const options = await screen.findAllByRole("option");
98
+ expect(options).toHaveLength(2);
99
+ expect(screen.getByText("acme/api")).toBeTruthy();
100
+ expect(screen.getByText("acme/web")).toBeTruthy();
101
+ });
102
+
103
+ it("highlights the matched substring", async () => {
104
+ const { container } = render(
105
+ <WorkspaceFileSearch
106
+ entries={[makeEntry()]}
107
+ lister={vi.fn(async () => FILES)}
108
+ onOpenFile={vi.fn()}
109
+ />,
110
+ );
111
+ typeQuery("button");
112
+ await screen.findByRole("option");
113
+ const highlight = container.querySelector(".font-semibold");
114
+ expect(highlight?.textContent).toBe("button");
115
+ });
116
+
117
+ it("marks the currently open file with aria-current", async () => {
118
+ const entry = makeEntry();
119
+ render(
120
+ <WorkspaceFileSearch
121
+ entries={[entry]}
122
+ lister={vi.fn(async () => FILES)}
123
+ onOpenFile={vi.fn()}
124
+ selectedFile={{ entryId: entry.id, path: "src/button.tsx" }}
125
+ />,
126
+ );
127
+ typeQuery("button");
128
+ const option = await screen.findByRole("option");
129
+ expect(option.getAttribute("aria-current")).toBe("true");
130
+ });
131
+
132
+ it("shows a no-match state", async () => {
133
+ render(
134
+ <WorkspaceFileSearch
135
+ entries={[makeEntry()]}
136
+ lister={vi.fn(async () => FILES)}
137
+ onOpenFile={vi.fn()}
138
+ />,
139
+ );
140
+ typeQuery("zzzzz");
141
+ // Assert the visible message, not the parallel sr-only live-region copy.
142
+ expect(
143
+ await screen.findByText(/no files matching/i, { ignore: ".sr-only" }),
144
+ ).toBeTruthy();
145
+ });
146
+
147
+ it("renders a truncation notice from a notice entry", async () => {
148
+ render(
149
+ <WorkspaceFileSearch
150
+ entries={[makeEntry()]}
151
+ lister={vi.fn(async () => [
152
+ { path: "src/button.tsx", isDirectory: false },
153
+ { path: "... truncated", isDirectory: false, notice: true as const },
154
+ ])}
155
+ onOpenFile={vi.fn()}
156
+ />,
157
+ );
158
+ typeQuery("button");
159
+ await screen.findByRole("option");
160
+ expect(screen.getByText(/too many files to search in full/i)).toBeTruthy();
161
+ });
162
+
163
+ it("moves virtual focus with ArrowDown and opens on Enter", async () => {
164
+ const entry = makeEntry();
165
+ const onOpenFile = vi.fn();
166
+ render(
167
+ <WorkspaceFileSearch
168
+ entries={[entry]}
169
+ lister={vi.fn(async () => FILES)}
170
+ onOpenFile={onOpenFile}
171
+ />,
172
+ );
173
+
174
+ const input = typeQuery("src"); // matches src/index.ts and src/button.tsx
175
+ await screen.findAllByRole("option");
176
+
177
+ fireEvent.keyDown(input, { key: "ArrowDown" });
178
+ const active = input.getAttribute("aria-activedescendant");
179
+ expect(active).toBeTruthy();
180
+ const focusedOption = document.getElementById(active!);
181
+ expect(focusedOption?.getAttribute("aria-selected")).toBe("true");
182
+
183
+ fireEvent.keyDown(input, { key: "Enter" });
184
+ expect(onOpenFile).toHaveBeenCalledTimes(1);
185
+ expect(onOpenFile.mock.calls[0][0]).toBe(entry.id);
186
+ });
187
+
188
+ it("announces result status in a polite live region", async () => {
189
+ render(
190
+ <WorkspaceFileSearch
191
+ entries={[makeEntry()]}
192
+ lister={vi.fn(async () => FILES)}
193
+ onOpenFile={vi.fn()}
194
+ />,
195
+ );
196
+ const status = screen.getByRole("status");
197
+ expect(status.getAttribute("aria-live")).toBe("polite");
198
+
199
+ typeQuery("button");
200
+ await screen.findByRole("option");
201
+ expect(status.textContent).toMatch(/matching file/i);
202
+
203
+ typeQuery("zzzzz");
204
+ await screen.findByText(/no files matching/i, { ignore: ".sr-only" });
205
+ expect(status.textContent).toMatch(/no files matching/i);
206
+ });
207
+
208
+ it("caps rendered results and reports the total", async () => {
209
+ const entry = makeEntry();
210
+ const many: WorkspaceFileEntry[] = Array.from({ length: 250 }, (_, i) => ({
211
+ path: `src/match-${i}.ts`,
212
+ isDirectory: false,
213
+ }));
214
+ render(
215
+ <WorkspaceFileSearch
216
+ entries={[entry]}
217
+ lister={vi.fn(async () => many)}
218
+ onOpenFile={vi.fn()}
219
+ />,
220
+ );
221
+
222
+ typeQuery("match");
223
+ await screen.findAllByRole("option");
224
+ expect(screen.getAllByRole("option")).toHaveLength(200);
225
+ expect(screen.getByText(/showing the first 200 of 250 matches/i)).toBeTruthy();
226
+ });
227
+ });
@@ -0,0 +1,418 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { render, screen, cleanup, fireEvent, within } from "@testing-library/react";
3
+ import { forwardRef } from "react";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // WorkspaceSurface composes the activity rail (Explorer / Search), a resizable
7
+ // sidebar, and the read-only editor. The explorer tree, search, and viewer are
8
+ // exercised by their own suites; here we mock them as probes and prove the
9
+ // surface's own composition — rail switching, the editor tabs/toolbar, collapse,
10
+ // breadcrumbs, and that the active file (+ its change) reaches a chrome-less
11
+ // FileViewer.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ type CapturedProps = Record<string, unknown>;
15
+
16
+ const viewerProps: CapturedProps[] = [];
17
+ vi.mock("../FileViewer", () => ({
18
+ FileViewer: forwardRef((props: CapturedProps, _ref) => {
19
+ viewerProps.push(props);
20
+ return <div data-testid="viewer-probe" />;
21
+ }),
22
+ }));
23
+
24
+ vi.mock("../ExplorerTree", () => ({
25
+ ExplorerTree: () => <div data-testid="explorer-probe" />,
26
+ }));
27
+
28
+ vi.mock("../WorkspaceFileSearch", () => ({
29
+ WorkspaceFileSearch: () => <div data-testid="search-probe" />,
30
+ }));
31
+
32
+ vi.mock("../WorkspaceContentSearch", () => ({
33
+ WorkspaceContentSearch: () => <div data-testid="content-search-probe" />,
34
+ }));
35
+
36
+ import { WorkspaceSurface } from "../WorkspaceSurface";
37
+ import { virtualEntryId } from "../../internal/store/index.js";
38
+ import type { WorkspaceEntry } from "../useWorkspaceEntries";
39
+ import type { WorkspaceFileLister } from "../WorkspaceFileLister";
40
+ import type { WorkspaceFileReader } from "../WorkspaceFileReader";
41
+ import type { WorkspaceContentSearcher } from "../WorkspaceContentSearcher";
42
+
43
+ const gitEntry: WorkspaceEntry = {
44
+ id: "e1",
45
+ name: "acme/app",
46
+ type: "git",
47
+ gitUrl: "https://github.com/acme/app.git",
48
+ gitBranch: "main",
49
+ } as WorkspaceEntry;
50
+
51
+ const lister: WorkspaceFileLister = vi.fn(async () => []) as unknown as WorkspaceFileLister;
52
+ const reader: WorkspaceFileReader = vi.fn(async () => null) as unknown as WorkspaceFileReader;
53
+ const searcher: WorkspaceContentSearcher = vi.fn(async () => ({
54
+ matches: [],
55
+ truncated: false,
56
+ })) as unknown as WorkspaceContentSearcher;
57
+
58
+ function renderSurface(overrides: Partial<React.ComponentProps<typeof WorkspaceSurface>> = {}) {
59
+ const onOpenFile = vi.fn();
60
+ const onActivateEditor = vi.fn();
61
+ const onPinEditor = vi.fn();
62
+ const onCloseEditor = vi.fn();
63
+ const onCollapse = vi.fn();
64
+ render(
65
+ <WorkspaceSurface
66
+ entries={[gitEntry]}
67
+ lister={lister}
68
+ reader={reader}
69
+ editors={[{ entryId: "e1", path: "src/main.go", preview: false }]}
70
+ selectedFile={{ entryId: "e1", path: "src/main.go" }}
71
+ onOpenFile={onOpenFile}
72
+ onActivateEditor={onActivateEditor}
73
+ onPinEditor={onPinEditor}
74
+ onCloseEditor={onCloseEditor}
75
+ onCollapse={onCollapse}
76
+ {...overrides}
77
+ />,
78
+ );
79
+ return { onOpenFile, onActivateEditor, onPinEditor, onCloseEditor, onCollapse };
80
+ }
81
+
82
+ beforeEach(() => {
83
+ viewerProps.length = 0;
84
+ });
85
+ afterEach(() => {
86
+ cleanup();
87
+ vi.clearAllMocks();
88
+ });
89
+
90
+ describe("WorkspaceSurface", () => {
91
+ it("renders the activity rail with Explorer selected by default", () => {
92
+ renderSurface();
93
+ const explorer = screen.getByRole("radio", { name: "Explorer" });
94
+ const search = screen.getByRole("radio", { name: "Search" });
95
+ expect(explorer.getAttribute("aria-checked")).toBe("true");
96
+ expect(search.getAttribute("aria-checked")).toBe("false");
97
+ // Explorer view: the explorer tree renders; the search surface does not.
98
+ expect(screen.getByTestId("explorer-probe")).toBeTruthy();
99
+ expect(screen.queryByTestId("search-probe")).toBeNull();
100
+ });
101
+
102
+ it("switches to the search surface when Search is selected", () => {
103
+ renderSurface();
104
+ fireEvent.click(screen.getByRole("radio", { name: "Search" }));
105
+ expect(screen.getByTestId("search-probe")).toBeTruthy();
106
+ expect(screen.queryByTestId("explorer-probe")).toBeNull();
107
+ });
108
+
109
+ it("shows only filename search (no Name|Text toggle) when no content searcher is injected", () => {
110
+ renderSurface();
111
+ fireEvent.click(screen.getByRole("radio", { name: "Search" }));
112
+ expect(screen.queryByRole("radiogroup", { name: "Search mode" })).toBeNull();
113
+ expect(screen.getByTestId("search-probe")).toBeTruthy();
114
+ expect(screen.queryByTestId("content-search-probe")).toBeNull();
115
+ });
116
+
117
+ it("offers a Name|Text toggle when a content searcher is injected, routing Text to content search", () => {
118
+ renderSurface({ searcher });
119
+ fireEvent.click(screen.getByRole("radio", { name: "Search" }));
120
+
121
+ const toggle = screen.getByRole("radiogroup", { name: "Search mode" });
122
+ expect(toggle).toBeTruthy();
123
+ // Filename search is the default mode.
124
+ expect(screen.getByTestId("search-probe")).toBeTruthy();
125
+ expect(screen.queryByTestId("content-search-probe")).toBeNull();
126
+
127
+ // Switching to Text routes to the content-search surface.
128
+ fireEvent.click(within(toggle).getByRole("radio", { name: "Text" }));
129
+ expect(screen.getByTestId("content-search-probe")).toBeTruthy();
130
+ expect(screen.queryByTestId("search-probe")).toBeNull();
131
+ });
132
+
133
+ it("hands the open file (and its change) to a chrome-less FileViewer", () => {
134
+ const change = { path: "src/main.go" } as never;
135
+ renderSurface({ change });
136
+ const props = viewerProps.at(-1);
137
+ expect(props?.selectedFile).toEqual({ entryId: "e1", path: "src/main.go" });
138
+ expect(props?.showHeader).toBe(false);
139
+ expect(props?.change).toBe(change);
140
+ });
141
+
142
+ it("forwards a jump-to-line reveal to the FileViewer", () => {
143
+ const reveal = { line: 12, nonce: 3 };
144
+ renderSurface({ reveal });
145
+ expect(viewerProps.at(-1)?.reveal).toBe(reveal);
146
+ });
147
+
148
+ it("shows the open file as a tab", () => {
149
+ renderSurface();
150
+ const tab = screen.getByRole("tab", { name: /main\.go/ });
151
+ expect(tab).toBeTruthy();
152
+ expect(tab.getAttribute("aria-selected")).toBe("true");
153
+ });
154
+
155
+ it("associates the active tab with the single editor tabpanel", () => {
156
+ renderSurface();
157
+ const tab = screen.getByRole("tab", { name: /main\.go/ });
158
+ const panel = screen.getByRole("tabpanel");
159
+ const panelId = panel.getAttribute("id");
160
+ expect(panelId).toBeTruthy();
161
+ expect(tab.getAttribute("id")).toBeTruthy();
162
+ // Tab -> panel and panel -> active tab, both directions wired.
163
+ expect(tab.getAttribute("aria-controls")).toBe(panelId);
164
+ expect(panel.getAttribute("aria-labelledby")).toBe(tab.getAttribute("id"));
165
+ });
166
+
167
+ it("renders no tabpanel when no editor is open (nothing to label)", () => {
168
+ renderSurface({ selectedFile: null, editors: [] });
169
+ expect(screen.queryByRole("tabpanel")).toBeNull();
170
+ });
171
+
172
+ it("collapses back to chat when the toolbar control is clicked", () => {
173
+ const { onCollapse } = renderSurface();
174
+ fireEvent.click(screen.getByRole("button", { name: "Back to chat" }));
175
+ expect(onCollapse).toHaveBeenCalledTimes(1);
176
+ });
177
+
178
+ it("shows an empty editor when no file is selected", () => {
179
+ renderSurface({ selectedFile: null, editors: [] });
180
+ expect(viewerProps.length).toBe(0);
181
+ expect(screen.getByText("Select a file to view its contents.")).toBeTruthy();
182
+ });
183
+ });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Host-injected rail views (extraViews) — the unified-panel extension seam
187
+ // ---------------------------------------------------------------------------
188
+
189
+ const configView = {
190
+ id: "configure",
191
+ label: "Config",
192
+ icon: <span data-testid="config-icon" />,
193
+ content: <div data-testid="config-probe" />,
194
+ };
195
+
196
+ const changesView = {
197
+ id: "changes",
198
+ label: "Changes",
199
+ icon: <span />,
200
+ badge: 3,
201
+ content: <div data-testid="changes-probe" />,
202
+ };
203
+
204
+ describe("WorkspaceSurface extraViews", () => {
205
+ it("renders injected views in the rail after the built-ins", () => {
206
+ renderSurface({ extraViews: [configView, changesView] });
207
+ const radios = screen.getAllByRole("radio");
208
+ expect(radios.map((r) => r.getAttribute("title"))).toEqual([
209
+ "Explorer",
210
+ "Search",
211
+ "Config",
212
+ "Changes",
213
+ ]);
214
+ });
215
+
216
+ it("shows a count badge on a rail view and folds it into the accessible name", () => {
217
+ renderSurface({ extraViews: [changesView] });
218
+ const changes = screen.getByRole("radio", { name: "Changes (3)" });
219
+ expect(changes.textContent).toContain("3");
220
+ });
221
+
222
+ it("renders an extra view's content in the sidebar (with heading) when selected, keeping the editor area", () => {
223
+ renderSurface({ extraViews: [configView] });
224
+ fireEvent.click(screen.getByRole("radio", { name: "Config" }));
225
+ expect(screen.getByTestId("config-probe")).toBeTruthy();
226
+ expect(screen.queryByTestId("explorer-probe")).toBeNull();
227
+ // Sidebar heading + the editor area (file tab) both present.
228
+ expect(screen.getByText("Config")).toBeTruthy();
229
+ expect(screen.getByRole("tab", { name: /main\.go/ })).toBeTruthy();
230
+ });
231
+
232
+ it("supports controlled view selection via view/onViewChange", () => {
233
+ const onViewChange = vi.fn();
234
+ renderSurface({
235
+ extraViews: [configView],
236
+ view: "configure",
237
+ onViewChange,
238
+ });
239
+ expect(screen.getByTestId("config-probe")).toBeTruthy();
240
+ fireEvent.click(screen.getByRole("radio", { name: "Search" }));
241
+ expect(onViewChange).toHaveBeenCalledWith("search");
242
+ // Controlled: the view does not change until the owner re-renders.
243
+ expect(screen.getByTestId("config-probe")).toBeTruthy();
244
+ });
245
+
246
+ it("falls back to the explorer when the active view id no longer exists", () => {
247
+ // e.g. a contextual view (Inspect) that disappeared while active.
248
+ renderSurface({ extraViews: [], view: "inspect" });
249
+ expect(screen.getByTestId("explorer-probe")).toBeTruthy();
250
+ expect(
251
+ screen.getByRole("radio", { name: "Explorer" }).getAttribute("aria-checked"),
252
+ ).toBe("true");
253
+ });
254
+
255
+ it("moves across built-in and injected views with arrow keys", () => {
256
+ renderSurface({ extraViews: [configView] });
257
+ const search = screen.getByRole("radio", { name: "Search" });
258
+ fireEvent.click(search);
259
+ fireEvent.keyDown(search, { key: "ArrowDown" });
260
+ expect(screen.getByTestId("config-probe")).toBeTruthy();
261
+ });
262
+
263
+ it("reaches an injected view beyond the second by keyboard (focus follows selection)", () => {
264
+ // Regression guard: the old handler moved selection but never DOM focus, so
265
+ // focus stayed pinned on the entered button and every view past the second
266
+ // (Config, Changes, …) was keyboard-unreachable. Each arrow must advance
267
+ // focus so the next arrow computes from the new position.
268
+ renderSurface({ extraViews: [configView, changesView] });
269
+ const explorer = screen.getByRole("radio", { name: "Explorer" });
270
+ explorer.focus();
271
+ expect(document.activeElement).toBe(explorer);
272
+
273
+ // Step 1: Explorer -> Search, focus moves with the selection.
274
+ fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
275
+ const search = screen.getByRole("radio", { name: "Search" });
276
+ expect(document.activeElement).toBe(search);
277
+ expect(search.getAttribute("aria-checked")).toBe("true");
278
+
279
+ // Step 2: Search -> Config. The step the old code could not make.
280
+ fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
281
+ const config = screen.getByRole("radio", { name: "Config" });
282
+ expect(document.activeElement).toBe(config);
283
+ expect(config.getAttribute("aria-checked")).toBe("true");
284
+ expect(screen.getByTestId("config-probe")).toBeTruthy();
285
+ });
286
+
287
+ it("jumps to the last and first rail views with End and Home", () => {
288
+ renderSurface({ extraViews: [configView, changesView] });
289
+ const explorer = screen.getByRole("radio", { name: "Explorer" });
290
+ explorer.focus();
291
+
292
+ fireEvent.keyDown(document.activeElement!, { key: "End" });
293
+ const changes = screen.getByRole("radio", { name: "Changes (3)" });
294
+ expect(document.activeElement).toBe(changes);
295
+ expect(changes.getAttribute("aria-checked")).toBe("true");
296
+
297
+ fireEvent.keyDown(document.activeElement!, { key: "Home" });
298
+ const explorerAgain = screen.getByRole("radio", { name: "Explorer" });
299
+ expect(document.activeElement).toBe(explorerAgain);
300
+ expect(explorerAgain.getAttribute("aria-checked")).toBe("true");
301
+ });
302
+
303
+ it("moves focus with selection in the Name|Text search-mode toggle", () => {
304
+ renderSurface({ searcher });
305
+ fireEvent.click(screen.getByRole("radio", { name: "Search" }));
306
+ const toggle = screen.getByRole("radiogroup", { name: "Search mode" });
307
+ const name = within(toggle).getByRole("radio", { name: "Name" });
308
+ name.focus();
309
+
310
+ fireEvent.keyDown(document.activeElement!, { key: "ArrowRight" });
311
+ const text = within(toggle).getByRole("radio", { name: "Text" });
312
+ expect(document.activeElement).toBe(text);
313
+ expect(text.getAttribute("aria-checked")).toBe("true");
314
+ });
315
+ });
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Host-injected virtual documents — editor tabs that are not workspace files
319
+ // (the session's plan.md today). Same open/pin/close semantics as file tabs;
320
+ // only the body rendering diverges.
321
+ // ---------------------------------------------------------------------------
322
+
323
+ const PLAN_ENTRY_ID = virtualEntryId("plan");
324
+
325
+ const planDocument = {
326
+ entryId: PLAN_ENTRY_ID,
327
+ path: "plan.md",
328
+ content: <div data-testid="plan-doc-probe" />,
329
+ };
330
+
331
+ describe("WorkspaceSurface virtualDocuments", () => {
332
+ it("renders the virtual document's content — no FileViewer, no breadcrumbs", () => {
333
+ renderSurface({
334
+ virtualDocuments: [planDocument],
335
+ editors: [{ entryId: PLAN_ENTRY_ID, path: "plan.md", preview: false }],
336
+ selectedFile: { entryId: PLAN_ENTRY_ID, path: "plan.md" },
337
+ });
338
+ expect(screen.getByTestId("plan-doc-probe")).toBeTruthy();
339
+ expect(viewerProps.length).toBe(0);
340
+ expect(screen.queryByRole("navigation", { name: "File location" })).toBeNull();
341
+ });
342
+
343
+ it("shows the virtual document as an ordinary tab (basename label)", () => {
344
+ renderSurface({
345
+ virtualDocuments: [planDocument],
346
+ editors: [
347
+ { entryId: "e1", path: "src/main.go", preview: false },
348
+ { entryId: PLAN_ENTRY_ID, path: "plan.md", preview: false },
349
+ ],
350
+ selectedFile: { entryId: PLAN_ENTRY_ID, path: "plan.md" },
351
+ });
352
+ const tab = screen.getByRole("tab", { name: /plan\.md/ });
353
+ expect(tab.getAttribute("aria-selected")).toBe("true");
354
+ });
355
+
356
+ it("keeps file tabs on the FileViewer path while a virtual document is injected", () => {
357
+ renderSurface({
358
+ virtualDocuments: [planDocument],
359
+ editors: [
360
+ { entryId: "e1", path: "src/main.go", preview: false },
361
+ { entryId: PLAN_ENTRY_ID, path: "plan.md", preview: false },
362
+ ],
363
+ selectedFile: { entryId: "e1", path: "src/main.go" },
364
+ });
365
+ expect(screen.queryByTestId("plan-doc-probe")).toBeNull();
366
+ expect(viewerProps.at(-1)?.selectedFile).toEqual({
367
+ entryId: "e1",
368
+ path: "src/main.go",
369
+ });
370
+ });
371
+
372
+ it("a virtual entry id can never alias a real file of the same path", () => {
373
+ // The NUL-namespaced id is the collision guard: a workspace file named
374
+ // plan.md under a real entry must not hijack the virtual document body.
375
+ renderSurface({
376
+ virtualDocuments: [planDocument],
377
+ editors: [{ entryId: "e1", path: "plan.md", preview: false }],
378
+ selectedFile: { entryId: "e1", path: "plan.md" },
379
+ });
380
+ expect(screen.queryByTestId("plan-doc-probe")).toBeNull();
381
+ expect(viewerProps.at(-1)?.selectedFile).toEqual({
382
+ entryId: "e1",
383
+ path: "plan.md",
384
+ });
385
+ });
386
+
387
+ it("scrolls the document body vertically only — wide content reflows, never a sideways scrollbar", () => {
388
+ renderSurface({
389
+ virtualDocuments: [planDocument],
390
+ editors: [{ entryId: PLAN_ENTRY_ID, path: "plan.md", preview: false }],
391
+ selectedFile: { entryId: PLAN_ENTRY_ID, path: "plan.md" },
392
+ });
393
+ const body = screen.getByTestId("plan-doc-probe").parentElement!;
394
+ expect(body.className).toContain("overflow-y-auto");
395
+ // overflow-y alone computes overflow-x to auto; the explicit hidden +
396
+ // min-w-0 pair is what forces documents to reflow at narrow widths.
397
+ expect(body.className).toContain("overflow-x-hidden");
398
+ expect(body.className).toContain("min-w-0");
399
+ });
400
+ });
401
+
402
+ // ---------------------------------------------------------------------------
403
+ // Workspace entry management in the Explorer sidebar
404
+ // ---------------------------------------------------------------------------
405
+
406
+ describe("WorkspaceSurface entry management", () => {
407
+ it("renders an Add Folder footer action when onAddLocalFolder is provided", () => {
408
+ const onAddLocalFolder = vi.fn();
409
+ renderSurface({ onAddLocalFolder });
410
+ fireEvent.click(screen.getByRole("button", { name: "Add Folder" }));
411
+ expect(onAddLocalFolder).toHaveBeenCalledTimes(1);
412
+ });
413
+
414
+ it("omits the Add Folder action without the callback (web has no native picker)", () => {
415
+ renderSurface();
416
+ expect(screen.queryByRole("button", { name: "Add Folder" })).toBeNull();
417
+ });
418
+ });