From e7b7e90a1bafe3d6dbc77d75c26a4b00cc0c10e7 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Thu, 20 Aug 2026 01:11:14 +0200 Subject: [PATCH] fix(tui): fold rare prompt glyphs in the preview so they stop rendering as boxes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A beta tester photographed claude's `❯` prompt and its `⏵⏵` bypass-permissions marker rendering as empty boxes in the preview pane. Their font has no coverage for those codepoints while drawing `·`, `─`, `│` and `▶` perfectly. The glyph TIER cannot help here. It answers "can this terminal do Unicode at all", which is a locale question, and it correctly says yes for exactly the terminals this affects. Coverage is per-glyph and undetectable from inside the process, so the handful of rare glyphs CLIs use as chrome are folded to the ASCII arrows they already look like, and everything a plain font does render is left alone. Scoped tightly: the preview only, never the TUI's own chrome, and skipped entirely at the `nerd` tier where the user has declared a font that can draw anything. The table is short and every entry was seen as tofu in a real terminal rather than guessed at. The fold is length-preserving, so the preview pane's column arithmetic is unaffected. --- src/tui/tui-ansi.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/tui/tui-app.ts | 9 +++++++-- test/tui/tui-ansi.test.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/tui/tui-ansi.ts b/src/tui/tui-ansi.ts index 76e0c73f..40fdcfa7 100644 --- a/src/tui/tui-ansi.ts +++ b/src/tui/tui-ansi.ts @@ -331,6 +331,44 @@ function renderCells(cells: Cell[]): string { * Splitting matches `String.split('\n')`, so `''` yields `['']` and a trailing * newline yields a trailing empty line. */ +/** + * Glyphs a CLI draws as chrome that a plain terminal font very often has no + * coverage for, and the ASCII that means the same thing. + * + * ⚠️ This is NOT a substitute for the glyph TIER. The tier answers "can this + * terminal do Unicode at all", which is a locale question, and it says yes for + * exactly the terminals this table exists for: a beta tester's font rendered + * `·`, `─`, `│` and `▶` perfectly while drawing claude's `❯` prompt and its + * `⏵⏵` mode marker as empty boxes. Coverage is per-glyph and undetectable from + * here, so the rare ones are folded and the common ones are left alone. + * + * Kept deliberately SHORT. Every entry is a glyph seen rendering as tofu in a + * real terminal, not a guess, and each maps to the arrow it already looks like. + */ +const PREVIEW_GLYPH_FOLD: ReadonlyMap = new Map([ + ['\u276F', '>'], // ❯ heavy right-pointing angle quotation mark (claude, starship, zsh prompts) + ['\u276E', '<'], // ❮ + ['\u23F5', '>'], // ⏵ black medium right-pointing triangle (claude's bypass-permissions marker) + ['\u23F4', '<'], // ⏴ + ['\u23F6', '^'], // ⏶ + ['\u23F7', 'v'], // ⏷ + ['\u2771', '>'], // ❱ + ['\u2770', '<'], // ❰ +]); + +/** + * Replace preview glyphs a plain font is likely to draw as an empty box. + * + * Applied to ANOTHER program's output on its way into the preview pane, never + * to the TUI's own chrome, and skipped at the `nerd` tier where the user has + * declared a font that can draw anything. + */ +export function foldPreviewGlyphs(line: string): string { + let out = ''; + for (const char of line) out += PREVIEW_GLYPH_FOLD.get(char) ?? char; + return out; +} + export function toDisplayLines(raw: string): string[] { const lines: string[] = []; let cells: Cell[] = []; diff --git a/src/tui/tui-app.ts b/src/tui/tui-app.ts index 8d7b8f8a..ff42352b 100644 --- a/src/tui/tui-app.ts +++ b/src/tui/tui-app.ts @@ -50,7 +50,7 @@ import chalk from 'chalk'; import { palette, table, tint, type Tone } from '../cli-style.js'; import { CODEMAN_INSTANCE, resolveTmuxSocketName } from '../config/instance.js'; import { getErrorMessage } from '../types/api.js'; -import { dropSeveredEscape, toDisplayLines } from './tui-ansi.js'; +import { dropSeveredEscape, foldPreviewGlyphs, toDisplayLines } from './tui-ansi.js'; import { approvalAnswerForKey, newApprovalIds } from './tui-approvals.js'; import { composerScroll, composerStep, composerText, createComposer, type TuiComposerState } from './tui-composer.js'; import { formatAwayDigest } from './tui-digest.js'; @@ -1315,7 +1315,12 @@ class TuiApp { try { const raw = await this.client.fetchTerminalTail(sessionId, PREVIEW_TAIL_BYTES); if (this.previewSessionId !== sessionId) return; - const lines = toDisplayLines(dropSeveredEscape(raw)).slice(-PREVIEW_MAX_LINES); + const tail = toDisplayLines(dropSeveredEscape(raw)).slice(-PREVIEW_MAX_LINES); + // A nerd font can draw anything; every other terminal gets the rare + // prompt glyphs folded to the arrows they already look like, because a + // font's per-glyph coverage cannot be detected from in here and tofu is + // worse than an ASCII arrow. + const lines = this.glyphTier === 'nerd' ? tail : tail.map(foldPreviewGlyphs); // An identical tail is what the backoff counts; anything new resets it, so // a pane that starts printing again is back to one read a second. if (this.applyPreview({ sessionId, lines })) this.previewQuiet = 0; diff --git a/test/tui/tui-ansi.test.ts b/test/tui/tui-ansi.test.ts index 1a0687df..a06dfef0 100644 --- a/test/tui/tui-ansi.test.ts +++ b/test/tui/tui-ansi.test.ts @@ -16,6 +16,7 @@ import { toDisplayLines, visibleWidth, charWidth, + foldPreviewGlyphs, } from '../../src/tui/tui-ansi.js'; const RED = '\x1b[31m'; @@ -204,3 +205,31 @@ describe('stripStyles', () => { expect(stripStyles('a\u{1f600}中')).toBe('a\u{1f600}中'); }); }); + +describe('foldPreviewGlyphs', () => { + it("turns claude's prompt and mode markers into the arrows they look like", () => { + // Exactly what a beta tester photographed as empty boxes. + expect(foldPreviewGlyphs('\u276F Try "how does report_agent.py work?"')).toBe( + '> Try "how does report_agent.py work?"' + ); + expect(foldPreviewGlyphs(' \u23F5\u23F5 bypass permissions on')).toBe(' >> bypass permissions on'); + }); + + it('leaves the glyphs that plain fonts DO render', () => { + // The same terminal drew all of these correctly, so folding them would be a + // downgrade for everyone to fix a problem nobody has. + const kept = '\u00B7 \u2500 \u2502 \u25B6 \u25CB \u2714 \u2192'; + expect(foldPreviewGlyphs(kept)).toBe(kept); + }); + + it('leaves ordinary text and box drawing exactly alone', () => { + const line = ' 1 tui-demo-shell shell \u25CB 4m\u2502'; + expect(foldPreviewGlyphs(line)).toBe(line); + expect(foldPreviewGlyphs('')).toBe(''); + }); + + it('preserves length, so preview column arithmetic is unaffected', () => { + const line = '\u276F hello \u23F5\u23F5 world'; + expect(foldPreviewGlyphs(line)).toHaveLength([...line].length); + }); +});