source dump of claude code
at main 686 lines 23 kB view raw
1import chalk from 'chalk' 2import { logForDebugging } from 'src/utils/debug.js' 3import { fileHistoryEnabled } from 'src/utils/fileHistory.js' 4import { 5 getInitialSettings, 6 getSettings_DEPRECATED, 7 getSettingsForSource, 8} from 'src/utils/settings/settings.js' 9import { shouldOfferTerminalSetup } from '../../commands/terminalSetup/terminalSetup.js' 10import { getDesktopUpsellConfig } from '../../components/DesktopUpsell/DesktopUpsellStartup.js' 11import { color } from '../../components/design-system/color.js' 12import { shouldShowOverageCreditUpsell } from '../../components/LogoV2/OverageCreditUpsell.js' 13import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js' 14import { isKairosCronEnabled } from '../../tools/ScheduleCronTool/prompt.js' 15import { is1PApiCustomer } from '../../utils/auth.js' 16import { countConcurrentSessions } from '../../utils/concurrentSessions.js' 17import { getGlobalConfig } from '../../utils/config.js' 18import { 19 getEffortEnvOverride, 20 modelSupportsEffort, 21} from '../../utils/effort.js' 22import { env } from '../../utils/env.js' 23import { cacheKeys } from '../../utils/fileStateCache.js' 24import { getWorktreeCount } from '../../utils/git.js' 25import { 26 detectRunningIDEsCached, 27 getSortedIdeLockfiles, 28 isCursorInstalled, 29 isSupportedTerminal, 30 isSupportedVSCodeTerminal, 31 isVSCodeInstalled, 32 isWindsurfInstalled, 33} from '../../utils/ide.js' 34import { 35 getMainLoopModel, 36 getUserSpecifiedModelSetting, 37} from '../../utils/model/model.js' 38import { getPlatform } from '../../utils/platform.js' 39import { isPluginInstalled } from '../../utils/plugins/installedPluginsManager.js' 40import { loadKnownMarketplacesConfigSafe } from '../../utils/plugins/marketplaceManager.js' 41import { OFFICIAL_MARKETPLACE_NAME } from '../../utils/plugins/officialMarketplace.js' 42import { 43 getCurrentSessionAgentColor, 44 isCustomTitleEnabled, 45} from '../../utils/sessionStorage.js' 46import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' 47import { 48 formatGrantAmount, 49 getCachedOverageCreditGrant, 50} from '../api/overageCreditGrant.js' 51import { 52 checkCachedPassesEligibility, 53 formatCreditAmount, 54 getCachedReferrerReward, 55} from '../api/referral.js' 56import { getSessionsSinceLastShown } from './tipHistory.js' 57import type { Tip, TipContext } from './types.js' 58 59let _isOfficialMarketplaceInstalledCache: boolean | undefined 60async function isOfficialMarketplaceInstalled(): Promise<boolean> { 61 if (_isOfficialMarketplaceInstalledCache !== undefined) { 62 return _isOfficialMarketplaceInstalledCache 63 } 64 const config = await loadKnownMarketplacesConfigSafe() 65 _isOfficialMarketplaceInstalledCache = OFFICIAL_MARKETPLACE_NAME in config 66 return _isOfficialMarketplaceInstalledCache 67} 68 69async function isMarketplacePluginRelevant( 70 pluginName: string, 71 context: TipContext | undefined, 72 signals: { filePath?: RegExp; cli?: string[] }, 73): Promise<boolean> { 74 if (!(await isOfficialMarketplaceInstalled())) { 75 return false 76 } 77 if (isPluginInstalled(`${pluginName}@${OFFICIAL_MARKETPLACE_NAME}`)) { 78 return false 79 } 80 const { bashTools } = context ?? {} 81 if (signals.cli && bashTools?.size) { 82 if (signals.cli.some(cmd => bashTools.has(cmd))) { 83 return true 84 } 85 } 86 if (signals.filePath && context?.readFileState) { 87 const readFiles = cacheKeys(context.readFileState) 88 if (readFiles.some(fp => signals.filePath!.test(fp))) { 89 return true 90 } 91 } 92 return false 93} 94 95const externalTips: Tip[] = [ 96 { 97 id: 'new-user-warmup', 98 content: async () => 99 `Start with small features or bug fixes, tell Claude to propose a plan, and verify its suggested edits`, 100 cooldownSessions: 3, 101 async isRelevant() { 102 const config = getGlobalConfig() 103 return config.numStartups < 10 104 }, 105 }, 106 { 107 id: 'plan-mode-for-complex-tasks', 108 content: async () => 109 `Use Plan Mode to prepare for a complex request before making changes. Press ${getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab')} twice to enable.`, 110 cooldownSessions: 5, 111 isRelevant: async () => { 112 if (process.env.USER_TYPE === 'ant') return false 113 const config = getGlobalConfig() 114 // Show to users who haven't used plan mode recently (7+ days) 115 const daysSinceLastUse = config.lastPlanModeUse 116 ? (Date.now() - config.lastPlanModeUse) / (1000 * 60 * 60 * 24) 117 : Infinity 118 return daysSinceLastUse > 7 119 }, 120 }, 121 { 122 id: 'default-permission-mode-config', 123 content: async () => 124 `Use /config to change your default permission mode (including Plan Mode)`, 125 cooldownSessions: 10, 126 isRelevant: async () => { 127 try { 128 const config = getGlobalConfig() 129 const settings = getSettings_DEPRECATED() 130 // Show if they've used plan mode but haven't set a default 131 const hasUsedPlanMode = Boolean(config.lastPlanModeUse) 132 const hasDefaultMode = Boolean(settings?.permissions?.defaultMode) 133 return hasUsedPlanMode && !hasDefaultMode 134 } catch (error) { 135 logForDebugging( 136 `Failed to check default-permission-mode-config tip relevance: ${error}`, 137 { level: 'warn' }, 138 ) 139 return false 140 } 141 }, 142 }, 143 { 144 id: 'git-worktrees', 145 content: async () => 146 'Use git worktrees to run multiple Claude sessions in parallel.', 147 cooldownSessions: 10, 148 isRelevant: async () => { 149 try { 150 const config = getGlobalConfig() 151 const worktreeCount = await getWorktreeCount() 152 return worktreeCount <= 1 && config.numStartups > 50 153 } catch (_) { 154 return false 155 } 156 }, 157 }, 158 { 159 id: 'color-when-multi-clauding', 160 content: async () => 161 'Running multiple Claude sessions? Use /color and /rename to tell them apart at a glance.', 162 cooldownSessions: 10, 163 isRelevant: async () => { 164 if (getCurrentSessionAgentColor()) return false 165 const count = await countConcurrentSessions() 166 return count >= 2 167 }, 168 }, 169 { 170 id: 'terminal-setup', 171 content: async () => 172 env.terminal === 'Apple_Terminal' 173 ? 'Run /terminal-setup to enable convenient terminal integration like Option + Enter for new line and more' 174 : 'Run /terminal-setup to enable convenient terminal integration like Shift + Enter for new line and more', 175 cooldownSessions: 10, 176 async isRelevant() { 177 const config = getGlobalConfig() 178 if (env.terminal === 'Apple_Terminal') { 179 return !config.optionAsMetaKeyInstalled 180 } 181 return !config.shiftEnterKeyBindingInstalled 182 }, 183 }, 184 { 185 id: 'shift-enter', 186 content: async () => 187 env.terminal === 'Apple_Terminal' 188 ? 'Press Option+Enter to send a multi-line message' 189 : 'Press Shift+Enter to send a multi-line message', 190 cooldownSessions: 10, 191 async isRelevant() { 192 const config = getGlobalConfig() 193 return Boolean( 194 (env.terminal === 'Apple_Terminal' 195 ? config.optionAsMetaKeyInstalled 196 : config.shiftEnterKeyBindingInstalled) && config.numStartups > 3, 197 ) 198 }, 199 }, 200 { 201 id: 'shift-enter-setup', 202 content: async () => 203 env.terminal === 'Apple_Terminal' 204 ? 'Run /terminal-setup to enable Option+Enter for new lines' 205 : 'Run /terminal-setup to enable Shift+Enter for new lines', 206 cooldownSessions: 10, 207 async isRelevant() { 208 if (!shouldOfferTerminalSetup()) { 209 return false 210 } 211 const config = getGlobalConfig() 212 return !(env.terminal === 'Apple_Terminal' 213 ? config.optionAsMetaKeyInstalled 214 : config.shiftEnterKeyBindingInstalled) 215 }, 216 }, 217 { 218 id: 'memory-command', 219 content: async () => 'Use /memory to view and manage Claude memory', 220 cooldownSessions: 15, 221 async isRelevant() { 222 const config = getGlobalConfig() 223 return config.memoryUsageCount <= 0 224 }, 225 }, 226 { 227 id: 'theme-command', 228 content: async () => 'Use /theme to change the color theme', 229 cooldownSessions: 20, 230 isRelevant: async () => true, 231 }, 232 { 233 id: 'colorterm-truecolor', 234 content: async () => 235 'Try setting environment variable COLORTERM=truecolor for richer colors', 236 cooldownSessions: 30, 237 isRelevant: async () => !process.env.COLORTERM && chalk.level < 3, 238 }, 239 { 240 id: 'powershell-tool-env', 241 content: async () => 242 'Set CLAUDE_CODE_USE_POWERSHELL_TOOL=1 to enable the PowerShell tool (preview)', 243 cooldownSessions: 10, 244 isRelevant: async () => 245 getPlatform() === 'windows' && 246 process.env.CLAUDE_CODE_USE_POWERSHELL_TOOL === undefined, 247 }, 248 { 249 id: 'status-line', 250 content: async () => 251 'Use /statusline to set up a custom status line that will display beneath the input box', 252 cooldownSessions: 25, 253 isRelevant: async () => getSettings_DEPRECATED().statusLine === undefined, 254 }, 255 { 256 id: 'prompt-queue', 257 content: async () => 258 'Hit Enter to queue up additional messages while Claude is working.', 259 cooldownSessions: 5, 260 async isRelevant() { 261 const config = getGlobalConfig() 262 return config.promptQueueUseCount <= 3 263 }, 264 }, 265 { 266 id: 'enter-to-steer-in-relatime', 267 content: async () => 268 'Send messages to Claude while it works to steer Claude in real-time', 269 cooldownSessions: 20, 270 isRelevant: async () => true, 271 }, 272 { 273 id: 'todo-list', 274 content: async () => 275 'Ask Claude to create a todo list when working on complex tasks to track progress and remain on track', 276 cooldownSessions: 20, 277 isRelevant: async () => true, 278 }, 279 { 280 id: 'vscode-command-install', 281 content: async () => 282 `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`, 283 cooldownSessions: 0, 284 async isRelevant() { 285 // Only show this tip if we're in a VS Code-style terminal 286 if (!isSupportedVSCodeTerminal()) { 287 return false 288 } 289 if (getPlatform() !== 'macos') { 290 return false 291 } 292 293 // Check if the relevant command is available 294 switch (env.terminal) { 295 case 'vscode': 296 return !(await isVSCodeInstalled()) 297 case 'cursor': 298 return !(await isCursorInstalled()) 299 case 'windsurf': 300 return !(await isWindsurfInstalled()) 301 default: 302 return false 303 } 304 }, 305 }, 306 { 307 id: 'ide-upsell-external-terminal', 308 content: async () => 'Connect Claude to your IDE · /ide', 309 cooldownSessions: 4, 310 async isRelevant() { 311 if (isSupportedTerminal()) { 312 return false 313 } 314 315 // Use lockfiles as a (quicker) signal for running IDEs 316 const lockfiles = await getSortedIdeLockfiles() 317 if (lockfiles.length !== 0) { 318 return false 319 } 320 321 const runningIDEs = await detectRunningIDEsCached() 322 return runningIDEs.length > 0 323 }, 324 }, 325 { 326 id: 'install-github-app', 327 content: async () => 328 'Run /install-github-app to tag @claude right from your Github issues and PRs', 329 cooldownSessions: 10, 330 isRelevant: async () => !getGlobalConfig().githubActionSetupCount, 331 }, 332 { 333 id: 'install-slack-app', 334 content: async () => 'Run /install-slack-app to use Claude in Slack', 335 cooldownSessions: 10, 336 isRelevant: async () => !getGlobalConfig().slackAppInstallCount, 337 }, 338 { 339 id: 'permissions', 340 content: async () => 341 'Use /permissions to pre-approve and pre-deny bash, edit, and MCP tools', 342 cooldownSessions: 10, 343 async isRelevant() { 344 const config = getGlobalConfig() 345 return config.numStartups > 10 346 }, 347 }, 348 { 349 id: 'drag-and-drop-images', 350 content: async () => 351 'Did you know you can drag and drop image files into your terminal?', 352 cooldownSessions: 10, 353 isRelevant: async () => !env.isSSH(), 354 }, 355 { 356 id: 'paste-images-mac', 357 content: async () => 358 'Paste images into Claude Code using control+v (not cmd+v!)', 359 cooldownSessions: 10, 360 isRelevant: async () => getPlatform() === 'macos', 361 }, 362 { 363 id: 'double-esc', 364 content: async () => 365 'Double-tap esc to rewind the conversation to a previous point in time', 366 cooldownSessions: 10, 367 isRelevant: async () => !fileHistoryEnabled(), 368 }, 369 { 370 id: 'double-esc-code-restore', 371 content: async () => 372 'Double-tap esc to rewind the code and/or conversation to a previous point in time', 373 cooldownSessions: 10, 374 isRelevant: async () => fileHistoryEnabled(), 375 }, 376 { 377 id: 'continue', 378 content: async () => 379 'Run claude --continue or claude --resume to resume a conversation', 380 cooldownSessions: 10, 381 isRelevant: async () => true, 382 }, 383 { 384 id: 'rename-conversation', 385 content: async () => 386 'Name your conversations with /rename to find them easily in /resume later', 387 cooldownSessions: 15, 388 isRelevant: async () => 389 isCustomTitleEnabled() && getGlobalConfig().numStartups > 10, 390 }, 391 { 392 id: 'custom-commands', 393 content: async () => 394 'Create skills by adding .md files to .claude/skills/ in your project or ~/.claude/skills/ for skills that work in any project', 395 cooldownSessions: 15, 396 async isRelevant() { 397 const config = getGlobalConfig() 398 return config.numStartups > 10 399 }, 400 }, 401 { 402 id: 'shift-tab', 403 content: async () => 404 process.env.USER_TYPE === 'ant' 405 ? `Hit ${getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab')} to cycle between default mode and auto mode` 406 : `Hit ${getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab')} to cycle between default mode, auto-accept edit mode, and plan mode`, 407 cooldownSessions: 10, 408 isRelevant: async () => true, 409 }, 410 { 411 id: 'image-paste', 412 content: async () => 413 `Use ${getShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v')} to paste images from your clipboard`, 414 cooldownSessions: 20, 415 isRelevant: async () => true, 416 }, 417 { 418 id: 'custom-agents', 419 content: async () => 420 'Use /agents to optimize specific tasks. Eg. Software Architect, Code Writer, Code Reviewer', 421 cooldownSessions: 15, 422 async isRelevant() { 423 const config = getGlobalConfig() 424 return config.numStartups > 5 425 }, 426 }, 427 { 428 id: 'agent-flag', 429 content: async () => 430 'Use --agent <agent_name> to directly start a conversation with a subagent', 431 cooldownSessions: 15, 432 async isRelevant() { 433 const config = getGlobalConfig() 434 return config.numStartups > 5 435 }, 436 }, 437 { 438 id: 'desktop-app', 439 content: async () => 440 'Run Claude Code locally or remotely using the Claude desktop app: clau.de/desktop', 441 cooldownSessions: 15, 442 isRelevant: async () => getPlatform() !== 'linux', 443 }, 444 { 445 id: 'desktop-shortcut', 446 content: async ctx => { 447 const blue = color('suggestion', ctx.theme) 448 return `Continue your session in Claude Code Desktop with ${blue('/desktop')}` 449 }, 450 cooldownSessions: 15, 451 isRelevant: async () => { 452 if (!getDesktopUpsellConfig().enable_shortcut_tip) return false 453 return ( 454 process.platform === 'darwin' || 455 (process.platform === 'win32' && process.arch === 'x64') 456 ) 457 }, 458 }, 459 { 460 id: 'web-app', 461 content: async () => 462 'Run tasks in the cloud while you keep coding locally · clau.de/web', 463 cooldownSessions: 15, 464 isRelevant: async () => true, 465 }, 466 { 467 id: 'mobile-app', 468 content: async () => 469 '/mobile to use Claude Code from the Claude app on your phone', 470 cooldownSessions: 15, 471 isRelevant: async () => true, 472 }, 473 { 474 id: 'opusplan-mode-reminder', 475 content: async () => 476 `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.`, 477 cooldownSessions: 2, 478 async isRelevant() { 479 if (process.env.USER_TYPE === 'ant') return false 480 const config = getGlobalConfig() 481 const modelSetting = getUserSpecifiedModelSetting() 482 const hasOpusPlanMode = modelSetting === 'opusplan' 483 // Show reminder if they have Opus Plan Mode and haven't used plan mode recently (3+ days) 484 const daysSinceLastUse = config.lastPlanModeUse 485 ? (Date.now() - config.lastPlanModeUse) / (1000 * 60 * 60 * 24) 486 : Infinity 487 return hasOpusPlanMode && daysSinceLastUse > 3 488 }, 489 }, 490 { 491 id: 'frontend-design-plugin', 492 content: async ctx => { 493 const blue = color('suggestion', ctx.theme) 494 return `Working with HTML/CSS? Install the frontend-design plugin:\n${blue(`/plugin install frontend-design@${OFFICIAL_MARKETPLACE_NAME}`)}` 495 }, 496 cooldownSessions: 3, 497 isRelevant: async context => 498 isMarketplacePluginRelevant('frontend-design', context, { 499 filePath: /\.(html|css|htm)$/i, 500 }), 501 }, 502 { 503 id: 'vercel-plugin', 504 content: async ctx => { 505 const blue = color('suggestion', ctx.theme) 506 return `Working with Vercel? Install the vercel plugin:\n${blue(`/plugin install vercel@${OFFICIAL_MARKETPLACE_NAME}`)}` 507 }, 508 cooldownSessions: 3, 509 isRelevant: async context => 510 isMarketplacePluginRelevant('vercel', context, { 511 filePath: /(?:^|[/\\])vercel\.json$/i, 512 cli: ['vercel'], 513 }), 514 }, 515 { 516 id: 'effort-high-nudge', 517 content: async ctx => { 518 const blue = color('suggestion', ctx.theme) 519 const cmd = blue('/effort high') 520 const variant = getFeatureValue_CACHED_MAY_BE_STALE< 521 'off' | 'copy_a' | 'copy_b' 522 >('tengu_tide_elm', 'off') 523 return variant === 'copy_b' 524 ? `Use ${cmd} for better one-shot answers. Claude thinks it through first.` 525 : `Working on something tricky? ${cmd} gives better first answers` 526 }, 527 cooldownSessions: 3, 528 isRelevant: async () => { 529 if (!is1PApiCustomer()) return false 530 if (!modelSupportsEffort(getMainLoopModel())) return false 531 if (getSettingsForSource('policySettings')?.effortLevel !== undefined) { 532 return false 533 } 534 if (getEffortEnvOverride() !== undefined) return false 535 const persisted = getInitialSettings().effortLevel 536 if (persisted === 'high' || persisted === 'max') return false 537 return ( 538 getFeatureValue_CACHED_MAY_BE_STALE<'off' | 'copy_a' | 'copy_b'>( 539 'tengu_tide_elm', 540 'off', 541 ) !== 'off' 542 ) 543 }, 544 }, 545 { 546 id: 'subagent-fanout-nudge', 547 content: async ctx => { 548 const blue = color('suggestion', ctx.theme) 549 const variant = getFeatureValue_CACHED_MAY_BE_STALE< 550 'off' | 'copy_a' | 'copy_b' 551 >('tengu_tern_alloy', 'off') 552 return variant === 'copy_b' 553 ? `For big tasks, tell Claude to ${blue('use subagents')}. They work in parallel and keep your main thread clean.` 554 : `Say ${blue('"fan out subagents"')} and Claude sends a team. Each one digs deep so nothing gets missed.` 555 }, 556 cooldownSessions: 3, 557 isRelevant: async () => { 558 if (!is1PApiCustomer()) return false 559 return ( 560 getFeatureValue_CACHED_MAY_BE_STALE<'off' | 'copy_a' | 'copy_b'>( 561 'tengu_tern_alloy', 562 'off', 563 ) !== 'off' 564 ) 565 }, 566 }, 567 { 568 id: 'loop-command-nudge', 569 content: async ctx => { 570 const blue = color('suggestion', ctx.theme) 571 const variant = getFeatureValue_CACHED_MAY_BE_STALE< 572 'off' | 'copy_a' | 'copy_b' 573 >('tengu_timber_lark', 'off') 574 return variant === 'copy_b' 575 ? `Use ${blue('/loop 5m check the deploy')} to run any prompt on a schedule. Set it and forget it.` 576 : `${blue('/loop')} runs any prompt on a recurring schedule. Great for monitoring deploys, babysitting PRs, or polling status.` 577 }, 578 cooldownSessions: 3, 579 isRelevant: async () => { 580 if (!is1PApiCustomer()) return false 581 if (!isKairosCronEnabled()) return false 582 return ( 583 getFeatureValue_CACHED_MAY_BE_STALE<'off' | 'copy_a' | 'copy_b'>( 584 'tengu_timber_lark', 585 'off', 586 ) !== 'off' 587 ) 588 }, 589 }, 590 { 591 id: 'guest-passes', 592 content: async ctx => { 593 const claude = color('claude', ctx.theme) 594 const reward = getCachedReferrerReward() 595 return reward 596 ? `Share Claude Code and earn ${claude(formatCreditAmount(reward))} of extra usage · ${claude('/passes')}` 597 : `You have free guest passes to share · ${claude('/passes')}` 598 }, 599 cooldownSessions: 3, 600 isRelevant: async () => { 601 const config = getGlobalConfig() 602 if (config.hasVisitedPasses) { 603 return false 604 } 605 const { eligible } = checkCachedPassesEligibility() 606 return eligible 607 }, 608 }, 609 { 610 id: 'overage-credit', 611 content: async ctx => { 612 const claude = color('claude', ctx.theme) 613 const info = getCachedOverageCreditGrant() 614 const amount = info ? formatGrantAmount(info) : null 615 if (!amount) return '' 616 // Copy from "OC & Bulk Overages copy" doc (#5 — CLI Rotating tip) 617 return `${claude(`${amount} in extra usage, on us`)} · third-party apps · ${claude('/extra-usage')}` 618 }, 619 cooldownSessions: 3, 620 isRelevant: async () => shouldShowOverageCreditUpsell(), 621 }, 622 { 623 id: 'feedback-command', 624 content: async () => 'Use /feedback to help us improve!', 625 cooldownSessions: 15, 626 async isRelevant() { 627 if (process.env.USER_TYPE === 'ant') { 628 return false 629 } 630 const config = getGlobalConfig() 631 return config.numStartups > 5 632 }, 633 }, 634] 635const internalOnlyTips: Tip[] = 636 process.env.USER_TYPE === 'ant' 637 ? [ 638 { 639 id: 'important-claudemd', 640 content: async () => 641 '[ANT-ONLY] Use "IMPORTANT:" prefix for must-follow CLAUDE.md rules', 642 cooldownSessions: 30, 643 isRelevant: async () => true, 644 }, 645 { 646 id: 'skillify', 647 content: async () => 648 '[ANT-ONLY] Use /skillify at the end of a workflow to turn it into a reusable skill', 649 cooldownSessions: 15, 650 isRelevant: async () => true, 651 }, 652 ] 653 : [] 654 655function getCustomTips(): Tip[] { 656 const settings = getInitialSettings() 657 const override = settings.spinnerTipsOverride 658 if (!override?.tips?.length) return [] 659 660 return override.tips.map((content, i) => ({ 661 id: `custom-tip-${i}`, 662 content: async () => content, 663 cooldownSessions: 0, 664 isRelevant: async () => true, 665 })) 666} 667 668export async function getRelevantTips(context?: TipContext): Promise<Tip[]> { 669 const settings = getInitialSettings() 670 const override = settings.spinnerTipsOverride 671 const customTips = getCustomTips() 672 673 // If excludeDefault is true and there are custom tips, skip built-in tips entirely 674 if (override?.excludeDefault && customTips.length > 0) { 675 return customTips 676 } 677 678 // Otherwise, filter built-in tips as before and combine with custom 679 const tips = [...externalTips, ...internalOnlyTips] 680 const isRelevant = await Promise.all(tips.map(_ => _.isRelevant(context))) 681 const filtered = tips 682 .filter((_, index) => isRelevant[index]) 683 .filter(_ => getSessionsSinceLastShown(_.id) >= _.cooldownSessions) 684 685 return [...filtered, ...customTips] 686}