Files
orca/src/shared/feature-tips.ts
T
Brennan Benson 820277c207 Show pre-upgrade Experimental Chat UI users a one-time native chat upgrade tip (#26710)
* feat(native-chat): one-time upgrade tip for pre-upgrade Experimental Chat UI users

Show the new native chat upgrade tip through the normal startup feature-tip
flow, only to profiles whose saved Experimental Chat UI setting was on before
this release. Main records that audience once, from the profile as saved by the
previous build, and never re-derives it from the live setting, so later
opt-ins, new installs and the future graduation stay excluded.

* fix(native-chat): keep the upgrade tip readable and dismissed

Focus Got it without scrolling and tighten the ja/es/fr copy so the title is
visible on open. Show the tip exactly like other startup tips (no resume-dialog
wait), keep the illustration's upward menu unclipped, and merge seen tip ids
on save so a stale client cannot bring a dismissed tip back.

* fix(native-chat): say "between chat and CLI" in French and pin the audience basis in tests
2026-10-08 19:30:43 -07:00

163 lines
4.9 KiB
TypeScript

import {
hasFeatureInteraction,
type FeatureInteractionId,
type FeatureInteractionState
} from './feature-interactions'
export type FeatureTipId =
| 'native-chat-upgrade'
| 'voice-dictation'
| 'orca-cli'
| 'cmd-j-palette'
| 'agent-session-search'
export type FeatureTipPriority = 'new' | 'unseen'
export type FeatureTipAction =
| 'learn-native-chat-upgrade'
| 'enable-voice'
| 'setup-cli'
| 'learn-cmd-j-palette'
| 'enable-session-search'
export type FeatureTip = {
id: FeatureTipId
priority: FeatureTipPriority
eyebrow: string
title: string
description: string
action: FeatureTipAction
ctaLabel: string
/** Feature interactions that mean this tip is no longer useful to show. */
completedByFeatureInteractions?: readonly FeatureInteractionId[]
}
export type CompletedFeatureTipState = {
cliInstalled: boolean
voiceDictationEnabled: boolean
/** Search is on, or this client cannot turn it on. */
sessionSearchTipCompleted: boolean
/** Main's once-decided record; only profiles with Chat UI on before the chat upgrade are in it. */
inNativeChatUpgradeTipAudience: boolean
featureInteractions?: FeatureInteractionState
}
export const FEATURE_TIPS = [
{
// Why: first so its audience sees it on the first launch after the upgrade.
id: 'native-chat-upgrade',
priority: 'new',
eyebrow: 'New',
title: 'Native chat got an upgrade',
description:
'New chats with supported agents now open in the upgraded chat view. To move between chat and CLI, open Agent Session History in the right sidebar:',
action: 'learn-native-chat-upgrade',
ctaLabel: 'Got it',
completedByFeatureInteractions: []
},
{
id: 'agent-session-search',
priority: 'new',
eyebrow: 'New',
title: 'Search every agent session',
description:
'Find any past conversation by what was said in it, then pick up where the agent left off.',
action: 'enable-session-search',
ctaLabel: 'Turn on session search',
completedByFeatureInteractions: []
},
{
id: 'orca-cli',
priority: 'new',
eyebrow: 'Tip',
title: 'Let agents drive Orca with the Orca CLI',
description: 'Enable agents to coordinate child worktrees and communicate between worktrees.',
action: 'setup-cli',
ctaLabel: 'Install CLI & Skills',
completedByFeatureInteractions: []
},
{
id: 'cmd-j-palette',
priority: 'new',
eyebrow: 'Tip',
// Why: "<shortcut>" is a placeholder token; the cmd-j dialog splits the
// title on it and inlines the live, platform-correct keybinding as a <kbd>.
title: 'Jump to a worktree with <shortcut>',
description:
'Search worktrees, switch tabs, tweak settings, or spin up a new worktree, all without leaving the keyboard.',
action: 'learn-cmd-j-palette',
ctaLabel: 'Got it',
completedByFeatureInteractions: []
},
{
id: 'voice-dictation',
priority: 'unseen',
eyebrow: 'Tip',
title: 'Dictate into any pane',
description: 'Start voice dictation in any focused pane, then use the shortcut again to stop.',
action: 'enable-voice',
ctaLabel: 'Set up voice dictation',
completedByFeatureInteractions: ['voice-dictation']
}
] as const satisfies readonly FeatureTip[]
export const FEATURE_TIP_IDS = FEATURE_TIPS.map((tip) => tip.id)
export function isFeatureTipId(value: unknown): value is FeatureTipId {
return typeof value === 'string' && FEATURE_TIP_IDS.includes(value as FeatureTipId)
}
export function normalizeFeatureTipIds(value: unknown): FeatureTipId[] {
if (!Array.isArray(value)) {
return []
}
const seen = new Set<FeatureTipId>()
for (const item of value) {
if (isFeatureTipId(item)) {
seen.add(item)
}
}
return [...seen]
}
export function getCompletedFeatureTipIds(state: CompletedFeatureTipState): Set<FeatureTipId> {
const completedIds = new Set<FeatureTipId>()
if (state.cliInstalled) {
completedIds.add('orca-cli')
}
if (state.voiceDictationEnabled) {
completedIds.add('voice-dictation')
}
if (state.sessionSearchTipCompleted) {
completedIds.add('agent-session-search')
}
if (!state.inNativeChatUpgradeTipAudience) {
completedIds.add('native-chat-upgrade')
}
for (const tip of FEATURE_TIPS) {
if (
tip.completedByFeatureInteractions?.some((id) =>
hasFeatureInteraction(state.featureInteractions, id)
)
) {
completedIds.add(tip.id)
}
}
return completedIds
}
export function getOrderedUnseenFeatureTips(args: {
seenTipIds: ReadonlySet<FeatureTipId>
completedTipIds?: ReadonlySet<FeatureTipId>
}): FeatureTip[] {
const completedTipIds = args.completedTipIds ?? new Set<FeatureTipId>()
const unseenTips = FEATURE_TIPS.filter(
(tip) => !args.seenTipIds.has(tip.id) && !completedTipIds.has(tip.id)
)
return [
...unseenTips.filter((tip) => tip.priority === 'new'),
...unseenTips.filter((tip) => tip.priority !== 'new')
]
}