mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
fix: read a row-addressed repaint as lines in the preview
Measured against a live Claude pane: an Ink TUI paints by ROW and emits
almost no newlines, so dropping cursor-position sequences collapsed a whole
screen into one unreadable line, and a tail cut mid-sequence printed the
remains of it (";1H") as text. Now a jump to column 1 starts a display line,
a jump inside a row moves the write position (capped, since a stream may
address a column no terminal has), and a severed CSI head is dropped before
parsing.
The preview is readable against a real session as a result: tool calls, the
working line and the composer all land where they belong.
Also drop the repeated session name from a search row, whose snippet opens
with the name the row already shows in its first column.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
clipStyledLine,
|
||||
dropSeveredEscape,
|
||||
padDisplay,
|
||||
stripStyles,
|
||||
toDisplayLines,
|
||||
@@ -42,14 +43,31 @@ describe('toDisplayLines', () => {
|
||||
expect(toDisplayLines('\x1b]0;window title\x1b\\text')).toEqual(['text']);
|
||||
});
|
||||
|
||||
it('strips DECSET/DECRST, cursor movement and charset selection', () => {
|
||||
it('strips DECSET/DECRST, relative cursor movement and charset selection', () => {
|
||||
expect(toDisplayLines('\x1b[?25lvisible\x1b[?25h')).toEqual(['visible']);
|
||||
expect(toDisplayLines('a\x1b[5Cb')).toEqual(['ab']);
|
||||
expect(toDisplayLines('\x1b[2J\x1b[H\x1b[1;1Hhome')).toEqual(['home']);
|
||||
expect(toDisplayLines('\x1b(0lqk\x1b(B')).toEqual(['lqk']);
|
||||
expect(toDisplayLines('\x1b=app\x1b>')).toEqual(['app']);
|
||||
});
|
||||
|
||||
it('splits a row-addressed repaint into lines, which is how an Ink TUI paints', () => {
|
||||
// Claude Code emits almost no newlines: without this the whole screen is
|
||||
// one line and nothing in the preview is readable.
|
||||
expect(toDisplayLines('\x1b[1;1Hfirst\x1b[2;1Hsecond\x1b[3;1Hthird')).toEqual(['', 'first', 'second', 'third']);
|
||||
// A jump inside a row is a write position, not a new line.
|
||||
expect(toDisplayLines('\x1b[1;1Hab\x1b[1;5Hcd')).toEqual(['', 'ab cd']);
|
||||
expect(toDisplayLines('\x1b[1;1Habcdef\x1b[1;2HXY')).toEqual(['', 'aXYdef']);
|
||||
// Both parameters default to 1, so a bare CUP is a fresh row.
|
||||
expect(toDisplayLines('a\x1b[Hb')).toEqual(['a', 'b']);
|
||||
expect(toDisplayLines('a\x1b[3;1fb')).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('refuses to allocate a line for a column no terminal has', () => {
|
||||
const lines = toDisplayLines('\x1b[1;99999Hx');
|
||||
expect(lines).toHaveLength(1);
|
||||
expect(visibleWidth(lines[0])).toBeLessThanOrEqual(1001);
|
||||
});
|
||||
|
||||
it('strips C1 controls and their sequences', () => {
|
||||
expect(toDisplayLines('a\x9b31mb')).toEqual(['ab']);
|
||||
expect(toDisplayLines('a\x9d0;title\x9cb')).toEqual(['ab']);
|
||||
@@ -89,6 +107,20 @@ describe('toDisplayLines', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('dropSeveredEscape', () => {
|
||||
it('drops the remains of a sequence a byte-sliced tail starts inside', () => {
|
||||
expect(dropSeveredEscape(';1Hstill here')).toBe('still here');
|
||||
expect(dropSeveredEscape('12;3Htext')).toBe('text');
|
||||
expect(dropSeveredEscape('31mred')).toBe('red');
|
||||
});
|
||||
|
||||
it('leaves ordinary text alone', () => {
|
||||
expect(dropSeveredEscape('hello world')).toBe('hello world');
|
||||
expect(dropSeveredEscape('\x1b[31mred')).toBe('\x1b[31mred');
|
||||
expect(dropSeveredEscape('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('visibleWidth', () => {
|
||||
it('ignores escape sequences', () => {
|
||||
expect(visibleWidth(`${RED}abc${RESET}`)).toBe(3);
|
||||
|
||||
@@ -332,7 +332,7 @@ describe('search results', () => {
|
||||
sessionId: 'live-1',
|
||||
sessionName: 'w1-alpha',
|
||||
timestamp: NOW,
|
||||
snippet: '/tmp/alpha',
|
||||
snippet: 'w1-alpha — /tmp/alpha',
|
||||
exactMatch: true,
|
||||
jumpTo: { kind: 'session', sessionId: 'live-1' },
|
||||
},
|
||||
@@ -368,6 +368,8 @@ describe('search results', () => {
|
||||
expect(entries.map((entry) => entry.kind)).toEqual(['header', 'result', 'result', 'header', 'result']);
|
||||
expect(entries[0].text).toBe('SESSIONS');
|
||||
expect(entries[1]).toMatchObject({ text: 'w1-alpha', sessionId: 'live-1', live: true });
|
||||
// The snippet opens with the session name, which the row already shows.
|
||||
expect(entries[1].detail).toBe('/tmp/alpha');
|
||||
// A session that is not on the list cannot be selected into.
|
||||
expect(entries[2]).toMatchObject({ text: 'w9-old', live: false });
|
||||
expect(entries[4]).toMatchObject({ text: 'docs/notes.md', live: false });
|
||||
|
||||
Reference in New Issue
Block a user