@chrysb/alphaclaw 0.9.0-beta.7 → 0.9.1-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.
Files changed (70) hide show
  1. package/bin/alphaclaw.js +26 -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 +107 -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 +31 -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 +110 -16
  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/gateway.js +1 -0
  50. package/lib/server/init/register-server-routes.js +0 -8
  51. package/lib/server/init/server-lifecycle.js +2 -0
  52. package/lib/server/model-catalog-cache.js +251 -0
  53. package/lib/server/onboarding/github.js +83 -2
  54. package/lib/server/onboarding/index.js +7 -0
  55. package/lib/server/routes/models.js +14 -23
  56. package/lib/server/routes/nodes.js +9 -23
  57. package/lib/server/routes/system.js +3 -16
  58. package/lib/server/routes/webhooks.js +12 -1
  59. package/lib/server/startup.js +8 -0
  60. package/lib/server/watchdog-notify.js +172 -55
  61. package/lib/server.js +17 -2
  62. package/lib/setup/core-prompts/AGENTS.md +12 -0
  63. package/lib/setup/core-prompts/TOOLS.md +12 -0
  64. package/package.json +2 -2
  65. package/patches/openclaw+2026.4.9.patch +13 -0
  66. package/lib/public/js/components/mcp-tab/index.js +0 -237
  67. package/lib/public/js/components/routes/mcp-route.js +0 -7
  68. package/lib/server/mcp-bridge.js +0 -158
  69. package/lib/server/routes/mcp.js +0 -292
  70. package/patches/openclaw+2026.3.28.patch +0 -13
@@ -34,20 +34,62 @@ const getReleaseUrl = (tag) =>
34
34
  ? `https://github.com/chrysb/alphaclaw/releases/tag/${encodeURIComponent(tag)}`
35
35
  : "https://github.com/chrysb/alphaclaw/releases";
36
36
 
37
+ const VersionSummaryRow = ({
38
+ label = "",
39
+ currentVersion = "",
40
+ latestVersion = "",
41
+ }) => {
42
+ const currentLabel = String(currentVersion || "").trim() || "Unknown";
43
+ const latestLabel = String(latestVersion || "").trim() || "Unknown";
44
+ const changed = currentLabel !== latestLabel;
45
+ return html`
46
+ <div class="ac-surface-inset border border-border rounded-lg px-3 py-2">
47
+ <p class="text-[11px] uppercase tracking-[0.18em] text-fg-muted">${label}</p>
48
+ <p class="mt-1 text-sm text-body">
49
+ ${changed
50
+ ? html`
51
+ <span>${currentLabel}</span>
52
+ <span class="mx-2 text-fg-muted">→</span>
53
+ <span class="font-semibold">${latestLabel}</span>
54
+ `
55
+ : html`<span>${currentLabel}</span>`}
56
+ </p>
57
+ </div>
58
+ `;
59
+ };
60
+
37
61
  export const UpdateModal = ({
38
62
  visible = false,
39
63
  onClose = () => {},
64
+ currentVersion = "",
65
+ currentOpenclawVersion = "",
40
66
  version = "",
67
+ latestOpenclawVersion = "",
68
+ updateStrategy = null,
41
69
  onUpdate = () => {},
42
70
  updating = false,
43
71
  }) => {
44
72
  const requestedTag = useMemo(() => getReleaseTagFromVersion(version), [version]);
73
+ const shouldLoadReleaseNotes =
74
+ visible &&
75
+ String(version || "").trim() &&
76
+ String(currentVersion || "").trim() &&
77
+ String(version || "").trim() !== String(currentVersion || "").trim();
78
+ const canApplyUpdate =
79
+ updateStrategy?.action === "self-update" ||
80
+ updateStrategy?.action === "managed-update";
45
81
  const [loadingNotes, setLoadingNotes] = useState(false);
46
82
  const [notesError, setNotesError] = useState("");
47
83
  const [notesData, setNotesData] = useState(null);
48
84
 
49
85
  useEffect(() => {
50
86
  if (!visible) return;
87
+ if (!shouldLoadReleaseNotes) {
88
+ setLoadingNotes(false);
89
+ setNotesError("");
90
+ setNotesData(null);
91
+ return;
92
+ }
51
93
  let isActive = true;
52
94
  const loadNotes = async () => {
53
95
  setLoadingNotes(true);
@@ -74,12 +116,11 @@ export const UpdateModal = ({
74
116
  return () => {
75
117
  isActive = false;
76
118
  };
77
- }, [visible, requestedTag]);
119
+ }, [visible, requestedTag, shouldLoadReleaseNotes]);
78
120
 
79
121
  const effectiveTag = String(notesData?.tag || requestedTag || "").trim();
80
122
  const effectiveReleaseUrl =
81
123
  String(notesData?.htmlUrl || "").trim() || getReleaseUrl(effectiveTag);
82
- const updateLabel = effectiveTag ? `Update to ${effectiveTag}` : "Update now";
83
124
  const publishedAtLabel = formatPublishedAt(notesData?.publishedAt);
84
125
  const releaseBody = String(notesData?.body || "").trim();
85
126
  const releasePreviewHtml = useMemo(
@@ -90,6 +131,33 @@ export const UpdateModal = ({
90
131
  }),
91
132
  [releaseBody],
92
133
  );
134
+ const strategyLabel = String(updateStrategy?.label || "").trim();
135
+ const strategyDescription = String(updateStrategy?.description || "").trim();
136
+ const strategySteps = Array.isArray(updateStrategy?.steps)
137
+ ? updateStrategy.steps
138
+ : [];
139
+ const templateRepoUrl = String(updateStrategy?.templateRepoUrl || "").trim();
140
+ const showStrategyDetails =
141
+ updateStrategy?.provider === "apex" &&
142
+ updateStrategy?.action === "managed-update"
143
+ ? false
144
+ : Boolean(strategyDescription || strategySteps.length > 0 || templateRepoUrl);
145
+ const primaryActionUrl = String(updateStrategy?.primaryActionUrl || "").trim();
146
+ const primaryLabel = canApplyUpdate
147
+ ? String(updateStrategy?.primaryActionLabel || "").trim() || "Update now"
148
+ : "Done";
149
+ const handlePrimaryAction = () => {
150
+ if (canApplyUpdate) {
151
+ onUpdate();
152
+ return;
153
+ }
154
+ if (primaryActionUrl) {
155
+ try {
156
+ window.open(primaryActionUrl, "_blank", "noopener,noreferrer");
157
+ } catch {}
158
+ }
159
+ onClose();
160
+ };
93
161
 
94
162
  return html`
95
163
  <${ModalShell}
@@ -106,52 +174,107 @@ export const UpdateModal = ({
106
174
  <${CloseIcon} className="w-3.5 h-3.5 text-body" />
107
175
  </button>
108
176
  <div class="space-y-1 pr-10">
109
- <h3 class="text-sm font-semibold">AlphaClaw release notes</h3>
110
- ${publishedAtLabel
111
- ? html`<p class="text-xs text-fg-muted">Published ${publishedAtLabel}</p>`
112
- : null}
177
+ <h3 class="text-sm font-semibold">Update available</h3>
178
+ <p class="text-xs text-fg-muted">
179
+ ${strategyLabel
180
+ ? `Detected deployment target: ${strategyLabel}`
181
+ : "Review the latest bundled versions before updating."}
182
+ </p>
113
183
  </div>
114
- <div class="ac-surface-inset border border-border rounded-lg p-2 overflow-auto min-h-[220px] max-h-[66vh]">
115
- ${loadingNotes
116
- ? html`
117
- <div class="min-h-[200px] flex items-center justify-center text-fg-muted">
118
- <span class="inline-flex items-center gap-2 text-sm">
119
- <${LoadingSpinner} className="h-4 w-4" />
120
- Loading release notes...
121
- </span>
122
- </div>
123
- `
124
- : notesError
184
+
185
+ <div class="grid gap-2 sm:grid-cols-2">
186
+ <${VersionSummaryRow}
187
+ label="AlphaClaw"
188
+ currentVersion=${currentVersion}
189
+ latestVersion=${version}
190
+ />
191
+ <${VersionSummaryRow}
192
+ label="OpenClaw"
193
+ currentVersion=${currentOpenclawVersion}
194
+ latestVersion=${latestOpenclawVersion || currentOpenclawVersion}
195
+ />
196
+ </div>
197
+
198
+ ${shouldLoadReleaseNotes
199
+ ? html`
200
+ ${publishedAtLabel
201
+ ? html`<p class="text-xs text-fg-muted">Published ${publishedAtLabel}</p>`
202
+ : null}
203
+ <div class="ac-surface-inset border border-border rounded-lg p-2 overflow-auto min-h-[220px] max-h-[52vh]">
204
+ ${loadingNotes
205
+ ? html`
206
+ <div class="min-h-[200px] flex items-center justify-center text-fg-muted">
207
+ <span class="inline-flex items-center gap-2 text-sm">
208
+ <${LoadingSpinner} className="h-4 w-4" />
209
+ Loading release notes...
210
+ </span>
211
+ </div>
212
+ `
213
+ : notesError
214
+ ? html`
215
+ <div class="space-y-2">
216
+ <p class="text-sm text-status-error">${notesError}</p>
217
+ <a
218
+ class="ac-tip-link text-xs"
219
+ href=${effectiveReleaseUrl}
220
+ target="_blank"
221
+ rel="noreferrer"
222
+ >View release on GitHub</a
223
+ >
224
+ </div>
225
+ `
226
+ : releaseBody
227
+ ? html`<div
228
+ class="file-viewer-preview release-notes-preview"
229
+ dangerouslySetInnerHTML=${{ __html: releasePreviewHtml }}
230
+ ></div>`
231
+ : html`
232
+ <div class="space-y-2">
233
+ <p class="text-sm text-body">
234
+ No release notes were published for this tag.
235
+ </p>
236
+ <a
237
+ class="ac-tip-link text-xs"
238
+ href=${effectiveReleaseUrl}
239
+ target="_blank"
240
+ rel="noreferrer"
241
+ >Open release on GitHub</a
242
+ >
243
+ </div>
244
+ `}
245
+ </div>
246
+ `
247
+ : null}
248
+
249
+ ${showStrategyDetails &&
250
+ html`
251
+ <div class="ac-surface-inset border border-border rounded-lg p-3 space-y-2">
252
+ ${strategyDescription
253
+ ? html`<p class="text-sm text-body">${strategyDescription}</p>`
254
+ : null}
255
+ ${strategySteps.length > 0
125
256
  ? html`
126
- <div class="space-y-2">
127
- <p class="text-sm text-status-error">${notesError}</p>
128
- <a
129
- class="ac-tip-link text-xs"
130
- href=${effectiveReleaseUrl}
131
- target="_blank"
132
- rel="noreferrer"
133
- >View release on GitHub</a
134
- >
135
- </div>
257
+ <ol class="space-y-2 text-sm text-body list-decimal list-outside ml-6 pl-0">
258
+ ${strategySteps.map(
259
+ (step) => html`<li key=${step}>${step}</li>`,
260
+ )}
261
+ </ol>
136
262
  `
137
- : releaseBody
138
- ? html`<div
139
- class="file-viewer-preview release-notes-preview"
140
- dangerouslySetInnerHTML=${{ __html: releasePreviewHtml }}
141
- ></div>`
142
- : html`
143
- <div class="space-y-2">
144
- <p class="text-sm text-body">No release notes were published for this tag.</p>
145
- <a
146
- class="ac-tip-link text-xs"
147
- href=${effectiveReleaseUrl}
148
- target="_blank"
149
- rel="noreferrer"
150
- >Open release on GitHub</a
151
- >
152
- </div>
153
- `}
154
- </div>
263
+ : null}
264
+ ${templateRepoUrl
265
+ ? html`
266
+ <a
267
+ class="ac-tip-link text-xs block mt-3"
268
+ href=${templateRepoUrl}
269
+ target="_blank"
270
+ rel="noreferrer"
271
+ >View deployment template</a
272
+ >
273
+ `
274
+ : null}
275
+ </div>
276
+ `}
277
+
155
278
  <div class="flex items-center justify-end gap-2 pt-1">
156
279
  <${ActionButton}
157
280
  onClick=${onClose}
@@ -160,12 +283,12 @@ export const UpdateModal = ({
160
283
  disabled=${updating}
161
284
  />
162
285
  <${ActionButton}
163
- onClick=${onUpdate}
164
- tone="warning"
165
- idleLabel=${updateLabel}
166
- loadingLabel="Updating..."
167
- loading=${updating}
168
- disabled=${loadingNotes}
286
+ onClick=${handlePrimaryAction}
287
+ tone=${canApplyUpdate ? "warning" : "neutral"}
288
+ idleLabel=${primaryLabel}
289
+ loadingLabel=${canApplyUpdate ? "Updating..." : primaryLabel}
290
+ loading=${canApplyUpdate && updating}
291
+ disabled=${canApplyUpdate ? loadingNotes : false}
169
292
  />
170
293
  </div>
171
294
  </${ModalShell}>
@@ -17,9 +17,6 @@ export const WatchdogTab = ({
17
17
  restartingGateway = false,
18
18
  onRestartGateway,
19
19
  restartSignal = 0,
20
- openclawUpdateInProgress = false,
21
- onOpenclawVersionActionComplete = () => {},
22
- onOpenclawUpdate,
23
20
  }) => {
24
21
  const state = useWatchdogTab({
25
22
  watchdogStatus,
@@ -37,9 +34,6 @@ export const WatchdogTab = ({
37
34
  watchdogStatus=${state.currentWatchdogStatus}
38
35
  onRepair=${state.onRepair}
39
36
  repairing=${state.isRepairInProgress}
40
- openclawUpdateInProgress=${openclawUpdateInProgress}
41
- onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
42
- onOpenclawUpdate=${onOpenclawUpdate}
43
37
  />
44
38
 
45
39
  <${WatchdogResourcesCard}
@@ -102,12 +102,10 @@ export const Welcome = ({ onComplete, acVersion }) => {
102
102
  error=${state.formError}
103
103
  step=${state.step}
104
104
  totalGroups=${kWelcomeGroups.length}
105
- currentGroupValid=${state.currentGroupValid}
106
105
  goBack=${actions.goBack}
107
106
  goNext=${actions.goNext}
108
107
  loading=${state.loading}
109
108
  githubStepLoading=${state.githubStepLoading}
110
- allValid=${state.allValid}
111
109
  handleSubmit=${actions.handleSubmit}
112
110
  />
113
111
  `}
@@ -6,6 +6,7 @@ import {
6
6
  applyImport,
7
7
  fetchModels,
8
8
  } from "../../lib/api.js";
9
+ import { useCachedFetch } from "../../hooks/use-cached-fetch.js";
9
10
  import {
10
11
  getModelProvider,
11
12
  getAuthProviderFromModelProvider,
@@ -13,8 +14,16 @@ import {
13
14
  getVisibleAiFieldKeys,
14
15
  kProviderAuthFields,
15
16
  } from "../../lib/model-config.js";
17
+ import {
18
+ getInitialOnboardingModelKey,
19
+ getModelCatalogModels,
20
+ kModelCatalogCacheKey,
21
+ preloadModelCatalog,
22
+ } from "../../lib/model-catalog.js";
16
23
  import {
17
24
  kWelcomeGroups,
25
+ getWelcomeGroupError,
26
+ findFirstInvalidWelcomeGroup,
18
27
  isValidGithubRepoInput,
19
28
  kGithubFlowFresh,
20
29
  kGithubFlowImport,
@@ -76,11 +85,18 @@ const normalizePlaceholderReview = (review) => {
76
85
  export const useWelcome = ({ onComplete }) => {
77
86
  const kSetupStepIndex = kWelcomeGroups.length;
78
87
  const kPairingStepIndex = kSetupStepIndex + 1;
79
- const { vals, setVals, setValue, step, setStep, setupError, setSetupError } =
80
- useWelcomeStorage({
81
- kSetupStepIndex,
82
- kPairingStepIndex,
83
- });
88
+ const {
89
+ vals,
90
+ setVals,
91
+ setValue: setStoredValue,
92
+ step,
93
+ setStep,
94
+ setupError,
95
+ setSetupError,
96
+ } = useWelcomeStorage({
97
+ kSetupStepIndex,
98
+ kPairingStepIndex,
99
+ });
84
100
  const [models, setModels] = useState([]);
85
101
  const [modelsLoading, setModelsLoading] = useState(true);
86
102
  const [modelsError, setModelsError] = useState(null);
@@ -110,6 +126,19 @@ export const useWelcome = ({ onComplete }) => {
110
126
  const [importScanResult, setImportScanResult] = useState(null);
111
127
  const [importScanning, setImportScanning] = useState(false);
112
128
  const [importError, setImportError] = useState(null);
129
+ const modelsFetchState = useCachedFetch(kModelCatalogCacheKey, fetchModels, {
130
+ maxAgeMs: 30000,
131
+ });
132
+
133
+ useEffect(() => {
134
+ // Warm the real catalog immediately so the AI step usually opens ready.
135
+ preloadModelCatalog().catch(() => {});
136
+ }, []);
137
+
138
+ const setValue = (key, value) => {
139
+ if (formError) setFormError(null);
140
+ setStoredValue(key, value);
141
+ };
113
142
 
114
143
  const setImportStep = (nextStep) => {
115
144
  setImportStepState(nextStep);
@@ -129,21 +158,54 @@ export const useWelcome = ({ onComplete }) => {
129
158
  };
130
159
 
131
160
  useEffect(() => {
132
- fetchModels()
133
- .then((result) => {
134
- const list = Array.isArray(result.models) ? result.models : [];
135
- const featured = getFeaturedModels(list);
136
- setModels(list);
137
- if (!vals.MODEL_KEY && list.length > 0) {
138
- const defaultModel = featured[0] || list[0];
139
- setVals((prev) => ({ ...prev, MODEL_KEY: defaultModel.key }));
140
- }
141
- })
142
- .catch(() => setModelsError("Failed to load models"))
143
- .finally(() => setModelsLoading(false));
144
- }, []);
161
+ const list = getModelCatalogModels(modelsFetchState.data);
162
+ if (!modelsFetchState.data) return;
163
+ setModels(list);
164
+ setModelsError(list.length > 0 ? null : "No models found");
165
+ const defaultModelKey = getInitialOnboardingModelKey({
166
+ catalog: list,
167
+ currentModelKey: vals.MODEL_KEY,
168
+ });
169
+ if (!vals.MODEL_KEY && defaultModelKey) {
170
+ setVals((prev) => ({ ...prev, MODEL_KEY: defaultModelKey }));
171
+ }
172
+ }, [modelsFetchState.data, setVals, vals.MODEL_KEY]);
173
+
174
+ useEffect(() => {
175
+ const hasModels = getModelCatalogModels(modelsFetchState.data).length > 0;
176
+ setModelsLoading(modelsFetchState.loading && !hasModels);
177
+ }, [modelsFetchState.data, modelsFetchState.loading]);
178
+
179
+ useEffect(() => {
180
+ if (!modelsFetchState.error) return;
181
+ setModelsError("Failed to load models");
182
+ setModelsLoading(false);
183
+ }, [modelsFetchState.error]);
184
+
185
+ const getValidationContext = (currentVals = {}) => {
186
+ const currentSelectedProvider = getModelProvider(
187
+ String(currentVals.MODEL_KEY || "").trim(),
188
+ );
189
+ const currentSelectedAuthProvider =
190
+ getAuthProviderFromModelProvider(currentSelectedProvider);
191
+ const currentProviderAuthFields =
192
+ kProviderAuthFields[currentSelectedAuthProvider] || [];
193
+ const currentHasAi =
194
+ currentSelectedProvider === "openai-codex"
195
+ ? !!codexStatus.connected
196
+ : currentProviderAuthFields.some((field) =>
197
+ !!String(currentVals[field.key] || "").trim(),
198
+ );
199
+
200
+ return {
201
+ hasAi: currentHasAi,
202
+ selectedProvider: currentSelectedProvider,
203
+ codexLoading,
204
+ };
205
+ };
145
206
 
146
- const selectedProvider = getModelProvider(vals.MODEL_KEY);
207
+ const validationContext = getValidationContext(vals);
208
+ const { selectedProvider, hasAi } = validationContext;
147
209
  const placeholderReview = normalizePlaceholderReview(
148
210
  vals[kImportPlaceholderReviewKey],
149
211
  );
@@ -164,23 +226,10 @@ export const useWelcome = ({ onComplete }) => {
164
226
  const canToggleFullCatalog =
165
227
  featuredModels.length > 0 && models.length > featuredModels.length;
166
228
  const visibleAiFieldKeys = getVisibleAiFieldKeys(selectedProvider);
167
- const selectedAuthProvider = getAuthProviderFromModelProvider(selectedProvider);
168
- const selectedProviderAuthFields = kProviderAuthFields[selectedAuthProvider] || [];
169
- const hasAi =
170
- selectedProvider === "openai-codex"
171
- ? !!codexStatus.connected
172
- : selectedProviderAuthFields.some(
173
- (field) => !!String(vals[field.key] || "").trim(),
174
- );
175
-
176
- const allValid = kWelcomeGroups.every((group) => group.validate(vals, { hasAi }));
177
229
  const isPreStep = step === -1;
178
230
  const isSetupStep = step === kSetupStepIndex;
179
231
  const isPairingStep = step === kPairingStepIndex;
180
232
  const activeGroup = step >= 0 && step < kSetupStepIndex ? kWelcomeGroups[step] : null;
181
- const currentGroupValid = activeGroup
182
- ? activeGroup.validate(vals, { hasAi })
183
- : false;
184
233
  const selectedPairingChannel = String(
185
234
  vals[kPairingChannelKey] || getPreferredPairingChannel(vals),
186
235
  );
@@ -202,7 +251,21 @@ export const useWelcome = ({ onComplete }) => {
202
251
  const handleSubmit = async () => {
203
252
  const { normalizedVals, didChange } = normalizeOnboardingVals(vals);
204
253
  if (didChange) setVals(normalizedVals);
205
- if (!kWelcomeGroups.every((group) => group.validate(normalizedVals, { hasAi }))) {
254
+ const submitValidationContext = getValidationContext(normalizedVals);
255
+ const invalidGroup = findFirstInvalidWelcomeGroup(
256
+ normalizedVals,
257
+ submitValidationContext,
258
+ );
259
+ if (invalidGroup) {
260
+ setFormError(
261
+ getWelcomeGroupError(
262
+ invalidGroup.id,
263
+ normalizedVals,
264
+ submitValidationContext,
265
+ ),
266
+ );
267
+ setSetupError(null);
268
+ setStep(kWelcomeGroups.findIndex((group) => group.id === invalidGroup.id));
206
269
  return;
207
270
  }
208
271
  if (loading) return;
@@ -309,7 +372,17 @@ export const useWelcome = ({ onComplete }) => {
309
372
  const goNext = async () => {
310
373
  const { normalizedVals, didChange } = normalizeOnboardingVals(vals);
311
374
  if (didChange) setVals(normalizedVals);
312
- if (!activeGroup || !activeGroup.validate(normalizedVals, { hasAi })) return;
375
+ if (!activeGroup) return;
376
+ const stepValidationContext = getValidationContext(normalizedVals);
377
+ const stepValidationError = getWelcomeGroupError(
378
+ activeGroup.id,
379
+ normalizedVals,
380
+ stepValidationContext,
381
+ );
382
+ if (stepValidationError) {
383
+ setFormError(stepValidationError);
384
+ return;
385
+ }
313
386
  setFormError(null);
314
387
  if (activeGroup.id === "github") {
315
388
  const githubFlow = normalizedVals._GITHUB_FLOW || kGithubFlowFresh;
@@ -545,12 +618,10 @@ export const useWelcome = ({ onComplete }) => {
545
618
  canToggleFullCatalog,
546
619
  visibleAiFieldKeys,
547
620
  hasAi,
548
- allValid,
549
621
  isPreStep,
550
622
  isSetupStep,
551
623
  isPairingStep,
552
624
  activeGroup,
553
- currentGroupValid,
554
625
  selectedPairingChannel,
555
626
  placeholderReview,
556
627
  isImportStep,
@@ -10,7 +10,6 @@ import {
10
10
  restartGateway,
11
11
  fetchWatchdogStatus,
12
12
  fetchDoctorStatus,
13
- updateOpenclaw,
14
13
  subscribeStatusEvents,
15
14
  } from "../lib/api.js";
16
15
  import { shouldRequireRestartForBrowsePath } from "../lib/browse-restart-policy.js";
@@ -22,8 +21,11 @@ export const useAppShellController = ({ location = "" } = {}) => {
22
21
  const [onboarded, setOnboarded] = useState(null);
23
22
  const [authEnabled, setAuthEnabled] = useState(false);
24
23
  const [acVersion, setAcVersion] = useState(null);
24
+ const [acCurrentOpenclawVersion, setAcCurrentOpenclawVersion] = useState(null);
25
25
  const [acLatest, setAcLatest] = useState(null);
26
+ const [acLatestOpenclawVersion, setAcLatestOpenclawVersion] = useState(null);
26
27
  const [acHasUpdate, setAcHasUpdate] = useState(false);
28
+ const [acUpdateStrategy, setAcUpdateStrategy] = useState(null);
27
29
  const [acUpdating, setAcUpdating] = useState(false);
28
30
  const [restartRequired, setRestartRequired] = useState(false);
29
31
  const [browseRestartRequired, setBrowseRestartRequired] = useState(false);
@@ -31,7 +33,6 @@ export const useAppShellController = ({ location = "" } = {}) => {
31
33
  const [gatewayRestartSignal, setGatewayRestartSignal] = useState(0);
32
34
  const [statusPollCadenceMs, setStatusPollCadenceMs] = useState(15000);
33
35
  const [statusPollingGraceElapsed, setStatusPollingGraceElapsed] = useState(false);
34
- const [openclawUpdateInProgress, setOpenclawUpdateInProgress] = useState(false);
35
36
  const [statusStreamConnected, setStatusStreamConnected] = useState(false);
36
37
  const [statusStreamStatus, setStatusStreamStatus] = useState(null);
37
38
  const [statusStreamWatchdog, setStatusStreamWatchdog] = useState(null);
@@ -138,8 +139,11 @@ export const useAppShellController = ({ location = "" } = {}) => {
138
139
  const data = await fetchAlphaclawVersion(refresh);
139
140
  if (!active) return;
140
141
  setAcVersion(data.currentVersion || null);
142
+ setAcCurrentOpenclawVersion(data.currentOpenclawVersion || null);
141
143
  setAcLatest(data.latestVersion || null);
144
+ setAcLatestOpenclawVersion(data.latestOpenclawVersion || null);
142
145
  setAcHasUpdate(!!data.hasUpdate);
146
+ setAcUpdateStrategy(data.updateStrategy || null);
143
147
  } catch {}
144
148
  };
145
149
  check(true);
@@ -236,40 +240,19 @@ export const useAppShellController = ({ location = "" } = {}) => {
236
240
  }
237
241
  }, [refreshRestartStatus, refreshSharedStatuses, restartingGateway]);
238
242
 
239
- const handleOpenclawUpdate = useCallback(async () => {
240
- if (openclawUpdateInProgress) {
241
- return { ok: false, error: "OpenClaw update already in progress" };
242
- }
243
- setOpenclawUpdateInProgress(true);
244
- try {
245
- const data = await updateOpenclaw();
246
- return data;
247
- } finally {
248
- setOpenclawUpdateInProgress(false);
249
- refreshSharedStatuses();
250
- setTimeout(refreshSharedStatuses, 1200);
251
- setTimeout(refreshSharedStatuses, 3500);
252
- setTimeout(refreshRestartStatus, 1200);
253
- }
254
- }, [openclawUpdateInProgress, refreshRestartStatus, refreshSharedStatuses]);
255
-
256
- const handleOpenclawVersionActionComplete = useCallback(
257
- ({ type }) => {
258
- if (type !== "update") return;
259
- refreshSharedStatuses();
260
- setTimeout(refreshSharedStatuses, 1200);
261
- },
262
- [refreshSharedStatuses],
263
- );
264
-
265
243
  const handleAcUpdate = useCallback(async () => {
266
244
  if (acUpdating) return;
267
245
  setAcUpdating(true);
268
246
  try {
269
247
  const data = await updateAlphaclaw();
270
248
  if (data.ok) {
271
- showToast("AlphaClaw updated — restarting...", "success");
272
- setTimeout(() => window.location.reload(), 5000);
249
+ showToast(
250
+ data.managedUpdate
251
+ ? "Deployment update started — reconnecting..."
252
+ : "AlphaClaw updated — restarting...",
253
+ "success",
254
+ );
255
+ setTimeout(() => window.location.reload(), data.managedUpdate ? 8000 : 5000);
273
256
  } else {
274
257
  showToast(data.error || "AlphaClaw update failed", "error");
275
258
  setAcUpdating(false);
@@ -296,13 +279,15 @@ export const useAppShellController = ({ location = "" } = {}) => {
296
279
  state: {
297
280
  acHasUpdate,
298
281
  acLatest,
282
+ acLatestOpenclawVersion,
283
+ acCurrentOpenclawVersion,
284
+ acUpdateStrategy,
299
285
  acUpdating,
300
286
  acVersion,
301
287
  authEnabled,
302
288
  gatewayRestartSignal,
303
289
  isAnyRestartRequired,
304
290
  onboarded,
305
- openclawUpdateInProgress,
306
291
  restartingGateway,
307
292
  sharedDoctorStatus,
308
293
  sharedStatus,
@@ -312,8 +297,6 @@ export const useAppShellController = ({ location = "" } = {}) => {
312
297
  handleAcUpdate,
313
298
  handleGatewayRestart,
314
299
  handleOnboardingComplete: () => setOnboarded(true),
315
- handleOpenclawUpdate,
316
- handleOpenclawVersionActionComplete,
317
300
  refreshSharedStatuses,
318
301
  dismissRestartBanner,
319
302
  setRestartRequired,
@@ -469,17 +469,6 @@ export async function fetchDashboardUrl() {
469
469
  return res.json();
470
470
  }
471
471
 
472
- export async function fetchOpenclawVersion(refresh = false) {
473
- const query = refresh ? "?refresh=1" : "";
474
- const res = await authFetch(`/api/openclaw/version${query}`);
475
- return res.json();
476
- }
477
-
478
- export async function updateOpenclaw() {
479
- const res = await authFetch("/api/openclaw/update", { method: "POST" });
480
- return res.json();
481
- }
482
-
483
472
  export async function fetchAlphaclawVersion(refresh = false) {
484
473
  const query = refresh ? "?refresh=1" : "";
485
474
  const res = await authFetch(`/api/alphaclaw/version${query}`);
@@ -1365,20 +1354,3 @@ export const syncBrowseChanges = async (message = "") => {
1365
1354
  });
1366
1355
  return parseJsonOrThrow(res, "Could not sync changes");
1367
1356
  };
1368
-
1369
- // ── MCP ──────────────────────────────────────────────────────────
1370
-
1371
- export const fetchMcpInfo = async () => {
1372
- const res = await authFetch("/api/mcp/info");
1373
- return res.json();
1374
- };
1375
-
1376
- export const startMcpBridge = async () => {
1377
- const res = await authFetch("/api/mcp/start", { method: "POST" });
1378
- return res.json();
1379
- };
1380
-
1381
- export const stopMcpBridge = async () => {
1382
- const res = await authFetch("/api/mcp/stop", { method: "POST" });
1383
- return res.json();
1384
- };