Skip to content

Input component horizontal scrolling uses value.length instead of visibleWidth(), causing TUI crash with CJK/fullwidth text #1982

@saemanas

Description

@saemanas

Input component horizontal scrolling uses value.length instead of visibleWidth(), causing TUI crash with CJK/fullwidth text

Bug Description

The Input component's render() method uses this.value.length (character count) to decide whether horizontal scrolling is needed. For CJK characters and other double-width Unicode text, character count significantly underestimates the actual display width, causing rendered lines to exceed terminal width and crash the TUI.

Root Cause

In packages/tui/src/components/input.ts, the render() method:

if (this.value.length < availableWidth) {
    // Everything fits (leave room for cursor at end)
    visibleText = this.value;
}

this.value.length returns the character count, but availableWidth is in terminal columns. CJK characters occupy 2 columns each, so a 60-character Korean string can be 120 columns wide. The condition passes incorrectly, no scrolling is applied, and the full text overflows.

Additionally, the scrolling branch has the same class of bug -- scrollWidth, halfWidth, and cursor offset calculations all treat character offsets as column positions:

const scrollWidth = this.cursor === this.value.length ? availableWidth - 1 : availableWidth;
const halfWidth = Math.floor(scrollWidth / 2);
// ...
if (this.cursor < halfWidth) { ... }

Reproduction

Minimal Node.js script (requires @mariozechner/pi-tui):

import { Input, visibleWidth } from "@mariozechner/pi-tui";

const WIDTH = 93;
const AVAIL = WIDTH - 2; // "> " prompt

const cases = [
  ["Korean",          "가나다라마바사아자차카타파하 한글 텍스트가 터미널 너비를 초과하면 크래시가 발생합니다 이것은 재현용 테스트입니다"],
  ["Japanese",        "これはテスト文章です。日本語のテキストが正しく表示されるかどうかを確認するためのサンプルテキストです。あいうえお"],
  ["Chinese",         "这是一段测试文本,用于验证中文字符在终端中的显示宽度是否被正确计算,如果不正确就会导致用户界面崩溃的问题"],
  ["Fullwidth ASCII", "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklm"],
];

for (const [label, text] of cases) {
  const input = new Input();
  input.setValue(text);
  input.focused = true;

  const rendered = visibleWidth((input.render(WIDTH))[0] ?? "");
  const overflow = rendered > WIDTH;

  console.log(`${label}: chars=${text.length}, visibleWidth=${visibleWidth(text)}, rendered=${rendered} ${overflow ? "OVERFLOW" : "OK"}`);
}

Output:

Korean:          chars=61,  visibleWidth=112, rendered=114 OVERFLOW
Japanese:        chars=56,  visibleWidth=112, rendered=114 OVERFLOW
Chinese:         chars=52,  visibleWidth=104, rendered=106 OVERFLOW
Fullwidth ASCII: chars=49,  visibleWidth=98,  rendered=100 OVERFLOW

The TUI crashes with:

Error: Rendered line 118 exceeds terminal width (130 > 93).
This is likely caused by a custom TUI component not truncating its output.

Affected Contexts

Any Input-based UI with CJK/fullwidth text long enough that value.length < availableWidth but visibleWidth(value) >= availableWidth. Observed in the /todos command search field, but applies to all Input usages (model selector filter, extension search inputs, etc.).

Suggested Fix

  1. Replace this.value.length with visibleWidth(this.value) for the scroll-needed check.
  2. Rewrite the scrolling window logic to use visibleWidth() for all column calculations instead of character offsets.

Minimal diff for the guard (step 1):

- if (this.value.length < availableWidth) {
+ if (visibleWidth(this.value) < availableWidth) {

Step 2 is more involved -- the entire scrolling branch (scrollWidth, halfWidth, cursor positioning) needs to track visible width rather than string length.

Related Issues

Environment

  • pi-coding-agent: 0.57.1
  • pi-tui: 0.57.1
  • Terminal width: 93
  • Node.js: v25.4.0
  • OS: macOS (arm64)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions