@geminilight/mindos 0.5.51 → 0.5.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,7 @@ MindOS is where you think, and where your AI agents act — a local-first knowle
40
40
  >
41
41
  > **✨ Try it now:** After installation, give these a try:
42
42
  > ```
43
- > Read my MindOS knowledge base, see what's inside, then help me write my self-introduction into Profile.
43
+ > Here's my resume, read it and organize my info into MindOS.
44
44
  > ```
45
45
  > ```
46
46
  > Help me distill the experience from this conversation into MindOS as a reusable SOP.
@@ -51,19 +51,19 @@ MindOS is where you think, and where your AI agents act — a local-first knowle
51
51
 
52
52
  ## 🧠 Human-AI Shared Mind
53
53
 
54
- > No more fragmented memory, no more black-box behavior, no more lost experience.
54
+ > You shape AI through thinking, AI empowers you through execution. Human and AI, growing together in one shared brain.
55
55
 
56
56
  **1. Global Sync — Breaking Memory Silos**
57
57
 
58
- Each Agent keeps its own memory switching tools means manually hauling context. **MindOS lets all Agents share one knowledge base via MCP and Skills record once, reuse everywhere.**
58
+ Switch tools or start a new chat and you're re-transporting context, scattering knowledge. **With a built-in MCP server (20+ tools), MindOS connects all Agents to your core knowledge base with zero config. Record profile and project memory once to empower all AI tools.**
59
59
 
60
- **2. Transparent & Controllable — No More Black Boxes**
60
+ **2. Transparent & Controllable — No Black Boxes**
61
61
 
62
- What did your Agent remember? Is it even correct? You have no way to know. **MindOS saves every read/write as local plain text humans can audit, correct, and delete in the GUI.**
62
+ Agent memory locked in black boxes makes reasoning unauditable, erasing trust as hallucinations compound. **MindOS saves every retrieval, reflection & action as local plain text. You hold absolute mind-correction rights with a full GUI to recalibrate Agents anytime.**
63
63
 
64
- **3. Symbiotic Evolution — Experience Flows Back as Instructions**
64
+ **3. Symbiotic Evolution — Experience Flows Back As Instructions**
65
65
 
66
- All that experience from your conversations gone the moment you close the window. **MindOS auto-distills conversation experience into Skills/SOPs. Notes are instructions. The knowledge base gets better with use.**
66
+ You express preferences but the next chat starts from zero, leaving your thinking useless for AI. **MindOS auto-distills every thought into your knowledge base. Clarify your standards through interaction and sharpen your cognition with each iteration—AI will never repeat the same mistake.**
67
67
 
68
68
  > **Foundation:** Local-first by default — all data stays in local plain text for privacy, ownership, and speed.
69
69
 
package/README_zh.md CHANGED
@@ -40,7 +40,7 @@ MindOS 是你思考的地方,也是 AI Agent 行动的起点——一个你和
40
40
  >
41
41
  > **✨ 立即体验:** 安装完成后,不妨试试:
42
42
  > ```
43
- > 读一下我的 MindOS 知识库,看看里面有什么,然后帮我把自我介绍写进 Profile。
43
+ > 这是我的简历,读一下,把我的信息整理到 MindOS 里。
44
44
  > ```
45
45
  > ```
46
46
  > 帮我把这次对话的经验沉淀到 MindOS,形成一个可复用的工作流。
@@ -51,19 +51,19 @@ MindOS 是你思考的地方,也是 AI Agent 行动的起点——一个你和
51
51
 
52
52
  ## 🧠 人机共享心智
53
53
 
54
- > 记忆不再割裂,行为不再黑箱,经验不再断流。
54
+ > 你在思考中塑造 AI,AI 在执行中反哺你。人和 AI,在同一个大脑里共同成长。
55
55
 
56
56
  **1. 全局同步 — 打破记忆割裂**
57
57
 
58
- 多个 Agent 各记各的,切换工具靠人工搬运上下文。**MindOS 通过 MCP Skill 让所有 Agent 共享同一份知识库——一处记录,全局复用。**
58
+ 切换工具带来上下文割裂,个人深度背景散落各处,导致知识无法复用。**MindOS 内置 MCP Server (支持 20+ 工具),所有 Agent 零配置直连核心知识库。项目记忆与 SOP 仅需一处记录,全局赋能所有 AI 工具。**
59
59
 
