@chrysb/alphaclaw 0.4.6-beta.7 → 0.4.6-beta.9

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 (33) hide show
  1. package/bin/alphaclaw.js +2 -32
  2. package/lib/public/css/theme.css +19 -0
  3. package/lib/public/js/app.js +1 -1
  4. package/lib/public/js/components/doctor/helpers.js +71 -5
  5. package/lib/public/js/components/doctor/index.js +89 -28
  6. package/lib/public/js/components/envars.js +0 -1
  7. package/lib/public/js/components/onboarding/welcome-config.js +39 -17
  8. package/lib/public/js/components/onboarding/welcome-form-step.js +142 -47
  9. package/lib/public/js/components/onboarding/welcome-import-step.js +306 -0
  10. package/lib/public/js/components/onboarding/welcome-placeholder-review-step.js +99 -0
  11. package/lib/public/js/components/onboarding/welcome-secret-review-step.js +191 -0
  12. package/lib/public/js/components/segmented-control.js +7 -1
  13. package/lib/public/js/components/welcome/index.js +112 -0
  14. package/lib/public/js/components/welcome/use-welcome.js +561 -0
  15. package/lib/public/js/lib/api.js +221 -161
  16. package/lib/server/commands.js +1 -0
  17. package/lib/server/constants.js +0 -1
  18. package/lib/server/doctor/bootstrap-context.js +191 -0
  19. package/lib/server/doctor/prompt.js +20 -4
  20. package/lib/server/doctor/service.js +18 -4
  21. package/lib/server/gateway.js +15 -40
  22. package/lib/server/onboarding/github.js +120 -19
  23. package/lib/server/onboarding/import/import-applier.js +321 -0
  24. package/lib/server/onboarding/import/import-config.js +69 -0
  25. package/lib/server/onboarding/import/import-scanner.js +469 -0
  26. package/lib/server/onboarding/import/import-temp.js +63 -0
  27. package/lib/server/onboarding/import/secret-detector.js +289 -0
  28. package/lib/server/onboarding/index.js +256 -29
  29. package/lib/server/onboarding/workspace.js +38 -6
  30. package/lib/server/routes/onboarding.js +281 -12
  31. package/lib/server.js +12 -3
  32. package/package.json +1 -1
  33. package/lib/public/js/components/welcome.js +0 -318
@@ -4,7 +4,14 @@ import htm from "https://esm.sh/htm";
4
4
  import { SecretInput } from "../secret-input.js";
5
5
  import { ActionButton } from "../action-button.js";
6
6
  import { Badge } from "../badge.js";
7
- import { isValidGithubRepoInput } from "./welcome-config.js";
7
+ import { SegmentedControl } from "../segmented-control.js";
8
+ import {
9
+ isValidGithubRepoInput,
10
+ kGithubFlowFresh,
11
+ kGithubFlowImport,
12
+ kGithubTargetRepoModeCreate,
13
+ kGithubTargetRepoModeExistingEmpty,
14
+ } from "./welcome-config.js";
8
15
 
9
16
  const html = htm.bind(h);
10
17
 
@@ -42,14 +49,16 @@ export const WelcomeFormStep = ({
42
49
  allValid,
43
50
  handleSubmit,
44
51
  }) => {
45
- const [repoTouched, setRepoTouched] = useState(false);
46
52
  const [showOptionalOpenai, setShowOptionalOpenai] = useState(false);
47
53
  const [showOptionalGemini, setShowOptionalGemini] = useState(false);
54
+ const githubFlow = vals._GITHUB_FLOW || kGithubFlowFresh;
55
+ const freshRepoMode =
56
+ githubFlow === kGithubFlowImport
57
+ ? kGithubTargetRepoModeCreate
58
+ : vals._GITHUB_TARGET_REPO_MODE || kGithubTargetRepoModeCreate;
48
59
 
49
60
  useEffect(() => {
50
- if (activeGroup.id !== "github") {
51
- setRepoTouched(false);
52
- }
61
+ if (activeGroup.id !== "github") return;
53
62
  }, [activeGroup.id]);
54
63
 
55
64
  useEffect(() => {
@@ -183,9 +192,62 @@ export const WelcomeFormStep = ({
183
192
  `}
184
193
  </div>
185
194
  `}
195
+ ${activeGroup.id === "github" &&
196
+ html`
197
+ <div class="space-y-3">
198
+ <div class="space-y-1 pt-1">
199
+ <div>
200
+ <label class="text-xs font-medium text-gray-400">Setup mode</label>
201
+ </div>
202
+ <${SegmentedControl}
203
+ options=${[
204
+ { label: "Start fresh", value: kGithubFlowFresh },
205
+ { label: "Import existing setup", value: kGithubFlowImport },
206
+ ]}
207
+ value=${githubFlow}
208
+ onChange=${(value) => setValue("_GITHUB_FLOW", value)}
209
+ size="md"
210
+ fullWidth=${true}
211
+ />
212
+ </div>
213
+ ${githubFlow === kGithubFlowFresh
214
+ ? html`
215
+ <div class="space-y-1">
216
+ <div>
217
+ <label class="text-xs font-medium text-gray-400"
218
+ >Repository setup</label
219
+ >
220
+ </div>
221
+ <${SegmentedControl}
222
+ options=${[
223
+ {
224
+ label: "Create new repo",
225
+ value: kGithubTargetRepoModeCreate,
226
+ },
227
+ {
228
+ label: "Use existing empty repo",
229
+ value: kGithubTargetRepoModeExistingEmpty,
230
+ },
231
+ ]}
232
+ value=${freshRepoMode}
233
+ onChange=${(value) =>
234
+ setValue("_GITHUB_TARGET_REPO_MODE", value)}
235
+ fullWidth=${true}
236
+ />
237
+ </div>
238
+ `
239
+ : null}
240
+ </div>
241
+ `}
186
242
  ${(activeGroup.id === "ai"
187
243
  ? activeGroup.fields.filter((field) => visibleAiFieldKeys.has(field.key))
188
- : activeGroup.fields
244
+ : activeGroup.id === "github"
245
+ ? activeGroup.fields.filter((field) =>
246
+ githubFlow === kGithubFlowImport
247
+ ? true
248
+ : field.key !== "_GITHUB_SOURCE_REPO",
249
+ )
250
+ : activeGroup.fields
189
251
  ).map(
190
252
  (field) => html`
191
253
  <div class="space-y-1" key=${field.key}>
@@ -196,27 +258,51 @@ export const WelcomeFormStep = ({
196
258
  key=${field.key}
197
259
  value=${vals[field.key] || ""}
198
260
  onInput=${(e) => setValue(field.key, e.target.value)}
199
- onBlur=${field.key === "GITHUB_WORKSPACE_REPO"
200
- ? () => setRepoTouched(true)
201
- : undefined}
202
261
  placeholder=${field.placeholder || ""}
203
262
  isSecret=${!field.isText}
204
263
  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"
205
264
  />
206
- <p class="text-xs text-gray-600">${field.hint}</p>
265
+ <p class="text-xs text-gray-600">
266
+ ${activeGroup.id === "github" &&
267
+ field.key === "GITHUB_WORKSPACE_REPO"
268
+ ? githubFlow === kGithubFlowImport
269
+ ? "Your new project will live here"
270
+ : freshRepoMode === kGithubTargetRepoModeExistingEmpty
271
+ ? "Enter the owner/repo of an existing empty repository"
272
+ : "A new private repo will be created for you"
273
+ : activeGroup.id === "github" &&
274
+ field.key === "_GITHUB_SOURCE_REPO"
275
+ ? "The repo to import from"
276
+ : activeGroup.id === "github" && field.key === "GITHUB_TOKEN"
277
+ ? githubFlow === kGithubFlowImport
278
+ ? freshRepoMode === kGithubTargetRepoModeCreate
279
+ ? html`Use a classic PAT with${" "}<code
280
+ class="text-xs bg-black/30 px-1 rounded"
281
+ >repo</code
282
+ >${" "}scope to create the target repo. Fine-grained
283
+ works if the target already exists and can access both
284
+ repos.`
285
+ : html`Use a classic PAT with${" "}<code
286
+ class="text-xs bg-black/30 px-1 rounded"
287
+ >repo</code
288
+ >${" "}scope, or a fine-grained token with Contents +
289
+ Metadata access to both the source repo and target
290
+ repo`
291
+ : freshRepoMode === kGithubTargetRepoModeExistingEmpty
292
+ ? html`Use a classic PAT with${" "}<code
293
+ class="text-xs bg-black/30 px-1 rounded"
294
+ >repo</code
295
+ >${" "}scope, or a fine-grained token with Contents +
296
+ Metadata access to this repo`
297
+ : html`Use a classic PAT with${" "}<code
298
+ class="text-xs bg-black/30 px-1 rounded"
299
+ >repo</code
300
+ >${" "}scope to create a new private repository`
301
+ : field.hint}
302
+ </p>
207
303
  </div>
208
304
  `,
209
305
  )}
210
- ${activeGroup.id === "github" &&
211
- repoTouched &&
212
- vals.GITHUB_WORKSPACE_REPO &&
213
- !isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO)
214
- ? html`<div class="text-xs text-red-300">
215
- Workspace Repo must be in
216
- <code class="text-xs bg-black/30 px-1 rounded">owner/repo</code>
217
- format.
218
- </div>`
219
- : null}
220
306
  ${error
221
307
  ? html`<div
222
308
  class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm"
@@ -281,39 +367,48 @@ export const WelcomeFormStep = ({
281
367
  ${step < totalGroups - 1
282
368
  ? html`
283
369
  ${step > 0
284
- ? html`<button
285
- onclick=${goBack}
286
- class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-secondary"
287
- >
288
- Back
289
- </button>`
370
+ ? html`<${ActionButton}
371
+ onClick=${goBack}
372
+ tone="secondary"
373
+ size="md"
374
+ idleLabel="Back"
375
+ className="w-full"
376
+ />`
290
377
  : html`<div class="w-full"></div>`}
291
- <button
292
- onclick=${goNext}
293
- disabled=${!currentGroupValid || githubStepLoading}
294
- class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan"
295
- >
296
- ${activeGroup.id === "github" && githubStepLoading
297
- ? "Checking..."
378
+ <${ActionButton}
379
+ onClick=${goNext}
380
+ disabled=${!currentGroupValid}
381
+ loading=${activeGroup.id === "github" && githubStepLoading}
382
+ tone="primary"
383
+ size="md"
384
+ idleLabel=${activeGroup.id === "github" &&
385
+ githubFlow === kGithubFlowImport
386
+ ? "Check compatibility"
298
387
  : "Next"}
299
- </button>
388
+ loadingLabel="Checking..."
389
+ className="w-full"
390
+ />
300
391
  `
301
392
  : html`
302
393
  ${step > 0
303
- ? html`<button
304
- onclick=${goBack}
305
- class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-secondary"
306
- >
307
- Back
308
- </button>`
394
+ ? html`<${ActionButton}
395
+ onClick=${goBack}
396
+ tone="secondary"
397
+ size="md"
398
+ idleLabel="Back"
399
+ className="w-full"
400
+ />`
309
401
  : html`<div class="w-full"></div>`}
310
- <button
311
- onclick=${handleSubmit}
312
- disabled=${!allValid || loading}
313
- class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan"
314
- >
315
- ${loading ? "Starting..." : "Next"}
316
- </button>
402
+ <${ActionButton}
403
+ onClick=${handleSubmit}
404
+ disabled=${!allValid}
405
+ loading=${loading}
406
+ tone="primary"
407
+ size="md"
408
+ idleLabel="Next"
409
+ loadingLabel="Starting..."
410
+ className="w-full"
411
+ />
317
412
  `}
318
413
  </div>
319
414
  `;
@@ -0,0 +1,306 @@
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
+ import { ActionButton } from "../action-button.js";
5
+ import { LoadingSpinner } from "../loading-spinner.js";
6
+
7
+ const html = htm.bind(h);
8
+
9
+ const kCategories = [
10
+ {
11
+ key: "gatewayConfig",
12
+ label: "Gateway Config",
13
+ icon: "⚙️",
14
+ description: "openclaw.json configuration",
15
+ showFiles: true,
16
+ },
17
+ {
18
+ key: "envFiles",
19
+ label: "Environment Files",
20
+ icon: "🔐",
21
+ description: ".env files with variables",
22
+ showFiles: true,
23
+ },
24
+ {
25
+ key: "workspaceFiles",
26
+ label: "Workspace Files",
27
+ icon: "📄",
28
+ description: "Prompt files (AGENTS.md, SOUL.md, etc.)",
29
+ showFiles: true,
30
+ },
31
+ {
32
+ key: "skills",
33
+ label: "Skills",
34
+ icon: "🛠",
35
+ description: "Custom skill definitions",
36
+ showFiles: true,
37
+ },
38
+ {
39
+ key: "cronJobs",
40
+ label: "Cron Jobs",
41
+ icon: "⏰",
42
+ description: "Scheduled tasks",
43
+ showFiles: true,
44
+ },
45
+ {
46
+ key: "webhooks",
47
+ label: "Hooks",
48
+ icon: "🔗",
49
+ description: "Webhook mappings and internal hooks",
50
+ showDirs: true,
51
+ },
52
+ {
53
+ key: "memory",
54
+ label: "Memory",
55
+ icon: "🧠",
56
+ description: "Agent memory and embeddings",
57
+ showDirs: true,
58
+ },
59
+ ];
60
+
61
+ const CategoryCard = ({ category, data }) => {
62
+ const [expanded, setExpanded] = useState(false);
63
+ if (!data?.found) return null;
64
+ const isHooksCategory = category.key === "webhooks";
65
+ const warningItems = Array.isArray(data.transformWarnings)
66
+ ? data.transformWarnings
67
+ : [];
68
+ const warningPathPrefixes = new Set(
69
+ warningItems
70
+ .map((warning) => String(warning.actualPath || "").trim())
71
+ .filter(Boolean)
72
+ .map((pathValue) => pathValue.split("/").slice(0, -2).join("/")),
73
+ );
74
+
75
+ const items = [
76
+ ...(data.jobNames || []),
77
+ ...(data.hookNames || []),
78
+ ...(data.files || []),
79
+ ...(data.dirs || []).filter((dir) => !warningPathPrefixes.has(dir)),
80
+ ...(data.extraMarkdown || []),
81
+ ];
82
+ const count =
83
+ typeof data.jobCount === "number" && data.jobCount > 0
84
+ ? data.jobCount
85
+ : typeof data.hookCount === "number" && data.hookCount > 0
86
+ ? data.hookCount
87
+ : items.length;
88
+ const warningCount =
89
+ typeof data.warningCount === "number"
90
+ ? data.warningCount
91
+ : warningItems.length;
92
+
93
+ return html`
94
+ <div class="border border-border rounded-lg p-3">
95
+ <button
96
+ type="button"
97
+ onclick=${() => setExpanded((p) => !p)}
98
+ class="w-full flex items-center justify-between text-left"
99
+ >
100
+ <div class="flex items-center gap-2">
101
+ <span class="text-sm">${category.icon}</span>
102
+ <span class="text-xs font-medium text-gray-200"
103
+ >${category.label}</span
104
+ >
105
+ <span
106
+ class="text-xs px-1.5 py-0.5 rounded-full bg-cyan-900/40 text-cyan-300"
107
+ >${count}</span
108
+ >
109
+ </div>
110
+ <div class="flex items-center gap-2">
111
+ ${warningCount > 0
112
+ ? html`
113
+ <span
114
+ class="text-xs px-1.5 py-0.5 rounded-full bg-yellow-900/30 text-yellow-300"
115
+ >
116
+ ⚠ ${warningCount}
117
+ </span>
118
+ `
119
+ : null}
120
+ <span class="text-xs text-gray-500">${expanded ? "▲" : "▼"}</span>
121
+ </div>
122
+ </button>
123
+ ${expanded &&
124
+ items.length > 0 &&
125
+ html`
126
+ <div class="mt-2 space-y-1">
127
+ ${items.map(
128
+ (item) => html`
129
+ <div
130
+ class="text-xs font-mono bg-black/20 rounded px-2 py-1 text-gray-500"
131
+ >
132
+ ${item}
133
+ </div>
134
+ `,
135
+ )}
136
+ ${isHooksCategory
137
+ ? warningItems.map(
138
+ (warning) => html`
139
+ <div
140
+ class="text-xs font-mono bg-black/20 rounded px-2 py-1 text-yellow-300"
141
+ >
142
+ ${warning.actualPath}
143
+ </div>
144
+ `,
145
+ )
146
+ : null}
147
+ </div>
148
+ `}
149
+ </div>
150
+ `;
151
+ };
152
+
153
+ export const WelcomeImportStep = ({
154
+ scanResult,
155
+ scanning,
156
+ error,
157
+ onApprove,
158
+ onShowSecretReview,
159
+ onBack,
160
+ }) => {
161
+ if (scanning) {
162
+ return html`
163
+ <div class="flex flex-col items-center justify-center py-8 gap-3">
164
+ <${LoadingSpinner} />
165
+ <p class="text-sm text-gray-400">Scanning repository...</p>
166
+ </div>
167
+ `;
168
+ }
169
+
170
+ if (error) {
171
+ return html`
172
+ <div class="space-y-3">
173
+ <div
174
+ class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm"
175
+ >
176
+ ${error}
177
+ </div>
178
+ <button
179
+ onclick=${onBack}
180
+ class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-secondary"
181
+ >
182
+ Back
183
+ </button>
184
+ </div>
185
+ `;
186
+ }
187
+
188
+ if (!scanResult) return null;
189
+
190
+ const secretCount = (scanResult.secrets || []).length;
191
+ const hasConflicts = scanResult.managedConflicts?.found;
192
+
193
+ return html`
194
+ <div class="space-y-3">
195
+ <div>
196
+ <h2 class="text-sm font-medium text-gray-200">Import Summary</h2>
197
+ <p class="text-xs text-gray-500">
198
+ ${scanResult.hasOpenclawSetup
199
+ ? "Found an existing OpenClaw setup"
200
+ : "No OpenClaw config detected — we'll set up fresh after import"}
201
+ </p>
202
+ </div>
203
+
204
+ <div class="space-y-2">
205
+ ${kCategories.map(
206
+ (cat) => html`
207
+ <${CategoryCard}
208
+ key=${cat.key}
209
+ category=${cat}
210
+ data=${scanResult[cat.key]}
211
+ />
212
+ `,
213
+ )}
214
+ </div>
215
+
216
+ ${scanResult.credentials?.found &&
217
+ html`
218
+ <div
219
+ class="bg-yellow-900/20 border border-yellow-800/50 rounded-lg p-3 text-xs text-yellow-300"
220
+ >
221
+ Deployment-specific files found (credentials, device identity) — these
222
+ will not be imported.
223
+ </div>
224
+ `}
225
+ ${hasConflicts &&
226
+ html`
227
+ <div
228
+ class="bg-yellow-900/20 border border-yellow-800/50 rounded-lg p-3 text-xs text-yellow-300"
229
+ >
230
+ AlphaClaw-managed files detected
231
+ (${(scanResult.managedConflicts.files || []).join(", ")}). These will
232
+ be overwritten with AlphaClaw defaults.
233
+ </div>
234
+ `}
235
+ ${scanResult.managedEnvConflicts?.found
236
+ ? html`
237
+ <div
238
+ class="bg-yellow-900/20 border border-yellow-800/50 rounded-lg p-3 text-xs text-yellow-300"
239
+ >
240
+ AlphaClaw controls deployment env vars
241
+ (${(scanResult.managedEnvConflicts.vars || []).join(", ")}).
242
+ Imported values for these will be normalized during import.
243
+ </div>
244
+ `
245
+ : null}
246
+ ${scanResult.webhooks?.warningCount > 0
247
+ ? html`
248
+ <div
249
+ class="bg-yellow-900/20 border border-yellow-800/50 rounded-lg p-3 text-xs text-yellow-300"
250
+ >
251
+ AlphaClaw expects hook transforms at
252
+ <code class="text-xs bg-black/30 px-1 rounded"
253
+ >hooks/transforms/name/name-transform.mjs</code
254
+ >. We found some that do not match and will try to patch them
255
+ during import. The originals will be backed up under
256
+ <code class="text-xs bg-black/30 px-1 rounded"
257
+ >hooks/transforms/_backup</code
258
+ >.
259
+ </div>
260
+ `
261
+ : null}
262
+ ${secretCount > 0 &&
263
+ html`
264
+ <div
265
+ class="bg-cyan-900/20 border border-cyan-800/50 rounded-lg p-3 flex items-center justify-between"
266
+ >
267
+ <div>
268
+ <span class="text-xs text-cyan-300 font-medium">
269
+ ${secretCount} possible secret${secretCount === 1 ? "" : "s"}
270
+ detected
271
+ </span>
272
+ <p class="text-xs text-gray-500 mt-0.5">
273
+ Review and extract to environment variables
274
+ </p>
275
+ </div>
276
+ <${ActionButton}
277
+ onClick=${onShowSecretReview}
278
+ tone="primary"
279
+ size="sm"
280
+ idleLabel="Review"
281
+ className="font-medium"
282
+ />
283
+ </div>
284
+ `}
285
+
286
+ <div class="grid grid-cols-2 gap-2 pt-1">
287
+ <${ActionButton}
288
+ onClick=${onBack}
289
+ tone="secondary"
290
+ size="md"
291
+ idleLabel="Back"
292
+ className="w-full"
293
+ />
294
+ <${ActionButton}
295
+ onClick=${() => onApprove([])}
296
+ loading=${scanning}
297
+ tone="primary"
298
+ size="md"
299
+ idleLabel="Import"
300
+ loadingLabel="Importing..."
301
+ className="w-full"
302
+ />
303
+ </div>
304
+ </div>
305
+ `;
306
+ };
@@ -0,0 +1,99 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useMemo } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import { ActionButton } from "../action-button.js";
5
+ import { SecretInput } from "../secret-input.js";
6
+
7
+ const html = htm.bind(h);
8
+
9
+ const isResolvedValue = (value) => {
10
+ const normalized = String(value || "").trim();
11
+ return !!normalized && normalized !== "placeholder";
12
+ };
13
+
14
+ const PlaceholderRow = ({ item, value, onInput }) => {
15
+ return html`
16
+ <div class="border border-border rounded-lg p-3 space-y-2">
17
+ <div class="flex items-start justify-between gap-3">
18
+ <div class="min-w-0">
19
+ <div class="flex items-center gap-2 flex-wrap">
20
+ <code
21
+ class="text-xs text-gray-200 bg-black/30 px-1.5 py-0.5 rounded"
22
+ >${item.key}</code
23
+ >
24
+ </div>
25
+ </div>
26
+ </div>
27
+ <${SecretInput}
28
+ value=${value}
29
+ onInput=${(event) => onInput(event.target.value)}
30
+ placeholder="Enter value"
31
+ inputClass="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 font-mono"
32
+ />
33
+ </div>
34
+ `;
35
+ };
36
+
37
+ export const WelcomePlaceholderReviewStep = ({
38
+ placeholderReview,
39
+ vals,
40
+ setValue,
41
+ onContinue,
42
+ }) => {
43
+ const items = Array.isArray(placeholderReview?.vars)
44
+ ? placeholderReview.vars
45
+ : [];
46
+ const unresolvedItems = useMemo(
47
+ () =>
48
+ items
49
+ .filter((item) => !isResolvedValue(vals[item.key]))
50
+ .map((item) => item.key),
51
+ [items, vals],
52
+ );
53
+ const unresolvedCount = unresolvedItems.length;
54
+
55
+ if (items.length === 0) return null;
56
+
57
+ return html`
58
+ <div class="space-y-3">
59
+ <div>
60
+ <h2 class="text-sm font-medium text-gray-200">Add Missing Env Vars</h2>
61
+ </div>
62
+
63
+ <div class="space-y-2 max-h-80 overflow-y-auto">
64
+ ${items.map(
65
+ (item) => html`
66
+ <${PlaceholderRow}
67
+ key=${item.key}
68
+ item=${item}
69
+ value=${String(vals[item.key] || "") === "placeholder"
70
+ ? ""
71
+ : vals[item.key] || ""}
72
+ onInput=${(nextValue) => setValue(item.key, nextValue)}
73
+ />
74
+ `,
75
+ )}
76
+ </div>
77
+
78
+ <div
79
+ class="bg-yellow-900/20 border border-yellow-800/50 rounded-lg p-3 text-xs text-yellow-300"
80
+ >
81
+ ${unresolvedCount > 0
82
+ ? `${unresolvedCount} detected env var${unresolvedCount === 1 ? "" : "s"} need values. You can continue without them, but the gateway might fail to start.`
83
+ : "All imported placeholder env vars have values now."}
84
+ </div>
85
+
86
+ <div class="pt-1">
87
+ <${ActionButton}
88
+ onClick=${onContinue}
89
+ tone="primary"
90
+ size="md"
91
+ idleLabel=${unresolvedCount > 0
92
+ ? `Continue with ${unresolvedCount} Unresolved`
93
+ : "Continue"}
94
+ className="w-full"
95
+ />
96
+ </div>
97
+ </div>
98
+ `;
99
+ };