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:
- 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 ...でも / 出 / た.
- 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.
- 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
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:
•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:
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 theright edge as
...でも/出/た. That suggests live streaming and finalizedhistory need the same conservative right-edge policy; neither path should let
the terminal perform the first physical wrap.
Environment
1.26.1271.0 were installed.
Microsoft.WindowsTerminalCanary_1.26.1271.0_x64__8wekyb3d8bbwe.ja-JP.C.UTF-8.2abdeb34d5b7a0bbdf082ce8be1d5dae6c645ffd.Direct CPR measurement in the active terminal showed the current profile renders
•,→,…, and─as one cell, whileで,❤️, and👨🦰advance as twocells. So this report should not be read as "current Windows Terminal Canary
default makes
•wide". The broader problem is that Codex history/live outputcan 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 reservedprefix/gutter columns and does not reserve an additional history right-edge
guard.
AgentMessageCellwraps live-streamed assistant lines withRtOptions::new(width as usize)and the•/ two-space indentation.AgentMarkdownCellreserves exactly two columns for the•/two-space prefix before prefixing rendered Markdown lines.
new_info_event()builds aPlainHistoryCell, so long status rows like• Goal active Objective: ...do not get a wrapped continuation prefix.cases where the terminal can do the first wrap.
Observed Codex triggers
The strongest observed Codex-facing triggers are:
通常の応答文でも出たshould have stayed on one logical row, but the terminalsplit it at the right edge as
...でも/出/た.• Goal active Objective: ....Current upstream builds
new_info_event()as aPlainHistoryCell, so thiskind of row can bypass a wrapped prefixed history-cell path.
logical wrap as
• ... remote endpoint再実行でfollowed byはなく...,while the terminal displayed
でon its own physical row before Codex'scontinuation line.
Minimal standalone reproducer for the exact-fit Unicode class
Output:
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:
disagree,
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
finalized assistant history. If finalized history reserves a right-edge guard,
live streaming must reserve it too.
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.
plain one-line cell when they can contain long dynamic text.
preference; avoid letting the terminal perform the first wrap.
raw/copy mode source-preserving.
ambiguous-wide behavior, while still keeping a conservative guard for emoji
and renderer-specific cases that static Unicode tables cannot fully predict.
Related issues
exact-fit terminal width policy, live/finalized width mismatch, or
long-token/code-block/tab classes.
rendered width mismatch, but it is not history rendering.
This issue identifies a current right-edge terminal/Codex wrap-model
divergence class rather than a general cut-off symptom.
corruption/layout mismatch bug.
References
https://www.unicode.org/reports/tr11/
https://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt
https://docs.rs/textwrap/latest/textwrap/core/fn.display_width.html
Add configurable ambiguous-width policy (narrow/wide) microsoft/terminal#19864
https://wezterm.org/config/lua/config/treat_east_asian_ambiguous_width_as_wide.html