@geminilight/mindos 0.6.30 → 0.6.32

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 (52) hide show
  1. package/README_zh.md +10 -4
  2. package/app/app/api/ask/route.ts +12 -7
  3. package/app/app/api/export/route.ts +105 -0
  4. package/app/app/globals.css +2 -2
  5. package/app/app/trash/page.tsx +7 -0
  6. package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
  7. package/app/components/ExportModal.tsx +220 -0
  8. package/app/components/FileTree.tsx +22 -2
  9. package/app/components/HomeContent.tsx +91 -20
  10. package/app/components/MarkdownView.tsx +45 -10
  11. package/app/components/Sidebar.tsx +10 -1
  12. package/app/components/TrashPageClient.tsx +263 -0
  13. package/app/components/ask/ToolCallBlock.tsx +102 -18
  14. package/app/components/changes/ChangesContentPage.tsx +58 -14
  15. package/app/components/explore/ExploreContent.tsx +4 -7
  16. package/app/components/explore/UseCaseCard.tsx +18 -1
  17. package/app/components/explore/use-cases.generated.ts +76 -0
  18. package/app/components/explore/use-cases.yaml +185 -0
  19. package/app/components/panels/DiscoverPanel.tsx +1 -1
  20. package/app/components/renderers/workflow-yaml/StepEditor.tsx +98 -91
  21. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +72 -72
  22. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +175 -119
  23. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +61 -61
  24. package/app/components/renderers/workflow-yaml/execution.ts +64 -12
  25. package/app/components/renderers/workflow-yaml/selectors.tsx +65 -13
  26. package/app/components/settings/AiTab.tsx +191 -174
  27. package/app/components/settings/AppearanceTab.tsx +168 -77
  28. package/app/components/settings/KnowledgeTab.tsx +131 -136
  29. package/app/components/settings/McpTab.tsx +11 -11
  30. package/app/components/settings/Primitives.tsx +60 -0
  31. package/app/components/settings/SettingsContent.tsx +15 -8
  32. package/app/components/settings/SyncTab.tsx +12 -12
  33. package/app/components/settings/UninstallTab.tsx +8 -18
  34. package/app/components/settings/UpdateTab.tsx +82 -82
  35. package/app/components/settings/types.ts +17 -8
  36. package/app/lib/acp/session.ts +12 -3
  37. package/app/lib/actions.ts +57 -3
  38. package/app/lib/agent/stream-consumer.ts +18 -0
  39. package/app/lib/agent/tools.ts +56 -9
  40. package/app/lib/core/export.ts +116 -0
  41. package/app/lib/core/trash.ts +241 -0
  42. package/app/lib/fs.ts +47 -0
  43. package/app/lib/hooks/usePinnedFiles.ts +90 -0
  44. package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
  45. package/app/lib/i18n/index.ts +3 -0
  46. package/app/lib/i18n/modules/knowledge.ts +120 -6
  47. package/app/lib/i18n/modules/onboarding.ts +2 -134
  48. package/app/lib/i18n/modules/settings.ts +12 -0
  49. package/app/package.json +8 -2
  50. package/app/scripts/generate-explore.ts +145 -0
  51. package/package.json +1 -1
  52. package/app/components/explore/use-cases.ts +0 -58
@@ -0,0 +1,220 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useRef } from 'react';
4
+ import { X, Download, FileText, Globe, Archive, Check, Loader2 } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+ import { toast } from '@/lib/toast';
7
+
8
+ type ExportFormat = 'md' | 'html' | 'zip' | 'zip-html';
9
+
10
+ interface ExportModalProps {
11
+ open: boolean;
12
+ onClose: () => void;
13
+ filePath: string;
14
+ isDirectory: boolean;
15
+ fileName: string;
16
+ }
17
+
18
+ export default function ExportModal({ open, onClose, filePath, isDirectory, fileName }: ExportModalProps) {
19
+ const { t } = useLocale();
20
+ const [format, setFormat] = useState<ExportFormat>(isDirectory ? 'zip' : 'md');
21
+ const [state, setState] = useState<'idle' | 'exporting' | 'done' | 'error'>('idle');
22
+ const [error, setError] = useState('');
23
+ const abortRef = useRef<AbortController | null>(null);
24
+
25
+ const handleExport = useCallback(() => {
26
+ setState('exporting');
27
+ setError('');
28
+
29
+ const controller = new AbortController();
30
+ abortRef.current = controller;
31
+
32
+ const url = `/api/export?path=${encodeURIComponent(filePath)}&format=${format}`;
33
+
34
+ fetch(url, { signal: controller.signal })
35
+ .then(res => {
36
+ if (!res.ok) {
37
+ const ct = res.headers.get('content-type') ?? '';
38
+ if (ct.includes('json')) {
39
+ return res.json().then((data: { error?: string }) => { throw new Error(data.error || 'Export failed'); });
40
+ }
41
+ throw new Error(`Export failed (${res.status})`);
42
+ }
43
+ return res.blob().then(blob => ({ blob, res }));
44
+ })
45
+ .then(({ blob, res }) => {
46
+ const disposition = res.headers.get('Content-Disposition') ?? '';
47
+ const match = disposition.match(/filename="?([^"]+)"?/);
48
+ const downloadName = match ? decodeURIComponent(match[1]) : fileName;
49
+
50
+ const link = document.createElement('a');
51
+ link.href = URL.createObjectURL(blob);
52
+ link.download = downloadName;
53
+ document.body.appendChild(link);
54
+ link.click();
55
+ document.body.removeChild(link);
56
+ URL.revokeObjectURL(link.href);
57
+
58
+ setState('done');
59
+ toast.success(t.export?.downloaded ?? 'Downloaded');
60
+ })
61
+ .catch((err: Error) => {
62
+ if (err.name === 'AbortError') return; // user cancelled
63
+ setState('error');
64
+ setError(err.message ?? 'Export failed');
65
+ })
66
+ .finally(() => { abortRef.current = null; });
67
+ }, [filePath, format, fileName, t]);
68
+
69
+ const handleCancel = useCallback(() => {
70
+ if (abortRef.current) {
71
+ abortRef.current.abort();
72
+ abortRef.current = null;
73
+ }
74
+ setState('idle');
75
+ setError('');
76
+ }, []);
77
+
78
+ const handleRetry = useCallback(() => {
79
+ setState('idle');
80
+ setError('');
81
+ }, []);
82
+
83
+ const handleClose = useCallback(() => {
84
+ if (abortRef.current) abortRef.current.abort();
85
+ abortRef.current = null;
86
+ setState('idle');
87
+ setError('');
88
+ onClose();
89
+ }, [onClose]);
90
+
91
+ if (!open) return null;
92
+
93
+ const formats: { value: ExportFormat; label: string; desc: string; icon: React.ReactNode; disabled?: boolean }[] = isDirectory
94
+ ? [
95
+ { value: 'zip', label: t.export?.formatZipMd ?? 'Markdown ZIP', desc: t.export?.formatZipMdDesc ?? 'All files in original format', icon: <Archive size={14} /> },
96
+ { value: 'zip-html', label: t.export?.formatZipHtml ?? 'HTML ZIP', desc: t.export?.formatZipHtmlDesc ?? 'All files as webpages', icon: <Globe size={14} /> },
97
+ ]
98
+ : [
99
+ { value: 'md', label: t.export?.formatMd ?? 'Markdown (.md)', desc: t.export?.formatMdDesc ?? 'Original format, editable', icon: <FileText size={14} /> },
100
+ { value: 'html', label: t.export?.formatHtml ?? 'HTML (.html)', desc: t.export?.formatHtmlDesc ?? 'Static webpage, shareable', icon: <Globe size={14} /> },
101
+ ];
102
+
103
+ return (
104
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" onClick={state === 'exporting' ? undefined : handleClose}>
105
+ <div
106
+ className="bg-card border border-border rounded-xl shadow-xl max-w-md w-full mx-4 animate-in fade-in-0 zoom-in-95 duration-200"
107
+ onClick={e => e.stopPropagation()}
108
+ >
109
+ {/* Header */}
110
+ <div className="flex items-center justify-between px-5 py-4 border-b border-border">
111
+ <div className="flex items-center gap-2">
112
+ <Download size={15} className="text-[var(--amber)]" />
113
+ <h3 className="text-sm font-semibold font-display">
114
+ {isDirectory ? (t.export?.exportSpace ?? 'Export Space') : (t.export?.exportFile ?? 'Export File')}
115
+ </h3>
116
+ </div>
117
+ <button onClick={handleClose} className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
118
+ <X size={14} />
119
+ </button>
120
+ </div>
121
+
122
+ {/* Body */}
123
+ <div className="px-5 py-4">
124
+ {state === 'done' ? (
125
+ <div className="text-center py-6">
126
+ <div className="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center mx-auto mb-3">
127
+ <Check size={20} className="text-success" />
128
+ </div>
129
+ <p className="text-sm font-medium font-display">{t.export?.done ?? 'Export Complete'}</p>
130
+ <p className="text-xs text-muted-foreground mt-1">{fileName}</p>
131
+ </div>
132
+ ) : state === 'error' ? (
133
+ <div className="text-center py-6">
134
+ <p className="text-sm font-medium text-error">{t.export?.error ?? 'Export failed'}</p>
135
+ <p className="text-xs text-muted-foreground mt-1">{error}</p>
136
+ </div>
137
+ ) : (
138
+ <>
139
+ <p className="text-xs text-muted-foreground mb-3 truncate" title={filePath}>{filePath}</p>
140
+ <p className="text-xs font-medium font-display text-foreground mb-2">{t.export?.chooseFormat ?? 'Choose format'}</p>
141
+ <div className="space-y-1.5">
142
+ {formats.map(f => (
143
+ <label
144
+ key={f.value}
145
+ className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
146
+ format === f.value
147
+ ? 'border-[var(--amber)]/40 bg-[var(--amber-dim)]'
148
+ : 'border-border hover:border-border/80 hover:bg-muted/30'
149
+ } ${f.disabled ? 'opacity-40 cursor-not-allowed' : ''}`}
150
+ >
151
+ <input
152
+ type="radio"
153
+ name="export-format"
154
+ value={f.value}
155
+ checked={format === f.value}
156
+ onChange={() => !f.disabled && setFormat(f.value)}
157
+ disabled={f.disabled}
158
+ className="sr-only"
159
+ />
160
+ <span className={`mt-0.5 ${format === f.value ? 'text-[var(--amber)]' : 'text-muted-foreground'}`}>
161
+ {f.icon}
162
+ </span>
163
+ <div>
164
+ <span className="text-sm font-medium text-foreground">{f.label}</span>
165
+ <span className="text-xs text-muted-foreground block mt-0.5">{f.desc}</span>
166
+ </div>
167
+ </label>
168
+ ))}
169
+ </div>
170
+ </>
171
+ )}
172
+ </div>
173
+
174
+ {/* Footer */}
175
+ <div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-border">
176
+ {state === 'done' ? (
177
+ <>
178
+ <button onClick={handleRetry} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
179
+ {t.export?.downloadAgain ?? 'Download Again'}
180
+ </button>
181
+ <button onClick={handleClose} className="px-3 py-1.5 rounded-md text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] hover:opacity-80 transition-colors">
182
+ {t.export?.cancel === 'Cancel' ? 'Done' : (t.export?.done ?? 'Done')}
183
+ </button>
184
+ </>
185
+ ) : state === 'error' ? (
186
+ <>
187
+ <button onClick={handleClose} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
188
+ {t.export?.cancel ?? 'Cancel'}
189
+ </button>
190
+ <button onClick={handleRetry} className="px-3 py-1.5 rounded-md text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] hover:opacity-80 transition-colors">
191
+ {t.export?.retry ?? 'Retry'}
192
+ </button>
193
+ </>
194
+ ) : state === 'exporting' ? (
195
+ <>
196
+ <button onClick={handleCancel} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
197
+ {t.export?.cancel ?? 'Cancel'}
198
+ </button>
199
+ <span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium text-[var(--amber-text)] opacity-70">
200
+ <Loader2 size={12} className="animate-spin" /> {t.export?.exporting ?? 'Exporting...'}
201
+ </span>
202
+ </>
203
+ ) : (
204
+ <>
205
+ <button onClick={handleClose} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
206
+ {t.export?.cancel ?? 'Cancel'}
207
+ </button>
208
+ <button
209
+ onClick={handleExport}
210
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] hover:opacity-80 transition-colors"
211
+ >
212
+ <Download size={12} /> {t.export?.exportButton ?? 'Export'}
213
+ </button>
214
+ </>
215
+ )}
216
+ </div>
217
+ </div>
218
+ </div>
219
+ );
220
+ }
@@ -6,11 +6,12 @@ import { FileNode } from '@/lib/types';
6
6
  import { encodePath } from '@/lib/utils';
7
7
  import {
8
8
  ChevronDown, FileText, Table, Folder, FolderOpen, Plus, Loader2,
9
- Trash2, Pencil, Layers, ScrollText, FolderInput, Copy, MoreHorizontal,
9
+ Trash2, Pencil, Layers, ScrollText, FolderInput, Copy, MoreHorizontal, Star,
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
13
  import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
14
+ import { usePinnedFiles } from '@/lib/hooks/usePinnedFiles';
14
15
 
15
16
  function notifyFilesChanged() {
16
17
  window.dispatchEvent(new Event('mindos:files-changed'));
@@ -134,9 +135,15 @@ function SpaceContextMenu({ x, y, node, onClose, onRename, onImport, onDelete }:
134
135
  }) {
135
136
  const router = useRouter();
136
137
  const { t } = useLocale();
138
+ const { isPinned, togglePin } = usePinnedFiles();
139
+ const pinned = isPinned(node.path);
137
140
 
138
141
  return (
139
142
  <ContextMenuShell x={x} y={y} onClose={onClose}>
143
+ <button className={MENU_ITEM} onClick={() => { togglePin(node.path); onClose(); }}>
144
+ <Star size={14} className={`shrink-0 ${pinned ? 'fill-[var(--amber)] text-[var(--amber)]' : ''}`} />
145
+ {pinned ? t.fileTree.removeFromFavorites : t.fileTree.pinToFavorites}
146
+ </button>
140
147
  <button className={MENU_ITEM} onClick={() => { router.push(`/view/${encodePath(`${node.path}/INSTRUCTION.md`)}`); onClose(); }}>
141
148
  <ScrollText size={14} className="shrink-0" /> {t.fileTree.editRules}
142
149
  </button>
@@ -168,9 +175,15 @@ function FolderContextMenu({ x, y, node, onClose, onRename, onDelete }: {
168
175
  const router = useRouter();
169
176
  const { t } = useLocale();
170
177
  const [isPending, startTransition] = useTransition();
178
+ const { isPinned, togglePin } = usePinnedFiles();
179
+ const pinned = isPinned(node.path);
171
180
 
172
181
  return (
173
- <ContextMenuShell x={x} y={y} onClose={onClose} menuHeight={140}>
182
+ <ContextMenuShell x={x} y={y} onClose={onClose} menuHeight={180}>
183
+ <button className={MENU_ITEM} onClick={() => { togglePin(node.path); onClose(); }}>
184
+ <Star size={14} className={`shrink-0 ${pinned ? 'fill-[var(--amber)] text-[var(--amber)]' : ''}`} />
185
+ {pinned ? t.fileTree.removeFromFavorites : t.fileTree.pinToFavorites}
186
+ </button>
174
187
  <button className={MENU_ITEM} disabled={isPending} onClick={() => {
175
188
  startTransition(async () => {
176
189
  const result = await convertToSpaceAction(node.path);
@@ -570,6 +583,8 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
570
583
  const [, startDeleteTransition] = useTransition();
571
584
  const renameRef = useRef<HTMLInputElement>(null);
572
585
  const { t } = useLocale();
586
+ const { isPinned, togglePin } = usePinnedFiles();
587
+ const pinned = isPinned(node.path);
573
588
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
574
589
  const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
575
590
 
@@ -659,6 +674,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
659
674
  >
660
675
  {getIcon(node)}
661
676
  <span className="truncate leading-5" suppressHydrationWarning>{node.name}</span>
677
+ {pinned && <Star size={10} className="shrink-0 fill-[var(--amber)] text-[var(--amber)] opacity-60" />}
662
678
  </button>
663
679
  <div className="absolute right-1 top-1/2 -translate-y-1/2 hidden group-hover/file:flex items-center gap-0.5">
664
680
  <button
@@ -684,6 +700,10 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
684
700
  <button className={MENU_ITEM} onClick={() => { copyPathToClipboard(node.path); setContextMenu(null); }}>
685
701
  <Copy size={14} className="shrink-0" /> {t.fileTree.copyPath}
686
702
  </button>
703
+ <button className={MENU_ITEM} onClick={() => { togglePin(node.path); setContextMenu(null); }}>
704
+ <Star size={14} className={`shrink-0 ${pinned ? 'fill-[var(--amber)] text-[var(--amber)]' : ''}`} />
705
+ {pinned ? t.fileTree.removeFromFavorites : t.fileTree.pinToFavorites}
706
+ </button>
687
707
  <button className={MENU_ITEM} onClick={(e) => { setContextMenu(null); startRename(e); }}>
688
708
  <Pencil size={14} className="shrink-0" /> {t.fileTree.rename}
689
709
  </button>
@@ -1,11 +1,12 @@
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, Zap, History, SlidersHorizontal, ListTodo } 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, Star } from 'lucide-react';
5
5
  import type { LucideIcon } from 'lucide-react';
6
6
  import { useState, useEffect, useMemo, useCallback } from 'react';
7
7
  import { useLocale } from '@/lib/LocaleContext';
8
8
  import { encodePath, relativeTime, extractEmoji, stripEmoji } from '@/lib/utils';
9
+ import { usePinnedFiles } from '@/lib/hooks/usePinnedFiles';
9
10
  import { getAllRenderers, getPluginRenderers } from '@/lib/renderers/registry';
10
11
  import OnboardingView from './OnboardingView';
11
12
  import GuideCard from './GuideCard';
@@ -252,14 +253,14 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
252
253
  <ExampleCleanupBanner />
253
254
 
254
255
  {/* ── Hero ── */}
255
- <div className="mb-10">
256
+ <div className="mb-14">
256
257
  <div className="flex items-center gap-2 mb-3">
257
- <div className="w-1 h-5 rounded-full bg-[var(--amber)]" />
258
+ <div className="w-1 h-6 rounded-full bg-gradient-to-b from-[var(--amber)] to-[var(--amber)]/30" />
258
259
  <h1 className="text-2xl font-semibold tracking-tight font-display text-foreground">
259
260
  MindOS
260
261
  </h1>
261
262
  </div>
262
- <p className="text-sm leading-relaxed mb-5 text-muted-foreground pl-4">
263
+ <p className="text-base leading-relaxed mb-5 text-muted-foreground pl-4 max-w-md">
263
264
  {t.app.tagline}
264
265
  </p>
265
266
 
@@ -269,7 +270,7 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
269
270
  onClick={triggerAsk}
270
271
  title="⌘/"
271
272
  data-walkthrough="ask-button"
272
- className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-card transition-all duration-150 hover:border-[var(--amber)]/50 hover:bg-[var(--amber-dim)]"
273
+ className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border border-border/60 shadow-sm bg-card transition-all duration-150 hover:border-[var(--amber)]/50 hover:bg-[var(--amber-dim)]"
273
274
  >
274
275
  <Sparkles size={15} className="shrink-0 text-[var(--amber)]" />
275
276
  <span className="text-sm flex-1 text-left text-foreground">
@@ -293,7 +294,7 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
293
294
  </div>
294
295
 
295
296
  {/* Quick Actions */}
296
- <div className="flex flex-wrap gap-2.5 mt-4 pl-4">
297
+ <div className="flex flex-wrap gap-3 mt-5 pl-4">
297
298
  <button
298
299
  onClick={() => window.dispatchEvent(new CustomEvent('mindos:open-import'))}
299
300
  className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-all duration-150 hover:translate-x-0.5 bg-[var(--amber-dim)] text-[var(--amber-text)]"
@@ -331,8 +332,11 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
331
332
  </div>
332
333
  </div>
333
334
 
334
- {/* ── Section 1: Spaces ── */}
335
- <section className="mb-8">
335
+ {/* ── Section: Pinned Files ── */}
336
+ <PinnedFilesSection formatTime={formatTime} />
337
+
338
+ {/* ── Section: Spaces ── */}
339
+ <section className="mb-12">
336
340
  <SectionTitle
337
341
  icon={<Brain size={13} />}
338
342
  count={spaceList.length > 0 ? spaceList.length : undefined}
@@ -392,9 +396,9 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
392
396
  )}
393
397
  </section>
394
398
 
395
- {/* ── Section 2: Tools ── */}
399
+ {/* ── Section: Tools ── */}
396
400
  {builtinFeatures.length > 0 && (
397
- <section className="mb-8">
401
+ <section className="mb-12">
398
402
  <SectionTitle icon={<Zap size={13} />} count={builtinFeatures.length}>
399
403
  {t.home.builtinFeatures}
400
404
  </SectionTitle>
@@ -419,9 +423,9 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
419
423
  </section>
420
424
  )}
421
425
 
422
- {/* ── Section 3: Extensions ── */}
426
+ {/* ── Section: Extensions ── */}
423
427
  {availablePlugins.length > 0 && (
424
- <section className="mb-8">
428
+ <section className="mb-12">
425
429
  <SectionTitle
426
430
  icon={<Puzzle size={13} />}
427
431
  count={availablePlugins.length}
@@ -446,7 +450,7 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
446
450
  </section>
447
451
  )}
448
452
 
449
- {/* ── Section 4: Recently Edited ── */}
453
+ {/* ── Section: Recently Edited ── */}
450
454
  {recent.length > 0 && (
451
455
  <section className="mb-12">
452
456
  <SectionTitle
@@ -465,8 +469,30 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
465
469
  {t.home.recentlyEdited}
466
470
  </SectionTitle>
467
471
 
472
+ {/* Spotlight — latest file */}
473
+ <Link
474
+ href={`/view/${encodePath(recent[0].path)}`}
475
+ className="block mb-5 p-4 rounded-xl border border-border/50 bg-gradient-to-r from-[var(--amber-subtle)] to-transparent hover:border-[var(--amber)]/40 hover:shadow-sm transition-all group"
476
+ >
477
+ <div className="flex items-center gap-3">
478
+ <div className="w-10 h-10 rounded-lg bg-[var(--amber)]/10 flex items-center justify-center shrink-0">
479
+ {recent[0].path.endsWith('.csv')
480
+ ? <Table size={18} className="text-[var(--amber)]" />
481
+ : <FileText size={18} className="text-[var(--amber)]" />}
482
+ </div>
483
+ <div className="flex-1 min-w-0">
484
+ <span className="text-base font-medium text-foreground block truncate">
485
+ {recent[0].path.split('/').pop()}
486
+ </span>
487
+ <span className="text-xs text-muted-foreground mt-0.5 block" suppressHydrationWarning>
488
+ {recent[0].path.split('/').slice(0, -1).join('/') || 'Root'} · {formatTime(recent[0].mtime)}
489
+ </span>
490
+ </div>
491
+ <ArrowRight size={16} className="text-muted-foreground group-hover:text-[var(--amber)] transition-colors shrink-0" />
492
+ </div>
493
+ </Link>
494
+
468
495
  {groups.length > 0 ? (
469
- /* Space-Grouped View */
470
496
  <div className="flex flex-col gap-4">
471
497
  {groups.map((group) => {
472
498
  const visibleFiles = showAll ? group.files : group.files.slice(0, FILES_PER_GROUP);
@@ -505,7 +531,6 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
505
531
  );
506
532
  })}
507
533
 
508
- {/* Root-level files (Other) */}
509
534
  {rootFiles.length > 0 && (
510
535
  <div>
511
536
  <div className="flex items-center gap-2 px-1 py-1.5">
@@ -522,7 +547,6 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
522
547
  </div>
523
548
  )}
524
549
 
525
- {/* Show more / less */}
526
550
  {groups.some(g => g.files.length > FILES_PER_GROUP) && (
527
551
  <ToggleButton
528
552
  expanded={showAll}
@@ -534,7 +558,6 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
534
558
  )}
535
559
  </div>
536
560
  ) : (
537
- /* Flat Timeline Fallback */
538
561
  <div className="relative pl-4">
539
562
  <div className="absolute left-0 top-1 bottom-1 w-px bg-border" />
540
563
  <div className="flex flex-col gap-0.5">
@@ -548,8 +571,8 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
548
571
  aria-hidden="true"
549
572
  className={`absolute -left-4 top-1/2 -translate-y-1/2 rounded-full transition-all duration-150 group-hover:scale-150 ${
550
573
  idx === 0
551
- ? 'w-2 h-2 bg-[var(--amber)] outline-2 outline-[var(--amber-dim)]'
552
- : 'w-1.5 h-1.5 bg-border'
574
+ ? 'w-2.5 h-2.5 bg-[var(--amber)] ring-2 ring-[var(--amber)]/20'
575
+ : 'w-1.5 h-1.5 bg-muted-foreground/30'
553
576
  }`}
554
577
  />
555
578
  <Link
@@ -587,7 +610,7 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
587
610
  )}
588
611
 
589
612
  {/* Footer */}
590
- <div className="mt-16 flex items-center gap-1.5 text-xs font-display text-muted-foreground opacity-60">
613
+ <div className="py-6 border-t border-border/30 flex items-center gap-1.5 text-xs font-display text-muted-foreground opacity-40">
591
614
  <Sparkles size={10} className="text-[var(--amber)]" />
592
615
  <span>{t.app.footer}</span>
593
616
  </div>
@@ -595,6 +618,54 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
595
618
  );
596
619
  }
597
620
 
621
+ /* ── Pinned Files Section ── */
622
+ function PinnedFilesSection({ formatTime }: { formatTime: (t: number) => string }) {
623
+ const { t } = useLocale();
624
+ const { pinnedFiles, removePin } = usePinnedFiles();
625
+
626
+ if (pinnedFiles.length === 0) return null;
627
+
628
+ return (
629
+ <section className="mb-12">
630
+ <SectionTitle icon={<Star size={13} />} count={pinnedFiles.length}>
631
+ {t.pinnedFiles.title}
632
+ </SectionTitle>
633
+ <div className="flex flex-col gap-0.5">
634
+ {pinnedFiles.map((filePath) => {
635
+ const name = filePath.split('/').pop() || filePath;
636
+ const dir = filePath.split('/').slice(0, -1).join('/');
637
+ const isCSV = filePath.endsWith('.csv');
638
+ return (
639
+ <div key={filePath} className="group/pin relative">
640
+ <Link
641
+ href={`/view/${encodePath(filePath)}`}
642
+ className="flex items-center gap-3 px-3 py-2 rounded-lg transition-all duration-100 hover:translate-x-0.5 hover:bg-muted"
643
+ >
644
+ <Star size={12} className="shrink-0 fill-[var(--amber)] text-[var(--amber)]" />
645
+ {isCSV
646
+ ? <Table size={12} className="shrink-0 text-success" />
647
+ : <FileText size={12} className="shrink-0 text-muted-foreground" />
648
+ }
649
+ <div className="flex-1 min-w-0">
650
+ <span className="text-sm truncate block text-foreground" suppressHydrationWarning>{name}</span>
651
+ {dir && <span className="text-xs truncate block text-muted-foreground opacity-50" suppressHydrationWarning>{dir}</span>}
652
+ </div>
653
+ </Link>
654
+ <button
655
+ onClick={(e) => { e.preventDefault(); e.stopPropagation(); removePin(filePath); }}
656
+ className="absolute right-2 top-1/2 -translate-y-1/2 hidden group-hover/pin:flex p-1 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
657
+ title={t.pinnedFiles.removedToast}
658
+ >
659
+ <X size={12} />
660
+ </button>
661
+ </div>
662
+ );
663
+ })}
664
+ </div>
665
+ </section>
666
+ );
667
+ }
668
+
598
669
  /* ── Create Space: title-bar button ── */
599
670
  function CreateSpaceButton({ t }: { t: ReturnType<typeof useLocale>['t'] }) {
600
671
  return (
@@ -6,13 +6,17 @@ import rehypeHighlight from 'rehype-highlight';
6
6
  import rehypeRaw from 'rehype-raw';
7
7
  import rehypeSlug from 'rehype-slug';
8
8
  import { useState, useCallback } from 'react';
9
- import { Copy, Check } from 'lucide-react';
9
+ import { Copy, Check, X } from 'lucide-react';
10
10
  import { copyToClipboard } from '@/lib/clipboard';
11
11
  import { toast } from '@/lib/toast';
12
12
  import type { Components } from 'react-markdown';
13
13
 
14
14
  interface MarkdownViewProps {
15
15
  content: string;
16
+ /** Lines changed by AI (1-indexed). Shows banner + fades after timeout. */
17
+ highlightLines?: number[];
18
+ /** Callback to dismiss the highlight banner */
19
+ onDismissHighlight?: () => void;
16
20
  }
17
21
 
18
22
  function CopyButton({ code }: { code: string }) {
@@ -111,16 +115,47 @@ function extractText(node: React.ReactNode): string {
111
115
  return '';
112
116
  }
113
117
 
114
- export default function MarkdownView({ content }: MarkdownViewProps) {
118
+ export default function MarkdownView({ content, highlightLines, onDismissHighlight }: MarkdownViewProps) {
119
+ const hasHighlights = highlightLines && highlightLines.length > 0;
120
+
115
121
  return (
116
- <div className="prose max-w-none">
117
- <ReactMarkdown
118
- remarkPlugins={[remarkGfm]}
119
- rehypePlugins={[rehypeSlug, rehypeHighlight, rehypeRaw]}
120
- components={components}
121
- >
122
- {content}
123
- </ReactMarkdown>
122
+ <div>
123
+ {/* Change indicator banner */}
124
+ {hasHighlights && (
125
+ <div
126
+ className="mb-4 flex items-center gap-2 rounded-md border px-3 py-2 text-xs animate-in fade-in-0 duration-300"
127
+ style={{
128
+ borderColor: 'color-mix(in srgb, var(--amber) 40%, var(--border))',
129
+ background: 'color-mix(in srgb, var(--amber) 8%, var(--card))',
130
+ color: 'var(--amber)',
131
+ }}
132
+ data-highlight-line
133
+ >
134
+ <span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)] animate-pulse shrink-0" />
135
+ <span className="font-display font-medium flex-1">
136
+ {highlightLines.length} line{highlightLines.length !== 1 ? 's' : ''} updated by AI
137
+ </span>
138
+ {onDismissHighlight && (
139
+ <button
140
+ type="button"
141
+ onClick={onDismissHighlight}
142
+ className="p-0.5 rounded hover:bg-[var(--amber)]/15 transition-colors shrink-0"
143
+ aria-label="Dismiss"
144
+ >
145
+ <X size={12} />
146
+ </button>
147
+ )}
148
+ </div>
149
+ )}
150
+ <div className="prose max-w-none">
151
+ <ReactMarkdown
152
+ remarkPlugins={[remarkGfm]}
153
+ rehypePlugins={[rehypeSlug, rehypeHighlight, rehypeRaw]}
154
+ components={components}
155
+ >
156
+ {content}
157
+ </ReactMarkdown>
158
+ </div>
124
159
  </div>
125
160
  );
126
161
  }
@@ -3,7 +3,7 @@
3
3
  import { useState, useEffect } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { useRouter, usePathname } from 'next/navigation';
6
- import { Search, PanelLeftClose, PanelLeftOpen, Menu, X, Settings } from 'lucide-react';
6
+ import { Search, PanelLeftClose, PanelLeftOpen, Menu, X, Settings, Trash2 } from 'lucide-react';
7
7
  import FileTree from './FileTree';
8
8
  import SearchModal from './SearchModal';
9
9
  import AskModal from './AskModal';
@@ -117,6 +117,15 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
117
117
  <div className="flex-1 overflow-y-auto min-h-0 px-2 py-2">
118
118
  <FileTree nodes={fileTree} onNavigate={() => setMobileOpen(false)} />
119
119
  </div>
120
+ <div className="px-2 pb-1">
121
+ <Link
122
+ href="/trash"
123
+ className="flex items-center gap-2 px-2 py-1.5 rounded-md text-xs text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
124
+ >
125
+ <Trash2 size={13} />
126
+ <span>{t.trash?.title ?? 'Trash'}</span>
127
+ </Link>
128
+ </div>
120
129
  <SyncStatusBar
121
130
  collapsed={collapsed}
122
131
  onOpenSyncSettings={openSyncSettings}