@marimo-team/islands 0.25.3-dev6 → 0.25.3-dev61

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 (236) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CNMliVzw.js → ConnectedDataExplorerComponent-Da5OoEUL.js} +14 -14
  2. package/dist/{ErrorBoundary-Ca-Sa492.js → ErrorBoundary-BEfUX0zz.js} +8 -8
  3. package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-Bz3mrprX.js} +2 -2
  4. package/dist/any-language-editor-k2B8YI3Y.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.js → architectureDiagram-5GKGNRK7-Cs5ujb9K.js} +1 -1
  6. package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
  7. package/dist/{button-CvjkwdSk.js → button-BxB3RDjN.js} +1 -1
  8. package/dist/{capabilities-CQ5IjhzL.js → capabilities-BgR0osfN.js} +2 -2
  9. package/dist/{chat-ui-0lKdOoQ3.js → chat-ui-C0_lCzql.js} +1139 -1140
  10. package/dist/{check-EDwSwP89.js → check-P35dc2dH.js} +1 -1
  11. package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-CcTSaxZt.js} +1 -1
  12. package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-DcJcjOaO.js} +1 -1
  13. package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-BVtDTjaF.js} +1 -1
  14. package/dist/{chunk-TICWLB2K-Cu_XMjDM.js → chunk-TICWLB2K-BkDFkh8q.js} +1 -1
  15. package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
  16. package/dist/{classDiagram-ZZMXUADV-BXXV13Hq.js → classDiagram-ZZMXUADV-CfAt7HT5.js} +2 -2
  17. package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-CGEjNYa2.js} +2 -2
  18. package/dist/{code-visibility-NGRjhbtA.js → code-visibility-CA0UOEgk.js} +6 -6
  19. package/dist/{common-B7_ZSg0P.js → common-BgoPSdwN.js} +4960 -4835
  20. package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-CPoxUG7G.js} +3 -3
  21. package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-BW6NmfJi.js} +1 -1
  22. package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-CWg3CTuF.js} +1 -1
  23. package/dist/{data-grid-overlay-editor-B_cmrkBm.js → data-grid-overlay-editor-CRjm80Ab.js} +5 -5
  24. package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
  25. package/dist/{diagram-S7CK7UJ4-BsSJJldB.js → diagram-S7CK7UJ4-BPxtoS-z.js} +1 -1
  26. package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-DA-vJdFL.js} +1 -1
  27. package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-D6bkR84S.js} +1 -1
  28. package/dist/{diagram-Z3DM3KII-BD8mXjOj.js → diagram-Z3DM3KII-m60g4cAT.js} +1 -1
  29. package/dist/{dialog-CEBFZNyc.js → dialog-C1PcpCqX.js} +3 -3
  30. package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
  31. package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-B3eUiVJB.js} +177 -169
  32. package/dist/{ebnfDiagram-PWID7BFC-BIN1sweE.js → ebnfDiagram-PWID7BFC-CK99sho2.js} +2 -2
  33. package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-Bryq0tdE.js} +1 -1
  34. package/dist/{error-banner-D-WCIiMt.js → error-banner-Sl2FlSJ4.js} +2 -2
  35. package/dist/{error-entries-DSvFWT_M.js → error-entries-CMNtA8NK.js} +959 -959
  36. package/dist/{feedback-button-CTF2Nj9s.js → feedback-button-CZA7i0Ir.js} +29 -29
  37. package/dist/{flowDiagram-HODETNUW-3EfLrlFb.js → flowDiagram-HODETNUW-D-3aRpfS.js} +2 -2
  38. package/dist/{formats-CeOmU1MU.js → formats-6pLWWrY2.js} +3 -3
  39. package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
  40. package/dist/{glide-data-editor-BQHF9puC.js → glide-data-editor-Bd52MLtf.js} +370 -368
  41. package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Bt8_pBa2.js} +16 -16
  42. package/dist/{globals-Cj8iqMEp.js → globals-BvANmUu6.js} +3 -3
  43. package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
  44. package/dist/{html-to-image-B8s5SgGm.js → html-to-image-6xcoTKhC.js} +29 -29
  45. package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-s1C1kgfU.js} +1 -1
  46. package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-CCsRgolY.js} +1 -1
  47. package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.js → kanban-definition-UXKFOSKX-BrmxDlcc.js} +1 -1
  48. package/dist/{label-BDKF3Afc.js → label-BRnUC0NX.js} +2 -2
  49. package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
  50. package/dist/{loader-BQOfjTIJ.js → loader-Bm8cy3e8.js} +12 -12
  51. package/dist/main.js +3177 -2980
  52. package/dist/{mermaid-CHo49Yv-.js → mermaid-DKI0869I.js} +38 -38
  53. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  54. package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-641xJysF.js} +1 -1
  55. package/dist/{number-overlay-editor-DBcnm0SY.js → number-overlay-editor-C7ffluCS.js} +3 -3
  56. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  57. package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
  58. package/dist/{pegDiagram-XKGWAZYB-BqvywNJA.js → pegDiagram-XKGWAZYB-keVUAUeS.js} +2 -2
  59. package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-DHktXPO_.js} +1 -1
  60. package/dist/precisionRound-DhAzeRcm.js +49 -0
  61. package/dist/{process-output-CKRXyszb.js → process-output-BJRJFobf.js} +4 -4
  62. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  63. package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.js → railroadDiagram-O6MQD6OU-BVEF4OV_.js} +2 -2
  64. package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
  65. package/dist/{requirementDiagram-BXWQKSXE-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CWo-UAKV.js} +1 -1
  66. package/dist/{reveal-component-DTBSuGkC.js → reveal-component-Dfuz8zQ_.js} +19 -19
  67. package/dist/{select-BButFi-d.js → select-B0LRDH0g.js} +4 -4
  68. package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-CHQvLnq4.js} +1 -1
  69. package/dist/{spec-BwarBpUJ.js → spec-C-LRKGtT.js} +4 -4
  70. package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-BOJZgbUJ.js} +2 -2
  71. package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH-C53XXdYH.js} +2 -2
  72. package/dist/style.css +1 -1
  73. package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-D2o1gOtw.js} +1 -1
  74. package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.js → swimlanesDiagram-VR7AAH4N-BHFzN1Wk.js} +2 -2
  75. package/dist/{swiper-component-DKntyn8p.js → swiper-component-DfhRvAFi.js} +2 -2
  76. package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
  77. package/dist/{timeline-definition-24CTP7MA-D3c46WM5.js → timeline-definition-24CTP7MA-rv7QUa46.js} +1 -1
  78. package/dist/{toDate-vIXljnsr.js → toDate-CjPuEPou.js} +21 -20
  79. package/dist/{toast-B9UMEi8u.js → toast-CFWpihJU.js} +4 -4
  80. package/dist/{tooltip-BO1REbqs.js → tooltip-CKO1pHJo.js} +2 -2
  81. package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-DBCITLeF.js} +1 -1
  82. package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-ChgNtdmE.js} +15 -14
  83. package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-c_yJtkAe.js} +2 -2
  84. package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-Bet7kSsI.js} +1 -1
  85. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BkdeVdXC.js} +1 -1
  86. package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-VVmz_24o.js} +3 -3
  87. package/dist/{useTheme-CRnjkku0.js → useTheme-DympCsQg.js} +5 -5
  88. package/dist/{vega-component-DTG6zxaY.js → vega-component-DnfQeoyt.js} +147 -147
  89. package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
  90. package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
  91. package/dist/{vennDiagram-4TSXK5OY-BSZD8JcZ.js → vennDiagram-4TSXK5OY-D36Zk7t-.js} +1 -1
  92. package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-BLBolvyQ.js} +1 -1
  93. package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.js → xychartDiagram-S5SC5T6Z-CfH57l0v.js} +2 -2
  94. package/package.json +4 -4
  95. package/src/__tests__/host-imports.test.ts +1 -0
  96. package/src/components/app-config/ai-config.tsx +6 -2
  97. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  98. package/src/components/chat/chat-panel.tsx +3 -7
  99. package/src/components/chat/chat-utils.ts +4 -8
  100. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  101. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  102. package/src/components/data-table/__tests__/export-actions.test.tsx +434 -6
  103. package/src/components/data-table/charts/charts.tsx +14 -19
  104. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  105. package/src/components/data-table/columns.tsx +2 -1
  106. package/src/components/data-table/context-menu.tsx +36 -20
  107. package/src/components/data-table/date-popover.tsx +6 -4
  108. package/src/components/data-table/export-actions.tsx +267 -163
  109. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
  110. package/src/components/data-table/schemas.ts +14 -36
  111. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  112. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +24 -0
  113. package/src/components/editor/actions/pair-with-agent-modal.tsx +18 -15
  114. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  115. package/src/components/editor/cell/CreateCellButton.tsx +8 -10
  116. package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
  117. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  118. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  119. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  120. package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
  121. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +30 -1
  122. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
  123. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +12 -18
  124. package/src/components/editor/connections/components.tsx +1 -1
  125. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  126. package/src/components/editor/controls/command-palette.tsx +22 -10
  127. package/src/components/editor/dynamic-favicon.tsx +21 -40
  128. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +12 -23
  129. package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
  130. package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
  131. package/src/components/editor/file-tree/file-explorer.tsx +10 -5
  132. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  133. package/src/components/editor/file-tree/use-file-search.ts +35 -21
  134. package/src/components/editor/output/MarimoTracebackOutput.tsx +1 -1
  135. package/src/components/editor/output/console/ConsoleOutput.tsx +8 -18
  136. package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
  137. package/src/components/icons/large-spinner.tsx +3 -14
  138. package/src/components/pages/home-page.tsx +4 -4
  139. package/src/components/slides/slide-cell-view.tsx +1 -1
  140. package/src/components/tracing/tracing.tsx +2 -2
  141. package/src/core/attachments/state.ts +5 -8
  142. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  143. package/src/core/cells/cells.ts +2 -1
  144. package/src/core/cells/pending-delete-service.ts +2 -6
  145. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  146. package/src/core/datasets/data-source-discovery.ts +4 -6
  147. package/src/core/edit-app.tsx +2 -2
  148. package/src/core/host/AGENTS.md +11 -2
  149. package/src/core/host/__tests__/attachments.test.ts +56 -0
  150. package/src/core/host/__tests__/document.test.ts +126 -0
  151. package/src/core/host/__tests__/events.test.ts +49 -0
  152. package/src/core/host/__tests__/fixtures.ts +41 -0
  153. package/src/core/host/__tests__/notebook-client.test.ts +496 -0
  154. package/src/core/host/connection.ts +81 -0
  155. package/src/core/host/events.ts +68 -0
  156. package/src/core/host/notebook-client.ts +316 -0
  157. package/src/core/host/resources/attachments.ts +19 -0
  158. package/src/core/host/resources/document.ts +101 -0
  159. package/src/core/hotkeys/actions.ts +2 -4
  160. package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
  161. package/src/core/islands/bootstrap.ts +0 -1
  162. package/src/core/network/connection.ts +4 -2
  163. package/src/core/network/useConnectionNotice.ts +8 -2
  164. package/src/core/run-app.tsx +2 -2
  165. package/src/core/runtime/__tests__/runtime.test.ts +5 -12
  166. package/src/core/runtime/runtime.ts +5 -5
  167. package/src/core/static/files.ts +2 -0
  168. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  169. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  170. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +149 -0
  171. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +168 -253
  172. package/src/core/websocket/connection-state.ts +70 -0
  173. package/src/core/websocket/state.ts +7 -0
  174. package/src/core/websocket/transports/__tests__/sse.test.ts +18 -4
  175. package/src/core/websocket/transports/create.ts +31 -0
  176. package/src/core/websocket/transports/sse.ts +14 -2
  177. package/src/core/websocket/transports/transport.ts +12 -23
  178. package/src/core/websocket/types.ts +9 -39
  179. package/src/core/websocket/useNotebookClient.ts +78 -0
  180. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +53 -233
  181. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  182. package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
  183. package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
  184. package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
  185. package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
  186. package/src/hooks/__tests__/useScript.test.tsx +77 -0
  187. package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
  188. package/src/hooks/useAsyncData.ts +3 -3
  189. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  190. package/src/hooks/useDebounce.ts +5 -8
  191. package/src/hooks/useDeepCompareMemoize.ts +1 -1
  192. package/src/hooks/useDelayElapsed.ts +14 -10
  193. package/src/hooks/useElapsedTime.ts +3 -3
  194. package/src/hooks/useInputHistory.ts +4 -7
  195. package/src/hooks/useInternalStateWithSync.ts +6 -6
  196. package/src/hooks/useResizeObserver.ts +4 -7
  197. package/src/hooks/useScript.ts +67 -70
  198. package/src/hooks/useStateForKey.ts +93 -0
  199. package/src/plugins/core/registerReactComponent.tsx +1 -1
  200. package/src/plugins/impl/DataTablePlugin.tsx +14 -15
  201. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  202. package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
  203. package/src/plugins/impl/FormPlugin.tsx +1 -1
  204. package/src/plugins/impl/SliderPlugin.tsx +6 -8
  205. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +201 -1
  206. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
  207. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
  208. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
  209. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  210. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  211. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  212. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
  213. package/src/plugins/impl/chat/chat-ui.tsx +10 -9
  214. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  215. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  216. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  217. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  218. package/src/plugins/impl/panel/PanelPlugin.tsx +15 -16
  219. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  220. package/src/plugins/impl/panel/utils.ts +17 -0
  221. package/src/plugins/impl/vega/make-selectable.ts +3 -6
  222. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  223. package/src/stories/editor.stories.tsx +24 -17
  224. package/src/utils/__tests__/arrays.test.ts +35 -0
  225. package/src/utils/__tests__/numbers.test.ts +48 -1
  226. package/src/utils/__tests__/objects.test.ts +21 -0
  227. package/src/utils/arrays.ts +10 -0
  228. package/src/utils/numbers.ts +28 -17
  229. package/src/utils/objects.ts +3 -7
  230. package/src/utils/once.ts +19 -19
  231. package/dist/any-language-editor-DZ_YTVus.js +0 -76
  232. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  233. package/dist/precisionRound-DMIVQrt2.js +0 -97
  234. package/src/core/host/__tests__/protocol.test.ts +0 -214
  235. package/src/core/host/protocol.ts +0 -1943
  236. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -46,38 +46,80 @@ function abortSignalAny(signals: AbortSignal[]): AbortSignal {
46
46
  return controller.signal;
47
47
  }
