@marimo-team/islands 0.23.14-dev2 → 0.23.14-dev23
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.
- package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-DXBx_nQg.js} +4 -4
- package/dist/{chat-ui-CsPewo4h.js → chat-ui-CO8WuXGb.js} +3243 -3039
- package/dist/{code-visibility-CRYdBxcA.js → code-visibility-B3ntFQLc.js} +1115 -917
- package/dist/{formats-d6MhLuQ9.js → formats-WsOgyW_K.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-qpmKyAPn.js} +2 -2
- package/dist/{html-to-image-DXwLcQ6l.js → html-to-image-DLSOS-bE.js} +2351 -2282
- package/dist/{input-CbEz_aj_.js → input-BSdZp5Ng.js} +1 -1
- package/dist/main.js +1139 -1071
- package/dist/{mermaid-CJW9vIyO.js → mermaid-D-HYBMEV.js} +2 -2
- package/dist/{process-output-C6_e1pT_.js → process-output-B9ysqk1y.js} +1 -1
- package/dist/{reveal-component-CbqUMhp8.js → reveal-component-C8j14RKU.js} +16 -16
- package/dist/{spec-Bv-XlYiv.js → spec-CnTgI25l.js} +1 -1
- package/dist/{toDate-D-l5s8nn.js → toDate-D1Z7ZXWh.js} +1 -1
- package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-BMc8itk2.js} +1 -1
- package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-ZwmDBRDY.js} +1 -1
- package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-CxffarYV.js} +1 -1
- package/dist/{useTheme-BrYvK-_A.js → useTheme-yGsGEk82.js} +26 -24
- package/dist/{vega-component-Pk6lyc_a.js → vega-component-BFJTyykA.js} +5 -5
- package/package.json +2 -2
- package/src/components/chat/acp/agent-panel.tsx +35 -1
- package/src/components/chat/chat-panel.tsx +68 -29
- package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
- package/src/components/editor/actions/useNotebookActions.tsx +2 -2
- package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -2
- package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
- package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
- package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
- package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
- package/src/components/editor/code/readonly-python-code.tsx +16 -2
- package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
- package/src/components/editor/errors/auto-fix.tsx +88 -34
- package/src/components/editor/errors/fix-mode.ts +1 -1
- package/src/components/editor/output/MarimoTracebackOutput.tsx +10 -1
- package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +6 -2
- package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
- package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
- package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
- package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
- package/src/components/slides/slide-cell-view.tsx +7 -6
- package/src/core/ai/config.ts +2 -2
- package/src/core/ai/context/__tests__/registry.test.ts +133 -3
- package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
- package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
- package/src/core/ai/context/providers/datasource.ts +27 -2
- package/src/core/ai/context/providers/error.ts +226 -36
- package/src/core/ai/context/registry.ts +77 -43
- package/src/core/ai/state.ts +11 -0
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/readonly-code-display.ts +35 -0
- package/src/core/codemirror/__tests__/setup.test.ts +33 -1
- package/src/core/codemirror/ai/resources.ts +15 -10
- package/src/core/codemirror/cells/extensions.ts +1 -4
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +94 -0
- package/src/core/codemirror/completion/completer.ts +12 -1
- package/src/core/codemirror/completion/signature-hint.ts +68 -0
- package/src/core/codemirror/language/languages/python.ts +10 -4
- package/src/core/codemirror/utils.ts +15 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
- package/src/core/lifecycle/render-policy.ts +125 -0
- package/src/core/mime.ts +11 -4
- package/src/core/run-app.tsx +27 -23
- package/src/core/runtime/__tests__/adapter.test.ts +160 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/wasm/PyodideLoader.tsx +20 -62
- package/src/core/wasm/bridge.ts +9 -4
- package/src/core/wasm/state.ts +8 -19
- package/src/core/wasm/store.ts +1 -1
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
- package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
- package/src/core/wasm/__tests__/state.test.ts +0 -124
- /package/dist/{files → export_demos}/wasm-intro.py +0 -0
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { render } from "@testing-library/react";
|
|
4
|
+
import { Provider } from "jotai";
|
|
4
5
|
import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
|
6
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
5
7
|
import { Tracebacks } from "@/__mocks__/tracebacks";
|
|
6
8
|
import { cellId } from "@/__tests__/branded";
|
|
7
9
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
8
10
|
import { initialModeAtom } from "@/core/mode";
|
|
11
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
9
12
|
import { store } from "@/core/state/jotai";
|
|
10
13
|
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
11
14
|
import {
|
|
@@ -20,13 +23,16 @@ describe("traceback component", () => {
|
|
|
20
23
|
beforeEach(() => {
|
|
21
24
|
vi.resetAllMocks();
|
|
22
25
|
store.set(initialModeAtom, "edit");
|
|
26
|
+
store.set(requestClientAtom, MockRequestClient.create());
|
|
23
27
|
});
|
|
24
28
|
|
|
25
29
|
test("extracts cell-link", () => {
|
|
26
30
|
const traceback = (
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
31
|
+
<Provider store={store}>
|
|
32
|
+
<TooltipProvider>
|
|
33
|
+
<MarimoTracebackOutput traceback={Tracebacks.raw} cellId={cid} />
|
|
34
|
+
</TooltipProvider>
|
|
35
|
+
</Provider>
|
|
30
36
|
);
|
|
31
37
|
const { unmount, getAllByRole } = render(traceback);
|
|
32
38
|
|
|
@@ -44,9 +50,11 @@ describe("traceback component", () => {
|
|
|
44
50
|
|
|
45
51
|
test("renames File to Cell for relevant lines", () => {
|
|
46
52
|
const traceback = (
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
53
|
+
<Provider store={store}>
|
|
54
|
+
<TooltipProvider>
|
|
55
|
+
<MarimoTracebackOutput traceback={Tracebacks.raw} cellId={cid} />
|
|
56
|
+
</TooltipProvider>
|
|
57
|
+
</Provider>
|
|
50
58
|
);
|
|
51
59
|
const { unmount, container } = render(traceback);
|
|
52
60
|
|
|
@@ -29,6 +29,7 @@ import { outputIsLoading, outputIsStale } from "@/core/cells/cell";
|
|
|
29
29
|
import type { CellId } from "@/core/cells/ids";
|
|
30
30
|
import { isOutputEmpty } from "@/core/cells/outputs";
|
|
31
31
|
import type { CellData, CellRuntimeState } from "@/core/cells/types";
|
|
32
|
+
import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
|
|
32
33
|
import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
|
|
33
34
|
import { useResolvedMarimoConfig } from "@/core/config/config";
|
|
34
35
|
import { CSSClasses, KnownQueryParams } from "@/core/constants";
|
|
@@ -384,6 +385,8 @@ const VerticalCell = memo(
|
|
|
384
385
|
|
|
385
386
|
// Hide the code if it's pure markdown and there's an output, or if the code is empty
|
|
386
387
|
const hideCode = shouldHideCode(code, output);
|
|
388
|
+
// Only unwrap SQL when the code will actually be rendered.
|
|
389
|
+
const display = hideCode ? null : getReadonlyCodeDisplay(code);
|
|
387
390
|
|
|
388
391
|
return (
|
|
389
392
|
<div
|
|
@@ -393,11 +396,12 @@ const VerticalCell = memo(
|
|
|
393
396
|
{...cellDomProps(cellId, name)}
|
|
394
397
|
>
|
|
395
398
|
{cellOutputArea === "above" && outputArea}
|
|
396
|
-
{
|
|
399
|
+
{display && (
|
|
397
400
|
<div className="tray">
|
|
398
401
|
<ReadonlyCode
|
|
399
402
|
initiallyHideCode={config.hide_code || kiosk}
|
|
400
|
-
code={code}
|
|
403
|
+
code={display.code}
|
|
404
|
+
language={display.language}
|
|
401
405
|
/>
|
|
402
406
|
</div>
|
|
403
407
|
)}
|
|
@@ -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
|
+
});
|
|
@@ -26,7 +26,7 @@ import { connectionAtom } from "@/core/network/connection";
|
|
|
26
26
|
import { useTheme } from "@/theme/useTheme";
|
|
27
27
|
import { cn } from "@/utils/cn";
|
|
28
28
|
import { ReadonlyCode } from "../editor/code/readonly-python-code";
|
|
29
|
-
import {
|
|
29
|
+
import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
|
|
30
30
|
|
|
31
31
|
type RuntimeCell = CellRuntimeState & CellData;
|
|
32
32
|
|
|
@@ -188,10 +188,7 @@ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
|
|
|
188
188
|
const [userConfig] = useUserConfig();
|
|
189
189
|
const cellOutputPosition = userConfig.display.cell_output;
|
|
190
190
|
|
|
191
|
-
const
|
|
192
|
-
const adapter = languageAdapterFromCode(cell.code.trim());
|
|
193
|
-
return adapter.type === "sql" ? "sql" : "python";
|
|
194
|
-
}, [cell.code]);
|
|
191
|
+
const display = useMemo(() => getReadonlyCodeDisplay(cell.code), [cell.code]);
|
|
195
192
|
|
|
196
193
|
const output = (
|
|
197
194
|
<CellOutputSlide
|
|
@@ -203,7 +200,11 @@ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
|
|
|
203
200
|
|
|
204
201
|
const editor = (
|
|
205
202
|
<div className="marimo-cell">
|
|
206
|
-
<ReadonlyCode
|
|
203
|
+
<ReadonlyCode
|
|
204
|
+
code={display.code}
|
|
205
|
+
language={display.language}
|
|
206
|
+
showHideCode={false}
|
|
207
|
+
/>
|
|
207
208
|
</div>
|
|
208
209
|
);
|
|
209
210
|
|
package/src/core/ai/config.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
|
638
|
+
const store = createStore();
|
|
639
|
+
const context = getDatasourceContext(cellId("1"), store);
|
|
638
640
|
expect(context).toBeNull();
|
|
639
641
|
});
|
|
640
642
|
});
|