@clappstore/connect 0.7.0 → 0.7.2
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.
- package/clapps/settings/README.md +74 -0
- package/clapps/settings/clapp.json +25 -0
- package/clapps/settings/components/ProviderEditor.tsx +512 -0
- package/clapps/settings/components/ProviderList.tsx +300 -0
- package/clapps/settings/components/SessionList.tsx +189 -0
- package/clapps/settings/handlers/settings-handler.js +760 -0
- package/clapps/settings/views/default.settings.view.md +38 -0
- package/clapps/settings/views/settings.app.md +12 -0
- package/dist/clapp-handler.d.ts +16 -0
- package/dist/clapp-handler.d.ts.map +1 -0
- package/dist/clapp-handler.js +2 -0
- package/dist/clapp-handler.js.map +1 -0
- package/dist/clapp-loader.d.ts +3 -0
- package/dist/clapp-loader.d.ts.map +1 -0
- package/dist/clapp-loader.js +61 -0
- package/dist/clapp-loader.js.map +1 -0
- package/dist/defaults.d.ts +9 -2
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +130 -71
- package/dist/defaults.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -12
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -1
- package/dist/server.js.map +1 -1
- package/package.json +8 -4
- package/dist/settings-handler.d.ts +0 -26
- package/dist/settings-handler.d.ts.map +0 -1
- package/dist/settings-handler.js +0 -144
- package/dist/settings-handler.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
|
+
}
|