mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Keep Chat UI users with a Terminal default on the terminal, with a chat mode switch in the upgrade tip (#26915)
* Keep Chat UI users who opened new tabs in the terminal on the terminal Before the chat upgrade, Experimental Chat UI had a separate Default view choice, and it defaulted to Terminal. The upgrade retired that choice so Chat UI on now means new agent tabs open as chat, which moved people who had kept the terminal default into chat with no obvious way back. On load, a profile saved by a pre-upgrade build with Chat UI on and Default view not set to Chat now gets Chat UI turned off, and is left out of the one-time native chat upgrade tip. The old key is stripped on the first save, so the migration runs once and a later opt-in sticks. * Update the saved-settings test for Chat UI on with Default view Terminal * Show the upgrade tip to terminal-default users, with a switch to turn on chat mode People who had Chat UI on with new tabs opening in the terminal now keep the terminal on upgrade, but they still need the tip's directions for moving between chat and CLI. They now get the same upgrade tip, plus a note that new agent tabs still open in the terminal and a "Turn on chat mode" switch. The switch shows when Chat UI is off as the tip opens and stays put while it is flipped. The tip is taller in that case so nothing scrolls. * Decide the chat mode switch from the saved tip audience, not the live setting The upgrade tip's 'Turn on chat mode' switch showed whenever Chat UI was off when the tip opened, so a chat-view member who had turned Chat UI off also got it. Main now answers with one tip variant (none, standard, keep-terminal) from the saved audience, and only keep-terminal shows the note and switch. Also moves the feature tip panel styling into a DialogContent surface variant and drops the Label restyle, and tests that the switch stays put after flipping. * Let the upgrade tip decide the chat mode switch from its variant The modal now passes the saved tip variant to the dialog, which decides the switch itself, so the dialog tests cover that decision.
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { sanitizeOnboardingUpdate, type Store } from '../persistence'
|
||||
import type { OnboardingState } from '../../shared/onboarding-state-types'
|
||||
import type { NativeChatUpgradeTipVariant } from '../../shared/native-chat-upgrade-tip-audience'
|
||||
|
||||
export function registerOnboardingHandlers(store: Store): void {
|
||||
ipcMain.removeHandler('onboarding:get')
|
||||
ipcMain.removeHandler('onboarding:update')
|
||||
ipcMain.removeHandler('onboarding:isInNativeChatUpgradeTipAudience')
|
||||
ipcMain.removeHandler('onboarding:getNativeChatUpgradeTipVariant')
|
||||
|
||||
ipcMain.handle('onboarding:get', (): OnboardingState => store.getOnboarding())
|
||||
// Why: never trust renderer input — a compromised/buggy caller could send
|
||||
@@ -14,7 +15,7 @@ export function registerOnboardingHandlers(store: Store): void {
|
||||
ipcMain.handle('onboarding:update', (_event, updates: unknown): OnboardingState => {
|
||||
return store.updateOnboarding(sanitizeOnboardingUpdate(updates))
|
||||
})
|
||||
ipcMain.handle('onboarding:isInNativeChatUpgradeTipAudience', (): boolean =>
|
||||
store.isInNativeChatUpgradeTipAudience()
|
||||
ipcMain.handle('onboarding:getNativeChatUpgradeTipVariant', (): NativeChatUpgradeTipVariant =>
|
||||
store.getNativeChatUpgradeTipVariant()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { getDefaultPersistedState } from '../shared/constants'
|
||||
import type { NativeChatUpgradeTipAudience } from '../shared/native-chat-upgrade-tip-audience'
|
||||
import type {
|
||||
NativeChatUpgradeTipAudience,
|
||||
NativeChatUpgradeTipVariant
|
||||
} from '../shared/native-chat-upgrade-tip-audience'
|
||||
import { normalizeProfileProjectState } from './orca-profiles/profile-project-state-file'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
@@ -28,8 +31,11 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
// Every build before the chat upgrade saved the whole settings document, defaults included.
|
||||
const preUpgradeSettings = (experimentalNativeChat?: boolean): Record<string, unknown> => ({
|
||||
openAgentTabsInChatByDefault: false,
|
||||
const preUpgradeSettings = (
|
||||
experimentalNativeChat?: boolean,
|
||||
openAgentTabsInChatByDefault = experimentalNativeChat === true
|
||||
): Record<string, unknown> => ({
|
||||
openAgentTabsInChatByDefault,
|
||||
...(experimentalNativeChat === undefined ? {} : { experimentalNativeChat })
|
||||
})
|
||||
|
||||
@@ -38,16 +44,16 @@ async function reopen(): Promise<ReturnType<typeof createStore>> {
|
||||
return createStore()
|
||||
}
|
||||
|
||||
/** Turns Chat UI on, restarts, and reports membership after each of several launches. */
|
||||
async function membershipAfterLaterOptIn(
|
||||
/** Turns Chat UI on, restarts, and reports the tip variant after each of several launches. */
|
||||
async function variantAfterLaterOptIn(
|
||||
store: ReturnType<typeof createStore>
|
||||
): Promise<boolean[]> {
|
||||
): Promise<NativeChatUpgradeTipVariant[]> {
|
||||
store.updateSettings({ experimentalNativeChat: true })
|
||||
store.flush()
|
||||
const launches: boolean[] = []
|
||||
const launches: NativeChatUpgradeTipVariant[] = []
|
||||
for (let launch = 0; launch < 3; launch += 1) {
|
||||
const reopened = await reopen()
|
||||
launches.push(reopened.isInNativeChatUpgradeTipAudience())
|
||||
launches.push(reopened.getNativeChatUpgradeTipVariant())
|
||||
reopened.updateSettings({ experimentalNativeChat: true })
|
||||
reopened.flush()
|
||||
}
|
||||
@@ -56,30 +62,41 @@ async function membershipAfterLaterOptIn(
|
||||
|
||||
describe('native chat upgrade tip audience', () => {
|
||||
it.each([
|
||||
['Chat UI on', preUpgradeSettings(true), 'eligible', 'chat-ui-on'],
|
||||
['Chat UI off', preUpgradeSettings(false), 'excluded', 'chat-ui-off'],
|
||||
['Chat UI never set', preUpgradeSettings(), 'excluded', 'chat-ui-unset'],
|
||||
['Chat UI on', preUpgradeSettings(true), 'eligible', 'chat-ui-on', 'standard'],
|
||||
[
|
||||
'Chat UI on but new tabs opening in the terminal',
|
||||
preUpgradeSettings(true, false),
|
||||
'eligible',
|
||||
'chat-ui-on-terminal-default',
|
||||
'keep-terminal'
|
||||
],
|
||||
['Chat UI off', preUpgradeSettings(false), 'excluded', 'chat-ui-off', 'none'],
|
||||
['Chat UI never set', preUpgradeSettings(), 'excluded', 'chat-ui-unset', 'none'],
|
||||
[
|
||||
'Chat UI on but saved by a build that already had the chat upgrade',
|
||||
{ experimentalNativeChat: true },
|
||||
'excluded',
|
||||
'chat-ui-on-unproven'
|
||||
'chat-ui-on-unproven',
|
||||
'none'
|
||||
]
|
||||
])('decides an existing profile with %s from what it saved', (_, settings, membership, basis) => {
|
||||
writeDataFile({ settings })
|
||||
const store = createStore()
|
||||
expect(store.isInNativeChatUpgradeTipAudience()).toBe(membership === 'eligible')
|
||||
store.flush()
|
||||
expect(readDataFile()).toHaveProperty('nativeChatUpgradeTipAudience', {
|
||||
version: 1,
|
||||
membership,
|
||||
basis
|
||||
})
|
||||
})
|
||||
])(
|
||||
'decides an existing profile with %s from what it saved',
|
||||
(_, settings, membership, basis, variant) => {
|
||||
writeDataFile({ settings })
|
||||
const store = createStore()
|
||||
expect(store.getNativeChatUpgradeTipVariant()).toBe(variant)
|
||||
store.flush()
|
||||
expect(readDataFile()).toHaveProperty('nativeChatUpgradeTipAudience', {
|
||||
version: 1,
|
||||
membership,
|
||||
basis
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps a brand new profile out of the audience', () => {
|
||||
const store = createStore()
|
||||
expect(store.isInNativeChatUpgradeTipAudience()).toBe(false)
|
||||
expect(store.getNativeChatUpgradeTipVariant()).toBe('none')
|
||||
store.flush()
|
||||
expect(readDataFile()).toHaveProperty('nativeChatUpgradeTipAudience.basis', 'new-profile')
|
||||
})
|
||||
@@ -93,17 +110,44 @@ describe('native chat upgrade tip audience', () => {
|
||||
async (_, seed) => {
|
||||
seed()
|
||||
const store = createStore()
|
||||
expect(store.isInNativeChatUpgradeTipAudience()).toBe(false)
|
||||
expect(await membershipAfterLaterOptIn(store)).toEqual([false, false, false])
|
||||
expect(store.getNativeChatUpgradeTipVariant()).toBe('none')
|
||||
expect(await variantAfterLaterOptIn(store)).toEqual(['none', 'none', 'none'])
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps an original member in the audience after turning Chat UI off', async () => {
|
||||
it('turns Chat UI off for a profile whose new tabs opened in the terminal, once', async () => {
|
||||
writeDataFile({ settings: preUpgradeSettings(true, false) })
|
||||
const store = createStore()
|
||||
expect(store.getSettings().experimentalNativeChat).toBe(false)
|
||||
store.flush()
|
||||
expect(readDataFile()).toHaveProperty('settings.experimentalNativeChat', false)
|
||||
// Turning it back on later sticks: the saved Default view is gone after the first save.
|
||||
store.updateSettings({ experimentalNativeChat: true })
|
||||
store.flush()
|
||||
expect((await reopen()).getSettings().experimentalNativeChat).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps Chat UI on for a profile whose new tabs opened in chat', () => {
|
||||
writeDataFile({ settings: preUpgradeSettings(true) })
|
||||
expect(createStore().getSettings().experimentalNativeChat).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps a chat-view member on the standard tip after turning Chat UI off', async () => {
|
||||
writeDataFile({ settings: preUpgradeSettings(true) })
|
||||
const store = createStore()
|
||||
store.updateSettings({ experimentalNativeChat: false })
|
||||
store.flush()
|
||||
expect((await reopen()).isInNativeChatUpgradeTipAudience()).toBe(true)
|
||||
expect((await reopen()).getNativeChatUpgradeTipVariant()).toBe('standard')
|
||||
})
|
||||
|
||||
it('keeps a terminal-view member on the chat mode switch tip after turning Chat UI on', async () => {
|
||||
writeDataFile({ settings: preUpgradeSettings(true, false) })
|
||||
const store = createStore()
|
||||
expect(await variantAfterLaterOptIn(store)).toEqual([
|
||||
'keep-terminal',
|
||||
'keep-terminal',
|
||||
'keep-terminal'
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps an excluded record when a later build turns Chat UI on by default', async () => {
|
||||
@@ -114,7 +158,7 @@ describe('native chat upgrade tip audience', () => {
|
||||
})
|
||||
for (let launch = 0; launch < 3; launch += 1) {
|
||||
const store = launch === 0 ? createStore() : await reopen()
|
||||
expect(store.isInNativeChatUpgradeTipAudience()).toBe(false)
|
||||
expect(store.getNativeChatUpgradeTipVariant()).toBe('none')
|
||||
store.flush()
|
||||
}
|
||||
expect(readDataFile()).toHaveProperty('nativeChatUpgradeTipAudience.basis', 'chat-ui-off')
|
||||
@@ -124,7 +168,7 @@ describe('native chat upgrade tip audience', () => {
|
||||
// A record lost by some later rewrite must not be recaptured from the live switch.
|
||||
writeDataFile({ settings: { experimentalNativeChat: true } })
|
||||
const store = createStore()
|
||||
expect(store.isInNativeChatUpgradeTipAudience()).toBe(false)
|
||||
expect(store.getNativeChatUpgradeTipVariant()).toBe('none')
|
||||
store.flush()
|
||||
expect(readDataFile()).toHaveProperty(
|
||||
'nativeChatUpgradeTipAudience.basis',
|
||||
@@ -153,7 +197,7 @@ describe('native chat upgrade tip audience', () => {
|
||||
])('fails closed on a saved record with %s', (_, record) => {
|
||||
writeDataFile({ settings: preUpgradeSettings(true), nativeChatUpgradeTipAudience: record })
|
||||
const store = createStore()
|
||||
expect(store.isInNativeChatUpgradeTipAudience()).toBe(false)
|
||||
expect(store.getNativeChatUpgradeTipVariant()).toBe('none')
|
||||
store.flush()
|
||||
expect(readDataFile()).toHaveProperty('nativeChatUpgradeTipAudience.basis', 'unreadable-record')
|
||||
})
|
||||
@@ -163,7 +207,7 @@ describe('native chat upgrade tip audience', () => {
|
||||
settings: preUpgradeSettings(false),
|
||||
nativeChatUpgradeTipAudience: { version: 1, membership: 'eligible', basis: 'chat-ui-on' }
|
||||
})
|
||||
expect(createStore().isInNativeChatUpgradeTipAudience()).toBe(true)
|
||||
expect(createStore().getNativeChatUpgradeTipVariant()).toBe('standard')
|
||||
})
|
||||
|
||||
it('profile transfers carry a saved record and never invent one', () => {
|
||||
|
||||
@@ -91,6 +91,21 @@ export function stripRetiredGlobalSettings(
|
||||
return rest
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat UI was on but the retired Default view left new agent tabs in the terminal. Every build
|
||||
* before the retirement saved that key, so a profile without it was already upgraded.
|
||||
*/
|
||||
export function savedChatUiWithTerminalDefaultView(settings: unknown): boolean {
|
||||
return (
|
||||
typeof settings === 'object' &&
|
||||
settings !== null &&
|
||||
'experimentalNativeChat' in settings &&
|
||||
settings.experimentalNativeChat === true &&
|
||||
'openAgentTabsInChatByDefault' in settings &&
|
||||
settings.openAgentTabsInChatByDefault !== true
|
||||
)
|
||||
}
|
||||
|
||||
export function migrateTerminalScrollbackRows(settings: unknown): {
|
||||
rows: number
|
||||
needsSave: boolean
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
parseNativeChatUpgradeTipAudience,
|
||||
type NativeChatUpgradeTipAudience
|
||||
} from '../../../shared/native-chat-upgrade-tip-audience'
|
||||
import { savedChatUiWithTerminalDefaultView } from '../applying-settings/terminal-settings-migrations'
|
||||
|
||||
import type { StoreRuntimeState } from './store-runtime-state'
|
||||
|
||||
@@ -51,9 +52,16 @@ export class LoadedCohortMigrationOperations {
|
||||
if (chatUi === true) {
|
||||
// Why: every build before the chat upgrade saved this key and the upgrade strips it, so
|
||||
// without it the opt-in may postdate the upgrade and cannot prove membership.
|
||||
return settings && Object.hasOwn(settings, 'openAgentTabsInChatByDefault')
|
||||
? { version: 1, membership: 'eligible', basis: 'chat-ui-on' }
|
||||
: { version: 1, membership: 'excluded', basis: 'chat-ui-on-unproven' }
|
||||
if (!settings || !Object.hasOwn(settings, 'openAgentTabsInChatByDefault')) {
|
||||
return { version: 1, membership: 'excluded', basis: 'chat-ui-on-unproven' }
|
||||
}
|
||||
return {
|
||||
version: 1,
|
||||
membership: 'eligible',
|
||||
basis: savedChatUiWithTerminalDefaultView(settings)
|
||||
? 'chat-ui-on-terminal-default'
|
||||
: 'chat-ui-on'
|
||||
}
|
||||
}
|
||||
return {
|
||||
version: 1,
|
||||
|
||||
@@ -50,12 +50,23 @@ describe('retired chat default selectors', () => {
|
||||
const normalized = normalizeLegacyProfile({
|
||||
experimentalNativeChat: true,
|
||||
experimentalStructuredNativeChat: false,
|
||||
openAgentTabsInChatByDefault: false
|
||||
openAgentTabsInChatByDefault: true
|
||||
})
|
||||
expect(normalized.experimentalNativeChat).toBe(true)
|
||||
expect(normalized).not.toHaveProperty('experimentalStructuredNativeChat')
|
||||
expect(normalized).not.toHaveProperty('openAgentTabsInChatByDefault')
|
||||
})
|
||||
|
||||
it('turns Chat UI off when new tabs opened in the terminal, dropping both older keys', () => {
|
||||
const normalized = normalizeLegacyProfile({
|
||||
experimentalNativeChat: true,
|
||||
experimentalStructuredNativeChat: false,
|
||||
openAgentTabsInChatByDefault: false
|
||||
})
|
||||
expect(normalized.experimentalNativeChat).toBe(false)
|
||||
expect(normalized).not.toHaveProperty('experimentalStructuredNativeChat')
|
||||
expect(normalized).not.toHaveProperty('openAgentTabsInChatByDefault')
|
||||
})
|
||||
})
|
||||
|
||||
describe('structured chat shell environment settings', () => {
|
||||
|
||||
@@ -9,7 +9,10 @@ import { normalizeTerminalCustomThemes } from '../../../shared/terminal-custom-t
|
||||
import { projectSourceControlAiToLegacyCommitMessageAi } from '../../../shared/source-control-ai'
|
||||
import { normalizeUiLanguage } from '../../../shared/ui-language'
|
||||
import { normalizeNativeChatShellEnvironmentVariables } from '../../../shared/native-chat-shell-environment'
|
||||
import { stripRetiredGlobalSettings } from '../applying-settings/terminal-settings-migrations'
|
||||
import {
|
||||
savedChatUiWithTerminalDefaultView,
|
||||
stripRetiredGlobalSettings
|
||||
} from '../applying-settings/terminal-settings-migrations'
|
||||
import { readLegacySidekickFlag } from '../applying-settings/onboarding-normalization'
|
||||
import { normalizeMachineName } from '../../../shared/machine-name'
|
||||
import type { PersistedState } from '../../../shared/persisted-state-types'
|
||||
@@ -61,6 +64,10 @@ export function normalizeLoadedGlobalSettings(
|
||||
// old default indistinguishable from a real opt-in. Preserve stored `true`; only
|
||||
// the default changed.
|
||||
...stripRetiredGlobalSettings(parsed.settings),
|
||||
// Why: Chat UI on now opens new tabs in chat; keep users whose new tabs opened in the terminal there.
|
||||
...(savedChatUiWithTerminalDefaultView(parsed.settings)
|
||||
? { experimentalNativeChat: false }
|
||||
: {}),
|
||||
nativeChatAppearance: normalizeNativeChatAppearanceSettings(
|
||||
parsed.settings?.nativeChatAppearance
|
||||
),
|
||||
|
||||
@@ -3,7 +3,11 @@ import type { OnboardingChecklistState } from '../../../shared/onboarding-state-
|
||||
import type { PersistedState } from '../../../shared/persisted-state-types'
|
||||
import { getDefaultOnboardingState } from '../../../shared/onboarding-defaults'
|
||||
import type { FeatureInteractionId } from '../../../shared/feature-interactions'
|
||||
import { parseNativeChatUpgradeTipAudience } from '../../../shared/native-chat-upgrade-tip-audience'
|
||||
import {
|
||||
nativeChatUpgradeTipVariant,
|
||||
parseNativeChatUpgradeTipAudience,
|
||||
type NativeChatUpgradeTipVariant
|
||||
} from '../../../shared/native-chat-upgrade-tip-audience'
|
||||
import {
|
||||
updateSettings as updateSettingsOperation,
|
||||
type SettingsMutationOperations
|
||||
@@ -167,12 +171,13 @@ export class ProfilePreferences {
|
||||
return this.getOnboarding()
|
||||
}
|
||||
|
||||
/** Fails closed: a missing or damaged record reads as not in the audience. */
|
||||
isInNativeChatUpgradeTipAudience(): boolean {
|
||||
const audience = parseNativeChatUpgradeTipAudience(
|
||||
this[profilePreferencesContext].runtime.state.nativeChatUpgradeTipAudience
|
||||
/** Fails closed: a missing or damaged record reads as no tip. */
|
||||
getNativeChatUpgradeTipVariant(): NativeChatUpgradeTipVariant {
|
||||
return nativeChatUpgradeTipVariant(
|
||||
parseNativeChatUpgradeTipAudience(
|
||||
this[profilePreferencesContext].runtime.state.nativeChatUpgradeTipAudience
|
||||
)
|
||||
)
|
||||
return audience?.membership === 'eligible'
|
||||
}
|
||||
|
||||
getGitHubCache(): PersistedState['githubCache'] {
|
||||
|
||||
@@ -305,24 +305,30 @@ describe('profile state Store authority factory', () => {
|
||||
|
||||
it.each([
|
||||
[
|
||||
{ experimentalNativeChat: true, openAgentTabsInChatByDefault: false },
|
||||
{ experimentalNativeChat: true, openAgentTabsInChatByDefault: true },
|
||||
false,
|
||||
true,
|
||||
'standard',
|
||||
'chat-ui-on'
|
||||
],
|
||||
[
|
||||
{ experimentalNativeChat: true, openAgentTabsInChatByDefault: false },
|
||||
true,
|
||||
'keep-terminal',
|
||||
'chat-ui-on-terminal-default'
|
||||
],
|
||||
[
|
||||
{ experimentalNativeChat: false, openAgentTabsInChatByDefault: false },
|
||||
true,
|
||||
false,
|
||||
'none',
|
||||
'chat-ui-off'
|
||||
]
|
||||
])(
|
||||
'decides the native chat upgrade tip audience once from the startup snapshot (%o)',
|
||||
(settings, laterChatUi, inAudience, basis) => {
|
||||
(settings, laterChatUi, variant, basis) => {
|
||||
const options = createOptions()
|
||||
writeFileSync(options.dataFile, JSON.stringify({ settings }))
|
||||
const first = createProfileStateStore({ ...options })
|
||||
expect(first.store.isInNativeChatUpgradeTipAudience()).toBe(inAudience)
|
||||
expect(first.store.getNativeChatUpgradeTipVariant()).toBe(variant)
|
||||
|
||||
// Why: a later Chat UI change must never move a profile into or out of the audience.
|
||||
first.store.updateSettings({ experimentalNativeChat: laterChatUi })
|
||||
@@ -330,9 +336,12 @@ describe('profile state Store authority factory', () => {
|
||||
rmSync(options.dataFile)
|
||||
const restarted = createProfileStateStore({ ...options })
|
||||
|
||||
expect(restarted.store.isInNativeChatUpgradeTipAudience()).toBe(inAudience)
|
||||
expect(restarted.store.getNativeChatUpgradeTipVariant()).toBe(variant)
|
||||
expect(JSON.parse(restarted.store.prepareProfileStateExport().json)).toMatchObject({
|
||||
nativeChatUpgradeTipAudience: { membership: inAudience ? 'eligible' : 'excluded', basis }
|
||||
nativeChatUpgradeTipAudience: {
|
||||
membership: variant === 'none' ? 'excluded' : 'eligible',
|
||||
basis
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { NativeChatUpgradeTipVariant } from '../../shared/native-chat-upgrade-tip-audience'
|
||||
import type { OnboardingState } from '../../shared/onboarding-state-types'
|
||||
|
||||
export type OnboardingApi = {
|
||||
@@ -9,7 +10,7 @@ export type OnboardingApi = {
|
||||
}
|
||||
) => Promise<OnboardingState>
|
||||
/** Read-only: main decides the audience once, from the profile as saved before the chat upgrade. */
|
||||
isInNativeChatUpgradeTipAudience: () => Promise<boolean>
|
||||
getNativeChatUpgradeTipVariant: () => Promise<NativeChatUpgradeTipVariant>
|
||||
}
|
||||
|
||||
export type StarNagApi = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
import type { NativeChatUpgradeTipVariant } from '../../shared/native-chat-upgrade-tip-audience'
|
||||
import type { OnboardingState } from '../../shared/onboarding-state-types'
|
||||
import type { PreloadApi } from '../api-types'
|
||||
|
||||
@@ -9,6 +10,6 @@ export const onboardingApi = {
|
||||
checklist?: Partial<OnboardingState['checklist']>
|
||||
}
|
||||
): Promise<OnboardingState> => ipcRenderer.invoke('onboarding:update', updates),
|
||||
isInNativeChatUpgradeTipAudience: (): Promise<boolean> =>
|
||||
ipcRenderer.invoke('onboarding:isInNativeChatUpgradeTipAudience')
|
||||
getNativeChatUpgradeTipVariant: (): Promise<NativeChatUpgradeTipVariant> =>
|
||||
ipcRenderer.invoke('onboarding:getNativeChatUpgradeTipVariant')
|
||||
} satisfies PreloadApi['onboarding']
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { useAppStore } from '../store'
|
||||
import { isWebClientLocation } from '../lib/web-client-location'
|
||||
import type { OnboardingState } from '../../../shared/onboarding-state-types'
|
||||
import { isInNativeChatUpgradeTipAudience } from '../../../shared/native-chat-upgrade-tip-audience'
|
||||
|
||||
export type OnboardingGate = ReturnType<typeof useOnboardingAndFeatureTips>
|
||||
|
||||
@@ -32,13 +33,15 @@ export function useOnboardingAndFeatureTips() {
|
||||
const persistedUIReady = useAppStore((s) => s.persistedUIReady)
|
||||
const featureTipsSeenIds = useAppStore((s) => s.featureTipsSeenIds)
|
||||
const featureInteractions = useAppStore((s) => s.featureInteractions)
|
||||
const inNativeChatUpgradeTipAudience = useAppStore((s) => s.inNativeChatUpgradeTipAudience)
|
||||
const inNativeChatUpgradeTipAudience = useAppStore((s) =>
|
||||
isInNativeChatUpgradeTipAudience(s.nativeChatUpgradeTipVariant)
|
||||
)
|
||||
const contextualToursAutoEligible = useAppStore((s) => s.contextualToursAutoEligible)
|
||||
const actions = useAppStore(
|
||||
useShallow((s) => ({
|
||||
openModal: s.openModal,
|
||||
markFeatureTipsSeen: s.markFeatureTipsSeen,
|
||||
setInNativeChatUpgradeTipAudience: s.setInNativeChatUpgradeTipAudience,
|
||||
setNativeChatUpgradeTipVariant: s.setNativeChatUpgradeTipVariant,
|
||||
setContextualToursAutoEligible: s.setContextualToursAutoEligible,
|
||||
setContextualToursOnboardingVisible: s.setContextualToursOnboardingVisible
|
||||
}))
|
||||
@@ -99,16 +102,16 @@ export function useOnboardingAndFeatureTips() {
|
||||
|
||||
let cancelled = false
|
||||
void window.api.onboarding
|
||||
.isInNativeChatUpgradeTipAudience()
|
||||
.then((inAudience) => {
|
||||
.getNativeChatUpgradeTipVariant()
|
||||
.then((variant) => {
|
||||
if (!cancelled) {
|
||||
actions.setInNativeChatUpgradeTipAudience(inAudience)
|
||||
actions.setNativeChatUpgradeTipVariant(variant)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Why: fail closed, and never hold the other tips behind a failed read.
|
||||
if (!cancelled) {
|
||||
actions.setInNativeChatUpgradeTipAudience(false)
|
||||
actions.setNativeChatUpgradeTipVariant('none')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ComponentProps, JSX, ReactNode } from 'react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/** Shared two-column tip layout: copy and actions on the left, a feature visual on the right. */
|
||||
export function FeatureTipDialogFrame({
|
||||
@@ -8,18 +9,25 @@ export function FeatureTipDialogFrame({
|
||||
onOpenChange,
|
||||
onOpenAutoFocus,
|
||||
visual,
|
||||
tall = false,
|
||||
children
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onOpenAutoFocus: ComponentProps<typeof DialogContent>['onOpenAutoFocus']
|
||||
visual: ReactNode
|
||||
/** Room for an extra row of copy without scrolling. */
|
||||
tall?: boolean
|
||||
children: ReactNode
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="!flex max-h-[calc(100vh-2rem)] flex-col gap-0 overflow-hidden bg-[color-mix(in_srgb,var(--foreground)_8%,var(--background))] p-0 dark:bg-[color-mix(in_srgb,var(--foreground)_16%,var(--background))] sm:max-w-4xl md:!h-[min(27rem,calc(100vh-2rem))] md:!flex-row"
|
||||
surface="feature-tip"
|
||||
className={cn(
|
||||
'!flex max-h-[calc(100vh-2rem)] flex-col overflow-hidden sm:max-w-4xl md:!flex-row',
|
||||
tall ? 'md:!h-[min(33rem,calc(100vh-2rem))]' : 'md:!h-[min(27rem,calc(100vh-2rem))]'
|
||||
)}
|
||||
showCloseButton
|
||||
onOpenAutoFocus={onOpenAutoFocus}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState, type JSX } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { getDefaultVoiceSettings } from '../../../../shared/constants'
|
||||
import { isInNativeChatUpgradeTipAudience } from '../../../../shared/native-chat-upgrade-tip-audience'
|
||||
import {
|
||||
ORCHESTRATION_ENABLED_STORAGE_KEY,
|
||||
ORCHESTRATION_SETUP_DISMISSED_STORAGE_KEY,
|
||||
@@ -34,7 +35,7 @@ export default function FeatureTipsModal(): JSX.Element | null {
|
||||
const updateSettings = useAppStore((s) => s.updateSettings)
|
||||
const seenTipIds = useAppStore((s) => s.featureTipsSeenIds)
|
||||
const featureInteractions = useAppStore((s) => s.featureInteractions)
|
||||
const inNativeChatUpgradeTipAudience = useAppStore((s) => s.inNativeChatUpgradeTipAudience)
|
||||
const nativeChatUpgradeTipVariant = useAppStore((s) => s.nativeChatUpgradeTipVariant)
|
||||
const markFeatureTipsSeen = useAppStore((s) => s.markFeatureTipsSeen)
|
||||
const modalData = useAppStore((s) => s.modalData)
|
||||
const showAiVaultSearch = useAppStore((s) => s.showAiVaultSearch)
|
||||
@@ -49,7 +50,7 @@ export default function FeatureTipsModal(): JSX.Element | null {
|
||||
modalData,
|
||||
seenTipIds,
|
||||
featureInteractions,
|
||||
inNativeChatUpgradeTipAudience,
|
||||
inNativeChatUpgradeTipAudience: isInNativeChatUpgradeTipAudience(nativeChatUpgradeTipVariant),
|
||||
settings,
|
||||
webClient: isWebClientLocation()
|
||||
})
|
||||
@@ -300,8 +301,11 @@ export default function FeatureTipsModal(): JSX.Element | null {
|
||||
open={isOpen}
|
||||
tip={currentTip}
|
||||
primaryBusy={primaryBusy}
|
||||
variant={nativeChatUpgradeTipVariant ?? 'none'}
|
||||
chatModeOn={settings?.experimentalNativeChat === true}
|
||||
onOpenChange={handleOpenChange}
|
||||
onPrimaryAction={() => void handlePrimaryAction()}
|
||||
onChatModeChange={(on) => void updateSettings({ experimentalNativeChat: on })}
|
||||
onSettingsClick={openNativeChatSettings}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import type { ReactNode } from 'react'
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, type JSX, type ReactNode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { FEATURE_TIPS, type FeatureTip } from '../../../../shared/feature-tips'
|
||||
import type { NativeChatUpgradeTipVariant } from '../../../../shared/native-chat-upgrade-tip-audience'
|
||||
import { NativeChatUpgradeTipDialog } from './NativeChatUpgradeTipDialog'
|
||||
import { NativeChatUpgradeFeatureTipVisual } from './NativeChatUpgradeFeatureTipVisual'
|
||||
|
||||
vi.mock('@/components/ui/dialog', () => ({
|
||||
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogContent: ({ children, className }: { children: ReactNode; className?: string }) => (
|
||||
<div className={className}>{children}</div>
|
||||
),
|
||||
DialogDescription: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogFooter: ({ children }: { children: ReactNode }) => <footer>{children}</footer>,
|
||||
DialogHeader: ({ children }: { children: ReactNode }) => <header>{children}</header>,
|
||||
@@ -29,20 +35,32 @@ function textOf(html: string): string {
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
// Why: repo convention — React only suppresses its act() warning when this global is set.
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
function dialog(variant: NativeChatUpgradeTipVariant, chatModeOn: boolean): JSX.Element {
|
||||
return (
|
||||
<NativeChatUpgradeTipDialog
|
||||
open
|
||||
tip={getTip()}
|
||||
primaryBusy={false}
|
||||
variant={variant}
|
||||
chatModeOn={chatModeOn}
|
||||
onOpenChange={() => {}}
|
||||
onPrimaryAction={() => {}}
|
||||
onChatModeChange={() => {}}
|
||||
onSettingsClick={() => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function renderDialog(variant: NativeChatUpgradeTipVariant, chatModeOn: boolean): string {
|
||||
return renderToStaticMarkup(dialog(variant, chatModeOn))
|
||||
}
|
||||
|
||||
describe('NativeChatUpgradeTipDialog', () => {
|
||||
it('shows the approved copy with one Got it button and the Experimental settings link', () => {
|
||||
const text = textOf(
|
||||
renderToStaticMarkup(
|
||||
<NativeChatUpgradeTipDialog
|
||||
open
|
||||
tip={getTip()}
|
||||
primaryBusy={false}
|
||||
onOpenChange={() => {}}
|
||||
onPrimaryAction={() => {}}
|
||||
onSettingsClick={() => {}}
|
||||
/>
|
||||
)
|
||||
)
|
||||
const text = textOf(renderDialog('standard', true))
|
||||
expect(text).toContain('NEW')
|
||||
expect(text).toContain('Native chat got an upgrade')
|
||||
expect(text).toContain(
|
||||
@@ -57,6 +75,50 @@ describe('NativeChatUpgradeTipDialog', () => {
|
||||
expect(text).toContain('Manage Chat UI in Settings → Experimental.')
|
||||
expect(text).toContain('Got it')
|
||||
expect(text).not.toContain('Maybe Later')
|
||||
expect(text).not.toContain('Turn on chat mode')
|
||||
expect(renderDialog('standard', true)).toContain('27rem')
|
||||
})
|
||||
|
||||
it('never offers chat mode to a chat-view member, even with Chat UI turned off', () => {
|
||||
const html = renderDialog('standard', false)
|
||||
const text = textOf(html)
|
||||
expect(text).not.toContain('New agent tabs still open in the terminal')
|
||||
expect(text).not.toContain('Turn on chat mode')
|
||||
expect(html).not.toContain('role="switch"')
|
||||
expect(html).toContain('27rem')
|
||||
expect(html).not.toContain('33rem')
|
||||
})
|
||||
|
||||
it('offers chat mode, in a taller tip, when new agent tabs still open in the terminal', () => {
|
||||
const html = renderDialog('keep-terminal', false)
|
||||
const text = textOf(html)
|
||||
expect(text).toContain('New agent tabs still open in the terminal, as before.')
|
||||
expect(text).toContain('Turn on chat mode')
|
||||
expect(text).toContain('New agent tabs open as chat instead.')
|
||||
expect(html).toContain('role="switch"')
|
||||
expect(html).toContain('aria-checked="false"')
|
||||
expect(html).toContain('33rem')
|
||||
// The way back to the terminal stays in the tip.
|
||||
expect(text).toContain('Resume in New CLI copies a Claude or Codex chat')
|
||||
})
|
||||
|
||||
it('keeps offering chat mode, switched on, when Chat UI was already on', () => {
|
||||
const html = renderDialog('keep-terminal', true)
|
||||
expect(textOf(html)).toContain('Turn on chat mode')
|
||||
expect(html).toContain('aria-checked="true"')
|
||||
expect(html).toContain('33rem')
|
||||
})
|
||||
|
||||
it('keeps the switch in place, now on, after it is flipped', () => {
|
||||
const container = document.createElement('div')
|
||||
const root = createRoot(container)
|
||||
act(() => root.render(dialog('keep-terminal', false)))
|
||||
expect(container.querySelector('[role="switch"]')?.getAttribute('aria-checked')).toBe('false')
|
||||
act(() => root.render(dialog('keep-terminal', true)))
|
||||
expect(container.querySelector('[role="switch"]')?.getAttribute('aria-checked')).toBe('true')
|
||||
expect(container.textContent).toContain('New agent tabs still open in the terminal, as before.')
|
||||
expect(container.innerHTML).toContain('33rem')
|
||||
act(() => root.unmount())
|
||||
})
|
||||
|
||||
it('pictures both real menu actions, one at a time', () => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useRef, type JSX } from 'react'
|
||||
import { useId, useRef, type JSX } from 'react'
|
||||
import { MessagesSquare, SquareTerminal, type LucideIcon } from 'lucide-react'
|
||||
import type { FeatureTip } from '../../../../shared/feature-tips'
|
||||
import type { NativeChatUpgradeTipVariant } from '../../../../shared/native-chat-upgrade-tip-audience'
|
||||
import { DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { FeatureTipActions } from './FeatureTipActions'
|
||||
import {
|
||||
@@ -51,22 +54,67 @@ function ActionLine({
|
||||
)
|
||||
}
|
||||
|
||||
/** For users whose new agent tabs still open in the terminal: say so and offer the switch. */
|
||||
function ChatModeSwitch({
|
||||
chatModeOn,
|
||||
onChatModeChange
|
||||
}: {
|
||||
chatModeOn: boolean
|
||||
onChatModeChange: (on: boolean) => void
|
||||
}): JSX.Element {
|
||||
const switchId = useId()
|
||||
return (
|
||||
<div className="mt-3 space-y-2 text-sm leading-relaxed">
|
||||
<p className="text-muted-foreground">
|
||||
{translate(
|
||||
'featureTips.nativeChatUpgrade.chatModeNote',
|
||||
'New agent tabs still open in the terminal, as before.'
|
||||
)}
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<Label htmlFor={switchId}>
|
||||
{translate('featureTips.nativeChatUpgrade.chatModeLabel', 'Turn on chat mode')}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'featureTips.nativeChatUpgrade.chatModeHint',
|
||||
'New agent tabs open as chat instead.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id={switchId} checked={chatModeOn} onCheckedChange={onChatModeChange} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function NativeChatUpgradeTipDialog({
|
||||
open,
|
||||
tip,
|
||||
primaryBusy,
|
||||
variant,
|
||||
chatModeOn,
|
||||
onOpenChange,
|
||||
onPrimaryAction,
|
||||
onChatModeChange,
|
||||
onSettingsClick
|
||||
}: {
|
||||
open: boolean
|
||||
tip: FeatureTip
|
||||
primaryBusy: boolean
|
||||
/** From the saved tip audience, never the live setting. */
|
||||
variant: NativeChatUpgradeTipVariant
|
||||
/** Live Chat UI setting. */
|
||||
chatModeOn: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onPrimaryAction: () => void
|
||||
onChatModeChange: (on: boolean) => void
|
||||
onSettingsClick: () => void
|
||||
}): JSX.Element {
|
||||
const primaryButtonRef = useRef<HTMLButtonElement>(null)
|
||||
// Why: only profiles whose new agent tabs opened in the terminal before the upgrade.
|
||||
const offerChatMode = variant === 'keep-terminal'
|
||||
|
||||
return (
|
||||
<FeatureTipDialogFrame
|
||||
@@ -78,6 +126,7 @@ export function NativeChatUpgradeTipDialog({
|
||||
primaryButtonRef.current?.focus({ preventScroll: true })
|
||||
}}
|
||||
visual={<NativeChatUpgradeFeatureTipVisual />}
|
||||
tall={offerChatMode}
|
||||
>
|
||||
<DialogHeader className="text-left">
|
||||
<div>
|
||||
@@ -85,6 +134,9 @@ export function NativeChatUpgradeTipDialog({
|
||||
<DialogTitle size="display">
|
||||
{translate('featureTips.nativeChatUpgrade.title', 'Native chat got an upgrade')}
|
||||
</DialogTitle>
|
||||
{offerChatMode ? (
|
||||
<ChatModeSwitch chatModeOn={chatModeOn} onChatModeChange={onChatModeChange} />
|
||||
) : null}
|
||||
<DialogDescription className="mt-3 max-w-2xl">
|
||||
<span className="block space-y-3 leading-relaxed">
|
||||
<span className="block">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { XIcon } from 'lucide-react'
|
||||
import * as DialogPrimitive from 'radix-ui/dialog'
|
||||
|
||||
@@ -44,16 +45,30 @@ function DialogOverlay({
|
||||
)
|
||||
}
|
||||
|
||||
const dialogContentSurfaceVariants = cva('', {
|
||||
variants: {
|
||||
surface: {
|
||||
default: '',
|
||||
/** The tinted, edge-to-edge panel of the two-column feature tip dialogs. */
|
||||
'feature-tip':
|
||||
'gap-0 bg-[color-mix(in_srgb,var(--foreground)_8%,var(--background))] p-0 dark:bg-[color-mix(in_srgb,var(--foreground)_16%,var(--background))]'
|
||||
}
|
||||
},
|
||||
defaultVariants: { surface: 'default' }
|
||||
})
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
overlayClassName,
|
||||
showCloseButton = true,
|
||||
surface,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
overlayClassName?: string
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> &
|
||||
VariantProps<typeof dialogContentSurfaceVariants> & {
|
||||
overlayClassName?: string
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay className={overlayClassName} />
|
||||
@@ -69,6 +84,7 @@ function DialogContent({
|
||||
// panel and pushes justify-end footers outside the visible surface.
|
||||
className={cn(
|
||||
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] grid-cols-[minmax(0,1fr)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-black/14 bg-background/96 p-6 text-foreground shadow-[0_20px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-2xl duration-200 outline-none dark:border-white/14 dark:bg-[rgba(23,23,23,0.96)] dark:shadow-[0_24px_72px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.06)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',
|
||||
dialogContentSurfaceVariants({ surface }),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -19164,6 +19164,9 @@
|
||||
"nativeChatUpgrade": {
|
||||
"eyebrow": "New",
|
||||
"title": "Native chat got an upgrade",
|
||||
"chatModeNote": "New agent tabs still open in the terminal, as before.",
|
||||
"chatModeLabel": "Turn on chat mode",
|
||||
"chatModeHint": "New agent tabs open as chat instead.",
|
||||
"intro": "New chats with supported agents now open in the upgraded chat view. To move between chat and CLI, open {{panel}} in the right sidebar:",
|
||||
"resumeInNativeChat": "{{action}} opens a supported CLI conversation in native chat.",
|
||||
"resumeInCli": "{{action}} copies a Claude or Codex chat into a new CLI session. The original chat stays as it is.",
|
||||
|
||||
@@ -19086,6 +19086,9 @@
|
||||
"nativeChatUpgrade": {
|
||||
"eyebrow": "Nuevo",
|
||||
"title": "El chat nativo mejora",
|
||||
"chatModeNote": "Las pestañas de agente nuevas siguen abriéndose en la terminal, como antes.",
|
||||
"chatModeLabel": "Activar el modo chat",
|
||||
"chatModeHint": "Las pestañas de agente nuevas se abren como chat.",
|
||||
"intro": "Los chats nuevos de agentes compatibles usan la vista de chat mejorada. Para alternar entre chat y CLI, abre {{panel}} (barra lateral derecha):",
|
||||
"resumeInNativeChat": "{{action}} abre una conversación de CLI compatible en el chat nativo.",
|
||||
"resumeInCli": "{{action}} copia un chat de Claude/Codex a una CLI nueva sin tocar el original.",
|
||||
|
||||
@@ -19062,6 +19062,9 @@
|
||||
"nativeChatUpgrade": {
|
||||
"eyebrow": "Nouveau",
|
||||
"title": "Le chat natif évolue",
|
||||
"chatModeNote": "Les nouveaux onglets d'agent s'ouvrent toujours dans le terminal, comme avant.",
|
||||
"chatModeLabel": "Activer le mode chat",
|
||||
"chatModeHint": "Les nouveaux onglets d'agent s'ouvrent en chat.",
|
||||
"intro": "Les nouveaux chats des agents compatibles utilisent la vue améliorée. Pour alterner entre chat et CLI, ouvrez {{panel}} (barre latérale droite) :",
|
||||
"resumeInNativeChat": "{{action}} ouvre une conversation CLI compatible dans le chat natif.",
|
||||
"resumeInCli": "{{action}} copie un chat Claude/Codex dans une CLI neuve ; l'original reste intact.",
|
||||
|
||||
@@ -19114,6 +19114,9 @@
|
||||
"nativeChatUpgrade": {
|
||||
"eyebrow": "新機能",
|
||||
"title": "ネイティブチャットを刷新",
|
||||
"chatModeNote": "新しい Agent タブは、これまでどおりターミナルで開きます。",
|
||||
"chatModeLabel": "チャットモードをオンにする",
|
||||
"chatModeHint": "新しい Agent タブをチャットで開きます。",
|
||||
"intro": "対応 Agent の新しいチャットはアップグレード版のチャットビューで開きます。チャットと CLI の切り替えは右サイドバーの{{panel}}から:",
|
||||
"resumeInNativeChat": "{{action}}: 対応する CLI の会話をネイティブチャットで開きます。",
|
||||
"resumeInCli": "{{action}}: Claude/Codex のチャットを新しい CLI セッションに複製します。元のチャットは残ります。",
|
||||
|
||||
@@ -19114,6 +19114,9 @@
|
||||
"nativeChatUpgrade": {
|
||||
"eyebrow": "새 기능",
|
||||
"title": "기본 채팅이 업그레이드되었습니다",
|
||||
"chatModeNote": "새 Agent 탭은 이전처럼 터미널에서 열립니다.",
|
||||
"chatModeLabel": "채팅 모드 켜기",
|
||||
"chatModeHint": "새 Agent 탭을 채팅으로 엽니다.",
|
||||
"intro": "이제 지원되는 Agent의 새 채팅은 업그레이드된 채팅 보기에서 열립니다. 채팅과 CLI 사이를 오가려면 오른쪽 사이드바에서 {{panel}}을 여세요:",
|
||||
"resumeInNativeChat": "{{action}}: 지원되는 CLI 대화를 기본 채팅에서 엽니다.",
|
||||
"resumeInCli": "{{action}}: Claude 또는 Codex 채팅을 새 CLI 세션으로 복사합니다. 원래 채팅은 그대로 유지됩니다.",
|
||||
|
||||
@@ -19114,6 +19114,9 @@
|
||||
"nativeChatUpgrade": {
|
||||
"eyebrow": "新功能",
|
||||
"title": "本机聊天已升级",
|
||||
"chatModeNote": "新的智能体标签页仍像以前一样在终端中打开。",
|
||||
"chatModeLabel": "开启聊天模式",
|
||||
"chatModeHint": "新的智能体标签页改为以聊天方式打开。",
|
||||
"intro": "受支持智能体的新聊天现在会在升级后的聊天视图中打开。要在聊天和 CLI 之间切换,请打开右侧边栏中的{{panel}}:",
|
||||
"resumeInNativeChat": "{{action}}:在本机聊天中打开受支持的 CLI 对话。",
|
||||
"resumeInCli": "{{action}}:将 Claude 或 Codex 聊天复制到新的 CLI 会话中。原始聊天保持不变。",
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
FeatureInteractionState
|
||||
} from '../../../../../shared/feature-interactions'
|
||||
import type { FeatureTipId } from '../../../../../shared/feature-tips'
|
||||
import type { NativeChatUpgradeTipVariant } from '../../../../../shared/native-chat-upgrade-tip-audience'
|
||||
import type { ContextualTourId } from '../../../../../shared/contextual-tours'
|
||||
import type { OrcaHookScriptKind } from '../../../lib/orca-hook-trust'
|
||||
import type { SettingsNavigationTarget } from '../../../lib/settings-navigation-types'
|
||||
@@ -55,9 +56,9 @@ export type UISliceContextual = {
|
||||
closeModal: () => void
|
||||
featureTipsSeenIds: FeatureTipId[]
|
||||
markFeatureTipsSeen: (ids: FeatureTipId[]) => void
|
||||
/** Main's once-decided native chat upgrade tip audience; null until read. */
|
||||
inNativeChatUpgradeTipAudience: boolean | null
|
||||
setInNativeChatUpgradeTipAudience: (inAudience: boolean) => void
|
||||
/** Main's once-decided native chat upgrade tip variant; null until read. */
|
||||
nativeChatUpgradeTipVariant: NativeChatUpgradeTipVariant | null
|
||||
setNativeChatUpgradeTipVariant: (variant: NativeChatUpgradeTipVariant) => void
|
||||
featureInteractions: FeatureInteractionState
|
||||
recordFeatureInteraction: (id: FeatureInteractionId) => Promise<void>
|
||||
contextualToursSeenIds: ContextualTourId[]
|
||||
|
||||
@@ -29,9 +29,8 @@ export function createUiFeatureActions(set: UISliceSet, get: UISliceGet): Partia
|
||||
window.api.ui.set({ featureTipsSeenIds: next }).catch(console.error)
|
||||
return { featureTipsSeenIds: next }
|
||||
}),
|
||||
inNativeChatUpgradeTipAudience: null,
|
||||
setInNativeChatUpgradeTipAudience: (inAudience) =>
|
||||
set({ inNativeChatUpgradeTipAudience: inAudience }),
|
||||
nativeChatUpgradeTipVariant: null,
|
||||
setNativeChatUpgradeTipVariant: (variant) => set({ nativeChatUpgradeTipVariant: variant }),
|
||||
featureInteractions: {},
|
||||
recordFeatureInteraction: (id) => {
|
||||
let tourProgression: ReturnType<typeof getContextualTourProgressionForFeatureInteraction> =
|
||||
|
||||
@@ -55,7 +55,7 @@ export function createWebOnboardingApi(): Partial<PreloadApi> {
|
||||
return next
|
||||
},
|
||||
// Why: the browser client has no local profile from before the upgrade, so it is never in the audience.
|
||||
isInNativeChatUpgradeTipAudience: () => Promise.resolve(false)
|
||||
getNativeChatUpgradeTipVariant: () => Promise.resolve('none')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isInNativeChatUpgradeTipAudience,
|
||||
nativeChatUpgradeTipVariant
|
||||
} from './native-chat-upgrade-tip-audience'
|
||||
|
||||
describe('nativeChatUpgradeTipVariant', () => {
|
||||
it.each([
|
||||
['eligible', 'chat-ui-on', 'standard'],
|
||||
['eligible', 'chat-ui-on-terminal-default', 'keep-terminal'],
|
||||
['excluded', 'chat-ui-on-terminal-default', 'none'],
|
||||
['excluded', 'chat-ui-off', 'none'],
|
||||
['excluded', 'new-profile', 'none']
|
||||
] as const)('gives %s/%s the %s tip', (membership, basis, variant) => {
|
||||
expect(nativeChatUpgradeTipVariant({ version: 1, membership, basis })).toBe(variant)
|
||||
})
|
||||
|
||||
it('shows no tip without a readable record', () => {
|
||||
expect(nativeChatUpgradeTipVariant(null)).toBe('none')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isInNativeChatUpgradeTipAudience', () => {
|
||||
it.each([
|
||||
[null, null],
|
||||
['none', false],
|
||||
['standard', true],
|
||||
['keep-terminal', true]
|
||||
] as const)('reads variant %s as %s', (variant, inAudience) => {
|
||||
expect(isInNativeChatUpgradeTipAudience(variant)).toBe(inAudience)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,12 @@
|
||||
export const NATIVE_CHAT_UPGRADE_TIP_MEMBERSHIPS = ['eligible', 'excluded'] as const
|
||||
export type NativeChatUpgradeTipMembership = (typeof NATIVE_CHAT_UPGRADE_TIP_MEMBERSHIPS)[number]
|
||||
|
||||
/** Why a profile landed in or out of the audience; provenance only, readers use `membership`. */
|
||||
/** Why a profile landed in or out of the audience; readers use `nativeChatUpgradeTipVariant`. */
|
||||
export const NATIVE_CHAT_UPGRADE_TIP_AUDIENCE_BASES = [
|
||||
'chat-ui-on',
|
||||
'chat-ui-on-unproven',
|
||||
/** Chat UI was on with new tabs opening in the terminal; load turned Chat UI off. */
|
||||
'chat-ui-on-terminal-default',
|
||||
'chat-ui-off',
|
||||
'chat-ui-unset',
|
||||
'new-profile',
|
||||
@@ -23,6 +25,25 @@ export type NativeChatUpgradeTipAudience = {
|
||||
basis: NativeChatUpgradeTipAudienceBasis
|
||||
}
|
||||
|
||||
/** Which upgrade tip a profile gets: none, the standard tip, or the tip with the chat mode switch. */
|
||||
export type NativeChatUpgradeTipVariant = 'none' | 'standard' | 'keep-terminal'
|
||||
|
||||
export function nativeChatUpgradeTipVariant(
|
||||
audience: NativeChatUpgradeTipAudience | null
|
||||
): NativeChatUpgradeTipVariant {
|
||||
if (audience?.membership !== 'eligible') {
|
||||
return 'none'
|
||||
}
|
||||
return audience.basis === 'chat-ui-on-terminal-default' ? 'keep-terminal' : 'standard'
|
||||
}
|
||||
|
||||
/** Whether the profile gets any upgrade tip; null while the variant is still unknown. */
|
||||
export function isInNativeChatUpgradeTipAudience(
|
||||
variant: NativeChatUpgradeTipVariant | null
|
||||
): boolean | null {
|
||||
return variant === null ? null : variant !== 'none'
|
||||
}
|
||||
|
||||
export function createNewProfileNativeChatUpgradeTipAudience(): NativeChatUpgradeTipAudience {
|
||||
return { version: 1, membership: 'excluded', basis: 'new-profile' }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user