@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.
- package/app/app/api/init/route.ts +7 -41
- 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/settings/route.ts +3 -0
- package/app/app/api/setup/generate-token/route.ts +23 -0
- package/app/app/api/setup/route.ts +81 -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 +22 -2
- package/app/app/setup/page.tsx +9 -0
- 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 +479 -0
- 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 +270 -10
- package/app/lib/renderers/index.ts +20 -89
- package/app/lib/renderers/registry.ts +4 -1
- package/app/lib/settings.ts +15 -1
- package/app/lib/template.ts +45 -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 +117 -1
- /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useSyncExternalStore, useMemo } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import { FileText, Table, Folder, FolderOpen, LayoutGrid, List } from 'lucide-react';
|
|
5
|
+
import { FileText, Table, Folder, FolderOpen, LayoutGrid, List, FilePlus } from 'lucide-react';
|
|
6
6
|
import Breadcrumb from '@/components/Breadcrumb';
|
|
7
|
-
import { encodePath } from '@/lib/utils';
|
|
7
|
+
import { encodePath, relativeTime } from '@/lib/utils';
|
|
8
8
|
import { FileNode } from '@/lib/types';
|
|
9
9
|
import { useLocale } from '@/lib/LocaleContext';
|
|
10
10
|
|
|
@@ -57,6 +57,7 @@ function useDirViewPref() {
|
|
|
57
57
|
export default function DirView({ dirPath, entries }: DirViewProps) {
|
|
58
58
|
const [view, setView] = useDirViewPref();
|
|
59
59
|
const { t } = useLocale();
|
|
60
|
+
const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
|
|
60
61
|
const fileCounts = useMemo(() => {
|
|
61
62
|
const map = new Map<string, number>();
|
|
62
63
|
for (const e of entries) map.set(e.path, countFiles(e));
|
|
@@ -71,22 +72,31 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
|
|
|
71
72
|
<div className="min-w-0 flex-1">
|
|
72
73
|
<Breadcrumb filePath={dirPath} />
|
|
73
74
|
</div>
|
|
74
|
-
{/* View toggle */}
|
|
75
|
-
<div className="flex items-center gap-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
className=
|
|
79
|
-
title={t.dirView.gridView}
|
|
75
|
+
{/* New file + View toggle */}
|
|
76
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
77
|
+
<Link
|
|
78
|
+
href={`/view/${encodePath(dirPath ? `${dirPath}/Untitled.md` : 'Untitled.md')}`}
|
|
79
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg transition-colors text-muted-foreground hover:text-foreground hover:bg-muted"
|
|
80
80
|
>
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
<FilePlus size={13} />
|
|
82
|
+
<span className="hidden sm:inline">{t.dirView.newFile}</span>
|
|
83
|
+
</Link>
|
|
84
|
+
<div className="flex items-center gap-1 p-1 bg-muted rounded-lg">
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => setView('grid')}
|
|
87
|
+
className={`p-1.5 rounded transition-colors ${view === 'grid' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
|
|
88
|
+
title={t.dirView.gridView}
|
|
89
|
+
>
|
|
90
|
+
<LayoutGrid size={14} />
|
|
91
|
+
</button>
|
|
92
|
+
<button
|
|
93
|
+
onClick={() => setView('list')}
|
|
94
|
+
className={`p-1.5 rounded transition-colors ${view === 'list' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
|
|
95
|
+
title={t.dirView.listView}
|
|
96
|
+
>
|
|
97
|
+
<List size={14} />
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
90
100
|
</div>
|
|
91
101
|
</div>
|
|
92
102
|
</div>
|
|
@@ -102,15 +112,26 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
|
|
|
102
112
|
<Link
|
|
103
113
|
key={entry.path}
|
|
104
114
|
href={`/view/${encodePath(entry.path)}`}
|
|
105
|
-
className=
|
|
115
|
+
className={
|
|
116
|
+
entry.type === 'directory'
|
|
117
|
+
? 'flex flex-col items-center gap-1.5 p-3 rounded-xl border border-border bg-card hover:bg-accent hover:border-border/80 transition-all duration-100 text-center'
|
|
118
|
+
: 'flex flex-col items-center gap-2 p-4 rounded-xl border border-border bg-card hover:bg-accent hover:border-border/80 transition-all duration-100 text-center'
|
|
119
|
+
}
|
|
106
120
|
>
|
|
107
|
-
|
|
121
|
+
{entry.type === 'directory'
|
|
122
|
+
? <FolderOpen size={22} className="text-yellow-400" />
|
|
123
|
+
: <FileIconLarge node={entry} />}
|
|
108
124
|
<span className="text-xs text-foreground leading-snug line-clamp-2 w-full" suppressHydrationWarning>
|
|
109
125
|
{entry.name}
|
|
110
126
|
</span>
|
|
111
127
|
{entry.type === 'directory' && (
|
|
112
128
|
<span className="text-[10px] text-muted-foreground">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
|
|
113
129
|
)}
|
|
130
|
+
{entry.type === 'file' && entry.mtime && (
|
|
131
|
+
<span className="text-[10px] text-muted-foreground font-display" suppressHydrationWarning>
|
|
132
|
+
{formatTime(entry.mtime)}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
114
135
|
</Link>
|
|
115
136
|
))}
|
|
116
137
|
</div>
|
|
@@ -129,8 +150,8 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
|
|
|
129
150
|
{entry.type === 'directory' ? (
|
|
130
151
|
<span className="text-xs text-muted-foreground shrink-0">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
|
|
131
152
|
) : entry.mtime ? (
|
|
132
|
-
<span className="text-xs text-muted-foreground shrink-0 tabular-nums
|
|
133
|
-
{
|
|
153
|
+
<span className="text-xs text-muted-foreground shrink-0 tabular-nums font-display" suppressHydrationWarning>
|
|
154
|
+
{formatTime(entry.mtime)}
|
|
134
155
|
</span>
|
|
135
156
|
) : null}
|
|
136
157
|
</Link>
|
|
@@ -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>
|