mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
* fix(tui): recognize standard DSR cursor position reports in input parser The CURSOR_POSITION_RE regex only matched DECXCPR reports (CSI ? row;col R) but not standard DSR reports (CSI row;col R without the ? marker). Terminals that respond to CSI ? 6 n with the plain DSR form had their cursor position reports fall through to parseKeypress, where they were inserted as literal text — garbling the composer input with escape sequences like ESC[22;1R. Fix: make the regex match both forms. For the standard form (no ?), only treat it as a cursor position report when row > 1, since modified F3 keys (Shift+F3 = CSI 1;2 R, etc.) always use row 1 and are genuinely ambiguous with row-1 cursor reports. * fix(tui): reject invalid row-zero DSR cursor position reports Follow-up to the standard-DSR recognition fix. The row guard rejected only row === 1, which let CSI 0;col R (row 0, no ? marker) through and misclassified it as a cursorPosition report. Terminal coordinates are 1-indexed, so row 0 is an invalid DSR report and must remain unclassified. Change the guard to row <= 1 to match the stated 'row > 1' semantics, and add a boundary test asserting CSI 0;col R is not emitted as a response. Supersedes #48762; incorporates review feedback from that PR. --------- Co-authored-by: Alex Yates <43525405+yatesjalex@users.noreply.github.com>
This commit is contained in:
parent
54459e76ed
commit
2f6a4e099b
2 changed files with 87 additions and 6 deletions
|
|
@ -131,3 +131,68 @@ describe('flush-boundary SGR mouse reassembly', () => {
|
|||
expect(key).toMatchObject({ name: 'wheelup' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('cursor position report parsing', () => {
|
||||
it('parses DECXCPR cursor position report (CSI ? row;col R)', () => {
|
||||
const [[key]] = parseMultipleKeypresses(INITIAL_STATE, '\x1b[?22;1R')
|
||||
|
||||
expect(key).toMatchObject({
|
||||
kind: 'response',
|
||||
response: { type: 'cursorPosition', row: 22, col: 1 }
|
||||
})
|
||||
})
|
||||
|
||||
it('parses standard DSR cursor position report (CSI row;col R) when row > 1', () => {
|
||||
// Terminals that don't support DECXCPR may respond to CSI ? 6 n with
|
||||
// the plain DSR form (no ?). These must be recognized as responses,
|
||||
// not inserted as literal text.
|
||||
const [[key]] = parseMultipleKeypresses(INITIAL_STATE, '\x1b[22;1R')
|
||||
|
||||
expect(key).toMatchObject({
|
||||
kind: 'response',
|
||||
response: { type: 'cursorPosition', row: 22, col: 1 }
|
||||
})
|
||||
})
|
||||
|
||||
it('parses standard DSR report with multi-digit row and col', () => {
|
||||
const [[key]] = parseMultipleKeypresses(INITIAL_STATE, '\x1b[10;80R')
|
||||
|
||||
expect(key).toMatchObject({
|
||||
kind: 'response',
|
||||
response: { type: 'cursorPosition', row: 10, col: 80 }
|
||||
})
|
||||
})
|
||||
|
||||
it('does NOT treat CSI 1;2 R as a cursor position report (Shift+F3 ambiguity)', () => {
|
||||
// CSI 1;2 R is Shift+F3 in xterm. Without the ? marker, row 1 is
|
||||
// ambiguous with F3 modifiers — must fall through to parseKeypress,
|
||||
// not be silently dropped as a terminal response.
|
||||
const [[key]] = parseMultipleKeypresses(INITIAL_STATE, '\x1b[1;2R')
|
||||
|
||||
expect(key.kind).not.toBe('response')
|
||||
})
|
||||
|
||||
it('does NOT treat CSI 1;5 R as a cursor position report (Ctrl+F3 ambiguity)', () => {
|
||||
const [[key]] = parseMultipleKeypresses(INITIAL_STATE, '\x1b[1;5R')
|
||||
|
||||
expect(key.kind).not.toBe('response')
|
||||
})
|
||||
|
||||
it('does NOT treat CSI 0;col R as a cursor position report (invalid row-zero DSR)', () => {
|
||||
// Terminal coordinates are 1-indexed, so a plain DSR report with row 0 is
|
||||
// invalid. Without the ? marker it must remain unclassified rather than be
|
||||
// reported as a cursor position (guard is row <= 1, not row === 1).
|
||||
const [[key]] = parseMultipleKeypresses(INITIAL_STATE, '\x1b[0;5R')
|
||||
|
||||
expect(key.kind).not.toBe('response')
|
||||
})
|
||||
|
||||
it('treats DECXCPR at row 1 as a cursor position report (? disambiguates from F3)', () => {
|
||||
const [[key]] = parseMultipleKeypresses(INITIAL_STATE, '\x1b[?1;2R')
|
||||
|
||||
expect(key).toMatchObject({
|
||||
kind: 'response',
|
||||
response: { type: 'cursorPosition', row: 1, col: 2 }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -44,11 +44,14 @@ const DA2_RE = /^\x1b\[>([\d;]*)c$/
|
|||
// (private ? marker distinguishes from CSI u key events)
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const KITTY_FLAGS_RE = /^\x1b\[\?(\d+)u$/
|
||||
// DECXCPR cursor position: CSI ? row ; col R
|
||||
// The ? marker disambiguates from modified F3 keys (Shift+F3 = CSI 1;2 R,
|
||||
// Ctrl+F3 = CSI 1;5 R, etc.) — plain CSI row;col R is genuinely ambiguous.
|
||||
// Cursor position report: CSI [?] row ; col R
|
||||
// DECXCPR (CSI ? row;col R) is unambiguous and always matched.
|
||||
// Standard DSR (CSI row;col R, no ?) is also matched, but only when
|
||||
// row > 1 — modified F3 keys use CSI 1;modifier R (Shift+F3 = CSI 1;2 R,
|
||||
// Ctrl+F3 = CSI 1;5 R, etc.), and F3 always has row 1. Reports at row 1
|
||||
// without the ? marker fall through to parseKeypress to preserve F3.
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const CURSOR_POSITION_RE = /^\x1b\[\?(\d+);(\d+)R$/
|
||||
const CURSOR_POSITION_RE = /^\x1b\[(\??)(\d+);(\d+)R$/
|
||||
// OSC response: OSC code ; data (BEL|ST)
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const OSC_RESPONSE_RE = /^\x1b\](\d+);(.*?)(?:\x07|\x1b\\)$/s
|
||||
|
|
@ -145,10 +148,23 @@ function parseTerminalResponse(s: string): TerminalResponse | null {
|
|||
}
|
||||
|
||||
if ((m = CURSOR_POSITION_RE.exec(s))) {
|
||||
const hasPrivateMarker = m[1] === '?'
|
||||
const row = parseInt(m[2]!, 10)
|
||||
|
||||
// Without the DEC-private '?' marker, CSI row;col R is ambiguous with
|
||||
// modified F3 keys (Shift+F3 = CSI 1;2 R, etc.). F3 always uses row 1,
|
||||
// so only treat the standard form as a cursor position report when
|
||||
// row > 1. Row <= 1 reports without '?' fall through to parseKeypress:
|
||||
// row 1 preserves F3, and row 0 is an invalid DSR report (terminal
|
||||
// coordinates are 1-indexed) that should remain unclassified.
|
||||
if (!hasPrivateMarker && row <= 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'cursorPosition',
|
||||
row: parseInt(m[1]!, 10),
|
||||
col: parseInt(m[2]!, 10)
|
||||
row,
|
||||
col: parseInt(m[3]!, 10)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue