@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
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useCallback, useEffect, type KeyboardEvent } from "react";
4
+ import { cn } from "@stigmer/theme";
4
5
  import type { UseWorkspaceEntriesReturn } from "./useWorkspaceEntries.js";
5
6
  import type { UseGitHubConnectionReturn } from "../github/useGitHubConnection.js";
6
7
  import { GitHubRepoPicker } from "../github/GitHubRepoPicker.js";
@@ -136,11 +137,11 @@ export function WorkspaceEditor({
136
137
 
137
138
  if (activePanel === "github" && enableGitHub) {
138
139
  return (
139
- <div className={["space-y-2", className].filter(Boolean).join(" ")}>
140
+ <div className={cn("space-y-2", className)}>
140
141
  <button
141
142
  type="button"
142
143
  onClick={goBack}
143
- className="inline-flex items-center gap-1 text-[0.65rem] text-muted-foreground hover:text-foreground transition-colors"
144
+ className="inline-flex items-center gap-1 rounded-sm text-[0.65rem] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
144
145
  >
145
146
  <ChevronLeftIcon />
146
147
  Back
@@ -180,7 +181,7 @@ export function WorkspaceEditor({
180
181
  // ---------------------------------------------------------------------------
181
182
 
182
183
  return (
183
- <div className={["space-y-2", className].filter(Boolean).join(" ")}>
184
+ <div className={cn("space-y-2", className)}>
184
185
  {/* Entry list */}
185
186
  {workspace.entries.length > 0 && (
186
187
  <div className="relative">
@@ -202,10 +203,10 @@ export function WorkspaceEditor({
202
203
  </span>
203
204
  )}
204
205
  <span
205
- className={[
206
+ className={cn(
206
207
  "min-w-0 flex-1 truncate text-foreground",
207
- entry.type === "local" ? "[direction:rtl] text-left" : "",
208
- ].join(" ")}
208
+ entry.type === "local" && "[direction:rtl] text-left",
209
+ )}
209
210
  title={entry.name}
210
211
  >
211
212
  <bdi>{entry.name}</bdi>
@@ -214,7 +215,7 @@ export function WorkspaceEditor({
214
215
  type="button"
215
216
  onClick={() => workspace.remove(entry.id)}
216
217
  disabled={disabled}
217
- className="shrink-0 text-muted-foreground hover:text-destructive disabled:pointer-events-none"
218
+ className="shrink-0 rounded-sm text-muted-foreground transition-colors hover:text-destructive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none"
218
219
  aria-label={`Remove ${entry.name}`}
219
220
  >
220
221
  <XIcon />
@@ -237,7 +238,7 @@ export function WorkspaceEditor({
237
238
  if (path) workspace.addLocalPath(path);
238
239
  }}
239
240
  disabled={disabled}
240
- className="flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-xs text-foreground transition-colors hover:bg-accent-hover disabled:pointer-events-none disabled:opacity-40"
241
+ className="flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-xs text-foreground transition-colors hover:bg-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40"
241
242
  >
242
243
  <FolderIcon />
243
244
  <span className="flex-1 text-left">Browse Folder</span>
@@ -248,7 +249,7 @@ export function WorkspaceEditor({
248
249
  type="button"
249
250
  onClick={() => setActivePanel("github")}
250
251
  disabled={disabled}
251
- className="flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-xs text-foreground transition-colors hover:bg-accent-hover disabled:pointer-events-none disabled:opacity-40"
252
+ className="flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-xs text-foreground transition-colors hover:bg-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40"
252
253
  >
253
254
  <GitHubIcon />
254
255
  <span className="flex-1 text-left">Connect GitHub</span>
@@ -307,14 +308,14 @@ function GitHubPanel({
307
308
  <button
308
309
  type="button"
309
310
  onClick={() => connection.connect(redirectUri, { popup: true })}
310
- className="rounded-md px-2.5 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
311
+ className="rounded-md px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
311
312
  >
312
313
  Try again
313
314
  </button>
314
315
  <button
315
316
  type="button"
316
317
  onClick={() => connection.connect(redirectUri)}
317
- className="inline-flex items-center gap-2 rounded-md bg-foreground px-3 py-1.5 text-xs text-background hover:bg-foreground-hover transition-colors"
318
+ className="inline-flex items-center gap-2 rounded-md bg-foreground px-3 py-1.5 text-xs text-background transition-colors hover:bg-foreground-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
318
319
  >
319
320
  <GitHubIcon />
320
321
  <span>Continue with redirect</span>
@@ -325,7 +326,7 @@ function GitHubPanel({
325
326
  <button
326
327
  type="button"
327
328
  onClick={() => connection.connect(redirectUri, { popup: true })}
328
- className="inline-flex items-center gap-2 rounded-md bg-foreground px-3 py-1.5 text-xs text-background hover:bg-foreground-hover transition-colors"
329
+ className="inline-flex items-center gap-2 rounded-md bg-foreground px-3 py-1.5 text-xs text-background transition-colors hover:bg-foreground-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
329
330
  >
330
331
  <GitHubIcon />
331
332
  <span>Connect GitHub</span>
@@ -353,7 +354,7 @@ function GitHubPanel({
353
354
  <button
354
355
  type="button"
355
356
  onClick={connection.disconnect}
356
- className="text-[0.6rem] text-muted-foreground hover:text-destructive transition-colors"
357
+ className="rounded-sm text-[0.6rem] text-muted-foreground transition-colors hover:text-destructive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
357
358
  >
358
359
  Disconnect
359
360
  </button>
@@ -392,6 +393,7 @@ function ManualGitPanel({
392
393
  <div className="space-y-2">
393
394
  <input
394
395
  type="url"
396
+ aria-label="Git repository URL"
395
397
  placeholder="https://github.com/org/repo.git"
396
398
  value={url}
397
399
  onChange={(e) => onUrlChange(e.target.value)}
@@ -401,6 +403,7 @@ function ManualGitPanel({
401
403
  />
402
404
  <input
403
405
  type="text"
406
+ aria-label="Branch (optional)"
404
407
  placeholder="Branch (optional)"
405
408
  value={branch}
406
409
  onChange={(e) => onBranchChange(e.target.value)}
@@ -411,7 +414,7 @@ function ManualGitPanel({
411
414
  <button
412
415
  type="button"
413
416
  onClick={onCancel}
414
- className="rounded-md px-2.5 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
417
+ className="rounded-md px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
415
418
  >
416
419
  Cancel
417
420
  </button>
@@ -419,7 +422,7 @@ function ManualGitPanel({
419
422
  type="button"
420
423
  onClick={onAdd}
421
424
  disabled={!url.trim()}
422
- className="rounded-md bg-primary px-2.5 py-1 text-xs text-primary-foreground hover:bg-primary-hover transition-colors disabled:opacity-40"
425
+ className="rounded-md bg-primary px-2.5 py-1 text-xs text-primary-foreground transition-colors hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40"
423
426
  >
424
427
  Add
425
428
  </button>
@@ -453,7 +456,7 @@ function GitHubConnectingState({ onCancel }: { onCancel: () => void }) {
453
456
  <button
454
457
  type="button"
455
458
  onClick={onCancel}
456
- className="text-[0.65rem] text-muted-foreground hover:text-foreground transition-colors"
459
+ className="rounded-sm text-[0.65rem] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
457
460
  >
458
461
  Cancel
459
462
  </button>
@@ -12,6 +12,17 @@ export interface WorkspaceFileEntry {
12
12
  readonly path: string;
13
13
  /** Whether this entry is a directory. */
14
14
  readonly isDirectory: boolean;
15
+ /**
16
+ * Marks this entry as an advisory notice rather than an openable file — for
17
+ * example a "listing truncated" message when a repository exceeds the
18
+ * backend's entry limit. When `true`, `path` carries the human-readable
19
+ * message. Consumers must not treat a notice as a real file: it is stripped
20
+ * from the openable listing (never a tree leaf, never a search hit) and is
21
+ * surfaced instead as a single `truncated` banner (see `workspaceListingCache`).
22
+ *
23
+ * Optional and additive: listers that never truncate simply omit it.
24
+ */
25
+ readonly notice?: true;
15
26
  }
16
27
 
17
28
  /**
@@ -0,0 +1,57 @@
1
+ // Content-fetch capability — the byte-reading sibling of WorkspaceFileLister.
2
+ // Domain: workspace. Injected by the host (DD-004) so the SDK never learns how
3
+ // bytes are fetched: GitHub Contents/blob on web, Tauri fs on desktop.
4
+
5
+ import type { WorkspaceEntry } from "./useWorkspaceEntries.js";
6
+
7
+ /**
8
+ * Shared soft cap for a single file read, enforced identically on web and
9
+ * desktop so both substrates truncate at the same boundary. Files larger than
10
+ * this return their first {@link MAX_WORKSPACE_FILE_READ_BYTES} bytes in `text`
11
+ * with `truncated: true`; `size` still reports the full file size.
12
+ */
13
+ export const MAX_WORKSPACE_FILE_READ_BYTES = 1_048_576; // 1 MiB
14
+
15
+ /**
16
+ * Decoded content (plus metadata) for a single workspace file.
17
+ *
18
+ * `text` is `null` whenever the bytes are not displayable as UTF-8 — either
19
+ * binary (`isBinary`) or an undecodable byte sequence (`encoding: "unknown"`).
20
+ * The renderer uses `isBinary`/`text === null` for its fallback state.
21
+ */
22
+ export interface WorkspaceFileContent {
23
+ /** Decoded UTF-8 text, or `null` when binary or undecodable. */
24
+ readonly text: string | null;
25
+ /** Whether the bytes were detected as binary (NUL-byte heuristic). */
26
+ readonly isBinary: boolean;
27
+ /** Full file size in bytes — independent of truncation. */
28
+ readonly size: number;
29
+ /** How the backend delivered the bytes. `"none"` is GitHub's >1 MB signal. */
30
+ readonly encoding: "utf-8" | "base64" | "none" | "unknown";
31
+ /** `true` when `text` holds only the first {@link MAX_WORKSPACE_FILE_READ_BYTES} bytes. */
32
+ readonly truncated?: boolean;
33
+ }
34
+
35
+ /**
36
+ * Platform-injected callback that reads the content of a single workspace file.
37
+ *
38
+ * The byte-reading sibling of {@link WorkspaceFileLister}:
39
+ * the SDK calls it when a user opens a file; the platform decides *how* the
40
+ * bytes are fetched (GitHub on web, Tauri on desktop).
41
+ *
42
+ * Two distinct failure axes — do not conflate them:
43
+ * - Returns `null` **only** when the substrate is unsupported for this entry
44
+ * (git-on-desktop, local-on-web, missing token, non-GitHub URL). This is the
45
+ * honest "unavailable here" state, mirroring the lister's null contract.
46
+ * - **Throws** on a genuine failure (404, network error, 5xx, unreadable file,
47
+ * a directory path). Consumers catch this into a distinct error state — a
48
+ * clicked file that 404s is "failed to load," not "unsupported here."
49
+ *
50
+ * `path` is repo-relative for git entries and root-relative for local entries —
51
+ * exactly the strings the matching `WorkspaceFileLister` emits, so a file-tree
52
+ * node path can be handed straight to the reader with no transformation.
53
+ */
54
+ export type WorkspaceFileReader = (
55
+ entry: WorkspaceEntry,
56
+ path: string,
57
+ ) => Promise<WorkspaceFileContent | null>;
@@ -0,0 +1,452 @@
1
+ "use client";
2
+
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useId,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type KeyboardEvent,
11
+ } from "react";
12
+ import { cn } from "@stigmer/theme";
13
+ import type { WorkspaceEntry } from "./useWorkspaceEntries.js";
14
+ import type { WorkspaceFileLister } from "./WorkspaceFileLister.js";
15
+ import type { SelectedWorkspaceFile } from "../internal/store/workspace-file-selection-store.js";
16
+ import {
17
+ useWorkspaceFileSearch,
18
+ type WorkspaceFileSearchGroup,
19
+ } from "./useWorkspaceFileSearch.js";
20
+ import type { WorkspaceFileMatch } from "./matchWorkspaceFiles.js";
21
+
22
+ /** Cap on rendered rows — keeps the DOM bounded on huge repositories. */
23
+ const MAX_RESULTS = 200;
24
+
25
+ /** Props for {@link WorkspaceFileSearch}. */
26
+ export interface WorkspaceFileSearchProps {
27
+ /** All workspace entries to search across. */
28
+ readonly entries: readonly WorkspaceEntry[];
29
+ /** Platform-injected file lister. `undefined` → search is unavailable. */
30
+ readonly lister: WorkspaceFileLister | undefined;
31
+ /** Opens a result in the viewer. */
32
+ readonly onOpenFile: (entryId: string, path: string) => void;
33
+ /** The file currently open in the viewer, highlighted in the results. */
34
+ readonly selectedFile?: SelectedWorkspaceFile | null;
35
+ /** Additional CSS classes for the root element. */
36
+ readonly className?: string;
37
+ }
38
+
39
+ /** A rendered result, carrying its owning entry and flat index for keyboard nav. */
40
+ interface FlatResult {
41
+ readonly entryId: string;
42
+ readonly entry: WorkspaceEntry;
43
+ readonly match: WorkspaceFileMatch;
44
+ }
45
+
46
+ /**
47
+ * Workspace-wide filename/path search surface.
48
+ *
49
+ * Searches the already-loaded listings across every workspace entry (via
50
+ * {@link useWorkspaceFileSearch}) and opens a hit in the viewer through
51
+ * `onOpenFile` — the same seam a file-tree click uses. Filename-first by design;
52
+ * content search is a separate substrate-specific capability (DD-09).
53
+ *
54
+ * Keyboard/a11y follows the platform's combobox+listbox pattern (see
55
+ * `mcp-server/McpServerPicker`): the input is a `role="combobox"` driving
56
+ * `aria-activedescendant`, results are a `role="listbox"` of `role="option"`
57
+ * rows, and ArrowUp/Down move a virtual focus with Enter to open — focus stays
58
+ * in the input. All visual properties flow through `--stgm-*` tokens (DD-005).
59
+ */
60
+ export function WorkspaceFileSearch({
61
+ entries,
62
+ lister,
63
+ onOpenFile,
64
+ selectedFile,
65
+ className,
66
+ }: WorkspaceFileSearchProps) {
67
+ const [query, setQuery] = useState("");
68
+ const [focusIndex, setFocusIndex] = useState(-1);
69
+
70
+ const inputRef = useRef<HTMLInputElement>(null);
71
+ const listboxId = useId();
72
+ const optionId = useCallback(
73
+ (index: number) => `${listboxId}-opt-${index}`,
74
+ [listboxId],
75
+ );
76
+
77
+ const { groups, totalMatches, isLoading, isUnsupported } =
78
+ useWorkspaceFileSearch({ entries, lister, query });
79
+
80
+ // Flat, capped view of every rendered match — the index space the keyboard
81
+ // and `aria-activedescendant` share with the grouped render below.
82
+ const flatResults = useMemo<readonly FlatResult[]>(() => {
83
+ const flat: FlatResult[] = [];
84
+ for (const group of groups) {
85
+ for (const match of group.matches) {
86
+ if (flat.length >= MAX_RESULTS) return flat;
87
+ flat.push({ entryId: group.entry.id, entry: group.entry, match });
88
+ }
89
+ }
90
+ return flat;
91
+ }, [groups]);
92
+
93
+ // A new query invalidates the previous virtual focus.
94
+ useEffect(() => {
95
+ setFocusIndex(-1);
96
+ }, [query]);
97
+
98
+ // Autofocus on mount — the user switched into Search intending to type.
99
+ useEffect(() => {
100
+ inputRef.current?.focus();
101
+ }, []);
102
+
103
+ // Keep the virtually-focused option in view (no-op where unsupported, e.g. tests).
104
+ useEffect(() => {
105
+ if (focusIndex < 0) return;
106
+ const el =
107
+ typeof document !== "undefined"
108
+ ? document.getElementById(optionId(focusIndex))
109
+ : null;
110
+ el?.scrollIntoView?.({ block: "nearest" });
111
+ }, [focusIndex, optionId]);
112
+
113
+ const handleKeyDown = useCallback(
114
+ (e: KeyboardEvent<HTMLInputElement>) => {
115
+ if (e.key === "ArrowDown") {
116
+ e.preventDefault();
117
+ setFocusIndex((prev) =>
118
+ prev < flatResults.length - 1 ? prev + 1 : prev,
119
+ );
120
+ } else if (e.key === "ArrowUp") {
121
+ e.preventDefault();
122
+ setFocusIndex((prev) => (prev > 0 ? prev - 1 : -1));
123
+ } else if (e.key === "Enter") {
124
+ e.preventDefault();
125
+ const target = flatResults[focusIndex];
126
+ if (target) onOpenFile(target.entryId, target.match.path);
127
+ }
128
+ },
129
+ [flatResults, focusIndex, onOpenFile],
130
+ );
131
+
132
+ const trimmed = query.trim();
133
+ const showGroupHeaders = groups.length > 1;
134
+
135
+ // A concise, screen-reader-only summary of the current result state. It lives
136
+ // in a persistent polite live region (below) so status changes are announced
137
+ // even as the visible states — which live in separate, unmounting branches —
138
+ // swap. Deliberately terse: never the result list itself, only its shape.
139
+ const statusMessage = isUnsupported
140
+ ? ""
141
+ : trimmed.length === 0
142
+ ? ""
143
+ : isLoading && totalMatches === 0 && groups.length === 0
144
+ ? "Searching files…"
145
+ : groups.length === 0
146
+ ? `No files matching ${trimmed}.`
147
+ : totalMatches > flatResults.length
148
+ ? `Showing the first ${flatResults.length} of ${totalMatches} matching files.`
149
+ : `${flatResults.length} matching ${flatResults.length === 1 ? "file" : "files"}.`;
150
+
151
+ return (
152
+ <div className={cn("flex h-full flex-col", className)}>
153
+ <div role="status" aria-live="polite" className="sr-only">
154
+ {statusMessage}
155
+ </div>
156
+ <div className="flex items-center gap-1.5 border-b border-border px-2 py-1.5">
157
+ <SearchIcon />
158
+ <input
159
+ ref={inputRef}
160
+ type="text"
161
+ role="combobox"
162
+ aria-expanded
163
+ aria-controls={listboxId}
164
+ aria-activedescendant={
165
+ focusIndex >= 0 ? optionId(focusIndex) : undefined
166
+ }
167
+ value={query}
168
+ onChange={(e) => setQuery(e.target.value)}
169
+ onKeyDown={handleKeyDown}
170
+ placeholder="Search files by name…"
171
+ disabled={isUnsupported}
172
+ className={cn(
173
+ "min-w-0 flex-1 bg-transparent text-xs text-foreground",
174
+ "placeholder:text-muted-foreground",
175
+ "focus-visible:outline-none disabled:cursor-not-allowed",
176
+ )}
177
+ aria-label="Search workspace files by name"
178
+ />
179
+ {trimmed.length > 0 && (
180
+ <button
181
+ type="button"
182
+ onClick={() => {
183
+ setQuery("");
184
+ inputRef.current?.focus();
185
+ }}
186
+ className="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
187
+ aria-label="Clear search"
188
+ >
189
+ <ClearIcon />
190
+ </button>
191
+ )}
192
+ </div>
193
+
194
+ <div className="min-h-0 flex-1 overflow-y-auto">
195
+ {isUnsupported ? (
196
+ <MessageState>File search isn&rsquo;t available here.</MessageState>
197
+ ) : trimmed.length === 0 ? (
198
+ <MessageState>
199
+ Search files by name across the whole workspace.
200
+ </MessageState>
201
+ ) : isLoading && totalMatches === 0 && groups.length === 0 ? (
202
+ <MessageState>Searching…</MessageState>
203
+ ) : groups.length === 0 ? (
204
+ <MessageState>
205
+ No files matching &ldquo;{trimmed}&rdquo;.
206
+ </MessageState>
207
+ ) : (
208
+ <>
209
+ <ul
210
+ id={listboxId}
211
+ role="listbox"
212
+ aria-label="Search results"
213
+ className="py-0.5"
214
+ >
215
+ {renderRows({
216
+ groups,
217
+ showGroupHeaders,
218
+ selectedFile: selectedFile ?? null,
219
+ focusIndex,
220
+ optionId,
221
+ onOpenFile,
222
+ })}
223
+ </ul>
224
+ {totalMatches > flatResults.length && (
225
+ <p className="border-t border-border px-3 py-1.5 text-[0.65rem] text-muted-foreground">
226
+ Showing the first {flatResults.length} of {totalMatches} matches
227
+ — refine your search to narrow the list.
228
+ </p>
229
+ )}
230
+ </>
231
+ )}
232
+ </div>
233
+ </div>
234
+ );
235
+ }
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // Result rows — a single pass over groups that keeps the flat match index
239
+ // aligned with `flatResults` (and thus keyboard focus / aria-activedescendant).
240
+ // ---------------------------------------------------------------------------
241
+
242
+ function renderRows({
243
+ groups,
244
+ showGroupHeaders,
245
+ selectedFile,
246
+ focusIndex,
247
+ optionId,
248
+ onOpenFile,
249
+ }: {
250
+ readonly groups: readonly WorkspaceFileSearchGroup[];
251
+ readonly showGroupHeaders: boolean;
252
+ readonly selectedFile: SelectedWorkspaceFile | null;
253
+ readonly focusIndex: number;
254
+ readonly optionId: (index: number) => string;
255
+ readonly onOpenFile: (entryId: string, path: string) => void;
256
+ }) {
257
+ const rows: React.ReactNode[] = [];
258
+ let matchIndex = 0;
259
+
260
+ for (const group of groups) {
261
+ if (showGroupHeaders) {
262
+ rows.push(<GroupHeader key={`h:${group.entry.id}`} name={group.entry.name} />);
263
+ }
264
+
265
+ for (const match of group.matches) {
266
+ if (matchIndex >= MAX_RESULTS) break;
267
+ const index = matchIndex++;
268
+ const isOpen =
269
+ selectedFile?.entryId === group.entry.id &&
270
+ selectedFile.path === match.path;
271
+ rows.push(
272
+ <ResultRow
273
+ key={`${group.entry.id}:${match.path}`}
274
+ id={optionId(index)}
275
+ match={match}
276
+ isFocused={index === focusIndex}
277
+ isOpen={isOpen}
278
+ onOpen={() => onOpenFile(group.entry.id, match.path)}
279
+ />,
280
+ );
281
+ }
282
+
283
+ if (group.truncated) {
284
+ rows.push(
285
+ <GroupNotice key={`t:${group.entry.id}`}>
286
+ Partial listing — too many files to search in full.
287
+ </GroupNotice>,
288
+ );
289
+ }
290
+ if (group.error) {
291
+ rows.push(
292
+ <GroupNotice key={`e:${group.entry.id}`} isError>
293
+ Couldn&rsquo;t list files: {group.error.message}
294
+ </GroupNotice>,
295
+ );
296
+ }
297
+
298
+ if (matchIndex >= MAX_RESULTS) break;
299
+ }
300
+
301
+ return rows;
302
+ }
303
+
304
+ function ResultRow({
305
+ id,
306
+ match,
307
+ isFocused,
308
+ isOpen,
309
+ onOpen,
310
+ }: {
311
+ readonly id: string;
312
+ readonly match: WorkspaceFileMatch;
313
+ readonly isFocused: boolean;
314
+ readonly isOpen: boolean;
315
+ readonly onOpen: () => void;
316
+ }) {
317
+ const lastSlash = match.path.lastIndexOf("/");
318
+ const basename = match.path.slice(lastSlash + 1);
319
+ const dir = lastSlash >= 0 ? match.path.slice(0, lastSlash) : "";
320
+
321
+ // The `role="option"` IS the interactive leaf of the listbox (keyboard
322
+ // activation flows through the combobox input's Enter handler +
323
+ // aria-activedescendant). It must not nest another interactive control — a
324
+ // button inside would violate WCAG 4.1.2 (axe `nested-interactive`). So the
325
+ // click handler and row styling live on the option itself.
326
+ return (
327
+ <li
328
+ id={id}
329
+ role="option"
330
+ aria-selected={isFocused}
331
+ aria-current={isOpen ? "true" : undefined}
332
+ onClick={onOpen}
333
+ className={cn(
334
+ "flex cursor-pointer items-baseline gap-1.5 px-3 py-1 text-xs transition-colors",
335
+ isFocused && "bg-muted",
336
+ isOpen && "bg-muted font-medium text-foreground",
337
+ !isFocused && !isOpen && "hover:bg-muted",
338
+ )}
339
+ >
340
+ <span className="truncate text-foreground">
341
+ <HighlightedPath
342
+ text={basename}
343
+ offset={lastSlash + 1}
344
+ matchStart={match.matchStart}
345
+ matchEnd={match.matchEnd}
346
+ />
347
+ </span>
348
+ {dir && (
349
+ <span className="truncate text-[0.65rem] text-muted-foreground">
350
+ <HighlightedPath
351
+ text={dir}
352
+ offset={0}
353
+ matchStart={match.matchStart}
354
+ matchEnd={match.matchEnd}
355
+ />
356
+ </span>
357
+ )}
358
+ </li>
359
+ );
360
+ }
361
+
362
+ /**
363
+ * Renders a slice of the path (`text`, starting at `offset` within the full
364
+ * path), emphasizing the intersection with the match range `[matchStart,
365
+ * matchEnd)`. Non-overlapping slices render plain.
366
+ */
367
+ function HighlightedPath({
368
+ text,
369
+ offset,
370
+ matchStart,
371
+ matchEnd,
372
+ }: {
373
+ readonly text: string;
374
+ readonly offset: number;
375
+ readonly matchStart: number;
376
+ readonly matchEnd: number;
377
+ }) {
378
+ const localStart = Math.max(0, matchStart - offset);
379
+ const localEnd = Math.min(text.length, matchEnd - offset);
380
+
381
+ if (localStart >= localEnd) return <>{text}</>;
382
+
383
+ return (
384
+ <>
385
+ {text.slice(0, localStart)}
386
+ <span className="font-semibold text-foreground">
387
+ {text.slice(localStart, localEnd)}
388
+ </span>
389
+ {text.slice(localEnd)}
390
+ </>
391
+ );
392
+ }
393
+
394
+ function GroupHeader({ name }: { readonly name: string }) {
395
+ return (
396
+ <li
397
+ role="presentation"
398
+ className="truncate px-3 pb-0.5 pt-2 text-[0.6rem] font-medium uppercase tracking-wide text-muted-foreground"
399
+ >
400
+ {name}
401
+ </li>
402
+ );
403
+ }
404
+
405
+ function GroupNotice({
406
+ children,
407
+ isError,
408
+ }: {
409
+ readonly children: React.ReactNode;
410
+ readonly isError?: boolean;
411
+ }) {
412
+ return (
413
+ <li
414
+ role="presentation"
415
+ className={cn(
416
+ "px-3 py-1 text-[0.65rem]",
417
+ isError ? "text-destructive" : "text-muted-foreground",
418
+ )}
419
+ >
420
+ {children}
421
+ </li>
422
+ );
423
+ }
424
+
425
+ function MessageState({ children }: { readonly children: React.ReactNode }) {
426
+ return (
427
+ <div className="flex h-full items-center justify-center p-8 text-center text-xs text-muted-foreground">
428
+ {children}
429
+ </div>
430
+ );
431
+ }
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // Inline SVG icons (SDK independence — no lucide dependency)
435
+ // ---------------------------------------------------------------------------
436
+
437
+ function SearchIcon() {
438
+ return (
439
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="shrink-0 text-muted-foreground" aria-hidden="true">
440
+ <circle cx="5.25" cy="5.25" r="3.5" />
441
+ <path d="M7.75 7.75L10.5 10.5" />
442
+ </svg>
443
+ );
444
+ }
445
+
446
+ function ClearIcon() {
447
+ return (
448
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
449
+ <path d="M3 3L9 9M9 3L3 9" />
450
+ </svg>
451
+ );
452
+ }