@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
@@ -1,9 +1,11 @@
1
1
  import { FileCode2Icon, FileWarningIcon, LoaderCircleIcon } from "lucide-react";
2
2
  import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
3
+ import { Group, Panel, Separator } from "react-resizable-panels";
3
4
  import { cn } from "../lib/cn";
4
5
  import { useThemeType } from "../lib/use-theme-type";
5
6
  import {
6
7
  CapturedFileUnavailableError,
8
+ type SandboxFilesGitSummary,
7
9
  type UseSandboxFilesResult,
8
10
  } from "../hooks/use-sandbox-files";
9
11
  import type { UseSandboxGitResult } from "../hooks/use-sandbox-git";
@@ -14,8 +16,8 @@ import { PierreFile } from "./pierre-file";
14
16
  export type SandboxFilesProps = {
15
17
  /** From `useSandboxFiles(...)`. */
16
18
  files: UseSandboxFilesResult;
17
- /** From `useSandboxGit(...)` drives the branch/dirty header only. */
18
- git: UseSandboxGitResult;
19
+ /** Compatibility fallback for callers that have not yet adopted `files.gitSummary`. */
20
+ git?: UseSandboxGitResult | undefined;
19
21
  /** @deprecated Diffs live in the dedicated Changes tab now; accepted for
20
22
  * source-compat but unused (the Files surface is a browser + viewer). */
21
23
  stagedGit?: UseSandboxGitResult | undefined;
@@ -111,15 +113,29 @@ export function SandboxFiles({
111
113
  const [wide, setWide] = useState(false);
112
114
  useEffect(() => {
113
115
  const el = rootRef.current;
114
- if (!el || typeof ResizeObserver === "undefined") return;
115
- const observer = new ResizeObserver((entries) => {
116
- const w = entries[0]?.contentRect.width ?? 0;
117
- setWide(w >= 720);
118
- });
116
+ if (!el) return;
117
+ const update = () => setWide(el.getBoundingClientRect().width >= 720);
118
+ update();
119
+ if (typeof ResizeObserver === "undefined") return;
120
+ const observer = new ResizeObserver(update);
119
121
  observer.observe(el);
120
- return () => observer.disconnect();
122
+ // WorkspaceDock maximization changes its persistent surface from a sized
123
+ // panel child to `fixed inset-0`. Chromium can retain the child's observer
124
+ // size across that containing-block change even though its visible bounding
125
+ // box is now viewport-wide. Follow the surface mode as a deterministic
126
+ // second signal so the tree/viewer orientation matches what is painted.
127
+ const surface = el.closest("[data-workspace-surface]");
128
+ const surfaceObserver =
129
+ surface && typeof MutationObserver !== "undefined" ? new MutationObserver(update) : null;
130
+ surfaceObserver?.observe(surface!, {
131
+ attributes: true,
132
+ attributeFilter: ["class", "style"],
133
+ });
134
+ return () => {
135
+ observer.disconnect();
136
+ surfaceObserver?.disconnect();
137
+ };
121
138
  }, []);
122
-
123
139
  // Resolve the effective viewer theme from the host palette (the `data-og-theme`
124
140
  // attribute the demo/app sets), defaulting to dark, unless the caller forced one.
125
141
  const resolvedTheme = useThemeType(themeType);
@@ -128,7 +144,11 @@ export function SandboxFiles({
128
144
  // writable). Nothing is auto-selected — the pane waits for a tree click, so the
129
145
  // Files tab opens as a calm browser, not a diff.
130
146
  const viewPath = selected;
131
- const fileView = useFileView(viewPath, files.readFile, viewReloadRevision);
147
+ const fileView = useFileView(
148
+ viewPath,
149
+ files.readFile,
150
+ (files.contentRevision ?? 0) + viewReloadRevision,
151
+ );
132
152
 
133
153
  // Selecting a (different) file always returns to View — never drop the user into
134
154
  // an editor whose buffer belongs to the previously-selected path. Manual
@@ -165,19 +185,6 @@ export function SandboxFiles({
165
185
  (!liveWorkspaceReady || files.loading) &&
166
186
  captureFileUnavailable !== null;
167
187
 
168
- if (!fileSystemAvailable) {
169
- return (
170
- <Notice
171
- className={className}
172
- icon={<FileWarningIcon className="size-5" aria-hidden />}
173
- title="Files unavailable"
174
- announce="alert"
175
- >
176
- This sandbox does not expose a file system.
177
- </Notice>
178
- );
179
- }
180
-
181
188
  if (workspaceResting || workspaceWaking) {
182
189
  return (
183
190
  <div className={cn("h-full", className)} data-opengeni-workspace-resting>
@@ -208,19 +215,51 @@ export function SandboxFiles({
208
215
  );
209
216
  }
210
217
 
218
+ if (!fileSystemAvailable) {
219
+ return (
220
+ <Notice
221
+ className={className}
222
+ icon={<FileWarningIcon className="size-5" aria-hidden />}
223
+ title="Files unavailable"
224
+ announce="alert"
225
+ >
226
+ This sandbox does not expose a file system.
227
+ </Notice>
228
+ );
229
+ }
230
+
211
231
  return (
212
232
  <div ref={rootRef} className={cn("flex h-full min-h-0 min-w-0 flex-col", className)}>
213
233
  {/* Branch + dirty header (context; NOT the changed-files list). */}
214
- <GitHeader git={git} dirtyCount={git.diff.length} />
234
+ <GitHeader
235
+ loading={
236
+ Boolean(files.loading || files.gitLoading || files.source === null || git?.loading) &&
237
+ files.gitSummary === null
238
+ }
239
+ git={
240
+ files.gitSummary ?? {
241
+ branch: git?.branch ?? null,
242
+ isRepo: git?.isRepo ?? false,
243
+ repoCount: git?.repoCount ?? 0,
244
+ ahead: git?.ahead ?? 0,
245
+ behind: git?.behind ?? 0,
246
+ dirtyCount: git?.diff.length ?? 0,
247
+ }
248
+ }
249
+ />
215
250
 
216
- <div className={cn("flex min-h-0 flex-1", wide ? "flex-row" : "flex-col")}>
251
+ <Group
252
+ orientation={wide ? "horizontal" : "vertical"}
253
+ className={cn("min-h-0 flex-1", wide ? "flex-row" : "flex-col")}
254
+ >
217
255
  {/* Tree pane: the full lazy file tree. A fixed left column when wide, a top
218
256
  band when narrow. */}
219
- <div
220
- className={cn(
221
- "flex min-h-0 flex-col",
222
- wide ? "w-[280px] shrink-0 border-r border-og-border" : "flex-1",
223
- )}
257
+ <Panel
258
+ id="sandbox-files-tree"
259
+ defaultSize={wide ? "280px" : "40%"}
260
+ minSize={wide ? "180px" : "120px"}
261
+ maxSize={wide ? "60%" : "70%"}
262
+ className="flex min-h-0 min-w-0 flex-col"
224
263
  >
225
264
  <FileBrowser
226
265
  result={files}
@@ -230,16 +269,26 @@ export function SandboxFiles({
230
269
  emptyState="This directory is empty"
231
270
  className="min-w-0 flex-1"
232
271
  />
233
- </div>
272
+ </Panel>
273
+
274
+ <Separator
275
+ aria-label="Resize file tree"
276
+ className={cn(
277
+ "group relative z-10 shrink-0 cursor-col-resize outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent",
278
+ "w-px bg-og-border after:absolute after:inset-y-0 after:left-1/2 after:w-3 after:-translate-x-1/2",
279
+ "hover:bg-og-accent focus-visible:bg-og-accent data-[separator-state=dragging]:bg-og-accent",
280
+ !wide &&
281
+ "h-px w-full cursor-row-resize after:inset-x-0 after:inset-y-auto after:top-1/2 after:h-3 after:w-full after:-translate-x-0 after:-translate-y-1/2",
282
+ )}
283
+ />
234
284
 
235
285
  {/* Viewer pane: the selected file's contents (read-only) or the editor.
236
286
  Fills the remaining width when side-by-side, sits below the tree when
237
287
  stacked. */}
238
- <div
239
- className={cn(
240
- "flex min-h-0 min-w-0 flex-col",
241
- wide ? "flex-1" : "flex-[1.4] border-t border-og-border",
242
- )}
288
+ <Panel
289
+ id="sandbox-files-viewer"
290
+ minSize={wide ? "240px" : "160px"}
291
+ className={cn("flex min-h-0 min-w-0 flex-col", !wide && "border-t border-og-border")}
243
292
  >
244
293
  <div className="flex shrink-0 items-center justify-between gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1">
245
294
  <span
@@ -270,8 +319,8 @@ export function SandboxFiles({
270
319
  path={viewPath}
271
320
  initialContents={fileView.content}
272
321
  themeType={resolvedTheme}
273
- onSave={(contents) =>
274
- files.writeFile(viewPath, contents, { expectedContent: fileView.content! })
322
+ onSave={(contents, expectedContents) =>
323
+ files.writeFile(viewPath, contents, { expectedContent: expectedContents })
275
324
  }
276
325
  onOverwrite={(contents) => files.writeFile(viewPath, contents, { force: true })}
277
326
  onReload={() => setViewReloadRevision((revision) => revision + 1)}
@@ -371,8 +420,8 @@ export function SandboxFiles({
371
420
  </Notice>
372
421
  )}
373
422
  </div>
374
- </div>
375
- </div>
423
+ </Panel>
424
+ </Group>
376
425
  </div>
377
426
  );
378
427
  }
@@ -382,15 +431,30 @@ function WakeButton({ children, onClick }: { children: ReactNode; onClick: () =>
382
431
  <button
383
432
  type="button"
384
433
  onClick={onClick}
385
- className="mt-1 inline-flex min-h-11 items-center justify-center rounded-og-md bg-og-accent-deep px-3 py-2 text-og-sm font-medium text-og-accent-fg shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg"
434
+ className="mt-1 inline-flex min-h-11 items-center justify-center rounded-og-md bg-og-accent-deep px-3 py-2 text-og-sm font-medium text-og-accent-fg shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg"
386
435
  >
387
436
  {children}
388
437
  </button>
389
438
  );
390
439
  }
391
440
 
392
- function GitHeader({ git, dirtyCount }: { git: UseSandboxGitResult; dirtyCount: number }) {
393
- const dirty = dirtyCount > 0;
441
+ function GitHeader({ git, loading }: { git: SandboxFilesGitSummary; loading: boolean }) {
442
+ if (loading) {
443
+ return (
444
+ <div
445
+ className="flex shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-sm text-og-fg-muted"
446
+ role="status"
447
+ aria-live="polite"
448
+ >
449
+ <LoaderCircleIcon
450
+ className="size-3.5 shrink-0 animate-spin motion-reduce:animate-none"
451
+ aria-hidden
452
+ />
453
+ <span>Loading repository…</span>
454
+ </div>
455
+ );
456
+ }
457
+ const dirty = git.dirtyCount > 0;
394
458
  return (
395
459
  <div className="flex shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-sm">
396
460
  <span
@@ -401,7 +465,7 @@ function GitHeader({ git, dirtyCount }: { git: UseSandboxGitResult; dirtyCount:
401
465
  )}
402
466
  />
403
467
  <span className="sr-only">
404
- {dirty ? `Working tree has ${dirtyCount} changed files` : "Working tree clean"}
468
+ {dirty ? `Uncommitted changes in ${git.dirtyCount} files` : "No uncommitted changes"}
405
469
  </span>
406
470
  <span className="truncate font-og-mono text-og-fg">
407
471
  {git.repoCount > 1
@@ -420,7 +484,7 @@ function GitHeader({ git, dirtyCount }: { git: UseSandboxGitResult; dirtyCount:
420
484
  data-contrast-audited
421
485
  className="ml-auto shrink-0 text-og-xs text-og-fg-subtle"
422
486
  >
423
- {dirtyCount} changed
487
+ {git.dirtyCount} changed
424
488
  </span>
425
489
  )}
426
490
  </div>
@@ -444,7 +508,7 @@ function Segmented({
444
508
  type="button"
445
509
  onClick={() => onChange(opt.value)}
446
510
  className={cn(
447
- "min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
511
+ "min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
448
512
  opt.value === value
449
513
  ? "bg-og-accent-soft text-og-fg"
450
514
  : "text-og-fg-subtle hover:text-og-fg",
@@ -480,6 +544,7 @@ function useFileView(
480
544
  readFile: UseSandboxFilesResult["readFile"],
481
545
  reloadRevision = 0,
482
546
  ): FileViewState {
547
+ const previousPathRef = useRef<string | null>(null);
483
548
  const [state, setState] = useState<FileViewState>({
484
549
  content: null,
485
550
  isBinary: false,
@@ -490,6 +555,7 @@ function useFileView(
490
555
  });
491
556
  useEffect(() => {
492
557
  if (!path) {
558
+ previousPathRef.current = null;
493
559
  setState({
494
560
  content: null,
495
561
  isBinary: false,
@@ -500,16 +566,25 @@ function useFileView(
500
566
  });
501
567
  return;
502
568
  }
569
+ const pathChanged = previousPathRef.current !== path;
570
+ previousPathRef.current = path;
503
571
  let cancelled = false;
504
572
  const abort = new AbortController();
505
- setState({
506
- content: null,
507
- isBinary: false,
508
- truncated: false,
509
- sizeBytes: null,
510
- loading: true,
511
- error: null,
512
- });
573
+ if (pathChanged) {
574
+ setState({
575
+ content: null,
576
+ isBinary: false,
577
+ truncated: false,
578
+ sizeBytes: null,
579
+ loading: true,
580
+ error: null,
581
+ });
582
+ } else {
583
+ // Keep the last successful preview visible while a remote Modal read
584
+ // revalidates it. Replacing useful content with a multi-second spinner
585
+ // makes a live workspace feel slower and hides the state being refreshed.
586
+ setState((previous) => ({ ...previous, error: null }));
587
+ }
513
588
  void readFile(path, { signal: abort.signal })
514
589
  .then((res) => {
515
590
  if (cancelled) return;
@@ -529,14 +604,19 @@ function useFileView(
529
604
  })
530
605
  .catch((cause) => {
531
606
  if (cancelled) return;
532
- setState({
533
- content: null,
534
- isBinary: false,
535
- truncated: false,
536
- sizeBytes: null,
537
- loading: false,
538
- error: cause instanceof Error ? cause : new Error(String(cause)),
539
- });
607
+ const error = cause instanceof Error ? cause : new Error(String(cause));
608
+ setState((previous) =>
609
+ pathChanged
610
+ ? {
611
+ content: null,
612
+ isBinary: false,
613
+ truncated: false,
614
+ sizeBytes: null,
615
+ loading: false,
616
+ error,
617
+ }
618
+ : { ...previous, loading: false, error },
619
+ );
540
620
  });
541
621
  return () => {
542
622
  cancelled = true;
@@ -1,9 +1,11 @@
1
1
  import type { TerminalCapability } from "@opengeni/sdk";
2
2
  import { type ReactNode, useEffect, useRef, useState } from "react";
3
3
  import { cn } from "../lib/cn";
4
- import { useTerminalStream } from "../hooks/use-terminal-stream";
4
+ import { type TerminalStreamStatus, useTerminalStream } from "../hooks/use-terminal-stream";
5
5
  import type { UseSandboxTerminalResult } from "../hooks/use-sandbox-terminal";
6
6
  import { resolveTerminalFont, xtermThemeFromTokens } from "../lib/xterm-theme";
7
+ import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
8
+ import { terminalCanAcquirePty } from "../lib/terminal-capability";
7
9
  import { attachRenderer, type RendererLoaders, type RendererTier } from "../lib/xterm-renderer";
8
10
 
9
11
  /**
@@ -60,7 +62,7 @@ export type SandboxTerminalProps = {
60
62
  fontSize?: number | undefined;
61
63
  /**
62
64
  * Lease liveness (`cold` | `warming` | `warm` | `draining`). Drives the
63
- * boot-in-terminal status lines: once the user has focused the terminal and
65
+ * boot-in-terminal status lines: once the user has engaged the terminal and
64
66
  * the box is not yet warm, a styled "● waking machine — Ns…" line is rendered
65
67
  * INSIDE xterm (no overlay/spinner) until the live PTY connects.
66
68
  */
@@ -110,6 +112,51 @@ type XtermLike = {
110
112
  };
111
113
  type FitAddonLike = { fit: () => void };
112
114
 
115
+ export type TerminalSurfaceState =
116
+ | "loading"
117
+ | "connecting"
118
+ | "error"
119
+ | "waking"
120
+ | "output-only"
121
+ | "interactive";
122
+
123
+ /**
124
+ * Stable semantic state for hosts and browser acceptance. The xterm helper
125
+ * textarea exists before the live PTY socket is ready, so its presence alone
126
+ * must never be treated as proof that keystrokes can reach the sandbox.
127
+ */
128
+ export function terminalSurfaceState(input: {
129
+ ready: boolean;
130
+ acceptsInput: boolean;
131
+ inputPending: boolean;
132
+ ptyStatus: TerminalStreamStatus;
133
+ booting: boolean;
134
+ }): TerminalSurfaceState {
135
+ if (!input.ready) return "loading";
136
+ if (input.acceptsInput) return "interactive";
137
+ if (input.inputPending) return "connecting";
138
+ if (input.ptyStatus === "connecting") return "connecting";
139
+ if (input.ptyStatus === "error") return "error";
140
+ if (input.booting) return "waking";
141
+ return "output-only";
142
+ }
143
+
144
+ export function subscribeTerminalInput(
145
+ term: Pick<XtermLike, "onData"> | null,
146
+ sink: ((data: string) => void) | null,
147
+ enabled: boolean,
148
+ ): { dispose: () => void } | null {
149
+ if (!enabled || !term || !sink) return null;
150
+ return term.onData(sink);
151
+ }
152
+
153
+ /** Clear the transient wake line and put the first live prompt at cell 1,1.
154
+ * `Terminal.clear()` removes scrollback but deliberately preserves the cursor,
155
+ * which lets the first PTY frame append to the old wake message. */
156
+ export function clearTerminalBootStatus(term: Pick<XtermLike, "write">): void {
157
+ term.write("\r\x1b[2K\x1b[2J\x1b[H");
158
+ }
159
+
113
160
  /** Debug seam (dev/evidence only, never a typed public API): a global hook the
114
161
  * screenshot harness reads to probe the settled renderer tier + resolved font
115
162
  * without mounting a real WebGL context in unit tests. */
@@ -186,41 +233,61 @@ export function SandboxTerminal({
186
233
  const wroteFirehoseRef = useRef(false);
187
234
  const bootActiveRef = useRef(false);
188
235
  const [ready, setReady] = useState(false);
236
+ const [inputReady, setInputReady] = useState(false);
189
237
  const [activated, setActivated] = useState(false);
190
238
 
191
239
  // ── Interactive transport switch ─────────────────────────────────────────────
192
- const ptyWs = terminalCapability?.transport === "pty-ws" && Boolean(terminalCapability?.url);
193
- // Output frames buffer here until xterm has mounted; the write effect drains it.
194
- const ptyOutputQueueRef = useRef<string[]>([]);
240
+ const ptyDescriptor = terminalCapability?.transport === "pty-ws";
241
+ const ptyAttachable = terminalCanAcquirePty(terminalCapability);
195
242
  const {
196
243
  status: ptyStatus,
197
244
  write: ptyWrite,
198
245
  resize: resizePty,
199
246
  connected: ptyConnected,
200
247
  } = useTerminalStream({
201
- capability: ptyWs
202
- ? {
203
- transport: terminalCapability!.transport,
204
- url: terminalCapability!.url,
205
- token: terminalCapability!.token,
206
- }
207
- : null,
248
+ // Connect only after xterm exists. This makes the renderer the sole output
249
+ // owner from the first websocket frame and removes an otherwise unbounded
250
+ // pre-mount output queue during large command bursts.
251
+ capability:
252
+ ptyAttachable && ready
253
+ ? {
254
+ transport: "pty-ws",
255
+ url: ptyDescriptor ? terminalCapability!.url : null,
256
+ token: ptyDescriptor ? terminalCapability!.token : null,
257
+ expiresAt: ptyDescriptor ? terminalCapability!.expiresAt : null,
258
+ }
259
+ : null,
208
260
  onOutput: (data) => {
209
- const term = termRef.current;
210
- if (term) term.write(data);
211
- else ptyOutputQueueRef.current.push(data);
261
+ termRef.current?.write(data);
212
262
  },
213
263
  });
214
264
 
215
- // PTY mode = a live ttyd socket drives the screen. Firehose mode = the
216
- // read-only projection (or the legacy HTTP-write fallback).
217
- const ptyMode = ptyWs && ptyStatus !== "closed";
218
- const interactive = !readOnly && (ptyMode || result.write !== null);
265
+ // A pty-ws capability exclusively owns the screen even while connecting or
266
+ // after a visible connection failure; never silently switch one terminal UI
267
+ // between two PTYs. Input is enabled during the handshake because the stream
268
+ // hook buffers it strictly and flushes only after ttyd auth succeeds.
269
+ const ptyMode = ptyDescriptor;
270
+ // Keep xterm stdin live for an attachable descriptor even before its URL has
271
+ // arrived. Focus/click triggers the grant; any immediately following input is
272
+ // bounded in the stream hook instead of disappearing in that HTTP interval.
273
+ const ptyCapturesInput = ptyAttachable && ptyStatus !== "error";
274
+ const interactive = !readOnly && (ptyAttachable ? ptyCapturesInput : result.write !== null);
275
+ // Public "interactive" means the mounted input subscription can reach a live
276
+ // PTY now. Before that point xterm still captures into the bounded queue, but
277
+ // acceptance and embedders see an explicit connecting state.
278
+ const acceptsInput = inputReady && (!ptyAttachable || ptyConnected);
279
+ const inputPending = inputReady && ptyAttachable && !ptyConnected && activated;
280
+ let terminalModeLabel: string | null;
281
+ if (readOnly) terminalModeLabel = "read-only";
282
+ else if (!ptyAttachable) terminalModeLabel = acceptsInput ? null : "output only";
283
+ else if (ptyStatus === "error") terminalModeLabel = "connection error";
284
+ else if (acceptsInput) terminalModeLabel = null;
285
+ else terminalModeLabel = activated ? "connecting" : "connect on input";
219
286
 
220
- // Boot-in-terminal: after the user focuses a not-yet-warm box, show styled
287
+ // Boot-in-terminal: after the user engages a not-yet-warm box, show styled
221
288
  // status lines INSIDE xterm instead of an overlay — but only when there is no
222
289
  // firehose transcript to show yet (otherwise the projected output is shown).
223
- const warm = liveness === "warm" || liveness === "draining";
290
+ const warm = sandboxAcceptsLiveIo(liveness);
224
291
  const booting =
225
292
  activated &&
226
293
  !ptyMode &&
@@ -228,6 +295,13 @@ export function SandboxTerminal({
228
295
  liveness !== null &&
229
296
  liveness !== undefined &&
230
297
  result.chunks.length === 0;
298
+ const surfaceState = terminalSurfaceState({
299
+ ready,
300
+ acceptsInput,
301
+ inputPending,
302
+ ptyStatus,
303
+ booting,
304
+ });
231
305
 
232
306
  function handleActivate() {
233
307
  if (!activated) setActivated(true);
@@ -345,10 +419,6 @@ export function SandboxTerminal({
345
419
  requestAnimationFrame(() => {
346
420
  if (disposed) return;
347
421
  settle();
348
- if (ptyOutputQueueRef.current.length > 0) {
349
- for (const data of ptyOutputQueueRef.current) term.write(data);
350
- ptyOutputQueueRef.current = [];
351
- }
352
422
  el.setAttribute("data-og-term-ready", "true");
353
423
  if (typeof globalThis.__OG_TERMINAL_DEBUG__ === "function") {
354
424
  globalThis.__OG_TERMINAL_DEBUG__({
@@ -407,7 +477,7 @@ export function SandboxTerminal({
407
477
  if (!booting) {
408
478
  if (bootActiveRef.current) {
409
479
  bootActiveRef.current = false;
410
- if (!wroteFirehoseRef.current) term.clear();
480
+ if (!wroteFirehoseRef.current) clearTerminalBootStatus(term);
411
481
  }
412
482
  return;
413
483
  }
@@ -443,12 +513,19 @@ export function SandboxTerminal({
443
513
  // socket; firehose mode uses the legacy ptyWrite-over-HTTP fn.
444
514
  useEffect(() => {
445
515
  const term = termRef.current;
446
- if (!ready || !term || !interactive) return;
447
- const sink = ptyMode ? ptyWrite : result.write;
448
- if (!sink) return;
449
- const sub = term.onData((data) => sink(data));
450
- return () => sub.dispose();
451
- }, [ready, interactive, ptyMode, ptyWrite, result.write]);
516
+ if (!ready || !term || !interactive) {
517
+ setInputReady(false);
518
+ return;
519
+ }
520
+ const sink = ptyAttachable ? ptyWrite : result.write;
521
+ if (!sink) {
522
+ setInputReady(false);
523
+ return;
524
+ }
525
+ const sub = subscribeTerminalInput(term, sink, true);
526
+ setInputReady(sub !== null);
527
+ return () => sub?.dispose();
528
+ }, [ready, interactive, ptyAttachable, ptyWrite, result.write]);
452
529
 
453
530
  // In PTY mode, tell ttyd the window size on the xterm resize event.
454
531
  useEffect(() => {
@@ -496,9 +573,12 @@ export function SandboxTerminal({
496
573
  <span className="truncate font-og-mono">pty: {shell ?? "shell"}</span>
497
574
  </span>
498
575
  <span className="flex shrink-0 items-center gap-2">
499
- {!interactive && (
500
- <span className="rounded-og-sm bg-og-surface-2 px-1.5 py-0.5 text-og-xs uppercase tracking-wide">
501
- read-only
576
+ {terminalModeLabel && (
577
+ <span
578
+ className="rounded-og-sm bg-og-surface-2 px-1.5 py-0.5 text-og-xs uppercase tracking-wide"
579
+ role="status"
580
+ >
581
+ {terminalModeLabel}
502
582
  </span>
503
583
  )}
504
584
  <button
@@ -532,6 +612,10 @@ export function SandboxTerminal({
532
612
  className="h-full w-full"
533
613
  style={{ visibility: ready ? "visible" : "hidden" }}
534
614
  data-opengeni-terminal
615
+ data-opengeni-terminal-ready={ready ? "true" : "false"}
616
+ data-opengeni-terminal-interactive={surfaceState === "interactive" ? "true" : "false"}
617
+ data-opengeni-terminal-state={surfaceState}
618
+ data-opengeni-terminal-status={ptyAttachable ? ptyStatus : "firehose"}
535
619
  />
536
620
  </div>
537
621
  </div>