60
60
  **2. 透明可控 — 消除记忆黑箱**
61
61
 
62
- Agent 记了什么、记对没有,用户无从知晓。**MindOS 将每次读写沉淀为本地纯文本,人类可在 GUI 中审查、修正、删除。**
62
+ Agent 记忆锁在黑箱中,推理无法审查,错误极难纠正且幻觉持续累积。**MindOS 将每次检索与执行沉淀为本地纯文本,提供完整的审查干预界面,人类拥有绝对的心智纠偏权,随时校准 Agent 行为。**
63
63
 
64
64
  **3. 共生演进 — 经验回流为指令**
65
65
 
66
- 对话里攒下的经验,关掉窗口就散了。**MindOS 自动将对话经验沉淀为 Skill/SOP,笔记即指令,知识库越用越好。**
66
+ 反复表达偏好但新对话又从零开始,思考未变成 AI 的能力与自身的方法论。**MindOS 将每次思考自动沉淀为知识库。在交互中厘清想法与标准,下次应对更默契拒绝重复犯错,认知随每次沉淀变得锐利。**
67
67
 
68
68
  > **底层原则:** 默认本地优先,全部数据以本地纯文本保存,兼顾隐私、主权与性能。
69
69
 
@@ -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>
@@ -2,13 +2,13 @@
2
2
 
3
3
  import { useRef, useCallback, useState, useEffect } from 'react';
4
4
  import Link from 'next/link';
5
- import { FolderTree, Search, Settings, RefreshCw, Blocks, Bot, Compass, HelpCircle, ChevronLeft, ChevronRight } from 'lucide-react';
5
+ import { FolderTree, Search, Settings, RefreshCw, Blocks, Bot, Compass, HelpCircle, ChevronLeft, ChevronRight, Radio } from 'lucide-react';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
  import { DOT_COLORS, getStatusLevel } from './SyncStatusBar';
8
8
  import type { SyncStatus } from './settings/SyncTab';
9
9
  import Logo from './Logo';
10
10
 
11
- export type PanelId = 'files' | 'search' | 'plugins' | 'agents' | 'discover';
11
+ export type PanelId = 'files' | 'search' | 'echo' | 'plugins' | 'agents' | 'discover';
12
12
 
13
13
  export const RAIL_WIDTH_COLLAPSED = 48;
14
14
  export const RAIL_WIDTH_EXPANDED = 180;
@@ -157,6 +157,7 @@ export default function ActivityBar({
157
157
  {/* ── Middle: Core panel toggles ── */}
158
158
  <div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
159
159
  <RailButton icon={<FolderTree size={18} />} label={t.sidebar.files} active={activePanel === 'files'} expanded={expanded} onClick={() => toggle('files')} walkthroughId="files-panel" />
160
+ <RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => toggle('echo')} />
160
161
  <RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} walkthroughId="search-button" />
161
162
  <RailButton icon={<Blocks size={18} />} label={t.sidebar.plugins} active={activePanel === 'plugins'} expanded={expanded} onClick={() => toggle('plugins')} />
162
163
  <RailButton icon={<Bot size={18} />} label={t.sidebar.agents} active={activePanel === 'agents'} expanded={expanded} onClick={() => toggle('agents')} />
@@ -26,6 +26,7 @@ function getMaxDepth(nodes: FileNode[], current = 0): number {
26
26
  const DEFAULT_PANEL_WIDTH: Record<PanelId, number> = {
27
27
  files: 280,
28
28
  search: 280,
29
+ echo: 280,
29
30
  plugins: 280,
30
31
  agents: 280,
31
32
  discover: 280,
@@ -0,0 +1,121 @@
1
+ 'use client';
2
+
3
+ import { useMemo, useEffect } from 'react';
4
+ import { useMcpData } from '@/hooks/useMcpData';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+ import { useResizeDrag } from '@/hooks/useResizeDrag';
7
+ import AgentsPanelAgentDetail from '@/components/panels/AgentsPanelAgentDetail';
8
+ import { resolveAgentDetailStatus } from '@/components/panels/agents-panel-resolve-status';
9
+
10
+ const DEFAULT_WIDTH = 400;
11
+ const MIN_WIDTH = 300;
12
+ const MAX_WIDTH_ABS = 640;
13
+ const MAX_WIDTH_RATIO = 0.42;
14
+
15
+ interface RightAgentDetailPanelProps {
16
+ open: boolean;
17
+ agentKey: string | null;
18
+ onClose: () => void;
19
+ /** Right offset in px when Ask panel is open (stack panels side-by-side). */
20
+ rightOffset: number;
21
+ width: number;
22
+ onWidthChange: (w: number) => void;
23
+ onWidthCommit: (w: number) => void;
24
+ }
25
+
26
+ export default function RightAgentDetailPanel({
27
+ open,
28
+ agentKey,
29
+ onClose,
30
+ rightOffset,
31
+ width,
32
+ onWidthChange,
33
+ onWidthCommit,
34
+ }: RightAgentDetailPanelProps) {
35
+ const mcp = useMcpData();
36
+ const { t } = useLocale();
37
+ const p = t.panels.agents;
38
+
39
+ const connected = mcp.agents.filter(a => a.present && a.installed);
40
+ const detected = mcp.agents.filter(a => a.present && !a.installed);
41
+ const notFound = mcp.agents.filter(a => !a.present);
42
+
43
+ const resolved = useMemo(() => {
44
+ if (!agentKey) return null;
45
+ const agent = mcp.agents.find(a => a.key === agentKey);
46
+ if (!agent) return null;
47
+ const status = resolveAgentDetailStatus(agentKey, connected, detected, notFound);
48
+ if (!status) return null;
49
+ return { agent, status };
50
+ }, [agentKey, mcp.agents, connected, detected, notFound]);
51
+
52
+ useEffect(() => {
53
+ if (agentKey && !resolved) {
54
+ const id = requestAnimationFrame(() => onClose());
55
+ return () => cancelAnimationFrame(id);
56
+ }
57
+ }, [agentKey, resolved, onClose]);
58
+
59
+ const handleMouseDown = useResizeDrag({
60
+ width,
61
+ minWidth: MIN_WIDTH,
62
+ maxWidth: MAX_WIDTH_ABS,
63
+ maxWidthRatio: MAX_WIDTH_RATIO,
64
+ direction: 'left',
65
+ onResize: onWidthChange,
66
+ onResizeEnd: onWidthCommit,
67
+ });
68
+
69
+ const detailCopy = {
70
+ connected: p.connected,
71
+ installing: p.installing,
72
+ install: p.install,
73
+ copyConfig: p.copyConfig,
74
+ copied: p.copied,
75
+ transportLocal: p.transportLocal,
76
+ transportRemote: p.transportRemote,
77
+ configPath: p.configPath,
78
+ notFoundDetail: p.notFoundDetail,
79
+ backToList: p.backToList,
80
+ closeDetail: p.closeAgentDetail,
81
+ agentDetailTransport: p.agentDetailTransport,
82
+ agentDetailSnippet: p.agentDetailSnippet,
83
+ };
84
+
85
+ return (
86
+ <aside
87
+ className={`
88
+ hidden md:flex fixed top-0 h-screen z-[31] flex-col bg-card border-l border-border shadow-sm
89
+ transition-transform duration-200 ease-out
90
+ ${open ? 'translate-x-0' : 'translate-x-full pointer-events-none'}
91
+ `}
92
+ style={{ width: `${width}px`, right: `${rightOffset}px` }}
93
+ role="complementary"
94
+ aria-label={p.agentDetailPanelAria}
95
+ aria-hidden={!open || !resolved}
96
+ >
97
+ {resolved && (
98
+ <div className="flex flex-col flex-1 min-h-0 overflow-hidden">
99
+ <AgentsPanelAgentDetail
100
+ agent={resolved.agent}
101
+ agentStatus={resolved.status}
102
+ mcpStatus={mcp.status}
103
+ onBack={onClose}
104
+ onInstallAgent={mcp.installAgent}
105
+ copy={detailCopy}
106
+ headerVariant="dock"
107
+ />
108
+ </div>
109
+ )}
110
+
111
+ <div
112
+ className="absolute top-0 -left-[3px] w-[6px] h-full cursor-col-resize z-40 group hidden md:block"
113
+ onMouseDown={handleMouseDown}
114
+ >
115
+ <div className="absolute left-[2px] top-0 w-[2px] h-full opacity-0 group-hover:opacity-100 bg-[var(--amber)]/50 transition-opacity" />
116
+ </div>
117
+ </aside>
118
+ );
119
+ }
120
+
121
+ export { DEFAULT_WIDTH as RIGHT_AGENT_DETAIL_DEFAULT_WIDTH, MIN_WIDTH as RIGHT_AGENT_DETAIL_MIN_WIDTH, MAX_WIDTH_ABS as RIGHT_AGENT_DETAIL_MAX_WIDTH };
@@ -12,7 +12,13 @@ import SearchPanel from './panels/SearchPanel';
12
12
  import PluginsPanel from './panels/PluginsPanel';
13
13
  import AgentsPanel from './panels/AgentsPanel';
14
14
  import DiscoverPanel from './panels/DiscoverPanel';
15
+ import EchoPanel from './panels/EchoPanel';
15
16
  import RightAskPanel from './RightAskPanel';
17
+ import RightAgentDetailPanel, {
18
+ RIGHT_AGENT_DETAIL_DEFAULT_WIDTH,
19
+ RIGHT_AGENT_DETAIL_MIN_WIDTH,
20
+ RIGHT_AGENT_DETAIL_MAX_WIDTH,
21
+ } from './RightAgentDetailPanel';
16
22
  import AskFab from './AskFab';
17
23
  import SyncPopover from './panels/SyncPopover';
18
24
  import SearchModal from './SearchModal';
@@ -48,6 +54,20 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
48
54
  const [syncPopoverOpen, setSyncPopoverOpen] = useState(false);
49
55
  const [syncAnchorRect, setSyncAnchorRect] = useState<DOMRect | null>(null);
50
56
 
57
+ // ── Agent MCP detail (right dock, does not replace left Agents list) ──
58
+ const [agentDetailKey, setAgentDetailKey] = useState<string | null>(null);
59
+ const [agentDetailWidth, setAgentDetailWidth] = useState(() => {
60
+ if (typeof window === 'undefined') return RIGHT_AGENT_DETAIL_DEFAULT_WIDTH;
61
+ try {
62
+ const stored = localStorage.getItem('right-agent-detail-panel-width');
63
+ if (stored) {
64
+ const w = parseInt(stored, 10);
65
+ if (w >= RIGHT_AGENT_DETAIL_MIN_WIDTH && w <= RIGHT_AGENT_DETAIL_MAX_WIDTH) return w;
66
+ }
67
+ } catch { /* ignore */ }
68
+ return RIGHT_AGENT_DETAIL_DEFAULT_WIDTH;
69
+ });
70
+
51
71
  // ── Mobile state ──
52
72
  const [mobileOpen, setMobileOpen] = useState(false);
53
73
  const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
@@ -90,7 +110,21 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
90
110
  }, [ap.askOpenSource]);
91
111
 
92
112
  // Close mobile drawer on route change
93
- useEffect(() => { setMobileOpen(false); }, [pathname]);
113
+ useEffect(() => {
114
+ const id = requestAnimationFrame(() => setMobileOpen(false));
115
+ return () => cancelAnimationFrame(id);
116
+ }, [pathname]);
117
+
118
+ const handleAgentDetailWidthCommit = useCallback((w: number) => {
119
+ setAgentDetailWidth(w);
120
+ try {
121
+ localStorage.setItem('right-agent-detail-panel-width', String(w));
122
+ } catch { /* ignore */ }
123
+ }, []);
124
+
125
+ const closeAgentDetailPanel = useCallback(() => setAgentDetailKey(null), []);
126
+
127
+ const agentDockOpen = agentDetailKey !== null && lp.activePanel === 'agents';
94
128
 
95
129
  // Refresh file tree periodically
96
130
  useEffect(() => {
@@ -112,6 +146,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
112
146
  const handler = (e: KeyboardEvent) => {
113
147
  if (e.key === 'Escape') {
114
148
  if (lp.panelMaximized) { lp.handlePanelMaximize(); return; }
149
+ if (agentDockOpen) { setAgentDetailKey(null); return; }
115
150
  if (ap.askPanelOpen) { ap.closeAskPanel(); return; }
116
151
  if (ap.desktopAskPopupOpen) { ap.closeDesktopAskPopup(); return; }
117
152
  }
@@ -138,7 +173,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
138
173
  };
139
174
  window.addEventListener('keydown', handler);
140
175
  return () => window.removeEventListener('keydown', handler);
141
- }, [lp.panelMaximized, ap.askPanelOpen, ap.desktopAskPopupOpen, ap.toggleAskPanel, lp]);
176
+ }, [agentDockOpen, lp, ap]);
142
177
 
