@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.
- package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
- package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
- package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
- package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
- package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
- package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
- package/dist/{formats-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
- package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
- package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
- package/dist/main.js +3630 -1801
- package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.js} +2 -2
- package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
- package/dist/{process-output-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
- package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
- package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
- package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
- package/package.json +4 -4
- package/src/__mocks__/requests.ts +1 -0
- package/src/__tests__/CellStatus.test.tsx +1 -2
- package/src/components/app-config/user-config-form.tsx +52 -0
- 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/charts/__tests__/altair-generator.test.ts +24 -0
- package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
- package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
- package/src/components/data-table/charts/charts.tsx +23 -1
- 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/debugger/debugger-code.tsx +7 -2
- package/src/components/editor/actions/useNotebookActions.tsx +2 -2
- package/src/components/editor/cell/CellStatus.tsx +1 -20
- package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
- package/src/components/editor/cell/cell-context-menu.tsx +2 -2
- package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
- 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/__tests__/readonly-python-code.test.tsx +79 -0
- package/src/components/editor/code/readonly-python-code.tsx +60 -33
- 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/notebook-cell.tsx +7 -0
- package/src/components/editor/output/ImageOutput.tsx +20 -6
- package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
- package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
- package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
- 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/__tests__/slide.test.tsx +33 -0
- package/src/components/slides/minimap.tsx +7 -1
- package/src/components/slides/reveal-component.tsx +3 -0
- package/src/components/slides/slide-cell-view.tsx +10 -6
- package/src/components/slides/slide.tsx +16 -13
- package/src/components/tracing/tracing.tsx +2 -1
- 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__/cell.test.ts +39 -0
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/cell.ts +5 -3
- 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/__tests__/debugger-decorations.test.ts +185 -0
- package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
- package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
- package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
- package/src/core/codemirror/cells/debugger-state.ts +133 -0
- package/src/core/codemirror/cells/extensions.ts +32 -4
- package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
- package/src/core/codemirror/go-to-definition/extension.ts +3 -3
- package/src/core/codemirror/go-to-definition/underline.ts +5 -14
- package/src/core/codemirror/go-to-definition/utils.ts +49 -1
- package/src/core/codemirror/utils.ts +15 -0
- package/src/core/config/__tests__/config-schema.test.ts +17 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +10 -0
- package/src/core/config/feature-flag.tsx +4 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/islands/bootstrap.ts +4 -3
- package/src/core/islands/bridge.ts +1 -0
- 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/network/__tests__/api.test.ts +17 -0
- package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
- package/src/core/network/api.ts +17 -6
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +10 -1
- package/src/core/network/types.ts +2 -0
- package/src/core/run-app.tsx +27 -23
- package/src/core/runtime/__tests__/adapter.test.ts +160 -0
- package/src/core/runtime/__tests__/runtime.test.ts +45 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/runtime/runtime.ts +30 -16
- package/src/core/wasm/PyodideLoader.tsx +20 -62
- package/src/core/wasm/bridge.ts +13 -4
- package/src/core/wasm/state.ts +8 -19
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
- package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
- package/src/core/websocket/transports/sse.ts +308 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
- package/src/core/websocket/useWebSocket.tsx +12 -2
- package/src/css/app/codemirror.css +14 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/mount.tsx +3 -3
- package/src/plugins/core/RenderHTML.tsx +35 -1
- package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
- package/src/plugins/impl/DataTablePlugin.tsx +1 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
- package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
- package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
- package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
- package/src/plugins/impl/anywidget/host.ts +54 -0
- package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
- package/src/plugins/impl/anywidget/model.ts +59 -239
- package/src/plugins/impl/anywidget/registry.ts +268 -0
- package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
- package/src/plugins/impl/anywidget/runtime.ts +422 -0
- package/src/plugins/impl/anywidget/types.ts +14 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
- package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
- package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
- package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
- package/src/utils/errors.ts +15 -0
- package/src/utils/time.ts +20 -0
- package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
- package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
- package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
- package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
- 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/src/plugins/impl/anywidget/schemas.ts +0 -32
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { ImageOutput } from "../ImageOutput";
|
|
6
|
+
|
|
7
|
+
const src = "data:image/png;base64,iVBORw0KGgo=";
|
|
8
|
+
|
|
9
|
+
describe("ImageOutput", () => {
|
|
10
|
+
it("renders numeric dimensions as HTML attributes", () => {
|
|
11
|
+
// Attributes (not inline styles) let stylesheet rules like
|
|
12
|
+
// `max-width: 100%; height: auto` shrink the image proportionally,
|
|
13
|
+
// preserving its aspect ratio in width-constrained containers.
|
|
14
|
+
const { container } = render(
|
|
15
|
+
<ImageOutput src={src} width={640} height={480} />,
|
|
16
|
+
);
|
|
17
|
+
const img = container.querySelector("img");
|
|
18
|
+
expect(img).toHaveAttribute("width", "640");
|
|
19
|
+
expect(img).toHaveAttribute("height", "480");
|
|
20
|
+
expect(img?.style.width).toBe("");
|
|
21
|
+
expect(img?.style.height).toBe("");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders string dimensions as inline styles", () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<ImageOutput src={src} width="50%" height="100%" />,
|
|
27
|
+
);
|
|
28
|
+
const img = container.querySelector("img");
|
|
29
|
+
expect(img).not.toHaveAttribute("width");
|
|
30
|
+
expect(img).not.toHaveAttribute("height");
|
|
31
|
+
expect(img?.style.width).toBe("50%");
|
|
32
|
+
expect(img?.style.height).toBe("100%");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("supports mixed numeric and string dimensions", () => {
|
|
36
|
+
const { container } = render(
|
|
37
|
+
<ImageOutput src={src} width={640} height="100%" />,
|
|
38
|
+
);
|
|
39
|
+
const img = container.querySelector("img");
|
|
40
|
+
expect(img).toHaveAttribute("width", "640");
|
|
41
|
+
expect(img).not.toHaveAttribute("height");
|
|
42
|
+
expect(img?.style.height).toBe("100%");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("renders without dimensions", () => {
|
|
46
|
+
const { container } = render(<ImageOutput src={src} />);
|
|
47
|
+
const img = container.querySelector("img");
|
|
48
|
+
expect(img).not.toBeNull();
|
|
49
|
+
expect(img).not.toHaveAttribute("width");
|
|
50
|
+
expect(img).not.toHaveAttribute("height");
|
|
51
|
+
expect(img).toHaveAttribute("src", src);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -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
|
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
import { Label } from "@/components/ui/label";
|
|
36
36
|
import { NumberField } from "@/components/ui/number-field";
|
|
37
37
|
import { Switch } from "@/components/ui/switch";
|
|
38
|
-
import { outputIsLoading } from "@/core/cells/cell";
|
|
38
|
+
import { outputIsLoading, outputIsStale } from "@/core/cells/cell";
|
|
39
39
|
import type { CellId } from "@/core/cells/ids";
|
|
40
40
|
import type { AppMode } from "@/core/mode";
|
|
41
41
|
import { useIsDragging } from "@/hooks/useIsDragging";
|
|
@@ -222,6 +222,7 @@ export const GridLayoutRenderer: React.FC<Props> = ({
|
|
|
222
222
|
cellId={cell.id}
|
|
223
223
|
output={cell.output}
|
|
224
224
|
status={cell.status}
|
|
225
|
+
stale={outputIsStale(cell, false)}
|
|
225
226
|
isScrollable={isScrollable}
|
|
226
227
|
side={side}
|
|
227
228
|
hidden={cell.errored || cell.interrupted || cell.stopped}
|
|
@@ -288,6 +289,7 @@ export const GridLayoutRenderer: React.FC<Props> = ({
|
|
|
288
289
|
cellId={cell.id}
|
|
289
290
|
output={cell.output}
|
|
290
291
|
status={cell.status}
|
|
292
|
+
stale={outputIsStale(cell, false)}
|
|
291
293
|
isScrollable={false}
|
|
292
294
|
hidden={false}
|
|
293
295
|
/>
|
|
@@ -358,6 +360,7 @@ export const GridLayoutRenderer: React.FC<Props> = ({
|
|
|
358
360
|
output={cell.output}
|
|
359
361
|
isScrollable={false}
|
|
360
362
|
status={cell.status}
|
|
363
|
+
stale={outputIsStale(cell, false)}
|
|
361
364
|
hidden={false}
|
|
362
365
|
/>
|
|
363
366
|
</div>
|
|
@@ -376,6 +379,7 @@ interface GridCellProps extends Pick<CellRuntimeState, "output" | "status"> {
|
|
|
376
379
|
hidden: boolean;
|
|
377
380
|
isScrollable: boolean;
|
|
378
381
|
side?: GridLayoutCellSide;
|
|
382
|
+
stale: boolean;
|
|
379
383
|
}
|
|
380
384
|
|
|
381
385
|
const GridCell = memo(
|
|
@@ -389,6 +393,7 @@ const GridCell = memo(
|
|
|
389
393
|
isScrollable,
|
|
390
394
|
side,
|
|
391
395
|
className,
|
|
396
|
+
stale,
|
|
392
397
|
}: GridCellProps) => {
|
|
393
398
|
const loading = outputIsLoading(status);
|
|
394
399
|
|
|
@@ -415,7 +420,7 @@ const GridCell = memo(
|
|
|
415
420
|
allowExpand={false}
|
|
416
421
|
output={output}
|
|
417
422
|
cellId={cellId}
|
|
418
|
-
stale={
|
|
423
|
+
stale={stale}
|
|
419
424
|
loading={loading}
|
|
420
425
|
/>
|
|
421
426
|
</div>
|
|
@@ -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
|
-
initiallyHideCode={config.hide_code
|
|
400
|
-
code={code}
|
|
402
|
+
initiallyHideCode={config.hide_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
|
+
});
|
|
@@ -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
|
|
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 {
|
|
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
|
|
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
|
|
206
|
+
<ReadonlyCode
|
|
207
|
+
code={display.code}
|
|
208
|
+
language={display.language}
|
|
209
|
+
showHideCode={false}
|
|
210
|
+
/>
|
|
207
211
|
</div>
|
|
208
212
|
);
|
|
209
213
|
|