mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05:51 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 />}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user