@chrysb/alphaclaw 0.8.1-beta.7 → 0.8.1-beta.8

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
  `;
@@ -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,
@@ -261,6 +262,18 @@ export const useAppShellController = ({ location = "" } = {}) => {
261
262
  }
262
263
  }, [acUpdating]);
263
264
 
265
+ const dismissRestartBanner = useCallback(async () => {
266
+ setRestartRequired(false);
267
+ setBrowseRestartRequired(false);
268
+ try {
269
+ await dismissRestartStatus();
270
+ await refreshRestartStatus();
271
+ } catch (err) {
272
+ showToast(err.message || "Could not dismiss restart banner", "error");
273
+ await refreshRestartStatus();
274
+ }
275
+ }, [refreshRestartStatus]);
276
+
264
277
  return {
265
278
  state: {
266
279
  acHasUpdate,
@@ -284,6 +297,7 @@ export const useAppShellController = ({ location = "" } = {}) => {
284
297
  handleOpenclawUpdate,
285
298
  handleOpenclawVersionActionComplete,
286
299
  refreshSharedStatuses,
300
+ dismissRestartBanner,
287
301
  setRestartRequired,
288
302
  },
289
303
  };
@@ -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) => {
@@ -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-beta.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },