@geminilight/mindos 0.5.69 → 0.5.70
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/file/import/route.ts +197 -0
- package/app/components/ActivityBar.tsx +3 -4
- package/app/components/FileTree.tsx +35 -9
- package/app/components/ImportModal.tsx +415 -0
- package/app/components/OnboardingView.tsx +9 -0
- package/app/components/Panel.tsx +4 -2
- package/app/components/SidebarLayout.tsx +83 -8
- package/app/components/TableOfContents.tsx +1 -0
- package/app/components/agents/AgentDetailContent.tsx +37 -28
- package/app/components/agents/AgentsMcpSection.tsx +16 -12
- package/app/components/agents/AgentsOverviewSection.tsx +48 -34
- package/app/components/agents/AgentsPrimitives.tsx +41 -20
- package/app/components/agents/AgentsSkillsSection.tsx +16 -7
- package/app/components/agents/SkillDetailPopover.tsx +11 -11
- package/app/components/ask/AskContent.tsx +11 -0
- package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -6
- package/app/components/panels/AgentsPanelHubNav.tsx +3 -3
- package/app/components/panels/DiscoverPanel.tsx +88 -2
- package/app/hooks/useFileImport.ts +191 -0
- package/app/hooks/useFileUpload.ts +11 -0
- package/app/lib/agent/tools.ts +146 -0
- package/app/lib/core/file-convert.ts +97 -0
- package/app/lib/core/organize.ts +105 -0
- package/app/lib/i18n-en.ts +51 -0
- package/app/lib/i18n-zh.ts +51 -0
- package/package.json +1 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
export const runtime = 'nodejs';
|
|
3
|
+
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
7
|
+
import { revalidatePath } from 'next/cache';
|
|
8
|
+
import { sanitizeFileName, convertToMarkdown } from '@/lib/core/file-convert';
|
|
9
|
+
import { resolveSafe } from '@/lib/core/security';
|
|
10
|
+
import { scaffoldIfNewSpace } from '@/lib/core/space-scaffold';
|
|
11
|
+
import { organizeAfterImport } from '@/lib/core/organize';
|
|
12
|
+
import { invalidateSearchIndex } from '@/lib/core/search';
|
|
13
|
+
import { effectiveSopRoot } from '@/lib/settings';
|
|
14
|
+
import { invalidateCache } from '@/lib/fs';
|
|
15
|
+
|
|
16
|
+
const MAX_FILES = 20;
|
|
17
|
+
const MAX_CONTENT_LENGTH = 5 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
type ConflictMode = 'skip' | 'rename' | 'overwrite';
|
|
20
|
+
|
|
21
|
+
interface ImportRequest {
|
|
22
|
+
files: Array<{
|
|
23
|
+
name: string;
|
|
24
|
+
content: string;
|
|
25
|
+
encoding?: 'text' | 'base64';
|
|
26
|
+
}>;
|
|
27
|
+
targetSpace?: string;
|
|
28
|
+
organize?: boolean;
|
|
29
|
+
conflict?: ConflictMode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeTargetSpace(raw: unknown): string {
|
|
33
|
+
if (raw === undefined || raw === null) return '';
|
|
34
|
+
if (typeof raw !== 'string') return '';
|
|
35
|
+
return raw.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '').trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decodeFileContent(
|
|
39
|
+
encoding: 'text' | 'base64' | undefined,
|
|
40
|
+
content: string,
|
|
41
|
+
sanitizedName: string,
|
|
42
|
+
): string {
|
|
43
|
+
if (encoding === 'base64') {
|
|
44
|
+
const buf = Buffer.from(content, 'base64');
|
|
45
|
+
if (sanitizedName.toLowerCase().endsWith('.pdf')) {
|
|
46
|
+
return buf.toString('latin1');
|
|
47
|
+
}
|
|
48
|
+
return buf.toString('utf-8');
|
|
49
|
+
}
|
|
50
|
+
return content;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveUniquePath(
|
|
54
|
+
mindRoot: string,
|
|
55
|
+
relPath: string,
|
|
56
|
+
conflict: ConflictMode,
|
|
57
|
+
): { relPath: string; resolved: string; skipped?: string } {
|
|
58
|
+
let rel = relPath.replace(/\\/g, '/');
|
|
59
|
+
let resolved = resolveSafe(mindRoot, rel);
|
|
60
|
+
if (!fs.existsSync(resolved)) {
|
|
61
|
+
return { relPath: rel, resolved };
|
|
62
|
+
}
|
|
63
|
+
if (conflict === 'skip') {
|
|
64
|
+
return { relPath: rel, resolved, skipped: 'file exists' };
|
|
65
|
+
}
|
|
66
|
+
if (conflict === 'overwrite') {
|
|
67
|
+
return { relPath: rel, resolved };
|
|
68
|
+
}
|
|
69
|
+
let n = 0;
|
|
70
|
+
while (fs.existsSync(resolved)) {
|
|
71
|
+
n += 1;
|
|
72
|
+
const dir = path.posix.dirname(rel);
|
|
73
|
+
const base = path.posix.basename(rel);
|
|
74
|
+
const ext = path.posix.extname(base);
|
|
75
|
+
const stem = ext ? base.slice(0, -ext.length) : base;
|
|
76
|
+
const newBase = `${stem}-${n}${ext}`;
|
|
77
|
+
rel = dir && dir !== '.' ? path.posix.join(dir, newBase) : newBase;
|
|
78
|
+
resolved = resolveSafe(mindRoot, rel);
|
|
79
|
+
}
|
|
80
|
+
return { relPath: rel, resolved };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function POST(req: NextRequest) {
|
|
84
|
+
let body: unknown;
|
|
85
|
+
try {
|
|
86
|
+
body = await req.json();
|
|
87
|
+
} catch {
|
|
88
|
+
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const mindRoot = effectiveSopRoot().trim();
|
|
92
|
+
if (!mindRoot) {
|
|
93
|
+
return NextResponse.json({ error: 'MIND_ROOT is not configured' }, { status: 400 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!body || typeof body !== 'object') {
|
|
97
|
+
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const reqBody = body as ImportRequest;
|
|
101
|
+
if (!Array.isArray(reqBody.files)) {
|
|
102
|
+
return NextResponse.json({ error: 'files must be an array' }, { status: 400 });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (reqBody.files.length > MAX_FILES) {
|
|
106
|
+
return NextResponse.json({ error: `At most ${MAX_FILES} files per request` }, { status: 400 });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const targetSpaceNorm = normalizeTargetSpace(reqBody.targetSpace);
|
|
110
|
+
const organize = reqBody.organize !== false;
|
|
111
|
+
const conflict: ConflictMode =
|
|
112
|
+
reqBody.conflict === 'skip' || reqBody.conflict === 'overwrite' || reqBody.conflict === 'rename'
|
|
113
|
+
? reqBody.conflict
|
|
114
|
+
: 'rename';
|
|
115
|
+
|
|
116
|
+
const created: Array<{ original: string; path: string }> = [];
|
|
117
|
+
const skipped: Array<{ name: string; reason: string }> = [];
|
|
118
|
+
const errors: Array<{ name: string; error: string }> = [];
|
|
119
|
+
const createdPaths: string[] = [];
|
|
120
|
+
const updatedFiles: string[] = [];
|
|
121
|
+
|
|
122
|
+
for (const entry of reqBody.files) {
|
|
123
|
+
const originalName = typeof entry?.name === 'string' ? entry.name : '';
|
|
124
|
+
try {
|
|
125
|
+
if (typeof entry?.name !== 'string' || typeof entry?.content !== 'string') {
|
|
126
|
+
errors.push({ name: originalName || '(unknown)', error: 'name and content must be strings' });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!entry.name.trim()) {
|
|
130
|
+
errors.push({ name: '(empty)', error: 'name must not be empty' });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (entry.content.length > MAX_CONTENT_LENGTH) {
|
|
134
|
+
errors.push({ name: entry.name, error: `content exceeds ${MAX_CONTENT_LENGTH} characters` });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const sanitized = sanitizeFileName(entry.name);
|
|
139
|
+
const encoding = entry.encoding === 'base64' ? 'base64' : 'text';
|
|
140
|
+
const raw = decodeFileContent(encoding, entry.content, sanitized);
|
|
141
|
+
const convertResult = convertToMarkdown(sanitized, raw);
|
|
142
|
+
|
|
143
|
+
let relPath = targetSpaceNorm
|
|
144
|
+
? path.posix.join(targetSpaceNorm, convertResult.targetName)
|
|
145
|
+
: convertResult.targetName;
|
|
146
|
+
|
|
147
|
+
const { relPath: finalRel, resolved, skipped: skipReason } = resolveUniquePath(
|
|
148
|
+
mindRoot,
|
|
149
|
+
relPath,
|
|
150
|
+
conflict,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (skipReason) {
|
|
154
|
+
skipped.push({ name: entry.name, reason: skipReason });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
159
|
+
fs.writeFileSync(resolved, convertResult.content, 'utf-8');
|
|
160
|
+
scaffoldIfNewSpace(mindRoot, finalRel);
|
|
161
|
+
|
|
162
|
+
created.push({ original: entry.name, path: finalRel });
|
|
163
|
+
createdPaths.push(finalRel);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
errors.push({ name: originalName || '(unknown)', error: (e as Error).message });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (organize && createdPaths.length > 0) {
|
|
170
|
+
try {
|
|
171
|
+
const { readmeUpdated } = organizeAfterImport(mindRoot, createdPaths, targetSpaceNorm);
|
|
172
|
+
if (readmeUpdated && targetSpaceNorm) {
|
|
173
|
+
updatedFiles.push(path.posix.join(targetSpaceNorm, 'README.md'));
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
/* organize is best-effort */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (created.length > 0 || updatedFiles.length > 0) {
|
|
181
|
+
invalidateSearchIndex();
|
|
182
|
+
invalidateCache();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
revalidatePath('/');
|
|
187
|
+
} catch {
|
|
188
|
+
/* noop in test env */
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return NextResponse.json({
|
|
192
|
+
created,
|
|
193
|
+
skipped,
|
|
194
|
+
errors,
|
|
195
|
+
updatedFiles,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { useRef, useCallback, useState, useEffect } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import { FolderTree, Search, Settings, RefreshCw,
|
|
5
|
+
import { FolderTree, Search, Settings, RefreshCw, Bot, Compass, HelpCircle, ChevronLeft, ChevronRight, Radio } from 'lucide-react';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { DOT_COLORS, getStatusLevel } from './SyncStatusBar';
|
|
8
8
|
import type { SyncStatus } from './settings/SyncTab';
|
|
9
9
|
import Logo from './Logo';
|
|
10
10
|
|
|
11
|
-
export type PanelId = 'files' | 'search' | 'echo' | '
|
|
11
|
+
export type PanelId = 'files' | 'search' | 'echo' | 'agents' | 'discover';
|
|
12
12
|
|
|
13
13
|
export const RAIL_WIDTH_COLLAPSED = 48;
|
|
14
14
|
export const RAIL_WIDTH_EXPANDED = 180;
|
|
@@ -181,9 +181,8 @@ export default function ActivityBar({
|
|
|
181
181
|
{/* ── Middle: Core panel toggles ── */}
|
|
182
182
|
<div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
|
|
183
183
|
<RailButton icon={<FolderTree size={18} />} label={t.sidebar.files} active={activePanel === 'files'} expanded={expanded} onClick={() => toggle('files')} walkthroughId="files-panel" />
|
|
184
|
-
<RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => toggle('echo')} />
|
|
185
184
|
<RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} walkthroughId="search-button" />
|
|
186
|
-
<RailButton icon={<
|
|
185
|
+
<RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => toggle('echo')} />
|
|
187
186
|
<RailButton
|
|
188
187
|
icon={<Bot size={18} />}
|
|
189
188
|
label={t.sidebar.agents}
|
|
@@ -6,7 +6,7 @@ import { FileNode } from '@/lib/types';
|
|
|
6
6
|
import { encodePath } from '@/lib/utils';
|
|
7
7
|
import {
|
|
8
8
|
ChevronDown, FileText, Table, Folder, FolderOpen, Plus, Loader2,
|
|
9
|
-
Trash2, Pencil, Layers, ScrollText,
|
|
9
|
+
Trash2, Pencil, Layers, ScrollText, FolderInput,
|
|
10
10
|
} from 'lucide-react';
|
|
11
11
|
import { createFileAction, deleteFileAction, renameFileAction, renameSpaceAction, deleteSpaceAction, convertToSpaceAction, deleteFolderAction } from '@/lib/actions';
|
|
12
12
|
import { useLocale } from '@/lib/LocaleContext';
|
|
@@ -23,6 +23,7 @@ interface FileTreeProps {
|
|
|
23
23
|
onNavigate?: () => void;
|
|
24
24
|
maxOpenDepth?: number | null;
|
|
25
25
|
parentIsSpace?: boolean;
|
|
26
|
+
onImport?: (space: string) => void;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function getIcon(node: FileNode) {
|
|
@@ -96,8 +97,8 @@ const MENU_DIVIDER = "my-1 border-t border-border/50";
|
|
|
96
97
|
|
|
97
98
|
// ─── SpaceContextMenu ─────────────────────────────────────────────────────────
|
|
98
99
|
|
|
99
|
-
function SpaceContextMenu({ x, y, node, onClose, onRename }: {
|
|
100
|
-
x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void;
|
|
100
|
+
function SpaceContextMenu({ x, y, node, onClose, onRename, onImport }: {
|
|
101
|
+
x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void; onImport?: (space: string) => void;
|
|
101
102
|
}) {
|
|
102
103
|
const router = useRouter();
|
|
103
104
|
const { t } = useLocale();
|
|
@@ -108,6 +109,11 @@ function SpaceContextMenu({ x, y, node, onClose, onRename }: {
|
|
|
108
109
|
<button className={MENU_ITEM} onClick={() => { router.push(`/view/${encodePath(`${node.path}/INSTRUCTION.md`)}`); onClose(); }}>
|
|
109
110
|
<ScrollText size={14} className="shrink-0" /> {t.fileTree.editRules}
|
|
110
111
|
</button>
|
|
112
|
+
{onImport && (
|
|
113
|
+
<button className={MENU_ITEM} onClick={() => { onImport(node.path); onClose(); }}>
|
|
114
|
+
<FolderInput size={14} className="shrink-0" /> {t.fileTree.importFile}
|
|
115
|
+
</button>
|
|
116
|
+
)}
|
|
111
117
|
<button className={MENU_ITEM} onClick={() => { onRename(); onClose(); }}>
|
|
112
118
|
<Pencil size={14} className="shrink-0" /> {t.fileTree.renameSpace}
|
|
113
119
|
</button>
|
|
@@ -240,9 +246,9 @@ function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: num
|
|
|
240
246
|
|
|
241
247
|
// ─── DirectoryNode ────────────────────────────────────────────────────────────
|
|
242
248
|
|
|
243
|
-
function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
249
|
+
function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onImport }: {
|
|
244
250
|
node: FileNode; depth: number; currentPath: string; onNavigate?: () => void;
|
|
245
|
-
maxOpenDepth?: number | null;
|
|
251
|
+
maxOpenDepth?: number | null; onImport?: (space: string) => void;
|
|
246
252
|
}) {
|
|
247
253
|
const router = useRouter();
|
|
248
254
|
const isActive = currentPath.startsWith(node.path + '/') || currentPath === node.path;
|
|
@@ -255,6 +261,8 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
255
261
|
const renameRef = useRef<HTMLInputElement>(null);
|
|
256
262
|
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
257
263
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
|
264
|
+
const [plusPopover, setPlusPopover] = useState(false);
|
|
265
|
+
const plusRef = useRef<HTMLButtonElement>(null);
|
|
258
266
|
const { t } = useLocale();
|
|
259
267
|
|
|
260
268
|
const toggle = useCallback(() => setOpen(v => !v), []);
|
|
@@ -391,12 +399,12 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
391
399
|
</button>
|
|
392
400
|
<div className="absolute right-1 top-1/2 -translate-y-1/2 hidden group-hover/dir:flex items-center gap-0.5 z-10">
|
|
393
401
|
<button
|
|
402
|
+
ref={plusRef}
|
|
394
403
|
type="button"
|
|
395
404
|
onClick={(e) => {
|
|
396
405
|
e.preventDefault();
|
|
397
406
|
e.stopPropagation();
|
|
398
|
-
|
|
399
|
-
setShowNewFile(true);
|
|
407
|
+
setPlusPopover(v => !v);
|
|
400
408
|
}}
|
|
401
409
|
className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
402
410
|
title={t.fileTree.newFileTitle}
|
|
@@ -444,6 +452,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
444
452
|
onNavigate={onNavigate}
|
|
445
453
|
maxOpenDepth={maxOpenDepth}
|
|
446
454
|
parentIsSpace={isSpace}
|
|
455
|
+
onImport={onImport}
|
|
447
456
|
/>
|
|
448
457
|
)}
|
|
449
458
|
{showNewFile && (
|
|
@@ -462,6 +471,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
462
471
|
node={node}
|
|
463
472
|
onClose={() => setContextMenu(null)}
|
|
464
473
|
onRename={() => startRename()}
|
|
474
|
+
onImport={onImport}
|
|
465
475
|
/>
|
|
466
476
|
) : (
|
|
467
477
|
<FolderContextMenu
|
|
@@ -472,6 +482,22 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
472
482
|
onRename={() => startRename()}
|
|
473
483
|
/>
|
|
474
484
|
))}
|
|
485
|
+
|
|
486
|
+
{plusPopover && plusRef.current && (() => {
|
|
487
|
+
const rect = plusRef.current!.getBoundingClientRect();
|
|
488
|
+
return (
|
|
489
|
+
<ContextMenuShell x={rect.left} y={rect.bottom + 4} onClose={() => setPlusPopover(false)} menuHeight={80}>
|
|
490
|
+
<button className={MENU_ITEM} onClick={() => { setPlusPopover(false); setOpen(true); setShowNewFile(true); }}>
|
|
491
|
+
<FileText size={14} className="shrink-0" /> {t.fileTree.newFile}
|
|
492
|
+
</button>
|
|
493
|
+
{onImport && (
|
|
494
|
+
<button className={MENU_ITEM} onClick={() => { setPlusPopover(false); onImport(node.path); }}>
|
|
495
|
+
<FolderInput size={14} className="shrink-0" /> {t.fileTree.importFile}
|
|
496
|
+
</button>
|
|
497
|
+
)}
|
|
498
|
+
</ContextMenuShell>
|
|
499
|
+
);
|
|
500
|
+
})()}
|
|
475
501
|
</div>
|
|
476
502
|
);
|
|
477
503
|
}
|
|
@@ -589,7 +615,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
589
615
|
|
|
590
616
|
// ─── FileTree (root) ──────────────────────────────────────────────────────────
|
|
591
617
|
|
|
592
|
-
export default function FileTree({ nodes, depth = 0, onNavigate, maxOpenDepth, parentIsSpace }: FileTreeProps) {
|
|
618
|
+
export default function FileTree({ nodes, depth = 0, onNavigate, maxOpenDepth, parentIsSpace, onImport }: FileTreeProps) {
|
|
593
619
|
const pathname = usePathname();
|
|
594
620
|
const currentPath = getCurrentFilePath(pathname);
|
|
595
621
|
|
|
@@ -609,7 +635,7 @@ export default function FileTree({ nodes, depth = 0, onNavigate, maxOpenDepth, p
|
|
|
609
635
|
<div className="flex flex-col gap-0.5">
|
|
610
636
|
{visibleNodes.map((node) =>
|
|
611
637
|
node.type === 'directory' ? (
|
|
612
|
-
<DirectoryNode key={node.path} node={node} depth={depth} currentPath={currentPath} onNavigate={onNavigate} maxOpenDepth={maxOpenDepth} />
|
|
638
|
+
<DirectoryNode key={node.path} node={node} depth={depth} currentPath={currentPath} onNavigate={onNavigate} maxOpenDepth={maxOpenDepth} onImport={onImport} />
|
|
613
639
|
) : (
|
|
614
640
|
<FileNodeItem key={node.path} node={node} depth={depth} currentPath={currentPath} onNavigate={onNavigate} />
|
|
615
641
|
)
|