@marimo-team/frontend 0.25.2-dev0 → 0.25.2-dev10

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 (133) hide show
  1. package/dist/assets/{ConnectedDataExplorerComponent-DJ5jwKfp.js → ConnectedDataExplorerComponent-BFDHLGyp.js} +1 -1
  2. package/dist/assets/{ErrorBoundary-BeE1cdgM.js → ErrorBoundary-CtGZ1MAN.js} +2 -2
  3. package/dist/assets/{LazyAnyLanguageCodeMirror-B_IYTCX5.js → LazyAnyLanguageCodeMirror-CaEYnB8x.js} +2 -2
  4. package/dist/assets/{RunButton-C2kFUhyO.js → RunButton-YN1KKz4a.js} +1 -1
  5. package/dist/assets/{abnfDiagram-VCTEODGH-CSPJSGaM.js → abnfDiagram-VCTEODGH-CqPJWAgx.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-BFgdVPaN.js → add-cell-with-ai-Dceg80EG.js} +1 -1
  7. package/dist/assets/{agent-panel-Dh7yWh9P.js → agent-panel-CEG8eS3F.js} +1 -1
  8. package/dist/assets/{ai-model-dropdown-DJ2elMLM.js → ai-model-dropdown-SOxGsjRY.js} +1 -1
  9. package/dist/assets/{alert-dialog-CeE3i3Qa.js → alert-dialog-BsB0FYEL.js} +1 -1
  10. package/dist/assets/{any-language-editor-Cj_HzlZY.js → any-language-editor-b9spizfh.js} +1 -1
  11. package/dist/assets/{app-config-button-DBCTefOX.js → app-config-button-CTTg_TIX.js} +1 -1
  12. package/dist/assets/{architectureDiagram-5GKGNRK7-7iukRoC9.js → architectureDiagram-5GKGNRK7-DXjBUUE6.js} +1 -1
  13. package/dist/assets/{blockDiagram-I7D4REHJ-cuhpQB60.js → blockDiagram-I7D4REHJ-DS6I1u-N.js} +1 -1
  14. package/dist/assets/{button-BoklZTGL.js → button-qFeU84ax.js} +1 -1
  15. package/dist/assets/{cache-panel-C0oB1kE2.js → cache-panel-Ct9EFgP7.js} +1 -1
  16. package/dist/assets/{capabilities-Bj3s1hGN.js → capabilities-BqUNf14U.js} +1 -1
  17. package/dist/assets/{cells-CzugpTQu.js → cells-CU9huD49.js} +1 -1
  18. package/dist/assets/{chat-display-DevO4F8W.js → chat-display-DWPnai8M.js} +1 -1
  19. package/dist/assets/{chat-panel-BGw0bqDk.js → chat-panel-DGnu6v96.js} +1 -1
  20. package/dist/assets/{chat-ui-CZZFGlZi.js → chat-ui-BqLN7iW4.js} +1 -1
  21. package/dist/assets/{chunk-IMKFNOWR-BIILwBhp.js → chunk-IMKFNOWR-s8kQsL8K.js} +1 -1
  22. package/dist/assets/{chunk-SHT3W25Y-BtLGdeDB.js → chunk-SHT3W25Y-CM7-8w1-.js} +1 -1
  23. package/dist/assets/{chunk-SVP7TREG-B0UfxhUP.js → chunk-SVP7TREG-BErtDMhh.js} +1 -1
  24. package/dist/assets/{chunk-TICWLB2K-BP7vel_P.js → chunk-TICWLB2K-CGVR3aYq.js} +1 -1
  25. package/dist/assets/{circle-CeQt3rdW.js → circle-CzTMKZwz.js} +1 -1
  26. package/dist/assets/{classDiagram-ZZMXUADV-CoNXKNMZ.js → classDiagram-ZZMXUADV-isldMhyu.js} +1 -1
  27. package/dist/assets/{classDiagram-v2-VYDZK3BY-CoNXKNMZ.js → classDiagram-v2-VYDZK3BY-isldMhyu.js} +1 -1
  28. package/dist/assets/{code-visibility-BFNYI7hr.js → code-visibility-BGq9NyQN.js} +1 -1
  29. package/dist/assets/{command-palette-DN4JoOLD.js → command-palette-CyBHUvlK.js} +1 -1
  30. package/dist/assets/{common-DUCPhoyP.js → common-CdJQaC_v.js} +1 -1
  31. package/dist/assets/{components-CK7CeYlo.js → components-DsJLlugL.js} +1 -1
  32. package/dist/assets/{components-DAmFBASm.js → components-zfCkpDjF.js} +1 -1
  33. package/dist/assets/{config-B4VJJibu.js → config-2Nts63h2.js} +1 -1
  34. package/dist/assets/{config-BxZgoFvi.js → config-BdH5RvM0.js} +1 -1
  35. package/dist/assets/{config-DpGPGznL.js → config-D7CP5wR5.js} +1 -1
  36. package/dist/assets/{copy-BhJktFeI.js → copy-B--GzTEX.js} +1 -1
  37. package/dist/assets/{copy-icon-DFhUGBjO.js → copy-icon-DQuttmjH.js} +1 -1
  38. package/dist/assets/{createReducer-BPvK7C_R.js → createReducer-CI8rlZE9.js} +1 -1
  39. package/dist/assets/{cynefinDiagram-5FMLGOSQ-BARZRQBb.js → cynefinDiagram-5FMLGOSQ-B7iZDq2a.js} +1 -1
  40. package/dist/assets/{dagre-DVyyr7Cb.js → dagre-DkKsNZNe.js} +1 -1
  41. package/dist/assets/{dagre-GXQ25YYZ-79s0lOP0.js → dagre-GXQ25YYZ-CLT8-BB_.js} +1 -1
  42. package/dist/assets/{data-grid-overlay-editor-DPfnIudL.js → data-grid-overlay-editor-Bt4aaHfB.js} +1 -1
  43. package/dist/assets/{dependency-graph-panel-BN4CLayU.js → dependency-graph-panel-DPFdz5rd.js} +1 -1
  44. package/dist/assets/{diagram-S7CK7UJ4-BCviGhvd.js → diagram-S7CK7UJ4-SuzEYncV.js} +1 -1
  45. package/dist/assets/{diagram-UQ7AKVKN-Ds-BT46T.js → diagram-UQ7AKVKN-cuBTTzq8.js} +1 -1
  46. package/dist/assets/{diagram-VX7I27RA-BtL2eh0t.js → diagram-VX7I27RA-h6SkZKIl.js} +1 -1
  47. package/dist/assets/{diagram-Z3DM3KII-C6SmbLBS.js → diagram-Z3DM3KII-C-Vebae0.js} +1 -1
  48. package/dist/assets/{dist-CyYN7zJg.js → dist-DFI04FfI.js} +1 -1
  49. package/dist/assets/{documentation-panel-CzRbXV5u.js → documentation-panel-YTTKKIe_.js} +1 -1
  50. package/dist/assets/{dropdown-menu-VuyoeQOD.js → dropdown-menu-Crx6knLg.js} +1 -1
  51. package/dist/assets/{ebnfDiagram-PWID7BFC-D_JU4cMQ.js → ebnfDiagram-PWID7BFC-Bm_qbW1t.js} +1 -1
  52. package/dist/assets/{edit-page-jGNYBqQo.js → edit-page-CWbb1Sdk.js} +45 -45
  53. package/dist/assets/{erDiagram-RLTQ6QDP-Z9MJ6X-r.js → erDiagram-RLTQ6QDP-Dwwt91bq.js} +1 -1
  54. package/dist/assets/{error-entries-CDSsy76p.js → error-entries-DvgFKvod.js} +1 -1
  55. package/dist/assets/{error-panel-ABvjFG3p.js → error-panel-D1XBOiHc.js} +1 -1
  56. package/dist/assets/{feedback-button-CZhLiwkO.js → feedback-button-CXjDdARJ.js} +1 -1
  57. package/dist/assets/{file-explorer-panel-DTiEpmVE.js → file-explorer-panel-BKJMBA3O.js} +1 -1
  58. package/dist/assets/{file-name-input-C1C8xrjA.js → file-name-input-Bd3Q-WUK.js} +1 -1
  59. package/dist/assets/{filename-fxG7K_MU.js → filename-rhW-R6K2.js} +1 -1
  60. package/dist/assets/{flowDiagram-HODETNUW-ncLciK2K.js → flowDiagram-HODETNUW-CqLwY5NX.js} +1 -1
  61. package/dist/assets/{fullscreen-BmzAqMKq.js → fullscreen-3wS6IaIC.js} +1 -1
  62. package/dist/assets/{gallery-page-Moi4ZdJ0.js → gallery-page-CrkxyZTA.js} +1 -1
  63. package/dist/assets/{glide-data-editor-CbsCWu-K.js → glide-data-editor-B5WqL5gm.js} +4 -4
  64. package/dist/assets/{graphlib-3maKwbRO.js → graphlib-6Yb0YQgW.js} +1 -1
  65. package/dist/assets/has-B4C7ywdy.js +1 -0
  66. package/dist/assets/{home-page-UAgVOkCZ.js → home-page-BNKKAvoF.js} +1 -1
  67. package/dist/assets/{index-DafnX1Xx.js → index-C9FPQNuH.js} +8 -8
  68. package/dist/assets/{infoDiagram-27XIBGKW-Dk8U81nJ.js → infoDiagram-27XIBGKW-dRXMROiS.js} +1 -1
  69. package/dist/assets/{input-BDakodZ2.js → input-D4bMj5Nb.js} +1 -1
  70. package/dist/assets/{ishikawaDiagram-5VMMS53U-CzHQlMUQ.js → ishikawaDiagram-5VMMS53U-CvBCDJJe.js} +1 -1
  71. package/dist/assets/{kanban-definition-UXKFOSKX-CVyVYiI6.js → kanban-definition-UXKFOSKX-fIKpyolW.js} +1 -1
  72. package/dist/assets/{logs-panel-Bz5ysVX4.js → logs-panel-adAvx6o3.js} +1 -1
  73. package/dist/assets/{menu-items-BBdeXCvU.js → menu-items-D-vbZmqL.js} +1 -1
  74. package/dist/assets/{mermaid-D_jPUVqz.js → mermaid-DpSqZGNM.js} +4 -4
  75. package/dist/assets/{mindmap-definition-YA3MSWOX-D5t6Aq5Z.js → mindmap-definition-YA3MSWOX-B95LGKLt.js} +1 -1
  76. package/dist/assets/{number-overlay-editor-FAD8sjFU.js → number-overlay-editor-Bs5xzKX-.js} +1 -1
  77. package/dist/assets/{numbers-DsgsDslU.js → numbers-DyJNv6Vk.js} +1 -1
  78. package/dist/assets/{outline-panel-85oiQ0XX.js → outline-panel-B9u6Wgol.js} +1 -1
  79. package/dist/assets/{packages-panel-ClKIaP5S.js → packages-panel-D-g71io7.js} +1 -1
  80. package/dist/assets/{panels-B48zp96D.js → panels-C5xQNcZv.js} +3 -3
  81. package/dist/assets/{pegDiagram-XKGWAZYB-BTkTm4ej.js → pegDiagram-XKGWAZYB-CMaSc5Ln.js} +1 -1
  82. package/dist/assets/{pieDiagram-E7YTZNPT-Bq6X88M1.js → pieDiagram-E7YTZNPT-CZY0tLgh.js} +1 -1
  83. package/dist/assets/{process-output-Cg2ta3vY.js → process-output-CpF79y4D.js} +1 -1
  84. package/dist/assets/{railroadDiagram-O6MQD6OU-fv8HXW_g.js → railroadDiagram-O6MQD6OU-BSMijs6s.js} +1 -1
  85. package/dist/assets/{requirementDiagram-BXWQKSXE-DDOOMUfC.js → requirementDiagram-BXWQKSXE-DjzkUUZk.js} +1 -1
  86. package/dist/assets/{reveal-component-CR43gpTZ.js → reveal-component-ZyDayfDQ.js} +1 -1
  87. package/dist/assets/{run-page-NR_KFWk8.js → run-page-Ba_64fL5.js} +1 -1
  88. package/dist/assets/{scratchpad-panel-DHc-oVb1.js → scratchpad-panel-7bxRkpPb.js} +1 -1
  89. package/dist/assets/{scroll-to-bottom-button-BSCkDXet.js → scroll-to-bottom-button-BLNi0TGS.js} +1 -1
  90. package/dist/assets/{secrets-panel-C-65ZjnM.js → secrets-panel-DmKgIV5x.js} +1 -1
  91. package/dist/assets/{select-BplWpfFo.js → select-CWHHoM53.js} +1 -1
  92. package/dist/assets/{sequenceDiagram-WJ2MYXX4-DdpBhQaS.js → sequenceDiagram-WJ2MYXX4-Dp7JdkJ6.js} +1 -1
  93. package/dist/assets/{session-panel-DiwQqIbt.js → session-panel-D9qVJogC.js} +1 -1
  94. package/dist/assets/{session-DXKMv4Bs.js → session-ztCnZJHI.js} +1 -1
  95. package/dist/assets/{snippets-panel-BLmXrneD.js → snippets-panel-QdZvjlBm.js} +1 -1
  96. package/dist/assets/{state-Ozzur_f2.js → state-0NZOvqJz.js} +1 -1
  97. package/dist/assets/{stateDiagram-D77RDMKH-D3p19fac.js → stateDiagram-D77RDMKH-CR5QDK_-.js} +1 -1
  98. package/dist/assets/{stateDiagram-v2-MP3YSRHH-CplsDaDl.js → stateDiagram-v2-MP3YSRHH-BmMYN-vz.js} +1 -1
  99. package/dist/assets/{strings-BHZPE0Mc.js → strings-LjhpYwmx.js} +1 -1
  100. package/dist/assets/{swimlanes-42K2YHIH-CLxCSvU4.js → swimlanes-42K2YHIH-By_1jYHM.js} +1 -1
  101. package/dist/assets/{swimlanesDiagram-VR7AAH4N-DPoIFx8T.js → swimlanesDiagram-VR7AAH4N-DjMyvNFc.js} +1 -1
  102. package/dist/assets/{swiper-component-rqJChT_P.js → swiper-component-Cbtej5b-.js} +1 -1
  103. package/dist/assets/{terminal-CY4h1UmO.js → terminal-Ri8br8XX.js} +1 -1
  104. package/dist/assets/{timeline-definition-24CTP7MA-Cw2PY--x.js → timeline-definition-24CTP7MA-BjNxu6yh.js} +1 -1
  105. package/dist/assets/{toast-KNfsFUiB.js → toast-DXkezxWI.js} +1 -1
  106. package/dist/assets/{tooltip-PLkI_UfK.js → tooltip-BQ9qHrrN.js} +1 -1
  107. package/dist/assets/{tooltip-DR2Z2N4w.js → tooltip-CamoSiGF.js} +1 -1
  108. package/dist/assets/{tracing-Q8NAdlrA.js → tracing-BxMH9HGb.js} +1 -1
  109. package/dist/assets/{tracing-panel-Ci4kY4dr.js → tracing-panel-EeL3md5x.js} +2 -2
  110. package/dist/assets/{use-restore-focus-B98SEiGc.js → use-restore-focus-D0UtBPD6.js} +1 -1
  111. package/dist/assets/{useCellActionButton-DpYtVfFe.js → useCellActionButton-Di4NkDbT.js} +1 -1
  112. package/dist/assets/{useEventListener-BdZUApHd.js → useEventListener-CUPWI_1X.js} +1 -1
  113. package/dist/assets/{useInterval-ChchS0Rl.js → useInterval-DluQa5sq.js} +1 -1
  114. package/dist/assets/{useTheme-DcnXlv0_.js → useTheme-CnWB5G1X.js} +1 -1
  115. package/dist/assets/{vega-component-BpoSew4K.js → vega-component-DGTXJwmx.js} +1 -1
  116. package/dist/assets/{vega-embed-container-BOJG8mCA.js → vega-embed-container-Din5FYpB.js} +1 -1
  117. package/dist/assets/{vennDiagram-4TSXK5OY-D8bN6lVl.js → vennDiagram-4TSXK5OY-BR66J2hx.js} +1 -1
  118. package/dist/assets/{wardleyDiagram-VM6X3IG4-CbZdo0SA.js → wardleyDiagram-VM6X3IG4-Kg0-lASD.js} +1 -1
  119. package/dist/assets/{write-secret-modal-BiG2tahJ.js → write-secret-modal-wFgr-1dA.js} +1 -1
  120. package/dist/assets/{xychartDiagram-S5SC5T6Z-DUUQ9tau.js → xychartDiagram-S5SC5T6Z-BSXFWSRW.js} +1 -1
  121. package/dist/assets/{youtube-A9VT7NHb.js → youtube-Ck_NlGi9.js} +1 -1
  122. package/dist/index.html +31 -31
  123. package/package.json +7 -7
  124. package/src/components/data-table/range-focus/__tests__/atoms.test.ts +11 -11
  125. package/src/components/editor/actions/__tests__/useSetOutputsExpanded.test.ts +42 -0
  126. package/src/components/editor/actions/useNotebookActions.tsx +20 -0
  127. package/src/components/editor/actions/useSetOutputsExpanded.ts +67 -0
  128. package/src/components/tracing/tracing.test.tsx +30 -30
  129. package/src/core/edit-app.tsx +16 -0
  130. package/src/core/hotkeys/hotkeys.ts +12 -0
  131. package/src/plugins/impl/FileBrowserPlugin.tsx +45 -15
  132. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +85 -2
  133. package/dist/assets/has-cJsIKK9t.js +0 -1
@@ -0,0 +1,67 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useCallback } from "react";
4
+ import {
5
+ getNotebook,
6
+ type NotebookState,
7
+ useCellActions,
8
+ } from "@/core/cells/cells";
9
+ import type { CellId } from "@/core/cells/ids";
10
+ import { isOutputEmpty } from "@/core/cells/outputs";
11
+ import { useRequestClient } from "@/core/network/requests";
12
+ import type { CellConfig } from "@/core/network/types";
13
+ import { Objects } from "@/utils/objects";
14
+
15
+ /**
16
+ * The `expand_output` config changes needed to expand or clamp every output.
17
+ *
18
+ * Both actions skip cells without output, so their source isn't annotated
19
+ * with a config that has no effect.
20
+ */
21
+ export function outputExpansionConfigs(
22
+ notebook: NotebookState,
23
+ expanded: boolean,
24
+ ): Record<CellId, Partial<CellConfig>> {
25
+ const configs: Record<CellId, Partial<CellConfig>> = {};
26
+ for (const cellId of notebook.cellIds.inOrderIds) {
27
+ const cell = notebook.cellData[cellId];
28
+ if (
29
+ cell === undefined ||
30
+ (cell.config.expand_output ?? false) === expanded
31
+ ) {
32
+ continue;
33
+ }
34
+ if (isOutputEmpty(notebook.cellRuntime[cellId]?.output)) {
35
+ continue;
36
+ }
37
+ configs[cellId] = { expand_output: expanded };
38
+ }
39
+ return configs;
40
+ }
41
+
42
+ /**
43
+ * Hook returning a callback that shows every cell's output in full, or clamps
44
+ * every output to a fixed height, by setting each cell's `expand_output` config.
45
+ */
46
+ export const useSetOutputsExpanded = () => {
47
+ const { updateCellConfig } = useCellActions();
48
+ const { saveCellConfig } = useRequestClient();
49
+
50
+ return useCallback(
51
+ async (expanded: boolean) => {
52
+ const newConfigs = outputExpansionConfigs(getNotebook(), expanded);
53
+
54
+ const entries = Objects.entries(newConfigs);
55
+ if (entries.length === 0) {
56
+ return;
57
+ }
58
+
59
+ await saveCellConfig({ configs: newConfigs });
60
+
61
+ for (const [cellId, config] of entries) {
62
+ updateCellConfig({ cellId, config });
63
+ }
64
+ },
65
+ [updateCellConfig, saveCellConfig],
66
+ );
67
+ };
@@ -5,36 +5,36 @@ import { formatChartTime } from "./utils";
5
5
  describe("formatChartTime", () => {
6
6
  beforeAll(() => {
7
7
  // Mock Date to always use UTC
8
- vi.spyOn(global.Date.prototype, "getFullYear").mockImplementation(
9
- function (this: Date) {
10
- return this.getUTCFullYear();
11
- },
12
- );
13
- vi.spyOn(global.Date.prototype, "getMonth").mockImplementation(
14
- function (this: Date) {
15
- return this.getUTCMonth();
16
- },
17
- );
18
- vi.spyOn(global.Date.prototype, "getDate").mockImplementation(
19
- function (this: Date) {
20
- return this.getUTCDate();
21
- },
22
- );
23
- vi.spyOn(global.Date.prototype, "getHours").mockImplementation(
24
- function (this: Date) {
25
- return this.getUTCHours();
26
- },
27
- );
28
- vi.spyOn(global.Date.prototype, "getMinutes").mockImplementation(
29
- function (this: Date) {
30
- return this.getUTCMinutes();
31
- },
32
- );
33
- vi.spyOn(global.Date.prototype, "getSeconds").mockImplementation(
34
- function (this: Date) {
35
- return this.getUTCSeconds();
36
- },
37
- );
8
+ vi.spyOn(global.Date.prototype, "getFullYear").mockImplementation(function (
9
+ this: Date,
10
+ ) {
11
+ return this.getUTCFullYear();
12
+ });
13
+ vi.spyOn(global.Date.prototype, "getMonth").mockImplementation(function (
14
+ this: Date,
15
+ ) {
16
+ return this.getUTCMonth();
17
+ });
18
+ vi.spyOn(global.Date.prototype, "getDate").mockImplementation(function (
19
+ this: Date,
20
+ ) {
21
+ return this.getUTCDate();
22
+ });
23
+ vi.spyOn(global.Date.prototype, "getHours").mockImplementation(function (
24
+ this: Date,
25
+ ) {
26
+ return this.getUTCHours();
27
+ });
28
+ vi.spyOn(global.Date.prototype, "getMinutes").mockImplementation(function (
29
+ this: Date,
30
+ ) {
31
+ return this.getUTCMinutes();
32
+ });
33
+ vi.spyOn(global.Date.prototype, "getSeconds").mockImplementation(function (
34
+ this: Date,
35
+ ) {
36
+ return this.getUTCSeconds();
37
+ });
38
38
  vi.spyOn(global.Date.prototype, "getMilliseconds").mockImplementation(
39
39
  function (this: Date) {
40
40
  return this.getUTCMilliseconds();
@@ -23,6 +23,7 @@ import {
23
23
  useRunStaleCells,
24
24
  } from "../components/editor/cell/useRunCells";
25
25
  import { useSetCodeVisibility } from "../components/editor/actions/useSetCodeVisibility";
26
+ import { useSetOutputsExpanded } from "../components/editor/actions/useSetOutputsExpanded";
26
27
  import { CellArray } from "../components/editor/renderers/cell-array";
27
28
  import { CellsRenderer } from "../components/editor/renderers/cells-renderer";
28
29
  import { useHotkey } from "../hooks/useHotkey";
@@ -115,6 +116,7 @@ export const EditApp: React.FC<AppProps> = ({
115
116
  const runAllCells = useRunAllCells();
116
117
  const togglePresenting = useTogglePresenting();
117
118
  const setCodeVisibility = useSetCodeVisibility();
119
+ const setOutputsExpanded = useSetOutputsExpanded();
118
120
 
119
121
  // HOTKEYS
120
122
  useHotkey("global.runStale", () => {
@@ -141,6 +143,20 @@ export const EditApp: React.FC<AppProps> = ({
141
143
  useHotkey("global.hideAllMarkdownCode", () => {
142
144
  setCodeVisibility(true, "markdown");
143
145
  });
146
+ useHotkey(
147
+ "global.expandAllOutputs",
148
+ () => {
149
+ setOutputsExpanded(true);
150
+ },
151
+ { disabled: !isEditing },
152
+ );
153
+ useHotkey(
154
+ "global.clampAllOutputs",
155
+ () => {
156
+ setOutputsExpanded(false);
157
+ },
158
+ { disabled: !isEditing },
159
+ );
144
160
  useHotkey("global.collapseAllSections", () => {
145
161
  collapseAllCells();
146
162
  });
@@ -389,6 +389,18 @@ const DEFAULT_HOT_KEY = {
389
389
  group: "Editing",
390
390
  key: NOT_SET,
391
391
  },
392
+ "global.expandAllOutputs": {
393
+ name: "Expand all outputs",
394
+ group: "Editing",
395
+ key: NOT_SET,
396
+ additionalKeywords: ["show full output", "unclamp"],
397
+ },
398
+ "global.clampAllOutputs": {
399
+ name: "Clamp all outputs",
400
+ group: "Editing",
401
+ key: NOT_SET,
402
+ additionalKeywords: ["collapse outputs", "shrink outputs"],
403
+ },
392
404
  "global.collapseAllSections": {
393
405
  name: "Collapse all sections",
394
406
  group: "Editing",
@@ -1,6 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { type LucideIcon, CornerLeftUp } from "lucide-react";
3
+ import { type LucideIcon, ChevronRightIcon, CornerLeftUp } from "lucide-react";
4
4
  import { type JSX, useEffect, useLayoutEffect, useRef, useState } from "react";
5
5
  import { z } from "zod";
6
6
  import {
@@ -12,7 +12,6 @@ import { Spinner } from "@/components/icons/spinner";
12
12
  import { Button } from "@/components/ui/button";
13
13
  import { Checkbox } from "@/components/ui/checkbox";
14
14
  import { Label } from "@/components/ui/label";
15
- import { NativeSelect } from "@/components/ui/native-select";
16
15
  import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
17
16
  import { toast } from "@/components/ui/use-toast";
18
17
  import { RANDOM_ID_ATTR } from "@/core/dom/ui-element-constants";
@@ -472,12 +471,19 @@ export const FileBrowser = ({
472
471
  //
473
472
  // Assumes that path contains at least one delimiter, which is true
474
473
  // only if this is an absolute path.
475
- const { parentDirectories } = getProtocolAndParentDirectories({
474
+ const { protocol, parentDirectories } = getProtocolAndParentDirectories({
476
475
  path,
477
476
  delimiter,
478
477
  initialPath,
479
478
  restrictNavigation,
480
479
  });
480
+ const breadcrumbs = parentDirectories.toReversed().map((dir) => ({
481
+ path: dir,
482
+ name:
483
+ dir === protocol
484
+ ? protocol
485
+ : (dir.split(delimiter).findLast(Boolean) ?? dir),
486
+ }));
481
487
 
482
488
  const selectionKindLabel =
483
489
  selectionMode === "all"
@@ -515,18 +521,42 @@ export const FileBrowser = ({
515
521
  <div>
516
522
  {error && <Banner kind="danger">{error.message}</Banner>}
517
523
  {renderHeader()}
518
- <NativeSelect
519
- className="mt-2 w-full"
520
- placeholder={path}
521
- value={path}
522
- onChange={(e) => setNewPath(e.target.value)}
523
- >
524
- {parentDirectories.map((dir) => (
525
- <option value={dir} key={dir}>
526
- {dir}
527
- </option>
528
- ))}
529
- </NativeSelect>
524
+ <nav aria-label="Current directory" className="mt-2">
525
+ <ol className="flex flex-wrap items-center gap-0.5 text-sm">
526
+ {breadcrumbs.map((crumb, index) => {
527
+ const isCurrent = index === breadcrumbs.length - 1;
528
+ return (
529
+ <li key={crumb.path} className="flex items-center gap-0.5">
530
+ {index > 0 && (
531
+ <ChevronRightIcon
532
+ size={14}
533
+ className="text-muted-foreground shrink-0"
534
+ aria-hidden={true}
535
+ />
536
+ )}
537
+ <button
538
+ type="button"
539
+ title={crumb.path}
540
+ aria-current={isCurrent ? "page" : undefined}
541
+ onClick={() => {
542
+ if (!isCurrent) {
543
+ setNewPath(crumb.path);
544
+ }
545
+ }}
546
+ className={cn(
547
+ "rounded px-1 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring",
548
+ isCurrent
549
+ ? "font-medium cursor-default"
550
+ : "text-muted-foreground hover:bg-accent hover:text-foreground",
551
+ )}
552
+ >
553
+ {crumb.name}
554
+ </button>
555
+ </li>
556
+ );
557
+ })}
558
+ </ol>
559
+ </nav>
530
560
 
531
561
  {data && typeof data.total_count === "number" && (
532
562
  <div className="text-xs text-muted-foreground mt-1 px-1">
@@ -1,6 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import {
4
+ fireEvent,
5
+ render,
6
+ screen,
7
+ waitFor,
8
+ within,
9
+ } from "@testing-library/react";
4
10
  import { beforeAll, describe, expect, it, vi } from "vitest";
5
11
  import { SetupMocks } from "@/__mocks__/common";
6
12
  import { initialModeAtom } from "@/core/mode";
@@ -187,7 +193,7 @@ describe("FileBrowserPlugin keyboard accessibility", () => {
187
193
  const docsRow = docs.closest('[role="row"]')!;
188
194
  fireEvent.keyDown(docsRow, { key: "ArrowDown" }); // move active off the parent
189
195
  fireEvent.click(docs); // navigate into "docs"
190
- await screen.findByText("docs"); // listing reloads (mock returns same files)
196
+ await within(screen.getByRole("grid")).findByText("docs"); // listing reloads (mock returns same files)
191
197
  const rows = screen.getAllByRole("row");
192
198
  expect(rows[0]).toHaveAttribute("tabindex", "0");
193
199
  // the focused row must match the only tabbable row
@@ -361,3 +367,80 @@ describe("FileBrowserPlugin keyboard accessibility", () => {
361
367
  function rowFor(name: string): HTMLElement {
362
368
  return screen.getByText(name).closest('[role="row"]') as HTMLElement;
363
369
  }
370
+
371
+ describe("FileBrowserPlugin breadcrumbs", () => {
372
+ function breadcrumbs() {
373
+ return within(
374
+ screen.getByRole("navigation", { name: "Current directory" }),
375
+ );
376
+ }
377
+
378
+ it("renders a crumb per ancestor with the current directory last", async () => {
379
+ renderBrowser();
380
+ await screen.findByText("docs");
381
+ const items = breadcrumbs().getAllByRole("listitem");
382
+ expect(items.map((li) => li.textContent)).toEqual(["/", "home", "user"]);
383
+ expect(breadcrumbs().getByRole("button", { name: "user" })).toHaveAttribute(
384
+ "aria-current",
385
+ "page",
386
+ );
387
+ expect(
388
+ breadcrumbs().getByRole("button", { name: "home" }),
389
+ ).not.toHaveAttribute("aria-current");
390
+ });
391
+
392
+ it("keeps focus on a crumb after navigating to it", async () => {
393
+ const { listDirectory } = renderBrowser();
394
+ await screen.findByText("docs");
395
+ const home = breadcrumbs().getByRole("button", { name: "home" });
396
+ home.focus();
397
+ fireEvent.click(home);
398
+ await waitFor(() =>
399
+ expect(listDirectory).toHaveBeenLastCalledWith({ path: "/home" }),
400
+ );
401
+ const current = breadcrumbs().getByRole("button", { name: "home" });
402
+ expect(current).toHaveAttribute("aria-current", "page");
403
+ expect(current).toBe(home);
404
+ expect(current).toHaveFocus();
405
+ });
406
+
407
+ it("does not reload when the current crumb is clicked", async () => {
408
+ const { listDirectory } = renderBrowser();
409
+ await screen.findByText("docs");
410
+ fireEvent.click(breadcrumbs().getByRole("button", { name: "user" }));
411
+ expect(listDirectory).toHaveBeenCalledTimes(1);
412
+ });
413
+
414
+ it("navigates to an ancestor when a crumb is clicked", async () => {
415
+ const { listDirectory } = renderBrowser();
416
+ await screen.findByText("docs");
417
+ fireEvent.click(breadcrumbs().getByRole("button", { name: "home" }));
418
+ await waitFor(() =>
419
+ expect(listDirectory).toHaveBeenLastCalledWith({ path: "/home" }),
420
+ );
421
+ fireEvent.click(breadcrumbs().getByRole("button", { name: "/" }));
422
+ await waitFor(() =>
423
+ expect(listDirectory).toHaveBeenLastCalledWith({ path: "/" }),
424
+ );
425
+ });
426
+
427
+ it("hides ancestors outside the initial path when navigation is restricted", async () => {
428
+ render(
429
+ <FileBrowser
430
+ initialPath="/home/user"
431
+ filetypes={[]}
432
+ selectionMode="all"
433
+ multiple={true}
434
+ label={null}
435
+ restrictNavigation={true}
436
+ value={[]}
437
+ setValue={vi.fn()}
438
+ host={document.createElement("div")}
439
+ list_directory={mockListDirectory(FILES)}
440
+ />,
441
+ );
442
+ await screen.findByText("docs");
443
+ const items = breadcrumbs().getAllByRole("listitem");
444
+ expect(items.map((li) => li.textContent)).toEqual(["user"]);
445
+ });
446
+ });
@@ -1 +0,0 @@
1
- import{Wa as e}from"./index-DafnX1Xx.js";var t=Object.prototype.hasOwnProperty;function n(e,n){return e!=null&&t.call(e,n)}function r(t,r){return t!=null&&e(t,r,n)}export{r as t};