@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
@@ -3,14 +3,11 @@
3
3
  import { useEffect, useRef, useState, useCallback } from 'react';
4
4
  import {
5
5
  X, FolderInput, FolderOpen, Sparkles, FileText, AlertCircle,
6
- AlertTriangle, Loader2, Check, FilePlus, FileEdit, Undo2, ChevronDown,
6
+ AlertTriangle, Loader2, Check, ChevronDown,
7
7
  } from 'lucide-react';
8
- import ReactMarkdown from 'react-markdown';
9
- import remarkGfm from 'remark-gfm';
10
8
  import { useLocale } from '@/lib/LocaleContext';
11
9
  import { useFileImport, type ImportIntent, type ConflictMode } from '@/hooks/useFileImport';
12
- import { useAiOrganize, stripThinkingTags } from '@/hooks/useAiOrganize';
13
- import type { OrganizeStageHint } from '@/hooks/useAiOrganize';
10
+ import type { useAiOrganize } from '@/hooks/useAiOrganize';
14
11
  import { ALLOWED_IMPORT_EXTENSIONS } from '@/lib/core/file-convert';
15
12
  import type { LocalAttachment } from '@/lib/types';
16
13
 
@@ -19,279 +16,23 @@ interface ImportModalProps {
19
16
  onClose: () => void;
20
17
  defaultSpace?: string;
21
18
  initialFiles?: File[];
22
- }
23
-
24
- const ACCEPT = Array.from(ALLOWED_IMPORT_EXTENSIONS).join(',');
25
-
26
- const THINKING_TIMEOUT_MS = 5000;
27
-
28
- function stageHintText(
29
- t: { fileImport: Record<string, unknown> },
30
- hint: { stage: OrganizeStageHint; detail?: string } | null,
31
- ): string {
32
- const fi = t.fileImport as {
33
- organizeConnecting: string;
34
- organizeAnalyzing: string;
35
- organizeReading: (d?: string) => string;
36
- organizeThinking: string;
37
- organizeWriting: (d?: string) => string;
38
- organizeProcessing: string;
39
- };
40
- if (!hint) return fi.organizeProcessing;
41
- switch (hint.stage) {
42
- case 'connecting': return fi.organizeConnecting;
43
- case 'analyzing': return fi.organizeAnalyzing;
44
- case 'reading': return fi.organizeReading(hint.detail);
45
- case 'thinking': return fi.organizeThinking;
46
- case 'writing': return fi.organizeWriting(hint.detail);
47
- default: return fi.organizeProcessing;
48
- }
49
- }
50
-
51
- /**
52
- * Hook: elapsed timer + thinking-override for the organizing phase.
53
- * Lifted to ImportModal level so both full modal and minimized bar share the same state.
54
- */
55
- function useOrganizeTimer(isOrganizing: boolean, stageHint: ReturnType<typeof useAiOrganize>['stageHint']) {
56
- const [elapsed, setElapsed] = useState(0);
57
- const [thinkingOverride, setThinkingOverride] = useState(false);
58
- const lastEventRef = useRef(Date.now());
59
-
60
- useEffect(() => {
61
- lastEventRef.current = Date.now();
62
- setThinkingOverride(false);
63
- }, [stageHint]);
64
-
65
- useEffect(() => {
66
- if (!isOrganizing) { setElapsed(0); setThinkingOverride(false); return; }
67
- const timer = setInterval(() => {
68
- setElapsed(e => e + 1);
69
- if (Date.now() - lastEventRef.current >= THINKING_TIMEOUT_MS) {
70
- setThinkingOverride(true);
71
- }
72
- }, 1000);
73
- return () => clearInterval(timer);
74
- }, [isOrganizing]);
75
-
76
- const displayHint = thinkingOverride
77
- ? { stage: 'thinking' as const }
78
- : stageHint;
79
-
80
- return { elapsed, displayHint };
81
- }
82
-
83
- /**
84
- * Full-size organizing progress view (shown inside the modal).
85
- */
86
- function OrganizingProgress({
87
- aiOrganize,
88
- t,
89
- elapsed,
90
- displayHint,
91
- onMinimize,
92
- onCancel,
93
- }: {
19
+ /** Lifted AI organize hook from SidebarLayout (shared with OrganizeToast) */
94
20
  aiOrganize: ReturnType<typeof useAiOrganize>;
95
- t: ReturnType<typeof useLocale>['t'];
96
- elapsed: number;
97
- displayHint: { stage: OrganizeStageHint; detail?: string } | null;
98
- onMinimize: () => void;
99
- onCancel: () => void;
100
- }) {
101
- const fi = t.fileImport as { organizeElapsed: (s: number) => string };
102
- const summaryPreview = aiOrganize.summary ? stripThinkingTags(aiOrganize.summary).trim().slice(0, 200) : '';
103
-
104
- return (
105
- <div className="mt-4 space-y-3">
106
- {/* Status header */}
107
- <div className="flex items-center gap-3">
108
- <div className="relative shrink-0">
109
- <Sparkles size={20} className="text-[var(--amber)]" />
110
- <Loader2 size={12} className="absolute -bottom-0.5 -right-0.5 text-[var(--amber)] animate-spin" />
111
- </div>
112
- <div className="flex-1 min-w-0">
113
- <p className="text-sm text-foreground font-medium truncate">
114
- {stageHintText(t, displayHint)}
115
- </p>
116
- <span className="text-xs text-muted-foreground/60 tabular-nums">
117
- {fi.organizeElapsed(elapsed)}
118
- </span>
119
- </div>
120
- </div>
121
-
122
- {/* Live activity feed */}
123
- <div className="bg-muted/30 rounded-lg border border-border/50 overflow-hidden">
124
- <div className="max-h-[180px] overflow-y-auto p-3 space-y-2">
125
- {/* Streaming AI text */}
126
- {summaryPreview && (
127
- <p className="text-xs text-muted-foreground leading-relaxed whitespace-pre-wrap">
128
- {summaryPreview}
129
- {summaryPreview.length >= 200 ? '...' : ''}
130
- </p>
131
- )}
132
-
133
- {/* Current tool being executed */}
134
- {aiOrganize.currentTool && (
135
- <div className="flex items-center gap-2 text-xs text-[var(--amber)] animate-pulse">
136
- <Loader2 size={11} className="animate-spin shrink-0" />
137
- <span className="truncate">
138
- {aiOrganize.currentTool.name.startsWith('create')
139
- ? (t.fileImport as { organizeCreating: (p: string) => string }).organizeCreating(aiOrganize.currentTool.path)
140
- : (t.fileImport as { organizeUpdating: (p: string) => string }).organizeUpdating(aiOrganize.currentTool.path)}
141
- </span>
142
- </div>
143
- )}
144
-
145
- {/* Completed file operations */}
146
- {aiOrganize.changes.map((c, idx) => (
147
- <div key={`${c.path}-${idx}`} className="flex items-center gap-2 text-xs">
148
- {c.action === 'create' ? (
149
- <FilePlus size={12} className="text-success shrink-0" />
150
- ) : (
151
- <FileEdit size={12} className="text-[var(--amber)] shrink-0" />
152
- )}
153
- <span className="truncate text-foreground/80">{c.path}</span>
154
- <Check size={11} className="text-success shrink-0 ml-auto" />
155
- </div>
156
- ))}
157
-
158
- {/* Empty state — show pulsing dots */}
159
- {!summaryPreview && !aiOrganize.currentTool && aiOrganize.changes.length === 0 && (
160
- <div className="flex items-center justify-center gap-1 py-2">
161
- <span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)]/40 animate-pulse" />
162
- <span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)]/40 animate-pulse [animation-delay:150ms]" />
163
- <span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)]/40 animate-pulse [animation-delay:300ms]" />
164
- </div>
165
- )}
166
- </div>
167
- </div>
168
-
169
- {/* Actions */}
170
- <div className="flex items-center justify-center gap-4">
171
- <button
172
- type="button"
173
- onClick={onMinimize}
174
- className="text-xs text-muted-foreground/70 hover:text-foreground transition-colors px-3 py-1.5"
175
- >
176
- {(t.fileImport as { organizeMinimize: string }).organizeMinimize}
177
- </button>
178
- <button
179
- type="button"
180
- onClick={onCancel}
181
- className="text-xs text-muted-foreground/70 hover:text-muted-foreground transition-colors px-3 py-1.5"
182
- >
183
- {(t.fileImport as { organizeCancel: string }).organizeCancel}
184
- </button>
185
- </div>
186
- </div>
187
- );
188
21
  }
189
22
 
190
- const SUMMARY_PROSE = [
191
- 'prose prose-sm prose-panel dark:prose-invert max-w-none text-foreground',
192
- 'prose-p:my-1 prose-p:leading-relaxed',
193
- 'prose-headings:font-semibold prose-headings:my-2 prose-headings:text-[13px]',
194
- 'prose-ul:my-1 prose-li:my-0.5 prose-ol:my-1',
195
- 'prose-code:text-[0.8em] prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none',
196
- 'prose-pre:bg-muted prose-pre:text-foreground prose-pre:text-xs',
197
- 'prose-blockquote:border-l-amber-400 prose-blockquote:text-muted-foreground',
198
- 'prose-a:text-amber-500 prose-a:no-underline hover:prose-a:underline',
199
- 'prose-strong:text-foreground prose-strong:font-semibold',
200
- 'prose-table:text-xs prose-th:py-1 prose-td:py-1',
201
- ].join(' ');
202
-
203
- /**
204
- * Clean raw AI markdown for plain-text preview (progress view):
205
- * strip heading markers, excess blank lines, truncate.
206
- */
207
- function cleanSummaryForDisplay(raw: string): string {
208
- return stripThinkingTags(raw)
209
- .replace(/^#{1,4}\s+/gm, '')
210
- .replace(/\n{3,}/g, '\n\n')
211
- .trim()
212
- .slice(0, 500);
213
- }
214
-
215
- /**
216
- * Clean raw AI markdown for rendered display:
217
- * strip thinking tags & excess blank lines, keep markdown formatting.
218
- */
219
- function cleanSummaryForMarkdown(raw: string): string {
220
- return stripThinkingTags(raw)
221
- .replace(/\n{3,}/g, '\n\n')
222
- .trim();
223
- }
224
-
225
- /**
226
- * Organize result when no tracked file changes were detected.
227
- * Two sub-states:
228
- * 1. AI completed work + provided summary → show summary as primary content
229
- * 2. No summary → brief "up to date" message
230
- */
231
- function OrganizeNoChangesView({
232
- summary,
233
- toolCallCount,
234
- t,
235
- onDone,
236
- }: {
237
- summary: string;
238
- toolCallCount: number;
239
- t: ReturnType<typeof useLocale>['t'];
240
- onDone: () => void;
241
- }) {
242
- const fi = t.fileImport as Record<string, unknown>;
243
- const mdSummary = summary ? cleanSummaryForMarkdown(summary) : '';
244
- const hasSubstance = !!mdSummary;
23
+ const ACCEPT = Array.from(ALLOWED_IMPORT_EXTENSIONS).join(',');
245
24
 
246
- return (
247
- <div className="flex flex-col gap-3 py-4">
248
- {hasSubstance ? (
249
- <>
250
- <div className="flex items-start gap-2.5">
251
- <Sparkles size={16} className="text-[var(--amber)] mt-0.5 shrink-0" />
252
- <div className={SUMMARY_PROSE}>
253
- <ReactMarkdown remarkPlugins={[remarkGfm]}>{mdSummary}</ReactMarkdown>
254
- </div>
255
- </div>
256
- {toolCallCount > 0 && (
257
- <p className="text-xs text-muted-foreground/50 text-center">
258
- {(fi.organizeToolCallsInfo as ((n: number) => string) | undefined)?.(toolCallCount)}
259
- </p>
260
- )}
261
- </>
262
- ) : (
263
- <div className="flex flex-col items-center gap-2">
264
- <Sparkles size={24} className="text-muted-foreground" />
265
- <p className="text-sm text-muted-foreground">
266
- {fi.organizeNoChanges as string}
267
- </p>
268
- </div>
269
- )}
270
- <div className="flex justify-center pt-1">
271
- <button
272
- onClick={onDone}
273
- className="px-4 py-2 rounded-lg text-sm font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90 transition-all duration-200"
274
- >
275
- {fi.organizeDone as string}
276
- </button>
277
- </div>
278
- </div>
279
- );
280
- }
281
25
 
282
- export default function ImportModal({ open, onClose, defaultSpace, initialFiles }: ImportModalProps) {
26
+ export default function ImportModal({ open, onClose, defaultSpace, initialFiles, aiOrganize }: ImportModalProps) {
283
27
  const { t } = useLocale();
284
28
  const im = useFileImport();
285
- const aiOrganize = useAiOrganize();
286
29
  const overlayRef = useRef<HTMLDivElement>(null);
287
30
  const fileInputRef = useRef<HTMLInputElement>(null);
288
31
  const [spaces, setSpaces] = useState<Array<{ name: string; path: string }>>([]);
289
32
  const [closing, setClosing] = useState(false);
290
33
  const [showSuccess, setShowSuccess] = useState(false);
291
- const [undoing, setUndoing] = useState(false);
292
34
  const [conflictFiles, setConflictFiles] = useState<string[]>([]);
293
35
  const [showConflictOptions, setShowConflictOptions] = useState(false);
294
- const [minimized, setMinimized] = useState(false);
295
36
  const initializedRef = useRef(false);
296
37
 
297
38
  useEffect(() => {
@@ -302,11 +43,8 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
302
43
  if (initializedRef.current) return;
303
44
  initializedRef.current = true;
304
45
  im.reset();
305
- aiOrganize.reset();
306
- setUndoing(false);
307
46
  setConflictFiles([]);
308
47
  setShowConflictOptions(false);
309
- setMinimized(false);
310
48
  if (defaultSpace) im.setTargetSpace(defaultSpace);
311
49
  if (initialFiles && initialFiles.length > 0) {
312
50
  im.addFiles(initialFiles);
@@ -318,16 +56,12 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
318
56
  }, [open, defaultSpace, initialFiles, im]);
319
57
 
320
58
  const handleClose = useCallback(() => {
321
- if (im.step === 'organizing') {
322
- setMinimized(true);
323
- return;
324
- }
325
- if (im.files.length > 0 && im.step !== 'done' && im.step !== 'organize_review') {
59
+ if (im.files.length > 0 && im.step !== 'done') {
326
60
  if (!confirm(t.fileImport.discardMessage(im.files.length))) return;
327
61
  }
328
62
  setClosing(true);
329
- setTimeout(() => { setClosing(false); onClose(); im.reset(); aiOrganize.reset(); setUndoing(false); setConflictFiles([]); setShowConflictOptions(false); setMinimized(false); }, 150);
330
- }, [im, onClose, t, aiOrganize]);
63
+ setTimeout(() => { setClosing(false); onClose(); im.reset(); setConflictFiles([]); setShowConflictOptions(false); }, 150);
64
+ }, [im, onClose, t]);
331
65
 
332
66
  useEffect(() => {
333
67
  if (!open) return;
@@ -366,10 +100,12 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
366
100
  const prompt = attachments.length === 1
367
101
  ? (t.fileImport.digestPromptSingle as (name: string, space?: string) => string)(attachments[0].name, space)
368
102
  : (t.fileImport.digestPromptMulti as (n: number, space?: string) => string)(attachments.length, space);
369
- im.setStep('organizing');
103
+ // Start AI organize and immediately close modal — toast takes over
370
104
  aiOrganize.start(attachments, prompt);
105
+ onClose();
106
+ im.reset();
371
107
  }
372
- }, [im, t, aiOrganize]);
108
+ }, [im, t, aiOrganize, onClose]);
373
109
 
374
110
  const handleArchiveSubmit = useCallback(async () => {
375
111
  await im.doArchive();
@@ -396,53 +132,6 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
396
132
  // eslint-disable-next-line react-hooks/exhaustive-deps
397
133
  }, [im.targetSpace]);
398
134
 
399
- useEffect(() => {
400
- if (im.step === 'organizing' && (aiOrganize.phase === 'done' || aiOrganize.phase === 'error')) {
401
- im.setStep('organize_review');
402
- }
403
- }, [im.step, aiOrganize.phase, im]);
404
-
405
- const handleOrganizeDone = useCallback(() => {
406
- setClosing(true);
407
- setTimeout(() => {
408
- setClosing(false);
409
- onClose();
410
- im.reset();
411
- aiOrganize.reset();
412
- setUndoing(false);
413
- window.dispatchEvent(new Event('mindos:files-changed'));
414
- }, 150);
415
- }, [onClose, im, aiOrganize]);
416
-
417
- const handleOrganizeUndo = useCallback(async () => {
418
- setUndoing(true);
419
- const reverted = await aiOrganize.undoAll();
420
- setUndoing(false);
421
- setClosing(true);
422
- setTimeout(() => {
423
- setClosing(false);
424
- onClose();
425
- im.reset();
426
- aiOrganize.reset();
427
- if (reverted > 0) {
428
- window.dispatchEvent(new Event('mindos:files-changed'));
429
- }
430
- }, 150);
431
- }, [onClose, im, aiOrganize]);
432
-
433
- const handleOrganizeRetry = useCallback(() => {
434
- const attachments: LocalAttachment[] = im.validFiles.map(f => ({
435
- name: f.name,
436
- content: f.content!,
437
- }));
438
- const space = im.targetSpace || undefined;
439
- const prompt = attachments.length === 1
440
- ? (t.fileImport.digestPromptSingle as (name: string, space?: string) => string)(attachments[0].name, space)
441
- : (t.fileImport.digestPromptMulti as (n: number, space?: string) => string)(attachments.length, space);
442
- aiOrganize.reset();
443
- im.setStep('organizing');
444
- aiOrganize.start(attachments, prompt);
445
- }, [im, t, aiOrganize]);
446
135
 
447
136
  useEffect(() => {
448
137
  if (im.step === 'done' && im.result) {
@@ -467,61 +156,14 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
467
156
  const isSelectStep = im.step === 'select';
468
157
  const isArchiveConfig = im.step === 'archive_config';
469
158
  const isImporting = im.step === 'importing';
470
- const isOrganizing = im.step === 'organizing';
471
- const isOrganizeReview = im.step === 'organize_review';
472
-
473
- const { elapsed, displayHint } = useOrganizeTimer(isOrganizing, aiOrganize.stageHint);
474
-
475
- useEffect(() => {
476
- if (minimized && im.step === 'organize_review') {
477
- setMinimized(false);
478
- }
479
- }, [minimized, im.step]);
480
159
 
481
160
  if (!open && !closing) return null;
482
161
 
483
- const fi = t.fileImport as {
484
- organizeElapsed: (s: number) => string;
485
- organizeCancel: string;
486
- organizeExpand: string;
487
- };
488
-
489
- if (minimized && isOrganizing) {
490
- return (
491
- <div 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-sm">
492
- <div className="relative shrink-0">
493
- <Sparkles size={16} className="text-[var(--amber)]" />
494
- <Loader2 size={10} className="absolute -bottom-0.5 -right-0.5 text-[var(--amber)] animate-spin" />
495
- </div>
496
- <span className="text-xs text-foreground truncate">
497
- {stageHintText(t, displayHint)}
498
- </span>
499
- <span className="text-xs text-muted-foreground/60 tabular-nums shrink-0">
500
- {fi.organizeElapsed(elapsed)}
501
- </span>
502
- <button
503
- type="button"
504
- onClick={() => setMinimized(false)}
505
- className="text-xs font-medium text-[var(--amber)] hover:opacity-80 transition-colors shrink-0"
506
- >
507
- {fi.organizeExpand}
508
- </button>
509
- <button
510
- type="button"
511
- onClick={() => { aiOrganize.abort(); aiOrganize.reset(); im.setStep('select'); setMinimized(false); }}
512
- className="text-xs text-muted-foreground/50 hover:text-muted-foreground transition-colors shrink-0"
513
- >
514
- <X size={14} />
515
- </button>
516
- </div>
517
- );
518
- }
519
-
520
162
  return (
521
163
  <>
522
164
  <div
523
165
  ref={overlayRef}
524
- className={`fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 transition-opacity duration-200 ${closing ? 'opacity-0' : 'opacity-100'}`}
166
+ className={`fixed inset-0 z-50 modal-backdrop flex items-center justify-center p-4 transition-opacity duration-200 ${closing ? 'opacity-0' : 'opacity-100'}`}
525
167
  onClick={(e) => { if (e.target === overlayRef.current) handleClose(); }}
526
168
  >
527
169
  <div
@@ -542,20 +184,11 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
542
184
  </button>
543
185
  )}
544
186
  <h2 className="text-base font-semibold text-foreground">
545
- {isOrganizing ? t.fileImport.organizeTitle
546
- : isOrganizeReview
547
- ? (aiOrganize.phase === 'error' ? t.fileImport.organizeErrorTitle : t.fileImport.organizeReviewTitle)
548
- : isArchiveConfig ? t.fileImport.archiveConfigTitle
549
- : t.fileImport.title}
187
+ {isArchiveConfig ? t.fileImport.archiveConfigTitle : t.fileImport.title}
550
188
  </h2>
551
189
  {isSelectStep && (
552
190
  <p className="text-xs text-muted-foreground mt-0.5">{t.fileImport.subtitle}</p>
553
191
  )}
554
- {isOrganizeReview && aiOrganize.phase === 'done' && aiOrganize.changes.length > 0 && (
555
- <p className="text-xs text-muted-foreground mt-0.5">
556
- {t.fileImport.organizeReviewDesc(aiOrganize.changes.filter(c => c.ok).length)}
557
- </p>
558
- )}
559
192
  </div>
560
193
  <button
561
194
  onClick={handleClose}
@@ -619,7 +252,7 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
619
252
  />
620
253
 
621
254
  {/* File list */}
622
- {hasFiles && !isOrganizing && !isOrganizeReview && (
255
+ {hasFiles && (
623
256
  <div className="mt-3">
624
257
  <div className="flex items-center justify-between mb-1.5">
625
258
  <span className="text-xs text-muted-foreground">{t.fileImport.fileCount(im.files.length)}</span>
@@ -713,7 +346,7 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
713
346
  <button
714
347
  onClick={() => handleIntentSelect('digest')}
715
348
  className="flex flex-col items-center gap-2 p-4 border border-border rounded-lg cursor-pointer transition-all duration-150 bg-card hover:border-[var(--amber)]/50 hover:shadow-sm active:scale-[0.98] text-left"
716
- disabled={im.validFiles.length === 0}
349
+ disabled={im.validFiles.length === 0 || aiOrganize.phase === 'organizing'}
717
350
  >
718
351
  <Sparkles size={24} className="text-[var(--amber)]" />
719
352
  <span className="text-sm font-medium text-foreground">{t.fileImport.digestTitle}</span>
@@ -818,100 +451,6 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
818
451
  </div>
819
452
  )}
820
453
 
821
- {/* AI Organizing (progress) */}
822
- {isOrganizing && (
823
- <OrganizingProgress
824
- aiOrganize={aiOrganize}
825
- t={t}
826
- elapsed={elapsed}
827
- displayHint={displayHint}
828
- onMinimize={() => setMinimized(true)}
829
- onCancel={() => { aiOrganize.abort(); aiOrganize.reset(); im.setStep('select'); }}
830
- />
831
- )}
832
-
833
- {/* AI Organize review */}
834
- {isOrganizeReview && (
835
- <div className="mt-4 space-y-4">
836
- {aiOrganize.phase === 'error' ? (
837
- <div className="flex flex-col items-center gap-3 py-4">
838
- <AlertCircle size={28} className="text-error" />
839
- <p className="text-xs text-muted-foreground text-center max-w-[300px]">{aiOrganize.error}</p>
840
- <div className="flex gap-3 mt-2">
841
- <button
842
- onClick={handleClose}
843
- className="text-sm text-muted-foreground hover:text-foreground transition-colors px-3 py-2"
844
- >
845
- {t.fileImport.cancel}
846
- </button>
847
- <button
848
- onClick={handleOrganizeRetry}
849
- className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90 transition-all duration-200"
850
- >
851
- {t.fileImport.organizeRetry}
852
- </button>
853
- </div>
854
- </div>
855
- ) : aiOrganize.changes.length === 0 ? (
856
- <OrganizeNoChangesView
857
- summary={aiOrganize.summary}
858
- toolCallCount={aiOrganize.toolCallCount}
859
- t={t}
860
- onDone={handleOrganizeDone}
861
- />
862
- ) : (
863
- <>
864
- <div className="max-h-[200px] overflow-y-auto space-y-1">
865
- {aiOrganize.changes.map((c, idx) => (
866
- <div key={`${c.path}-${idx}`} className="flex items-center gap-2 px-3 py-2 bg-muted/50 rounded-md text-sm">
867
- {c.action === 'create' ? (
868
- <FilePlus size={14} className="text-success shrink-0" />
869
- ) : (
870
- <FileEdit size={14} className="text-[var(--amber)] shrink-0" />
871
- )}
872
- <span className="truncate flex-1 text-foreground">{c.path}</span>
873
- <span className={`text-xs shrink-0 ${c.ok ? 'text-muted-foreground' : 'text-error'}`}>
874
- {!c.ok ? t.fileImport.organizeFailed
875
- : c.action === 'create' ? t.fileImport.organizeCreated
876
- : t.fileImport.organizeUpdated}
877
- </span>
878
- </div>
879
- ))}
880
- </div>
881
- {aiOrganize.summary?.trim() && (
882
- <div className={SUMMARY_PROSE}>
883
- <ReactMarkdown remarkPlugins={[remarkGfm]}>
884
- {cleanSummaryForMarkdown(aiOrganize.summary)}
885
- </ReactMarkdown>
886
- </div>
887
- )}
888
- <div className="flex items-center justify-end gap-3 pt-2">
889
- {aiOrganize.changes.some(c => c.action === 'create' && c.ok) && (
890
- <button
891
- onClick={handleOrganizeUndo}
892
- disabled={undoing}
893
- className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors px-3 py-2 disabled:opacity-50"
894
- >
895
- {undoing ? (
896
- <Loader2 size={14} className="animate-spin" />
897
- ) : (
898
- <Undo2 size={14} />
899
- )}
900
- {t.fileImport.organizeUndoAll}
901
- </button>
902
- )}
903
- <button
904
- onClick={handleOrganizeDone}
905
- className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90 transition-all duration-200"
906
- >
907
- <Check size={14} />
908
- {t.fileImport.organizeDone}
909
- </button>
910
- </div>
911
- </>
912
- )}
913
- </div>
914
- )}
915
454
  </div>
916
455
  </div>
917
456
  </div>
@@ -77,6 +77,9 @@ const components: Components = {
77
77
  </div>
78
78
  );
79
79
  },
80
+ li({ children, ...props }) {
81
+ return <li {...props} suppressHydrationWarning>{children}</li>;
82
+ },
80
83
  a({ href, children, ...props }) {
81
84
  const isExternal = href?.startsWith('http');
82
85
  return (
@@ -137,7 +137,7 @@ export default function OnboardingView() {
137
137
  <button
138
138
  type="button"
139
139
  onClick={() => window.dispatchEvent(new CustomEvent('mindos:open-import'))}
140
- className="text-xs text-[var(--amber)] hover:underline transition-colors"
140
+ className="text-xs text-[var(--amber-text)] hover:underline transition-colors"
141
141
  >
142
142
  {t.fileImport.onboardingHint}
143
143
  </button>