@geminilight/mindos 0.5.51 → 0.5.54

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.
@@ -1,20 +1,31 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
4
- import { Loader2, RefreshCw, ChevronDown, ChevronRight, CheckCircle2, AlertCircle, Settings } from 'lucide-react';
3
+ import { useState, useRef, useCallback } from 'react';
4
+ import { Loader2, RefreshCw, ChevronDown, ChevronRight, Settings } from 'lucide-react';
5
5
  import { useMcpData } from '@/hooks/useMcpData';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
  import { Toggle } from '../settings/Primitives';
8
- import type { AgentInfo, SkillInfo } from '../settings/types';
8
+ import type { SkillInfo } from '../settings/types';
9
9
  import PanelHeader from './PanelHeader';
10
+ import { AgentsPanelHubNav } from './AgentsPanelHubNav';
11
+ import { AgentsPanelAgentGroups } from './AgentsPanelAgentGroups';
10
12
 
11
13
  interface AgentsPanelProps {
12
14
  active: boolean;
13
15
  maximized?: boolean;
14
16
  onMaximize?: () => void;
17
+ /** Highlights the row for the agent whose detail is open in the right dock. */
18
+ selectedAgentKey?: string | null;
19
+ onOpenAgentDetail?: (key: string) => void;
15
20
  }
16
21
 
17
- export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPanelProps) {
22
+ export default function AgentsPanel({
23
+ active,
24
+ maximized,
25
+ onMaximize,
26
+ selectedAgentKey = null,
27
+ onOpenAgentDetail,
28
+ }: AgentsPanelProps) {
18
29
  const { t } = useLocale();
19
30
  const p = t.panels.agents;
20
31
  const mcp = useMcpData();
@@ -22,12 +33,23 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
22
33
  const [showNotDetected, setShowNotDetected] = useState(false);
23
34
  const [showBuiltinSkills, setShowBuiltinSkills] = useState(false);
24
35
 
36
+ const overviewRef = useRef<HTMLDivElement>(null);
37
+ const skillsRef = useRef<HTMLDivElement>(null);
38
+
25
39
  const handleRefresh = async () => {
26
40
  setRefreshing(true);
27
41
  await mcp.refresh();
28
42
  setRefreshing(false);
29
43
  };
30
44
 
45
+ const scrollTo = useCallback((el: HTMLElement | null) => {
46
+ el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
47
+ }, []);
48
+
49
+ const openAdvancedConfig = () => {
50
+ window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'mcp' } }));
51
+ };
52
+
31
53
  const connected = mcp.agents.filter(a => a.present && a.installed);
32
54
  const detected = mcp.agents.filter(a => a.present && !a.installed);
33
55
  const notFound = mcp.agents.filter(a => !a.present);
@@ -36,20 +58,45 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
36
58
  const builtinSkills = mcp.skills.filter(s => s.source === 'builtin');
37
59
  const activeSkillCount = mcp.skills.filter(s => s.enabled).length;
38
60
 
39
- const openAdvancedConfig = () => {
40
- window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'mcp' } }));
61
+ const listCopy = {
62
+ installing: p.installing,
63
+ install: p.install,
41
64
  };
42
65
 
66
+ const hubCopy = {
67
+ navOverview: p.navOverview,
68
+ navMcp: p.navMcp,
69
+ navSkills: p.navSkills,
70
+ };
71
+
72
+ const hub = (
73
+ <AgentsPanelHubNav
74
+ copy={hubCopy}
75
+ connectedCount={connected.length}
76
+ overviewRef={overviewRef}
77
+ skillsRef={skillsRef}
78
+ scrollTo={scrollTo}
79
+ openAdvancedConfig={openAdvancedConfig}
80
+ />
81
+ );
82
+
43
83
  return (
44
84
  <div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
45
85
  <PanelHeader title={p.title} maximized={maximized} onMaximize={onMaximize}>
46
86
  <div className="flex items-center gap-1.5">
47
87
  {!mcp.loading && (
48
- <span className="text-2xs text-muted-foreground">{connected.length} {p.connected}</span>
88
+ <span className="text-2xs text-muted-foreground">
89
+ {connected.length} {p.connected}
90
+ </span>
49
91
  )}
50
- <button onClick={handleRefresh} disabled={refreshing}
51
- className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground disabled:opacity-50 transition-colors"
52
- aria-label={p.refresh} title={p.refresh}>
92
+ <button
93
+ onClick={handleRefresh}
94
+ disabled={refreshing}
95
+ className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground disabled:opacity-50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
96
+ aria-label={p.refresh}
97
+ title={p.refresh}
98
+ type="button"
99
+ >
53
100
  <RefreshCw size={12} className={refreshing ? 'animate-spin' : ''} />
54
101
  </button>
55
102
  </div>
@@ -57,19 +104,14 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
57
104
 
58
105
  <div className="flex-1 overflow-y-auto min-h-0">
59
106
  {mcp.loading ? (
60
- <div className="flex justify-center py-8"><Loader2 size={16} className="animate-spin text-muted-foreground" /></div>
61
- ) : mcp.agents.length === 0 && mcp.skills.length === 0 ? (
62
- <div className="flex flex-col items-center gap-2 py-8 text-center px-4">
63
- <p className="text-xs text-muted-foreground">{p.noAgents}</p>
64
- <button onClick={handleRefresh}
65
- 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">
66
- <RefreshCw size={11} /> {p.retry}
67
- </button>
107
+ <div className="flex justify-center py-8">
108
+ <Loader2 size={16} className="animate-spin text-muted-foreground" />
68
109
  </div>
69
- ) : (
70
- <div className="px-3 py-3 space-y-4">
71
- {/* MCP Server status — single line */}
72
- <div className="rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between">
110
+ ) : mcp.agents.length === 0 && mcp.skills.length === 0 ? (
111
+ <div className="flex flex-col gap-2 py-4 px-0">
112
+ {hub}
113
+ <div className="mx-4 border-t border-border" />
114
+ <div ref={overviewRef} className="mx-3 rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between scroll-mt-2">
73
115
  <span className="text-xs font-medium text-foreground">{p.mcpServer}</span>
74
116
  {mcp.status?.running ? (
75
117
  <span className="flex items-center gap-1.5 text-[11px]">
@@ -84,121 +126,117 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
84
126
  </span>
85
127
  )}
86
128
  </div>
129
+ <div ref={skillsRef} className="mx-3 scroll-mt-2 rounded-lg border border-dashed border-border px-3 py-3 text-center">
130
+ <p className="text-xs text-muted-foreground mb-2">{p.noAgents}</p>
131
+ <p className="text-2xs text-muted-foreground mb-3">{p.skillsEmptyHint}</p>
132
+ <button
133
+ onClick={handleRefresh}
134
+ type="button"
135
+ className="inline-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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
136
+ >
137
+ <RefreshCw size={11} /> {p.retry}
138
+ </button>
139
+ </div>
140
+ </div>
141
+ ) : (
142
+ <div className="pb-3">
143
+ {hub}
87
144
 
88
- {/* Connected Agents */}
89
- {connected.length > 0 && (
90
- <section>
91
- <h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionConnected} ({connected.length})</h3>
92
- <div className="space-y-1.5">
93
- {connected.map(agent => (
94
- <AgentCard
95
- key={agent.key}
96
- agent={agent}
97
- agentStatus="connected"
98
- onInstallAgent={mcp.installAgent}
99
- t={p}
100
- />
101
- ))}
102
- </div>
103
- </section>
104
- )}
105
-
106
- {/* Detected Agents */}
107
- {detected.length > 0 && (
108
- <section>
109
- <h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionDetected} ({detected.length})</h3>
110
- <div className="space-y-1.5">
111
- {detected.map(agent => (
112
- <AgentCard
113
- key={agent.key}
114
- agent={agent}
115
- agentStatus="detected"
116
- onInstallAgent={mcp.installAgent}
117
- t={p}
118
- />
119
- ))}
120
- </div>
121
- </section>
122
- )}
145
+ <div className="mx-4 border-t border-border" />
123
146
 
124
- {/* Not Found Agents (collapsed) */}
125
- {notFound.length > 0 && (
126
- <section>
127
- <button onClick={() => setShowNotDetected(!showNotDetected)}
128
- className="flex items-center gap-1 text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 hover:text-foreground transition-colors">
129
- {showNotDetected ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
130
- {p.sectionNotDetected} ({notFound.length})
131
- </button>
132
- {showNotDetected && (
133
- <div className="space-y-1.5">
134
- {notFound.map(agent => (
135
- <AgentCard
136
- key={agent.key}
137
- agent={agent}
138
- agentStatus="notFound"
139
- onInstallAgent={mcp.installAgent}
140
- t={p}
141
- />
142
- ))}
143
- </div>
147
+ <div className="px-3 py-3 space-y-4">
148
+ <div ref={overviewRef} className="rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between scroll-mt-2">
149
+ <span className="text-xs font-medium text-foreground">{p.mcpServer}</span>
150
+ {mcp.status?.running ? (
151
+ <span className="flex items-center gap-1.5 text-[11px]">
152
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 inline-block" />
153
+ <span className="text-emerald-600 dark:text-emerald-400">:{mcp.status.port}</span>
154
+ <span className="text-muted-foreground">· {mcp.status.toolCount} tools</span>
155
+ </span>
156
+ ) : (
157
+ <span className="flex items-center gap-1.5 text-[11px]">
158
+ <span className="w-1.5 h-1.5 rounded-full bg-zinc-400 inline-block" />
159
+ <span className="text-muted-foreground">{p.stopped}</span>
160
+ </span>
144
161
  )}
145
- </section>
146
- )}
162
+ </div>
147
163
 
148
- {/* ── Skills Section ── */}
149
- {mcp.skills.length > 0 && (
150
- <section>
151
- <div className="flex items-center justify-between mb-2">
152
- <h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
153
- {p.skillsTitle} <span className="normal-case font-normal">{activeSkillCount} {p.skillsActive}</span>
154
- </h3>
155
- <button
156
- onClick={openAdvancedConfig}
157
- className="text-2xs text-muted-foreground hover:text-foreground transition-colors"
158
- >
159
- {p.newSkill}
160
- </button>
161
- </div>
164
+ <AgentsPanelAgentGroups
165
+ connected={connected}
166
+ detected={detected}
167
+ notFound={notFound}
168
+ onOpenDetail={onOpenAgentDetail}
169
+ selectedAgentKey={selectedAgentKey}
170
+ mcp={mcp}
171
+ listCopy={listCopy}
172
+ showNotDetected={showNotDetected}
173
+ setShowNotDetected={setShowNotDetected}
174
+ p={{
175
+ rosterLabel: p.rosterLabel,
176
+ sectionConnected: p.sectionConnected,
177
+ sectionDetected: p.sectionDetected,
178
+ sectionNotDetected: p.sectionNotDetected,
179
+ }}
180
+ />
162
181
 
163
- {/* Custom skills */}
164
- {customSkills.length > 0 && (
165
- <div className="space-y-0.5 mb-2">
166
- {customSkills.map(skill => (
167
- <SkillRow key={skill.name} skill={skill} onToggle={mcp.toggleSkill} />
168
- ))}
169
- </div>
170
- )}
171
-
172
- {/* Built-in skills (collapsed) */}
173
- {builtinSkills.length > 0 && (
182
+ <section ref={skillsRef} className="scroll-mt-2">
183
+ {mcp.skills.length > 0 ? (
174
184
  <>
175
- <button
176
- onClick={() => setShowBuiltinSkills(!showBuiltinSkills)}
177
- className="flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground transition-colors mb-1"
178
- >
179
- {showBuiltinSkills ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
180
- {p.builtinSkills} ({builtinSkills.length})
181
- </button>
182
- {showBuiltinSkills && (
183
- <div className="space-y-0.5">
184
- {builtinSkills.map(skill => (
185
+ <div className="flex items-center justify-between mb-2">
186
+ <h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
187
+ {p.skillsTitle} <span className="normal-case font-normal">{activeSkillCount} {p.skillsActive}</span>
188
+ </h3>
189
+ <button
190
+ type="button"
191
+ onClick={openAdvancedConfig}
192
+ className="text-2xs text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
193
+ >
194
+ {p.newSkill}
195
+ </button>
196
+ </div>
197
+
198
+ {customSkills.length > 0 && (
199
+ <div className="space-y-0.5 mb-2">
200
+ {customSkills.map(skill => (
185
201
  <SkillRow key={skill.name} skill={skill} onToggle={mcp.toggleSkill} />
186
202
  ))}
187
203
  </div>
188
204
  )}
205
+
206
+ {builtinSkills.length > 0 && (
207
+ <>
208
+ <button
209
+ type="button"
210
+ onClick={() => setShowBuiltinSkills(!showBuiltinSkills)}
211
+ className="flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground transition-colors mb-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
212
+ >
213
+ {showBuiltinSkills ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
214
+ {p.builtinSkills} ({builtinSkills.length})
215
+ </button>
216
+ {showBuiltinSkills && (
217
+ <div className="space-y-0.5">
218
+ {builtinSkills.map(skill => (
219
+ <SkillRow key={skill.name} skill={skill} onToggle={mcp.toggleSkill} />
220
+ ))}
221
+ </div>
222
+ )}
223
+ </>
224
+ )}
189
225
  </>
226
+ ) : (
227
+ <p className="text-2xs text-muted-foreground py-1">{p.skillsEmptyHint}</p>
190
228
  )}
191
229
  </section>
192
- )}
230
+ </div>
193
231
  </div>
194
232
  )}
195
233
  </div>
196
234
 
197
- {/* Footer: Advanced Config link */}
198
235
  <div className="px-3 py-2 border-t border-border shrink-0">
199
236
  <button
237
+ type="button"
200
238
  onClick={openAdvancedConfig}
201
- className="flex items-center gap-1.5 text-2xs text-muted-foreground hover:text-foreground transition-colors w-full"
239
+ className="flex items-center gap-1.5 text-2xs text-muted-foreground hover:text-foreground transition-colors w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
202
240
  >
203
241
  <Settings size={11} />
204
242
  {p.advancedConfig}
@@ -208,71 +246,11 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
208
246
  );
209
247
  }
210
248
 
211
- /* ── Skill Row ── */
212
-
213
249
  function SkillRow({ skill, onToggle }: { skill: SkillInfo; onToggle: (name: string, enabled: boolean) => void }) {
214
250
  return (
215
251
  <div className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-md hover:bg-muted/30 transition-colors">
216
252
  <span className="text-xs text-foreground truncate">{skill.name}</span>
217
- <Toggle
218
- size="sm"
219
- checked={skill.enabled}
220
- onChange={(v) => onToggle(skill.name, v)}
221
- />
222
- </div>
223
- );
224
- }
225
-
226
- /* ── Agent Card (compact — no snippet, config viewing is in Settings) ── */
227
-
228
- function AgentCard({ agent, agentStatus, onInstallAgent, t }: {
229
- agent: AgentInfo;
230
- agentStatus: 'connected' | 'detected' | 'notFound';
231
- onInstallAgent: (key: string) => Promise<boolean>;
232
- t: Record<string, any>;
233
- }) {
234
- const [installing, setInstalling] = useState(false);
235
- const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
236
-
237
- const dot = agentStatus === 'connected' ? 'bg-emerald-500' : agentStatus === 'detected' ? 'bg-amber-500' : 'bg-zinc-400';
238
-
239
- const handleInstall = async () => {
240
- setInstalling(true);
241
- setResult(null);
242
- const ok = await onInstallAgent(agent.key);
243
- setResult(ok
244
- ? { type: 'success', text: `${agent.name} ${t.connected}` }
245
- : { type: 'error', text: 'Install failed' });
246
- setInstalling(false);
247
- };
248
-
249
- return (
250
- <div className="rounded-lg border border-border/60 bg-card/30 px-3 py-2 flex items-center justify-between gap-2">
251
- <div className="flex items-center gap-2 min-w-0">
252
- <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
253
- <span className="text-xs font-medium text-foreground truncate">{agent.name}</span>
254
- {agentStatus === 'connected' && agent.transport && (
255
- <span className="text-2xs px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{agent.transport}</span>
256
- )}
257
- </div>
258
-
259
- {/* Detected: Install button */}
260
- {agentStatus === 'detected' && (
261
- <button onClick={handleInstall} disabled={installing}
262
- className="flex items-center gap-1 px-2 py-1 text-2xs rounded-md font-medium text-white disabled:opacity-50 transition-colors shrink-0"
263
- style={{ background: 'var(--amber)' }}>
264
- {installing ? <Loader2 size={10} className="animate-spin" /> : null}
265
- {installing ? t.installing : t.install(agent.name)}
266
- </button>
267
- )}
268
-
269
- {/* Install result */}
270
- {result && (
271
- <span className={`flex items-center gap-1 text-2xs shrink-0 ${result.type === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive'}`}>
272
- {result.type === 'success' ? <CheckCircle2 size={10} /> : <AlertCircle size={10} />}
273
- {result.text}
274
- </span>
275
- )}
253
+ <Toggle size="sm" checked={skill.enabled} onChange={v => onToggle(skill.name, v)} />
276
254
  </div>
277
255
  );
278
256
  }
@@ -0,0 +1,193 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo } from 'react';
4
+ import { ChevronLeft, X, Loader2, CheckCircle2, AlertCircle, Copy, Check, Monitor, Globe } from 'lucide-react';
5
+ import { generateSnippet } from '@/lib/mcp-snippets';
6
+ import { copyToClipboard } from '@/lib/clipboard';
7
+ import type { AgentInfo, McpStatus } from '../settings/types';
8
+ import type { AgentsPanelAgentDetailStatus } from './agents-panel-resolve-status';
9
+
10
+ export type { AgentsPanelAgentDetailStatus };
11
+
12
+ export interface AgentsPanelAgentDetailCopy {
13
+ connected: string;
14
+ installing: string;
15
+ install: (name: string) => string;
16
+ copyConfig: string;
17
+ copied: string;
18
+ transportLocal: string;
19
+ transportRemote: string;
20
+ configPath: string;
21
+ notFoundDetail: string;
22
+ backToList: string;
23
+ /** Close button (dock header) — aria-label */
24
+ closeDetail?: string;
25
+ agentDetailTransport: string;
26
+ agentDetailSnippet: string;
27
+ }
28
+
29
+ export default function AgentsPanelAgentDetail({
30
+ agent,
31
+ agentStatus,
32
+ mcpStatus,
33
+ onBack,
34
+ onInstallAgent,
35
+ copy,
36
+ headerVariant = 'inline',
37
+ }: {
38
+ agent: AgentInfo;
39
+ agentStatus: AgentsPanelAgentDetailStatus;
40
+ mcpStatus: McpStatus | null;
41
+ onBack: () => void;
42
+ onInstallAgent: (key: string) => Promise<boolean>;
43
+ copy: AgentsPanelAgentDetailCopy;
44
+ /** `dock`: right-side sheet title + X. `inline`: back chevron (legacy sidebar drill). */
45
+ headerVariant?: 'inline' | 'dock';
46
+ }) {
47
+ const [installing, setInstalling] = useState(false);
48
+ const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
49
+ const [transport, setTransport] = useState<'stdio' | 'http'>(() => agent.preferredTransport);
50
+ const [copied, setCopied] = useState(false);
51
+
52
+ const snippet = useMemo(() => {
53
+ if (agentStatus === 'notFound') return null;
54
+ return generateSnippet(agent, mcpStatus, transport);
55
+ }, [agent, mcpStatus, transport, agentStatus]);
56
+
57
+ const handleInstall = async () => {
58
+ setInstalling(true);
59
+ setResult(null);
60
+ const ok = await onInstallAgent(agent.key);
61
+ setResult(
62
+ ok
63
+ ? { type: 'success', text: `${agent.name} ${copy.connected}` }
64
+ : { type: 'error', text: 'Install failed' },
65
+ );
66
+ setInstalling(false);
67
+ };
68
+
69
+ const handleCopy = async () => {
70
+ if (!snippet) return;
71
+ const ok = await copyToClipboard(snippet.snippet);
72
+ if (ok) {
73
+ setCopied(true);
74
+ setTimeout(() => setCopied(false), 2000);
75
+ }
76
+ };
77
+
78
+ const dot =
79
+ agentStatus === 'connected' ? 'bg-emerald-500' : agentStatus === 'detected' ? 'bg-amber-500' : 'bg-zinc-400';
80
+
81
+ return (
82
+ <div className="flex flex-col h-full min-h-0">
83
+ {headerVariant === 'dock' ? (
84
+ <header className="shrink-0 flex items-center justify-between gap-3 border-b border-border px-4 py-3 bg-card">
85
+ <div className="flex items-center gap-2.5 min-w-0">
86
+ <span className={`w-2 h-2 rounded-full shrink-0 ${dot}`} />
87
+ <h2 className="text-sm font-semibold text-foreground truncate font-display">{agent.name}</h2>
88
+ </div>
89
+ <button
90
+ type="button"
91
+ onClick={onBack}
92
+ className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0"
93
+ aria-label={copy.closeDetail ?? 'Close'}
94
+ >
95
+ <X size={16} />
96
+ </button>
97
+ </header>
98
+ ) : (
99
+ <div className="sticky top-0 z-10 flex items-center gap-2 border-b border-border bg-background/95 px-3 py-2.5 backdrop-blur-sm shrink-0">
100
+ <button
101
+ type="button"
102
+ onClick={onBack}
103
+ className="flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground rounded-md px-1 py-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0"
104
+ >
105
+ <ChevronLeft size={16} />
106
+ {copy.backToList}
107
+ </button>
108
+ <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
109
+ <span className="text-sm font-medium text-foreground truncate">{agent.name}</span>
110
+ </div>
111
+ )}
112
+
113
+ <div className="px-4 py-4 space-y-4 flex-1 min-h-0 overflow-y-auto">
114
+ {agentStatus === 'detected' && (
115
+ <div className="flex flex-wrap items-center gap-2">
116
+ <button
117
+ type="button"
118
+ onClick={handleInstall}
119
+ disabled={installing}
120
+ className="inline-flex items-center gap-1.5 px-3 py-2 text-xs rounded-lg font-medium text-[var(--amber-foreground)] disabled:opacity-50 bg-[var(--amber)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
121
+ >
122
+ {installing ? <Loader2 size={14} className="animate-spin" /> : null}
123
+ {installing ? copy.installing : copy.install(agent.name)}
124
+ </button>
125
+ {result && (
126
+ <span
127
+ className={`flex items-center gap-1 text-2xs ${result.type === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive'}`}
128
+ >
129
+ {result.type === 'success' ? <CheckCircle2 size={12} /> : <AlertCircle size={12} />}
130
+ {result.text}
131
+ </span>
132
+ )}
133
+ </div>
134
+ )}
135
+
136
+ {agentStatus === 'notFound' && (
137
+ <p className="text-sm text-muted-foreground leading-relaxed">{copy.notFoundDetail}</p>
138
+ )}
139
+
140
+ {agentStatus !== 'notFound' && snippet && (
141
+ <>
142
+ <div>
143
+ <p className="text-2xs font-medium text-muted-foreground uppercase tracking-wider mb-1">{copy.configPath}</p>
144
+ <p className="text-xs font-mono text-foreground break-all">{snippet.path}</p>
145
+ </div>
146
+
147
+ <div>
148
+ <p className="text-2xs font-medium text-muted-foreground uppercase tracking-wider mb-2">{copy.agentDetailTransport}</p>
149
+ <div className="flex items-center gap-1 p-0.5 rounded-lg bg-muted/40 w-fit">
150
+ <button
151
+ type="button"
152
+ onClick={() => setTransport('stdio')}
153
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
154
+ transport === 'stdio' ? 'bg-card shadow-sm text-foreground' : 'text-muted-foreground'
155
+ }`}
156
+ >
157
+ <Monitor size={13} />
158
+ {copy.transportLocal}
159
+ </button>
160
+ <button
161
+ type="button"
162
+ onClick={() => setTransport('http')}
163
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
164
+ transport === 'http' ? 'bg-card shadow-sm text-foreground' : 'text-muted-foreground'
165
+ }`}
166
+ >
167
+ <Globe size={13} />
168
+ {copy.transportRemote}
169
+ </button>
170
+ </div>
171
+ </div>
172
+
173
+ <div>
174
+ <p className="text-2xs font-medium text-muted-foreground uppercase tracking-wider mb-2">{copy.agentDetailSnippet}</p>
175
+ <pre className="text-[11px] font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre max-h-[min(320px,50vh)] overflow-y-auto select-all">
176
+ {snippet.displaySnippet}
177
+ </pre>
178
+ </div>
179
+
180
+ <button
181
+ type="button"
182
+ onClick={handleCopy}
183
+ className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
184
+ >
185
+ {copied ? <Check size={14} /> : <Copy size={14} />}
186
+ {copied ? copy.copied : copy.copyConfig}
187
+ </button>
188
+ </>
189
+ )}
190
+ </div>
191
+ </div>
192
+ );
193
+ }