@marimo-team/frontend 0.24.3-dev64 → 0.24.3-dev68

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 (115) hide show
  1. package/dist/assets/{ConnectedDataExplorerComponent-BSvwB270.js → ConnectedDataExplorerComponent-B8qRI1J3.js} +1 -1
  2. package/dist/assets/{ErrorBoundary-TsEoIDRr.js → ErrorBoundary-B24BGOv-.js} +2 -2
  3. package/dist/assets/{LazyAnyLanguageCodeMirror-BSKy620l.js → LazyAnyLanguageCodeMirror-BiRrYKh3.js} +2 -2
  4. package/dist/assets/{RunButton-Dul9caMU.js → RunButton-CCeB-E93.js} +1 -1
  5. package/dist/assets/__vite-browser-external-Cw9Ccr_O.js +1 -0
  6. package/dist/assets/__vite-browser-external-DBuuG05M.js +1 -0
  7. package/dist/assets/{abnfDiagram-VCTEODGH-Cjyff0Dj.js → abnfDiagram-VCTEODGH-BAf7PJTE.js} +1 -1
  8. package/dist/assets/{add-cell-with-ai-BhDPL21V.js → add-cell-with-ai-Bfw26bS2.js} +1 -1
  9. package/dist/assets/{agent-panel-CYUY6ClW.js → agent-panel-DDQFUPQQ.js} +4 -4
  10. package/dist/assets/{ai-model-dropdown-CnDRzQfQ.js → ai-model-dropdown-BveigXNm.js} +1 -1
  11. package/dist/assets/{any-language-editor-C6lBHXNY.js → any-language-editor-qIp2874m.js} +1 -1
  12. package/dist/assets/{app-config-button-9I3Y-dHh.js → app-config-button-D12AZ5Jw.js} +1 -1
  13. package/dist/assets/{architectureDiagram-5GKGNRK7-BBYvsdwV.js → architectureDiagram-5GKGNRK7-DODMm5nu.js} +1 -1
  14. package/dist/assets/{blockDiagram-I7D4REHJ-XAi6eabU.js → blockDiagram-I7D4REHJ-CwnPIH04.js} +1 -1
  15. package/dist/assets/{cache-panel-DKXBQfDt.js → cache-panel-B_aqFT1r.js} +1 -1
  16. package/dist/assets/{cells-DdE_4Bcx.js → cells-BY2lhea7.js} +1 -1
  17. package/dist/assets/{chat-display-BlIEWw3Q.js → chat-display-DJTGR5xt.js} +1 -1
  18. package/dist/assets/{chat-panel-C_XE2bS-.js → chat-panel-Csz1liLp.js} +1 -1
  19. package/dist/assets/{chat-ui-BRq__jk9.js → chat-ui-CQkBUB_Q.js} +1 -1
  20. package/dist/assets/{chunk-IMKFNOWR-bmBDB0cN.js → chunk-IMKFNOWR-CMvWRWPq.js} +1 -1
  21. package/dist/assets/{chunk-SHT3W25Y-DFrgoI5E.js → chunk-SHT3W25Y-XssGvFwO.js} +1 -1
  22. package/dist/assets/{chunk-SVP7TREG-usES5uzt.js → chunk-SVP7TREG-JYPY-7he.js} +1 -1
  23. package/dist/assets/{chunk-TICWLB2K-DmYGvTDJ.js → chunk-TICWLB2K-DmYV5I8j.js} +1 -1
  24. package/dist/assets/{classDiagram-ZZMXUADV-8oWUrTSA.js → classDiagram-ZZMXUADV-DGESMhgO.js} +1 -1
  25. package/dist/assets/{classDiagram-v2-VYDZK3BY-8oWUrTSA.js → classDiagram-v2-VYDZK3BY-DGESMhgO.js} +1 -1
  26. package/dist/assets/{code-visibility-ChrgD3_b.js → code-visibility-DcZtpJ7Y.js} +1 -1
  27. package/dist/assets/{command-palette-D_70H77j.js → command-palette-DB5rqESX.js} +1 -1
  28. package/dist/assets/{common-BfKDjmi0.js → common-DUhY30A6.js} +1 -1
  29. package/dist/assets/{components-DyBejhtt.js → components-BTJRth05.js} +1 -1
  30. package/dist/assets/{components-BFKAEF3R.js → components-mqvQ3nnU.js} +1 -1
  31. package/dist/assets/{cynefinDiagram-5FMLGOSQ-CGpZt6Mr.js → cynefinDiagram-5FMLGOSQ-5s8Gxv8p.js} +1 -1
  32. package/dist/assets/{dagre-6C2JCFmZ.js → dagre-DIObFfj5.js} +1 -1
  33. package/dist/assets/{dagre-GXQ25YYZ-DwPg2iyK.js → dagre-GXQ25YYZ-DLWNkQz-.js} +1 -1
  34. package/dist/assets/{data-grid-overlay-editor-BHzBco-G.js → data-grid-overlay-editor-BhBuKg9_.js} +1 -1
  35. package/dist/assets/{dependency-graph-panel-DxbTpdSy.js → dependency-graph-panel-DbYPnFAW.js} +1 -1
  36. package/dist/assets/{diagram-S7CK7UJ4-D3rnjIpt.js → diagram-S7CK7UJ4-Vc_7tbGR.js} +1 -1
  37. package/dist/assets/{diagram-UQ7AKVKN--mvb4nAR.js → diagram-UQ7AKVKN-sFxmIB_B.js} +1 -1
  38. package/dist/assets/{diagram-VX7I27RA-DLd2xbGj.js → diagram-VX7I27RA-CeT45gHf.js} +1 -1
  39. package/dist/assets/{diagram-Z3DM3KII-DCQ7kQxu.js → diagram-Z3DM3KII-3qePplNH.js} +1 -1
  40. package/dist/assets/{documentation-panel-D1oy9mJX.js → documentation-panel-StHaPmjq.js} +1 -1
  41. package/dist/assets/{ebnfDiagram-PWID7BFC-BEPSCGTU.js → ebnfDiagram-PWID7BFC-fltqffql.js} +1 -1
  42. package/dist/assets/{edit-page-KRRlhyb9.js → edit-page-ByS6kRQA.js} +4 -4
  43. package/dist/assets/{erDiagram-RLTQ6QDP-DVP8JuYC.js → erDiagram-RLTQ6QDP-Bow67h2D.js} +1 -1
  44. package/dist/assets/{error-entries-8TMYgS7M.js → error-entries-BGsjaQkq.js} +1 -1
  45. package/dist/assets/{error-panel-DSHUnrwV.js → error-panel-CD7f6MTl.js} +1 -1
  46. package/dist/assets/{feedback-button-C-rqcadY.js → feedback-button-BfgkWpt7.js} +1 -1
  47. package/dist/assets/{file-explorer-panel-W4vRK0IK.js → file-explorer-panel-BAL_Pzqi.js} +1 -1
  48. package/dist/assets/{file-name-input-BQURTAD0.js → file-name-input-ClwxBT3X.js} +1 -1
  49. package/dist/assets/{flowDiagram-HODETNUW-CxI8veho.js → flowDiagram-HODETNUW-CC0MXK8f.js} +1 -1
  50. package/dist/assets/{gallery-page-CCiwxBoC.js → gallery-page-BfqIPyEv.js} +1 -1
  51. package/dist/assets/{glide-data-editor-YdUhj2-W.js → glide-data-editor-cUeC-J43.js} +4 -4
  52. package/dist/assets/{graphlib-Dqk_AhmH.js → graphlib-DNXvxVH3.js} +1 -1
  53. package/dist/assets/has-BWBvNXSE.js +1 -0
  54. package/dist/assets/{home-page-Bhfe_Ooj.js → home-page-CZOroZdb.js} +1 -1
  55. package/dist/assets/{index-kGHM0O4l.js → index-DPnCyt1L.js} +35 -35
  56. package/dist/assets/{infoDiagram-27XIBGKW-vkRRAmNG.js → infoDiagram-27XIBGKW-DUckrGOc.js} +1 -1
  57. package/dist/assets/{ishikawaDiagram-5VMMS53U-DwnkA3qK.js → ishikawaDiagram-5VMMS53U-BI5lrbqL.js} +1 -1
  58. package/dist/assets/{kanban-definition-UXKFOSKX-DTM4IJzH.js → kanban-definition-UXKFOSKX-BE_mJxg6.js} +1 -1
  59. package/dist/assets/{logs-panel-CpmZ43Xd.js → logs-panel-X7YYNXWi.js} +1 -1
  60. package/dist/assets/{mermaid-BYaoBhgB.js → mermaid-I9sRPejB.js} +4 -4
  61. package/dist/assets/{mindmap-definition-YA3MSWOX-BtrIeuN2.js → mindmap-definition-YA3MSWOX-BMFx2VBe.js} +1 -1
  62. package/dist/assets/{number-overlay-editor-uOD-jX8J.js → number-overlay-editor-EQ27ERwk.js} +1 -1
  63. package/dist/assets/{outline-panel-DHBB8jr2.js → outline-panel-DbnyiE-B.js} +1 -1
  64. package/dist/assets/{packages-panel-Ba0BIKw-.js → packages-panel-wCsQlp5Z.js} +1 -1
  65. package/dist/assets/{panels-CVG8esk7.js → panels-8_Hfok8J.js} +3 -3
  66. package/dist/assets/{pegDiagram-XKGWAZYB-72Js5lku.js → pegDiagram-XKGWAZYB-Bke1_i45.js} +1 -1
  67. package/dist/assets/{pieDiagram-E7YTZNPT-Bj3RCLfN.js → pieDiagram-E7YTZNPT-4n1GeAi8.js} +1 -1
  68. package/dist/assets/{process-output-cqVv_LQh.js → process-output-DDr6kJst.js} +1 -1
  69. package/dist/assets/{railroadDiagram-O6MQD6OU-B10zJ5xk.js → railroadDiagram-O6MQD6OU-DzSnQBmP.js} +1 -1
  70. package/dist/assets/{requirementDiagram-BXWQKSXE-BF4LA-gO.js → requirementDiagram-BXWQKSXE-Bjqcm878.js} +1 -1
  71. package/dist/assets/{reveal-component-Cvagx4WC.js → reveal-component-MnNIqkZg.js} +1 -1
  72. package/dist/assets/{run-page-O9BXJwTz.js → run-page-BGQahlCc.js} +1 -1
  73. package/dist/assets/{save-worker-CMa6Cvnx.js → save-worker-Dw8ybFWq.js} +3 -3
  74. package/dist/assets/{scratchpad-panel-CJacTdzi.js → scratchpad-panel-Wt_FZRjv.js} +1 -1
  75. package/dist/assets/{secrets-panel-BvtVCy_x.js → secrets-panel-DiIOsz3V.js} +1 -1
  76. package/dist/assets/{sequenceDiagram-WJ2MYXX4-Cz9d4L3l.js → sequenceDiagram-WJ2MYXX4-r-c3v7Ho.js} +1 -1
  77. package/dist/assets/{session-panel-C7l7daPJ.js → session-panel-F6G-YcGG.js} +1 -1
  78. package/dist/assets/{snippets-panel-BP4UI0bt.js → snippets-panel-mWSX1anY.js} +1 -1
  79. package/dist/assets/{stateDiagram-D77RDMKH-BYVxo4-Y.js → stateDiagram-D77RDMKH-COUIwUQY.js} +1 -1
  80. package/dist/assets/{stateDiagram-v2-MP3YSRHH-BO2Qvl2_.js → stateDiagram-v2-MP3YSRHH-DxCCjAQm.js} +1 -1
  81. package/dist/assets/{swimlanes-42K2YHIH-nJMxtt_e.js → swimlanes-42K2YHIH-CFQCC6A2.js} +1 -1
  82. package/dist/assets/{swimlanesDiagram-VR7AAH4N-2_BeIocM.js → swimlanesDiagram-VR7AAH4N-BEVDZiRo.js} +1 -1
  83. package/dist/assets/{swiper-component-VHJ-MBld.js → swiper-component-DGc_akcB.js} +1 -1
  84. package/dist/assets/{terminal-DfWCJPGz.js → terminal-BdlA-Q6e.js} +1 -1
  85. package/dist/assets/{timeline-definition-24CTP7MA-LaRikknb.js → timeline-definition-24CTP7MA-BJ-VJeHJ.js} +1 -1
  86. package/dist/assets/{tracing-njWlk06F.js → tracing-Bsl9kRwF.js} +1 -1
  87. package/dist/assets/{tracing-panel-BFMefFq8.js → tracing-panel-Cpes4Agb.js} +2 -2
  88. package/dist/assets/{useCellActionButton-Cie43jOq.js → useCellActionButton-BEOOTct7.js} +1 -1
  89. package/dist/assets/{vega-component-CrJlHxXg.js → vega-component-UjBXVqkX.js} +1 -1
  90. package/dist/assets/{vega-embed-container-DkDtQznq.js → vega-embed-container-NxkXguyz.js} +1 -1
  91. package/dist/assets/{vennDiagram-4TSXK5OY-y3U656IE.js → vennDiagram-4TSXK5OY-DT7_rCm5.js} +1 -1
  92. package/dist/assets/{wardleyDiagram-VM6X3IG4-COdor5IG.js → wardleyDiagram-VM6X3IG4-Ce6qiu-G.js} +1 -1
  93. package/dist/assets/{worker-B8c-1v-F.js → worker-ByzyDs4N.js} +3 -3
  94. package/dist/assets/{xychartDiagram-S5SC5T6Z-BHauP6L-.js → xychartDiagram-S5SC5T6Z-jjkhfqC-.js} +1 -1
  95. package/dist/index.html +4 -4
  96. package/package.json +1 -1
  97. package/src/components/chat/acp/__tests__/agent-panel-initialization.test.tsx +383 -0
  98. package/src/components/chat/acp/__tests__/agent-panel.test.tsx +10 -8
  99. package/src/components/chat/acp/__tests__/state.test.ts +2 -2
  100. package/src/components/chat/acp/agent-panel.tsx +70 -30
  101. package/src/components/chat/acp/state.ts +1 -1
  102. package/src/core/cells/__tests__/cells.test.ts +144 -0
  103. package/src/core/cells/cells.ts +7 -17
  104. package/src/core/wasm/__tests__/bridge.test.ts +5 -4
  105. package/src/core/wasm/__tests__/runtime-config.test.ts +8 -1
  106. package/src/core/wasm/bridge.ts +7 -8
  107. package/src/core/wasm/runtime-config.ts +4 -0
  108. package/src/core/wasm/worker/constants.ts +1 -0
  109. package/src/core/wasm/worker/getController.ts +3 -1
  110. package/src/core/wasm/worker/save-worker.ts +4 -10
  111. package/src/core/wasm/worker/types.ts +0 -1
  112. package/src/core/wasm/worker/worker.ts +4 -10
  113. package/dist/assets/__vite-browser-external-BKSyd8Ok.js +0 -1
  114. package/dist/assets/__vite-browser-external-ZTY8EBdt.js +0 -1
  115. package/dist/assets/has-BXV7_iwm.js +0 -1
