@geminilight/mindos 0.5.69 → 0.6.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/ask/route.ts +122 -92
- package/app/app/api/file/import/route.ts +197 -0
- package/app/app/api/mcp/agents/route.ts +53 -2
- package/app/app/api/mcp/status/route.ts +1 -1
- package/app/app/api/skills/route.ts +10 -114
- package/app/components/ActivityBar.tsx +5 -7
- package/app/components/CreateSpaceModal.tsx +31 -6
- package/app/components/FileTree.tsx +68 -11
- package/app/components/GuideCard.tsx +197 -131
- package/app/components/HomeContent.tsx +85 -18
- 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 +96 -8
- package/app/components/SpaceInitToast.tsx +173 -0
- package/app/components/TableOfContents.tsx +1 -0
- package/app/components/agents/AgentDetailContent.tsx +69 -45
- package/app/components/agents/AgentsContentPage.tsx +2 -1
- package/app/components/agents/AgentsMcpSection.tsx +16 -12
- package/app/components/agents/AgentsOverviewSection.tsx +37 -36
- 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/agents/agents-content-model.ts +16 -8
- package/app/components/ask/AskContent.tsx +148 -50
- package/app/components/ask/MentionPopover.tsx +16 -8
- package/app/components/ask/SlashCommandPopover.tsx +62 -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/components/settings/KnowledgeTab.tsx +61 -0
- package/app/components/walkthrough/steps.ts +11 -6
- package/app/hooks/useFileImport.ts +191 -0
- package/app/hooks/useFileUpload.ts +11 -0
- package/app/hooks/useMention.ts +14 -6
- package/app/hooks/useSlashCommand.ts +114 -0
- package/app/lib/actions.ts +79 -2
- package/app/lib/agent/index.ts +1 -1
- package/app/lib/agent/prompt.ts +2 -0
- package/app/lib/agent/tools.ts +252 -0
- package/app/lib/core/create-space.ts +11 -4
- package/app/lib/core/file-convert.ts +97 -0
- package/app/lib/core/index.ts +1 -1
- package/app/lib/core/organize.ts +105 -0
- package/app/lib/i18n-en.ts +102 -46
- package/app/lib/i18n-zh.ts +101 -45
- package/app/lib/mcp-agents.ts +8 -0
- package/app/lib/pdf-extract.ts +33 -0
- package/app/lib/pi-integration/extensions.ts +45 -0
- package/app/lib/pi-integration/mcporter.ts +219 -0
- package/app/lib/pi-integration/session-store.ts +62 -0
- package/app/lib/pi-integration/skills.ts +116 -0
- package/app/lib/settings.ts +1 -1
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +1 -1
- package/app/package.json +2 -0
- package/mcp/src/index.ts +29 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import { readSettings, writeSettings } from '@/lib/settings';
|
|
7
|
+
import { parseSkillMd, readSkillContentByName, scanSkillDirs } from '@/lib/pi-integration/skills';
|
|
7
8
|
|
|
8
9
|
const PROJECT_ROOT = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
|
|
9
10
|
|
|
@@ -12,113 +13,15 @@ function getMindRoot(): string {
|
|
|
12
13
|
return s.mindRoot || process.env.MIND_ROOT || path.join(os.homedir(), 'MindOS', 'mind');
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
interface SkillInfo {
|
|
16
|
-
name: string;
|
|
17
|
-
description: string;
|
|
18
|
-
path: string;
|
|
19
|
-
source: 'builtin' | 'user';
|
|
20
|
-
enabled: boolean;
|
|
21
|
-
editable: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function parseSkillMd(content: string): { name: string; description: string } {
|
|
25
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
26
|
-
if (!match) return { name: '', description: '' };
|
|
27
|
-
const yaml = match[1];
|
|
28
|
-
const nameMatch = yaml.match(/^name:\s*(.+)/m);
|
|
29
|
-
const descMatch = yaml.match(/^description:\s*>?\s*\n?([\s\S]*?)(?=\n\w|\n---)/m);
|
|
30
|
-
const name = nameMatch ? nameMatch[1].trim() : '';
|
|
31
|
-
let description = '';
|
|
32
|
-
if (descMatch) {
|
|
33
|
-
description = descMatch[1].trim().split('\n').map(l => l.trim()).join(' ').slice(0, 200);
|
|
34
|
-
} else {
|
|
35
|
-
const simpleDesc = yaml.match(/^description:\s*(.+)/m);
|
|
36
|
-
if (simpleDesc) description = simpleDesc[1].trim().slice(0, 200);
|
|
37
|
-
}
|
|
38
|
-
return { name, description };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function scanSkillDirs(disabledSkills: string[]): SkillInfo[] {
|
|
42
|
-
const skills: SkillInfo[] = [];
|
|
43
|
-
const seen = new Set<string>();
|
|
44
|
-
|
|
45
|
-
// 1. app/data/skills/ — builtin
|
|
46
|
-
const builtinDir = path.join(PROJECT_ROOT, 'app', 'data', 'skills');
|
|
47
|
-
if (fs.existsSync(builtinDir)) {
|
|
48
|
-
for (const entry of fs.readdirSync(builtinDir, { withFileTypes: true })) {
|
|
49
|
-
if (!entry.isDirectory()) continue;
|
|
50
|
-
const skillFile = path.join(builtinDir, entry.name, 'SKILL.md');
|
|
51
|
-
if (!fs.existsSync(skillFile)) continue;
|
|
52
|
-
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
53
|
-
const { name, description } = parseSkillMd(content);
|
|
54
|
-
const skillName = name || entry.name;
|
|
55
|
-
seen.add(skillName);
|
|
56
|
-
skills.push({
|
|
57
|
-
name: skillName,
|
|
58
|
-
description,
|
|
59
|
-
path: `app/data/skills/${entry.name}/SKILL.md`,
|
|
60
|
-
source: 'builtin',
|
|
61
|
-
enabled: !disabledSkills.includes(skillName),
|
|
62
|
-
editable: false,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 2. skills/ — project root builtin
|
|
68
|
-
const skillsDir = path.join(PROJECT_ROOT, 'skills');
|
|
69
|
-
if (fs.existsSync(skillsDir)) {
|
|
70
|
-
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
71
|
-
if (!entry.isDirectory()) continue;
|
|
72
|
-
const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
73
|
-
if (!fs.existsSync(skillFile)) continue;
|
|
74
|
-
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
75
|
-
const { name, description } = parseSkillMd(content);
|
|
76
|
-
const skillName = name || entry.name;
|
|
77
|
-
if (seen.has(skillName)) continue; // already listed from app/data/skills/
|
|
78
|
-
seen.add(skillName);
|
|
79
|
-
skills.push({
|
|
80
|
-
name: skillName,
|
|
81
|
-
description,
|
|
82
|
-
path: `skills/${entry.name}/SKILL.md`,
|
|
83
|
-
source: 'builtin',
|
|
84
|
-
enabled: !disabledSkills.includes(skillName),
|
|
85
|
-
editable: false,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 3. {mindRoot}/.skills/ — user custom
|
|
91
|
-
const mindRoot = getMindRoot();
|
|
92
|
-
const userSkillsDir = path.join(mindRoot, '.skills');
|
|
93
|
-
if (fs.existsSync(userSkillsDir)) {
|
|
94
|
-
for (const entry of fs.readdirSync(userSkillsDir, { withFileTypes: true })) {
|
|
95
|
-
if (!entry.isDirectory()) continue;
|
|
96
|
-
const skillFile = path.join(userSkillsDir, entry.name, 'SKILL.md');
|
|
97
|
-
if (!fs.existsSync(skillFile)) continue;
|
|
98
|
-
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
99
|
-
const { name, description } = parseSkillMd(content);
|
|
100
|
-
const skillName = name || entry.name;
|
|
101
|
-
if (seen.has(skillName)) continue;
|
|
102
|
-
seen.add(skillName);
|
|
103
|
-
skills.push({
|
|
104
|
-
name: skillName,
|
|
105
|
-
description,
|
|
106
|
-
path: `{mindRoot}/.skills/${entry.name}/SKILL.md`,
|
|
107
|
-
source: 'user',
|
|
108
|
-
enabled: !disabledSkills.includes(skillName),
|
|
109
|
-
editable: true,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return skills;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
16
|
export async function GET() {
|
|
118
17
|
try {
|
|
119
18
|
const settings = readSettings();
|
|
120
19
|
const disabledSkills = settings.disabledSkills ?? [];
|
|
121
|
-
const skills = scanSkillDirs(
|
|
20
|
+
const skills = scanSkillDirs({
|
|
21
|
+
projectRoot: PROJECT_ROOT,
|
|
22
|
+
mindRoot: getMindRoot(),
|
|
23
|
+
disabledSkills,
|
|
24
|
+
});
|
|
122
25
|
return NextResponse.json({ skills });
|
|
123
26
|
} catch (err) {
|
|
124
27
|
return NextResponse.json({ error: String(err) }, { status: 500 });
|
|
@@ -205,18 +108,11 @@ export async function POST(req: NextRequest) {
|
|
|
205
108
|
|
|
206
109
|
case 'read': {
|
|
207
110
|
if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 });
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
path.join(userSkillsDir, name),
|
|
212
|
-
];
|
|
213
|
-
for (const dir of dirs) {
|
|
214
|
-
const file = path.join(dir, 'SKILL.md');
|
|
215
|
-
if (fs.existsSync(file)) {
|
|
216
|
-
return NextResponse.json({ content: fs.readFileSync(file, 'utf-8') });
|
|
217
|
-
}
|
|
111
|
+
const content = readSkillContentByName(name, { projectRoot: PROJECT_ROOT, mindRoot });
|
|
112
|
+
if (!content) {
|
|
113
|
+
return NextResponse.json({ error: 'Skill not found' }, { status: 404 });
|
|
218
114
|
}
|
|
219
|
-
return NextResponse.json({
|
|
115
|
+
return NextResponse.json({ content });
|
|
220
116
|
}
|
|
221
117
|
|
|
222
118
|
case 'read-native': {
|
|
@@ -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;
|
|
@@ -162,7 +162,6 @@ export default function ActivityBar({
|
|
|
162
162
|
role="toolbar"
|
|
163
163
|
aria-label="Navigation"
|
|
164
164
|
aria-orientation="vertical"
|
|
165
|
-
data-walkthrough="activity-bar"
|
|
166
165
|
>
|
|
167
166
|
{/* Content wrapper — overflow-hidden prevents text flash during width transitions */}
|
|
168
167
|
<div className="flex flex-col h-full w-full overflow-hidden">
|
|
@@ -181,15 +180,15 @@ export default function ActivityBar({
|
|
|
181
180
|
{/* ── Middle: Core panel toggles ── */}
|
|
182
181
|
<div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
|
|
183
182
|
<RailButton icon={<FolderTree size={18} />} label={t.sidebar.files} active={activePanel === 'files'} expanded={expanded} onClick={() => toggle('files')} walkthroughId="files-panel" />
|
|
184
|
-
<RailButton icon={<
|
|
185
|
-
<RailButton icon={<
|
|
186
|
-
<RailButton icon={<Blocks size={18} />} label={t.sidebar.plugins} active={activePanel === 'plugins'} expanded={expanded} onClick={() => toggle('plugins')} />
|
|
183
|
+
<RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} />
|
|
184
|
+
<RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => toggle('echo')} walkthroughId="echo-panel" />
|
|
187
185
|
<RailButton
|
|
188
186
|
icon={<Bot size={18} />}
|
|
189
187
|
label={t.sidebar.agents}
|
|
190
188
|
active={activePanel === 'agents'}
|
|
191
189
|
expanded={expanded}
|
|
192
190
|
onClick={() => onAgentsClick ? debounced(onAgentsClick) : toggle('agents')}
|
|
191
|
+
walkthroughId="agents-panel"
|
|
193
192
|
/>
|
|
194
193
|
<RailButton icon={<Compass size={18} />} label={t.sidebar.discover} active={activePanel === 'discover'} expanded={expanded} onClick={() => toggle('discover')} />
|
|
195
194
|
</div>
|
|
@@ -212,7 +211,6 @@ export default function ActivityBar({
|
|
|
212
211
|
shortcut="⌘,"
|
|
213
212
|
expanded={expanded}
|
|
214
213
|
onClick={() => debounced(onSettingsClick)}
|
|
215
|
-
walkthroughId="settings-button"
|
|
216
214
|
badge={hasUpdate ? (
|
|
217
215
|
<span className={`absolute ${expanded ? 'left-[26px] top-1.5' : 'top-1.5 right-1.5'} w-2 h-2 rounded-full bg-error`} />
|
|
218
216
|
) : undefined}
|
|
@@ -93,17 +93,42 @@ export default function CreateSpaceModal({ t, dirPaths }: { t: ReturnType<typeof
|
|
|
93
93
|
if (useAi && aiAvailable) {
|
|
94
94
|
const isZh = document.documentElement.lang === 'zh';
|
|
95
95
|
const prompt = isZh
|
|
96
|
-
? `初始化新建的心智空间「${trimmed}」,路径为「${createdPath}/」。${description ? `描述:「${description}」。` : ''}
|
|
97
|
-
: `Initialize
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
? `初始化新建的心智空间「${trimmed}」,路径为「${createdPath}/」。${description ? `描述:「${description}」。` : ''}两个文件均已存在模板,用 write_file 覆盖:\n1. 「${createdPath}/README.md」— 写入空间用途、结构概览、使用指南\n2. 「${createdPath}/INSTRUCTION.md」— 写入 AI Agent 在此空间中的行为规则和操作约定\n\n内容简洁实用,直接使用工具写入。`
|
|
97
|
+
: `Initialize the new Mind Space "${trimmed}" at "${createdPath}/". ${description ? `Description: "${description}". ` : ''}Both files already exist with templates — use write_file to overwrite:\n1. "${createdPath}/README.md" — write purpose, structure overview, usage guidelines\n2. "${createdPath}/INSTRUCTION.md" — write rules for AI agents operating in this space\n\nKeep content concise and actionable. Write files directly using tools.`;
|
|
98
|
+
|
|
99
|
+
window.dispatchEvent(new CustomEvent('mindos:ai-init', {
|
|
100
|
+
detail: { spaceName: trimmed, spacePath: createdPath, description, state: 'working' },
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
// /api/ask returns SSE — use raw fetch and consume the stream
|
|
104
|
+
// so the server-side agent runs to completion.
|
|
105
|
+
fetch('/api/ask', {
|
|
100
106
|
method: 'POST',
|
|
101
107
|
headers: { 'Content-Type': 'application/json' },
|
|
102
108
|
body: JSON.stringify({
|
|
103
109
|
messages: [{ role: 'user', content: prompt }],
|
|
104
|
-
|
|
110
|
+
currentFile: createdPath + '/INSTRUCTION.md',
|
|
105
111
|
}),
|
|
106
|
-
}).
|
|
112
|
+
}).then(async (res) => {
|
|
113
|
+
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
|
|
114
|
+
const reader = res.body.getReader();
|
|
115
|
+
try {
|
|
116
|
+
while (true) {
|
|
117
|
+
const { done } = await reader.read();
|
|
118
|
+
if (done) break;
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
reader.releaseLock();
|
|
122
|
+
}
|
|
123
|
+
window.dispatchEvent(new CustomEvent('mindos:ai-init', {
|
|
124
|
+
detail: { spacePath: createdPath, state: 'done' },
|
|
125
|
+
}));
|
|
126
|
+
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
127
|
+
}).catch(() => {
|
|
128
|
+
window.dispatchEvent(new CustomEvent('mindos:ai-init', {
|
|
129
|
+
detail: { spacePath: createdPath, state: 'error' },
|
|
130
|
+
}));
|
|
131
|
+
});
|
|
107
132
|
}
|
|
108
133
|
|
|
109
134
|
close();
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useCallback, useRef, useTransition, useEffect } from 'react';
|
|
3
|
+
import { useState, useCallback, useRef, useTransition, useEffect, useSyncExternalStore } 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
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';
|
|
@@ -17,12 +17,40 @@ function notifyFilesChanged() {
|
|
|
17
17
|
|
|
18
18
|
const SYSTEM_FILES = new Set(['INSTRUCTION.md', 'README.md']);
|
|
19
19
|
|
|
20
|
+
const HIDDEN_FILES_KEY = 'show-hidden-files';
|
|
21
|
+
|
|
22
|
+
function subscribeHiddenFiles(cb: () => void) {
|
|
23
|
+
const handler = (e: StorageEvent) => { if (e.key === HIDDEN_FILES_KEY) cb(); };
|
|
24
|
+
const custom = () => cb();
|
|
25
|
+
window.addEventListener('storage', handler);
|
|
26
|
+
window.addEventListener('mindos:hidden-files-changed', custom);
|
|
27
|
+
return () => {
|
|
28
|
+
window.removeEventListener('storage', handler);
|
|
29
|
+
window.removeEventListener('mindos:hidden-files-changed', custom);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getShowHiddenFiles() {
|
|
34
|
+
if (typeof window === 'undefined') return false;
|
|
35
|
+
return localStorage.getItem(HIDDEN_FILES_KEY) === 'true';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function setShowHiddenFiles(value: boolean) {
|
|
39
|
+
localStorage.setItem(HIDDEN_FILES_KEY, String(value));
|
|
40
|
+
window.dispatchEvent(new Event('mindos:hidden-files-changed'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function useShowHiddenFiles() {
|
|
44
|
+
return useSyncExternalStore(subscribeHiddenFiles, getShowHiddenFiles, () => false);
|
|
45
|
+
}
|
|
46
|
+
|
|
20
47
|
interface FileTreeProps {
|
|
21
48
|
nodes: FileNode[];
|
|
22
49
|
depth?: number;
|
|
23
50
|
onNavigate?: () => void;
|
|
24
51
|
maxOpenDepth?: number | null;
|
|
25
52
|
parentIsSpace?: boolean;
|
|
53
|
+
onImport?: (space: string) => void;
|
|
26
54
|
}
|
|
27
55
|
|
|
28
56
|
function getIcon(node: FileNode) {
|
|
@@ -96,8 +124,8 @@ const MENU_DIVIDER = "my-1 border-t border-border/50";
|
|
|
96
124
|
|
|
97
125
|
// ─── SpaceContextMenu ─────────────────────────────────────────────────────────
|
|
98
126
|
|
|
99
|
-
function SpaceContextMenu({ x, y, node, onClose, onRename }: {
|
|
100
|
-
x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void;
|
|
127
|
+
function SpaceContextMenu({ x, y, node, onClose, onRename, onImport }: {
|
|
128
|
+
x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void; onImport?: (space: string) => void;
|
|
101
129
|
}) {
|
|
102
130
|
const router = useRouter();
|
|
103
131
|
const { t } = useLocale();
|
|
@@ -108,6 +136,11 @@ function SpaceContextMenu({ x, y, node, onClose, onRename }: {
|
|
|
108
136
|
<button className={MENU_ITEM} onClick={() => { router.push(`/view/${encodePath(`${node.path}/INSTRUCTION.md`)}`); onClose(); }}>
|
|
109
137
|
<ScrollText size={14} className="shrink-0" /> {t.fileTree.editRules}
|
|
110
138
|
</button>
|
|
139
|
+
{onImport && (
|
|
140
|
+
<button className={MENU_ITEM} onClick={() => { onImport(node.path); onClose(); }}>
|
|
141
|
+
<FolderInput size={14} className="shrink-0" /> {t.fileTree.importFile}
|
|
142
|
+
</button>
|
|
143
|
+
)}
|
|
111
144
|
<button className={MENU_ITEM} onClick={() => { onRename(); onClose(); }}>
|
|
112
145
|
<Pencil size={14} className="shrink-0" /> {t.fileTree.renameSpace}
|
|
113
146
|
</button>
|
|
@@ -240,9 +273,9 @@ function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: num
|
|
|
240
273
|
|
|
241
274
|
// ─── DirectoryNode ────────────────────────────────────────────────────────────
|
|
242
275
|
|
|
243
|
-
function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
276
|
+
function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onImport }: {
|
|
244
277
|
node: FileNode; depth: number; currentPath: string; onNavigate?: () => void;
|
|
245
|
-
maxOpenDepth?: number | null;
|
|
278
|
+
maxOpenDepth?: number | null; onImport?: (space: string) => void;
|
|
246
279
|
}) {
|
|
247
280
|
const router = useRouter();
|
|
248
281
|
const isActive = currentPath.startsWith(node.path + '/') || currentPath === node.path;
|
|
@@ -255,6 +288,8 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
255
288
|
const renameRef = useRef<HTMLInputElement>(null);
|
|
256
289
|
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
257
290
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
|
291
|
+
const [plusPopover, setPlusPopover] = useState(false);
|
|
292
|
+
const plusRef = useRef<HTMLButtonElement>(null);
|
|
258
293
|
const { t } = useLocale();
|
|
259
294
|
|
|
260
295
|
const toggle = useCallback(() => setOpen(v => !v), []);
|
|
@@ -391,12 +426,12 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
391
426
|
</button>
|
|
392
427
|
<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
428
|
<button
|
|
429
|
+
ref={plusRef}
|
|
394
430
|
type="button"
|
|
395
431
|
onClick={(e) => {
|
|
396
432
|
e.preventDefault();
|
|
397
433
|
e.stopPropagation();
|
|
398
|
-
|
|
399
|
-
setShowNewFile(true);
|
|
434
|
+
setPlusPopover(v => !v);
|
|
400
435
|
}}
|
|
401
436
|
className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
402
437
|
title={t.fileTree.newFileTitle}
|
|
@@ -444,6 +479,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
444
479
|
onNavigate={onNavigate}
|
|
445
480
|
maxOpenDepth={maxOpenDepth}
|
|
446
481
|
parentIsSpace={isSpace}
|
|
482
|
+
onImport={onImport}
|
|
447
483
|
/>
|
|
448
484
|
)}
|
|
449
485
|
{showNewFile && (
|
|
@@ -462,6 +498,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
462
498
|
node={node}
|
|
463
499
|
onClose={() => setContextMenu(null)}
|
|
464
500
|
onRename={() => startRename()}
|
|
501
|
+
onImport={onImport}
|
|
465
502
|
/>
|
|
466
503
|
) : (
|
|
467
504
|
<FolderContextMenu
|
|
@@ -472,6 +509,22 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
|
|
|
472
509
|
onRename={() => startRename()}
|
|
473
510
|
/>
|
|
474
511
|
))}
|
|
512
|
+
|
|
513
|
+
{plusPopover && plusRef.current && (() => {
|
|
514
|
+
const rect = plusRef.current!.getBoundingClientRect();
|
|
515
|
+
return (
|
|
516
|
+
<ContextMenuShell x={rect.left} y={rect.bottom + 4} onClose={() => setPlusPopover(false)} menuHeight={80}>
|
|
517
|
+
<button className={MENU_ITEM} onClick={() => { setPlusPopover(false); setOpen(true); setShowNewFile(true); }}>
|
|
518
|
+
<FileText size={14} className="shrink-0" /> {t.fileTree.newFile}
|
|
519
|
+
</button>
|
|
520
|
+
{onImport && (
|
|
521
|
+
<button className={MENU_ITEM} onClick={() => { setPlusPopover(false); onImport(node.path); }}>
|
|
522
|
+
<FolderInput size={14} className="shrink-0" /> {t.fileTree.importFile}
|
|
523
|
+
</button>
|
|
524
|
+
)}
|
|
525
|
+
</ContextMenuShell>
|
|
526
|
+
);
|
|
527
|
+
})()}
|
|
475
528
|
</div>
|
|
476
529
|
);
|
|
477
530
|
}
|
|
@@ -589,12 +642,16 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
589
642
|
|
|
590
643
|
// ─── FileTree (root) ──────────────────────────────────────────────────────────
|
|
591
644
|
|
|
592
|
-
export default function FileTree({ nodes, depth = 0, onNavigate, maxOpenDepth, parentIsSpace }: FileTreeProps) {
|
|
645
|
+
export default function FileTree({ nodes, depth = 0, onNavigate, maxOpenDepth, parentIsSpace, onImport }: FileTreeProps) {
|
|
593
646
|
const pathname = usePathname();
|
|
594
647
|
const currentPath = getCurrentFilePath(pathname);
|
|
648
|
+
const showHidden = useShowHiddenFiles();
|
|
595
649
|
|
|
596
650
|
const isInsideDir = depth > 0;
|
|
597
|
-
|
|
651
|
+
let visibleNodes = isInsideDir ? filterVisibleNodes(nodes, !!parentIsSpace) : nodes;
|
|
652
|
+
if (!isInsideDir && !showHidden) {
|
|
653
|
+
visibleNodes = visibleNodes.filter(n => !n.name.startsWith('.'));
|
|
654
|
+
}
|
|
598
655
|
|
|
599
656
|
useEffect(() => {
|
|
600
657
|
if (!currentPath || depth !== 0) return;
|
|
@@ -609,7 +666,7 @@ export default function FileTree({ nodes, depth = 0, onNavigate, maxOpenDepth, p
|
|
|
609
666
|
<div className="flex flex-col gap-0.5">
|
|
610
667
|
{visibleNodes.map((node) =>
|
|
611
668
|
node.type === 'directory' ? (
|
|
612
|
-
<DirectoryNode key={node.path} node={node} depth={depth} currentPath={currentPath} onNavigate={onNavigate} maxOpenDepth={maxOpenDepth} />
|
|
669
|
+
<DirectoryNode key={node.path} node={node} depth={depth} currentPath={currentPath} onNavigate={onNavigate} maxOpenDepth={maxOpenDepth} onImport={onImport} />
|
|
613
670
|
) : (
|
|
614
671
|
<FileNodeItem key={node.path} node={node} depth={depth} currentPath={currentPath} onNavigate={onNavigate} />
|
|
615
672
|
)
|