@marimo-team/islands 0.25.3-dev169 → 0.25.3-dev170

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 (123) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BbDrlHvG.js → ConnectedDataExplorerComponent-BbSXV-Xa.js} +8 -8
  2. package/dist/{ErrorBoundary-RaIwGWT3.js → ErrorBoundary-NE86qEWZ.js} +4 -4
  3. package/dist/{abnfDiagram-VCTEODGH-Dr-fo8_z.js → abnfDiagram-VCTEODGH-D7n07nXf.js} +2 -2
  4. package/dist/{architectureDiagram-5GKGNRK7-ByKIl9GG.js → architectureDiagram-5GKGNRK7-D6CjRib5.js} +1 -1
  5. package/dist/{chat-ui-BsV2WH-4.js → chat-ui-CC7GekpG.js} +34 -34
  6. package/dist/{chunk-IMKFNOWR-IKDP18MQ.js → chunk-IMKFNOWR-6dtPz7uS.js} +1 -1
  7. package/dist/{chunk-SHT3W25Y-LkIgegwh.js → chunk-SHT3W25Y-BwnFNKDA.js} +1 -1
  8. package/dist/{chunk-SVP7TREG-DReBYtnB.js → chunk-SVP7TREG-D2eV4vBJ.js} +1 -1
  9. package/dist/{chunk-TICWLB2K-DQ0A0-1D.js → chunk-TICWLB2K-BDs4-_-r.js} +1 -1
  10. package/dist/{classDiagram-ZZMXUADV-CwvXdCNE.js → classDiagram-ZZMXUADV-DCJnf_8v.js} +2 -2
  11. package/dist/{classDiagram-v2-VYDZK3BY-D0XAxDsc.js → classDiagram-v2-VYDZK3BY-BOmSzPBD.js} +2 -2
  12. package/dist/{code-visibility-BMi0KLAQ.js → code-visibility-Bzvc6ajl.js} +23 -23
  13. package/dist/{common-BMkJrZsD.js → common-Ciw4zkh2.js} +3412 -3412
  14. package/dist/{cynefinDiagram-5FMLGOSQ-BFN68nHr.js → cynefinDiagram-5FMLGOSQ-Bln_Soky.js} +1 -1
  15. package/dist/{dagre-GXQ25YYZ-CZt4rtLq.js → dagre-GXQ25YYZ-D-nf7WzW.js} +1 -1
  16. package/dist/{data-grid-overlay-editor-CCc1P42P.js → data-grid-overlay-editor-AQREmxI7.js} +1 -1
  17. package/dist/{diagram-S7CK7UJ4-D5cOz47_.js → diagram-S7CK7UJ4-PsgSMkY2.js} +1 -1
  18. package/dist/{diagram-UQ7AKVKN-Qgdppb4a.js → diagram-UQ7AKVKN-DfwfUuSw.js} +1 -1
  19. package/dist/{diagram-VX7I27RA-BsDCvily.js → diagram-VX7I27RA-C9_e-oMc.js} +1 -1
  20. package/dist/{diagram-Z3DM3KII-igTkGjKI.js → diagram-Z3DM3KII-CWLa5tuY.js} +1 -1
  21. package/dist/{dropdown-menu-BWnnAstC.js → dropdown-menu-B6ftyzOg.js} +1 -1
  22. package/dist/{ebnfDiagram-PWID7BFC-BHuZGjIr.js → ebnfDiagram-PWID7BFC-B1QRM58v.js} +2 -2
  23. package/dist/{erDiagram-RLTQ6QDP-DKObXTVE.js → erDiagram-RLTQ6QDP-DJK_FqvW.js} +1 -1
  24. package/dist/{error-entries-BpUJSLUZ.js → error-entries-DgK3wUJR.js} +5640 -5591
  25. package/dist/{feedback-button-CMVkEos_.js → feedback-button-8a6SETmT.js} +24 -24
  26. package/dist/{flowDiagram-HODETNUW-DZI_kit0.js → flowDiagram-HODETNUW-Uxc25XSJ.js} +2 -2
  27. package/dist/{formats-CwJHWasD.js → formats-DQCyPd5Z.js} +1 -1
  28. package/dist/{glide-data-editor-BA2pcHSV.js → glide-data-editor-B70oke_v.js} +5 -5
  29. package/dist/{globals-B5jjJ0ms.js → globals-Cxd5hNlI.js} +2 -2
  30. package/dist/{html-to-image-CQr2-IDA.js → html-to-image-D4yG5Ru0.js} +25 -25
  31. package/dist/{infoDiagram-27XIBGKW-DdPl-7-n.js → infoDiagram-27XIBGKW-DmjxqwL0.js} +1 -1
  32. package/dist/{ishikawaDiagram-5VMMS53U-DbyW6l59.js → ishikawaDiagram-5VMMS53U-NgCLHqQi.js} +1 -1
  33. package/dist/{kanban-definition-UXKFOSKX-BMX9SyBS.js → kanban-definition-UXKFOSKX-CZaG-iam.js} +1 -1
  34. package/dist/main.js +187 -188
  35. package/dist/{mermaid-C8W8mTbA.js → mermaid-CSvw7c71.js} +34 -34
  36. package/dist/{mindmap-definition-YA3MSWOX-Zd_tAxWL.js → mindmap-definition-YA3MSWOX-DJYQBDH_.js} +1 -1
  37. package/dist/{number-overlay-editor-BIWiV-TU.js → number-overlay-editor-ChK-SKUk.js} +1 -1
  38. package/dist/{pegDiagram-XKGWAZYB-DjYydPdo.js → pegDiagram-XKGWAZYB-CHKO_dAe.js} +2 -2
  39. package/dist/{pieDiagram-E7YTZNPT-R5LKjO2n.js → pieDiagram-E7YTZNPT-DVevbMAo.js} +1 -1
  40. package/dist/{process-output-Uw-VJE5V.js → process-output-DxvBAeL3.js} +3 -3
  41. package/dist/{railroadDiagram-O6MQD6OU-C7uNKtfm.js → railroadDiagram-O6MQD6OU-Detj1kcB.js} +2 -2
  42. package/dist/{requirementDiagram-BXWQKSXE-CxAIzQfl.js → requirementDiagram-BXWQKSXE-BLFD0T_Z.js} +1 -1
  43. package/dist/{reveal-component-XiecZ3Ua.js → reveal-component-CXMXeDZD.js} +6 -6
  44. package/dist/{sequenceDiagram-WJ2MYXX4-B5tmlPd5.js → sequenceDiagram-WJ2MYXX4-BZBgatQV.js} +1 -1
  45. package/dist/{spec-45FnujvG.js → spec-Mnkn3XEr.js} +1 -1
  46. package/dist/{stateDiagram-D77RDMKH-C1hc5H7Y.js → stateDiagram-D77RDMKH-4BZW9-6_.js} +2 -2
  47. package/dist/{stateDiagram-v2-MP3YSRHH-DRWeqQTn.js → stateDiagram-v2-MP3YSRHH-C4sKC8Iq.js} +2 -2
  48. package/dist/style.css +1 -1
  49. package/dist/{swimlanes-42K2YHIH-NTLas--s.js → swimlanes-42K2YHIH-SL5yeGe2.js} +1 -1
  50. package/dist/{swimlanesDiagram-VR7AAH4N-ChPggljD.js → swimlanesDiagram-VR7AAH4N-DqPmAYUv.js} +2 -2
  51. package/dist/{timeline-definition-24CTP7MA-B4CZ9srJ.js → timeline-definition-24CTP7MA-BfXUMWkq.js} +1 -1
  52. package/dist/{toDate-rySyYxqk.js → toDate-TMubKIVL.js} +22 -22
  53. package/dist/{useAsyncData-B7MiltyV.js → useAsyncData-DjMGFzFy.js} +1 -1
  54. package/dist/{useDeepCompareMemoize-DZmHBD_1.js → useDeepCompareMemoize-BQR5TJot.js} +1 -1
  55. package/dist/{useLifecycle-DRbRhzxg.js → useLifecycle-C_GvKWbQ.js} +26 -23
  56. package/dist/{useTheme-TLkqJsvo.js → useTheme-Bns2gJZ6.js} +179 -178
  57. package/dist/{vega-component-DC6P127L.js → vega-component-BsVptuhz.js} +5 -5
  58. package/dist/{vennDiagram-4TSXK5OY-DJIvMt44.js → vennDiagram-4TSXK5OY-CH65znGa.js} +1 -1
  59. package/dist/{wardleyDiagram-VM6X3IG4-45H0zg0u.js → wardleyDiagram-VM6X3IG4-z9pWKb2_.js} +1 -1
  60. package/dist/{xychartDiagram-S5SC5T6Z-ZT-XzcPN.js → xychartDiagram-S5SC5T6Z-MxyaWjjq.js} +1 -1
  61. package/package.json +1 -1
  62. package/src/__mocks__/requests.ts +0 -1
  63. package/src/components/editor/cell/StagedAICell.tsx +10 -2
  64. package/src/components/editor/cell/code/cell-editor.tsx +7 -14
  65. package/src/components/editor/chrome/wrapper/__tests__/document-sync-status.test.tsx +92 -0
  66. package/src/components/editor/chrome/wrapper/footer-items/document-sync-status.tsx +54 -0
  67. package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
  68. package/src/core/ai/__tests__/staged-cells.test.ts +8 -5
  69. package/src/core/ai/staged-cells.ts +7 -2
  70. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +3 -15
  71. package/src/core/ai/tools/edit-notebook-tool.ts +14 -3
  72. package/src/core/cells/__tests__/cells.test.ts +0 -297
  73. package/src/core/cells/__tests__/document.test.ts +410 -0
  74. package/src/core/cells/__tests__/flush-document.test.ts +40 -0
  75. package/src/core/cells/__tests__/notebook-outline.test.ts +1 -6
  76. package/src/core/cells/__tests__/with-document-save.test.ts +67 -0
  77. package/src/core/cells/cells.ts +27 -158
  78. package/src/core/cells/columns.ts +38 -0
  79. package/src/core/cells/document.ts +360 -0
  80. package/src/core/cells/types.ts +12 -0
  81. package/src/core/codemirror/cells/__tests__/extensions.test.ts +27 -1
  82. package/src/core/codemirror/cells/extensions.ts +14 -0
  83. package/src/core/codemirror/format.ts +6 -5
  84. package/src/core/codemirror/language/__tests__/utils.test.ts +187 -15
  85. package/src/core/codemirror/language/cell-code.ts +114 -0
  86. package/src/core/codemirror/language/extension.ts +47 -30
  87. package/src/core/codemirror/language/languages/sql/sql.ts +7 -17
  88. package/src/core/codemirror/language/utils.ts +9 -39
  89. package/src/core/codemirror/lsp/notebook-lsp.ts +6 -5
  90. package/src/core/codemirror/rtc/extension.ts +0 -1
  91. package/src/core/edit-app.tsx +4 -3
  92. package/src/core/export/layout.ts +2 -2
  93. package/src/core/islands/bridge.ts +0 -1
  94. package/src/core/network/requests-lazy.ts +0 -1
  95. package/src/core/network/requests-network.ts +0 -9
  96. package/src/core/network/requests-static.ts +0 -1
  97. package/src/core/network/requests-toasting.tsx +0 -1
  98. package/src/core/network/types.ts +0 -5
  99. package/src/core/notebook/document/__tests__/codemirror.test.ts +76 -14
  100. package/src/core/notebook/document/__tests__/hold.test.ts +88 -0
  101. package/src/core/notebook/document/__tests__/hooks.test.tsx +3 -3
  102. package/src/core/notebook/document/__tests__/lifecycle-scenarios.test.ts +3 -3
  103. package/src/core/notebook/document/__tests__/model.test.ts +71 -15
  104. package/src/core/notebook/document/__tests__/scenario-fixture.ts +1 -1
  105. package/src/core/notebook/document/codemirror.ts +27 -122
  106. package/src/core/notebook/document/hooks.tsx +2 -2
  107. package/src/core/notebook/document/index.ts +9 -1
  108. package/src/core/notebook/document/model.ts +74 -24
  109. package/src/core/notebook/document/sync.ts +53 -4
  110. package/src/core/run-app.tsx +44 -41
  111. package/src/core/saving/save-component.tsx +2 -5
  112. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +63 -3
  113. package/src/core/session/active-notebook.ts +4 -0
  114. package/src/core/session/useNotebookSession.tsx +39 -19
  115. package/src/core/wasm/__tests__/bridge.test.ts +3 -3
  116. package/src/core/wasm/bridge.ts +5 -12
  117. package/src/core/wasm/worker/worker.ts +0 -9
  118. package/src/utils/__tests__/{createReducer.test.ts → createReducer.test.tsx} +28 -0
  119. package/src/utils/createReducer.ts +42 -21
  120. package/src/core/cells/__tests__/apply-transaction.test.ts +0 -991
  121. package/src/core/cells/__tests__/document-changes.test.ts +0 -1140
  122. package/src/core/cells/__tests__/document-roundtrip.test.ts +0 -392
  123. package/src/core/cells/document-changes.ts +0 -944
