@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { useAtom, useSetAtom } from "jotai";
|
|
3
|
+
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
4
4
|
import { useRef } from "react";
|
|
5
5
|
import { useErrorBoundary } from "react-error-boundary";
|
|
6
6
|
import { toast } from "@/components/ui/use-toast";
|
|
@@ -16,8 +16,8 @@ import { useConnectionTransport } from "@/core/websocket/useWebSocket";
|
|
|
16
16
|
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
17
17
|
import {
|
|
18
18
|
handleWidgetMessage,
|
|
19
|
-
|
|
20
|
-
} from "@/plugins/impl/anywidget/
|
|
19
|
+
WIDGET_REGISTRY,
|
|
20
|
+
} from "@/plugins/impl/anywidget/registry";
|
|
21
21
|
import { logNever } from "@/utils/assertNever";
|
|
22
22
|
import { prettyError } from "@/utils/errors";
|
|
23
23
|
import {
|
|
@@ -32,9 +32,13 @@ import { cacheInfoAtom } from "../cache/requests";
|
|
|
32
32
|
import { SCRATCH_CELL_ID } from "../cells/ids";
|
|
33
33
|
import { useRunsActions } from "../cells/runs";
|
|
34
34
|
import { focusAndScrollCellOutputIntoView } from "../cells/scrollCellIntoView";
|
|
35
|
+
import {
|
|
36
|
+
resyncBreakpoints,
|
|
37
|
+
setActiveLine,
|
|
38
|
+
} from "../codemirror/cells/debugger-state";
|
|
35
39
|
import type { CellData } from "../cells/types";
|
|
36
40
|
import { capabilitiesAtom } from "../config/capabilities";
|
|
37
|
-
import { useSetAppConfig } from "../config/config";
|
|
41
|
+
import { connectionTransportTypeAtom, useSetAppConfig } from "../config/config";
|
|
38
42
|
import { useDataSourceActions } from "../datasets/data-source-connections";
|
|
39
43
|
import type { ConnectionName } from "../datasets/engines";
|
|
40
44
|
import {
|
|
@@ -80,13 +84,18 @@ const SUPPORTS_LAZY_KERNELS = true;
|
|
|
80
84
|
|
|
81
85
|
// All MARIMO_* reasons except TRANSPORT_EXHAUSTED are emitted by the backend
|
|
82
86
|
// (marimo/_server/api/endpoints/ws_endpoint.py and ws/*.py). Keep in sync with
|
|
83
|
-
// the backend literals.
|
|
87
|
+
// the backend literals. Over the SSE transport these arrive as in-band
|
|
88
|
+
// `close` events rather than WebSocket close frames, so every terminal
|
|
89
|
+
// reason must be classified here — an unclassified reason falls into the
|
|
90
|
+
// retry path.
|
|
84
91
|
export type CloseReason =
|
|
85
92
|
| "MARIMO_NO_FILE_KEY"
|
|
86
93
|
| "MARIMO_NO_SESSION_ID"
|
|
87
94
|
| "MARIMO_NO_SESSION"
|
|
88
95
|
| "MARIMO_SHUTDOWN"
|
|
89
96
|
| "MARIMO_KERNEL_STARTUP_ERROR"
|
|
97
|
+
| "MARIMO_UNAUTHORIZED"
|
|
98
|
+
| "MARIMO_KIOSK_NOT_ALLOWED"
|
|
90
99
|
| typeof TRANSPORT_EXHAUSTED_REASON;
|
|
91
100
|
|
|
92
101
|
export type CloseDecision =
|
|
@@ -118,6 +127,26 @@ export function classifyCloseEvent(event: { reason?: string }): CloseDecision {
|
|
|
118
127
|
},
|
|
119
128
|
closeTransport: true,
|
|
120
129
|
};
|
|
130
|
+
case "MARIMO_UNAUTHORIZED":
|
|
131
|
+
return {
|
|
132
|
+
kind: "terminal",
|
|
133
|
+
status: {
|
|
134
|
+
state: WebSocketState.CLOSED,
|
|
135
|
+
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
136
|
+
reason: "not authorized",
|
|
137
|
+
},
|
|
138
|
+
closeTransport: true,
|
|
139
|
+
};
|
|
140
|
+
case "MARIMO_KIOSK_NOT_ALLOWED":
|
|
141
|
+
return {
|
|
142
|
+
kind: "terminal",
|
|
143
|
+
status: {
|
|
144
|
+
state: WebSocketState.CLOSED,
|
|
145
|
+
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
146
|
+
reason: "kiosk mode is not available for this session",
|
|
147
|
+
},
|
|
148
|
+
closeTransport: true,
|
|
149
|
+
};
|
|
121
150
|
case "MARIMO_KERNEL_STARTUP_ERROR":
|
|
122
151
|
return {
|
|
123
152
|
kind: "terminal",
|
|
@@ -194,6 +223,7 @@ export function useMarimoKernelConnection(opts: {
|
|
|
194
223
|
const setKioskMode = useSetAtom(kioskModeAtom);
|
|
195
224
|
const setCapabilities = useSetAtom(capabilitiesAtom);
|
|
196
225
|
const runtimeManager = useRuntimeManager();
|
|
226
|
+
const transportType = useAtomValue(connectionTransportTypeAtom);
|
|
197
227
|
const setCacheInfo = useSetAtom(cacheInfoAtom);
|
|
198
228
|
const setKernelStartupError = useSetAtom(kernelStartupErrorAtom);
|
|
199
229
|
const {
|
|
@@ -221,6 +251,11 @@ export function useMarimoKernelConnection(opts: {
|
|
|
221
251
|
existingCells,
|
|
222
252
|
});
|
|
223
253
|
setKioskMode(msg.data.kiosk);
|
|
254
|
+
// A freshly started kernel has no breakpoints of its own; re-push
|
|
255
|
+
// the client's set so they still apply, and clear the stale
|
|
256
|
+
// highlighted line from the previous kernel instance.
|
|
257
|
+
setActiveLine(null);
|
|
258
|
+
resyncBreakpoints();
|
|
224
259
|
return;
|
|
225
260
|
}
|
|
226
261
|
|
|
@@ -248,7 +283,7 @@ export function useMarimoKernelConnection(opts: {
|
|
|
248
283
|
}
|
|
249
284
|
|
|
250
285
|
case "model-lifecycle":
|
|
251
|
-
handleWidgetMessage(
|
|
286
|
+
handleWidgetMessage(WIDGET_REGISTRY, msg.data);
|
|
252
287
|
return;
|
|
253
288
|
|
|
254
289
|
case "remove-ui-elements":
|
|
@@ -392,6 +427,13 @@ export function useMarimoKernelConnection(opts: {
|
|
|
392
427
|
case "focus-cell":
|
|
393
428
|
focusAndScrollCellOutputIntoView(msg.data.cell_id);
|
|
394
429
|
return;
|
|
430
|
+
case "active-line":
|
|
431
|
+
setActiveLine(
|
|
432
|
+
msg.data.line == null
|
|
433
|
+
? null
|
|
434
|
+
: { cellId: msg.data.cell_id, line: msg.data.line },
|
|
435
|
+
);
|
|
436
|
+
return;
|
|
395
437
|
case "notebook-document-transaction":
|
|
396
438
|
handleDocumentTransaction(msg.data.transaction);
|
|
397
439
|
return;
|
|
@@ -439,10 +481,19 @@ export function useMarimoKernelConnection(opts: {
|
|
|
439
481
|
|
|
440
482
|
const ws = useConnectionTransport({
|
|
441
483
|
static: isStaticNotebook(),
|
|
484
|
+
transportType,
|
|
442
485
|
/**
|
|
443
486
|
* Unique URL for this session.
|
|
444
487
|
*/
|
|
445
|
-
url: () =>
|
|
488
|
+
url: () =>
|
|
489
|
+
transportType === "sse"
|
|
490
|
+
? runtimeManager.getSseURL(sessionId).toString()
|
|
491
|
+
: runtimeManager.getWsURL(sessionId).toString(),
|
|
492
|
+
/**
|
|
493
|
+
* Auth headers for the SSE transport. WebSockets cannot send headers
|
|
494
|
+
* and instead put the access token in the URL.
|
|
495
|
+
*/
|
|
496
|
+
headers: () => runtimeManager.headers(),
|
|
446
497
|
|
|
447
498
|
/**
|
|
448
499
|
* Open callback. Set the connection status to open.
|
|
@@ -5,12 +5,16 @@ import { Logger } from "@/utils/Logger";
|
|
|
5
5
|
import { createPyodideConnection } from "../wasm/bridge";
|
|
6
6
|
import { isWasm } from "../wasm/utils";
|
|
7
7
|
import { BasicTransport } from "./transports/basic";
|
|
8
|
+
import { SseTransport } from "./transports/sse";
|
|
8
9
|
import { WsTransport } from "./transports/ws";
|
|
9
10
|
import type { IConnectionTransport } from "./transports/transport";
|
|
10
11
|
|
|
11
12
|
interface UseConnectionTransportOptions {
|
|
12
13
|
url: () => string;
|
|
13
14
|
static: boolean;
|
|
15
|
+
transportType: "websocket" | "sse";
|
|
16
|
+
/** Request headers for the SSE transport (auth); unused by WebSockets. */
|
|
17
|
+
headers: () => Record<string, string>;
|
|
14
18
|
waitToConnect: () => Promise<void>;
|
|
15
19
|
onOpen: (event: WebSocketEventMap["open"]) => void;
|
|
16
20
|
onMessage: (event: WebSocketEventMap["message"]) => void;
|
|
@@ -18,8 +22,11 @@ interface UseConnectionTransportOptions {
|
|
|
18
22
|
onError: (event: WebSocketEventMap["error"]) => void;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
function createConnectionTransport(
|
|
22
|
-
options: Pick<
|
|
25
|
+
export function createConnectionTransport(
|
|
26
|
+
options: Pick<
|
|
27
|
+
UseConnectionTransportOptions,
|
|
28
|
+
"url" | "static" | "transportType" | "headers"
|
|
29
|
+
>,
|
|
23
30
|
): IConnectionTransport {
|
|
24
31
|
if (options.static) {
|
|
25
32
|
return BasicTransport.empty();
|
|
@@ -28,6 +35,9 @@ function createConnectionTransport(
|
|
|
28
35
|
return createPyodideConnection();
|
|
29
36
|
}
|
|
30
37
|
// urlProvider is passed lazily; it may change after a runtime redirect.
|
|
38
|
+
if (options.transportType === "sse") {
|
|
39
|
+
return new SseTransport(options.url, options.headers);
|
|
40
|
+
}
|
|
31
41
|
return new WsTransport(options.url);
|
|
32
42
|
}
|
|
33
43
|
|
|
@@ -138,3 +138,17 @@
|
|
|
138
138
|
display: inline-block;
|
|
139
139
|
padding: 2px;
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
/* Rename symbol popup (F2). The upstream popup hardcodes white inline styles;
|
|
143
|
+
override with theme tokens. */
|
|
144
|
+
.cm-rename-popup {
|
|
145
|
+
background: var(--popover) !important;
|
|
146
|
+
border-color: var(--border) !important;
|
|
147
|
+
color: var(--popover-foreground) !important;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.cm-rename-popup input {
|
|
151
|
+
background: var(--background) !important;
|
|
152
|
+
border-color: var(--border) !important;
|
|
153
|
+
color: var(--foreground) !important;
|
|
154
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from "@testing-library/react";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { useDelayElapsed } from "../useDelayElapsed";
|
|
6
|
+
|
|
7
|
+
describe("useDelayElapsed", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.useFakeTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.useRealTimers();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("resolves true immediately when delay <= 0", () => {
|
|
17
|
+
const { result } = renderHook(() => useDelayElapsed(0));
|
|
18
|
+
expect(result.current).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("stays false until the delay elapses, then flips true", () => {
|
|
22
|
+
const { result } = renderHook(() => useDelayElapsed(2000));
|
|
23
|
+
expect(result.current).toBe(false);
|
|
24
|
+
|
|
25
|
+
act(() => vi.advanceTimersByTime(1999));
|
|
26
|
+
expect(result.current).toBe(false);
|
|
27
|
+
|
|
28
|
+
act(() => vi.advanceTimersByTime(1));
|
|
29
|
+
expect(result.current).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("restarts the timer when the delay changes", () => {
|
|
33
|
+
const { result, rerender } = renderHook(
|
|
34
|
+
({ delay }) => useDelayElapsed(delay),
|
|
35
|
+
{ initialProps: { delay: 1000 } },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
act(() => vi.advanceTimersByTime(1000));
|
|
39
|
+
expect(result.current).toBe(true);
|
|
40
|
+
|
|
41
|
+
rerender({ delay: 500 });
|
|
42
|
+
expect(result.current).toBe(false);
|
|
43
|
+
|
|
44
|
+
act(() => vi.advanceTimersByTime(500));
|
|
45
|
+
expect(result.current).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("clears the timer on unmount", () => {
|
|
49
|
+
const { unmount } = renderHook(() => useDelayElapsed(1000));
|
|
50
|
+
expect(vi.getTimerCount()).toBe(1);
|
|
51
|
+
unmount();
|
|
52
|
+
// The pending timeout must be cleared, not left dangling.
|
|
53
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useLayoutEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns `false` until `delayMs` has elapsed since mount (or since `delayMs`
|
|
7
|
+
* last changed), then `true`. Resolves to `true` immediately when
|
|
8
|
+
* `delayMs <= 0`. Useful for suppressing spinner/fallback flashes.
|
|
9
|
+
*/
|
|
10
|
+
export function useDelayElapsed(delayMs: number): boolean {
|
|
11
|
+
const [elapsed, setElapsed] = useState(delayMs <= 0);
|
|
12
|
+
|
|
13
|
+
// Layout effect (not a plain effect) so that when `delayMs` flips from `<= 0`
|
|
14
|
+
// to `> 0` — e.g. a gate re-closing — we reset `elapsed` to `false` before
|
|
15
|
+
// the browser paints, rather than briefly flashing the fallback for a frame.
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
if (delayMs <= 0) {
|
|
18
|
+
setElapsed(true);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
setElapsed(false);
|
|
22
|
+
const timeout = setTimeout(() => setElapsed(true), delayMs);
|
|
23
|
+
return () => clearTimeout(timeout);
|
|
24
|
+
}, [delayMs]);
|
|
25
|
+
|
|
26
|
+
return elapsed;
|
|
27
|
+
}
|
package/src/mount.tsx
CHANGED
|
@@ -54,8 +54,8 @@ import { notebookFileStore } from "./core/wasm/store";
|
|
|
54
54
|
import { WebSocketState } from "./core/websocket/types";
|
|
55
55
|
import {
|
|
56
56
|
handleWidgetMessage,
|
|
57
|
-
|
|
58
|
-
} from "./plugins/impl/anywidget/
|
|
57
|
+
WIDGET_REGISTRY,
|
|
58
|
+
} from "./plugins/impl/anywidget/registry";
|
|
59
59
|
import { vegaLoader } from "./plugins/impl/vega/loader";
|
|
60
60
|
import { initializePlugins } from "./plugins/plugins";
|
|
61
61
|
import { ThemeProvider } from "./theme/ThemeProvider";
|
|
@@ -369,7 +369,7 @@ function hydrateStaticModels(): void {
|
|
|
369
369
|
return;
|
|
370
370
|
}
|
|
371
371
|
for (const notification of notifications) {
|
|
372
|
-
handleWidgetMessage(
|
|
372
|
+
handleWidgetMessage(WIDGET_REGISTRY, notification);
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
@@ -199,6 +199,37 @@ const keyImagesBySrc: TransformFn = (
|
|
|
199
199
|
return cloneElement(reactNode, { key: `${src}-${index}` });
|
|
200
200
|
};
|
|
201
201
|
|
|
202
|
+
// Decorator: applies a checked-state-based key to task-list checkbox
|
|
203
|
+
// <input> elements (e.g. GFM `- [ ]` / `- [x]` rendered by
|
|
204
|
+
// pymdownx.tasklist) so they remount when their checked-ness changes.
|
|
205
|
+
//
|
|
206
|
+
// pymdownx.tasklist omits the `checked` HTML attribute entirely for
|
|
207
|
+
// unchecked items (rather than emitting checked="false"). When markdown
|
|
208
|
+
// edits shift which item lands at a given tree position (e.g. deleting
|
|
209
|
+
// completed items above unchecked ones), reusing the same DOM <input>
|
|
210
|
+
// across a checked -> unchecked item is a controlled -> uncontrolled prop
|
|
211
|
+
// transition for React: React stops managing the DOM `checked` property in
|
|
212
|
+
// that transition instead of resetting it, leaving the previous item's
|
|
213
|
+
// checked state visually stuck until a full remount. Baking checked-ness
|
|
214
|
+
// into the key forces a fresh node instead of reusing the stale one.
|
|
215
|
+
const keyTaskListCheckboxesByChecked: TransformFn = (
|
|
216
|
+
reactNode: ReactNode,
|
|
217
|
+
domNode: DOMNode,
|
|
218
|
+
index: number,
|
|
219
|
+
): JSX.Element | undefined => {
|
|
220
|
+
if (!(domNode instanceof Element) || domNode.name !== "input") {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
if (domNode.attribs?.type !== "checkbox") {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
if (!isValidElement(reactNode)) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
const checked = "checked" in domNode.attribs;
|
|
230
|
+
return cloneElement(reactNode, { key: `checkbox-${index}-${checked}` });
|
|
231
|
+
};
|
|
232
|
+
|
|
202
233
|
// Wrap elements with data-marimo-doc attribute in a DocHoverTarget
|
|
203
234
|
const wrapDocHoverTargets: TransformFn = (
|
|
204
235
|
reactNode: ReactNode,
|
|
@@ -326,7 +357,10 @@ function parseHtml({
|
|
|
326
357
|
// and may further wrap/clone it. Used for cross-cutting concerns that
|
|
327
358
|
// should apply regardless of which (if any) match-and-replace transform
|
|
328
359
|
// ran above.
|
|
329
|
-
const decoratorFunctions: TransformFn[] = [
|
|
360
|
+
const decoratorFunctions: TransformFn[] = [
|
|
361
|
+
keyImagesBySrc,
|
|
362
|
+
keyTaskListCheckboxesByChecked,
|
|
363
|
+
];
|
|
330
364
|
|
|
331
365
|
return parse(html, {
|
|
332
366
|
replace: (domNode: DOMNode, index: number) => {
|
|
@@ -114,6 +114,50 @@ describe("parseHtml", () => {
|
|
|
114
114
|
expect(result.key).toBe("https://cdn.example.com/b.png-0");
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
test("checked task-list checkbox is keyed by index and checked state", () => {
|
|
118
|
+
const html = '<li><input type="checkbox" checked disabled> item</li>';
|
|
119
|
+
const result = parseHtml({ html }) as React.ReactElement<{
|
|
120
|
+
children: React.ReactElement[];
|
|
121
|
+
}>;
|
|
122
|
+
const checkbox = result.props.children[0];
|
|
123
|
+
expect(checkbox.key).toBe("checkbox-0-true");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("unchecked task-list checkbox is keyed by index and checked state", () => {
|
|
127
|
+
const html = '<li><input type="checkbox" disabled> item</li>';
|
|
128
|
+
const result = parseHtml({ html }) as React.ReactElement<{
|
|
129
|
+
children: React.ReactElement[];
|
|
130
|
+
}>;
|
|
131
|
+
const checkbox = result.props.children[0];
|
|
132
|
+
expect(checkbox.key).toBe("checkbox-0-false");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("task list: item reused at same position gets a new key when checked state flips", () => {
|
|
136
|
+
// Simulates editing markdown so an unchecked item now sits where a
|
|
137
|
+
// checked item used to be (e.g. an earlier checked item was deleted).
|
|
138
|
+
const before = parseHtml({
|
|
139
|
+
html: '<ul><li><input type="checkbox" checked disabled> a</li></ul>',
|
|
140
|
+
}) as React.ReactElement<{ children: React.ReactElement }>;
|
|
141
|
+
const after = parseHtml({
|
|
142
|
+
html: '<ul><li><input type="checkbox" disabled> b</li></ul>',
|
|
143
|
+
}) as React.ReactElement<{ children: React.ReactElement }>;
|
|
144
|
+
const beforeLi = before.props.children as React.ReactElement<{
|
|
145
|
+
children: React.ReactElement[];
|
|
146
|
+
}>;
|
|
147
|
+
const afterLi = after.props.children as React.ReactElement<{
|
|
148
|
+
children: React.ReactElement[];
|
|
149
|
+
}>;
|
|
150
|
+
const beforeKey = beforeLi.props.children[0].key;
|
|
151
|
+
const afterKey = afterLi.props.children[0].key;
|
|
152
|
+
expect(beforeKey).not.toBe(afterKey);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("non-checkbox input is left alone", () => {
|
|
156
|
+
const html = '<input type="text" value="hi">';
|
|
157
|
+
const result = parseHtml({ html }) as React.ReactElement;
|
|
158
|
+
expect(result.key).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
|
|
117
161
|
test("codehilite with copy button", () => {
|
|
118
162
|
const html =
|
|
119
163
|
'<div class="codehilite"><pre><code>console.log("Hello");</code></pre></div>';
|