diff --git a/src/renderer/components/settings/dialogs/OAuthPromptDialog.tsx b/src/renderer/components/settings/dialogs/OAuthPromptDialog.tsx new file mode 100644 index 00000000..7b7ec2d8 --- /dev/null +++ b/src/renderer/components/settings/dialogs/OAuthPromptDialog.tsx @@ -0,0 +1,167 @@ +import { useEffect, useState } from "react"; +import type { DesktopEvent } from "@shared/types"; +import { ActionButton, Modal } from "../../ui"; + +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 + * and replies via `submitOAuthPrompt` / `submitOAuthManualCode`. + */ +export function OAuthPromptDialog() { + const [pending, setPending] = useState(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(""); + } + }); + return unsubscribe; + }, []); + + const handleSubmit = async () => { + if (!desktop || !pending) return; + if (pending.kind === "prompt") { + const trimmed = value; + if (!pending.allowEmpty && trimmed.trim().length === 0) return; + await desktop.submitOAuthPrompt({ + connectorId: pending.connectorId, + requestId: pending.requestId, + value: trimmed, + }); + } else { + if (value.trim().length === 0) return; + await desktop.submitOAuthManualCode({ + connectorId: pending.connectorId, + requestId: pending.requestId, + code: value.trim(), + }); + } + setPending(null); + setValue(""); + }; + + const handleCancel = async () => { + if (!desktop || !pending) return; + await desktop.cancelOAuth({ connectorId: pending.connectorId }); + setPending(null); + setValue(""); + }; + + const isManualCode = pending?.kind === "manual-code"; + const title = isManualCode ? "Finish signing in" : "Provider needs more info"; + const submitDisabled = !pending + ? true + : value.trim().length === 0 && !(pending.kind === "prompt" && pending.allowEmpty); + + return ( + void handleCancel()} title={title} width="28rem"> + {pending ? ( + <> +
+ {pending.connectorId} +
+ + {isManualCode ? ( +
+
    +
  1. + 1 + + {pending.authUrl ? ( + <> + Open{" "} + + the authorization page + {" "} + and click Authorize. + + ) : ( + <>Complete authorization in the browser window that opened. + )} + +
  2. +
  3. + 2 + Copy the authorization code shown on the confirmation page. +
  4. +
  5. + 3 + Paste it below and press Submit. +
  6. +
+
+ ) : ( +

+ {(pending as { message: string }).message} +

+ )} + + setValue(event.target.value)} + placeholder={isManualCode ? "Authorization code" : pending.placeholder} + aria-label={isManualCode ? "Authorization code" : (pending as { message: string }).message} + onKeyDown={(event) => { + if (event.key === "Enter") void handleSubmit(); + }} + className="w-full rounded-md border border-border bg-background px-3 py-2 font-mono text-[13px] text-foreground outline-none ring-0 transition-colors placeholder:text-muted-foreground/50 focus:border-foreground/40" + autoComplete="off" + spellCheck={false} + /> + +
+ void handleCancel()} /> + void handleSubmit()} disabled={submitDisabled} /> +
+ + ) : null} +
+ ); +}