Skip to content

Commit 1bcd816

Browse files
committed
Improve Codex OAuth reconnect flow
1 parent e98a4de commit 1bcd816

File tree

7 files changed

+277
-155
lines changed

7 files changed

+277
-155
lines changed

lib/public/js/components/models-tab/provider-auth-card.js

Lines changed: 60 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { Badge } from "../badge.js";
55
import { SecretInput } from "../secret-input.js";
66
import { ActionButton } from "../action-button.js";
77
import { exchangeCodexOAuth, disconnectCodex } from "../../lib/api.js";
8+
import {
9+
isCodexAuthCallbackMessage,
10+
openCodexAuthWindow,
11+
} from "../../lib/codex-oauth-window.js";
812
import { showToast } from "../toast.js";
913
import {
1014
kProviderAuthFields,
@@ -108,6 +112,7 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
108112
const [authWaiting, setAuthWaiting] = useState(false);
109113
const [manualInput, setManualInput] = useState("");
110114
const [exchanging, setExchanging] = useState(false);
115+
const exchangeInFlightRef = useRef(false);
111116
const popupPollRef = useRef(null);
112117

113118
useEffect(
@@ -117,13 +122,39 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
117122
[],
118123
);
119124

125+
const submitAuthInput = async (input) => {
126+
const normalizedInput = String(input || "").trim();
127+
if (!normalizedInput || exchangeInFlightRef.current) return;
128+
exchangeInFlightRef.current = true;
129+
setManualInput(normalizedInput);
130+
setExchanging(true);
131+
try {
132+
const result = await exchangeCodexOAuth(normalizedInput);
133+
if (!result.ok)
134+
throw new Error(result.error || "Codex OAuth exchange failed");
135+
setManualInput("");
136+
showToast("Codex connected", "success");
137+
setAuthStarted(false);
138+
setAuthWaiting(false);
139+
await onRefreshCodex();
140+
} catch (err) {
141+
setAuthWaiting(false);
142+
showToast(err.message || "Codex OAuth exchange failed", "error");
143+
} finally {
144+
exchangeInFlightRef.current = false;
145+
setExchanging(false);
146+
}
147+
};
148+
120149
useEffect(() => {
121150
const onMessage = async (e) => {
122151
if (e.data?.codex === "success") {
123152
showToast("Codex connected", "success");
124153
setAuthStarted(false);
125154
setAuthWaiting(false);
126155
await onRefreshCodex();
156+
} else if (isCodexAuthCallbackMessage(e.data)) {
157+
await submitAuthInput(e.data.input);
127158
} else if (e.data?.codex === "error") {
128159
showToast(
129160
`Codex auth failed: ${e.data.message || "unknown error"}`,
@@ -133,19 +164,14 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
133164
};
134165
window.addEventListener("message", onMessage);
135166
return () => window.removeEventListener("message", onMessage);
136-
}, [onRefreshCodex]);
167+
}, [onRefreshCodex, submitAuthInput]);
137168

138169
const startAuth = () => {
139170
setAuthStarted(true);
140171
setAuthWaiting(true);
141-
const popup = window.open(
142-
"/auth/codex/start",
143-
"codex-auth",
144-
"popup=yes,width=640,height=780",
145-
);
172+
const popup = openCodexAuthWindow();
146173
if (!popup || popup.closed) {
147174
setAuthWaiting(false);
148-
window.location.href = "/auth/codex/start";
149175
return;
150176
}
151177
if (popupPollRef.current) clearInterval(popupPollRef.current);
@@ -159,22 +185,7 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
159185
};
160186

161187
const completeAuth = async () => {
162-
if (!manualInput.trim() || exchanging) return;
163-
setExchanging(true);
164-
try {
165-
const result = await exchangeCodexOAuth(manualInput.trim());
166-
if (!result.ok)
167-
throw new Error(result.error || "Codex OAuth exchange failed");
168-
setManualInput("");
169-
showToast("Codex connected", "success");
170-
setAuthStarted(false);
171-
setAuthWaiting(false);
172-
await onRefreshCodex();
173-
} catch (err) {
174-
showToast(err.message || "Codex OAuth exchange failed", "error");
175-
} finally {
176-
setExchanging(false);
177-
}
188+
await submitAuthInput(manualInput);
178189
};
179190

180191
const handleDisconnect = async () => {
@@ -198,7 +209,23 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
198209
? html`<${Badge} tone="success">Connected</${Badge}>`
199210
: html`<${Badge} tone="warning">Not connected</${Badge}>`}
200211
</div>
201-
${codexStatus.connected
212+
${authStarted
213+
? html`
214+
<div class="flex items-center justify-between gap-2">
215+
<p class="text-xs text-fg-muted">
216+
${authWaiting
217+
? "Complete login in the popup. AlphaClaw should finish automatically, but you can paste the redirect URL below if it doesn't."
218+
: "Paste the redirect URL from your browser to finish connecting."}
219+
</p>
220+
<button
221+
onclick=${startAuth}
222+
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary shrink-0"
223+
>
224+
Restart
225+
</button>
226+
</div>
227+
`
228+
: codexStatus.connected
202229
? html`
203230
<div class="flex gap-2">
204231
<button
@@ -215,32 +242,16 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
215242
</button>
216243
</div>
217244
`
218-
: !authStarted
245+
: html`
246+
<button
247+
onclick=${startAuth}
248+
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
249+
>
250+
Connect Codex OAuth
251+
</button>
252+
`}
253+
${authStarted
219254
? html`
220-
<button
221-
onclick=${startAuth}
222-
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
223-
>
224-
Connect Codex OAuth
225-
</button>
226-
`
227-
: html`
228-
<div class="flex items-center justify-between gap-2">
229-
<p class="text-xs text-fg-muted">
230-
${authWaiting
231-
? "Complete login in the popup, then paste the redirect URL."
232-
: "Paste the redirect URL from your browser to finish connecting."}
233-
</p>
234-
<button
235-
onclick=${startAuth}
236-
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary shrink-0"
237-
>
238-
Restart
239-
</button>
240-
</div>
241-
`}
242-
${!codexStatus.connected && authStarted
243-
? html`
244255
<p class="text-xs text-fg-muted">
245256
After login, copy the full redirect URL (starts with
246257
<code class="text-xs bg-field px-1 rounded"

lib/public/js/components/models.js

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import {
2424
kProviderLabels,
2525
kProviderOrder,
2626
} from "../lib/model-config.js";
27+
import {
28+
isCodexAuthCallbackMessage,
29+
openCodexAuthWindow,
30+
} from "../lib/codex-oauth-window.js";
2731

2832
const html = htm.bind(h);
2933

@@ -51,6 +55,7 @@ export const Models = () => {
5155
const [savedModel, setSavedModel] = useState(() => kModelsTabCache?.savedModel || "");
5256
const [modelDirty, setModelDirty] = useState(false);
5357
const [savedAiValues, setSavedAiValues] = useState(() => kModelsTabCache?.savedAiValues || {});
58+
const codexExchangeInFlightRef = useRef(false);
5459
const codexPopupPollRef = useRef(null);
5560

5661
const refresh = async () => {
@@ -122,18 +127,43 @@ export const Models = () => {
122127
}
123128
}, []);
124129

130+
const submitCodexAuthInput = async (input) => {
131+
const normalizedInput = String(input || "").trim();
132+
if (!normalizedInput || codexExchangeInFlightRef.current) return;
133+
codexExchangeInFlightRef.current = true;
134+
setCodexManualInput(normalizedInput);
135+
setCodexExchanging(true);
136+
try {
137+
const result = await exchangeCodexOAuth(normalizedInput);
138+
if (!result.ok) throw new Error(result.error || "Codex OAuth exchange failed");
139+
setCodexManualInput("");
140+
showToast("Codex connected", "success");
141+
setCodexAuthStarted(false);
142+
setCodexAuthWaiting(false);
143+
await refreshCodexConnection();
144+
} catch (err) {
145+
setCodexAuthWaiting(false);
146+
showToast(err.message || "Codex OAuth exchange failed", "error");
147+
} finally {
148+
codexExchangeInFlightRef.current = false;
149+
setCodexExchanging(false);
150+
}
151+
};
152+
125153
useEffect(() => {
126154
const onMessage = async (e) => {
127155
if (e.data?.codex === "success") {
128156
showToast("Codex connected", "success");
129157
await refreshCodexConnection();
158+
} else if (isCodexAuthCallbackMessage(e.data)) {
159+
await submitCodexAuthInput(e.data.input);
130160
} else if (e.data?.codex === "error") {
131161
showToast(`Codex auth failed: ${e.data.message || "unknown error"}`, "error");
132162
}
133163
};
134164
window.addEventListener("message", onMessage);
135165
return () => window.removeEventListener("message", onMessage);
136-
}, []);
166+
}, [submitCodexAuthInput]);
137167

138168
const setEnvValue = (key, value) => {
139169
setEnvVars((prev) => {
@@ -194,10 +224,9 @@ export const Models = () => {
194224
if (codexStatus.connected) return;
195225
setCodexAuthStarted(true);
196226
setCodexAuthWaiting(true);
197-
const popup = window.open("/auth/codex/start", "codex-auth", "popup=yes,width=640,height=780");
227+
const popup = openCodexAuthWindow();
198228
if (!popup || popup.closed) {
199229
setCodexAuthWaiting(false);
200-
window.location.href = "/auth/codex/start";
201230
return;
202231
}
203232
if (codexPopupPollRef.current) {
@@ -213,21 +242,7 @@ export const Models = () => {
213242
};
214243

215244
const completeCodexAuth = async () => {
216-
if (!codexManualInput.trim() || codexExchanging) return;
217-
setCodexExchanging(true);
218-
try {
219-
const result = await exchangeCodexOAuth(codexManualInput.trim());
220-
if (!result.ok) throw new Error(result.error || "Codex OAuth exchange failed");
221-
setCodexManualInput("");
222-
showToast("Codex connected", "success");
223-
setCodexAuthStarted(false);
224-
setCodexAuthWaiting(false);
225-
await refreshCodexConnection();
226-
} catch (err) {
227-
showToast(err.message || "Codex OAuth exchange failed", "error");
228-
} finally {
229-
setCodexExchanging(false);
230-
}
245+
await submitCodexAuthInput(codexManualInput);
231246
};
232247

233248
const handleCodexDisconnect = async () => {
@@ -301,7 +316,23 @@ export const Models = () => {
301316
? html`<${Badge} tone="success">Connected</${Badge}>`
302317
: html`<${Badge} tone="warning">Not connected</${Badge}>`}
303318
</div>
304-
${codexStatus.connected
319+
${codexAuthStarted
320+
? html`
321+
<div class="flex items-center justify-between gap-2">
322+
<p class="text-xs text-fg-muted">
323+
${codexAuthWaiting
324+
? "Complete login in the popup. AlphaClaw should finish automatically, but you can paste the redirect URL below if it doesn't."
325+
: "Paste the redirect URL from your browser to finish connecting."}
326+
</p>
327+
<button
328+
onclick=${startCodexAuth}
329+
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary shrink-0"
330+
>
331+
Restart
332+
</button>
333+
</div>
334+
`
335+
: codexStatus.connected
305336
? html`
306337
<div class="flex gap-2">
307338
<button
@@ -318,31 +349,15 @@ export const Models = () => {
318349
</button>
319350
</div>
320351
`
321-
: !codexAuthStarted
322-
? html`
352+
: html`
323353
<button
324354
onclick=${startCodexAuth}
325355
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
326356
>
327357
Connect Codex OAuth
328358
</button>
329-
`
330-
: html`
331-
<div class="flex items-center justify-between gap-2">
332-
<p class="text-xs text-fg-muted">
333-
${codexAuthWaiting
334-
? "Complete login in the popup, then paste the redirect URL."
335-
: "Paste the redirect URL from your browser to finish connecting."}
336-
</p>
337-
<button
338-
onclick=${startCodexAuth}
339-
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary shrink-0"
340-
>
341-
Restart
342-
</button>
343-
</div>
344359
`}
345-
${!codexStatus.connected && codexAuthStarted
360+
${codexAuthStarted
346361
? html`
347362
<p class="text-xs text-fg-muted">
348363
After login, copy the full redirect URL (starts with

0 commit comments

Comments
 (0)