From 4e5e44d22dca7accd2d2d308dd27726652382084 Mon Sep 17 00:00:00 2001 From: fanghanjun <17737663888@163.com> Date: Mon, 13 Apr 2026 01:35:23 -0700 Subject: [PATCH] fix(tui): buffer stdout writes for Terminal.app to prevent CJK crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- packages/cli/src/tui/app.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/tui/app.ts b/packages/cli/src/tui/app.ts index 4fb56978..eb28a5f6 100644 --- a/packages/cli/src/tui/app.ts +++ b/packages/cli/src/tui/app.ts @@ -110,11 +110,30 @@ export async function launchTui( projectRoot: string, toolsOverride?: InteractionRuntimeTools, ): Promise { - // 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);