diff --git a/src/renderer/src/components/settings/BrowserPane.tsx b/src/renderer/src/components/settings/BrowserPane.tsx index 8f5da6e5103..4019b192c06 100644 --- a/src/renderer/src/components/settings/BrowserPane.tsx +++ b/src/renderer/src/components/settings/BrowserPane.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import { Plus } from 'lucide-react' import { toast } from 'sonner' import type { GlobalSettings } from '../../../../shared/types' @@ -24,6 +24,29 @@ import { BrowserUseSetup } from './BrowserUsePane' import { KagiSessionLinkForm } from './KagiSessionLinkForm' export { BROWSER_PANE_SEARCH_ENTRIES } +type HomePageDraftState = { + sourceUrl: string + draft: string +} + +function createHomePageDraftState( + browserDefaultUrl: string | null | undefined +): HomePageDraftState { + const sourceUrl = browserDefaultUrl ?? '' + return { + sourceUrl, + draft: sourceUrl + } +} + +function resolveHomePageDraftState( + state: HomePageDraftState, + browserDefaultUrl: string | null | undefined +): HomePageDraftState { + const sourceUrl = browserDefaultUrl ?? '' + return state.sourceUrl === sourceUrl ? state : { sourceUrl, draft: sourceUrl } +} + type BrowserPaneProps = { settings: GlobalSettings updateSettings: (updates: Partial) => void @@ -47,17 +70,29 @@ export function BrowserPane({ const setDefaultBrowserSessionProfileId = useAppStore((s) => s.setDefaultBrowserSessionProfileId) const defaultProfile = browserSessionProfiles.find((p) => p.id === 'default') const nonDefaultProfiles = browserSessionProfiles.filter((p) => p.scope !== 'default') - const [homePageDraft, setHomePageDraft] = useState(browserDefaultUrl ?? '') + const [homePageDraftState, setHomePageDraftState] = useState(() => + createHomePageDraftState(browserDefaultUrl) + ) const [newProfileDialogOpen, setNewProfileDialogOpen] = useState(false) const [newProfileName, setNewProfileName] = useState('') const [isCreatingProfile, setIsCreatingProfile] = useState(false) - // 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 resolvedHomePageDraftState = resolveHomePageDraftState( + homePageDraftState, + browserDefaultUrl + ) + if (resolvedHomePageDraftState !== homePageDraftState) { + // Why: the address bar can save a new home page outside Settings; reconcile + // the draft before paint so the field never flashes stale text. + setHomePageDraftState(resolvedHomePageDraftState) + } + const homePageDraft = resolvedHomePageDraftState.draft + const updateHomePageDraft = (draft: string): void => { + setHomePageDraftState((current) => ({ + ...resolveHomePageDraftState(current, browserDefaultUrl), + draft + })) + } const selectedSearchEngine = browserDefaultSearchEngine ?? 'google' @@ -116,19 +151,20 @@ export function BrowserPane({ const trimmed = homePageDraft.trim() if (!trimmed) { setBrowserDefaultUrl(null) + setHomePageDraftState(createHomePageDraftState(null)) return } const normalized = normalizeBrowserNavigationUrl(trimmed) if (normalized && normalized !== ORCA_BROWSER_BLANK_URL) { setBrowserDefaultUrl(normalized) - setHomePageDraft(normalized) + setHomePageDraftState(createHomePageDraftState(normalized)) toast.success('Home page saved.') } }} > setHomePageDraft(e.target.value)} + onChange={(e) => updateHomePageDraft(e.target.value)} placeholder="https://google.com" spellCheck={false} autoCapitalize="none" diff --git a/src/renderer/src/components/settings/KagiSessionLinkForm.tsx b/src/renderer/src/components/settings/KagiSessionLinkForm.tsx index 02d53615770..4c241035aa4 100644 --- a/src/renderer/src/components/settings/KagiSessionLinkForm.tsx +++ b/src/renderer/src/components/settings/KagiSessionLinkForm.tsx @@ -1,26 +1,59 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import { toast } from 'sonner' import { normalizeKagiSessionLink } from '../../../../shared/browser-url' import { useAppStore } from '../../store' import { Button } from '../ui/button' import { Input } from '../ui/input' +type KagiSessionLinkDraftState = { + sourceLink: string + draft: string +} + +function createKagiSessionLinkDraftState( + browserKagiSessionLink: string | null | undefined +): KagiSessionLinkDraftState { + const sourceLink = browserKagiSessionLink ?? '' + return { + sourceLink, + draft: sourceLink + } +} + +function resolveKagiSessionLinkDraftState( + state: KagiSessionLinkDraftState, + browserKagiSessionLink: string | null | undefined +): KagiSessionLinkDraftState { + const sourceLink = browserKagiSessionLink ?? '' + return state.sourceLink === sourceLink ? state : { sourceLink, draft: sourceLink } +} + export function KagiSessionLinkForm(): React.JSX.Element { const browserKagiSessionLink = useAppStore((s) => s.browserKagiSessionLink) const setBrowserKagiSessionLink = useAppStore((s) => s.setBrowserKagiSessionLink) - const [draft, setDraft] = useState(browserKagiSessionLink ?? '') + const [draftState, setDraftState] = useState(() => + createKagiSessionLinkDraftState(browserKagiSessionLink) + ) - // Why: the Kagi token is edited as a masked draft so accidental typing or - // external settings updates do not immediately overwrite the persisted secret. - useEffect(() => { - setDraft(browserKagiSessionLink ?? '') - }, [browserKagiSessionLink]) + const resolvedDraftState = resolveKagiSessionLinkDraftState(draftState, browserKagiSessionLink) + if (resolvedDraftState !== draftState) { + // Why: the masked draft tracks persisted secret updates without committing + // accidental typing back to settings or waiting for an after-paint Effect. + setDraftState(resolvedDraftState) + } + const draft = resolvedDraftState.draft + const updateDraft = (nextDraft: string): void => { + setDraftState((current) => ({ + ...resolveKagiSessionLinkDraftState(current, browserKagiSessionLink), + draft: nextDraft + })) + } const save = (): void => { const trimmed = draft.trim() if (!trimmed) { setBrowserKagiSessionLink(null) - setDraft('') + setDraftState(createKagiSessionLinkDraftState(null)) toast.success('Kagi session link cleared.') return } @@ -30,7 +63,7 @@ export function KagiSessionLinkForm(): React.JSX.Element { return } setBrowserKagiSessionLink(normalized) - setDraft(normalized) + setDraftState(createKagiSessionLinkDraftState(normalized)) toast.success('Kagi session link saved.') } @@ -49,7 +82,7 @@ export function KagiSessionLinkForm(): React.JSX.Element { setDraft(e.target.value)} + onChange={(e) => updateDraft(e.target.value)} placeholder="https://kagi.com/search?token=..." spellCheck={false} autoCapitalize="none" @@ -69,7 +102,7 @@ export function KagiSessionLinkForm(): React.JSX.Element { className="h-7 text-xs" onClick={() => { setBrowserKagiSessionLink(null) - setDraft('') + setDraftState(createKagiSessionLinkDraftState(null)) toast.success('Kagi session link cleared.') }} >