@geminilight/mindos 0.5.18 → 0.5.20

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 (38) hide show
  1. package/app/app/api/ask/route.ts +5 -4
  2. package/app/app/api/file/route.ts +35 -11
  3. package/app/app/api/setup/route.ts +64 -1
  4. package/app/app/api/skills/route.ts +22 -3
  5. package/app/app/globals.css +1 -0
  6. package/app/components/AskFab.tsx +49 -3
  7. package/app/components/AskModal.tsx +11 -2
  8. package/app/components/GuideCard.tsx +361 -0
  9. package/app/components/HomeContent.tsx +2 -2
  10. package/app/components/Sidebar.tsx +21 -1
  11. package/app/components/ask/ToolCallBlock.tsx +2 -1
  12. package/app/components/settings/KnowledgeTab.tsx +64 -2
  13. package/app/components/settings/McpTab.tsx +286 -56
  14. package/app/components/setup/StepAI.tsx +9 -1
  15. package/app/components/setup/index.tsx +4 -0
  16. package/app/components/setup/types.ts +2 -0
  17. package/app/hooks/useAskModal.ts +46 -0
  18. package/app/lib/agent/stream-consumer.ts +4 -2
  19. package/app/lib/agent/tools.ts +26 -12
  20. package/app/lib/fs.ts +9 -1
  21. package/app/lib/i18n.ts +16 -0
  22. package/app/lib/settings.ts +29 -0
  23. package/app/next-env.d.ts +1 -1
  24. package/app/next.config.ts +7 -0
  25. package/bin/cli.js +135 -9
  26. package/bin/lib/build.js +2 -7
  27. package/bin/lib/mcp-spawn.js +2 -13
  28. package/bin/lib/utils.js +23 -0
  29. package/package.json +1 -1
  30. package/scripts/setup.js +13 -0
  31. package/skills/mindos/SKILL.md +10 -168
  32. package/skills/mindos-zh/SKILL.md +14 -172
  33. package/skills/project-wiki/SKILL.md +80 -74
  34. package/skills/project-wiki/references/file-reference.md +6 -2
  35. package/templates/skill-rules/en/skill-rules.md +222 -0
  36. package/templates/skill-rules/en/user-rules.md +20 -0
  37. package/templates/skill-rules/zh/skill-rules.md +222 -0
  38. package/templates/skill-rules/zh/user-rules.md +20 -0
@@ -0,0 +1,361 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { X, Sparkles, FolderOpen, MessageCircle, RefreshCw, Check, ChevronRight } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+ import { openAskModal } from '@/hooks/useAskModal';
7
+ import type { GuideState } from '@/lib/settings';
8
+
9
+ const DIR_ICONS: Record<string, string> = {
10
+ profile: '👤', notes: '📝', connections: '🔗',
11
+ workflows: '🔄', resources: '📚', projects: '🚀',
12
+ };
13
+
14
+ const EMPTY_FILES = ['INSTRUCTION.md', 'README.md', 'CONFIG.json'];
15
+
16
+ interface GuideCardProps {
17
+ /** Called when user clicks a file/dir to open it in FileView */
18
+ onNavigate?: (path: string) => void;
19
+ }
20
+
21
+ export default function GuideCard({ onNavigate }: GuideCardProps) {
22
+ const { t } = useLocale();
23
+ const g = t.guide;
24
+
25
+ const [guideState, setGuideState] = useState<GuideState | null>(null);
26
+ const [expanded, setExpanded] = useState<'kb' | 'ai' | 'sync' | null>(null);
27
+ const [isFirstVisit, setIsFirstVisit] = useState(false);
28
+ const [browsedCount, setBrowsedCount] = useState(0);
29
+
30
+ // Fetch guide state from backend
31
+ const fetchGuideState = useCallback(() => {
32
+ fetch('/api/setup')
33
+ .then(r => r.json())
34
+ .then(data => {
35
+ const gs = data.guideState;
36
+ if (gs?.active && !gs.dismissed) {
37
+ setGuideState(gs);
38
+ if (gs.step1Done) setBrowsedCount(1);
39
+ } else {
40
+ // Guide inactive or dismissed — clear local state
41
+ setGuideState(null);
42
+ }
43
+ })
44
+ .catch(() => {});
45
+ }, []);
46
+
47
+ useEffect(() => {
48
+ fetchGuideState();
49
+
50
+ // ?welcome=1 → first visit, auto-expand explore task
51
+ const params = new URLSearchParams(window.location.search);
52
+ if (params.get('welcome') === '1') {
53
+ setIsFirstVisit(true);
54
+ const url = new URL(window.location.href);
55
+ url.searchParams.delete('welcome');
56
+ window.history.replaceState({}, '', url.pathname + (url.search || ''));
57
+ }
58
+
59
+ // Re-fetch when guide state is updated (e.g. after AskFab patches askedAI)
60
+ const handleGuideUpdate = () => fetchGuideState();
61
+ window.addEventListener('focus', handleGuideUpdate);
62
+ window.addEventListener('guide-state-updated', handleGuideUpdate);
63
+ return () => {
64
+ window.removeEventListener('focus', handleGuideUpdate);
65
+ window.removeEventListener('guide-state-updated', handleGuideUpdate);
66
+ };
67
+ }, [fetchGuideState]);
68
+
69
+ // Auto-expand KB task on first visit
70
+ useEffect(() => {
71
+ if (isFirstVisit && guideState && !guideState.step1Done) {
72
+ setExpanded('kb');
73
+ }
74
+ }, [isFirstVisit, guideState]);
75
+
76
+ // Patch guideState to backend
77
+ const patchGuide = useCallback((patch: Partial<GuideState>) => {
78
+ setGuideState(prev => prev ? { ...prev, ...patch } : prev);
79
+ fetch('/api/setup', {
80
+ method: 'PATCH',
81
+ headers: { 'Content-Type': 'application/json' },
82
+ body: JSON.stringify({ guideState: patch }),
83
+ }).catch(() => {});
84
+ }, []);
85
+
86
+ const handleDismiss = useCallback(() => {
87
+ patchGuide({ dismissed: true });
88
+ setGuideState(null);
89
+ }, [patchGuide]);
90
+
91
+ const handleFileOpen = useCallback((path: string) => {
92
+ onNavigate?.(path);
93
+ if (browsedCount === 0) {
94
+ setBrowsedCount(1);
95
+ patchGuide({ step1Done: true });
96
+ // Collapse after a beat
97
+ setTimeout(() => setExpanded(null), 300);
98
+ }
99
+ }, [browsedCount, patchGuide, onNavigate]);
100
+
101
+ const handleSkipKB = useCallback(() => {
102
+ setBrowsedCount(1);
103
+ patchGuide({ step1Done: true });
104
+ setExpanded(null);
105
+ }, [patchGuide]);
106
+
107
+ const handleStartAI = useCallback(() => {
108
+ const gs = guideState;
109
+ const isEmpty = gs?.template === 'empty';
110
+ const prompt = isEmpty ? g.ai.promptEmpty : g.ai.prompt;
111
+ openAskModal(prompt, 'guide');
112
+ // Don't optimistically set askedAI here — wait until user actually sends a message
113
+ // AskFab.onFirstMessage will PATCH askedAI:true
114
+ }, [guideState, g]);
115
+
116
+ const handleNextStepClick = useCallback(() => {
117
+ if (!guideState) return;
118
+ const idx = guideState.nextStepIndex;
119
+ const steps = g.done.steps;
120
+ if (idx < steps.length) {
121
+ openAskModal(steps[idx].prompt, 'guide-next');
122
+ // Optimistic local update — AskFab will persist to backend on first message
123
+ patchGuide({ nextStepIndex: idx + 1 });
124
+ }
125
+ }, [guideState, g, patchGuide]);
126
+
127
+ const handleSyncClick = useCallback(() => {
128
+ // Dispatch ⌘, to open Settings modal
129
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: ',', metaKey: true, bubbles: true }));
130
+ }, []);
131
+
132
+ // Auto-dismiss final state after 8 seconds
133
+ const autoDismissRef = useRef<ReturnType<typeof setTimeout>>(null);
134
+ const step1Done_ = guideState?.step1Done;
135
+ const step2Done_ = guideState?.askedAI;
136
+ const nextIdx_ = guideState?.nextStepIndex ?? 0;
137
+ const allDone_ = step1Done_ && step2Done_;
138
+ const allNextDone_ = allDone_ && nextIdx_ >= g.done.steps.length;
139
+
140
+ useEffect(() => {
141
+ if (allNextDone_) {
142
+ autoDismissRef.current = setTimeout(() => handleDismiss(), 8000);
143
+ }
144
+ return () => { if (autoDismissRef.current) clearTimeout(autoDismissRef.current); };
145
+ }, [allNextDone_, handleDismiss]);
146
+
147
+ // Don't render if no active guide
148
+ if (!guideState) return null;
149
+
150
+ const step1Done = guideState.step1Done;
151
+ const step2Done = guideState.askedAI;
152
+ const allDone = step1Done && step2Done;
153
+ const nextIdx = guideState.nextStepIndex;
154
+ const nextSteps = g.done.steps;
155
+ const allNextDone = nextIdx >= nextSteps.length;
156
+ const isEmptyTemplate = guideState.template === 'empty';
157
+
158
+ // After all next-steps done → final state (auto-dismisses after 8s)
159
+ if (allDone && allNextDone) {
160
+ return (
161
+ <div className="mb-6 rounded-xl border px-5 py-4 flex items-center gap-3 animate-in fade-in duration-300"
162
+ style={{ background: 'var(--amber-subtle, rgba(200,135,30,0.08))', borderColor: 'var(--amber)' }}>
163
+ <Sparkles size={16} className="animate-spin-slow" style={{ color: 'var(--amber)' }} />
164
+ <span className="text-sm font-semibold flex-1" style={{ color: 'var(--foreground)' }}>
165
+ ✨ {g.done.titleFinal}
166
+ </span>
167
+ <button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors"
168
+ style={{ color: 'var(--muted-foreground)' }}>
169
+ <X size={14} />
170
+ </button>
171
+ </div>
172
+ );
173
+ }
174
+
175
+ // Collapsed done state with next-step prompts
176
+ if (allDone) {
177
+ const step = nextSteps[nextIdx];
178
+ return (
179
+ <div className="mb-6 rounded-xl border px-5 py-4 animate-in fade-in duration-300"
180
+ style={{ background: 'var(--amber-subtle, rgba(200,135,30,0.08))', borderColor: 'var(--amber)' }}>
181
+ <div className="flex items-center gap-3">
182
+ <Sparkles size={16} style={{ color: 'var(--amber)' }} />
183
+ <span className="text-sm font-semibold flex-1" style={{ color: 'var(--foreground)' }}>
184
+ 🎉 {g.done.title}
185
+ </span>
186
+ <button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors"
187
+ style={{ color: 'var(--muted-foreground)' }}>
188
+ <X size={14} />
189
+ </button>
190
+ </div>
191
+ {step && (
192
+ <button
193
+ onClick={handleNextStepClick}
194
+ className="mt-3 flex items-center gap-2 text-sm transition-colors hover:opacity-80 cursor-pointer animate-in fade-in slide-in-from-left-2 duration-300"
195
+ style={{ color: 'var(--amber)' }}
196
+ >
197
+ <ChevronRight size={14} />
198
+ <span>{step.hint}</span>
199
+ </button>
200
+ )}
201
+ </div>
202
+ );
203
+ }
204
+
205
+ // Main guide card with 3 tasks
206
+ return (
207
+ <div className="mb-6 rounded-xl border overflow-hidden"
208
+ style={{ background: 'var(--amber-subtle, rgba(200,135,30,0.08))', borderColor: 'var(--amber)' }}>
209
+
210
+ {/* Header */}
211
+ <div className="flex items-center gap-3 px-5 pt-4 pb-2">
212
+ <Sparkles size={16} style={{ color: 'var(--amber)' }} />
213
+ <span className="text-sm font-semibold flex-1 font-display" style={{ color: 'var(--foreground)' }}>
214
+ {g.title}
215
+ </span>
216
+ <button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors"
217
+ style={{ color: 'var(--muted-foreground)' }}>
218
+ <X size={14} />
219
+ </button>
220
+ </div>
221
+
222
+ {/* Task cards */}
223
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-2 px-5 py-3">
224
+ {/* ① Explore KB */}
225
+ <TaskCard
226
+ icon={<FolderOpen size={16} />}
227
+ title={g.kb.title}
228
+ cta={g.kb.cta}
229
+ done={step1Done}
230
+ active={expanded === 'kb'}
231
+ onClick={() => step1Done ? null : setExpanded(expanded === 'kb' ? null : 'kb')}
232
+ />
233
+ {/* ② Chat with AI */}
234
+ <TaskCard
235
+ icon={<MessageCircle size={16} />}
236
+ title={g.ai.title}
237
+ cta={g.ai.cta}
238
+ done={step2Done}
239
+ active={expanded === 'ai'}
240
+ onClick={() => {
241
+ if (!step2Done) handleStartAI();
242
+ }}
243
+ />
244
+ {/* ③ Sync (optional) */}
245
+ <TaskCard
246
+ icon={<RefreshCw size={16} />}
247
+ title={g.sync.title}
248
+ cta={g.sync.cta}
249
+ done={false}
250
+ optional={g.sync.optional}
251
+ active={false}
252
+ onClick={handleSyncClick}
253
+ />
254
+ </div>
255
+
256
+ {/* Expanded content: Explore KB */}
257
+ {expanded === 'kb' && !step1Done && (
258
+ <div className="px-5 pb-4 animate-in slide-in-from-top-2 duration-200">
259
+ <div className="rounded-lg border p-4" style={{ background: 'var(--card)', borderColor: 'var(--border)' }}>
260
+ <p className="text-xs mb-3" style={{ color: 'var(--muted-foreground)' }}>
261
+ {isEmptyTemplate ? g.kb.emptyDesc : g.kb.fullDesc}
262
+ </p>
263
+
264
+ {isEmptyTemplate ? (
265
+ <div className="flex flex-col gap-1.5">
266
+ {EMPTY_FILES.map(file => (
267
+ <button key={file} onClick={() => handleFileOpen(file)}
268
+ className="text-left text-xs px-3 py-2 rounded-lg border transition-colors hover:border-amber-500/30 hover:bg-muted/50"
269
+ style={{ borderColor: 'var(--border)', color: 'var(--foreground)' }}>
270
+ 📄 {(g.kb.emptyFiles as Record<string, string>)[file.split('.')[0].toLowerCase()] || file}
271
+ </button>
272
+ ))}
273
+ <p className="text-xs mt-2" style={{ color: 'var(--muted-foreground)', opacity: 0.7 }}>
274
+ {g.kb.emptyHint}
275
+ </p>
276
+ </div>
277
+ ) : (
278
+ <>
279
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-1.5">
280
+ {Object.entries(DIR_ICONS).map(([key, icon]) => (
281
+ <button key={key} onClick={() => handleFileOpen(key.charAt(0).toUpperCase() + key.slice(1))}
282
+ className="text-left text-xs px-3 py-2 rounded-lg border transition-colors hover:border-amber-500/30 hover:bg-muted/50"
283
+ style={{ borderColor: 'var(--border)', color: 'var(--foreground)' }}>
284
+ <span className="mr-1.5">{icon}</span>
285
+ <span className="capitalize">{key}</span>
286
+ <span className="block text-2xs mt-0.5" style={{ color: 'var(--muted-foreground)' }}>
287
+ {(g.kb.dirs as Record<string, string>)[key]}
288
+ </span>
289
+ </button>
290
+ ))}
291
+ </div>
292
+ <p className="text-xs mt-3" style={{ color: 'var(--amber)' }}>
293
+ 💡 {g.kb.instructionHint}
294
+ </p>
295
+ </>
296
+ )}
297
+
298
+ <div className="flex items-center justify-between mt-3 pt-3" style={{ borderTop: '1px solid var(--border)' }}>
299
+ <span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
300
+ {g.kb.progress(browsedCount)}
301
+ </span>
302
+ <button onClick={handleSkipKB}
303
+ className="text-xs px-3 py-1 rounded-lg transition-colors hover:bg-muted"
304
+ style={{ color: 'var(--muted-foreground)' }}>
305
+ {g.skip}
306
+ </button>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ )}
311
+ </div>
312
+ );
313
+ }
314
+
315
+ // Expose callback for AskModal integration
316
+ export { type GuideCardProps };
317
+
318
+ // Reusable sub-component
319
+ function TaskCard({ icon, title, cta, done, active, optional, onClick }: {
320
+ icon: React.ReactNode;
321
+ title: string;
322
+ cta: string;
323
+ done: boolean;
324
+ active: boolean;
325
+ optional?: string;
326
+ onClick: () => void;
327
+ }) {
328
+ return (
329
+ <button
330
+ onClick={onClick}
331
+ disabled={done}
332
+ className={`
333
+ flex flex-col items-center gap-1.5 px-3 py-3 rounded-lg border text-center
334
+ transition-all duration-150
335
+ ${done ? 'opacity-60' : 'hover:border-amber-500/30 hover:bg-muted/50 cursor-pointer'}
336
+ ${active ? 'border-amber-500/40 bg-muted/50' : ''}
337
+ `}
338
+ style={{ borderColor: done || active ? 'var(--amber)' : 'var(--border)' }}
339
+ >
340
+ <span
341
+ className={done ? 'animate-in zoom-in-50 duration-300' : ''}
342
+ style={{ color: done ? 'var(--success)' : 'var(--amber)' }}
343
+ >
344
+ {done ? <Check size={16} /> : icon}
345
+ </span>
346
+ <span className="text-xs font-medium" style={{ color: 'var(--foreground)' }}>
347
+ {title}
348
+ </span>
349
+ {optional && (
350
+ <span className="text-2xs px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
351
+ {optional}
352
+ </span>
353
+ )}
354
+ {!done && !optional && (
355
+ <span className="text-2xs" style={{ color: 'var(--amber)' }}>
356
+ {cta} →
357
+ </span>
358
+ )}
359
+ </button>
360
+ );
361
+ }
@@ -8,7 +8,7 @@ import { encodePath, relativeTime } from '@/lib/utils';
8
8
  import { getAllRenderers } from '@/lib/renderers/registry';
9
9
  import '@/lib/renderers/index'; // registers all renderers
10
10
  import OnboardingView from './OnboardingView';
11
- import WelcomeBanner from './WelcomeBanner';
11
+ import GuideCard from './GuideCard';
12
12
 
13
13
  interface RecentFile {
14
14
  path: string;
@@ -70,7 +70,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
70
70
 
71
71
  return (
72
72
  <div className="content-width px-4 md:px-6 py-8 md:py-12">
73
- <WelcomeBanner />
73
+ <GuideCard onNavigate={(path) => { window.location.href = `/view/${encodeURIComponent(path)}`; }} />
74
74
  {/* Hero */}
75
75
  <div className="mb-10">
76
76
  <div className="flex items-center gap-2 mb-3">
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState, useEffect } from 'react';
4
4
  import Link from 'next/link';
5
- import { usePathname } from 'next/navigation';
5
+ import { useRouter, usePathname } from 'next/navigation';
6
6
  import { Search, PanelLeftClose, PanelLeftOpen, Menu, X, Settings } from 'lucide-react';
7
7
  import FileTree from './FileTree';
8
8
  import SearchModal from './SearchModal';
@@ -45,6 +45,7 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
45
45
  const [settingsTab, setSettingsTab] = useState<Tab | undefined>(undefined);
46
46
  const [mobileOpen, setMobileOpen] = useState(false);
47
47
  const { t } = useLocale();
48
+ const router = useRouter();
48
49
 
49
50
  // Shared sync status for collapsed dot & mobile dot
50
51
  const { status: syncStatus } = useSyncStatus();
@@ -54,6 +55,25 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
54
55
  ? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
55
56
  : undefined;
56
57
 
58
+ // Refresh file tree when tab becomes visible (catches external changes from
59
+ // MCP agents, CLI edits, or other browser tabs) and periodically while visible.
60
+ useEffect(() => {
61
+ const onVisible = () => {
62
+ if (document.visibilityState === 'visible') router.refresh();
63
+ };
64
+ document.addEventListener('visibilitychange', onVisible);
65
+
66
+ // Light periodic refresh every 30s while tab is visible
67
+ const interval = setInterval(() => {
68
+ if (document.visibilityState === 'visible') router.refresh();
69
+ }, 30_000);
70
+
71
+ return () => {
72
+ document.removeEventListener('visibilitychange', onVisible);
73
+ clearInterval(interval);
74
+ };
75
+ }, [router]);
76
+
57
77
  useEffect(() => {
58
78
  const handler = (e: KeyboardEvent) => {
59
79
  if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setSearchOpen(v => !v); }
@@ -41,7 +41,8 @@ function formatInput(input: unknown): string {
41
41
  return parts.join(', ');
42
42
  }
43
43
 
44
- function truncateOutput(output: string, maxLen = 200): string {
44
+ function truncateOutput(output: string | undefined, maxLen = 200): string {
45
+ if (!output) return '';
45
46
  if (output.length <= maxLen) return output;
46
47
  return output.slice(0, maxLen) + '…';
47
48
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { useState, useSyncExternalStore } from 'react';
4
- import { Copy, Check, RefreshCw, Trash2 } from 'lucide-react';
3
+ import { useState, useEffect, useCallback, useSyncExternalStore } from 'react';
4
+ import { Copy, Check, RefreshCw, Trash2, Sparkles } from 'lucide-react';
5
5
  import type { SettingsData } from './types';
6
6
  import { Field, Input, EnvBadge, SectionLabel } from './Primitives';
7
7
  import { apiFetch } from '@/lib/api';
@@ -16,6 +16,38 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
16
16
  const env = data.envOverrides ?? {};
17
17
  const k = t.settings.knowledge;
18
18
 
19
+ // Guide state toggle
20
+ const [guideActive, setGuideActive] = useState<boolean | null>(null);
21
+ const [guideDismissed, setGuideDismissed] = useState(false);
22
+
23
+ useEffect(() => {
24
+ fetch('/api/setup')
25
+ .then(r => r.json())
26
+ .then(d => {
27
+ const gs = d.guideState;
28
+ if (gs) {
29
+ setGuideActive(gs.active);
30
+ setGuideDismissed(!!gs.dismissed);
31
+ }
32
+ })
33
+ .catch(() => {});
34
+ }, []);
35
+
36
+ const handleGuideToggle = useCallback(() => {
37
+ const newDismissed = !guideDismissed;
38
+ setGuideDismissed(newDismissed);
39
+ // If re-enabling, also ensure active is true
40
+ const patch: Record<string, boolean> = { dismissed: newDismissed };
41
+ if (!newDismissed) patch.active = true;
42
+ fetch('/api/setup', {
43
+ method: 'PATCH',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify({ guideState: patch }),
46
+ })
47
+ .then(() => window.dispatchEvent(new Event('guide-state-updated')))
48
+ .catch(() => setGuideDismissed(!newDismissed)); // rollback on failure
49
+ }, [guideDismissed]);
50
+
19
51
  const origin = useSyncExternalStore(
20
52
  () => () => {},
21
53
  () => `${window.location.protocol}//${window.location.hostname}`,
@@ -158,6 +190,36 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
158
190
  )}
159
191
  </div>
160
192
  </Field>
193
+
194
+ {/* Getting Started Guide toggle */}
195
+ {guideActive !== null && (
196
+ <div className="border-t border-border pt-5">
197
+ <SectionLabel>{t.guide?.title ?? 'Getting Started'}</SectionLabel>
198
+ <div className="flex items-center justify-between py-2">
199
+ <div className="flex items-center gap-2">
200
+ <Sparkles size={14} style={{ color: 'var(--amber)' }} />
201
+ <div>
202
+ <div className="text-sm text-foreground">{t.guide?.showGuide ?? 'Show getting started guide'}</div>
203
+ </div>
204
+ </div>
205
+ <button
206
+ type="button"
207
+ role="switch"
208
+ aria-checked={!guideDismissed}
209
+ onClick={handleGuideToggle}
210
+ className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
211
+ !guideDismissed ? 'bg-amber-500' : 'bg-muted'
212
+ }`}
213
+ >
214
+ <span
215
+ className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
216
+ !guideDismissed ? 'translate-x-4' : 'translate-x-0'
217
+ }`}
218
+ />
219
+ </button>
220
+ </div>
221
+ </div>
222
+ )}
161
223
  </div>
162
224
  );
163
225
  }