@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
@@ -9,11 +9,19 @@ import { MultiMap } from "@/utils/multi-map";
9
9
  import type { TypedString } from "@/utils/typed";
10
10
 
11
11
  /**
12
- * Unique identifier for a context item in the format "type:id"
12
+ * Unique identifier for a context item in the format "type://id"
13
13
  * e.g., "variable://my_var", "data://users", "file://config.py"
14
14
  */
15
15
  export type ContextLocatorId = TypedString<"ContextLocatorId">;
16
16
 
17
+ function parseContextType(uri: ContextLocatorId): string | undefined {
18
+ const separator = uri.indexOf("://");
19
+ if (separator === -1) {
20
+ return undefined;
21
+ }
22
+ return uri.slice(0, separator);
23
+ }
24
+
17
25
  /**
18
26
  * Base interface for context items that can be mentioned in AI prompts
19
27
  */
@@ -142,40 +150,80 @@ export class AIContextRegistry<T extends AIContextItem> {
142
150
  );
143
151
  }
144
152
 
153
+ private findProviderForUri(
154
+ uri: ContextLocatorId,
155
+ ): AIContextProvider | undefined {
156
+ const type = parseContextType(uri);
157
+ if (!type) {
158
+ return undefined;
159
+ }
160
+ return [...this.providers].find(
161
+ (provider) =>
162
+ provider.contextType === type &&
163
+ provider.getItems().some((item) => item.uri === uri),
164
+ );
165
+ }
166
+
145
167
  /**
146
- * Get context information for mentioned items
168
+ * Resolve only the requested context items, querying each matching provider
147
169
  */
148
- getContextInfo(contextIds: ContextLocatorId[]): T[] {
149
- const contextInfo: T[] = [];
150
- const allItems = new Map<ContextLocatorId, T>(
151
- this.getAllItems().map((item) => [item.uri as ContextLocatorId, item]),
152
- );
170
+ resolveItems(contextIds: ContextLocatorId[]): T[] {
171
+ if (contextIds.length === 0) {
172
+ return [];
173
+ }
174
+
175
+ const idsByType = new MultiMap<string, ContextLocatorId>();
176
+ for (const contextId of contextIds) {
177
+ const type = parseContextType(contextId);
178
+ if (type) {
179
+ idsByType.add(type, contextId);
180
+ }
181
+ }
182
+
183
+ const itemsById = new Map<ContextLocatorId, T>();
184
+ for (const [type, ids] of idsByType.entries()) {
185
+ const providers = [...this.providers].filter(
186
+ (provider) => provider.contextType === type,
187
+ );
188
+ for (const provider of providers) {
189
+ const itemsByUri = new Map<ContextLocatorId, T>(
190
+ provider
191
+ .getItems()
192
+ .map((item) => [item.uri as ContextLocatorId, item as T]),
193
+ );
194
+ for (const id of ids) {
195
+ const item = itemsByUri.get(id);
196
+ if (item) {
197
+ itemsById.set(id, item);
198
+ }
199
+ }
200
+ }
201
+ }
153
202
 
203
+ // Preserve the order in which the ids were requested, so formatted context
204
+ // matches the order the user mentioned them in the prompt.
205
+ const results: T[] = [];
154
206
  for (const contextId of contextIds) {
155
- const item = allItems.get(contextId);
207
+ const item = itemsById.get(contextId);
156
208
  if (item) {
157
- contextInfo.push(item);
209
+ results.push(item);
158
210
  }
159
211
  }
212
+ return results;
213
+ }
160
214
 
161
- return contextInfo;
215
+ /**
216
+ * Get context information for mentioned items
217
+ */
218
+ getContextInfo(contextIds: ContextLocatorId[]): T[] {
219
+ return this.resolveItems(contextIds);
162
220
  }
163
221
 
164
222
  /**
165
223
  * Format context for AI prompt inclusion
166
224
  */
167
225
  formatContextForAI(contextIds: ContextLocatorId[]): string {
168
- const allItems = new Map<ContextLocatorId, T>(
169
- this.getAllItems().map((item) => [item.uri as ContextLocatorId, item]),
170
- );
171
-
172
- const contextInfo: T[] = [];
173
- for (const contextId of contextIds) {
174
- const item = allItems.get(contextId);
175
- if (item) {
176
- contextInfo.push(item);
177
- }
178
- }
226
+ const contextInfo = this.resolveItems(contextIds);
179
227
 
180
228
  if (contextInfo.length === 0) {
181
229
  return "";
@@ -183,7 +231,7 @@ export class AIContextRegistry<T extends AIContextItem> {
183
231
 
184
232
  return contextInfo
185
233
  .map((item) => {
186
- const provider = this.getProvider(item.type);
234
+ const provider = this.findProviderForUri(item.uri as ContextLocatorId);
187
235
  return provider?.formatContext(item) || "";
188
236
  })
189
237
  .join("\n\n");
@@ -195,36 +243,22 @@ export class AIContextRegistry<T extends AIContextItem> {
195
243
  async getAttachmentsForContext(
196
244
  contextIds: ContextLocatorId[],
197
245
  ): Promise<FileUIPart[]> {
198
- const allItems = new Map<ContextLocatorId, T>(
199
- this.getAllItems().map((item) => [item.uri as ContextLocatorId, item]),
200
- );
201
-
202
- const contextInfo: T[] = [];
203
- for (const contextId of contextIds) {
204
- const item = allItems.get(contextId);
205
- if (item) {
206
- contextInfo.push(item);
207
- }
208
- }
246
+ const contextInfo = this.resolveItems(contextIds);
209
247
 
210
248
  if (contextInfo.length === 0) {
211
249
  return [];
212
250
  }
213
251
 
214
- // Group items by provider type to batch attachment requests
215
- const itemsByProvider = new MultiMap<string, T>();
252
+ const itemsByProvider = new MultiMap<AIContextProvider, T>();
216
253
  for (const item of contextInfo) {
217
- const providerType = item.type;
218
- itemsByProvider.add(providerType, item);
254
+ const provider = this.findProviderForUri(item.uri as ContextLocatorId);
255
+ if (provider) {
256
+ itemsByProvider.add(provider, item);
257
+ }
219
258
  }
220
259
 
221
- // Collect attachments from all providers
222
260
  const attachmentPromises = [...itemsByProvider.entries()].map(
223
- async ([providerType, items]) => {
224
- const provider = this.getProvider(providerType);
225
- if (!provider) {
226
- return [];
227
- }
261
+ async ([provider, items]) => {
228
262
  try {
229
263
  return await provider.getAttachments(items);
230
264
  } catch (error) {
@@ -21,6 +21,17 @@ export interface AiCompletionCell {
21
21
 
22
22
  export const aiCompletionCellAtom = atom<AiCompletionCell | null>(null);
23
23
 
24
+ /**
25
+ * A prompt queued to be delivered to the AI assistant panel.
26
+ * The active panel consumes this on mount/when ready, then clears it.
27
+ */
28
+ export interface PendingAiPrompt {
29
+ prompt: string;
30
+ submit: boolean;
31
+ }
32
+
33
+ export const pendingAiPromptAtom = atom<PendingAiPrompt | null>(null);
34
+
24
35
  const INCLUDE_OTHER_CELLS_KEY = "marimo:ai:includeOtherCells";
25
36
  export const includeOtherCellsAtom = atomWithStorage<boolean>(
26
37
  INCLUDE_OTHER_CELLS_KEY,
@@ -218,3 +218,42 @@ describe("transitionCell serialization", () => {
218
218
  );
219
219
  });
220
220
  });
221
+
222
+ describe("transitionCell stdin resolution", () => {
223
+ function stdinOutput(): OutputMessage {
224
+ return {
225
+ channel: "stdin",
226
+ mimetype: "text/plain",
227
+ data: "(Pdb) ",
228
+ timestamp: Date.now() as Seconds,
229
+ } as OutputMessage;
230
+ }
231
+
232
+ it("resolves a dangling pdb prompt when the cell goes idle", () => {
233
+ // A paused-pdb cell shows an unanswered stdin prompt (the debugger box);
234
+ // once it finishes (e.g. after quitting with `q`), the box is removed.
235
+ const cell = createCellRuntimeState({
236
+ status: "running",
237
+ debuggerActive: true,
238
+ consoleOutputs: [stdinOutput()],
239
+ });
240
+ const next = transitionCell(cell, {
241
+ cell_id: "cell-1",
242
+ status: "idle",
243
+ } as CellMessage);
244
+ expect(next.debuggerActive).toBe(false);
245
+ expect(next.consoleOutputs[0].response).toBe("");
246
+ });
247
+
248
+ it("leaves an unanswered prompt interactive while still running", () => {
249
+ const cell = createCellRuntimeState({
250
+ status: "running",
251
+ consoleOutputs: [stdinOutput()],
252
+ });
253
+ const next = transitionCell(cell, {
254
+ cell_id: "cell-1",
255
+ status: null,
256
+ } as CellMessage);
257
+ expect(next.consoleOutputs[0].response).toBeUndefined();
258
+ });
259
+ });
@@ -0,0 +1,46 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { getReadonlyCodeDisplay } from "../readonly-code-display";
5
+
6
+ describe("getReadonlyCodeDisplay", () => {
7
+ it("unwraps SQL cells to their inner query and marks them as sql", () => {
8
+ const result = getReadonlyCodeDisplay(
9
+ 'my_table = mo.sql("""SELECT 1 AS id""")',
10
+ );
11
+ expect(result.language).toBe("sql");
12
+ expect(result.code).toBe("SELECT 1 AS id");
13
+ });
14
+
15
+ it("unwraps f-string SQL cells", () => {
16
+ const result = getReadonlyCodeDisplay(
17
+ 'my_table = mo.sql(f"""SELECT 1 AS id""")',
18
+ );
19
+ expect(result.language).toBe("sql");
20
+ expect(result.code).toBe("SELECT 1 AS id");
21
+ });
22
+
23
+ it("leaves plain Python cells untouched", () => {
24
+ const code = "x = 1 + 2\nprint(x)";
25
+ const result = getReadonlyCodeDisplay(code);
26
+ expect(result.language).toBe("python");
27
+ expect(result.code).toBe(code);
28
+ });
29
+
30
+ it("unwraps markdown cells to their inner content", () => {
31
+ const result = getReadonlyCodeDisplay('mo.md("""## Heading""")');
32
+ expect(result.language).toBe("markdown");
33
+ expect(result.code).toBe("## Heading");
34
+ });
35
+
36
+ it("treats empty or whitespace-only code as python", () => {
37
+ expect(getReadonlyCodeDisplay("")).toEqual({
38
+ code: "",
39
+ language: "python",
40
+ });
41
+ expect(getReadonlyCodeDisplay(" \n\t ")).toEqual({
42
+ code: " \n\t ",
43
+ language: "python",
44
+ });
45
+ });
46
+ });
@@ -118,9 +118,11 @@ export function transitionCell(
118
118
  // Coalesce console outputs, which are streamed during execution.
119
119
  let consoleOutputs = cell.consoleOutputs;
120
120
 
121
- // If interrupted on the incoming message,
122
- // remove the debugger and resolve all stdin for previous console outputs
123
- if (didInterruptFromThisMessage) {
121
+ // When the cell is interrupted or finishes (idle), there is no live
122
+ // stdin/pdb session, so resolve any dangling stdin prompt. This removes the
123
+ // debugger box — e.g. after quitting pdb with `q`, which stops the cell
124
+ // (idle) rather than interrupting it.
125
+ if (didInterruptFromThisMessage || message.status === "idle") {
124
126
  nextCell.debuggerActive = false;
125
127
  consoleOutputs = consoleOutputs.map((output) => {
126
128
  if (output.channel === "stdin") {
@@ -0,0 +1,35 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { MarkdownParser, SQLParser } from "@marimo-team/smart-cells";
4
+ import type { LanguageAdapterType } from "@/core/codemirror/language/types";
5
+
6
+ export interface ReadonlyCodeDisplay {
7
+ code: string;
8
+ language: LanguageAdapterType;
9
+ }
10
+
11
+ const markdownParser = new MarkdownParser();
12
+ const sqlParser = new SQLParser();
13
+
14
+ /**
15
+ * Unwrap SQL and markdown cells so read-only views show inner content with the
16
+ * correct syntax highlighting instead of the raw Python wrapper.
17
+ */
18
+ export function getReadonlyCodeDisplay(code: string): ReadonlyCodeDisplay {
19
+ const trimmed = code.trim();
20
+ if (!trimmed) {
21
+ return { code, language: "python" };
22
+ }
23
+
24
+ if (markdownParser.isSupported(trimmed)) {
25
+ return {
26
+ code: markdownParser.transformIn(trimmed).code,
27
+ language: "markdown",
28
+ };
29
+ }
30
+
31
+ if (sqlParser.isSupported(trimmed)) {
32
+ return { code: sqlParser.transformIn(trimmed).code, language: "sql" };
33
+ }
34
+ return { code, language: "python" };
35
+ }
@@ -1,7 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { EditorState, type Extension } from "@codemirror/state";
4
- import { keymap } from "@codemirror/view";
4
+ import { EditorView, keymap } from "@codemirror/view";
5
5
  import { describe, expect, test, vi } from "vitest";
6
6
  import { cellId } from "@/__tests__/branded";
7
7
  import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
@@ -163,6 +163,38 @@ test("auto_close_pairs: false removes closeBrackets keymaps", () => {
163
163
  expect(hasBracketPairHandler(withoutAutoClose)).toBe(false);
164
164
  });
165
165
 
166
+ test("go to definition falls through to lower-priority LSP keymap", () => {
167
+ const lspGoToDefinition = vi.fn(() => true);
168
+ const goToDefinitionKey = getOpts().hotkeys.getHotkey(
169
+ "cell.goToDefinition",
170
+ ).key;
171
+
172
+ const view = new EditorView({
173
+ state: EditorState.create({
174
+ doc: "parser.add_argument('--foo')",
175
+ selection: { anchor: "parser.".length },
176
+ extensions: [
177
+ ...setup(),
178
+ keymap.of([{ key: goToDefinitionKey, run: lspGoToDefinition }]),
179
+ ],
180
+ }),
181
+ parent: document.body,
182
+ });
183
+
184
+ try {
185
+ const event = new KeyboardEvent("keydown", {
186
+ key: goToDefinitionKey,
187
+ bubbles: true,
188
+ cancelable: true,
189
+ });
190
+ view.contentDOM.dispatchEvent(event);
191
+
192
+ expect(lspGoToDefinition).toHaveBeenCalledOnce();
193
+ } finally {
194
+ view.destroy();
195
+ }
196
+ });
197
+
166
198
  test("placeholder adds another extension", () => {
167
199
  const opts = getOpts();
168
200
  const withAI = new PythonLanguageAdapter()
@@ -11,6 +11,7 @@ import {
11
11
  resourceDecorations,
12
12
  resourceInputFilter,
13
13
  resourcesField,
14
+ resourceSync,
14
15
  resourceTheme,
15
16
  } from "@marimo-team/codemirror-mcp";
16
17
  import {
@@ -38,25 +39,28 @@ const NONE_RESOURCE_FORMAT_COMPLETION = {
38
39
  },
39
40
  };
40
41
 
42
+ function getRegistryResources(store: JotaiStore): Resource[] {
43
+ const registry = getAIContextRegistry(store);
44
+ const resources = registry.getAllItems();
45
+ if (resources.length === 0) {
46
+ return NONE_RESOURCE;
47
+ }
48
+ return resources;
49
+ }
50
+
41
51
  export function resourceExtension(opts: {
42
52
  language: Language;
43
53
  store: JotaiStore;
44
54
  onAddFiles?: (files: File[]) => void;
45
55
  }): Extension[] {
46
56
  const { language, store, onAddFiles } = opts;
57
+ const getResources = () => getRegistryResources(store);
47
58
 
48
59
  return [
49
60
  language.data.of({
50
61
  // Resource completion for static resources (variables, tables, etc.)
51
62
  autocomplete: resourceCompletion(
52
- async (): Promise<Resource[]> => {
53
- const registry = getAIContextRegistry(store);
54
- const resources = registry.getAllItems();
55
- if (resources.length === 0) {
56
- return NONE_RESOURCE;
57
- }
58
- return resources;
59
- },
63
+ async (): Promise<Resource[]> => getResources(),
60
64
  (resource) => {
61
65
  if (resource.type === NONE_RESOURCE_TYPE) {
62
66
  return NONE_RESOURCE_FORMAT_COMPLETION;
@@ -92,9 +96,10 @@ export function resourceExtension(opts: {
92
96
  : []),
93
97
  resourceDecorations,
94
98
  resourceInputFilter,
99
+ // Resolve @-mention chips for programmatic prefills.
100
+ resourceSync(getResources, { logger: Logger }),
95
101
  resourcesField.init(() => {
96
- const registry = getAIContextRegistry(store);
97
- const resources = registry.getAllItems();
102
+ const resources = getRegistryResources(store);
98
103
  return new Map(resources.map((resource) => [resource.uri, resource]));
99
104
  }),
100
105
  resourceTheme,
@@ -0,0 +1,185 @@
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, vi } from "vitest";
7
+ import { cellId } from "@/__tests__/branded";
8
+ import type { CellId } from "@/core/cells/ids";
9
+ import { createMockObservable } from "@/core/state/__mocks__/mocks";
10
+ import type { Observable } from "@/core/state/observable";
11
+ import {
12
+ breakpointGutter,
13
+ debuggerLineHighlighter,
14
+ } from "../debugger-decorations";
15
+
16
+ vi.mock("../debugger-state", () => ({
17
+ toggleBreakpoint: vi.fn(),
18
+ }));
19
+
20
+ import { toggleBreakpoint } from "../debugger-state";
21
+
22
+ const CODE = `def my_function():
23
+ x = 1
24
+ y = 2
25
+ return x + y
26
+
27
+ result = my_function()`;
28
+
29
+ function createLineHighlighterEditor(
30
+ lineObservable: Observable<number | null>,
31
+ ) {
32
+ const state = EditorState.create({
33
+ doc: CODE,
34
+ extensions: [python(), debuggerLineHighlighter(lineObservable)],
35
+ });
36
+ return new EditorView({ state, parent: document.body });
37
+ }
38
+
39
+ function createBreakpointGutterEditor(
40
+ cid: CellId,
41
+ breakpointsObservable: Observable<ReadonlySet<number>>,
42
+ ) {
43
+ const state = EditorState.create({
44
+ doc: CODE,
45
+ extensions: [python(), breakpointGutter(cid, breakpointsObservable)],
46
+ });
47
+ return new EditorView({ state, parent: document.body });
48
+ }
49
+
50
+ /** Flush the microtask queue and apply the resulting state changes to `view`. */
51
+ async function flush(view: EditorView): Promise<void> {
52
+ await Promise.resolve();
53
+ view.dispatch({});
54
+ }
55
+
56
+ /**
57
+ * Count rendered breakpoint markers, excluding the always-present hidden
58
+ * spacer element (`initialSpacer`) that isn't an actual breakpoint.
59
+ */
60
+ function visibleMarkerCount(view: EditorView): number {
61
+ const gutterElement = view.dom.querySelector(".cm-breakpoint-gutter");
62
+ if (!gutterElement) {
63
+ return 0;
64
+ }
65
+ return gutterElement.querySelectorAll(
66
+ '.cm-gutterElement:not([style*="visibility: hidden"]) .cm-breakpoint-marker',
67
+ ).length;
68
+ }
69
+
70
+ describe("debugger-decorations", () => {
71
+ let view: EditorView | null = null;
72
+
73
+ afterEach(() => {
74
+ if (view) {
75
+ view.destroy();
76
+ view = null;
77
+ }
78
+ document.body.innerHTML = "";
79
+ vi.clearAllMocks();
80
+ });
81
+
82
+ describe("debuggerLineHighlighter", () => {
83
+ it("renders no highlight when the observable starts null", () => {
84
+ const lineObservable = createMockObservable<number | null>(null);
85
+ view = createLineHighlighterEditor(lineObservable);
86
+
87
+ expect(view.dom.querySelector(".cm-debugger-current-line")).toBeNull();
88
+ });
89
+
90
+ it("highlights the line reported by the observable", () => {
91
+ const lineObservable = createMockObservable<number | null>(null);
92
+ view = createLineHighlighterEditor(lineObservable);
93
+
94
+ lineObservable.set(2);
95
+ view.dispatch({});
96
+
97
+ expect(
98
+ view.dom.querySelector(".cm-debugger-current-line"),
99
+ ).not.toBeNull();
100
+ });
101
+
102
+ it("clears the highlight when the line is set back to null", () => {
103
+ const lineObservable = createMockObservable<number | null>(2);
104
+ view = createLineHighlighterEditor(lineObservable);
105
+ view.dispatch({});
106
+ expect(
107
+ view.dom.querySelector(".cm-debugger-current-line"),
108
+ ).not.toBeNull();
109
+
110
+ lineObservable.set(null);
111
+ view.dispatch({});
112
+
113
+ expect(view.dom.querySelector(".cm-debugger-current-line")).toBeNull();
114
+ });
115
+
116
+ it("does not throw for an out-of-range line", () => {
117
+ const lineObservable = createMockObservable<number | null>(null);
118
+ view = createLineHighlighterEditor(lineObservable);
119
+
120
+ expect(() => {
121
+ lineObservable.set(9999);
122
+ view?.dispatch({});
123
+ }).not.toThrow();
124
+ expect(view.dom.querySelector(".cm-debugger-current-line")).toBeNull();
125
+ });
126
+ });
127
+
128
+ describe("breakpointGutter", () => {
129
+ it("renders no markers when there are no breakpoints", () => {
130
+ const cid = cellId("cell1");
131
+ const breakpointsObservable = createMockObservable<ReadonlySet<number>>(
132
+ new Set(),
133
+ );
134
+ view = createBreakpointGutterEditor(cid, breakpointsObservable);
135
+
136
+ expect(visibleMarkerCount(view)).toBe(0);
137
+ });
138
+
139
+ it("syncs pre-existing breakpoints on mount", async () => {
140
+ const cid = cellId("cell1");
141
+ const breakpointsObservable = createMockObservable<ReadonlySet<number>>(
142
+ new Set([2]),
143
+ );
144
+ view = createBreakpointGutterEditor(cid, breakpointsObservable);
145
+ await flush(view); // initial sync is deferred to a microtask
146
+
147
+ expect(visibleMarkerCount(view)).toBe(1);
148
+ });
149
+
150
+ it("adds a marker when the observable reports a new breakpoint", () => {
151
+ const cid = cellId("cell1");
152
+ const breakpointsObservable = createMockObservable<ReadonlySet<number>>(
153
+ new Set(),
154
+ );
155
+ view = createBreakpointGutterEditor(cid, breakpointsObservable);
156
+
157
+ breakpointsObservable.set(new Set([1, 3]));
158
+ view.dispatch({});
159
+
160
+ expect(visibleMarkerCount(view)).toBe(2);
161
+ });
162
+
163
+ it("toggles a breakpoint when an existing marker is clicked", async () => {
164
+ const cid = cellId("cell1");
165
+ // A single breakpoint on line 1 renders exactly one non-spacer
166
+ // gutter element, so the click lands unambiguously on that line.
167
+ const breakpointsObservable = createMockObservable<ReadonlySet<number>>(
168
+ new Set([1]),
169
+ );
170
+ view = createBreakpointGutterEditor(cid, breakpointsObservable);
171
+ await flush(view);
172
+
173
+ const gutterElement = view.dom.querySelector(".cm-breakpoint-gutter");
174
+ const lineElement = gutterElement?.querySelector(
175
+ '.cm-gutterElement:not([style*="visibility: hidden"])',
176
+ );
177
+ expect(lineElement).not.toBeNull();
178
+ lineElement?.dispatchEvent(
179
+ new MouseEvent("mousedown", { bubbles: true }),
180
+ );
181
+
182
+ expect(toggleBreakpoint).toHaveBeenCalledWith(cid, 1);
183
+ });
184
+ });
185
+ });