@geminilight/mindos 0.3.0 → 0.5.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 (80) hide show
  1. package/app/app/api/mcp/agents/route.ts +72 -0
  2. package/app/app/api/mcp/install/route.ts +95 -0
  3. package/app/app/api/mcp/status/route.ts +47 -0
  4. package/app/app/api/setup/check-port/route.ts +41 -0
  5. package/app/app/api/skills/route.ts +208 -0
  6. package/app/app/api/sync/route.ts +54 -3
  7. package/app/app/api/update-check/route.ts +52 -0
  8. package/app/app/globals.css +12 -0
  9. package/app/app/layout.tsx +4 -2
  10. package/app/app/login/page.tsx +20 -13
  11. package/app/app/page.tsx +19 -2
  12. package/app/app/setup/page.tsx +2 -0
  13. package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
  14. package/app/app/view/[...path]/loading.tsx +1 -1
  15. package/app/app/view/[...path]/not-found.tsx +101 -0
  16. package/app/components/AskFab.tsx +1 -1
  17. package/app/components/AskModal.tsx +1 -1
  18. package/app/components/Backlinks.tsx +1 -1
  19. package/app/components/Breadcrumb.tsx +13 -3
  20. package/app/components/CsvView.tsx +5 -6
  21. package/app/components/DirView.tsx +42 -21
  22. package/app/components/FindInPage.tsx +211 -0
  23. package/app/components/HomeContent.tsx +97 -44
  24. package/app/components/JsonView.tsx +1 -2
  25. package/app/components/MarkdownEditor.tsx +1 -2
  26. package/app/components/OnboardingView.tsx +6 -7
  27. package/app/components/SettingsModal.tsx +5 -2
  28. package/app/components/SetupWizard.tsx +499 -172
  29. package/app/components/Sidebar.tsx +1 -1
  30. package/app/components/UpdateBanner.tsx +101 -0
  31. package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
  32. package/app/components/renderers/agent-inspector/manifest.ts +14 -0
  33. package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
  34. package/app/components/renderers/backlinks/manifest.ts +14 -0
  35. package/app/components/renderers/config/manifest.ts +14 -0
  36. package/app/components/renderers/csv/BoardView.tsx +12 -12
  37. package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
  38. package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
  39. package/app/components/renderers/csv/GalleryView.tsx +3 -3
  40. package/app/components/renderers/csv/TableView.tsx +4 -5
  41. package/app/components/renderers/csv/manifest.ts +14 -0
  42. package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
  43. package/app/components/renderers/diff/manifest.ts +14 -0
  44. package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
  45. package/app/components/renderers/graph/manifest.ts +14 -0
  46. package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
  47. package/app/components/renderers/summary/manifest.ts +14 -0
  48. package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
  49. package/app/components/renderers/timeline/manifest.ts +14 -0
  50. package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
  51. package/app/components/renderers/todo/manifest.ts +14 -0
  52. package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
  53. package/app/components/renderers/workflow/manifest.ts +14 -0
  54. package/app/components/settings/McpTab.tsx +549 -0
  55. package/app/components/settings/SyncTab.tsx +139 -50
  56. package/app/components/settings/types.ts +1 -1
  57. package/app/data/pages/home.png +0 -0
  58. package/app/lib/i18n.ts +226 -19
  59. package/app/lib/renderers/index.ts +20 -89
  60. package/app/lib/renderers/registry.ts +4 -1
  61. package/app/lib/settings.ts +3 -0
  62. package/app/package.json +1 -0
  63. package/app/types/semver.d.ts +8 -0
  64. package/bin/cli.js +137 -24
  65. package/bin/lib/build.js +53 -18
  66. package/bin/lib/colors.js +3 -1
  67. package/bin/lib/config.js +4 -0
  68. package/bin/lib/constants.js +2 -0
  69. package/bin/lib/debug.js +10 -0
  70. package/bin/lib/mcp-install.js +4 -1
  71. package/bin/lib/port.js +8 -2
  72. package/bin/lib/startup.js +21 -20
  73. package/bin/lib/stop.js +41 -3
  74. package/bin/lib/sync.js +65 -53
  75. package/bin/lib/update-check.js +94 -0
  76. package/bin/lib/utils.js +2 -2
  77. package/package.json +1 -1
  78. package/scripts/gen-renderer-index.js +57 -0
  79. package/scripts/setup.js +205 -10
  80. /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
@@ -0,0 +1,211 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { X, ChevronUp, ChevronDown } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+
7
+ interface FindInPageProps {
8
+ containerRef: React.RefObject<HTMLElement | null>;
9
+ onClose: () => void;
10
+ }
11
+
12
+ const MARK_CLASS = 'findip-highlight';
13
+ const MARK_ACTIVE_CLASS = 'findip-active';
14
+
15
+ export default function FindInPage({ containerRef, onClose }: FindInPageProps) {
16
+ const { t } = useLocale();
17
+ const [query, setQuery] = useState('');
18
+ const [current, setCurrent] = useState(0);
19
+ const inputRef = useRef<HTMLInputElement>(null);
20
+ const marksRef = useRef<HTMLElement[]>([]);
21
+
22
+ // Focus input on mount
23
+ useEffect(() => {
24
+ inputRef.current?.focus();
25
+ }, []);
26
+
27
+ // Clear highlights helper
28
+ const clearHighlights = useCallback(() => {
29
+ for (const el of marksRef.current) {
30
+ const parent = el.parentNode;
31
+ if (parent) {
32
+ parent.replaceChild(document.createTextNode(el.textContent || ''), el);
33
+ parent.normalize();
34
+ }
35
+ }
36
+ marksRef.current = [];
37
+ }, []);
38
+
39
+ // Search and highlight
40
+ useEffect(() => {
41
+ clearHighlights();
42
+ setCurrent(0);
43
+
44
+ const container = containerRef.current;
45
+ if (!container || !query.trim()) return;
46
+
47
+ const needle = query.toLowerCase();
48
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
49
+ acceptNode(node) {
50
+ // Skip text nodes inside the find bar itself
51
+ if (node.parentElement?.closest('[data-find-in-page]')) return NodeFilter.FILTER_REJECT;
52
+ return NodeFilter.FILTER_ACCEPT;
53
+ },
54
+ });
55
+ const textNodes: Text[] = [];
56
+ while (walker.nextNode()) {
57
+ textNodes.push(walker.currentNode as Text);
58
+ }
59
+
60
+ const newMarks: HTMLElement[] = [];
61
+ for (const node of textNodes) {
62
+ const text = node.textContent || '';
63
+ const lower = text.toLowerCase();
64
+ let startIdx = 0;
65
+ const positions: number[] = [];
66
+
67
+ while (true) {
68
+ const idx = lower.indexOf(needle, startIdx);
69
+ if (idx === -1) break;
70
+ positions.push(idx);
71
+ startIdx = idx + needle.length;
72
+ }
73
+
74
+ if (positions.length === 0) continue;
75
+
76
+ // Split text node into fragments with <mark> wrappers
77
+ const parent = node.parentNode;
78
+ if (!parent) continue;
79
+
80
+ const frag = document.createDocumentFragment();
81
+ let lastEnd = 0;
82
+
83
+ for (const pos of positions) {
84
+ if (pos > lastEnd) {
85
+ frag.appendChild(document.createTextNode(text.slice(lastEnd, pos)));
86
+ }
87
+ const mark = document.createElement('mark');
88
+ mark.className = MARK_CLASS;
89
+ mark.textContent = text.slice(pos, pos + needle.length);
90
+ frag.appendChild(mark);
91
+ newMarks.push(mark);
92
+ lastEnd = pos + needle.length;
93
+ }
94
+
95
+ if (lastEnd < text.length) {
96
+ frag.appendChild(document.createTextNode(text.slice(lastEnd)));
97
+ }
98
+
99
+ parent.replaceChild(frag, node);
100
+ }
101
+
102
+ marksRef.current = newMarks;
103
+ setCurrent(newMarks.length > 0 ? 1 : 0);
104
+
105
+ // Highlight first match
106
+ if (newMarks.length > 0) {
107
+ newMarks[0].classList.add(MARK_ACTIVE_CLASS);
108
+ newMarks[0].scrollIntoView({ block: 'center', behavior: 'smooth' });
109
+ }
110
+
111
+ return () => {
112
+ // cleanup on query change is handled by next effect run
113
+ };
114
+ }, [query, containerRef, clearHighlights]);
115
+
116
+ // Cleanup on unmount
117
+ useEffect(() => {
118
+ return () => {
119
+ clearHighlights();
120
+ };
121
+ }, [clearHighlights]);
122
+
123
+ const totalMarks = marksRef.current.length;
124
+
125
+ const goTo = useCallback((index: number) => {
126
+ const marks = marksRef.current;
127
+ if (marks.length === 0) return;
128
+ // Remove active from previous
129
+ for (const m of marks) m.classList.remove(MARK_ACTIVE_CLASS);
130
+ // Wrap around
131
+ const wrapped = ((index - 1) % marks.length + marks.length) % marks.length;
132
+ marks[wrapped].classList.add(MARK_ACTIVE_CLASS);
133
+ marks[wrapped].scrollIntoView({ block: 'center', behavior: 'smooth' });
134
+ setCurrent(wrapped + 1);
135
+ }, []);
136
+
137
+ const goNext = useCallback(() => goTo(current + 1), [current, goTo]);
138
+ const goPrev = useCallback(() => goTo(current - 1), [current, goTo]);
139
+
140
+ // Keyboard handling
141
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
142
+ if (e.key === 'Escape') {
143
+ e.preventDefault();
144
+ onClose();
145
+ } else if (e.key === 'Enter') {
146
+ e.preventDefault();
147
+ if (e.shiftKey) goPrev();
148
+ else goNext();
149
+ }
150
+ }, [onClose, goNext, goPrev]);
151
+
152
+ return (
153
+ <>
154
+ <style>{`
155
+ mark.${MARK_CLASS} {
156
+ background: rgba(250, 204, 21, 0.3);
157
+ color: inherit;
158
+ border-radius: 2px;
159
+ padding: 0;
160
+ }
161
+ mark.${MARK_ACTIVE_CLASS} {
162
+ background: rgba(250, 204, 21, 0.7);
163
+ outline: 2px solid rgba(250, 204, 21, 0.5);
164
+ }
165
+ `}</style>
166
+ <div className="sticky top-[96px] md:top-[44px] z-30 flex justify-end px-4 md:px-6 pointer-events-none" data-find-in-page>
167
+ <div className="pointer-events-auto flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border bg-card shadow-lg">
168
+ <input
169
+ ref={inputRef}
170
+ type="text"
171
+ value={query}
172
+ onChange={e => setQuery(e.target.value)}
173
+ onKeyDown={handleKeyDown}
174
+ placeholder={t.findInPage.placeholder}
175
+ className="w-[180px] sm:w-[220px] px-2 py-1 text-sm bg-transparent text-foreground placeholder:text-muted-foreground outline-none"
176
+ />
177
+ <span className="text-xs text-muted-foreground tabular-nums shrink-0 min-w-[48px] text-center font-display">
178
+ {query.trim()
179
+ ? totalMarks > 0
180
+ ? t.findInPage.matchCount(current, totalMarks)
181
+ : t.findInPage.noResults
182
+ : ''}
183
+ </span>
184
+ <button
185
+ onClick={goPrev}
186
+ disabled={totalMarks === 0}
187
+ className="p-1 rounded text-muted-foreground hover:text-foreground disabled:opacity-30 transition-colors"
188
+ aria-label="Previous match"
189
+ >
190
+ <ChevronUp size={14} />
191
+ </button>
192
+ <button
193
+ onClick={goNext}
194
+ disabled={totalMarks === 0}
195
+ className="p-1 rounded text-muted-foreground hover:text-foreground disabled:opacity-30 transition-colors"
196
+ aria-label="Next match"
197
+ >
198
+ <ChevronDown size={14} />
199
+ </button>
200
+ <button
201
+ onClick={onClose}
202
+ className="p-1 rounded text-muted-foreground hover:text-foreground transition-colors"
203
+ aria-label="Close find"
204
+ >
205
+ <X size={14} />
206
+ </button>
207
+ </div>
208
+ </div>
209
+ </>
210
+ );
211
+ }
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import Link from 'next/link';
4
- import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown, Terminal } from 'lucide-react';
5
- import { useState } from 'react';
4
+ import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown } from 'lucide-react';
5
+ import { useState, useEffect, useRef } from 'react';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
  import { encodePath, relativeTime } from '@/lib/utils';
8
8
  import { getAllRenderers } from '@/lib/renderers/registry';
@@ -14,37 +14,45 @@ interface RecentFile {
14
14
  mtime: number;
15
15
  }
16
16
 
17
- // Maps a renderer id to a canonical entry file path
18
- const RENDERER_ENTRY: Record<string, string> = {
19
- todo: 'TODO.md',
20
- csv: 'Resources/Products.csv',
21
- graph: 'README.md',
22
- timeline: 'CHANGELOG.md',
23
- backlinks: 'BACKLINKS.md',
24
- summary: 'DAILY.md',
25
- 'agent-inspector': '.agent-log.json',
26
- workflow: 'Workflow.md',
27
- 'diff-viewer': 'Agent-Diff.md',
28
- 'config-panel': 'CONFIG.json',
29
- };
30
-
31
- function deriveEntryPath(id: string): string | null {
32
- return RENDERER_ENTRY[id] ?? null;
33
- }
34
-
35
17
  function triggerSearch() {
36
- // Dispatch ⌘K to open the Sidebar's SearchModal
37
18
  window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, bubbles: true }));
38
19
  }
39
20
 
40
21
  function triggerAsk() {
41
- // Dispatch ⌘/ to open the Sidebar's AskModal
42
22
  window.dispatchEvent(new KeyboardEvent('keydown', { key: '/', metaKey: true, bubbles: true }));
43
23
  }
44
24
 
45
- export default function HomeContent({ recent }: { recent: RecentFile[] }) {
25
+ export default function HomeContent({ recent, existingFiles }: { recent: RecentFile[]; existingFiles?: string[] }) {
46
26
  const { t } = useLocale();
47
27
  const [showAll, setShowAll] = useState(false);
28
+ const [suggestionIdx, setSuggestionIdx] = useState(0);
29
+ const [hintId, setHintId] = useState<string | null>(null);
30
+ const hintTimer = useRef<ReturnType<typeof setTimeout>>(null);
31
+
32
+ const suggestions = t.ask?.suggestions ?? [
33
+ 'Summarize this document',
34
+ 'List all action items and TODOs',
35
+ 'What are the key points?',
36
+ 'Find related notes on this topic',
37
+ ];
38
+
39
+ useEffect(() => {
40
+ const interval = setInterval(() => {
41
+ setSuggestionIdx(i => (i + 1) % suggestions.length);
42
+ }, 3500);
43
+ return () => clearInterval(interval);
44
+ }, [suggestions.length]);
45
+
46
+ // Cleanup hint timer on unmount
47
+ useEffect(() => () => { if (hintTimer.current) clearTimeout(hintTimer.current); }, []);
48
+
49
+ function showHint(id: string) {
50
+ if (hintTimer.current) clearTimeout(hintTimer.current);
51
+ setHintId(id);
52
+ hintTimer.current = setTimeout(() => setHintId(null), 3000);
53
+ }
54
+
55
+ const existingSet = new Set(existingFiles ?? []);
48
56
 
49
57
  // Empty knowledge base → show onboarding
50
58
  if (recent.length === 0) {
@@ -63,7 +71,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
63
71
  <div className="mb-10">
64
72
  <div className="flex items-center gap-2 mb-3">
65
73
  <div className="w-1 h-5 rounded-full" style={{ background: 'var(--amber)' }} />
66
- <h1 className="text-2xl font-semibold tracking-tight" style={{ fontFamily: "'IBM Plex Mono', monospace", color: 'var(--foreground)' }}>
74
+ <h1 className="text-2xl font-semibold tracking-tight font-display" style={{ color: 'var(--foreground)' }}>
67
75
  MindOS
68
76
  </h1>
69
77
  </div>
@@ -86,8 +94,8 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
86
94
  style={{ background: 'var(--card)', borderColor: 'var(--border)' }}
87
95
  >
88
96
  <Sparkles size={15} style={{ color: 'var(--amber)' }} className="shrink-0" />
89
- <span className="text-sm flex-1 text-left" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}>
90
- {t.home.shortcuts.askAI}
97
+ <span className="text-sm flex-1 text-left" style={{ color: 'var(--foreground)' }}>
98
+ {suggestions[suggestionIdx]}
91
99
  </span>
92
100
  <kbd
93
101
  className="hidden sm:inline-flex items-center gap-0.5 px-2 py-0.5 rounded text-[11px] font-mono font-medium"
@@ -102,7 +110,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
102
110
  onClick={triggerSearch}
103
111
  title="⌘K"
104
112
  className="flex items-center gap-2 px-3 py-3 rounded-xl border text-sm transition-colors shrink-0 hover:bg-muted"
105
- style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}
113
+ style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)' }}
106
114
  >
107
115
  <Search size={14} />
108
116
  <span className="hidden sm:inline">{t.home.shortcuts.searchFiles}</span>
@@ -121,7 +129,6 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
121
129
  style={{
122
130
  background: 'var(--amber-dim)',
123
131
  color: 'var(--amber)',
124
- fontFamily: "'IBM Plex Sans', sans-serif",
125
132
  }}
126
133
  >
127
134
  <ArrowRight size={14} />
@@ -137,7 +144,6 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
137
144
  style={{
138
145
  background: 'var(--muted)',
139
146
  color: 'var(--muted-foreground)',
140
- fontFamily: "'IBM Plex Sans', sans-serif",
141
147
  }}
142
148
  >
143
149
  <FilePlus size={14} />
@@ -147,35 +153,81 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
147
153
 
148
154
  </div>
149
155
 
150
- {/* Plugins — compact 3-column grid */}
156
+ {/* Plugins */}
151
157
  {renderers.length > 0 && (
152
158
  <section className="mb-12">
153
159
  <div className="flex items-center gap-2 mb-4">
154
160
  <Puzzle size={13} style={{ color: 'var(--amber)' }} />
155
- <h2 className="text-xs font-semibold uppercase tracking-[0.08em]" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}>
161
+ <h2 className="text-xs font-semibold uppercase tracking-[0.08em] font-display" style={{ color: 'var(--muted-foreground)' }}>
156
162
  {t.home.plugins}
157
163
  </h2>
158
- <span className="text-xs" style={{ color: 'var(--muted-foreground)', opacity: 0.5 }}>
164
+ <span className="text-xs" style={{ color: 'var(--muted-foreground)', opacity: 0.65 }}>
159
165
  {renderers.length}
160
166
  </span>
161
167
  </div>
162
168
 
163
- <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
169
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 items-start">
164
170
  {renderers.map((r) => {
165
- const entryPath = deriveEntryPath(r.id);
171
+ const entryPath = r.entryPath ?? null;
172
+ const available = !entryPath || existingSet.has(entryPath);
173
+
174
+ if (!available) {
175
+ return (
176
+ <button
177
+ key={r.id}
178
+ onClick={() => showHint(r.id)}
179
+ className="group flex flex-col gap-1.5 px-3.5 py-3 rounded-lg border transition-all opacity-60 cursor-pointer hover:opacity-80 text-left"
180
+ style={{ borderColor: 'var(--border)' }}
181
+ >
182
+ <div className="flex items-center gap-2.5">
183
+ <span className="text-base leading-none shrink-0" suppressHydrationWarning>{r.icon}</span>
184
+ <span className="text-xs font-semibold truncate font-display" style={{ color: 'var(--foreground)' }}>
185
+ {r.name}
186
+ </span>
187
+ </div>
188
+ <p className="text-[11px] leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
189
+ {r.description}
190
+ </p>
191
+ {hintId === r.id ? (
192
+ <p className="text-[10px] animate-in" style={{ color: 'var(--amber)' }} role="status">
193
+ {(t.home.createToActivate ?? 'Create {file} to activate').replace('{file}', entryPath ?? '')}
194
+ </p>
195
+ ) : (
196
+ <div className="flex flex-wrap gap-1">
197
+ {r.tags.slice(0, 3).map(tag => (
198
+ <span key={tag} className="text-[10px] px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
199
+ {tag}
200
+ </span>
201
+ ))}
202
+ </div>
203
+ )}
204
+ </button>
205
+ );
206
+ }
207
+
166
208
  return (
167
209
  <Link
168
210
  key={r.id}
169
211
  href={entryPath ? `/view/${encodePath(entryPath)}` : '#'}
170
- className="group flex items-center gap-2.5 px-3 py-2.5 rounded-lg border transition-all hover:border-amber-500/30 hover:bg-muted/50"
212
+ className="group flex flex-col gap-1.5 px-3.5 py-3 rounded-lg border transition-all hover:border-amber-500/30 hover:bg-muted/50"
171
213
  style={{ borderColor: 'var(--border)' }}
172
214
  >
173
- <span className="text-base leading-none shrink-0" suppressHydrationWarning>{r.icon}</span>
174
- <div className="flex-1 min-w-0">
175
- <span className="text-xs font-semibold truncate block" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}>
215
+ <div className="flex items-center gap-2.5">
216
+ <span className="text-base leading-none shrink-0" suppressHydrationWarning>{r.icon}</span>
217
+ <span className="text-xs font-semibold truncate font-display" style={{ color: 'var(--foreground)' }}>
176
218
  {r.name}
177
219
  </span>
178
220
  </div>
221
+ <p className="text-[11px] leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
222
+ {r.description}
223
+ </p>
224
+ <div className="flex flex-wrap gap-1">
225
+ {r.tags.slice(0, 3).map(tag => (
226
+ <span key={tag} className="text-[10px] px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
227
+ {tag}
228
+ </span>
229
+ ))}
230
+ </div>
179
231
  </Link>
180
232
  );
181
233
  })}
@@ -193,7 +245,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
193
245
  <section className="mb-12">
194
246
  <div className="flex items-center gap-2 mb-5">
195
247
  <Clock size={13} style={{ color: 'var(--amber)' }} />
196
- <h2 className="text-xs font-semibold uppercase tracking-[0.08em]" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}>
248
+ <h2 className="text-xs font-semibold uppercase tracking-[0.08em] font-display" style={{ color: 'var(--muted-foreground)' }}>
197
249
  {t.home.recentlyModified}
198
250
  </h2>
199
251
  </div>
@@ -211,7 +263,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
211
263
  <div key={filePath} className="relative group">
212
264
  {/* Timeline dot */}
213
265
  <div
214
- className="absolute -left-4 top-1/2 -translate-y-1/2 w-1.5 h-1.5 rounded-full transition-all duration-150 group-hover:scale-150"
266
+ className={`absolute -left-4 top-1/2 -translate-y-1/2 rounded-full transition-all duration-150 group-hover:scale-150 ${idx === 0 ? 'w-2 h-2' : 'w-1.5 h-1.5'}`}
215
267
  style={{
216
268
  background: idx === 0 ? 'var(--amber)' : 'var(--border)',
217
269
  outline: idx === 0 ? '2px solid var(--amber-dim)' : 'none',
@@ -229,7 +281,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
229
281
  <span className="text-sm font-medium truncate block" style={{ color: 'var(--foreground)' }} suppressHydrationWarning>{name}</span>
230
282
  {dir && <span className="text-xs truncate block" style={{ color: 'var(--muted-foreground)', opacity: 0.6 }} suppressHydrationWarning>{dir}</span>}
231
283
  </div>
232
- <span className="text-xs shrink-0 tabular-nums" style={{ color: 'var(--muted-foreground)', opacity: 0.5, fontFamily: "'IBM Plex Mono', monospace" }} suppressHydrationWarning>
284
+ <span className="text-xs shrink-0 tabular-nums font-display" style={{ color: 'var(--muted-foreground)', opacity: 0.5 }} suppressHydrationWarning>
233
285
  {formatTime(mtime)}
234
286
  </span>
235
287
  </Link>
@@ -242,8 +294,9 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
242
294
  {hasMore && (
243
295
  <button
244
296
  onClick={() => setShowAll(v => !v)}
245
- className="flex items-center gap-1.5 mt-2 ml-3 text-xs font-medium transition-colors hover:opacity-80 cursor-pointer"
246
- style={{ color: 'var(--amber)', fontFamily: "'IBM Plex Mono', monospace" }}
297
+ aria-expanded={showAll}
298
+ style={{ color: 'var(--amber)' }}
299
+ className="flex items-center gap-1.5 mt-2 ml-3 text-xs font-medium transition-colors hover:opacity-80 cursor-pointer font-display"
247
300
  >
248
301
  <ChevronDown
249
302
  size={12}
@@ -259,7 +312,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
259
312
  })()}
260
313
 
261
314
  {/* Footer */}
262
- <div className="mt-16 flex items-center gap-1.5 text-xs" style={{ color: 'var(--muted-foreground)', opacity: 0.4, fontFamily: "'IBM Plex Mono', monospace" }}>
315
+ <div className="mt-16 flex items-center gap-1.5 text-xs font-display" style={{ color: 'var(--muted-foreground)', opacity: 0.6 }}>
263
316
  <Sparkles size={10} style={{ color: 'var(--amber)' }} />
264
317
  <span>{t.app.footer}</span>
265
318
  </div>
@@ -17,8 +17,7 @@ export default function JsonView({ content }: JsonViewProps) {
17
17
 
18
18
  return (
19
19
  <pre
20
- className="rounded-xl border border-border bg-card px-4 py-3 overflow-x-auto text-sm leading-relaxed"
21
- style={{ fontFamily: "'IBM Plex Mono', monospace" }}
20
+ className="rounded-xl border border-border bg-card px-4 py-3 overflow-x-auto text-sm leading-relaxed font-display"
22
21
  suppressHydrationWarning
23
22
  >
24
23
  <code>{pretty}</code>
@@ -35,12 +35,11 @@ export default function MarkdownEditor({ value, onChange, viewMode, onViewModeCh
35
35
  <button
36
36
  key={m.id}
37
37
  onClick={() => onViewModeChange(m.id)}
38
- className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded text-xs font-medium transition-colors ${
38
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded text-xs font-medium transition-colors font-display ${
39
39
  viewMode === m.id
40
40
  ? 'bg-card text-foreground shadow-sm'
41
41
  : 'text-muted-foreground hover:text-foreground'
42
42
  }`}
43
- style={{ fontFamily: "'IBM Plex Mono', monospace" }}
44
43
  >
45
44
  {m.icon}
46
45
  {m.label}
@@ -62,8 +62,8 @@ export default function OnboardingView() {
62
62
  <div className="inline-flex items-center gap-2 mb-4">
63
63
  <Sparkles size={18} style={{ color: 'var(--amber)' }} />
64
64
  <h1
65
- className="text-2xl font-semibold tracking-tight"
66
- style={{ fontFamily: "'IBM Plex Mono', monospace", color: 'var(--foreground)' }}
65
+ className="text-2xl font-semibold tracking-tight font-display"
66
+ style={{ color: 'var(--foreground)' }}
67
67
  >
68
68
  MindOS
69
69
  </h1>
@@ -94,7 +94,7 @@ export default function OnboardingView() {
94
94
  <span style={{ color: 'var(--amber)' }}>{tpl.icon}</span>
95
95
  <span
96
96
  className="text-sm font-semibold"
97
- style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}
97
+ style={{ color: 'var(--foreground)' }}
98
98
  >
99
99
  {ob.templates[tpl.id].title}
100
100
  </span>
@@ -110,10 +110,9 @@ export default function OnboardingView() {
110
110
 
111
111
  {/* Directory preview */}
112
112
  <div
113
- className="w-full rounded-lg px-3 py-2 text-[11px] leading-relaxed"
113
+ className="w-full rounded-lg px-3 py-2 text-[11px] leading-relaxed font-display"
114
114
  style={{
115
115
  background: 'var(--muted)',
116
- fontFamily: "'IBM Plex Mono', monospace",
117
116
  color: 'var(--muted-foreground)',
118
117
  opacity: 0.8,
119
118
  }}
@@ -129,8 +128,8 @@ export default function OnboardingView() {
129
128
 
130
129
  {/* Import hint */}
131
130
  <p
132
- className="text-center text-xs leading-relaxed max-w-sm mx-auto"
133
- style={{ color: 'var(--muted-foreground)', opacity: 0.6, fontFamily: "'IBM Plex Mono', monospace" }}
131
+ className="text-center text-xs leading-relaxed max-w-sm mx-auto font-display"
132
+ style={{ color: 'var(--muted-foreground)', opacity: 0.6 }}
134
133
  >
135
134
  {ob.importHint}
136
135
  </p>
@@ -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>