@stigmer/react 3.1.1 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. package/composer/SessionComposer.d.ts +24 -1
  2. package/composer/SessionComposer.d.ts.map +1 -1
  3. package/composer/SessionComposer.js +19 -8
  4. package/composer/SessionComposer.js.map +1 -1
  5. package/execution/ArtifactContentRenderer.d.ts +11 -1
  6. package/execution/ArtifactContentRenderer.d.ts.map +1 -1
  7. package/execution/ArtifactContentRenderer.js +39 -15
  8. package/execution/ArtifactContentRenderer.js.map +1 -1
  9. package/execution/FilePathContext.d.ts +8 -1
  10. package/execution/FilePathContext.d.ts.map +1 -1
  11. package/execution/FilePathContext.js.map +1 -1
  12. package/execution/FilePathLink.d.ts.map +1 -1
  13. package/execution/FilePathLink.js +21 -6
  14. package/execution/FilePathLink.js.map +1 -1
  15. package/execution/MessageEntry.d.ts +29 -0
  16. package/execution/MessageEntry.d.ts.map +1 -1
  17. package/execution/MessageEntry.js +29 -5
  18. package/execution/MessageEntry.js.map +1 -1
  19. package/execution/MessageThread.d.ts +76 -13
  20. package/execution/MessageThread.d.ts.map +1 -1
  21. package/execution/MessageThread.js +178 -24
  22. package/execution/MessageThread.js.map +1 -1
  23. package/execution/PlanArtifactCard.d.ts +27 -18
  24. package/execution/PlanArtifactCard.d.ts.map +1 -1
  25. package/execution/PlanArtifactCard.js +13 -23
  26. package/execution/PlanArtifactCard.js.map +1 -1
  27. package/execution/PlanDocumentMessage.d.ts +32 -0
  28. package/execution/PlanDocumentMessage.d.ts.map +1 -0
  29. package/execution/PlanDocumentMessage.js +37 -0
  30. package/execution/PlanDocumentMessage.js.map +1 -0
  31. package/execution/PlanStreamingCard.d.ts +45 -0
  32. package/execution/PlanStreamingCard.d.ts.map +1 -0
  33. package/execution/PlanStreamingCard.js +35 -0
  34. package/execution/PlanStreamingCard.js.map +1 -0
  35. package/execution/index.d.ts +5 -1
  36. package/execution/index.d.ts.map +1 -1
  37. package/execution/index.js +2 -0
  38. package/execution/index.js.map +1 -1
  39. package/execution/useCreateAgentExecution.d.ts +10 -0
  40. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  41. package/execution/useCreateAgentExecution.js +5 -1
  42. package/execution/useCreateAgentExecution.js.map +1 -1
  43. package/github/decodeGitHubContent.d.ts +29 -0
  44. package/github/decodeGitHubContent.d.ts.map +1 -0
  45. package/github/decodeGitHubContent.js +72 -0
  46. package/github/decodeGitHubContent.js.map +1 -0
  47. package/github/index.d.ts +1 -0
  48. package/github/index.d.ts.map +1 -1
  49. package/github/index.js +1 -0
  50. package/github/index.js.map +1 -1
  51. package/github/useGitHubFileReader.d.ts +26 -0
  52. package/github/useGitHubFileReader.d.ts.map +1 -0
  53. package/github/useGitHubFileReader.js +106 -0
  54. package/github/useGitHubFileReader.js.map +1 -0
  55. package/github/useGitHubTreeLister.d.ts.map +1 -1
  56. package/github/useGitHubTreeLister.js +6 -2
  57. package/github/useGitHubTreeLister.js.map +1 -1
  58. package/index.d.ts +9 -9
  59. package/index.d.ts.map +1 -1
  60. package/index.js +5 -5
  61. package/index.js.map +1 -1
  62. package/internal/ResizableSplit.d.ts +72 -22
  63. package/internal/ResizableSplit.d.ts.map +1 -1
  64. package/internal/ResizableSplit.js +96 -38
  65. package/internal/ResizableSplit.js.map +1 -1
  66. package/internal/VirtualizedThread.d.ts +5 -3
  67. package/internal/VirtualizedThread.d.ts.map +1 -1
  68. package/internal/VirtualizedThread.js +6 -4
  69. package/internal/VirtualizedThread.js.map +1 -1
  70. package/internal/file-icons/index.d.ts +17 -0
  71. package/internal/file-icons/index.d.ts.map +1 -0
  72. package/internal/file-icons/index.js +60 -0
  73. package/internal/file-icons/index.js.map +1 -0
  74. package/internal/file-tree/FileTreeNode.d.ts +18 -1
  75. package/internal/file-tree/FileTreeNode.d.ts.map +1 -1
  76. package/internal/file-tree/FileTreeNode.js +11 -2
  77. package/internal/file-tree/FileTreeNode.js.map +1 -1
  78. package/internal/markdown-components.d.ts +38 -1
  79. package/internal/markdown-components.d.ts.map +1 -1
  80. package/internal/markdown-components.js +86 -11
  81. package/internal/markdown-components.js.map +1 -1
  82. package/{workflow → internal}/motion-preference.d.ts +7 -4
  83. package/internal/motion-preference.d.ts.map +1 -0
  84. package/{workflow → internal}/motion-preference.js +7 -4
  85. package/internal/motion-preference.js.map +1 -0
  86. package/internal/store/index.d.ts +33 -0
  87. package/internal/store/index.d.ts.map +1 -1
  88. package/internal/store/index.js +59 -0
  89. package/internal/store/index.js.map +1 -1
  90. package/internal/store/workspace-editors-store.d.ts +145 -0
  91. package/internal/store/workspace-editors-store.d.ts.map +1 -0
  92. package/internal/store/workspace-editors-store.js +208 -0
  93. package/internal/store/workspace-editors-store.js.map +1 -0
  94. package/internal/store/workspace-file-selection-store.d.ts +38 -0
  95. package/internal/store/workspace-file-selection-store.d.ts.map +1 -0
  96. package/internal/store/workspace-file-selection-store.js +62 -0
  97. package/internal/store/workspace-file-selection-store.js.map +1 -0
  98. package/internal/useRevealLine.d.ts +49 -0
  99. package/internal/useRevealLine.d.ts.map +1 -0
  100. package/internal/useRevealLine.js +43 -0
  101. package/internal/useRevealLine.js.map +1 -0
  102. package/library/detect-plan-artifact.d.ts +21 -0
  103. package/library/detect-plan-artifact.d.ts.map +1 -1
  104. package/library/detect-plan-artifact.js +20 -0
  105. package/library/detect-plan-artifact.js.map +1 -1
  106. package/library/detect-streaming-plan.d.ts +45 -0
  107. package/library/detect-streaming-plan.d.ts.map +1 -0
  108. package/library/detect-streaming-plan.js +83 -0
  109. package/library/detect-streaming-plan.js.map +1 -0
  110. package/library/index.d.ts +4 -1
  111. package/library/index.d.ts.map +1 -1
  112. package/library/index.js +2 -1
  113. package/library/index.js.map +1 -1
  114. package/package.json +4 -4
  115. package/session/NewSessionViewer.d.ts +19 -4
  116. package/session/NewSessionViewer.d.ts.map +1 -1
  117. package/session/NewSessionViewer.js +52 -25
  118. package/session/NewSessionViewer.js.map +1 -1
  119. package/session/PlanEditor.d.ts +55 -0
  120. package/session/PlanEditor.d.ts.map +1 -0
  121. package/session/PlanEditor.js +88 -0
  122. package/session/PlanEditor.js.map +1 -0
  123. package/session/PlanStreamingDocument.d.ts +30 -0
  124. package/session/PlanStreamingDocument.d.ts.map +1 -0
  125. package/session/PlanStreamingDocument.js +37 -0
  126. package/session/PlanStreamingDocument.js.map +1 -0
  127. package/session/SessionPanelChip.d.ts +26 -0
  128. package/session/SessionPanelChip.d.ts.map +1 -0
  129. package/session/SessionPanelChip.js +40 -0
  130. package/session/SessionPanelChip.js.map +1 -0
  131. package/session/SessionViewer.d.ts +34 -7
  132. package/session/SessionViewer.d.ts.map +1 -1
  133. package/session/SessionViewer.js +315 -33
  134. package/session/SessionViewer.js.map +1 -1
  135. package/session/{inspector → facets}/ArtifactsTab.d.ts +10 -2
  136. package/session/facets/ArtifactsTab.d.ts.map +1 -0
  137. package/session/{inspector → facets}/ArtifactsTab.js +8 -3
  138. package/session/facets/ArtifactsTab.js.map +1 -0
  139. package/session/{inspector → facets}/ChangesTab.d.ts +3 -3
  140. package/session/facets/ChangesTab.d.ts.map +1 -0
  141. package/session/{inspector → facets}/ChangesTab.js +3 -3
  142. package/session/facets/ChangesTab.js.map +1 -0
  143. package/session/facets/InspectTab.d.ts.map +1 -0
  144. package/session/facets/InspectTab.js.map +1 -0
  145. package/session/{inspector → facets}/SetupTab.d.ts +11 -2
  146. package/session/facets/SetupTab.d.ts.map +1 -0
  147. package/session/{inspector → facets}/SetupTab.js +4 -3
  148. package/session/facets/SetupTab.js.map +1 -0
  149. package/session/{inspector → facets}/UsageTab.d.ts +1 -1
  150. package/session/facets/UsageTab.d.ts.map +1 -0
  151. package/session/{inspector → facets}/UsageTab.js +1 -1
  152. package/session/facets/UsageTab.js.map +1 -0
  153. package/session/{inspector → facets}/index.d.ts +0 -6
  154. package/session/facets/index.d.ts.map +1 -0
  155. package/session/{inspector → facets}/index.js +0 -3
  156. package/session/facets/index.js.map +1 -0
  157. package/session/index.d.ts +11 -2
  158. package/session/index.d.ts.map +1 -1
  159. package/session/index.js +15 -2
  160. package/session/index.js.map +1 -1
  161. package/session/plan-document.d.ts +9 -0
  162. package/session/plan-document.d.ts.map +1 -0
  163. package/session/plan-document.js +17 -0
  164. package/session/plan-document.js.map +1 -0
  165. package/session/useOpenFileChange.d.ts +20 -0
  166. package/session/useOpenFileChange.d.ts.map +1 -0
  167. package/session/useOpenFileChange.js +31 -0
  168. package/session/useOpenFileChange.js.map +1 -0
  169. package/session/usePlanDraft.d.ts +52 -0
  170. package/session/usePlanDraft.d.ts.map +1 -0
  171. package/session/usePlanDraft.js +63 -0
  172. package/session/usePlanDraft.js.map +1 -0
  173. package/session/useSessionConversation.d.ts +6 -0
  174. package/session/useSessionConversation.d.ts.map +1 -1
  175. package/session/useSessionConversation.js +1 -0
  176. package/session/useSessionConversation.js.map +1 -1
  177. package/session/useSessionPageFlow.d.ts.map +1 -1
  178. package/session/useSessionPageFlow.js +1 -0
  179. package/session/useSessionPageFlow.js.map +1 -1
  180. package/session/useSessionPanel.d.ts +120 -0
  181. package/session/useSessionPanel.d.ts.map +1 -0
  182. package/session/useSessionPanel.js +130 -0
  183. package/session/useSessionPanel.js.map +1 -0
  184. package/session/useSessionRailViews.d.ts +45 -0
  185. package/session/useSessionRailViews.d.ts.map +1 -0
  186. package/session/useSessionRailViews.js +105 -0
  187. package/session/useSessionRailViews.js.map +1 -0
  188. package/src/composer/SessionComposer.tsx +52 -10
  189. package/src/composer/__tests__/SessionComposer-contract.test.tsx +58 -4
  190. package/src/execution/ArtifactContentRenderer.tsx +101 -24
  191. package/src/execution/FilePathContext.tsx +8 -1
  192. package/src/execution/FilePathLink.tsx +22 -6
  193. package/src/execution/MessageEntry.tsx +111 -1
  194. package/src/execution/MessageThread.tsx +291 -36
  195. package/src/execution/PlanArtifactCard.tsx +63 -31
  196. package/src/execution/PlanDocumentMessage.tsx +96 -0
  197. package/src/execution/PlanStreamingCard.tsx +137 -0
  198. package/src/execution/__tests__/ArtifactContentRenderer.test.tsx +80 -1
  199. package/src/execution/__tests__/FilePathLink.test.tsx +87 -0
  200. package/src/execution/__tests__/MessageThread.test.tsx +101 -0
  201. package/src/execution/__tests__/PlanArtifactCard.test.tsx +66 -12
  202. package/src/execution/__tests__/PlanDocumentMessage.test.tsx +55 -0
  203. package/src/execution/__tests__/PlanStreamingCard.test.tsx +49 -0
  204. package/src/execution/__tests__/buildThreadItems-plan-completion.test.ts +504 -22
  205. package/src/execution/__tests__/message-entry.test.tsx +48 -0
  206. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +85 -0
  207. package/src/execution/index.ts +7 -1
  208. package/src/execution/useCreateAgentExecution.ts +16 -1
  209. package/src/github/__tests__/decodeGitHubContent.test.ts +102 -0
  210. package/src/github/__tests__/useGitHubFileReader.test.ts +183 -0
  211. package/src/github/decodeGitHubContent.ts +83 -0
  212. package/src/github/index.ts +1 -0
  213. package/src/github/useGitHubFileReader.ts +152 -0
  214. package/src/github/useGitHubTreeLister.ts +6 -2
  215. package/src/index.ts +63 -9
  216. package/src/internal/ResizableSplit.tsx +184 -51
  217. package/src/internal/VirtualizedThread.tsx +12 -4
  218. package/src/internal/__tests__/ResizableSplit.test.tsx +125 -3
  219. package/src/internal/__tests__/markdown-components.test.tsx +68 -0
  220. package/src/internal/__tests__/useRevealLine.test.tsx +97 -0
  221. package/src/internal/file-icons/__tests__/file-icons.test.tsx +35 -0
  222. package/src/internal/file-icons/index.tsx +137 -0
  223. package/src/internal/file-tree/FileTreeNode.tsx +43 -3
  224. package/src/internal/file-tree/__tests__/FileTreeNode-explorer.test.tsx +86 -0
  225. package/src/internal/markdown-components.tsx +116 -11
  226. package/src/{workflow → internal}/motion-preference.ts +7 -4
  227. package/src/internal/store/__tests__/workspace-editors-store.test.ts +216 -0
  228. package/src/internal/store/__tests__/workspace-file-selection-store.test.ts +118 -0
  229. package/src/internal/store/index.ts +94 -0
  230. package/src/internal/store/workspace-editors-store.ts +315 -0
  231. package/src/internal/store/workspace-file-selection-store.ts +88 -0
  232. package/src/internal/useRevealLine.ts +81 -0
  233. package/src/library/__tests__/detect-plan-artifact.test.ts +48 -0
  234. package/src/library/__tests__/detect-streaming-plan.test.ts +234 -0
  235. package/src/library/detect-plan-artifact.ts +35 -0
  236. package/src/library/detect-streaming-plan.ts +108 -0
  237. package/src/library/index.ts +5 -0
  238. package/src/session/NewSessionViewer.tsx +123 -55
  239. package/src/session/PlanEditor.tsx +361 -0
  240. package/src/session/PlanStreamingDocument.tsx +97 -0
  241. package/src/session/SessionPanelChip.tsx +103 -0
  242. package/src/session/SessionViewer.tsx +617 -90
  243. package/src/session/__tests__/NewSessionViewer.test.tsx +148 -0
  244. package/src/session/__tests__/PlanEditor.test.tsx +217 -0
  245. package/src/session/__tests__/PlanStreamingDocument.test.tsx +50 -0
  246. package/src/session/__tests__/SessionPanelChip.test.tsx +42 -0
  247. package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +9 -4
  248. package/src/session/__tests__/SessionViewer-transcriptFileOpen.test.tsx +205 -0
  249. package/src/session/__tests__/audienceWiring.test.tsx +55 -19
  250. package/src/session/__tests__/useOpenFileChange.test.tsx +82 -0
  251. package/src/session/__tests__/usePlanDraft.test.ts +102 -0
  252. package/src/session/__tests__/useSessionPanel.test.ts +298 -0
  253. package/src/session/__tests__/useSessionRailViews.test.tsx +124 -0
  254. package/src/session/{inspector → facets}/ArtifactsTab.tsx +27 -5
  255. package/src/session/{inspector → facets}/ChangesTab.tsx +3 -3
  256. package/src/session/{inspector → facets}/SetupTab.tsx +13 -1
  257. package/src/session/{inspector → facets}/UsageTab.tsx +1 -1
  258. package/src/session/{inspector → facets}/__tests__/ArtifactsTab.test.tsx +35 -1
  259. package/src/session/facets/__tests__/SetupTab.test.tsx +44 -0
  260. package/src/session/{inspector → facets}/index.ts +0 -16
  261. package/src/session/index.ts +30 -13
  262. package/src/session/plan-document.ts +19 -0
  263. package/src/session/useOpenFileChange.ts +53 -0
  264. package/src/session/usePlanDraft.ts +109 -0
  265. package/src/session/useSessionConversation.ts +7 -0
  266. package/src/session/useSessionPageFlow.ts +1 -0
  267. package/src/session/useSessionPanel.ts +288 -0
  268. package/src/session/useSessionRailViews.tsx +200 -0
  269. package/src/workflow/WorkflowExecutionGraph.tsx +1 -1
  270. package/src/workflow/index.ts +1 -1
  271. package/src/workflow/useFollowExecution.ts +1 -1
  272. package/src/workspace/EditorTabs.tsx +145 -0
  273. package/src/workspace/ExplorerTree.tsx +247 -0
  274. package/src/workspace/FileViewer.tsx +516 -0
  275. package/src/workspace/WorkspaceContentSearch.tsx +478 -0
  276. package/src/workspace/WorkspaceContentSearcher.ts +70 -0
  277. package/src/workspace/WorkspaceFileLister.ts +11 -0
  278. package/src/workspace/WorkspaceFileReader.ts +57 -0
  279. package/src/workspace/WorkspaceFileSearch.tsx +429 -0
  280. package/src/workspace/WorkspaceSurface.tsx +781 -0
  281. package/src/workspace/__tests__/EditorTabs.test.tsx +100 -0
  282. package/src/workspace/__tests__/ExplorerTree.test.tsx +152 -0
  283. package/src/workspace/__tests__/FileViewer.test.tsx +421 -0
  284. package/src/workspace/__tests__/WorkspaceContentSearch.test.tsx +196 -0
  285. package/src/workspace/__tests__/WorkspaceFileSearch.test.tsx +206 -0
  286. package/src/workspace/__tests__/WorkspaceSurface.test.tsx +348 -0
  287. package/src/workspace/__tests__/findChangeForSelection.test.ts +132 -0
  288. package/src/workspace/__tests__/matchContentPreview.test.ts +55 -0
  289. package/src/workspace/__tests__/matchWorkspaceFiles.test.ts +104 -0
  290. package/src/workspace/__tests__/resolveWorkspaceFileSelection.test.ts +135 -0
  291. package/src/workspace/__tests__/useWorkspaceContentSearch.test.tsx +196 -0
  292. package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +190 -0
  293. package/src/workspace/__tests__/useWorkspaceFileSearch.test.tsx +162 -0
  294. package/src/workspace/findChangeForSelection.ts +61 -0
  295. package/src/workspace/index.ts +56 -2
  296. package/src/workspace/matchContentPreview.ts +51 -0
  297. package/src/workspace/matchWorkspaceFiles.ts +86 -0
  298. package/src/workspace/resolveWorkspaceFileSelection.ts +119 -0
  299. package/src/workspace/useWorkspaceContentSearch.ts +228 -0
  300. package/src/workspace/useWorkspaceFileContent.ts +129 -0
  301. package/src/workspace/useWorkspaceFileSearch.ts +184 -0
  302. package/src/workspace/useWorkspaceFiles.ts +27 -27
  303. package/src/workspace/workspaceListingCache.ts +93 -0
  304. package/styles.css +1 -1
  305. package/workflow/WorkflowExecutionGraph.js +1 -1
  306. package/workflow/WorkflowExecutionGraph.js.map +1 -1
  307. package/workflow/index.d.ts +1 -1
  308. package/workflow/index.d.ts.map +1 -1
  309. package/workflow/index.js +1 -1
  310. package/workflow/index.js.map +1 -1
  311. package/workflow/useFollowExecution.js +1 -1
  312. package/workflow/useFollowExecution.js.map +1 -1
  313. package/workspace/EditorTabs.d.ts +29 -0
  314. package/workspace/EditorTabs.d.ts.map +1 -0
  315. package/workspace/EditorTabs.js +66 -0
  316. package/workspace/EditorTabs.js.map +1 -0
  317. package/workspace/ExplorerTree.d.ts +32 -0
  318. package/workspace/ExplorerTree.d.ts.map +1 -0
  319. package/workspace/ExplorerTree.js +45 -0
  320. package/workspace/ExplorerTree.js.map +1 -0
  321. package/workspace/FileViewer.d.ts +93 -0
  322. package/workspace/FileViewer.d.ts.map +1 -0
  323. package/workspace/FileViewer.js +182 -0
  324. package/workspace/FileViewer.js.map +1 -0
  325. package/workspace/WorkspaceContentSearch.d.ts +41 -0
  326. package/workspace/WorkspaceContentSearch.d.ts.map +1 -0
  327. package/workspace/WorkspaceContentSearch.js +195 -0
  328. package/workspace/WorkspaceContentSearch.js.map +1 -0
  329. package/workspace/WorkspaceContentSearcher.d.ts +59 -0
  330. package/workspace/WorkspaceContentSearcher.d.ts.map +1 -0
  331. package/workspace/WorkspaceContentSearcher.js +7 -0
  332. package/workspace/WorkspaceContentSearcher.js.map +1 -0
  333. package/workspace/WorkspaceFileLister.d.ts +11 -0
  334. package/workspace/WorkspaceFileLister.d.ts.map +1 -1
  335. package/workspace/WorkspaceFileReader.d.ts +48 -0
  336. package/workspace/WorkspaceFileReader.d.ts.map +1 -0
  337. package/workspace/WorkspaceFileReader.js +11 -0
  338. package/workspace/WorkspaceFileReader.js.map +1 -0
  339. package/workspace/WorkspaceFileSearch.d.ts +32 -0
  340. package/workspace/WorkspaceFileSearch.d.ts.map +1 -0
  341. package/workspace/WorkspaceFileSearch.js +155 -0
  342. package/workspace/WorkspaceFileSearch.js.map +1 -0
  343. package/workspace/WorkspaceSurface.d.ts +151 -0
  344. package/workspace/WorkspaceSurface.d.ts.map +1 -0
  345. package/workspace/WorkspaceSurface.js +184 -0
  346. package/workspace/WorkspaceSurface.js.map +1 -0
  347. package/workspace/findChangeForSelection.d.ts +19 -0
  348. package/workspace/findChangeForSelection.d.ts.map +1 -0
  349. package/workspace/findChangeForSelection.js +46 -0
  350. package/workspace/findChangeForSelection.js.map +1 -0
  351. package/workspace/index.d.ts +24 -2
  352. package/workspace/index.d.ts.map +1 -1
  353. package/workspace/index.js +13 -1
  354. package/workspace/index.js.map +1 -1
  355. package/workspace/matchContentPreview.d.ts +21 -0
  356. package/workspace/matchContentPreview.d.ts.map +1 -0
  357. package/workspace/matchContentPreview.js +36 -0
  358. package/workspace/matchContentPreview.js.map +1 -0
  359. package/workspace/matchWorkspaceFiles.d.ts +30 -0
  360. package/workspace/matchWorkspaceFiles.d.ts.map +1 -0
  361. package/workspace/matchWorkspaceFiles.js +58 -0
  362. package/workspace/matchWorkspaceFiles.js.map +1 -0
  363. package/workspace/resolveWorkspaceFileSelection.d.ts +19 -0
  364. package/workspace/resolveWorkspaceFileSelection.d.ts.map +1 -0
  365. package/workspace/resolveWorkspaceFileSelection.js +115 -0
  366. package/workspace/resolveWorkspaceFileSelection.js.map +1 -0
  367. package/workspace/useWorkspaceContentSearch.d.ts +69 -0
  368. package/workspace/useWorkspaceContentSearch.d.ts.map +1 -0
  369. package/workspace/useWorkspaceContentSearch.js +154 -0
  370. package/workspace/useWorkspaceContentSearch.js.map +1 -0
  371. package/workspace/useWorkspaceFileContent.d.ts +73 -0
  372. package/workspace/useWorkspaceFileContent.d.ts.map +1 -0
  373. package/workspace/useWorkspaceFileContent.js +57 -0
  374. package/workspace/useWorkspaceFileContent.js.map +1 -0
  375. package/workspace/useWorkspaceFileSearch.d.ts +61 -0
  376. package/workspace/useWorkspaceFileSearch.d.ts.map +1 -0
  377. package/workspace/useWorkspaceFileSearch.js +119 -0
  378. package/workspace/useWorkspaceFileSearch.js.map +1 -0
  379. package/workspace/useWorkspaceFiles.d.ts +13 -5
  380. package/workspace/useWorkspaceFiles.d.ts.map +1 -1
  381. package/workspace/useWorkspaceFiles.js +18 -19
  382. package/workspace/useWorkspaceFiles.js.map +1 -1
  383. package/workspace/workspaceListingCache.d.ts +47 -0
  384. package/workspace/workspaceListingCache.d.ts.map +1 -0
  385. package/workspace/workspaceListingCache.js +55 -0
  386. package/workspace/workspaceListingCache.js.map +1 -0
  387. package/session/inspector/ArtifactsTab.d.ts.map +0 -1
  388. package/session/inspector/ArtifactsTab.js.map +0 -1
  389. package/session/inspector/ChangesTab.d.ts.map +0 -1
  390. package/session/inspector/ChangesTab.js.map +0 -1
  391. package/session/inspector/InspectTab.d.ts.map +0 -1
  392. package/session/inspector/InspectTab.js.map +0 -1
  393. package/session/inspector/SessionInspector.d.ts +0 -64
  394. package/session/inspector/SessionInspector.d.ts.map +0 -1
  395. package/session/inspector/SessionInspector.js +0 -53
  396. package/session/inspector/SessionInspector.js.map +0 -1
  397. package/session/inspector/SetupTab.d.ts.map +0 -1
  398. package/session/inspector/SetupTab.js.map +0 -1
  399. package/session/inspector/UsageTab.d.ts.map +0 -1
  400. package/session/inspector/UsageTab.js.map +0 -1
  401. package/session/inspector/WorkspaceTab.d.ts +0 -35
  402. package/session/inspector/WorkspaceTab.d.ts.map +0 -1
  403. package/session/inspector/WorkspaceTab.js +0 -75
  404. package/session/inspector/WorkspaceTab.js.map +0 -1
  405. package/session/inspector/index.d.ts.map +0 -1
  406. package/session/inspector/index.js.map +0 -1
  407. package/session/inspector/useSessionInspector.d.ts +0 -49
  408. package/session/inspector/useSessionInspector.d.ts.map +0 -1
  409. package/session/inspector/useSessionInspector.js +0 -122
  410. package/session/inspector/useSessionInspector.js.map +0 -1
  411. package/src/session/inspector/SessionInspector.tsx +0 -162
  412. package/src/session/inspector/WorkspaceTab.tsx +0 -245
  413. package/src/session/inspector/__tests__/useSessionInspector.test.ts +0 -251
  414. package/src/session/inspector/useSessionInspector.ts +0 -178
  415. package/src/workspace/WorkspaceEntryFiles.tsx +0 -196
  416. package/workflow/motion-preference.d.ts.map +0 -1
  417. package/workflow/motion-preference.js.map +0 -1
  418. package/workspace/WorkspaceEntryFiles.d.ts +0 -22
  419. package/workspace/WorkspaceEntryFiles.d.ts.map +0 -1
  420. package/workspace/WorkspaceEntryFiles.js +0 -58
  421. package/workspace/WorkspaceEntryFiles.js.map +0 -1
  422. /package/session/{inspector → facets}/InspectTab.d.ts +0 -0
  423. /package/session/{inspector → facets}/InspectTab.js +0 -0
  424. /package/src/{workflow → internal}/__tests__/motion-preference.test.ts +0 -0
  425. /package/src/session/{inspector → facets}/InspectTab.tsx +0 -0
  426. /package/src/session/{inspector → facets}/__tests__/ChangesTab.test.tsx +0 -0
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import type { Stigmer } from "@stigmer/sdk";
5
+ import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
6
+ import { StigmerContext } from "../../context";
7
+ import { useCreateAgentExecution } from "../useCreateAgentExecution";
8
+
9
+ const mockCreate = vi.fn();
10
+
11
+ function makeMockClient(): Stigmer {
12
+ return {
13
+ agentExecution: { create: mockCreate },
14
+ } as unknown as Stigmer;
15
+ }
16
+
17
+ function createWrapper(client: Stigmer) {
18
+ return function Wrapper({ children }: { children: ReactNode }) {
19
+ return <StigmerContext.Provider value={client}>{children}</StigmerContext.Provider>;
20
+ };
21
+ }
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ mockCreate.mockResolvedValue({ metadata: { id: "aex-1" } });
26
+ });
27
+
28
+ describe("useCreateAgentExecution — executionConfig mapping", () => {
29
+ it("maps buildFromPlan into executionConfig (the Build-from-plan turn)", async () => {
30
+ const { result } = renderHook(() => useCreateAgentExecution(), {
31
+ wrapper: createWrapper(makeMockClient()),
32
+ });
33
+
34
+ await act(async () => {
35
+ await result.current.create({
36
+ org: "acme",
37
+ sessionId: "ses-1",
38
+ message: "Build from plan",
39
+ interactionMode: "agent",
40
+ buildFromPlan: true,
41
+ });
42
+ });
43
+
44
+ expect(mockCreate).toHaveBeenCalledTimes(1);
45
+ expect(mockCreate.mock.calls[0][0].executionConfig).toMatchObject({
46
+ interactionMode: InteractionMode.AGENT,
47
+ buildFromPlan: true,
48
+ });
49
+ });
50
+
51
+ it("builds an executionConfig when buildFromPlan is the only config input", async () => {
52
+ const { result } = renderHook(() => useCreateAgentExecution(), {
53
+ wrapper: createWrapper(makeMockClient()),
54
+ });
55
+
56
+ await act(async () => {
57
+ await result.current.create({
58
+ org: "acme",
59
+ sessionId: "ses-1",
60
+ message: "Build from plan",
61
+ buildFromPlan: true,
62
+ });
63
+ });
64
+
65
+ expect(mockCreate.mock.calls[0][0].executionConfig).toMatchObject({
66
+ buildFromPlan: true,
67
+ });
68
+ });
69
+
70
+ it("omits executionConfig entirely for an ordinary message", async () => {
71
+ const { result } = renderHook(() => useCreateAgentExecution(), {
72
+ wrapper: createWrapper(makeMockClient()),
73
+ });
74
+
75
+ await act(async () => {
76
+ await result.current.create({
77
+ org: "acme",
78
+ sessionId: "ses-1",
79
+ message: "Hello",
80
+ });
81
+ });
82
+
83
+ expect(mockCreate.mock.calls[0][0].executionConfig).toBeUndefined();
84
+ });
85
+ });
@@ -41,6 +41,12 @@ export type { PlanCompletionCardProps } from "./PlanCompletionCard.js";
41
41
  export { PlanArtifactCard } from "./PlanArtifactCard.js";
