mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
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:
parent
949f3e396a
commit
c655c13629
7 changed files with 338 additions and 460 deletions
|
|
@ -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 & 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()
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
283
src/renderer/src/components/settings/BrowserPane.tsx
Normal file
283
src/renderer/src/components/settings/BrowserPane.tsx
Normal 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 & 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 & 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">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
32
src/renderer/src/components/settings/browser-search.ts
Normal file
32
src/renderer/src/components/settings/browser-search.ts
Normal 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'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue