@geminilight/mindos 0.6.8 → 0.6.13

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.
Files changed (79) hide show
  1. package/README.md +2 -0
  2. package/README_zh.md +2 -0
  3. package/app/app/api/mcp/install/route.ts +4 -1
  4. package/app/app/api/setup/check-path/route.ts +2 -7
  5. package/app/app/api/setup/ls/route.ts +3 -9
  6. package/app/app/api/setup/path-utils.ts +8 -0
  7. package/app/app/api/setup/route.ts +2 -7
  8. package/app/app/api/uninstall/route.ts +47 -0
  9. package/app/app/globals.css +11 -0
  10. package/app/components/ActivityBar.tsx +10 -3
  11. package/app/components/AskFab.tsx +7 -3
  12. package/app/components/CreateSpaceModal.tsx +1 -1
  13. package/app/components/DirView.tsx +1 -1
  14. package/app/components/FileTree.tsx +30 -23
  15. package/app/components/GuideCard.tsx +1 -1
  16. package/app/components/HomeContent.tsx +137 -109
  17. package/app/components/ImportModal.tsx +16 -477
  18. package/app/components/MarkdownView.tsx +3 -0
  19. package/app/components/OnboardingView.tsx +1 -1
  20. package/app/components/OrganizeToast.tsx +386 -0
  21. package/app/components/Panel.tsx +23 -2
  22. package/app/components/Sidebar.tsx +1 -1
  23. package/app/components/SidebarLayout.tsx +44 -1
  24. package/app/components/agents/AgentDetailContent.tsx +33 -12
  25. package/app/components/agents/AgentsMcpSection.tsx +1 -1
  26. package/app/components/agents/AgentsOverviewSection.tsx +3 -4
  27. package/app/components/agents/AgentsPrimitives.tsx +2 -2
  28. package/app/components/agents/AgentsSkillsSection.tsx +2 -2
  29. package/app/components/agents/SkillDetailPopover.tsx +24 -8
  30. package/app/components/ask/AskContent.tsx +124 -75
  31. package/app/components/ask/HighlightMatch.tsx +14 -0
  32. package/app/components/ask/MentionPopover.tsx +5 -3
  33. package/app/components/ask/MessageList.tsx +39 -11
  34. package/app/components/ask/SlashCommandPopover.tsx +4 -2
  35. package/app/components/changes/ChangesBanner.tsx +20 -2
  36. package/app/components/changes/ChangesContentPage.tsx +10 -2
  37. package/app/components/echo/EchoHero.tsx +1 -1
  38. package/app/components/echo/EchoInsightCollapsible.tsx +1 -1
  39. package/app/components/echo/EchoPageSections.tsx +1 -1
  40. package/app/components/explore/UseCaseCard.tsx +1 -1
  41. package/app/components/panels/DiscoverPanel.tsx +29 -25
  42. package/app/components/panels/ImportHistoryPanel.tsx +195 -0
  43. package/app/components/panels/PluginsPanel.tsx +2 -2
  44. package/app/components/settings/AiTab.tsx +24 -0
  45. package/app/components/settings/KnowledgeTab.tsx +1 -1
  46. package/app/components/settings/McpSkillCreateForm.tsx +1 -1
  47. package/app/components/settings/McpSkillRow.tsx +1 -1
  48. package/app/components/settings/McpSkillsSection.tsx +2 -2
  49. package/app/components/settings/McpTab.tsx +2 -2
  50. package/app/components/settings/PluginsTab.tsx +1 -1
  51. package/app/components/settings/Primitives.tsx +118 -6
  52. package/app/components/settings/SettingsContent.tsx +5 -2
  53. package/app/components/settings/UninstallTab.tsx +179 -0
  54. package/app/components/settings/UpdateTab.tsx +17 -5
  55. package/app/components/settings/types.ts +2 -1
  56. package/app/components/setup/StepDots.tsx +2 -2
  57. package/app/components/ui/dialog.tsx +1 -1
  58. package/app/hooks/useAiOrganize.ts +122 -10
  59. package/app/hooks/useMention.ts +21 -3
  60. package/app/hooks/useSlashCommand.ts +18 -4
  61. package/app/lib/agent/reconnect.ts +40 -0
  62. package/app/lib/core/backlinks.ts +2 -2
  63. package/app/lib/core/git.ts +14 -10
  64. package/app/lib/fs.ts +2 -1
  65. package/app/lib/i18n-en.ts +46 -2
  66. package/app/lib/i18n-zh.ts +46 -2
  67. package/app/lib/organize-history.ts +74 -0
  68. package/app/lib/settings.ts +2 -0
  69. package/app/lib/types.ts +2 -0
  70. package/app/next.config.ts +23 -5
  71. package/bin/cli.js +6 -9
  72. package/bin/lib/mcp-build.js +74 -0
  73. package/bin/lib/mcp-spawn.js +8 -5
  74. package/bin/lib/port.js +17 -2
  75. package/bin/lib/stop.js +12 -2
  76. package/mcp/dist/index.cjs +43 -43
  77. package/mcp/src/index.ts +58 -12
  78. package/package.json +1 -1
  79. 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
- * Handles both complete `<thinking>...</thinking>` blocks and unclosed trailing tags.
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]*?<\/thinking>/gi, '');
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
- const undoAll = useCallback(async (): Promise<number> => {
299
- const createdFiles = changes.filter(c => c.action === 'create' && c.ok);
300
- let reverted = 0;
301
- for (const file of createdFiles) {
302
- try {
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: file.path }),
347
+ body: JSON.stringify({ op: 'delete_file', path: target.path }),
307
348
  });
308
- if (res.ok) reverted++;
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
  }
@@ -47,15 +47,33 @@ export function useMention() {
47
47
  return;
48
48
  }
49
49
  const q = query.toLowerCase();
50
- const filtered = allFiles.filter((f) => f.toLowerCase().includes(q)).slice(0, 30);
51
- if (filtered.length === 0) {
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(filtered);
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[] = 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 }));
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, '\\$&');
@@ -1,4 +1,4 @@
1
- import { execSync } from 'child_process';
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
- execSync('git rev-parse --is-inside-work-tree', { cwd: mindRoot, stdio: 'pipe' });
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 = execSync(
21
- `git log --follow --format="%H%x00%aI%x00%s%x00%an" -n ${limit} -- "${resolved}"`,
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 = execSync(
37
- `git ls-files --full-name "${resolved}"`,
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 execSync(
42
- `git show ${commitHash}:"${filePath}"`,
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 execSync(
47
- `git show ${commitHash}:"${relFromGitRoot}"`,
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
- return coreFindBacklinks(getMindRoot(), targetPath);
588
+ const { allFiles } = ensureCache();
589
+ return coreFindBacklinks(getMindRoot(), targetPath, allFiles);
589
590
  }
590
591
 
@@ -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
- connecting: 'Thinking with your mind...',
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.',
@@ -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
- connecting: '正在与你的心智一起思考...',
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: '知识库为空,选择一个模板快速开始。',