@geminilight/mindos 0.5.51 → 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/app/app/layout.tsx +2 -0
- package/app/components/UpdateOverlay.tsx +124 -0
- package/app/components/help/HelpContent.tsx +10 -7
- package/app/components/settings/McpSkillsSection.tsx +88 -2
- package/app/components/settings/McpTab.tsx +26 -0
- package/app/components/settings/UpdateTab.tsx +65 -27
- package/app/lib/i18n-en.ts +2 -0
- package/app/lib/i18n-zh.ts +2 -0
- package/app/next-env.d.ts +1 -1
- package/package.json +1 -1
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>
|
|
@@ -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
|
}
|
|
@@ -219,6 +219,32 @@ function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, cu
|
|
|
219
219
|
|
|
220
220
|
{currentAgent && (
|
|
221
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
|
+
|
|
222
248
|
{/* Transport toggle */}
|
|
223
249
|
<div className="flex items-center rounded-lg border border-border overflow-hidden w-fit">
|
|
224
250
|
<button
|
|
@@ -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',
|
package/app/lib/i18n-zh.ts
CHANGED
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.
|
package/package.json
CHANGED