@marimo-team/frontend 0.23.14-dev19 → 0.23.14-dev21

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 (90) hide show
  1. package/dist/assets/{CellStatus-B3d9WY2x.js → CellStatus-CSZHI8sz.js} +1 -1
  2. package/dist/assets/{JsonOutput-C3c9GGaV.js → JsonOutput-CYIo9GSP.js} +12 -12
  3. package/dist/assets/MarimoErrorOutput-BokuSnpU.js +11 -0
  4. package/dist/assets/{RenderHTML-v15QgtRg.js → RenderHTML-B9mw7TD1.js} +1 -1
  5. package/dist/assets/{RunButton-Bz4eaTJM.js → RunButton-CX_nNY62.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-wau9Yzcj.js → add-cell-with-ai-DwzlK27e.js} +47 -34
  7. package/dist/assets/{add-connection-dialog-_KCPEn7U.js → add-connection-dialog-fmiSisf0.js} +6 -6
  8. package/dist/assets/{agent-panel-Bn9AgwJH.js → agent-panel-BDO44WWH.js} +5 -5
  9. package/dist/assets/{ai-model-dropdown-BeZrAec-.js → ai-model-dropdown-BP8gjnOy.js} +3 -3
  10. package/dist/assets/{app-config-button-BG9ONWMU.js → app-config-button-CVpG64c2.js} +1 -1
  11. package/dist/assets/{cell-editor-24ykqaHj.js → cell-editor-Bbir67_U.js} +10 -10
  12. package/dist/assets/cell-link-CBRggkzf.js +1 -0
  13. package/dist/assets/{cells-DENsmH9t.js → cells-RJTD-euN.js} +44 -44
  14. package/dist/assets/{chat-display-CcL_dydg.js → chat-display-17k5Uy0b.js} +1 -1
  15. package/dist/assets/{chat-panel-CA-2-S1C.js → chat-panel-CGwZggen.js} +2 -2
  16. package/dist/assets/{chat-ui-DQeA0_b5.js → chat-ui-BrSyHh93.js} +1 -1
  17. package/dist/assets/{column-preview-BPgYXdDd.js → column-preview-dW7xtQ7S.js} +1 -1
  18. package/dist/assets/{command-palette-WlkKgdrI.js → command-palette-DtKhXIMn.js} +1 -1
  19. package/dist/assets/{common-DdBQICbD.js → common-DHzo8K8C.js} +1 -1
  20. package/dist/assets/components-ChBJEsmj.js +1 -0
  21. package/dist/assets/datasource-RP7m6Xrh.js +3 -0
  22. package/dist/assets/{dependency-graph-panel-nYHPHFU4.js → dependency-graph-panel-CjdEazj5.js} +4 -4
  23. package/dist/assets/{documentation-panel-Dw9tYBNf.js → documentation-panel-BcUF41gA.js} +1 -1
  24. package/dist/assets/{download-4NoBestd.js → download-CJ6tdZ-1.js} +4 -4
  25. package/dist/assets/{edit-page-CAfBRrHn.js → edit-page-lo6LC4Ie.js} +6 -6
  26. package/dist/assets/{error-panel-Ddr_mbqR.js → error-panel-D_1OmgVJ.js} +1 -1
  27. package/dist/assets/{file-explorer-panel-DhpN8_pR.js → file-explorer-panel-CG5na8ox.js} +3 -3
  28. package/dist/assets/{file-icons-BFLcd4J7.js → file-icons-CGJ6uWXm.js} +1 -1
  29. package/dist/assets/{file-name-input-x53prQQ6.js → file-name-input-DC8-M5r1.js} +1 -1
  30. package/dist/assets/{floating-outline--OlHG_yX.js → floating-outline-BOkj3eC_.js} +1 -1
  31. package/dist/assets/{focus-CZUEQpxX.js → focus-BViZMdHR.js} +1 -1
  32. package/dist/assets/{form-CJ3pB1ow.js → form-BbqpGicG.js} +2 -2
  33. package/dist/assets/{home-page-pH8-XovY.js → home-page-CU6li-ts.js} +2 -2
  34. package/dist/assets/{hooks-BqjZMbeu.js → hooks-CPpze3O4.js} +1 -1
  35. package/dist/assets/{html-to-image-B6eCuh-2.js → html-to-image-B8ja7wwq.js} +2 -2
  36. package/dist/assets/{index-CNmQbGXF.js → index-BcxGcwkJ.js} +6 -6
  37. package/dist/assets/kiosk-mode-BP35ALfP.js +1 -0
  38. package/dist/assets/{layout-DLJQyOOv.js → layout-BSHt96Sb.js} +5 -5
  39. package/dist/assets/{logs-panel-BBGNeMzu.js → logs-panel-CbZLi2XS.js} +1 -1
  40. package/dist/assets/{markdown-renderer-CcLl4-oB.js → markdown-renderer-jghC8PFL.js} +2 -2
  41. package/dist/assets/{name-cell-input-CNq9BTL7.js → name-cell-input-CN4MRZOm.js} +1 -1
  42. package/dist/assets/{outline-panel-CnGBTEs0.js → outline-panel-CvB3hUJ2.js} +1 -1
  43. package/dist/assets/{packages-panel-B17awHIF.js → packages-panel-BGgZLApn.js} +1 -1
  44. package/dist/assets/{panels-P8wFgP-8.js → panels-DCWntQim.js} +1 -1
  45. package/dist/assets/process-output-xDK6-i-G.js +1 -0
  46. package/dist/assets/{radio-group-B8OldelF.js → radio-group-DFkF8CeQ.js} +1 -1
  47. package/dist/assets/{readonly-code-display-bvftv2Nk.js → readonly-code-display-BHx0vyug.js} +1 -1
  48. package/dist/assets/{readonly-python-code-CoFR55Yt.js → readonly-python-code-Cla-uJpn.js} +1 -1
  49. package/dist/assets/{reveal-component-CfBFUsP0.js → reveal-component-Bvxyfpeh.js} +15 -15
  50. package/dist/assets/{run-page-CFoRuGGY.js → run-page-Cxc4jqCp.js} +1 -1
  51. package/dist/assets/scratchpad-panel-Bp4sg_00.js +1 -0
  52. package/dist/assets/{session-panel-D5OQ5JSm.js → session-panel-WkryRIGA.js} +1 -1
  53. package/dist/assets/{snippets-panel-DvfXacKM.js → snippets-panel-BcDwLUNv.js} +1 -1
  54. package/dist/assets/{state-CQn0dE8X.js → state-CXzKtmF5.js} +2 -2
  55. package/dist/assets/{state-LM4pQmYL.js → state-DHqaTLr2.js} +1 -1
  56. package/dist/assets/{textarea-Tswrj-fB.js → textarea-B5Bp08T3.js} +1 -1
  57. package/dist/assets/{tracing-BMvzhCBv.js → tracing-clJBeQm6.js} +1 -1
  58. package/dist/assets/{tracing-panel-DJF6cCQK.js → tracing-panel-7j9NY9lO.js} +2 -2
  59. package/dist/assets/{tree-actions-Bnmh7YGo.js → tree-actions-DLySKXUq.js} +1 -1
  60. package/dist/assets/useCellActionButton-CvQRbg1P.js +1 -0
  61. package/dist/assets/{useDeleteCell-DwNTIWuB.js → useDeleteCell-Sj06CzJn.js} +1 -1
  62. package/dist/assets/{useDependencyPanelTab-BH_Eu2Kq.js → useDependencyPanelTab-BTgQmu1R.js} +1 -1
  63. package/dist/assets/{useNotebookActions-BmO8IOgN.js → useNotebookActions-9PPlpXk8.js} +1 -1
  64. package/dist/assets/useRunCells-D5aAUcAa.js +1 -0
  65. package/dist/assets/useSplitCell-CGE6jkuF.js +1 -0
  66. package/dist/index.html +22 -22
  67. package/package.json +2 -2
  68. package/src/components/editor/errors/__tests__/auto-fix.test.ts +11 -107
  69. package/src/components/editor/errors/auto-fix.tsx +11 -31
  70. package/src/components/editor/output/MarimoTracebackOutput.tsx +2 -2
  71. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  72. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  73. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  74. package/src/core/ai/context/providers/datasource.ts +27 -2
  75. package/src/core/ai/context/providers/error.ts +226 -36
  76. package/src/core/ai/context/registry.ts +77 -43
  77. package/src/core/codemirror/ai/resources.ts +15 -10
  78. package/src/core/errors/errors.ts +2 -1
  79. package/src/core/mime.ts +11 -4
  80. package/dist/assets/MarimoErrorOutput-D07aAGkQ.js +0 -12
  81. package/dist/assets/cell-link-exzcyUhT.js +0 -1
  82. package/dist/assets/components-DinLkTiR.js +0 -1
  83. package/dist/assets/datasource-BCOYpQ69.js +0 -3
  84. package/dist/assets/kiosk-mode-BBfHxqEx.js +0 -1
  85. package/dist/assets/process-output-BGcDSQBk.js +0 -1
  86. package/dist/assets/scratchpad-panel-DdKvJKy4.js +0 -1
  87. package/dist/assets/useCellActionButton-ibDHNca0.js +0 -1
  88. package/dist/assets/useRunCells-CTpvxsqp.js +0 -1
  89. package/dist/assets/useSplitCell-CFO4Cg5B.js +0 -1
  90. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
@@ -57,7 +57,7 @@ describe("ErrorContextProvider", () => {
57
57
 
58
58
  const items = provider.getItems();
59
59
 
60
- expect(items).toHaveLength(1);
60
+ expect(items).toHaveLength(3);
61
61
  expect(items[0]).toMatchObject({
62
62
  name: "Errors",
63
63
  type: "error",
@@ -78,6 +78,18 @@ describe("ErrorContextProvider", () => {
78
78
  ],
79
79
  },
80
80
  });
81
+ expect(items[1]).toMatchObject({
82
+ uri: `error://${cellId1}`,
83
+ name: "Error: Cell 1",
84
+ description: "Invalid syntax",
85
+ data: { type: "cell-error", error: { cellId: cellId1 } },
86
+ });
87
+ expect(items[2]).toMatchObject({
88
+ uri: `error://${cellId2}`,
89
+ name: "Error: Cell 2",
90
+ description: "Runtime error",
91
+ data: { type: "cell-error", error: { cellId: cellId2 } },
92
+ });
81
93
  });
82
94
 
83
95
  it("should handle cells without names", () => {
@@ -92,7 +104,12 @@ describe("ErrorContextProvider", () => {
92
104
  ]);
93
105
 
94
106
  const items = provider.getItems();
95
- expect(items[0].data.errors[0].cellName).toBe("");
107
+ expect(items[1].data.type).toBe("cell-error");
108
+ if (items[1].data.type === "cell-error") {
109
+ expect(items[1].data.error.cellName).toBe("cell-0");
110
+ }
111
+ expect(items[1].name).toBe("Error: cell-0");
112
+ expect(items[1].description).toBe("Invalid syntax");
96
113
  });
97
114
  });
98
115
 
@@ -119,7 +136,7 @@ describe("ErrorContextProvider", () => {
119
136
 
120
137
  expect(completion).toMatchInlineSnapshot(`
121
138
  {
122
- "apply": "@Errors",
139
+ "apply": "@error://all",
123
140
  "detail": "2 errors",
124
141
  "displayLabel": "Errors",
125
142
  "info": [Function],
@@ -170,6 +187,24 @@ describe("ErrorContextProvider", () => {
170
187
  expect(completion.detail).toBe("1 error");
171
188
  });
172
189
 
190
+ it("formats completion for a single cell error", () => {
191
+ const cellId = CellIdClass.create();
192
+
193
+ createMockNotebookWithErrors([
194
+ {
195
+ cellId,
196
+ cellName: "Cell 1",
197
+ errorData: [MockNotebook.errors.syntax("Invalid syntax")],
198
+ },
199
+ ]);
200
+
201
+ const items = provider.getItems();
202
+ const completion = provider.formatCompletion(items[1]);
203
+ expect(completion.apply).toBe(`@error://${cellId}`);
204
+ expect(completion.displayLabel).toBe("Error: Cell 1");
205
+ expect(items[1].name).toBe("Error: Cell 1");
206
+ });
207
+
173
208
  it("should handle fallback for unknown error types", () => {
174
209
  const item = {
175
210
  uri: "error://unknown",
@@ -210,7 +245,8 @@ describe("ErrorContextProvider", () => {
210
245
 
211
246
  const items = provider.getItems();
212
247
  const context = provider.formatContext(items[0]);
213
- expect(context).toMatchSnapshot("basic-error-context");
248
+ expect(context).toContain("Invalid syntax");
249
+ expect(context).toContain("Code:");
214
250
  });
215
251
 
216
252
  it("should format context for multiple error types", () => {
@@ -239,14 +275,12 @@ describe("ErrorContextProvider", () => {
239
275
  const items = provider.getItems();
240
276
  const context = provider.formatContext(items[0]);
241
277
 
242
- // Check for expected content instead of exact snapshot match due to random cell IDs
243
- expect(context).toMatchInlineSnapshot(`
244
- "<error name="Cell 1" description="Invalid syntax
245
- Runtime error"></error>
246
-
247
- <error name="Cell cell-2" description="This cell is in a cycle
248
- The variable 'variable_x' was defined by another cell"></error>"
249
- `);
278
+ expect(context).toContain("Invalid syntax");
279
+ expect(context).toContain("Runtime error");
280
+ expect(context).toContain("This cell is in a cycle");
281
+ expect(context).toContain(
282
+ "The variable 'variable_x' was defined by another cell",
283
+ );
250
284
  });
251
285
 
252
286
  it("should handle cells without names", () => {
@@ -261,8 +295,158 @@ describe("ErrorContextProvider", () => {
261
295
  ]);
262
296
 
263
297
  const items = provider.getItems();
264
- const context = provider.formatContext(items[0]);
265
- expect(context).toContain(`Cell ${cellId}`);
298
+ const context = provider.formatContext(items[1]);
299
+ expect(context).toContain(`cellId="${cellId}"`);
300
+ expect(context).toContain("Invalid syntax");
301
+ });
302
+
303
+ it("includes exception tracebacks in formatted context", () => {
304
+ const cellId = CellIdClass.create();
305
+
306
+ createMockNotebookWithErrors([
307
+ {
308
+ cellId,
309
+ cellName: "Cell 1",
310
+ errorData: [
311
+ {
312
+ type: "exception",
313
+ msg: "boom",
314
+ exception_type: "ValueError",
315
+ raising_cell: null,
316
+ traceback: "<pre>line 1\nline 2</pre>",
317
+ },
318
+ ],
319
+ },
320
+ ]);
321
+
322
+ const items = provider.getItems();
323
+ const context = provider.formatContext(items[1]);
324
+ expect(context).toContain("boom");
325
+ expect(context).toContain("line 1");
326
+ expect(context).toContain("line 2");
327
+ });
328
+
329
+ it("includes traceback-only cell outputs", () => {
330
+ const cellId = CellIdClass.create();
331
+ const notebookState = MockNotebook.notebookState({
332
+ cellData: {
333
+ [cellId]: {
334
+ name: "Cell 1",
335
+ code: "raise ValueError('boom')",
336
+ },
337
+ },
338
+ });
339
+ notebookState.cellRuntime[cellId] = {
340
+ ...notebookState.cellRuntime[cellId],
341
+ output: {
342
+ channel: "marimo-error",
343
+ data: "<pre>ValueError: boom\n line 1</pre>",
344
+ mimetype: "application/vnd.marimo+traceback",
345
+ timestamp: Date.now(),
346
+ },
347
+ };
348
+ store.set(notebookAtom, notebookState);
349
+
350
+ const items = provider.getItems();
351
+ expect(items).toHaveLength(2);
352
+ expect(items[1].description).toBe("ValueError: boom");
353
+ const context = provider.formatContext(items[1]);
354
+ expect(context).toContain("raise ValueError('boom')");
355
+ expect(context).toContain("ValueError: boom");
356
+ expect(context).toContain("line 1");
357
+ });
358
+
359
+ it("includes console tracebacks for marimo error outputs", () => {
360
+ const cellId = CellIdClass.create();
361
+ const notebookState = MockNotebook.notebookState({
362
+ cellData: {
363
+ [cellId]: {
364
+ name: "Cell 1",
365
+ code: "raise ValueError('boom')",
366
+ },
367
+ },
368
+ });
369
+ notebookState.cellRuntime[cellId] = {
370
+ ...notebookState.cellRuntime[cellId],
371
+ output: {
372
+ channel: "marimo-error",
373
+ data: [
374
+ {
375
+ type: "exception",
376
+ msg: "boom",
377
+ exception_type: "ValueError",
378
+ raising_cell: null,
379
+ },
380
+ ],
381
+ mimetype: "application/vnd.marimo+error",
382
+ timestamp: Date.now(),
383
+ },
384
+ consoleOutputs: [
385
+ {
386
+ channel: "stderr",
387
+ data: '<pre>ValueError: boom\n File "notebook.py", line 1</pre>',
388
+ mimetype: "application/vnd.marimo+traceback",
389
+ timestamp: Date.now(),
390
+ },
391
+ ],
392
+ };
393
+ store.set(notebookAtom, notebookState);
394
+
395
+ const items = provider.getItems();
396
+ const context = provider.formatContext(items[1]);
397
+ expect(context).toContain("boom");
398
+ expect(context).toContain("ValueError: boom");
399
+ expect(context).toContain('File "notebook.py", line 1');
400
+ expect(items[1].description).toBe("boom");
401
+ });
402
+
403
+ it("does not duplicate console traceback when error already has one", () => {
404
+ const cellId = CellIdClass.create();
405
+ const notebookState = MockNotebook.notebookState({
406
+ cellData: {
407
+ [cellId]: {
408
+ name: "Cell 1",
409
+ code: "raise ValueError('boom')",
410
+ },
411
+ },
412
+ });
413
+ notebookState.cellRuntime[cellId] = {
414
+ ...notebookState.cellRuntime[cellId],
415
+ output: {
416
+ channel: "marimo-error",
417
+ data: [
418
+ {
419
+ type: "exception",
420
+ msg: "boom",
421
+ exception_type: "ValueError",
422
+ raising_cell: null,
423
+ traceback: "<pre>embedded line 1</pre>",
424
+ },
425
+ ],
426
+ mimetype: "application/vnd.marimo+error",
427
+ timestamp: Date.now(),
428
+ },
429
+ consoleOutputs: [
430
+ {
431
+ channel: "stderr",
432
+ data: "<pre>console line 1</pre>",
433
+ mimetype: "application/vnd.marimo+traceback",
434
+ timestamp: Date.now(),
435
+ },
436
+ ],
437
+ };
438
+ store.set(notebookAtom, notebookState);
439
+
440
+ const items = provider.getItems();
441
+ if (items[1].data.type !== "cell-error") {
442
+ throw new Error("Expected cell-error item");
443
+ }
444
+ expect(items[1].data.error.tracebackHtml).toBeUndefined();
445
+
446
+ const context = provider.formatContext(items[1]);
447
+ expect(context).toContain("embedded line 1");
448
+ expect(context).not.toContain("console line 1");
449
+ expect(items[1].description).toBe("boom");
266
450
  });
267
451
  });
268
452
 
@@ -319,7 +503,8 @@ describe("ErrorContextProvider", () => {
319
503
  testStore.set(notebookAtom, notebookState);
320
504
 
321
505
  const items = testProvider.getItems();
322
- expect(items).toHaveLength(1);
506
+ // One "all-errors" item plus one per-cell error item.
507
+ expect(items).toHaveLength(2);
323
508
  const context = testProvider.formatContext(items[0]);
324
509
  expect(context).toContain(expected);
325
510
  }
@@ -8,6 +8,7 @@ import type { CellId } from "@/core/cells/ids";
8
8
  import { LanguageAdapters } from "@/core/codemirror/language/LanguageAdapters";
9
9
  import { renderDatasourceInfo } from "@/core/codemirror/language/languages/sql/renderers";
10
10
  import {
11
+ allTablesAtom,
11
12
  type ConnectionsMap,
12
13
  type DatasetTablesMap,
13
14
  dataSourceConnectionsAtom,
@@ -18,7 +19,7 @@ import {
18
19
  INTERNAL_SQL_ENGINES,
19
20
  } from "@/core/datasets/engines";
20
21
  import type { DataSourceConnection, DataTable } from "@/core/kernel/messages";
21
- import { store } from "@/core/state/jotai";
22
+ import type { JotaiStore } from "@/core/state/jotai";
22
23
  import type { AIContextItem } from "../registry";
23
24
  import { AIContextProvider } from "../registry";
24
25
  import { contextToXml } from "../utils";
@@ -148,7 +149,10 @@ export class DatasourceContextProvider extends AIContextProvider<DatasourceConte
148
149
  }
149
150
  }
150
151
 
151
- export function getDatasourceContext(cellId: CellId): string | null {
152
+ export function getDatasourceContext(
153
+ cellId: CellId,
154
+ store: JotaiStore,
155
+ ): string | null {
152
156
  const cellData = store.get(cellDataAtom(cellId));
153
157
  const code = cellData?.code;
154
158
  if (!code || code.trim() === "") {
@@ -164,3 +168,24 @@ export function getDatasourceContext(cellId: CellId): string | null {
164
168
  }
165
169
  return null;
166
170
  }
171
+
172
+ export function formatDatasourceContextForCell(
173
+ cellId: CellId,
174
+ store: JotaiStore,
175
+ ): string | null {
176
+ const mention = getDatasourceContext(cellId, store);
177
+ if (!mention) {
178
+ return null;
179
+ }
180
+
181
+ const connectionsMap = store.get(dataSourceConnectionsAtom).connectionsMap;
182
+ const tablesMap = store.get(allTablesAtom);
183
+ const provider = new DatasourceContextProvider(connectionsMap, tablesMap);
184
+ const uri = mention.slice(1);
185
+ const item = provider.getItems().find((candidate) => candidate.uri === uri);
186
+ if (!item) {
187
+ return null;
188
+ }
189
+
190
+ return provider.formatContext(item);
191
+ }
@@ -1,26 +1,133 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import type { Completion } from "@codemirror/autocomplete";
4
- import { cellErrorsAtom } from "@/core/cells/cells";
4
+ import { notebookAtom } from "@/core/cells/cells";
5
5
  import type { CellId } from "@/core/cells/ids";
6
- import type { MarimoError } from "@/core/kernel/messages";
6
+ import { displayCellName } from "@/core/cells/names";
7
+ import type { MarimoError, OutputMessage } from "@/core/kernel/messages";
8
+ import { isErrorMime, isMarimoErrorsMime, isTracebackMime } from "@/core/mime";
7
9
  import type { JotaiStore } from "@/core/state/jotai";
8
10
  import { logNever } from "@/utils/assertNever";
11
+ import { parseHtmlContent } from "@/utils/dom";
9
12
  import { PluralWord } from "@/utils/pluralize";
10
13
  import { type AIContextItem, AIContextProvider } from "../registry";
11
14
  import { contextToXml } from "../utils";
12
15
  import { Sections } from "./common";
16
+ import { formatDatasourceContextForCell } from "./datasource";
17
+
18
+ export interface CellErrorEntry {
19
+ cellId: CellId;
20
+ cellName: string;
21
+ cellCode: string;
22
+ errorData: MarimoError[];
23
+ tracebackHtml?: string;
24
+ }
25
+
26
+ export type ErrorContextItemData =
27
+ | {
28
+ type: "all-errors";
29
+ errors: CellErrorEntry[];
30
+ }
31
+ | {
32
+ type: "cell-error";
33
+ error: CellErrorEntry;
34
+ };
13
35
 
14
36
  export interface ErrorContextItem extends AIContextItem {
15
37
  type: "error";
16
- data: {
17
- type: "all-errors";
18
- errors: {
19
- cellId: CellId;
20
- cellName: string;
21
- errorData: MarimoError[];
22
- }[];
23
- };
38
+ data: ErrorContextItemData;
39
+ }
40
+
41
+ function parseCellErrorOutput(
42
+ output: OutputMessage,
43
+ ): Pick<CellErrorEntry, "errorData" | "tracebackHtml"> | null {
44
+ if (isMarimoErrorsMime(output.mimetype)) {
45
+ if (!Array.isArray(output.data)) {
46
+ return null;
47
+ }
48
+ const errorData = output.data.filter(
49
+ (error) => !error.type.includes("ancestor"),
50
+ );
51
+ if (errorData.length === 0) {
52
+ return null;
53
+ }
54
+ return { errorData };
55
+ }
56
+
57
+ if (isTracebackMime(output.mimetype)) {
58
+ if (typeof output.data !== "string" || output.data.length === 0) {
59
+ return null;
60
+ }
61
+ return { errorData: [], tracebackHtml: output.data };
62
+ }
63
+
64
+ return null;
65
+ }
66
+
67
+ function errorDataHasTraceback(errorData: MarimoError[]): boolean {
68
+ return errorData.some((error) => "traceback" in error && error.traceback);
69
+ }
70
+
71
+ function getTracebackFromConsole(
72
+ consoleOutputs: OutputMessage[] | undefined,
73
+ ): string | undefined {
74
+ const tracebackOutput = consoleOutputs?.find((output) =>
75
+ isTracebackMime(output.mimetype),
76
+ );
77
+ if (
78
+ !tracebackOutput ||
79
+ typeof tracebackOutput.data !== "string" ||
80
+ tracebackOutput.data.length === 0
81
+ ) {
82
+ return undefined;
83
+ }
84
+ return tracebackOutput.data;
85
+ }
86
+
87
+ function resolveTracebackHtml(
88
+ parsed: Pick<CellErrorEntry, "errorData" | "tracebackHtml">,
89
+ consoleOutputs: OutputMessage[] | undefined,
90
+ ): string | undefined {
91
+ if (parsed.tracebackHtml) {
92
+ return parsed.tracebackHtml;
93
+ }
94
+ if (errorDataHasTraceback(parsed.errorData)) {
95
+ return undefined;
96
+ }
97
+ return getTracebackFromConsole(consoleOutputs);
98
+ }
99
+
100
+ export function getCellErrorEntries(store: JotaiStore): CellErrorEntry[] {
101
+ const { cellIds, cellRuntime, cellData } = store.get(notebookAtom);
102
+ const entries: CellErrorEntry[] = [];
103
+
104
+ for (const [cellIndex, cellId] of cellIds.inOrderIds.entries()) {
105
+ const cell = cellRuntime[cellId];
106
+ const output = cell.output;
107
+ if (!output || !isErrorMime(output.mimetype)) {
108
+ continue;
109
+ }
110
+
111
+ const parsed = parseCellErrorOutput(output);
112
+ if (!parsed) {
113
+ continue;
114
+ }
115
+
116
+ const cellName = displayCellName(cellData[cellId].name, cellIndex);
117
+ // Prefer the code from the last execution: runtime errors correspond to
118
+ // the previous run, while `code` may already contain unsaved edits.
119
+ const cellCode = cellData[cellId].lastCodeRun ?? cellData[cellId].code;
120
+
121
+ entries.push({
122
+ cellId,
123
+ cellName,
124
+ cellCode,
125
+ errorData: parsed.errorData,
126
+ tracebackHtml: resolveTracebackHtml(parsed, cell.consoleOutputs),
127
+ });
128
+ }
129
+
130
+ return entries;
24
131
  }
25
132
 
26
133
  function describeError(error: MarimoError): string {
@@ -67,6 +174,77 @@ function describeError(error: MarimoError): string {
67
174
  return "Unknown error";
68
175
  }
69
176
 
177
+ function formatSingleError(error: MarimoError): string {
178
+ let text = describeError(error);
179
+ if ("traceback" in error && error.traceback) {
180
+ text += `\n\nTraceback:\n${parseHtmlContent(error.traceback)}`;
181
+ }
182
+ return text;
183
+ }
184
+
185
+ function formatCellErrorDetails(
186
+ entry: CellErrorEntry,
187
+ store: JotaiStore,
188
+ ): string {
189
+ const parts: string[] = [`Code:\n${entry.cellCode}`];
190
+
191
+ if (entry.tracebackHtml) {
192
+ parts.push(`Traceback:\n${parseHtmlContent(entry.tracebackHtml)}`);
193
+ }
194
+
195
+ if (entry.errorData.length > 0) {
196
+ parts.push(
197
+ entry.errorData.map((error) => formatSingleError(error)).join("\n\n"),
198
+ );
199
+ }
200
+
201
+ if (entry.errorData.some((error) => error.type === "sql-error")) {
202
+ const datasourceContext = formatDatasourceContextForCell(
203
+ entry.cellId,
204
+ store,
205
+ );
206
+ if (datasourceContext) {
207
+ parts.push(`Database schema:\n${datasourceContext}`);
208
+ }
209
+ }
210
+
211
+ return parts.join("\n\n");
212
+ }
213
+
214
+ function formatCellErrorXml(entry: CellErrorEntry, store: JotaiStore): string {
215
+ return contextToXml({
216
+ type: "error",
217
+ data: {
218
+ name: entry.cellName || `Cell ${entry.cellId}`,
219
+ cellId: entry.cellId,
220
+ },
221
+ details: formatCellErrorDetails(entry, store),
222
+ });
223
+ }
224
+
225
+ function summarizeCellError(entry: CellErrorEntry): string {
226
+ if (entry.errorData.length === 1) {
227
+ return describeError(entry.errorData[0]);
228
+ }
229
+ if (entry.errorData.length > 1) {
230
+ return `${entry.errorData.length} errors`;
231
+ }
232
+ if (entry.tracebackHtml) {
233
+ const text = parseHtmlContent(entry.tracebackHtml).trim();
234
+ const firstLine = text
235
+ .split("\n")
236
+ .find((line) => line.trim())
237
+ ?.trim();
238
+ return firstLine || "traceback";
239
+ }
240
+ return "error";
241
+ }
242
+
243
+ function errorContextName(entry: CellErrorEntry): string {
244
+ const cellName = entry.cellName || `Cell ${entry.cellId}`;
245
+ return `Error: ${cellName}`;
246
+ }
247
+
70
248
  const errorsTxt = new PluralWord("error", "errors");
71
249
  export class ErrorContextProvider extends AIContextProvider<ErrorContextItem> {
72
250
  readonly title = "Errors";
@@ -80,39 +258,50 @@ export class ErrorContextProvider extends AIContextProvider<ErrorContextItem> {
80
258
  }
81
259
 
82
260
  getItems(): ErrorContextItem[] {
83
- const errors = this.store.get(cellErrorsAtom);
261
+ const errors = getCellErrorEntries(this.store);
262
+
84
263
  if (errors.length === 0) {
85
264
  return [];
86
265
  }
87
266
 
88
- return [
267
+ const items: ErrorContextItem[] = [
89
268
  {
90
269
  uri: this.asURI("all"),
91
270
  name: "Errors",
92
271
  type: this.contextType,
93
272
  data: {
94
273
  type: "all-errors",
95
- errors: errors.map((error) => ({
96
- cellId: error.cellId,
97
- cellName: error.cellName,
98
- errorData: error.output.data,
99
- })),
274
+ errors,
100
275
  },
101
276
  description: "All errors in the notebook",
102
277
  },
103
278
  ];
104
279
 
105
- // TODO: maybe handle single errors or grouped by types
280
+ for (const error of errors) {
281
+ items.push({
282
+ uri: this.asURI(error.cellId),
283
+ name: errorContextName(error),
284
+ type: this.contextType,
285
+ data: {
286
+ type: "cell-error",
287
+ error,
288
+ },
289
+ description: summarizeCellError(error),
290
+ });
291
+ }
292
+
293
+ return items;
106
294
  }
107
295
 
108
296
  formatCompletion(item: ErrorContextItem): Completion {
109
297
  if (item.data.type === "all-errors") {
298
+ const errorCount = item.data.errors.length;
110
299
  return {
111
300
  label: "@Errors",
112
301
  displayLabel: "Errors",
113
- detail: `${item.data.errors.length} ${errorsTxt.pluralize(item.data.errors.length)}`,
302
+ detail: `${errorCount} ${errorsTxt.pluralize(errorCount)}`,
114
303
  type: "error",
115
- apply: "@Errors",
304
+ apply: "@error://all",
116
305
  section: Sections.ERROR,
117
306
  info: () => {
118
307
  const infoContainer = document.createElement("div");
@@ -136,7 +325,7 @@ export class ErrorContextProvider extends AIContextProvider<ErrorContextItem> {
136
325
 
137
326
  const descriptionDiv = document.createElement("div");
138
327
  descriptionDiv.classList.add("text-sm", "text-muted-foreground");
139
- descriptionDiv.textContent = `${item.data.errors.length} ${errorsTxt.pluralize(item.data.errors.length)}`;
328
+ descriptionDiv.textContent = `${errorCount} ${errorsTxt.pluralize(errorCount)}`;
140
329
  headerDiv.append(descriptionDiv);
141
330
 
142
331
  infoContainer.append(headerDiv);
@@ -146,6 +335,17 @@ export class ErrorContextProvider extends AIContextProvider<ErrorContextItem> {
146
335
  };
147
336
  }
148
337
 
338
+ if (item.data.type === "cell-error") {
339
+ const { error } = item.data;
340
+ return {
341
+ ...this.createBasicCompletion(item, {
342
+ detail: summarizeCellError(error),
343
+ type: "error",
344
+ }),
345
+ section: Sections.ERROR,
346
+ };
347
+ }
348
+
149
349
  return {
150
350
  label: "Error",
151
351
  displayLabel: "Error",
@@ -154,21 +354,11 @@ export class ErrorContextProvider extends AIContextProvider<ErrorContextItem> {
154
354
  }
155
355
 
156
356
  formatContext(item: ErrorContextItem): string {
157
- const { data } = item;
158
-
159
- const xmls = data.errors.map((err) => {
160
- return contextToXml({
161
- type: this.contextType,
162
- data: {
163
- name: err.cellName || `Cell ${err.cellId}`,
164
- description: err.errorData
165
- .map((err) => describeError(err))
166
- .join("\n"),
167
- },
168
- });
169
- });
357
+ const entries =
358
+ item.data.type === "all-errors" ? item.data.errors : [item.data.error];
170
359
 
171
- const xml = xmls.join("\n\n");
172
- return xml;
360
+ return entries
361
+ .map((entry) => formatCellErrorXml(entry, this.store))
362
+ .join("\n\n");
173
363
  }
174
364
  }