Ctrl+T transcript diff leaves stale glyphs when source lines contain tabs
What version of Codex CLI is running?
Current source checkout at 69c48bfc5b83bc8f5a5b9b4120a3114d84e41c9f (codex-tui workspace version 0.144.0). (issue since very beginning of full transcript TUI render view)
Which model were you using?
GPT-5.6 Sol, although this is model-independent TUI rendering.
What platform is your computer?
Linux 6.17.0-35-generic x86_64, Ghostty 1.3.1.
What issue are you seeing?
The full transcript overlay opened with Ctrl+T renders patch/diff rows with stale trailing glyphs and fragments of previously rendered text when the changed source contains literal tab indentation.
The corruption appears inside the colored added/deleted rows. Text after a tab can be shifted relative to Codex's logical buffer, while cells that should have been replaced remain visible as overlapping fragments. Non-diff transcript rows remain stable, and space-indented diff rows do not show the same pattern.
This is especially easy to reproduce with Go patches because gofmt uses tabs for leading indentation.
What steps can reproduce the bug?
- Run Codex in a Go repository.
- Have Codex apply a patch containing several tab-indented added and removed lines, for example:
func example() error {
value, err := loadValue()
if err != nil {
return err
}
return useValue(value)
}
- Press
Ctrl+Tto open the full transcript viewer. - Scroll to the
Edited N filespatch cell. - Observe the added/deleted rows containing tab indentation.
The background color is painted, but code text is shifted and stale glyph fragments remain within the row. Reopening or scrolling the transcript can produce the same corruption.
Expected behavior
Patch cells in the full transcript should render identically and remain cell-aligned regardless of whether source indentation uses tabs or spaces. No literal tab control character should be emitted to the terminal backend.
Actual behavior
The diff wrapper accounts for a tab as four logical columns while retaining the literal \t in the rendered span. The physical terminal interprets that control character using its own tab stops, so its cursor position diverges from Ratatui's buffer position. Subsequent incremental cell updates then leave stale or overlapping glyphs.
Source analysis
wrap_styled_spans() explicitly assigns tabs TAB_WIDTH logical columns:
However, the accumulated chunk is converted directly back into an RtSpan, preserving the literal tab:
let (chunk, rest) = remaining.split_at(byte_end);
current_line.push(RtSpan::styled(chunk.to_string(), style));
The custom terminal width helper uses Unicode display width, for which tabs have no printable width, and the draw path ultimately prints the cell symbol to the terminal:
https://github.com/openai/codex/blob/69c48bfc5b83bc8f5a5b9b4120a3114d84e41c9f/codex-rs/tui/src/custom_terminal.rs#L49-L80
https://github.com/openai/codex/blob/69c48bfc5b83bc8f5a5b9b4120a3114d84e41c9f/codex-rs/tui/src/custom_terminal.rs#L650-L688
This explains why the logical wrapping calculation and physical cursor movement disagree specifically at tabs.
Suggested fix
Expand each tab to TAB_WIDTH styled spaces before constructing the output RtSpan, while preserving the existing wrapping decision and syntax style. Doing this in the diff wrapper is preferable to expanding it in the terminal backend, because the wrapper owns the intended four-column layout and the terminal buffer has already assigned cell positions by draw time.
Regression coverage should assert that:
- a tab still contributes
TAB_WIDTHto wrapping; - no output span from
wrap_styled_spans()contains\t; - syntax styles survive expansion;
- a tab near the wrap boundary remains an atomic four-column chunk;
- rendering a Go diff into a Ratatui buffer produces only printable cells.
I have a narrow local patch implementing span-level expansion, but have not compiled or run it on the reproduction machine.
Related issues
- #22109 describes the broader class where Codex's logical width diverges from physical terminal wrapping and mentions TSV-like tabs. This report is narrower: it identifies the concrete
Ctrl+Tpatch-cell path, the literal-tab preservation point, and the resulting stale cells inside colored diff rows. - #30975 concerns Codex Desktop/CJK repaint corruption rather than the CLI full-transcript diff renderer.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