@marimo-team/frontend 0.19.3-dev7 → 0.19.3

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 (189) hide show
  1. package/dist/assets/{CellStatus-Ba6Af_Tb.js → CellStatus--kUu6N2K.js} +1 -1
  2. package/dist/assets/{ConnectedDataExplorerComponent-KlUs_Sz3.js → ConnectedDataExplorerComponent-BKJwCHu7.js} +1 -1
  3. package/dist/assets/{ErrorBoundary-Drf1manw.js → ErrorBoundary-C7JBxSzd.js} +1 -1
  4. package/dist/assets/{ImperativeModal-q6QlC2aZ.js → ImperativeModal-DVhvP4lH.js} +1 -1
  5. package/dist/assets/{JsonOutput-4ruRfyOj.js → JsonOutput-BSGE-MRo.js} +5 -5
  6. package/dist/assets/{LazyAnyLanguageCodeMirror-jpEDlD0M.js → LazyAnyLanguageCodeMirror-Cp2punaU.js} +2 -2
  7. package/dist/assets/{MarimoErrorOutput-DnjH3pD8.js → MarimoErrorOutput-CX0SCJOZ.js} +2 -2
  8. package/dist/assets/{RenderHTML-DaJXe2U2.js → RenderHTML-Do_PVqRy.js} +1 -1
  9. package/dist/assets/VisuallyHidden-B9t3FhTP.js +1 -0
  10. package/dist/assets/{add-cell-with-ai-Bsds_6SU.js → add-cell-with-ai-manh7kBT.js} +21 -21
  11. package/dist/assets/{add-database-form-CqIp3_WN.js → add-database-form-CgkV0MRs.js} +2 -2
  12. package/dist/assets/agent-panel-D-OmT-rw.js +287 -0
  13. package/dist/assets/{ai-model-dropdown-LK8Wr5iu.js → ai-model-dropdown-DzyBY5VA.js} +1 -1
  14. package/dist/assets/{alert-dialog-k5KxevGr.js → alert-dialog-jcHA5geR.js} +1 -1
  15. package/dist/assets/{any-language-editor-DQu1Tt2N.js → any-language-editor-Cm83E7D_.js} +1 -1
  16. package/dist/assets/{app-config-button-BaVc4Y5z.js → app-config-button-DC3alCuB.js} +1 -1
  17. package/dist/assets/button-B8cGZzP5.js +1 -0
  18. package/dist/assets/{cache-panel-C1So4Zu3.js → cache-panel-1FqnpB9y.js} +1 -1
  19. package/dist/assets/cell-editor-RHFZmO74.js +23 -0
  20. package/dist/assets/cell-link-Dqj_nfXA.js +1 -0
  21. package/dist/assets/{cells-KYKWFk6C.js → cells-BNQUQiDS.js} +49 -49
  22. package/dist/assets/{chat-components-O6DUIpBx.js → chat-components-CWiXtKu6.js} +1 -1
  23. package/dist/assets/{chat-display-DD3KokYi.js → chat-display-CGnOamQG.js} +1 -1
  24. package/dist/assets/{chat-panel-D4DIcOM1.js → chat-panel-Dh1M55c9.js} +2 -2
  25. package/dist/assets/client-CDjmJmVw.js +4 -0
  26. package/dist/assets/{column-preview-EpCGr4Xp.js → column-preview-CKxT2s-S.js} +1 -1
  27. package/dist/assets/{command-Dqe0kvHp.js → command-YPFTinLj.js} +1 -1
  28. package/dist/assets/{command-palette-DWacsFDk.js → command-palette-7fVEhKGc.js} +1 -1
  29. package/dist/assets/common-DJkPpBxC.js +1 -0
  30. package/dist/assets/config-D6nhy4FA.js +1 -0
  31. package/dist/assets/context-DHfVoQfl.js +1 -0
  32. package/dist/assets/{copy-icon-B69c-352.js → copy-icon-jWsqdLn1.js} +1 -1
  33. package/dist/assets/{datasource-JeWYnuIr.js → datasource-DerBLc6V.js} +2 -2
  34. package/dist/assets/{dependency-graph-panel-BJibnwCO.js → dependency-graph-panel-Vd-OsVLa.js} +4 -4
  35. package/dist/assets/{dialog-DUEuLcT2.js → dialog-CF5DtF1E.js} +1 -1
  36. package/dist/assets/{dist-DOFFh6Ii.js → dist-Dg7UO_Vw.js} +1 -1
  37. package/dist/assets/{documentation-panel-B2W3q2YB.js → documentation-panel-xG2-zpwg.js} +1 -1
  38. package/dist/assets/{download-NfnO_JCs.js → download-B6EJS7Ar.js} +1 -1
  39. package/dist/assets/edit-page-7Hkti2j_.js +12 -0
  40. package/dist/assets/{error-banner-DU5Qb8a8.js → error-banner-DvT0IGDZ.js} +1 -1
  41. package/dist/assets/{error-panel-Bv-7GYgJ.js → error-panel-BxBpZYvt.js} +1 -1
  42. package/dist/assets/{es-KtEicG7U.js → es-BoHEdemq.js} +1 -1
  43. package/dist/assets/{field-DDKGFzpC.js → field-Clr_fqUr.js} +1 -1
  44. package/dist/assets/{file-explorer-panel-CToUezud.js → file-explorer-panel-C9K0vIPl.js} +1 -1
  45. package/dist/assets/{floating-outline-Db40vhG8.js → floating-outline-DCrTuu2G.js} +1 -1
  46. package/dist/assets/{focus-BCdX47jS.js → focus-DM53w5BH.js} +1 -1
  47. package/dist/assets/{form-DwtJQd_Z.js → form-BcKfhfZc.js} +2 -2
  48. package/dist/assets/{glide-data-editor-D_bRnWfy.js → glide-data-editor-CRb9AiCG.js} +1 -1
  49. package/dist/assets/{globals-MS86g8oR.js → globals-Bf30kOQF.js} +1 -1
  50. package/dist/assets/{home-page-BfVf41OG.js → home-page-BRyNf7fl.js} +2 -2
  51. package/dist/assets/house-CncUa_LL.js +1 -0
  52. package/dist/assets/index-CBMqMxiq.js +43 -0
  53. package/dist/assets/index-DDc_1b-N.css +2 -0
  54. package/dist/assets/input-B80Yt1uu.js +1 -0
  55. package/dist/assets/{kiosk-mode-CEhvsEr0.js → kiosk-mode-P-NYHJID.js} +1 -1
  56. package/dist/assets/{label-qwandMoh.js → label-CNZLffHW.js} +1 -1
  57. package/dist/assets/{layout-Cvaok8Kj.js → layout-DT91GUei.js} +4 -4
  58. package/dist/assets/links-D529u6GQ.js +1 -0
  59. package/dist/assets/{logs-panel-J2FKnKaj.js → logs-panel-C2dfrRig.js} +1 -1
  60. package/dist/assets/{markdown-renderer-BlG9DgUG.js → markdown-renderer-BPnVa0ym.js} +2 -2
  61. package/dist/assets/{mermaid-BPkO79lo.js → mermaid--ZwxKP7u.js} +1 -1
  62. package/dist/assets/mode-Dq8MKjNR.js +1 -0
  63. package/dist/assets/{multi-map-fjX9ImVF.js → multi-map-CQd4MZr5.js} +1 -1
  64. package/dist/assets/name-cell-input-BaEPC7ON.js +1 -0
  65. package/dist/assets/{outline-panel-Doj3GJrQ.js → outline-panel-Cca864H0.js} +1 -1
  66. package/dist/assets/{packages-panel-nqWXQzKf.js → packages-panel-Cy_KAYmq.js} +1 -1
  67. package/dist/assets/panels-BzlLZfye.js +1 -0
  68. package/dist/assets/{process-output-DiSW8Nbo.js → process-output-Dn1rOp26.js} +1 -1
  69. package/dist/assets/{readonly-python-code-CKY5LsMp.js → readonly-python-code-CXeF74Iq.js} +1 -1
  70. package/dist/assets/{renderShortcut-D0Pei-OA.js → renderShortcut-eU5Hsfml.js} +1 -1
  71. package/dist/assets/run-page-CM_n6pXD.js +1 -0
  72. package/dist/assets/scratchpad-panel-XCkVY3Hp.js +1 -0
  73. package/dist/assets/{secrets-panel-CDWmmmBS.js → secrets-panel-BMY6PPth.js} +1 -1
  74. package/dist/assets/{select-D0g5GnIs.js → select-D9lTzMzP.js} +1 -1
  75. package/dist/assets/{session-panel-CGFRSBw9.js → session-panel-BDt6Y_mU.js} +1 -1
  76. package/dist/assets/{slides-component-MkPkpql1.js → slides-component-Dp0Yv5b0.js} +1 -1
  77. package/dist/assets/{snippets-panel-ClHeSpc5.js → snippets-panel-K-JKJQBf.js} +1 -1
  78. package/dist/assets/state-DWRZTH2y.js +1 -0
  79. package/dist/assets/state-JzO-Ni5T.js +1 -0
  80. package/dist/assets/{switch-BmbGJWHc.js → switch-RowEjq0T.js} +1 -1
  81. package/dist/assets/{terminal-BvgBa6Ri.js → terminal-BhbNfCNw.js} +1 -1
  82. package/dist/assets/{textarea-WklymBeK.js → textarea-Di1KKcL4.js} +1 -1
  83. package/dist/assets/{tracing-D0WYhZdr.js → tracing-nvbrZdpf.js} +1 -1
  84. package/dist/assets/{tracing-panel-CNxN58z7.js → tracing-panel-CTXJaO-A.js} +2 -2
  85. package/dist/assets/{types-BrgXpvGt.js → types-CT2U5Ljy.js} +1 -1
  86. package/dist/assets/{useAddCell-a9qZ0_KE.js → useAddCell-COb93CUl.js} +1 -1
  87. package/dist/assets/{useBoolean-5kuXz69O.js → useBoolean-B_S7yTZz.js} +1 -1
  88. package/dist/assets/{useCellActionButton-9W_R41MM.js → useCellActionButton-D5Zt1dDz.js} +1 -1
  89. package/dist/assets/{useDateFormatter-CV0QXb5P.js → useDateFormatter-DsANziQR.js} +1 -1
  90. package/dist/assets/useDeleteCell-DHF_xvAh.js +1 -0
  91. package/dist/assets/{useDependencyPanelTab-0reaqvvh.js → useDependencyPanelTab-D59iW_MD.js} +1 -1
  92. package/dist/assets/useInterval-BGPIviJp.js +1 -0
  93. package/dist/assets/useNotebookActions-DEl-rH-3.js +1 -0
  94. package/dist/assets/{useNumberFormatter-D8ks3oPN.js → useNumberFormatter-FoXhpyAb.js} +1 -1
  95. package/dist/assets/usePress-DTwIUo40.js +7 -0
  96. package/dist/assets/useRunCells-CKEmgeKM.js +1 -0
  97. package/dist/assets/useSplitCell-D9YiO-z5.js +1 -0
  98. package/dist/assets/{useTheme-DfP1CWaW.js → useTheme-CNj0G_ol.js} +1 -1
  99. package/dist/assets/utilities.esm-DG4qccZc.js +3 -0
  100. package/dist/assets/utils-pfqq9IdB.js +1 -0
  101. package/dist/assets/{vega-component-DpAAiTdH.js → vega-component-C1voDf5W.js} +1 -1
  102. package/dist/assets/{write-secret-modal-CLm48gMe.js → write-secret-modal-hOetwavI.js} +1 -1
  103. package/dist/index.html +57 -57
  104. package/package.json +5 -5
  105. package/src/__mocks__/requests.ts +1 -0
  106. package/src/__tests__/mount.test.ts +128 -0
  107. package/src/components/app-config/__tests__/get-dirty-values.test.ts +1 -1
  108. package/src/components/app-config/ai-config.tsx +328 -28
  109. package/src/components/app-config/user-config-form.tsx +10 -3
  110. package/src/components/chat/acp/agent-panel.tsx +56 -43
  111. package/src/components/chat/chat-utils.ts +0 -19
  112. package/src/components/data-table/column-header.tsx +1 -1
  113. package/src/components/editor/KernelStartupErrorModal.tsx +101 -0
  114. package/src/components/editor/actions/name-cell-input.tsx +10 -4
  115. package/src/components/editor/ai/completion-handlers.tsx +1 -1
  116. package/src/components/editor/alerts/connecting-alert.tsx +33 -6
  117. package/src/components/editor/chrome/types.ts +2 -4
  118. package/src/components/editor/chrome/wrapper/app-chrome.tsx +55 -58
  119. package/src/components/editor/chrome/wrapper/footer-items/runtime-settings.tsx +150 -96
  120. package/src/components/editor/renderers/vertical-layout/__tests__/useFocusFirstEditor.test.ts +27 -0
  121. package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +6 -0
  122. package/src/components/utils/lazy-mount.tsx +29 -8
  123. package/src/core/MarimoApp.tsx +2 -0
  124. package/src/core/ai/ids/ids.ts +12 -4
  125. package/src/core/cells/cells.ts +2 -0
  126. package/src/core/cells/scrollCellIntoView.ts +3 -2
  127. package/src/core/codemirror/cm.ts +2 -0
  128. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +123 -0
  129. package/src/core/codemirror/lsp/notebook-lsp.ts +44 -4
  130. package/src/core/codemirror/misc/__tests__/string-braces.test.ts +200 -0
  131. package/src/core/codemirror/misc/string-braces.ts +37 -0
  132. package/src/core/config/__tests__/config-schema.test.ts +36 -0
  133. package/src/core/config/config-schema.ts +1 -0
  134. package/src/core/errors/state.ts +7 -1
  135. package/src/core/export/__tests__/hooks.test.ts +504 -0
  136. package/src/core/export/hooks.ts +93 -4
  137. package/src/core/islands/bridge.ts +1 -0
  138. package/src/core/islands/main.ts +2 -0
  139. package/src/core/kernel/__tests__/handlers.test.ts +2 -2
  140. package/src/core/kernel/state.ts +1 -0
  141. package/src/core/network/__tests__/requests-lazy.test.ts +1 -1
  142. package/src/core/network/__tests__/requests-network.test.ts +0 -18
  143. package/src/core/network/requests-lazy.ts +3 -2
  144. package/src/core/network/requests-network.ts +10 -7
  145. package/src/core/network/requests-static.ts +1 -0
  146. package/src/core/network/requests-toasting.tsx +1 -0
  147. package/src/core/network/types.ts +2 -0
  148. package/src/core/wasm/bridge.ts +1 -0
  149. package/src/core/websocket/types.ts +1 -0
  150. package/src/core/websocket/useMarimoKernelConnection.tsx +18 -1
  151. package/src/css/globals.css +2 -0
  152. package/src/hooks/__tests__/useInterval.test.tsx +104 -0
  153. package/src/hooks/useInterval.ts +32 -6
  154. package/src/mount.tsx +6 -0
  155. package/src/plugins/impl/chat/ChatPlugin.tsx +2 -4
  156. package/src/plugins/impl/chat/chat-ui.tsx +62 -191
  157. package/src/plugins/impl/chat/types.ts +5 -12
  158. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +3 -1
  159. package/src/utils/events.ts +1 -0
  160. package/dist/assets/VisuallyHidden-BodIky8L.js +0 -1
  161. package/dist/assets/agent-panel-CaAPVPdJ.js +0 -287
  162. package/dist/assets/button-DuYGqRtX.js +0 -1
  163. package/dist/assets/cell-editor-OFm-OSAP.js +0 -23
  164. package/dist/assets/cell-link-CfLJRl3p.js +0 -1
  165. package/dist/assets/client-Cha_JfGC.js +0 -4
  166. package/dist/assets/common-A6YWtmpq.js +0 -1
  167. package/dist/assets/config-babG4OBR.js +0 -1
  168. package/dist/assets/context-BAYdLMF_.js +0 -1
  169. package/dist/assets/edit-page-nuU4FVXi.js +0 -12
  170. package/dist/assets/globe-CY9im410.js +0 -1
  171. package/dist/assets/index-BI88xbv4.js +0 -43
  172. package/dist/assets/index-Chgc_07S.css +0 -2
  173. package/dist/assets/input-CaEtLL8p.js +0 -1
  174. package/dist/assets/links-ENMiP32L.js +0 -1
  175. package/dist/assets/mode-CK5Oq-Jz.js +0 -1
  176. package/dist/assets/name-cell-input-D7axzd6k.js +0 -1
  177. package/dist/assets/panels-CdYbZBqo.js +0 -1
  178. package/dist/assets/run-page-GP8eGE39.js +0 -1
  179. package/dist/assets/scratchpad-panel-B1p8zqAE.js +0 -1
  180. package/dist/assets/state-BBgXjqJI.js +0 -1
  181. package/dist/assets/state-CP7_TGWl.js +0 -1
  182. package/dist/assets/useDeleteCell-5kJUaejE.js +0 -1
  183. package/dist/assets/useInterval-DpipYmgs.js +0 -1
  184. package/dist/assets/useNotebookActions-o341ZCMJ.js +0 -1
  185. package/dist/assets/usePress-C2LPFxyv.js +0 -7
  186. package/dist/assets/useRunCells-wXhl9zOP.js +0 -1
  187. package/dist/assets/useSplitCell-mmm5jxn2.js +0 -1
  188. package/dist/assets/utilities.esm-Ckt5kMF-.js +0 -3
  189. package/dist/assets/utils-CJJIceVn.js +0 -1
