@chrysb/alphaclaw 0.1.14 → 0.1.16

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%; }
@@ -257,17 +257,17 @@ function App() {
257
257
  useEffect(() => {
258
258
  if (!onboarded) return;
259
259
  let active = true;
260
- const check = async () => {
260
+ const check = async (refresh = false) => {
261
261
  try {
262
- const data = await fetchAlphaclawVersion(false);
262
+ const data = await fetchAlphaclawVersion(refresh);
263
263
  if (!active) return;
264
264
  setAcVersion(data.currentVersion || null);
265
265
  setAcLatest(data.latestVersion || null);
266
266
  setAcHasUpdate(!!data.hasUpdate);
267
267
  } catch {}
268
268
  };
269
- check();
270
- const id = setInterval(check, 5 * 60 * 1000);
269
+ check(true);
270
+ const id = setInterval(() => check(false), 5 * 60 * 1000);
271
271
  return () => { active = false; clearInterval(id); };
272
272
  }, [onboarded]);
273
273
 
@@ -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
+ `;