@@ -718,13 +718,28 @@ const AgentPanel: React.FC = () => {
718
718
  clearNotifications,
719
719
  } = acpClient;
720
720
 
721
+ const isConnected = connectionState.status === "connected";
722
+ const [initializedConnection, setInitializedConnection] = useState<{
723
+ agent: NonNullable<typeof agent>;
724
+ wsUrl: string;
725
+ } | null>(null);
726
+ const isAgentReady =
727
+ isConnected &&
728
+ agent !== null &&
729
+ initializedConnection?.agent === agent &&
730
+ initializedConnection.wsUrl === wsUrl;
731
+
721
732
  useEffect(() => {
722
- if (!agent) {
733
+ setInitializedConnection(null);
734
+ setActiveSessionId(null);
735
+ if (!agent || !isConnected) {
723
736
  return;
724
737
  }
738
+ let cancelled = false;
739
+ setError(null);
725
740
 
726
- const initAndAuth = async () => {
727
- const response = await agent.initialize({
741
+ const initialize = async () => {
742
+ await agent.initialize({
728
743
  protocolVersion: 1,
729
744
  clientCapabilities: {
730
745
  fs: {
@@ -734,23 +749,26 @@ const AgentPanel: React.FC = () => {
734
749
  },
735
750
  });
736
751
 
737
- // We try to authenticate with the agent if it supports it.
738
- // The user must then restart the session
739
- const authMethods = response?.authMethods;
740
- if (authMethods && authMethods.length > 0) {
741
- await agent.authenticate({ methodId: authMethods[0].id });
752
+ // Agents use credentials from their CLI login. Advertised auth methods
753
+ // describe login options, not whether authentication is required.
754
+ if (!cancelled) {
755
+ setInitializedConnection({ agent, wsUrl });
742
756
  }
743
757
  };
744
758
 
745
- initAndAuth().catch((error) => {
746
- logger.error("Failed to initialize/authenticate agent", { error });
759
+ initialize().catch((error) => {
760
+ if (!cancelled) {
761
+ logger.error("Failed to initialize agent", { error });
762
+ setError(error instanceof Error ? error : String(error));
763
+ }
747
764
  });
748
- }, [agent]);
765
+ return () => {
766
+ cancelled = true;
767
+ };
768
+ }, [agent, isConnected, wsUrl, setActiveSessionId]);
749
769
 
750
770
  // Auto-connect to agent when we have an active session, but only once per session
751
771
  useEffect(() => {
752
- setActiveSessionId(null);
753
-
754
772
  if (wsUrl === NO_WS_SET) {
755
773
  return;
756
774
  }
@@ -770,7 +788,7 @@ const AgentPanel: React.FC = () => {
770
788
  }, [wsUrl]);
771
789
 
772
790
  const handleNewSession = useEvent(async () => {
773
- if (!agent) {
791
+ if (!agent || !isAgentReady) {
774
792
  return;
775
793
  }
776
794
 
@@ -814,7 +832,7 @@ const AgentPanel: React.FC = () => {
814
832
 
815
833
  const handleResumeSession = useEvent(
816
834
  async (previousSessionId: ExternalAgentSessionId) => {
817
- if (!agent) {
835
+ if (!agent || !isAgentReady) {
818
836
  return;
819
837
  }
820
838
  logger.debug("Resuming agent session", {
@@ -825,6 +843,8 @@ const AgentPanel: React.FC = () => {
825
843
  }
826
844
  creatingOrResumingSession.current = true;
827
845
  try {
846
+ // Loading replays the agent's history through session notifications.
847
+ clearNotifications(previousSessionId);
828
848
  const loadedSession = await agent.loadSession({
829
849
  sessionId: previousSessionId,
830
850
  cwd: getCwd(),
@@ -848,12 +868,19 @@ const AgentPanel: React.FC = () => {
848
868
  },
849
869
  );
850
870
 
851
- // Create or resume a session when successfully connected
852
- const isConnected = connectionState.status === "connected";
871
+ const handleRestartSession = useEvent(async () => {
872
+ setError(null);
873
+ try {
874
+ await handleNewSession();
875
+ } catch (error) {
876
+ setError(error instanceof Error ? error : String(error));
877
+ }
878
+ });
879
+
880
+ // Create or resume a session once initialization finishes.
853
881
  const tabLastActiveSessionId = selectedTab?.externalAgentSessionId;
854
882
  useEffect(() => {
855
- // No need to do anything if we're not connected, don't have an agent, or don't have a selected tab
856
- if (!isConnected || !selectedTab || !agent) {
883
+ if (!isAgentReady || !selectedTab || !agent) {
857
884
  return;
858
885
  }
859
886
 
@@ -893,12 +920,12 @@ const AgentPanel: React.FC = () => {
893
920
 
894
921
  createOrResumeSession();
895
922
  // oxlint-disable-next-line react-hooks/exhaustive-deps
896
- }, [isConnected, agent, tabLastActiveSessionId, activeSessionId]);
923
+ }, [isAgentReady, agent, tabLastActiveSessionId, activeSessionId]);
897
924
 
898
925
  // Handler for prompt submission
899
926
  const handlePromptSubmit = useEvent(
900
927
  async (_e: KeyboardEvent | undefined, prompt: string) => {
901
- if (!activeSessionId || !agent || isLoading) {
928
+ if (!activeSessionId || !agent || !isAgentReady || isLoading) {
902
929
  return;
903
930
  }
904
931
 
@@ -983,7 +1010,7 @@ const AgentPanel: React.FC = () => {
983
1010
  !activeSessionId ||
984
1011
  !agent ||
985
1012
  isLoading ||
986
- connectionState.status !== "connected" ||
1013
+ !isAgentReady ||
987
1014
  !pendingPrompt
988
1015
  ) {
989
1016
  return;
@@ -999,7 +1026,7 @@ const AgentPanel: React.FC = () => {
999
1026
  activeSessionId,
1000
1027
  agent,
1001
1028
  isLoading,
1002
- connectionState.status,
1029
+ isAgentReady,
1003
1030
  pendingPrompt,
1004
1031
  setPendingPrompt,
1005
1032
  handlePromptSubmit,
@@ -1019,7 +1046,10 @@ const AgentPanel: React.FC = () => {
1019
1046
  logger.debug("Manual connect requested", {
1020
1047
  currentStatus: connectionState.status,
1021
1048
  });
1022
- connect();
1049
+ setError(null);
1050
+ void connect().catch((error) => {
1051
+ setError(error instanceof Error ? error : String(error));
1052
+ });
1023
1053
  });
1024
1054
 
1025
1055
  // Handler for manual disconnect
@@ -1112,14 +1142,22 @@ const AgentPanel: React.FC = () => {
1112
1142
  className="w-3/4 mx-auto mt-10"
1113
1143
  error={displayError}
1114
1144
  action={
1115
- isAuthError ? (
1145
+ !isAgentReady ? (
1116
1146
  <Button
1117
1147
  variant="linkDestructive"
1118
1148
  size="sm"
1119
1149
  onClick={() => {
1120
- setError(null);
1121
- handleNewSession();
1150
+ disconnect();
1151
+ handleManualConnect();
1122
1152
  }}
1153
+ >
1154
+ Retry connection
1155
+ </Button>
1156
+ ) : isAuthError ? (
1157
+ <Button
1158
+ variant="linkDestructive"
1159
+ size="sm"
1160
+ onClick={handleRestartSession}
1123
1161
  >
1124
1162
  Restart session
1125
1163
  </Button>
@@ -1153,14 +1191,16 @@ const AgentPanel: React.FC = () => {
1153
1191
  }
1154
1192
 
1155
1193
  const isLoadingSession =
1156
- tabLastActiveSessionId == null && connectionState.status === "connected";
1194
+ isConnected && (!isAgentReady || tabLastActiveSessionId == null);
1157
1195
  if (isLoadingSession) {
1158
1196
  return (
1159
1197
  <DelayMount milliseconds={delay}>
1160
1198
  <div className="flex items-center justify-center h-full min-h-[200px] flex-col">
1161
1199
  <Spinner size="medium" className="text-primary" />
1162
1200
  <span className="text-sm text-muted-foreground">
1163
- Creating a new session...
1201
+ {isAgentReady
1202
+ ? "Creating a new session..."
1203
+ : "Initializing agent..."}
1164
1204
  </span>
1165
1205
  </div>
1166
1206
  </DelayMount>
@@ -1232,7 +1272,7 @@ const AgentPanel: React.FC = () => {
1232
1272
  currentAgentId={selectedTab?.agentId}
1233
1273
  onConnect={handleManualConnect}
1234
1274
  onDisconnect={handleManualDisconnect}
1235
- onRestartThread={handleNewSession}
1275
+ onRestartThread={isAgentReady ? handleRestartSession : undefined}
1236
1276
  hasActiveSession={true}
1237
1277
  shouldShowConnectionControl={wsUrl !== NO_WS_SET}
1238
1278
  />
@@ -257,7 +257,7 @@ interface AgentConfig {
257
257
  const AGENT_CONFIG: Record<ExternalAgentId, AgentConfig> = {
258
258
  claude: {
259
259
  port: 3017,
260
- command: "npx @zed-industries/claude-code-acp",
260
+ command: "npx @agentclientprotocol/claude-agent-acp",
261
261
  sessionSupport: "single",
262
262
  },
263
263
  gemini: {
@@ -2037,6 +2037,150 @@ describe("cell reducer", () => {
2037
2037
  );
2038
2038
  });
2039
2039
 
2040
+ it("can collapse all cells when nested heading ranges end at the same cell", () => {
2041
+ actions.createNewCell({ cellId: firstCellId, before: false });
2042
+ actions.createNewCell({
2043
+ cellId: cellId("1"),
2044
+ before: false,
2045
+ code: "# Header",
2046
+ });
2047
+ actions.createNewCell({
2048
+ cellId: cellId("2"),
2049
+ before: false,
2050
+ code: "## Subheader",
2051
+ });
2052
+ actions.createNewCell({
2053
+ cellId: cellId("3"),
2054
+ before: false,
2055
+ code: "### Subsubheader",
2056
+ });
2057
+
2058
+ const headerId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2059
+ state.cellRuntime[headerId] = {
2060
+ ...state.cellRuntime[headerId],
2061
+ outline: {
2062
+ items: [{ name: "Header", level: 1, by: { id: "header" } }],
2063
+ },
2064
+ };
2065
+
2066
+ const subheaderId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2);
2067
+ state.cellRuntime[subheaderId] = {
2068
+ ...state.cellRuntime[subheaderId],
2069
+ outline: {
2070
+ items: [{ name: "Subheader", level: 2, by: { id: "subheader" } }],
2071
+ },
2072
+ };
2073
+
2074
+ const subsubheaderId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(3);
2075
+ state.cellRuntime[subsubheaderId] = {
2076
+ ...state.cellRuntime[subsubheaderId],
2077
+ outline: {
2078
+ items: [{ name: "Subsubheader", level: 3, by: { id: "subsubheader" } }],
2079
+ },
2080
+ };
2081
+
2082
+ // The header, subheader and subsubheader ranges all end at the last
2083
+ // cell. Collapsing all used to throw "Node ... not found in tree" and
2084
+ // leave the whole notebook unchanged.
2085
+ actions.collapseAllCells();
2086
+ expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2087
+ true,
2088
+ );
2089
+ expect(state.cellIds.atOrThrow(FIRST_COLUMN).topLevelIds).toEqual([
2090
+ firstCellId,
2091
+ headerId,
2092
+ ]);
2093
+
2094
+ // Each level is nested under its parent, collapsed
2095
+ actions.expandCell({ cellId: headerId });
2096
+ expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subheaderId)).toBe(
2097
+ true,
2098
+ );
2099
+ actions.expandCell({ cellId: subheaderId });
2100
+ expect(
2101
+ state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subsubheaderId),
2102
+ ).toBe(true);
2103
+ });
2104
+
2105
+ it("can collapse all cells when a deep chain is followed by a later section", () => {
2106
+ actions.createNewCell({ cellId: firstCellId, before: false });
2107
+ actions.createNewCell({
2108
+ cellId: cellId("1"),
2109
+ before: false,
2110
+ code: "# Header",
2111
+ });
2112
+ actions.createNewCell({
2113
+ cellId: cellId("2"),
2114
+ before: false,
2115
+ code: "## Section A",
2116
+ });
2117
+ actions.createNewCell({
2118
+ cellId: cellId("3"),
2119
+ before: false,
2120
+ code: "### Sub of A",
2121
+ });
2122
+ actions.createNewCell({
2123
+ cellId: cellId("4"),
2124
+ before: false,
2125
+ code: "## Section B",
2126
+ });
2127
+
2128
+ const headerId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2129
+ state.cellRuntime[headerId] = {
2130
+ ...state.cellRuntime[headerId],
2131
+ outline: {
2132
+ items: [{ name: "Header", level: 1, by: { id: "header" } }],
2133
+ },
2134
+ };
2135
+
2136
+ const sectionAId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2);
2137
+ state.cellRuntime[sectionAId] = {
2138
+ ...state.cellRuntime[sectionAId],
2139
+ outline: {
2140
+ items: [{ name: "Section A", level: 2, by: { id: "section-a" } }],
2141
+ },
2142
+ };
2143
+
2144
+ const subsectionId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(3);
2145
+ state.cellRuntime[subsectionId] = {
2146
+ ...state.cellRuntime[subsectionId],
2147
+ outline: {
2148
+ items: [{ name: "Sub of A", level: 3, by: { id: "sub-of-a" } }],
2149
+ },
2150
+ };
2151
+
2152
+ const sectionBId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(4);
2153
+ state.cellRuntime[sectionBId] = {
2154
+ ...state.cellRuntime[sectionBId],
2155
+ outline: {
2156
+ items: [{ name: "Section B", level: 2, by: { id: "section-b" } }],
2157
+ },
2158
+ };
2159
+
2160
+ actions.collapseAllCells();
2161
+ expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2162
+ true,
2163
+ );
2164
+ expect(state.cellIds.atOrThrow(FIRST_COLUMN).topLevelIds).toEqual([
2165
+ firstCellId,
2166
+ headerId,
2167
+ ]);
2168
+
2169
+ // Both sections sit under the header, collapsed, with the subsection
2170
+ // nested under section A
2171
+ actions.expandCell({ cellId: headerId });
2172
+ expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(sectionAId)).toBe(
2173
+ true,
2174
+ );
2175
+ expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(sectionBId)).toBe(
2176
+ true,
2177
+ );
2178
+ actions.expandCell({ cellId: sectionAId });
2179
+ expect(
2180
+ state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subsectionId),
2181
+ ).toBe(true);
2182
+ });
2183
+
2040
2184
  it("can show hidden cells", () => {
2041
2185
  actions.createNewCell({ cellId: firstCellId, before: false });
2042
2186
  actions.createNewCell({ cellId: cellId("1"), before: false });
@@ -1365,10 +1365,7 @@ const {
1365
1365
 
1366
1366
  // Find the start/end of the collapsed ranges
1367
1367
  const nodes = [...column.nodes];
1368
- const rangeIndexes: {
1369
- start: CellIndex;
1370
- end: CellIndex;
1371
- }[] = [];
1368
+ const visibleEndByOriginalEnd = new Map<CellIndex, CellIndex>();
1372
1369
  const reversedCollapseRanges = [];
1373
1370
 
1374
1371
  // Iterate in reverse order (bottom-up) to process children first
@@ -1377,20 +1374,13 @@ const {
1377
1374
  const range = findCollapseRange(i, outlines);
1378
1375
  if (range) {
1379
1376
  const startIndex = i;
1380
- let endIndex = range[1];
1377
+ const originalEnd = range[1];
1378
+ const endIndex =
1379
+ visibleEndByOriginalEnd.get(originalEnd) ?? originalEnd;
1381
1380
 
1382
- // Check if the parent's end point is inside any already-collapsed child range
1383
- const parentEndInChild = rangeIndexes.find(
1384
- (child) => child.start <= endIndex && child.end === endIndex,
1385
- );
1386
-
1387
- if (parentEndInChild) {
1388
- // Adjust the new endIndex to the child's start
1389
- endIndex = parentEndInChild.start;
1390
- }
1391
-
1392
- // Store this range for future child checks
1393
- rangeIndexes.push({ start: startIndex, end: endIndex });
1381
+ // After this section collapses, its heading represents this
1382
+ // endpoint for any enclosing section that shares it.
1383
+ visibleEndByOriginalEnd.set(originalEnd, startIndex);
1394
1384
 
1395
1385
  // Add the range to the list of ranges
1396
1386
  const cellId = column.atOrThrow(startIndex);
@@ -126,6 +126,7 @@ describe("PyodideBridge.readCode", () => {
126
126
  await PyodideBridge.INSTANCE.readCode();
127
127
 
128
128
  expect(mockBootstrap).toHaveBeenCalledWith({
129
+ version: "0.0.0-test",
129
130
  pyodideIndexUrl: undefined,
130
131
  pyodideLockfileUrl: undefined,
131
132
  pypiIndexUrl: undefined,
@@ -223,21 +224,21 @@ describe("getWasmWorkerName", () => {
223
224
  .__MARIMO_HAS_WASM_CONTROLLER__;
224
225
  });
225
226
 
226
- it("returns the version without suffix by default", () => {
227
- expect(getWasmWorkerName()).toBe("0.0.0-test");
227
+ it("returns the default worker name without suffix", () => {
228
+ expect(getWasmWorkerName()).toBe("marimo");
228
229
  });
229
230
 
230
231
  it("appends ::controller when the host opts in", () => {
231
232
  (
232
233
  window as unknown as { __MARIMO_HAS_WASM_CONTROLLER__?: boolean }
233
234
  ).__MARIMO_HAS_WASM_CONTROLLER__ = true;
234
- expect(getWasmWorkerName()).toBe("0.0.0-test::controller");
235
+ expect(getWasmWorkerName()).toBe("marimo::controller");
235
236
  });
236
237
 
237
238
  it("does not append the suffix for non-true values", () => {
238
239
  (
239
240
  window as unknown as { __MARIMO_HAS_WASM_CONTROLLER__?: unknown }
240
241
  ).__MARIMO_HAS_WASM_CONTROLLER__ = "true";
241
- expect(getWasmWorkerName()).toBe("0.0.0-test");
242
+ expect(getWasmWorkerName()).toBe("marimo");
242
243
  });
243
244
  });
@@ -1,8 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { afterEach, assert, describe, expect, it } from "vitest";
3
+ import { afterEach, assert, describe, expect, it, vi } from "vitest";
4
4
  import { getWasmRuntimeConfig } from "../runtime-config";
5
5
 
6
+ vi.mock("@/core/meta/globals", () => ({
7
+ getMarimoVersion: () => "0.0.0-test",
8
+ }));
9
+
6
10
  afterEach(() => {
7
11
  document.head.innerHTML = "";
8
12
  });
@@ -10,6 +14,7 @@ afterEach(() => {
10
14
  describe("getWasmRuntimeConfig", () => {
11
15
  it("uses runtime defaults when no URLs are configured", () => {
12
16
  expect(getWasmRuntimeConfig()).toEqual({
17
+ version: "0.0.0-test",
13
18
  pyodideIndexUrl: undefined,
14
19
  pyodideLockfileUrl: undefined,
15
20
  pypiIndexUrl: undefined,
@@ -25,6 +30,7 @@ describe("getWasmRuntimeConfig", () => {
25
30
  data-pypi-index-url="https://packages.example.com/{package_name}/json"
26
31
  ></marimo-wasm>`;
27
32
  expect(getWasmRuntimeConfig()).toEqual({
33
+ version: "0.0.0-test",
28
34
  pyodideIndexUrl: "https://example.com/blog/runtime/",
29
35
  pyodideLockfileUrl: "https://example.com/locks/pyodide.json?v=1&x=2",
30
36
  pypiIndexUrl: "https://packages.example.com/{package_name}/json",
@@ -46,6 +52,7 @@ describe("getWasmRuntimeConfig", () => {
46
52
  element.dataset[key] = "https://invalid host/";
47
53
 
48
54
  expect(getWasmRuntimeConfig()).toEqual({
55
+ version: "0.0.0-test",
49
56
  pyodideIndexUrl: "https://example.com/notebook/pyodide/",
50
57
  pyodideLockfileUrl:
51
58
  "https://example.com/notebook/lockfile/pyodide.json",
@@ -12,7 +12,6 @@ import { generateUUID } from "@/utils/uuid";
12
12
  import { createModuleWorker } from "@/utils/worker";
13
13
  import { notebookIsRunningAtom } from "../cells/cells";
14
14
  import type { CommandMessage } from "../kernel/messages";
15
- import { getMarimoVersion } from "../meta/globals";
16
15
  import { getInitialAppMode } from "../mode";
17
16
  import { API } from "../network/api";
18
17
  import type {
@@ -49,6 +48,7 @@ import { fallbackFileStore, notebookFileStore } from "./store";
49
48
  import { isWasm } from "./utils";
50
49
  import saveWorkerUrl from "./worker/save-worker.ts?worker&url";
51
50
  import workerUrl from "./worker/worker.ts?worker&url";
51
+ import { CUSTOM_CONTROLLER_SUFFIX } from "./worker/constants";
52
52
  import type { SaveWorkerSchema } from "./worker/save-worker";
53
53
  import type { WorkerSchema } from "./worker/worker";
54
54
 
@@ -91,7 +91,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
91
91
  const saveWorker = createModuleWorker(
92
92
  new URL(saveWorkerUrl, import.meta.url),
93
93
  {
94
- // Pass the version (and optional capability suffix) to the worker
94
+ // Pass the optional custom-controller capability to the worker.
95
95
  name: getWasmWorkerName(),
96
96
  },
97
97
  );
@@ -110,7 +110,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
110
110
 
111
111
  // Create a worker
112
112
  const worker = createModuleWorker(new URL(workerUrl, import.meta.url), {
113
- // Pass the version (and optional capability suffix) to the worker
113
+ // Pass the optional custom-controller capability to the worker.
114
114
  name: getWasmWorkerName(),
115
115
  });
116
116
 
@@ -726,10 +726,9 @@ export function createPyodideConnection(): IConnectionTransport {
726
726
  });
727
727
  }
728
728
 
729
- // Compose the worker name. The version prefix is read by getMarimoVersion()
730
- // in the worker; the optional "::controller" suffix tells getController.ts
731
- // that the host page provides a custom /wasm/controller.js and that the
732
- // dynamic import should be attempted. Hosts opt in by setting
729
+ // Compose the worker name. The optional "::controller" suffix tells
730
+ // getController.ts that the host page provides a custom /wasm/controller.js
731
+ // and that the dynamic import should be attempted. Hosts opt in by setting
733
732
  // `window.__MARIMO_HAS_WASM_CONTROLLER__ = true` before
734
733
  // PyodideBridge/worker initialization.
735
734
  export function getWasmWorkerName(): string {
@@ -737,5 +736,5 @@ export function getWasmWorkerName(): string {
737
736
  typeof window !== "undefined" &&
738
737
  (window as unknown as { __MARIMO_HAS_WASM_CONTROLLER__?: boolean })
739
738
  .__MARIMO_HAS_WASM_CONTROLLER__ === true;
740
- return getMarimoVersion() + (hasCustomController ? "::controller" : "");
739
+ return `marimo${hasCustomController ? CUSTOM_CONTROLLER_SUFFIX : ""}`;
741
740
  }
@@ -1,6 +1,9 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { getMarimoVersion } from "@/core/meta/globals";
4
+
3
5
  export interface WasmRuntimeConfig {
6
+ version: string;
4
7
  pyodideIndexUrl?: string;
5
8
  pyodideLockfileUrl?: string;
6
9
  pypiIndexUrl?: string;
@@ -16,6 +19,7 @@ export function getWasmRuntimeConfig(): WasmRuntimeConfig {
16
19
  }
17
20
  };
18
21
  return {
22
+ version: getMarimoVersion(),
19
23
  pyodideIndexUrl: resolve(element?.dataset.pyodideIndexUrl),
20
24
  pyodideLockfileUrl: resolve(element?.dataset.pyodideLockfileUrl),
21
25
  // URL serialization escapes the placeholder understood by micropip.
@@ -1,2 +1,3 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
+ export const CUSTOM_CONTROLLER_SUFFIX = "::controller";
2
3
  export const TRANSPORT_ID = "marimo-transport";
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { DefaultWasmController } from "./bootstrap";
3
+ import { CUSTOM_CONTROLLER_SUFFIX } from "./constants";
3
4
  import type { WasmController } from "./types";
4
5
 
5
6
  // Load the controller
@@ -8,7 +9,8 @@ export async function getController(version: string): Promise<WasmController> {
8
9
  // Hosts that provide a custom /wasm/controller.js opt in via the worker
9
10
  // name (see bridge.ts). Default: skip the dynamic import to avoid a
10
11
  // guaranteed-404 round trip on the standard build.
11
- const hasCustomController = self.name?.includes("::controller") ?? false;
12
+ const hasCustomController =
13
+ self.name?.includes(CUSTOM_CONTROLLER_SUFFIX) ?? false;
12
14
  if (!hasCustomController) {
13
15
  return new DefaultWasmController();
14
16
  }
@@ -33,15 +33,14 @@ const runtimeConfig = new Deferred<WasmRuntimeConfig>();
33
33
  async function loadPyodideAndPackages() {
34
34
  try {
35
35
  // Import pyodide
36
- const marimoVersion = getMarimoVersion();
37
- const pyodideVersion = getPyodideVersion(marimoVersion);
36
+ const config = await runtimeConfig.promise;
37
+ const pyodideVersion = getPyodideVersion(config.version);
38
38
 
39
39
  // Bootstrap the controller
40
- const controller = await getController(marimoVersion);
40
+ const controller = await getController(config.version);
41
41
  self.controller = controller;
42
42
  self.pyodide = await controller.bootstrap({
43
- ...(await runtimeConfig.promise),
44
- version: marimoVersion,
43
+ ...config,
45
44
  pyodideVersion: pyodideVersion,
46
45
  });
47
46
 
@@ -108,8 +107,3 @@ const rpc = createRPC<SaveWorkerSchema, ParentSchema>({
108
107
  rpc.addMessageListener("bootstrap", (config) => runtimeConfig.resolve(config));
109
108
 
110
109
  rpc.send("ready", {});
111
-
112
- function getMarimoVersion() {
113
- // Worker name is "<version>" or "<version>::<capability>" — see bridge.ts.
114
- return self.name.split("::")[0];
115
- }
@@ -46,7 +46,6 @@ export interface WasmController {
46
46
  */
47
47
  bootstrap(
48
48
  opts: WasmRuntimeConfig & {
49
- version: string;
50
49
  pyodideVersion: string;
51
50
  },
52
51
  ): Promise<PyodideInterface>;
@@ -53,16 +53,15 @@ const runtimeConfig = new Deferred<WasmRuntimeConfig>();
53
53
  // Initialize pyodide
54
54
  async function loadPyodideAndPackages() {
55
55
  try {
56
- const marimoVersion = getMarimoVersion();
57
- const pyodideVersion = getPyodideVersion(marimoVersion);
58
- const controller = await t.wrapAsync(getController)(marimoVersion);
56
+ const config = await runtimeConfig.promise;
57
+ const pyodideVersion = getPyodideVersion(config.version);
58
+ const controller = await t.wrapAsync(getController)(config.version);
59
59
  self.controller = controller;
60
60
  rpc.send.initializingMessage({
61
61
  message: "Loading marimo...",
62
62
  });
63
63
  self.pyodide = await t.wrapAsync(controller.bootstrap.bind(controller))({
64
- ...(await runtimeConfig.promise),
65
- version: marimoVersion,
64
+ ...config,
66
65
  pyodideVersion: pyodideVersion,
67
66
  });
68
67
  } catch (error) {
@@ -380,9 +379,4 @@ const namesThatRequireSync = new Set<keyof RawBridge>([
380
379
  "update_file",
381
380
  ]);
382
381
 
383
- function getMarimoVersion() {
384
- // Worker name is "<version>" or "<version>::<capability>" — see bridge.ts.
385
- return self.name.split("::")[0];
386
- }
387
-
388
382
  const pyodideReadyPromise = t.wrapAsync(loadPyodideAndPackages)();
@@ -1 +0,0 @@
1
- import{t as e}from"./worker-B8c-1v-F.js";var t=e(((e,t)=>{t.exports={}}));export default t();
@@ -1 +0,0 @@
1
- import{t as e}from"./save-worker-CMa6Cvnx.js";var t=e(((e,t)=>{t.exports={}}));export default t();
@@ -1 +0,0 @@
1
- import{Ka as e}from"./index-kGHM0O4l.js";var t=Object.prototype.hasOwnProperty;function n(e,n){return e!=null&&t.call(e,n)}function r(t,r){return t!=null&&e(t,r,n)}export{r as t};