Defer protected app data reads until user action (#1370)

This commit is contained in:
Neil 2026-05-02 18:15:52 -07:00 committed by GitHub
parent b309f49ee3
commit ae0fc9c49a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 52 additions and 26 deletions

View File

@ -175,18 +175,12 @@ describe('fetchGeminiRateLimits fallback oauth creds', () => {
expect(result.error).toContain('Gemini project ID not found')
})
it('returns unavailable when geminiCliOAuthEnabled=false and no google entry in auth.json', async () => {
readFileMock.mockImplementation(async (filePath: string) => {
if (filePath.includes('auth.json')) {
return JSON.stringify({ 'opencode-go': { type: 'api', key: 'k' } })
}
throw { code: 'ENOENT' }
})
it('returns unavailable without reading OAuth files when geminiCliOAuthEnabled=false', async () => {
const result = await fetchGeminiRateLimits(false)
expect(result.status).toBe('unavailable')
expect(result.error).toContain('disabled')
expect(readFileMock).not.toHaveBeenCalled()
// No network calls should have been made.
expect(netFetchMock).not.toHaveBeenCalled()
})

View File

@ -203,22 +203,25 @@ async function fetchViaOauthCreds(
export async function fetchGeminiRateLimits(
geminiCliOAuthEnabled = false
): Promise<ProviderRateLimits> {
if (!geminiCliOAuthEnabled) {
// Why: the OAuth sources include other apps' data folders on macOS.
// Do not touch them during background polling unless the user opts in.
return {
provider: 'gemini',
session: null,
weekly: null,
updatedAt: Date.now(),
error: 'Gemini CLI OAuth is disabled in settings',
status: 'unavailable'
}
}
try {
const authJson = await readAuthJson()
const result =
authJson?.google?.type === 'oauth'
? await fetchViaAuthJson(authJson.google, geminiCliOAuthEnabled)
: await (async () => {
if (!geminiCliOAuthEnabled) {
return {
provider: 'gemini',
session: null,
weekly: null,
updatedAt: Date.now(),
error: 'Gemini CLI OAuth is disabled in settings',
status: 'unavailable'
} as ProviderRateLimits
}
const creds = await readGeminiCredentials()
return !creds
? ({

View File

@ -96,7 +96,6 @@ function App(): React.JSX.Element {
hydrateEditorSession: s.hydrateEditorSession,
hydrateBrowserSession: s.hydrateBrowserSession,
fetchBrowserSessionProfiles: s.fetchBrowserSessionProfiles,
fetchDetectedBrowsers: s.fetchDetectedBrowsers,
reconnectPersistedTerminals: s.reconnectPersistedTerminals,
setDeferredSshReconnectTargets: s.setDeferredSshReconnectTargets,
setSshConnectionState: s.setSshConnectionState,
@ -233,7 +232,6 @@ function App(): React.JSX.Element {
actions.hydrateEditorSession(session)
actions.hydrateBrowserSession(session)
await actions.fetchBrowserSessionProfiles()
await actions.fetchDetectedBrowsers()
// Why: SSH connections must be re-established BEFORE terminal
// reconnect so that reconnectPersistedTerminals can route SSH-backed

View File

@ -42,6 +42,7 @@ export function BrowserToolbarMenu({
const createBrowserSessionProfile = useAppStore((s) => s.createBrowserSessionProfile)
const importCookiesFromBrowser = useAppStore((s) => s.importCookiesFromBrowser)
const importCookiesToProfile = useAppStore((s) => s.importCookiesToProfile)
const fetchDetectedBrowsers = useAppStore((s) => s.fetchDetectedBrowsers)
const browserSessionImportState = useAppStore((s) => s.browserSessionImportState)
const [newProfileDialogOpen, setNewProfileDialogOpen] = useState(false)
@ -171,7 +172,15 @@ export function BrowserToolbarMenu({
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSub
onOpenChange={(open) => {
if (open) {
// Why: macOS treats other browsers' profile folders as app
// data. Only probe them when the user opens the import menu.
void fetchDetectedBrowsers()
}
}}
>
<DropdownMenuSubTrigger disabled={browserSessionImportState?.status === 'importing'}>
<Import className="mr-2 size-3.5" />
Import Cookies

View File

@ -46,6 +46,7 @@ export function BrowserProfileRow({
isDefault
}: BrowserProfileRowProps): React.JSX.Element {
const isImporting = importState?.profileId === profile.id && importState.status === 'importing'
const fetchDetectedBrowsers = useAppStore((s) => s.fetchDetectedBrowsers)
const handleImportFromBrowser = async (
browserFamily: string,
@ -115,7 +116,15 @@ export function BrowserProfileRow({
)}
</div>
<div className="flex shrink-0 items-center gap-1" onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenu
onOpenChange={(open) => {
if (open) {
// Why: macOS treats other browsers' profile folders as app
// data. Only probe them when the user opens the import menu.
void fetchDetectedBrowsers()
}
}}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"

View File

@ -77,8 +77,7 @@ export function BrowserUseSetup({
}
void refreshCli()
void fetchBrowserSessionProfiles()
void fetchDetectedBrowsers()
}, [browserUseEnabled, fetchBrowserSessionProfiles, fetchDetectedBrowsers])
}, [browserUseEnabled, fetchBrowserSessionProfiles])
const defaultProfile = browserSessionProfiles.find((p) => p.id === 'default')
// Why: this step explicitly imports into the default profile, so completion
@ -325,7 +324,15 @@ export function BrowserUseSetup({
</button>
) : null}
</div>
<DropdownMenu>
<DropdownMenu
onOpenChange={(open) => {
if (open) {
// Why: macOS treats other browsers' profile folders as app
// data. Only probe them when the user opens the import menu.
void fetchDetectedBrowsers()
}
}}
>
<DropdownMenuTrigger asChild>
<Button
variant={cookiesImported ? 'outline' : 'default'}

View File

@ -104,6 +104,7 @@ export type BrowserSlice = {
profiles: { name: string; directory: string }[]
selectedProfile: string
}[]
detectedBrowsersLoaded: boolean
fetchDetectedBrowsers: () => Promise<void>
importCookiesFromBrowser: (
profileId: string,
@ -1283,8 +1284,12 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
},
detectedBrowsers: [],
detectedBrowsersLoaded: false,
fetchDetectedBrowsers: async () => {
if (get().detectedBrowsersLoaded) {
return
}
try {
const browsers = (await window.api.browser.sessionDetectBrowsers()) as {
family: string
@ -1292,9 +1297,10 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
profiles: { name: string; directory: string }[]
selectedProfile: string
}[]
set({ detectedBrowsers: browsers })
set({ detectedBrowsers: browsers, detectedBrowsersLoaded: true })
} catch {
/* best-effort — empty list is acceptable fallback */
set({ detectedBrowsersLoaded: true })
}
},