@chrysb/alphaclaw 0.1.14 → 0.1.15

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.
@@ -10,6 +10,7 @@
10
10
  --text-dim: #383d47;
11
11
  --accent: #63ebff;
12
12
  --accent-dim: rgba(99, 235, 255, 0.4);
13
+ --accent-link: rgba(99, 235, 255, 0.6);
13
14
  }
14
15
 
15
16
  html, body { height: 100%; }
@@ -366,11 +366,15 @@ function App() {
366
366
 
367
367
  <div class="app-content">
368
368
  <div class="max-w-2xl w-full mx-auto space-y-4">
369
- ${tab === "general"
370
- ? html`<${GeneralTab} onSwitchTab=${setTab} />`
371
- : tab === "models"
372
- ? html`<${Models} />`
373
- : html`<${Envars} />`}
369
+ <div style=${{ display: tab === "general" ? "" : "none" }}>
370
+ <${GeneralTab} onSwitchTab=${setTab} />
371
+ </div>
372
+ <div style=${{ display: tab === "models" ? "" : "none" }}>
373
+ <${Models} />
374
+ </div>
375
+ <div style=${{ display: tab === "envars" ? "" : "none" }}>
376
+ <${Envars} />
377
+ </div>
374
378
  </div>
375
379
  </div>
376
380
 
@@ -2,6 +2,7 @@ import { h } from "https://esm.sh/preact";
2
2
  import { useState, useRef } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
4
  import { saveGoogleCredentials } from "../lib/api.js";
5
+ import { SecretInput } from "./secret-input.js";
5
6
  const html = htm.bind(h);
6
7
 
7
8
  export const CredentialsModal = ({ visible, onClose, onSaved }) => {
@@ -110,7 +111,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
110
111
  href="https://console.cloud.google.com/apis/credentials"
111
112
  target="_blank"
112
113
  class="hover:text-white"
113
- style="color: var(--accent)"
114
+ style="color: rgba(99, 235, 255, 0.6)"
114
115
  >Create one →</a
115
116
  >
116
117
  </p>
@@ -148,7 +149,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
148
149
  href="https://console.cloud.google.com/projectcreate"
149
150
  target="_blank"
150
151
  class="hover:text-white"
151
- style="color: var(--accent)"
152
+ style="color: rgba(99, 235, 255, 0.6)"
152
153
  >Create a Google Cloud project</a
153
154
  >${" "}(or use existing)
154
155
  </li>
@@ -157,7 +158,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
157
158
  href="https://console.cloud.google.com/auth/audience"
158
159
  target="_blank"
159
160
  class="hover:text-white"
160
- style="color: var(--accent)"
161
+ style="color: rgba(99, 235, 255, 0.6)"
161
162
  >OAuth consent screen</a
162
163
  >${" "}→ set to <strong>External</strong>
163
164
  </li>
@@ -166,7 +167,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
166
167
  href="https://console.cloud.google.com/auth/audience"
167
168
  target="_blank"
168
169
  class="hover:text-white"
169
- style="color: var(--accent)"
170
+ style="color: rgba(99, 235, 255, 0.6)"
170
171
  >Test users</a
171
172
  >, <strong>add your own email</strong>
172
173
  </li>
@@ -175,7 +176,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
175
176
  href="https://console.cloud.google.com/apis/library"
176
177
  target="_blank"
177
178
  class="hover:text-white"
178
- style="color: var(--accent)"
179
+ style="color: rgba(99, 235, 255, 0.6)"
179
180
  >Enable APIs</a
180
181
  >${" "}for the services you selected below
181
182
  </li>
@@ -184,7 +185,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
184
185
  href="https://console.cloud.google.com/apis/credentials"
185
186
  target="_blank"
186
187
  class="hover:text-white"
187
- style="color: var(--accent)"
188
+ style="color: rgba(99, 235, 255, 0.6)"
188
189
  >Credentials</a
189
190
  >${" "}→ Create OAuth 2.0 Client ID (Web application)
190
191
  </li>
@@ -195,7 +196,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
195
196
  Copy Client ID + Secret (or download credentials JSON)
196
197
  </li>
197
198
  </ol>
198
- <p class="mt-3" style="color: rgba(99, 235, 255, 0.6)">
199
+ <p class="mt-3 text-yellow-500/80">
199
200
  ⚠️ App will be in "Testing" mode. Only emails added as
200
201
  Test Users can sign in (up to 100).
201
202
  </p>
@@ -209,7 +210,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
209
210
  href="https://console.cloud.google.com/projectcreate"
210
211
  target="_blank"
211
212
  class="hover:text-white"
212
- style="color: var(--accent)"
213
+ style="color: rgba(99, 235, 255, 0.6)"
213
214
  >Create a Google Cloud project</a
214
215
  >${" "}(or use existing)
215
216
  </li>
@@ -218,7 +219,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
218
219
  href="https://console.cloud.google.com/auth/audience"
219
220
  target="_blank"
220
221
  class="hover:text-white"
221
- style="color: var(--accent)"
222
+ style="color: rgba(99, 235, 255, 0.6)"
222
223
  >OAuth consent screen</a
223
224
  >${" "}→ set to <strong>Internal</strong> (Workspace
224
225
  only)
@@ -228,7 +229,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
228
229
  href="https://console.cloud.google.com/apis/library"
229
230
  target="_blank"
230
231
  class="hover:text-white"
231
- style="color: var(--accent)"
232
+ style="color: rgba(99, 235, 255, 0.6)"
232
233
  >Enable APIs</a
233
234
  >${" "}for the services you selected below
234
235
  </li>
@@ -237,7 +238,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
237
238
  href="https://console.cloud.google.com/apis/credentials"
238
239
  target="_blank"
239
240
  class="hover:text-white"
240
- style="color: var(--accent)"
241
+ style="color: rgba(99, 235, 255, 0.6)"
241
242
  >Credentials</a
242
243
  >${" "}→ Create OAuth 2.0 Client ID (Web application)
243
244
  </li>
@@ -248,7 +249,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
248
249
  Copy Client ID + Secret (or download credentials JSON)
249
250
  </li>
250
251
  </ol>
251
- <p class="mt-3" style="color: rgba(99, 235, 255, 0.6)">
252
+ <p class="mt-3 text-green-500/80">
252
253
  ✓ Internal apps skip test users and verification. Only
253
254
  users in your Workspace org can authorize this Google app.
254
255
  </p>
@@ -285,24 +286,22 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
285
286
  </div>
286
287
  <div>
287
288
  <label class="text-sm text-gray-400 block mb-1">Client ID</label>
288
- <input
289
- type="text"
289
+ <${SecretInput}
290
290
  value=${clientId}
291
291
  onInput=${(e) => setClientId(e.target.value)}
292
292
  placeholder="xxxx.apps.googleusercontent.com"
293
- class="w-full bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
293
+ inputClass="flex-1 bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
294
294
  />
295
295
  </div>
296
296
  <div>
297
297
  <label class="text-sm text-gray-400 block mb-1"
298
298
  >Client Secret</label
299
299
  >
300
- <input
301
- type="password"
300
+ <${SecretInput}
302
301
  value=${clientSecret}
303
302
  onInput=${(e) => setClientSecret(e.target.value)}
304
303
  placeholder="GOCSPX-..."
305
- class="w-full bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
304
+ inputClass="flex-1 bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
306
305
  />
307
306
  </div>
308
307
  <div>
@@ -3,6 +3,7 @@ import { useState, useEffect, useCallback } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
4
  import { fetchEnvVars, saveEnvVars, restartGateway } from "../lib/api.js";
5
5
  import { showToast } from "./toast.js";
6
+ import { SecretInput } from "./secret-input.js";
6
7
  const html = htm.bind(h);
7
8
 
8
9
  const kGroupLabels = {
@@ -15,22 +16,20 @@ const kGroupLabels = {
15
16
  const kGroupOrder = ["github", "channels", "tools", "custom"];
16
17
 
17
18
  const kHintByKey = {
18
- ANTHROPIC_API_KEY: html`from <a href="https://console.anthropic.com" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">console.anthropic.com</a>`,
19
+ ANTHROPIC_API_KEY: html`from <a href="https://console.anthropic.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">console.anthropic.com</a>`,
19
20
  ANTHROPIC_TOKEN: html`from <code class="text-xs bg-black/30 px-1 rounded">claude setup-token</code>`,
20
- OPENAI_API_KEY: html`from <a href="https://platform.openai.com" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">platform.openai.com</a>`,
21
- GEMINI_API_KEY: html`from <a href="https://aistudio.google.com" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">aistudio.google.com</a>`,
22
- GITHUB_TOKEN: html`classic PAT · <code class="text-xs bg-black/30 px-1 rounded">repo</code> scope · <a href="https://github.com/settings/tokens" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">github settings</a>`,
21
+ OPENAI_API_KEY: html`from <a href="https://platform.openai.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">platform.openai.com</a>`,
22
+ GEMINI_API_KEY: html`from <a href="https://aistudio.google.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">aistudio.google.com</a>`,
23
+ GITHUB_TOKEN: html`classic PAT · <code class="text-xs bg-black/30 px-1 rounded">repo</code> scope · <a href="https://github.com/settings/tokens" target="_blank" class="hover:underline" style="color: var(--accent-link)">github settings</a>`,
23
24
  GITHUB_WORKSPACE_REPO: html`use <code class="text-xs bg-black/30 px-1 rounded">owner/repo</code> or <code class="text-xs bg-black/30 px-1 rounded">https://github.com/owner/repo</code>`,
24
- TELEGRAM_BOT_TOKEN: html`from <a href="https://t.me/BotFather" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">@BotFather</a> · <a href="https://docs.openclaw.ai/channels/telegram" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">full guide</a>`,
25
- DISCORD_BOT_TOKEN: html`from <a href="https://discord.com/developers/applications" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">developer portal</a> · <a href="https://docs.openclaw.ai/channels/discord" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">full guide</a>`,
26
- BRAVE_API_KEY: html`from <a href="https://brave.com/search/api/" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">brave.com/search/api</a> — free tier available`,
25
+ TELEGRAM_BOT_TOKEN: html`from <a href="https://t.me/BotFather" target="_blank" class="hover:underline" style="color: var(--accent-link)">@BotFather</a> · <a href="https://docs.openclaw.ai/channels/telegram" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
26
+ DISCORD_BOT_TOKEN: html`from <a href="https://discord.com/developers/applications" target="_blank" class="hover:underline" style="color: var(--accent-link)">developer portal</a> · <a href="https://docs.openclaw.ai/channels/discord" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
27
+ BRAVE_API_KEY: html`from <a href="https://brave.com/search/api/" target="_blank" class="hover:underline" style="color: var(--accent-link)">brave.com/search/api</a> — free tier available`,
27
28
  };
28
29
 
29
30
  const getHintContent = (envVar) => kHintByKey[envVar.key] || envVar.hint || "";
30
31
 
31
32
  const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
32
- const [visible, setVisible] = useState(false);
33
- const isSecret = !!envVar.value;
34
33
  const hint = getHintContent(envVar);
35
34
 
36
35
  return html`
@@ -45,23 +44,14 @@ const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
45
44
  </div>
46
45
  <div class="flex-1 min-w-0">
47
46
  <div class="flex items-center gap-1">
48
- <input
49
- type=${isSecret && !visible ? "password" : "text"}
47
+ <${SecretInput}
50
48
  value=${envVar.value}
51
- placeholder=${envVar.value ? "" : "not set"}
52
49
  onInput=${(e) => onChange(envVar.key, e.target.value)}
53
- class="w-full bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
50
+ placeholder=${envVar.value ? "" : "not set"}
51
+ isSecret=${!!envVar.value}
52
+ inputClass="flex-1 min-w-0 bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
54
53
  disabled=${disabled}
55
54
  />
56
- ${isSecret
57
- ? html`<button
58
- onclick=${() => setVisible(!visible)}
59
- class="text-gray-500 hover:text-gray-300 px-1 text-xs shrink-0"
60
- title=${visible ? "Hide" : "Show"}
61
- >
62
- ${visible ? "Hide" : "Show"}
63
- </button>`
64
- : null}
65
55
  ${envVar.group === "custom"
66
56
  ? html`<button
67
57
  onclick=${() => onDelete(envVar.key)}
@@ -0,0 +1,108 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { kAllAiAuthFields } from "../../lib/model-config.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const kWelcomeGroups = [
8
+ {
9
+ id: "ai",
10
+ title: "Primary Agent Model",
11
+ description: "Choose your main model and authenticate its provider",
12
+ fields: kAllAiAuthFields,
13
+ validate: (vals, ctx = {}) => !!(vals.MODEL_KEY && ctx.hasAi),
14
+ },
15
+ {
16
+ id: "github",
17
+ title: "GitHub",
18
+ description: "Backs up your agent's config and workspace",
19
+ fields: [
20
+ {
21
+ key: "GITHUB_WORKSPACE_REPO",
22
+ label: "Workspace Repo",
23
+ hint: "A new private repo will be created for you",
24
+ placeholder: "username/my-agent",
25
+ isText: true,
26
+ },
27
+ {
28
+ key: "GITHUB_TOKEN",
29
+ label: "Personal Access Token",
30
+ hint: html`Create a classic PAT on${" "}<a
31
+ href="https://github.com/settings/tokens"
32
+ target="_blank"
33
+ class="hover:underline"
34
+ style="color: var(--accent-link)"
35
+ >GitHub settings</a
36
+ >${" "}with${" "}<code class="text-xs bg-black/30 px-1 rounded"
37
+ >repo</code
38
+ >${" "}scope`,
39
+ placeholder: "ghp_...",
40
+ },
41
+ ],
42
+ validate: (vals) => !!(vals.GITHUB_TOKEN && vals.GITHUB_WORKSPACE_REPO),
43
+ },
44
+ {
45
+ id: "channels",
46
+ title: "Channels",
47
+ description: "At least one is required to talk to your agent",
48
+ fields: [
49
+ {
50
+ key: "TELEGRAM_BOT_TOKEN",
51
+ label: "Telegram Bot Token",
52
+ hint: html`From${" "}<a
53
+ href="https://t.me/BotFather"
54
+ target="_blank"
55
+ class="hover:underline"
56
+ style="color: var(--accent-link)"
57
+ >@BotFather</a
58
+ >${" "}·${" "}<a
59
+ href="https://docs.openclaw.ai/channels/telegram"
60
+ target="_blank"
61
+ class="hover:underline"
62
+ style="color: var(--accent-link)"
63
+ >full guide</a
64
+ >`,
65
+ placeholder: "123456789:AAH...",
66
+ },
67
+ {
68
+ key: "DISCORD_BOT_TOKEN",
69
+ label: "Discord Bot Token",
70
+ hint: html`From${" "}<a
71
+ href="https://discord.com/developers/applications"
72
+ target="_blank"
73
+ class="hover:underline"
74
+ style="color: var(--accent-link)"
75
+ >Developer Portal</a
76
+ >${" "}·${" "}<a
77
+ href="https://docs.openclaw.ai/channels/discord"
78
+ target="_blank"
79
+ class="hover:underline"
80
+ style="color: var(--accent-link)"
81
+ >full guide</a
82
+ >`,
83
+ placeholder: "MTQ3...",
84
+ },
85
+ ],
86
+ validate: (vals) => !!(vals.TELEGRAM_BOT_TOKEN || vals.DISCORD_BOT_TOKEN),
87
+ },
88
+ {
89
+ id: "tools",
90
+ title: "Tools (optional)",
91
+ description: "Enable extra capabilities for your agent",
92
+ fields: [
93
+ {
94
+ key: "BRAVE_API_KEY",
95
+ label: "Brave Search API Key",
96
+ hint: html`From${" "}<a
97
+ href="https://brave.com/search/api/"
98
+ target="_blank"
99
+ class="hover:underline"
100
+ style="color: var(--accent-link)"
101
+ >brave.com/search/api</a
102
+ >${" "}-${" "}free tier available`,
103
+ placeholder: "BSA...",
104
+ },
105
+ ],
106
+ validate: () => true,
107
+ },
108
+ ];
@@ -0,0 +1,283 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { SecretInput } from "../secret-input.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const WelcomeFormStep = ({
8
+ activeGroup,
9
+ vals,
10
+ hasAi,
11
+ setValue,
12
+ modelOptions,
13
+ modelsLoading,
14
+ modelsError,
15
+ canToggleFullCatalog,
16
+ showAllModels,
17
+ setShowAllModels,
18
+ selectedProvider,
19
+ codexLoading,
20
+ codexStatus,
21
+ startCodexAuth,
22
+ handleCodexDisconnect,
23
+ codexAuthStarted,
24
+ codexAuthWaiting,
25
+ codexManualInput,
26
+ setCodexManualInput,
27
+ completeCodexAuth,
28
+ codexExchanging,
29
+ visibleAiFieldKeys,
30
+ error,
31
+ step,
32
+ totalGroups,
33
+ currentGroupValid,
34
+ goBack,
35
+ goNext,
36
+ loading,
37
+ allValid,
38
+ handleSubmit,
39
+ }) => html`
40
+ <div class="flex items-center justify-between">
41
+ <div>
42
+ <h2 class="text-sm font-medium text-gray-200">${activeGroup.title}</h2>
43
+ <p class="text-xs text-gray-500">${activeGroup.description}</p>
44
+ </div>
45
+ ${activeGroup.validate(vals, { hasAi })
46
+ ? html`<span
47
+ class="text-xs font-medium px-2 py-0.5 rounded-full bg-green-900/50 text-green-400"
48
+ >✓</span
49
+ >`
50
+ : activeGroup.id !== "tools"
51
+ ? html`<span
52
+ class="text-xs font-medium px-2 py-0.5 rounded-full bg-yellow-900/50 text-yellow-400"
53
+ >Required</span
54
+ >`
55
+ : null}
56
+ </div>
57
+
58
+ ${activeGroup.id === "ai" &&
59
+ html`
60
+ <div class="space-y-1">
61
+ <label class="text-xs font-medium text-gray-400">Model</label>
62
+ <select
63
+ value=${vals.MODEL_KEY || ""}
64
+ onInput=${(e) => setValue("MODEL_KEY", e.target.value)}
65
+ class="w-full bg-black/30 border border-border rounded-lg pl-3 pr-8 py-2 text-sm text-gray-200 outline-none focus:border-gray-500"
66
+ >
67
+ <option value="">Select a model</option>
68
+ ${modelOptions.map(
69
+ (model) => html`
70
+ <option value=${model.key}>${model.label || model.key}</option>
71
+ `,
72
+ )}
73
+ </select>
74
+ <p class="text-xs text-gray-600">
75
+ ${modelsLoading
76
+ ? "Loading model catalog..."
77
+ : modelsError
78
+ ? modelsError
79
+ : ""}
80
+ </p>
81
+ ${canToggleFullCatalog &&
82
+ html`
83
+ <button
84
+ type="button"
85
+ onclick=${() => setShowAllModels((prev) => !prev)}
86
+ class="text-xs text-gray-500 hover:text-gray-300"
87
+ >
88
+ ${showAllModels
89
+ ? "Show recommended models"
90
+ : "Show full model catalog"}
91
+ </button>
92
+ `}
93
+ </div>
94
+ `}
95
+ ${activeGroup.id === "ai" &&
96
+ selectedProvider === "openai-codex" &&
97
+ html`
98
+ <div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
99
+ <div class="flex items-center justify-between">
100
+ <span class="text-xs text-gray-400">Codex OAuth</span>
101
+ ${codexLoading
102
+ ? html`<span class="text-xs text-gray-500">Checking...</span>`
103
+ : codexStatus.connected
104
+ ? html`<span class="text-xs text-green-400">Connected</span>`
105
+ : html`<span class="text-xs text-yellow-400">Not connected</span>`}
106
+ </div>
107
+ <div class="flex gap-2">
108
+ <button
109
+ type="button"
110
+ onclick=${startCodexAuth}
111
+ class="text-xs font-medium px-3 py-1.5 rounded-lg ${codexStatus.connected
112
+ ? "border border-border text-gray-300 hover:border-gray-500"
113
+ : "bg-white text-black hover:opacity-85"}"
114
+ >
115
+ ${codexStatus.connected ? "Reconnect Codex" : "Connect Codex OAuth"}
116
+ </button>
117
+ ${codexStatus.connected &&
118
+ html`
119
+ <button
120
+ type="button"
121
+ onclick=${handleCodexDisconnect}
122
+ class="text-xs font-medium px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
123
+ >
124
+ Disconnect
125
+ </button>
126
+ `}
127
+ </div>
128
+ ${!codexStatus.connected &&
129
+ codexAuthStarted &&
130
+ html`
131
+ <div class="space-y-1 pt-1">
132
+ <p class="text-xs text-gray-500">
133
+ ${codexAuthWaiting
134
+ ? "Complete login in the popup, then paste the full redirect URL from the address bar (starts with "
135
+ : "Paste the full redirect URL from the address bar (starts with "}
136
+ <code class="text-xs bg-black/30 px-1 rounded"
137
+ >http://localhost:1455/auth/callback</code
138
+ >) ${codexAuthWaiting ? " to finish setup." : " to finish setup."}
139
+ </p>
140
+ <input
141
+ type="text"
142
+ value=${codexManualInput}
143
+ onInput=${(e) => setCodexManualInput(e.target.value)}
144
+ placeholder="http://localhost:1455/auth/callback?code=...&state=..."
145
+ 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"
146
+ />
147
+ <button
148
+ type="button"
149
+ onclick=${completeCodexAuth}
150
+ disabled=${!codexManualInput.trim() || codexExchanging}
151
+ class="text-xs font-medium px-3 py-1.5 rounded-lg ${!codexManualInput.trim() ||
152
+ codexExchanging
153
+ ? "bg-gray-700 text-gray-400 cursor-not-allowed"
154
+ : "bg-white text-black hover:opacity-85"}"
155
+ >
156
+ ${codexExchanging ? "Completing..." : "Complete Codex OAuth"}
157
+ </button>
158
+ </div>
159
+ `}
160
+ </div>
161
+ `}
162
+ ${(activeGroup.id === "ai"
163
+ ? activeGroup.fields.filter((field) => visibleAiFieldKeys.has(field.key))
164
+ : activeGroup.fields
165
+ ).map(
166
+ (field) => html`
167
+ <div class="space-y-1" key=${field.key}>
168
+ <label class="text-xs font-medium text-gray-400">${field.label}</label>
169
+ <${SecretInput}
170
+ key=${field.key}
171
+ value=${vals[field.key] || ""}
172
+ onInput=${(e) => setValue(field.key, e.target.value)}
173
+ placeholder=${field.placeholder || ""}
174
+ isSecret=${!field.isText}
175
+ inputClass="flex-1 bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
176
+ />
177
+ <p class="text-xs text-gray-600">${field.hint}</p>
178
+ </div>
179
+ `,
180
+ )}
181
+ ${error
182
+ ? html`<div
183
+ class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm"
184
+ >
185
+ ${error}
186
+ </div>`
187
+ : null}
188
+ ${step === totalGroups - 1 && (!vals.OPENAI_API_KEY || !vals.GEMINI_API_KEY)
189
+ ? html`
190
+ ${!vals.OPENAI_API_KEY
191
+ ? html`<div class="space-y-1">
192
+ <label class="text-xs font-medium text-gray-400"
193
+ >OpenAI API Key</label
194
+ >
195
+ <${SecretInput}
196
+ value=${vals.OPENAI_API_KEY || ""}
197
+ onInput=${(e) => setValue("OPENAI_API_KEY", e.target.value)}
198
+ placeholder="sk-..."
199
+ isSecret=${true}
200
+ inputClass="flex-1 bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
201
+ />
202
+ <p class="text-xs text-gray-600">
203
+ Used for memory embeddings -${" "}
204
+ <a
205
+ href="https://platform.openai.com"
206
+ target="_blank"
207
+ class="hover:underline"
208
+ style="color: var(--accent-link)"
209
+ >get key</a
210
+ >
211
+ </p>
212
+ </div>`
213
+ : null}
214
+ ${!vals.GEMINI_API_KEY
215
+ ? html`<div class="space-y-1">
216
+ <label class="text-xs font-medium text-gray-400"
217
+ >Gemini API Key</label
218
+ >
219
+ <${SecretInput}
220
+ value=${vals.GEMINI_API_KEY || ""}
221
+ onInput=${(e) => setValue("GEMINI_API_KEY", e.target.value)}
222
+ placeholder="AI..."
223
+ isSecret=${true}
224
+ inputClass="flex-1 bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
225
+ />
226
+ <p class="text-xs text-gray-600">
227
+ Used for memory embeddings and Nano Banana -${" "}
228
+ <a
229
+ href="https://aistudio.google.com"
230
+ target="_blank"
231
+ class="hover:underline"
232
+ style="color: var(--accent-link)"
233
+ >get key</a
234
+ >
235
+ </p>
236
+ </div>`
237
+ : null}
238
+ `
239
+ : null}
240
+
241
+ <div class="grid grid-cols-2 gap-2 pt-3">
242
+ ${step < totalGroups - 1
243
+ ? html`
244
+ ${step > 0
245
+ ? html`<button
246
+ onclick=${goBack}
247
+ 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"
248
+ >
249
+ Back
250
+ </button>`
251
+ : html`<div class="w-full"></div>`}
252
+ <button
253
+ onclick=${goNext}
254
+ disabled=${!currentGroupValid}
255
+ class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ${currentGroupValid
256
+ ? "bg-white text-black hover:opacity-85"
257
+ : "bg-gray-800 text-gray-500 cursor-not-allowed"}"
258
+ >
259
+ Next
260
+ </button>
261
+ `
262
+ : html`
263
+ ${step > 0
264
+ ? html`<button
265
+ onclick=${goBack}
266
+ 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"
267
+ >
268
+ Back
269
+ </button>`
270
+ : html`<div class="w-full"></div>`}
271
+ <button
272
+ onclick=${handleSubmit}
273
+ disabled=${!allValid || loading}
274
+ class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ${allValid &&
275
+ !loading
276
+ ? "bg-white text-black hover:opacity-85"
277
+ : "bg-gray-800 text-gray-500 cursor-not-allowed"}"
278
+ >
279
+ ${loading ? "Starting..." : "Complete Setup"}
280
+ </button>
281
+ `}
282
+ </div>
283
+ `;
@@ -0,0 +1,57 @@
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 WelcomeHeader = ({
7
+ groups,
8
+ step,
9
+ isSetupStep,
10
+ stepNumber,
11
+ activeStepLabel,
12
+ vals,
13
+ hasAi,
14
+ }) => {
15
+ const progressSteps = [...groups, { id: "setup", title: "Initializing" }];
16
+
17
+ return html`
18
+ <div class="text-center mb-1">
19
+ <img
20
+ src="./img/logo.svg"
21
+ alt="alphaclaw"
22
+ class="mx-auto mb-3"
23
+ width="32"
24
+ height="33"
25
+ />
26
+ <h1 class="text-2xl font-semibold mb-2">Setup</h1>
27
+ <p style="color: var(--text-muted)" class="text-sm">
28
+ Let's get your agent running
29
+ </p>
30
+ <p class="text-xs my-2" style="color: var(--text-dim)">
31
+ Step ${stepNumber} of ${progressSteps.length} - ${activeStepLabel}
32
+ </p>
33
+ </div>
34
+
35
+ <div class="flex items-center gap-2">
36
+ ${progressSteps.map((group, idx) => {
37
+ const isFinalStep = idx === progressSteps.length - 1;
38
+ const isActive = idx === step;
39
+ const isComplete = isFinalStep
40
+ ? isSetupStep
41
+ : idx < step && group.validate(vals, { hasAi });
42
+ const bg = isActive
43
+ ? "rgba(99, 235, 255, 0.9)"
44
+ : isComplete
45
+ ? "rgba(99, 235, 255, 0.55)"
46
+ : "rgba(82, 94, 122, 0.45)";
47
+ return html`
48
+ <div
49
+ class="h-1 flex-1 rounded-full transition-colors"
50
+ style=${{ background: bg }}
51
+ title=${group.title}
52
+ ></div>
53
+ `;
54
+ })}
55
+ </div>
56
+ `;
57
+ };
@@ -0,0 +1,45 @@
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 WelcomeSetupStep = ({ error, loading, onRetry }) => html`
7
+ <div class="py-10 flex flex-col items-center text-center gap-4">
8
+ <svg
9
+ class="animate-spin h-8 w-8 text-white"
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ >
13
+ <circle
14
+ class="opacity-25"
15
+ cx="12"
16
+ cy="12"
17
+ r="10"
18
+ stroke="currentColor"
19
+ stroke-width="4"
20
+ />
21
+ <path
22
+ class="opacity-75"
23
+ fill="currentColor"
24
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
25
+ />
26
+ </svg>
27
+ <h3 class="text-lg font-semibold text-white">Initializing OpenClaw...</h3>
28
+ <p class="text-sm text-gray-500">This could take 10-15 seconds</p>
29
+ </div>
30
+
31
+ ${error
32
+ ? html`<div class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm">
33
+ ${error}
34
+ </div>
35
+ <button
36
+ onclick=${onRetry}
37
+ disabled=${loading}
38
+ class="w-full text-sm font-medium px-4 py-3 rounded-xl transition-all ${loading
39
+ ? "bg-gray-800 text-gray-500 cursor-not-allowed"
40
+ : "bg-white text-black hover:opacity-85"}"
41
+ >
42
+ ${loading ? "Retrying..." : "Retry"}
43
+ </button>`
44
+ : null}
45
+ `;
@@ -46,7 +46,7 @@ export function ScopePicker({ scopes, onToggle, apiStatus, loading }) {
46
46
  apiIndicator = html`<span class="text-gray-500 text-xs flex items-center gap-1"><span class="inline-block w-3 h-3 border-2 border-gray-500 border-t-transparent rounded-full animate-spin"></span></span>`;
47
47
  } else if (api) {
48
48
  if (api.status === 'ok') {
49
- apiIndicator = html`<a href=${api.enableUrl || getApiEnableUrl(s.key)} target="_blank" class="text-green-500 hover:text-green-300 text-xs">✓ API</a>`;
49
+ apiIndicator = html`<a href=${api.enableUrl || getApiEnableUrl(s.key)} target="_blank" class="text-green-500 hover:text-green-300 text-xs px-1.5 py-0.5 rounded bg-green-500/10">API ✓</a>`;
50
50
  } else if (api.status === 'not_enabled') {
51
51
  apiIndicator = html`<a href=${api.enableUrl} target="_blank" class="text-red-400 hover:text-red-300 text-xs underline">Enable API</a>`;
52
52
  } else if (api.status === 'error') {
@@ -0,0 +1,45 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useState } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ const html = htm.bind(h);
5
+
6
+ /**
7
+ * Reusable input with show/hide toggle for secret values.
8
+ *
9
+ * Props:
10
+ * value, onInput, placeholder, inputClass, disabled
11
+ * isSecret – treat as password field (default true)
12
+ */
13
+ export const SecretInput = ({
14
+ value = "",
15
+ onInput,
16
+ placeholder = "",
17
+ inputClass = "",
18
+ disabled = false,
19
+ isSecret = true,
20
+ }) => {
21
+ const [visible, setVisible] = useState(false);
22
+ const showToggle = isSecret;
23
+
24
+ return html`
25
+ <div class="flex-1 min-w-0 flex items-center gap-1">
26
+ <input
27
+ type=${isSecret && !visible ? "password" : "text"}
28
+ value=${value}
29
+ placeholder=${placeholder}
30
+ onInput=${onInput}
31
+ disabled=${disabled}
32
+ class=${inputClass}
33
+ />
34
+ ${showToggle
35
+ ? html`<button
36
+ type="button"
37
+ onclick=${() => setVisible((v) => !v)}
38
+ class="text-gray-500 hover:text-gray-300 px-1 text-xs shrink-0"
39
+ >
40
+ ${visible ? "Hide" : "Show"}
41
+ </button>`
42
+ : null}
43
+ </div>
44
+ `;
45
+ };
@@ -12,113 +12,24 @@ import {
12
12
  getModelProvider,
13
13
  getFeaturedModels,
14
14
  getVisibleAiFieldKeys,
15
- kAllAiAuthFields,
16
15
  } from "../lib/model-config.js";
16
+ import { kWelcomeGroups } from "./onboarding/welcome-config.js";
17
+ import { WelcomeHeader } from "./onboarding/welcome-header.js";
18
+ import { WelcomeSetupStep } from "./onboarding/welcome-setup-step.js";
19
+ import { WelcomeFormStep } from "./onboarding/welcome-form-step.js";
17
20
  const html = htm.bind(h);
18
-
19
- const kGroups = [
20
- {
21
- id: "ai",
22
- title: "Primary Agent Model",
23
- description: "Choose your main model and authenticate its provider",
24
- fields: kAllAiAuthFields,
25
- validate: (vals, ctx = {}) => !!(vals.MODEL_KEY && ctx.hasAi),
26
- },
27
- {
28
- id: "github",
29
- title: "GitHub",
30
- description: "Backs up your agent's config and workspace",
31
- fields: [
32
- {
33
- key: "GITHUB_TOKEN",
34
- label: "Personal Access Token",
35
- hint: html`Create a classic PAT at${" "}<a
36
- href="https://github.com/settings/tokens"
37
- target="_blank"
38
- class="hover:underline" style="color: var(--accent)"
39
- >github.com/settings/tokens</a
40
- >${" "}with${" "}<code class="text-xs bg-black/30 px-1 rounded">repo</code>${" "}scope`,
41
- placeholder: "ghp_...",
42
- },
43
- {
44
- key: "GITHUB_WORKSPACE_REPO",
45
- label: "Workspace Repo",
46
- hint: "A new private repo will be created for you",
47
- placeholder: "username/my-agent",
48
- isText: true,
49
- },
50
- ],
51
- validate: (vals) => !!(vals.GITHUB_TOKEN && vals.GITHUB_WORKSPACE_REPO),
52
- },
53
- {
54
- id: "channels",
55
- title: "Channels",
56
- description: "At least one is required to talk to your agent",
57
- fields: [
58
- {
59
- key: "TELEGRAM_BOT_TOKEN",
60
- label: "Telegram Bot Token",
61
- hint: html`From${" "}<a
62
- href="https://t.me/BotFather"
63
- target="_blank"
64
- class="hover:underline" style="color: var(--accent)"
65
- >@BotFather</a
66
- >${" "}·${" "}<a
67
- href="https://docs.openclaw.ai/channels/telegram"
68
- target="_blank"
69
- class="hover:underline" style="color: var(--accent)"
70
- >full guide</a
71
- >`,
72
- placeholder: "123456789:AAH...",
73
- },
74
- {
75
- key: "DISCORD_BOT_TOKEN",
76
- label: "Discord Bot Token",
77
- hint: html`From${" "}<a
78
- href="https://discord.com/developers/applications"
79
- target="_blank"
80
- class="hover:underline" style="color: var(--accent)"
81
- >Developer Portal</a
82
- >${" "}·${" "}<a
83
- href="https://docs.openclaw.ai/channels/discord"
84
- target="_blank"
85
- class="hover:underline" style="color: var(--accent)"
86
- >full guide</a
87
- >`,
88
- placeholder: "MTQ3...",
89
- },
90
- ],
91
- validate: (vals) => !!(vals.TELEGRAM_BOT_TOKEN || vals.DISCORD_BOT_TOKEN),
92
- },
93
- {
94
- id: "tools",
95
- title: "Tools (optional)",
96
- description: "Enable extra capabilities for your agent",
97
- fields: [
98
- {
99
- key: "BRAVE_API_KEY",
100
- label: "Brave Search API Key",
101
- hint: html`From${" "}<a
102
- href="https://brave.com/search/api/"
103
- target="_blank"
104
- class="hover:underline" style="color: var(--accent)"
105
- >brave.com/search/api</a
106
- >${" "}-${" "}free tier available`,
107
- placeholder: "BSA...",
108
- },
109
- ],
110
- validate: () => true,
111
- },
112
- ];
21
+ const kOnboardingStorageKey = "openclaw_setup";
22
+ const kOnboardingStepKey = "_step";
113
23
 
114
24
  export const Welcome = ({ onComplete }) => {
115
- const [vals, setVals] = useState(() => {
25
+ const [initialSetupState] = useState(() => {
116
26
  try {
117
- return JSON.parse(localStorage.getItem("openclaw_setup") || "{}");
27
+ return JSON.parse(localStorage.getItem(kOnboardingStorageKey) || "{}");
118
28
  } catch {
119
29
  return {};
120
30
  }
121
31
  });
32
+ const [vals, setVals] = useState(() => ({ ...initialSetupState }));
122
33
  const [models, setModels] = useState([]);
123
34
  const [modelsLoading, setModelsLoading] = useState(true);
124
35
  const [modelsError, setModelsError] = useState(null);
@@ -133,10 +44,6 @@ export const Welcome = ({ onComplete }) => {
133
44
  const [error, setError] = useState(null);
134
45
  const codexPopupPollRef = useRef(null);
135
46
 
136
- useEffect(() => {
137
- localStorage.setItem("openclaw_setup", JSON.stringify(vals));
138
- }, [vals]);
139
-
140
47
  useEffect(() => {
141
48
  fetchModels()
142
49
  .then((result) => {
@@ -225,7 +132,31 @@ export const Welcome = ({ onComplete }) => {
225
132
  ? !!(codexStatus.connected || vals.OPENAI_API_KEY)
226
133
  : false;
227
134
 
228
- const allValid = kGroups.every((g) => g.validate(vals, { hasAi }));
135
+ const allValid = kWelcomeGroups.every((g) => g.validate(vals, { hasAi }));
136
+ const kFinalSetupStep = kWelcomeGroups.length;
137
+ const [step, setStep] = useState(() => {
138
+ const parsedStep = Number.parseInt(
139
+ String(initialSetupState?.[kOnboardingStepKey] || ""),
140
+ 10,
141
+ );
142
+ if (!Number.isFinite(parsedStep)) return 0;
143
+ return Math.max(0, Math.min(kFinalSetupStep, parsedStep));
144
+ });
145
+ const isSetupStep = step === kFinalSetupStep;
146
+ const activeGroup = !isSetupStep ? kWelcomeGroups[step] : null;
147
+ const currentGroupValid = activeGroup
148
+ ? activeGroup.validate(vals, { hasAi })
149
+ : false;
150
+
151
+ useEffect(() => {
152
+ localStorage.setItem(
153
+ kOnboardingStorageKey,
154
+ JSON.stringify({
155
+ ...vals,
156
+ [kOnboardingStepKey]: step,
157
+ }),
158
+ );
159
+ }, [vals, step]);
229
160
 
230
161
  const startCodexAuth = () => {
231
162
  if (codexStatus.connected) return;
@@ -287,6 +218,7 @@ export const Welcome = ({ onComplete }) => {
287
218
 
288
219
  const handleSubmit = async () => {
289
220
  if (!allValid || loading) return;
221
+ setStep(kFinalSetupStep);
290
222
  setLoading(true);
291
223
  setError(null);
292
224
 
@@ -297,7 +229,7 @@ export const Welcome = ({ onComplete }) => {
297
229
  .map(([key, value]) => ({ key, value }));
298
230
  const result = await runOnboard(vars, vals.MODEL_KEY);
299
231
  if (!result.ok) throw new Error(result.error || "Onboarding failed");
300
- localStorage.removeItem("openclaw_setup");
232
+ localStorage.removeItem(kOnboardingStorageKey);
301
233
  onComplete();
302
234
  } catch (err) {
303
235
  console.error("Onboard error:", err);
@@ -306,237 +238,76 @@ export const Welcome = ({ onComplete }) => {
306
238
  }
307
239
  };
308
240
 
309
- if (loading) {
310
- return html`
311
- <div
312
- class="fixed inset-0 flex items-center justify-center z-50"
313
- style="background: var(--bg)"
314
- >
315
- <div class="flex flex-col items-center gap-4">
316
- <svg
317
- class="animate-spin h-8 w-8 text-white"
318
- viewBox="0 0 24 24"
319
- fill="none"
320
- >
321
- <circle
322
- class="opacity-25"
323
- cx="12"
324
- cy="12"
325
- r="10"
326
- stroke="currentColor"
327
- stroke-width="4"
328
- />
329
- <path
330
- class="opacity-75"
331
- fill="currentColor"
332
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
333
- />
334
- </svg>
335
- <h2 class="text-lg font-semibold text-white">
336
- Initializing <span style="color: var(--accent)">alpha</span>claw
337
- </h2>
338
- <p class="text-sm text-gray-500">This could take 10–15 seconds</p>
339
- </div>
340
- </div>
341
- `;
342
- }
241
+ const goBack = () => {
242
+ if (isSetupStep) return;
243
+ setStep((prev) => Math.max(0, prev - 1));
244
+ };
343
245
 
344
- return html`
345
- <div class="max-w-lg w-full space-y-4">
346
- <div class="flex items-center gap-3">
347
- <div class="text-4xl">🐾</div>
348
- <div>
349
- <h1 class="text-2xl font-semibold">Welcome to <span style="color: var(--accent)">alpha</span>claw</h1>
350
- <p style="color: var(--text-muted)" class="text-sm">Let's get your agent running</p>
351
- </div>
352
- </div>
246
+ const goNext = () => {
247
+ if (!activeGroup || !currentGroupValid) return;
248
+ setStep((prev) => Math.min(kWelcomeGroups.length - 1, prev + 1));
249
+ };
353
250
 
354
- ${kGroups.map(
355
- (group) => html`
356
- <div class="bg-surface border border-border rounded-xl p-4 space-y-3">
357
- <div class="flex items-center justify-between">
358
- <div>
359
- <h2 class="text-sm font-medium text-gray-200">
360
- ${group.title}
361
- </h2>
362
- <p class="text-xs text-gray-500">${group.description}</p>
363
- </div>
364
- ${group.validate(vals, { hasAi })
365
- ? html`<span
366
- class="text-xs font-medium px-2 py-0.5 rounded-full bg-green-900/50 text-green-400"
367
- >✓</span
368
- >`
369
- : group.id !== "tools"
370
- ? html`<span
371
- class="text-xs font-medium px-2 py-0.5 rounded-full bg-yellow-900/50 text-yellow-400"
372
- >Required</span
373
- >`
374
- : null}
375
- </div>
251
+ const activeStepLabel = isSetupStep
252
+ ? "Initializing"
253
+ : activeGroup?.title || "Setup";
254
+ const stepNumber = isSetupStep ? kWelcomeGroups.length + 1 : step + 1;
376
255
 
377
- ${group.id === "ai" &&
378
- html`
379
- <div class="space-y-1">
380
- <label class="text-xs font-medium text-gray-400">Model</label>
381
- <select
382
- value=${vals.MODEL_KEY || ""}
383
- onInput=${(e) => set("MODEL_KEY", e.target.value)}
384
- class="w-full bg-black/30 border border-border rounded-lg pl-3 pr-8 py-2 text-sm text-gray-200 outline-none focus:border-gray-500"
385
- >
386
- <option value="">Select a model</option>
387
- ${modelOptions.map(
388
- (model) => html`
389
- <option value=${model.key}>
390
- ${model.label || model.key}
391
- </option>
392
- `,
393
- )}
394
- </select>
395
- <p class="text-xs text-gray-600">
396
- ${modelsLoading
397
- ? "Loading model catalog..."
398
- : modelsError
399
- ? modelsError
400
- : ""}
401
- </p>
402
- ${canToggleFullCatalog &&
403
- html`
404
- <button
405
- type="button"
406
- onclick=${() => setShowAllModels((prev) => !prev)}
407
- class="text-xs text-gray-500 hover:text-gray-300"
408
- >
409
- ${showAllModels
410
- ? "Show recommended models"
411
- : "Show full model catalog"}
412
- </button>
413
- `}
414
- </div>
415
- `}
416
- ${group.id === "ai" &&
417
- selectedProvider === "openai-codex" &&
418
- html`
419
- <div
420
- class="bg-black/20 border border-border rounded-lg p-3 space-y-2"
421
- >
422
- <div class="flex items-center justify-between">
423
- <span class="text-xs text-gray-400">Codex OAuth</span>
424
- ${codexLoading
425
- ? html`<span class="text-xs text-gray-500"
426
- >Checking...</span
427
- >`
428
- : codexStatus.connected
429
- ? html`<span class="text-xs text-green-400"
430
- >Connected</span
431
- >`
432
- : html`<span class="text-xs text-yellow-400"
433
- >Not connected</span
434
- >`}
435
- </div>
436
- <div class="flex gap-2">
437
- <button
438
- type="button"
439
- onclick=${startCodexAuth}
440
- class="text-xs font-medium px-3 py-1.5 rounded-lg ${codexStatus.connected
441
- ? "border border-border text-gray-300 hover:border-gray-500"
442
- : "bg-white text-black hover:opacity-85"}"
443
- >
444
- ${codexStatus.connected
445
- ? "Reconnect Codex"
446
- : "Connect Codex OAuth"}
447
- </button>
448
- ${codexStatus.connected &&
449
- html`
450
- <button
451
- type="button"
452
- onclick=${handleCodexDisconnect}
453
- class="text-xs font-medium px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
454
- >
455
- Disconnect
456
- </button>
457
- `}
458
- </div>
459
- ${!codexStatus.connected &&
460
- codexAuthStarted &&
461
- html`
462
- <div class="space-y-1 pt-1">
463
- <p class="text-xs text-gray-500">
464
- ${codexAuthWaiting
465
- ? "Complete login in the popup, then paste the full redirect URL from the address bar (starts with "
466
- : "Paste the full redirect URL from the address bar (starts with "}
467
- <code class="text-xs bg-black/30 px-1 rounded"
468
- >http://localhost:1455/auth/callback</code
469
- >)
470
- ${codexAuthWaiting
471
- ? " to finish setup."
472
- : " to finish setup."}
473
- </p>
474
- <input
475
- type="text"
476
- value=${codexManualInput}
477
- onInput=${(e) => setCodexManualInput(e.target.value)}
478
- placeholder="http://localhost:1455/auth/callback?code=...&state=..."
479
- 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"
480
- />
481
- <button
482
- type="button"
483
- onclick=${completeCodexAuth}
484
- disabled=${!codexManualInput.trim() || codexExchanging}
485
- class="text-xs font-medium px-3 py-1.5 rounded-lg ${!codexManualInput.trim() ||
486
- codexExchanging
487
- ? "bg-gray-700 text-gray-400 cursor-not-allowed"
488
- : "bg-white text-black hover:opacity-85"}"
489
- >
490
- ${codexExchanging
491
- ? "Completing..."
492
- : "Complete Codex OAuth"}
493
- </button>
494
- </div>
495
- `}
496
- </div>
497
- `}
498
- ${(group.id === "ai"
499
- ? group.fields.filter((field) =>
500
- visibleAiFieldKeys.has(field.key),
501
- )
502
- : group.fields
503
- ).map(
504
- (field) => html`
505
- <div class="space-y-1">
506
- <label class="text-xs font-medium text-gray-400"
507
- >${field.label}</label
508
- >
509
- <input
510
- type=${field.isText ? "text" : "password"}
511
- placeholder=${field.placeholder || ""}
512
- value=${vals[field.key] || ""}
513
- onInput=${(e) => set(field.key, e.target.value)}
514
- class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
515
- />
516
- <p class="text-xs text-gray-600">${field.hint}</p>
517
- </div>
518
- `,
519
- )}
520
- </div>
521
- `,
522
- )}
523
- ${error
524
- ? html`<div
525
- class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm"
526
- >
527
- ${error}
528
- </div>`
529
- : null}
256
+ return html`
257
+ <div class="max-w-lg w-full space-y-5">
258
+ <${WelcomeHeader}
259
+ groups=${kWelcomeGroups}
260
+ step=${step}
261
+ isSetupStep=${isSetupStep}
262
+ stepNumber=${stepNumber}
263
+ activeStepLabel=${activeStepLabel}
264
+ vals=${vals}
265
+ hasAi=${hasAi}
266
+ />
530
267
 
531
- <button
532
- onclick=${handleSubmit}
533
- disabled=${!allValid}
534
- class="w-full text-sm font-medium px-4 py-3 rounded-xl transition-all ${allValid
535
- ? "bg-white text-black hover:opacity-85"
536
- : "bg-gray-800 text-gray-500 cursor-not-allowed"}"
537
- >
538
- Complete Setup
539
- </button>
268
+ <div class="bg-surface border border-border rounded-xl p-4 space-y-3">
269
+ ${isSetupStep
270
+ ? html`<${WelcomeSetupStep}
271
+ error=${error}
272
+ loading=${loading}
273
+ onRetry=${handleSubmit}
274
+ />`
275
+ : html`
276
+ <${WelcomeFormStep}
277
+ activeGroup=${activeGroup}
278
+ vals=${vals}
279
+ hasAi=${hasAi}
280
+ setValue=${set}
281
+ modelOptions=${modelOptions}
282
+ modelsLoading=${modelsLoading}
283
+ modelsError=${modelsError}
284
+ canToggleFullCatalog=${canToggleFullCatalog}
285
+ showAllModels=${showAllModels}
286
+ setShowAllModels=${setShowAllModels}
287
+ selectedProvider=${selectedProvider}
288
+ codexLoading=${codexLoading}
289
+ codexStatus=${codexStatus}
290
+ startCodexAuth=${startCodexAuth}
291
+ handleCodexDisconnect=${handleCodexDisconnect}
292
+ codexAuthStarted=${codexAuthStarted}
293
+ codexAuthWaiting=${codexAuthWaiting}
294
+ codexManualInput=${codexManualInput}
295
+ setCodexManualInput=${setCodexManualInput}
296
+ completeCodexAuth=${completeCodexAuth}
297
+ codexExchanging=${codexExchanging}
298
+ visibleAiFieldKeys=${visibleAiFieldKeys}
299
+ error=${error}
300
+ step=${step}
301
+ totalGroups=${kWelcomeGroups.length}
302
+ currentGroupValid=${currentGroupValid}
303
+ goBack=${goBack}
304
+ goNext=${goNext}
305
+ loading=${loading}
306
+ allValid=${allValid}
307
+ handleSubmit=${handleSubmit}
308
+ />
309
+ `}
310
+ </div>
540
311
  </div>
541
312
  `;
542
313
  };
@@ -40,7 +40,7 @@
40
40
  <span style="color: var(--accent)">alpha</span>claw
41
41
  </h1>
42
42
  <p style="color: var(--text-muted)" class="text-xs mb-4">
43
- OpenClaw made easier
43
+ OpenClaw made easy
44
44
  </p>
45
45
  </div>
46
46
  <form
@@ -72,13 +72,21 @@
72
72
  const submitButtonEl = document.getElementById("submit-btn");
73
73
 
74
74
  const kEnabledClasses = ["bg-white", "text-black", "hover:opacity-85"];
75
- const kDisabledClasses = ["bg-gray-800", "text-gray-500", "cursor-not-allowed"];
75
+ const kDisabledClasses = [
76
+ "bg-gray-800",
77
+ "text-gray-500",
78
+ "cursor-not-allowed",
79
+ ];
76
80
 
77
81
  const syncButtonState = () => {
78
82
  const hasValue = passwordEl.value.length > 0;
79
83
  submitButtonEl.disabled = !hasValue;
80
- kEnabledClasses.forEach((c) => submitButtonEl.classList.toggle(c, hasValue));
81
- kDisabledClasses.forEach((c) => submitButtonEl.classList.toggle(c, !hasValue));
84
+ kEnabledClasses.forEach((c) =>
85
+ submitButtonEl.classList.toggle(c, hasValue),
86
+ );
87
+ kDisabledClasses.forEach((c) =>
88
+ submitButtonEl.classList.toggle(c, !hasValue),
89
+ );
82
90
  };
83
91
 
84
92
  passwordEl.addEventListener("input", syncButtonState);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },