@chrysb/alphaclaw 0.8.1-beta.7 → 0.8.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.
@@ -44,6 +44,26 @@
44
44
  flex-shrink: 0;
45
45
  }
46
46
 
47
+ .global-restart-banner__actions {
48
+ display: inline-flex;
49
+ align-items: center;
50
+ gap: 8px;
51
+ flex-shrink: 0;
52
+ }
53
+
54
+ .global-restart-banner__dismiss {
55
+ display: inline-flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ padding: 2px;
59
+ color: #fde68a;
60
+ opacity: 0.85;
61
+ }
62
+
63
+ .global-restart-banner__dismiss:hover {
64
+ opacity: 1;
65
+ }
66
+
47
67
  .app-content {
48
68
  grid-column: 3;
49
69
  grid-row: 2;
@@ -392,6 +412,10 @@
392
412
  position: static;
393
413
  transform: none;
394
414
  }
415
+ .global-restart-banner__actions {
416
+ width: 100%;
417
+ justify-content: flex-end;
418
+ }
395
419
  .app-content {
396
420
  grid-column: 1;
397
421
  grid-row: 2;
@@ -169,6 +169,7 @@ const App = () => {
169
169
  visible=${controllerState.isAnyRestartRequired}
170
170
  restarting=${controllerState.restartingGateway}
171
171
  onRestart=${controllerActions.handleGatewayRestart}
172
+ onDismiss=${controllerActions.dismissRestartBanner}
172
173
  />
173
174
  <${AppSidebar}
174
175
  mobileSidebarOpen=${shellState.mobileSidebarOpen}
@@ -333,7 +333,9 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
333
333
  setVars(nextVars);
334
334
  setPendingCustomKeys([]);
335
335
  setReservedKeys(new Set(data.reservedKeys || []));
336
- onRestartRequired(!!data.restartRequired);
336
+ if (data.restartRequired) {
337
+ onRestartRequired(true);
338
+ }
337
339
  },
338
340
  [onRestartRequired],
339
341
  );
@@ -389,11 +391,11 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
389
391
  : "Environment variables saved",
390
392
  "success",
391
393
  );
392
- const sortedVars = sortCustomVarsAlphabetically(vars);
393
- setVars(sortedVars);
394
- setPendingCustomKeys([]);
394
+ // Force-refresh /api/env so stale cached payload cannot overwrite newly
395
+ // saved values with older state right after save.
396
+ const latestPayload = await refreshEnvPayload({ force: true });
397
+ applyEnvPayload(latestPayload);
395
398
  setSecretMaskEpoch((prev) => prev + 1);
396
- baselineSignatureRef.current = getVarsSignature(sortedVars);
397
399
  setDirty(false);
398
400
  } catch (err) {
399
401
  showToast("Failed to save: " + err.message, "error");
@@ -1,6 +1,7 @@
1
1
  import { h } from "https://esm.sh/preact";
2
2
  import htm from "https://esm.sh/htm";
3
3
  import { UpdateActionButton } from "./update-action-button.js";
4
+ import { CloseIcon } from "./icons.js";
4
5
 
5
6
  const html = htm.bind(h);
6
7
 
@@ -8,6 +9,7 @@ export const GlobalRestartBanner = ({
8
9
  visible = false,
9
10
  restarting = false,
10
11
  onRestart,
12
+ onDismiss = () => {},
11
13
  }) => {
12
14
  if (!visible) return null;
13
15
  return html`
@@ -16,15 +18,26 @@ export const GlobalRestartBanner = ({
16
18
  <p class="global-restart-banner__text">
17
19
  Gateway restart required to apply pending configuration changes.
18
20
  </p>
19
- <${UpdateActionButton}
20
- onClick=${onRestart}
21
- disabled=${restarting}
22
- loading=${restarting}
23
- warning=${true}
24
- idleLabel="Restart Gateway"
25
- loadingLabel="Restarting..."
26
- className="global-restart-banner__button"
27
- />
21
+ <div class="global-restart-banner__actions">
22
+ <${UpdateActionButton}
23
+ onClick=${onRestart}
24
+ disabled=${restarting}
25
+ loading=${restarting}
26
+ warning=${true}
27
+ idleLabel="Restart Gateway"
28
+ loadingLabel="Restarting..."
29
+ className="global-restart-banner__button"
30
+ />
31
+ <button
32
+ type="button"
33
+ onclick=${onDismiss}
34
+ class="global-restart-banner__dismiss ac-btn-ghost"
35
+ aria-label="Dismiss restart banner"
36
+ title="Dismiss"
37
+ >
38
+ <${CloseIcon} className="h-3.5 w-3.5" />
39
+ </button>
40
+ </div>
28
41
  </div>
29
42
  </div>
30
43
  `;
@@ -60,26 +60,16 @@ export const BrowserAttachCard = () => {
60
60
  );
61
61
 
62
62
  return html`
63
- <div class="bg-surface border border-border rounded-xl p-4 space-y-3">
64
- <div class="space-y-1">
65
- <h3 class="font-semibold text-sm">Live Chrome Attach (Mac Node)</h3>
66
- <p class="text-xs text-gray-500">
67
- Connect your agent to real Chrome sessions (logged-in tabs, cookies,
68
- and all) using the built-in <code>user</code> profile.
69
- </p>
70
- </div>
71
-
72
- <details class="rounded-lg border border-border bg-black/20 px-3 py-2.5">
73
- <summary
74
- class="cursor-pointer text-xs text-gray-300 hover:text-gray-200"
75
- >
76
- Setup instructions
77
- </summary>
78
- <div
79
- class="pt-3 file-viewer-preview release-notes-preview text-xs leading-5"
80
- dangerouslySetInnerHTML=${{ __html: setupInstructionsHtml }}
81
- ></div>
82
- </details>
83
- </div>
63
+ <details
64
+ class="ac-surface-inset rounded-lg border border-border px-3 py-2.5"
65
+ >
66
+ <summary class="cursor-pointer text-xs text-gray-300 hover:text-gray-200">
67
+ Chrome debugging setup / troubleshooting
68
+ </summary>
69
+ <div
70
+ class="pt-3 px-2 file-viewer-preview release-notes-preview text-xs leading-5"
71
+ dangerouslySetInnerHTML=${{ __html: setupInstructionsHtml }}
72
+ ></div>
73
+ </details>
84
74
  `;
85
75
  };
@@ -9,7 +9,7 @@ import { fetchNodeBrowserStatusForNode, removeNode } from "../../../lib/api.js";
9
9
  import { readUiSettings, updateUiSettings } from "../../../lib/ui-settings.js";
10
10
  import { showToast } from "../../toast.js";
11
11
 
12
- const kBrowserCheckTimeoutMs = 15000;
12
+ const kBrowserCheckTimeoutMs = 35000;
13
13
  const kBrowserPollIntervalMs = 10000;
14
14
  const kBrowserAttachStateByNodeKey = "nodesBrowserAttachStateByNode";
15
15
 
@@ -212,6 +212,32 @@ export const useConnectedNodesCard = ({
212
212
  removingNodeId,
213
213
  ]);
214
214
 
215
+ useEffect(() => {
216
+ if (checkingBrowserNodeId) return;
217
+ const pendingInitialNodeId = nodes
218
+ .map((node) => ({
219
+ nodeId: String(node?.nodeId || "").trim(),
220
+ connected: node?.connected === true,
221
+ browserCapable: isBrowserCapableNode(node),
222
+ }))
223
+ .find((entry) => {
224
+ if (!entry.nodeId || !entry.connected || !entry.browserCapable) return false;
225
+ if (browserAttachStateByNodeId?.[entry.nodeId] !== true) return false;
226
+ if (browserStatusByNodeId?.[entry.nodeId]) return false;
227
+ if (browserErrorByNodeId?.[entry.nodeId]) return false;
228
+ return true;
229
+ })?.nodeId;
230
+ if (!pendingInitialNodeId) return;
231
+ handleCheckNodeBrowser(pendingInitialNodeId, { silent: true });
232
+ }, [
233
+ browserAttachStateByNodeId,
234
+ browserErrorByNodeId,
235
+ browserStatusByNodeId,
236
+ checkingBrowserNodeId,
237
+ handleCheckNodeBrowser,
238
+ nodes,
239
+ ]);
240
+
215
241
  useEffect(() => {
216
242
  if (checkingBrowserNodeId) return;
217
243
  const pollableNodeIds = nodes
@@ -219,13 +245,17 @@ export const useConnectedNodesCard = ({
219
245
  nodeId: String(node?.nodeId || "").trim(),
220
246
  connected: node?.connected === true,
221
247
  browserCapable: isBrowserCapableNode(node),
248
+ browserRunning:
249
+ browserStatusByNodeId?.[String(node?.nodeId || "").trim()]?.running ===
250
+ true,
222
251
  }))
223
252
  .filter(
224
253
  (entry) =>
225
254
  entry.nodeId &&
226
255
  entry.connected &&
227
256
  entry.browserCapable &&
228
- browserAttachStateByNodeId?.[entry.nodeId] === true,
257
+ browserAttachStateByNodeId?.[entry.nodeId] === true &&
258
+ entry.browserRunning,
229
259
  )
230
260
  .map((entry) => entry.nodeId);
231
261
  if (!pollableNodeIds.length) return;
@@ -244,7 +274,13 @@ export const useConnectedNodesCard = ({
244
274
  active = false;
245
275
  clearInterval(timer);
246
276
  };
247
- }, [browserAttachStateByNodeId, checkingBrowserNodeId, handleCheckNodeBrowser, nodes]);
277
+ }, [
278
+ browserAttachStateByNodeId,
279
+ browserStatusByNodeId,
280
+ checkingBrowserNodeId,
281
+ handleCheckNodeBrowser,
282
+ nodes,
283
+ ]);
248
284
 
