mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(tui): Cmd+Backspace kills the line; Option/Ctrl+Backspace stay delete-word
On terminals that send Cmd+Backspace as a CSI-u sequence rather than rewriting it to Ctrl+U, the keystroke fell through to the word-delete branch and erased a single word. The modifier this hinges on is easy to get wrong. isActionMod accepts key.meta on macOS, and hermes-ink reports Option as meta — so keying on it would turn Option+Backspace, the platform's delete-word shortcut, into delete-the-whole-line. On Linux/Windows isActionMod is key.ctrl, where Ctrl+Backspace is likewise delete-word in readline, VS Code, browsers, and Windows Terminal. Only the super bit, set by kitty CSI-u and xterm modifyOtherKeys, unambiguously means Cmd. Extract that decision into isLineKillModifier so the reasoning lives in one place and the regressions above are pinned by tests.
This commit is contained in:
parent
8d112c05f7
commit
3f5ec8b45d
2 changed files with 74 additions and 2 deletions
46
ui-tui/src/__tests__/textInputLineKill.test.ts
Normal file
46
ui-tui/src/__tests__/textInputLineKill.test.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isLineKillModifier } from '../components/textInput.js'
|
||||
|
||||
// Cmd+Backspace should kill to the line boundary, but the modifier it is
|
||||
// distinguished by matters a great deal. `isActionMod` is the wrong test:
|
||||
// - on macOS it accepts `key.meta`, and hermes-ink reports Option as
|
||||
// `meta` — so Option+Backspace (delete-word, the macOS standard) would
|
||||
// silently become "delete the whole line".
|
||||
// - on Linux/Windows it is `key.ctrl`, and Ctrl+Backspace is delete-word
|
||||
// there in readline, VS Code, browsers, and Windows Terminal.
|
||||
// Only the kitty CSI-u / modifyOtherKeys `super` bit means Cmd.
|
||||
|
||||
const key = (over: Partial<{ ctrl: boolean; meta: boolean; super: boolean }> = {}) => ({
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
super: false,
|
||||
...over
|
||||
})
|
||||
|
||||
describe('isLineKillModifier', () => {
|
||||
it('accepts the super bit (Cmd via kitty CSI-u / modifyOtherKeys)', () => {
|
||||
expect(isLineKillModifier(key({ super: true }))).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts super even when the terminal also sets a benign ctrl bit', () => {
|
||||
// VS Code/Cursor forward Cmd chords as CSI-u with super + ctrl set.
|
||||
expect(isLineKillModifier(key({ ctrl: true, super: true }))).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects meta so Option+Backspace stays delete-word on macOS', () => {
|
||||
expect(isLineKillModifier(key({ meta: true }))).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects ctrl so Ctrl+Backspace stays delete-word on Linux/Windows', () => {
|
||||
expect(isLineKillModifier(key({ ctrl: true }))).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects an unmodified keypress', () => {
|
||||
expect(isLineKillModifier(key())).toBe(false)
|
||||
})
|
||||
|
||||
it('treats a missing super field as absent rather than truthy', () => {
|
||||
expect(isLineKillModifier({ ctrl: false, meta: false })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -321,6 +321,24 @@ export function resolveCursorLayout(display: string, cur: number, curRefCurrent:
|
|||
return cursorLayout(display, curRefCurrent, columns)
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a Backspace / ForwardDelete keystroke should kill to the line
|
||||
* boundary rather than delete a single word.
|
||||
*
|
||||
* Only the *super* bit qualifies. It is tempting to reuse `isActionMod`,
|
||||
* but that accepts `key.meta` on macOS — and hermes-ink reports Option as
|
||||
* `meta`, so Option+Backspace (delete-word, the macOS standard) would be
|
||||
* swallowed. On Linux/Windows `isActionMod` is `key.ctrl`, and
|
||||
* Ctrl+Backspace is delete-word there too. `super` is set only by kitty
|
||||
* CSI-u / xterm modifyOtherKeys, where it unambiguously means Cmd.
|
||||
*
|
||||
* Terminals that instead rewrite Cmd+Backspace to Ctrl+U are handled by
|
||||
* the `isMacActionFallback` kill-to-start path, not by this predicate.
|
||||
*/
|
||||
export function isLineKillModifier(key: { ctrl: boolean; meta: boolean; super?: boolean }): boolean {
|
||||
return key.super === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure computation for the fast-echo backspace bypass: given the
|
||||
* current value/cursor (already validated by `canFastBackspaceShape`),
|
||||
|
|
@ -1198,7 +1216,12 @@ export function TextInput({
|
|||
v = v.slice(0, range.start) + v.slice(range.end)
|
||||
c = range.start
|
||||
} else if (k.backspace && c > 0) {
|
||||
if (wordMod) {
|
||||
if (isLineKillModifier(k)) {
|
||||
// Cmd+Backspace — kill backward to start of line, matching the
|
||||
// Ctrl+U (unix-line-discard) path below.
|
||||
v = v.slice(c)
|
||||
c = 0
|
||||
} else if (wordMod) {
|
||||
const t = wordLeft(v, c)
|
||||
v = v.slice(0, t) + v.slice(c)
|
||||
c = t
|
||||
|
|
@ -1222,7 +1245,10 @@ export function TextInput({
|
|||
c = t
|
||||
}
|
||||
} else if (delFwd && c < v.length) {
|
||||
if (wordMod) {
|
||||
if (isLineKillModifier(k)) {
|
||||
// Cmd+ForwardDelete — kill to end of line, matching Ctrl+K.
|
||||
v = v.slice(0, c)
|
||||
} else if (wordMod) {
|
||||
const t = wordRight(v, c)
|
||||
v = v.slice(0, c) + v.slice(t)
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue