@geminilight/mindos 0.2.1 → 0.4.0

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.
Files changed (82) hide show
  1. package/app/app/api/init/route.ts +7 -41
  2. package/app/app/api/mcp/agents/route.ts +72 -0
  3. package/app/app/api/mcp/install/route.ts +95 -0
  4. package/app/app/api/mcp/status/route.ts +47 -0
  5. package/app/app/api/settings/route.ts +3 -0
  6. package/app/app/api/setup/generate-token/route.ts +23 -0
  7. package/app/app/api/setup/route.ts +81 -0
  8. package/app/app/api/skills/route.ts +208 -0
  9. package/app/app/api/sync/route.ts +54 -3
  10. package/app/app/api/update-check/route.ts +52 -0
  11. package/app/app/globals.css +12 -0
  12. package/app/app/layout.tsx +4 -2
  13. package/app/app/login/page.tsx +20 -13
  14. package/app/app/page.tsx +22 -2
  15. package/app/app/setup/page.tsx +9 -0
  16. package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
  17. package/app/app/view/[...path]/loading.tsx +1 -1
  18. package/app/app/view/[...path]/not-found.tsx +101 -0
  19. package/app/components/AskFab.tsx +1 -1
  20. package/app/components/AskModal.tsx +1 -1
  21. package/app/components/Backlinks.tsx +1 -1
  22. package/app/components/Breadcrumb.tsx +13 -3
  23. package/app/components/CsvView.tsx +5 -6
  24. package/app/components/DirView.tsx +42 -21
  25. package/app/components/FindInPage.tsx +211 -0
  26. package/app/components/HomeContent.tsx +97 -44
  27. package/app/components/JsonView.tsx +1 -2
  28. package/app/components/MarkdownEditor.tsx +1 -2
  29. package/app/components/OnboardingView.tsx +6 -7
  30. package/app/components/SettingsModal.tsx +5 -2
  31. package/app/components/SetupWizard.tsx +479 -0
  32. package/app/components/Sidebar.tsx +1 -1
  33. package/app/components/UpdateBanner.tsx +101 -0
  34. package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
  35. package/app/components/renderers/agent-inspector/manifest.ts +14 -0
  36. package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
  37. package/app/components/renderers/backlinks/manifest.ts +14 -0
  38. package/app/components/renderers/config/manifest.ts +14 -0
  39. package/app/components/renderers/csv/BoardView.tsx +12 -12
  40. package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
  41. package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
  42. package/app/components/renderers/csv/GalleryView.tsx +3 -3
  43. package/app/components/renderers/csv/TableView.tsx +4 -5
  44. package/app/components/renderers/csv/manifest.ts +14 -0
  45. package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
  46. package/app/components/renderers/diff/manifest.ts +14 -0
  47. package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
  48. package/app/components/renderers/graph/manifest.ts +14 -0
  49. package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
  50. package/app/components/renderers/summary/manifest.ts +14 -0
  51. package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
  52. package/app/components/renderers/timeline/manifest.ts +14 -0
  53. package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
  54. package/app/components/renderers/todo/manifest.ts +14 -0
  55. package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
  56. package/app/components/renderers/workflow/manifest.ts +14 -0
  57. package/app/components/settings/McpTab.tsx +549 -0
  58. package/app/components/settings/SyncTab.tsx +139 -50
  59. package/app/components/settings/types.ts +1 -1
  60. package/app/data/pages/home.png +0 -0
  61. package/app/lib/i18n.ts +270 -10
  62. package/app/lib/renderers/index.ts +20 -89
  63. package/app/lib/renderers/registry.ts +4 -1
  64. package/app/lib/settings.ts +15 -1
  65. package/app/lib/template.ts +45 -0
  66. package/app/package.json +1 -0
  67. package/app/types/semver.d.ts +8 -0
  68. package/bin/cli.js +137 -24
  69. package/bin/lib/build.js +53 -18
  70. package/bin/lib/colors.js +3 -1
  71. package/bin/lib/config.js +4 -0
  72. package/bin/lib/constants.js +2 -0
  73. package/bin/lib/debug.js +10 -0
  74. package/bin/lib/startup.js +21 -20
  75. package/bin/lib/stop.js +41 -3
  76. package/bin/lib/sync.js +65 -53
  77. package/bin/lib/update-check.js +94 -0
  78. package/bin/lib/utils.js +2 -2
  79. package/package.json +1 -1
  80. package/scripts/gen-renderer-index.js +57 -0
  81. package/scripts/setup.js +117 -1
  82. /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
@@ -14,6 +14,7 @@ import { KnowledgeTab } from './settings/KnowledgeTab';
14
14
  import { PluginsTab } from './settings/PluginsTab';
15
15
  import { ShortcutsTab } from './settings/ShortcutsTab';
16
16
  import { SyncTab } from './settings/SyncTab';
17
+ import { McpTab } from './settings/McpTab';
17
18
 
18
19
  interface SettingsModalProps {
19
20
  open: boolean;
@@ -139,6 +140,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
139
140
  { id: 'appearance', label: t.settings.tabs.appearance },
140
141
  { id: 'knowledge', label: t.settings.tabs.knowledge },
141
142
  { id: 'sync', label: t.settings.tabs.sync ?? 'Sync' },
143
+ { id: 'mcp', label: t.settings.tabs.mcp ?? 'MCP' },
142
144
  { id: 'plugins', label: t.settings.tabs.plugins },
143
145
  { id: 'shortcuts', label: t.settings.tabs.shortcuts },
144
146
  ];
@@ -158,7 +160,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
158
160
  <div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
159
161
  <div className="flex items-center gap-2 text-sm font-medium text-foreground">
160
162
  <Settings size={15} className="text-muted-foreground" />
161
- <span style={{ fontFamily: "'IBM Plex Mono', monospace" }}>{t.settings.title}</span>
163
+ <span className="font-display">{t.settings.title}</span>
162
164
  </div>
163
165
  <button onClick={onClose} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
164
166
  <X size={15} />
@@ -190,7 +192,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
190
192
  <p className="text-sm text-destructive font-medium">Failed to load settings</p>
191
193
  <p className="text-xs text-muted-foreground">Check that the server is running and AUTH_TOKEN is configured correctly.</p>
192
194
  </div>
193
- ) : !data && tab !== 'shortcuts' && tab !== 'appearance' ? (
195
+ ) : !data && tab !== 'shortcuts' && tab !== 'appearance' && tab !== 'mcp' && tab !== 'sync' ? (
194
196
  <div className="flex justify-center py-8">
195
197
  <Loader2 size={18} className="animate-spin text-muted-foreground" />
196
198
  </div>
@@ -202,6 +204,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
202
204
  {tab === 'plugins' && <PluginsTab pluginStates={pluginStates} setPluginStates={setPluginStates} t={t} />}
203
205
  {tab === 'shortcuts' && <ShortcutsTab t={t} />}
204
206
  {tab === 'sync' && <SyncTab t={t} />}
207
+ {tab === 'mcp' && <McpTab t={t} />}
205
208
  </>
206
209
  )}
207
210
  </div>
