mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
fix(tui): make mac copy use pbcopy
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { readClipboardText } from '../lib/clipboard.js'
|
||||
import { readClipboardText, writeClipboardText } from '../lib/clipboard.js'
|
||||
|
||||
describe('readClipboardText', () => {
|
||||
it('does nothing off macOS', async () => {
|
||||
@@ -23,3 +23,47 @@ describe('readClipboardText', () => {
|
||||
await expect(readClipboardText('darwin', run)).resolves.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('writeClipboardText', () => {
|
||||
it('does nothing off macOS', async () => {
|
||||
const start = vi.fn()
|
||||
|
||||
await expect(writeClipboardText('hello', 'linux', start)).resolves.toBe(false)
|
||||
expect(start).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('writes text to pbcopy on macOS', async () => {
|
||||
const stdin = { end: vi.fn() }
|
||||
const child = {
|
||||
once: vi.fn((event: string, cb: (code?: number) => void) => {
|
||||
if (event === 'close') {
|
||||
cb(0)
|
||||
}
|
||||
|
||||
return child
|
||||
}),
|
||||
stdin
|
||||
}
|
||||
const start = vi.fn().mockReturnValue(child)
|
||||
|
||||
await expect(writeClipboardText('hello world', 'darwin', start as any)).resolves.toBe(true)
|
||||
expect(start).toHaveBeenCalledWith('pbcopy', [], expect.objectContaining({ stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true }))
|
||||
expect(stdin.end).toHaveBeenCalledWith('hello world')
|
||||
})
|
||||
|
||||
it('returns false when pbcopy fails', async () => {
|
||||
const child = {
|
||||
once: vi.fn((event: string, cb: () => void) => {
|
||||
if (event === 'error') {
|
||||
cb()
|
||||
}
|
||||
|
||||
return child
|
||||
}),
|
||||
stdin: { end: vi.fn() }
|
||||
}
|
||||
const start = vi.fn().mockReturnValue(child)
|
||||
|
||||
await expect(writeClipboardText('hello world', 'darwin', start as any)).resolves.toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,8 @@ import type {
|
||||
SudoRespondResponse,
|
||||
VoiceRecordResponse
|
||||
} from '../gatewayTypes.js'
|
||||
|
||||
import { writeClipboardText } from '../lib/clipboard.js'
|
||||
import { writeOsc52Clipboard } from '../lib/osc52.js'
|
||||
import { isAction, isMac } from '../lib/platform.js'
|
||||
|
||||
@@ -30,9 +32,17 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||
const copySelection = () => {
|
||||
const text = terminal.selection.copySelection()
|
||||
|
||||
if (text) {
|
||||
actions.sys(`copied ${text.length} chars`)
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
|
||||
void writeClipboardText(text).then(copied => {
|
||||
if (!copied) {
|
||||
writeOsc52Clipboard(text)
|
||||
}
|
||||
})
|
||||
|
||||
actions.sys(`copied ${text.length} chars`)
|
||||
}
|
||||
|
||||
const clearSelection = () => {
|
||||
@@ -249,7 +259,14 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||
const inputSel = getInputSelection()
|
||||
|
||||
if (inputSel && inputSel.end > inputSel.start) {
|
||||
writeOsc52Clipboard(inputSel.value.slice(inputSel.start, inputSel.end))
|
||||
const text = inputSel.value.slice(inputSel.start, inputSel.end)
|
||||
|
||||
void writeClipboardText(text).then(copied => {
|
||||
if (!copied) {
|
||||
writeOsc52Clipboard(text)
|
||||
}
|
||||
})
|
||||
|
||||
inputSel.clear()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { readClipboardText, writeClipboardText } from '../lib/clipboard.js'
|
||||
import { isActionMod, isMac } from '../lib/platform.js'
|
||||
import { writeOsc52Clipboard } from '../lib/osc52.js'
|
||||
|
||||
@@ -528,7 +528,13 @@ export function TextInput({
|
||||
const range = selRange()
|
||||
|
||||
if (range) {
|
||||
writeOsc52Clipboard(vRef.current.slice(range.start, range.end))
|
||||
const text = vRef.current.slice(range.start, range.end)
|
||||
|
||||
void writeClipboardText(text).then(copied => {
|
||||
if (!copied) {
|
||||
writeOsc52Clipboard(text)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import { execFile, spawn } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
@@ -26,3 +26,33 @@ export async function readClipboardText(
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write plain text to the system clipboard.
|
||||
*
|
||||
* On macOS this uses `pbcopy`. On other platforms we intentionally return
|
||||
* false for now; non-mac copy still falls back to OSC52.
|
||||
*/
|
||||
export async function writeClipboardText(
|
||||
text: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
start: typeof spawn = spawn
|
||||
): Promise<boolean> {
|
||||
if (platform !== 'darwin') {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const ok = await new Promise<boolean>(resolve => {
|
||||
const child = start('pbcopy', [], { stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true })
|
||||
|
||||
child.once('error', () => resolve(false))
|
||||
child.once('close', code => resolve(code === 0))
|
||||
child.stdin.end(text)
|
||||
})
|
||||
|
||||
return ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user