An audit of every non-ASCII character this product draws, prompted by users reporting that they see boxes or unrecognisable characters in some terminals.
What is already right, so nobody rebuilds it
The glyph system is deliberate and well built, and none of the work below should replace it.
internal/tui2/tokens holds one vocabulary with three spellings of every mark: a Nerd Font icon, a geometric Unicode shape, and one ASCII character. GlyphSet.Glyph (internal/tui2/tokens/glyphset.go:351) is the single door.
tokens.DetectGlyphSet (internal/tui2/tokens/detect.go:75) vetoes the rich tier on no TERM, TERM=dumb, the Linux console, Apple Terminal, a CJK locale and the legacy Windows console.
- The design position is stated outright at
internal/tui2/tokens/detect.go:6-13: no terminal reliably reports its font, so detection may only veto, never confirm. The rich default is a decision, not an oversight.
TestTierWidthParity (internal/tui2/tokens/parity_test.go:84) renders the same surfaces in both tiers and fails if a tier moves a column.
internal/tui3/cellwidth.go performs a real runtime capability handshake, reading DEC mode 2027 and switching the ruler only when the terminal answers.
internal/iconlaw/iconlaw_test.go:122 forbids any surface from spelling an icon itself.
1. The command-line path has no detection and no fallback
cmd/codeaf holds no palette, does no detection, and is absent from the icon law's surface list (internal/iconlaw/iconlaw_test.go:47-51). It writes non-ASCII characters straight to stdout.
cmd/codeaf/do.go:1615 writes U+23F3 HOURGLASS WITH FLOWING SAND. That character is on this product's own banned list at internal/tui2/tokens/glyph.go:568, recorded there as two cells. It is also default-emoji presentation, so most fonts draw it as a colour emoji. It ships to any terminal, including TERM=dumb in a C locale, and nothing catches it.
Also drawn raw from that package: U+2500, U+2014, U+00B7, U+2192, U+25B6, U+2713, U+2717, U+21BB, U+22EF.
Fix: correct the hourglass, then extend the icon law to cover cmd/codeaf so this class cannot return.
2. The correct width ruler is used in 2 places out of 561
internal/tui3/cellwidth.go exists because a rail sat two columns left on rows carrying emoji. Its own comment at cellwidth.go:54-57 says every layout decision that has to line up with a drawn column belongs on cellRuler.cells rather than on ansi.StringWidth.
cellRuler.cells has two callers: internal/tui3/task.go:5572 and internal/tui3/taskstable.go:78. ansi.StringWidth has 566 non-test call sites, 559 of them in internal/tui3, including every padding and truncation primitive: fitWidth (internal/tui3/render.go:4275), fitPad (internal/tui3/hop.go:1648), rightPad (hop.go:1661).
The file was written to fix this class and is wired into almost none of it.
3. Seventy-three drawn characters are East Asian Ambiguous
Measured as one cell, drawn as two on a terminal configured ambiguous-wide. The set includes every box-drawing character, every block element, every arrow, the middle dot separator used roughly two thousand times, and five of the six state marks: U+25CB, U+25D0, U+25A0, U+25B2, U+25CF.
Every column after such a character on that row shifts. Multiplied by depth in the work tree, where the connectors ├─, └─ and │ are all ambiguous (internal/tui3/task.go:2241-2247).
ansi.StringWidth is narrow-fixed for ambiguous runes and there is no East Asian width configuration anywhere in the tree.
4. The users most exposed are the ones the vetoes miss
Two independent mitigations exist and neither covers this case.
tokens.DetectGlyphSet drops the icon tier to plain on a CJK locale (internal/tui2/tokens/detect.go:90). It does not touch box drawing, separators or rails.
detectASCII (internal/tui3/styles.go:1199) drops the whole surface to ASCII, but only when the locale is not UTF-8.
So a user on ja_JP.UTF-8, the exact population most likely to run a terminal with ambiguous-wide on, keeps all 73 ambiguous characters in the layout. Neither gate fires.
5. The rich tier is never rendered in a terminal test
Every end-to-end suite forces the plain tier (internal/e2e/harness_test.go:154-159, questions_e2e_test.go:1306, secondwindow_e2e_test.go:206, tmux_test.go:132). The Nerd Font path has unit coverage and no terminal-level assertion. Nothing renders under an ambiguous-wide terminal at all; TestAmbiguousWidthFlags records the property without exercising it.
6. Smaller findings
- U+276F is on the banned list at
internal/tui2/tokens/glyph.go:570 as font-fragile, and is drawn at internal/tui3/task.go:2381.
internal/standing/standing.go:576 returns U+25B2, U+25CF, U+2219, U+25E6 as bare literals with no tier. internal/tui3 wraps it defensively; internal/session/tools_standing.go:1366 consumes it raw.
- Characters with both text and emoji presentation are drawn with no variation selector, so each is a font-dependent one-or-two-cell outcome: U+25B6, U+2197, U+23FA, U+2699, U+2692, U+2696.
- Likely absent from common monospace fonts, so a visible miss rather than a width bug: U+27F2, U+29C9, U+233E, U+2315 (used for both search and filter, so a miss is constant), U+21E1, U+25F7, U+22D4.
Suggested order
- The hourglass, and the icon law extended over
cmd/codeaf.
- A decision on ambiguous width, with a test that renders under it. This is the one that silently bends columns for a whole class of users.
- Move the layout primitives onto
cellRuler.cells, starting with fitPad and rightPad.
- One terminal-level test of the rich tier.
One thing that is not a bug
A tab draws the plain question mark for needs-you even when the rest of the screen is on the rich tier, so one screen can show two shapes for one meaning. That is deliberate: tabSignalGlyph (internal/tui3/tabsignal.go:199) takes the ASCII-safe character because it needs no fallback. Worth a decision rather than a fix.
An audit of every non-ASCII character this product draws, prompted by users reporting that they see boxes or unrecognisable characters in some terminals.
What is already right, so nobody rebuilds it
The glyph system is deliberate and well built, and none of the work below should replace it.
internal/tui2/tokensholds one vocabulary with three spellings of every mark: a Nerd Font icon, a geometric Unicode shape, and one ASCII character.GlyphSet.Glyph(internal/tui2/tokens/glyphset.go:351) is the single door.tokens.DetectGlyphSet(internal/tui2/tokens/detect.go:75) vetoes the rich tier on no TERM,TERM=dumb, the Linux console, Apple Terminal, a CJK locale and the legacy Windows console.internal/tui2/tokens/detect.go:6-13: no terminal reliably reports its font, so detection may only veto, never confirm. The rich default is a decision, not an oversight.TestTierWidthParity(internal/tui2/tokens/parity_test.go:84) renders the same surfaces in both tiers and fails if a tier moves a column.internal/tui3/cellwidth.goperforms a real runtime capability handshake, reading DEC mode 2027 and switching the ruler only when the terminal answers.internal/iconlaw/iconlaw_test.go:122forbids any surface from spelling an icon itself.1. The command-line path has no detection and no fallback
cmd/codeafholds no palette, does no detection, and is absent from the icon law's surface list (internal/iconlaw/iconlaw_test.go:47-51). It writes non-ASCII characters straight to stdout.cmd/codeaf/do.go:1615writes U+23F3 HOURGLASS WITH FLOWING SAND. That character is on this product's own banned list atinternal/tui2/tokens/glyph.go:568, recorded there as two cells. It is also default-emoji presentation, so most fonts draw it as a colour emoji. It ships to any terminal, includingTERM=dumbin a C locale, and nothing catches it.Also drawn raw from that package: U+2500, U+2014, U+00B7, U+2192, U+25B6, U+2713, U+2717, U+21BB, U+22EF.
Fix: correct the hourglass, then extend the icon law to cover
cmd/codeafso this class cannot return.2. The correct width ruler is used in 2 places out of 561
internal/tui3/cellwidth.goexists because a rail sat two columns left on rows carrying emoji. Its own comment atcellwidth.go:54-57says every layout decision that has to line up with a drawn column belongs oncellRuler.cellsrather than onansi.StringWidth.cellRuler.cellshas two callers:internal/tui3/task.go:5572andinternal/tui3/taskstable.go:78.ansi.StringWidthhas 566 non-test call sites, 559 of them ininternal/tui3, including every padding and truncation primitive:fitWidth(internal/tui3/render.go:4275),fitPad(internal/tui3/hop.go:1648),rightPad(hop.go:1661).The file was written to fix this class and is wired into almost none of it.
3. Seventy-three drawn characters are East Asian Ambiguous
Measured as one cell, drawn as two on a terminal configured ambiguous-wide. The set includes every box-drawing character, every block element, every arrow, the middle dot separator used roughly two thousand times, and five of the six state marks: U+25CB, U+25D0, U+25A0, U+25B2, U+25CF.
Every column after such a character on that row shifts. Multiplied by depth in the work tree, where the connectors
├─,└─and│are all ambiguous (internal/tui3/task.go:2241-2247).ansi.StringWidthis narrow-fixed for ambiguous runes and there is no East Asian width configuration anywhere in the tree.4. The users most exposed are the ones the vetoes miss
Two independent mitigations exist and neither covers this case.
tokens.DetectGlyphSetdrops the icon tier to plain on a CJK locale (internal/tui2/tokens/detect.go:90). It does not touch box drawing, separators or rails.detectASCII(internal/tui3/styles.go:1199) drops the whole surface to ASCII, but only when the locale is not UTF-8.So a user on
ja_JP.UTF-8, the exact population most likely to run a terminal with ambiguous-wide on, keeps all 73 ambiguous characters in the layout. Neither gate fires.5. The rich tier is never rendered in a terminal test
Every end-to-end suite forces the plain tier (
internal/e2e/harness_test.go:154-159,questions_e2e_test.go:1306,secondwindow_e2e_test.go:206,tmux_test.go:132). The Nerd Font path has unit coverage and no terminal-level assertion. Nothing renders under an ambiguous-wide terminal at all;TestAmbiguousWidthFlagsrecords the property without exercising it.6. Smaller findings
internal/tui2/tokens/glyph.go:570as font-fragile, and is drawn atinternal/tui3/task.go:2381.internal/standing/standing.go:576returns U+25B2, U+25CF, U+2219, U+25E6 as bare literals with no tier.internal/tui3wraps it defensively;internal/session/tools_standing.go:1366consumes it raw.Suggested order
cmd/codeaf.cellRuler.cells, starting withfitPadandrightPad.One thing that is not a bug
A tab draws the plain question mark for needs-you even when the rest of the screen is on the rich tier, so one screen can show two shapes for one meaning. That is deliberate:
tabSignalGlyph(internal/tui3/tabsignal.go:199) takes the ASCII-safe character because it needs no fallback. Worth a decision rather than a fix.