@geminilight/mindos 0.5.28 → 0.5.30

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 (34) hide show
  1. package/app/app/api/update/route.ts +41 -0
  2. package/app/app/explore/page.tsx +12 -0
  3. package/app/components/ActivityBar.tsx +14 -7
  4. package/app/components/GuideCard.tsx +21 -7
  5. package/app/components/HomeContent.tsx +31 -97
  6. package/app/components/KeyboardShortcuts.tsx +102 -0
  7. package/app/components/Panel.tsx +12 -7
  8. package/app/components/SidebarLayout.tsx +21 -1
  9. package/app/components/UpdateBanner.tsx +19 -21
  10. package/app/components/explore/ExploreContent.tsx +100 -0
  11. package/app/components/explore/UseCaseCard.tsx +50 -0
  12. package/app/components/explore/use-cases.ts +30 -0
  13. package/app/components/panels/AgentsPanel.tsx +268 -131
  14. package/app/components/panels/PluginsPanel.tsx +87 -27
  15. package/app/components/settings/AiTab.tsx +5 -3
  16. package/app/components/settings/McpSkillsSection.tsx +12 -0
  17. package/app/components/settings/McpTab.tsx +28 -30
  18. package/app/components/settings/SettingsContent.tsx +5 -2
  19. package/app/components/settings/UpdateTab.tsx +195 -0
  20. package/app/components/settings/types.ts +1 -1
  21. package/app/components/walkthrough/WalkthroughOverlay.tsx +224 -0
  22. package/app/components/walkthrough/WalkthroughProvider.tsx +133 -0
  23. package/app/components/walkthrough/WalkthroughTooltip.tsx +129 -0
  24. package/app/components/walkthrough/index.ts +3 -0
  25. package/app/components/walkthrough/steps.ts +21 -0
  26. package/app/hooks/useMcpData.tsx +166 -0
  27. package/app/lib/i18n-en.ts +182 -5
  28. package/app/lib/i18n-zh.ts +181 -4
  29. package/app/lib/mcp-snippets.ts +103 -0
  30. package/app/lib/settings.ts +4 -0
  31. package/app/next-env.d.ts +1 -1
  32. package/app/package.json +1 -0
  33. package/package.json +1 -1
  34. package/app/components/settings/McpServerStatus.tsx +0 -274
@@ -1,230 +1,367 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useCallback, useRef } from 'react';
4
- import { Loader2, RefreshCw, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
5
- import { apiFetch } from '@/lib/api';
6
- import type { McpStatus, AgentInfo } from '../settings/types';
3
+ import { useState, useCallback, useMemo } from 'react';
4
+ import { Loader2, RefreshCw, ChevronDown, ChevronRight, CheckCircle2, AlertCircle, Copy, Check, Monitor, Globe, Settings } from 'lucide-react';
5
+ import { useMcpData } from '@/hooks/useMcpData';
6
+ import { useLocale } from '@/lib/LocaleContext';
7
+ import { generateSnippet } from '@/lib/mcp-snippets';
8
+ import { copyToClipboard } from '@/lib/clipboard';
9
+ import { Toggle } from '../settings/Primitives';
10
+ import type { AgentInfo, McpStatus, SkillInfo } from '../settings/types';
7
11
  import PanelHeader from './PanelHeader';
8
12
 
9
13
  interface AgentsPanelProps {
10
14
  active: boolean;
11
15
  maximized?: boolean;
12
16
  onMaximize?: () => void;
13
- /** Opens Settings Modal on a specific tab */
14
- onOpenSettings?: (tab: 'mcp') => void;
15
17
  }
16
18
 
17
- export default function AgentsPanel({ active, maximized, onMaximize, onOpenSettings }: AgentsPanelProps) {
18
- const [agents, setAgents] = useState<AgentInfo[]>([]);
19
- const [mcpStatus, setMcpStatus] = useState<McpStatus | null>(null);
20
- const [loading, setLoading] = useState(true);
21
- const [error, setError] = useState(false);
19
+ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPanelProps) {
20
+ const { t } = useLocale();
21
+ const p = t.panels.agents;
22
+ const mcp = useMcpData();
22
23
  const [refreshing, setRefreshing] = useState(false);
23
24
  const [showNotDetected, setShowNotDetected] = useState(false);
24
- const intervalRef = useRef<ReturnType<typeof setInterval>>(undefined);
25
-
26
- const fetchAll = useCallback(async (silent = false) => {
27
- if (!silent) setError(false);
28
- try {
29
- const [statusData, agentsData] = await Promise.all([
30
- apiFetch<McpStatus>('/api/mcp/status'),
31
- apiFetch<{ agents: AgentInfo[] }>('/api/mcp/agents'),
32
- ]);
33
- setMcpStatus(statusData);
34
- setAgents(agentsData.agents);
35
- setError(false);
36
- } catch {
37
- if (!silent) setError(true);
38
- }
39
- setLoading(false);
25
+ const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
26
+ const [showBuiltinSkills, setShowBuiltinSkills] = useState(false);
27
+
28
+ const handleRefresh = async () => {
29
+ setRefreshing(true);
30
+ await mcp.refresh();
40
31
  setRefreshing(false);
41
- }, []);
32
+ };
42
33
 
43
- // Fetch when panel becomes active + 30s auto-refresh
44
- const prevActive = useRef(false);
45
- useEffect(() => {
46
- if (active && !prevActive.current) {
47
- fetchAll();
48
- }
49
- prevActive.current = active;
50
- }, [active, fetchAll]);
34
+ const toggleAgent = (key: string) => {
35
+ setExpandedAgent(prev => prev === key ? null : key);
36
+ };
51
37
 
52
- useEffect(() => {
53
- if (!active) {
54
- clearInterval(intervalRef.current);
55
- return;
56
- }
57
- intervalRef.current = setInterval(() => fetchAll(true), 30_000);
58
- return () => clearInterval(intervalRef.current);
59
- }, [active, fetchAll]);
38
+ const connected = mcp.agents.filter(a => a.present && a.installed);
39
+ const detected = mcp.agents.filter(a => a.present && !a.installed);
40
+ const notFound = mcp.agents.filter(a => !a.present);
60
41
 
61
- const handleRefresh = () => {
62
- setRefreshing(true);
63
- fetchAll();
64
- };
42
+ const customSkills = mcp.skills.filter(s => s.source === 'user');
43
+ const builtinSkills = mcp.skills.filter(s => s.source === 'builtin');
44
+ const activeSkillCount = mcp.skills.filter(s => s.enabled).length;
65
45
 
66
- // Group agents
67
- const connected = agents.filter(a => a.present && a.installed);
68
- const detected = agents.filter(a => a.present && !a.installed);
69
- const notFound = agents.filter(a => !a.present);
70
- const connectedCount = connected.length;
46
+ const openAdvancedConfig = () => {
47
+ window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'mcp' } }));
48
+ };
71
49
 
72
50
  return (
73
51
  <div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
74
- <PanelHeader title="Agents" maximized={maximized} onMaximize={onMaximize}>
52
+ <PanelHeader title={p.title} maximized={maximized} onMaximize={onMaximize}>
75
53
  <div className="flex items-center gap-1.5">
76
- {!loading && (
77
- <span className="text-2xs text-muted-foreground">{connectedCount} connected</span>
54
+ {!mcp.loading && (
55
+ <span className="text-2xs text-muted-foreground">{connected.length} {p.connected}</span>
78
56
  )}
79
- <button
80
- onClick={handleRefresh}
81
- disabled={refreshing}
57
+ <button onClick={handleRefresh} disabled={refreshing}
82
58
  className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground disabled:opacity-50 transition-colors"
83
- aria-label="Refresh"
84
- title="Refresh agent status"
85
- >
59
+ aria-label={p.refresh} title={p.refresh}>
86
60
  <RefreshCw size={12} className={refreshing ? 'animate-spin' : ''} />
87
61
  </button>
88
62
  </div>
89
63
  </PanelHeader>
90
64
 
91
65
  <div className="flex-1 overflow-y-auto min-h-0">
92
- {loading ? (
93
- <div className="flex justify-center py-8">
94
- <Loader2 size={16} className="animate-spin text-muted-foreground" />
95
- </div>
96
- ) : error && agents.length === 0 ? (
66
+ {mcp.loading ? (
67
+ <div className="flex justify-center py-8"><Loader2 size={16} className="animate-spin text-muted-foreground" /></div>
68
+ ) : mcp.agents.length === 0 && mcp.skills.length === 0 ? (
97
69
  <div className="flex flex-col items-center gap-2 py-8 text-center px-4">
98
- <p className="text-xs text-destructive">Failed to load agents</p>
99
- <button
100
- onClick={handleRefresh}
101
- className="flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
102
- >
103
- <RefreshCw size={11} /> Retry
70
+ <p className="text-xs text-muted-foreground">{p.noAgents}</p>
71
+ <button onClick={handleRefresh}
72
+ className="flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
73
+ <RefreshCw size={11} /> {p.retry}
104
74
  </button>
105
75
  </div>
106
76
  ) : (
107
77
  <div className="px-3 py-3 space-y-4">
108
- {/* MCP Server Statuscompact */}
78
+ {/* MCP Server statussingle line */}
109
79
  <div className="rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between">
110
- <span className="text-xs font-medium text-foreground">MCP Server</span>
111
- {mcpStatus?.running ? (
80
+ <span className="text-xs font-medium text-foreground">{p.mcpServer}</span>
81
+ {mcp.status?.running ? (
112
82
  <span className="flex items-center gap-1.5 text-[11px]">
113
83
  <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 inline-block" />
114
- <span className="text-emerald-600 dark:text-emerald-400">:{mcpStatus.port}</span>
84
+ <span className="text-emerald-600 dark:text-emerald-400">:{mcp.status.port}</span>
85
+ <span className="text-muted-foreground">· {mcp.status.toolCount} tools</span>
115
86
  </span>
116
87
  ) : (
117
88
  <span className="flex items-center gap-1.5 text-[11px]">
118
89
  <span className="w-1.5 h-1.5 rounded-full bg-zinc-400 inline-block" />
119
- <span className="text-muted-foreground">Stopped</span>
90
+ <span className="text-muted-foreground">{p.stopped}</span>
120
91
  </span>
121
92
  )}
122
93
  </div>
123
94
 
124
- {/* Connected */}
95
+ {/* Connected Agents */}
125
96
  {connected.length > 0 && (
126
97
  <section>
127
- <h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">
128
- Connected ({connected.length})
129
- </h3>
98
+ <h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionConnected} ({connected.length})</h3>
130
99
  <div className="space-y-1.5">
131
100
  {connected.map(agent => (
132
- <AgentCard key={agent.key} agent={agent} status="connected" />
101
+ <AgentCard
102
+ key={agent.key}
103
+ agent={agent}
104
+ agentStatus="connected"
105
+ mcpStatus={mcp.status}
106
+ expanded={expandedAgent === agent.key}
107
+ onToggle={() => toggleAgent(agent.key)}
108
+ onInstallAgent={mcp.installAgent}
109
+ t={p}
110
+ />
133
111
  ))}
134
112
  </div>
135
113
  </section>
136
114
  )}
137
115
 
138
- {/* Detected but not configured */}
116
+ {/* Detected Agents */}
139
117
  {detected.length > 0 && (
140
118
  <section>
141
- <h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">
142
- Detected ({detected.length})
143
- </h3>
119
+ <h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionDetected} ({detected.length})</h3>
144
120
  <div className="space-y-1.5">
145
121
  {detected.map(agent => (
146
122
  <AgentCard
147
123
  key={agent.key}
148
124
  agent={agent}
149
- status="detected"
150
- onConnect={() => onOpenSettings?.('mcp')}
125
+ agentStatus="detected"
126
+ mcpStatus={mcp.status}
127
+ expanded={expandedAgent === agent.key}
128
+ onToggle={() => toggleAgent(agent.key)}
129
+ onInstallAgent={mcp.installAgent}
130
+ t={p}
151
131
  />
152
132
  ))}
153
133
  </div>
154
134
  </section>
155
135
  )}
156
136
 
157
- {/* Not Detected collapsible */}
137
+ {/* Not Found Agents (collapsed) */}
158
138
  {notFound.length > 0 && (
159
139
  <section>
160
- <button
161
- onClick={() => setShowNotDetected(!showNotDetected)}
162
- className="flex items-center gap-1 text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 hover:text-foreground transition-colors"
163
- >
140
+ <button onClick={() => setShowNotDetected(!showNotDetected)}
141
+ className="flex items-center gap-1 text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 hover:text-foreground transition-colors">
164
142
  {showNotDetected ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
165
- Not Detected ({notFound.length})
143
+ {p.sectionNotDetected} ({notFound.length})
166
144
  </button>
167
145
  {showNotDetected && (
168
146
  <div className="space-y-1.5">
169
147
  {notFound.map(agent => (
170
- <AgentCard key={agent.key} agent={agent} status="notFound" />
148
+ <AgentCard
149
+ key={agent.key}
150
+ agent={agent}
151
+ agentStatus="notFound"
152
+ mcpStatus={mcp.status}
153
+ expanded={expandedAgent === agent.key}
154
+ onToggle={() => toggleAgent(agent.key)}
155
+ onInstallAgent={mcp.installAgent}
156
+ t={p}
157
+ />
171
158
  ))}
172
159
  </div>
173
160
  )}
174
161
  </section>
175
162
  )}
176
163
 
177
- {/* Empty state */}
178
- {agents.length === 0 && (
179
- <p className="text-xs text-muted-foreground text-center py-4">
180
- No agents detected.
181
- </p>
164
+ {/* ── Skills Section ── */}
165
+ {mcp.skills.length > 0 && (
166
+ <section>
167
+ <div className="flex items-center justify-between mb-2">
168
+ <h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
169
+ {p.skillsTitle} <span className="normal-case font-normal">{activeSkillCount} {p.skillsActive}</span>
170
+ </h3>
171
+ <button
172
+ onClick={openAdvancedConfig}
173
+ className="text-2xs text-muted-foreground hover:text-foreground transition-colors"
174
+ >
175
+ {p.newSkill}
176
+ </button>
177
+ </div>
178
+
179
+ {/* Custom skills */}
180
+ {customSkills.length > 0 && (
181
+ <div className="space-y-0.5 mb-2">
182
+ {customSkills.map(skill => (
183
+ <SkillRow key={skill.name} skill={skill} onToggle={mcp.toggleSkill} />
184
+ ))}
185
+ </div>
186
+ )}
187
+
188
+ {/* Built-in skills (collapsed) */}
189
+ {builtinSkills.length > 0 && (
190
+ <>
191
+ <button
192
+ onClick={() => setShowBuiltinSkills(!showBuiltinSkills)}
193
+ className="flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground transition-colors mb-1"
194
+ >
195
+ {showBuiltinSkills ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
196
+ {p.builtinSkills} ({builtinSkills.length})
197
+ </button>
198
+ {showBuiltinSkills && (
199
+ <div className="space-y-0.5">
200
+ {builtinSkills.map(skill => (
201
+ <SkillRow key={skill.name} skill={skill} onToggle={mcp.toggleSkill} />
202
+ ))}
203
+ </div>
204
+ )}
205
+ </>
206
+ )}
207
+ </section>
182
208
  )}
183
209
  </div>
184
210
  )}
185
211
  </div>
186
212
 
187
- {/* Footer */}
213
+ {/* Footer: Advanced Config link */}
188
214
  <div className="px-3 py-2 border-t border-border shrink-0">
189
- <p className="text-2xs text-muted-foreground/60">Auto-refresh every 30s</p>
215
+ <button
216
+ onClick={openAdvancedConfig}
217
+ className="flex items-center gap-1.5 text-2xs text-muted-foreground hover:text-foreground transition-colors w-full"
218
+ >
219
+ <Settings size={11} />
220
+ {p.advancedConfig}
221
+ </button>
190
222
  </div>
191
223
  </div>
192
224
  );
193
225
  }
194
226
 
195
- /* ── Agent Card (panel-compact variant) ── */
227
+ /* ── Skill Row ── */
228
+
229
+ function SkillRow({ skill, onToggle }: { skill: SkillInfo; onToggle: (name: string, enabled: boolean) => void }) {
230
+ return (
231
+ <div className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-md hover:bg-muted/30 transition-colors">
232
+ <span className="text-xs text-foreground truncate">{skill.name}</span>
233
+ <Toggle
234
+ size="sm"
235
+ checked={skill.enabled}
236
+ onChange={(v) => onToggle(skill.name, v)}
237
+ />
238
+ </div>
239
+ );
240
+ }
241
+
242
+ /* ── Agent Card ── */
196
243
 
197
- function AgentCard({
198
- agent,
199
- status,
200
- onConnect,
201
- }: {
244
+ function AgentCard({ agent, agentStatus, mcpStatus, expanded, onToggle, onInstallAgent, t }: {
202
245
  agent: AgentInfo;
203
- status: 'connected' | 'detected' | 'notFound';
204
- onConnect?: () => void;
246
+ agentStatus: 'connected' | 'detected' | 'notFound';
247
+ mcpStatus: McpStatus | null;
248
+ expanded: boolean;
249
+ onToggle: () => void;
250
+ onInstallAgent: (key: string, opts?: { scope?: string; transport?: string }) => Promise<boolean>;
251
+ t: Record<string, any>;
205
252
  }) {
206
- const dot =
207
- status === 'connected' ? 'bg-emerald-500' :
208
- status === 'detected' ? 'bg-amber-500' :
209
- 'bg-zinc-400';
253
+ const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
254
+ const [copied, setCopied] = useState(false);
255
+ const [installing, setInstalling] = useState(false);
256
+ const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
257
+
258
+ const dot = agentStatus === 'connected' ? 'bg-emerald-500' : agentStatus === 'detected' ? 'bg-amber-500' : 'bg-zinc-400';
259
+
260
+ const snippet = useMemo(() => generateSnippet(agent, mcpStatus, transport), [agent, mcpStatus, transport]);
261
+
262
+ const handleCopy = useCallback(async () => {
263
+ const ok = await copyToClipboard(snippet.snippet);
264
+ if (ok) {
265
+ setCopied(true);
266
+ setTimeout(() => setCopied(false), 2000);
267
+ }
268
+ }, [snippet.snippet]);
269
+
270
+ const handleInstall = async () => {
271
+ setInstalling(true);
272
+ setResult(null);
273
+ const ok = await onInstallAgent(agent.key);
274
+ if (ok) {
275
+ setResult({ type: 'success', text: `${agent.name} ${t.connected}` });
276
+ } else {
277
+ setResult({ type: 'error', text: 'Install failed' });
278
+ }
279
+ setInstalling(false);
280
+ };
210
281
 
211
282
  return (
212
- <div className="rounded-lg border border-border/60 bg-card/30 px-3 py-2 flex items-center justify-between gap-2">
213
- <div className="flex items-center gap-2 min-w-0">
214
- <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
215
- <span className="text-xs font-medium text-foreground truncate">{agent.name}</span>
216
- {status === 'connected' && agent.transport && (
217
- <span className="text-2xs px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{agent.transport}</span>
218
- )}
219
- </div>
220
- {status === 'detected' && onConnect && (
221
- <button
222
- onClick={onConnect}
223
- className="flex items-center gap-1 px-2 py-0.5 text-2xs rounded-md bg-amber-500/10 text-amber-600 dark:text-amber-400 hover:bg-amber-500/20 transition-colors shrink-0"
224
- >
225
- Connect
226
- <ExternalLink size={10} />
227
- </button>
283
+ <div className="rounded-lg border border-border/60 bg-card/30 overflow-hidden">
284
+ {/* Header row always clickable to expand */}
285
+ <button
286
+ onClick={onToggle}
287
+ className="w-full px-3 py-2 flex items-center justify-between gap-2 hover:bg-muted/30 transition-colors text-left"
288
+ >
289
+ <div className="flex items-center gap-2 min-w-0">
290
+ <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
291
+ <span className="text-xs font-medium text-foreground truncate">{agent.name}</span>
292
+ {agentStatus === 'connected' && agent.transport && (
293
+ <span className="text-2xs px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{agent.transport}</span>
294
+ )}
295
+ </div>
296
+ {expanded ? <ChevronDown size={10} className="text-muted-foreground shrink-0" /> : <ChevronRight size={10} className="text-muted-foreground shrink-0" />}
297
+ </button>
298
+
299
+ {/* Expanded: snippet + actions */}
300
+ {expanded && (
301
+ <div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-2.5">
302
+ {/* Detected: Connect button */}
303
+ {agentStatus === 'detected' && (
304
+ <>
305
+ <button onClick={handleInstall} disabled={installing}
306
+ className="w-full flex items-center justify-center gap-1.5 px-2.5 py-1.5 text-2xs rounded-md font-medium text-white disabled:opacity-50 transition-colors"
307
+ style={{ background: 'var(--amber)' }}>
308
+ {installing ? <Loader2 size={11} className="animate-spin" /> : null}
309
+ {installing ? t.installing : t.install(agent.name)}
310
+ </button>
311
+ {result && (
312
+ <div className={`flex items-center gap-1.5 text-2xs ${result.type === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive'}`}>
313
+ {result.type === 'success' ? <CheckCircle2 size={11} /> : <AlertCircle size={11} />}
314
+ {result.text}
315
+ </div>
316
+ )}
317
+ </>
318
+ )}
319
+
320
+ {/* Transport toggle */}
321
+ <div className="flex items-center rounded-md border border-border overflow-hidden w-fit">
322
+ <button
323
+ onClick={() => setTransport('stdio')}
324
+ className={`flex items-center gap-1 px-2 py-1 text-2xs transition-colors ${
325
+ transport === 'stdio' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
326
+ }`}
327
+ >
328
+ <Monitor size={10} />
329
+ {t.transportLocal}
330
+ </button>
331
+ <button
332
+ onClick={() => setTransport('http')}
333
+ className={`flex items-center gap-1 px-2 py-1 text-2xs transition-colors ${
334
+ transport === 'http' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
335
+ }`}
336
+ >
337
+ <Globe size={10} />
338
+ {t.transportRemote}
339
+ </button>
340
+ </div>
341
+
342
+ {/* No auth warning for HTTP */}
343
+ {transport === 'http' && mcpStatus && !mcpStatus.authConfigured && (
344
+ <p className="text-2xs" style={{ color: 'var(--amber)' }}>{t.noAuthWarning}</p>
345
+ )}
346
+
347
+ {/* Config snippet */}
348
+ <pre className="text-[10px] font-mono bg-muted/50 border border-border rounded-lg p-2.5 overflow-x-auto whitespace-pre select-all max-h-[200px] overflow-y-auto">
349
+ {snippet.displaySnippet}
350
+ </pre>
351
+
352
+ {/* Copy + path */}
353
+ <div className="flex items-center gap-2 text-2xs">
354
+ <button
355
+ onClick={handleCopy}
356
+ className="inline-flex items-center gap-1 px-2 py-1 rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
357
+ >
358
+ {copied ? <Check size={10} /> : <Copy size={10} />}
359
+ {copied ? t.copied : t.copyConfig}
360
+ </button>
361
+ <span className="text-muted-foreground">→</span>
362
+ <span className="font-mono text-muted-foreground truncate">{snippet.path}</span>
363
+ </div>
364
+ </div>
228
365
  )}
229
366
  </div>
230
367
  );