@hileeon/mcc 0.1.4 → 0.1.5
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/dist/dashboard-server.js +1 -1
- package/dist/dashboard-server.js.map +1 -1
- package/dist/mcc.js +1 -1
- package/{ui/dist → dist/ui}/index.html +2 -2
- package/package.json +5 -1
- package/.claude/CLAUDE.md +0 -204
- package/.claude/agents/.gitkeep +0 -0
- package/.claude/settings.json +0 -9
- package/.claude/skills/.gitkeep +0 -0
- package/docs/decisions.md +0 -33
- package/docs/lessons.md +0 -8
- package/docs/product.md +0 -37
- package/src/accounts/instance-manager.ts +0 -58
- package/src/accounts/shared-manager.ts +0 -154
- package/src/accounts/store.ts +0 -111
- package/src/core/model-router.ts +0 -82
- package/src/dashboard-server.ts +0 -427
- package/src/mcc.ts +0 -482
- package/src/mcp/external-registry.ts +0 -73
- package/src/mcp/installer.ts +0 -258
- package/src/mcp/mcp-config.ts +0 -168
- package/src/mcp/registry.ts +0 -89
- package/src/proxy/proxy-daemon.ts +0 -184
- package/src/proxy/proxy-entry.ts +0 -63
- package/src/proxy/proxy-paths.ts +0 -97
- package/src/proxy/proxy-server.ts +0 -278
- package/src/proxy/upstream-url.ts +0 -38
- package/src/shared/logger.ts +0 -140
- package/src/shared/provider-preset-catalog.ts +0 -340
- package/tsconfig.json +0 -33
- package/ui/.prettierrc +0 -9
- package/ui/index.html +0 -12
- package/ui/package.json +0 -33
- package/ui/postcss.config.js +0 -6
- package/ui/src/App.tsx +0 -753
- package/ui/src/components/ui/button.tsx +0 -48
- package/ui/src/components/ui/card.tsx +0 -50
- package/ui/src/components/ui/input.tsx +0 -21
- package/ui/src/components/ui/label.tsx +0 -20
- package/ui/src/components/ui/select.tsx +0 -80
- package/ui/src/components/ui/switch.tsx +0 -26
- package/ui/src/components/ui/tabs.tsx +0 -52
- package/ui/src/index.css +0 -33
- package/ui/src/lib/api.ts +0 -185
- package/ui/src/lib/utils.ts +0 -6
- package/ui/src/main.tsx +0 -10
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/tailwind.config.js +0 -49
- package/ui/tsconfig.json +0 -25
- package/ui/vite.config.ts +0 -20
- /package/{ui/dist → dist/ui}/assets/index-B16lhKZ6.js +0 -0
- /package/{ui/dist → dist/ui}/assets/index-jEfiB6-h.css +0 -0
package/ui/src/App.tsx
DELETED
|
@@ -1,753 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
3
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
4
|
-
import { Button } from '@/components/ui/button';
|
|
5
|
-
import { Input } from '@/components/ui/input';
|
|
6
|
-
import { Label } from '@/components/ui/label';
|
|
7
|
-
import { Switch } from '@/components/ui/switch';
|
|
8
|
-
import {
|
|
9
|
-
getProfiles,
|
|
10
|
-
addProfile,
|
|
11
|
-
updateProfile,
|
|
12
|
-
deleteProfile,
|
|
13
|
-
setDefaultProfile,
|
|
14
|
-
getMcpServers,
|
|
15
|
-
getAllMcpServers,
|
|
16
|
-
toggleMcp,
|
|
17
|
-
getStatus,
|
|
18
|
-
getMcpConfig,
|
|
19
|
-
updateMcpConfig,
|
|
20
|
-
getProviderPresets,
|
|
21
|
-
getExternalMcpServers,
|
|
22
|
-
addExternalMcpServer,
|
|
23
|
-
removeExternalMcpServer,
|
|
24
|
-
ping,
|
|
25
|
-
type Profile,
|
|
26
|
-
type McpServer,
|
|
27
|
-
type McpConfig,
|
|
28
|
-
type ProviderPresets,
|
|
29
|
-
type AllMcpServer,
|
|
30
|
-
type ExternalMcpServer,
|
|
31
|
-
} from '@/lib/api';
|
|
32
|
-
|
|
33
|
-
export default function App() {
|
|
34
|
-
const [profiles, setProfiles] = useState<Profile[]>([]);
|
|
35
|
-
const [mcpServers, setMcpServers] = useState<McpServer[]>([]);
|
|
36
|
-
const [allMcpServers, setAllMcpServers] = useState<AllMcpServer[]>([]);
|
|
37
|
-
const [externalMcpServers, setExternalMcpServers] = useState<ExternalMcpServer[]>([]);
|
|
38
|
-
const [currentProfile, setCurrentProfile] = useState<string>('');
|
|
39
|
-
const [connected, setConnected] = useState(true);
|
|
40
|
-
const [loading, setLoading] = useState(true);
|
|
41
|
-
const [error, setError] = useState('');
|
|
42
|
-
|
|
43
|
-
// MCP config state
|
|
44
|
-
const [mcpConfig, setMcpConfig] = useState<McpConfig | null>(null);
|
|
45
|
-
const [presets, setPresets] = useState<ProviderPresets | null>(null);
|
|
46
|
-
const [mcpSaving, setMcpSaving] = useState(false);
|
|
47
|
-
const [mcpDirty, setMcpDirty] = useState(false);
|
|
48
|
-
const [mcpSaved, setMcpSaved] = useState(false);
|
|
49
|
-
const savedTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
|
50
|
-
|
|
51
|
-
// Form state
|
|
52
|
-
const [newName, setNewName] = useState('');
|
|
53
|
-
const [newBaseUrl, setNewBaseUrl] = useState('');
|
|
54
|
-
const [newApiKey, setNewApiKey] = useState('');
|
|
55
|
-
const [newModel, setNewModel] = useState('');
|
|
56
|
-
const [newProtocol, setNewProtocol] = useState<'anthropic' | 'openai'>('anthropic');
|
|
57
|
-
const [newOpus, setNewOpus] = useState('');
|
|
58
|
-
const [newSonnet, setNewSonnet] = useState('');
|
|
59
|
-
const [newHaiku, setNewHaiku] = useState('');
|
|
60
|
-
const [editingProfile, setEditingProfile] = useState<string | null>(null);
|
|
61
|
-
|
|
62
|
-
// External MCP form state
|
|
63
|
-
const [showAddExternal, setShowAddExternal] = useState(false);
|
|
64
|
-
const [extName, setExtName] = useState('');
|
|
65
|
-
const [extDisplayName, setExtDisplayName] = useState('');
|
|
66
|
-
const [extDescription, setExtDescription] = useState('');
|
|
67
|
-
const [extCommand, setExtCommand] = useState('uvx');
|
|
68
|
-
const [extArgs, setExtArgs] = useState('minimax-coding-plan-mcp,-y');
|
|
69
|
-
const [extProviderRef, setExtProviderRef] = useState('minimax');
|
|
70
|
-
const [extEnabledByDefault, setExtEnabledByDefault] = useState(false);
|
|
71
|
-
|
|
72
|
-
async function loadAll() {
|
|
73
|
-
try {
|
|
74
|
-
const [profs, mcps, status, config, pres, allMcps, externalMcps] = await Promise.all([
|
|
75
|
-
getProfiles(),
|
|
76
|
-
getMcpServers(),
|
|
77
|
-
getStatus(),
|
|
78
|
-
getMcpConfig(),
|
|
79
|
-
getProviderPresets(),
|
|
80
|
-
getAllMcpServers(),
|
|
81
|
-
getExternalMcpServers(),
|
|
82
|
-
]);
|
|
83
|
-
setProfiles(profs);
|
|
84
|
-
setMcpServers(mcps);
|
|
85
|
-
setCurrentProfile(status.currentProfile || '');
|
|
86
|
-
setMcpConfig(config);
|
|
87
|
-
setPresets(pres);
|
|
88
|
-
setAllMcpServers(allMcps);
|
|
89
|
-
setExternalMcpServers(externalMcps);
|
|
90
|
-
setMcpDirty(false);
|
|
91
|
-
setError('');
|
|
92
|
-
} catch (e) {
|
|
93
|
-
setError(e instanceof Error ? e.message : 'Failed to load');
|
|
94
|
-
} finally {
|
|
95
|
-
setLoading(false);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
useEffect(() => { loadAll(); }, []);
|
|
100
|
-
|
|
101
|
-
// Poll connection status every 5s
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
let cancelled = false;
|
|
104
|
-
const poll = async () => {
|
|
105
|
-
const ok = await ping();
|
|
106
|
-
if (!cancelled) setConnected(ok);
|
|
107
|
-
};
|
|
108
|
-
poll();
|
|
109
|
-
const interval = setInterval(poll, 5000);
|
|
110
|
-
return () => { cancelled = true; clearInterval(interval); };
|
|
111
|
-
}, []);
|
|
112
|
-
|
|
113
|
-
function startEditProfile(p: Profile) {
|
|
114
|
-
setEditingProfile(p.name);
|
|
115
|
-
setNewName(p.name);
|
|
116
|
-
setNewBaseUrl(p.baseUrl);
|
|
117
|
-
setNewApiKey('');
|
|
118
|
-
setNewModel(p.model);
|
|
119
|
-
setNewProtocol(p.protocol || 'anthropic');
|
|
120
|
-
setNewOpus(p.opusModel || '');
|
|
121
|
-
setNewSonnet(p.sonnetModel || '');
|
|
122
|
-
setNewHaiku(p.haikuModel || '');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function cancelEdit() {
|
|
126
|
-
setEditingProfile(null);
|
|
127
|
-
setNewName(''); setNewBaseUrl(''); setNewApiKey(''); setNewModel('');
|
|
128
|
-
setNewProtocol('anthropic');
|
|
129
|
-
setNewOpus(''); setNewSonnet(''); setNewHaiku('');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function handleAddProfile() {
|
|
133
|
-
if (!newName || !newBaseUrl || !newModel) return;
|
|
134
|
-
if (editingProfile) {
|
|
135
|
-
// Update
|
|
136
|
-
if (!newBaseUrl || !newModel) return;
|
|
137
|
-
try {
|
|
138
|
-
await updateProfile(editingProfile, {
|
|
139
|
-
baseUrl: newBaseUrl,
|
|
140
|
-
apiKey: newApiKey || undefined,
|
|
141
|
-
model: newModel,
|
|
142
|
-
protocol: newProtocol,
|
|
143
|
-
opusModel: newOpus || undefined,
|
|
144
|
-
sonnetModel: newSonnet || undefined,
|
|
145
|
-
haikuModel: newHaiku || undefined,
|
|
146
|
-
});
|
|
147
|
-
cancelEdit();
|
|
148
|
-
await loadAll();
|
|
149
|
-
} catch (e) {
|
|
150
|
-
setError(e instanceof Error ? e.message : 'Failed to update profile');
|
|
151
|
-
}
|
|
152
|
-
} else {
|
|
153
|
-
// Add
|
|
154
|
-
if (!newApiKey) return;
|
|
155
|
-
try {
|
|
156
|
-
await addProfile({
|
|
157
|
-
name: newName,
|
|
158
|
-
baseUrl: newBaseUrl,
|
|
159
|
-
apiKey: newApiKey,
|
|
160
|
-
model: newModel,
|
|
161
|
-
protocol: newProtocol,
|
|
162
|
-
opusModel: newOpus || undefined,
|
|
163
|
-
sonnetModel: newSonnet || undefined,
|
|
164
|
-
haikuModel: newHaiku || undefined,
|
|
165
|
-
});
|
|
166
|
-
setNewName(''); setNewBaseUrl(''); setNewApiKey(''); setNewModel('');
|
|
167
|
-
setNewProtocol('anthropic');
|
|
168
|
-
setNewOpus(''); setNewSonnet(''); setNewHaiku('');
|
|
169
|
-
await loadAll();
|
|
170
|
-
} catch (e) {
|
|
171
|
-
setError(e instanceof Error ? e.message : 'Failed to add profile');
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function handleDelete(name: string) {
|
|
177
|
-
try { await deleteProfile(name); await loadAll(); }
|
|
178
|
-
catch (e) { setError(e instanceof Error ? e.message : 'Failed to delete'); }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function handleSetDefault(name: string) {
|
|
182
|
-
try { await setDefaultProfile(name); setCurrentProfile(name); await loadAll(); }
|
|
183
|
-
catch (e) { setError(e instanceof Error ? e.message : 'Failed to set default'); }
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function handleToggleMcp(name: string, enabled: boolean, instance?: string) {
|
|
187
|
-
try { await toggleMcp(name, enabled, instance); await loadAll(); }
|
|
188
|
-
catch (e) { setError(e instanceof Error ? e.message : 'Failed to toggle MCP'); }
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function handleAddExternalMcp() {
|
|
192
|
-
if (!extName || !extCommand || !extArgs) return;
|
|
193
|
-
try {
|
|
194
|
-
const server: ExternalMcpServer = {
|
|
195
|
-
name: extName,
|
|
196
|
-
displayName: extDisplayName || extName,
|
|
197
|
-
description: extDescription,
|
|
198
|
-
command: extCommand,
|
|
199
|
-
args: extArgs.split(',').map((s) => s.trim()),
|
|
200
|
-
envVars: extProviderRef
|
|
201
|
-
? { MINIMAX_API_KEY: `\${MCC_PROVIDER_KEY:${extProviderRef}}` }
|
|
202
|
-
: {},
|
|
203
|
-
enabledByDefault: extEnabledByDefault,
|
|
204
|
-
};
|
|
205
|
-
await addExternalMcpServer(server);
|
|
206
|
-
setExtName(''); setExtDisplayName(''); setExtDescription('');
|
|
207
|
-
setExtCommand('uvx'); setExtArgs('minimax-coding-plan-mcp,-y');
|
|
208
|
-
setExtProviderRef('minimax'); setExtEnabledByDefault(false);
|
|
209
|
-
setShowAddExternal(false);
|
|
210
|
-
await loadAll();
|
|
211
|
-
} catch (e) {
|
|
212
|
-
setError(e instanceof Error ? e.message : 'Failed to add external MCP');
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function handleRemoveExternalMcp(name: string) {
|
|
217
|
-
try { await removeExternalMcpServer(name); await loadAll(); }
|
|
218
|
-
catch (e) { setError(e instanceof Error ? e.message : 'Failed to remove external MCP'); }
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// MCP config save — explicit save entry point
|
|
222
|
-
const handleSaveMcpConfig = useCallback(async () => {
|
|
223
|
-
if (!mcpConfig || !mcpDirty) return;
|
|
224
|
-
setMcpSaving(true);
|
|
225
|
-
try {
|
|
226
|
-
await updateMcpConfig(mcpConfig);
|
|
227
|
-
setMcpDirty(false);
|
|
228
|
-
setMcpSaved(true);
|
|
229
|
-
if (savedTimerRef.current) clearTimeout(savedTimerRef.current);
|
|
230
|
-
savedTimerRef.current = setTimeout(() => setMcpSaved(false), 2000);
|
|
231
|
-
} catch (e) {
|
|
232
|
-
setError(e instanceof Error ? e.message : 'Failed to save MCP config');
|
|
233
|
-
} finally {
|
|
234
|
-
setMcpSaving(false);
|
|
235
|
-
}
|
|
236
|
-
}, [mcpConfig, mcpDirty]);
|
|
237
|
-
|
|
238
|
-
function updateWsProvider(id: string, field: string, value: unknown) {
|
|
239
|
-
setMcpConfig(prev => {
|
|
240
|
-
if (!prev) return prev;
|
|
241
|
-
const next = structuredClone(prev);
|
|
242
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
243
|
-
(next.websearch.providers[id] as any)[field] = value;
|
|
244
|
-
return next;
|
|
245
|
-
});
|
|
246
|
-
setMcpDirty(true);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function updateIaProvider(id: string, field: string, value: unknown) {
|
|
250
|
-
setMcpConfig(prev => {
|
|
251
|
-
if (!prev) return prev;
|
|
252
|
-
const next = structuredClone(prev);
|
|
253
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
254
|
-
(next.imageAnalysis.providers[id] as any)[field] = value;
|
|
255
|
-
return next;
|
|
256
|
-
});
|
|
257
|
-
setMcpDirty(true);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function toggleWsEnabled(id: string) {
|
|
261
|
-
setMcpConfig(prev => {
|
|
262
|
-
if (!prev) return prev;
|
|
263
|
-
const next = structuredClone(prev);
|
|
264
|
-
next.websearch.providers[id].enabled = !next.websearch.providers[id].enabled;
|
|
265
|
-
return next;
|
|
266
|
-
});
|
|
267
|
-
setMcpDirty(true);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function toggleIaEnabled(id: string) {
|
|
271
|
-
setMcpConfig(prev => {
|
|
272
|
-
if (!prev) return prev;
|
|
273
|
-
const next = structuredClone(prev);
|
|
274
|
-
next.imageAnalysis.providers[id].enabled = !next.imageAnalysis.providers[id].enabled;
|
|
275
|
-
return next;
|
|
276
|
-
});
|
|
277
|
-
setMcpDirty(true);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function toggleMcpSection(section: 'websearch' | 'imageAnalysis') {
|
|
281
|
-
setMcpConfig(prev => {
|
|
282
|
-
if (!prev) return prev;
|
|
283
|
-
const next = structuredClone(prev);
|
|
284
|
-
next[section].enabled = !next[section].enabled;
|
|
285
|
-
return next;
|
|
286
|
-
});
|
|
287
|
-
setMcpDirty(true);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (loading) {
|
|
291
|
-
return <div className="flex h-screen items-center justify-center text-muted-foreground">Loading...</div>;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return (
|
|
295
|
-
<div className="min-h-screen bg-background p-6">
|
|
296
|
-
<div className="mx-auto max-w-4xl">
|
|
297
|
-
<div className="mb-6 flex items-center justify-between">
|
|
298
|
-
<div>
|
|
299
|
-
<h1 className="text-2xl font-bold">MCC Dashboard</h1>
|
|
300
|
-
<p className="text-sm text-muted-foreground">
|
|
301
|
-
{currentProfile ? `Current profile: ${currentProfile}` : 'No profile selected'}
|
|
302
|
-
</p>
|
|
303
|
-
</div>
|
|
304
|
-
<div className="flex items-center gap-3">
|
|
305
|
-
<span className={`flex items-center gap-1.5 text-xs font-medium ${connected ? 'text-green-600' : 'text-red-500'}`}>
|
|
306
|
-
<span className={`inline-block h-2 w-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`} />
|
|
307
|
-
{connected ? 'Connected' : 'Disconnected'}
|
|
308
|
-
</span>
|
|
309
|
-
<Button variant="outline" onClick={loadAll}>Refresh</Button>
|
|
310
|
-
</div>
|
|
311
|
-
</div>
|
|
312
|
-
|
|
313
|
-
{error && <div className="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-600">{error}</div>}
|
|
314
|
-
|
|
315
|
-
<Tabs defaultValue="profiles">
|
|
316
|
-
<TabsList className="mb-4">
|
|
317
|
-
<TabsTrigger value="profiles">Profiles</TabsTrigger>
|
|
318
|
-
<TabsTrigger value="mcp">MCP</TabsTrigger>
|
|
319
|
-
</TabsList>
|
|
320
|
-
|
|
321
|
-
<TabsContent value="profiles">
|
|
322
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
323
|
-
<Card>
|
|
324
|
-
<CardHeader>
|
|
325
|
-
<CardTitle>Profiles</CardTitle>
|
|
326
|
-
<CardDescription>Your configured profiles</CardDescription>
|
|
327
|
-
</CardHeader>
|
|
328
|
-
<CardContent>
|
|
329
|
-
{profiles.length === 0 ? (
|
|
330
|
-
<p className="text-sm text-muted-foreground">No profiles yet.</p>
|
|
331
|
-
) : (
|
|
332
|
-
<div className="space-y-3">
|
|
333
|
-
{profiles.map((p) => (
|
|
334
|
-
<div key={p.name} className="rounded-lg border p-4">
|
|
335
|
-
{/* Header: name + badges */}
|
|
336
|
-
<div className="flex items-center gap-2 mb-1">
|
|
337
|
-
<span className="font-semibold text-sm">{p.name}</span>
|
|
338
|
-
{p.name === currentProfile && (
|
|
339
|
-
<span className="inline-flex items-center rounded-full bg-emerald-50 px-2 py-0.5 text-[10px] font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20 dark:bg-emerald-950 dark:text-emerald-400 dark:ring-emerald-500/20">
|
|
340
|
-
default
|
|
341
|
-
</span>
|
|
342
|
-
)}
|
|
343
|
-
<span className="inline-flex items-center rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
|
344
|
-
{p.protocol || 'anthropic'}
|
|
345
|
-
</span>
|
|
346
|
-
</div>
|
|
347
|
-
|
|
348
|
-
{/* Base URL */}
|
|
349
|
-
<p className="text-xs text-muted-foreground/60 truncate mb-3 font-mono">{p.baseUrl}</p>
|
|
350
|
-
|
|
351
|
-
{/* Model details — label-value pairs with consistent alignment */}
|
|
352
|
-
<div className="space-y-0.5 mb-3 text-xs">
|
|
353
|
-
<div className="flex gap-3">
|
|
354
|
-
<span className="text-muted-foreground w-14 shrink-0 text-right">default</span>
|
|
355
|
-
<span className="font-mono text-foreground/80 truncate">{p.model}</span>
|
|
356
|
-
</div>
|
|
357
|
-
{p.opusModel && (
|
|
358
|
-
<div className="flex gap-3">
|
|
359
|
-
<span className="text-muted-foreground w-14 shrink-0 text-right">opus</span>
|
|
360
|
-
<span className="font-mono text-foreground/80 truncate">{p.opusModel}</span>
|
|
361
|
-
</div>
|
|
362
|
-
)}
|
|
363
|
-
{p.sonnetModel && (
|
|
364
|
-
<div className="flex gap-3">
|
|
365
|
-
<span className="text-muted-foreground w-14 shrink-0 text-right">sonnet</span>
|
|
366
|
-
<span className="font-mono text-foreground/80 truncate">{p.sonnetModel}</span>
|
|
367
|
-
</div>
|
|
368
|
-
)}
|
|
369
|
-
{p.haikuModel && (
|
|
370
|
-
<div className="flex gap-3">
|
|
371
|
-
<span className="text-muted-foreground w-14 shrink-0 text-right">haiku</span>
|
|
372
|
-
<span className="font-mono text-foreground/80 truncate">{p.haikuModel}</span>
|
|
373
|
-
</div>
|
|
374
|
-
)}
|
|
375
|
-
</div>
|
|
376
|
-
|
|
377
|
-
{/* Actions */}
|
|
378
|
-
<div className="flex items-center gap-1 pt-2 border-t border-border/50">
|
|
379
|
-
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs" onClick={() => startEditProfile(p)}>Edit</Button>
|
|
380
|
-
{p.name !== currentProfile && (
|
|
381
|
-
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs" onClick={() => handleSetDefault(p.name)}>Set default</Button>
|
|
382
|
-
)}
|
|
383
|
-
<span className="flex-1" />
|
|
384
|
-
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs text-destructive hover:text-destructive" onClick={() => handleDelete(p.name)}>Delete</Button>
|
|
385
|
-
</div>
|
|
386
|
-
</div>
|
|
387
|
-
))}
|
|
388
|
-
</div>
|
|
389
|
-
)}
|
|
390
|
-
</CardContent>
|
|
391
|
-
</Card>
|
|
392
|
-
|
|
393
|
-
<Card>
|
|
394
|
-
<CardHeader>
|
|
395
|
-
<CardTitle>{editingProfile ? 'Edit Profile' : 'Add Profile'}</CardTitle>
|
|
396
|
-
<CardDescription>{editingProfile ? `Editing: ${editingProfile}` : 'Configure a new profile'}</CardDescription>
|
|
397
|
-
</CardHeader>
|
|
398
|
-
<CardContent className="space-y-3">
|
|
399
|
-
<div className="space-y-2">
|
|
400
|
-
<Label htmlFor="name">Profile Name</Label>
|
|
401
|
-
<Input id="name" placeholder="e.g. prod" value={newName} onChange={(e) => setNewName(e.target.value)} disabled={!!editingProfile} />
|
|
402
|
-
</div>
|
|
403
|
-
<div className="space-y-2">
|
|
404
|
-
<Label htmlFor="baseUrl">Base URL</Label>
|
|
405
|
-
<Input id="baseUrl" placeholder="https://api.deepseek.com/anthropic" value={newBaseUrl} onChange={(e) => setNewBaseUrl(e.target.value)} />
|
|
406
|
-
</div>
|
|
407
|
-
<div className="space-y-2">
|
|
408
|
-
<Label htmlFor="apiKey">API Key</Label>
|
|
409
|
-
<Input id="apiKey" type="password" placeholder={editingProfile ? '(unchanged if empty)' : 'sk-...'} value={newApiKey} onChange={(e) => setNewApiKey(e.target.value)} />
|
|
410
|
-
</div>
|
|
411
|
-
<div className="space-y-2">
|
|
412
|
-
<Label htmlFor="model">Default Model</Label>
|
|
413
|
-
<Input id="model" placeholder="e.g. deepseek-chat" value={newModel} onChange={(e) => setNewModel(e.target.value)} />
|
|
414
|
-
</div>
|
|
415
|
-
<div className="space-y-2">
|
|
416
|
-
<Label htmlFor="protocol">Protocol</Label>
|
|
417
|
-
<select
|
|
418
|
-
id="protocol"
|
|
419
|
-
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
420
|
-
value={newProtocol}
|
|
421
|
-
onChange={(e) => setNewProtocol(e.target.value as 'anthropic' | 'openai')}
|
|
422
|
-
>
|
|
423
|
-
<option value="anthropic">Anthropic (direct)</option>
|
|
424
|
-
<option value="openai">OpenAI-compatible (translation proxy)</option>
|
|
425
|
-
</select>
|
|
426
|
-
</div>
|
|
427
|
-
<div className="grid grid-cols-3 gap-2">
|
|
428
|
-
<div className="space-y-2">
|
|
429
|
-
<Label htmlFor="opus">Opus</Label>
|
|
430
|
-
<Input id="opus" placeholder="Opus model" value={newOpus} onChange={(e) => setNewOpus(e.target.value)} />
|
|
431
|
-
</div>
|
|
432
|
-
<div className="space-y-2">
|
|
433
|
-
<Label htmlFor="sonnet">Sonnet</Label>
|
|
434
|
-
<Input id="sonnet" placeholder="Sonnet model" value={newSonnet} onChange={(e) => setNewSonnet(e.target.value)} />
|
|
435
|
-
</div>
|
|
436
|
-
<div className="space-y-2">
|
|
437
|
-
<Label htmlFor="haiku">Haiku</Label>
|
|
438
|
-
<Input id="haiku" placeholder="Haiku model" value={newHaiku} onChange={(e) => setNewHaiku(e.target.value)} />
|
|
439
|
-
</div>
|
|
440
|
-
</div>
|
|
441
|
-
<div className="flex gap-2">
|
|
442
|
-
<Button className="flex-1" onClick={handleAddProfile}>{editingProfile ? 'Update Profile' : 'Add Profile'}</Button>
|
|
443
|
-
{editingProfile && <Button variant="outline" onClick={cancelEdit}>Cancel</Button>}
|
|
444
|
-
</div>
|
|
445
|
-
</CardContent>
|
|
446
|
-
</Card>
|
|
447
|
-
</div>
|
|
448
|
-
</TabsContent>
|
|
449
|
-
|
|
450
|
-
<TabsContent value="mcp">
|
|
451
|
-
<div
|
|
452
|
-
className="space-y-4"
|
|
453
|
-
onKeyDown={(e: React.KeyboardEvent) => {
|
|
454
|
-
if (e.key === 'Enter' && mcpDirty && !mcpSaving) {
|
|
455
|
-
const tag = (e.target as HTMLElement).tagName;
|
|
456
|
-
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
457
|
-
e.preventDefault();
|
|
458
|
-
handleSaveMcpConfig();
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}}
|
|
462
|
-
>
|
|
463
|
-
{/* Save bar */}
|
|
464
|
-
{(mcpDirty || mcpSaved) && (
|
|
465
|
-
<div className={`rounded-lg border p-3 flex items-center justify-between ${
|
|
466
|
-
mcpSaved
|
|
467
|
-
? 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950'
|
|
468
|
-
: 'border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950'
|
|
469
|
-
}`}>
|
|
470
|
-
<div>
|
|
471
|
-
{mcpSaved ? (
|
|
472
|
-
<>
|
|
473
|
-
<span className="text-sm text-green-700 dark:text-green-400 font-medium">Saved</span>
|
|
474
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
475
|
-
Changes won't affect running MCC instances. Restart to apply.
|
|
476
|
-
</p>
|
|
477
|
-
</>
|
|
478
|
-
) : (
|
|
479
|
-
<span className="text-sm text-amber-700 dark:text-amber-400">You have unsaved changes</span>
|
|
480
|
-
)}
|
|
481
|
-
</div>
|
|
482
|
-
{mcpDirty && (
|
|
483
|
-
<Button size="sm" onClick={handleSaveMcpConfig} disabled={mcpSaving}>
|
|
484
|
-
{mcpSaving ? 'Saving...' : 'Save'}
|
|
485
|
-
</Button>
|
|
486
|
-
)}
|
|
487
|
-
</div>
|
|
488
|
-
)}
|
|
489
|
-
|
|
490
|
-
{/* WebSearch Section */}
|
|
491
|
-
<Card>
|
|
492
|
-
<CardHeader>
|
|
493
|
-
<div className="flex items-center justify-between">
|
|
494
|
-
<div>
|
|
495
|
-
<CardTitle>WebSearch</CardTitle>
|
|
496
|
-
<CardDescription>Web search providers for Claude Code</CardDescription>
|
|
497
|
-
</div>
|
|
498
|
-
<Switch
|
|
499
|
-
checked={mcpConfig?.websearch.enabled ?? true}
|
|
500
|
-
onCheckedChange={() => toggleMcpSection('websearch')}
|
|
501
|
-
/>
|
|
502
|
-
</div>
|
|
503
|
-
</CardHeader>
|
|
504
|
-
<CardContent>
|
|
505
|
-
{mcpConfig && presets && (
|
|
506
|
-
<div className="space-y-3">
|
|
507
|
-
{Object.entries(presets.websearch).map(([id, preset]) => {
|
|
508
|
-
const provider = mcpConfig.websearch.providers[id];
|
|
509
|
-
if (!provider) return null;
|
|
510
|
-
return (
|
|
511
|
-
<div key={id} className="rounded-lg border p-4">
|
|
512
|
-
<div className="flex items-center justify-between">
|
|
513
|
-
<div>
|
|
514
|
-
<p className="font-medium">{preset.name}</p>
|
|
515
|
-
<p className="text-xs text-muted-foreground">{preset.description}</p>
|
|
516
|
-
</div>
|
|
517
|
-
<Switch
|
|
518
|
-
checked={provider.enabled}
|
|
519
|
-
onCheckedChange={() => toggleWsEnabled(id)}
|
|
520
|
-
disabled={!mcpConfig.websearch.enabled}
|
|
521
|
-
/>
|
|
522
|
-
</div>
|
|
523
|
-
{preset.needsApiKey && provider.enabled && (
|
|
524
|
-
<div className="mt-3">
|
|
525
|
-
<Label className="text-xs">API Key</Label>
|
|
526
|
-
<Input
|
|
527
|
-
type="password"
|
|
528
|
-
placeholder="Enter API key..."
|
|
529
|
-
value={provider.apiKey || ''}
|
|
530
|
-
onChange={(e) => updateWsProvider(id, 'apiKey', e.target.value)}
|
|
531
|
-
className="mt-1"
|
|
532
|
-
/>
|
|
533
|
-
</div>
|
|
534
|
-
)}
|
|
535
|
-
</div>
|
|
536
|
-
);
|
|
537
|
-
})}
|
|
538
|
-
</div>
|
|
539
|
-
)}
|
|
540
|
-
</CardContent>
|
|
541
|
-
</Card>
|
|
542
|
-
|
|
543
|
-
{/* ImageAnalysis Section */}
|
|
544
|
-
<Card>
|
|
545
|
-
<CardHeader>
|
|
546
|
-
<div className="flex items-center justify-between">
|
|
547
|
-
<div>
|
|
548
|
-
<CardTitle>Image Analysis</CardTitle>
|
|
549
|
-
<CardDescription>Vision providers for image/PDF understanding</CardDescription>
|
|
550
|
-
</div>
|
|
551
|
-
<Switch
|
|
552
|
-
checked={mcpConfig?.imageAnalysis.enabled ?? true}
|
|
553
|
-
onCheckedChange={() => toggleMcpSection('imageAnalysis')}
|
|
554
|
-
/>
|
|
555
|
-
</div>
|
|
556
|
-
</CardHeader>
|
|
557
|
-
<CardContent>
|
|
558
|
-
{mcpConfig && presets && (
|
|
559
|
-
<div className="space-y-4">
|
|
560
|
-
{Object.entries(presets.imageAnalysis).map(([id, preset]) => {
|
|
561
|
-
const provider = mcpConfig.imageAnalysis.providers[id];
|
|
562
|
-
if (!provider) return null;
|
|
563
|
-
const datalistId = `ia-model-${id}`;
|
|
564
|
-
return (
|
|
565
|
-
<div key={id} className="rounded-lg border p-4">
|
|
566
|
-
<div className="flex items-center justify-between">
|
|
567
|
-
<div>
|
|
568
|
-
<p className="font-medium">{preset.name}</p>
|
|
569
|
-
<p className="text-xs text-muted-foreground">
|
|
570
|
-
Format: {preset.format}
|
|
571
|
-
</p>
|
|
572
|
-
</div>
|
|
573
|
-
<Switch
|
|
574
|
-
checked={provider.enabled}
|
|
575
|
-
onCheckedChange={() => toggleIaEnabled(id)}
|
|
576
|
-
disabled={!mcpConfig.imageAnalysis.enabled}
|
|
577
|
-
/>
|
|
578
|
-
</div>
|
|
579
|
-
{provider.enabled && (
|
|
580
|
-
<div className="mt-3 space-y-3">
|
|
581
|
-
<div>
|
|
582
|
-
<Label className="text-xs">Endpoint (Base URL)</Label>
|
|
583
|
-
<Input
|
|
584
|
-
placeholder={preset.baseUrl}
|
|
585
|
-
value={provider.baseUrl}
|
|
586
|
-
onChange={(e) => updateIaProvider(id, 'baseUrl', e.target.value)}
|
|
587
|
-
className="mt-1"
|
|
588
|
-
/>
|
|
589
|
-
</div>
|
|
590
|
-
<div>
|
|
591
|
-
<Label className="text-xs">API Key</Label>
|
|
592
|
-
<Input
|
|
593
|
-
type="password"
|
|
594
|
-
placeholder="Enter API key..."
|
|
595
|
-
value={provider.apiKey}
|
|
596
|
-
onChange={(e) => updateIaProvider(id, 'apiKey', e.target.value)}
|
|
597
|
-
className="mt-1"
|
|
598
|
-
/>
|
|
599
|
-
</div>
|
|
600
|
-
<div>
|
|
601
|
-
<Label className="text-xs">Model</Label>
|
|
602
|
-
<Input
|
|
603
|
-
list={datalistId}
|
|
604
|
-
placeholder="Select or type model name..."
|
|
605
|
-
value={provider.model}
|
|
606
|
-
onChange={(e) => updateIaProvider(id, 'model', e.target.value)}
|
|
607
|
-
className="mt-1"
|
|
608
|
-
/>
|
|
609
|
-
<datalist id={datalistId}>
|
|
610
|
-
{preset.models.map((m) => (
|
|
611
|
-
<option key={m} value={m} />
|
|
612
|
-
))}
|
|
613
|
-
</datalist>
|
|
614
|
-
</div>
|
|
615
|
-
</div>
|
|
616
|
-
)}
|
|
617
|
-
</div>
|
|
618
|
-
);
|
|
619
|
-
})}
|
|
620
|
-
</div>
|
|
621
|
-
)}
|
|
622
|
-
</CardContent>
|
|
623
|
-
</Card>
|
|
624
|
-
|
|
625
|
-
{/* MCP Server Status */}
|
|
626
|
-
<Card>
|
|
627
|
-
<CardHeader>
|
|
628
|
-
<CardTitle>MCP Server Status</CardTitle>
|
|
629
|
-
<CardDescription>Running MCP servers for the current session</CardDescription>
|
|
630
|
-
</CardHeader>
|
|
631
|
-
<CardContent>
|
|
632
|
-
<div className="space-y-3">
|
|
633
|
-
{mcpServers.map((server) => (
|
|
634
|
-
<div key={server.name} className="flex items-center justify-between rounded-lg border p-4">
|
|
635
|
-
<div>
|
|
636
|
-
<p className="font-medium">{server.displayName}</p>
|
|
637
|
-
<p className="text-sm text-muted-foreground">{server.description}</p>
|
|
638
|
-
</div>
|
|
639
|
-
<div className="flex items-center gap-3">
|
|
640
|
-
<span className="text-xs text-muted-foreground">{server.enabled ? 'Enabled' : 'Disabled'}</span>
|
|
641
|
-
<Switch checked={server.enabled} onCheckedChange={(checked) => handleToggleMcp(server.name, checked)} />
|
|
642
|
-
</div>
|
|
643
|
-
</div>
|
|
644
|
-
))}
|
|
645
|
-
</div>
|
|
646
|
-
</CardContent>
|
|
647
|
-
</Card>
|
|
648
|
-
|
|
649
|
-
{/* External MCP Servers */}
|
|
650
|
-
<Card>
|
|
651
|
-
<CardHeader>
|
|
652
|
-
<div className="flex items-center justify-between">
|
|
653
|
-
<div>
|
|
654
|
-
<CardTitle>External MCP Servers</CardTitle>
|
|
655
|
-
<CardDescription>User-added MCP servers (e.g. MiniMax Token Plan)</CardDescription>
|
|
656
|
-
</div>
|
|
657
|
-
<Button size="sm" variant="outline" onClick={() => setShowAddExternal(!showAddExternal)}>
|
|
658
|
-
{showAddExternal ? 'Cancel' : 'Add'}
|
|
659
|
-
</Button>
|
|
660
|
-
</div>
|
|
661
|
-
</CardHeader>
|
|
662
|
-
<CardContent>
|
|
663
|
-
{showAddExternal && (
|
|
664
|
-
<div className="mb-4 space-y-3 rounded-lg border p-4">
|
|
665
|
-
<div className="grid grid-cols-2 gap-3">
|
|
666
|
-
<div className="space-y-1">
|
|
667
|
-
<Label className="text-xs">Name (unique ID)</Label>
|
|
668
|
-
<Input placeholder="minimax-plan" value={extName} onChange={(e) => setExtName(e.target.value)} />
|
|
669
|
-
</div>
|
|
670
|
-
<div className="space-y-1">
|
|
671
|
-
<Label className="text-xs">Display Name</Label>
|
|
672
|
-
<Input placeholder="MiniMax Token Plan" value={extDisplayName} onChange={(e) => setExtDisplayName(e.target.value)} />
|
|
673
|
-
</div>
|
|
674
|
-
</div>
|
|
675
|
-
<div className="space-y-1">
|
|
676
|
-
<Label className="text-xs">Description</Label>
|
|
677
|
-
<Input placeholder="Web search and image understanding via MiniMax Token Plan" value={extDescription} onChange={(e) => setExtDescription(e.target.value)} />
|
|
678
|
-
</div>
|
|
679
|
-
<div className="grid grid-cols-2 gap-3">
|
|
680
|
-
<div className="space-y-1">
|
|
681
|
-
<Label className="text-xs">Command</Label>
|
|
682
|
-
<Input placeholder="uvx" value={extCommand} onChange={(e) => setExtCommand(e.target.value)} />
|
|
683
|
-
</div>
|
|
684
|
-
<div className="space-y-1">
|
|
685
|
-
<Label className="text-xs">Args (comma-separated)</Label>
|
|
686
|
-
<Input placeholder="minimax-coding-plan-mcp,-y" value={extArgs} onChange={(e) => setExtArgs(e.target.value)} />
|
|
687
|
-
</div>
|
|
688
|
-
</div>
|
|
689
|
-
<div className="flex items-center gap-4">
|
|
690
|
-
<div className="space-y-1">
|
|
691
|
-
<Label className="text-xs">Provider API Key Source</Label>
|
|
692
|
-
<select
|
|
693
|
-
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
694
|
-
value={extProviderRef}
|
|
695
|
-
onChange={(e) => setExtProviderRef(e.target.value)}
|
|
696
|
-
>
|
|
697
|
-
<option value="minimax">MiniMax (Token Plan)</option>
|
|
698
|
-
<option value="ali">Ali (DashScope)</option>
|
|
699
|
-
<option value="kimi">Kimi</option>
|
|
700
|
-
<option value="deepseek">DeepSeek</option>
|
|
701
|
-
<option value="">None</option>
|
|
702
|
-
</select>
|
|
703
|
-
</div>
|
|
704
|
-
<div className="flex items-center gap-2 pt-5">
|
|
705
|
-
<Switch checked={extEnabledByDefault} onCheckedChange={setExtEnabledByDefault} />
|
|
706
|
-
<span className="text-xs">Enabled by default</span>
|
|
707
|
-
</div>
|
|
708
|
-
</div>
|
|
709
|
-
<Button className="w-full" onClick={handleAddExternalMcp}>Add External MCP</Button>
|
|
710
|
-
</div>
|
|
711
|
-
)}
|
|
712
|
-
<div className="space-y-3">
|
|
713
|
-
{externalMcpServers.length === 0 && !showAddExternal && (
|
|
714
|
-
<p className="text-sm text-muted-foreground">No external MCP servers. Click Add to register one.</p>
|
|
715
|
-
)}
|
|
716
|
-
{externalMcpServers.map((server) => {
|
|
717
|
-
const serverState = allMcpServers.find((s) => s.name === server.name);
|
|
718
|
-
return (
|
|
719
|
-
<div key={server.name} className="flex items-center justify-between rounded-lg border p-4">
|
|
720
|
-
<div>
|
|
721
|
-
<p className="font-medium">{server.displayName}</p>
|
|
722
|
-
<p className="text-sm text-muted-foreground">{server.description}</p>
|
|
723
|
-
<p className="text-xs text-muted-foreground">
|
|
724
|
-
{server.command} {server.args.join(' ')}
|
|
725
|
-
</p>
|
|
726
|
-
</div>
|
|
727
|
-
<div className="flex items-center gap-3">
|
|
728
|
-
{serverState && (
|
|
729
|
-
<span className="text-xs text-muted-foreground">
|
|
730
|
-
{serverState.enabled ? 'Enabled' : 'Disabled'}
|
|
731
|
-
</span>
|
|
732
|
-
)}
|
|
733
|
-
<Switch
|
|
734
|
-
checked={serverState?.enabled ?? false}
|
|
735
|
-
onCheckedChange={(checked) => handleToggleMcp(server.name, checked, currentProfile)}
|
|
736
|
-
/>
|
|
737
|
-
<Button size="sm" variant="destructive" onClick={() => handleRemoveExternalMcp(server.name)}>
|
|
738
|
-
Delete
|
|
739
|
-
</Button>
|
|
740
|
-
</div>
|
|
741
|
-
</div>
|
|
742
|
-
);
|
|
743
|
-
})}
|
|
744
|
-
</div>
|
|
745
|
-
</CardContent>
|
|
746
|
-
</Card>
|
|
747
|
-
</div>
|
|
748
|
-
</TabsContent>
|
|
749
|
-
</Tabs>
|
|
750
|
-
</div>
|
|
751
|
-
</div>
|
|
752
|
-
);
|
|
753
|
-
}
|