mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
Merge pull request #31084 from NousResearch/bb/tui-right-click-copy-selection
fix(tui): right-click copies active transcript selection
This commit is contained in:
parent
4117fc3645
commit
0ec0cafdd0
3 changed files with 123 additions and 1 deletions
90
ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts
Normal file
90
ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { handleMouseEvent } from './components/App.js'
|
||||
import { createSelectionState, startSelection, updateSelection } from './selection.js'
|
||||
|
||||
const makeApp = () => {
|
||||
const selection = createSelectionState()
|
||||
|
||||
return {
|
||||
clickCount: 1,
|
||||
lastHoverCol: -1,
|
||||
lastHoverRow: -1,
|
||||
mouseCaptureTarget: undefined,
|
||||
props: {
|
||||
getSelectedText: vi.fn(() => 'selected text'),
|
||||
onCopySelectionNoClear: vi.fn(async () => 'selected text'),
|
||||
onHoverAt: vi.fn(),
|
||||
onMouseDownAt: vi.fn(),
|
||||
onMouseDragAt: vi.fn(),
|
||||
onMouseUpAt: vi.fn(),
|
||||
onSelectionChange: vi.fn(),
|
||||
selection
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
describe('handleMouseEvent right-click selection behavior', () => {
|
||||
it('copies an active selection instead of dispatching right-click paste handlers', async () => {
|
||||
const app = makeApp()
|
||||
|
||||
startSelection(app.props.selection, 0, 0)
|
||||
updateSelection(app.props.selection, 4, 0)
|
||||
|
||||
handleMouseEvent(app, { action: 'press', button: 2, col: 3, kind: 'mouse', row: 1 })
|
||||
await Promise.resolve()
|
||||
|
||||
expect(app.props.onCopySelectionNoClear).toHaveBeenCalledOnce()
|
||||
expect(app.props.onMouseDownAt).not.toHaveBeenCalled()
|
||||
expect(app.clickCount).toBe(0)
|
||||
})
|
||||
|
||||
it('falls back to right-click handlers when selection copy has no clipboard path', async () => {
|
||||
const app = makeApp()
|
||||
app.props.onCopySelectionNoClear.mockResolvedValue('')
|
||||
|
||||
startSelection(app.props.selection, 0, 0)
|
||||
updateSelection(app.props.selection, 4, 0)
|
||||
|
||||
handleMouseEvent(app, { action: 'press', button: 2, col: 3, kind: 'mouse', row: 1 })
|
||||
await Promise.resolve()
|
||||
|
||||
expect(app.props.onCopySelectionNoClear).toHaveBeenCalledOnce()
|
||||
expect(app.props.onMouseDownAt).toHaveBeenCalledWith(2, 0, 2)
|
||||
})
|
||||
|
||||
it('does not paste when highlighted selection text is empty', async () => {
|
||||
const app = makeApp()
|
||||
app.props.getSelectedText.mockReturnValue('')
|
||||
|
||||
startSelection(app.props.selection, 0, 0)
|
||||
updateSelection(app.props.selection, 4, 0)
|
||||
|
||||
handleMouseEvent(app, { action: 'press', button: 2, col: 3, kind: 'mouse', row: 1 })
|
||||
await Promise.resolve()
|
||||
|
||||
expect(app.props.onCopySelectionNoClear).not.toHaveBeenCalled()
|
||||
expect(app.props.onMouseDownAt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not repeatedly copy or paste during right-button motion events over a selection', () => {
|
||||
const app = makeApp()
|
||||
|
||||
startSelection(app.props.selection, 0, 0)
|
||||
updateSelection(app.props.selection, 4, 0)
|
||||
|
||||
handleMouseEvent(app, { action: 'press', button: 0x20 | 2, col: 3, kind: 'mouse', row: 1 })
|
||||
|
||||
expect(app.props.onCopySelectionNoClear).not.toHaveBeenCalled()
|
||||
expect(app.props.onMouseDownAt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still dispatches right-click handlers when no text is selected', () => {
|
||||
const app = makeApp()
|
||||
|
||||
handleMouseEvent(app, { action: 'press', button: 2, col: 3, kind: 'mouse', row: 1 })
|
||||
|
||||
expect(app.props.onCopySelectionNoClear).not.toHaveBeenCalled()
|
||||
expect(app.props.onMouseDownAt).toHaveBeenCalledWith(2, 0, 2)
|
||||
})
|
||||
})
|
||||
|
|
@ -76,6 +76,10 @@ type Props = {
|
|||
// DOM elements. Called for mode-1003 motion events with no button held.
|
||||
// No-op outside fullscreen (Ink.dispatchHover gates on altScreenActive).
|
||||
readonly onHoverAt: (col: number, row: number) => void
|
||||
// Copy the active fullscreen text selection without clearing the highlight.
|
||||
// Used for terminal-native right-click-copy behaviour.
|
||||
readonly onCopySelectionNoClear: () => Promise<string>
|
||||
readonly getSelectedText: () => string
|
||||
// Look up the OSC 8 hyperlink at (col, row) synchronously at click
|
||||
// time. Returns the URL or undefined. The browser-open is deferred by
|
||||
// MULTI_CLICK_TIMEOUT_MS so double-click can cancel it.
|
||||
|
|
@ -631,6 +635,28 @@ export function handleMouseEvent(app: App, m: ParsedMouse): void {
|
|||
if (baseButton !== 0) {
|
||||
// Non-left press breaks the multi-click chain.
|
||||
app.clickCount = 0
|
||||
|
||||
if (baseButton === 2 && hasSelection(sel)) {
|
||||
if ((m.button & 0x20) !== 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!app.props.getSelectedText()) {
|
||||
return
|
||||
}
|
||||
|
||||
void app.props
|
||||
.onCopySelectionNoClear()
|
||||
.then(text => {
|
||||
if (!text) {
|
||||
app.props.onMouseDownAt(col, row, baseButton)
|
||||
}
|
||||
})
|
||||
.catch(() => app.props.onMouseDownAt(col, row, baseButton))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
app.props.onMouseDownAt(col, row, baseButton)
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1492,7 +1492,7 @@ export default class Ink {
|
|||
return ''
|
||||
}
|
||||
|
||||
const text = getSelectedText(this.selection, this.frontFrame.screen)
|
||||
const text = this.getTextSelectionText()
|
||||
|
||||
if (text) {
|
||||
try {
|
||||
|
|
@ -1514,6 +1514,10 @@ export default class Ink {
|
|||
return ''
|
||||
}
|
||||
|
||||
getTextSelectionText(): string {
|
||||
return hasSelection(this.selection) ? getSelectedText(this.selection, this.frontFrame.screen) : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the current text selection to the system clipboard via OSC 52
|
||||
* and clear the selection. Returns the copied text (empty if no selection
|
||||
|
|
@ -2332,7 +2336,9 @@ export default class Ink {
|
|||
dispatchKeyboardEvent={this.dispatchKeyboardEvent}
|
||||
exitOnCtrlC={this.options.exitOnCtrlC}
|
||||
getHyperlinkAt={this.getHyperlinkAt}
|
||||
getSelectedText={this.getTextSelectionText}
|
||||
onClickAt={this.dispatchClick}
|
||||
onCopySelectionNoClear={this.copySelectionNoClear}
|
||||
onCursorAdvance={this.noteExternalCursorAdvance}
|
||||
onCursorDeclaration={this.setCursorDeclaration}
|
||||
onExit={this.unmount}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue