@marimo-team/frontend 0.24.1-dev99 → 0.24.2-dev129

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 (304) hide show
  1. package/dist/assets/{CellStatus-wvvZkEPY.js → CellStatus-LoTwk7fR.js} +1 -1
  2. package/dist/assets/{ConnectedDataExplorerComponent-Dkqk66Mv.js → ConnectedDataExplorerComponent-DW6cJ-Lr.js} +1 -1
  3. package/dist/assets/{ErrorBoundary-npvnLp9x.js → ErrorBoundary-CDVGFYfr.js} +1 -1
  4. package/dist/assets/JsonOutput-CzIGPEZQ.js +50 -0
  5. package/dist/assets/{LazyAnyLanguageCodeMirror-zSwE3dvU.js → LazyAnyLanguageCodeMirror-LboTfNDv.js} +2 -2
  6. package/dist/assets/{MarimoErrorOutput-CeUo5oka.js → MarimoErrorOutput-DqpoYOvw.js} +1 -1
  7. package/dist/assets/RenderHTML-DwhMAd9l.js +1 -0
  8. package/dist/assets/{RunButton-BaV8A-z4.js → RunButton-BlpbweyH.js} +1 -1
  9. package/dist/assets/{VisuallyHidden-SChAyKIQ.js → VisuallyHidden-CBMDmozt.js} +1 -1
  10. package/dist/assets/{add-cell-with-ai-DoLkfzYn.js → add-cell-with-ai-R-3DS79K.js} +1 -1
  11. package/dist/assets/{add-connection-dialog-C1IF1nR0.js → add-connection-dialog-BGfpw5lC.js} +1 -1
  12. package/dist/assets/{agent-panel-CG154QrU.js → agent-panel-Cgt_K0tD.js} +1 -1
  13. package/dist/assets/ai-model-dropdown-DCmiuk7x.js +3 -0
  14. package/dist/assets/alert-dialog-CA-2mBs3.js +11 -0
  15. package/dist/assets/any-language-editor-DflLjPSQ.js +1 -0
  16. package/dist/assets/{app-config-button-ktBH1pEB.js → app-config-button-DUJC6b6x.js} +1 -1
  17. package/dist/assets/badge-BSgtkC2e.js +1 -0
  18. package/dist/assets/{blob-DVtvZmWH.js → blob-B2d_dHwI.js} +1 -1
  19. package/dist/assets/button-Dh8EwhsF.js +1 -0
  20. package/dist/assets/{cache-panel-C7jsiqCa.js → cache-panel-DqgtC98s.js} +1 -1
  21. package/dist/assets/{capabilities-LWzb3E3M.js → capabilities-D8kPgMKr.js} +1 -1
  22. package/dist/assets/{cell-editor-DoDqaCZp.js → cell-editor-Dw09J9yD.js} +6 -6
  23. package/dist/assets/{cell-link-Dqqt6Rgs.js → cell-link-6burT2XF.js} +1 -1
  24. package/dist/assets/{cells-D4RwofRx.js → cells-Cp-G34Rz.js} +56 -56
  25. package/dist/assets/{chat-display-COmzHgx5.js → chat-display-Bu405iyc.js} +1 -1
  26. package/dist/assets/{chat-panel-B2tyEfGW.js → chat-panel-BlUwNY5j.js} +1 -1
  27. package/dist/assets/{chat-ui-B5bsr-7V.js → chat-ui-DGiVDzNh.js} +1 -1
  28. package/dist/assets/{circle--ysFeW9z.js → circle-ByJdIf5p.js} +1 -1
  29. package/dist/assets/{column-preview-CW5DPFvy.js → column-preview-riSXbYe_.js} +1 -1
  30. package/dist/assets/{command-palette-B9dyvMvJ.js → command-palette-CCVzJcyf.js} +1 -1
  31. package/dist/assets/{common-CC3tuCpi.js → common-CHR7w7aQ.js} +1 -1
  32. package/dist/assets/{components-D02ZZZMP.js → components-C7mWrmXr.js} +1 -1
  33. package/dist/assets/{components-CBlGIIAX.js → components-DUYCXngU.js} +1 -1
  34. package/dist/assets/config-B-r81uZ6.js +1 -0
  35. package/dist/assets/{config-D4frVGQ4.js → config-BlNM0caL.js} +1 -1
  36. package/dist/assets/{copy-Cg05mM3C.js → copy-DlEomWzI.js} +1 -1
  37. package/dist/assets/{copy-icon-CRxVZTJ9.js → copy-icon-z8fuTE3w.js} +1 -1
  38. package/dist/assets/{createReducer-Bq-nQJiK.js → createReducer-B_XmHROl.js} +1 -1
  39. package/dist/assets/{datasource-Do8f2hFf.js → datasource-DrODSIs0.js} +1 -1
  40. package/dist/assets/{dates-DK5JikJo.js → dates-B58T85Jq.js} +1 -1
  41. package/dist/assets/dependency-graph-panel-AY5BEVH2.js +5 -0
  42. package/dist/assets/{dist-7qdHLg-L.js → dist-B0RXffUm.js} +1 -1
  43. package/dist/assets/dist-B0qYy1Ye.js +1 -0
  44. package/dist/assets/{dist-B97dKQGP.js → dist-B8pdsUrS.js} +1 -1
  45. package/dist/assets/{dist-BcfF-rkl.js → dist-BF7Lrekv.js} +1 -1
  46. package/dist/assets/dist-BL8RjfvI.js +1 -0
  47. package/dist/assets/dist-BVZxge7j.js +1 -0
  48. package/dist/assets/{dist-fxnYzq-R.js → dist-BaRNSaca.js} +1 -1
  49. package/dist/assets/dist-Bk2yQtw4.js +1 -0
  50. package/dist/assets/dist-BsfiiKoy.js +1 -0
  51. package/dist/assets/dist-BskmVd3B.js +1 -0
  52. package/dist/assets/{dist-DL1Thauf.js → dist-Bsu3h9O7.js} +1 -1
  53. package/dist/assets/{dist-PIe1xcZo.js → dist-BtC9ym6k.js} +7 -7
  54. package/dist/assets/{dist-DeuWxYVk.js → dist-Bv0S1RlZ.js} +1 -1
  55. package/dist/assets/{dist-CidBzUqR.js → dist-C--ZICxQ.js} +1 -1
  56. package/dist/assets/dist-C-2fD3XY.js +1 -0
  57. package/dist/assets/{dist-DzAWvPVK.js → dist-CKUpxYGQ.js} +1 -1
  58. package/dist/assets/dist-CMFMfBm1.js +1 -0
  59. package/dist/assets/dist-CZNULd6Z.js +2 -0
  60. package/dist/assets/dist-CjFduPiC.js +1 -0
  61. package/dist/assets/{dist-DoAjHgMj.js → dist-CmImLjus.js} +1 -1
  62. package/dist/assets/{dist-BKx0MLbb.js → dist-Cq8NOszX.js} +1 -1
  63. package/dist/assets/{dist-DCc0VfiO.js → dist-Cx4BE7jC.js} +3 -3
  64. package/dist/assets/dist-D45ScydS.js +1 -0
  65. package/dist/assets/dist-D6dg87r7.js +1 -0
  66. package/dist/assets/dist-DEjfQLz0.js +1 -0
  67. package/dist/assets/{dist-BcYW-EJk.js → dist-DJOWOALB.js} +1 -1
  68. package/dist/assets/{dist-CxxZaSme.js → dist-DPoYlxSC.js} +1 -1
  69. package/dist/assets/dist-DR-Y-nl3.js +1 -0
  70. package/dist/assets/dist-Dahlp9JQ.js +1 -0
  71. package/dist/assets/{dist-DIkuvChK.js → dist-Db-HC93w.js} +1 -1
  72. package/dist/assets/{dist-DTM7BAUu.js → dist-DdOlZ6mj.js} +1 -1
  73. package/dist/assets/{dist-Dz_04liz.js → dist-DiYjQAhI.js} +1 -1
  74. package/dist/assets/{dist-BO5c5n_b.js → dist-DkPPv0FB.js} +1 -1
  75. package/dist/assets/{dist-DkCO0b-0.js → dist-DmGtPaVM.js} +1 -1
  76. package/dist/assets/{dist-CXmhxecl.js → dist-DnxYYyU7.js} +1 -1
  77. package/dist/assets/{dist-CO1Hfv5H.js → dist-Dw9YRsCU.js} +1 -1
  78. package/dist/assets/dist-Dwx3KdUP.js +1 -0
  79. package/dist/assets/{dist-BgL43w5r.js → dist-DzCQtGkt.js} +1 -1
  80. package/dist/assets/dist-JweaDt6P.js +1 -0
  81. package/dist/assets/{dist-Dbu5sPgR.js → dist-gELFCfHH.js} +1 -1
  82. package/dist/assets/dist-kGDJ0AWF.js +1 -0
  83. package/dist/assets/{dist-Cqm1W-Zm.js → dist-kaLvV_d4.js} +1 -1
  84. package/dist/assets/{dist-CEeRVpMD.js → dist-qvk3g06H.js} +1 -1
  85. package/dist/assets/dist-wCw2twuB.js +1 -0
  86. package/dist/assets/dist-wF42tUcr.js +1 -0
  87. package/dist/assets/dist-wY2yia-1.js +3 -0
  88. package/dist/assets/{documentation-panel-DvHjxlli.js → documentation-panel-CaJF7e-j.js} +1 -1
  89. package/dist/assets/{download-BP8MrprC.js → download-DMxOzI7b.js} +1 -1
  90. package/dist/assets/dropdown-menu-C40jM9o4.js +1 -0
  91. package/dist/assets/{edit-page-71cK-Nhe.js → edit-page-Vqd9NedM.js} +6 -6
  92. package/dist/assets/{error-banner-BYN8YUMe.js → error-banner-Dw5YY9wq.js} +1 -1
  93. package/dist/assets/{error-entries-DOZ1u97X.js → error-entries-Cv2m1yhc.js} +1 -1
  94. package/dist/assets/{error-panel-CINcf76Z.js → error-panel-Bhlvt6_H.js} +1 -1
  95. package/dist/assets/{es-BuMFDpR4.js → es-CrFhA96G.js} +1 -1
  96. package/dist/assets/{esm-Dw0vvSg2.js → esm-BHMOvJzM.js} +1 -1
  97. package/dist/assets/{esm-BGpfQNhD.js → esm-DmqQGMel.js} +1 -1
  98. package/dist/assets/file-explorer-panel-6HvNfCrJ.js +1 -0
  99. package/dist/assets/{file-icons-DgKj9mrV.js → file-icons-DdGYuADJ.js} +1 -1
  100. package/dist/assets/file-name-input-K4MKESI_.js +3 -0
  101. package/dist/assets/{floating-outline-CRXIsrPm.js → floating-outline-DaUHnl_e.js} +1 -1
  102. package/dist/assets/{formats-DSzGCKeO.js → formats-C6s96nu2.js} +1 -1
  103. package/dist/assets/{formatting-BJlT1s_3.js → formatting-BWZH2nBg.js} +1 -1
  104. package/dist/assets/{fullscreen-0EEPR7uc.js → fullscreen-BH2jz-F4.js} +5 -5
  105. package/dist/assets/{gallery-page-TqnFYECu.js → gallery-page-C3pBpHlY.js} +1 -1
  106. package/dist/assets/{glide-data-editor-DcY3nXI8.js → glide-data-editor-sICUMEhk.js} +1 -1
  107. package/dist/assets/{glide-utils-CR1sWLZU.js → glide-utils-UGsrRHYj.js} +1 -1
  108. package/dist/assets/home-page-BLd6rnbk.js +4 -0
  109. package/dist/assets/{html-to-image-DMn_7CCA.js → html-to-image-D6GUC5q6.js} +1 -1
  110. package/dist/assets/index-BGT7have.css +2 -0
  111. package/dist/assets/index-ipFYAeCF.js +29 -0
  112. package/dist/assets/{input-Daz6sk8l.js → input-DEbY281O.js} +2 -2
  113. package/dist/assets/{kiosk-mode-CzGt3mc2.js → kiosk-mode-mxgb5lWf.js} +1 -1
  114. package/dist/assets/{layout-KJMQADEj.js → layout-DWnrxIo2.js} +5 -5
  115. package/dist/assets/links-DLvOXyO_.js +1 -0
  116. package/dist/assets/{logs-panel-DFibPeCT.js → logs-panel-BGS2v3ba.js} +1 -1
  117. package/dist/assets/lua-1tNpM92n.js +1 -0
  118. package/dist/assets/{lua-CRIS5afH.js → lua-DXBxhmso.js} +1 -1
  119. package/dist/assets/{markdown-renderer-CdZX2MvM.js → markdown-renderer-BZKlbm2Q.js} +1 -1
  120. package/dist/assets/menu-items-BSRA2YI9.js +1 -0
  121. package/dist/assets/{mermaid-CeseGGnS.js → mermaid-l6lQkCib.js} +1 -1
  122. package/dist/assets/{name-cell-input-B-d5Jwh4.js → name-cell-input-BMCdK-el.js} +1 -1
  123. package/dist/assets/{numbers-DKurV_vq.js → numbers-Bc-r3jsC.js} +1 -1
  124. package/dist/assets/{outline-panel-D2Wk10hY.js → outline-panel-Dr5u73V6.js} +1 -1
  125. package/dist/assets/{packages-panel-CQsbxIlG.js → packages-panel-BdpRg9tX.js} +1 -1
  126. package/dist/assets/{pair-with-agent-modal-CNxmIMFM.js → pair-with-agent-modal-CbUFKJHP.js} +1 -1
  127. package/dist/assets/{panel-accordion-state-D6-FMXE2.js → panel-accordion-state-DjUTN7i5.js} +1 -1
  128. package/dist/assets/{panels-BgcK_4M7.js → panels-CWn0Yh1D.js} +1 -1
  129. package/dist/assets/process-output-Ck7-P_xp.js +1 -0
  130. package/dist/assets/{readonly-code-display-BvqLeUsJ.js → readonly-code-display--S2daDa6.js} +1 -1
  131. package/dist/assets/{readonly-python-code-GSN4D4sb.js → readonly-python-code-DpIyjHqY.js} +1 -1
  132. package/dist/assets/{renderShortcut-DK_OucLT.js → renderShortcut-X-CJUSM8.js} +1 -1
  133. package/dist/assets/{reveal-component-u9ED2diI.js → reveal-component-nZIAapoD.js} +1 -1
  134. package/dist/assets/{run-page-U5iuhyz8.js → run-page-9pl8qhQR.js} +1 -1
  135. package/dist/assets/{runs-BQTlv6Ja.js → runs-euHSzlw_.js} +1 -1
  136. package/dist/assets/{scratchpad-panel--YOR0kUB.js → scratchpad-panel-BleGt2KM.js} +1 -1
  137. package/dist/assets/{scroll-to-bottom-button-xua0e9rA.js → scroll-to-bottom-button-DufiYUD7.js} +1 -1
  138. package/dist/assets/{secrets-panel-CZPbFAZH.js → secrets-panel-D3BpDIgg.js} +1 -1
  139. package/dist/assets/select-nLt2y0Sw.js +1 -0
  140. package/dist/assets/{session-CEdta5_Q.js → session-Sz5nSFku.js} +1 -1
  141. package/dist/assets/session-panel-DRGPlL5J.js +1 -0
  142. package/dist/assets/{share-C2MPy_VX.js → share-CPtBiNgr.js} +1 -1
  143. package/dist/assets/{skeleton-CsceCAjw.js → skeleton-BKeh8hf3.js} +1 -1
  144. package/dist/assets/{snippets-panel-BxwHsPmW.js → snippets-panel-D5yy7kj0.js} +1 -1
  145. package/dist/assets/{spec-CUFquO6R.js → spec-DKsAmLTi.js} +1 -1
  146. package/dist/assets/{state-BqrF--O0.js → state-B6AklCzD.js} +1 -1
  147. package/dist/assets/{state-C2c3_VuY.js → state-B8XRqyFg.js} +1 -1
  148. package/dist/assets/{state-CHplO0zn.js → state-Di9TbuV9.js} +1 -1
  149. package/dist/assets/{strings-CpYReX2Q.js → strings-CReV1AYG.js} +1 -1
  150. package/dist/assets/{swiper-component-Bot7ehYH.js → swiper-component-5Kdi1-Gn.js} +1 -1
  151. package/dist/assets/{switch-CtH5Ct94.js → switch-DjXr9Y9D.js} +1 -1
  152. package/dist/assets/{tabs-c6mpccpH.js → tabs-Cy1qXLzH.js} +1 -1
  153. package/dist/assets/{terminal-C73iz1fU.js → terminal-CQAFGBfN.js} +1 -1
  154. package/dist/assets/{tooltip-Cwltdez8.js → tooltip-BG35uS_N.js} +1 -1
  155. package/dist/assets/{tooltip-D7dq6bnc.js → tooltip-BUtPzKB4.js} +1 -1
  156. package/dist/assets/{tracing-Dhq121dR.js → tracing-DNk3gmWv.js} +1 -1
  157. package/dist/assets/{tracing-panel-IKt1933_.js → tracing-panel-Yqazr6w_.js} +2 -2
  158. package/dist/assets/{tree-actions-DIfWonCA.js → tree-actions-CG5g7aUQ.js} +1 -1
  159. package/dist/assets/{useAddCell-BYrVaYfZ.js → useAddCell-DdB_uBif.js} +1 -1
  160. package/dist/assets/{useBoolean-ZU5StAbr.js → useBoolean-BXC4CItZ.js} +1 -1
  161. package/dist/assets/{useCellActionButton-DbXDs0MA.js → useCellActionButton-B31neI3m.js} +1 -1
  162. package/dist/assets/{useDebounce-Dulm61-t.js → useDebounce-DgpSQhrP.js} +1 -1
  163. package/dist/assets/{useDeleteCell-Wgcl3CRG.js → useDeleteCell-Bz49_cFj.js} +1 -1
  164. package/dist/assets/{useDependencyPanelTab-C5AU8GKL.js → useDependencyPanelTab-C_ps0r--.js} +1 -1
  165. package/dist/assets/{useEventListener-D27Dii_l.js → useEventListener-92uWqz-P.js} +1 -1
  166. package/dist/assets/{useIframeCapabilities-DEpTCh1Q.js → useIframeCapabilities-C7Di8f2y.js} +1 -1
  167. package/dist/assets/{useInstallPackage-u7d0wo79.js → useInstallPackage-DsD08SLK.js} +1 -1
  168. package/dist/assets/{useInterval-B1_J4wdT.js → useInterval-B4v7_zd0.js} +1 -1
  169. package/dist/assets/{useNotebookActions-FuMERtHq.js → useNotebookActions-CQtbk9CB.js} +2 -2
  170. package/dist/assets/{useRunCells-DTdMRbkE.js → useRunCells-CX7oW5zy.js} +1 -1
  171. package/dist/assets/{useSplitCell-2z0zEB8X.js → useSplitCell-Dy2_DmJR.js} +1 -1
  172. package/dist/assets/{useTheme-CF-zG9YH.js → useTheme-C-ri8BtY.js} +1 -1
  173. package/dist/assets/utils-D5Ywm7-q.js +1 -0
  174. package/dist/assets/{vega-component-BSXJcTJb.js → vega-component-C7TCzyaU.js} +1 -1
  175. package/dist/assets/write-secret-modal-D6zoZgLY.js +2 -0
  176. package/dist/index.html +73 -73
  177. package/package.json +26 -26
  178. package/src/components/ai/ai-provider-icon.tsx +2 -2
  179. package/src/components/ai/ai-utils.ts +1 -1
  180. package/src/components/app-config/ai-config.tsx +33 -37
  181. package/src/components/app-config/user-config-form.tsx +1 -1
  182. package/src/components/data-table/__tests__/column-explorer.test.tsx +64 -4
  183. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +137 -4
  184. package/src/components/data-table/__tests__/url-detector.test.tsx +1 -1
  185. package/src/components/data-table/__tests__/useColumnVisibility.test.ts +153 -1
  186. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +54 -25
  187. package/src/components/data-table/column-visibility-dropdown.tsx +62 -14
  188. package/src/components/data-table/date-popover.tsx +2 -1
  189. package/src/components/data-table/hooks/use-column-visibility.ts +76 -0
  190. package/src/components/data-table/renderers.tsx +4 -8
  191. package/src/components/data-table/show-only-column-button.tsx +58 -0
  192. package/src/components/dependency-graph/__tests__/elements.test.ts +151 -0
  193. package/src/components/dependency-graph/custom-edge.tsx +68 -0
  194. package/src/components/dependency-graph/custom-node.tsx +54 -26
  195. package/src/components/dependency-graph/dependency-graph-tree.tsx +110 -21
  196. package/src/components/dependency-graph/elements.ts +225 -31
  197. package/src/components/dependency-graph/panels.tsx +23 -2
  198. package/src/components/dependency-graph/utils/__tests__/layout.test.ts +78 -0
  199. package/src/components/dependency-graph/utils/layout.ts +108 -18
  200. package/src/components/editor/Output.tsx +40 -0
  201. package/src/components/editor/__tests__/Output.test.tsx +85 -2
  202. package/src/components/editor/connections/secret-combobox.tsx +3 -1
  203. package/src/components/editor/file-tree/__tests__/file-explorer-navigation.test.tsx +832 -0
  204. package/src/components/editor/file-tree/__tests__/file-viewer.test.tsx +27 -0
  205. package/src/components/editor/file-tree/__tests__/requesting-tree.test.ts +130 -0
  206. package/src/components/editor/file-tree/file-browser-row.tsx +82 -0
  207. package/src/components/editor/file-tree/file-explorer.tsx +377 -159
  208. package/src/components/editor/file-tree/file-operations.tsx +1 -1
  209. package/src/components/editor/file-tree/file-search-results.tsx +142 -0
  210. package/src/components/editor/file-tree/file-tree-row.tsx +40 -0
  211. package/src/components/editor/file-tree/file-viewer.tsx +2 -1
  212. package/src/components/editor/file-tree/renderers.tsx +4 -2
  213. package/src/components/editor/file-tree/requesting-tree.tsx +162 -32
  214. package/src/components/editor/file-tree/use-file-search.ts +142 -0
  215. package/src/components/editor/file-tree/use-hover-expand.ts +34 -0
  216. package/src/components/editor/navigation/__tests__/navigation.test.ts +30 -0
  217. package/src/components/editor/navigation/navigation.ts +20 -0
  218. package/src/components/forms/form.tsx +6 -4
  219. package/src/components/home/components.tsx +1 -0
  220. package/src/components/static-html/share-modal.tsx +3 -2
  221. package/src/components/ui/__tests__/badge.test.ts +63 -0
  222. package/src/components/ui/__tests__/button.test.ts +19 -0
  223. package/src/components/ui/__tests__/fullscreen.test.tsx +88 -0
  224. package/src/components/ui/__tests__/menu-items.test.ts +31 -0
  225. package/src/components/ui/badge.tsx +9 -7
  226. package/src/components/ui/button.tsx +1 -0
  227. package/src/components/ui/combobox.tsx +1 -1
  228. package/src/components/ui/date-input.tsx +1 -1
  229. package/src/components/ui/dialog.tsx +3 -1
  230. package/src/components/ui/fullscreen.tsx +43 -1
  231. package/src/components/ui/input.tsx +5 -1
  232. package/src/components/ui/links.tsx +2 -1
  233. package/src/components/ui/menu-items.tsx +9 -9
  234. package/src/components/ui/native-select.tsx +2 -2
  235. package/src/components/ui/navigation.tsx +1 -1
  236. package/src/components/ui/range-slider.tsx +5 -3
  237. package/src/components/ui/select.tsx +1 -1
  238. package/src/components/ui/sheet.tsx +1 -1
  239. package/src/components/ui/toast.tsx +8 -5
  240. package/src/components/ui/toaster.tsx +2 -2
  241. package/src/core/ai/ids/__tests__/ids.test.ts +0 -8
  242. package/src/core/ai/ids/ids.ts +1 -1
  243. package/src/core/cells/__tests__/add-missing-import.test.ts +20 -0
  244. package/src/core/codemirror/language/__tests__/utils.test.ts +1 -1
  245. package/src/core/config/config-schema.ts +1 -0
  246. package/src/core/hotkeys/hotkeys.ts +5 -0
  247. package/src/core/network/__tests__/requests-toasting.test.tsx +14 -0
  248. package/src/core/network/requests-toasting.tsx +1 -1
  249. package/src/css/globals.css +28 -0
  250. package/src/hooks/useEventListener.ts +1 -1
  251. package/src/plugins/core/RenderHTML.tsx +6 -4
  252. package/src/plugins/impl/MatrixPlugin.tsx +1 -1
  253. package/src/plugins/impl/code/any-language-editor.tsx +7 -4
  254. package/src/plugins/impl/data-frames/__tests__/schema.test.ts +34 -1
  255. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +25 -25
  256. package/src/plugins/impl/data-frames/schema.ts +5 -11
  257. package/src/plugins/impl/panel/__tests__/utils.test.ts +4 -4
  258. package/src/plugins/impl/vega/__tests__/vega.test.ts +1 -1
  259. package/src/stories/cell.stories.tsx +15 -11
  260. package/src/utils/__tests__/events.test.ts +1 -1
  261. package/dist/assets/JsonOutput-CRLQQVKf.js +0 -50
  262. package/dist/assets/RenderHTML-YkwVPe9V.js +0 -1
  263. package/dist/assets/ai-model-dropdown-CrSrb8Zz.js +0 -3
  264. package/dist/assets/alert-dialog-BVi_GyOB.js +0 -11
  265. package/dist/assets/any-language-editor-BL8H-DwB.js +0 -1
  266. package/dist/assets/badge-D_-uqy2N.js +0 -1
  267. package/dist/assets/button-Btj6GXNy.js +0 -1
  268. package/dist/assets/config-ycxJQP2b.js +0 -1
  269. package/dist/assets/dependency-graph-panel-Dg-tUB9T.js +0 -5
  270. package/dist/assets/dist-2oRmY-BC.js +0 -1
  271. package/dist/assets/dist-8lhGoJ54.js +0 -3
  272. package/dist/assets/dist-B4QaGCvF.js +0 -1
  273. package/dist/assets/dist-B_ABJVTW.js +0 -1
  274. package/dist/assets/dist-Bke0Ybxy.js +0 -1
  275. package/dist/assets/dist-Bw8CAY5i.js +0 -1
  276. package/dist/assets/dist-BwmO3SSk.js +0 -1
  277. package/dist/assets/dist-C3KxgR0m.js +0 -1
  278. package/dist/assets/dist-CLE562nZ.js +0 -1
  279. package/dist/assets/dist-CLi0j3bw.js +0 -1
  280. package/dist/assets/dist-CMIyXejt.js +0 -1
  281. package/dist/assets/dist-CbQrIaPV.js +0 -2
  282. package/dist/assets/dist-CpgfIIzU.js +0 -1
  283. package/dist/assets/dist-D7aYPqs9.js +0 -1
  284. package/dist/assets/dist-DKCF5R0T.js +0 -1
  285. package/dist/assets/dist-DbFa9cik.js +0 -1
  286. package/dist/assets/dist-DdPuhs_-.js +0 -1
  287. package/dist/assets/dist-DkfdZExz.js +0 -1
  288. package/dist/assets/dist-hOh6JBwi.js +0 -1
  289. package/dist/assets/dist-mY1j_5eA.js +0 -1
  290. package/dist/assets/dist-tg3aVnpD.js +0 -1
  291. package/dist/assets/dropdown-menu-CqNabM1f.js +0 -1
  292. package/dist/assets/file-explorer-panel-cflxW8a7.js +0 -1
  293. package/dist/assets/file-name-input-CQF4Ovwx.js +0 -3
  294. package/dist/assets/home-page-DPRHPAQM.js +0 -4
  295. package/dist/assets/index-BkIXRAbi.css +0 -2
  296. package/dist/assets/index-p49fbIlR.js +0 -29
  297. package/dist/assets/links-DPh7yoP-.js +0 -1
  298. package/dist/assets/lua-_4wuYo1j.js +0 -1
  299. package/dist/assets/menu-items-BSVrMleU.js +0 -1
  300. package/dist/assets/process-output-DvTtgoMs.js +0 -1
  301. package/dist/assets/select-czJLm6jf.js +0 -1
  302. package/dist/assets/session-panel-C_N3WGWQ.js +0 -1
  303. package/dist/assets/utils-CWSZQEjr.js +0 -1
  304. package/dist/assets/write-secret-modal-lNU_7c2x.js +0 -2
