| 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() |
| |
| 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() |
| |
| 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() { |
| |
| if (!isSupportedVSCodeTerminal()) { |
| return false |
| } |
| if (getPlatform() !== 'macos') { |
| return false |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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' |
| |
| 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 '' |
| |
| 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 (override?.excludeDefault && customTips.length > 0) { |
| return customTips |
| } |
|
|
| |
| 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] |
| } |
|
|