@chrysb/alphaclaw 0.2.3 → 0.3.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 (63) hide show
  1. package/bin/alphaclaw.js +79 -0
  2. package/lib/public/css/shell.css +57 -2
  3. package/lib/public/css/theme.css +184 -0
  4. package/lib/public/js/app.js +330 -89
  5. package/lib/public/js/components/action-button.js +92 -0
  6. package/lib/public/js/components/channels.js +16 -7
  7. package/lib/public/js/components/confirm-dialog.js +25 -19
  8. package/lib/public/js/components/credentials-modal.js +32 -23
  9. package/lib/public/js/components/device-pairings.js +15 -2
  10. package/lib/public/js/components/envars.js +22 -65
  11. package/lib/public/js/components/features.js +1 -1
  12. package/lib/public/js/components/gateway.js +139 -32
  13. package/lib/public/js/components/global-restart-banner.js +31 -0
  14. package/lib/public/js/components/google.js +9 -9
  15. package/lib/public/js/components/icons.js +19 -0
  16. package/lib/public/js/components/info-tooltip.js +18 -0
  17. package/lib/public/js/components/loading-spinner.js +32 -0
  18. package/lib/public/js/components/modal-shell.js +42 -0
  19. package/lib/public/js/components/models.js +34 -29
  20. package/lib/public/js/components/onboarding/welcome-form-step.js +45 -32
  21. package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
  22. package/lib/public/js/components/onboarding/welcome-setup-step.js +7 -24
  23. package/lib/public/js/components/page-header.js +13 -0
  24. package/lib/public/js/components/pairings.js +15 -2
  25. package/lib/public/js/components/providers.js +216 -142
  26. package/lib/public/js/components/scope-picker.js +1 -1
  27. package/lib/public/js/components/secret-input.js +1 -0
  28. package/lib/public/js/components/telegram-workspace.js +37 -49
  29. package/lib/public/js/components/toast.js +34 -5
  30. package/lib/public/js/components/toggle-switch.js +25 -0
  31. package/lib/public/js/components/update-action-button.js +13 -53
  32. package/lib/public/js/components/watchdog-tab.js +312 -0
  33. package/lib/public/js/components/webhooks.js +981 -0
  34. package/lib/public/js/components/welcome.js +2 -1
  35. package/lib/public/js/lib/api.js +102 -1
  36. package/lib/public/js/lib/model-config.js +0 -5
  37. package/lib/server/alphaclaw-version.js +5 -3
  38. package/lib/server/constants.js +33 -0
  39. package/lib/server/discord-api.js +48 -0
  40. package/lib/server/gateway.js +64 -4
  41. package/lib/server/log-writer.js +102 -0
  42. package/lib/server/onboarding/github.js +21 -1
  43. package/lib/server/openclaw-version.js +2 -6
  44. package/lib/server/restart-required-state.js +86 -0
  45. package/lib/server/routes/auth.js +9 -4
  46. package/lib/server/routes/proxy.js +12 -14
  47. package/lib/server/routes/system.js +61 -15
  48. package/lib/server/routes/telegram.js +17 -48
  49. package/lib/server/routes/watchdog.js +68 -0
  50. package/lib/server/routes/webhooks.js +214 -0
  51. package/lib/server/telegram-api.js +11 -0
  52. package/lib/server/watchdog-db.js +148 -0
  53. package/lib/server/watchdog-notify.js +93 -0
  54. package/lib/server/watchdog.js +585 -0
  55. package/lib/server/webhook-middleware.js +195 -0
  56. package/lib/server/webhooks-db.js +265 -0
  57. package/lib/server/webhooks.js +238 -0
  58. package/lib/server.js +119 -4
  59. package/lib/setup/core-prompts/AGENTS.md +84 -0
  60. package/lib/setup/core-prompts/TOOLS.md +13 -0
  61. package/lib/setup/core-prompts/UI-DRY-OPPORTUNITIES.md +50 -0
  62. package/lib/setup/gitignore +2 -0
  63. package/package.json +2 -1
@@ -65,13 +65,13 @@ export function Google({ gatewayStatus }) {
65
65
  useEffect(() => {
66
66
  const handler = async (e) => {
67
67
  if (e.data?.google === "success") {
68
- showToast("✓ Google account connected", "green");
68
+ showToast("✓ Google account connected", "success");
69
69
  setApiStatus({});
70
70
  await refresh();
71
71
  } else if (e.data?.google === "error") {
72
72
  showToast(
73
73
  "✗ Google auth failed: " + (e.data.message || "unknown"),
74
- "red",
74
+ "error",
75
75
  );
76
76
  }
77
77
  };
@@ -111,7 +111,7 @@ export function Google({ gatewayStatus }) {
111
111
  });
112
112
  setApiStatus({});
113
113
  setScopes(getDefaultScopes());
114
- showToast("Google account disconnected", "green");
114
+ showToast("Google account disconnected", "success");
115
115
  } else {
116
116
  alert("Failed to disconnect: " + (data.error || "unknown"));
117
117
  }
@@ -155,7 +155,7 @@ export function Google({ gatewayStatus }) {
155
155
  ${isAuthed &&
156
156
  html`<button
157
157
  onclick=${handleCheckApis}
158
- class="text-xs text-gray-500 hover:text-gray-300"
158
+ class="text-xs px-2 py-1 rounded-lg ac-btn-ghost"
159
159
  >
160
160
  ↻ Check APIs
161
161
  </button>`}
@@ -166,26 +166,26 @@ export function Google({ gatewayStatus }) {
166
166
  apiStatus=${isAuthed ? apiStatus : {}}
167
167
  loading=${isAuthed && checkingApis}
168
168
  />
169
- <div class="flex justify-between items-center pt-1">
170
- <div class="flex items-center gap-2">
169
+ <div class="pt-1 space-y-2 sm:space-y-0 sm:flex sm:justify-between sm:items-center">
170
+ <div class="grid grid-cols-2 gap-2 w-full sm:w-auto sm:flex sm:items-center">
171
171
  <button
172
172
  onclick=${() => startAuth(email)}
173
173
  disabled=${isAuthed && !scopesChanged}
174
- class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
174
+ class="w-full sm:w-auto text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
175
175
  >
176
176
  ${isAuthed ? "Update Permissions" : "Sign in with Google"}
177
177
  </button>
178
178
  <button
179
179
  type="button"
180
180
  onclick=${() => setModalOpen(true)}
181
- class="text-xs font-medium px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
181
+ class="w-full sm:w-auto text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary"
182
182
  >
183
183
  Edit credentials
184
184
  </button>
185
185
  </div>
186
186
  <button
187
187
  onclick=${() => setDisconnectDialogOpen(true)}
188
- class="text-xs text-red-400/60 hover:text-red-400"
188
+ class="text-xs px-2 py-1 rounded-lg ac-btn-ghost w-full sm:w-auto"
189
189
  >
190
190
  Disconnect
191
191
  </button>
@@ -21,3 +21,22 @@ export const ChevronDownIcon = ({ className = "" }) => html`
21
21
  />
22
22
  </svg>
23
23
  `;
24
+
25
+ export const CloseIcon = ({ className = "" }) => html`
26
+ <svg
27
+ class=${className}
28
+ width="16"
29
+ height="16"
30
+ viewBox="0 0 16 16"
31
+ fill="none"
32
+ aria-hidden="true"
33
+ >
34
+ <path
35
+ d="M4 4L12 12M12 4L4 12"
36
+ stroke="currentColor"
37
+ stroke-width="1.5"
38
+ stroke-linecap="round"
39
+ stroke-linejoin="round"
40
+ />
41
+ </svg>
42
+ `;
@@ -0,0 +1,18 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+
4
+ const html = htm.bind(h);
5
+
6
+ export const InfoTooltip = ({ text = "", widthClass = "w-64" }) => html`
7
+ <span class="relative group inline-flex items-center cursor-default select-none">
8
+ <span
9
+ class="inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-500 text-[10px] text-gray-400 cursor-default"
10
+ aria-label=${text}
11
+ >?</span
12
+ >
13
+ <span
14
+ class=${`pointer-events-none absolute left-1/2 top-full z-10 mt-2 hidden -translate-x-1/2 rounded-md border border-border bg-modal px-2 py-1 text-[11px] text-gray-300 shadow-lg group-hover:block ${widthClass}`}
15
+ >${text}</span
16
+ >
17
+ </span>
18
+ `;
@@ -0,0 +1,32 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+
4
+ const html = htm.bind(h);
5
+
6
+ export const LoadingSpinner = ({
7
+ className = "h-4 w-4",
8
+ ariaHidden = true,
9
+ style = "",
10
+ }) => html`
11
+ <svg
12
+ class=${`ac-spinner ${className}`.trim()}
13
+ viewBox="0 0 24 24"
14
+ fill="none"
15
+ aria-hidden=${ariaHidden ? "true" : "false"}
16
+ style=${style}
17
+ >
18
+ <circle
19
+ class="opacity-25"
20
+ cx="12"
21
+ cy="12"
22
+ r="10"
23
+ stroke="currentColor"
24
+ stroke-width="4"
25
+ />
26
+ <path
27
+ class="opacity-75"
28
+ fill="currentColor"
29
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
30
+ />
31
+ </svg>
32
+ `;
@@ -0,0 +1,42 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useEffect } from "https://esm.sh/preact/hooks";
3
+ import { createPortal } from "https://esm.sh/preact/compat";
4
+ import htm from "https://esm.sh/htm";
5
+
6
+ const html = htm.bind(h);
7
+
8
+ export const ModalShell = ({
9
+ visible = false,
10
+ onClose = () => {},
11
+ closeOnOverlayClick = true,
12
+ closeOnEscape = true,
13
+ panelClassName = "bg-modal border border-border rounded-xl p-5 max-w-md w-full space-y-3",
14
+ children = null,
15
+ }) => {
16
+ useEffect(() => {
17
+ if (!visible || !closeOnEscape) return;
18
+
19
+ const handleKeydown = (event) => {
20
+ if (event.key === "Escape") onClose?.();
21
+ };
22
+
23
+ window.addEventListener("keydown", handleKeydown);
24
+ return () => window.removeEventListener("keydown", handleKeydown);
25
+ }, [visible, closeOnEscape, onClose]);
26
+
27
+ if (!visible) return null;
28
+
29
+ return createPortal(
30
+ html`
31
+ <div
32
+ class="fixed inset-0 bg-black/70 flex items-center justify-center p-4 z-50"
33
+ onclick=${(event) => {
34
+ if (closeOnOverlayClick && event.target === event.currentTarget) onClose?.();
35
+ }}
36
+ >
37
+ <div class=${panelClassName}>${children}</div>
38
+ </div>
39
+ `,
40
+ document.body,
41
+ );
42
+ };
@@ -14,6 +14,8 @@ import {
14
14
  import { showToast } from "./toast.js";
15
15
  import { Badge } from "./badge.js";
16
16
  import { SecretInput } from "./secret-input.js";
17
+ import { LoadingSpinner } from "./loading-spinner.js";
18
+ import { ActionButton } from "./action-button.js";
17
19
  import {
18
20
  getModelProvider,
19
21
  getAuthProviderFromModelProvider,
@@ -87,7 +89,7 @@ export const Models = () => {
87
89
  };
88
90
  } catch (err) {
89
91
  setModelsError("Failed to load model settings");
90
- showToast(`Failed to load model settings: ${err.message}`, "red");
92
+ showToast(`Failed to load model settings: ${err.message}`, "error");
91
93
  } finally {
92
94
  setReady(true);
93
95
  setModelsLoading(false);
@@ -123,10 +125,10 @@ export const Models = () => {
123
125
  useEffect(() => {
124
126
  const onMessage = async (e) => {
125
127
  if (e.data?.codex === "success") {
126
- showToast("Codex connected", "green");
128
+ showToast("Codex connected", "success");
127
129
  await refreshCodexConnection();
128
130
  } else if (e.data?.codex === "error") {
129
- showToast(`Codex auth failed: ${e.data.message || "unknown error"}`, "red");
131
+ showToast(`Codex auth failed: ${e.data.message || "unknown error"}`, "error");
130
132
  }
131
133
  };
132
134
  window.addEventListener("message", onMessage);
@@ -145,7 +147,7 @@ export const Models = () => {
145
147
  if (savingChanges) return;
146
148
  if (!modelDirty && !aiCredentialsDirty) return;
147
149
  if (modelDirty && !hasSelectedProviderAuth) {
148
- showToast("Add credentials for the selected model provider before saving model changes", "red");
150
+ showToast("Add credentials for the selected model provider before saving model changes", "error");
149
151
  return;
150
152
  }
151
153
  setSavingChanges(true);
@@ -176,10 +178,10 @@ export const Models = () => {
176
178
  kModelsTabCache = { ...(kModelsTabCache || {}), selectedModel: targetModel, savedModel: targetModel };
177
179
  }
178
180
 
179
- showToast("Changes saved", "green");
181
+ showToast("Changes saved", "success");
180
182
  await refresh();
181
183
  } catch (err) {
182
- showToast(err.message || "Failed to save changes", "red");
184
+ showToast(err.message || "Failed to save changes", "error");
183
185
  } finally {
184
186
  setSavingChanges(false);
185
187
  }
@@ -214,12 +216,12 @@ export const Models = () => {
214
216
  const result = await exchangeCodexOAuth(codexManualInput.trim());
215
217
  if (!result.ok) throw new Error(result.error || "Codex OAuth exchange failed");
216
218
  setCodexManualInput("");
217
- showToast("Codex connected", "green");
219
+ showToast("Codex connected", "success");
218
220
  setCodexAuthStarted(false);
219
221
  setCodexAuthWaiting(false);
220
222
  await refreshCodexConnection();
221
223
  } catch (err) {
222
- showToast(err.message || "Codex OAuth exchange failed", "red");
224
+ showToast(err.message || "Codex OAuth exchange failed", "error");
223
225
  } finally {
224
226
  setCodexExchanging(false);
225
227
  }
@@ -228,10 +230,10 @@ export const Models = () => {
228
230
  const handleCodexDisconnect = async () => {
229
231
  const result = await disconnectCodex();
230
232
  if (!result.ok) {
231
- showToast(result.error || "Failed to disconnect Codex", "red");
233
+ showToast(result.error || "Failed to disconnect Codex", "error");
232
234
  return;
233
235
  }
234
- showToast("Codex disconnected", "green");
236
+ showToast("Codex disconnected", "success");
235
237
  setCodexAuthStarted(false);
236
238
  setCodexAuthWaiting(false);
237
239
  setCodexManualInput("");
@@ -305,13 +307,13 @@ export const Models = () => {
305
307
  <div class="flex gap-2">
306
308
  <button
307
309
  onclick=${startCodexAuth}
308
- class="text-xs font-medium px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
310
+ class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary"
309
311
  >
310
312
  Reconnect Codex
311
313
  </button>
312
314
  <button
313
315
  onclick=${handleCodexDisconnect}
314
- class="text-xs font-medium px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
316
+ class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-ghost"
315
317
  >
316
318
  Disconnect
317
319
  </button>
@@ -335,7 +337,7 @@ export const Models = () => {
335
337
  </p>
336
338
  <button
337
339
  onclick=${startCodexAuth}
338
- class="text-xs font-medium px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500 shrink-0"
340
+ class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary shrink-0"
339
341
  >
340
342
  Restart
341
343
  </button>
@@ -355,13 +357,16 @@ export const Models = () => {
355
357
  placeholder="http://localhost:1455/auth/callback?code=...&state=..."
356
358
  class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 outline-none focus:border-gray-500"
357
359
  />
358
- <button
359
- onclick=${completeCodexAuth}
360
+ <${ActionButton}
361
+ onClick=${completeCodexAuth}
360
362
  disabled=${!codexManualInput.trim() || codexExchanging}
361
- class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
362
- >
363
- ${codexExchanging ? "Completing..." : "Complete Codex OAuth"}
364
- </button>
363
+ loading=${codexExchanging}
364
+ tone="primary"
365
+ size="sm"
366
+ idleLabel="Complete Codex OAuth"
367
+ loadingLabel="Completing..."
368
+ className="text-xs font-medium px-3 py-1.5"
369
+ />
365
370
  `
366
371
  : null}
367
372
  </div>
@@ -373,10 +378,7 @@ export const Models = () => {
373
378
  return html`
374
379
  <div class="bg-surface border border-border rounded-xl p-4">
375
380
  <div class="flex items-center gap-2 text-sm text-gray-400">
376
- <svg class="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
377
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
378
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
379
- </svg>
381
+ <${LoadingSpinner} className="h-4 w-4" />
380
382
  Loading model settings...
381
383
  </div>
382
384
  </div>
@@ -440,13 +442,16 @@ export const Models = () => {
440
442
  )}
441
443
  </div>
442
444
 
443
- <button
444
- onclick=${saveChanges}
445
+ <${ActionButton}
446
+ onClick=${saveChanges}
445
447
  disabled=${!canSaveChanges}
446
- class="w-full text-sm font-medium px-4 py-2.5 rounded-xl transition-all ac-btn-cyan"
447
- >
448
- ${savingChanges ? "Saving..." : "Save changes"}
449
- </button>
448
+ loading=${savingChanges}
449
+ tone="primary"
450
+ size="md"
451
+ idleLabel="Save changes"
452
+ loadingLabel="Saving..."
453
+ className="w-full py-2.5 transition-all"
454
+ />
450
455
  ${modelDirty && !hasSelectedProviderAuth
451
456
  ? html`
452
457
  <p class="text-xs text-yellow-500">
@@ -2,6 +2,8 @@ import { h } from "https://esm.sh/preact";
2
2
  import { useEffect, useState } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
4
  import { SecretInput } from "../secret-input.js";
5
+ import { ActionButton } from "../action-button.js";
6
+ import { Badge } from "../badge.js";
5
7
  import { isValidGithubRepoInput } from "./welcome-config.js";
6
8
 
7
9
  const html = htm.bind(h);
@@ -41,6 +43,8 @@ export const WelcomeFormStep = ({
41
43
  handleSubmit,
42
44
  }) => {
43
45
  const [repoTouched, setRepoTouched] = useState(false);
46
+ const [showOptionalOpenai, setShowOptionalOpenai] = useState(false);
47
+ const [showOptionalGemini, setShowOptionalGemini] = useState(false);
44
48
 
45
49
  useEffect(() => {
46
50
  if (activeGroup.id !== "github") {
@@ -48,6 +52,13 @@ export const WelcomeFormStep = ({
48
52
  }
49
53
  }, [activeGroup.id]);
50
54
 
55
+ useEffect(() => {
56
+ if (step === totalGroups - 1) {
57
+ setShowOptionalOpenai(!vals.OPENAI_API_KEY);
58
+ setShowOptionalGemini(!vals.GEMINI_API_KEY);
59
+ }
60
+ }, [step === totalGroups - 1]);
61
+
51
62
  return html`
52
63
  <div class="flex items-center justify-between">
53
64
  <div>
@@ -113,30 +124,30 @@ export const WelcomeFormStep = ({
113
124
  ${codexLoading
114
125
  ? html`<span class="text-xs text-gray-500">Checking...</span>`
115
126
  : codexStatus.connected
116
- ? html`<span class="text-xs text-green-400">Connected</span>`
117
- : html`<span class="text-xs text-yellow-400"
118
- >Not connected</span
119
- >`}
127
+ ? html`<${Badge} tone="success">Connected</${Badge}>`
128
+ : html`<${Badge} tone="warning">Not connected</${Badge}>`}
120
129
  </div>
121
130
  <div class="flex gap-2">
122
- <button
123
- type="button"
124
- onclick=${startCodexAuth}
125
- class="text-xs font-medium px-3 py-1.5 rounded-lg ${codexStatus.connected
126
- ? "border border-border text-gray-300 hover:border-gray-500"
127
- : "ac-btn-cyan"}"
128
- >
129
- ${codexStatus.connected ? "Reconnect Codex" : "Connect Codex OAuth"}
130
- </button>
131
+ <${ActionButton}
132
+ onClick=${startCodexAuth}
133
+ tone=${codexStatus.connected || codexAuthStarted
134
+ ? "neutral"
135
+ : "primary"}
136
+ size="sm"
137
+ idleLabel=${codexStatus.connected
138
+ ? "Reconnect Codex"
139
+ : "Connect Codex OAuth"}
140
+ className="font-medium"
141
+ />
131
142
  ${codexStatus.connected &&
132
143
  html`
133
- <button
134
- type="button"
135
- onclick=${handleCodexDisconnect}
136
- class="text-xs font-medium px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
137
- >
138
- Disconnect
139
- </button>
144
+ <${ActionButton}
145
+ onClick=${handleCodexDisconnect}
146
+ tone="ghost"
147
+ size="sm"
148
+ idleLabel="Disconnect"
149
+ className="font-medium"
150
+ />
140
151
  `}
141
152
  </div>
142
153
  ${!codexStatus.connected &&
@@ -158,14 +169,16 @@ export const WelcomeFormStep = ({
158
169
  placeholder="http://localhost:1455/auth/callback?code=...&state=..."
159
170
  class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 outline-none focus:border-gray-500"
160
171
  />
161
- <button
162
- type="button"
163
- onclick=${completeCodexAuth}
172
+ <${ActionButton}
173
+ onClick=${completeCodexAuth}
164
174
  disabled=${!codexManualInput.trim() || codexExchanging}
165
- class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
166
- >
167
- ${codexExchanging ? "Completing..." : "Complete Codex OAuth"}
168
- </button>
175
+ loading=${codexExchanging}
176
+ tone="primary"
177
+ size="sm"
178
+ idleLabel="Complete Codex OAuth"
179
+ loadingLabel="Completing..."
180
+ className="font-medium"
181
+ />
169
182
  </div>
170
183
  `}
171
184
  </div>
@@ -211,9 +224,9 @@ export const WelcomeFormStep = ({
211
224
  ${error}
212
225
  </div>`
213
226
  : null}
214
- ${step === totalGroups - 1 && (!vals.OPENAI_API_KEY || !vals.GEMINI_API_KEY)
227
+ ${step === totalGroups - 1 && (showOptionalOpenai || showOptionalGemini)
215
228
  ? html`
216
- ${!vals.OPENAI_API_KEY
229
+ ${showOptionalOpenai
217
230
  ? html`<div class="space-y-1">
218
231
  <label class="text-xs font-medium text-gray-400"
219
232
  >OpenAI API Key</label
@@ -237,7 +250,7 @@ export const WelcomeFormStep = ({
237
250
  </p>
238
251
  </div>`
239
252
  : null}
240
- ${!vals.GEMINI_API_KEY
253
+ ${showOptionalGemini
241
254
  ? html`<div class="space-y-1">
242
255
  <label class="text-xs font-medium text-gray-400"
243
256
  >Gemini API Key</label
@@ -270,7 +283,7 @@ export const WelcomeFormStep = ({
270
283
  ${step > 0
271
284
  ? html`<button
272
285
  onclick=${goBack}
273
- class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500"
286
+ class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-secondary"
274
287
  >
275
288
  Back
276
289
  </button>`
@@ -289,7 +302,7 @@ export const WelcomeFormStep = ({
289
302
  ${step > 0
290
303
  ? html`<button
291
304
  onclick=${goBack}
292
- class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500"
305
+ class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-secondary"
293
306
  >
294
307
  Back
295
308
  </button>`
@@ -54,14 +54,14 @@ const PairingRow = ({ pairing, onApprove, onReject }) => {
54
54
  <button
55
55
  onclick=${handleApprove}
56
56
  disabled=${!!busyAction}
57
- class="ac-btn-green text-xs font-medium px-3 py-1.5 rounded-lg ${busyAction ? "opacity-60 cursor-not-allowed" : ""}"
57
+ class="ac-btn-green text-xs font-medium px-3 py-1.5 rounded-lg ${busyAction ? "opacity-50 cursor-not-allowed" : ""}"
58
58
  >
59
59
  ${busyAction === "approve" ? "Approving..." : "Approve"}
60
60
  </button>
61
61
  <button
62
62
  onclick=${handleReject}
63
63
  disabled=${!!busyAction}
64
- class="bg-gray-800 text-gray-300 text-xs px-3 py-1.5 rounded-lg hover:bg-gray-700 ${busyAction ? "opacity-60 cursor-not-allowed" : ""}"
64
+ class="ac-btn-secondary text-xs font-medium px-3 py-1.5 rounded-lg ${busyAction ? "opacity-50 cursor-not-allowed" : ""}"
65
65
  >
66
66
  ${busyAction === "reject" ? "Rejecting..." : "Reject"}
67
67
  </button>
@@ -1,6 +1,7 @@
1
1
  import { h } from "https://esm.sh/preact";
2
2
  import { useEffect, useState } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
+ import { LoadingSpinner } from "../loading-spinner.js";
4
5
 
5
6
  const html = htm.bind(h);
6
7
  const kSetupTips = [
@@ -56,8 +57,8 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
56
57
  <button
57
58
  onclick=${onBack}
58
59
  disabled=${loading}
59
- class="w-full text-sm font-medium px-4 py-3 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500 ${loading
60
- ? "opacity-60 cursor-not-allowed"
60
+ class="w-full text-sm font-medium px-4 py-3 rounded-xl transition-all ac-btn-secondary ${loading
61
+ ? "opacity-50 cursor-not-allowed"
61
62
  : ""}"
62
63
  >
63
64
  Back
@@ -65,9 +66,9 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
65
66
  <button
66
67
  onclick=${onRetry}
67
68
  disabled=${loading}
68
- class="w-full text-sm font-medium px-4 py-3 rounded-xl transition-all ${loading
69
- ? "bg-gray-800 text-gray-500 cursor-not-allowed"
70
- : "bg-white text-black hover:opacity-85"}"
69
+ class="w-full text-sm font-medium px-4 py-3 rounded-xl transition-all ac-btn-cyan ${loading
70
+ ? "opacity-50 cursor-not-allowed"
71
+ : ""}"
71
72
  >
72
73
  ${loading ? "Retrying..." : "Retry"}
73
74
  </button>
@@ -82,25 +83,7 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
82
83
  <div
83
84
  class="flex-1 flex flex-col items-center justify-center text-center gap-4"
84
85
  >
85
- <svg
86
- class="animate-spin h-8 w-8 text-white"
87
- viewBox="0 0 24 24"
88
- fill="none"
89
- >
90
- <circle
91
- class="opacity-25"
92
- cx="12"
93
- cy="12"
94
- r="10"
95
- stroke="currentColor"
96
- stroke-width="4"
97
- />
98
- <path
99
- class="opacity-75"
100
- fill="currentColor"
101
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
102
- />
103
- </svg>
86
+ <${LoadingSpinner} className="h-8 w-8 text-white" />
104
87
  <h3 class="text-lg font-semibold text-white">
105
88
  Initializing OpenClaw...
106
89
  </h3>
@@ -0,0 +1,13 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+
4
+ const html = htm.bind(h);
5
+
6
+ export const PageHeader = ({ title = "", actions = null, leading = null }) => html`
7
+ <div class="flex items-center justify-between gap-3">
8
+ <div>
9
+ ${leading || html`<h2 class="font-semibold text-base">${title}</h2>`}
10
+ </div>
11
+ <div class="flex items-center gap-2">${actions}</div>
12
+ </div>
13
+ `;
@@ -1,6 +1,7 @@
1
1
  import { h } from 'https://esm.sh/preact';
2
2
  import { useState } from 'https://esm.sh/preact/hooks';
3
3
  import htm from 'https://esm.sh/htm';
4
+ import { ActionButton } from './action-button.js';
4
5
  const html = htm.bind(h);
5
6
 
6
7
  const PairingRow = ({ p, onApprove, onReject }) => {
@@ -37,8 +38,20 @@ const PairingRow = ({ p, onApprove, onReject }) => {
37
38
  <div class="bg-black/30 rounded-lg p-3 mb-2">
38
39
  <div class="font-medium text-sm mb-2">${label} · <code class="text-gray-400">${p.code || p.id || '?'}</code></div>
39
40
  <div class="flex gap-2">
40
- <button onclick=${() => handle("approve")} class="ac-btn-green text-xs font-medium px-3 py-1.5 rounded-lg">Approve</button>
41
- <button onclick=${() => handle("reject")} class="bg-gray-800 text-gray-300 text-xs px-3 py-1.5 rounded-lg hover:bg-gray-700">Reject</button>
41
+ <${ActionButton}
42
+ onClick=${() => handle("approve")}
43
+ tone="success"
44
+ size="sm"
45
+ idleLabel="Approve"
46
+ className="font-medium px-3 py-1.5"
47
+ />
48
+ <${ActionButton}
49
+ onClick=${() => handle("reject")}
50
+ tone="secondary"
51
+ size="sm"
52
+ idleLabel="Reject"
53
+ className="font-medium px-3 py-1.5"
54
+ />
42
55
  </div>
43
56
  </div>`;
44
57
  };