feat(cli): add experimental.useOSC52Copy setting (#19488)

This commit is contained in:
Tommaso Sciortino
2026-02-19 10:22:11 -08:00
committed by GitHub
parent ba3e327ba1
commit 09b623fbd7
8 changed files with 98 additions and 19 deletions

View File

@@ -125,6 +125,7 @@ describe('copyCommand', () => {
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'Hi there! How can I help you?',
expect.anything(),
);
});
@@ -143,7 +144,10 @@ describe('copyCommand', () => {
const result = await copyCommand.action(mockContext, '');
expect(mockCopyToClipboard).toHaveBeenCalledWith('Part 1: Part 2: Part 3');
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'Part 1: Part 2: Part 3',
expect.anything(),
);
expect(result).toEqual({
type: 'message',
messageType: 'info',
@@ -170,7 +174,10 @@ describe('copyCommand', () => {
const result = await copyCommand.action(mockContext, '');
expect(mockCopyToClipboard).toHaveBeenCalledWith('Text part more text');
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'Text part more text',
expect.anything(),
);
expect(result).toEqual({
type: 'message',
messageType: 'info',
@@ -201,7 +208,10 @@ describe('copyCommand', () => {
const result = await copyCommand.action(mockContext, '');
expect(mockCopyToClipboard).toHaveBeenCalledWith('Second AI response');
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'Second AI response',
expect.anything(),
);
expect(result).toEqual({
type: 'message',
messageType: 'info',
@@ -230,6 +240,11 @@ describe('copyCommand', () => {
messageType: 'error',
content: `Failed to copy to the clipboard. ${clipboardError.message}`,
});
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'AI response',
expect.anything(),
);
});
it('should handle non-Error clipboard errors', async () => {
@@ -253,6 +268,11 @@ describe('copyCommand', () => {
messageType: 'error',
content: `Failed to copy to the clipboard. ${rejectedValue}`,
});
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'AI response',
expect.anything(),
);
});
it('should return info message when no text parts found in AI message', async () => {

View File

@@ -38,7 +38,8 @@ export const copyCommand: SlashCommand = {
if (lastAiOutput) {
try {
await copyToClipboard(lastAiOutput);
const settings = context.services.settings.merged;
await copyToClipboard(lastAiOutput, settings);
return {
type: 'message',

View File

@@ -14,6 +14,7 @@ import {
copyToClipboard,
getUrlOpenCommand,
} from './commandUtils.js';
import type { Settings } from '../../config/settingsSchema.js';
// Constants used by OSC-52 tests
const ESC = '\u001B';
@@ -257,6 +258,29 @@ describe('commandUtils', () => {
expect(mockClipboardyWrite).not.toHaveBeenCalled();
});
it('uses OSC-52 when useOSC52Copy setting is enabled', async () => {
const testText = 'forced-osc52';
const tty = makeWritable({ isTTY: true });
mockFs.createWriteStream.mockImplementation(() => {
setTimeout(() => tty.emit('open'), 0);
return tty;
});
// NO environment signals for SSH/WSL/etc.
const settings = {
experimental: { useOSC52Copy: true },
} as unknown as Settings;
await copyToClipboard(testText, settings);
const b64 = Buffer.from(testText, 'utf8').toString('base64');
const expected = `${ESC}]52;c;${b64}${BEL}`;
expect(tty.write).toHaveBeenCalledTimes(1);
expect(tty.write.mock.calls[0][0]).toBe(expected);
expect(mockClipboardyWrite).not.toHaveBeenCalled();
});
it('wraps OSC-52 for tmux when in SSH', async () => {
const testText = 'tmux-copy';
const tty = makeWritable({ isTTY: true });

View File

@@ -9,6 +9,7 @@ import clipboardy from 'clipboardy';
import type { SlashCommand } from '../commands/types.js';
import fs from 'node:fs';
import type { Writable } from 'node:stream';
import type { Settings } from '../../config/settingsSchema.js';
/**
* Checks if a query string potentially represents an '@' command.
@@ -157,8 +158,13 @@ const isWindowsTerminal = (): boolean =>
const isDumbTerm = (): boolean => (process.env['TERM'] ?? '') === 'dumb';
const shouldUseOsc52 = (tty: TtyTarget): boolean =>
Boolean(tty) && !isDumbTerm() && (isSSH() || isWSL() || isWindowsTerminal());
const shouldUseOsc52 = (tty: TtyTarget, settings?: Settings): boolean =>
Boolean(tty) &&
!isDumbTerm() &&
(settings?.experimental?.useOSC52Copy ||
isSSH() ||
isWSL() ||
isWindowsTerminal());
const safeUtf8Truncate = (buf: Buffer, maxBytes: number): Buffer => {
if (buf.length <= maxBytes) return buf;
@@ -237,12 +243,15 @@ const writeAll = (stream: Writable, data: string): Promise<void> =>
});
// Copies a string snippet to the clipboard with robust OSC-52 support.
export const copyToClipboard = async (text: string): Promise<void> => {
export const copyToClipboard = async (
text: string,
settings?: Settings,
): Promise<void> => {
if (!text) return;
const tty = await pickTty();
if (shouldUseOsc52(tty)) {
if (shouldUseOsc52(tty, settings)) {
const osc = buildOsc52(text);
const payload = inTmux()
? wrapForTmux(osc)