@@ -3,20 +3,20 @@ import { g as t, h as n, m as r, o as i } from "./button-BQRdqQi1.js";
3
3
  import { t as a } from "./react-sHtZdjeP.js";
4
4
  import { t as o } from "./compiler-runtime-CldRdSsv.js";
5
5
  import { t as s } from "./tooltip-CJ4Rqasp.js";
6
- import { C as c, a as l } from "./toDate-rySyYxqk.js";
6
+ import { C as c, a as l } from "./toDate-TMubKIVL.js";
7
7
  import { t as u } from "./jsx-runtime-3BD1kT0s.js";
8
8
  import { n as d } from "./error-banner-D40pmfQQ.js";
9
9
  import { t as f } from "./debounce-C3jyATpc.js";
10
- import { A as p, n as ee } from "./useTheme-TLkqJsvo.js";
10
+ import { A as p, n as ee } from "./useTheme-Bns2gJZ6.js";
11
11
  import { c as m, t as h } from "./arrays-R6JsImiT.js";
12
12
  import { t as te } from "./objects-DCmMvZtn.js";
13
- import { t as ne } from "./useAsyncData-B7MiltyV.js";
14
- import { a as g, i as re, n as ie, t as ae } from "./formats-CwJHWasD.js";
13
+ import { t as ne } from "./useAsyncData-DjMGFzFy.js";
14
+ import { a as g, i as re, n as ie, t as ae } from "./formats-DQCyPd5Z.js";
15
15
  import { t as _ } from "./semaphore-89jsoHtS.js";
16
16
  import { D as oe, n as se } from "./vega-loader.browser-B9Pz0EGP.js";
17
17
  import { t as ce } from "./react-vega-C4T9Rb_w.js";
18
18
  import { a as le, n as v } from "./loader-DOVB0Syl.js";
19
- import { t as ue } from "./useDeepCompareMemoize-DZmHBD_1.js";
19
+ import { t as ue } from "./useDeepCompareMemoize-BQR5TJot.js";
20
20
  import { t as de } from "./use-vega-container-remeasure-Bz4PZMtt.js";
21
21
  //#region src/plugins/impl/vega/fix-relative-url.ts
22
22
  var fe = /* @__PURE__ */ e(a(), 1), pe = o();
@@ -3,7 +3,7 @@ import { t } from "./src-CA_m4ONl.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-BwiyaNpg.js";
5
5
  import { t as _ } from "./rough.esm-CtV9AnV1.js";
6
- import { p as v } from "./mermaid-C8W8mTbA.js";
6
+ import { p as v } from "./mermaid-CSvw7c71.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-D7gxIs2Q.js";
2
2
  import { n as t } from "./src-CA_m4ONl.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-BwiyaNpg.js";
5
- import { p as m } from "./mermaid-C8W8mTbA.js";
5
+ import { p as m } from "./mermaid-CSvw7c71.js";
6
6
  import { n as h } from "./mermaid-parser.core-DxuSfYwJ.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-BwiyaNpg.js";
10
10
  import "./chunk-PWAF6VOD-CQw5qATZ.js";
11
11
  import { t as b } from "./chunk-GMAD6QVW-BhaJx0X3.js";
12
- import { p as x } from "./mermaid-C8W8mTbA.js";
12
+ import { p as x } from "./mermaid-CSvw7c71.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-dev169",
3
+ "version": "0.25.3-dev170",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -17,7 +17,6 @@ export const MockRequestClient = {
17
17
  sendModelValue: vi.fn().mockResolvedValue({}),
18
18
  sendRename: vi.fn().mockResolvedValue({}),
19
19
  sendRestart: vi.fn().mockResolvedValue({}),
20
- sendDocumentTransaction: vi.fn().mockResolvedValue({}),
21
20
  sendSave: vi.fn().mockResolvedValue({}),
22
21
  sendCopy: vi.fn().mockResolvedValue({}),
23
22
  sendStdin: vi.fn().mockResolvedValue({}),
@@ -11,7 +11,7 @@ import {
11
11
  } from "@/core/ai/staged-cells";
12
12
  import { getCellEditorView } from "@/core/cells/cells";
13
13
  import type { CellId } from "@/core/cells/ids";
14
- import { updateEditorCodeFromPython } from "@/core/codemirror/language/utils";
14
+ import { prepareCellCodeUpdate } from "@/core/codemirror/language/cell-code";
15
15
  import { cn } from "@/utils/cn";
16
16
  import { Logger } from "@/utils/Logger";
17
17
  import {
@@ -144,7 +144,15 @@ export function rejectStagedCell(
144
144
  break;
145
145
  }
146
146
 
147
- updateEditorCodeFromPython(editorView, edit.previousCode);
147
+ const update = prepareCellCodeUpdate(
148
+ editorView.state,
149
+ edit.previousCode,
150
+ { initialize: true },
151
+ );
152
+
153
+ if (update) {
154
+ editorView.dispatch(update);
155
+ }
148
156
  removeStagedCell(cellId);
149
157
  break;
150
158
  }
@@ -24,10 +24,8 @@ import {
24
24
  reconfigureLanguageEffect,
25
25
  switchLanguage,
26
26
  } from "@/core/codemirror/language/extension";
27
- import {
28
- getEditorCodeAsPython,
29
- updateEditorCodeFromPython,
30
- } from "@/core/codemirror/language/utils";
27
+ import { prepareCellCodeUpdate } from "@/core/codemirror/language/cell-code";
28
+ import { getEditorCodeAsPython } from "@/core/codemirror/language/utils";
31
29
  import { MARKDOWN_INITIAL_HIDE_CODE } from "@/core/codemirror/language/languages/markdown";
32
30
  import type { LanguageAdapterType } from "@/core/codemirror/language/types";
33
31
  import {
@@ -253,7 +251,10 @@ const CellEditorInternal = ({
253
251
  ? `${existing.trimEnd()}\n\n${code}`
254
252
  : code;
255
253
  if (view) {
256
- updateEditorCodeFromPython(view, merged);
254
+ const update = prepareCellCodeUpdate(view.state, merged);
255
+ if (update) {
256
+ view.dispatch(update);
257
+ }
257
258
  }
258
259
  cellActions.updateCellCode({
259
260
  cellId: SETUP_CELL_ID,
@@ -363,15 +364,7 @@ const CellEditorInternal = ({
363
364
  return composeRtcEditorConfig(code, extensions, undefined);
364
365
  }
365
366
 
366
- const rtc = realTimeCollaboration(
367
- cellId,
368
- (code) => {
369
- // It's not really a formatting change,
370
- // but this means it won't be marked as stale
371
- cellActions.updateCellCode({ cellId, code, formattingChange: true });
372
- },
373
- initialCode,
374
- );
367
+ const rtc = realTimeCollaboration(cellId, initialCode);
375
368
  return composeRtcEditorConfig(code, extensions, rtc);
376
369
  };
377
370
 
@@ -0,0 +1,92 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, fireEvent, render, screen } from "@testing-library/react";
4
+ import { atom, createStore, Provider } from "jotai";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { TooltipProvider } from "@/components/ui/tooltip";
7
+ import { createDocumentModel } from "@/core/notebook/document";
8
+ import { document } from "@/core/notebook/document/__tests__/fixtures";
9
+ import {
10
+ type FakeEdit,
11
+ fakeBackend,
12
+ } from "@/core/notebook/replica/__tests__/fixtures";
13
+ import { activeDocumentModelAtom } from "@/core/session/active-notebook";
14
+ import { DocumentSyncStatus } from "../footer-items/document-sync-status";
15
+
16
+ const lifetimes: AbortController[] = [];
17
+
18
+ afterEach(() => {
19
+ act(() => lifetimes.splice(0).forEach((controller) => controller.abort()));
20
+ });
21
+
22
+ const conflict = {
23
+ ok: false as const,
24
+ status: 409,
25
+ problem: {
26
+ type: "tag:marimo.io,2026:host/conflict",
27
+ title: "Conflict",
28
+ status: 409,
29
+ detail: "cell c1 has changed since it was read.",
30
+ },
31
+ };
32
+
33
+ function setup(edit: FakeEdit) {
34
+ const store = createStore();
35
+ const model = createDocumentModel({
36
+ store,
37
+ documentAtom: atom(document()),
38
+ backendAtom: atom(fakeBackend({ edit })),
39
+ });
40
+ const controller = new AbortController();
41
+ lifetimes.push(controller);
42
+ model.start({ signal: controller.signal });
43
+ store.set(activeDocumentModelAtom, model);
44
+ render(
45
+ <Provider store={store}>
46
+ <TooltipProvider>
47
+ <DocumentSyncStatus />
48
+ </TooltipProvider>
49
+ </Provider>,
50
+ );
51
+ return { store, model };
52
+ }
53
+
54
+ describe("DocumentSyncStatus", () => {
55
+ it("renders nothing while edits reach the server", () => {
56
+ setup(vi.fn<FakeEdit>());
57
+ expect(screen.queryByTestId("footer-document-sync")).toBeNull();
58
+ });
59
+
60
+ it("shows a rejected edit and resubmits it on click", async () => {
61
+ const accepted = document().revision;
62
+ const edit = vi
63
+ .fn<FakeEdit>()
64
+ .mockResolvedValueOnce(conflict)
65
+ .mockResolvedValueOnce({
66
+ ok: true,
67
+ status: 200,
68
+ data: {
69
+ document: { ...accepted, version: 2, etag: "document-2" },
70
+ cells: {},
71
+ },
72
+ });
73
+ const { store, model } = setup(edit);
74
+
75
+ await act(async () => {
76
+ store.set(model.editAtom, (tx) => tx.setCode("c1", "x = 2"));
77
+ await model.flush().catch(() => undefined);
78
+ });
79
+ const item = screen.getByTestId("footer-document-sync");
80
+ expect(item).toHaveAttribute(
81
+ "aria-label",
82
+ "Edits are not reaching the server",
83
+ );
84
+
85
+ await act(async () => {
86
+ fireEvent.click(item);
87
+ await Promise.resolve();
88
+ });
89
+ expect(edit).toHaveBeenCalledTimes(2);
90
+ expect(screen.queryByTestId("footer-document-sync")).toBeNull();
91
+ });
92
+ });
@@ -0,0 +1,54 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { atom, useAtomValue } from "jotai";
4
+ import { AlertCircleIcon } from "lucide-react";
5
+ import type React from "react";
6
+ import { activeDocumentModelAtom } from "@/core/session/active-notebook";
7
+ import { FooterItem } from "../footer-item";
8
+
9
+ /** The active notebook's document sync status, or null without a model. */
10
+ const syncStatusAtom = atom((get) => {
11
+ const model = get(activeDocumentModelAtom);
12
+ return model === null ? null : get(model.syncStatusAtom);
13
+ });
14
+
15
+ /**
16
+ * Shown when this client's edits stop reaching the server.
17
+ *
18
+ * Typing keeps working locally and Run still uses the editor's code, but
19
+ * other clients stop seeing the edits and Save fails until they are
20
+ * through. A rejected edit is retried on click.
21
+ */
22
+ export const DocumentSyncStatus: React.FC = () => {
23
+ const model = useAtomValue(activeDocumentModelAtom);
24
+ const status = useAtomValue(syncStatusAtom);
25
+ if (model === null || status === null || status.status !== "error") {
26
+ return null;
27
+ }
28
+ const retry = status.retryable
29
+ ? () => {
30
+ // The status shows the outcome, so a failed retry needs no handling.
31
+ model.retry().catch(() => undefined);
32
+ }
33
+ : undefined;
34
+ const detail = status.problem?.detail ?? status.error.message;
35
+ return (
36
+ <FooterItem
37
+ tooltip={
38
+ <div className="w-[260px] space-y-1">
39
+ <div>Edits are not reaching the server.</div>
40
+ <div className="text-muted-foreground">{detail}</div>
41
+ <div>
42
+ {retry ? "Click to retry." : "Reload the page to continue."}
43
+ </div>
44
+ </div>
45
+ }
46
+ selected={false}
47
+ onClick={retry}
48
+ aria-label="Edits are not reaching the server"
49
+ data-testid="footer-document-sync"
50
+ >
51
+ <AlertCircleIcon className="w-4 h-4 text-destructive" />
52
+ </FooterItem>
53
+ );
54
+ };
@@ -18,6 +18,7 @@ import {
18
18
  connectionStatusAtom,
19
19
  } from "./footer-items/backend-status";
20
20
  import { CopilotStatusIcon } from "./footer-items/copilot-status";
21
+ import { DocumentSyncStatus } from "./footer-items/document-sync-status";
21
22
  import { MachineStats } from "./footer-items/machine-stats";
22
23
  import { PyodideStatus } from "./footer-items/pyodide-status";
23
24
  import { RTCStatus } from "./footer-items/rtc-status";
@@ -107,6 +108,7 @@ export const Footer: React.FC = () => {
107
108
  </ShowInKioskMode>
108
109
 
109
110
  <div className="flex items-center shrink-0 min-w-0">
111
+ <DocumentSyncStatus />
110
112
  <MachineStats />
111
113
  <AttachmentStatus />
112
114
  <AIStatusIcon />
@@ -7,7 +7,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import { cellId } from "@/__tests__/branded";
8
8
  import { CellId } from "@/core/cells/ids";
9
9
  import { getCellEditorView } from "../../cells/cells";
10
- import { updateEditorCodeFromPython } from "../../codemirror/language/utils";
10
+ import { prepareCellCodeUpdate } from "../../codemirror/language/cell-code";
11
11
  import {
12
12
  stagedAICellsAtom,
13
13
  stagedGenerationInProgressAtom,
@@ -57,8 +57,8 @@ vi.mock("@/core/network/requests", () => ({
57
57
  getRequestClient: () => ({ sendRun: vi.fn() }),
58
58
  }));
59
59
 
60
- vi.mock("../../codemirror/language/utils", () => ({
61
- updateEditorCodeFromPython: vi.fn(),
60
+ vi.mock("../../codemirror/language/cell-code", () => ({
61
+ prepareCellCodeUpdate: vi.fn(() => ({ effects: [] })),
62
62
  }));
63
63
 
64
64
  // Mock CellId.create
@@ -402,7 +402,7 @@ describe("staged cell generation", () => {
402
402
 
403
403
  // No cell or cell update should have been called
404
404
  expect(mockCreateNewCell).not.toHaveBeenCalled();
405
- expect(updateEditorCodeFromPython).not.toHaveBeenCalled();
405
+ expect(prepareCellCodeUpdate).not.toHaveBeenCalled();
406
406
  expect(store.get(stagedGenerationInProgressAtom)).toBe(true);
407
407
  });
408
408
 
@@ -686,7 +686,10 @@ describe("staged cell generation", () => {
686
686
  expect(mockCreateNewCell).toHaveBeenCalledTimes(2);
687
687
  expect(store.get(stagedAICellsAtom).has(cellId("first-cell"))).toBe(true);
688
688
  expect(store.get(stagedAICellsAtom).has(cellId("second-cell"))).toBe(true);
689
- expect(updateEditorCodeFromPython).toHaveBeenCalledTimes(2);
689
+ expect(prepareCellCodeUpdate).toHaveBeenCalledTimes(2);
690
+ expect(
691
+ mockCellHandle.current.editorViewOrNull.dispatch,
692
+ ).toHaveBeenCalledTimes(2);
690
693
  });
691
694
 
692
695
  it("should update notebook state when the cell editor is not mounted", () => {
@@ -19,7 +19,7 @@ import {
19
19
  getCellEditorView,
20
20
  useCellActions,
21
21
  } from "../cells/cells";
22
- import { updateEditorCodeFromPython } from "../codemirror/language/utils";
22
+ import { prepareCellCodeUpdate } from "../codemirror/language/cell-code";
23
23
  import type { JotaiStore } from "../state/jotai";
24
24
  import type { EditType } from "./tools/edit-notebook-tool";
25
25
 
@@ -124,7 +124,12 @@ export function useStagedCells(store: JotaiStore) {
124
124
 
125
125
  const editorView = getCellEditorView(cellId);
126
126
  if (editorView) {
127
- updateEditorCodeFromPython(editorView, code);
127
+ const update = prepareCellCodeUpdate(editorView.state, code, {
128
+ initialize: true,
129
+ });
130
+ if (update) {
131
+ editorView.dispatch(update);
132
+ }
128
133
  } else {
129
134
  updateCellCode({ cellId, code, formattingChange: false });
130
135
  }
@@ -8,7 +8,6 @@ import { MockNotebook } from "@/__mocks__/notebook";
8
8
  import { cellId } from "@/__tests__/branded";
9
9
  import { notebookAtom } from "@/core/cells/cells";
10
10
  import type { CellId } from "@/core/cells/ids";
11
- import { updateEditorCodeFromPython } from "@/core/codemirror/language/utils";
12
11
  import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
13
12
  import { MultiColumn } from "@/utils/id-tree";
14
13
  import { cellConfigExtension } from "../../../codemirror/config/extension";
@@ -22,11 +21,6 @@ vi.mock("@/components/editor/links/cell-link", () => ({
22
21
  scrollAndHighlightCell: vi.fn(),
23
22
  }));
24
23
 
25
- // Mock updateEditorCodeFromPython
26
- vi.mock("@/core/codemirror/language/utils", () => ({
27
- updateEditorCodeFromPython: vi.fn(),
28
- }));
29
-
30
24
  function createMockEditorView(code: string): EditorView {
31
25
  return new EditorView({
32
26
  state: EditorState.create({
@@ -140,10 +134,7 @@ describe("EditNotebookTool", () => {
140
134
  );
141
135
 
142
136
  expect(result.status).toBe("success");
143
- expect(vi.mocked(updateEditorCodeFromPython)).toHaveBeenCalledWith(
144
- editorView,
145
- newCode,
146
- );
137
+ expect(editorView.state.doc.toString()).toBe(newCode);
147
138
 
148
139
  // Check that cell was staged
149
140
  const stagedCells = store.get(stagedAICellsAtom);
@@ -166,10 +157,7 @@ describe("EditNotebookTool", () => {
166
157
  );
167
158
 
168
159
  expect(result2.status).toBe("success");
169
- expect(vi.mocked(updateEditorCodeFromPython)).toHaveBeenCalledWith(
170
- editorView,
171
- "x = 3",
172
- );
160
+ expect(editorView.state.doc.toString()).toBe("x = 3");
173
161
 
174
162
  const stagedCells2 = store.get(stagedAICellsAtom);
175
163
  expect(stagedCells2.has(cellId1)).toBe(true);
@@ -206,7 +194,7 @@ describe("EditNotebookTool", () => {
206
194
 
207
195
  // Should NOT stage the cell or update the editor
208
196
  expect(toolContext.addStagedCell).not.toHaveBeenCalled();
209
- expect(vi.mocked(updateEditorCodeFromPython)).not.toHaveBeenCalled();
197
+ expect(editorView.state.doc.toString()).toBe(code);
210
198
 
211
199
  // No staged cells
212
200
  const stagedCells = store.get(stagedAICellsAtom);
@@ -9,7 +9,7 @@ import {
9
9
  notebookAtom,
10
10
  } from "@/core/cells/cells";
11
11
  import { CellId } from "@/core/cells/ids";
12
- import { updateEditorCodeFromPython } from "@/core/codemirror/language/utils";
12
+ import { prepareCellCodeUpdate } from "@/core/codemirror/language/cell-code";
13
13
  import type { CellColumnId } from "@/utils/id-tree";
14
14
  import { stagedAICellsAtom } from "../staged-cells";
15
15
  import {
@@ -117,7 +117,12 @@ export class EditNotebookTool implements AiTool<
117
117
 
118
118
  // If previous edit was from a new cell, just replace editor code with the new code
119
119
  if (existingStagedCell?.type === "add_cell") {
120
- updateEditorCodeFromPython(editorView, code);
120
+ const update = prepareCellCodeUpdate(editorView.state, code, {
121
+ initialize: true,
122
+ });
123
+ if (update) {
124
+ editorView.dispatch(update);
125
+ }
121
126
  break;
122
127
  }
123
128
 
@@ -137,7 +142,13 @@ export class EditNotebookTool implements AiTool<
137
142
  edit: { type: "update_cell", previousCode: previousCode },
138
143
  });
139
144
 
140
- updateEditorCodeFromPython(editorView, code);
145
+ const update = prepareCellCodeUpdate(editorView.state, code, {
146
+ initialize: true,
147
+ });
148
+
149
+ if (update) {
150
+ editorView.dispatch(update);
151
+ }
141
152
 
142
153
  break;
143
154
  }