Merge pull request #59778 from frizikk/fix/desktop-sudo-dialog-dismiss-59765

fix(desktop): dismiss stale prompt overlays
This commit is contained in:
brooklyn! 2026-07-12 05:52:16 -05:00 committed by GitHub
commit 2d9fd870b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 100 additions and 1 deletions

View file

@ -0,0 +1,67 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { I18nProvider } from '@/i18n'
import { $gateway } from '@/store/gateway'
import { notifyError } from '@/store/notifications'
import { $secretRequest, $sudoRequest, clearAllPrompts, setSecretRequest, setSudoRequest } from '@/store/prompts'
import { $activeSessionId } from '@/store/session'
import { PromptOverlays } from './prompt-overlays'
vi.mock('@/lib/haptics', () => ({ triggerHaptic: vi.fn() }))
vi.mock('@/store/notifications', () => ({ notifyError: vi.fn() }))
function renderPrompts() {
render(
<I18nProvider configClient={null}>
<PromptOverlays />
</I18nProvider>
)
}
afterEach(() => {
cleanup()
clearAllPrompts()
$activeSessionId.set(null)
$gateway.set(null)
vi.clearAllMocks()
})
describe('PromptOverlays', () => {
it('dismisses a stale sudo dialog when the gateway no longer has the password request', async () => {
const request = vi.fn().mockRejectedValue(new Error('no pending password request'))
$activeSessionId.set('s1')
$gateway.set({ request } as never)
setSudoRequest({ requestId: 'sudo-1', sessionId: 's1' })
renderPrompts()
expect(screen.getByText('Administrator password')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
await waitFor(() => expect($sudoRequest.get()).toBeNull())
expect(request).toHaveBeenCalledWith('sudo.respond', { password: '', request_id: 'sudo-1' })
expect(notifyError).not.toHaveBeenCalled()
})
it('dismisses a stale secret dialog when the gateway no longer has the value request', async () => {
const request = vi.fn().mockRejectedValue(new Error('no pending value request'))
$activeSessionId.set('s1')
$gateway.set({ request } as never)
setSecretRequest({ envVar: 'TEST_SECRET', prompt: 'Paste a secret', requestId: 'secret-1', sessionId: 's1' })
renderPrompts()
expect(screen.getByText('TEST_SECRET')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
await waitFor(() => expect($secretRequest.get()).toBeNull())
expect(request).toHaveBeenCalledWith('secret.respond', { request_id: 'secret-1', value: '' })
expect(notifyError).not.toHaveBeenCalled()
})
})

View file

@ -15,6 +15,7 @@ import {
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { useI18n } from '@/i18n'
import { isMissingPendingPromptRequest } from '@/lib/gateway-rpc'
import { triggerHaptic } from '@/lib/haptics'
import { KeyRound, Loader2, Lock } from '@/lib/icons'
import { $gateway } from '@/store/gateway'
@ -69,6 +70,12 @@ function SudoDialog() {
triggerHaptic('submit')
clearSudoRequest(request.sessionId, request.requestId)
} catch (error) {
if (isMissingPendingPromptRequest(error, 'password')) {
clearSudoRequest(request.sessionId, request.requestId)
return
}
notifyError(error, copy.sudoSendFailed)
setSubmitting(false)
}
@ -165,6 +172,12 @@ function SecretDialog() {
triggerHaptic('submit')
clearSecretRequest(request.sessionId, request.requestId)
} catch (error) {
if (isMissingPendingPromptRequest(error, 'value')) {
clearSecretRequest(request.sessionId, request.requestId)
return
}
notifyError(error, copy.secretSendFailed)
setSubmitting(false)
}

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { isMissingRpcMethod } from './gateway-rpc'
import { isMissingPendingPromptRequest, isMissingRpcMethod } from './gateway-rpc'
describe('isMissingRpcMethod', () => {
it('detects JSON-RPC method-not-found errors', () => {
@ -14,3 +14,15 @@ describe('isMissingRpcMethod', () => {
expect(isMissingRpcMethod(new Error('no such project'))).toBe(false)
})
})
describe('isMissingPendingPromptRequest', () => {
it('detects stale prompt response errors from the gateway', () => {
expect(isMissingPendingPromptRequest(new Error('no pending password request'), 'password')).toBe(true)
expect(isMissingPendingPromptRequest(new Error('RPC failed: no pending value request'), 'value')).toBe(true)
})
it('ignores unrelated gateway failures', () => {
expect(isMissingPendingPromptRequest(new Error('gateway not connected'), 'password')).toBe(false)
expect(isMissingPendingPromptRequest(new Error('no pending value request'), 'password')).toBe(false)
})
})

View file

@ -4,3 +4,10 @@ export function isMissingRpcMethod(error: unknown): boolean {
return /method not found|-32601|unknown method|no such method/i.test(message)
}
/** True when a prompt response raced a backend-side timeout / completion. */
export function isMissingPendingPromptRequest(error: unknown, key: string): boolean {
const message = error instanceof Error ? error.message : String(error)
return message.toLowerCase().includes(`no pending ${key.toLowerCase()} request`)
}