@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
@@ -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,15 @@ import {
13
14
  getVisibleAiFieldKeys,
14
15
  kProviderAuthFields,
15
16
  } from "../../lib/model-config.js";
17
+ import {
18
+ getInitialOnboardingModelKey,
19
+ getModelCatalogModels,
20
+ kModelCatalogCacheKey,
21
+ } from "../../lib/model-catalog.js";
16
22
  import {
17
23
  kWelcomeGroups,
24
+ getWelcomeGroupError,
25
+ findFirstInvalidWelcomeGroup,
18
26
  isValidGithubRepoInput,
19
27
  kGithubFlowFresh,
20
28
  kGithubFlowImport,
@@ -76,11 +84,18 @@ const normalizePlaceholderReview = (review) => {
76
84
  export const useWelcome = ({ onComplete }) => {
77
85
  const kSetupStepIndex = kWelcomeGroups.length;
78
86
  const kPairingStepIndex = kSetupStepIndex + 1;
79
- const { vals, setVals, setValue, step, setStep, setupError, setSetupError } =
80
- useWelcomeStorage({
81
- kSetupStepIndex,
82
- kPairingStepIndex,
83
- });
87
+ const {
88
+ vals,
89
+ setVals,
90
+ setValue: setStoredValue,
91
+ step,
92
+ setStep,
93
+ setupError,
94
+ setSetupError,
95
+ } = useWelcomeStorage({
96
+ kSetupStepIndex,
97
+ kPairingStepIndex,
98
+ });
84
99
  const [models, setModels] = useState([]);
85
100
  const [modelsLoading, setModelsLoading] = useState(true);
86
101
  const [modelsError, setModelsError] = useState(null);
@@ -110,6 +125,14 @@ export const useWelcome = ({ onComplete }) => {
110
125
  const [importScanResult, setImportScanResult] = useState(null);
111
126
  const [importScanning, setImportScanning] = useState(false);
112
127
  const [importError, setImportError] = useState(null);
128
+ const modelsFetchState = useCachedFetch(kModelCatalogCacheKey, fetchModels, {
129
+ maxAgeMs: 30000,
130
+ });
131
+
132
+ const setValue = (key, value) => {
133
+ if (formError) setFormError(null);
134
+ setStoredValue(key, value);
135
+ };
113
136
 
114
137
  const setImportStep = (nextStep) => {
115
138
  setImportStepState(nextStep);
@@ -129,21 +152,54 @@ export const useWelcome = ({ onComplete }) => {
129
152
  };
130
153
 
131
154
  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
- }, []);
155
+ const list = getModelCatalogModels(modelsFetchState.data);
156
+ if (!modelsFetchState.data) return;
157
+ setModels(list);
158
+ setModelsError(list.length > 0 ? null : "No models found");
159
+ const defaultModelKey = getInitialOnboardingModelKey({
160
+ catalog: list,
161
+ currentModelKey: vals.MODEL_KEY,
162
+ });
163
+ if (!vals.MODEL_KEY && defaultModelKey) {
164
+ setVals((prev) => ({ ...prev, MODEL_KEY: defaultModelKey }));
165
+ }
166
+ }, [modelsFetchState.data, setVals, vals.MODEL_KEY]);
167
+
168
+ useEffect(() => {
169
+ const hasModels = getModelCatalogModels(modelsFetchState.data).length > 0;
170
+ setModelsLoading(modelsFetchState.loading && !hasModels);
171
+ }, [modelsFetchState.data, modelsFetchState.loading]);
172
+
173
+ useEffect(() => {
174
+ if (!modelsFetchState.error) return;
175
+ setModelsError("Failed to load models");
176
+ setModelsLoading(false);
177
+ }, [modelsFetchState.error]);
178
+
179
+ const getValidationContext = (currentVals = {}) => {
180
+ const currentSelectedProvider = getModelProvider(
181
+ String(currentVals.MODEL_KEY || "").trim(),
182
+ );
183
+ const currentSelectedAuthProvider =
184
+ getAuthProviderFromModelProvider(currentSelectedProvider);
185
+ const currentProviderAuthFields =
186
+ kProviderAuthFields[currentSelectedAuthProvider] || [];
187
+ const currentHasAi =
188
+ currentSelectedProvider === "openai-codex"
189
+ ? !!codexStatus.connected
190
+ : currentProviderAuthFields.some((field) =>
191
+ !!String(currentVals[field.key] || "").trim(),
192
+ );
145
193
 
146
- const selectedProvider = getModelProvider(vals.MODEL_KEY);
194
+ return {
195
+ hasAi: currentHasAi,
196
+ selectedProvider: currentSelectedProvider,
197
+ codexLoading,
198
+ };
199
+ };
200
+
201
+ const validationContext = getValidationContext(vals);
202
+ const { selectedProvider, hasAi } = validationContext;
147
203
  const placeholderReview = normalizePlaceholderReview(
148
204
  vals[kImportPlaceholderReviewKey],
149
205
  );
@@ -164,23 +220,10 @@ export const useWelcome = ({ onComplete }) => {
164
220
  const canToggleFullCatalog =
165
221
  featuredModels.length > 0 && models.length > featuredModels.length;
166
222
  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
223
  const isPreStep = step === -1;
178
224
  const isSetupStep = step === kSetupStepIndex;
179
225
  const isPairingStep = step === kPairingStepIndex;
180
226
  const activeGroup = step >= 0 && step < kSetupStepIndex ? kWelcomeGroups[step] : null;
181
- const currentGroupValid = activeGroup
182
- ? activeGroup.validate(vals, { hasAi })
183
- : false;
184
227
  const selectedPairingChannel = String(
185
228
  vals[kPairingChannelKey] || getPreferredPairingChannel(vals),
186
229
  );
@@ -202,7 +245,21 @@ export const useWelcome = ({ onComplete }) => {
202
245
  const handleSubmit = async () => {
203
246
  const { normalizedVals, didChange } = normalizeOnboardingVals(vals);
204
247
  if (didChange) setVals(normalizedVals);
205
- if (!kWelcomeGroups.every((group) => group.validate(normalizedVals, { hasAi }))) {
248
+ const submitValidationContext = getValidationContext(normalizedVals);
249
+ const invalidGroup = findFirstInvalidWelcomeGroup(
250
+ normalizedVals,
251
+ submitValidationContext,
252
+ );
253
+ if (invalidGroup) {
254
+ setFormError(
255
+ getWelcomeGroupError(
256
+ invalidGroup.id,
257
+ normalizedVals,
258
+ submitValidationContext,
259
+ ),
260
+ );
261
+ setSetupError(null);
262
+ setStep(kWelcomeGroups.findIndex((group) => group.id === invalidGroup.id));
206
263
  return;
207
264
  }
208
265
  if (loading) return;
@@ -309,7 +366,17 @@ export const useWelcome = ({ onComplete }) => {
309
366
  const goNext = async () => {
310
367
  const { normalizedVals, didChange } = normalizeOnboardingVals(vals);
311
368
  if (didChange) setVals(normalizedVals);
312
- if (!activeGroup || !activeGroup.validate(normalizedVals, { hasAi })) return;
369
+ if (!activeGroup) return;
370
+ const stepValidationContext = getValidationContext(normalizedVals);
371
+ const stepValidationError = getWelcomeGroupError(
372
+ activeGroup.id,
373
+ normalizedVals,
374
+ stepValidationContext,
375
+ );
376
+ if (stepValidationError) {
377
+ setFormError(stepValidationError);
378
+ return;
379
+ }
313
380
  setFormError(null);
314
381
  if (activeGroup.id === "github") {
315
382
  const githubFlow = normalizedVals._GITHUB_FLOW || kGithubFlowFresh;
@@ -545,12 +612,10 @@ export const useWelcome = ({ onComplete }) => {
545
612
  canToggleFullCatalog,
546
613
  visibleAiFieldKeys,
547
614
  hasAi,
548
- allValid,
549
615
  isPreStep,
550
616
  isSetupStep,
551
617
  isPairingStep,
552
618
  activeGroup,
553
- currentGroupValid,
554
619
  selectedPairingChannel,
555
620
  placeholderReview,
556
621
  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
- };