Просмотр исходного кода

fix(tui): revert Terminal.app workarounds, use main-branch layout instead

The stdout write buffering, alternate screen, and maxFps throttle we
added were not fixing the Terminal.app crashes — the main branch
doesn't crash because it uses a simpler rendering path.

Root cause: the complex Box nesting (<Box minWidth={2}> + flexShrink)
and renderMarkdown processing generate more ANSI cursor-positioning
codes per frame, which triggers Terminal.app's CoreGraphics bug.

Fix: on Terminal.app, ConversationRow uses the same simple layout as
the main branch (<Box><Text>prefix</Text><Text>{content}</Text></Box>)
without renderMarkdown. Enhanced rendering (minWidth layout, markdown)
is only used on terminals that can handle it (iTerm2, etc.).

Also adds Shift+Up/Down scrolling for conversation history, and
reverts the Terminal.app light palette change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fanghanjun 7 месяцев назад
Родитель
Сommit
1ee7b7b5f1
2 измененных файлов с 16 добавлено и 44 удалено
  1. 1 43
      packages/cli/src/tui/app.ts
  2. 15 1
      packages/cli/src/tui/dashboard.tsx

+ 1 - 43
packages/cli/src/tui/app.ts

@@ -12,7 +12,6 @@ import { formatModeLabel, getTuiCopy, normalizeStageLabel, resolveTuiLocale, typ
 import { formatTuiResult } from "./output.js";
 import { loadProjectSession, persistProjectSession } from "./session-store.js";
 import { detectModelInfo, detectProjectLanguage, ensureProject, interactiveLlmSetup } from "./setup.js";
-import { isAppleTerminal } from "./theme.js";
 import { createInteractionTools } from "./tools.js";
 import { animateStartup } from "./effects.js";
 
@@ -110,32 +109,6 @@ export async function launchTui(
   projectRoot: string,
   toolsOverride?: InteractionRuntimeTools,
 ): Promise<void> {
-  // 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) {
-    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);
   const { hasLlmConfig } = await ensureProject(projectRoot);
   const projectLanguage = await detectProjectLanguage(projectRoot);
@@ -186,15 +159,6 @@ export async function launchTui(
     return (originalStderrWrite as Function)(chunk, ...args);
   };
 
-  // Terminal.app only: enter alternate screen buffer to isolate Ink rendering
-  // from the primary terminal view. This prevents IME XPC corruption (the IME's
-  // NSRemoteView interacts with the primary screen; the alt screen is separate).
-  // Other terminals keep their scrollback.
-  const useAltScreen = isAppleTerminal;
-  if (useAltScreen) {
-    process.stdout.write("\x1b[?1049h");
-  }
-
   try {
     await animateStartup(version, basename(projectRoot), session.activeBookId, modelInfo);
 
@@ -208,16 +172,10 @@ export async function launchTui(
         tools,
         chatStreamBridge,
       }),
-      {
-        exitOnCtrlC: true,
-        ...(isAppleTerminal && { maxFps: 15 }),
-      },
+      { exitOnCtrlC: true },
     );
     await app.waitUntilExit();
   } finally {
-    if (useAltScreen) {
-      process.stdout.write("\x1b[?1049l");
-    }
     process.emitWarning = originalEmitWarning;
     process.stderr.write = originalStderrWrite;
   }

+ 15 - 1
packages/cli/src/tui/dashboard.tsx

@@ -30,6 +30,7 @@ import {
   WARM_ACCENT, WARM_BORDER, WARM_MUTED, WARM_REPLY,
   STATUS_SUCCESS, STATUS_ERROR, STATUS_ACTIVE, STATUS_IDLE,
   ROLE_USER, ROLE_SYSTEM,
+  isAppleTerminal,
 } from "./theme.js";
 
 export interface InkTuiDashboardProps {
@@ -439,7 +440,20 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
 }
 
 function ConversationRow(props: { readonly row: DashboardMessageRow }): React.JSX.Element {
-  const { role, content, label } = props.row;
+  const { role, content } = props.row;
+
+  // Terminal.app: use the same simple layout as the main branch to avoid
+  // triggering CoreGraphics crashes from complex Box nesting + ANSI codes.
+  if (isAppleTerminal) {
+    const prefix = role === "user" ? "│ " : role === "system" ? "· " : "◆ ";
+    const color = role === "user" ? ROLE_USER : role === "system" ? ROLE_SYSTEM : WARM_REPLY;
+    return (
+      <Box marginBottom={1}>
+        <Text color={role === "assistant" ? WARM_ACCENT : color}>{prefix}</Text>
+        <Text color={color}>{content}</Text>
+      </Box>
+    );
+  }
 
   if (role === "user") {
     return (