@geminilight/mindos 0.6.8 → 0.6.12
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 +2 -0
- package/README_zh.md +2 -0
- package/app/app/api/mcp/install/route.ts +4 -1
- package/app/app/api/setup/check-path/route.ts +2 -7
- package/app/app/api/setup/ls/route.ts +3 -9
- package/app/app/api/setup/path-utils.ts +8 -0
- package/app/app/api/setup/route.ts +2 -7
- package/app/app/api/uninstall/route.ts +47 -0
- package/app/app/globals.css +11 -0
- package/app/components/ActivityBar.tsx +10 -3
- package/app/components/AskFab.tsx +7 -3
- package/app/components/CreateSpaceModal.tsx +1 -1
- package/app/components/DirView.tsx +1 -1
- package/app/components/FileTree.tsx +30 -23
- package/app/components/GuideCard.tsx +1 -1
- package/app/components/HomeContent.tsx +137 -109
- package/app/components/ImportModal.tsx +16 -477
- package/app/components/MarkdownView.tsx +3 -0
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/OrganizeToast.tsx +386 -0
- package/app/components/Panel.tsx +23 -2
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/SidebarLayout.tsx +44 -1
- package/app/components/agents/AgentDetailContent.tsx +33 -12
- package/app/components/agents/AgentsMcpSection.tsx +1 -1
- package/app/components/agents/AgentsOverviewSection.tsx +3 -4
- package/app/components/agents/AgentsPrimitives.tsx +2 -2
- package/app/components/agents/AgentsSkillsSection.tsx +2 -2
- package/app/components/agents/SkillDetailPopover.tsx +24 -8
- package/app/components/ask/AskContent.tsx +124 -75
- package/app/components/ask/HighlightMatch.tsx +14 -0
- package/app/components/ask/MentionPopover.tsx +5 -3
- package/app/components/ask/MessageList.tsx +39 -11
- package/app/components/ask/SlashCommandPopover.tsx +4 -2
- package/app/components/changes/ChangesBanner.tsx +20 -2
- package/app/components/changes/ChangesContentPage.tsx +10 -2
- package/app/components/echo/EchoHero.tsx +1 -1
- package/app/components/echo/EchoInsightCollapsible.tsx +1 -1
- package/app/components/echo/EchoPageSections.tsx +1 -1
- package/app/components/explore/UseCaseCard.tsx +1 -1
- package/app/components/panels/DiscoverPanel.tsx +29 -25
- package/app/components/panels/ImportHistoryPanel.tsx +195 -0
- package/app/components/panels/PluginsPanel.tsx +2 -2
- package/app/components/settings/AiTab.tsx +24 -0
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpSkillCreateForm.tsx +1 -1
- package/app/components/settings/McpSkillRow.tsx +1 -1
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +2 -2
- package/app/components/settings/PluginsTab.tsx +1 -1
- package/app/components/settings/Primitives.tsx +118 -6
- package/app/components/settings/SettingsContent.tsx +5 -2
- package/app/components/settings/UninstallTab.tsx +179 -0
- package/app/components/settings/UpdateTab.tsx +17 -5
- package/app/components/settings/types.ts +2 -1
- package/app/components/ui/dialog.tsx +1 -1
- package/app/hooks/useAiOrganize.ts +122 -10
- package/app/hooks/useMention.ts +21 -3
- package/app/hooks/useSlashCommand.ts +18 -4
- package/app/lib/agent/reconnect.ts +40 -0
- package/app/lib/core/backlinks.ts +2 -2
- package/app/lib/core/git.ts +14 -10
- package/app/lib/fs.ts +2 -1
- package/app/lib/i18n-en.ts +46 -2
- package/app/lib/i18n-zh.ts +46 -2
- package/app/lib/organize-history.ts +74 -0
- package/app/lib/settings.ts +2 -0
- package/app/lib/types.ts +2 -0
- package/app/next.config.ts +23 -5
- package/bin/cli.js +6 -9
- package/bin/lib/mcp-build.js +74 -0
- package/bin/lib/mcp-spawn.js +8 -5
- package/bin/lib/port.js +17 -2
- package/bin/lib/stop.js +12 -2
- package/mcp/dist/index.cjs +43 -43
- package/mcp/src/index.ts +58 -12
- package/package.json +1 -1
- package/scripts/setup.js +2 -2
|
@@ -15,8 +15,13 @@ export interface OrganizeFileChange {
|
|
|
15
15
|
toolCallId: string;
|
|
16
16
|
/** Whether the tool call completed successfully */
|
|
17
17
|
ok: boolean;
|
|
18
|
+
/** Marked true after user undoes this change */
|
|
19
|
+
undone?: boolean;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
/** In-memory file snapshots captured before AI writes, keyed by path */
|
|
23
|
+
export type FileSnapshots = Map<string, string>;
|
|
24
|
+
|
|
20
25
|
/** User-facing stage hint derived from SSE events */
|
|
21
26
|
export type OrganizeStageHint =
|
|
22
27
|
| 'connecting'
|
|
@@ -43,11 +48,12 @@ export interface AiOrganizeState {
|
|
|
43
48
|
|
|
44
49
|
/**
|
|
45
50
|
* Strip model chain-of-thought tags that should never be shown to users.
|
|
46
|
-
*
|
|
51
|
+
* Covers <thinking>, <reasoning>, <scratchpad> per wiki/80-known-pitfalls.md.
|
|
52
|
+
* Handles both complete blocks and unclosed trailing tags (streaming).
|
|
47
53
|
*/
|
|
48
54
|
export function stripThinkingTags(text: string): string {
|
|
49
|
-
let cleaned = text.replace(/<thinking>[\s\S]
|
|
50
|
-
cleaned = cleaned.replace(/<thinking>[\s\S]*$/gi, '');
|
|
55
|
+
let cleaned = text.replace(/<(thinking|reasoning|scratchpad)>[\s\S]*?<\/\1>/gi, '');
|
|
56
|
+
cleaned = cleaned.replace(/<(?:thinking|reasoning|scratchpad)>[\s\S]*$/gi, '');
|
|
51
57
|
return cleaned.trim();
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -104,9 +110,25 @@ function extractPathFromArgs(toolName: string, args: unknown): string {
|
|
|
104
110
|
return '';
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Fetch a file's current content for snapshot (best-effort, never throws).
|
|
115
|
+
* Returns empty string on failure — undo will be unavailable for that file.
|
|
116
|
+
*/
|
|
117
|
+
async function captureSnapshot(path: string): Promise<string> {
|
|
118
|
+
try {
|
|
119
|
+
const res = await fetch(`/api/file?path=${encodeURIComponent(path)}&op=read_file`);
|
|
120
|
+
if (!res.ok) return '';
|
|
121
|
+
const data = await res.json() as { content?: string };
|
|
122
|
+
return data.content ?? '';
|
|
123
|
+
} catch {
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
107
128
|
async function consumeOrganizeStream(
|
|
108
129
|
body: ReadableStream<Uint8Array>,
|
|
109
130
|
onProgress: (state: Partial<AiOrganizeState> & { summary?: string }) => void,
|
|
131
|
+
onSnapshot: (path: string, content: string) => void,
|
|
110
132
|
signal?: AbortSignal,
|
|
111
133
|
): Promise<{ changes: OrganizeFileChange[]; summary: string; toolCallCount: number }> {
|
|
112
134
|
const reader = body.getReader();
|
|
@@ -117,6 +139,7 @@ async function consumeOrganizeStream(
|
|
|
117
139
|
const pendingTools = new Map<string, { name: string; path: string; action: 'create' | 'update' | 'unknown' }>();
|
|
118
140
|
let summary = '';
|
|
119
141
|
let toolCallCount = 0;
|
|
142
|
+
const snapshotted = new Set<string>();
|
|
120
143
|
|
|
121
144
|
try {
|
|
122
145
|
while (true) {
|
|
@@ -154,6 +177,15 @@ async function consumeOrganizeStream(
|
|
|
154
177
|
let action: 'create' | 'update' | 'unknown' = 'update';
|
|
155
178
|
if (toolName === 'create_file' || toolName === 'batch_create_files') action = 'create';
|
|
156
179
|
else if (toolName === 'delete_file' || toolName === 'rename_file' || toolName === 'move_file') action = 'unknown';
|
|
180
|
+
|
|
181
|
+
// Capture snapshot for update operations (fire-and-forget)
|
|
182
|
+
if (action === 'update' && path && !snapshotted.has(path)) {
|
|
183
|
+
snapshotted.add(path);
|
|
184
|
+
captureSnapshot(path).then(content => {
|
|
185
|
+
if (content) onSnapshot(path, content);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
157
189
|
pendingTools.set(toolCallId, { name: toolName, path, action });
|
|
158
190
|
onProgress({ currentTool: { name: toolName, path } });
|
|
159
191
|
}
|
|
@@ -214,6 +246,8 @@ export function useAiOrganize() {
|
|
|
214
246
|
const [toolCallCount, setToolCallCount] = useState(0);
|
|
215
247
|
const abortRef = useRef<AbortController | null>(null);
|
|
216
248
|
const lastEventRef = useRef<number>(0);
|
|
249
|
+
const snapshotsRef = useRef<FileSnapshots>(new Map());
|
|
250
|
+
const [sourceFileNames, setSourceFileNames] = useState<string[]>([]);
|
|
217
251
|
|
|
218
252
|
const start = useCallback(async (files: LocalAttachment[], prompt: string) => {
|
|
219
253
|
setPhase('organizing');
|
|
@@ -223,6 +257,8 @@ export function useAiOrganize() {
|
|
|
223
257
|
setSummary('');
|
|
224
258
|
setError(null);
|
|
225
259
|
setToolCallCount(0);
|
|
260
|
+
setSourceFileNames(files.map(f => f.name));
|
|
261
|
+
snapshotsRef.current = new Map();
|
|
226
262
|
lastEventRef.current = Date.now();
|
|
227
263
|
|
|
228
264
|
const controller = new AbortController();
|
|
@@ -271,6 +307,9 @@ export function useAiOrganize() {
|
|
|
271
307
|
if (partial.stageHint) setStageHint(partial.stageHint);
|
|
272
308
|
if (partial.summary !== undefined) setSummary(partial.summary);
|
|
273
309
|
},
|
|
310
|
+
(path, content) => {
|
|
311
|
+
snapshotsRef.current.set(path, content);
|
|
312
|
+
},
|
|
274
313
|
controller.signal,
|
|
275
314
|
);
|
|
276
315
|
|
|
@@ -295,22 +334,88 @@ export function useAiOrganize() {
|
|
|
295
334
|
abortRef.current?.abort();
|
|
296
335
|
}, []);
|
|
297
336
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
337
|
+
/** Undo a single file change — supports both create (delete) and update (restore snapshot) */
|
|
338
|
+
const undoOne = useCallback(async (path: string): Promise<boolean> => {
|
|
339
|
+
const target = changes.find(c => c.path === path && c.ok && !c.undone);
|
|
340
|
+
if (!target) return false;
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
if (target.action === 'create') {
|
|
303
344
|
const res = await fetch('/api/file', {
|
|
304
345
|
method: 'POST',
|
|
305
346
|
headers: { 'Content-Type': 'application/json' },
|
|
306
|
-
body: JSON.stringify({ op: 'delete_file', path:
|
|
347
|
+
body: JSON.stringify({ op: 'delete_file', path: target.path }),
|
|
307
348
|
});
|
|
308
|
-
if (res.ok)
|
|
349
|
+
if (!res.ok) return false;
|
|
350
|
+
} else if (target.action === 'update') {
|
|
351
|
+
const snapshot = snapshotsRef.current.get(path);
|
|
352
|
+
if (!snapshot) return false;
|
|
353
|
+
const res = await fetch('/api/file', {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
356
|
+
body: JSON.stringify({ op: 'write_file', path: target.path, content: snapshot }),
|
|
357
|
+
});
|
|
358
|
+
if (!res.ok) return false;
|
|
359
|
+
} else {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
setChanges(prev => prev.map(c =>
|
|
364
|
+
c.path === path && c.ok && !c.undone ? { ...c, undone: true } : c,
|
|
365
|
+
));
|
|
366
|
+
return true;
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}, [changes]);
|
|
371
|
+
|
|
372
|
+
const undoAll = useCallback(async (): Promise<number> => {
|
|
373
|
+
const undoable = changes.filter(c => c.ok && !c.undone && (c.action === 'create' || (c.action === 'update' && snapshotsRef.current.has(c.path))));
|
|
374
|
+
let reverted = 0;
|
|
375
|
+
for (const file of undoable) {
|
|
376
|
+
try {
|
|
377
|
+
if (file.action === 'create') {
|
|
378
|
+
const res = await fetch('/api/file', {
|
|
379
|
+
method: 'POST',
|
|
380
|
+
headers: { 'Content-Type': 'application/json' },
|
|
381
|
+
body: JSON.stringify({ op: 'delete_file', path: file.path }),
|
|
382
|
+
});
|
|
383
|
+
if (res.ok) reverted++;
|
|
384
|
+
} else if (file.action === 'update') {
|
|
385
|
+
const snapshot = snapshotsRef.current.get(file.path);
|
|
386
|
+
if (snapshot) {
|
|
387
|
+
const res = await fetch('/api/file', {
|
|
388
|
+
method: 'POST',
|
|
389
|
+
headers: { 'Content-Type': 'application/json' },
|
|
390
|
+
body: JSON.stringify({ op: 'write_file', path: file.path, content: snapshot }),
|
|
391
|
+
});
|
|
392
|
+
if (res.ok) reverted++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
309
395
|
} catch {}
|
|
310
396
|
}
|
|
397
|
+
if (reverted > 0) {
|
|
398
|
+
setChanges(prev => prev.map(c => {
|
|
399
|
+
if (!c.ok || c.undone) return c;
|
|
400
|
+
if (c.action === 'create') return { ...c, undone: true };
|
|
401
|
+
if (c.action === 'update' && snapshotsRef.current.has(c.path)) return { ...c, undone: true };
|
|
402
|
+
return c;
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
311
405
|
return reverted;
|
|
312
406
|
}, [changes]);
|
|
313
407
|
|
|
408
|
+
/** Check if a specific file can be undone */
|
|
409
|
+
const canUndo = useCallback((path: string): boolean => {
|
|
410
|
+
const c = changes.find(ch => ch.path === path && ch.ok && !ch.undone);
|
|
411
|
+
if (!c) return false;
|
|
412
|
+
if (c.action === 'create') return true;
|
|
413
|
+
if (c.action === 'update') return snapshotsRef.current.has(path);
|
|
414
|
+
return false;
|
|
415
|
+
}, [changes]);
|
|
416
|
+
|
|
417
|
+
const hasAnyUndoable = changes.some(c => c.ok && !c.undone && (c.action === 'create' || (c.action === 'update' && snapshotsRef.current.has(c.path))));
|
|
418
|
+
|
|
314
419
|
const reset = useCallback(() => {
|
|
315
420
|
setPhase('idle');
|
|
316
421
|
setChanges([]);
|
|
@@ -319,6 +424,8 @@ export function useAiOrganize() {
|
|
|
319
424
|
setSummary('');
|
|
320
425
|
setError(null);
|
|
321
426
|
setToolCallCount(0);
|
|
427
|
+
setSourceFileNames([]);
|
|
428
|
+
snapshotsRef.current = new Map();
|
|
322
429
|
lastEventRef.current = 0;
|
|
323
430
|
}, []);
|
|
324
431
|
|
|
@@ -330,9 +437,14 @@ export function useAiOrganize() {
|
|
|
330
437
|
summary,
|
|
331
438
|
error,
|
|
332
439
|
toolCallCount,
|
|
440
|
+
sourceFileNames,
|
|
441
|
+
snapshots: snapshotsRef,
|
|
333
442
|
start,
|
|
334
443
|
abort,
|
|
444
|
+
undoOne,
|
|
335
445
|
undoAll,
|
|
446
|
+
canUndo,
|
|
447
|
+
hasAnyUndoable,
|
|
336
448
|
reset,
|
|
337
449
|
};
|
|
338
450
|
}
|
package/app/hooks/useMention.ts
CHANGED
|
@@ -47,15 +47,33 @@ export function useMention() {
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
const q = query.toLowerCase();
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if (!q) {
|
|
51
|
+
setMentionQuery(query);
|
|
52
|
+
setMentionResults(allFiles.slice(0, 30));
|
|
53
|
+
setMentionIndex(0);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const scored = allFiles
|
|
57
|
+
.map((f) => {
|
|
58
|
+
const name = (f.split('/').pop() ?? f).toLowerCase();
|
|
59
|
+
const fl = f.toLowerCase();
|
|
60
|
+
let score = 0;
|
|
61
|
+
if (name.startsWith(q)) score = 100;
|
|
62
|
+
else if (name.includes(q)) score = 50;
|
|
63
|
+
else if (fl.includes(q)) score = 10;
|
|
64
|
+
return { path: f, score };
|
|
65
|
+
})
|
|
66
|
+
.filter((x) => x.score > 0)
|
|
67
|
+
.sort((a, b) => b.score - a.score)
|
|
68
|
+
.slice(0, 30);
|
|
69
|
+
if (scored.length === 0) {
|
|
52
70
|
setMentionQuery(null);
|
|
53
71
|
setMentionResults([]);
|
|
54
72
|
setMentionIndex(0);
|
|
55
73
|
return;
|
|
56
74
|
}
|
|
57
75
|
setMentionQuery(query);
|
|
58
|
-
setMentionResults(
|
|
76
|
+
setMentionResults(scored.map((x) => x.path));
|
|
59
77
|
setMentionIndex(0);
|
|
60
78
|
},
|
|
61
79
|
[allFiles],
|
|
@@ -66,10 +66,24 @@ export function useSlashCommand() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const q = query.toLowerCase();
|
|
69
|
-
const items: SlashItem[] =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
const items: SlashItem[] = (q
|
|
70
|
+
? allSkills
|
|
71
|
+
.map((s) => {
|
|
72
|
+
const nl = s.name.toLowerCase();
|
|
73
|
+
let score = 0;
|
|
74
|
+
if (nl.startsWith(q)) score = 100;
|
|
75
|
+
else if (nl.includes(q)) score = 50;
|
|
76
|
+
else if (s.description.toLowerCase().includes(q)) score = 10;
|
|
77
|
+
return { s, score };
|
|
78
|
+
})
|
|
79
|
+
.filter((x) => x.score > 0)
|
|
80
|
+
.sort((a, b) => b.score - a.score)
|
|
81
|
+
.slice(0, 20)
|
|
82
|
+
.map((x) => ({ type: 'skill' as const, name: x.s.name, description: x.s.description }))
|
|
83
|
+
: allSkills
|
|
84
|
+
.slice(0, 20)
|
|
85
|
+
.map((s) => ({ type: 'skill' as const, name: s.name, description: s.description }))
|
|
86
|
+
);
|
|
73
87
|
|
|
74
88
|
if (items.length === 0) {
|
|
75
89
|
setSlashQuery(null);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Auto-reconnect utilities for Ask AI streaming connections. */
|
|
2
|
+
|
|
3
|
+
const NON_RETRYABLE_STATUS = new Set([401, 403, 429]);
|
|
4
|
+
|
|
5
|
+
const NON_RETRYABLE_PATTERNS = [
|
|
6
|
+
/api.?key/i,
|
|
7
|
+
/model.*not.?found/i,
|
|
8
|
+
/authentication/i,
|
|
9
|
+
/unauthorized/i,
|
|
10
|
+
/forbidden/i,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function isRetryableError(err: unknown, httpStatus?: number): boolean {
|
|
14
|
+
if (err instanceof DOMException && err.name === 'AbortError') return false;
|
|
15
|
+
if (httpStatus && NON_RETRYABLE_STATUS.has(httpStatus)) return false;
|
|
16
|
+
|
|
17
|
+
if (err instanceof Error) {
|
|
18
|
+
const msg = err.message;
|
|
19
|
+
if (NON_RETRYABLE_PATTERNS.some(p => p.test(msg))) return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const BASE_DELAY = 1000;
|
|
26
|
+
const MAX_DELAY = 10_000;
|
|
27
|
+
|
|
28
|
+
/** Exponential backoff: 1s, 2s, 4s, 8s... capped at 10s */
|
|
29
|
+
export function retryDelay(attempt: number): number {
|
|
30
|
+
return Math.min(BASE_DELAY * 2 ** attempt, MAX_DELAY);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const abortReason = () => signal?.reason ?? new DOMException('The operation was aborted.', 'AbortError');
|
|
36
|
+
if (signal?.aborted) { reject(abortReason()); return; }
|
|
37
|
+
const timer = setTimeout(resolve, ms);
|
|
38
|
+
signal?.addEventListener('abort', () => { clearTimeout(timer); reject(abortReason()); }, { once: true });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -7,8 +7,8 @@ import type { BacklinkEntry } from './types';
|
|
|
7
7
|
* Finds files that reference the given targetPath via wikilinks,
|
|
8
8
|
* markdown links, or backtick references.
|
|
9
9
|
*/
|
|
10
|
-
export function findBacklinks(mindRoot: string, targetPath: string): BacklinkEntry[] {
|
|
11
|
-
const allFiles = collectAllFiles(mindRoot).filter(f => f.endsWith('.md') && f !== targetPath);
|
|
10
|
+
export function findBacklinks(mindRoot: string, targetPath: string, cachedFiles?: string[]): BacklinkEntry[] {
|
|
11
|
+
const allFiles = (cachedFiles ?? collectAllFiles(mindRoot)).filter(f => f.endsWith('.md') && f !== targetPath);
|
|
12
12
|
const results: BacklinkEntry[] = [];
|
|
13
13
|
const bname = path.basename(targetPath, '.md');
|
|
14
14
|
const escapedTarget = targetPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
package/app/lib/core/git.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
2
|
import { resolveSafe } from './security';
|
|
3
3
|
import type { GitLogEntry } from './types';
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ import type { GitLogEntry } from './types';
|
|
|
7
7
|
*/
|
|
8
8
|
export function isGitRepo(mindRoot: string): boolean {
|
|
9
9
|
try {
|
|
10
|
-
|
|
10
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: mindRoot, stdio: 'pipe' });
|
|
11
11
|
return true;
|
|
12
12
|
} catch { return false; }
|
|
13
13
|
}
|
|
@@ -17,8 +17,9 @@ export function isGitRepo(mindRoot: string): boolean {
|
|
|
17
17
|
*/
|
|
18
18
|
export function gitLog(mindRoot: string, filePath: string, limit: number): GitLogEntry[] {
|
|
19
19
|
const resolved = resolveSafe(mindRoot, filePath);
|
|
20
|
-
const output =
|
|
21
|
-
|
|
20
|
+
const output = execFileSync(
|
|
21
|
+
'git',
|
|
22
|
+
['log', '--follow', '--format=%H%x00%aI%x00%s%x00%an', '-n', String(limit), '--', resolved],
|
|
22
23
|
{ cwd: mindRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
23
24
|
).trim();
|
|
24
25
|
if (!output) return [];
|
|
@@ -33,18 +34,21 @@ export function gitLog(mindRoot: string, filePath: string, limit: number): GitLo
|
|
|
33
34
|
*/
|
|
34
35
|
export function gitShowFile(mindRoot: string, filePath: string, commitHash: string): string {
|
|
35
36
|
const resolved = resolveSafe(mindRoot, filePath);
|
|
36
|
-
const relFromGitRoot =
|
|
37
|
-
|
|
37
|
+
const relFromGitRoot = execFileSync(
|
|
38
|
+
'git',
|
|
39
|
+
['ls-files', '--full-name', resolved],
|
|
38
40
|
{ cwd: mindRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
39
41
|
).trim();
|
|
40
42
|
if (!relFromGitRoot) {
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
+
return execFileSync(
|
|
44
|
+
'git',
|
|
45
|
+
['show', `${commitHash}:${filePath}`],
|
|
43
46
|
{ cwd: mindRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
44
47
|
);
|
|
45
48
|
}
|
|
46
|
-
return
|
|
47
|
-
|
|
49
|
+
return execFileSync(
|
|
50
|
+
'git',
|
|
51
|
+
['show', `${commitHash}:${relFromGitRoot}`],
|
|
48
52
|
{ cwd: mindRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
49
53
|
);
|
|
50
54
|
}
|
package/app/lib/fs.ts
CHANGED
|
@@ -585,6 +585,7 @@ export type { MindSpaceSummary } from './core';
|
|
|
585
585
|
export type { ContentChangeEvent, ContentChangeInput, ContentChangeSummary, ContentChangeSource } from './core';
|
|
586
586
|
|
|
587
587
|
export function findBacklinks(targetPath: string): BacklinkEntry[] {
|
|
588
|
-
|
|
588
|
+
const { allFiles } = ensureCache();
|
|
589
|
+
return coreFindBacklinks(getMindRoot(), targetPath, allFiles);
|
|
589
590
|
}
|
|
590
591
|
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -72,12 +72,15 @@ export const en = {
|
|
|
72
72
|
agents: 'Agents',
|
|
73
73
|
echo: 'Echo',
|
|
74
74
|
discover: 'Discover',
|
|
75
|
+
history: 'History',
|
|
75
76
|
help: 'Help',
|
|
76
77
|
syncLabel: 'Sync',
|
|
77
78
|
collapseTitle: 'Collapse sidebar',
|
|
78
79
|
expandTitle: 'Expand sidebar',
|
|
79
80
|
collapseLevel: 'Collapse one level',
|
|
80
81
|
expandLevel: 'Expand one level',
|
|
82
|
+
importFile: 'Import file',
|
|
83
|
+
newFile: 'New file',
|
|
81
84
|
sync: {
|
|
82
85
|
synced: 'Synced',
|
|
83
86
|
unpushed: 'awaiting push',
|
|
@@ -105,6 +108,7 @@ export const en = {
|
|
|
105
108
|
},
|
|
106
109
|
ask: {
|
|
107
110
|
title: 'MindOS Agent',
|
|
111
|
+
fabLabel: 'Ask AI',
|
|
108
112
|
placeholder: 'Ask a question... @ files, / skills',
|
|
109
113
|
emptyPrompt: 'Ask anything about your knowledge base',
|
|
110
114
|
send: 'send',
|
|
@@ -118,13 +122,17 @@ export const en = {
|
|
|
118
122
|
skillsHint: 'skills',
|
|
119
123
|
attachCurrent: 'attach current file',
|
|
120
124
|
stopTitle: 'Stop',
|
|
121
|
-
|
|
125
|
+
cancelReconnect: 'Cancel reconnect',
|
|
126
|
+
connecting: 'Thinking with you...',
|
|
122
127
|
thinking: 'Thinking...',
|
|
123
128
|
thinkingLabel: 'Thinking',
|
|
124
129
|
searching: 'Searching knowledge base...',
|
|
125
130
|
generating: 'Generating response...',
|
|
126
131
|
stopped: 'Generation stopped.',
|
|
127
132
|
errorNoResponse: 'No response from AI. Please check your API key and provider settings.',
|
|
133
|
+
reconnecting: (attempt: number, max: number) => `Connection lost. Reconnecting (${attempt}/${max})...`,
|
|
134
|
+
reconnectFailed: 'Connection failed after multiple attempts.',
|
|
135
|
+
retry: 'Retry',
|
|
128
136
|
suggestions: [
|
|
129
137
|
'Summarize this document',
|
|
130
138
|
'List all action items and TODOs',
|
|
@@ -637,6 +645,8 @@ export const en = {
|
|
|
637
645
|
},
|
|
638
646
|
detailSubtitle: '',
|
|
639
647
|
detailNotFound: 'Agent not found — it may have been removed or renamed.',
|
|
648
|
+
detailNotFoundHint: 'The agent may have disconnected or its configuration file was moved. Try restarting the agent or check the MCP configuration.',
|
|
649
|
+
detailNotFoundSuggestion: 'Connected agents you can explore:',
|
|
640
650
|
},
|
|
641
651
|
shortcutPanel: {
|
|
642
652
|
title: 'Keyboard Shortcuts',
|
|
@@ -758,8 +768,17 @@ export const en = {
|
|
|
758
768
|
organizeRetry: 'Retry',
|
|
759
769
|
organizeDone: 'Done',
|
|
760
770
|
organizeUndoAll: 'Undo All',
|
|
771
|
+
organizeUndoOne: 'Undo',
|
|
772
|
+
organizeUndone: 'Undone',
|
|
773
|
+
organizeViewFile: 'View file',
|
|
761
774
|
organizeUndoSuccess: (n: number) => `Reverted ${n} file${n !== 1 ? 's' : ''}`,
|
|
762
775
|
},
|
|
776
|
+
importHistory: {
|
|
777
|
+
title: 'Import History',
|
|
778
|
+
clearAll: 'Clear history',
|
|
779
|
+
emptyTitle: 'No import history yet',
|
|
780
|
+
emptyDesc: 'AI organize results will appear here',
|
|
781
|
+
},
|
|
763
782
|
dirView: {
|
|
764
783
|
gridView: 'Grid view',
|
|
765
784
|
listView: 'List view',
|
|
@@ -778,7 +797,7 @@ export const en = {
|
|
|
778
797
|
},
|
|
779
798
|
settings: {
|
|
780
799
|
title: 'Settings',
|
|
781
|
-
tabs: { ai: 'AI', appearance: 'Appearance', knowledge: 'General', sync: 'Sync', mcp: 'MCP & Skills', plugins: 'Plugins', shortcuts: 'Shortcuts', monitoring: 'Monitoring', agents: 'Agents', update: 'Update' },
|
|
800
|
+
tabs: { ai: 'AI', appearance: 'Appearance', knowledge: 'General', sync: 'Sync', mcp: 'MCP & Skills', plugins: 'Plugins', shortcuts: 'Shortcuts', monitoring: 'Monitoring', agents: 'Agents', update: 'Update', uninstall: 'Uninstall' },
|
|
782
801
|
ai: {
|
|
783
802
|
provider: 'Provider',
|
|
784
803
|
model: 'Model',
|
|
@@ -813,6 +832,8 @@ export const en = {
|
|
|
813
832
|
thinkingHint: "Show Claude's reasoning process (uses more tokens)",
|
|
814
833
|
thinkingBudget: 'Thinking Budget',
|
|
815
834
|
thinkingBudgetHint: 'Max tokens for reasoning (1000-50000)',
|
|
835
|
+
reconnectRetries: 'Auto Reconnect',
|
|
836
|
+
reconnectRetriesHint: 'When connection drops, automatically retry this many times before giving up (0 = disabled)',
|
|
816
837
|
},
|
|
817
838
|
appearance: {
|
|
818
839
|
readingFont: 'Reading font',
|
|
@@ -1037,6 +1058,29 @@ export const en = {
|
|
|
1037
1058
|
desktopRestart: 'Restart Now',
|
|
1038
1059
|
desktopHint: 'Updates are delivered through the Desktop app auto-updater.',
|
|
1039
1060
|
},
|
|
1061
|
+
uninstall: {
|
|
1062
|
+
title: 'Uninstall MindOS',
|
|
1063
|
+
descCli: 'Remove MindOS CLI, background services, and configuration files from this machine.',
|
|
1064
|
+
descDesktop: 'Remove MindOS Desktop, background services, and configuration files from this machine.',
|
|
1065
|
+
warning: 'Select what to clean up. Your knowledge base files are always kept safe.',
|
|
1066
|
+
stopServices: 'Stop services & remove daemon',
|
|
1067
|
+
stopServicesDesc: 'Stop all running MindOS processes and remove the background daemon.',
|
|
1068
|
+
removeConfig: 'Remove configuration',
|
|
1069
|
+
removeConfigDesc: 'Delete ~/.mindos/ directory (config, logs, PID files).',
|
|
1070
|
+
removeNpm: 'Uninstall CLI package',
|
|
1071
|
+
removeNpmDesc: 'Run npm uninstall -g @geminilight/mindos.',
|
|
1072
|
+
removeApp: 'Move Desktop app to Trash',
|
|
1073
|
+
removeAppDesc: 'Move MindOS.app to Trash. You can restore it later if needed.',
|
|
1074
|
+
confirmTitle: 'Confirm Uninstall',
|
|
1075
|
+
confirmButton: 'Uninstall',
|
|
1076
|
+
cancelButton: 'Cancel',
|
|
1077
|
+
running: 'Uninstalling...',
|
|
1078
|
+
success: 'MindOS has been uninstalled.',
|
|
1079
|
+
successDesktop: 'MindOS has been uninstalled. The app will quit now.',
|
|
1080
|
+
error: 'Uninstall failed. You can run `mindos uninstall` in terminal manually.',
|
|
1081
|
+
nothingSelected: 'Select at least one item to uninstall.',
|
|
1082
|
+
kbSafe: 'Your knowledge base files are always safe — they are never deleted by this action.',
|
|
1083
|
+
},
|
|
1040
1084
|
},
|
|
1041
1085
|
onboarding: {
|
|
1042
1086
|
subtitle: 'Your knowledge base is empty. Pick a starter template to get going.',
|
package/app/lib/i18n-zh.ts
CHANGED
|
@@ -97,12 +97,15 @@ export const zh = {
|
|
|
97
97
|
agents: '智能体',
|
|
98
98
|
echo: '回响',
|
|
99
99
|
discover: '探索',
|
|
100
|
+
history: '历史',
|
|
100
101
|
help: '帮助',
|
|
101
102
|
syncLabel: '同步',
|
|
102
103
|
collapseTitle: '收起侧栏',
|
|
103
104
|
expandTitle: '展开侧栏',
|
|
104
105
|
collapseLevel: '折叠一级',
|
|
105
106
|
expandLevel: '展开一级',
|
|
107
|
+
importFile: '导入文件',
|
|
108
|
+
newFile: '新建文件',
|
|
106
109
|
sync: {
|
|
107
110
|
synced: '已同步',
|
|
108
111
|
unpushed: '待推送',
|
|
@@ -130,6 +133,7 @@ export const zh = {
|
|
|
130
133
|
},
|
|
131
134
|
ask: {
|
|
132
135
|
title: 'MindOS Agent',
|
|
136
|
+
fabLabel: 'AI 助手',
|
|
133
137
|
placeholder: '输入问题… @ 附加文件,/ 技能',
|
|
134
138
|
emptyPrompt: '可以问任何关于知识库的问题',
|
|
135
139
|
send: '发送',
|
|
@@ -143,13 +147,17 @@ export const zh = {
|
|
|
143
147
|
skillsHint: '技能',
|
|
144
148
|
attachCurrent: '附加当前文件',
|
|
145
149
|
stopTitle: '停止',
|
|
146
|
-
|
|
150
|
+
cancelReconnect: '取消重连',
|
|
151
|
+
connecting: '正在和你一起思考...',
|
|
147
152
|
thinking: '思考中...',
|
|
148
153
|
thinkingLabel: '思考中',
|
|
149
154
|
searching: '正在搜索知识库...',
|
|
150
155
|
generating: '正在生成回复...',
|
|
151
156
|
stopped: '已停止生成。',
|
|
152
157
|
errorNoResponse: 'AI 未返回响应,请检查 API Key 和服务商设置。',
|
|
158
|
+
reconnecting: (attempt: number, max: number) => `连接中断,正在重连 (${attempt}/${max})...`,
|
|
159
|
+
reconnectFailed: '多次重连失败,请检查网络后重试。',
|
|
160
|
+
retry: '重试',
|
|
153
161
|
suggestions: [
|
|
154
162
|
'总结这篇文档',
|
|
155
163
|
'列出所有待办事项',
|
|
@@ -661,6 +669,8 @@ export const zh = {
|
|
|
661
669
|
},
|
|
662
670
|
detailSubtitle: '',
|
|
663
671
|
detailNotFound: '未找到该 Agent,可能已移除或重命名。',
|
|
672
|
+
detailNotFoundHint: '该 Agent 可能已断开连接或配置文件已移动。请尝试重启 Agent 或检查 MCP 配置。',
|
|
673
|
+
detailNotFoundSuggestion: '已连接的 Agent:',
|
|
664
674
|
},
|
|
665
675
|
shortcutPanel: {
|
|
666
676
|
title: '快捷键',
|
|
@@ -782,8 +792,17 @@ export const zh = {
|
|
|
782
792
|
organizeRetry: '重试',
|
|
783
793
|
organizeDone: '完成',
|
|
784
794
|
organizeUndoAll: '撤销全部',
|
|
795
|
+
organizeUndoOne: '撤销',
|
|
796
|
+
organizeUndone: '已撤销',
|
|
797
|
+
organizeViewFile: '查看文件',
|
|
785
798
|
organizeUndoSuccess: (n: number) => `已撤销 ${n} 个文件`,
|
|
786
799
|
},
|
|
800
|
+
importHistory: {
|
|
801
|
+
title: '导入历史',
|
|
802
|
+
clearAll: '清空历史',
|
|
803
|
+
emptyTitle: '暂无导入记录',
|
|
804
|
+
emptyDesc: 'AI 整理的结果会出现在这里',
|
|
805
|
+
},
|
|
787
806
|
dirView: {
|
|
788
807
|
gridView: '网格视图',
|
|
789
808
|
listView: '列表视图',
|
|
@@ -802,7 +821,7 @@ export const zh = {
|
|
|
802
821
|
},
|
|
803
822
|
settings: {
|
|
804
823
|
title: '设置',
|
|
805
|
-
tabs: { ai: 'AI', appearance: '外观', knowledge: '通用', sync: '同步', mcp: 'MCP & Skills', plugins: '插件', shortcuts: '快捷键', monitoring: '监控', agents: 'Agents', update: '更新' },
|
|
824
|
+
tabs: { ai: 'AI', appearance: '外观', knowledge: '通用', sync: '同步', mcp: 'MCP & Skills', plugins: '插件', shortcuts: '快捷键', monitoring: '监控', agents: 'Agents', update: '更新', uninstall: '卸载' },
|
|
806
825
|
ai: {
|
|
807
826
|
provider: '服务商',
|
|
808
827
|
model: '模型',
|
|
@@ -837,6 +856,8 @@ export const zh = {
|
|
|
837
856
|
thinkingHint: '显示 Claude 的推理过程(消耗更多 token)',
|
|
838
857
|
thinkingBudget: '思考预算',
|
|
839
858
|
thinkingBudgetHint: '推理最大 token 数(1000-50000)',
|
|
859
|
+
reconnectRetries: '自动重连',
|
|
860
|
+
reconnectRetriesHint: '连接断开时自动重试次数,重试耗尽后停止(0 = 关闭)',
|
|
840
861
|
},
|
|
841
862
|
appearance: {
|
|
842
863
|
readingFont: '正文字体',
|
|
@@ -1061,6 +1082,29 @@ export const zh = {
|
|
|
1061
1082
|
desktopRestart: '立即重启',
|
|
1062
1083
|
desktopHint: '更新通过桌面端自动更新推送。',
|
|
1063
1084
|
},
|
|
1085
|
+
uninstall: {
|
|
1086
|
+
title: '卸载 MindOS',
|
|
1087
|
+
descCli: '从本机移除 MindOS CLI、后台服务和配置文件。',
|
|
1088
|
+
descDesktop: '从本机移除 MindOS Desktop、后台服务和配置文件。',
|
|
1089
|
+
warning: '选择要清理的内容。你的知识库文件始终是安全的。',
|
|
1090
|
+
stopServices: '停止服务并移除守护进程',
|
|
1091
|
+
stopServicesDesc: '停止所有运行中的 MindOS 进程并移除后台守护进程。',
|
|
1092
|
+
removeConfig: '移除配置',
|
|
1093
|
+
removeConfigDesc: '删除 ~/.mindos/ 目录(配置、日志、PID 文件)。',
|
|
1094
|
+
removeNpm: '卸载 CLI 包',
|
|
1095
|
+
removeNpmDesc: '执行 npm uninstall -g @geminilight/mindos。',
|
|
1096
|
+
removeApp: '将 Desktop 移入废纸篓',
|
|
1097
|
+
removeAppDesc: '将 MindOS.app 移入废纸篓,之后可以恢复。',
|
|
1098
|
+
confirmTitle: '确认卸载',
|
|
1099
|
+
confirmButton: '卸载',
|
|
1100
|
+
cancelButton: '取消',
|
|
1101
|
+
running: '正在卸载...',
|
|
1102
|
+
success: 'MindOS 已卸载。',
|
|
1103
|
+
successDesktop: 'MindOS 已卸载,应用即将退出。',
|
|
1104
|
+
error: '卸载失败,可在终端手动运行 `mindos uninstall`。',
|
|
1105
|
+
nothingSelected: '请至少选择一项要卸载的内容。',
|
|
1106
|
+
kbSafe: '你的知识库文件始终是安全的——此操作绝不会删除它们。',
|
|
1107
|
+
},
|
|
1064
1108
|
},
|
|
1065
1109
|
onboarding: {
|
|
1066
1110
|
subtitle: '知识库为空,选择一个模板快速开始。',
|