@clappstore/connect 0.6.1 → 0.7.1

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 (60) hide show
  1. package/clapps/settings/README.md +74 -0
  2. package/clapps/settings/clapp.json +25 -0
  3. package/clapps/settings/components/ProviderEditor.tsx +512 -0
  4. package/clapps/settings/components/ProviderList.tsx +300 -0
  5. package/clapps/settings/components/SessionList.tsx +189 -0
  6. package/clapps/settings/handlers/settings-handler.js +760 -0
  7. package/clapps/settings/views/default.settings.view.md +38 -0
  8. package/clapps/settings/views/settings.app.md +12 -0
  9. package/dist/clapp-handler.d.ts +16 -0
  10. package/dist/clapp-handler.d.ts.map +1 -0
  11. package/dist/clapp-handler.js +2 -0
  12. package/dist/clapp-handler.js.map +1 -0
  13. package/dist/clapp-loader.d.ts +3 -0
  14. package/dist/clapp-loader.d.ts.map +1 -0
  15. package/dist/clapp-loader.js +61 -0
  16. package/dist/clapp-loader.js.map +1 -0
  17. package/dist/defaults.d.ts +9 -5
  18. package/dist/defaults.d.ts.map +1 -1
  19. package/dist/defaults.js +130 -79
  20. package/dist/defaults.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +57 -89
  24. package/dist/index.js.map +1 -1
  25. package/dist/server.d.ts +14 -0
  26. package/dist/server.d.ts.map +1 -0
  27. package/dist/server.js +204 -0
  28. package/dist/server.js.map +1 -0
  29. package/package.json +13 -5
  30. package/web-app/assets/index-BYIO2GGA.css +1 -0
  31. package/web-app/assets/index-kbnfuSZC.js +128 -0
  32. package/web-app/index.html +13 -0
  33. package/dist/agent-client.d.ts +0 -28
  34. package/dist/agent-client.d.ts.map +0 -1
  35. package/dist/agent-client.js +0 -159
  36. package/dist/agent-client.js.map +0 -1
  37. package/dist/agent-handler.d.ts +0 -18
  38. package/dist/agent-handler.d.ts.map +0 -1
  39. package/dist/agent-handler.js +0 -17
  40. package/dist/agent-handler.js.map +0 -1
  41. package/dist/credentials.d.ts +0 -5
  42. package/dist/credentials.d.ts.map +0 -1
  43. package/dist/credentials.js +0 -32
  44. package/dist/credentials.js.map +0 -1
  45. package/dist/intent-poller.d.ts +0 -22
  46. package/dist/intent-poller.d.ts.map +0 -1
  47. package/dist/intent-poller.js +0 -51
  48. package/dist/intent-poller.js.map +0 -1
  49. package/dist/relay-client.d.ts +0 -16
  50. package/dist/relay-client.d.ts.map +0 -1
  51. package/dist/relay-client.js +0 -121
  52. package/dist/relay-client.js.map +0 -1
  53. package/dist/settings-handler.d.ts +0 -26
  54. package/dist/settings-handler.d.ts.map +0 -1
  55. package/dist/settings-handler.js +0 -149
  56. package/dist/settings-handler.js.map +0 -1
  57. package/dist/state-watcher.d.ts +0 -23
  58. package/dist/state-watcher.d.ts.map +0 -1
  59. package/dist/state-watcher.js +0 -121
  60. package/dist/state-watcher.js.map +0 -1
@@ -0,0 +1,74 @@
1
+ # Settings Clapp
2
+
3
+ The default settings clapp for OpenClaw. Provides UI for managing AI providers, models, and session configurations.
4
+
5
+ This is the canonical location for the settings handler. During build, the handler is compiled and bundled into `packages/connect/clapps/settings/`, where it is loaded dynamically at startup via the `clapp.json` manifest.
6
+
7
+ ## Features
8
+
9
+ - **Provider Management**: Add, edit, and delete AI provider credentials
10
+ - Anthropic (API key + Claude subscription)
11
+ - OpenAI (API key + Codex subscription)
12
+ - Kimi Coding (API key)
13
+
14
+ - **Model Selection**: Choose default model for new sessions
15
+
16
+ - **Session Management**: View active sessions and their model overrides
17
+ - See which sessions differ from the system default
18
+ - Reset individual sessions to default
19
+ - Apply default to all sessions at once
20
+
21
+ ## Structure
22
+
23
+ ```
24
+ settings/
25
+ ├── clapp.json # Manifest
26
+ ├── views/
27
+ │ ├── settings.app.md # App definition
28
+ │ └── default.settings.view.md # Main view layout
29
+ ├── components/
30
+ │ ├── ProviderList.tsx # Provider/model selector
31
+ │ ├── ProviderEditor.tsx # Add/edit provider modal
32
+ │ └── SessionList.tsx # Active sessions display
33
+ ├── handlers/
34
+ │ └── settings-handler.ts # Intent handler (factory function, zero external imports)
35
+ └── README.md
36
+ ```
37
+
38
+ ## Handler
39
+
40
+ The handler (`handlers/settings-handler.ts`) is a self-contained factory function that receives a `ClappHandlerContext` and returns a `ClappHandler`. It has zero external package imports — only Node.js built-ins — so it can be compiled standalone and loaded dynamically.
41
+
42
+ The handler implements:
43
+ - `handleIntent()` — intercepts all `settings.*` intents
44
+ - `init()` — writes initial settings state on startup
45
+ - `onConnect()` — refreshes state when a browser client connects
46
+ - `refresh()` — periodic refresh to detect external config changes
47
+
48
+ ## Intents
49
+
50
+ | Intent | Description |
51
+ |--------|-------------|
52
+ | `settings.setAnthropicKey` | Set Anthropic API key |
53
+ | `settings.setClaudeToken` | Set Claude subscription token |
54
+ | `settings.setOpenAIKey` | Set OpenAI API key |
55
+ | `settings.setKimiCodingKey` | Set Kimi Coding API key |
56
+ | `settings.setActiveModel` | Set the default model |
57
+ | `settings.deleteProvider` | Remove a provider profile |
58
+ | `settings.listSessions` | Refresh session list |
59
+ | `settings.resetSessionModel` | Reset session to default |
60
+ | `settings.applyDefaultToAll` | Sync all sessions to default |
61
+
62
+ ## Development
63
+
64
+ ```bash
65
+ # Build (compiles handler + copies assets into packages/connect/clapps/)
66
+ pnpm --filter @clappstore/connect build
67
+
68
+ # Run the connect server
69
+ node packages/connect/dist/index.js
70
+ ```
71
+
72
+ ## Customization
73
+
74
+ The clapp is installed to `~/.openclaw/clapps/settings/` on first run. Edit files there for local customization — changes take effect on reload.
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@clapps/settings",
3
+ "version": "0.6.0",
4
+ "description": "AI provider and model settings for OpenClaw",
5
+ "author": "openclaw",
6
+ "license": "MIT",
7
+ "main": "views/settings.app.md",
8
+ "views": [
9
+ "views/settings.app.md",
10
+ "views/default.settings.view.md"
11
+ ],
12
+ "components": [
13
+ "components/ProviderList.tsx",
14
+ "components/ProviderEditor.tsx",
15
+ "components/SessionList.tsx"
16
+ ],
17
+ "handlers": [
18
+ "handlers/settings-handler.ts"
19
+ ],
20
+ "openclaw": {
21
+ "minVersion": "2026.2.0",
22
+ "permissions": ["auth-profiles", "config", "sessions"],
23
+ "default": true
24
+ }
25
+ }
@@ -0,0 +1,512 @@
1
+ import { useState, useEffect } from "react";
2
+ import { useIntent } from "@clapps/renderer";
3
+ import { cn } from "@/lib/utils";
4
+ import { X, Eye, EyeOff, Loader2, ExternalLink } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+
7
+ export type ProviderType = "anthropic" | "openai" | "kimi-coding";
8
+ export type AuthMode = "api-key" | "subscription";
9
+
10
+ interface ProviderConfig {
11
+ type: ProviderType;
12
+ name: string;
13
+ description: string;
14
+ authModes: {
15
+ mode: AuthMode;
16
+ label: string;
17
+ description: string;
18
+ fields: FieldConfig[];
19
+ helpUrl?: string;
20
+ }[];
21
+ }
22
+
23
+ interface FieldConfig {
24
+ name: string;
25
+ label: string;
26
+ type: "text" | "password" | "textarea";
27
+ placeholder?: string;
28
+ helpText?: string;
29
+ }
30
+
31
+ const PROVIDER_CONFIGS: ProviderConfig[] = [
32
+ {
33
+ type: "anthropic",
34
+ name: "Anthropic",
35
+ description: "Claude models via API or subscription",
36
+ authModes: [
37
+ {
38
+ mode: "api-key",
39
+ label: "API Key",
40
+ description: "Use your Anthropic API key (usage-based billing)",
41
+ helpUrl: "https://console.anthropic.com/settings/keys",
42
+ fields: [
43
+ {
44
+ name: "apiKey",
45
+ label: "API Key",
46
+ type: "password",
47
+ placeholder: "sk-ant-api03-...",
48
+ helpText: "Get your API key from the Anthropic Console",
49
+ },
50
+ ],
51
+ },
52
+ {
53
+ mode: "subscription",
54
+ label: "Claude Subscription",
55
+ description: "Use your Claude Pro/Max subscription",
56
+ helpUrl: "https://docs.openclaw.ai/providers/anthropic#option-b-claude-setup-token",
57
+ fields: [
58
+ {
59
+ name: "setupToken",
60
+ label: "Setup Token",
61
+ type: "textarea",
62
+ placeholder: "Paste your setup token here...",
63
+ helpText: "Run 'claude setup-token' in your terminal to generate this",
64
+ },
65
+ ],
66
+ },
67
+ ],
68
+ },
69
+ {
70
+ type: "openai",
71
+ name: "OpenAI",
72
+ description: "GPT models via API or Codex subscription",
73
+ authModes: [
74
+ {
75
+ mode: "api-key",
76
+ label: "API Key",
77
+ description: "Use your OpenAI API key (usage-based billing)",
78
+ helpUrl: "https://platform.openai.com/api-keys",
79
+ fields: [
80
+ {
81
+ name: "apiKey",
82
+ label: "API Key",
83
+ type: "password",
84
+ placeholder: "sk-...",
85
+ helpText: "Get your API key from the OpenAI dashboard",
86
+ },
87
+ ],
88
+ },
89
+ {
90
+ mode: "subscription",
91
+ label: "Codex Subscription",
92
+ description: "Use your ChatGPT Plus/Pro subscription via Codex",
93
+ helpUrl: "https://docs.openclaw.ai/providers/openai#option-b-openai-code-codex-subscription",
94
+ fields: [], // OAuth flow - no fields, just a button
95
+ },
96
+ ],
97
+ },
98
+ {
99
+ type: "kimi-coding",
100
+ name: "Kimi Coding",
101
+ description: "Moonshot's Kimi K2 models for coding",
102
+ authModes: [
103
+ {
104
+ mode: "api-key",
105
+ label: "API Key",
106
+ description: "Use your Kimi Coding API key",
107
+ helpUrl: "https://platform.moonshot.cn/console/api-keys",
108
+ fields: [
109
+ {
110
+ name: "apiKey",
111
+ label: "API Key",
112
+ type: "password",
113
+ placeholder: "sk-...",
114
+ helpText: "Get your API key from the Moonshot platform",
115
+ },
116
+ ],
117
+ },
118
+ ],
119
+ },
120
+ ];
121
+
122
+ interface ProviderEditorProps {
123
+ isOpen: boolean;
124
+ onClose: () => void;
125
+ editingProvider?: {
126
+ id: string;
127
+ name: string;
128
+ type: ProviderType;
129
+ mode: string;
130
+ authType?: "api-key" | "subscription" | "oauth";
131
+ maskedCredential?: string;
132
+ } | null;
133
+ }
134
+
135
+ export function ProviderEditor({ isOpen, onClose, editingProvider }: ProviderEditorProps) {
136
+ const { emit } = useIntent();
137
+
138
+ const [step, setStep] = useState<"select" | "configure">("select");
139
+ const [selectedProvider, setSelectedProvider] = useState<ProviderType | null>(null);
140
+ const [selectedAuthMode, setSelectedAuthMode] = useState<AuthMode | null>(null);
141
+ const [customName, setCustomName] = useState("");
142
+ const [fieldValues, setFieldValues] = useState<Record<string, string>>({});
143
+ const [showPassword, setShowPassword] = useState<Record<string, boolean>>({});
144
+ const [isSaving, setIsSaving] = useState(false);
145
+ const [error, setError] = useState<string | null>(null);
146
+
147
+ // Reset state when modal opens/closes
148
+ useEffect(() => {
149
+ if (isOpen) {
150
+ if (editingProvider) {
151
+ // Editing existing provider
152
+ setSelectedProvider(editingProvider.type);
153
+ // Use authType if available, otherwise fall back to mode-based detection
154
+ const authMode: AuthMode = editingProvider.authType === "subscription" || editingProvider.authType === "oauth"
155
+ ? "subscription"
156
+ : editingProvider.mode === "oauth"
157
+ ? "subscription"
158
+ : "api-key";
159
+ setSelectedAuthMode(authMode);
160
+ setCustomName(editingProvider.name);
161
+ setStep("configure");
162
+ } else {
163
+ // Adding new provider
164
+ setStep("select");
165
+ setSelectedProvider(null);
166
+ setSelectedAuthMode(null);
167
+ setCustomName("");
168
+ }
169
+ setFieldValues({});
170
+ setShowPassword({});
171
+ setError(null);
172
+ setIsSaving(false);
173
+ }
174
+ }, [isOpen, editingProvider]);
175
+
176
+ const providerConfig = selectedProvider
177
+ ? PROVIDER_CONFIGS.find(p => p.type === selectedProvider)
178
+ : null;
179
+
180
+ const authModeConfig = providerConfig && selectedAuthMode
181
+ ? providerConfig.authModes.find(a => a.mode === selectedAuthMode)
182
+ : null;
183
+
184
+ const handleProviderSelect = (type: ProviderType) => {
185
+ setSelectedProvider(type);
186
+ const config = PROVIDER_CONFIGS.find(p => p.type === type);
187
+ if (config) {
188
+ // Auto-select first auth mode
189
+ setSelectedAuthMode(config.authModes[0].mode);
190
+ // Set default name
191
+ setCustomName(config.name);
192
+ }
193
+ setStep("configure");
194
+ };
195
+
196
+ const handleFieldChange = (fieldName: string, value: string) => {
197
+ setFieldValues(prev => ({ ...prev, [fieldName]: value }));
198
+ setError(null);
199
+ };
200
+
201
+ const togglePasswordVisibility = (fieldName: string) => {
202
+ setShowPassword(prev => ({ ...prev, [fieldName]: !prev[fieldName] }));
203
+ };
204
+
205
+ const handleSave = async () => {
206
+ if (!selectedProvider || !selectedAuthMode || !providerConfig) return;
207
+
208
+ // Validate required fields
209
+ if (authModeConfig?.fields.length) {
210
+ for (const field of authModeConfig.fields) {
211
+ if (!fieldValues[field.name]?.trim()) {
212
+ setError(`${field.label} is required`);
213
+ return;
214
+ }
215
+ }
216
+ }
217
+
218
+ setIsSaving(true);
219
+ setError(null);
220
+
221
+ try {
222
+ // Determine the intent based on provider and auth mode
223
+ const intentName = getIntentName(selectedProvider, selectedAuthMode);
224
+ const payload: Record<string, unknown> = {
225
+ customName: customName.trim() || providerConfig.name,
226
+ profileId: editingProvider?.id,
227
+ };
228
+
229
+ // Add field values to payload
230
+ for (const field of authModeConfig?.fields ?? []) {
231
+ payload[field.name] = fieldValues[field.name]?.trim();
232
+ }
233
+
234
+ emit(intentName, payload);
235
+
236
+ // Wait a bit for the handler to process
237
+ await new Promise(resolve => setTimeout(resolve, 1000));
238
+
239
+ onClose();
240
+ } catch (err) {
241
+ setError(err instanceof Error ? err.message : "Failed to save provider");
242
+ } finally {
243
+ setIsSaving(false);
244
+ }
245
+ };
246
+
247
+ const handleOAuthLogin = () => {
248
+ if (!selectedProvider) return;
249
+
250
+ setIsSaving(true);
251
+ emit("settings.startOAuth", {
252
+ provider: selectedProvider === "openai" ? "openai-codex" : selectedProvider,
253
+ customName: customName.trim() || providerConfig?.name,
254
+ });
255
+
256
+ // OAuth will redirect, so just show loading
257
+ };
258
+
259
+ const handleDelete = () => {
260
+ if (!editingProvider) return;
261
+
262
+ if (confirm(`Delete "${editingProvider.name}"? This cannot be undone.`)) {
263
+ emit("settings.deleteProvider", { profileId: editingProvider.id });
264
+ onClose();
265
+ }
266
+ };
267
+
268
+ if (!isOpen) return null;
269
+
270
+ return (
271
+ <div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
272
+ {/* Backdrop */}
273
+ <div
274
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
275
+ onClick={onClose}
276
+ />
277
+
278
+ {/* Modal */}
279
+ <div className="relative w-full sm:max-w-md bg-background rounded-t-2xl sm:rounded-2xl shadow-xl max-h-[90vh] overflow-hidden flex flex-col animate-in slide-in-from-bottom-4 sm:slide-in-from-bottom-0 sm:zoom-in-95">
280
+ {/* Header */}
281
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
282
+ <h2 className="text-lg font-semibold">
283
+ {editingProvider ? "Edit Provider" : "Add Provider"}
284
+ </h2>
285
+ <button
286
+ onClick={onClose}
287
+ className="p-1 rounded-full hover:bg-muted transition-colors"
288
+ aria-label="Close"
289
+ >
290
+ <X className="h-5 w-5" />
291
+ </button>
292
+ </div>
293
+
294
+ {/* Content */}
295
+ <div className="flex-1 overflow-y-auto p-4">
296
+ {step === "select" && (
297
+ <div className="space-y-3">
298
+ <p className="text-sm text-muted-foreground mb-4">
299
+ Choose a provider to add:
300
+ </p>
301
+ {PROVIDER_CONFIGS.map(provider => (
302
+ <button
303
+ key={provider.type}
304
+ onClick={() => handleProviderSelect(provider.type)}
305
+ className="w-full text-left p-4 rounded-lg border border-border hover:border-primary hover:bg-muted/50 transition-colors"
306
+ >
307
+ <div className="font-medium">{provider.name}</div>
308
+ <div className="text-sm text-muted-foreground">{provider.description}</div>
309
+ </button>
310
+ ))}
311
+ </div>
312
+ )}
313
+
314
+ {step === "configure" && providerConfig && (
315
+ <div className="space-y-4">
316
+ {/* Provider name input */}
317
+ <div className="space-y-2">
318
+ <label className="text-sm font-medium">Display Name</label>
319
+ <input
320
+ type="text"
321
+ value={customName}
322
+ onChange={(e) => setCustomName(e.target.value)}
323
+ placeholder={providerConfig.name}
324
+ className="w-full h-10 px-3 rounded-md border border-input bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary"
325
+ />
326
+ <p className="text-xs text-muted-foreground">
327
+ Give this configuration a custom name (e.g., "Work Account", "Personal")
328
+ </p>
329
+ </div>
330
+
331
+ {/* Auth mode selector (if multiple options) */}
332
+ {providerConfig.authModes.length > 1 && (
333
+ <div className="space-y-2">
334
+ <label className="text-sm font-medium">Authentication Method</label>
335
+ <div className="grid grid-cols-2 gap-2">
336
+ {providerConfig.authModes.map(authMode => (
337
+ <button
338
+ key={authMode.mode}
339
+ onClick={() => setSelectedAuthMode(authMode.mode)}
340
+ className={cn(
341
+ "p-3 rounded-lg border text-left transition-colors",
342
+ selectedAuthMode === authMode.mode
343
+ ? "border-primary bg-primary/10"
344
+ : "border-border hover:border-muted-foreground"
345
+ )}
346
+ >
347
+ <div className="text-sm font-medium">{authMode.label}</div>
348
+ <div className="text-xs text-muted-foreground mt-0.5">
349
+ {authMode.description}
350
+ </div>
351
+ </button>
352
+ ))}
353
+ </div>
354
+ </div>
355
+ )}
356
+
357
+ {/* Auth fields */}
358
+ {authModeConfig && (
359
+ <div className="space-y-4">
360
+ {authModeConfig.fields.length > 0 ? (
361
+ authModeConfig.fields.map(field => (
362
+ <div key={field.name} className="space-y-2">
363
+ <label className="text-sm font-medium">{field.label}</label>
364
+ {/* Show current credential when editing */}
365
+ {editingProvider?.maskedCredential && !fieldValues[field.name] && (
366
+ <div className="text-xs text-muted-foreground bg-muted/50 px-2 py-1 rounded mb-1">
367
+ Current: {editingProvider.maskedCredential}
368
+ </div>
369
+ )}
370
+ {field.type === "textarea" ? (
371
+ <textarea
372
+ value={fieldValues[field.name] ?? ""}
373
+ onChange={(e) => handleFieldChange(field.name, e.target.value)}
374
+ placeholder={editingProvider ? "Enter new value to replace..." : field.placeholder}
375
+ rows={4}
376
+ className="w-full px-3 py-2 rounded-md border border-input bg-background text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary resize-none"
377
+ />
378
+ ) : (
379
+ <div className="relative">
380
+ <input
381
+ type={field.type === "password" && !showPassword[field.name] ? "password" : "text"}
382
+ value={fieldValues[field.name] ?? ""}
383
+ onChange={(e) => handleFieldChange(field.name, e.target.value)}
384
+ placeholder={editingProvider ? "Enter new value to replace..." : field.placeholder}
385
+ className="w-full h-10 px-3 pr-10 rounded-md border border-input bg-background text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary"
386
+ />
387
+ {field.type === "password" && (
388
+ <button
389
+ type="button"
390
+ onClick={() => togglePasswordVisibility(field.name)}
391
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-muted-foreground hover:text-foreground"
392
+ >
393
+ {showPassword[field.name] ? (
394
+ <EyeOff className="h-4 w-4" />
395
+ ) : (
396
+ <Eye className="h-4 w-4" />
397
+ )}
398
+ </button>
399
+ )}
400
+ </div>
401
+ )}
402
+ {field.helpText && (
403
+ <p className="text-xs text-muted-foreground">{field.helpText}</p>
404
+ )}
405
+ </div>
406
+ ))
407
+ ) : (
408
+ /* OAuth flow - show connect button */
409
+ <div className="space-y-3">
410
+ <p className="text-sm text-muted-foreground">
411
+ Click the button below to sign in with your {providerConfig.name} account.
412
+ </p>
413
+ <Button
414
+ onClick={handleOAuthLogin}
415
+ disabled={isSaving}
416
+ className="w-full"
417
+ >
418
+ {isSaving ? (
419
+ <>
420
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
421
+ Connecting...
422
+ </>
423
+ ) : (
424
+ `Sign in with ${providerConfig.name}`
425
+ )}
426
+ </Button>
427
+ </div>
428
+ )}
429
+
430
+ {/* Help link */}
431
+ {authModeConfig.helpUrl && (
432
+ <a
433
+ href={authModeConfig.helpUrl}
434
+ target="_blank"
435
+ rel="noopener noreferrer"
436
+ className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
437
+ >
438
+ Learn more
439
+ <ExternalLink className="h-3 w-3" />
440
+ </a>
441
+ )}
442
+ </div>
443
+ )}
444
+
445
+ {/* Error message */}
446
+ {error && (
447
+ <div className="p-3 rounded-md bg-destructive/10 text-destructive text-sm">
448
+ {error}
449
+ </div>
450
+ )}
451
+ </div>
452
+ )}
453
+ </div>
454
+
455
+ {/* Footer */}
456
+ {step === "configure" && authModeConfig?.fields.length !== 0 && (
457
+ <div className="flex items-center gap-2 px-4 py-3 border-t border-border shrink-0">
458
+ {editingProvider && (
459
+ <Button
460
+ variant="destructive"
461
+ onClick={handleDelete}
462
+ disabled={isSaving}
463
+ className="mr-auto"
464
+ >
465
+ Delete
466
+ </Button>
467
+ )}
468
+ <Button
469
+ variant="outline"
470
+ onClick={() => editingProvider ? onClose() : setStep("select")}
471
+ disabled={isSaving}
472
+ >
473
+ {editingProvider ? "Cancel" : "Back"}
474
+ </Button>
475
+ <Button
476
+ onClick={handleSave}
477
+ disabled={isSaving}
478
+ >
479
+ {isSaving ? (
480
+ <>
481
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
482
+ Saving...
483
+ </>
484
+ ) : (
485
+ editingProvider ? "Update" : "Add Provider"
486
+ )}
487
+ </Button>
488
+ </div>
489
+ )}
490
+ </div>
491
+ </div>
492
+ );
493
+ }
494
+
495
+ function getIntentName(provider: ProviderType, authMode: AuthMode): string {
496
+ const map: Record<string, Record<AuthMode, string>> = {
497
+ anthropic: {
498
+ "api-key": "settings.setAnthropicKey",
499
+ subscription: "settings.setClaudeToken",
500
+ },
501
+ openai: {
502
+ "api-key": "settings.setOpenAIKey",
503
+ subscription: "settings.startOAuth", // Handled separately
504
+ },
505
+ "kimi-coding": {
506
+ "api-key": "settings.setKimiCodingKey",
507
+ subscription: "settings.setKimiCodingKey", // No subscription mode
508
+ },
509
+ };
510
+
511
+ return map[provider]?.[authMode] ?? "settings.addProvider";
512
+ }