feat(settings): give browser its own settings pane (#764)

- Move browser-related settings out of the in-browser gear dialog and
  out of General into a dedicated Browser pane in Settings, and route
  the browser gear button to open it.
This commit is contained in:
Jinjing 2026-04-17 10:35:52 -07:00 committed by GitHub
parent 949f3e396a
commit c655c13629
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 338 additions and 460 deletions

View file

@ -19,7 +19,6 @@ import {
SquareCode
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
DropdownMenu,
DropdownMenuContent,
@ -29,14 +28,6 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useAppStore } from '@/store'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { ORCA_BROWSER_BLANK_URL, ORCA_BROWSER_PARTITION } from '../../../../shared/constants'
import type {
BrowserLoadError,
@ -396,14 +387,11 @@ function BrowserPagePane({
const grab = useGrabMode(browserTab.id)
const createBrowserTab = useAppStore((s) => s.createBrowserTab)
const consumeAddressBarFocusRequest = useAppStore((s) => s.consumeAddressBarFocusRequest)
const browserDefaultUrl = useAppStore((s) => s.browserDefaultUrl)
const setBrowserDefaultUrl = useAppStore((s) => s.setBrowserDefaultUrl)
const browserSessionProfiles = useAppStore((s) => s.browserSessionProfiles)
const sessionProfile = sessionProfileId
? (browserSessionProfiles.find((p) => p.id === sessionProfileId) ?? null)
: null
const webviewPartition = sessionProfile?.partition ?? ORCA_BROWSER_PARTITION
const detectedBrowsers = useAppStore((s) => s.detectedBrowsers)
const browserSessionImportState = useAppStore((s) => s.browserSessionImportState)
const clearBrowserSessionImportState = useAppStore((s) => s.clearBrowserSessionImportState)
@ -434,29 +422,6 @@ function BrowserPagePane({
}, [resourceNotice])
const keepAddressBarFocusRef = useRef(false)
const [settingsOpen, setSettingsOpen] = useState(false)
const [homePageDraft, setHomePageDraft] = useState('')
const saveHomePage = useCallback(() => {
const trimmed = homePageDraft.trim()
if (!trimmed) {
// Why: empty input treated as "clear" so the user can remove a home page
// without having to click the separate Clear button.
setBrowserDefaultUrl(null)
} else {
const normalized = normalizeBrowserNavigationUrl(trimmed)
if (normalized && normalized !== ORCA_BROWSER_BLANK_URL) {
setBrowserDefaultUrl(normalized)
}
// Why: if the URL is not navigable (e.g. plain text with no scheme),
// leave the draft as-is so the user can correct it rather than silently
// discarding their input.
if (!normalized || normalized === ORCA_BROWSER_BLANK_URL) {
return
}
}
setSettingsOpen(false)
}, [homePageDraft, setBrowserDefaultUrl])
// Inline toast that appears near the grabbed element instead of the global
// bottom-right toaster, so feedback feels spatially connected to the action.
@ -478,14 +443,6 @@ function BrowserPagePane({
return () => clearTimeout(grabToastTimerRef.current)
}, [])
// Why: populate the home-page draft from the stored value each time the
// settings dialog opens so the user sees the current setting pre-filled
// rather than an empty field or a stale in-memory edit.
useEffect(() => {
if (settingsOpen) {
setHomePageDraft(browserDefaultUrl ?? '')
}
}, [settingsOpen, browserDefaultUrl])
const grabRef = useRef(grab)
grabRef.current = grab
@ -1757,136 +1714,6 @@ function BrowserPagePane({
)
: null}
{/* Browser Settings dialog — uses Radix Portal so layout is unaffected */}
<Dialog open={settingsOpen} onOpenChange={setSettingsOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Browser Settings</DialogTitle>
</DialogHeader>
<div className="py-1">
<p className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
General
</p>
<div className="space-y-1.5">
<Label htmlFor="browser-home-page">Home Page</Label>
<p className="text-xs text-muted-foreground">
URL to open when creating a new tab. Leave empty for a blank tab.
</p>
<Input
id="browser-home-page"
value={homePageDraft}
onChange={(e) => setHomePageDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
saveHomePage()
}
}}
placeholder="https://google.com"
spellCheck={false}
autoCapitalize="none"
autoCorrect="off"
className="h-8 text-sm"
/>
</div>
</div>
<div className="border-t border-border pt-4">
<p className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Session &amp; Cookies
</p>
{sessionProfile ? (
<div className="mb-3 flex items-center gap-2 rounded-lg bg-accent/40 px-3 py-2">
<Import className="size-4 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">{sessionProfile.label}</p>
<p className="text-xs text-muted-foreground">
{sessionProfile.source
? `Imported from ${sessionProfile.source.browserFamily}${sessionProfile.source.profileName ? ` (${sessionProfile.source.profileName})` : ''}`
: 'Custom session profile'}
</p>
</div>
</div>
) : (
<p className="mb-3 text-xs text-muted-foreground">
Using the default shared session. Import cookies from your browser to use existing
logins.
</p>
)}
<div className="space-y-1">
{detectedBrowsers.map((browser) => (
<Button
key={browser.family}
variant="outline"
size="sm"
className="w-full justify-start gap-2"
onClick={async () => {
const store = useAppStore.getState()
let targetProfileId = sessionProfileId
if (!targetProfileId) {
const profile = await store.createBrowserSessionProfile(
'imported',
`${browser.label} Session`
)
if (!profile) {
return
}
targetProfileId = profile.id
}
void store.importCookiesFromBrowser(targetProfileId, browser.family)
setSettingsOpen(false)
}}
>
<Import className="size-3.5" />
Import from {browser.label}
</Button>
))}
<Button
variant="outline"
size="sm"
className="w-full justify-start gap-2"
onClick={async () => {
const store = useAppStore.getState()
let targetProfileId = sessionProfileId
if (!targetProfileId) {
const profile = await store.createBrowserSessionProfile(
'imported',
'Imported Session'
)
if (!profile) {
return
}
targetProfileId = profile.id
}
void store.importCookiesToProfile(targetProfileId)
setSettingsOpen(false)
}}
>
<Import className="size-3.5" />
Import from File
</Button>
</div>
{sessionProfile && (
<Button
variant="ghost"
size="sm"
className="mt-2 w-full justify-start gap-2 text-destructive hover:text-destructive"
onClick={async () => {
await useAppStore.getState().deleteBrowserSessionProfile(sessionProfile.id)
setSettingsOpen(false)
}}
>
Clear Imported Session
</Button>
)}
</div>
<DialogFooter>
<Button size="sm" onClick={saveHomePage}>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<div className="relative z-10 flex items-center gap-2 border-b border-border/70 bg-background/95 px-3 py-1.5">
<Button
size="icon"
@ -1992,7 +1819,7 @@ function BrowserPagePane({
className="h-8 w-8"
title="Browser Settings"
onClick={() => {
useAppStore.getState().openSettingsTarget({ pane: 'general', repoId: null })
useAppStore.getState().openSettingsTarget({ pane: 'browser', repoId: null })
useAppStore.getState().openSettingsPage()
}}
>

View file

@ -0,0 +1,283 @@
import { useEffect, useState } from 'react'
import { Import, Loader2, Trash2 } from 'lucide-react'
import { toast } from 'sonner'
import type { GlobalSettings } from '../../../../shared/types'
import { Button } from '../ui/button'
import { Input } from '../ui/input'
import { Label } from '../ui/label'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '../ui/dropdown-menu'
import { useAppStore } from '../../store'
import { ORCA_BROWSER_BLANK_URL } from '../../../../shared/constants'
import { normalizeBrowserNavigationUrl } from '../../../../shared/browser-url'
import { SearchableSetting } from './SearchableSetting'
import { matchesSettingsSearch } from './settings-search'
import { BROWSER_PANE_SEARCH_ENTRIES } from './browser-search'
export { BROWSER_PANE_SEARCH_ENTRIES }
type BrowserPaneProps = {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
}
export function BrowserPane({ settings, updateSettings }: BrowserPaneProps): React.JSX.Element {
const searchQuery = useAppStore((s) => s.settingsSearchQuery)
const browserDefaultUrl = useAppStore((s) => s.browserDefaultUrl)
const setBrowserDefaultUrl = useAppStore((s) => s.setBrowserDefaultUrl)
const detectedBrowsers = useAppStore((s) => s.detectedBrowsers)
const browserSessionProfiles = useAppStore((s) => s.browserSessionProfiles)
const browserSessionImportState = useAppStore((s) => s.browserSessionImportState)
const defaultProfile = browserSessionProfiles.find((p) => p.id === 'default')
const orphanedProfiles = browserSessionProfiles.filter((p) => p.scope !== 'default')
const [homePageDraft, setHomePageDraft] = useState(browserDefaultUrl ?? '')
// Why: sync draft with store value whenever it changes externally (e.g. the
// in-app browser tab's address bar saves a home page). Without this, the
// settings field would show stale text after another surface wrote the value.
useEffect(() => {
setHomePageDraft(browserDefaultUrl ?? '')
}, [browserDefaultUrl])
const showHomePage = matchesSettingsSearch(searchQuery, [BROWSER_PANE_SEARCH_ENTRIES[0]])
const showLinkRouting = matchesSettingsSearch(searchQuery, [BROWSER_PANE_SEARCH_ENTRIES[1]])
const showCookies = matchesSettingsSearch(searchQuery, [BROWSER_PANE_SEARCH_ENTRIES[2]])
return (
<div className="space-y-4">
{showHomePage ? (
<SearchableSetting
title="Default Home Page"
description="URL opened when creating a new browser tab. Leave empty to open a blank tab."
keywords={['browser', 'home', 'homepage', 'default', 'url', 'new tab', 'blank']}
className="flex items-start justify-between gap-4 px-1 py-2"
>
<div className="min-w-0 shrink space-y-0.5">
<Label>Default Home Page</Label>
<p className="text-xs text-muted-foreground">
URL opened when creating a new browser tab. Leave empty to open a blank tab.
</p>
</div>
<form
className="flex shrink-0 items-center gap-2"
onSubmit={(e) => {
e.preventDefault()
const trimmed = homePageDraft.trim()
if (!trimmed) {
setBrowserDefaultUrl(null)
return
}
const normalized = normalizeBrowserNavigationUrl(trimmed)
if (normalized && normalized !== ORCA_BROWSER_BLANK_URL) {
setBrowserDefaultUrl(normalized)
setHomePageDraft(normalized)
toast.success('Home page saved.')
}
}}
>
<Input
value={homePageDraft}
onChange={(e) => setHomePageDraft(e.target.value)}
placeholder="https://google.com"
spellCheck={false}
autoCapitalize="none"
autoCorrect="off"
className="h-7 w-52 text-xs"
/>
<Button type="submit" size="sm" variant="outline" className="h-7 text-xs">
Save
</Button>
</form>
</SearchableSetting>
) : null}
{showLinkRouting ? (
<SearchableSetting
title="Terminal Link Routing"
description="Cmd/Ctrl+click opens terminal http(s) links in Orca. Shift+Cmd/Ctrl+click uses the system browser."
keywords={['browser', 'preview', 'links', 'localhost', 'webview']}
className="flex items-center justify-between gap-4 px-1 py-2"
>
<div className="space-y-0.5">
<Label>Terminal Link Routing</Label>
<p className="text-xs text-muted-foreground">
Cmd/Ctrl+click opens terminal links in Orca. Shift+Cmd/Ctrl+click opens the same link
in your system browser.
</p>
</div>
<button
role="switch"
aria-checked={settings.openLinksInApp}
onClick={() => updateSettings({ openLinksInApp: !settings.openLinksInApp })}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
settings.openLinksInApp ? 'bg-foreground' : 'bg-muted-foreground/30'
}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-background shadow-sm transition-transform ${
settings.openLinksInApp ? 'translate-x-4' : 'translate-x-0.5'
}`}
/>
</button>
</SearchableSetting>
) : null}
{showCookies ? (
<SearchableSetting
title="Session & Cookies"
description="Import cookies from Chrome, Edge, or other browsers to use existing logins inside Orca."
keywords={[
'cookies',
'session',
'import',
'auth',
'login',
'chrome',
'edge',
'arc',
'profile'
]}
className="space-y-3 px-1 py-2"
>
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<Label>Session &amp; Cookies</Label>
<p className="text-xs text-muted-foreground">
Import cookies from your system browser to reuse existing logins inside Orca.
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="xs"
className="shrink-0 gap-1.5"
disabled={browserSessionImportState?.status === 'importing'}
>
{browserSessionImportState?.status === 'importing' ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Import className="size-3" />
)}
Import Cookies
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{detectedBrowsers.map((browser) => (
<DropdownMenuItem
key={browser.family}
onSelect={async () => {
const store = useAppStore.getState()
const result = await store.importCookiesFromBrowser('default', browser.family)
if (result.ok) {
toast.success(
`Imported ${result.summary.importedCookies} cookies from ${browser.label}.`
)
} else {
toast.error(result.reason)
}
}}
>
From {browser.label}
</DropdownMenuItem>
))}
{detectedBrowsers.length > 0 && <DropdownMenuSeparator />}
<DropdownMenuItem
onSelect={async () => {
const store = useAppStore.getState()
const result = await store.importCookiesToProfile('default')
if (result.ok) {
toast.success(`Imported ${result.summary.importedCookies} cookies from file.`)
} else if (result.reason !== 'canceled') {
toast.error(result.reason)
}
}}
>
From File
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{defaultProfile?.source ? (
<div className="flex w-full items-center justify-between gap-3 rounded-md border border-border/70 px-3 py-2.5">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate text-sm font-medium">
Imported from {defaultProfile.source.browserFamily}
{defaultProfile.source.profileName
? ` (${defaultProfile.source.profileName})`
: ''}
</span>
{defaultProfile.source.importedAt ? (
<span className="truncate text-[11px] text-muted-foreground">
{new Date(defaultProfile.source.importedAt).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
})}
</span>
) : null}
</div>
<Button
variant="ghost"
size="xs"
className="gap-1 text-muted-foreground hover:text-destructive"
onClick={async () => {
const ok = await useAppStore.getState().clearDefaultSessionCookies()
if (ok) {
toast.success('Cookies cleared.')
}
}}
>
<Trash2 className="size-3" />
Clear
</Button>
</div>
) : null}
{orphanedProfiles.length > 0 ? (
<div className="space-y-2">
{orphanedProfiles.map((profile) => (
<div
key={profile.id}
className="flex w-full items-center justify-between gap-3 rounded-md border border-border/70 px-3 py-2.5"
>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate text-sm font-medium">{profile.label}</span>
<span className="truncate text-[11px] text-muted-foreground">
{profile.source
? `Imported from ${profile.source.browserFamily}${profile.source.profileName ? ` (${profile.source.profileName})` : ''}`
: 'Unused session'}
</span>
</div>
<Button
variant="ghost"
size="xs"
className="gap-1 text-muted-foreground hover:text-destructive"
onClick={async () => {
const ok = await useAppStore
.getState()
.deleteBrowserSessionProfile(profile.id)
if (ok) {
toast.success('Session removed.')
}
}}
>
<Trash2 className="size-3" />
Remove
</Button>
</div>
))}
</div>
) : null}
</SearchableSetting>
) : null}
</div>
)
}

View file

@ -8,20 +8,17 @@ import { Button } from '../ui/button'
import { Input } from '../ui/input'
import { Label } from '../ui/label'
import { Separator } from '../ui/separator'
import { Download, FolderOpen, Import, Loader2, Plus, RefreshCw, Timer, Trash2 } from 'lucide-react'
import { Download, FolderOpen, Loader2, Plus, RefreshCw, Timer, Trash2 } from 'lucide-react'
import { useAppStore } from '../../store'
import { CliSection } from './CliSection'
import { toast } from 'sonner'
import {
DEFAULT_EDITOR_AUTO_SAVE_DELAY_MS,
MAX_EDITOR_AUTO_SAVE_DELAY_MS,
MIN_EDITOR_AUTO_SAVE_DELAY_MS,
ORCA_BROWSER_BLANK_URL
MIN_EDITOR_AUTO_SAVE_DELAY_MS
} from '../../../../shared/constants'
import { normalizeBrowserNavigationUrl } from '../../../../shared/browser-url'
import { clampNumber } from '@/lib/terminal-theme'
import {
GENERAL_BROWSER_SEARCH_ENTRIES,
GENERAL_CODEX_ACCOUNTS_SEARCH_ENTRIES,
GENERAL_CACHE_TIMER_SEARCH_ENTRIES,
GENERAL_CLI_SEARCH_ENTRIES,
@ -30,13 +27,6 @@ import {
GENERAL_UPDATE_SEARCH_ENTRIES,
GENERAL_WORKSPACE_SEARCH_ENTRIES
} from './general-search'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '../ui/dropdown-menu'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
import { SearchableSetting } from './SearchableSetting'
import { matchesSettingsSearch } from './settings-search'
@ -105,14 +95,6 @@ export function GeneralPane({ settings, updateSettings }: GeneralPaneProps): Rea
const searchQuery = useAppStore((s) => s.settingsSearchQuery)
const updateStatus = useAppStore((s) => s.updateStatus)
const fetchSettings = useAppStore((s) => s.fetchSettings)
const browserDefaultUrl = useAppStore((s) => s.browserDefaultUrl)
const setBrowserDefaultUrl = useAppStore((s) => s.setBrowserDefaultUrl)
const detectedBrowsers = useAppStore((s) => s.detectedBrowsers)
const browserSessionProfiles = useAppStore((s) => s.browserSessionProfiles)
const browserSessionImportState = useAppStore((s) => s.browserSessionImportState)
const defaultProfile = browserSessionProfiles.find((p) => p.id === 'default')
const orphanedProfiles = browserSessionProfiles.filter((p) => p.scope !== 'default')
const [homePageDraft, setHomePageDraft] = useState(browserDefaultUrl ?? '')
const [appVersion, setAppVersion] = useState<string | null>(null)
const [autoSaveDelayDraft, setAutoSaveDelayDraft] = useState(
String(settings.editorAutoSaveDelayMs)
@ -312,239 +294,6 @@ export function GeneralPane({ settings, updateSettings }: GeneralPaneProps): Rea
</SearchableSetting>
</section>
) : null,
matchesSettingsSearch(searchQuery, GENERAL_BROWSER_SEARCH_ENTRIES) ? (
<section key="browser" className="space-y-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold">Browser</h3>
<p className="text-xs text-muted-foreground">
Control how Orca handles links and browser workspace defaults.
</p>
</div>
<SearchableSetting
title="Default Home Page"
description="URL opened when creating a new browser tab. Leave empty to open a blank tab."
keywords={['browser', 'home', 'homepage', 'default', 'url', 'new tab', 'blank']}
className="flex items-start justify-between gap-4 px-1 py-2"
>
<div className="min-w-0 shrink space-y-0.5">
<Label>Default Home Page</Label>
<p className="text-xs text-muted-foreground">
URL opened when creating a new browser tab. Leave empty to open a blank tab.
</p>
</div>
<form
className="flex shrink-0 items-center gap-2"
onSubmit={(e) => {
e.preventDefault()
const trimmed = homePageDraft.trim()
if (!trimmed) {
setBrowserDefaultUrl(null)
return
}
const normalized = normalizeBrowserNavigationUrl(trimmed)
if (normalized && normalized !== ORCA_BROWSER_BLANK_URL) {
setBrowserDefaultUrl(normalized)
setHomePageDraft(normalized)
toast.success('Home page saved.')
}
}}
>
<Input
value={homePageDraft}
onChange={(e) => setHomePageDraft(e.target.value)}
placeholder="https://google.com"
spellCheck={false}
autoCapitalize="none"
autoCorrect="off"
className="h-7 w-52 text-xs"
/>
<Button type="submit" size="sm" variant="outline" className="h-7 text-xs">
Save
</Button>
</form>
</SearchableSetting>
<SearchableSetting
title="Terminal Link Routing"
description="Cmd/Ctrl+click opens terminal http(s) links in Orca. Shift+Cmd/Ctrl+click uses the system browser."
keywords={['browser', 'preview', 'links', 'localhost', 'webview']}
className="flex items-center justify-between gap-4 px-1 py-2"
>
<div className="space-y-0.5">
<Label>Terminal Link Routing</Label>
<p className="text-xs text-muted-foreground">
Cmd/Ctrl+click opens terminal links in Orca. Shift+Cmd/Ctrl+click opens the same link
in your system browser.
</p>
</div>
<button
role="switch"
aria-checked={settings.openLinksInApp}
onClick={() => updateSettings({ openLinksInApp: !settings.openLinksInApp })}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
settings.openLinksInApp ? 'bg-foreground' : 'bg-muted-foreground/30'
}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-background shadow-sm transition-transform ${
settings.openLinksInApp ? 'translate-x-4' : 'translate-x-0.5'
}`}
/>
</button>
</SearchableSetting>
<SearchableSetting
title="Session & Cookies"
description="Import cookies from Chrome, Edge, or other browsers to use existing logins inside Orca."
keywords={[
'cookies',
'session',
'import',
'auth',
'login',
'chrome',
'edge',
'arc',
'profile'
]}
className="space-y-3 px-1 py-2"
>
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<Label>Session &amp; Cookies</Label>
<p className="text-xs text-muted-foreground">
Import cookies from your system browser to reuse existing logins inside Orca.
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="xs"
className="shrink-0 gap-1.5"
disabled={browserSessionImportState?.status === 'importing'}
>
{browserSessionImportState?.status === 'importing' ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Import className="size-3" />
)}
Import Cookies
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{detectedBrowsers.map((browser) => (
<DropdownMenuItem
key={browser.family}
onSelect={async () => {
const store = useAppStore.getState()
const result = await store.importCookiesFromBrowser('default', browser.family)
if (result.ok) {
toast.success(
`Imported ${result.summary.importedCookies} cookies from ${browser.label}.`
)
} else {
toast.error(result.reason)
}
}}
>
From {browser.label}
</DropdownMenuItem>
))}
{detectedBrowsers.length > 0 && <DropdownMenuSeparator />}
<DropdownMenuItem
onSelect={async () => {
const store = useAppStore.getState()
const result = await store.importCookiesToProfile('default')
if (result.ok) {
toast.success(`Imported ${result.summary.importedCookies} cookies from file.`)
} else if (result.reason !== 'canceled') {
toast.error(result.reason)
}
}}
>
From File
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{defaultProfile?.source ? (
<div className="flex w-full items-center justify-between gap-3 rounded-md border border-border/70 px-3 py-2.5">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate text-sm font-medium">
Imported from {defaultProfile.source.browserFamily}
{defaultProfile.source.profileName
? ` (${defaultProfile.source.profileName})`
: ''}
</span>
{defaultProfile.source.importedAt ? (
<span className="truncate text-[11px] text-muted-foreground">
{new Date(defaultProfile.source.importedAt).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
})}
</span>
) : null}
</div>
<Button
variant="ghost"
size="xs"
className="gap-1 text-muted-foreground hover:text-destructive"
onClick={async () => {
const ok = await useAppStore.getState().clearDefaultSessionCookies()
if (ok) {
toast.success('Cookies cleared.')
}
}}
>
<Trash2 className="size-3" />
Clear
</Button>
</div>
) : null}
{orphanedProfiles.length > 0 ? (
<div className="space-y-2">
{orphanedProfiles.map((profile) => (
<div
key={profile.id}
className="flex w-full items-center justify-between gap-3 rounded-md border border-border/70 px-3 py-2.5"
>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate text-sm font-medium">{profile.label}</span>
<span className="truncate text-[11px] text-muted-foreground">
{profile.source
? `Imported from ${profile.source.browserFamily}${profile.source.profileName ? ` (${profile.source.profileName})` : ''}`
: 'Unused session'}
</span>
</div>
<Button
variant="ghost"
size="xs"
className="gap-1 text-muted-foreground hover:text-destructive"
onClick={async () => {
const ok = await useAppStore
.getState()
.deleteBrowserSessionProfile(profile.id)
if (ok) {
toast.success('Session removed.')
}
}}
>
<Trash2 className="size-3" />
Remove
</Button>
</div>
))}
</div>
) : null}
</SearchableSetting>
</section>
) : null,
matchesSettingsSearch(searchQuery, GENERAL_EDITOR_SEARCH_ENTRIES) ? (
<section key="editor" className="space-y-4">
<div className="space-y-1">

View file

@ -5,6 +5,7 @@ import {
Bell,
Bot,
GitBranch,
Globe,
Keyboard,
Palette,
Server,
@ -18,6 +19,7 @@ import { useSystemPrefersDark } from '@/components/terminal-pane/use-system-pref
import { isMacUserAgent, isWindowsUserAgent } from '@/components/terminal-pane/pane-helpers'
import { SCROLLBACK_PRESETS_MB, getFallbackTerminalFonts } from './SettingsConstants'
import { GeneralPane, GENERAL_PANE_SEARCH_ENTRIES } from './GeneralPane'
import { BrowserPane, BROWSER_PANE_SEARCH_ENTRIES } from './BrowserPane'
import { AppearancePane, APPEARANCE_PANE_SEARCH_ENTRIES } from './AppearancePane'
import { ShortcutsPane, SHORTCUTS_PANE_SEARCH_ENTRIES } from './ShortcutsPane'
import { TerminalPane } from './TerminalPane'
@ -34,6 +36,7 @@ import { matchesSettingsSearch, type SettingsSearchEntry } from './settings-sear
type SettingsNavTarget =
| 'general'
| 'browser'
| 'git'
| 'appearance'
| 'terminal'
@ -263,6 +266,13 @@ function Settings(): React.JSX.Element {
icon: SquareTerminal,
searchEntries: terminalPaneSearchEntries
},
{
id: 'browser',
title: 'Browser',
description: 'Home page, link routing, and session cookies.',
icon: Globe,
searchEntries: BROWSER_PANE_SEARCH_ENTRIES
},
{
id: 'notifications',
title: 'Notifications',
@ -487,6 +497,15 @@ function Settings(): React.JSX.Element {
/>
</SettingsSection>
<SettingsSection
id="browser"
title="Browser"
description="Home page, link routing, and session cookies."
searchEntries={BROWSER_PANE_SEARCH_ENTRIES}
>
<BrowserPane settings={settings} updateSettings={updateSettings} />
</SettingsSection>
<SettingsSection
id="notifications"
title="Notifications"

View file

@ -0,0 +1,32 @@
import type { SettingsSearchEntry } from './settings-search'
export const BROWSER_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
title: 'Default Home Page',
description: 'URL opened when creating a new browser tab. Leave empty to open a blank tab.',
keywords: ['browser', 'home', 'homepage', 'default', 'url', 'new tab', 'blank', 'landing']
},
{
title: 'Terminal Link Routing',
description:
'Cmd/Ctrl+click opens terminal http(s) links in Orca. Shift+Cmd/Ctrl+click uses the system browser.',
keywords: ['browser', 'preview', 'links', 'localhost', 'webview', 'shift', 'cmd', 'ctrl']
},
{
title: 'Session & Cookies',
description:
'Import cookies from Chrome, Edge, or other browsers to use existing logins inside Orca.',
keywords: [
'browser',
'cookies',
'session',
'import',
'auth',
'login',
'chrome',
'edge',
'arc',
'profile'
]
}
]

View file

@ -60,37 +60,6 @@ export const GENERAL_CACHE_TIMER_SEARCH_ENTRIES: SettingsSearchEntry[] = [
}
]
export const GENERAL_BROWSER_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
title: 'Default Home Page',
description: 'URL opened when creating a new browser tab. Leave empty to open a blank tab.',
keywords: ['browser', 'home', 'homepage', 'default', 'url', 'new tab', 'blank', 'landing']
},
{
title: 'Terminal Link Routing',
description:
'Cmd/Ctrl+click opens terminal http(s) links in Orca. Shift+Cmd/Ctrl+click uses the system browser.',
keywords: ['browser', 'preview', 'links', 'localhost', 'webview', 'shift', 'cmd', 'ctrl']
},
{
title: 'Session & Cookies',
description:
'Import cookies from Chrome, Edge, or other browsers to use existing logins inside Orca.',
keywords: [
'browser',
'cookies',
'session',
'import',
'auth',
'login',
'chrome',
'edge',
'arc',
'profile'
]
}
]
export const GENERAL_CODEX_ACCOUNTS_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
title: 'Codex Accounts',
@ -114,7 +83,6 @@ export const GENERAL_AGENT_SEARCH_ENTRIES: SettingsSearchEntry[] = [
export const GENERAL_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
...GENERAL_WORKSPACE_SEARCH_ENTRIES,
...GENERAL_BROWSER_SEARCH_ENTRIES,
...GENERAL_EDITOR_SEARCH_ENTRIES,
...GENERAL_CLI_SEARCH_ENTRIES,
...GENERAL_CACHE_TIMER_SEARCH_ENTRIES,

View file

@ -84,7 +84,7 @@ export type UISlice = {
openSettingsPage: () => void
closeSettingsPage: () => void
settingsNavigationTarget: {
pane: 'general' | 'appearance' | 'terminal' | 'shortcuts' | 'repo' | 'agents'
pane: 'general' | 'browser' | 'appearance' | 'terminal' | 'shortcuts' | 'repo' | 'agents'
repoId: string | null
sectionId?: string
} | null