@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.
- package/app/app/api/ask/route.ts +122 -92
- package/app/app/api/mcp/agents/route.ts +53 -2
- package/app/app/api/mcp/status/route.ts +1 -1
- package/app/app/api/skills/route.ts +10 -114
- package/app/components/ActivityBar.tsx +3 -4
- package/app/components/CreateSpaceModal.tsx +31 -6
- package/app/components/FileTree.tsx +33 -2
- package/app/components/GuideCard.tsx +197 -131
- package/app/components/HomeContent.tsx +85 -18
- package/app/components/SidebarLayout.tsx +13 -0
- package/app/components/SpaceInitToast.tsx +173 -0
- package/app/components/agents/AgentDetailContent.tsx +32 -17
- package/app/components/agents/AgentsContentPage.tsx +2 -1
- package/app/components/agents/AgentsOverviewSection.tsx +1 -14
- package/app/components/agents/agents-content-model.ts +16 -8
- package/app/components/ask/AskContent.tsx +137 -50
- package/app/components/ask/MentionPopover.tsx +16 -8
- package/app/components/ask/SlashCommandPopover.tsx +62 -0
- package/app/components/settings/KnowledgeTab.tsx +61 -0
- package/app/components/walkthrough/steps.ts +11 -6
- package/app/hooks/useMention.ts +14 -6
- package/app/hooks/useSlashCommand.ts +114 -0
- package/app/lib/actions.ts +79 -2
- package/app/lib/agent/index.ts +1 -1
- package/app/lib/agent/prompt.ts +2 -0
- package/app/lib/agent/tools.ts +106 -0
- package/app/lib/core/create-space.ts +11 -4
- package/app/lib/core/index.ts +1 -1
- package/app/lib/i18n-en.ts +51 -46
- package/app/lib/i18n-zh.ts +50 -45
- package/app/lib/mcp-agents.ts +8 -0
- package/app/lib/pdf-extract.ts +33 -0
- package/app/lib/pi-integration/extensions.ts +45 -0
- package/app/lib/pi-integration/mcporter.ts +219 -0
- package/app/lib/pi-integration/session-store.ts +62 -0
- package/app/lib/pi-integration/skills.ts +116 -0
- package/app/lib/settings.ts +1 -1
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +1 -1
- package/app/package.json +2 -0
- package/mcp/src/index.ts +29 -0
- package/package.json +1 -1
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
|
|
4
|
-
import { Sparkles, Send,
|
|
4
|
+
import { Sparkles, Send, Paperclip, StopCircle, RotateCcw, History, X, Zap, Maximize2, Minimize2, PanelRight, AppWindow } from 'lucide-react';
|
|
5
5
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
6
|
import type { Message } from '@/lib/types';
|
|
7
7
|
import { useAskSession } from '@/hooks/useAskSession';
|
|
8
8
|
import { useFileUpload } from '@/hooks/useFileUpload';
|
|
9
9
|
import { useMention } from '@/hooks/useMention';
|
|
10
|
+
import { useSlashCommand } from '@/hooks/useSlashCommand';
|
|
11
|
+
import type { SlashItem } from '@/hooks/useSlashCommand';
|
|
10
12
|
import MessageList from '@/components/ask/MessageList';
|
|
11
13
|
import MentionPopover from '@/components/ask/MentionPopover';
|
|
14
|
+
import SlashCommandPopover from '@/components/ask/SlashCommandPopover';
|
|
12
15
|
import SessionHistory from '@/components/ask/SessionHistory';
|
|
13
16
|
import FileChip from '@/components/ask/FileChip';
|
|
14
17
|
import { consumeUIMessageStream } from '@/lib/agent/stream-consumer';
|
|
@@ -173,9 +176,12 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
173
176
|
const [showHistory, setShowHistory] = useState(false);
|
|
174
177
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
175
178
|
|
|
179
|
+
const [selectedSkill, setSelectedSkill] = useState<SlashItem | null>(null);
|
|
180
|
+
|
|
176
181
|
const session = useAskSession(currentFile);
|
|
177
182
|
const upload = useFileUpload();
|
|
178
183
|
const mention = useMention();
|
|
184
|
+
const slash = useSlashCommand();
|
|
179
185
|
|
|
180
186
|
useEffect(() => {
|
|
181
187
|
const handler = (e: Event) => {
|
|
@@ -203,6 +209,8 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
203
209
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
204
210
|
upload.clearAttachments();
|
|
205
211
|
mention.resetMention();
|
|
212
|
+
slash.resetSlash();
|
|
213
|
+
setSelectedSkill(null);
|
|
206
214
|
setShowHistory(false);
|
|
207
215
|
} else if (!visible && variant === 'modal') {
|
|
208
216
|
// Modal: abort streaming on close
|
|
@@ -226,12 +234,13 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
226
234
|
const handler = (e: KeyboardEvent) => {
|
|
227
235
|
if (e.key === 'Escape') {
|
|
228
236
|
if (mention.mentionQuery !== null) { mention.resetMention(); return; }
|
|
237
|
+
if (slash.slashQuery !== null) { slash.resetSlash(); return; }
|
|
229
238
|
onClose();
|
|
230
239
|
}
|
|
231
240
|
};
|
|
232
241
|
window.addEventListener('keydown', handler);
|
|
233
242
|
return () => window.removeEventListener('keydown', handler);
|
|
234
|
-
}, [variant, visible, onClose, mention]);
|
|
243
|
+
}, [variant, visible, onClose, mention, slash]);
|
|
235
244
|
|
|
236
245
|
useEffect(() => {
|
|
237
246
|
if (!isPanel) return;
|
|
@@ -252,29 +261,67 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
252
261
|
}, [input, isPanel, isLoading, visible, panelComposerHeight]);
|
|
253
262
|
|
|
254
263
|
const mentionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
255
|
-
const
|
|
264
|
+
const slashTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
265
|
+
const handleInputChange = useCallback((val: string, cursorPos?: number) => {
|
|
256
266
|
setInput(val);
|
|
267
|
+
const pos = cursorPos ?? val.length;
|
|
257
268
|
if (mentionTimerRef.current) clearTimeout(mentionTimerRef.current);
|
|
258
|
-
|
|
259
|
-
|
|
269
|
+
if (slashTimerRef.current) clearTimeout(slashTimerRef.current);
|
|
270
|
+
mentionTimerRef.current = setTimeout(() => mention.updateMentionFromInput(val, pos), 80);
|
|
271
|
+
slashTimerRef.current = setTimeout(() => slash.updateSlashFromInput(val, pos), 80);
|
|
272
|
+
}, [mention, slash]);
|
|
260
273
|
|
|
261
274
|
useEffect(() => {
|
|
262
|
-
return () => {
|
|
275
|
+
return () => {
|
|
276
|
+
if (mentionTimerRef.current) clearTimeout(mentionTimerRef.current);
|
|
277
|
+
if (slashTimerRef.current) clearTimeout(slashTimerRef.current);
|
|
278
|
+
};
|
|
263
279
|
}, []);
|
|
264
280
|
|
|
265
281
|
const selectMention = useCallback((filePath: string) => {
|
|
266
|
-
const
|
|
267
|
-
|
|
282
|
+
const el = inputRef.current;
|
|
283
|
+
const cursorPos = el
|
|
284
|
+
? (el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement ? el.selectionStart ?? input.length : input.length)
|
|
285
|
+
: input.length;
|
|
286
|
+
const before = input.slice(0, cursorPos);
|
|
287
|
+
const atIdx = before.lastIndexOf('@');
|
|
288
|
+
const newVal = input.slice(0, atIdx) + input.slice(cursorPos);
|
|
289
|
+
setInput(newVal);
|
|
268
290
|
mention.resetMention();
|
|
269
291
|
if (!attachedFiles.includes(filePath)) {
|
|
270
292
|
setAttachedFiles(prev => [...prev, filePath]);
|
|
271
293
|
}
|
|
272
|
-
setTimeout(() =>
|
|
294
|
+
setTimeout(() => {
|
|
295
|
+
inputRef.current?.focus();
|
|
296
|
+
inputRef.current?.setSelectionRange(atIdx, atIdx);
|
|
297
|
+
}, 0);
|
|
273
298
|
}, [input, attachedFiles, mention]);
|
|
274
299
|
|
|
300
|
+
const selectSlashCommand = useCallback((item: SlashItem) => {
|
|
301
|
+
const el = inputRef.current;
|
|
302
|
+
const cursorPos = el
|
|
303
|
+
? (el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement ? el.selectionStart ?? input.length : input.length)
|
|
304
|
+
: input.length;
|
|
305
|
+
const before = input.slice(0, cursorPos);
|
|
306
|
+
const slashIdx = before.lastIndexOf('/');
|
|
307
|
+
const newVal = input.slice(0, slashIdx) + input.slice(cursorPos);
|
|
308
|
+
setInput(newVal);
|
|
309
|
+
setSelectedSkill(item);
|
|
310
|
+
slash.resetSlash();
|
|
311
|
+
setTimeout(() => {
|
|
312
|
+
inputRef.current?.focus();
|
|
313
|
+
inputRef.current?.setSelectionRange(slashIdx, slashIdx);
|
|
314
|
+
}, 0);
|
|
315
|
+
}, [input, slash]);
|
|
316
|
+
|
|
275
317
|
const handleInputKeyDown = useCallback(
|
|
276
318
|
(e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
277
319
|
if (mention.mentionQuery !== null) {
|
|
320
|
+
if (e.key === 'Escape') {
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
mention.resetMention();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
278
325
|
if (e.key === 'ArrowDown') {
|
|
279
326
|
e.preventDefault();
|
|
280
327
|
mention.navigateMention('down');
|
|
@@ -290,29 +337,51 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
290
337
|
}
|
|
291
338
|
return;
|
|
292
339
|
}
|
|
293
|
-
|
|
340
|
+
if (slash.slashQuery !== null) {
|
|
341
|
+
if (e.key === 'Escape') {
|
|
342
|
+
e.preventDefault();
|
|
343
|
+
slash.resetSlash();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (e.key === 'ArrowDown') {
|
|
347
|
+
e.preventDefault();
|
|
348
|
+
slash.navigateSlash('down');
|
|
349
|
+
} else if (e.key === 'ArrowUp') {
|
|
350
|
+
e.preventDefault();
|
|
351
|
+
slash.navigateSlash('up');
|
|
352
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
353
|
+
if (e.key === 'Enter' && e.shiftKey) return;
|
|
354
|
+
if (slash.slashResults.length > 0) {
|
|
355
|
+
e.preventDefault();
|
|
356
|
+
selectSlashCommand(slash.slashResults[slash.slashIndex]);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
294
361
|
if (variant === 'panel' && e.key === 'Enter' && !e.shiftKey && !isLoading && input.trim()) {
|
|
295
362
|
e.preventDefault();
|
|
296
363
|
(e.currentTarget as HTMLTextAreaElement).form?.requestSubmit();
|
|
297
364
|
}
|
|
298
365
|
},
|
|
299
|
-
[mention, selectMention, variant, isLoading, input],
|
|
366
|
+
[mention, selectMention, slash, selectSlashCommand, variant, isLoading, input],
|
|
300
367
|
);
|
|
301
368
|
|
|
302
369
|
const handleStop = useCallback(() => { abortRef.current?.abort(); }, []);
|
|
303
370
|
|
|
304
371
|
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
|
305
372
|
e.preventDefault();
|
|
306
|
-
if (mention.mentionQuery !== null) return;
|
|
373
|
+
if (mention.mentionQuery !== null || slash.slashQuery !== null) return;
|
|
307
374
|
const text = input.trim();
|
|
308
375
|
if (!text || isLoading) return;
|
|
309
376
|
|
|
310
|
-
|
|
311
|
-
|
|
377
|
+
const content = selectedSkill
|
|
378
|
+
? `Use the skill ${selectedSkill.name}: ${text}`
|
|
379
|
+
: text;
|
|
380
|
+
const userMsg: Message = { role: 'user', content, timestamp: Date.now() };
|
|
312
381
|
const requestMessages = [...session.messages, userMsg];
|
|
313
|
-
// And for the incoming assistant response, give it an initial timestamp
|
|
314
382
|
session.setMessages([...requestMessages, { role: 'assistant', content: '', timestamp: Date.now() }]);
|
|
315
383
|
setInput('');
|
|
384
|
+
setSelectedSkill(null);
|
|
316
385
|
if (onFirstMessage && !firstMessageFired.current) {
|
|
317
386
|
firstMessageFired.current = true;
|
|
318
387
|
onFirstMessage();
|
|
@@ -410,7 +479,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
410
479
|
setIsLoading(false);
|
|
411
480
|
abortRef.current = null;
|
|
412
481
|
}
|
|
413
|
-
}, [input, session, isLoading, currentFile, attachedFiles, upload.localAttachments, mention.mentionQuery, t.ask.errorNoResponse, t.ask.stopped, onFirstMessage]);
|
|
482
|
+
}, [input, session, isLoading, currentFile, attachedFiles, upload.localAttachments, mention.mentionQuery, slash.slashQuery, selectedSkill, t.ask.errorNoResponse, t.ask.stopped, onFirstMessage]);
|
|
414
483
|
|
|
415
484
|
const handleResetSession = useCallback(() => {
|
|
416
485
|
if (isLoading) return;
|
|
@@ -419,9 +488,11 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
419
488
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
420
489
|
upload.clearAttachments();
|
|
421
490
|
mention.resetMention();
|
|
491
|
+
slash.resetSlash();
|
|
492
|
+
setSelectedSkill(null);
|
|
422
493
|
setShowHistory(false);
|
|
423
494
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
424
|
-
}, [isLoading, currentFile, session, upload, mention]);
|
|
495
|
+
}, [isLoading, currentFile, session, upload, mention, slash]);
|
|
425
496
|
|
|
426
497
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
427
498
|
if (e.dataTransfer.types.includes('text/mindos-path')) {
|
|
@@ -449,8 +520,10 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
449
520
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
450
521
|
upload.clearAttachments();
|
|
451
522
|
mention.resetMention();
|
|
523
|
+
slash.resetSlash();
|
|
524
|
+
setSelectedSkill(null);
|
|
452
525
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
453
|
-
}, [session, currentFile, upload, mention]);
|
|
526
|
+
}, [session, currentFile, upload, mention, slash]);
|
|
454
527
|
|
|
455
528
|
const iconSize = isPanel ? 13 : 14;
|
|
456
529
|
const inputIconSize = isPanel ? 14 : 15;
|
|
@@ -520,6 +593,27 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
520
593
|
labels={{ connecting: t.ask.connecting, thinking: t.ask.thinking, generating: t.ask.generating }}
|
|
521
594
|
/>
|
|
522
595
|
|
|
596
|
+
{/* Popovers — rendered outside overflow containers so they can extend freely */}
|
|
597
|
+
{mention.mentionQuery !== null && mention.mentionResults.length > 0 && (
|
|
598
|
+
<div className="shrink-0 px-2 pb-1">
|
|
599
|
+
<MentionPopover
|
|
600
|
+
results={mention.mentionResults}
|
|
601
|
+
selectedIndex={mention.mentionIndex}
|
|
602
|
+
onSelect={selectMention}
|
|
603
|
+
/>
|
|
604
|
+
</div>
|
|
605
|
+
)}
|
|
606
|
+
|
|
607
|
+
{slash.slashQuery !== null && slash.slashResults.length > 0 && (
|
|
608
|
+
<div className="shrink-0 px-2 pb-1">
|
|
609
|
+
<SlashCommandPopover
|
|
610
|
+
results={slash.slashResults}
|
|
611
|
+
selectedIndex={slash.slashIndex}
|
|
612
|
+
onSelect={selectSlashCommand}
|
|
613
|
+
/>
|
|
614
|
+
</div>
|
|
615
|
+
)}
|
|
616
|
+
|
|
523
617
|
{/* Input area — panel: fixed-height shell + top drag handle (persisted); modal: simple block */}
|
|
524
618
|
<div
|
|
525
619
|
className={cn(
|
|
@@ -558,7 +652,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
558
652
|
{attachedFiles.length > 0 && (
|
|
559
653
|
<div className={cn('shrink-0', isPanel ? 'px-3 pt-2 pb-1' : 'px-4 pt-2.5 pb-1')}>
|
|
560
654
|
<div className={`text-muted-foreground/70 mb-1 ${isPanel ? 'text-[10px]' : 'text-xs'}`}>
|
|
561
|
-
{
|
|
655
|
+
{t.ask.attachFile}
|
|
562
656
|
</div>
|
|
563
657
|
<div className={`flex flex-wrap ${isPanel ? 'gap-1' : 'gap-1.5'}`}>
|
|
564
658
|
{attachedFiles.map(f => (
|
|
@@ -571,7 +665,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
571
665
|
{upload.localAttachments.length > 0 && (
|
|
572
666
|
<div className={cn('shrink-0', isPanel ? 'px-3 pb-1' : 'px-4 pb-1')}>
|
|
573
667
|
<div className={`text-muted-foreground/70 mb-1 ${isPanel ? 'text-[10px]' : 'text-xs'}`}>
|
|
574
|
-
{
|
|
668
|
+
{t.ask.uploadedFiles}
|
|
575
669
|
</div>
|
|
576
670
|
<div className={`flex flex-wrap ${isPanel ? 'gap-1' : 'gap-1.5'}`}>
|
|
577
671
|
{upload.localAttachments.map((f, idx) => (
|
|
@@ -581,16 +675,25 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
581
675
|
</div>
|
|
582
676
|
)}
|
|
583
677
|
|
|
584
|
-
{
|
|
585
|
-
<div className={cn('shrink-0
|
|
678
|
+
{selectedSkill && (
|
|
679
|
+
<div className={cn('shrink-0', isPanel ? 'px-3 pt-1.5 pb-1' : 'px-4 pt-2 pb-1')}>
|
|
680
|
+
<span className="inline-flex items-center gap-1.5 pl-2 pr-1 py-1 rounded-md text-xs bg-[var(--amber)]/10 border border-[var(--amber)]/25 text-foreground">
|
|
681
|
+
<Zap size={11} className="text-[var(--amber)] shrink-0" />
|
|
682
|
+
<span className="font-medium">{selectedSkill.name}</span>
|
|
683
|
+
<button
|
|
684
|
+
type="button"
|
|
685
|
+
onClick={() => { setSelectedSkill(null); inputRef.current?.focus(); }}
|
|
686
|
+
className="p-0.5 rounded text-muted-foreground hover:text-foreground transition-colors shrink-0"
|
|
687
|
+
aria-label={`Remove skill ${selectedSkill.name}`}
|
|
688
|
+
>
|
|
689
|
+
<X size={10} />
|
|
690
|
+
</button>
|
|
691
|
+
</span>
|
|
692
|
+
</div>
|
|
586
693
|
)}
|
|
587
694
|
|
|
588
|
-
{
|
|
589
|
-
<
|
|
590
|
-
results={mention.mentionResults}
|
|
591
|
-
selectedIndex={mention.mentionIndex}
|
|
592
|
-
onSelect={selectMention}
|
|
593
|
-
/>
|
|
695
|
+
{upload.uploadError && (
|
|
696
|
+
<div className={cn('shrink-0 pb-1 text-xs text-error', isPanel ? 'px-3' : 'px-4')}>{upload.uploadError}</div>
|
|
594
697
|
)}
|
|
595
698
|
|
|
596
699
|
<form
|
|
@@ -618,32 +721,13 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
618
721
|
}}
|
|
619
722
|
/>
|
|
620
723
|
|
|
621
|
-
<button
|
|
622
|
-
type="button"
|
|
623
|
-
onClick={() => {
|
|
624
|
-
const el = inputRef.current;
|
|
625
|
-
if (!el) return;
|
|
626
|
-
const pos = el.selectionStart ?? input.length;
|
|
627
|
-
const newVal = input.slice(0, pos) + '@' + input.slice(pos);
|
|
628
|
-
handleInputChange(newVal);
|
|
629
|
-
setTimeout(() => {
|
|
630
|
-
el.focus();
|
|
631
|
-
el.setSelectionRange(pos + 1, pos + 1);
|
|
632
|
-
}, 0);
|
|
633
|
-
}}
|
|
634
|
-
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
|
|
635
|
-
title="@ mention file"
|
|
636
|
-
>
|
|
637
|
-
<AtSign size={inputIconSize} />
|
|
638
|
-
</button>
|
|
639
|
-
|
|
640
724
|
{isPanel ? (
|
|
641
725
|
<textarea
|
|
642
726
|
ref={(el) => {
|
|
643
727
|
inputRef.current = el;
|
|
644
728
|
}}
|
|
645
729
|
value={input}
|
|
646
|
-
onChange={e => handleInputChange(e.target.value)}
|
|
730
|
+
onChange={e => handleInputChange(e.target.value, e.target.selectionStart ?? undefined)}
|
|
647
731
|
onKeyDown={handleInputKeyDown}
|
|
648
732
|
placeholder={t.ask.placeholder}
|
|
649
733
|
rows={1}
|
|
@@ -655,7 +739,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
655
739
|
inputRef.current = el;
|
|
656
740
|
}}
|
|
657
741
|
value={input}
|
|
658
|
-
onChange={e => handleInputChange(e.target.value)}
|
|
742
|
+
onChange={e => handleInputChange(e.target.value, e.target.selectionStart ?? undefined)}
|
|
659
743
|
onKeyDown={handleInputKeyDown}
|
|
660
744
|
placeholder={t.ask.placeholder}
|
|
661
745
|
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none min-w-0"
|
|
@@ -667,7 +751,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
667
751
|
<StopCircle size={inputIconSize} />
|
|
668
752
|
</button>
|
|
669
753
|
) : (
|
|
670
|
-
<button type="submit" disabled={!input.trim() || mention.mentionQuery !== null} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0 bg-[var(--amber)] text-[var(--amber-foreground)]">
|
|
754
|
+
<button type="submit" disabled={!input.trim() || mention.mentionQuery !== null || slash.slashQuery !== null} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0 bg-[var(--amber)] text-[var(--amber-foreground)]">
|
|
671
755
|
<Send size={isPanel ? 13 : 14} />
|
|
672
756
|
</button>
|
|
673
757
|
)}
|
|
@@ -696,6 +780,9 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
696
780
|
<span suppressHydrationWarning>
|
|
697
781
|
<kbd className="font-mono">@</kbd> {t.ask.attachFile}
|
|
698
782
|
</span>
|
|
783
|
+
<span suppressHydrationWarning>
|
|
784
|
+
<kbd className="font-mono">/</kbd> {t.ask.skillsHint}
|
|
785
|
+
</span>
|
|
699
786
|
{!isPanel && (
|
|
700
787
|
<span suppressHydrationWarning>
|
|
701
788
|
<kbd className="font-mono">ESC</kbd> {t.search.close}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
|
-
import { FileText, Table } from 'lucide-react';
|
|
4
|
+
import { FileText, Table, FolderOpen } from 'lucide-react';
|
|
5
5
|
|
|
6
6
|
interface MentionPopoverProps {
|
|
7
7
|
results: string[];
|
|
@@ -22,10 +22,16 @@ export default function MentionPopover({ results, selectedIndex, onSelect }: Men
|
|
|
22
22
|
if (results.length === 0) return null;
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
-
<div className="
|
|
26
|
-
<div
|
|
25
|
+
<div className="border border-border rounded-lg bg-card shadow-lg overflow-hidden">
|
|
26
|
+
<div className="px-3 py-1.5 border-b border-border flex items-center gap-1.5">
|
|
27
|
+
<FolderOpen size={11} className="text-muted-foreground/50" />
|
|
28
|
+
<span className="text-2xs font-medium text-muted-foreground/70 uppercase tracking-wider">Files</span>
|
|
29
|
+
<span className="text-2xs text-muted-foreground/40 ml-auto">{results.length}</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div ref={listRef} className="max-h-[360px] overflow-y-auto">
|
|
27
32
|
{results.map((f, idx) => {
|
|
28
33
|
const name = f.split('/').pop() ?? f;
|
|
34
|
+
const dir = f.split('/').slice(0, -1).join('/');
|
|
29
35
|
const isCsv = name.endsWith('.csv');
|
|
30
36
|
return (
|
|
31
37
|
<button
|
|
@@ -46,15 +52,17 @@ export default function MentionPopover({ results, selectedIndex, onSelect }: Men
|
|
|
46
52
|
) : (
|
|
47
53
|
<FileText size={13} className="text-muted-foreground shrink-0" />
|
|
48
54
|
)}
|
|
49
|
-
<span className="truncate flex-1">{name}</span>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
<span className="truncate font-medium flex-1">{name}</span>
|
|
56
|
+
{dir && (
|
|
57
|
+
<span className="text-2xs text-muted-foreground/40 truncate max-w-[140px] shrink-0">
|
|
58
|
+
{dir}
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
53
61
|
</button>
|
|
54
62
|
);
|
|
55
63
|
})}
|
|
56
64
|
</div>
|
|
57
|
-
<div className="px-3 py-1.5 border-t border-border flex gap-3 text-2xs text-muted-foreground/
|
|
65
|
+
<div className="px-3 py-1.5 border-t border-border flex gap-3 text-2xs text-muted-foreground/40 shrink-0">
|
|
58
66
|
<span>↑↓ navigate</span>
|
|
59
67
|
<span>↵ select</span>
|
|
60
68
|
<span>ESC dismiss</span>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { Zap } from 'lucide-react';
|
|
5
|
+
import type { SlashItem } from '@/hooks/useSlashCommand';
|
|
6
|
+
|
|
7
|
+
interface SlashCommandPopoverProps {
|
|
8
|
+
results: SlashItem[];
|
|
9
|
+
selectedIndex: number;
|
|
10
|
+
onSelect: (item: SlashItem) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function SlashCommandPopover({ results, selectedIndex, onSelect }: SlashCommandPopoverProps) {
|
|
14
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const container = listRef.current;
|
|
18
|
+
if (!container) return;
|
|
19
|
+
const selected = container.children[selectedIndex] as HTMLElement | undefined;
|
|
20
|
+
selected?.scrollIntoView({ block: 'nearest' });
|
|
21
|
+
}, [selectedIndex]);
|
|
22
|
+
|
|
23
|
+
if (results.length === 0) return null;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="border border-border rounded-lg bg-card shadow-lg overflow-hidden">
|
|
27
|
+
<div className="px-3 py-1.5 border-b border-border flex items-center gap-1.5">
|
|
28
|
+
<Zap size={11} className="text-[var(--amber)]/50" />
|
|
29
|
+
<span className="text-2xs font-medium text-muted-foreground/70 uppercase tracking-wider">Skills</span>
|
|
30
|
+
<span className="text-2xs text-muted-foreground/40 ml-auto">{results.length}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div ref={listRef} className="max-h-[360px] overflow-y-auto">
|
|
33
|
+
{results.map((item, idx) => (
|
|
34
|
+
<button
|
|
35
|
+
key={item.name}
|
|
36
|
+
type="button"
|
|
37
|
+
onMouseDown={(e) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
onSelect(item);
|
|
40
|
+
}}
|
|
41
|
+
className={`w-full flex items-center gap-2.5 px-3 py-2 text-left transition-colors ${
|
|
42
|
+
idx === selectedIndex
|
|
43
|
+
? 'bg-accent text-foreground'
|
|
44
|
+
: 'text-muted-foreground hover:bg-muted'
|
|
45
|
+
}`}
|
|
46
|
+
>
|
|
47
|
+
<Zap size={13} className="text-[var(--amber)] shrink-0" />
|
|
48
|
+
<span className="text-sm font-medium shrink-0">/{item.name}</span>
|
|
49
|
+
{item.description && (
|
|
50
|
+
<span className="text-2xs text-muted-foreground/50 truncate min-w-0 flex-1">{item.description}</span>
|
|
51
|
+
)}
|
|
52
|
+
</button>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="px-3 py-1.5 border-t border-border flex gap-3 text-2xs text-muted-foreground/40 shrink-0">
|
|
56
|
+
<span>↑↓ navigate</span>
|
|
57
|
+
<span>↵ / Tab select</span>
|
|
58
|
+
<span>ESC dismiss</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -7,11 +7,27 @@ import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
|
|
|
7
7
|
import { apiFetch } from '@/lib/api';
|
|
8
8
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
9
9
|
import { formatBytes, formatUptime } from '@/lib/format';
|
|
10
|
+
import { setShowHiddenFiles } from '@/components/FileTree';
|
|
11
|
+
import { scanExampleFilesAction, cleanupExamplesAction } from '@/lib/actions';
|
|
10
12
|
|
|
11
13
|
export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
12
14
|
const env = data.envOverrides ?? {};
|
|
13
15
|
const k = t.settings.knowledge;
|
|
14
16
|
|
|
17
|
+
// Hidden files toggle
|
|
18
|
+
const [showHidden, setShowHidden] = useState(() =>
|
|
19
|
+
typeof window !== 'undefined' && localStorage.getItem('show-hidden-files') === 'true'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Example files cleanup
|
|
23
|
+
const [exampleCount, setExampleCount] = useState<number | null>(null);
|
|
24
|
+
const [cleaningUp, setCleaningUp] = useState(false);
|
|
25
|
+
const [cleanupResult, setCleanupResult] = useState<number | null>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
scanExampleFilesAction().then(r => setExampleCount(r.files.length)).catch(() => {});
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
15
31
|
// Guide state toggle
|
|
16
32
|
const [guideActive, setGuideActive] = useState<boolean | null>(null);
|
|
17
33
|
const [guideDismissed, setGuideDismissed] = useState(false);
|
|
@@ -130,6 +146,51 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
130
146
|
/>
|
|
131
147
|
</Field>
|
|
132
148
|
|
|
149
|
+
<div className="flex items-center justify-between">
|
|
150
|
+
<div>
|
|
151
|
+
<div className="text-sm text-foreground">{k.showHiddenFiles}</div>
|
|
152
|
+
<div className="text-xs text-muted-foreground mt-0.5">{k.showHiddenFilesHint}</div>
|
|
153
|
+
</div>
|
|
154
|
+
<Toggle checked={showHidden} onChange={() => {
|
|
155
|
+
const next = !showHidden;
|
|
156
|
+
setShowHidden(next);
|
|
157
|
+
setShowHiddenFiles(next);
|
|
158
|
+
}} />
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{exampleCount !== null && exampleCount > 0 && cleanupResult === null && (
|
|
162
|
+
<div className="flex items-center justify-between">
|
|
163
|
+
<div>
|
|
164
|
+
<div className="text-sm text-foreground">{k.cleanupExamples}</div>
|
|
165
|
+
<div className="text-xs text-muted-foreground mt-0.5">{k.cleanupExamplesHint}</div>
|
|
166
|
+
</div>
|
|
167
|
+
<button
|
|
168
|
+
onClick={async () => {
|
|
169
|
+
if (!confirm(k.cleanupExamplesConfirm(exampleCount))) return;
|
|
170
|
+
setCleaningUp(true);
|
|
171
|
+
const r = await cleanupExamplesAction();
|
|
172
|
+
setCleaningUp(false);
|
|
173
|
+
if (r.success) {
|
|
174
|
+
setCleanupResult(r.deleted);
|
|
175
|
+
setExampleCount(0);
|
|
176
|
+
}
|
|
177
|
+
}}
|
|
178
|
+
disabled={cleaningUp}
|
|
179
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0 disabled:opacity-50"
|
|
180
|
+
>
|
|
181
|
+
{cleaningUp ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
|
|
182
|
+
{k.cleanupExamplesButton}
|
|
183
|
+
<span className="ml-1 tabular-nums text-2xs opacity-70">{exampleCount}</span>
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
{cleanupResult !== null && (
|
|
188
|
+
<div className="flex items-center gap-2 text-xs text-success">
|
|
189
|
+
<Check size={14} />
|
|
190
|
+
{k.cleanupExamplesDone(cleanupResult)}
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
|
|
133
194
|
<div className="border-t border-border pt-5">
|
|
134
195
|
<SectionLabel>Security</SectionLabel>
|
|
135
196
|
</div>
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/** Walkthrough step anchors — these data-walkthrough attributes are added to target components */
|
|
2
2
|
export type WalkthroughAnchor =
|
|
3
|
-
| 'activity-bar'
|
|
4
3
|
| 'files-panel'
|
|
5
4
|
| 'ask-button'
|
|
6
|
-
| '
|
|
7
|
-
| '
|
|
5
|
+
| 'agents-panel'
|
|
6
|
+
| 'echo-panel';
|
|
8
7
|
|
|
9
8
|
export interface WalkthroughStep {
|
|
10
9
|
anchor: WalkthroughAnchor;
|
|
@@ -12,10 +11,16 @@ export interface WalkthroughStep {
|
|
|
12
11
|
position: 'right' | 'bottom';
|
|
13
12
|
}
|
|
14
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 4-step value-driven walkthrough aligned with the Dual-Layer Wedge strategy:
|
|
16
|
+
* 0. Project Memory (foundation)
|
|
17
|
+
* 1. AI That Already Knows You (wedge)
|
|
18
|
+
* 2. Multi-Agent Sharing (differentiation)
|
|
19
|
+
* 3. Echo — Cognitive Compound Interest (retention seed)
|
|
20
|
+
*/
|
|
15
21
|
export const walkthroughSteps: WalkthroughStep[] = [
|
|
16
|
-
{ anchor: 'activity-bar', position: 'right' },
|
|
17
22
|
{ anchor: 'files-panel', position: 'right' },
|
|
18
23
|
{ anchor: 'ask-button', position: 'right' },
|
|
19
|
-
{ anchor: '
|
|
20
|
-
{ anchor: '
|
|
24
|
+
{ anchor: 'agents-panel', position: 'right' },
|
|
25
|
+
{ anchor: 'echo-panel', position: 'right' },
|
|
21
26
|
];
|
package/app/hooks/useMention.ts
CHANGED
|
@@ -27,19 +27,27 @@ export function useMention() {
|
|
|
27
27
|
}, [loadFiles]);
|
|
28
28
|
|
|
29
29
|
const updateMentionFromInput = useCallback(
|
|
30
|
-
(val: string) => {
|
|
31
|
-
const
|
|
30
|
+
(val: string, cursorPos?: number) => {
|
|
31
|
+
const pos = cursorPos ?? val.length;
|
|
32
|
+
const before = val.slice(0, pos);
|
|
33
|
+
const atIdx = before.lastIndexOf('@');
|
|
32
34
|
if (atIdx === -1) {
|
|
33
35
|
setMentionQuery(null);
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
if (atIdx > 0 && before !== ' ') {
|
|
38
|
+
if (atIdx > 0 && before[atIdx - 1] !== ' ' && before[atIdx - 1] !== '\n') {
|
|
38
39
|
setMentionQuery(null);
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
|
-
const query =
|
|
42
|
-
|
|
42
|
+
const query = before.slice(atIdx + 1);
|
|
43
|
+
if (query.includes(' ') || query.includes('\n')) {
|
|
44
|
+
setMentionQuery(null);
|
|
45
|
+
setMentionResults([]);
|
|
46
|
+
setMentionIndex(0);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const q = query.toLowerCase();
|
|
50
|
+
const filtered = allFiles.filter((f) => f.toLowerCase().includes(q)).slice(0, 30);
|
|
43
51
|
if (filtered.length === 0) {
|
|
44
52
|
setMentionQuery(null);
|
|
45
53
|
setMentionResults([]);
|