@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
@@ -206,7 +206,7 @@ export const FileActionsDropdown = ({
206
206
  </DropdownMenuTrigger>
207
207
  <DropdownMenuContent
208
208
  align="end"
209
- className={contentClassName ?? "print:hidden w-[220px]"}
209
+ className={contentClassName ?? "print:hidden w-[240px]"}
210
210
  onClick={(e) => e.stopPropagation()}
211
211
  onCloseAutoFocus={(e) => e.preventDefault()}
212
212
  >
@@ -0,0 +1,142 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { FileIcon, FolderIcon } from "lucide-react";
4
+ import type { RefObject } from "react";
5
+ import {
6
+ Tree,
7
+ type NodeRendererProps,
8
+ type RowRendererProps,
9
+ type TreeApi,
10
+ } from "react-arborist";
11
+ import { Spinner } from "@/components/icons/spinner";
12
+ import { cn } from "@/utils/cn";
13
+ import { useTreeDndManager } from "./dnd-wrapper";
14
+ import { FileTreeRow } from "./file-tree-row";
15
+ import type { FileTreeNode } from "./requesting-tree";
16
+ import type { useFileSearch } from "./use-file-search";
17
+
18
+ export function FileSearchResults({
19
+ treeRef,
20
+ search,
21
+ height,
22
+ onReveal,
23
+ }: {
24
+ treeRef: RefObject<TreeApi<FileTreeNode> | null>;
25
+ search: ReturnType<typeof useFileSearch>;
26
+ height: number;
27
+ onReveal: (node: FileTreeNode) => Promise<void>;
28
+ }) {
29
+ const dndManager = useTreeDndManager();
30
+ const { state, refetch } = search;
31
+ if (state.status === "idle") {
32
+ return null;
33
+ }
34
+
35
+ if (state.status === "error") {
36
+ return (
37
+ <div role="alert" className="px-3 py-2 text-sm">
38
+ Could not search files.{" "}
39
+ <button type="button" className="underline" onClick={refetch}>
40
+ Retry
41
+ </button>
42
+ </div>
43
+ );
44
+ }
45
+ if (state.status === "loading") {
46
+ return (
47
+ <div
48
+ role="status"
49
+ className="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground"
50
+ >
51
+ <Spinner size="small" />
52
+ Searching files…
53
+ </div>
54
+ );
55
+ }
56
+ const { files, hasMore, isPartial } = state;
57
+ const count = files.length;
58
+ return (
59
+ <div
60
+ onKeyDownCapture={(event) => {
61
+ if (
62
+ event.key === "Enter" &&
63
+ event.target instanceof HTMLElement &&
64
+ event.target.closest('[role="tree"]')
65
+ ) {
66
+ event.preventDefault();
67
+ event.stopPropagation();
68
+ treeRef.current?.focusedNode?.activate();
69
+ }
70
+ }}
71
+ >
72
+ {isPartial && (
73
+ <div role="alert" className="px-3 py-2 text-xs text-muted-foreground">
74
+ Some locations could not be searched.{" "}
75
+ <button type="button" className="underline" onClick={refetch}>
76
+ Retry
77
+ </button>
78
+ </div>
79
+ )}
80
+ <p role="status" className="px-3 py-2 text-xs text-muted-foreground">
81
+ {files.length
82
+ ? `${count} ${count === 1 ? "match" : "matches"}`
83
+ : "No matching files or folders."}
84
+ {hasMore && " · Refine your search for more results."}
85
+ </p>
86
+ <Tree<FileTreeNode>
87
+ ref={treeRef}
88
+ data={files}
89
+ childrenAccessor={() => null}
90
+ width="100%"
91
+ height={Math.max(0, height - 48 - (isPartial ? 48 : 0))}
92
+ rowHeight={48}
93
+ overscanCount={8}
94
+ dndManager={dndManager}
95
+ disableDrag={true}
96
+ disableDrop={true}
97
+ disableEdit={true}
98
+ disableMultiSelection={true}
99
+ selectionFollowsFocus={true}
100
+ rowClassName="outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring"
101
+ renderRow={SearchResultRow}
102
+ onActivate={(node) => {
103
+ void onReveal(node.data);
104
+ }}
105
+ >
106
+ {SearchResult}
107
+ </Tree>
108
+ <p className="px-3 text-xs text-muted-foreground">
109
+ Enter or double-click to open · Esc to clear
110
+ </p>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ function SearchResultRow(props: RowRendererProps<FileTreeNode>) {
116
+ return <FileTreeRow {...props} ariaLabel={props.node.data.path} />;
117
+ }
118
+
119
+ function SearchResult({ node }: NodeRendererProps<FileTreeNode>) {
120
+ const Icon = node.data.isDirectory ? FolderIcon : FileIcon;
121
+ return (
122
+ <div
123
+ className={cn(
124
+ "flex h-full items-start gap-2.5 px-3 py-1 cursor-pointer hover:bg-accent/50",
125
+ node.isSelected && "bg-accent/60",
126
+ )}
127
+ onDoubleClick={(event) => {
128
+ event.stopPropagation();
129
+ node.activate();
130
+ }}
131
+ title={node.data.path}
132
+ >
133
+ <Icon className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
134
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5">
135
+ <div className="truncate text-sm leading-5">{node.data.name}</div>
136
+ <div className="truncate text-xs leading-4 text-muted-foreground">
137
+ {node.data.path}
138
+ </div>
139
+ </div>
140
+ </div>
141
+ );
142
+ }
@@ -0,0 +1,40 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { RowRendererProps } from "react-arborist";
4
+ import type { FileTreeNode } from "./requesting-tree";
5
+
6
+ export function FileTreeRow({
7
+ node,
8
+ attrs,
9
+ innerRef,
10
+ children,
11
+ ariaLabel = node.data.name,
12
+ }: RowRendererProps<FileTreeNode> & { ariaLabel?: string }) {
13
+ return (
14
+ <div
15
+ {...attrs}
16
+ ref={innerRef}
17
+ aria-label={ariaLabel}
18
+ aria-expanded={node.isInternal ? node.isOpen : undefined}
19
+ onFocus={(event) => {
20
+ event.stopPropagation();
21
+ attrs.onFocus?.(event);
22
+ if (event.target === event.currentTarget && !node.isFocused) {
23
+ node.focus();
24
+ }
25
+ }}
26
+ onClick={(event) => {
27
+ if (attrs.onClick) {
28
+ attrs.onClick(event);
29
+ } else {
30
+ node.select();
31
+ }
32
+ if (!event.defaultPrevented) {
33
+ event.currentTarget.focus({ preventScroll: true });
34
+ }
35
+ }}
36
+ >
37
+ {children}
38
+ </div>
39
+ );
40
+ }
@@ -156,11 +156,12 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
156
156
  onDownload={disableFileDownloads ? undefined : handleDownload}
157
157
  actions={
158
158
  <>
159
- {file.isMarimoFile && onOpenNotebook && !isWasm() && (
159
+ {data.file.isMarimoFile && onOpenNotebook && !isWasm() && (
160
160
  <Tooltip content="Open notebook">
161
161
  <Button
162
162
  variant="text"
163
163
  size="xs"
164
+ aria-label="Open notebook"
164
165
  onClick={(evt) => onOpenNotebook(evt)}
165
166
  >
166
167
  <ExternalLinkIcon className="h-3.5 w-3.5" />
@@ -189,6 +189,8 @@ export const MediaRenderer: React.FC<MediaSource & { mimeType: string }> = ({
189
189
  * based on MIME type. Used by both the local file viewer and the
190
190
  * storage file viewer.
191
191
  */
192
+ const EMPTY_EXTENSIONS: readonly Extension[] = [];
193
+
192
194
  export const FileContentRenderer: React.FC<{
193
195
  mimeType: string;
194
196
  filename?: string;
@@ -199,7 +201,7 @@ export const FileContentRenderer: React.FC<{
199
201
  readOnly?: boolean;
200
202
  onChange?: (value: string) => void;
201
203
  /** Additional CodeMirror extensions (e.g. save hotkey). */
202
- extensions?: Extension[];
204
+ extensions?: readonly Extension[];
203
205
  }> = ({
204
206
  mimeType,
205
207
  filename,
@@ -207,7 +209,7 @@ export const FileContentRenderer: React.FC<{
207
209
  mediaSource,
208
210
  readOnly = true,
209
211
  onChange,
210
- extensions = [],
212
+ extensions = EMPTY_EXTENSIONS,
211
213
  }) => {
212
214
  const { theme } = useTheme();
213
215
 
@@ -22,6 +22,7 @@ export type FileTreeNode = Omit<FileInfo, "children"> & {
22
22
  isRoot: boolean;
23
23
  rootPath: string;
24
24
  isPrimaryRoot: boolean;
25
+ loadState?: "unloaded" | "loading" | "loaded" | "error";
25
26
  };
26
27
 
27
28
  /**
@@ -66,6 +67,7 @@ export class RequestingTree {
66
67
  }
67
68
 
68
69
  private roots: FileRoot[] = [];
70
+ private pendingExpansions = new Map<string, Promise<boolean>>();
69
71
  private onChange: (data: FileTreeNode[]) => void = Functions.NOOP;
70
72
  private path = new PathBuilder("/");
71
73
 
@@ -87,12 +89,7 @@ export class RequestingTree {
87
89
  const data = await this.callbacks.listFiles({
88
90
  path: primaryRoot.path,
89
91
  });
90
- this.delegate.update({
91
- id: this.getPrimaryRootId(),
92
- changes: {
93
- children: annotateFiles(data.files, primaryRoot),
94
- },
95
- });
92
+ this.updateDirectory(this.getPrimaryRootId(), data.files);
96
93
  }
97
94
  } catch (error) {
98
95
  toast({
@@ -105,24 +102,91 @@ export class RequestingTree {
105
102
  this.emitChange();
106
103
  };
107
104
 
108
- async expand(id: string): Promise<boolean> {
105
+ expand(id: string): Promise<boolean> {
106
+ return this.requestDirectory(id);
107
+ }
108
+
109
+ private requestDirectory(id: string, force = false): Promise<boolean> {
110
+ const pending = this.pendingExpansions.get(id);
111
+ if (pending) {
112
+ return pending;
113
+ }
109
114
  const node = this.delegate.find(id);
110
115
  if (!node?.data.isDirectory) {
111
- return false;
116
+ return Promise.resolve(false);
112
117
  }
113
118
 
114
- if (node.children && node.children.length > 0) {
115
- return true;
119
+ if (!force && node.data.loadState === "loaded") {
120
+ return Promise.resolve(true);
116
121
  }
117
122
 
118
- const data = await this.callbacks.listFiles({ path: node.data.path });
119
- this.delegate.update({
120
- id,
121
- changes: {
122
- children: annotateFiles(data.files, this.getRootForNode(node.data)),
123
- },
123
+ const request = this.loadDirectory(id, node.data).finally(() => {
124
+ this.pendingExpansions.delete(id);
124
125
  });
126
+ this.pendingExpansions.set(id, request);
127
+ return request;
128
+ }
129
+
130
+ private async loadDirectory(
131
+ id: string,
132
+ node: FileTreeNode,
133
+ ): Promise<boolean> {
134
+ this.delegate.update({ id, changes: { loadState: "loading" } });
125
135
  this.emitChange();
136
+ try {
137
+ const data = await this.callbacks.listFiles({ path: node.path });
138
+ this.updateDirectory(id, data.files);
139
+ return true;
140
+ } catch (error) {
141
+ this.delegate.update({ id, changes: { loadState: "error" } });
142
+ toast({
143
+ title: `Could not load ${node.name}`,
144
+ description: prettyError(error),
145
+ });
146
+ return false;
147
+ } finally {
148
+ this.emitChange();
149
+ }
150
+ }
151
+
152
+ getRoots(): readonly FileRoot[] {
153
+ return this.roots;
154
+ }
155
+
156
+ async reveal(node: FileTreeNode): Promise<boolean> {
157
+ const relative = relativePath(node.path as FilePath, node.rootPath);
158
+ if (relative === null) {
159
+ return false;
160
+ }
161
+ let path = node.rootPath as FilePath;
162
+ const parts = relative.split(pathDelimiter(node.rootPath)).filter(Boolean);
163
+ if (!node.isDirectory) {
164
+ parts.pop();
165
+ }
166
+ for (const part of [null, ...parts]) {
167
+ if (part !== null) {
168
+ const parentId = fileTreeNodeId(node.rootPath, path);
169
+ path = joinPath(path, part);
170
+ // Search sees the live filesystem; a cached parent may not contain
171
+ // a directory that was created since its last listing.
172
+ if (!this.delegate.find(fileTreeNodeId(node.rootPath, path))) {
173
+ if (!(await this.requestDirectory(parentId, true))) {
174
+ return false;
175
+ }
176
+ }
177
+ }
178
+ const id = fileTreeNodeId(node.rootPath, path);
179
+ if (!this.delegate.find(id)) {
180
+ toast({
181
+ title: "Could not reveal folder",
182
+ description: `The folder may have moved or been deleted: ${path}`,
183
+ });
184
+ return false;
185
+ }
186
+ if (!(await this.expand(id))) {
187
+ return false;
188
+ }
189
+ }
126
190
  return true;
127
191
  }
128
192
 
@@ -252,7 +316,13 @@ export class RequestingTree {
252
316
  .map((id) => this.delegate.find(id)?.data.path)
253
317
  .filter((path): path is string => Boolean(path)),
254
318
  ];
255
- await this.refreshPaths(paths);
319
+ await this.refreshPaths(
320
+ paths,
321
+ new Set([
322
+ ...this.roots.map((root) => fileTreeNodeId(root.path, root.path)),
323
+ ...ids,
324
+ ]),
325
+ );
256
326
  };
257
327
 
258
328
  refreshPath = async (path: FilePath): Promise<void> => {
@@ -344,8 +414,13 @@ export class RequestingTree {
344
414
  return node;
345
415
  }
346
416
 
347
- private refreshPaths = async (paths: string[]): Promise<void> => {
348
- const uniquePaths = [...new Set(paths)];
417
+ private refreshPaths = async (
418
+ paths: string[],
419
+ retainedIds?: ReadonlySet<string>,
420
+ ): Promise<void> => {
421
+ const uniquePaths = [...new Set(paths)].toSorted(
422
+ (left, right) => left.length - right.length,
423
+ );
349
424
  if (uniquePaths.length === 0) {
350
425
  return;
351
426
  }
@@ -360,31 +435,84 @@ export class RequestingTree {
360
435
  if (!result) {
361
436
  continue;
362
437
  }
363
- // The same absolute path may appear below multiple overlapping roots.
364
- // Refresh every occurrence, while keeping each occurrence in its own ID
365
- // namespace and preserving its root metadata.
438
+ // Fetch overlapping paths once, but retain loaded folders independently
439
+ // for each root-qualified occurrence.
366
440
  for (const root of this.roots) {
367
- const node = this.delegate.find(fileTreeNodeId(root.path, path));
368
- if (!node?.data.isDirectory) {
369
- continue;
441
+ const id = fileTreeNodeId(root.path, path);
442
+ if (!retainedIds || retainedIds.has(id)) {
443
+ this.updateDirectory(id, result.files, retainedIds);
370
444
  }
371
- this.delegate.update({
372
- id: node.id,
373
- changes: {
374
- children: annotateFiles(result.files, root),
375
- },
376
- });
377
445
  }
378
446
  }
379
447
  this.emitChange();
380
448
  };
381
449
 
450
+ private updateDirectory(
451
+ id: string,
452
+ files: FileInfo[],
453
+ retainedIds?: ReadonlySet<string>,
454
+ ): void {
455
+ const node = this.delegate.find(id)?.data;
456
+ if (!node?.isDirectory) {
457
+ return;
458
+ }
459
+ const children = mergeDirectoryChildren(
460
+ annotateFiles(files, this.getRootForNode(node)),
461
+ node.children,
462
+ );
463
+ this.delegate.update({
464
+ id,
465
+ changes: {
466
+ children: retainedIds
467
+ ? invalidateClosedDirectories(children, retainedIds)
468
+ : children,
469
+ loadState: "loaded",
470
+ },
471
+ });
472
+ }
473
+
382
474
  private emitChange(): void {
383
475
  const data = this.delegate.data;
384
- this.onChange(this.roots.length === 1 ? (data[0]?.children ?? []) : data);
476
+ // SimpleTree mutates child arrays in place. Publish a new array so React
477
+ // observes completed directory loads even when the root is flattened.
478
+ this.onChange(
479
+ this.roots.length === 1 ? [...(data[0]?.children ?? [])] : data,
480
+ );
385
481
  }
386
482
  }
387
483
 
484
+ function mergeDirectoryChildren(
485
+ files: FileTreeNode[],
486
+ previous: FileTreeNode[],
487
+ ): FileTreeNode[] {
488
+ const byId = new Map(previous.map((node) => [node.id, node]));
489
+ return files.map((file) => {
490
+ const old = byId.get(file.id);
491
+ if (!file.isDirectory || !old?.isDirectory) {
492
+ return file;
493
+ }
494
+ return { ...file, children: old.children, loadState: old.loadState };
495
+ });
496
+ }
497
+
498
+ function invalidateClosedDirectories(
499
+ files: FileTreeNode[],
500
+ openIds: ReadonlySet<string>,
501
+ ): FileTreeNode[] {
502
+ return files.map((file) => {
503
+ if (!file.isDirectory) {
504
+ return file;
505
+ }
506
+ if (!openIds.has(file.id)) {
507
+ return { ...file, children: [], loadState: "unloaded" };
508
+ }
509
+ return {
510
+ ...file,
511
+ children: invalidateClosedDirectories(file.children, openIds),
512
+ };
513
+ });
514
+ }
515
+
388
516
  function toRootNode(root: FileRoot): FileTreeNode {
389
517
  return {
390
518
  id: fileTreeNodeId(root.path, root.path),
@@ -396,6 +524,7 @@ function toRootNode(root: FileRoot): FileTreeNode {
396
524
  isRoot: true,
397
525
  rootPath: root.path,
398
526
  isPrimaryRoot: root.isPrimary,
527
+ loadState: "unloaded",
399
528
  };
400
529
  }
401
530
 
@@ -404,6 +533,7 @@ function annotateFiles(files: FileInfo[], root: FileRoot): FileTreeNode[] {
404
533
  ...file,
405
534
  id: fileTreeNodeId(root.path, file.path),
406
535
  children: annotateFiles(file.children ?? [], root),
536
+ loadState: file.children?.length ? "loaded" : "unloaded",
407
537
  isRoot: false,
408
538
  rootPath: root.path,
409
539
  isPrimaryRoot: root.isPrimary,
@@ -0,0 +1,142 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { useRequestClient } from "@/core/network/requests";
5
+ import { useDebounce } from "@/hooks/useDebounce";
6
+ import {
7
+ fileTreeNodeId,
8
+ type FileTreeNode,
9
+ type RequestingTree,
10
+ } from "./requesting-tree";
11
+
12
+ const RESULT_LIMIT = 200;
13
+
14
+ export type FileSearchState =
15
+ | { status: "idle" }
16
+ | { status: "loading"; query: string }
17
+ | { status: "error"; query: string; error: unknown }
18
+ | {
19
+ status: "success";
20
+ query: string;
21
+ files: FileTreeNode[];
22
+ hasMore: boolean;
23
+ isPartial: boolean;
24
+ };
25
+
26
+ export function useFileSearch({
27
+ query,
28
+ tree,
29
+ showHiddenFiles,
30
+ }: {
31
+ query: string;
32
+ tree: RequestingTree;
33
+ showHiddenFiles: boolean;
34
+ }): { state: FileSearchState; refetch: () => void } {
35
+ const { sendSearchFiles } = useRequestClient();
36
+ const debouncedQuery = useDebounce(query, 300);
37
+ const pending = useRef<Promise<void>>(Promise.resolve());
38
+ const [revision, setRevision] = useState(0);
39
+ const refetch = useCallback(() => setRevision((value) => value + 1), []);
40
+ const [result, setResult] = useState<FileSearchState>({ status: "idle" });
41
+
42
+ useEffect(() => {
43
+ let cancelled = false;
44
+ if (!query || query !== debouncedQuery) {
45
+ return;
46
+ }
47
+ setResult({ status: "loading", query });
48
+ // Let the in-flight server scan finish before starting another. Superseded
49
+ // queued queries and remaining roots are skipped, keeping scan work bounded.
50
+ const request = pending.current.then(async () => {
51
+ const filesByPath = new Map<string, FileTreeNode>();
52
+ let rootLimitReached = false;
53
+ let failedRootCount = 0;
54
+ const roots = tree.getRoots();
55
+ for (const root of roots) {
56
+ if (cancelled) {
57
+ return;
58
+ }
59
+ try {
60
+ const response = await sendSearchFiles({
61
+ query,
62
+ path: root.path,
63
+ includeFiles: true,
64
+ includeDirectories: true,
65
+ includeHidden: showHiddenFiles,
66
+ depth: 20,
67
+ limit: RESULT_LIMIT,
68
+ });
69
+ rootLimitReached ||= response.files.length >= RESULT_LIMIT;
70
+ for (const file of response.files) {
71
+ const existing = filesByPath.get(file.path);
72
+ if (existing && (!root.isPrimary || existing.isPrimaryRoot)) {
73
+ continue;
74
+ }
75
+ filesByPath.set(file.path, {
76
+ ...file,
77
+ id: fileTreeNodeId(root.path, file.path),
78
+ children: [],
79
+ isRoot: false,
80
+ rootPath: root.path,
81
+ isPrimaryRoot: root.isPrimary,
82
+ });
83
+ }
84
+ } catch {
85
+ failedRootCount += 1;
86
+ }
87
+ }
88
+ if (!cancelled) {
89
+ if (roots.length > 0 && failedRootCount === roots.length) {
90
+ setResult({
91
+ status: "error",
92
+ query,
93
+ error: new Error("Could not search any root"),
94
+ });
95
+ return;
96
+ }
97
+ const rank = (file: FileTreeNode) => {
98
+ const name = file.name.toLowerCase();
99
+ const needle = query.toLowerCase();
100
+ if (name === needle) {
101
+ return 0;
102
+ }
103
+ if (name.startsWith(needle)) {
104
+ return 1;
105
+ }
106
+ return 2;
107
+ };
108
+ const sortedFiles = [...filesByPath.values()].toSorted(
109
+ (left, right) =>
110
+ rank(left) - rank(right) || left.name.localeCompare(right.name),
111
+ );
112
+ setResult({
113
+ status: "success",
114
+ isPartial: failedRootCount > 0,
115
+ query,
116
+ files: sortedFiles.slice(0, RESULT_LIMIT),
117
+ hasMore: rootLimitReached || sortedFiles.length > RESULT_LIMIT,
118
+ });
119
+ }
120
+ });
121
+ pending.current = request.catch((error: unknown) => {
122
+ if (!cancelled) {
123
+ setResult({ status: "error", query, error });
124
+ }
125
+ });
126
+ return () => {
127
+ cancelled = true;
128
+ };
129
+ }, [query, debouncedQuery, tree, showHiddenFiles, revision, sendSearchFiles]);
130
+
131
+ if (!query) {
132
+ return { state: { status: "idle" }, refetch };
133
+ }
134
+ if (
135
+ query !== debouncedQuery ||
136
+ result.status === "idle" ||
137
+ result.query !== query
138
+ ) {
139
+ return { state: { status: "loading", query }, refetch };
140
+ }
141
+ return { state: result, refetch };
142
+ }
@@ -0,0 +1,34 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useEffect, useRef } from "react";
4
+ import type { NodeApi } from "react-arborist";
5
+ import type { FileTreeNode } from "./requesting-tree";
6
+
7
+ export const HOVER_EXPAND_DELAY = 600;
8
+
9
+ export function useHoverExpand(
10
+ node: NodeApi<FileTreeNode>,
11
+ isHovering: boolean,
12
+ ) {
13
+ const tree = node.tree;
14
+ const attemptedNode = useRef<string | null>(null);
15
+ useEffect(() => {
16
+ if (!isHovering) {
17
+ attemptedNode.current = null;
18
+ return;
19
+ }
20
+ if (
21
+ !node.data.isDirectory ||
22
+ node.isOpen ||
23
+ attemptedNode.current === node.id
24
+ ) {
25
+ return;
26
+ }
27
+ const timeout = window.setTimeout(() => {
28
+ // A failed load closes the folder. Retry only after leaving and reentering.
29
+ attemptedNode.current = node.id;
30
+ tree.open(node.id);
31
+ }, HOVER_EXPAND_DELAY);
32
+ return () => window.clearTimeout(timeout);
33
+ }, [isHovering, node.data.isDirectory, node.isOpen, node.id, tree]);
34
+ }