@marimo-team/islands 0.23.14-dev5 → 0.23.14-dev50

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 (128) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-DXBx_nQg.js} +4 -4
  2. package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DXPRhRO2.js} +3248 -3044
  3. package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
  4. package/dist/{code-visibility-rxZi4Phe.js → code-visibility-BFDT-GEQ.js} +1153 -946
  5. package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
  6. package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
  7. package/dist/{formats-d6MhLuQ9.js → formats-WsOgyW_K.js} +1 -1
  8. package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-7wxMGXjG.js} +2084 -1889
  9. package/dist/{html-to-image-CGp_08St.js → html-to-image-DOqQBSQc.js} +2367 -2301
  10. package/dist/{input-CbEz_aj_.js → input-BSdZp5Ng.js} +1 -1
  11. package/dist/main.js +2707 -1186
  12. package/dist/{mermaid-CJW9vIyO.js → mermaid-D-HYBMEV.js} +2 -2
  13. package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
  14. package/dist/{process-output-R6JsYrv3.js → process-output-kGk2Jc2x.js} +1 -1
  15. package/dist/{reveal-component-sEb3Wd1x.js → reveal-component-uSCMaaUE.js} +600 -596
  16. package/dist/{spec-Bv-XlYiv.js → spec-CnTgI25l.js} +1 -1
  17. package/dist/style.css +1 -1
  18. package/dist/{toDate-D-l5s8nn.js → toDate-D1Z7ZXWh.js} +1 -1
  19. package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-BMc8itk2.js} +1 -1
  20. package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-ZwmDBRDY.js} +1 -1
  21. package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-CxffarYV.js} +1 -1
  22. package/dist/{useTheme-BrYvK-_A.js → useTheme-yGsGEk82.js} +26 -24
  23. package/dist/{vega-component-Pk6lyc_a.js → vega-component-BFJTyykA.js} +5 -5
  24. package/package.json +3 -3
  25. package/src/__mocks__/requests.ts +1 -0
  26. package/src/components/app-config/user-config-form.tsx +26 -0
  27. package/src/components/chat/acp/agent-panel.tsx +35 -1
  28. package/src/components/chat/chat-panel.tsx +68 -29
  29. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  30. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  31. package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
  32. package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
  33. package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
  34. package/src/components/data-table/charts/charts.tsx +23 -1
  35. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  36. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  37. package/src/components/debugger/debugger-code.tsx +7 -2
  38. package/src/components/editor/actions/useNotebookActions.tsx +2 -2
  39. package/src/components/editor/cell/cell-context-menu.tsx +2 -2
  40. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
  41. package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  42. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  43. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  44. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  45. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
  46. package/src/components/editor/code/readonly-python-code.tsx +60 -33
  47. package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
  48. package/src/components/editor/errors/auto-fix.tsx +88 -34
  49. package/src/components/editor/errors/fix-mode.ts +1 -1
  50. package/src/components/editor/notebook-cell.tsx +7 -0
  51. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
  52. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  53. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
  54. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
  55. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  56. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  57. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  58. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  59. package/src/components/slides/__tests__/slide.test.tsx +33 -0
  60. package/src/components/slides/minimap.tsx +7 -1
  61. package/src/components/slides/reveal-component.tsx +3 -0
  62. package/src/components/slides/slide-cell-view.tsx +10 -6
  63. package/src/components/slides/slide.tsx +16 -13
  64. package/src/core/ai/config.ts +2 -2
  65. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  66. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  67. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  68. package/src/core/ai/context/providers/datasource.ts +27 -2
  69. package/src/core/ai/context/providers/error.ts +226 -36
  70. package/src/core/ai/context/registry.ts +77 -43
  71. package/src/core/ai/state.ts +11 -0
  72. package/src/core/cells/__tests__/cell.test.ts +39 -0
  73. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  74. package/src/core/cells/cell.ts +5 -3
  75. package/src/core/cells/readonly-code-display.ts +35 -0
  76. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  77. package/src/core/codemirror/ai/resources.ts +15 -10
  78. package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
  79. package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +139 -0
  80. package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
  81. package/src/core/codemirror/cells/debugger-state.ts +100 -0
  82. package/src/core/codemirror/cells/extensions.ts +23 -4
  83. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
  84. package/src/core/codemirror/go-to-definition/extension.ts +3 -3
  85. package/src/core/codemirror/go-to-definition/underline.ts +5 -14
  86. package/src/core/codemirror/go-to-definition/utils.ts +49 -1
  87. package/src/core/codemirror/utils.ts +15 -0
  88. package/src/core/config/feature-flag.tsx +2 -0
  89. package/src/core/edit-app.tsx +8 -5
  90. package/src/core/errors/errors.ts +2 -1
  91. package/src/core/islands/bootstrap.ts +1 -0
  92. package/src/core/islands/bridge.ts +1 -0
  93. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  94. package/src/core/lifecycle/render-policy.ts +125 -0
  95. package/src/core/mime.ts +11 -4
  96. package/src/core/network/__tests__/api.test.ts +17 -0
  97. package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
  98. package/src/core/network/api.ts +17 -6
  99. package/src/core/network/requests-lazy.ts +1 -0
  100. package/src/core/network/requests-network.ts +8 -0
  101. package/src/core/network/requests-static.ts +1 -0
  102. package/src/core/network/requests-toasting.tsx +10 -1
  103. package/src/core/network/types.ts +2 -0
  104. package/src/core/run-app.tsx +27 -23
  105. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  106. package/src/core/runtime/adapter.ts +182 -0
  107. package/src/core/wasm/PyodideLoader.tsx +20 -62
  108. package/src/core/wasm/bridge.ts +13 -4
  109. package/src/core/wasm/state.ts +8 -19
  110. package/src/core/websocket/useMarimoKernelConnection.tsx +17 -0
  111. package/src/css/app/codemirror.css +14 -0
  112. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  113. package/src/hooks/useDelayElapsed.ts +27 -0
  114. package/src/plugins/core/RenderHTML.tsx +35 -1
  115. package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
  116. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  117. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +53 -2
  118. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +52 -2
  119. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
  120. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
  121. package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
  122. package/src/utils/errors.ts +15 -0
  123. package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
  124. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
  125. package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
  126. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  127. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  128. package/src/core/wasm/__tests__/state.test.ts +0 -124
