@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
@@ -0,0 +1,386 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import {
6
+ Check, X, Loader2, Sparkles, AlertCircle, Undo2,
7
+ ChevronDown, FilePlus, FileEdit, ExternalLink,
8
+ } from 'lucide-react';
9
+ import { useLocale } from '@/lib/LocaleContext';
10
+ import type { useAiOrganize } from '@/hooks/useAiOrganize';
11
+ import type { OrganizeStageHint } from '@/hooks/useAiOrganize';
12
+ import { encodePath } from '@/lib/utils';
13
+ import {
14
+ appendEntry, updateEntry, generateEntryId,
15
+ type OrganizeHistoryEntry,
16
+ } from '@/lib/organize-history';
17
+
18
+ const AUTO_DISMISS_MS = 3 * 60 * 1000; // 3 minutes
19
+ const THINKING_TIMEOUT_MS = 5000;
20
+
21
+ type AiOrganize = ReturnType<typeof useAiOrganize>;
22
+
23
+ function stageText(
24
+ t: ReturnType<typeof useLocale>['t'],
25
+ hint: { stage: OrganizeStageHint; detail?: string } | null,
26
+ ): string {
27
+ const fi = t.fileImport as Record<string, unknown>;
28
+ if (!hint) return fi.organizeProcessing as string;
29
+ switch (hint.stage) {
30
+ case 'connecting': return fi.organizeConnecting as string;
31
+ case 'analyzing': return fi.organizeAnalyzing as string;
32
+ case 'reading': return (fi.organizeReading as (d?: string) => string)(hint.detail);
33
+ case 'thinking': return fi.organizeThinking as string;
34
+ case 'writing': return (fi.organizeWriting as (d?: string) => string)(hint.detail);
35
+ default: return fi.organizeProcessing as string;
36
+ }
37
+ }
38
+
39
+ /** Self-contained timer for organizing phase */
40
+ function useOrganizeTimer(isOrganizing: boolean, stageHint: AiOrganize['stageHint']) {
41
+ const [elapsed, setElapsed] = useState(0);
42
+ const [thinkingOverride, setThinkingOverride] = useState(false);
43
+ const lastEventRef = useRef(Date.now());
44
+
45
+ useEffect(() => {
46
+ lastEventRef.current = Date.now();
47
+ setThinkingOverride(false);
48
+ }, [stageHint]);
49
+
50
+ useEffect(() => {
51
+ if (!isOrganizing) { setElapsed(0); setThinkingOverride(false); return; }
52
+ const timer = setInterval(() => {
53
+ setElapsed(e => e + 1);
54
+ if (Date.now() - lastEventRef.current >= THINKING_TIMEOUT_MS) {
55
+ setThinkingOverride(true);
56
+ }
57
+ }, 1000);
58
+ return () => clearInterval(timer);
59
+ }, [isOrganizing]);
60
+
61
+ return {
62
+ elapsed,
63
+ displayHint: thinkingOverride ? { stage: 'thinking' as const } : stageHint,
64
+ };
65
+ }
66
+
67
+ interface OrganizeToastProps {
68
+ aiOrganize: AiOrganize;
69
+ onDismiss: () => void;
70
+ /** Called when AI organize flow should be cancelled entirely */
71
+ onCancel: () => void;
72
+ /** Callback to notify history panel of updates */
73
+ onHistoryUpdate?: () => void;
74
+ }
75
+
76
+ export default function OrganizeToast({
77
+ aiOrganize, onDismiss, onCancel, onHistoryUpdate,
78
+ }: OrganizeToastProps) {
79
+ const { t } = useLocale();
80
+ const router = useRouter();
81
+ const fi = t.fileImport as Record<string, unknown>;
82
+
83
+ const isOrganizing = aiOrganize.phase === 'organizing';
84
+ const { elapsed, displayHint } = useOrganizeTimer(isOrganizing, aiOrganize.stageHint);
85
+
86
+ const [expanded, setExpanded] = useState(false);
87
+ const [undoing, setUndoing] = useState(false);
88
+ const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
89
+ const historyIdRef = useRef<string | null>(null);
90
+
91
+ const isDone = aiOrganize.phase === 'done';
92
+ const isError = aiOrganize.phase === 'error';
93
+ const isActive = isOrganizing || isDone || isError;
94
+
95
+ const okCount = aiOrganize.changes.filter(c => c.ok && !c.undone).length;
96
+
97
+ // Reset historyId when a new organize session starts
98
+ useEffect(() => {
99
+ if (isOrganizing) {
100
+ historyIdRef.current = null;
101
+ }
102
+ }, [isOrganizing]);
103
+
104
+ // Write to history when organize completes
105
+ useEffect(() => {
106
+ if (isDone && !historyIdRef.current) {
107
+ const id = generateEntryId();
108
+ historyIdRef.current = id;
109
+ appendEntry({
110
+ id,
111
+ timestamp: Date.now(),
112
+ sourceFiles: aiOrganize.sourceFileNames,
113
+ files: aiOrganize.changes.map(c => ({
114
+ action: c.action,
115
+ path: c.path,
116
+ ok: c.ok,
117
+ undone: c.undone,
118
+ })),
119
+ status: 'completed',
120
+ });
121
+ onHistoryUpdate?.();
122
+ }
123
+ }, [isDone, aiOrganize.changes, aiOrganize.sourceFileNames, onHistoryUpdate]);
124
+
125
+ // Auto-dismiss timer (3 min after done, reset on user interaction)
126
+ const resetTimer = useCallback(() => {
127
+ if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
128
+ if (isDone || isError) {
129
+ dismissTimerRef.current = setTimeout(() => {
130
+ onDismiss();
131
+ }, AUTO_DISMISS_MS);
132
+ }
133
+ }, [isDone, isError, onDismiss]);
134
+
135
+ useEffect(() => {
136
+ resetTimer();
137
+ return () => { if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current); };
138
+ }, [resetTimer]);
139
+
140
+ const handleUserAction = useCallback(() => {
141
+ resetTimer();
142
+ }, [resetTimer]);
143
+
144
+ const handleUndoOne = useCallback(async (path: string) => {
145
+ handleUserAction();
146
+ setUndoing(true);
147
+ const ok = await aiOrganize.undoOne(path);
148
+ setUndoing(false);
149
+ if (ok) {
150
+ window.dispatchEvent(new Event('mindos:files-changed'));
151
+ // History sync deferred to next render when aiOrganize.changes is updated
152
+ setTimeout(() => {
153
+ if (historyIdRef.current) {
154
+ const updated = aiOrganize.changes.map(c => ({
155
+ action: c.action, path: c.path, ok: c.ok,
156
+ undone: c.path === path ? true : c.undone,
157
+ }));
158
+ const allUndone = updated.every(c => !c.ok || c.undone);
159
+ updateEntry(historyIdRef.current, {
160
+ files: updated,
161
+ status: allUndone ? 'undone' : 'partial',
162
+ });
163
+ onHistoryUpdate?.();
164
+ }
165
+ }, 0);
166
+ }
167
+ }, [aiOrganize, handleUserAction, onHistoryUpdate]);
168
+
169
+ const handleUndoAll = useCallback(async () => {
170
+ handleUserAction();
171
+ setUndoing(true);
172
+ const reverted = await aiOrganize.undoAll();
173
+ setUndoing(false);
174
+ if (reverted > 0) {
175
+ window.dispatchEvent(new Event('mindos:files-changed'));
176
+ if (historyIdRef.current) {
177
+ updateEntry(historyIdRef.current, {
178
+ files: aiOrganize.changes.map(c => ({
179
+ action: c.action, path: c.path, ok: c.ok,
180
+ undone: c.ok ? true : c.undone,
181
+ })),
182
+ status: 'undone',
183
+ });
184
+ onHistoryUpdate?.();
185
+ }
186
+ }
187
+ }, [aiOrganize, handleUserAction, onHistoryUpdate]);
188
+
189
+ const handleViewFile = useCallback((path: string) => {
190
+ handleUserAction();
191
+ router.push(`/view/${encodePath(path)}`);
192
+ }, [router, handleUserAction]);
193
+
194
+ const handleDismiss = useCallback(() => {
195
+ onDismiss();
196
+ }, [onDismiss]);
197
+
198
+ if (!isActive) return null;
199
+
200
+ // Expanded panel (file list with per-file undo)
201
+ if (expanded && (isDone || isError)) {
202
+ return (
203
+ <div
204
+ className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 w-[420px] max-w-[calc(100vw-2rem)] bg-card border border-border rounded-xl shadow-xl animate-in fade-in-0 slide-in-from-bottom-2 duration-200"
205
+ onClick={handleUserAction}
206
+ >
207
+ {/* Header */}
208
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border">
209
+ <div className="flex items-center gap-2">
210
+ {isDone ? <Check size={14} className="text-success" /> : <AlertCircle size={14} className="text-error" />}
211
+ <span className="text-xs font-medium text-foreground">
212
+ {isDone ? fi.organizeReviewTitle as string : fi.organizeErrorTitle as string}
213
+ </span>
214
+ </div>
215
+ <button
216
+ type="button"
217
+ onClick={() => setExpanded(false)}
218
+ className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
219
+ >
220
+ <ChevronDown size={14} />
221
+ </button>
222
+ </div>
223
+
224
+ {/* File list */}
225
+ {isDone && (
226
+ <div className="max-h-[240px] overflow-y-auto p-2 space-y-0.5">
227
+ {aiOrganize.changes.map((c, idx) => {
228
+ const wasUndone = c.undone;
229
+ const undoable = aiOrganize.canUndo(c.path);
230
+ const fileName = c.path.split('/').pop() ?? c.path;
231
+
232
+ return (
233
+ <div
234
+ key={`${c.path}-${idx}`}
235
+ className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors ${wasUndone ? 'bg-muted/30 opacity-50' : 'bg-muted/50'}`}
236
+ >
237
+ {wasUndone ? (
238
+ <Undo2 size={14} className="text-muted-foreground shrink-0" />
239
+ ) : c.action === 'create' ? (
240
+ <FilePlus size={14} className="text-success shrink-0" />
241
+ ) : (
242
+ <FileEdit size={14} className="text-[var(--amber)] shrink-0" />
243
+ )}
244
+ <span className={`truncate flex-1 ${wasUndone ? 'line-through text-muted-foreground' : 'text-foreground'}`}>
245
+ {fileName}
246
+ </span>
247
+ {wasUndone ? (
248
+ <span className="text-xs text-muted-foreground shrink-0">{fi.organizeUndone as string}</span>
249
+ ) : (
250
+ <span className={`text-xs shrink-0 ${c.ok ? 'text-muted-foreground' : 'text-error'}`}>
251
+ {!c.ok ? fi.organizeFailed as string
252
+ : c.action === 'create' ? fi.organizeCreated as string
253
+ : fi.organizeUpdated as string}
254
+ </span>
255
+ )}
256
+ {undoable && (
257
+ <button
258
+ type="button"
259
+ onClick={() => handleUndoOne(c.path)}
260
+ disabled={undoing}
261
+ className="text-2xs text-muted-foreground/60 hover:text-foreground transition-colors shrink-0 px-1 disabled:opacity-40"
262
+ title={fi.organizeUndoOne as string}
263
+ >
264
+ <Undo2 size={12} />
265
+ </button>
266
+ )}
267
+ {c.ok && !c.undone && (
268
+ <button
269
+ type="button"
270
+ onClick={() => handleViewFile(c.path)}
271
+ className="text-2xs text-muted-foreground/60 hover:text-[var(--amber)] transition-colors shrink-0 px-1"
272
+ title={fi.organizeViewFile as string}
273
+ >
274
+ <ExternalLink size={12} />
275
+ </button>
276
+ )}
277
+ </div>
278
+ );
279
+ })}
280
+ </div>
281
+ )}
282
+
283
+ {isError && (
284
+ <div className="p-4 text-center">
285
+ <p className="text-xs text-muted-foreground">{aiOrganize.error}</p>
286
+ </div>
287
+ )}
288
+
289
+ {/* Actions */}
290
+ <div className="flex items-center justify-end gap-3 px-4 py-3 border-t border-border">
291
+ {isDone && aiOrganize.hasAnyUndoable && (
292
+ <button
293
+ onClick={handleUndoAll}
294
+ disabled={undoing}
295
+ className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1.5 disabled:opacity-50"
296
+ >
297
+ {undoing ? <Loader2 size={12} className="animate-spin" /> : <Undo2 size={12} />}
298
+ {fi.organizeUndoAll as string}
299
+ </button>
300
+ )}
301
+ <button
302
+ onClick={handleDismiss}
303
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90 transition-all"
304
+ >
305
+ <Check size={12} />
306
+ {fi.organizeDone as string}
307
+ </button>
308
+ </div>
309
+ </div>
310
+ );
311
+ }
312
+
313
+ // Compact toast bar
314
+ return (
315
+ <div
316
+ className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 bg-card border border-border rounded-xl shadow-lg px-4 py-3 max-w-md animate-in fade-in-0 slide-in-from-bottom-2 duration-200"
317
+ onClick={handleUserAction}
318
+ >
319
+ {isDone ? (
320
+ <>
321
+ <Check size={16} className="text-success shrink-0" />
322
+ <span className="text-xs text-foreground truncate">
323
+ {(fi.organizeReviewDesc as (n: number) => string)(okCount)}
324
+ </span>
325
+ {aiOrganize.changes.length > 0 && (
326
+ <button
327
+ type="button"
328
+ onClick={() => { setExpanded(true); handleUserAction(); }}
329
+ className="flex items-center gap-1 text-xs font-medium text-[var(--amber)] hover:opacity-80 transition-colors shrink-0"
330
+ >
331
+ <ChevronDown size={12} className="rotate-180" />
332
+ {fi.organizeExpand as string}
333
+ </button>
334
+ )}
335
+ <button
336
+ type="button"
337
+ onClick={handleDismiss}
338
+ className="flex items-center gap-1 text-xs font-medium text-[var(--amber)] hover:opacity-80 transition-colors shrink-0"
339
+ >
340
+ <Check size={12} />
341
+ {fi.organizeDone as string}
342
+ </button>
343
+ </>
344
+ ) : isError ? (
345
+ <>
346
+ <AlertCircle size={16} className="text-error shrink-0" />
347
+ <span className="text-xs text-foreground truncate">{fi.organizeError as string}</span>
348
+ <button
349
+ type="button"
350
+ onClick={() => { setExpanded(true); handleUserAction(); }}
351
+ className="text-xs font-medium text-[var(--amber)] hover:opacity-80 transition-colors shrink-0"
352
+ >
353
+ {fi.organizeExpand as string}
354
+ </button>
355
+ <button
356
+ type="button"
357
+ onClick={handleDismiss}
358
+ className="text-muted-foreground/50 hover:text-muted-foreground transition-colors shrink-0"
359
+ >
360
+ <X size={14} />
361
+ </button>
362
+ </>
363
+ ) : (
364
+ <>
365
+ <div className="relative shrink-0">
366
+ <Sparkles size={16} className="text-[var(--amber)]" />
367
+ <Loader2 size={10} className="absolute -bottom-0.5 -right-0.5 text-[var(--amber)] animate-spin" />
368
+ </div>
369
+ <span className="text-xs text-foreground truncate">
370
+ {stageText(t, displayHint)}
371
+ </span>
372
+ <span className="text-xs text-muted-foreground/60 tabular-nums shrink-0">
373
+ {(fi.organizeElapsed as (s: number) => string)(elapsed)}
374
+ </span>
375
+ <button
376
+ type="button"
377
+ onClick={handleDismiss}
378
+ className="text-muted-foreground/50 hover:text-muted-foreground transition-colors shrink-0"
379
+ >
380
+ <X size={14} />
381
+ </button>
382
+ </>
383
+ )}
384
+ </div>
385
+ );
386
+ }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useMemo, useState } from 'react';
4
- import { ChevronsDownUp, ChevronsUpDown } from 'lucide-react';
4
+ import { ChevronsDownUp, ChevronsUpDown, FilePlus, Import } from 'lucide-react';
5
5
  import type { PanelId } from './ActivityBar';
6
6
  import type { FileNode } from '@/lib/types';
7
7
  import FileTree from './FileTree';
@@ -29,6 +29,7 @@ const DEFAULT_PANEL_WIDTH: Record<PanelId, number> = {
29
29
  echo: 280,
30
30
  agents: 280,
31
31
  discover: 280,
32
+ history: 280,
32
33
  };
33
34
 
34
35
  const MIN_PANEL_WIDTH = 240;
@@ -52,7 +53,7 @@ interface PanelProps {
52
53
  /** Callback to toggle maximize */
53
54
  onMaximize?: () => void;
54
55
  /** Callback to open import modal for a space */
55
- onImport?: (space: string) => void;
56
+ onImport?: (space?: string) => void;
56
57
  /** Lazy-loaded panel content for search/ask/plugins */
57
58
  children?: React.ReactNode;
58
59
  }
@@ -108,11 +109,30 @@ export default function Panel({
108
109
  <div className={`flex flex-col h-full ${activePanel === 'files' ? '' : 'hidden'}`}>
109
110
  <PanelHeader title={t.sidebar.files}>
110
111
  <div className="flex items-center gap-0.5">
112
+ <button
113
+ type="button"
114
+ onClick={() => onImport?.()}
115
+ className="p-1 rounded text-[var(--amber)] hover:bg-muted hover:text-[var(--amber)] transition-colors focus-visible:ring-1 focus-visible:ring-ring"
116
+ aria-label={t.sidebar.importFile}
117
+ title={t.sidebar.importFile}
118
+ >
119
+ <Import size={13} />
120
+ </button>
121
+ <button
122
+ type="button"
123
+ onClick={() => window.location.assign('/view/Untitled.md')}
124
+ className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors focus-visible:ring-1 focus-visible:ring-ring"
125
+ aria-label={t.sidebar.newFile}
126
+ title={t.sidebar.newFile}
127
+ >
128
+ <FilePlus size={13} />
129
+ </button>
111
130
  <button
112
131
  onClick={() => setMaxOpenDepth(prev => {
113
132
  const current = prev ?? treeMaxDepth;
114
133
  return Math.max(-1, current - 1);
115
134
  })}
135
+ onDoubleClick={() => setMaxOpenDepth(-1)}
116
136
  className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
117
137
  aria-label={t.sidebar.collapseLevel}
118
138
  title={t.sidebar.collapseLevel}
@@ -128,6 +148,7 @@ export default function Panel({
128
148
  }
129
149
  return next;
130
150
  })}
151
+ onDoubleClick={() => setMaxOpenDepth(null)}
131
152
  className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
132
153
  aria-label={t.sidebar.expandLevel}
133
154
  title={t.sidebar.expandLevel}
@@ -167,7 +167,7 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
167
167
  </div>
168
168
  </header>
169
169
 
170
- {mobileOpen && <div className="md:hidden fixed inset-0 z-40 bg-black/60 backdrop-blur-sm" onClick={() => setMobileOpen(false)} />}
170
+ {mobileOpen && <div className="md:hidden fixed inset-0 z-40 overlay-backdrop" onClick={() => setMobileOpen(false)} />}
171
171
 
172
172
  <aside className={`md:hidden fixed top-0 left-0 h-screen w-[85vw] max-w-[320px] z-50 bg-card border-r border-border flex flex-col transition-transform duration-300 ease-in-out ${mobileOpen ? 'translate-x-0' : '-translate-x-full'}`}>
173
173
  {sidebarContent}
@@ -13,6 +13,7 @@ import SearchPanel from './panels/SearchPanel';
13
13
  import AgentsPanel from './panels/AgentsPanel';
14
14
  import DiscoverPanel from './panels/DiscoverPanel';
15
15
  import EchoPanel from './panels/EchoPanel';
16
+ import ImportHistoryPanel from './panels/ImportHistoryPanel';
16
17
  import RightAskPanel from './RightAskPanel';
17
18
  import RightAgentDetailPanel, {
18
19
  RIGHT_AGENT_DETAIL_DEFAULT_WIDTH,
@@ -27,6 +28,7 @@ import SettingsModal from './SettingsModal';
27
28
  import KeyboardShortcuts from './KeyboardShortcuts';
28
29
  import ChangesBanner from './changes/ChangesBanner';
29
30
  import SpaceInitToast from './SpaceInitToast';
31
+ import OrganizeToast from './OrganizeToast';
30
32
  import { MobileSyncDot, useSyncStatus } from './SyncStatusBar';
31
33
  import { FileNode } from '@/lib/types';
32
34
  import { useLocale } from '@/lib/LocaleContext';
@@ -38,6 +40,7 @@ import McpProvider from '@/hooks/useMcpData';
38
40
  import '@/lib/renderers/index'; // client-side renderer registration source of truth
39
41
  import { useLeftPanel } from '@/hooks/useLeftPanel';
40
42
  import { useAskPanel } from '@/hooks/useAskPanel';
43
+ import { useAiOrganize } from '@/hooks/useAiOrganize';
41
44
  import type { Tab } from './settings/types';
42
45
 
43
46
  interface SidebarLayoutProps {
@@ -74,6 +77,33 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
74
77
  return RIGHT_AGENT_DETAIL_DEFAULT_WIDTH;
75
78
  });
76
79
 
80
+ // ── AI Organize (lifted from ImportModal so toast shares state) ──
81
+ const aiOrganize = useAiOrganize();
82
+ const [organizeToastVisible, setOrganizeToastVisible] = useState(false);
83
+ const [historyRefreshToken, setHistoryRefreshToken] = useState(0);
84
+
85
+ // Show toast whenever organize is active
86
+ useEffect(() => {
87
+ if (aiOrganize.phase === 'organizing' || aiOrganize.phase === 'done' || aiOrganize.phase === 'error') {
88
+ setOrganizeToastVisible(true);
89
+ }
90
+ }, [aiOrganize.phase]);
91
+
92
+ const handleOrganizeToastDismiss = useCallback(() => {
93
+ setOrganizeToastVisible(false);
94
+ if (aiOrganize.phase !== 'organizing') {
95
+ aiOrganize.reset();
96
+ } else {
97
+ aiOrganize.abort();
98
+ aiOrganize.reset();
99
+ }
100
+ }, [aiOrganize]);
101
+
102
+ const handleHistoryUpdate = useCallback(() => {
103
+ setHistoryRefreshToken(t => t + 1);
104
+ window.dispatchEvent(new Event('mindos:organize-history-update'));
105
+ }, []);
106
+
77
107
  // ── Import modal state ──
78
108
  const [importModalOpen, setImportModalOpen] = useState(false);
79
109
  const [importDefaultSpace, setImportDefaultSpace] = useState<string | undefined>(undefined);
@@ -345,6 +375,9 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
345
375
  <div className={`flex flex-col h-full ${lp.activePanel === 'discover' ? '' : 'hidden'}`}>
346
376
  <DiscoverPanel active={lp.activePanel === 'discover'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
347
377
  </div>
378
+ <div className={`flex flex-col h-full ${lp.activePanel === 'history' ? '' : 'hidden'}`}>
379
+ <ImportHistoryPanel active={lp.activePanel === 'history'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} refreshToken={historyRefreshToken} />
380
+ </div>
348
381
  </Panel>
349
382
 
350
383
  {/* ── Right-side Ask AI Panel ── */}
@@ -418,7 +451,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
418
451
  </div>
419
452
  </header>
420
453
 
421
- {mobileOpen && <div className="md:hidden fixed inset-0 z-40 bg-black/60 backdrop-blur-sm" onClick={() => setMobileOpen(false)} />}
454
+ {mobileOpen && <div className="md:hidden fixed inset-0 z-40 overlay-backdrop" onClick={() => setMobileOpen(false)} />}
422
455
  <aside className={`md:hidden fixed top-0 left-0 h-screen w-[85vw] max-w-[320px] z-50 bg-card border-r border-border flex flex-col transition-transform duration-300 ease-in-out ${mobileOpen ? 'translate-x-0' : '-translate-x-full'}`}>
423
456
  <div className="flex items-center justify-between px-4 py-4 border-b border-border shrink-0">
424
457
  <Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
@@ -489,8 +522,18 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
489
522
  onClose={handleCloseImport}
490
523
  defaultSpace={importDefaultSpace}
491
524
  initialFiles={importInitialFiles}
525
+ aiOrganize={aiOrganize}
492
526
  />
493
527
 
528
+ {organizeToastVisible && (
529
+ <OrganizeToast
530
+ aiOrganize={aiOrganize}
531
+ onDismiss={handleOrganizeToastDismiss}
532
+ onCancel={() => { aiOrganize.abort(); aiOrganize.reset(); setOrganizeToastVisible(false); }}
533
+ onHistoryUpdate={handleHistoryUpdate}
534
+ />
535
+ )}
536
+
494
537
  <style>{`
495
538
  @media (min-width: 768px) {
496
539
  :root {
@@ -192,14 +192,35 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
192
192
  }, [a.detail.mcpServerHint]);
193
193
 
194
194
  if (!agent) {
195
+ const connectedAgents = mcp.agents
196
+ .filter((ag) => ag.key !== agentKey && resolveAgentStatus(ag) === 'connected')
197
+ .slice(0, 3);
198
+
195
199
  return (
196
200
  <div className="content-width px-4 md:px-6 py-8 md:py-10">
197
201
  <Link href="/agents" className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground mb-4">
198
202
  <ArrowLeft size={14} />
199
203
  {a.backToOverview}
200
204
  </Link>
201
- <div className="rounded-lg border border-border bg-card p-6">
202
- <p className="text-sm text-foreground">{a.detailNotFound}</p>
205
+ <div className="rounded-lg border border-border bg-card p-6 space-y-4">
206
+ <p className="text-sm text-foreground font-medium">{a.detailNotFound}</p>
207
+ <p className="text-xs text-muted-foreground">{a.detailNotFoundHint}</p>
208
+ {connectedAgents.length > 0 && (
209
+ <div className="pt-2 border-t border-border">
210
+ <p className="text-xs text-muted-foreground mb-2">{a.detailNotFoundSuggestion}</p>
211
+ <div className="flex flex-wrap gap-2">
212
+ {connectedAgents.map((ag) => (
213
+ <Link
214
+ key={ag.key}
215
+ href={`/agents/${encodeURIComponent(ag.key)}`}
216
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-muted text-foreground hover:bg-muted/80 transition-colors"
217
+ >
218
+ {ag.name}
219
+ </Link>
220
+ ))}
221
+ </div>
222
+ </div>
223
+ )}
203
224
  </div>
204
225
  </div>
205
226
  );
@@ -219,11 +240,11 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
219
240
  <div className="flex items-center gap-3.5 px-5 py-4">
220
241
  <AgentAvatar name={agent.name} status={status} size="md" />
221
242
  <div className="min-w-0 flex-1">
222
- <h1 className="text-lg font-semibold tracking-tight font-display text-foreground">{agent.name}</h1>
243
+ <h1 className="text-lg font-semibold tracking-tight font-display text-foreground truncate">{agent.name}</h1>
223
244
  <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 mt-0.5 text-2xs text-muted-foreground/60">
224
245
  <span className={`font-medium px-1.5 py-px rounded-full ${
225
246
  status === 'connected' ? 'bg-success/10 text-success'
226
- : status === 'detected' ? 'bg-[var(--amber-subtle)] text-[var(--amber)]'
247
+ : status === 'detected' ? 'bg-[var(--amber-subtle)] text-[var(--amber-text)]'
227
248
  : 'bg-muted text-muted-foreground'
228
249
  }`}>{status}</span>
229
250
  <span className="font-mono">{agent.transport ?? agent.preferredTransport}</span>
@@ -240,12 +261,12 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
240
261
 
241
262
  {/* ═══════════ MCP MANAGEMENT ═══════════ */}
242
263
  <section className="rounded-xl border border-border bg-card p-4 space-y-3">
243
- <div className="flex items-center justify-between">
244
- <h2 className="text-xs font-semibold text-foreground flex items-center gap-2">
264
+ <div className="flex flex-wrap items-center justify-between gap-2">
265
+ <h2 className="text-xs font-semibold text-foreground flex items-center gap-2 shrink-0">
245
266
  <Server size={12} className="text-muted-foreground/50" />
246
267
  {a.detail.mcpManagement}
247
268
  </h2>
248
- <div className="flex items-center gap-1.5">
269
+ <div className="flex flex-wrap items-center gap-1.5">
249
270
  {!isMindOS && (
250
271
  <ActionButton
251
272
  onClick={() => void handleCopySnippet()}
@@ -339,8 +360,8 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
339
360
 
340
361
  {/* ═══════════ SKILL ASSIGNMENTS ═══════════ */}
341
362
  <section className="rounded-xl border border-border bg-card p-4 space-y-3">
342
- <div className="flex items-center justify-between">
343
- <h2 className="text-xs font-semibold text-foreground flex items-center gap-2">
363
+ <div className="flex flex-wrap items-center justify-between gap-2">
364
+ <h2 className="text-xs font-semibold text-foreground flex items-center gap-2 shrink-0">
344
365
  <Zap size={12} className="text-muted-foreground/50" />
345
366
  {a.detail.skillAssignments}
346
367
  </h2>
@@ -395,7 +416,7 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
395
416
  >
396
417
  {skill.name}
397
418
  </button>
398
- <span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${skill.source === 'builtin' ? 'bg-muted text-muted-foreground' : 'bg-[var(--amber-dim)] text-[var(--amber)]'}`}>
419
+ <span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${skill.source === 'builtin' ? 'bg-muted text-muted-foreground' : 'bg-[var(--amber-dim)] text-[var(--amber-text)]'}`}>
399
420
  {skill.source === 'builtin' ? a.detail.skillsSourceBuiltin : a.detail.skillsSourceUser}
400
421
  </span>
401
422
 
@@ -520,9 +541,9 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
520
541
 
521
542
  function DetailLine({ label, value }: { label: string; value: string }) {
522
543
  return (
523
- <div className="flex items-baseline gap-2 px-0.5">
544
+ <div className="flex items-baseline gap-2 px-0.5 min-w-0">
524
545
  <span className="text-2xs text-muted-foreground/50 uppercase tracking-wider shrink-0 min-w-[60px]">{label}</span>
525
- <span className="text-xs text-foreground/80 font-mono truncate">{value}</span>
546
+ <span className="text-xs text-foreground/80 font-mono truncate min-w-0">{value}</span>
526
547
  </div>
527
548
  );
528
549
  }
@@ -306,7 +306,7 @@ function ByAgentView({
306
306
  {agent.name}
307
307
  </Link>
308
308
  <span className="text-2xs text-muted-foreground font-mono shrink-0">{agent.transport ?? agent.preferredTransport}</span>
309
- <span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${status === 'connected' ? 'bg-success/10 text-success' : status === 'detected' ? 'bg-[var(--amber-dim)] text-[var(--amber)]' : 'bg-muted text-muted-foreground'}`}>
309
+ <span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${status === 'connected' ? 'bg-success/10 text-success' : status === 'detected' ? 'bg-[var(--amber-dim)] text-[var(--amber-text)]' : 'bg-muted text-muted-foreground'}`}>
310
310
  {copy.status[status]}
311
311
  </span>
312
312
  </div>