fix(ui): consistent frame glyphs on Windows — agree with clack, keep raw path ASCII (#1307)

codegraph's glyphs were ASCII on every Windows console while
@clack/prompts drew its Unicode frame around them, so one index block
mixed `|` and `│` rails (#398). supportsUnicode() now mirrors the
is-unicode-supported detection clack bundles (Windows Terminal, VS
Code, ConEmu/Cmder, Alacritty, xterm-256color, JetBrains, CI), so both
systems always pick the same glyph family.

The shimmer worker's raw fs.writeSync(1) bytes still decode through the
console codepage (OEM codepages mojibake UTF-8 even under Windows
Terminal — the #168 regression to avoid), so:

- the raw path gets its own supportsUnicodeRawWrites() that stays ASCII
  on win32 unless CODEGRAPH_UNICODE=1, and
- the persistent "phase done" lines move from the worker to the parent,
  written via process.stdout (wide-char console API, codepage-immune) at
  phase transitions — the main thread is alive there, it's delivering
  the progress callback. Only transient, self-erasing animation frames
  remain on the raw path, so ASCII never lands in scrollback.

Fixes #398

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-16 14:52:11 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent d6efd437b3
commit 30421953ac
6 changed files with 209 additions and 32 deletions
+88 -4
View File
@@ -10,6 +10,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
supportsUnicode,
supportsUnicodeRawWrites,
getGlyphs,
UNICODE_GLYPHS,
ASCII_GLYPHS,
@@ -54,13 +55,84 @@ describe('supportsUnicode', () => {
_resetGlyphsCache();
});
it('returns false on Windows by default (mojibake-prone consoles)', () => {
withEnv({ CODEGRAPH_ASCII: undefined, CODEGRAPH_UNICODE: undefined, TERM: undefined }, () => {
/** Clears every signal the Windows detection reads, so cases are explicit. */
const NO_TERMINAL_SIGNALS: Record<string, string | undefined> = {
CODEGRAPH_ASCII: undefined,
CODEGRAPH_UNICODE: undefined,
TERM: undefined,
CI: undefined,
WT_SESSION: undefined,
TERMINUS_SUBLIME: undefined,
ConEmuTask: undefined,
TERM_PROGRAM: undefined,
TERMINAL_EMULATOR: undefined,
};
it('returns false on Windows in an unrecognized console (mojibake-prone legacy conhost)', () => {
withEnv(NO_TERMINAL_SIGNALS, () => {
setPlatform('win32');
expect(supportsUnicode()).toBe(false);
});
});
// The Windows allowlist must match @clack/prompts' bundled detection —
// wherever clack draws its Unicode frame, our rails must be Unicode too,
// or `codegraph index` mixes `|` and `│` in one output block (#398).
it.each([
['Windows Terminal', { WT_SESSION: 'a-guid' }],
['VS Code terminal', { TERM_PROGRAM: 'vscode' }],
['ConEmu/Cmder', { ConEmuTask: '{cmd::Cmder}' }],
['Alacritty', { TERM: 'alacritty' }],
['xterm-256color', { TERM: 'xterm-256color' }],
['JetBrains terminal', { TERMINAL_EMULATOR: 'JetBrains-JediTerm' }],
['CI', { CI: 'true' }],
])('returns true on Windows in %s (agrees with clack, #398)', (_name, envPatch) => {
withEnv({ ...NO_TERMINAL_SIGNALS, ...envPatch }, () => {
setPlatform('win32');
expect(supportsUnicode()).toBe(true);
});
});
it('CODEGRAPH_ASCII=1 still wins inside Windows Terminal (escape hatch)', () => {
withEnv({ ...NO_TERMINAL_SIGNALS, CODEGRAPH_ASCII: '1', WT_SESSION: 'a-guid' }, () => {
setPlatform('win32');
expect(supportsUnicode()).toBe(false);
});
});
// The raw fs.writeSync(1) path (shimmer animation frames) decodes through
// the console CODEPAGE on Windows, so it must stay ASCII there even in
// terminals where the codepage-immune main-thread path goes Unicode (#168).
describe('supportsUnicodeRawWrites', () => {
it('stays ASCII on Windows even inside Windows Terminal / vscode / CI', () => {
for (const envPatch of [{ WT_SESSION: 'a-guid' }, { TERM_PROGRAM: 'vscode' }, { CI: 'true' }]) {
withEnv({ ...NO_TERMINAL_SIGNALS, ...envPatch }, () => {
setPlatform('win32');
expect(supportsUnicodeRawWrites()).toBe(false);
expect(supportsUnicode()).toBe(true); // main-thread path DOES go Unicode there
});
}
});
it('CODEGRAPH_UNICODE=1 opts the raw path in on Windows', () => {
withEnv({ ...NO_TERMINAL_SIGNALS, CODEGRAPH_UNICODE: '1' }, () => {
setPlatform('win32');
expect(supportsUnicodeRawWrites()).toBe(true);
});
});
it('matches supportsUnicode() off Windows (Unicode on macOS, ASCII on TERM=linux)', () => {
withEnv({ ...NO_TERMINAL_SIGNALS }, () => {
setPlatform('darwin');
expect(supportsUnicodeRawWrites()).toBe(true);
});
withEnv({ ...NO_TERMINAL_SIGNALS, TERM: 'linux' }, () => {
setPlatform('linux');
expect(supportsUnicodeRawWrites()).toBe(false);
});
});
});
it('returns true on macOS by default', () => {
withEnv({ CODEGRAPH_ASCII: undefined, CODEGRAPH_UNICODE: undefined, TERM: undefined }, () => {
setPlatform('darwin');
@@ -117,8 +189,20 @@ describe('getGlyphs', () => {
_resetGlyphsCache();
});
it('returns ASCII glyphs on Windows', () => {
withEnv({ CODEGRAPH_ASCII: undefined, CODEGRAPH_UNICODE: undefined }, () => {
it('returns ASCII glyphs on Windows in an unrecognized console', () => {
withEnv(
{
CODEGRAPH_ASCII: undefined,
CODEGRAPH_UNICODE: undefined,
TERM: undefined,
CI: undefined,
WT_SESSION: undefined,
TERMINUS_SUBLIME: undefined,
ConEmuTask: undefined,
TERM_PROGRAM: undefined,
TERMINAL_EMULATOR: undefined,
},
() => {
setPlatform('win32');
const g = getGlyphs();
expect(g).toBe(ASCII_GLYPHS);