@geminilight/mindos 0.5.57 → 0.5.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README_zh.md +1 -1
- package/app/app/api/ask/route.ts +4 -1
- package/app/app/api/extract-pdf/route.ts +4 -2
- package/app/app/api/file/route.ts +54 -2
- package/app/app/api/health/route.ts +3 -1
- package/app/app/api/mcp/install-skill/route.ts +4 -2
- package/app/app/api/mcp/restart/route.ts +1 -1
- package/app/app/api/restart/route.ts +1 -1
- package/app/app/api/skills/route.ts +1 -1
- package/app/app/api/sync/route.ts +2 -2
- package/app/app/api/update/route.ts +1 -1
- package/app/app/api/update-check/route.ts +11 -3
- package/app/app/globals.css +4 -0
- package/app/app/layout.tsx +6 -0
- package/app/app/login/page.tsx +12 -1
- package/app/app/register-sw.tsx +4 -0
- package/app/app/view/[...path]/page.tsx +3 -3
- package/app/components/DirView.tsx +96 -9
- package/app/components/FileTree.tsx +245 -27
- package/app/components/Sidebar.tsx +1 -0
- package/app/components/SidebarLayout.tsx +1 -1
- package/app/components/ask/AskContent.tsx +56 -14
- package/app/instrumentation.ts +2 -1
- package/app/lib/actions.ts +53 -25
- package/app/lib/core/create-space.ts +36 -0
- package/app/lib/core/fs-ops.ts +78 -1
- package/app/lib/core/index.ts +8 -0
- package/app/lib/core/list-spaces.ts +58 -0
- package/app/lib/core/types.ts +7 -0
- package/app/lib/fs.ts +78 -5
- package/app/lib/i18n-en.ts +10 -0
- package/app/lib/i18n-zh.ts +10 -0
- package/app/lib/project-root.ts +13 -0
- package/app/lib/template.ts +13 -6
- package/app/next-env.d.ts +1 -1
- package/bin/lib/mcp-spawn.js +38 -1
- package/mcp/README.md +5 -2
- package/mcp/src/index.ts +80 -0
- package/package.json +1 -1
- package/scripts/setup.js +1 -1
- package/skills/mindos/SKILL.md +4 -1
- package/skills/mindos/references/README.md +11 -0
- package/skills/mindos/references/post-task-hooks.md +53 -0
- package/skills/mindos/references/preference-capture.md +41 -0
- package/skills/mindos/references/sop-template.md +74 -0
- package/skills/mindos-zh/SKILL.md +4 -1
- package/skills/mindos-zh/references/README.md +11 -0
- package/skills/mindos-zh/references/post-task-hooks.md +53 -0
- package/skills/mindos-zh/references/preference-capture.md +41 -0
- package/skills/mindos-zh/references/sop-template.md +74 -0
- package/templates/empty/CONFIG.json +7 -5
- package/templates/empty/INSTRUCTION.md +5 -5
- package/templates/empty/README.md +1 -2
- package/templates/en/CONFIG.json +7 -5
- package/templates/en/INSTRUCTION.md +5 -5
- package/templates/en/README.md +1 -2
- package/templates/en//360/237/223/232 Resources/README.md" +4 -4
- package/templates/en//360/237/223/232 Resources//360/237/247/276 Books.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 Learning Resources.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 People to Follow.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 Tools.csv" +1 -0
- package/templates/en//360/237/223/235 Notes/Ideas//360/237/247/252_example_product_idea.md" +9 -12
- package/templates/en//360/237/224/204 Workflows/Configurations//360/237/247/252_example_config_update_sop.md" +2 -3
- package/templates/en//360/237/224/204 Workflows/INSTRUCTION.md" +13 -5
- package/templates/en//360/237/232/200 Projects/Products//360/237/247/252_example_product_project_brief.md" +12 -14
- package/templates/template-generation-skill.md +4 -5
- package/templates/zh/CONFIG.json +7 -5
- package/templates/zh/INSTRUCTION.md +5 -5
- package/templates/zh/README.md +1 -2
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217/README.md" +4 -4
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217//342/232/231/357/270/{217 Preferences.md" → 217 /345/201/217/345/245/275.md" } +1 -1
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/216/{257 Focus.md" → 257 /350/201/232/347/204/246.md" } +1 -1
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/221/{244 Identity.md" → 244 /350/272/253/344/273/275.md" } +1 -1
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220/README.md" +4 -4
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 /344/271/246/345/215/225.csv" +1 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 /345/200/274/345/276/227/345/205/263/346/263/250/347/232/204/344/272/272.csv" +1 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 /345/255/246/344/271/240/350/265/204/346/272/220.csv" +1 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 /345/267/245/345/205/267/346/270/205/345/215/225.csv" +1 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/203/263/346/263/225//360/237/247/252_example_/344/272/247/345/223/201/346/203/263/346/263/225.md" +8 -11
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213/README.md" +1 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/210/233/344/270/232/README.md" +3 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/210/233/344/270/232//360/237/247/252_example_/346/257/217/345/221/250/345/210/233/345/247/213/344/272/272/350/277/220/350/220/245/350/212/202/345/245/217.md" +22 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//351/205/215/347/275/256//360/237/247/252_example_/351/205/215/347/275/256/346/233/264/346/226/260/346/265/201/347/250/213.md" +1 -1
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256//344/272/247/345/223/201//360/237/247/252_example_/344/272/247/345/223/201/351/241/271/347/233/256/347/256/200/346/212/245.md" +12 -14
- package/templates/empty/CONFIG.md +0 -73
- package/templates/en/CONFIG.md +0 -73
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Influencers.csv" +0 -1
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Products.csv" +0 -1
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Scholars.csv" +0 -1
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Tools.csv" +0 -1
- package/templates/zh/CONFIG.md +0 -66
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI Inferencers.csv" +0 -1
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /344/272/247/345/223/201.csv" +0 -1
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /345/255/246/350/200/205/346/270/205/345/215/225.csv" +0 -1
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /345/267/245/345/205/267/346/270/205/345/215/225.csv" +0 -1
|
@@ -4,16 +4,21 @@ import { useState, useCallback, useRef, useTransition, useEffect } from 'react';
|
|
|
4
4
|
import { useRouter, usePathname } from 'next/navigation';
|
|
5
5
|
import { FileNode } from '@/lib/types';
|
|
6
6
|
import { encodePath } from '@/lib/utils';
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
ChevronDown, FileText, Table, Folder, FolderOpen, Plus, Loader2,
|
|
9
|
+
Trash2, Pencil, Layers, ScrollText,
|
|
10
|
+
} from 'lucide-react';
|
|
11
|
+
import { createFileAction, deleteFileAction, renameFileAction, renameSpaceAction, deleteSpaceAction, convertToSpaceAction, deleteFolderAction } from '@/lib/actions';
|
|
9
12
|
import { useLocale } from '@/lib/LocaleContext';
|
|
10
13
|
|
|
14
|
+
const SYSTEM_FILES = new Set(['INSTRUCTION.md', 'README.md']);
|
|
15
|
+
|
|
11
16
|
interface FileTreeProps {
|
|
12
17
|
nodes: FileNode[];
|
|
13
18
|
depth?: number;
|
|
14
19
|
onNavigate?: () => void;
|
|
15
|
-
/** When set, directories with depth <= this value open, others close. null = no override (manual control). */
|
|
16
20
|
maxOpenDepth?: number | null;
|
|
21
|
+
parentIsSpace?: boolean;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
function getIcon(node: FileNode) {
|
|
@@ -29,12 +34,143 @@ function getCurrentFilePath(pathname: string): string {
|
|
|
29
34
|
return encoded.split('/').map(decodeURIComponent).join('/');
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
function countContentFiles(node: FileNode): number {
|
|
38
|
+
if (node.type === 'file') return SYSTEM_FILES.has(node.name) ? 0 : 1;
|
|
39
|
+
return (node.children ?? []).reduce((sum, c) => sum + countContentFiles(c), 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function filterVisibleNodes(nodes: FileNode[], parentIsSpace: boolean): FileNode[] {
|
|
43
|
+
return nodes.filter(node => {
|
|
44
|
+
if (node.type !== 'file') return true;
|
|
45
|
+
if (parentIsSpace && SYSTEM_FILES.has(node.name)) return false;
|
|
46
|
+
if (!parentIsSpace && node.name === 'README.md') return false;
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Context Menu Shell ───────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function ContextMenuShell({ x, y, onClose, menuHeight, children }: {
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
onClose: () => void;
|
|
57
|
+
menuHeight?: number;
|
|
58
|
+
children: React.ReactNode;
|
|
59
|
+
}) {
|
|
60
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const handler = (e: MouseEvent) => {
|
|
64
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) onClose();
|
|
65
|
+
};
|
|
66
|
+
const keyHandler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
|
67
|
+
document.addEventListener('mousedown', handler);
|
|
68
|
+
document.addEventListener('keydown', keyHandler);
|
|
69
|
+
return () => {
|
|
70
|
+
document.removeEventListener('mousedown', handler);
|
|
71
|
+
document.removeEventListener('keydown', keyHandler);
|
|
72
|
+
};
|
|
73
|
+
}, [onClose]);
|
|
74
|
+
|
|
75
|
+
const adjustedY = Math.min(y, window.innerHeight - (menuHeight ?? 160));
|
|
76
|
+
const adjustedX = Math.min(x, window.innerWidth - 200);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
ref={menuRef}
|
|
81
|
+
className="fixed z-50 min-w-[180px] bg-card border border-border rounded-lg shadow-lg py-1"
|
|
82
|
+
style={{ top: adjustedY, left: adjustedX }}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const MENU_ITEM = "w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-muted transition-colors text-left";
|
|
90
|
+
const MENU_DANGER = "w-full flex items-center gap-2 px-3 py-2 text-sm text-error hover:bg-error/10 transition-colors text-left";
|
|
91
|
+
const MENU_DIVIDER = "my-1 border-t border-border/50";
|
|
92
|
+
|
|
93
|
+
// ─── SpaceContextMenu ─────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function SpaceContextMenu({ x, y, node, onClose, onRename }: {
|
|
96
|
+
x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void;
|
|
97
|
+
}) {
|
|
98
|
+
const router = useRouter();
|
|
99
|
+
const { t } = useLocale();
|
|
100
|
+
const [isPending, startTransition] = useTransition();
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<ContextMenuShell x={x} y={y} onClose={onClose}>
|
|
104
|
+
<button className={MENU_ITEM} onClick={() => { router.push(`/view/${encodePath(`${node.path}/INSTRUCTION.md`)}`); onClose(); }}>
|
|
105
|
+
<ScrollText size={14} className="shrink-0" /> {t.fileTree.editRules}
|
|
106
|
+
</button>
|
|
107
|
+
<button className={MENU_ITEM} onClick={() => { onRename(); onClose(); }}>
|
|
108
|
+
<Pencil size={14} className="shrink-0" /> {t.fileTree.renameSpace}
|
|
109
|
+
</button>
|
|
110
|
+
<div className={MENU_DIVIDER} />
|
|
111
|
+
<button className={MENU_DANGER} disabled={isPending} onClick={() => {
|
|
112
|
+
if (!confirm(t.fileTree.confirmDeleteSpace(node.name))) return;
|
|
113
|
+
startTransition(async () => {
|
|
114
|
+
const result = await deleteSpaceAction(node.path);
|
|
115
|
+
if (result.success) { router.push('/'); router.refresh(); }
|
|
116
|
+
onClose();
|
|
117
|
+
});
|
|
118
|
+
}}>
|
|
119
|
+
<Trash2 size={14} className="shrink-0" />
|
|
120
|
+
{isPending ? <Loader2 size={14} className="animate-spin" /> : t.fileTree.deleteSpace}
|
|
121
|
+
</button>
|
|
122
|
+
</ContextMenuShell>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── FolderContextMenu ────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function FolderContextMenu({ x, y, node, onClose, onRename }: {
|
|
129
|
+
x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void;
|
|
130
|
+
}) {
|
|
131
|
+
const router = useRouter();
|
|
132
|
+
const { t } = useLocale();
|
|
133
|
+
const [isPending, startTransition] = useTransition();
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<ContextMenuShell x={x} y={y} onClose={onClose} menuHeight={140}>
|
|
137
|
+
<button className={MENU_ITEM} disabled={isPending} onClick={() => {
|
|
138
|
+
startTransition(async () => {
|
|
139
|
+
const result = await convertToSpaceAction(node.path);
|
|
140
|
+
if (result.success) router.refresh();
|
|
141
|
+
onClose();
|
|
142
|
+
});
|
|
143
|
+
}}>
|
|
144
|
+
<Layers size={14} className="shrink-0" style={{ color: 'var(--amber)' }} /> {t.fileTree.convertToSpace}
|
|
145
|
+
</button>
|
|
146
|
+
<button className={MENU_ITEM} onClick={() => { onRename(); onClose(); }}>
|
|
147
|
+
<Pencil size={14} className="shrink-0" /> {t.fileTree.rename}
|
|
148
|
+
</button>
|
|
149
|
+
<div className={MENU_DIVIDER} />
|
|
150
|
+
<button className={MENU_DANGER} disabled={isPending} onClick={() => {
|
|
151
|
+
if (!confirm(t.fileTree.confirmDeleteFolder(node.name))) return;
|
|
152
|
+
startTransition(async () => {
|
|
153
|
+
const result = await deleteFolderAction(node.path);
|
|
154
|
+
if (result.success) { router.push('/'); router.refresh(); }
|
|
155
|
+
onClose();
|
|
156
|
+
});
|
|
157
|
+
}}>
|
|
158
|
+
<Trash2 size={14} className="shrink-0" />
|
|
159
|
+
{isPending ? <Loader2 size={14} className="animate-spin" /> : t.fileTree.deleteFolder}
|
|
160
|
+
</button>
|
|
161
|
+
</ContextMenuShell>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── NewFileInline ────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
32
167
|
function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: number; onDone: () => void }) {
|
|
33
168
|
const [value, setValue] = useState('');
|
|
34
169
|
const [isPending, startTransition] = useTransition();
|
|
35
170
|
const [error, setError] = useState('');
|
|
36
171
|
const router = useRouter();
|
|
37
172
|
const { t } = useLocale();
|
|
173
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
38
174
|
|
|
39
175
|
const handleSubmit = useCallback(() => {
|
|
40
176
|
const name = value.trim();
|
|
@@ -51,8 +187,18 @@ function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: num
|
|
|
51
187
|
});
|
|
52
188
|
}, [value, dirPath, onDone, router, t]);
|
|
53
189
|
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const handler = (e: MouseEvent) => {
|
|
192
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
193
|
+
onDone();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const timer = setTimeout(() => document.addEventListener('mousedown', handler), 0);
|
|
197
|
+
return () => { clearTimeout(timer); document.removeEventListener('mousedown', handler); };
|
|
198
|
+
}, [onDone]);
|
|
199
|
+
|
|
54
200
|
return (
|
|
55
|
-
<div className="px-2 pb-1" style={{ paddingLeft: `${depth * 12 + 20}px` }}>
|
|
201
|
+
<div ref={containerRef} className="px-2 pb-1" style={{ paddingLeft: `${depth * 12 + 20}px` }}>
|
|
56
202
|
<div className="flex items-center gap-1">
|
|
57
203
|
<input
|
|
58
204
|
autoFocus
|
|
@@ -87,12 +233,15 @@ function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: num
|
|
|
87
233
|
);
|
|
88
234
|
}
|
|
89
235
|
|
|
236
|
+
// ─── DirectoryNode ────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
90
238
|
function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
91
239
|
node: FileNode; depth: number; currentPath: string; onNavigate?: () => void;
|
|
92
240
|
maxOpenDepth?: number | null;
|
|
93
241
|
}) {
|
|
94
242
|
const router = useRouter();
|
|
95
243
|
const isActive = currentPath.startsWith(node.path + '/') || currentPath === node.path;
|
|
244
|
+
const isSpace = !!node.isSpace;
|
|
96
245
|
const [open, setOpen] = useState(depth === 0 ? true : isActive);
|
|
97
246
|
const [showNewFile, setShowNewFile] = useState(false);
|
|
98
247
|
const [renaming, setRenaming] = useState(false);
|
|
@@ -100,18 +249,17 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
100
249
|
const [isPending, startTransition] = useTransition();
|
|
101
250
|
const renameRef = useRef<HTMLInputElement>(null);
|
|
102
251
|
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
252
|
+
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
|
103
253
|
const { t } = useLocale();
|
|
104
254
|
|
|
105
255
|
const toggle = useCallback(() => setOpen(v => !v), []);
|
|
106
256
|
|
|
107
|
-
// React to maxOpenDepth changes from parent
|
|
108
257
|
const prevMaxOpenDepth = useRef<number | null | undefined>(undefined);
|
|
109
258
|
useEffect(() => {
|
|
110
259
|
if (maxOpenDepth === null || maxOpenDepth === undefined) {
|
|
111
260
|
prevMaxOpenDepth.current = maxOpenDepth;
|
|
112
261
|
return;
|
|
113
262
|
}
|
|
114
|
-
// Only react when value actually changes
|
|
115
263
|
if (prevMaxOpenDepth.current !== maxOpenDepth) {
|
|
116
264
|
setOpen(depth <= maxOpenDepth);
|
|
117
265
|
prevMaxOpenDepth.current = maxOpenDepth;
|
|
@@ -140,7 +288,8 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
140
288
|
const newName = renameValue.trim();
|
|
141
289
|
if (!newName || newName === node.name) { setRenaming(false); return; }
|
|
142
290
|
startTransition(async () => {
|
|
143
|
-
const
|
|
291
|
+
const action = isSpace ? renameSpaceAction : renameFileAction;
|
|
292
|
+
const result = await action(node.path, newName);
|
|
144
293
|
if (result.success && result.newPath) {
|
|
145
294
|
setRenaming(false);
|
|
146
295
|
router.push(`/view/${encodePath(result.newPath)}`);
|
|
@@ -149,7 +298,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
149
298
|
setRenaming(false);
|
|
150
299
|
}
|
|
151
300
|
});
|
|
152
|
-
}, [renameValue, node.name, node.path, router]);
|
|
301
|
+
}, [renameValue, node.name, node.path, router, isSpace]);
|
|
153
302
|
|
|
154
303
|
const handleSingleClick = useCallback(() => {
|
|
155
304
|
if (renaming) return;
|
|
@@ -162,8 +311,17 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
162
311
|
}, [renaming, router, node.path, onNavigate]);
|
|
163
312
|
|
|
164
313
|
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
|
|
314
|
+
if (isSpace) return;
|
|
165
315
|
startRename(e);
|
|
166
|
-
}, [startRename]);
|
|
316
|
+
}, [startRename, isSpace]);
|
|
317
|
+
|
|
318
|
+
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
e.stopPropagation();
|
|
321
|
+
setContextMenu({ x: e.clientX, y: e.clientY });
|
|
322
|
+
}, []);
|
|
323
|
+
|
|
324
|
+
const contentCount = isSpace ? countContentFiles(node) : 0;
|
|
167
325
|
|
|
168
326
|
if (renaming) {
|
|
169
327
|
return (
|
|
@@ -185,9 +343,14 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
185
343
|
);
|
|
186
344
|
}
|
|
187
345
|
|
|
346
|
+
const showBorder = isSpace && depth === 0;
|
|
347
|
+
|
|
188
348
|
return (
|
|
189
349
|
<div>
|
|
190
|
-
<div
|
|
350
|
+
<div
|
|
351
|
+
className="relative group/dir flex items-center"
|
|
352
|
+
onContextMenu={handleContextMenu}
|
|
353
|
+
>
|
|
191
354
|
<button
|
|
192
355
|
onClick={toggle}
|
|
193
356
|
className="shrink-0 p-1 rounded hover:bg-muted text-zinc-500 transition-colors"
|
|
@@ -209,11 +372,16 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
209
372
|
${isActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground'}
|
|
210
373
|
`}
|
|
211
374
|
>
|
|
212
|
-
{
|
|
213
|
-
? <
|
|
214
|
-
:
|
|
375
|
+
{isSpace
|
|
376
|
+
? <Layers size={14} className="shrink-0" style={{ color: 'var(--amber)' }} />
|
|
377
|
+
: open
|
|
378
|
+
? <FolderOpen size={14} className="text-yellow-400 shrink-0" />
|
|
379
|
+
: <Folder size={14} className="text-yellow-400 shrink-0" />
|
|
215
380
|
}
|
|
216
381
|
<span className="truncate leading-5" suppressHydrationWarning>{node.name}</span>
|
|
382
|
+
{isSpace && !open && (
|
|
383
|
+
<span className="ml-auto text-xs text-muted-foreground shrink-0 tabular-nums pr-1">{contentCount}</span>
|
|
384
|
+
)}
|
|
217
385
|
</button>
|
|
218
386
|
<div className="absolute right-1 top-1/2 -translate-y-1/2 hidden group-hover/dir:flex items-center gap-0.5 z-10">
|
|
219
387
|
<button
|
|
@@ -229,36 +397,81 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
229
397
|
>
|
|
230
398
|
<Plus size={13} />
|
|
231
399
|
</button>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
400
|
+
{isSpace ? (
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
onClick={(e) => {
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
e.stopPropagation();
|
|
406
|
+
router.push(`/view/${encodePath(`${node.path}/INSTRUCTION.md`)}`);
|
|
407
|
+
onNavigate?.();
|
|
408
|
+
}}
|
|
409
|
+
className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
410
|
+
title={t.fileTree.editRules}
|
|
411
|
+
>
|
|
412
|
+
<ScrollText size={12} />
|
|
413
|
+
</button>
|
|
414
|
+
) : (
|
|
415
|
+
<button
|
|
416
|
+
type="button"
|
|
417
|
+
onClick={startRename}
|
|
418
|
+
className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
419
|
+
title={t.fileTree.rename}
|
|
420
|
+
>
|
|
421
|
+
<Pencil size={12} />
|
|
422
|
+
</button>
|
|
423
|
+
)}
|
|
240
424
|
</div>
|
|
241
425
|
</div>
|
|
242
426
|
|
|
243
427
|
<div
|
|
244
|
-
className=
|
|
245
|
-
style={{
|
|
428
|
+
className={`overflow-hidden transition-all duration-200 ${showBorder ? 'border-l-2 ml-[18px]' : ''}`}
|
|
429
|
+
style={{
|
|
430
|
+
maxHeight: open ? '9999px' : '0px',
|
|
431
|
+
...(showBorder ? { borderColor: 'color-mix(in srgb, var(--amber) 30%, transparent)' } : {}),
|
|
432
|
+
}}
|
|
246
433
|
>
|
|
247
434
|
{node.children && (
|
|
248
|
-
<FileTree
|
|
435
|
+
<FileTree
|
|
436
|
+
nodes={node.children}
|
|
437
|
+
depth={showBorder ? 1 : depth + 1}
|
|
438
|
+
onNavigate={onNavigate}
|
|
439
|
+
maxOpenDepth={maxOpenDepth}
|
|
440
|
+
parentIsSpace={isSpace}
|
|
441
|
+
/>
|
|
249
442
|
)}
|
|
250
443
|
{showNewFile && (
|
|
251
444
|
<NewFileInline
|
|
252
445
|
dirPath={node.path}
|
|
253
|
-
depth={depth}
|
|
446
|
+
depth={showBorder ? 0 : depth}
|
|
254
447
|
onDone={() => setShowNewFile(false)}
|
|
255
448
|
/>
|
|
256
449
|
)}
|
|
257
450
|
</div>
|
|
451
|
+
|
|
452
|
+
{contextMenu && (isSpace ? (
|
|
453
|
+
<SpaceContextMenu
|
|
454
|
+
x={contextMenu.x}
|
|
455
|
+
y={contextMenu.y}
|
|
456
|
+
node={node}
|
|
457
|
+
onClose={() => setContextMenu(null)}
|
|
458
|
+
onRename={() => startRename()}
|
|
459
|
+
/>
|
|
460
|
+
) : (
|
|
461
|
+
<FolderContextMenu
|
|
462
|
+
x={contextMenu.x}
|
|
463
|
+
y={contextMenu.y}
|
|
464
|
+
node={node}
|
|
465
|
+
onClose={() => setContextMenu(null)}
|
|
466
|
+
onRename={() => startRename()}
|
|
467
|
+
/>
|
|
468
|
+
))}
|
|
258
469
|
</div>
|
|
259
470
|
);
|
|
260
471
|
}
|
|
261
472
|
|
|
473
|
+
// ─── FileNodeItem ─────────────────────────────────────────────────────────────
|
|
474
|
+
|
|
262
475
|
function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
263
476
|
node: FileNode; depth: number; currentPath: string; onNavigate?: () => void;
|
|
264
477
|
}) {
|
|
@@ -359,10 +572,15 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
359
572
|
);
|
|
360
573
|
}
|
|
361
574
|
|
|
362
|
-
|
|
575
|
+
// ─── FileTree (root) ──────────────────────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
export default function FileTree({ nodes, depth = 0, onNavigate, maxOpenDepth, parentIsSpace }: FileTreeProps) {
|
|
363
578
|
const pathname = usePathname();
|
|
364
579
|
const currentPath = getCurrentFilePath(pathname);
|
|
365
580
|
|
|
581
|
+
const isInsideDir = depth > 0;
|
|
582
|
+
const visibleNodes = isInsideDir ? filterVisibleNodes(nodes, !!parentIsSpace) : nodes;
|
|
583
|
+
|
|
366
584
|
useEffect(() => {
|
|
367
585
|
if (!currentPath || depth !== 0) return;
|
|
368
586
|
const timer = setTimeout(() => {
|
|
@@ -374,7 +592,7 @@ export default function FileTree({ nodes, depth = 0, onNavigate, maxOpenDepth }:
|
|
|
374
592
|
|
|
375
593
|
return (
|
|
376
594
|
<div className="flex flex-col gap-0.5">
|
|
377
|
-
{
|
|
595
|
+
{visibleNodes.map((node) =>
|
|
378
596
|
node.type === 'directory' ? (
|
|
379
597
|
<DirectoryNode key={node.path} node={node} depth={depth} currentPath={currentPath} onNavigate={onNavigate} maxOpenDepth={maxOpenDepth} />
|
|
380
598
|
) : (
|
|
@@ -90,6 +90,7 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
|
|
|
90
90
|
|
|
91
91
|
const sidebarContent = (
|
|
92
92
|
<div className="flex flex-col h-full">
|
|
93
|
+
<div className="shrink-0 electron-mac-titlebar-pad" />
|
|
93
94
|
<div className="flex items-center justify-between px-4 py-4 border-b border-border shrink-0">
|
|
94
95
|
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
|
95
96
|
<Logo id="desktop" />
|
|
@@ -376,7 +376,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
376
376
|
#main-content {
|
|
377
377
|
padding-left: ${lp.panelOpen && lp.panelMaximized ? '100vw' : `${lp.panelOpen ? lp.railWidth + lp.effectivePanelWidth : lp.railWidth}px`} !important;
|
|
378
378
|
padding-right: calc(var(--right-panel-width) + var(--right-agent-detail-width)) !important;
|
|
379
|
-
padding-top: 0
|
|
379
|
+
padding-top: 0;
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
`}</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
|
|
4
4
|
import { Sparkles, Send, AtSign, Paperclip, StopCircle, RotateCcw, History, X, Maximize2, Minimize2, PanelRight, AppWindow } from 'lucide-react';
|
|
5
5
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
6
|
import type { Message } from '@/lib/types';
|
|
@@ -21,6 +21,27 @@ const PANEL_COMPOSER_MIN = 84;
|
|
|
21
21
|
const PANEL_COMPOSER_MAX_ABS = 440;
|
|
22
22
|
const PANEL_COMPOSER_MAX_VIEW = 0.48;
|
|
23
23
|
const PANEL_COMPOSER_KEY_STEP = 24;
|
|
24
|
+
/** 输入框随内容增高,超过此行数后在框内滚动(与常见 IM 一致) */
|
|
25
|
+
const PANEL_TEXTAREA_MAX_VISIBLE_LINES = 8;
|
|
26
|
+
|
|
27
|
+
function syncPanelTextareaToContent(el: HTMLTextAreaElement, maxVisibleLines: number, availableHeight?: number): void {
|
|
28
|
+
const style = getComputedStyle(el);
|
|
29
|
+
const parsedLh = parseFloat(style.lineHeight);
|
|
30
|
+
const parsedFs = parseFloat(style.fontSize);
|
|
31
|
+
const fontSize = Number.isFinite(parsedFs) ? parsedFs : 14;
|
|
32
|
+
const lineHeight = Number.isFinite(parsedLh) ? parsedLh : fontSize * 1.375;
|
|
33
|
+
const pad =
|
|
34
|
+
(Number.isFinite(parseFloat(style.paddingTop)) ? parseFloat(style.paddingTop) : 0) +
|
|
35
|
+
(Number.isFinite(parseFloat(style.paddingBottom)) ? parseFloat(style.paddingBottom) : 0);
|
|
36
|
+
let maxH = lineHeight * maxVisibleLines + pad;
|
|
37
|
+
if (availableHeight && Number.isFinite(availableHeight) && availableHeight > 0) {
|
|
38
|
+
maxH = Math.min(maxH, availableHeight);
|
|
39
|
+
}
|
|
40
|
+
if (!Number.isFinite(maxH) || maxH <= 0) return;
|
|
41
|
+
el.style.height = '0px';
|
|
42
|
+
const next = Math.min(el.scrollHeight, maxH);
|
|
43
|
+
el.style.height = `${Number.isFinite(next) ? next : maxH}px`;
|
|
44
|
+
}
|
|
24
45
|
|
|
25
46
|
function panelComposerMaxForViewport(): number {
|
|
26
47
|
if (typeof window === 'undefined') return PANEL_COMPOSER_MAX_ABS;
|
|
@@ -67,10 +88,18 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
67
88
|
const firstMessageFired = useRef(false);
|
|
68
89
|
const { t } = useLocale();
|
|
69
90
|
|
|
70
|
-
const [panelComposerHeight, setPanelComposerHeight] = useState(
|
|
91
|
+
const [panelComposerHeight, setPanelComposerHeight] = useState(PANEL_COMPOSER_DEFAULT);
|
|
71
92
|
const panelComposerHRef = useRef(panelComposerHeight);
|
|
72
93
|
panelComposerHRef.current = panelComposerHeight;
|
|
73
94
|
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const stored = readStoredPanelComposerHeight();
|
|
97
|
+
if (stored !== PANEL_COMPOSER_DEFAULT) {
|
|
98
|
+
setPanelComposerHeight(stored);
|
|
99
|
+
panelComposerHRef.current = stored;
|
|
100
|
+
}
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
74
103
|
const getPanelComposerHeight = useCallback(() => panelComposerHRef.current, []);
|
|
75
104
|
const persistPanelComposerHeight = useCallback((h: number) => {
|
|
76
105
|
try {
|
|
@@ -89,7 +118,11 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
89
118
|
persist: persistPanelComposerHeight,
|
|
90
119
|
});
|
|
91
120
|
|
|
92
|
-
const [panelComposerViewportMax, setPanelComposerViewportMax] = useState(
|
|
121
|
+
const [panelComposerViewportMax, setPanelComposerViewportMax] = useState(PANEL_COMPOSER_MAX_ABS);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
setPanelComposerViewportMax(panelComposerMaxForViewport());
|
|
125
|
+
}, []);
|
|
93
126
|
|
|
94
127
|
const applyPanelComposerClampAndPersist = useCallback(() => {
|
|
95
128
|
const maxH = panelComposerMaxForViewport();
|
|
@@ -195,11 +228,28 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
195
228
|
return () => window.removeEventListener('resize', applyPanelComposerClampAndPersist);
|
|
196
229
|
}, [isPanel, applyPanelComposerClampAndPersist]);
|
|
197
230
|
|
|
231
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
232
|
+
|
|
233
|
+
useLayoutEffect(() => {
|
|
234
|
+
if (!isPanel || !visible) return;
|
|
235
|
+
const el = inputRef.current;
|
|
236
|
+
if (!el || !(el instanceof HTMLTextAreaElement)) return;
|
|
237
|
+
const form = formRef.current;
|
|
238
|
+
const availableH = form ? form.clientHeight - 40 : undefined;
|
|
239
|
+
syncPanelTextareaToContent(el, PANEL_TEXTAREA_MAX_VISIBLE_LINES, availableH);
|
|
240
|
+
}, [input, isPanel, isLoading, visible, panelComposerHeight]);
|
|
241
|
+
|
|
242
|
+
const mentionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
198
243
|
const handleInputChange = useCallback((val: string) => {
|
|
199
244
|
setInput(val);
|
|
200
|
-
|
|
245
|
+
if (mentionTimerRef.current) clearTimeout(mentionTimerRef.current);
|
|
246
|
+
mentionTimerRef.current = setTimeout(() => mention.updateMentionFromInput(val), 80);
|
|
201
247
|
}, [mention]);
|
|
202
248
|
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
return () => { if (mentionTimerRef.current) clearTimeout(mentionTimerRef.current); };
|
|
251
|
+
}, []);
|
|
252
|
+
|
|
203
253
|
const selectMention = useCallback((filePath: string) => {
|
|
204
254
|
const atIdx = input.lastIndexOf('@');
|
|
205
255
|
setInput(input.slice(0, atIdx));
|
|
@@ -498,6 +548,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
498
548
|
)}
|
|
499
549
|
|
|
500
550
|
<form
|
|
551
|
+
ref={formRef}
|
|
501
552
|
onSubmit={handleSubmit}
|
|
502
553
|
className={cn(
|
|
503
554
|
'flex',
|
|
@@ -551,7 +602,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
551
602
|
placeholder={t.ask.placeholder}
|
|
552
603
|
disabled={isLoading}
|
|
553
604
|
rows={1}
|
|
554
|
-
className="min-
|
|
605
|
+
className="min-w-0 flex-1 resize-none overflow-y-auto bg-transparent py-2 text-sm leading-snug text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50"
|
|
555
606
|
/>
|
|
556
607
|
) : (
|
|
557
608
|
<input
|
|
@@ -598,15 +649,6 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
598
649
|
<kbd className="font-mono ml-0.5">↵</kbd> {t.ask.newlineHint}
|
|
599
650
|
</span>
|
|
600
651
|
) : null}
|
|
601
|
-
{isPanel ? (
|
|
602
|
-
<span
|
|
603
|
-
className="hidden sm:inline"
|
|
604
|
-
suppressHydrationWarning
|
|
605
|
-
title={`${t.ask.panelComposerResize} · ${t.ask.panelComposerResetHint} · ${t.ask.panelComposerKeyboard}`}
|
|
606
|
-
>
|
|
607
|
-
<kbd className="font-mono">↕</kbd> {t.ask.panelComposerFooter}
|
|
608
|
-
</span>
|
|
609
|
-
) : null}
|
|
610
652
|
<span suppressHydrationWarning>
|
|
611
653
|
<kbd className="font-mono">@</kbd> {t.ask.attachFile}
|
|
612
654
|
</span>
|
package/app/instrumentation.ts
CHANGED
|
@@ -11,7 +11,8 @@ export async function register() {
|
|
|
11
11
|
// createRequire() calls. The only way to load a runtime-computed path
|
|
12
12
|
// is to hide the require call inside a Function constructor, which is
|
|
13
13
|
// opaque to bundler static analysis.
|
|
14
|
-
const
|
|
14
|
+
const projRoot = process.env.MINDOS_PROJECT_ROOT || resolve(process.cwd(), '..');
|
|
15
|
+
const syncModule = resolve(projRoot, 'bin', 'lib', 'sync.js');
|
|
15
16
|
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
16
17
|
const dynamicRequire = new Function('id', 'return require(id)') as (id: string) => any;
|
|
17
18
|
const { startSyncDaemon } = dynamicRequire(syncModule);
|
package/app/lib/actions.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use server';
|
|
2
2
|
|
|
3
|
-
import { createFile, deleteFile, renameFile } from '@/lib/fs';
|
|
3
|
+
import { createFile, deleteFile, deleteDirectory, convertToSpace, renameFile, renameSpace, getMindRoot, invalidateCache } from '@/lib/fs';
|
|
4
|
+
import { createSpaceFilesystem } from '@/lib/core/create-space';
|
|
4
5
|
import { revalidatePath } from 'next/cache';
|
|
5
6
|
|
|
6
7
|
export async function createFileAction(dirPath: string, fileName: string): Promise<{ success: boolean; filePath?: string; error?: string }> {
|
|
@@ -39,6 +40,55 @@ export async function renameFileAction(oldPath: string, newName: string): Promis
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
export async function convertToSpaceAction(
|
|
44
|
+
dirPath: string,
|
|
45
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
46
|
+
try {
|
|
47
|
+
convertToSpace(dirPath);
|
|
48
|
+
revalidatePath('/', 'layout');
|
|
49
|
+
return { success: true };
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return { success: false, error: err instanceof Error ? err.message : 'Failed to convert to space' };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function deleteFolderAction(
|
|
56
|
+
dirPath: string,
|
|
57
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
58
|
+
try {
|
|
59
|
+
deleteDirectory(dirPath);
|
|
60
|
+
revalidatePath('/', 'layout');
|
|
61
|
+
return { success: true };
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return { success: false, error: err instanceof Error ? err.message : 'Failed to delete folder' };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function renameSpaceAction(
|
|
68
|
+
spacePath: string,
|
|
69
|
+
newName: string,
|
|
70
|
+
): Promise<{ success: boolean; newPath?: string; error?: string }> {
|
|
71
|
+
try {
|
|
72
|
+
const newPath = renameSpace(spacePath, newName);
|
|
73
|
+
revalidatePath('/', 'layout');
|
|
74
|
+
return { success: true, newPath };
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return { success: false, error: err instanceof Error ? err.message : 'Failed to rename space' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function deleteSpaceAction(
|
|
81
|
+
spacePath: string,
|
|
82
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
83
|
+
try {
|
|
84
|
+
deleteDirectory(spacePath);
|
|
85
|
+
revalidatePath('/', 'layout');
|
|
86
|
+
return { success: true };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return { success: false, error: err instanceof Error ? err.message : 'Failed to delete space' };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
42
92
|
/**
|
|
43
93
|
* Create a new Mind Space (top-level directory) with README.md + auto-scaffolded INSTRUCTION.md.
|
|
44
94
|
* The description is written into README.md so it appears on the homepage Space card
|
|
@@ -50,34 +100,12 @@ export async function createSpaceAction(
|
|
|
50
100
|
parentPath: string = ''
|
|
51
101
|
): Promise<{ success: boolean; path?: string; error?: string }> {
|
|
52
102
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
if (trimmed.includes('/') || trimmed.includes('\\')) {
|
|
56
|
-
return { success: false, error: 'Space name must not contain path separators' };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Sanitize parentPath — reject traversal attempts
|
|
60
|
-
const cleanParent = parentPath.replace(/\/+$/, '').trim();
|
|
61
|
-
if (cleanParent.includes('..') || cleanParent.startsWith('/') || cleanParent.includes('\\')) {
|
|
62
|
-
return { success: false, error: 'Invalid parent path' };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Build full path: parentPath + name
|
|
66
|
-
const prefix = cleanParent ? cleanParent + '/' : '';
|
|
67
|
-
const fullPath = `${prefix}${trimmed}`;
|
|
68
|
-
|
|
69
|
-
// Strip emoji for clean title in README content
|
|
70
|
-
const cleanName = trimmed.replace(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}\s]+/u, '') || trimmed;
|
|
71
|
-
const desc = description.trim() || '(Describe the purpose and usage of this space.)';
|
|
72
|
-
const readmeContent = `# ${cleanName}\n\n${desc}\n\n## 📁 Structure\n\n\`\`\`bash\n${fullPath}/\n├── INSTRUCTION.md\n├── README.md\n└── (your files here)\n\`\`\`\n\n## 💡 Usage\n\n(Add usage guidelines for this space.)\n`;
|
|
73
|
-
|
|
74
|
-
// createFile triggers scaffoldIfNewSpace → auto-generates INSTRUCTION.md
|
|
75
|
-
createFile(`${fullPath}/README.md`, readmeContent);
|
|
103
|
+
const { path: fullPath } = createSpaceFilesystem(getMindRoot(), name, description, parentPath);
|
|
104
|
+
invalidateCache();
|
|
76
105
|
revalidatePath('/', 'layout');
|
|
77
106
|
return { success: true, path: fullPath };
|
|
78
107
|
} catch (err) {
|
|
79
108
|
const msg = err instanceof Error ? err.message : 'Failed to create space';
|
|
80
|
-
// Make "already exists" error more user-friendly
|
|
81
109
|
if (msg.includes('already exists')) {
|
|
82
110
|
return { success: false, error: 'A space with this name already exists' };
|
|
83
111
|
}
|