mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-2): 格式化 commands (79 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-5): 格式化 components其余 + hooks + tools (232 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md - README.md: 大幅重写,更详细版本历史和配置示例 - Run.ps1: 新增 Windows 启动脚本 - TODO.md: 新增包完成清单 - V6.md: 删除(架构重构规划已不适用) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复以前的问题 * fix: 修复 login 面板的问题 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,46 +1,76 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import { appendFileSync } from 'fs';
|
||||
import React from 'react';
|
||||
import { logEvent } from 'src/services/analytics/index.js';
|
||||
import { gracefulShutdown, gracefulShutdownSync } from 'src/utils/gracefulShutdown.js';
|
||||
import { type ChannelEntry, getAllowedChannels, setAllowedChannels, setHasDevChannels, setSessionTrustAccepted, setStatsStore } from './bootstrap/state.js';
|
||||
import type { Command } from './commands.js';
|
||||
import { createStatsStore, type StatsStore } from './context/stats.js';
|
||||
import { getSystemContext } from './context.js';
|
||||
import { initializeTelemetryAfterTrust } from './entrypoints/init.js';
|
||||
import { isSynchronizedOutputSupported } from './ink/terminal.js';
|
||||
import type { RenderOptions, Root, TextProps } from './ink.js';
|
||||
import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js';
|
||||
import { startDeferredPrefetches } from './main.js';
|
||||
import { checkGate_CACHED_OR_BLOCKING, initializeGrowthBook, resetGrowthBook } from './services/analytics/growthbook.js';
|
||||
import { isQualifiedForGrove } from './services/api/grove.js';
|
||||
import { handleMcpjsonServerApprovals } from './services/mcpServerApproval.js';
|
||||
import { AppStateProvider } from './state/AppState.js';
|
||||
import { onChangeAppState } from './state/onChangeAppState.js';
|
||||
import { normalizeApiKeyForConfig } from './utils/authPortable.js';
|
||||
import { getExternalClaudeMdIncludes, getMemoryFiles, shouldShowClaudeMdExternalIncludesWarning } from './utils/claudemd.js';
|
||||
import { checkHasTrustDialogAccepted, getCustomApiKeyStatus, getGlobalConfig, saveGlobalConfig } from './utils/config.js';
|
||||
import { updateDeepLinkTerminalPreference } from './utils/deepLink/terminalPreference.js';
|
||||
import { isEnvTruthy, isRunningOnHomespace } from './utils/envUtils.js';
|
||||
import { type FpsMetrics, FpsTracker } from './utils/fpsTracker.js';
|
||||
import { updateGithubRepoPathMapping } from './utils/githubRepoPathMapping.js';
|
||||
import { applyConfigEnvironmentVariables } from './utils/managedEnv.js';
|
||||
import type { PermissionMode } from './utils/permissions/PermissionMode.js';
|
||||
import { getBaseRenderOptions } from './utils/renderOptions.js';
|
||||
import { getSettingsWithAllErrors } from './utils/settings/allErrors.js';
|
||||
import { hasAutoModeOptIn, hasSkipDangerousModePermissionPrompt } from './utils/settings/settings.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import { appendFileSync } from 'fs'
|
||||
import React from 'react'
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import {
|
||||
gracefulShutdown,
|
||||
gracefulShutdownSync,
|
||||
} from 'src/utils/gracefulShutdown.js'
|
||||
import {
|
||||
type ChannelEntry,
|
||||
getAllowedChannels,
|
||||
setAllowedChannels,
|
||||
setHasDevChannels,
|
||||
setSessionTrustAccepted,
|
||||
setStatsStore,
|
||||
} from './bootstrap/state.js'
|
||||
import type { Command } from './commands.js'
|
||||
import { createStatsStore, type StatsStore } from './context/stats.js'
|
||||
import { getSystemContext } from './context.js'
|
||||
import { initializeTelemetryAfterTrust } from './entrypoints/init.js'
|
||||
import { isSynchronizedOutputSupported } from './ink/terminal.js'
|
||||
import type { RenderOptions, Root, TextProps } from './ink.js'
|
||||
import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js'
|
||||
import { startDeferredPrefetches } from './main.js'
|
||||
import {
|
||||
checkGate_CACHED_OR_BLOCKING,
|
||||
initializeGrowthBook,
|
||||
resetGrowthBook,
|
||||
} from './services/analytics/growthbook.js'
|
||||
import { isQualifiedForGrove } from './services/api/grove.js'
|
||||
import { handleMcpjsonServerApprovals } from './services/mcpServerApproval.js'
|
||||
import { AppStateProvider } from './state/AppState.js'
|
||||
import { onChangeAppState } from './state/onChangeAppState.js'
|
||||
import { normalizeApiKeyForConfig } from './utils/authPortable.js'
|
||||
import {
|
||||
getExternalClaudeMdIncludes,
|
||||
getMemoryFiles,
|
||||
shouldShowClaudeMdExternalIncludesWarning,
|
||||
} from './utils/claudemd.js'
|
||||
import {
|
||||
checkHasTrustDialogAccepted,
|
||||
getCustomApiKeyStatus,
|
||||
getGlobalConfig,
|
||||
saveGlobalConfig,
|
||||
} from './utils/config.js'
|
||||
import { updateDeepLinkTerminalPreference } from './utils/deepLink/terminalPreference.js'
|
||||
import { isEnvTruthy, isRunningOnHomespace } from './utils/envUtils.js'
|
||||
import { type FpsMetrics, FpsTracker } from './utils/fpsTracker.js'
|
||||
import { updateGithubRepoPathMapping } from './utils/githubRepoPathMapping.js'
|
||||
import { applyConfigEnvironmentVariables } from './utils/managedEnv.js'
|
||||
import type { PermissionMode } from './utils/permissions/PermissionMode.js'
|
||||
import { getBaseRenderOptions } from './utils/renderOptions.js'
|
||||
import { getSettingsWithAllErrors } from './utils/settings/allErrors.js'
|
||||
import {
|
||||
hasAutoModeOptIn,
|
||||
hasSkipDangerousModePermissionPrompt,
|
||||
} from './utils/settings/settings.js'
|
||||
|
||||
export function completeOnboarding(): void {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
hasCompletedOnboarding: true,
|
||||
lastOnboardingVersion: MACRO.VERSION
|
||||
}));
|
||||
lastOnboardingVersion: MACRO.VERSION,
|
||||
}))
|
||||
}
|
||||
export function showDialog<T = void>(root: Root, renderer: (done: (result: T) => void) => React.ReactNode): Promise<T> {
|
||||
export function showDialog<T = void>(
|
||||
root: Root,
|
||||
renderer: (done: (result: T) => void) => React.ReactNode,
|
||||
): Promise<T> {
|
||||
return new Promise<T>(resolve => {
|
||||
const done = (result: T): void => void resolve(result);
|
||||
root.render(renderer(done));
|
||||
});
|
||||
const done = (result: T): void => void resolve(result)
|
||||
root.render(renderer(done))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,11 +79,12 @@ export function showDialog<T = void>(root: Root, renderer: (done: (result: T) =>
|
||||
* console.error is swallowed by Ink's patchConsole, so we render
|
||||
* through the React tree instead.
|
||||
*/
|
||||
export async function exitWithError(root: Root, message: string, beforeExit?: () => Promise<void>): Promise<never> {
|
||||
return exitWithMessage(root, message, {
|
||||
color: 'error',
|
||||
beforeExit
|
||||
});
|
||||
export async function exitWithError(
|
||||
root: Root,
|
||||
message: string,
|
||||
beforeExit?: () => Promise<void>,
|
||||
): Promise<never> {
|
||||
return exitWithMessage(root, message, { color: 'error', beforeExit })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,64 +93,93 @@ export async function exitWithError(root: Root, message: string, beforeExit?: ()
|
||||
* console output is swallowed by Ink's patchConsole, so we render
|
||||
* through the React tree instead.
|
||||
*/
|
||||
export async function exitWithMessage(root: Root, message: string, options?: {
|
||||
color?: TextProps['color'];
|
||||
exitCode?: number;
|
||||
beforeExit?: () => Promise<void>;
|
||||
}): Promise<never> {
|
||||
const {
|
||||
Text
|
||||
} = await import('./ink.js');
|
||||
const color = options?.color;
|
||||
const exitCode = options?.exitCode ?? 1;
|
||||
root.render(color ? <Text color={color}>{message}</Text> : <Text>{message}</Text>);
|
||||
root.unmount();
|
||||
await options?.beforeExit?.();
|
||||
export async function exitWithMessage(
|
||||
root: Root,
|
||||
message: string,
|
||||
options?: {
|
||||
color?: TextProps['color']
|
||||
exitCode?: number
|
||||
beforeExit?: () => Promise<void>
|
||||
},
|
||||
): Promise<never> {
|
||||
const { Text } = await import('./ink.js')
|
||||
const color = options?.color
|
||||
const exitCode = options?.exitCode ?? 1
|
||||
root.render(
|
||||
color ? <Text color={color}>{message}</Text> : <Text>{message}</Text>,
|
||||
)
|
||||
root.unmount()
|
||||
await options?.beforeExit?.()
|
||||
// eslint-disable-next-line custom-rules/no-process-exit -- exit after Ink unmount
|
||||
process.exit(exitCode);
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a setup dialog wrapped in AppStateProvider + KeybindingSetup.
|
||||
* Reduces boilerplate in showSetupScreens() where every dialog needs these wrappers.
|
||||
*/
|
||||
export function showSetupDialog<T = void>(root: Root, renderer: (done: (result: T) => void) => React.ReactNode, options?: {
|
||||
onChangeAppState?: typeof onChangeAppState;
|
||||
}): Promise<T> {
|
||||
return showDialog<T>(root, done => <AppStateProvider onChangeAppState={options?.onChangeAppState}>
|
||||
export function showSetupDialog<T = void>(
|
||||
root: Root,
|
||||
renderer: (done: (result: T) => void) => React.ReactNode,
|
||||
options?: { onChangeAppState?: typeof onChangeAppState },
|
||||
): Promise<T> {
|
||||
return showDialog<T>(root, done => (
|
||||
<AppStateProvider onChangeAppState={options?.onChangeAppState}>
|
||||
<KeybindingSetup>{renderer(done)}</KeybindingSetup>
|
||||
</AppStateProvider>);
|
||||
</AppStateProvider>
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export async function renderAndRun(root: Root, element: React.ReactNode): Promise<void> {
|
||||
root.render(element);
|
||||
startDeferredPrefetches();
|
||||
await root.waitUntilExit();
|
||||
await gracefulShutdown(0);
|
||||
export async function renderAndRun(
|
||||
root: Root,
|
||||
element: React.ReactNode,
|
||||
): Promise<void> {
|
||||
root.render(element)
|
||||
startDeferredPrefetches()
|
||||
await root.waitUntilExit()
|
||||
await gracefulShutdown(0)
|
||||
}
|
||||
export async function showSetupScreens(root: Root, permissionMode: PermissionMode, allowDangerouslySkipPermissions: boolean, commands?: Command[], claudeInChrome?: boolean, devChannels?: ChannelEntry[]): Promise<boolean> {
|
||||
if (("production" as string) === 'test' || isEnvTruthy(false) || process.env.IS_DEMO // Skip onboarding in demo mode
|
||||
|
||||
export async function showSetupScreens(
|
||||
root: Root,
|
||||
permissionMode: PermissionMode,
|
||||
allowDangerouslySkipPermissions: boolean,
|
||||
commands?: Command[],
|
||||
claudeInChrome?: boolean,
|
||||
devChannels?: ChannelEntry[],
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
"production" === 'test' ||
|
||||
isEnvTruthy(false) ||
|
||||
process.env.IS_DEMO // Skip onboarding in demo mode
|
||||
) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
const config = getGlobalConfig();
|
||||
let onboardingShown = false;
|
||||
if (!config.theme || !config.hasCompletedOnboarding // always show onboarding at least once
|
||||
|
||||
const config = getGlobalConfig()
|
||||
let onboardingShown = false
|
||||
if (
|
||||
!config.theme ||
|
||||
!config.hasCompletedOnboarding // always show onboarding at least once
|
||||
) {
|
||||
onboardingShown = true;
|
||||
const {
|
||||
Onboarding
|
||||
} = await import('./components/Onboarding.js');
|
||||
await showSetupDialog(root, done => <Onboarding onDone={() => {
|
||||
completeOnboarding();
|
||||
void done();
|
||||
}} />, {
|
||||
onChangeAppState
|
||||
});
|
||||
onboardingShown = true
|
||||
const { Onboarding } = await import('./components/Onboarding.js')
|
||||
await showSetupDialog(
|
||||
root,
|
||||
done => (
|
||||
<Onboarding
|
||||
onDone={() => {
|
||||
completeOnboarding()
|
||||
void done()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
{ onChangeAppState },
|
||||
)
|
||||
}
|
||||
|
||||
// Always show the trust dialog in interactive sessions, regardless of permission mode.
|
||||
@@ -133,70 +193,83 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod
|
||||
// If it returns true, the TrustDialog would auto-resolve regardless of
|
||||
// security features, so we can skip the dynamic import and render cycle.
|
||||
if (!checkHasTrustDialogAccepted()) {
|
||||
const {
|
||||
TrustDialog
|
||||
} = await import('./components/TrustDialog/TrustDialog.js');
|
||||
await showSetupDialog(root, done => <TrustDialog commands={commands} onDone={done} />);
|
||||
const { TrustDialog } = await import(
|
||||
'./components/TrustDialog/TrustDialog.js'
|
||||
)
|
||||
await showSetupDialog(root, done => (
|
||||
<TrustDialog commands={commands} onDone={done} />
|
||||
))
|
||||
}
|
||||
|
||||
// Signal that trust has been verified for this session.
|
||||
// GrowthBook checks this to decide whether to include auth headers.
|
||||
setSessionTrustAccepted(true);
|
||||
setSessionTrustAccepted(true)
|
||||
|
||||
// Reset and reinitialize GrowthBook after trust is established.
|
||||
// Defense for login/logout: clears any prior client so the next init
|
||||
// picks up fresh auth headers.
|
||||
resetGrowthBook();
|
||||
void initializeGrowthBook();
|
||||
resetGrowthBook()
|
||||
void initializeGrowthBook()
|
||||
|
||||
// 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
|
||||
const {
|
||||
errors: allErrors
|
||||
} = getSettingsWithAllErrors();
|
||||
const { errors: allErrors } = getSettingsWithAllErrors()
|
||||
if (allErrors.length === 0) {
|
||||
await handleMcpjsonServerApprovals(root);
|
||||
await handleMcpjsonServerApprovals(root)
|
||||
}
|
||||
|
||||
// Check for claude.md includes that need approval
|
||||
if (await shouldShowClaudeMdExternalIncludesWarning()) {
|
||||
const externalIncludes = getExternalClaudeMdIncludes(await getMemoryFiles(true));
|
||||
const {
|
||||
ClaudeMdExternalIncludesDialog
|
||||
} = await import('./components/ClaudeMdExternalIncludesDialog.js');
|
||||
await showSetupDialog(root, done => <ClaudeMdExternalIncludesDialog onDone={done} isStandaloneDialog externalIncludes={externalIncludes} />);
|
||||
const externalIncludes = getExternalClaudeMdIncludes(
|
||||
await getMemoryFiles(true),
|
||||
)
|
||||
const { ClaudeMdExternalIncludesDialog } = await import(
|
||||
'./components/ClaudeMdExternalIncludesDialog.js'
|
||||
)
|
||||
await showSetupDialog(root, done => (
|
||||
<ClaudeMdExternalIncludesDialog
|
||||
onDone={done}
|
||||
isStandaloneDialog
|
||||
externalIncludes={externalIncludes}
|
||||
/>
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Track current repo path for teleport directory switching (fire-and-forget)
|
||||
// This must happen AFTER trust to prevent untrusted directories from poisoning the mapping
|
||||
void updateGithubRepoPathMapping();
|
||||
void updateGithubRepoPathMapping()
|
||||
if (feature('LODESTONE')) {
|
||||
updateDeepLinkTerminalPreference();
|
||||
updateDeepLinkTerminalPreference()
|
||||
}
|
||||
|
||||
// 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 normal mode, this happens after the trust dialog is accepted
|
||||
// This includes potentially dangerous environment variables from untrusted sources
|
||||
applyConfigEnvironmentVariables();
|
||||
applyConfigEnvironmentVariables()
|
||||
|
||||
// Initialize telemetry after env vars are applied so OTEL endpoint env vars and
|
||||
// otelHeadersHelper (which requires trust to execute) are available.
|
||||
// Defer to next tick so the OTel dynamic import resolves after first render
|
||||
// instead of during the pre-render microtask queue.
|
||||
setImmediate(() => initializeTelemetryAfterTrust());
|
||||
setImmediate(() => initializeTelemetryAfterTrust())
|
||||
|
||||
if (await isQualifiedForGrove()) {
|
||||
const {
|
||||
GroveDialog
|
||||
} = await import('src/components/grove/Grove.js');
|
||||
const decision = await showSetupDialog<string>(root, done => <GroveDialog showIfAlreadyViewed={false} location={onboardingShown ? 'onboarding' : 'policy_update_modal'} onDone={done} />);
|
||||
const { GroveDialog } = await import('src/components/grove/Grove.js')
|
||||
const decision = await showSetupDialog<string>(root, done => (
|
||||
<GroveDialog
|
||||
showIfAlreadyViewed={false}
|
||||
location={onboardingShown ? 'onboarding' : 'policy_update_modal'}
|
||||
onDone={done}
|
||||
/>
|
||||
))
|
||||
if (decision === 'escape') {
|
||||
logEvent('tengu_grove_policy_exited', {});
|
||||
gracefulShutdownSync(0);
|
||||
return false;
|
||||
logEvent('tengu_grove_policy_exited', {})
|
||||
gracefulShutdownSync(0)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,33 +277,54 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod
|
||||
// On homespace, ANTHROPIC_API_KEY is preserved in process.env for child
|
||||
// processes but ignored by Claude Code itself (see auth.ts).
|
||||
if (process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace()) {
|
||||
const customApiKeyTruncated = normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY);
|
||||
const keyStatus = getCustomApiKeyStatus(customApiKeyTruncated);
|
||||
const customApiKeyTruncated = normalizeApiKeyForConfig(
|
||||
process.env.ANTHROPIC_API_KEY,
|
||||
)
|
||||
const keyStatus = getCustomApiKeyStatus(customApiKeyTruncated)
|
||||
if (keyStatus === 'new') {
|
||||
const {
|
||||
ApproveApiKey
|
||||
} = await import('./components/ApproveApiKey.js');
|
||||
await showSetupDialog<boolean>(root, done => <ApproveApiKey customApiKeyTruncated={customApiKeyTruncated} onDone={done} />, {
|
||||
onChangeAppState
|
||||
});
|
||||
const { ApproveApiKey } = await import('./components/ApproveApiKey.js')
|
||||
await showSetupDialog<boolean>(
|
||||
root,
|
||||
done => (
|
||||
<ApproveApiKey
|
||||
customApiKeyTruncated={customApiKeyTruncated}
|
||||
onDone={done}
|
||||
/>
|
||||
),
|
||||
{ onChangeAppState },
|
||||
)
|
||||
}
|
||||
}
|
||||
if ((permissionMode === 'bypassPermissions' || allowDangerouslySkipPermissions) && !hasSkipDangerousModePermissionPrompt()) {
|
||||
const {
|
||||
BypassPermissionsModeDialog
|
||||
} = await import('./components/BypassPermissionsModeDialog.js');
|
||||
await showSetupDialog(root, done => <BypassPermissionsModeDialog onAccept={done} />);
|
||||
|
||||
if (
|
||||
(permissionMode === 'bypassPermissions' ||
|
||||
allowDangerouslySkipPermissions) &&
|
||||
!hasSkipDangerousModePermissionPrompt()
|
||||
) {
|
||||
const { BypassPermissionsModeDialog } = await import(
|
||||
'./components/BypassPermissionsModeDialog.js'
|
||||
)
|
||||
await showSetupDialog(root, done => (
|
||||
<BypassPermissionsModeDialog onAccept={done} />
|
||||
))
|
||||
}
|
||||
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
// Only show the opt-in dialog if auto mode actually resolved — if the
|
||||
// gate denied it (org not allowlisted, settings disabled), showing
|
||||
// consent for an unavailable feature is pointless. The
|
||||
// verifyAutoModeGateAccess notification will explain why instead.
|
||||
if (permissionMode === 'auto' && !hasAutoModeOptIn()) {
|
||||
const {
|
||||
AutoModeOptInDialog
|
||||
} = await import('./components/AutoModeOptInDialog.js');
|
||||
await showSetupDialog(root, done => <AutoModeOptInDialog onAccept={done} onDecline={() => gracefulShutdownSync(1)} declineExits />);
|
||||
const { AutoModeOptInDialog } = await import(
|
||||
'./components/AutoModeOptInDialog.js'
|
||||
)
|
||||
await showSetupDialog(root, done => (
|
||||
<AutoModeOptInDialog
|
||||
onAccept={done}
|
||||
onDecline={() => gracefulShutdownSync(1)}
|
||||
declineExits
|
||||
/>
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,14 +342,15 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod
|
||||
// initializeGrowthBook promise fired earlier). Also warms the
|
||||
// isChannelsEnabled() check in the dev-channels dialog below.
|
||||
if (getAllowedChannels().length > 0 || (devChannels?.length ?? 0) > 0) {
|
||||
await checkGate_CACHED_OR_BLOCKING('tengu_harbor');
|
||||
await checkGate_CACHED_OR_BLOCKING('tengu_harbor')
|
||||
}
|
||||
|
||||
if (devChannels && devChannels.length > 0) {
|
||||
const [{
|
||||
isChannelsEnabled
|
||||
}, {
|
||||
getClaudeAIOAuthTokens
|
||||
}] = await Promise.all([import('./services/mcp/channelAllowlist.js'), import('./utils/auth.js')]);
|
||||
const [{ isChannelsEnabled }, { getClaudeAIOAuthTokens }] =
|
||||
await Promise.all([
|
||||
import('./services/mcp/channelAllowlist.js'),
|
||||
import('./utils/auth.js'),
|
||||
])
|
||||
// Skip the dialog when channels are blocked (tengu_harbor off or no
|
||||
// OAuth) — accepting then immediately seeing "not available" in
|
||||
// ChannelsNotice is worse than no dialog. Append entries anyway so
|
||||
@@ -264,102 +359,115 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod
|
||||
// (hasNonDev check); the allowlist bypass it also grants is moot
|
||||
// since the gate blocks upstream.
|
||||
if (!isChannelsEnabled() || !getClaudeAIOAuthTokens()?.accessToken) {
|
||||
setAllowedChannels([...getAllowedChannels(), ...devChannels.map(c => ({
|
||||
...c,
|
||||
dev: true
|
||||
}))]);
|
||||
setHasDevChannels(true);
|
||||
setAllowedChannels([
|
||||
...getAllowedChannels(),
|
||||
...devChannels.map(c => ({ ...c, dev: true })),
|
||||
])
|
||||
setHasDevChannels(true)
|
||||
} else {
|
||||
const {
|
||||
DevChannelsDialog
|
||||
} = await import('./components/DevChannelsDialog.js');
|
||||
await showSetupDialog(root, done => <DevChannelsDialog channels={devChannels} onAccept={() => {
|
||||
// Mark dev entries per-entry so the allowlist bypass doesn't leak
|
||||
// to --channels entries when both flags are passed.
|
||||
setAllowedChannels([...getAllowedChannels(), ...devChannels.map(c => ({
|
||||
...c,
|
||||
dev: true
|
||||
}))]);
|
||||
setHasDevChannels(true);
|
||||
void done();
|
||||
}} />);
|
||||
const { DevChannelsDialog } = await import(
|
||||
'./components/DevChannelsDialog.js'
|
||||
)
|
||||
await showSetupDialog(root, done => (
|
||||
<DevChannelsDialog
|
||||
channels={devChannels}
|
||||
onAccept={() => {
|
||||
// Mark dev entries per-entry so the allowlist bypass doesn't leak
|
||||
// to --channels entries when both flags are passed.
|
||||
setAllowedChannels([
|
||||
...getAllowedChannels(),
|
||||
...devChannels.map(c => ({ ...c, dev: true })),
|
||||
])
|
||||
setHasDevChannels(true)
|
||||
void done()
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show Chrome onboarding for first-time Claude in Chrome users
|
||||
if (claudeInChrome && !getGlobalConfig().hasCompletedClaudeInChromeOnboarding) {
|
||||
const {
|
||||
ClaudeInChromeOnboarding
|
||||
} = await import('./components/ClaudeInChromeOnboarding.js');
|
||||
await showSetupDialog(root, done => <ClaudeInChromeOnboarding onDone={done} />);
|
||||
if (
|
||||
claudeInChrome &&
|
||||
!getGlobalConfig().hasCompletedClaudeInChromeOnboarding
|
||||
) {
|
||||
const { ClaudeInChromeOnboarding } = await import(
|
||||
'./components/ClaudeInChromeOnboarding.js'
|
||||
)
|
||||
await showSetupDialog(root, done => (
|
||||
<ClaudeInChromeOnboarding onDone={done} />
|
||||
))
|
||||
}
|
||||
return onboardingShown;
|
||||
|
||||
return onboardingShown
|
||||
}
|
||||
|
||||
export function getRenderContext(exitOnCtrlC: boolean): {
|
||||
renderOptions: RenderOptions;
|
||||
getFpsMetrics: () => FpsMetrics | undefined;
|
||||
stats: StatsStore;
|
||||
renderOptions: RenderOptions
|
||||
getFpsMetrics: () => FpsMetrics | undefined
|
||||
stats: StatsStore
|
||||
} {
|
||||
let lastFlickerTime = 0;
|
||||
const baseOptions = getBaseRenderOptions(exitOnCtrlC);
|
||||
let lastFlickerTime = 0
|
||||
const baseOptions = getBaseRenderOptions(exitOnCtrlC)
|
||||
|
||||
// Log analytics event when stdin override is active
|
||||
if (baseOptions.stdin) {
|
||||
logEvent('tengu_stdin_interactive', {});
|
||||
logEvent('tengu_stdin_interactive', {})
|
||||
}
|
||||
const fpsTracker = new FpsTracker();
|
||||
const stats = createStatsStore();
|
||||
setStatsStore(stats);
|
||||
|
||||
const fpsTracker = new FpsTracker()
|
||||
const stats = createStatsStore()
|
||||
setStatsStore(stats)
|
||||
|
||||
// Bench mode: when set, append per-frame phase timings as JSONL for
|
||||
// offline analysis by bench/repl-scroll.ts. Captures the full TUI
|
||||
// render pipeline (yoga → screen buffer → diff → optimize → stdout)
|
||||
// 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 {
|
||||
getFpsMetrics: () => fpsTracker.getMetrics(),
|
||||
stats,
|
||||
renderOptions: {
|
||||
...baseOptions,
|
||||
onFrame: event => {
|
||||
fpsTracker.record(event.durationMs);
|
||||
stats.observe('frame_duration_ms', event.durationMs);
|
||||
fpsTracker.record(event.durationMs)
|
||||
stats.observe('frame_duration_ms', event.durationMs)
|
||||
if (frameTimingLogPath && event.phases) {
|
||||
// Bench-only env-var-gated path: sync write so no frames dropped
|
||||
// on abrupt exit. ~100 bytes at ≤60fps is negligible. rss/cpu are
|
||||
// single syscalls; cpu is cumulative — bench side computes delta.
|
||||
const line =
|
||||
// eslint-disable-next-line custom-rules/no-direct-json-operations -- tiny object, hot bench path
|
||||
JSON.stringify({
|
||||
total: event.durationMs,
|
||||
...event.phases,
|
||||
rss: process.memoryUsage.rss(),
|
||||
cpu: process.cpuUsage()
|
||||
}) + '\n';
|
||||
// eslint-disable-next-line custom-rules/no-direct-json-operations -- tiny object, hot bench path
|
||||
JSON.stringify({
|
||||
total: event.durationMs,
|
||||
...event.phases,
|
||||
rss: process.memoryUsage.rss(),
|
||||
cpu: process.cpuUsage(),
|
||||
}) + '\n'
|
||||
// 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 —
|
||||
// DEC 2026 buffers between BSU/ESU so clear+redraw is atomic.
|
||||
if (isSynchronizedOutputSupported()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
for (const flicker of event.flickers) {
|
||||
if (flicker.reason === 'resize') {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
const now = Date.now();
|
||||
const now = Date.now()
|
||||
if (now - lastFlickerTime < 1000) {
|
||||
logEvent('tengu_flicker', {
|
||||
desiredHeight: flicker.desiredHeight,
|
||||
actualHeight: flicker.availableHeight,
|
||||
reason: flicker.reason
|
||||
} as unknown as Record<string, boolean | number | undefined>);
|
||||
reason: flicker.reason,
|
||||
} as unknown as Record<string, boolean | number | undefined>)
|
||||
}
|
||||
lastFlickerTime = now;
|
||||
lastFlickerTime = now
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user