@opengeni/react 0.46.1 → 0.52.1

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 (341) hide show
  1. package/README.md +158 -14
  2. package/dist/artifacts-document.d.ts +3 -0
  3. package/dist/artifacts-document.js +17 -0
  4. package/dist/artifacts-document.js.map +1 -0
  5. package/dist/artifacts-presentation.d.ts +3 -0
  6. package/dist/artifacts-presentation.js +17 -0
  7. package/dist/artifacts-presentation.js.map +1 -0
  8. package/dist/artifacts-spreadsheet.d.ts +3 -0
  9. package/dist/artifacts-spreadsheet.js +17 -0
  10. package/dist/artifacts-spreadsheet.js.map +1 -0
  11. package/dist/artifacts.d.ts +2 -0
  12. package/dist/artifacts.js +170 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/chunk-6BBWWOOU.js +82 -0
  15. package/dist/chunk-6BBWWOOU.js.map +1 -0
  16. package/dist/{chunk-HFO4ERGQ.js → chunk-724BW4TW.js} +586 -35
  17. package/dist/chunk-724BW4TW.js.map +1 -0
  18. package/dist/chunk-7I2KB3OK.js +3895 -0
  19. package/dist/chunk-7I2KB3OK.js.map +1 -0
  20. package/dist/chunk-7WCLAEFV.js +2828 -0
  21. package/dist/chunk-7WCLAEFV.js.map +1 -0
  22. package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
  23. package/dist/chunk-FK6VG4LL.js.map +1 -0
  24. package/dist/{chunk-SRFUT2ZU.js → chunk-FWS3KC3Z.js} +120 -64
  25. package/dist/chunk-FWS3KC3Z.js.map +1 -0
  26. package/dist/{chunk-EKZH6IDG.js → chunk-HDBG7XGO.js} +62 -1
  27. package/dist/chunk-HDBG7XGO.js.map +1 -0
  28. package/dist/chunk-HUIKIDM5.js +2148 -0
  29. package/dist/chunk-HUIKIDM5.js.map +1 -0
  30. package/dist/{chunk-LGVMUIRV.js → chunk-JAGDUCZQ.js} +1053 -390
  31. package/dist/chunk-JAGDUCZQ.js.map +1 -0
  32. package/dist/{chunk-LWR4MXSS.js → chunk-JR3QFY6B.js} +420 -35
  33. package/dist/chunk-JR3QFY6B.js.map +1 -0
  34. package/dist/chunk-JVYQRRC4.js +53 -0
  35. package/dist/chunk-JVYQRRC4.js.map +1 -0
  36. package/dist/chunk-KCGXPP5V.js +157 -0
  37. package/dist/chunk-KCGXPP5V.js.map +1 -0
  38. package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
  39. package/dist/chunk-KZ73RL76.js.map +1 -0
  40. package/dist/chunk-PEI6RANI.js +3234 -0
  41. package/dist/chunk-PEI6RANI.js.map +1 -0
  42. package/dist/{chunk-3PB7MIT6.js → chunk-ROIRTM37.js} +220 -72
  43. package/dist/chunk-ROIRTM37.js.map +1 -0
  44. package/dist/chunk-VZTQ73XT.js +86 -0
  45. package/dist/chunk-VZTQ73XT.js.map +1 -0
  46. package/dist/{chunk-VCBFUSUZ.js → chunk-WSK7G5LE.js} +72 -4
  47. package/dist/chunk-WSK7G5LE.js.map +1 -0
  48. package/dist/{chunk-FGZUCXZF.js → chunk-WYA65Y3F.js} +16 -16
  49. package/dist/chunk-WYA65Y3F.js.map +1 -0
  50. package/dist/{chunk-6EDV6YFE.js → chunk-X4S56Y2H.js} +147 -35
  51. package/dist/chunk-X4S56Y2H.js.map +1 -0
  52. package/dist/client.d.ts +7 -1
  53. package/dist/clipboard.d.ts +1 -0
  54. package/dist/clipboard.js +7 -0
  55. package/dist/clipboard.js.map +1 -0
  56. package/dist/commands/index.d.ts +3 -3
  57. package/dist/commands/registry.d.ts +1 -1
  58. package/dist/commands/types.d.ts +1 -1
  59. package/dist/components/approval-surface.d.ts +2 -2
  60. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  61. package/dist/components/artifacts/document-editor.d.ts +172 -0
  62. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  63. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  64. package/dist/components/artifacts/editable-document.d.ts +24 -0
  65. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  66. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  67. package/dist/components/artifacts/index.d.ts +8 -0
  68. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  69. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  70. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  71. package/dist/components/browser-viewer.d.ts +29 -0
  72. package/dist/components/chat-composer.d.ts +13 -8
  73. package/dist/components/code-editor.d.ts +25 -3
  74. package/dist/components/command-palette.d.ts +2 -2
  75. package/dist/components/composer-transcription-control.d.ts +4 -2
  76. package/dist/components/composer.d.ts +29 -15
  77. package/dist/components/computer-viewer.d.ts +23 -0
  78. package/dist/components/copy-button.d.ts +2 -2
  79. package/dist/components/desktop-viewer.d.ts +25 -21
  80. package/dist/components/diff-view.d.ts +1 -1
  81. package/dist/components/enrollment-consent.d.ts +1 -1
  82. package/dist/components/enrollment-device-flow.d.ts +1 -1
  83. package/dist/components/file-browser.d.ts +2 -2
  84. package/dist/components/fleet-tile.d.ts +1 -1
  85. package/dist/components/generated-video-player.d.ts +10 -0
  86. package/dist/components/human-input-form.d.ts +1 -1
  87. package/dist/components/human-input-surface.d.ts +2 -2
  88. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  89. package/dist/components/machine-card.d.ts +2 -2
  90. package/dist/components/machine-dock-bar.d.ts +3 -3
  91. package/dist/components/machine-health-pill.d.ts +2 -2
  92. package/dist/components/machine-input-display.d.ts +1 -1
  93. package/dist/components/machine-metrics.d.ts +2 -2
  94. package/dist/components/machine-status-pill.d.ts +4 -4
  95. package/dist/components/machines/health.d.ts +1 -1
  96. package/dist/components/machines/machine-detail.d.ts +5 -3
  97. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  98. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  99. package/dist/components/machines/series.d.ts +2 -2
  100. package/dist/components/machines-dashboard.d.ts +5 -5
  101. package/dist/components/markdown.d.ts +1 -1
  102. package/dist/components/message-timeline.d.ts +22 -5
  103. package/dist/components/model-picker.d.ts +2 -2
  104. package/dist/components/model-policy-picker.d.ts +7 -7
  105. package/dist/components/pierre-diff.d.ts +1 -1
  106. package/dist/components/pierre-file.d.ts +1 -1
  107. package/dist/components/queue-draft-policy.d.ts +1 -1
  108. package/dist/components/queue-surface-implementation.d.ts +3 -3
  109. package/dist/components/queue-surface-state.d.ts +4 -4
  110. package/dist/components/queue-surface.d.ts +5 -5
  111. package/dist/components/sandbox-files.d.ts +5 -5
  112. package/dist/components/sandbox-terminal.d.ts +50 -4
  113. package/dist/components/sandbox-workspace.d.ts +30 -10
  114. package/dist/components/session-chrome.d.ts +4 -4
  115. package/dist/components/session-status.d.ts +2 -2
  116. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  117. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  118. package/dist/components/timeline-annotations.d.ts +13 -0
  119. package/dist/components/tooltip.d.ts +4 -4
  120. package/dist/components/user-message-body.d.ts +2 -2
  121. package/dist/components/workbench-changes.d.ts +2 -2
  122. package/dist/components/workspace-dock.d.ts +3 -1
  123. package/dist/composer.d.ts +11 -11
  124. package/dist/composer.js +7 -7
  125. package/dist/hooks/internal.d.ts +7 -1
  126. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  127. package/dist/hooks/use-available-models.d.ts +2 -2
  128. package/dist/hooks/use-billing-usage.d.ts +1 -1
  129. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  130. package/dist/hooks/use-browser-identities.d.ts +26 -0
  131. package/dist/hooks/use-browser-session.d.ts +28 -0
  132. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  133. package/dist/hooks/use-codex-accounts.d.ts +3 -3
  134. package/dist/hooks/use-composer.d.ts +36 -9
  135. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  136. package/dist/hooks/use-computer-session.d.ts +28 -0
  137. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  138. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  139. package/dist/hooks/use-environments.d.ts +11 -5
  140. package/dist/hooks/use-file-attachments.d.ts +1 -1
  141. package/dist/hooks/use-goal.d.ts +2 -2
  142. package/dist/hooks/use-human-input.d.ts +2 -2
  143. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  144. package/dist/hooks/use-machine-chip.d.ts +2 -2
  145. package/dist/hooks/use-machines.d.ts +10 -3
  146. package/dist/hooks/use-packs.d.ts +7 -2
  147. package/dist/hooks/use-rigs.d.ts +1 -1
  148. package/dist/hooks/use-sandbox-files.d.ts +25 -4
  149. package/dist/hooks/use-sandbox-git.d.ts +13 -5
  150. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  151. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  152. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  153. package/dist/hooks/use-session-control.d.ts +1 -1
  154. package/dist/hooks/use-session-events.d.ts +2 -2
  155. package/dist/hooks/use-session-lineage.d.ts +1 -1
  156. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  157. package/dist/hooks/use-session.d.ts +2 -2
  158. package/dist/hooks/use-slash-commands.d.ts +1 -1
  159. package/dist/hooks/use-terminal-stream.d.ts +8 -2
  160. package/dist/hooks/use-transcription.d.ts +2 -2
  161. package/dist/hooks/use-turn-queue.d.ts +2 -2
  162. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  163. package/dist/hooks/use-voice-input.d.ts +6 -4
  164. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  165. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  166. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  167. package/dist/hooks/use-workspaces.d.ts +1 -1
  168. package/dist/index.d.ts +158 -154
  169. package/dist/index.js +1718 -908
  170. package/dist/index.js.map +1 -1
  171. package/dist/interaction.d.ts +23 -0
  172. package/dist/interaction.js +36 -0
  173. package/dist/interaction.js.map +1 -0
  174. package/dist/interactive-workbench-viewers-KHNOHWQE.js +13 -0
  175. package/dist/interactive-workbench-viewers-KHNOHWQE.js.map +1 -0
  176. package/dist/lib/composer-responsive-context.d.ts +9 -0
  177. package/dist/lib/relay-wire.d.ts +4 -0
  178. package/dist/lib/sandbox-liveness.d.ts +9 -0
  179. package/dist/lib/terminal-capability.d.ts +5 -0
  180. package/dist/lib/use-portal-token-style.d.ts +14 -3
  181. package/dist/lib/xterm-theme.d.ts +1 -1
  182. package/dist/machines.d.ts +30 -30
  183. package/dist/machines.js +3 -3
  184. package/dist/provider.d.ts +4 -4
  185. package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-2KVJTYXH.js} +117 -64
  186. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  187. package/dist/realtime/dropdown-menu.d.ts +8 -8
  188. package/dist/realtime/realtime-control.d.ts +15 -6
  189. package/dist/realtime.d.ts +4 -4
  190. package/dist/realtime.js +30 -33
  191. package/dist/realtime.js.map +1 -1
  192. package/dist/session-context.d.ts +19 -1
  193. package/dist/session-ui.d.ts +14 -14
  194. package/dist/session-ui.js +6 -5
  195. package/dist/session.d.ts +28 -28
  196. package/dist/session.js +5 -5
  197. package/dist/timeline/activity-rail.d.ts +6 -3
  198. package/dist/timeline/disclosure-context.d.ts +1 -1
  199. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  200. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  201. package/dist/timeline/index.d.ts +19 -19
  202. package/dist/timeline/parsers.d.ts +6 -3
  203. package/dist/timeline/projection.d.ts +3 -3
  204. package/dist/timeline/registry.d.ts +10 -1
  205. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  206. package/dist/timeline/shared.d.ts +21 -10
  207. package/dist/timeline/tool-diff.d.ts +2 -2
  208. package/dist/timeline/tool-renderers.d.ts +1 -1
  209. package/dist/timeline/turn-summary.d.ts +3 -3
  210. package/dist/timeline/types.d.ts +42 -2
  211. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  212. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  213. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  214. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  215. package/dist/voice-recording-store.d.ts +8 -2
  216. package/package.json +52 -7
  217. package/src/artifacts-document.ts +31 -0
  218. package/src/artifacts-presentation.ts +36 -0
  219. package/src/artifacts-spreadsheet.ts +21 -0
  220. package/src/artifacts.ts +86 -0
  221. package/src/client.ts +90 -1
  222. package/src/clipboard.ts +2 -0
  223. package/src/components/artifacts/artifact-surface.tsx +93 -0
  224. package/src/components/artifacts/document-editor.tsx +2255 -0
  225. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  226. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  227. package/src/components/artifacts/editable-document.tsx +657 -0
  228. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  229. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  230. package/src/components/artifacts/index.ts +99 -0
  231. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  232. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  233. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  234. package/src/components/browser-viewer.tsx +1592 -0
  235. package/src/components/chat-composer.tsx +25 -2
  236. package/src/components/code-editor.tsx +90 -31
  237. package/src/components/command-palette.tsx +1 -1
  238. package/src/components/composer-transcription-control.tsx +51 -22
  239. package/src/components/composer.tsx +49 -23
  240. package/src/components/computer-viewer.tsx +1139 -0
  241. package/src/components/copy-button.tsx +1 -1
  242. package/src/components/desktop-viewer.tsx +103 -75
  243. package/src/components/file-browser.tsx +39 -17
  244. package/src/components/generated-video-player.tsx +89 -0
  245. package/src/components/human-input-form.tsx +2 -2
  246. package/src/components/interactive-workbench-viewers.ts +2 -0
  247. package/src/components/machine-card.tsx +1 -1
  248. package/src/components/machine-input-display.ts +3 -0
  249. package/src/components/machines/machine-detail.tsx +39 -15
  250. package/src/components/machines-dashboard.tsx +7 -6
  251. package/src/components/message-timeline.tsx +327 -37
  252. package/src/components/model-picker.tsx +2 -2
  253. package/src/components/model-policy-picker.tsx +16 -15
  254. package/src/components/queue-surface-implementation.tsx +135 -75
  255. package/src/components/queue-surface-state.tsx +2 -2
  256. package/src/components/sandbox-files.tsx +142 -62
  257. package/src/components/sandbox-terminal.tsx +119 -35
  258. package/src/components/sandbox-workspace.tsx +394 -228
  259. package/src/components/session-chrome.tsx +67 -27
  260. package/src/components/timeline-annotation-selection.tsx +221 -0
  261. package/src/components/timeline-annotations-dialog.tsx +213 -0
  262. package/src/components/timeline-annotations.tsx +84 -0
  263. package/src/components/tooltip.tsx +13 -16
  264. package/src/components/user-message-body.tsx +1 -1
  265. package/src/components/workbench-changes.tsx +192 -35
  266. package/src/components/workspace-dock.tsx +13 -5
  267. package/src/composer.ts +3 -0
  268. package/src/hooks/internal.ts +132 -38
  269. package/src/hooks/use-attached-browsers.ts +152 -0
  270. package/src/hooks/use-browser-frame-stream.ts +316 -0
  271. package/src/hooks/use-browser-identities.ts +225 -0
  272. package/src/hooks/use-browser-session.ts +476 -0
  273. package/src/hooks/use-browser-sessions.ts +281 -0
  274. package/src/hooks/use-codex-accounts.ts +4 -4
  275. package/src/hooks/use-composer.ts +769 -24
  276. package/src/hooks/use-computer-frame-stream.ts +311 -0
  277. package/src/hooks/use-computer-session.ts +442 -0
  278. package/src/hooks/use-computer-sessions.ts +245 -0
  279. package/src/hooks/use-environments.ts +24 -5
  280. package/src/hooks/use-file-attachments.ts +70 -32
  281. package/src/hooks/use-goal.ts +17 -5
  282. package/src/hooks/use-machine-chip.ts +5 -3
  283. package/src/hooks/use-machines.ts +79 -3
  284. package/src/hooks/use-packs.ts +51 -0
  285. package/src/hooks/use-sandbox-files.ts +382 -88
  286. package/src/hooks/use-sandbox-git.ts +208 -46
  287. package/src/hooks/use-sandbox-terminal.ts +3 -2
  288. package/src/hooks/use-session-capabilities.ts +94 -70
  289. package/src/hooks/use-session-events.ts +18 -4
  290. package/src/hooks/use-terminal-stream.ts +102 -23
  291. package/src/hooks/use-transcription.ts +4 -14
  292. package/src/hooks/use-turn-queue.ts +17 -5
  293. package/src/hooks/use-video-artifact-playback.ts +28 -0
  294. package/src/hooks/use-voice-input.ts +163 -19
  295. package/src/hooks/use-windowed-sections.ts +9 -7
  296. package/src/hooks/use-workspace-edit.ts +2 -1
  297. package/src/index.ts +21 -6
  298. package/src/interaction.ts +71 -0
  299. package/src/lib/composer-responsive-context.ts +15 -0
  300. package/src/lib/relay-wire.ts +4 -0
  301. package/src/lib/sandbox-liveness.ts +11 -0
  302. package/src/lib/terminal-capability.ts +10 -0
  303. package/src/lib/use-portal-token-style.ts +60 -15
  304. package/src/provider.tsx +52 -7
  305. package/src/realtime/dropdown-menu.tsx +15 -19
  306. package/src/realtime/realtime-control.tsx +16 -5
  307. package/src/session-context.ts +90 -0
  308. package/src/timeline/activity-rail.tsx +31 -6
  309. package/src/timeline/index.ts +4 -1
  310. package/src/timeline/parsers.ts +46 -20
  311. package/src/timeline/projection.ts +233 -13
  312. package/src/timeline/registry.ts +19 -0
  313. package/src/timeline/screenshot-lightbox.tsx +30 -9
  314. package/src/timeline/shared.tsx +59 -11
  315. package/src/timeline/tool-renderers.tsx +421 -24
  316. package/src/timeline/turn-summary.tsx +2 -2
  317. package/src/timeline/types.ts +58 -2
  318. package/src/voice-recording-store.ts +15 -0
  319. package/styles/compiled.css +5298 -0
  320. package/styles/compiled.d.ts +2 -0
  321. package/styles/effective-tokens.css +27 -0
  322. package/styles/index.css +18 -0
  323. package/styles/responsive.css +208 -0
  324. package/styles/responsive.d.ts +2 -0
  325. package/styles/tokens.css +50 -17
  326. package/dist/chunk-3PB7MIT6.js.map +0 -1
  327. package/dist/chunk-6EDV6YFE.js.map +0 -1
  328. package/dist/chunk-EB67J347.js.map +0 -1
  329. package/dist/chunk-EKZH6IDG.js.map +0 -1
  330. package/dist/chunk-FGZUCXZF.js.map +0 -1
  331. package/dist/chunk-HFO4ERGQ.js.map +0 -1
  332. package/dist/chunk-JB5725BI.js +0 -48
  333. package/dist/chunk-JB5725BI.js.map +0 -1
  334. package/dist/chunk-LGVMUIRV.js.map +0 -1
  335. package/dist/chunk-LWR4MXSS.js.map +0 -1
  336. package/dist/chunk-SRFUT2ZU.js.map +0 -1
  337. package/dist/chunk-U3CSCMWA.js.map +0 -1
  338. package/dist/chunk-VCBFUSUZ.js.map +0 -1
  339. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  340. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  341. package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
@@ -5,12 +5,15 @@ import type {
5
5
  FsWriteResponse,
6
6
  GitChangedPayload,
7
7
  GitFileStatusCode,
8
+ GitStatusResponse,
8
9
  OpenGeniRequestOptions,
10
+ SessionCapabilities,
9
11
  SessionEvent,
10
12
  WorkspaceCaptureManifest,
11
13
  } from "@opengeni/sdk";
12
- import { useCallback, useEffect, useRef, useState } from "react";
13
- import { useOpenGeni, type ClientOverride } from "../provider";
14
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
15
+ import { type ClientOverride, useOpenGeni } from "../provider";
16
+ import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
14
17
 
15
18
  /** The git-status overlay a file row may carry (tints modified files in the tree). */
16
19
  export type FileTreeStatus = "added" | "modified" | "deleted" | "renamed" | "untracked";
@@ -82,12 +85,18 @@ export type UseSandboxFilesOptions = ClientOverride & {
82
85
  rootPath?: string | undefined;
83
86
  /** Hold off the initial list (e.g. panel collapsed). Default true. */
84
87
  enabled?: boolean | undefined;
85
- /** The lease liveness ("cold" | "warm" | "draining"). The structured FileSystem
88
+ /** Pause live reads/invalidation while preserving the last rendered result. */
89
+ active?: boolean | undefined;
90
+ /** Repository roots advertised by capabilities, relative to the workspace. */
91
+ repoPaths?: readonly string[] | undefined;
92
+ /** The lease liveness. `null` means capability negotiation has not produced
93
+ * an authoritative lifecycle value yet and therefore blocks live I/O. Omitting
94
+ * the option preserves the legacy direct-hook behavior. The structured FileSystem
86
95
  * capability is advertised even on a COLD box, so the mount-time list can race
87
96
  * the box: it lists before the box is warm, gets an empty/errored result, and
88
97
  * (with no `fs.changed` event) never re-lists. Passing liveness re-lists when
89
98
  * the box first becomes warm, so the tree populates as soon as the box is up. */
90
- liveness?: string | undefined;
99
+ liveness?: SessionCapabilities["liveness"] | null | undefined;
91
100
  /** The latest turn-end workspace capture (from `useWorkspaceCapture`). The tree
92
101
  * paints immediately from this durable index, then a warm box reconciles live
93
102
  * in place. If that live read fails, the capture remains a truthful read-only
@@ -127,15 +136,58 @@ export type UseSandboxFilesResult = {
127
136
  moveEntry: (path: string, newPath: string, opts?: { overwrite?: boolean }) => Promise<void>;
128
137
  /** Re-list the whole tree from the root. */
129
138
  refresh: () => Promise<void>;
139
+ /** Monotonic invalidation token for open file previews. Advances when an
140
+ * external filesystem change or completed agent tool may have changed bytes. */
141
+ contentRevision?: number | undefined;
130
142
  /** Which source the tree is currently served from: the live box, the turn-end
131
143
  * capture (cold/offline), or neither yet. M5's source badge reads this. */
132
144
  source: "live" | "capture" | null;
133
145
  /** When the served capture was taken (ISO), when `source === "capture"`. */
134
146
  capturedAt: string | null;
147
+ /** Lightweight repository context loaded by the same status request that tints files. */
148
+ gitSummary?: SandboxFilesGitSummary | null | undefined;
149
+ /** True only while the initial repository-status overlay is still loading. */
150
+ gitLoading?: boolean | undefined;
135
151
  loading: boolean;
136
152
  error: Error | null;
137
153
  };
138
154
 
155
+ export type SandboxFilesGitSummary = {
156
+ branch: string | null;
157
+ isRepo: boolean;
158
+ repoCount: number;
159
+ ahead: number;
160
+ behind: number;
161
+ dirtyCount: number;
162
+ };
163
+
164
+ function normalizeRoot(root: string): string {
165
+ const normalized = root
166
+ .replaceAll("\\", "/")
167
+ .replace(/^\.\/+/, "")
168
+ .replace(/\/+$/, "");
169
+ return normalized === "." ? "" : normalized;
170
+ }
171
+
172
+ function qualifyStatusPath(root: string, path: string): string {
173
+ const normalized = normalizeRoot(root);
174
+ return normalized ? `${normalized}/${path}` : path;
175
+ }
176
+
177
+ function summarizeStatuses(
178
+ statuses: Array<{ root: string; status: GitStatusResponse }>,
179
+ ): SandboxFilesGitSummary {
180
+ const repositories = statuses.filter(({ status }) => status.isRepo);
181
+ return {
182
+ branch: repositories.length === 1 ? (repositories[0]?.status.head ?? null) : null,
183
+ isRepo: repositories.length > 0,
184
+ repoCount: repositories.length,
185
+ ahead: repositories.reduce((sum, { status }) => sum + status.ahead, 0),
186
+ behind: repositories.reduce((sum, { status }) => sum + status.behind, 0),
187
+ dirtyCount: repositories.reduce((sum, { status }) => sum + status.files.length, 0),
188
+ };
189
+ }
190
+
139
191
  /** The workspace-relative parent directory of a POSIX path ("" for a root entry). */