143
178
  // ── Settings helpers ──
144
179
  const openSyncSettings = useCallback(() => {
@@ -208,6 +243,9 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
208
243
  maximized={lp.panelMaximized}
209
244
  onMaximize={lp.handlePanelMaximize}
210
245
  >
246
+ <div className={`flex flex-col h-full ${lp.activePanel === 'echo' ? '' : 'hidden'}`}>
247
+ <EchoPanel active={lp.activePanel === 'echo'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
248
+ </div>
211
249
  <div className={`flex flex-col h-full ${lp.activePanel === 'search' ? '' : 'hidden'}`}>
212
250
  <SearchPanel active={lp.activePanel === 'search'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
213
251
  </div>
@@ -215,7 +253,13 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
215
253
  <PluginsPanel active={lp.activePanel === 'plugins'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
216
254
  </div>
217
255
  <div className={`flex flex-col h-full ${lp.activePanel === 'agents' ? '' : 'hidden'}`}>
218
- <AgentsPanel active={lp.activePanel === 'agents'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
256
+ <AgentsPanel
257
+ active={lp.activePanel === 'agents'}
258
+ maximized={lp.panelMaximized}
259
+ onMaximize={lp.handlePanelMaximize}
260
+ selectedAgentKey={agentDockOpen ? agentDetailKey : null}
261
+ onOpenAgentDetail={setAgentDetailKey}
262
+ />
219
263
  </div>
220
264
  <div className={`flex flex-col h-full ${lp.activePanel === 'discover' ? '' : 'hidden'}`}>
221
265
  <DiscoverPanel active={lp.activePanel === 'discover'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
@@ -236,6 +280,16 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
236
280
  onModeSwitch={ap.handleAskModeSwitch}
237
281
  />
238
282
 
283
+ <RightAgentDetailPanel
284
+ open={agentDockOpen}
285
+ agentKey={agentDetailKey}
286
+ onClose={closeAgentDetailPanel}
287
+ rightOffset={ap.askPanelOpen ? ap.askPanelWidth : 0}
288
+ width={agentDetailWidth}
289
+ onWidthChange={setAgentDetailWidth}
290
+ onWidthCommit={handleAgentDetailWidthCommit}
291
+ />
292
+
239
293
  <AskModal
240
294
  open={ap.desktopAskPopupOpen}
241
295
  onClose={ap.closeDesktopAskPopup}
@@ -308,10 +362,13 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
308
362
 
309
363
  <style>{`
310
364
  @media (min-width: 768px) {
311
- :root { --right-panel-width: ${ap.askPanelOpen ? ap.askPanelWidth : 0}px; }
365
+ :root {
366
+ --right-panel-width: ${ap.askPanelOpen ? ap.askPanelWidth : 0}px;
367
+ --right-agent-detail-width: ${agentDockOpen ? agentDetailWidth : 0}px;
368
+ }
312
369
  #main-content {
313
370
  padding-left: ${lp.panelOpen && lp.panelMaximized ? '100vw' : `${lp.panelOpen ? lp.railWidth + lp.effectivePanelWidth : lp.railWidth}px`} !important;
314
- padding-right: var(--right-panel-width) !important;
371
+ padding-right: calc(var(--right-panel-width) + var(--right-agent-detail-width)) !important;
315
372
  padding-top: 0 !important;
316
373
  }
317
374
  }
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { Loader2, CheckCircle2 } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+
7
+ const UPDATE_STATE_KEY = 'mindos_update_in_progress';
8
+ const POLL_INTERVAL = 3_000;
9
+
10
+ /**
11
+ * Global overlay shown when MindOS update kills the server.
12
+ * Mounted in root layout — persists across page navigations and Settings close.
13
+ * Reads localStorage flag set by UpdateTab. Auto-reloads when server comes back.
14
+ */
15
+ export default function UpdateOverlay() {
16
+ const [visible, setVisible] = useState(false);
17
+ const [done, setDone] = useState(false);
18
+ const pollRef = useRef<ReturnType<typeof setInterval>>(undefined);
19
+ const { locale } = useLocale();
20
+ const zh = locale === 'zh';
21
+
22
+ const startPolling = useCallback(() => {
23
+ pollRef.current = setInterval(async () => {
24
+ try {
25
+ const res = await fetch('/api/health', { signal: AbortSignal.timeout(3000) });
26
+ if (res.ok) {
27
+ clearInterval(pollRef.current);
28
+ pollRef.current = undefined;
29
+ // Server is back — check if version changed
30
+ try {
31
+ const saved = localStorage.getItem(UPDATE_STATE_KEY);
32
+ if (saved) {
33
+ const { originalVer } = JSON.parse(saved);
34
+ const data = await fetch('/api/update-check').then(r => r.json());
35
+ if (data.current && data.current !== originalVer) {
36
+ setDone(true);
37
+ localStorage.removeItem(UPDATE_STATE_KEY);
38
+ localStorage.removeItem('mindos_update_latest');
39
+ localStorage.removeItem('mindos_update_dismissed');
40
+ setTimeout(() => window.location.reload(), 1500);
41
+ return;
42
+ }
43
+ }
44
+ } catch { /* check failed, still reload */ }
45
+ // Server is back but version unchanged (or no saved state) — just reload
46
+ localStorage.removeItem(UPDATE_STATE_KEY);
47
+ window.location.reload();
48
+ }
49
+ } catch {
50
+ // Still down
51
+ }
52
+ }, POLL_INTERVAL);
53
+ }, []);
54
+
55
+ // Check on mount and listen for update-started event from UpdateTab
56
+ useEffect(() => {
57
+ const check = () => {
58
+ const saved = localStorage.getItem(UPDATE_STATE_KEY);
59
+ if (saved) {
60
+ setVisible(true);
61
+ if (!pollRef.current) startPolling();
62
+ }
63
+ };
64
+
65
+ // Check immediately (handles page reload during update)
66
+ check();
67
+
68
+ // Listen for same-tab update start (localStorage 'storage' event only fires cross-tab)
69
+ const handler = () => check();
70
+ window.addEventListener('mindos:update-started', handler);
71
+ window.addEventListener('storage', handler); // cross-tab fallback
72
+
73
+ return () => {
74
+ clearInterval(pollRef.current);
75
+ pollRef.current = undefined;
76
+ window.removeEventListener('mindos:update-started', handler);
77
+ window.removeEventListener('storage', handler);
78
+ };
79
+ }, [startPolling]);
80
+
81
+ if (!visible) return null;
82
+
83
+ return (
84
+ <div
85
+ style={{
86
+ position: 'fixed',
87
+ inset: 0,
88
+ zIndex: 99999,
89
+ background: 'rgba(0,0,0,0.7)',
90
+ backdropFilter: 'blur(8px)',
91
+ display: 'flex',
92
+ flexDirection: 'column',
93
+ alignItems: 'center',
94
+ justifyContent: 'center',
95
+ fontFamily: 'system-ui, -apple-system, sans-serif',
96
+ }}
97
+ >
98
+ {done ? (
99
+ <>
100
+ <CheckCircle2 size={32} style={{ color: '#7aad80', marginBottom: 12 }} />
101
+ <div style={{ color: '#e8e4dc', fontSize: 18, fontWeight: 600 }}>
102
+ {zh ? '更新成功!' : 'Update Complete!'}
103
+ </div>
104
+ <div style={{ color: '#8a8275', fontSize: 13, marginTop: 6 }}>
105
+ {zh ? '正在刷新页面...' : 'Reloading...'}
106
+ </div>
107
+ </>
108
+ ) : (
109
+ <>
110
+ <Loader2 size={32} style={{ color: '#d4954a', marginBottom: 12, animation: 'spin 1s linear infinite' }} />
111
+ <div style={{ color: '#e8e4dc', fontSize: 18, fontWeight: 600 }}>
112
+ {zh ? 'MindOS 正在更新...' : 'MindOS is Updating...'}
113
+ </div>
114
+ <div style={{ color: '#8a8275', fontSize: 13, marginTop: 6, textAlign: 'center', maxWidth: 300, lineHeight: 1.5 }}>
115
+ {zh
116
+ ? '服务正在重启,请勿关闭此页面。完成后将自动刷新。'
117
+ : 'The server is restarting. Please do not close this page. It will auto-reload when ready.'}
118
+ </div>
119
+ </>
120
+ )}
121
+ <style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
122
+ </div>
123
+ );
124
+ }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useMemo, useCallback } from 'react';
3
+ import { useState, useMemo, useCallback, useEffect } from 'react';
4
4
  import { BookOpen, Rocket, Brain, Keyboard, HelpCircle, Bot, ChevronDown, Copy, Check } from 'lucide-react';
5
5
  import { useLocale } from '@/lib/LocaleContext';
6
6
 
@@ -39,7 +39,7 @@ function Section({ icon, title, defaultOpen = false, children }: {
39
39
  function StepCard({ step, title, desc }: { step: number; title: string; desc: string }) {
40
40
  return (
41
41
  <div className="flex gap-4 items-start">
42
- <div className="shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold font-mono" style={{ background: 'var(--amber-dim)', color: 'var(--amber)' }}>
42
+ <div className="shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold font-mono bg-[var(--amber-dim)] text-[var(--amber)]">
43
43
  {step}
44
44
  </div>
45
45
  <div className="min-w-0">
@@ -59,12 +59,12 @@ function PromptBlock({ text, copyLabel }: { text: string; copyLabel: string }) {
59
59
  navigator.clipboard.writeText(clean).then(() => {
60
60
  setCopied(true);
61
61
  setTimeout(() => setCopied(false), 1500);
62
- });
62
+ }).catch(() => {});
63
63
  }, [text]);
64
64
 
65
65
  return (
66
66
  <div className="group/prompt mt-2 flex items-start gap-2 bg-background border border-border rounded-md px-3 py-2">
67
- <p className="flex-1 text-xs font-mono leading-relaxed" style={{ color: 'var(--amber)' }}>{text}</p>
67
+ <p className="flex-1 text-xs font-mono leading-relaxed text-[var(--amber)]">{text}</p>
68
68
  <button
69
69
  onClick={handleCopy}
70
70
  className="shrink-0 p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors opacity-0 group-hover/prompt:opacity-100 focus-visible:opacity-100"
@@ -122,8 +122,11 @@ export default function HelpContent() {
122
122
  const { t } = useLocale();
123
123
  const h = t.help;
124
124
 
125
- const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent);
126
- const mod = isMac ? '⌘' : 'Ctrl';
125
+ const [mod, setMod] = useState('');
126
+ useEffect(() => {
127
+ const isMac = /Mac|iPhone|iPad/.test(navigator.userAgent);
128
+ setMod(isMac ? '⌘' : 'Ctrl');
129
+ }, []);
127
130
 
128
131
  const shortcuts = useMemo(() => [
129
132
  { keys: `${mod} K`, label: h.shortcuts.search },
@@ -141,7 +144,7 @@ export default function HelpContent() {
141
144
  {/* ── Header ── */}
142
145
  <div className="mb-8">
143
146
  <div className="flex items-center gap-2 mb-1">
144
- <div className="w-1 h-6 rounded-full" style={{ background: 'var(--amber)' }} />
147
+ <div className="w-1 h-6 rounded-full bg-[var(--amber)]" />
145
148
  <h1 className="text-2xl font-bold font-display text-foreground">{h.title}</h1>
146
149
  </div>
147
150
  <p className="text-muted-foreground text-sm ml-3 mt-1">{h.subtitle}</p>