@marimo-team/islands 0.23.14-dev6 → 0.23.14-dev61

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 (171) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
  2. package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
  3. package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
  4. package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
  5. package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
  6. package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
  7. package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
  8. package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
  9. package/dist/{formats-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
  10. package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
  11. package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
  12. package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
  13. package/dist/main.js +3630 -1801
  14. package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.js} +2 -2
  15. package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
  16. package/dist/{process-output-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
  17. package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
  18. package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
  19. package/dist/style.css +1 -1
  20. package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
  21. package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
  22. package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
  23. package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
  24. package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
  25. package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
  26. package/package.json +4 -4
  27. package/src/__mocks__/requests.ts +1 -0
  28. package/src/__tests__/CellStatus.test.tsx +1 -2
  29. package/src/components/app-config/user-config-form.tsx +52 -0
  30. package/src/components/chat/acp/agent-panel.tsx +35 -1
  31. package/src/components/chat/chat-panel.tsx +68 -29
  32. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  33. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  34. package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
  35. package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
  36. package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
  37. package/src/components/data-table/charts/charts.tsx +23 -1
  38. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  39. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  40. package/src/components/debugger/debugger-code.tsx +7 -2
  41. package/src/components/editor/actions/useNotebookActions.tsx +2 -2
  42. package/src/components/editor/cell/CellStatus.tsx +1 -20
  43. package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
  44. package/src/components/editor/cell/cell-context-menu.tsx +2 -2
  45. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
  46. package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  47. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  48. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  49. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  50. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
  51. package/src/components/editor/code/readonly-python-code.tsx +60 -33
  52. package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
  53. package/src/components/editor/errors/auto-fix.tsx +88 -34
  54. package/src/components/editor/errors/fix-mode.ts +1 -1
  55. package/src/components/editor/notebook-cell.tsx +7 -0
  56. package/src/components/editor/output/ImageOutput.tsx +20 -6
  57. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
  58. package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
  59. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  60. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
  61. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
  62. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  63. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  64. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  65. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  66. package/src/components/slides/__tests__/slide.test.tsx +33 -0
  67. package/src/components/slides/minimap.tsx +7 -1
  68. package/src/components/slides/reveal-component.tsx +3 -0
  69. package/src/components/slides/slide-cell-view.tsx +10 -6
  70. package/src/components/slides/slide.tsx +16 -13
  71. package/src/components/tracing/tracing.tsx +2 -1
  72. package/src/core/ai/config.ts +2 -2
  73. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  74. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  75. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  76. package/src/core/ai/context/providers/datasource.ts +27 -2
  77. package/src/core/ai/context/providers/error.ts +226 -36
  78. package/src/core/ai/context/registry.ts +77 -43
  79. package/src/core/ai/state.ts +11 -0
  80. package/src/core/cells/__tests__/cell.test.ts +39 -0
  81. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  82. package/src/core/cells/cell.ts +5 -3
  83. package/src/core/cells/readonly-code-display.ts +35 -0
  84. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  85. package/src/core/codemirror/ai/resources.ts +15 -10
  86. package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
  87. package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
  88. package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
  89. package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
  90. package/src/core/codemirror/cells/debugger-state.ts +133 -0
  91. package/src/core/codemirror/cells/extensions.ts +32 -4
  92. package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
  93. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
  94. package/src/core/codemirror/go-to-definition/extension.ts +3 -3
  95. package/src/core/codemirror/go-to-definition/underline.ts +5 -14
  96. package/src/core/codemirror/go-to-definition/utils.ts +49 -1
  97. package/src/core/codemirror/utils.ts +15 -0
  98. package/src/core/config/__tests__/config-schema.test.ts +17 -0
  99. package/src/core/config/config-schema.ts +1 -0
  100. package/src/core/config/config.ts +10 -0
  101. package/src/core/config/feature-flag.tsx +4 -0
  102. package/src/core/edit-app.tsx +8 -5
  103. package/src/core/errors/errors.ts +2 -1
  104. package/src/core/islands/bootstrap.ts +4 -3
  105. package/src/core/islands/bridge.ts +1 -0
  106. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  107. package/src/core/lifecycle/render-policy.ts +125 -0
  108. package/src/core/mime.ts +11 -4
  109. package/src/core/network/__tests__/api.test.ts +17 -0
  110. package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
  111. package/src/core/network/api.ts +17 -6
  112. package/src/core/network/requests-lazy.ts +1 -0
  113. package/src/core/network/requests-network.ts +8 -0
  114. package/src/core/network/requests-static.ts +1 -0
  115. package/src/core/network/requests-toasting.tsx +10 -1
  116. package/src/core/network/types.ts +2 -0
  117. package/src/core/run-app.tsx +27 -23
  118. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  119. package/src/core/runtime/__tests__/runtime.test.ts +45 -0
  120. package/src/core/runtime/adapter.ts +182 -0
  121. package/src/core/runtime/runtime.ts +30 -16
  122. package/src/core/wasm/PyodideLoader.tsx +20 -62
  123. package/src/core/wasm/bridge.ts +13 -4
  124. package/src/core/wasm/state.ts +8 -19
  125. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
  126. package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
  127. package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
  128. package/src/core/websocket/transports/sse.ts +308 -0
  129. package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
  130. package/src/core/websocket/useWebSocket.tsx +12 -2
  131. package/src/css/app/codemirror.css +14 -0
  132. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  133. package/src/hooks/useDelayElapsed.ts +27 -0
  134. package/src/mount.tsx +3 -3
  135. package/src/plugins/core/RenderHTML.tsx +35 -1
  136. package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
  137. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  138. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
  139. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
  140. package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
  141. package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
  142. package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
  143. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
  144. package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
  145. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
  146. package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
  147. package/src/plugins/impl/anywidget/host.ts +54 -0
  148. package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
  149. package/src/plugins/impl/anywidget/model.ts +59 -239
  150. package/src/plugins/impl/anywidget/registry.ts +268 -0
  151. package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
  152. package/src/plugins/impl/anywidget/runtime.ts +422 -0
  153. package/src/plugins/impl/anywidget/types.ts +14 -0
  154. package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
  155. package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
  156. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
  157. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
  158. package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
  159. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
  160. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
  161. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
  162. package/src/utils/errors.ts +15 -0
  163. package/src/utils/time.ts +20 -0
  164. package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
  165. package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
  166. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
  167. package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
  168. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  169. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  170. package/src/core/wasm/__tests__/state.test.ts +0 -124
  171. package/src/plugins/impl/anywidget/schemas.ts +0 -32
@@ -11,19 +11,22 @@ interface SlideContentProps extends Pick<
11
11
  "output" | "status"
12
12
  > {
13
13
  cellId: CellId;
14
+ stale: boolean;
14
15
  }
15
16
 
16
- export const Slide = memo(({ output, cellId, status }: SlideContentProps) => {
17
- const loading = outputIsLoading(status);
18
- return (
19
- <OutputArea
20
- className="contents"
21
- allowExpand={false}
22
- output={output}
23
- cellId={cellId}
24
- stale={loading}
25
- loading={loading}
26
- />
27
- );
28
- });
17
+ export const Slide = memo(
18
+ ({ output, cellId, status, stale }: SlideContentProps) => {
19
+ const loading = outputIsLoading(status);
20
+ return (
21
+ <OutputArea
22
+ className="contents"
23
+ allowExpand={false}
24
+ output={output}
25
+ cellId={cellId}
26
+ stale={stale}
27
+ loading={loading}
28
+ />
29
+ );
30
+ },
31
+ );
29
32
  Slide.displayName = "Slide";
@@ -28,9 +28,10 @@ import {
28
28
  } from "@/core/cells/runs";
29
29
  import { type ResolvedTheme, useTheme } from "@/theme/useTheme";
30
30
  import { cn } from "@/utils/cn";
31
+ import { formatElapsedTime } from "@/utils/time";
31
32
  import { ClearButton } from "../buttons/clear-button";
32
33
  import type { SignalListener } from "../charts/types";
33
- import { ElapsedTime, formatElapsedTime } from "../editor/cell/CellStatus";
34
+ import { ElapsedTime } from "../editor/cell/CellStatus";
34
35
  import { PanelEmptyState } from "../editor/chrome/panels/empty-state";
35
36
  import { usePanelSection } from "../editor/chrome/panels/panel-context";
36
37
  import { CellLink } from "../editor/links/cell-link";
@@ -60,7 +60,7 @@ export const useModelChange = () => {
60
60
  },
61
61
  };
62
62
 
63
- saveConfig(newConfig);
63
+ await saveConfig(newConfig);
64
64
  };
65
65
 
66
66
  const saveModeChange = async (newMode: CopilotMode) => {
@@ -71,7 +71,7 @@ export const useModelChange = () => {
71
71
  },
72
72
  };
73
73
 
74
- saveConfig(newConfig);
74
+ await saveConfig(newConfig);
75
75
  };
76
76
 
77
77
  return { saveModelChange, saveModeChange };
@@ -176,6 +176,33 @@ class FileContextProvider extends AIContextProvider<FileContextItem> {
176
176
  }
177
177
  }
178
178
 
179
+ // Second mock provider with the same context type as MockContextProvider
180
+ class SecondaryMockProvider extends AIContextProvider<MockContextItem> {
181
+ readonly title = "Secondary Mock Items";
182
+ readonly mentionPrefix = "@";
183
+ readonly contextType = "mock";
184
+
185
+ getItems(): MockContextItem[] {
186
+ return [
187
+ {
188
+ type: "mock",
189
+ uri: this.asURI("secondary-item"),
190
+ name: "Secondary Item",
191
+ description: "From the second mock provider",
192
+ data: { value: "secondary" },
193
+ },
194
+ ];
195
+ }
196
+
197
+ formatContext(item: MockContextItem): string {
198
+ return `Secondary: ${item.name}`;
199
+ }
200
+
201
+ formatCompletion(item: MockContextItem): Completion {
202
+ return this.createBasicCompletion(item);
203
+ }
204
+ }
205
+
179
206
  describe("AIContextProvider", () => {
180
207
  let provider: MockContextProvider;
181
208
 
@@ -411,6 +438,95 @@ describe("AIContextRegistry", () => {
411
438
  });
412
439
  });
413
440
 
441
+ describe("resolveItems", () => {
442
+ let mockGetItems: ReturnType<typeof vi.spyOn>;
443
+ let fileGetItems: ReturnType<typeof vi.spyOn>;
444
+
445
+ beforeEach(() => {
446
+ registry.register(mockProvider);
447
+ registry.register(fileProvider);
448
+ mockGetItems = vi.spyOn(mockProvider, "getItems");
449
+ fileGetItems = vi.spyOn(fileProvider, "getItems");
450
+ });
451
+
452
+ it("should resolve valid IDs from the matching providers only", () => {
453
+ const resolved = registry.resolveItems([
454
+ "mock://item1",
455
+ "file://config.py",
456
+ ] as ContextLocatorId[]);
457
+
458
+ expect(resolved).toHaveLength(2);
459
+ expect(resolved.map((item) => item.uri)).toEqual([
460
+ "mock://item1",
461
+ "file://config.py",
462
+ ]);
463
+ expect(mockGetItems).toHaveBeenCalledTimes(1);
464
+ expect(fileGetItems).toHaveBeenCalledTimes(1);
465
+ });
466
+
467
+ it("should not call unrelated providers", () => {
468
+ registry.resolveItems(["mock://item1"] as ContextLocatorId[]);
469
+
470
+ expect(mockGetItems).toHaveBeenCalledTimes(1);
471
+ expect(fileGetItems).not.toHaveBeenCalled();
472
+ });
473
+
474
+ it("should ignore unknown or invalid URIs", () => {
475
+ const resolved = registry.resolveItems([
476
+ "mock://nonexistent",
477
+ "unknown://item",
478
+ "malformed" as ContextLocatorId,
479
+ ] as ContextLocatorId[]);
480
+
481
+ expect(resolved).toEqual([]);
482
+ expect(mockGetItems).toHaveBeenCalledTimes(1);
483
+ expect(fileGetItems).not.toHaveBeenCalled();
484
+ });
485
+
486
+ it("should return empty array for empty input", () => {
487
+ expect(registry.resolveItems([])).toEqual([]);
488
+ expect(mockGetItems).not.toHaveBeenCalled();
489
+ expect(fileGetItems).not.toHaveBeenCalled();
490
+ });
491
+
492
+ it("should preserve requested order when providers are interleaved", () => {
493
+ const resolved = registry.resolveItems([
494
+ "mock://item1",
495
+ "file://config.py",
496
+ "mock://item2",
497
+ "file://utils/helpers.py",
498
+ ] as ContextLocatorId[]);
499
+
500
+ expect(resolved.map((item) => item.uri)).toEqual([
501
+ "mock://item1",
502
+ "file://config.py",
503
+ "mock://item2",
504
+ "file://utils/helpers.py",
505
+ ]);
506
+ // Each provider is still queried only once, regardless of interleaving.
507
+ expect(mockGetItems).toHaveBeenCalledTimes(1);
508
+ expect(fileGetItems).toHaveBeenCalledTimes(1);
509
+ });
510
+
511
+ it("should resolve items from all providers that share a context type", () => {
512
+ const secondaryProvider = new SecondaryMockProvider();
513
+ const secondaryGetItems = vi.spyOn(secondaryProvider, "getItems");
514
+ registry.register(secondaryProvider);
515
+
516
+ const resolved = registry.resolveItems([
517
+ "mock://item1",
518
+ "mock://secondary-item",
519
+ ] as ContextLocatorId[]);
520
+
521
+ expect(resolved.map((item) => item.uri)).toEqual([
522
+ "mock://item1",
523
+ "mock://secondary-item",
524
+ ]);
525
+ expect(mockGetItems).toHaveBeenCalledTimes(1);
526
+ expect(secondaryGetItems).toHaveBeenCalledTimes(1);
527
+ });
528
+ });
529
+
414
530
  describe("getContextInfo", () => {
415
531
  beforeEach(() => {
416
532
  registry.register(mockProvider);
@@ -531,6 +647,19 @@ describe("AIContextRegistry", () => {
531
647
  expect(formatted).toContain("Mock: Item 1 (value1)");
532
648
  expect(formatted.split("\n\n")).toHaveLength(1);
533
649
  });
650
+
651
+ it("should format items from the provider that owns them", () => {
652
+ registry.register(new SecondaryMockProvider());
653
+
654
+ const formatted = registry.formatContextForAI([
655
+ "mock://item1",
656
+ "mock://secondary-item",
657
+ ] as ContextLocatorId[]);
658
+
659
+ expect(formatted).toBe(
660
+ "Mock: Item 1 (value1)\n\nSecondary: Secondary Item",
661
+ );
662
+ });
534
663
  });
535
664
 
536
665
  describe("edge cases and error handling", () => {
@@ -621,7 +750,7 @@ describe("AIContextRegistry", () => {
621
750
  expect(provider).toBe(errorProvider);
622
751
 
623
752
  const items = registry.getAllItems();
624
- expect(items).toHaveLength(1);
753
+ expect(items).toHaveLength(3);
625
754
  expect(items[0].type).toBe("error");
626
755
  expect(items[0].name).toBe("Errors");
627
756
  });
@@ -654,10 +783,11 @@ describe("AIContextRegistry", () => {
654
783
  expect(provider).toBeDefined();
655
784
 
656
785
  const items = provider!.getItems();
657
- expect(items).toHaveLength(1);
786
+ expect(items).toHaveLength(3);
658
787
 
659
788
  const completion = provider!.formatCompletion(items[0]);
660
789
  expect(completion.label).toBe("@Errors");
790
+ expect(completion.apply).toBe("@error://all");
661
791
  expect(completion.type).toBe("error");
662
792
  });
663
793
 
@@ -690,7 +820,7 @@ describe("AIContextRegistry", () => {
690
820
  const errorItems = items.filter((item) => item.type === "error");
691
821
  const mockItems = items.filter((item) => item.type === "mock");
692
822
 
693
- expect(errorItems).toHaveLength(1);
823
+ expect(errorItems).toHaveLength(3);
694
824
  expect(mockItems).toHaveLength(3);
695
825
  });
696
826
 
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  /* oxlint-disable typescript/no-explicit-any */
3
3
 
4
+ import { createStore } from "jotai";
4
5
  import { beforeEach, describe, expect, it } from "vitest";
5
6
  import { cellId, variableName } from "@/__tests__/branded";
6
7
  import type {
@@ -634,7 +635,8 @@ describe("DatasourceContextProvider", () => {
634
635
 
635
636
  describe("getDatasourceContext", () => {
636
637
  it("should return null if no cell ID is found", () => {
637
- const context = getDatasourceContext(cellId("1"));
638
+ const store = createStore();
639
+ const context = getDatasourceContext(cellId("1"), store);
638
640
  expect(context).toBeNull();
639
641
  });
640
642
  });
@@ -57,7 +57,7 @@ describe("ErrorContextProvider", () => {
57
57
 
58
58
  const items = provider.getItems();
59
59
 
60
- expect(items).toHaveLength(1);
60
+ expect(items).toHaveLength(3);
61
61
  expect(items[0]).toMatchObject({
62
62
  name: "Errors",
63
63
  type: "error",
@@ -78,6 +78,18 @@ describe("ErrorContextProvider", () => {
78
78
  ],
79
79
  },
80
80
  });
81
+ expect(items[1]).toMatchObject({
82
+ uri: `error://${cellId1}`,
83
+ name: "Error: Cell 1",
84
+ description: "Invalid syntax",
85
+ data: { type: "cell-error", error: { cellId: cellId1 } },
86
+ });
87
+ expect(items[2]).toMatchObject({
88
+ uri: `error://${cellId2}`,
89
+ name: "Error: Cell 2",
90
+ description: "Runtime error",
91
+ data: { type: "cell-error", error: { cellId: cellId2 } },
92
+ });
81
93
  });
82
94
 
83
95
  it("should handle cells without names", () => {
@@ -92,7 +104,12 @@ describe("ErrorContextProvider", () => {
92
104
  ]);
93
105
 
94
106
  const items = provider.getItems();
95
- expect(items[0].data.errors[0].cellName).toBe("");
107
+ expect(items[1].data.type).toBe("cell-error");
108
+ if (items[1].data.type === "cell-error") {
109
+ expect(items[1].data.error.cellName).toBe("cell-0");
110
+ }
111
+ expect(items[1].name).toBe("Error: cell-0");
112
+ expect(items[1].description).toBe("Invalid syntax");
96
113
  });
97
114
  });
98
115
 
@@ -119,7 +136,7 @@ describe("ErrorContextProvider", () => {
119
136
 
120
137
  expect(completion).toMatchInlineSnapshot(`
121
138
  {
122
- "apply": "@Errors",
139
+ "apply": "@error://all",
123
140
  "detail": "2 errors",
124
141
  "displayLabel": "Errors",
125
142
  "info": [Function],
@@ -170,6 +187,24 @@ describe("ErrorContextProvider", () => {
170
187
  expect(completion.detail).toBe("1 error");
171
188
  });
172
189
 
190
+ it("formats completion for a single cell error", () => {
191
+ const cellId = CellIdClass.create();
192
+
193
+ createMockNotebookWithErrors([
194
+ {
195
+ cellId,
196
+ cellName: "Cell 1",
197
+ errorData: [MockNotebook.errors.syntax("Invalid syntax")],
198
+ },
199
+ ]);
200
+
201
+ const items = provider.getItems();
202
+ const completion = provider.formatCompletion(items[1]);
203
+ expect(completion.apply).toBe(`@error://${cellId}`);
204
+ expect(completion.displayLabel).toBe("Error: Cell 1");
205
+ expect(items[1].name).toBe("Error: Cell 1");
206
+ });
207
+
173
208
  it("should handle fallback for unknown error types", () => {
174
209
  const item = {
175
210
  uri: "error://unknown",
@@ -210,7 +245,8 @@ describe("ErrorContextProvider", () => {
210
245
 
211
246
  const items = provider.getItems();
212
247
  const context = provider.formatContext(items[0]);
213
- expect(context).toMatchSnapshot("basic-error-context");
248
+ expect(context).toContain("Invalid syntax");
249
+ expect(context).toContain("Code:");
214
250
  });
215
251
 
216
252
  it("should format context for multiple error types", () => {
@@ -239,14 +275,12 @@ describe("ErrorContextProvider", () => {
239
275
  const items = provider.getItems();
240
276
  const context = provider.formatContext(items[0]);
241
277
 
242
- // Check for expected content instead of exact snapshot match due to random cell IDs
243
- expect(context).toMatchInlineSnapshot(`
244
- "<error name="Cell 1" description="Invalid syntax
245
- Runtime error"></error>
246
-
247
- <error name="Cell cell-2" description="This cell is in a cycle
248
- The variable 'variable_x' was defined by another cell"></error>"
249
- `);
278
+ expect(context).toContain("Invalid syntax");
279
+ expect(context).toContain("Runtime error");
280
+ expect(context).toContain("This cell is in a cycle");
281
+ expect(context).toContain(
282
+ "The variable 'variable_x' was defined by another cell",
283
+ );
250
284
  });
251
285
 
252
286
  it("should handle cells without names", () => {
@@ -261,8 +295,158 @@ describe("ErrorContextProvider", () => {
261
295
  ]);
262
296
 
263
297
  const items = provider.getItems();
264
- const context = provider.formatContext(items[0]);
265
- expect(context).toContain(`Cell ${cellId}`);
298
+ const context = provider.formatContext(items[1]);
299
+ expect(context).toContain(`cellId="${cellId}"`);
300
+ expect(context).toContain("Invalid syntax");
301
+ });
302
+
303
+ it("includes exception tracebacks in formatted context", () => {
304
+ const cellId = CellIdClass.create();
305
+
306
+ createMockNotebookWithErrors([
307
+ {
308
+ cellId,
309
+ cellName: "Cell 1",
310
+ errorData: [
311
+ {
312
+ type: "exception",
313
+ msg: "boom",
314
+ exception_type: "ValueError",
315
+ raising_cell: null,
316
+ traceback: "<pre>line 1\nline 2</pre>",
317
+ },
318
+ ],
319
+ },
320
+ ]);
321
+
322
+ const items = provider.getItems();
323
+ const context = provider.formatContext(items[1]);
324
+ expect(context).toContain("boom");
325
+ expect(context).toContain("line 1");
326
+ expect(context).toContain("line 2");
327
+ });
328
+
329
+ it("includes traceback-only cell outputs", () => {
330
+ const cellId = CellIdClass.create();
331
+ const notebookState = MockNotebook.notebookState({
332
+ cellData: {
333
+ [cellId]: {
334
+ name: "Cell 1",
335
+ code: "raise ValueError('boom')",
336
+ },
337
+ },
338
+ });
339
+ notebookState.cellRuntime[cellId] = {
340
+ ...notebookState.cellRuntime[cellId],
341
+ output: {
342
+ channel: "marimo-error",
343
+ data: "<pre>ValueError: boom\n line 1</pre>",
344
+ mimetype: "application/vnd.marimo+traceback",
345
+ timestamp: Date.now(),
346
+ },
347
+ };
348
+ store.set(notebookAtom, notebookState);
349
+
350
+ const items = provider.getItems();
351
+ expect(items).toHaveLength(2);
352
+ expect(items[1].description).toBe("ValueError: boom");
353
+ const context = provider.formatContext(items[1]);
354
+ expect(context).toContain("raise ValueError('boom')");
355
+ expect(context).toContain("ValueError: boom");
356
+ expect(context).toContain("line 1");
357
+ });
358
+
359
+ it("includes console tracebacks for marimo error outputs", () => {
360
+ const cellId = CellIdClass.create();
361
+ const notebookState = MockNotebook.notebookState({
362
+ cellData: {
363
+ [cellId]: {
364
+ name: "Cell 1",
365
+ code: "raise ValueError('boom')",
366
+ },
367
+ },
368
+ });
369
+ notebookState.cellRuntime[cellId] = {
370
+ ...notebookState.cellRuntime[cellId],
371
+ output: {
372
+ channel: "marimo-error",
373
+ data: [
374
+ {
375
+ type: "exception",
376
+ msg: "boom",
377
+ exception_type: "ValueError",
378
+ raising_cell: null,
379
+ },
380
+ ],
381
+ mimetype: "application/vnd.marimo+error",
382
+ timestamp: Date.now(),
383
+ },
384
+ consoleOutputs: [
385
+ {
386
+ channel: "stderr",
387
+ data: '<pre>ValueError: boom\n File "notebook.py", line 1</pre>',
388
+ mimetype: "application/vnd.marimo+traceback",
389
+ timestamp: Date.now(),
390
+ },
391
+ ],
392
+ };
393
+ store.set(notebookAtom, notebookState);
394
+
395
+ const items = provider.getItems();
396
+ const context = provider.formatContext(items[1]);
397
+ expect(context).toContain("boom");
398
+ expect(context).toContain("ValueError: boom");
399
+ expect(context).toContain('File "notebook.py", line 1');
400
+ expect(items[1].description).toBe("boom");
401
+ });
402
+
403
+ it("does not duplicate console traceback when error already has one", () => {
404
+ const cellId = CellIdClass.create();
405
+ const notebookState = MockNotebook.notebookState({
406
+ cellData: {
407
+ [cellId]: {
408
+ name: "Cell 1",
409
+ code: "raise ValueError('boom')",
410
+ },
411
+ },
412
+ });
413
+ notebookState.cellRuntime[cellId] = {
414
+ ...notebookState.cellRuntime[cellId],
415
+ output: {
416
+ channel: "marimo-error",
417
+ data: [
418
+ {
419
+ type: "exception",
420
+ msg: "boom",
421
+ exception_type: "ValueError",
422
+ raising_cell: null,
423
+ traceback: "<pre>embedded line 1</pre>",
424
+ },
425
+ ],
426
+ mimetype: "application/vnd.marimo+error",
427
+ timestamp: Date.now(),
428
+ },
429
+ consoleOutputs: [
430
+ {
431
+ channel: "stderr",
432
+ data: "<pre>console line 1</pre>",
433
+ mimetype: "application/vnd.marimo+traceback",
434
+ timestamp: Date.now(),
435
+ },
436
+ ],
437
+ };
438
+ store.set(notebookAtom, notebookState);
439
+
440
+ const items = provider.getItems();
441
+ if (items[1].data.type !== "cell-error") {
442
+ throw new Error("Expected cell-error item");
443
+ }
444
+ expect(items[1].data.error.tracebackHtml).toBeUndefined();
445
+
446
+ const context = provider.formatContext(items[1]);
447
+ expect(context).toContain("embedded line 1");
448
+ expect(context).not.toContain("console line 1");
449
+ expect(items[1].description).toBe("boom");
266
450
  });
267
451
  });
268
452
 
@@ -319,7 +503,8 @@ describe("ErrorContextProvider", () => {
319
503
  testStore.set(notebookAtom, notebookState);
320
504
 
321
505
  const items = testProvider.getItems();
322
- expect(items).toHaveLength(1);
506
+ // One "all-errors" item plus one per-cell error item.
507
+ expect(items).toHaveLength(2);
323
508
  const context = testProvider.formatContext(items[0]);
324
509
  expect(context).toContain(expected);
325
510
  }
@@ -8,6 +8,7 @@ import type { CellId } from "@/core/cells/ids";
8
8
  import { LanguageAdapters } from "@/core/codemirror/language/LanguageAdapters";
9
9
  import { renderDatasourceInfo } from "@/core/codemirror/language/languages/sql/renderers";
10
10
  import {
11
+ allTablesAtom,
11
12
  type ConnectionsMap,
12
13
  type DatasetTablesMap,
13
14
  dataSourceConnectionsAtom,
@@ -18,7 +19,7 @@ import {
18
19
  INTERNAL_SQL_ENGINES,
19
20
  } from "@/core/datasets/engines";
20
21
  import type { DataSourceConnection, DataTable } from "@/core/kernel/messages";
21
- import { store } from "@/core/state/jotai";
22
+ import type { JotaiStore } from "@/core/state/jotai";
22
23
  import type { AIContextItem } from "../registry";
23
24
  import { AIContextProvider } from "../registry";
24
25
  import { contextToXml } from "../utils";
@@ -148,7 +149,10 @@ export class DatasourceContextProvider extends AIContextProvider<DatasourceConte
148
149
  }
149
150
  }
150
151
 
151
- export function getDatasourceContext(cellId: CellId): string | null {
152
+ export function getDatasourceContext(
153
+ cellId: CellId,
154
+ store: JotaiStore,
155
+ ): string | null {
152
156
  const cellData = store.get(cellDataAtom(cellId));
153
157
  const code = cellData?.code;
154
158
  if (!code || code.trim() === "") {
@@ -164,3 +168,24 @@ export function getDatasourceContext(cellId: CellId): string | null {
164
168
  }
165
169
  return null;
166
170
  }
171
+
172
+ export function formatDatasourceContextForCell(
173
+ cellId: CellId,
174
+ store: JotaiStore,
175
+ ): string | null {
176
+ const mention = getDatasourceContext(cellId, store);
177
+ if (!mention) {
178
+ return null;
179
+ }
180
+
181
+ const connectionsMap = store.get(dataSourceConnectionsAtom).connectionsMap;
182
+ const tablesMap = store.get(allTablesAtom);
183
+ const provider = new DatasourceContextProvider(connectionsMap, tablesMap);
184
+ const uri = mention.slice(1);
185
+ const item = provider.getItems().find((candidate) => candidate.uri === uri);
186
+ if (!item) {
187
+ return null;
188
+ }
189
+
190
+ return provider.formatContext(item);
191
+ }