@marimo-team/islands 0.25.3-dev4 → 0.25.3-dev6

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/{ConnectedDataExplorerComponent-CNSxdKyJ.js → ConnectedDataExplorerComponent-CNMliVzw.js} +1 -1
  2. package/dist/{ErrorBoundary-CuU5CuVr.js → ErrorBoundary-Ca-Sa492.js} +1 -1
  3. package/dist/{abnfDiagram-VCTEODGH-DQDjNXt3.js → abnfDiagram-VCTEODGH-D-Ez91gV.js} +2 -2
  4. package/dist/{any-language-editor-B9REGlD9.js → any-language-editor-DZ_YTVus.js} +1 -1
  5. package/dist/{architectureDiagram-5GKGNRK7-Nmls7Wxn.js → architectureDiagram-5GKGNRK7-CTpcNtO8.js} +1 -1
  6. package/dist/{chat-ui-BDpymeoe.js → chat-ui-0lKdOoQ3.js} +436 -437
  7. package/dist/{chunk-IMKFNOWR-CoIFEo-1.js → chunk-IMKFNOWR-DGxi-zeo.js} +1 -1
  8. package/dist/{chunk-SHT3W25Y-lHiGx3ZC.js → chunk-SHT3W25Y-CV9J3rAB.js} +1 -1
  9. package/dist/{chunk-SVP7TREG-BvsHIrY_.js → chunk-SVP7TREG-BL-9Fqgg.js} +1 -1
  10. package/dist/{chunk-TICWLB2K-BC8YIm_n.js → chunk-TICWLB2K-Cu_XMjDM.js} +1 -1
  11. package/dist/{classDiagram-ZZMXUADV-Bt98lkkv.js → classDiagram-ZZMXUADV-BXXV13Hq.js} +2 -2
  12. package/dist/{classDiagram-v2-VYDZK3BY-D9bfbcC3.js → classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js} +2 -2
  13. package/dist/{code-visibility-Lu0dkrFO.js → code-visibility-NGRjhbtA.js} +14 -14
  14. package/dist/{common-DiWGWDIM.js → common-B7_ZSg0P.js} +6426 -6256
  15. package/dist/{cynefinDiagram-5FMLGOSQ-CgWra7wt.js → cynefinDiagram-5FMLGOSQ-CH96rwba.js} +1 -1
  16. package/dist/{dagre-GXQ25YYZ-CFZ8-35c.js → dagre-GXQ25YYZ-BD3PmpFY.js} +1 -1
  17. package/dist/{data-grid-overlay-editor-ga7ytgbj.js → data-grid-overlay-editor-B_cmrkBm.js} +1 -1
  18. package/dist/{diagram-S7CK7UJ4-lCNQrW7d.js → diagram-S7CK7UJ4-BsSJJldB.js} +1 -1
  19. package/dist/{diagram-UQ7AKVKN-CQPQ6pP_.js → diagram-UQ7AKVKN-muQtNZc4.js} +1 -1
  20. package/dist/{diagram-VX7I27RA-Cf3b0llz.js → diagram-VX7I27RA-CdhuZAqR.js} +1 -1
  21. package/dist/{diagram-Z3DM3KII-Bt4QLrpn.js → diagram-Z3DM3KII-BD8mXjOj.js} +1 -1
  22. package/dist/{ebnfDiagram-PWID7BFC-D3NAyPFY.js → ebnfDiagram-PWID7BFC-BIN1sweE.js} +2 -2
  23. package/dist/{erDiagram-RLTQ6QDP-Tlu09_Nm.js → erDiagram-RLTQ6QDP-Ctp5cfoN.js} +1 -1
  24. package/dist/{error-banner-Da600cwz.js → error-banner-D-WCIiMt.js} +10 -5
  25. package/dist/{cells-C8iCYvLU.js → error-entries-DSvFWT_M.js} +2262 -2206
  26. package/dist/{feedback-button-DQA-pzxi.js → feedback-button-CTF2Nj9s.js} +115 -116
  27. package/dist/{flowDiagram-HODETNUW-CAYbpJmV.js → flowDiagram-HODETNUW-3EfLrlFb.js} +2 -2
  28. package/dist/{formats-BGh2thRc.js → formats-CeOmU1MU.js} +1 -1
  29. package/dist/{glide-data-editor-Bv75MH6g.js → glide-data-editor-BQHF9puC.js} +3 -3
  30. package/dist/{globals-EDQGFIvi.js → globals-Cj8iqMEp.js} +1 -1
  31. package/dist/{html-to-image-CZY1pIZH.js → html-to-image-B8s5SgGm.js} +23 -23
  32. package/dist/{infoDiagram-27XIBGKW-XrDxhr0x.js → infoDiagram-27XIBGKW-DipQoHxl.js} +1 -1
  33. package/dist/{ishikawaDiagram-5VMMS53U-CD1muzov.js → ishikawaDiagram-5VMMS53U-DgFMjSGz.js} +1 -1
  34. package/dist/{kanban-definition-UXKFOSKX-Dg9W142_.js → kanban-definition-UXKFOSKX-w1ACVjgl.js} +1 -1
  35. package/dist/main.js +164 -163
  36. package/dist/{mermaid-CiAXjLpA.js → mermaid-CHo49Yv-.js} +33 -33
  37. package/dist/{mindmap-definition-YA3MSWOX-DgCidgYJ.js → mindmap-definition-YA3MSWOX-CrT0KSfX.js} +1 -1
  38. package/dist/{number-overlay-editor-BCP3MxhC.js → number-overlay-editor-DBcnm0SY.js} +1 -1
  39. package/dist/{pegDiagram-XKGWAZYB-coOYYKB6.js → pegDiagram-XKGWAZYB-BqvywNJA.js} +2 -2
  40. package/dist/{pieDiagram-E7YTZNPT-BnXTc3bj.js → pieDiagram-E7YTZNPT-BfxJ_XCc.js} +1 -1
  41. package/dist/{process-output-e4jj9Ijt.js → process-output-CKRXyszb.js} +3 -3
  42. package/dist/{railroadDiagram-O6MQD6OU-B28-u83n.js → railroadDiagram-O6MQD6OU-8vFLVTPu.js} +2 -2
  43. package/dist/{requirementDiagram-BXWQKSXE-CdYCTMu0.js → requirementDiagram-BXWQKSXE-a_MxGvxe.js} +1 -1
  44. package/dist/{reveal-component-DO5BJB9l.js → reveal-component-DTBSuGkC.js} +4 -4
  45. package/dist/{sequenceDiagram-WJ2MYXX4-CM3aRAe_.js → sequenceDiagram-WJ2MYXX4-CXFMnxGh.js} +1 -1
  46. package/dist/{stateDiagram-D77RDMKH-DOZUMVj8.js → stateDiagram-D77RDMKH-DQeA_CwI.js} +2 -2
  47. package/dist/{stateDiagram-v2-MP3YSRHH-CISYxma6.js → stateDiagram-v2-MP3YSRHH-BxxQucm1.js} +2 -2
  48. package/dist/style.css +1 -1
  49. package/dist/{swimlanes-42K2YHIH-BkSppxp8.js → swimlanes-42K2YHIH-Do6L-cYg.js} +1 -1
  50. package/dist/{swimlanesDiagram-VR7AAH4N-c8G5pGxC.js → swimlanesDiagram-VR7AAH4N-DUzPvSVU.js} +2 -2
  51. package/dist/{timeline-definition-24CTP7MA-CaMQayqK.js → timeline-definition-24CTP7MA-D3c46WM5.js} +1 -1
  52. package/dist/{toDate-BNJf01u1.js → toDate-vIXljnsr.js} +39 -38
  53. package/dist/{vega-component-DG29TAB7.js → vega-component-DTG6zxaY.js} +3 -3
  54. package/dist/{vennDiagram-4TSXK5OY-CF8pfaK3.js → vennDiagram-4TSXK5OY-BSZD8JcZ.js} +1 -1
  55. package/dist/{wardleyDiagram-VM6X3IG4-dGPu39d-.js → wardleyDiagram-VM6X3IG4-48cUWkfA.js} +1 -1
  56. package/dist/{xychartDiagram-S5SC5T6Z-CP6-9q56.js → xychartDiagram-S5SC5T6Z-CswYgYeu.js} +1 -1
  57. package/package.json +1 -1
  58. package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
  59. package/src/__tests__/mount.test.ts +8 -8
  60. package/src/components/editor/Output.tsx +9 -2
  61. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
  62. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
  63. package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
  64. package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
  65. package/src/components/editor/chrome/panels/error-panel.tsx +1 -0
  66. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +372 -0
  67. package/src/components/editor/chrome/wrapper/footer-item.tsx +32 -24
  68. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +237 -0
  69. package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
  70. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +548 -0
  71. package/src/components/editor/errors/send-error-report-button.tsx +69 -0
  72. package/src/components/editor/output/MarimoErrorOutput.tsx +21 -5
  73. package/src/components/editor/output/MarimoTracebackOutput.tsx +35 -20
  74. package/src/components/editor/output/__tests__/ansi-reduce.test.ts +18 -0
  75. package/src/components/editor/output/ansi-reduce.ts +6 -0
  76. package/src/core/attachments/__tests__/toast.test.tsx +58 -0
  77. package/src/core/attachments/state.ts +21 -0
  78. package/src/core/attachments/toast.ts +15 -0
  79. package/src/core/cells/__tests__/cells.test.ts +52 -0
  80. package/src/core/cells/__tests__/error-report.test.ts +139 -0
  81. package/src/core/cells/__tests__/session.test.ts +50 -1
  82. package/src/core/cells/error-report.ts +63 -0
  83. package/src/core/cells/session.ts +28 -1
  84. package/src/core/config/pair.ts +5 -3
  85. package/src/core/islands/__tests__/bootstrap.test.ts +26 -0
  86. package/src/core/islands/bootstrap.ts +1 -0
  87. package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +193 -0
  88. package/src/core/websocket/useMarimoKernelConnection.tsx +9 -0
  89. package/src/mount.tsx +3 -3
  90. package/dist/error-entries-CzoYkIQZ.js +0 -56
@@ -22,6 +22,7 @@ import {
22
22
  import { Kbd } from "@/components/ui/kbd";
23
23
  import { Tooltip } from "@/components/ui/tooltip";
24
24
  import { getCellEditorView } from "@/core/cells/cells";
25
+ import { buildErrorReport } from "@/core/cells/error-report";
25
26
  import type { CellId } from "@/core/cells/ids";
26
27
  import { SCRATCH_CELL_ID } from "@/core/cells/ids";
27
28
  import { toggleBreakpoint } from "@/core/codemirror/cells/debugger-state";
@@ -34,6 +35,7 @@ import { isWasm } from "@/core/wasm/utils";
34
35
  import { renderHTML } from "@/plugins/core/RenderHTML";
35
36
  import { sanitizeHtml } from "@/plugins/core/sanitize-html";
36
37
  import { copyToClipboard } from "@/utils/copy";
38
+ import { parseHtmlContent } from "@/utils/dom";
37
39
  import {
38
40
  containsMangledLocal,
39
41
  splitMangledLocals,
@@ -46,33 +48,45 @@ import {
46
48
  import { useOpenAiAssistant } from "../chrome/wrapper/useOpenAiAssistant";
47
49
  import { AIFixButton, buildFixInChatPrompt } from "../errors/auto-fix";
48
50
  import { MangledSegments } from "../errors/mangled-local-chip";
51
+ import { SendErrorReportButton } from "../errors/send-error-report-button";
49
52
  import { CellLinkTraceback } from "../links/cell-link";
50
53
  import type { OnRefactorWithAI } from "../Output";
51
54
 
52
55
  interface Props {
53
56
  cellId: CellId | undefined;
54
57
  traceback: string;
58
+ tracebackFormat?: "html" | "text";
59
+ code?: string;
55
60
  onRefactorWithAI?: OnRefactorWithAI;
56
61
  }
57
62
 
58
- /**
59
- * List of errors due to violations of Marimo semantics.
60
- */
63
+ /** Render a traceback and actions for its captured execution. */
61
64
  export const MarimoTracebackOutput = ({
62
65
  onRefactorWithAI,
63
66
  traceback,
67
+ tracebackFormat = "html",
64
68
  cellId,
69
+ code,
65
70
  }: Props): JSX.Element => {
66
- const htmlTraceback = renderHTML({
67
- html: traceback,
68
- additionalReplacements: [
69
- replaceTracebackFilenames,
70
- replaceTracebackPrefix,
71
- replaceMangledLocal,
72
- ],
73
- });
71
+ const htmlTraceback =
72
+ tracebackFormat === "text" ? (
73
+ <pre>{traceback}</pre>
74
+ ) : (
75
+ renderHTML({
76
+ html: traceback,
77
+ additionalReplacements: [
78
+ replaceTracebackFilenames,
79
+ replaceTracebackPrefix,
80
+ replaceMangledLocal,
81
+ ],
82
+ })
83
+ );
74
84
 
75
- const lastTracebackLine = lastLine(traceback);
85
+ const textTraceback =
86
+ tracebackFormat === "html"
87
+ ? parseHtmlContent(sanitizeHtml(traceback))
88
+ : traceback;
89
+ const lastTracebackLine = lastLine(textTraceback);
76
90
  const aiFeaturesEnabled = useAtomValue(aiFeaturesEnabledAtom);
77
91
  const openAiAssistant = useOpenAiAssistant();
78
92
 
@@ -131,6 +145,13 @@ export const MarimoTracebackOutput = ({
131
145
  </div>
132
146
  )}
133
147
  <div className="flex gap-2">
148
+ {cellId && lastTracebackLine && (
149
+ <SendErrorReportButton
150
+ getContent={() =>
151
+ buildErrorReport(cellId, { code, traceback, tracebackFormat })
152
+ }
153
+ />
154
+ )}
134
155
  {showAIFix && (
135
156
  <AIFixButton
136
157
  tooltip="Fix with AI"
@@ -186,11 +207,7 @@ export const MarimoTracebackOutput = ({
186
207
  </DropdownMenuItem>
187
208
  <DropdownMenuItem
188
209
  onClick={() => {
189
- // Strip HTML from the traceback (sanitize first to prevent XSS)
190
- const div = document.createElement("div");
191
- div.innerHTML = sanitizeHtml(traceback);
192
- const textContent = div.textContent || "";
193
- copyToClipboard(textContent);
210
+ copyToClipboard(textTraceback);
194
211
  }}
195
212
  >
196
213
  <CopyIcon className="h-4 w-4 mr-2" />
@@ -205,9 +222,7 @@ export const MarimoTracebackOutput = ({
205
222
  };
206
223
 
207
224
  function lastLine(text: string): string {
208
- const el = document.createElement("div");
209
- el.innerHTML = sanitizeHtml(text);
210
- const lines = el.textContent?.split("\n").filter(Boolean);
225
+ const lines = text.split("\n").filter(Boolean);
211
226
  return lines?.at(-1) || "";
212
227
  }
213
228
 
@@ -7,6 +7,24 @@ import {
7
7
  TerminalBuffer,
8
8
  } from "../ansi-reduce";
9
9
 
10
+ test("preserves source evidence through stateful output and serialization", () => {
11
+ const output = StatefulOutputMessage.create({
12
+ channel: "stderr",
13
+ mimetype: "application/vnd.marimo+traceback",
14
+ timestamp: 1,
15
+ data: "<pre>ValueError:",
16
+ code: "raise ValueError('failed code')",
17
+ }).appendData(" failed code</pre>");
18
+ expect(output.code).toBe("raise ValueError('failed code')");
19
+ expect(output.toJSON()).toEqual({
20
+ channel: "stderr",
21
+ mimetype: "application/vnd.marimo+traceback",
22
+ timestamp: 1,
23
+ data: "<pre>ValueError: failed code</pre>",
24
+ code: "raise ValueError('failed code')",
25
+ });
26
+ });
27
+
10
28
  describe("TerminalBuffer", () => {
11
29
  test("writeChar writes single character", () => {
12
30
  const buffer = new TerminalBuffer();
@@ -368,6 +368,7 @@ export class StatefulOutputMessage implements OutputMessage {
368
368
  public readonly mimetype: OutputMessage["mimetype"];
369
369
  public readonly channel: OutputMessage["channel"];
370
370
  public readonly timestamp: OutputMessage["timestamp"];
371
+ public readonly code: OutputMessage["code"];
371
372
  private ansiReducer = new AnsiReducer();
372
373
  private _data: string;
373
374
 
@@ -383,6 +384,7 @@ export class StatefulOutputMessage implements OutputMessage {
383
384
  message.channel,
384
385
  message.timestamp,
385
386
  ansiReducer,
387
+ message.code,
386
388
  );
387
389
  }
388
390
 
@@ -391,10 +393,12 @@ export class StatefulOutputMessage implements OutputMessage {
391
393
  channel: OutputMessage["channel"],
392
394
  timestamp: OutputMessage["timestamp"],
393
395
  ansiReducer: AnsiReducer,
396
+ code: OutputMessage["code"],
394
397
  ) {
395
398
  this.mimetype = mimetype;
396
399
  this.channel = channel;
397
400
  this.timestamp = timestamp;
401
+ this.code = code;
398
402
  this.ansiReducer = ansiReducer;
399
403
  this._data = this.ansiReducer.render();
400
404
  }
@@ -406,6 +410,7 @@ export class StatefulOutputMessage implements OutputMessage {
406
410
  this.channel,
407
411
  this.timestamp,
408
412
  this.ansiReducer,
413
+ this.code,
409
414
  );
410
415
  }
411
416
 
@@ -415,6 +420,7 @@ export class StatefulOutputMessage implements OutputMessage {
415
420
  channel: this.channel,
416
421
  timestamp: this.timestamp,
417
422
  data: this.data,
423
+ ...(this.code === undefined ? {} : { code: this.code }),
418
424
  };
419
425
  }
420
426
  }
@@ -0,0 +1,58 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import { act, render, renderHook } from "@testing-library/react";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { Toaster } from "@/components/ui/toaster";
7
+ import { clearToasts, toast, useToast } from "@/components/ui/use-toast";
8
+ import { agentToast } from "../toast";
9
+
10
+ describe("agent notifications", () => {
11
+ beforeEach(() => {
12
+ vi.useFakeTimers();
13
+ clearToasts();
14
+ });
15
+
16
+ afterEach(() => {
17
+ act(() => {
18
+ clearToasts();
19
+ vi.runOnlyPendingTimers();
20
+ });
21
+ vi.useRealTimers();
22
+ });
23
+
24
+ it.each([
25
+ { variant: undefined, duration: 3000 },
26
+ { variant: "danger" as const, duration: 5000 },
27
+ ])("expires while the window is blurred: %j", ({ variant, duration }) => {
28
+ const { result } = renderHook(useToast);
29
+ render(<Toaster />);
30
+ act(() => agentToast({ title: "Agent notice", variant }));
31
+ act(() => window.dispatchEvent(new Event("blur")));
32
+ act(() => vi.advanceTimersByTime(duration - 1));
33
+ expect(result.current.toasts[0].open).toBe(true);
34
+ act(() => vi.advanceTimersByTime(1));
35
+ expect(result.current.toasts[0].open).toBe(false);
36
+ });
37
+
38
+ it("preserves pause behavior for ordinary notifications", () => {
39
+ const { result } = renderHook(useToast);
40
+ render(<Toaster />);
41
+ act(() => toast({ title: "Ordinary notice", duration: 3000 }));
42
+ act(() => window.dispatchEvent(new Event("blur")));
43
+ act(() => vi.advanceTimersByTime(5000));
44
+ expect(result.current.toasts[0].open).toBe(true);
45
+ });
46
+
47
+ it("does not let an older timer dismiss a newer notification", () => {
48
+ const { result } = renderHook(useToast);
49
+ act(() => agentToast({ title: "First" }));
50
+ act(() => vi.advanceTimersByTime(1000));
51
+ act(() => agentToast({ title: "Second" }));
52
+ act(() => vi.advanceTimersByTime(2000));
53
+ expect(result.current.toasts[0].title).toBe("Second");
54
+ expect(result.current.toasts[0].open).toBe(true);
55
+ act(() => vi.advanceTimersByTime(1000));
56
+ expect(result.current.toasts[0].open).toBe(false);
57
+ });
58
+ });
@@ -0,0 +1,21 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { components } from "@marimo-team/marimo-api";
4
+ import { atom } from "jotai";
5
+ import { isConnectedAtom } from "@/core/network/connection";
6
+
7
+ export type Attachment = components["schemas"]["Attachment"];
8
+
9
+ export const attachmentsAtom = atom<Attachment[]>([]);
10
+ export const agentAttachmentsAtom = atom((get) => {
11
+ if (!get(isConnectedAtom)) {
12
+ return [];
13
+ }
14
+ return get(attachmentsAtom).filter(
15
+ (attachment) => attachment.kind === "agent",
16
+ );
17
+ });
18
+
19
+ export function attachmentLabel(attachment: Attachment): string {
20
+ return attachment.name || "Agent";
21
+ }
@@ -0,0 +1,15 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { toast } from "@/components/ui/use-toast";
4
+
5
+ export function agentToast(
6
+ options: Pick<
7
+ Parameters<typeof toast>[0],
8
+ "title" | "variant" | "description"
9
+ >,
10
+ ) {
11
+ const duration = options.variant === "danger" ? 5000 : 3000;
12
+ const { dismiss } = toast({ ...options, duration });
13
+ // Agent notices must also expire while the user focuses the agent terminal.
14
+ window.setTimeout(dismiss, duration);
15
+ }
@@ -789,6 +789,58 @@ describe("cell reducer", () => {
789
789
  `);
790
790
  });
791
791
 
792
+ it("keeps error evidence after an edit and a new queued run", () => {
793
+ const code = "raise ValueError('old failure')";
794
+ const output: OutputMessage = {
795
+ channel: "marimo-error",
796
+ mimetype: "application/vnd.marimo+error",
797
+ data: [
798
+ {
799
+ type: "exception",
800
+ msg: "old failure",
801
+ exception_type: "ValueError",
802
+ raising_cell: null,
803
+ traceback: "<pre>ValueError: old failure</pre>",
804
+ },
805
+ ],
806
+ code,
807
+ timestamp: 1,
808
+ };
809
+ actions.handleCellMessage({
810
+ cell_id: firstCellId,
811
+ output,
812
+ console: {
813
+ channel: "stderr",
814
+ mimetype: "application/vnd.marimo+traceback",
815
+ data: "<pre>ValueError: old failure</pre>",
816
+ code,
817
+ timestamp: 1,
818
+ },
819
+ status: "idle",
820
+ stale_inputs: null,
821
+ timestamp: 1,
822
+ });
823
+ actions.updateCellCode({
824
+ cellId: firstCellId,
825
+ code: "print('new code')",
826
+ formattingChange: false,
827
+ });
828
+ actions.prepareForRun({ cellId: firstCellId });
829
+ actions.handleCellMessage({
830
+ cell_id: firstCellId,
831
+ output: null,
832
+ console: null,
833
+ status: "queued",
834
+ stale_inputs: null,
835
+ timestamp: 2,
836
+ });
837
+ const runtime = state.cellRuntime[firstCellId];
838
+ expect(state.cellData[firstCellId].lastCodeRun).toBe("print('new code')");
839
+ expect(runtime.output).toEqual(output);
840
+ expect(runtime.consoleOutputs[0].code).toBe(code);
841
+ expect(runtime.consoleOutputs[0].data).toContain("old failure");
842
+ });
843
+
792
844
  it("can run cell and receive cell messages", () => {
793
845
  // HAPPY PATH
794
846
  /////////////////
@@ -0,0 +1,139 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import { describe, expect, it } from "vitest";
5
+ import { cellId } from "@/__tests__/branded";
6
+ import type { MarimoError } from "@/core/kernel/messages";
7
+ import { buildErrorReport, ERROR_MESSAGE_INSTRUCTIONS } from "../error-report";
8
+
9
+ const CELL_ID = cellId("failed-cell");
10
+
11
+ describe("buildErrorReport", () => {
12
+ it("preserves plain traceback text without treating angle brackets as HTML", () => {
13
+ const traceback =
14
+ ' File "<cell>", line 1\nValueError: expected <value> & got <other>';
15
+ expect(
16
+ buildErrorReport(CELL_ID, { traceback, tracebackFormat: "text" }).output,
17
+ ).toEqual({
18
+ type: "error",
19
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
20
+ message: "ValueError: expected <value> & got <other>",
21
+ traceback,
22
+ });
23
+ });
24
+ it("owns the instruction to repair the live notebook", () => {
25
+ const report = buildErrorReport(CELL_ID, {
26
+ errors: [{ type: "syntax", msg: "invalid code" }],
27
+ });
28
+ expect(report.output.instructions).toEqual("Fix this notebook error.");
29
+ });
30
+
31
+ it("keeps repair intent visible before oversized evidence", () => {
32
+ const report = buildErrorReport(CELL_ID, {
33
+ code: "# long code\n".repeat(10_000),
34
+ errors: [
35
+ {
36
+ type: "exception",
37
+ exception_type: "ValueError",
38
+ msg: "long error ".repeat(10_000),
39
+ raising_cell: null,
40
+ traceback: `<pre>Traceback\n${"long frame\n".repeat(10_000)}</pre>`,
41
+ },
42
+ ],
43
+ });
44
+ const serialized = JSON.stringify(report);
45
+
46
+ expect(serialized.length).toBeGreaterThan(8192);
47
+ expect(serialized.slice(0, 256)).toContain(
48
+ '"instructions":"Fix this notebook error."',
49
+ );
50
+ });
51
+
52
+ it("retains every message and complete traceback without display limits", () => {
53
+ const traceback = `Traceback\n${"long frame\n".repeat(10_000)}ValueError: bad <value>`;
54
+ const errors: MarimoError[] = [
55
+ {
56
+ type: "exception",
57
+ msg: "bad <value>",
58
+ exception_type: "ValueError",
59
+ raising_cell: null,
60
+ traceback: `<pre>${traceback.replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</pre>`,
61
+ },
62
+ { type: "syntax", msg: "another error" },
63
+ ];
64
+ expect(buildErrorReport(CELL_ID, { code: "failed()", errors })).toEqual({
65
+ type: "cell-output",
66
+ cell_id: CELL_ID,
67
+ code: "failed()",
68
+ output: {
69
+ type: "error",
70
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
71
+ message: "bad <value>\nanother error",
72
+ traceback,
73
+ },
74
+ });
75
+ });
76
+
77
+ it("leaves source unknown for a restored legacy exception", () => {
78
+ expect(
79
+ buildErrorReport(CELL_ID, {
80
+ errors: [
81
+ {
82
+ type: "exception",
83
+ exception_type: "exception",
84
+ msg: "legacy error",
85
+ raising_cell: null,
86
+ traceback: null,
87
+ },
88
+ ],
89
+ }),
90
+ ).toEqual({
91
+ type: "cell-output",
92
+ cell_id: CELL_ID,
93
+ output: {
94
+ type: "error",
95
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
96
+ message: "legacy error",
97
+ },
98
+ });
99
+ });
100
+
101
+ it("preserves empty captured code", () => {
102
+ expect(
103
+ buildErrorReport(CELL_ID, {
104
+ code: "",
105
+ errors: [{ type: "syntax", msg: "invalid code" }],
106
+ }),
107
+ ).toEqual({
108
+ type: "cell-output",
109
+ cell_id: CELL_ID,
110
+ code: "",
111
+ output: {
112
+ type: "error",
113
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
114
+ message: "invalid code",
115
+ },
116
+ });
117
+ });
118
+
119
+ it("keeps separate structured tracebacks in order", () => {
120
+ const errors: MarimoError[] = ["first", "second"].map((message) => ({
121
+ type: "exception",
122
+ exception_type: "ValueError",
123
+ raising_cell: null,
124
+ msg: message,
125
+ traceback: `<pre>Traceback\nValueError: ${message}</pre>`,
126
+ }));
127
+ expect(buildErrorReport(CELL_ID, { errors })).toEqual({
128
+ type: "cell-output",
129
+ cell_id: CELL_ID,
130
+ output: {
131
+ type: "error",
132
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
133
+ message: "first\nsecond",
134
+ traceback:
135
+ "Traceback\nValueError: first\n\nTraceback\nValueError: second",
136
+ },
137
+ });
138
+ });
139
+ });
@@ -174,8 +174,11 @@ describe("notebookStateFromSession", () => {
174
174
  channel: "marimo-error",
175
175
  data: [
176
176
  {
177
- type: "unknown",
177
+ type: "exception",
178
+ exception_type: "ValueError",
178
179
  msg: "Something went wrong",
180
+ raising_cell: null,
181
+ traceback: "Traceback line 1",
179
182
  },
180
183
  ],
181
184
  mimetype: "application/vnd.marimo+error",
@@ -183,6 +186,52 @@ describe("notebookStateFromSession", () => {
183
186
  });
184
187
  });
185
188
 
189
+ it("restores matching code from the session", () => {
190
+ const code = "raise ValueError('cached failure')";
191
+ const session = createSession([
192
+ createSessionCell(
193
+ "cell-1",
194
+ [
195
+ {
196
+ type: "error",
197
+ ename: "ValueError",
198
+ evalue: "cached failure",
199
+ traceback: ["Traceback\n", "ValueError: cached failure"],
200
+ code,
201
+ },
202
+ ],
203
+ [
204
+ {
205
+ type: "stream",
206
+ name: "stderr",
207
+ text: "ValueError: cached <failure>",
208
+ mimetype: "application/vnd.marimo+traceback",
209
+ traceback_format: "text",
210
+ code,
211
+ },
212
+ ],
213
+ ),
214
+ ]);
215
+ const state = notebookStateFromSession(session, null);
216
+ invariant(state, "state is null");
217
+ expect(state.cellRuntime[CELL_1].output).toMatchObject({
218
+ code,
219
+ data: [
220
+ {
221
+ type: "exception",
222
+ exception_type: "ValueError",
223
+ msg: "cached failure",
224
+ traceback: "Traceback\nValueError: cached failure",
225
+ },
226
+ ],
227
+ });
228
+ expect(state.cellRuntime[CELL_1].consoleOutputs[0].code).toBe(code);
229
+ expect(state.cellRuntime[CELL_1].consoleOutputs[0]).toMatchObject({
230
+ traceback_format: "text",
231
+ data: "ValueError: cached <failure>",
232
+ });
233
+ });
234
+
186
235
  it("handles data output in session cell", () => {
187
236
  const dataOutput = {
188
237
  type: "data" as const,
@@ -0,0 +1,63 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describeError } from "@/core/errors/error-entries";
4
+ import type { MarimoError } from "@/core/kernel/messages";
5
+ import { sanitizeHtml } from "@/plugins/core/sanitize-html";
6
+ import { parseHtmlContent } from "@/utils/dom";
7
+ import type { CellId } from "./ids";
8
+
9
+ export interface CellOutputMessage<Output> {
10
+ type: "cell-output";
11
+ cell_id: CellId;
12
+ code?: string;
13
+ output: Output;
14
+ }
15
+
16
+ export interface ErrorMessageOutput {
17
+ type: "error";
18
+ instructions: string;
19
+ message: string;
20
+ traceback?: string;
21
+ }
22
+
23
+ export const ERROR_MESSAGE_INSTRUCTIONS = "Fix this notebook error.";
24
+
25
+ export function buildErrorReport(
26
+ cellId: CellId,
27
+ evidence: { code?: string } & (
28
+ | { errors: MarimoError[] }
29
+ | { traceback: string; tracebackFormat: "html" | "text" }
30
+ ),
31
+ ): CellOutputMessage<ErrorMessageOutput> {
32
+ let message: string;
33
+ let traceback: string;
34
+ if ("errors" in evidence) {
35
+ message = evidence.errors.map(describeError).join("\n");
36
+ traceback = evidence.errors
37
+ .flatMap((error) =>
38
+ "traceback" in error && error.traceback
39
+ ? [parseHtmlContent(sanitizeHtml(error.traceback))]
40
+ : [],
41
+ )
42
+ .join("\n\n");
43
+ } else {
44
+ traceback =
45
+ evidence.tracebackFormat === "html"
46
+ ? parseHtmlContent(sanitizeHtml(evidence.traceback))
47
+ : evidence.traceback;
48
+ message = traceback.split("\n").findLast((line) => line.length > 0) ?? "";
49
+ }
50
+
51
+ // Put repair intent before evidence so it survives prefix truncation.
52
+ return {
53
+ type: "cell-output",
54
+ cell_id: cellId,
55
+ output: {
56
+ type: "error",
57
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
58
+ message,
59
+ ...(traceback ? { traceback } : {}),
60
+ },
61
+ ...(evidence.code === undefined ? {} : { code: evidence.code }),
62
+ };
63
+ }
@@ -206,8 +206,20 @@ function createCellRuntimeFromSession(
206
206
  channel: "marimo-error",
207
207
  data: [
208
208
  {
209
- type: "unknown",
209
+ type: "exception",
210
+ exception_type: primaryOutput.ename,
210
211
  msg: primaryOutput.evalue,
212
+ raising_cell: null,
213
+ traceback:
214
+ primaryOutput.traceback.length === 0
215
+ ? null
216
+ : primaryOutput.traceback
217
+ .map((line, index, lines) =>
218
+ index < lines.length - 1 && !line.endsWith("\n")
219
+ ? `${line}\n`
220
+ : line,
221
+ )
222
+ .join(""),
211
223
  },
212
224
  ],
213
225
  mimetype: "application/vnd.marimo+error",
@@ -223,6 +235,9 @@ function createCellRuntimeFromSession(
223
235
  timestamp: DEFAULT_TIMESTAMP,
224
236
  };
225
237
  }
238
+ if (runtimeState.output && primaryOutput.code !== undefined) {
239
+ runtimeState.output.code = primaryOutput.code;
240
+ }
226
241
  }
227
242
 
228
243
  const consoleOutputs = sessionCell.console || [];
@@ -237,7 +252,13 @@ function createCellRuntimeFromSession(
237
252
  channel: "media",
238
253
  data: consoleOutput.data,
239
254
  mimetype: consoleOutput.mimetype,
255
+ ...(consoleOutput.traceback_format === undefined
256
+ ? {}
257
+ : { traceback_format: consoleOutput.traceback_format }),
240
258
  timestamp: DEFAULT_TIMESTAMP,
259
+ ...(consoleOutput.code === undefined
260
+ ? {}
261
+ : { code: consoleOutput.code }),
241
262
  };
242
263
  }
243
264
  // Handle StreamOutput (type: "stream")
@@ -245,7 +266,13 @@ function createCellRuntimeFromSession(
245
266
  channel: consoleOutput.name === "stderr" ? "stderr" : "stdout",
246
267
  data: consoleOutput.text,
247
268
  mimetype: consoleOutput.mimetype ?? "text/plain",
269
+ ...(consoleOutput.traceback_format === undefined
270
+ ? {}
271
+ : { traceback_format: consoleOutput.traceback_format }),
248
272
  timestamp: DEFAULT_TIMESTAMP,
273
+ ...(consoleOutput.code === undefined
274
+ ? {}
275
+ : { code: consoleOutput.code }),
249
276
  };
250
277
  }),
251
278
  };
@@ -3,16 +3,18 @@
3
3
  import { atom } from "jotai";
4
4
  import { z } from "zod";
5
5
 
6
- export const pairPreviewSchema = z.object({
6
+ export const pairConfigSchema = z.object({
7
7
  command: z.string(),
8
+ hostEnabled: z.boolean().default(false),
8
9
  templates: z.object({
9
10
  prompt: z.string(),
11
+ notebook: z.string(),
10
12
  session: z.string(),
11
13
  token_file: z.string(),
12
14
  token: z.string(),
13
15
  }),
14
16
  });
15
17
 
16
- export type PairPreviewConfig = z.infer<typeof pairPreviewSchema>;
18
+ export type PairConfig = z.infer<typeof pairConfigSchema>;
17
19
 
18
- export const pairPreviewAtom = atom<PairPreviewConfig | undefined>(undefined);
20
+ export const pairConfigAtom = atom<PairConfig | undefined>(undefined);