From 36d45af062ec4022265aabac8fc6a9d36eecedf1 Mon Sep 17 00:00:00 2001
From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Date: Wed, 12 Aug 2026 00:53:02 -0700
Subject: [PATCH] feat(browser): guide Google sign-in after cookie imports
(STA-3811) (#13666)
---
.../BrowserCookieImportDisclosure.tsx | 28 +++++
...r-cookie-import-google-disclosure.test.tsx | 101 +++++++++++----
.../browser-pane/BrowserImportHintButton.tsx | 8 +-
.../browser-pane/BrowserToolbarMenu.tsx | 6 +-
.../browser-toolbar-menu-dropdown.tsx | 9 +-
.../components/settings/BrowserProfileRow.tsx | 15 +--
.../settings/BrowserUseCookieImportStep.tsx | 8 +-
src/renderer/src/i18n/locales/en.json | 14 ++-
.../lib/browser-cookie-import-toast.test.ts | 116 ++++++++++++++++--
.../src/lib/browser-cookie-import-toast.ts | 57 +++++++--
src/renderer/src/store/slices/browser.test.ts | 47 +++++++
src/renderer/src/store/slices/browser.ts | 29 +++++
12 files changed, 367 insertions(+), 71 deletions(-)
create mode 100644 src/renderer/src/components/BrowserCookieImportDisclosure.tsx
diff --git a/src/renderer/src/components/BrowserCookieImportDisclosure.tsx b/src/renderer/src/components/BrowserCookieImportDisclosure.tsx
new file mode 100644
index 00000000000..9e7c203675d
--- /dev/null
+++ b/src/renderer/src/components/BrowserCookieImportDisclosure.tsx
@@ -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 (
+ <>
+
+
+
+
+
+ {translate(
+ 'auto.components.BrowserCookieImportDisclosure.title',
+ "Google logins aren't imported"
+ )}
+
+
+ {translate(
+ 'auto.components.BrowserCookieImportDisclosure.description',
+ 'Sign in to Google directly in Orca.'
+ )}
+
+
+
+ >
+ )
+}
diff --git a/src/renderer/src/components/browser-cookie-import-google-disclosure.test.tsx b/src/renderer/src/components/browser-cookie-import-google-disclosure.test.tsx
index 6ef0481edab..0d17baed21b 100644
--- a/src/renderer/src/components/browser-cookie-import-google-disclosure.test.tsx
+++ b/src/renderer/src/components/browser-cookie-import-google-disclosure.test.tsx
@@ -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',
+ () => (
{
onApplyViewportPreset={vi.fn()}
/>
)
- })
-
- expect(container.textContent).toContain(DISCLOSURE)
- })
-
- it('is shown in the Settings browser-profile import menu', () => {
- act(() => {
- root.render(
+ ],
+ ['browser toolbar hint', () => ],
+ [
+ 'Settings browser-use setup',
+ () => (
+
+ )
+ ],
+ [
+ 'Settings browser-profile row',
+ () => (
{
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())
+
+ 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 {
DropdownMenu: passthrough,
DropdownMenuContent: block,
DropdownMenuItem: block,
- DropdownMenuLabel: block,
+ DropdownMenuLabel: ({ children }: { children?: ReactNode }): ReactNode => (
+ {children}
+ ),
DropdownMenuPortal: passthrough,
DropdownMenuRadioGroup: passthrough,
DropdownMenuRadioItem: block,
- DropdownMenuSeparator: () => null,
+ DropdownMenuSeparator: () =>
,
DropdownMenuSub: passthrough,
DropdownMenuSubContent: block,
DropdownMenuSubTrigger: block,
@@ -125,11 +152,33 @@ function dropdownMenuStubs(): Record {
}
}
+function popoverStubs(): Record {
+ const passthrough = ({ children }: { children?: ReactNode }): ReactNode => children
+ const block = ({ children }: { children?: ReactNode }): ReactNode => {children}
+ 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
diff --git a/src/renderer/src/components/browser-pane/BrowserImportHintButton.tsx b/src/renderer/src/components/browser-pane/BrowserImportHintButton.tsx
index 32af12eeb10..18f5cf28e1c 100644
--- a/src/renderer/src/components/browser-pane/BrowserImportHintButton.tsx
+++ b/src/renderer/src/components/browser-pane/BrowserImportHintButton.tsx
@@ -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…'
)}
+
diff --git a/src/renderer/src/components/browser-pane/BrowserToolbarMenu.tsx b/src/renderer/src/components/browser-pane/BrowserToolbarMenu.tsx
index 90afa5aec59..9d250d20221 100644
--- a/src/renderer/src/components/browser-pane/BrowserToolbarMenu.tsx
+++ b/src/renderer/src/components/browser-pane/BrowserToolbarMenu.tsx
@@ -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)
diff --git a/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx b/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx
index 0491f55db05..508eb059ca2 100644
--- a/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx
+++ b/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx
@@ -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({
-
- {translate(
- 'auto.components.browser.pane.BrowserToolbarMenu.c186b4d890',
- 'Google requires signing in directly - imports skip it.'
- )}
-
{detectedBrowsers.map((browser) =>
browser.profiles.length > 1 ? (
@@ -177,6 +171,7 @@ export function BrowserToolbarMenuDropdown({
'From File…'
)}
+
diff --git a/src/renderer/src/components/settings/BrowserProfileRow.tsx b/src/renderer/src/components/settings/BrowserProfileRow.tsx
index 7ab502698ee..1240a37ed0f 100644
--- a/src/renderer/src/components/settings/BrowserProfileRow.tsx
+++ b/src/renderer/src/components/settings/BrowserProfileRow.tsx
@@ -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({
-
- {translate(
- 'auto.components.settings.BrowserProfileRow.654a0c2073',
- 'Google requires signing in directly - imports skip it.'
- )}
-
{detectedBrowsers.map((browser) =>
browser.profiles.length > 1 ? (
@@ -220,6 +216,7 @@ export function BrowserProfileRow({
void handleImportFromFile()}>
{translate('auto.components.settings.BrowserProfileRow.ebb78dfd6f', 'From File…')}
+
{isDefault ? (
diff --git a/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx b/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx
index 1332883e4a1..0fcb3eb4e81 100644
--- a/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx
+++ b/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx
@@ -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({
void handleImportFromFile()}>
{translate('auto.components.settings.BrowserUsePane.be6df68384', 'From File…')}
+
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 473157aacf7..b984ac7aad8 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -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",
diff --git a/src/renderer/src/lib/browser-cookie-import-toast.test.ts b/src/renderer/src/lib/browser-cookie-import-toast.test.ts
index 6a23e0f4a43..3fbd97d6d4b 100644
--- a/src/renderer/src/lib/browser-cookie-import-toast.test.ts
+++ b/src/renderer/src/lib/browser-cookie-import-toast.test.ts
@@ -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))
+ })
})
diff --git a/src/renderer/src/lib/browser-cookie-import-toast.ts b/src/renderer/src/lib/browser-cookie-import-toast.ts
index 6ded5ef9969..2bd4538007a 100644
--- a/src/renderer/src/lib/browser-cookie-import-toast.ts
+++ b/src/renderer/src/lib/browser-cookie-import-toast.ts
@@ -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
@@ -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)
}
diff --git a/src/renderer/src/store/slices/browser.test.ts b/src/renderer/src/store/slices/browser.test.ts
index c760b37a655..420813a4a36 100644
--- a/src/renderer/src/store/slices/browser.test.ts
+++ b/src/renderer/src/store/slices/browser.test.ts
@@ -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'))
diff --git a/src/renderer/src/store/slices/browser.ts b/src/renderer/src/store/slices/browser.ts
index 3911830c072..cc6e399651d 100644
--- a/src/renderer/src/store/slices/browser.ts
+++ b/src/renderer/src/store/slices/browser.ts
@@ -135,6 +135,7 @@ export type BrowserSlice = {
options?: CreateBrowserTabOptions
) => BrowserWorkspace
openNewBrowserTabInActiveWorkspace: (groupId: string) => Promise
+ openBrowserProfileTabInActiveWorkspace: (url: string, profileId: string) => Promise
closeBrowserTab: (tabId: string) => void
shutdownWorktreeBrowsers: (worktreeId: string) => Promise
reopenClosedBrowserTab: (worktreeId: string) => BrowserWorkspace | null
@@ -706,6 +707,34 @@ export const createBrowserSlice: StateCreator =
})
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) => {