@marimo-team/islands 0.23.16-dev9 → 0.23.17-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 (164) hide show
  1. package/dist/{chat-ui-JeekqoMH.js → chat-ui-DsZJj75A.js} +3104 -3107
  2. package/dist/{code-visibility-DveLKNtj.js → common-CotPLtrU.js} +1068 -1036
  3. package/dist/{formats-CO8cn5_K.js → formats-Dwq1THhr.js} +1 -1
  4. package/dist/{html-to-image-Bif4utQZ.js → html-to-image-CZ1kLKkq.js} +7336 -6342
  5. package/dist/main.js +211 -147
  6. package/dist/{node-sql-parser-DLl_zNw0.js → node-sql-parser-v1Vi-Fft.js} +16867 -16665
  7. package/dist/{process-output-TAzy-GUk.js → process-output-bVfbcx5_.js} +30 -26
  8. package/dist/{reveal-component-DxV4Ppa2.js → reveal-component-IoHipy82.js} +634 -626
  9. package/dist/style.css +1 -1
  10. package/dist/toDate-CqhBWjvO.js +713 -0
  11. package/dist/{vega-component-DphdxaJ_.js → vega-component-C7nO5zcf.js} +2 -2
  12. package/package.json +3 -4
  13. package/src/__mocks__/requests.ts +56 -4
  14. package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
  15. package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
  16. package/src/components/data-table/__tests__/columns.test.tsx +9 -1
  17. package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
  18. package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
  19. package/src/components/data-table/column-summary/utils.ts +26 -7
  20. package/src/components/data-table/columns.tsx +11 -1
  21. package/src/components/data-table/types.ts +1 -1
  22. package/src/components/editor/__tests__/viewer-banner.test.tsx +14 -0
  23. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
  24. package/src/components/editor/actions/__tests__/pdf-export.test.ts +32 -13
  25. package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
  26. package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
  27. package/src/components/editor/actions/pdf-export.ts +11 -14
  28. package/src/components/editor/actions/useNotebookActions.tsx +28 -19
  29. package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
  30. package/src/components/editor/cell/code/cell-editor.tsx +13 -7
  31. package/src/components/editor/chrome/__tests__/state.test.ts +7 -1
  32. package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
  33. package/src/components/editor/chrome/panels/__tests__/panel-accordion-state.test.ts +42 -0
  34. package/src/components/editor/chrome/panels/file-explorer-panel.tsx +6 -17
  35. package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
  36. package/src/components/editor/chrome/panels/panel-accordion-state.ts +51 -0
  37. package/src/components/editor/chrome/panels/session-panel.tsx +5 -16
  38. package/src/components/editor/chrome/panels/write-secret-modal.tsx +40 -12
  39. package/src/components/editor/chrome/state.ts +10 -0
  40. package/src/components/editor/connections/__tests__/json-credentials.test.ts +114 -0
  41. package/src/components/editor/connections/__tests__/paths.test.ts +39 -0
  42. package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +113 -0
  43. package/src/components/editor/connections/add-connection-dialog.tsx +2 -0
  44. package/src/components/editor/connections/components.tsx +6 -2
  45. package/src/components/editor/connections/database/__tests__/__snapshots__/as-code.test.ts.snap +27 -5
  46. package/src/components/editor/connections/database/__tests__/as-code.test.ts +33 -0
  47. package/src/components/editor/connections/database/as-code.ts +37 -13
  48. package/src/components/editor/connections/database/schemas.ts +6 -5
  49. package/src/components/editor/connections/form-renderers.tsx +99 -1
  50. package/src/components/editor/connections/json-credentials.ts +57 -0
  51. package/src/components/editor/connections/paths.ts +21 -0
  52. package/src/components/editor/connections/quick-add-data-sources.tsx +106 -0
  53. package/src/components/editor/connections/secret-combobox.tsx +41 -20
  54. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +88 -2
  55. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +128 -8
  56. package/src/components/editor/connections/storage/as-code.ts +79 -36
  57. package/src/components/editor/connections/storage/schemas.ts +85 -44
  58. package/src/components/editor/file-tree/__tests__/renderers.test.ts +31 -0
  59. package/src/components/editor/file-tree/file-viewer.tsx +11 -7
  60. package/src/components/editor/file-tree/renderers.tsx +21 -0
  61. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +2 -2
  62. package/src/components/editor/viewer-banner.tsx +6 -1
  63. package/src/components/forms/form.tsx +59 -24
  64. package/src/components/forms/options.ts +2 -1
  65. package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
  66. package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
  67. package/src/components/slides/reveal-component.tsx +22 -17
  68. package/src/components/slides/slide-cell-view.tsx +1 -1
  69. package/src/components/slides/slides.css +4 -2
  70. package/src/components/static-html/share-modal.tsx +1 -1
  71. package/src/core/ai/context/providers/file.ts +8 -29
  72. package/src/core/cells/__tests__/cells.test.ts +12 -2
  73. package/src/core/cells/__tests__/logs.test.ts +5 -5
  74. package/src/core/cells/cells.ts +17 -4
  75. package/src/core/cells/document-changes.ts +1 -0
  76. package/src/core/cells/logs.ts +14 -4
  77. package/src/core/codemirror/cm.ts +3 -0
  78. package/src/core/codemirror/code-lens/__tests__/actions.test.ts +68 -0
  79. package/src/core/codemirror/code-lens/__tests__/analyzer.test.ts +224 -0
  80. package/src/core/codemirror/code-lens/__tests__/entities.test.ts +175 -0
  81. package/src/core/codemirror/code-lens/__tests__/extension.test.ts +325 -0
  82. package/src/core/codemirror/code-lens/__tests__/perf.test.ts +121 -0
  83. package/src/core/codemirror/code-lens/__tests__/popover.test.ts +93 -0
  84. package/src/core/codemirror/code-lens/actions.ts +33 -0
  85. package/src/core/codemirror/code-lens/analyzer.ts +284 -0
  86. package/src/core/codemirror/code-lens/entities.ts +123 -0
  87. package/src/core/codemirror/code-lens/extension.ts +298 -0
  88. package/src/core/codemirror/code-lens/icons.ts +36 -0
  89. package/src/core/codemirror/code-lens/popover.tsx +240 -0
  90. package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
  91. package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
  92. package/src/core/codemirror/copilot/language-server.ts +182 -70
  93. package/src/core/codemirror/copilot/transport.ts +15 -207
  94. package/src/core/codemirror/copilot/types.ts +9 -2
  95. package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
  96. package/src/core/codemirror/find-replace/navigate.ts +2 -0
  97. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
  98. package/src/core/codemirror/go-to-definition/commands.ts +93 -90
  99. package/src/core/codemirror/language/__tests__/sql.test.ts +627 -61
  100. package/src/core/codemirror/language/languages/python.ts +23 -11
  101. package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
  102. package/src/core/codemirror/language/languages/sql/renderers.tsx +3 -3
  103. package/src/core/codemirror/language/languages/sql/sql.ts +251 -16
  104. package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
  105. package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
  106. package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
  107. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
  108. package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
  109. package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
  110. package/src/core/codemirror/lsp/log-messages.ts +37 -0
  111. package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
  112. package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
  113. package/src/core/codemirror/lsp/transports.ts +8 -1
  114. package/src/core/codemirror/lsp/types.ts +68 -8
  115. package/src/core/codemirror/lsp/utils.ts +14 -0
  116. package/src/core/codemirror/python-node-names.ts +90 -0
  117. package/src/core/codemirror/reactive-references/analyzer.ts +52 -57
  118. package/src/core/codemirror/utils.ts +28 -0
  119. package/src/core/config/feature-flag.tsx +3 -1
  120. package/src/core/datasets/data-source-discovery.ts +5 -0
  121. package/src/core/datasets/request-registry.ts +11 -0
  122. package/src/core/edit-app.tsx +1 -2
  123. package/src/core/export/__tests__/hooks.test.ts +2 -4
  124. package/src/core/export/hooks.ts +4 -3
  125. package/src/core/islands/__tests__/bridge.test.ts +107 -0
  126. package/src/core/islands/bootstrap.ts +1 -0
  127. package/src/core/islands/bridge.ts +18 -0
  128. package/src/core/kernel/messages.ts +2 -0
  129. package/src/core/lsp/__tests__/transport.test.ts +509 -337
  130. package/src/core/lsp/transport.ts +304 -132
  131. package/src/core/mode.ts +10 -0
  132. package/src/core/network/__tests__/api.test.ts +42 -0
  133. package/src/core/network/__tests__/connection.test.ts +45 -0
  134. package/src/core/network/__tests__/requests-lazy.test.ts +47 -1
  135. package/src/core/network/__tests__/requests-network.test.ts +48 -0
  136. package/src/core/network/api.ts +27 -0
  137. package/src/core/network/connection.ts +15 -0
  138. package/src/core/network/requests-lazy.ts +3 -0
  139. package/src/core/network/requests-network.ts +31 -8
  140. package/src/core/network/requests-static.ts +3 -0
  141. package/src/core/network/requests-toasting.tsx +3 -0
  142. package/src/core/network/types.ts +27 -6
  143. package/src/core/rtc/state.ts +9 -0
  144. package/src/core/static/download-html.ts +5 -14
  145. package/src/core/wasm/__tests__/bridge.test.ts +99 -5
  146. package/src/core/wasm/bridge.ts +42 -9
  147. package/src/core/wasm/worker/types.ts +10 -3
  148. package/src/core/wasm/worker/worker.ts +7 -8
  149. package/src/core/websocket/useMarimoKernelConnection.tsx +4 -0
  150. package/src/css/app/codemirror.css +16 -2
  151. package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +80 -0
  152. package/src/hooks/useDataSourceDiscovery.ts +18 -0
  153. package/src/plugins/impl/DataTablePlugin.tsx +4 -1
  154. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +158 -0
  155. package/src/plugins/impl/anywidget/__tests__/model.test.ts +55 -0
  156. package/src/plugins/impl/anywidget/model.ts +20 -5
  157. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +33 -21
  158. package/src/plugins/impl/data-frames/forms/renderers.tsx +11 -8
  159. package/src/stories/log-viewer.stories.tsx +12 -6
  160. package/src/utils/__tests__/blob.test.ts +27 -1
  161. package/src/utils/__tests__/download.test.tsx +44 -4
  162. package/src/utils/blob.ts +24 -1
  163. package/src/utils/download.ts +14 -15
  164. package/dist/toDate-CmR_xR3P.js +0 -692
@@ -74,17 +74,24 @@ export const ZodForm = <T extends FieldValues>({
74
74
  return (
75
75
  <FormProvider {...form}>
76
76
  {children}
77
- {renderZodSchema(schema, form, path, renderers)}
77
+ {renderZodSchema({ schema, form, path, renderers })}
78
78
  </FormProvider>
79
79
  );
80
80
  };
81
81
 
82
- export function renderZodSchema<T extends FieldValues, S>(
83
- schema: z.ZodType<S>,
84
- form: UseFormReturn<T>,
85
- path: Path<T>,
86
- renderers: FormRenderer<T>[],
87
- ) {
82
+ export interface RenderZodSchemaOptions<T extends FieldValues, S> {
83
+ schema: z.ZodType<S>;
84
+ form: UseFormReturn<T>;
85
+ path: Path<T>;
86
+ renderers: FormRenderer<T>[];
87
+ }
88
+
89
+ export function renderZodSchema<T extends FieldValues, S>({
90
+ schema,
91
+ form,
92
+ path,
93
+ renderers,
94
+ }: RenderZodSchemaOptions<T, S>) {
88
95
  // Try custom renderers first
89
96
  for (const renderer of renderers) {
90
97
  const { isMatch, Component } = renderer;
@@ -107,7 +114,7 @@ export function renderZodSchema<T extends FieldValues, S>(
107
114
  !inner.description && schema.description
108
115
  ? inner.describe(schema.description)
109
116
  : inner;
110
- return renderZodSchema(inner, form, path, renderers);
117
+ return renderZodSchema({ schema: inner, form, path, renderers });
111
118
  }
112
119
 
113
120
  if (schema instanceof z.ZodOptional) {
@@ -116,7 +123,7 @@ export function renderZodSchema<T extends FieldValues, S>(
116
123
  !inner.description && schema.description
117
124
  ? inner.describe(schema.description)
118
125
  : inner;
119
- return renderZodSchema(inner, form, path, renderers);
126
+ return renderZodSchema({ schema: inner, form, path, renderers });
120
127
  }
121
128
 
122
129
  if (schema instanceof z.ZodObject) {
@@ -134,12 +141,12 @@ export function renderZodSchema<T extends FieldValues, S>(
134
141
  <FormLabel>{label}</FormLabel>
135
142
  {Objects.entries(schema.shape).map(([key, value]) => {
136
143
  const isLiteral = value instanceof z.ZodLiteral;
137
- const childForm = renderZodSchema(
138
- value as z.ZodType,
144
+ const childForm = renderZodSchema({
145
+ schema: value as z.ZodType,
139
146
  form,
140
- joinPath(path, key),
147
+ path: joinPath(path, key),
141
148
  renderers,
142
- );
149
+ });
143
150
 
144
151
  if (isLiteral) {
145
152
  return <React.Fragment key={key}>{childForm}</React.Fragment>;
@@ -189,16 +196,24 @@ export function renderZodSchema<T extends FieldValues, S>(
189
196
  name={path}
190
197
  render={({ field }) => (
191
198
  <FormItem>
192
- <div className="flex flex-row items-start space-x-2">
193
- <FormLabel>{label}</FormLabel>
194
- <FormDescription>{description}</FormDescription>
199
+ <div
200
+ className={cn(
201
+ "flex flex-row gap-2",
202
+ description ? "items-start" : "items-center",
203
+ )}
204
+ >
195
205
  <FormControl>
196
206
  <Checkbox
197
207
  data-testid="marimo-plugin-data-frames-boolean-checkbox"
198
208
  checked={field.value}
199
209
  onCheckedChange={field.onChange}
210
+ className={description ? "mt-0.5" : undefined}
200
211
  />
201
212
  </FormControl>
213
+ <div className="flex flex-col gap-1">
214
+ <FormLabel>{label}</FormLabel>
215
+ <FormDescription>{description}</FormDescription>
216
+ </div>
202
217
  </div>
203
218
  <FormMessage />
204
219
  </FormItem>
@@ -363,13 +378,13 @@ export function renderZodSchema<T extends FieldValues, S>(
363
378
 
364
379
  return (
365
380
  <div className="flex flex-col">
366
- <FormLabel>{label}</FormLabel>
367
- <div className="flex border-b mb-4 -mt-2">
381
+ <FormLabel className="mb-2">{label}</FormLabel>
382
+ <div className="flex border-b mb-4">
368
383
  {types.map((type: string) => (
369
384
  <button
370
385
  key={type}
371
386
  type="button"
372
- className={`px-4 py-2 ${
387
+ className={`px-4 py-2 text-sm ${
373
388
  unionTypeValue === type
374
389
  ? "border-b-2 border-primary font-medium"
375
390
  : "text-muted-foreground"
@@ -389,7 +404,12 @@ export function renderZodSchema<T extends FieldValues, S>(
389
404
  </div>
390
405
  <div className="flex flex-col" key={unionTypeValue}>
391
406
  {selectedOption &&
392
- renderZodSchema(selectedOption, form, path, renderers)}
407
+ renderZodSchema({
408
+ schema: selectedOption,
409
+ form,
410
+ path,
411
+ renderers,
412
+ })}
393
413
  </div>
394
414
  </div>
395
415
  );
@@ -434,7 +454,12 @@ export function renderZodSchema<T extends FieldValues, S>(
434
454
  })}
435
455
  </NativeSelect>
436
456
  {selectedOption &&
437
- renderZodSchema(selectedOption, form, path, renderers)}
457
+ renderZodSchema({
458
+ schema: selectedOption,
459
+ form,
460
+ path,
461
+ renderers,
462
+ })}
438
463
  </div>
439
464
  );
440
465
  }}
@@ -459,10 +484,15 @@ export function renderZodSchema<T extends FieldValues, S>(
459
484
  }
460
485
  if ("unwrap" in schema) {
461
486
  // Handle ZodEffects (transforms/refinements)
462
- return renderZodSchema(maybeUnwrap(schema), form, path, renderers);
487
+ return renderZodSchema({
488
+ schema: maybeUnwrap(schema),
489
+ form,
490
+ path,
491
+ renderers,
492
+ });
463
493
  }
464
494
  if (isZodPipe(schema)) {
465
- return renderZodSchema(schema.in, form, path, renderers);
495
+ return renderZodSchema({ schema: schema.in, form, path, renderers });
466
496
  }
467
497
 
468
498
  return (
@@ -512,7 +542,12 @@ const FormArray = ({
512
542
  key={field.id}
513
543
  onKeyDown={Events.onEnter((e) => e.preventDefault())}
514
544
  >
515
- {renderZodSchema(schema, form, `${path}[${index}]`, renderers)}
545
+ {renderZodSchema({
546
+ schema,
547
+ form,
548
+ path: `${path}[${index}]`,
549
+ renderers,
550
+ })}
516
551
  {canRemove && (
517
552
  <Trash2Icon
518
553
  className="w-4 h-4 ml-2 my-1 text-muted-foreground hover:text-destructive cursor-pointer absolute right-0 top-5"
@@ -28,7 +28,8 @@ export interface FieldOptions {
28
28
  | "date"
29
29
  | "datetime"
30
30
  | "time"
31
- | "tabs";
31
+ | "tabs"
32
+ | "secret_textarea";
32
33
  /**
33
34
  * Only show options that match the regex
34
35
  */
@@ -0,0 +1,40 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render } from "@testing-library/react";
4
+ import { describe, expect, it } from "vitest";
5
+ import { cellId } from "@/__tests__/branded";
6
+ import { TooltipProvider } from "@/components/ui/tooltip";
7
+ import { CellOutputId } from "@/core/cells/ids";
8
+ import { createCell, createCellRuntimeState } from "@/core/cells/types";
9
+ import { CellOutputBlock } from "../reveal-component";
10
+
11
+ describe("CellOutputBlock", () => {
12
+ it("renders the cell's data-cell-id and data-cell-name so it can be targeted by custom CSS", () => {
13
+ const id = cellId("test");
14
+ const cell = {
15
+ ...createCell({ id, name: "title" }),
16
+ ...createCellRuntimeState({
17
+ output: {
18
+ channel: "output",
19
+ mimetype: "text/plain",
20
+ data: "hello",
21
+ timestamp: 0,
22
+ },
23
+ }),
24
+ };
25
+
26
+ const { container } = render(
27
+ <TooltipProvider>
28
+ <CellOutputBlock cell={cell} />
29
+ </TooltipProvider>,
30
+ );
31
+
32
+ const cellElement = container.querySelector(`[data-cell-id="${id}"]`);
33
+ expect(cellElement).toBeTruthy();
34
+ expect(cellElement?.getAttribute("data-cell-name")).toBe("title");
35
+
36
+ // The wrapper's `data-cell-id` must coexist with the output's own
37
+ // `CellOutputId`-derived id, without colliding.
38
+ expect(document.getElementById(CellOutputId.create(id))).toBeTruthy();
39
+ });
40
+ });
@@ -0,0 +1,33 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render } from "@testing-library/react";
4
+ import { beforeAll, describe, expect, it } from "vitest";
5
+ import { SetupMocks } from "@/__mocks__/common";
6
+ import { cellId } from "@/__tests__/branded";
7
+ import { TooltipProvider } from "@/components/ui/tooltip";
8
+ import { createCell, createCellRuntimeState } from "@/core/cells/types";
9
+ import { SlideCellReadOnlyView } from "../slide-cell-view";
10
+
11
+ beforeAll(() => {
12
+ SetupMocks.resizeObserver();
13
+ });
14
+
15
+ describe("SlideCellReadOnlyView", () => {
16
+ it("renders the cell's data-cell-id and data-cell-name so it can be targeted by custom CSS", () => {
17
+ const id = cellId("test");
18
+ const cell = {
19
+ ...createCell({ id, name: "title", code: "x = 1" }),
20
+ ...createCellRuntimeState(),
21
+ };
22
+
23
+ const { container } = render(
24
+ <TooltipProvider>
25
+ <SlideCellReadOnlyView cell={cell} />
26
+ </TooltipProvider>,
27
+ );
28
+
29
+ const cellElement = container.querySelector(`[data-cell-id="${id}"]`);
30
+ expect(cellElement).toBeTruthy();
31
+ expect(cellElement?.getAttribute("data-cell-name")).toBe("title");
32
+ });
33
+ });
@@ -13,6 +13,7 @@ import useEvent from "react-use-event-hook";
13
13
  import { CodeIcon, ExpandIcon, EyeOffIcon } from "lucide-react";
14
14
  import { Deck, Fragment, Slide, Stack } from "@revealjs/react";
15
15
  import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
16
+ import { cellDomProps } from "@/components/editor/common";
16
17
  import { Slide as CellOutputSlide } from "@/components/slides/slide";
17
18
  import { SlideScrollContainer } from "@/components/slides/slide-scroll-hint";
18
19
  import { Button } from "@/components/ui/button";
@@ -304,6 +305,25 @@ function resolveSlideContentStyle(
304
305
  }
305
306
  }
306
307
 
308
+ /**
309
+ * Renders a cell's output wrapped in a `data-cell-id`/`data-cell-name` div
310
+ * (see `cellDomProps`), so it can be targeted by custom CSS in the slides
311
+ * view just like it can in the vertical layout. The wrapper is required
312
+ * because `CellOutputSlide` sets its own `id` from `CellOutputId` (a
313
+ * different scheme than `HTMLCellId`) and its inner `OutputArea` uses
314
+ * `display: contents`, which can't be targeted for background/border styling.
315
+ */
316
+ export const CellOutputBlock = ({ cell }: { cell: RuntimeCell }) => (
317
+ <div className="flex flex-col" {...cellDomProps(cell.id, cell.name)}>
318
+ <CellOutputSlide
319
+ cellId={cell.id}
320
+ status={cell.status}
321
+ output={cell.output}
322
+ stale={outputIsStale(cell, false)}
323
+ />
324
+ </div>
325
+ );
326
+
307
327
  const SubslideView = ({
308
328
  subslide,
309
329
  resolveShowCode,
@@ -340,15 +360,7 @@ const SubslideView = ({
340
360
  {subslide.blocks.map((block, i) => {
341
361
  const rendered = block.cells.map((cell) => {
342
362
  if (!resolveShowCode(cell.id)) {
343
- return (
344
- <CellOutputSlide
345
- key={cell.id}
346
- cellId={cell.id}
347
- status={cell.status}
348
- output={cell.output}
349
- stale={outputIsStale(cell, false)}
350
- />
351
- );
363
+ return <CellOutputBlock key={cell.id} cell={cell} />;
352
364
  }
353
365
  return isEditable ? (
354
366
  <SlideCellView key={cell.id} cell={cell} />
@@ -406,14 +418,7 @@ const ParkedPreviewContent = ({
406
418
  <SlideCellReadOnlyView cell={cell} />
407
419
  );
408
420
  }
409
- return (
410
- <CellOutputSlide
411
- cellId={cell.id}
412
- status={cell.status}
413
- output={cell.output}
414
- stale={outputIsStale(cell, false)}
415
- />
416
- );
421
+ return <CellOutputBlock cell={cell} />;
417
422
  };
418
423
 
419
424
  // There is an upstream react bug in dev mode (https://github.com/facebook/react/issues/34840)
@@ -202,7 +202,7 @@ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
202
202
  );
203
203
 
204
204
  const editor = (
205
- <div className="marimo-cell">
205
+ <div className="marimo-cell" {...cellDomProps(cell.id, cell.name)}>
206
206
  <ReadonlyCode
207
207
  code={display.code}
208
208
  language={display.language}
@@ -15,8 +15,10 @@
15
15
  width: unset;
16
16
  }
17
17
 
18
- /* If the first output is the only child, make it flex 1 */
19
- > *:only-child > .output:only-child {
18
+ /* If the first output is the only child, make it flex 1. Cells are wrapped
19
+ in a `data-cell-id`/`data-cell-name` div (see `cellDomProps`), so `.output`
20
+ is a grandchild rather than a direct child here. */
21
+ > *:only-child:has(> .output:only-child, > * > .output:only-child) {
20
22
  flex: 1;
21
23
  }
22
24
 
@@ -40,7 +40,7 @@ export const ShareStaticNotebookModal: React.FC<{
40
40
  e.preventDefault();
41
41
 
42
42
  onClose();
43
- const html = await exportAsHTML({
43
+ const { contents: html } = await exportAsHTML({
44
44
  download: false,
45
45
  includeCode: true,
46
46
  files: VirtualFileTracker.INSTANCE.filenames(),
@@ -10,8 +10,7 @@ import {
10
10
  import { toast } from "@/components/ui/use-toast";
11
11
  import { contextCallbacks } from "@/core/codemirror/ai/state";
12
12
  import type { EditRequests, FileInfo, RunRequests } from "@/core/network/types";
13
- import { deserializeBlob } from "@/utils/blob";
14
- import { type Base64String, base64ToDataURL } from "@/utils/json/base64";
13
+ import { fileDetailsToBlob } from "@/utils/blob";
15
14
  import { Logger } from "@/utils/Logger";
16
15
  import { type AIContextItem, AIContextProvider } from "../registry";
17
16
  import { contextToXml } from "../utils";
@@ -219,33 +218,13 @@ export class FileContextProvider extends AIContextProvider<FileContextItem> {
219
218
 
220
219
  const mimeType = fileDetails.mimeType || "text/plain";
221
220
 
222
- // Handle binary vs text files
223
- let blob: Blob;
224
- if (
225
- mimeType.startsWith("text/") ||
226
- mimeType.includes("json") ||
227
- mimeType.includes("xml")
228
- ) {
229
- // Text files - create blob directly from contents
230
- blob = new Blob([fileDetails.contents || ""], { type: mimeType });
231
- } else {
232
- // Binary files - use blob utility to decode base64
233
- if (fileDetails.contents) {
234
- try {
235
- // Create data URL using utility and deserialize blob
236
- const dataURL = base64ToDataURL(
237
- fileDetails.contents as Base64String,
238
- mimeType,
239
- );
240
- blob = deserializeBlob(dataURL);
241
- } catch {
242
- // Fallback to treating as text
243
- blob = new Blob([fileDetails.contents], { type: mimeType });
244
- }
245
- } else {
246
- blob = new Blob([""], { type: mimeType });
247
- }
248
- }
221
+ // The backend's `isBase64` flag authoritatively distinguishes binary
222
+ // (base64-encoded) from UTF-8 text contents.
223
+ const blob = fileDetailsToBlob({
224
+ contents: fileDetails.contents ?? "",
225
+ mimeType,
226
+ isBase64: fileDetails.isBase64 ?? false,
227
+ });
249
228
 
250
229
  const file = new File([blob], name, { type: mimeType });
251
230
  addAttachment(file);
@@ -1870,8 +1870,18 @@ describe("cell reducer", () => {
1870
1870
 
1871
1871
  it("can clear logs", () => {
1872
1872
  state.cellLogs = [
1873
- { level: "stderr", message: "log1", timestamp: 0, cellId: firstCellId },
1874
- { level: "stderr", message: "log1", timestamp: 0, cellId: firstCellId },
1873
+ {
1874
+ level: "stderr",
1875
+ message: "log1",
1876
+ timestamp: 0,
1877
+ source: { type: "cell", cellId: firstCellId },
1878
+ },
1879
+ {
1880
+ level: "stderr",
1881
+ message: "log1",
1882
+ timestamp: 0,
1883
+ source: { type: "cell", cellId: firstCellId },
1884
+ },
1875
1885
  ];
1876
1886
  actions.clearLogs();
1877
1887
  expect(state.cellLogs).toEqual([]);
@@ -46,7 +46,7 @@ describe("getCellLogsForMessage", () => {
46
46
  timestamp: 1_234_567_890,
47
47
  level: "stdout",
48
48
  message: "Hello, World!",
49
- cellId: "cell-1",
49
+ source: { type: "cell", cellId: "cell-1" },
50
50
  });
51
51
  });
52
52
 
@@ -74,7 +74,7 @@ describe("getCellLogsForMessage", () => {
74
74
  timestamp: 1_234_567_890,
75
75
  level: "stderr",
76
76
  message: "Error occurred",
77
- cellId: "cell-2",
77
+ source: { type: "cell", cellId: "cell-2" },
78
78
  });
79
79
  });
80
80
 
@@ -102,7 +102,7 @@ describe("getCellLogsForMessage", () => {
102
102
  timestamp: 1_234_567_890,
103
103
  level: "stdout",
104
104
  message: "Error: Something went wrong",
105
- cellId: "cell-3",
105
+ source: { type: "cell", cellId: "cell-3" },
106
106
  });
107
107
  });
108
108
 
@@ -130,7 +130,7 @@ describe("getCellLogsForMessage", () => {
130
130
  timestamp: 1_234_567_890,
131
131
  level: "stderr",
132
132
  message: "Critical Error: System failure",
133
- cellId: "cell-4",
133
+ source: { type: "cell", cellId: "cell-4" },
134
134
  });
135
135
  });
136
136
 
@@ -157,7 +157,7 @@ describe("getCellLogsForMessage", () => {
157
157
  expect(logs[0].level).toBe("stderr"); // marimo-error should be treated as stderr
158
158
  expect(logs[0].message).toContain("Traceback (most recent call last):");
159
159
  expect(logs[0].message).toContain('File "test.py", line 1');
160
- expect(logs[0].cellId).toBe("cell-5");
160
+ expect(logs[0].source).toEqual({ type: "cell", cellId: "cell-5" });
161
161
  });
162
162
 
163
163
  test("handles multiple console outputs with different MIME types", () => {
@@ -28,7 +28,7 @@ import { findCollapseRange, mergeOutlines } from "../dom/outline";
28
28
  import type { CellMessage } from "../kernel/messages";
29
29
  import { isErrorMime } from "../mime";
30
30
  import type { CellConfig } from "../network/types";
31
- import { isRtcEnabled } from "../rtc/state";
31
+ import { canUseRtc, isRtcEnabled } from "../rtc/state";
32
32
  import { createDeepEqualAtom, store } from "../state/jotai";
33
33
  import { isWasm } from "../wasm/utils";
34
34
  import { prepareCellForExecution, transitionCell } from "./cell";
@@ -927,6 +927,12 @@ const {
927
927
  cellLogs: [...nextState.cellLogs, ...getCellLogsForMessage(message)],
928
928
  };
929
929
  },
930
+ addLogs: (state, action: { logs: CellLog[] }) => {
931
+ return {
932
+ ...state,
933
+ cellLogs: [...state.cellLogs, ...action.logs],
934
+ };
935
+ },
930
936
  setCellIds: (state, action: { cellIds: CellId[] }) => {
931
937
  const isTheSame = isEqual(state.cellIds.inOrderIds, action.cellIds);
932
938
  if (isTheSame) {
@@ -1037,9 +1043,9 @@ const {
1037
1043
  };
1038
1044
  }
1039
1045
 
1040
- // Update codemirror if mounted
1041
- // If RTC is enabled, the editor view will already be updated, so we don't need to do this
1042
- if (!isRtcEnabled()) {
1046
+ // Update codemirror if mounted. RTC-backed cells sync via Loro; others
1047
+ // (e.g. scratchpad) still need an explicit editor update.
1048
+ if (!isRtcEnabled() || !canUseRtc(cellId)) {
1043
1049
  const cellHandle = nextState.cellHandles[cellId].current;
1044
1050
  if (cellHandle?.editorViewOrNull) {
1045
1051
  updateEditorCodeFromPython(cellHandle.editorViewOrNull, code);
@@ -1698,6 +1704,13 @@ export {
1698
1704
  notebookAtom,
1699
1705
  };
1700
1706
 
1707
+ /** Actions for dispatching outside of React. */
1708
+ const imperativeActions = createActions((action) => {
1709
+ store.set(notebookAtom, (state) => reducer(state, action));
1710
+ });
1711
+
1712
+ export const addLogs = imperativeActions.addLogs;
1713
+
1701
1714
  /// ATOMS
1702
1715
 
1703
1716
  export const cellIdsAtom = atom((get) => get(notebookAtom).cellIds);
@@ -369,6 +369,7 @@ export function toDocumentChanges(
369
369
  case "clearCellConsoleOutput":
370
370
  case "clearAllCellOutputs":
371
371
  case "clearLogs":
372
+ case "addLogs":
372
373
  return [];
373
374
 
374
375
  // Editor UI state — no document changes.
@@ -13,11 +13,21 @@ import { tracebackModalAtom } from "../errors/traceback-atom";
13
13
  import React from "react";
14
14
  import { ToastAction } from "@/components/ui/toast";
15
15
 
16
+ /** Logs come from a cell, or from a language server, which has no cell. */
17
+ export type LogSource =
18
+ | { type: "cell"; cellId: CellId }
19
+ | { type: "lsp"; name: string };
20
+
16
21
  export interface CellLog {
22
+ /** Unix timestamp, in seconds. */
17
23
  timestamp: number;
18
24
  level: "stdout" | "stderr";
19
25
  message: string;
20
- cellId: CellId;
26
+ source: LogSource;
27
+ }
28
+
29
+ export function logSourceLabel(source: LogSource): string {
30
+ return source.type === "cell" ? source.cellId : `lsp:${source.name}`;
21
31
  }
22
32
 
23
33
  let didAlreadyToastError = false;
@@ -48,7 +58,7 @@ export function getCellLogsForMessage(cell: CellMessage): CellLog[] {
48
58
  timestamp: output.timestamp || Date.now(),
49
59
  level: isError ? "stderr" : "stdout",
50
60
  message: message,
51
- cellId: cell.cell_id,
61
+ source: { type: "cell", cellId: cell.cell_id },
52
62
  });
53
63
  break;
54
64
  }
@@ -71,7 +81,7 @@ export function getCellLogsForMessage(cell: CellMessage): CellLog[] {
71
81
  cell.output.data.forEach((error) => {
72
82
  CellLogLogger.log({
73
83
  level: "stderr",
74
- cellId: cell.cell_id,
84
+ source: { type: "cell", cellId: cell.cell_id },
75
85
  timestamp: cell.timestamp ?? 0,
76
86
  message: JSON.stringify(error),
77
87
  });
@@ -150,7 +160,7 @@ const CellLogLogger = {
150
160
  `%c[${status}]`,
151
161
  `color:${color}; padding:2px 0; border-radius:2px; font-weight:bold`,
152
162
  `[${formatLogTimestamp(payload.timestamp)}]`,
153
- `(${payload.cellId}) ${payload.message}`,
163
+ `(${logSourceLabel(payload.source)}) ${payload.message}`,
154
164
  );
155
165
  },
156
166
  };
@@ -50,6 +50,7 @@ import type { HotkeyProvider } from "../hotkeys/hotkeys";
50
50
  import { requestEditCompletion } from "./ai/request";
51
51
  import { cellBundle } from "./cells/extensions";
52
52
  import type { CodemirrorCellActions } from "./cells/state";
53
+ import { codeLensBundle } from "./code-lens/extension";
53
54
  import { jupyterHelpExtension } from "./compat/jupyter";
54
55
  import { hintTooltip } from "./completion/hints";
55
56
  import { completionKeymap } from "./completion/keymap";
@@ -198,6 +199,8 @@ export const setupCodeMirror = (opts: CodeMirrorSetupOpts): Extension[] => {
198
199
  cellId,
199
200
  displayConfig.reference_highlighting ?? true,
200
201
  ),
202
+ // Inline icons linking datasources/buckets/caches to their panels
203
+ codeLensBundle(cellId),
201
204
  ];
202
205
  };
203
206
 
@@ -0,0 +1,68 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import {
5
+ fileExplorerPanelAtom,
6
+ sessionPanelAtom,
7
+ } from "@/components/editor/chrome/panels/panel-accordion-state";
8
+ import { openPanel } from "@/components/editor/chrome/state";
9
+ import { store } from "@/core/state/jotai";
10
+ import { openLensTarget } from "../actions";
11
+
12
+ vi.mock("@/components/editor/chrome/state", () => ({
13
+ openPanel: vi.fn(),
14
+ }));
15
+
16
+ describe("openLensTarget", () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ store.set(sessionPanelAtom, {
20
+ openSections: ["variables"],
21
+ hasUserInteracted: false,
22
+ });
23
+ store.set(fileExplorerPanelAtom, {
24
+ openSections: ["files"],
25
+ hasUserInteracted: true,
26
+ });
27
+ });
28
+
29
+ it.each(["table", "connection"] as const)(
30
+ "opens the variables panel and expands datasources for a %s",
31
+ (kind) => {
32
+ openLensTarget(kind);
33
+
34
+ expect(openPanel).toHaveBeenCalledWith("variables");
35
+ expect(store.get(sessionPanelAtom).openSections).toEqual([
36
+ "variables",
37
+ "datasources",
38
+ ]);
39
+ },
40
+ );
41
+
42
+ it("opens the files panel and expands remote storage for a bucket", () => {
43
+ openLensTarget("bucket");
44
+
45
+ expect(openPanel).toHaveBeenCalledWith("files");
46
+ expect(store.get(fileExplorerPanelAtom).openSections).toEqual([
47
+ "files",
48
+ "remote-storage",
49
+ ]);
50
+ });
51
+
52
+ it("opens the cache panel for a cache", () => {
53
+ openLensTarget("cache");
54
+
55
+ expect(openPanel).toHaveBeenCalledWith("cache");
56
+ expect(store.get(sessionPanelAtom).openSections).toEqual(["variables"]);
57
+ expect(store.get(fileExplorerPanelAtom).openSections).toEqual(["files"]);
58
+ });
59
+
60
+ it("does not duplicate an already-open section or mark interaction", () => {
61
+ openLensTarget("table");
62
+ openLensTarget("table");
63
+
64
+ const state = store.get(sessionPanelAtom);
65
+ expect(state.openSections).toEqual(["variables", "datasources"]);
66
+ expect(state.hasUserInteracted).toBe(false);
67
+ });
68
+ });