@opengeni/react 0.48.0 → 0.57.0

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 (325) hide show
  1. package/README.md +138 -2
  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 +199 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/{chunk-NJ7M2B6Z.js → chunk-5UQPNLIO.js} +16 -8
  15. package/dist/{chunk-NJ7M2B6Z.js.map → chunk-5UQPNLIO.js.map} +1 -1
  16. package/dist/{chunk-LHLYWG5R.js → chunk-6KLX2RH5.js} +461 -38
  17. package/dist/chunk-6KLX2RH5.js.map +1 -0
  18. package/dist/chunk-7WCLAEFV.js +2828 -0
  19. package/dist/chunk-7WCLAEFV.js.map +1 -0
  20. package/dist/{chunk-I4MPFDRP.js → chunk-B6WPZSTT.js} +104 -104
  21. package/dist/{chunk-I4MPFDRP.js.map → chunk-B6WPZSTT.js.map} +1 -1
  22. package/dist/{chunk-EKZH6IDG.js → chunk-C5ZPSSS5.js} +111 -1
  23. package/dist/chunk-C5ZPSSS5.js.map +1 -0
  24. package/dist/{chunk-OL6QVXGF.js → chunk-HA46CYQK.js} +152 -8
  25. package/dist/chunk-HA46CYQK.js.map +1 -0
  26. package/dist/chunk-HUIKIDM5.js +2148 -0
  27. package/dist/chunk-HUIKIDM5.js.map +1 -0
  28. package/dist/{chunk-FWXGSNXT.js → chunk-IGC6A2ZI.js} +585 -34
  29. package/dist/chunk-IGC6A2ZI.js.map +1 -0
  30. package/dist/chunk-JVYQRRC4.js +53 -0
  31. package/dist/chunk-JVYQRRC4.js.map +1 -0
  32. package/dist/chunk-KCGXPP5V.js +157 -0
  33. package/dist/chunk-KCGXPP5V.js.map +1 -0
  34. package/dist/chunk-LLOLUHVG.js +5937 -0
  35. package/dist/chunk-LLOLUHVG.js.map +1 -0
  36. package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
  37. package/dist/chunk-MKPIAGVL.js.map +1 -0
  38. package/dist/chunk-PEI6RANI.js +3234 -0
  39. package/dist/chunk-PEI6RANI.js.map +1 -0
  40. package/dist/chunk-R3AZOAB5.js +933 -0
  41. package/dist/chunk-R3AZOAB5.js.map +1 -0
  42. package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
  43. package/dist/chunk-WSK7G5LE.js.map +1 -0
  44. package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
  45. package/dist/chunk-XBZWOM2K.js.map +1 -0
  46. package/dist/{chunk-XCNTXWBB.js → chunk-Y7OEDUJ5.js} +856 -338
  47. package/dist/chunk-Y7OEDUJ5.js.map +1 -0
  48. package/dist/client.d.ts +10 -2
  49. package/dist/clipboard.d.ts +1 -0
  50. package/dist/clipboard.js +7 -0
  51. package/dist/clipboard.js.map +1 -0
  52. package/dist/commands/index.d.ts +3 -3
  53. package/dist/commands/registry.d.ts +1 -1
  54. package/dist/commands/types.d.ts +1 -1
  55. package/dist/components/approval-surface.d.ts +2 -2
  56. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  57. package/dist/components/artifacts/document-editor.d.ts +172 -0
  58. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  59. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  60. package/dist/components/artifacts/editable-document.d.ts +24 -0
  61. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  62. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  63. package/dist/components/artifacts/index.d.ts +9 -0
  64. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  65. package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
  66. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  67. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  68. package/dist/components/browser-input.d.ts +7 -0
  69. package/dist/components/browser-viewer.d.ts +30 -0
  70. package/dist/components/chat-composer.d.ts +8 -8
  71. package/dist/components/code-editor.d.ts +25 -3
  72. package/dist/components/command-palette.d.ts +2 -2
  73. package/dist/components/composer-transcription-control.d.ts +2 -2
  74. package/dist/components/composer.d.ts +17 -17
  75. package/dist/components/computer-viewer.d.ts +24 -0
  76. package/dist/components/copy-button.d.ts +2 -2
  77. package/dist/components/desktop-viewer.d.ts +50 -22
  78. package/dist/components/diff-view.d.ts +1 -1
  79. package/dist/components/enrollment-consent.d.ts +1 -1
  80. package/dist/components/enrollment-device-flow.d.ts +1 -1
  81. package/dist/components/file-browser.d.ts +2 -2
  82. package/dist/components/fleet-tile.d.ts +1 -1
  83. package/dist/components/generated-video-player.d.ts +10 -0
  84. package/dist/components/human-input-form.d.ts +1 -1
  85. package/dist/components/human-input-surface.d.ts +2 -2
  86. package/dist/components/interaction-intervention-banner.d.ts +11 -0
  87. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  88. package/dist/components/machine-card.d.ts +2 -2
  89. package/dist/components/machine-dock-bar.d.ts +3 -3
  90. package/dist/components/machine-health-pill.d.ts +2 -2
  91. package/dist/components/machine-input-display.d.ts +1 -1
  92. package/dist/components/machine-metrics.d.ts +2 -2
  93. package/dist/components/machine-status-pill.d.ts +4 -4
  94. package/dist/components/machines/health.d.ts +1 -1
  95. package/dist/components/machines/machine-detail.d.ts +3 -3
  96. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  97. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  98. package/dist/components/machines/series.d.ts +2 -2
  99. package/dist/components/machines-dashboard.d.ts +2 -2
  100. package/dist/components/markdown.d.ts +1 -1
  101. package/dist/components/message-timeline.d.ts +12 -5
  102. package/dist/components/model-picker.d.ts +2 -2
  103. package/dist/components/model-policy-picker.d.ts +7 -7
  104. package/dist/components/pierre-diff.d.ts +1 -1
  105. package/dist/components/pierre-file.d.ts +1 -1
  106. package/dist/components/queue-draft-policy.d.ts +1 -1
  107. package/dist/components/queue-surface-implementation.d.ts +3 -3
  108. package/dist/components/queue-surface-state.d.ts +4 -4
  109. package/dist/components/queue-surface.d.ts +5 -5
  110. package/dist/components/sandbox-files.d.ts +3 -3
  111. package/dist/components/sandbox-terminal.d.ts +24 -4
  112. package/dist/components/sandbox-workspace.d.ts +28 -9
  113. package/dist/components/session-chrome.d.ts +4 -4
  114. package/dist/components/session-status.d.ts +2 -2
  115. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  116. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  117. package/dist/components/timeline-annotations.d.ts +13 -0
  118. package/dist/components/tooltip.d.ts +4 -4
  119. package/dist/components/user-message-body.d.ts +2 -2
  120. package/dist/components/workbench-changes.d.ts +2 -2
  121. package/dist/components/workspace-dock.d.ts +1 -1
  122. package/dist/composer.d.ts +11 -11
  123. package/dist/composer.js +7 -7
  124. package/dist/hooks/internal.d.ts +1 -1
  125. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  126. package/dist/hooks/use-auth-runs.d.ts +32 -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-downloads.d.ts +25 -0
  130. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  131. package/dist/hooks/use-browser-identities.d.ts +30 -0
  132. package/dist/hooks/use-browser-session.d.ts +29 -0
  133. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  134. package/dist/hooks/use-channels.d.ts +26 -0
  135. package/dist/hooks/use-codex-accounts.d.ts +2 -2
  136. package/dist/hooks/use-composer.d.ts +36 -9
  137. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  138. package/dist/hooks/use-computer-session.d.ts +29 -0
  139. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  140. package/dist/hooks/use-desktop-stream.d.ts +5 -1
  141. package/dist/hooks/use-environments.d.ts +1 -1
  142. package/dist/hooks/use-file-attachments.d.ts +1 -1
  143. package/dist/hooks/use-goal.d.ts +2 -2
  144. package/dist/hooks/use-human-input.d.ts +2 -2
  145. package/dist/hooks/use-interaction-interventions.d.ts +27 -0
  146. package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
  147. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  148. package/dist/hooks/use-machine-chip.d.ts +2 -2
  149. package/dist/hooks/use-machines.d.ts +4 -4
  150. package/dist/hooks/use-network-routes.d.ts +25 -0
  151. package/dist/hooks/use-packs.d.ts +7 -2
  152. package/dist/hooks/use-rigs.d.ts +1 -1
  153. package/dist/hooks/use-sandbox-files.d.ts +6 -4
  154. package/dist/hooks/use-sandbox-git.d.ts +6 -5
  155. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  156. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  157. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  158. package/dist/hooks/use-session-control.d.ts +1 -1
  159. package/dist/hooks/use-session-events.d.ts +2 -2
  160. package/dist/hooks/use-session-lineage.d.ts +1 -1
  161. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  162. package/dist/hooks/use-session.d.ts +2 -2
  163. package/dist/hooks/use-site-auth-connections.d.ts +25 -0
  164. package/dist/hooks/use-slash-commands.d.ts +1 -1
  165. package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
  166. package/dist/hooks/use-terminal-stream.d.ts +11 -2
  167. package/dist/hooks/use-turn-queue.d.ts +2 -2
  168. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  169. package/dist/hooks/use-voice-input.d.ts +1 -1
  170. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  171. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  172. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  173. package/dist/hooks/use-workspaces.d.ts +1 -1
  174. package/dist/index.d.ts +162 -155
  175. package/dist/index.js +1822 -1703
  176. package/dist/index.js.map +1 -1
  177. package/dist/interaction.d.ts +33 -0
  178. package/dist/interaction.js +550 -0
  179. package/dist/interaction.js.map +1 -0
  180. package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
  181. package/dist/interactive-workbench-viewers-TOHBXJEE.js.map +1 -0
  182. package/dist/lib/host-platform.d.ts +5 -0
  183. package/dist/lib/relay-wire.d.ts +24 -1
  184. package/dist/lib/sandbox-liveness.d.ts +9 -0
  185. package/dist/lib/terminal-capability.d.ts +5 -0
  186. package/dist/lib/xterm-theme.d.ts +1 -1
  187. package/dist/machines.d.ts +30 -30
  188. package/dist/machines.js +2 -2
  189. package/dist/model-policy.d.ts +1 -1
  190. package/dist/model-policy.js +1 -1
  191. package/dist/provider.d.ts +6 -5
  192. package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
  193. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  194. package/dist/realtime/dropdown-menu.d.ts +8 -8
  195. package/dist/realtime/realtime-control.d.ts +8 -8
  196. package/dist/realtime.d.ts +4 -4
  197. package/dist/realtime.js +38 -11
  198. package/dist/realtime.js.map +1 -1
  199. package/dist/session-context.d.ts +28 -2
  200. package/dist/session-ui.d.ts +14 -14
  201. package/dist/session-ui.js +5 -4
  202. package/dist/session.d.ts +28 -28
  203. package/dist/session.js +10 -10
  204. package/dist/timeline/activity-rail.d.ts +5 -3
  205. package/dist/timeline/disclosure-context.d.ts +1 -1
  206. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  207. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  208. package/dist/timeline/index.d.ts +19 -19
  209. package/dist/timeline/parsers.d.ts +4 -1
  210. package/dist/timeline/projection.d.ts +3 -3
  211. package/dist/timeline/registry.d.ts +8 -2
  212. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  213. package/dist/timeline/shared.d.ts +21 -10
  214. package/dist/timeline/tool-diff.d.ts +2 -2
  215. package/dist/timeline/tool-renderers.d.ts +1 -1
  216. package/dist/timeline/turn-summary.d.ts +3 -3
  217. package/dist/timeline/types.d.ts +44 -3
  218. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  219. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  220. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  221. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  222. package/package.json +36 -7
  223. package/src/artifacts-document.ts +31 -0
  224. package/src/artifacts-presentation.ts +36 -0
  225. package/src/artifacts-spreadsheet.ts +21 -0
  226. package/src/artifacts.ts +89 -0
  227. package/src/client.ts +157 -1
  228. package/src/clipboard.ts +2 -0
  229. package/src/components/artifacts/artifact-surface.tsx +93 -0
  230. package/src/components/artifacts/document-editor.tsx +2255 -0
  231. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  232. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  233. package/src/components/artifacts/editable-document.tsx +657 -0
  234. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  235. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  236. package/src/components/artifacts/index.ts +104 -0
  237. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  238. package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
  239. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  240. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  241. package/src/components/browser-input.ts +65 -0
  242. package/src/components/browser-viewer.tsx +2702 -0
  243. package/src/components/chat-composer.tsx +13 -0
  244. package/src/components/code-editor.tsx +89 -30
  245. package/src/components/composer-transcription-control.tsx +10 -6
  246. package/src/components/computer-viewer.tsx +1636 -0
  247. package/src/components/desktop-viewer.tsx +215 -74
  248. package/src/components/generated-video-player.tsx +89 -0
  249. package/src/components/interaction-intervention-banner.tsx +99 -0
  250. package/src/components/interactive-workbench-viewers.ts +2 -0
  251. package/src/components/machine-input-display.ts +3 -0
  252. package/src/components/machines/machine-detail.tsx +1 -1
  253. package/src/components/message-timeline.tsx +289 -32
  254. package/src/components/model-policy-picker.tsx +15 -0
  255. package/src/components/queue-surface-implementation.tsx +116 -60
  256. package/src/components/sandbox-files.tsx +2 -2
  257. package/src/components/sandbox-terminal.tsx +128 -46
  258. package/src/components/sandbox-workspace.tsx +256 -90
  259. package/src/components/session-chrome.tsx +53 -13
  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/workbench-changes.tsx +188 -31
  264. package/src/components/workspace-dock.tsx +59 -5
  265. package/src/hooks/use-attached-browsers.ts +154 -0
  266. package/src/hooks/use-auth-runs.ts +278 -0
  267. package/src/hooks/use-browser-downloads.ts +218 -0
  268. package/src/hooks/use-browser-frame-stream.ts +425 -0
  269. package/src/hooks/use-browser-identities.ts +266 -0
  270. package/src/hooks/use-browser-session.ts +543 -0
  271. package/src/hooks/use-browser-sessions.ts +286 -0
  272. package/src/hooks/use-channels.ts +99 -0
  273. package/src/hooks/use-composer.ts +769 -24
  274. package/src/hooks/use-computer-frame-stream.ts +434 -0
  275. package/src/hooks/use-computer-session.ts +462 -0
  276. package/src/hooks/use-computer-sessions.ts +251 -0
  277. package/src/hooks/use-desktop-stream.ts +39 -7
  278. package/src/hooks/use-interaction-interventions.ts +242 -0
  279. package/src/hooks/use-interaction-invalidation.ts +66 -0
  280. package/src/hooks/use-machine-chip.ts +15 -3
  281. package/src/hooks/use-machines.ts +33 -3
  282. package/src/hooks/use-network-routes.ts +214 -0
  283. package/src/hooks/use-packs.ts +51 -0
  284. package/src/hooks/use-sandbox-files.ts +98 -55
  285. package/src/hooks/use-sandbox-git.ts +93 -38
  286. package/src/hooks/use-sandbox-terminal.ts +3 -2
  287. package/src/hooks/use-session-capabilities.ts +59 -46
  288. package/src/hooks/use-site-auth-connections.ts +226 -0
  289. package/src/hooks/use-supergrok-accounts.ts +81 -0
  290. package/src/hooks/use-terminal-stream.ts +359 -47
  291. package/src/hooks/use-video-artifact-playback.ts +28 -0
  292. package/src/hooks/use-workspace-edit.ts +2 -1
  293. package/src/index.ts +18 -0
  294. package/src/interaction.ts +101 -0
  295. package/src/lib/host-platform.ts +10 -0
  296. package/src/lib/relay-wire.ts +47 -4
  297. package/src/lib/sandbox-liveness.ts +11 -0
  298. package/src/lib/terminal-capability.ts +10 -0
  299. package/src/model-policy.ts +19 -4
  300. package/src/provider.tsx +133 -1
  301. package/src/realtime/realtime-control.tsx +34 -6
  302. package/src/session-context.ts +156 -1
  303. package/src/timeline/activity-rail.tsx +25 -3
  304. package/src/timeline/index.ts +3 -0
  305. package/src/timeline/parsers.ts +14 -1
  306. package/src/timeline/projection.ts +269 -14
  307. package/src/timeline/registry.ts +13 -1
  308. package/src/timeline/screenshot-lightbox.tsx +19 -9
  309. package/src/timeline/shared.tsx +56 -8
  310. package/src/timeline/tool-renderers.tsx +303 -50
  311. package/src/timeline/types.ts +57 -3
  312. package/styles/compiled.css +312 -21
  313. package/styles/index.css +15 -0
  314. package/styles/responsive.css +13 -1
  315. package/dist/chunk-23EJ676W.js.map +0 -1
  316. package/dist/chunk-EKZH6IDG.js.map +0 -1
  317. package/dist/chunk-FWXGSNXT.js.map +0 -1
  318. package/dist/chunk-LHLYWG5R.js.map +0 -1
  319. package/dist/chunk-O4VXOBGF.js.map +0 -1
  320. package/dist/chunk-OL6QVXGF.js.map +0 -1
  321. package/dist/chunk-WYA65Y3F.js.map +0 -1
  322. package/dist/chunk-XCNTXWBB.js.map +0 -1
  323. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  324. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  325. package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
