@marimo-team/frontend 0.24.1-dev84 → 0.24.1-dev86

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 (105) hide show
  1. package/dist/assets/{CellStatus-GcHBZKLm.js → CellStatus-D40JXMpi.js} +1 -1
  2. package/dist/assets/{ConnectedDataExplorerComponent-BU1NZpRe.js → ConnectedDataExplorerComponent-Dkqk66Mv.js} +1 -1
  3. package/dist/assets/{JsonOutput-CUJlEdsN.js → JsonOutput-DeeAtdky.js} +1 -1
  4. package/dist/assets/{MarimoErrorOutput-DCAezqcJ.js → MarimoErrorOutput-Dh-877oV.js} +1 -1
  5. package/dist/assets/{RenderHTML-D9-75ltP.js → RenderHTML-C7JgAfHC.js} +1 -1
  6. package/dist/assets/{RunButton-plSsbubG.js → RunButton-C9d_w3qn.js} +1 -1
  7. package/dist/assets/{add-cell-with-ai-07LjLydm.js → add-cell-with-ai-zrX9HOPy.js} +1 -1
  8. package/dist/assets/{add-connection-dialog-By0i0Fq5.js → add-connection-dialog-OQxtKyCV.js} +1 -1
  9. package/dist/assets/{agent-panel-D01f7IIt.js → agent-panel-DF7_IEEH.js} +1 -1
  10. package/dist/assets/{ai-model-dropdown-DPxSumqI.js → ai-model-dropdown-DIItpu01.js} +1 -1
  11. package/dist/assets/{app-config-button-DV7ZvBMx.js → app-config-button-CeDIYZUc.js} +1 -1
  12. package/dist/assets/{blob-yxfhyyvD.js → blob-iewSQmkZ.js} +1 -1
  13. package/dist/assets/{cell-editor-Bl5DkKIR.js → cell-editor-BkHTcrF-.js} +1 -1
  14. package/dist/assets/{cell-link-hHrWcozB.js → cell-link-ZLZTXwR8.js} +1 -1
  15. package/dist/assets/{cells-yuYDzbAy.js → cells-DyE9dSiM.js} +1 -1
  16. package/dist/assets/{chat-display-BFCcdgia.js → chat-display-B9XOYJaA.js} +1 -1
  17. package/dist/assets/{chat-panel-CS9bLpq9.js → chat-panel-Drx8ToF5.js} +1 -1
  18. package/dist/assets/{chat-ui-lssAqY7E.js → chat-ui-CJ1VFz1m.js} +1 -1
  19. package/dist/assets/{column-preview-BEmcYaKl.js → column-preview-Ls8UnJOt.js} +1 -1
  20. package/dist/assets/{command-palette-DInNqneC.js → command-palette-DWyHSatV.js} +1 -1
  21. package/dist/assets/{common-Bs2kaOs2.js → common-03vbDqrN.js} +1 -1
  22. package/dist/assets/{components-ib4asSo7.js → components-CBlGIIAX.js} +1 -1
  23. package/dist/assets/{components-ByyAzQxP.js → components-J_vBLfsO.js} +1 -1
  24. package/dist/assets/{config-DaNyNpZL.js → config-D4frVGQ4.js} +1 -1
  25. package/dist/assets/{config-Cl3eMWxb.js → config-ycxJQP2b.js} +1 -1
  26. package/dist/assets/{datasource-DeZYq5UY.js → datasource-B4VfwXg_.js} +1 -1
  27. package/dist/assets/{dependency-graph-panel-g10osAFc.js → dependency-graph-panel-B24NhlCS.js} +1 -1
  28. package/dist/assets/{documentation-panel-CZ1mKkDh.js → documentation-panel-C4y_ufGj.js} +1 -1
  29. package/dist/assets/{download-D1crMjD8.js → download-Kg8_rqQH.js} +1 -1
  30. package/dist/assets/{edit-page-ZyGzkf7q.js → edit-page-DL_yo5rn.js} +4 -4
  31. package/dist/assets/{error-entries-B6XTLRyE.js → error-entries-Cy1y8FaI.js} +1 -1
  32. package/dist/assets/{error-panel-lexB3L6f.js → error-panel-D_pwhWfh.js} +1 -1
  33. package/dist/assets/file-explorer-panel-QoJu0RbE.js +1 -0
  34. package/dist/assets/{file-icons-Dx3v6lMu.js → file-icons-BN7fZ7-c.js} +1 -1
  35. package/dist/assets/file-name-input-qU-dl8Og.js +3 -0
  36. package/dist/assets/{floating-outline-3rQEbxxk.js → floating-outline-Coq72JrA.js} +1 -1
  37. package/dist/assets/{glide-data-editor-DZ2Minh8.js → glide-data-editor-DcY3nXI8.js} +1 -1
  38. package/dist/assets/{home-page-Ck-_oVJT.js → home-page-C6NwS9L9.js} +1 -1
  39. package/dist/assets/{index-g9ny2vs0.js → index-DMvKFHTV.js} +5 -5
  40. package/dist/assets/{kiosk-mode-CjT3sqy8.js → kiosk-mode-CzGt3mc2.js} +1 -1
  41. package/dist/assets/{layout-D0Q03BhB.js → layout-ByAlvlct.js} +3 -3
  42. package/dist/assets/{logs-panel-CSbUhhRo.js → logs-panel-zYelRd3A.js} +1 -1
  43. package/dist/assets/{markdown-renderer-BgUTGtM0.js → markdown-renderer-BqMslrdb.js} +1 -1
  44. package/dist/assets/{mermaid-Nzih1WTG.js → mermaid-CeseGGnS.js} +1 -1
  45. package/dist/assets/{name-cell-input-pEE4FGAL.js → name-cell-input-jrtswt5e.js} +1 -1
  46. package/dist/assets/{outline-panel-BgnjEQWg.js → outline-panel-B0P8XNnQ.js} +1 -1
  47. package/dist/assets/{packages-panel-BBP_xysn.js → packages-panel-C08E4Wjm.js} +1 -1
  48. package/dist/assets/{pair-with-agent-modal-Gg8MZHWW.js → pair-with-agent-modal-BZBengJw.js} +1 -1
  49. package/dist/assets/{panel-accordion-state-vpqEc3Kd.js → panel-accordion-state-Cyy4afrx.js} +1 -1
  50. package/dist/assets/{panels-fKt2XFRB.js → panels-DRtPsJ1u.js} +1 -1
  51. package/dist/assets/{process-output-siRm_lza.js → process-output-BiA5MKpM.js} +1 -1
  52. package/dist/assets/{readonly-code-display-CLWV76FT.js → readonly-code-display-B45Jy4vH.js} +1 -1
  53. package/dist/assets/{readonly-python-code-Bor9b9pV.js → readonly-python-code-BQ5Ti6Hw.js} +1 -1
  54. package/dist/assets/{renderShortcut-CS4ccH3x.js → renderShortcut-DK_OucLT.js} +1 -1
  55. package/dist/assets/{reveal-component-B55bWtCO.js → reveal-component-Bna3jncc.js} +1 -1
  56. package/dist/assets/{run-page-mD_6P_t5.js → run-page-CFC5C_It.js} +1 -1
  57. package/dist/assets/{scratchpad-panel-B_7IHnPD.js → scratchpad-panel-m2LRBFI5.js} +1 -1
  58. package/dist/assets/{session-panel-DB5qEYqt.js → session-panel-BV8Whoy8.js} +1 -1
  59. package/dist/assets/{skeleton-TY77TiqZ.js → skeleton-CWFWp9eU.js} +1 -1
  60. package/dist/assets/{snippets-panel-B8Yi81tz.js → snippets-panel-DBzdOGwQ.js} +1 -1
  61. package/dist/assets/state-DPS9Q717.js +3 -0
  62. package/dist/assets/{state-O8w98G11.js → state-rP7TRCla.js} +1 -1
  63. package/dist/assets/{switch-Gl6cozkm.js → switch-CtH5Ct94.js} +1 -1
  64. package/dist/assets/{terminal-DOqqA9Ha.js → terminal-C73iz1fU.js} +1 -1
  65. package/dist/assets/{tracing-DuLkr7ts.js → tracing-BP1J14GT.js} +1 -1
  66. package/dist/assets/{tracing-panel-CeLKVG8M.js → tracing-panel-CQasWudc.js} +2 -2
  67. package/dist/assets/{tree-actions-Bh-Nnaub.js → tree-actions-CrB9REp3.js} +1 -1
  68. package/dist/assets/{useAddCell-_JRYhAFd.js → useAddCell-DHYPnl7W.js} +1 -1
  69. package/dist/assets/{useBoolean-Cu7Eado1.js → useBoolean-ZU5StAbr.js} +1 -1
  70. package/dist/assets/{useCellActionButton-Bi8Lnv3F.js → useCellActionButton-C7bEu9tJ.js} +1 -1
  71. package/dist/assets/{useDeleteCell-CBQiavzs.js → useDeleteCell-CIzxZKUw.js} +1 -1
  72. package/dist/assets/{useDependencyPanelTab-BOn3UJ07.js → useDependencyPanelTab-CLmRGbOD.js} +1 -1
  73. package/dist/assets/{useNotebookActions-C1YIorGp.js → useNotebookActions-Q97EgZxR.js} +1 -1
  74. package/dist/assets/{useRunCells-BFQfLlUl.js → useRunCells-Csh5VYKu.js} +1 -1
  75. package/dist/assets/{useSplitCell-Ig9P6lAn.js → useSplitCell-BfT7UcKM.js} +1 -1
  76. package/dist/assets/{useTheme-D3zvxdF3.js → useTheme-CF-zG9YH.js} +1 -1
  77. package/dist/assets/{utils-CUZQNAWJ.js → utils-CWSZQEjr.js} +1 -1
  78. package/dist/assets/{vega-component-B0MZ_r-y.js → vega-component-Bel2PDO-.js} +1 -1
  79. package/dist/index.html +26 -26
  80. package/package.json +1 -1
  81. package/src/__mocks__/requests.ts +1 -0
  82. package/src/components/editor/chrome/panels/file-explorer-panel.tsx +3 -2
  83. package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +31 -18
  84. package/src/components/editor/file-tree/__tests__/file-explorer.test.ts +18 -12
  85. package/src/components/editor/file-tree/__tests__/requesting-tree.test.ts +364 -387
  86. package/src/components/editor/file-tree/file-explorer.tsx +137 -97
  87. package/src/components/editor/file-tree/file-name-input.tsx +5 -1
  88. package/src/components/editor/file-tree/file-viewer.tsx +2 -2
  89. package/src/components/editor/file-tree/requesting-tree.tsx +291 -171
  90. package/src/components/editor/file-tree/state.tsx +1 -0
  91. package/src/core/config/__tests__/config-schema.test.ts +15 -0
  92. package/src/core/config/config-schema.ts +15 -0
  93. package/src/core/islands/bridge.ts +1 -0
  94. package/src/core/network/__tests__/requests-network.test.ts +7 -0
  95. package/src/core/network/__tests__/requests-toasting.test.tsx +10 -0
  96. package/src/core/network/requests-lazy.ts +1 -0
  97. package/src/core/network/requests-network.ts +4 -0
  98. package/src/core/network/requests-static.ts +1 -0
  99. package/src/core/network/requests-toasting.tsx +1 -0
  100. package/src/core/network/types.ts +3 -0
  101. package/src/core/wasm/bridge.ts +9 -0
  102. package/src/core/wasm/worker/types.ts +2 -0
  103. package/dist/assets/file-explorer-panel-DHRH2J-3.js +0 -1
  104. package/dist/assets/file-name-input-DzL8D_7x.js +0 -3
  105. package/dist/assets/state-D1N9_41P.js +0 -3