140
192
  function parentOf(path: string): string {
141
193
  const i = path.lastIndexOf("/");
@@ -209,7 +261,10 @@ function replaceChildren(
209
261
  return { ...node, children };
210
262
  }
211
263
  if (node.kind === "dir" && node.children && targetPath.startsWith(`${node.path}/`)) {
212
- return { ...node, children: replaceChildren(node.children, targetPath, children) };
264
+ return {
265
+ ...node,
266
+ children: replaceChildren(node.children, targetPath, children),
267
+ };
213
268
  }
214
269
  return node;
215
270
  });
@@ -227,6 +282,42 @@ function findNodeByPath(nodes: FileTreeNode[], path: string): FileTreeNode | und
227
282
  return undefined;
228
283
  }
229
284
 
285
+ /** Root plus every directory whose children are currently loaded. Re-listing
286
+ * this small visible frontier refreshes agent shell edits without expanding or
287
+ * traversing hidden directories. */
288
+ function loadedDirectoryPaths(nodes: FileTreeNode[]): string[] {
289
+ const paths = [""];
290
+ const walk = (list: FileTreeNode[]) => {
291
+ for (const node of list) {
292
+ if (node.kind !== "dir" || node.children === undefined) continue;
293
+ paths.push(node.path);
294
+ walk(node.children);
295
+ }
296
+ };
297
+ walk(nodes);
298
+ return paths;
299
+ }
300
+
301
+ /** Repository paths are capability-advertised and therefore safe to use as a
302
+ * bounded initial lazy-tree frontier. Listing their ancestors concurrently with
303
+ * the root avoids a root → repositories → repo remote waterfall. */
304
+ function initialDirectoryFrontier(rootPath: string, repoPaths: readonly string[]): string[] {
305
+ const rootParts = normalizeRoot(rootPath).split("/").filter(Boolean);
306
+ const paths = new Set<string>();
307
+ for (const repoPath of repoPaths) {
308
+ const parts = normalizeRoot(repoPath).split("/").filter(Boolean);
309
+ if (rootParts.some((part, index) => parts[index] !== part)) continue;
310
+ for (let depth = rootParts.length + 1; depth <= parts.length; depth += 1) {
311
+ paths.add(parts.slice(0, depth).join("/"));
312
+ }
313
+ }
314
+ // A workspace with hundreds of repositories should remain lazy. This still
315
+ // preloads the shared repository container and the first visible roots.
316
+ return [...paths]
317
+ .sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b))
318
+ .slice(0, 8);
319
+ }
320
+
230
321
  // ── In-place (optimistic) tree mutations ────────────────────────────────────
231
322
  // The Pierre mutation-handle analogue. These splice a single node in/out/across
232
323
  // the immutable tree, PRESERVING every other node's `children` (and therefore the
