@chrysb/alphaclaw 0.9.0-beta.6 → 0.9.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 +25 -25
- package/lib/cli/git-runtime.js +97 -0
- package/lib/public/css/chat.css +0 -12
- 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 +2770 -2762
- package/lib/public/js/app.js +26 -14
- package/lib/public/js/components/agents-tab/create-channel-modal.js +259 -59
- package/lib/public/js/components/gateway.js +0 -286
- package/lib/public/js/components/general/index.js +0 -7
- package/lib/public/js/components/icons.js +26 -25
- package/lib/public/js/components/modal-shell.js +1 -1
- 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 +2 -7
- 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/chat-route.js +2 -9
- package/lib/public/js/components/routes/general-route.js +0 -6
- package/lib/public/js/components/routes/index.js +0 -1
- package/lib/public/js/components/routes/watchdog-route.js +0 -6
- package/lib/public/js/components/sidebar.js +21 -7
- package/lib/public/js/components/theme-toggle.js +113 -0
- package/lib/public/js/components/update-modal.js +174 -51
- package/lib/public/js/components/watchdog-tab/index.js +0 -6
- package/lib/public/js/components/welcome/index.js +0 -2
- package/lib/public/js/components/welcome/use-welcome.js +101 -36
- package/lib/public/js/hooks/use-app-shell-controller.js +16 -33
- package/lib/public/js/lib/api.js +0 -28
- package/lib/public/js/lib/app-navigation.js +0 -2
- package/lib/public/js/lib/channel-provider-availability.js +1 -2
- 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/scripts/git +47 -1
- package/lib/server/agents/channels.js +1 -4
- package/lib/server/alphaclaw-version.js +590 -132
- package/lib/server/constants.js +5 -0
- package/lib/server/db/webhooks/index.js +48 -8
- package/lib/server/exec-defaults-config.js +163 -0
- package/lib/server/init/register-server-routes.js +0 -8
- package/lib/server/init/server-lifecycle.js +2 -0
- package/lib/server/model-catalog-cache.js +251 -0
- package/lib/server/onboarding/index.js +5 -0
- package/lib/server/routes/models.js +14 -23
- package/lib/server/routes/nodes.js +9 -23
- package/lib/server/routes/system.js +3 -16
- package/lib/server/routes/webhooks.js +12 -1
- package/lib/server/startup.js +8 -0
- package/lib/server/watchdog-notify.js +172 -55
- package/lib/server.js +17 -2
- package/package.json +2 -2
- package/patches/openclaw+2026.4.9.patch +13 -0
- package/lib/public/js/components/mcp-tab/index.js +0 -237
- package/lib/public/js/components/routes/mcp-route.js +0 -7
- package/lib/server/mcp-bridge.js +0 -158
- package/lib/server/routes/mcp.js +0 -252
- package/patches/openclaw+2026.3.28.patch +0 -13
|
@@ -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
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
exchangeCodexOAuth,
|
|
5
5
|
fetchCodexStatus,
|
|
6
6
|
} from "../../lib/api.js";
|
|
7
|
+
import {
|
|
8
|
+
isCodexAuthCallbackMessage,
|
|
9
|
+
openCodexAuthWindow,
|
|
10
|
+
} from "../../lib/codex-oauth-window.js";
|
|
7
11
|
|
|
8
12
|
export const useWelcomeCodex = ({ setFormError } = {}) => {
|
|
9
13
|
const [codexStatus, setCodexStatus] = useState({ connected: false });
|
|
@@ -12,6 +16,7 @@ export const useWelcomeCodex = ({ setFormError } = {}) => {
|
|
|
12
16
|
const [codexExchanging, setCodexExchanging] = useState(false);
|
|
13
17
|
const [codexAuthStarted, setCodexAuthStarted] = useState(false);
|
|
14
18
|
const [codexAuthWaiting, setCodexAuthWaiting] = useState(false);
|
|
19
|
+
const codexExchangeInFlightRef = useRef(false);
|
|
15
20
|
const codexPopupPollRef = useRef(null);
|
|
16
21
|
|
|
17
22
|
const refreshCodexStatus = async () => {
|
|
@@ -33,10 +38,36 @@ export const useWelcomeCodex = ({ setFormError } = {}) => {
|
|
|
33
38
|
refreshCodexStatus();
|
|
34
39
|
}, []);
|
|
35
40
|
|
|
41
|
+
const submitCodexAuthInput = async (input) => {
|
|
42
|
+
const normalizedInput = String(input || "").trim();
|
|
43
|
+
if (!normalizedInput || codexExchangeInFlightRef.current) return;
|
|
44
|
+
codexExchangeInFlightRef.current = true;
|
|
45
|
+
setCodexManualInput(normalizedInput);
|
|
46
|
+
setCodexExchanging(true);
|
|
47
|
+
setFormError(null);
|
|
48
|
+
try {
|
|
49
|
+
const result = await exchangeCodexOAuth(normalizedInput);
|
|
50
|
+
if (!result.ok)
|
|
51
|
+
throw new Error(result.error || "Codex OAuth exchange failed");
|
|
52
|
+
setCodexManualInput("");
|
|
53
|
+
setCodexAuthStarted(false);
|
|
54
|
+
setCodexAuthWaiting(false);
|
|
55
|
+
await refreshCodexStatus();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
setCodexAuthWaiting(false);
|
|
58
|
+
setFormError(err.message || "Codex OAuth exchange failed");
|
|
59
|
+
} finally {
|
|
60
|
+
codexExchangeInFlightRef.current = false;
|
|
61
|
+
setCodexExchanging(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
36
65
|
useEffect(() => {
|
|
37
66
|
const onMessage = async (e) => {
|
|
38
67
|
if (e.data?.codex === "success") {
|
|
39
68
|
await refreshCodexStatus();
|
|
69
|
+
} else if (isCodexAuthCallbackMessage(e.data)) {
|
|
70
|
+
await submitCodexAuthInput(e.data.input);
|
|
40
71
|
}
|
|
41
72
|
if (e.data?.codex === "error") {
|
|
42
73
|
setFormError(`Codex auth failed: ${e.data.message || "unknown error"}`);
|
|
@@ -44,7 +75,7 @@ export const useWelcomeCodex = ({ setFormError } = {}) => {
|
|
|
44
75
|
};
|
|
45
76
|
window.addEventListener("message", onMessage);
|
|
46
77
|
return () => window.removeEventListener("message", onMessage);
|
|
47
|
-
}, [setFormError]);
|
|
78
|
+
}, [setFormError, submitCodexAuthInput]);
|
|
48
79
|
|
|
49
80
|
useEffect(
|
|
50
81
|
() => () => {
|
|
@@ -60,15 +91,9 @@ export const useWelcomeCodex = ({ setFormError } = {}) => {
|
|
|
60
91
|
if (codexStatus.connected) return;
|
|
61
92
|
setCodexAuthStarted(true);
|
|
62
93
|
setCodexAuthWaiting(true);
|
|
63
|
-
const
|
|
64
|
-
const popup = window.open(
|
|
65
|
-
authUrl,
|
|
66
|
-
"codex-auth",
|
|
67
|
-
"popup=yes,width=640,height=780",
|
|
68
|
-
);
|
|
94
|
+
const popup = openCodexAuthWindow();
|
|
69
95
|
if (!popup || popup.closed) {
|
|
70
96
|
setCodexAuthWaiting(false);
|
|
71
|
-
window.location.href = authUrl;
|
|
72
97
|
return;
|
|
73
98
|
}
|
|
74
99
|
if (codexPopupPollRef.current) {
|
|
@@ -84,22 +109,7 @@ export const useWelcomeCodex = ({ setFormError } = {}) => {
|
|
|
84
109
|
};
|
|
85
110
|
|
|
86
111
|
const completeCodexAuth = async () => {
|
|
87
|
-
|
|
88
|
-
setCodexExchanging(true);
|
|
89
|
-
setFormError(null);
|
|
90
|
-
try {
|
|
91
|
-
const result = await exchangeCodexOAuth(codexManualInput.trim());
|
|
92
|
-
if (!result.ok)
|
|
93
|
-
throw new Error(result.error || "Codex OAuth exchange failed");
|
|
94
|
-
setCodexManualInput("");
|
|
95
|
-
setCodexAuthStarted(false);
|
|
96
|
-
setCodexAuthWaiting(false);
|
|
97
|
-
await refreshCodexStatus();
|
|
98
|
-
} catch (err) {
|
|
99
|
-
setFormError(err.message || "Codex OAuth exchange failed");
|
|
100
|
-
} finally {
|
|
101
|
-
setCodexExchanging(false);
|
|
102
|
-
}
|
|
112
|
+
await submitCodexAuthInput(codexManualInput);
|
|
103
113
|
};
|
|
104
114
|
|
|
105
115
|
const handleCodexDisconnect = async () => {
|
|
@@ -11,6 +11,8 @@ export const kGithubFlowImport = "import";
|
|
|
11
11
|
export const kGithubTargetRepoModeCreate = "create";
|
|
12
12
|
export const kGithubTargetRepoModeExistingEmpty = "existing-empty";
|
|
13
13
|
|
|
14
|
+
const hasValue = (value) => !!String(value || "").trim();
|
|
15
|
+
|
|
14
16
|
export const normalizeGithubRepoInput = (repoInput) =>
|
|
15
17
|
String(repoInput || "")
|
|
16
18
|
.trim()
|
|
@@ -25,6 +27,74 @@ export const isValidGithubRepoInput = (repoInput) => {
|
|
|
25
27
|
return parts.length === 2 && !parts.some((part) => /\s/.test(part));
|
|
26
28
|
};
|
|
27
29
|
|
|
30
|
+
const getGithubGroupError = (vals) => {
|
|
31
|
+
const githubFlow = vals._GITHUB_FLOW || kGithubFlowFresh;
|
|
32
|
+
if (!hasValue(vals.GITHUB_TOKEN)) {
|
|
33
|
+
return "Enter a GitHub personal access token to continue.";
|
|
34
|
+
}
|
|
35
|
+
if (!hasValue(vals.GITHUB_WORKSPACE_REPO)) {
|
|
36
|
+
return 'Enter the target repo as "owner/repo".';
|
|
37
|
+
}
|
|
38
|
+
if (!isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO)) {
|
|
39
|
+
return 'Target repo must be in "owner/repo" format.';
|
|
40
|
+
}
|
|
41
|
+
if (githubFlow === kGithubFlowImport) {
|
|
42
|
+
if (!hasValue(vals._GITHUB_SOURCE_REPO)) {
|
|
43
|
+
return 'Enter the source repo as "owner/repo".';
|
|
44
|
+
}
|
|
45
|
+
if (!isValidGithubRepoInput(vals._GITHUB_SOURCE_REPO)) {
|
|
46
|
+
return 'Source repo must be in "owner/repo" format.';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getAiGroupError = (vals, ctx = {}) => {
|
|
53
|
+
if (!hasValue(vals.MODEL_KEY) || !String(vals.MODEL_KEY).includes("/")) {
|
|
54
|
+
return "Choose a model to continue.";
|
|
55
|
+
}
|
|
56
|
+
if (ctx.selectedProvider === "openai-codex" && ctx.codexLoading) {
|
|
57
|
+
return "Checking Codex OAuth status. Try Next again in a moment.";
|
|
58
|
+
}
|
|
59
|
+
if (!ctx.hasAi) {
|
|
60
|
+
return ctx.selectedProvider === "openai-codex"
|
|
61
|
+
? "Connect Codex OAuth to continue."
|
|
62
|
+
: "Add credentials for the selected model provider to continue.";
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const getChannelsGroupError = (vals) => {
|
|
68
|
+
const hasTelegram = hasValue(vals.TELEGRAM_BOT_TOKEN);
|
|
69
|
+
const hasDiscord = hasValue(vals.DISCORD_BOT_TOKEN);
|
|
70
|
+
const hasSlackBot = hasValue(vals.SLACK_BOT_TOKEN);
|
|
71
|
+
const hasSlackApp = hasValue(vals.SLACK_APP_TOKEN);
|
|
72
|
+
|
|
73
|
+
if (hasSlackBot && !hasSlackApp) {
|
|
74
|
+
return "Add the Slack app token to continue with Slack.";
|
|
75
|
+
}
|
|
76
|
+
if (!hasSlackBot && hasSlackApp) {
|
|
77
|
+
return "Add the Slack bot token to continue with Slack.";
|
|
78
|
+
}
|
|
79
|
+
if (!hasTelegram && !hasDiscord && !(hasSlackBot && hasSlackApp)) {
|
|
80
|
+
return "Add at least one channel to continue.";
|
|
81
|
+
}
|
|
82
|
+
return "";
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const getWelcomeGroupError = (groupId, vals, ctx = {}) => {
|
|
86
|
+
switch (groupId) {
|
|
87
|
+
case "github":
|
|
88
|
+
return getGithubGroupError(vals);
|
|
89
|
+
case "ai":
|
|
90
|
+
return getAiGroupError(vals, ctx);
|
|
91
|
+
case "channels":
|
|
92
|
+
return getChannelsGroupError(vals);
|
|
93
|
+
default:
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
28
98
|
export const kWelcomeGroups = [
|
|
29
99
|
{
|
|
30
100
|
id: "github",
|
|
@@ -64,21 +134,14 @@ export const kWelcomeGroups = [
|
|
|
64
134
|
placeholder: "ghp_... or github_pat_...",
|
|
65
135
|
},
|
|
66
136
|
],
|
|
67
|
-
validate: (vals) =>
|
|
68
|
-
const githubFlow = vals._GITHUB_FLOW || kGithubFlowFresh;
|
|
69
|
-
const hasTarget = isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO);
|
|
70
|
-
const hasSource =
|
|
71
|
-
githubFlow !== kGithubFlowImport ||
|
|
72
|
-
isValidGithubRepoInput(vals._GITHUB_SOURCE_REPO);
|
|
73
|
-
return !!(vals.GITHUB_TOKEN && hasTarget && hasSource);
|
|
74
|
-
},
|
|
137
|
+
validate: (vals, ctx = {}) => !getWelcomeGroupError("github", vals, ctx),
|
|
75
138
|
},
|
|
76
139
|
{
|
|
77
140
|
id: "ai",
|
|
78
141
|
title: "Primary Agent Model",
|
|
79
142
|
description: "Choose your main model and authenticate its provider",
|
|
80
143
|
fields: kAllAiAuthFields,
|
|
81
|
-
validate: (vals, ctx = {}) =>
|
|
144
|
+
validate: (vals, ctx = {}) => !getWelcomeGroupError("ai", vals, ctx),
|
|
82
145
|
},
|
|
83
146
|
{
|
|
84
147
|
id: "channels",
|
|
@@ -152,7 +215,7 @@ export const kWelcomeGroups = [
|
|
|
152
215
|
placeholder: "xapp-...",
|
|
153
216
|
},
|
|
154
217
|
],
|
|
155
|
-
validate: (vals) =>
|
|
218
|
+
validate: (vals, ctx = {}) => !getWelcomeGroupError("channels", vals, ctx),
|
|
156
219
|
},
|
|
157
220
|
{
|
|
158
221
|
id: "tools",
|
|
@@ -175,3 +238,6 @@ export const kWelcomeGroups = [
|
|
|
175
238
|
validate: () => true,
|
|
176
239
|
},
|
|
177
240
|
];
|
|
241
|
+
|
|
242
|
+
export const findFirstInvalidWelcomeGroup = (vals, ctx = {}) =>
|
|
243
|
+
kWelcomeGroups.find((group) => getWelcomeGroupError(group.id, vals, ctx)) || null;
|
|
@@ -50,12 +50,10 @@ export const WelcomeFormStep = ({
|
|
|
50
50
|
error,
|
|
51
51
|
step,
|
|
52
52
|
totalGroups,
|
|
53
|
-
currentGroupValid,
|
|
54
53
|
goBack,
|
|
55
54
|
goNext,
|
|
56
55
|
loading,
|
|
57
56
|
githubStepLoading,
|
|
58
|
-
allValid,
|
|
59
57
|
handleSubmit,
|
|
60
58
|
}) => {
|
|
61
59
|
const [showOptionalOpenai, setShowOptionalOpenai] = useState(false);
|
|
@@ -294,13 +292,12 @@ export const WelcomeFormStep = ({
|
|
|
294
292
|
/>
|
|
295
293
|
`}
|
|
296
294
|
</div>
|
|
297
|
-
${
|
|
298
|
-
codexAuthStarted &&
|
|
295
|
+
${codexAuthStarted &&
|
|
299
296
|
html`
|
|
300
297
|
<div class="space-y-1 pt-1">
|
|
301
298
|
<p class="text-xs text-fg-muted">
|
|
302
299
|
${codexAuthWaiting
|
|
303
|
-
? "Complete login in the popup,
|
|
300
|
+
? "Complete login in the popup. AlphaClaw should finish automatically, but if it doesn't, paste the full redirect URL from the address bar (starts with "
|
|
304
301
|
: "Paste the full redirect URL from the address bar (starts with "}
|
|
305
302
|
<code class="text-xs bg-field px-1 rounded"
|
|
306
303
|
>http://localhost:1455/auth/callback</code
|
|
@@ -442,7 +439,6 @@ export const WelcomeFormStep = ({
|
|
|
442
439
|
: html`<div class="w-full"></div>`}
|
|
443
440
|
<${ActionButton}
|
|
444
441
|
onClick=${goNext}
|
|
445
|
-
disabled=${!currentGroupValid}
|
|
446
442
|
loading=${activeGroup.id === "github" && githubStepLoading}
|
|
447
443
|
tone="primary"
|
|
448
444
|
size="md"
|
|
@@ -466,7 +462,6 @@ export const WelcomeFormStep = ({
|
|
|
466
462
|
: html`<div class="w-full"></div>`}
|
|
467
463
|
<${ActionButton}
|
|
468
464
|
onClick=${handleSubmit}
|
|
469
|
-
disabled=${!allValid}
|
|
470
465
|
loading=${loading}
|
|
471
466
|
tone="primary"
|
|
472
467
|
size="md"
|
|
@@ -20,13 +20,11 @@ export const WelcomeHeader = ({
|
|
|
20
20
|
|
|
21
21
|
return html`
|
|
22
22
|
<div class="text-center mb-1">
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
height="33"
|
|
29
|
-
/>
|
|
23
|
+
<span
|
|
24
|
+
class="ac-logo-mark block mx-auto mb-3"
|
|
25
|
+
style="--ac-logo-width: 32px; --ac-logo-height: 33px;"
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
></span>
|
|
30
28
|
<h1 class="text-2xl font-semibold mb-2">Setup</h1>
|
|
31
29
|
<p style="color: var(--text-muted)" class="text-sm">
|
|
32
30
|
Let's get your agent running
|
|
@@ -34,7 +32,7 @@ export const WelcomeHeader = ({
|
|
|
34
32
|
<div class="mt-4 mb-2 flex items-center justify-center">
|
|
35
33
|
<span
|
|
36
34
|
class="text-[11px] px-2.5 py-1 rounded-full border border-border font-medium"
|
|
37
|
-
style="background:
|
|
35
|
+
style="background: var(--field-bg-contrast); color: var(--text-muted)"
|
|
38
36
|
>
|
|
39
37
|
${isPreStep
|
|
40
38
|
? "Choose your destiny"
|
|
@@ -51,16 +49,16 @@ export const WelcomeHeader = ({
|
|
|
51
49
|
const isPairingComplete =
|
|
52
50
|
idx < step || (isPairingStep && group.id === "pairing");
|
|
53
51
|
const bg = isPreStep
|
|
54
|
-
? "
|
|
52
|
+
? "var(--border-strong)"
|
|
55
53
|
: isActive
|
|
56
|
-
? "
|
|
54
|
+
? "var(--accent)"
|
|
57
55
|
: group.id === "pairing"
|
|
58
56
|
? isPairingComplete
|
|
59
|
-
? "
|
|
60
|
-
: "
|
|
57
|
+
? "var(--accent-dim)"
|
|
58
|
+
: "var(--border-strong)"
|
|
61
59
|
: isComplete
|
|
62
|
-
? "
|
|
63
|
-
: "
|
|
60
|
+
? "var(--accent-dim)"
|
|
61
|
+
: "var(--border-strong)";
|
|
64
62
|
return html`
|
|
65
63
|
<div
|
|
66
64
|
class="h-1 flex-1 rounded-full transition-colors ${isActive ? "ac-step-pill-pulse" : ""}"
|
|
@@ -45,7 +45,7 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
|
|
|
45
45
|
if (error) {
|
|
46
46
|
return html`
|
|
47
47
|
<div class="py-4 flex flex-col items-center text-center gap-3">
|
|
48
|
-
<h3 class="text-lg font-semibold text-
|
|
48
|
+
<h3 class="text-lg font-semibold text-body">Setup failed</h3>
|
|
49
49
|
<p class="text-sm text-fg-muted">Fix the values and try again.</p>
|
|
50
50
|
</div>
|
|
51
51
|
<div
|
|
@@ -83,8 +83,8 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
|
|
|
83
83
|
<div
|
|
84
84
|
class="flex-1 flex flex-col items-center justify-center text-center gap-4"
|
|
85
85
|
>
|
|
86
|
-
<${LoadingSpinner} className="h-8 w-8 text-
|
|
87
|
-
<h3 class="text-lg font-semibold text-
|
|
86
|
+
<${LoadingSpinner} className="h-8 w-8 text-body" />
|
|
87
|
+
<h3 class="text-lg font-semibold text-body">
|
|
88
88
|
Initializing OpenClaw...
|
|
89
89
|
</h3>
|
|
90
90
|
<p class="text-sm text-fg-muted">This could take 10-15 seconds</p>
|