Skip to content

Fix ghost text never rendering on pi-tui >= 1.4 or with a framed editor - #2

Open
bitnom wants to merge 1 commit into
jcv:mainfrom
Apothic-AI:fix/ghost-render-editor-chrome
Open

bitnom wants to merge 1 commit into
jcv:mainfrom
Apothic-AI:fix/ghost-render-editor-chrome

Conversation

@bitnom

@bitnom bitnom commented Oct 28, 2026

Copy link
Copy Markdown

The ghost silently fails to appear in two situations. Neither logs anything and neither errors — the request runs, the status indicator shows `suggesting...`, and `renderGhostText` just returns the lines untouched.

1. pi-tui >= 1.4 changed how the cursor cell is encoded

pi-tui 1.4 wraps the drawn cursor in zero-width APC markers and resolves them to inverse video later:

```js
export const CURSOR_MARKER = "\x1b_pi:c\x07";
export const FAKE_CURSOR_START = "\x1b_pi:fc\x07";
export const FAKE_CURSOR_END = "\x1b_pi:/fc\x07";
// resolveFakeCursors() later maps these to \x1b[7m / \x1b[27m
```

The cursor skip assumed an SGR sequence. On an APC marker it consumed one ESC byte, then failed the trailing-padding check and bailed.

2. A framing extension leaves a border at end of line

pi-pretty-tui patches `CustomEditor.prototype.render` — the very class this editor extends — to re-render at `width - 2` and wrap each row:

```js
const innerWidth = width - 2;
const lines = originalEditorRender.call(this, innerWidth);
if (index < bottomBorderIndex) return this.borderColor("│") + fitted + this.borderColor("│");
```

That leaves `│` after the padding, so requiring "spaces to end of line" rejected it. Reproduced by copying that patch verbatim:

```
plain layout -> ghost drawn: true
pretty-tui -> ghost drawn: false (cursor row ends with │)
```

Fix

One matcher skips SGR sequences and APC strings alike:

```js
const DECOR = String.raw`(?:\x1b[[0-9;:]m|\x1b_[^\x07]\x07)`;
```

The padding match now captures whatever chrome follows the blank run, reserves its visible width so the ghost can never overdraw the frame, and re-emits it unchanged. A row with no usable blank run, or with a second cursor marker, still bails — so the original "don't touch a layout you don't understand" guard survives for genuinely unknown content, while a border after real padding is correctly treated as chrome.

Verification

18 tests pass. New cases cover the APC spelling, a styled frame border (SGR-wrapped, as `borderColor` produces), and confirm the untouched-content guard still refuses.

Both failures are invisible at runtime, which is what made them expensive to find; the tests are the guard against a third one.

…s the editor

Two independent reasons the ghost silently failed to appear, both in
renderGhostText. Neither produced an error; the request ran, the status
indicator showed, and nothing was ever drawn.

1. pi-tui >= 1.4 wraps the cell drawn as the hardware cursor in zero-width APC
   markers (`ESC _ pi:fc BEL` ... `ESC _ pi:/fc BEL`) and resolves them to
   inverse video later, in resolveFakeCursors(). The skip logic only understood
   SGR runs, so it consumed a single ESC byte, fell through to the padding check
   and returned the lines untouched.

2. Any extension that frames the editor by patching CustomEditor.prototype.render
   -- pi-pretty-tui does exactly this, re-rendering at width - 2 and wrapping each
   row in border columns -- leaves a `|` at end of line. Requiring the rest of the
   row to be spaces then failed on that border.

The padding match now skips SGR and APC decorations alike, captures whatever chrome
follows the blank run, reserves its visible width so the ghost cannot overdraw the
frame, and re-emits it byte-for-byte. A row with no usable blank run, or with a
second cursor marker, still bails, so genuinely unexpected layouts are left alone.

18 tests pass, covering both cursor spellings, a styled frame border, and the
unchanged-content guard.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant