@geminilight/mindos 0.6.14 → 0.6.16

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.
@@ -10,6 +10,7 @@ import {
10
10
  } from 'lucide-react';
11
11
  import { createFileAction, deleteFileAction, renameFileAction, renameSpaceAction, deleteSpaceAction, convertToSpaceAction, deleteFolderAction } from '@/lib/actions';
12
12
  import { useLocale } from '@/lib/LocaleContext';
13
+ import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
13
14
 
14
15
  function notifyFilesChanged() {
15
16
  window.dispatchEvent(new Event('mindos:files-changed'));
@@ -124,12 +125,11 @@ const MENU_DIVIDER = "my-1 border-t border-border/50";
124
125
 
125
126
  // ─── SpaceContextMenu ─────────────────────────────────────────────────────────
126
127
 
127
- function SpaceContextMenu({ x, y, node, onClose, onRename, onImport }: {
128
- x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void; onImport?: (space: string) => void;
128
+ function SpaceContextMenu({ x, y, node, onClose, onRename, onImport, onDelete }: {
129
+ x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void; onImport?: (space: string) => void; onDelete: () => void;
129
130
  }) {
130
131
  const router = useRouter();
131
132
  const { t } = useLocale();
132
- const [isPending, startTransition] = useTransition();
133
133
 
134
134
  return (
135
135
  <ContextMenuShell x={x} y={y} onClose={onClose}>
@@ -145,16 +145,9 @@ function SpaceContextMenu({ x, y, node, onClose, onRename, onImport }: {
145
145
  <Pencil size={14} className="shrink-0" /> {t.fileTree.renameSpace}
146
146
  </button>
147
147
  <div className={MENU_DIVIDER} />
148
- <button className={MENU_DANGER} disabled={isPending} onClick={() => {
149
- if (!confirm(t.fileTree.confirmDeleteSpace(node.name))) return;
150
- startTransition(async () => {
151
- const result = await deleteSpaceAction(node.path);
152
- if (result.success) { router.push('/'); router.refresh(); notifyFilesChanged(); }
153
- onClose();
154
- });
155
- }}>
148
+ <button className={MENU_DANGER} onClick={() => { onClose(); onDelete(); }}>
156
149
  <Trash2 size={14} className="shrink-0" />
157
- {isPending ? <Loader2 size={14} className="animate-spin" /> : t.fileTree.deleteSpace}
150
+ {t.fileTree.deleteSpace}
158
151
  </button>
159
152
  </ContextMenuShell>
160
153
  );
@@ -162,8 +155,8 @@ function SpaceContextMenu({ x, y, node, onClose, onRename, onImport }: {
162
155
 
163
156
  // ─── FolderContextMenu ────────────────────────────────────────────────────────
164
157
 
165
- function FolderContextMenu({ x, y, node, onClose, onRename }: {
166
- x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void;
158
+ function FolderContextMenu({ x, y, node, onClose, onRename, onDelete }: {
159
+ x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void; onDelete: () => void;
167
160
  }) {
168
161
  const router = useRouter();
169
162
  const { t } = useLocale();
@@ -184,16 +177,9 @@ function FolderContextMenu({ x, y, node, onClose, onRename }: {
184
177
  <Pencil size={14} className="shrink-0" /> {t.fileTree.rename}
185
178
  </button>
186
179
  <div className={MENU_DIVIDER} />
187
- <button className={MENU_DANGER} disabled={isPending} onClick={() => {
188
- if (!confirm(t.fileTree.confirmDeleteFolder(node.name))) return;
189
- startTransition(async () => {
190
- const result = await deleteFolderAction(node.path);
191
- if (result.success) { router.push('/'); router.refresh(); notifyFilesChanged(); }
192
- onClose();
193
- });
194
- }}>
180
+ <button className={MENU_DANGER} onClick={() => { onClose(); onDelete(); }}>
195
181
  <Trash2 size={14} className="shrink-0" />
196
- {isPending ? <Loader2 size={14} className="animate-spin" /> : t.fileTree.deleteFolder}
182
+ {t.fileTree.deleteFolder}
197
183
  </button>
198
184
  </ContextMenuShell>
199
185
  );
@@ -291,6 +277,8 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onI
291
277
  const [plusPopover, setPlusPopover] = useState(false);
292
278
  const plusRef = useRef<HTMLButtonElement>(null);
293
279
  const { t } = useLocale();
280
+ const [deleteConfirm, setDeleteConfirm] = useState<null | 'space' | 'folder'>(null);
281
+ const [isPendingDelete, startDeleteTransition] = useTransition();
294
282
 
295
283
  const toggle = useCallback(() => setOpen(v => !v), []);
296
284
 
@@ -506,6 +494,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onI
506
494
  onClose={() => setContextMenu(null)}
507
495
  onRename={() => startRename()}
508
496
  onImport={onImport}
497
+ onDelete={() => setDeleteConfirm('space')}
509
498
  />
510
499
  ) : (
511
500
  <FolderContextMenu
@@ -514,9 +503,30 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onI
514
503
  node={node}
515
504
  onClose={() => setContextMenu(null)}
516
505
  onRename={() => startRename()}
506
+ onDelete={() => setDeleteConfirm('folder')}
517
507
  />
518
508
  ))}
519
509
 
510
+ <ConfirmDialog
511
+ open={deleteConfirm !== null}
512
+ title={deleteConfirm === 'space' ? t.fileTree.deleteSpace : t.fileTree.deleteFolder}
513
+ message={deleteConfirm === 'space' ? t.fileTree.confirmDeleteSpace(node.name) : t.fileTree.confirmDeleteFolder(node.name)}
514
+ confirmLabel={deleteConfirm === 'space' ? t.fileTree.deleteSpace : t.fileTree.deleteFolder}
515
+ cancelLabel="Cancel"
516
+ variant="destructive"
517
+ onCancel={() => setDeleteConfirm(null)}
518
+ onConfirm={() => {
519
+ const kind = deleteConfirm;
520
+ setDeleteConfirm(null);
521
+ startDeleteTransition(async () => {
522
+ const result = kind === 'space'
523
+ ? await deleteSpaceAction(node.path)
524
+ : await deleteFolderAction(node.path);
525
+ if (result.success) { router.push('/'); router.refresh(); notifyFilesChanged(); }
526
+ });
527
+ }}
528
+ />
529
+
520
530
  {plusPopover && plusRef.current && (() => {
521
531
  const rect = plusRef.current!.getBoundingClientRect();
522
532
  return (
@@ -546,8 +556,10 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
546
556
  const [renaming, setRenaming] = useState(false);
547
557
  const [renameValue, setRenameValue] = useState(node.name);
548
558
  const [isPending, startTransition] = useTransition();
559
+ const [isPendingDelete, startDeleteTransition] = useTransition();
549
560
  const renameRef = useRef<HTMLInputElement>(null);
550
561
  const { t } = useLocale();
562
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
551
563
 
552
564
  const handleClick = useCallback(() => {
553
565
  if (renaming) return;
@@ -580,14 +592,8 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
580
592
 
581
593
  const handleDelete = useCallback((e: React.MouseEvent) => {
582
594
  e.stopPropagation();
583
- if (!confirm(t.fileTree.confirmDelete(node.name))) return;
584
- startTransition(async () => {
585
- await deleteFileAction(node.path);
586
- if (currentPath === node.path) router.push('/');
587
- router.refresh();
588
- notifyFilesChanged();
589
- });
590
- }, [node.name, node.path, currentPath, router, t]);
595
+ setShowDeleteConfirm(true);
596
+ }, []);
591
597
 
592
598
  const handleDragStart = useCallback((e: React.DragEvent) => {
593
599
  e.dataTransfer.setData('text/mindos-path', node.path);
@@ -640,9 +646,25 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
640
646
  <Pencil size={12} />
641
647
  </button>
642
648
  <button onClick={handleDelete} className="p-0.5 rounded text-muted-foreground hover:text-error hover:bg-muted transition-colors" title={t.fileTree.delete}>
643
- <Trash2 size={12} />
649
+ {isPendingDelete ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
644
650
  </button>
645
651
  </div>
652
+ <ConfirmDialog
653
+ open={showDeleteConfirm}
654
+ title={t.fileTree.delete}
655
+ message={t.fileTree.confirmDelete(node.name)}
656
+ confirmLabel={t.fileTree.delete}
657
+ cancelLabel="Cancel"
658
+ variant="destructive"
659
+ onCancel={() => setShowDeleteConfirm(false)}
660
+ onConfirm={() => {
661
+ setShowDeleteConfirm(false);
662
+ startDeleteTransition(async () => {
663
+ const result = await deleteFileAction(node.path);
664
+ if (result.success) { router.push('/'); router.refresh(); notifyFilesChanged(); }
665
+ });
666
+ }}
667
+ />
646
668
  </div>
647
669
  );
648
670
  }
@@ -1,14 +1,14 @@
1
1
  'use client';
2
2
 
3
3
  import Link from 'next/link';
4
- import { FileText, Table, Clock, Sparkles, ArrowRight, FilePlus, Search, ChevronDown, Compass, Folder, Puzzle, Brain, Plus, Trash2, Check, Loader2, X, FolderInput } from 'lucide-react';
4
+ import { FileText, Table, Clock, Sparkles, ArrowRight, FilePlus, Search, ChevronDown, Compass, Folder, Puzzle, Brain, Plus, Trash2, Check, Loader2, X, FolderInput, Zap, History, SlidersHorizontal, ListTodo } from 'lucide-react';
5
+ import type { LucideIcon } from 'lucide-react';
5
6
  import { useState, useEffect, useMemo, useCallback } from 'react';
6
7
  import { useLocale } from '@/lib/LocaleContext';
7
8
  import { encodePath, relativeTime, extractEmoji, stripEmoji } from '@/lib/utils';
8
9
  import { getAllRenderers, getPluginRenderers } from '@/lib/renderers/registry';
9
10
  import OnboardingView from './OnboardingView';
10
11
  import GuideCard from './GuideCard';
11
- import CreateSpaceModal from './CreateSpaceModal';
12
12
  import { scanExampleFilesAction, cleanupExamplesAction } from '@/lib/actions';
13
13
  import type { SpaceInfo } from '@/app/page';
14
14
 
@@ -138,7 +138,7 @@ function FileRow({ filePath, mtime, formatTime, subPath }: {
138
138
  );
139
139
  }
140
140
 
141
- /** Reusable chip for builtin features / plugins */
141
+ /** Compact chip for extension renderers */
142
142
  function FeatureChip({ id, icon, name, entryPath, active, inactiveTitle }: {
143
143
  id: string;
144
144
  icon: string;
@@ -164,11 +164,52 @@ function FeatureChip({ id, icon, name, entryPath, active, inactiveTitle }: {
164
164
  return <span key={id} className={cls} title={inactiveTitle}>{inner}</span>;
165
165
  }
166
166
 
167
+ const TOOL_ICONS: Record<string, LucideIcon> = {
168
+ 'agent-inspector': Search,
169
+ 'config-panel': SlidersHorizontal,
170
+ 'todo': ListTodo,
171
+ };
172
+
173
+ /** Mini-card for built-in tools — visually distinct from plugin chips */
174
+ function ToolCard({ id, name, description, entryPath, active, inactiveTitle }: {
175
+ id: string;
176
+ name: string;
177
+ description: string;
178
+ entryPath?: string;
179
+ active: boolean;
180
+ inactiveTitle?: string;
181
+ }) {
182
+ const Icon = TOOL_ICONS[id] ?? Zap;
183
+ const inner = (
184
+ <div
185
+ className={`flex items-start gap-3 px-3.5 py-3 rounded-xl border transition-all duration-150 ${
186
+ active
187
+ ? 'border-border hover:border-[var(--amber)]/30 hover:shadow-sm'
188
+ : 'border-dashed border-border/50 opacity-60'
189
+ }`}
190
+ title={active ? undefined : inactiveTitle}
191
+ >
192
+ <span className="shrink-0 mt-0.5 text-[var(--amber)]">
193
+ <Icon size={16} />
194
+ </span>
195
+ <div className="min-w-0 flex-1">
196
+ <span className="text-sm font-medium block text-foreground">{name}</span>
197
+ <span className="text-xs text-muted-foreground line-clamp-2 mt-0.5">{description}</span>
198
+ </div>
199
+ </div>
200
+ );
201
+
202
+ if (active && entryPath) {
203
+ return <Link key={id} href={`/view/${encodePath(entryPath)}`}>{inner}</Link>;
204
+ }
205
+ return <div key={id}>{inner}</div>;
206
+ }
207
+
167
208
  const FILES_PER_GROUP = 3;
168
209
  const SPACES_PER_ROW = 6;
169
210
  const PLUGINS_INITIAL = 4;
170
211
 
171
- export default function HomeContent({ recent, existingFiles, spaces, dirPaths }: { recent: RecentFile[]; existingFiles?: string[]; spaces?: SpaceInfo[]; dirPaths?: string[] }) {
212
+ export default function HomeContent({ recent, existingFiles, spaces }: { recent: RecentFile[]; existingFiles?: string[]; spaces?: SpaceInfo[] }) {
172
213
  const { t } = useLocale();
173
214
  const [showAll, setShowAll] = useState(false);
174
215
  const [showAllSpaces, setShowAllSpaces] = useState(false);
@@ -348,24 +389,25 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
348
389
  {t.home.noSpacesYet ?? 'No spaces yet. Create one to organize your knowledge.'}
349
390
  </p>
350
391
  )}
351
- <CreateSpaceModal t={t} dirPaths={dirPaths ?? []} />
352
392
  </section>
353
393
 
354
- {/* ── Section 2: Built-in capabilities ── */}
394
+ {/* ── Section 2: Tools ── */}
355
395
  {builtinFeatures.length > 0 && (
356
396
  <section className="mb-8">
357
- <SectionTitle icon={<Puzzle size={13} />} count={builtinFeatures.length}>
397
+ <SectionTitle icon={<Zap size={13} />} count={builtinFeatures.length}>
358
398
  {t.home.builtinFeatures}
359
399
  </SectionTitle>
360
- <div className="flex flex-wrap gap-2">
400
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
361
401
  {builtinFeatures.map((r) => {
362
402
  const active = !!r.entryPath && existingSet.has(r.entryPath);
403
+ const toolNames = t.home.toolName as Record<string, string> | undefined;
404
+ const toolDescs = t.home.toolDesc as Record<string, string> | undefined;
363
405
  return (
364
- <FeatureChip
406
+ <ToolCard
365
407
  key={r.id}
366
408
  id={r.id}
367
- icon={r.icon}
368
- name={r.name}
409
+ name={toolNames?.[r.id] ?? r.name}
410
+ description={toolDescs?.[r.id] ?? r.description}
369
411
  entryPath={r.entryPath}
370
412
  active={active}
371
413
  inactiveTitle={r.entryPath ? t.home.createToActivate.replace('{file}', r.entryPath) : t.home.builtinInactive}
@@ -406,7 +448,21 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
406
448
  {/* ── Section 4: Recently Edited ── */}
407
449
  {recent.length > 0 && (
408
450
  <section className="mb-12">
409
- <SectionTitle icon={<Clock size={13} />} count={recent.length}>{t.home.recentlyEdited}</SectionTitle>
451
+ <SectionTitle
452
+ icon={<Clock size={13} />}
453
+ count={recent.length}
454
+ action={
455
+ <Link
456
+ href="/changes"
457
+ className="flex items-center gap-1.5 text-xs font-medium text-[var(--amber)] transition-colors hover:opacity-80 font-display"
458
+ >
459
+ <History size={12} />
460
+ <span>{t.home.changeHistory}</span>
461
+ </Link>
462
+ }
463
+ >
464
+ {t.home.recentlyEdited}
465
+ </SectionTitle>
410
466
 
411
467
  {groups.length > 0 ? (
412
468
  /* Space-Grouped View */
@@ -543,7 +599,7 @@ function CreateSpaceButton({ t }: { t: ReturnType<typeof useLocale>['t'] }) {
543
599
  return (
544
600
  <button
545
601
  onClick={() => window.dispatchEvent(new Event('mindos:create-space'))}
546
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium bg-[var(--amber)] text-white transition-colors hover:opacity-90 cursor-pointer"
602
+ className="flex items-center gap-1.5 text-xs font-medium text-[var(--amber)] transition-colors hover:opacity-80 cursor-pointer font-display"
547
603
  >
548
604
  <Plus size={12} />
549
605
  <span>{t.home.newSpace}</span>
@@ -10,6 +10,8 @@ import { useFileImport, type ImportIntent, type ConflictMode } from '@/hooks/use
10
10
  import type { useAiOrganize } from '@/hooks/useAiOrganize';
11
11
  import { ALLOWED_IMPORT_EXTENSIONS } from '@/lib/core/file-convert';
12
12
  import type { LocalAttachment } from '@/lib/types';
13
+ import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
14
+ import DirPicker from './DirPicker';
13
15
 
14
16
  interface ImportModalProps {
15
17
  open: boolean;
@@ -18,21 +20,24 @@ interface ImportModalProps {
18
20
  initialFiles?: File[];
19
21
  /** Lifted AI organize hook from SidebarLayout (shared with OrganizeToast) */
20
22
  aiOrganize: ReturnType<typeof useAiOrganize>;
23
+ /** Flat list of directory paths for the DirPicker */
24
+ dirPaths: string[];
21
25
  }
22
26
 
23
27
  const ACCEPT = Array.from(ALLOWED_IMPORT_EXTENSIONS).join(',');
24
28
 
25
29
 
26
- export default function ImportModal({ open, onClose, defaultSpace, initialFiles, aiOrganize }: ImportModalProps) {
30
+ export default function ImportModal({ open, onClose, defaultSpace, initialFiles, aiOrganize, dirPaths }: ImportModalProps) {
27
31
  const { t } = useLocale();
28
32
  const im = useFileImport();
29
33
  const overlayRef = useRef<HTMLDivElement>(null);
30
34
  const fileInputRef = useRef<HTMLInputElement>(null);
31
- const [spaces, setSpaces] = useState<Array<{ name: string; path: string }>>([]);
32
35
  const [closing, setClosing] = useState(false);
33
36
  const [showSuccess, setShowSuccess] = useState(false);
34
37
  const [conflictFiles, setConflictFiles] = useState<string[]>([]);
35
38
  const [showConflictOptions, setShowConflictOptions] = useState(false);
39
+ const [showDiscard, setShowDiscard] = useState(false);
40
+ const [recommendedSpace, setRecommendedSpace] = useState('');
36
41
  const initializedRef = useRef(false);
37
42
 
38
43
  useEffect(() => {
@@ -49,19 +54,21 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
49
54
  if (initialFiles && initialFiles.length > 0) {
50
55
  im.addFiles(initialFiles);
51
56
  }
52
- fetch('/api/file?op=list_spaces')
53
- .then(r => r.json())
54
- .then(d => { if (d.spaces) setSpaces(d.spaces); })
55
- .catch(() => {});
56
57
  }, [open, defaultSpace, initialFiles, im]);
57
58
 
59
+ const doClose = useCallback(() => {
60
+ setShowDiscard(false);
61
+ setClosing(true);
62
+ setTimeout(() => { setClosing(false); onClose(); im.reset(); setConflictFiles([]); setShowConflictOptions(false); setRecommendedSpace(''); }, 150);
63
+ }, [im, onClose]);
64
+
58
65
  const handleClose = useCallback(() => {
59
66
  if (im.files.length > 0 && im.step !== 'done') {
60
- if (!confirm(t.fileImport.discardMessage(im.files.length))) return;
67
+ setShowDiscard(true);
68
+ return;
61
69
  }
62
- setClosing(true);
63
- setTimeout(() => { setClosing(false); onClose(); im.reset(); setConflictFiles([]); setShowConflictOptions(false); }, 150);
64
- }, [im, onClose, t]);
70
+ doClose();
71
+ }, [im, doClose]);
65
72
 
66
73
  useEffect(() => {
67
74
  if (!open) return;
@@ -132,6 +139,35 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
132
139
  // eslint-disable-next-line react-hooks/exhaustive-deps
133
140
  }, [im.targetSpace]);
134
141
 
142
+ // V1: keyword-based space recommendation when entering archive_config
143
+ useEffect(() => {
144
+ if (im.step !== 'archive_config' || dirPaths.length === 0 || im.validFiles.length === 0) return;
145
+ if (defaultSpace) return;
146
+
147
+ const fileTokens = im.validFiles
148
+ .flatMap(f => f.name.replace(/\.[^.]+$/, '').toLowerCase().split(/[\s_\-/\\]+/))
149
+ .filter(w => w.length >= 2);
150
+
151
+ let bestPath = '';
152
+ let bestScore = 0;
153
+ for (const dp of dirPaths) {
154
+ const name = dp.split('/').pop() || dp;
155
+ const spaceTokens = name.toLowerCase().split(/[\s_\-/\\]+/).filter(w => w.length >= 2);
156
+ let score = 0;
157
+ for (const ft of fileTokens) {
158
+ for (const st of spaceTokens) {
159
+ if (ft.includes(st) || st.includes(ft)) score += Math.min(ft.length, st.length);
160
+ }
161
+ }
162
+ if (score > bestScore) { bestScore = score; bestPath = dp; }
163
+ }
164
+ if (bestPath && bestScore >= 2) {
165
+ im.setTargetSpace(bestPath);
166
+ setRecommendedSpace(bestPath);
167
+ }
168
+ // eslint-disable-next-line react-hooks/exhaustive-deps
169
+ }, [im.step, dirPaths.length, im.validFiles.length]);
170
+
135
171
 
136
172
  useEffect(() => {
137
173
  if (im.step === 'done' && im.result) {
@@ -161,6 +197,15 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
161
197
 
162
198
  return (
163
199
  <>
200
+ <ConfirmDialog
201
+ open={showDiscard}
202
+ title={t.fileImport.discardTitle}
203
+ message={t.fileImport.discardMessage(im.files.length)}
204
+ confirmLabel={t.fileImport.discardConfirm}
205
+ cancelLabel={t.fileImport.discardCancel}
206
+ onConfirm={doClose}
207
+ onCancel={() => setShowDiscard(false)}
208
+ />
164
209
  <div
165
210
  ref={overlayRef}
166
211
  className={`fixed inset-0 z-50 modal-backdrop flex items-center justify-center p-4 transition-opacity duration-200 ${closing ? 'opacity-0' : 'opacity-100'}`}
@@ -305,29 +350,35 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
305
350
  ))}
306
351
  </div>
307
352
 
308
- {/* Archive config: target path preview */}
309
- {isArchiveConfig && (
310
- <div className="flex flex-col gap-1 mt-2 max-h-[120px] overflow-y-auto">
311
- {im.validFiles.map((f, idx) => {
312
- const ext = f.name.split('.').pop()?.toLowerCase();
313
- const targetExt = (ext === 'txt' || ext === 'html' || ext === 'htm' || ext === 'yaml' || ext === 'yml' || ext === 'xml')
314
- ? 'md' : ext;
315
- const stem = f.name.replace(/\.[^.]+$/, '');
316
- const targetName = `${stem}.${targetExt}`;
317
- const targetPath = im.targetSpace ? `${im.targetSpace}/${targetName}` : targetName;
318
- const hasConflict = conflictFiles.includes(f.name);
319
- return (
353
+ {/* Archive config: target path preview — only shown when path differs from source */}
354
+ {isArchiveConfig && (() => {
355
+ const previews = im.validFiles.map((f) => {
356
+ const ext = f.name.split('.').pop()?.toLowerCase();
357
+ const targetExt = (ext === 'txt' || ext === 'html' || ext === 'htm' || ext === 'yaml' || ext === 'yml' || ext === 'xml')
358
+ ? 'md' : ext;
359
+ const stem = f.name.replace(/\.[^.]+$/, '');
360
+ const targetName = `${stem}.${targetExt}`;
361
+ const targetPath = im.targetSpace ? `${im.targetSpace}/${targetName}` : targetName;
362
+ const hasConflict = conflictFiles.includes(f.name);
363
+ const changed = targetName !== f.name || !!im.targetSpace;
364
+ return { f, targetPath, hasConflict, changed };
365
+ });
366
+ const anyChanged = previews.some(p => p.changed || p.hasConflict);
367
+ if (!anyChanged) return null;
368
+ return (
369
+ <div className="flex flex-col gap-1 mt-2 max-h-[120px] overflow-y-auto">
370
+ {previews.filter(p => p.changed || p.hasConflict).map((p, idx) => (
320
371
  <div key={`preview-${idx}`} className="flex items-center gap-1.5 text-xs text-muted-foreground px-3">
321
- <span className="truncate">{f.name}</span>
372
+ <span className="truncate">{p.f.name}</span>
322
373
  <span className="text-muted-foreground/50 shrink-0">{t.fileImport.arrowTo}</span>
323
374
  <FolderOpen size={12} className="text-muted-foreground/60 shrink-0" />
324
- <span className={`truncate ${hasConflict ? 'text-[var(--amber)]' : ''}`}>{targetPath}</span>
325
- {hasConflict && <AlertTriangle size={11} className="text-[var(--amber)] shrink-0" />}
375
+ <span className={`truncate ${p.hasConflict ? 'text-[var(--amber)]' : ''}`}>{p.targetPath}</span>
376
+ {p.hasConflict && <AlertTriangle size={11} className="text-[var(--amber)] shrink-0" />}
326
377
  </div>
327
- );
328
- })}
329
- </div>
330
- )}
378
+ ))}
379
+ </div>
380
+ );
381
+ })()}
331
382
  </div>
332
383
  )}
333
384
 
@@ -361,16 +412,18 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
361
412
  {/* Space selector */}
362
413
  <div>
363
414
  <label className="text-xs font-medium text-foreground mb-1 block">{t.fileImport.targetSpace}</label>
364
- <select
415
+ <DirPicker
416
+ dirPaths={dirPaths}
365
417
  value={im.targetSpace}
366
- onChange={(e) => im.setTargetSpace(e.target.value)}
367
- className="w-full bg-muted border border-border rounded-lg px-3 py-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
368
- >
369
- <option value="">{t.fileImport.rootDir}</option>
370
- {spaces.map(s => (
371
- <option key={s.path} value={s.path}>{s.name}</option>
372
- ))}
373
- </select>
418
+ onChange={(val) => { im.setTargetSpace(val); setRecommendedSpace(''); }}
419
+ rootLabel={t.fileImport.rootDir}
420
+ />
421
+ {recommendedSpace && im.targetSpace === recommendedSpace && (
422
+ <p className="text-2xs text-muted-foreground/70 mt-1 flex items-center gap-1">
423
+ <Sparkles size={10} className="text-[var(--amber)]" />
424
+ {t.fileImport.aiRecommendedHint}
425
+ </p>
426
+ )}
374
427
  </div>
375
428
 
376
429
  {/* Conflict strategy — progressive disclosure */}
@@ -404,7 +457,7 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
404
457
  value={opt.value}
405
458
  checked={im.conflict === opt.value}
406
459
  onChange={() => im.setConflict(opt.value)}
407
- className="accent-[var(--amber)]"
460
+ className="form-radio"
408
461
  />
409
462
  {opt.label}
410
463
  {opt.value === 'overwrite' && (