@geminilight/mindos 0.5.50 → 0.5.52

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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">MindOS</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Human Thinks Here, Agent Acts There.</strong>
8
+ <strong>Human Thinks Here, Agents Act There.</strong>
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -20,7 +20,7 @@
20
20
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-6366f1.svg?style=for-the-badge" alt="MIT License"></a>
21
21
  </p>
22
22
 
23
- MindOS is a **Human-AI Collaborative Mind System**—a local-first knowledge base that ensures your notes, workflows, and personal context are both human-readable and directly executable by Agents. **Share your brain with every AI — auditable, correctable, and more YOU with every use.**
23
+ MindOS is where you think, and where your AI agents act a local-first knowledge base shared between you and every AI you use. **Share your brain with every AI — every thought grows.**
24
24
 
25
25
  ---
26
26
 
package/README_zh.md CHANGED
@@ -20,7 +20,7 @@
20
20
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-6366f1.svg?style=for-the-badge" alt="MIT License"></a>
21
21
  </p>
22
22
 
23
- MindOS 是一个**人机协同心智系统**——基于本地优先的协作知识库,让你的笔记、工作流、个人上下文既对人类阅读友好,也能直接被 Agent 调用和执行。**和所有 AI 共享你的大脑——可审计、可修正、越用越是你。**
23
+ MindOS 是你思考的地方,也是 AI Agent 行动的起点——一个你和所有 AI 共享的本地知识库。**每次思考,都在生长。**
24
24
 
25
25
  ---
26
26
 
@@ -4,6 +4,13 @@ import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { MCP_AGENTS, expandHome } from '@/lib/mcp-agents';
6
6
 
