style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -1,57 +1,51 @@
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { TEARDROP_ASTERISK } from '../../constants/figures.js'
import { Box, Text, useAnimationFrame } from '@anthropic/ink'
import { getInitialSettings } from '../../utils/settings/settings.js'
import { hueToRgb, toRGBColor } from '../Spinner/utils.js'
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import { TEARDROP_ASTERISK } from '../../constants/figures.js';
import { Box, Text, useAnimationFrame } from '@anthropic/ink';
import { getInitialSettings } from '../../utils/settings/settings.js';
import { hueToRgb, toRGBColor } from '../Spinner/utils.js';
const SWEEP_DURATION_MS = 1500
const SWEEP_COUNT = 2
const TOTAL_ANIMATION_MS = SWEEP_DURATION_MS * SWEEP_COUNT
const SETTLED_GREY = toRGBColor({ r: 153, g: 153, b: 153 })
const SWEEP_DURATION_MS = 1500;
const SWEEP_COUNT = 2;
const TOTAL_ANIMATION_MS = SWEEP_DURATION_MS * SWEEP_COUNT;
const SETTLED_GREY = toRGBColor({ r: 153, g: 153, b: 153 });
export function AnimatedAsterisk({
char = TEARDROP_ASTERISK,
}: {
char?: string
}): React.ReactNode {
export function AnimatedAsterisk({ char = TEARDROP_ASTERISK }: { char?: string }): React.ReactNode {
// Read prefersReducedMotion once at mount — no useSettings() subscription,
// since that would re-render whenever settings change.
const [reducedMotion] = useState(
() => getInitialSettings().prefersReducedMotion ?? false,
)
const [done, setDone] = useState(reducedMotion)
const [reducedMotion] = useState(() => getInitialSettings().prefersReducedMotion ?? false);
const [done, setDone] = useState(reducedMotion);
// useAnimationFrame's clock is shared — capture our start offset so the
// sweep always begins at hue 0 regardless of when we mount.
const startTimeRef = useRef<number | null>(null)
const startTimeRef = useRef<number | null>(null);
// Wire the ref so useAnimationFrame's viewport-pause kicks in: if the
// user submits a message before the sweep finishes, the clock stops
// automatically once this row enters scrollback (prevents flicker).
const [ref, time] = useAnimationFrame(done ? null : 50)
const [ref, time] = useAnimationFrame(done ? null : 50);
useEffect(() => {
if (done) return
const t = setTimeout(setDone, TOTAL_ANIMATION_MS, true)
return () => clearTimeout(t)
}, [done])
if (done) return;
const t = setTimeout(setDone, TOTAL_ANIMATION_MS, true);
return () => clearTimeout(t);
}, [done]);
if (done) {
return (
<Box ref={ref}>
<Text color={SETTLED_GREY}>{char}</Text>
</Box>
)
);
}
if (startTimeRef.current === null) {
startTimeRef.current = time
startTimeRef.current = time;
}
const elapsed = time - startTimeRef.current
const hue = ((elapsed / SWEEP_DURATION_MS) * 360) % 360
const elapsed = time - startTimeRef.current;
const hue = ((elapsed / SWEEP_DURATION_MS) * 360) % 360;
return (
<Box ref={ref}>
<Text color={toRGBColor(hueToRgb(hue))}>{char}</Text>
</Box>
)
);
}

View File

@@ -1,14 +1,14 @@
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { Box } from '@anthropic/ink'
import { getInitialSettings } from '../../utils/settings/settings.js'
import { Clawd, type ClawdPose } from './Clawd.js'
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import { Box } from '@anthropic/ink';
import { getInitialSettings } from '../../utils/settings/settings.js';
import { Clawd, type ClawdPose } from './Clawd.js';
type Frame = { pose: ClawdPose; offset: number }
type Frame = { pose: ClawdPose; offset: number };
/** Hold a pose for n frames (60ms each). */
function hold(pose: ClawdPose, offset: number, frames: number): Frame[] {
return Array.from({ length: frames }, () => ({ pose, offset }))
return Array.from({ length: frames }, () => ({ pose, offset }));
}
// Offset semantics: marginTop in a fixed-height-3 container. 0 = normal,
@@ -24,21 +24,21 @@ const JUMP_WAVE: readonly Frame[] = [
...hold('default', 1, 2), // crouch again
...hold('arms-up', 0, 3), // spring!
...hold('default', 0, 1),
]
];
// Click animation: glance right, then left, then back.
const LOOK_AROUND: readonly Frame[] = [
...hold('look-right', 0, 5),
...hold('look-left', 0, 5),
...hold('default', 0, 1),
]
];
const CLICK_ANIMATIONS: readonly (readonly Frame[])[] = [JUMP_WAVE, LOOK_AROUND]
const CLICK_ANIMATIONS: readonly (readonly Frame[])[] = [JUMP_WAVE, LOOK_AROUND];
const IDLE: Frame = { pose: 'default', offset: 0 }
const FRAME_MS = 60
const incrementFrame = (i: number) => i + 1
const CLAWD_HEIGHT = 3
const IDLE: Frame = { pose: 'default', offset: 0 };
const FRAME_MS = 60;
const incrementFrame = (i: number) => i + 1;
const CLAWD_HEIGHT = 3;
/**
* Clawd with click-triggered animations (crouch-jump with arms up, or
@@ -49,48 +49,44 @@ const CLAWD_HEIGHT = 3
* elsewhere this renders and behaves identically to plain `<Clawd />`.
*/
export function AnimatedClawd(): React.ReactNode {
const { pose, bounceOffset, onClick } = useClawdAnimation()
const { pose, bounceOffset, onClick } = useClawdAnimation();
return (
<Box height={CLAWD_HEIGHT} flexDirection="column" onClick={onClick}>
<Box marginTop={bounceOffset} flexShrink={0}>
<Clawd pose={pose} />
</Box>
</Box>
)
);
}
function useClawdAnimation(): {
pose: ClawdPose
bounceOffset: number
onClick: () => void
pose: ClawdPose;
bounceOffset: number;
onClick: () => void;
} {
// Read once at mount — no useSettings() subscription, since that would
// re-render on any settings change.
const [reducedMotion] = useState(
() => getInitialSettings().prefersReducedMotion ?? false,
)
const [frameIndex, setFrameIndex] = useState(-1)
const sequenceRef = useRef<readonly Frame[]>(JUMP_WAVE)
const [reducedMotion] = useState(() => getInitialSettings().prefersReducedMotion ?? false);
const [frameIndex, setFrameIndex] = useState(-1);
const sequenceRef = useRef<readonly Frame[]>(JUMP_WAVE);
const onClick = () => {
if (reducedMotion || frameIndex !== -1) return
sequenceRef.current =
CLICK_ANIMATIONS[Math.floor(Math.random() * CLICK_ANIMATIONS.length)]!
setFrameIndex(0)
}
if (reducedMotion || frameIndex !== -1) return;
sequenceRef.current = CLICK_ANIMATIONS[Math.floor(Math.random() * CLICK_ANIMATIONS.length)]!;
setFrameIndex(0);
};
useEffect(() => {
if (frameIndex === -1) return
if (frameIndex === -1) return;
if (frameIndex >= sequenceRef.current.length) {
setFrameIndex(-1)
return
setFrameIndex(-1);
return;
}
const timer = setTimeout(setFrameIndex, FRAME_MS, incrementFrame)
return () => clearTimeout(timer)
}, [frameIndex])
const timer = setTimeout(setFrameIndex, FRAME_MS, incrementFrame);
return () => clearTimeout(timer);
}, [frameIndex]);
const seq = sequenceRef.current
const current =
frameIndex >= 0 && frameIndex < seq.length ? seq[frameIndex]! : IDLE
return { pose: current.pose, bounceOffset: current.offset, onClick }
const seq = sequenceRef.current;
const current = frameIndex >= 0 && frameIndex < seq.length ? seq[frameIndex]! : IDLE;
return { pose: current.pose, bounceOffset: current.offset, onClick };
}

View File

@@ -4,49 +4,44 @@
// docs/feature-gating.md). Do NOT import this module statically from
// unguarded code.
import * as React from 'react'
import { useState } from 'react'
import {
type ChannelEntry,
getAllowedChannels,
getHasDevChannels,
} from '../../bootstrap/state.js'
import { getBuiltinPlugins } from '../../plugins/builtinPlugins.js'
import { Box, Text } from '@anthropic/ink'
import { getMcpConfigsByScope } from '../../services/mcp/config.js'
import { loadInstalledPluginsV2 } from '../../utils/plugins/installedPluginsManager.js'
import * as React from 'react';
import { useState } from 'react';
import { type ChannelEntry, getAllowedChannels, getHasDevChannels } from '../../bootstrap/state.js';
import { getBuiltinPlugins } from '../../plugins/builtinPlugins.js';
import { Box, Text } from '@anthropic/ink';
import { getMcpConfigsByScope } from '../../services/mcp/config.js';
import { loadInstalledPluginsV2 } from '../../utils/plugins/installedPluginsManager.js';
export function ChannelsNotice(): React.ReactNode {
// Snapshot all reads at mount. This notice enters scrollback immediately
// after the logo; any re-render past that point forces a full terminal
// reset.
const [{ channels, list, unmatched }] =
useState(() => {
const ch = getAllowedChannels()
if (ch.length === 0)
return {
channels: ch,
list: '',
unmatched: [] as Unmatched[],
}
const l = ch.map(formatEntry).join(', ')
const [{ channels, list, unmatched }] = useState(() => {
const ch = getAllowedChannels();
if (ch.length === 0)
return {
channels: ch,
list: l,
unmatched: findUnmatched(ch),
}
})
if (channels.length === 0) return null
list: '',
unmatched: [] as Unmatched[],
};
const l = ch.map(formatEntry).join(', ');
return {
channels: ch,
list: l,
unmatched: findUnmatched(ch),
};
});
if (channels.length === 0) return null;
// When both flags are passed, the list mixes entries and a single flag
// name would be wrong for half of it. entry.dev distinguishes origin.
const hasNonDev = channels.some(c => !c.dev)
const hasNonDev = channels.some(c => !c.dev);
const flag =
getHasDevChannels() && hasNonDev
? 'Channels'
: getHasDevChannels()
? '--dangerously-load-development-channels'
: '--channels'
: '--channels';
// "Listening for" not "active" — at this point we only know the allowlist
// was set. Server connection, capability declaration, and whether the name
@@ -55,9 +50,8 @@ export function ChannelsNotice(): React.ReactNode {
<Box paddingLeft={2} flexDirection="column">
<Text color="error">Listening for channel messages from: {list}</Text>
<Text dimColor>
Experimental · inbound messages will be pushed into this session, this
carries prompt injection risks. Restart Claude Code without {flag} to
disable.
Experimental · inbound messages will be pushed into this session, this carries prompt injection risks. Restart
Claude Code without {flag} to disable.
</Text>
{unmatched.map(u => (
<Text key={`${formatEntry(u.entry)}:${u.why}`} color="warning">
@@ -65,62 +59,61 @@ export function ChannelsNotice(): React.ReactNode {
</Text>
))}
</Box>
)
);
}
function formatEntry(c: ChannelEntry): string {
return c.kind === 'plugin'
? `plugin:${c.name}@${c.marketplace}`
: `server:${c.name}`
return c.kind === 'plugin' ? `plugin:${c.name}@${c.marketplace}` : `server:${c.name}`;
}
type Unmatched = { entry: ChannelEntry; why: string }
type Unmatched = { entry: ChannelEntry; why: string };
type FindUnmatchedDeps = {
configuredServerNames?: ReadonlySet<string>
installedPluginIds?: ReadonlySet<string>
}
configuredServerNames?: ReadonlySet<string>;
installedPluginIds?: ReadonlySet<string>;
};
export function findUnmatched(
entries: readonly ChannelEntry[],
deps?: FindUnmatchedDeps,
): Unmatched[] {
export function findUnmatched(entries: readonly ChannelEntry[], deps?: FindUnmatchedDeps): Unmatched[] {
// Server-kind: build one Set from all scopes up front. getMcpConfigsByScope
// is not cached (project scope walks the dir tree); getMcpConfigByName would
// redo that walk per entry.
const configured = deps?.configuredServerNames ?? (() => {
const scopes = ['enterprise', 'user', 'project', 'local'] as const
const names = new Set<string>()
for (const scope of scopes) {
for (const name of Object.keys(getMcpConfigsByScope(scope).servers)) {
names.add(name)
const configured =
deps?.configuredServerNames ??
(() => {
const scopes = ['enterprise', 'user', 'project', 'local'] as const;
const names = new Set<string>();
for (const scope of scopes) {
for (const name of Object.keys(getMcpConfigsByScope(scope).servers)) {
names.add(name);
}
}
}
return names
})()
return names;
})();
// Plugin-kind installed check: installed_plugins.json keys are
// `name@marketplace`. loadInstalledPluginsV2 is cached.
const installedPluginIds = deps?.installedPluginIds ?? (() => {
const ids = new Set(Object.keys(loadInstalledPluginsV2().plugins))
const builtinPlugins = getBuiltinPlugins()
for (const plugin of [...builtinPlugins.enabled, ...builtinPlugins.disabled]) {
ids.add(plugin.source)
}
return ids
})()
const installedPluginIds =
deps?.installedPluginIds ??
(() => {
const ids = new Set(Object.keys(loadInstalledPluginsV2().plugins));
const builtinPlugins = getBuiltinPlugins();
for (const plugin of [...builtinPlugins.enabled, ...builtinPlugins.disabled]) {
ids.add(plugin.source);
}
return ids;
})();
const out: Unmatched[] = []
const out: Unmatched[] = [];
for (const entry of entries) {
if (entry.kind === 'server') {
if (!configured.has(entry.name)) {
out.push({ entry, why: 'no MCP server configured with that name' })
out.push({ entry, why: 'no MCP server configured with that name' });
}
continue
continue;
}
if (!installedPluginIds.has(`${entry.name}@${entry.marketplace}`)) {
out.push({ entry, why: 'plugin not installed' })
out.push({ entry, why: 'plugin not installed' });
}
}
return out
return out;
}

View File

@@ -1,16 +1,16 @@
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';
import { env } from '../../utils/env.js';
export type ClawdPose =
| 'default'
| 'arms-up' // both arms raised (used during jump)
| 'look-left' // both pupils shifted left
| 'look-right' // both pupils shifted right
| 'look-right'; // both pupils shifted 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
@@ -23,23 +23,23 @@ type Props = {
// default (▛/▜, bottom pupils) — otherwise only one eye would appear to move.
type Segments = {
/** row 1 left (no bg): optional raised arm + side */
r1L: string
r1L: string;
/** row 1 eyes (with bg): left-eye, forehead, right-eye */
r1E: string
r1E: string;
/** row 1 right (no bg): side + optional raised arm */
r1R: string
r1R: string;
/** row 2 left (no bg): arm + body curve */
r2L: string
r2L: string;
/** row 2 right (no bg): body curve + arm */
r2R: string
}
r2R: string;
};
const POSES: Record<ClawdPose, Segments> = {
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.
@@ -48,13 +48,13 @@ const APPLE_EYES: Record<ClawdPose, string> = {
'look-left': ' ▘ ▘ ',
'look-right': ' ▝ ▝ ',
'arms-up': ' ▗ ▖ ',
}
};
export function Clawd({ pose = 'default' }: Props = {}): React.ReactNode {
if (env.terminal === 'Apple_Terminal') {
return <AppleTerminalClawd pose={pose} />
return <AppleTerminalClawd pose={pose} />;
}
const p = POSES[pose]
const p = POSES[pose];
return (
<Box flexDirection="column">
<Text>
@@ -75,7 +75,7 @@ export function Clawd({ pose = 'default' }: Props = {}): React.ReactNode {
{' '} {' '}
</Text>
</Box>
)
);
}
function AppleTerminalClawd({ pose }: { pose: ClawdPose }): React.ReactNode {
@@ -94,5 +94,5 @@ function AppleTerminalClawd({ pose }: { pose: ClawdPose }): React.ReactNode {
<Text backgroundColor="clawd_body">{' '.repeat(7)}</Text>
<Text color="clawd_body"> </Text>
</Box>
)
);
}

View File

@@ -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),
)
const versionPrefix = 'Claude Code 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
@@ -86,33 +74,28 @@ export function CondensedLogo(): ReactNode {
return (
<OffscreenFreeze>
<Box flexDirection="row" gap={2} alignItems="center">
{isFullscreenEnvEnabled() ? <AnimatedClawd /> : <Clawd />}
{isFullscreenEnvEnabled() ? <AnimatedClawd /> : <Clawd />}
{/* Info */}
<Box flexDirection="column">
<Text>
<Text bold>Claude Code</Text>{' '}
<Text dimColor>v{truncatedVersion}</Text>
</Text>
{shouldSplit ? (
<>
<Text dimColor>{truncatedModel}</Text>
<Text dimColor>{truncatedBilling}</Text>
</>
) : (
<Text dimColor>
{truncatedModel} · {truncatedBilling}
{/* Info */}
<Box flexDirection="column">
<Text>
<Text bold>Claude Code</Text> <Text dimColor>v{truncatedVersion}</Text>
</Text>
)}
<Text dimColor>
{agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd}
</Text>
{showGuestPassesUpsell && <GuestPassesUpsell />}
{!showGuestPassesUpsell && showOverageCreditUpsell && (
<OverageCreditUpsell maxWidth={textWidth} twoLine />
)}
</Box>
{shouldSplit ? (
<>
<Text dimColor>{truncatedModel}</Text>
<Text dimColor>{truncatedBilling}</Text>
</>
) : (
<Text dimColor>
{truncatedModel} · {truncatedBilling}
</Text>
)}
<Text dimColor>{agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd}</Text>
{showGuestPassesUpsell && <GuestPassesUpsell />}
{!showGuestPassesUpsell && showOverageCreditUpsell && <OverageCreditUpsell maxWidth={textWidth} twoLine />}
</Box>
</Box>
</OffscreenFreeze>
)
);
}

View File

@@ -1,34 +1,31 @@
import * as React from 'react'
import { useEffect, useMemo } from 'react'
import { Box, Text } from '@anthropic/ink'
import { getDynamicConfig_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js'
import * as React from 'react';
import { useEffect, useMemo } from 'react';
import { Box, Text } from '@anthropic/ink';
import { getDynamicConfig_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js';
const CONFIG_NAME = 'tengu-top-of-feed-tip'
const CONFIG_NAME = 'tengu-top-of-feed-tip';
export function EmergencyTip(): React.ReactNode {
const tip = useMemo(getTipOfFeed, [])
const tip = useMemo(getTipOfFeed, []);
// Memoize to prevent re-reads after we save - we want the value at mount time
const lastShownTip = useMemo(
() => getGlobalConfig().lastShownEmergencyTip,
[],
)
const lastShownTip = useMemo(() => getGlobalConfig().lastShownEmergencyTip, []);
// Only show if this is a new/different tip
const shouldShow = tip.tip && tip.tip !== lastShownTip
const shouldShow = tip.tip && tip.tip !== lastShownTip;
// Save the tip we're showing so we don't show it again
useEffect(() => {
if (shouldShow) {
saveGlobalConfig(current => {
if (current.lastShownEmergencyTip === tip.tip) return current
return { ...current, lastShownEmergencyTip: tip.tip }
})
if (current.lastShownEmergencyTip === tip.tip) return current;
return { ...current, lastShownEmergencyTip: tip.tip };
});
}
}, [shouldShow, tip.tip])
}, [shouldShow, tip.tip]);
if (!shouldShow) {
return null
return null;
}
return (
@@ -43,23 +40,20 @@ export function EmergencyTip(): React.ReactNode {
{tip.tip}
</Text>
</Box>
)
);
}
type TipOfFeed = {
tip: string
color?: 'dim' | 'warning' | 'error'
}
tip: string;
color?: 'dim' | 'warning' | 'error';
};
const DEFAULT_TIP: TipOfFeed = { tip: '', color: 'dim' }
const DEFAULT_TIP: TipOfFeed = { tip: '', color: 'dim' };
/**
* Get the tip of the feed from dynamic config with caching
* Returns cached value immediately, updates in background
*/
function getTipOfFeed(): TipOfFeed {
return getDynamicConfig_CACHED_MAY_BE_STALE<TipOfFeed>(
CONFIG_NAME,
DEFAULT_TIP,
)
return getDynamicConfig_CACHED_MAY_BE_STALE<TipOfFeed>(CONFIG_NAME, DEFAULT_TIP);
}

View File

@@ -1,9 +1,9 @@
import * as React from 'react'
import * as React from 'react';
/**
* Internal-only component. Shows experiment enrollment status for internal
* users. Stubbed — returns null in non-internal builds.
*/
export function ExperimentEnrollmentNotice(): React.ReactNode {
return null
return null;
}

View File

@@ -1,65 +1,57 @@
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 (
<Box flexDirection="column" width={actualWidth}>
@@ -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 (
<Text key={index}>
{maxTimestampWidth > 0 && (
<>
<Text dimColor>
{(line.timestamp || '').padEnd(maxTimestampWidth)}
</Text>
<Text dimColor>{(line.timestamp || '').padEnd(maxTimestampWidth)}</Text>
{gap}
</>
)}
<Text>{truncate(line.text, textWidth)}</Text>
</Text>
)
);
})}
{footer && (
<Text dimColor italic>
@@ -108,5 +94,5 @@ export function Feed({ config, actualWidth }: FeedProps): React.ReactNode {
</>
)}
</Box>
)
);
}

View File

@@ -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 (
<Box flexDirection="column">
{feeds.map((feed, index) => (
<React.Fragment key={index}>
<Feed config={feed} actualWidth={actualWidth} />
{index < feeds.length - 1 && (
<Divider color="claude" width={actualWidth} />
)}
{index < feeds.length - 1 && <Divider color="claude" width={actualWidth} />}
</React.Fragment>
))}
</Box>
)
);
}

View File

@@ -1,4 +1,4 @@
import * as React from 'react'
import * as React from 'react';
/**
* Internal-only component. Displays a warning when feature-gate overrides
@@ -6,5 +6,5 @@ import * as React from 'react'
* non-internal builds.
*/
export function GateOverridesWarning(): React.ReactNode {
return null
return null;
}

View File

@@ -1,73 +1,72 @@
import * as React from 'react'
import { useState } from 'react'
import { Text } from '@anthropic/ink'
import { logEvent } from '../../services/analytics/index.js'
import * as React from 'react';
import { useState } from 'react';
import { Text } from '@anthropic/ink';
import { logEvent } from '../../services/analytics/index.js';
import {
checkCachedPassesEligibility,
formatCreditAmount,
getCachedReferrerReward,
getCachedRemainingPasses,
} from '../../services/api/referral.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
} from '../../services/api/referral.js';
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
function resetIfPassesRefreshed(): void {
const remaining = getCachedRemainingPasses()
if (remaining == null || remaining <= 0) return
const config = getGlobalConfig()
const lastSeen = config.passesLastSeenRemaining ?? 0
const remaining = getCachedRemainingPasses();
if (remaining == null || remaining <= 0) return;
const config = getGlobalConfig();
const lastSeen = config.passesLastSeenRemaining ?? 0;
if (remaining > lastSeen) {
saveGlobalConfig(prev => ({
...prev,
passesUpsellSeenCount: 0,
hasVisitedPasses: false,
passesLastSeenRemaining: remaining,
}))
}));
}
}
function shouldShowGuestPassesUpsell(): boolean {
const { eligible, hasCache } = checkCachedPassesEligibility()
const { eligible, hasCache } = checkCachedPassesEligibility();
// Only show if eligible and cache exists (don't block on fetch)
if (!eligible || !hasCache) return false
if (!eligible || !hasCache) return false;
// Reset upsell counters if passes were refreshed (covers both campaign change and pass refresh)
resetIfPassesRefreshed()
resetIfPassesRefreshed();
const config = getGlobalConfig()
if ((config.passesUpsellSeenCount ?? 0) >= 3) return false
if (config.hasVisitedPasses) return false
const config = getGlobalConfig();
if ((config.passesUpsellSeenCount ?? 0) >= 3) return false;
if (config.hasVisitedPasses) return false;
return true
return true;
}
export function useShowGuestPassesUpsell(): boolean {
const [show] = useState(() => shouldShowGuestPassesUpsell())
return show
const [show] = useState(() => shouldShowGuestPassesUpsell());
return show;
}
export function incrementGuestPassesSeenCount(): void {
let newCount = 0
let newCount = 0;
saveGlobalConfig(prev => {
newCount = (prev.passesUpsellSeenCount ?? 0) + 1
newCount = (prev.passesUpsellSeenCount ?? 0) + 1;
return {
...prev,
passesUpsellSeenCount: newCount,
}
})
};
});
logEvent('tengu_guest_passes_upsell_shown', {
seen_count: newCount,
})
});
}
// Condensed layout for mini welcome screen
export function GuestPassesUpsell(): React.ReactNode {
const reward = getCachedReferrerReward()
const reward = getCachedReferrerReward();
return (
<Text dimColor>
<Text color="claude">[]</Text> <Text color="claude">[]</Text>{' '}
<Text color="claude">[]</Text> ·{' '}
<Text color="claude">[]</Text> <Text color="claude">[]</Text> <Text color="claude">[]</Text> ·{' '}
{reward
? `Share Claude Code and earn ${formatCreditAmount(reward)} of extra usage · /passes`
: '3 guest passes at /passes'}
</Text>
)
);
}

View File

@@ -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/>),
// 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 (
<>
<CondensedLogo />
@@ -192,17 +155,13 @@ export function LogoV2(): React.ReactNode {
{isDebugMode() && (
<Box paddingLeft={2} flexDirection="column">
<Text color="warning">Debug mode enabled</Text>
<Text dimColor>
Logging to: {isDebugToStdErr() ? 'stderr' : getDebugLogPath()}
</Text>
<Text dimColor>Logging to: {isDebugToStdErr() ? 'stderr' : getDebugLogPath()}</Text>
</Box>
)}
<EmergencyTip />
{process.env.CLAUDE_CODE_TMUX_SESSION && (
<Box paddingLeft={2} flexDirection="column">
<Text dimColor>
tmux session: {process.env.CLAUDE_CODE_TMUX_SESSION}
</Text>
<Text dimColor>tmux session: {process.env.CLAUDE_CODE_TMUX_SESSION}</Text>
<Text dimColor>
{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 && (
<Box paddingLeft={2} flexDirection="column">
{!process.env.IS_DEMO && config.oauthAccount?.organizationName && (
<Text dimColor>
Message from {config.oauthAccount.organizationName}:
</Text>
<Text dimColor>Message from {config.oauthAccount.organizationName}:</Text>
)}
<Text>{announcement}</Text>
</Box>
@@ -228,51 +185,41 @@ export function LogoV2(): React.ReactNode {
{process.env.USER_TYPE === 'ant' && !process.env.DEMO_VERSION && (
<Box paddingLeft={2} flexDirection="column">
<Text color="warning">[ANT-ONLY] Logs:</Text>
<Text dimColor>
API calls: {getDisplayPath(getDumpPromptsPath())}
</Text>
<Text dimColor>
Debug logs: {getDisplayPath(getDebugLogPath())}
</Text>
<Text dimColor>API calls: {getDisplayPath(getDumpPromptsPath())}</Text>
<Text dimColor>Debug logs: {getDisplayPath(getDebugLogPath())}</Text>
{isDetailedProfilingEnabled() && (
<Text dimColor>
Startup Perf: {getDisplayPath(getStartupPerfLogPath())}
</Text>
<Text dimColor>Startup Perf: {getDisplayPath(getStartupPerfLogPath())}</Text>
)}
</Box>
)}
{process.env.USER_TYPE === 'ant' && <GateOverridesWarning />}
{process.env.USER_TYPE === 'ant' && <ExperimentEnrollmentNotice />}
</>
)
);
}
// 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('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)
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.
@@ -300,9 +247,7 @@ export function LogoV2(): React.ReactNode {
</Box>
<Text dimColor>{modelDisplayName}</Text>
<Text dimColor>{billingType}</Text>
<Text dimColor>
{agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd}
</Text>
<Text dimColor>{agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd}</Text>
</Box>
</OffscreenFreeze>
<VoiceModeNotice />
@@ -310,45 +255,32 @@ export function LogoV2(): React.ReactNode {
{ChannelsNoticeModule && <ChannelsNoticeModule.ChannelsNotice />}
{showSandboxStatus && (
<Box marginTop={1} flexDirection="column">
<Text color="warning">
Your bash commands will be sandboxed. Disable with /sandbox.
</Text>
<Text color="warning">Your bash commands will be sandboxed. Disable with /sandbox.</Text>
</Box>
)}
{process.env.USER_TYPE === 'ant' && <GateOverridesWarning />}
{process.env.USER_TYPE === 'ant' && <ExperimentEnrollmentNotice />}
</>
)
);
}
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 (
<>
@@ -365,11 +297,7 @@ export function LogoV2(): React.ReactNode {
}}
>
{/* Main content */}
<Box
flexDirection={layoutMode === 'horizontal' ? 'row' : 'column'}
paddingX={1}
gap={1}
>
<Box flexDirection={layoutMode === 'horizontal' ? 'row' : 'column'} paddingX={1} gap={1}>
{/* Left Panel */}
<Box
flexDirection="column"
@@ -408,24 +336,12 @@ export function LogoV2(): React.ReactNode {
<FeedColumn
feeds={
showOnboarding
? [
createProjectOnboardingFeed(getSteps()),
createRecentActivityFeed(activities),
]
? [createProjectOnboardingFeed(getSteps()), createRecentActivityFeed(activities)]
: showGuestPassesUpsell
? [
createRecentActivityFeed(activities),
createGuestPassesFeed(),
]
? [createRecentActivityFeed(activities), createGuestPassesFeed()]
: showOverageCreditUpsell
? [
createRecentActivityFeed(activities),
createOverageCreditFeed(),
]
: [
createRecentActivityFeed(activities),
createWhatsNewFeed(changelog),
]
? [createRecentActivityFeed(activities), createOverageCreditFeed()]
: [createRecentActivityFeed(activities), createWhatsNewFeed(changelog)]
}
maxWidth={rightWidth}
/>
@@ -439,17 +355,13 @@ export function LogoV2(): React.ReactNode {
{isDebugMode() && (
<Box paddingLeft={2} flexDirection="column">
<Text color="warning">Debug mode enabled</Text>
<Text dimColor>
Logging to: {isDebugToStdErr() ? 'stderr' : getDebugLogPath()}
</Text>
<Text dimColor>Logging to: {isDebugToStdErr() ? 'stderr' : getDebugLogPath()}</Text>
</Box>
)}
<EmergencyTip />
{process.env.CLAUDE_CODE_TMUX_SESSION && (
<Box paddingLeft={2} flexDirection="column">
<Text dimColor>
tmux session: {process.env.CLAUDE_CODE_TMUX_SESSION}
</Text>
<Text dimColor>tmux session: {process.env.CLAUDE_CODE_TMUX_SESSION}</Text>
<Text dimColor>
{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 && (
<Box paddingLeft={2} flexDirection="column">
{!process.env.IS_DEMO && config.oauthAccount?.organizationName && (
<Text dimColor>
Message from {config.oauthAccount.organizationName}:
</Text>
<Text dimColor>Message from {config.oauthAccount.organizationName}:</Text>
)}
<Text>{announcement}</Text>
</Box>
)}
{showSandboxStatus && (
<Box paddingLeft={2} flexDirection="column">
<Text color="warning">
Your bash commands will be sandboxed. Disable with /sandbox.
</Text>
<Text color="warning">Your bash commands will be sandboxed. Disable with /sandbox.</Text>
</Box>
)}
{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 && (
<Box paddingLeft={2} flexDirection="column">
<Text color="warning">[ANT-ONLY] Logs:</Text>
<Text dimColor>
API calls: {getDisplayPath(getDumpPromptsPath())}
</Text>
<Text dimColor>API calls: {getDisplayPath(getDumpPromptsPath())}</Text>
<Text dimColor>Debug logs: {getDisplayPath(getDebugLogPath())}</Text>
{isDetailedProfilingEnabled() && (
<Text dimColor>
Startup Perf: {getDisplayPath(getStartupPerfLogPath())}
</Text>
<Text dimColor>Startup Perf: {getDisplayPath(getStartupPerfLogPath())}</Text>
)}
</Box>
)}
{process.env.USER_TYPE === 'ant' && <GateOverridesWarning />}
{process.env.USER_TYPE === 'ant' && <ExperimentEnrollmentNotice />}
</>
)
);
}

View File

@@ -1,41 +1,35 @@
import * as React from 'react'
import { useEffect, useState } from 'react'
import { UP_ARROW } from '../../constants/figures.js'
import { Box, Text } from '@anthropic/ink'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { isOpus1mMergeEnabled } from '../../utils/model/model.js'
import { AnimatedAsterisk } from './AnimatedAsterisk.js'
import * as React from 'react';
import { useEffect, useState } from 'react';
import { UP_ARROW } from '../../constants/figures.js';
import { Box, Text } from '@anthropic/ink';
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
import { isOpus1mMergeEnabled } from '../../utils/model/model.js';
import { AnimatedAsterisk } from './AnimatedAsterisk.js';
const MAX_SHOW_COUNT = 6
const MAX_SHOW_COUNT = 6;
export function shouldShowOpus1mMergeNotice(): boolean {
return (
isOpus1mMergeEnabled() &&
(getGlobalConfig().opus1mMergeNoticeSeenCount ?? 0) < MAX_SHOW_COUNT
)
return isOpus1mMergeEnabled() && (getGlobalConfig().opus1mMergeNoticeSeenCount ?? 0) < MAX_SHOW_COUNT;
}
export function Opus1mMergeNotice(): React.ReactNode {
const [show] = useState(shouldShowOpus1mMergeNotice)
const [show] = useState(shouldShowOpus1mMergeNotice);
useEffect(() => {
if (!show) return
const newCount = (getGlobalConfig().opus1mMergeNoticeSeenCount ?? 0) + 1
if (!show) return;
const newCount = (getGlobalConfig().opus1mMergeNoticeSeenCount ?? 0) + 1;
saveGlobalConfig(prev => {
if ((prev.opus1mMergeNoticeSeenCount ?? 0) >= newCount) return prev
return { ...prev, opus1mMergeNoticeSeenCount: newCount }
})
}, [show])
if ((prev.opus1mMergeNoticeSeenCount ?? 0) >= newCount) return prev;
return { ...prev, opus1mMergeNoticeSeenCount: newCount };
});
}, [show]);
if (!show) return null
if (!show) return null;
return (
<Box paddingLeft={2}>
<AnimatedAsterisk char={UP_ARROW} />
<Text dimColor>
{' '}
Opus now defaults to 1M context · 5x more room, same pricing
</Text>
<Text dimColor> Opus now defaults to 1M context · 5x more room, same pricing</Text>
</Box>
)
);
}

View File

@@ -1,17 +1,17 @@
import * as React from 'react'
import { useState } from 'react'
import { Text } from '@anthropic/ink'
import { logEvent } from '../../services/analytics/index.js'
import * as React from 'react';
import { useState } from 'react';
import { Text } from '@anthropic/ink';
import { logEvent } from '../../services/analytics/index.js';
import {
formatGrantAmount,
getCachedOverageCreditGrant,
refreshOverageCreditGrantCache,
} from '../../services/api/overageCreditGrant.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { truncate } from '../../utils/format.js'
import type { FeedConfig } from './Feed.js'
} from '../../services/api/overageCreditGrant.js';
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
import { truncate } from '../../utils/format.js';
import type { FeedConfig } from './Feed.js';
const MAX_IMPRESSIONS = 3
const MAX_IMPRESSIONS = 3;
/**
* Whether to show the overage credit upsell on any surface.
@@ -29,20 +29,19 @@ const MAX_IMPRESSIONS = 3
* (welcome feed, tips).
*/
export function isEligibleForOverageCreditGrant(): boolean {
const info = getCachedOverageCreditGrant()
if (!info || !info.available || info.granted) return false
return formatGrantAmount(info) !== null
const info = getCachedOverageCreditGrant();
if (!info || !info.available || info.granted) return false;
return formatGrantAmount(info) !== null;
}
export function shouldShowOverageCreditUpsell(): boolean {
if (!isEligibleForOverageCreditGrant()) return false
if (!isEligibleForOverageCreditGrant()) return false;
const config = getGlobalConfig()
if (config.hasVisitedExtraUsage) return false
if ((config.overageCreditUpsellSeenCount ?? 0) >= MAX_IMPRESSIONS)
return false
const config = getGlobalConfig();
if (config.hasVisitedExtraUsage) return false;
if ((config.overageCreditUpsellSeenCount ?? 0) >= MAX_IMPRESSIONS) return false;
return true
return true;
}
/**
@@ -50,78 +49,71 @@ export function shouldShowOverageCreditUpsell(): boolean {
* unconditionally on mount — it no-ops if cache is fresh.
*/
export function maybeRefreshOverageCreditCache(): void {
if (getCachedOverageCreditGrant() !== null) return
void refreshOverageCreditGrantCache()
if (getCachedOverageCreditGrant() !== null) return;
void refreshOverageCreditGrantCache();
}
export function useShowOverageCreditUpsell(): boolean {
const [show] = useState(() => {
maybeRefreshOverageCreditCache()
return shouldShowOverageCreditUpsell()
})
return show
maybeRefreshOverageCreditCache();
return shouldShowOverageCreditUpsell();
});
return show;
}
export function incrementOverageCreditUpsellSeenCount(): void {
let newCount = 0
let newCount = 0;
saveGlobalConfig(prev => {
newCount = (prev.overageCreditUpsellSeenCount ?? 0) + 1
newCount = (prev.overageCreditUpsellSeenCount ?? 0) + 1;
return {
...prev,
overageCreditUpsellSeenCount: newCount,
}
})
logEvent('tengu_overage_credit_upsell_shown', { seen_count: newCount })
};
});
logEvent('tengu_overage_credit_upsell_shown', { seen_count: newCount });
}
// Copy from "OC & Bulk Overages copy" doc (#6 — CLI /usage)
function getUsageText(amount: string): string {
return `${amount} in extra usage for third-party apps · /extra-usage`
return `${amount} in extra usage for third-party apps · /extra-usage`;
}
// Copy from "OC & Bulk Overages copy" doc (#4 — CLI Welcome screen).
// Char budgets: title ≤19, subtitle ≤48.
const FEED_SUBTITLE = 'On us. Works on third-party apps · /extra-usage'
const FEED_SUBTITLE = 'On us. Works on third-party apps · /extra-usage';
function getFeedTitle(amount: string): string {
return `${amount} in extra usage`
return `${amount} in extra usage`;
}
type Props = { maxWidth?: number; twoLine?: boolean }
type Props = { maxWidth?: number; twoLine?: boolean };
export function OverageCreditUpsell({
maxWidth,
twoLine,
}: Props): React.ReactNode {
const info = getCachedOverageCreditGrant()
if (!info) return null
const amount = formatGrantAmount(info)
if (!amount) return null
export function OverageCreditUpsell({ maxWidth, twoLine }: Props): React.ReactNode {
const info = getCachedOverageCreditGrant();
if (!info) return null;
const amount = formatGrantAmount(info);
if (!amount) return null;
if (twoLine) {
const title = getFeedTitle(amount)
const title = getFeedTitle(amount);
return (
<>
<Text color="claude">
{maxWidth ? truncate(title, maxWidth) : title}
</Text>
<Text dimColor>
{maxWidth ? truncate(FEED_SUBTITLE, maxWidth) : FEED_SUBTITLE}
</Text>
<Text color="claude">{maxWidth ? truncate(title, maxWidth) : title}</Text>
<Text dimColor>{maxWidth ? truncate(FEED_SUBTITLE, maxWidth) : FEED_SUBTITLE}</Text>
</>
)
);
}
const text = getUsageText(amount)
const display = maxWidth ? truncate(text, maxWidth) : text
const highlightLen = Math.min(getFeedTitle(amount).length, display.length)
const text = getUsageText(amount);
const display = maxWidth ? truncate(text, maxWidth) : text;
const highlightLen = Math.min(getFeedTitle(amount).length, display.length);
return (
<Text dimColor>
<Text color="claude">{display.slice(0, highlightLen)}</Text>
{display.slice(highlightLen)}
</Text>
)
);
}
/**
@@ -132,9 +124,9 @@ export function OverageCreditUpsell({
* Char budgets: title ≤19, subtitle ≤48.
*/
export function createOverageCreditFeed(): FeedConfig {
const info = getCachedOverageCreditGrant()
const amount = info ? formatGrantAmount(info) : null
const title = amount ? getFeedTitle(amount) : 'extra usage credit'
const info = getCachedOverageCreditGrant();
const amount = info ? formatGrantAmount(info) : null;
const title = amount ? getFeedTitle(amount) : 'extra usage credit';
return {
title,
lines: [],
@@ -142,5 +134,5 @@ export function createOverageCreditFeed(): FeedConfig {
content: <Text dimColor>{FEED_SUBTITLE}</Text>,
width: Math.max(title.length, FEED_SUBTITLE.length),
},
}
};
}

View File

