@marimo-team/islands 0.23.14-dev5 → 0.23.14-dev51

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 (128) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-DXBx_nQg.js} +4 -4
  2. package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DXPRhRO2.js} +3248 -3044
  3. package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
  4. package/dist/{code-visibility-rxZi4Phe.js → code-visibility-BA5VF477.js} +1153 -946
  5. package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
  6. package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
  7. package/dist/{formats-d6MhLuQ9.js → formats-WsOgyW_K.js} +1 -1
  8. package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-7wxMGXjG.js} +2084 -1889
  9. package/dist/{html-to-image-CGp_08St.js → html-to-image-DOqQBSQc.js} +2367 -2301
  10. package/dist/{input-CbEz_aj_.js → input-BSdZp5Ng.js} +1 -1
  11. package/dist/main.js +2707 -1186
  12. package/dist/{mermaid-CJW9vIyO.js → mermaid-D-HYBMEV.js} +2 -2
  13. package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
  14. package/dist/{process-output-R6JsYrv3.js → process-output-kGk2Jc2x.js} +1 -1
  15. package/dist/{reveal-component-sEb3Wd1x.js → reveal-component-BOL6pLBN.js} +600 -596
  16. package/dist/{spec-Bv-XlYiv.js → spec-CnTgI25l.js} +1 -1
  17. package/dist/style.css +1 -1
  18. package/dist/{toDate-D-l5s8nn.js → toDate-D1Z7ZXWh.js} +1 -1
  19. package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-BMc8itk2.js} +1 -1
  20. package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-ZwmDBRDY.js} +1 -1
  21. package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-CxffarYV.js} +1 -1
  22. package/dist/{useTheme-BrYvK-_A.js → useTheme-yGsGEk82.js} +26 -24
  23. package/dist/{vega-component-Pk6lyc_a.js → vega-component-BFJTyykA.js} +5 -5
  24. package/package.json +3 -3
  25. package/src/__mocks__/requests.ts +1 -0
  26. package/src/components/app-config/user-config-form.tsx +26 -0
  27. package/src/components/chat/acp/agent-panel.tsx +35 -1
  28. package/src/components/chat/chat-panel.tsx +68 -29
  29. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  30. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  31. package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
  32. package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
  33. package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
  34. package/src/components/data-table/charts/charts.tsx +23 -1
  35. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  36. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  37. package/src/components/debugger/debugger-code.tsx +7 -2
  38. package/src/components/editor/actions/useNotebookActions.tsx +2 -2
  39. package/src/components/editor/cell/cell-context-menu.tsx +2 -2
  40. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
  41. package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  42. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  43. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  44. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  45. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
  46. package/src/components/editor/code/readonly-python-code.tsx +60 -33
  47. package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
  48. package/src/components/editor/errors/auto-fix.tsx +88 -34
  49. package/src/components/editor/errors/fix-mode.ts +1 -1
  50. package/src/components/editor/notebook-cell.tsx +7 -0
  51. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
  52. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  53. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
  54. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
  55. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  56. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  57. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  58. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  59. package/src/components/slides/__tests__/slide.test.tsx +33 -0
  60. package/src/components/slides/minimap.tsx +7 -1
  61. package/src/components/slides/reveal-component.tsx +3 -0
  62. package/src/components/slides/slide-cell-view.tsx +10 -6
  63. package/src/components/slides/slide.tsx +16 -13
  64. package/src/core/ai/config.ts +2 -2
  65. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  66. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  67. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  68. package/src/core/ai/context/providers/datasource.ts +27 -2
  69. package/src/core/ai/context/providers/error.ts +226 -36
  70. package/src/core/ai/context/registry.ts +77 -43
  71. package/src/core/ai/state.ts +11 -0
  72. package/src/core/cells/__tests__/cell.test.ts +39 -0
  73. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  74. package/src/core/cells/cell.ts +5 -3
  75. package/src/core/cells/readonly-code-display.ts +35 -0
  76. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  77. package/src/core/codemirror/ai/resources.ts +15 -10
  78. package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
  79. package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +139 -0
  80. package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
  81. package/src/core/codemirror/cells/debugger-state.ts +100 -0
  82. package/src/core/codemirror/cells/extensions.ts +23 -4
  83. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
  84. package/src/core/codemirror/go-to-definition/extension.ts +3 -3
  85. package/src/core/codemirror/go-to-definition/underline.ts +5 -14
  86. package/src/core/codemirror/go-to-definition/utils.ts +49 -1
  87. package/src/core/codemirror/utils.ts +15 -0
  88. package/src/core/config/feature-flag.tsx +2 -0
  89. package/src/core/edit-app.tsx +8 -5
  90. package/src/core/errors/errors.ts +2 -1
  91. package/src/core/islands/bootstrap.ts +1 -0
  92. package/src/core/islands/bridge.ts +1 -0
  93. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  94. package/src/core/lifecycle/render-policy.ts +125 -0
  95. package/src/core/mime.ts +11 -4
  96. package/src/core/network/__tests__/api.test.ts +17 -0
  97. package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
  98. package/src/core/network/api.ts +17 -6
  99. package/src/core/network/requests-lazy.ts +1 -0
  100. package/src/core/network/requests-network.ts +8 -0
  101. package/src/core/network/requests-static.ts +1 -0
  102. package/src/core/network/requests-toasting.tsx +10 -1
  103. package/src/core/network/types.ts +2 -0
  104. package/src/core/run-app.tsx +27 -23
  105. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  106. package/src/core/runtime/adapter.ts +182 -0
  107. package/src/core/wasm/PyodideLoader.tsx +20 -62
  108. package/src/core/wasm/bridge.ts +13 -4
  109. package/src/core/wasm/state.ts +8 -19
  110. package/src/core/websocket/useMarimoKernelConnection.tsx +17 -0
  111. package/src/css/app/codemirror.css +14 -0
  112. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  113. package/src/hooks/useDelayElapsed.ts +27 -0
  114. package/src/plugins/core/RenderHTML.tsx +35 -1
  115. package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
  116. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  117. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +53 -2
  118. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +52 -2
  119. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
  120. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
  121. package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
  122. package/src/utils/errors.ts +15 -0
  123. package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
  124. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
  125. package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
  126. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  127. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  128. package/src/core/wasm/__tests__/state.test.ts +0 -124
@@ -0,0 +1,42 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { type Atom, useAtomValue } from "jotai";
4
+ import type React from "react";
5
+ import type { PropsWithChildren } from "react";
6
+ import { useDelayElapsed } from "@/hooks/useDelayElapsed";
7
+
8
+ interface Props {
9
+ /** Children render once this atom resolves true. */
10
+ requires: Atom<boolean>;
11
+ fallback?: React.ReactNode;
12
+ /** Suppress the fallback for this many ms; avoids spinner flashes. */
13
+ delay?: number;
14
+ }
15
+
16
+ /**
17
+ * ```tsx
18
+ * <ProgressiveBoundary requires={canPaintAtom} delay={2000} fallback={<Spinner />}>
19
+ * <Editor />
20
+ * </ProgressiveBoundary>
21
+ * ```
22
+ */
23
+ export const ProgressiveBoundary: React.FC<PropsWithChildren<Props>> = ({
24
+ requires,
25
+ fallback = null,
26
+ delay = 0,
27
+ children,
28
+ }) => {
29
+ const ready = useAtomValue(requires);
30
+ // Key the delay off `ready` so the suppression window re-arms whenever the
31
+ // gate closes again — otherwise a `true → false` flip would show the
32
+ // fallback immediately and reintroduce the flash `delay` is meant to avoid.
33
+ const delayElapsed = useDelayElapsed(ready ? 0 : delay);
34
+
35
+ if (ready) {
36
+ return children;
37
+ }
38
+ if (!delayElapsed) {
39
+ return null;
40
+ }
41
+ return fallback;
42
+ };
@@ -0,0 +1,59 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useAtomValue } from "jotai";
4
+ import { AlertCircleIcon } from "lucide-react";
5
+ import type React from "react";
6
+ import { Spinner } from "@/components/icons/spinner";
7
+ import { Tooltip } from "@/components/ui/tooltip";
8
+ import { type AdapterState, runtimeAdapterAtom } from "@/core/runtime/adapter";
9
+
10
+ /**
11
+ * Footer pill that surfaces the active runtime's connection/initialization
12
+ * status via its adapter. Hides itself once the runtime is ready.
13
+ */
14
+ export const RuntimeStatusBadge: React.FC = () => {
15
+ const adapter = useAtomValue(runtimeAdapterAtom);
16
+ const state = useAtomValue(adapter.state);
17
+
18
+ if (state.kind === "ready") {
19
+ return null;
20
+ }
21
+
22
+ const icon =
23
+ state.kind === "failed" ? (
24
+ <AlertCircleIcon className="w-4 h-4 text-destructive" />
25
+ ) : (
26
+ <Spinner size="small" />
27
+ );
28
+
29
+ return (
30
+ <Tooltip
31
+ content={
32
+ <div className="text-sm whitespace-pre-line">
33
+ {tooltipFor(state, adapter.label)}
34
+ </div>
35
+ }
36
+ data-testid="footer-runtime-status"
37
+ >
38
+ <button
39
+ type="button"
40
+ className="p-1 hover:bg-accent rounded flex items-center gap-1.5 text-xs text-muted-foreground"
41
+ data-testid="runtime-status-footer"
42
+ >
43
+ {icon}
44
+ <span>{adapter.label}</span>
45
+ </button>
46
+ </Tooltip>
47
+ );
48
+ };
49
+
50
+ function tooltipFor(state: AdapterState, label: string): string {
51
+ switch (state.kind) {
52
+ case "failed":
53
+ return state.error.message;
54
+ case "connecting":
55
+ return state.progress?.label ?? `${label} starting…`;
56
+ case "ready":
57
+ return `${label} ready`;
58
+ }
59
+ }
@@ -0,0 +1,147 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, render, screen } from "@testing-library/react";
4
+ import { atom, createStore, Provider } from "jotai";
5
+ import type { ReactNode } from "react";
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { ProgressiveBoundary } from "../ProgressiveBoundary";
8
+
9
+ describe("ProgressiveBoundary", () => {
10
+ let store: ReturnType<typeof createStore>;
11
+
12
+ const wrapper = ({ children }: { children: ReactNode }) => (
13
+ <Provider store={store}>{children}</Provider>
14
+ );
15
+
16
+ beforeEach(() => {
17
+ vi.useFakeTimers();
18
+ store = createStore();
19
+ });
20
+
21
+ afterEach(() => {
22
+ vi.useRealTimers();
23
+ });
24
+
25
+ it("renders children once the capability resolves true", () => {
26
+ const capability = atom(true);
27
+ render(
28
+ <ProgressiveBoundary requires={capability}>
29
+ <span>content</span>
30
+ </ProgressiveBoundary>,
31
+ { wrapper },
32
+ );
33
+ expect(screen.getByText("content")).toBeInTheDocument();
34
+ });
35
+
36
+ it("blocks children with no fallback when capability is false", () => {
37
+ const capability = atom(false);
38
+ const { container } = render(
39
+ <ProgressiveBoundary requires={capability}>
40
+ <span>content</span>
41
+ </ProgressiveBoundary>,
42
+ { wrapper },
43
+ );
44
+ expect(container).toBeEmptyDOMElement();
45
+ });
46
+
47
+ it("renders the fallback when capability is false", () => {
48
+ const capability = atom(false);
49
+ render(
50
+ <ProgressiveBoundary requires={capability} fallback={<span>wait</span>}>
51
+ <span>content</span>
52
+ </ProgressiveBoundary>,
53
+ { wrapper },
54
+ );
55
+ expect(screen.getByText("wait")).toBeInTheDocument();
56
+ expect(screen.queryByText("content")).not.toBeInTheDocument();
57
+ });
58
+
59
+ it("renders children once a previously-false capability flips true", () => {
60
+ const capability = atom(false);
61
+ render(
62
+ <ProgressiveBoundary requires={capability} fallback={<span>wait</span>}>
63
+ <span>content</span>
64
+ </ProgressiveBoundary>,
65
+ { wrapper },
66
+ );
67
+ expect(screen.getByText("wait")).toBeInTheDocument();
68
+
69
+ act(() => store.set(capability, true));
70
+ expect(screen.getByText("content")).toBeInTheDocument();
71
+ });
72
+
73
+ it("reopens the gate when a capability flips back to false", () => {
74
+ const capability = atom(true);
75
+ render(
76
+ <ProgressiveBoundary requires={capability} fallback={<span>wait</span>}>
77
+ <span>content</span>
78
+ </ProgressiveBoundary>,
79
+ { wrapper },
80
+ );
81
+ expect(screen.getByText("content")).toBeInTheDocument();
82
+
83
+ act(() => store.set(capability, false));
84
+ expect(screen.queryByText("content")).not.toBeInTheDocument();
85
+ expect(screen.getByText("wait")).toBeInTheDocument();
86
+ });
87
+
88
+ it("delays the fallback by `delay` ms", () => {
89
+ const capability = atom(false);
90
+ render(
91
+ <ProgressiveBoundary
92
+ requires={capability}
93
+ delay={2000}
94
+ fallback={<span>wait</span>}
95
+ >
96
+ <span>content</span>
97
+ </ProgressiveBoundary>,
98
+ { wrapper },
99
+ );
100
+ // Before the delay elapses, no fallback.
101
+ expect(screen.queryByText("wait")).not.toBeInTheDocument();
102
+
103
+ act(() => vi.advanceTimersByTime(2000));
104
+ expect(screen.getByText("wait")).toBeInTheDocument();
105
+ });
106
+
107
+ it("re-arms the delay when the gate closes again", () => {
108
+ const capability = atom(true);
109
+ render(
110
+ <ProgressiveBoundary
111
+ requires={capability}
112
+ delay={2000}
113
+ fallback={<span>wait</span>}
114
+ >
115
+ <span>content</span>
116
+ </ProgressiveBoundary>,
117
+ { wrapper },
118
+ );
119
+ expect(screen.getByText("content")).toBeInTheDocument();
120
+
121
+ // Gate closes: fallback should stay suppressed until the delay re-elapses.
122
+ act(() => store.set(capability, false));
123
+ expect(screen.queryByText("wait")).not.toBeInTheDocument();
124
+ expect(screen.queryByText("content")).not.toBeInTheDocument();
125
+
126
+ act(() => vi.advanceTimersByTime(2000));
127
+ expect(screen.getByText("wait")).toBeInTheDocument();
128
+ });
129
+
130
+ it("skips the fallback entirely if the gate opens before the delay", () => {
131
+ const capability = atom(false);
132
+ render(
133
+ <ProgressiveBoundary
134
+ requires={capability}
135
+ delay={5000}
136
+ fallback={<span>wait</span>}
137
+ >
138
+ <span>content</span>
139
+ </ProgressiveBoundary>,
140
+ { wrapper },
141
+ );
142
+ expect(screen.queryByText("wait")).not.toBeInTheDocument();
143
+
144
+ act(() => store.set(capability, true));
145
+ expect(screen.getByText("content")).toBeInTheDocument();
146
+ });
147
+ });
@@ -0,0 +1,72 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render, screen } from "@testing-library/react";
4
+ import { atom, createStore, Provider } from "jotai";
5
+ import type { ReactNode } from "react";
6
+ import { beforeEach, describe, expect, it } from "vitest";
7
+ import { TooltipProvider } from "@/components/ui/tooltip";
8
+ import {
9
+ type AdapterState,
10
+ type RuntimeAdapter,
11
+ runtimeAdapterAtom,
12
+ } from "@/core/runtime/adapter";
13
+ import { RuntimeStatusBadge } from "../RuntimeStatusBadge";
14
+
15
+ function makeAdapter(initial: AdapterState): RuntimeAdapter {
16
+ return {
17
+ kind: "remote",
18
+ label: "Kernel",
19
+ capabilities: {
20
+ canHealthCheck: true,
21
+ canShutdown: true,
22
+ canRestart: true,
23
+ supportsLsp: true,
24
+ },
25
+ state: atom<AdapterState>(initial),
26
+ };
27
+ }
28
+
29
+ let store: ReturnType<typeof createStore>;
30
+
31
+ const wrapper = ({ children }: { children: ReactNode }) => (
32
+ <Provider store={store}>
33
+ <TooltipProvider>{children}</TooltipProvider>
34
+ </Provider>
35
+ );
36
+
37
+ beforeEach(() => {
38
+ store = createStore();
39
+ });
40
+
41
+ describe("RuntimeStatusBadge", () => {
42
+ it("hides itself when the runtime is ready", () => {
43
+ store.set(runtimeAdapterAtom, makeAdapter({ kind: "ready" }));
44
+ const { container } = render(<RuntimeStatusBadge />, { wrapper });
45
+ expect(container).toBeEmptyDOMElement();
46
+ });
47
+
48
+ it("shows a labeled spinner pill while connecting", () => {
49
+ store.set(
50
+ runtimeAdapterAtom,
51
+ makeAdapter({
52
+ kind: "connecting",
53
+ progress: { label: "Loading Pyodide…" },
54
+ }),
55
+ );
56
+ render(<RuntimeStatusBadge />, { wrapper });
57
+ expect(screen.getByTestId("runtime-status-footer")).toBeInTheDocument();
58
+ expect(screen.getByText("Kernel")).toBeInTheDocument();
59
+ });
60
+
61
+ it("shows the failure icon when the runtime fails", () => {
62
+ store.set(
63
+ runtimeAdapterAtom,
64
+ makeAdapter({
65
+ kind: "failed",
66
+ error: { message: "Boom", errorKind: "init" },
67
+ }),
68
+ );
69
+ render(<RuntimeStatusBadge />, { wrapper });
70
+ expect(screen.getByTestId("runtime-status-footer")).toBeInTheDocument();
71
+ });
72
+ });
@@ -0,0 +1,33 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render } from "@testing-library/react";
4
+ import { describe, expect, it } from "vitest";
5
+ import type { CellId } from "@/core/cells/ids";
6
+ import type { OutputMessage } from "@/core/kernel/messages";
7
+ import type { Seconds } from "@/utils/time";
8
+ import { Slide } from "../slide";
9
+
10
+ const cellId = "cell-1" as CellId;
11
+
12
+ const output: OutputMessage = {
13
+ channel: "output",
14
+ mimetype: "text/plain",
15
+ data: "hello",
16
+ timestamp: 0 as Seconds,
17
+ };
18
+
19
+ describe("Slide", () => {
20
+ it("does not grey out the output when stale is false", () => {
21
+ const { container } = render(
22
+ <Slide cellId={cellId} status="running" output={output} stale={false} />,
23
+ );
24
+ expect(container.querySelector(".marimo-output-stale")).toBeNull();
25
+ });
26
+
27
+ it("greys out the output when stale is true", () => {
28
+ const { container } = render(
29
+ <Slide cellId={cellId} status="queued" output={output} stale={true} />,
30
+ );
31
+ expect(container.querySelector(".marimo-output-stale")).not.toBeNull();
32
+ });
33
+ });
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { useDeleteCellCallback } from "@/components/editor/cell/useDeleteCell";
4
+ import { outputIsStale } from "@/core/cells/cell";
4
5
  import { useCellActions, useCellIds } from "@/core/cells/cells";
5
6
  import type { CellId } from "@/core/cells/ids";
6
7
  import type { CellColumnId } from "@/utils/id-tree";
@@ -670,7 +671,12 @@ const SlideThumbnailCard = ({
670
671
  {isNoOutput ? (
671
672
  <MiniCodePreview code={cell.code} />
672
673
  ) : (
673
- <Slide cellId={cell.id} status={cell.status} output={cell.output} />
674
+ <Slide
675
+ cellId={cell.id}
676
+ status={cell.status}
677
+ output={cell.output}
678
+ stale={outputIsStale(cell, false)}
679
+ />
674
680
  )}
675
681
  </div>
676
682
  )}
@@ -16,6 +16,7 @@ import { Slide as CellOutputSlide } from "@/components/slides/slide";
16
16
  import { Button } from "@/components/ui/button";
17
17
  import { useFullScreenElement } from "@/components/ui/fullscreen";
18
18
  import { Tooltip } from "@/components/ui/tooltip";
19
+ import { outputIsStale } from "@/core/cells/cell";
19
20
  import type { CellId } from "@/core/cells/ids";
20
21
  import type { RuntimeCell } from "@/core/cells/types";
21
22
  import type { RevealApi, RevealConfig } from "reveal.js";
@@ -333,6 +334,7 @@ const SubslideView = ({
333
334
  cellId={cell.id}
334
335
  status={cell.status}
335
336
  output={cell.output}
337
+ stale={outputIsStale(cell, false)}
336
338
  />
337
339
  );
338
340
  }