7
+ /** Parse JSONC — strips single-line (//) and block comments before JSON.parse */
8
+ function parseJsonc(text: string): Record<string, unknown> {
9
+ let stripped = text.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*$)/gm, (m, g) => g ? '' : m);
10
+ stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '');
11
+ return JSON.parse(stripped);
12
+ }
13
+
7
14
  interface AgentInstallItem {
8
15
  key: string;
9
16
  scope: 'project' | 'global';
@@ -92,7 +99,7 @@ export async function POST(req: NextRequest) {
92
99
  // Read existing config
93
100
  let config: Record<string, unknown> = {};
94
101
  if (fs.existsSync(absPath)) {
95
- config = JSON.parse(fs.readFileSync(absPath, 'utf-8'));
102
+ config = parseJsonc(fs.readFileSync(absPath, 'utf-8'));
96
103
  }
97
104
 
98
105
  // Merge — only touch mcpServers.mindos
@@ -0,0 +1,88 @@
1
+ export const dynamic = 'force-dynamic';
2
+ import { NextResponse } from 'next/server';
3
+ import { execSync, spawn } from 'node:child_process';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { resolve } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+
8
+ const CONFIG_PATH = resolve(homedir(), '.mindos', 'config.json');
9
+
10
+ function readConfig(): Record<string, unknown> {
11
+ try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); }
12
+ catch { return {}; }
13
+ }
14
+
15
+ /**
16
+ * Kill process(es) listening on the given port.
17
+ * Tries lsof first, falls back to ss + manual kill.
18
+ */
19
+ function killByPort(port: number) {
20
+ try {
21
+ execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'ignore' });
22
+ return;
23
+ } catch { /* lsof not available */ }
24
+ try {
25
+ const output = execSync('ss -tlnp 2>/dev/null', { encoding: 'utf-8' });
26
+ const portRe = new RegExp(`:${port}(?!\\d)`);
27
+ for (const line of output.split('\n')) {
28
+ if (!portRe.test(line)) continue;
29
+ const pidMatch = line.match(/pid=(\d+)/g);
30
+ if (pidMatch) {
31
+ for (const m of pidMatch) {
32
+ const pid = Number(m.slice(4));
33
+ if (pid > 0) try { process.kill(pid, 'SIGKILL'); } catch {}
34
+ }
35
+ }
36
+ }
37
+ } catch { /* no process to kill */ }
38
+ }
39
+
40
+ /**
41
+ * POST /api/mcp/restart — kill the MCP server process and spawn a new one.
42
+ *
43
+ * Unlike /api/restart which restarts the entire MindOS (Web + MCP),
44
+ * this endpoint only restarts the MCP server. The Web UI stays up.
45
+ */
46
+ export async function POST() {
47
+ try {
48
+ const cfg = readConfig();
49
+ const mcpPort = (cfg.mcpPort as number) ?? 8781;
50
+ const webPort = process.env.MINDOS_WEB_PORT || '3456';
51
+ const authToken = cfg.authToken as string | undefined;
52
+
53
+ // Step 1: Kill process on MCP port
54
+ killByPort(mcpPort);
55
+
56
+ // Step 2: Wait briefly for port to free
57
+ await new Promise(r => setTimeout(r, 1000));
58
+
59
+ // Step 3: Spawn new MCP server
60
+ const root = resolve(process.cwd(), '..');
61
+ const mcpDir = resolve(root, 'mcp');
62
+
63
+ if (!existsSync(resolve(mcpDir, 'node_modules'))) {
64
+ return NextResponse.json({ error: 'MCP dependencies not installed' }, { status: 500 });
65
+ }
66
+
67
+ const env: NodeJS.ProcessEnv = {
68
+ ...process.env,
69
+ MCP_PORT: String(mcpPort),
70
+ MCP_HOST: process.env.MCP_HOST || '0.0.0.0',
71
+ MINDOS_URL: process.env.MINDOS_URL || `http://127.0.0.1:${webPort}`,
72
+ ...(authToken ? { AUTH_TOKEN: authToken } : {}),
73
+ };
74
+
75
+ const child = spawn('npx', ['tsx', 'src/index.ts'], {
76
+ cwd: mcpDir,
77
+ detached: true,
78
+ stdio: 'ignore',
79
+ env,
80
+ });
81
+ child.unref();
82
+
83
+ return NextResponse.json({ ok: true, pid: child.pid, port: mcpPort });
84
+ } catch (err) {
85
+ const message = err instanceof Error ? err.message : String(err);
86
+ return NextResponse.json({ error: message }, { status: 500 });
87
+ }
88
+ }
@@ -8,6 +8,7 @@ import { LocaleProvider } from '@/lib/LocaleContext';
8
8
  import ErrorBoundary from '@/components/ErrorBoundary';
9
9
  import RegisterSW from './register-sw';
10
10
  import UpdateBanner from '@/components/UpdateBanner';
11
+ import UpdateOverlay from '@/components/UpdateOverlay';
11
12
  import { cookies } from 'next/headers';
12
13
  import type { Locale } from '@/lib/i18n';
13
14
 
@@ -108,6 +109,7 @@ export default async function RootLayout({
108
109
  </ErrorBoundary>
109
110
  </TooltipProvider>
110
111
  <RegisterSW />
112
+ <UpdateOverlay />
111
113
  </LocaleProvider>
112
114
  </body>
113
115
  </html>
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { Loader2, CheckCircle2 } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+
7
+ const UPDATE_STATE_KEY = 'mindos_update_in_progress';
8
+ const POLL_INTERVAL = 3_000;
9
+
10
+ /**
11
+ * Global overlay shown when MindOS update kills the server.
12
+ * Mounted in root layout — persists across page navigations and Settings close.
13
+ * Reads localStorage flag set by UpdateTab. Auto-reloads when server comes back.
14
+ */
15
+ export default function UpdateOverlay() {
16
+ const [visible, setVisible] = useState(false);
17
+ const [done, setDone] = useState(false);
18
+ const pollRef = useRef<ReturnType<typeof setInterval>>(undefined);
19
+ const { locale } = useLocale();
20
+ const zh = locale === 'zh';
21
+
22
+ const startPolling = useCallback(() => {
23
+ pollRef.current = setInterval(async () => {
24
+ try {
25
+ const res = await fetch('/api/health', { signal: AbortSignal.timeout(3000) });
26
+ if (res.ok) {
27
+ clearInterval(pollRef.current);
28
+ pollRef.current = undefined;
29
+ // Server is back — check if version changed
30
+ try {
31
+ const saved = localStorage.getItem(UPDATE_STATE_KEY);
32
+ if (saved) {
33
+ const { originalVer } = JSON.parse(saved);
34
+ const data = await fetch('/api/update-check').then(r => r.json());
35
+ if (data.current && data.current !== originalVer) {
36
+ setDone(true);
37
+ localStorage.removeItem(UPDATE_STATE_KEY);
38
+ localStorage.removeItem('mindos_update_latest');
39
+ localStorage.removeItem('mindos_update_dismissed');
40
+ setTimeout(() => window.location.reload(), 1500);
41
+ return;
42
+ }
43
+ }
44
+ } catch { /* check failed, still reload */ }
45
+ // Server is back but version unchanged (or no saved state) — just reload
46
+ localStorage.removeItem(UPDATE_STATE_KEY);
47
+ window.location.reload();
48
+ }
49
+ } catch {
50
+ // Still down
51
+ }
52
+ }, POLL_INTERVAL);
53
+ }, []);
54
+
55
+ // Check on mount and listen for update-started event from UpdateTab
56
+ useEffect(() => {
57
+ const check = () => {
58
+ const saved = localStorage.getItem(UPDATE_STATE_KEY);
59
+ if (saved) {
60
+ setVisible(true);
61
+ if (!pollRef.current) startPolling();
62
+ }
63
+ };
64
+
65
+ // Check immediately (handles page reload during update)
66
+ check();
67
+
68
+ // Listen for same-tab update start (localStorage 'storage' event only fires cross-tab)
69
+ const handler = () => check();
70
+ window.addEventListener('mindos:update-started', handler);
71
+ window.addEventListener('storage', handler); // cross-tab fallback
72
+
73
+ return () => {
74
+ clearInterval(pollRef.current);
75
+ pollRef.current = undefined;
76
+ window.removeEventListener('mindos:update-started', handler);
77
+ window.removeEventListener('storage', handler);
78
+ };
79
+ }, [startPolling]);
80
+
81
+ if (!visible) return null;
82
+
83
+ return (
84
+ <div
85
+ style={{
86
+ position: 'fixed',
87
+ inset: 0,
88
+ zIndex: 99999,
89
+ background: 'rgba(0,0,0,0.7)',
90
+ backdropFilter: 'blur(8px)',
91
+ display: 'flex',
92
+ flexDirection: 'column',
93
+ alignItems: 'center',
94
+ justifyContent: 'center',
95
+ fontFamily: 'system-ui, -apple-system, sans-serif',
96
+ }}
97
+ >
98
+ {done ? (
99
+ <>
100
+ <CheckCircle2 size={32} style={{ color: '#7aad80', marginBottom: 12 }} />
101
+ <div style={{ color: '#e8e4dc', fontSize: 18, fontWeight: 600 }}>
102
+ {zh ? '更新成功!' : 'Update Complete!'}
103
+ </div>
104
+ <div style={{ color: '#8a8275', fontSize: 13, marginTop: 6 }}>
105
+ {zh ? '正在刷新页面...' : 'Reloading...'}
106
+ </div>
107
+ </>
108
+ ) : (
109
+ <>
110
+ <Loader2 size={32} style={{ color: '#d4954a', marginBottom: 12, animation: 'spin 1s linear infinite' }} />
111
+ <div style={{ color: '#e8e4dc', fontSize: 18, fontWeight: 600 }}>
112
+ {zh ? 'MindOS 正在更新...' : 'MindOS is Updating...'}
113
+ </div>
114
+ <div style={{ color: '#8a8275', fontSize: 13, marginTop: 6, textAlign: 'center', maxWidth: 300, lineHeight: 1.5 }}>
115
+ {zh
116
+ ? '服务正在重启,请勿关闭此页面。完成后将自动刷新。'
117
+ : 'The server is restarting. Please do not close this page. It will auto-reload when ready.'}
118
+ </div>
119
+ </>
120
+ )}
121
+ <style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
122
+ </div>
123
+ );
124
+ }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useMemo, useCallback } from 'react';
3
+ import { useState, useMemo, useCallback, useEffect } from 'react';
4
4
  import { BookOpen, Rocket, Brain, Keyboard, HelpCircle, Bot, ChevronDown, Copy, Check } from 'lucide-react';
5
5
  import { useLocale } from '@/lib/LocaleContext';
6
6
 
@@ -39,7 +39,7 @@ function Section({ icon, title, defaultOpen = false, children }: {
39
39
  function StepCard({ step, title, desc }: { step: number; title: string; desc: string }) {
40
40
  return (
41
41
  <div className="flex gap-4 items-start">
42
- <div className="shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold font-mono" style={{ background: 'var(--amber-dim)', color: 'var(--amber)' }}>
42
+ <div className="shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold font-mono bg-[var(--amber-dim)] text-[var(--amber)]">
43
43
  {step}
44
44
  </div>
45
45
  <div className="min-w-0">
@@ -59,12 +59,12 @@ function PromptBlock({ text, copyLabel }: { text: string; copyLabel: string }) {
59
59
  navigator.clipboard.writeText(clean).then(() => {
60
60
  setCopied(true);
61
61
  setTimeout(() => setCopied(false), 1500);
62
- });
62
+ }).catch(() => {});
63
63
  }, [text]);
64
64
 
65
65
  return (
66
66
  <div className="group/prompt mt-2 flex items-start gap-2 bg-background border border-border rounded-md px-3 py-2">
67
- <p className="flex-1 text-xs font-mono leading-relaxed" style={{ color: 'var(--amber)' }}>{text}</p>
67
+ <p className="flex-1 text-xs font-mono leading-relaxed text-[var(--amber)]">{text}</p>
68
68
  <button
69
69
  onClick={handleCopy}
70
70
  className="shrink-0 p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors opacity-0 group-hover/prompt:opacity-100 focus-visible:opacity-100"
@@ -122,8 +122,11 @@ export default function HelpContent() {
122
122
  const { t } = useLocale();
123
123
  const h = t.help;
124
124
 
125
- const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent);
126
- const mod = isMac ? '⌘' : 'Ctrl';
125
+ const [mod, setMod] = useState('');
126
+ useEffect(() => {
127
+ const isMac = /Mac|iPhone|iPad/.test(navigator.userAgent);
128
+ setMod(isMac ? '⌘' : 'Ctrl');
129
+ }, []);
127
130
 
128
131
  const shortcuts = useMemo(() => [
129
132
  { keys: `${mod} K`, label: h.shortcuts.search },
@@ -141,7 +144,7 @@ export default function HelpContent() {
141
144
  {/* ── Header ── */}
142
145
  <div className="mb-8">
143
146
  <div className="flex items-center gap-2 mb-1">
144
- <div className="w-1 h-6 rounded-full" style={{ background: 'var(--amber)' }} />
147
+ <div className="w-1 h-6 rounded-full bg-[var(--amber)]" />
145
148
  <h1 className="text-2xl font-bold font-display text-foreground">{h.title}</h1>
146
149
  </div>
147
150
  <p className="text-muted-foreground text-sm ml-3 mt-1">{h.subtitle}</p>
@@ -1,13 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { useState, useCallback, useMemo } from 'react';
4
- import { Loader2, RefreshCw, ChevronDown, ChevronRight, CheckCircle2, AlertCircle, Copy, Check, Monitor, Globe, Settings } from 'lucide-react';
3
+ import { useState } from 'react';
4
+ import { Loader2, RefreshCw, ChevronDown, ChevronRight, CheckCircle2, AlertCircle, Settings } from 'lucide-react';
5
5
  import { useMcpData } from '@/hooks/useMcpData';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
- import { generateSnippet } from '@/lib/mcp-snippets';
8
- import { copyToClipboard } from '@/lib/clipboard';
9
7
  import { Toggle } from '../settings/Primitives';
10
- import type { AgentInfo, McpStatus, SkillInfo } from '../settings/types';
8
+ import type { AgentInfo, SkillInfo } from '../settings/types';
11
9
  import PanelHeader from './PanelHeader';
12
10
 
13
11
  interface AgentsPanelProps {
@@ -22,7 +20,6 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
22
20
  const mcp = useMcpData();
23
21
  const [refreshing, setRefreshing] = useState(false);
24
22
  const [showNotDetected, setShowNotDetected] = useState(false);
25
- const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
26
23
  const [showBuiltinSkills, setShowBuiltinSkills] = useState(false);
27
24
 
28
25
  const handleRefresh = async () => {
@@ -31,10 +28,6 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
31
28
  setRefreshing(false);
32
29
  };
33
30
 
34
- const toggleAgent = (key: string) => {
35
- setExpandedAgent(prev => prev === key ? null : key);
36
- };
37
-
38
31
  const connected = mcp.agents.filter(a => a.present && a.installed);
39
32
  const detected = mcp.agents.filter(a => a.present && !a.installed);
40
33
  const notFound = mcp.agents.filter(a => !a.present);
@@ -102,9 +95,6 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
102
95
  key={agent.key}
103
96
  agent={agent}
104
97
  agentStatus="connected"
105
- mcpStatus={mcp.status}
106
- expanded={expandedAgent === agent.key}
107
- onToggle={() => toggleAgent(agent.key)}
108
98
  onInstallAgent={mcp.installAgent}
109
99
  t={p}
110
100
  />
@@ -123,9 +113,6 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
123
113
  key={agent.key}
124
114
  agent={agent}
125
115
  agentStatus="detected"
126
- mcpStatus={mcp.status}
127
- expanded={expandedAgent === agent.key}
128
- onToggle={() => toggleAgent(agent.key)}
129
116
  onInstallAgent={mcp.installAgent}
130
117
  t={p}
131
118
  />
@@ -149,9 +136,6 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
149
136
  key={agent.key}
150
137
  agent={agent}
151
138
  agentStatus="notFound"
152
- mcpStatus={mcp.status}
153
- expanded={expandedAgent === agent.key}
154
- onToggle={() => toggleAgent(agent.key)}
155
139
  onInstallAgent={mcp.installAgent}
156
140
  t={p}
157
141
  />
@@ -239,129 +223,55 @@ function SkillRow({ skill, onToggle }: { skill: SkillInfo; onToggle: (name: stri
239
223
  );
240
224
  }
241
225
 
242
- /* ── Agent Card ── */
226
+ /* ── Agent Card (compact — no snippet, config viewing is in Settings) ── */
243
227
 
244
- function AgentCard({ agent, agentStatus, mcpStatus, expanded, onToggle, onInstallAgent, t }: {
228
+ function AgentCard({ agent, agentStatus, onInstallAgent, t }: {
245
229
  agent: AgentInfo;
246
230
  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>;
231
+ onInstallAgent: (key: string) => Promise<boolean>;
251
232
  t: Record<string, any>;
252
233
  }) {
253
- const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
254
- const [copied, setCopied] = useState(false);
255
234
  const [installing, setInstalling] = useState(false);
256
235
  const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
257
236
 
258
237
  const dot = agentStatus === 'connected' ? 'bg-emerald-500' : agentStatus === 'detected' ? 'bg-amber-500' : 'bg-zinc-400';
259
238
 
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
239
  const handleInstall = async () => {
271
240
  setInstalling(true);
272
241
  setResult(null);
273
242
  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
- }
243
+ setResult(ok
244
+ ? { type: 'success', text: `${agent.name} ${t.connected}` }
245
+ : { type: 'error', text: 'Install failed' });
279
246
  setInstalling(false);
280
247
  };
281
248
 
282
249
  return (
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
- )}
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>
346
258
 
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>
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
+ )}
351
268
 
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>
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>
365
275
  )}
366
276
  </div>
367
277
  );