@chrysb/alphaclaw 0.8.5 → 0.8.7-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/alphaclaw.js +56 -20
- package/lib/public/css/explorer.css +48 -0
- package/lib/public/css/shell.css +149 -0
- package/lib/public/css/tailwind.generated.css +1 -1
- package/lib/public/css/theme.css +265 -0
- package/lib/public/dist/app.bundle.js +2441 -2352
- package/lib/public/js/app.js +7 -0
- package/lib/public/js/components/gateway.js +6 -3
- package/lib/public/js/components/general/index.js +2 -0
- package/lib/public/js/components/icons.js +38 -0
- package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
- package/lib/public/js/components/models-tab/use-models.js +74 -9
- package/lib/public/js/components/models.js +52 -37
- package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
- package/lib/public/js/components/onboarding/welcome-config.js +76 -10
- package/lib/public/js/components/onboarding/welcome-form-step.js +31 -11
- package/lib/public/js/components/onboarding/welcome-header.js +12 -14
- package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
- package/lib/public/js/components/providers.js +53 -42
- package/lib/public/js/components/routes/general-route.js +2 -0
- package/lib/public/js/components/routes/watchdog-route.js +2 -0
- package/lib/public/js/components/sidebar.js +29 -8
- package/lib/public/js/components/theme-toggle.js +113 -0
- package/lib/public/js/components/update-modal-helpers.js +12 -0
- package/lib/public/js/components/update-modal.js +2 -1
- package/lib/public/js/components/watchdog-tab/index.js +2 -0
- package/lib/public/js/components/welcome/index.js +1 -2
- package/lib/public/js/components/welcome/use-welcome.js +153 -38
- package/lib/public/js/hooks/use-app-shell-controller.js +33 -9
- package/lib/public/js/lib/api.js +35 -0
- package/lib/public/js/lib/codex-oauth-window.js +22 -0
- package/lib/public/js/lib/model-catalog.js +20 -0
- package/lib/public/js/lib/storage-keys.js +1 -1
- package/lib/public/login.html +8 -4
- package/lib/public/setup.html +9 -0
- package/lib/server/alphaclaw-version.js +30 -127
- package/lib/server/db/webhooks/index.js +48 -8
- package/lib/server/model-catalog-cache.js +251 -0
- package/lib/server/openclaw-version.js +59 -130
- package/lib/server/pending-alphaclaw-update.js +71 -0
- package/lib/server/pending-openclaw-update.js +71 -0
- package/lib/server/routes/models.js +14 -23
- package/lib/server/routes/system.js +6 -1
- package/lib/server/routes/webhooks.js +12 -1
- package/package.json +1 -1
package/lib/public/js/app.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "wouter-preact";
|
|
10
10
|
import { logout } from "./lib/api.js";
|
|
11
11
|
import { Welcome } from "./components/welcome/index.js";
|
|
12
|
+
import { ThemeToggle } from "./components/theme-toggle.js";
|
|
12
13
|
import { ToastContainer } from "./components/toast.js";
|
|
13
14
|
import { GlobalRestartBanner } from "./components/global-restart-banner.js";
|
|
14
15
|
import { LoadingSpinner } from "./components/loading-spinner.js";
|
|
@@ -160,6 +161,9 @@ const App = () => {
|
|
|
160
161
|
class="min-h-screen flex flex-col items-center pt-12 pb-8 px-4"
|
|
161
162
|
style="position: relative; z-index: 1"
|
|
162
163
|
>
|
|
164
|
+
<div style="position: fixed; top: 16px; right: 16px; z-index: 50;">
|
|
165
|
+
<${ThemeToggle} />
|
|
166
|
+
</div>
|
|
163
167
|
<${Welcome}
|
|
164
168
|
onComplete=${controllerActions.handleOnboardingComplete}
|
|
165
169
|
acVersion=${controllerState.acVersion}
|
|
@@ -198,6 +202,7 @@ const App = () => {
|
|
|
198
202
|
onPreviewBrowseFile=${browseActions.handleBrowsePreviewFile}
|
|
199
203
|
acHasUpdate=${controllerState.acHasUpdate}
|
|
200
204
|
acLatest=${controllerState.acLatest}
|
|
205
|
+
acRestarting=${controllerState.acRestarting}
|
|
201
206
|
acUpdating=${controllerState.acUpdating}
|
|
202
207
|
onAcUpdate=${controllerActions.handleAcUpdate}
|
|
203
208
|
agents=${agentsState.agents}
|
|
@@ -380,6 +385,7 @@ const App = () => {
|
|
|
380
385
|
restartingGateway=${controllerState.restartingGateway}
|
|
381
386
|
onRestartGateway=${controllerActions.handleGatewayRestart}
|
|
382
387
|
restartSignal=${controllerState.gatewayRestartSignal}
|
|
388
|
+
openclawRestarting=${controllerState.openclawRestarting}
|
|
383
389
|
openclawUpdateInProgress=${controllerState.openclawUpdateInProgress}
|
|
384
390
|
onOpenclawVersionActionComplete=${controllerActions.handleOpenclawVersionActionComplete}
|
|
385
391
|
onOpenclawUpdate=${controllerActions.handleOpenclawUpdate}
|
|
@@ -415,6 +421,7 @@ const App = () => {
|
|
|
415
421
|
restartingGateway=${controllerState.restartingGateway}
|
|
416
422
|
onRestartGateway=${controllerActions.handleGatewayRestart}
|
|
417
423
|
restartSignal=${controllerState.gatewayRestartSignal}
|
|
424
|
+
openclawRestarting=${controllerState.openclawRestarting}
|
|
418
425
|
openclawUpdateInProgress=${controllerState.openclawUpdateInProgress}
|
|
419
426
|
onOpenclawVersionActionComplete=${controllerActions.handleOpenclawVersionActionComplete}
|
|
420
427
|
onOpenclawUpdate=${controllerActions.handleOpenclawUpdate}
|
|
@@ -27,6 +27,7 @@ const VersionRow = ({
|
|
|
27
27
|
fetchVersion,
|
|
28
28
|
applyUpdate,
|
|
29
29
|
updateInProgress = false,
|
|
30
|
+
updateLoadingLabel = "Updating...",
|
|
30
31
|
onActionComplete = () => {},
|
|
31
32
|
}) => {
|
|
32
33
|
const [checking, setChecking] = useState(false);
|
|
@@ -236,7 +237,7 @@ const VersionRow = ({
|
|
|
236
237
|
? updateIdleLabel
|
|
237
238
|
: "Check updates"}
|
|
238
239
|
loadingLabel=${isUpdateActionActive
|
|
239
|
-
?
|
|
240
|
+
? updateLoadingLabel
|
|
240
241
|
: "Checking..."}
|
|
241
242
|
className="hidden md:inline-flex"
|
|
242
243
|
/>
|
|
@@ -250,7 +251,7 @@ const VersionRow = ({
|
|
|
250
251
|
? updateIdleLabel
|
|
251
252
|
: "Check updates"}
|
|
252
253
|
loadingLabel=${isUpdateActionActive
|
|
253
|
-
?
|
|
254
|
+
? updateLoadingLabel
|
|
254
255
|
: "Checking..."}
|
|
255
256
|
/>
|
|
256
257
|
`}
|
|
@@ -272,7 +273,7 @@ const VersionRow = ({
|
|
|
272
273
|
loading=${updateButtonLoading}
|
|
273
274
|
warning=${isUpdateActionActive}
|
|
274
275
|
idleLabel=${updateIdleLabel}
|
|
275
|
-
loadingLabel
|
|
276
|
+
loadingLabel=${updateLoadingLabel}
|
|
276
277
|
className="flex-1 h-9 px-3"
|
|
277
278
|
/>
|
|
278
279
|
</div>
|
|
@@ -299,6 +300,7 @@ export const Gateway = ({
|
|
|
299
300
|
onOpenWatchdog,
|
|
300
301
|
onRepair,
|
|
301
302
|
repairing = false,
|
|
303
|
+
openclawRestarting = false,
|
|
302
304
|
openclawUpdateInProgress = false,
|
|
303
305
|
onOpenclawVersionActionComplete = () => {},
|
|
304
306
|
onOpenclawUpdate = updateOpenclaw,
|
|
@@ -443,6 +445,7 @@ export const Gateway = ({
|
|
|
443
445
|
fetchVersion=${fetchOpenclawVersion}
|
|
444
446
|
applyUpdate=${onOpenclawUpdate}
|
|
445
447
|
updateInProgress=${openclawUpdateInProgress}
|
|
448
|
+
updateLoadingLabel=${openclawRestarting ? "Restarting..." : "Updating..."}
|
|
446
449
|
onActionComplete=${onOpenclawVersionActionComplete}
|
|
447
450
|
/>
|
|
448
451
|
</div>
|
|
@@ -28,6 +28,7 @@ export const GeneralTab = ({
|
|
|
28
28
|
restartingGateway = false,
|
|
29
29
|
onRestartGateway = () => {},
|
|
30
30
|
restartSignal = 0,
|
|
31
|
+
openclawRestarting = false,
|
|
31
32
|
openclawUpdateInProgress = false,
|
|
32
33
|
onOpenclawVersionActionComplete = () => {},
|
|
33
34
|
onOpenclawUpdate = () => {},
|
|
@@ -54,6 +55,7 @@ export const GeneralTab = ({
|
|
|
54
55
|
onOpenWatchdog=${() => onSwitchTab("watchdog")}
|
|
55
56
|
onRepair=${actions.handleWatchdogRepair}
|
|
56
57
|
repairing=${state.repairingWatchdog}
|
|
58
|
+
openclawRestarting=${openclawRestarting}
|
|
57
59
|
openclawUpdateInProgress=${openclawUpdateInProgress}
|
|
58
60
|
onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
|
|
59
61
|
onOpenclawUpdate=${onOpenclawUpdate}
|
|
@@ -508,6 +508,44 @@ export const EyeLineIcon = ({ className = "" }) => html`
|
|
|
508
508
|
</svg>
|
|
509
509
|
`;
|
|
510
510
|
|
|
511
|
+
export const SunIcon = ({ className = "" }) => html`
|
|
512
|
+
<svg
|
|
513
|
+
class=${className}
|
|
514
|
+
viewBox="0 0 24 24"
|
|
515
|
+
fill="none"
|
|
516
|
+
stroke="currentColor"
|
|
517
|
+
stroke-width="2"
|
|
518
|
+
stroke-linecap="round"
|
|
519
|
+
stroke-linejoin="round"
|
|
520
|
+
aria-hidden="true"
|
|
521
|
+
>
|
|
522
|
+
<circle cx="12" cy="12" r="5" />
|
|
523
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
524
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
525
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
526
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
527
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
528
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
529
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
530
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
531
|
+
</svg>
|
|
532
|
+
`;
|
|
533
|
+
|
|
534
|
+
export const MoonIcon = ({ className = "" }) => html`
|
|
535
|
+
<svg
|
|
536
|
+
class=${className}
|
|
537
|
+
viewBox="0 0 24 24"
|
|
538
|
+
fill="none"
|
|
539
|
+
stroke="currentColor"
|
|
540
|
+
stroke-width="2"
|
|
541
|
+
stroke-linecap="round"
|
|
542
|
+
stroke-linejoin="round"
|
|
543
|
+
aria-hidden="true"
|
|
544
|
+
>
|
|
545
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
546
|
+
</svg>
|
|
547
|
+
`;
|
|
548
|
+
|
|
511
549
|
export const FullscreenLineIcon = ({ className = "" }) => html`
|
|
512
550
|
<svg
|
|
513
551
|
class=${className}
|
|
@@ -5,6 +5,10 @@ import { Badge } from "../badge.js";
|
|
|
5
5
|
import { SecretInput } from "../secret-input.js";
|
|
6
6
|
import { ActionButton } from "../action-button.js";
|
|
7
7
|
import { exchangeCodexOAuth, disconnectCodex } from "../../lib/api.js";
|
|
8
|
+
import {
|
|
9
|
+
isCodexAuthCallbackMessage,
|
|
10
|
+
openCodexAuthWindow,
|
|
11
|
+
} from "../../lib/codex-oauth-window.js";
|
|
8
12
|
import { showToast } from "../toast.js";
|
|
9
13
|
import {
|
|
10
14
|
kProviderAuthFields,
|
|
@@ -108,6 +112,7 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
|
|
|
108
112
|
const [authWaiting, setAuthWaiting] = useState(false);
|
|
109
113
|
const [manualInput, setManualInput] = useState("");
|
|
110
114
|
const [exchanging, setExchanging] = useState(false);
|
|
115
|
+
const exchangeInFlightRef = useRef(false);
|
|
111
116
|
const popupPollRef = useRef(null);
|
|
112
117
|
|
|
113
118
|
useEffect(
|
|
@@ -117,6 +122,30 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
|
|
|
117
122
|
[],
|
|
118
123
|
);
|
|
119
124
|
|
|
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
|
+
|
|
120
149
|
useEffect(() => {
|
|
121
150
|
const onMessage = async (e) => {
|
|
122
151
|
if (e.data?.codex === "success") {
|
|
@@ -124,6 +153,8 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
|
|
|
124
153
|
setAuthStarted(false);
|
|
125
154
|
setAuthWaiting(false);
|
|
126
155
|
await onRefreshCodex();
|
|
156
|
+
} else if (isCodexAuthCallbackMessage(e.data)) {
|
|
157
|
+
await submitAuthInput(e.data.input);
|
|
127
158
|
} else if (e.data?.codex === "error") {
|
|
128
159
|
showToast(
|
|
129
160
|
`Codex auth failed: ${e.data.message || "unknown error"}`,
|
|
@@ -133,19 +164,14 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
|
|
|
133
164
|
};
|
|
134
165
|
window.addEventListener("message", onMessage);
|
|
135
166
|
return () => window.removeEventListener("message", onMessage);
|
|
136
|
-
}, [onRefreshCodex]);
|
|
167
|
+
}, [onRefreshCodex, submitAuthInput]);
|
|
137
168
|
|
|
138
169
|
const startAuth = () => {
|
|
139
170
|
setAuthStarted(true);
|
|
140
171
|
setAuthWaiting(true);
|
|
141
|
-
const popup =
|
|
142
|
-
"/auth/codex/start",
|
|
143
|
-
"codex-auth",
|
|
144
|
-
"popup=yes,width=640,height=780",
|
|
145
|
-
);
|
|
172
|
+
const popup = openCodexAuthWindow();
|
|
146
173
|
if (!popup || popup.closed) {
|
|
147
174
|
setAuthWaiting(false);
|
|
148
|
-
window.location.href = "/auth/codex/start";
|
|
149
175
|
return;
|
|
150
176
|
}
|
|
151
177
|
if (popupPollRef.current) clearInterval(popupPollRef.current);
|
|
@@ -159,22 +185,7 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
|
|
|
159
185
|
};
|
|
160
186
|
|
|
161
187
|
const completeAuth = async () => {
|
|
162
|
-
|
|
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);
|
|
178
189
|
};
|
|
179
190
|
|
|
180
191
|
const handleDisconnect = async () => {
|
|
@@ -198,7 +209,23 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
|
|
|
198
209
|
? html`<${Badge} tone="success">Connected</${Badge}>`
|
|
199
210
|
: html`<${Badge} tone="warning">Not connected</${Badge}>`}
|
|
200
211
|
</div>
|
|
201
|
-
${
|
|
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
|
|
202
229
|
? html`
|
|
203
230
|
<div class="flex gap-2">
|
|
204
231
|
<button
|
|
@@ -215,32 +242,16 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
|
|
|
215
242
|
</button>
|
|
216
243
|
</div>
|
|
217
244
|
`
|
|
218
|
-
:
|
|
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
|
|
219
254
|
? 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`
|
|
244
255
|
<p class="text-xs text-fg-muted">
|
|
245
256
|
After login, copy the full redirect URL (starts with
|
|
246
257
|
<code class="text-xs bg-field px-1 rounded"
|
|
@@ -8,16 +8,35 @@ import {
|
|
|
8
8
|
} from "../../lib/api.js";
|
|
9
9
|
import { showToast } from "../toast.js";
|
|
10
10
|
import { useCachedFetch } from "../../hooks/use-cached-fetch.js";
|
|
11
|
+
import { usePolling } from "../../hooks/usePolling.js";
|
|
12
|
+
import { invalidateCache } from "../../lib/api-cache.js";
|
|
13
|
+
import {
|
|
14
|
+
getModelCatalogModels,
|
|
15
|
+
isModelCatalogRefreshing,
|
|
16
|
+
kModelCatalogCacheKey,
|
|
17
|
+
kModelCatalogPollIntervalMs,
|
|
18
|
+
} from "../../lib/model-catalog.js";
|
|
11
19
|
|
|
12
20
|
let kModelsTabCache = null;
|
|
13
21
|
const getCredentialValue = (value) =>
|
|
14
22
|
String(value?.key || value?.token || value?.access || "").trim();
|
|
23
|
+
const kNoModelsFoundError = "No models found";
|
|
24
|
+
const kModelSettingsLoadError = "Failed to load model settings";
|
|
15
25
|
|
|
16
26
|
export const useModels = (agentId) => {
|
|
17
27
|
const isScoped = !!agentId;
|
|
18
28
|
const normalizedAgentId = String(agentId || "").trim();
|
|
19
29
|
const useCache = !isScoped;
|
|
20
30
|
const [catalog, setCatalog] = useState(() => (useCache && kModelsTabCache?.catalog) || []);
|
|
31
|
+
const [catalogStatus, setCatalogStatus] = useState(
|
|
32
|
+
() =>
|
|
33
|
+
(useCache && kModelsTabCache?.catalogStatus) || {
|
|
34
|
+
source: "",
|
|
35
|
+
fetchedAt: null,
|
|
36
|
+
stale: false,
|
|
37
|
+
refreshing: false,
|
|
38
|
+
},
|
|
39
|
+
);
|
|
21
40
|
const [primary, setPrimary] = useState(() => (useCache && kModelsTabCache?.primary) || "");
|
|
22
41
|
const [configuredModels, setConfiguredModels] = useState(
|
|
23
42
|
() => (useCache && kModelsTabCache?.configuredModels) || {},
|
|
@@ -48,7 +67,7 @@ export const useModels = (agentId) => {
|
|
|
48
67
|
const modelsConfigCacheKey = normalizedAgentId
|
|
49
68
|
? `/api/models/config?agentId=${encodeURIComponent(normalizedAgentId)}`
|
|
50
69
|
: "/api/models/config";
|
|
51
|
-
const catalogFetchState = useCachedFetch(
|
|
70
|
+
const catalogFetchState = useCachedFetch(kModelCatalogCacheKey, fetchModels, {
|
|
52
71
|
maxAgeMs: 30000,
|
|
53
72
|
});
|
|
54
73
|
const configFetchState = useCachedFetch(
|
|
@@ -59,6 +78,41 @@ export const useModels = (agentId) => {
|
|
|
59
78
|
const codexFetchState = useCachedFetch("/api/codex/status", fetchCodexStatus, {
|
|
60
79
|
maxAgeMs: 15000,
|
|
61
80
|
});
|
|
81
|
+
const catalogPoll = usePolling(fetchModels, kModelCatalogPollIntervalMs, {
|
|
82
|
+
enabled: ready && isModelCatalogRefreshing(catalogStatus),
|
|
83
|
+
pauseWhenHidden: true,
|
|
84
|
+
cacheKey: kModelCatalogCacheKey,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const syncCatalogError = useCallback((catalogModels) => {
|
|
88
|
+
setError((current) => {
|
|
89
|
+
if (catalogModels.length > 0) {
|
|
90
|
+
return current === kNoModelsFoundError ? "" : current;
|
|
91
|
+
}
|
|
92
|
+
return current || kNoModelsFoundError;
|
|
93
|
+
});
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
const applyCatalogResult = useCallback(
|
|
97
|
+
(catalogResult) => {
|
|
98
|
+
const catalogModels = getModelCatalogModels(catalogResult);
|
|
99
|
+
const nextCatalogStatus = {
|
|
100
|
+
source: String(catalogResult?.source || ""),
|
|
101
|
+
fetchedAt: Number(catalogResult?.fetchedAt || 0) || null,
|
|
102
|
+
stale: Boolean(catalogResult?.stale),
|
|
103
|
+
refreshing: Boolean(catalogResult?.refreshing),
|
|
104
|
+
};
|
|
105
|
+
setCatalog(catalogModels);
|
|
106
|
+
setCatalogStatus(nextCatalogStatus);
|
|
107
|
+
updateCache({
|
|
108
|
+
catalog: catalogModels,
|
|
109
|
+
catalogStatus: nextCatalogStatus,
|
|
110
|
+
});
|
|
111
|
+
syncCatalogError(catalogModels);
|
|
112
|
+
return catalogModels;
|
|
113
|
+
},
|
|
114
|
+
[syncCatalogError, updateCache],
|
|
115
|
+
);
|
|
62
116
|
|
|
63
117
|
const refresh = useCallback(async () => {
|
|
64
118
|
if (!ready) setLoading(true);
|
|
@@ -69,10 +123,7 @@ export const useModels = (agentId) => {
|
|
|
69
123
|
configFetchState.refresh({ force: true }),
|
|
70
124
|
codexFetchState.refresh({ force: true }),
|
|
71
125
|
]);
|
|
72
|
-
const catalogModels =
|
|
73
|
-
? catalogResult.models
|
|
74
|
-
: [];
|
|
75
|
-
setCatalog(catalogModels);
|
|
126
|
+
const catalogModels = applyCatalogResult(catalogResult);
|
|
76
127
|
const p = configResult.primary || "";
|
|
77
128
|
const cm = configResult.configuredModels || {};
|
|
78
129
|
const ap = configResult.authProfiles || [];
|
|
@@ -94,20 +145,31 @@ export const useModels = (agentId) => {
|
|
|
94
145
|
authOrder: ao,
|
|
95
146
|
codexStatus: codex || { connected: false },
|
|
96
147
|
});
|
|
97
|
-
if (!catalogModels.length) setError("No models found");
|
|
98
148
|
} catch (err) {
|
|
99
|
-
setError(
|
|
100
|
-
showToast(
|
|
149
|
+
setError(kModelSettingsLoadError);
|
|
150
|
+
showToast(`${kModelSettingsLoadError}: ${err.message}`, "error");
|
|
101
151
|
} finally {
|
|
102
152
|
setReady(true);
|
|
103
153
|
setLoading(false);
|
|
104
154
|
}
|
|
105
|
-
}, [
|
|
155
|
+
}, [
|
|
156
|
+
applyCatalogResult,
|
|
157
|
+
catalogFetchState,
|
|
158
|
+
codexFetchState,
|
|
159
|
+
configFetchState,
|
|
160
|
+
ready,
|
|
161
|
+
updateCache,
|
|
162
|
+
]);
|
|
106
163
|
|
|
107
164
|
useEffect(() => {
|
|
108
165
|
refresh();
|
|
109
166
|
}, [agentId]);
|
|
110
167
|
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!catalogPoll.data) return;
|
|
170
|
+
applyCatalogResult(catalogPoll.data);
|
|
171
|
+
}, [applyCatalogResult, catalogPoll.data]);
|
|
172
|
+
|
|
111
173
|
const stableStringify = (obj) =>
|
|
112
174
|
JSON.stringify(Object.keys(obj).sort().reduce((acc, k) => { acc[k] = obj[k]; return acc; }, {}));
|
|
113
175
|
|
|
@@ -261,6 +323,7 @@ export const useModels = (agentId) => {
|
|
|
261
323
|
if (result.syncWarning) {
|
|
262
324
|
showToast(`Saved, but git-sync failed: ${result.syncWarning}`, "warning");
|
|
263
325
|
}
|
|
326
|
+
invalidateCache(kModelCatalogCacheKey);
|
|
264
327
|
await refresh();
|
|
265
328
|
} catch (err) {
|
|
266
329
|
showToast(err.message || "Failed to save changes", "error");
|
|
@@ -274,6 +337,8 @@ export const useModels = (agentId) => {
|
|
|
274
337
|
profileEdits,
|
|
275
338
|
orderEdits,
|
|
276
339
|
authProfiles,
|
|
340
|
+
isScoped,
|
|
341
|
+
agentId,
|
|
277
342
|
refresh,
|
|
278
343
|
]);
|
|
279
344
|
|
|
@@ -24,6 +24,10 @@ import {
|
|
|
24
24
|
kProviderLabels,
|
|
25
25
|
kProviderOrder,
|
|
26
26
|
} from "../lib/model-config.js";
|
|
27
|
+
import {
|
|
28
|
+
isCodexAuthCallbackMessage,
|
|
29
|
+
openCodexAuthWindow,
|
|
30
|
+
} from "../lib/codex-oauth-window.js";
|
|
27
31
|
|
|
28
32
|
const html = htm.bind(h);
|
|
29
33
|
|
|
@@ -51,6 +55,7 @@ export const Models = () => {
|
|
|
51
55
|
const [savedModel, setSavedModel] = useState(() => kModelsTabCache?.savedModel || "");
|
|
52
56
|
const [modelDirty, setModelDirty] = useState(false);
|
|
53
57
|
const [savedAiValues, setSavedAiValues] = useState(() => kModelsTabCache?.savedAiValues || {});
|
|
58
|
+
const codexExchangeInFlightRef = useRef(false);
|
|
54
59
|
const codexPopupPollRef = useRef(null);
|
|
55
60
|
|
|
56
61
|
const refresh = async () => {
|
|
@@ -122,18 +127,43 @@ export const Models = () => {
|
|
|
122
127
|
}
|
|
123
128
|
}, []);
|
|
124
129
|
|
|
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
|
+
|
|
125
153
|
useEffect(() => {
|
|
126
154
|
const onMessage = async (e) => {
|
|
127
155
|
if (e.data?.codex === "success") {
|
|
128
156
|
showToast("Codex connected", "success");
|
|
129
157
|
await refreshCodexConnection();
|
|
158
|
+
} else if (isCodexAuthCallbackMessage(e.data)) {
|
|
159
|
+
await submitCodexAuthInput(e.data.input);
|
|
130
160
|
} else if (e.data?.codex === "error") {
|
|
131
161
|
showToast(`Codex auth failed: ${e.data.message || "unknown error"}`, "error");
|
|
132
162
|
}
|
|
133
163
|
};
|
|
134
164
|
window.addEventListener("message", onMessage);
|
|
135
165
|
return () => window.removeEventListener("message", onMessage);
|
|
136
|
-
}, []);
|
|
166
|
+
}, [submitCodexAuthInput]);
|
|
137
167
|
|
|
138
168
|
const setEnvValue = (key, value) => {
|
|
139
169
|
setEnvVars((prev) => {
|
|
@@ -194,10 +224,9 @@ export const Models = () => {
|
|
|
194
224
|
if (codexStatus.connected) return;
|
|
195
225
|
setCodexAuthStarted(true);
|
|
196
226
|
setCodexAuthWaiting(true);
|
|
197
|
-
const popup =
|
|
227
|
+
const popup = openCodexAuthWindow();
|
|
198
228
|
if (!popup || popup.closed) {
|
|
199
229
|
setCodexAuthWaiting(false);
|
|
200
|
-
window.location.href = "/auth/codex/start";
|
|
201
230
|
return;
|
|
202
231
|
}
|
|
203
232
|
if (codexPopupPollRef.current) {
|
|
@@ -213,21 +242,7 @@ export const Models = () => {
|
|
|
213
242
|
};
|
|
214
243
|
|
|
215
244
|
const completeCodexAuth = async () => {
|
|
216
|
-
|
|
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);
|
|
231
246
|
};
|
|
232
247
|
|
|
233
248
|
const handleCodexDisconnect = async () => {
|
|
@@ -301,7 +316,23 @@ export const Models = () => {
|
|
|
301
316
|
? html`<${Badge} tone="success">Connected</${Badge}>`
|
|
302
317
|
: html`<${Badge} tone="warning">Not connected</${Badge}>`}
|
|
303
318
|
</div>
|
|
304
|
-
${
|
|
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
|
|
305
336
|
? html`
|
|
306
337
|
<div class="flex gap-2">
|
|
307
338
|
<button
|
|
@@ -318,31 +349,15 @@ export const Models = () => {
|
|
|
318
349
|
</button>
|
|
319
350
|
</div>
|
|
320
351
|
`
|
|
321
|
-
:
|
|
322
|
-
? html`
|
|
352
|
+
: html`
|
|
323
353
|
<button
|
|
324
354
|
onclick=${startCodexAuth}
|
|
325
355
|
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
|
|
326
356
|
>
|
|
327
357
|
Connect Codex OAuth
|
|
328
358
|
</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>
|
|
344
359
|
`}
|
|
345
|
-
${
|
|
360
|
+
${codexAuthStarted
|
|
346
361
|
? html`
|
|
347
362
|
<p class="text-xs text-fg-muted">
|
|
348
363
|
After login, copy the full redirect URL (starts with
|