@chrysb/alphaclaw 0.4.6-beta.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) 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/envars.js +0 -1
  5. package/lib/public/js/components/onboarding/welcome-config.js +39 -17
  6. package/lib/public/js/components/onboarding/welcome-form-step.js +142 -47
  7. package/lib/public/js/components/onboarding/welcome-import-step.js +306 -0
  8. package/lib/public/js/components/onboarding/welcome-placeholder-review-step.js +99 -0
  9. package/lib/public/js/components/onboarding/welcome-secret-review-step.js +191 -0
  10. package/lib/public/js/components/segmented-control.js +7 -1
  11. package/lib/public/js/components/welcome/index.js +112 -0
  12. package/lib/public/js/components/welcome/use-welcome.js +561 -0
  13. package/lib/public/js/lib/api.js +221 -161
  14. package/lib/server/commands.js +1 -0
  15. package/lib/server/constants.js +0 -1
  16. package/lib/server/gateway.js +15 -40
  17. package/lib/server/onboarding/github.js +120 -19
  18. package/lib/server/onboarding/import/import-applier.js +321 -0
  19. package/lib/server/onboarding/import/import-config.js +69 -0
  20. package/lib/server/onboarding/import/import-scanner.js +469 -0
  21. package/lib/server/onboarding/import/import-temp.js +63 -0
  22. package/lib/server/onboarding/import/secret-detector.js +289 -0
  23. package/lib/server/onboarding/index.js +256 -29
  24. package/lib/server/onboarding/workspace.js +38 -6
  25. package/lib/server/routes/onboarding.js +281 -12
  26. package/lib/server.js +11 -2
  27. package/package.json +1 -1
  28. package/lib/public/js/components/welcome.js +0 -318
@@ -0,0 +1,112 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { kWelcomeGroups } from "../onboarding/welcome-config.js";
4
+ import { WelcomeImportStep } from "../onboarding/welcome-import-step.js";
5
+ import { WelcomePlaceholderReviewStep } from "../onboarding/welcome-placeholder-review-step.js";
6
+ import { WelcomeSecretReviewStep } from "../onboarding/welcome-secret-review-step.js";
7
+ import { WelcomeHeader } from "../onboarding/welcome-header.js";
8
+ import { WelcomeSetupStep } from "../onboarding/welcome-setup-step.js";
9
+ import { WelcomeFormStep } from "../onboarding/welcome-form-step.js";
10
+ import { WelcomePairingStep } from "../onboarding/welcome-pairing-step.js";
11
+ import { useWelcome } from "./use-welcome.js";
12
+
13
+ const html = htm.bind(h);
14
+
15
+ export const Welcome = ({ onComplete }) => {
16
+ const { state, actions } = useWelcome({ onComplete });
17
+
18
+ return html`
19
+ <div class="max-w-lg w-full space-y-5">
20
+ <${WelcomeHeader}
21
+ groups=${kWelcomeGroups}
22
+ step=${state.step}
23
+ isSetupStep=${state.isSetupStep}
24
+ isPairingStep=${state.isPairingStep}
25
+ stepNumber=${state.stepNumber}
26
+ activeStepLabel=${state.activeStepLabel}
27
+ />
28
+
29
+ <div class="bg-surface border border-border rounded-xl p-4 space-y-3">
30
+ ${state.isImportStep
31
+ ? html`<${WelcomeImportStep}
32
+ scanResult=${state.importScanResult}
33
+ scanning=${state.importScanning}
34
+ error=${state.importError}
35
+ onApprove=${actions.handleImportApprove}
36
+ onShowSecretReview=${actions.handleShowSecretReview}
37
+ onBack=${actions.handleImportBack}
38
+ />`
39
+ : state.isSecretReviewStep
40
+ ? html`<${WelcomeSecretReviewStep}
41
+ secrets=${state.importScanResult?.secrets || []}
42
+ onApprove=${actions.handleImportApprove}
43
+ onBack=${actions.handleSecretReviewBack}
44
+ loading=${state.importScanning}
45
+ error=${state.importError}
46
+ />`
47
+ : state.isPlaceholderReviewStep
48
+ ? html`<${WelcomePlaceholderReviewStep}
49
+ placeholderReview=${state.placeholderReview}
50
+ vals=${state.vals}
51
+ setValue=${actions.setValue}
52
+ onContinue=${actions.handlePlaceholderReviewContinue}
53
+ />`
54
+ : state.isSetupStep
55
+ ? html`<${WelcomeSetupStep}
56
+ error=${state.setupError}
57
+ loading=${state.loading}
58
+ onRetry=${actions.handleSubmit}
59
+ onBack=${actions.goBackFromSetupError}
60
+ />`
61
+ : state.isPairingStep
62
+ ? html`<${WelcomePairingStep}
63
+ channel=${state.selectedPairingChannel}
64
+ pairings=${state.pairingRequestsPoll.data || []}
65
+ channels=${state.pairingChannels}
66
+ loading=${!state.pairingStatusPoll.data}
67
+ error=${state.pairingError}
68
+ onApprove=${actions.handlePairingApprove}
69
+ onReject=${actions.handlePairingReject}
70
+ canFinish=${state.pairingComplete || state.canFinishPairing}
71
+ onContinue=${actions.finishOnboarding}
72
+ />`
73
+ : html`
74
+ <${WelcomeFormStep}
75
+ activeGroup=${state.activeGroup}
76
+ vals=${state.vals}
77
+ hasAi=${state.hasAi}
78
+ setValue=${actions.setValue}
79
+ modelOptions=${state.modelOptions}
80
+ modelsLoading=${state.modelsLoading}
81
+ modelsError=${state.modelsError}
82
+ canToggleFullCatalog=${state.canToggleFullCatalog}
83
+ showAllModels=${state.showAllModels}
84
+ setShowAllModels=${actions.setShowAllModels}
85
+ selectedProvider=${state.selectedProvider}
86
+ codexLoading=${state.codexLoading}
87
+ codexStatus=${state.codexStatus}
88
+ startCodexAuth=${actions.startCodexAuth}
89
+ handleCodexDisconnect=${actions.handleCodexDisconnect}
90
+ codexAuthStarted=${state.codexAuthStarted}
91
+ codexAuthWaiting=${state.codexAuthWaiting}
92
+ codexManualInput=${state.codexManualInput}
93
+ setCodexManualInput=${actions.setCodexManualInput}
94
+ completeCodexAuth=${actions.completeCodexAuth}
95
+ codexExchanging=${state.codexExchanging}
96
+ visibleAiFieldKeys=${state.visibleAiFieldKeys}
97
+ error=${state.formError}
98
+ step=${state.step}
99
+ totalGroups=${kWelcomeGroups.length}
100
+ currentGroupValid=${state.currentGroupValid}
101
+ goBack=${actions.goBack}
102
+ goNext=${actions.goNext}
103
+ loading=${state.loading}
104
+ githubStepLoading=${state.githubStepLoading}
105
+ allValid=${state.allValid}
106
+ handleSubmit=${actions.handleSubmit}
107
+ />
108
+ `}
109
+ </div>
110
+ </div>
111
+ `;
112
+ };
@@ -0,0 +1,561 @@
1
+ import { useEffect, useState } from "https://esm.sh/preact/hooks";
2
+ import {
3
+ runOnboard,
4
+ verifyGithubOnboardingRepo,
5
+ scanImportRepo,
6
+ applyImport,
7
+ fetchModels,
8
+ } from "../../lib/api.js";
9
+ import {
10
+ getModelProvider,
11
+ getFeaturedModels,
12
+ getVisibleAiFieldKeys,
13
+ } from "../../lib/model-config.js";
14
+ import {
15
+ kWelcomeGroups,
16
+ isValidGithubRepoInput,
17
+ kGithubFlowFresh,
18
+ kGithubFlowImport,
19
+ kGithubTargetRepoModeCreate,
20
+ kGithubTargetRepoModeExistingEmpty,
21
+ kRepoModeNew,
22
+ kRepoModeExisting,
23
+ } from "../onboarding/welcome-config.js";
24
+ import { getPreferredPairingChannel } from "../onboarding/pairing-utils.js";
25
+ import {
26
+ kOnboardingStorageKey,
27
+ kPairingChannelKey,
28
+ useWelcomeStorage,
29
+ } from "../onboarding/use-welcome-storage.js";
30
+ import { useWelcomeCodex } from "../onboarding/use-welcome-codex.js";
31
+ import { useWelcomePairing } from "../onboarding/use-welcome-pairing.js";
32
+
33
+ const kMaxOnboardingVars = 64;
34
+ const kMaxEnvKeyLength = 128;
35
+ const kMaxEnvValueLength = 4096;
36
+ export const kImportStepId = "import";
37
+ export const kSecretReviewStepId = "secret-review";
38
+ export const kPlaceholderReviewStepId = "placeholder-review";
39
+ const kImportSubstepKey = "_IMPORT_SUBSTEP";
40
+ const kImportPlaceholderReviewKey = "_IMPORT_PLACEHOLDER_REVIEW";
41
+ const kImportPlaceholderSkipConfirmedKey = "_IMPORT_PLACEHOLDER_SKIP_CONFIRMED";
42
+
43
+ const normalizePlaceholderReview = (review) => {
44
+ if (!review || !Array.isArray(review.vars) || review.vars.length === 0) {
45
+ return { found: false, count: 0, vars: [] };
46
+ }
47
+ return {
48
+ found: true,
49
+ count:
50
+ typeof review.count === "number" ? review.count : review.vars.length,
51
+ vars: review.vars
52
+ .map((item) => ({
53
+ key: String(item?.key || "").trim(),
54
+ status: String(item?.status || "missing").trim() || "missing",
55
+ }))
56
+ .filter((item) => item.key),
57
+ };
58
+ };
59
+
60
+ export const useWelcome = ({ onComplete }) => {
61
+ const kSetupStepIndex = kWelcomeGroups.length;
62
+ const kPairingStepIndex = kSetupStepIndex + 1;
63
+ const { vals, setVals, setValue, step, setStep, setupError, setSetupError } =
64
+ useWelcomeStorage({
65
+ kSetupStepIndex,
66
+ kPairingStepIndex,
67
+ });
68
+ const [models, setModels] = useState([]);
69
+ const [modelsLoading, setModelsLoading] = useState(true);
70
+ const [modelsError, setModelsError] = useState(null);
71
+ const [showAllModels, setShowAllModels] = useState(false);
72
+ const [loading, setLoading] = useState(false);
73
+ const [githubStepLoading, setGithubStepLoading] = useState(false);
74
+ const [formError, setFormError] = useState(null);
75
+ const {
76
+ codexStatus,
77
+ codexLoading,
78
+ codexManualInput,
79
+ setCodexManualInput,
80
+ codexExchanging,
81
+ codexAuthStarted,
82
+ codexAuthWaiting,
83
+ startCodexAuth,
84
+ completeCodexAuth,
85
+ handleCodexDisconnect,
86
+ } = useWelcomeCodex({ setFormError });
87
+ const [importStep, setImportStepState] = useState(() => {
88
+ const storedStep = String(vals[kImportSubstepKey] || "").trim();
89
+ return storedStep === kPlaceholderReviewStepId
90
+ ? storedStep
91
+ : null;
92
+ });
93
+ const [importTempDir, setImportTempDir] = useState(null);
94
+ const [importScanResult, setImportScanResult] = useState(null);
95
+ const [importScanning, setImportScanning] = useState(false);
96
+ const [importError, setImportError] = useState(null);
97
+
98
+ const setImportStep = (nextStep) => {
99
+ setImportStepState(nextStep);
100
+ setVals((prev) => ({
101
+ ...prev,
102
+ [kImportSubstepKey]:
103
+ nextStep === kPlaceholderReviewStepId ? nextStep : "",
104
+ }));
105
+ };
106
+
107
+ const clearPlaceholderReview = () => {
108
+ setVals((prev) => ({
109
+ ...prev,
110
+ [kImportPlaceholderReviewKey]: null,
111
+ [kImportPlaceholderSkipConfirmedKey]: false,
112
+ }));
113
+ };
114
+
115
+ useEffect(() => {
116
+ fetchModels()
117
+ .then((result) => {
118
+ const list = Array.isArray(result.models) ? result.models : [];
119
+ const featured = getFeaturedModels(list);
120
+ setModels(list);
121
+ if (!vals.MODEL_KEY && list.length > 0) {
122
+ const defaultModel = featured[0] || list[0];
123
+ setVals((prev) => ({ ...prev, MODEL_KEY: defaultModel.key }));
124
+ }
125
+ })
126
+ .catch(() => setModelsError("Failed to load models"))
127
+ .finally(() => setModelsLoading(false));
128
+ }, []);
129
+
130
+ const selectedProvider = getModelProvider(vals.MODEL_KEY);
131
+ const placeholderReview = normalizePlaceholderReview(
132
+ vals[kImportPlaceholderReviewKey],
133
+ );
134
+ const featuredModels = getFeaturedModels(models);
135
+ const baseModelOptions = showAllModels
136
+ ? models
137
+ : featuredModels.length > 0
138
+ ? featuredModels
139
+ : models;
140
+ const selectedModelOption = models.find(
141
+ (model) => model.key === vals.MODEL_KEY,
142
+ );
143
+ const modelOptions =
144
+ selectedModelOption &&
145
+ !baseModelOptions.some((model) => model.key === selectedModelOption.key)
146
+ ? [...baseModelOptions, selectedModelOption]
147
+ : baseModelOptions;
148
+ const canToggleFullCatalog =
149
+ featuredModels.length > 0 && models.length > featuredModels.length;
150
+ const visibleAiFieldKeys = getVisibleAiFieldKeys(selectedProvider);
151
+ const hasAi =
152
+ selectedProvider === "anthropic"
153
+ ? !!(vals.ANTHROPIC_API_KEY || vals.ANTHROPIC_TOKEN)
154
+ : selectedProvider === "openai"
155
+ ? !!vals.OPENAI_API_KEY
156
+ : selectedProvider === "google"
157
+ ? !!vals.GEMINI_API_KEY
158
+ : selectedProvider === "openai-codex"
159
+ ? !!codexStatus.connected
160
+ : false;
161
+
162
+ const allValid = kWelcomeGroups.every((group) => group.validate(vals, { hasAi }));
163
+ const isSetupStep = step === kSetupStepIndex;
164
+ const isPairingStep = step === kPairingStepIndex;
165
+ const activeGroup = step < kSetupStepIndex ? kWelcomeGroups[step] : null;
166
+ const currentGroupValid = activeGroup
167
+ ? activeGroup.validate(vals, { hasAi })
168
+ : false;
169
+ const selectedPairingChannel = String(
170
+ vals[kPairingChannelKey] || getPreferredPairingChannel(vals),
171
+ );
172
+ const {
173
+ pairingStatusPoll,
174
+ pairingRequestsPoll,
175
+ pairingChannels,
176
+ canFinishPairing,
177
+ pairingError,
178
+ pairingComplete,
179
+ handlePairingApprove,
180
+ handlePairingReject,
181
+ resetPairingState,
182
+ } = useWelcomePairing({
183
+ isPairingStep,
184
+ selectedPairingChannel,
185
+ });
186
+
187
+ const handleSubmit = async () => {
188
+ if (!allValid || loading) return;
189
+ const vars = Object.entries(vals)
190
+ .filter(
191
+ ([key]) => key !== "MODEL_KEY" && !String(key || "").startsWith("_"),
192
+ )
193
+ .filter(([, value]) => value)
194
+ .map(([key, value]) => ({ key, value }));
195
+ const preflightError = (() => {
196
+ if (!vals.MODEL_KEY || !String(vals.MODEL_KEY).includes("/")) {
197
+ return "A model selection is required";
198
+ }
199
+ if (vars.length > kMaxOnboardingVars) {
200
+ return `Too many environment variables (max ${kMaxOnboardingVars})`;
201
+ }
202
+ for (const entry of vars) {
203
+ const key = String(entry?.key || "");
204
+ const value = String(entry?.value || "");
205
+ if (!key) return "Each variable must include a key";
206
+ if (key.length > kMaxEnvKeyLength) {
207
+ return `Variable key is too long: ${key.slice(0, 32)}...`;
208
+ }
209
+ if (value.length > kMaxEnvValueLength) {
210
+ return `Value too long for ${key} (max ${kMaxEnvValueLength} chars)`;
211
+ }
212
+ }
213
+ if (
214
+ !vals.GITHUB_TOKEN ||
215
+ !isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO)
216
+ ) {
217
+ return 'Target repo must be in "owner/repo" format.';
218
+ }
219
+ if (
220
+ (vals._GITHUB_FLOW || kGithubFlowFresh) === kGithubFlowImport &&
221
+ !isValidGithubRepoInput(vals._GITHUB_SOURCE_REPO)
222
+ ) {
223
+ return 'Source repo must be in "owner/repo" format.';
224
+ }
225
+ return "";
226
+ })();
227
+ if (preflightError) {
228
+ setFormError(preflightError);
229
+ setSetupError(null);
230
+ setStep(
231
+ Math.max(
232
+ 0,
233
+ kWelcomeGroups.findIndex((group) => group.id === "github"),
234
+ ),
235
+ );
236
+ return;
237
+ }
238
+ setStep(kSetupStepIndex);
239
+ setLoading(true);
240
+ setFormError(null);
241
+ setSetupError(null);
242
+ resetPairingState();
243
+
244
+ const wasImport =
245
+ (vals._GITHUB_FLOW || kGithubFlowFresh) === kGithubFlowImport;
246
+ try {
247
+ const result = await runOnboard(vars, vals.MODEL_KEY, {
248
+ importMode: wasImport,
249
+ });
250
+ if (!result.ok) throw new Error(result.error || "Onboarding failed");
251
+ const pairingChannel = getPreferredPairingChannel(vals);
252
+ if (!pairingChannel) {
253
+ throw new Error(
254
+ "No Telegram or Discord bot token configured for pairing.",
255
+ );
256
+ }
257
+ setVals((prev) => ({
258
+ ...prev,
259
+ [kPairingChannelKey]: pairingChannel,
260
+ }));
261
+ setLoading(false);
262
+ setStep(kPairingStepIndex);
263
+ resetPairingState();
264
+ setSetupError(null);
265
+ } catch (err) {
266
+ console.error("Onboard error:", err);
267
+ setSetupError(err.message || "Onboarding failed");
268
+ setLoading(false);
269
+ }
270
+ };
271
+
272
+ const finishOnboarding = () => {
273
+ localStorage.removeItem(kOnboardingStorageKey);
274
+ onComplete();
275
+ };
276
+
277
+ const goBack = () => {
278
+ if (isSetupStep) return;
279
+ setFormError(null);
280
+ setStep((prev) => Math.max(0, prev - 1));
281
+ };
282
+
283
+ const goBackFromSetupError = () => {
284
+ setLoading(false);
285
+ setSetupError(null);
286
+ setStep(kWelcomeGroups.length - 1);
287
+ };
288
+
289
+ const goNext = async () => {
290
+ if (!activeGroup || !currentGroupValid) return;
291
+ setFormError(null);
292
+ if (activeGroup.id === "github") {
293
+ const githubFlow = vals._GITHUB_FLOW || kGithubFlowFresh;
294
+ const targetRepoMode =
295
+ githubFlow === kGithubFlowImport
296
+ ? kGithubTargetRepoModeCreate
297
+ : vals._GITHUB_TARGET_REPO_MODE || kGithubTargetRepoModeCreate;
298
+ const targetVerifyMode =
299
+ targetRepoMode === kGithubTargetRepoModeExistingEmpty
300
+ ? kRepoModeExisting
301
+ : kRepoModeNew;
302
+ const sourceRepo =
303
+ githubFlow === kGithubFlowImport
304
+ ? vals._GITHUB_SOURCE_REPO
305
+ : vals.GITHUB_WORKSPACE_REPO;
306
+ setGithubStepLoading(true);
307
+ clearPlaceholderReview();
308
+ try {
309
+ if (githubFlow === kGithubFlowImport) {
310
+ const sourceResult = await verifyGithubOnboardingRepo(
311
+ sourceRepo,
312
+ vals.GITHUB_TOKEN,
313
+ kRepoModeExisting,
314
+ );
315
+ if (!sourceResult?.ok) {
316
+ setFormError(sourceResult?.error || "GitHub source verification failed");
317
+ return;
318
+ }
319
+ if (sourceResult.repoIsEmpty) {
320
+ setFormError(
321
+ "That source repository is empty. Use Start fresh if you want AlphaClaw to bootstrap a new setup there.",
322
+ );
323
+ return;
324
+ }
325
+ const targetResult = await verifyGithubOnboardingRepo(
326
+ vals.GITHUB_WORKSPACE_REPO,
327
+ vals.GITHUB_TOKEN,
328
+ kRepoModeNew,
329
+ );
330
+ if (!targetResult?.ok) {
331
+ setFormError(targetResult?.error || "GitHub target verification failed");
332
+ return;
333
+ }
334
+ if (
335
+ targetRepoMode === kGithubTargetRepoModeCreate &&
336
+ targetResult.repoExists
337
+ ) {
338
+ setFormError(
339
+ "That target repository already exists. Choose Use existing empty repo or pick a new target repo name.",
340
+ );
341
+ return;
342
+ }
343
+ if (
344
+ targetRepoMode === kGithubTargetRepoModeExistingEmpty &&
345
+ !targetResult.repoExists
346
+ ) {
347
+ setFormError(
348
+ "That target repository does not exist yet. Choose Create new repo or enter an existing empty target repo.",
349
+ );
350
+ return;
351
+ }
352
+ if (sourceResult.tempDir && !sourceResult.repoIsEmpty) {
353
+ setImportTempDir(sourceResult.tempDir);
354
+ setImportStep(kImportStepId);
355
+ setImportScanning(true);
356
+ setImportError(null);
357
+ try {
358
+ const scanResult = await scanImportRepo(sourceResult.tempDir);
359
+ if (!scanResult?.ok) {
360
+ setImportError(scanResult?.error || "Import scan failed");
361
+ setImportScanning(false);
362
+ return;
363
+ }
364
+ setImportScanResult(scanResult);
365
+ } catch (scanErr) {
366
+ setImportError(scanErr?.message || "Import scan failed");
367
+ } finally {
368
+ setImportScanning(false);
369
+ }
370
+ return;
371
+ }
372
+ }
373
+ const targetResult = await verifyGithubOnboardingRepo(
374
+ vals.GITHUB_WORKSPACE_REPO,
375
+ vals.GITHUB_TOKEN,
376
+ targetVerifyMode,
377
+ );
378
+ if (!targetResult?.ok) {
379
+ setFormError(targetResult?.error || "GitHub verification failed");
380
+ return;
381
+ }
382
+ if (
383
+ targetRepoMode === kGithubTargetRepoModeCreate &&
384
+ targetResult.repoExists
385
+ ) {
386
+ setFormError(
387
+ "That target repository already exists. Choose Use existing empty repo or pick a new target repo name.",
388
+ );
389
+ return;
390
+ }
391
+ if (
392
+ targetRepoMode === kGithubTargetRepoModeExistingEmpty &&
393
+ !targetResult.repoExists
394
+ ) {
395
+ setFormError(
396
+ "That target repository does not exist yet. Choose Create new repo or enter an existing empty target repo.",
397
+ );
398
+ return;
399
+ }
400
+ } catch (err) {
401
+ setFormError(err?.message || "GitHub verification failed");
402
+ return;
403
+ } finally {
404
+ setGithubStepLoading(false);
405
+ }
406
+ }
407
+ setStep((prev) => Math.min(kWelcomeGroups.length - 1, prev + 1));
408
+ };
409
+
410
+ const handleImportApprove = async (approvedSecrets = []) => {
411
+ setImportScanning(true);
412
+ setImportError(null);
413
+ try {
414
+ const skipSecretExtraction = approvedSecrets.length === 0;
415
+ const result = await applyImport({
416
+ tempDir: importTempDir,
417
+ approvedSecrets,
418
+ skipSecretExtraction,
419
+ githubRepo: vals.GITHUB_WORKSPACE_REPO,
420
+ githubToken: vals.GITHUB_TOKEN,
421
+ });
422
+ if (!result?.ok) {
423
+ setImportError(result?.error || "Import failed");
424
+ setImportScanning(false);
425
+ return;
426
+ }
427
+ const nextPlaceholderReview = normalizePlaceholderReview(
428
+ result.placeholderReview,
429
+ );
430
+ setVals((prev) => ({
431
+ ...prev,
432
+ ...(result.preFill || {}),
433
+ [kImportPlaceholderReviewKey]: nextPlaceholderReview,
434
+ [kImportPlaceholderSkipConfirmedKey]: false,
435
+ }));
436
+ if (nextPlaceholderReview.found) {
437
+ setImportStep(kPlaceholderReviewStepId);
438
+ return;
439
+ }
440
+ clearPlaceholderReview();
441
+ setImportStep(null);
442
+ setStep((prev) => Math.min(kWelcomeGroups.length - 1, prev + 1));
443
+ } catch (err) {
444
+ setImportError(err?.message || "Import failed");
445
+ } finally {
446
+ setImportScanning(false);
447
+ }
448
+ };
449
+
450
+ const handleShowSecretReview = () => {
451
+ setImportStep(kSecretReviewStepId);
452
+ };
453
+
454
+ const handleSecretReviewBack = () => {
455
+ setImportStep(kImportStepId);
456
+ };
457
+
458
+ const handleImportBack = () => {
459
+ setImportStep(null);
460
+ setImportTempDir(null);
461
+ setImportScanResult(null);
462
+ setImportError(null);
463
+ clearPlaceholderReview();
464
+ };
465
+
466
+ const handlePlaceholderReviewContinue = () => {
467
+ clearPlaceholderReview();
468
+ setImportStep(null);
469
+ setStep((prev) => Math.min(kWelcomeGroups.length - 1, prev + 1));
470
+ };
471
+
472
+ const isImportStep = importStep === kImportStepId;
473
+ const isSecretReviewStep = importStep === kSecretReviewStepId;
474
+ const isPlaceholderReviewStep = importStep === kPlaceholderReviewStepId;
475
+ const activeStepLabel = isImportStep
476
+ ? "Import"
477
+ : isSecretReviewStep
478
+ ? "Review Secrets"
479
+ : isPlaceholderReviewStep
480
+ ? "Review Env Vars"
481
+ : isSetupStep
482
+ ? "Initializing"
483
+ : isPairingStep
484
+ ? "Pairing"
485
+ : activeGroup?.title || "Setup";
486
+ const stepNumber =
487
+ isImportStep || isSecretReviewStep || isPlaceholderReviewStep
488
+ ? step + 1
489
+ : isSetupStep
490
+ ? kWelcomeGroups.length + 1
491
+ : isPairingStep
492
+ ? kWelcomeGroups.length + 2
493
+ : step + 1;
494
+
495
+ return {
496
+ state: {
497
+ vals,
498
+ step,
499
+ setupError,
500
+ modelsLoading,
501
+ modelsError,
502
+ showAllModels,
503
+ loading,
504
+ githubStepLoading,
505
+ formError,
506
+ importScanResult,
507
+ importScanning,
508
+ importError,
509
+ selectedProvider,
510
+ modelOptions,
511
+ canToggleFullCatalog,
512
+ visibleAiFieldKeys,
513
+ hasAi,
514
+ allValid,
515
+ isSetupStep,
516
+ isPairingStep,
517
+ activeGroup,
518
+ currentGroupValid,
519
+ selectedPairingChannel,
520
+ placeholderReview,
521
+ isImportStep,
522
+ isSecretReviewStep,
523
+ isPlaceholderReviewStep,
524
+ activeStepLabel,
525
+ stepNumber,
526
+ codexStatus,
527
+ codexLoading,
528
+ codexManualInput,
529
+ codexExchanging,
530
+ codexAuthStarted,
531
+ codexAuthWaiting,
532
+ pairingStatusPoll,
533
+ pairingRequestsPoll,
534
+ pairingChannels,
535
+ canFinishPairing,
536
+ pairingError,
537
+ pairingComplete,
538
+ },
539
+ actions: {
540
+ setVals,
541
+ setValue,
542
+ setShowAllModels,
543
+ setCodexManualInput,
544
+ startCodexAuth,
545
+ completeCodexAuth,
546
+ handleCodexDisconnect,
547
+ handleSubmit,
548
+ finishOnboarding,
549
+ goBack,
550
+ goBackFromSetupError,
551
+ goNext,
552
+ handleImportApprove,
553
+ handleShowSecretReview,
554
+ handleSecretReviewBack,
555
+ handleImportBack,
556
+ handlePlaceholderReviewContinue,
557
+ handlePairingApprove,
558
+ handlePairingReject,
559
+ },
560
+ };
561
+ };