@marimo-team/islands 0.25.3-dev231 → 0.25.3-dev233
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/{ErrorBoundary-BJSoVLSv.js → ErrorBoundary-CSUxOLGH.js} +1 -1
- package/dist/{chat-ui-DljFsfWf.js → chat-ui-DgaVxFrc.js} +570 -570
- package/dist/{code-visibility-C29f__4k.js → code-visibility-Dqc_vlop.js} +1 -1
- package/dist/{common-B6tzS_uB.js → common-D4YIvHFJ.js} +5518 -5509
- package/dist/{data-grid-overlay-editor-BsVL6sfo.js → data-grid-overlay-editor-2zdJpvtI.js} +1 -1
- package/dist/{feedback-button-X2gMVUoS.js → feedback-button-C9xDd4jy.js} +2 -2
- package/dist/{glide-data-editor-D3OoHtUh.js → glide-data-editor-DyC7T9ey.js} +3 -3
- package/dist/{globals-DVh9Owsi.js → globals-BzCxCJpU.js} +1 -1
- package/dist/{html-to-image-0FooaKop.js → html-to-image-Gx4q3ZoD.js} +1 -1
- package/dist/main.js +271 -271
- package/dist/{number-overlay-editor-CEMlW8ug.js → number-overlay-editor-DvLSuv8j.js} +1 -1
- package/dist/{process-output-BOvkTEDt.js → process-output-D2ISIyIs.js} +1 -1
- package/dist/{reveal-component-BeUSmTRU.js → reveal-component-BM4snSvm.js} +2 -2
- package/package.json +1 -1
- package/src/components/editor/actions/useNotebookActions.tsx +6 -6
- package/src/components/editor/output/VideoOutput.tsx +6 -0
- package/src/components/editor/output/__tests__/VideoOutput.test.tsx +131 -0
- package/src/core/vscode/__tests__/vscode-bindings.test.ts +125 -0
- package/src/core/vscode/vscode-bindings.ts +14 -2
- package/src/plugins/impl/data-frames/panel.tsx +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
|
|
2
2
|
import { t } from "./react-sHtZdjeP.js";
|
|
3
|
-
import { a as n } from "./glide-data-editor-
|
|
3
|
+
import { a as n } from "./glide-data-editor-DyC7T9ey.js";
|
|
4
4
|
//#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.3.0_27cffe7f1bc30287d59ac27db3240c5b/node_modules/@glideapps/glide-data-grid/dist/esm/internal/data-grid-overlay-editor/private/number-overlay-editor-style.js
|
|
5
5
|
var r = /* @__PURE__ */ e(t()), i = /*#__PURE__*/ n("div")({
|
|
6
6
|
name: "NumberOverlayEditorStyle",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { k as e } from "./html-to-image-
|
|
1
|
+
import { k as e } from "./html-to-image-Gx4q3ZoD.js";
|
|
2
2
|
import { m as t } from "./tooltip-C3UnvsRe.js";
|
|
3
3
|
import { $ as n, et as r } from "./error-entries-CRPfxMqm.js";
|
|
4
4
|
import { t as i } from "./strings-BGW-fXVJ.js";
|
|
@@ -3,7 +3,7 @@ import { c as t, g as n, h as r, m as i, t as a } from "./button-B6RjDKJx.js";
|
|
|
3
3
|
import { t as o } from "./react-sHtZdjeP.js";
|
|
4
4
|
import { t as s } from "./compiler-runtime-CldRdSsv.js";
|
|
5
5
|
import { m as c, t as l } from "./tooltip-C3UnvsRe.js";
|
|
6
|
-
import { $ as u, Q as d, Z as f, an as p, i as m, l as h, ln as g, n as _, on as v, r as y, t as b, u as x } from "./common-
|
|
6
|
+
import { $ as u, Q as d, Z as f, an as p, i as m, l as h, ln as g, n as _, on as v, r as y, t as b, u as x } from "./common-D4YIvHFJ.js";
|
|
7
7
|
import { M as S } from "./select-BSxlITBo.js";
|
|
8
8
|
import { Y as ee } from "./dropdown-menu-BRiE7Vnp.js";
|
|
9
9
|
import { Pt as C } from "./error-entries-CRPfxMqm.js";
|
|
@@ -11,7 +11,7 @@ import { t as w } from "./jsx-runtime-3BD1kT0s.js";
|
|
|
11
11
|
import { r as T } from "./use-restore-focus-BWO0z1lo.js";
|
|
12
12
|
import { D as E, t as D, x as O } from "./utils-DXkj82sK.js";
|
|
13
13
|
import { o as k } from "./static-state-BldGd99T.js";
|
|
14
|
-
import { a as A, n as te, t as ne } from "./code-visibility-
|
|
14
|
+
import { a as A, n as te, t as ne } from "./code-visibility-Dqc_vlop.js";
|
|
15
15
|
/* empty css */
|
|
16
16
|
var j = c("sticky-note", [["path", {
|
|
17
17
|
d: "M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z",
|
package/package.json
CHANGED
|
@@ -349,7 +349,7 @@ export function useNotebookActions({
|
|
|
349
349
|
code: code.contents,
|
|
350
350
|
baseUrl: `${Constants.molab}/new`,
|
|
351
351
|
});
|
|
352
|
-
window.open(url, "_blank");
|
|
352
|
+
window.open(url, "_blank", "noopener");
|
|
353
353
|
},
|
|
354
354
|
},
|
|
355
355
|
],
|
|
@@ -662,35 +662,35 @@ export function useNotebookActions({
|
|
|
662
662
|
icon: <BookMarkedIcon size={14} strokeWidth={1.5} />,
|
|
663
663
|
label: "Documentation",
|
|
664
664
|
handle: () => {
|
|
665
|
-
window.open(Constants.docsPage, "_blank");
|
|
665
|
+
window.open(Constants.docsPage, "_blank", "noopener");
|
|
666
666
|
},
|
|
667
667
|
},
|
|
668
668
|
{
|
|
669
669
|
icon: <GitHubIcon className="h-3.5 w-3.5" />,
|
|
670
670
|
label: "GitHub",
|
|
671
671
|
handle: () => {
|
|
672
|
-
window.open(Constants.githubPage, "_blank");
|
|
672
|
+
window.open(Constants.githubPage, "_blank", "noopener");
|
|
673
673
|
},
|
|
674
674
|
},
|
|
675
675
|
{
|
|
676
676
|
icon: <MessagesSquareIcon size={14} strokeWidth={1.5} />,
|
|
677
677
|
label: "Discord Community",
|
|
678
678
|
handle: () => {
|
|
679
|
-
window.open(Constants.discordLink, "_blank");
|
|
679
|
+
window.open(Constants.discordLink, "_blank", "noopener");
|
|
680
680
|
},
|
|
681
681
|
},
|
|
682
682
|
{
|
|
683
683
|
icon: <YouTubeIcon className="h-3.5 w-3.5" />,
|
|
684
684
|
label: "YouTube",
|
|
685
685
|
handle: () => {
|
|
686
|
-
window.open(Constants.youtube, "_blank");
|
|
686
|
+
window.open(Constants.youtube, "_blank", "noopener");
|
|
687
687
|
},
|
|
688
688
|
},
|
|
689
689
|
{
|
|
690
690
|
icon: <FileTextIcon size={14} strokeWidth={1.5} />,
|
|
691
691
|
label: "Changelog",
|
|
692
692
|
handle: () => {
|
|
693
|
-
window.open(Constants.releasesPage, "_blank");
|
|
693
|
+
window.open(Constants.releasesPage, "_blank", "noopener");
|
|
694
694
|
},
|
|
695
695
|
},
|
|
696
696
|
],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import type { JSX } from "react";
|
|
3
|
+
import { useSanitizeHtml } from "@/plugins/core/sanitize";
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
src: string;
|
|
@@ -7,5 +8,10 @@ interface Props {
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export const VideoOutput = ({ src, className }: Props): JSX.Element => {
|
|
11
|
+
const shouldSanitize = useSanitizeHtml();
|
|
12
|
+
if (shouldSanitize) {
|
|
13
|
+
return <video className={className} src={src} controls={true} />;
|
|
14
|
+
}
|
|
15
|
+
// oxlint-disable-next-line react/iframe-missing-sandbox, react-doctor/iframe-missing-sandbox -- Trusted notebook output can include active documents.
|
|
10
16
|
return <iframe className={className} src={src} title="Video output" />;
|
|
11
17
|
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { act, render } from "@testing-library/react";
|
|
3
|
+
import { Provider } from "jotai";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
|
|
6
|
+
import { OutputRenderer } from "@/components/editor/Output";
|
|
7
|
+
import { configOverridesAtom } from "@/core/config/config";
|
|
8
|
+
import { initialModeAtom } from "@/core/mode";
|
|
9
|
+
import { store } from "@/core/state/jotai";
|
|
10
|
+
import { VideoOutput } from "../VideoOutput";
|
|
11
|
+
|
|
12
|
+
function snapshotTrustState() {
|
|
13
|
+
return {
|
|
14
|
+
hasRunAnyCell: store.get(hasRunAnyCellAtom),
|
|
15
|
+
overrides: store.get(configOverridesAtom),
|
|
16
|
+
mode: store.get(initialModeAtom),
|
|
17
|
+
exportContext: window.__MARIMO_EXPORT_CONTEXT__,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("VideoOutput", () => {
|
|
22
|
+
let previous: ReturnType<typeof snapshotTrustState>;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
previous = snapshotTrustState();
|
|
26
|
+
store.set(hasRunAnyCellAtom, false);
|
|
27
|
+
store.set(configOverridesAtom, { runtime: { auto_instantiate: false } });
|
|
28
|
+
store.set(initialModeAtom, "edit");
|
|
29
|
+
delete window.__MARIMO_EXPORT_CONTEXT__;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
store.set(hasRunAnyCellAtom, previous.hasRunAnyCell);
|
|
34
|
+
store.set(configOverridesAtom, previous.overrides);
|
|
35
|
+
store.set(initialModeAtom, previous.mode);
|
|
36
|
+
if (previous.exportContext) {
|
|
37
|
+
window.__MARIMO_EXPORT_CONTEXT__ = previous.exportContext;
|
|
38
|
+
} else {
|
|
39
|
+
delete window.__MARIMO_EXPORT_CONTEXT__;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function renderVideo(src = "https://example.com/video.mp4") {
|
|
44
|
+
return render(
|
|
45
|
+
<Provider store={store}>
|
|
46
|
+
<VideoOutput src={src} className="output" />
|
|
47
|
+
</Provider>,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it.each([
|
|
52
|
+
"https://example.com/video.mp4",
|
|
53
|
+
"data:video/mp4;base64,AAAA",
|
|
54
|
+
"./@file/123-video.mp4",
|
|
55
|
+
])("preserves the media source and controls before execution: %s", (src) => {
|
|
56
|
+
const { container } = renderVideo(src);
|
|
57
|
+
const video = container.querySelector("video");
|
|
58
|
+
expect(video).toHaveAttribute("src", src);
|
|
59
|
+
expect(video).toHaveAttribute("controls");
|
|
60
|
+
expect(video).toHaveClass("output");
|
|
61
|
+
expect(container.querySelector("iframe")).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("keeps direct and JSON video outputs out of frames before execution", () => {
|
|
65
|
+
const src = `data:text/html,${encodeURIComponent("<script>parent.postMessage('cached-video', '*')</script>")}`;
|
|
66
|
+
const { container } = render(
|
|
67
|
+
<Provider store={store}>
|
|
68
|
+
<OutputRenderer
|
|
69
|
+
message={{ channel: "output", mimetype: "video/mp4", data: src }}
|
|
70
|
+
/>
|
|
71
|
+
<OutputRenderer
|
|
72
|
+
message={{
|
|
73
|
+
channel: "output",
|
|
74
|
+
mimetype: "application/json",
|
|
75
|
+
data: { video: `video/mp4:${src}` },
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
</Provider>,
|
|
79
|
+
);
|
|
80
|
+
expect(container.querySelector("iframe")).toBeNull();
|
|
81
|
+
const videos = container.querySelectorAll("video");
|
|
82
|
+
expect(videos).toHaveLength(2);
|
|
83
|
+
for (const video of videos) {
|
|
84
|
+
expect(video).toHaveAttribute("src", src);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("updates an existing output after the user runs a cell", () => {
|
|
89
|
+
const { container } = renderVideo();
|
|
90
|
+
expect(container.querySelector("video")).not.toBeNull();
|
|
91
|
+
act(() => store.set(hasRunAnyCellAtom, true));
|
|
92
|
+
expect(container.querySelector("video")).toBeNull();
|
|
93
|
+
expect(container.querySelector("iframe")).toHaveAttribute(
|
|
94
|
+
"src",
|
|
95
|
+
"https://example.com/video.mp4",
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it.each(["app", "auto-instantiate", "export"] as const)(
|
|
100
|
+
"preserves iframe output in a trusted %s context",
|
|
101
|
+
(context) => {
|
|
102
|
+
switch (context) {
|
|
103
|
+
case "app":
|
|
104
|
+
store.set(initialModeAtom, "read");
|
|
105
|
+
break;
|
|
106
|
+
case "auto-instantiate":
|
|
107
|
+
store.set(configOverridesAtom, {
|
|
108
|
+
runtime: { auto_instantiate: true },
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
case "export":
|
|
112
|
+
window.__MARIMO_EXPORT_CONTEXT__ = { trusted: true };
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
const { container } = renderVideo();
|
|
116
|
+
expect(container.querySelector("video")).toBeNull();
|
|
117
|
+
expect(container.querySelector("iframe")).toHaveAttribute(
|
|
118
|
+
"title",
|
|
119
|
+
"Video output",
|
|
120
|
+
);
|
|
121
|
+
expect(container.querySelector("iframe")).toHaveClass("output");
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
it("uses native video before the app mode is initialized", () => {
|
|
126
|
+
store.set(initialModeAtom, undefined);
|
|
127
|
+
const { container } = renderVideo();
|
|
128
|
+
expect(container.querySelector("video")).not.toBeNull();
|
|
129
|
+
expect(container.querySelector("iframe")).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const logger = vi.hoisted(() => ({
|
|
5
|
+
log: vi.fn(),
|
|
6
|
+
warn: vi.fn(),
|
|
7
|
+
error: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock("@/utils/Logger", () => ({ Logger: logger }));
|
|
10
|
+
vi.mock("@/core/hotkeys/shortcuts", () => ({ isPlatformMac: () => true }));
|
|
11
|
+
|
|
12
|
+
describe("VS Code paste messages", () => {
|
|
13
|
+
const originalUrl = window.location.href;
|
|
14
|
+
const setData = vi.fn();
|
|
15
|
+
let parent: Window;
|
|
16
|
+
let child: Window;
|
|
17
|
+
let input: HTMLTextAreaElement;
|
|
18
|
+
let cleanupListeners: () => void;
|
|
19
|
+
const onPaste = vi.fn();
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
vi.resetModules();
|
|
23
|
+
const parentFrame = document.createElement("iframe");
|
|
24
|
+
const childFrame = document.createElement("iframe");
|
|
25
|
+
input = document.createElement("textarea");
|
|
26
|
+
document.body.append(parentFrame, childFrame, input);
|
|
27
|
+
if (!parentFrame.contentWindow || !childFrame.contentWindow) {
|
|
28
|
+
throw new Error("Test frames need windows");
|
|
29
|
+
}
|
|
30
|
+
parent = parentFrame.contentWindow;
|
|
31
|
+
child = childFrame.contentWindow;
|
|
32
|
+
vi.stubGlobal("parent", parent);
|
|
33
|
+
vi.stubGlobal(
|
|
34
|
+
"DataTransfer",
|
|
35
|
+
class {
|
|
36
|
+
public setData = setData;
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
vi.stubGlobal(
|
|
40
|
+
"ClipboardEvent",
|
|
41
|
+
class extends Event {
|
|
42
|
+
public clipboardData: DataTransfer | undefined;
|
|
43
|
+
public constructor(type: string, init: ClipboardEventInit) {
|
|
44
|
+
super(type);
|
|
45
|
+
this.clipboardData = init.clipboardData ?? undefined;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
window.history.replaceState(null, "", "?vscode=true");
|
|
50
|
+
input.addEventListener("paste", onPaste);
|
|
51
|
+
input.focus();
|
|
52
|
+
const windowListeners = vi.spyOn(window, "addEventListener");
|
|
53
|
+
const documentListeners = vi.spyOn(document, "addEventListener");
|
|
54
|
+
cleanupListeners = () => {
|
|
55
|
+
for (const [type, listener, options] of windowListeners.mock.calls) {
|
|
56
|
+
window.removeEventListener(type, listener, options);
|
|
57
|
+
}
|
|
58
|
+
for (const [type, listener, options] of documentListeners.mock.calls) {
|
|
59
|
+
document.removeEventListener(type, listener, options);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const { maybeRegisterVSCodeBindings } = await import("../vscode-bindings");
|
|
63
|
+
maybeRegisterVSCodeBindings();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
cleanupListeners();
|
|
68
|
+
vi.restoreAllMocks();
|
|
69
|
+
vi.unstubAllGlobals();
|
|
70
|
+
document.body.replaceChildren();
|
|
71
|
+
window.history.replaceState(null, "", originalUrl);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function send(data: unknown, source: Window | null = parent) {
|
|
75
|
+
window.dispatchEvent(
|
|
76
|
+
new MessageEvent("message", {
|
|
77
|
+
data,
|
|
78
|
+
source,
|
|
79
|
+
origin: "vscode-webview://dynamic-parent",
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
it.each(["clipboard text", ""])(
|
|
85
|
+
"accepts parent paste with text %j",
|
|
86
|
+
(text) => {
|
|
87
|
+
send({ command: "paste", text });
|
|
88
|
+
expect(setData).toHaveBeenCalledWith("text/plain", text);
|
|
89
|
+
expect(onPaste).toHaveBeenCalledOnce();
|
|
90
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
it("rejects a child frame even when its reported origin matches", () => {
|
|
95
|
+
send({ command: "paste", text: "unsolicited" }, child);
|
|
96
|
+
expect(onPaste).not.toHaveBeenCalled();
|
|
97
|
+
expect(setData).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("rejects a message without a source window", () => {
|
|
101
|
+
send({ command: "paste", text: "unsolicited" }, null);
|
|
102
|
+
expect(onPaste).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it.each(
|
|
106
|
+
[
|
|
107
|
+
null,
|
|
108
|
+
[],
|
|
109
|
+
{},
|
|
110
|
+
"paste",
|
|
111
|
+
1,
|
|
112
|
+
true,
|
|
113
|
+
{ command: "paste" },
|
|
114
|
+
{ command: "paste", text: {} },
|
|
115
|
+
{ command: "paste", text: 12 },
|
|
116
|
+
{ command: "paste", text: null },
|
|
117
|
+
{ command: "execute", text: "unexpected" },
|
|
118
|
+
].map((data) => ({ data })),
|
|
119
|
+
)("ignores malformed parent messages without errors: %j", ({ data }) => {
|
|
120
|
+
send(data);
|
|
121
|
+
expect(onPaste).not.toHaveBeenCalled();
|
|
122
|
+
expect(setData).not.toHaveBeenCalled();
|
|
123
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { Logger } from "@/utils/Logger";
|
|
4
|
+
import { isRecord } from "@/utils/records";
|
|
4
5
|
import { KnownQueryParams } from "../constants";
|
|
5
6
|
import { isPlatformMac } from "../hotkeys/shortcuts";
|
|
6
7
|
import { isWasm } from "../wasm/utils";
|
|
@@ -53,9 +54,20 @@ function registerCopyPaste() {
|
|
|
53
54
|
});
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
window.addEventListener("message", async (event) => {
|
|
57
|
+
window.addEventListener("message", async (event: MessageEvent<unknown>) => {
|
|
58
|
+
// VS Code assigns each parent webview a different origin.
|
|
59
|
+
if (event.source !== window.parent) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const message = event.data;
|
|
63
|
+
if (
|
|
64
|
+
!isRecord(message) ||
|
|
65
|
+
message.command !== "paste" ||
|
|
66
|
+
typeof message.text !== "string"
|
|
67
|
+
) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
57
70
|
try {
|
|
58
|
-
const message = event.data;
|
|
59
71
|
const isMac = isPlatformMac();
|
|
60
72
|
switch (message.command) {
|
|
61
73
|
case "paste":
|