fix(tui): restore clipboard hotkeys in clarify mode

This commit is contained in:
kshitijk4poor
2026-04-19 12:12:26 +05:30
committed by kshitij
parent 8c9fdedaf5
commit c3af012a35
4 changed files with 109 additions and 4 deletions

View File

@@ -0,0 +1,29 @@
import { describe, expect, it, vi } from 'vitest'
import { readClipboardText } from '../lib/clipboard.js'
describe('readClipboardText', () => {
it('does nothing off macOS', () => {
const run = vi.fn()
expect(readClipboardText('linux', run)).toBeNull()
expect(run).not.toHaveBeenCalled()
})
it('reads text from pbpaste on macOS', () => {
const run = vi.fn().mockReturnValue({ status: 0, stdout: 'hello world\n' })
expect(readClipboardText('darwin', run)).toBe('hello world\n')
expect(run).toHaveBeenCalledWith(
'pbpaste',
[],
expect.objectContaining({ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
)
})
it('returns null when pbpaste fails', () => {
const run = vi.fn().mockReturnValue({ status: 1, stdout: '' })
expect(readClipboardText('darwin', run)).toBeNull()
})
})

View File

@@ -5,6 +5,7 @@ import type { Theme } from '../theme.js'
import type { ApprovalReq, ClarifyReq, ConfirmReq } from '../types.js'
import { TextInput } from './textInput.js'
import { isMac } from '../lib/platform.js'
const OPTS = ['once', 'session', 'always', 'deny'] as const
const LABELS = { always: 'Always allow', deny: 'Deny', once: 'Allow once', session: 'Allow this session' } as const
@@ -125,10 +126,12 @@ export function ClarifyPrompt({ cols = 80, onAnswer, onCancel, req, t }: Clarify
<Box>
<Text color={t.color.label}>{'> '}</Text>
<TextInput columns={Math.max(20, cols - 6)} onChange={setCustom} onSubmit={onAnswer} value={custom} />
<TextInput columns={Math.max(20, cols - 6)} allowClipboardHotkeys={isMac} onChange={setCustom} onSubmit={onAnswer} value={custom} />
</Box>
<Text color={t.color.dim}>Enter send · Esc {choices.length ? 'back' : 'cancel'} · Ctrl+C cancel</Text>
<Text color={t.color.dim}>
Enter send · Esc {choices.length ? 'back' : 'cancel'} · {isMac ? 'Cmd+C copy · Cmd+V paste · Ctrl+C cancel' : 'Ctrl+C cancel'}
</Text>
</Box>
)
}

View File

@@ -3,6 +3,9 @@ import * as Ink from '@hermes/ink'
import { useEffect, useMemo, useRef, useState } from 'react'
import { setInputSelection } from '../app/inputSelectionStore.js'
import { readClipboardText } from '../lib/clipboard.js'
import { isMac } from '../lib/platform.js'
import { writeOsc52Clipboard } from '../lib/osc52.js'
type InkExt = typeof Ink & {
stringWidth: (s: string) => number
@@ -279,6 +282,7 @@ export function TextInput({
onChange,
onPaste,
onSubmit,
allowClipboardHotkeys = false,
mask,
placeholder = '',
focus = true
@@ -484,12 +488,50 @@ export function TextInput({
const ins = (v: string, c: number, s: string) => v.slice(0, c) + s + v.slice(c)
const pastePlainText = (text: string) => {
const cleaned = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
if (!cleaned) {
return
}
const range = selRange()
const nextValue = range
? vRef.current.slice(0, range.start) + cleaned + vRef.current.slice(range.end)
: vRef.current.slice(0, curRef.current) + cleaned + vRef.current.slice(curRef.current)
const nextCursor = range ? range.start + cleaned.length : curRef.current + cleaned.length
commit(nextValue, nextCursor)
}
useInput(
(inp: string, k: Key, event: InputEvent) => {
const eventRaw = event.keypress.raw
if (eventRaw === '\x1bv' || eventRaw === '\x1bV' || eventRaw === '\x16') {
return void emitPaste({ cursor: curRef.current, hotkey: true, text: '', value: vRef.current })
if (eventRaw === '\x1bv' || eventRaw === '\x1bV' || eventRaw === '\x16' || (allowClipboardHotkeys && isMac && k.meta && inp.toLowerCase() === 'v')) {
if (cbPaste.current) {
return void emitPaste({ cursor: curRef.current, hotkey: true, text: '', value: vRef.current })
}
if (allowClipboardHotkeys) {
const text = readClipboardText()
if (text) {
return pastePlainText(text)
}
}
return
}
if (allowClipboardHotkeys && isMac && k.meta && inp.toLowerCase() === 'c') {
const range = selRange()
if (range) {
writeOsc52Clipboard(vRef.current.slice(range.start, range.end))
}
return
}
if (
@@ -687,6 +729,7 @@ export interface PasteEvent {
}
interface TextInputProps {
allowClipboardHotkeys?: boolean
columns?: number
focus?: boolean
mask?: string

View File

@@ -0,0 +1,30 @@
import { spawnSync, type SpawnSyncOptions } from 'node:child_process'
const DEFAULT_SPAWN_OPTS: SpawnSyncOptions = {
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8'
}
/**
* Read plain text from the system clipboard.
*
* On macOS this uses `pbpaste`. On other platforms we intentionally return
* null for now; the TUI's text-paste hotkeys are primarily targeted at the
* macOS clarify/input flow.
*/
export function readClipboardText(
platform: NodeJS.Platform = process.platform,
run = spawnSync
): string | null {
if (platform !== 'darwin') {
return null
}
const result = run('pbpaste', [], DEFAULT_SPAWN_OPTS)
if (result.status !== 0 || typeof result.stdout !== 'string') {
return null
}
return result.stdout
}