42
42
  export type { PlanArtifactCardProps } from "./PlanArtifactCard.js";
43
43
 
44
+ export { PlanStreamingCard } from "./PlanStreamingCard.js";
45
+ export type { PlanStreamingCardProps } from "./PlanStreamingCard.js";
46
+
47
+ export { PlanDocumentMessage } from "./PlanDocumentMessage.js";
48
+ export type { PlanDocumentMessageProps } from "./PlanDocumentMessage.js";
49
+
44
50
  export { useExecutionArtifacts } from "./useExecutionArtifacts.js";
45
51
  export type { UseExecutionArtifactsReturn } from "./useExecutionArtifacts.js";
46
52
 
@@ -160,7 +166,7 @@ export { MessageEntry } from "./MessageEntry.js";
160
166
  export type { MessageEntryProps } from "./MessageEntry.js";
161
167
 
162
168
  export { MessageThread } from "./MessageThread.js";
163
- export type { MessageThreadProps } from "./MessageThread.js";
169
+ export type { MessageThreadProps, ThreadContentColumn } from "./MessageThread.js";
164
170
 
165
171
  export { ThreadSkeleton } from "./ThreadSkeleton.js";
166
172
  export type { ThreadSkeletonProps } from "./ThreadSkeleton.js";
@@ -54,6 +54,16 @@ export interface CreateAgentExecutionInput {
54
54
  * Maps to `ExecutionConfig.interaction_mode` in the proto.
55
55
  */
56
56
  readonly interactionMode?: "agent" | "plan";
57
+ /**
58
+ * Marks this execution as a "Build from plan" turn: the user approved a
59
+ * plan from a prior Plan-mode execution and asked the agent to implement
60
+ * it. The runner injects the implement-plan directive (pointing at the
61
+ * attached approved plan when present); the thread renders the turn as a
62
+ * compact chip instead of the raw message text.
63
+ *
64
+ * Maps to `ExecutionConfig.build_from_plan` in the proto.
65
+ */
66
+ readonly buildFromPlan?: boolean;
57
67
  /**
58
68
  * Auto-approve every tool call for this execution.
59
69
  *
@@ -158,7 +168,11 @@ export function useCreateAgentExecution(): UseCreateAgentExecutionReturn {
158
168
  setError(null);
159
169
 
160
170
  try {
161
- const hasConfig = input.modelName || input.interactionMode || input.structuredOutputSchema;
171
+ const hasConfig =
172
+ input.modelName ||
173
+ input.interactionMode ||
174
+ input.structuredOutputSchema ||
175
+ input.buildFromPlan;
162
176
  const executionConfig = hasConfig
163
177
  ? {
164
178
  ...(input.modelName ? { modelName: input.modelName } : {}),
@@ -168,6 +182,7 @@ export function useCreateAgentExecution(): UseCreateAgentExecutionReturn {
168
182
  ...(input.structuredOutputSchema
169
183
  ? { structuredOutputSchema: input.structuredOutputSchema }
170
184
  : {}),
185
+ ...(input.buildFromPlan ? { buildFromPlan: true } : {}),
171
186
  }
172
187
  : undefined;
173
188
 
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ base64ToBytes,
4
+ detectBinary,
5
+ bytesToText,
6
+ normalizeGitHubContent,
7
+ } from "../decodeGitHubContent";
8
+ import { MAX_WORKSPACE_FILE_READ_BYTES } from "../../workspace/WorkspaceFileReader";
9
+
10
+ function utf8(s: string): Uint8Array {
11
+ return new TextEncoder().encode(s);
12
+ }
13
+
14
+ describe("base64ToBytes", () => {
15
+ it("decodes plain base64 to bytes", () => {
16
+ expect(bytesToText(base64ToBytes("SGVsbG8gV29ybGQh"))).toBe("Hello World!");
17
+ });
18
+
19
+ it("strips the newlines GitHub inserts every 60 chars", () => {
20
+ // GitHub's Contents API line-wraps its base64; atob would otherwise reject it.
21
+ const wrapped = "SGVsbG8g\nV29ybGQh\n";
22
+ expect(bytesToText(base64ToBytes(wrapped))).toBe("Hello World!");
23
+ });
24
+ });
25
+
26
+ describe("detectBinary", () => {
27
+ it("flags a buffer containing a NUL byte", () => {
28
+ expect(detectBinary(new Uint8Array([0x48, 0x00, 0x49]))).toBe(true);
29
+ });
30
+
31
+ it("treats NUL-free text as non-binary", () => {
32
+ expect(detectBinary(utf8("just some text"))).toBe(false);
33
+ });
34
+
35
+ it("only sniffs the head — a NUL past the sniff window is not detected", () => {
36
+ const bytes = new Uint8Array(9000).fill(0x61); // non-zero baseline
37
+ bytes[8500] = 0; // beyond the 8000-byte sniff window
38
+ expect(detectBinary(bytes)).toBe(false);
39
+ });
40
+ });
41
+
42
+ describe("bytesToText", () => {
43
+ it("decodes valid UTF-8", () => {
44
+ expect(bytesToText(utf8("héllo — 世界"))).toBe("héllo — 世界");
45
+ });
46
+
47
+ it("returns null for invalid UTF-8", () => {
48
+ // 0xFF is never a valid UTF-8 lead byte; the fatal decoder throws → null.
49
+ expect(bytesToText(new Uint8Array([0xff]))).toBeNull();
50
+ });
51
+ });
52
+
53
+ describe("normalizeGitHubContent", () => {
54
+ it("maps clean text to a utf-8 content record with the full size", () => {
55
+ const bytes = utf8("hello");
56
+ expect(normalizeGitHubContent(bytes, bytes.length)).toEqual({
57
+ text: "hello",
58
+ isBinary: false,
59
+ size: bytes.length,
60
+ encoding: "utf-8",
61
+ });
62
+ });
63
+
64
+ it("reports binary content with text null", () => {
65
+ const result = normalizeGitHubContent(new Uint8Array([0x00, 0x01, 0x02]), 3);
66
+ expect(result).toEqual({
67
+ text: null,
68
+ isBinary: true,
69
+ size: 3,
70
+ encoding: "base64",
71
+ });
72
+ });
73
+
74
+ it("marks undecodable (non-binary) bytes as unknown encoding", () => {
75
+ // 0xC0 is an invalid UTF-8 lead byte but not a NUL, so it is not "binary".
76
+ const result = normalizeGitHubContent(new Uint8Array([0xc0, 0x41]), 2);
77
+ expect(result).toEqual({
78
+ text: null,
79
+ isBinary: false,
80
+ size: 2,
81
+ encoding: "unknown",
82
+ });
83
+ });
84
+
85
+ it("truncates at the 1 MB cap and flags it, while size reports the full length", () => {
86
+ const total = MAX_WORKSPACE_FILE_READ_BYTES + 500;
87
+ const bytes = new Uint8Array(total).fill(0x61); // 'a'
88
+ const result = normalizeGitHubContent(bytes, total);
89
+
90
+ expect(result.truncated).toBe(true);
91
+ expect(result.size).toBe(total);
92
+ expect(result.text).toHaveLength(MAX_WORKSPACE_FILE_READ_BYTES);
93
+ expect(result.encoding).toBe("utf-8");
94
+ });
95
+
96
+ it("does not set truncated when exactly at the cap", () => {
97
+ const bytes = new Uint8Array(MAX_WORKSPACE_FILE_READ_BYTES).fill(0x61);
98
+ const result = normalizeGitHubContent(bytes, bytes.length);
99
+ expect(result.truncated).toBeUndefined();
100
+ expect(result.text).toHaveLength(MAX_WORKSPACE_FILE_READ_BYTES);
101
+ });
102
+ });
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useGitHubFileReader } from "../useGitHubFileReader";
4
+ import type { WorkspaceEntry } from "../../workspace/useWorkspaceEntries";
5
+
6
+ function gitEntry(overrides?: Partial<WorkspaceEntry>): WorkspaceEntry {
7
+ return {
8
+ id: "ws-1",
9
+ name: "acme/api",
10
+ type: "git",
11
+ gitUrl: "https://github.com/acme/api",
12
+ gitBranch: "main",
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ function localEntry(): WorkspaceEntry {
18
+ return {
19
+ id: "ws-2",
20
+ name: "/Users/dev/project",
21
+ type: "local",
22
+ localPath: "/Users/dev/project",
23
+ };
24
+ }
25
+
26
+ function b64(s: string): string {
27
+ return Buffer.from(s, "utf-8").toString("base64");
28
+ }
29
+
30
+ function jsonResponse(body: unknown, status = 200): Response {
31
+ return new Response(JSON.stringify(body), { status });
32
+ }
33
+
34
+ describe("useGitHubFileReader", () => {
35
+ const originalFetch = globalThis.fetch;
36
+
37
+ beforeEach(() => {
38
+ globalThis.fetch = vi.fn();
39
+ });
40
+
41
+ afterEach(() => {
42
+ globalThis.fetch = originalFetch;
43
+ });
44
+
45
+ it("returns undefined when token is null", () => {
46
+ const { result } = renderHook(() => useGitHubFileReader(null));
47
+ expect(result.current).toBeUndefined();
48
+ });
49
+
50
+ it("returns a reader function when a token is provided", () => {
51
+ const { result } = renderHook(() => useGitHubFileReader("ghp_abc"));
52
+ expect(typeof result.current).toBe("function");
53
+ });
54
+
55
+ it("returns null for non-git entries without fetching", async () => {
56
+ const { result } = renderHook(() => useGitHubFileReader("ghp_abc"));
57
+ const content = await result.current!(localEntry(), "src/index.ts");
58
+ expect(content).toBeNull();
59
+ expect(globalThis.fetch).not.toHaveBeenCalled();
60
+ });
61
+
62
+ it("returns null for non-GitHub git URLs", async () => {
63
+ const { result } = renderHook(() => useGitHubFileReader("ghp_abc"));
64
+ const content = await result.current!(
65
+ gitEntry({ gitUrl: "https://gitlab.com/acme/api" }),
66
+ "src/index.ts",
67
+ );
68
+ expect(content).toBeNull();
69
+ expect(globalThis.fetch).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it("decodes a base64 text file from the Contents API", async () => {
73
+ vi.mocked(globalThis.fetch).mockResolvedValue(
74
+ jsonResponse({ encoding: "base64", size: 12, content: b64("Hello World!"), sha: "s1" }),
75
+ );
76
+
77
+ const { result } = renderHook(() => useGitHubFileReader("ghp_abc"));
78
+ const content = await result.current!(gitEntry(), "src/index.ts");
79
+
80
+ expect(globalThis.fetch).toHaveBeenCalledWith(
81
+ "https://api.github.com/repos/acme/api/contents/src/index.ts?ref=main",
82
+ { headers: { Authorization: "Bearer ghp_abc" } },
83
+ );
84
+ expect(content).toEqual({
85
+ text: "Hello World!",
86
+ isBinary: false,
87
+ size: 12,
88
+ encoding: "utf-8",
89
+ });
90
+ });
91
+
92
+ it("encodes the path per segment and the branch", async () => {
93
+ vi.mocked(globalThis.fetch).mockResolvedValue(
94
+ jsonResponse({ encoding: "base64", size: 1, content: b64("x"), sha: "s1" }),
95
+ );
96
+
97
+ const { result } = renderHook(() => useGitHubFileReader("ghp_abc"));
98
+ await result.current!(
99
+ gitEntry({ gitBranch: "feat/my branch" }),
100
+ "src/a b/c#d.ts",
101
+ );
102
+
103
+ expect(globalThis.fetch).toHaveBeenCalledWith(
104
+ "https://api.github.com/repos/acme/api/contents/src/a%20b/c%23d.ts?ref=feat%2Fmy%20branch",
105
+ expect.anything(),
106
+ );
107
+ });
108
+
109
+ it("defaults to the main branch when gitBranch is undefined", async () => {
110
+ vi.mocked(globalThis.fetch).mockResolvedValue(
111
+ jsonResponse({ encoding: "base64", size: 1, content: b64("x"), sha: "s1" }),
112
+ );
113
+
114
+ const { result } = renderHook(() => useGitHubFileReader("ghp_abc"));
115
+ await result.current!(gitEntry({ gitBranch: undefined }), "f.ts");
116
+
117
+ expect(globalThis.fetch).toHaveBeenCalledWith(
118
+ expect.stringContaining("?ref=main"),
119
+ expect.anything(),
120
+ );
121
+ });
122
+
123
+ it("throws on a non-OK response (404 = failed to load, not unsupported)", async () => {
124
+ vi.mocked(globalThis.fetch).mockResolvedValue(new Response("nope", { status: 404 }));
125
+
126
+ const { result } = renderHook(() => useGitHubFileReader("ghp_abc"));
127
+ await expect(result.current!(gitEntry(), "missing.ts")).rejects.toThrow("HTTP 404");
128
+ });
129
+
130
+ it("throws when the path is a directory (Contents API returns an array)", async () => {
131
+ vi.mocked(globalThis.fetch).mockResolvedValue(jsonResponse([{ name: "index.ts" }]));
132
+
133
+ const { result } = renderHook(() => useGitHubFileReader("ghp_abc"));
134
+ await expect(result.current!(gitEntry(), "src")).rejects.toThrow("is a directory");
135
+ });
136
+
137
+ it("falls back to the Blob API when Contents returns encoding:none", async () => {
138
+ const big = "x".repeat(2_000_000); // > 1 MB, ≤ 10 MB ceiling
139
+ vi.mocked(globalThis.fetch)
140
+ .mockResolvedValueOnce(
141
+ jsonResponse({ encoding: "none", size: big.length, content: "", sha: "blobsha" }),
142
+ )
143
+ .mockResolvedValueOnce(
144
+ jsonResponse({ encoding: "base64", size: big.length, content: b64(big) }),
145
+ );
146
+
147
+ const { result } = renderHook(() => useGitHubFileReader("ghp_abc"));
148
+ const content = await result.current!(gitEntry(), "big.txt");
149
+
150
+ expect(vi.mocked(globalThis.fetch).mock.calls[1][0]).toBe(
151
+ "https://api.github.com/repos/acme/api/git/blobs/blobsha",
152
+ );
153
+ expect(content!.size).toBe(big.length);
154
+ expect(content!.truncated).toBe(true);
155
+ expect(content!.text).toHaveLength(1_048_576);
156
+ });
157
+
158
+ it("reports files above the blob ceiling as too-large without downloading", async () => {
159
+ const hugeSize = 50 * 1024 * 1024; // 50 MB > 10 MB ceiling
160
+ vi.mocked(globalThis.fetch).mockResolvedValue(
161
+ jsonResponse({ encoding: "none", size: hugeSize, content: "", sha: "blobsha" }),
162
+ );
163
+
164
+ const { result } = renderHook(() => useGitHubFileReader("ghp_abc"));
165
+ const content = await result.current!(gitEntry(), "huge.bin");
166
+
167
+ expect(globalThis.fetch).toHaveBeenCalledTimes(1); // no blob download
168
+ expect(content).toEqual({
169
+ text: null,
170
+ isBinary: false,
171
+ size: hugeSize,
172
+ encoding: "none",
173
+ truncated: true,
174
+ });
175
+ });
176
+
177
+ it("returns a stable reader reference across renders", () => {
178
+ const { result, rerender } = renderHook(() => useGitHubFileReader("ghp_abc"));
179
+ const first = result.current;
180
+ rerender();
181
+ expect(result.current).toBe(first);
182
+ });
183
+ });
@@ -0,0 +1,83 @@
1
+ // Pure byte/text decode helpers for the GitHub file reader.
2
+ // Kept free of React and fetch so the Slice 0 spike behavior (base64 decode,
3
+ // binary detection, the 1 MB cap) is unit-testable in isolation.
4
+
5
+ import {
6
+ MAX_WORKSPACE_FILE_READ_BYTES,
7
+ type WorkspaceFileContent,
8
+ } from "../workspace/WorkspaceFileReader.js";
9
+
10
+ // Scan only the head for a NUL byte — git uses the first 8000 bytes as its
11
+ // text/binary heuristic, and scanning the whole buffer buys nothing.
12
+ const BINARY_SNIFF_BYTES = 8000;
13
+
14
+ /**
15
+ * Decode a GitHub base64 payload to raw bytes.
16
+ *
17
+ * GitHub's Contents API line-wraps its base64 with `\n` every 60 chars, and
18
+ * `atob` rejects embedded newlines in some engines — strip all ASCII
19
+ * whitespace first.
20
+ */
21
+ export function base64ToBytes(b64: string): Uint8Array {
22
+ const clean = b64.replace(/\s/g, "");
23
+ const binary = atob(clean);
24
+ return Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
25
+ }
26
+
27
+ /** Detect binary content via a NUL byte in the head of the buffer. */
28
+ export function detectBinary(bytes: Uint8Array): boolean {
29
+ const limit = Math.min(bytes.length, BINARY_SNIFF_BYTES);
30
+ for (let i = 0; i < limit; i++) {
31
+ if (bytes[i] === 0) return true;
32
+ }
33
+ return false;
34
+ }
35
+
36
+ /**
37
+ * Decode bytes as strict UTF-8, returning `null` when the sequence is invalid.
38
+ * `{ fatal: true }` makes `TextDecoder` throw rather than emit replacement
39
+ * characters, so undecodable files degrade to the binary/unknown fallback
40
+ * instead of silently rendering mojibake.
41
+ */
42
+ export function bytesToText(bytes: Uint8Array): string | null {
43
+ try {
44
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Turn decoded bytes into a {@link WorkspaceFileContent}, applying binary
52
+ * detection and the shared 1 MB display cap.
53
+ *
54
+ * @param bytes the full decoded bytes as delivered by GitHub
55
+ * @param fullSize the file's true size in bytes (GitHub's `size` field), which
56
+ * may exceed `bytes.length` when the caller has already capped
57
+ * the download — reported verbatim as `size`.
58
+ */
59
+ export function normalizeGitHubContent(
60
+ bytes: Uint8Array,
61
+ fullSize: number,
62
+ ): WorkspaceFileContent {
63
+ if (detectBinary(bytes)) {
64
+ return { text: null, isBinary: true, size: fullSize, encoding: "base64" };
65
+ }
66
+
67
+ // Decode the full buffer, then cap the resulting *string* — capping the bytes
68
+ // first could split a multibyte char at the boundary and make the fatal
69
+ // decoder reject an otherwise-valid file.
70
+ const text = bytesToText(bytes);
71
+ if (text === null) {
72
+ return { text: null, isBinary: false, size: fullSize, encoding: "unknown" };
73
+ }
74
+
75
+ const truncated = bytes.length > MAX_WORKSPACE_FILE_READ_BYTES;
76
+ return {
77
+ text: truncated ? text.slice(0, MAX_WORKSPACE_FILE_READ_BYTES) : text,
78
+ isBinary: false,
79
+ size: fullSize,
80
+ encoding: "utf-8",
81
+ ...(truncated ? { truncated: true } : {}),
82
+ };
83
+ }
@@ -25,4 +25,5 @@ export {
25
25
  } from "./GitHubRepoPicker.js";
26
26
 
27
27
  export { useGitHubTreeLister } from "./useGitHubTreeLister.js";
28
+ export { useGitHubFileReader } from "./useGitHubFileReader.js";
28
29
  export { parseGitUrl, type ParsedGitRepo } from "./parseGitUrl.js";
@@ -0,0 +1,152 @@
1
+ "use client";
2
+
3
+ import { useCallback } from "react";
4
+ import type { WorkspaceEntry } from "../workspace/useWorkspaceEntries.js";
5
+ import type {
6
+ WorkspaceFileContent,
7
+ WorkspaceFileReader,
8
+ } from "../workspace/WorkspaceFileReader.js";
9
+ import { base64ToBytes, normalizeGitHubContent } from "./decodeGitHubContent.js";
10
+ import { parseGitUrl } from "./parseGitUrl.js";
11
+
12
+ const GITHUB_API = "https://api.github.com/repos";
13
+
14
+ /**
15
+ * Upper bound on files we will pull through the Blob API (Gate 1, Option B).
16
+ *
17
+ * The Contents API caps at ~1 MB and signals larger files with
18
+ * `encoding: "none"`; the Blob API then serves them in full (up to GitHub's own
19
+ * ~100 MB blob limit). Since we only ever display the first ~1 MB, downloading
20
+ * a 90 MB blob to show 1 MB is wasteful — so above this ceiling we report the
21
+ * file as too-large
22
+ * (`text: null`, `truncated: true`) using the size GitHub already told us,
23
+ * without downloading anything.
24
+ */
25
+ const BLOB_FETCH_CEILING_BYTES = 10 * 1024 * 1024; // 10 MiB
26
+
27
+ /** Single-file shape of `GET /repos/{o}/{r}/contents/{path}`. */
28
+ interface GitHubContentsFile {
29
+ readonly encoding: "base64" | "none";
30
+ readonly size: number;
31
+ readonly content: string;
32
+ readonly sha: string;
33
+ }
34
+
35
+ /** Shape of `GET /repos/{o}/{r}/git/blobs/{sha}`. */
36
+ interface GitHubBlob {
37
+ readonly encoding: "base64";
38
+ readonly size: number;
39
+ readonly content: string;
40
+ }
41
+
42
+ /** Encode a repo-relative path per segment, preserving the slashes. */
43
+ function encodePath(path: string): string {
44
+ return path.split("/").map(encodeURIComponent).join("/");
45
+ }
46
+
47
+ /**
48
+ * Creates a {@link WorkspaceFileReader} backed by the GitHub Contents API,
49
+ * with a Blob-API fallback for files above the Contents 1 MB cap.
50
+ *
51
+ * Uses the same client-side OAuth token as {@link useGitHubTreeLister}
52
+ * (`Authorization: Bearer`), inheriting its private-repo scope. Returns
53
+ * `undefined` when no token is present, mirroring the tree lister so callers
54
+ * can treat "no reader" and "no lister" identically.
55
+ *
56
+ * The reader itself:
57
+ * - Returns `null` when the entry is not a readable GitHub git repo
58
+ * (non-git entry, missing/unparseable URL) — the "unsupported here" state.
59
+ * - **Throws** on a real failure: a non-OK response, or a directory path
60
+ * (which the Contents API returns as a JSON array). Consumers surface these
61
+ * as an error, distinct from the null "unsupported" state.
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * const gitHubConnection = useGitHubConnection(org);
66
+ * const fileReader = useGitHubFileReader(gitHubConnection.token);
67
+ * <SessionViewer workspaceFileReader={fileReader} />
68
+ * ```
69
+ */
70
+ export function useGitHubFileReader(
71
+ token: string | null,
72
+ ): WorkspaceFileReader | undefined {
73
+ const reader = useCallback(
74
+ async (
75
+ entry: WorkspaceEntry,
76
+ path: string,
77
+ ): Promise<WorkspaceFileContent | null> => {
78
+ if (entry.type !== "git" || !entry.gitUrl || !token) return null;
79
+
80
+ const parsed = parseGitUrl(entry.gitUrl);
81
+ if (!parsed) return null;
82
+
83
+ const branch = entry.gitBranch || "main";
84
+ const url =
85
+ `${GITHUB_API}/${parsed.owner}/${parsed.repo}/contents/${encodePath(path)}` +
86
+ `?ref=${encodeURIComponent(branch)}`;
87
+
88
+ const resp = await fetch(url, {
89
+ headers: { Authorization: `Bearer ${token}` },
90
+ });
91
+ if (!resp.ok) {
92
+ throw new Error(
93
+ `GitHub content fetch failed for ${path} (HTTP ${resp.status})`,
94
+ );
95
+ }
96
+
97
+ const data: unknown = await resp.json();
98
+ // The Contents API returns an array for a directory — a misdirected read,
99
+ // not an unsupported substrate, so fail loudly rather than return null.
100
+ if (Array.isArray(data)) {
101
+ throw new Error(`Cannot read "${path}": path is a directory`);
102
+ }
103
+
104
+ const file = data as GitHubContentsFile;
105
+ if (file.encoding === "base64") {
106
+ return normalizeGitHubContent(base64ToBytes(file.content), file.size);
107
+ }
108
+
109
+ // encoding: "none" is GitHub's signal that the file exceeds the Contents
110
+ // API's ~1 MB cap; the bytes live only behind the Blob API.
111
+ return readViaBlob(parsed, file, token);
112
+ },
113
+ [token],
114
+ );
115
+
116
+ if (!token) return undefined;
117
+ return reader;
118
+ }
119
+
120
+ /**
121
+ * Gate 1 / Option B — size-gated blob fetch for files the Contents API refused
122
+ * (`encoding: "none"`). Isolated so the large-file policy can change without
123
+ * touching the reader's control flow.
124
+ */
125
+ async function readViaBlob(
126
+ parsed: { owner: string; repo: string },
127
+ file: GitHubContentsFile,
128
+ token: string,
129
+ ): Promise<WorkspaceFileContent> {
130
+ // Above the ceiling we never download — GitHub already told us the size, so
131
+ // report it as too-large-to-preview without spending the bandwidth.
132
+ if (file.size > BLOB_FETCH_CEILING_BYTES) {
133
+ return {
134
+ text: null,
135
+ isBinary: false,
136
+ size: file.size,
137
+ encoding: "none",
138
+ truncated: true,
139
+ };
140
+ }
141
+
142
+ const url = `${GITHUB_API}/${parsed.owner}/${parsed.repo}/git/blobs/${file.sha}`;
143
+ const resp = await fetch(url, {
144
+ headers: { Authorization: `Bearer ${token}` },
145
+ });
146
+ if (!resp.ok) {
147
+ throw new Error(`GitHub blob fetch failed for ${file.sha} (HTTP ${resp.status})`);
148
+ }
149
+
150
+ const blob: GitHubBlob = await resp.json();
151
+ return normalizeGitHubContent(base64ToBytes(blob.content), blob.size);
152
+ }
@@ -22,12 +22,16 @@ interface GitHubTreeResponse {
22
22
  }
23
23
 
24
24
  /**
25
- * Sentinel entry appended to the listing when the GitHub API indicates
26
- * that the tree was truncated (repository exceeds the API's entry limit).
25
+ * Advisory entry appended to the listing when the GitHub API indicates that the
26
+ * tree was truncated (repository exceeds the API's entry limit). Flagged with
27
+ * `notice: true` so generic consumers treat it as a "results incomplete" banner
28
+ * rather than an openable file (see `workspaceListingCache`) — never a tree leaf,
29
+ * never a search hit.
27
30
  */
28
31
  const TRUNCATION_MARKER: WorkspaceFileEntry = {
29
32
  path: "... (tree truncated by GitHub — repository has too many files)",
30
33
  isDirectory: false,
34
+ notice: true,
31
35
  };
32
36
 
33
37
  /**