@marimo-team/islands 0.19.10-dev9 → 0.19.11-dev0

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 (94) hide show
  1. package/dist/Plot-C9vQQj4X.js +172249 -0
  2. package/dist/{any-language-editor-CKEbZakX.js → any-language-editor-t_VsTNa-.js} +16 -16
  3. package/dist/dist-4YNZxwMI.js +8 -0
  4. package/dist/dist-7nR3r2kG.js +5 -0
  5. package/dist/{dist-CBA36Nuy.js → dist-B2-r9y-0.js} +109 -109
  6. package/dist/dist-B2gkyT3r.js +5 -0
  7. package/dist/{dist-DRtGOCCq.js → dist-B4tYJP_i.js} +2 -2
  8. package/dist/{dist-C0e1aNzV.js → dist-B5ATpkxy.js} +2 -2
  9. package/dist/dist-B8G3I6vJ.js +8 -0
  10. package/dist/{dist-DKnxaCRl.js → dist-B94MxrQS.js} +2 -2
  11. package/dist/dist-BJ96Ykfp.js +8 -0
  12. package/dist/dist-BKLIWGw4.js +5 -0
  13. package/dist/{dist-l0KayR2-.js → dist-BLwfpZD-.js} +2 -2
  14. package/dist/{dist-CzKXtzDE.js → dist-BYmtF1W6.js} +2 -2
  15. package/dist/{dist-BJUs1DAG.js → dist-BbBnU4tG.js} +1 -1
  16. package/dist/dist-Bf3ou00A.js +6 -0
  17. package/dist/{dist-CITQGRtG.js → dist-BfactX3G.js} +4 -4
  18. package/dist/{dist-DiCjkKC2.js → dist-BoAHOW2l.js} +2 -2
  19. package/dist/{dist-BsBHh4jO.js → dist-Bsv_ARko.js} +4 -4
  20. package/dist/dist-BvkKXuPm.js +5 -0
  21. package/dist/{dist-DsqQCNKw.js → dist-C2-m5aEk.js} +119 -119
  22. package/dist/dist-C6NJ3n6r.js +5 -0
  23. package/dist/{dist-yI-ah_iK.js → dist-CC9VUnXd.js} +1 -1
  24. package/dist/{dist-tdABwZK5.js → dist-CE43BRmt.js} +1 -1
  25. package/dist/{dist-COp5dkis.js → dist-CJrHMxlI.js} +31 -31
  26. package/dist/{dist-Ct5hkOvC.js → dist-CPTE45iS.js} +1 -1
  27. package/dist/{dist-C-at-5cM.js → dist-CcOGT46m.js} +27 -27
  28. package/dist/dist-CecLPYY5.js +5 -0
  29. package/dist/{dist-BSMZYwqW.js → dist-Cgf353Ki.js} +1 -1
  30. package/dist/dist-Ch0SwRzK.js +5 -0
  31. package/dist/{dist-BqYNqP5W.js → dist-CkEUrAus.js} +2 -2
  32. package/dist/{dist-D4ObdSdT.js → dist-CmZYrgd_.js} +1 -1
  33. package/dist/{dist-D1q38GZb.js → dist-Crk9ejOy.js} +4 -4
  34. package/dist/dist-D6eWHiFh.js +6 -0
  35. package/dist/dist-DCQ710Bv.js +5 -0
  36. package/dist/{dist-r6N_0WG-.js → dist-DOil6y-3.js} +4 -4
  37. package/dist/{dist-CPd_adhw.js → dist-Dc1SFk5I.js} +2 -2
  38. package/dist/dist-Dit9tk8a.js +1242 -0
  39. package/dist/{dist-B7NoEgR4.js → dist-DqJdzAYM.js} +2 -2
  40. package/dist/dist-P_pkS5f-.js +8 -0
  41. package/dist/{dist-CsjsvW0K.js → dist-T4g7Sr6e.js} +3 -3
  42. package/dist/{dist-WETuLs_C.js → dist-glA_fIK_.js} +2 -2
  43. package/dist/{dist-bRBEzJF8.js → dist-iiugPhCC.js} +1 -1
  44. package/dist/{dist-D7jHtwN8.js → dist-r8ecBV-v.js} +135 -65
  45. package/dist/{dist-BlRm4v0e.js → dist-yVJ4xE5n.js} +5 -5
  46. package/dist/{esm-QY6C-Sev.js → esm-BAS2d2Ad.js} +1421 -1454
  47. package/dist/main.js +421 -383
  48. package/package.json +11 -11
  49. package/src/components/data-table/TableActions.tsx +8 -1
  50. package/src/components/data-table/data-table.tsx +2 -0
  51. package/src/components/data-table/download-actions.tsx +6 -1
  52. package/src/components/dependency-graph/dependency-graph-tree.tsx +10 -1
  53. package/src/components/dependency-graph/dependency-graph.tsx +1 -0
  54. package/src/components/dependency-graph/elements.ts +20 -9
  55. package/src/components/dependency-graph/panels.tsx +27 -11
  56. package/src/components/dependency-graph/types.ts +1 -0
  57. package/src/components/editor/chrome/wrapper/app-chrome.tsx +3 -0
  58. package/src/components/editor/package-alert.tsx +4 -4
  59. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +3 -5
  60. package/src/core/codemirror/misc/__tests__/paste.test.ts +18 -0
  61. package/src/core/codemirror/misc/paste.ts +14 -10
  62. package/src/core/kernel/messages.ts +1 -0
  63. package/src/core/static/static-state.ts +5 -0
  64. package/src/core/static/types.ts +2 -0
  65. package/src/core/wasm/__tests__/store.test.ts +33 -0
  66. package/src/core/wasm/bridge.ts +2 -1
  67. package/src/core/wasm/store.ts +13 -1
  68. package/src/mount.tsx +23 -1
  69. package/src/plugins/impl/DataTablePlugin.tsx +4 -0
  70. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +7 -5
  71. package/src/plugins/impl/anywidget/__tests__/model.test.ts +53 -0
  72. package/src/plugins/impl/anywidget/model.ts +13 -10
  73. package/src/plugins/impl/chat/ChatPlugin.tsx +2 -0
  74. package/src/plugins/impl/chat/chat-ui.tsx +10 -1
  75. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +4 -0
  76. package/src/plugins/impl/plotly/Plot.tsx +2 -0
  77. package/src/plugins/impl/plotly/PlotlyPlugin.tsx +36 -0
  78. package/src/theme/ThemeProvider.tsx +2 -0
  79. package/dist/Plot-CmsrWWji.js +0 -169233
  80. package/dist/dist-BKTAAusE.js +0 -5
  81. package/dist/dist-BNXv9Wjt.js +0 -6
  82. package/dist/dist-C-tlm9eD.js +0 -6
  83. package/dist/dist-CFi_P6cs.js +0 -5
  84. package/dist/dist-CgQuqOGS.js +0 -5
  85. package/dist/dist-CgkWmw0c.js +0 -5
  86. package/dist/dist-DGkeEIsV.js +0 -8
  87. package/dist/dist-DOwZz8aI.js +0 -5
  88. package/dist/dist-DRiJGkDN.js +0 -8
  89. package/dist/dist-GR6ABeNk.js +0 -8
  90. package/dist/dist-NcujbSeH.js +0 -8
  91. package/dist/dist-g1p2PEVs.js +0 -1242
  92. package/dist/dist-pNvwCQqJ.js +0 -5
  93. package/dist/dist-wudNDAiO.js +0 -5
  94. package/dist/dist-yK8yJfz2.js +0 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.19.10-dev9",
3
+ "version": "0.19.11-dev0",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -24,19 +24,19 @@
24
24
  "@ai-sdk/react": "^2.0.125",
25
25
  "@anywidget/types": "^0.2.0",
26
26
  "@codemirror/autocomplete": "^6.20.0",
27
- "@codemirror/commands": "^6.10.1",
27
+ "@codemirror/commands": "^6.10.2",
28
28
  "@codemirror/lang-markdown": "^6.5.0",
29
29
  "@codemirror/lang-python": "^6.2.1",
30
30
  "@codemirror/lang-sql": "^6.10.0",
31
31
  "@codemirror/language": "^6.12.1",
32
32
  "@codemirror/language-data": "^6.5.2",
33
33
  "@codemirror/legacy-modes": "^6.5.2",
34
- "@codemirror/lint": "^6.9.2",
34
+ "@codemirror/lint": "^6.9.3",
35
35
  "@codemirror/merge": "^6.11.2",
36
- "@codemirror/search": "^6.5.11",
37
- "@codemirror/state": "^6.5.3",
36
+ "@codemirror/search": "^6.6.0",
37
+ "@codemirror/state": "^6.5.4",
38
38
  "@codemirror/theme-one-dark": "^6.1.3",
39
- "@codemirror/view": "^6.39.8",
39
+ "@codemirror/view": "^6.39.12",
40
40
  "@dagrejs/dagre": "^1.1.8",
41
41
  "@date-fns/tz": "^1.4.1",
42
42
  "@dnd-kit/core": "^6.3.1",
@@ -49,13 +49,13 @@
49
49
  "@hookform/resolvers": "^5.2.2",
50
50
  "@img-comparison-slider/react": "^8.0.2",
51
51
  "@internationalized/date": "^3.10.1",
52
- "@lezer/common": "^1.5.0",
52
+ "@lezer/common": "^1.5.1",
53
53
  "@lezer/highlight": "^1.2.3",
54
- "@lezer/lr": "^1.4.5",
55
- "@lezer/markdown": "^1.6.2",
54
+ "@lezer/lr": "^1.4.8",
55
+ "@lezer/markdown": "^1.6.3",
56
56
  "@lezer/python": "^1.1.18",
57
57
  "@marimo-team/codemirror-ai": "^0.3.5",
58
- "@marimo-team/codemirror-languageserver": "^1.16.11",
58
+ "@marimo-team/codemirror-languageserver": "^1.16.12",
59
59
  "@marimo-team/codemirror-mcp": "^0.1.5",
60
60
  "@marimo-team/codemirror-sql": "^0.2.4",
61
61
  "@marimo-team/llm-info": "workspace:*",
@@ -144,7 +144,7 @@
144
144
  "mermaid": "^11.12.2",
145
145
  "partysocket": "1.1.10",
146
146
  "path-to-regexp": "^8.3.0",
147
- "plotly.js": "^2.35.3",
147
+ "plotly.js": "^3.3.1",
148
148
  "pyodide": "0.27.7",
149
149
  "react-arborist": "^3.4.3",
150
150
  "react-aria": "3.44.0",
