@geminilight/mindos 0.3.0 → 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.
- package/app/app/api/mcp/agents/route.ts +72 -0
- package/app/app/api/mcp/install/route.ts +95 -0
- package/app/app/api/mcp/status/route.ts +47 -0
- package/app/app/api/skills/route.ts +208 -0
- package/app/app/api/sync/route.ts +54 -3
- package/app/app/api/update-check/route.ts +52 -0
- package/app/app/globals.css +12 -0
- package/app/app/layout.tsx +4 -2
- package/app/app/login/page.tsx +20 -13
- package/app/app/page.tsx +17 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
- package/app/app/view/[...path]/loading.tsx +1 -1
- package/app/app/view/[...path]/not-found.tsx +101 -0
- package/app/components/AskFab.tsx +1 -1
- package/app/components/AskModal.tsx +1 -1
- package/app/components/Backlinks.tsx +1 -1
- package/app/components/Breadcrumb.tsx +13 -3
- package/app/components/CsvView.tsx +5 -6
- package/app/components/DirView.tsx +42 -21
- package/app/components/FindInPage.tsx +211 -0
- package/app/components/HomeContent.tsx +97 -44
- package/app/components/JsonView.tsx +1 -2
- package/app/components/MarkdownEditor.tsx +1 -2
- package/app/components/OnboardingView.tsx +6 -7
- package/app/components/SettingsModal.tsx +5 -2
- package/app/components/SetupWizard.tsx +4 -4
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/UpdateBanner.tsx +101 -0
- package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
- package/app/components/renderers/agent-inspector/manifest.ts +14 -0
- package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
- package/app/components/renderers/backlinks/manifest.ts +14 -0
- package/app/components/renderers/config/manifest.ts +14 -0
- package/app/components/renderers/csv/BoardView.tsx +12 -12
- package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
- package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
- package/app/components/renderers/csv/GalleryView.tsx +3 -3
- package/app/components/renderers/csv/TableView.tsx +4 -5
- package/app/components/renderers/csv/manifest.ts +14 -0
- package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
- package/app/components/renderers/diff/manifest.ts +14 -0
- package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
- package/app/components/renderers/graph/manifest.ts +14 -0
- package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
- package/app/components/renderers/summary/manifest.ts +14 -0
- package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
- package/app/components/renderers/timeline/manifest.ts +14 -0
- package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
- package/app/components/renderers/todo/manifest.ts +14 -0
- package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
- package/app/components/renderers/workflow/manifest.ts +14 -0
- package/app/components/settings/McpTab.tsx +549 -0
- package/app/components/settings/SyncTab.tsx +139 -50
- package/app/components/settings/types.ts +1 -1
- package/app/data/pages/home.png +0 -0
- package/app/lib/i18n.ts +178 -10
- package/app/lib/renderers/index.ts +20 -89
- package/app/lib/renderers/registry.ts +4 -1
- package/app/lib/settings.ts +3 -0
- package/app/package.json +1 -0
- package/app/types/semver.d.ts +8 -0
- package/bin/cli.js +137 -24
- package/bin/lib/build.js +53 -18
- package/bin/lib/colors.js +3 -1
- package/bin/lib/config.js +4 -0
- package/bin/lib/constants.js +2 -0
- package/bin/lib/debug.js +10 -0
- package/bin/lib/startup.js +21 -20
- package/bin/lib/stop.js +41 -3
- package/bin/lib/sync.js +65 -53
- package/bin/lib/update-check.js +94 -0
- package/bin/lib/utils.js +2 -2
- package/package.json +1 -1
- package/scripts/gen-renderer-index.js +57 -0
- package/scripts/setup.js +24 -0
- /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
|
|
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={{
|
|
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)'
|
|
90
|
-
{
|
|
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)'
|
|
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
|
|
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)'
|
|
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.
|
|
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-
|
|
169
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 items-start">
|
|
164
170
|
{renderers.map((r) => {
|
|
165
|
-
const entryPath =
|
|
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
|
|
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
|
-
<
|
|
174
|
-
|
|
175
|
-
<span className="text-xs font-semibold truncate
|
|
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)'
|
|
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=
|
|
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
|
|
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
|
-
|
|
246
|
-
style={{ color: 'var(--amber)'
|
|
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.
|
|
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={{
|
|
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)'
|
|
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
|
|
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
|
|
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>
|
|
@@ -202,8 +202,8 @@ export default function SetupWizard() {
|
|
|
202
202
|
</span>
|
|
203
203
|
</div>
|
|
204
204
|
<div
|
|
205
|
-
className="w-full rounded-lg px-2.5 py-1.5 text-[11px] leading-relaxed"
|
|
206
|
-
style={{ background: 'var(--muted)',
|
|
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
207
|
>
|
|
208
208
|
{tpl.dirs.map(d => <div key={d}>{d}</div>)}
|
|
209
209
|
</div>
|
|
@@ -418,8 +418,8 @@ export default function SetupWizard() {
|
|
|
418
418
|
<div className="inline-flex items-center gap-2 mb-2">
|
|
419
419
|
<Sparkles size={18} style={{ color: 'var(--amber)' }} />
|
|
420
420
|
<h1
|
|
421
|
-
className="text-2xl font-semibold tracking-tight"
|
|
422
|
-
style={{
|
|
421
|
+
className="text-2xl font-semibold tracking-tight font-display"
|
|
422
|
+
style={{ color: 'var(--foreground)' }}
|
|
423
423
|
>
|
|
424
424
|
MindOS
|
|
425
425
|
</h1>
|
|
@@ -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
|
|
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">
|