diff --git a/scripts/defines.ts b/scripts/defines.ts index 33ed2b564..d339b0eb3 100644 --- a/scripts/defines.ts +++ b/scripts/defines.ts @@ -7,7 +7,7 @@ */ export function getMacroDefines(): Record { return { - "MACRO.VERSION": JSON.stringify("2.1.888"), + "MACRO.VERSION": JSON.stringify("4.0.1"), "MACRO.BUILD_TIME": JSON.stringify(new Date().toISOString()), "MACRO.FEEDBACK_CHANNEL": JSON.stringify(""), "MACRO.ISSUES_EXPLAINER": JSON.stringify(""), diff --git a/src/components/LogoV2/Clawd.tsx b/src/components/LogoV2/Clawd.tsx index 6969466bc..8367f7ff6 100644 --- a/src/components/LogoV2/Clawd.tsx +++ b/src/components/LogoV2/Clawd.tsx @@ -1,98 +1,21 @@ -import * as React from 'react' -import { Box, Text } from '@anthropic/ink' -import { env } from '../../utils/env.js' +import * as React from 'react'; +import { Box, Text } from '@anthropic/ink'; -export type ClawdPose = - | 'default' - | 'arms-up' // both arms raised (used during jump) - | 'look-left' // both pupils shifted left - | 'look-right' // both pupils shifted right +export type ClawdPose = 'default' | 'arms-up' | 'look-left' | 'look-right'; type Props = { - pose?: ClawdPose -} + pose?: ClawdPose; +}; -// Standard-terminal pose fragments. Each row is split into segments so we can -// vary only the parts that change (eyes, arms) while keeping the body/bg spans -// stable. All poses end up 9 cols wide. -// -// arms-up: the row-2 arm shapes (▝▜ / ▛▘) move to row 1 as their -// bottom-heavy mirrors (▗▟ / ▙▖) — same silhouette, one row higher. -// -// look-* use top-quadrant eye chars (▙/▟) so both eyes change from the -// default (▛/▜, bottom pupils) — otherwise only one eye would appear to move. -type Segments = { - /** row 1 left (no bg): optional raised arm + side */ - r1L: string - /** row 1 eyes (with bg): left-eye, forehead, right-eye */ - r1E: string - /** row 1 right (no bg): side + optional raised arm */ - r1R: string - /** row 2 left (no bg): arm + body curve */ - r2L: string - /** row 2 right (no bg): body curve + arm */ - r2R: string -} - -const POSES: Record = { - default: { r1L: ' ▐', r1E: '▛███▜', r1R: '▌', r2L: '▝▜', r2R: '▛▘' }, - 'look-left': { r1L: ' ▐', r1E: '▟███▟', r1R: '▌', r2L: '▝▜', r2R: '▛▘' }, - 'look-right': { r1L: ' ▐', r1E: '▙███▙', r1R: '▌', r2L: '▝▜', r2R: '▛▘' }, - 'arms-up': { r1L: '▗▟', r1E: '▛███▜', r1R: '▙▖', r2L: ' ▜', r2R: '▛ ' }, -} - -// Apple Terminal uses a bg-fill trick (see below), so only eye poses make -// sense. Arm poses fall back to default. -const APPLE_EYES: Record = { - default: ' ▗ ▖ ', - 'look-left': ' ▘ ▘ ', - 'look-right': ' ▝ ▝ ', - 'arms-up': ' ▗ ▖ ', -} +// CoStrict ASCII art 大字标志,两行块字符风格: +// █▀▀ █▀█ █▀ ▀█▀ █▀█ █ █▀▀ ▀█▀ +// █▄▄ █▄█ ▄█ █ █▀▄ █ █▄▄ █ export function Clawd({ pose = 'default' }: Props = {}): React.ReactNode { - if (env.terminal === 'Apple_Terminal') { - return - } - const p = POSES[pose] - return ( - - - {p.r1L} - - {p.r1E} - - {p.r1R} - - - {p.r2L} - - █████ - - {p.r2R} - - - {' '}▘▘ ▝▝{' '} - - - ) -} - -function AppleTerminalClawd({ pose }: { pose: ClawdPose }): React.ReactNode { - // Apple's Terminal renders vertical space between chars by default. - // It does NOT render vertical space between background colors - // so we use background color to draw the main shape. return ( - - - - {APPLE_EYES[pose]} - - - - {' '.repeat(7)} - ▘▘ ▝▝ + {'█▀▀ █▀█ █▀ ▀█▀ █▀█ █ █▀▀ ▀█▀'} + {'█▄▄ █▄█ ▄█ █ █▀▄ █ █▄▄ █ '} - ) + ); } diff --git a/src/components/LogoV2/CondensedLogo.tsx b/src/components/LogoV2/CondensedLogo.tsx index eb048ec2d..13419d62c 100644 --- a/src/components/LogoV2/CondensedLogo.tsx +++ b/src/components/LogoV2/CondensedLogo.tsx @@ -1,83 +1,71 @@ -import * as React from 'react' -import { type ReactNode, useEffect } from 'react' -import { useMainLoopModel } from '../../hooks/useMainLoopModel.js' -import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { Box, Text, stringWidth } from '@anthropic/ink' -import { useAppState } from '../../state/AppState.js' -import { getEffortSuffix } from '../../utils/effort.js' -import { truncate } from '../../utils/format.js' -import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js' -import { - formatModelAndBilling, - getLogoDisplayData, - truncatePath, -} from '../../utils/logoV2Utils.js' -import { renderModelSetting } from '../../utils/model/model.js' -import { OffscreenFreeze } from '../OffscreenFreeze.js' -import { AnimatedClawd } from './AnimatedClawd.js' -import { Clawd } from './Clawd.js' -import { - GuestPassesUpsell, - incrementGuestPassesSeenCount, - useShowGuestPassesUpsell, -} from './GuestPassesUpsell.js' +import * as React from 'react'; +import { type ReactNode, useEffect } from 'react'; +import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; +import { useTerminalSize } from '../../hooks/useTerminalSize.js'; +import { Box, Text, stringWidth } from '@anthropic/ink'; +import { useAppState } from '../../state/AppState.js'; +import { getEffortSuffix } from '../../utils/effort.js'; +import { truncate } from '../../utils/format.js'; +import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; +import { formatModelAndBilling, getLogoDisplayData, truncatePath } from '../../utils/logoV2Utils.js'; +import { renderModelSetting } from '../../utils/model/model.js'; +import { OffscreenFreeze } from '../OffscreenFreeze.js'; +import { AnimatedClawd } from './AnimatedClawd.js'; +import { Clawd } from './Clawd.js'; +import { GuestPassesUpsell, incrementGuestPassesSeenCount, useShowGuestPassesUpsell } from './GuestPassesUpsell.js'; import { incrementOverageCreditUpsellSeenCount, OverageCreditUpsell, useShowOverageCreditUpsell, -} from './OverageCreditUpsell.js' +} from './OverageCreditUpsell.js'; export function CondensedLogo(): ReactNode { - const { columns } = useTerminalSize() - const agent = useAppState(s => s.agent) - const effortValue = useAppState(s => s.effortValue) - const model = useMainLoopModel() - const modelDisplayName = renderModelSetting(model) - const { version, cwd, billingType, agentName: agentNameFromSettings } = getLogoDisplayData() + const { columns } = useTerminalSize(); + const agent = useAppState(s => s.agent); + const effortValue = useAppState(s => s.effortValue); + const model = useMainLoopModel(); + const modelDisplayName = renderModelSetting(model); + const { version, cwd, billingType, agentName: agentNameFromSettings } = getLogoDisplayData(); // Prefer AppState.agent (set from --agent CLI flag) over settings - const agentName = agent ?? agentNameFromSettings - const showGuestPassesUpsell = useShowGuestPassesUpsell() - const showOverageCreditUpsell = useShowOverageCreditUpsell() + const agentName = agent ?? agentNameFromSettings; + const showGuestPassesUpsell = useShowGuestPassesUpsell(); + const showOverageCreditUpsell = useShowOverageCreditUpsell(); useEffect(() => { if (showGuestPassesUpsell) { - incrementGuestPassesSeenCount() + incrementGuestPassesSeenCount(); } - }, [showGuestPassesUpsell]) + }, [showGuestPassesUpsell]); useEffect(() => { if (showOverageCreditUpsell && !showGuestPassesUpsell) { - incrementOverageCreditUpsellSeenCount() + incrementOverageCreditUpsellSeenCount(); } - }, [showOverageCreditUpsell, showGuestPassesUpsell]) + }, [showOverageCreditUpsell, showGuestPassesUpsell]); // Calculate available width for text content // Account for: condensed clawd width (11 chars) + gap (2) + padding (2) = 15 chars - const textWidth = Math.max(columns - 15, 20) + const textWidth = Math.max(columns - 15, 20); - // Truncate version to fit within available width, accounting for "Claude Code v" prefix - const versionPrefix = 'Claude Code v' - const truncatedVersion = truncate( - version, - Math.max(textWidth - versionPrefix.length, 6), - ) + // Truncate version to fit within available width, accounting for "CoStrict v" prefix + const versionPrefix = 'CoStrict v'; + const truncatedVersion = truncate(version, Math.max(textWidth - versionPrefix.length, 6)); - const effortSuffix = getEffortSuffix(model, effortValue) - const { shouldSplit, truncatedModel, truncatedBilling } = - formatModelAndBilling( - modelDisplayName + effortSuffix, - billingType, - textWidth, - ) + const effortSuffix = getEffortSuffix(model, effortValue); + const { shouldSplit, truncatedModel, truncatedBilling } = formatModelAndBilling( + modelDisplayName + effortSuffix, + billingType, + textWidth, + ); // Truncate path, accounting for agent name if present - const separator = ' · ' - const atPrefix = '@' + const separator = ' · '; + const atPrefix = '@'; const cwdAvailableWidth = agentName ? textWidth - atPrefix.length - stringWidth(agentName) - separator.length - : textWidth - const truncatedCwd = truncatePath(cwd, Math.max(cwdAvailableWidth, 10)) + : textWidth; + const truncatedCwd = truncatePath(cwd, Math.max(cwdAvailableWidth, 10)); // OffscreenFreeze: the logo sits at the top of the message list and is the // first thing to enter scrollback. useMainLoopModel() subscribes to model @@ -85,34 +73,40 @@ export function CondensedLogo(): ReactNode { // of which changing while in scrollback would force a full terminal reset. return ( - - {isFullscreenEnvEnabled() ? : } + + {isFullscreenEnvEnabled() ? : } - {/* Info */} - - - Claude Code{' '} - v{truncatedVersion} - - {shouldSplit ? ( - <> - {truncatedModel} - {truncatedBilling} - - ) : ( - - {truncatedModel} · {truncatedBilling} + {/* Info */} + + + + CoStrict + {' '} + v{truncatedVersion} - )} - - {agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd} - - {showGuestPassesUpsell && } - {!showGuestPassesUpsell && showOverageCreditUpsell && ( - - )} - + {shouldSplit ? ( + <> + {truncatedModel} + {truncatedBilling} + + ) : ( + + {truncatedModel} · {truncatedBilling} + + )} + {agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd} + {showGuestPassesUpsell && } + {!showGuestPassesUpsell && showOverageCreditUpsell && } + - ) + ); } diff --git a/src/components/LogoV2/Feed.tsx b/src/components/LogoV2/Feed.tsx index be849bfd7..e333496a9 100644 --- a/src/components/LogoV2/Feed.tsx +++ b/src/components/LogoV2/Feed.tsx @@ -1,69 +1,61 @@ -import * as React from 'react' -import { Box, Text, stringWidth } from '@anthropic/ink' -import { truncate } from '../../utils/format.js' +import * as React from 'react'; +import { Box, Text, stringWidth } from '@anthropic/ink'; +import { truncate } from '../../utils/format.js'; export type FeedLine = { - text: string - timestamp?: string -} + text: string; + timestamp?: string; +}; export type FeedConfig = { - title: string - lines: FeedLine[] - footer?: string - emptyMessage?: string - customContent?: { content: React.ReactNode; width: number } -} + title: string; + lines: FeedLine[]; + footer?: string; + emptyMessage?: string; + customContent?: { content: React.ReactNode; width: number }; +}; type FeedProps = { - config: FeedConfig - actualWidth: number -} + config: FeedConfig; + actualWidth: number; +}; export function calculateFeedWidth(config: FeedConfig): number { - const { title, lines, footer, emptyMessage, customContent } = config + const { title, lines, footer, emptyMessage, customContent } = config; - let maxWidth = stringWidth(title) + let maxWidth = stringWidth(title); if (customContent !== undefined) { - maxWidth = Math.max(maxWidth, customContent.width) + maxWidth = Math.max(maxWidth, customContent.width); } else if (lines.length === 0 && emptyMessage) { - maxWidth = Math.max(maxWidth, stringWidth(emptyMessage)) + maxWidth = Math.max(maxWidth, stringWidth(emptyMessage)); } else { - const gap = ' ' - const maxTimestampWidth = Math.max( - 0, - ...lines.map(line => (line.timestamp ? stringWidth(line.timestamp) : 0)), - ) + const gap = ' '; + const maxTimestampWidth = Math.max(0, ...lines.map(line => (line.timestamp ? stringWidth(line.timestamp) : 0))); for (const line of lines) { - const timestampWidth = maxTimestampWidth > 0 ? maxTimestampWidth : 0 - const lineWidth = - stringWidth(line.text) + - (timestampWidth > 0 ? timestampWidth + gap.length : 0) - maxWidth = Math.max(maxWidth, lineWidth) + const timestampWidth = maxTimestampWidth > 0 ? maxTimestampWidth : 0; + const lineWidth = stringWidth(line.text) + (timestampWidth > 0 ? timestampWidth + gap.length : 0); + maxWidth = Math.max(maxWidth, lineWidth); } } if (footer) { - maxWidth = Math.max(maxWidth, stringWidth(footer)) + maxWidth = Math.max(maxWidth, stringWidth(footer)); } - return maxWidth + return maxWidth; } export function Feed({ config, actualWidth }: FeedProps): React.ReactNode { - const { title, lines, footer, emptyMessage, customContent } = config + const { title, lines, footer, emptyMessage, customContent } = config; - const gap = ' ' - const maxTimestampWidth = Math.max( - 0, - ...lines.map(line => (line.timestamp ? stringWidth(line.timestamp) : 0)), - ) + const gap = ' '; + const maxTimestampWidth = Math.max(0, ...lines.map(line => (line.timestamp ? stringWidth(line.timestamp) : 0))); return ( - + {title} {customContent ? ( @@ -80,25 +72,19 @@ export function Feed({ config, actualWidth }: FeedProps): React.ReactNode { ) : ( <> {lines.map((line, index) => { - const textWidth = Math.max( - 10, - actualWidth - - (maxTimestampWidth > 0 ? maxTimestampWidth + gap.length : 0), - ) + const textWidth = Math.max(10, actualWidth - (maxTimestampWidth > 0 ? maxTimestampWidth + gap.length : 0)); return ( {maxTimestampWidth > 0 && ( <> - - {(line.timestamp || '').padEnd(maxTimestampWidth)} - + {(line.timestamp || '').padEnd(maxTimestampWidth)} {gap} )} {truncate(line.text, textWidth)} - ) + ); })} {footer && ( @@ -108,5 +94,5 @@ export function Feed({ config, actualWidth }: FeedProps): React.ReactNode { )} - ) + ); } diff --git a/src/components/LogoV2/FeedColumn.tsx b/src/components/LogoV2/FeedColumn.tsx index 4c6ae84f5..a774c1d16 100644 --- a/src/components/LogoV2/FeedColumn.tsx +++ b/src/components/LogoV2/FeedColumn.tsx @@ -1,32 +1,27 @@ -import * as React from 'react' -import { Box } from '@anthropic/ink' -import { Divider } from '@anthropic/ink' -import type { FeedConfig } from './Feed.js' -import { calculateFeedWidth, Feed } from './Feed.js' +import * as React from 'react'; +import { Box } from '@anthropic/ink'; +import { Divider } from '@anthropic/ink'; +import type { FeedConfig } from './Feed.js'; +import { calculateFeedWidth, Feed } from './Feed.js'; type FeedColumnProps = { - feeds: FeedConfig[] - maxWidth: number -} + feeds: FeedConfig[]; + maxWidth: number; +}; -export function FeedColumn({ - feeds, - maxWidth, -}: FeedColumnProps): React.ReactNode { - const feedWidths = feeds.map(feed => calculateFeedWidth(feed)) - const maxOfAllFeeds = Math.max(...feedWidths) - const actualWidth = Math.min(maxOfAllFeeds, maxWidth) +export function FeedColumn({ feeds, maxWidth }: FeedColumnProps): React.ReactNode { + const feedWidths = feeds.map(feed => calculateFeedWidth(feed)); + const maxOfAllFeeds = Math.max(...feedWidths); + const actualWidth = Math.min(maxOfAllFeeds, maxWidth); return ( {feeds.map((feed, index) => ( - {index < feeds.length - 1 && ( - - )} + {index < feeds.length - 1 && } ))} - ) + ); } diff --git a/src/components/LogoV2/LogoV2.tsx b/src/components/LogoV2/LogoV2.tsx index c7dcf4139..7ae609bed 100644 --- a/src/components/LogoV2/LogoV2.tsx +++ b/src/components/LogoV2/LogoV2.tsx @@ -1,7 +1,7 @@ // 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 * as React from 'react'; +import { Box, Text, color, stringWidth } from '@anthropic/ink'; +import { useTerminalSize } from '../../hooks/useTerminalSize.js'; import { getLayoutMode, calculateLayoutDimensions, @@ -11,46 +11,39 @@ import { 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' +} 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' +} 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' +} 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 @@ -61,128 +54,98 @@ import { feature } from 'bun:bundle' const ChannelsNoticeModule = feature('KAIROS') || feature('KAIROS_CHANNELS') ? (require('./ChannelsNotice.js') as typeof import('./ChannelsNotice.js')) - : null + : null; /* eslint-enable @typescript-eslint/no-require-imports */ -import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js' -import { - useShowGuestPassesUpsell, - incrementGuestPassesSeenCount, -} from './GuestPassesUpsell.js' +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' +} 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 +const LEFT_PANEL_MAX_WIDTH = 50; export function LogoV2(): React.ReactNode { - const activities = getRecentActivitySync() - const username = getGlobalConfig().oauthAccount?.displayName ?? '' + 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 { 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() + const config = getGlobalConfig(); - let changelog: string[] + let changelog: string[]; try { - changelog = getRecentReleaseNotesSync(3) + changelog = getRecentReleaseNotesSync(3); } catch { - changelog = [] + 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 + 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, - ) + : announcements[Math.floor(Math.random() * announcements.length)]; + }); + const { hasReleaseNotes } = checkForReleaseNotesSync(config.lastReleaseNotesSeen); useEffect(() => { - const currentConfig = getGlobalConfig() + const currentConfig = getGlobalConfig(); if (currentConfig.lastReleaseNotesSeen === MACRO.VERSION) { - return + return; } saveGlobalConfig(current => { - if (current.lastReleaseNotesSeen === MACRO.VERSION) return current - return { ...current, lastReleaseNotesSeen: MACRO.VERSION } - }) + if (current.lastReleaseNotesSeen === MACRO.VERSION) return current; + return { ...current, lastReleaseNotesSeen: MACRO.VERSION }; + }); if (showOnboarding) { - incrementProjectOnboardingSeenCount() + incrementProjectOnboardingSeenCount(); } - }, [config, showOnboarding]) + }, [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) + const isCondensedMode = !hasReleaseNotes && !showOnboarding && !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO); useEffect(() => { if (showGuestPassesUpsell && !showOnboarding && !isCondensedMode) { - incrementGuestPassesSeenCount() + incrementGuestPassesSeenCount(); } - }, [showGuestPassesUpsell, showOnboarding, isCondensedMode]) + }, [showGuestPassesUpsell, showOnboarding, isCondensedMode]); useEffect(() => { - if ( - showOverageCreditUpsell && - !showOnboarding && - !showGuestPassesUpsell && - !isCondensedMode - ) { - incrementOverageCreditUpsellSeenCount() + if (showOverageCreditUpsell && !showOnboarding && !showGuestPassesUpsell && !isCondensedMode) { + incrementOverageCreditUpsellSeenCount(); } - }, [ - showOverageCreditUpsell, - showOnboarding, - showGuestPassesUpsell, - isCondensedMode, - ]) + }, [showOverageCreditUpsell, showOnboarding, showGuestPassesUpsell, isCondensedMode]); - const model = useMainLoopModel() - const fullModelDisplayName = renderModelSetting(model) - const { - version, - cwd, - billingType, - agentName: agentNameFromSettings, - } = getLogoDisplayData() + 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 + 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, - ) + 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) - ) { + if (!hasReleaseNotes && !showOnboarding && !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO)) { return ( <> @@ -192,17 +155,13 @@ export function LogoV2(): React.ReactNode { {isDebugMode() && ( Debug mode enabled - - Logging to: {isDebugToStdErr() ? 'stderr' : getDebugLogPath()} - + Logging to: {isDebugToStdErr() ? 'stderr' : getDebugLogPath()} )} {process.env.CLAUDE_CODE_TMUX_SESSION && ( - - tmux session: {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})` @@ -213,9 +172,7 @@ export function LogoV2(): React.ReactNode { {announcement && ( {!process.env.IS_DEMO && config.oauthAccount?.organizationName && ( - - Message from {config.oauthAccount.organizationName}: - + Message from {config.oauthAccount.organizationName}: )} {announcement} @@ -228,51 +185,41 @@ export function LogoV2(): React.ReactNode { {process.env.USER_TYPE === 'ant' && !process.env.DEMO_VERSION && ( [ANT-ONLY] Logs: - - API calls: {getDisplayPath(getDumpPromptsPath())} - - - Debug logs: {getDisplayPath(getDebugLogPath())} - + API calls: {getDisplayPath(getDumpPromptsPath())} + Debug logs: {getDisplayPath(getDebugLogPath())} {isDetailedProfilingEnabled() && ( - - Startup Perf: {getDisplayPath(getStartupPerfLogPath())} - + Startup Perf: {getDisplayPath(getStartupPerfLogPath())} )} )} {process.env.USER_TYPE === 'ant' && } {process.env.USER_TYPE === 'ant' && } - ) + ); } // Calculate layout and display values - const layoutMode = getLayoutMode(columns) + 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 ') + const userTheme = resolveThemeSetting(getGlobalConfig().theme); + const borderTitle = ` ${color('claudeBlue_FOR_SYSTEM_SPINNER', userTheme)('CoStrict')} ${color('inactive', userTheme)(`v${version}`)} `; + const compactBorderTitle = color('claudeBlue_FOR_SYSTEM_SPINNER', userTheme)(' CoStrict '); // Early return for compact mode if (layoutMode === 'compact') { - const layoutWidth = 4 // border + padding - let welcomeMessage = formatWelcomeMessage(username) + const layoutWidth = 4; // border + padding + let welcomeMessage = formatWelcomeMessage(username); if (stringWidth(welcomeMessage) > columns - layoutWidth) { - welcomeMessage = formatWelcomeMessage(null) + welcomeMessage = formatWelcomeMessage(null); } // Calculate cwd width accounting for agent name if present - const separator = ' · ' - const atPrefix = '@' + 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)) + ? 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. @@ -282,7 +229,7 @@ export function LogoV2(): React.ReactNode { {modelDisplayName} {billingType} - - {agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd} - + {agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd} @@ -310,45 +255,32 @@ export function LogoV2(): React.ReactNode { {ChannelsNoticeModule && } {showSandboxStatus && ( - - Your bash commands will be sandboxed. Disable with /sandbox. - + Your bash commands will be sandboxed. Disable with /sandbox. )} {process.env.USER_TYPE === 'ant' && } {process.env.USER_TYPE === 'ant' && } - ) + ); } - const welcomeMessage = formatWelcomeMessage(username) + const welcomeMessage = formatWelcomeMessage(username); const modelLine = !process.env.IS_DEMO && config.oauthAccount?.organizationName ? `${modelDisplayName} · ${billingType} · ${config.oauthAccount.organizationName}` - : `${modelDisplayName} · ${billingType}` + : `${modelDisplayName} · ${billingType}`; // Calculate cwd width accounting for agent name if present - const cwdSeparator = ' · ' - const cwdAtPrefix = '@' + 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, - ) + ? 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, - ) + const { leftWidth, rightWidth } = calculateLayoutDimensions(columns, layoutMode, optimalLeftWidth); return ( <> @@ -356,7 +288,7 @@ export function LogoV2(): React.ReactNode { {/* Main content */} - + {/* Left Panel */} @@ -439,17 +355,13 @@ export function LogoV2(): React.ReactNode { {isDebugMode() && ( Debug mode enabled - - Logging to: {isDebugToStdErr() ? 'stderr' : getDebugLogPath()} - + Logging to: {isDebugToStdErr() ? 'stderr' : getDebugLogPath()} )} {process.env.CLAUDE_CODE_TMUX_SESSION && ( - - tmux session: {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})` @@ -460,18 +372,14 @@ export function LogoV2(): React.ReactNode { {announcement && ( {!process.env.IS_DEMO && config.oauthAccount?.organizationName && ( - - Message from {config.oauthAccount.organizationName}: - + Message from {config.oauthAccount.organizationName}: )} {announcement} )} {showSandboxStatus && ( - - Your bash commands will be sandboxed. Disable with /sandbox. - + Your bash commands will be sandboxed. Disable with /sandbox. )} {process.env.USER_TYPE === 'ant' && !process.env.DEMO_VERSION && ( @@ -482,20 +390,15 @@ export function LogoV2(): React.ReactNode { {process.env.USER_TYPE === 'ant' && !process.env.DEMO_VERSION && ( [ANT-ONLY] Logs: - - API calls: {getDisplayPath(getDumpPromptsPath())} - + API calls: {getDisplayPath(getDumpPromptsPath())} Debug logs: {getDisplayPath(getDebugLogPath())} {isDetailedProfilingEnabled() && ( - - Startup Perf: {getDisplayPath(getStartupPerfLogPath())} - + Startup Perf: {getDisplayPath(getStartupPerfLogPath())} )} )} {process.env.USER_TYPE === 'ant' && } {process.env.USER_TYPE === 'ant' && } - ) + ); } - diff --git a/src/components/LogoV2/WelcomeV2.tsx b/src/components/LogoV2/WelcomeV2.tsx index ccbbcbf44..235c7cada 100644 --- a/src/components/LogoV2/WelcomeV2.tsx +++ b/src/components/LogoV2/WelcomeV2.tsx @@ -1,17 +1,15 @@ -import React from 'react' -import { Box, Text, useTheme } from '@anthropic/ink' -import { env } from '../../utils/env.js' +import React from 'react'; +import { Box, Text, useTheme } from '@anthropic/ink'; +import { env } from '../../utils/env.js'; -const WELCOME_V2_WIDTH = 58 +const WELCOME_V2_WIDTH = 58; export function WelcomeV2(): React.ReactNode { - const [theme] = useTheme() - const welcomeMessage = 'Welcome to Claude Code' + const [theme] = useTheme(); + const welcomeMessage = 'Welcome to CoStrict'; if (env.terminal === 'Apple_Terminal') { - return ( - - ) + return ; } if (['light', 'light-daltonized', 'light-ansi'].includes(theme)) { @@ -22,30 +20,14 @@ export function WelcomeV2(): React.ReactNode { {welcomeMessage} v{MACRO.VERSION} - - {'…………………………………………………………………………………………………………………………………………………………'} - - - {' '} - - - {' '} - - - {' '} - - - {' ░░░░░░ '} - - - {' ░░░ ░░░░░░░░░░ '} - - - {' ░░░░░░░░░░░░░░░░░░░ '} - - - {' '} - + {'…………………………………………………………………………………………………………………………………………………………'} + {' '} + {' '} + {' '} + {' ░░░░░░ '} + {' ░░░ ░░░░░░░░░░ '} + {' ░░░░░░░░░░░░░░░░░░░ '} + {' '} {' ░░░░'} {' ██ '} @@ -54,9 +36,7 @@ export function WelcomeV2(): React.ReactNode { {' ░░░░░░░░░░'} {' ██▒▒██ '} - - {' ▒▒ ██ ▒'} - + {' ▒▒ ██ ▒'} {' '} █████████ @@ -81,7 +61,7 @@ export function WelcomeV2(): React.ReactNode { - ) + ); } return ( @@ -91,41 +71,21 @@ export function WelcomeV2(): React.ReactNode { {welcomeMessage} v{MACRO.VERSION} - - {'…………………………………………………………………………………………………………………………………………………………'} - - - {' '} - - - {' * █████▓▓░ '} - - - {' * ███▓░ ░░ '} - - - {' ░░░░░░ ███▓░ '} - - - {' ░░░ ░░░░░░░░░░ ███▓░ '} - + {'…………………………………………………………………………………………………………………………………………………………'} + {' '} + {' * █████▓▓░ '} + {' * ███▓░ ░░ '} + {' ░░░░░░ ███▓░ '} + {' ░░░ ░░░░░░░░░░ ███▓░ '} {' ░░░░░░░░░░░░░░░░░░░ '} * {' ██▓░░ ▓ '} - - {' ░▓▓███▓▓░ '} - - - {' * ░░░░ '} - - - {' ░░░░░░░░ '} - - - {' ░░░░░░░░░░░░░░░░ '} - + {' ░▓▓███▓▓░ '} + {' * ░░░░ '} + {' ░░░░░░░░ '} + {' ░░░░░░░░░░░░░░░░ '} {' '} █████████ @@ -152,21 +112,16 @@ export function WelcomeV2(): React.ReactNode { - ) + ); } type AppleTerminalWelcomeV2Props = { - theme: string - welcomeMessage: string -} + theme: string; + welcomeMessage: string; +}; -function AppleTerminalWelcomeV2({ - theme, - welcomeMessage, -}: AppleTerminalWelcomeV2Props): React.ReactNode { - const isLightTheme = ['light', 'light-daltonized', 'light-ansi'].includes( - theme, - ) +function AppleTerminalWelcomeV2({ theme, welcomeMessage }: AppleTerminalWelcomeV2Props): React.ReactNode { + const isLightTheme = ['light', 'light-daltonized', 'light-ansi'].includes(theme); if (isLightTheme) { return ( @@ -176,30 +131,14 @@ function AppleTerminalWelcomeV2({ {welcomeMessage} v{MACRO.VERSION} - - {'…………………………………………………………………………………………………………………………………………………………'} - - - {' '} - - - {' '} - - - {' '} - - - {' ░░░░░░ '} - - - {' ░░░ ░░░░░░░░░░ '} - - - {' ░░░░░░░░░░░░░░░░░░░ '} - - - {' '} - + {'…………………………………………………………………………………………………………………………………………………………'} + {' '} + {' '} + {' '} + {' ░░░░░░ '} + {' ░░░ ░░░░░░░░░░ '} + {' ░░░░░░░░░░░░░░░░░░░ '} + {' '} {' ░░░░'} {' ██ '} @@ -208,12 +147,8 @@ function AppleTerminalWelcomeV2({ {' ░░░░░░░░░░'} {' ██▒▒██ '} - - {' ▒▒ ██ ▒'} - - - {' ▒▒░░▒▒ ▒ ▒▒'} - + {' ▒▒ ██ ▒'} + {' ▒▒░░▒▒ ▒ ▒▒'} {' '} @@ -242,7 +177,7 @@ function AppleTerminalWelcomeV2({ - ) + ); } return ( @@ -252,41 +187,21 @@ function AppleTerminalWelcomeV2({ {welcomeMessage} v{MACRO.VERSION} - - {'…………………………………………………………………………………………………………………………………………………………'} - - - {' '} - - - {' * █████▓▓░ '} - - - {' * ███▓░ ░░ '} - - - {' ░░░░░░ ███▓░ '} - - - {' ░░░ ░░░░░░░░░░ ███▓░ '} - + {'…………………………………………………………………………………………………………………………………………………………'} + {' '} + {' * █████▓▓░ '} + {' * ███▓░ ░░ '} + {' ░░░░░░ ███▓░ '} + {' ░░░ ░░░░░░░░░░ ███▓░ '} {' ░░░░░░░░░░░░░░░░░░░ '} * {' ██▓░░ ▓ '} - - {' ░▓▓███▓▓░ '} - - - {' * ░░░░ '} - - - {' ░░░░░░░░ '} - - - {' ░░░░░░░░░░░░░░░░ '} - + {' ░▓▓███▓▓░ '} + {' * ░░░░ '} + {' ░░░░░░░░ '} + {' ░░░░░░░░░░░░░░░░ '} {' '} * @@ -322,5 +237,5 @@ function AppleTerminalWelcomeV2({ - ) + ); } diff --git a/src/components/LogoV2/costrict-logo.png b/src/components/LogoV2/costrict-logo.png new file mode 100644 index 000000000..12b72ecfc Binary files /dev/null and b/src/components/LogoV2/costrict-logo.png differ