diff --git a/changelog.d/fixes/codex-manual-loopback-action.md b/changelog.d/fixes/codex-manual-loopback-action.md new file mode 100644 index 0000000000..98979c8ec6 --- /dev/null +++ b/changelog.d/fixes/codex-manual-loopback-action.md @@ -0,0 +1 @@ +- Add a visible manual callback-entry action to the Codex loopback warning so remote users can paste the authorization result instead of setting up an SSH tunnel. diff --git a/src/shared/components/OAuthModal.tsx b/src/shared/components/OAuthModal.tsx index bf6d47b57b..42c3884d65 100644 --- a/src/shared/components/OAuthModal.tsx +++ b/src/shared/components/OAuthModal.tsx @@ -374,11 +374,10 @@ export default function OAuthModal({ [provider, onSuccess, reauthConnection, t] ); - // Start OAuth flow. `opts.grokBrowser` lets the grok-cli method tabs force a - // specific branch synchronously (avoids reading a just-set state value through - // a stale closure); when omitted, falls back to the grokBrowserMode state. + // Start OAuth flow. Options let method buttons select a branch synchronously + // instead of reading a just-set state value through a stale closure. const startOAuthFlow = useCallback( - async (opts?: { grokBrowser?: boolean }) => { + async (opts?: { grokBrowser?: boolean; manualLoopback?: boolean }) => { if (!provider) return; try { setError(null); @@ -533,7 +532,7 @@ export default function OAuthModal({ setPolling(false); forceManual = true; } - } else if (isLocalhost) { + } else if (isLocalhost && !opts?.manualLoopback) { setLoopbackHint(buildPkceLoopbackMismatchHint(provider, loopbackLocation)); setStep("loopback-mismatch"); return; @@ -1160,6 +1159,7 @@ export default function OAuthModal({ void startOAuthFlow({ manualLoopback: true })} onClose={handleClose} /> )} diff --git a/src/shared/components/OAuthModalPanels.tsx b/src/shared/components/OAuthModalPanels.tsx index a691a20a39..c39a6d7941 100644 --- a/src/shared/components/OAuthModalPanels.tsx +++ b/src/shared/components/OAuthModalPanels.tsx @@ -89,6 +89,7 @@ export function OAuthDeviceCodePanel({ type OAuthLoopbackMismatchPanelProps = { providerName: string; hint: PkceLoopbackMismatchHint; + onManualInput: () => void; onClose: () => void; }; @@ -103,6 +104,7 @@ type OAuthLoopbackMismatchPanelProps = { export function OAuthLoopbackMismatchPanel({ providerName, hint, + onManualInput, onClose, }: OAuthLoopbackMismatchPanelProps) { const t = useTranslations("oauthModal"); @@ -194,6 +196,9 @@ export function OAuthLoopbackMismatchPanel({

{t("loopbackMismatchAlternative")}

+ diff --git a/tests/unit/oauth-modal-codex-lan-ip-8046.test.ts b/tests/unit/oauth-modal-codex-lan-ip-8046.test.ts index 6bc0cbc7f7..9add496077 100644 --- a/tests/unit/oauth-modal-codex-lan-ip-8046.test.ts +++ b/tests/unit/oauth-modal-codex-lan-ip-8046.test.ts @@ -41,7 +41,7 @@ test("PKCE callback-server providers now warn (not window.open) on a LAN-IP orig // that surfaces the loopback mismatch instead of falling through silently. // The follow-up swapped the flat warning string for the structured hint that feeds // the dedicated panel (buildPkceLoopbackMismatchHint) — the guard itself is unchanged. - const arm = modal.match(/else if \(isLocalhost\) \{[\s\S]{0,300}?\n {10}\}/); + const arm = modal.match(/else if \(isLocalhost(?: && [^)]+)?\) \{[\s\S]{0,300}?\n {10}\}/); assert.ok(arm, "expected an `else if (isLocalhost)` arm inside the callback-server branch"); assert.match( arm![0], @@ -57,6 +57,29 @@ test("PKCE callback-server providers now warn (not window.open) on a LAN-IP orig ); }); +test("loopback warning offers the existing manual callback flow", () => { + const panels = readFileSync( + resolve(here, "../../src/shared/components/OAuthModalPanels.tsx"), + "utf8" + ); + + assert.match( + modal, + /isLocalhost && !opts\?\.manualLoopback/, + "the warning guard must be bypassable only through an explicit manual-flow action" + ); + assert.match( + modal, + /onManualInput=\{\(\) => void startOAuthFlow\(\{ manualLoopback: true \}\)\}/, + "the mismatch panel action must initialize the existing authorization-code flow" + ); + assert.match( + panels, + /