@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
@@ -1,7 +1,7 @@
1
1
  import { a as e } from "./rolldown-runtime-BPCzCO3I.js";
2
2
  import { t } from "./react-Bgs_wQh3.js";
3
- import { h as n } from "./tooltip-DBv8QUvS.js";
4
- import { C as r } from "./select-Cv6lR39D.js";
3
+ import { h as n } from "./tooltip-BVbJPkcl.js";
4
+ import { C as r } from "./select-suaAxFuo.js";
5
5
  var i = n("chart-pie", [["path", {
6
6
  d: "M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z",
7
7
  key: "pzmjnu"
@@ -1,6 +1,6 @@
1
1
  import { a as e } from "./rolldown-runtime-BPCzCO3I.js";
2
2
  import { t } from "./react-Bgs_wQh3.js";
3
- import { Mt as n } from "./use-restore-focus-BxJh5Zfa.js";
3
+ import { Pt as n } from "./use-restore-focus-tPmARo1Q.js";
4
4
  //#region src/hooks/useDeepCompareMemoize.ts
5
5
  var r = /* @__PURE__ */ e(t(), 1);
6
6
  function i(e) {
@@ -2,7 +2,7 @@ import { a as e } from "./rolldown-runtime-BPCzCO3I.js";
2
2
  import { h as t, r as n } from "./button-qQhINE7z.js";
3
3
  import { t as r } from "./react-Bgs_wQh3.js";
4
4
  import { t as i } from "./compiler-runtime-C5KHtL5D.js";
5
- import { h as a } from "./tooltip-DBv8QUvS.js";
5
+ import { h as a } from "./tooltip-BVbJPkcl.js";
6
6
  import { t as o } from "./jsx-runtime-D8btoEHk.js";
7
7
  var s = a("calendar", [
8
8
  ["path", {
@@ -2,22 +2,22 @@ import { a as e } from "./rolldown-runtime-BPCzCO3I.js";
2
2
  import { g as t, h as n, m as r, s as i } from "./button-qQhINE7z.js";
3
3
  import { t as a } from "./react-Bgs_wQh3.js";
4
4
  import { t as o } from "./compiler-runtime-C5KHtL5D.js";
5
- import { t as s } from "./tooltip-DBv8QUvS.js";
6
- import { l as c, t as l } from "./config-B171m9zn.js";
5
+ import { t as s } from "./tooltip-BVbJPkcl.js";
6
+ import { l as c, t as l } from "./config-CvBnR945.js";
7
7
  import { t as u } from "./jsx-runtime-D8btoEHk.js";
8
- import { u as d } from "./use-restore-focus-BxJh5Zfa.js";
9
- import { n as f } from "./error-banner-3_4Lc93Y.js";
8
+ import { d } from "./use-restore-focus-tPmARo1Q.js";
9
+ import { n as f } from "./error-banner-DUB_qUeL.js";
10
10
  import { t as p } from "./debounce-DrhhziJv.js";
11
11
  import { t as ee } from "./objects-DCmMvZtn.js";
12
12
  import { t as te } from "./useEvent-BoN7U38T.js";
13
- import { a as ne, i as re, n as ie, t as m } from "./formats-aUcw6MrS.js";
13
+ import { a as ne, i as re, n as ie, t as m } from "./formats-hF6fETr6.js";
14
14
  import { a as h, t as g } from "./arrays-DoQ-AILx.js";
15
15
  import { t as ae } from "./useAsyncData-P5-Oovhq.js";
16
16
  import { t as _ } from "./semaphore-8KhQouXt.js";
17
17
  import { W as oe, n as se } from "./vega-loader.browser-Co9gYE8Z.js";
18
18
  import { t as ce } from "./react-vega-CGgQEf0O.js";
19
19
  import { a as le, n as v } from "./loader-Bin_dutt.js";
20
- import { t as ue } from "./useDeepCompareMemoize-Cb2GRQIS.js";
20
+ import { t as ue } from "./useDeepCompareMemoize-Bcu0DkCS.js";
21
21
  import { t as de } from "./use-vega-container-remeasure-BrUpWQfF.js";
22
22
  //#region src/plugins/impl/vega/fix-relative-url.ts
23
23
  var fe = /* @__PURE__ */ e(a(), 1), pe = o();
@@ -3,7 +3,7 @@ import { t } from "./src-D5uZaZib.js";
3
3
  import { n } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
4
4
  import { $ as r, H as i, K as a, U as o, a as s, b as c, c as l, et as u, f as d, nt as f, tt as p, v as m, w as h, y as g } from "./chunk-DU6HZSFF-4SDIhUTY.js";
5
5
  import { t as _ } from "./rough.esm-CtV9AnV1.js";
6
- import { p as v } from "./mermaid-C7lQ_2LJ.js";
6
+ import { p as v } from "./mermaid--2K0cmt9.js";
7
7
  //#region ../node_modules/.pnpm/khroma@2.1.0/node_modules/khroma/dist/methods/transparentize.js
8
8
  var y = (e, t) => p(e, "a", -t), b = 1e-10;
9
9
  function x(e, t) {
@@ -2,7 +2,7 @@ import { i as e } from "./chunk-75Z2AOVW-76L6My3P.js";
2
2
  import { n as t } from "./src-D5uZaZib.js";
3
3
  import { n } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
4
4
  import { D as r, H as i, K as a, U as o, a as s, b as c, c as l, v as u, w as d, x as f, y as p } from "./chunk-DU6HZSFF-4SDIhUTY.js";
5
- import { p as m } from "./mermaid-C7lQ_2LJ.js";
5
+ import { p as m } from "./mermaid--2K0cmt9.js";
6
6
  import { n as h } from "./mermaid-parser.core-DtOnl7m4.js";
7
7
  import { t as g } from "./chunk-JWPE2WC7-DJvv1t8U.js";
8
8
  //#region ../node_modules/.pnpm/mermaid@11.17.2/node_modules/mermaid/dist/chunks/mermaid.core/wardleyDiagram-VM6X3IG4.mjs
@@ -9,7 +9,7 @@ import { n as s } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
9
9
  import { D as c, H as l, K as u, U as d, a as f, b as p, c as m, f as h, v as g, w as _, y as v, z as y } from "./chunk-DU6HZSFF-4SDIhUTY.js";
10
10
  import "./chunk-PWAF6VOD-CRu_RGP2.js";
11
11
  import { t as b } from "./chunk-GMAD6QVW-BJGF8QZL.js";
12
- import { p as x } from "./mermaid-C7lQ_2LJ.js";
12
+ import { p as x } from "./mermaid--2K0cmt9.js";
13
13
  //#region ../node_modules/.pnpm/d3-scale@4.0.2/node_modules/d3-scale/src/band.js
14
14
  function S() {
15
15
  var e = t().unknown(void 0), r = e.domain, i = e.range, o = 0, s = 1, c, l, u = !1, d = 0, f = 0, p = .5;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.25.3-dev322",
3
+ "version": "0.25.3-dev348",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -19,9 +19,7 @@ export const MockRequestClient = {
19
19
  sendRestart: vi.fn().mockResolvedValue({}),
20
20
  saveLayout: vi.fn().mockResolvedValue({}),
21
21
  sendCopy: vi.fn().mockResolvedValue({}),
22
- sendStdin: vi.fn().mockResolvedValue({}),
23
22
  sendFormat: vi.fn().mockResolvedValue({ codes: {} }),
24
- sendInterrupt: vi.fn().mockResolvedValue({}),
25
23
  sendShutdown: vi.fn().mockResolvedValue({}),
26
24
  sendRunScratchpad: vi.fn().mockResolvedValue({}),
27
25
  sendInstantiate: vi.fn().mockResolvedValue({}),
@@ -0,0 +1,199 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { type UIMessage, validateUIMessages } from "ai";
4
+ import { describe, expect, it } from "vitest";
5
+ import { withNotebookContext } from "../notebook-context";
6
+
7
+ const question: UIMessage["parts"] = [{ type: "text", text: "Help me" }];
8
+
9
+ function user(parts: UIMessage["parts"]): UIMessage {
10
+ return { id: "user", role: "user", parts };
11
+ }
12
+
13
+ function context(data: unknown) {
14
+ return { type: "data-marimo-notebook", data };
15
+ }
16
+
17
+ const initial = {
18
+ sessionId: "session-1",
19
+ cells: { a: "x = 1", b: "y = x + 1" },
20
+ };
21
+
22
+ function initialHistory() {
23
+ return [user(withNotebookContext(question, [], initial))];
24
+ }
25
+
26
+ describe("notebook context", () => {
27
+ it("captures a complete initial snapshot including empty cells", () => {
28
+ expect(
29
+ withNotebookContext(question, [], {
30
+ sessionId: "session-1",
31
+ cells: { a: "" },
32
+ }),
33
+ ).toEqual([
34
+ context({
35
+ sessionId: "session-1",
36
+ cellIds: ["a"],
37
+ cells: [{ id: "a", code: "" }],
38
+ }),
39
+ ...question,
40
+ ]);
41
+ });
42
+
43
+ it("sends only a snapshot marker after restoring unchanged history", async () => {
44
+ const savedHistory = JSON.stringify(initialHistory());
45
+ const restored = await validateUIMessages({
46
+ messages: JSON.parse(savedHistory),
47
+ });
48
+ expect(withNotebookContext(question, restored, initial)).toEqual([
49
+ context({ sessionId: "session-1", cells: [] }),
50
+ ...question,
51
+ ]);
52
+ });
53
+
54
+ it("sends only changed or added cells without mutating the history", () => {
55
+ const history = initialHistory();
56
+ const saved = structuredClone(history);
57
+ expect(
58
+ withNotebookContext(question, history, {
59
+ sessionId: "session-1",
60
+ cells: { a: "x = 2", b: "y = x + 1", c: "" },
61
+ }),
62
+ ).toEqual([
63
+ context({
64
+ sessionId: "session-1",
65
+ cellIds: ["a", "b", "c"],
66
+ cells: [
67
+ { id: "a", code: "x = 2" },
68
+ { id: "c", code: "" },
69
+ ],
70
+ }),
71
+ ...question,
72
+ ]);
73
+ expect(history).toEqual(saved);
74
+ });
75
+
76
+ it("tracks deletions, reordering, and re-added cells", () => {
77
+ const history = initialHistory();
78
+ const deleted = withNotebookContext(question, history, {
79
+ sessionId: "session-1",
80
+ cells: { b: "y = x + 1" },
81
+ });
82
+ expect(deleted).toEqual([
83
+ context({ sessionId: "session-1", cellIds: ["b"], cells: [] }),
84
+ ...question,
85
+ ]);
86
+ history.push(user(deleted));
87
+ const readded = withNotebookContext(question, history, initial);
88
+ expect(readded).toEqual([
89
+ context({
90
+ sessionId: "session-1",
91
+ cellIds: ["a", "b"],
92
+ cells: [{ id: "a", code: "x = 1" }],
93
+ }),
94
+ ...question,
95
+ ]);
96
+ history.push(user(readded));
97
+ expect(
98
+ withNotebookContext(question, history, {
99
+ sessionId: "session-1",
100
+ cells: { b: "y = x + 1", a: "x = 1" },
101
+ }),
102
+ ).toEqual([
103
+ context({ sessionId: "session-1", cellIds: ["b", "a"], cells: [] }),
104
+ ...question,
105
+ ]);
106
+ });
107
+
108
+ it("represents an empty notebook explicitly", () => {
109
+ expect(
110
+ withNotebookContext(question, initialHistory(), {
111
+ sessionId: "session-1",
112
+ cells: {},
113
+ }),
114
+ ).toEqual([
115
+ context({ sessionId: "session-1", cellIds: [], cells: [] }),
116
+ ...question,
117
+ ]);
118
+ });
119
+
120
+ it("uses only the retained history when editing a message", () => {
121
+ const history = initialHistory();
122
+ const changed = { sessionId: "session-1", cells: { a: "x = 2" } };
123
+ const next = withNotebookContext(question, history, changed);
124
+ expect(
125
+ withNotebookContext(question, [...history, user(next)], initial),
126
+ ).toEqual([
127
+ context({
128
+ sessionId: "session-1",
129
+ cellIds: ["a", "b"],
130
+ cells: [
131
+ { id: "a", code: "x = 1" },
132
+ { id: "b", code: "y = x + 1" },
133
+ ],
134
+ }),
135
+ ...question,
136
+ ]);
137
+ expect(withNotebookContext(question, history, initial)).toEqual([
138
+ context({ sessionId: "session-1", cells: [] }),
139
+ ...question,
140
+ ]);
141
+ });
142
+
143
+ it("updates session identity without repeating code", () => {
144
+ expect(
145
+ withNotebookContext(question, initialHistory(), {
146
+ ...initial,
147
+ sessionId: "session-2",
148
+ }),
149
+ ).toEqual([context({ sessionId: "session-2", cells: [] }), ...question]);
150
+ });
151
+
152
+ it("captures a full snapshot for chats created before notebook context existed", () => {
153
+ const legacyHistory = [user(question)];
154
+ expect(withNotebookContext(question, legacyHistory, initial)).toEqual(
155
+ initialHistory()[0].parts,
156
+ );
157
+ });
158
+
159
+ it("compares against user snapshots independently of tool results", () => {
160
+ const history = initialHistory();
161
+ history.push({
162
+ id: "tool-result",
163
+ role: "assistant",
164
+ parts: [
165
+ {
166
+ type: "tool-edit_cell",
167
+ toolCallId: "edit-a",
168
+ state: "output-available",
169
+ input: { id: "a", code: "x = 2" },
170
+ output: { code: "x = 2" },
171
+ },
172
+ ],
173
+ });
174
+ expect(
175
+ withNotebookContext(question, history, {
176
+ sessionId: "session-1",
177
+ cells: { a: "x = 2", b: "y = x + 1" },
178
+ }),
179
+ ).toEqual([
180
+ context({
181
+ sessionId: "session-1",
182
+ cells: [{ id: "a", code: "x = 2" }],
183
+ }),
184
+ ...question,
185
+ ]);
186
+ });
187
+
188
+ it("can enter and leave full access without sending its notebook code", () => {
189
+ const full = withNotebookContext(question, [], { sessionId: "session-1" });
190
+ expect(full).toEqual([context({ sessionId: "session-1" }), ...question]);
191
+ const read = withNotebookContext(question, [user(full)], initial);
192
+ expect(read).toEqual(initialHistory()[0].parts);
193
+ expect(
194
+ withNotebookContext(question, [user(full), user(read)], {
195
+ sessionId: "session-1",
196
+ }),
197
+ ).toEqual([context({ sessionId: "session-1" }), ...question]);
198
+ });
199
+ });
@@ -93,7 +93,9 @@ import {
93
93
  useFileState,
94
94
  useMessageQueue,
95
95
  } from "./chat-utils";
96
- import { getCodes } from "@/core/codemirror/copilot/getCodes";
96
+ import { notebookCellCodes } from "@/core/codemirror/copilot/getCodes";
97
+ import { getSessionId } from "@/core/kernel/session";
98
+ import { withNotebookContext } from "./notebook-context";
97
99
  import { focusInputAndMoveToEnd } from "@/core/codemirror/utils";
98
100
  import ScrollToBottomButton from "./acp/scroll-to-bottom-button";
99
101
  import { useChatControllerId } from "./use-chat-controller-id";
@@ -501,7 +503,6 @@ const ChatPanelBody = () => {
501
503
 
502
504
  const completionBody = {
503
505
  uiMessages: options.messages,
504
- includeOtherCode: getCodes(""),
505
506
  options: store.get(chatOptionsAtom),
506
507
  };
507
508
 
@@ -588,10 +589,21 @@ const ChatPanelBody = () => {
588
589
  },
589
590
  });
590
591
 
592
+ const contextualize = useEvent(
593
+ (parts: ChatMessagePart[], history: UIMessage[]) =>
594
+ withNotebookContext(parts, history, {
595
+ sessionId: getSessionId(),
596
+ cells:
597
+ store.get(aiAtom)?.mode === "code_mode"
598
+ ? undefined
599
+ : store.get(notebookCellCodes),
600
+ }),
601
+ );
602
+
591
603
  const sendUserMessage = useEvent((parts: ChatMessagePart[]) => {
592
604
  clearAbortState();
593
605
  turn.start();
594
- sendMessage({ role: "user", parts });
606
+ sendMessage({ role: "user", parts: contextualize(parts, messages) });
595
607
  });
596
608
 
597
609
  const tryFlushQueuedMessages = useEvent(
@@ -753,12 +765,15 @@ const ChatPanelBody = () => {
753
765
  turn.start();
754
766
  sendMessage({
755
767
  role: "user",
756
- parts: [
757
- { type: "text" as const, text: initialMessage },
758
- ...(contextPart ? [contextPart] : []),
759
- ...(fileParts ?? []),
760
- ...attachments,
761
- ],
768
+ parts: contextualize(
769
+ [
770
+ { type: "text" as const, text: initialMessage },
771
+ ...(contextPart ? [contextPart] : []),
772
+ ...(fileParts ?? []),
773
+ ...attachments,
774
+ ],
775
+ [],
776
+ ),
762
777
  });
763
778
  clearFiles();
764
779
  setInput("");
@@ -812,12 +827,15 @@ const ChatPanelBody = () => {
812
827
  sendMessage({
813
828
  messageId: messageId, // replace the message
814
829
  role: "user",
815
- parts: [
816
- { type: "text", text: newValue },
817
- ...(contextPart ? [contextPart] : []),
818
- ...userFileParts,
819
- ...attachments,
820
- ],
830
+ parts: contextualize(
831
+ [
832
+ { type: "text", text: newValue },
833
+ ...(contextPart ? [contextPart] : []),
834
+ ...userFileParts,
835
+ ...attachments,
836
+ ],
837
+ messages.slice(0, index),
838
+ ),
821
839
  });
822
840
  },
823
841
  );
@@ -0,0 +1,80 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { DataUIPart, UIMessage } from "ai";
4
+ import { z } from "zod";
5
+
6
+ const notebookContextSchema = z.object({
7
+ sessionId: z.string(),
8
+ cells: z.array(z.object({ id: z.string(), code: z.string() })).optional(),
9
+ cellIds: z.array(z.string()).optional(),
10
+ });
11
+
12
+ type NotebookContext = z.infer<typeof notebookContextSchema>;
13
+ type NotebookContextPart = DataUIPart<{
14
+ "marimo-notebook": NotebookContext;
15
+ }>;
16
+
17
+ export const NOTEBOOK_CONTEXT_PART_TYPE =
18
+ "data-marimo-notebook" as const satisfies NotebookContextPart["type"];
19
+
20
+ interface NotebookSnapshot {
21
+ sessionId: string;
22
+ cells?: Readonly<Record<string, string>>;
23
+ }
24
+
25
+ export function withNotebookContext(
26
+ parts: UIMessage["parts"],
27
+ history: UIMessage[],
28
+ snapshot: NotebookSnapshot,
29
+ ): UIMessage["parts"] {
30
+ let cellIds: string[] | undefined;
31
+ const cells = new Map<string, string>();
32
+ for (const message of history) {
33
+ if (message.role !== "user") {
34
+ continue;
35
+ }
36
+ for (const part of message.parts) {
37
+ if (part.type !== NOTEBOOK_CONTEXT_PART_TYPE) {
38
+ continue;
39
+ }
40
+ const parsed = notebookContextSchema.safeParse(part.data);
41
+ if (!parsed.success) {
42
+ continue;
43
+ }
44
+ if (parsed.data.cellIds) {
45
+ cellIds = parsed.data.cellIds;
46
+ const retained = new Set(cellIds);
47
+ for (const id of cells.keys()) {
48
+ if (!retained.has(id)) {
49
+ cells.delete(id);
50
+ }
51
+ }
52
+ }
53
+ for (const cell of parsed.data.cells ?? []) {
54
+ cells.set(cell.id, cell.code);
55
+ }
56
+ }
57
+ }
58
+
59
+ const update: NotebookContext = { sessionId: snapshot.sessionId };
60
+ if (snapshot.cells) {
61
+ const nextIds = Object.keys(snapshot.cells);
62
+ if (
63
+ !cellIds ||
64
+ nextIds.length !== cellIds.length ||
65
+ nextIds.some((id, index) => id !== cellIds[index])
66
+ ) {
67
+ update.cellIds = nextIds;
68
+ }
69
+ const changed = Object.entries(snapshot.cells)
70
+ .filter(([id, code]) => cells.get(id) !== code)
71
+ .map(([id, code]) => ({ id, code }));
72
+ update.cells = changed;
73
+ }
74
+
75
+ const context: NotebookContextPart = {
76
+ type: NOTEBOOK_CONTEXT_PART_TYPE,
77
+ data: update,
78
+ };
79
+ return [context, ...parts];
80
+ }
@@ -1,7 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { SquareIcon } from "lucide-react";
3
3
  import type { JSX } from "react";
4
- import { useRequestClient } from "@/core/network/requests";
4
+ import { interruptRuntime } from "@/core/executions";
5
+ import { store } from "@/core/state/jotai";
5
6
  import type { RuntimeState } from "@/core/network/types";
6
7
  import { isAppInteractionDisabled } from "@/core/session/connection-utils";
7
8
  import type { ConnectionState } from "@/core/session/types";
@@ -15,7 +16,6 @@ export const StopButton = (props: {
15
16
  connectionState: ConnectionState;
16
17
  }): JSX.Element => {
17
18
  const { connectionState, status } = props;
18
- const { sendInterrupt } = useRequestClient();
19
19
 
20
20
  const running = status === "running";
21
21
 
@@ -26,7 +26,7 @@ export const StopButton = (props: {
26
26
  <ToolbarItem
27
27
  tooltip={renderShortcut("global.interrupt")}
28
28
  disabled={isAppInteractionDisabled(connectionState) || !showInterrupt}
29
- onClick={showInterrupt ? sendInterrupt : Functions.NOOP}
29
+ onClick={showInterrupt ? () => interruptRuntime(store) : Functions.NOOP}
30
30
  variant={showInterrupt ? "stale" : "disabled"}
31
31
  data-testid="run-button"
32
32
  >
@@ -20,6 +20,7 @@ import {
20
20
  import { menuItemVariants } from "@/components/ui/menu-items";
21
21
  import { Tooltip } from "@/components/ui/tooltip";
22
22
  import { toast } from "@/components/ui/use-toast";
23
+ import { useRestoreFocus } from "@/components/ui/use-restore-focus";
23
24
  import { useCellData, useCellRuntime } from "@/core/cells/cells";
24
25
  import { CellOutputId } from "@/core/cells/ids";
25
26
  import { isOutputEmpty } from "@/core/cells/outputs";
@@ -71,6 +72,8 @@ export const CellActionsContextMenu = ({
71
72
  const [imageRightClicked, setImageRightClicked] =
72
73
  React.useState<HTMLImageElement>();
73
74
  const suppressCloseAutoFocus = React.useRef(false);
75
+ const pendingAction = React.useRef<(() => void) | null>(null);
76
+ const restoreFocus = useRestoreFocus();
74
77
 
75
78
  const DEFAULT_CONTEXT_MENU_ITEMS: ActionButton[] = [
76
79
  {
@@ -194,7 +197,13 @@ export const CellActionsContextMenu = ({
194
197
  .filter((group) => group.length > 0);
195
198
 
196
199
  return (
197
- <ContextMenu>
200
+ <ContextMenu
201
+ onOpenChange={(open) => {
202
+ if (open) {
203
+ restoreFocus.onOpenAutoFocus();
204
+ }
205
+ }}
206
+ >
198
207
  <ContextMenuTrigger
199
208
  disabled={disabled}
200
209
  onContextMenu={(evt) => {
@@ -212,6 +221,13 @@ export const CellActionsContextMenu = ({
212
221
  className="w-[300px]"
213
222
  scrollable={true}
214
223
  onCloseAutoFocus={(evt) => {
224
+ const action = pendingAction.current;
225
+ pendingAction.current = null;
226
+ if (action) {
227
+ restoreFocus.onCloseAutoFocus(evt);
228
+ evt.preventDefault();
229
+ action();
230
+ }
215
231
  if (suppressCloseAutoFocus.current) {
216
232
  evt.preventDefault();
217
233
  suppressCloseAutoFocus.current = false;
@@ -270,7 +286,7 @@ export const CellActionsContextMenu = ({
270
286
  if (action.disableClick || action.disabled) {
271
287
  return;
272
288
  }
273
- action.handle(evt);
289
+ pendingAction.current = () => action.handle(evt);
274
290
  }}
275
291
  variant={action.variant}
276
292
  >
@@ -7,7 +7,7 @@ import { createRef } from "react";
7
7
  import { afterEach, describe, expect, it, vi } from "vitest";
8
8
  import { TooltipProvider } from "@/components/ui/tooltip";
9
9
  import { switchLanguage } from "@/core/codemirror/language/extension";
10
- import { LanguageToggle } from "../language-toggle";
10
+ import { LanguageToggle, LanguageToggles } from "../language-toggle";
11
11
 
12
12
  vi.mock("@/core/codemirror/language/extension", () => ({
13
13
  switchLanguage: vi.fn(),
@@ -51,4 +51,41 @@ describe("LanguageToggle", () => {
51
51
  view.destroy();
52
52
  }
53
53
  });
54
+
55
+ it("uses the correct callback for each language toggle", () => {
56
+ const view = new EditorView({ state: EditorState.create() });
57
+ const editorViewRef = createRef<EditorView>();
58
+ editorViewRef.current = view;
59
+
60
+ const onAfterToggleSQL = vi.fn();
61
+ const onAfterToggleMarkdown = vi.fn();
62
+
63
+ try {
64
+ render(
65
+ <TooltipProvider>
66
+ <LanguageToggles
67
+ editorViewRef={editorViewRef}
68
+ code=""
69
+ currentLanguageAdapter="python"
70
+ onAfterToggleSQL={onAfterToggleSQL}
71
+ onAfterToggleMarkdown={onAfterToggleMarkdown}
72
+ />
73
+ </TooltipProvider>,
74
+ );
75
+
76
+ const toggleButtons = screen.getAllByTestId("language-toggle-button");
77
+
78
+ fireEvent.click(toggleButtons[0]);
79
+
80
+ expect(onAfterToggleSQL).toHaveBeenCalledOnce();
81
+ expect(onAfterToggleMarkdown).not.toHaveBeenCalled();
82
+
83
+ fireEvent.click(toggleButtons[1]);
84
+
85
+ expect(onAfterToggleMarkdown).toHaveBeenCalledOnce();
86
+ expect(onAfterToggleSQL).toHaveBeenCalledOnce();
87
+ } finally {
88
+ view.destroy();
89
+ }
90
+ });
54
91
  });
@@ -511,7 +511,8 @@ const CellEditorInternal = ({
511
511
  code={code}
512
512
  editorViewRef={editorViewRef}
513
513
  currentLanguageAdapter={languageAdapter}
514
- onAfterToggle={afterToggleMarkdown}
514
+ onAfterToggleSQL={afterToggleSQL}
515
+ onAfterToggleMarkdown={afterToggleMarkdown}
515
516
  />
516
517
  </div>
517
518
  )}
@@ -16,7 +16,8 @@ interface LanguageTogglesProps {
16
16
  editorViewRef: React.RefObject<EditorView | null>;
17
17
  code: string;
18
18
  currentLanguageAdapter: LanguageAdapter["type"] | undefined;
19
- onAfterToggle: () => void;
19
+ onAfterToggleSQL: () => void;
20
+ onAfterToggleMarkdown: () => void;
20
21
  /**
21
22
  * Classes for the wrapper element. Defaults to the absolutely-positioned,
22
23
  * hover-revealed placement used inside the notebook cell editor.
@@ -28,7 +29,8 @@ export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
28
29
  editorViewRef,
29
30
  code,
30
31
  currentLanguageAdapter,
31
- onAfterToggle,
32
+ onAfterToggleSQL,
33
+ onAfterToggleMarkdown,
32
34
  className = "absolute right-3 top-2 z-20 flex hover-action gap-1",
33
35
  }) => {
34
36
  const canUseMarkdown = useMemo(
@@ -55,7 +57,7 @@ export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
55
57
  }
56
58
  toType="sql"
57
59
  displayName="SQL"
58
- onAfterToggle={onAfterToggle}
60
+ onAfterToggle={onAfterToggleSQL}
59
61
  />
60
62
  <LanguageToggle
61
63
  editorViewRef={editorViewRef}
@@ -72,7 +74,7 @@ export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
72
74
  }
73
75
  toType="markdown"
74
76
  displayName="Markdown"
75
- onAfterToggle={onAfterToggle}
77
+ onAfterToggle={onAfterToggleMarkdown}
76
78
  />
77
79
  <LanguageToggle
78
80
  editorViewRef={editorViewRef}