@marimo-team/islands 0.23.14-dev6 → 0.23.14-dev61

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 (171) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
  2. package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
  3. package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
  4. package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
  5. package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
  6. package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
  7. package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
  8. package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
  9. package/dist/{formats-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
  10. package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
  11. package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
  12. package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
  13. package/dist/main.js +3630 -1801
  14. package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.js} +2 -2
  15. package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
  16. package/dist/{process-output-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
  17. package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
  18. package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
  19. package/dist/style.css +1 -1
  20. package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
  21. package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
  22. package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
  23. package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
  24. package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
  25. package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
  26. package/package.json +4 -4
  27. package/src/__mocks__/requests.ts +1 -0
  28. package/src/__tests__/CellStatus.test.tsx +1 -2
  29. package/src/components/app-config/user-config-form.tsx +52 -0
  30. package/src/components/chat/acp/agent-panel.tsx +35 -1
  31. package/src/components/chat/chat-panel.tsx +68 -29
  32. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  33. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  34. package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
  35. package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
  36. package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
  37. package/src/components/data-table/charts/charts.tsx +23 -1
  38. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  39. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  40. package/src/components/debugger/debugger-code.tsx +7 -2
  41. package/src/components/editor/actions/useNotebookActions.tsx +2 -2
  42. package/src/components/editor/cell/CellStatus.tsx +1 -20
  43. package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
  44. package/src/components/editor/cell/cell-context-menu.tsx +2 -2
  45. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
  46. package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  47. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  48. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  49. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  50. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
  51. package/src/components/editor/code/readonly-python-code.tsx +60 -33
  52. package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
  53. package/src/components/editor/errors/auto-fix.tsx +88 -34
  54. package/src/components/editor/errors/fix-mode.ts +1 -1
  55. package/src/components/editor/notebook-cell.tsx +7 -0
  56. package/src/components/editor/output/ImageOutput.tsx +20 -6
  57. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
  58. package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
  59. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  60. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
  61. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
  62. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  63. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  64. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  65. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  66. package/src/components/slides/__tests__/slide.test.tsx +33 -0
  67. package/src/components/slides/minimap.tsx +7 -1
  68. package/src/components/slides/reveal-component.tsx +3 -0
  69. package/src/components/slides/slide-cell-view.tsx +10 -6
  70. package/src/components/slides/slide.tsx +16 -13
  71. package/src/components/tracing/tracing.tsx +2 -1
  72. package/src/core/ai/config.ts +2 -2
  73. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  74. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  75. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  76. package/src/core/ai/context/providers/datasource.ts +27 -2
  77. package/src/core/ai/context/providers/error.ts +226 -36
  78. package/src/core/ai/context/registry.ts +77 -43
  79. package/src/core/ai/state.ts +11 -0
  80. package/src/core/cells/__tests__/cell.test.ts +39 -0
  81. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  82. package/src/core/cells/cell.ts +5 -3
  83. package/src/core/cells/readonly-code-display.ts +35 -0
  84. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  85. package/src/core/codemirror/ai/resources.ts +15 -10
  86. package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
  87. package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
  88. package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
  89. package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
  90. package/src/core/codemirror/cells/debugger-state.ts +133 -0
  91. package/src/core/codemirror/cells/extensions.ts +32 -4
  92. package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
  93. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
  94. package/src/core/codemirror/go-to-definition/extension.ts +3 -3
  95. package/src/core/codemirror/go-to-definition/underline.ts +5 -14
  96. package/src/core/codemirror/go-to-definition/utils.ts +49 -1
  97. package/src/core/codemirror/utils.ts +15 -0
  98. package/src/core/config/__tests__/config-schema.test.ts +17 -0
  99. package/src/core/config/config-schema.ts +1 -0
  100. package/src/core/config/config.ts +10 -0
  101. package/src/core/config/feature-flag.tsx +4 -0
  102. package/src/core/edit-app.tsx +8 -5
  103. package/src/core/errors/errors.ts +2 -1
  104. package/src/core/islands/bootstrap.ts +4 -3
  105. package/src/core/islands/bridge.ts +1 -0
  106. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  107. package/src/core/lifecycle/render-policy.ts +125 -0
  108. package/src/core/mime.ts +11 -4
  109. package/src/core/network/__tests__/api.test.ts +17 -0
  110. package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
  111. package/src/core/network/api.ts +17 -6
  112. package/src/core/network/requests-lazy.ts +1 -0
  113. package/src/core/network/requests-network.ts +8 -0
  114. package/src/core/network/requests-static.ts +1 -0
  115. package/src/core/network/requests-toasting.tsx +10 -1
  116. package/src/core/network/types.ts +2 -0
  117. package/src/core/run-app.tsx +27 -23
  118. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  119. package/src/core/runtime/__tests__/runtime.test.ts +45 -0
  120. package/src/core/runtime/adapter.ts +182 -0
  121. package/src/core/runtime/runtime.ts +30 -16
  122. package/src/core/wasm/PyodideLoader.tsx +20 -62
  123. package/src/core/wasm/bridge.ts +13 -4
  124. package/src/core/wasm/state.ts +8 -19
  125. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
  126. package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
  127. package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
  128. package/src/core/websocket/transports/sse.ts +308 -0
  129. package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
  130. package/src/core/websocket/useWebSocket.tsx +12 -2
  131. package/src/css/app/codemirror.css +14 -0
  132. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  133. package/src/hooks/useDelayElapsed.ts +27 -0
  134. package/src/mount.tsx +3 -3
  135. package/src/plugins/core/RenderHTML.tsx +35 -1
  136. package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
  137. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  138. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
  139. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
  140. package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
  141. package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
  142. package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
  143. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
  144. package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
  145. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
  146. package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
  147. package/src/plugins/impl/anywidget/host.ts +54 -0
  148. package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
  149. package/src/plugins/impl/anywidget/model.ts +59 -239
  150. package/src/plugins/impl/anywidget/registry.ts +268 -0
  151. package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
  152. package/src/plugins/impl/anywidget/runtime.ts +422 -0
  153. package/src/plugins/impl/anywidget/types.ts +14 -0
  154. package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
  155. package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
  156. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
  157. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
  158. package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
  159. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
  160. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
  161. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
  162. package/src/utils/errors.ts +15 -0
  163. package/src/utils/time.ts +20 -0
  164. package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
  165. package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
  166. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
  167. package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
  168. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  169. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  170. package/src/core/wasm/__tests__/state.test.ts +0 -124
  171. package/src/plugins/impl/anywidget/schemas.ts +0 -32
@@ -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,133 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { atom } from "jotai";
4
+ import type { CellId } from "@/core/cells/ids";
5
+ import type { ActiveLineInfo } from "@/core/codemirror/cells/line-timing-decorations";
6
+ import { getRequestClient } from "@/core/network/requests";
7
+ import { store } from "@/core/state/jotai";
8
+ import { Logger } from "@/utils/Logger";
9
+
10
+ /**
11
+ * State for the experimental live debugger and line-timing highlight.
12
+ *
13
+ * - `activeLineAtom` holds the cell + line the kernel's frame watcher is
14
+ * currently executing (driven by `active-line` notifications), plus when
15
+ * the frontend first saw execution on that line. Only one cell runs at a
16
+ * time, so a single global value suffices.
17
+ * - `breakpointsAtom` holds the user's gutter breakpoints, session-only. It is
18
+ * the source of truth; mutations are mirrored to the kernel via
19
+ * `sendSetBreakpoints`.
20
+ */
21
+
22
+ export interface ActiveLine {
23
+ cellId: CellId;
24
+ line: number;
25
+ /** When the frontend first saw execution on this (cellId, line). */
26
+ startedAtMs: number;
27
+ }
28
+
29
+ export const activeLineAtom = atom<ActiveLine | null>(null);
30
+
31
+ /**
32
+ * Update the active line, stamping `startedAtMs` only when the (cellId, line)
33
+ * actually changes, so duplicate notifications (e.g. reconnects) never reset
34
+ * the elapsed-time baseline.
35
+ */
36
+ export function setActiveLine(
37
+ next: { cellId: CellId; line: number } | null,
38
+ ): void {
39
+ const prev = store.get(activeLineAtom);
40
+ if (next === null) {
41
+ store.set(activeLineAtom, null);
42
+ return;
43
+ }
44
+ if (prev?.cellId === next.cellId && prev.line === next.line) {
45
+ return;
46
+ }
47
+ store.set(activeLineAtom, { ...next, startedAtMs: Date.now() });
48
+ }
49
+
50
+ export const breakpointsAtom = atom<ReadonlyMap<CellId, ReadonlySet<number>>>(
51
+ new Map<CellId, ReadonlySet<number>>(),
52
+ );
53
+
54
+ const EMPTY_LINES: ReadonlySet<number> = new Set();
55
+
56
+ /** Per-cell derived atom: the current debug line for `cellId`, or `null`. */
57
+ export function createDebuggerLineAtom(cellId: CellId) {
58
+ return atom((get) => {
59
+ const current = get(activeLineAtom);
60
+ return current?.cellId === cellId ? current.line : null;
61
+ });
62
+ }
63
+
64
+ /** Per-cell derived atom: the active line + start time for `cellId`, or `null`. */
65
+ export function createActiveLineInfoAtom(cellId: CellId) {
66
+ return atom((get): ActiveLineInfo | null => {
67
+ const current = get(activeLineAtom);
68
+ return current?.cellId === cellId
69
+ ? { line: current.line, startedAtMs: current.startedAtMs }
70
+ : null;
71
+ });
72
+ }
73
+
74
+ /** Per-cell derived atom: the breakpoint lines for `cellId`. */
75
+ export function createCellBreakpointsAtom(cellId: CellId) {
76
+ return atom((get) => get(breakpointsAtom).get(cellId) ?? EMPTY_LINES);
77
+ }
78
+
79
+ function sendBreakpoints(map: ReadonlyMap<CellId, ReadonlySet<number>>): void {
80
+ const breakpoints: Record<string, number[]> = {};
81
+ for (const [cellId, lines] of map) {
82
+ if (lines.size > 0) {
83
+ breakpoints[cellId] = [...lines].toSorted((a, b) => a - b);
84
+ }
85
+ }
86
+ // Fire-and-forget: the request layer rethrows on failure (e.g. static or
87
+ // wasm mode with no live kernel). Swallow it so toggling a breakpoint never
88
+ // surfaces as an unhandled promise rejection.
89
+ getRequestClient()
90
+ .sendSetBreakpoints({ breakpoints })
91
+ .catch((error) => {
92
+ Logger.debug("Failed to sync breakpoints to kernel", { error });
93
+ });
94
+ }
95
+
96
+ /** Remove all breakpoints for a cell and sync the full set to the kernel. */
97
+ export function clearCellBreakpoints(cellId: CellId): void {
98
+ const prev = store.get(breakpointsAtom);
99
+ if (!prev.has(cellId)) {
100
+ return;
101
+ }
102
+ const next = new Map(prev);
103
+ next.delete(cellId);
104
+ store.set(breakpointsAtom, next);
105
+ sendBreakpoints(next);
106
+ }
107
+
108
+ /** Re-push the current breakpoints to a freshly started kernel, whose `MarimoPdb` starts empty. */
109
+ export function resyncBreakpoints(): void {
110
+ const current = store.get(breakpointsAtom);
111
+ if (current.size > 0) {
112
+ sendBreakpoints(current);
113
+ }
114
+ }
115
+
116
+ /** Toggle a breakpoint at `(cellId, line)` and sync the full set to the kernel. */
117
+ export function toggleBreakpoint(cellId: CellId, line: number): void {
118
+ const prev = store.get(breakpointsAtom);
119
+ const next = new Map(prev);
120
+ const lines = new Set(next.get(cellId) ?? []);
121
+ if (lines.has(line)) {
122
+ lines.delete(line);
123
+ } else {
124
+ lines.add(line);
125
+ }
126
+ if (lines.size > 0) {
127
+ next.set(cellId, lines);
128
+ } else {
129
+ next.delete(cellId);
130
+ }
131
+ store.set(breakpointsAtom, next);
132
+ sendBreakpoints(next);
133
+ }
@@ -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,16 @@ 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
+ createActiveLineInfoAtom,
35
+ createCellBreakpointsAtom,
36
+ createDebuggerLineAtom,
37
+ } from "./debugger-state";
38
+ import { activeLineTimer } from "./line-timing-decorations";
28
39
  import {
29
40
  type CodemirrorCellActions,
30
41
  cellActionsState,
@@ -106,11 +117,8 @@ function cellKeymaps({
106
117
  keybindings.push(
107
118
  {
108
119
  key: hotkeys.getHotkey("cell.goToDefinition").key,
109
- preventDefault: true,
110
- stopPropagation: true,
111
120
  run: (ev) => {
112
- goToDefinitionAtCursorPosition(ev);
113
- return true;
121
+ return goToDefinitionAtCursorPosition(ev);
114
122
  },
115
123
  },
116
124
  {
@@ -423,6 +431,8 @@ export function cellBundle({
423
431
  cellActions: CodemirrorCellActions;
424
432
  keymapConfig: KeymapConfig;
425
433
  }): Extension[] {
434
+ const debuggerOn = getFeatureFlag("debugger");
435
+ const lineTimingOn = getFeatureFlag("line_timing");
426
436
  return [
427
437
  cellActionsState.of(cellActions),
428
438
  cellIdState.of(cellId),
@@ -431,6 +441,24 @@ export function cellBundle({
431
441
  errorLineHighlighter(
432
442
  createObservable(createTracebackInfoAtom(cellId), store),
433
443
  ),
444
+ // Experimental live debugger and line-timing highlight. Gated so there is
445
+ // no gutter/overhead when disabled. Both track the same active line; when
446
+ // both flags are on, the green timing highlight replaces the amber one.
447
+ debuggerOn
448
+ ? breakpointGutter(
449
+ cellId,
450
+ createObservable(createCellBreakpointsAtom(cellId), store),
451
+ )
452
+ : [],
453
+ lineTimingOn
454
+ ? activeLineTimer(
455
+ createObservable(createActiveLineInfoAtom(cellId), store),
456
+ )
457
+ : debuggerOn
458
+ ? debuggerLineHighlighter(
459
+ createObservable(createDebuggerLineAtom(cellId), store),
460
+ )
461
+ : [],
434
462
  ];
435
463
  }
436
464
 
@@ -0,0 +1,165 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ type EditorState,
5
+ type Extension,
6
+ RangeSetBuilder,
7
+ } from "@codemirror/state";
8
+ import {
9
+ Decoration,
10
+ type DecorationSet,
11
+ EditorView,
12
+ type PluginValue,
13
+ ViewPlugin,
14
+ type ViewUpdate,
15
+ WidgetType,
16
+ } from "@codemirror/view";
17
+ import type { Observable } from "@/core/state/observable";
18
+ import { Logger } from "@/utils/Logger";
19
+ import { formatElapsedTime } from "@/utils/time";
20
+
21
+ /** The line a cell is currently executing and when execution first hit it. */
22
+ export interface ActiveLineInfo {
23
+ line: number;
24
+ startedAtMs: number;
25
+ }
26
+
27
+ // Only show the timer once a line has been busy this long, so short-lived
28
+ // lines never flash a pill.
29
+ const SHOW_TIMER_AFTER_MS = 500;
30
+ const TICK_INTERVAL_MS = 250;
31
+
32
+ /**
33
+ * End-of-line elapsed-time pill for the currently executing line.
34
+ *
35
+ * Ticking is pure DOM mutation on the widget's own interval; no editor
36
+ * transactions are dispatched while the timer counts.
37
+ */
38
+ class LineTimerWidget extends WidgetType {
39
+ // Track intervals per DOM node: CodeMirror may reuse the node across
40
+ // decoration remaps (`eq` below), and `destroy` only receives the node.
41
+ private static readonly intervals = new WeakMap<HTMLElement, number>();
42
+
43
+ private readonly startedAtMs: number;
44
+
45
+ constructor(startedAtMs: number) {
46
+ super();
47
+ this.startedAtMs = startedAtMs;
48
+ }
49
+
50
+ override eq(other: LineTimerWidget): boolean {
51
+ return other.startedAtMs === this.startedAtMs;
52
+ }
53
+
54
+ override toDOM(): HTMLElement {
55
+ const el = document.createElement("span");
56
+ el.className = "cm-line-timer";
57
+ el.setAttribute("aria-hidden", "true");
58
+ const tick = () => {
59
+ const elapsed = Date.now() - this.startedAtMs;
60
+ el.textContent =
61
+ elapsed >= SHOW_TIMER_AFTER_MS ? formatElapsedTime(elapsed) : "";
62
+ };
63
+ tick();
64
+ LineTimerWidget.intervals.set(
65
+ el,
66
+ window.setInterval(tick, TICK_INTERVAL_MS),
67
+ );
68
+ return el;
69
+ }
70
+
71
+ override destroy(dom: HTMLElement): void {
72
+ const interval = LineTimerWidget.intervals.get(dom);
73
+ if (interval !== undefined) {
74
+ window.clearInterval(interval);
75
+ LineTimerWidget.intervals.delete(dom);
76
+ }
77
+ }
78
+
79
+ override ignoreEvent(): boolean {
80
+ return true;
81
+ }
82
+ }
83
+
84
+ function createTimingDecorations(
85
+ state: EditorState,
86
+ info: ActiveLineInfo | null,
87
+ ): DecorationSet {
88
+ if (info === null || info.line < 1 || info.line > state.doc.lines) {
89
+ return Decoration.none;
90
+ }
91
+ const builder = new RangeSetBuilder<Decoration>();
92
+ try {
93
+ const line = state.doc.line(info.line);
94
+ builder.add(
95
+ line.from,
96
+ line.from,
97
+ Decoration.line({ class: "cm-timing-current-line" }),
98
+ );
99
+ builder.add(
100
+ line.to,
101
+ line.to,
102
+ Decoration.widget({
103
+ widget: new LineTimerWidget(info.startedAtMs),
104
+ side: 1,
105
+ }),
106
+ );
107
+ } catch (error) {
108
+ Logger.debug("Invalid timing line", { error });
109
+ }
110
+ return builder.finish();
111
+ }
112
+
113
+ class ActiveLineTimer implements PluginValue {
114
+ private unsubscribe: () => void;
115
+ decorations: DecorationSet;
116
+
117
+ constructor(
118
+ view: EditorView,
119
+ infoObservable: Observable<ActiveLineInfo | null>,
120
+ ) {
121
+ this.decorations = createTimingDecorations(
122
+ view.state,
123
+ infoObservable.get(),
124
+ );
125
+ this.unsubscribe = infoObservable.sub((info) => {
126
+ this.decorations = createTimingDecorations(view.state, info);
127
+ // Force a re-render; the decoration set changed out of band.
128
+ view.dispatch({ userEvent: "marimo.timing-line-update" });
129
+ });
130
+ }
131
+
132
+ update(update: ViewUpdate) {
133
+ this.decorations = this.decorations.map(update.changes);
134
+ }
135
+
136
+ destroy() {
137
+ this.unsubscribe();
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Highlight the line a cell's frame watcher is currently executing in green,
143
+ * with a dim elapsed-time pill at the end of the line once it has been busy
144
+ * for a while.
145
+ */
146
+ export function activeLineTimer(
147
+ infoObservable: Observable<ActiveLineInfo | null>,
148
+ ): Extension {
149
+ return [
150
+ ViewPlugin.define((view) => new ActiveLineTimer(view, infoObservable), {
151
+ decorations: (plugin) => plugin.decorations,
152
+ }),
153
+ EditorView.theme({
154
+ ".cm-timing-current-line": {
155
+ backgroundColor: "color-mix(in srgb, var(--grass-4) 50%, transparent)",
156
+ },
157
+ ".cm-line-timer": {
158
+ color: "var(--grass-11)",
159
+ fontSize: "0.85em",
160
+ marginLeft: "1.5ch",
161
+ pointerEvents: "none",
162
+ },
163
+ }),
164
+ ];
165
+ }
@@ -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": {