@geminilight/mindos 0.5.19 → 0.5.21
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/app/app/api/ask/route.ts +308 -172
- package/app/app/api/file/route.ts +35 -11
- package/app/app/api/skills/route.ts +22 -3
- package/app/components/SettingsModal.tsx +52 -58
- package/app/components/Sidebar.tsx +21 -1
- package/app/components/settings/AiTab.tsx +4 -25
- package/app/components/settings/AppearanceTab.tsx +31 -13
- package/app/components/settings/KnowledgeTab.tsx +13 -28
- package/app/components/settings/McpAgentInstall.tsx +227 -0
- package/app/components/settings/McpServerStatus.tsx +172 -0
- package/app/components/settings/McpSkillsSection.tsx +583 -0
- package/app/components/settings/McpTab.tsx +16 -728
- package/app/components/settings/PluginsTab.tsx +4 -27
- package/app/components/settings/Primitives.tsx +69 -0
- package/app/components/settings/ShortcutsTab.tsx +2 -4
- package/app/components/settings/SyncTab.tsx +8 -24
- package/app/components/settings/types.ts +116 -2
- package/app/lib/agent/context.ts +151 -87
- package/app/lib/agent/index.ts +4 -3
- package/app/lib/agent/model.ts +76 -10
- package/app/lib/agent/stream-consumer.ts +73 -77
- package/app/lib/agent/to-agent-messages.ts +106 -0
- package/app/lib/agent/tools.ts +260 -266
- package/app/lib/i18n-en.ts +480 -0
- package/app/lib/i18n-zh.ts +505 -0
- package/app/lib/i18n.ts +4 -947
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +7 -0
- package/app/package-lock.json +3258 -3093
- package/app/package.json +6 -3
- package/bin/cli.js +140 -5
- package/package.json +4 -1
- package/scripts/setup.js +13 -0
- package/skills/mindos/SKILL.md +10 -168
- package/skills/mindos-zh/SKILL.md +14 -172
- package/templates/skill-rules/en/skill-rules.md +222 -0
- package/templates/skill-rules/en/user-rules.md +20 -0
- package/templates/skill-rules/zh/skill-rules.md +222 -0
- package/templates/skill-rules/zh/user-rules.md +20 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
|
|
5
|
+
import { apiFetch } from '@/lib/api';
|
|
6
|
+
import type { AgentInfo, McpAgentInstallProps } from './types';
|
|
7
|
+
|
|
8
|
+
/* ── Agent Install ─────────────────────────────────────────────── */
|
|
9
|
+
|
|
10
|
+
export default function AgentInstall({ agents, t, onRefresh }: McpAgentInstallProps) {
|
|
11
|
+
const m = t.settings?.mcp;
|
|
12
|
+
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
13
|
+
const [transport, setTransport] = useState<'auto' | 'stdio' | 'http'>('auto');
|
|
14
|
+
const [httpUrl, setHttpUrl] = useState('http://localhost:8787/mcp');
|
|
15
|
+
const [httpToken, setHttpToken] = useState('');
|
|
16
|
+
const [scopes, setScopes] = useState<Record<string, 'project' | 'global'>>({});
|
|
17
|
+
const [installing, setInstalling] = useState(false);
|
|
18
|
+
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
19
|
+
|
|
20
|
+
const getEffectiveTransport = (agent: AgentInfo) => {
|
|
21
|
+
if (transport === 'auto') return agent.preferredTransport;
|
|
22
|
+
return transport;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const toggle = (key: string) => {
|
|
26
|
+
setSelected(prev => {
|
|
27
|
+
const next = new Set(prev);
|
|
28
|
+
if (next.has(key)) next.delete(key); else next.add(key);
|
|
29
|
+
return next;
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleInstall = async () => {
|
|
34
|
+
if (selected.size === 0) return;
|
|
35
|
+
setInstalling(true);
|
|
36
|
+
setMessage(null);
|
|
37
|
+
try {
|
|
38
|
+
const payload = {
|
|
39
|
+
agents: [...selected].map(key => {
|
|
40
|
+
const agent = agents.find(a => a.key === key);
|
|
41
|
+
const effectiveTransport = transport === 'auto'
|
|
42
|
+
? (agent?.preferredTransport || 'stdio')
|
|
43
|
+
: transport;
|
|
44
|
+
return {
|
|
45
|
+
key,
|
|
46
|
+
scope: scopes[key] || (agent?.hasProjectScope ? 'project' : 'global'),
|
|
47
|
+
transport: effectiveTransport,
|
|
48
|
+
};
|
|
49
|
+
}),
|
|
50
|
+
transport,
|
|
51
|
+
...(transport === 'http' ? { url: httpUrl, token: httpToken } : {}),
|
|
52
|
+
// For auto mode, pass http settings for agents that need it
|
|
53
|
+
...(transport === 'auto' ? { url: httpUrl, token: httpToken } : {}),
|
|
54
|
+
};
|
|
55
|
+
const res = await apiFetch<{ results: Array<{ agent: string; status: string; message?: string }> }>('/api/mcp/install', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify(payload),
|
|
59
|
+
});
|
|
60
|
+
const ok = res.results.filter(r => r.status === 'ok').length;
|
|
61
|
+
const fail = res.results.filter(r => r.status === 'error');
|
|
62
|
+
if (fail.length > 0) {
|
|
63
|
+
setMessage({ type: 'error', text: fail.map(f => `${f.agent}: ${f.message}`).join('; ') });
|
|
64
|
+
} else {
|
|
65
|
+
setMessage({ type: 'success', text: m?.installSuccess ? m.installSuccess(ok) : `${ok} agent(s) configured` });
|
|
66
|
+
}
|
|
67
|
+
setSelected(new Set());
|
|
68
|
+
onRefresh();
|
|
69
|
+
} catch {
|
|
70
|
+
setMessage({ type: 'error', text: m?.installFailed ?? 'Install failed' });
|
|
71
|
+
} finally {
|
|
72
|
+
setInstalling(false);
|
|
73
|
+
setTimeout(() => setMessage(null), 4000);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Show http fields if transport is 'http', or 'auto' with any http-preferred agent selected
|
|
78
|
+
const showHttpFields = transport === 'http' || (transport === 'auto' && [...selected].some(key => {
|
|
79
|
+
const agent = agents.find(a => a.key === key);
|
|
80
|
+
return agent?.preferredTransport === 'http';
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="space-y-3 pt-2">
|
|
85
|
+
{/* Agent list */}
|
|
86
|
+
<div className="space-y-1">
|
|
87
|
+
{agents.map(agent => (
|
|
88
|
+
<div key={agent.key} className="flex items-center gap-3 py-1.5 text-sm">
|
|
89
|
+
<input
|
|
90
|
+
type="checkbox"
|
|
91
|
+
checked={selected.has(agent.key)}
|
|
92
|
+
onChange={() => toggle(agent.key)}
|
|
93
|
+
className="rounded border-border"
|
|
94
|
+
style={{ accentColor: 'var(--amber)' }}
|
|
95
|
+
/>
|
|
96
|
+
<span className="w-28 shrink-0 text-xs">{agent.name}</span>
|
|
97
|
+
<span className="text-2xs px-1.5 py-0.5 rounded font-mono"
|
|
98
|
+
style={{ background: 'rgba(100,100,120,0.08)' }}>
|
|
99
|
+
{getEffectiveTransport(agent)}
|
|
100
|
+
</span>
|
|
101
|
+
{agent.installed ? (
|
|
102
|
+
<>
|
|
103
|
+
<span className="text-2xs px-1.5 py-0.5 rounded bg-success/15 text-success font-mono">
|
|
104
|
+
{agent.transport}
|
|
105
|
+
</span>
|
|
106
|
+
<span className="text-2xs text-muted-foreground">{agent.scope}</span>
|
|
107
|
+
</>
|
|
108
|
+
) : (
|
|
109
|
+
<span className="text-2xs text-muted-foreground">
|
|
110
|
+
{agent.present ? (m?.detected ?? 'Detected') : (m?.notFound ?? 'Not found')}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
{/* Scope selector */}
|
|
114
|
+
{selected.has(agent.key) && agent.hasProjectScope && agent.hasGlobalScope && (
|
|
115
|
+
<select
|
|
116
|
+
value={scopes[agent.key] || 'project'}
|
|
117
|
+
onChange={e => setScopes({ ...scopes, [agent.key]: e.target.value as 'project' | 'global' })}
|
|
118
|
+
className="ml-auto text-2xs px-1.5 py-0.5 rounded border border-border bg-background text-foreground"
|
|
119
|
+
>
|
|
120
|
+
<option value="project">{m?.project ?? 'Project'}</option>
|
|
121
|
+
<option value="global">{m?.global ?? 'Global'}</option>
|
|
122
|
+
</select>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Select detected / Clear buttons */}
|
|
129
|
+
<div className="flex gap-2 text-xs pt-1">
|
|
130
|
+
<button type="button"
|
|
131
|
+
onClick={() => setSelected(new Set(
|
|
132
|
+
agents.filter(a => !a.installed && a.present).map(a => a.key)
|
|
133
|
+
))}
|
|
134
|
+
className="px-2.5 py-1 rounded-md border transition-colors hover:bg-muted/50"
|
|
135
|
+
style={{ borderColor: 'var(--amber)', color: 'var(--amber)' }}>
|
|
136
|
+
{m?.selectDetected ?? 'Select Detected'}
|
|
137
|
+
</button>
|
|
138
|
+
<button type="button"
|
|
139
|
+
onClick={() => setSelected(new Set())}
|
|
140
|
+
className="px-2.5 py-1 rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground">
|
|
141
|
+
{m?.clearSelection ?? 'Clear'}
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Transport selector */}
|
|
146
|
+
<div className="flex items-center gap-4 text-xs pt-1">
|
|
147
|
+
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
148
|
+
<input
|
|
149
|
+
type="radio"
|
|
150
|
+
name="transport"
|
|
151
|
+
checked={transport === 'auto'}
|
|
152
|
+
onChange={() => setTransport('auto')}
|
|
153
|
+
style={{ accentColor: 'var(--amber)' }}
|
|
154
|
+
/>
|
|
155
|
+
{m?.transportAuto ?? 'auto (recommended)'}
|
|
156
|
+
</label>
|
|
157
|
+
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
158
|
+
<input
|
|
159
|
+
type="radio"
|
|
160
|
+
name="transport"
|
|
161
|
+
checked={transport === 'stdio'}
|
|
162
|
+
onChange={() => setTransport('stdio')}
|
|
163
|
+
style={{ accentColor: 'var(--amber)' }}
|
|
164
|
+
/>
|
|
165
|
+
{m?.transportStdio ?? 'stdio'}
|
|
166
|
+
</label>
|
|
167
|
+
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
168
|
+
<input
|
|
169
|
+
type="radio"
|
|
170
|
+
name="transport"
|
|
171
|
+
checked={transport === 'http'}
|
|
172
|
+
onChange={() => setTransport('http')}
|
|
173
|
+
style={{ accentColor: 'var(--amber)' }}
|
|
174
|
+
/>
|
|
175
|
+
{m?.transportHttp ?? 'http'}
|
|
176
|
+
</label>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* HTTP settings */}
|
|
180
|
+
{showHttpFields && (
|
|
181
|
+
<div className="space-y-2 pl-5 text-xs">
|
|
182
|
+
<div className="space-y-1">
|
|
183
|
+
<label className="text-muted-foreground">{m?.httpUrl ?? 'MCP URL'}</label>
|
|
184
|
+
<input
|
|
185
|
+
type="text"
|
|
186
|
+
value={httpUrl}
|
|
187
|
+
onChange={e => setHttpUrl(e.target.value)}
|
|
188
|
+
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="space-y-1">
|
|
192
|
+
<label className="text-muted-foreground">{m?.httpToken ?? 'Auth Token'}</label>
|
|
193
|
+
<input
|
|
194
|
+
type="password"
|
|
195
|
+
value={httpToken}
|
|
196
|
+
onChange={e => setHttpToken(e.target.value)}
|
|
197
|
+
placeholder="Bearer token"
|
|
198
|
+
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{/* Install button */}
|
|
205
|
+
<button
|
|
206
|
+
onClick={handleInstall}
|
|
207
|
+
disabled={selected.size === 0 || installing}
|
|
208
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
209
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
210
|
+
>
|
|
211
|
+
{installing && <Loader2 size={12} className="animate-spin" />}
|
|
212
|
+
{installing ? (m?.installing ?? 'Installing...') : (m?.installSelected ?? 'Install Selected')}
|
|
213
|
+
</button>
|
|
214
|
+
|
|
215
|
+
{/* Message */}
|
|
216
|
+
{message && (
|
|
217
|
+
<div className="flex items-center gap-1.5 text-xs" role="status">
|
|
218
|
+
{message.type === 'success' ? (
|
|
219
|
+
<><CheckCircle2 size={12} className="text-success" /><span className="text-success">{message.text}</span></>
|
|
220
|
+
) : (
|
|
221
|
+
<><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{message.text}</span></>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import { Plug, Copy, Check, ChevronDown } from 'lucide-react';
|
|
5
|
+
import type { McpStatus, AgentInfo, McpServerStatusProps } from './types';
|
|
6
|
+
|
|
7
|
+
/* ── Helpers ───────────────────────────────────────────────────── */
|
|
8
|
+
|
|
9
|
+
function CopyButton({ text, label, copiedLabel }: { text: string; label: string; copiedLabel?: string }) {
|
|
10
|
+
const [copied, setCopied] = useState(false);
|
|
11
|
+
const handleCopy = async () => {
|
|
12
|
+
try {
|
|
13
|
+
await navigator.clipboard.writeText(text);
|
|
14
|
+
setCopied(true);
|
|
15
|
+
setTimeout(() => setCopied(false), 2000);
|
|
16
|
+
} catch { /* clipboard unavailable */ }
|
|
17
|
+
};
|
|
18
|
+
return (
|
|
19
|
+
<button
|
|
20
|
+
onClick={handleCopy}
|
|
21
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
22
|
+
>
|
|
23
|
+
{copied ? <Check size={11} /> : <Copy size={11} />}
|
|
24
|
+
{copied ? (copiedLabel ?? 'Copied!') : label}
|
|
25
|
+
</button>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ── Config Snippet Generator ─────────────────────────────────── */
|
|
30
|
+
|
|
31
|
+
function generateConfigSnippet(
|
|
32
|
+
agent: AgentInfo,
|
|
33
|
+
status: McpStatus,
|
|
34
|
+
token?: string,
|
|
35
|
+
): { snippet: string; path: string } {
|
|
36
|
+
const isRunning = status.running;
|
|
37
|
+
|
|
38
|
+
// Determine entry (stdio vs http)
|
|
39
|
+
const stdioEntry: Record<string, unknown> = { type: 'stdio', command: 'mindos', args: ['mcp'] };
|
|
40
|
+
const httpEntry: Record<string, unknown> = { url: status.endpoint };
|
|
41
|
+
if (token) httpEntry.headers = { Authorization: `Bearer ${token}` };
|
|
42
|
+
const entry = isRunning ? httpEntry : stdioEntry;
|
|
43
|
+
|
|
44
|
+
// TOML format (Codex)
|
|
45
|
+
if (agent.format === 'toml') {
|
|
46
|
+
const lines: string[] = [`[${agent.configKey}.mindos]`];
|
|
47
|
+
if (isRunning) {
|
|
48
|
+
lines.push(`type = "http"`);
|
|
49
|
+
lines.push(`url = "${status.endpoint}"`);
|
|
50
|
+
if (token) {
|
|
51
|
+
lines.push('');
|
|
52
|
+
lines.push(`[${agent.configKey}.mindos.headers]`);
|
|
53
|
+
lines.push(`Authorization = "Bearer ${token}"`);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
lines.push(`command = "mindos"`);
|
|
57
|
+
lines.push(`args = ["mcp"]`);
|
|
58
|
+
lines.push('');
|
|
59
|
+
lines.push(`[${agent.configKey}.mindos.env]`);
|
|
60
|
+
lines.push(`MCP_TRANSPORT = "stdio"`);
|
|
61
|
+
}
|
|
62
|
+
return { snippet: lines.join('\n'), path: agent.globalPath };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// JSON with globalNestedKey (VS Code project-level uses flat key)
|
|
66
|
+
if (agent.globalNestedKey) {
|
|
67
|
+
// project-level: flat key structure
|
|
68
|
+
const projectSnippet = JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
|
|
69
|
+
return { snippet: projectSnippet, path: agent.projectPath ?? agent.globalPath };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Standard JSON
|
|
73
|
+
const snippet = JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
|
|
74
|
+
return { snippet, path: agent.globalPath };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ── MCP Server Status ─────────────────────────────────────────── */
|
|
78
|
+
|
|
79
|
+
export default function ServerStatus({ status, agents, t }: McpServerStatusProps) {
|
|
80
|
+
const m = t.settings?.mcp;
|
|
81
|
+
const [expanded, setExpanded] = useState(false);
|
|
82
|
+
const [selectedAgent, setSelectedAgent] = useState<string>('');
|
|
83
|
+
|
|
84
|
+
// Auto-select first installed or first detected agent
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (agents.length > 0 && !selectedAgent) {
|
|
87
|
+
const first = agents.find(a => a.installed) ?? agents.find(a => a.present) ?? agents[0];
|
|
88
|
+
if (first) setSelectedAgent(first.key);
|
|
89
|
+
}
|
|
90
|
+
}, [agents, selectedAgent]);
|
|
91
|
+
|
|
92
|
+
if (!status) return null;
|
|
93
|
+
|
|
94
|
+
const currentAgent = agents.find(a => a.key === selectedAgent);
|
|
95
|
+
// 🟡 MINOR #9: Memoize snippet generation to avoid recomputing on every render
|
|
96
|
+
const snippetResult = useMemo(() => currentAgent ? generateConfigSnippet(currentAgent, status) : null, [currentAgent, status]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div>
|
|
100
|
+
{/* Summary line — always visible */}
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={() => setExpanded(!expanded)}
|
|
104
|
+
className="w-full flex items-center gap-2.5 text-xs"
|
|
105
|
+
>
|
|
106
|
+
<Plug size={14} className="text-muted-foreground shrink-0" />
|
|
107
|
+
<span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${status.running ? 'bg-success' : 'bg-muted-foreground'}`} />
|
|
108
|
+
<span className="text-foreground font-medium">
|
|
109
|
+
{status.running ? (m?.running ?? 'Running') : (m?.stopped ?? 'Stopped')}
|
|
110
|
+
</span>
|
|
111
|
+
{status.running && (
|
|
112
|
+
<>
|
|
113
|
+
<span className="text-muted-foreground">·</span>
|
|
114
|
+
<span className="font-mono text-muted-foreground">{status.transport.toUpperCase()}</span>
|
|
115
|
+
<span className="text-muted-foreground">·</span>
|
|
116
|
+
<span className="text-muted-foreground">{m?.toolsRegistered ? m.toolsRegistered(status.toolCount) : `${status.toolCount} tools`}</span>
|
|
117
|
+
<span className="text-muted-foreground">·</span>
|
|
118
|
+
<span className={status.authConfigured ? 'text-success' : 'text-muted-foreground'}>
|
|
119
|
+
{status.authConfigured ? (m?.authSet ?? 'Token set') : (m?.authNotSet ?? 'No token')}
|
|
120
|
+
</span>
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
<ChevronDown size={12} className={`ml-auto text-muted-foreground transition-transform shrink-0 ${expanded ? 'rotate-180' : ''}`} />
|
|
124
|
+
</button>
|
|
125
|
+
|
|
126
|
+
{/* Expanded details */}
|
|
127
|
+
{expanded && (
|
|
128
|
+
<div className="pt-3 mt-3 border-t border-border space-y-3">
|
|
129
|
+
{/* Endpoint + copy */}
|
|
130
|
+
<div className="flex items-center gap-2 text-xs">
|
|
131
|
+
<span className="text-muted-foreground shrink-0">{m?.endpoint ?? 'Endpoint'}</span>
|
|
132
|
+
<span className="font-mono text-foreground truncate">{status.endpoint}</span>
|
|
133
|
+
<CopyButton text={status.endpoint} label={m?.copyEndpoint ?? 'Copy'} copiedLabel={m?.copied} />
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Quick Setup */}
|
|
137
|
+
{agents.length > 0 && (
|
|
138
|
+
<div className="space-y-2.5">
|
|
139
|
+
<div className="flex items-center gap-2 text-xs">
|
|
140
|
+
<span className="text-muted-foreground shrink-0">{m?.configureFor ?? 'Configure for'}</span>
|
|
141
|
+
<select
|
|
142
|
+
value={selectedAgent}
|
|
143
|
+
onChange={e => setSelectedAgent(e.target.value)}
|
|
144
|
+
className="text-xs px-2 py-1 rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
145
|
+
>
|
|
146
|
+
{agents.map(a => (
|
|
147
|
+
<option key={a.key} value={a.key}>
|
|
148
|
+
{a.name}{a.installed ? ' ✓' : a.present ? ' ·' : ''}
|
|
149
|
+
</option>
|
|
150
|
+
))}
|
|
151
|
+
</select>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{snippetResult && (
|
|
155
|
+
<>
|
|
156
|
+
<div className="flex items-center gap-2 text-xs">
|
|
157
|
+
<span className="text-muted-foreground shrink-0">{m?.configPath ?? 'Config path'}</span>
|
|
158
|
+
<span className="font-mono text-foreground text-2xs">{snippetResult.path}</span>
|
|
159
|
+
</div>
|
|
160
|
+
<pre className="text-xs font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre">
|
|
161
|
+
{snippetResult.snippet}
|
|
162
|
+
</pre>
|
|
163
|
+
<CopyButton text={snippetResult.snippet} label={m?.copyConfig ?? 'Copy Config'} copiedLabel={m?.copied} />
|
|
164
|
+
</>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|