diff --git a/docs/cli/commands.md b/docs/cli/commands.md index 979ca59dfc..da29410533 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -73,6 +73,9 @@ Slash commands provide meta-level control over the CLI itself. - **`/copy`** - **Description:** Copies the last output produced by Gemini CLI to your clipboard, for easy sharing or reuse. + - **Behavior:** + - Local sessions use system clipboard tools (pbcopy/xclip/clip). + - Remote sessions (SSH/WSL) use OSC 52 and require terminal support. - **Note:** This command requires platform-specific clipboard tools to be installed. - On Linux, it requires `xclip` or `xsel`. You can typically install them diff --git a/packages/cli/src/ui/utils/commandUtils.test.ts b/packages/cli/src/ui/utils/commandUtils.test.ts index 7a2e62a947..ba5aeaab35 100644 --- a/packages/cli/src/ui/utils/commandUtils.test.ts +++ b/packages/cli/src/ui/utils/commandUtils.test.ts @@ -239,11 +239,12 @@ describe('commandUtils', () => { expect(mockClipboardyWrite).not.toHaveBeenCalled(); }); - it('wraps OSC-52 for tmux', async () => { + it('wraps OSC-52 for tmux when in SSH', async () => { const testText = 'tmux-copy'; const tty = makeWritable({ isTTY: true }); mockFs.createWriteStream.mockReturnValue(tty); + process.env['SSH_CONNECTION'] = '1'; process.env['TMUX'] = '1'; await copyToClipboard(testText); @@ -257,12 +258,13 @@ describe('commandUtils', () => { expect(mockClipboardyWrite).not.toHaveBeenCalled(); }); - it('wraps OSC-52 for GNU screen with chunked DCS', async () => { + it('wraps OSC-52 for GNU screen with chunked DCS when in SSH', async () => { // ensure payload > chunk size (240) so there are multiple chunks const testText = 'x'.repeat(1200); const tty = makeWritable({ isTTY: true }); mockFs.createWriteStream.mockReturnValue(tty); + process.env['SSH_CONNECTION'] = '1'; process.env['STY'] = 'screen-session'; await copyToClipboard(testText); @@ -358,6 +360,21 @@ describe('commandUtils', () => { expect(tty.end).not.toHaveBeenCalled(); }); + it('uses clipboardy in tmux when not in SSH/WSL', async () => { + const tty = makeWritable({ isTTY: true }); + mockFs.createWriteStream.mockReturnValue(tty); + const text = 'tmux-local'; + mockClipboardyWrite.mockResolvedValue(undefined); + + process.env['TMUX'] = '1'; + + await copyToClipboard(text); + + expect(mockClipboardyWrite).toHaveBeenCalledWith(text); + expect(tty.write).not.toHaveBeenCalled(); + expect(tty.end).not.toHaveBeenCalled(); + }); + it('skips /dev/tty on Windows and uses stderr fallback for OSC-52', async () => { mockProcess.platform = 'win32'; const stderrStream = makeWritable({ isTTY: true }); diff --git a/packages/cli/src/ui/utils/commandUtils.ts b/packages/cli/src/ui/utils/commandUtils.ts index c1bd755221..87a873cb64 100644 --- a/packages/cli/src/ui/utils/commandUtils.ts +++ b/packages/cli/src/ui/utils/commandUtils.ts @@ -113,9 +113,7 @@ const isWSL = (): boolean => const isDumbTerm = (): boolean => (process.env['TERM'] ?? '') === 'dumb'; const shouldUseOsc52 = (tty: TtyTarget): boolean => - Boolean(tty) && - !isDumbTerm() && - (isSSH() || inTmux() || inScreen() || isWSL()); + Boolean(tty) && !isDumbTerm() && (isSSH() || isWSL()); const safeUtf8Truncate = (buf: Buffer, maxBytes: number): Buffer => { if (buf.length <= maxBytes) return buf;