Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions site/src/modules/terminal/WorkspaceTerminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,30 @@ export const WorkspaceTerminal = ({
}),
);

// OSC 52 clipboard support. Programs like tmux send this escape
// sequence to copy text to the host's system clipboard.
nextTerminal.parser.registerOscHandler(52, (data) => {
const separatorIndex = data.indexOf(";");
if (separatorIndex === -1) {
return false;
}
const payload = data.slice(separatorIndex + 1);
// A "?" payload is a clipboard read query. Responding would
// require writing back to the PTY, which is not supported.
if (!payload || payload === "?") {
return false;
}
try {
const bytes = Uint8Array.from(atob(payload), (c) => c.charCodeAt(0));
const decoded = new TextDecoder().decode(bytes);
void copyToClipboard(decoded);
} catch {
// Invalid base64; ignore.
return false;
Comment thread
aqandrew marked this conversation as resolved.
}
return true;
});

const copySelection = () => {
const selection = nextTerminal.getSelection();
if (selection) {
Expand Down
75 changes: 74 additions & 1 deletion site/src/pages/TerminalPage/TerminalPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { HttpResponse, http } from "msw";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { API } from "#/api/api";
import type { Workspace } from "#/api/typesGenerated";
import {
Expand Down Expand Up @@ -249,6 +249,79 @@ describe("TerminalPage", () => {
expect(closeSpy).toHaveBeenCalled();
});

describe("OSC 52 clipboard", () => {
let writeTextMock: ReturnType<typeof vi.fn>;
let originalClipboard: PropertyDescriptor | undefined;

beforeEach(() => {
originalClipboard = Object.getOwnPropertyDescriptor(
navigator,
"clipboard",
);
writeTextMock = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
value: { writeText: writeTextMock, readText: vi.fn() },
writable: true,
configurable: true,
});
});

afterEach(() => {
if (originalClipboard) {
Object.defineProperty(navigator, "clipboard", originalClipboard);
}
});

it("copies to clipboard via OSC 52", async () => {
const ws = createWorkspaceTerminalWebSocket();
await renderTerminal();
await ws.nextMessage;

// OSC 52 sequence per #16577:
// printf "\033]52;c;$(echo -n 'Coder is Cool' | base64)\a"
const base64 = btoa("Coder is Cool");
ws.send(`\x1b]52;c;${base64}\x07`);

await waitFor(() => {
expect(writeTextMock).toHaveBeenCalledWith("Coder is Cool");
});
});

it("ignores OSC 52 with missing separator", async () => {
const ws = createWorkspaceTerminalWebSocket();
await renderTerminal();
await ws.nextMessage;

ws.send("\x1b]52;no-separator\x07");

// Give the parser time to process, then verify no clipboard write.
await new Promise((r) => setTimeout(r, 100));
expect(writeTextMock).not.toHaveBeenCalled();
});

it("ignores OSC 52 clipboard read query", async () => {
const ws = createWorkspaceTerminalWebSocket();
await renderTerminal();
await ws.nextMessage;

ws.send("\x1b]52;c;?\x07");

await new Promise((r) => setTimeout(r, 100));
expect(writeTextMock).not.toHaveBeenCalled();
});

it("ignores OSC 52 with invalid base64", async () => {
const ws = createWorkspaceTerminalWebSocket();
await renderTerminal();
await ws.nextMessage;

ws.send("\x1b]52;c;!!!invalid-base64!!!\x07");

await new Promise((r) => setTimeout(r, 100));
expect(writeTextMock).not.toHaveBeenCalled();
});
});

it("skips confirmation dialog for trusted app commands", async () => {
// Override the workspace response so the agent has an app with
// a command that matches the ?app= slug.
Expand Down
Loading