@@ -397,6 +399,7 @@ const ParkedPreviewContent = ({
397
399
  cellId={cell.id}
398
400
  status={cell.status}
399
401
  output={cell.output}
402
+ stale={outputIsStale(cell, false)}
400
403
  />
401
404
  );
402
405
  };
@@ -13,6 +13,7 @@ import { StopButton } from "@/components/editor/cell/StopButton";
13
13
  import { useRunCell } from "@/components/editor/cell/useRunCells";
14
14
  import { Slide as CellOutputSlide } from "@/components/slides/slide";
15
15
  import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
16
+ import { outputIsStale } from "@/core/cells/cell";
16
17
  import { useCellActions } from "@/core/cells/cells";
17
18
  import { autoInstantiateAtom, useUserConfig } from "@/core/config/config";
18
19
  import {
@@ -26,7 +27,7 @@ import { connectionAtom } from "@/core/network/connection";
26
27
  import { useTheme } from "@/theme/useTheme";
27
28
  import { cn } from "@/utils/cn";
28
29
  import { ReadonlyCode } from "../editor/code/readonly-python-code";
29
- import { languageAdapterFromCode } from "@/core/codemirror/language/extension";
30
+ import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
30
31
 
31
32
  type RuntimeCell = CellRuntimeState & CellData;
32
33
 
@@ -96,6 +97,7 @@ export const SlideCellView = ({ cell }: { cell: RuntimeCell }) => {
96
97
  cellId={cell.id}
97
98
  status={cell.status}
98
99
  output={cell.output}
100
+ stale={outputIsStale(cell, false)}
99
101
  />
100
102
  );
101
103
 
@@ -188,22 +190,24 @@ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
188
190
  const [userConfig] = useUserConfig();
189
191
  const cellOutputPosition = userConfig.display.cell_output;
190
192
 
191
- const language = useMemo(() => {
192
- const adapter = languageAdapterFromCode(cell.code.trim());
193
- return adapter.type === "sql" ? "sql" : "python";
194
- }, [cell.code]);
193
+ const display = useMemo(() => getReadonlyCodeDisplay(cell.code), [cell.code]);
195
194
 
196
195
  const output = (
197
196
  <CellOutputSlide
198
197
  cellId={cell.id}
199
198
  status={cell.status}
200
199
  output={cell.output}
200
+ stale={outputIsStale(cell, false)}
201
201
  />
202
202
  );
203
203
 
204
204
  const editor = (
205
205
  <div className="marimo-cell">
206
- <ReadonlyCode code={cell.code} language={language} showHideCode={false} />
206
+ <ReadonlyCode
207
+ code={display.code}
208
+ language={display.language}
209
+ showHideCode={false}
210
+ />
207
211
  </div>
208
212
  );
209
213
 
@@ -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";
@@ -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