@marimo-team/islands 0.25.3-dev13 → 0.25.3-dev135

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 (440) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CNMliVzw.js → ConnectedDataExplorerComponent-BAiSVaHj.js} +17 -15
  2. package/dist/{ErrorBoundary-BWYjMPq8.js → ErrorBoundary-C1PyX4Kv.js} +16 -16
  3. package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-BBHN92-F.js} +2 -2
  4. package/dist/any-language-editor-3E24SnCV.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.js → architectureDiagram-5GKGNRK7-C0Rd64qh.js} +1 -1
  6. package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
  7. package/dist/assets/__vite-browser-external-SaDYcg7x.js +1 -0
  8. package/dist/assets/{worker-HPY9fbhz.js → worker-CfME3uyi.js} +3 -2
  9. package/dist/{button-CvjkwdSk.js → button-BQRdqQi1.js} +199 -192
  10. package/dist/{capabilities-CQ5IjhzL.js → capabilities-CI4xUPsp.js} +2 -2
  11. package/dist/{chat-ui-DaHynk8g.js → chat-ui-DqcauBr6.js} +1142 -1141
  12. package/dist/{check-EDwSwP89.js → check-SpOMoCLP.js} +1 -1
  13. package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-hJXey5p2.js} +1 -1
  14. package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-JMzTmmVW.js} +1 -1
  15. package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-BIyoHTXi.js} +1 -1
  16. package/dist/{chunk-TICWLB2K-Cu_XMjDM.js → chunk-TICWLB2K-CCzH2Uwa.js} +1 -1
  17. package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
  18. package/dist/{classDiagram-ZZMXUADV-BXXV13Hq.js → classDiagram-ZZMXUADV-DB1kOPS6.js} +2 -2
  19. package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-CPaFL0OB.js} +2 -2
  20. package/dist/{code-visibility-O0uJkFyz.js → code-visibility-BS43kDO4.js} +6 -6
  21. package/dist/{common-PAvTWnXO.js → common-Bi8KemMn.js} +6762 -6586
  22. package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-C1trdvVt.js} +3 -3
  23. package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-CF93uliH.js} +1 -1
  24. package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-BBB-_p9D.js} +1 -1
  25. package/dist/{data-grid-overlay-editor-DeWySy0K.js → data-grid-overlay-editor-Cxje4LxB.js} +5 -5
  26. package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
  27. package/dist/{diagram-S7CK7UJ4-BsSJJldB.js → diagram-S7CK7UJ4-BynCnbyt.js} +1 -1
  28. package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-svOxikMa.js} +1 -1
  29. package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-BVQSnPx0.js} +1 -1
  30. package/dist/{diagram-Z3DM3KII-BD8mXjOj.js → diagram-Z3DM3KII-nR1J0BH5.js} +1 -1
  31. package/dist/{dialog-CEBFZNyc.js → dialog-DgeqyrEZ.js} +3 -3
  32. package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
  33. package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-CgL3fm8P.js} +1262 -1229
  34. package/dist/{ebnfDiagram-PWID7BFC-BIN1sweE.js → ebnfDiagram-PWID7BFC-C9tq9-Qo.js} +2 -2
  35. package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-BiaeB7Fi.js} +1 -1
  36. package/dist/{error-banner-D-WCIiMt.js → error-banner-D40pmfQQ.js} +83 -83
  37. package/dist/{error-entries-Bv6bZPF3.js → error-entries-CmFDQ9CA.js} +1018 -977
  38. package/dist/{feedback-button-WbN_sdUE.js → feedback-button-n0-eDhBU.js} +23 -23
  39. package/dist/{flowDiagram-HODETNUW-3EfLrlFb.js → flowDiagram-HODETNUW-f-sLm87k.js} +2 -2
  40. package/dist/{formats-27CW_UP4.js → formats-CCxaLPNs.js} +3 -3
  41. package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
  42. package/dist/{glide-data-editor-_oc1WE9x.js → glide-data-editor-BQBctTSS.js} +370 -368
  43. package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Dz_F4pAY.js} +16 -16
  44. package/dist/{globals-C-fUiztk.js → globals-C9s-sVIk.js} +3 -3
  45. package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
  46. package/dist/{html-to-image-Bf8QMGi4.js → html-to-image-CfgsWObF.js} +630 -593
  47. package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-BHrWywBe.js} +1 -1
  48. package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-Da4s0Reg.js} +1 -1
  49. package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.js → kanban-definition-UXKFOSKX-DDf1GVLj.js} +1 -1
  50. package/dist/{label-BDKF3Afc.js → label-D9_-zSg-.js} +3 -3
  51. package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
  52. package/dist/{loader-BQOfjTIJ.js → loader-DOVB0Syl.js} +12 -12
  53. package/dist/main.js +7441 -7115
  54. package/dist/{mermaid-CHo49Yv-.js → mermaid-ClwTt7OD.js} +38 -38
  55. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  56. package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-D60oMZtT.js} +1 -1
  57. package/dist/{number-overlay-editor-DdPkN3h1.js → number-overlay-editor-Bp1CMtol.js} +3 -3
  58. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  59. package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
  60. package/dist/{pegDiagram-XKGWAZYB-BqvywNJA.js → pegDiagram-XKGWAZYB-B9wFpMga.js} +2 -2
  61. package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-BZwvMuBT.js} +1 -1
  62. package/dist/precisionRound-DhAzeRcm.js +49 -0
  63. package/dist/{process-output-BZg56CCb.js → process-output-B64ZYWuI.js} +5 -5
  64. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  65. package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.js → railroadDiagram-O6MQD6OU-DqhZGsN2.js} +2 -2
  66. package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
  67. package/dist/{requirementDiagram-BXWQKSXE-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CL5dYARV.js} +1 -1
  68. package/dist/{reveal-component-Bs9X_N3n.js → reveal-component-C2qjH1P6.js} +14 -14
  69. package/dist/{select-BButFi-d.js → select-CkF_GBxO.js} +537 -458
  70. package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-BSUCRd2x.js} +1 -1
  71. package/dist/{spec-BwarBpUJ.js → spec-DiSh_qJC.js} +41 -11
  72. package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-B-1Hvyb0.js} +2 -2
  73. package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH--HtiR4yB.js} +2 -2
  74. package/dist/style.css +1 -1
  75. package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-Bdfhrd-2.js} +1 -1
  76. package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.js → swimlanesDiagram-VR7AAH4N-BJIfxSz0.js} +2 -2
  77. package/dist/{swiper-component-DKntyn8p.js → swiper-component-BkCm-kLg.js} +2 -2
  78. package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
  79. package/dist/{timeline-definition-24CTP7MA-D3c46WM5.js → timeline-definition-24CTP7MA-Cs4EfFD3.js} +1 -1
  80. package/dist/{toDate-UtiwmnH2.js → toDate-DGvRHwTk.js} +19 -18
  81. package/dist/{toast-B9UMEi8u.js → toast-Btbve3Lm.js} +231 -213
  82. package/dist/{tooltip-BO1REbqs.js → tooltip-CJ4Rqasp.js} +327 -303
  83. package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-CmGwRNId.js} +1482 -1433
  84. package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-BmVgnMP5.js} +15 -14
  85. package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-eCL_SdeG.js} +2 -2
  86. package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-Ch_AeGu6.js} +1 -1
  87. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-CSdWn6NP.js} +1 -1
  88. package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-B5BX921H.js} +3 -3
  89. package/dist/{useTheme-CRnjkku0.js → useTheme-Bq9pi_vz.js} +6 -6
  90. package/dist/{vega-component-BeF9dfyO.js → vega-component-BgyZ-COt.js} +147 -147
  91. package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
  92. package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
  93. package/dist/{vennDiagram-4TSXK5OY-BSZD8JcZ.js → vennDiagram-4TSXK5OY-DRu0vRjO.js} +1 -1
  94. package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-DzbDxwDY.js} +1 -1
  95. package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.js → xychartDiagram-S5SC5T6Z-CLWj6ZFQ.js} +2 -2
  96. package/package.json +14 -14
  97. package/src/__tests__/host-imports.test.ts +1 -0
  98. package/src/__tests__/module-entry.test.ts +32 -0
  99. package/src/__tests__/mount.test.ts +0 -20
  100. package/src/components/ai/__tests__/ai-model-dropdown.test.tsx +63 -0
  101. package/src/components/ai/ai-model-dropdown.tsx +11 -6
  102. package/src/components/app-config/ai-config.tsx +7 -2
  103. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  104. package/src/components/chat/acp/__tests__/session-tabs.test.tsx +43 -0
  105. package/src/components/chat/acp/__tests__/state.test.ts +2 -2
  106. package/src/components/chat/acp/blocks.tsx +3 -2
  107. package/src/components/chat/acp/session-tabs.tsx +8 -3
  108. package/src/components/chat/chat-components.tsx +2 -0
  109. package/src/components/chat/chat-panel.tsx +9 -8
  110. package/src/components/chat/chat-utils.ts +4 -8
  111. package/src/components/data-table/TableTopBar.tsx +2 -0
  112. package/src/components/data-table/__tests__/chart-spec-model.test.ts +195 -195
  113. package/src/components/data-table/__tests__/column-explorer.test.tsx +27 -1
  114. package/src/components/data-table/__tests__/columns.test.tsx +105 -0
  115. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  116. package/src/components/data-table/__tests__/data-table.test.tsx +1 -0
  117. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  118. package/src/components/data-table/__tests__/export-actions.test.tsx +105 -0
  119. package/src/components/data-table/__tests__/keyboard-selection.test.tsx +188 -0
  120. package/src/components/data-table/__tests__/search-highlight.test.tsx +109 -0
  121. package/src/components/data-table/__tests__/url-detector.test.tsx +74 -0
  122. package/src/components/data-table/cell-selection/feature.ts +2 -2
  123. package/src/components/data-table/cell-selection/types.ts +2 -2
  124. package/src/components/data-table/charts/__tests__/chart-panel.test.tsx +74 -1
  125. package/src/components/data-table/charts/__tests__/spec-snapshot.test.ts +20 -20
  126. package/src/components/data-table/charts/__tests__/spec.test.ts +78 -52
  127. package/src/components/data-table/charts/__tests__/storage.test.ts +4 -1
  128. package/src/components/data-table/charts/chart-spec/spec.ts +68 -43
  129. package/src/components/data-table/charts/charts.tsx +40 -30
  130. package/src/components/data-table/charts/storage.ts +16 -5
  131. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +1 -1
  132. package/src/components/data-table/column-summary/chart-spec-model.tsx +23 -16
  133. package/src/components/data-table/column-summary/column-summary.tsx +2 -4
  134. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  135. package/src/components/data-table/columns.tsx +45 -46
  136. package/src/components/data-table/context-menu.tsx +36 -20
  137. package/src/components/data-table/data-table.tsx +13 -1
  138. package/src/components/data-table/date-popover.tsx +6 -4
  139. package/src/components/data-table/export-actions.tsx +76 -48
  140. package/src/components/data-table/range-focus/atoms.ts +4 -6
  141. package/src/components/data-table/range-focus/use-cell-range-selection.ts +65 -38
  142. package/src/components/data-table/range-focus/use-scroll-into-view.ts +21 -2
  143. package/src/components/data-table/renderers.tsx +16 -7
  144. package/src/components/data-table/row-viewer-panel/__tests__/row-viewer.test.tsx +24 -1
  145. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +4 -4
  146. package/src/components/data-table/search-highlight.tsx +79 -0
  147. package/src/components/data-table/types.ts +1 -0
  148. package/src/components/data-table/url-detector.tsx +49 -9
  149. package/src/components/datasources/__tests__/column-actions.test.tsx +97 -0
  150. package/src/components/datasources/__tests__/column-preview.test.tsx +98 -5
  151. package/src/components/datasources/__tests__/datasources-interactions.test.tsx +181 -0
  152. package/src/components/datasources/__tests__/table-list.test.tsx +118 -0
  153. package/src/components/datasources/__tests__/utils.test.ts +57 -6
  154. package/src/components/datasources/column-preview.tsx +13 -4
  155. package/src/components/datasources/datasources.tsx +51 -39
  156. package/src/components/datasources/utils.ts +32 -23
  157. package/src/components/debugger/debugger-code.tsx +5 -0
  158. package/src/components/dependency-graph/dependency-graph-tree.tsx +8 -8
  159. package/src/components/dependency-graph/elements.ts +24 -21
  160. package/src/components/dependency-graph/panels.tsx +2 -1
  161. package/src/components/dependency-graph/utils/__tests__/layout.test.ts +18 -11
  162. package/src/components/editor/SortableCell.tsx +1 -3
  163. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  164. package/src/components/editor/__tests__/package-alert.test.tsx +22 -1
  165. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +24 -0
  166. package/src/components/editor/actions/pair-with-agent-modal.tsx +18 -15
  167. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  168. package/src/components/editor/ai/ai-completion-editor.tsx +1 -0
  169. package/src/components/editor/alerts/startup-logs-alert.tsx +1 -0
  170. package/src/components/editor/cell/CreateCellButton.tsx +8 -10
  171. package/src/components/editor/cell/DeleteButton.tsx +1 -0
  172. package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
  173. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  174. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  175. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  176. package/src/components/editor/cell/collapse.tsx +5 -3
  177. package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
  178. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +30 -1
  179. package/src/components/editor/chrome/panels/cache-panel.tsx +1 -0
  180. package/src/components/editor/chrome/panels/outline/__tests__/outline-keyboard.test.tsx +84 -0
  181. package/src/components/editor/chrome/panels/outline/floating-outline.tsx +39 -14
  182. package/src/components/editor/chrome/panels/outline/useActiveOutline.tsx +3 -2
  183. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
  184. package/src/components/editor/chrome/panels/secrets-panel.tsx +1 -0
  185. package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -1
  186. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +12 -18
  187. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +58 -1
  188. package/src/components/editor/chrome/wrapper/footer-items/runtime-settings.tsx +1 -7
  189. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +12 -2
  190. package/src/components/editor/chrome/wrapper/storage.ts +1 -1
  191. package/src/components/editor/connections/components.tsx +1 -1
  192. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  193. package/src/components/editor/controls/__tests__/keyboard-shortcuts.test.tsx +61 -0
  194. package/src/components/editor/controls/command-palette.tsx +22 -10
  195. package/src/components/editor/controls/keyboard-shortcuts.tsx +14 -5
  196. package/src/components/editor/dynamic-favicon.tsx +21 -40
  197. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +12 -23
  198. package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
  199. package/src/components/editor/file-tree/__tests__/file-explorer-navigation.test.tsx +47 -0
  200. package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
  201. package/src/components/editor/file-tree/file-explorer.tsx +45 -8
  202. package/src/components/editor/file-tree/file-name-input.tsx +1 -0
  203. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  204. package/src/components/editor/file-tree/use-file-search.ts +35 -21
  205. package/src/components/editor/header/status.tsx +28 -5
  206. package/src/components/editor/links/__tests__/cell-link.test.tsx +146 -0
  207. package/src/components/editor/links/cell-link.tsx +14 -5
  208. package/src/components/editor/navigation/focus-utils.ts +4 -4
  209. package/src/components/editor/notebook-banner.tsx +1 -0
  210. package/src/components/editor/notebook-cell.tsx +11 -2
  211. package/src/components/editor/output/MarimoTracebackOutput.tsx +19 -17
  212. package/src/components/editor/output/__tests__/traceback.test.tsx +27 -1
  213. package/src/components/editor/output/ansi-reduce.ts +33 -20
  214. package/src/components/editor/output/console/ConsoleOutput.tsx +19 -25
  215. package/src/components/editor/package-alert.tsx +14 -7
  216. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +12 -6
  217. package/src/components/editor/renderers/vertical-layout/sidebar/toggle.tsx +7 -1
  218. package/src/components/home/components.tsx +17 -31
  219. package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
  220. package/src/components/icons/large-spinner.tsx +3 -14
  221. package/src/components/icons/spinner.tsx +1 -1
  222. package/src/components/mcp/mcp-status-indicator.tsx +1 -1
  223. package/src/components/pages/__tests__/home-page.test.tsx +116 -0
  224. package/src/components/pages/home-page.tsx +164 -91
  225. package/src/components/scratchpad/scratchpad.test.tsx +95 -0
  226. package/src/components/scratchpad/scratchpad.tsx +16 -3
  227. package/src/components/slides/reveal-component.tsx +4 -2
  228. package/src/components/slides/slide-cell-view.tsx +1 -1
  229. package/src/components/slides/slide-form.tsx +1 -0
  230. package/src/components/tracing/tracing-keyboard.test.tsx +85 -0
  231. package/src/components/tracing/tracing.tsx +45 -41
  232. package/src/components/ui/__tests__/draggable-popover.test.tsx +24 -0
  233. package/src/components/ui/alert-dialog.tsx +1 -1
  234. package/src/components/ui/draggable-popover.tsx +24 -2
  235. package/src/core/ai/context/providers/datasource.ts +6 -2
  236. package/src/core/ai/tools/base.ts +16 -7
  237. package/src/core/ai/tools/edit-notebook-tool.ts +47 -45
  238. package/src/core/ai/tools/registry.ts +13 -8
  239. package/src/core/ai/tools/sample-tool.ts +6 -7
  240. package/src/core/attachments/state.ts +5 -8
  241. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +4 -0
  242. package/src/core/cells/__tests__/cell.test.ts +39 -0
  243. package/src/core/cells/__tests__/cells.test.ts +3 -1
  244. package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +54 -0
  245. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  246. package/src/core/cells/__tests__/session.test.ts +117 -36
  247. package/src/core/cells/cell.ts +14 -3
  248. package/src/core/cells/cells.ts +3 -1
  249. package/src/core/cells/pending-delete-service.ts +2 -6
  250. package/src/core/cells/session.ts +1 -1
  251. package/src/core/cells/types.ts +10 -1
  252. package/src/core/codemirror/__tests__/setup.test.ts +29 -29
  253. package/src/core/codemirror/cells/__tests__/extensions.test.ts +15 -0
  254. package/src/core/codemirror/cells/extensions.ts +7 -3
  255. package/src/core/codemirror/keymaps/vim.ts +2 -6
  256. package/src/core/codemirror/language/__tests__/sql.test.ts +22 -20
  257. package/src/core/codemirror/language/extension.ts +8 -8
  258. package/src/core/codemirror/language/languages/markdown.ts +8 -6
  259. package/src/core/codemirror/language/languages/python.ts +16 -10
  260. package/src/core/codemirror/language/languages/sql/sql.ts +9 -7
  261. package/src/core/codemirror/language/panel/panel.tsx +1 -0
  262. package/src/core/codemirror/language/types.ts +7 -7
  263. package/src/core/codemirror/rtc/extension.ts +24 -12
  264. package/src/core/codemirror/rtc/loro/awareness.ts +34 -19
  265. package/src/core/codemirror/rtc/loro/sync.ts +1 -0
  266. package/src/core/codemirror/utils.ts +1 -0
  267. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  268. package/src/core/datasets/data-source-discovery.ts +4 -6
  269. package/src/core/document/__tests__/codemirror.test.ts +485 -0
  270. package/src/core/document/__tests__/draft.test.ts +322 -0
  271. package/src/core/document/__tests__/fixtures.ts +92 -0
  272. package/src/core/document/__tests__/hooks.test.tsx +386 -0
  273. package/src/core/document/__tests__/lifecycle-scenarios.test.ts +61 -0
  274. package/src/core/document/__tests__/model.test.ts +191 -0
  275. package/src/core/document/__tests__/protocol-fixtures.ts +38 -0
  276. package/src/core/document/__tests__/recovery-scenarios.test.ts +230 -0
  277. package/src/core/document/__tests__/scenario-fixture.ts +43 -0
  278. package/src/core/document/__tests__/scenarios.test.ts +205 -0
  279. package/src/core/document/__tests__/state.test.ts +438 -0
  280. package/src/core/document/__tests__/submission.test.ts +203 -0
  281. package/src/core/document/__tests__/test-host.test.ts +92 -0
  282. package/src/core/document/__tests__/test-host.ts +247 -0
  283. package/src/core/document/__tests__/test-notebook.ts +149 -0
  284. package/src/core/document/__tests__/user.ts +61 -0
  285. package/src/core/document/codemirror.ts +311 -0
  286. package/src/core/document/draft.ts +472 -0
  287. package/src/core/document/hooks.tsx +118 -0
  288. package/src/core/document/index.ts +15 -0
  289. package/src/core/document/model.ts +55 -0
  290. package/src/core/document/select.ts +41 -0
  291. package/src/core/document/state.ts +374 -0
  292. package/src/core/document/sync.ts +369 -0
  293. package/src/core/host/AGENTS.md +8 -2
  294. package/src/core/host/__tests__/attachments.test.ts +56 -0
  295. package/src/core/host/__tests__/document.test.ts +126 -0
  296. package/src/core/host/__tests__/fixtures.ts +71 -0
  297. package/src/core/host/__tests__/replica.test.ts +550 -0
  298. package/src/core/host/__tests__/static.test.ts +169 -0
  299. package/src/core/host/__tests__/wasm.test.ts +162 -0
  300. package/src/core/host/backend.ts +19 -0
  301. package/src/core/host/connection.ts +1 -40
  302. package/src/core/host/index.ts +14 -0
  303. package/src/core/host/replica.ts +286 -0
  304. package/src/core/host/resources/attachments.ts +22 -0
  305. package/src/core/host/resources/document.ts +101 -0
  306. package/src/core/host/static.ts +72 -0
  307. package/src/core/host/wasm.ts +102 -0
  308. package/src/core/hotkeys/actions.ts +2 -4
  309. package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
  310. package/src/core/islands/bootstrap.ts +0 -1
  311. package/src/core/islands/components/__tests__/IslandControls.test.tsx +7 -5
  312. package/src/core/kernel/__tests__/RuntimeState.test.ts +1 -1
  313. package/src/core/network/connection.ts +4 -2
  314. package/src/core/network/useConnectionNotice.ts +8 -2
  315. package/src/core/runtime/__tests__/createWsUrl.test.ts +77 -7
  316. package/src/core/runtime/__tests__/runtime.test.ts +32 -58
  317. package/src/core/runtime/runtime.ts +38 -42
  318. package/src/core/runtime/types.ts +0 -2
  319. package/src/core/state/__tests__/jotai.test.tsx +108 -0
  320. package/src/core/state/jotai.ts +1 -1
  321. package/src/core/static/__tests__/files.test.ts +38 -10
  322. package/src/core/static/files.ts +2 -0
  323. package/src/core/wasm/bridge.ts +38 -10
  324. package/src/core/wasm/store.ts +3 -3
  325. package/src/core/wasm/worker/bootstrap.ts +2 -1
  326. package/src/core/wasm/worker/getMarimoWheel.ts +1 -1
  327. package/src/core/wasm/worker/types.ts +11 -0
  328. package/src/core/wasm/worker/worker.ts +48 -0
  329. package/src/core/websocket/__tests__/backend.test.ts +200 -0
  330. package/src/core/websocket/__tests__/connection-state.test.ts +0 -12
  331. package/src/core/websocket/__tests__/useNotebookReplica.hook.test.tsx +141 -0
  332. package/src/core/websocket/__tests__/useNotebookSession.hook.test.tsx +154 -428
  333. package/src/core/websocket/backend.ts +106 -0
  334. package/src/core/websocket/connection-state.ts +1 -11
  335. package/src/core/websocket/state.ts +7 -0
  336. package/src/core/websocket/transports/__tests__/notebook.test.ts +445 -0
  337. package/src/core/websocket/transports/notebook.ts +190 -0
  338. package/src/core/websocket/types.ts +2 -2
  339. package/src/core/websocket/useNotebookReplica.ts +69 -0
  340. package/src/core/websocket/useNotebookSession.tsx +10 -22
  341. package/src/css/globals.css +5 -0
  342. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  343. package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
  344. package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
  345. package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
  346. package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
  347. package/src/hooks/__tests__/useScript.test.tsx +77 -0
  348. package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
  349. package/src/hooks/useAsyncData.ts +3 -3
  350. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  351. package/src/hooks/useDebounce.ts +6 -10
  352. package/src/hooks/useDeepCompareMemoize.ts +1 -1
  353. package/src/hooks/useDelayElapsed.ts +14 -10
  354. package/src/hooks/useElapsedTime.ts +3 -3
  355. package/src/hooks/useEventListener.ts +1 -2
  356. package/src/hooks/useInputHistory.ts +4 -7
  357. package/src/hooks/useInternalStateWithSync.ts +6 -6
  358. package/src/hooks/useResizeObserver.ts +4 -7
  359. package/src/hooks/useScript.ts +67 -70
  360. package/src/hooks/useStateForKey.ts +93 -0
  361. package/src/mount.tsx +0 -4
  362. package/src/plugins/__tests__/plugin-object-schemas.test.ts +104 -0
  363. package/src/plugins/core/registerReactComponent.tsx +1 -1
  364. package/src/plugins/impl/CodeEditorPlugin.tsx +1 -1
  365. package/src/plugins/impl/DataEditorPlugin.tsx +1 -1
  366. package/src/plugins/impl/DataTablePlugin.tsx +29 -33
  367. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  368. package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
  369. package/src/plugins/impl/FormPlugin.tsx +1 -1
  370. package/src/plugins/impl/RangeSliderPlugin.tsx +0 -1
  371. package/src/plugins/impl/RefreshPlugin.tsx +1 -0
  372. package/src/plugins/impl/SliderPlugin.tsx +6 -8
  373. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +117 -1
  374. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
  375. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
  376. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
  377. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  378. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  379. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  380. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
  381. package/src/plugins/impl/chat/chat-ui.tsx +14 -11
  382. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  383. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  384. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  385. package/src/plugins/impl/data-explorer/ConnectedDataExplorerComponent.tsx +1 -1
  386. package/src/plugins/impl/data-explorer/components/column-summary.tsx +4 -2
  387. package/src/plugins/impl/data-explorer/state/reducer.ts +1 -1
  388. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -2
  389. package/src/plugins/impl/data-frames/__tests__/sidebar.test.tsx +160 -0
  390. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +66 -66
  391. package/src/plugins/impl/data-frames/panel.tsx +43 -16
  392. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  393. package/src/plugins/impl/panel/PanelPlugin.tsx +16 -17
  394. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  395. package/src/plugins/impl/panel/utils.ts +17 -0
  396. package/src/plugins/impl/plotly/PlotlyPlugin.tsx +2 -5
  397. package/src/plugins/impl/plotly/selection.ts +25 -19
  398. package/src/plugins/impl/vega/VegaPlugin.tsx +2 -3
  399. package/src/plugins/impl/vega/make-selectable.ts +4 -7
  400. package/src/plugins/layout/RoutesPlugin.tsx +1 -1
  401. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  402. package/src/stories/button.stories.tsx +1 -0
  403. package/src/stories/editor.stories.tsx +24 -17
  404. package/src/stories/form-wrapper.stories.tsx +2 -2
  405. package/src/stories/select.stories.tsx +2 -2
  406. package/src/stories/switchable-multi-select.stories.tsx +1 -1
  407. package/src/utils/__tests__/arrays.test.ts +35 -0
  408. package/src/utils/__tests__/batch-requests.test.ts +3 -3
  409. package/src/utils/__tests__/numbers.test.ts +48 -1
  410. package/src/utils/__tests__/objects.test.ts +21 -0
  411. package/src/utils/arrays.ts +10 -0
  412. package/src/utils/blob.ts +1 -1
  413. package/src/utils/numbers.ts +28 -17
  414. package/src/utils/objects.ts +3 -7
  415. package/src/utils/once.ts +19 -19
  416. package/src/utils/tracer.ts +9 -5
  417. package/src/utils/zod-utils.ts +1 -3
  418. package/dist/any-language-editor-DZ_YTVus.js +0 -76
  419. package/dist/assets/__vite-browser-external-BXYkiFsk.js +0 -1
  420. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  421. package/dist/precisionRound-DMIVQrt2.js +0 -97
  422. package/src/core/host/__tests__/events.test.ts +0 -46
  423. package/src/core/host/__tests__/notebook-client.test.ts +0 -191
  424. package/src/core/host/__tests__/protocol.test.ts +0 -214
  425. package/src/core/host/events.ts +0 -60
  426. package/src/core/host/notebook-client.ts +0 -226
  427. package/src/core/host/protocol.ts +0 -1943
  428. package/src/core/state/__tests__/jotai.test.ts +0 -25
  429. package/src/core/websocket/__tests__/createConnectionTransport.test.ts +0 -49
  430. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +0 -140
  431. package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
  432. package/src/core/websocket/transports/__tests__/sse.test.ts +0 -335
  433. package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
  434. package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
  435. package/src/core/websocket/transports/basic.ts +0 -102
  436. package/src/core/websocket/transports/create.ts +0 -31
  437. package/src/core/websocket/transports/sse.ts +0 -308
  438. package/src/core/websocket/transports/transport.ts +0 -50
  439. package/src/core/websocket/transports/ws.ts +0 -98
  440. package/src/core/websocket/useNotebookClient.ts +0 -80
@@ -52,4 +52,32 @@ describe("useDelayElapsed", () => {
52
52
  // The pending timeout must be cleared, not left dangling.
53
53
  expect(vi.getTimerCount()).toBe(0);
54
54
  });
55
+
56
+ it("handles a NaN delay without resetting on every render", () => {
57
+ const { result, rerender } = renderHook(() => useDelayElapsed(Number.NaN));
58
+ expect(result.current).toBe(false);
59
+
60
+ act(() => vi.runAllTimers());
61
+ expect(result.current).toBe(true);
62
+ rerender();
63
+ expect(result.current).toBe(true);
64
+ expect(vi.getTimerCount()).toBe(0);
65
+ });
66
+
67
+ it("restarts a completed delay after the gate is temporarily disabled", () => {
68
+ const { result, rerender } = renderHook(
69
+ ({ delay }) => useDelayElapsed(delay),
70
+ { initialProps: { delay: 1000 } },
71
+ );
72
+ act(() => vi.advanceTimersByTime(1000));
73
+ expect(result.current).toBe(true);
74
+ rerender({ delay: 0 });
75
+ expect(result.current).toBe(true);
76
+ rerender({ delay: 1000 });
77
+ expect(result.current).toBe(false);
78
+ act(() => vi.advanceTimersByTime(999));
79
+ expect(result.current).toBe(false);
80
+ act(() => vi.advanceTimersByTime(1));
81
+ expect(result.current).toBe(true);
82
+ });
55
83
  });
@@ -32,6 +32,26 @@ describe("useInternalStateWithSync", () => {
32
32
  expect(result.current[0]).toBe("new value");
33
33
  });
34
34
 
35
+ it("preserves function values without calling them and resets when they change", () => {
36
+ const initial = vi.fn(() => "initial");
37
+ const draft = vi.fn(() => "draft");
38
+ const changed = vi.fn(() => "changed");
39
+ const { result, rerender } = renderHook(
40
+ ({ value }) => useInternalStateWithSync(value),
41
+ { initialProps: { value: initial } },
42
+ );
43
+
44
+ expect(result.current[0]).toBe(initial);
45
+ act(() => result.current[1](() => draft));
46
+ rerender({ value: initial });
47
+ expect(result.current[0]).toBe(draft);
48
+ rerender({ value: changed });
49
+ expect(result.current[0]).toBe(changed);
50
+ expect(initial).not.toHaveBeenCalled();
51
+ expect(draft).not.toHaveBeenCalled();
52
+ expect(changed).not.toHaveBeenCalled();
53
+ });
54
+
35
55
  it("should not update internal state if initial value remains the same", () => {
36
56
  const { result, rerender } = renderHook(
37
57
  ({ value }) => useInternalStateWithSync(value),
@@ -0,0 +1,77 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { act, renderHook } from "@testing-library/react";
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import { useScript } from "../useScript";
5
+
6
+ afterEach(() => {
7
+ document.querySelectorAll("script").forEach((script) => script.remove());
8
+ });
9
+
10
+ describe("useScript", () => {
11
+ it("observes loading and completion of a new script", async () => {
12
+ const { result } = renderHook(() => useScript("/first.js"));
13
+ expect(result.current).toBe("loading");
14
+ await act(async () => {
15
+ document.querySelector("script")?.dispatchEvent(new Event("load"));
16
+ });
17
+ expect(result.current).toBe("ready");
18
+ });
19
+
20
+ it("uses the new source's status when the source changes", async () => {
21
+ const { result, rerender } = renderHook(({ src }) => useScript(src), {
22
+ initialProps: { src: "/first.js" },
23
+ });
24
+ await act(async () => {
25
+ document.querySelector("script")?.dispatchEvent(new Event("load"));
26
+ });
27
+ expect(result.current).toBe("ready");
28
+ rerender({ src: "/second.js" });
29
+ expect(result.current).toBe("loading");
30
+ await act(async () => {
31
+ document
32
+ .querySelector('script[src="/second.js"]')
33
+ ?.dispatchEvent(new Event("error"));
34
+ });
35
+ expect(result.current).toBe("error");
36
+ });
37
+
38
+ it("shares an existing script and only removes scripts it owns", async () => {
39
+ const owner = renderHook(() =>
40
+ useScript("/first.js", { removeOnUnmount: true }),
41
+ );
42
+ const subscriber = renderHook(() =>
43
+ useScript("/first.js", { removeOnUnmount: true }),
44
+ );
45
+ expect(document.querySelectorAll("script")).toHaveLength(1);
46
+ await act(async () => {
47
+ document.querySelector("script")?.dispatchEvent(new Event("load"));
48
+ });
49
+ expect(subscriber.result.current).toBe("ready");
50
+ subscriber.unmount();
51
+ expect(document.querySelectorAll("script")).toHaveLength(1);
52
+ owner.unmount();
53
+ expect(document.querySelectorAll("script")).toHaveLength(0);
54
+ });
55
+
56
+ it("settles for remaining subscribers when the first unmounts mid-load", async () => {
57
+ const first = renderHook(() => useScript("/shared.js"));
58
+ const second = renderHook(() => useScript("/shared.js"));
59
+ first.unmount();
60
+ expect(second.result.current).toBe("loading");
61
+ await act(async () => {
62
+ document.querySelector("script")?.dispatchEvent(new Event("load"));
63
+ });
64
+ expect(second.result.current).toBe("ready");
65
+ // A later subscriber sees the settled status
66
+ expect(renderHook(() => useScript("/shared.js")).result.current).toBe(
67
+ "ready",
68
+ );
69
+ });
70
+
71
+ it("handles sources that are not valid inside a CSS selector", () => {
72
+ const src = '/odd"name].js';
73
+ const { result } = renderHook(() => useScript(src));
74
+ expect(result.current).toBe("loading");
75
+ expect(document.querySelector("script")?.getAttribute("src")).toBe(src);
76
+ });
77
+ });
@@ -0,0 +1,257 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import {
3
+ act,
4
+ fireEvent,
5
+ render,
6
+ renderHook,
7
+ screen,
8
+ } from "@testing-library/react";
9
+ import {
10
+ Profiler,
11
+ startTransition,
12
+ StrictMode,
13
+ Suspense,
14
+ useLayoutEffect,
15
+ useState,
16
+ } from "react";
17
+ import { describe, expect, it, vi } from "vitest";
18
+ import { Deferred } from "@/utils/Deferred";
19
+ import { useStateForKey } from "../useStateForKey";
20
+
21
+ describe("useStateForKey", () => {
22
+ it("preserves edits for the same key and creates fresh state on key changes", () => {
23
+ const initialize = vi.fn(() => new Set<string>());
24
+ const { result, rerender } = renderHook(
25
+ ({ key }) => useStateForKey(key, initialize),
26
+ { initialProps: { key: "first" } },
27
+ );
28
+ const initial = result.current[0];
29
+ act(() => result.current[1](new Set(["expanded"])));
30
+ rerender({ key: "first" });
31
+ expect(result.current[0]).toEqual(new Set(["expanded"]));
32
+ expect(initialize).toHaveBeenCalledOnce();
33
+
34
+ rerender({ key: "second" });
35
+ expect(result.current[0]).toEqual(new Set());
36
+ expect(result.current[0]).not.toBe(initial);
37
+ expect(initialize).toHaveBeenCalledTimes(2);
38
+
39
+ rerender({ key: "first" });
40
+ expect(result.current[0]).toEqual(new Set());
41
+ expect(initialize).toHaveBeenCalledTimes(3);
42
+ });
43
+
44
+ it("preserves queued functional updates and ignores setters from another key", () => {
45
+ const { result, rerender } = renderHook(
46
+ ({ key }) => useStateForKey(key, () => 0),
47
+ { initialProps: { key: "first" } },
48
+ );
49
+ const oldSetter = result.current[1];
50
+ act(() => {
51
+ result.current[1]((value) => value + 1);
52
+ result.current[1]((value) => value + 1);
53
+ });
54
+ expect(result.current[0]).toBe(2);
55
+
56
+ rerender({ key: "second" });
57
+ const staleUpdate = vi.fn(() => 99);
58
+ act(() => oldSetter(staleUpdate));
59
+ act(() => oldSetter(99));
60
+ expect(result.current[0]).toBe(0);
61
+ expect(staleUpdate).not.toHaveBeenCalled();
62
+ });
63
+
64
+ it("uses Object.is for NaN and signed zero keys", () => {
65
+ const { result, rerender } = renderHook(
66
+ ({ key }) => useStateForKey(key, () => "initial"),
67
+ { initialProps: { key: Number.NaN } },
68
+ );
69
+ act(() => result.current[1]("draft"));
70
+ rerender({ key: Number.NaN });
71
+ expect(result.current[0]).toBe("draft");
72
+
73
+ rerender({ key: 0 });
74
+ expect(result.current[0]).toBe("initial");
75
+ act(() => result.current[1]("draft"));
76
+ rerender({ key: -0 });
77
+ expect(result.current[0]).toBe("initial");
78
+ });
79
+
80
+ it("stores function values without invoking them", () => {
81
+ const initial = vi.fn();
82
+ const changed = vi.fn();
83
+ const { result, rerender } = renderHook(
84
+ ({ key, value }) => useStateForKey(key, () => value),
85
+ { initialProps: { key: "first", value: initial } },
86
+ );
87
+ expect(result.current[0]).toBe(initial);
88
+ act(() => result.current[1](() => changed));
89
+ expect(result.current[0]).toBe(changed);
90
+ rerender({ key: "second", value: initial });
91
+ expect(result.current[0]).toBe(initial);
92
+ expect(initial).not.toHaveBeenCalled();
93
+ expect(changed).not.toHaveBeenCalled();
94
+ });
95
+
96
+ it("uses a changed initializer only after the key changes", () => {
97
+ const first = vi.fn(() => "first default");
98
+ const second = vi.fn(() => "second default");
99
+ const { result, rerender } = renderHook(
100
+ ({ key, initialize }) => useStateForKey(key, initialize),
101
+ { initialProps: { key: "A", initialize: first } },
102
+ );
103
+ act(() => result.current[1]("draft"));
104
+ rerender({ key: "A", initialize: second });
105
+ expect(result.current[0]).toBe("draft");
106
+ expect(second).not.toHaveBeenCalled();
107
+ rerender({ key: "B", initialize: second });
108
+ expect(result.current[0]).toBe("second default");
109
+ expect(second).toHaveBeenCalledOnce();
110
+ });
111
+
112
+ it("compares object keys by reference rather than their contents", () => {
113
+ const key = { id: "A" };
114
+ const { result, rerender } = renderHook(
115
+ ({ key }) => useStateForKey(key, () => 0),
116
+ { initialProps: { key } },
117
+ );
118
+ act(() => result.current[1](1));
119
+ rerender({ key });
120
+ expect(result.current[0]).toBe(1);
121
+ rerender({ key: { id: "A" } });
122
+ expect(result.current[0]).toBe(0);
123
+ });
124
+
125
+ it("keeps setters stable for the same key and accepts old setters when that key returns", () => {
126
+ const { result, rerender } = renderHook(
127
+ ({ key }) => useStateForKey(key, () => 0),
128
+ { initialProps: { key: "A" } },
129
+ );
130
+ const setA = result.current[1];
131
+ act(() => setA(1));
132
+ rerender({ key: "A" });
133
+ expect(result.current[1]).toBe(setA);
134
+ rerender({ key: "B" });
135
+ expect(result.current[1]).not.toBe(setA);
136
+ act(() => setA(2));
137
+ expect(result.current[0]).toBe(0);
138
+ rerender({ key: "A" });
139
+ act(() => setA(3));
140
+ expect(result.current[0]).toBe(3);
141
+ });
142
+
143
+ it("retries the owner without rendering or committing stale state to children", () => {
144
+ const onOwnerRender = vi.fn();
145
+ const onChildRender = vi.fn();
146
+ const onCommit = vi.fn();
147
+ const onLayout = vi.fn();
148
+ function Child({ owner, value }: { owner: string; value: string }) {
149
+ onChildRender(owner, value);
150
+ useLayoutEffect(() => onLayout(owner, value), [owner, value]);
151
+ return <span>{value}</span>;
152
+ }
153
+ function Owner({ owner }: { owner: string }) {
154
+ onOwnerRender();
155
+ const [value, setValue] = useStateForKey(owner, () => `${owner} default`);
156
+ return (
157
+ <>
158
+ <button onClick={() => setValue("draft")}>Edit</button>
159
+ <Child owner={owner} value={value} />
160
+ </>
161
+ );
162
+ }
163
+ const content = (owner: string) => (
164
+ <Profiler id="owner" onRender={onCommit}>
165
+ <Owner owner={owner} />
166
+ </Profiler>
167
+ );
168
+ const { rerender } = render(content("A"));
169
+ const button = screen.getByRole("button", { name: "Edit" });
170
+ fireEvent.click(button);
171
+ expect(screen.getByText("draft")).toBeInTheDocument();
172
+ onOwnerRender.mockClear();
173
+ onChildRender.mockClear();
174
+ onCommit.mockClear();
175
+ onLayout.mockClear();
176
+
177
+ rerender(content("B"));
178
+ expect(onOwnerRender.mock.calls.length).toBeGreaterThan(1);
179
+ expect(onChildRender.mock.calls).toEqual([["B", "B default"]]);
180
+ expect(onLayout.mock.calls).toEqual([["B", "B default"]]);
181
+ expect(onCommit).toHaveBeenCalledOnce();
182
+ expect(screen.getByRole("button", { name: "Edit" })).toBe(button);
183
+ });
184
+
185
+ it("keeps state correct when Strict Mode repeats initialization and updates", () => {
186
+ const initialize = vi.fn(() => new Set<string>());
187
+ const { result, rerender } = renderHook(
188
+ ({ key }) => useStateForKey(key, initialize),
189
+ { initialProps: { key: "A" }, wrapper: StrictMode },
190
+ );
191
+ expect(initialize.mock.calls.length).toBeGreaterThan(1);
192
+ act(() => result.current[1]((previous) => new Set([...previous, "draft"])));
193
+ expect(result.current[0]).toEqual(new Set(["draft"]));
194
+ rerender({ key: "B" });
195
+ expect(result.current[0]).toEqual(new Set());
196
+ });
197
+
198
+ it.each(["completes", "is canceled"])(
199
+ "preserves visible edits when a suspended transition %s",
200
+ async (outcome) => {
201
+ const pending = new Deferred<void>();
202
+ let blocked = true;
203
+ const suspended = vi.fn();
204
+ function Pending({ owner }: { owner: string }) {
205
+ if (owner === "B" && blocked) {
206
+ suspended();
207
+ throw pending.promise;
208
+ }
209
+ return null;
210
+ }
211
+ function Owner({ owner }: { owner: string }) {
212
+ const [value, setValue] = useStateForKey(owner, () => 0);
213
+ return (
214
+ <>
215
+ <button onClick={() => setValue((previous) => previous + 1)}>
216
+ {owner}:{value}
217
+ </button>
218
+ <Pending owner={owner} />
219
+ </>
220
+ );
221
+ }
222
+ function Harness() {
223
+ const [owner, setOwner] = useState("A");
224
+ return (
225
+ <>
226
+ <button onClick={() => startTransition(() => setOwner("B"))}>
227
+ Switch owner
228
+ </button>
229
+ <button onClick={() => setOwner("A")}>Keep owner</button>
230
+ <Suspense fallback="Loading">
231
+ <Owner owner={owner} />
232
+ </Suspense>
233
+ </>
234
+ );
235
+ }
236
+ render(<Harness />);
237
+ fireEvent.click(screen.getByRole("button", { name: "A:0" }));
238
+ fireEvent.click(screen.getByRole("button", { name: "Switch owner" }));
239
+ expect(suspended).toHaveBeenCalled();
240
+ fireEvent.click(screen.getByRole("button", { name: "A:1" }));
241
+ expect(screen.getByRole("button", { name: "A:2" })).toBeInTheDocument();
242
+ if (outcome === "is canceled") {
243
+ fireEvent.click(screen.getByRole("button", { name: "Keep owner" }));
244
+ }
245
+ await act(async () => {
246
+ blocked = false;
247
+ pending.resolve();
248
+ await pending.promise;
249
+ });
250
+ expect(
251
+ screen.getByRole("button", {
252
+ name: outcome === "completes" ? "B:0" : "A:2",
253
+ }),
254
+ ).toBeInTheDocument();
255
+ },
256
+ );
257
+ });
@@ -322,7 +322,7 @@ export function useAsyncData<T>(
322
322
  const [nonce, setNonce] = useState(0);
323
323
  const [result, setResult] = useState<
324
324
  PendingResult<T> | LoadingResult<T> | ErrorResult<T> | SuccessResult<T>
325
- >(Result.pending());
325
+ >(() => Result.pending());
326
326
 
327
327
  const asProps =
328
328
  typeof loaderOrProps === "function"
@@ -339,12 +339,12 @@ export function useAsyncData<T>(
339
339
  keepPrevious = true;
340
340
  },
341
341
  };
342
+ // Activity can restart this effect without changing dependencies, so
343
+ // loading state must follow the request lifecycle.
342
344
  setResult((prevResult) => {
343
- // If we have previous data, show reloading state
344
345
  if (prevResult.status === "success" || prevResult.status === "loading") {
345
346
  return Result.loading(prevResult.data);
346
347
  }
347
- // Otherwise, show initial loading state
348
348
  return Result.pending();
349
349
  });
350
350
  fetchStable(context)
@@ -42,11 +42,13 @@ export function useDetectedDataSources(
42
42
  dialects: [...connections.values()].map((connection) =>
43
43
  connection.dialect.toLowerCase(),
44
44
  ),
45
- storageProtocols: storageNamespaces.map((namespace) =>
46
- namespace.protocol.toLowerCase(),
45
+ storageProtocols: new Set(
46
+ storageNamespaces.map((namespace) => namespace.protocol.toLowerCase()),
47
47
  ),
48
- storageBackendTypes: storageNamespaces.map((namespace) =>
49
- namespace.backendType.toLowerCase(),
48
+ storageBackendTypes: new Set(
49
+ storageNamespaces.map((namespace) =>
50
+ namespace.backendType.toLowerCase(),
51
+ ),
50
52
  ),
51
53
  };
52
54
 
@@ -2,6 +2,7 @@
2
2
  import { debounce } from "lodash-es";
3
3
  import { useEffect, useMemo, useState } from "react";
4
4
  import useEvent from "react-use-event-hook";
5
+ import { useInternalStateWithSync } from "./useInternalStateWithSync";
5
6
 
6
7
  export function useDebounce<T>(value: T, delay: number): T {
7
8
  const [debouncedValue, setDebouncedValue] = useState<T>(value);
@@ -41,18 +42,14 @@ export function useDebounceControlledState<T>(opts: {
41
42
  disabled?: boolean;
42
43
  }) {
43
44
  const { initialValue, onChange, delay, disabled } = opts;
44
- const [internalValue, setInternalValue] = useState<T>(initialValue);
45
+ const [internalValue, setInternalValue] = useInternalStateWithSync(
46
+ initialValue,
47
+ Object.is,
48
+ );
45
49
  const debouncedValue = useDebounce(internalValue, delay || 200);
46
50
 
47
51
  const onUpdate = useEvent(onChange);
48
52
 
49
- // Handle external changes:
50
- useEffect(() => {
51
- // TODO: Replace this synchronization with a controlled-state primitive.
52
- // oxlint-disable-next-line react/no-deriving-state-in-effects -- Synchronize the debounced draft when the controlled value changes.
53
- setInternalValue(initialValue);
54
- }, [initialValue]);
55
-
56
53
  // Handle debounced updates for user input
57
54
  useEffect(() => {
58
55
  if (disabled) {
@@ -80,8 +77,7 @@ export function useDebounceControlledState<T>(opts: {
80
77
  };
81
78
  }
82
79
 
83
- // oxlint-disable-next-line typescript/no-explicit-any
84
- export function useDebouncedCallback<T extends (...args: any[]) => unknown>(
80
+ export function useDebouncedCallback<T extends (...args: never[]) => unknown>(
85
81
  callback: T,
86
82
  delay: number,
87
83
  ) {
@@ -12,7 +12,7 @@ import React from "react";
12
12
  * Useful when the re-render if more expensive than the deep compare.
13
13
  */
14
14
  export function useDeepCompareMemoize<T>(value: T) {
15
- const ref = React.useRef<T>(value);
15
+ const ref = React.useRef(value);
16
16
 
17
17
  if (!dequal(value, ref.current)) {
18
18
  ref.current = value;
@@ -1,6 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { useLayoutEffect, useState } from "react";
3
+ import { useEffect, useState } from "react";
4
4
 
5
5
  /**
6
6
  * Returns `false` until `delayMs` has elapsed since mount (or since `delayMs`
@@ -8,20 +8,24 @@ import { useLayoutEffect, useState } from "react";
8
8
  * `delayMs <= 0`. Useful for suppressing spinner/fallback flashes.
9
9
  */
10
10
  export function useDelayElapsed(delayMs: number): boolean {
11
- const [elapsed, setElapsed] = useState(delayMs <= 0);
11
+ const [timer, setTimer] = useState({ delayMs, elapsed: delayMs <= 0 });
12
12
 
13
- // Layout effect (not a plain effect) so that when `delayMs` flips from `<= 0`
14
- // to `> 0` — e.g. a gate re-closing — we reset `elapsed` to `false` before
15
- // the browser paints, rather than briefly flashing the fallback for a frame.
16
- useLayoutEffect(() => {
13
+ // Reset during render so a changed delay never commits a stale `elapsed`
14
+ // (e.g. a gate re-closing shows no fallback flash before paint)
15
+ if (!Object.is(timer.delayMs, delayMs)) {
16
+ setTimer({ delayMs, elapsed: delayMs <= 0 });
17
+ }
18
+
19
+ useEffect(() => {
17
20
  if (delayMs <= 0) {
18
- setElapsed(true);
19
21
  return;
20
22
  }
21
- setElapsed(false);
22
- const timeout = setTimeout(() => setElapsed(true), delayMs);
23
+ const timeout = setTimeout(
24
+ () => setTimer({ delayMs, elapsed: true }),
25
+ delayMs,
26
+ );
23
27
  return () => clearTimeout(timeout);
24
28
  }, [delayMs]);
25
29
 
26
- return elapsed;
30
+ return timer.elapsed;
27
31
  }
@@ -1,13 +1,13 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { useEffect, useRef, useState } from "react";
3
+ import { useEffect, useState } from "react";
4
4
  import { type Milliseconds, Time } from "@/utils/time";
5
5
 
6
6
  /**
7
7
  * Returns the elapsed time since mount, in milliseconds.
8
8
  */
9
9
  export function useElapsedTime(initialStartTimeMs: Milliseconds) {
10
- const startTime = useRef(initialStartTimeMs);
10
+ const [startTime] = useState(initialStartTimeMs);
11
11
  const [endTime, setEndTime] = useState(initialStartTimeMs);
12
12
 
13
13
  useEffect(() => {
@@ -21,5 +21,5 @@ export function useElapsedTime(initialStartTimeMs: Milliseconds) {
21
21
  return () => clearInterval(interval);
22
22
  }, []);
23
23
 
24
- return endTime - startTime.current;
24
+ return endTime - startTime;
25
25
  }
@@ -62,8 +62,7 @@ export function useEventListener(
62
62
  return;
63
63
  }
64
64
 
65
- // oxlint-disable-next-line typescript/no-explicit-any
66
- const eventListener = (event: any) => savedListener.current(event);
65
+ const eventListener = (event: Event) => savedListener.current(event);
67
66
  target.addEventListener(type, eventListener, options);
68
67
 
69
68
  return () => {
@@ -37,12 +37,9 @@ export function useInputHistory({
37
37
  const historyIndexRef = React.useRef<number>(-1);
38
38
  // Store the current input when navigating history
39
39
  const pendingInputRef = React.useRef<string>("");
40
- // Keep a ref to history for use in callbacks without causing re-renders
41
- const historyRef = React.useRef(history);
42
- historyRef.current = history;
43
40
 
44
41
  const navigateUp = React.useCallback(() => {
45
- const currentHistory = historyRef.current;
42
+ const currentHistory = history;
46
43
  if (currentHistory.length === 0) {
47
44
  return;
48
45
  }
@@ -59,10 +56,10 @@ export function useInputHistory({
59
56
  historyIndexRef.current = newIndex;
60
57
  setValue(currentHistory[currentHistory.length - 1 - newIndex]);
61
58
  }
62
- }, [value, setValue]);
59
+ }, [history, value, setValue]);
63
60
 
64
61
  const navigateDown = React.useCallback(() => {
65
- const currentHistory = historyRef.current;
62
+ const currentHistory = history;
66
63
  if (currentHistory.length === 0) {
67
64
  return;
68
65
  }
@@ -79,7 +76,7 @@ export function useInputHistory({
79
76
  historyIndexRef.current = -1;
80
77
  setValue(pendingInputRef.current);
81
78
  }
82
- }, [setValue]);
79
+ }, [history, setValue]);
83
80
 
84
81
  const addToHistory = React.useCallback((command: string) => {
85
82
  setHistory((prev) => {
@@ -1,5 +1,5 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { useRef, useState } from "react";
2
+ import { useState } from "react";
3
3
 
4
4
  type EqualityFn<T> = (a: T, b: T) => boolean;
5
5
 
@@ -13,13 +13,13 @@ export function useInternalStateWithSync<T>(
13
13
  initial: T,
14
14
  equalityFn: EqualityFn<T> = defaultEqualityFn,
15
15
  ) {
16
- const [state, setState] = useState(initial);
17
- const ref = useRef(initial);
16
+ const [state, setState] = useState(() => initial);
17
+ const [previousInitial, setPreviousInitial] = useState(() => initial);
18
18
 
19
19
  // Whenever the outside state changes, update the internal state
20
- if (!equalityFn(ref.current, initial)) {
21
- setState(initial); // react is ok to setState during render
22
- ref.current = initial;
20
+ if (!equalityFn(previousInitial, initial)) {
21
+ setState(() => initial);
22
+ setPreviousInitial(() => initial);
23
23
  }
24
24
 
25
25
  return [state, setState] as const;
@@ -6,6 +6,7 @@
6
6
 
7
7
  import type { RefObject } from "react";
8
8
  import { useEffect, useRef } from "react";
9
+ import useEvent from "react-use-event-hook";
9
10
 
10
11
  interface Size {
11
12
  width: number | undefined;
@@ -30,9 +31,7 @@ export function useResizeObserver<T extends HTMLElement = HTMLElement>(
30
31
  const { ref, box = "content-box", skip } = options;
31
32
  // oxlint-disable-next-line react/hook-use-state
32
33
  const previousSize = useRef<Size>({ ...initialSize });
33
- const onResize = useRef<((size: Size) => void) | undefined>(undefined);
34
- // oxlint-disable-next-line react-hooks/react-compiler
35
- onResize.current = options.onResize;
34
+ const onResize = useEvent((size: Size) => options.onResize?.(size));
36
35
 
37
36
  useEffect(() => {
38
37
  if (!ref.current || skip) {
@@ -63,9 +62,7 @@ export function useResizeObserver<T extends HTMLElement = HTMLElement>(
63
62
  previousSize.current.width = newWidth;
64
63
  previousSize.current.height = newHeight;
65
64
 
66
- if (onResize.current) {
67
- onResize.current(newSize);
68
- }
65
+ onResize(newSize);
69
66
  }
70
67
  });
71
68
 
@@ -74,7 +71,7 @@ export function useResizeObserver<T extends HTMLElement = HTMLElement>(
74
71
  return () => {
75
72
  observer.disconnect();
76
73
  };
77
- }, [box, ref, skip]);
74
+ }, [box, ref, skip, onResize]);
78
75
  }
79
76
 
80
77
  type BoxSizesKey = keyof Pick<