// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered import * as React from 'react' import { Box, Text, color, stringWidth } from '@anthropic/ink' import { useTerminalSize } from '../../hooks/useTerminalSize.js' import { getLayoutMode, calculateLayoutDimensions, calculateOptimalLeftWidth, formatWelcomeMessage, truncatePath, getRecentActivitySync, getRecentReleaseNotesSync, getLogoDisplayData, } from '../../utils/logoV2Utils.js' import { truncate } from '../../utils/format.js' import { getDisplayPath } from '../../utils/file.js' import { Clawd } from './Clawd.js' import { FeedColumn } from './FeedColumn.js' import { createRecentActivityFeed, createWhatsNewFeed, createProjectOnboardingFeed, createGuestPassesFeed, } from './feedConfigs.js' import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js' import { resolveThemeSetting } from 'src/utils/systemTheme.js' import { getInitialSettings } from 'src/utils/settings/settings.js' import { isDebugMode, isDebugToStdErr, getDebugLogPath, } from 'src/utils/debug.js' import { useEffect, useState } from 'react' import { getSteps, shouldShowProjectOnboarding, incrementProjectOnboardingSeenCount, } from '../../projectOnboardingState.js' import { CondensedLogo } from './CondensedLogo.js' import { OffscreenFreeze } from '../OffscreenFreeze.js' import { checkForReleaseNotesSync } from '../../utils/releaseNotes.js' import { getDumpPromptsPath } from 'src/services/api/dumpPrompts.js' import { isEnvTruthy } from 'src/utils/envUtils.js' import { getStartupPerfLogPath, isDetailedProfilingEnabled, } from 'src/utils/startupProfiler.js' import { EmergencyTip } from './EmergencyTip.js' import { VoiceModeNotice } from './VoiceModeNotice.js' import { Opus1mMergeNotice } from './Opus1mMergeNotice.js' import { GateOverridesWarning } from './GateOverridesWarning.js' import { ExperimentEnrollmentNotice } from './ExperimentEnrollmentNotice.js' import { feature } from 'bun:bundle' // Conditional require so ChannelsNotice.tsx tree-shakes when both flags are // false. A module-scope helper component inside a feature() ternary does NOT // tree-shake (docs/feature-gating.md); the require pattern eliminates the // whole file. VoiceModeNotice uses the unsafe helper pattern but VOICE_MODE // is external: true so it's moot there. /* eslint-disable @typescript-eslint/no-require-imports */ const ChannelsNoticeModule = feature('KAIROS') || feature('KAIROS_CHANNELS') ? (require('./ChannelsNotice.js') as typeof import('./ChannelsNotice.js')) : null /* eslint-enable @typescript-eslint/no-require-imports */ import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js' import { useShowGuestPassesUpsell, incrementGuestPassesSeenCount, } from './GuestPassesUpsell.js' import { useShowOverageCreditUpsell, incrementOverageCreditUpsellSeenCount, createOverageCreditFeed, } from './OverageCreditUpsell.js' import { plural } from '../../utils/stringUtils.js' import { useAppState } from '../../state/AppState.js' import { getEffortSuffix } from '../../utils/effort.js' import { useMainLoopModel } from '../../hooks/useMainLoopModel.js' import { renderModelSetting } from '../../utils/model/model.js' const LEFT_PANEL_MAX_WIDTH = 50 export function LogoV2(): React.ReactNode { const activities = getRecentActivitySync() const username = getGlobalConfig().oauthAccount?.displayName ?? '' const { columns } = useTerminalSize() const showOnboarding = shouldShowProjectOnboarding() const showSandboxStatus = SandboxManager.isSandboxingEnabled() const showGuestPassesUpsell = useShowGuestPassesUpsell() const showOverageCreditUpsell = useShowOverageCreditUpsell() const agent = useAppState(s => s.agent) const effortValue = useAppState(s => s.effortValue) const config = getGlobalConfig() let changelog: string[] try { changelog = getRecentReleaseNotesSync(3) } catch { changelog = [] } // Get company announcements and select one: // - First startup (numStartups === 1): show first announcement // - All other startups: randomly select from announcements const [announcement] = useState(() => { const announcements = getInitialSettings().companyAnnouncements if (!announcements || announcements.length === 0) return undefined return config.numStartups === 1 ? announcements[0] : announcements[Math.floor(Math.random() * announcements.length)] }) const { hasReleaseNotes } = checkForReleaseNotesSync( config.lastReleaseNotesSeen, ) useEffect(() => { const currentConfig = getGlobalConfig() if (currentConfig.lastReleaseNotesSeen === MACRO.VERSION) { return } saveGlobalConfig(current => { if (current.lastReleaseNotesSeen === MACRO.VERSION) return current return { ...current, lastReleaseNotesSeen: MACRO.VERSION } }) if (showOnboarding) { incrementProjectOnboardingSeenCount() } }, [config, showOnboarding]) // In condensed mode (early-return below renders ), // CondensedLogo's own useEffect handles the impression count. Skipping // here avoids double-counting since hooks fire before the early return. const isCondensedMode = !hasReleaseNotes && !showOnboarding && !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO) useEffect(() => { if (showGuestPassesUpsell && !showOnboarding && !isCondensedMode) { incrementGuestPassesSeenCount() } }, [showGuestPassesUpsell, showOnboarding, isCondensedMode]) useEffect(() => { if ( showOverageCreditUpsell && !showOnboarding && !showGuestPassesUpsell && !isCondensedMode ) { incrementOverageCreditUpsellSeenCount() } }, [ showOverageCreditUpsell, showOnboarding, showGuestPassesUpsell, isCondensedMode, ]) const model = useMainLoopModel() const fullModelDisplayName = renderModelSetting(model) const { version, cwd, billingType, agentName: agentNameFromSettings, } = getLogoDisplayData() // Prefer AppState.agent (set from --agent CLI flag) over settings const agentName = agent ?? agentNameFromSettings // -20 to account for the max length of subscription name " · Claude Enterprise". const effortSuffix = getEffortSuffix(model, effortValue) const modelDisplayName = truncate( fullModelDisplayName + effortSuffix, LEFT_PANEL_MAX_WIDTH - 20, ) // Show condensed logo if no new changelog and not showing onboarding and not forcing full logo if ( !hasReleaseNotes && !showOnboarding && !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO) ) { return ( <> {ChannelsNoticeModule && } {isDebugMode() && ( Debug mode enabled Logging to: {isDebugToStdErr() ? 'stderr' : getDebugLogPath()} )} {process.env.CLAUDE_CODE_TMUX_SESSION && ( tmux session: {process.env.CLAUDE_CODE_TMUX_SESSION} {process.env.CLAUDE_CODE_TMUX_PREFIX_CONFLICTS ? `Detach: ${process.env.CLAUDE_CODE_TMUX_PREFIX} ${process.env.CLAUDE_CODE_TMUX_PREFIX} d (press prefix twice - Claude uses ${process.env.CLAUDE_CODE_TMUX_PREFIX})` : `Detach: ${process.env.CLAUDE_CODE_TMUX_PREFIX} d`} )} {announcement && ( {!process.env.IS_DEMO && config.oauthAccount?.organizationName && ( Message from {config.oauthAccount.organizationName}: )} {announcement} )} {process.env.USER_TYPE === 'ant' && !process.env.DEMO_VERSION && ( Use /issue to report model behavior issues )} {process.env.USER_TYPE === 'ant' && !process.env.DEMO_VERSION && ( [ANT-ONLY] Logs: API calls: {getDisplayPath(getDumpPromptsPath())} Debug logs: {getDisplayPath(getDebugLogPath())} {isDetailedProfilingEnabled() && ( Startup Perf: {getDisplayPath(getStartupPerfLogPath())} )} )} {process.env.USER_TYPE === 'ant' && } {process.env.USER_TYPE === 'ant' && } ) } // Calculate layout and display values const layoutMode = getLayoutMode(columns) const userTheme = resolveThemeSetting(getGlobalConfig().theme) const borderTitle = ` ${color('claude', userTheme)('Claude Code')} ${color('inactive', userTheme)(`v${version}`)} ` const compactBorderTitle = color('claude', userTheme)(' Claude Code ') // Early return for compact mode if (layoutMode === 'compact') { const layoutWidth = 4 // border + padding let welcomeMessage = formatWelcomeMessage(username) if (stringWidth(welcomeMessage) > columns - layoutWidth) { welcomeMessage = formatWelcomeMessage(null) } // Calculate cwd width accounting for agent name if present const separator = ' · ' const atPrefix = '@' const cwdAvailableWidth = agentName ? columns - layoutWidth - atPrefix.length - stringWidth(agentName) - separator.length : columns - layoutWidth const truncatedCwd = truncatePath(cwd, Math.max(cwdAvailableWidth, 10)) // OffscreenFreeze: logo is the first thing to enter scrollback; useMainLoopModel() // subscribes to model changes and getLogoDisplayData() reads cwd/subscription — // any change while in scrollback forces a full reset. return ( <> {welcomeMessage} {modelDisplayName} {billingType} {agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd} {ChannelsNoticeModule && } {showSandboxStatus && ( Your bash commands will be sandboxed. Disable with /sandbox. )} {process.env.USER_TYPE === 'ant' && } {process.env.USER_TYPE === 'ant' && } ) } const welcomeMessage = formatWelcomeMessage(username) const modelLine = !process.env.IS_DEMO && config.oauthAccount?.organizationName ? `${modelDisplayName} · ${billingType} · ${config.oauthAccount.organizationName}` : `${modelDisplayName} · ${billingType}` // Calculate cwd width accounting for agent name if present const cwdSeparator = ' · ' const cwdAtPrefix = '@' const cwdAvailableWidth = agentName ? LEFT_PANEL_MAX_WIDTH - cwdAtPrefix.length - stringWidth(agentName) - cwdSeparator.length : LEFT_PANEL_MAX_WIDTH const truncatedCwd = truncatePath(cwd, Math.max(cwdAvailableWidth, 10)) const cwdLine = agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd const optimalLeftWidth = calculateOptimalLeftWidth( welcomeMessage, cwdLine, modelLine, ) // Calculate layout dimensions const { leftWidth, rightWidth } = calculateLayoutDimensions( columns, layoutMode, optimalLeftWidth, ) return ( <> {/* Main content */} {/* Left Panel */} {welcomeMessage} {modelLine} {cwdLine} {/* Vertical divider */} {layoutMode === 'horizontal' && ( )} {/* Right Panel - Project Onboarding or Recent Activity and What's New */} {layoutMode === 'horizontal' && ( )} {ChannelsNoticeModule && } {isDebugMode() && ( Debug mode enabled Logging to: {isDebugToStdErr() ? 'stderr' : getDebugLogPath()} )} {process.env.CLAUDE_CODE_TMUX_SESSION && ( tmux session: {process.env.CLAUDE_CODE_TMUX_SESSION} {process.env.CLAUDE_CODE_TMUX_PREFIX_CONFLICTS ? `Detach: ${process.env.CLAUDE_CODE_TMUX_PREFIX} ${process.env.CLAUDE_CODE_TMUX_PREFIX} d (press prefix twice - Claude uses ${process.env.CLAUDE_CODE_TMUX_PREFIX})` : `Detach: ${process.env.CLAUDE_CODE_TMUX_PREFIX} d`} )} {announcement && ( {!process.env.IS_DEMO && config.oauthAccount?.organizationName && ( Message from {config.oauthAccount.organizationName}: )} {announcement} )} {showSandboxStatus && ( Your bash commands will be sandboxed. Disable with /sandbox. )} {process.env.USER_TYPE === 'ant' && !process.env.DEMO_VERSION && ( Use /issue to report model behavior issues )} {process.env.USER_TYPE === 'ant' && !process.env.DEMO_VERSION && ( [ANT-ONLY] Logs: API calls: {getDisplayPath(getDumpPromptsPath())} Debug logs: {getDisplayPath(getDebugLogPath())} {isDetailedProfilingEnabled() && ( Startup Perf: {getDisplayPath(getStartupPerfLogPath())} )} )} {process.env.USER_TYPE === 'ant' && } {process.env.USER_TYPE === 'ant' && } ) }