48
48
 
49
+ interface ModuleEntry {
50
+ promise: Promise<unknown>;
51
+ references: number;
52
+ dispose: () => void;
53
+ }
54
+
49
55
  /**
50
56
  * Deduplicates ESM imports by jsHash.
51
57
  * A single import is shared across all widget instances using the same module.
52
58
  */
53
59
  class WidgetDefRegistry {
54
- #cache = new Map<string, Promise<unknown>>();
60
+ #cache = new Map<string, ModuleEntry>();
55
61
 
56
62
  /**
57
63
  * Get (or start) the ESM import for a widget module, cached by
58
64
  * jsHash so multiple instances share one import. Pass
59
65
  * `kernelAuthored: true` only for URLs from an `EsmSpec`; it widens
60
- * the import gate to remote and data URLs.
66
+ * the import gate to remote and data URLs. A lifetime retains the module's
67
+ * temporary URL until that signal aborts; omit it to retain the URL for
68
+ * the notebook's lifetime. The evaluated module itself stays cached.
61
69
  */
62
70
  public getModule(options: {
63
71
  jsUrl: string;
64
72
  jsHash: string;
65
73
  kernelAuthored?: boolean;
74
+ lifetime?: AbortSignal;
66
75
  }): Promise<unknown> {
67
- const { jsUrl, jsHash, kernelAuthored = false } = options;
68
- const cached = this.#cache.get(jsHash);
69
- if (cached) {
70
- return cached;
76
+ const { jsUrl, jsHash, kernelAuthored = false, lifetime } = options;
77
+ let entry = this.#cache.get(jsHash);
78
+ if (!entry) {
79
+ const imported = this.#doImport(jsUrl, { kernelAuthored });
80
+ const created: ModuleEntry = {
81
+ references: 0,
82
+ dispose: () => undefined,
83
+ promise: imported.then(
84
+ ({ module, dispose }) => {
85
+ created.dispose = dispose;
86
+ return module;
87
+ },
88
+ (error) => {
89
+ // An invalidated import must not evict its replacement.
90
+ if (this.#cache.get(jsHash) === created) {
91
+ this.#cache.delete(jsHash);
92
+ }
93
+ throw error;
94
+ },
95
+ ),
96
+ };
97
+ this.#cache.set(jsHash, created);
98
+ entry = created;
71
99
  }
72
-
73
- const promise = this.#doImport(jsUrl, { kernelAuthored }).catch((error) => {
74
- // On failure, remove from cache so a retry with a new URL can work
75
- this.#cache.delete(jsHash);
76
- throw error;
77
- });
78
-
79
- this.#cache.set(jsHash, promise);
80
- return promise;
100
+ const retained = entry;
101
+ retained.references += 1;
102
+ const release = () => {
103
+ retained.references -= 1;
104
+ if (retained.references === 0) {
105
+ // Keep the evaluated module cached: re-importing would re-run its
106
+ // top-level side effects (e.g. `customElements.define`). Only the
107
+ // temporary URL is released.
108
+ void retained.promise.then(
109
+ () => {
110
+ retained.dispose();
111
+ retained.dispose = () => undefined;
112
+ },
113
+ () => undefined,
114
+ );
115
+ }
116
+ };
117
+ if (lifetime?.aborted) {
118
+ release();
119
+ } else {
120
+ lifetime?.addEventListener("abort", release, { once: true });
121
+ }
122
+ return retained.promise;
81
123
  }
