Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
9.8 kB · 350 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351import { getDirectConnectServerUrl, getSessionId } from '../bootstrap/state.js'import { stringWidth } from '../ink/stringWidth.js'import type { LogOption } from '../types/logs.js'import { getSubscriptionName, isClaudeAISubscriber } from './auth.js'import { getCwd } from './cwd.js'import { getDisplayPath } from './file.js'import { truncate, truncateToWidth, truncateToWidthNoEllipsis,} from './format.js'import { getStoredChangelogFromMemory, parseChangelog } from './releaseNotes.js'import { gt } from './semver.js'import { loadMessageLogs } from './sessionStorage.js'import { getInitialSettings } from './settings/settings.js'
// Layout constantsconst MAX_LEFT_WIDTH = 50const MAX_USERNAME_LENGTH = 20const BORDER_PADDING = 4const DIVIDER_WIDTH = 1const CONTENT_PADDING = 2
export type LayoutMode = 'horizontal' | 'compact'
export type LayoutDimensions = { leftWidth: number rightWidth: number totalWidth: number}
/** * Determines the layout mode based on terminal width */export function getLayoutMode(columns: number): LayoutMode { if (columns >= 70) return 'horizontal' return 'compact'}
/** * Calculates layout dimensions for the LogoV2 component */export function calculateLayoutDimensions( columns: number, layoutMode: LayoutMode, optimalLeftWidth: number,): LayoutDimensions { if (layoutMode === 'horizontal') { const leftWidth = optimalLeftWidth const usedSpace = BORDER_PADDING + CONTENT_PADDING + DIVIDER_WIDTH + leftWidth const availableForRight = columns - usedSpace
let rightWidth = Math.max(30, availableForRight) const totalWidth = Math.min( leftWidth + rightWidth + DIVIDER_WIDTH + CONTENT_PADDING, columns - BORDER_PADDING, )
// Recalculate right width if we had to cap the total if (totalWidth < leftWidth + rightWidth + DIVIDER_WIDTH + CONTENT_PADDING) { rightWidth = totalWidth - leftWidth - DIVIDER_WIDTH - CONTENT_PADDING }
return { leftWidth, rightWidth, totalWidth } }
// Vertical mode const totalWidth = Math.min(columns - BORDER_PADDING, MAX_LEFT_WIDTH + 20) return { leftWidth: totalWidth, rightWidth: totalWidth, totalWidth, }}
/** * Calculates optimal left panel width based on content */export function calculateOptimalLeftWidth( welcomeMessage: string, truncatedCwd: string, modelLine: string,): number { const contentWidth = Math.max( stringWidth(welcomeMessage), stringWidth(truncatedCwd), stringWidth(modelLine), 20, // Minimum for clawd art ) return Math.min(contentWidth + 4, MAX_LEFT_WIDTH) // +4 for padding}
/** * Formats the welcome message based on username */export function formatWelcomeMessage(username: string | null): string { if (!username || username.length > MAX_USERNAME_LENGTH) { return 'Welcome back!' } return `Welcome back ${username}!`}
/** * Truncates a path in the middle if it's too long. * Width-aware: uses stringWidth() for correct CJK/emoji measurement. */export function truncatePath(path: string, maxLength: number): string { if (stringWidth(path) <= maxLength) return path
const separator = '/' const ellipsis = '…' const ellipsisWidth = 1 // '…' is always 1 column const separatorWidth = 1
const parts = path.split(separator) const first = parts[0] || '' const last = parts[parts.length - 1] || '' const firstWidth = stringWidth(first) const lastWidth = stringWidth(last)
// Only one part, so show as much of it as we can if (parts.length === 1) { return truncateToWidth(path, maxLength) }
// We don't have enough space to show the last part, so truncate it // But since firstPart is empty (unix) we don't want the extra ellipsis if (first === '' && ellipsisWidth + separatorWidth + lastWidth >= maxLength) { return `${separator}${truncateToWidth(last, Math.max(1, maxLength - separatorWidth))}` }
// We have a first part so let's show the ellipsis and truncate last part if ( first !== '' && ellipsisWidth * 2 + separatorWidth + lastWidth >= maxLength ) { return `${ellipsis}${separator}${truncateToWidth(last, Math.max(1, maxLength - ellipsisWidth - separatorWidth))}` }
// Truncate first and leave last if (parts.length === 2) { const availableForFirst = maxLength - ellipsisWidth - separatorWidth - lastWidth return `${truncateToWidthNoEllipsis(first, availableForFirst)}${ellipsis}${separator}${last}` }
// Now we start removing middle parts
let available = maxLength - firstWidth - lastWidth - ellipsisWidth - 2 * separatorWidth
// Just the first and last are too long, so truncate first if (available <= 0) { const availableForFirst = Math.max( 0, maxLength - lastWidth - ellipsisWidth - 2 * separatorWidth, ) const truncatedFirst = truncateToWidthNoEllipsis(first, availableForFirst) return `${truncatedFirst}${separator}${ellipsis}${separator}${last}` }
// Try to keep as many middle parts as possible const middleParts = [] for (let i = parts.length - 2; i > 0; i--) { const part = parts[i] if (part && stringWidth(part) + separatorWidth <= available) { middleParts.unshift(part) available -= stringWidth(part) + separatorWidth } else { break } }
if (middleParts.length === 0) { return `${first}${separator}${ellipsis}${separator}${last}` }
return `${first}${separator}${ellipsis}${separator}${middleParts.join(separator)}${separator}${last}`}
// Simple cache for preloaded activitylet cachedActivity: LogOption[] = []let cachePromise: Promise<LogOption[]> | null = null
/** * Preloads recent conversations for display in Logo v2 */export async function getRecentActivity(): Promise<LogOption[]> { // Return existing promise if already loading if (cachePromise) { return cachePromise }
const currentSessionId = getSessionId() cachePromise = loadMessageLogs(10) .then(logs => { cachedActivity = logs .filter(log => { if (log.isSidechain) return false if (log.sessionId === currentSessionId) return false if (log.summary?.includes('I apologize')) return false
// Filter out sessions where both summary and firstPrompt are "No prompt" or missing const hasSummary = log.summary && log.summary !== 'No prompt' const hasFirstPrompt = log.firstPrompt && log.firstPrompt !== 'No prompt' return hasSummary || hasFirstPrompt }) .slice(0, 3) return cachedActivity }) .catch(() => { cachedActivity = [] return cachedActivity })
return cachePromise}
/** * Gets cached activity synchronously */export function getRecentActivitySync(): LogOption[] { return cachedActivity}
/** * Formats release notes for display, with smart truncation */export function formatReleaseNoteForDisplay( note: string, maxWidth: number,): string { // Simply truncate at the max width, same as Recent Activity descriptions return truncate(note, maxWidth)}
/** * Gets the common logo display data used by both LogoV2 and CondensedLogo */export function getLogoDisplayData(): { version: string cwd: string billingType: string agentName: string | undefined} { const version = process.env.DEMO_VERSION ?? MACRO.VERSION const serverUrl = getDirectConnectServerUrl() const displayPath = process.env.DEMO_VERSION ? '/code/claude' : getDisplayPath(getCwd()) const cwd = serverUrl ? `${displayPath} in ${serverUrl.replace(/^https?:\/\//, '')}` : displayPath const billingType = isClaudeAISubscriber() ? getSubscriptionName() : 'API Usage Billing' const agentName = getInitialSettings().agent
return { version, cwd, billingType, agentName, }}
/** * Determines how to display model and billing information based on available width */export function formatModelAndBilling( modelName: string, billingType: string, availableWidth: number,): { shouldSplit: boolean truncatedModel: string truncatedBilling: string} { const separator = ' · ' const combinedWidth = stringWidth(modelName) + separator.length + stringWidth(billingType) const shouldSplit = combinedWidth > availableWidth
if (shouldSplit) { return { shouldSplit: true, truncatedModel: truncate(modelName, availableWidth), truncatedBilling: truncate(billingType, availableWidth), } }
return { shouldSplit: false, truncatedModel: truncate( modelName, Math.max( availableWidth - stringWidth(billingType) - separator.length, 10, ), ), truncatedBilling: billingType, }}
/** * Gets recent release notes for Logo v2 display * For ants, uses commits bundled at build time * For external users, uses public changelog */export function getRecentReleaseNotesSync(maxItems: number): string[] { // For ants, use bundled changelog if (process.env.USER_TYPE === 'ant') { const changelog = MACRO.VERSION_CHANGELOG if (changelog) { const commits = changelog.trim().split('\n').filter(Boolean) return commits.slice(0, maxItems) } return [] }
const changelog = getStoredChangelogFromMemory() if (!changelog) { return [] }
let parsed try { parsed = parseChangelog(changelog) } catch { return [] }
// Get notes from recent versions const allNotes: string[] = [] const versions = Object.keys(parsed) .sort((a, b) => (gt(a, b) ? -1 : 1)) .slice(0, 3) // Look at top 3 recent versions
for (const version of versions) { const notes = parsed[version] if (notes) { allNotes.push(...notes) } }
// Return raw notes without filtering or premature truncation return allNotes.slice(0, maxItems)}