@@ -0,0 +1,139 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { MockRequestClient } from "@/__mocks__/requests";
5
+ import { cellId } from "@/__tests__/branded";
6
+ import { store } from "@/core/state/jotai";
7
+ import {
8
+ breakpointsAtom,
9
+ clearCellBreakpoints,
10
+ createCellBreakpointsAtom,
11
+ createDebuggerLineAtom,
12
+ debuggerCurrentLineAtom,
13
+ resyncBreakpoints,
14
+ toggleBreakpoint,
15
+ } from "../debugger-state";
16
+
17
+ const mockRequestClient = MockRequestClient.create();
18
+ vi.mock("@/core/network/requests", () => ({
19
+ getRequestClient: () => mockRequestClient,
20
+ }));
21
+
22
+ const cell1 = cellId("cell1");
23
+ const cell2 = cellId("cell2");
24
+
25
+ describe("debugger-state", () => {
26
+ beforeEach(() => {
27
+ store.set(breakpointsAtom, new Map());
28
+ store.set(debuggerCurrentLineAtom, null);
29
+ vi.clearAllMocks();
30
+ });
31
+
32
+ describe("toggleBreakpoint", () => {
33
+ it("adds a breakpoint and syncs the full set to the kernel", () => {
34
+ toggleBreakpoint(cell1, 3);
35
+
36
+ expect(store.get(breakpointsAtom).get(cell1)).toEqual(new Set([3]));
37
+ expect(mockRequestClient.sendSetBreakpoints).toHaveBeenCalledWith({
38
+ breakpoints: { [cell1]: [3] },
39
+ });
40
+ });
41
+
42
+ it("removes a breakpoint on second toggle and drops the cell once empty", () => {
43
+ toggleBreakpoint(cell1, 3);
44
+ toggleBreakpoint(cell1, 3);
45
+
46
+ expect(store.get(breakpointsAtom).has(cell1)).toBe(false);
47
+ expect(mockRequestClient.sendSetBreakpoints).toHaveBeenLastCalledWith({
48
+ breakpoints: {},
49
+ });
50
+ });
51
+
52
+ it("sends lines sorted ascending for a cell with multiple breakpoints", () => {
53
+ toggleBreakpoint(cell1, 5);
54
+ toggleBreakpoint(cell1, 2);
55
+
56
+ expect(mockRequestClient.sendSetBreakpoints).toHaveBeenLastCalledWith({
57
+ breakpoints: { [cell1]: [2, 5] },
58
+ });
59
+ });
60
+
61
+ it("keeps breakpoints for other cells independent", () => {
62
+ toggleBreakpoint(cell1, 1);
63
+ toggleBreakpoint(cell2, 2);
64
+
65
+ expect(store.get(breakpointsAtom).get(cell1)).toEqual(new Set([1]));
66
+ expect(store.get(breakpointsAtom).get(cell2)).toEqual(new Set([2]));
67
+ });
68
+ });
69
+
70
+ describe("clearCellBreakpoints", () => {
71
+ it("removes all breakpoints for a cell and syncs", () => {
72
+ toggleBreakpoint(cell1, 1);
73
+ toggleBreakpoint(cell1, 2);
74
+ vi.clearAllMocks();
75
+
76
+ clearCellBreakpoints(cell1);
77
+
78
+ expect(store.get(breakpointsAtom).has(cell1)).toBe(false);
79
+ expect(mockRequestClient.sendSetBreakpoints).toHaveBeenCalledWith({
80
+ breakpoints: {},
81
+ });
82
+ });
83
+
84
+ it("is a no-op when the cell has no breakpoints", () => {
85
+ clearCellBreakpoints(cell1);
86
+
87
+ expect(mockRequestClient.sendSetBreakpoints).not.toHaveBeenCalled();
88
+ });
89
+ });
90
+
91
+ describe("resyncBreakpoints", () => {
92
+ it("re-sends the current breakpoint set", () => {
93
+ toggleBreakpoint(cell1, 4);
94
+ vi.clearAllMocks();
95
+
96
+ resyncBreakpoints();
97
+
98
+ expect(mockRequestClient.sendSetBreakpoints).toHaveBeenCalledWith({
99
+ breakpoints: { [cell1]: [4] },
100
+ });
101
+ });
102
+
103
+ it("does not send when there are no breakpoints", () => {
104
+ resyncBreakpoints();
105
+
106
+ expect(mockRequestClient.sendSetBreakpoints).not.toHaveBeenCalled();
107
+ });
108
+ });
109
+
110
+ describe("createCellBreakpointsAtom", () => {
111
+ it("returns the empty set for a cell with no breakpoints", () => {
112
+ const atom = createCellBreakpointsAtom(cell1);
113
+ expect(store.get(atom)).toEqual(new Set());
114
+ });
115
+
116
+ it("reflects breakpoints for its own cell only", () => {
117
+ toggleBreakpoint(cell1, 7);
118
+ const cell1Atom = createCellBreakpointsAtom(cell1);
119
+ const cell2Atom = createCellBreakpointsAtom(cell2);
120
+
121
+ expect(store.get(cell1Atom)).toEqual(new Set([7]));
122
+ expect(store.get(cell2Atom)).toEqual(new Set());
123
+ });
124
+ });
125
+
126
+ describe("createDebuggerLineAtom", () => {
127
+ it("returns null when no line is set", () => {
128
+ const atom = createDebuggerLineAtom(cell1);
129
+ expect(store.get(atom)).toBeNull();
130
+ });
131
+
132
+ it("returns the line only for the matching cell", () => {
133
+ store.set(debuggerCurrentLineAtom, { cellId: cell1, line: 10 });
134
+
135
+ expect(store.get(createDebuggerLineAtom(cell1))).toBe(10);
136
+ expect(store.get(createDebuggerLineAtom(cell2))).toBeNull();
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,188 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ type EditorState,
5
+ type Extension,
6
+ RangeSet,
7
+ RangeSetBuilder,
8
+ StateEffect,
9
+ StateField,
10
+ } from "@codemirror/state";
11
+ import {
12
+ Decoration,
13
+ type DecorationSet,
14
+ EditorView,
15
+ gutter,
16
+ GutterMarker,
17
+ type PluginValue,
18
+ ViewPlugin,
19
+ type ViewUpdate,
20
+ } from "@codemirror/view";
21
+ import type { CellId } from "@/core/cells/ids";
22
+ import type { Observable } from "@/core/state/observable";
23
+ import { Logger } from "@/utils/Logger";
24
+ import { toggleBreakpoint } from "./debugger-state";
25
+
26
+ // --- Current-line highlight -------------------------------------------------
27
+
28
+ function createLineDecoration(
29
+ state: EditorState,
30
+ line: number | null,
31
+ ): DecorationSet {
32
+ if (line == null || line < 1 || line > state.doc.lines) {
33
+ return Decoration.none;
34
+ }
35
+ const builder = new RangeSetBuilder<Decoration>();
36
+ try {
37
+ const doc = state.doc.line(line);
38
+ builder.add(
39
+ doc.from,
40
+ doc.from,
41
+ Decoration.line({ class: "cm-debugger-current-line" }),
42
+ );
43
+ } catch (error) {
44
+ Logger.debug("Invalid debugger line", { error });
45
+ }
46
+ return builder.finish();
47
+ }
48
+
49
+ class CurrentLineHighlighter implements PluginValue {
50
+ private unsubscribe: () => void;
51
+ decorations: DecorationSet;
52
+
53
+ constructor(view: EditorView, lineObservable: Observable<number | null>) {
54
+ this.decorations = createLineDecoration(view.state, lineObservable.get());
55
+ this.unsubscribe = lineObservable.sub((line) => {
56
+ this.decorations = createLineDecoration(view.state, line);
57
+ // Force a re-render; the decoration set changed out of band.
58
+ view.dispatch({ userEvent: "marimo.debugger-line-update" });
59
+ });
60
+ }
61
+
62
+ update(update: ViewUpdate) {
63
+ this.decorations = this.decorations.map(update.changes);
64
+ }
65
+
66
+ destroy() {
67
+ this.unsubscribe();
68
+ }
69
+ }
70
+
71
+ /** Highlight the line a cell's frame watcher is currently executing. */
72
+ export function debuggerLineHighlighter(
73
+ lineObservable: Observable<number | null>,
74
+ ): Extension {
75
+ return [
76
+ ViewPlugin.define(
77
+ (view) => new CurrentLineHighlighter(view, lineObservable),
78
+ { decorations: (plugin) => plugin.decorations },
79
+ ),
80
+ EditorView.theme({
81
+ ".cm-debugger-current-line": {
82
+ backgroundColor: "color-mix(in srgb, var(--amber-4) 50%, transparent)",
83
+ },
84
+ }),
85
+ ];
86
+ }
87
+
88
+ // --- Breakpoint gutter ------------------------------------------------------
89
+
90
+ class BreakpointMarker extends GutterMarker {
91
+ override toDOM() {
92
+ const dot = document.createElement("div");
93
+ dot.className = "cm-breakpoint-marker";
94
+ return dot;
95
+ }
96
+ }
97
+
98
+ const breakpointMarker = new BreakpointMarker();
99
+
100
+ const setBreakpointLines = StateEffect.define<ReadonlySet<number>>();
101
+
102
+ const breakpointLinesField = StateField.define<ReadonlySet<number>>({
103
+ create: () => new Set<number>(),
104
+ update(value, tr) {
105
+ for (const effect of tr.effects) {
106
+ if (effect.is(setBreakpointLines)) {
107
+ return effect.value;
108
+ }
109
+ }
110
+ return value;
111
+ },
112
+ });
113
+
114
+ function buildBreakpointMarkers(state: EditorState): RangeSet<GutterMarker> {
115
+ const lines = state.field(breakpointLinesField);
116
+ if (lines.size === 0) {
117
+ return RangeSet.empty;
118
+ }
119
+ const builder = new RangeSetBuilder<GutterMarker>();
120
+ for (const lineNo of [...lines].toSorted((a, b) => a - b)) {
121
+ if (lineNo >= 1 && lineNo <= state.doc.lines) {
122
+ const line = state.doc.line(lineNo);
123
+ builder.add(line.from, line.from, breakpointMarker);
124
+ }
125
+ }
126
+ return builder.finish();
127
+ }
128
+
129
+ /** Keep `breakpointLinesField` in sync with the per-cell breakpoints atom. */
130
+ class BreakpointSync implements PluginValue {
131
+ private unsubscribe: () => void;
132
+
133
+ constructor(view: EditorView, observable: Observable<ReadonlySet<number>>) {
134
+ const apply = (lines: ReadonlySet<number>) => {
135
+ view.dispatch({ effects: setBreakpointLines.of(lines) });
136
+ };
137
+ // Dispatching during view construction is disallowed; defer the initial
138
+ // sync (e.g. for editors that mount after breakpoints already exist).
139
+ // Read the value fresh inside the microtask so a `sub` update that lands
140
+ // first isn't clobbered by a stale snapshot.
141
+ if (observable.get().size > 0) {
142
+ queueMicrotask(() => apply(observable.get()));
143
+ }
144
+ this.unsubscribe = observable.sub(apply);
145
+ }
146
+
147
+ destroy() {
148
+ this.unsubscribe();
149
+ }
150
+ }
151
+
152
+ /** A clickable gutter for toggling breakpoints on a cell's lines. */
153
+ export function breakpointGutter(
154
+ cellId: CellId,
155
+ breakpointsObservable: Observable<ReadonlySet<number>>,
156
+ ): Extension {
157
+ return [
158
+ breakpointLinesField,
159
+ ViewPlugin.define(
160
+ (view) => new BreakpointSync(view, breakpointsObservable),
161
+ ),
162
+ gutter({
163
+ class: "cm-breakpoint-gutter",
164
+ markers: (view) => buildBreakpointMarkers(view.state),
165
+ initialSpacer: () => breakpointMarker,
166
+ domEventHandlers: {
167
+ mousedown: (view, line) => {
168
+ const lineNo = view.state.doc.lineAt(line.from).number;
169
+ toggleBreakpoint(cellId, lineNo);
170
+ return true;
171
+ },
172
+ },
173
+ }),
174
+ EditorView.theme({
175
+ ".cm-breakpoint-gutter": {
176
+ width: "14px",
177
+ cursor: "pointer",
178
+ },
179
+ ".cm-breakpoint-marker": {
180
+ width: "8px",
181
+ height: "8px",
182
+ borderRadius: "50%",
183
+ backgroundColor: "var(--red-9)",
184
+ margin: "4px auto",
185
+ },
186
+ }),
187
+ ];
188
+ }
@@ -0,0 +1,100 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { atom } from "jotai";
4
+ import type { CellId } from "@/core/cells/ids";
5
+ import { getRequestClient } from "@/core/network/requests";
6
+ import { store } from "@/core/state/jotai";
7
+ import { Logger } from "@/utils/Logger";
8
+
9
+ /**
10
+ * State for the experimental live debugger.
11
+ *
12
+ * - `debuggerCurrentLineAtom` holds the cell + line the kernel's frame watcher
13
+ * is currently executing (driven by `active-line` notifications). Only one
14
+ * cell runs at a time, so a single global value suffices.
15
+ * - `breakpointsAtom` holds the user's gutter breakpoints, session-only. It is
16
+ * the source of truth; mutations are mirrored to the kernel via
17
+ * `sendSetBreakpoints`.
18
+ */
19
+
20
+ export interface DebuggerLine {
21
+ cellId: CellId;
22
+ line: number;
23
+ }
24
+
25
+ export const debuggerCurrentLineAtom = atom<DebuggerLine | null>(null);
26
+
27
+ export const breakpointsAtom = atom<ReadonlyMap<CellId, ReadonlySet<number>>>(
28
+ new Map<CellId, ReadonlySet<number>>(),
29
+ );
30
+
31
+ const EMPTY_LINES: ReadonlySet<number> = new Set();
32
+
33
+ /** Per-cell derived atom: the current debug line for `cellId`, or `null`. */
34
+ export function createDebuggerLineAtom(cellId: CellId) {
35
+ return atom((get) => {
36
+ const current = get(debuggerCurrentLineAtom);
37
+ return current?.cellId === cellId ? current.line : null;
38
+ });
39
+ }
40
+
41
+ /** Per-cell derived atom: the breakpoint lines for `cellId`. */
42
+ export function createCellBreakpointsAtom(cellId: CellId) {
43
+ return atom((get) => get(breakpointsAtom).get(cellId) ?? EMPTY_LINES);
44
+ }
45
+
46
+ function sendBreakpoints(map: ReadonlyMap<CellId, ReadonlySet<number>>): void {
47
+ const breakpoints: Record<string, number[]> = {};
48
+ for (const [cellId, lines] of map) {
49
+ if (lines.size > 0) {
50
+ breakpoints[cellId] = [...lines].toSorted((a, b) => a - b);
51
+ }
52
+ }
53
+ // Fire-and-forget: the request layer rethrows on failure (e.g. static or
54
+ // wasm mode with no live kernel). Swallow it so toggling a breakpoint never
55
+ // surfaces as an unhandled promise rejection.
56
+ getRequestClient()
57
+ .sendSetBreakpoints({ breakpoints })
58
+ .catch((error) => {
59
+ Logger.debug("Failed to sync breakpoints to kernel", { error });
60
+ });
61
+ }
62
+
63
+ /** Remove all breakpoints for a cell and sync the full set to the kernel. */
64
+ export function clearCellBreakpoints(cellId: CellId): void {
65
+ const prev = store.get(breakpointsAtom);
66
+ if (!prev.has(cellId)) {
67
+ return;
68
+ }
69
+ const next = new Map(prev);
70
+ next.delete(cellId);
71
+ store.set(breakpointsAtom, next);
72
+ sendBreakpoints(next);
73
+ }
74
+
75
+ /** Re-push the current breakpoints to a freshly started kernel, whose `MarimoPdb` starts empty. */
76
+ export function resyncBreakpoints(): void {
77
+ const current = store.get(breakpointsAtom);
78
+ if (current.size > 0) {
79
+ sendBreakpoints(current);
80
+ }
81
+ }
82
+
83
+ /** Toggle a breakpoint at `(cellId, line)` and sync the full set to the kernel. */
84
+ export function toggleBreakpoint(cellId: CellId, line: number): void {
85
+ const prev = store.get(breakpointsAtom);
86
+ const next = new Map(prev);
87
+ const lines = new Set(next.get(cellId) ?? []);
88
+ if (lines.has(line)) {
89
+ lines.delete(line);
90
+ } else {
91
+ lines.add(line);
92
+ }
93
+ if (lines.size > 0) {
94
+ next.set(cellId, lines);
95
+ } else {
96
+ next.delete(cellId);
97
+ }
98
+ store.set(breakpointsAtom, next);
99
+ sendBreakpoints(next);
100
+ }
@@ -17,6 +17,7 @@ import {
17
17
  import { loroSyncAnnotation } from "@/core/codemirror/rtc/loro/sync";
18
18
  import type { KeymapConfig } from "@/core/config/config-schema";
19
19
  import type { HotkeyProvider } from "@/core/hotkeys/hotkeys";
20
+ import { getFeatureFlag } from "@/core/config/feature-flag";
20
21
  import { duplicateWithCtrlModifier } from "@/core/hotkeys/shortcuts";
21
22
  import { store } from "@/core/state/jotai";
22
23
  import { createObservable } from "@/core/state/observable";
@@ -25,6 +26,14 @@ import { formattingChangeEffect } from "../format";
25
26
  import { goToDefinitionAtCursorPosition } from "../go-to-definition/utils";
26
27
  import { getEditorCodeAsPython } from "../language/utils";
27
28
  import { isAtEndOfEditor, isAtStartOfEditor } from "../utils";
29
+ import {
30
+ breakpointGutter,
31
+ debuggerLineHighlighter,
32
+ } from "./debugger-decorations";
33
+ import {
34
+ createCellBreakpointsAtom,
35
+ createDebuggerLineAtom,
36
+ } from "./debugger-state";
28
37
  import {
29
38
  type CodemirrorCellActions,
30
39
  cellActionsState,
@@ -106,11 +115,8 @@ function cellKeymaps({
106
115
  keybindings.push(
107
116
  {
108
117
  key: hotkeys.getHotkey("cell.goToDefinition").key,
109
- preventDefault: true,
110
- stopPropagation: true,
111
118
  run: (ev) => {
112
- goToDefinitionAtCursorPosition(ev);
113
- return true;
119
+ return goToDefinitionAtCursorPosition(ev);
114
120
  },
115
121
  },
116
122
  {
@@ -431,6 +437,19 @@ export function cellBundle({
431
437
  errorLineHighlighter(
432
438
  createObservable(createTracebackInfoAtom(cellId), store),
433
439
  ),
440
+ // Experimental live debugger: clickable breakpoint gutter + current-line
441
+ // highlight. Gated so there is no gutter/overhead when disabled.
442
+ getFeatureFlag("debugger")
443
+ ? [
444
+ breakpointGutter(
445
+ cellId,
446
+ createObservable(createCellBreakpointsAtom(cellId), store),
447
+ ),
448
+ debuggerLineHighlighter(
449
+ createObservable(createDebuggerLineAtom(cellId), store),
450
+ ),
451
+ ]
452
+ : [],
434
453
  ];
435
454
  }
436
455
 
@@ -2,13 +2,17 @@
2
2
 
3
3
  import { python } from "@codemirror/lang-python";
4
4
  import { EditorState } from "@codemirror/state";
5
- import { EditorView } from "@codemirror/view";
6
- import { afterEach, describe, expect, test } from "vitest";
5
+ import { EditorView, keymap } from "@codemirror/view";
6
+ import { afterEach, describe, expect, test, vi } from "vitest";
7
7
  import { cellId, variableName } from "@/__tests__/branded";
8
8
  import { initialNotebookState, notebookAtom } from "@/core/cells/cells";
9
9
  import { store } from "@/core/state/jotai";
10
10
  import { variablesAtom } from "@/core/variables/state";
11
- import { goToDefinitionAtCursorPosition } from "../utils";
11
+ import {
12
+ goToDefinitionAtCursorPosition,
13
+ goToDefinitionWithLspFallback,
14
+ requestLspGoToDefinition,
15
+ } from "../utils";
12
16
 
13
17
  async function tick(): Promise<void> {
14
18
  await new Promise((resolve) => requestAnimationFrame(resolve));
@@ -181,3 +185,71 @@ print(mymodule)`;
181
185
  );
182
186
  });
183
187
  });
188
+
189
+ describe("goToDefinitionWithLspFallback", () => {
190
+ test("falls through to LSP when marimo cannot resolve the symbol", () => {
191
+ const lspGoToDefinition = vi.fn(() => true);
192
+ const view = new EditorView({
193
+ state: EditorState.create({
194
+ doc: "parser.add_argument('--foo')",
195
+ selection: { anchor: "parser.add_argument".indexOf("add_argument") },
196
+ extensions: [
197
+ python(),
198
+ keymap.of([{ key: "F12", run: lspGoToDefinition }]),
199
+ ],
200
+ }),
201
+ parent: document.body,
202
+ });
203
+ views.push(view);
204
+
205
+ const result = goToDefinitionWithLspFallback(view);
206
+
207
+ expect(result).toBe(true);
208
+ expect(lspGoToDefinition).toHaveBeenCalledOnce();
209
+ });
210
+
211
+ test("does not invoke LSP when marimo resolves the symbol", async () => {
212
+ const lspGoToDefinition = vi.fn(() => true);
213
+ const code = "a = 10\nprint(a)";
214
+ const view = new EditorView({
215
+ state: EditorState.create({
216
+ doc: code,
217
+ selection: { anchor: code.indexOf("a", 3) },
218
+ extensions: [
219
+ python(),
220
+ keymap.of([{ key: "F12", run: lspGoToDefinition }]),
221
+ ],
222
+ }),
223
+ parent: document.body,
224
+ });
225
+ views.push(view);
226
+
227
+ const result = goToDefinitionWithLspFallback(view);
228
+
229
+ expect(result).toBe(true);
230
+ expect(lspGoToDefinition).not.toHaveBeenCalled();
231
+ await tick();
232
+ expect(view.state.selection.main.head).toBe(0);
233
+ });
234
+
235
+ test("falls through with a modified shortcut like Ctrl-F12", () => {
236
+ const lspGoToDefinition = vi.fn(() => true);
237
+ const view = new EditorView({
238
+ state: EditorState.create({
239
+ doc: "parser.add_argument('--foo')",
240
+ selection: { anchor: "parser.add_argument".indexOf("add_argument") },
241
+ extensions: [
242
+ python(),
243
+ keymap.of([{ key: "Ctrl-F12", run: lspGoToDefinition }]),
244
+ ],
245
+ }),
246
+ parent: document.body,
247
+ });
248
+ views.push(view);
249
+
250
+ const result = requestLspGoToDefinition(view, "Ctrl-F12");
251
+
252
+ expect(result).toBe(true);
253
+ expect(lspGoToDefinition).toHaveBeenCalledOnce();
254
+ });
255
+ });
@@ -1,7 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { EditorView } from "@codemirror/view";
3
3
  import { createUnderlinePlugin, underlineField } from "./underline";
4
- import { goToDefinition } from "./utils";
4
+ import { goToDefinitionWithLspFallback } from "./utils";
5
5
 
6
6
  /**
7
7
  * Create a go-to-definition extension.
@@ -9,8 +9,8 @@ import { goToDefinition } from "./utils";
9
9
  export function goToDefinitionBundle() {
10
10
  return [
11
11
  underlineField,
12
- createUnderlinePlugin((view, variableName) => {
13
- goToDefinition(view, variableName);
12
+ createUnderlinePlugin((view) => {
13
+ goToDefinitionWithLspFallback(view);
14
14
  }),
15
15
  EditorView.baseTheme({
16
16
  ".underline": {
@@ -55,12 +55,9 @@ class MetaUnderlineVariablePlugin {
55
55
  private view: EditorView;
56
56
  private commandClickMode: boolean;
57
57
  private hoveredRange: { from: number; to: number; position: number } | null;
58
- private onClick: (view: EditorView, variableName: string) => void;
58
+ private onClick: (view: EditorView) => void;
59
59
 
60
- constructor(
61
- view: EditorView,
62
- onClick: (view: EditorView, variableName: string) => void,
63
- ) {
60
+ constructor(view: EditorView, onClick: (view: EditorView) => void) {
64
61
  this.view = view;
65
62
  this.commandClickMode = false;
66
63
  this.hoveredRange = null;
@@ -191,20 +188,16 @@ class MetaUnderlineVariablePlugin {
191
188
  // If we have a hovered range, go to it
192
189
  private click = (event: MouseEvent) => {
193
190
  if (this.hoveredRange) {
194
- const variableName = this.view.state.doc.sliceString(
195
- this.hoveredRange.from,
196
- this.hoveredRange.to,
197
- );
198
191
  event.preventDefault();
199
192
  event.stopPropagation();
200
- this.onClick(this.view, variableName);
201
- // Move the cursor to the clicked position
193
+ // Move the cursor before resolving so LSP uses the clicked position.
202
194
  this.view.dispatch({
203
195
  selection: {
204
196
  head: this.hoveredRange.position,
205
197
  anchor: this.hoveredRange.position,
206
198
  },
207
199
  });
200
+ this.onClick(this.view);
208
201
  }
209
202
  };
210
203
 
@@ -217,7 +210,5 @@ class MetaUnderlineVariablePlugin {
217
210
  }
218
211
  }
219
212
 
220
- export const createUnderlinePlugin = (
221
- onClick: (view: EditorView, variableName: string) => void,
222
- ) =>
213
+ export const createUnderlinePlugin = (onClick: (view: EditorView) => void) =>
223
214
  ViewPlugin.define((view) => new MetaUnderlineVariablePlugin(view, onClick));