fix(runtime): a write op at the right clip edge still runs its transformers (Ink parity) (#109)

The whole-op horizontal clip skip used `x >= clipH.x2`, dropping a write op that
starts exactly AT the right clip edge. Ink's skip is strict `x > clip.x2`
(output.ts:188): at x === clip.x2 it proceeds to clip each line to empty
(sliceAnsi 0,0) and runs the transformers on the empty slice. A transformer that
produces output from empty input (e.g. `() => '中'`, `s => s + 'X'`) emits at the
clip edge in Ink but was short-circuited away in vue. Changed `>=` to `>`. Normal
and identity ops at x === clip.x2 still emit nothing (empty slice →
characters.length === 0 → skip); the inner per-line clip already used strict `>`.

Tests lock the transform-on-empty + append-on-empty cases (Ink "    中" / "    X")
and the identity/plain controls (""). Also drops a stale comment reference to the
deleted parity-ledger.md.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-06-01 01:14:28 +08:00
committed by GitHub
parent e68e36f26b
commit 99839397c6
2 changed files with 93 additions and 4 deletions
+10 -3
View File
@@ -168,8 +168,15 @@ class Output {
? { x1: clip.x1, x2: clip.x2 }
: null;
// Safe early skip: entire write starts at or past right clip edge
if (clipH && x >= clipH.x2) continue;
// Safe early skip: entire write starts strictly PAST the right clip edge.
// Must be strict `>` (not `>=`), matching Ink output.ts:188 (`x > clip.x2`):
// an op that starts AT x === clip.x2 still has to take the per-line clip path
// so its transformers run on the (empty) clipped slice. A transformer that
// produces output from empty input (e.g. `() => '中'`) emits AT the clip edge;
// a whole-op `>=` skip would wrongly drop it. The inner per-line clip below
// already uses strict `>`, so for normal/identity ops x === clip.x2 still
// clips to empty → characters.length === 0 → skip (no net output).
if (clipH && x > clipH.x2) continue;
let offsetY = 0;
@@ -205,7 +212,7 @@ class Output {
// whole and the kept content begins at this origin with NO leading
// offset. (We deliberately do NOT advance the origin by the dropped
// glyph's extra column — that produced a vue-tui-specific leading space
// that Ink never emits; see G63 decisions-log in parity-ledger.md.)
// that Ink never emits.)
if (lineX < clipH.x1) lineX = clipH.x1;
const maxWidth = clipH.x2 - lineX;
line = safeSliceEnd(sliceAnsi(line, from, to), maxWidth);