@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.
- package/app/app/api/ask/route.ts +5 -4
- package/app/app/api/file/route.ts +35 -11
- package/app/app/api/setup/route.ts +64 -1
- package/app/app/api/skills/route.ts +22 -3
- package/app/app/globals.css +1 -0
- package/app/components/AskFab.tsx +49 -3
- package/app/components/AskModal.tsx +11 -2
- package/app/components/GuideCard.tsx +361 -0
- package/app/components/HomeContent.tsx +2 -2
- package/app/components/Sidebar.tsx +21 -1
- package/app/components/ask/ToolCallBlock.tsx +2 -1
- package/app/components/settings/KnowledgeTab.tsx +64 -2
- package/app/components/settings/McpTab.tsx +286 -56
- package/app/components/setup/StepAI.tsx +9 -1
- package/app/components/setup/index.tsx +4 -0
- package/app/components/setup/types.ts +2 -0
- package/app/hooks/useAskModal.ts +46 -0
- package/app/lib/agent/stream-consumer.ts +4 -2
- package/app/lib/agent/tools.ts +26 -12
- package/app/lib/fs.ts +9 -1
- package/app/lib/i18n.ts +16 -0
- package/app/lib/settings.ts +29 -0
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +7 -0
- package/bin/cli.js +135 -9
- package/bin/lib/build.js +2 -7
- package/bin/lib/mcp-spawn.js +2 -13
- package/bin/lib/utils.js +23 -0
- package/package.json +1 -1
- package/scripts/setup.js +13 -0
- package/skills/mindos/SKILL.md +10 -168
- package/skills/mindos-zh/SKILL.md +14 -172
- package/skills/project-wiki/SKILL.md +80 -74
- package/skills/project-wiki/references/file-reference.md +6 -2
- package/templates/skill-rules/en/skill-rules.md +222 -0
- package/templates/skill-rules/en/user-rules.md +20 -0
- package/templates/skill-rules/zh/skill-rules.md +222 -0
- 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
|
|
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
|
-
<
|
|
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
|
}
|