@@ -0,0 +1,479 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { Sparkles, Globe, BookOpen, FileText, Copy, Check, RefreshCw, Loader2, ChevronLeft, ChevronRight, AlertTriangle } from 'lucide-react';
6
+ import { useLocale } from '@/lib/LocaleContext';
7
+ import { Field, Input, Select, ApiKeyInput } from '@/components/settings/Primitives';
8
+
9
+ type Template = 'en' | 'zh' | 'empty' | '';
10
+
11
+ interface SetupState {
12
+ mindRoot: string;
13
+ template: Template;
14
+ provider: 'anthropic' | 'openai' | 'skip';
15
+ anthropicKey: string;
16
+ anthropicModel: string;
17
+ openaiKey: string;
18
+ openaiModel: string;
19
+ openaiBaseUrl: string;
20
+ webPort: number;
21
+ mcpPort: number;
22
+ authToken: string;
23
+ webPassword: string;
24
+ }
25
+
26
+ const TEMPLATES: Array<{
27
+ id: Template;
28
+ icon: React.ReactNode;
29
+ dirs: string[];
30
+ }> = [
31
+ { id: 'en', icon: <Globe size={18} />, dirs: ['Profile/', 'Connections/', 'Notes/', 'Workflows/', 'Resources/', 'Projects/'] },
32
+ { id: 'zh', icon: <BookOpen size={18} />, dirs: ['画像/', '关系/', '笔记/', '流程/', '资源/', '项目/'] },
33
+ { id: 'empty', icon: <FileText size={18} />, dirs: ['README.md', 'CONFIG.json', 'INSTRUCTION.md'] },
34
+ ];
35
+
36
+ const TOTAL_STEPS = 5;
37
+
38
+ export default function SetupWizard() {
39
+ const { t } = useLocale();
40
+ const router = useRouter();
41
+ const s = t.setup;
42
+
43
+ const [step, setStep] = useState(0);
44
+ const [state, setState] = useState<SetupState>({
45
+ mindRoot: '~/MindOS',
46
+ template: 'en',
47
+ provider: 'anthropic',
48
+ anthropicKey: '',
49
+ anthropicModel: 'claude-sonnet-4-6',
50
+ openaiKey: '',
51
+ openaiModel: 'gpt-5.4',
52
+ openaiBaseUrl: '',
53
+ webPort: 3000,
54
+ mcpPort: 8787,
55
+ authToken: '',
56
+ webPassword: '',
57
+ });
58
+ const [tokenCopied, setTokenCopied] = useState(false);
59
+ const [submitting, setSubmitting] = useState(false);
60
+ const [error, setError] = useState('');
61
+ const [portChanged, setPortChanged] = useState(false);
62
+
63
+ // Generate token on mount
64
+ useEffect(() => {
65
+ fetch('/api/setup/generate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
66
+ .then(r => r.json())
67
+ .then(data => { if (data.token) setState(prev => ({ ...prev, authToken: data.token })); })
68
+ .catch(() => {});
69
+ }, []);
70
+
71
+ const update = useCallback(<K extends keyof SetupState>(key: K, val: SetupState[K]) => {
72
+ setState(prev => ({ ...prev, [key]: val }));
73
+ }, []);
74
+
75
+ const generateToken = async (seed?: string) => {
76
+ try {
77
+ const res = await fetch('/api/setup/generate-token', {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify({ seed: seed || undefined }),
81
+ });
82
+ const data = await res.json();
83
+ if (data.token) update('authToken', data.token);
84
+ } catch { /* ignore */ }
85
+ };
86
+
87
+ const copyToken = () => {
88
+ navigator.clipboard.writeText(state.authToken);
89
+ setTokenCopied(true);
90
+ setTimeout(() => setTokenCopied(false), 2000);
91
+ };
92
+
93
+ const handleComplete = async () => {
94
+ setSubmitting(true);
95
+ setError('');
96
+ try {
97
+ const payload = {
98
+ mindRoot: state.mindRoot.startsWith('~')
99
+ ? state.mindRoot // server will resolve
100
+ : state.mindRoot,
101
+ template: state.template || undefined,
102
+ port: state.webPort,
103
+ mcpPort: state.mcpPort,
104
+ authToken: state.authToken,
105
+ webPassword: state.webPassword,
106
+ ai: state.provider === 'skip' ? undefined : {
107
+ provider: state.provider,
108
+ providers: {
109
+ anthropic: { apiKey: state.anthropicKey, model: state.anthropicModel },
110
+ openai: { apiKey: state.openaiKey, model: state.openaiModel, baseUrl: state.openaiBaseUrl },
111
+ },
112
+ },
113
+ };
114
+ const res = await fetch('/api/setup', {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json' },
117
+ body: JSON.stringify(payload),
118
+ });
119
+ const data = await res.json();
120
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
121
+ if (data.portChanged) setPortChanged(true);
122
+ else router.push('/');
123
+ } catch (e) {
124
+ setError(e instanceof Error ? e.message : String(e));
125
+ } finally {
126
+ setSubmitting(false);
127
+ }
128
+ };
129
+
130
+ const canNext = () => {
131
+ if (step === 0) return state.mindRoot.trim().length > 0;
132
+ if (step === 2) return state.webPort >= 1024 && state.webPort <= 65535 && state.mcpPort >= 1024 && state.mcpPort <= 65535;
133
+ return true;
134
+ };
135
+
136
+ const maskKey = (key: string) => {
137
+ if (!key) return '(not set)';
138
+ if (key.length <= 8) return '•••';
139
+ return key.slice(0, 6) + '•••' + key.slice(-3);
140
+ };
141
+
142
+ // Step indicator dots
143
+ const StepDots = () => (
144
+ <div className="flex items-center gap-2 mb-8">
145
+ {s.stepTitles.map((title: string, i: number) => (
146
+ <div key={i} className="flex items-center gap-2">
147
+ {i > 0 && <div className="w-8 h-px" style={{ background: i <= step ? 'var(--amber)' : 'var(--border)' }} />}
148
+ <button
149
+ onClick={() => i < step && setStep(i)}
150
+ className="flex items-center gap-1.5"
151
+ disabled={i > step}
152
+ >
153
+ <div
154
+ className="w-6 h-6 rounded-full text-xs font-medium flex items-center justify-center transition-colors"
155
+ style={{
156
+ background: i === step ? 'var(--amber)' : i < step ? 'var(--amber)' : 'var(--muted)',
157
+ color: i <= step ? 'white' : 'var(--muted-foreground)',
158
+ opacity: i <= step ? 1 : 0.5,
159
+ }}
160
+ >
161
+ {i + 1}
162
+ </div>
163
+ <span
164
+ className="text-xs hidden sm:inline"
165
+ style={{ color: i === step ? 'var(--foreground)' : 'var(--muted-foreground)', opacity: i <= step ? 1 : 0.5 }}
166
+ >
167
+ {title}
168
+ </span>
169
+ </button>
170
+ </div>
171
+ ))}
172
+ </div>
173
+ );
174
+
175
+ // Step 1: Knowledge Base
176
+ const Step1 = () => (
177
+ <div className="space-y-6">
178
+ <Field label={s.kbPath} hint={s.kbPathHint}>
179
+ <Input
180
+ value={state.mindRoot}
181
+ onChange={e => update('mindRoot', e.target.value)}
182
+ placeholder={s.kbPathDefault}
183
+ />
184
+ </Field>
185
+ <div>
186
+ <label className="text-sm text-foreground font-medium mb-3 block">{s.template}</label>
187
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
188
+ {TEMPLATES.map(tpl => (
189
+ <button
190
+ key={tpl.id}
191
+ onClick={() => update('template', tpl.id)}
192
+ className="flex flex-col items-start gap-2 p-4 rounded-xl border text-left transition-all duration-150"
193
+ style={{
194
+ background: state.template === tpl.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
195
+ borderColor: state.template === tpl.id ? 'var(--amber)' : 'var(--border)',
196
+ }}
197
+ >
198
+ <div className="flex items-center gap-2">
199
+ <span style={{ color: 'var(--amber)' }}>{tpl.icon}</span>
200
+ <span className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>
201
+ {t.onboarding.templates[tpl.id as 'en' | 'zh' | 'empty'].title}
202
+ </span>
203
+ </div>
204
+ <div
205
+ className="w-full rounded-lg px-2.5 py-1.5 text-[11px] leading-relaxed font-display"
206
+ style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}
207
+ >
208
+ {tpl.dirs.map(d => <div key={d}>{d}</div>)}
209
+ </div>
210
+ </button>
211
+ ))}
212
+ </div>
213
+ </div>
214
+ </div>
215
+ );
216
+
217
+ // Step 2: AI Provider
218
+ const Step2 = () => (
219
+ <div className="space-y-5">
220
+ <Field label={s.aiProvider} hint={s.aiProviderHint}>
221
+ <Select value={state.provider} onChange={e => update('provider', e.target.value as SetupState['provider'])}>
222
+ <option value="anthropic">Anthropic</option>
223
+ <option value="openai">OpenAI</option>
224
+ <option value="skip">{s.aiSkip}</option>
225
+ </Select>
226
+ </Field>
227
+ {state.provider !== 'skip' && (
228
+ <>
229
+ <Field label={s.apiKey}>
230
+ <ApiKeyInput
231
+ value={state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey}
232
+ onChange={v => update(state.provider === 'anthropic' ? 'anthropicKey' : 'openaiKey', v)}
233
+ placeholder={state.provider === 'anthropic' ? 'sk-ant-...' : 'sk-...'}
234
+ />
235
+ </Field>
236
+ <Field label={s.model}>
237
+ <Input
238
+ value={state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel}
239
+ onChange={e => update(state.provider === 'anthropic' ? 'anthropicModel' : 'openaiModel', e.target.value)}
240
+ />
241
+ </Field>
242
+ {state.provider === 'openai' && (
243
+ <Field label={s.baseUrl} hint={s.baseUrlHint}>
244
+ <Input
245
+ value={state.openaiBaseUrl}
246
+ onChange={e => update('openaiBaseUrl', e.target.value)}
247
+ placeholder="https://api.openai.com/v1"
248
+ />
249
+ </Field>
250
+ )}
251
+ </>
252
+ )}
253
+ </div>
254
+ );
255
+
256
+ // Step 3: Ports
257
+ const Step3 = () => (
258
+ <div className="space-y-5">
259
+ <Field label={s.webPort} hint={s.portHint}>
260
+ <Input
261
+ type="number"
262
+ min={1024}
263
+ max={65535}
264
+ value={state.webPort}
265
+ onChange={e => update('webPort', parseInt(e.target.value, 10) || 3000)}
266
+ />
267
+ </Field>
268
+ <Field label={s.mcpPort} hint={s.portHint}>
269
+ <Input
270
+ type="number"
271
+ min={1024}
272
+ max={65535}
273
+ value={state.mcpPort}
274
+ onChange={e => update('mcpPort', parseInt(e.target.value, 10) || 8787)}
275
+ />
276
+ </Field>
277
+ <p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--muted-foreground)' }}>
278
+ <AlertTriangle size={12} />
279
+ {s.portRestartWarning}
280
+ </p>
281
+ </div>
282
+ );
283
+
284
+ // Step 4: Security
285
+ const Step4 = () => {
286
+ const [seed, setSeed] = useState('');
287
+ const [showSeed, setShowSeed] = useState(false);
288
+
289
+ return (
290
+ <div className="space-y-5">
291
+ <Field label={s.authToken} hint={s.authTokenHint}>
292
+ <div className="flex gap-2">
293
+ <Input value={state.authToken} readOnly className="font-mono text-xs" />
294
+ <button
295
+ onClick={copyToken}
296
+ className="flex items-center gap-1 px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
297
+ style={{ color: 'var(--foreground)' }}
298
+ >
299
+ {tokenCopied ? <Check size={14} /> : <Copy size={14} />}
300
+ {tokenCopied ? s.copiedToken : s.copyToken}
301
+ </button>
302
+ <button
303
+ onClick={() => generateToken()}
304
+ className="flex items-center gap-1 px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
305
+ style={{ color: 'var(--foreground)' }}
306
+ >
307
+ <RefreshCw size={14} />
308
+ </button>
309
+ </div>
310
+ </Field>
311
+
312
+ <div>
313
+ <button
314
+ onClick={() => setShowSeed(!showSeed)}
315
+ className="text-xs underline"
316
+ style={{ color: 'var(--muted-foreground)' }}
317
+ >
318
+ {s.authTokenSeed}
319
+ </button>
320
+ {showSeed && (
321
+ <div className="mt-2 flex gap-2">
322
+ <Input
323
+ value={seed}
324
+ onChange={e => setSeed(e.target.value)}
325
+ placeholder={s.authTokenSeedHint}
326
+ />
327
+ <button
328
+ onClick={() => { if (seed.trim()) generateToken(seed); }}
329
+ className="px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
330
+ style={{ color: 'var(--foreground)' }}
331
+ >
332
+ {s.generateToken}
333
+ </button>
334
+ </div>
335
+ )}
336
+ </div>
337
+
338
+ <Field label={s.webPassword} hint={s.webPasswordHint}>
339
+ <Input
340
+ type="password"
341
+ value={state.webPassword}
342
+ onChange={e => update('webPassword', e.target.value)}
343
+ placeholder="(optional)"
344
+ />
345
+ </Field>
346
+ </div>
347
+ );
348
+ };
349
+
350
+ // Step 5: Review
351
+ const Step5 = () => {
352
+ const rows: [string, string][] = [
353
+ [s.kbPath, state.mindRoot],
354
+ [s.template, state.template || '—'],
355
+ [s.aiProvider, state.provider],
356
+ ...(state.provider !== 'skip' ? [
357
+ [s.apiKey, maskKey(state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey)] as [string, string],
358
+ [s.model, state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel] as [string, string],
359
+ ] : []),
360
+ [s.webPort, String(state.webPort)],
361
+ [s.mcpPort, String(state.mcpPort)],
362
+ [s.authToken, state.authToken || '—'],
363
+ [s.webPassword, state.webPassword ? '••••••••' : '(none)'],
364
+ ];
365
+
366
+ return (
367
+ <div className="space-y-5">
368
+ <p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.reviewHint}</p>
369
+ <div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
370
+ {rows.map(([label, value], i) => (
371
+ <div
372
+ key={i}
373
+ className="flex items-center justify-between px-4 py-3 text-sm"
374
+ style={{
375
+ background: i % 2 === 0 ? 'var(--card)' : 'transparent',
376
+ borderTop: i > 0 ? '1px solid var(--border)' : undefined,
377
+ }}
378
+ >
379
+ <span style={{ color: 'var(--muted-foreground)' }}>{label}</span>
380
+ <span className="font-mono text-xs" style={{ color: 'var(--foreground)' }}>{value}</span>
381
+ </div>
382
+ ))}
383
+ </div>
384
+
385
+ {error && (
386
+ <div className="p-3 rounded-lg text-sm text-red-500" style={{ background: 'rgba(239,68,68,0.1)' }}>
387
+ {s.completeFailed}: {error}
388
+ </div>
389
+ )}
390
+
391
+ {portChanged && (
392
+ <div className="space-y-3">
393
+ <div className="p-3 rounded-lg text-sm flex items-center gap-2" style={{ background: 'rgba(200,135,30,0.1)', color: 'var(--amber)' }}>
394
+ <AlertTriangle size={14} />
395
+ {s.portChanged}
396
+ </div>
397
+ <a
398
+ href="/"
399
+ className="inline-flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors"
400
+ style={{ background: 'var(--amber)', color: 'white' }}
401
+ >
402
+ {s.completeDone} &rarr;
403
+ </a>
404
+ </div>
405
+ )}
406
+ </div>
407
+ );
408
+ };
409
+
410
+ const steps = [Step1, Step2, Step3, Step4, Step5];
411
+ const CurrentStep = steps[step];
412
+
413
+ return (
414
+ <div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto" style={{ background: 'var(--background)' }}>
415
+ <div className="w-full max-w-xl mx-auto px-6 py-12">
416
+ {/* Header */}
417
+ <div className="text-center mb-8">
418
+ <div className="inline-flex items-center gap-2 mb-2">
419
+ <Sparkles size={18} style={{ color: 'var(--amber)' }} />
420
+ <h1
421
+ className="text-2xl font-semibold tracking-tight font-display"
422
+ style={{ color: 'var(--foreground)' }}
423
+ >
424
+ MindOS
425
+ </h1>
426
+ </div>
427
+ </div>
428
+
429
+ {/* Step dots */}
430
+ <div className="flex justify-center">
431
+ <StepDots />
432
+ </div>
433
+
434
+ {/* Step title */}
435
+ <h2 className="text-lg font-semibold mb-5" style={{ color: 'var(--foreground)' }}>
436
+ {s.stepTitles[step]}
437
+ </h2>
438
+
439
+ {/* Step content */}
440
+ <CurrentStep />
441
+
442
+ {/* Navigation */}
443
+ <div className="flex items-center justify-between mt-8 pt-6" style={{ borderTop: '1px solid var(--border)' }}>
444
+ <button
445
+ onClick={() => setStep(step - 1)}
446
+ disabled={step === 0}
447
+ className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg border border-border hover:bg-muted transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
448
+ style={{ color: 'var(--foreground)' }}
449
+ >
450
+ <ChevronLeft size={14} />
451
+ {s.back}
452
+ </button>
453
+
454
+ {step < TOTAL_STEPS - 1 ? (
455
+ <button
456
+ onClick={() => setStep(step + 1)}
457
+ disabled={!canNext()}
458
+ className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
459
+ style={{ background: 'var(--amber)', color: 'white' }}
460
+ >
461
+ {s.next}
462
+ <ChevronRight size={14} />
463
+ </button>
464
+ ) : (
465
+ <button
466
+ onClick={handleComplete}
467
+ disabled={submitting || portChanged}
468
+ className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
469
+ style={{ background: 'var(--amber)', color: 'white' }}
470
+ >
471
+ {submitting && <Loader2 size={14} className="animate-spin" />}
472
+ {submitting ? s.completing : portChanged ? s.completeDone : s.complete}
473
+ </button>
474
+ )}
475
+ </div>
476
+ </div>
477
+ </div>
478
+ );
479
+ }
@@ -73,7 +73,7 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
73
73
  <div className="flex items-center justify-between px-4 py-4 border-b border-border shrink-0">
74
74
  <Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
75
75
  <Logo id="desktop" />
76
- <span className="font-semibold text-foreground text-sm tracking-wide" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>MindOS</span>
76
+ <span className="font-semibold text-foreground text-sm tracking-wide font-display">MindOS</span>
77
77
  </Link>
78
78
  {/* Mobile close */}
79
79
  <button onClick={() => setMobileOpen(false)} className="md:hidden p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { X } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+ import { apiFetch } from '@/lib/api';
7
+
8
+ interface UpdateInfo {
9
+ current: string;
10
+ latest: string;
11
+ }
12
+
13
+ export default function UpdateBanner() {
14
+ const { t } = useLocale();
15
+ const [info, setInfo] = useState<UpdateInfo | null>(null);
16
+
17
+ useEffect(() => {
18
+ // Don't check for updates on setup or login pages
19
+ if (typeof window !== 'undefined') {
20
+ const path = window.location.pathname;
21
+ if (path === '/setup' || path === '/login') return;
22
+ }
23
+
24
+ const timer = setTimeout(async () => {
25
+ try {
26
+ const data = await apiFetch<{ hasUpdate: boolean; latest: string; current: string }>('/api/update-check');
27
+ if (!data.hasUpdate) return;
28
+
29
+ const dismissed = localStorage.getItem('mindos_update_dismissed');
30
+ if (data.latest === dismissed) return;
31
+
32
+ setInfo({ latest: data.latest, current: data.current });
33
+ } catch {
34
+ // Network error / API failure — silent
35
+ }
36
+ }, 3000); // Check 3s after page load, don't block first paint
37
+
38
+ return () => clearTimeout(timer);
39
+ }, []);
40
+
41
+ if (!info) return null;
42
+
43
+ const handleDismiss = () => {
44
+ localStorage.setItem('mindos_update_dismissed', info.latest);
45
+ setInfo(null);
46
+ };
47
+
48
+ const updateT = t.updateBanner;
49
+
50
+ return (
51
+ <div
52
+ className="flex items-center justify-between gap-3 px-4 py-2 text-xs"
53
+ style={{ background: 'var(--amber-subtle, rgba(200,135,30,0.08))', borderBottom: '1px solid var(--border)' }}
54
+ >
55
+ <div className="flex items-center gap-2 min-w-0">
56
+ <span className="font-medium" style={{ color: 'var(--amber)' }}>
57
+ {updateT?.newVersion
58
+ ? updateT.newVersion(info.latest, info.current)
59
+ : `MindOS v${info.latest} available (current: v${info.current})`}
60
+ </span>
61
+ <span className="text-muted-foreground">
62
+ {updateT?.runUpdate ?? 'Run'}{' '}
63
+ <code className="px-1 py-0.5 rounded bg-muted font-mono text-[11px]">mindos update</code>
64
+ {updateT?.orSee ? (
65
+ <>
66
+ {' '}{updateT.orSee}{' '}
67
+ <a
68
+ href="https://github.com/GeminiLight/mindos/releases"
69
+ target="_blank"
70
+ rel="noopener noreferrer"
71
+ className="underline hover:text-foreground transition-colors"
72
+ >
73
+ {updateT.releaseNotes}
74
+ </a>
75
+ </>
76
+ ) : (
77
+ <>
78
+ {' '}or{' '}
79
+ <a
80
+ href="https://github.com/GeminiLight/mindos/releases"
81
+ target="_blank"
82
+ rel="noopener noreferrer"
83
+ className="underline hover:text-foreground transition-colors"
84
+ >
85
+ view release notes
86
+ </a>
87
+ </>
88
+ )}
89
+ </span>
90
+ </div>
91
+ <button
92
+ onClick={handleDismiss}
93
+ className="p-0.5 rounded hover:bg-muted transition-colors shrink-0"
94
+ style={{ color: 'var(--muted-foreground)' }}
95
+ title="Dismiss"
96
+ >
97
+ <X size={14} />
98
+ </button>
99
+ </div>
100
+ );
101
+ }