@@ -279,7 +370,10 @@ function removeNode(nodes: FileTreeNode[], path: string): FileTreeNode[] {
279
370
  return nodes.map((node) => {
280
371
  if (node.path === parent) {
281
372
  if (node.kind !== "dir" || node.children === undefined) return node;
282
- return { ...node, children: node.children.filter((n) => n.path !== path) };
373
+ return {
374
+ ...node,
375
+ children: node.children.filter((n) => n.path !== path),
376
+ };
283
377
  }
284
378
  if (node.kind === "dir" && node.children && parent.startsWith(`${node.path}/`)) {
285
379
  return { ...node, children: removeNode(node.children, path) };
@@ -390,10 +484,21 @@ export function useSandboxFiles(
390
484
  ): UseSandboxFilesResult {
391
485
  const { client, workspaceId } = useOpenGeni(options);
392
486
  const enabled = (options.enabled ?? true) && Boolean(sessionId);
487
+ const active = options.active ?? true;
393
488
  const rootPath = options.rootPath ?? "";
489
+ const repoPathsKey = [
490
+ ...new Set((options.repoPaths?.length ? options.repoPaths : [rootPath]).map(normalizeRoot)),
491
+ ].join("\u0000");
492
+ const repoPaths = useMemo(() => repoPathsKey.split("\u0000"), [repoPathsKey]);
394
493
  const capture = options.capture ?? null;
395
- const isLive = options.liveness === "warm" || options.liveness === "draining";
396
- const identityKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${rootPath}`;
494
+ const isLive = sandboxAcceptsLiveIo(options.liveness);
495
+ // `liveness` predates this hook option. Preserve legacy embedders that omit it;
496
+ // explicit `null` (negotiating) and lifecycle values require authoritative warm.
497
+ const acceptsEventReads = options.liveness === undefined || isLive;
498
+ const acceptsEventReadsRef = useRef(acceptsEventReads);
499
+ acceptsEventReadsRef.current = acceptsEventReads;
500
+ const hasLiveIoFence = options.liveness !== undefined;
501
+ const identityKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${rootPath}\u0000${repoPathsKey}`;
397
502
 
398
503
  const [tree, setTree] = useState<FileTreeNode[]>([]);
399
504
  // A ref mirror of the current tree — lets the optimistic path snapshot the
@@ -404,6 +509,9 @@ export function useSandboxFiles(
404
509
  const [loading, setLoading] = useState(false);
405
510
  const [expandingPaths, setExpandingPaths] = useState<Set<string>>(new Set());
406
511
  const [error, setError] = useState<Error | null>(null);
512
+ const [gitSummary, setGitSummary] = useState<SandboxFilesGitSummary | null>(null);
513
+ const [gitLoading, setGitLoading] = useState(false);
514
+ const [contentRevision, setContentRevision] = useState(0);
407
515
  const [stateIdentity, setStateIdentity] = useState(identityKey);
408
516
  // Which source the tree currently reflects — "capture" (cold paint) or "live".
409
517
  const [source, setSource] = useState<"live" | "capture" | null>(null);
@@ -414,11 +522,14 @@ export function useSandboxFiles(
414
522
  const refreshGenerationRef = useRef(0);
415
523
  const refreshAbortRef = useRef<AbortController | null>(null);
416
524
  const identityAbortRef = useRef(new AbortController());
525
+ const eventReadAbortRef = useRef(new AbortController());
417
526
  const identityGenerationRef = useRef(0);
418
527
  const lastSeqRef = useRef(0);
419
528
  const pendingParentsRef = useRef<Set<string>>(new Set());
420
529
  const pendingGitRef = useRef(false);
530
+ const pendingAgentToolRef = useRef(false);
421
531
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
532
+ const lastCommandRefreshAtRef = useRef(0);
422
533
 
423
534
  // ── Self-emitted fs.changed de-dupe ───────────────────────────────────────
424
535
  // Every one of OUR mutations emits an `fs.changed` event back on the live log
@@ -470,55 +581,97 @@ export function useSandboxFiles(
470
581
  refreshAbortRef.current = refreshAbort;
471
582
  const generation = (refreshGenerationRef.current += 1);
472
583
  setLoading(true);
584
+ setGitLoading(true);
473
585
  setError(null);
474
586
  try {
475
- // Pull the git-status overlay first (best-effort a non-repo box just
476
- // returns isRepo:false), then the tree, so the first paint is tinted.
477
- try {
478
- const status = await client.gitStatus(
479
- workspaceId,
480
- sessionId,
481
- { path: rootPath },
482
- { signal: refreshAbort.signal },
483
- );
484
- if (refreshGenerationRef.current !== generation) return;
485
- const overlay = new Map<string, FileTreeStatus>();
486
- for (const file of status.files) {
487
- const code = file.worktree ?? file.index;
488
- const mapped = code ? GIT_STATUS_TO_TREE[code] : undefined;
489
- if (mapped) overlay.set(file.path, mapped);
490
- }
491
- statusRef.current = overlay;
492
- } catch {
493
- if (refreshGenerationRef.current !== generation) return;
494
- statusRef.current = new Map();
495
- }
496
- const listed = await client.fsList(
587
+ // Start the optional Git tint and the actual directory listing together.
588
+ // A remote status read must never block the first useful file-tree paint.
589
+ const frontierPaths = initialDirectoryFrontier(rootPath, repoPaths);
590
+ const listRequests = [rootPath, ...frontierPaths].map((path) => ({
591
+ path,
592
+ depth: 1,
593
+ }));
594
+ const statusPromise =
595
+ repoPaths.length > 0
596
+ ? client
597
+ .gitReadBatch(
598
+ workspaceId,
599
+ sessionId,
600
+ {
601
+ requests: repoPaths.map((root) => ({
602
+ status: { path: root },
603
+ })),
604
+ },
605
+ { signal: refreshAbort.signal },
606
+ )
607
+ .then((batch) =>
608
+ batch.results.map((result, index) => ({
609
+ root: repoPaths[index] ?? "",
610
+ status: result.status,
611
+ })),
612
+ )
613
+ .catch(() => null)
614
+ : Promise.resolve([]);
615
+ // Root plus the bounded repository frontier share one API request, one
616
+ // direct holder, and one provider attach. The old per-directory fan-out
617
+ // paid that fixed control-plane cost up to nine times on every Files open.
618
+ const listedBatch = await client.fsListBatch(
497
619
  workspaceId,
498
620
  sessionId,
499
- { path: rootPath, depth: 1 },
621
+ { requests: listRequests },
500
622
  { signal: refreshAbort.signal },
501
623
  );
502
624
  if (refreshGenerationRef.current !== generation) return;
625
+ const listed = listedBatch.results[0];
626
+ if (!listed) throw new Error("Workspace list batch did not return the root result.");
503
627
  const children = (listed.root.children ?? []).map((node) => fsNodeToTree(node));
504
- // Merge rather than replace so an explicit refresh / cold→warm re-list folds
505
- // in new entries WITHOUT collapsing the dirs the user already expanded (and,
506
- // via the identity-preserving merge, WITHOUT remounting unchanged rows — the
507
- // no-flicker cold→warm reconcile). On a first (empty) load this is a plain set.
508
- setTree((prev) =>
509
- applyStatus(prev.length === 0 ? children : mergeRootChildren(prev, children)),
510
- );
511
- // Live data is now serving — flip the source off the capture snapshot.
628
+ // Paint the root and already-returned frontier in one commit so preloaded
629
+ // repository folders never flicker through a collapsed intermediate tree.
630
+ setTree((prev) => {
631
+ let hydrated = prev.length === 0 ? children : mergeRootChildren(prev, children);
632
+ for (const [index, path] of frontierPaths.entries()) {
633
+ const prefetched = listedBatch.results[index + 1];
634
+ if (!prefetched) continue;
635
+ hydrated = replaceChildren(
636
+ hydrated,
637
+ path,
638
+ (prefetched.root.children ?? []).map((node) => fsNodeToTree(node)),
639
+ );
640
+ }
641
+ return applyStatus(hydrated);
642
+ });
512
643
  sourceRef.current = "live";
513
644
  setSource("live");
645
+ setLoading(false);
646
+
647
+ const statuses = await statusPromise;
648
+ if (refreshGenerationRef.current !== generation) return;
649
+ if (statuses) {
650
+ const overlay = new Map<string, FileTreeStatus>();
651
+ for (const { root, status } of statuses) {
652
+ for (const file of status.files) {
653
+ const code = file.worktree ?? file.index;
654
+ const mapped = code ? GIT_STATUS_TO_TREE[code] : undefined;
655
+ if (mapped) overlay.set(qualifyStatusPath(root, file.path), mapped);
656
+ }
657
+ }
658
+ statusRef.current = overlay;
659
+ setGitSummary(summarizeStatuses(statuses));
660
+ } else {
661
+ statusRef.current = new Map();
662
+ setGitSummary(null);
663
+ }
664
+ setGitLoading(false);
665
+ setTree((prev) => applyStatus(prev));
514
666
  } catch (cause) {
515
667
  if (refreshGenerationRef.current !== generation) return;
516
668
  setError(cause instanceof Error ? cause : new Error(String(cause)));
517
669
  } finally {
518
670
  if (refreshGenerationRef.current === generation) setLoading(false);
671
+ if (refreshGenerationRef.current === generation) setGitLoading(false);
519
672
  if (refreshAbortRef.current === refreshAbort) refreshAbortRef.current = null;
520
673
  }
521
- }, [client, workspaceId, sessionId, rootPath, applyStatus]);
674
+ }, [client, workspaceId, sessionId, rootPath, repoPaths, applyStatus]);
522
675
 
523
676
  // Seed (or re-seed) the tree from a turn-end capture — the COLD/offline paint,
524
677
  // zero Channel-A calls. The tree index is workspace-relative (`treeIndex`); the
@@ -543,6 +696,15 @@ export function useSandboxFiles(
543
696
  );
544
697
  sourceRef.current = "capture";
545
698
  setSource("capture");
699
+ setGitSummary({
700
+ branch: manifest.repos.length === 1 ? (manifest.repos[0]?.head ?? null) : null,
701
+ isRepo: manifest.repos.length > 0,
702
+ repoCount: manifest.repos.length,
703
+ ahead: manifest.repos.reduce((sum, repo) => sum + repo.ahead, 0),
704
+ behind: manifest.repos.reduce((sum, repo) => sum + repo.behind, 0),
705
+ dirtyCount: manifest.files.length,
706
+ });
707
+ setGitLoading(false);
546
708
  setError(null);
547
709
  },
548
710
  [applyStatus],
@@ -695,20 +857,18 @@ export function useSandboxFiles(
695
857
  // of a parent that isn't loaded (collapsed/unmounted) is a no-op — there's
696
858
  // nothing visible to update, and it re-lists fresh when the user expands it.
697
859
  const reconcilePath = useCallback(
698
- async (path: string) => {
860
+ async (path: string, requestSignal?: AbortSignal) => {
699
861
  if (!sessionId) return;
700
862
  const identityGeneration = identityGenerationRef.current;
701
863
  const identitySignal = identityAbortRef.current.signal;
864
+ const signal = requestSignal
865
+ ? AbortSignal.any([identitySignal, requestSignal])
866
+ : identitySignal;
702
867
  // Skip parents that aren't currently loaded/expanded in the tree (nothing
703
868
  // visible to update; they re-list fresh on the next expand).
704
869
  if (!parentIsLoaded(treeRef.current, path)) return;
705
870
  try {
706
- const listed = await client.fsList(
707
- workspaceId,
708
- sessionId,
709
- { path, depth: 1 },
710
- { signal: identitySignal },
711
- );
871
+ const listed = await client.fsList(workspaceId, sessionId, { path, depth: 1 }, { signal });
712
872
  if (identityGenerationRef.current !== identityGeneration) return;
713
873
  const children = (listed.root.children ?? []).map((node) => fsNodeToTree(node));
714
874
  if (path === "") setTree((prev) => applyStatus(mergeRootChildren(prev, children)));
@@ -795,7 +955,12 @@ export function useSandboxFiles(
795
955
  // exist in a loaded parent (an editor SAVE to an existing file mutates no
796
956
  // tree shape — just content — so it needs no optimistic node, no reconcile).
797
957
  const exists = Boolean(findNodeByPath(tree, path));
798
- const node: FileTreeNode = { path, name: leafOf(path), kind: "file", size: content.length };
958
+ const node: FileTreeNode = {
959
+ path,
960
+ name: leafOf(path),
961
+ kind: "file",
962
+ size: content.length,
963
+ };
799
964
  return await runOptimistic(
800
965
  "write",
801
966
  (nodes) => (exists ? nodes : insertNode(nodes, parent, node)),
@@ -835,11 +1000,21 @@ export function useSandboxFiles(
835
1000
  async (path: string): Promise<void> => {
836
1001
  if (!sessionId) throw new Error("no session");
837
1002
  const parent = parentOf(path);
838
- const node: FileTreeNode = { path, name: leafOf(path), kind: "file", size: 0 };
1003
+ const node: FileTreeNode = {
1004
+ path,
1005
+ name: leafOf(path),
1006
+ kind: "file",
1007
+ size: 0,
1008
+ };
839
1009
  await runOptimistic(
840
1010
  "create file",
841
1011
  (nodes) => insertNode(nodes, parent, node),
842
- () => client.fsWrite(workspaceId, sessionId, { path, content: "", overwrite: false }),
1012
+ () =>
1013
+ client.fsWrite(workspaceId, sessionId, {
1014
+ path,
1015
+ content: "",
1016
+ overwrite: false,
1017
+ }),
843
1018
  [parent],
844
1019
  );
845
1020
  },
@@ -852,7 +1027,12 @@ export function useSandboxFiles(
852
1027
  const parent = parentOf(path);
853
1028
  // A freshly-created dir is empty + expanded: children:[] (not undefined, so
854
1029
  // it doesn't show the lazy-expand marker over a dir we KNOW is empty).
855
- const node: FileTreeNode = { path, name: leafOf(path), kind: "dir", children: [] };
1030
+ const node: FileTreeNode = {
1031
+ path,
1032
+ name: leafOf(path),
1033
+ kind: "dir",
1034
+ children: [],
1035
+ };
856
1036
  await runOptimistic(
857
1037
  "create folder",
858
1038
  (nodes) => insertNode(nodes, parent, node),
@@ -904,9 +1084,10 @@ export function useSandboxFiles(
904
1084
 
905
1085
  // Initial paint + reset on identity change. Source selection:
906
1086
  // • any box WITH a capture → paint instantly from the durable capture index;
907
- // a warm/draining box then reconciles live in place.
1087
+ // a holder-confirmed warm box then reconciles live in place.
908
1088
  // • if that live reconciliation fails, keep the capture visible and read-only.
909
- // • cold box with NO capture best-effort live list (status quo — never worse).
1089
+ // • legacy callers that omit liveness and have no capture retain their live
1090
+ // fallback; an explicitly cold/draining lease never wakes from a passive mount.
910
1091
  // Key the seed on the capture's REVISION (a primitive), not the manifest object
911
1092
  // — a consumer passing a fresh object each render (or a new revision) must not
912
1093
  // spin the effect. The latest manifest is read from a ref at run time.
@@ -921,6 +1102,8 @@ export function useSandboxFiles(
921
1102
  refreshAbortRef.current?.abort();
922
1103
  refreshAbortRef.current = null;
923
1104
  refreshGenerationRef.current += 1;
1105
+ eventReadAbortRef.current.abort();
1106
+ eventReadAbortRef.current = new AbortController();
924
1107
  const identityChanged = previousIdentityRef.current !== identityKey;
925
1108
  previousIdentityRef.current = identityKey;
926
1109
  if (identityChanged || !enabled) {
@@ -930,6 +1113,8 @@ export function useSandboxFiles(
930
1113
  lastSeqRef.current = 0;
931
1114
  pendingParentsRef.current = new Set();
932
1115
  pendingGitRef.current = false;
1116
+ pendingAgentToolRef.current = false;
1117
+ lastCommandRefreshAtRef.current = 0;
933
1118
  if (debounceRef.current) {
934
1119
  clearTimeout(debounceRef.current);
935
1120
  debounceRef.current = null;
@@ -944,6 +1129,9 @@ export function useSandboxFiles(
944
1129
  setSource(null);
945
1130
  setLoading(false);
946
1131
  setError(null);
1132
+ setGitSummary(null);
1133
+ setGitLoading(false);
1134
+ setContentRevision(0);
947
1135
  }
948
1136
  if (!enabled) {
949
1137
  return;
@@ -952,43 +1140,76 @@ export function useSandboxFiles(
952
1140
  if (currentCapture && (identityChanged || !isLive || sourceRef.current !== "live")) {
953
1141
  seedFromCapture(currentCapture);
954
1142
  }
955
- if (isLive || !currentCapture) {
1143
+ if (!active) {
1144
+ if (!active) {
1145
+ setLoading(false);
1146
+ setGitLoading(false);
1147
+ }
1148
+ return;
1149
+ }
1150
+ if (isLive || (!hasLiveIoFence && !currentCapture)) {
956
1151
  void refresh();
957
1152
  }
958
1153
  return () => {
959
1154
  refreshAbortRef.current?.abort();
960
1155
  refreshAbortRef.current = null;
961
1156
  refreshGenerationRef.current += 1;
1157
+ eventReadAbortRef.current.abort();
962
1158
  };
963
- }, [enabled, isLive, captureRevision, identityKey, refresh, seedFromCapture]);
1159
+ }, [
1160
+ enabled,
1161
+ active,
1162
+ isLive,
1163
+ hasLiveIoFence,
1164
+ captureRevision,
1165
+ identityKey,
1166
+ refresh,
1167
+ seedFromCapture,
1168
+ ]);
964
1169
 
965
1170
  // Re-pull JUST the git-status overlay and re-tint the existing tree in place —
966
1171
  // no fs re-list, no collapse. This is all a `git.changed` (commit/stage/checkout)
967
1172
  // needs: the tree SHAPE is unchanged, only the tints move.
968
- const refreshGitOverlay = useCallback(async () => {
969
- if (!sessionId) return;
970
- const identityGeneration = identityGenerationRef.current;
971
- const identitySignal = identityAbortRef.current.signal;
972
- try {
973
- const status = await client.gitStatus(
974
- workspaceId,
975
- sessionId,
976
- { path: rootPath },
977
- { signal: identitySignal },
978
- );
979
- if (identityGenerationRef.current !== identityGeneration) return;
980
- const overlay = new Map<string, FileTreeStatus>();
981
- for (const file of status.files) {
982
- const code = file.worktree ?? file.index;
983
- const mapped = code ? GIT_STATUS_TO_TREE[code] : undefined;
984
- if (mapped) overlay.set(file.path, mapped);
1173
+ const refreshGitOverlay = useCallback(
1174
+ async (requestSignal?: AbortSignal) => {
1175
+ if (!sessionId) return;
1176
+ const identityGeneration = identityGenerationRef.current;
1177
+ const identitySignal = identityAbortRef.current.signal;
1178
+ const signal = requestSignal
1179
+ ? AbortSignal.any([identitySignal, requestSignal])
1180
+ : identitySignal;
1181
+ setGitLoading(true);
1182
+ try {
1183
+ const batch = await client.gitReadBatch(
1184
+ workspaceId,
1185
+ sessionId,
1186
+ { requests: repoPaths.map((root) => ({ status: { path: root } })) },
1187
+ { signal },
1188
+ );
1189
+ const statuses = batch.results.map((result, index) => ({
1190
+ root: repoPaths[index] ?? "",
1191
+ status: result.status,
1192
+ }));
1193
+ if (identityGenerationRef.current !== identityGeneration) return;
1194
+ const overlay = new Map<string, FileTreeStatus>();
1195
+ for (const { root, status } of statuses) {
1196
+ for (const file of status.files) {
1197
+ const code = file.worktree ?? file.index;
1198
+ const mapped = code ? GIT_STATUS_TO_TREE[code] : undefined;
1199
+ if (mapped) overlay.set(qualifyStatusPath(root, file.path), mapped);
1200
+ }
1201
+ }
1202
+ statusRef.current = overlay;
1203
+ setGitSummary(summarizeStatuses(statuses));
1204
+ setTree((prev) => applyStatus(prev));
1205
+ } catch {
1206
+ /* a non-repo box has no overlay — leave the tree untinted */
1207
+ } finally {
1208
+ if (identityGenerationRef.current === identityGeneration) setGitLoading(false);
985
1209
  }
986
- statusRef.current = overlay;
987
- setTree((prev) => applyStatus(prev));
988
- } catch {
989
- /* a non-repo box has no overlay — leave the tree untinted */
990
- }
991
- }, [client, workspaceId, sessionId, rootPath, applyStatus]);
1210
+ },
1211
+ [client, workspaceId, sessionId, repoPaths, applyStatus],
1212
+ );
992
1213
 
993
1214
  // Auto-reconcile on fs/git change notifications — TARGETED, never a root
994
1215
  // collapse-reload, and de-duped against our OWN mutations.
@@ -1004,7 +1225,25 @@ export function useSandboxFiles(
1004
1225
 
1005
1226
  useEffect(() => {
1006
1227
  if (!enabled || !events) return;
1228
+ // An inactive tab or holderless draining/cold lease must not replay the
1229
+ // historical log into live provider reads. The capture already includes
1230
+ // those events; a later warm transition performs one authoritative refresh.
1231
+ if (!active || !acceptsEventReads) {
1232
+ for (const event of events) {
1233
+ if (event.sequence > lastSeqRef.current) lastSeqRef.current = event.sequence;
1234
+ }
1235
+ if (debounceRef.current) {
1236
+ clearTimeout(debounceRef.current);
1237
+ debounceRef.current = null;
1238
+ }
1239
+ pendingParentsRef.current = new Set();
1240
+ pendingGitRef.current = false;
1241
+ pendingAgentToolRef.current = false;
1242
+ return;
1243
+ }
1007
1244
  let sawNew = false;
1245
+ let contentChanged = false;
1246
+ let sawCommandDelta = false;
1008
1247
  for (const event of events) {
1009
1248
  if (event.sequence <= lastSeqRef.current) continue;
1010
1249
  if (event.type === "fs.changed") {
@@ -1015,6 +1254,7 @@ export function useSandboxFiles(
1015
1254
  if (payload.source === "write") continue;
1016
1255
  if (typeof payload.revision === "number" && ownRevisionsRef.current.has(payload.revision))
1017
1256
  continue;
1257
+ contentChanged = true;
1018
1258
  for (const change of payload.changes ?? []) {
1019
1259
  pendingParentsRef.current.add(parentOf(change.path));
1020
1260
  if (change.oldPath) pendingParentsRef.current.add(parentOf(change.oldPath));
@@ -1033,39 +1273,90 @@ export function useSandboxFiles(
1033
1273
  )
1034
1274
  continue;
1035
1275
  pendingGitRef.current = true;
1276
+ } else if (event.type === "agent.toolCall.output") {
1277
+ // Agent shell/apply-patch writes do not necessarily pass through the
1278
+ // structured FS service, so they may have no fs.changed notification.
1279
+ // A completed tool is a bounded invalidation point: refresh only the
1280
+ // currently-loaded directory frontier, never poll and never traverse a
1281
+ // collapsed subtree.
1282
+ sawNew = true;
1283
+ contentChanged = true;
1284
+ pendingAgentToolRef.current = true;
1285
+ // Shell/apply-patch writes bypass the structured Git service too, so no
1286
+ // git.changed event is guaranteed. Refresh the summary/tints alongside
1287
+ // the visible tree frontier; both remote reads can run concurrently.
1288
+ pendingGitRef.current = true;
1289
+ } else if (event.type === "sandbox.command.output.delta") {
1290
+ // Ordinary shell writes can bypass Channel-A and therefore emit no
1291
+ // fs.changed/git.changed event until the command completes. Reconcile
1292
+ // the visible frontier at most once per second while output streams so
1293
+ // Files and Changes stay useful without turning the dock into polling.
1294
+ sawNew = true;
1295
+ contentChanged = true;
1296
+ sawCommandDelta = true;
1297
+ pendingAgentToolRef.current = true;
1298
+ pendingGitRef.current = true;
1036
1299
  }
1037
1300
  }
1038
1301
  // Advance the high-water mark past everything we've folded.
1039
1302
  for (const event of events)
1040
1303
  if (event.sequence > lastSeqRef.current) lastSeqRef.current = event.sequence;
1041
1304
  if (!sawNew) return;
1305
+ if (contentChanged && !sawCommandDelta) setContentRevision((revision) => revision + 1);
1042
1306
 
1043
1307
  if (debounceRef.current) clearTimeout(debounceRef.current);
1044
1308
  const identityGeneration = identityGenerationRef.current;
1309
+ const delay = sawCommandDelta
1310
+ ? Math.max(0, 1_000 - (Date.now() - lastCommandRefreshAtRef.current))
1311
+ : 150;
1045
1312
  debounceRef.current = setTimeout(() => {
1046
1313
  debounceRef.current = null;
1047
1314
  if (identityGenerationRef.current !== identityGeneration) return;
1315
+ if (!acceptsEventReadsRef.current) {
1316
+ pendingParentsRef.current = new Set();
1317
+ pendingGitRef.current = false;
1318
+ pendingAgentToolRef.current = false;
1319
+ return;
1320
+ }
1321
+ if (sawCommandDelta) {
1322
+ lastCommandRefreshAtRef.current = Date.now();
1323
+ setContentRevision((revision) => revision + 1);
1324
+ }
1048
1325
  const parents = pendingParentsRef.current;
1049
1326
  pendingParentsRef.current = new Set();
1050
1327
  const wantGit = pendingGitRef.current;
1051
1328
  pendingGitRef.current = false;
1052
- // Reconcile the changed directories (in place). A git change re-tints first
1053
- // so the freshly-listed nodes get the correct overlay.
1329
+ const wantAgentTool = pendingAgentToolRef.current;
1330
+ pendingAgentToolRef.current = false;
1331
+ // Reconcile the changed directories and Git overlay concurrently. Both are
1332
+ // independent remote reads; applyStatus makes their completion order safe.
1333
+ const eventReadSignal = eventReadAbortRef.current.signal;
1054
1334
  void (async () => {
1055
- if (wantGit) await refreshGitOverlay();
1056
- if (identityGenerationRef.current !== identityGeneration) return;
1057
- for (const parent of parents) {
1058
- if (identityGenerationRef.current !== identityGeneration) return;
1059
- await reconcilePath(parent);
1060
- }
1335
+ const paths = wantAgentTool ? loadedDirectoryPaths(treeRef.current) : [...parents];
1336
+ await Promise.all([
1337
+ wantGit ? refreshGitOverlay(eventReadSignal) : Promise.resolve(),
1338
+ Promise.all(paths.map((path) => reconcilePath(path, eventReadSignal))),
1339
+ ]);
1061
1340
  })();
1062
- }, 150);
1063
- }, [enabled, events, reconcilePath, refreshGitOverlay]);
1341
+ }, delay);
1342
+ }, [enabled, active, acceptsEventReads, events, reconcilePath, refreshGitOverlay]);
1343
+
1344
+ useEffect(() => {
1345
+ if (active && acceptsEventReads) return;
1346
+ if (debounceRef.current) {
1347
+ clearTimeout(debounceRef.current);
1348
+ debounceRef.current = null;
1349
+ }
1350
+ pendingParentsRef.current = new Set();
1351
+ pendingGitRef.current = false;
1352
+ pendingAgentToolRef.current = false;
1353
+ }, [active, acceptsEventReads]);
1064
1354
 
1065
1355
  useEffect(
1066
1356
  () => () => {
1067
1357
  refreshAbortRef.current?.abort();
1068
1358
  identityAbortRef.current.abort();
1359
+ eventReadAbortRef.current.abort();
1069
1360
  identityGenerationRef.current += 1;
1070
1361
  if (debounceRef.current) clearTimeout(debounceRef.current);
1071
1362
  },
@@ -1084,8 +1375,11 @@ export function useSandboxFiles(
1084
1375
  deleteEntry,
1085
1376
  moveEntry,
1086
1377
  refresh,
1378
+ contentRevision: identityMatches ? contentRevision : 0,
1087
1379
  source: identityMatches ? source : null,
1088
1380
  capturedAt: identityMatches && source === "capture" ? (capture?.capturedAt ?? null) : null,
1381
+ gitSummary: identityMatches ? gitSummary : null,
1382
+ gitLoading: identityMatches && gitLoading,
1089
1383
  loading: identityMatches && loading,
1090
1384
  error: identityMatches ? error : null,
1091
1385
  };