@@ -30,6 +30,7 @@ interface TableActionsProps<TData> {
30
30
  onRowSelectionChange?: (value: RowSelectionState) => void;
31
31
  table: Table<TData>;
32
32
  downloadAs?: DownloadActionProps["downloadAs"];
33
+ downloadFileName?: string;
33
34
  getRowIds?: GetRowIds;
34
35
  toggleDisplayHeader?: () => void;
35
36
  showChartBuilder?: boolean;
@@ -52,6 +53,7 @@ export const TableActions = <TData,>({
52
53
  onRowSelectionChange,
53
54
  table,
54
55
  downloadAs,
56
+ downloadFileName,
55
57
  getRowIds,
56
58
  toggleDisplayHeader,
57
59
  showChartBuilder,
@@ -182,7 +184,12 @@ export const TableActions = <TData,>({
182
184
  />
183
185
  )}
184
186
  <div className="ml-auto">
185
- {downloadAs && <DownloadAs downloadAs={downloadAs} />}
187
+ {downloadAs && (
188
+ <DownloadAs
189
+ downloadAs={downloadAs}
190
+ downloadFileName={downloadFileName}
191
+ />
192
+ )}
186
193
  </div>
187
194
  </div>
188
195
  );
@@ -116,6 +116,7 @@ const DataTableInternal = <TData,>({
116
116
  paginationState,
117
117
  setPaginationState,
118
118
  downloadAs,
119
+ downloadFileName,
119
120
  manualPagination = false,
120
121
  pagination = false,
121
122
  onRowSelectionChange,
@@ -334,6 +335,7 @@ const DataTableInternal = <TData,>({
334
335
  onRowSelectionChange={onRowSelectionChange}
335
336
  table={table}
336
337
  downloadAs={downloadAs}
338
+ downloadFileName={downloadFileName}
337
339
  getRowIds={getRowIds}
338
340
  toggleDisplayHeader={toggleDisplayHeader}
339
341
  showChartBuilder={showChartBuilder}
@@ -14,6 +14,7 @@ import { logNever } from "@/utils/assertNever";
14
14
  import { copyToClipboard } from "@/utils/copy";
15
15
  import { downloadByURL } from "@/utils/download";
16
16
  import { prettyError } from "@/utils/errors";
17
+ import { Filenames } from "@/utils/filenames";
17
18
  import {
18
19
  jsonParseWithSpecialChar,
19
20
  jsonToMarkdown,
@@ -36,6 +37,7 @@ type DownloadFormat = "csv" | "json" | "parquet";
36
37
 
37
38
  export interface DownloadActionProps {
38
39
  downloadAs: (req: { format: DownloadFormat }) => Promise<string>;
40
+ downloadFileName?: string;
39
41
  }
40
42
 
41
43
  const options = [
@@ -158,7 +160,10 @@ export const DownloadAs: React.FC<DownloadActionProps> = (props) => {
158
160
  onSelect={async () => {
159
161
  const downloadUrl = await getDownloadUrl(option.format);
160
162
  const ext = option.format;
161
- downloadByURL(downloadUrl, `download.${ext}`);
163
+ const rawName = (props.downloadFileName ?? "").trim();
164
+ const baseName =
165
+ Filenames.withoutExtension(rawName) || "download";
166
+ downloadByURL(downloadUrl, `${baseName}.${ext}`);
162
167
  }}
163
168
  >
164
169
  <option.icon className="mo-dropdown-icon" />
@@ -58,6 +58,7 @@ export const DependencyGraphTree: React.FC<PropsWithChildren<Props>> = ({
58
58
  cellAtoms,
59
59
  variables,
60
60
  settings.hidePureMarkdown,
61
+ settings.hideReusableFunctions,
61
62
  );
62
63
  elements = layoutElements({
63
64
  nodes: elements.nodes,
@@ -94,9 +95,17 @@ export const DependencyGraphTree: React.FC<PropsWithChildren<Props>> = ({
94
95
  cellAtoms,
95
96
  variables,
96
97
  settings.hidePureMarkdown,
98
+ settings.hideReusableFunctions,
97
99
  ),
98
100
  );
99
- }, [cellIds, variables, cellAtoms, syncChanges, settings.hidePureMarkdown]);
101
+ }, [
102
+ cellIds,
103
+ variables,
104
+ cellAtoms,
105
+ syncChanges,
106
+ settings.hidePureMarkdown,
107
+ settings.hideReusableFunctions,
108
+ ]);
100
109
 
101
110
  const [selection, setSelection] = useState<GraphSelection>();
102
111
  useFitToViewOnDimensionChange();
@@ -24,6 +24,7 @@ interface Props {
24
24
  const graphViewAtom = atom<LayoutDirection>("TB");
25
25
  const graphViewSettings = atom<GraphSettings>({
26
26
  hidePureMarkdown: true,
27
+ hideReusableFunctions: false,
27
28
  });
28
29
 
29
30
  export const DependencyGraph: React.FC<Props> = (props) => {
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { Atom } from "jotai";
4
4
  import { type Edge, MarkerType, type Node, type NodeProps } from "reactflow";
5
+ import { getNotebook } from "@/core/cells/cells";
5
6
  import type { CellId } from "@/core/cells/ids";
6
7
  import type { CellData } from "@/core/cells/types";
7
8
  import { store } from "@/core/state/jotai";
@@ -29,6 +30,7 @@ interface ElementsBuilder {
29
30
  cellAtoms: Atom<CellData>[],
30
31
  variables: Variables,
31
32
  hidePureMarkdown: boolean,
33
+ hideReusableFunctions: boolean,
32
34
  ) => { nodes: Node<NodeData>[]; edges: Edge[] };
33
35
  }
34
36
 
@@ -76,6 +78,7 @@ export class VerticalElementsBuilder implements ElementsBuilder {
76
78
  cellAtoms: Atom<CellData>[],
77
79
  variables: Variables,
78
80
  _hidePureMarkdown: boolean,
81
+ _hideReusableFunctions: boolean,
79
82
  ) {
80
83
  let prevY = 0;
81
84
  const nodes: Node<NodeData>[] = [];
@@ -143,6 +146,7 @@ export class TreeElementsBuilder implements ElementsBuilder {
143
146
  cellAtoms: Atom<CellData>[],
144
147
  variables: Variables,
145
148
  hidePureMarkdown: boolean,
149
+ hideReusableFunctions: boolean,
146
150
  ) {
147
151
  const nodes: Node<NodeData>[] = [];
148
152
  const edges: Edge[] = [];
@@ -171,18 +175,25 @@ export class TreeElementsBuilder implements ElementsBuilder {
171
175
  }
172
176
  }
173
177
 
174
- for (const [cellId, cellAtom] of Arrays.zip(cellIds, cellAtoms)) {
175
- // Show every cell
176
- if (!hidePureMarkdown) {
177
- nodes.push(this.createNode(cellId, cellAtom));
178
- }
178
+ const cellRuntime = getNotebook().cellRuntime;
179
179
 
180
+ for (const [cellId, cellAtom] of Arrays.zip(cellIds, cellAtoms)) {
181
+ const code = store.get(cellAtom).code.trim();
180
182
  const hasEdge = nodesWithEdges.has(cellId);
181
- const isMarkdown = store.get(cellAtom).code.trim().startsWith("mo.md");
182
- // Show only cells with edges or non-markdown cells
183
- if (hasEdge || !isMarkdown) {
184
- nodes.push(this.createNode(cellId, cellAtom));
183
+ const isMarkdown = code.startsWith("mo.md");
184
+ const runtime = cellRuntime[cellId];
185
+ const isReusable = runtime?.serialization?.toLowerCase() === "valid";
186
+
187
+ // Apply filters
188
+ if (hidePureMarkdown && isMarkdown && !hasEdge) {
189
+ continue;
190
+ }
191
+ if (hideReusableFunctions && isReusable && !hasEdge) {
192
+ continue;
185
193
  }
194
+
195
+ // Show every cell that wasn't filtered out
196
+ nodes.push(this.createNode(cellId, cellAtom));
186
197
  }
187
198
 
188
199
  return { nodes, edges };
@@ -43,7 +43,8 @@ export const GraphToolbar: React.FC<Props> = memo(
43
43
  onSettingsChange({ ...settings, [key]: value });
44
44
  };
45
45
 
46
- const checkboxId = useId();
46
+ const markdownCheckboxId = useId();
47
+ const functionsCheckboxId = useId();
47
48
 
48
49
  const settingsButton = (
49
50
  <Popover>
@@ -54,16 +55,31 @@ export const GraphToolbar: React.FC<Props> = memo(
54
55
  </PopoverTrigger>
55
56
  <PopoverContent className="w-auto p-2 text-muted-foreground">
56
57
  <div className="font-semibold pb-4">Settings</div>
57
- <div className="flex items-center gap-2">
58
- <Checkbox
59
- data-testid="hide-pure-markdown-checkbox"
60
- id={checkboxId}
61
- checked={settings.hidePureMarkdown}
62
- onCheckedChange={(checked) =>
63
- handleSettingChange("hidePureMarkdown", Boolean(checked))
64
- }
65
- />
66
- <Label htmlFor={checkboxId}>Hide pure markdown</Label>
58
+ <div className="flex flex-col gap-2">
59
+ <div className="flex items-center gap-2">
60
+ <Checkbox
61
+ data-testid="hide-pure-markdown-checkbox"
62
+ id={markdownCheckboxId}
63
+ checked={settings.hidePureMarkdown}
64
+ onCheckedChange={(checked) =>
65
+ handleSettingChange("hidePureMarkdown", Boolean(checked))
66
+ }
67
+ />
68
+ <Label htmlFor={markdownCheckboxId}>Hide pure markdown</Label>
69
+ </div>
70
+ <div className="flex items-center gap-2">
71
+ <Checkbox
72
+ data-testid="hide-reusable-functions-checkbox"
73
+ id={functionsCheckboxId}
74
+ checked={settings.hideReusableFunctions}
75
+ onCheckedChange={(checked) =>
76
+ handleSettingChange("hideReusableFunctions", Boolean(checked))
77
+ }
78
+ />
79
+ <Label htmlFor={functionsCheckboxId}>
80
+ Hide reusable functions
81
+ </Label>
82
+ </div>
67
83
  </div>
68
84
  </PopoverContent>
69
85
  </Popover>
@@ -17,4 +17,5 @@ export type GraphSelection =
17
17
 
18
18
  export interface GraphSettings {
19
19
  hidePureMarkdown: boolean;
20
+ hideReusableFunctions: boolean;
20
21
  }
@@ -226,7 +226,9 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
226
226
 
227
227
  const helperResizeHandle = (
228
228
  <PanelResizeHandle
229
+ disabled={!isSidebarOpen}
229
230
  onDragging={handleDragging}
231
+ hitAreaMargins={{ coarse: 15, fine: 2 }}
230
232
  className={cn(
231
233
  "border-border print:hidden z-10",
232
234
  isSidebarOpen ? "resize-handle" : "resize-handle-collapsed",
@@ -237,6 +239,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
237
239
 
238
240
  const panelResizeHandle = (
239
241
  <PanelResizeHandle
242
+ disabled={!isDeveloperPanelOpen}
240
243
  onDragging={handleDragging}
241
244
  className={cn(
242
245
  "border-border print:hidden z-20",
@@ -96,10 +96,10 @@ export const PackageAlert: React.FC = () => {
96
96
 
97
97
  if (isMissingPackageAlert(packageAlert)) {
98
98
  return (
99
- <div className="flex flex-col gap-4 mb-5 fixed top-5 left-12 min-w-[400px] z-200 opacity-95 max-w-[600px]">
99
+ <div className="flex flex-col gap-4 mb-5 fixed top-5 left-12 min-w-[400px] z-200 opacity-95 max-w-[600px] pointer-events-none">
100
100
  <Banner
101
101
  kind="danger"
102
- className="flex flex-col rounded py-3 px-5 animate-in slide-in-from-left overflow-auto max-h-[80vh] scrollbar-thin"
102
+ className="flex flex-col rounded py-3 px-5 animate-in slide-in-from-left overflow-auto max-h-[80vh] scrollbar-thin pointer-events-auto"
103
103
  >
104
104
  <div className="flex justify-between">
105
105
  <span className="font-bold text-lg flex items-center mb-2">
@@ -207,10 +207,10 @@ export const PackageAlert: React.FC = () => {
207
207
  }
208
208
 
209
209
  return (
210
- <div className="flex flex-col gap-4 mb-5 fixed top-5 left-12 min-w-[400px] z-200 opacity-95 max-w-[600px] ">
210
+ <div className="flex flex-col gap-4 mb-5 fixed top-5 left-12 min-w-[400px] z-200 opacity-95 max-w-[600px] pointer-events-none">
211
211
  <Banner
212
212
  kind={status === "failed" ? "danger" : "info"}
213
- className="flex flex-col rounded pt-3 pb-4 px-5 overflow-auto max-h-[80vh] scrollbar-thin"
213
+ className="flex flex-col rounded pt-3 pb-4 px-5 overflow-auto max-h-[80vh] scrollbar-thin pointer-events-auto"
214
214
  >
215
215
  <div className="flex justify-between">
216
216
  <span className="font-bold text-lg flex items-center mb-2">
@@ -172,11 +172,9 @@ const VerticalLayoutRenderer: React.FC<VerticalLayoutProps> = ({
172
172
  // spacing is handled elsewhere
173
173
  return (
174
174
  <VerticalLayoutWrapper invisible={invisible} appConfig={appConfig}>
175
- {showCode && canShowCode ? (
176
- <div className="flex flex-col gap-5"> {renderCells()}</div>
177
- ) : (
178
- renderCells()
179
- )}
175
+ <div className={cn("flex flex-col", showCode && canShowCode && "gap-5")}>
176
+ {renderCells()}
177
+ </div>
180
178
  {mode === "read" && (
181
179
  <ActionButtons
182
180
  canShowCode={canShowCode}
@@ -140,6 +140,24 @@ def _(
140
140
  expect(extractCells(input)).toEqual(["x = a + b + c"]);
141
141
  });
142
142
 
143
+ it("preserves return statements inside nested functions", () => {
144
+ const input = `
145
+ @app.cell
146
+ def _(mo, px):
147
+ def make_fig():
148
+ data = {'category': ['foo', 'bar'], 'value': [10, 20]}
149
+ fig = px.bar(data, x='category', y='value')
150
+ return fig
151
+
152
+ fig = make_fig()
153
+ mo.ui.plotly(fig)
154
+ return
155
+ `;
156
+ expect(extractCells(input)).toEqual([
157
+ "def make_fig():\n data = {'category': ['foo', 'bar'], 'value': [10, 20]}\n fig = px.bar(data, x='category', y='value')\n return fig\n\nfig = make_fig()\nmo.ui.plotly(fig)",
158
+ ]);
159
+ });
160
+
143
161
  it("handles cells with config", () => {
144
162
  const input = `
145
163
  @app.cell(hide_code=True, column=2)
@@ -42,6 +42,7 @@ export function extractCells(text: string): string[] {
42
42
  let inMultilineArgs = false;
43
43
  let inMultilineReturn = false;
44
44
  let parenCount = 0;
45
+ let cellBaseIndent: number | null = null;
45
46
 
46
47
  // Pre-compile regex patterns
47
48
  const leadingParenRegex = /\(/g;
@@ -55,19 +56,16 @@ export function extractCells(text: string): string[] {
55
56
  );
56
57
  }
57
58
 
59
+ function getIndent(line: string): number {
60
+ const match = line.match(/^\s*/);
61
+ return match ? match[0].length : 0;
62
+ }
63
+
58
64
  function finalizeCellIfNeeded() {
59
65
  if (currentCell.length === 0) {
60
66
  return;
61
67
  }
62
68
 
63
- // Remove trailing returns
64
- while (
65
- currentCell.length > 0 &&
66
- currentCell[currentCell.length - 1].trim().startsWith("return")
67
- ) {
68
- currentCell.pop();
69
- }
70
-
71
69
  // Only add non-empty cells
72
70
  if (currentCell.some((l) => l.trim() !== "")) {
73
71
  cells.push(dedent(currentCell.join("\n")));
@@ -88,6 +86,7 @@ export function extractCells(text: string): string[] {
88
86
  finalizeCellIfNeeded();
89
87
  inCell = true;
90
88
  skipLines = 1; // Skip the def line
89
+ cellBaseIndent = null;
91
90
  continue;
92
91
  }
93
92
 
@@ -125,8 +124,13 @@ export function extractCells(text: string): string[] {
125
124
  continue;
126
125
  }
127
126
 
128
- // Handle return statements
129
- if (trimmed.startsWith("return")) {
127
+ // Detect base indentation of cell body from first content line
128
+ if (cellBaseIndent === null && trimmed) {
129
+ cellBaseIndent = getIndent(line);
130
+ }
131
+
132
+ // Handle return statements — only strip cell-level returns
133
+ if (trimmed.startsWith("return") && getIndent(line) === cellBaseIndent) {
130
134
  if (trimmed.includes("(") && !trimmed.endsWith(")")) {
131
135
  inMultilineReturn = true;
132
136
  parenCount = countParens(trimmed);
@@ -13,6 +13,7 @@ export const DATA_TYPES = [
13
13
  "time",
14
14
  "unknown",
15
15
  ] as const;
16
+ export type ModelLifecycle = NotificationMessageData<"model-lifecycle">;
16
17
  export type Banner = NotificationMessageData<"banner">;
17
18
  export type AiInlineCompletionRequest = schemas["AiInlineCompletionRequest"];
18
19
  export type DataTableColumn = schemas["DataTableColumn"];
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { invariant } from "@/utils/invariant";
3
+ import type { ModelLifecycle } from "../kernel/messages";
3
4
  import type { MarimoStaticState, StaticVirtualFiles } from "./types";
4
5
 
5
6
  declare global {
@@ -17,3 +18,7 @@ export function getStaticVirtualFiles(): StaticVirtualFiles {
17
18
 
18
19
  return window.__MARIMO_STATIC__.files;
19
20
  }
21
+
22
+ export function getStaticModelNotifications(): ModelLifecycle[] | undefined {
23
+ return window?.__MARIMO_STATIC__?.modelNotifications;
24
+ }
@@ -1,8 +1,10 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import type { DataURLString } from "@/utils/json/base64";
3
+ import type { ModelLifecycle } from "../kernel/messages";
3
4
 
4
5
  export type StaticVirtualFiles = Record<string, DataURLString>;
5
6
 
6
7
  export interface MarimoStaticState {
7
8
  files: StaticVirtualFiles;
9
+ modelNotifications?: ModelLifecycle[];
8
10
  }
@@ -1,9 +1,13 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { describe, expect, it, vi } from "vitest";
3
+ import { codeAtom } from "../../saving/file-state";
4
+ import { store } from "../../state/jotai";
3
5
  import {
4
6
  CompositeFileStore,
5
7
  domElementFileStore,
6
8
  localStorageFileStore,
9
+ mountConfigFileStore,
10
+ notebookFileStore,
7
11
  } from "../store";
8
12
 
9
13
  describe("localStorageFileStore", () => {
@@ -53,3 +57,32 @@ describe("CompositeFileStore", () => {
53
57
  expect(saved).toHaveBeenCalledTimes(3);
54
58
  });
55
59
  });
60
+
61
+ describe("mountConfigFileStore", () => {
62
+ it("returns null when no code is set", () => {
63
+ store.set(codeAtom, undefined);
64
+ expect(mountConfigFileStore.readFile()).toBeNull();
65
+ });
66
+
67
+ it("returns code when set", () => {
68
+ store.set(codeAtom, "print('hello')");
69
+ expect(mountConfigFileStore.readFile()).toBe("print('hello')");
70
+ });
71
+
72
+ it("does not save files", () => {
73
+ store.set(codeAtom, "original");
74
+ mountConfigFileStore.saveFile("new content");
75
+ expect(mountConfigFileStore.readFile()).toBe("original");
76
+ });
77
+ });
78
+
79
+ describe("notebookFileStore priority", () => {
80
+ it("prefers mount config over marimo-code and URL", () => {
81
+ store.set(codeAtom, "mount config code");
82
+ const element = document.createElement("marimo-code");
83
+ element.textContent = "marimo-code element";
84
+ document.body.replaceChildren(element);
85
+
86
+ expect(notebookFileStore.readFile()).toBe("mount config code");
87
+ });
88
+ });
@@ -29,6 +29,7 @@ import type {
29
29
  SaveUserConfigurationRequest,
30
30
  Snippets,
31
31
  } from "../network/types";
32
+ import { filenameAtom } from "../saving/file-state";
32
33
  import { store } from "../state/jotai";
33
34
  import { BasicTransport } from "../websocket/transports/basic";
34
35
  import type { IConnectionTransport } from "../websocket/transports/transport";
@@ -146,7 +147,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
146
147
 
147
148
  const code = await notebookFileStore.readFile();
148
149
  const fallbackCode = await fallbackFileStore.readFile();
149
- const filename = PyodideRouter.getFilename();
150
+ const filename = store.get(filenameAtom) ?? PyodideRouter.getFilename();
150
151
  const userConfig = store.get(userConfigAtom);
151
152
 
152
153
  const queryParameters: Record<string, string | string[]> = {};
@@ -4,6 +4,8 @@ import {
4
4
  compressToEncodedURIComponent,
5
5
  decompressFromEncodedURIComponent,
6
6
  } from "lz-string";
7
+ import { codeAtom } from "@/core/saving/file-state";
8
+ import { store } from "@/core/state/jotai";
7
9
  import { TypedLocalStorage } from "@/utils/storage/typed";
8
10
  import { PyodideRouter } from "./router";
9
11
 
@@ -67,6 +69,15 @@ const remoteDefaultFileStore: FileStore = {
67
69
  },
68
70
  };
69
71
 
72
+ export const mountConfigFileStore: FileStore = {
73
+ saveFile(_contents: string) {
74
+ // Read-only: mount config code is set via codeAtom
75
+ },
76
+ readFile() {
77
+ return store.get(codeAtom) ?? null;
78
+ },
79
+ };
80
+
70
81
  const emptyFileStore: FileStore = {
71
82
  saveFile(contents: string) {
72
83
  // Do nothing
@@ -112,7 +123,8 @@ export class CompositeFileStore implements FileStore {
112
123
  }
113
124
 
114
125
  export const notebookFileStore = new CompositeFileStore([
115
- // Prefer <marimo-code>, then URL
126
+ // Prefer mount config, then <marimo-code>, then URL
127
+ mountConfigFileStore,
116
128
  domElementFileStore,
117
129
  urlFileStore,
118
130
  ]);
package/src/mount.tsx CHANGED
@@ -39,11 +39,18 @@ import {
39
39
  import { codeAtom, filenameAtom } from "./core/saving/file-state";
40
40
  import { store } from "./core/state/jotai";
41
41
  import { patchFetch, patchVegaLoader } from "./core/static/files";
42
- import { isStaticNotebook } from "./core/static/static-state";
42
+ import {
43
+ getStaticModelNotifications,
44
+ isStaticNotebook,
45
+ } from "./core/static/static-state";
43
46
  import { maybeRegisterVSCodeBindings } from "./core/vscode/vscode-bindings";
44
47
  import type { FileStore } from "./core/wasm/store";
45
48
  import { notebookFileStore } from "./core/wasm/store";
46
49
  import { WebSocketState } from "./core/websocket/types";
50
+ import {
51
+ handleWidgetMessage,
52
+ MODEL_MANAGER,
53
+ } from "./plugins/impl/anywidget/model";
47
54
  import { vegaLoader } from "./plugins/impl/vega/loader";
48
55
  import { initializePlugins } from "./plugins/plugins";
49
56
  import { ThemeProvider } from "./theme/ThemeProvider";
@@ -77,6 +84,7 @@ export function mount(options: unknown, el: Element): Error | undefined {
77
84
  // If we're in static mode, we need to patch fetch to use the virtual file
78
85
  patchFetch();
79
86
  patchVegaLoader(vegaLoader);
87
+ hydrateStaticModels();
80
88
  }
81
89
 
82
90
  // Init store
@@ -330,6 +338,20 @@ function initStore(options: unknown) {
330
338
  }
331
339
  }
332
340
 
341
+ /**
342
+ * Hydrate anywidget models from embedded static state so widgets
343
+ * render immediately without a kernel connection.
344
+ */
345
+ function hydrateStaticModels(): void {
346
+ const notifications = getStaticModelNotifications();
347
+ if (!notifications) {
348
+ return;
349
+ }
350
+ for (const notification of notifications) {
351
+ handleWidgetMessage(MODEL_MANAGER, notification);
352
+ }
353
+ }
354
+
333
355
  export const visibleForTesting = {
334
356
  reset: () => {
335
357
  hasMounted = false;
@@ -195,6 +195,7 @@ interface Data<T> {
195
195
  hasStableRowId: boolean;
196
196
  lazy: boolean;
197
197
  cellHoverTexts?: Record<string, Record<string, string | null>> | null;
198
+ downloadFileName?: string;
198
199
  }
199
200
 
200
201
  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
@@ -278,6 +279,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
278
279
  // If lazy, this will preload the first page of data
279
280
  // without user confirmation.
280
281
  preload: z.boolean().default(false),
282
+ downloadFileName: z.string().optional(),
281
283
  }),
282
284
  )
283
285
  .withFunctions<DataTableFunctions>({
@@ -752,6 +754,7 @@ const DataTableComponent = ({
752
754
  cellStyles,
753
755
  hoverTemplate,
754
756
  cellHoverTexts,
757
+ downloadFileName,
755
758
  toggleDisplayHeader,
756
759
  calculate_top_k_rows,
757
760
  preview_column,
@@ -981,6 +984,7 @@ const DataTableComponent = ({
981
984
  hoverTemplate={hoverTemplate}
982
985
  cellHoverTexts={cellHoverTexts}
983
986
  downloadAs={showDownload ? downloadAs : undefined}
987
+ downloadFileName={downloadFileName}
984
988
  enableSearch={enableSearch}
985
989
  searchQuery={searchQuery}
986
990
  onSearchQueryChange={setSearchQuery}