diff --git a/DEV-LOG.md b/DEV-LOG.md new file mode 100644 index 000000000..f8e4fe569 --- /dev/null +++ b/DEV-LOG.md @@ -0,0 +1,12 @@ +# DEV-LOG + +## USER_TYPE=ant TUI 修复 (2026-04-02) + +`global.d.ts` 声明的全局函数在反编译版本运行时未定义,导致 `USER_TYPE=ant` 时 TUI 崩溃。 + +修复方式:显式 import / 本地 stub / 全局 stub / 新建 stub 文件。涉及文件: +`cli.tsx`, `model.ts`, `context.ts`, `effort.ts`, `thinking.ts`, `undercover.ts`, `Spinner.tsx`, `AntModelSwitchCallout.tsx`(新建), `UndercoverAutoCallout.tsx`(新建) + +注意: +- `USER_TYPE=ant` 启用 alt-screen 全屏模式,中心区域满屏是预期行为 +- `global.d.ts` 中剩余未 stub 的全局函数(`getAntModels` 等)遇到 `X is not defined` 时按同样模式处理 diff --git a/src/components/AntModelSwitchCallout.tsx b/src/components/AntModelSwitchCallout.tsx new file mode 100644 index 000000000..96b9b7bc4 --- /dev/null +++ b/src/components/AntModelSwitchCallout.tsx @@ -0,0 +1,12 @@ +// Stub — ant-only component, not available in decompiled build +import React from 'react'; + +export function AntModelSwitchCallout(_props: { + onDone: (selection: string, modelAlias?: string) => void; +}): React.ReactElement | null { + return null; +} + +export function shouldShowModelSwitchCallout(): boolean { + return false; +} diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index 9ac9ffe99..502f97a5a 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -1,4 +1,4 @@ -import { c as _c } from "react/compiler-runtime"; +import { c as _c } from 'react/compiler-runtime'; // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered import { Box, Text } from '../ink.js'; import * as React from 'react'; @@ -46,7 +46,15 @@ type Props = { pauseStartTimeRef: React.RefObject; spinnerTip?: string; responseLengthRef: React.RefObject; - apiMetricsRef?: React.RefObject>; + apiMetricsRef?: React.RefObject< + Array<{ + ttftMs: number; + firstTokenTime: number; + lastTokenTime: number; + responseLengthBaseline: number; + endResponseLength: number; + }> + >; overrideColor?: keyof Theme | null; overrideShimmerColor?: keyof Theme | null; overrideMessage?: string | null; @@ -57,6 +65,9 @@ type Props = { leaderIsIdle?: boolean; }; +// Polyfill ant-only global functions that are normally injected by the bundler. +const computeTtftText = (metrics: ApiMetricEntry[]): string => ''; + // Thin wrapper: branches on isBriefOnly so the two variants have independent // hook call chains. Without this split, toggling /brief mid-render would // violate Rules of Hooks (the inner variant calls ~10 more hooks). @@ -68,14 +79,22 @@ export function SpinnerWithVerb(props: Props): React.ReactNode { // teammate view needs the real spinner (which shows teammate status). const viewingAgentTaskId = useAppState(s_0 => s_0.viewingAgentTaskId); // Hoisted to mount-time — this component re-renders at animation framerate. - const briefEnvEnabled = feature('KAIROS') || feature('KAIROS_BRIEF') ? - // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant - useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_BRIEF), []) : false; + const briefEnvEnabled = + feature('KAIROS') || feature('KAIROS_BRIEF') + ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant + useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_BRIEF), []) + : false; // Runtime gate mirrors isBriefEnabled() but inlined — importing from // BriefTool.ts would leak tool-name strings into external builds. Single // spinner instance → hooks stay unconditional (two subs, negligible). - if ((feature('KAIROS') || feature('KAIROS_BRIEF')) && (getKairosActive() || getUserMsgOptIn() && (briefEnvEnabled || getFeatureValue_CACHED_MAY_BE_STALE('tengu_kairos_brief', false))) && isBriefOnly && !viewingAgentTaskId) { + if ( + (feature('KAIROS') || feature('KAIROS_BRIEF')) && + (getKairosActive() || + (getUserMsgOptIn() && (briefEnvEnabled || getFeatureValue_CACHED_MAY_BE_STALE('tengu_kairos_brief', false)))) && + isBriefOnly && + !viewingAgentTaskId + ) { return ; } return ; @@ -87,13 +106,14 @@ function SpinnerWithVerbInner({ pauseStartTimeRef, spinnerTip, responseLengthRef, + apiMetricsRef, overrideColor, overrideShimmerColor, overrideMessage, spinnerSuffix, verbose, hasActiveTools = false, - leaderIsIdle = false + leaderIsIdle = false, }: Props): React.ReactNode { const settings = useSettings(); const reducedMotion = settings.prefersReducedMotion ?? false; @@ -112,13 +132,13 @@ function SpinnerWithVerbInner({ const selectedIPAgentIndex = useAppState(s_2 => s_2.selectedIPAgentIndex); const viewSelectionMode = useAppState(s_3 => s_3.viewSelectionMode); // Get foregrounded teammate (if viewing a teammate's transcript) - const foregroundedTeammate = viewingAgentTaskId ? getViewedTeammateTask({ - viewingAgentTaskId, - tasks - }) : undefined; - const { - columns - } = useTerminalSize(); + const foregroundedTeammate = viewingAgentTaskId + ? getViewedTeammateTask({ + viewingAgentTaskId, + tasks, + }) + : undefined; + const { columns } = useTerminalSize(); const tasksV2 = useTasksV2(); // Track thinking status: 'thinking' | number (duration in ms) | null @@ -168,7 +188,10 @@ function SpinnerWithVerbInner({ // Leader's own verb (always the leader's, regardless of who is foregrounded) const leaderVerb = overrideMessage ?? currentTodo?.activeForm ?? currentTodo?.subject ?? randomVerb; - const effectiveVerb = foregroundedTeammate && !foregroundedTeammate.isIdle ? foregroundedTeammate.spinnerVerb ?? randomVerb : leaderVerb; + const effectiveVerb = + foregroundedTeammate && !foregroundedTeammate.isIdle + ? (foregroundedTeammate.spinnerVerb ?? randomVerb) + : leaderVerb; const message = effectiveVerb + '…'; // Track CLI activity when spinner is active @@ -203,7 +226,10 @@ function SpinnerWithVerbInner({ // Stale read of the refs for showBtwTip below — we're off the 50ms clock // so this only updates when props/app state change, which is sufficient for // a coarse 30s threshold. - const elapsedSnapshot = pauseStartTimeRef.current !== null ? pauseStartTimeRef.current - loadingStartTimeRef.current - totalPausedMsRef.current : Date.now() - loadingStartTimeRef.current - totalPausedMsRef.current; + const elapsedSnapshot = + pauseStartTimeRef.current !== null + ? pauseStartTimeRef.current - loadingStartTimeRef.current - totalPausedMsRef.current + : Date.now() - loadingStartTimeRef.current - totalPausedMsRef.current; // Leader token count for TeammateSpinnerTree — read raw (non-animated) from // the ref. The tree is only shown when teammates are running; teammate @@ -220,7 +246,7 @@ function SpinnerWithVerbInner({ // doesn't trigger re-renders; we pick up updates on the parent's ~25x/turn // re-render cadence, same as the old ApiMetricsLine did. let ttftText: string | null = null; - if ((process.env.USER_TYPE) === 'ant' && apiMetricsRef?.current && apiMetricsRef.current.length > 0) { + if (process.env.USER_TYPE === 'ant' && apiMetricsRef?.current && apiMetricsRef.current.length > 0) { ttftText = computeTtftText(apiMetricsRef.current); } @@ -228,26 +254,49 @@ function SpinnerWithVerbInner({ // show a static dim idle display instead of the animated spinner — otherwise // useStalledAnimation detects no new tokens after 3s and turns the spinner red. if (leaderIsIdle && hasRunningTeammates && !foregroundedTeammate) { - return + return ( + {TEARDROP_ASTERISK} Idle {!allIdle && ' · teammates running'} - {showSpinnerTree && } - ; + {showSpinnerTree && ( + + )} + + ); } // When viewing an idle teammate, show static idle display instead of animated spinner if (foregroundedTeammate?.isIdle) { - const idleText = allIdle ? `${TEARDROP_ASTERISK} Worked for ${formatDuration(Date.now() - foregroundedTeammate.startTime)}` : `${TEARDROP_ASTERISK} Idle`; - return + const idleText = allIdle + ? `${TEARDROP_ASTERISK} Worked for ${formatDuration(Date.now() - foregroundedTeammate.startTime)}` + : `${TEARDROP_ASTERISK} Idle`; + return ( + {idleText} - {showSpinnerTree && hasRunningTeammates && } - ; + {showSpinnerTree && hasRunningTeammates && ( + + )} + + ); } // Time-based tip overrides: coarse thresholds so a stale ref read (we're @@ -257,7 +306,13 @@ function SpinnerWithVerbInner({ const tipsEnabled = settings.spinnerTipsEnabled !== false; const showClearTip = tipsEnabled && elapsedSnapshot > 1_800_000; const showBtwTip = tipsEnabled && elapsedSnapshot > 30_000 && !getGlobalConfig().btwUseCount; - const effectiveTip = contextTipsActive ? undefined : showClearTip && !nextTask ? 'Use /clear to start fresh when switching topics and free up context' : showBtwTip && !nextTask ? "Use /btw to ask a quick side question without interrupting Claude's current work" : spinnerTip; + const effectiveTip = contextTipsActive + ? undefined + : showClearTip && !nextTask + ? 'Use /clear to start fresh when switching topics and free up context' + : showBtwTip && !nextTask + ? "Use /btw to ask a quick side question without interrupting Claude's current work" + : spinnerTip; // Budget text (ant-only) — shown above the tip line let budgetText: string | null = null; @@ -268,37 +323,77 @@ function SpinnerWithVerbInner({ if (tokens >= budget) { budgetText = `Target: ${formatNumber(tokens)} used (${formatNumber(budget)} min ${figures.tick})`; } else { - const pct = Math.round(tokens / budget * 100); + const pct = Math.round((tokens / budget) * 100); const remaining = budget - tokens; const rate = elapsedSnapshot > 5000 && tokens >= 2000 ? tokens / elapsedSnapshot : 0; - const eta = rate > 0 ? ` \u00B7 ~${formatDuration(remaining / rate, { - mostSignificantOnly: true - })}` : ''; + const eta = + rate > 0 + ? ` \u00B7 ~${formatDuration(remaining / rate, { + mostSignificantOnly: true, + })}` + : ''; budgetText = `Target: ${formatNumber(tokens)} / ${formatNumber(budget)} (${pct}%)${eta}`; } } } - return - - {showSpinnerTree && hasRunningTeammates ? : showExpandedTodos && tasksV2 && tasksV2.length > 0 ? + return ( + + + {showSpinnerTree && hasRunningTeammates ? ( + + ) : showExpandedTodos && tasksV2 && tasksV2.length > 0 ? ( + - : nextTask || effectiveTip || budgetText ? - // IMPORTANT: we need this width="100%" to avoid an Ink bug where the - // tip gets duplicated over and over while the spinner is running if - // the terminal is very small. TODO: fix this in Ink. - - {budgetText && + + ) : nextTask || effectiveTip || budgetText ? ( + // IMPORTANT: we need this width="100%" to avoid an Ink bug where the + // tip gets duplicated over and over while the spinner is running if + // the terminal is very small. TODO: fix this in Ink. + + {budgetText && ( + {budgetText} - } - {(nextTask || effectiveTip) && - - {nextTask ? `Next: ${nextTask.subject}` : `Tip: ${effectiveTip}`} - - } - : null} - ; + + )} + {(nextTask || effectiveTip) && ( + + {nextTask ? `Next: ${nextTask.subject}` : `Tip: ${effectiveTip}`} + + )} + + ) : null} + + ); } // Brief/assistant mode spinner: single status line. PromptInput drops its @@ -316,10 +411,7 @@ type BriefSpinnerProps = { }; function BriefSpinner(t0) { const $ = _c(31); - const { - mode, - overrideMessage - } = t0; + const { mode, overrideMessage } = t0; const settings = useSettings(); const reducedMotion = settings.prefersReducedMotion ?? false; const [randomVerb] = useState(_temp4); @@ -329,7 +421,7 @@ function BriefSpinner(t0) { let t2; if ($[0] !== mode) { t1 = () => { - const operationId = "spinner-" + mode; + const operationId = 'spinner-' + mode; activityManager.startCLIActivity(operationId); return () => { activityManager.endCLIActivity(operationId); @@ -346,12 +438,12 @@ function BriefSpinner(t0) { useEffect(t1, t2); const [, time] = useAnimationFrame(reducedMotion ? null : 120); const runningCount = useAppState(_temp6); - const showConnWarning = connStatus === "reconnecting" || connStatus === "disconnected"; - const connText = connStatus === "reconnecting" ? "Reconnecting" : "Disconnected"; + const showConnWarning = connStatus === 'reconnecting' || connStatus === 'disconnected'; + const connText = connStatus === 'reconnecting' ? 'Reconnecting' : 'Disconnected'; const dotFrame = Math.floor(time / 300) % 3; let t3; if ($[3] !== dotFrame || $[4] !== reducedMotion) { - t3 = reducedMotion ? "\u2026 " : ".".repeat(dotFrame + 1).padEnd(3); + t3 = reducedMotion ? '\u2026 ' : '.'.repeat(dotFrame + 1).padEnd(3); $[3] = dotFrame; $[4] = reducedMotion; $[5] = t3; @@ -370,7 +462,8 @@ function BriefSpinner(t0) { const verbWidth = t4; let t5; if ($[8] !== reducedMotion || $[9] !== showConnWarning || $[10] !== time || $[11] !== verb || $[12] !== verbWidth) { - const glimmerIndex = reducedMotion || showConnWarning ? -100 : computeGlimmerIndex(Math.floor(time / SHIMMER_INTERVAL_MS), verbWidth); + const glimmerIndex = + reducedMotion || showConnWarning ? -100 : computeGlimmerIndex(Math.floor(time / SHIMMER_INTERVAL_MS), verbWidth); t5 = computeShimmerSegments(verb, glimmerIndex); $[8] = reducedMotion; $[9] = showConnWarning; @@ -381,15 +474,9 @@ function BriefSpinner(t0) { } else { t5 = $[13]; } - const { - before, - shimmer, - after - } = t5; - const { - columns - } = useTerminalSize(); - const rightText = runningCount > 0 ? `${runningCount} in background` : ""; + const { before, shimmer, after } = t5; + const { columns } = useTerminalSize(); + const rightText = runningCount > 0 ? `${runningCount} in background` : ''; let t6; if ($[14] !== connText || $[15] !== showConnWarning || $[16] !== verbWidth) { t6 = showConnWarning ? stringWidth(connText) : verbWidth; @@ -403,8 +490,24 @@ function BriefSpinner(t0) { const leftWidth = t6 + 3; const pad = Math.max(1, columns - 2 - leftWidth - stringWidth(rightText)); let t7; - if ($[18] !== after || $[19] !== before || $[20] !== connText || $[21] !== dots || $[22] !== shimmer || $[23] !== showConnWarning) { - t7 = showConnWarning ? {connText + dots} : <>{before ? {before} : null}{shimmer ? {shimmer} : null}{after ? {after} : null}{dots}; + if ( + $[18] !== after || + $[19] !== before || + $[20] !== connText || + $[21] !== dots || + $[22] !== shimmer || + $[23] !== showConnWarning + ) { + t7 = showConnWarning ? ( + {connText + dots} + ) : ( + <> + {before ? {before} : null} + {shimmer ? {shimmer} : null} + {after ? {after} : null} + {dots} + + ); $[18] = after; $[19] = before; $[20] = connText; @@ -417,7 +520,12 @@ function BriefSpinner(t0) { } let t8; if ($[25] !== pad || $[26] !== rightText) { - t8 = rightText ? <>{" ".repeat(pad)}{rightText} : null; + t8 = rightText ? ( + <> + {' '.repeat(pad)} + {rightText} + + ) : null; $[25] = pad; $[26] = rightText; $[27] = t8; @@ -426,7 +534,12 @@ function BriefSpinner(t0) { } let t9; if ($[28] !== t7 || $[29] !== t8) { - t9 = {t7}{t8}; + t9 = ( + + {t7} + {t8} + + ); $[28] = t7; $[29] = t8; $[30] = t9; @@ -447,22 +560,20 @@ function _temp5(s) { return s.remoteConnectionStatus; } function _temp4() { - return sample(getSpinnerVerbs()) ?? "Working"; + return sample(getSpinnerVerbs()) ?? 'Working'; } export function BriefIdleStatus() { const $ = _c(9); const connStatus = useAppState(_temp7); const runningCount = useAppState(_temp8); - const { - columns - } = useTerminalSize(); - const showConnWarning = connStatus === "reconnecting" || connStatus === "disconnected"; - const connText = connStatus === "reconnecting" ? "Reconnecting\u2026" : "Disconnected"; - const leftText = showConnWarning ? connText : ""; - const rightText = runningCount > 0 ? `${runningCount} in background` : ""; + const { columns } = useTerminalSize(); + const showConnWarning = connStatus === 'reconnecting' || connStatus === 'disconnected'; + const connText = connStatus === 'reconnecting' ? 'Reconnecting\u2026' : 'Disconnected'; + const leftText = showConnWarning ? connText : ''; + const rightText = runningCount > 0 ? `${runningCount} in background` : ''; if (!leftText && !rightText) { let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + if ($[0] === Symbol.for('react.memo_cache_sentinel')) { t0 = ; $[0] = t0; } else { @@ -481,7 +592,12 @@ export function BriefIdleStatus() { } let t1; if ($[3] !== pad || $[4] !== rightText) { - t1 = rightText ? <>{" ".repeat(pad)}{rightText} : null; + t1 = rightText ? ( + <> + {' '.repeat(pad)} + {rightText} + + ) : null; $[3] = pad; $[4] = rightText; $[5] = t1; @@ -490,7 +606,14 @@ export function BriefIdleStatus() { } let t2; if ($[6] !== t0 || $[7] !== t1) { - t2 = {t0}{t1}; + t2 = ( + + + {t0} + {t1} + + + ); $[6] = t0; $[7] = t1; $[8] = t2; @@ -512,7 +635,7 @@ export function Spinner() { const [ref, time] = useAnimationFrame(reducedMotion ? null : 120); if (reducedMotion) { let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + if ($[0] === Symbol.for('react.memo_cache_sentinel')) { t0 = ; $[0] = t0; } else { @@ -520,7 +643,11 @@ export function Spinner() { } let t1; if ($[1] !== ref) { - t1 = {t0}; + t1 = ( + + {t0} + + ); $[1] = ref; $[2] = t1; } else { @@ -540,7 +667,11 @@ export function Spinner() { } let t2; if ($[5] !== ref || $[6] !== t1) { - t2 = {t1}; + t2 = ( + + {t1} + + ); $[5] = ref; $[6] = t1; $[7] = t2; diff --git a/src/components/UndercoverAutoCallout.tsx b/src/components/UndercoverAutoCallout.tsx new file mode 100644 index 000000000..29d071c6c --- /dev/null +++ b/src/components/UndercoverAutoCallout.tsx @@ -0,0 +1,9 @@ +// Stub — ant-only component, not available in decompiled build +import React, { useEffect } from 'react'; + +export function UndercoverAutoCallout({ onDone }: { onDone: () => void }): React.ReactElement | null { + useEffect(() => { + onDone(); + }, [onDone]); + return null; +} diff --git a/src/entrypoints/cli.tsx b/src/entrypoints/cli.tsx index 0ee6ff3a1..fa377cbc3 100644 --- a/src/entrypoints/cli.tsx +++ b/src/entrypoints/cli.tsx @@ -1,19 +1,17 @@ #!/usr/bin/env bun -import { feature } from 'bun:bundle' +import { feature } from 'bun:bundle'; // Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons // eslint-disable-next-line custom-rules/no-top-level-side-effects -process.env.COREPACK_ENABLE_AUTO_PIN = "0"; +process.env.COREPACK_ENABLE_AUTO_PIN = '0'; // Set max heap size for child processes in CCR environments (containers have 16GB) // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level, custom-rules/safe-env-boolean-check -if (process.env.CLAUDE_CODE_REMOTE === "true") { - // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level - const existing = process.env.NODE_OPTIONS || ""; - // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level - process.env.NODE_OPTIONS = existing - ? `${existing} --max-old-space-size=8192` - : "--max-old-space-size=8192"; +if (process.env.CLAUDE_CODE_REMOTE === 'true') { + // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level + const existing = process.env.NODE_OPTIONS || ''; + // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level + process.env.NODE_OPTIONS = existing ? `${existing} --max-old-space-size=8192` : '--max-old-space-size=8192'; } // Harness-science L0 ablation baseline. Inlined here (not init.ts) because @@ -21,19 +19,19 @@ if (process.env.CLAUDE_CODE_REMOTE === "true") { // module-level consts at import time — init() runs too late. feature() gate // DCEs this entire block from external builds. // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level -if (feature("ABLATION_BASELINE") && process.env.CLAUDE_CODE_ABLATION_BASELINE) { - for (const k of [ - "CLAUDE_CODE_SIMPLE", - "CLAUDE_CODE_DISABLE_THINKING", - "DISABLE_INTERLEAVED_THINKING", - "DISABLE_COMPACT", - "DISABLE_AUTO_COMPACT", - "CLAUDE_CODE_DISABLE_AUTO_MEMORY", - "CLAUDE_CODE_DISABLE_BACKGROUND_TASKS", - ]) { - // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level - process.env[k] ??= "1"; - } +if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) { + for (const k of [ + 'CLAUDE_CODE_SIMPLE', + 'CLAUDE_CODE_DISABLE_THINKING', + 'DISABLE_INTERLEAVED_THINKING', + 'DISABLE_COMPACT', + 'DISABLE_AUTO_COMPACT', + 'CLAUDE_CODE_DISABLE_AUTO_MEMORY', + 'CLAUDE_CODE_DISABLE_BACKGROUND_TASKS', + ]) { + // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level + process.env[k] ??= '1'; + } } /** @@ -42,262 +40,231 @@ if (feature("ABLATION_BASELINE") && process.env.CLAUDE_CODE_ABLATION_BASELINE) { * Fast-path for --version has zero imports beyond this file. */ async function main(): Promise { - const args = process.argv.slice(2); + const args = process.argv.slice(2); - // Fast-path for --version/-v: zero module loading needed - if ( - args.length === 1 && - (args[0] === "--version" || args[0] === "-v" || args[0] === "-V") - ) { - // MACRO.VERSION is inlined at build time - // biome-ignore lint/suspicious/noConsole:: intentional console output - console.log(`${MACRO.VERSION} (Claude Code)`); + // Fast-path for --version/-v: zero module loading needed + if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) { + // MACRO.VERSION is inlined at build time + // biome-ignore lint/suspicious/noConsole:: intentional console output + console.log(`${MACRO.VERSION} (Claude Code)`); + return; + } + + // For all other paths, load the startup profiler + const { profileCheckpoint } = await import('../utils/startupProfiler.js'); + profileCheckpoint('cli_entry'); + + // Fast-path for --dump-system-prompt: output the rendered system prompt and exit. + // Used by prompt sensitivity evals to extract the system prompt at a specific commit. + // Ant-only: eliminated from external builds via feature flag. + if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') { + profileCheckpoint('cli_dump_system_prompt_path'); + const { enableConfigs } = await import('../utils/config.js'); + enableConfigs(); + const { getMainLoopModel } = await import('../utils/model/model.js'); + const modelIdx = args.indexOf('--model'); + const model = (modelIdx !== -1 && args[modelIdx + 1]) || getMainLoopModel(); + const { getSystemPrompt } = await import('../constants/prompts.js'); + const prompt = await getSystemPrompt([], model); + // biome-ignore lint/suspicious/noConsole:: intentional console output + console.log(prompt.join('\n')); + return; + } + if (process.argv[2] === '--claude-in-chrome-mcp') { + profileCheckpoint('cli_claude_in_chrome_mcp_path'); + const { runClaudeInChromeMcpServer } = await import('../utils/claudeInChrome/mcpServer.js'); + await runClaudeInChromeMcpServer(); + return; + } else if (process.argv[2] === '--chrome-native-host') { + profileCheckpoint('cli_chrome_native_host_path'); + const { runChromeNativeHost } = await import('../utils/claudeInChrome/chromeNativeHost.js'); + await runChromeNativeHost(); + return; + } else if (feature('CHICAGO_MCP') && process.argv[2] === '--computer-use-mcp') { + profileCheckpoint('cli_computer_use_mcp_path'); + const { runComputerUseMcpServer } = await import('../utils/computerUse/mcpServer.js'); + await runComputerUseMcpServer(); + return; + } + + // Fast-path for `--daemon-worker=` (internal — supervisor spawns this). + // Must come before the daemon subcommand check: spawned per-worker, so + // perf-sensitive. No enableConfigs(), no analytics sinks at this layer — + // workers are lean. If a worker kind needs configs/auth (assistant will), + // it calls them inside its run() fn. + if (feature('DAEMON') && args[0] === '--daemon-worker') { + const { runDaemonWorker } = await import('../daemon/workerRegistry.js'); + await runDaemonWorker(args[1]); + return; + } + + // Fast-path for `claude remote-control` (also accepts legacy `claude remote` / `claude sync` / `claude bridge`): + // serve local machine as bridge environment. + // feature() must stay inline for build-time dead code elimination; + // isBridgeEnabled() checks the runtime GrowthBook gate. + if ( + feature('BRIDGE_MODE') && + (args[0] === 'remote-control' || + args[0] === 'rc' || + args[0] === 'remote' || + args[0] === 'sync' || + args[0] === 'bridge') + ) { + profileCheckpoint('cli_bridge_path'); + const { enableConfigs } = await import('../utils/config.js'); + enableConfigs(); + const { getBridgeDisabledReason, checkBridgeMinVersion } = await import('../bridge/bridgeEnabled.js'); + const { BRIDGE_LOGIN_ERROR } = await import('../bridge/types.js'); + const { bridgeMain } = await import('../bridge/bridgeMain.js'); + const { exitWithError } = await import('../utils/process.js'); + + // Auth check must come before the GrowthBook gate check — without auth, + // GrowthBook has no user context and would return a stale/default false. + // getBridgeDisabledReason awaits GB init, so the returned value is fresh + // (not the stale disk cache), but init still needs auth headers to work. + const { getClaudeAIOAuthTokens } = await import('../utils/auth.js'); + if (!getClaudeAIOAuthTokens()?.accessToken) { + exitWithError(BRIDGE_LOGIN_ERROR); + } + const disabledReason = await getBridgeDisabledReason(); + if (disabledReason) { + exitWithError(`Error: ${disabledReason}`); + } + const versionError = checkBridgeMinVersion(); + if (versionError) { + exitWithError(versionError); + } + + // Bridge is a remote control feature - check policy limits + const { waitForPolicyLimitsToLoad, isPolicyAllowed } = await import('../services/policyLimits/index.js'); + await waitForPolicyLimitsToLoad(); + if (!isPolicyAllowed('allow_remote_control')) { + exitWithError("Error: Remote Control is disabled by your organization's policy."); + } + await bridgeMain(args.slice(1)); + return; + } + + // Fast-path for `claude daemon [subcommand]`: long-running supervisor. + if (feature('DAEMON') && args[0] === 'daemon') { + profileCheckpoint('cli_daemon_path'); + const { enableConfigs } = await import('../utils/config.js'); + enableConfigs(); + const { initSinks } = await import('../utils/sinks.js'); + initSinks(); + const { daemonMain } = await import('../daemon/main.js'); + await daemonMain(args.slice(1)); + return; + } + + // Fast-path for `claude ps|logs|attach|kill` and `--bg`/`--background`. + // Session management against the ~/.claude/sessions/ registry. Flag + // literals are inlined so bg.js only loads when actually dispatching. + if ( + feature('BG_SESSIONS') && + (args[0] === 'ps' || + args[0] === 'logs' || + args[0] === 'attach' || + args[0] === 'kill' || + args.includes('--bg') || + args.includes('--background')) + ) { + profileCheckpoint('cli_bg_path'); + const { enableConfigs } = await import('../utils/config.js'); + enableConfigs(); + const bg = await import('../cli/bg.js'); + switch (args[0]) { + case 'ps': + await bg.psHandler(args.slice(1)); + break; + case 'logs': + await bg.logsHandler(args[1]); + break; + case 'attach': + await bg.attachHandler(args[1]); + break; + case 'kill': + await bg.killHandler(args[1]); + break; + default: + await bg.handleBgFlag(args); + } + return; + } + + // Fast-path for template job commands. + if (feature('TEMPLATES') && (args[0] === 'new' || args[0] === 'list' || args[0] === 'reply')) { + profileCheckpoint('cli_templates_path'); + const { templatesMain } = await import('../cli/handlers/templateJobs.js'); + await templatesMain(args); + // process.exit (not return) — mountFleetView's Ink TUI can leave event + // loop handles that prevent natural exit. + // eslint-disable-next-line custom-rules/no-process-exit + process.exit(0); + } + + // Fast-path for `claude environment-runner`: headless BYOC runner. + // feature() must stay inline for build-time dead code elimination. + if (feature('BYOC_ENVIRONMENT_RUNNER') && args[0] === 'environment-runner') { + profileCheckpoint('cli_environment_runner_path'); + const { environmentRunnerMain } = await import('../environment-runner/main.js'); + await environmentRunnerMain(args.slice(1)); + return; + } + + // Fast-path for `claude self-hosted-runner`: headless self-hosted-runner + // targeting the SelfHostedRunnerWorkerService API (register + poll; poll IS + // heartbeat). feature() must stay inline for build-time dead code elimination. + if (feature('SELF_HOSTED_RUNNER') && args[0] === 'self-hosted-runner') { + profileCheckpoint('cli_self_hosted_runner_path'); + const { selfHostedRunnerMain } = await import('../self-hosted-runner/main.js'); + await selfHostedRunnerMain(args.slice(1)); + return; + } + + // Fast-path for --worktree --tmux: exec into tmux before loading full CLI + const hasTmuxFlag = args.includes('--tmux') || args.includes('--tmux=classic'); + if ( + hasTmuxFlag && + (args.includes('-w') || args.includes('--worktree') || args.some(a => a.startsWith('--worktree='))) + ) { + profileCheckpoint('cli_tmux_worktree_fast_path'); + const { enableConfigs } = await import('../utils/config.js'); + enableConfigs(); + const { isWorktreeModeEnabled } = await import('../utils/worktreeModeEnabled.js'); + if (isWorktreeModeEnabled()) { + const { execIntoTmuxWorktree } = await import('../utils/worktree.js'); + const result = await execIntoTmuxWorktree(args); + if (result.handled) { return; + } + // If not handled (e.g., error), fall through to normal CLI + if (result.error) { + const { exitWithError } = await import('../utils/process.js'); + exitWithError(result.error); + } } + } - // For all other paths, load the startup profiler - const { profileCheckpoint } = await import("../utils/startupProfiler.js"); - profileCheckpoint("cli_entry"); + // Redirect common update flag mistakes to the update subcommand + if (args.length === 1 && (args[0] === '--update' || args[0] === '--upgrade')) { + process.argv = [process.argv[0]!, process.argv[1]!, 'update']; + } - // Fast-path for --dump-system-prompt: output the rendered system prompt and exit. - // Used by prompt sensitivity evals to extract the system prompt at a specific commit. - // Ant-only: eliminated from external builds via feature flag. - if (feature("DUMP_SYSTEM_PROMPT") && args[0] === "--dump-system-prompt") { - profileCheckpoint("cli_dump_system_prompt_path"); - const { enableConfigs } = await import("../utils/config.js"); - enableConfigs(); - const { getMainLoopModel } = await import("../utils/model/model.js"); - const modelIdx = args.indexOf("--model"); - const model = - (modelIdx !== -1 && args[modelIdx + 1]) || getMainLoopModel(); - const { getSystemPrompt } = await import("../constants/prompts.js"); - const prompt = await getSystemPrompt([], model); - // biome-ignore lint/suspicious/noConsole:: intentional console output - console.log(prompt.join("\n")); - return; - } - if (process.argv[2] === "--claude-in-chrome-mcp") { - profileCheckpoint("cli_claude_in_chrome_mcp_path"); - const { runClaudeInChromeMcpServer } = - await import("../utils/claudeInChrome/mcpServer.js"); - await runClaudeInChromeMcpServer(); - return; - } else if (process.argv[2] === "--chrome-native-host") { - profileCheckpoint("cli_chrome_native_host_path"); - const { runChromeNativeHost } = - await import("../utils/claudeInChrome/chromeNativeHost.js"); - await runChromeNativeHost(); - return; - } else if ( - feature("CHICAGO_MCP") && - process.argv[2] === "--computer-use-mcp" - ) { - profileCheckpoint("cli_computer_use_mcp_path"); - const { runComputerUseMcpServer } = - await import("../utils/computerUse/mcpServer.js"); - await runComputerUseMcpServer(); - return; - } + // --bare: set SIMPLE early so gates fire during module eval / commander + // option building (not just inside the action handler). + if (args.includes('--bare')) { + process.env.CLAUDE_CODE_SIMPLE = '1'; + } - // Fast-path for `--daemon-worker=` (internal — supervisor spawns this). - // Must come before the daemon subcommand check: spawned per-worker, so - // perf-sensitive. No enableConfigs(), no analytics sinks at this layer — - // workers are lean. If a worker kind needs configs/auth (assistant will), - // it calls them inside its run() fn. - if (feature("DAEMON") && args[0] === "--daemon-worker") { - const { runDaemonWorker } = await import("../daemon/workerRegistry.js"); - await runDaemonWorker(args[1]); - return; - } - - // Fast-path for `claude remote-control` (also accepts legacy `claude remote` / `claude sync` / `claude bridge`): - // serve local machine as bridge environment. - // feature() must stay inline for build-time dead code elimination; - // isBridgeEnabled() checks the runtime GrowthBook gate. - if ( - feature("BRIDGE_MODE") && - (args[0] === "remote-control" || - args[0] === "rc" || - args[0] === "remote" || - args[0] === "sync" || - args[0] === "bridge") - ) { - profileCheckpoint("cli_bridge_path"); - const { enableConfigs } = await import("../utils/config.js"); - enableConfigs(); - const { getBridgeDisabledReason, checkBridgeMinVersion } = - await import("../bridge/bridgeEnabled.js"); - const { BRIDGE_LOGIN_ERROR } = await import("../bridge/types.js"); - const { bridgeMain } = await import("../bridge/bridgeMain.js"); - const { exitWithError } = await import("../utils/process.js"); - - // Auth check must come before the GrowthBook gate check — without auth, - // GrowthBook has no user context and would return a stale/default false. - // getBridgeDisabledReason awaits GB init, so the returned value is fresh - // (not the stale disk cache), but init still needs auth headers to work. - const { getClaudeAIOAuthTokens } = await import("../utils/auth.js"); - if (!getClaudeAIOAuthTokens()?.accessToken) { - exitWithError(BRIDGE_LOGIN_ERROR); - } - const disabledReason = await getBridgeDisabledReason(); - if (disabledReason) { - exitWithError(`Error: ${disabledReason}`); - } - const versionError = checkBridgeMinVersion(); - if (versionError) { - exitWithError(versionError); - } - - // Bridge is a remote control feature - check policy limits - const { waitForPolicyLimitsToLoad, isPolicyAllowed } = - await import("../services/policyLimits/index.js"); - await waitForPolicyLimitsToLoad(); - if (!isPolicyAllowed("allow_remote_control")) { - exitWithError( - "Error: Remote Control is disabled by your organization's policy.", - ); - } - await bridgeMain(args.slice(1)); - return; - } - - // Fast-path for `claude daemon [subcommand]`: long-running supervisor. - if (feature("DAEMON") && args[0] === "daemon") { - profileCheckpoint("cli_daemon_path"); - const { enableConfigs } = await import("../utils/config.js"); - enableConfigs(); - const { initSinks } = await import("../utils/sinks.js"); - initSinks(); - const { daemonMain } = await import("../daemon/main.js"); - await daemonMain(args.slice(1)); - return; - } - - // Fast-path for `claude ps|logs|attach|kill` and `--bg`/`--background`. - // Session management against the ~/.claude/sessions/ registry. Flag - // literals are inlined so bg.js only loads when actually dispatching. - if ( - feature("BG_SESSIONS") && - (args[0] === "ps" || - args[0] === "logs" || - args[0] === "attach" || - args[0] === "kill" || - args.includes("--bg") || - args.includes("--background")) - ) { - profileCheckpoint("cli_bg_path"); - const { enableConfigs } = await import("../utils/config.js"); - enableConfigs(); - const bg = await import("../cli/bg.js"); - switch (args[0]) { - case "ps": - await bg.psHandler(args.slice(1)); - break; - case "logs": - await bg.logsHandler(args[1]); - break; - case "attach": - await bg.attachHandler(args[1]); - break; - case "kill": - await bg.killHandler(args[1]); - break; - default: - await bg.handleBgFlag(args); - } - return; - } - - // Fast-path for template job commands. - if ( - feature("TEMPLATES") && - (args[0] === "new" || args[0] === "list" || args[0] === "reply") - ) { - profileCheckpoint("cli_templates_path"); - const { templatesMain } = - await import("../cli/handlers/templateJobs.js"); - await templatesMain(args); - // process.exit (not return) — mountFleetView's Ink TUI can leave event - // loop handles that prevent natural exit. - // eslint-disable-next-line custom-rules/no-process-exit - process.exit(0); - } - - // Fast-path for `claude environment-runner`: headless BYOC runner. - // feature() must stay inline for build-time dead code elimination. - if ( - feature("BYOC_ENVIRONMENT_RUNNER") && - args[0] === "environment-runner" - ) { - profileCheckpoint("cli_environment_runner_path"); - const { environmentRunnerMain } = - await import("../environment-runner/main.js"); - await environmentRunnerMain(args.slice(1)); - return; - } - - // Fast-path for `claude self-hosted-runner`: headless self-hosted-runner - // targeting the SelfHostedRunnerWorkerService API (register + poll; poll IS - // heartbeat). feature() must stay inline for build-time dead code elimination. - if (feature("SELF_HOSTED_RUNNER") && args[0] === "self-hosted-runner") { - profileCheckpoint("cli_self_hosted_runner_path"); - const { selfHostedRunnerMain } = - await import("../self-hosted-runner/main.js"); - await selfHostedRunnerMain(args.slice(1)); - return; - } - - // Fast-path for --worktree --tmux: exec into tmux before loading full CLI - const hasTmuxFlag = - args.includes("--tmux") || args.includes("--tmux=classic"); - if ( - hasTmuxFlag && - (args.includes("-w") || - args.includes("--worktree") || - args.some((a) => a.startsWith("--worktree="))) - ) { - profileCheckpoint("cli_tmux_worktree_fast_path"); - const { enableConfigs } = await import("../utils/config.js"); - enableConfigs(); - const { isWorktreeModeEnabled } = - await import("../utils/worktreeModeEnabled.js"); - if (isWorktreeModeEnabled()) { - const { execIntoTmuxWorktree } = - await import("../utils/worktree.js"); - const result = await execIntoTmuxWorktree(args); - if (result.handled) { - return; - } - // If not handled (e.g., error), fall through to normal CLI - if (result.error) { - const { exitWithError } = await import("../utils/process.js"); - exitWithError(result.error); - } - } - } - - // Redirect common update flag mistakes to the update subcommand - if ( - args.length === 1 && - (args[0] === "--update" || args[0] === "--upgrade") - ) { - process.argv = [process.argv[0]!, process.argv[1]!, "update"]; - } - - // --bare: set SIMPLE early so gates fire during module eval / commander - // option building (not just inside the action handler). - if (args.includes("--bare")) { - process.env.CLAUDE_CODE_SIMPLE = "1"; - } - - // No special flags detected, load and run the full CLI - const { startCapturingEarlyInput } = await import("../utils/earlyInput.js"); - startCapturingEarlyInput(); - profileCheckpoint("cli_before_main_import"); - const { main: cliMain } = await import("../main.jsx"); - profileCheckpoint("cli_after_main_import"); - await cliMain(); - profileCheckpoint("cli_after_main_complete"); + // No special flags detected, load and run the full CLI + const { startCapturingEarlyInput } = await import('../utils/earlyInput.js'); + startCapturingEarlyInput(); + profileCheckpoint('cli_before_main_import'); + const { main: cliMain } = await import('../main.jsx'); + profileCheckpoint('cli_after_main_import'); + await cliMain(); + profileCheckpoint('cli_after_main_complete'); } // eslint-disable-next-line custom-rules/no-top-level-side-effects diff --git a/src/utils/context.ts b/src/utils/context.ts index d9714de9a..5ec51871c 100644 --- a/src/utils/context.ts +++ b/src/utils/context.ts @@ -3,6 +3,7 @@ import { CONTEXT_1M_BETA_HEADER } from '../constants/betas.js' import { getGlobalConfig } from './config.js' import { isEnvTruthy } from './envUtils.js' import { getCanonicalName } from './model/model.js' +import { resolveAntModel } from './model/antModels.js' import { getModelCapability } from './model/modelCapabilities.js' // Model context window size (200k tokens for all models right now) diff --git a/src/utils/effort.ts b/src/utils/effort.ts index abca97b51..e6e8a4db0 100644 --- a/src/utils/effort.ts +++ b/src/utils/effort.ts @@ -7,6 +7,8 @@ import { getAPIProvider } from './model/providers.js' import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js' import { isEnvTruthy } from './envUtils.js' import type { EffortLevel } from 'src/entrypoints/sdk/runtimeTypes.js' +import { resolveAntModel } from './model/antModels.js' +import { getAntModelOverrideConfig } from './model/antModels.js' export type { EffortLevel } diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 0414683ed..695076c86 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -6,6 +6,7 @@ * during dead code elimination */ import { getMainLoopModelOverride } from '../../bootstrap/state.js' +import { resolveAntModel, getAntModelOverrideConfig } from './antModels.js' import { getSubscriptionType, isClaudeAISubscriber, diff --git a/src/utils/thinking.ts b/src/utils/thinking.ts index b37fe42de..df62072a5 100644 --- a/src/utils/thinking.ts +++ b/src/utils/thinking.ts @@ -6,6 +6,7 @@ import { getCanonicalName } from './model/model.js' import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js' import { getAPIProvider } from './model/providers.js' import { getSettingsWithErrors } from './settings/settings.js' +import { resolveAntModel } from './model/antModels.js' export type ThinkingConfig = | { type: 'adaptive' }