fix(tui): Ctrl+C with input selection actually preserves input (lift handler to app level)

Previous fix in 9dbf1ec6 handled Ctrl+C inside textInput but the APP-level
useInputHandlers fires the same keypress in a separate React hook and ran
clearIn() regardless. Net effect: the OSC 52 copy succeeded but the input
wiped right after, so Brooklyn only noticed the wipe.

Lift the selection-aware Ctrl+C to a single place by threading input
selection state through a new nanostore (src/app/inputSelectionStore.ts).
textInput syncs its derived `selected` range + a clear() callback to the
store on every selection change, and the app-level Ctrl+C handler reads
the store before its clear/interrupt/die chain:

  - terminal-level selection (scrollback) → copy, existing behavior
  - in-input selection present → copy + clear selection, preserve input
  - input has text, no selection → clearIn(), existing behavior
  - empty + busy → interrupt turn
  - empty + idle → die

textInput no longer has its own Ctrl+C block; keypress falls through to
app-level like it did before 9dbf1ec6.
This commit is contained in:
Brooklyn Nicholson
2026-04-18 16:28:51 -05:00
parent bfac5d039d
commit fb06bc67de
3 changed files with 59 additions and 15 deletions

View File

@@ -2,7 +2,7 @@ import type { InputEvent, Key } from '@hermes/ink'
import * as Ink from '@hermes/ink'
import { useEffect, useMemo, useRef, useState } from 'react'
import { writeOsc52Clipboard } from '../lib/osc52.js'
import { setInputSelection } from '../app/inputSelectionStore.js'
type InkExt = typeof Ink & {
stringWidth: (s: string) => number
@@ -353,6 +353,28 @@ export function TextInput({
}
}, [value])
useEffect(() => {
if (!focus) {
return
}
if (selected) {
setInputSelection({
clear: () => {
selRef.current = null
setSel(null)
},
end: selected.end,
start: selected.start,
value: vRef.current
})
} else {
setInputSelection(null)
}
return () => setInputSelection(null)
}, [focus, selected])
useEffect(
() => () => {
if (pasteTimer.current) {
@@ -470,20 +492,16 @@ export function TextInput({
return void emitPaste({ cursor: curRef.current, hotkey: true, text: '', value: vRef.current })
}
if (k.ctrl && inp === 'c') {
const range = selRange()
if (range) {
writeOsc52Clipboard(vRef.current.slice(range.start, range.end))
clearSel()
return
}
return
}
if (k.upArrow || k.downArrow || k.tab || (k.shift && k.tab) || k.pageUp || k.pageDown || k.escape) {
if (
k.upArrow ||
k.downArrow ||
(k.ctrl && inp === 'c') ||
k.tab ||
(k.shift && k.tab) ||
k.pageUp ||
k.pageDown ||
k.escape
) {
return
}