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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
  2. package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
  3. package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
  4. package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
  5. package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
  6. package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
  7. package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
  8. package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
  9. package/dist/{formats-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
  10. package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
  11. package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
  12. package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
  13. package/dist/main.js +3630 -1801
  14. package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.js} +2 -2
  15. package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
  16. package/dist/{process-output-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
  17. package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
  18. package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
  19. package/dist/style.css +1 -1
  20. package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
  21. package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
  22. package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
  23. package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
  24. package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
  25. package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
  26. package/package.json +4 -4
  27. package/src/__mocks__/requests.ts +1 -0
  28. package/src/__tests__/CellStatus.test.tsx +1 -2
  29. package/src/components/app-config/user-config-form.tsx +52 -0
  30. package/src/components/chat/acp/agent-panel.tsx +35 -1
  31. package/src/components/chat/chat-panel.tsx +68 -29
  32. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  33. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  34. package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
  35. package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
  36. package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
  37. package/src/components/data-table/charts/charts.tsx +23 -1
  38. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  39. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  40. package/src/components/debugger/debugger-code.tsx +7 -2
  41. package/src/components/editor/actions/useNotebookActions.tsx +2 -2
  42. package/src/components/editor/cell/CellStatus.tsx +1 -20
  43. package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
  44. package/src/components/editor/cell/cell-context-menu.tsx +2 -2
  45. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
  46. package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  47. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  48. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  49. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  50. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
  51. package/src/components/editor/code/readonly-python-code.tsx +60 -33
  52. package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
  53. package/src/components/editor/errors/auto-fix.tsx +88 -34
  54. package/src/components/editor/errors/fix-mode.ts +1 -1
  55. package/src/components/editor/notebook-cell.tsx +7 -0
  56. package/src/components/editor/output/ImageOutput.tsx +20 -6
  57. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
  58. package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
  59. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  60. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
  61. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
  62. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  63. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  64. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  65. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  66. package/src/components/slides/__tests__/slide.test.tsx +33 -0
  67. package/src/components/slides/minimap.tsx +7 -1
  68. package/src/components/slides/reveal-component.tsx +3 -0
  69. package/src/components/slides/slide-cell-view.tsx +10 -6
  70. package/src/components/slides/slide.tsx +16 -13
  71. package/src/components/tracing/tracing.tsx +2 -1
  72. package/src/core/ai/config.ts +2 -2
  73. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  74. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  75. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  76. package/src/core/ai/context/providers/datasource.ts +27 -2
  77. package/src/core/ai/context/providers/error.ts +226 -36
  78. package/src/core/ai/context/registry.ts +77 -43
  79. package/src/core/ai/state.ts +11 -0
  80. package/src/core/cells/__tests__/cell.test.ts +39 -0
  81. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  82. package/src/core/cells/cell.ts +5 -3
  83. package/src/core/cells/readonly-code-display.ts +35 -0
  84. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  85. package/src/core/codemirror/ai/resources.ts +15 -10
  86. package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
  87. package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
  88. package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
  89. package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
  90. package/src/core/codemirror/cells/debugger-state.ts +133 -0
  91. package/src/core/codemirror/cells/extensions.ts +32 -4
  92. package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
  93. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
  94. package/src/core/codemirror/go-to-definition/extension.ts +3 -3
  95. package/src/core/codemirror/go-to-definition/underline.ts +5 -14
  96. package/src/core/codemirror/go-to-definition/utils.ts +49 -1
  97. package/src/core/codemirror/utils.ts +15 -0
  98. package/src/core/config/__tests__/config-schema.test.ts +17 -0
  99. package/src/core/config/config-schema.ts +1 -0
  100. package/src/core/config/config.ts +10 -0
  101. package/src/core/config/feature-flag.tsx +4 -0
  102. package/src/core/edit-app.tsx +8 -5
  103. package/src/core/errors/errors.ts +2 -1
  104. package/src/core/islands/bootstrap.ts +4 -3
  105. package/src/core/islands/bridge.ts +1 -0
  106. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  107. package/src/core/lifecycle/render-policy.ts +125 -0
  108. package/src/core/mime.ts +11 -4
  109. package/src/core/network/__tests__/api.test.ts +17 -0
  110. package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
  111. package/src/core/network/api.ts +17 -6
  112. package/src/core/network/requests-lazy.ts +1 -0
  113. package/src/core/network/requests-network.ts +8 -0
  114. package/src/core/network/requests-static.ts +1 -0
  115. package/src/core/network/requests-toasting.tsx +10 -1
  116. package/src/core/network/types.ts +2 -0
  117. package/src/core/run-app.tsx +27 -23
  118. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  119. package/src/core/runtime/__tests__/runtime.test.ts +45 -0
  120. package/src/core/runtime/adapter.ts +182 -0
  121. package/src/core/runtime/runtime.ts +30 -16
  122. package/src/core/wasm/PyodideLoader.tsx +20 -62
  123. package/src/core/wasm/bridge.ts +13 -4
  124. package/src/core/wasm/state.ts +8 -19
  125. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
  126. package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
  127. package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
  128. package/src/core/websocket/transports/sse.ts +308 -0
  129. package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
  130. package/src/core/websocket/useWebSocket.tsx +12 -2
  131. package/src/css/app/codemirror.css +14 -0
  132. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  133. package/src/hooks/useDelayElapsed.ts +27 -0
  134. package/src/mount.tsx +3 -3
  135. package/src/plugins/core/RenderHTML.tsx +35 -1
  136. package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
  137. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  138. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
  139. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
  140. package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
  141. package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
  142. package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
  143. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
  144. package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
  145. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
  146. package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
  147. package/src/plugins/impl/anywidget/host.ts +54 -0
  148. package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
  149. package/src/plugins/impl/anywidget/model.ts +59 -239
  150. package/src/plugins/impl/anywidget/registry.ts +268 -0
  151. package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
  152. package/src/plugins/impl/anywidget/runtime.ts +422 -0
  153. package/src/plugins/impl/anywidget/types.ts +14 -0
  154. package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
  155. package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
  156. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
  157. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
  158. package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
  159. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
  160. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
  161. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
  162. package/src/utils/errors.ts +15 -0
  163. package/src/utils/time.ts +20 -0
  164. package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
  165. package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
  166. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
  167. package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
  168. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  169. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  170. package/src/core/wasm/__tests__/state.test.ts +0 -124
  171. package/src/plugins/impl/anywidget/schemas.ts +0 -32
@@ -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
- MODEL_MANAGER,
20
- } from "@/plugins/impl/anywidget/model";
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(MODEL_MANAGER, msg.data);
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: () => runtimeManager.getWsURL(sessionId).toString(),
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<UseConnectionTransportOptions, "url" | "static">,
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
- MODEL_MANAGER,
58
- } from "./plugins/impl/anywidget/model";
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(MODEL_MANAGER, notification);
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[] = [keyImagesBySrc];
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>';
@@ -805,6 +805,7 @@ export const LoadingDataTableComponent = memo(
805
805
  dataTable={dataTable}
806
806
  getDataUrl={props.get_data_url}
807
807
  fieldTypes={props.fieldTypes}
808
+ rowHeaders={props.rowHeaders}
808
809
  cellId={cellId}
809
810
  />
810
811
  ) : (