@chrysb/alphaclaw 0.8.6 → 0.8.7-beta.1

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.
@@ -202,6 +202,7 @@ const App = () => {
202
202
  onPreviewBrowseFile=${browseActions.handleBrowsePreviewFile}
203
203
  acHasUpdate=${controllerState.acHasUpdate}
204
204
  acLatest=${controllerState.acLatest}
205
+ acRestarting=${controllerState.acRestarting}
205
206
  acUpdating=${controllerState.acUpdating}
206
207
  onAcUpdate=${controllerActions.handleAcUpdate}
207
208
  agents=${agentsState.agents}
@@ -384,6 +385,7 @@ const App = () => {
384
385
  restartingGateway=${controllerState.restartingGateway}
385
386
  onRestartGateway=${controllerActions.handleGatewayRestart}
386
387
  restartSignal=${controllerState.gatewayRestartSignal}
388
+ openclawRestarting=${controllerState.openclawRestarting}
387
389
  openclawUpdateInProgress=${controllerState.openclawUpdateInProgress}
388
390
  onOpenclawVersionActionComplete=${controllerActions.handleOpenclawVersionActionComplete}
389
391
  onOpenclawUpdate=${controllerActions.handleOpenclawUpdate}
@@ -419,6 +421,7 @@ const App = () => {
419
421
  restartingGateway=${controllerState.restartingGateway}
420
422
  onRestartGateway=${controllerActions.handleGatewayRestart}
421
423
  restartSignal=${controllerState.gatewayRestartSignal}
424
+ openclawRestarting=${controllerState.openclawRestarting}
422
425
  openclawUpdateInProgress=${controllerState.openclawUpdateInProgress}
423
426
  onOpenclawVersionActionComplete=${controllerActions.handleOpenclawVersionActionComplete}
424
427
  onOpenclawUpdate=${controllerActions.handleOpenclawUpdate}
@@ -27,6 +27,7 @@ const VersionRow = ({
27
27
  fetchVersion,
28
28
  applyUpdate,
29
29
  updateInProgress = false,
30
+ updateLoadingLabel = "Updating...",
30
31
  onActionComplete = () => {},
31
32
  }) => {
32
33
  const [checking, setChecking] = useState(false);
@@ -236,7 +237,7 @@ const VersionRow = ({
236
237
  ? updateIdleLabel
237
238
  : "Check updates"}
238
239
  loadingLabel=${isUpdateActionActive
239
- ? "Updating..."
240
+ ? updateLoadingLabel
240
241
  : "Checking..."}
241
242
  className="hidden md:inline-flex"
242
243
  />
@@ -250,7 +251,7 @@ const VersionRow = ({
250
251
  ? updateIdleLabel
251
252
  : "Check updates"}
252
253
  loadingLabel=${isUpdateActionActive
253
- ? "Updating..."
254
+ ? updateLoadingLabel
254
255
  : "Checking..."}
255
256
  />
256
257
  `}
@@ -272,7 +273,7 @@ const VersionRow = ({
272
273
  loading=${updateButtonLoading}
273
274
  warning=${isUpdateActionActive}
274
275
  idleLabel=${updateIdleLabel}
275
- loadingLabel="Updating..."
276
+ loadingLabel=${updateLoadingLabel}
276
277
  className="flex-1 h-9 px-3"
277
278
  />
278
279
  </div>
@@ -299,6 +300,7 @@ export const Gateway = ({
299
300
  onOpenWatchdog,
300
301
  onRepair,
301
302
  repairing = false,
303
+ openclawRestarting = false,
302
304
  openclawUpdateInProgress = false,
303
305
  onOpenclawVersionActionComplete = () => {},
304
306
  onOpenclawUpdate = updateOpenclaw,
@@ -443,6 +445,7 @@ export const Gateway = ({
443
445
  fetchVersion=${fetchOpenclawVersion}
444
446
  applyUpdate=${onOpenclawUpdate}
445
447
  updateInProgress=${openclawUpdateInProgress}
448
+ updateLoadingLabel=${openclawRestarting ? "Restarting..." : "Updating..."}
446
449
  onActionComplete=${onOpenclawVersionActionComplete}
447
450
  />
448
451
  </div>
@@ -28,6 +28,7 @@ export const GeneralTab = ({
28
28
  restartingGateway = false,
29
29
  onRestartGateway = () => {},
30
30
  restartSignal = 0,
31
+ openclawRestarting = false,
31
32
  openclawUpdateInProgress = false,
32
33
  onOpenclawVersionActionComplete = () => {},
33
34
  onOpenclawUpdate = () => {},
@@ -54,6 +55,7 @@ export const GeneralTab = ({
54
55
  onOpenWatchdog=${() => onSwitchTab("watchdog")}
55
56
  onRepair=${actions.handleWatchdogRepair}
56
57
  repairing=${state.repairingWatchdog}
58
+ openclawRestarting=${openclawRestarting}
57
59
  openclawUpdateInProgress=${openclawUpdateInProgress}
58
60
  onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
59
61
  onOpenclawUpdate=${onOpenclawUpdate}
@@ -50,6 +50,7 @@ export const WelcomeFormStep = ({
50
50
  error,
51
51
  step,
52
52
  totalGroups,
53
+ importApplied,
53
54
  goBack,
54
55
  goNext,
55
56
  loading,
@@ -88,18 +89,28 @@ export const WelcomeFormStep = ({
88
89
  });
89
90
  }, [activeGroup.id]);
90
91
 
91
- const renderStandardField = (field) => html`
92
+ const renderStandardField = (field) => {
93
+ const isLockedImportSourceField =
94
+ activeGroup.id === "github" &&
95
+ githubFlow === kGithubFlowImport &&
96
+ importApplied &&
97
+ field.key === "_GITHUB_SOURCE_REPO";
98
+
99
+ return html`
92
100
  <div class="space-y-1" key=${field.key}>
93
101
  <label class="text-xs font-medium text-fg-muted">${field.label}</label>
94
102
  <${SecretInput}
95
103
  key=${field.key}
96
104
  value=${vals[field.key] || ""}
97
105
  onInput=${(e) => setValue(field.key, e.target.value)}
106
+ disabled=${isLockedImportSourceField}
98
107
  placeholder=${activeGroup.id === "github" && field.key === "GITHUB_TOKEN"
99
108
  ? githubTokenPlaceholder
100
109
  : field.placeholder || ""}
101
110
  isSecret=${!field.isText}
102
- inputClass="flex-1 bg-field border border-border rounded-lg px-3 py-2 text-sm text-body outline-none focus:border-fg-muted font-mono"
111
+ inputClass=${`flex-1 bg-field border border-border rounded-lg px-3 py-2 text-sm text-body outline-none focus:border-fg-muted font-mono ${
112
+ isLockedImportSourceField ? "opacity-60 cursor-not-allowed" : ""
113
+ }`}
103
114
  />
104
115
  <p class="text-xs text-fg-dim">
105
116
  ${activeGroup.id === "github" &&
@@ -110,7 +121,9 @@ export const WelcomeFormStep = ({
110
121
  ? "Enter the owner/repo of an existing empty repository"
111
122
  : "A new private repo will be created for you"
112
123
  : activeGroup.id === "github" && field.key === "_GITHUB_SOURCE_REPO"
113
- ? "The repo to import from"
124
+ ? importApplied
125
+ ? "This source repo is already imported locally. You can still change the target repo below."
126
+ : "The repo to import from"
114
127
  : activeGroup.id === "github" && field.key === "GITHUB_TOKEN"
115
128
  ? githubFlow === kGithubFlowImport
116
129
  ? freshRepoMode === kGithubTargetRepoModeCreate
@@ -139,7 +152,8 @@ export const WelcomeFormStep = ({
139
152
  : field.hint}
140
153
  </p>
141
154
  </div>
142
- `;
155
+ `;
156
+ };
143
157
  const fieldLookup = new Map((activeGroup.fields || []).map((field) => [field.key, field]));
144
158
  const toggleChannelSection = (channelId) =>
145
159
  setExpandedChannels((current) => {
@@ -327,6 +341,17 @@ export const WelcomeFormStep = ({
327
341
  ${activeGroup.id === "github" &&
328
342
  html`
329
343
  <div class="space-y-3">
344
+ ${githubFlow === kGithubFlowImport && importApplied
345
+ ? html`
346
+ <div
347
+ class="bg-status-info-bg border border-status-info-border rounded-lg p-3 text-xs text-status-info"
348
+ >
349
+ The import source is already applied locally. You can still
350
+ change the target repo before finishing setup, but we will not
351
+ re-import the source repo a second time.
352
+ </div>
353
+ `
354
+ : null}
330
355
  ${githubFlow === kGithubFlowFresh
331
356
  ? html`
332
357
  <div class="space-y-1">
@@ -16,6 +16,7 @@ export const GeneralRoute = ({
16
16
  restartingGateway = false,
17
17
  onRestartGateway = () => {},
18
18
  restartSignal = 0,
19
+ openclawRestarting = false,
19
20
  openclawUpdateInProgress = false,
20
21
  onOpenclawVersionActionComplete = () => {},
21
22
  onOpenclawUpdate = () => {},
@@ -37,6 +38,7 @@ export const GeneralRoute = ({
37
38
  restartingGateway=${restartingGateway}
38
39
  onRestartGateway=${onRestartGateway}
39
40
  restartSignal=${restartSignal}
41
+ openclawRestarting=${openclawRestarting}
40
42
  openclawUpdateInProgress=${openclawUpdateInProgress}
41
43
  onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
42
44
  onOpenclawUpdate=${onOpenclawUpdate}
@@ -11,6 +11,7 @@ export const WatchdogRoute = ({
11
11
  restartingGateway = false,
12
12
  onRestartGateway = () => {},
13
13
  restartSignal = 0,
14
+ openclawRestarting = false,
14
15
  openclawUpdateInProgress = false,
15
16
  onOpenclawVersionActionComplete = () => {},
16
17
  onOpenclawUpdate = () => {},
@@ -24,6 +25,7 @@ export const WatchdogRoute = ({
24
25
  restartingGateway=${restartingGateway}
25
26
  onRestartGateway=${onRestartGateway}
26
27
  restartSignal=${restartSignal}
28
+ openclawRestarting=${openclawRestarting}
27
29
  openclawUpdateInProgress=${openclawUpdateInProgress}
28
30
  onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
29
31
  onOpenclawUpdate=${onOpenclawUpdate}
@@ -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,
@@ -110,6 +111,7 @@ export const AppSidebar = ({
110
111
  onPreviewBrowseFile = () => {},
111
112
  acHasUpdate = false,
112
113
  acLatest = "",
114
+ acRestarting = false,
113
115
  acUpdating = false,
114
116
  onAcUpdate = () => {},
115
117
  agents = [],
@@ -182,6 +184,19 @@ export const AppSidebar = ({
182
184
  writeUiSettings(settings);
183
185
  }, [browseBottomPanelHeightPx]);
184
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
+
185
200
  const getClampedBrowseBottomPanelHeight = (value) => {
186
201
  const layoutElement = browseLayoutRef.current;
187
202
  if (!layoutElement) return value;
@@ -364,7 +379,7 @@ export const AppSidebar = ({
364
379
  loading=${acUpdating}
365
380
  warning=${true}
366
381
  idleLabel=${`Update to v${acLatest}`}
367
- loadingLabel="Updating..."
382
+ loadingLabel=${acRestarting ? "Restarting..." : "Updating..."}
368
383
  className="w-full justify-center"
369
384
  />
370
385
  `
@@ -489,13 +504,11 @@ export const AppSidebar = ({
489
504
  </div>
490
505
  <${UpdateModal}
491
506
  visible=${updateModalOpen}
492
- onClose=${() => {
493
- if (acUpdating) return;
494
- setUpdateModalOpen(false);
495
- }}
507
+ onClose=${handleUpdateModalClose}
496
508
  version=${acLatest}
497
- onUpdate=${onAcUpdate}
509
+ onUpdate=${handleUpdateModalSubmit}
498
510
  updating=${acUpdating}
511
+ updateLoadingLabel=${acRestarting ? "Restarting..." : "Updating..."}
499
512
  />
500
513
  </div>
501
514
  `;
@@ -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,6 +102,7 @@ export const Welcome = ({ onComplete, acVersion }) => {
102
102
  error=${state.formError}
103
103
  step=${state.step}
104
104
  totalGroups=${kWelcomeGroups.length}
105
+ importApplied=${state.importApplied}
105
106
  goBack=${actions.goBack}
106
107
  goNext=${actions.goNext}
107
108
  loading=${state.loading}
@@ -23,6 +23,7 @@ import {
23
23
  kWelcomeGroups,
24
24
  getWelcomeGroupError,
25
25
  findFirstInvalidWelcomeGroup,
26
+ normalizeGithubRepoInput,
26
27
  isValidGithubRepoInput,
27
28
  kGithubFlowFresh,
28
29
  kGithubFlowImport,
@@ -48,6 +49,8 @@ export const kImportStepId = "import";
48
49
  export const kSecretReviewStepId = "secret-review";
49
50
  export const kPlaceholderReviewStepId = "placeholder-review";
50
51
  const kImportSubstepKey = "_IMPORT_SUBSTEP";
52
+ const kImportAppliedKey = "_IMPORT_APPLIED";
53
+ const kImportedSourceRepoKey = "_IMPORTED_SOURCE_REPO";
51
54
  const kImportPlaceholderReviewKey = "_IMPORT_PLACEHOLDER_REVIEW";
52
55
  const kImportPlaceholderSkipConfirmedKey = "_IMPORT_PLACEHOLDER_SKIP_CONFIRMED";
53
56
 
@@ -81,6 +84,30 @@ const normalizePlaceholderReview = (review) => {
81
84
  };
82
85
  };
83
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
+
84
111
  export const useWelcome = ({ onComplete }) => {
85
112
  const kSetupStepIndex = kWelcomeGroups.length;
86
113
  const kPairingStepIndex = kSetupStepIndex + 1;
@@ -203,6 +230,7 @@ export const useWelcome = ({ onComplete }) => {
203
230
  const placeholderReview = normalizePlaceholderReview(
204
231
  vals[kImportPlaceholderReviewKey],
205
232
  );
233
+ const importApplied = !!vals[kImportAppliedKey];
206
234
  const featuredModels = getFeaturedModels(models);
207
235
  const baseModelOptions = showAllModels
208
236
  ? models
@@ -385,6 +413,19 @@ export const useWelcome = ({ onComplete }) => {
385
413
  ? kGithubTargetRepoModeCreate
386
414
  : normalizedVals._GITHUB_TARGET_REPO_MODE ||
387
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
+ }
388
429
  const targetVerifyMode =
389
430
  targetRepoMode === kGithubTargetRepoModeExistingEmpty
390
431
  ? kRepoModeExisting
@@ -394,9 +435,11 @@ export const useWelcome = ({ onComplete }) => {
394
435
  ? normalizedVals._GITHUB_SOURCE_REPO
395
436
  : normalizedVals.GITHUB_WORKSPACE_REPO;
396
437
  setGithubStepLoading(true);
397
- clearPlaceholderReview();
438
+ if (!sourceImportAlreadyApplied) {
439
+ clearPlaceholderReview();
440
+ }
398
441
  try {
399
- if (githubFlow === kGithubFlowImport) {
442
+ if (githubFlow === kGithubFlowImport && !sourceImportAlreadyApplied) {
400
443
  const sourceResult = await verifyGithubOnboardingRepo(
401
444
  sourceRepo,
402
445
  normalizedVals.GITHUB_TOKEN,
@@ -518,10 +561,16 @@ export const useWelcome = ({ onComplete }) => {
518
561
  const nextPlaceholderReview = normalizePlaceholderReview(
519
562
  result.placeholderReview,
520
563
  );
564
+ const importedSourceRepo =
565
+ vals._GITHUB_FLOW === kGithubFlowImport
566
+ ? normalizeGithubRepoInput(vals._GITHUB_SOURCE_REPO)
567
+ : "";
521
568
  setVals((prev) => ({
522
569
  ...prev,
523
570
  ...approvedImportVals,
524
571
  ...(result.preFill || {}),
572
+ [kImportAppliedKey]: true,
573
+ [kImportedSourceRepoKey]: importedSourceRepo,
525
574
  [kImportPlaceholderReviewKey]: nextPlaceholderReview,
526
575
  [kImportPlaceholderSkipConfirmedKey]: false,
527
576
  }));
@@ -607,6 +656,7 @@ export const useWelcome = ({ onComplete }) => {
607
656
  importScanResult,
608
657
  importScanning,
609
658
  importError,
659
+ importApplied,
610
660
  selectedProvider,
611
661
  modelOptions,
612
662
  canToggleFullCatalog,
@@ -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,
@@ -527,6 +527,41 @@ export async function updateAlphaclaw() {
527
527
  return res.json();
528
528
  }
529
529
 
530
+ const delay = (ms) =>
531
+ new Promise((resolve) => {
532
+ setTimeout(resolve, Math.max(0, Number(ms) || 0));
533
+ });
534
+
535
+ export async function waitForAlphaclawRestart({
536
+ initialDelayMs = 1500,
537
+ intervalMs = 1000,
538
+ timeoutMs = 60000,
539
+ } = {}) {
540
+ const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
541
+ await delay(initialDelayMs);
542
+
543
+ while (Date.now() <= deadline) {
544
+ try {
545
+ const headers = new Headers();
546
+ const browserTimeZone = getBrowserTimeZone();
547
+ if (browserTimeZone) {
548
+ headers.set(kClientTimeZoneHeader, browserTimeZone);
549
+ }
550
+ const res = await fetch("/api/auth/status", {
551
+ cache: "no-store",
552
+ credentials: "same-origin",
553
+ headers,
554
+ });
555
+ if (res.status < 500) {
556
+ return { ok: true };
557
+ }
558
+ } catch {}
559
+ await delay(intervalMs);
560
+ }
561
+
562
+ throw new Error("AlphaClaw restart is taking longer than expected");
563
+ }
564
+
530
565
  export async function fetchSyncCron() {
531
566
  const res = await authFetch("/api/sync-cron");
532
567
  const text = await res.text();