@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.
- package/dist/{ConnectedDataExplorerComponent-CNSxdKyJ.js → ConnectedDataExplorerComponent-CNMliVzw.js} +1 -1
- package/dist/{ErrorBoundary-CuU5CuVr.js → ErrorBoundary-Ca-Sa492.js} +1 -1
- package/dist/{abnfDiagram-VCTEODGH-DQDjNXt3.js → abnfDiagram-VCTEODGH-D-Ez91gV.js} +2 -2
- package/dist/{any-language-editor-B9REGlD9.js → any-language-editor-DZ_YTVus.js} +1 -1
- package/dist/{architectureDiagram-5GKGNRK7-Nmls7Wxn.js → architectureDiagram-5GKGNRK7-CTpcNtO8.js} +1 -1
- package/dist/{chat-ui-BDpymeoe.js → chat-ui-0lKdOoQ3.js} +436 -437
- package/dist/{chunk-IMKFNOWR-CoIFEo-1.js → chunk-IMKFNOWR-DGxi-zeo.js} +1 -1
- package/dist/{chunk-SHT3W25Y-lHiGx3ZC.js → chunk-SHT3W25Y-CV9J3rAB.js} +1 -1
- package/dist/{chunk-SVP7TREG-BvsHIrY_.js → chunk-SVP7TREG-BL-9Fqgg.js} +1 -1
- package/dist/{chunk-TICWLB2K-BC8YIm_n.js → chunk-TICWLB2K-Cu_XMjDM.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-Bt98lkkv.js → classDiagram-ZZMXUADV-BXXV13Hq.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-D9bfbcC3.js → classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js} +2 -2
- package/dist/{code-visibility-Lu0dkrFO.js → code-visibility-NGRjhbtA.js} +14 -14
- package/dist/{common-DiWGWDIM.js → common-B7_ZSg0P.js} +6426 -6256
- package/dist/{cynefinDiagram-5FMLGOSQ-CgWra7wt.js → cynefinDiagram-5FMLGOSQ-CH96rwba.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-CFZ8-35c.js → dagre-GXQ25YYZ-BD3PmpFY.js} +1 -1
- package/dist/{data-grid-overlay-editor-ga7ytgbj.js → data-grid-overlay-editor-B_cmrkBm.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-lCNQrW7d.js → diagram-S7CK7UJ4-BsSJJldB.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-CQPQ6pP_.js → diagram-UQ7AKVKN-muQtNZc4.js} +1 -1
- package/dist/{diagram-VX7I27RA-Cf3b0llz.js → diagram-VX7I27RA-CdhuZAqR.js} +1 -1
- package/dist/{diagram-Z3DM3KII-Bt4QLrpn.js → diagram-Z3DM3KII-BD8mXjOj.js} +1 -1
- package/dist/{ebnfDiagram-PWID7BFC-D3NAyPFY.js → ebnfDiagram-PWID7BFC-BIN1sweE.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-Tlu09_Nm.js → erDiagram-RLTQ6QDP-Ctp5cfoN.js} +1 -1
- package/dist/{error-banner-Da600cwz.js → error-banner-D-WCIiMt.js} +10 -5
- package/dist/{cells-C8iCYvLU.js → error-entries-DSvFWT_M.js} +2262 -2206
- package/dist/{feedback-button-DQA-pzxi.js → feedback-button-CTF2Nj9s.js} +115 -116
- package/dist/{flowDiagram-HODETNUW-CAYbpJmV.js → flowDiagram-HODETNUW-3EfLrlFb.js} +2 -2
- package/dist/{formats-BGh2thRc.js → formats-CeOmU1MU.js} +1 -1
- package/dist/{glide-data-editor-Bv75MH6g.js → glide-data-editor-BQHF9puC.js} +3 -3
- package/dist/{globals-EDQGFIvi.js → globals-Cj8iqMEp.js} +1 -1
- package/dist/{html-to-image-CZY1pIZH.js → html-to-image-B8s5SgGm.js} +23 -23
- package/dist/{infoDiagram-27XIBGKW-XrDxhr0x.js → infoDiagram-27XIBGKW-DipQoHxl.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-CD1muzov.js → ishikawaDiagram-5VMMS53U-DgFMjSGz.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-Dg9W142_.js → kanban-definition-UXKFOSKX-w1ACVjgl.js} +1 -1
- package/dist/main.js +164 -163
- package/dist/{mermaid-CiAXjLpA.js → mermaid-CHo49Yv-.js} +33 -33
- package/dist/{mindmap-definition-YA3MSWOX-DgCidgYJ.js → mindmap-definition-YA3MSWOX-CrT0KSfX.js} +1 -1
- package/dist/{number-overlay-editor-BCP3MxhC.js → number-overlay-editor-DBcnm0SY.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-coOYYKB6.js → pegDiagram-XKGWAZYB-BqvywNJA.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-BnXTc3bj.js → pieDiagram-E7YTZNPT-BfxJ_XCc.js} +1 -1
- package/dist/{process-output-e4jj9Ijt.js → process-output-CKRXyszb.js} +3 -3
- package/dist/{railroadDiagram-O6MQD6OU-B28-u83n.js → railroadDiagram-O6MQD6OU-8vFLVTPu.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-CdYCTMu0.js → requirementDiagram-BXWQKSXE-a_MxGvxe.js} +1 -1
- package/dist/{reveal-component-DO5BJB9l.js → reveal-component-DTBSuGkC.js} +4 -4
- package/dist/{sequenceDiagram-WJ2MYXX4-CM3aRAe_.js → sequenceDiagram-WJ2MYXX4-CXFMnxGh.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-DOZUMVj8.js → stateDiagram-D77RDMKH-DQeA_CwI.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-CISYxma6.js → stateDiagram-v2-MP3YSRHH-BxxQucm1.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-BkSppxp8.js → swimlanes-42K2YHIH-Do6L-cYg.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-c8G5pGxC.js → swimlanesDiagram-VR7AAH4N-DUzPvSVU.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-CaMQayqK.js → timeline-definition-24CTP7MA-D3c46WM5.js} +1 -1
- package/dist/{toDate-BNJf01u1.js → toDate-vIXljnsr.js} +39 -38
- package/dist/{vega-component-DG29TAB7.js → vega-component-DTG6zxaY.js} +3 -3
- package/dist/{vennDiagram-4TSXK5OY-CF8pfaK3.js → vennDiagram-4TSXK5OY-BSZD8JcZ.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-dGPu39d-.js → wardleyDiagram-VM6X3IG4-48cUWkfA.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-CP6-9q56.js → xychartDiagram-S5SC5T6Z-CswYgYeu.js} +1 -1
- package/package.json +1 -1
- package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
- package/src/__tests__/mount.test.ts +8 -8
- package/src/components/editor/Output.tsx +9 -2
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
- package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
- package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
- package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
- package/src/components/editor/chrome/panels/error-panel.tsx +1 -0
- package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +372 -0
- package/src/components/editor/chrome/wrapper/footer-item.tsx +32 -24
- package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +237 -0
- package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +548 -0
- package/src/components/editor/errors/send-error-report-button.tsx +69 -0
- package/src/components/editor/output/MarimoErrorOutput.tsx +21 -5
- package/src/components/editor/output/MarimoTracebackOutput.tsx +35 -20
- package/src/components/editor/output/__tests__/ansi-reduce.test.ts +18 -0
- package/src/components/editor/output/ansi-reduce.ts +6 -0
- package/src/core/attachments/__tests__/toast.test.tsx +58 -0
- package/src/core/attachments/state.ts +21 -0
- package/src/core/attachments/toast.ts +15 -0
- package/src/core/cells/__tests__/cells.test.ts +52 -0
- package/src/core/cells/__tests__/error-report.test.ts +139 -0
- package/src/core/cells/__tests__/session.test.ts +50 -1
- package/src/core/cells/error-report.ts +63 -0
- package/src/core/cells/session.ts +28 -1
- package/src/core/config/pair.ts +5 -3
- package/src/core/islands/__tests__/bootstrap.test.ts +26 -0
- package/src/core/islands/bootstrap.ts +1 -0
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +193 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +9 -0
- package/src/mount.tsx +3 -3
- package/dist/error-entries-CzoYkIQZ.js +0 -56
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
// @vitest-environment jsdom
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
act,
|
|
6
|
+
fireEvent,
|
|
7
|
+
render,
|
|
8
|
+
screen,
|
|
9
|
+
waitFor,
|
|
10
|
+
} from "@testing-library/react";
|
|
11
|
+
import { createStore, Provider, useAtomValue } from "jotai";
|
|
12
|
+
import type React from "react";
|
|
13
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
|
+
import { MockNotebook } from "@/__mocks__/notebook";
|
|
15
|
+
import { cellId } from "@/__tests__/branded";
|
|
16
|
+
import { PAIR_CONFIG } from "@/__tests__/fixtures/pair-config";
|
|
17
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
18
|
+
import { toast } from "@/components/ui/use-toast";
|
|
19
|
+
import { type Attachment, attachmentsAtom } from "@/core/attachments/state";
|
|
20
|
+
import { exportedForTesting, notebookAtom } from "@/core/cells/cells";
|
|
21
|
+
import {
|
|
22
|
+
type CellOutputMessage,
|
|
23
|
+
type ErrorMessageOutput,
|
|
24
|
+
ERROR_MESSAGE_INSTRUCTIONS,
|
|
25
|
+
} from "@/core/cells/error-report";
|
|
26
|
+
import { pairConfigAtom } from "@/core/config/pair";
|
|
27
|
+
import { notebookStateFromSession } from "@/core/cells/session";
|
|
28
|
+
import type { MarimoError, OutputMessage } from "@/core/kernel/messages";
|
|
29
|
+
import {
|
|
30
|
+
type StableSessionId,
|
|
31
|
+
stableSessionIdAtom,
|
|
32
|
+
} from "@/core/kernel/session";
|
|
33
|
+
import { API } from "@/core/network/api";
|
|
34
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
35
|
+
import { WebSocketClosedReason, ConnectionState } from "@/core/websocket/types";
|
|
36
|
+
import { Deferred } from "@/utils/Deferred";
|
|
37
|
+
import { HTTPError } from "@/utils/errors";
|
|
38
|
+
import { MarimoErrorOutput } from "../../output/MarimoErrorOutput";
|
|
39
|
+
import { MarimoTracebackOutput } from "../../output/MarimoTracebackOutput";
|
|
40
|
+
import { OutputRenderer } from "../../Output";
|
|
41
|
+
import { SendErrorReportButton } from "../send-error-report-button";
|
|
42
|
+
|
|
43
|
+
vi.mock("@/components/ui/use-toast", () => ({ toast: vi.fn() }));
|
|
44
|
+
vi.mock("@/core/network/api", () => ({ API: { post: vi.fn() } }));
|
|
45
|
+
vi.mock("@/components/editor/chrome/wrapper/useOpenAiAssistant", () => ({
|
|
46
|
+
useOpenAiAssistant: () => vi.fn(),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const CELL_ID = cellId("test-cell");
|
|
50
|
+
const STABLE_ID = "stable/test" as StableSessionId;
|
|
51
|
+
const AGENT: Attachment = {
|
|
52
|
+
id: "a1",
|
|
53
|
+
kind: "agent",
|
|
54
|
+
name: "Pi",
|
|
55
|
+
since: "2026-10-14T00:00:00Z",
|
|
56
|
+
notebook_id: "nb-test",
|
|
57
|
+
identity: "pi-thread",
|
|
58
|
+
};
|
|
59
|
+
const ROUTE = "/pair/notebooks/stable%2Ftest/messages";
|
|
60
|
+
const MESSAGE = "df is not defined";
|
|
61
|
+
const CODE = "df.head()";
|
|
62
|
+
const REPORT: CellOutputMessage<ErrorMessageOutput> = {
|
|
63
|
+
type: "cell-output",
|
|
64
|
+
cell_id: CELL_ID,
|
|
65
|
+
code: CODE,
|
|
66
|
+
output: {
|
|
67
|
+
type: "error",
|
|
68
|
+
instructions: ERROR_MESSAGE_INSTRUCTIONS,
|
|
69
|
+
message: MESSAGE,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const ERRORS: MarimoError[] = [
|
|
73
|
+
{
|
|
74
|
+
type: "exception",
|
|
75
|
+
exception_type: "NameError",
|
|
76
|
+
raising_cell: null,
|
|
77
|
+
msg: MESSAGE,
|
|
78
|
+
traceback: null,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
function renderButton({
|
|
83
|
+
preview = true,
|
|
84
|
+
attachments = [AGENT],
|
|
85
|
+
stableId = STABLE_ID,
|
|
86
|
+
content = <SendErrorReportButton getContent={() => REPORT} />,
|
|
87
|
+
}: {
|
|
88
|
+
preview?: boolean;
|
|
89
|
+
attachments?: Attachment[];
|
|
90
|
+
stableId?: StableSessionId | null;
|
|
91
|
+
content?: React.ReactNode;
|
|
92
|
+
} = {}) {
|
|
93
|
+
const store = createStore();
|
|
94
|
+
store.set(connectionAtom, { state: ConnectionState.OPEN });
|
|
95
|
+
store.set(pairConfigAtom, { ...PAIR_CONFIG, hostEnabled: preview });
|
|
96
|
+
store.set(attachmentsAtom, attachments);
|
|
97
|
+
store.set(stableSessionIdAtom, stableId);
|
|
98
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
99
|
+
<Provider store={store}>
|
|
100
|
+
<TooltipProvider>{children}</TooltipProvider>
|
|
101
|
+
</Provider>
|
|
102
|
+
);
|
|
103
|
+
return { store, ...render(content, { wrapper }) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function send() {
|
|
107
|
+
fireEvent.click(screen.getByRole("button", { name: "Send error to agent" }));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe("SendErrorReportButton", () => {
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
vi.clearAllMocks();
|
|
113
|
+
vi.mocked(API.post).mockResolvedValue("");
|
|
114
|
+
vi.mocked(toast).mockReturnValue({
|
|
115
|
+
id: "notice",
|
|
116
|
+
dismiss: vi.fn(),
|
|
117
|
+
update: vi.fn(),
|
|
118
|
+
upsert: vi.fn(),
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("hides when host is disabled without building content", () => {
|
|
123
|
+
const getContent = vi.fn(() => REPORT);
|
|
124
|
+
expect(
|
|
125
|
+
renderButton({
|
|
126
|
+
preview: false,
|
|
127
|
+
content: <SendErrorReportButton getContent={getContent} />,
|
|
128
|
+
}).container,
|
|
129
|
+
).toBeEmptyDOMElement();
|
|
130
|
+
expect(getContent).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it.each([
|
|
134
|
+
{ attachments: [] },
|
|
135
|
+
{ attachments: [{ ...AGENT, kind: "client" as const }] },
|
|
136
|
+
{ stableId: null },
|
|
137
|
+
])("disables sending without an agent or stable ID: %j", (options) => {
|
|
138
|
+
renderButton(options);
|
|
139
|
+
expect(screen.getByRole("button")).toBeDisabled();
|
|
140
|
+
send();
|
|
141
|
+
expect(API.post).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("builds content on click and sends it to all agents", async () => {
|
|
145
|
+
const getContent = vi.fn(() => REPORT);
|
|
146
|
+
renderButton({
|
|
147
|
+
attachments: [AGENT, { ...AGENT, id: "a2", name: "Claude Code" }],
|
|
148
|
+
content: <SendErrorReportButton getContent={getContent} />,
|
|
149
|
+
});
|
|
150
|
+
expect(getContent).not.toHaveBeenCalled();
|
|
151
|
+
send();
|
|
152
|
+
await waitFor(() =>
|
|
153
|
+
expect(toast).toHaveBeenCalledWith({
|
|
154
|
+
title: "Sent to Pi, Claude Code",
|
|
155
|
+
duration: 3000,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
expect(API.post).toHaveBeenCalledWith(ROUTE, REPORT);
|
|
159
|
+
expect(getContent).toHaveBeenCalledOnce();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("prevents duplicate sends while a request is pending", async () => {
|
|
163
|
+
const deferred = new Deferred<string>();
|
|
164
|
+
vi.mocked(API.post).mockReturnValue(deferred.promise);
|
|
165
|
+
renderButton();
|
|
166
|
+
send();
|
|
167
|
+
expect(screen.getByRole("button")).toBeDisabled();
|
|
168
|
+
send();
|
|
169
|
+
expect(API.post).toHaveBeenCalledOnce();
|
|
170
|
+
await act(async () => deferred.resolve(""));
|
|
171
|
+
expect(screen.getByRole("button")).toBeEnabled();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("disables the button after the last agent disconnects", () => {
|
|
175
|
+
const { store } = renderButton();
|
|
176
|
+
act(() => store.set(attachmentsAtom, []));
|
|
177
|
+
expect(screen.getByRole("button")).toBeDisabled();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it.each([
|
|
181
|
+
{ state: ConnectionState.CONNECTING },
|
|
182
|
+
{ state: ConnectionState.CLOSING },
|
|
183
|
+
{ state: ConnectionState.NOT_STARTED },
|
|
184
|
+
{
|
|
185
|
+
state: ConnectionState.CLOSED,
|
|
186
|
+
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
187
|
+
reason: "offline",
|
|
188
|
+
},
|
|
189
|
+
])(
|
|
190
|
+
"does not send from a stale snapshot while the browser is %j",
|
|
191
|
+
(connection) => {
|
|
192
|
+
const { store } = renderButton();
|
|
193
|
+
act(() => store.set(connectionAtom, connection));
|
|
194
|
+
expect(screen.getByRole("button")).toBeDisabled();
|
|
195
|
+
send();
|
|
196
|
+
expect(API.post).not.toHaveBeenCalled();
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
it.each([
|
|
201
|
+
[new HTTPError(409, "Conflict"), "No agent attached"],
|
|
202
|
+
[new HTTPError(500, "Server error"), "Could not send error"],
|
|
203
|
+
])("reports request failures and permits retry", async (error, title) => {
|
|
204
|
+
vi.mocked(API.post).mockRejectedValueOnce(error);
|
|
205
|
+
renderButton();
|
|
206
|
+
send();
|
|
207
|
+
await waitFor(() =>
|
|
208
|
+
expect(toast).toHaveBeenCalledWith({
|
|
209
|
+
variant: "danger",
|
|
210
|
+
title,
|
|
211
|
+
description: error.message,
|
|
212
|
+
duration: 5000,
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
expect(screen.getByRole("button")).toBeEnabled();
|
|
216
|
+
send();
|
|
217
|
+
await waitFor(() =>
|
|
218
|
+
expect(toast).toHaveBeenCalledWith({
|
|
219
|
+
title: "Sent to Pi",
|
|
220
|
+
duration: 3000,
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
expect(API.post).toHaveBeenCalledTimes(2);
|
|
224
|
+
expect(API.post).toHaveBeenLastCalledWith(ROUTE, REPORT);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("sends captured code and full traceback from the error output", async () => {
|
|
228
|
+
const traceback = "<pre>Traceback\nNameError: df is not defined</pre>";
|
|
229
|
+
renderButton({
|
|
230
|
+
content: (
|
|
231
|
+
<OutputRenderer
|
|
232
|
+
cellId={CELL_ID}
|
|
233
|
+
message={{
|
|
234
|
+
channel: "marimo-error",
|
|
235
|
+
mimetype: "application/vnd.marimo+error",
|
|
236
|
+
code: CODE,
|
|
237
|
+
data: [{ ...ERRORS[0], traceback }],
|
|
238
|
+
}}
|
|
239
|
+
/>
|
|
240
|
+
),
|
|
241
|
+
});
|
|
242
|
+
send();
|
|
243
|
+
await waitFor(() =>
|
|
244
|
+
expect(API.post).toHaveBeenCalledWith(ROUTE, {
|
|
245
|
+
...REPORT,
|
|
246
|
+
output: {
|
|
247
|
+
type: "error",
|
|
248
|
+
instructions: ERROR_MESSAGE_INSTRUCTIONS,
|
|
249
|
+
message: MESSAGE,
|
|
250
|
+
traceback: "Traceback\nNameError: df is not defined",
|
|
251
|
+
},
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("sends a formatter error beside a normal main output", async () => {
|
|
257
|
+
const { store } = renderButton({
|
|
258
|
+
content: (
|
|
259
|
+
<OutputRenderer
|
|
260
|
+
cellId={CELL_ID}
|
|
261
|
+
message={{
|
|
262
|
+
channel: "stderr",
|
|
263
|
+
mimetype: "application/vnd.marimo+traceback",
|
|
264
|
+
code: CODE,
|
|
265
|
+
data: "<pre>Traceback\nValueError: bad <value></pre>",
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
268
|
+
),
|
|
269
|
+
});
|
|
270
|
+
store.set(
|
|
271
|
+
notebookAtom,
|
|
272
|
+
MockNotebook.notebookState({
|
|
273
|
+
cellData: { [CELL_ID]: { code: "later edit" } },
|
|
274
|
+
cellRuntime: {
|
|
275
|
+
[CELL_ID]: {
|
|
276
|
+
output: {
|
|
277
|
+
channel: "output",
|
|
278
|
+
mimetype: "text/plain",
|
|
279
|
+
data: "<Broken formatter>",
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
send();
|
|
286
|
+
await waitFor(() =>
|
|
287
|
+
expect(API.post).toHaveBeenCalledWith(ROUTE, {
|
|
288
|
+
...REPORT,
|
|
289
|
+
output: {
|
|
290
|
+
type: "error",
|
|
291
|
+
instructions: ERROR_MESSAGE_INSTRUCTIONS,
|
|
292
|
+
message: "ValueError: bad <value>",
|
|
293
|
+
traceback: "Traceback\nValueError: bad <value>",
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("sends plain code-mode traceback text without parsing it as HTML", async () => {
|
|
300
|
+
const traceback =
|
|
301
|
+
' File "<cell>", line 1\nValueError: bad <value> & <other>';
|
|
302
|
+
renderButton({
|
|
303
|
+
content: (
|
|
304
|
+
<OutputRenderer
|
|
305
|
+
cellId={CELL_ID}
|
|
306
|
+
message={{
|
|
307
|
+
channel: "stderr",
|
|
308
|
+
mimetype: "application/vnd.marimo+traceback",
|
|
309
|
+
traceback_format: "text",
|
|
310
|
+
code: CODE,
|
|
311
|
+
data: traceback,
|
|
312
|
+
}}
|
|
313
|
+
/>
|
|
314
|
+
),
|
|
315
|
+
});
|
|
316
|
+
expect(screen.getByText(/ValueError: bad <value>/)).toHaveTextContent(
|
|
317
|
+
"<cell>",
|
|
318
|
+
);
|
|
319
|
+
send();
|
|
320
|
+
await waitFor(() =>
|
|
321
|
+
expect(API.post).toHaveBeenCalledWith(ROUTE, {
|
|
322
|
+
...REPORT,
|
|
323
|
+
output: {
|
|
324
|
+
type: "error",
|
|
325
|
+
instructions: ERROR_MESSAGE_INSTRUCTIONS,
|
|
326
|
+
message: "ValueError: bad <value> & <other>",
|
|
327
|
+
traceback,
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("sends the restored exception type and complete traceback", async () => {
|
|
334
|
+
const state = notebookStateFromSession(
|
|
335
|
+
{
|
|
336
|
+
version: "1",
|
|
337
|
+
metadata: { marimo_version: "1", script_metadata_hash: null },
|
|
338
|
+
cells: [
|
|
339
|
+
{
|
|
340
|
+
id: CELL_ID,
|
|
341
|
+
code_hash: null,
|
|
342
|
+
console: [],
|
|
343
|
+
outputs: [
|
|
344
|
+
{
|
|
345
|
+
type: "error",
|
|
346
|
+
ename: "ValueError",
|
|
347
|
+
evalue: "cached failure",
|
|
348
|
+
traceback: [
|
|
349
|
+
"<pre>Traceback",
|
|
350
|
+
" failed()\n",
|
|
351
|
+
"ValueError: cached failure</pre>",
|
|
352
|
+
],
|
|
353
|
+
code: CODE,
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
null,
|
|
360
|
+
);
|
|
361
|
+
const message = state?.cellRuntime[CELL_ID].output;
|
|
362
|
+
expect(message).toBeDefined();
|
|
363
|
+
renderButton({
|
|
364
|
+
content: message && <OutputRenderer cellId={CELL_ID} message={message} />,
|
|
365
|
+
});
|
|
366
|
+
send();
|
|
367
|
+
await waitFor(() =>
|
|
368
|
+
expect(API.post).toHaveBeenCalledWith(ROUTE, {
|
|
369
|
+
...REPORT,
|
|
370
|
+
output: {
|
|
371
|
+
type: "error",
|
|
372
|
+
instructions: ERROR_MESSAGE_INSTRUCTIONS,
|
|
373
|
+
message: "cached failure",
|
|
374
|
+
traceback: "Traceback\n failed()\nValueError: cached failure",
|
|
375
|
+
},
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
expect(message?.data).toEqual([
|
|
379
|
+
expect.objectContaining({ exception_type: "ValueError" }),
|
|
380
|
+
]);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("sends updated content after the error changes", async () => {
|
|
384
|
+
const { rerender } = renderButton();
|
|
385
|
+
const updated = {
|
|
386
|
+
...REPORT,
|
|
387
|
+
output: { type: "error", message: "Another error" },
|
|
388
|
+
};
|
|
389
|
+
rerender(<SendErrorReportButton getContent={() => updated} />);
|
|
390
|
+
send();
|
|
391
|
+
await waitFor(() => expect(API.post).toHaveBeenCalledWith(ROUTE, updated));
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it.each([
|
|
395
|
+
"Hello agent",
|
|
396
|
+
{ lens: { selection: [1, 2], chart: "scatter" } },
|
|
397
|
+
[1, "two"],
|
|
398
|
+
null,
|
|
399
|
+
])("accepts arbitrary producer content: %j", async (content) => {
|
|
400
|
+
renderButton({
|
|
401
|
+
content: <SendErrorReportButton getContent={() => content} />,
|
|
402
|
+
});
|
|
403
|
+
send();
|
|
404
|
+
await waitFor(() => expect(API.post).toHaveBeenCalledWith(ROUTE, content));
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it.each([false, true])(
|
|
408
|
+
"retains failed code after edits and a queued run (%s)",
|
|
409
|
+
async (queue) => {
|
|
410
|
+
const output: OutputMessage = {
|
|
411
|
+
channel: "marimo-error",
|
|
412
|
+
mimetype: "application/vnd.marimo+error",
|
|
413
|
+
data: ERRORS,
|
|
414
|
+
code: CODE,
|
|
415
|
+
};
|
|
416
|
+
// Render from the store so reducer changes reach the button.
|
|
417
|
+
const StoreOutput = () => {
|
|
418
|
+
const message = useAtomValue(notebookAtom).cellRuntime[CELL_ID]?.output;
|
|
419
|
+
return message ? (
|
|
420
|
+
<OutputRenderer cellId={CELL_ID} message={message} />
|
|
421
|
+
) : null;
|
|
422
|
+
};
|
|
423
|
+
const { store } = renderButton({ content: <StoreOutput /> });
|
|
424
|
+
const actions = exportedForTesting.createActions((action) => {
|
|
425
|
+
store.set(notebookAtom, (state) =>
|
|
426
|
+
exportedForTesting.reducer(state, action),
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
act(() => {
|
|
430
|
+
store.set(
|
|
431
|
+
notebookAtom,
|
|
432
|
+
MockNotebook.notebookState({
|
|
433
|
+
cellData: { [CELL_ID]: { code: CODE, lastCodeRun: CODE } },
|
|
434
|
+
cellRuntime: { [CELL_ID]: { output } },
|
|
435
|
+
}),
|
|
436
|
+
);
|
|
437
|
+
actions.updateCellCode({
|
|
438
|
+
cellId: CELL_ID,
|
|
439
|
+
code: "print('edited')",
|
|
440
|
+
formattingChange: false,
|
|
441
|
+
});
|
|
442
|
+
if (queue) {
|
|
443
|
+
actions.prepareForRun({ cellId: CELL_ID });
|
|
444
|
+
actions.handleCellMessage({
|
|
445
|
+
cell_id: CELL_ID,
|
|
446
|
+
status: "queued",
|
|
447
|
+
timestamp: 2,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
send();
|
|
452
|
+
await waitFor(() => expect(API.post).toHaveBeenCalledWith(ROUTE, REPORT));
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
it("does not borrow an old console traceback for a cancelled descendant", async () => {
|
|
457
|
+
const cancelled: MarimoError[] = [
|
|
458
|
+
{
|
|
459
|
+
type: "exception",
|
|
460
|
+
exception_type: "ValueError",
|
|
461
|
+
msg: "Ancestor failed",
|
|
462
|
+
raising_cell: cellId("ancestor"),
|
|
463
|
+
traceback: null,
|
|
464
|
+
},
|
|
465
|
+
];
|
|
466
|
+
const { store } = renderButton({
|
|
467
|
+
content: (
|
|
468
|
+
<MarimoErrorOutput cellId={CELL_ID} code="x + 1" errors={cancelled} />
|
|
469
|
+
),
|
|
470
|
+
});
|
|
471
|
+
store.set(
|
|
472
|
+
notebookAtom,
|
|
473
|
+
MockNotebook.notebookState({
|
|
474
|
+
cellData: {
|
|
475
|
+
[CELL_ID]: { code: "unrelated edit", lastCodeRun: "older run" },
|
|
476
|
+
},
|
|
477
|
+
cellRuntime: {
|
|
478
|
+
[CELL_ID]: {
|
|
479
|
+
consoleOutputs: [
|
|
480
|
+
{
|
|
481
|
+
channel: "stderr",
|
|
482
|
+
mimetype: "application/vnd.marimo+traceback",
|
|
483
|
+
data: "<pre>Old unrelated traceback</pre>",
|
|
484
|
+
code: "older run",
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
}),
|
|
490
|
+
);
|
|
491
|
+
send();
|
|
492
|
+
await waitFor(() =>
|
|
493
|
+
expect(API.post).toHaveBeenCalledWith(ROUTE, {
|
|
494
|
+
type: "cell-output",
|
|
495
|
+
cell_id: CELL_ID,
|
|
496
|
+
code: "x + 1",
|
|
497
|
+
output: {
|
|
498
|
+
type: "error",
|
|
499
|
+
instructions: ERROR_MESSAGE_INSTRUCTIONS,
|
|
500
|
+
message: "Ancestor failed",
|
|
501
|
+
},
|
|
502
|
+
}),
|
|
503
|
+
);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("sends an interruption with its attempted code", async () => {
|
|
507
|
+
renderButton({
|
|
508
|
+
content: (
|
|
509
|
+
<MarimoErrorOutput
|
|
510
|
+
cellId={CELL_ID}
|
|
511
|
+
code="slow()"
|
|
512
|
+
errors={[{ type: "interruption" }]}
|
|
513
|
+
/>
|
|
514
|
+
),
|
|
515
|
+
});
|
|
516
|
+
send();
|
|
517
|
+
await waitFor(() =>
|
|
518
|
+
expect(API.post).toHaveBeenCalledWith(ROUTE, {
|
|
519
|
+
type: "cell-output",
|
|
520
|
+
cell_id: CELL_ID,
|
|
521
|
+
code: "slow()",
|
|
522
|
+
output: {
|
|
523
|
+
type: "error",
|
|
524
|
+
instructions: ERROR_MESSAGE_INSTRUCTIONS,
|
|
525
|
+
message: "This cell was interrupted and needs to be re-run",
|
|
526
|
+
},
|
|
527
|
+
}),
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("hides error actions without a cell identity or message", () => {
|
|
532
|
+
renderButton({
|
|
533
|
+
content: (
|
|
534
|
+
<>
|
|
535
|
+
<MarimoErrorOutput cellId={undefined} errors={ERRORS} />
|
|
536
|
+
<MarimoErrorOutput cellId={CELL_ID} errors={[]} />
|
|
537
|
+
<MarimoTracebackOutput
|
|
538
|
+
cellId={undefined}
|
|
539
|
+
traceback="<pre>ValueError: bad</pre>"
|
|
540
|
+
/>
|
|
541
|
+
</>
|
|
542
|
+
),
|
|
543
|
+
});
|
|
544
|
+
expect(
|
|
545
|
+
screen.queryByRole("button", { name: "Send error to agent" }),
|
|
546
|
+
).not.toBeInTheDocument();
|
|
547
|
+
});
|
|
548
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useAtomValue } from "jotai";
|
|
4
|
+
import { SendIcon } from "lucide-react";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
agentAttachmentsAtom,
|
|
9
|
+
attachmentLabel,
|
|
10
|
+
} from "@/core/attachments/state";
|
|
11
|
+
import { agentToast } from "@/core/attachments/toast";
|
|
12
|
+
import { pairConfigAtom } from "@/core/config/pair";
|
|
13
|
+
import { stableSessionIdAtom } from "@/core/kernel/session";
|
|
14
|
+
import { API } from "@/core/network/api";
|
|
15
|
+
import { HTTPError, prettyError } from "@/utils/errors";
|
|
16
|
+
|
|
17
|
+
export function SendErrorReportButton({
|
|
18
|
+
getContent,
|
|
19
|
+
}: {
|
|
20
|
+
getContent: () => unknown;
|
|
21
|
+
}) {
|
|
22
|
+
const hostEnabled = useAtomValue(pairConfigAtom)?.hostEnabled;
|
|
23
|
+
const agents = useAtomValue(agentAttachmentsAtom);
|
|
24
|
+
const stableSessionId = useAtomValue(stableSessionIdAtom);
|
|
25
|
+
const [sending, setSending] = useState(false);
|
|
26
|
+
|
|
27
|
+
if (!hostEnabled) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const send = async () => {
|
|
32
|
+
if (sending || agents.length === 0 || !stableSessionId) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setSending(true);
|
|
36
|
+
try {
|
|
37
|
+
await API.post<unknown, string>(
|
|
38
|
+
`/pair/notebooks/${encodeURIComponent(stableSessionId)}/messages`,
|
|
39
|
+
getContent(),
|
|
40
|
+
);
|
|
41
|
+
agentToast({
|
|
42
|
+
title: `Sent to ${agents.map(attachmentLabel).join(", ")}`,
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
agentToast({
|
|
46
|
+
variant: "danger",
|
|
47
|
+
title:
|
|
48
|
+
error instanceof HTTPError && error.status === 409
|
|
49
|
+
? "No agent attached"
|
|
50
|
+
: "Could not send error",
|
|
51
|
+
description: prettyError(error),
|
|
52
|
+
});
|
|
53
|
+
} finally {
|
|
54
|
+
setSending(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Button
|
|
60
|
+
size="xs"
|
|
61
|
+
variant="outline"
|
|
62
|
+
disabled={sending || agents.length === 0 || !stableSessionId}
|
|
63
|
+
onClick={send}
|
|
64
|
+
>
|
|
65
|
+
<SendIcon className="h-3 w-3 mr-2" />
|
|
66
|
+
Send error to agent
|
|
67
|
+
</Button>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -21,8 +21,10 @@ import { Button } from "@/components/ui/button";
|
|
|
21
21
|
import { Kbd } from "@/components/ui/kbd";
|
|
22
22
|
import { ExternalLink } from "@/components/ui/links";
|
|
23
23
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
24
|
+
import { buildErrorReport } from "@/core/cells/error-report";
|
|
24
25
|
import type { CellId } from "@/core/cells/ids";
|
|
25
26
|
import { resolvedMarimoConfigAtom } from "@/core/config/config";
|
|
27
|
+
import { viewStateAtom } from "@/core/mode";
|
|
26
28
|
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
27
29
|
import { splitMangledLocals } from "@/utils/local-variables";
|
|
28
30
|
import type { MarimoError } from "../../../core/kernel/messages";
|
|
@@ -32,6 +34,7 @@ import { openPackageManager } from "../chrome/panels/packages-utils";
|
|
|
32
34
|
import { useChromeActions } from "../chrome/state";
|
|
33
35
|
import { AutoFixButton } from "../errors/auto-fix";
|
|
34
36
|
import { MangledSegments } from "../errors/mangled-local-chip";
|
|
37
|
+
import { SendErrorReportButton } from "../errors/send-error-report-button";
|
|
35
38
|
import { CellLinkError } from "../links/cell-link";
|
|
36
39
|
import { processTextForUrls } from "./console/text-rendering";
|
|
37
40
|
|
|
@@ -93,6 +96,7 @@ const Tip = (props: {
|
|
|
93
96
|
interface Props {
|
|
94
97
|
cellId: CellId | undefined;
|
|
95
98
|
errors: MarimoError[];
|
|
99
|
+
code?: string;
|
|
96
100
|
className?: string;
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -102,6 +106,7 @@ interface Props {
|
|
|
102
106
|
export const MarimoErrorOutput = ({
|
|
103
107
|
errors,
|
|
104
108
|
cellId,
|
|
109
|
+
code,
|
|
105
110
|
className,
|
|
106
111
|
}: Props): JSX.Element => {
|
|
107
112
|
const chromeActions = useChromeActions();
|
|
@@ -111,6 +116,9 @@ export const MarimoErrorOutput = ({
|
|
|
111
116
|
// only helpful when outputs are displayed above the editor.
|
|
112
117
|
const showConsoleHint =
|
|
113
118
|
useAtomValue(resolvedMarimoConfigAtom).display.cell_output === "above";
|
|
119
|
+
// The editor shows tracebacks in the console area, so the inline copy is
|
|
120
|
+
// for views without a console: published apps and present mode.
|
|
121
|
+
const showInlineTraceback = useAtomValue(viewStateAtom).mode !== "edit";
|
|
114
122
|
|
|
115
123
|
let titleContents: string | null =
|
|
116
124
|
"This cell wasn't run because it has errors";
|
|
@@ -208,7 +216,6 @@ export const MarimoErrorOutput = ({
|
|
|
208
216
|
(e): e is Extract<MarimoError, { type: "sql-error" }> =>
|
|
209
217
|
e.type === "sql-error",
|
|
210
218
|
);
|
|
211
|
-
|
|
212
219
|
const openScratchpad = () => {
|
|
213
220
|
chromeActions.openApplication("scratchpad");
|
|
214
221
|
};
|
|
@@ -558,7 +565,9 @@ export const MarimoErrorOutput = ({
|
|
|
558
565
|
<p className="text-muted-foreground">
|
|
559
566
|
{processTextForUrls(error.msg, `exception-${idx}`)}
|
|
560
567
|
</p>
|
|
561
|
-
{
|
|
568
|
+
{showInlineTraceback &&
|
|
569
|
+
"traceback" in error &&
|
|
570
|
+
error.traceback ? (
|
|
562
571
|
<CollapsibleTraceback traceback={error.traceback} />
|
|
563
572
|
) : (
|
|
564
573
|
showConsoleHint && (
|
|
@@ -572,9 +581,11 @@ export const MarimoErrorOutput = ({
|
|
|
572
581
|
<div>
|
|
573
582
|
{processTextForUrls(error.msg, `exception-${idx}`)}
|
|
574
583
|
<CellLinkError cellId={error.raising_cell} />
|
|
575
|
-
{
|
|
576
|
-
|
|
577
|
-
|
|
584
|
+
{showInlineTraceback &&
|
|
585
|
+
"traceback" in error &&
|
|
586
|
+
error.traceback && (
|
|
587
|
+
<CollapsibleTraceback traceback={error.traceback} />
|
|
588
|
+
)}
|
|
578
589
|
</div>
|
|
579
590
|
)}
|
|
580
591
|
</li>
|
|
@@ -728,6 +739,11 @@ export const MarimoErrorOutput = ({
|
|
|
728
739
|
>
|
|
729
740
|
{title}
|
|
730
741
|
<div className="flex flex-col gap-4">{renderMessages()}</div>
|
|
742
|
+
{cellId && errors.length > 0 && (
|
|
743
|
+
<SendErrorReportButton
|
|
744
|
+
getContent={() => buildErrorReport(cellId, { code, errors })}
|
|
745
|
+
/>
|
|
746
|
+
)}
|
|
731
747
|
</Alert>
|
|
732
748
|
);
|
|
733
749
|
};
|