@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.
@@ -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(true);
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 { Loader2 } from 'lucide-react';
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 type { McpTabProps } from './types';
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 summary (minimal — full status is in sidebar AgentsPanel) */}
27
- {mcp.status && (
28
- <div className="rounded-xl border p-4 space-y-2" style={{ borderColor: 'var(--border)', background: 'var(--card)' }}>
29
- <div className="flex items-center gap-2.5 text-xs">
30
- <span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${mcp.status.running ? 'bg-success' : 'bg-muted-foreground'}`} />
31
- <span className="text-foreground font-medium">
32
- {mcp.status.running ? (m?.running ?? 'Running') : (m?.stopped ?? 'Stopped')}
33
- </span>
34
- {mcp.status.running && (
35
- <>
36
- <span className="text-muted-foreground">·</span>
37
- <span className="font-mono text-muted-foreground">{mcp.status.endpoint}</span>
38
- <span className="text-muted-foreground">·</span>
39
- <span className="text-muted-foreground">{mcp.status.toolCount} tools</span>
40
- </>
41
- )}
42
- </div>
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 (full CRUD — search, edit, delete, create, language switch) */}
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
- useEffect(() => cleanup, [cleanup]);
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 — also try update-check as fallback
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();
@@ -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...',
@@ -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: '刷新中...',
@@ -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: '~/.claude-internal/.claude.json',
104
+ global: '~/.codebuddy/mcp.json',
95
105
  key: 'mcpServers',
96
106
  preferredTransport: 'stdio',
97
- presenceCli: 'claude-internal',
98
- presenceDirs: ['~/.claude-internal/'],
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 = JSON.parse(content);
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.