249
285
  return {
250
286
  browserStatusByNodeId,
@@ -6,6 +6,7 @@ import {
6
6
  fetchAlphaclawVersion,
7
7
  updateAlphaclaw,
8
8
  fetchRestartStatus,
9
+ dismissRestartStatus,
9
10
  restartGateway,
10
11
  fetchWatchdogStatus,
11
12
  fetchDoctorStatus,
@@ -17,6 +18,7 @@ import { usePolling } from "./usePolling.js";
17
18
  import { showToast } from "../components/toast.js";
18
19
 
19
20
  export const useAppShellController = ({ location = "" } = {}) => {
21
+ const kInitialStatusPollDelayMs = 5000;
20
22
  const [onboarded, setOnboarded] = useState(null);
21
23
  const [authEnabled, setAuthEnabled] = useState(false);
22
24
  const [acVersion, setAcVersion] = useState(null);
@@ -28,6 +30,7 @@ export const useAppShellController = ({ location = "" } = {}) => {
28
30
  const [restartingGateway, setRestartingGateway] = useState(false);
29
31
  const [gatewayRestartSignal, setGatewayRestartSignal] = useState(0);
30
32
  const [statusPollCadenceMs, setStatusPollCadenceMs] = useState(15000);
33
+ const [statusPollingGraceElapsed, setStatusPollingGraceElapsed] = useState(false);
31
34
  const [openclawUpdateInProgress, setOpenclawUpdateInProgress] = useState(false);
32
35
  const [statusStreamConnected, setStatusStreamConnected] = useState(false);
33
36
  const [statusStreamStatus, setStatusStreamStatus] = useState(null);
@@ -35,15 +38,18 @@ export const useAppShellController = ({ location = "" } = {}) => {
35
38
  const [statusStreamDoctor, setStatusStreamDoctor] = useState(null);
36
39
 
37
40
  const sharedStatusPoll = usePolling(fetchStatus, statusPollCadenceMs, {
38
- enabled: onboarded === true && !statusStreamConnected,
41
+ enabled:
42
+ onboarded === true && !statusStreamConnected && statusPollingGraceElapsed,
39
43
  cacheKey: "/api/status",
40
44
  });
41
45
  const sharedWatchdogPoll = usePolling(fetchWatchdogStatus, statusPollCadenceMs, {
42
- enabled: onboarded === true && !statusStreamConnected,
46
+ enabled:
47
+ onboarded === true && !statusStreamConnected && statusPollingGraceElapsed,
43
48
  cacheKey: "/api/watchdog/status",
44
49
  });
45
50
  const sharedDoctorPoll = usePolling(fetchDoctorStatus, statusPollCadenceMs, {
46
- enabled: onboarded === true && !statusStreamConnected,
51
+ enabled:
52
+ onboarded === true && !statusStreamConnected && statusPollingGraceElapsed,
47
53
  cacheKey: "/api/doctor/status",
48
54
  });
49
55
  const sharedStatus = statusStreamStatus || sharedStatusPoll.data || null;
@@ -68,6 +74,19 @@ export const useAppShellController = ({ location = "" } = {}) => {
68
74
  .catch(() => {});
69
75
  }, []);
70
76
 
77
+ useEffect(() => {
78
+ if (onboarded !== true) {
79
+ setStatusPollingGraceElapsed(false);
80
+ return () => {};
81
+ }
82
+ const timerId = setTimeout(() => {
83
+ setStatusPollingGraceElapsed(true);
84
+ }, kInitialStatusPollDelayMs);
85
+ return () => {
86
+ clearTimeout(timerId);
87
+ };
88
+ }, [onboarded]);
89
+
71
90
  useEffect(() => {
72
91
  if (onboarded !== true) return;
73
92
  let disposed = false;
@@ -261,6 +280,18 @@ export const useAppShellController = ({ location = "" } = {}) => {
261
280
  }
262
281
  }, [acUpdating]);
263
282
 
283
+ const dismissRestartBanner = useCallback(async () => {
284
+ setRestartRequired(false);
285
+ setBrowseRestartRequired(false);
286
+ try {
287
+ await dismissRestartStatus();
288
+ await refreshRestartStatus();
289
+ } catch (err) {
290
+ showToast(err.message || "Could not dismiss restart banner", "error");
291
+ await refreshRestartStatus();
292
+ }
293
+ }, [refreshRestartStatus]);
294
+
264
295
  return {
265
296
  state: {
266
297
  acHasUpdate,
@@ -284,6 +315,7 @@ export const useAppShellController = ({ location = "" } = {}) => {
284
315
  handleOpenclawUpdate,
285
316
  handleOpenclawVersionActionComplete,
286
317
  refreshSharedStatuses,
318
+ dismissRestartBanner,
287
319
  setRestartRequired,
288
320
  },
289
321
  };
@@ -346,6 +346,13 @@ export async function fetchRestartStatus() {
346
346
  return parseJsonOrThrow(res, "Could not load restart status");
347
347
  }
348
348
 
349
+ export async function dismissRestartStatus() {
350
+ const res = await authFetch("/api/restart-status/dismiss", {
351
+ method: "POST",
352
+ });
353
+ return parseJsonOrThrow(res, "Could not dismiss restart status");
354
+ }
355
+
349
356
  export async function fetchWatchdogStatus() {
350
357
  const res = await authFetch("/api/watchdog/status");
351
358
  return parseJsonOrThrow(res, "Could not load watchdog status");
@@ -14,18 +14,40 @@ const {
14
14
  ensureAgentScaffold,
15
15
  } = require("./shared");
16
16
 
17
+ const toTitleWords = (value = "") =>
18
+ String(value || "")
19
+ .trim()
20
+ .split(/[-_\s]+/)
21
+ .filter(Boolean)
22
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
23
+ .join(" ");
24
+
25
+ const getFallbackAgentName = (agentId = "") => {
26
+ const normalizedAgentId = String(agentId || "").trim();
27
+ if (!normalizedAgentId) return "Agent";
28
+ const title = toTitleWords(normalizedAgentId) || normalizedAgentId;
29
+ return `${title} Agent`;
30
+ };
31
+
32
+ const getAgentDisplayName = (agent = {}) =>
33
+ String(agent?.identity?.name || "").trim() ||
34
+ String(agent?.name || "").trim() ||
35
+ getFallbackAgentName(agent?.id || "");
36
+
37
+ const toReadableAgent = (agent = {}) => ({
38
+ ...agent,
39
+ id: String(agent.id || "").trim(),
40
+ name: getAgentDisplayName(agent),
41
+ default: !!agent.default,
42
+ });
43
+
17
44
  const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
18
45
  const listAgents = () => {
19
46
  const cfg = withNormalizedAgentsConfig({
20
47
  OPENCLAW_DIR,
21
48
  cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
22
49
  });
23
- return (cfg.agents?.list || []).map((entry) => ({
24
- ...entry,
25
- id: String(entry.id || "").trim(),
26
- name: String(entry.name || "").trim() || String(entry.id || "").trim(),
27
- default: !!entry.default,
28
- }));
50
+ return (cfg.agents?.list || []).map((entry) => toReadableAgent(entry));
29
51
  };
30
52
 
31
53
  const getAgent = (agentId) => {
@@ -84,20 +106,29 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
84
106
  OPENCLAW_DIR,
85
107
  agentId,
86
108
  });
109
+ const requestedIdentity =
110
+ input.identity && typeof input.identity === "object"
111
+ ? { ...input.identity }
112
+ : {};
113
+ const requestedName = String(input.name || "").trim();
114
+ const identityName =
115
+ requestedName ||
116
+ String(requestedIdentity.name || "").trim() ||
117
+ getFallbackAgentName(agentId);
87
118
  const nextAgent = {
88
119
  id: agentId,
89
- name: String(input.name || "").trim() || agentId,
90
120
  default: false,
91
121
  workspace: scaffoldWorkspacePath,
92
122
  agentDir: agentDirPath,
123
+ identity: {
124
+ ...requestedIdentity,
125
+ name: identityName,
126
+ },
93
127
  ...(input.model ? { model: input.model } : {}),
94
- ...(input.identity && typeof input.identity === "object"
95
- ? { identity: { ...input.identity } }
96
- : {}),
97
128
  };
98
129
  cfg.agents.list = [...cfg.agents.list, nextAgent];
99
130
  saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
100
- return nextAgent;
131
+ return toReadableAgent(nextAgent);
101
132
  };
102
133
 
103
134
  const updateAgent = (agentId, patch = {}) => {
@@ -109,20 +140,29 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
109
140
  const index = cfg.agents.list.findIndex((entry) => entry.id === normalized);
110
141
  if (index < 0) throw new Error(`Agent "${normalized}" not found`);
111
142
  const current = cfg.agents.list[index];
112
- const next = {
113
- ...current,
114
- ...(patch.name !== undefined
115
- ? { name: String(patch.name || "").trim() }
116
- : {}),
117
- ...(patch.identity !== undefined
118
- ? {
119
- identity:
120
- patch.identity && typeof patch.identity === "object"
121
- ? { ...patch.identity }
122
- : {},
123
- }
124
- : {}),
125
- };
143
+ const next = { ...current };
144
+ const identityPatched =
145
+ patch.identity !== undefined || patch.name !== undefined;
146
+ if (identityPatched) {
147
+ const baseIdentity =
148
+ patch.identity !== undefined
149
+ ? patch.identity && typeof patch.identity === "object"
150
+ ? { ...patch.identity }
151
+ : {}
152
+ : current.identity && typeof current.identity === "object"
153
+ ? { ...current.identity }
154
+ : {};
155
+ const requestedName =
156
+ patch.name !== undefined
157
+ ? String(patch.name || "").trim()
158
+ : String(baseIdentity.name || "").trim();
159
+ const fallbackLegacyName = String(current.name || "").trim();
160
+ baseIdentity.name =
161
+ requestedName || fallbackLegacyName || getFallbackAgentName(normalized);
162
+ next.identity = baseIdentity;
163
+ // Only remove legacy top-level name once identity.name is persisted.
164
+ delete next.name;
165
+ }
126
166
  if (patch.model !== undefined) {
127
167
  if (patch.model === null) {
128
168
  delete next.model;
@@ -134,7 +174,10 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
134
174
  if (patch.tools && typeof patch.tools === "object") {
135
175
  const toolsCfg = {};
136
176
  if (patch.tools.profile) toolsCfg.profile = String(patch.tools.profile);
137
- if (Array.isArray(patch.tools.alsoAllow) && patch.tools.alsoAllow.length) {
177
+ if (
178
+ Array.isArray(patch.tools.alsoAllow) &&
179
+ patch.tools.alsoAllow.length
180
+ ) {
138
181
  toolsCfg.alsoAllow = patch.tools.alsoAllow.map(String);
139
182
  }
140
183
  if (Array.isArray(patch.tools.deny) && patch.tools.deny.length) {
@@ -145,10 +188,9 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
145
188
  delete next.tools;
146
189
  }
147
190
  }
148
- if (!String(next.name || "").trim()) next.name = normalized;
149
191
  cfg.agents.list[index] = next;
150
192
  saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
151
- return next;
193
+ return toReadableAgent(next);
152
194
  };
153
195
 
154
196
  const setDefaultAgent = (agentId) => {
@@ -31,6 +31,38 @@ const parseGithubErrorMessage = async (response) => {
31
31
  return response.statusText || `HTTP ${response.status}`;
32
32
  };
33
33
 
34
+ // Files GitHub may auto-create when initializing a repo — a repo containing
35
+ // only these is treated as empty for onboarding purposes.
36
+ const kBoilerplateNames = new Set([
37
+ "readme",
38
+ "readme.md",
39
+ "readme.txt",
40
+ "readme.rst",
41
+ "license",
42
+ "license.md",
43
+ "license.txt",
44
+ ".gitignore",
45
+ ".gitattributes",
46
+ ]);
47
+
48
+ const repoContainsOnlyBoilerplate = async (repoUrl, ghHeaders) => {
49
+ try {
50
+ const res = await fetch(
51
+ `https://api.github.com/repos/${repoUrl}/contents/`,
52
+ { headers: ghHeaders },
53
+ );
54
+ if (!res.ok) return false;
55
+ const entries = await res.json();
56
+ if (!Array.isArray(entries)) return false;
57
+ if (entries.length === 0) return true;
58
+ return entries.every(
59
+ (e) => e.type === "file" && kBoilerplateNames.has(e.name.toLowerCase()),
60
+ );
61
+ } catch {
62
+ return false;
63
+ }
64
+ };
65
+
34
66
  const isClassicPat = (token) => String(token || "").startsWith("ghp_");
35
67
  const isFineGrainedPat = (token) =>
36
68
  String(token || "").startsWith("github_pat_");
@@ -98,6 +130,13 @@ const verifyGithubRepoForOnboarding = async ({
98
130
  return { ok: true, repoExists: true, repoIsEmpty: true };
99
131
  }
100
132
  if (commitsRes.ok) {
133
+ const onlyBoilerplate = await repoContainsOnlyBoilerplate(
134
+ repoUrl,
135
+ ghHeaders,
136
+ );
137
+ if (onlyBoilerplate) {
138
+ return { ok: true, repoExists: true, repoIsEmpty: true };
139
+ }
101
140
  if (isExisting) {
102
141
  return { ok: true, repoExists: true, repoIsEmpty: false };
103
142
  }
@@ -7,8 +7,8 @@ const kAllowedExecHosts = new Set(["gateway", "node"]);
7
7
  const kAllowedExecSecurity = new Set(["deny", "allowlist", "full"]);
8
8
  const kAllowedExecAsk = new Set(["off", "on-miss", "always"]);
9
9
  const kSafeNodeIdPattern = /^[\w\-:.]+$/;
10
- const kNodeBrowserInvokeTimeoutMs = 15000;
11
- const kNodeBrowserCliTimeoutMs = 18000;
10
+ const kNodeBrowserInvokeTimeoutMs = 30000;
11
+ const kNodeBrowserCliTimeoutMs = 35000;
12
12
  const kNodeRouteCliTimeoutMs = 12000;
13
13
 
14
14
  const quoteCliArg = (value) => quoteShellArg(value, { strategy: "single" });
@@ -151,16 +151,40 @@ const registerSystemRoutes = ({
151
151
  }
152
152
  return "Telegram";
153
153
  };
154
- const getAgentLabelFromSessionKey = (key = "") => {
154
+ const getDefaultAgentLabel = (config = {}) => {
155
+ return "Main Agent";
156
+ };
157
+ const getFallbackAgentLabel = (agentId = "") => {
158
+ const normalizedAgentId = String(agentId || "").trim();
159
+ if (!normalizedAgentId) return "Agent";
160
+ const titledAgentId = toTitleWords(normalizedAgentId) || normalizedAgentId;
161
+ return `${titledAgentId} Agent`;
162
+ };
163
+ const getConfiguredAgentLabel = (config = {}, agentId = "") => {
164
+ const normalizedAgentId = String(agentId || "").trim();
165
+ if (!normalizedAgentId) return "Agent";
166
+ const configuredAgents = Array.isArray(config?.agents?.list)
167
+ ? config.agents.list
168
+ : [];
169
+ const configuredAgent = configuredAgents.find(
170
+ (entry) => String(entry?.id || "").trim() === normalizedAgentId,
171
+ );
172
+ const configuredName =
173
+ String(configuredAgent?.name || "").trim() ||
174
+ String(configuredAgent?.identity?.name || "").trim();
175
+ if (configuredName) return configuredName;
176
+ if (normalizedAgentId === "main") return getDefaultAgentLabel(config);
177
+ return getFallbackAgentLabel(normalizedAgentId);
178
+ };
179
+ const getAgentLabelFromSessionKey = (key = "", config = {}) => {
155
180
  const match = String(key || "").match(/^agent:([^:]+):/);
156
181
  const agentId = String(match?.[1] || "").trim();
157
182
  if (!agentId) return "Agent";
158
- if (agentId === "main") return "Main Agent";
159
- return toTitleWords(agentId);
183
+ return getConfiguredAgentLabel(config, agentId);
160
184
  };
161
185
  const buildSessionLabel = (sessionRow = {}, config = {}) => {
162
186
  const key = String(sessionRow?.key || "");
163
- const agentLabel = getAgentLabelFromSessionKey(key);
187
+ const agentLabel = getAgentLabelFromSessionKey(key, config);
164
188
  const agentKeyMatch = key.match(/^agent:([^:]+):/);
165
189
  const agentId = String(agentKeyMatch?.[1] || "").trim();
166
190
  const telegramChannelName = resolveTelegramChannelNameForAgent({
@@ -749,6 +773,22 @@ const registerSystemRoutes = ({
749
773
  }
750
774
  });
751
775
 
776
+ app.post("/api/restart-status/dismiss", async (req, res) => {
777
+ try {
778
+ envRestartPending = false;
779
+ restartRequiredState.clearRequired();
780
+ const snapshot = await restartRequiredState.getSnapshot();
781
+ res.json({
782
+ ok: true,
783
+ restartRequired: snapshot.restartRequired || envRestartPending,
784
+ restartInProgress: snapshot.restartInProgress,
785
+ gatewayRunning: snapshot.gatewayRunning,
786
+ });
787
+ } catch (err) {
788
+ res.status(500).json({ ok: false, error: err.message });
789
+ }
790
+ });
791
+
752
792
  app.post("/api/gateway/restart", async (req, res) => {
753
793
  if (!isOnboarded()) {
754
794
  return res.status(400).json({ ok: false, error: "Not onboarded" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.8.1-beta.7",
3
+ "version": "0.8.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },