fix(tui): buffer stdout writes for Terminal.app to prevent CJK crash

Terminal.app crashes (SIGSEGV in CGFontStrikeGetValue) when it sees
partial CJK UTF-8 bytes across multiple write() calls — the font
glyph cache gets corrupted with text bytes in pointer fields.

Buffer all stdout.write() calls within a single event-loop tick and
flush them as one atomic write via process.nextTick. This mirrors
Claude Code's single-write buffering approach (writeDiffToTerminal)
and ensures Terminal.app always receives complete frames.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fanghanjun
2026-04-13 01:35:23 -07:00
parent 43a6523381
commit 4e5e44d22d
+23 -4
View File
@@ -110,11 +110,30 @@ export async function launchTui(
projectRoot: string,
toolsOverride?: InteractionRuntimeTools,
): Promise<void> {
// Terminal.app has a CoreGraphics bug where UTF-8 bytes (e.g. CJK, em dash)
// end up in color space pointers during ANSI color rendering, causing SIGSEGV.
// Disable all color output to avoid triggering the crash.
// Terminal.app has a CoreGraphics bug: when it sees partial CJK UTF-8 bytes
// across multiple write() calls, the font glyph cache gets corrupted (text bytes
// end up in pointer fields → SIGSEGV in CGFontStrikeGetValue).
// Fix: buffer all writes within a single event-loop tick and flush once,
// so Terminal.app always sees complete frames. Same approach as Claude Code's
// single-write buffering in writeDiffToTerminal().
if (isAppleTerminal) {
process.env.FORCE_COLOR = "0";
let buffer = "";
const originalWrite = process.stdout.write.bind(process.stdout);
let scheduled = false;
process.stdout.write = ((chunk: string | Uint8Array): boolean => {
buffer += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString();
if (!scheduled) {
scheduled = true;
process.nextTick(() => {
scheduled = false;
const content = buffer;
buffer = "";
originalWrite(content);
});
}
return true;
}) as typeof process.stdout.write;
}
projectRoot = await resolveProjectRoot(projectRoot);