mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 00:19:42 +02:00
fix(tui): fold rare prompt glyphs in the preview so they stop rendering as boxes
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.
This commit is contained in:
@@ -331,6 +331,44 @@ function renderCells(cells: Cell[]): string {
|
|||||||
* Splitting matches `String.split('\n')`, so `''` yields `['']` and a trailing
|
* Splitting matches `String.split('\n')`, so `''` yields `['']` and a trailing
|
||||||
* newline yields a trailing empty line.
|
* 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<string, string> = 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[] {
|
export function toDisplayLines(raw: string): string[] {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
let cells: Cell[] = [];
|
let cells: Cell[] = [];
|
||||||
|
|||||||
+7
-2
@@ -50,7 +50,7 @@ import chalk from 'chalk';
|
|||||||
import { palette, table, tint, type Tone } from '../cli-style.js';
|
import { palette, table, tint, type Tone } from '../cli-style.js';
|
||||||
import { CODEMAN_INSTANCE, resolveTmuxSocketName } from '../config/instance.js';
|
import { CODEMAN_INSTANCE, resolveTmuxSocketName } from '../config/instance.js';
|
||||||
import { getErrorMessage } from '../types/api.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 { approvalAnswerForKey, newApprovalIds } from './tui-approvals.js';
|
||||||
import { composerScroll, composerStep, composerText, createComposer, type TuiComposerState } from './tui-composer.js';
|
import { composerScroll, composerStep, composerText, createComposer, type TuiComposerState } from './tui-composer.js';
|
||||||
import { formatAwayDigest } from './tui-digest.js';
|
import { formatAwayDigest } from './tui-digest.js';
|
||||||
@@ -1315,7 +1315,12 @@ class TuiApp {
|
|||||||
try {
|
try {
|
||||||
const raw = await this.client.fetchTerminalTail(sessionId, PREVIEW_TAIL_BYTES);
|
const raw = await this.client.fetchTerminalTail(sessionId, PREVIEW_TAIL_BYTES);
|
||||||
if (this.previewSessionId !== sessionId) return;
|
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
|
// 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.
|
// a pane that starts printing again is back to one read a second.
|
||||||
if (this.applyPreview({ sessionId, lines })) this.previewQuiet = 0;
|
if (this.applyPreview({ sessionId, lines })) this.previewQuiet = 0;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
toDisplayLines,
|
toDisplayLines,
|
||||||
visibleWidth,
|
visibleWidth,
|
||||||
charWidth,
|
charWidth,
|
||||||
|
foldPreviewGlyphs,
|
||||||
} from '../../src/tui/tui-ansi.js';
|
} from '../../src/tui/tui-ansi.js';
|
||||||
|
|
||||||
const RED = '\x1b[31m';
|
const RED = '\x1b[31m';
|
||||||
@@ -204,3 +205,31 @@ describe('stripStyles', () => {
|
|||||||
expect(stripStyles('a\u{1f600}中')).toBe('a\u{1f600}中');
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user