@marimo-team/islands 0.25.3-dev322 → 0.25.3-dev348

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 (125) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DkXBUBf3.js → ConnectedDataExplorerComponent-xliyyXNO.js} +16 -16
  2. package/dist/{ErrorBoundary-CYa6lyKz.js → ErrorBoundary-CIimOpqs.js} +7 -7
  3. package/dist/{abnfDiagram-VCTEODGH-BPS-pTU2.js → abnfDiagram-VCTEODGH-Dl9Nrtbf.js} +2 -2
  4. package/dist/{any-language-editor-DIS7IdV1.js → any-language-editor-CBHhdCC0.js} +2 -2
  5. package/dist/{architectureDiagram-5GKGNRK7-DB2MHOb7.js → architectureDiagram-5GKGNRK7-DTg032fQ.js} +1 -1
  6. package/dist/{assertNever-HbUt8DyH.js → assertNever-ie77PjMn.js} +13 -13
  7. package/dist/{chat-ui-85k50jaj.js → chat-ui-BV0qyfru.js} +51 -51
  8. package/dist/{check-CApDh1-n.js → check-DTIv5ZZ3.js} +1 -1
  9. package/dist/{chunk-IMKFNOWR-CjFCedjx.js → chunk-IMKFNOWR-CsFB-2LW.js} +1 -1
  10. package/dist/{chunk-SHT3W25Y-Dptltg0_.js → chunk-SHT3W25Y-ByuJHeBL.js} +1 -1
  11. package/dist/{chunk-SVP7TREG-2jBN760m.js → chunk-SVP7TREG-9oBZhbrr.js} +1 -1
  12. package/dist/{chunk-TICWLB2K-CS8BE-b9.js → chunk-TICWLB2K-Bx4m0cJ1.js} +1 -1
  13. package/dist/{classDiagram-ZZMXUADV-HtQWp-Cm.js → classDiagram-ZZMXUADV-Cms37-C9.js} +2 -2
  14. package/dist/{classDiagram-v2-VYDZK3BY-DA9LETN-.js → classDiagram-v2-VYDZK3BY-qAom4Bi7.js} +2 -2
  15. package/dist/{code-visibility-CGcIJMGj.js → code-visibility-_-8-dvb4.js} +20 -20
  16. package/dist/{common-B-Ng3J3K.js → common-CNXuhphD.js} +620 -620
  17. package/dist/{config-B171m9zn.js → config-CvBnR945.js} +3 -3
  18. package/dist/{copy-icon-k2_Fx5Qv.js → copy-icon-DmtNidSa.js} +2 -2
  19. package/dist/{createReducer-BPPI11pP.js → createReducer-BxBCTvKn.js} +5 -5
  20. package/dist/{cynefinDiagram-5FMLGOSQ-eyocrg_E.js → cynefinDiagram-5FMLGOSQ-BjJZwMEz.js} +1 -1
  21. package/dist/{dagre-GXQ25YYZ-Dtm96Nvx.js → dagre-GXQ25YYZ-Bohu7_09.js} +1 -1
  22. package/dist/{data-grid-overlay-editor-CUD6F0Vj.js → data-grid-overlay-editor--4Gk_rwA.js} +1 -1
  23. package/dist/{diagram-S7CK7UJ4-C09TrdNO.js → diagram-S7CK7UJ4-Chl0n835.js} +1 -1
  24. package/dist/{diagram-UQ7AKVKN-vHUsSML6.js → diagram-UQ7AKVKN-DHx8nzy7.js} +1 -1
  25. package/dist/{diagram-VX7I27RA-Ch4mRgz8.js → diagram-VX7I27RA-CKdpv1U4.js} +1 -1
  26. package/dist/{diagram-Z3DM3KII-DyFQNdAL.js → diagram-Z3DM3KII-sY-sI--2.js} +1 -1
  27. package/dist/{dialog-C2Muh059.js → dialog-Cqu5Hp5U.js} +21 -21
  28. package/dist/{dropdown-menu-BGbNnuFq.js → dropdown-menu-DXEYk99S.js} +707 -707
  29. package/dist/{ebnfDiagram-PWID7BFC-N3SvlrCs.js → ebnfDiagram-PWID7BFC-CD0P2xeH.js} +2 -2
  30. package/dist/{erDiagram-RLTQ6QDP-RR2q2xtd.js → erDiagram-RLTQ6QDP-BaKQuYxC.js} +1 -1
  31. package/dist/{error-banner-3_4Lc93Y.js → error-banner-DUB_qUeL.js} +58 -58
  32. package/dist/{error-entries-BhNAZWU1.js → error-entries-CTb_cxXY.js} +310 -310
  33. package/dist/{feedback-button-DK6zUzAk.js → feedback-button-BaA4r2dl.js} +7 -7
  34. package/dist/{flowDiagram-HODETNUW-nxd7WhlW.js → flowDiagram-HODETNUW-BcCpHh7-.js} +2 -2
  35. package/dist/{formats-aUcw6MrS.js → formats-hF6fETr6.js} +1 -1
  36. package/dist/{glide-data-editor-O-94UN5R.js → glide-data-editor-CRn3KXyR.js} +3129 -3119
  37. package/dist/{glide-utils-D4UP8TGv.js → glide-utils-Bnd_k48H.js} +2 -2
  38. package/dist/{globals-BVjYePCn.js → globals-_FmD97Ui.js} +3 -3
  39. package/dist/{html-to-image-DOOtP-4-.js → html-to-image-Bbl0PeC7.js} +478 -478
  40. package/dist/{infoDiagram-27XIBGKW-CUYXQKGf.js → infoDiagram-27XIBGKW-DWltseIa.js} +1 -1
  41. package/dist/{ishikawaDiagram-5VMMS53U-v6gsCrlK.js → ishikawaDiagram-5VMMS53U-DHgepBPo.js} +1 -1
  42. package/dist/{kanban-definition-UXKFOSKX-z236-0Ok.js → kanban-definition-UXKFOSKX-B2_BIjBq.js} +1 -1
  43. package/dist/main.js +6712 -6590
  44. package/dist/{mermaid-C7lQ_2LJ.js → mermaid--2K0cmt9.js} +34 -34
  45. package/dist/{mindmap-definition-YA3MSWOX--pZF7OCY.js → mindmap-definition-YA3MSWOX-DNebycIP.js} +1 -1
  46. package/dist/{number-overlay-editor-jkzzCN4e.js → number-overlay-editor-Bcg25qrp.js} +1 -1
  47. package/dist/{pegDiagram-XKGWAZYB-Bgw0Zz_6.js → pegDiagram-XKGWAZYB-CH4U3II-.js} +2 -2
  48. package/dist/{pencil-RXFY-Ek8.js → pencil-MMnw1YQh.js} +1 -1
  49. package/dist/{pieDiagram-E7YTZNPT-B27tDYvK.js → pieDiagram-E7YTZNPT-pfcrdUCa.js} +1 -1
  50. package/dist/{process-output-CiX_Dl4L.js → process-output-Dw_sI4Xp.js} +4 -4
  51. package/dist/{railroadDiagram-O6MQD6OU-CBjPhGpj.js → railroadDiagram-O6MQD6OU-BZKpAbNG.js} +2 -2
  52. package/dist/{requirementDiagram-BXWQKSXE-DDDrLEw9.js → requirementDiagram-BXWQKSXE-DnyfIHe3.js} +1 -1
  53. package/dist/{reveal-component-CvPmNI5C.js → reveal-component-BVC491am.js} +18 -18
  54. package/dist/{select-Cv6lR39D.js → select-suaAxFuo.js} +286 -286
  55. package/dist/{sequenceDiagram-WJ2MYXX4-DQPaMygQ.js → sequenceDiagram-WJ2MYXX4-BeYuWX7N.js} +1 -1
  56. package/dist/{spec-BoCLWa-M.js → spec-BQNx2-D7.js} +4 -4
  57. package/dist/{stateDiagram-D77RDMKH-BDoDVVnq.js → stateDiagram-D77RDMKH-B8FxwAM2.js} +2 -2
  58. package/dist/{stateDiagram-v2-MP3YSRHH-KNko46uJ.js → stateDiagram-v2-MP3YSRHH-CzWLBqm1.js} +2 -2
  59. package/dist/{static-state-DgdOadIH.js → static-state-Al_hmf1n.js} +6 -6
  60. package/dist/style.css +1 -1
  61. package/dist/{swimlanes-42K2YHIH-boFjOuiI.js → swimlanes-42K2YHIH-CzKmGMts.js} +1 -1
  62. package/dist/{swimlanesDiagram-VR7AAH4N-BC4FLNwY.js → swimlanesDiagram-VR7AAH4N-DnWIUN_9.js} +2 -2
  63. package/dist/{timeline-definition-24CTP7MA-CLVPYA-v.js → timeline-definition-24CTP7MA-Czf2QN2R.js} +1 -1
  64. package/dist/{toast-CUuX7uXg.js → toast-CsJa1nGj.js} +66 -66
  65. package/dist/{tooltip-DBv8QUvS.js → tooltip-BVbJPkcl.js} +49 -49
  66. package/dist/{use-restore-focus-BxJh5Zfa.js → use-restore-focus-tPmARo1Q.js} +1035 -1022
  67. package/dist/{useDateFormatter-2k4mHRBq.js → useDateFormatter-B4ygYuUU.js} +2 -2
  68. package/dist/{useDeepCompareMemoize-Cb2GRQIS.js → useDeepCompareMemoize-Bcu0DkCS.js} +1 -1
  69. package/dist/{useLifecycle-B2fV3c8N.js → useLifecycle-MTdtnR3-.js} +1 -1
  70. package/dist/{vega-component-do3stf0K.js → vega-component-BhBbP4S7.js} +6 -6
  71. package/dist/{vennDiagram-4TSXK5OY-BLq7sNjb.js → vennDiagram-4TSXK5OY-BrHqWB1U.js} +1 -1
  72. package/dist/{wardleyDiagram-VM6X3IG4-oyJEWWtb.js → wardleyDiagram-VM6X3IG4-Cms1K_3v.js} +1 -1
  73. package/dist/{xychartDiagram-S5SC5T6Z-DMjWgQPm.js → xychartDiagram-S5SC5T6Z-_kUiODzS.js} +1 -1
  74. package/package.json +1 -1
  75. package/src/__mocks__/requests.ts +0 -2
  76. package/src/components/chat/__tests__/notebook-context.test.ts +199 -0
  77. package/src/components/chat/chat-panel.tsx +33 -15
  78. package/src/components/chat/notebook-context.ts +80 -0
  79. package/src/components/editor/cell/StopButton.tsx +3 -3
  80. package/src/components/editor/cell/cell-context-menu.tsx +18 -2
  81. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +38 -1
  82. package/src/components/editor/cell/code/cell-editor.tsx +2 -1
  83. package/src/components/editor/cell/code/language-toggle.tsx +6 -4
  84. package/src/components/editor/chrome/wrapper/__tests__/document-sync-status.test.tsx +9 -6
  85. package/src/components/slides/slide-cell-view.tsx +2 -1
  86. package/src/components/ui/__tests__/fullscreen.test.tsx +56 -1
  87. package/src/components/ui/fullscreen.tsx +18 -2
  88. package/src/components/ui/slider.tsx +6 -2
  89. package/src/core/codemirror/copilot/getCodes.ts +1 -1
  90. package/src/core/edit-app.tsx +5 -4
  91. package/src/core/executions/__tests__/input.test.ts +1 -1
  92. package/src/core/executions/__tests__/interrupt.test.ts +200 -0
  93. package/src/core/executions/__tests__/run.test.ts +3 -3
  94. package/src/core/executions/index.ts +1 -0
  95. package/src/core/executions/interrupt.ts +49 -0
  96. package/src/core/islands/bridge.ts +0 -2
  97. package/src/core/network/requests-network.ts +0 -15
  98. package/src/core/network/requests-static.ts +0 -2
  99. package/src/core/network/requests-toasting.tsx +0 -2
  100. package/src/core/network/types.ts +0 -3
  101. package/src/core/notebook/document/__tests__/model.test.ts +4 -4
  102. package/src/core/notebook/document/__tests__/test-host.test.ts +6 -6
  103. package/src/core/notebook/document/__tests__/test-host.ts +42 -46
  104. package/src/core/notebook/document/__tests__/test-notebook.ts +5 -4
  105. package/src/core/notebook/document/sync.ts +12 -5
  106. package/src/core/notebook/replica/__tests__/fixtures.ts +8 -0
  107. package/src/core/notebook/replica/backend.ts +1 -1
  108. package/src/core/saving/__tests__/document.test.tsx +1 -1
  109. package/src/core/transports/__tests__/static.test.ts +1 -1
  110. package/src/core/transports/__tests__/wasm.test.ts +1 -1
  111. package/src/core/transports/static.ts +8 -2
  112. package/src/core/transports/wasm.ts +2 -4
  113. package/src/core/wasm/__tests__/bridge.test.ts +170 -26
  114. package/src/core/wasm/bridge.ts +64 -19
  115. package/src/core/wasm/worker/types.ts +0 -1
  116. package/src/plugins/impl/DataEditorPlugin.tsx +32 -21
  117. package/src/plugins/impl/SliderPlugin.tsx +35 -3
  118. package/src/plugins/impl/__test__/slider-labels.test.tsx +30 -0
  119. package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +103 -12
  120. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +40 -0
  121. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +159 -4
  122. package/src/plugins/impl/data-editor/glide-data-editor.tsx +30 -10
  123. package/src/plugins/impl/data-editor/glide-portal.tsx +5 -3
  124. package/src/plugins/layout/AccordionPlugin.tsx +2 -1
  125. package/src/plugins/layout/__tests__/AccordionPlugin.test.tsx +21 -0
@@ -20,15 +20,18 @@ afterEach(() => {
20
20
  });
21
21
 
22
22
  const conflict = {
23
- ok: false as const,
24
- status: 409,
23
+ ok: false,
24
+ status: 412,
25
25
  problem: {
26
- type: "tag:marimo.io,2026:host/conflict",
27
- title: "Conflict",
28
- status: 409,
26
+ type: "urn:marimo:problem:precondition-failed",
27
+ title: "Precondition failed",
28
+ status: 412,
29
29
  detail: "cell c1 has changed since it was read.",
30
+ index: 0,
31
+ cell: document().cells.get("c1")!,
32
+ document: document().revision,
30
33
  },
31
- };
34
+ } satisfies Awaited<ReturnType<FakeEdit>>;
32
35
 
33
36
  function setup(edit: FakeEdit) {
34
37
  const store = createStore();
@@ -117,7 +117,8 @@ export const SlideCellView = ({ cell }: { cell: RuntimeCell }) => {
117
117
  code={cell.code}
118
118
  editorViewRef={editorViewRef}
119
119
  currentLanguageAdapter={languageAdapter}
120
- onAfterToggle={afterToggleLanguage}
120
+ onAfterToggleSQL={afterToggleLanguage}
121
+ onAfterToggleMarkdown={afterToggleLanguage}
121
122
  className="flex items-center gap-1"
122
123
  />
123
124
  <div className="flex items-center shadow-none gap-1">
@@ -1,8 +1,63 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { act, render, renderHook } from "@testing-library/react";
3
3
  import { useRef } from "react";
4
+ import { createPortal } from "react-dom";
4
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
- import { useFullScreenClass, useIsFullScreen } from "../fullscreen";
6
+ import {
7
+ useFullScreenClass,
8
+ useFullScreenElement,
9
+ useIsFullScreen,
10
+ withFullScreenAsRoot,
11
+ } from "../fullscreen";
12
+
13
+ describe("full screen element inside a shadow root", () => {
14
+ let host: HTMLElement;
15
+ let target: HTMLElement;
16
+
17
+ beforeEach(() => {
18
+ host = document.createElement("div");
19
+ host.append(document.createElement("div"));
20
+ const shadowRoot = host.attachShadow({ mode: "open" });
21
+ target = document.createElement("div");
22
+ shadowRoot.append(target);
23
+ document.body.append(host);
24
+
25
+ // The browser retargets document.fullscreenElement to the shadow host and
26
+ // reports the element itself on the shadow root.
27
+ Object.defineProperty(document, "fullscreenElement", {
28
+ configurable: true,
29
+ get: () => host,
30
+ });
31
+ Object.defineProperty(shadowRoot, "fullscreenElement", {
32
+ configurable: true,
33
+ get: () => target,
34
+ });
35
+ });
36
+
37
+ afterEach(() => {
38
+ Reflect.deleteProperty(document, "fullscreenElement");
39
+ host.remove();
40
+ });
41
+
42
+ it("resolves the element that is actually full screen", () => {
43
+ const { result } = renderHook(useFullScreenElement);
44
+ expect(result.current).toBe(target);
45
+ });
46
+
47
+ it("portals into that element, not the host's light DOM", () => {
48
+ const Portal = withFullScreenAsRoot(
49
+ ({ container }: { container?: Element | DocumentFragment | null }) =>
50
+ createPortal(
51
+ <span data-testid="portalled" />,
52
+ container ?? document.body,
53
+ ),
54
+ );
55
+ render(<Portal />);
56
+
57
+ expect(host.children).toHaveLength(1);
58
+ expect(target.querySelector('[data-testid="portalled"]')).not.toBeNull();
59
+ });
60
+ });
6
61
 
7
62
  describe("useFullScreenClass", () => {
8
63
  let fullscreenElement: Element | null = null;
@@ -38,15 +38,31 @@ export function useFullScreenClass() {
38
38
  }, []);
39
39
  }
40
40
 
41
+ /**
42
+ * The element that is actually full screen.
43
+ *
44
+ * `document.fullscreenElement` is retargeted to the outermost shadow host, so
45
+ * for a target inside a plugin's shadow root it reports the plugin host.
46
+ * Portalling into the host appends to its light DOM, which is not rendered and
47
+ * which the host observes to rebuild its children.
48
+ */
49
+ export function getFullScreenElement(): Element | null {
50
+ let element = document.fullscreenElement;
51
+ while (element?.shadowRoot?.fullscreenElement) {
52
+ element = element.shadowRoot.fullscreenElement;
53
+ }
54
+ return element;
55
+ }
56
+
41
57
  /**
42
58
  * Get the full screen element if we are in full screen mode
43
59
  */
44
60
  export function useFullScreenElement() {
45
61
  const [fullScreenElement, setFullScreenElement] = useState<Element | null>(
46
- document.fullscreenElement,
62
+ getFullScreenElement,
47
63
  );
48
64
  useEventListener(document, "fullscreenchange", () => {
49
- setFullScreenElement(document.fullscreenElement);
65
+ setFullScreenElement(getFullScreenElement());
50
66
  });
51
67
  return fullScreenElement;
52
68
  }
@@ -18,8 +18,9 @@ const Slider = React.forwardRef<
18
18
  React.ElementRef<typeof SliderPrimitive.Root>,
19
19
  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
20
20
  valueMap: (sliderValue: number) => number;
21
+ maximumFractionDigits?: number;
21
22
  }
22
- >(({ className, valueMap, ...props }, ref) => {
23
+ >(({ className, valueMap, maximumFractionDigits, ...props }, ref) => {
23
24
  const [open, openActions] = useBoolean(false);
24
25
  const { locale } = useLocale();
25
26
 
@@ -70,7 +71,10 @@ const Slider = React.forwardRef<
70
71
  <TooltipPortal>
71
72
  {props.value != null && props.value.length === 1 && (
72
73
  <TooltipContent key={props.value[0]}>
73
- {prettyScientificNumber(valueMap(props.value[0]), { locale })}
74
+ {prettyScientificNumber(valueMap(props.value[0]), {
75
+ locale,
76
+ maximumFractionDigits,
77
+ })}
74
78
  </TooltipContent>
75
79
  )}
76
80
  </TooltipPortal>
@@ -9,7 +9,7 @@ import type { Variables } from "@/core/variables/types";
9
9
  import { Objects } from "@/utils/objects";
10
10
  import { getEditorCodeAsPython } from "../language/utils";
11
11
 
12
- const notebookCellCodes = atom((get) => {
12
+ export const notebookCellCodes = atom((get) => {
13
13
  const notebook = get(notebookAtom);
14
14
  const codes = Objects.fromEntries(
15
15
  notebook.cellIds.map((id) => {
@@ -27,7 +27,8 @@ import { CellArray } from "../components/editor/renderers/cell-array";
27
27
  import { CellsRenderer } from "../components/editor/renderers/cells-renderer";
28
28
  import { useHotkey } from "../hooks/useHotkey";
29
29
  import { hasCellsAtom } from "./cells/cells";
30
- import { notebookIsRunningAtom } from "./executions";
30
+ import { interruptRuntime, notebookIsRunningAtom } from "./executions";
31
+ import { store } from "./state/jotai";
31
32
  import { numColumnsAtom } from "./cells/layout";
32
33
  import { useCellActions } from "./cells/actions";
33
34
  import type { AppConfig, UserConfig } from "./config/config-schema";
@@ -73,7 +74,7 @@ export const EditApp: React.FC<EditAppProps> = ({
73
74
  const viewState = useAtomValue(viewStateAtom);
74
75
  const numColumns = useAtomValue(numColumnsAtom);
75
76
  const filename = useFilename();
76
- const { sendComponentValues, sendInterrupt } = useRequestClient();
77
+ const { sendComponentValues } = useRequestClient();
77
78
 
78
79
  const isEditing = viewState.mode === "edit";
79
80
  const isPresenting = viewState.mode === "present";
@@ -115,7 +116,7 @@ export const EditApp: React.FC<EditAppProps> = ({
115
116
  runStaleCells();
116
117
  });
117
118
  useHotkey("global.interrupt", () => {
118
- sendInterrupt();
119
+ void interruptRuntime(store);
119
120
  });
120
121
  useHotkey("global.hideCode", () => {
121
122
  togglePresenting();
@@ -208,7 +209,7 @@ export const EditApp: React.FC<EditAppProps> = ({
208
209
  <Controls
209
210
  presenting={isPresenting}
210
211
  onTogglePresenting={togglePresenting}
211
- onInterrupt={sendInterrupt}
212
+ onInterrupt={() => interruptRuntime(store)}
212
213
  onRun={runStaleCells}
213
214
  connectionState={connection.state}
214
215
  running={isRunning}
@@ -58,7 +58,7 @@ describe("replyInput", () => {
58
58
  ok: false,
59
59
  status: 409,
60
60
  problem: {
61
- type: "tag:marimo.io,2026:host/conflict",
61
+ type: "urn:marimo:problem:conflict",
62
62
  title: "Conflict",
63
63
  status: 409,
64
64
  detail: "input prompt-1 has already ended.",
@@ -0,0 +1,200 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { ProblemOf } from "@marimo-team/client";
4
+ import type * as Protocol from "@marimo-team/client/protocol";
5
+ import { createStore } from "jotai";
6
+ import { beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { MockExecutions } from "@/__mocks__/executions";
8
+ import {
9
+ fakeBackend,
10
+ type FakeInterrupt,
11
+ } from "@/core/notebook/replica/__tests__/fixtures";
12
+ import { activeNotebookReplicaAtom } from "@/core/session/active-notebook";
13
+ import { interruptRuntime } from "../interrupt";
14
+
15
+ type Refusal = ProblemOf<"runtime.interrupt">;
16
+
17
+ const toast = vi.fn();
18
+ vi.mock("@/components/ui/use-toast", () => ({
19
+ toast: (...args: unknown[]) => toast(...args),
20
+ }));
21
+
22
+ function run(
23
+ id: string,
24
+ status: Protocol.Lifecycle = "running",
25
+ runtimeId = "r1",
26
+ ): Protocol.Run {
27
+ return {
28
+ id,
29
+ notebook_id: "n1",
30
+ runtime_id: runtimeId,
31
+ cells: [],
32
+ status,
33
+ outcome: status === "completed" ? "succeeded" : null,
34
+ started_at: status === "queued" ? null : MockExecutions.T0,
35
+ completed_at: status === "completed" ? MockExecutions.T1 : null,
36
+ };
37
+ }
38
+
39
+ function setup(runs: Protocol.Run[]) {
40
+ const store = createStore();
41
+ const replica = MockExecutions.seed(
42
+ store,
43
+ [],
44
+ MockExecutions.runtime("running"),
45
+ );
46
+ const interrupt = vi.fn<FakeInterrupt>(async () => ({
47
+ ok: true,
48
+ status: 202,
49
+ data: MockExecutions.runtime("running"),
50
+ }));
51
+ store.set(replica.backendAtom, fakeBackend({ interrupt }));
52
+ store.set(replica.executionsAtom, {
53
+ runs: new Map(runs.map((run) => [run.id, run])),
54
+ executions: new Map(),
55
+ latest: new Map(),
56
+ });
57
+ return { store, replica, interrupt };
58
+ }
59
+
60
+ describe("interruptRuntime", () => {
61
+ beforeEach(() => toast.mockClear());
62
+
63
+ it.each([
64
+ { runs: [] },
65
+ { runs: [run("queued", "queued")] },
66
+ { runs: [run("browser"), run("code-mode"), run("queued", "queued")] },
67
+ ])(
68
+ "interrupts the runtime once regardless of recorded runs: %j",
69
+ async ({ runs }) => {
70
+ const { store, replica, interrupt } = setup(runs);
71
+
72
+ await interruptRuntime(store);
73
+
74
+ expect(interrupt.mock.calls).toEqual([["n1"]]);
75
+ // The stream, not the acknowledgement, determines the work's outcome.
76
+ expect([...store.get(replica.executionsAtom).runs.values()]).toEqual(
77
+ runs,
78
+ );
79
+ expect(toast).not.toHaveBeenCalled();
80
+ },
81
+ );
82
+
83
+ it.each<Refusal>([
84
+ {
85
+ type: "urn:marimo:problem:no-runtime",
86
+ title: "The runtime has stopped",
87
+ status: 409,
88
+ },
89
+ {
90
+ type: "urn:marimo:problem:conflict",
91
+ title: "The runtime has stopped",
92
+ status: 409,
93
+ },
94
+ ])(
95
+ "tolerates a runtime that stopped before the request: $type",
96
+ async (problem) => {
97
+ const { store, interrupt } = setup([]);
98
+ interrupt.mockResolvedValue({
99
+ ok: false,
100
+ status: problem.status,
101
+ problem,
102
+ });
103
+
104
+ await interruptRuntime(store);
105
+
106
+ expect(interrupt.mock.calls).toEqual([["n1"]]);
107
+ expect(toast).not.toHaveBeenCalled();
108
+ },
109
+ );
110
+
111
+ it("sends the request even when the local runtime record is absent", async () => {
112
+ const { store, replica, interrupt } = setup([]);
113
+ store.set(replica.runtimeAtom, null);
114
+
115
+ await interruptRuntime(store);
116
+
117
+ expect(interrupt.mock.calls).toEqual([["n1"]]);
118
+ });
119
+
120
+ it("does nothing without a connected notebook", async () => {
121
+ const { store, replica, interrupt } = setup([]);
122
+ store.set(replica.backendAtom, null);
123
+ await interruptRuntime(store);
124
+ store.set(activeNotebookReplicaAtom, null);
125
+ await interruptRuntime(store);
126
+
127
+ expect(interrupt).not.toHaveBeenCalled();
128
+ expect(toast).not.toHaveBeenCalled();
129
+ });
130
+
131
+ it.each<{ problem: Refusal; toasts: unknown[] }>([
132
+ {
133
+ problem: {
134
+ type: "urn:marimo:problem:forbidden",
135
+ title: "Interrupt refused",
136
+ status: 403,
137
+ },
138
+ toasts: [],
139
+ },
140
+ {
141
+ problem: {
142
+ type: "urn:marimo:problem:not-found",
143
+ title: "Interrupt refused",
144
+ status: 404,
145
+ },
146
+ toasts: [
147
+ [
148
+ {
149
+ title: "Failed to interrupt",
150
+ description: "Interrupt refused",
151
+ variant: "danger",
152
+ },
153
+ ],
154
+ ],
155
+ },
156
+ {
157
+ problem: {
158
+ type: "urn:marimo:problem:internal",
159
+ title: "Interrupt refused",
160
+ status: 500,
161
+ },
162
+ toasts: [
163
+ [
164
+ {
165
+ title: "Failed to interrupt",
166
+ description: "Interrupt refused",
167
+ variant: "danger",
168
+ },
169
+ ],
170
+ ],
171
+ },
172
+ ])(
173
+ "handles a host refusal with status $problem.status",
174
+ async ({ problem, toasts }) => {
175
+ const { store, interrupt } = setup([run("running")]);
176
+ interrupt.mockResolvedValue({
177
+ ok: false,
178
+ status: problem.status,
179
+ problem,
180
+ });
181
+
182
+ await interruptRuntime(store);
183
+
184
+ expect(toast.mock.calls).toEqual(toasts);
185
+ },
186
+ );
187
+
188
+ it("reports transport failures without leaving an unhandled rejection", async () => {
189
+ const { store, interrupt } = setup([run("running")]);
190
+ interrupt.mockRejectedValue(new Error("Disconnected"));
191
+
192
+ await interruptRuntime(store);
193
+
194
+ expect(toast).toHaveBeenCalledExactlyOnceWith({
195
+ title: "Failed to interrupt",
196
+ description: "Disconnected",
197
+ variant: "danger",
198
+ });
199
+ });
200
+ });
@@ -265,7 +265,7 @@ describe("runCells", () => {
265
265
  ok: false,
266
266
  status: 409,
267
267
  problem: {
268
- type: "tag:marimo.io,2026:host/conflict",
268
+ type: "urn:marimo:problem:conflict",
269
269
  title: "Conflict",
270
270
  status: 409,
271
271
  detail: "the runtime is starting.",
@@ -286,7 +286,7 @@ describe("runCells", () => {
286
286
  ok: false,
287
287
  status: 403,
288
288
  problem: {
289
- type: "tag:marimo.io,2026:host/forbidden",
289
+ type: "urn:marimo:problem:forbidden",
290
290
  title: "Forbidden",
291
291
  status: 403,
292
292
  },
@@ -355,7 +355,7 @@ describe("runCells", () => {
355
355
  ok: false,
356
356
  status: 409,
357
357
  problem: {
358
- type: "tag:marimo.io,2026:host/conflict",
358
+ type: "urn:marimo:problem:conflict",
359
359
  title: "Conflict",
360
360
  status: 409,
361
361
  },
@@ -22,6 +22,7 @@ export {
22
22
  } from "./atoms";
23
23
  export type { CellFreshness, CellFreshnessLookup } from "./freshness";
24
24
  export { replyInput } from "./input";
25
+ export { interruptRuntime } from "./interrupt";
25
26
  export { runCells, syncEdits } from "./run";
26
27
  export {
27
28
  useCellExecution,
@@ -0,0 +1,49 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { createStore } from "jotai";
4
+ import { toast } from "@/components/ui/use-toast";
5
+ import { activeNotebookReplicaAtom } from "@/core/session/active-notebook";
6
+ import { prettyError } from "@/utils/errors";
7
+ import { Logger } from "@/utils/Logger";
8
+
9
+ /** Interrupt active runtime work and end queued runs. */
10
+ export async function interruptRuntime(
11
+ store: ReturnType<typeof createStore>,
12
+ ): Promise<void> {
13
+ const replica = store.get(activeNotebookReplicaAtom);
14
+ if (replica === null) {
15
+ return;
16
+ }
17
+ const notebook = store.get(replica.notebookAtom);
18
+ const backend = store.get(replica.backendAtom);
19
+ if (notebook === null || backend === null) {
20
+ return;
21
+ }
22
+ try {
23
+ const result = await backend.notebook(notebook.id).interrupt();
24
+ if (result.ok) {
25
+ return;
26
+ }
27
+ // The runtime can stop before the request arrives.
28
+ if (result.status === 409) {
29
+ return;
30
+ }
31
+ if (result.status === 403) {
32
+ Logger.warn("Interrupt refused, connection is read-only", result.problem);
33
+ return;
34
+ }
35
+ Logger.error("The host refused the interrupt", result.problem);
36
+ toast({
37
+ title: "Failed to interrupt",
38
+ description: result.problem.detail ?? result.problem.title,
39
+ variant: "danger",
40
+ });
41
+ } catch (error) {
42
+ Logger.error("Failed to interrupt", error);
43
+ toast({
44
+ title: "Failed to interrupt",
45
+ description: prettyError(error),
46
+ variant: "danger",
47
+ });
48
+ }
49
+ }
@@ -375,8 +375,6 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
375
375
  public saveLayout = throwNotImplemented;
376
376
  public sendCopy = throwNotImplemented;
377
377
  public sendRunScratchpad = throwNotImplemented;
378
- public sendStdin = throwNotImplemented;
379
- public sendInterrupt = throwNotImplemented;
380
378
  public sendShutdown = throwNotImplemented;
381
379
  public sendFormat = throwNotImplemented;
382
380
  public sendInstallMissingPackages = throwNotImplemented;
@@ -94,13 +94,6 @@ export function createNetworkRequests(): EditRequests & RunRequests {
94
94
  })
95
95
  .then(handleResponse);
96
96
  },
97
- sendInterrupt: () => {
98
- return getClient()
99
- .POST("/api/kernel/interrupt", {
100
- params: getParams(),
101
- })
102
- .then(handleResponseReturnNull);
103
- },
104
97
  sendShutdown: () => {
105
98
  return getClient()
106
99
  .POST("/api/kernel/shutdown")
@@ -165,14 +158,6 @@ export function createNetworkRequests(): EditRequests & RunRequests {
165
158
  })
166
159
  .then(handleResponseReturnNull);
167
160
  },
168
- sendStdin: (request) => {
169
- return getClient()
170
- .POST("/api/kernel/stdin", {
171
- body: request,
172
- params: getParams(),
173
- })
174
- .then(handleResponseReturnNull);
175
- },
176
161
  sendInstallMissingPackages: (request) => {
177
162
  return getClient()
178
163
  .POST("/api/kernel/install_missing_packages", {
@@ -44,13 +44,11 @@ export function createStaticRequests(): EditRequests & RunRequests {
44
44
  sendRename: throwNotInEditMode,
45
45
  saveLayout: throwNotInEditMode,
46
46
  sendCopy: throwNotInEditMode,
47
- sendInterrupt: throwNotInEditMode,
48
47
  sendShutdown: throwNotInEditMode,
49
48
  sendFormat: throwNotInEditMode,
50
49
  sendCodeCompletionRequest: throwNotInEditMode,
51
50
  saveUserConfig: throwNotInEditMode,
52
51
  saveAppConfig: throwNotInEditMode,
53
- sendStdin: throwNotInEditMode,
54
52
  readCode: throwNotInEditMode,
55
53
  readSnippets: throwNotInEditMode,
56
54
  previewDatasetColumn: throwNotInEditMode,
@@ -19,13 +19,11 @@ export function createErrorToastingRequests(
19
19
  sendRename: "Failed to rename",
20
20
  saveLayout: "", // The save reports it
21
21
  sendCopy: "Failed to copy",
22
- sendInterrupt: "Failed to interrupt",
23
22
  sendShutdown: "Failed to shutdown",
24
23
  sendFormat: "Failed to format",
25
24
  sendCodeCompletionRequest: "Failed to complete code",
26
25
  saveUserConfig: "Failed to save user config",
27
26
  saveAppConfig: "Failed to save app config",
28
- sendStdin: "Failed to send stdin",
29
27
  readCode: "Failed to read code",
30
28
  readSnippets: "Failed to fetch snippets",
31
29
  previewDatasetColumn: "Failed to fetch data sources",
@@ -116,7 +116,6 @@ export type ShutdownSessionRequest = schemas["ShutdownSessionRequest"];
116
116
  export type Snippet = schemas["Snippet"];
117
117
  export type SnippetSection = schemas["SnippetSection"];
118
118
  export type Snippets = schemas["Snippets"];
119
- export type StdinRequest = schemas["StdinRequest"];
120
119
  export type SuccessResponse = schemas["SuccessResponse"];
121
120
  export type UpdateUIElementValuesRequest =
122
121
  schemas["UpdateUIElementValuesRequest"];
@@ -163,9 +162,7 @@ export interface EditRequests {
163
162
  */
164
163
  saveLayout: (request: SaveLayoutRequest) => Promise<null>;
165
164
  sendCopy: (request: CopyNotebookRequest) => Promise<null>;
166
- sendStdin: (request: StdinRequest) => Promise<null>;
167
165
  sendRunScratchpad: (request: ExecuteScratchpadRequest) => Promise<null>;
168
- sendInterrupt: () => Promise<null>;
169
166
  sendShutdown: () => Promise<null>;
170
167
  sendFormat: (request: FormatCellsRequest) => Promise<FormatResponse>;
171
168
  sendCodeCompletionRequest: (request: CodeCompletionRequest) => Promise<null>;
@@ -1,6 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import type { Result } from "@marimo-team/client";
3
+ import type { ResultOf } from "@marimo-team/client";
4
4
  import type * as Protocol from "@marimo-team/client/protocol";
5
5
  import { atom, createStore } from "jotai/vanilla";
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -11,7 +11,7 @@ import { createDocumentModel } from "../model";
11
11
  import { type FakeEdit, fakeBackend } from "../../replica/__tests__/fixtures";
12
12
  import { cell, document, withCells } from "./fixtures";
13
13
 
14
- function response(confirmed: Document): Result<Protocol.EditResponseOutput> {
14
+ function response(confirmed: Document): ResultOf<"notebook.edit"> {
15
15
  return {
16
16
  ok: true,
17
17
  status: 200,
@@ -41,9 +41,9 @@ function setup({ connected = true } = {}) {
41
41
  const source = atom<Document | null>(document());
42
42
  const unmounted = vi.fn();
43
43
  source.onMount = () => unmounted;
44
- const replies: Deferred<Result<Protocol.EditResponseOutput>>[] = [];
44
+ const replies: Deferred<ResultOf<"notebook.edit">>[] = [];
45
45
  const sent = vi.fn((_edit: SentEdit) => {
46
- const reply = new Deferred<Result<Protocol.EditResponseOutput>>();
46
+ const reply = new Deferred<ResultOf<"notebook.edit">>();
47
47
  replies.push(reply);
48
48
  return reply.promise;
49
49
  });
@@ -24,16 +24,16 @@ it("matches the portable host examples for accepted and conflicting edits", () =
24
24
  notebookId: "n1",
25
25
  idempotencyKey: "rejected",
26
26
  changes: Protocol.EditRequest.parse(
27
- protocolFixture("edit-conflict-request.json"),
27
+ protocolFixture("stale-edit-request.json"),
28
28
  ).changes,
29
29
  });
30
30
  const problem = Protocol.Problem.parse(
31
- protocolFixture("problems/edit-conflict.json"),
31
+ protocolFixture("problems/precondition-failed.json"),
32
32
  );
33
33
  expect(rejected).toEqual({
34
34
  result: {
35
35
  ok: false,
36
- status: 409,
36
+ status: 412,
37
37
  problem: { ...problem, detail: expect.any(String) },
38
38
  },
39
39
  });
@@ -48,7 +48,7 @@ it("rejects an entire batch when a later precondition is stale", () => {
48
48
  { kind: "delete_cell", id: "c3", base_etag: "stale" },
49
49
  ]);
50
50
  expect(result).toMatchObject({
51
- result: { ok: false, status: 409, problem: { edit: { change: 1 } } },
51
+ result: { ok: false, status: 412, problem: { index: 1 } },
52
52
  });
53
53
  expect(host.document()).toEqual(initial);
54
54
  expect(result.update).toBeUndefined();
@@ -67,7 +67,7 @@ it("keeps successful and rejected receipts tied to their original payload and re
67
67
  notebookId: "n1",
68
68
  idempotencyKey: "stale",
69
69
  changes: Protocol.EditRequest.parse(
70
- protocolFixture("edit-conflict-request.json"),
70
+ protocolFixture("stale-edit-request.json"),
71
71
  ).changes,
72
72
  };
73
73
  const rejected = host.process(stale);
@@ -86,7 +86,7 @@ it("keeps successful and rejected receipts tied to their original payload and re
86
86
  vi.advanceTimersByTime(300_000);
87
87
  expect(host.process(request).result).toMatchObject({
88
88
  ok: false,
89
- status: 409,
89
+ status: 412,
90
90
  });
91
91
  expect(host.document()).toEqual(latest);
92
92
  });