Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/renderer/src/components/feature-tips/FeatureTipsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function FeatureTipsModal(): JSX.Element | null {
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
const settings = useAppStore((s) => s.settings)
const updateSettings = useAppStore((s) => s.updateSettings)
const updateSettingsOrThrow = useAppStore((s) => s.updateSettingsOrThrow)
const seenTipIds = useAppStore((s) => s.featureTipsSeenIds)
const featureInteractions = useAppStore((s) => s.featureInteractions)
const nativeChatUpgradeTipVariant = useAppStore((s) => s.nativeChatUpgradeTipVariant)
Expand Down Expand Up @@ -137,9 +138,25 @@ export default function FeatureTipsModal(): JSX.Element | null {
return
}

markFeatureTipsSeen([currentTip.id])
if (currentTip.action !== 'learn-native-chat-upgrade') {
markFeatureTipsSeen([currentTip.id])
}
switch (currentTip.action) {
case 'learn-native-chat-upgrade': {
if (settings?.experimentalNativeChat !== true) {
setPrimaryBusy(true)
try {
await updateSettingsOrThrow({ experimentalNativeChat: true })
} catch {
toast.error(
translate('featureTips.nativeChatUpgrade.enableError', 'Could not enable chat mode.')
)
return
} finally {
setPrimaryBusy(false)
}
}
markFeatureTipsSeen([currentTip.id])
closeModal()
break
}
Expand Down Expand Up @@ -301,11 +318,9 @@ 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}
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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'

Expand Down Expand Up @@ -38,29 +37,32 @@ function textOf(html: string): string {
// 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 {
function dialog(
chatModeOn: boolean,
onPrimaryAction = (): void => {},
onOpenChange = (_open: boolean): void => {},
primaryBusy = false
): JSX.Element {
return (
<NativeChatUpgradeTipDialog
open
tip={getTip()}
primaryBusy={false}
variant={variant}
primaryBusy={primaryBusy}
chatModeOn={chatModeOn}
onOpenChange={() => {}}
onPrimaryAction={() => {}}
onChatModeChange={() => {}}
onOpenChange={onOpenChange}
onPrimaryAction={onPrimaryAction}
onSettingsClick={() => {}}
/>
)
}

function renderDialog(variant: NativeChatUpgradeTipVariant, chatModeOn: boolean): string {
return renderToStaticMarkup(dialog(variant, chatModeOn))
function renderDialog(chatModeOn: boolean): string {
return renderToStaticMarkup(dialog(chatModeOn))
}

describe('NativeChatUpgradeTipDialog', () => {
it('shows the approved copy with one Got it button and the Experimental settings link', () => {
const text = textOf(renderDialog('standard', true))
it('shows the approved copy with one Got It button and the Experimental settings link', () => {
const text = textOf(renderDialog(true))
expect(text).toContain('NEW')
expect(text).toContain('Native chat got an upgrade')
expect(text).toContain(
Expand All @@ -73,51 +75,66 @@ describe('NativeChatUpgradeTipDialog', () => {
'Resume in New CLI copies a Claude or Codex chat into a new CLI session. The original chat stays as it is.'
)
expect(text).toContain('Manage Chat UI in Settings → Experimental.')
expect(text).toContain('Got it')
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')
expect(renderDialog(true)).toContain('27rem')
})

it('never offers chat mode to a chat-view member, even with Chat UI turned off', () => {
const html = renderDialog('standard', false)
it('shows only Got It when chat mode is already on', () => {
const html = renderDialog(true)
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(text).not.toContain('Enable Now')
expect(text).not.toContain('Okay')
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)
it('offers Okay and Enable Now when chat mode is off, without a switch', () => {
const html = renderDialog(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(text).toContain('Okay')
expect(text).toContain('Enable Now')
expect(text).not.toContain('Got It')
expect(text).not.toContain('Turn on chat mode')
expect(html).not.toContain('role="switch"')
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', () => {
it('dismisses with Okay and runs the primary action only with Enable Now', () => {
const onPrimaryAction = vi.fn()
const onOpenChange = vi.fn()
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.render(dialog(false, onPrimaryAction, onOpenChange)))
const buttons = container.querySelectorAll<HTMLButtonElement>('footer button')
expect(buttons).toHaveLength(2)
expect(buttons[0]?.textContent).toBe('Okay')
expect(buttons[0]?.getAttribute('data-variant')).toBe('ghost')
expect(buttons[1]?.textContent).toBe('Enable Now')
expect(buttons[1]?.getAttribute('data-variant')).toBe('default')
act(() => buttons[0]?.click())
expect(onOpenChange).toHaveBeenCalledWith(false)
expect(onPrimaryAction).not.toHaveBeenCalled()
act(() => buttons[1]?.click())
expect(onPrimaryAction).toHaveBeenCalledOnce()
act(() => root.render(dialog(false, onPrimaryAction, onOpenChange, true)))
expect(
Array.from(container.querySelectorAll<HTMLButtonElement>('footer button')).every(
(b) => b.disabled
)
).toBe(true)
act(() => root.render(dialog(true, onPrimaryAction, onOpenChange)))
expect(container.querySelectorAll('footer button')).toHaveLength(1)
expect(container.querySelector('footer button')?.textContent).toBe('Got It')
expect(container.querySelector('[role="switch"]')).toBeNull()
expect(container.textContent).not.toContain('New agent tabs still open in the terminal')
act(() => root.unmount())
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { useId, useRef, type JSX } from 'react'
import { 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 { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import { FeatureTipActions } from './FeatureTipActions'
import {
Expand Down Expand Up @@ -54,67 +52,26 @@ 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'
const offerChatMode = !chatModeOn

return (
<FeatureTipDialogFrame
Expand All @@ -135,7 +92,12 @@ export function NativeChatUpgradeTipDialog({
{translate('featureTips.nativeChatUpgrade.title', 'Native chat got an upgrade')}
</DialogTitle>
{offerChatMode ? (
<ChatModeSwitch chatModeOn={chatModeOn} onChatModeChange={onChatModeChange} />
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
{translate(
'featureTips.nativeChatUpgrade.chatModeNote',
'New agent tabs still open in the terminal, as before.'
)}
</p>
) : null}
<DialogDescription className="mt-3 max-w-2xl">
<span className="block space-y-3 leading-relaxed">
Expand Down Expand Up @@ -194,16 +156,25 @@ export function NativeChatUpgradeTipDialog({
</div>
</DialogHeader>

<DialogFooter className="mt-8 flex sm:justify-stretch">
<DialogFooter className="mt-8 flex sm:justify-end">
{offerChatMode ? (
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={primaryBusy}>
{translate('featureTips.nativeChatUpgrade.okay', 'Okay')}
</Button>
) : null}
<FeatureTipActions
currentTip={tip}
primaryBusy={primaryBusy}
onPrimaryAction={onPrimaryAction}
onSkip={() => onOpenChange(false)}
showSkip={false}
fullWidth
fullWidth={!offerChatMode}
primaryButtonRef={primaryButtonRef}
label={translate('featureTips.nativeChatUpgrade.gotIt', 'Got it')}
label={
offerChatMode
? translate('featureTips.nativeChatUpgrade.enableNow', 'Enable Now')
: translate('featureTips.nativeChatUpgrade.gotIt', 'Got It')
}
/>
</DialogFooter>
</FeatureTipDialogFrame>
Expand Down
7 changes: 4 additions & 3 deletions src/renderer/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -19238,14 +19238,15 @@
"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.",
"okay": "Okay",
"enableNow": "Enable Now",
"enableError": "Could not enable chat mode.",
"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.",
"settingsInstruction": "Manage Chat UI in",
"settingsLink": "Settings → Experimental",
"gotIt": "Got it"
"gotIt": "Got It"
},
"voice": {
"demoPrompt": "Review this diff for edge cases and add tests for anything you find.",
Expand Down
Loading