feat(browser): guide Google sign-in after cookie imports (STA-3811) (#13666)

This commit is contained in:
Brennan Benson
2026-08-12 00:53:02 -07:00
committed by GitHub
parent 9c091cf77e
commit 36d45af062
12 changed files with 367 additions and 71 deletions
@@ -0,0 +1,28 @@
import { Info } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'
export function BrowserCookieImportDisclosure(): React.JSX.Element {
return (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel className="flex max-w-64 items-start gap-2 whitespace-normal py-2 font-normal">
<Info aria-hidden className="mt-0.5 size-3.5 shrink-0" />
<span className="min-w-0">
<span className="block font-medium leading-4 text-foreground">
{translate(
'auto.components.BrowserCookieImportDisclosure.title',
"Google logins aren't imported"
)}
</span>
<span className="block leading-4 text-muted-foreground">
{translate(
'auto.components.BrowserCookieImportDisclosure.description',
'Sign in to Google directly in Orca.'
)}
</span>
</span>
</DropdownMenuLabel>
</>
)
}
@@ -1,24 +1,29 @@
/**
* @vitest-environment happy-dom
*
* STA-3811: imports never touch the Google cookie family, so both import menus must say so at
* the moment of decision. Covers the browser toolbar menu and the Settings profile row.
* STA-3811: imports never touch the Google cookie family, so every import menu must disclose it
* at the moment of decision.
*/
import { act, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import en from '@/i18n/locales/en.json'
const DISCLOSURE = 'Google requires signing in directly - imports skip it.'
const DISCLOSURE_TITLE = "Google logins aren't imported"
const DISCLOSURE_DESCRIPTION = 'Sign in to Google directly in Orca.'
vi.mock('@/components/ui/dropdown-menu', () => dropdownMenuStubs())
vi.mock('../ui/dropdown-menu', () => dropdownMenuStubs())
vi.mock('@/components/ui/popover', () => popoverStubs())
vi.mock('@/store', () => ({ useAppStore: appStoreStub() }))
vi.mock('../../store', () => ({ useAppStore: appStoreStub() }))
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
import { BrowserCookieImportDisclosure } from './BrowserCookieImportDisclosure'
import { BrowserImportHintButton } from './browser-pane/BrowserImportHintButton'
import { BrowserToolbarMenuDropdown } from './browser-pane/browser-toolbar-menu-dropdown'
import { BrowserProfileRow } from './settings/BrowserProfileRow'
import { BrowserUseCookieImportStep } from './settings/BrowserUseCookieImportStep'
const DETECTED_BROWSERS = [
{
@@ -29,7 +34,7 @@ const DETECTED_BROWSERS = [
}
]
describe('cookie-import Google disclosure caption', () => {
describe('cookie-import Google disclosure footer', () => {
let container: HTMLDivElement
let root: Root
@@ -44,9 +49,10 @@ describe('cookie-import Google disclosure caption', () => {
container.remove()
})
it('is shown in the browser toolbar import menu', () => {
act(() => {
root.render(
it.each([
[
'browser toolbar overflow',
() => (
<BrowserToolbarMenuDropdown
menuOpen
onMenuOpenChange={vi.fn()}
@@ -63,14 +69,22 @@ describe('cookie-import Google disclosure caption', () => {
onApplyViewportPreset={vi.fn()}
/>
)
})
expect(container.textContent).toContain(DISCLOSURE)
})
it('is shown in the Settings browser-profile import menu', () => {
act(() => {
root.render(
],
['browser toolbar hint', () => <BrowserImportHintButton profileId="default" />],
[
'Settings browser-use setup',
() => (
<BrowserUseCookieImportStep
cookiesImported={false}
isImportingDefault={false}
step3Blocked={false}
sourceLabel={null}
/>
)
],
[
'Settings browser-profile row',
() => (
<BrowserProfileRow
profile={{ id: 'default', name: 'Default', partition: 'persist:default' } as never}
detectedBrowsers={DETECTED_BROWSERS}
@@ -79,18 +93,29 @@ describe('cookie-import Google disclosure caption', () => {
onSelect={vi.fn()}
/>
)
})
]
] satisfies [string, () => ReactNode][])('is shown in the %s menu', (_name, renderSurface) => {
act(() => root.render(renderSurface()))
expect(container.textContent).toContain(DISCLOSURE)
expect(container.textContent).toContain(DISCLOSURE_TITLE)
expect(container.textContent).toContain(DISCLOSURE_DESCRIPTION)
})
// Why: the rendered text comes from the catalog, not the translate() fallback, so a copy
// drift in en.json alone would otherwise slip through both render assertions.
it('reads the same copy from the catalog on both surfaces', () => {
expect(catalogEntry('auto.components.browser.pane.BrowserToolbarMenu.c186b4d890')).toBe(
DISCLOSURE
it('renders the icon and separator as non-interactive footer chrome', () => {
act(() => root.render(<BrowserCookieImportDisclosure />))
const label = container.querySelector('[data-testid="dropdown-menu-label"]')
expect(label?.querySelector('svg')).not.toBeNull()
expect(label?.previousElementSibling?.tagName).toBe('HR')
})
it('reads the footer copy from the catalog', () => {
expect(catalogEntry('auto.components.BrowserCookieImportDisclosure.title')).toBe(
DISCLOSURE_TITLE
)
expect(catalogEntry('auto.components.BrowserCookieImportDisclosure.description')).toBe(
DISCLOSURE_DESCRIPTION
)
expect(catalogEntry('auto.components.settings.BrowserProfileRow.654a0c2073')).toBe(DISCLOSURE)
})
})
@@ -113,11 +138,13 @@ function dropdownMenuStubs(): Record<string, unknown> {
DropdownMenu: passthrough,
DropdownMenuContent: block,
DropdownMenuItem: block,
DropdownMenuLabel: block,
DropdownMenuLabel: ({ children }: { children?: ReactNode }): ReactNode => (
<div data-testid="dropdown-menu-label">{children}</div>
),
DropdownMenuPortal: passthrough,
DropdownMenuRadioGroup: passthrough,
DropdownMenuRadioItem: block,
DropdownMenuSeparator: () => null,
DropdownMenuSeparator: () => <hr />,
DropdownMenuSub: passthrough,
DropdownMenuSubContent: block,
DropdownMenuSubTrigger: block,
@@ -125,11 +152,33 @@ function dropdownMenuStubs(): Record<string, unknown> {
}
}
function popoverStubs(): Record<string, unknown> {
const passthrough = ({ children }: { children?: ReactNode }): ReactNode => children
const block = ({ children }: { children?: ReactNode }): ReactNode => <div>{children}</div>
return { Popover: passthrough, PopoverContent: block, PopoverTrigger: passthrough }
}
function appStoreStub(): unknown {
const state = {
browserImportHintHidden: false,
browserSessionImportState: null,
detectedBrowsers: [
{
family: 'chrome',
label: 'Google Chrome',
profiles: [{ name: 'Default', directory: 'Default' }],
selectedProfile: 'Default'
}
],
detectedBrowsersLoaded: true,
fetchDetectedBrowsers: vi.fn(),
importCookiesFromBrowser: vi.fn(),
importCookiesToProfile: vi.fn(),
openSettingsTarget: vi.fn(),
openSettingsPage: vi.fn()
openSettingsPage: vi.fn(),
persistedUIReady: true,
setBrowserImportHintHidden: vi.fn(),
settingsSearchQuery: ''
}
const useAppStore = (selector?: (s: typeof state) => unknown): unknown =>
selector ? selector(state) : state
@@ -3,6 +3,7 @@ import { Import } from 'lucide-react'
import { toast } from 'sonner'
import { emitBrowserCookieImportToast } from '@/lib/browser-cookie-import-toast'
import { Button } from '@/components/ui/button'
import { BrowserCookieImportDisclosure } from '@/components/BrowserCookieImportDisclosure'
import {
DropdownMenu,
DropdownMenuContent,
@@ -100,7 +101,8 @@ export function BrowserImportHintButton({
value1: browser?.label ?? browserFamily,
value2: browserProfile ? ` (${browserProfile})` : ''
}
)
),
result.profileId
)
return
}
@@ -120,7 +122,8 @@ export function BrowserImportHintButton({
'auto.components.browser.pane.BrowserImportHintButton.d40d584769',
'Imported {{value0}} cookies from file.',
{ value0: result.summary.importedCookies }
)
),
result.profileId
)
return
}
@@ -257,6 +260,7 @@ export function BrowserImportHintButton({
'From File…'
)}
</DropdownMenuItem>
<BrowserCookieImportDisclosure />
</DropdownMenuContent>
</DropdownMenu>
@@ -205,7 +205,8 @@ export function BrowserToolbarMenu({
value0: result.summary.importedCookies,
value1: browser?.label ?? browserFamily
}
)
),
result.profileId
)
} else {
toast.error(result.reason)
@@ -221,7 +222,8 @@ export function BrowserToolbarMenu({
'auto.components.browser.pane.BrowserToolbarMenu.53bbe3dab4',
'Imported {{value0}} cookies from file.',
{ value0: result.summary.importedCookies }
)
),
result.profileId
)
} else if (result.reason !== 'canceled') {
toast.error(result.reason)
@@ -4,7 +4,6 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
@@ -14,6 +13,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { BrowserCookieImportDisclosure } from '@/components/BrowserCookieImportDisclosure'
import { useAppStore } from '@/store'
import { BROWSER_FAMILY_LABELS } from '../../../../shared/constants'
import type { BrowserSessionProfile, BrowserViewportPresetId } from '../../../../shared/types'
@@ -128,12 +128,6 @@ export function BrowserToolbarMenuDropdown({
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuLabel className="font-normal">
{translate(
'auto.components.browser.pane.BrowserToolbarMenu.c186b4d890',
'Google requires signing in directly - imports skip it.'
)}
</DropdownMenuLabel>
{detectedBrowsers.map((browser) =>
browser.profiles.length > 1 ? (
<DropdownMenuSub key={browser.family}>
@@ -177,6 +171,7 @@ export function BrowserToolbarMenuDropdown({
'From File…'
)}
</DropdownMenuItem>
<BrowserCookieImportDisclosure />
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
@@ -7,7 +7,6 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
@@ -15,6 +14,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger
} from '../ui/dropdown-menu'
import { BrowserCookieImportDisclosure } from '../BrowserCookieImportDisclosure'
import { useAppStore } from '../../store'
import { BROWSER_FAMILY_LABELS } from '../../../../shared/constants'
import { translate } from '@/i18n/i18n'
@@ -81,7 +81,8 @@ export function BrowserProfileRow({
value1: browser?.label ?? browserFamily,
value2: profile.label
}
)
),
result.profileId
)
} else {
toast.error(result.reason)
@@ -97,7 +98,8 @@ export function BrowserProfileRow({
'auto.components.settings.BrowserProfileRow.b4c167764d',
'Imported {{value0}} cookies from file into {{value1}}.',
{ value0: result.summary.importedCookies, value1: profile.label }
)
),
result.profileId
)
} else if (result.reason !== 'canceled') {
toast.error(result.reason)
@@ -172,12 +174,6 @@ export function BrowserProfileRow({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel className="font-normal">
{translate(
'auto.components.settings.BrowserProfileRow.654a0c2073',
'Google requires signing in directly - imports skip it.'
)}
</DropdownMenuLabel>
{detectedBrowsers.map((browser) =>
browser.profiles.length > 1 ? (
<DropdownMenuSub key={browser.family}>
@@ -220,6 +216,7 @@ export function BrowserProfileRow({
<DropdownMenuItem onSelect={() => void handleImportFromFile()}>
{translate('auto.components.settings.BrowserProfileRow.ebb78dfd6f', 'From File…')}
</DropdownMenuItem>
<BrowserCookieImportDisclosure />
</DropdownMenuContent>
</DropdownMenu>
{isDefault ? (
@@ -3,6 +3,7 @@ import { toast } from 'sonner'
import { emitBrowserCookieImportToast } from '@/lib/browser-cookie-import-toast'
import { cn } from '@/lib/utils'
import { Button } from '../ui/button'
import { BrowserCookieImportDisclosure } from '../BrowserCookieImportDisclosure'
import {
DropdownMenu,
DropdownMenuContent,
@@ -58,7 +59,8 @@ export function BrowserUseCookieImportStep({
value1: browser?.label ?? browserFamily,
value2: browserProfile ? ` (${browserProfile})` : ''
}
)
),
result.profileId
)
} else {
toast.error(result.reason)
@@ -74,7 +76,8 @@ export function BrowserUseCookieImportStep({
'auto.components.settings.BrowserUsePane.8f2675c2f3',
'Imported {{value0}} cookies from file.',
{ value0: result.summary.importedCookies }
)
),
result.profileId
)
} else if (result.reason !== 'canceled') {
toast.error(result.reason)
@@ -204,6 +207,7 @@ export function BrowserUseCookieImportStep({
<DropdownMenuItem onSelect={() => void handleImportFromFile()}>
{translate('auto.components.settings.BrowserUsePane.be6df68384', 'From File…')}
</DropdownMenuItem>
<BrowserCookieImportDisclosure />
</DropdownMenuContent>
</DropdownMenu>
</div>
+9 -5
View File
@@ -757,7 +757,9 @@
"toast": {
"restartFallbackUnavailableNone": "None of the {{value0}} cookies could be loaded, and the restart fallback was unavailable. The previous cookies for this profile were replaced. Try the import again.",
"restartFallbackUnavailablePartial": "Imported {{value0}} of {{value1}} cookies. The rest could not be loaded, and the restart fallback was unavailable. Try the import again.",
"googleCookiesSkipped": "Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google."
"googleCookiesSkipped": "Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.",
"googleDirectSignInAction": "Sign in to Google",
"googleDirectSignInUnavailable": "Could not open the browser profile. Open it and sign in at accounts.google.com."
}
}
}
@@ -926,6 +928,10 @@
}
},
"components": {
"BrowserCookieImportDisclosure": {
"title": "Google logins aren't imported",
"description": "Sign in to Google directly in Orca."
},
"CodexRestartChip": {
"a4c8e1b2f7": "Codex is still signed in as {{value0}}",
"c72a5fb234": "Restart",
@@ -5858,8 +5864,7 @@
"a3f8c2d1e0b4": "Imported {{value0}} cookies from {{value1}} ({{value2}}) into {{value3}}.",
"b4e9d3f2a1c5": "Imported {{value0}} cookies from {{value1}} into {{value2}}.",
"c5a273a809": "From {{value0}}",
"b5c0479e21": "Unmodified user agent",
"654a0c2073": "Google requires signing in directly - imports skip it."
"b5c0479e21": "Unmodified user agent"
},
"BrowserUseComputerUseNotice": {
"15b5e680ba": "Open Computer Use",
@@ -14214,8 +14219,7 @@
"bf648471c5": "Creating…",
"53bbe3dab4": "Imported {{value0}} cookies from file.",
"c5f0e4d3b2a1": "Imported {{value0}} cookies from {{value1}} ({{value2}}).",
"d6a1f5e4c3b2": "Imported {{value0}} cookies from {{value1}}.",
"c186b4d890": "Google requires signing in directly - imports skip it."
"d6a1f5e4c3b2": "Imported {{value0}} cookies from {{value1}}."
},
"GrabConfirmationSheet": {
"314a0aaa5b": "Attach to AI",
@@ -1,12 +1,18 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { successToastMock, warningToastMock } = vi.hoisted(() => ({
const { successToastMock, warningToastMock, errorToastMock, getStateMock } = vi.hoisted(() => ({
successToastMock: vi.fn(),
warningToastMock: vi.fn()
warningToastMock: vi.fn(),
errorToastMock: vi.fn(),
getStateMock: vi.fn()
}))
vi.mock('sonner', () => ({
toast: { success: successToastMock, warning: warningToastMock }
toast: { success: successToastMock, warning: warningToastMock, error: errorToastMock }
}))
vi.mock('@/store', () => ({
useAppStore: { getState: getStateMock }
}))
import type { BrowserCookieImportSummary } from '../../../shared/types'
@@ -23,6 +29,13 @@ describe('emitBrowserCookieImportToast', () => {
beforeEach(() => {
successToastMock.mockReset()
warningToastMock.mockReset()
errorToastMock.mockReset()
getStateMock.mockReset()
getStateMock.mockReturnValue({
activeWorktreeId: 'worktree-1',
closeSettingsPage: vi.fn(),
openBrowserProfileTabInActiveWorkspace: vi.fn().mockResolvedValue(true)
})
})
it('shows the localized total-failure warning', () => {
@@ -35,7 +48,8 @@ describe('emitBrowserCookieImportToast', () => {
failedCookies: 3
}
},
'Imported 3 cookies.'
'Imported 3 cookies.',
'profile-1'
)
expect(warningToastMock).toHaveBeenCalledWith(
@@ -54,7 +68,8 @@ describe('emitBrowserCookieImportToast', () => {
failedCookies: 1
}
},
'Imported 3 cookies.'
'Imported 3 cookies.',
'profile-1'
)
expect(warningToastMock).toHaveBeenCalledWith(
@@ -64,7 +79,7 @@ describe('emitBrowserCookieImportToast', () => {
})
it('shows success when the import has no warning', () => {
emitBrowserCookieImportToast(summary, 'Imported 3 cookies.')
emitBrowserCookieImportToast(summary, 'Imported 3 cookies.', 'profile-1')
expect(successToastMock).toHaveBeenCalledWith('Imported 3 cookies.')
expect(warningToastMock).not.toHaveBeenCalled()
@@ -73,13 +88,15 @@ describe('emitBrowserCookieImportToast', () => {
it('shows a separate Google sign-in warning after the concise success toast', () => {
emitBrowserCookieImportToast(
{ ...summary, importedCookies: 2, skippedCookies: 1, googleCookiesSkipped: 1 },
'Imported 2 cookies.'
'Imported 2 cookies.',
'profile-1'
)
expect(successToastMock).toHaveBeenCalledWith('Imported 2 cookies.')
expect(warningToastMock).toHaveBeenCalledWith(
expect(warningToastMock.mock.calls[0][0]).toBe(
'Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.'
)
expect(warningToastMock.mock.calls[0][1].action.label).toBe('Sign in to Google')
expect(successToastMock.mock.invocationCallOrder[0]).toBeLessThan(
warningToastMock.mock.invocationCallOrder[0]
)
@@ -88,7 +105,8 @@ describe('emitBrowserCookieImportToast', () => {
it('does not infer a Google warning from generic skipped cookies', () => {
emitBrowserCookieImportToast(
{ ...summary, importedCookies: 2, skippedCookies: 1 },
'Imported 2 cookies.'
'Imported 2 cookies.',
'profile-1'
)
expect(successToastMock).toHaveBeenCalledWith('Imported 2 cookies.')
@@ -108,7 +126,8 @@ describe('emitBrowserCookieImportToast', () => {
failedCookies: 1
}
},
'Imported 1 cookie.'
'Imported 1 cookie.',
'profile-1'
)
expect(successToastMock).not.toHaveBeenCalled()
@@ -117,4 +136,81 @@ describe('emitBrowserCookieImportToast', () => {
'Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.'
])
})
it('opens Google with the imported profile from the warning action', async () => {
const closeSettingsPage = vi.fn()
const openBrowserProfileTabInActiveWorkspace = vi.fn().mockResolvedValue(true)
getStateMock.mockReturnValue({
activeWorktreeId: 'worktree-1',
closeSettingsPage,
openBrowserProfileTabInActiveWorkspace
})
emitBrowserCookieImportToast(
{ ...summary, googleCookiesSkipped: 1 },
'Imported 2 cookies.',
'profile-1'
)
warningToastMock.mock.calls[0][1].action.onClick()
await vi.waitFor(() => expect(closeSettingsPage).toHaveBeenCalledTimes(1))
expect(openBrowserProfileTabInActiveWorkspace).toHaveBeenCalledWith(
'https://accounts.google.com/',
'profile-1'
)
})
it('keeps guidance but omits the action without an active worktree', () => {
getStateMock.mockReturnValue({ activeWorktreeId: null })
emitBrowserCookieImportToast(
{ ...summary, googleCookiesSkipped: 1 },
'Imported 2 cookies.',
'profile-1'
)
expect(warningToastMock).toHaveBeenLastCalledWith(
'Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.'
)
})
it('reports when the profile tab cannot be opened', async () => {
const openBrowserProfileTabInActiveWorkspace = vi.fn().mockResolvedValue(false)
getStateMock.mockReturnValue({
activeWorktreeId: 'worktree-1',
closeSettingsPage: vi.fn(),
openBrowserProfileTabInActiveWorkspace
})
emitBrowserCookieImportToast(
{ ...summary, googleCookiesSkipped: 1 },
'Imported 2 cookies.',
'profile-1'
)
warningToastMock.mock.calls[0][1].action.onClick()
await vi.waitFor(() => expect(errorToastMock).toHaveBeenCalledTimes(1))
expect(errorToastMock).toHaveBeenCalledWith(
'Could not open the browser profile. Open it and sign in at accounts.google.com.'
)
})
it('reports when opening the profile tab rejects', async () => {
getStateMock.mockReturnValue({
activeWorktreeId: 'worktree-1',
closeSettingsPage: vi.fn(),
openBrowserProfileTabInActiveWorkspace: vi
.fn()
.mockRejectedValue(new Error('runtime unavailable'))
})
emitBrowserCookieImportToast(
{ ...summary, googleCookiesSkipped: 1 },
'Imported 2 cookies.',
'profile-1'
)
warningToastMock.mock.calls[0][1].action.onClick()
await vi.waitFor(() => expect(errorToastMock).toHaveBeenCalledTimes(1))
})
})
@@ -1,6 +1,7 @@
import { toast } from 'sonner'
import type { BrowserCookieImportSummary } from '../../../shared/types'
import { translate } from '@/i18n/i18n'
import { useAppStore } from '@/store'
type CookieImportWarning = NonNullable<BrowserCookieImportSummary['warning']>
@@ -24,23 +25,63 @@ function formatCookieImportWarning(warning: CookieImportWarning): string {
}
}
function emitGoogleCookieImportWarning(summary: BrowserCookieImportSummary): void {
const GOOGLE_SIGN_IN_URL = 'https://accounts.google.com/'
function googleSignInErrorMessage(): string {
return translate(
'auto.lib.browser.cookie.import.toast.googleDirectSignInUnavailable',
'Could not open the browser profile. Open it and sign in at accounts.google.com.'
)
}
function emitGoogleCookieImportWarning(
summary: BrowserCookieImportSummary,
profileId: string
): void {
if (!summary.googleCookiesSkipped) {
return
}
toast.warning(
translate(
'auto.lib.browser.cookie.import.toast.googleCookiesSkipped',
'Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.'
)
const message = translate(
'auto.lib.browser.cookie.import.toast.googleCookiesSkipped',
'Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.'
)
if (!useAppStore.getState().activeWorktreeId) {
toast.warning(message)
return
}
toast.warning(message, {
duration: 12000,
action: {
label: translate(
'auto.lib.browser.cookie.import.toast.googleDirectSignInAction',
'Sign in to Google'
),
onClick: () => {
void useAppStore
.getState()
.openBrowserProfileTabInActiveWorkspace(GOOGLE_SIGN_IN_URL, profileId)
.then((opened) => {
if (!opened) {
toast.error(googleSignInErrorMessage())
return
}
// Why: Settings would cover the newly opened browser tab.
useAppStore.getState().closeSettingsPage()
})
.catch(() => {
toast.error(googleSignInErrorMessage())
})
}
}
})
}
// Why: a degraded import returns ok:true with a warning, so every call site must route it to a
// warning toast instead of reporting an unqualified success (#9355).
export function emitBrowserCookieImportToast(
summary: BrowserCookieImportSummary,
successMessage: string
successMessage: string,
profileId: string
): void {
const warning = summary.warning
if (warning) {
@@ -48,5 +89,5 @@ export function emitBrowserCookieImportToast(
} else {
toast.success(successMessage)
}
emitGoogleCookieImportWarning(summary)
emitGoogleCookieImportWarning(summary, profileId)
}
@@ -164,6 +164,21 @@ describe('createBrowserSlice annotations', () => {
expect(store.getState().recordFeatureInteraction).toHaveBeenCalledWith('browser-tab-created')
})
it('opens a local sign-in tab with the imported browser profile', async () => {
const store = createTestStore()
await expect(
store
.getState()
.openBrowserProfileTabInActiveWorkspace('https://accounts.google.com/', 'profile-1')
).resolves.toBe(true)
expect(store.getState().browserTabsByWorktree['wt-1']?.[0]).toMatchObject({
url: 'https://accounts.google.com/',
sessionProfileId: 'profile-1'
})
})
it('clears page annotations when the browser page URL changes', () => {
const store = createTestStore()
const tab = store.getState().createBrowserTab('wt-1', 'https://example.com')
@@ -1028,6 +1043,38 @@ describe('createBrowserSlice runtime guard', () => {
)
})
it('opens a remote sign-in tab with the imported browser profile', async () => {
const store = createTestStore()
store.setState({
activeWorktreeId: 'wt-remote',
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
worktreesByRepo: {
'repo-1': [
{
id: 'wt-remote',
repoId: 'repo-1',
hostId: 'local',
runtimeOwnerEnvironmentId: 'env-1'
} as never
]
}
})
await expect(
store
.getState()
.openBrowserProfileTabInActiveWorkspace('https://accounts.google.com/', 'profile-1')
).resolves.toBe(true)
expect(createWebRuntimeSessionBrowserTabMock).toHaveBeenCalledWith({
worktreeId: 'wt-remote',
environmentId: 'env-1',
url: 'https://accounts.google.com/',
profileId: 'profile-1'
})
expect(store.getState().browserTabsByWorktree['wt-remote']).toBeUndefined()
})
it('does not create a local fallback tab when remote browser creation throws', async () => {
const store = createTestStore()
createWebRuntimeSessionBrowserTabMock.mockRejectedValueOnce(new Error('remote down'))
+29
View File
@@ -135,6 +135,7 @@ export type BrowserSlice = {
options?: CreateBrowserTabOptions
) => BrowserWorkspace
openNewBrowserTabInActiveWorkspace: (groupId: string) => Promise<void>
openBrowserProfileTabInActiveWorkspace: (url: string, profileId: string) => Promise<boolean>
closeBrowserTab: (tabId: string) => void
shutdownWorktreeBrowsers: (worktreeId: string) => Promise<void>
reopenClosedBrowserTab: (worktreeId: string) => BrowserWorkspace | null
@@ -706,6 +707,34 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
})
get().recordFeatureInteraction('browser-tab-created')
},
openBrowserProfileTabInActiveWorkspace: async (url, profileId) => {
const state = get()
const worktreeId = state.activeWorktreeId
if (!worktreeId) {
return false
}
const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(state, worktreeId)
if (runtimeEnvironmentId) {
const { createWebRuntimeSessionBrowserTab } = await import('@/runtime/web-runtime-session')
try {
return await createWebRuntimeSessionBrowserTab({
worktreeId,
environmentId: runtimeEnvironmentId,
url,
profileId
})
} catch (error) {
console.warn(
'[browser] remote profile tab creation failed:',
error instanceof Error ? error.message : String(error)
)
return false
}
}
get().createBrowserTab(worktreeId, url, { activate: true, sessionProfileId: profileId })
return true
},
closeBrowserTab: (tabId) => {
let remotePagesToClose: { worktreeId: string; handle: RemoteBrowserPageHandle }[] = []
set((s) => {