@chrysb/alphaclaw 0.8.5 → 0.8.7-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/bin/alphaclaw.js +56 -20
  2. package/lib/public/css/explorer.css +48 -0
  3. package/lib/public/css/shell.css +149 -0
  4. package/lib/public/css/tailwind.generated.css +1 -1
  5. package/lib/public/css/theme.css +265 -0
  6. package/lib/public/dist/app.bundle.js +2441 -2352
  7. package/lib/public/js/app.js +7 -0
  8. package/lib/public/js/components/gateway.js +6 -3
  9. package/lib/public/js/components/general/index.js +2 -0
  10. package/lib/public/js/components/icons.js +38 -0
  11. package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
  12. package/lib/public/js/components/models-tab/use-models.js +74 -9
  13. package/lib/public/js/components/models.js +52 -37
  14. package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
  15. package/lib/public/js/components/onboarding/welcome-config.js +76 -10
  16. package/lib/public/js/components/onboarding/welcome-form-step.js +31 -11
  17. package/lib/public/js/components/onboarding/welcome-header.js +12 -14
  18. package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
  19. package/lib/public/js/components/providers.js +53 -42
  20. package/lib/public/js/components/routes/general-route.js +2 -0
  21. package/lib/public/js/components/routes/watchdog-route.js +2 -0
  22. package/lib/public/js/components/sidebar.js +29 -8
  23. package/lib/public/js/components/theme-toggle.js +113 -0
  24. package/lib/public/js/components/update-modal-helpers.js +12 -0
  25. package/lib/public/js/components/update-modal.js +2 -1
  26. package/lib/public/js/components/watchdog-tab/index.js +2 -0
  27. package/lib/public/js/components/welcome/index.js +1 -2
  28. package/lib/public/js/components/welcome/use-welcome.js +153 -38
  29. package/lib/public/js/hooks/use-app-shell-controller.js +33 -9
  30. package/lib/public/js/lib/api.js +35 -0
  31. package/lib/public/js/lib/codex-oauth-window.js +22 -0
  32. package/lib/public/js/lib/model-catalog.js +20 -0
  33. package/lib/public/js/lib/storage-keys.js +1 -1
  34. package/lib/public/login.html +8 -4
  35. package/lib/public/setup.html +9 -0
  36. package/lib/server/alphaclaw-version.js +30 -127
  37. package/lib/server/db/webhooks/index.js +48 -8
  38. package/lib/server/model-catalog-cache.js +251 -0
  39. package/lib/server/openclaw-version.js +59 -130
  40. package/lib/server/pending-alphaclaw-update.js +71 -0
  41. package/lib/server/pending-openclaw-update.js +71 -0
  42. package/lib/server/routes/models.js +14 -23
  43. package/lib/server/routes/system.js +6 -1
  44. package/lib/server/routes/webhooks.js +12 -1
  45. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { h } from "preact";
2
- import { useEffect, useMemo, useRef, useState } from "preact/hooks";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
3
3
  import htm from "htm";
4
4
  import {
5
5
  AddLineIcon,
@@ -22,6 +22,7 @@ import { OverflowMenu, OverflowMenuItem } from "./overflow-menu.js";
22
22
  import { UpdateActionButton } from "./update-action-button.js";
23
23
  import { SidebarGitPanel } from "./sidebar-git-panel.js";
24
24
  import { UpdateModal } from "./update-modal.js";
25
+ import { createUpdateModalSubmitHandler } from "./update-modal-helpers.js";
25
26
  import {
26
27
  readUiSettings,
27
28
  updateUiSettings,
@@ -33,6 +34,7 @@ import {
33
34
  getSessionDisplayLabel,
34
35
  getSessionRowKey,
35
36
  } from "../lib/session-keys.js";
37
+ import { ThemeToggle } from "./theme-toggle.js";
36
38
 
37
39
  const html = htm.bind(h);
38
40
  const kBrowseBottomPanelUiSettingKey = "browseBottomPanelHeightPx";
@@ -109,6 +111,7 @@ export const AppSidebar = ({
109
111
  onPreviewBrowseFile = () => {},
110
112
  acHasUpdate = false,
111
113
  acLatest = "",
114
+ acRestarting = false,
112
115
  acUpdating = false,
113
116
  onAcUpdate = () => {},
114
117
  agents = [],
@@ -181,6 +184,19 @@ export const AppSidebar = ({
181
184
  writeUiSettings(settings);
182
185
  }, [browseBottomPanelHeightPx]);
183
186
 
187
+ const handleUpdateModalClose = useCallback(() => {
188
+ if (acUpdating) return;
189
+ setUpdateModalOpen(false);
190
+ }, [acUpdating]);
191
+
192
+ const handleUpdateModalSubmit = useCallback(
193
+ createUpdateModalSubmitHandler({
194
+ onClose: () => setUpdateModalOpen(false),
195
+ onUpdate: onAcUpdate,
196
+ }),
197
+ [onAcUpdate],
198
+ );
199
+
184
200
  const getClampedBrowseBottomPanelHeight = (value) => {
185
201
  const layoutElement = browseLayoutRef.current;
186
202
  if (!layoutElement) return value;
@@ -246,8 +262,14 @@ export const AppSidebar = ({
246
262
  return html`
247
263
  <div class=${`app-sidebar ${mobileSidebarOpen ? "mobile-open" : ""}`}>
248
264
  <div class="sidebar-brand">
249
- <img src="./img/logo.svg" alt="" width="20" height="20" />
265
+ <span
266
+ class="ac-logo-mark"
267
+ style="--ac-logo-width: 20px; --ac-logo-height: 20px;"
268
+ aria-hidden="true"
269
+ ></span>
250
270
  <span><span style="color: var(--accent)">alpha</span>claw</span>
271
+ <span style="margin-left: auto; display: inline-flex; align-items: center; gap: 4px;">
272
+ <${ThemeToggle} />
251
273
  ${authEnabled && html`
252
274
  <${OverflowMenu}
253
275
  open=${menuOpen}
@@ -262,6 +284,7 @@ export const AppSidebar = ({
262
284
  </${OverflowMenuItem}>
263
285
  </${OverflowMenu}>
264
286
  `}
287
+ </span>
265
288
  </div>
266
289
  <div class="sidebar-tabs">
267
290
  <button
@@ -356,7 +379,7 @@ export const AppSidebar = ({
356
379
  loading=${acUpdating}
357
380
  warning=${true}
358
381
  idleLabel=${`Update to v${acLatest}`}
359
- loadingLabel="Updating..."
382
+ loadingLabel=${acRestarting ? "Restarting..." : "Updating..."}
360
383
  className="w-full justify-center"
361
384
  />
362
385
  `
@@ -481,13 +504,11 @@ export const AppSidebar = ({
481
504
  </div>
482
505
  <${UpdateModal}
483
506
  visible=${updateModalOpen}
484
- onClose=${() => {
485
- if (acUpdating) return;
486
- setUpdateModalOpen(false);
487
- }}
507
+ onClose=${handleUpdateModalClose}
488
508
  version=${acLatest}
489
- onUpdate=${onAcUpdate}
509
+ onUpdate=${handleUpdateModalSubmit}
490
510
  updating=${acUpdating}
511
+ updateLoadingLabel=${acRestarting ? "Restarting..." : "Updating..."}
491
512
  />
492
513
  </div>
493
514
  `;
@@ -0,0 +1,113 @@
1
+ import { h } from "preact";
2
+ import { useEffect, useRef, useState } from "preact/hooks";
3
+ import htm from "htm";
4
+ import { ComputerLineIcon, MoonIcon, SunIcon } from "./icons.js";
5
+ import { kThemeStorageKey } from "../lib/storage-keys.js";
6
+
7
+ const html = htm.bind(h);
8
+
9
+ const kOptions = [
10
+ { id: "dark", label: "Dark", Icon: MoonIcon },
11
+ { id: "light", label: "Light", Icon: SunIcon },
12
+ { id: "system", label: "System", Icon: ComputerLineIcon },
13
+ ];
14
+
15
+ /** Map a preference to the icon component shown on the trigger button. */
16
+ const kPrefIcon = { dark: MoonIcon, light: SunIcon, system: ComputerLineIcon };
17
+
18
+ /** Resolve a preference string to an effective "dark" | "light" value. */
19
+ const resolveEffective = (pref) => {
20
+ if (pref === "dark" || pref === "light") return pref;
21
+ try {
22
+ return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
23
+ } catch {
24
+ return "dark";
25
+ }
26
+ };
27
+
28
+ /** Read the stored preference. Falls back to "dark" (not OS). */
29
+ const readPreference = () => {
30
+ try {
31
+ const saved = localStorage.getItem(kThemeStorageKey);
32
+ if (saved === "dark" || saved === "light" || saved === "system") return saved;
33
+ } catch {}
34
+ return "dark";
35
+ };
36
+
37
+ const applyEffective = (effective) => {
38
+ document.documentElement.dataset.theme = effective;
39
+ };
40
+
41
+ const savePreference = (pref) => {
42
+ try { localStorage.setItem(kThemeStorageKey, pref); } catch {}
43
+ };
44
+
45
+ export const ThemeToggle = () => {
46
+ const [pref, setPref] = useState(readPreference);
47
+ const [open, setOpen] = useState(false);
48
+ const menuRef = useRef(null);
49
+
50
+ // Apply effective theme whenever preference changes (and listen for OS changes when "system").
51
+ useEffect(() => {
52
+ applyEffective(resolveEffective(pref));
53
+
54
+ if (pref !== "system") return;
55
+
56
+ const mql = window.matchMedia("(prefers-color-scheme: light)");
57
+ const onChange = () => applyEffective(resolveEffective("system"));
58
+ mql.addEventListener("change", onChange);
59
+ return () => mql.removeEventListener("change", onChange);
60
+ }, [pref]);
61
+
62
+ // Close dropdown on outside click.
63
+ useEffect(() => {
64
+ if (!open) return;
65
+ const handler = (e) => {
66
+ if (menuRef.current && !menuRef.current.contains(e.target)) setOpen(false);
67
+ };
68
+ window.addEventListener("click", handler, true);
69
+ return () => window.removeEventListener("click", handler, true);
70
+ }, [open]);
71
+
72
+ const select = (id) => {
73
+ setPref(id);
74
+ savePreference(id);
75
+ applyEffective(resolveEffective(id));
76
+ setOpen(false);
77
+ };
78
+
79
+ const TriggerIcon = kPrefIcon[pref] || MoonIcon;
80
+
81
+ return html`
82
+ <div
83
+ ref=${menuRef}
84
+ class="theme-toggle-menu"
85
+ >
86
+ <button
87
+ type="button"
88
+ onclick=${() => setOpen((o) => !o)}
89
+ title="Theme"
90
+ aria-label="Toggle theme"
91
+ aria-expanded=${open}
92
+ class="theme-toggle-trigger"
93
+ >
94
+ <${TriggerIcon} className="w-3.5 h-3.5" />
95
+ </button>
96
+ ${open && html`
97
+ <div class="theme-toggle-dropdown">
98
+ ${kOptions.map(({ id, label, Icon }) => html`
99
+ <button
100
+ key=${id}
101
+ type="button"
102
+ class="theme-toggle-option ${pref === id ? "active" : ""}"
103
+ onclick=${() => select(id)}
104
+ >
105
+ <${Icon} className="w-3.5 h-3.5" />
106
+ <span>${label}</span>
107
+ </button>
108
+ `)}
109
+ </div>
110
+ `}
111
+ </div>
112
+ `;
113
+ };
@@ -0,0 +1,12 @@
1
+ export const createUpdateModalSubmitHandler = ({
2
+ onClose = () => {},
3
+ onUpdate = async () => ({ ok: false }),
4
+ }) => {
5
+ return async () => {
6
+ const result = await onUpdate();
7
+ if (result?.ok) {
8
+ onClose();
9
+ }
10
+ return result;
11
+ };
12
+ };
@@ -40,6 +40,7 @@ export const UpdateModal = ({
40
40
  version = "",
41
41
  onUpdate = () => {},
42
42
  updating = false,
43
+ updateLoadingLabel = "Updating...",
43
44
  }) => {
44
45
  const requestedTag = useMemo(() => getReleaseTagFromVersion(version), [version]);
45
46
  const [loadingNotes, setLoadingNotes] = useState(false);
@@ -163,7 +164,7 @@ export const UpdateModal = ({
163
164
  onClick=${onUpdate}
164
165
  tone="warning"
165
166
  idleLabel=${updateLabel}
166
- loadingLabel="Updating..."
167
+ loadingLabel=${updateLoadingLabel}
167
168
  loading=${updating}
168
169
  disabled=${loadingNotes}
169
170
  />
@@ -17,6 +17,7 @@ export const WatchdogTab = ({
17
17
  restartingGateway = false,
18
18
  onRestartGateway,
19
19
  restartSignal = 0,
20
+ openclawRestarting = false,
20
21
  openclawUpdateInProgress = false,
21
22
  onOpenclawVersionActionComplete = () => {},
22
23
  onOpenclawUpdate,
@@ -37,6 +38,7 @@ export const WatchdogTab = ({
37
38
  watchdogStatus=${state.currentWatchdogStatus}
38
39
  onRepair=${state.onRepair}
39
40
  repairing=${state.isRepairInProgress}
41
+ openclawRestarting=${openclawRestarting}
40
42
  openclawUpdateInProgress=${openclawUpdateInProgress}
41
43
  onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
42
44
  onOpenclawUpdate=${onOpenclawUpdate}
@@ -102,12 +102,11 @@ export const Welcome = ({ onComplete, acVersion }) => {
102
102
  error=${state.formError}
103
103
  step=${state.step}
104
104
  totalGroups=${kWelcomeGroups.length}
105
- currentGroupValid=${state.currentGroupValid}
105
+ importApplied=${state.importApplied}
106
106
  goBack=${actions.goBack}
107
107
  goNext=${actions.goNext}
108
108
  loading=${state.loading}
109
109
  githubStepLoading=${state.githubStepLoading}
110
- allValid=${state.allValid}
111
110
  handleSubmit=${actions.handleSubmit}
112
111
  />
113
112
  `}
@@ -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
+ } from "../../lib/model-catalog.js";
16
22
  import {
17
23
  kWelcomeGroups,
24
+ getWelcomeGroupError,
25
+ findFirstInvalidWelcomeGroup,
26
+ normalizeGithubRepoInput,
18
27
  isValidGithubRepoInput,
19
28
  kGithubFlowFresh,
20
29
  kGithubFlowImport,
@@ -40,6 +49,8 @@ export const kImportStepId = "import";
40
49
  export const kSecretReviewStepId = "secret-review";
41
50
  export const kPlaceholderReviewStepId = "placeholder-review";
42
51
  const kImportSubstepKey = "_IMPORT_SUBSTEP";
52
+ const kImportAppliedKey = "_IMPORT_APPLIED";
53
+ const kImportedSourceRepoKey = "_IMPORTED_SOURCE_REPO";
43
54
  const kImportPlaceholderReviewKey = "_IMPORT_PLACEHOLDER_REVIEW";
44
55
  const kImportPlaceholderSkipConfirmedKey = "_IMPORT_PLACEHOLDER_SKIP_CONFIRMED";
45
56
 
@@ -73,14 +84,45 @@ const normalizePlaceholderReview = (review) => {
73
84
  };
74
85
  };
75
86
 
87
+ const normalizeTrackedRepo = (repoInput) =>
88
+ normalizeGithubRepoInput(repoInput).toLowerCase();
89
+
90
+ export const getImportReuseState = ({
91
+ githubFlow,
92
+ importApplied,
93
+ sourceRepo,
94
+ importedSourceRepo,
95
+ }) => {
96
+ const sourceImportAlreadyApplied =
97
+ githubFlow === kGithubFlowImport && !!importApplied;
98
+ const normalizedSourceRepo = normalizeTrackedRepo(sourceRepo);
99
+ const normalizedImportedSourceRepo = normalizeTrackedRepo(importedSourceRepo);
100
+ const sourceRepoChangedAfterImport =
101
+ sourceImportAlreadyApplied &&
102
+ !!normalizedImportedSourceRepo &&
103
+ normalizedSourceRepo !== normalizedImportedSourceRepo;
104
+
105
+ return {
106
+ sourceImportAlreadyApplied,
107
+ sourceRepoChangedAfterImport,
108
+ };
109
+ };
110
+
76
111
  export const useWelcome = ({ onComplete }) => {
77
112
  const kSetupStepIndex = kWelcomeGroups.length;
78
113
  const kPairingStepIndex = kSetupStepIndex + 1;
79
- const { vals, setVals, setValue, step, setStep, setupError, setSetupError } =
80
- useWelcomeStorage({
81
- kSetupStepIndex,
82
- kPairingStepIndex,
83
- });
114
+ const {
115
+ vals,
116
+ setVals,
117
+ setValue: setStoredValue,
118
+ step,
119
+ setStep,
120
+ setupError,
121
+ setSetupError,
122
+ } = useWelcomeStorage({
123
+ kSetupStepIndex,
124
+ kPairingStepIndex,
125
+ });
84
126
  const [models, setModels] = useState([]);
85
127
  const [modelsLoading, setModelsLoading] = useState(true);
86
128
  const [modelsError, setModelsError] = useState(null);
@@ -110,6 +152,14 @@ export const useWelcome = ({ onComplete }) => {
110
152
  const [importScanResult, setImportScanResult] = useState(null);
111
153
  const [importScanning, setImportScanning] = useState(false);
112
154
  const [importError, setImportError] = useState(null);
155
+ const modelsFetchState = useCachedFetch(kModelCatalogCacheKey, fetchModels, {
156
+ maxAgeMs: 30000,
157
+ });
158
+
159
+ const setValue = (key, value) => {
160
+ if (formError) setFormError(null);
161
+ setStoredValue(key, value);
162
+ };
113
163
 
114
164
  const setImportStep = (nextStep) => {
115
165
  setImportStepState(nextStep);
@@ -129,24 +179,58 @@ export const useWelcome = ({ onComplete }) => {
129
179
  };
130
180
 
131
181
  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
- }, []);
182
+ const list = getModelCatalogModels(modelsFetchState.data);
183
+ if (!modelsFetchState.data) return;
184
+ setModels(list);
185
+ setModelsError(list.length > 0 ? null : "No models found");
186
+ const defaultModelKey = getInitialOnboardingModelKey({
187
+ catalog: list,
188
+ currentModelKey: vals.MODEL_KEY,
189
+ });
190
+ if (!vals.MODEL_KEY && defaultModelKey) {
191
+ setVals((prev) => ({ ...prev, MODEL_KEY: defaultModelKey }));
192
+ }
193
+ }, [modelsFetchState.data, setVals, vals.MODEL_KEY]);
194
+
195
+ useEffect(() => {
196
+ const hasModels = getModelCatalogModels(modelsFetchState.data).length > 0;
197
+ setModelsLoading(modelsFetchState.loading && !hasModels);
198
+ }, [modelsFetchState.data, modelsFetchState.loading]);
145
199
 
146
- const selectedProvider = getModelProvider(vals.MODEL_KEY);
200
+ useEffect(() => {
201
+ if (!modelsFetchState.error) return;
202
+ setModelsError("Failed to load models");
203
+ setModelsLoading(false);
204
+ }, [modelsFetchState.error]);
205
+
206
+ const getValidationContext = (currentVals = {}) => {
207
+ const currentSelectedProvider = getModelProvider(
208
+ String(currentVals.MODEL_KEY || "").trim(),
209
+ );
210
+ const currentSelectedAuthProvider =
211
+ getAuthProviderFromModelProvider(currentSelectedProvider);
212
+ const currentProviderAuthFields =
213
+ kProviderAuthFields[currentSelectedAuthProvider] || [];
214
+ const currentHasAi =
215
+ currentSelectedProvider === "openai-codex"
216
+ ? !!codexStatus.connected
217
+ : currentProviderAuthFields.some((field) =>
218
+ !!String(currentVals[field.key] || "").trim(),
219
+ );
220
+
221
+ return {
222
+ hasAi: currentHasAi,
223
+ selectedProvider: currentSelectedProvider,
224
+ codexLoading,
225
+ };
226
+ };
227
+
228
+ const validationContext = getValidationContext(vals);
229
+ const { selectedProvider, hasAi } = validationContext;
147
230
  const placeholderReview = normalizePlaceholderReview(
148
231
  vals[kImportPlaceholderReviewKey],
149
232
  );
233
+ const importApplied = !!vals[kImportAppliedKey];
150
234
  const featuredModels = getFeaturedModels(models);
151
235
  const baseModelOptions = showAllModels
152
236
  ? models
@@ -164,23 +248,10 @@ export const useWelcome = ({ onComplete }) => {
164
248
  const canToggleFullCatalog =
165
249
  featuredModels.length > 0 && models.length > featuredModels.length;
166
250
  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
251
  const isPreStep = step === -1;
178
252
  const isSetupStep = step === kSetupStepIndex;
179
253
  const isPairingStep = step === kPairingStepIndex;
180
254
  const activeGroup = step >= 0 && step < kSetupStepIndex ? kWelcomeGroups[step] : null;
181
- const currentGroupValid = activeGroup
182
- ? activeGroup.validate(vals, { hasAi })
183
- : false;
184
255
  const selectedPairingChannel = String(
185
256
  vals[kPairingChannelKey] || getPreferredPairingChannel(vals),
186
257
  );
@@ -202,7 +273,21 @@ export const useWelcome = ({ onComplete }) => {
202
273
  const handleSubmit = async () => {
203
274
  const { normalizedVals, didChange } = normalizeOnboardingVals(vals);
204
275
  if (didChange) setVals(normalizedVals);
205
- if (!kWelcomeGroups.every((group) => group.validate(normalizedVals, { hasAi }))) {
276
+ const submitValidationContext = getValidationContext(normalizedVals);
277
+ const invalidGroup = findFirstInvalidWelcomeGroup(
278
+ normalizedVals,
279
+ submitValidationContext,
280
+ );
281
+ if (invalidGroup) {
282
+ setFormError(
283
+ getWelcomeGroupError(
284
+ invalidGroup.id,
285
+ normalizedVals,
286
+ submitValidationContext,
287
+ ),
288
+ );
289
+ setSetupError(null);
290
+ setStep(kWelcomeGroups.findIndex((group) => group.id === invalidGroup.id));
206
291
  return;
207
292
  }
208
293
  if (loading) return;
@@ -309,7 +394,17 @@ export const useWelcome = ({ onComplete }) => {
309
394
  const goNext = async () => {
310
395
  const { normalizedVals, didChange } = normalizeOnboardingVals(vals);
311
396
  if (didChange) setVals(normalizedVals);
312
- if (!activeGroup || !activeGroup.validate(normalizedVals, { hasAi })) return;
397
+ if (!activeGroup) return;
398
+ const stepValidationContext = getValidationContext(normalizedVals);
399
+ const stepValidationError = getWelcomeGroupError(
400
+ activeGroup.id,
401
+ normalizedVals,
402
+ stepValidationContext,
403
+ );
404
+ if (stepValidationError) {
405
+ setFormError(stepValidationError);
406
+ return;
407
+ }
313
408
  setFormError(null);
314
409
  if (activeGroup.id === "github") {
315
410
  const githubFlow = normalizedVals._GITHUB_FLOW || kGithubFlowFresh;
@@ -318,6 +413,19 @@ export const useWelcome = ({ onComplete }) => {
318
413
  ? kGithubTargetRepoModeCreate
319
414
  : normalizedVals._GITHUB_TARGET_REPO_MODE ||
320
415
  kGithubTargetRepoModeCreate;
416
+ const { sourceImportAlreadyApplied, sourceRepoChangedAfterImport } =
417
+ getImportReuseState({
418
+ githubFlow,
419
+ importApplied: normalizedVals[kImportAppliedKey],
420
+ sourceRepo: normalizedVals._GITHUB_SOURCE_REPO,
421
+ importedSourceRepo: normalizedVals[kImportedSourceRepoKey],
422
+ });
423
+ if (sourceRepoChangedAfterImport) {
424
+ setFormError(
425
+ "The source repo has already been imported into this setup. You can still change the target repo, but changing the source repo requires restarting onboarding.",
426
+ );
427
+ return;
428
+ }
321
429
  const targetVerifyMode =
322
430
  targetRepoMode === kGithubTargetRepoModeExistingEmpty
323
431
  ? kRepoModeExisting
@@ -327,9 +435,11 @@ export const useWelcome = ({ onComplete }) => {
327
435
  ? normalizedVals._GITHUB_SOURCE_REPO
328
436
  : normalizedVals.GITHUB_WORKSPACE_REPO;
329
437
  setGithubStepLoading(true);
330
- clearPlaceholderReview();
438
+ if (!sourceImportAlreadyApplied) {
439
+ clearPlaceholderReview();
440
+ }
331
441
  try {
332
- if (githubFlow === kGithubFlowImport) {
442
+ if (githubFlow === kGithubFlowImport && !sourceImportAlreadyApplied) {
333
443
  const sourceResult = await verifyGithubOnboardingRepo(
334
444
  sourceRepo,
335
445
  normalizedVals.GITHUB_TOKEN,
@@ -451,10 +561,16 @@ export const useWelcome = ({ onComplete }) => {
451
561
  const nextPlaceholderReview = normalizePlaceholderReview(
452
562
  result.placeholderReview,
453
563
  );
564
+ const importedSourceRepo =
565
+ vals._GITHUB_FLOW === kGithubFlowImport
566
+ ? normalizeGithubRepoInput(vals._GITHUB_SOURCE_REPO)
567
+ : "";
454
568
  setVals((prev) => ({
455
569
  ...prev,
456
570
  ...approvedImportVals,
457
571
  ...(result.preFill || {}),
572
+ [kImportAppliedKey]: true,
573
+ [kImportedSourceRepoKey]: importedSourceRepo,
458
574
  [kImportPlaceholderReviewKey]: nextPlaceholderReview,
459
575
  [kImportPlaceholderSkipConfirmedKey]: false,
460
576
  }));
@@ -540,17 +656,16 @@ export const useWelcome = ({ onComplete }) => {
540
656
  importScanResult,
541
657
  importScanning,
542
658
  importError,
659
+ importApplied,
543
660
  selectedProvider,
544
661
  modelOptions,
545
662
  canToggleFullCatalog,
546
663
  visibleAiFieldKeys,
547
664
  hasAi,
548
- allValid,
549
665
  isPreStep,
550
666
  isSetupStep,
551
667
  isPairingStep,
552
668
  activeGroup,
553
- currentGroupValid,
554
669
  selectedPairingChannel,
555
670
  placeholderReview,
556
671
  isImportStep,
@@ -5,6 +5,7 @@ import {
5
5
  fetchAuthStatus,
6
6
  fetchAlphaclawVersion,
7
7
  updateAlphaclaw,
8
+ waitForAlphaclawRestart,
8
9
  fetchRestartStatus,
9
10
  dismissRestartStatus,
10
11
  restartGateway,
@@ -25,6 +26,7 @@ export const useAppShellController = ({ location = "" } = {}) => {
25
26
  const [acLatest, setAcLatest] = useState(null);
26
27
  const [acHasUpdate, setAcHasUpdate] = useState(false);
27
28
  const [acUpdating, setAcUpdating] = useState(false);
29
+ const [acRestarting, setAcRestarting] = useState(false);
28
30
  const [restartRequired, setRestartRequired] = useState(false);
29
31
  const [browseRestartRequired, setBrowseRestartRequired] = useState(false);
30
32
  const [restartingGateway, setRestartingGateway] = useState(false);
@@ -32,6 +34,7 @@ export const useAppShellController = ({ location = "" } = {}) => {
32
34
  const [statusPollCadenceMs, setStatusPollCadenceMs] = useState(15000);
33
35
  const [statusPollingGraceElapsed, setStatusPollingGraceElapsed] = useState(false);
34
36
  const [openclawUpdateInProgress, setOpenclawUpdateInProgress] = useState(false);
37
+ const [openclawRestarting, setOpenclawRestarting] = useState(false);
35
38
  const [statusStreamConnected, setStatusStreamConnected] = useState(false);
36
39
  const [statusStreamStatus, setStatusStreamStatus] = useState(null);
37
40
  const [statusStreamWatchdog, setStatusStreamWatchdog] = useState(null);
@@ -241,17 +244,25 @@ export const useAppShellController = ({ location = "" } = {}) => {
241
244
  return { ok: false, error: "OpenClaw update already in progress" };
242
245
  }
243
246
  setOpenclawUpdateInProgress(true);
247
+ setOpenclawRestarting(false);
244
248
  try {
245
249
  const data = await updateOpenclaw();
250
+ if (data?.ok && data?.restarting) {
251
+ setOpenclawRestarting(true);
252
+ await waitForAlphaclawRestart();
253
+ window.location.reload();
254
+ return { ...data, restartHandled: true };
255
+ }
256
+ setOpenclawUpdateInProgress(false);
257
+ setOpenclawRestarting(false);
246
258
  return data;
247
- } finally {
259
+ } catch (err) {
260
+ const message = err.message || "Could not update OpenClaw";
248
261
  setOpenclawUpdateInProgress(false);
249
- refreshSharedStatuses();
250
- setTimeout(refreshSharedStatuses, 1200);
251
- setTimeout(refreshSharedStatuses, 3500);
252
- setTimeout(refreshRestartStatus, 1200);
262
+ setOpenclawRestarting(false);
263
+ return { ok: false, error: message };
253
264
  }
254
- }, [openclawUpdateInProgress, refreshRestartStatus, refreshSharedStatuses]);
265
+ }, [openclawUpdateInProgress]);
255
266
 
256
267
  const handleOpenclawVersionActionComplete = useCallback(
257
268
  ({ type }) => {
@@ -263,20 +274,31 @@ export const useAppShellController = ({ location = "" } = {}) => {
263
274
  );
264
275
 
265
276
  const handleAcUpdate = useCallback(async () => {
266
- if (acUpdating) return;
277
+ if (acUpdating) {
278
+ return { ok: false, error: "AlphaClaw update already in progress" };
279
+ }
267
280
  setAcUpdating(true);
281
+ setAcRestarting(false);
268
282
  try {
269
283
  const data = await updateAlphaclaw();
270
284
  if (data.ok) {
271
285
  showToast("AlphaClaw updated — restarting...", "success");
272
- setTimeout(() => window.location.reload(), 5000);
286
+ setAcRestarting(true);
287
+ await waitForAlphaclawRestart();
288
+ window.location.reload();
289
+ return data;
273
290
  } else {
274
291
  showToast(data.error || "AlphaClaw update failed", "error");
275
292
  setAcUpdating(false);
293
+ setAcRestarting(false);
294
+ return data;
276
295
  }
277
296
  } catch (err) {
278
- showToast(err.message || "Could not update AlphaClaw", "error");
297
+ const message = err.message || "Could not update AlphaClaw";
298
+ showToast(message, "error");
279
299
  setAcUpdating(false);
300
+ setAcRestarting(false);
301
+ return { ok: false, error: message };
280
302
  }
281
303
  }, [acUpdating]);
282
304
 
@@ -296,12 +318,14 @@ export const useAppShellController = ({ location = "" } = {}) => {
296
318
  state: {
297
319
  acHasUpdate,
298
320
  acLatest,
321
+ acRestarting,
299
322
  acUpdating,
300
323
  acVersion,
301
324
  authEnabled,
302
325
  gatewayRestartSignal,
303
326
  isAnyRestartRequired,
304
327
  onboarded,
328
+ openclawRestarting,
305
329
  openclawUpdateInProgress,
306
330
  restartingGateway,
307
331
  sharedDoctorStatus,