@marimo-team/islands 0.23.15-dev5 → 0.23.15-dev50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ConnectedDataExplorerComponent-CU4fZJzG.js → ConnectedDataExplorerComponent-Cmq_9LVS.js} +4 -4
- package/dist/{ErrorBoundary-DE6tzZf-.js → ErrorBoundary-CnMJ8jR9.js} +1 -1
- package/dist/{ImageComparisonComponent-CHrI72em.js → ImageComparisonComponent-1i5IBhon.js} +173 -129
- package/dist/assets/__vite-browser-external-CjNAy01L.js +1 -0
- package/dist/assets/worker-B3XPCb6y.js +98 -0
- package/dist/{chat-ui-DinOvbWu.js → chat-ui-Dy0arY8E.js} +3111 -3111
- package/dist/{code-visibility-DBuB5MVY.js → code-visibility-B5xe1Xp2.js} +946 -864
- package/dist/{constants-T20xxyNf.js → debounce-BOD3DbfP.js} +1 -24
- package/dist/{formats-Dzx4J_z1.js → formats-CdvkxTS6.js} +1 -1
- package/dist/{glide-data-editor-CjTu7ukN.js → glide-data-editor-D5w23DvC.js} +3 -3
- package/dist/{html-to-image-_wGfk8V-.js → html-to-image-f-kPEnn0.js} +2291 -2250
- package/dist/{input-DtsN7xm-.js → input-CF5Sgjba.js} +2 -2
- package/dist/main.js +1302 -1202
- package/dist/{mermaid-BYqXy_NE.js → mermaid-BaZJ2mds.js} +2 -2
- package/dist/{process-output-Mh4UrjwM.js → process-output-O9FeFAVM.js} +1 -1
- package/dist/{reveal-component-DfOZrkd6.js → reveal-component-BkZp_qsV.js} +618 -605
- package/dist/{spec-Cz-Bj1JI.js → spec-DxIove3q.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-CWNNlFEX.js → toDate-CEOdz9nC.js} +14 -5
- package/dist/{useAsyncData-KfHB8wQR.js → useAsyncData-Cs0mpJTy.js} +1 -1
- package/dist/{useDeepCompareMemoize-nJMtxhm4.js → useDeepCompareMemoize-DoOZicGb.js} +1 -1
- package/dist/{useLifecycle-DegSo0lV.js → useLifecycle-DR_F1d7F.js} +1 -1
- package/dist/{useTheme-6eZ3GOTS.js → useTheme-BA-8MM7N.js} +63 -27
- package/dist/{vega-component-DzyyM9fc.js → vega-component-BN0fz-QA.js} +6 -6
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +3 -0
- package/src/components/chat/__tests__/chat-utils.test.ts +244 -1
- package/src/components/chat/__tests__/message-queue.test.tsx +121 -0
- package/src/components/chat/chat-panel.tsx +196 -67
- package/src/components/chat/chat-utils.ts +111 -2
- package/src/components/editor/SortableCell.tsx +6 -2
- package/src/components/editor/__tests__/output-persistence.test.tsx +241 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +153 -0
- package/src/components/editor/actions/pair-with-agent-commands.ts +109 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +20 -64
- package/src/components/editor/actions/useNotebookActions.tsx +8 -0
- package/src/components/editor/ai/add-cell-with-ai.tsx +14 -5
- package/src/components/editor/ai/ai-completion-editor.tsx +4 -1
- package/src/components/editor/cell/cell-context-menu.tsx +7 -0
- package/src/components/editor/chrome/components/__tests__/feedback-button.test.tsx +221 -0
- package/src/components/editor/chrome/components/feedback-button.tsx +308 -77
- package/src/components/editor/chrome/panels/packages-panel.tsx +11 -1
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +105 -0
- package/src/components/editor/columns/cell-column.tsx +34 -4
- package/src/components/editor/columns/sortable-column.tsx +24 -4
- package/src/components/editor/file-tree/download.ts +46 -0
- package/src/components/editor/file-tree/file-explorer.tsx +3 -21
- package/src/components/editor/file-tree/file-viewer.tsx +4 -27
- package/src/components/editor/navigation/__tests__/navigation.test.ts +33 -0
- package/src/components/editor/navigation/navigation.ts +8 -1
- package/src/components/editor/notebook-cell.tsx +203 -106
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +66 -0
- package/src/components/editor/renderers/cell-array.tsx +26 -13
- package/src/components/editor/renderers/cells-renderer.tsx +8 -2
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +29 -0
- package/src/components/editor/renderers/slides-layout/types.ts +4 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/components/slides/reveal-component.tsx +37 -4
- package/src/components/slides/slide-form.tsx +72 -0
- package/src/components/ui/toast.tsx +11 -3
- package/src/components/ui/toaster.tsx +65 -9
- package/src/core/ai/context/providers/error.ts +6 -158
- package/src/core/cells/__tests__/utils.test.ts +59 -0
- package/src/core/cells/utils.ts +51 -0
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +97 -3
- package/src/core/codemirror/completion/signature-hint.ts +78 -11
- package/src/core/constants.ts +8 -0
- package/src/core/diagnostics/__tests__/issue-details.test.ts +173 -0
- package/src/core/diagnostics/issue-details.ts +139 -0
- package/src/core/errors/__tests__/error-entries.test.ts +25 -0
- package/src/core/errors/error-entries.ts +176 -0
- package/src/core/islands/__tests__/bridge.test.ts +341 -46
- package/src/core/islands/__tests__/main.test.ts +176 -0
- package/src/core/islands/__tests__/parse.test.ts +105 -0
- package/src/core/islands/bootstrap.ts +8 -3
- package/src/core/islands/bridge.ts +117 -23
- package/src/core/islands/components/__tests__/web-components.test.tsx +214 -0
- package/src/core/islands/components/web-components.tsx +76 -15
- package/src/core/islands/constants.ts +1 -0
- package/src/core/islands/main.ts +69 -3
- package/src/core/islands/parse.ts +70 -23
- package/src/core/islands/worker/__tests__/controller.test.ts +173 -0
- package/src/core/islands/worker/worker.tsx +145 -57
- package/src/core/network/__tests__/requests-lazy.test.ts +18 -0
- package/src/core/network/__tests__/requests-network.test.ts +7 -0
- package/src/core/network/requests-lazy.ts +13 -1
- package/src/core/network/requests-network.ts +3 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +3 -0
- package/src/core/runtime/__tests__/runtime.test.ts +64 -0
- package/src/core/runtime/runtime.ts +30 -10
- package/src/core/wasm/bridge.ts +8 -0
- package/src/core/wasm/worker/bootstrap.ts +113 -20
- package/src/core/wasm/worker/types.ts +2 -0
- package/src/css/app/Cell.css +10 -0
- package/src/hooks/__tests__/useHotkey.test.tsx +88 -0
- package/src/hooks/useHotkey.ts +29 -4
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +6 -9
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +3 -1
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +32 -61
- package/src/plugins/impl/anywidget/model-proxy.ts +0 -13
- package/src/plugins/impl/anywidget/runtime.ts +1 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +8 -34
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +53 -2
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +71 -0
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +71 -3
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -0
- package/src/theme/__tests__/useTheme.test.ts +68 -0
- package/src/theme/useTheme.ts +16 -1
- package/dist/assets/__vite-browser-external-BQCLNwri.js +0 -1
- package/dist/assets/worker-DAWRHcPq.js +0 -73
|
@@ -194,9 +194,12 @@ export const AiCompletionEditor: React.FC<Props> = ({
|
|
|
194
194
|
setCompletion("");
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
+
// Reject discards the suggestion but keeps the prompt open for refinement.
|
|
197
198
|
const handleDeclineCompletion = () => {
|
|
198
|
-
|
|
199
|
+
stop();
|
|
199
200
|
setCompletion("");
|
|
201
|
+
setShowInputPrompt(true);
|
|
202
|
+
inputRef.current?.view?.focus();
|
|
200
203
|
};
|
|
201
204
|
|
|
202
205
|
const showCompletionBanner =
|
|
@@ -39,12 +39,18 @@ interface Props extends Pick<
|
|
|
39
39
|
"cellId" | "getEditorView"
|
|
40
40
|
> {
|
|
41
41
|
children: React.ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* If true, the custom context menu is disabled and the native one is used
|
|
44
|
+
* (e.g. while presenting).
|
|
45
|
+
*/
|
|
46
|
+
disabled?: boolean;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export const CellActionsContextMenu = ({
|
|
45
50
|
children,
|
|
46
51
|
cellId,
|
|
47
52
|
getEditorView,
|
|
53
|
+
disabled,
|
|
48
54
|
}: Props) => {
|
|
49
55
|
const cellData = useCellData(cellId);
|
|
50
56
|
const cellRuntime = useCellRuntime(cellId);
|
|
@@ -187,6 +193,7 @@ export const CellActionsContextMenu = ({
|
|
|
187
193
|
return (
|
|
188
194
|
<ContextMenu>
|
|
189
195
|
<ContextMenuTrigger
|
|
196
|
+
disabled={disabled}
|
|
190
197
|
onContextMenu={(evt) => {
|
|
191
198
|
if (evt.target instanceof HTMLImageElement) {
|
|
192
199
|
setImageRightClicked(evt.target);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { Provider } from "jotai";
|
|
5
|
+
import type React from "react";
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
8
|
+
import { Dialog } from "@/components/ui/dialog";
|
|
9
|
+
import { Constants } from "@/core/constants";
|
|
10
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
11
|
+
import { viewStateAtom } from "@/core/mode";
|
|
12
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
13
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
14
|
+
import type { EnvironmentInfo } from "@/core/network/types";
|
|
15
|
+
import { filenameAtom } from "@/core/saving/file-state";
|
|
16
|
+
import { store } from "@/core/state/jotai";
|
|
17
|
+
import { WebSocketState } from "@/core/websocket/types";
|
|
18
|
+
import * as copyModule from "@/utils/copy";
|
|
19
|
+
import { FeedbackModal } from "../feedback-button";
|
|
20
|
+
|
|
21
|
+
vi.mock("@/utils/copy", () => ({
|
|
22
|
+
copyToClipboard: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const environment: EnvironmentInfo = {
|
|
26
|
+
marimo: "1.2.3",
|
|
27
|
+
editable: false,
|
|
28
|
+
location: "~/.venv/site-packages/marimo",
|
|
29
|
+
OS: "Darwin",
|
|
30
|
+
"OS Version": "25.0",
|
|
31
|
+
Processor: "arm",
|
|
32
|
+
"Python Version": "3.12.9",
|
|
33
|
+
Locale: "en_US",
|
|
34
|
+
Binaries: { Browser: "chrome 140", Node: "v22", uv: "0.11" },
|
|
35
|
+
Dependencies: { click: "8.4.2" },
|
|
36
|
+
"Optional Dependencies": { pandas: "3.0.0" },
|
|
37
|
+
"Experimental Flags": {},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function wrapper({ children }: { children: React.ReactNode }) {
|
|
41
|
+
return (
|
|
42
|
+
<Provider store={store}>
|
|
43
|
+
<TooltipProvider>
|
|
44
|
+
<Dialog open={true}>{children}</Dialog>
|
|
45
|
+
</TooltipProvider>
|
|
46
|
+
</Provider>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("FeedbackModal issue reporting", () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
store.set(requestClientAtom, MockRequestClient.create());
|
|
54
|
+
store.set(viewStateAtom, { mode: "edit", cellAnchor: null });
|
|
55
|
+
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
56
|
+
store.set(filenameAtom, "/project/example.py");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("loads and previews issue environment details", async () => {
|
|
60
|
+
store.set(
|
|
61
|
+
requestClientAtom,
|
|
62
|
+
MockRequestClient.create({
|
|
63
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
67
|
+
|
|
68
|
+
expect(screen.getByText("Loading environment details…")).toBeVisible();
|
|
69
|
+
await screen.findByText("Environment details");
|
|
70
|
+
expect(screen.getByText(/"marimo": "1.2.3"/)).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("toggles the environment preview between show more and show less", async () => {
|
|
74
|
+
store.set(
|
|
75
|
+
requestClientAtom,
|
|
76
|
+
MockRequestClient.create({
|
|
77
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
81
|
+
await screen.findByText("Environment details");
|
|
82
|
+
|
|
83
|
+
const toggle = screen.getByRole("button", { name: "Show more" });
|
|
84
|
+
fireEvent.click(toggle);
|
|
85
|
+
expect(
|
|
86
|
+
screen.getByRole("button", { name: "Show less" }),
|
|
87
|
+
).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("copies partial details when the environment request fails", async () => {
|
|
91
|
+
store.set(
|
|
92
|
+
requestClientAtom,
|
|
93
|
+
MockRequestClient.create({
|
|
94
|
+
getEnvironmentInfo: vi.fn().mockRejectedValue(new Error("offline")),
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
98
|
+
|
|
99
|
+
await screen.findByText("Server environment information unavailable");
|
|
100
|
+
fireEvent.click(screen.getByRole("button", { name: "Copy issue details" }));
|
|
101
|
+
await waitFor(() =>
|
|
102
|
+
expect(copyModule.copyToClipboard).toHaveBeenCalledWith(
|
|
103
|
+
expect.stringContaining("Environment Collection Error"),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("copies raw environment JSON", async () => {
|
|
109
|
+
store.set(
|
|
110
|
+
requestClientAtom,
|
|
111
|
+
MockRequestClient.create({
|
|
112
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
116
|
+
await screen.findByText("Environment details");
|
|
117
|
+
|
|
118
|
+
fireEvent.click(
|
|
119
|
+
screen.getByRole("button", { name: "Copy environment JSON" }),
|
|
120
|
+
);
|
|
121
|
+
await waitFor(() =>
|
|
122
|
+
expect(copyModule.copyToClipboard).toHaveBeenCalledWith(
|
|
123
|
+
expect.stringContaining('"marimo": "1.2.3"'),
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("includes notebook source only after explicit opt-in", async () => {
|
|
129
|
+
const readCode = vi.fn().mockResolvedValue({
|
|
130
|
+
contents: "secret = 'review before posting'",
|
|
131
|
+
});
|
|
132
|
+
store.set(
|
|
133
|
+
requestClientAtom,
|
|
134
|
+
MockRequestClient.create({
|
|
135
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
136
|
+
readCode,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
140
|
+
await screen.findByText("Environment details");
|
|
141
|
+
|
|
142
|
+
expect(readCode).not.toHaveBeenCalled();
|
|
143
|
+
fireEvent.click(
|
|
144
|
+
screen.getByRole("checkbox", { name: "Include full notebook source" }),
|
|
145
|
+
);
|
|
146
|
+
await waitFor(() => expect(readCode).toHaveBeenCalledOnce());
|
|
147
|
+
|
|
148
|
+
fireEvent.click(screen.getByRole("button", { name: "Copy issue details" }));
|
|
149
|
+
await waitFor(() =>
|
|
150
|
+
expect(copyModule.copyToClipboard).toHaveBeenCalledWith(
|
|
151
|
+
expect.stringContaining("Notebook source: example.py"),
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("discards notebook source after unchecking", async () => {
|
|
157
|
+
const readCode = vi.fn().mockResolvedValue({ contents: "x = 1" });
|
|
158
|
+
store.set(
|
|
159
|
+
requestClientAtom,
|
|
160
|
+
MockRequestClient.create({
|
|
161
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
162
|
+
readCode,
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
166
|
+
await screen.findByText("Environment details");
|
|
167
|
+
|
|
168
|
+
const checkbox = screen.getByRole("checkbox", {
|
|
169
|
+
name: "Include full notebook source",
|
|
170
|
+
});
|
|
171
|
+
fireEvent.click(checkbox);
|
|
172
|
+
await waitFor(() => expect(readCode).toHaveBeenCalledOnce());
|
|
173
|
+
fireEvent.click(checkbox);
|
|
174
|
+
|
|
175
|
+
fireEvent.click(screen.getByRole("button", { name: "Copy issue details" }));
|
|
176
|
+
await waitFor(() =>
|
|
177
|
+
expect(copyModule.copyToClipboard).toHaveBeenCalledWith(
|
|
178
|
+
expect.not.stringContaining("Notebook source"),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("disables the notebook option and explains why when disconnected", async () => {
|
|
184
|
+
store.set(connectionAtom, { state: WebSocketState.CONNECTING });
|
|
185
|
+
store.set(
|
|
186
|
+
requestClientAtom,
|
|
187
|
+
MockRequestClient.create({
|
|
188
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
192
|
+
await screen.findByText("Environment details");
|
|
193
|
+
|
|
194
|
+
expect(
|
|
195
|
+
screen.getByRole("checkbox", { name: "Include full notebook source" }),
|
|
196
|
+
).toBeDisabled();
|
|
197
|
+
expect(
|
|
198
|
+
screen.getByText("Connect the notebook to include its source."),
|
|
199
|
+
).toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("prefills the GitHub issue link with the environment once loaded", async () => {
|
|
203
|
+
store.set(
|
|
204
|
+
requestClientAtom,
|
|
205
|
+
MockRequestClient.create({
|
|
206
|
+
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
210
|
+
|
|
211
|
+
const link = screen.getByRole("link", { name: "Open GitHub issue" });
|
|
212
|
+
expect(link).toHaveAttribute("href", Constants.bugReportUrl);
|
|
213
|
+
|
|
214
|
+
await screen.findByText("Environment details");
|
|
215
|
+
await waitFor(() => {
|
|
216
|
+
const href = link.getAttribute("href") ?? "";
|
|
217
|
+
expect(href).toContain("&env=");
|
|
218
|
+
expect(href).toContain(encodeURIComponent('"marimo": "1.2.3"'));
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -4,16 +4,75 @@ import { Slot as SlotPrimitive } from "radix-ui";
|
|
|
4
4
|
|
|
5
5
|
const Slot = SlotPrimitive.Slot;
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { useAtomValue } from "jotai";
|
|
8
|
+
import { CheckIcon, ExternalLinkIcon, TriangleAlertIcon } from "lucide-react";
|
|
9
|
+
import React, { type PropsWithChildren, useMemo, useState } from "react";
|
|
10
|
+
import { CopyClipboardIcon } from "@/components/icons/copy-icon";
|
|
8
11
|
import { useImperativeModal } from "@/components/modal/ImperativeModal";
|
|
12
|
+
import { Button } from "@/components/ui/button";
|
|
13
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
9
14
|
import {
|
|
10
15
|
DialogContent,
|
|
11
16
|
DialogDescription,
|
|
12
17
|
DialogHeader,
|
|
13
18
|
DialogTitle,
|
|
14
19
|
} from "@/components/ui/dialog";
|
|
20
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
15
21
|
import { toast } from "@/components/ui/use-toast";
|
|
22
|
+
import { notebookAtom } from "@/core/cells/cells";
|
|
16
23
|
import { Constants } from "@/core/constants";
|
|
24
|
+
import {
|
|
25
|
+
buildBugReportUrl,
|
|
26
|
+
buildIssueDetails,
|
|
27
|
+
createPartialEnvironment,
|
|
28
|
+
type EnvironmentDiagnostics,
|
|
29
|
+
enrichEnvironment,
|
|
30
|
+
type NotebookSource,
|
|
31
|
+
} from "@/core/diagnostics/issue-details";
|
|
32
|
+
import {
|
|
33
|
+
formatCellError,
|
|
34
|
+
getCellErrorEntries,
|
|
35
|
+
} from "@/core/errors/error-entries";
|
|
36
|
+
import { useNotebookCodeAvailable } from "@/core/meta/code-visibility";
|
|
37
|
+
import { getMarimoVersion } from "@/core/meta/globals";
|
|
38
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
39
|
+
import { useRequestClient } from "@/core/network/requests";
|
|
40
|
+
import { filenameAtom } from "@/core/saving/file-state";
|
|
41
|
+
import { store } from "@/core/state/jotai";
|
|
42
|
+
import { WebSocketState } from "@/core/websocket/types";
|
|
43
|
+
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
44
|
+
import { cn } from "@/utils/cn";
|
|
45
|
+
import { copyToClipboard } from "@/utils/copy";
|
|
46
|
+
|
|
47
|
+
const CollapsiblePreview: React.FC<{ content: string }> = ({ content }) => {
|
|
48
|
+
const [expanded, setExpanded] = useState(false);
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex flex-col gap-1">
|
|
51
|
+
<div className="relative">
|
|
52
|
+
<pre
|
|
53
|
+
className={cn(
|
|
54
|
+
"text-xs bg-muted rounded p-2 overflow-x-auto whitespace-pre-wrap",
|
|
55
|
+
!expanded && "max-h-24 overflow-hidden",
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{content}
|
|
59
|
+
</pre>
|
|
60
|
+
{!expanded && (
|
|
61
|
+
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-10 rounded-b bg-gradient-to-b from-transparent to-muted" />
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
<Button
|
|
65
|
+
type="button"
|
|
66
|
+
variant="link"
|
|
67
|
+
size="xs"
|
|
68
|
+
className="self-start"
|
|
69
|
+
onClick={() => setExpanded((value) => !value)}
|
|
70
|
+
>
|
|
71
|
+
{expanded ? "Show less" : "Show more"}
|
|
72
|
+
</Button>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
17
76
|
|
|
18
77
|
export const FeedbackButton: React.FC<PropsWithChildren> = ({ children }) => {
|
|
19
78
|
const { openModal, closeModal } = useImperativeModal();
|
|
@@ -25,84 +84,256 @@ export const FeedbackButton: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
25
84
|
);
|
|
26
85
|
};
|
|
27
86
|
|
|
28
|
-
const FeedbackModal: React.FC<{
|
|
87
|
+
export const FeedbackModal: React.FC<{
|
|
29
88
|
onClose: () => void;
|
|
30
|
-
}> = (
|
|
89
|
+
}> = () => {
|
|
90
|
+
const { getEnvironmentInfo, readCode } = useRequestClient();
|
|
91
|
+
const environmentRequest = useAsyncData(
|
|
92
|
+
async () => getEnvironmentInfo(),
|
|
93
|
+
[getEnvironmentInfo],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const notebook = useAtomValue(notebookAtom);
|
|
97
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: recompute when the notebook changes
|
|
98
|
+
const errors = useMemo(() => getCellErrorEntries(store), [notebook]);
|
|
99
|
+
|
|
100
|
+
const cells = notebook.cellIds.inOrderIds.map(
|
|
101
|
+
(cellId) => notebook.cellData[cellId],
|
|
102
|
+
);
|
|
103
|
+
const codeAvailable = useNotebookCodeAvailable(cells);
|
|
104
|
+
const filename = useAtomValue(filenameAtom);
|
|
105
|
+
const connection = useAtomValue(connectionAtom);
|
|
106
|
+
const notebookSourceAvailable =
|
|
107
|
+
filename !== null &&
|
|
108
|
+
codeAvailable &&
|
|
109
|
+
connection.state === WebSocketState.OPEN;
|
|
110
|
+
|
|
111
|
+
const [includeNotebook, setIncludeNotebook] = useState(false);
|
|
112
|
+
|
|
113
|
+
const notebookSourceReason = notebookSourceAvailable
|
|
114
|
+
? undefined
|
|
115
|
+
: filename === null
|
|
116
|
+
? "Save the notebook first."
|
|
117
|
+
: !codeAvailable
|
|
118
|
+
? "Notebook source is hidden in this view."
|
|
119
|
+
: "Connect the notebook to include its source.";
|
|
120
|
+
|
|
121
|
+
const notebookSourceRequest = useAsyncData(async () => {
|
|
122
|
+
if (!includeNotebook || !notebookSourceAvailable || filename === null) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
const { contents } = await readCode();
|
|
126
|
+
return { filename, contents } satisfies NotebookSource;
|
|
127
|
+
}, [includeNotebook, notebookSourceAvailable, filename, readCode]);
|
|
128
|
+
|
|
129
|
+
const environment: EnvironmentDiagnostics | undefined =
|
|
130
|
+
environmentRequest.data
|
|
131
|
+
? enrichEnvironment(environmentRequest.data, navigator.userAgent)
|
|
132
|
+
: environmentRequest.status === "error"
|
|
133
|
+
? createPartialEnvironment(
|
|
134
|
+
getMarimoVersion(),
|
|
135
|
+
navigator.userAgent,
|
|
136
|
+
navigator.language,
|
|
137
|
+
"Server environment information unavailable",
|
|
138
|
+
)
|
|
139
|
+
: undefined;
|
|
140
|
+
|
|
141
|
+
const [issueDetailsCopied, setIssueDetailsCopied] = useState(false);
|
|
142
|
+
|
|
143
|
+
const githubIssueUrl = useMemo(() => {
|
|
144
|
+
if (!environment) {
|
|
145
|
+
return Constants.bugReportUrl;
|
|
146
|
+
}
|
|
147
|
+
return buildBugReportUrl(
|
|
148
|
+
Constants.bugReportUrl,
|
|
149
|
+
buildIssueDetails({ environment, errors }),
|
|
150
|
+
);
|
|
151
|
+
}, [environment, errors]);
|
|
152
|
+
|
|
153
|
+
const copyIssueDetails = async () => {
|
|
154
|
+
if (!environment) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const notebookForReport = includeNotebook
|
|
158
|
+
? notebookSourceRequest.data
|
|
159
|
+
: undefined;
|
|
160
|
+
await copyToClipboard(
|
|
161
|
+
buildIssueDetails({ environment, errors, notebook: notebookForReport }),
|
|
162
|
+
);
|
|
163
|
+
setIssueDetailsCopied(true);
|
|
164
|
+
setTimeout(() => setIssueDetailsCopied(false), 2000);
|
|
165
|
+
toast({
|
|
166
|
+
title:
|
|
167
|
+
environmentRequest.status === "error"
|
|
168
|
+
? "Partial issue details copied"
|
|
169
|
+
: "Issue details copied",
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
31
173
|
return (
|
|
32
|
-
<DialogContent className="w-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
174
|
+
<DialogContent className="w-[540px] max-w-[90vw]">
|
|
175
|
+
<DialogHeader>
|
|
176
|
+
<DialogTitle>Report an issue</DialogTitle>
|
|
177
|
+
<DialogDescription>
|
|
178
|
+
Copy your environment and any current errors to include in a GitHub
|
|
179
|
+
bug report. Nothing is uploaded automatically; review the details
|
|
180
|
+
before posting.
|
|
181
|
+
</DialogDescription>
|
|
182
|
+
</DialogHeader>
|
|
183
|
+
|
|
184
|
+
<div className="flex flex-col gap-4 max-h-[60vh] overflow-y-auto">
|
|
185
|
+
<div className="flex flex-wrap gap-2">
|
|
186
|
+
<Button
|
|
187
|
+
type="button"
|
|
188
|
+
variant="default"
|
|
189
|
+
size="xs"
|
|
190
|
+
disabled={
|
|
191
|
+
!environment ||
|
|
192
|
+
(includeNotebook && notebookSourceRequest.isFetching)
|
|
193
|
+
}
|
|
194
|
+
onClick={copyIssueDetails}
|
|
195
|
+
>
|
|
196
|
+
{issueDetailsCopied && (
|
|
197
|
+
<CheckIcon
|
|
198
|
+
className="w-4 h-4 mr-2 text-(--grass-11)"
|
|
199
|
+
color="white"
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
{issueDetailsCopied ? "Copied!" : "Copy issue details"}
|
|
203
|
+
</Button>
|
|
204
|
+
<Button type="button" variant="outline" size="xs" asChild={true}>
|
|
205
|
+
<a href={githubIssueUrl} target="_blank" rel="noreferrer">
|
|
206
|
+
<ExternalLinkIcon className="w-4 h-4 mr-2" />
|
|
207
|
+
Open GitHub issue
|
|
208
|
+
</a>
|
|
209
|
+
</Button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{includeNotebook && (
|
|
213
|
+
<p className="text-xs text-muted-foreground">
|
|
214
|
+
The GitHub link prefills your environment only. Use Copy issue
|
|
215
|
+
details to include the full notebook source.
|
|
216
|
+
</p>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{environmentRequest.status === "pending" && (
|
|
220
|
+
<div className="flex flex-col gap-2">
|
|
221
|
+
<span className="text-sm text-muted-foreground">
|
|
222
|
+
Loading environment details…
|
|
223
|
+
</span>
|
|
224
|
+
<Skeleton className="h-4 w-full" />
|
|
225
|
+
<Skeleton className="h-4 w-3/4" />
|
|
226
|
+
<Skeleton className="h-4 w-1/2" />
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{environmentRequest.status === "error" && (
|
|
231
|
+
<div className="flex items-center gap-2 text-sm">
|
|
232
|
+
<TriangleAlertIcon className="w-4 h-4 text-(--yellow-11) shrink-0" />
|
|
233
|
+
<span>Server environment information unavailable</span>
|
|
234
|
+
<Button
|
|
235
|
+
type="button"
|
|
236
|
+
variant="link"
|
|
237
|
+
size="xs"
|
|
238
|
+
onClick={() => environmentRequest.refetch()}
|
|
239
|
+
>
|
|
240
|
+
Retry
|
|
241
|
+
</Button>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{environment && (
|
|
246
|
+
<div className="flex flex-col gap-1">
|
|
247
|
+
<div className="flex items-center justify-between">
|
|
248
|
+
<span className="text-sm font-medium">Environment details</span>
|
|
249
|
+
<CopyClipboardIcon
|
|
250
|
+
className="w-3.5 h-3.5"
|
|
251
|
+
value={JSON.stringify(environment, null, 2)}
|
|
252
|
+
ariaLabel="Copy environment JSON"
|
|
253
|
+
toastTitle="Environment details copied"
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
<CollapsiblePreview
|
|
257
|
+
content={JSON.stringify(environment, null, 2)}
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
{environment && errors.length > 0 && (
|
|
263
|
+
<div className="flex flex-col gap-1">
|
|
264
|
+
<span className="text-sm font-medium">Current errors</span>
|
|
265
|
+
<CollapsiblePreview
|
|
266
|
+
content={errors.map(formatCellError).join("\n\n---\n\n")}
|
|
267
|
+
/>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
{environment && errors.length === 0 && (
|
|
272
|
+
<span className="text-sm text-muted-foreground">
|
|
273
|
+
No current errors detected.
|
|
274
|
+
</span>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
<div className="flex flex-col gap-1">
|
|
278
|
+
<div className="flex items-start gap-2 text-sm">
|
|
279
|
+
<Checkbox
|
|
280
|
+
id="issue-include-notebook"
|
|
281
|
+
className="mt-0.5"
|
|
282
|
+
checked={includeNotebook}
|
|
283
|
+
disabled={!notebookSourceAvailable}
|
|
284
|
+
onCheckedChange={(checked) =>
|
|
285
|
+
setIncludeNotebook(checked === true)
|
|
286
|
+
}
|
|
287
|
+
aria-label="Include full notebook source"
|
|
288
|
+
/>
|
|
289
|
+
<label htmlFor="issue-include-notebook">
|
|
290
|
+
Include full notebook source
|
|
291
|
+
</label>
|
|
292
|
+
</div>
|
|
293
|
+
<p className="text-xs text-muted-foreground ml-6">
|
|
294
|
+
Copies the entire saved Python source, including comments, literal
|
|
295
|
+
data, embedded credentials, and package metadata. Outputs and
|
|
296
|
+
external files are not included. Review it before posting.{" "}
|
|
297
|
+
<span className="font-bold">
|
|
298
|
+
For private notebooks, paste a minimal reproduction instead.
|
|
299
|
+
</span>
|
|
300
|
+
</p>
|
|
301
|
+
{notebookSourceReason && (
|
|
302
|
+
<span className="text-xs text-muted-foreground ml-6">
|
|
303
|
+
{notebookSourceReason}
|
|
304
|
+
</span>
|
|
305
|
+
)}
|
|
306
|
+
{includeNotebook && notebookSourceRequest.status === "error" && (
|
|
307
|
+
<span className="text-xs text-(--red-11) ml-6">
|
|
308
|
+
Notebook source could not be loaded.
|
|
309
|
+
</span>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<div className="border-t pt-3">
|
|
314
|
+
<p className="text-sm text-muted-foreground">
|
|
315
|
+
Other feedback? Take our{" "}
|
|
316
|
+
<a
|
|
317
|
+
href={Constants.feedbackForm}
|
|
318
|
+
target="_blank"
|
|
319
|
+
rel="noreferrer"
|
|
320
|
+
className="underline"
|
|
321
|
+
>
|
|
322
|
+
two-minute survey
|
|
323
|
+
</a>{" "}
|
|
324
|
+
or chat with us on{" "}
|
|
325
|
+
<a
|
|
326
|
+
href={Constants.discordLink}
|
|
327
|
+
target="_blank"
|
|
328
|
+
rel="noreferrer"
|
|
329
|
+
className="underline"
|
|
330
|
+
>
|
|
331
|
+
Discord
|
|
332
|
+
</a>
|
|
333
|
+
.
|
|
334
|
+
</p>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
106
337
|
</DialogContent>
|
|
107
338
|
);
|
|
108
339
|
};
|