@@ -9,6 +9,7 @@ import { invariant } from "@/utils/invariant";
9
9
  import { Logger } from "@/utils/Logger";
10
10
  import { LRUCache } from "@/utils/lru";
11
11
  import { Objects } from "@/utils/objects";
12
+ import { getPositionAtWordBounds } from "../completion/hints";
12
13
  import { topologicalCodesAtom } from "../copilot/getCodes";
13
14
  import {
14
15
  getEditorCodeAsPython,
@@ -22,6 +23,14 @@ import {
22
23
  } from "./types";
23
24
  import { getLSPDocument } from "./utils";
24
25
 
26
+ /**
27
+ * Check if a variable name is private (starts with underscore but not dunder).
28
+ * Private variables in marimo are cell-local and should not be renamed across cells.
29
+ */
30
+ function isPrivateVariable(name: string): boolean {
31
+ return name.startsWith("_") && !name.startsWith("__");
32
+ }
33
+
25
34
  class Snapshotter {
26
35
  private documentVersion = 0;
27
36
  private readonly getNotebookCode: () => {
@@ -433,15 +442,46 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
433
442
 
434
443
  // Update the code in the plugins manually
435
444
  const editors = this.getNotebookEditors();
436
- for (const [cellId, ev] of Objects.entries(editors)) {
437
- const newCode = editsToNewCode.get(cellId);
445
+
446
+ // Check if this is a private variable rename (should only affect current cell)
447
+ // Private variables in marimo are cell-local and should not be renamed across cells
448
+ const originEditor = editors[cellId];
449
+ let isPrivateRename = false;
450
+ if (originEditor) {
451
+ // Convert LSP position (line, character) to CodeMirror position
452
+ const line = originEditor.state.doc.line(params.position.line + 1);
453
+ const cmPosition = line.from + params.position.character;
454
+ const { startToken, endToken } = getPositionAtWordBounds(
455
+ originEditor.state.doc,
456
+ cmPosition,
457
+ );
458
+ const originalName = originEditor.state.doc.sliceString(
459
+ startToken,
460
+ endToken,
461
+ );
462
+ isPrivateRename = isPrivateVariable(originalName);
463
+ if (isPrivateRename) {
464
+ Logger.debug(
465
+ "[lsp] Private variable rename detected, limiting to current cell",
466
+ originalName,
467
+ );
468
+ }
469
+ }
470
+
471
+ for (const [currentCellId, ev] of Objects.entries(editors)) {
472
+ // For private variable renames, only update the originating cell
473
+ if (isPrivateRename && currentCellId !== cellId) {
474
+ continue;
475
+ }
476
+
477
+ const newCode = editsToNewCode.get(currentCellId);
438
478
  if (newCode == null) {
439
- Logger.warn("No new code for cell", cellId);
479
+ Logger.warn("No new code for cell", currentCellId);
440
480
  continue;
441
481
  }
442
482
 
443
483
  if (!ev) {
444
- Logger.warn("No view for plugin", cellId);
484
+ Logger.warn("No view for plugin", currentCellId);
445
485
  continue;
446
486
  }
447
487
 
@@ -0,0 +1,200 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { python } from "@codemirror/lang-python";
4
+ import { EditorState } from "@codemirror/state";
5
+ import { EditorView } from "@codemirror/view";
6
+ import { afterEach, describe, expect, it } from "vitest";
7
+ import { stringBraceInputHandler } from "../string-braces";
8
+
9
+ function createEditor(
10
+ initialContent: string,
11
+ cursorPosition: number,
12
+ ): EditorView {
13
+ const state = EditorState.create({
14
+ doc: initialContent,
15
+ selection: { anchor: cursorPosition },
16
+ extensions: [python()],
17
+ });
18
+
19
+ const view = new EditorView({
20
+ state,
21
+ parent: document.body,
22
+ });
23
+
24
+ return view;
25
+ }
26
+
27
+ describe("string brace auto-closing", () => {
28
+ let view: EditorView;
29
+
30
+ afterEach(() => {
31
+ if (view) {
32
+ view.destroy();
33
+ if (document.body.contains(view.dom)) {
34
+ view.dom.remove();
35
+ }
36
+ }
37
+ });
38
+
39
+ it("should auto-close braces in f-strings", () => {
40
+ view = createEditor('f"hello ', 8);
41
+ const result = stringBraceInputHandler(view, 8, 8, "{");
42
+
43
+ expect(result).toBe(true);
44
+ expect(view.state.doc.toString()).toBe('f"hello {}');
45
+ expect(view.state.selection.main.head).toBe(9);
46
+ });
47
+
48
+ it("should auto-close braces in regular double-quoted strings", () => {
49
+ view = createEditor('"hello ', 7);
50
+ const result = stringBraceInputHandler(view, 7, 7, "{");
51
+
52
+ expect(result).toBe(true);
53
+ expect(view.state.doc.toString()).toBe('"hello {}');
54
+ expect(view.state.selection.main.head).toBe(8);
55
+ });
56
+
57
+ it("should auto-close braces in rf-strings", () => {
58
+ view = createEditor('rf"hello ', 9);
59
+ const result = stringBraceInputHandler(view, 9, 9, "{");
60
+
61
+ expect(result).toBe(true);
62
+ expect(view.state.doc.toString()).toBe('rf"hello {}');
63
+ expect(view.state.selection.main.head).toBe(10);
64
+ });
65
+
66
+ it("should auto-close braces in fr-strings", () => {
67
+ view = createEditor('fr"hello ', 9);
68
+ const result = stringBraceInputHandler(view, 9, 9, "{");
69
+
70
+ expect(result).toBe(true);
71
+ expect(view.state.doc.toString()).toBe('fr"hello {}');
72
+ expect(view.state.selection.main.head).toBe(10);
73
+ });
74
+
75
+ it("should auto-close braces in single-quoted strings", () => {
76
+ view = createEditor("'hello ", 7);
77
+ const result = stringBraceInputHandler(view, 7, 7, "{");
78
+
79
+ expect(result).toBe(true);
80
+ expect(view.state.doc.toString()).toBe("'hello {}");
81
+ expect(view.state.selection.main.head).toBe(8);
82
+ });
83
+
84
+ it("should auto-close braces in uppercase F-strings", () => {
85
+ view = createEditor('F"hello ', 8);
86
+ const result = stringBraceInputHandler(view, 8, 8, "{");
87
+
88
+ expect(result).toBe(true);
89
+ expect(view.state.doc.toString()).toBe('F"hello {}');
90
+ expect(view.state.selection.main.head).toBe(9);
91
+ });
92
+
93
+ it("should auto-close braces in raw strings without f/t", () => {
94
+ view = createEditor('r"hello ', 8);
95
+ const result = stringBraceInputHandler(view, 8, 8, "{");
96
+
97
+ expect(result).toBe(true);
98
+ expect(view.state.doc.toString()).toBe('r"hello {}');
99
+ expect(view.state.selection.main.head).toBe(9);
100
+ });
101
+
102
+ // Handled by other CodeMirror handler(s)
103
+ it("should NOT auto-close braces outside strings", () => {
104
+ view = createEditor("x = ", 4);
105
+ const result = stringBraceInputHandler(view, 4, 4, "{");
106
+
107
+ expect(result).toBe(false);
108
+ expect(view.state.doc.toString()).toBe("x = ");
109
+ });
110
+
111
+ it("should NOT auto-close braces when typing other characters", () => {
112
+ view = createEditor('f"hello ', 8);
113
+ const result = stringBraceInputHandler(view, 8, 8, "a");
114
+
115
+ expect(result).toBe(false);
116
+ expect(view.state.doc.toString()).toBe('f"hello ');
117
+ });
118
+
119
+ it("should handle braces at the start of string", () => {
120
+ view = createEditor('f"', 2);
121
+ const result = stringBraceInputHandler(view, 2, 2, "{");
122
+
123
+ expect(result).toBe(true);
124
+ expect(view.state.doc.toString()).toBe('f"{}');
125
+ expect(view.state.selection.main.head).toBe(3);
126
+ });
127
+
128
+ it("should handle braces in the middle of string content", () => {
129
+ view = createEditor('f"hello world ', 14);
130
+ const result = stringBraceInputHandler(view, 14, 14, "{");
131
+
132
+ expect(result).toBe(true);
133
+ expect(view.state.doc.toString()).toBe('f"hello world {}');
134
+ expect(view.state.selection.main.head).toBe(15);
135
+ });
136
+
137
+ it("should handle multiple braces in string", () => {
138
+ view = createEditor('f"hello {} world', 16);
139
+ const result = stringBraceInputHandler(view, 16, 16, "{");
140
+
141
+ expect(result).toBe(true);
142
+ expect(view.state.doc.toString()).toBe('f"hello {} world{}');
143
+ expect(view.state.selection.main.head).toBe(17);
144
+ });
145
+
146
+ it("should handle empty string", () => {
147
+ view = createEditor('f""', 2);
148
+ const result = stringBraceInputHandler(view, 2, 2, "{");
149
+
150
+ expect(result).toBe(true);
151
+ expect(view.state.doc.toString()).toBe('f"{}"');
152
+ expect(view.state.selection.main.head).toBe(3);
153
+ });
154
+
155
+ it("should auto-close braces in triple-quoted strings", () => {
156
+ view = createEditor('"""hello ', 9);
157
+ const result = stringBraceInputHandler(view, 9, 9, "{");
158
+
159
+ expect(result).toBe(true);
160
+ expect(view.state.doc.toString()).toBe('"""hello {}');
161
+ expect(view.state.selection.main.head).toBe(10);
162
+ });
163
+
164
+ it("should auto-close braces in triple-quoted f-strings", () => {
165
+ view = createEditor('f"""hello ', 10);
166
+ const result = stringBraceInputHandler(view, 10, 10, "{");
167
+
168
+ expect(result).toBe(true);
169
+ expect(view.state.doc.toString()).toBe('f"""hello {}');
170
+ expect(view.state.selection.main.head).toBe(11);
171
+ });
172
+
173
+ it("should auto-close braces in triple single-quoted strings", () => {
174
+ view = createEditor("'''hello ", 9);
175
+ const result = stringBraceInputHandler(view, 9, 9, "{");
176
+
177
+ expect(result).toBe(true);
178
+ expect(view.state.doc.toString()).toBe("'''hello {}");
179
+ expect(view.state.selection.main.head).toBe(10);
180
+ });
181
+
182
+ it("should auto-close braces in triple single-quoted f-strings", () => {
183
+ view = createEditor("f'''hello ", 10);
184
+ const result = stringBraceInputHandler(view, 10, 10, "{");
185
+
186
+ expect(result).toBe(true);
187
+ expect(view.state.doc.toString()).toBe("f'''hello {}");
188
+ expect(view.state.selection.main.head).toBe(11);
189
+ });
190
+
191
+ it("should NOT auto-close braces when text is selected", () => {
192
+ view = createEditor('f"hello world"', 8);
193
+ // User has selected "world" (from position 8 to 13)
194
+ const result = stringBraceInputHandler(view, 8, 13, "{");
195
+
196
+ expect(result).toBe(false);
197
+ // Document should remain unchanged since we return false
198
+ expect(view.state.doc.toString()).toBe('f"hello world"');
199
+ });
200
+ });
@@ -0,0 +1,37 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { syntaxTree } from "@codemirror/language";
3
+ import type { Extension } from "@codemirror/state";
4
+ import { EditorView } from "@codemirror/view";
5
+
6
+ export function stringBraceInputHandler(
7
+ view: EditorView,
8
+ from: number,
9
+ to: number,
10
+ text: string,
11
+ ): boolean {
12
+ if (text !== "{") {
13
+ return false;
14
+ }
15
+
16
+ if (from !== to) {
17
+ return false;
18
+ }
19
+
20
+ const tree = syntaxTree(view.state);
21
+ const node = tree.resolveInner(from, -1);
22
+
23
+ if (!node?.type.name.includes("String")) {
24
+ return false;
25
+ }
26
+
27
+ view.dispatch({
28
+ changes: { from, to, insert: "{}" },
29
+ selection: { anchor: from + 1 },
30
+ userEvent: "input.type",
31
+ });
32
+ return true;
33
+ }
34
+
35
+ export function stringsAutoCloseBraces(): Extension {
36
+ return EditorView.inputHandler.of(stringBraceInputHandler);
37
+ }
@@ -45,6 +45,7 @@ test("default UserConfig - empty", () => {
45
45
  expect(defaultConfig).toMatchInlineSnapshot(`
46
46
  {
47
47
  "ai": {
48
+ "custom_providers": {},
48
49
  "inline_tooltip": false,
49
50
  "mode": "manual",
50
51
  "models": {
@@ -114,6 +115,7 @@ test("default UserConfig - one level", () => {
114
115
  expect(defaultConfig).toMatchInlineSnapshot(`
115
116
  {
116
117
  "ai": {
118
+ "custom_providers": {},
117
119
  "inline_tooltip": false,
118
120
  "mode": "manual",
119
121
  "models": {
@@ -198,6 +200,40 @@ test("default UserConfig with additional information", () => {
198
200
  );
199
201
  });
200
202
 
203
+ test("UserConfig with custom_providers", () => {
204
+ const config = UserConfigSchema.parse({
205
+ ai: {
206
+ custom_providers: {
207
+ my_provider: {
208
+ api_key: "test-key",
209
+ base_url: "https://api.example.com/v1",
210
+ },
211
+ another_provider: {
212
+ base_url: "https://api.another.com/v1",
213
+ },
214
+ },
215
+ },
216
+ });
217
+
218
+ expect(config.ai?.custom_providers).toEqual({
219
+ my_provider: {
220
+ api_key: "test-key",
221
+ base_url: "https://api.example.com/v1",
222
+ },
223
+ another_provider: {
224
+ base_url: "https://api.another.com/v1",
225
+ },
226
+ });
227
+ });
228
+
229
+ test("UserConfig custom_providers defaults to empty object", () => {
230
+ const config = UserConfigSchema.parse({
231
+ ai: {},
232
+ });
233
+
234
+ expect(config.ai?.custom_providers).toEqual({});
235
+ });
236
+
201
237
  test("resolvedMarimoConfigAtom overrides correctly and does not mutate the original array", () => {
202
238
  const initialUserConfig = {
203
239
  completion: {
@@ -175,6 +175,7 @@ export const UserConfigSchema = z
175
175
  aws_secret_access_key: z.string().optional(),
176
176
  })
177
177
  .optional(),
178
+ custom_providers: z.record(z.string(), AiConfigSchema).prefault({}),
178
179
  models: AiModelsSchema.prefault({
179
180
  displayed_models: [],
180
181
  custom_models: [],
@@ -1,11 +1,17 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { useAtomValue } from "jotai";
3
+ import { atom, useAtomValue } from "jotai";
4
4
  import { createReducerAndAtoms } from "@/utils/createReducer";
5
5
  import type { Identified } from "@/utils/typed";
6
6
  import { generateUUID } from "@/utils/uuid";
7
7
  import type { Banner } from "../kernel/messages";
8
8
 
9
+ /**
10
+ * Atom for storing kernel startup error message.
11
+ * When set to a non-null value, shows a modal with the error details.
12
+ */
13
+ export const kernelStartupErrorAtom = atom<string | null>(null);
14
+
9
15
  interface BannerState {
10
16
  banners: Identified<Banner>[];
11
17
  }