@@ -1,19 +1,19 @@
import { feature } from 'bun:bundle'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { Box, Text } from '@anthropic/ink'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { getInitialSettings } from '../../utils/settings/settings.js'
import { isVoiceModeEnabled } from '../../voice/voiceModeEnabled.js'
import { AnimatedAsterisk } from './AnimatedAsterisk.js'
import { shouldShowOpus1mMergeNotice } from './Opus1mMergeNotice.js'
import { feature } from 'bun:bundle';
import * as React from 'react';
import { useEffect, useState } from 'react';
import { Box, Text } from '@anthropic/ink';
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
import { getInitialSettings } from '../../utils/settings/settings.js';
import { isVoiceModeEnabled } from '../../voice/voiceModeEnabled.js';
import { AnimatedAsterisk } from './AnimatedAsterisk.js';
import { shouldShowOpus1mMergeNotice } from './Opus1mMergeNotice.js';
const MAX_SHOW_COUNT = 3
const MAX_SHOW_COUNT = 3;
export function VoiceModeNotice(): React.ReactNode {
// Positive ternary pattern — see docs/feature-gating.md.
// All strings must be inside the guarded branch for dead-code elimination.
return feature('VOICE_MODE') ? <VoiceModeNoticeInner /> : null
return feature('VOICE_MODE') ? <VoiceModeNoticeInner /> : null;
}
function VoiceModeNoticeInner(): React.ReactNode {
@@ -28,24 +28,24 @@ function VoiceModeNoticeInner(): React.ReactNode {
getInitialSettings().voiceEnabled !== true &&
(getGlobalConfig().voiceNoticeSeenCount ?? 0) < MAX_SHOW_COUNT &&
!shouldShowOpus1mMergeNotice(),
)
);
useEffect(() => {
if (!show) return
if (!show) return;
// Capture outside the updater so StrictMode's second invocation is a no-op.
const newCount = (getGlobalConfig().voiceNoticeSeenCount ?? 0) + 1
const newCount = (getGlobalConfig().voiceNoticeSeenCount ?? 0) + 1;
saveGlobalConfig(prev => {
if ((prev.voiceNoticeSeenCount ?? 0) >= newCount) return prev
return { ...prev, voiceNoticeSeenCount: newCount }
})
}, [show])
if ((prev.voiceNoticeSeenCount ?? 0) >= newCount) return prev;
return { ...prev, voiceNoticeSeenCount: newCount };
});
}, [show]);
if (!show) return null
if (!show) return null;
return (
<Box paddingLeft={2}>
<AnimatedAsterisk />
<Text dimColor> Voice mode is now available · /voice to enable</Text>
</Box>
)
);
}

View File

@@ -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 Claude Code';
if (env.terminal === 'Apple_Terminal') {
return (
<AppleTerminalWelcomeV2 theme={theme} welcomeMessage={welcomeMessage} />
)
return <AppleTerminalWelcomeV2 theme={theme} welcomeMessage={welcomeMessage} />;
}
if (['light', 'light-daltonized', 'light-ansi'].includes(theme)) {
@@ -22,30 +20,14 @@ export function WelcomeV2(): React.ReactNode {
<Text color="claude">{welcomeMessage} </Text>
<Text dimColor>v{MACRO.VERSION} </Text>
</Text>
<Text>
{'…………………………………………………………………………………………………………………………………………………………'}
</Text>
<Text>
{' '}
</Text>
<Text>
{' '}
</Text>
<Text>
{' '}
</Text>
<Text>
{' ░░░░░░ '}
</Text>
<Text>
{' ░░░ ░░░░░░░░░░ '}
</Text>
<Text>
{' ░░░░░░░░░░░░░░░░░░░ '}
</Text>
<Text>
{' '}
</Text>
<Text>{'…………………………………………………………………………………………………………………………………………………………'}</Text>
<Text>{' '}</Text>
<Text>{' '}</Text>
<Text>{' '}</Text>
<Text>{' ░░░░░░ '}</Text>
<Text>{' ░░░ ░░░░░░░░░░ '}</Text>
<Text>{' ░░░░░░░░░░░░░░░░░░░ '}</Text>
<Text>{' '}</Text>
<Text>
<Text dimColor>{' ░░░░'}</Text>
<Text>{' ██ '}</Text>
@@ -54,9 +36,7 @@ export function WelcomeV2(): React.ReactNode {
<Text dimColor>{' ░░░░░░░░░░'}</Text>
<Text>{' ██▒▒██ '}</Text>
</Text>
<Text>
{' ▒▒ ██ ▒'}
</Text>
<Text>{' ▒▒ ██ ▒'}</Text>
<Text>
{' '}
<Text color="clawd_body"> </Text>
@@ -81,7 +61,7 @@ export function WelcomeV2(): React.ReactNode {
</Text>
</Text>
</Box>
)
);
}
return (
@@ -91,41 +71,21 @@ export function WelcomeV2(): React.ReactNode {
<Text color="claude">{welcomeMessage} </Text>
<Text dimColor>v{MACRO.VERSION} </Text>
</Text>
<Text>
{'…………………………………………………………………………………………………………………………………………………………'}
</Text>
<Text>
{' '}
</Text>
<Text>
{' * █████▓▓░ '}
</Text>
<Text>
{' * ███▓░ ░░ '}
</Text>
<Text>
{' ░░░░░░ ███▓░ '}
</Text>
<Text>
{' ░░░ ░░░░░░░░░░ ███▓░ '}
</Text>
<Text>{'…………………………………………………………………………………………………………………………………………………………'}</Text>
<Text>{' '}</Text>
<Text>{' * █████▓▓░ '}</Text>
<Text>{' * ███▓░ ░░ '}</Text>
<Text>{' ░░░░░░ ███▓░ '}</Text>
<Text>{' ░░░ ░░░░░░░░░░ ███▓░ '}</Text>
<Text>
<Text>{' ░░░░░░░░░░░░░░░░░░░ '}</Text>
<Text bold>*</Text>
<Text>{' ██▓░░ ▓ '}</Text>
</Text>
<Text>
{' ░▓▓███▓▓░ '}
</Text>
<Text dimColor>
{' * ░░░░ '}
</Text>
<Text dimColor>
{' ░░░░░░░░ '}
</Text>
<Text dimColor>
{' ░░░░░░░░░░░░░░░░ '}
</Text>
<Text>{' ░▓▓███▓▓░ '}</Text>
<Text dimColor>{' * ░░░░ '}</Text>
<Text dimColor>{' ░░░░░░░░ '}</Text>
<Text dimColor>{' ░░░░░░░░░░░░░░░░ '}</Text>
<Text>
{' '}
<Text color="clawd_body"> </Text>
@@ -152,21 +112,16 @@ export function WelcomeV2(): React.ReactNode {
</Text>
</Text>
</Box>
)
);
}
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({
<Text color="claude">{welcomeMessage} </Text>
<Text dimColor>v{MACRO.VERSION} </Text>
</Text>
<Text>
{'…………………………………………………………………………………………………………………………………………………………'}
</Text>
<Text>
{' '}
</Text>
<Text>
{' '}
</Text>
<Text>
{' '}
</Text>
<Text>
{' ░░░░░░ '}
</Text>
<Text>
{' ░░░ ░░░░░░░░░░ '}
</Text>
<Text>
{' ░░░░░░░░░░░░░░░░░░░ '}
</Text>
<Text>
{' '}
</Text>
<Text>{'…………………………………………………………………………………………………………………………………………………………'}</Text>
<Text>{' '}</Text>
<Text>{' '}</Text>
<Text>{' '}</Text>
<Text>{' ░░░░░░ '}</Text>
<Text>{' ░░░ ░░░░░░░░░░ '}</Text>
<Text>{' ░░░░░░░░░░░░░░░░░░░ '}</Text>
<Text>{' '}</Text>
<Text>
<Text dimColor>{' ░░░░'}</Text>
<Text>{' ██ '}</Text>
@@ -208,12 +147,8 @@ function AppleTerminalWelcomeV2({
<Text dimColor>{' ░░░░░░░░░░'}</Text>
<Text>{' ██▒▒██ '}</Text>
</Text>
<Text>
{' ▒▒ ██ ▒'}
</Text>
<Text>
{' ▒▒░░▒▒ ▒ ▒▒'}
</Text>
<Text>{' ▒▒ ██ ▒'}</Text>
<Text>{' ▒▒░░▒▒ ▒ ▒▒'}</Text>
<Text>
{' '}
<Text color="clawd_body"></Text>
@@ -242,7 +177,7 @@ function AppleTerminalWelcomeV2({
</Text>
</Text>
</Box>
)
);
}
return (
@@ -252,41 +187,21 @@ function AppleTerminalWelcomeV2({
<Text color="claude">{welcomeMessage} </Text>
<Text dimColor>v{MACRO.VERSION} </Text>
</Text>
<Text>
{'…………………………………………………………………………………………………………………………………………………………'}
</Text>
<Text>
{' '}
</Text>
<Text>
{' * █████▓▓░ '}
</Text>
<Text>
{' * ███▓░ ░░ '}
</Text>
<Text>
{' ░░░░░░ ███▓░ '}
</Text>
<Text>
{' ░░░ ░░░░░░░░░░ ███▓░ '}
</Text>
<Text>{'…………………………………………………………………………………………………………………………………………………………'}</Text>
<Text>{' '}</Text>
<Text>{' * █████▓▓░ '}</Text>
<Text>{' * ███▓░ ░░ '}</Text>
<Text>{' ░░░░░░ ███▓░ '}</Text>
<Text>{' ░░░ ░░░░░░░░░░ ███▓░ '}</Text>
<Text>
<Text>{' ░░░░░░░░░░░░░░░░░░░ '}</Text>
<Text bold>*</Text>
<Text>{' ██▓░░ ▓ '}</Text>
</Text>
<Text>
{' ░▓▓███▓▓░ '}
</Text>
<Text dimColor>
{' * ░░░░ '}
</Text>
<Text dimColor>
{' ░░░░░░░░ '}
</Text>
<Text dimColor>
{' ░░░░░░░░░░░░░░░░ '}
</Text>
<Text>{' ░▓▓███▓▓░ '}</Text>
<Text dimColor>{' * ░░░░ '}</Text>
<Text dimColor>{' ░░░░░░░░ '}</Text>
<Text dimColor>{' ░░░░░░░░░░░░░░░░ '}</Text>
<Text>
{' '}
<Text dimColor>*</Text>
@@ -322,5 +237,5 @@ function AppleTerminalWelcomeV2({
</Text>
</Text>
</Box>
)
);
}

View File

@@ -1,103 +1,96 @@
import figures from 'figures'
import { homedir } from 'os'
import * as React from 'react'
import { Box, Text } from '@anthropic/ink'
import type { Step } from '../../projectOnboardingState.js'
import {
formatCreditAmount,
getCachedReferrerReward,
} from '../../services/api/referral.js'
import type { LogOption } from '../../types/logs.js'
import { getCwd } from '../../utils/cwd.js'
import { formatRelativeTimeAgo } from '../../utils/format.js'
import type { FeedConfig, FeedLine } from './Feed.js'
import figures from 'figures';
import { homedir } from 'os';
import * as React from 'react';
import { Box, Text } from '@anthropic/ink';
import type { Step } from '../../projectOnboardingState.js';
import { formatCreditAmount, getCachedReferrerReward } from '../../services/api/referral.js';
import type { LogOption } from '../../types/logs.js';
import { getCwd } from '../../utils/cwd.js';
import { formatRelativeTimeAgo } from '../../utils/format.js';
import type { FeedConfig, FeedLine } from './Feed.js';
export function createRecentActivityFeed(activities: LogOption[]): FeedConfig {
const lines: FeedLine[] = activities.map(log => {
const time = formatRelativeTimeAgo(log.modified)
const description =
log.summary && log.summary !== 'No prompt' ? log.summary : log.firstPrompt
const time = formatRelativeTimeAgo(log.modified);
const description = log.summary && log.summary !== 'No prompt' ? log.summary : log.firstPrompt;
return {
text: description || '',
timestamp: time,
}
})
};
});
return {
title: 'Recent activity',
lines,
footer: lines.length > 0 ? '/resume for more' : undefined,
emptyMessage: 'No recent activity',
}
};
}
export function createWhatsNewFeed(releaseNotes: string[]): FeedConfig {
const lines: FeedLine[] = releaseNotes.map(note => {
if (process.env.USER_TYPE === 'ant') {
const match = note.match(/^(\d+\s+\w+\s+ago)\s+(.+)$/)
const match = note.match(/^(\d+\s+\w+\s+ago)\s+(.+)$/);
if (match) {
return {
timestamp: match[1],
text: match[2] || '',
}
};
}
}
return {
text: note,
}
})
};
});
const emptyMessage =
process.env.USER_TYPE === 'ant'
? 'Unable to fetch latest claude-cli-internal commits'
: 'Check the Claude Code changelog for updates'
: 'Check the Claude Code changelog for updates';
return {
title:
process.env.USER_TYPE === 'ant'
? "What's new [ANT-ONLY: Latest CC commits]"
: "What's new",
title: process.env.USER_TYPE === 'ant' ? "What's new [ANT-ONLY: Latest CC commits]" : "What's new",
lines,
footer: lines.length > 0 ? '/release-notes for more' : undefined,
emptyMessage,
}
};
}
export function createProjectOnboardingFeed(steps: Step[]): FeedConfig {
const enabledSteps = steps
.filter(({ isEnabled }) => isEnabled)
.sort((a, b) => Number(a.isComplete) - Number(b.isComplete))
.sort((a, b) => Number(a.isComplete) - Number(b.isComplete));
const lines: FeedLine[] = enabledSteps.map(({ text, isComplete }) => {
const checkmark = isComplete ? `${figures.tick} ` : ''
const checkmark = isComplete ? `${figures.tick} ` : '';
return {
text: `${checkmark}${text}`,
}
})
};
});
const warningText =
getCwd() === homedir()
? 'Note: You have launched claude in your home directory. For the best experience, launch it in a project directory instead.'
: undefined
: undefined;
if (warningText) {
lines.push({
text: warningText,
})
});
}
return {
title: 'Tips for getting started',
lines,
}
};
}
export function createGuestPassesFeed(): FeedConfig {
const reward = getCachedReferrerReward()
const reward = getCachedReferrerReward();
const subtitle = reward
? `Share Claude Code and earn ${formatCreditAmount(reward)} of extra usage`
: 'Share Claude Code with friends'
: 'Share Claude Code with friends';
return {
title: '3 guest passes',
lines: [],
@@ -113,5 +106,5 @@ export function createGuestPassesFeed(): FeedConfig {
width: 48,
},
footer: '/passes',
}
};
}