@@ -4,26 +4,177 @@ import type { Atom } from "jotai";
4
4
  import { type Edge, MarkerType, type Node, type NodeProps } from "reactflow";
5
5
  import { getNotebook } from "@/core/cells/cells";
6
6
  import type { CellId } from "@/core/cells/ids";
7
- import type { CellData } from "@/core/cells/types";
7
+ import type { CellData, CellRuntimeState } from "@/core/cells/types";
8
8
  import { store } from "@/core/state/jotai";
9
- import type { Variables } from "@/core/variables/types";
9
+ import type { Variable, VariableName, Variables } from "@/core/variables/types";
10
10
  import { Arrays } from "@/utils/arrays";
11
+ import { extractCellPreview } from "./utils/cell-preview";
12
+
13
+ /** The edge type registered for dependency edges (see `custom-edge.tsx`). */
14
+ export const DEPENDENCY_EDGE_TYPE = "dependency";
15
+
16
+ // Stroke/marker colors shared by the dependency edge and its arrow marker.
17
+ export const STATE_FLOW_EDGE_COLOR = "var(--amber-10)";
18
+ export const DEFAULT_EDGE_COLOR = "var(--gray-8)";
19
+
20
+ export interface DependencyEdgeData {
21
+ /** Variables carried across this edge. */
22
+ variables: VariableName[];
23
+ /** True when any carried variable is a mutable `State`/`SetFunctor` flow. */
24
+ isStateFlow: boolean;
25
+ }
26
+
27
+ // A `State`/`SetFunctor` (from `mo.state`) is mutable mid-run, so it is drawn
28
+ // distinctly. `UIElement` is intentionally excluded: it is frontend-driven and
29
+ // immutable during a run.
30
+ const STATE_DATATYPES = new Set(["State", "SetFunctor"]);
31
+
32
+ export function isStateFlowVariable(variable: Variable): boolean {
33
+ return variable.dataType != null && STATE_DATATYPES.has(variable.dataType);
34
+ }
11
35
 