82
124
 
83
125
  public invalidate(jsHash: string): void {
@@ -87,7 +129,7 @@ class WidgetDefRegistry {
87
129
  async #doImport(
88
130
  jsUrl: string,
89
131
  opts: { kernelAuthored?: boolean } = {},
90
- ): Promise<unknown> {
132
+ ): Promise<{ module: unknown; dispose: () => void }> {
91
133
  // By default, only trust marimo virtual file paths: arbitrary URLs
92
134
  // would let markdown-injected elements import attacker-controlled
93
135
  // JavaScript. Kernel-authored URLs (from an `EsmSpec`) are exempt —
@@ -103,11 +145,23 @@ class WidgetDefRegistry {
103
145
  )}`,
104
146
  );
105
147
  }
106
- let url = asRemoteURL(jsUrl).toString();
107
- if (isStaticNotebook()) {
108
- url = resolveVirtualFileURL(url);
148
+ const remoteUrl = asRemoteURL(jsUrl).toString();
149
+ const url = isStaticNotebook()
150
+ ? resolveVirtualFileURL(remoteUrl)
151
+ : remoteUrl;
152
+ const dispose = () => {
153
+ // Only the static-file resolver's newly created URL belongs to us.
154
+ if (url !== remoteUrl) {
155
+ URL.revokeObjectURL(url);
156
+ }
157
+ };
158
+ try {
159
+ const module = await import(/* @vite-ignore */ url);
160
+ return { module, dispose };
161
+ } catch (error) {
162
+ dispose();
163
+ throw error;
109
164
  }
110
- return import(/* @vite-ignore */ url);
111
165
  }
112
166
  }
113
167
 
@@ -132,6 +186,7 @@ interface CreateWidgetBindingOptions<T extends ModelState> {
132
186
  model: Model<T>;
133
187
  createHost: HostFactory;
134
188
  controller?: AbortController;
189
+ onDestroy?: () => void;
135
190
  }
136
191
 
137
192
  async function safelyRunCleanup(cleanup: Cleanup, reason: string) {
@@ -159,6 +214,7 @@ export class WidgetBinding<T extends ModelState = ModelState> {
159
214
  #createHost: HostFactory;
160
215
  #cleanupTasks = new Set<Promise<void>>();
161
216
  #viewTasks = new Set<Promise<void>>();
217
+ #onDestroy?: () => void;
162
218
 
163
219
  private constructor(options: {
164
220
  widget: ResolvedWidget<T>;
@@ -166,12 +222,14 @@ export class WidgetBinding<T extends ModelState = ModelState> {
166
222
  exports: unknown;
167
223
  controller: AbortController;
168
224
  createHost: HostFactory;
225
+ onDestroy?: () => void;
169
226
  }) {
170
227
  this.#widget = options.widget;
171
228
  this.#model = options.model;
172
229
  this.#exports = options.exports;
173
230
  this.#controller = options.controller;
174
231
  this.#createHost = options.createHost;
232
+ this.#onDestroy = options.onDestroy;
175
233
  }
176
234
 
177
235
  /**
@@ -185,76 +243,94 @@ export class WidgetBinding<T extends ModelState = ModelState> {
185
243
  model,
186
244
  createHost,
187
245
  controller = new AbortController(),
246
+ onDestroy,
188
247
  }: CreateWidgetBindingOptions<T>): Promise<WidgetBinding<T>> {
189
248
  const signal = controller.signal;
190
- const widget: ResolvedWidget<T> =
191
- typeof widgetDef === "function" ? await widgetDef() : widgetDef;
249
+ let lateCleanup: Promise<void> | undefined;
250
+ try {
251
+ const widget: ResolvedWidget<T> =
252
+ typeof widgetDef === "function" ? await widgetDef() : widgetDef;
192
253
 
193
- const initPromise = Promise.resolve(
194
- widget.initialize?.({
195
- model: modelProxy(model, signal),
196
- experimental,
197
- signal,
198
- }),
199
- );
200
- // If destroyed mid-initialize, still run a late-arriving legacy
201
- // cleanup callback: the widget acquired resources.
202
- initPromise
203
- .then(async (settled) => {
204
- if (signal.aborted && isCleanup(settled)) {
205
- await safelyRunCleanup(settled, "late initialize");
206
- }
207
- })
208
- .catch(() => undefined);
254
+ if (signal.aborted) {
255
+ throw new Error("[anywidget] binding destroyed");
256
+ }
257
+ const initPromise = Promise.resolve(
258
+ widget.initialize?.({
259
+ model: modelProxy(model, signal),
260
+ experimental,
261
+ signal,
262
+ }),
263
+ );
264
+ // If destroyed mid-initialize, still run a late-arriving legacy
265
+ // cleanup callback: the widget acquired resources.
266
+ lateCleanup = initPromise
267
+ .then(async (settled) => {
268
+ if (signal.aborted && isCleanup(settled)) {
269
+ await safelyRunCleanup(settled, "late initialize");
270
+ }
271
+ })
272
+ .catch(() => undefined);
209
273
 
210
- // Race against destruction so a hung `initialize` can't strand
211
- // callers whose model already closed.
212
- const result = await Promise.race([
213
- initPromise,
214
- new Promise<never>((_, reject) => {
215
- const abort = () => reject(new Error("[anywidget] binding destroyed"));
216
- if (signal.aborted) {
217
- abort();
218
- return;
219
- }
220
- signal.addEventListener("abort", abort, { once: true });
221
- }),
222
- ]);
274
+ // Race against destruction so a hung `initialize` can't strand
275
+ // callers whose model already closed.
276
+ const result = await Promise.race([
277
+ initPromise,
278
+ new Promise<never>((_, reject) => {
279
+ const abort = () =>
280
+ reject(new Error("[anywidget] binding destroyed"));
281
+ if (signal.aborted) {
282
+ abort();
283
+ return;
284
+ }
285
+ signal.addEventListener("abort", abort, { once: true });
286
+ }),
287
+ ]);
223
288
 
224
- if (signal.aborted) {
225
- throw new Error("[anywidget] binding destroyed");
226
- }
289
+ if (signal.aborted) {
290
+ throw new Error("[anywidget] binding destroyed");
291
+ }
227
292
 
228
- // Distinguish anywidget's three return shapes:
229
- // function → legacy cleanup callback (run on destroy)
230
- // object → widget exports (anywidget>=0.11)
231
- // void → nothing
232
- let exports: unknown;
233
- let initializeCleanup: Cleanup | undefined;
234
- if (isCleanup(result)) {
235
- initializeCleanup = result;
236
- exports = undefined;
237
- } else if (typeof result === "object" && result !== null) {
238
- exports = result;
239
- } else {
240
- exports = undefined;
241
- }
293
+ // Distinguish anywidget's three return shapes:
294
+ // function → legacy cleanup callback (run on destroy)
295
+ // object → widget exports (anywidget>=0.11)
296
+ // void → nothing
297
+ let exports: unknown;
298
+ let initializeCleanup: Cleanup | undefined;
299
+ if (isCleanup(result)) {
300
+ initializeCleanup = result;
301
+ exports = undefined;
302
+ } else if (typeof result === "object" && result !== null) {
303
+ exports = result;
304
+ } else {
305
+ exports = undefined;
306
+ }
242
307
 
243
- const binding = new WidgetBinding({
244
- widget,
245
- model,
246
- exports,
247
- controller,
248
- createHost,
249
- });
250
- if (initializeCleanup) {
251
- signal.addEventListener(
252
- "abort",
253
- () => void binding.#trackCleanup(initializeCleanup, "initialize"),
254
- { once: true },
255
- );
308
+ const binding = new WidgetBinding({
309
+ widget,
310
+ model,
311
+ exports,
312
+ controller,
313
+ createHost,
314
+ onDestroy,
315
+ });
316
+ if (initializeCleanup) {
317
+ signal.addEventListener(
318
+ "abort",
319
+ () => void binding.#trackCleanup(initializeCleanup, "initialize"),
320
+ { once: true },
321
+ );
322
+ }
323
+ return binding;
324
+ } catch (error) {
325
+ // A canceled initialize may still be using import.meta.url in its
326
+ // late cleanup. Keep the module alive until that work settles.
327
+ if (lateCleanup) {
328
+ void lateCleanup.then(() => onDestroy?.());
329
+ } else {
330
+ onDestroy?.();
331
+ }
332
+ throw error;
256
333
  }
257
- return binding;
258
334
  }
259
335
 
260
336
  /**
@@ -353,6 +429,8 @@ export class WidgetBinding<T extends ModelState = ModelState> {
353
429
  while (this.#cleanupTasks.size > 0) {
354
430
  await Promise.all(this.#cleanupTasks);
355
431
  }
432
+ this.#onDestroy?.();
433
+ this.#onDestroy = undefined;
356
434
  }
357
435
  }
358
436
 
@@ -0,0 +1,93 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import type { ComponentProps } from "react";
5
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
6
+ import type { PopoverContent } from "@/components/ui/popover";
7
+ import { TooltipProvider } from "@/components/ui/tooltip";
8
+ import { Chatbot } from "../chat-ui";
9
+
10
+ vi.mock("@/components/editor/ai/add-cell-with-ai", () => ({
11
+ PromptInput: ({ value }: { value: string }) => (
12
+ <textarea aria-label="Chat prompt" value={value} readOnly={true} />
13
+ ),
14
+ }));
15
+
16
+ vi.mock("@/components/ui/popover", async (importOriginal) => {
17
+ const actual =
18
+ await importOriginal<typeof import("@/components/ui/popover")>();
19
+ return {
20
+ ...actual,
21
+ // Keep the form mounted when the prompt menu takes focus so this tests
22
+ // the prompt reset rather than the overlay's incidental unmount.
23
+ PopoverContent: (props: ComponentProps<typeof PopoverContent>) => (
24
+ <actual.PopoverContent
25
+ {...props}
26
+ onInteractOutside={(event) => event.preventDefault()}
27
+ />
28
+ ),
29
+ };
30
+ });
31
+
32
+ describe("chat prompt variables", () => {
33
+ const originalScrollTo = HTMLElement.prototype.scrollTo;
34
+ beforeAll(() => {
35
+ HTMLElement.prototype.scrollTo = vi.fn();
36
+ });
37
+ afterAll(() => {
38
+ HTMLElement.prototype.scrollTo = originalScrollTo;
39
+ });
40
+
41
+ it("clears entered values and replaces the fields when selecting another prompt", async () => {
42
+ const firstPrompt = "Tell {{name}} about {{topic}}";
43
+ const secondPrompt = "Send {{name}} to {{destination}}";
44
+ render(
45
+ <TooltipProvider>
46
+ <Chatbot
47
+ prompts={[firstPrompt, secondPrompt]}
48
+ config={{
49
+ max_tokens: null,
50
+ temperature: null,
51
+ top_p: null,
52
+ top_k: null,
53
+ frequency_penalty: null,
54
+ presence_penalty: null,
55
+ }}
56
+ showConfigurationControls={false}
57
+ maxHeight={undefined}
58
+ allowAttachments={false}
59
+ disabled={false}
60
+ value={[]}
61
+ setValue={vi.fn()}
62
+ host={document.createElement("div")}
63
+ get_chat_history={vi.fn().mockResolvedValue({ messages: [] })}
64
+ delete_chat_history={vi.fn().mockResolvedValue(null)}
65
+ delete_chat_message={vi.fn().mockResolvedValue(null)}
66
+ send_prompt={vi.fn().mockResolvedValue(null)}
67
+ cancel_prompt={vi.fn().mockResolvedValue(null)}
68
+ />
69
+ </TooltipProvider>,
70
+ );
71
+
72
+ const promptMenu = screen.getByRole("button", { expanded: false });
73
+ fireEvent.keyDown(promptMenu, { key: "ArrowDown" });
74
+ fireEvent.click(await screen.findByRole("menuitem", { name: firstPrompt }));
75
+ fireEvent.change(await screen.findByLabelText("name"), {
76
+ target: { value: "Ada" },
77
+ });
78
+ fireEvent.change(screen.getByLabelText("topic"), {
79
+ target: { value: "Python" },
80
+ });
81
+ expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled();
82
+
83
+ fireEvent.keyDown(promptMenu, { key: "ArrowDown" });
84
+ fireEvent.click(
85
+ await screen.findByRole("menuitem", { name: secondPrompt }),
86
+ );
87
+
88
+ expect(await screen.findByLabelText("destination")).toHaveValue("");
89
+ expect(screen.getByLabelText("name")).toHaveValue("");
90
+ expect(screen.queryByLabelText("topic")).not.toBeInTheDocument();
91
+ expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled();
92
+ });
93
+ });
@@ -24,7 +24,7 @@ import {
24
24
  Trash2Icon,
25
25
  X,
26
26
  } from "lucide-react";
27
- import React, { useEffect, useRef, useState } from "react";
27
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
28
28
  import { z } from "zod";
29
29
  import {
30
30
  describeChatAbortReason,
@@ -163,7 +163,9 @@ export const Chatbot: React.FC<Props> = (props) => {
163
163
 
164
164
  // Use a ref to avoid stale closure in the fetch callback
165
165
  const configRef = useRef<ChatConfig>(config);
166
- configRef.current = config;
166
+ useLayoutEffect(() => {
167
+ configRef.current = config;
168
+ }, [config]);
167
169
 
168
170
  // For frontend-managed streaming, create a controller to enqueue chunks to.
169
171
  const frontendStreamControllerRef =
@@ -411,9 +413,8 @@ export const Chatbot: React.FC<Props> = (props) => {
411
413
  });
412
414
  }, [messages.length, scrollContainerRef]);
413
415
 
414
- const codemirrorView = codeMirrorInputRef.current?.view;
415
-
416
416
  const resetInput = () => {
417
+ const codemirrorView = codeMirrorInputRef.current?.view;
417
418
  // Clear input immediately by directly manipulating the editor
418
419
  // There is some delay if we use setInput("") only
419
420
  if (codemirrorView) {
@@ -566,6 +567,7 @@ export const Chatbot: React.FC<Props> = (props) => {
566
567
  onSelect={(prompt) => {
567
568
  setInput(prompt);
568
569
  requestAnimationFrame(() => {
570
+ const codemirrorView = codeMirrorInputRef.current?.view;
569
571
  codemirrorView?.focus();
570
572
  moveToEndOfEditor(codemirrorView);
571
573
  });
@@ -862,6 +864,7 @@ const PromptsPopover: React.FC<{
862
864
 
863
865
  <PopoverContent side="right" align="end" className="min-w-80 px-2">
864
866
  <PromptVariablesForm
867
+ key={selectedPrompt}
865
868
  prompt={selectedPrompt}
866
869
  onClose={() => setIsPopoverOpen(false)}
867
870
  onSelect={onSelect}
@@ -876,9 +879,7 @@ const PromptVariablesForm: React.FC<{
876
879
  onClose: () => void;
877
880
  onSelect: (prompt: string) => void;
878
881
  }> = ({ prompt, onClose, onSelect }) => {
879
- const [variables, setVariables] = useState<{ [key: string]: string }>({});
880
-
881
- useEffect(() => {
882
+ const [variables, setVariables] = useState<{ [key: string]: string }>(() => {
882
883
  const variableRegex = /{{(\w+)}}/g;
883
884
  const matches = [...prompt.matchAll(variableRegex)];
884
885
  const initialVariables = matches.reduce<{ [key: string]: string }>(
@@ -888,8 +889,8 @@ const PromptVariablesForm: React.FC<{
888
889
  },
889
890
  {},
890
891
  );
891
- setVariables(initialVariables);
892
- }, [prompt]);
892
+ return initialVariables;
893
+ });
893
894
 
894
895
  const handleVariableChange = (variable: string, value: string) => {
895
896
  setVariables((prev) => ({ ...prev, [variable]: value }));
@@ -53,14 +53,14 @@ const AnyLanguageCodeMirror: React.FC<
53
53
  extensions?: readonly Extension[];
54
54
  }
55
55
  > = ({
56
- language,
56
+ language: requestedLanguage,
57
57
  hideUnsupportedLanguageErrors,
58
58
  showCopyButton,
59
59
  extensions = EMPTY_EXTENSIONS,
60
60
  ...props
61
61
  }) => {
62
62
  // Maybe normalize the language to the extension
63
- language = LANGUAGE_MAP[language || ""] || language;
63
+ const language = LANGUAGE_MAP[requestedLanguage || ""] || requestedLanguage;
64
64
 
65
65
  const isSupported = isSupportedLanguage(language);
66
66
  if (!isSupported) {
@@ -73,12 +73,16 @@ export const GlideDataEditor = ({
73
73
  });
74
74
 
75
75
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
76
+ const editableColumnSet = useMemo(
77
+ () => (editableColumns === "all" ? null : new Set(editableColumns)),
78
+ [editableColumns],
79
+ );
76
80
 
77
81
  const columns: ModifiedGridColumn[] = useMemo(() => {
78
82
  const columns: ModifiedGridColumn[] = [];
79
83
  for (const [columnName, fieldType] of columnFields) {
80
84
  const editable =
81
- editableColumns === "all" || editableColumns.includes(columnName);
85
+ editableColumnSet === null || editableColumnSet.has(columnName);
82
86
 
83
87
  columns.push({
84
88
  id: columnName,
@@ -100,7 +104,7 @@ export const GlideDataEditor = ({
100
104
  }
101
105
 
102
106
  return columns;
103
- }, [columnFields, columnWidths, editableColumns, theme]);
107
+ }, [columnFields, columnWidths, editableColumnSet, theme]);
104
108
 
105
109
  const getCellContent = useCallback(
106
110
  (cell: Item): GridCell => {
@@ -110,8 +114,7 @@ export const GlideDataEditor = ({
110
114
  const dataItem = dataRow[columns[col].title];
111
115
  const columnKind = columns[col].kind;
112
116
  const editable =
113
- editableColumns === "all" ||
114
- editableColumns.includes(columns[col].title);
117
+ editableColumnSet === null || editableColumnSet.has(columns[col].title);
115
118
 
116
119
  if (columnKind === GridCellKind.Boolean) {
117
120
  const value = Boolean(dataItem);
@@ -141,7 +144,7 @@ export const GlideDataEditor = ({
141
144
  data: String(dataItem),
142
145
  };
143
146
  },
144
- [columns, data, editableColumns],
147
+ [columns, data, editableColumnSet],
145
148
  );
146
149
 
147
150
  const onCellEdited = useCallback(
@@ -147,6 +147,8 @@ export function pasteCells(options: {
147
147
  }
148
148
 
149
149
  const edits: Edits["edits"] = [];
150
+ const editableColumnSet =
151
+ editableColumns === "all" ? null : new Set(editableColumns);
150
152
 
151
153
  for (const [rowIndex, dataRow] of parsedData.entries()) {
152
154
  if (!dataRow) {
@@ -175,7 +177,7 @@ export function pasteCells(options: {
175
177
  const column = columns[targetColIdx];
176
178
  const columnType = column.dataType;
177
179
  const editable =
178
- editableColumns === "all" || editableColumns.includes(column.title);
180
+ editableColumnSet === null || editableColumnSet.has(column.title);
179
181
 
180
182
  if (!editable) {
181
183
  continue;
@@ -1,7 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  /* oxlint-disable typescript/no-explicit-any */
3
3
 
4
- import { useCallback, useEffect, useRef } from "react";
4
+ import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
5
5
  import { z } from "zod";
6
6
  import { useEventListener } from "@/hooks/useEventListener";
7
7
  import { createPlugin } from "@/plugins/core/builder";
@@ -186,12 +186,14 @@ const MplInteractiveSlot = (props: IPluginProps<ModelIdRef, Data>) => {
186
186
  // the figure is built once, and switching to a new model is the rebind
187
187
  // effect's job, not a reason to tear the canvas down.
188
188
  const modelIdRef = useRef(modelId);
189
- modelIdRef.current = modelId;
190
189
  // The data attributes are re-parsed into fresh objects on every rerun, so
191
190
  // `toolbarImages` changes identity even when its contents do not. Read it
192
191
  // from a ref so it can't retrigger the mount effect and rebuild the canvas.
193
192
  const toolbarImagesRef = useRef(toolbarImages);
194
- toolbarImagesRef.current = toolbarImages;
193
+ useLayoutEffect(() => {
194
+ modelIdRef.current = modelId;
195
+ toolbarImagesRef.current = toolbarImages;
196
+ }, [modelId, toolbarImages]);
195
197
 
196
198
  // Bind the already-rendered figure/socket to a backend model, leaving the
197
199
  // DOM, figure, and socket in place so they survive across reruns. Disposes
@@ -1,7 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  /* oxlint-disable typescript/no-explicit-any */
3
3
 
4
- import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import useEvent from "react-use-event-hook";
5
6
  import { z } from "zod";
6
7
  import { MarimoIncomingMessageEvent } from "@/core/dom/events";
7
8
  import {
@@ -146,17 +147,10 @@ const PanelSlot = (props: Props) => {
146
147
  > | null>(null);
147
148
  const [loaded, setLoaded] = useState<boolean>(false);
148
149
  const [rendered, setRendered] = useState<string | null>(null);
150
+ const eventBufferRef = useRef<EventBuffer<unknown> | null>(null);
149
151
 
150
- // Store functions in a ref so the callback captured by EventBuffer stays current
151
- const functionsRef = useRef(functions);
152
- functionsRef.current = functions;
153
-
154
- const processEvents = useCallback(() => {
155
- if (!eventBufferRef.current) {
156
- return;
157
- }
158
-
159
- const events = eventBufferRef.current.getAndClear();
152
+ const processEvents = useEvent(() => {
153
+ const events = eventBufferRef.current?.getAndClear() ?? [];
160
154
  if (events.length === 0) {
161
155
  return;
162
156
  }
@@ -182,14 +176,19 @@ const PanelSlot = (props: Props) => {
182
176
  );
183
177
  const buffers: ArrayBuffer[] = [];
184
178
  message.content = extractBuffers(message.content, buffers);
185
- functionsRef.current.send_to_widget({ message, buffers }).catch((error) => {
179
+ functions.send_to_widget({ message, buffers }).catch((error) => {
186
180
  Logger.warn("Failed to send Panel event to backend", error);
187
181
  });
188
- }, []);
182
+ });
189
183
 
190
- const eventBufferRef = useRef<EventBuffer<unknown> | null>(
191
- new EventBuffer(processEvents),
192
- );
184
+ useEffect(() => {
185
+ const buffer = new EventBuffer(processEvents);
186
+ eventBufferRef.current = buffer;
187
+ return () => {
188
+ buffer.dispose();
189
+ eventBufferRef.current = null;
190
+ };
191
+ }, [processEvents]);
193
192
 
194
193
  // Load the bokeh extension
195
194
  useEffect(() => {