@@ -11,6 +11,7 @@ import {
11
11
  ExternalLinkIcon,
12
12
  FilePlus2Icon,
13
13
  FolderPlusIcon,
14
+ FolderRootIcon,
14
15
  ListTreeIcon,
15
16
  PlaySquareIcon,
16
17
  UploadIcon,
@@ -58,7 +59,6 @@ import { useCellActions } from "@/core/cells/cells";
58
59
  import { useLastFocusedCellId } from "@/core/cells/focus";
59
60
  import { disableFileDownloadsAtom } from "@/core/config/config";
60
61
  import { useRequestClient } from "@/core/network/requests";
61
- import type { FileInfo } from "@/core/network/types";
62
62
  import { isWasm } from "@/core/wasm/utils";
63
63
  import { useAsyncData } from "@/hooks/useAsyncData";
64
64
  import { ErrorBanner } from "@/plugins/impl/common/error-banner";
@@ -71,7 +71,7 @@ import { jotaiJsonStorage } from "@/utils/storage/jotai";
71
71
  import { useTreeDndManager } from "./dnd-wrapper";
72
72
  import { downloadFile } from "./download";
73
73
  import { FileViewer } from "./file-viewer";
74
- import type { RequestingTree } from "./requesting-tree";
74
+ import type { FileTreeNode, RequestingTree } from "./requesting-tree";
75
75
  import { openStateAtom, treeAtom } from "./state";
76
76
  import { PYTHON_CODE_FOR_FILE_TYPE } from "./types";
77
77
  import {
@@ -101,14 +101,15 @@ export const FileExplorer: React.FC<{
101
101
  height: number;
102
102
  externalDropDestinationPath?: FilePath | null;
103
103
  }> = ({ height, externalDropDestinationPath = null }) => {
104
- const treeRef = useRef<TreeApi<FileInfo>>(null);
104
+ const treeRef = useRef<TreeApi<FileTreeNode>>(null);
105
105
  const dndManager = useTreeDndManager();
106
106
  const [tree] = useAtom(treeAtom);
107
- const [data, setData] = useState<FileInfo[]>([]);
108
- const [openFile, setOpenFile] = useState<FileInfo | null>(null);
109
- const [selectedFolderPath, setSelectedFolderPath] = useState<FilePath | null>(
110
- null,
111
- );
107
+ const [data, setData] = useState<FileTreeNode[]>([]);
108
+ const [openFile, setOpenFile] = useState<FileTreeNode | null>(null);
109
+ const [selectedFolder, setSelectedFolder] = useState<{
110
+ id: string;
111
+ path: FilePath;
112
+ } | null>(null);
112
113
  const [showHiddenFiles, setShowHiddenFiles] =
113
114
  useAtom<boolean>(hiddenFilesState);
114
115
  // Keep external state to remember which folders are open when this
@@ -153,7 +154,7 @@ export const FileExplorer: React.FC<{
153
154
  openPrompt({
154
155
  title: "Folder name",
155
156
  onConfirm: async (name) => {
156
- tree.createFolder(name, null);
157
+ tree.createFolder(name, selectedFolder?.id ?? null);
157
158
  },
158
159
  });
159
160
  });
@@ -162,7 +163,7 @@ export const FileExplorer: React.FC<{
162
163
  openPrompt({
163
164
  title: "File name",
164
165
  onConfirm: async (name) => {
165
- tree.createFile({ name, parentId: null });
166
+ tree.createFile({ name, parentId: selectedFolder?.id ?? null });
166
167
  },
167
168
  });
168
169
  });
@@ -171,7 +172,11 @@ export const FileExplorer: React.FC<{
171
172
  openPrompt({
172
173
  title: "Notebook name",
173
174
  onConfirm: async (name) => {
174
- tree.createFile({ name, parentId: null, type: "notebook" });
175
+ tree.createFile({
176
+ name,
177
+ parentId: selectedFolder?.id ?? null,
178
+ type: "notebook",
179
+ });
175
180
  },
176
181
  });
177
182
  });
