From c1349fceabf995c2aa85ea635ddbb0514773a4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 22 Apr 2026 20:02:28 -0400 Subject: [PATCH] Fix OAuth prompt dismissal on terminal status --- .../settings/dialogs/OAuthPromptDialog.tsx | 49 +++----------- .../settings/dialogs/oauth-prompt-events.ts | 52 +++++++++++++++ tests/unit/oauth-prompt-events.test.ts | 64 +++++++++++++++++++ 3 files changed, 126 insertions(+), 39 deletions(-) create mode 100644 src/renderer/components/settings/dialogs/oauth-prompt-events.ts create mode 100644 tests/unit/oauth-prompt-events.test.ts diff --git a/src/renderer/components/settings/dialogs/OAuthPromptDialog.tsx b/src/renderer/components/settings/dialogs/OAuthPromptDialog.tsx index 6065e4c1..b55869e1 100644 --- a/src/renderer/components/settings/dialogs/OAuthPromptDialog.tsx +++ b/src/renderer/components/settings/dialogs/OAuthPromptDialog.tsx @@ -1,28 +1,12 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import type { DesktopEvent } from "@shared/types"; import { ActionButton, Modal } from "../../ui"; import { useT } from "../../../i18n"; +import { nextOAuthPromptState, type PendingOAuthPrompt } from "./oauth-prompt-events"; const desktop = (typeof window !== "undefined" ? window.agentCompany : null) as | (Window["agentCompany"] | null); -type PendingPrompt = - | { - kind: "prompt"; - connectorId: string; - requestId: string; - message: string; - placeholder?: string; - allowEmpty?: boolean; - } - | { - kind: "manual-code"; - connectorId: string; - requestId: string; - authUrl?: string; - instructions?: string; - }; - /** * Listens for `oauth:prompt-request` and `oauth:manual-code-request` push * events and renders a single modal at a time. The modal collects the input @@ -30,32 +14,17 @@ type PendingPrompt = */ export function OAuthPromptDialog() { const t = useT(); - const [pending, setPending] = useState(null); + const [pending, setPending] = useState(null); + const pendingRef = useRef(null); const [value, setValue] = useState(""); useEffect(() => { if (!desktop) return; const unsubscribe = desktop.subscribe((event: DesktopEvent) => { - if (event.type === "oauth:prompt-request") { - setPending({ - kind: "prompt", - connectorId: event.connectorId, - requestId: event.requestId, - message: event.message, - placeholder: event.placeholder, - allowEmpty: event.allowEmpty, - }); - setValue(""); - } else if (event.type === "oauth:manual-code-request") { - setPending({ - kind: "manual-code", - connectorId: event.connectorId, - requestId: event.requestId, - authUrl: event.authUrl, - instructions: event.instructions, - }); - setValue(""); - } + const next = nextOAuthPromptState(pendingRef.current, event); + if (next !== pendingRef.current) setValue(""); + pendingRef.current = next; + setPending(next); }); return unsubscribe; }, []); @@ -78,6 +47,7 @@ export function OAuthPromptDialog() { code: value.trim(), }); } + pendingRef.current = null; setPending(null); setValue(""); }; @@ -85,6 +55,7 @@ export function OAuthPromptDialog() { const handleCancel = async () => { if (!desktop || !pending) return; await desktop.cancelOAuth({ connectorId: pending.connectorId }); + pendingRef.current = null; setPending(null); setValue(""); }; diff --git a/src/renderer/components/settings/dialogs/oauth-prompt-events.ts b/src/renderer/components/settings/dialogs/oauth-prompt-events.ts new file mode 100644 index 00000000..bfcded84 --- /dev/null +++ b/src/renderer/components/settings/dialogs/oauth-prompt-events.ts @@ -0,0 +1,52 @@ +import type { DesktopEvent } from "@shared/types"; + +export type PendingOAuthPrompt = + | { + kind: "prompt"; + connectorId: string; + requestId: string; + message: string; + placeholder?: string; + allowEmpty?: boolean; + } + | { + kind: "manual-code"; + connectorId: string; + requestId: string; + authUrl?: string; + instructions?: string; + }; + +export function nextOAuthPromptState( + current: PendingOAuthPrompt | null, + event: DesktopEvent, +): PendingOAuthPrompt | null { + if (event.type === "oauth:prompt-request") { + return { + kind: "prompt", + connectorId: event.connectorId, + requestId: event.requestId, + message: event.message, + placeholder: event.placeholder, + allowEmpty: event.allowEmpty, + }; + } + + if (event.type === "oauth:manual-code-request") { + return { + kind: "manual-code", + connectorId: event.connectorId, + requestId: event.requestId, + authUrl: event.authUrl, + instructions: event.instructions, + }; + } + + if (event.type === "oauth:status" && current?.connectorId === event.connectorId) { + return event.status === "in_progress" || event.status === "pending" + ? current + : null; + } + + return current; +} diff --git a/tests/unit/oauth-prompt-events.test.ts b/tests/unit/oauth-prompt-events.test.ts new file mode 100644 index 00000000..493ed25c --- /dev/null +++ b/tests/unit/oauth-prompt-events.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "vitest"; +import type { DesktopEvent } from "@shared/types"; +import { + nextOAuthPromptState, + type PendingOAuthPrompt, +} from "../../src/renderer/components/settings/dialogs/oauth-prompt-events"; + +describe("OAuth prompt event state", () => { + const pendingManualCode: PendingOAuthPrompt = { + kind: "manual-code", + connectorId: "openai-codex", + requestId: "request-1", + authUrl: "https://example.com/oauth", + instructions: "Authorize", + }; + + it("closes a stale manual-code dialog when the same connector becomes active", () => { + const event: DesktopEvent = { + type: "oauth:status", + connectorId: "openai-codex", + status: "active", + }; + + expect(nextOAuthPromptState(pendingManualCode, event)).toBeNull(); + }); + + it("keeps the dialog open for in-progress status events", () => { + const event: DesktopEvent = { + type: "oauth:status", + connectorId: "openai-codex", + status: "in_progress", + }; + + expect(nextOAuthPromptState(pendingManualCode, event)).toEqual(pendingManualCode); + }); + + it("ignores terminal status events for a different connector", () => { + const event: DesktopEvent = { + type: "oauth:status", + connectorId: "claude-pro", + status: "active", + }; + + expect(nextOAuthPromptState(pendingManualCode, event)).toEqual(pendingManualCode); + }); + + it("opens manual-code requests from the event payload", () => { + const event: DesktopEvent = { + type: "oauth:manual-code-request", + connectorId: "openai-codex", + requestId: "request-2", + authUrl: "https://example.com/code", + instructions: "Copy code", + }; + + expect(nextOAuthPromptState(null, event)).toEqual({ + kind: "manual-code", + connectorId: "openai-codex", + requestId: "request-2", + authUrl: "https://example.com/code", + instructions: "Copy code", + }); + }); +});