Skip to content

fix(tui): plus-key bindings never match, NFD combining-mark width over-count, theme-dependent nested-list mis-render - #11633

Merged
lalalune merged 1 commit into
developfrom
fix/tui-render-utils-batch
Jul 2, 2026
Merged

lalalune merged 1 commit into
developfrom
fix/tui-render-utils-batch

Conversation

@NubsCarson

Copy link
Copy Markdown
Member

Summary

Three provable rendering/input bugs in @elizaos/tui (used by the agent CLI and the eliza-code cockpit surface), each with a concrete failing input, a minimal fix, and a mutation-checked regression test.

1. keys.ts — bindings on the + key can never match

"+" is a valid BaseKey (the KeyId type advertises "ctrl++" etc. and Key.plus exists), but parseKeyId() splits the keyId on "+", so the base key parses as the empty string and the function returns null.

Failure (pre-fix, reproduced live):

  • matchesKey("+", "+")false (typing a plus sign never matches a + binding)
  • matchesKey("\x1b[43;5u", "ctrl++")false (Kitty ctrl+plus, codepoint 43 mod 5)

Fix: a keyId whose last split part is empty and that ends with "+" resolves its base key to the literal "+". "ctrl+-" and every other keyId are untouched (asserted).

2. utils.tsvisibleWidth() counts NFD graphemes with 2+ combining marks as width-2 emoji

couldBeEmoji() treated any grapheme longer than 2 code units as an emoji (width 2). A base letter with two combining marks — e.g. Vietnamese ế in NFD (e + U+0302 + U+0301, which is exactly what macOS NFD filenames contain) — is 3 code units, so:

Failure (pre-fix, reproduced live): visibleWidth("ế") = 2 but visibleWidth("ế") (the identical visible character, NFC) = 1. The same glyph measured two different widths, corrupting truncateToWidth padding, wrap points, and markdown table column alignment for any NFD input.

Fix: replace the length > 2 heuristic with a ZWJ (U+200D) check. Multi-codepoint emoji remain covered: skin-tone modifiers, regional-indicator flags and ZWJ families start with a codepoint in the existing emoji ranges, keycaps carry VS16 — all re-asserted at width 2 in the new tests.

3. markdown/list-renderer.ts — nested-list detection hardcodes the cyan escape \x1b[36m

renderList() classified already-rendered lines as "nested list" by regex-matching a hardcoded basic-cyan SGR prefix. With colors disabled (NO_COLOR, piped/non-TTY output → chalk level 0) or any theme whose listBullet isn't exactly \x1b[36m (e.g. truecolor), detection fails.

Failure (pre-fix, reproduced live with an identity theme):

  • - a\n - b["- a", " - b"] (nested item double-indented)
  • - - b (marked genuinely emits an item whose first token is a nested list) → "- - b" (extra parent bullet, garbage output)

Fix: tag lines structurally at the source — internal renderListItemLines() returns { text, nested } so renderList() no longer sniffs theme-specific ANSI. The exported renderListItem() keeps its string[] signature (delegates), so no API break.

Tests

  • test/keys.test.ts: +5 tests (typed +, Kitty ctrl++/ctrl+shift++, negatives, ctrl+- guard)
  • test/visible-width.test.ts (new): +9 tests (NFC/NFD equality, two-combining-marks width, NFD string equality, NFD truncate+pad alignment; emoji guards: single-codepoint, ZWJ family, skin tone, flag, keycap)
  • test/markdown.test.ts: +3 tests (NO_COLOR nested indent, no extra bullet on nested-first item, continuation indent)

Evidence

  • Full suite: 25 files, 485 passed | 11 skipped, 0 failures (bunx vitest run packages/tui/test)
  • Typecheck: bun run --cwd packages/tui typecheck clean
  • Mutation checks (each fix reverted individually, tests re-run, then restored):
    • revert keys.ts fix → 3 failures in keys.test.ts ✅ caught
    • revert utils.ts fix → 3 failures in visible-width.test.ts ✅ caught
    • revert list-renderer.ts fix → 2 failures in markdown.test.ts ✅ caught
  • Biome: newly added/edited code clean; remaining findings in keys.ts/markdown.test.ts pre-exist byte-identical on origin/develop (tui has no lint gate)
  • Real-LLM trajectories: N/A — pure rendering/input library, no model/agent/prompt behavior touched
  • Screenshots / video / device capture: N/A — no packages/app UI surface; deterministic string-level renderer output asserted directly in unit tests (before/after rendered lines shown above)
  • Backend/frontend logs: N/A — no server or browser code path involved

Only packages/tui/src + packages/tui/test touched. packages/examples/code untouched.


Authored end-to-end by a Fable-5 agent (find + fix + tests + mutation-checks + push, single pass). Independently re-verified by the main loop: 6-file merge-base diff, 90/90 tests pass on a fresh run, and the utils.ts NFD-width mutation reproduced independently (revert → 3 fail; restore → green). — [phone-ui]

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59a58be9-9de3-4165-978f-e5bf6e5c1ffb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tui-render-utils-batch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ent nested-list detection

- keys.ts: parseKeyId() returned null for any keyId whose base key is the
  literal "+" symbol ("+", "ctrl++", ...) because split("+") yields a
  trailing empty part, so those bindings could never match despite being
  valid KeyIds. Resolve a trailing "+" separator to the "+" key.

- utils.ts: couldBeEmoji() treated every grapheme longer than 2 code units
  as an emoji (width 2), so decomposed (NFD) characters with two combining
  marks (e.g. Vietnamese ế as e+U+0302+U+0301) measured width 2 while
  their NFC form measured 1, breaking truncation/padding/wrap alignment for
  NFD text. Detect multi-codepoint emoji via ZWJ instead; skin tones, flags,
  keycaps and ZWJ families are still caught by the existing range/VS16 checks.

- markdown/list-renderer.ts: nested-list detection sniffed rendered lines
  for a hardcoded cyan SGR prefix (\x1b[36m). With colors disabled
  (NO_COLOR, piped output) or any non-cyan listBullet theme, nested lists
  got double indentation and items starting with a nested list grew an
  extra bullet ("-   - b"). Tag nested-list lines structurally instead of
  sniffing ANSI codes.
@lalalune
lalalune force-pushed the fix/tui-render-utils-batch branch from 30c34fd to de90b9f Compare July 2, 2026 22:18

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@lalalune lalalune left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the rebased diff and validated the TUI fixes locally.

Evidence:

  • bunx vitest run test/keys.test.ts test/visible-width.test.ts test/markdown.test.ts in packages/tui: 3 files / 90 tests passed.
  • bun run test in packages/tui: 26 files / 496 tests passed.
  • bun run typecheck in packages/tui: passed.
  • git diff --check origin/develop...HEAD && git diff --check: passed.
  • bunx @biomejs/biome@2.5.2 check src/components/markdown/list-renderer.ts src/utils.ts test/visible-width.test.ts test/keys.test.ts: passed.

I also ran Biome across all touched files; it reports existing diagnostics in src/keys.ts and test/markdown.test.ts that are already present on develop (control-regex/non-null assertions), and the PR's new hunks do not add those patterns.

@lalalune
lalalune merged commit 105b3ad into develop Jul 2, 2026
33 of 37 checks passed
@lalalune
lalalune deleted the fix/tui-render-utils-batch branch July 2, 2026 22:19
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions github-actions Bot added the Tests label Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants