Skip to content

TUI history and live assistant output can diverge from terminal wrapping at the right edge #22109

@Rokurolize

Description

@Rokurolize

What happened?

Codex TUI can render visually broken history/live output when Codex's logical
wrap model and the terminal's physical wrap behavior disagree at the right
edge.

The symptom is not limited to Japanese text. Japanese glyphs are just easy to
notice when they land at the right edge. The primary Codex-facing cases I found
are:

  • normal live assistant output,
  • finalized assistant history with the prefix,
  • /goal/status info rows such as • Goal active Objective: ....

The same invariant can also appear with boundary content such as long URL-like
tokens, emoji / VS16 / ZWJ sequences, long fenced-code-block lines, and
TSV-like text containing literal tab characters.

Typical visual shape:

Codex logical wrap:
• ... remote endpoint再実行で
  はなく、保存済み...

Terminal visual wrap:
• ... remote endpoint再実行
で
  はなく、保存済み...

The extra physical row comes from the terminal auto-wrapping differently from
Codex's retained history lines. After that, Codex's next continuation line is
written below the terminal's unexpected row, so the transcript looks shifted or
fragmented.

I also saw the same class on normal live assistant output at 120 columns: a
line that should have kept 通常の応答文でも出た together was able to split at the
right edge as ...でも / / . That suggests live streaming and finalized
history need the same conservative right-edge policy; neither path should let
the terminal perform the first physical wrap.

Environment

  • Windows Terminal Stable 1.24.10921.0 and Windows Terminal Canary
    1.26.1271.0 were installed.
  • Active terminal during measurement: Windows Terminal Canary
    Microsoft.WindowsTerminalCanary_1.26.1271.0_x64__8wekyb3d8bbwe.
  • WSL2 Ubuntu 24.04.4.
  • Windows culture/system locale: ja-JP.
  • Codex process locale: C.UTF-8.
  • Upstream checked on 2026-05-11 at
    2abdeb34d5b7a0bbdf082ce8be1d5dae6c645ffd.

Direct CPR measurement in the active terminal showed the current profile renders
, , , and as one cell, while , ❤️, and 👨‍🦰 advance as two
cells. So this report should not be read as "current Windows Terminal Canary
default makes wide". The broader problem is that Codex history/live output
can exact-fit the right edge or leave terminal-sensitive text to the terminal,
and even a one-cell disagreement creates an extra physical row.

Why I think upstream is still affected

On current upstream/main:

  • ChatWidget::current_stream_width() subtracts only the caller's reserved
    prefix/gutter columns and does not reserve an additional history right-edge
    guard.
  • AgentMessageCell wraps live-streamed assistant lines with
    RtOptions::new(width as usize) and the / two-space indentation.
  • Finalized AgentMarkdownCell reserves exactly two columns for the /
    two-space prefix before prefixing rendered Markdown lines.
  • new_info_event() builds a PlainHistoryCell, so long status rows like
    • Goal active Objective: ... do not get a wrapped continuation prefix.
  • URL-like wrapping still preserves overlong URL/path/domain tokens in some
    cases where the terminal can do the first wrap.
  • Markdown code-block tests still assert long code-block lines are not wrapped.

Observed Codex triggers

The strongest observed Codex-facing triggers are:

  1. A normal live assistant response at a 120-column viewport. The phrase
    通常の応答文でも出た should have stayed on one logical row, but the terminal
    split it at the right edge as ...でも / / .
  2. A long status/history info row such as • Goal active Objective: ....
    Current upstream builds new_info_event() as a PlainHistoryCell, so this
    kind of row can bypass a wrapped prefixed history-cell path.
  3. A finalized assistant history row where the selected text showed Codex's
    logical wrap as • ... remote endpoint再実行で followed by はなく...,
    while the terminal displayed on its own physical row before Codex's
    continuation line.

Minimal standalone reproducer for the exact-fit Unicode class

import unicodedata

cols = 159
text = (
    "• 完了監査前に、「多数の保存済みpayloadを使った」という要件を代理信号にしないため、"
    "286 requestの棚卸しから因子別の集計も出しておきます。remote endpoint再実行ではなく、"
    "保存済み実payloadのsafe metricsに対する追加分析です。"
)

def width(s, ambiguous_wide):
    total = 0
    for ch in s:
        if unicodedata.combining(ch):
            continue
        eaw = unicodedata.east_asian_width(ch)
        total += 2 if eaw in {"W", "F"} or (ambiguous_wide and eaw == "A") else 1
    return total

row = ""
for ch in text:
    if width(row + ch, False) > cols:
        break
    row += ch

print(width(row, False), width(row, True), row[-24:])
assert width(row, False) == 159
assert width(row, True) == 160

Output:

159 160 出しておきます。remote endpoint再実行で

This models one important class of the bug: the row exactly fits Codex's narrow
width model but is one cell too wide in a terminal/model that renders an earlier
ambiguous character as wide.

Additional boundary classes

The same invariant also applies to other content that can leave the first wrap
to the terminal:

  • long URL-like tokens that the wrapper preserves past the viewport edge,
  • emoji / VS16 / ZWJ sequences where static width tables and terminal shaping
    disagree,
  • long fenced-code-block lines that are intentionally not wrapped,
  • TSV-like text containing literal tab characters and terminal tab stops.

Expected behavior

History rows and live assistant output written to the terminal should not be
able to diverge from Codex's logical transcript layout because of a one-cell
width disagreement, overlong URL token, unwrapped code block, emoji shaping, or
literal tab stop.

At minimum, Codex should wrap TUI history/live rows before the physical terminal
does and measure height using the same content it actually emits.

Possible fix directions

  • Use the same conservative width budget for live assistant streaming and
    finalized assistant history. If finalized history reserves a right-edge guard,
    live streaming must reserve it too.
  • Consider a conservative one-column right margin for scrollback/history rows
    that are prefixed, styled, or otherwise vulnerable to terminal width
    disagreement. This should be treated as an off-by-one guard, not a complete
    Unicode/emoji width solution.
  • Render info/status rows through a wrapped prefixed history cell instead of a
    plain one-line cell when they can contain long dynamic text.
  • Hard-wrap overlong URL-like tokens only after preserving semantic URL split
    preference; avoid letting the terminal perform the first wrap.
  • Wrap displayed code-block lines when a viewport width is known, while keeping
    raw/copy mode source-preserving.
  • Normalize or explicitly expand literal tabs before Markdown history wrapping.
  • Longer term: expose a TUI Unicode width policy that can match terminals using
    ambiguous-wide behavior, while still keeping a conservative guard for emoji
    and renderer-specific cases that static Unicode tables cannot fully predict.

Related issues

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    TUIIssues related to the terminal user interface: text input, menus and dialogs, and terminal displaybugSomething isn't workingwindows-osIssues related to Codex on Windows systems

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions