@geminilight/mindos 0.5.70 → 0.6.0

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 (42) hide show
  1. package/app/app/api/ask/route.ts +122 -92
  2. package/app/app/api/mcp/agents/route.ts +53 -2
  3. package/app/app/api/mcp/status/route.ts +1 -1
  4. package/app/app/api/skills/route.ts +10 -114
  5. package/app/components/ActivityBar.tsx +3 -4
  6. package/app/components/CreateSpaceModal.tsx +31 -6
  7. package/app/components/FileTree.tsx +33 -2
  8. package/app/components/GuideCard.tsx +197 -131
  9. package/app/components/HomeContent.tsx +85 -18
  10. package/app/components/SidebarLayout.tsx +13 -0
  11. package/app/components/SpaceInitToast.tsx +173 -0
  12. package/app/components/agents/AgentDetailContent.tsx +32 -17
  13. package/app/components/agents/AgentsContentPage.tsx +2 -1
  14. package/app/components/agents/AgentsOverviewSection.tsx +1 -14
  15. package/app/components/agents/agents-content-model.ts +16 -8
  16. package/app/components/ask/AskContent.tsx +137 -50
  17. package/app/components/ask/MentionPopover.tsx +16 -8
  18. package/app/components/ask/SlashCommandPopover.tsx +62 -0
  19. package/app/components/settings/KnowledgeTab.tsx +61 -0
  20. package/app/components/walkthrough/steps.ts +11 -6
  21. package/app/hooks/useMention.ts +14 -6
  22. package/app/hooks/useSlashCommand.ts +114 -0
  23. package/app/lib/actions.ts +79 -2
  24. package/app/lib/agent/index.ts +1 -1
  25. package/app/lib/agent/prompt.ts +2 -0
  26. package/app/lib/agent/tools.ts +106 -0
  27. package/app/lib/core/create-space.ts +11 -4
  28. package/app/lib/core/index.ts +1 -1
  29. package/app/lib/i18n-en.ts +51 -46
  30. package/app/lib/i18n-zh.ts +50 -45
  31. package/app/lib/mcp-agents.ts +8 -0
  32. package/app/lib/pdf-extract.ts +33 -0
  33. package/app/lib/pi-integration/extensions.ts +45 -0
  34. package/app/lib/pi-integration/mcporter.ts +219 -0
  35. package/app/lib/pi-integration/session-store.ts +62 -0
  36. package/app/lib/pi-integration/skills.ts +116 -0
  37. package/app/lib/settings.ts +1 -1
  38. package/app/next-env.d.ts +1 -1
  39. package/app/next.config.ts +1 -1
  40. package/app/package.json +2 -0
  41. package/mcp/src/index.ts +29 -0
  42. package/package.json +1 -1
@@ -1,37 +1,21 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef } from 'react';
4
- import { X, Sparkles, FolderOpen, MessageCircle, RefreshCw, Check, ChevronRight } from 'lucide-react';
4
+ import { X, Sparkles, Upload, MessageCircle, ExternalLink, Check, ChevronRight, Copy } from 'lucide-react';
5
5
  import Link from 'next/link';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
  import { openAskModal } from '@/hooks/useAskModal';
8
- import { extractEmoji, stripEmoji } from '@/lib/utils';
9
8
  import { walkthroughSteps } from './walkthrough/steps';
10
9
  import type { GuideState } from '@/lib/settings';
11
- import type { SpaceInfo } from '@/app/page';
12
-
13
- interface RecentFile {
14
- path: string;
15
- mtime: number;
16
- }
17
-
18
- interface GuideCardProps {
19
- /** Called when user clicks a file/dir to open it in FileView */
20
- onNavigate?: (path: string) => void;
21
- /** Existing spaces for dynamic directory listing */
22
- spaces?: SpaceInfo[];
23
- /** Recent files for empty-template fallback */
24
- recentFiles?: RecentFile[];
25
- }
26
-
27
- export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }: GuideCardProps) {
10
+ export default function GuideCard() {
28
11
  const { t } = useLocale();
29
12
  const g = t.guide;
30
13
 
31
14
  const [guideState, setGuideState] = useState<GuideState | null>(null);
32
- const [expanded, setExpanded] = useState<'kb' | 'ai' | 'sync' | null>(null);
15
+ const [expanded, setExpanded] = useState<'import' | 'ai' | 'agent' | null>(null);
33
16
  const [isFirstVisit, setIsFirstVisit] = useState(false);
34
- const [browsedCount, setBrowsedCount] = useState(0);
17
+ const [copied, setCopied] = useState(false);
18
+ const [step3Done, setStep3Done] = useState(false);
35
19
 
36
20
  // Fetch guide state from backend
37
21
  const fetchGuideState = useCallback(() => {
@@ -41,7 +25,6 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
41
25
  const gs = data.guideState;
42
26
  if (gs?.active && !gs.dismissed) {
43
27
  setGuideState(gs);
44
- if (gs.step1Done) setBrowsedCount(1);
45
28
  } else {
46
29
  setGuideState(null);
47
30
  }
@@ -65,12 +48,6 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
65
48
  };
66
49
  }, [fetchGuideState]);
67
50
 
68
- useEffect(() => {
69
- if (isFirstVisit && guideState && !guideState.step1Done) {
70
- setExpanded('kb');
71
- }
72
- }, [isFirstVisit, guideState]);
73
-
74
51
  const patchGuide = useCallback((patch: Partial<GuideState>) => {
75
52
  setGuideState(prev => prev ? { ...prev, ...patch } : prev);
76
53
  fetch('/api/setup', {
@@ -85,21 +62,27 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
85
62
  setGuideState(null);
86
63
  }, [patchGuide]);
87
64
 
88
- const handleFileOpen = useCallback((path: string) => {
89
- onNavigate?.(path);
90
- if (browsedCount === 0) {
91
- setBrowsedCount(1);
92
- patchGuide({ step1Done: true });
93
- setTimeout(() => setExpanded(null), 300);
94
- }
95
- }, [browsedCount, patchGuide, onNavigate]);
65
+ // ── Step 1: Import ──
66
+
67
+ const handleImportClick = useCallback(() => {
68
+ window.dispatchEvent(new CustomEvent('mindos:open-import'));
69
+ }, []);
96
70
 
97
- const handleSkipKB = useCallback(() => {
98
- setBrowsedCount(1);
71
+ const handleSkipImport = useCallback(() => {
99
72
  patchGuide({ step1Done: true });
100
73
  setExpanded(null);
101
74
  }, [patchGuide]);
102
75
 
76
+ // Auto-mark step 1 done when files change (import completes, space created, etc.)
77
+ useEffect(() => {
78
+ if (!guideState || guideState.step1Done) return;
79
+ const handler = () => patchGuide({ step1Done: true });
80
+ window.addEventListener('mindos:files-changed', handler);
81
+ return () => window.removeEventListener('mindos:files-changed', handler);
82
+ }, [guideState, guideState?.step1Done, patchGuide]);
83
+
84
+ // ── Step 2: AI verify ──
85
+
103
86
  const handleStartAI = useCallback(() => {
104
87
  const gs = guideState;
105
88
  const isEmpty = gs?.template === 'empty';
@@ -107,6 +90,26 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
107
90
  openAskModal(prompt, 'guide');
108
91
  }, [guideState, g]);
109
92
 
93
+ // ── Step 3: Cross-agent copy ──
94
+
95
+ const handleCopyPrompt = useCallback(async () => {
96
+ try {
97
+ await navigator.clipboard.writeText(g.agent.copyPrompt);
98
+ setCopied(true);
99
+ setTimeout(() => setCopied(false), 2000);
100
+ } catch {
101
+ // Fallback: no clipboard API
102
+ }
103
+ }, [g]);
104
+
105
+ const handleStep3Done = useCallback(() => {
106
+ setStep3Done(true);
107
+ setExpanded(null);
108
+ patchGuide({ nextStepIndex: 0 });
109
+ }, [patchGuide]);
110
+
111
+ // ── Next-step prompts ──
112
+
110
113
  const handleNextStepClick = useCallback(() => {
111
114
  if (!guideState) return;
112
115
  const idx = guideState.nextStepIndex;
@@ -117,25 +120,47 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
117
120
  }
118
121
  }, [guideState, g, patchGuide]);
119
122
 
120
- const handleSyncClick = useCallback(() => {
121
- // Open settings directly to the Sync tab (SidebarLayout listens for this event)
122
- window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'sync' } }));
123
- }, []);
123
+ // ── Auto-expand on step transitions ──
124
124
 
125
- // Auto-dismiss final state after 8 seconds
126
- const autoDismissRef = useRef<ReturnType<typeof setTimeout>>(null);
127
- const step1Done_ = guideState?.step1Done;
128
- const step2Done_ = guideState?.askedAI;
129
- const nextIdx_ = guideState?.nextStepIndex ?? 0;
130
- const allDone_ = step1Done_ && step2Done_;
131
- const allNextDone_ = allDone_ && nextIdx_ >= g.done.steps.length;
125
+ // First visit: expand step 1
126
+ useEffect(() => {
127
+ if (isFirstVisit && guideState && !guideState.step1Done) {
128
+ setExpanded('import');
129
+ }
130
+ }, [isFirstVisit, guideState]);
132
131
 
132
+ // Step transition: auto-expand next step
133
+ const prevStepRef = useRef({ s1: false, s2: false });
133
134
  useEffect(() => {
134
- if (allNextDone_) {
135
+ if (!guideState) return;
136
+ const prev = prevStepRef.current;
137
+ if (guideState.step1Done && !prev.s1) {
138
+ setExpanded('ai');
139
+ prev.s1 = true;
140
+ }
141
+ if (guideState.askedAI && !prev.s2) {
142
+ setExpanded('agent');
143
+ prev.s2 = true;
144
+ }
145
+ }, [guideState?.step1Done, guideState?.askedAI, guideState]);
146
+
147
+ // ── Auto-dismiss final state ──
148
+
149
+ const step1Done = guideState?.step1Done ?? false;
150
+ const step2Done = guideState?.askedAI ?? false;
151
+ const nextIdx = guideState?.nextStepIndex ?? 0;
152
+ const allCoreDone = step1Done && step2Done && step3Done;
153
+ const allNextDone = allCoreDone && nextIdx >= g.done.steps.length;
154
+
155
+ const autoDismissRef = useRef<ReturnType<typeof setTimeout>>(null);
156
+ useEffect(() => {
157
+ if (allNextDone) {
135
158
  autoDismissRef.current = setTimeout(() => handleDismiss(), 8000);
136
159
  }
137
160
  return () => { if (autoDismissRef.current) clearTimeout(autoDismissRef.current); };
138
- }, [allNextDone_, handleDismiss]);
161
+ }, [allNextDone, handleDismiss]);
162
+
163
+ // ── Render guards ──
139
164
 
140
165
  if (!guideState) return null;
141
166
 
@@ -145,21 +170,18 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
145
170
  && !guideState.walkthroughDismissed;
146
171
  if (walkthroughActive) return null;
147
172
 
148
- const step1Done = guideState.step1Done;
149
- const step2Done = guideState.askedAI;
150
- const allDone = step1Done && step2Done;
151
- const nextIdx = guideState.nextStepIndex;
152
- const nextSteps = g.done.steps;
153
- const allNextDone = nextIdx >= nextSteps.length;
154
- const isEmptyTemplate = guideState.template === 'empty';
173
+ // If both steps 1+2 were already done on load (e.g. page refresh), step3Done is local
174
+ // and starts false — this lets the user see step 3 again, which is useful.
175
+ const showStep3 = step1Done && step2Done && !step3Done;
155
176
 
156
- // After all next-steps done → final state (auto-dismisses after 8s)
157
- if (allDone && allNextDone) {
177
+ // ── Final state: all next-steps done ──
178
+
179
+ if (allCoreDone && allNextDone) {
158
180
  return (
159
181
  <div className="mb-6 rounded-xl border border-[var(--amber)] px-5 py-4 flex items-center gap-3 animate-in fade-in duration-300 bg-[var(--amber-subtle)]">
160
182
  <Sparkles size={16} className="animate-spin-slow text-[var(--amber)]" />
161
183
  <span className="text-sm font-semibold flex-1 text-foreground">
162
- {g.done.titleFinal}
184
+ {g.done.titleFinal}
163
185
  </span>
164
186
  <Link
165
187
  href="/explore"
@@ -174,15 +196,16 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
174
196
  );
175
197
  }
176
198
 
177
- // Collapsed done state with next-step prompts
178
- if (allDone) {
179
- const step = nextSteps[nextIdx];
199
+ // ── Done state: next-step prompts ──
200
+
201
+ if (allCoreDone) {
202
+ const step = g.done.steps[nextIdx];
180
203
  return (
181
204
  <div className="mb-6 rounded-xl border border-[var(--amber)] px-5 py-4 animate-in fade-in duration-300 bg-[var(--amber-subtle)]">
182
205
  <div className="flex items-center gap-3">
183
206
  <Sparkles size={16} className="text-[var(--amber)]" />
184
207
  <span className="text-sm font-semibold flex-1 text-foreground">
185
- 🎉 {g.done.title}
208
+ {g.done.title}
186
209
  </span>
187
210
  <button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground">
188
211
  <X size={14} />
@@ -201,7 +224,8 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
201
224
  );
202
225
  }
203
226
 
204
- // Main guide card with 3 tasks
227
+ // ── Main guide card with 3 steps ──
228
+
205
229
  return (
206
230
  <div className="mb-6 rounded-xl border border-[var(--amber)] overflow-hidden bg-[var(--amber-subtle)]">
207
231
 
@@ -218,65 +242,114 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
218
242
 
219
243
  {/* Task cards */}
220
244
  <div className="grid grid-cols-1 sm:grid-cols-3 gap-2 px-5 py-3">
221
- <TaskCard icon={<FolderOpen size={16} />} title={g.kb.title} cta={g.kb.cta} done={step1Done} active={expanded === 'kb'} onClick={() => step1Done ? null : setExpanded(expanded === 'kb' ? null : 'kb')} />
222
- <TaskCard icon={<MessageCircle size={16} />} title={g.ai.title} cta={g.ai.cta} done={step2Done} active={expanded === 'ai'} onClick={() => { if (!step2Done) handleStartAI(); }} />
223
- <TaskCard icon={<RefreshCw size={16} />} title={g.sync.title} cta={g.sync.cta} done={false} optional={g.sync.optional} active={false} onClick={handleSyncClick} />
245
+ <TaskCard
246
+ icon={<Upload size={16} />}
247
+ title={g.import.title}
248
+ cta={g.import.cta}
249
+ done={step1Done}
250
+ active={expanded === 'import'}
251
+ onClick={() => !step1Done ? setExpanded(expanded === 'import' ? null : 'import') : null}
252
+ />
253
+ <TaskCard
254
+ icon={<MessageCircle size={16} />}
255
+ title={g.ai.title}
256
+ cta={g.ai.cta}
257
+ done={step2Done}
258
+ active={expanded === 'ai'}
259
+ dimmed={!step1Done}
260
+ onClick={() => {
261
+ if (step2Done || !step1Done) return;
262
+ setExpanded(expanded === 'ai' ? null : 'ai');
263
+ }}
264
+ />
265
+ <TaskCard
266
+ icon={<ExternalLink size={16} />}
267
+ title={g.agent.title}
268
+ cta={g.agent.cta}
269
+ done={step3Done}
270
+ active={expanded === 'agent'}
271
+ dimmed={!step1Done || !step2Done}
272
+ onClick={() => {
273
+ if (!step1Done || !step2Done) return;
274
+ setExpanded(expanded === 'agent' ? null : 'agent');
275
+ }}
276
+ />
224
277
  </div>
225
278
 
226
- {/* Expanded content: Explore KB */}
227
- {expanded === 'kb' && !step1Done && (
279
+ {/* ── Step 1 expanded: Import files ── */}
280
+ {expanded === 'import' && !step1Done && (
228
281
  <div className="px-5 pb-4 animate-in slide-in-from-top-2 duration-200">
229
282
  <div className="rounded-lg border border-border bg-card p-4">
230
- <p className="text-xs mb-3 text-muted-foreground">
231
- {isEmptyTemplate ? g.kb.emptyDesc : g.kb.fullDesc}
283
+ <p className="text-xs text-muted-foreground mb-3">
284
+ {g.import.desc}
232
285
  </p>
286
+ <div className="flex items-center justify-between">
287
+ <button
288
+ onClick={handleImportClick}
289
+ className="text-xs font-medium px-4 py-2 rounded-lg transition-all hover:opacity-90"
290
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
291
+ >
292
+ {g.import.button}
293
+ </button>
294
+ <button
295
+ onClick={handleSkipImport}
296
+ className="text-xs px-3 py-1 rounded-lg text-muted-foreground transition-colors hover:bg-muted"
297
+ >
298
+ {g.skip}
299
+ </button>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ )}
233
304
 
234
- {isEmptyTemplate ? (
235
- <div className="flex flex-col gap-1.5">
236
- {recentFiles.length > 0 ? (
237
- recentFiles.map(file => {
238
- const fileName = file.path.split('/').pop() || file.path;
239
- return (
240
- <button key={file.path} onClick={() => handleFileOpen(file.path)}
241
- className="text-left text-xs px-3 py-2 rounded-lg border border-border text-foreground transition-colors hover:border-[var(--amber)]/30 hover:bg-muted/50 truncate">
242
- 📄 {fileName}
243
- </button>
244
- );
245
- })
246
- ) : (
247
- <p className="text-xs text-muted-foreground">{g.kb.emptyHint}</p>
248
- )}
249
- </div>
250
- ) : (
251
- <>
252
- <div className="grid grid-cols-2 sm:grid-cols-3 gap-1.5">
253
- {spaces.slice(0, 6).map(s => {
254
- const emoji = extractEmoji(s.name);
255
- const label = stripEmoji(s.name);
256
- return (
257
- <button key={s.name} onClick={() => handleFileOpen(s.path)}
258
- className="text-left text-xs px-3 py-2 rounded-lg border border-border text-foreground transition-colors hover:border-[var(--amber)]/30 hover:bg-muted/50">
259
- <span className="mr-1.5">{emoji || '📁'}</span>
260
- <span>{label}</span>
261
- <span className="block text-2xs mt-0.5 text-muted-foreground">
262
- {t.home.nFiles(s.fileCount)}
263
- </span>
264
- </button>
265
- );
266
- })}
267
- </div>
268
- <p className="text-xs mt-3 text-[var(--amber)]">
269
- 💡 {g.kb.instructionHint}
270
- </p>
271
- </>
272
- )}
273
-
274
- <div className="flex items-center justify-between mt-3 pt-3 border-t border-border">
275
- <span className="text-xs text-muted-foreground">
276
- {g.kb.progress(browsedCount)}
277
- </span>
278
- <button onClick={handleSkipKB}
279
- className="text-xs px-3 py-1 rounded-lg text-muted-foreground transition-colors hover:bg-muted">
305
+ {/* ── Step 2 expanded: AI verification ── */}
306
+ {expanded === 'ai' && step1Done && !step2Done && (
307
+ <div className="px-5 pb-4 animate-in slide-in-from-top-2 duration-200">
308
+ <div className="rounded-lg border border-border bg-card p-4">
309
+ <p className="text-xs text-muted-foreground mb-3">
310
+ {g.ai.desc}
311
+ </p>
312
+ <button
313
+ onClick={handleStartAI}
314
+ className="text-xs font-medium px-4 py-2 rounded-lg transition-all hover:opacity-90"
315
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
316
+ >
317
+ {g.ai.cta}
318
+ </button>
319
+ </div>
320
+ </div>
321
+ )}
322
+
323
+ {/* ── Step 3 expanded: Cross-agent prompt ── */}
324
+ {expanded === 'agent' && showStep3 && (
325
+ <div className="px-5 pb-4 animate-in slide-in-from-top-2 duration-200">
326
+ <div className="rounded-lg border border-border bg-card p-4">
327
+ <p className="text-xs text-muted-foreground mb-3">
328
+ {g.agent.desc}
329
+ </p>
330
+ <div className="relative rounded-lg border border-border bg-muted/50 p-3 pr-16">
331
+ <p className="text-xs font-mono text-foreground leading-relaxed whitespace-pre-wrap">
332
+ {g.agent.copyPrompt}
333
+ </p>
334
+ <button
335
+ onClick={handleCopyPrompt}
336
+ className={`
337
+ absolute top-2 right-2 flex items-center gap-1 text-2xs font-medium
338
+ px-2.5 py-1.5 rounded-md border transition-all
339
+ ${copied
340
+ ? 'border-success/30 bg-success/10 text-success'
341
+ : 'border-border bg-card text-muted-foreground hover:text-foreground hover:border-[var(--amber)]/30'}
342
+ `}
343
+ >
344
+ {copied ? <Check size={11} /> : <Copy size={11} />}
345
+ {copied ? g.agent.copied : g.agent.copy}
346
+ </button>
347
+ </div>
348
+ <div className="flex items-center justify-end mt-3 pt-3 border-t border-border">
349
+ <button
350
+ onClick={handleStep3Done}
351
+ className="text-xs px-3 py-1 rounded-lg text-muted-foreground transition-colors hover:bg-muted"
352
+ >
280
353
  {g.skip}
281
354
  </button>
282
355
  </div>
@@ -287,25 +360,23 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
287
360
  );
288
361
  }
289
362
 
290
- export { type GuideCardProps };
291
-
292
- function TaskCard({ icon, title, cta, done, active, optional, onClick }: {
363
+ function TaskCard({ icon, title, cta, done, active, dimmed, onClick }: {
293
364
  icon: React.ReactNode;
294
365
  title: string;
295
366
  cta: string;
296
367
  done: boolean;
297
368
  active: boolean;
298
- optional?: string;
369
+ dimmed?: boolean;
299
370
  onClick: () => void;
300
371
  }) {
301
372
  return (
302
373
  <button
303
374
  onClick={onClick}
304
- disabled={done}
375
+ disabled={done || dimmed}
305
376
  className={`
306
377
  flex flex-col items-center gap-1.5 px-3 py-3 rounded-lg border text-center
307
378
  transition-all duration-150
308
- ${done ? 'opacity-60' : 'hover:border-[var(--amber)]/30 hover:bg-muted/50 cursor-pointer'}
379
+ ${done ? 'opacity-60' : dimmed ? 'opacity-40 cursor-default' : 'hover:border-[var(--amber)]/30 hover:bg-muted/50 cursor-pointer'}
309
380
  ${active ? 'border-[var(--amber)]/40 bg-muted/50' : ''}
310
381
  ${done || active ? 'border-[var(--amber)]' : 'border-border'}
311
382
  `}
@@ -316,12 +387,7 @@ function TaskCard({ icon, title, cta, done, active, optional, onClick }: {
316
387
  <span className="text-xs font-medium text-foreground">
317
388
  {title}
318
389
  </span>
319
- {optional && (
320
- <span className="text-2xs px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
321
- {optional}
322
- </span>
323
- )}
324
- {!done && !optional && (
390
+ {!done && !dimmed && (
325
391
  <span className="text-2xs text-[var(--amber)]">
326
392
  {cta} →
327
393
  </span>
@@ -1,14 +1,15 @@
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 } from 'lucide-react';
5
- import { useState, useEffect, useMemo } from 'react';
4
+ import { FileText, Table, Clock, Sparkles, ArrowRight, FilePlus, Search, ChevronDown, Compass, Folder, Puzzle, Brain, Plus, Trash2, Check, Loader2, X, FolderInput } from 'lucide-react';
5
+ import { useState, useEffect, useMemo, useCallback } from 'react';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
  import { encodePath, relativeTime, extractEmoji, stripEmoji } from '@/lib/utils';
8
8
  import { getAllRenderers, getPluginRenderers } from '@/lib/renderers/registry';
9
9
  import OnboardingView from './OnboardingView';
10
10
  import GuideCard from './GuideCard';
11
11
  import CreateSpaceModal from './CreateSpaceModal';
12
+ import { scanExampleFilesAction, cleanupExamplesAction } from '@/lib/actions';
12
13
  import type { SpaceInfo } from '@/app/page';
13
14
 
14
15
  interface RecentFile {
@@ -131,11 +132,8 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
131
132
 
132
133
  return (
133
134
  <div className="content-width px-4 md:px-6 py-8 md:py-12">
134
- <GuideCard
135
- onNavigate={(path) => { window.location.href = `/view/${encodeURIComponent(path)}`; }}
136
- spaces={spaceList}
137
- recentFiles={recent.slice(0, 5)}
138
- />
135
+ <GuideCard />
136
+ <ExampleCleanupBanner />
139
137
 
140
138
  {/* ── Hero ── */}
141
139
  <div className="mb-10">
@@ -180,25 +178,32 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
180
178
 
181
179
  {/* Quick Actions */}
182
180
  <div className="flex flex-wrap gap-2.5 mt-4 pl-4">
181
+ <button
182
+ onClick={() => window.dispatchEvent(new CustomEvent('mindos:open-import'))}
183
+ 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)]"
184
+ >
185
+ <FolderInput size={14} />
186
+ <span>{t.fileTree.importFile}</span>
187
+ </button>
188
+ <Link
189
+ href="/view/Untitled.md"
190
+ className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors bg-muted text-muted-foreground"
191
+ >
192
+ <FilePlus size={14} />
193
+ <span>{t.home.newNote}</span>
194
+ </Link>
183
195
  {lastFile && (
184
196
  <Link
185
197
  href={`/view/${encodePath(lastFile.path)}`}
186
- 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)]"
198
+ className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors text-muted-foreground hover:text-foreground"
187
199
  >
188
200
  <ArrowRight size={14} />
189
201
  <span>{t.home.continueEditing}</span>
190
- <span className="text-xs opacity-60 truncate max-w-[160px]" suppressHydrationWarning>
202
+ <span className="text-xs opacity-50 truncate max-w-[140px]" suppressHydrationWarning>
191
203
  {lastFile.path.split('/').pop()}
192
204
  </span>
193
205
  </Link>
194
206
  )}
195
- <Link
196
- href="/view/Untitled.md"
197
- className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors bg-muted text-muted-foreground"
198
- >
199
- <FilePlus size={14} />
200
- <span>{t.home.newNote}</span>
201
- </Link>
202
207
  <Link
203
208
  href="/explore"
204
209
  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 text-[var(--amber)]"
@@ -229,10 +234,10 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
229
234
  <Link
230
235
  key={s.name}
231
236
  href={`/view/${encodePath(s.path)}`}
232
- className={`flex items-start gap-3 px-3.5 py-3 rounded-xl border transition-all duration-150 hover:translate-x-0.5 ${
237
+ className={`flex items-start gap-3 px-3.5 py-3 rounded-xl border transition-all duration-150 ${
233
238
  isEmpty
234
239
  ? 'border-dashed border-border/50 opacity-50 hover:opacity-70'
235
- : 'border-border hover:border-[var(--amber)]/30 hover:bg-muted/40'
240
+ : 'border-border hover:border-[var(--amber)]/30 hover:shadow-sm'
236
241
  }`}
237
242
  >
238
243
  {emoji ? (
@@ -517,3 +522,65 @@ function CreateSpaceButton({ t }: { t: ReturnType<typeof useLocale>['t'] }) {
517
522
  );
518
523
  }
519
524
 
525
+ /* ── Example files cleanup banner ── */
526
+ function ExampleCleanupBanner() {
527
+ const { t } = useLocale();
528
+ const [count, setCount] = useState<number | null>(null);
529
+ const [cleaning, setCleaning] = useState(false);
530
+ const [done, setDone] = useState(false);
531
+ const [dismissed, setDismissed] = useState(false);
532
+
533
+ useEffect(() => {
534
+ scanExampleFilesAction().then(r => {
535
+ if (r.files.length > 0) setCount(r.files.length);
536
+ }).catch(() => {});
537
+ }, []);
538
+
539
+ const handleCleanup = useCallback(async () => {
540
+ if (count === null) return;
541
+ setCleaning(true);
542
+ try {
543
+ const r = await cleanupExamplesAction();
544
+ if (r.success) {
545
+ setDone(true);
546
+ setTimeout(() => setDismissed(true), 2500);
547
+ }
548
+ } catch { /* silent — banner stays, user can retry */ }
549
+ setCleaning(false);
550
+ }, [count]);
551
+
552
+ if (dismissed || count === null || count === 0) return null;
553
+
554
+ if (done) {
555
+ return (
556
+ <div className="mb-6 flex items-center gap-2.5 px-4 py-3 rounded-xl border border-success/30 bg-success/5 animate-in fade-in duration-300">
557
+ <Check size={14} className="text-success shrink-0" />
558
+ <span className="text-xs text-success">{t.home.cleanupExamplesDone}</span>
559
+ </div>
560
+ );
561
+ }
562
+
563
+ return (
564
+ <div className="mb-6 flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-muted/30 animate-in fade-in duration-300">
565
+ <span className="text-sm leading-none shrink-0">🧪</span>
566
+ <span className="text-xs text-muted-foreground flex-1">
567
+ {t.home.cleanupExamples(count)}
568
+ </span>
569
+ <button
570
+ onClick={handleCleanup}
571
+ disabled={cleaning}
572
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors shrink-0 disabled:opacity-50 bg-[var(--amber-dim)] text-[var(--amber)] hover:opacity-80"
573
+ >
574
+ {cleaning ? <Loader2 size={11} className="animate-spin" /> : <Trash2 size={11} />}
575
+ {t.home.cleanupExamplesButton}
576
+ </button>
577
+ <button
578
+ onClick={() => setDismissed(true)}
579
+ className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground shrink-0"
580
+ >
581
+ <X size={12} />
582
+ </button>
583
+ </div>
584
+ );
585
+ }
586
+
@@ -26,6 +26,7 @@ import AskModal from './AskModal';
26
26
  import SettingsModal from './SettingsModal';
27
27
  import KeyboardShortcuts from './KeyboardShortcuts';
28
28
  import ChangesBanner from './changes/ChangesBanner';
29
+ import SpaceInitToast from './SpaceInitToast';
29
30
  import { MobileSyncDot, useSyncStatus } from './SyncStatusBar';
30
31
  import { FileNode } from '@/lib/types';
31
32
  import { useLocale } from '@/lib/LocaleContext';
@@ -127,6 +128,16 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
127
128
  return () => window.removeEventListener('mindos:open-import', handler);
128
129
  }, [handleOpenImport]);
129
130
 
131
+ // Listen for cross-component "open panel" events (e.g. GuideCard → Agents)
132
+ useEffect(() => {
133
+ const handler = (e: Event) => {
134
+ const panel = (e as CustomEvent).detail?.panel;
135
+ if (panel) lp.setActivePanel(panel);
136
+ };
137
+ window.addEventListener('mindos:open-panel', handler);
138
+ return () => window.removeEventListener('mindos:open-panel', handler);
139
+ }, [lp]);
140
+
130
141
  // GuideCard first message handler
131
142
  const handleFirstMessage = useCallback(() => {
132
143
  const notifyGuide = () => window.dispatchEvent(new Event('guide-state-updated'));
@@ -459,6 +470,8 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
459
470
  {children}
460
471
  </div>
461
472
 
473
+ <SpaceInitToast />
474
+
462
475
  {/* Global drag overlay */}
463
476
  {dragOverlay && !importModalOpen && (
464
477
  <div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm transition-opacity duration-200">