@chrysb/alphaclaw 0.9.0-beta.7 → 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.
Files changed (66) hide show
  1. package/bin/alphaclaw.js +25 -25
  2. package/lib/cli/git-runtime.js +97 -0
  3. package/lib/public/css/chat.css +0 -12
  4. package/lib/public/css/explorer.css +48 -0
  5. package/lib/public/css/shell.css +149 -0
  6. package/lib/public/css/tailwind.generated.css +1 -1
  7. package/lib/public/css/theme.css +265 -0
  8. package/lib/public/dist/app.bundle.js +2770 -2762
  9. package/lib/public/js/app.js +26 -14
  10. package/lib/public/js/components/agents-tab/create-channel-modal.js +259 -59
  11. package/lib/public/js/components/gateway.js +0 -286
  12. package/lib/public/js/components/general/index.js +0 -7
  13. package/lib/public/js/components/icons.js +26 -25
  14. package/lib/public/js/components/modal-shell.js +1 -1
  15. package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
  16. package/lib/public/js/components/models-tab/use-models.js +74 -9
  17. package/lib/public/js/components/models.js +52 -37
  18. package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
  19. package/lib/public/js/components/onboarding/welcome-config.js +76 -10
  20. package/lib/public/js/components/onboarding/welcome-form-step.js +2 -7
  21. package/lib/public/js/components/onboarding/welcome-header.js +12 -14
  22. package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
  23. package/lib/public/js/components/providers.js +53 -42
  24. package/lib/public/js/components/routes/chat-route.js +2 -9
  25. package/lib/public/js/components/routes/general-route.js +0 -6
  26. package/lib/public/js/components/routes/index.js +0 -1
  27. package/lib/public/js/components/routes/watchdog-route.js +0 -6
  28. package/lib/public/js/components/sidebar.js +21 -7
  29. package/lib/public/js/components/theme-toggle.js +113 -0
  30. package/lib/public/js/components/update-modal.js +174 -51
  31. package/lib/public/js/components/watchdog-tab/index.js +0 -6
  32. package/lib/public/js/components/welcome/index.js +0 -2
  33. package/lib/public/js/components/welcome/use-welcome.js +101 -36
  34. package/lib/public/js/hooks/use-app-shell-controller.js +16 -33
  35. package/lib/public/js/lib/api.js +0 -28
  36. package/lib/public/js/lib/app-navigation.js +0 -2
  37. package/lib/public/js/lib/channel-provider-availability.js +1 -2
  38. package/lib/public/js/lib/codex-oauth-window.js +22 -0
  39. package/lib/public/js/lib/model-catalog.js +20 -0
  40. package/lib/public/js/lib/storage-keys.js +1 -1
  41. package/lib/public/login.html +8 -4
  42. package/lib/public/setup.html +9 -0
  43. package/lib/scripts/git +47 -1
  44. package/lib/server/agents/channels.js +1 -4
  45. package/lib/server/alphaclaw-version.js +590 -132
  46. package/lib/server/constants.js +5 -0
  47. package/lib/server/db/webhooks/index.js +48 -8
  48. package/lib/server/exec-defaults-config.js +163 -0
  49. package/lib/server/init/register-server-routes.js +0 -8
  50. package/lib/server/init/server-lifecycle.js +2 -0
  51. package/lib/server/model-catalog-cache.js +251 -0
  52. package/lib/server/onboarding/index.js +5 -0
  53. package/lib/server/routes/models.js +14 -23
  54. package/lib/server/routes/nodes.js +9 -23
  55. package/lib/server/routes/system.js +3 -16
  56. package/lib/server/routes/webhooks.js +12 -1
  57. package/lib/server/startup.js +8 -0
  58. package/lib/server/watchdog-notify.js +172 -55
  59. package/lib/server.js +17 -2
  60. package/package.json +2 -2
  61. package/patches/openclaw+2026.4.9.patch +13 -0
  62. package/lib/public/js/components/mcp-tab/index.js +0 -237
  63. package/lib/public/js/components/routes/mcp-route.js +0 -7
  64. package/lib/server/mcp-bridge.js +0 -158
  65. package/lib/server/routes/mcp.js +0 -292
  66. 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("/api/models", fetchModels, {
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 = Array.isArray(catalogResult.models)
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("Failed to load model settings");
100
- showToast(`Failed to load model settings: ${err.message}`, "error");
149
+ setError(kModelSettingsLoadError);
150
+ showToast(`${kModelSettingsLoadError}: ${err.message}`, "error");
101
151
  } finally {
102
152
  setReady(true);
103
153
  setLoading(false);
104
154
  }
105
- }, [catalogFetchState, codexFetchState, configFetchState, ready, updateCache, agentId, isScoped]);
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 = window.open("/auth/codex/start", "codex-auth", "popup=yes,width=640,height=780");
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
- 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);
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
- ${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
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
- : !codexAuthStarted
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
- ${!codexStatus.connected && codexAuthStarted
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 authUrl = "/auth/codex/start";
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
- if (!codexManualInput.trim() || codexExchanging) return;
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 = {}) => !!(vals.MODEL_KEY && ctx.hasAi),
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) => !!(vals.TELEGRAM_BOT_TOKEN || vals.DISCORD_BOT_TOKEN || (vals.SLACK_BOT_TOKEN && vals.SLACK_APP_TOKEN)),
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
- ${!codexStatus.connected &&
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, then paste the full redirect URL from the address bar (starts with "
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
- <img
24
- src="./img/logo.svg"
25
- alt="alphaclaw"
26
- class="mx-auto mb-3"
27
- width="32"
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: rgba(0, 0, 0, 0.3); color: var(--text-muted)"
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
- ? "rgba(82, 94, 122, 0.45)"
52
+ ? "var(--border-strong)"
55
53
  : isActive
56
- ? "rgba(99, 235, 255, 0.9)"
54
+ ? "var(--accent)"
57
55
  : group.id === "pairing"
58
56
  ? isPairingComplete
59
- ? "rgba(99, 235, 255, 0.55)"
60
- : "rgba(82, 94, 122, 0.45)"
57
+ ? "var(--accent-dim)"
58
+ : "var(--border-strong)"
61
59
  : isComplete
62
- ? "rgba(99, 235, 255, 0.55)"
63
- : "rgba(82, 94, 122, 0.45)";
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-white">Setup failed</h3>
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-white" />
87
- <h3 class="text-lg font-semibold text-white">
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>