12
36
  export interface NodeData {
13
37
  atom: Atom<CellData>;
14
- forceWidth?: number;
38
+ /** Variables this cell defines — the node's primary identity when collapsed. */
39
+ defs: VariableName[];
40
+ /** When true the node renders its full code instead of the compact ref list. */
41
+ expanded?: boolean;
15
42
  }
16
43
  export type CustomNodeProps = NodeProps<NodeData>;
17
44
 
45
+ const LINE_HEIGHT = 11; // matches TinyCode.css
46
+
18
47
  export function getNodeHeight(linesOfCode: number) {
19
- const LINE_HEIGHT = 11; // matches TinyCode.css
20
48
  return Math.min(linesOfCode * LINE_HEIGHT + 35, 200);
21
49
  }
22
50
 
51
+ // Compact-node sizing. A collapsed node wraps its defined variables into
52
+ // comma-separated lines at a character budget, so it grows taller rather than
53
+ // wider; the same budget drives the estimated width so dagre and the DOM agree.
54
+ export const NODE_HEADER_HEIGHT = 22;
55
+ const DEF_LINE_HEIGHT = 16;
56
+ const BODY_V_PADDING = 10;
57
+ const EXPANDED_NODE_WIDTH = 300;
58
+ const CHAR_WIDTH = 6.5; // text-xs monospace-ish
59
+ const NODE_H_PADDING = 22;
60
+ const MIN_NODE_WIDTH = 90;
61
+ const MAX_NODE_WIDTH = 240;
62
+ // Wrap defs to the longest single name, but never narrower than this — short
63
+ // names pack several per line, a long one gets a line to itself.
64
+ const WRAP_MIN_CHARS = 12;
65
+ // ... and never wider than fits MAX_NODE_WIDTH, or packed lines would be
66
+ // clipped by the width clamp. A single name over this budget still gets its
67
+ // own line; the renderer truncates it with an ellipsis and a title.
68
+ const WRAP_MAX_CHARS = Math.floor(
69
+ (MAX_NODE_WIDTH - NODE_H_PADDING) / CHAR_WIDTH,
70
+ );
71
+
72
+ export function collapsedNodeWidth(label: string): number {
73
+ const raw = label.length * CHAR_WIDTH + NODE_H_PADDING;
74
+ return Math.min(Math.max(raw, MIN_NODE_WIDTH), MAX_NODE_WIDTH);
75
+ }
76
+
77
+ /** The per-line character budget used to wrap a node's defs. */
78
+ function defsWrapBudget(defs: VariableName[]): number {
79
+ const longest = defs.reduce(
80
+ (max, def) => Math.max(max, def.length),
81
+ WRAP_MIN_CHARS,
82
+ );
83
+ return Math.min(longest, WRAP_MAX_CHARS);
84
+ }
85
+
86
+ /**
87
+ * Greedily pack `defs` into comma-separated lines, each no wider than the
88
+ * budget (the longest name, or `WRAP_MIN_CHARS`). No trailing commas.
89
+ */
90
+ export function wrapDefs(defs: VariableName[]): string[] {
91
+ const budget = defsWrapBudget(defs);
92
+ const lines: string[] = [];
93
+ let current = "";
94
+ for (const def of defs) {
95
+ if (current === "") {
96
+ current = def;
97
+ } else if (current.length + 2 + def.length <= budget) {
98
+ current = `${current}, ${def}`;
99
+ } else {
100
+ lines.push(current);
101
+ current = def;
102
+ }
103
+ }
104
+ if (current !== "") {
105
+ lines.push(current);
106
+ }
107
+ return lines;
108
+ }
109
+
110
+ /**
111
+ * The lines shown in a collapsed node's body: its defs wrapped to a budget, or
112
+ * a single code-preview line when the cell defines nothing.
113
+ */
114
+ export function nodeBodyLines(data: NodeData): string[] {
115
+ if (data.defs.length > 0) {
116
+ return wrapDefs(data.defs);
117
+ }
118
+ const preview = extractCellPreview(store.get(data.atom).code).text;
119
+ return preview ? [preview] : [];
120
+ }
121
+
122
+ export function nodeDimensions(data: NodeData): {
123
+ width: number;
124
+ height: number;
125
+ } {
126
+ if (data.expanded) {
127
+ const lines = store.get(data.atom).code.trim().split("\n").length;
128
+ return { width: EXPANDED_NODE_WIDTH, height: getNodeHeight(lines) };
129
+ }
130
+ const lines = nodeBodyLines(data);
131
+ const widthChars =
132
+ data.defs.length > 0
133
+ ? defsWrapBudget(data.defs)
134
+ : lines.reduce((max, line) => Math.max(max, line.length), 0);
135
+ const bodyLines = Math.max(lines.length, 1);
136
+ return {
137
+ width: collapsedNodeWidth("x".repeat(widthChars)),
138
+ height: NODE_HEADER_HEIGHT + bodyLines * DEF_LINE_HEIGHT + BODY_V_PADDING,
139
+ };
140
+ }
141
+
23
142
  // The nodes must have the same handle IDs to ensure edges connect correctly
24
143
  export const OUTPUTS_HANDLE_ID = "outputs";
25
144
  export const INPUTS_HANDLE_ID = "inputs";
26
145
 
146
+ /**
147
+ * Cells hidden by the reusable-functions filter: those the kernel marked as
148
+ * valid top-level definitions (serialization hint `Valid`).
149
+ */
150
+ export function reusableCellIds(
151
+ cellIds: CellId[],
152
+ cellRuntime: Record<CellId, CellRuntimeState>,
153
+ ): Set<CellId> {
154
+ const reusable = new Set<CellId>();
155
+ for (const cellId of cellIds) {
156
+ if (cellRuntime[cellId]?.serialization?.toLowerCase() === "valid") {
157
+ reusable.add(cellId);
158
+ }
159
+ }
160
+ return reusable;
161
+ }
162
+
163
+ /** Map each cell to the names of the variables it declares. */
164
+ export function computeDefsByCell(
165
+ variables: Variables,
166
+ ): Map<CellId, VariableName[]> {
167
+ const defsByCell = new Map<CellId, VariableName[]>();
168
+ for (const variable of Object.values(variables)) {
169
+ for (const cellId of variable.declaredBy) {
170
+ const defs = defsByCell.get(cellId) ?? [];
171
+ defs.push(variable.name);
172
+ defsByCell.set(cellId, defs);
173
+ }
174
+ }
175
+ return defsByCell;
176
+ }
177
+
27
178
  interface ElementsBuilder {
28
179
  createElements: (
29
180
  cellIds: CellId[],
@@ -59,16 +210,17 @@ export class VerticalElementsBuilder implements ElementsBuilder {
59
210
  private createNode(
60
211
  id: string,
61
212
  atom: Atom<CellData>,
213
+ defs: VariableName[],
62
214
  prevY: number,
63
215
  ): Node<NodeData> {
64
- const linesOfCode = store.get(atom).code.trim().split("\n").length;
65
- const height = getNodeHeight(linesOfCode);
216
+ const data: NodeData = { atom, defs };
217
+ const { width, height } = nodeDimensions(data);
66
218
  return {
67
219
  id: id,
68
- data: { atom },
69
- width: 250,
220
+ data,
221
+ width,
70
222
  type: "custom",
71
- height: height,
223
+ height,
72
224
  position: { x: 0, y: prevY + 20 },
73
225
  };
74
226
  }
@@ -83,8 +235,14 @@ export class VerticalElementsBuilder implements ElementsBuilder {
83
235
  let prevY = 0;
84
236
  const nodes: Node<NodeData>[] = [];
85
237
  const edges: Edge[] = [];
238
+ const defsByCell = computeDefsByCell(variables);
86
239
  for (const [cellId, cellAtom] of Arrays.zip(cellIds, cellAtoms)) {
87
- const node = this.createNode(cellId, cellAtom, prevY);
240
+ const node = this.createNode(
241
+ cellId,
242
+ cellAtom,
243
+ defsByCell.get(cellId) ?? [],
244
+ prevY,
245
+ );
88
246
  nodes.push(node);
89
247
  prevY = node.position.y + (node.height || 0);
90
248
  }
@@ -110,16 +268,24 @@ export class VerticalElementsBuilder implements ElementsBuilder {
110
268
  }
111
269
  }
112
270
 
271
+ interface AggregatedEdge {
272
+ source: CellId;
273
+ target: CellId;
274
+ variables: VariableName[];
275
+ isStateFlow: boolean;
276
+ }
277
+
113
278
  export class TreeElementsBuilder implements ElementsBuilder {
114
- private createEdge(source: CellId, target: CellId): Edge {
279
+ private createEdge(edge: AggregatedEdge): Edge<DependencyEdgeData> {
280
+ const { source, target, variables, isStateFlow } = edge;
115
281
  return {
116
- animated: true,
282
+ type: DEPENDENCY_EDGE_TYPE,
117
283
  markerEnd: {
118
284
  type: MarkerType.ArrowClosed,
285
+ color: isStateFlow ? STATE_FLOW_EDGE_COLOR : DEFAULT_EDGE_COLOR,
119
286
  },
120
287
  id: `${source}-${target}`,
121
- // Make thicker
122
- style: { strokeWidth: 2 },
288
+ data: { variables, isStateFlow },
123
289
  source: source,
124
290
  // Use the same handle ids as the custom node
125
291
  sourceHandle: OUTPUTS_HANDLE_ID,
@@ -128,15 +294,19 @@ export class TreeElementsBuilder implements ElementsBuilder {
128
294
  };
129
295
  }
130
296
 
131
- private createNode(id: string, atom: Atom<CellData>): Node<NodeData> {
132
- const linesOfCode = store.get(atom).code.trim().split("\n").length;
133
- const height = getNodeHeight(linesOfCode);
297
+ private createNode(
298
+ id: string,
299
+ atom: Atom<CellData>,
300
+ defs: VariableName[],
301
+ ): Node<NodeData> {
302
+ const data: NodeData = { atom, defs };
303
+ const { width, height } = nodeDimensions(data);
134
304
  return {
135
305
  id: id,
136
- data: { atom, forceWidth: 300 },
137
- width: 300,
306
+ data,
307
+ width,
138
308
  type: "custom",
139
- height: height,
309
+ height,
140
310
  position: { x: 0, y: 0 },
141
311
  };
142
312
  }
@@ -149,10 +319,19 @@ export class TreeElementsBuilder implements ElementsBuilder {
149
319
  hideReusableFunctions: boolean,
150
320
  ) {
151
321
  const nodes: Node<NodeData>[] = [];
152
- const edges: Edge[] = [];
322
+
323
+ const cellRuntime = getNotebook().cellRuntime;
324
+ // Hidden reusable cells are dropped along with their edges — a reusable
325
+ // function is nearly always referenced somewhere, so gating on "no edges"
326
+ // would make the filter a no-op.
327
+ const hiddenReusable = hideReusableFunctions
328
+ ? reusableCellIds(cellIds, cellRuntime)
329
+ : new Set<CellId>();
153
330
 
154
331
  const nodesWithEdges = new Set<CellId>();
155
- const visited = new Set<string>();
332
+ // Dedupe cell→cell edges, aggregating every variable that crosses them so a
333
+ // single edge can carry multiple refs and flag whether any is a state flow.
334
+ const edgesByPair = new Map<string, AggregatedEdge>();
156
335
 
157
336
  for (const variable of Object.values(variables)) {
158
337
  // Skip marimo, since likely every cell uses it
@@ -160,40 +339,55 @@ export class TreeElementsBuilder implements ElementsBuilder {
160
339
  continue;
161
340
  }
162
341
 
342
+ const isState = isStateFlowVariable(variable);
163
343
  const { declaredBy, usedBy } = variable;
164
344
  for (const fromId of declaredBy) {
165
345
  for (const toId of usedBy) {
166
- const key = `${fromId}-${toId}`;
167
- if (visited.has(key)) {
346
+ if (hiddenReusable.has(fromId) || hiddenReusable.has(toId)) {
168
347
  continue;
169
348
  }
170
- visited.add(key);
349
+ const key = `${fromId}-${toId}`;
350
+ const existing = edgesByPair.get(key);
351
+ if (existing) {
352
+ existing.variables.push(variable.name);
353
+ existing.isStateFlow ||= isState;
354
+ } else {
355
+ edgesByPair.set(key, {
356
+ source: fromId,
357
+ target: toId,
358
+ variables: [variable.name],
359
+ isStateFlow: isState,
360
+ });
361
+ }
171
362
  nodesWithEdges.add(fromId);
172
363
  nodesWithEdges.add(toId);
173
- edges.push(this.createEdge(fromId, toId));
174
364
  }
175
365
  }
176
366
  }
177
367
 
178
- const cellRuntime = getNotebook().cellRuntime;
368
+ const edges = [...edgesByPair.values()].map((edge) =>
369
+ this.createEdge(edge),
370
+ );
371
+
372
+ const defsByCell = computeDefsByCell(variables);
179
373
 
180
374
  for (const [cellId, cellAtom] of Arrays.zip(cellIds, cellAtoms)) {
181
375
  const code = store.get(cellAtom).code.trim();
182
376
  const hasEdge = nodesWithEdges.has(cellId);
183
377
  const isMarkdown = code.startsWith("mo.md");
184
- const runtime = cellRuntime[cellId];
185
- const isReusable = runtime?.serialization?.toLowerCase() === "valid";
186
378
 
187
379
  // Apply filters
188
380
  if (hidePureMarkdown && isMarkdown && !hasEdge) {
189
381
  continue;
190
382
  }
191
- if (hideReusableFunctions && isReusable && !hasEdge) {
383
+ if (hiddenReusable.has(cellId)) {
192
384
  continue;
193
385
  }
194
386
 
195
387
  // Show every cell that wasn't filtered out
196
- nodes.push(this.createNode(cellId, cellAtom));
388
+ nodes.push(
389
+ this.createNode(cellId, cellAtom, defsByCell.get(cellId) ?? []),
390
+ );
197
391
  }
198
392
 
199
393
  return { nodes, edges };
@@ -4,6 +4,7 @@ import {
4
4
  ArrowRightFromLineIcon,
5
5
  ArrowRightIcon,
6
6
  ArrowRightToLineIcon,
7
+ ExternalLinkIcon,
7
8
  MoreVerticalIcon,
8
9
  NetworkIcon,
9
10
  SettingsIcon,
@@ -13,17 +14,18 @@ import {
13
14
  } from "lucide-react";
14
15
  import React, { memo, useId } from "react";
15
16
  import { type Edge, Panel } from "reactflow";
16
- import { getCellEditorView } from "@/core/cells/cells";
17
+ import { getCellEditorView, useCellActions } from "@/core/cells/cells";
17
18
  import type { CellId } from "@/core/cells/ids";
18
19
  import { goToVariableDefinition } from "@/core/codemirror/go-to-definition/commands";
19
20
  import type { Variable, Variables } from "@/core/variables/types";
20
21
  import { ConnectionCellActionsDropdown } from "../editor/cell/cell-actions";
21
- import { CellLink } from "../editor/links/cell-link";
22
+ import { CellLink, scrollAndHighlightCell } from "../editor/links/cell-link";
22
23
  import { CellLinkList } from "../editor/links/cell-link-list";
23
24
  import { Button } from "../ui/button";
24
25
  import { Checkbox } from "../ui/checkbox";
25
26
  import { Label } from "../ui/label";
26
27
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
28
+ import { Tooltip } from "../ui/tooltip";
27
29
  import { VariableName } from "../variables/common";
28
30
  import type { GraphSelection, GraphSettings, LayoutDirection } from "./types";
29
31
 
@@ -122,6 +124,8 @@ export const GraphSelectionPanel: React.FC<{
122
124
  edges: Edge[];
123
125
  variables: Variables;
124
126
  }> = memo(({ selection, variables, onClearSelection }) => {
127
+ const { showCellIfHidden } = useCellActions();
128
+
125
129
  if (!selection) {
126
130
  return null;
127
131
  }
@@ -194,6 +198,23 @@ export const GraphSelectionPanel: React.FC<{
194
198
  <SquareFunction className="w-5 h-5" />
195
199
  <CellLink cellId={selection.id} />
196
200
  <div className="flex-1" />
201
+ <Tooltip content="Open cell in editor" delayDuration={200}>
202
+ <Button
203
+ aria-label="Open cell in editor"
204
+ variant="text"
205
+ size="icon"
206
+ onClick={() => {
207
+ // The cell may sit in a collapsed section; reveal it first
208
+ // or the scroll target won't be in the DOM.
209
+ showCellIfHidden({ cellId: selection.id });
210
+ requestAnimationFrame(() => {
211
+ scrollAndHighlightCell(selection.id, "focus");
212
+ });
213
+ }}
214
+ >
215
+ <ExternalLinkIcon className="w-4 h-4" />
216
+ </Button>
217
+ </Tooltip>
197
218
  <ConnectionCellActionsDropdown cellId={selection.id}>
198
219
  <Button variant="ghost" size="icon">
199
220
  <MoreVerticalIcon className="w-4 h-4" />
@@ -0,0 +1,78 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { Node } from "reactflow";
4
+ import { describe, expect, it } from "vitest";
5
+ import type { NodeData } from "../../elements";
6
+ import { boundingBox, placeIslands } from "../layout";
7
+
8
+ function node(
9
+ id: string,
10
+ x: number,
11
+ y: number,
12
+ width: number,
13
+ height: number,
14
+ ): Node<NodeData> {
15
+ return {
16
+ id,
17
+ position: { x, y },
18
+ width,
19
+ height,
20
+ data: { atom: {} as NodeData["atom"], defs: [] },
21
+ };
22
+ }
23
+
24
+ describe("boundingBox", () => {
25
+ it("is all-zero for an empty set", () => {
26
+ expect(boundingBox([])).toEqual({ minX: 0, minY: 0, maxX: 0, maxY: 0 });
27
+ });
28
+
29
+ it("spans position + size across nodes", () => {
30
+ const box = boundingBox([
31
+ node("a", 0, 0, 50, 50),
32
+ node("b", 10, 20, 100, 40),
33
+ ]);
34
+ expect(box).toEqual({ minX: 0, minY: 0, maxX: 110, maxY: 60 });
35
+ });
36
+
37
+ it("reads size from style.width/height when width/height are absent", () => {
38
+ const groupNode: Node = {
39
+ id: "g",
40
+ position: { x: 5, y: 5 },
41
+ data: {},
42
+ style: { width: 200, height: 100 },
43
+ };
44
+ expect(boundingBox([groupNode])).toEqual({
45
+ minX: 5,
46
+ minY: 5,
47
+ maxX: 205,
48
+ maxY: 105,
49
+ });
50
+ });
51
+ });
52
+
53
+ describe("placeIslands", () => {
54
+ it("returns nothing when there are no islands", () => {
55
+ expect(placeIslands([], [node("a", 0, 0, 100, 100)])).toEqual([]);
56
+ });
57
+
58
+ it("stacks islands in a column to the right of the placed graph", () => {
59
+ const placed = [node("main", 0, 0, 200, 300)];
60
+ const islands = [node("i0", 0, 0, 90, 40), node("i1", 0, 0, 90, 40)];
61
+ const result = placeIslands(islands, placed);
62
+ // startX = maxX(200) + margin(40) = 240; startY = minY(0).
63
+ expect(result[0].position).toEqual({ x: 240, y: 0 });
64
+ // Second island stacked below the first (height 40 + gap 20).
65
+ expect(result[1].position).toEqual({ x: 240, y: 60 });
66
+ });
67
+
68
+ it("wraps into a new column when the column gets too tall", () => {
69
+ const placed = [node("main", 0, 0, 200, 300)];
70
+ const islands = Array.from({ length: 10 }, (_, i) =>
71
+ node(`i${i}`, 0, 0, 90, 40),
72
+ );
73
+ const result = placeIslands(islands, placed);
74
+ const firstColumnX = result[0].position.x;
75
+ const wrapped = result.find((n) => n.position.x > firstColumnX);
76
+ expect(wrapped).toBeDefined();
77
+ });
78
+ });
@@ -1,45 +1,135 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { graphlib, layout } from "@dagrejs/dagre";
3
3
  import type { Edge, Node } from "reactflow";
4
+ import type { NodeData } from "../elements";
4
5
  import type { LayoutDirection } from "../types";
5
6
 
6
- const g = new graphlib.Graph().setDefaultEdgeLabel(() => ({}));
7
+ // Disconnected cells are stacked in a compact column to the side of the main
8
+ // graph, like TensorBoard's extracted/auxiliary nodes (extractXOffset 15,
9
+ // extractYOffset 20).
10
+ const ISLAND_GAP = 20;
11
+ const ISLAND_MARGIN = 40;
12
+ const ISLAND_MIN_COLUMN_HEIGHT = 320;
7
13
 
14
+ /**
15
+ * Lay out cell nodes with dagre. Disconnected cells are pulled out of the
16
+ * dagre flow and stacked in a compact column to the side of the main graph.
17
+ */
8
18
  export const layoutElements = ({
9
19
  nodes,
10
20
  edges,
11
21
  direction,
12
22
  }: {
13
- nodes: Node[];
23
+ nodes: Node<NodeData>[];
14
24
  edges: Edge[];
15
25
  direction: LayoutDirection;
16
- }) => {
26
+ }): { nodes: Node<NodeData>[]; edges: Edge[] } => {
27
+ const g = new graphlib.Graph().setDefaultEdgeLabel(() => ({}));
17
28
  g.setGraph({
18
29
  rankdir: direction,
19
- nodesep: 150,
20
- ranksep: 200,
21
- // So far, longest-path seems to give the best results as trees are
22
- // generally quite wide.
23
- ranker: "longest-path",
30
+ // Tight spacing mirroring TensorBoard's graph (nodesep 5, ranksep 25,
31
+ // edgesep 5), nudged up a little since our nodes are wider ref boxes than
32
+ // TensorBoard's op ellipses. The dagre default ranker (network-simplex)
33
+ // gives cleaner layered routing with fewer crossings than longest-path.
34
+ nodesep: 20,
35
+ ranksep: 30,
36
+ edgesep: 10,
24
37
  });
25
38
 
39
+ // Disconnected cells are pulled out of the dagre flow and stacked to the
40
+ // side, so they don't stretch the main graph's layout.
41
+ const connectedIds = new Set<string>();
42
+ for (const edge of edges) {
43
+ connectedIds.add(edge.source);
44
+ connectedIds.add(edge.target);
45
+ }
46
+ const isIsland = (node: Node<NodeData>) => !connectedIds.has(node.id);
47
+ const flowNodes = nodes.filter((node) => !isIsland(node));
48
+ const islands = nodes.filter(isIsland);
49
+
26
50
  edges.forEach((edge) => g.setEdge(edge.source, edge.target));
27
- nodes.forEach((node) =>
51
+ flowNodes.forEach((node) => {
28
52
  g.setNode(node.id, {
29
53
  ...node,
30
54
  width: node.width ?? 0,
31
55
  height: node.height ?? 0,
32
- }),
33
- );
56
+ });
57
+ });
34
58
 
35
59
  layout(g);
36
60
 
37
- return {
38
- nodes: nodes.map((node) => {
39
- const { x, y } = g.node(node.id);
40
-
41
- return { ...node, position: { x, y } };
42
- }),
43
- edges,
61
+ // dagre reports node coords as centers; ReactFlow wants top-left.
62
+ const topLeft = (id: string) => {
63
+ const { x, y, width, height } = g.node(id);
64
+ return { x: x - width / 2, y: y - height / 2 };
44
65
  };
66
+
67
+ const cellNodes = flowNodes.map((node) => ({
68
+ ...node,
69
+ position: topLeft(node.id),
70
+ }));
71
+
72
+ const islandNodes = placeIslands(islands, cellNodes);
73
+ return { nodes: [...cellNodes, ...islandNodes], edges };
45
74
  };
75
+
76
+ /** Bounding box of already-positioned nodes (top-left position + size). */
77
+ export function boundingBox(nodes: Node[]): {
78
+ minX: number;
79
+ minY: number;
80
+ maxX: number;
81
+ maxY: number;
82
+ } {
83
+ if (nodes.length === 0) {
84
+ return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
85
+ }
86
+ let minX = Infinity;
87
+ let minY = Infinity;
88
+ let maxX = -Infinity;
89
+ let maxY = -Infinity;
90
+ for (const node of nodes) {
91
+ const styleW = node.style?.width;
92
+ const styleH = node.style?.height;
93
+ const w = node.width ?? (typeof styleW === "number" ? styleW : 0);
94
+ const h = node.height ?? (typeof styleH === "number" ? styleH : 0);
95
+ minX = Math.min(minX, node.position.x);
96
+ minY = Math.min(minY, node.position.y);
97
+ maxX = Math.max(maxX, node.position.x + w);
98
+ maxY = Math.max(maxY, node.position.y + h);
99
+ }
100
+ return { minX, minY, maxX, maxY };
101
+ }
102
+
103
+ /**
104
+ * Stack disconnected nodes in a compact column (wrapping into more columns when
105
+ * tall) just to the right of the main graph.
106
+ */
107
+ export function placeIslands(
108
+ islands: Node<NodeData>[],
109
+ placed: Node[],
110
+ ): Node<NodeData>[] {
111
+ if (islands.length === 0) {
112
+ return [];
113
+ }
114
+ const box = boundingBox(placed);
115
+ const startX = (placed.length === 0 ? 0 : box.maxX) + ISLAND_MARGIN;
116
+ const startY = placed.length === 0 ? 0 : box.minY;
117
+ const columnHeight = Math.max(box.maxY - box.minY, ISLAND_MIN_COLUMN_HEIGHT);
118
+
119
+ let x = startX;
120
+ let y = startY;
121
+ let columnWidth = 0;
122
+ return islands.map((node) => {
123
+ const h = node.height ?? 0;
124
+ if (y > startY && y + h > startY + columnHeight) {
125
+ // Wrap to the next column.
126
+ x += columnWidth + ISLAND_MARGIN;
127
+ y = startY;
128
+ columnWidth = 0;
129
+ }
130
+ const position = { x, y };
131
+ y += h + ISLAND_GAP;
132
+ columnWidth = Math.max(columnWidth, node.width ?? 0);
133
+ return { ...node, position };
134
+ });
135
+ }