@marimo-team/islands 0.23.15-dev4 → 0.23.15-dev41

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 (92) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CU4fZJzG.js → ConnectedDataExplorerComponent-7p7rt9iw.js} +4 -4
  2. package/dist/{ErrorBoundary-DE6tzZf-.js → ErrorBoundary-B_CAG7_e.js} +1 -1
  3. package/dist/{ImageComparisonComponent-CHrI72em.js → ImageComparisonComponent-1i5IBhon.js} +173 -129
  4. package/dist/assets/__vite-browser-external-CjNAy01L.js +1 -0
  5. package/dist/assets/worker-B3XPCb6y.js +98 -0
  6. package/dist/{chat-ui-DinOvbWu.js → chat-ui-C0z13gJB.js} +3111 -3111
  7. package/dist/{code-visibility-Dk8cVOny.js → code-visibility-COokQS5X.js} +946 -864
  8. package/dist/{constants-T20xxyNf.js → debounce-BOD3DbfP.js} +1 -24
  9. package/dist/{formats-Dzx4J_z1.js → formats-CitsMtUm.js} +1 -1
  10. package/dist/{glide-data-editor-CjTu7ukN.js → glide-data-editor-CwZz71BD.js} +3 -3
  11. package/dist/{html-to-image-_wGfk8V-.js → html-to-image-CEo5pRYw.js} +2291 -2250
  12. package/dist/{input-DtsN7xm-.js → input-BGPrFH3g.js} +2 -2
  13. package/dist/main.js +1297 -1201
  14. package/dist/{mermaid-BYqXy_NE.js → mermaid-UdmxG2PZ.js} +2 -2
  15. package/dist/{process-output-Mh4UrjwM.js → process-output-BOvvilRG.js} +1 -1
  16. package/dist/{reveal-component-B5DXLyO7.js → reveal-component-D3ySPwH5.js} +618 -605
  17. package/dist/{spec-Cz-Bj1JI.js → spec-DSs9v0xx.js} +1 -1
  18. package/dist/style.css +1 -1
  19. package/dist/{toDate-CWNNlFEX.js → toDate-BRJgtOGm.js} +14 -5
  20. package/dist/{useAsyncData-KfHB8wQR.js → useAsyncData-BSOyAbac.js} +1 -1
  21. package/dist/{useDeepCompareMemoize-nJMtxhm4.js → useDeepCompareMemoize-BTLeWzuR.js} +1 -1
  22. package/dist/{useLifecycle-DegSo0lV.js → useLifecycle-C6wHjkhW.js} +1 -1
  23. package/dist/{useTheme-6eZ3GOTS.js → useTheme-Df_vGflw.js} +62 -27
  24. package/dist/{vega-component-DzyyM9fc.js → vega-component-BAvmvTjO.js} +6 -6
  25. package/package.json +1 -1
  26. package/src/components/chat/__tests__/chat-utils.test.ts +244 -1
  27. package/src/components/chat/__tests__/message-queue.test.tsx +121 -0
  28. package/src/components/chat/chat-panel.tsx +196 -67
  29. package/src/components/chat/chat-utils.ts +111 -2
  30. package/src/components/editor/SortableCell.tsx +6 -2
  31. package/src/components/editor/__tests__/output-persistence.test.tsx +241 -0
  32. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +153 -0
  33. package/src/components/editor/actions/pair-with-agent-commands.ts +109 -0
  34. package/src/components/editor/actions/pair-with-agent-modal.tsx +20 -64
  35. package/src/components/editor/ai/add-cell-with-ai.tsx +14 -5
  36. package/src/components/editor/ai/ai-completion-editor.tsx +4 -1
  37. package/src/components/editor/cell/cell-context-menu.tsx +7 -0
  38. package/src/components/editor/chrome/panels/packages-panel.tsx +11 -1
  39. package/src/components/editor/columns/__tests__/cell-column.test.tsx +105 -0
  40. package/src/components/editor/columns/cell-column.tsx +34 -4
  41. package/src/components/editor/columns/sortable-column.tsx +24 -4
  42. package/src/components/editor/file-tree/download.ts +46 -0
  43. package/src/components/editor/file-tree/file-explorer.tsx +3 -21
  44. package/src/components/editor/file-tree/file-viewer.tsx +4 -27
  45. package/src/components/editor/navigation/__tests__/navigation.test.ts +33 -0
  46. package/src/components/editor/navigation/navigation.ts +8 -1
  47. package/src/components/editor/notebook-cell.tsx +203 -106
  48. package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +66 -0
  49. package/src/components/editor/renderers/cell-array.tsx +26 -13
  50. package/src/components/editor/renderers/cells-renderer.tsx +8 -2
  51. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +29 -0
  52. package/src/components/editor/renderers/slides-layout/types.ts +4 -0
  53. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
  54. package/src/components/slides/reveal-component.tsx +37 -4
  55. package/src/components/slides/slide-form.tsx +72 -0
  56. package/src/components/ui/toast.tsx +11 -3
  57. package/src/components/ui/toaster.tsx +65 -9
  58. package/src/core/cells/__tests__/utils.test.ts +59 -0
  59. package/src/core/cells/utils.ts +51 -0
  60. package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +97 -3
  61. package/src/core/codemirror/completion/signature-hint.ts +78 -11
  62. package/src/core/constants.ts +6 -0
  63. package/src/core/islands/__tests__/bridge.test.ts +341 -46
  64. package/src/core/islands/__tests__/main.test.ts +176 -0
  65. package/src/core/islands/__tests__/parse.test.ts +105 -0
  66. package/src/core/islands/bootstrap.ts +8 -3
  67. package/src/core/islands/bridge.ts +116 -23
  68. package/src/core/islands/components/__tests__/web-components.test.tsx +214 -0
  69. package/src/core/islands/components/web-components.tsx +76 -15
  70. package/src/core/islands/constants.ts +1 -0
  71. package/src/core/islands/main.ts +69 -3
  72. package/src/core/islands/parse.ts +70 -23
  73. package/src/core/islands/worker/__tests__/controller.test.ts +173 -0
  74. package/src/core/islands/worker/worker.tsx +145 -57
  75. package/src/core/runtime/__tests__/runtime.test.ts +64 -0
  76. package/src/core/runtime/runtime.ts +30 -10
  77. package/src/core/wasm/worker/bootstrap.ts +113 -20
  78. package/src/css/app/Cell.css +10 -0
  79. package/src/hooks/__tests__/useHotkey.test.tsx +88 -0
  80. package/src/hooks/useHotkey.ts +29 -4
  81. package/src/plugins/impl/anywidget/__tests__/host.test.ts +6 -9
  82. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +22 -61
  83. package/src/plugins/impl/anywidget/model-proxy.ts +0 -13
  84. package/src/plugins/impl/anywidget/widget-binding.ts +4 -34
  85. package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +53 -2
  86. package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +71 -0
  87. package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +71 -3
  88. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -0
  89. package/src/theme/__tests__/useTheme.test.ts +68 -0
  90. package/src/theme/useTheme.ts +16 -1
  91. package/dist/assets/__vite-browser-external-BQCLNwri.js +0 -1
  92. package/dist/assets/worker-DAWRHcPq.js +0 -73
@@ -39,12 +39,18 @@ interface Props extends Pick<
39
39
  "cellId" | "getEditorView"
40
40
  > {
41
41
  children: React.ReactNode;
42
+ /**
43
+ * If true, the custom context menu is disabled and the native one is used
44
+ * (e.g. while presenting).
45
+ */
46
+ disabled?: boolean;
42
47
  }
43
48
 
44
49
  export const CellActionsContextMenu = ({
45
50
  children,
46
51
  cellId,
47
52
  getEditorView,
53
+ disabled,
48
54
  }: Props) => {
49
55
  const cellData = useCellData(cellId);
50
56
  const cellRuntime = useCellRuntime(cellId);
@@ -187,6 +193,7 @@ export const CellActionsContextMenu = ({
187
193
  return (
188
194
  <ContextMenu>
189
195
  <ContextMenuTrigger
196
+ disabled={disabled}
190
197
  onContextMenu={(evt) => {
191
198
  if (evt.target instanceof HTMLImageElement) {
192
199
  setImageRightClicked(evt.target);
@@ -303,6 +303,16 @@ const PackagesList: React.FC<{
303
303
  onSuccess: () => void;
304
304
  packages: { name: string; version: string }[];
305
305
  }> = ({ onSuccess, packages }) => {
306
+ // Sort case-insensitively so packages are strictly alphabetical
307
+ // regardless of capitalization (package managers sort inconsistently).
308
+ const sortedPackages = React.useMemo(
309
+ () =>
310
+ packages.toSorted((a, b) =>
311
+ a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
312
+ ),
313
+ [packages],
314
+ );
315
+
306
316
  if (packages.length === 0) {
307
317
  return (
308
318
  <PanelEmptyState
@@ -323,7 +333,7 @@ const PackagesList: React.FC<{
323
333
  </TableRow>
324
334
  </TableHeader>
325
335
  <TableBody>
326
- {packages.map((item) => (
336
+ {sortedPackages.map((item) => (
327
337
  <TableRow
328
338
  key={item.name}
329
339
  className="group"
@@ -0,0 +1,105 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { render } from "@testing-library/react";
3
+ import { createStore, Provider } from "jotai";
4
+ import { describe, expect, it } from "vitest";
5
+ import { TooltipProvider } from "@/components/ui/tooltip";
6
+ import type { CellColumnId } from "@/utils/id-tree";
7
+ import { Column } from "../cell-column";
8
+
9
+ const columnId = "col-1" as CellColumnId;
10
+
11
+ function renderColumn({
12
+ width,
13
+ presenting,
14
+ }: {
15
+ width: "compact" | "columns";
16
+ presenting: boolean;
17
+ }) {
18
+ const store = createStore();
19
+ return render(
20
+ <Provider store={store}>
21
+ <TooltipProvider>
22
+ <Column
23
+ columnId={columnId}
24
+ index={0}
25
+ width={width}
26
+ canDelete={false}
27
+ canMoveLeft={false}
28
+ canMoveRight={false}
29
+ presenting={presenting}
30
+ >
31
+ <div data-testid="column-children" />
32
+ </Column>
33
+ </TooltipProvider>
34
+ </Provider>,
35
+ );
36
+ }
37
+
38
+ describe("Column (single-column notebook)", () => {
39
+ it("spaces cells with the --notebook-cell-gap variable", () => {
40
+ const { getByTestId } = renderColumn({
41
+ width: "compact",
42
+ presenting: false,
43
+ });
44
+ const column = getByTestId("cell-column");
45
+ expect(column.classList.contains("gap-(--notebook-cell-gap)")).toBe(true);
46
+ expect(column.classList.contains("[--notebook-cell-gap:0px]")).toBe(false);
47
+ });
48
+
49
+ it("zeroes the cell gap while presenting", () => {
50
+ const { getByTestId } = renderColumn({
51
+ width: "compact",
52
+ presenting: true,
53
+ });
54
+ const column = getByTestId("cell-column");
55
+ expect(column.classList.contains("gap-(--notebook-cell-gap)")).toBe(true);
56
+ expect(column.classList.contains("[--notebook-cell-gap:0px]")).toBe(true);
57
+ });
58
+ });
59
+
60
+ describe("Column (multi-column notebook)", () => {
61
+ it("shows column chrome when not presenting", () => {
62
+ const { getByTestId, getAllByTestId } = renderColumn({
63
+ width: "columns",
64
+ presenting: false,
65
+ });
66
+
67
+ expect(getByTestId("column-header").hidden).toBe(false);
68
+ expect(getByTestId("column-frame").classList.contains("border")).toBe(true);
69
+ for (const handle of getAllByTestId("column-resize-handle")) {
70
+ expect(handle.hidden).toBe(false);
71
+ }
72
+ });
73
+
74
+ it("hides column chrome while presenting, without unmounting it", () => {
75
+ const { getByTestId, getAllByTestId } = renderColumn({
76
+ width: "columns",
77
+ presenting: true,
78
+ });
79
+
80
+ // Header (drag handle, move/delete/add buttons) is hidden but mounted
81
+ expect(getByTestId("column-header").hidden).toBe(true);
82
+ // No border around the column
83
+ expect(getByTestId("column-frame").classList.contains("border")).toBe(
84
+ false,
85
+ );
86
+ // Resize handles are hidden
87
+ const handles = getAllByTestId("column-resize-handle");
88
+ expect(handles.length).toBeGreaterThan(0);
89
+ for (const handle of handles) {
90
+ expect(handle.hidden).toBe(true);
91
+ }
92
+ // Cell gap is zeroed on the column root (cascades to descendants)
93
+ const columnRoot = getByTestId("column-frame").parentElement;
94
+ expect(columnRoot?.classList.contains("[--notebook-cell-gap:0px]")).toBe(
95
+ true,
96
+ );
97
+ // Editor geometry (saved widths, min-width, gutters) is replaced by the
98
+ // published content width
99
+ const inner = getByTestId("column-children").parentElement;
100
+ expect(inner?.classList.contains("min-w-[500px]")).toBe(false);
101
+ expect(inner?.classList.contains("w-(--content-width)")).toBe(true);
102
+ // Children stay mounted
103
+ expect(getByTestId("column-children")).toBeTruthy();
104
+ });
105
+ });
@@ -3,6 +3,7 @@
3
3
  import React, { memo, useRef } from "react";
4
4
  import type { AppConfig } from "@/core/config/config-schema";
5
5
  import { useResizeHandle } from "@/hooks/useResizeHandle";
6
+ import { cn } from "@/utils/cn";
6
7
  import type { CellColumnId } from "@/utils/id-tree";
7
8
  import { SortableColumn } from "./sortable-column";
8
9
  import { storageFn } from "./storage";
@@ -17,6 +18,11 @@ interface Props {
17
18
  canDelete: boolean;
18
19
  canMoveLeft: boolean;
19
20
  canMoveRight: boolean;
21
+ /**
22
+ * If true, column chrome (drag handles, resize handles, borders) is hidden
23
+ * while keeping the same component tree mounted.
24
+ */
25
+ presenting?: boolean;
20
26
  }
21
27
 
22
28
  const { getColumnWidth, saveColumnWidth } = storageFn;
@@ -24,6 +30,10 @@ const { getColumnWidth, saveColumnWidth } = storageFn;
24
30
  export const Column = memo((props: Props) => {
25
31
  const columnRef = useRef<HTMLDivElement>(null);
26
32
 
33
+ // Published cells are not spaced apart. Set once on the column root; the
34
+ // CSS variable cascades to the gap-using descendants.
35
+ const zeroGapWhenPresenting = props.presenting && "[--notebook-cell-gap:0px]";
36
+
27
37
  if (props.width === "columns") {
28
38
  return (
29
39
  <SortableColumn
@@ -33,14 +43,16 @@ export const Column = memo((props: Props) => {
33
43
  columnId={props.columnId}
34
44
  canMoveLeft={props.canMoveLeft}
35
45
  canMoveRight={props.canMoveRight}
36
- className="group/column"
46
+ className={cn("group/column", zeroGapWhenPresenting)}
37
47
  footer={props.footer}
48
+ presenting={props.presenting}
38
49
  >
39
50
  <ResizableComponent
40
51
  startingWidth={getColumnWidth(props.index)}
41
52
  onResize={(width: number) => {
42
53
  saveColumnWidth(props.index, width);
43
54
  }}
55
+ presenting={props.presenting}
44
56
  >
45
57
  {props.children}
46
58
  </ResizableComponent>
@@ -50,7 +62,13 @@ export const Column = memo((props: Props) => {
50
62
 
51
63
  return (
52
64
  <>
53
- <div data-testid="cell-column" className="flex flex-col gap-5">
65
+ <div
66
+ data-testid="cell-column"
67
+ className={cn(
68
+ "flex flex-col gap-(--notebook-cell-gap)",
69
+ zeroGapWhenPresenting,
70
+ )}
71
+ >
54
72
  {props.children}
55
73
  </div>
56
74
  {props.footer}
@@ -64,12 +82,14 @@ interface ResizableComponentProps {
64
82
  startingWidth: number | "contentWidth";
65
83
  onResize?: (width: number) => void;
66
84
  children: React.ReactNode;
85
+ presenting?: boolean;
67
86
  }
68
87
 
69
88
  const ResizableComponent = ({
70
89
  startingWidth,
71
90
  onResize,
72
91
  children,
92
+ presenting,
73
93
  }: ResizableComponentProps) => {
74
94
  const { resizableDivRef, handleRefs, style } = useResizeHandle({
75
95
  startingWidth,
@@ -80,11 +100,13 @@ const ResizableComponent = ({
80
100
  return (
81
101
  <div
82
102
  ref={ref}
103
+ data-testid="column-resize-handle"
83
104
  className={`w-[3px] cursor-col-resize transition-colors duration-200 z-100
84
105
  relative before:content-[''] before:absolute before:inset-y-0 before:left-[-3px]
85
106
  before:right-[-3px] before:w-[9px] before:z-[-1]
86
107
  hover/column:bg-[var(--slate-3)] dark:hover/column:bg-[var(--slate-5)]
87
108
  hover/column:hover:bg-primary/60 dark:hover/column:hover:bg-primary/60`}
109
+ hidden={presenting}
88
110
  />
89
111
  );
90
112
  };
@@ -94,8 +116,16 @@ const ResizableComponent = ({
94
116
  {renderResizeHandler(handleRefs.left)}
95
117
  <div
96
118
  ref={resizableDivRef}
97
- className="flex flex-col gap-5 box-content min-h-[100px] px-11 pt-3 pb-6 min-w-[500px] z-1"
98
- style={style}
119
+ className={cn(
120
+ "flex flex-col gap-(--notebook-cell-gap) box-content z-1",
121
+ // While presenting, use the published content width instead of the
122
+ // editor geometry (saved pixel width, 500px minimum, gutters),
123
+ // which overflows narrow screens.
124
+ presenting
125
+ ? "w-(--content-width)"
126
+ : "min-h-[100px] px-11 pt-3 pb-6 min-w-[500px]",
127
+ )}
128
+ style={presenting ? undefined : style}
99
129
  >
100
130
  {children}
101
131
  </div>
@@ -30,11 +30,24 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
30
30
  canMoveLeft: boolean;
31
31
  canMoveRight: boolean;
32
32
  footer?: React.ReactNode;
33
+ /**
34
+ * If true, dragging is disabled and the column chrome is hidden
35
+ * (e.g. while presenting).
36
+ */
37
+ presenting?: boolean;
33
38
  }
34
39
 
35
40
  const SortableColumnInternal = React.forwardRef(
36
41
  (
37
- { columnId, canDelete, canMoveLeft, canMoveRight, footer, ...props }: Props,
42
+ {
43
+ columnId,
44
+ canDelete,
45
+ canMoveLeft,
46
+ canMoveRight,
47
+ footer,
48
+ presenting,
49
+ ...props
50
+ }: Props,
38
51
  ref: React.Ref<HTMLDivElement>,
39
52
  ) => {
40
53
  const {
@@ -45,7 +58,7 @@ const SortableColumnInternal = React.forwardRef(
45
58
  transition,
46
59
  isDragging,
47
60
  isOver,
48
- } = useSortable({ id: columnId });
61
+ } = useSortable({ id: columnId, disabled: presenting });
49
62
 
50
63
  const style: React.CSSProperties = {
51
64
  transform: transform
@@ -78,7 +91,11 @@ const SortableColumnInternal = React.forwardRef(
78
91
  };
79
92
 
80
93
  const dragHandle = (
81
- <div className="px-2 pb-0 group flex items-center overflow-hidden border-b border-(--slate-7)">
94
+ <div
95
+ data-testid="column-header"
96
+ className="px-2 pb-0 group flex items-center overflow-hidden border-b border-(--slate-7)"
97
+ hidden={presenting}
98
+ >
82
99
  <Tooltip content="Move column left" side="top" delayDuration={300}>
83
100
  <Button
84
101
  variant="text"
@@ -161,7 +178,10 @@ const SortableColumnInternal = React.forwardRef(
161
178
  isOver && "bg-accent/20", // Add a background color when dragging over
162
179
  )}
163
180
  >
164
- <div className="border border-(--slate-7)">
181
+ <div
182
+ data-testid="column-frame"
183
+ className={cn(!presenting && "border border-(--slate-7)")}
184
+ >
165
185
  {dragHandle}
166
186
  {props.children}
167
187
  </div>
@@ -0,0 +1,46 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { getRequestClient } from "@/core/network/requests";
4
+ import { getRuntimeManager } from "@/core/runtime/config";
5
+ import { isWasm } from "@/core/wasm/utils";
6
+ import { deserializeBlob } from "@/utils/blob";
7
+ import { downloadBlob, downloadByURL } from "@/utils/download";
8
+ import { type Base64String, base64ToDataURL } from "@/utils/json/base64";
9
+
10
+ /**
11
+ * Download a workspace file by path.
12
+ *
13
+ * Over HTTP this navigates to a streaming download endpoint, so the
14
+ * browser's download manager handles the transfer and no file contents
15
+ * are buffered in memory. In WASM there is no HTTP server to stream
16
+ * from; contents come over the pyodide bridge as base64 and are saved
17
+ * via a Blob.
18
+ */
19
+ export async function downloadFile(path: string, name: string): Promise<void> {
20
+ if (!isWasm()) {
21
+ const url = getRuntimeManager().formatNavigableHttpURL(
22
+ "api/files/download",
23
+ new URLSearchParams({ path }),
24
+ );
25
+ downloadByURL(url.toString(), name);
26
+ return;
27
+ }
28
+
29
+ const details = await getRequestClient().sendFileDetails({ path });
30
+ if (details.isBase64 && details.contents) {
31
+ const blob = deserializeBlob(
32
+ base64ToDataURL(
33
+ details.contents as Base64String,
34
+ details.mimeType || "application/octet-stream",
35
+ ),
36
+ );
37
+ downloadBlob(blob, name);
38
+ } else {
39
+ downloadBlob(
40
+ new Blob([details.contents || ""], {
41
+ type: details.mimeType || "application/octet-stream",
42
+ }),
43
+ name,
44
+ );
45
+ }
46
+ }
@@ -62,16 +62,14 @@ 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";
65
- import { deserializeBlob } from "@/utils/blob";
66
65
  import { cn } from "@/utils/cn";
67
66
  import { copyToClipboard } from "@/utils/copy";
68
- import { downloadBlob } from "@/utils/download";
69
- import { type Base64String, base64ToDataURL } from "@/utils/json/base64";
70
67
  import { openNotebook } from "@/utils/links";
71
68
  import type { FilePath } from "@/utils/paths";
72
69
  import { makeDuplicateName } from "@/utils/pathUtils";
73
70
  import { jotaiJsonStorage } from "@/utils/storage/jotai";
74
71
  import { useTreeDndManager } from "./dnd-wrapper";
72
+ import { downloadFile } from "./download";
75
73
  import { FileViewer } from "./file-viewer";
76
74
  import type { RequestingTree } from "./requesting-tree";
77
75
  import { openStateAtom, treeAtom } from "./state";
@@ -393,7 +391,7 @@ const Show = ({
393
391
  };
394
392
 
395
393
  const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
396
- const { openFile, sendFileDetails } = useRequestClient();
394
+ const { openFile } = useRequestClient();
397
395
  const disableFileDownloads = useAtomValue(disableFileDownloadsAtom);
398
396
 
399
397
  const fileType: FileIconType = node.data.isDirectory
@@ -645,23 +643,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
645
643
  <>
646
644
  <DropdownMenuItem
647
645
  onSelect={async () => {
648
- const details = await sendFileDetails({
649
- path: node.data.path,
650
- });
651
- if (details.isBase64 && details.contents) {
652
- const blob = deserializeBlob(
653
- base64ToDataURL(
654
- details.contents as Base64String,
655
- details.mimeType || "application/octet-stream",
656
- ),
657
- );
658
- downloadBlob(blob, node.data.name);
659
- } else {
660
- downloadBlob(
661
- new Blob([details.contents || ""]),
662
- node.data.name,
663
- );
664
- }
646
+ await downloadFile(node.data.path, node.data.name);
665
647
  }}
666
648
  data-testid="file-explorer-download-menu-item"
667
649
  >
@@ -21,10 +21,9 @@ import { filenameAtom } from "@/core/saving/file-state";
21
21
  import { isWasm } from "@/core/wasm/utils";
22
22
  import { useAsyncData } from "@/hooks/useAsyncData";
23
23
  import { ErrorBanner } from "@/plugins/impl/common/error-banner";
24
- import { deserializeBlob } from "@/utils/blob";
25
24
  import { copyToClipboard } from "@/utils/copy";
26
- import { downloadBlob, downloadByURL } from "@/utils/download";
27
- import { type Base64String, base64ToDataURL } from "@/utils/json/base64";
25
+ import type { Base64String } from "@/utils/json/base64";
26
+ import { downloadFile } from "./download";
28
27
  import { FilePreviewHeader } from "./file-header";
29
28
  import {
30
29
  FileContentRenderer,
@@ -121,30 +120,8 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
121
120
  );
122
121
  }
123
122
 
124
- const handleDownload = () => {
125
- if (data.isBase64 && data.contents) {
126
- if (isMediaMime(mimeType)) {
127
- const dataURL = base64ToDataURL(
128
- data.contents as Base64String,
129
- mimeType,
130
- );
131
- downloadByURL(dataURL, data.file.name);
132
- } else {
133
- const blob = deserializeBlob(
134
- base64ToDataURL(
135
- data.contents as Base64String,
136
- data.mimeType || "application/octet-stream",
137
- ),
138
- );
139
- downloadBlob(blob, data.file.name);
140
- }
141
- return;
142
- }
143
-
144
- downloadBlob(
145
- new Blob([data.contents || internalValue], { type: mimeType }),
146
- data.file.name,
147
- );
123
+ const handleDownload = async () => {
124
+ await downloadFile(file.path, data.file.name);
148
125
  };
149
126
 
150
127
  const header = (
@@ -12,6 +12,7 @@ import { MockRequestClient } from "@/__mocks__/requests";
12
12
  import { aiCompletionCellAtom } from "@/core/ai/state";
13
13
  import type { CellActions } from "@/core/cells/cells";
14
14
  import { notebookAtom } from "@/core/cells/cells";
15
+ import { signatureHintField } from "@/core/codemirror/completion/signature-hint";
15
16
  import {
16
17
  configOverridesAtom,
17
18
  platformAtom,
@@ -1825,6 +1826,38 @@ describe("useCellEditorNavigationProps", () => {
1825
1826
  expect(focusCell).not.toHaveBeenCalled();
1826
1827
  });
1827
1828
 
1829
+ it("should dismiss the signature hint when Escape is pressed without exiting to command mode", () => {
1830
+ mockSimplifySelection.mockReturnValue(false);
1831
+ mockCompletionStatus.mockReturnValue(null);
1832
+
1833
+ const dispatch = vi.fn();
1834
+ const mockEditorView = {
1835
+ current: {
1836
+ state: {
1837
+ selection: { main: { from: 9, to: 9, empty: true } },
1838
+ field: vi.fn((field: unknown) =>
1839
+ field === signatureHintField ? { pos: 9 } : false,
1840
+ ),
1841
+ },
1842
+ dispatch,
1843
+ } as unknown as EditorView,
1844
+ };
1845
+
1846
+ const { result } = renderWithProvider(() =>
1847
+ useCellEditorNavigationProps(mockCellId, mockEditorView),
1848
+ );
1849
+
1850
+ const mockEvent = Mocks.keyboardEvent({ key: "Escape" });
1851
+
1852
+ act(() => {
1853
+ result.current.onKeyDown?.(mockEvent);
1854
+ });
1855
+
1856
+ // The hint is dismissed (dispatch called) but we stay in edit mode.
1857
+ expect(dispatch).toHaveBeenCalled();
1858
+ expect(focusCell).not.toHaveBeenCalled();
1859
+ });
1860
+
1828
1861
  it("should continue propagation for other keys", () => {
1829
1862
  const mockEditorView = { current: null };
1830
1863
  const { result } = renderWithProvider(() =>
@@ -22,6 +22,10 @@ import {
22
22
  } from "@/core/cells/pending-cut-service";
23
23
  import { usePendingDeleteService } from "@/core/cells/pending-delete-service";
24
24
  import { scrollCellIntoView } from "@/core/cells/scrollCellIntoView";
25
+ import {
26
+ closeSignatureHint,
27
+ signatureHintField,
28
+ } from "@/core/codemirror/completion/signature-hint";
25
29
  import {
26
30
  hotkeysAtom,
27
31
  isAiFeatureEnabled,
@@ -713,7 +717,9 @@ export function useCellEditorNavigationProps(
713
717
  return;
714
718
  }
715
719
 
716
- const hasSignatureHelp = state.field(signatureHelpTooltipField, false);
720
+ const hasSignatureHelp =
721
+ Boolean(state.field(signatureHelpTooltipField, false)) ||
722
+ Boolean(state.field(signatureHintField, false));
717
723
  const hasAutocompletePopup = completionStatus(state) !== null;
718
724
  if (hasSignatureHelp) {
719
725
  closeSignatureHelp(view);
@@ -784,4 +790,5 @@ export function closeSignatureHelp(view: EditorView) {
784
790
  if (view.state.field(signatureHelpTooltipField, false)) {
785
791
  view.dispatch({ effects: setSignatureHelpTooltip.of(null) });
786
792
  }
793
+ closeSignatureHint(view);
787
794
  }