// 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' && }
>
)
}