hermes-agent/ui-tui/src/__tests__/textInputLineNav.test.ts
Brooklyn Nicholson d30f6ac44e fix(tui): up-arrow inside a multi-line buffer moves cursor, not history
Reported during TUI v2 blitz retest: typing a multi-line message with
shift-Enter and then pressing Up to edit an earlier line swapped the
whole buffer for the previous history entry instead of moving the
cursor up a line.  Down then restored the draft → the buffer appeared
to "flip" between the draft and a prior prompt.

`useInputHandlers` cycles history on Up/Down, but textInput only
checked `inputBuf.length` — that only counts lines committed with a
trailing backslash, not shift-Enter newlines inside `input` itself.

Fix: detect logical lines inside the input string and move the cursor
one line up/down preserving column offset (clamp to line end when the
destination is shorter, standard editor behavior).  Only fall through
to history cycling when the cursor is already on the first line (Up)
or last line (Down).

Adds unit coverage for the new `lineNav` helper.
2026-04-21 18:31:35 -05:00

55 lines
1.7 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { lineNav } from '../components/textInput.js'
describe('lineNav', () => {
it('returns null for single-line input (up)', () => {
expect(lineNav('hello world', 6, -1)).toBeNull()
})
it('returns null for single-line input (down)', () => {
expect(lineNav('hello world', 6, 1)).toBeNull()
})
it('returns null when cursor already on first line of a multiline block', () => {
expect(lineNav('one\ntwo\nthree', 2, -1)).toBeNull()
})
it('returns null when cursor on last line of a multiline block', () => {
expect(lineNav('one\ntwo\nthree', 10, 1)).toBeNull()
})
it('moves cursor up one line preserving column', () => {
// "hello\nworld" — cursor at col 3 of line 1 ('l' in world) → col 3 of line 0 ('l' in hello)
expect(lineNav('hello\nworld', 9, -1)).toBe(3)
})
it('moves cursor down one line preserving column', () => {
// cursor at col 2 of line 0 → col 2 of line 1
expect(lineNav('hello\nworld', 2, 1)).toBe(8)
})
it('clamps to end of shorter destination line on up', () => {
// col 10 on long line → clamp to end of short line "abc"
const s = 'abc\nlong long text'
const from = 14
expect(lineNav(s, from, -1)).toBe(3)
})
it('clamps to end of shorter destination line on down', () => {
// col 10 on line 0 → clamp to end of "abc" on line 1
const s = 'long long text\nabc'
expect(lineNav(s, 10, 1)).toBe(18)
})
it('handles empty lines correctly', () => {
// "a\n\nb" — cursor at line 2 (b) → up to empty line 1
expect(lineNav('a\n\nb', 3, -1)).toBe(2)
})
it('handles leading newline without crashing', () => {
expect(lineNav('\nfoo', 2, -1)).toBe(0)
})
})