@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 +2 -2
- package/README_zh.md +1 -1
- package/app/app/api/mcp/install/route.ts +8 -1
- package/app/app/api/mcp/restart/route.ts +88 -0
- package/app/app/layout.tsx +2 -0
- package/app/components/UpdateOverlay.tsx +124 -0
- package/app/components/help/HelpContent.tsx +10 -7
- package/app/components/panels/AgentsPanel.tsx +32 -122
- package/app/components/settings/McpSkillsSection.tsx +88 -2
- package/app/components/settings/McpTab.tsx +258 -20
- package/app/components/settings/UpdateTab.tsx +65 -27
- package/app/lib/i18n-en.ts +4 -0
- package/app/lib/i18n-zh.ts +4 -0
- package/app/lib/mcp-agents.ts +14 -4
- package/app/next-env.d.ts +1 -1
- package/bin/lib/mcp-install.js +3 -3
- package/bin/lib/utils.js +12 -0
- package/package.json +1 -1
- package/scripts/setup.js +10 -3
- package/app/components/settings/AgentsTab.tsx +0 -240
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,
|
|
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
|
|
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
|
|
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 =
|
|
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
|
+
}
|
package/app/app/layout.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
126
|
-
|
|
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
|
|
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
|
|
4
|
-
import { Loader2, RefreshCw, ChevronDown, ChevronRight, CheckCircle2, AlertCircle,
|
|
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,
|
|
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,
|
|
228
|
+
function AgentCard({ agent, agentStatus, onInstallAgent, t }: {
|
|
245
229
|
agent: AgentInfo;
|
|
246
230
|
agentStatus: 'connected' | 'detected' | 'notFound';
|
|
247
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
);
|