@@ -186,13 +191,10 @@ export const FileExplorer: React.FC<{
186
191
  [data, showHiddenFiles],
187
192
  );
188
193
  React.useEffect(() => {
189
- if (
190
- selectedFolderPath &&
191
- !treeContainsPath(visibleData, selectedFolderPath)
192
- ) {
193
- setSelectedFolderPath(null);
194
+ if (selectedFolder && !treeContainsId(visibleData, selectedFolder.id)) {
195
+ setSelectedFolder(null);
194
196
  }
195
- }, [selectedFolderPath, visibleData]);
197
+ }, [selectedFolder, visibleData]);
196
198
  const contextValue = React.useMemo<FileExplorerContextValue>(
197
199
  () => ({
198
200
  tree,
@@ -227,11 +229,17 @@ export const FileExplorer: React.FC<{
227
229
  </div>
228
230
  <Suspense>
229
231
  <FileViewer
230
- onOpenNotebook={(evt) =>
231
- openMarimoNotebook(
232
- evt,
233
- tree.relativeFromRoot(openFile.path as FilePath),
234
- )
232
+ onOpenNotebook={
233
+ tree.isPrimaryNode(openFile)
234
+ ? (evt) => {
235
+ const path = tree.getPrimaryRelativePath(
236
+ openFile.path as FilePath,
237
+ );
238
+ if (path !== null) {
239
+ openMarimoNotebook(evt, path);
240
+ }
241
+ }
242
+ : undefined
235
243
  }
236
244
  file={openFile}
237
245
  />
@@ -254,15 +262,17 @@ export const FileExplorer: React.FC<{
254
262
  onCreateNotebook={handleCreateNotebook}
255
263
  onCreateFolder={handleCreateFolder}
256
264
  onCollapseAll={handleCollapseAll}
257
- uploadDestinationPath={selectedFolderPath ?? tree.getRootPath()}
265
+ uploadDestinationPath={
266
+ selectedFolder?.path ?? tree.getPrimaryRootPath()
267
+ }
258
268
  uploadDestinationLabel={getUploadDestinationLabel(
259
269
  tree,
260
- selectedFolderPath ?? tree.getRootPath(),
270
+ selectedFolder?.path ?? tree.getPrimaryRootPath(),
261
271
  )}
262
272
  onUpload={handleUploadFiles}
263
273
  />
264
274
  <RequestingTreeContext value={contextValue}>
265
- <Tree<FileInfo>
275
+ <Tree<FileTreeNode>
266
276
  width="100%"
267
277
  ref={treeRef}
268
278
  height={height - 33}
@@ -275,7 +285,11 @@ export const FileExplorer: React.FC<{
275
285
  // Hide the drop cursor
276
286
  renderCursor={() => null}
277
287
  // Disable dropping files into files
278
- disableDrop={({ parentNode }) => !parentNode.data.isDirectory}
288
+ disableDrag={(node) => node.isRoot}
289
+ disableDrop={({ parentNode, dragNodes }) =>
290
+ dragNodes.some((node) => node.data.isRoot) ||
291
+ (parentNode ? !parentNode.data.isDirectory : false)
292
+ }
279
293
  onDelete={async ({ ids }) => {
280
294
  for (const id of ids) {
281
295
  await tree.delete(id);
@@ -290,14 +304,17 @@ export const FileExplorer: React.FC<{
290
304
  onSelect={(nodes) => {
291
305
  const first = nodes[0];
292
306
  if (!first) {
293
- setSelectedFolderPath(null);
307
+ setSelectedFolder(null);
294
308
  return;
295
309
  }
296
310
  if (first.data.isDirectory) {
297
- setSelectedFolderPath(first.data.path as FilePath);
311
+ setSelectedFolder({
312
+ id: first.id,
313
+ path: first.data.path as FilePath,
314
+ });
298
315
  return;
299
316
  }
300
- setSelectedFolderPath(null);
317
+ setSelectedFolder(null);
301
318
  setOpenFile(first.data);
302
319
  }}
303
320
  onToggle={async (id) => {
@@ -422,12 +439,12 @@ const Show = ({
422
439
  node,
423
440
  onOpenMarimoFile,
424
441
  }: {
425
- node: NodeApi<FileInfo>;
442
+ node: NodeApi<FileTreeNode>;
426
443
  onOpenMarimoFile: (
427
444
  evt: Pick<Event, "stopPropagation" | "preventDefault">,
428
445
  ) => void;
429
446
  }) => {
430
- return (
447
+ const label = (
431
448
  <span
432
449
  className="flex-1 overflow-hidden text-ellipsis"
433
450
  onClick={(e) => {
@@ -439,7 +456,7 @@ const Show = ({
439
456
  }}
440
457
  >
441
458
  {node.data.name}
442
- {node.data.isMarimoFile && !isWasm() && (
459
+ {node.data.isMarimoFile && node.data.isPrimaryRoot && !isWasm() && (
443
460
  <span
444
461
  data-testid="file-explorer-open-marimo-button"
445
462
  className="shrink-0 ml-2 text-sm hidden group-hover:inline hover:underline"
@@ -450,9 +467,15 @@ const Show = ({
450
467
  )}
451
468
  </span>
452
469
  );
470
+
471
+ return node.data.isRoot ? (
472
+ <Tooltip content={node.data.path}>{label}</Tooltip>
473
+ ) : (
474
+ label
475
+ );
453
476
  };
454
477
 
455
- const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
478
+ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileTreeNode>) => {
456
479
  const { openFile } = useRequestClient();
457
480
  const disableFileDownloads = useAtomValue(disableFileDownloadsAtom);
458
481
 
@@ -482,10 +505,10 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
482
505
  const handleOpenMarimoFile = async (
483
506
  evt: Pick<Event, "stopPropagation" | "preventDefault">,
484
507
  ) => {
485
- const path = tree
486
- ? tree.relativeFromRoot(node.data.path as FilePath)
487
- : node.data.path;
488
- openMarimoNotebook(evt, path);
508
+ const path = tree?.getPrimaryRelativePath(node.data.path as FilePath);
509
+ if (path !== null && path !== undefined) {
510
+ openMarimoNotebook(evt, path);
511
+ }
489
512
  };
490
513
 
491
514
  const handleDeleteFile = async (evt: Event) => {
@@ -555,7 +578,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
555
578
  className={cn(
556
579
  "flex items-center cursor-pointer ml-1 text-muted-foreground whitespace-nowrap group",
557
580
  )}
558
- draggable={true}
581
+ draggable={!node.data.isRoot}
559
582
  onClick={(evt) => {
560
583
  evt.stopPropagation();
561
584
  if (node.data.isDirectory) {
@@ -577,7 +600,12 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
577
600
  "bg-primary/15 hover:bg-primary/15 text-accent-foreground ring-1 ring-inset ring-primary",
578
601
  )}
579
602
  >
580
- {node.data.isMarimoFile ? (
603
+ {node.data.isRoot ? (
604
+ <FolderRootIcon
605
+ className="w-5 h-5 shrink-0 mr-2 text-primary"
606
+ strokeWidth={1.5}
607
+ />
608
+ ) : node.data.isMarimoFile ? (
581
609
  <MarimoIcon className="w-5 h-5 shrink-0 mr-2" strokeWidth={1.5} />
582
610
  ) : (
583
611
  <Icon
@@ -649,14 +677,18 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
649
677
  <DropdownMenuSeparator />
650
678
  </>
651
679
  )}
652
- <RenameMenuItem
653
- onSelect={() => node.edit()}
654
- testId="file-explorer-rename-menu-item"
655
- />
656
- <DuplicateMenuItem
657
- onSelect={handleDuplicate}
658
- testId="file-explorer-duplicate-menu-item"
659
- />
680
+ {!node.data.isRoot && (
681
+ <>
682
+ <RenameMenuItem
683
+ onSelect={() => node.edit()}
684
+ testId="file-explorer-rename-menu-item"
685
+ />
686
+ <DuplicateMenuItem
687
+ onSelect={handleDuplicate}
688
+ testId="file-explorer-duplicate-menu-item"
689
+ />
690
+ </>
691
+ )}
660
692
  <DropdownMenuItem
661
693
  onSelect={async () => {
662
694
  await copyToClipboard(node.data.path);
@@ -667,13 +699,16 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
667
699
  <ListTreeIcon className={MENU_ITEM_ICON_CLASS} />
668
700
  Copy path
669
701
  </DropdownMenuItem>
670
- {tree && (
702
+ {tree && node.data.isPrimaryRoot && !node.data.isRoot && (
671
703
  <DropdownMenuItem
672
704
  onSelect={async () => {
673
- await copyToClipboard(
674
- tree.relativeFromRoot(node.data.path as FilePath),
705
+ const path = tree.getPrimaryRelativePath(
706
+ node.data.path as FilePath,
675
707
  );
676
- toast({ title: "Copied to clipboard" });
708
+ if (path !== null) {
709
+ await copyToClipboard(path);
710
+ toast({ title: "Copied to clipboard" });
711
+ }
677
712
  }}
678
713
  data-testid="file-explorer-copy-relative-path-menu-item"
679
714
  >
@@ -681,35 +716,39 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
681
716
  Copy relative path
682
717
  </DropdownMenuItem>
683
718
  )}
684
- <DropdownMenuSeparator />
685
- <DropdownMenuItem
686
- onSelect={() => {
687
- const { path } = node.data;
688
- const pythonCode = PYTHON_CODE_FOR_FILE_TYPE[fileType](path);
689
- handleInsertCode(pythonCode);
690
- }}
691
- data-testid="file-explorer-insert-snippet-menu-item"
692
- >
693
- <BetweenHorizontalStartIcon className={MENU_ITEM_ICON_CLASS} />
694
- Insert snippet for reading file
695
- </DropdownMenuItem>
696
- <DropdownMenuItem
697
- onSelect={async () => {
698
- toast({
699
- title: "Copied to clipboard",
700
- description:
701
- "Code to open the file has been copied to your clipboard. You can also drag and drop this file into the editor",
702
- });
703
- const { path } = node.data;
704
- const pythonCode = PYTHON_CODE_FOR_FILE_TYPE[fileType](path);
705
- await copyToClipboard(pythonCode);
706
- }}
707
- data-testid="file-explorer-copy-snippet-menu-item"
708
- >
709
- <BracesIcon className={MENU_ITEM_ICON_CLASS} />
710
- Copy snippet for reading file
711
- </DropdownMenuItem>
712
- {node.data.isMarimoFile && !isWasm() && (
719
+ {!node.data.isRoot && (
720
+ <>
721
+ <DropdownMenuSeparator />
722
+ <DropdownMenuItem
723
+ onSelect={() => {
724
+ const { path } = node.data;
725
+ const pythonCode = PYTHON_CODE_FOR_FILE_TYPE[fileType](path);
726
+ handleInsertCode(pythonCode);
727
+ }}
728
+ data-testid="file-explorer-insert-snippet-menu-item"
729
+ >
730
+ <BetweenHorizontalStartIcon className={MENU_ITEM_ICON_CLASS} />
731
+ Insert snippet for reading file
732
+ </DropdownMenuItem>
733
+ <DropdownMenuItem
734
+ onSelect={async () => {
735
+ toast({
736
+ title: "Copied to clipboard",
737
+ description:
738
+ "Code to open the file has been copied to your clipboard. You can also drag and drop this file into the editor",
739
+ });
740
+ const { path } = node.data;
741
+ const pythonCode = PYTHON_CODE_FOR_FILE_TYPE[fileType](path);
742
+ await copyToClipboard(pythonCode);
743
+ }}
744
+ data-testid="file-explorer-copy-snippet-menu-item"
745
+ >
746
+ <BracesIcon className={MENU_ITEM_ICON_CLASS} />
747
+ Copy snippet for reading file
748
+ </DropdownMenuItem>
749
+ </>
750
+ )}
751
+ {node.data.isMarimoFile && node.data.isPrimaryRoot && !isWasm() && (
713
752
  <>
714
753
  <DropdownMenuSeparator />
715
754
  <DropdownMenuItem
@@ -721,9 +760,9 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
721
760
  </DropdownMenuItem>
722
761
  </>
723
762
  )}
724
- <DropdownMenuSeparator />
725
763
  {!node.data.isDirectory && !disableFileDownloads && (
726
764
  <>
765
+ <DropdownMenuSeparator />
727
766
  <DropdownMenuItem
728
767
  onSelect={async () => {
729
768
  await downloadFile(node.data.path, node.data.name);
@@ -733,20 +772,24 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
733
772
  <DownloadIcon className={MENU_ITEM_ICON_CLASS} />
734
773
  Download
735
774
  </DropdownMenuItem>
775
+ </>
776
+ )}
777
+ {!node.data.isRoot && (
778
+ <>
736
779
  <DropdownMenuSeparator />
780
+ <DeleteMenuItem
781
+ onSelect={handleDeleteFile}
782
+ testId="file-explorer-delete-menu-item"
783
+ />
737
784
  </>
738
785
  )}
739
- <DeleteMenuItem
740
- onSelect={handleDeleteFile}
741
- testId="file-explorer-delete-menu-item"
742
- />
743
786
  </FileActionsDropdown>
744
787
  </span>
745
788
  </div>
746
789
  );
747
790
  };
748
791
 
749
- const FolderArrow = ({ node }: { node: NodeApi<FileInfo> }) => {
792
+ const FolderArrow = ({ node }: { node: NodeApi<FileTreeNode> }) => {
750
793
  if (!node.data.isDirectory) {
751
794
  return <span className="w-4 h-4 shrink-0" />;
752
795
  }
@@ -767,23 +810,20 @@ export function getUploadDestinationLabel(
767
810
  tree: RequestingTree,
768
811
  destinationPath: FilePath,
769
812
  ): string {
770
- if (destinationPath === tree.getRootPath()) {
771
- return "workspace root";
772
- }
773
- return tree.relativeFromRoot(destinationPath);
813
+ return tree.getDisplayPath(destinationPath);
774
814
  }
775
815
 
776
816
  export function filterHiddenTree(
777
- list: FileInfo[],
817
+ list: FileTreeNode[],
778
818
  showHidden: boolean,
779
- ): FileInfo[] {
819
+ ): FileTreeNode[] {
780
820
  if (showHidden) {
781
821
  return list;
782
822
  }
783
823
 
784
- const out: FileInfo[] = [];
824
+ const out: FileTreeNode[] = [];
785
825
  for (const item of list) {
786
- if (isDirectoryOrFileHidden(item.name)) {
826
+ if (!item.isRoot && isDirectoryOrFileHidden(item.name)) {
787
827
  continue;
788
828
  }
789
829
  let next = item;
@@ -805,10 +845,10 @@ export function isDirectoryOrFileHidden(filename: string): boolean {
805
845
  return false;
806
846
  }
807
847
 
808
- function treeContainsPath(list: FileInfo[], path: FilePath): boolean {
848
+ function treeContainsId(list: FileTreeNode[], id: string): boolean {
809
849
  return list.some(
810
850
  (item) =>
811
- item.path === path ||
812
- (item.children ? treeContainsPath(item.children, path) : false),
851
+ item.id === id ||
852
+ (item.children ? treeContainsId(item.children, id) : false),
813
853
  );
814
854
  }
@@ -9,7 +9,11 @@ import type { FileInfo } from "@/core/network/types";
9
9
  * is true. Auto-focuses and selects everything except the extension so a
10
10
  * user can type straight into the name.
11
11
  */
12
- export const FileNameInput = ({ node }: { node: NodeApi<FileInfo> }) => {
12
+ export const FileNameInput = <T extends FileInfo>({
13
+ node,
14
+ }: {
15
+ node: NodeApi<T>;
16
+ }) => {
13
17
  const ref = useRef<HTMLInputElement>(null);
14
18
  useEffect(() => {
15
19
  ref.current?.focus();
@@ -32,7 +32,7 @@ export const MAX_FILE_PREVIEW_BYTES = 10 * 1024 * 1024;
32
32
 
33
33
  interface Props {
34
34
  file: FileInfo;
35
- onOpenNotebook: (
35
+ onOpenNotebook?: (
36
36
  evt: Pick<Event, "stopPropagation" | "preventDefault">,
37
37
  ) => void;
38
38
  }
@@ -156,7 +156,7 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
156
156
  onDownload={disableFileDownloads ? undefined : handleDownload}
157
157
  actions={
158
158
  <>
159
- {file.isMarimoFile && !isWasm() && (
159
+ {file.isMarimoFile && onOpenNotebook && !isWasm() && (
160
160
  <Tooltip content="Open notebook">
161
161
  <Button
162
162
  variant="text"