@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
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
Loader2, ChevronDown, ChevronRight,
|
|
6
|
-
Plus, X, Search,
|
|
6
|
+
Plus, X, Search, Copy, Check,
|
|
7
7
|
} from 'lucide-react';
|
|
8
8
|
import { apiFetch } from '@/lib/api';
|
|
9
9
|
import { useMcpDataOptional } from '@/hooks/useMcpData';
|
|
10
|
+
import { copyToClipboard } from '@/lib/clipboard';
|
|
10
11
|
import type { SkillInfo, McpSkillsSectionProps } from './types';
|
|
11
12
|
import SkillRow from './McpSkillRow';
|
|
12
13
|
import SkillCreateForm from './McpSkillCreateForm';
|
|
@@ -24,7 +25,7 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
24
25
|
const [createError, setCreateError] = useState('');
|
|
25
26
|
|
|
26
27
|
const [search, setSearch] = useState('');
|
|
27
|
-
const [builtinCollapsed, setBuiltinCollapsed] = useState(
|
|
28
|
+
const [builtinCollapsed, setBuiltinCollapsed] = useState(false);
|
|
28
29
|
const [editing, setEditing] = useState<string | null>(null);
|
|
29
30
|
const [editContent, setEditContent] = useState('');
|
|
30
31
|
const [editError, setEditError] = useState('');
|
|
@@ -354,6 +355,91 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
354
355
|
{m?.addSkill ?? '+ Add Skill'}
|
|
355
356
|
</button>
|
|
356
357
|
)}
|
|
358
|
+
|
|
359
|
+
{/* CLI install hint with agent selector */}
|
|
360
|
+
<SkillCliHint
|
|
361
|
+
agents={mcp?.agents ?? []}
|
|
362
|
+
skillName={(() => {
|
|
363
|
+
const mindosEnabled = skills.find(s => s.name === 'mindos')?.enabled ?? true;
|
|
364
|
+
return mindosEnabled ? 'mindos' : 'mindos-zh';
|
|
365
|
+
})()}
|
|
366
|
+
m={m}
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* ── Skill CLI Install Hint ── */
|
|
373
|
+
|
|
374
|
+
function SkillCliHint({ agents, skillName, m }: {
|
|
375
|
+
agents: { key: string; name: string; present?: boolean; installed?: boolean }[];
|
|
376
|
+
skillName: string;
|
|
377
|
+
m: Record<string, any> | undefined;
|
|
378
|
+
}) {
|
|
379
|
+
const [selectedAgent, setSelectedAgent] = useState('claude-code');
|
|
380
|
+
const [copied, setCopied] = useState(false);
|
|
381
|
+
|
|
382
|
+
const cmd = `npx skills add GeminiLight/MindOS --skill ${skillName} -a ${selectedAgent} -g -y`;
|
|
383
|
+
const skillPath = `~/.agents/skills/${skillName}/SKILL.md`;
|
|
384
|
+
|
|
385
|
+
const handleCopy = async () => {
|
|
386
|
+
const ok = await copyToClipboard(cmd);
|
|
387
|
+
if (ok) { setCopied(true); setTimeout(() => setCopied(false), 2000); }
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// Group agents: connected first, then detected, then not found
|
|
391
|
+
const connected = agents.filter(a => a.present && a.installed);
|
|
392
|
+
const detected = agents.filter(a => a.present && !a.installed);
|
|
393
|
+
const notFound = agents.filter(a => !a.present);
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<div className="border-t border-border pt-3 mt-3 space-y-2.5">
|
|
397
|
+
<p className="text-2xs font-medium text-muted-foreground">
|
|
398
|
+
{m?.cliInstallHint ?? 'Install via CLI:'}
|
|
399
|
+
</p>
|
|
400
|
+
|
|
401
|
+
{/* Agent selector */}
|
|
402
|
+
<div className="relative">
|
|
403
|
+
<select
|
|
404
|
+
value={selectedAgent}
|
|
405
|
+
onChange={(e) => setSelectedAgent(e.target.value)}
|
|
406
|
+
className="w-full appearance-none px-2.5 py-1.5 pr-7 text-2xs rounded-md border border-border bg-background text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
407
|
+
>
|
|
408
|
+
{connected.length > 0 && (
|
|
409
|
+
<optgroup label={m?.connectedGroup ?? 'Connected'}>
|
|
410
|
+
{connected.map(a => <option key={a.key} value={a.key}>✓ {a.name}</option>)}
|
|
411
|
+
</optgroup>
|
|
412
|
+
)}
|
|
413
|
+
{detected.length > 0 && (
|
|
414
|
+
<optgroup label={m?.detectedGroup ?? 'Detected'}>
|
|
415
|
+
{detected.map(a => <option key={a.key} value={a.key}>○ {a.name}</option>)}
|
|
416
|
+
</optgroup>
|
|
417
|
+
)}
|
|
418
|
+
{notFound.length > 0 && (
|
|
419
|
+
<optgroup label={m?.notFoundGroup ?? 'Not Installed'}>
|
|
420
|
+
{notFound.map(a => <option key={a.key} value={a.key}>· {a.name}</option>)}
|
|
421
|
+
</optgroup>
|
|
422
|
+
)}
|
|
423
|
+
</select>
|
|
424
|
+
<ChevronDown size={12} className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none" />
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
{/* Command */}
|
|
428
|
+
<div className="flex items-center gap-1.5">
|
|
429
|
+
<code className="flex-1 text-[10px] font-mono bg-muted/50 border border-border rounded-lg px-2.5 py-2 text-muted-foreground select-all overflow-x-auto whitespace-nowrap">
|
|
430
|
+
{cmd}
|
|
431
|
+
</code>
|
|
432
|
+
<button onClick={handleCopy}
|
|
433
|
+
className="p-1.5 rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0">
|
|
434
|
+
{copied ? <Check size={11} /> : <Copy size={11} />}
|
|
435
|
+
</button>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
{/* Path hint */}
|
|
439
|
+
<p className="text-2xs text-muted-foreground">
|
|
440
|
+
{m?.skillPathHint ?? 'Skill files installed at:'}{' '}
|
|
441
|
+
<code className="font-mono text-[10px] bg-muted px-1 py-0.5 rounded">{skillPath}</code>
|
|
442
|
+
</p>
|
|
357
443
|
</div>
|
|
358
444
|
);
|
|
359
445
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState, useMemo, useRef, useEffect } from 'react';
|
|
2
|
+
import { Loader2, ChevronDown, Copy, Check, Monitor, Globe, AlertCircle, RotateCcw, RefreshCw } from 'lucide-react';
|
|
2
3
|
import { useMcpDataOptional } from '@/hooks/useMcpData';
|
|
3
|
-
import
|
|
4
|
+
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
5
|
+
import { copyToClipboard } from '@/lib/clipboard';
|
|
6
|
+
import { apiFetch } from '@/lib/api';
|
|
7
|
+
import type { McpTabProps, McpStatus, AgentInfo } from './types';
|
|
4
8
|
import AgentInstall from './McpAgentInstall';
|
|
5
9
|
import SkillsSection from './McpSkillsSection';
|
|
6
10
|
|
|
@@ -13,6 +17,15 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
13
17
|
const mcp = useMcpDataOptional();
|
|
14
18
|
const m = t.settings?.mcp;
|
|
15
19
|
|
|
20
|
+
const [restarting, setRestarting] = useState(false);
|
|
21
|
+
const [selectedAgent, setSelectedAgent] = useState('');
|
|
22
|
+
const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
|
|
23
|
+
const [copied, setCopied] = useState(false);
|
|
24
|
+
const restartPollRef = useRef<ReturnType<typeof setInterval>>(undefined);
|
|
25
|
+
|
|
26
|
+
// Cleanup restart poll on unmount
|
|
27
|
+
useEffect(() => () => clearInterval(restartPollRef.current), []);
|
|
28
|
+
|
|
16
29
|
if (!mcp || mcp.loading) {
|
|
17
30
|
return (
|
|
18
31
|
<div className="flex justify-center py-8">
|
|
@@ -21,29 +34,62 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
21
34
|
);
|
|
22
35
|
}
|
|
23
36
|
|
|
37
|
+
const connectedAgents = mcp.agents.filter(a => a.present && a.installed);
|
|
38
|
+
const detectedAgents = mcp.agents.filter(a => a.present && !a.installed);
|
|
39
|
+
const notFoundAgents = mcp.agents.filter(a => !a.present);
|
|
40
|
+
|
|
41
|
+
// Auto-select first agent if none selected
|
|
42
|
+
const effectiveSelected = selectedAgent || (mcp.agents[0]?.key ?? '');
|
|
43
|
+
const currentAgent = mcp.agents.find(a => a.key === effectiveSelected);
|
|
44
|
+
|
|
24
45
|
return (
|
|
25
46
|
<div className="space-y-6">
|
|
26
|
-
{/* Server status
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
{/* Server status with restart */}
|
|
48
|
+
<McpStatusCard
|
|
49
|
+
status={mcp.status}
|
|
50
|
+
restarting={restarting}
|
|
51
|
+
onRestart={async () => {
|
|
52
|
+
setRestarting(true);
|
|
53
|
+
try { await apiFetch('/api/mcp/restart', { method: 'POST' }); } catch {}
|
|
54
|
+
const deadline = Date.now() + 60_000;
|
|
55
|
+
clearInterval(restartPollRef.current);
|
|
56
|
+
restartPollRef.current = setInterval(async () => {
|
|
57
|
+
if (Date.now() > deadline) { clearInterval(restartPollRef.current); setRestarting(false); return; }
|
|
58
|
+
try {
|
|
59
|
+
const s = await apiFetch<McpStatus>('/api/mcp/status', { timeout: 3000 });
|
|
60
|
+
if (s.running) { clearInterval(restartPollRef.current); setRestarting(false); mcp.refresh(); }
|
|
61
|
+
} catch {}
|
|
62
|
+
}, 3000);
|
|
63
|
+
}}
|
|
64
|
+
onRefresh={mcp.refresh}
|
|
65
|
+
m={m}
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
{/* MCP Config Viewer */}
|
|
69
|
+
{mcp.agents.length > 0 && (
|
|
70
|
+
<div>
|
|
71
|
+
<h3 className="text-sm font-medium text-foreground mb-3">MCP</h3>
|
|
72
|
+
<AgentConfigViewer
|
|
73
|
+
connectedAgents={connectedAgents}
|
|
74
|
+
detectedAgents={detectedAgents}
|
|
75
|
+
notFoundAgents={notFoundAgents}
|
|
76
|
+
currentAgent={currentAgent ?? null}
|
|
77
|
+
mcpStatus={mcp.status}
|
|
78
|
+
selectedAgent={effectiveSelected}
|
|
79
|
+
onSelectAgent={(key) => setSelectedAgent(key)}
|
|
80
|
+
transport={transport}
|
|
81
|
+
onTransportChange={setTransport}
|
|
82
|
+
copied={copied}
|
|
83
|
+
onCopy={async (snippet) => {
|
|
84
|
+
const ok = await copyToClipboard(snippet);
|
|
85
|
+
if (ok) { setCopied(true); setTimeout(() => setCopied(false), 2000); }
|
|
86
|
+
}}
|
|
87
|
+
m={m}
|
|
88
|
+
/>
|
|
43
89
|
</div>
|
|
44
90
|
)}
|
|
45
91
|
|
|
46
|
-
{/* Skills
|
|
92
|
+
{/* Skills */}
|
|
47
93
|
<div>
|
|
48
94
|
<h3 className="text-sm font-medium text-foreground mb-3">{m?.skillsTitle ?? 'Skills'}</h3>
|
|
49
95
|
<SkillsSection t={t} />
|
|
@@ -57,3 +103,195 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
57
103
|
</div>
|
|
58
104
|
);
|
|
59
105
|
}
|
|
106
|
+
|
|
107
|
+
/* ── MCP Status Card ── */
|
|
108
|
+
|
|
109
|
+
function McpStatusCard({ status, restarting, onRestart, onRefresh, m }: {
|
|
110
|
+
status: McpStatus | null;
|
|
111
|
+
restarting: boolean;
|
|
112
|
+
onRestart: () => void;
|
|
113
|
+
onRefresh: () => void;
|
|
114
|
+
m: Record<string, any> | undefined;
|
|
115
|
+
}) {
|
|
116
|
+
if (!status) return null;
|
|
117
|
+
return (
|
|
118
|
+
<div className="rounded-xl border border-border bg-card p-4 flex items-center justify-between">
|
|
119
|
+
<div className="flex items-center gap-2.5 text-xs">
|
|
120
|
+
{restarting ? (
|
|
121
|
+
<>
|
|
122
|
+
<Loader2 size={12} className="animate-spin" style={{ color: 'var(--amber)' }} />
|
|
123
|
+
<span style={{ color: 'var(--amber)' }}>{m?.restarting ?? 'Restarting...'}</span>
|
|
124
|
+
</>
|
|
125
|
+
) : (
|
|
126
|
+
<>
|
|
127
|
+
<span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${status.running ? 'bg-success' : 'bg-muted-foreground'}`} />
|
|
128
|
+
<span className="text-foreground font-medium">
|
|
129
|
+
{status.running ? (m?.running ?? 'Running') : (m?.stopped ?? 'Stopped')}
|
|
130
|
+
</span>
|
|
131
|
+
{status.running && (
|
|
132
|
+
<>
|
|
133
|
+
<span className="text-muted-foreground">·</span>
|
|
134
|
+
<span className="font-mono text-muted-foreground">{status.endpoint}</span>
|
|
135
|
+
<span className="text-muted-foreground">·</span>
|
|
136
|
+
<span className="text-muted-foreground">{status.toolCount} tools</span>
|
|
137
|
+
</>
|
|
138
|
+
)}
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
<div className="flex items-center gap-2">
|
|
143
|
+
{!status.running && !restarting && (
|
|
144
|
+
<button onClick={onRestart}
|
|
145
|
+
className="flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg font-medium text-white transition-colors"
|
|
146
|
+
style={{ background: 'var(--amber)' }}>
|
|
147
|
+
<RotateCcw size={12} /> {m?.restart ?? 'Restart'}
|
|
148
|
+
</button>
|
|
149
|
+
)}
|
|
150
|
+
<button onClick={onRefresh}
|
|
151
|
+
className="p-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
152
|
+
<RefreshCw size={12} />
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ── Agent Config Viewer (dropdown + snippet) ── */
|
|
160
|
+
|
|
161
|
+
function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, currentAgent, mcpStatus, selectedAgent, onSelectAgent, transport, onTransportChange, copied, onCopy, m }: {
|
|
162
|
+
connectedAgents: AgentInfo[];
|
|
163
|
+
detectedAgents: AgentInfo[];
|
|
164
|
+
notFoundAgents: AgentInfo[];
|
|
165
|
+
currentAgent: AgentInfo | null;
|
|
166
|
+
mcpStatus: McpStatus | null;
|
|
167
|
+
selectedAgent: string;
|
|
168
|
+
onSelectAgent: (key: string) => void;
|
|
169
|
+
transport: 'stdio' | 'http';
|
|
170
|
+
onTransportChange: (t: 'stdio' | 'http') => void;
|
|
171
|
+
copied: boolean;
|
|
172
|
+
onCopy: (snippet: string) => void;
|
|
173
|
+
m: Record<string, any> | undefined;
|
|
174
|
+
}) {
|
|
175
|
+
const snippet = useMemo(
|
|
176
|
+
() => currentAgent ? generateSnippet(currentAgent, mcpStatus, transport) : null,
|
|
177
|
+
[currentAgent, mcpStatus, transport]
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div className="rounded-xl border border-border bg-card p-4 space-y-3">
|
|
182
|
+
{/* Agent selector */}
|
|
183
|
+
<div className="relative">
|
|
184
|
+
<select
|
|
185
|
+
value={selectedAgent}
|
|
186
|
+
onChange={(e) => onSelectAgent(e.target.value)}
|
|
187
|
+
className="w-full appearance-none px-3 py-2 pr-8 text-xs rounded-lg border border-border bg-background text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
188
|
+
>
|
|
189
|
+
{connectedAgents.length > 0 && (
|
|
190
|
+
<optgroup label={m?.connectedGroup ?? 'Connected'}>
|
|
191
|
+
{connectedAgents.map(a => (
|
|
192
|
+
<option key={a.key} value={a.key}>
|
|
193
|
+
✓ {a.name} — {a.transport ?? 'stdio'} · {a.scope ?? 'global'}
|
|
194
|
+
</option>
|
|
195
|
+
))}
|
|
196
|
+
</optgroup>
|
|
197
|
+
)}
|
|
198
|
+
{detectedAgents.length > 0 && (
|
|
199
|
+
<optgroup label={m?.detectedGroup ?? 'Detected (not configured)'}>
|
|
200
|
+
{detectedAgents.map(a => (
|
|
201
|
+
<option key={a.key} value={a.key}>
|
|
202
|
+
○ {a.name} — {m?.notConfigured ?? 'not configured'}
|
|
203
|
+
</option>
|
|
204
|
+
))}
|
|
205
|
+
</optgroup>
|
|
206
|
+
)}
|
|
207
|
+
{notFoundAgents.length > 0 && (
|
|
208
|
+
<optgroup label={m?.notFoundGroup ?? 'Not Installed'}>
|
|
209
|
+
{notFoundAgents.map(a => (
|
|
210
|
+
<option key={a.key} value={a.key}>
|
|
211
|
+
· {a.name}
|
|
212
|
+
</option>
|
|
213
|
+
))}
|
|
214
|
+
</optgroup>
|
|
215
|
+
)}
|
|
216
|
+
</select>
|
|
217
|
+
<ChevronDown size={14} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none" />
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{currentAgent && (
|
|
221
|
+
<>
|
|
222
|
+
{/* Agent status badge */}
|
|
223
|
+
<div className="flex items-center gap-2">
|
|
224
|
+
{currentAgent.present && currentAgent.installed ? (
|
|
225
|
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-2xs font-medium bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
|
|
226
|
+
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 inline-block" />
|
|
227
|
+
{m?.tagConnected ?? 'Connected'}
|
|
228
|
+
</span>
|
|
229
|
+
) : currentAgent.present && !currentAgent.installed ? (
|
|
230
|
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-2xs font-medium" style={{ background: 'var(--amber-subtle, rgba(200,135,58,0.1))', color: 'var(--amber)' }}>
|
|
231
|
+
<span className="w-1.5 h-1.5 rounded-full inline-block" style={{ background: 'var(--amber)' }} />
|
|
232
|
+
{m?.tagDetected ?? 'Detected — not configured'}
|
|
233
|
+
</span>
|
|
234
|
+
) : (
|
|
235
|
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-2xs font-medium bg-muted text-muted-foreground">
|
|
236
|
+
<span className="w-1.5 h-1.5 rounded-full bg-zinc-400 inline-block" />
|
|
237
|
+
{m?.tagNotInstalled ?? 'Not installed'}
|
|
238
|
+
</span>
|
|
239
|
+
)}
|
|
240
|
+
{currentAgent.transport && (
|
|
241
|
+
<span className="px-1.5 py-0.5 rounded text-2xs bg-muted text-muted-foreground">{currentAgent.transport}</span>
|
|
242
|
+
)}
|
|
243
|
+
{currentAgent.scope && (
|
|
244
|
+
<span className="px-1.5 py-0.5 rounded text-2xs bg-muted text-muted-foreground">{currentAgent.scope}</span>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{/* Transport toggle */}
|
|
249
|
+
<div className="flex items-center rounded-lg border border-border overflow-hidden w-fit">
|
|
250
|
+
<button
|
|
251
|
+
onClick={() => onTransportChange('stdio')}
|
|
252
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs transition-colors ${
|
|
253
|
+
transport === 'stdio' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
|
|
254
|
+
}`}
|
|
255
|
+
>
|
|
256
|
+
<Monitor size={12} /> {m?.transportLocal ?? 'Local (stdio)'}
|
|
257
|
+
</button>
|
|
258
|
+
<button
|
|
259
|
+
onClick={() => onTransportChange('http')}
|
|
260
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs transition-colors ${
|
|
261
|
+
transport === 'http' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
|
|
262
|
+
}`}
|
|
263
|
+
>
|
|
264
|
+
<Globe size={12} /> {m?.transportRemote ?? 'Remote (HTTP)'}
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{/* Auth warning */}
|
|
269
|
+
{transport === 'http' && mcpStatus && !mcpStatus.authConfigured && (
|
|
270
|
+
<p className="flex items-center gap-1.5 text-xs" style={{ color: 'var(--amber)' }}>
|
|
271
|
+
<AlertCircle size={12} />
|
|
272
|
+
{m?.noAuthWarning ?? 'Auth not configured. Run `mindos token` to set up.'}
|
|
273
|
+
</p>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{/* Snippet */}
|
|
277
|
+
{snippet && (
|
|
278
|
+
<>
|
|
279
|
+
<pre className="text-[11px] font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre select-all max-h-[240px] overflow-y-auto">
|
|
280
|
+
{snippet.displaySnippet}
|
|
281
|
+
</pre>
|
|
282
|
+
<div className="flex items-center gap-3 text-xs">
|
|
283
|
+
<button onClick={() => onCopy(snippet.snippet)}
|
|
284
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0">
|
|
285
|
+
{copied ? <Check size={12} /> : <Copy size={12} />}
|
|
286
|
+
{copied ? (m?.copied ?? 'Copied!') : (m?.copyConfig ?? 'Copy config')}
|
|
287
|
+
</button>
|
|
288
|
+
<span className="text-muted-foreground">→</span>
|
|
289
|
+
<span className="font-mono text-muted-foreground truncate text-2xs">{snippet.path}</span>
|
|
290
|
+
</div>
|
|
291
|
+
</>
|
|
292
|
+
)}
|
|
293
|
+
</>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
@@ -29,6 +29,7 @@ type UpdateState = 'idle' | 'checking' | 'updating' | 'updated' | 'error' | 'tim
|
|
|
29
29
|
const CHANGELOG_URL = 'https://github.com/GeminiLight/MindOS/releases';
|
|
30
30
|
const POLL_INTERVAL = 3_000;
|
|
31
31
|
const POLL_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
32
|
+
const UPDATE_STATE_KEY = 'mindos_update_in_progress';
|
|
32
33
|
|
|
33
34
|
const STAGE_LABELS: Record<string, { en: string; zh: string }> = {
|
|
34
35
|
downloading: { en: 'Downloading update', zh: '下载更新' },
|
|
@@ -77,8 +78,6 @@ export function UpdateTab() {
|
|
|
77
78
|
}
|
|
78
79
|
}, [u]);
|
|
79
80
|
|
|
80
|
-
useEffect(() => { checkUpdate(); }, [checkUpdate]);
|
|
81
|
-
|
|
82
81
|
const cleanup = useCallback(() => {
|
|
83
82
|
clearInterval(pollRef.current);
|
|
84
83
|
clearTimeout(timeoutRef.current);
|
|
@@ -91,33 +90,14 @@ export function UpdateTab() {
|
|
|
91
90
|
setState('updated');
|
|
92
91
|
localStorage.removeItem('mindos_update_latest');
|
|
93
92
|
localStorage.removeItem('mindos_update_dismissed');
|
|
93
|
+
localStorage.removeItem(UPDATE_STATE_KEY);
|
|
94
94
|
window.dispatchEvent(new Event('mindos:update-dismissed'));
|
|
95
95
|
setTimeout(() => window.location.reload(), 2000);
|
|
96
96
|
}, [cleanup]);
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const handleUpdate = useCallback(async () => {
|
|
101
|
-
setState('updating');
|
|
102
|
-
setErrorMsg('');
|
|
103
|
-
setUpdateError(null);
|
|
104
|
-
setServerDown(false);
|
|
105
|
-
setStages([
|
|
106
|
-
{ id: 'downloading', status: 'pending' },
|
|
107
|
-
{ id: 'skills', status: 'pending' },
|
|
108
|
-
{ id: 'rebuilding', status: 'pending' },
|
|
109
|
-
{ id: 'restarting', status: 'pending' },
|
|
110
|
-
]);
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
await apiFetch('/api/update', { method: 'POST' });
|
|
114
|
-
} catch {
|
|
115
|
-
// Expected — server may die during update
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Poll update-status for stage progress
|
|
98
|
+
/** Start polling for update progress */
|
|
99
|
+
const startPolling = useCallback(() => {
|
|
119
100
|
pollRef.current = setInterval(async () => {
|
|
120
|
-
// Try status endpoint first (may fail when server is restarting)
|
|
121
101
|
try {
|
|
122
102
|
const status = await apiFetch<UpdateStatus>('/api/update-status', { timeout: 5000 });
|
|
123
103
|
setServerDown(false);
|
|
@@ -128,13 +108,13 @@ export function UpdateTab() {
|
|
|
128
108
|
|
|
129
109
|
if (status.stage === 'failed') {
|
|
130
110
|
cleanup();
|
|
111
|
+
localStorage.removeItem(UPDATE_STATE_KEY);
|
|
131
112
|
setUpdateError(status.error || 'Update failed');
|
|
132
113
|
setState('error');
|
|
133
114
|
return;
|
|
134
115
|
}
|
|
135
116
|
|
|
136
117
|
if (status.stage === 'done') {
|
|
137
|
-
// Verify version actually changed
|
|
138
118
|
try {
|
|
139
119
|
const data = await apiFetch<UpdateInfo>('/api/update-check');
|
|
140
120
|
if (data.current !== originalVersion.current) {
|
|
@@ -144,11 +124,11 @@ export function UpdateTab() {
|
|
|
144
124
|
} catch { /* new server may not be fully ready */ }
|
|
145
125
|
}
|
|
146
126
|
} catch {
|
|
147
|
-
// Server restarting —
|
|
127
|
+
// Server restarting — try update-check as fallback
|
|
148
128
|
setServerDown(true);
|
|
149
129
|
try {
|
|
150
130
|
const data = await apiFetch<UpdateInfo>('/api/update-check', { timeout: 5000 });
|
|
151
|
-
if (data.current !== originalVersion.current) {
|
|
131
|
+
if (data.current && data.current !== originalVersion.current) {
|
|
152
132
|
setStages(prev => prev.map(s => ({ ...s, status: 'done' as const })));
|
|
153
133
|
completeUpdate(data);
|
|
154
134
|
}
|
|
@@ -160,10 +140,68 @@ export function UpdateTab() {
|
|
|
160
140
|
|
|
161
141
|
timeoutRef.current = setTimeout(() => {
|
|
162
142
|
cleanup();
|
|
143
|
+
localStorage.removeItem(UPDATE_STATE_KEY);
|
|
163
144
|
setState('timeout');
|
|
164
145
|
}, POLL_TIMEOUT);
|
|
165
146
|
}, [cleanup, completeUpdate]);
|
|
166
147
|
|
|
148
|
+
// On mount: check if an update was in progress (survives page reload / white screen)
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const savedState = localStorage.getItem(UPDATE_STATE_KEY);
|
|
151
|
+
if (savedState) {
|
|
152
|
+
try {
|
|
153
|
+
const { originalVer } = JSON.parse(savedState);
|
|
154
|
+
originalVersion.current = originalVer;
|
|
155
|
+
setState('updating');
|
|
156
|
+
setServerDown(true);
|
|
157
|
+
setStages([
|
|
158
|
+
{ id: 'downloading', status: 'done' },
|
|
159
|
+
{ id: 'skills', status: 'done' },
|
|
160
|
+
{ id: 'rebuilding', status: 'done' },
|
|
161
|
+
{ id: 'restarting', status: 'running' },
|
|
162
|
+
]);
|
|
163
|
+
startPolling();
|
|
164
|
+
} catch {
|
|
165
|
+
localStorage.removeItem(UPDATE_STATE_KEY);
|
|
166
|
+
checkUpdate();
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
checkUpdate();
|
|
170
|
+
}
|
|
171
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
useEffect(() => cleanup, [cleanup]);
|
|
175
|
+
|
|
176
|
+
const handleUpdate = useCallback(async () => {
|
|
177
|
+
setState('updating');
|
|
178
|
+
setErrorMsg('');
|
|
179
|
+
setUpdateError(null);
|
|
180
|
+
setServerDown(false);
|
|
181
|
+
setStages([
|
|
182
|
+
{ id: 'downloading', status: 'pending' },
|
|
183
|
+
{ id: 'skills', status: 'pending' },
|
|
184
|
+
{ id: 'rebuilding', status: 'pending' },
|
|
185
|
+
{ id: 'restarting', status: 'pending' },
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
// Persist update state to localStorage — survives process restart / page reload
|
|
189
|
+
localStorage.setItem(UPDATE_STATE_KEY, JSON.stringify({
|
|
190
|
+
originalVer: originalVersion.current || info?.current,
|
|
191
|
+
startedAt: Date.now(),
|
|
192
|
+
}));
|
|
193
|
+
// Notify UpdateOverlay (same-tab, storage event doesn't fire for same-tab writes)
|
|
194
|
+
window.dispatchEvent(new Event('mindos:update-started'));
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await apiFetch('/api/update', { method: 'POST' });
|
|
198
|
+
} catch {
|
|
199
|
+
// Expected — server may die during update
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
startPolling();
|
|
203
|
+
}, [startPolling, info]);
|
|
204
|
+
|
|
167
205
|
const handleRetry = useCallback(() => {
|
|
168
206
|
setUpdateError(null);
|
|
169
207
|
handleUpdate();
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -352,6 +352,8 @@ export const en = {
|
|
|
352
352
|
skillBuiltin: 'Built-in',
|
|
353
353
|
skillUser: 'Custom',
|
|
354
354
|
addSkill: '+ Add Skill',
|
|
355
|
+
cliInstallHint: 'Install via CLI:',
|
|
356
|
+
skillPathHint: 'Skill files installed at:',
|
|
355
357
|
deleteSkill: 'Delete',
|
|
356
358
|
editSkill: 'Edit',
|
|
357
359
|
saveSkill: 'Save',
|
|
@@ -415,6 +417,8 @@ export const en = {
|
|
|
415
417
|
mcpServer: 'MCP Server',
|
|
416
418
|
running: 'Running',
|
|
417
419
|
stopped: 'Not running',
|
|
420
|
+
restarting: 'Restarting...',
|
|
421
|
+
restart: 'Restart',
|
|
418
422
|
onPort: (port: number) => `on :${port}`,
|
|
419
423
|
refresh: 'Refresh',
|
|
420
424
|
refreshing: 'Refreshing...',
|
package/app/lib/i18n-zh.ts
CHANGED
|
@@ -377,6 +377,8 @@ export const zh = {
|
|
|
377
377
|
skillBuiltin: '内置',
|
|
378
378
|
skillUser: '自定义',
|
|
379
379
|
addSkill: '+ 添加 Skill',
|
|
380
|
+
cliInstallHint: '通过命令行安装:',
|
|
381
|
+
skillPathHint: 'Skill 文件安装路径:',
|
|
380
382
|
deleteSkill: '删除',
|
|
381
383
|
editSkill: '编辑',
|
|
382
384
|
saveSkill: '保存',
|
|
@@ -440,6 +442,8 @@ export const zh = {
|
|
|
440
442
|
mcpServer: 'MCP 服务器',
|
|
441
443
|
running: '运行中',
|
|
442
444
|
stopped: '未运行',
|
|
445
|
+
restarting: '重启中...',
|
|
446
|
+
restart: '重启',
|
|
443
447
|
onPort: (port: number) => `端口 :${port}`,
|
|
444
448
|
refresh: '刷新',
|
|
445
449
|
refreshing: '刷新中...',
|
package/app/lib/mcp-agents.ts
CHANGED
|
@@ -3,6 +3,16 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
|
|
6
|
+
/** Parse JSONC — strips single-line (//) and block comments before JSON.parse */
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
function parseJsonc(text: string): any {
|
|
9
|
+
// Strip single-line comments (not inside strings)
|
|
10
|
+
let stripped = text.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*$)/gm, (m, g) => g ? '' : m);
|
|
11
|
+
// Strip block comments
|
|
12
|
+
stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
13
|
+
return JSON.parse(stripped);
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
export function expandHome(p: string): string {
|
|
7
17
|
return p.startsWith('~/') ? path.resolve(os.homedir(), p.slice(2)) : p;
|
|
8
18
|
}
|
|
@@ -91,11 +101,11 @@ export const MCP_AGENTS: Record<string, AgentDef> = {
|
|
|
91
101
|
'codebuddy': {
|
|
92
102
|
name: 'CodeBuddy',
|
|
93
103
|
project: null,
|
|
94
|
-
global: '~/.
|
|
104
|
+
global: '~/.codebuddy/mcp.json',
|
|
95
105
|
key: 'mcpServers',
|
|
96
106
|
preferredTransport: 'stdio',
|
|
97
|
-
presenceCli: '
|
|
98
|
-
presenceDirs: ['~/.
|
|
107
|
+
presenceCli: 'codebuddy',
|
|
108
|
+
presenceDirs: ['~/.codebuddy/'],
|
|
99
109
|
},
|
|
100
110
|
'iflow-cli': {
|
|
101
111
|
name: 'iFlow CLI',
|
|
@@ -227,7 +237,7 @@ export function detectInstalled(agentKey: string): { installed: boolean; scope?:
|
|
|
227
237
|
}
|
|
228
238
|
} else {
|
|
229
239
|
// JSON format (default)
|
|
230
|
-
const config =
|
|
240
|
+
const config = parseJsonc(content);
|
|
231
241
|
const servers = config[agent.key];
|
|
232
242
|
if (servers?.mindos) {
|
|
233
243
|
const entry = servers.mindos;
|
package/app/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
3
|
+
import "./.next/dev/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|