@@ -0,0 +1,733 @@
1
+ import {
2
+ SPREADSHEET_ARTIFACT_COMMAND_VERSION,
3
+ editableArtifactStableId,
4
+ spreadsheetSheetId,
5
+ type EditableArtifactSession,
6
+ type EditableSpreadsheetCellValue,
7
+ type EditableSpreadsheetMetadataProjection,
8
+ type EditableSpreadsheetSheetMetadata,
9
+ type EditableSpreadsheetViewportProjection,
10
+ type EditableSpreadsheetViewportQuery,
11
+ type SpreadsheetCellInput,
12
+ type SpreadsheetSheetGeneration,
13
+ } from "@opengeni/sdk/editable-artifacts";
14
+ import { PlusIcon } from "lucide-react";
15
+ import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
16
+
17
+ import { cn } from "../../lib/cn";
18
+ import { ArtifactSurface } from "./artifact-surface";
19
+ import {
20
+ editableArtifactAccessRevoked,
21
+ editableArtifactStatusLabel,
22
+ EditableArtifactMessage,
23
+ useEditableArtifactView,
24
+ } from "./editable-artifact-ui";
25
+ import { SparseSpreadsheetCellIndex } from "./spreadsheet-canvas";
26
+ import {
27
+ SpreadsheetProjectionGrid,
28
+ type SpreadsheetCommit,
29
+ type SpreadsheetGridProjection,
30
+ type SpreadsheetRangeCommit,
31
+ type SpreadsheetSelection,
32
+ type SpreadsheetViewport,
33
+ } from "./spreadsheet-grid";
34
+
35
+ const EXCEL_MAX_ROWS = 1_048_576;
36
+ const EXCEL_MAX_COLUMNS = 16_384;
37
+ const INITIAL_VIEWPORT_ROWS = 64;
38
+ const INITIAL_VIEWPORT_COLUMNS = 32;
39
+ const MAX_INTERACTIVE_QUERY_CELLS = 65_536;
40
+ const MAX_INTERACTIVE_QUERY_BYTES = 8 * 1024 * 1024;
41
+ const EMPTY_FORMAT = Object.freeze({});
42
+ const EMPTY_SHEETS: readonly EditableSpreadsheetSheetMetadata[] = [];
43
+
44
+ export type EditableSpreadsheetGridProps = {
45
+ session: EditableArtifactSession;
46
+ sheet: EditableSpreadsheetSheetMetadata;
47
+ metadataRevision: bigint;
48
+ readOnly?: boolean | undefined;
49
+ rowCount?: number | undefined;
50
+ columnCount?: number | undefined;
51
+ overscanRows?: number | undefined;
52
+ overscanColumns?: number | undefined;
53
+ onSelectionChange?: ((selection: SpreadsheetSelection) => void) | undefined;
54
+ onCommit?: ((commit: SpreadsheetCommit) => void) | undefined;
55
+ onCommandError?: ((error: Error) => void) | undefined;
56
+ onViewportChange?: ((viewport: SpreadsheetViewport) => void) | undefined;
57
+ className?: string | undefined;
58
+ };
59
+
60
+ export type EditableSpreadsheetArtifactSurfaceProps = Omit<
61
+ EditableSpreadsheetGridProps,
62
+ "metadataRevision" | "sheet"
63
+ > & {
64
+ title?: string | undefined;
65
+ subtitle?: ReactNode | undefined;
66
+ actions?: ReactNode | undefined;
67
+ initialSheetId?: string | undefined;
68
+ allowAddSheet?: boolean | undefined;
69
+ };
70
+
71
+ type ProjectionState = {
72
+ query: EditableSpreadsheetViewportQuery;
73
+ projection: EditableSpreadsheetViewportProjection | null;
74
+ error: Error | null;
75
+ };
76
+
77
+ /**
78
+ * Durable spreadsheet editor over one SDK session. Canonical state remains in
79
+ * the dedicated Worker; React receives one bounded immutable viewport only.
80
+ */
81
+ export function EditableSpreadsheetGrid({
82
+ session,
83
+ sheet,
84
+ metadataRevision,
85
+ readOnly = false,
86
+ rowCount: requestedRowCount,
87
+ columnCount: requestedColumnCount,
88
+ overscanRows = 3,
89
+ overscanColumns = 2,
90
+ onSelectionChange,
91
+ onCommit,
92
+ onCommandError,
93
+ onViewportChange,
94
+ className,
95
+ }: EditableSpreadsheetGridProps) {
96
+ const rowCount = boundedSheetCount(requestedRowCount, EXCEL_MAX_ROWS, sheet.usedBounds?.endRow);
97
+ const columnCount = boundedSheetCount(
98
+ requestedColumnCount,
99
+ EXCEL_MAX_COLUMNS,
100
+ sheet.usedBounds?.endColumn,
101
+ );
102
+ const [state, setState] = useState<ProjectionState>(() => ({
103
+ query: initialViewportQuery(sheet.sheetId, rowCount, columnCount),
104
+ projection: null,
105
+ error: null,
106
+ }));
107
+ const activeCellRef = useRef({ row: 0, column: 0 });
108
+
109
+ useEffect(() => {
110
+ setState({
111
+ query: initialViewportQuery(sheet.sheetId, rowCount, columnCount),
112
+ projection: null,
113
+ error: null,
114
+ });
115
+ activeCellRef.current = { row: 0, column: 0 };
116
+ }, [columnCount, rowCount, session, sheet.sheetId]);
117
+
118
+ useEffect(() => {
119
+ const query = state.query;
120
+ return session.subscribeSpreadsheetViewport(
121
+ query,
122
+ (projection) => {
123
+ if (!sameViewport(projection, query)) return;
124
+ if (sheet.generationId !== null && projection.generationId !== sheet.generationId) {
125
+ setState((current) =>
126
+ sameViewportQuery(current.query, query)
127
+ ? {
128
+ ...current,
129
+ projection: null,
130
+ error: new Error("Spreadsheet generation changed; refreshing metadata"),
131
+ }
132
+ : current,
133
+ );
134
+ return;
135
+ }
136
+ setState((current) =>
137
+ sameViewportQuery(current.query, query) ? { query, projection, error: null } : current,
138
+ );
139
+ },
140
+ {
141
+ onError(error) {
142
+ setState((current) =>
143
+ sameViewportQuery(current.query, query) ? { ...current, error } : current,
144
+ );
145
+ },
146
+ },
147
+ );
148
+ }, [session, sheet.generationId, state.query]);
149
+
150
+ const projection = useMemo(
151
+ () =>
152
+ projectSdkViewport(
153
+ sheet,
154
+ metadataRevision,
155
+ state.query,
156
+ state.projection,
157
+ rowCount,
158
+ columnCount,
159
+ ),
160
+ [columnCount, metadataRevision, rowCount, sheet, state.projection, state.query],
161
+ );
162
+ const generation = useMemo(() => sheetGeneration(sheet), [sheet]);
163
+ const editable = !readOnly && generation !== null;
164
+
165
+ const handleCommit = useCallback(
166
+ async (commit: SpreadsheetCommit) => {
167
+ if (!generation) throw new Error("This sheet generation is not writable yet");
168
+ const input = spreadsheetCellInput(commit.input, commit.kind);
169
+ await session.applySpreadsheetCommands({
170
+ version: SPREADSHEET_ARTIFACT_COMMAND_VERSION,
171
+ commands: [
172
+ {
173
+ kind: "cells.set",
174
+ sheet: generation,
175
+ anchor: { row: commit.cell.row, column: commit.cell.col },
176
+ rows: 1,
177
+ columns: 1,
178
+ cells: [input],
179
+ },
180
+ ],
181
+ });
182
+ onCommit?.(commit);
183
+ },
184
+ [generation, onCommit, session],
185
+ );
186
+
187
+ const handleClear = useCallback(
188
+ async (selection: SpreadsheetSelection) => {
189
+ if (!generation) throw new Error("This sheet generation is not writable yet");
190
+ const top = Math.min(selection.anchor.row, selection.focus.row);
191
+ const bottom = Math.max(selection.anchor.row, selection.focus.row);
192
+ const left = Math.min(selection.anchor.col, selection.focus.col);
193
+ const right = Math.max(selection.anchor.col, selection.focus.col);
194
+ await session.applySpreadsheetCommands({
195
+ version: SPREADSHEET_ARTIFACT_COMMAND_VERSION,
196
+ commands: [
197
+ {
198
+ kind: "range.clear",
199
+ sheet: generation,
200
+ range: {
201
+ start: { row: top, column: left },
202
+ end: { row: bottom, column: right },
203
+ },
204
+ },
205
+ ],
206
+ });
207
+ },
208
+ [generation, session],
209
+ );
210
+
211
+ const handleCommitRange = useCallback(
212
+ async (commit: SpreadsheetRangeCommit) => {
213
+ if (!generation) throw new Error("This sheet generation is not writable yet");
214
+ await session.applySpreadsheetCommands({
215
+ version: SPREADSHEET_ARTIFACT_COMMAND_VERSION,
216
+ commands: [
217
+ {
218
+ kind: "cells.set",
219
+ sheet: generation,
220
+ anchor: { row: commit.anchor.row, column: commit.anchor.col },
221
+ rows: commit.rows,
222
+ columns: commit.columns,
223
+ cells: commit.inputs.map((input) =>
224
+ spreadsheetCellInput(input, input.startsWith("=") ? "formula" : "value"),
225
+ ),
226
+ },
227
+ ],
228
+ });
229
+ },
230
+ [generation, session],
231
+ );
232
+
233
+ const handleSelection = useCallback(
234
+ (selection: SpreadsheetSelection) => {
235
+ activeCellRef.current = {
236
+ row: selection.focus.row,
237
+ column: selection.focus.col,
238
+ };
239
+ setState((current) => {
240
+ if (queryContains(current.query, selection.focus.row, selection.focus.col)) return current;
241
+ return {
242
+ query: boundedViewportQuery(
243
+ sheet.sheetId,
244
+ selection.focus.row,
245
+ selection.focus.row + 1,
246
+ selection.focus.col,
247
+ selection.focus.col + 1,
248
+ rowCount,
249
+ columnCount,
250
+ activeCellRef.current,
251
+ ),
252
+ projection: current.projection,
253
+ error: null,
254
+ };
255
+ });
256
+ onSelectionChange?.(selection);
257
+ },
258
+ [columnCount, onSelectionChange, rowCount, sheet.sheetId],
259
+ );
260
+
261
+ const handleViewport = useCallback(
262
+ (viewport: SpreadsheetViewport) => {
263
+ const next = boundedViewportQuery(
264
+ sheet.sheetId,
265
+ viewport.overscanRowStart,
266
+ viewport.overscanRowEnd,
267
+ viewport.overscanColumnStart,
268
+ viewport.overscanColumnEnd,
269
+ rowCount,
270
+ columnCount,
271
+ activeCellRef.current,
272
+ );
273
+ setState((current) =>
274
+ sameViewportQuery(current.query, next)
275
+ ? current
276
+ : { query: next, projection: current.projection, error: null },
277
+ );
278
+ onViewportChange?.(viewport);
279
+ },
280
+ [columnCount, onViewportChange, rowCount, sheet.sheetId],
281
+ );
282
+
283
+ return (
284
+ <div className={cn("relative h-full min-h-0", className)}>
285
+ <SpreadsheetProjectionGrid
286
+ projection={projection}
287
+ readOnly={!editable}
288
+ overscanRows={overscanRows}
289
+ overscanColumns={overscanColumns}
290
+ onSelectionChange={handleSelection}
291
+ commit={editable ? handleCommit : undefined}
292
+ commitRange={editable ? handleCommitRange : undefined}
293
+ clear={editable ? handleClear : undefined}
294
+ onCommandError={onCommandError}
295
+ onViewportChange={handleViewport}
296
+ />
297
+ {state.error ? (
298
+ <output
299
+ role="status"
300
+ className="pointer-events-none absolute bottom-2 left-2 z-50 rounded-og-sm border border-og-status-failed/30 bg-og-surface-1/95 px-2 py-1 text-og-xs text-og-status-failed shadow-og-sm"
301
+ style={{ maxWidth: "min(28rem, calc(100% - 1rem))" }}
302
+ >
303
+ {state.error.message}
304
+ </output>
305
+ ) : null}
306
+ </div>
307
+ );
308
+ }
309
+
310
+ /** Artifact chrome, sheet navigation, and one Worker-backed spreadsheet grid. */
311
+ export function EditableSpreadsheetArtifactSurface({
312
+ session,
313
+ title = "Workbook",
314
+ subtitle,
315
+ actions,
316
+ initialSheetId,
317
+ allowAddSheet = true,
318
+ readOnly = false,
319
+ ...gridProps
320
+ }: EditableSpreadsheetArtifactSurfaceProps) {
321
+ const { metadata, error: metadataError } = useSpreadsheetMetadata(session);
322
+ const view = useEditableArtifactView(session);
323
+ const [activeSheetId, setActiveSheetId] = useState<string | null>(initialSheetId ?? null);
324
+ const [creatingSheet, setCreatingSheet] = useState(false);
325
+ const [surfaceError, setSurfaceError] = useState<Error | null>(null);
326
+ const sheets = metadata?.sheets ?? EMPTY_SHEETS;
327
+ const activeSheet =
328
+ sheets.find((sheet) => sheet.sheetId === activeSheetId) ??
329
+ (initialSheetId ? sheets.find((sheet) => sheet.sheetId === initialSheetId) : undefined) ??
330
+ sheets[0] ??
331
+ null;
332
+ const writable = !readOnly && view.writable;
333
+ const accessRevoked = editableArtifactAccessRevoked(view);
334
+
335
+ const addSheet = useCallback(async () => {
336
+ if (!writable || creatingSheet) return;
337
+ setCreatingSheet(true);
338
+ setSurfaceError(null);
339
+ try {
340
+ const after = activeSheet ? sheetGeneration(activeSheet) : null;
341
+ const created = await session.createSpreadsheetSheet({
342
+ name: nextAvailableSheetName(sheets),
343
+ after,
344
+ });
345
+ setActiveSheetId(created.sheetId);
346
+ } catch (cause) {
347
+ setSurfaceError(asError(cause));
348
+ } finally {
349
+ setCreatingSheet(false);
350
+ }
351
+ }, [activeSheet, creatingSheet, session, sheets, writable]);
352
+
353
+ const footer = (
354
+ <div
355
+ className="flex min-h-9 items-center gap-1 overflow-x-auto px-2"
356
+ role="tablist"
357
+ aria-label="Worksheets"
358
+ >
359
+ {sheets.map((sheet) => (
360
+ <button
361
+ key={`${sheet.sheetId}:${sheet.generationId ?? "pending"}`}
362
+ type="button"
363
+ role="tab"
364
+ aria-selected={sheet.sheetId === activeSheet?.sheetId}
365
+ onClick={() => setActiveSheetId(sheet.sheetId)}
366
+ className={cn(
367
+ "h-7 shrink-0 rounded-og-sm px-2.5 text-og-sm outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-og-accent",
368
+ sheet.sheetId === activeSheet?.sheetId
369
+ ? "bg-og-surface-3 font-medium text-og-fg"
370
+ : "text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg",
371
+ )}
372
+ >
373
+ {sheet.name}
374
+ </button>
375
+ ))}
376
+ {writable && allowAddSheet ? (
377
+ <button
378
+ type="button"
379
+ onClick={() => void addSheet()}
380
+ disabled={creatingSheet}
381
+ aria-label={creatingSheet ? "Adding worksheet" : "Add worksheet"}
382
+ className="grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted outline-hidden hover:bg-og-surface-3 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent disabled:opacity-50 [&>svg]:size-3.5"
383
+ >
384
+ <PlusIcon />
385
+ </button>
386
+ ) : null}
387
+ </div>
388
+ );
389
+ const error = surfaceError ?? metadataError;
390
+
391
+ return (
392
+ <ArtifactSurface
393
+ modality="spreadsheet"
394
+ title={title}
395
+ subtitle={
396
+ subtitle ??
397
+ (accessRevoked
398
+ ? editableArtifactStatusLabel(view)
399
+ : metadata
400
+ ? `${sheets.length} sheet${sheets.length === 1 ? "" : "s"}`
401
+ : editableArtifactStatusLabel(view))
402
+ }
403
+ actions={actions}
404
+ footer={accessRevoked ? undefined : footer}
405
+ busy={!accessRevoked && !metadata && !error}
406
+ >
407
+ {accessRevoked ? (
408
+ <EditableArtifactMessage
409
+ title="Access changed"
410
+ detail={editableArtifactStatusLabel(view)}
411
+ />
412
+ ) : activeSheet && metadata ? (
413
+ <EditableSpreadsheetGrid
414
+ key={`${activeSheet.sheetId}:${activeSheet.generationId ?? "pending"}`}
415
+ {...gridProps}
416
+ session={session}
417
+ sheet={activeSheet}
418
+ metadataRevision={metadata.revision}
419
+ readOnly={!writable}
420
+ />
421
+ ) : error ? (
422
+ <EditableArtifactMessage title="Could not open this workbook" detail={error.message} />
423
+ ) : metadata ? (
424
+ <div className="grid h-full place-items-center bg-og-bg p-6 text-center">
425
+ <div>
426
+ <p className="text-og-base font-medium text-og-fg">This workbook has no worksheets.</p>
427
+ {writable && allowAddSheet ? (
428
+ <button
429
+ type="button"
430
+ onClick={() => void addSheet()}
431
+ disabled={creatingSheet}
432
+ className="mt-3 rounded-og-sm bg-og-accent-deep px-3 py-1.5 text-og-sm font-medium text-og-accent-fg outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent disabled:opacity-50"
433
+ >
434
+ {creatingSheet ? "Adding…" : "Add worksheet"}
435
+ </button>
436
+ ) : null}
437
+ </div>
438
+ </div>
439
+ ) : (
440
+ <EditableArtifactMessage
441
+ title="Opening workbook"
442
+ detail={editableArtifactStatusLabel(view)}
443
+ />
444
+ )}
445
+ </ArtifactSurface>
446
+ );
447
+ }
448
+
449
+ function useSpreadsheetMetadata(session: EditableArtifactSession): {
450
+ metadata: EditableSpreadsheetMetadataProjection | null;
451
+ error: Error | null;
452
+ } {
453
+ const [state, setState] = useState<{
454
+ metadata: EditableSpreadsheetMetadataProjection | null;
455
+ error: Error | null;
456
+ }>({ metadata: null, error: null });
457
+ useEffect(
458
+ () =>
459
+ session.subscribeSpreadsheetMetadata({}, (metadata) => setState({ metadata, error: null }), {
460
+ onError: (error) => setState((current) => ({ ...current, error })),
461
+ }),
462
+ [session],
463
+ );
464
+ return state;
465
+ }
466
+
467
+ function projectSdkViewport(
468
+ sheet: EditableSpreadsheetSheetMetadata,
469
+ metadataRevision: bigint,
470
+ query: EditableSpreadsheetViewportQuery,
471
+ viewport: EditableSpreadsheetViewportProjection | null,
472
+ rowCount: number,
473
+ columnCount: number,
474
+ ): SpreadsheetGridProjection {
475
+ const current = viewport && sameViewport(viewport, query) ? viewport : null;
476
+ const projectedCells = (current?.cells ?? []).map((cell) => ({
477
+ row: cell.row,
478
+ col: cell.column,
479
+ value: projectedCellValue(cell.value),
480
+ formula: cell.formula,
481
+ format: EMPTY_FORMAT,
482
+ }));
483
+ const cells = new SparseSpreadsheetCellIndex(projectedCells);
484
+ const byCoordinate = new Map(
485
+ projectedCells.map((cell) => [`${cell.row}:${cell.col}`, cell] as const),
486
+ );
487
+ const used = sheet.usedBounds;
488
+ return {
489
+ sheetId: sheet.sheetId,
490
+ sheetName: sheet.name,
491
+ generationId: current?.generationId ?? sheet.generationId,
492
+ revision: `${metadataRevision}:${current?.revision ?? "loading"}`,
493
+ dimensionRevision: metadataRevision.toString(),
494
+ rowCount,
495
+ columnCount,
496
+ usedRange: used
497
+ ? {
498
+ row: used.startRow,
499
+ col: used.startColumn,
500
+ rowCount: used.endRow - used.startRow + 1,
501
+ colCount: used.endColumn - used.startColumn + 1,
502
+ }
503
+ : null,
504
+ coverage: current
505
+ ? {
506
+ rowStart: current.startRow,
507
+ rowEnd: current.startRow + current.rowCount,
508
+ columnStart: current.startColumn,
509
+ columnEnd: current.startColumn + current.columnCount,
510
+ }
511
+ : {
512
+ rowStart: query.startRow,
513
+ rowEnd: query.startRow,
514
+ columnStart: query.startColumn,
515
+ columnEnd: query.startColumn,
516
+ },
517
+ cells,
518
+ valueAt: (cell) => cell.value,
519
+ readCell: (row, column) => {
520
+ const cell = byCoordinate.get(`${row}:${column}`);
521
+ return cell
522
+ ? {
523
+ value: cell.value,
524
+ input: cell.formula ?? displayValue(cell.value),
525
+ format: cell.format,
526
+ }
527
+ : null;
528
+ },
529
+ };
530
+ }
531
+
532
+ function initialViewportQuery(
533
+ sheetId: string,
534
+ rowCount: number,
535
+ columnCount: number,
536
+ ): EditableSpreadsheetViewportQuery {
537
+ return {
538
+ sheetId,
539
+ startRow: 0,
540
+ startColumn: 0,
541
+ rowCount: Math.min(rowCount, INITIAL_VIEWPORT_ROWS),
542
+ columnCount: Math.min(columnCount, INITIAL_VIEWPORT_COLUMNS),
543
+ maxCells: MAX_INTERACTIVE_QUERY_CELLS,
544
+ maxBytes: MAX_INTERACTIVE_QUERY_BYTES,
545
+ };
546
+ }
547
+
548
+ function boundedViewportQuery(
549
+ sheetId: string,
550
+ desiredRowStart: number,
551
+ desiredRowEnd: number,
552
+ desiredColumnStart: number,
553
+ desiredColumnEnd: number,
554
+ totalRows: number,
555
+ totalColumns: number,
556
+ focus: { row: number; column: number },
557
+ ): EditableSpreadsheetViewportQuery {
558
+ let rowStart = clampInteger(desiredRowStart, 0, totalRows - 1);
559
+ let rowEnd = clampInteger(desiredRowEnd, rowStart + 1, totalRows);
560
+ let columnStart = clampInteger(desiredColumnStart, 0, totalColumns - 1);
561
+ let columnEnd = clampInteger(desiredColumnEnd, columnStart + 1, totalColumns);
562
+ const desiredRows = rowEnd - rowStart;
563
+ const desiredColumns = columnEnd - columnStart;
564
+ if (desiredRows * desiredColumns > MAX_INTERACTIVE_QUERY_CELLS) {
565
+ const columns = Math.min(
566
+ desiredColumns,
567
+ Math.max(1, Math.floor(Math.sqrt(MAX_INTERACTIVE_QUERY_CELLS * 2))),
568
+ );
569
+ const rows = Math.max(1, Math.floor(MAX_INTERACTIVE_QUERY_CELLS / columns));
570
+ columnStart = clampWindowStart(columnStart, columnEnd, focus.column, columns, totalColumns);
571
+ columnEnd = Math.min(totalColumns, columnStart + columns);
572
+ rowStart = clampWindowStart(rowStart, rowEnd, focus.row, rows, totalRows);
573
+ rowEnd = Math.min(totalRows, rowStart + rows);
574
+ }
575
+ return {
576
+ sheetId,
577
+ startRow: rowStart,
578
+ startColumn: columnStart,
579
+ rowCount: rowEnd - rowStart,
580
+ columnCount: columnEnd - columnStart,
581
+ maxCells: MAX_INTERACTIVE_QUERY_CELLS,
582
+ maxBytes: MAX_INTERACTIVE_QUERY_BYTES,
583
+ };
584
+ }
585
+
586
+ function clampWindowStart(
587
+ desiredStart: number,
588
+ desiredEnd: number,
589
+ focus: number,
590
+ size: number,
591
+ total: number,
592
+ ): number {
593
+ if (desiredEnd - desiredStart <= size) return Math.min(desiredStart, Math.max(0, total - size));
594
+ const centered = focus - Math.floor(size / 2);
595
+ return Math.max(desiredStart, Math.min(desiredEnd - size, centered));
596
+ }
597
+
598
+ function queryContains(
599
+ query: EditableSpreadsheetViewportQuery,
600
+ row: number,
601
+ column: number,
602
+ ): boolean {
603
+ return (
604
+ row >= query.startRow &&
605
+ row < query.startRow + query.rowCount &&
606
+ column >= query.startColumn &&
607
+ column < query.startColumn + query.columnCount
608
+ );
609
+ }
610
+
611
+ function sameViewport(
612
+ projection: EditableSpreadsheetViewportProjection,
613
+ query: EditableSpreadsheetViewportQuery,
614
+ ): boolean {
615
+ return (
616
+ projection.sheetId === query.sheetId &&
617
+ projection.startRow === query.startRow &&
618
+ projection.startColumn === query.startColumn &&
619
+ projection.rowCount === query.rowCount &&
620
+ projection.columnCount === query.columnCount
621
+ );
622
+ }
623
+
624
+ function sameViewportQuery(
625
+ left: EditableSpreadsheetViewportQuery,
626
+ right: EditableSpreadsheetViewportQuery,
627
+ ): boolean {
628
+ return (
629
+ left.sheetId === right.sheetId &&
630
+ left.startRow === right.startRow &&
631
+ left.startColumn === right.startColumn &&
632
+ left.rowCount === right.rowCount &&
633
+ left.columnCount === right.columnCount &&
634
+ left.maxCells === right.maxCells &&
635
+ left.maxBytes === right.maxBytes
636
+ );
637
+ }
638
+
639
+ function sheetGeneration(
640
+ sheet: EditableSpreadsheetSheetMetadata,
641
+ ): SpreadsheetSheetGeneration | null {
642
+ if (sheet.generationId === null) return null;
643
+ return {
644
+ kind: "generation",
645
+ sheetId: spreadsheetSheetId(sheet.sheetId),
646
+ creationOperationId: editableArtifactStableId(sheet.generationId),
647
+ };
648
+ }
649
+
650
+ function spreadsheetCellInput(
651
+ input: string,
652
+ kind: SpreadsheetCommit["kind"],
653
+ ): SpreadsheetCellInput {
654
+ if (kind === "formula") return { formula: input, cached: null };
655
+ if (input === "") return null;
656
+ const normalized = input.trim();
657
+ if (/^(?:true|false)$/i.test(normalized)) return normalized.toLowerCase() === "true";
658
+ if (/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(normalized)) {
659
+ const value = Number(normalized);
660
+ if (Number.isFinite(value)) return value;
661
+ }
662
+ return input;
663
+ }
664
+
665
+ function projectedCellValue(value: EditableSpreadsheetCellValue): unknown {
666
+ switch (value.kind) {
667
+ case "empty":
668
+ return null;
669
+ case "boolean":
670
+ case "number":
671
+ case "text":
672
+ return value.value;
673
+ case "date":
674
+ return new Date(value.value);
675
+ case "error":
676
+ return spreadsheetErrorText(value.value);
677
+ }
678
+ }
679
+
680
+ function spreadsheetErrorText(
681
+ value: Extract<EditableSpreadsheetCellValue, { kind: "error" }>["value"],
682
+ ): string {
683
+ if (typeof value === "object") return value.custom;
684
+ return {
685
+ null: "#NULL!",
686
+ divide_by_zero: "#DIV/0!",
687
+ value: "#VALUE!",
688
+ reference: "#REF!",
689
+ name: "#NAME?",
690
+ number: "#NUM!",
691
+ not_available: "#N/A",
692
+ spill: "#SPILL!",
693
+ calculation: "#CALC!",
694
+ }[value];
695
+ }
696
+
697
+ function boundedSheetCount(
698
+ requested: number | undefined,
699
+ maximum: number,
700
+ usedEnd: number | undefined,
701
+ ): number {
702
+ const requestedCount =
703
+ requested === undefined || !Number.isFinite(requested)
704
+ ? maximum
705
+ : Math.max(1, Math.min(maximum, Math.floor(requested)));
706
+ const usedCount =
707
+ usedEnd === undefined || !Number.isSafeInteger(usedEnd)
708
+ ? 1
709
+ : Math.max(1, Math.min(maximum, usedEnd + 1));
710
+ return Math.max(requestedCount, usedCount);
711
+ }
712
+
713
+ function displayValue(value: unknown): string {
714
+ if (value === null || value === undefined) return "";
715
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toISOString();
716
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
717
+ return String(value);
718
+ }
719
+
720
+ function nextAvailableSheetName(sheets: readonly EditableSpreadsheetSheetMetadata[]): string {
721
+ let index = sheets.length + 1;
722
+ while (sheets.some((sheet) => sheet.name === `Sheet${index}`)) index += 1;
723
+ return `Sheet${index}`;
724
+ }
725
+
726
+ function clampInteger(value: number, minimum: number, maximum: number): number {
727
+ if (!Number.isFinite(value)) return minimum;
728
+ return Math.min(maximum, Math.max(minimum, Math.floor(value)));
729
+ }
730
+
731
+ function asError(cause: unknown): Error {
732
+ return cause instanceof Error ? cause : new Error("Spreadsheet change failed");
733
+ }