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
13 changes: 7 additions & 6 deletions docs/cli/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,13 @@ they appear in the UI.

### Experimental

| UI Label | Setting | Description | Default |
| -------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions). | `false` |
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| UI Label | Setting | Description | Default |
| -------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |

### Skills

Expand Down
11 changes: 9 additions & 2 deletions docs/get-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -941,8 +941,15 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes

- **`experimental.useOSC52Paste`** (boolean):
- **Description:** Use OSC 52 sequence for pasting instead of clipboardy
(useful for remote sessions).
- **Description:** Use OSC 52 for pasting. This may be more robust than the
default system when using remote terminal sessions (if your terminal is
configured to allow it).
- **Default:** `false`

- **`experimental.useOSC52Copy`** (boolean):
- **Description:** Use OSC 52 for copying. This may be more robust than the
default system when using remote terminal sessions (if your terminal is
configured to allow it).
- **Default:** `false`

- **`experimental.plan`** (boolean):
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1632,7 +1632,17 @@ const SETTINGS_SCHEMA = {
requiresRestart: false,
default: false,
description:
'Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions).',
'Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).',
showInDialog: true,
},
useOSC52Copy: {
type: 'boolean',
label: 'Use OSC 52 Copy',
category: 'Experimental',
requiresRestart: false,
default: false,
description:
'Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).',
showInDialog: true,
},
plan: {
Expand Down
26 changes: 23 additions & 3 deletions packages/cli/src/ui/commands/copyCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ describe('copyCommand', () => {

expect(mockCopyToClipboard).toHaveBeenCalledWith(
'Hi there! How can I help you?',
expect.anything(),
);
});

Expand All @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/ui/commands/copyCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
24 changes: 24 additions & 0 deletions packages/cli/src/ui/utils/commandUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 });
Expand Down
17 changes: 13 additions & 4 deletions packages/cli/src/ui/utils/commandUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 9 additions & 2 deletions schemas/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1587,8 +1587,15 @@
},
"useOSC52Paste": {
"title": "Use OSC 52 Paste",
"description": "Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions).",
"markdownDescription": "Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions).\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `false`",
"description": "Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).",
"markdownDescription": "Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"useOSC52Copy": {
"title": "Use OSC 52 Copy",
"description": "Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).",
"markdownDescription": "Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
Expand Down
Loading