Compare commits

..

8 Commits

Author SHA1 Message Date
claude-code-best
465c95ae53 chore: 1.11.1 2026-04-30 20:51:17 +08:00
claude-code-best
42100d6268 feat: 关闭 skill learning 2026-04-30 20:42:07 +08:00
claude-code-best
ca29e4e8f7 fix: 禁用 FORK_SUBAGENT 恢复 Explore 子代理的 haiku 模型分发
启用 FORK_SUBAGENT 后,Agent prompt 引导模型用 fork(继承父模型)
替代 Explore 子代理(haiku),导致探索任务使用同等级模型而非低成本模型。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:41:30 +08:00
claude-code-best
cd8136f4b1 Merge pull request #395 from bonerush/fix/theme-switching
fix: theme switching always defaults to dark mode
2026-04-30 18:11:59 +08:00
Bonerush
71c89e9de4 fix: theme switching always defaults to dark mode
Root causes:
1. ThemeProvider was imported but never used in App.tsx and showSetupDialog
2. setThemeConfigCallbacks was never called to inject persistence callbacks
3. Preview/save/cancel theme lifecycle had no provider to coordinate

Changes:
- Export setThemeConfigCallbacks from @anthropic/ink
- Wrap App.tsx children with ThemeProvider (initialState from config, onThemeSave persists)
- Wrap showSetupDialog with ThemeProvider for onboarding/trust dialogs
- Call setThemeConfigCallbacks in init.ts to register load/save callbacks
- Update SnapshotUpdateDialog test to account for new ThemeProvider wrapper

Fixes #theme-switching
2026-04-30 16:15:27 +08:00
claude-code-best
632f3e199e Merge pull request #381 from LittleApple-fp16/patch-1
Fix formatting in README.md links section
2026-04-30 09:08:26 +08:00
claude-code-best
282d515043 chore: v1.11.0 2026-04-29 22:12:08 +08:00
LittleApple
4b97e6638e Fix formatting in README.md links section 2026-04-28 11:53:30 +08:00
8 changed files with 173 additions and 207 deletions

View File

@@ -34,7 +34,7 @@
| GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) | | GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) | | /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
- 🚀 [想要启动项目](#快速开始源码版) - 🚀 [想要启动项目](#-快速开始源码版)
- 🐛 [想要调试项目](#vs-code-调试) - 🐛 [想要调试项目](#vs-code-调试)
- 📖 [想要学习项目](#teach-me-学习项目) - 📖 [想要学习项目](#teach-me-学习项目)

View File

@@ -1,6 +1,6 @@
{ {
"name": "claude-code-best", "name": "claude-code-best",
"version": "1.10.11", "version": "1.11.1",
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal", "description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
"type": "module", "type": "module",
"author": "claude-code-best <claude-code-best@proton.me>", "author": "claude-code-best <claude-code-best@proton.me>",

View File

@@ -154,6 +154,7 @@ export { TerminalWriteProvider, useTerminalNotification, type TerminalNotificati
// ============================================================ // ============================================================
export { export {
ThemeProvider, ThemeProvider,
setThemeConfigCallbacks,
usePreviewTheme, usePreviewTheme,
useTheme, useTheme,
useThemeSetting, useThemeSetting,

View File

@@ -52,7 +52,7 @@ export const DEFAULT_BUILD_FEATURES = [
'HISTORY_SNIP', // 历史消息裁剪,压缩上下文窗口 'HISTORY_SNIP', // 历史消息裁剪,压缩上下文窗口
'CONTEXT_COLLAPSE', // 上下文折叠,自动压缩旧消息 'CONTEXT_COLLAPSE', // 上下文折叠,自动压缩旧消息
'MONITOR_TOOL', // Monitor 工具,流式监控后台进程输出 'MONITOR_TOOL', // Monitor 工具,流式监控后台进程输出
'FORK_SUBAGENT', // Fork 子代理,在隔离上下文中并行执行任务 // 'FORK_SUBAGENT', // 已禁用:启用后 prompt 引导模型用 fork继承父模型替代 Explorehaiku导致探索任务使用同等级模型
// 'UDS_INBOX', // inbox 数组只增不减(非 GB 级主因) // 'UDS_INBOX', // inbox 数组只增不减(非 GB 级主因)
'KAIROS', // Kairos 定时任务系统核心 'KAIROS', // Kairos 定时任务系统核心
// 'COORDINATOR_MODE', // 已禁用AgentSummary 30s fork 循环GB 级泄露主因 // 'COORDINATOR_MODE', // 已禁用AgentSummary 30s fork 循环GB 级泄露主因
@@ -74,8 +74,8 @@ export const DEFAULT_BUILD_FEATURES = [
// this branch (see docs/agent/sur-skill-overflow-bugs.md) close the // this branch (see docs/agent/sur-skill-overflow-bugs.md) close the
// overflow risk, but Haiku-on-first-Chinese-query and disk-side // overflow risk, but Haiku-on-first-Chinese-query and disk-side
// observation accumulation remain operator-discretion concerns. // observation accumulation remain operator-discretion concerns.
'EXPERIMENTAL_SKILL_SEARCH', // 'EXPERIMENTAL_SKILL_SEARCH',
'SKILL_LEARNING', // 'SKILL_LEARNING',
// P3: poor mode // P3: poor mode
'POOR', // 穷鬼模式,跳过 extract_memories/prompt_suggestion 减少消耗 'POOR', // 穷鬼模式,跳过 extract_memories/prompt_suggestion 减少消耗
// Team Memory // Team Memory

View File

@@ -1,38 +1,36 @@
import React from 'react' import React from 'react';
import { FpsMetricsProvider } from '../context/fpsMetrics.js' import { FpsMetricsProvider } from '../context/fpsMetrics.js';
import { StatsProvider, type StatsStore } from '../context/stats.js' import { StatsProvider, type StatsStore } from '../context/stats.js';
import { type AppState, AppStateProvider } from '../state/AppState.js' import { type AppState, AppStateProvider } from '../state/AppState.js';
import { onChangeAppState } from '../state/onChangeAppState.js' import { onChangeAppState } from '../state/onChangeAppState.js';
import type { FpsMetrics } from '../utils/fpsTracker.js' import type { FpsMetrics } from '../utils/fpsTracker.js';
import { ThemeProvider } from '@anthropic/ink' import { ThemeProvider } from '@anthropic/ink';
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js';
type Props = { type Props = {
getFpsMetrics: () => FpsMetrics | undefined getFpsMetrics: () => FpsMetrics | undefined;
stats?: StatsStore stats?: StatsStore;
initialState: AppState initialState: AppState;
children: React.ReactNode children: React.ReactNode;
} };
/** /**
* Top-level wrapper for interactive sessions. * Top-level wrapper for interactive sessions.
* Provides FPS metrics, stats context, and app state to the component tree. * Provides FPS metrics, stats context, and app state to the component tree.
*/ */
export function App({ export function App({ getFpsMetrics, stats, initialState, children }: Props): React.ReactNode {
getFpsMetrics,
stats,
initialState,
children,
}: Props): React.ReactNode {
return ( return (
<FpsMetricsProvider getFpsMetrics={getFpsMetrics}> <FpsMetricsProvider getFpsMetrics={getFpsMetrics}>
<StatsProvider store={stats}> <StatsProvider store={stats}>
<AppStateProvider <AppStateProvider initialState={initialState} onChangeAppState={onChangeAppState}>
initialState={initialState} <ThemeProvider
onChangeAppState={onChangeAppState} initialState={getGlobalConfig().theme}
> onThemeSave={setting => saveGlobalConfig(current => ({ ...current, theme: setting }))}
{children} >
{children}
</ThemeProvider>
</AppStateProvider> </AppStateProvider>
</StatsProvider> </StatsProvider>
</FpsMetricsProvider> </FpsMetricsProvider>
) );
} }

View File

@@ -5,7 +5,10 @@ import { buildMergePrompt, SnapshotUpdateDialog } from '../SnapshotUpdateDialog.
import { Select } from '../../CustomSelect/index.js'; import { Select } from '../../CustomSelect/index.js';
function getSnapshotDialogFromRenderedTree(rendered: React.ReactElement) { function getSnapshotDialogFromRenderedTree(rendered: React.ReactElement) {
const appStateProvider = rendered as React.ReactElement<{ const themeProvider = rendered as React.ReactElement<{
children: React.ReactElement;
}>;
const appStateProvider = themeProvider.props.children as React.ReactElement<{
children: React.ReactElement; children: React.ReactElement;
}>; }>;
const keybindingSetup = appStateProvider.props.children as React.ReactElement<{ const keybindingSetup = appStateProvider.props.children as React.ReactElement<{

View File

@@ -20,7 +20,12 @@ import {
import { preconnectAnthropicApi } from '../utils/apiPreconnect.js' import { preconnectAnthropicApi } from '../utils/apiPreconnect.js'
import { applyExtraCACertsFromConfig } from '../utils/caCertsConfig.js' import { applyExtraCACertsFromConfig } from '../utils/caCertsConfig.js'
import { registerCleanup } from '../utils/cleanupRegistry.js' import { registerCleanup } from '../utils/cleanupRegistry.js'
import { enableConfigs, recordFirstStartTime } from '../utils/config.js' import {
enableConfigs,
getGlobalConfig,
recordFirstStartTime,
saveGlobalConfig,
} from '../utils/config.js'
import { logForDebugging } from '../utils/debug.js' import { logForDebugging } from '../utils/debug.js'
import { detectCurrentRepository } from '../utils/detectRepository.js' import { detectCurrentRepository } from '../utils/detectRepository.js'
import { logForDiagnosticsNoPII } from '../utils/diagLogs.js' import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
@@ -51,6 +56,7 @@ import { setShellIfWindows } from '../utils/windowsPaths.js'
import { initSentry } from '../utils/sentry.js' import { initSentry } from '../utils/sentry.js'
import { initUser } from '../utils/user.js' import { initUser } from '../utils/user.js'
import { initLangfuse, shutdownLangfuse } from '../services/langfuse/index.js' import { initLangfuse, shutdownLangfuse } from '../services/langfuse/index.js'
import { setThemeConfigCallbacks } from '@anthropic/ink'
// initialize1PEventLogging is dynamically imported to defer OpenTelemetry sdk-logs/resources // initialize1PEventLogging is dynamically imported to defer OpenTelemetry sdk-logs/resources
@@ -66,6 +72,11 @@ export const init = memoize(async (): Promise<void> => {
try { try {
const configsStart = Date.now() const configsStart = Date.now()
enableConfigs() enableConfigs()
setThemeConfigCallbacks({
loadTheme: () => getGlobalConfig().theme,
saveTheme: setting =>
saveGlobalConfig(current => ({ ...current, theme: setting })),
})
logForDiagnosticsNoPII('info', 'init_configs_enabled', { logForDiagnosticsNoPII('info', 'init_configs_enabled', {
duration_ms: Date.now() - configsStart, duration_ms: Date.now() - configsStart,
}) })

View File

@@ -1,11 +1,8 @@
import { feature } from 'bun:bundle' import { feature } from 'bun:bundle';
import { appendFileSync } from 'fs' import { appendFileSync } from 'fs';
import React from 'react' import React from 'react';
import { logEvent } from 'src/services/analytics/index.js' import { logEvent } from 'src/services/analytics/index.js';
import { import { gracefulShutdown, gracefulShutdownSync } from 'src/utils/gracefulShutdown.js';
gracefulShutdown,
gracefulShutdownSync,
} from 'src/utils/gracefulShutdown.js'
import { import {
type ChannelEntry, type ChannelEntry,
getAllowedChannels, getAllowedChannels,
@@ -13,63 +10,59 @@ import {
setHasDevChannels, setHasDevChannels,
setSessionTrustAccepted, setSessionTrustAccepted,
setStatsStore, setStatsStore,
} from './bootstrap/state.js' } from './bootstrap/state.js';
import type { Command } from './commands.js' import type { Command } from './commands.js';
import { createStatsStore, type StatsStore } from './context/stats.js' import { createStatsStore, type StatsStore } from './context/stats.js';
import { getSystemContext } from './context.js' import { getSystemContext } from './context.js';
import { initializeTelemetryAfterTrust } from './entrypoints/init.js' import { initializeTelemetryAfterTrust } from './entrypoints/init.js';
import { isSynchronizedOutputSupported } from '@anthropic/ink' import { isSynchronizedOutputSupported } from '@anthropic/ink';
import type { RenderOptions, Root, TextProps } from '@anthropic/ink' import type { RenderOptions, Root, TextProps } from '@anthropic/ink';
import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js' import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js';
import { startDeferredPrefetches } from './main.js' import { startDeferredPrefetches } from './main.js';
import { import {
checkGate_CACHED_OR_BLOCKING, checkGate_CACHED_OR_BLOCKING,
initializeGrowthBook, initializeGrowthBook,
resetGrowthBook, resetGrowthBook,
} from './services/analytics/growthbook.js' } from './services/analytics/growthbook.js';
import { isQualifiedForGrove } from './services/api/grove.js' import { isQualifiedForGrove } from './services/api/grove.js';
import { handleMcpjsonServerApprovals } from './services/mcpServerApproval.js' import { handleMcpjsonServerApprovals } from './services/mcpServerApproval.js';
import { AppStateProvider } from './state/AppState.js' import { AppStateProvider } from './state/AppState.js';
import { onChangeAppState } from './state/onChangeAppState.js' import { onChangeAppState } from './state/onChangeAppState.js';
import { normalizeApiKeyForConfig } from './utils/authPortable.js' import { ThemeProvider } from '@anthropic/ink';
import { normalizeApiKeyForConfig } from './utils/authPortable.js';
import { import {
getExternalClaudeMdIncludes, getExternalClaudeMdIncludes,
getMemoryFiles, getMemoryFiles,
shouldShowClaudeMdExternalIncludesWarning, shouldShowClaudeMdExternalIncludesWarning,
} from './utils/claudemd.js' } from './utils/claudemd.js';
import { import {
checkHasTrustDialogAccepted, checkHasTrustDialogAccepted,
getCustomApiKeyStatus, getCustomApiKeyStatus,
getGlobalConfig, getGlobalConfig,
saveGlobalConfig, saveGlobalConfig,
} from './utils/config.js' } from './utils/config.js';
import { updateDeepLinkTerminalPreference } from './utils/deepLink/terminalPreference.js' import { updateDeepLinkTerminalPreference } from './utils/deepLink/terminalPreference.js';
import { isEnvTruthy, isRunningOnHomespace } from './utils/envUtils.js' import { isEnvTruthy, isRunningOnHomespace } from './utils/envUtils.js';
import { type FpsMetrics, FpsTracker } from './utils/fpsTracker.js' import { type FpsMetrics, FpsTracker } from './utils/fpsTracker.js';
import { updateGithubRepoPathMapping } from './utils/githubRepoPathMapping.js' import { updateGithubRepoPathMapping } from './utils/githubRepoPathMapping.js';
import { applyConfigEnvironmentVariables } from './utils/managedEnv.js' import { applyConfigEnvironmentVariables } from './utils/managedEnv.js';
import type { PermissionMode } from './utils/permissions/PermissionMode.js' import type { PermissionMode } from './utils/permissions/PermissionMode.js';
import { getBaseRenderOptions } from './utils/renderOptions.js' import { getBaseRenderOptions } from './utils/renderOptions.js';
import { getSettingsWithAllErrors } from './utils/settings/allErrors.js' import { getSettingsWithAllErrors } from './utils/settings/allErrors.js';
import { import { hasSkipDangerousModePermissionPrompt } from './utils/settings/settings.js';
hasSkipDangerousModePermissionPrompt,
} from './utils/settings/settings.js'
export function completeOnboarding(): void { export function completeOnboarding(): void {
saveGlobalConfig(current => ({ saveGlobalConfig(current => ({
...current, ...current,
hasCompletedOnboarding: true, hasCompletedOnboarding: true,
lastOnboardingVersion: MACRO.VERSION, lastOnboardingVersion: MACRO.VERSION,
})) }));
} }
export function showDialog<T = void>( export function showDialog<T = void>(root: Root, renderer: (done: (result: T) => void) => React.ReactNode): Promise<T> {
root: Root,
renderer: (done: (result: T) => void) => React.ReactNode,
): Promise<T> {
return new Promise<T>(resolve => { return new Promise<T>(resolve => {
const done = (result: T): void => void resolve(result) const done = (result: T): void => void resolve(result);
root.render(renderer(done)) root.render(renderer(done));
}) });
} }
/** /**
@@ -78,12 +71,8 @@ export function showDialog<T = void>(
* console.error is swallowed by Ink's patchConsole, so we render * console.error is swallowed by Ink's patchConsole, so we render
* through the React tree instead. * through the React tree instead.
*/ */
export async function exitWithError( export async function exitWithError(root: Root, message: string, beforeExit?: () => Promise<void>): Promise<never> {
root: Root, return exitWithMessage(root, message, { color: 'error', beforeExit });
message: string,
beforeExit?: () => Promise<void>,
): Promise<never> {
return exitWithMessage(root, message, { color: 'error', beforeExit })
} }
/** /**
@@ -96,21 +85,19 @@ export async function exitWithMessage(
root: Root, root: Root,
message: string, message: string,
options?: { options?: {
color?: TextProps['color'] color?: TextProps['color'];
exitCode?: number exitCode?: number;
beforeExit?: () => Promise<void> beforeExit?: () => Promise<void>;
}, },
): Promise<never> { ): Promise<never> {
const { Text } = await import('@anthropic/ink') const { Text } = await import('@anthropic/ink');
const color = options?.color const color = options?.color;
const exitCode = options?.exitCode ?? 1 const exitCode = options?.exitCode ?? 1;
root.render( root.render(color ? <Text color={color}>{message}</Text> : <Text>{message}</Text>);
color ? <Text color={color}>{message}</Text> : <Text>{message}</Text>, root.unmount();
) await options?.beforeExit?.();
root.unmount()
await options?.beforeExit?.()
// eslint-disable-next-line custom-rules/no-process-exit -- exit after Ink unmount // eslint-disable-next-line custom-rules/no-process-exit -- exit after Ink unmount
process.exit(exitCode) process.exit(exitCode);
} }
/** /**
@@ -123,24 +110,26 @@ export function showSetupDialog<T = void>(
options?: { onChangeAppState?: typeof onChangeAppState }, options?: { onChangeAppState?: typeof onChangeAppState },
): Promise<T> { ): Promise<T> {
return showDialog<T>(root, done => ( return showDialog<T>(root, done => (
<AppStateProvider onChangeAppState={options?.onChangeAppState}> <ThemeProvider
<KeybindingSetup>{renderer(done)}</KeybindingSetup> initialState={getGlobalConfig().theme}
</AppStateProvider> onThemeSave={setting => saveGlobalConfig(current => ({ ...current, theme: setting }))}
)) >
<AppStateProvider onChangeAppState={options?.onChangeAppState}>
<KeybindingSetup>{renderer(done)}</KeybindingSetup>
</AppStateProvider>
</ThemeProvider>
));
} }
/** /**
* Render the main UI into the root and wait for it to exit. * Render the main UI into the root and wait for it to exit.
* Handles the common epilogue: start deferred prefetches, wait for exit, graceful shutdown. * Handles the common epilogue: start deferred prefetches, wait for exit, graceful shutdown.
*/ */
export async function renderAndRun( export async function renderAndRun(root: Root, element: React.ReactNode): Promise<void> {
root: Root, root.render(element);
element: React.ReactNode, startDeferredPrefetches();
): Promise<void> { await root.waitUntilExit();
root.render(element) await gracefulShutdown(0);
startDeferredPrefetches()
await root.waitUntilExit()
await gracefulShutdown(0)
} }
export async function showSetupScreens( export async function showSetupScreens(
@@ -156,29 +145,29 @@ export async function showSetupScreens(
isEnvTruthy(false) || isEnvTruthy(false) ||
process.env.IS_DEMO // Skip onboarding in demo mode process.env.IS_DEMO // Skip onboarding in demo mode
) { ) {
return false return false;
} }
const config = getGlobalConfig() const config = getGlobalConfig();
let onboardingShown = false let onboardingShown = false;
if ( if (
!config.theme || !config.theme ||
!config.hasCompletedOnboarding // always show onboarding at least once !config.hasCompletedOnboarding // always show onboarding at least once
) { ) {
onboardingShown = true onboardingShown = true;
const { Onboarding } = await import('./components/Onboarding.js') const { Onboarding } = await import('./components/Onboarding.js');
await showSetupDialog( await showSetupDialog(
root, root,
done => ( done => (
<Onboarding <Onboarding
onDone={() => { onDone={() => {
completeOnboarding() completeOnboarding();
void done() void done();
}} }}
/> />
), ),
{ onChangeAppState }, { onChangeAppState },
) );
} }
// Always show the trust dialog in interactive sessions, regardless of permission mode. // Always show the trust dialog in interactive sessions, regardless of permission mode.
@@ -192,83 +181,71 @@ export async function showSetupScreens(
// If it returns true, the TrustDialog would auto-resolve regardless of // If it returns true, the TrustDialog would auto-resolve regardless of
// security features, so we can skip the dynamic import and render cycle. // security features, so we can skip the dynamic import and render cycle.
if (!checkHasTrustDialogAccepted()) { if (!checkHasTrustDialogAccepted()) {
const { TrustDialog } = await import( const { TrustDialog } = await import('./components/TrustDialog/TrustDialog.js');
'./components/TrustDialog/TrustDialog.js' await showSetupDialog(root, done => <TrustDialog commands={commands} onDone={done} />);
)
await showSetupDialog(root, done => (
<TrustDialog commands={commands} onDone={done} />
))
} }
// Signal that trust has been verified for this session. // Signal that trust has been verified for this session.
// GrowthBook checks this to decide whether to include auth headers. // GrowthBook checks this to decide whether to include auth headers.
setSessionTrustAccepted(true) setSessionTrustAccepted(true);
// Reset and reinitialize GrowthBook after trust is established. // Reset and reinitialize GrowthBook after trust is established.
// Defense for login/logout: clears any prior client so the next init // Defense for login/logout: clears any prior client so the next init
// picks up fresh auth headers. // picks up fresh auth headers.
resetGrowthBook() resetGrowthBook();
void initializeGrowthBook() void initializeGrowthBook();
// Now that trust is established, prefetch system context if it wasn't already // Now that trust is established, prefetch system context if it wasn't already
void getSystemContext() void getSystemContext();
// If settings are valid, check for any mcp.json servers that need approval // If settings are valid, check for any mcp.json servers that need approval
const { errors: allErrors } = getSettingsWithAllErrors() const { errors: allErrors } = getSettingsWithAllErrors();
if (allErrors.length === 0) { if (allErrors.length === 0) {
await handleMcpjsonServerApprovals(root) await handleMcpjsonServerApprovals(root);
} }
// Check for claude.md includes that need approval // Check for claude.md includes that need approval
if (await shouldShowClaudeMdExternalIncludesWarning()) { if (await shouldShowClaudeMdExternalIncludesWarning()) {
const externalIncludes = getExternalClaudeMdIncludes( const externalIncludes = getExternalClaudeMdIncludes(await getMemoryFiles(true));
await getMemoryFiles(true), const { ClaudeMdExternalIncludesDialog } = await import('./components/ClaudeMdExternalIncludesDialog.js');
)
const { ClaudeMdExternalIncludesDialog } = await import(
'./components/ClaudeMdExternalIncludesDialog.js'
)
await showSetupDialog(root, done => ( await showSetupDialog(root, done => (
<ClaudeMdExternalIncludesDialog <ClaudeMdExternalIncludesDialog onDone={done} isStandaloneDialog externalIncludes={externalIncludes} />
onDone={done} ));
isStandaloneDialog
externalIncludes={externalIncludes}
/>
))
} }
} }
// Track current repo path for teleport directory switching (fire-and-forget) // Track current repo path for teleport directory switching (fire-and-forget)
// This must happen AFTER trust to prevent untrusted directories from poisoning the mapping // This must happen AFTER trust to prevent untrusted directories from poisoning the mapping
void updateGithubRepoPathMapping() void updateGithubRepoPathMapping();
if (feature('LODESTONE')) { if (feature('LODESTONE')) {
updateDeepLinkTerminalPreference() updateDeepLinkTerminalPreference();
} }
// Apply full environment variables after trust dialog is accepted OR in bypass mode // Apply full environment variables after trust dialog is accepted OR in bypass mode
// In bypass mode (CI/CD, automation), we trust the environment so apply all variables // In bypass mode (CI/CD, automation), we trust the environment so apply all variables
// In normal mode, this happens after the trust dialog is accepted // In normal mode, this happens after the trust dialog is accepted
// This includes potentially dangerous environment variables from untrusted sources // This includes potentially dangerous environment variables from untrusted sources
applyConfigEnvironmentVariables() applyConfigEnvironmentVariables();
// Initialize telemetry after env vars are applied so OTEL endpoint env vars and // Initialize telemetry after env vars are applied so OTEL endpoint env vars and
// otelHeadersHelper (which requires trust to execute) are available. // otelHeadersHelper (which requires trust to execute) are available.
// Defer to next tick so the OTel dynamic import resolves after first render // Defer to next tick so the OTel dynamic import resolves after first render
// instead of during the pre-render microtask queue. // instead of during the pre-render microtask queue.
setImmediate(() => initializeTelemetryAfterTrust()) setImmediate(() => initializeTelemetryAfterTrust());
if (await isQualifiedForGrove()) { if (await isQualifiedForGrove()) {
const { GroveDialog } = await import('src/components/grove/Grove.js') const { GroveDialog } = await import('src/components/grove/Grove.js');
const decision = await showSetupDialog<string>(root, done => ( const decision = await showSetupDialog<string>(root, done => (
<GroveDialog <GroveDialog
showIfAlreadyViewed={false} showIfAlreadyViewed={false}
location={onboardingShown ? 'onboarding' : 'policy_update_modal'} location={onboardingShown ? 'onboarding' : 'policy_update_modal'}
onDone={done} onDone={done}
/> />
)) ));
if (decision === 'escape') { if (decision === 'escape') {
logEvent('tengu_grove_policy_exited', {}) logEvent('tengu_grove_policy_exited', {});
gracefulShutdownSync(0) gracefulShutdownSync(0);
return false return false;
} }
} }
@@ -276,36 +253,24 @@ export async function showSetupScreens(
// On homespace, ANTHROPIC_API_KEY is preserved in process.env for child // On homespace, ANTHROPIC_API_KEY is preserved in process.env for child
// processes but ignored by Claude Code itself (see auth.ts). // processes but ignored by Claude Code itself (see auth.ts).
if (process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace()) { if (process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace()) {
const customApiKeyTruncated = normalizeApiKeyForConfig( const customApiKeyTruncated = normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY);
process.env.ANTHROPIC_API_KEY, const keyStatus = getCustomApiKeyStatus(customApiKeyTruncated);
)
const keyStatus = getCustomApiKeyStatus(customApiKeyTruncated)
if (keyStatus === 'new') { if (keyStatus === 'new') {
const { ApproveApiKey } = await import('./components/ApproveApiKey.js') const { ApproveApiKey } = await import('./components/ApproveApiKey.js');
await showSetupDialog<boolean>( await showSetupDialog<boolean>(
root, root,
done => ( done => <ApproveApiKey customApiKeyTruncated={customApiKeyTruncated} onDone={done} />,
<ApproveApiKey
customApiKeyTruncated={customApiKeyTruncated}
onDone={done}
/>
),
{ onChangeAppState }, { onChangeAppState },
) );
} }
} }
if ( if (
(permissionMode === 'bypassPermissions' || (permissionMode === 'bypassPermissions' || allowDangerouslySkipPermissions) &&
allowDangerouslySkipPermissions) &&
!hasSkipDangerousModePermissionPrompt() !hasSkipDangerousModePermissionPrompt()
) { ) {
const { BypassPermissionsModeDialog } = await import( const { BypassPermissionsModeDialog } = await import('./components/BypassPermissionsModeDialog.js');
'./components/BypassPermissionsModeDialog.js' await showSetupDialog(root, done => <BypassPermissionsModeDialog onAccept={done} />);
)
await showSetupDialog(root, done => (
<BypassPermissionsModeDialog onAccept={done} />
))
} }
// --dangerously-load-development-channels confirmation. On accept, append // --dangerously-load-development-channels confirmation. On accept, append
@@ -313,72 +278,60 @@ export async function showSetupScreens(
// is NOT bypassed — gateChannelServer() still runs; this flag only exists // is NOT bypassed — gateChannelServer() still runs; this flag only exists
// to sidestep the --channels approved-server allowlist. // to sidestep the --channels approved-server allowlist.
if (devChannels && devChannels.length > 0) { if (devChannels && devChannels.length > 0) {
const { DevChannelsDialog } = await import( const { DevChannelsDialog } = await import('./components/DevChannelsDialog.js');
'./components/DevChannelsDialog.js'
)
await showSetupDialog(root, done => ( await showSetupDialog(root, done => (
<DevChannelsDialog <DevChannelsDialog
channels={devChannels} channels={devChannels}
onAccept={() => { onAccept={() => {
// Mark dev entries per-entry so the allowlist bypass doesn't leak // Mark dev entries per-entry so the allowlist bypass doesn't leak
// to --channels entries when both flags are passed. // to --channels entries when both flags are passed.
setAllowedChannels([ setAllowedChannels([...getAllowedChannels(), ...devChannels.map(c => ({ ...c, dev: true }))]);
...getAllowedChannels(), setHasDevChannels(true);
...devChannels.map(c => ({ ...c, dev: true })), void done();
])
setHasDevChannels(true)
void done()
}} }}
/> />
)) ));
} }
// Show Chrome onboarding for first-time Claude in Chrome users // Show Chrome onboarding for first-time Claude in Chrome users
if ( if (claudeInChrome && !getGlobalConfig().hasCompletedClaudeInChromeOnboarding) {
claudeInChrome && const { ClaudeInChromeOnboarding } = await import('./components/ClaudeInChromeOnboarding.js');
!getGlobalConfig().hasCompletedClaudeInChromeOnboarding await showSetupDialog(root, done => <ClaudeInChromeOnboarding onDone={done} />);
) {
const { ClaudeInChromeOnboarding } = await import(
'./components/ClaudeInChromeOnboarding.js'
)
await showSetupDialog(root, done => (
<ClaudeInChromeOnboarding onDone={done} />
))
} }
return onboardingShown return onboardingShown;
} }
export function getRenderContext(exitOnCtrlC: boolean): { export function getRenderContext(exitOnCtrlC: boolean): {
renderOptions: RenderOptions renderOptions: RenderOptions;
getFpsMetrics: () => FpsMetrics | undefined getFpsMetrics: () => FpsMetrics | undefined;
stats: StatsStore stats: StatsStore;
} { } {
let lastFlickerTime = 0 let lastFlickerTime = 0;
const baseOptions = getBaseRenderOptions(exitOnCtrlC) const baseOptions = getBaseRenderOptions(exitOnCtrlC);
// Log analytics event when stdin override is active // Log analytics event when stdin override is active
if (baseOptions.stdin) { if (baseOptions.stdin) {
logEvent('tengu_stdin_interactive', {}) logEvent('tengu_stdin_interactive', {});
} }
const fpsTracker = new FpsTracker() const fpsTracker = new FpsTracker();
const stats = createStatsStore() const stats = createStatsStore();
setStatsStore(stats) setStatsStore(stats);
// Bench mode: when set, append per-frame phase timings as JSONL for // Bench mode: when set, append per-frame phase timings as JSONL for
// offline analysis by bench/repl-scroll.ts. Captures the full TUI // offline analysis by bench/repl-scroll.ts. Captures the full TUI
// render pipeline (yoga → screen buffer → diff → optimize → stdout) // render pipeline (yoga → screen buffer → diff → optimize → stdout)
// so perf work on any phase can be validated against real user flows. // so perf work on any phase can be validated against real user flows.
const frameTimingLogPath = process.env.CLAUDE_CODE_FRAME_TIMING_LOG const frameTimingLogPath = process.env.CLAUDE_CODE_FRAME_TIMING_LOG;
return { return {
getFpsMetrics: () => fpsTracker.getMetrics(), getFpsMetrics: () => fpsTracker.getMetrics(),
stats, stats,
renderOptions: { renderOptions: {
...baseOptions, ...baseOptions,
onFrame: event => { onFrame: event => {
fpsTracker.record(event.durationMs) fpsTracker.record(event.durationMs);
stats.observe('frame_duration_ms', event.durationMs) stats.observe('frame_duration_ms', event.durationMs);
if (frameTimingLogPath && event.phases) { if (frameTimingLogPath && event.phases) {
// Bench-only env-var-gated path: sync write so no frames dropped // Bench-only env-var-gated path: sync write so no frames dropped
// on abrupt exit. ~100 bytes at ≤60fps is negligible. rss/cpu are // on abrupt exit. ~100 bytes at ≤60fps is negligible. rss/cpu are
@@ -390,30 +343,30 @@ export function getRenderContext(exitOnCtrlC: boolean): {
...event.phases, ...event.phases,
rss: process.memoryUsage.rss(), rss: process.memoryUsage.rss(),
cpu: process.cpuUsage(), cpu: process.cpuUsage(),
}) + '\n' }) + '\n';
// eslint-disable-next-line custom-rules/no-sync-fs -- bench-only, sync so no frames dropped on exit // eslint-disable-next-line custom-rules/no-sync-fs -- bench-only, sync so no frames dropped on exit
appendFileSync(frameTimingLogPath, line) appendFileSync(frameTimingLogPath, line);
} }
// Skip flicker reporting for terminals with synchronized output — // Skip flicker reporting for terminals with synchronized output —
// DEC 2026 buffers between BSU/ESU so clear+redraw is atomic. // DEC 2026 buffers between BSU/ESU so clear+redraw is atomic.
if (isSynchronizedOutputSupported()) { if (isSynchronizedOutputSupported()) {
return return;
} }
for (const flicker of event.flickers) { for (const flicker of event.flickers) {
if (flicker.reason === 'resize') { if (flicker.reason === 'resize') {
continue continue;
} }
const now = Date.now() const now = Date.now();
if (now - lastFlickerTime < 1000) { if (now - lastFlickerTime < 1000) {
logEvent('tengu_flicker', { logEvent('tengu_flicker', {
desiredHeight: flicker.desiredHeight, desiredHeight: flicker.desiredHeight,
actualHeight: flicker.availableHeight, actualHeight: flicker.availableHeight,
reason: flicker.reason, reason: flicker.reason,
} as unknown as Record<string, boolean | number | undefined>) } as unknown as Record<string, boolean | number | undefined>);
} }
lastFlickerTime = now lastFlickerTime = now;
} }
}, },
}, },
} };
} }