687 lines
23 KiB
TypeScript
687 lines
23 KiB
TypeScript
import chalk from 'chalk'
|
|
import { logForDebugging } from 'src/utils/debug.js'
|
|
import { fileHistoryEnabled } from 'src/utils/fileHistory.js'
|
|
import {
|
|
getInitialSettings,
|
|
getSettings_DEPRECATED,
|
|
getSettingsForSource,
|
|
} from 'src/utils/settings/settings.js'
|
|
import { shouldOfferTerminalSetup } from '../../commands/terminalSetup/terminalSetup.js'
|
|
import { getDesktopUpsellConfig } from '../../components/DesktopUpsell/DesktopUpsellStartup.js'
|
|
import { color } from '../../components/design-system/color.js'
|
|
import { shouldShowOverageCreditUpsell } from '../../components/LogoV2/OverageCreditUpsell.js'
|
|
import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js'
|
|
import { isKairosCronEnabled } from '../../tools/ScheduleCronTool/prompt.js'
|
|
import { is1PApiCustomer } from '../../utils/auth.js'
|
|
import { countConcurrentSessions } from '../../utils/concurrentSessions.js'
|
|
import { getGlobalConfig } from '../../utils/config.js'
|
|
import {
|
|
getEffortEnvOverride,
|
|
modelSupportsEffort,
|
|
} from '../../utils/effort.js'
|
|
import { env } from '../../utils/env.js'
|
|
import { cacheKeys } from '../../utils/fileStateCache.js'
|
|
import { getWorktreeCount } from '../../utils/git.js'
|
|
import {
|
|
detectRunningIDEsCached,
|
|
getSortedIdeLockfiles,
|
|
isCursorInstalled,
|
|
isSupportedTerminal,
|
|
isSupportedVSCodeTerminal,
|
|
isVSCodeInstalled,
|
|
isWindsurfInstalled,
|
|
} from '../../utils/ide.js'
|
|
import {
|
|
getMainLoopModel,
|
|
getUserSpecifiedModelSetting,
|
|
} from '../../utils/model/model.js'
|
|
import { getPlatform } from '../../utils/platform.js'
|
|
import { isPluginInstalled } from '../../utils/plugins/installedPluginsManager.js'
|
|
import { loadKnownMarketplacesConfigSafe } from '../../utils/plugins/marketplaceManager.js'
|
|
import { OFFICIAL_MARKETPLACE_NAME } from '../../utils/plugins/officialMarketplace.js'
|
|
import {
|
|
getCurrentSessionAgentColor,
|
|
isCustomTitleEnabled,
|
|
} from '../../utils/sessionStorage.js'
|
|
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js'
|
|
import {
|
|
formatGrantAmount,
|
|
getCachedOverageCreditGrant,
|
|
} from '../api/overageCreditGrant.js'
|
|
import {
|
|
checkCachedPassesEligibility,
|
|
formatCreditAmount,
|
|
getCachedReferrerReward,
|
|
} from '../api/referral.js'
|
|
import { getSessionsSinceLastShown } from './tipHistory.js'
|
|
import type { Tip, TipContext } from './types.js'
|
|
|
|
let _isOfficialMarketplaceInstalledCache: boolean | undefined
|
|
async function isOfficialMarketplaceInstalled(): Promise<boolean> {
|
|
if (_isOfficialMarketplaceInstalledCache !== undefined) {
|
|
return _isOfficialMarketplaceInstalledCache
|
|
}
|
|
const config = await loadKnownMarketplacesConfigSafe()
|
|
_isOfficialMarketplaceInstalledCache = OFFICIAL_MARKETPLACE_NAME in config
|
|
return _isOfficialMarketplaceInstalledCache
|
|
}
|
|
|
|
async function isMarketplacePluginRelevant(
|
|
pluginName: string,
|
|
context: TipContext | undefined,
|
|
signals: { filePath?: RegExp; cli?: string[] },
|
|
): Promise<boolean> {
|
|
if (!(await isOfficialMarketplaceInstalled())) {
|
|
return false
|
|
}
|
|
if (isPluginInstalled(`${pluginName}@${OFFICIAL_MARKETPLACE_NAME}`)) {
|
|
return false
|
|
}
|
|
const { bashTools } = context ?? {}
|
|
if (signals.cli && bashTools?.size) {
|
|
if (signals.cli.some(cmd => bashTools.has(cmd))) {
|
|
return true
|
|
}
|
|
}
|
|
if (signals.filePath && context?.readFileState) {
|
|
const readFiles = cacheKeys(context.readFileState)
|
|
if (readFiles.some(fp => signals.filePath!.test(fp))) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
const externalTips: Tip[] = [
|
|
{
|
|
id: 'new-user-warmup',
|
|
content: async () =>
|
|
`Start with small features or bug fixes, tell Claude to propose a plan, and verify its suggested edits`,
|
|
cooldownSessions: 3,
|
|
async isRelevant() {
|
|
const config = getGlobalConfig()
|
|
return config.numStartups < 10
|
|
},
|
|
},
|
|
{
|
|
id: 'plan-mode-for-complex-tasks',
|
|
content: async () =>
|
|
`Use Plan Mode to prepare for a complex request before making changes. Press ${getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab')} twice to enable.`,
|
|
cooldownSessions: 5,
|
|
isRelevant: async () => {
|
|
if (process.env.USER_TYPE === 'ant') return false
|
|
const config = getGlobalConfig()
|
|
// Show to users who haven't used plan mode recently (7+ days)
|
|
const daysSinceLastUse = config.lastPlanModeUse
|
|
? (Date.now() - config.lastPlanModeUse) / (1000 * 60 * 60 * 24)
|
|
: Infinity
|
|
return daysSinceLastUse > 7
|
|
},
|
|
},
|
|
{
|
|
id: 'default-permission-mode-config',
|
|
content: async () =>
|
|
`Use /config to change your default permission mode (including Plan Mode)`,
|
|
cooldownSessions: 10,
|
|
isRelevant: async () => {
|
|
try {
|
|
const config = getGlobalConfig()
|
|
const settings = getSettings_DEPRECATED()
|
|
// Show if they've used plan mode but haven't set a default
|
|
const hasUsedPlanMode = Boolean(config.lastPlanModeUse)
|
|
const hasDefaultMode = Boolean(settings?.permissions?.defaultMode)
|
|
return hasUsedPlanMode && !hasDefaultMode
|
|
} catch (error) {
|
|
logForDebugging(
|
|
`Failed to check default-permission-mode-config tip relevance: ${error}`,
|
|
{ level: 'warn' },
|
|
)
|
|
return false
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: 'git-worktrees',
|
|
content: async () =>
|
|
'Use git worktrees to run multiple Claude sessions in parallel.',
|
|
cooldownSessions: 10,
|
|
isRelevant: async () => {
|
|
try {
|
|
const config = getGlobalConfig()
|
|
const worktreeCount = await getWorktreeCount()
|
|
return worktreeCount <= 1 && config.numStartups > 50
|
|
} catch (_) {
|
|
return false
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: 'color-when-multi-clauding',
|
|
content: async () =>
|
|
'Running multiple Claude sessions? Use /color and /rename to tell them apart at a glance.',
|
|
cooldownSessions: 10,
|
|
isRelevant: async () => {
|
|
if (getCurrentSessionAgentColor()) return false
|
|
const count = await countConcurrentSessions()
|
|
return count >= 2
|
|
},
|
|
},
|
|
{
|
|
id: 'terminal-setup',
|
|
content: async () =>
|
|
env.terminal === 'Apple_Terminal'
|
|
? 'Run /terminal-setup to enable convenient terminal integration like Option + Enter for new line and more'
|
|
: 'Run /terminal-setup to enable convenient terminal integration like Shift + Enter for new line and more',
|
|
cooldownSessions: 10,
|
|
async isRelevant() {
|
|
const config = getGlobalConfig()
|
|
if (env.terminal === 'Apple_Terminal') {
|
|
return !config.optionAsMetaKeyInstalled
|
|
}
|
|
return !config.shiftEnterKeyBindingInstalled
|
|
},
|
|
},
|
|
{
|
|
id: 'shift-enter',
|
|
content: async () =>
|
|
env.terminal === 'Apple_Terminal'
|
|
? 'Press Option+Enter to send a multi-line message'
|
|
: 'Press Shift+Enter to send a multi-line message',
|
|
cooldownSessions: 10,
|
|
async isRelevant() {
|
|
const config = getGlobalConfig()
|
|
return Boolean(
|
|
(env.terminal === 'Apple_Terminal'
|
|
? config.optionAsMetaKeyInstalled
|
|
: config.shiftEnterKeyBindingInstalled) && config.numStartups > 3,
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'shift-enter-setup',
|
|
content: async () =>
|
|
env.terminal === 'Apple_Terminal'
|
|
? 'Run /terminal-setup to enable Option+Enter for new lines'
|
|
: 'Run /terminal-setup to enable Shift+Enter for new lines',
|
|
cooldownSessions: 10,
|
|
async isRelevant() {
|
|
if (!shouldOfferTerminalSetup()) {
|
|
return false
|
|
}
|
|
const config = getGlobalConfig()
|
|
return !(env.terminal === 'Apple_Terminal'
|
|
? config.optionAsMetaKeyInstalled
|
|
: config.shiftEnterKeyBindingInstalled)
|
|
},
|
|
},
|
|
{
|
|
id: 'memory-command',
|
|
content: async () => 'Use /memory to view and manage Claude memory',
|
|
cooldownSessions: 15,
|
|
async isRelevant() {
|
|
const config = getGlobalConfig()
|
|
return config.memoryUsageCount <= 0
|
|
},
|
|
},
|
|
{
|
|
id: 'theme-command',
|
|
content: async () => 'Use /theme to change the color theme',
|
|
cooldownSessions: 20,
|
|
isRelevant: async () => true,
|
|
},
|
|
{
|
|
id: 'colorterm-truecolor',
|
|
content: async () =>
|
|
'Try setting environment variable COLORTERM=truecolor for richer colors',
|
|
cooldownSessions: 30,
|
|
isRelevant: async () => !process.env.COLORTERM && chalk.level < 3,
|
|
},
|
|
{
|
|
id: 'powershell-tool-env',
|
|
content: async () =>
|
|
'Set CLAUDE_CODE_USE_POWERSHELL_TOOL=1 to enable the PowerShell tool (preview)',
|
|
cooldownSessions: 10,
|
|
isRelevant: async () =>
|
|
getPlatform() === 'windows' &&
|
|
process.env.CLAUDE_CODE_USE_POWERSHELL_TOOL === undefined,
|
|
},
|
|
{
|
|
id: 'status-line',
|
|
content: async () =>
|
|
'Use /statusline to set up a custom status line that will display beneath the input box',
|
|
cooldownSessions: 25,
|
|
isRelevant: async () => getSettings_DEPRECATED().statusLine === undefined,
|
|
},
|
|
{
|
|
id: 'prompt-queue',
|
|
content: async () =>
|
|
'Hit Enter to queue up additional messages while Claude is working.',
|
|
cooldownSessions: 5,
|
|
async isRelevant() {
|
|
const config = getGlobalConfig()
|
|
return config.promptQueueUseCount <= 3
|
|
},
|
|
},
|
|
{
|
|
id: 'enter-to-steer-in-relatime',
|
|
content: async () =>
|
|
'Send messages to Claude while it works to steer Claude in real-time',
|
|
cooldownSessions: 20,
|
|
isRelevant: async () => true,
|
|
},
|
|
{
|
|
id: 'todo-list',
|
|
content: async () =>
|
|
'Ask Claude to create a todo list when working on complex tasks to track progress and remain on track',
|
|
cooldownSessions: 20,
|
|
isRelevant: async () => true,
|
|
},
|
|
{
|
|
id: 'vscode-command-install',
|
|
content: async () =>
|
|
`Open the Command Palette (Cmd+Shift+P) and run "Shell Command: Install '${env.terminal === 'vscode' ? 'code' : env.terminal}' command in PATH" to enable IDE integration`,
|
|
cooldownSessions: 0,
|
|
async isRelevant() {
|
|
// Only show this tip if we're in a VS Code-style terminal
|
|
if (!isSupportedVSCodeTerminal()) {
|
|
return false
|
|
}
|
|
if (getPlatform() !== 'macos') {
|
|
return false
|
|
}
|
|
|
|
// Check if the relevant command is available
|
|
switch (env.terminal) {
|
|
case 'vscode':
|
|
return !(await isVSCodeInstalled())
|
|
case 'cursor':
|
|
return !(await isCursorInstalled())
|
|
case 'windsurf':
|
|
return !(await isWindsurfInstalled())
|
|
default:
|
|
return false
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: 'ide-upsell-external-terminal',
|
|
content: async () => 'Connect Claude to your IDE · /ide',
|
|
cooldownSessions: 4,
|
|
async isRelevant() {
|
|
if (isSupportedTerminal()) {
|
|
return false
|
|
}
|
|
|
|
// Use lockfiles as a (quicker) signal for running IDEs
|
|
const lockfiles = await getSortedIdeLockfiles()
|
|
if (lockfiles.length !== 0) {
|
|
return false
|
|
}
|
|
|
|
const runningIDEs = await detectRunningIDEsCached()
|
|
return runningIDEs.length > 0
|
|
},
|
|
},
|
|
{
|
|
id: 'install-github-app',
|
|
content: async () =>
|
|
'Run /install-github-app to tag @claude right from your Github issues and PRs',
|
|
cooldownSessions: 10,
|
|
isRelevant: async () => !getGlobalConfig().githubActionSetupCount,
|
|
},
|
|
{
|
|
id: 'install-slack-app',
|
|
content: async () => 'Run /install-slack-app to use Claude in Slack',
|
|
cooldownSessions: 10,
|
|
isRelevant: async () => !getGlobalConfig().slackAppInstallCount,
|
|
},
|
|
{
|
|
id: 'permissions',
|
|
content: async () =>
|
|
'Use /permissions to pre-approve and pre-deny bash, edit, and MCP tools',
|
|
cooldownSessions: 10,
|
|
async isRelevant() {
|
|
const config = getGlobalConfig()
|
|
return config.numStartups > 10
|
|
},
|
|
},
|
|
{
|
|
id: 'drag-and-drop-images',
|
|
content: async () =>
|
|
'Did you know you can drag and drop image files into your terminal?',
|
|
cooldownSessions: 10,
|
|
isRelevant: async () => !env.isSSH(),
|
|
},
|
|
{
|
|
id: 'paste-images-mac',
|
|
content: async () =>
|
|
'Paste images into Claude Code using control+v (not cmd+v!)',
|
|
cooldownSessions: 10,
|
|
isRelevant: async () => getPlatform() === 'macos',
|
|
},
|
|
{
|
|
id: 'double-esc',
|
|
content: async () =>
|
|
'Double-tap esc to rewind the conversation to a previous point in time',
|
|
cooldownSessions: 10,
|
|
isRelevant: async () => !fileHistoryEnabled(),
|
|
},
|
|
{
|
|
id: 'double-esc-code-restore',
|
|
content: async () =>
|
|
'Double-tap esc to rewind the code and/or conversation to a previous point in time',
|
|
cooldownSessions: 10,
|
|
isRelevant: async () => fileHistoryEnabled(),
|
|
},
|
|
{
|
|
id: 'continue',
|
|
content: async () =>
|
|
'Run claude --continue or claude --resume to resume a conversation',
|
|
cooldownSessions: 10,
|
|
isRelevant: async () => true,
|
|
},
|
|
{
|
|
id: 'rename-conversation',
|
|
content: async () =>
|
|
'Name your conversations with /rename to find them easily in /resume later',
|
|
cooldownSessions: 15,
|
|
isRelevant: async () =>
|
|
isCustomTitleEnabled() && getGlobalConfig().numStartups > 10,
|
|
},
|
|
{
|
|
id: 'custom-commands',
|
|
content: async () =>
|
|
'Create skills by adding .md files to .claude/skills/ in your project or ~/.claude/skills/ for skills that work in any project',
|
|
cooldownSessions: 15,
|
|
async isRelevant() {
|
|
const config = getGlobalConfig()
|
|
return config.numStartups > 10
|
|
},
|
|
},
|
|
{
|
|
id: 'shift-tab',
|
|
content: async () =>
|
|
process.env.USER_TYPE === 'ant'
|
|
? `Hit ${getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab')} to cycle between default mode and auto mode`
|
|
: `Hit ${getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab')} to cycle between default mode, auto-accept edit mode, and plan mode`,
|
|
cooldownSessions: 10,
|
|
isRelevant: async () => true,
|
|
},
|
|
{
|
|
id: 'image-paste',
|
|
content: async () =>
|
|
`Use ${getShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v')} to paste images from your clipboard`,
|
|
cooldownSessions: 20,
|
|
isRelevant: async () => true,
|
|
},
|
|
{
|
|
id: 'custom-agents',
|
|
content: async () =>
|
|
'Use /agents to optimize specific tasks. Eg. Software Architect, Code Writer, Code Reviewer',
|
|
cooldownSessions: 15,
|
|
async isRelevant() {
|
|
const config = getGlobalConfig()
|
|
return config.numStartups > 5
|
|
},
|
|
},
|
|
{
|
|
id: 'agent-flag',
|
|
content: async () =>
|
|
'Use --agent <agent_name> to directly start a conversation with a subagent',
|
|
cooldownSessions: 15,
|
|
async isRelevant() {
|
|
const config = getGlobalConfig()
|
|
return config.numStartups > 5
|
|
},
|
|
},
|
|
{
|
|
id: 'desktop-app',
|
|
content: async () =>
|
|
'Run Claude Code locally or remotely using the Claude desktop app: clau.de/desktop',
|
|
cooldownSessions: 15,
|
|
isRelevant: async () => getPlatform() !== 'linux',
|
|
},
|
|
{
|
|
id: 'desktop-shortcut',
|
|
content: async ctx => {
|
|
const blue = color('suggestion', ctx.theme)
|
|
return `Continue your session in Claude Code Desktop with ${blue('/desktop')}`
|
|
},
|
|
cooldownSessions: 15,
|
|
isRelevant: async () => {
|
|
if (!getDesktopUpsellConfig().enable_shortcut_tip) return false
|
|
return (
|
|
process.platform === 'darwin' ||
|
|
(process.platform === 'win32' && process.arch === 'x64')
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'web-app',
|
|
content: async () =>
|
|
'Run tasks in the cloud while you keep coding locally · clau.de/web',
|
|
cooldownSessions: 15,
|
|
isRelevant: async () => true,
|
|
},
|
|
{
|
|
id: 'mobile-app',
|
|
content: async () =>
|
|
'/mobile to use Claude Code from the Claude app on your phone',
|
|
cooldownSessions: 15,
|
|
isRelevant: async () => true,
|
|
},
|
|
{
|
|
id: 'opusplan-mode-reminder',
|
|
content: async () =>
|
|
`Your default model setting is Opus Plan Mode. Press ${getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab')} twice to activate Plan Mode and plan with Claude Opus.`,
|
|
cooldownSessions: 2,
|
|
async isRelevant() {
|
|
if (process.env.USER_TYPE === 'ant') return false
|
|
const config = getGlobalConfig()
|
|
const modelSetting = getUserSpecifiedModelSetting()
|
|
const hasOpusPlanMode = modelSetting === 'opusplan'
|
|
// Show reminder if they have Opus Plan Mode and haven't used plan mode recently (3+ days)
|
|
const daysSinceLastUse = config.lastPlanModeUse
|
|
? (Date.now() - config.lastPlanModeUse) / (1000 * 60 * 60 * 24)
|
|
: Infinity
|
|
return hasOpusPlanMode && daysSinceLastUse > 3
|
|
},
|
|
},
|
|
{
|
|
id: 'frontend-design-plugin',
|
|
content: async ctx => {
|
|
const blue = color('suggestion', ctx.theme)
|
|
return `Working with HTML/CSS? Install the frontend-design plugin:\n${blue(`/plugin install frontend-design@${OFFICIAL_MARKETPLACE_NAME}`)}`
|
|
},
|
|
cooldownSessions: 3,
|
|
isRelevant: async context =>
|
|
isMarketplacePluginRelevant('frontend-design', context, {
|
|
filePath: /\.(html|css|htm)$/i,
|
|
}),
|
|
},
|
|
{
|
|
id: 'vercel-plugin',
|
|
content: async ctx => {
|
|
const blue = color('suggestion', ctx.theme)
|
|
return `Working with Vercel? Install the vercel plugin:\n${blue(`/plugin install vercel@${OFFICIAL_MARKETPLACE_NAME}`)}`
|
|
},
|
|
cooldownSessions: 3,
|
|
isRelevant: async context =>
|
|
isMarketplacePluginRelevant('vercel', context, {
|
|
filePath: /(?:^|[/\\])vercel\.json$/i,
|
|
cli: ['vercel'],
|
|
}),
|
|
},
|
|
{
|
|
id: 'effort-high-nudge',
|
|
content: async ctx => {
|
|
const blue = color('suggestion', ctx.theme)
|
|
const cmd = blue('/effort high')
|
|
const variant = getFeatureValue_CACHED_MAY_BE_STALE<
|
|
'off' | 'copy_a' | 'copy_b'
|
|
>('tengu_tide_elm', 'off')
|
|
return variant === 'copy_b'
|
|
? `Use ${cmd} for better one-shot answers. Claude thinks it through first.`
|
|
: `Working on something tricky? ${cmd} gives better first answers`
|
|
},
|
|
cooldownSessions: 3,
|
|
isRelevant: async () => {
|
|
if (!is1PApiCustomer()) return false
|
|
if (!modelSupportsEffort(getMainLoopModel())) return false
|
|
if (getSettingsForSource('policySettings')?.effortLevel !== undefined) {
|
|
return false
|
|
}
|
|
if (getEffortEnvOverride() !== undefined) return false
|
|
const persisted = getInitialSettings().effortLevel
|
|
if (persisted === 'high' || persisted === 'max') return false
|
|
return (
|
|
getFeatureValue_CACHED_MAY_BE_STALE<'off' | 'copy_a' | 'copy_b'>(
|
|
'tengu_tide_elm',
|
|
'off',
|
|
) !== 'off'
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'subagent-fanout-nudge',
|
|
content: async ctx => {
|
|
const blue = color('suggestion', ctx.theme)
|
|
const variant = getFeatureValue_CACHED_MAY_BE_STALE<
|
|
'off' | 'copy_a' | 'copy_b'
|
|
>('tengu_tern_alloy', 'off')
|
|
return variant === 'copy_b'
|
|
? `For big tasks, tell Claude to ${blue('use subagents')}. They work in parallel and keep your main thread clean.`
|
|
: `Say ${blue('"fan out subagents"')} and Claude sends a team. Each one digs deep so nothing gets missed.`
|
|
},
|
|
cooldownSessions: 3,
|
|
isRelevant: async () => {
|
|
if (!is1PApiCustomer()) return false
|
|
return (
|
|
getFeatureValue_CACHED_MAY_BE_STALE<'off' | 'copy_a' | 'copy_b'>(
|
|
'tengu_tern_alloy',
|
|
'off',
|
|
) !== 'off'
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'loop-command-nudge',
|
|
content: async ctx => {
|
|
const blue = color('suggestion', ctx.theme)
|
|
const variant = getFeatureValue_CACHED_MAY_BE_STALE<
|
|
'off' | 'copy_a' | 'copy_b'
|
|
>('tengu_timber_lark', 'off')
|
|
return variant === 'copy_b'
|
|
? `Use ${blue('/loop 5m check the deploy')} to run any prompt on a schedule. Set it and forget it.`
|
|
: `${blue('/loop')} runs any prompt on a recurring schedule. Great for monitoring deploys, babysitting PRs, or polling status.`
|
|
},
|
|
cooldownSessions: 3,
|
|
isRelevant: async () => {
|
|
if (!is1PApiCustomer()) return false
|
|
if (!isKairosCronEnabled()) return false
|
|
return (
|
|
getFeatureValue_CACHED_MAY_BE_STALE<'off' | 'copy_a' | 'copy_b'>(
|
|
'tengu_timber_lark',
|
|
'off',
|
|
) !== 'off'
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'guest-passes',
|
|
content: async ctx => {
|
|
const claude = color('claude', ctx.theme)
|
|
const reward = getCachedReferrerReward()
|
|
return reward
|
|
? `Share Claude Code and earn ${claude(formatCreditAmount(reward))} of extra usage · ${claude('/passes')}`
|
|
: `You have free guest passes to share · ${claude('/passes')}`
|
|
},
|
|
cooldownSessions: 3,
|
|
isRelevant: async () => {
|
|
const config = getGlobalConfig()
|
|
if (config.hasVisitedPasses) {
|
|
return false
|
|
}
|
|
const { eligible } = checkCachedPassesEligibility()
|
|
return eligible
|
|
},
|
|
},
|
|
{
|
|
id: 'overage-credit',
|
|
content: async ctx => {
|
|
const claude = color('claude', ctx.theme)
|
|
const info = getCachedOverageCreditGrant()
|
|
const amount = info ? formatGrantAmount(info) : null
|
|
if (!amount) return ''
|
|
// Copy from "OC & Bulk Overages copy" doc (#5 — CLI Rotating tip)
|
|
return `${claude(`${amount} in extra usage, on us`)} · third-party apps · ${claude('/extra-usage')}`
|
|
},
|
|
cooldownSessions: 3,
|
|
isRelevant: async () => shouldShowOverageCreditUpsell(),
|
|
},
|
|
{
|
|
id: 'feedback-command',
|
|
content: async () => 'Use /feedback to help us improve!',
|
|
cooldownSessions: 15,
|
|
async isRelevant() {
|
|
if (process.env.USER_TYPE === 'ant') {
|
|
return false
|
|
}
|
|
const config = getGlobalConfig()
|
|
return config.numStartups > 5
|
|
},
|
|
},
|
|
]
|
|
const internalOnlyTips: Tip[] =
|
|
process.env.USER_TYPE === 'ant'
|
|
? [
|
|
{
|
|
id: 'important-claudemd',
|
|
content: async () =>
|
|
'[ANT-ONLY] Use "IMPORTANT:" prefix for must-follow CLAUDE.md rules',
|
|
cooldownSessions: 30,
|
|
isRelevant: async () => true,
|
|
},
|
|
{
|
|
id: 'skillify',
|
|
content: async () =>
|
|
'[ANT-ONLY] Use /skillify at the end of a workflow to turn it into a reusable skill',
|
|
cooldownSessions: 15,
|
|
isRelevant: async () => true,
|
|
},
|
|
]
|
|
: []
|
|
|
|
function getCustomTips(): Tip[] {
|
|
const settings = getInitialSettings()
|
|
const override = settings.spinnerTipsOverride
|
|
if (!override?.tips?.length) return []
|
|
|
|
return override.tips.map((content, i) => ({
|
|
id: `custom-tip-${i}`,
|
|
content: async () => content,
|
|
cooldownSessions: 0,
|
|
isRelevant: async () => true,
|
|
}))
|
|
}
|
|
|
|
export async function getRelevantTips(context?: TipContext): Promise<Tip[]> {
|
|
const settings = getInitialSettings()
|
|
const override = settings.spinnerTipsOverride
|
|
const customTips = getCustomTips()
|
|
|
|
// If excludeDefault is true and there are custom tips, skip built-in tips entirely
|
|
if (override?.excludeDefault && customTips.length > 0) {
|
|
return customTips
|
|
}
|
|
|
|
// Otherwise, filter built-in tips as before and combine with custom
|
|
const tips = [...externalTips, ...internalOnlyTips]
|
|
const isRelevant = await Promise.all(tips.map(_ => _.isRelevant(context)))
|
|
const filtered = tips
|
|
.filter((_, index) => isRelevant[index])
|
|
.filter(_ => getSessionsSinceLastShown(_.id) >= _.cooldownSessions)
|
|
|
|
return [...filtered, ...customTips]
|
|
}
|