Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Code v99.99.999

## Unreleased

### Fixed

- Terminal applications using OSC 52 can now copy to the browser clipboard instead of an internal clipboard.

## [4.141.0](https://github.com/coder/code-server/releases/tag/v4.141.0) - 2026-10-08

Code v1.141.0
Expand Down
1 change: 1 addition & 0 deletions patches/series
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ cli-window-open.diff
getting-started.diff
keepalive.diff
clipboard.diff
terminal-clipboard.diff
display-language.diff
trusted-domains.diff
signature-verification.diff
Expand Down
75 changes: 75 additions & 0 deletions patches/terminal-clipboard.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
Route normal OSC 52 writes to the default clipboard so browser clients receive
terminal copy requests. Explicit clipboard types use an in-memory map in
BrowserClipboardService, so sending the normal 'c' destination as 'clipboard'
never reaches navigator.clipboard.writeText. Keep PRIMARY selection and reads
unchanged.

Reproduce in a browser terminal by running:
printf '\033]52;c;Q0xJUEJPQVJEX1RFU1Q=\007'
Pasting into another application should yield CLIPBOARD_TEST. Without the fix,
the browser clipboard retains its previous value. Covered end-to-end in
test/e2e/terminal.test.ts, with destination mapping cases in the VS Code patch.

Index: code-server/lib/vscode/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts
+++ code-server/lib/vscode/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts
@@ -343,7 +343,8 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
return _clipboardService.readText(type === 'p' ? 'selection' : 'clipboard');
},
async writeText(type: string, text: string): Promise<void> {
- return _clipboardService.writeText(text, type === 'p' ? 'selection' : 'clipboard');
+ // Explicit clipboard types are in-memory only in the browser.
+ return _clipboardService.writeText(text, type === 'p' ? 'selection' : undefined);
}
});
this.raw.loadAddon(this._clipboardAddon);
Index: code-server/lib/vscode/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts
+++ code-server/lib/vscode/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts
@@ -13,6 +13,7 @@ import { toDisposable } from '../../../../../../base/common/lifecycle.js';
import { isMacintosh } from '../../../../../../base/common/platform.js';
import { mock } from '../../../../../../base/test/common/mock.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
+import { IClipboardService } from '../../../../../../platform/clipboard/common/clipboardService.js';
import { IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js';
import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js';
import { IConfigurationChangeEvent } from '../../../../../../platform/configuration/common/configuration.js';
@@ -109,6 +110,9 @@ suite('XtermTerminal', () => {

XTermBaseCtor = (await importAMDNodeModule<typeof import('@xterm/xterm')>('@xterm/xterm', 'lib/xterm.js')).Terminal;

+ // Load before construction so the clipboard addon is ready when setup completes.
+ await new TestXtermAddonImporter().importAddon('clipboard');
+
const capabilityStore = store.add(new TerminalCapabilityStore());
xterm = store.add(instantiationService.createInstance(XtermTerminal, undefined, XTermBaseCtor, {
cols: 80,
@@ -129,6 +133,26 @@ suite('XtermTerminal', () => {
strictEqual(xterm.raw.rows, 30);
});

+ suite('OSC 52 clipboard writes', () => {
+ for (const [destination, expectedType] of [
+ ['c', undefined],
+ ['', undefined],
+ ['s', undefined],
+ ['p', 'selection'],
+ ] as const) {
+ test(`routes ${destination || 'default'} to ${expectedType || 'system clipboard'}`, async () => {
+ const writeText = stub(instantiationService.get(IClipboardService), 'writeText').resolves();
+ try {
+ await write(`\x1b]52;${destination};UEFOTy1URVNU\x07`);
+ strictEqual(writeText.callCount, 1);
+ deepStrictEqual(writeText.firstCall.args, ['PANO-TEST', expectedType]);
+ } finally {
+ writeText.restore();
+ }
+ });
+ }
+ });
+
suite('fontRendering', () => {
async function setTerminalConfiguration(configuration: Partial<ITerminalConfiguration>): Promise<void> {
await configurationService.setUserConfiguration('terminal.integrated', {
14 changes: 14 additions & 0 deletions test/e2e/terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ describe("Integrated Terminal", ["--disable-workspace-trust"], {}, () => {
expect(stdout).toMatch(address)
})

test("should copy OSC 52 terminal output to the browser clipboard", async ({ codeServerPage }) => {
const page = codeServerPage.page
await page.context().grantPermissions(["clipboard-read", "clipboard-write"])
await page.evaluate(() => navigator.clipboard.writeText("PREVIOUS_CLIPBOARD_VALUE"))

await codeServerPage.focusTerminal()
const text = "CODE_SERVER_OSC52_CLIPBOARD_TEST"
const payload = Buffer.from(text).toString("base64")
await page.keyboard.type(`printf '\\033]52;c;${payload}\\007'`)
await page.keyboard.press("Enter")

await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(text)
})

// TODO@jsjoeio - add test to make sure full code-server path works
test("should be able to invoke `code-server` to open a file", async ({ codeServerPage }) => {
const tmpFolderPath = await tmpdir(testName)
Expand Down