Browse Source

fix(theme): update startupAccent color description to reflect Claude Code CN pastel green

Jarvis 2 months ago
parent
commit
b2cf3ac6b3

+ 1 - 0
.gitignore

@@ -2,3 +2,4 @@ node_modules/
 dist/
 cli
 cli-dev
+package-lock.json

+ 41 - 34
README.md

@@ -26,6 +26,7 @@ curl -fsSL https://raw.githubusercontent.com/go-enols/Claude-CN/main/install.sh
 此脚本将自动检测系统、安装 Bun(如需要)、克隆代码库、构建并启用所有功能,然后将其添加到 PATH 中。
 
 安装完成后,运行以下命令启动:
+
 ```bash
 export ANTHROPIC_API_KEY="sk-ant-..."
 claude-cn
@@ -71,12 +72,12 @@ bun run compile
 
 ### 构建变体
 
-| 命令 | 输出 | 功能 | 说明 |
-|---|---|---|---|
-| `bun run build` | `./cli` | 仅 `VOICE_MODE` | 生产级二进制文件 |
-| `bun run build:dev` | `./cli-dev` | 仅 `VOICE_MODE` | 开发版本标识 |
-| `bun run build:dev:full` | `./cli-dev` | 所有45+实验性标志 | 完整解锁构建 |
-| `bun run compile` | `./dist/cli` | 仅 `VOICE_MODE` | 备用输出目录 |
+| 命令                     | 输出         | 功能              | 说明             |
+| ------------------------ | ------------ | ----------------- | ---------------- |
+| `bun run build`          | `./cli`      | 仅 `VOICE_MODE`   | 生产级二进制文件 |
+| `bun run build:dev`      | `./cli-dev`  | 仅 `VOICE_MODE`   | 开发版本标识     |
+| `bun run build:dev:full` | `./cli-dev`  | 所有45+实验性标志 | 完整解锁构建     |
+| `bun run compile`        | `./dist/cli` | 仅 `VOICE_MODE`   | 备用输出目录     |
 
 ### 单独启用功能标志
 
@@ -124,6 +125,12 @@ export ANTHROPIC_API_KEY="sk-ant-..."
 ./cli --model claude-sonnet-4-6-20250514
 ```
 
+### 快速使用
+
+- 编译好后可安装CC-Switch已切换自定义模型
+
+- 前往 **_[cc-switch GitHub Releases](https://github.com/farion1231/cc-switch/blob/main/docs/release-notes/v3.12.3-zh.md)_** 页面下载最新版本的安装包。
+
 ---
 
 ## 核心改动
@@ -152,24 +159,24 @@ Anthropic 在每次对话中注入系统级指令,超出模型本身的约束
 
 Claude Code 附带数十个通过 `bun:bundle` 编译时开关门控的功能标志。本构建解锁了所有能正常编译的 45+ 个标志,包括:
 
-| 功能标志 | 说明 |
-|---|---|
-| `ULTRAPLAN` | Claude Code 网页上的远程多智能体规划(Opus级别) |
-| `ULTRATHINK` | 深度思考模式 - 输入"ultrathink"提升推理努力 |
-| `VOICE_MODE` | 按键通话语音输入和听写 |
-| `AGENT_TRIGGERS` | 后台自动化本地 cron/触发器工具 |
-| `BRIDGE_MODE` | IDE 远程控制桥(VS Code、JetBrains) |
-| `TOKEN_BUDGET` | Token 预算跟踪和使用警告 |
-| `BUILTIN_EXPLORE_PLAN_AGENTS` | 内置探索/规划智能体预设 |
-| `VERIFICATION_AGENT` | 任务验证智能体 |
-| `BASH_CLASSIFIER` | 分类器辅助的 bash 权限决策 |
-| `EXTRACT_MEMORIES` | 查询后自动记忆提取 |
-| `HISTORY_PICKER` | 交互式提示历史选择器 |
-| `MESSAGE_ACTIONS` | UI 中的消息操作入口点 |
-| `QUICK_SEARCH` | 提示快速搜索 |
-| `SHOT_STATS` | Shot分布统计 |
-| `COMPACTION_REMINDERS` | 上下文压缩周围的智能提醒 |
-| `CACHED_MICROCOMPACT` | 通过查询流程的缓存微压缩状态 |
+| 功能标志                      | 说明                                             |
+| ----------------------------- | ------------------------------------------------ |
+| `ULTRAPLAN`                   | Claude Code 网页上的远程多智能体规划(Opus级别) |
+| `ULTRATHINK`                  | 深度思考模式 - 输入"ultrathink"提升推理努力      |
+| `VOICE_MODE`                  | 按键通话语音输入和听写                           |
+| `AGENT_TRIGGERS`              | 后台自动化本地 cron/触发器工具                   |
+| `BRIDGE_MODE`                 | IDE 远程控制桥(VS Code、JetBrains)             |
+| `TOKEN_BUDGET`                | Token 预算跟踪和使用警告                         |
+| `BUILTIN_EXPLORE_PLAN_AGENTS` | 内置探索/规划智能体预设                          |
+| `VERIFICATION_AGENT`          | 任务验证智能体                                   |
+| `BASH_CLASSIFIER`             | 分类器辅助的 bash 权限决策                       |
+| `EXTRACT_MEMORIES`            | 查询后自动记忆提取                               |
+| `HISTORY_PICKER`              | 交互式提示历史选择器                             |
+| `MESSAGE_ACTIONS`             | UI 中的消息操作入口点                            |
+| `QUICK_SEARCH`                | 提示快速搜索                                     |
+| `SHOT_STATS`                  | Shot分布统计                                     |
+| `COMPACTION_REMINDERS`        | 上下文压缩周围的智能提醒                         |
+| `CACHED_MICROCOMPACT`         | 通过查询流程的缓存微压缩状态                     |
 
 完整的功能标志审计请参阅 [FEATURES.md](FEATURES.md)。
 
@@ -206,16 +213,16 @@ src/
 
 ## 技术栈
 
-| | |
-|---|---|
-| 运行时 | [Bun](https://bun.sh) |
-| 语言 | TypeScript |
-| 终端 UI | React + [Ink](https://github.com/vadimdemedes/ink) |
-| CLI 解析 | [Commander.js](https://github.com/tj/commander.js) |
-| Schema 验证 | Zod v4 |
-| 代码搜索 | ripgrep(已打包) |
-| 协议 | MCP、LSP |
-| API | Anthropic Messages API |
+|             |                                                    |
+| ----------- | -------------------------------------------------- |
+| 运行时      | [Bun](https://bun.sh)                              |
+| 语言        | TypeScript                                         |
+| 终端 UI     | React + [Ink](https://github.com/vadimdemedes/ink) |
+| CLI 解析    | [Commander.js](https://github.com/tj/commander.js) |
+| Schema 验证 | Zod v4                                             |
+| 代码搜索    | ripgrep(已打包)                                  |
+| 协议        | MCP、LSP                                           |
+| API         | Anthropic Messages API                             |
 
 ---
 

+ 1 - 1
src/components/LogoV2/ChannelsNotice.tsx

@@ -135,7 +135,7 @@ export function ChannelsNotice() {
   }
   let t2;
   if ($[24] !== flag) {
-    t2 = <Text dimColor={true}>Experimental · inbound messages will be pushed into this session, this carries prompt injection risks. Restart Free Code without {flag} to disable.</Text>;
+    t2 = <Text dimColor={true}>Experimental · inbound messages will be pushed into this session, this carries prompt injection risks. Restart Claude Code CN without {flag} to disable.</Text>;
     $[24] = flag;
     $[25] = t2;
   } else {

+ 1 - 1
src/components/LogoV2/CondensedLogo.tsx

@@ -88,7 +88,7 @@ export function CondensedLogo() {
   }
   let t5;
   if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
-    t5 = <Text bold={true}>Free Code</Text>;
+    t5 = <Text bold={true}>Claude Code CN</Text>;
     $[8] = t5;
   } else {
     t5 = $[8];

+ 1 - 1
src/components/LogoV2/GuestPassesUpsell.tsx

@@ -60,7 +60,7 @@ export function GuestPassesUpsell() {
   let t0;
   if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
     const reward = getCachedReferrerReward();
-    t0 = <Text dimColor={true}><Text color="startupAccent">[✻]</Text> <Text color="startupAccent">[✻]</Text>{" "}<Text color="startupAccent">[✻]</Text> ·{" "}{reward ? `分享 Free Code 并赚取 ${formatCreditAmount(reward)} 的额外用量 · /passes` : "3 个访客通行证在 /passes"}</Text>;
+    t0 = <Text dimColor={true}><Text color="startupAccent">[✻]</Text> <Text color="startupAccent">[✻]</Text>{" "}<Text color="startupAccent">[✻]</Text> ·{" "}{reward ? `分享 Claude Code CN 并赚取 ${formatCreditAmount(reward)} 的额外用量 · /passes` : "3 个访客通行证在 /passes"}</Text>;
     $[0] = t0;
   } else {
     t0 = $[0];

+ 2 - 2
src/components/LogoV2/LogoV2.tsx

@@ -248,8 +248,8 @@ export function LogoV2() {
   }
   const layoutMode = getLayoutMode(columns);
   const userTheme = resolveThemeSetting(getGlobalConfig().theme);
-  const borderTitle = ` ${color("startupAccent", userTheme)("Free Code")} ${color("inactive", userTheme)(`v${version}`)} `;
-  const compactBorderTitle = color("startupAccent", userTheme)(" Free Code ");
+  const borderTitle = ` ${color("startupAccent", userTheme)("Claude Code CN")} ${color("inactive", userTheme)(`v${version}`)} `;
+  const compactBorderTitle = color("startupAccent", userTheme)(" Claude Code CN ");
   if (layoutMode === "compact") {
     let welcomeMessage = formatWelcomeMessage(username);
     if (stringWidth(welcomeMessage) > columns - 4) {

+ 3 - 3
src/components/LogoV2/WelcomeV2.tsx

@@ -9,7 +9,7 @@ export function WelcomeV2() {
   if (env.terminal === "Apple_Terminal") {
     let t0;
     if ($[0] !== theme) {
-      t0 = <AppleTerminalWelcomeV2 theme={theme} welcomeMessage="欢迎使用 Free Code" />;
+      t0 = <AppleTerminalWelcomeV2 theme={theme} welcomeMessage="欢迎使用 Claude Code CN" />;
       $[0] = theme;
       $[1] = t0;
     } else {
@@ -28,7 +28,7 @@ export function WelcomeV2() {
     let t7;
     let t8;
     if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
-      t0 = <Text><Text color="startupAccent">{"欢迎使用 Free Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
+      t0 = <Text><Text color="startupAccent">{"欢迎使用 Claude Code CN"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
       t1 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>;
       t2 = <Text>{"                                                          "}</Text>;
       t3 = <Text>{"                                                          "}</Text>;
@@ -113,7 +113,7 @@ export function WelcomeV2() {
   let t5;
   let t6;
   if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
-    t0 = <Text><Text color="startupAccent">{"欢迎使用 Free Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
+    t0 = <Text><Text color="startupAccent">{"欢迎使用 Claude Code CN"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
     t1 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>;
     t2 = <Text>{"                                                          "}</Text>;
     t3 = <Text>{"     *                                       \u2588\u2588\u2588\u2588\u2588\u2593\u2593\u2591     "}</Text>;

+ 7 - 7
src/components/LogoV2/feedConfigs.tsx

@@ -39,7 +39,7 @@ export function createWhatsNewFeed(releaseNotes: string[]): FeedConfig {
       text: note
     };
   });
-  const emptyMessage = "external" === 'ant' ? '无法获取最新的 claude-cli-internal 提交' : '查看 Free Code 更新日志以获取更新';
+  const emptyMessage = "external" === 'ant' ? '无法获取最新的 claude-cli-internal 提交' : '查看 Claude Code CN 更新日志以获取更新';
   return {
     title: "external" === 'ant' ? "新功能 [ANT-ONLY: Latest CC commits]" : "新功能",
     lines,
@@ -73,17 +73,17 @@ export function createProjectOnboardingFeed(steps: Step[]): FeedConfig {
 }
 export function createGuestPassesFeed(): FeedConfig {
   const reward = getCachedReferrerReward();
-  const subtitle = reward ? `分享 Free Code 并赚取 ${formatCreditAmount(reward)} 的额外用量` : '与朋友分享 Free Code';
+  const subtitle = reward ? `分享 Claude Code CN 并赚取 ${formatCreditAmount(reward)} 的额外用量` : '与朋友分享 Claude Code CN';
   return {
     title: '3 个访客通行证',
     lines: [],
     customContent: {
       content: <>
-          <Box marginY={1}>
-            <Text color="startupAccent">[✻] [✻] [✻]</Text>
-          </Box>
-          <Text dimColor>{subtitle}</Text>
-        </>,
+        <Box marginY={1}>
+          <Text color="startupAccent">[✻] [✻] [✻]</Text>
+        </Box>
+        <Text dimColor>{subtitle}</Text>
+      </>,
       width: 48
     },
     footer: '/passes'

+ 310 - 310
src/screens/REPL.tsx

@@ -97,8 +97,8 @@ import { logError } from '../utils/log.js';
 /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
 const useVoiceIntegration: typeof import('../hooks/useVoiceIntegration.js').useVoiceIntegration = feature('VOICE_MODE') ? require('../hooks/useVoiceIntegration.js').useVoiceIntegration : () => ({
   stripTrailing: () => 0,
-  handleKeyEvent: () => {},
-  resetAnchor: () => {}
+  handleKeyEvent: () => { },
+  resetAnchor: () => { }
 });
 const VoiceKeybindingHandler: typeof import('../hooks/useVoiceIntegration.js').VoiceKeybindingHandler = feature('VOICE_MODE') ? require('../hooks/useVoiceIntegration.js').VoiceKeybindingHandler : () => null;
 // Frustration detection is ant-only (dogfooding). Conditional require so external
@@ -106,11 +106,11 @@ const VoiceKeybindingHandler: typeof import('../hooks/useVoiceIntegration.js').V
 // on every messages change, plus the GrowthBook fetch).
 const useFrustrationDetection: typeof import('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection = "external" === 'ant' ? require('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection : () => ({
   state: 'closed',
-  handleTranscriptSelect: () => {}
+  handleTranscriptSelect: () => { }
 });
 // Ant-only org warning. Conditional require so the org UUID list is
 // eliminated from external builds (one UUID is on excluded-strings).
-const useAntOrgWarningNotification: typeof import('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification = "external" === 'ant' ? require('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification : () => {};
+const useAntOrgWarningNotification: typeof import('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification = "external" === 'ant' ? require('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification : () => { };
 // Dead code elimination: conditional import for coordinator mode
 const getCoordinatorUserContext: (mcpClients: ReadonlyArray<{
   name: string;
@@ -192,7 +192,7 @@ import { useInboxPoller } from '../hooks/useInboxPoller.js';
 // Dead code elimination: conditional import for loop mode
 /* eslint-disable @typescript-eslint/no-require-imports */
 const proactiveModule = feature('PROACTIVE') || feature('KAIROS') ? require('../proactive/index.js') : null;
-const PROACTIVE_NO_OP_SUBSCRIBE = (_cb: () => void) => () => {};
+const PROACTIVE_NO_OP_SUBSCRIBE = (_cb: () => void) => () => { };
 const PROACTIVE_FALSE = () => false;
 const SUGGEST_BG_PR_NOOP = (_p: string, _n: string): boolean => false;
 const useProactive = feature('PROACTIVE') || feature('KAIROS') ? require('../proactive/useProactive.js').useProactive : null;
@@ -299,7 +299,7 @@ const EMPTY_MCP_CLIENTS: MCPServerConnection[] = [];
 // Stable stub for useAssistantHistory's non-KAIROS branch — avoids a new
 // function identity each render, which would break composedOnScroll's memo.
 const HISTORY_STUB = {
-  maybeLoadOlder: (_: ScrollBoxHandle) => {}
+  maybeLoadOlder: (_: ScrollBoxHandle) => { }
 };
 // Window after a user-initiated scroll during which type-into-empty does NOT
 // repin to bottom. Josh Rosen's workflow: Claude emits long output → scroll
@@ -450,28 +450,28 @@ function TranscriptSearchBar({
   const off = cursorOffset;
   const cursorChar = off < query.length ? query[off] : ' ';
   return <Box borderTopDimColor borderBottom={false} borderLeft={false} borderRight={false} borderStyle="single" marginTop={1} paddingLeft={2} width="100%"
-  // applySearchHighlight scans the whole screen buffer. The query
-  // text rendered here IS on screen — /foo matches its own 'foo' in
-  // the bar. With no content matches that's the ONLY visible match →
-  // gets CURRENT → underlined. noSelect makes searchHighlight.ts:76
-  // skip these cells (same exclusion as gutters). You can't text-
-  // select the bar either; it's transient chrome, fine.
-  noSelect>
-      <Text>/</Text>
-      <Text>{query.slice(0, off)}</Text>
-      <Text inverse>{cursorChar}</Text>
-      {off < query.length && <Text>{query.slice(off + 1)}</Text>}
-      <Box flexGrow={1} />
-      {indexStatus === 'building' ? <Text dimColor>索引中… </Text> : indexStatus ? <Text dimColor>{indexStatus.ms}毫秒索引完成 </Text> : count === 0 && query ? <Text color="error">无匹配 </Text> : count > 0 ?
-    // Engine-counted (indexOf on extractSearchText). May drift from
-    // render-count for ghost/phantom messages — badge is a rough
-    // location hint. scanElement gives exact per-message positions
-    // but counting ALL would cost ~1-3ms × matched-messages.
-    <Text dimColor>
-          {current}/{count}
-          {'  '}
-        </Text> : null}
-    </Box>;
+    // applySearchHighlight scans the whole screen buffer. The query
+    // text rendered here IS on screen — /foo matches its own 'foo' in
+    // the bar. With no content matches that's the ONLY visible match →
+    // gets CURRENT → underlined. noSelect makes searchHighlight.ts:76
+    // skip these cells (same exclusion as gutters). You can't text-
+    // select the bar either; it's transient chrome, fine.
+    noSelect>
+    <Text>/</Text>
+    <Text>{query.slice(0, off)}</Text>
+    <Text inverse>{cursorChar}</Text>
+    {off < query.length && <Text>{query.slice(off + 1)}</Text>}
+    <Box flexGrow={1} />
+    {indexStatus === 'building' ? <Text dimColor>索引中… </Text> : indexStatus ? <Text dimColor>{indexStatus.ms}毫秒索引完成 </Text> : count === 0 && query ? <Text color="error">无匹配 </Text> : count > 0 ?
+      // Engine-counted (indexOf on extractSearchText). May drift from
+      // render-count for ghost/phantom messages — badge is a rough
+      // location hint. scanElement gives exact per-message positions
+      // but counting ALL would cost ~1-3ms × matched-messages.
+      <Text dimColor>
+        {current}/{count}
+        {'  '}
+      </Text> : null}
+  </Box>;
 }
 const TITLE_ANIMATION_FRAMES = ['⠂', '⠐'];
 const TITLE_STATIC_PREFIX = '✳';
@@ -607,8 +607,8 @@ export function REPL({
   const moreRightEnabled = useMemo(() => "external" === 'ant' && isEnvTruthy(process.env.CLAUDE_MORERIGHT), []);
   const disableVirtualScroll = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_VIRTUAL_SCROLL), []);
   const disableMessageActions = feature('MESSAGE_ACTIONS') ?
-  // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
-  useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MESSAGE_ACTIONS), []) : false;
+    // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+    useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MESSAGE_ACTIONS), []) : false;
 
   // Log REPL mount/unmount lifecycle
   useEffect(() => {
@@ -873,11 +873,11 @@ export function REPL({
 
   // Ref for the bridge result callback — set after useReplBridge initializes,
   // read in the onQuery finally block to notify mobile clients that a turn ended.
-  const sendBridgeResultRef = useRef<() => void>(() => {});
+  const sendBridgeResultRef = useRef<() => void>(() => { });
 
   // Ref for the synchronous restore callback — set after restoreMessageSync is
   // defined, read in the onQuery finally block for auto-restore on interrupt.
-  const restoreMessageSyncRef = useRef<(m: UserMessage) => void>(() => {});
+  const restoreMessageSyncRef = useRef<(m: UserMessage) => void>(() => { });
 
   // Ref to the fullscreen layout's scroll box for keyboard scrolling.
   // Null when fullscreen mode is disabled (ref never attached).
@@ -1135,7 +1135,7 @@ export function REPL({
   // session from mid-conversation context.
   const haikuTitleAttemptedRef = useRef((initialMessages?.length ?? 0) > 0);
   const agentTitle = mainThreadAgentDefinition?.agentType;
-  const terminalTitle = sessionTitle ?? agentTitle ?? haikuTitle ?? 'Free Code';
+  const terminalTitle = sessionTitle ?? agentTitle ?? haikuTitle ?? 'Claude Code CN';
   const isWaitingForApproval = toolUseConfirmQueue.length > 0 || promptQueue.length > 0 || pendingWorkerRequest || pendingSandboxRequest;
   // Local-jsx commands (like /plugin, /config) show user-facing dialogs that
   // wait for input. Require jsx != null — if the flag is stuck true but jsx
@@ -1254,8 +1254,8 @@ export function REPL({
   const cursorNavRef = useRef<MessageActionsNav | null>(null);
   // Memoized so Messages' React.memo holds.
   const unseenDivider = useMemo(() => computeUnseenDivider(messages, dividerIndex),
-  // eslint-disable-next-line react-hooks/exhaustive-deps -- length change covers appends; useUnseenDivider's count-drop guard clears dividerIndex on replace/rewind
-  [dividerIndex, messages.length]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps -- length change covers appends; useUnseenDivider's count-drop guard clears dividerIndex on replace/rewind
+    [dividerIndex, messages.length]);
   // Re-pin scroll to bottom and clear the unseen-messages baseline. Called
   // on any user-driven return-to-live action (submit, type-into-empty,
   // overlay appear/dismiss).
@@ -1284,13 +1284,13 @@ export function REPL({
   const {
     maybeLoadOlder
   } = feature('KAIROS') ?
-  // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
-  useAssistantHistory({
-    config: remoteSessionConfig,
-    setMessages,
-    scrollRef,
-    onPrepend: shiftDivider
-  }) : HISTORY_STUB;
+      // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+      useAssistantHistory({
+        config: remoteSessionConfig,
+        setMessages,
+        scrollRef,
+        onPrepend: shiftDivider
+      }) : HISTORY_STUB;
   // Compose useUnseenDivider's callbacks with the lazy-load trigger.
   const composedOnScroll = useCallback((sticky: boolean, handle: ScrollBoxHandle) => {
     lastUserScrollTsRef.current = Date.now();
@@ -1601,12 +1601,12 @@ export function REPL({
       swarmStartTimeRef.current = null;
       swarmBudgetInfoRef.current = undefined;
       setMessages(prev => [...prev, createTurnDurationMessage(totalMs, deferredBudget,
-      // Count only what recordTranscript will persist — ephemeral
-      // progress ticks and non-ant attachments are filtered by
-      // isLoggableMessage and never reach disk. Using raw prev.length
-      // would make checkResumeConsistency report false delta<0 for
-      // every turn that ran a progress-emitting tool.
-      count(prev, isLoggableMessage))]);
+        // Count only what recordTranscript will persist — ephemeral
+        // progress ticks and non-ant attachments are filtered by
+        // isLoggableMessage and never reach disk. Using raw prev.length
+        // would make checkResumeConsistency report false delta<0 for
+        // every turn that ran a progress-emitting tool.
+        count(prev, isLoggableMessage))]);
     }
   }, [hasRunningTeammates, setMessages]);
 
@@ -1673,19 +1673,19 @@ export function REPL({
     setToolJSX
   });
   const showSpinner = (!toolJSX || toolJSX.showSpinner === true) && toolUseConfirmQueue.length === 0 && promptQueue.length === 0 && (
-  // Show spinner during input processing, API call, while teammates are running,
-  // or while pending task notifications are queued (prevents spinner bounce between consecutive notifications)
-  isLoading || userInputOnProcessing || hasRunningTeammates ||
-  // Keep spinner visible while task notifications are queued for processing.
-  // Without this, the spinner briefly disappears between consecutive notifications
-  // (e.g., multiple background agents completing in rapid succession) because
-  // isLoading goes false momentarily between processing each one.
-  getCommandQueueLength() > 0) &&
-  // Hide spinner when waiting for leader to approve permission request
-  !pendingWorkerRequest && !onlySleepToolActive && (
-  // Hide spinner when streaming text is visible (the text IS the feedback),
-  // but keep it when isBriefOnly suppresses the streaming text display
-  !visibleStreamingText || isBriefOnly);
+    // Show spinner during input processing, API call, while teammates are running,
+    // or while pending task notifications are queued (prevents spinner bounce between consecutive notifications)
+    isLoading || userInputOnProcessing || hasRunningTeammates ||
+    // Keep spinner visible while task notifications are queued for processing.
+    // Without this, the spinner briefly disappears between consecutive notifications
+    // (e.g., multiple background agents completing in rapid succession) because
+    // isLoading goes false momentarily between processing each one.
+    getCommandQueueLength() > 0) &&
+    // Hide spinner when waiting for leader to approve permission request
+    !pendingWorkerRequest && !onlySleepToolActive && (
+      // Hide spinner when streaming text is visible (the text IS the feedback),
+      // but keep it when isBriefOnly suppresses the streaming text display
+      !visibleStreamingText || isBriefOnly);
 
   // Check if any permission or ask question prompt is currently visible
   // This is used to prevent the survey from opening while prompts are active
@@ -2331,9 +2331,9 @@ export function REPL({
     addNotification({
       key: 'sandbox-unavailable',
       jsx: <>
-          <Text color="warning">沙箱已禁用</Text>
-          <Text dimColor> · /sandbox</Text>
-        </>,
+        <Text color="warning">沙箱已禁用</Text>
+        <Text dimColor> · /sandbox</Text>
+      </>,
       priority: 'medium'
     });
   }, [addNotification]);
@@ -2694,7 +2694,7 @@ export function REPL({
       if (text && !text.startsWith(`<${LOCAL_COMMAND_STDOUT_TAG}>`) && !text.startsWith(`<${COMMAND_MESSAGE_TAG}>`) && !text.startsWith(`<${COMMAND_NAME_TAG}>`) && !text.startsWith(`<${BASH_INPUT_TAG}>`)) {
         haikuTitleAttemptedRef.current = true;
         void generateSessionTitle(text, new AbortController().signal).then(title => {
-          if (title) setHaikuTitle(title);else haikuTitleAttemptedRef.current = false;
+          if (title) setHaikuTitle(title); else haikuTitleAttemptedRef.current = false;
         }, () => {
           haikuTitleAttemptedRef.current = false;
         });
@@ -2768,11 +2768,11 @@ export function REPL({
       });
     }
     queryCheckpoint('query_context_loading_start');
-    const [,, defaultSystemPrompt, baseUserContext, systemContext] = await Promise.all([
-    // IMPORTANT: do this after setMessages() above, to avoid UI jank
-    checkAndDisableBypassPermissionsIfNeeded(toolPermissionContext, setAppState),
-    // Gated on TRANSCRIPT_CLASSIFIER so GrowthBook kill switch runs wherever auto mode is built in
-    feature('TRANSCRIPT_CLASSIFIER') ? checkAndDisableAutoModeIfNeeded(toolPermissionContext, setAppState, store.getState().fastMode) : undefined, getSystemPrompt(freshTools, mainLoopModelParam, Array.from(toolPermissionContext.additionalWorkingDirectories.keys()), freshMcpClients), getUserContext(), getSystemContext()]);
+    const [, , defaultSystemPrompt, baseUserContext, systemContext] = await Promise.all([
+      // IMPORTANT: do this after setMessages() above, to avoid UI jank
+      checkAndDisableBypassPermissionsIfNeeded(toolPermissionContext, setAppState),
+      // Gated on TRANSCRIPT_CLASSIFIER so GrowthBook kill switch runs wherever auto mode is built in
+      feature('TRANSCRIPT_CLASSIFIER') ? checkAndDisableAutoModeIfNeeded(toolPermissionContext, setAppState, store.getState().fastMode) : undefined, getSystemPrompt(freshTools, mainLoopModelParam, Array.from(toolPermissionContext.additionalWorkingDirectories.keys()), freshMcpClients), getUserContext(), getSystemContext()]);
     const userContext = {
       ...baseUserContext,
       ...getCoordinatorUserContext(freshMcpClients, isScratchpadEnabled() ? getScratchpadDir() : undefined),
@@ -3118,9 +3118,9 @@ export function REPL({
       if (typeof content === 'string' && !initialMsg.message.planContent) {
         // Route through onSubmit for proper processing including UserPromptSubmit hooks
         void onSubmit(content, {
-          setCursorOffset: () => {},
-          clearBuffer: () => {},
-          resetHistory: () => {}
+          setCursorOffset: () => { },
+          clearBuffer: () => { },
+          resetHistory: () => { }
         });
       } else {
         // Plan messages or complex content (images, etc.) - send directly to model
@@ -3129,10 +3129,10 @@ export function REPL({
         const newAbortController = createAbortController();
         setAbortController(newAbortController);
         void onQuery([initialMsg.message], newAbortController, true,
-        // shouldQuery
-        [],
-        // additionalAllowedTools
-        mainLoopModel);
+          // shouldQuery
+          [],
+          // additionalAllowedTools
+          mainLoopModel);
       }
 
       // Reset ref after a delay to allow new initial messages
@@ -3534,18 +3534,18 @@ export function REPL({
       setStashedPrompt(undefined);
     }
   }, [queryGuard,
-  // isLoading is read at the !isLoading checks above for input-clearing
-  // and submitCount gating. It's derived from isQueryActive || isExternalLoading,
-  // so including it here ensures the closure captures the fresh value.
-  isLoading, isExternalLoading, inputMode, commands, setInputValue, setInputMode, setPastedContents, setSubmitCount, setIDESelection, setToolJSX, getToolUseContext,
-  // messages is read via messagesRef.current inside the callback to
-  // keep onSubmit stable across message updates (see L2384/L2400/L2662).
-  // Without this, each setMessages call (~30× per turn) recreates
-  // onSubmit, pinning the REPL render scope (1776B) + that render's
-  // messages array in downstream closures (PromptInput, handleAutoRunIssue).
-  // Heap analysis showed ~9 REPL scopes and ~15 messages array versions
-  // accumulating after #20174/#20175, all traced to this dep.
-  mainLoopModel, pastedContents, ideSelection, setUserInputOnProcessing, setAbortController, addNotification, onQuery, stashedPrompt, setStashedPrompt, setAppState, onBeforeQuery, canUseTool, remoteSession, setMessages, awaitPendingHooks, repinScroll]);
+    // isLoading is read at the !isLoading checks above for input-clearing
+    // and submitCount gating. It's derived from isQueryActive || isExternalLoading,
+    // so including it here ensures the closure captures the fresh value.
+    isLoading, isExternalLoading, inputMode, commands, setInputValue, setInputMode, setPastedContents, setSubmitCount, setIDESelection, setToolJSX, getToolUseContext,
+    // messages is read via messagesRef.current inside the callback to
+    // keep onSubmit stable across message updates (see L2384/L2400/L2662).
+    // Without this, each setMessages call (~30× per turn) recreates
+    // onSubmit, pinning the REPL render scope (1776B) + that render's
+    // messages array in downstream closures (PromptInput, handleAutoRunIssue).
+    // Heap analysis showed ~9 REPL scopes and ~15 messages array versions
+    // accumulating after #20174/#20175, all traced to this dep.
+    mainLoopModel, pastedContents, ideSelection, setUserInputOnProcessing, setAbortController, addNotification, onQuery, stashedPrompt, setStashedPrompt, setAppState, onBeforeQuery, canUseTool, remoteSession, setMessages, awaitPendingHooks, repinScroll]);
 
   // Callback for when user submits input while viewing a teammate's transcript
   const onAgentSubmit = useCallback(async (input: string, task: InProcessTeammateTaskState | LocalAgentTaskState, helpers: PromptInputHelpers) => {
@@ -3566,8 +3566,8 @@ export function REPL({
           addNotification({
             key: `resume-agent-failed-${task.id}`,
             jsx: <Text color="error">
-                  Failed to resume agent: {errorMessage(err)}
-                </Text>,
+              Failed to resume agent: {errorMessage(err)}
+            </Text>,
             priority: 'low'
           });
         });
@@ -3585,9 +3585,9 @@ export function REPL({
     const command = autoRunIssueReason ? getAutoRunCommand(autoRunIssueReason) : '/issue';
     setAutoRunIssueReason(null); // Clear the state
     onSubmit(command, {
-      setCursorOffset: () => {},
-      clearBuffer: () => {},
-      resetHistory: () => {}
+      setCursorOffset: () => { },
+      clearBuffer: () => { },
+      resetHistory: () => { }
     }).catch(err => {
       logForDebugging(`Auto-run ${command} failed: ${errorMessage(err)}`);
     });
@@ -3600,9 +3600,9 @@ export function REPL({
   const handleSurveyRequestFeedback = useCallback(() => {
     const command = "external" === 'ant' ? '/issue' : '/feedback';
     onSubmit(command, {
-      setCursorOffset: () => {},
-      clearBuffer: () => {},
-      resetHistory: () => {}
+      setCursorOffset: () => { },
+      clearBuffer: () => { },
+      resetHistory: () => { }
     }).catch(err => {
       logForDebugging(`Survey feedback request failed: ${err instanceof Error ? err.message : String(err)}`);
     });
@@ -3617,9 +3617,9 @@ export function REPL({
   onSubmitRef.current = onSubmit;
   const handleOpenRateLimitOptions = useCallback(() => {
     void onSubmitRef.current('/rate-limit-options', {
-      setCursorOffset: () => {},
-      clearBuffer: () => {},
-      resetHistory: () => {}
+      setCursorOffset: () => { },
+      clearBuffer: () => { },
+      resetHistory: () => { }
     });
   }, []);
   const handleExit = useCallback(async () => {
@@ -3636,14 +3636,14 @@ export function REPL({
     }
     const showWorktree = getCurrentWorktreeSession() !== null;
     if (showWorktree) {
-      setExitFlow(<ExitFlow showWorktree onDone={() => {}} onCancel={() => {
+      setExitFlow(<ExitFlow showWorktree onDone={() => { }} onCancel={() => {
         setExitFlow(null);
         setIsExiting(false);
       }} />);
       return;
     }
     const exitMod = await exit.load();
-    const exitFlowResult = await exitMod.call(() => {});
+    const exitFlowResult = await exitMod.call(() => { });
     setExitFlow(exitFlowResult);
     // If call() returned without killing the process (bg session detach),
     // clear isExiting so the UI is usable on reattach. No-op on the normal
@@ -3757,18 +3757,18 @@ export function REPL({
   };
   const messageActionCaps: MessageActionCaps = {
     copy: text =>
-    // setClipboard RETURNS OSC 52 — caller must stdout.write (tmux side-effects load-buffer, but that's tmux-only).
-    void setClipboard(text).then(raw => {
-      if (raw) process.stdout.write(raw);
-      addNotification({
-        // Same key as text-selection copy — repeated copies replace toast, don't queue.
-        key: 'selection-copied',
-        text: 'copied',
-        color: 'success',
-        priority: 'immediate',
-        timeoutMs: 2000
-      });
-    }),
+      // setClipboard RETURNS OSC 52 — caller must stdout.write (tmux side-effects load-buffer, but that's tmux-only).
+      void setClipboard(text).then(raw => {
+        if (raw) process.stdout.write(raw);
+        addNotification({
+          // Same key as text-selection copy — repeated copies replace toast, don't queue.
+          key: 'selection-copied',
+          text: 'copied',
+          color: 'success',
+          priority: 'immediate',
+          timeoutMs: 2000
+        });
+      }),
     edit: async msg => {
       // Same skip-confirm check as /rewind: lossless → direct, else confirm dialog.
       const rawIdx = findRawIndex(msg.uuid);
@@ -3864,14 +3864,14 @@ export function REPL({
   const executeQueuedInput = useCallback(async (queuedCommands: QueuedCommand[]) => {
     await handlePromptSubmit({
       helpers: {
-        setCursorOffset: () => {},
-        clearBuffer: () => {},
-        resetHistory: () => {}
+        setCursorOffset: () => { },
+        clearBuffer: () => { },
+        resetHistory: () => { }
       },
       queryGuard,
       commands,
-      onInputChange: () => {},
-      setPastedContents: () => {},
+      onInputChange: () => { },
+      setPastedContents: () => { },
       setToolJSX,
       getToolUseContext,
       messages,
@@ -3932,8 +3932,8 @@ export function REPL({
       // User hasn't interacted since response ended, check other conditions
       const idleTimeSinceResponse = Date.now() - lastQueryCompletionTime;
       if (!isLoading && !toolJSX &&
-      // Use ref to get current dialog state, avoiding stale closure
-      focusedInputDialogRef.current === undefined && idleTimeSinceResponse >= getGlobalConfig().messageIdleNotifThresholdMs) {
+        // Use ref to get current dialog state, avoiding stale closure
+        focusedInputDialogRef.current === undefined && idleTimeSinceResponse >= getGlobalConfig().messageIdleNotifThresholdMs) {
         void sendNotification({
           message: 'Claude 正在等待您的输入',
           notificationType: 'idle_prompt'
@@ -3965,13 +3965,13 @@ export function REPL({
       addNotif({
         key: 'idle-return-hint',
         jsx: mode === 'hint_v2' ? <>
-                <Text dimColor>new task? </Text>
-                <Text color="suggestion">/clear</Text>
-                <Text dimColor> to save </Text>
-                <Text color="suggestion">{formattedTokens} tokens</Text>
-              </> : <Text color="warning">
-                new task? /clear to save {formattedTokens} tokens
-              </Text>,
+          <Text dimColor>new task? </Text>
+          <Text color="suggestion">/clear</Text>
+          <Text dimColor> to save </Text>
+          <Text color="suggestion">{formattedTokens} tokens</Text>
+        </> : <Text color="warning">
+          new task? /clear to save {formattedTokens} tokens
+        </Text>,
         priority: 'medium',
         // Persist until submit — the hint fires at T+75min idle, user may
         // not return for hours. removeNotification in useEffect cleanup
@@ -4023,17 +4023,17 @@ export function REPL({
 
   // Voice input integration (VOICE_MODE builds only)
   const voice = feature('VOICE_MODE') ?
-  // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
-  useVoiceIntegration({
-    setInputValueRaw,
-    inputValueRef,
-    insertTextRef
-  }) : {
-    stripTrailing: () => 0,
-    handleKeyEvent: () => {},
-    resetAnchor: () => {},
-    interimRange: null
-  };
+    // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+    useVoiceIntegration({
+      setInputValueRaw,
+      inputValueRef,
+      insertTextRef
+    }) : {
+      stripTrailing: () => 0,
+      handleKeyEvent: () => { },
+      resetAnchor: () => { },
+      interimRange: null
+    };
   useInboxPoller({
     enabled: isAgentSwarmsEnabled(),
     isLoading,
@@ -4236,11 +4236,11 @@ export function REPL({
       event.stopImmediatePropagation();
     }
   },
-  // Search needs virtual scroll (jumpRef drives VirtualMessageList). [
-  // kills it, so !dumpMode — after [ there's nothing to jump in.
-  {
-    isActive: screen === 'transcript' && virtualScrollActive && !searchOpen && !dumpMode
-  });
+    // Search needs virtual scroll (jumpRef drives VirtualMessageList). [
+    // kills it, so !dumpMode — after [ there's nothing to jump in.
+    {
+      isActive: screen === 'transcript' && virtualScrollActive && !searchOpen && !dumpMode
+    });
   const {
     setQuery: setHighlight,
     scanElement,
@@ -4331,12 +4331,12 @@ export function REPL({
       })();
     }
   },
-  // !searchOpen: typing 'v' or '[' in the search bar is search input, not
-  // a command. No !dumpMode here — v should work after [ (the [ handler
-  // guards itself inline).
-  {
-    isActive: screen === 'transcript' && virtualScrollActive && !searchOpen
-  });
+    // !searchOpen: typing 'v' or '[' in the search bar is search input, not
+    // a command. No !dumpMode here — v should work after [ (the [ handler
+    // guards itself inline).
+    {
+      isActive: screen === 'transcript' && virtualScrollActive && !searchOpen
+    });
 
   // Fresh `less` per transcript entry. Prevents stale highlights matching
   // unrelated normal-mode text (overlay is alt-screen-global) and avoids
@@ -4404,78 +4404,78 @@ export function REPL({
     const transcriptScrollRef = isFullscreenEnvEnabled() && !disableVirtualScroll && !dumpMode ? scrollRef : undefined;
     const transcriptMessagesElement = <Messages messages={transcriptMessages} tools={tools} commands={commands} verbose={true} toolJSX={null} toolUseConfirmQueue={[]} inProgressToolUseIDs={inProgressToolUseIDs} isMessageSelectorVisible={false} conversationId={conversationId} screen={screen} agentDefinitions={agentDefinitions} streamingToolUses={transcriptStreamingToolUses} showAllInTranscript={showAllInTranscript} onOpenRateLimitOptions={handleOpenRateLimitOptions} isLoading={isLoading} hidePastThinking={true} streamingThinking={streamingThinking} scrollRef={transcriptScrollRef} jumpRef={jumpRef} onSearchMatchesChange={onSearchMatchesChange} scanElement={scanElement} setPositions={setPositions} disableRenderCap={dumpMode} />;
     const transcriptToolJSX = toolJSX && <Box flexDirection="column" width="100%">
-        {toolJSX.jsx}
-      </Box>;
+      {toolJSX.jsx}
+    </Box>;
     const transcriptReturn = <KeybindingSetup>
-        <AnimatedTerminalTitle isAnimating={titleIsAnimating} title={terminalTitle} disabled={titleDisabled} noPrefix={showStatusInTerminalTab} />
-        <GlobalKeybindingHandlers {...globalKeybindingProps} />
-        {feature('VOICE_MODE') ? <VoiceKeybindingHandler voiceHandleKeyEvent={voice.handleKeyEvent} stripTrailing={voice.stripTrailing} resetAnchor={voice.resetAnchor} isActive={!toolJSX?.isLocalJSXCommand} /> : null}
-        <CommandKeybindingHandlers onSubmit={onSubmit} isActive={!toolJSX?.isLocalJSXCommand} />
-        {transcriptScrollRef ?
-      // ScrollKeybindingHandler must mount before CancelRequestHandler so
-      // ctrl+c-with-selection copies instead of cancelling the active task.
-      // Its raw useInput handler only stops propagation when a selection
-      // exists — without one, ctrl+c falls through to CancelRequestHandler.
-      <ScrollKeybindingHandler scrollRef={scrollRef}
-      // Yield wheel/ctrl+u/d to UltraplanChoiceDialog's own scroll
-      // handler while the modal is showing.
-      isActive={focusedInputDialog !== 'ultraplan-choice'}
-      // g/G/j/k/ctrl+u/ctrl+d would eat keystrokes the search bar
-      // wants. Off while searching.
-      isModal={!searchOpen}
-      // Manual scroll exits the search context — clear the yellow
-      // current-match marker. Positions are (msg, rowOffset)-keyed;
-      // j/k changes scrollTop so rowOffset is stale → wrong row
-      // gets yellow. Next n/N re-establishes via step()→jump().
-      onScroll={() => jumpRef.current?.disarmSearch()} /> : null}
-        <CancelRequestHandler {...cancelRequestProps} />
-        {transcriptScrollRef ? <FullscreenLayout scrollRef={scrollRef} scrollable={<>
-                {transcriptMessagesElement}
-                {transcriptToolJSX}
-                <SandboxViolationExpandedView />
-              </>} bottom={searchOpen ? <TranscriptSearchBar jumpRef={jumpRef}
-      // Seed was tried (c01578c8) — broke /hello muscle
-      // memory (cursor lands after 'foo', /hello → foohello).
-      // Cancel-restore handles the 'don't lose prior search'
-      // concern differently (onCancel re-applies searchQuery).
-      initialQuery="" count={searchCount} current={searchCurrent} onClose={q => {
-        // Enter — commit. 0-match guard: junk query shouldn't
-        // persist (badge hidden, n/N dead anyway).
-        setSearchQuery(searchCount > 0 ? q : '');
-        setSearchOpen(false);
-        // onCancel path: bar unmounts before its useEffect([query])
-        // can fire with ''. Without this, searchCount stays stale
-        // (n guard at :4956 passes) and VML's matches[] too
-        // (nextMatch walks the old array). Phantom nav, no
-        // highlight. onExit (Enter, q non-empty) still commits.
-        if (!q) {
-          setSearchCount(0);
-          setSearchCurrent(0);
+      <AnimatedTerminalTitle isAnimating={titleIsAnimating} title={terminalTitle} disabled={titleDisabled} noPrefix={showStatusInTerminalTab} />
+      <GlobalKeybindingHandlers {...globalKeybindingProps} />
+      {feature('VOICE_MODE') ? <VoiceKeybindingHandler voiceHandleKeyEvent={voice.handleKeyEvent} stripTrailing={voice.stripTrailing} resetAnchor={voice.resetAnchor} isActive={!toolJSX?.isLocalJSXCommand} /> : null}
+      <CommandKeybindingHandlers onSubmit={onSubmit} isActive={!toolJSX?.isLocalJSXCommand} />
+      {transcriptScrollRef ?
+        // ScrollKeybindingHandler must mount before CancelRequestHandler so
+        // ctrl+c-with-selection copies instead of cancelling the active task.
+        // Its raw useInput handler only stops propagation when a selection
+        // exists — without one, ctrl+c falls through to CancelRequestHandler.
+        <ScrollKeybindingHandler scrollRef={scrollRef}
+          // Yield wheel/ctrl+u/d to UltraplanChoiceDialog's own scroll
+          // handler while the modal is showing.
+          isActive={focusedInputDialog !== 'ultraplan-choice'}
+          // g/G/j/k/ctrl+u/ctrl+d would eat keystrokes the search bar
+          // wants. Off while searching.
+          isModal={!searchOpen}
+          // Manual scroll exits the search context — clear the yellow
+          // current-match marker. Positions are (msg, rowOffset)-keyed;
+          // j/k changes scrollTop so rowOffset is stale → wrong row
+          // gets yellow. Next n/N re-establishes via step()→jump().
+          onScroll={() => jumpRef.current?.disarmSearch()} /> : null}
+      <CancelRequestHandler {...cancelRequestProps} />
+      {transcriptScrollRef ? <FullscreenLayout scrollRef={scrollRef} scrollable={<>
+        {transcriptMessagesElement}
+        {transcriptToolJSX}
+        <SandboxViolationExpandedView />
+      </>} bottom={searchOpen ? <TranscriptSearchBar jumpRef={jumpRef}
+        // Seed was tried (c01578c8) — broke /hello muscle
+        // memory (cursor lands after 'foo', /hello → foohello).
+        // Cancel-restore handles the 'don't lose prior search'
+        // concern differently (onCancel re-applies searchQuery).
+        initialQuery="" count={searchCount} current={searchCurrent} onClose={q => {
+          // Enter — commit. 0-match guard: junk query shouldn't
+          // persist (badge hidden, n/N dead anyway).
+          setSearchQuery(searchCount > 0 ? q : '');
+          setSearchOpen(false);
+          // onCancel path: bar unmounts before its useEffect([query])
+          // can fire with ''. Without this, searchCount stays stale
+          // (n guard at :4956 passes) and VML's matches[] too
+          // (nextMatch walks the old array). Phantom nav, no
+          // highlight. onExit (Enter, q non-empty) still commits.
+          if (!q) {
+            setSearchCount(0);
+            setSearchCurrent(0);
+            jumpRef.current?.setSearchQuery('');
+          }
+        }} onCancel={() => {
+          // Esc/ctrl+c/ctrl+g — undo. Bar's effect last fired
+          // with whatever was typed. searchQuery (REPL state)
+          // is unchanged since / (onClose = commit, didn't run).
+          // Two VML calls: '' restores anchor (0-match else-
+          // branch), then searchQuery re-scans from anchor's
+          // nearest. Both synchronous — one React batch.
+          // setHighlight explicit: REPL's sync-effect dep is
+          // searchQuery (unchanged), wouldn't re-fire.
+          setSearchOpen(false);
           jumpRef.current?.setSearchQuery('');
-        }
-      }} onCancel={() => {
-        // Esc/ctrl+c/ctrl+g — undo. Bar's effect last fired
-        // with whatever was typed. searchQuery (REPL state)
-        // is unchanged since / (onClose = commit, didn't run).
-        // Two VML calls: '' restores anchor (0-match else-
-        // branch), then searchQuery re-scans from anchor's
-        // nearest. Both synchronous — one React batch.
-        // setHighlight explicit: REPL's sync-effect dep is
-        // searchQuery (unchanged), wouldn't re-fire.
-        setSearchOpen(false);
-        jumpRef.current?.setSearchQuery('');
-        jumpRef.current?.setSearchQuery(searchQuery);
-        setHighlight(searchQuery);
-      }} setHighlight={setHighlight} /> : <TranscriptModeFooter showAllInTranscript={showAllInTranscript} virtualScroll={true} status={editorStatus || undefined} searchBadge={searchQuery && searchCount > 0 ? {
-        current: searchCurrent,
-        count: searchCount
-      } : undefined} />} /> : <>
-            {transcriptMessagesElement}
-            {transcriptToolJSX}
-            <SandboxViolationExpandedView />
-            <TranscriptModeFooter showAllInTranscript={showAllInTranscript} virtualScroll={false} suppressShowAll={dumpMode} status={editorStatus || undefined} />
-          </>}
-      </KeybindingSetup>;
+          jumpRef.current?.setSearchQuery(searchQuery);
+          setHighlight(searchQuery);
+        }} setHighlight={setHighlight} /> : <TranscriptModeFooter showAllInTranscript={showAllInTranscript} virtualScroll={true} status={editorStatus || undefined} searchBadge={searchQuery && searchCount > 0 ? {
+          current: searchCurrent,
+          count: searchCount
+        } : undefined} />} /> : <>
+        {transcriptMessagesElement}
+        {transcriptToolJSX}
+        <SandboxViolationExpandedView />
+        <TranscriptModeFooter showAllInTranscript={showAllInTranscript} virtualScroll={false} suppressShowAll={dumpMode} status={editorStatus || undefined} />
+      </>}
+    </KeybindingSetup>;
     // The virtual-scroll branch (FullscreenLayout above) needs
     // <AlternateScreen>'s <Box height={rows}> constraint — without it,
     // ScrollBox's flexGrow has no ceiling, viewport = content height,
@@ -4486,8 +4486,8 @@ export function REPL({
     // unwrapped — it wants native terminal scrollback.
     if (transcriptScrollRef) {
       return <AlternateScreen mouseTracking={isMouseTrackingEnabled()}>
-          {transcriptReturn}
-        </AlternateScreen>;
+        {transcriptReturn}
+      </AlternateScreen>;
     }
     return transcriptReturn;
   }
@@ -4549,11 +4549,11 @@ export function REPL({
   // early return above wraps its virtual-scroll branch the same way; only
   // the 30-cap dump branch stays unwrapped for native terminal scrollback.
   const mainReturn = <KeybindingSetup>
-      <AnimatedTerminalTitle isAnimating={titleIsAnimating} title={terminalTitle} disabled={titleDisabled} noPrefix={showStatusInTerminalTab} />
-      <GlobalKeybindingHandlers {...globalKeybindingProps} />
-      {feature('VOICE_MODE') ? <VoiceKeybindingHandler voiceHandleKeyEvent={voice.handleKeyEvent} stripTrailing={voice.stripTrailing} resetAnchor={voice.resetAnchor} isActive={!toolJSX?.isLocalJSXCommand} /> : null}
-      <CommandKeybindingHandlers onSubmit={onSubmit} isActive={!toolJSX?.isLocalJSXCommand} />
-      {/* ScrollKeybindingHandler must mount before CancelRequestHandler so
+    <AnimatedTerminalTitle isAnimating={titleIsAnimating} title={terminalTitle} disabled={titleDisabled} noPrefix={showStatusInTerminalTab} />
+    <GlobalKeybindingHandlers {...globalKeybindingProps} />
+    {feature('VOICE_MODE') ? <VoiceKeybindingHandler voiceHandleKeyEvent={voice.handleKeyEvent} stripTrailing={voice.stripTrailing} resetAnchor={voice.resetAnchor} isActive={!toolJSX?.isLocalJSXCommand} /> : null}
+    <CommandKeybindingHandlers onSubmit={onSubmit} isActive={!toolJSX?.isLocalJSXCommand} />
+    {/* ScrollKeybindingHandler must mount before CancelRequestHandler so
           ctrl+c-with-selection copies instead of cancelling the active task.
           Its raw useInput handler only stops propagation when a selection
           exists — without one, ctrl+c falls through to CancelRequestHandler.
@@ -4561,40 +4561,40 @@ export function REPL({
           the modal's inner ScrollBox is not keyboard-driven. onScroll
           stays suppressed while a modal is showing so scroll doesn't
           stamp divider/pill state. */}
-      <ScrollKeybindingHandler scrollRef={scrollRef} isActive={isFullscreenEnvEnabled() && (centeredModal != null || !focusedInputDialog || focusedInputDialog === 'tool-permission')} onScroll={centeredModal || toolPermissionOverlay || viewedAgentTask ? undefined : composedOnScroll} />
-      {feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? <MessageActionsKeybindings handlers={messageActionHandlers} isActive={cursor !== null} /> : null}
-      <CancelRequestHandler {...cancelRequestProps} />
-      <MCPConnectionManager key={remountKey} dynamicMcpConfig={dynamicMcpConfig} isStrictMcpConfig={strictMcpConfig}>
-        <FullscreenLayout scrollRef={scrollRef} overlay={toolPermissionOverlay} bottomFloat={feature('BUDDY') && companionVisible && !companionNarrow ? <CompanionFloatingBubble /> : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => {
+    <ScrollKeybindingHandler scrollRef={scrollRef} isActive={isFullscreenEnvEnabled() && (centeredModal != null || !focusedInputDialog || focusedInputDialog === 'tool-permission')} onScroll={centeredModal || toolPermissionOverlay || viewedAgentTask ? undefined : composedOnScroll} />
+    {feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? <MessageActionsKeybindings handlers={messageActionHandlers} isActive={cursor !== null} /> : null}
+    <CancelRequestHandler {...cancelRequestProps} />
+    <MCPConnectionManager key={remountKey} dynamicMcpConfig={dynamicMcpConfig} isStrictMcpConfig={strictMcpConfig}>
+      <FullscreenLayout scrollRef={scrollRef} overlay={toolPermissionOverlay} bottomFloat={feature('BUDDY') && companionVisible && !companionNarrow ? <CompanionFloatingBubble /> : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => {
         setCursor(null);
         jumpToNew(scrollRef.current);
       }} scrollable={<>
-              <TeammateViewHeader />
-              <Messages messages={displayedMessages} tools={tools} commands={commands} verbose={verbose} toolJSX={toolJSX} toolUseConfirmQueue={toolUseConfirmQueue} inProgressToolUseIDs={viewedTeammateTask ? viewedTeammateTask.inProgressToolUseIDs ?? new Set() : inProgressToolUseIDs} isMessageSelectorVisible={isMessageSelectorVisible} conversationId={conversationId} screen={screen} streamingToolUses={streamingToolUses} showAllInTranscript={showAllInTranscript} agentDefinitions={agentDefinitions} onOpenRateLimitOptions={handleOpenRateLimitOptions} isLoading={isLoading} streamingText={isLoading && !viewedAgentTask ? visibleStreamingText : null} isBriefOnly={viewedAgentTask ? false : isBriefOnly} unseenDivider={viewedAgentTask ? undefined : unseenDivider} scrollRef={isFullscreenEnvEnabled() ? scrollRef : undefined} trackStickyPrompt={isFullscreenEnvEnabled() ? true : undefined} cursor={cursor} setCursor={setCursor} cursorNavRef={cursorNavRef} />
-              <AwsAuthStatusBox />
-              {/* Hide the processing placeholder while a modal is showing —
+        <TeammateViewHeader />
+        <Messages messages={displayedMessages} tools={tools} commands={commands} verbose={verbose} toolJSX={toolJSX} toolUseConfirmQueue={toolUseConfirmQueue} inProgressToolUseIDs={viewedTeammateTask ? viewedTeammateTask.inProgressToolUseIDs ?? new Set() : inProgressToolUseIDs} isMessageSelectorVisible={isMessageSelectorVisible} conversationId={conversationId} screen={screen} streamingToolUses={streamingToolUses} showAllInTranscript={showAllInTranscript} agentDefinitions={agentDefinitions} onOpenRateLimitOptions={handleOpenRateLimitOptions} isLoading={isLoading} streamingText={isLoading && !viewedAgentTask ? visibleStreamingText : null} isBriefOnly={viewedAgentTask ? false : isBriefOnly} unseenDivider={viewedAgentTask ? undefined : unseenDivider} scrollRef={isFullscreenEnvEnabled() ? scrollRef : undefined} trackStickyPrompt={isFullscreenEnvEnabled() ? true : undefined} cursor={cursor} setCursor={setCursor} cursorNavRef={cursorNavRef} />
+        <AwsAuthStatusBox />
+        {/* Hide the processing placeholder while a modal is showing —
                   it would sit at the last visible transcript row right above
                   the ▔ divider, showing "❯ /config" as redundant clutter
                   (the modal IS the /config UI). Outside modals it stays so
                   the user sees their input echoed while Claude processes. */}
-              {!disabled && placeholderText && !centeredModal && <UserTextMessage param={{
+        {!disabled && placeholderText && !centeredModal && <UserTextMessage param={{
           text: placeholderText,
           type: 'text'
         }} addMargin={true} verbose={verbose} />}
-              {toolJSX && !(toolJSX.isLocalJSXCommand && toolJSX.isImmediate) && !toolJsxCentered && <Box flexDirection="column" width="100%">
-                    {toolJSX.jsx}
-                  </Box>}
-              {"external" === 'ant' && <TungstenLiveMonitor />}
-              {feature('WEB_BROWSER_TOOL') ? WebBrowserPanelModule && <WebBrowserPanelModule.WebBrowserPanel /> : null}
-              <Box flexGrow={1} />
-              {showSpinner && <SpinnerWithVerb mode={streamMode} spinnerTip={spinnerTip} responseLengthRef={responseLengthRef} apiMetricsRef={apiMetricsRef} overrideMessage={spinnerMessage} spinnerSuffix={stopHookSpinnerSuffix} verbose={verbose} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} overrideColor={spinnerColor} overrideShimmerColor={spinnerShimmerColor} hasActiveTools={inProgressToolUseIDs.size > 0} leaderIsIdle={!isLoading} />}
-              {!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && <BriefIdleStatus />}
-              {isFullscreenEnvEnabled() && <PromptInputQueuedCommands />}
-            </>} bottom={<Box flexDirection={feature('BUDDY') && companionNarrow ? 'column' : 'row'} width="100%" alignItems={feature('BUDDY') && companionNarrow ? undefined : 'flex-end'}>
-              {feature('BUDDY') && companionNarrow && isFullscreenEnvEnabled() && companionVisible ? <CompanionSprite /> : null}
-              <Box flexDirection="column" flexGrow={1}>
-                {permissionStickyFooter}
-                {/* Immediate local-jsx commands (/btw, /sandbox, /assistant,
+        {toolJSX && !(toolJSX.isLocalJSXCommand && toolJSX.isImmediate) && !toolJsxCentered && <Box flexDirection="column" width="100%">
+          {toolJSX.jsx}
+        </Box>}
+        {"external" === 'ant' && <TungstenLiveMonitor />}
+        {feature('WEB_BROWSER_TOOL') ? WebBrowserPanelModule && <WebBrowserPanelModule.WebBrowserPanel /> : null}
+        <Box flexGrow={1} />
+        {showSpinner && <SpinnerWithVerb mode={streamMode} spinnerTip={spinnerTip} responseLengthRef={responseLengthRef} apiMetricsRef={apiMetricsRef} overrideMessage={spinnerMessage} spinnerSuffix={stopHookSpinnerSuffix} verbose={verbose} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} overrideColor={spinnerColor} overrideShimmerColor={spinnerShimmerColor} hasActiveTools={inProgressToolUseIDs.size > 0} leaderIsIdle={!isLoading} />}
+        {!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && <BriefIdleStatus />}
+        {isFullscreenEnvEnabled() && <PromptInputQueuedCommands />}
+      </>} bottom={<Box flexDirection={feature('BUDDY') && companionNarrow ? 'column' : 'row'} width="100%" alignItems={feature('BUDDY') && companionNarrow ? undefined : 'flex-end'}>
+        {feature('BUDDY') && companionNarrow && isFullscreenEnvEnabled() && companionVisible ? <CompanionSprite /> : null}
+        <Box flexDirection="column" flexGrow={1}>
+          {permissionStickyFooter}
+          {/* Immediate local-jsx commands (/btw, /sandbox, /assistant,
                   /issue) render here, NOT inside scrollable. They stay mounted
                   while the main conversation streams behind them, so ScrollBox
                   relayouts on each new message would drag them around. bottom
@@ -4603,13 +4603,13 @@ export function REPL({
                   stays in scrollable: the main loop is paused so no jiggle,
                   and their tall content (DiffDetailView renders up to 400
                   lines with no internal scroll) needs the outer ScrollBox. */}
-                {toolJSX?.isLocalJSXCommand && toolJSX.isImmediate && !toolJsxCentered && <Box flexDirection="column" width="100%">
-                      {toolJSX.jsx}
-                    </Box>}
-                {!showSpinner && !toolJSX?.isLocalJSXCommand && showExpandedTodos && tasksV2 && tasksV2.length > 0 && <Box width="100%" flexDirection="column">
-                      <TaskListV2 tasks={tasksV2} isStandalone={true} />
-                    </Box>}
-                {focusedInputDialog === 'sandbox-permission' && <SandboxPermissionRequest key={sandboxPermissionRequestQueue[0]!.hostPattern.host} hostPattern={sandboxPermissionRequestQueue[0]!.hostPattern} onUserResponse={(response: {
+          {toolJSX?.isLocalJSXCommand && toolJSX.isImmediate && !toolJsxCentered && <Box flexDirection="column" width="100%">
+            {toolJSX.jsx}
+          </Box>}
+          {!showSpinner && !toolJSX?.isLocalJSXCommand && showExpandedTodos && tasksV2 && tasksV2.length > 0 && <Box width="100%" flexDirection="column">
+            <TaskListV2 tasks={tasksV2} isStandalone={true} />
+          </Box>}
+          {focusedInputDialog === 'sandbox-permission' && <SandboxPermissionRequest key={sandboxPermissionRequestQueue[0]!.hostPattern.host} hostPattern={sandboxPermissionRequestQueue[0]!.hostPattern} onUserResponse={(response: {
             allow: boolean;
             persistToSettings: boolean;
           }) => {
@@ -4658,7 +4658,7 @@ export function REPL({
               sandboxBridgeCleanupRef.current.delete(approvedHost);
             }
           }} />}
-                {focusedInputDialog === 'prompt' && <PromptDialog key={promptQueue[0]!.request.prompt} title={promptQueue[0]!.title} toolInputSummary={promptQueue[0]!.toolInputSummary} request={promptQueue[0]!.request} onRespond={selectedKey => {
+          {focusedInputDialog === 'prompt' && <PromptDialog key={promptQueue[0]!.request.prompt} title={promptQueue[0]!.title} toolInputSummary={promptQueue[0]!.toolInputSummary} request={promptQueue[0]!.request} onRespond={selectedKey => {
             const item = promptQueue[0];
             if (!item) return;
             item.resolve({
@@ -4672,12 +4672,12 @@ export function REPL({
             item.reject(new Error('Prompt cancelled by user'));
             setPromptQueue(([, ...tail]) => tail);
           }} />}
-                {/* Show pending indicator on worker while waiting for leader approval */}
-                {pendingWorkerRequest && <WorkerPendingPermission toolName={pendingWorkerRequest.toolName} description={pendingWorkerRequest.description} />}
-                {/* Show pending indicator for sandbox permission on worker side */}
-                {pendingSandboxRequest && <WorkerPendingPermission toolName="Network Access" description={`Waiting for leader to approve network access to ${pendingSandboxRequest.host}`} />}
-                {/* Worker sandbox permission requests from swarm workers */}
-                {focusedInputDialog === 'worker-sandbox-permission' && <SandboxPermissionRequest key={workerSandboxPermissions.queue[0]!.requestId} hostPattern={{
+          {/* Show pending indicator on worker while waiting for leader approval */}
+          {pendingWorkerRequest && <WorkerPendingPermission toolName={pendingWorkerRequest.toolName} description={pendingWorkerRequest.description} />}
+          {/* Show pending indicator for sandbox permission on worker side */}
+          {pendingSandboxRequest && <WorkerPendingPermission toolName="Network Access" description={`Waiting for leader to approve network access to ${pendingSandboxRequest.host}`} />}
+          {/* Worker sandbox permission requests from swarm workers */}
+          {focusedInputDialog === 'worker-sandbox-permission' && <SandboxPermissionRequest key={workerSandboxPermissions.queue[0]!.requestId} hostPattern={{
             host: workerSandboxPermissions.queue[0]!.host,
             port: undefined
           } as NetworkHostPattern} onUserResponse={(response: {
@@ -4721,7 +4721,7 @@ export function REPL({
               }
             }));
           }} />}
-                {focusedInputDialog === 'elicitation' && <ElicitationDialog key={elicitation.queue[0]!.serverName + ':' + String(elicitation.queue[0]!.requestId)} event={elicitation.queue[0]!} onResponse={(action, content) => {
+          {focusedInputDialog === 'elicitation' && <ElicitationDialog key={elicitation.queue[0]!.serverName + ':' + String(elicitation.queue[0]!.requestId)} event={elicitation.queue[0]!} onResponse={(action, content) => {
             const currentRequest = elicitation.queue[0];
             if (!currentRequest) return;
             // Call respond callback to resolve Promise
@@ -4750,7 +4750,7 @@ export function REPL({
             }));
             currentRequest?.onWaitingDismiss?.(action);
           }} />}
-                {focusedInputDialog === 'cost' && <CostThresholdDialog onDone={() => {
+          {focusedInputDialog === 'cost' && <CostThresholdDialog onDone={() => {
             setShowCostDialog(false);
             setHaveShownCostDialog(true);
             saveGlobalConfig(current => ({
@@ -4759,7 +4759,7 @@ export function REPL({
             }));
             logEvent('tengu_cost_threshold_acknowledged', {});
           }} />}
-                {focusedInputDialog === 'idle-return' && idleReturnPending && <IdleReturnDialog idleMinutes={idleReturnPending.idleMinutes} totalInputTokens={getTotalInputTokens()} onDone={async action => {
+          {focusedInputDialog === 'idle-return' && idleReturnPending && <IdleReturnDialog idleMinutes={idleReturnPending.idleMinutes} totalInputTokens={getTotalInputTokens()} onDone={async action => {
             const pending = idleReturnPending;
             setIdleReturnPending(null);
             logEvent('tengu_idle_return_action', {
@@ -4801,13 +4801,13 @@ export function REPL({
             }
             skipIdleCheckRef.current = true;
             void onSubmitRef.current(pending.input, {
-              setCursorOffset: () => {},
-              clearBuffer: () => {},
-              resetHistory: () => {}
+              setCursorOffset: () => { },
+              clearBuffer: () => { },
+              resetHistory: () => { }
             });
           }} />}
-                {focusedInputDialog === 'ide-onboarding' && <IdeOnboardingDialog onDone={() => setShowIdeOnboarding(false)} installationStatus={ideInstallationStatus} />}
-                {"external" === 'ant' && focusedInputDialog === 'model-switch' && AntModelSwitchCallout && <AntModelSwitchCallout onDone={(selection: string, modelAlias?: string) => {
+          {focusedInputDialog === 'ide-onboarding' && <IdeOnboardingDialog onDone={() => setShowIdeOnboarding(false)} installationStatus={ideInstallationStatus} />}
+          {"external" === 'ant' && focusedInputDialog === 'model-switch' && AntModelSwitchCallout && <AntModelSwitchCallout onDone={(selection: string, modelAlias?: string) => {
             setShowModelSwitchCallout(false);
             if (selection === 'switch' && modelAlias) {
               setAppState(prev => ({
@@ -4817,8 +4817,8 @@ export function REPL({
               }));
             }
           }} />}
-                {"external" === 'ant' && focusedInputDialog === 'undercover-callout' && UndercoverAutoCallout && <UndercoverAutoCallout onDone={() => setShowUndercoverCallout(false)} />}
-                {focusedInputDialog === 'effort-callout' && <EffortCallout model={mainLoopModel} onDone={selection => {
+          {"external" === 'ant' && focusedInputDialog === 'undercover-callout' && UndercoverAutoCallout && <UndercoverAutoCallout onDone={() => setShowUndercoverCallout(false)} />}
+          {focusedInputDialog === 'effort-callout' && <EffortCallout model={mainLoopModel} onDone={selection => {
             setShowEffortCallout(false);
             if (selection !== 'dismiss') {
               setAppState(prev => ({
@@ -4827,7 +4827,7 @@ export function REPL({
               }));
             }
           }} />}
-                {focusedInputDialog === 'remote-callout' && <RemoteCallout onDone={selection => {
+          {focusedInputDialog === 'remote-callout' && <RemoteCallout onDone={selection => {
             setAppState(prev => {
               if (!prev.showRemoteCallout) return prev;
               return {
@@ -4842,17 +4842,17 @@ export function REPL({
             });
           }} />}
 
-                {exitFlow}
+          {exitFlow}
 
-                {focusedInputDialog === 'plugin-hint' && hintRecommendation && <PluginHintMenu pluginName={hintRecommendation.pluginName} pluginDescription={hintRecommendation.pluginDescription} marketplaceName={hintRecommendation.marketplaceName} sourceCommand={hintRecommendation.sourceCommand} onResponse={handleHintResponse} />}
+          {focusedInputDialog === 'plugin-hint' && hintRecommendation && <PluginHintMenu pluginName={hintRecommendation.pluginName} pluginDescription={hintRecommendation.pluginDescription} marketplaceName={hintRecommendation.marketplaceName} sourceCommand={hintRecommendation.sourceCommand} onResponse={handleHintResponse} />}
 
-                {focusedInputDialog === 'lsp-recommendation' && lspRecommendation && <LspRecommendationMenu pluginName={lspRecommendation.pluginName} pluginDescription={lspRecommendation.pluginDescription} fileExtension={lspRecommendation.fileExtension} onResponse={handleLspResponse} />}
+          {focusedInputDialog === 'lsp-recommendation' && lspRecommendation && <LspRecommendationMenu pluginName={lspRecommendation.pluginName} pluginDescription={lspRecommendation.pluginDescription} fileExtension={lspRecommendation.fileExtension} onResponse={handleLspResponse} />}
 
-                {focusedInputDialog === 'desktop-upsell' && <DesktopUpsellStartup onDone={() => setShowDesktopUpsellStartup(false)} />}
+          {focusedInputDialog === 'desktop-upsell' && <DesktopUpsellStartup onDone={() => setShowDesktopUpsellStartup(false)} />}
 
-                {feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-choice' && ultraplanPendingChoice && <UltraplanChoiceDialog plan={ultraplanPendingChoice.plan} sessionId={ultraplanPendingChoice.sessionId} taskId={ultraplanPendingChoice.taskId} setMessages={setMessages} readFileState={readFileState.current} getAppState={() => store.getState()} setConversationId={setConversationId} /> : null}
+          {feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-choice' && ultraplanPendingChoice && <UltraplanChoiceDialog plan={ultraplanPendingChoice.plan} sessionId={ultraplanPendingChoice.sessionId} taskId={ultraplanPendingChoice.taskId} setMessages={setMessages} readFileState={readFileState.current} getAppState={() => store.getState()} setConversationId={setConversationId} /> : null}
 
-                {feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-launch' && ultraplanLaunchPending && <UltraplanLaunchDialog onChoice={(choice, opts) => {
+          {feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-launch' && ultraplanLaunchPending && <UltraplanLaunchDialog onChoice={(choice, opts) => {
             const blurb = ultraplanLaunchPending.blurb;
             setAppState(prev => prev.ultraplanLaunchPending ? {
               ...prev,
@@ -4892,26 +4892,26 @@ export function REPL({
             }).then(appendStdout).catch(logError);
           }} /> : null}
 
-                {mrRender()}
-
-                {!toolJSX?.shouldHidePromptInput && !focusedInputDialog && !isExiting && !disabled && !cursor && <>
-                      {autoRunIssueReason && <AutoRunIssueNotification onRun={handleAutoRunIssue} onCancel={handleCancelAutoRunIssue} reason={getAutoRunIssueReasonText(autoRunIssueReason)} />}
-                      {postCompactSurvey.state !== 'closed' ? <FeedbackSurvey state={postCompactSurvey.state} lastResponse={postCompactSurvey.lastResponse} handleSelect={postCompactSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} /> : memorySurvey.state !== 'closed' ? <FeedbackSurvey state={memorySurvey.state} lastResponse={memorySurvey.lastResponse} handleSelect={memorySurvey.handleSelect} handleTranscriptSelect={memorySurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} message="How well did Claude use its memory? (optional)" /> : <FeedbackSurvey state={feedbackSurvey.state} lastResponse={feedbackSurvey.lastResponse} handleSelect={feedbackSurvey.handleSelect} handleTranscriptSelect={feedbackSurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={didAutoRunIssueRef.current ? undefined : handleSurveyRequestFeedback} />}
-                      {/* Frustration-triggered transcript sharing prompt */}
-                      {frustrationDetection.state !== 'closed' && <FeedbackSurvey state={frustrationDetection.state} lastResponse={null} handleSelect={() => {}} handleTranscriptSelect={frustrationDetection.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />}
-                      {/* Skill improvement survey - appears when improvements detected (ant-only) */}
-                      {"external" === 'ant' && skillImprovementSurvey.suggestion && <SkillImprovementSurvey isOpen={skillImprovementSurvey.isOpen} skillName={skillImprovementSurvey.suggestion.skillName} updates={skillImprovementSurvey.suggestion.updates} handleSelect={skillImprovementSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} />}
-                      {showIssueFlagBanner && <IssueFlagBanner />}
-                      {}
-                      <PromptInput debug={debug} ideSelection={ideSelection} hasSuppressedDialogs={!!hasSuppressedDialogs} isLocalJSXCommandActive={isShowingLocalJSXCommand} getToolUseContext={getToolUseContext} toolPermissionContext={toolPermissionContext} setToolPermissionContext={setToolPermissionContext} apiKeyStatus={apiKeyStatus} commands={commands} agents={agentDefinitions.activeAgents} isLoading={isLoading} onExit={handleExit} verbose={verbose} messages={messages} onAutoUpdaterResult={setAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} input={inputValue} onInputChange={setInputValue} mode={inputMode} onModeChange={setInputMode} stashedPrompt={stashedPrompt} setStashedPrompt={setStashedPrompt} submitCount={submitCount} onShowMessageSelector={handleShowMessageSelector} onMessageActionsEnter={
-            // Works during isLoading — edit cancels first; uuid selection survives appends.
-            feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? enterMessageActions : undefined} mcpClients={mcpClients} pastedContents={pastedContents} setPastedContents={setPastedContents} vimMode={vimMode} setVimMode={setVimMode} showBashesDialog={showBashesDialog} setShowBashesDialog={setShowBashesDialog} onSubmit={onSubmit} onAgentSubmit={onAgentSubmit} isSearchingHistory={isSearchingHistory} setIsSearchingHistory={setIsSearchingHistory} helpOpen={isHelpOpen} setHelpOpen={setIsHelpOpen} insertTextRef={feature('VOICE_MODE') ? insertTextRef : undefined} voiceInterimRange={voice.interimRange} />
-                      <SessionBackgroundHint onBackgroundSession={handleBackgroundSession} isLoading={isLoading} />
-                    </>}
-                {cursor &&
-          // inputValue is REPL state; typed text survives the round-trip.
-          <MessageActionsBar cursor={cursor} />}
-                {focusedInputDialog === 'message-selector' && <MessageSelector messages={messages} preselectedMessage={messageSelectorPreselect} onPreRestore={onCancel} onRestoreCode={async (message: UserMessage) => {
+          {mrRender()}
+
+          {!toolJSX?.shouldHidePromptInput && !focusedInputDialog && !isExiting && !disabled && !cursor && <>
+            {autoRunIssueReason && <AutoRunIssueNotification onRun={handleAutoRunIssue} onCancel={handleCancelAutoRunIssue} reason={getAutoRunIssueReasonText(autoRunIssueReason)} />}
+            {postCompactSurvey.state !== 'closed' ? <FeedbackSurvey state={postCompactSurvey.state} lastResponse={postCompactSurvey.lastResponse} handleSelect={postCompactSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} /> : memorySurvey.state !== 'closed' ? <FeedbackSurvey state={memorySurvey.state} lastResponse={memorySurvey.lastResponse} handleSelect={memorySurvey.handleSelect} handleTranscriptSelect={memorySurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} message="How well did Claude use its memory? (optional)" /> : <FeedbackSurvey state={feedbackSurvey.state} lastResponse={feedbackSurvey.lastResponse} handleSelect={feedbackSurvey.handleSelect} handleTranscriptSelect={feedbackSurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={didAutoRunIssueRef.current ? undefined : handleSurveyRequestFeedback} />}
+            {/* Frustration-triggered transcript sharing prompt */}
+            {frustrationDetection.state !== 'closed' && <FeedbackSurvey state={frustrationDetection.state} lastResponse={null} handleSelect={() => { }} handleTranscriptSelect={frustrationDetection.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />}
+            {/* Skill improvement survey - appears when improvements detected (ant-only) */}
+            {"external" === 'ant' && skillImprovementSurvey.suggestion && <SkillImprovementSurvey isOpen={skillImprovementSurvey.isOpen} skillName={skillImprovementSurvey.suggestion.skillName} updates={skillImprovementSurvey.suggestion.updates} handleSelect={skillImprovementSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} />}
+            {showIssueFlagBanner && <IssueFlagBanner />}
+            { }
+            <PromptInput debug={debug} ideSelection={ideSelection} hasSuppressedDialogs={!!hasSuppressedDialogs} isLocalJSXCommandActive={isShowingLocalJSXCommand} getToolUseContext={getToolUseContext} toolPermissionContext={toolPermissionContext} setToolPermissionContext={setToolPermissionContext} apiKeyStatus={apiKeyStatus} commands={commands} agents={agentDefinitions.activeAgents} isLoading={isLoading} onExit={handleExit} verbose={verbose} messages={messages} onAutoUpdaterResult={setAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} input={inputValue} onInputChange={setInputValue} mode={inputMode} onModeChange={setInputMode} stashedPrompt={stashedPrompt} setStashedPrompt={setStashedPrompt} submitCount={submitCount} onShowMessageSelector={handleShowMessageSelector} onMessageActionsEnter={
+              // Works during isLoading — edit cancels first; uuid selection survives appends.
+              feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? enterMessageActions : undefined} mcpClients={mcpClients} pastedContents={pastedContents} setPastedContents={setPastedContents} vimMode={vimMode} setVimMode={setVimMode} showBashesDialog={showBashesDialog} setShowBashesDialog={setShowBashesDialog} onSubmit={onSubmit} onAgentSubmit={onAgentSubmit} isSearchingHistory={isSearchingHistory} setIsSearchingHistory={setIsSearchingHistory} helpOpen={isHelpOpen} setHelpOpen={setIsHelpOpen} insertTextRef={feature('VOICE_MODE') ? insertTextRef : undefined} voiceInterimRange={voice.interimRange} />
+            <SessionBackgroundHint onBackgroundSession={handleBackgroundSession} isLoading={isLoading} />
+          </>}
+          {cursor &&
+            // inputValue is REPL state; typed text survives the round-trip.
+            <MessageActionsBar cursor={cursor} />}
+          {focusedInputDialog === 'message-selector' && <MessageSelector messages={messages} preselectedMessage={messageSelectorPreselect} onPreRestore={onCancel} onRestoreCode={async (message: UserMessage) => {
             await fileHistoryRewind((updater: (prev: FileHistoryState) => FileHistoryState) => {
               setAppState(prev => ({
                 ...prev,
@@ -4993,16 +4993,16 @@ export function REPL({
             setIsMessageSelectorVisible(false);
             setMessageSelectorPreselect(undefined);
           }} />}
-                {"external" === 'ant' && <DevBar />}
-              </Box>
-              {feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? <CompanionSprite /> : null}
-            </Box>} />
-      </MCPConnectionManager>
-    </KeybindingSetup>;
+          {"external" === 'ant' && <DevBar />}
+        </Box>
+        {feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? <CompanionSprite /> : null}
+      </Box>} />
+    </MCPConnectionManager>
+  </KeybindingSetup>;
   if (isFullscreenEnvEnabled()) {
     return <AlternateScreen mouseTracking={isMouseTrackingEnabled()}>
-        {mainReturn}
-      </AlternateScreen>;
+      {mainReturn}
+    </AlternateScreen>;
   }
   return mainReturn;
 }

+ 4 - 4
src/utils/theme.ts

@@ -117,7 +117,7 @@ const lightTheme: Theme = {
   autoAccept: 'rgb(135,0,255)', // Electric violet
   bashBorder: 'rgb(255,0,135)', // Vibrant pink
   claude: 'rgb(215,119,87)', // Claude orange
-  startupAccent: 'rgb(124,176,133)', // Free Code pastel green
+  startupAccent: 'rgb(124,176,133)', // Claude Code CN pastel green
   claudeShimmer: 'rgb(245,149,117)', // Lighter claude orange for shimmer effect
   claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(87,105,247)', // Medium blue for system spinner
   claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(117,135,255)', // Lighter blue for system spinner shimmer
@@ -364,7 +364,7 @@ const lightDaltonizedTheme: Theme = {
   autoAccept: 'rgb(135,0,255)', // Electric violet
   bashBorder: 'rgb(0,102,204)', // Blue instead of pink
   claude: 'rgb(255,153,51)', // Orange adjusted for deuteranopia
-  startupAccent: 'rgb(124,176,133)', // Free Code pastel green
+  startupAccent: 'rgb(124,176,133)', // Claude Code CN pastel green
   claudeShimmer: 'rgb(255,183,101)', // Lighter orange for shimmer effect
   claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(51,102,255)', // Bright blue for system spinner
   claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(101,152,255)', // Lighter bright blue for system spinner shimmer
@@ -446,7 +446,7 @@ const darkTheme: Theme = {
   autoAccept: 'rgb(175,135,255)', // Electric violet
   bashBorder: 'rgb(253,93,177)', // Bright pink
   claude: 'rgb(215,119,87)', // Claude orange
-  startupAccent: 'rgb(184,225,174)', // Free Code pastel green
+  startupAccent: 'rgb(184,225,174)', // Claude Code CN pastel green
   claudeShimmer: 'rgb(235,159,127)', // Lighter claude orange for shimmer effect
   claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(147,165,255)', // Blue for system spinner
   claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(177,195,255)', // Lighter blue for system spinner shimmer
@@ -528,7 +528,7 @@ const darkDaltonizedTheme: Theme = {
   autoAccept: 'rgb(175,135,255)', // Electric violet
   bashBorder: 'rgb(51,153,255)', // Bright blue
   claude: 'rgb(255,153,51)', // Orange adjusted for deuteranopia
-  startupAccent: 'rgb(184,225,174)', // Free Code pastel green
+  startupAccent: 'rgb(184,225,174)', // Claude Code CN pastel green
   claudeShimmer: 'rgb(255,183,101)', // Lighter orange for shimmer effect
   claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(153,204,255)', // Light blue for system spinner
   claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(183,224,255)', // Lighter blue for system spinner shimmer