@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
|
@@ -7,11 +7,27 @@ import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
|
|
|
7
7
|
import { apiFetch } from '@/lib/api';
|
|
8
8
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
9
9
|
import { formatBytes, formatUptime } from '@/lib/format';
|
|
10
|
+
import { setShowHiddenFiles } from '@/components/FileTree';
|
|
11
|
+
import { scanExampleFilesAction, cleanupExamplesAction } from '@/lib/actions';
|
|
10
12
|
|
|
11
13
|
export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
12
14
|
const env = data.envOverrides ?? {};
|
|
13
15
|
const k = t.settings.knowledge;
|
|
14
16
|
|
|
17
|
+
// Hidden files toggle
|
|
18
|
+
const [showHidden, setShowHidden] = useState(() =>
|
|
19
|
+
typeof window !== 'undefined' && localStorage.getItem('show-hidden-files') === 'true'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Example files cleanup
|
|
23
|
+
const [exampleCount, setExampleCount] = useState<number | null>(null);
|
|
24
|
+
const [cleaningUp, setCleaningUp] = useState(false);
|
|
25
|
+
const [cleanupResult, setCleanupResult] = useState<number | null>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
scanExampleFilesAction().then(r => setExampleCount(r.files.length)).catch(() => {});
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
15
31
|
// Guide state toggle
|
|
16
32
|
const [guideActive, setGuideActive] = useState<boolean | null>(null);
|
|
17
33
|
const [guideDismissed, setGuideDismissed] = useState(false);
|
|
@@ -130,6 +146,51 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
130
146
|
/>
|
|
131
147
|
</Field>
|
|
132
148
|
|
|
149
|
+
<div className="flex items-center justify-between">
|
|
150
|
+
<div>
|
|
151
|
+
<div className="text-sm text-foreground">{k.showHiddenFiles}</div>
|
|
152
|
+
<div className="text-xs text-muted-foreground mt-0.5">{k.showHiddenFilesHint}</div>
|
|
153
|
+
</div>
|
|
154
|
+
<Toggle checked={showHidden} onChange={() => {
|
|
155
|
+
const next = !showHidden;
|
|
156
|
+
setShowHidden(next);
|
|
157
|
+
setShowHiddenFiles(next);
|
|
158
|
+
}} />
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{exampleCount !== null && exampleCount > 0 && cleanupResult === null && (
|
|
162
|
+
<div className="flex items-center justify-between">
|
|
163
|
+
<div>
|
|
164
|
+
<div className="text-sm text-foreground">{k.cleanupExamples}</div>
|
|
165
|
+
<div className="text-xs text-muted-foreground mt-0.5">{k.cleanupExamplesHint}</div>
|
|
166
|
+
</div>
|
|
167
|
+
<button
|
|
168
|
+
onClick={async () => {
|
|
169
|
+
if (!confirm(k.cleanupExamplesConfirm(exampleCount))) return;
|
|
170
|
+
setCleaningUp(true);
|
|
171
|
+
const r = await cleanupExamplesAction();
|
|
172
|
+
setCleaningUp(false);
|
|
173
|
+
if (r.success) {
|
|
174
|
+
setCleanupResult(r.deleted);
|
|
175
|
+
setExampleCount(0);
|
|
176
|
+
}
|
|
177
|
+
}}
|
|
178
|
+
disabled={cleaningUp}
|
|
179
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0 disabled:opacity-50"
|
|
180
|
+
>
|
|
181
|
+
{cleaningUp ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
|
|
182
|
+
{k.cleanupExamplesButton}
|
|
183
|
+
<span className="ml-1 tabular-nums text-2xs opacity-70">{exampleCount}</span>
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
{cleanupResult !== null && (
|
|
188
|
+
<div className="flex items-center gap-2 text-xs text-success">
|
|
189
|
+
<Check size={14} />
|
|
190
|
+
{k.cleanupExamplesDone(cleanupResult)}
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
|
|
133
194
|
<div className="border-t border-border pt-5">
|
|
134
195
|
<SectionLabel>Security</SectionLabel>
|
|
135
196
|
</div>
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/** Walkthrough step anchors — these data-walkthrough attributes are added to target components */
|
|
2
2
|
export type WalkthroughAnchor =
|
|
3
|
-
| 'activity-bar'
|
|
4
3
|
| 'files-panel'
|
|
5
4
|
| 'ask-button'
|
|
6
|
-
| '
|
|
7
|
-
| '
|
|
5
|
+
| 'agents-panel'
|
|
6
|
+
| 'echo-panel';
|
|
8
7
|
|
|
9
8
|
export interface WalkthroughStep {
|
|
10
9
|
anchor: WalkthroughAnchor;
|
|
@@ -12,10 +11,16 @@ export interface WalkthroughStep {
|
|
|
12
11
|
position: 'right' | 'bottom';
|
|
13
12
|
}
|
|
14
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 4-step value-driven walkthrough aligned with the Dual-Layer Wedge strategy:
|
|
16
|
+
* 0. Project Memory (foundation)
|
|
17
|
+
* 1. AI That Already Knows You (wedge)
|
|
18
|
+
* 2. Multi-Agent Sharing (differentiation)
|
|
19
|
+
* 3. Echo — Cognitive Compound Interest (retention seed)
|
|
20
|
+
*/
|
|
15
21
|
export const walkthroughSteps: WalkthroughStep[] = [
|
|
16
|
-
{ anchor: 'activity-bar', position: 'right' },
|
|
17
22
|
{ anchor: 'files-panel', position: 'right' },
|
|
18
23
|
{ anchor: 'ask-button', position: 'right' },
|
|
19
|
-
{ anchor: '
|
|
20
|
-
{ anchor: '
|
|
24
|
+
{ anchor: 'agents-panel', position: 'right' },
|
|
25
|
+
{ anchor: 'echo-panel', position: 'right' },
|
|
21
26
|
];
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef } from 'react';
|
|
4
|
+
import { ALLOWED_IMPORT_EXTENSIONS } from '@/lib/core/file-convert';
|
|
5
|
+
|
|
6
|
+
export type ImportIntent = 'archive' | 'digest';
|
|
7
|
+
export type ImportStep = 'select' | 'archive_config' | 'importing' | 'done';
|
|
8
|
+
export type ConflictMode = 'skip' | 'rename' | 'overwrite';
|
|
9
|
+
|
|
10
|
+
export interface ImportFile {
|
|
11
|
+
file: File;
|
|
12
|
+
name: string;
|
|
13
|
+
size: number;
|
|
14
|
+
content: string | null;
|
|
15
|
+
loading: boolean;
|
|
16
|
+
error: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
20
|
+
const MAX_PDF_SIZE = 12 * 1024 * 1024;
|
|
21
|
+
const MAX_FILES = 20;
|
|
22
|
+
|
|
23
|
+
function getExt(name: string): string {
|
|
24
|
+
const idx = name.lastIndexOf('.');
|
|
25
|
+
return idx >= 0 ? name.slice(idx).toLowerCase() : '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatSize(bytes: number): string {
|
|
29
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
30
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
31
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useFileImport() {
|
|
35
|
+
const [files, setFiles] = useState<ImportFile[]>([]);
|
|
36
|
+
const [step, setStep] = useState<ImportStep>('select');
|
|
37
|
+
const [intent, setIntent] = useState<ImportIntent>('archive');
|
|
38
|
+
const [targetSpace, setTargetSpace] = useState('');
|
|
39
|
+
const [conflict, setConflict] = useState<ConflictMode>('rename');
|
|
40
|
+
const [importing, setImporting] = useState(false);
|
|
41
|
+
const [result, setResult] = useState<{
|
|
42
|
+
created: Array<{ original: string; path: string }>;
|
|
43
|
+
skipped: Array<{ name: string; reason: string }>;
|
|
44
|
+
errors: Array<{ name: string; error: string }>;
|
|
45
|
+
updatedFiles: string[];
|
|
46
|
+
} | null>(null);
|
|
47
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
48
|
+
|
|
49
|
+
const addFiles = useCallback(async (fileList: FileList | File[]) => {
|
|
50
|
+
const incoming = Array.from(fileList).slice(0, MAX_FILES);
|
|
51
|
+
const newFiles: ImportFile[] = [];
|
|
52
|
+
|
|
53
|
+
for (const file of incoming) {
|
|
54
|
+
const ext = getExt(file.name);
|
|
55
|
+
const maxSize = ext === '.pdf' ? MAX_PDF_SIZE : MAX_FILE_SIZE;
|
|
56
|
+
|
|
57
|
+
let error: string | null = null;
|
|
58
|
+
if (!ALLOWED_IMPORT_EXTENSIONS.has(ext)) {
|
|
59
|
+
error = 'unsupported';
|
|
60
|
+
} else if (file.size > maxSize) {
|
|
61
|
+
error = 'tooLarge';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
newFiles.push({
|
|
65
|
+
file,
|
|
66
|
+
name: file.name,
|
|
67
|
+
size: file.size,
|
|
68
|
+
content: null,
|
|
69
|
+
loading: !error,
|
|
70
|
+
error,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setFiles(prev => {
|
|
75
|
+
const merged = [...prev];
|
|
76
|
+
for (const f of newFiles) {
|
|
77
|
+
const isDup = merged.some(m =>
|
|
78
|
+
m.name === f.name && m.size === f.size
|
|
79
|
+
);
|
|
80
|
+
if (!isDup && merged.length < MAX_FILES) merged.push(f);
|
|
81
|
+
}
|
|
82
|
+
return merged;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
for (const f of newFiles) {
|
|
86
|
+
if (f.error) continue;
|
|
87
|
+
try {
|
|
88
|
+
const text = await f.file.text();
|
|
89
|
+
setFiles(prev => prev.map(p =>
|
|
90
|
+
p.name === f.name && p.size === f.size
|
|
91
|
+
? { ...p, content: text, loading: false }
|
|
92
|
+
: p
|
|
93
|
+
));
|
|
94
|
+
} catch {
|
|
95
|
+
setFiles(prev => prev.map(p =>
|
|
96
|
+
p.name === f.name && p.size === f.size
|
|
97
|
+
? { ...p, loading: false, error: 'readFailed' }
|
|
98
|
+
: p
|
|
99
|
+
));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
const removeFile = useCallback((index: number) => {
|
|
105
|
+
setFiles(prev => prev.filter((_, i) => i !== index));
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const clearFiles = useCallback(() => {
|
|
109
|
+
setFiles([]);
|
|
110
|
+
setStep('select');
|
|
111
|
+
setResult(null);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
const validFiles = files.filter(f => !f.error && f.content !== null);
|
|
115
|
+
const allReady = files.length > 0 && files.every(f => !f.loading);
|
|
116
|
+
const hasErrors = files.some(f => f.error);
|
|
117
|
+
|
|
118
|
+
const doArchive = useCallback(async () => {
|
|
119
|
+
if (validFiles.length === 0) return;
|
|
120
|
+
setImporting(true);
|
|
121
|
+
setStep('importing');
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const payload = {
|
|
125
|
+
files: validFiles.map(f => ({
|
|
126
|
+
name: f.name,
|
|
127
|
+
content: f.content!,
|
|
128
|
+
})),
|
|
129
|
+
targetSpace: targetSpace || undefined,
|
|
130
|
+
conflict,
|
|
131
|
+
organize: true,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const res = await fetch('/api/file/import', {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
body: JSON.stringify(payload),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const data = await res.json();
|
|
141
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
142
|
+
|
|
143
|
+
setResult(data);
|
|
144
|
+
setStep('done');
|
|
145
|
+
} catch (err) {
|
|
146
|
+
setResult({
|
|
147
|
+
created: [],
|
|
148
|
+
skipped: [],
|
|
149
|
+
errors: [{ name: '*', error: (err as Error).message }],
|
|
150
|
+
updatedFiles: [],
|
|
151
|
+
});
|
|
152
|
+
setStep('done');
|
|
153
|
+
} finally {
|
|
154
|
+
setImporting(false);
|
|
155
|
+
}
|
|
156
|
+
}, [validFiles, targetSpace, conflict]);
|
|
157
|
+
|
|
158
|
+
const reset = useCallback(() => {
|
|
159
|
+
setFiles([]);
|
|
160
|
+
setStep('select');
|
|
161
|
+
setIntent('archive');
|
|
162
|
+
setTargetSpace('');
|
|
163
|
+
setConflict('rename');
|
|
164
|
+
setImporting(false);
|
|
165
|
+
setResult(null);
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
files,
|
|
170
|
+
step,
|
|
171
|
+
intent,
|
|
172
|
+
targetSpace,
|
|
173
|
+
conflict,
|
|
174
|
+
importing,
|
|
175
|
+
result,
|
|
176
|
+
inputRef,
|
|
177
|
+
validFiles,
|
|
178
|
+
allReady,
|
|
179
|
+
hasErrors,
|
|
180
|
+
addFiles,
|
|
181
|
+
removeFile,
|
|
182
|
+
clearFiles,
|
|
183
|
+
setStep,
|
|
184
|
+
setIntent,
|
|
185
|
+
setTargetSpace,
|
|
186
|
+
setConflict,
|
|
187
|
+
doArchive,
|
|
188
|
+
reset,
|
|
189
|
+
formatSize,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -115,6 +115,16 @@ export function useFileUpload() {
|
|
|
115
115
|
setUploadError('');
|
|
116
116
|
}, []);
|
|
117
117
|
|
|
118
|
+
const injectFiles = useCallback((files: LocalAttachment[]) => {
|
|
119
|
+
setLocalAttachments(prev => {
|
|
120
|
+
const merged = [...prev];
|
|
121
|
+
for (const item of files) {
|
|
122
|
+
if (!merged.some(m => m.name === item.name)) merged.push(item);
|
|
123
|
+
}
|
|
124
|
+
return merged;
|
|
125
|
+
});
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
118
128
|
return {
|
|
119
129
|
localAttachments,
|
|
120
130
|
uploadError,
|
|
@@ -122,5 +132,6 @@ export function useFileUpload() {
|
|
|
122
132
|
pickFiles,
|
|
123
133
|
removeAttachment,
|
|
124
134
|
clearAttachments,
|
|
135
|
+
injectFiles,
|
|
125
136
|
};
|
|
126
137
|
}
|
package/app/hooks/useMention.ts
CHANGED
|
@@ -27,19 +27,27 @@ export function useMention() {
|
|
|
27
27
|
}, [loadFiles]);
|
|
28
28
|
|
|
29
29
|
const updateMentionFromInput = useCallback(
|
|
30
|
-
(val: string) => {
|
|
31
|
-
const
|
|
30
|
+
(val: string, cursorPos?: number) => {
|
|
31
|
+
const pos = cursorPos ?? val.length;
|
|
32
|
+
const before = val.slice(0, pos);
|
|
33
|
+
const atIdx = before.lastIndexOf('@');
|
|
32
34
|
if (atIdx === -1) {
|
|
33
35
|
setMentionQuery(null);
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
if (atIdx > 0 && before !== ' ') {
|
|
38
|
+
if (atIdx > 0 && before[atIdx - 1] !== ' ' && before[atIdx - 1] !== '\n') {
|
|
38
39
|
setMentionQuery(null);
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
|
-
const query =
|
|
42
|
-
|
|
42
|
+
const query = before.slice(atIdx + 1);
|
|
43
|
+
if (query.includes(' ') || query.includes('\n')) {
|
|
44
|
+
setMentionQuery(null);
|
|
45
|
+
setMentionResults([]);
|
|
46
|
+
setMentionIndex(0);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const q = query.toLowerCase();
|
|
50
|
+
const filtered = allFiles.filter((f) => f.toLowerCase().includes(q)).slice(0, 30);
|
|
43
51
|
if (filtered.length === 0) {
|
|
44
52
|
setMentionQuery(null);
|
|
45
53
|
setMentionResults([]);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import type { SkillInfo } from '@/components/settings/types';
|
|
5
|
+
|
|
6
|
+
export interface SlashItem {
|
|
7
|
+
type: 'skill';
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function safeFetchSkills(): Promise<SkillInfo[]> {
|
|
13
|
+
return fetch('/api/skills')
|
|
14
|
+
.then((r) => (r.ok ? r.json() : { skills: [] }))
|
|
15
|
+
.then((data) => (Array.isArray(data?.skills) ? data.skills : []))
|
|
16
|
+
.catch(() => [] as SkillInfo[]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useSlashCommand() {
|
|
20
|
+
const [allSkills, setAllSkills] = useState<SkillInfo[]>([]);
|
|
21
|
+
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
|
22
|
+
const [slashResults, setSlashResults] = useState<SlashItem[]>([]);
|
|
23
|
+
const [slashIndex, setSlashIndex] = useState(0);
|
|
24
|
+
const loaded = useRef(false);
|
|
25
|
+
|
|
26
|
+
const loadSkills = useCallback(async () => {
|
|
27
|
+
const skills = await safeFetchSkills();
|
|
28
|
+
setAllSkills(skills.filter((s) => s.enabled));
|
|
29
|
+
loaded.current = true;
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
loadSkills();
|
|
34
|
+
const handler = () => loadSkills();
|
|
35
|
+
window.addEventListener('mindos:skills-changed', handler);
|
|
36
|
+
return () => window.removeEventListener('mindos:skills-changed', handler);
|
|
37
|
+
}, [loadSkills]);
|
|
38
|
+
|
|
39
|
+
const updateSlashFromInput = useCallback(
|
|
40
|
+
(val: string, cursorPos: number) => {
|
|
41
|
+
const before = val.slice(0, cursorPos);
|
|
42
|
+
const slashIdx = before.lastIndexOf('/');
|
|
43
|
+
|
|
44
|
+
if (slashIdx === -1) {
|
|
45
|
+
setSlashQuery(null);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// `/` must be at line start or preceded by whitespace
|
|
50
|
+
if (slashIdx > 0 && before[slashIdx - 1] !== ' ' && before[slashIdx - 1] !== '\n') {
|
|
51
|
+
setSlashQuery(null);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// No space in the typed query — slash commands are single tokens
|
|
56
|
+
const query = before.slice(slashIdx + 1);
|
|
57
|
+
if (query.includes(' ')) {
|
|
58
|
+
setSlashQuery(null);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!loaded.current) {
|
|
63
|
+
loadSkills();
|
|
64
|
+
setSlashQuery(null);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const q = query.toLowerCase();
|
|
69
|
+
const items: SlashItem[] = allSkills
|
|
70
|
+
.filter((s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q))
|
|
71
|
+
.slice(0, 20)
|
|
72
|
+
.map((s) => ({ type: 'skill', name: s.name, description: s.description }));
|
|
73
|
+
|
|
74
|
+
if (items.length === 0) {
|
|
75
|
+
setSlashQuery(null);
|
|
76
|
+
setSlashResults([]);
|
|
77
|
+
setSlashIndex(0);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setSlashQuery(query);
|
|
82
|
+
setSlashResults(items);
|
|
83
|
+
setSlashIndex(0);
|
|
84
|
+
},
|
|
85
|
+
[allSkills, loadSkills],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const navigateSlash = useCallback(
|
|
89
|
+
(direction: 'up' | 'down') => {
|
|
90
|
+
if (slashResults.length === 0) return;
|
|
91
|
+
if (direction === 'down') {
|
|
92
|
+
setSlashIndex((i) => Math.min(i + 1, slashResults.length - 1));
|
|
93
|
+
} else {
|
|
94
|
+
setSlashIndex((i) => Math.max(i - 1, 0));
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[slashResults.length],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const resetSlash = useCallback(() => {
|
|
101
|
+
setSlashQuery(null);
|
|
102
|
+
setSlashResults([]);
|
|
103
|
+
setSlashIndex(0);
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
slashQuery,
|
|
108
|
+
slashResults,
|
|
109
|
+
slashIndex,
|
|
110
|
+
updateSlashFromInput,
|
|
111
|
+
navigateSlash,
|
|
112
|
+
resetSlash,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/app/lib/actions.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
'use server';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { createFile, deleteFile, deleteDirectory, convertToSpace, renameFile, renameSpace, getMindRoot, invalidateCache, collectAllFiles } from '@/lib/fs';
|
|
6
|
+
import { createSpaceFilesystem, generateReadmeTemplate } from '@/lib/core/create-space';
|
|
7
|
+
import { INSTRUCTION_TEMPLATE, cleanDirName } from '@/lib/core/space-scaffold';
|
|
5
8
|
import { revalidatePath } from 'next/cache';
|
|
6
9
|
|
|
7
10
|
export async function createFileAction(dirPath: string, fileName: string): Promise<{ success: boolean; filePath?: string; error?: string }> {
|
|
@@ -112,3 +115,77 @@ export async function createSpaceAction(
|
|
|
112
115
|
return { success: false, error: msg };
|
|
113
116
|
}
|
|
114
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Revert AI-generated space content back to scaffold templates.
|
|
121
|
+
* Called when user discards AI initialization from SpaceInitToast.
|
|
122
|
+
*/
|
|
123
|
+
export async function revertSpaceInitAction(
|
|
124
|
+
spacePath: string,
|
|
125
|
+
name: string,
|
|
126
|
+
description: string,
|
|
127
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
128
|
+
try {
|
|
129
|
+
const mindRoot = getMindRoot();
|
|
130
|
+
const absDir = path.resolve(mindRoot, spacePath);
|
|
131
|
+
if (!absDir.startsWith(mindRoot)) {
|
|
132
|
+
return { success: false, error: 'Invalid path' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const readmePath = path.join(absDir, 'README.md');
|
|
136
|
+
const instructionPath = path.join(absDir, 'INSTRUCTION.md');
|
|
137
|
+
|
|
138
|
+
const readmeContent = generateReadmeTemplate(spacePath, name, description);
|
|
139
|
+
fs.writeFileSync(readmePath, readmeContent, 'utf-8');
|
|
140
|
+
|
|
141
|
+
const dirName = cleanDirName(name);
|
|
142
|
+
fs.writeFileSync(instructionPath, INSTRUCTION_TEMPLATE(dirName), 'utf-8');
|
|
143
|
+
|
|
144
|
+
invalidateCache();
|
|
145
|
+
revalidatePath('/', 'layout');
|
|
146
|
+
return { success: true };
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return { success: false, error: err instanceof Error ? err.message : 'Failed to revert' };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const EXAMPLE_PREFIX = '🧪_example_';
|
|
153
|
+
|
|
154
|
+
export async function scanExampleFilesAction(): Promise<{ files: string[] }> {
|
|
155
|
+
const all = collectAllFiles();
|
|
156
|
+
const examples = all.filter(f => path.basename(f).startsWith(EXAMPLE_PREFIX));
|
|
157
|
+
return { files: examples };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function cleanupExamplesAction(): Promise<{ success: boolean; deleted: number; error?: string }> {
|
|
161
|
+
try {
|
|
162
|
+
const { files } = await scanExampleFilesAction();
|
|
163
|
+
if (files.length === 0) return { success: true, deleted: 0 };
|
|
164
|
+
|
|
165
|
+
const root = getMindRoot();
|
|
166
|
+
for (const relPath of files) {
|
|
167
|
+
const absPath = path.resolve(root, relPath);
|
|
168
|
+
if (absPath.startsWith(root) && fs.existsSync(absPath)) {
|
|
169
|
+
fs.unlinkSync(absPath);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Clean up empty directories left behind
|
|
174
|
+
const dirs = new Set(files.map(f => path.dirname(path.resolve(root, f))));
|
|
175
|
+
const sortedDirs = [...dirs].sort((a, b) => b.length - a.length);
|
|
176
|
+
for (const dir of sortedDirs) {
|
|
177
|
+
try {
|
|
178
|
+
if (dir.startsWith(root) && dir !== root) {
|
|
179
|
+
const entries = fs.readdirSync(dir);
|
|
180
|
+
if (entries.length === 0) fs.rmdirSync(dir);
|
|
181
|
+
}
|
|
182
|
+
} catch { /* directory not empty or already removed */ }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
invalidateCache();
|
|
186
|
+
revalidatePath('/', 'layout');
|
|
187
|
+
return { success: true, deleted: files.length };
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return { success: false, deleted: 0, error: err instanceof Error ? err.message : 'Failed to cleanup' };
|
|
190
|
+
}
|
|
191
|
+
}
|
package/app/lib/agent/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { getModelConfig } from './model';
|
|
2
|
-
export { knowledgeBaseTools, WRITE_TOOLS, truncate } from './tools';
|
|
2
|
+
export { getRequestScopedTools, knowledgeBaseTools, WRITE_TOOLS, truncate } from './tools';
|
|
3
3
|
export { AGENT_SYSTEM_PROMPT } from './prompt';
|
|
4
4
|
export {
|
|
5
5
|
estimateTokens, estimateStringTokens, getContextLimit, needsCompact,
|
package/app/lib/agent/prompt.ts
CHANGED
|
@@ -27,6 +27,8 @@ Persona: Methodical, strictly objective, execution-oriented. Zero fluff. Never u
|
|
|
27
27
|
|
|
28
28
|
- **Auto-loaded**: Configs, instructions, and SKILL.md are already in your context. Do not search for them unless explicitly asked.
|
|
29
29
|
- **Uploaded Files**: Local files attached by the user appear in the "⚠️ USER-UPLOADED FILES" section below. Use this content directly. Do NOT use tools to read/search them.
|
|
30
|
+
- **Skills**: Use the list_skills and load_skill tools to discover available skills on demand.
|
|
31
|
+
- **MCP**: The MindOS MCP server is built-in. Use list_mcp_tools and call_mcp_tool to inspect and invoke additional MCP tools configured in ~/.mindos/mcp.json.
|
|
30
32
|
|
|
31
33
|
## Output
|
|
32
34
|
|