@geminilight/mindos 0.5.69 → 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/file/import/route.ts +197 -0
- 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 +5 -7
- package/app/components/CreateSpaceModal.tsx +31 -6
- package/app/components/FileTree.tsx +68 -11
- package/app/components/GuideCard.tsx +197 -131
- package/app/components/HomeContent.tsx +85 -18
- package/app/components/ImportModal.tsx +415 -0
- package/app/components/OnboardingView.tsx +9 -0
- package/app/components/Panel.tsx +4 -2
- package/app/components/SidebarLayout.tsx +96 -8
- package/app/components/SpaceInitToast.tsx +173 -0
- package/app/components/TableOfContents.tsx +1 -0
- package/app/components/agents/AgentDetailContent.tsx +69 -45
- package/app/components/agents/AgentsContentPage.tsx +2 -1
- package/app/components/agents/AgentsMcpSection.tsx +16 -12
- package/app/components/agents/AgentsOverviewSection.tsx +37 -36
- package/app/components/agents/AgentsPrimitives.tsx +41 -20
- package/app/components/agents/AgentsSkillsSection.tsx +16 -7
- package/app/components/agents/SkillDetailPopover.tsx +11 -11
- package/app/components/agents/agents-content-model.ts +16 -8
- package/app/components/ask/AskContent.tsx +148 -50
- package/app/components/ask/MentionPopover.tsx +16 -8
- package/app/components/ask/SlashCommandPopover.tsx +62 -0
- package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -6
- package/app/components/panels/AgentsPanelHubNav.tsx +3 -3
- package/app/components/panels/DiscoverPanel.tsx +88 -2
- package/app/components/settings/KnowledgeTab.tsx +61 -0
- package/app/components/walkthrough/steps.ts +11 -6
- package/app/hooks/useFileImport.ts +191 -0
- package/app/hooks/useFileUpload.ts +11 -0
- 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 +252 -0
- package/app/lib/core/create-space.ts +11 -4
- package/app/lib/core/file-convert.ts +97 -0
- package/app/lib/core/index.ts +1 -1
- package/app/lib/core/organize.ts +105 -0
- package/app/lib/i18n-en.ts +102 -46
- package/app/lib/i18n-zh.ts +101 -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,23 @@ 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();
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const handler = (e: Event) => {
|
|
188
|
+
const files = (e as CustomEvent).detail?.files;
|
|
189
|
+
if (Array.isArray(files) && files.length > 0) {
|
|
190
|
+
upload.injectFiles(files);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
window.addEventListener('mindos:inject-ask-files', handler);
|
|
194
|
+
return () => window.removeEventListener('mindos:inject-ask-files', handler);
|
|
195
|
+
}, [upload]);
|
|
179
196
|
|
|
180
197
|
// Focus and init session when becoming visible (edge-triggered for panel, level-triggered for modal)
|
|
181
198
|
const prevVisibleRef = useRef(false);
|
|
@@ -192,6 +209,8 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
192
209
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
193
210
|
upload.clearAttachments();
|
|
194
211
|
mention.resetMention();
|
|
212
|
+
slash.resetSlash();
|
|
213
|
+
setSelectedSkill(null);
|
|
195
214
|
setShowHistory(false);
|
|
196
215
|
} else if (!visible && variant === 'modal') {
|
|
197
216
|
// Modal: abort streaming on close
|
|
@@ -215,12 +234,13 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
215
234
|
const handler = (e: KeyboardEvent) => {
|
|
216
235
|
if (e.key === 'Escape') {
|
|
217
236
|
if (mention.mentionQuery !== null) { mention.resetMention(); return; }
|
|
237
|
+
if (slash.slashQuery !== null) { slash.resetSlash(); return; }
|
|
218
238
|
onClose();
|
|
219
239
|
}
|
|
220
240
|
};
|
|
221
241
|
window.addEventListener('keydown', handler);
|
|
222
242
|
return () => window.removeEventListener('keydown', handler);
|
|
223
|
-
}, [variant, visible, onClose, mention]);
|
|
243
|
+
}, [variant, visible, onClose, mention, slash]);
|
|
224
244
|
|
|
225
245
|
useEffect(() => {
|
|
226
246
|
if (!isPanel) return;
|
|
@@ -241,29 +261,67 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
241
261
|
}, [input, isPanel, isLoading, visible, panelComposerHeight]);
|
|
242
262
|
|
|
243
263
|
const mentionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
244
|
-
const
|
|
264
|
+
const slashTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
265
|
+
const handleInputChange = useCallback((val: string, cursorPos?: number) => {
|
|
245
266
|
setInput(val);
|
|
267
|
+
const pos = cursorPos ?? val.length;
|
|
246
268
|
if (mentionTimerRef.current) clearTimeout(mentionTimerRef.current);
|
|
247
|
-
|
|
248
|
-
|
|
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]);
|
|
249
273
|
|
|
250
274
|
useEffect(() => {
|
|
251
|
-
return () => {
|
|
275
|
+
return () => {
|
|
276
|
+
if (mentionTimerRef.current) clearTimeout(mentionTimerRef.current);
|
|
277
|
+
if (slashTimerRef.current) clearTimeout(slashTimerRef.current);
|
|
278
|
+
};
|
|
252
279
|
}, []);
|
|
253
280
|
|
|
254
281
|
const selectMention = useCallback((filePath: string) => {
|
|
255
|
-
const
|
|
256
|
-
|
|
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);
|
|
257
290
|
mention.resetMention();
|
|
258
291
|
if (!attachedFiles.includes(filePath)) {
|
|
259
292
|
setAttachedFiles(prev => [...prev, filePath]);
|
|
260
293
|
}
|
|
261
|
-
setTimeout(() =>
|
|
294
|
+
setTimeout(() => {
|
|
295
|
+
inputRef.current?.focus();
|
|
296
|
+
inputRef.current?.setSelectionRange(atIdx, atIdx);
|
|
297
|
+
}, 0);
|
|
262
298
|
}, [input, attachedFiles, mention]);
|
|
263
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
|
+
|
|
264
317
|
const handleInputKeyDown = useCallback(
|
|
265
318
|
(e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
266
319
|
if (mention.mentionQuery !== null) {
|
|
320
|
+
if (e.key === 'Escape') {
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
mention.resetMention();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
267
325
|
if (e.key === 'ArrowDown') {
|
|
268
326
|
e.preventDefault();
|
|
269
327
|
mention.navigateMention('down');
|
|
@@ -279,29 +337,51 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
279
337
|
}
|
|
280
338
|
return;
|
|
281
339
|
}
|
|
282
|
-
|
|
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
|
+
}
|
|
283
361
|
if (variant === 'panel' && e.key === 'Enter' && !e.shiftKey && !isLoading && input.trim()) {
|
|
284
362
|
e.preventDefault();
|
|
285
363
|
(e.currentTarget as HTMLTextAreaElement).form?.requestSubmit();
|
|
286
364
|
}
|
|
287
365
|
},
|
|
288
|
-
[mention, selectMention, variant, isLoading, input],
|
|
366
|
+
[mention, selectMention, slash, selectSlashCommand, variant, isLoading, input],
|
|
289
367
|
);
|
|
290
368
|
|
|
291
369
|
const handleStop = useCallback(() => { abortRef.current?.abort(); }, []);
|
|
292
370
|
|
|
293
371
|
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
|
294
372
|
e.preventDefault();
|
|
295
|
-
if (mention.mentionQuery !== null) return;
|
|
373
|
+
if (mention.mentionQuery !== null || slash.slashQuery !== null) return;
|
|
296
374
|
const text = input.trim();
|
|
297
375
|
if (!text || isLoading) return;
|
|
298
376
|
|
|
299
|
-
|
|
300
|
-
|
|
377
|
+
const content = selectedSkill
|
|
378
|
+
? `Use the skill ${selectedSkill.name}: ${text}`
|
|
379
|
+
: text;
|
|
380
|
+
const userMsg: Message = { role: 'user', content, timestamp: Date.now() };
|
|
301
381
|
const requestMessages = [...session.messages, userMsg];
|
|
302
|
-
// And for the incoming assistant response, give it an initial timestamp
|
|
303
382
|
session.setMessages([...requestMessages, { role: 'assistant', content: '', timestamp: Date.now() }]);
|
|
304
383
|
setInput('');
|
|
384
|
+
setSelectedSkill(null);
|
|
305
385
|
if (onFirstMessage && !firstMessageFired.current) {
|
|
306
386
|
firstMessageFired.current = true;
|
|
307
387
|
onFirstMessage();
|
|
@@ -399,7 +479,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
399
479
|
setIsLoading(false);
|
|
400
480
|
abortRef.current = null;
|
|
401
481
|
}
|
|
402
|
-
}, [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]);
|
|
403
483
|
|
|
404
484
|
const handleResetSession = useCallback(() => {
|
|
405
485
|
if (isLoading) return;
|
|
@@ -408,9 +488,11 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
408
488
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
409
489
|
upload.clearAttachments();
|
|
410
490
|
mention.resetMention();
|
|
491
|
+
slash.resetSlash();
|
|
492
|
+
setSelectedSkill(null);
|
|
411
493
|
setShowHistory(false);
|
|
412
494
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
413
|
-
}, [isLoading, currentFile, session, upload, mention]);
|
|
495
|
+
}, [isLoading, currentFile, session, upload, mention, slash]);
|
|
414
496
|
|
|
415
497
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
416
498
|
if (e.dataTransfer.types.includes('text/mindos-path')) {
|
|
@@ -438,8 +520,10 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
438
520
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
439
521
|
upload.clearAttachments();
|
|
440
522
|
mention.resetMention();
|
|
523
|
+
slash.resetSlash();
|
|
524
|
+
setSelectedSkill(null);
|
|
441
525
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
442
|
-
}, [session, currentFile, upload, mention]);
|
|
526
|
+
}, [session, currentFile, upload, mention, slash]);
|
|
443
527
|
|
|
444
528
|
const iconSize = isPanel ? 13 : 14;
|
|
445
529
|
const inputIconSize = isPanel ? 14 : 15;
|
|
@@ -509,6 +593,27 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
509
593
|
labels={{ connecting: t.ask.connecting, thinking: t.ask.thinking, generating: t.ask.generating }}
|
|
510
594
|
/>
|
|
511
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
|
+
|
|
512
617
|
{/* Input area — panel: fixed-height shell + top drag handle (persisted); modal: simple block */}
|
|
513
618
|
<div
|
|
514
619
|
className={cn(
|
|
@@ -547,7 +652,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
547
652
|
{attachedFiles.length > 0 && (
|
|
548
653
|
<div className={cn('shrink-0', isPanel ? 'px-3 pt-2 pb-1' : 'px-4 pt-2.5 pb-1')}>
|
|
549
654
|
<div className={`text-muted-foreground/70 mb-1 ${isPanel ? 'text-[10px]' : 'text-xs'}`}>
|
|
550
|
-
{
|
|
655
|
+
{t.ask.attachFile}
|
|
551
656
|
</div>
|
|
552
657
|
<div className={`flex flex-wrap ${isPanel ? 'gap-1' : 'gap-1.5'}`}>
|
|
553
658
|
{attachedFiles.map(f => (
|
|
@@ -560,7 +665,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
560
665
|
{upload.localAttachments.length > 0 && (
|
|
561
666
|
<div className={cn('shrink-0', isPanel ? 'px-3 pb-1' : 'px-4 pb-1')}>
|
|
562
667
|
<div className={`text-muted-foreground/70 mb-1 ${isPanel ? 'text-[10px]' : 'text-xs'}`}>
|
|
563
|
-
{
|
|
668
|
+
{t.ask.uploadedFiles}
|
|
564
669
|
</div>
|
|
565
670
|
<div className={`flex flex-wrap ${isPanel ? 'gap-1' : 'gap-1.5'}`}>
|
|
566
671
|
{upload.localAttachments.map((f, idx) => (
|
|
@@ -570,16 +675,25 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
570
675
|
</div>
|
|
571
676
|
)}
|
|
572
677
|
|
|
573
|
-
{
|
|
574
|
-
<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>
|
|
575
693
|
)}
|
|
576
694
|
|
|
577
|
-
{
|
|
578
|
-
<
|
|
579
|
-
results={mention.mentionResults}
|
|
580
|
-
selectedIndex={mention.mentionIndex}
|
|
581
|
-
onSelect={selectMention}
|
|
582
|
-
/>
|
|
695
|
+
{upload.uploadError && (
|
|
696
|
+
<div className={cn('shrink-0 pb-1 text-xs text-error', isPanel ? 'px-3' : 'px-4')}>{upload.uploadError}</div>
|
|
583
697
|
)}
|
|
584
698
|
|
|
585
699
|
<form
|
|
@@ -607,32 +721,13 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
607
721
|
}}
|
|
608
722
|
/>
|
|
609
723
|
|
|
610
|
-
<button
|
|
611
|
-
type="button"
|
|
612
|
-
onClick={() => {
|
|
613
|
-
const el = inputRef.current;
|
|
614
|
-
if (!el) return;
|
|
615
|
-
const pos = el.selectionStart ?? input.length;
|
|
616
|
-
const newVal = input.slice(0, pos) + '@' + input.slice(pos);
|
|
617
|
-
handleInputChange(newVal);
|
|
618
|
-
setTimeout(() => {
|
|
619
|
-
el.focus();
|
|
620
|
-
el.setSelectionRange(pos + 1, pos + 1);
|
|
621
|
-
}, 0);
|
|
622
|
-
}}
|
|
623
|
-
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
|
|
624
|
-
title="@ mention file"
|
|
625
|
-
>
|
|
626
|
-
<AtSign size={inputIconSize} />
|
|
627
|
-
</button>
|
|
628
|
-
|
|
629
724
|
{isPanel ? (
|
|
630
725
|
<textarea
|
|
631
726
|
ref={(el) => {
|
|
632
727
|
inputRef.current = el;
|
|
633
728
|
}}
|
|
634
729
|
value={input}
|
|
635
|
-
onChange={e => handleInputChange(e.target.value)}
|
|
730
|
+
onChange={e => handleInputChange(e.target.value, e.target.selectionStart ?? undefined)}
|
|
636
731
|
onKeyDown={handleInputKeyDown}
|
|
637
732
|
placeholder={t.ask.placeholder}
|
|
638
733
|
rows={1}
|
|
@@ -644,7 +739,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
644
739
|
inputRef.current = el;
|
|
645
740
|
}}
|
|
646
741
|
value={input}
|
|
647
|
-
onChange={e => handleInputChange(e.target.value)}
|
|
742
|
+
onChange={e => handleInputChange(e.target.value, e.target.selectionStart ?? undefined)}
|
|
648
743
|
onKeyDown={handleInputKeyDown}
|
|
649
744
|
placeholder={t.ask.placeholder}
|
|
650
745
|
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none min-w-0"
|
|
@@ -656,7 +751,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
656
751
|
<StopCircle size={inputIconSize} />
|
|
657
752
|
</button>
|
|
658
753
|
) : (
|
|
659
|
-
<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)]">
|
|
660
755
|
<Send size={isPanel ? 13 : 14} />
|
|
661
756
|
</button>
|
|
662
757
|
)}
|
|
@@ -685,6 +780,9 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
685
780
|
<span suppressHydrationWarning>
|
|
686
781
|
<kbd className="font-mono">@</kbd> {t.ask.attachFile}
|
|
687
782
|
</span>
|
|
783
|
+
<span suppressHydrationWarning>
|
|
784
|
+
<kbd className="font-mono">/</kbd> {t.ask.skillsHint}
|
|
785
|
+
</span>
|
|
688
786
|
{!isPanel && (
|
|
689
787
|
<span suppressHydrationWarning>
|
|
690
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
|
+
}
|
|
@@ -34,13 +34,14 @@ export function AgentsPanelAgentGroups({
|
|
|
34
34
|
}) {
|
|
35
35
|
return (
|
|
36
36
|
<div>
|
|
37
|
-
<div className="px-0 py-1 mb-
|
|
38
|
-
<span className="text-2xs font-semibold text-muted-foreground uppercase tracking-wider">{p.rosterLabel}</span>
|
|
37
|
+
<div className="px-0 py-1.5 mb-1">
|
|
38
|
+
<span className="text-2xs font-semibold text-muted-foreground/70 uppercase tracking-wider">{p.rosterLabel}</span>
|
|
39
39
|
</div>
|
|
40
40
|
{connected.length > 0 && (
|
|
41
41
|
<section className="mb-3">
|
|
42
|
-
<h3 className="text-[11px] font-medium text-muted-foreground/
|
|
43
|
-
|
|
42
|
+
<h3 className="flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground/80 uppercase tracking-wider mb-2 pl-0.5">
|
|
43
|
+
<span className="w-1 h-3 rounded-full bg-[var(--success)]/50" aria-hidden="true" />
|
|
44
|
+
{p.sectionConnected} <span className="text-muted-foreground/50 tabular-nums">({connected.length})</span>
|
|
44
45
|
</h3>
|
|
45
46
|
<div className="space-y-1.5">
|
|
46
47
|
{connected.map(agent => (
|
|
@@ -60,8 +61,9 @@ export function AgentsPanelAgentGroups({
|
|
|
60
61
|
|
|
61
62
|
{detected.length > 0 && (
|
|
62
63
|
<section className="mb-3">
|
|
63
|
-
<h3 className="text-[11px] font-medium text-muted-foreground/
|
|
64
|
-
|
|
64
|
+
<h3 className="flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground/80 uppercase tracking-wider mb-2 pl-0.5">
|
|
65
|
+
<span className="w-1 h-3 rounded-full bg-[var(--amber)]/50" aria-hidden="true" />
|
|
66
|
+
{p.sectionDetected} <span className="text-muted-foreground/50 tabular-nums">({detected.length})</span>
|
|
65
67
|
</h3>
|
|
66
68
|
<div className="space-y-1.5">
|
|
67
69
|
{detected.map(agent => (
|
|
@@ -27,18 +27,18 @@ export function AgentsPanelHubNav({
|
|
|
27
27
|
<PanelNavRow
|
|
28
28
|
icon={<LayoutDashboard size={14} className="text-[var(--amber)]" />}
|
|
29
29
|
title={copy.navOverview}
|
|
30
|
-
badge={<span className="text-2xs tabular-nums text-muted-foreground">{connectedCount}</span>}
|
|
30
|
+
badge={<span className="text-2xs tabular-nums text-muted-foreground/60 px-1.5 py-0.5 rounded bg-muted/40 font-medium">{connectedCount}</span>}
|
|
31
31
|
href="/agents"
|
|
32
32
|
active={inAgentsRoute && (tab === null || tab === 'overview')}
|
|
33
33
|
/>
|
|
34
34
|
<PanelNavRow
|
|
35
|
-
icon={<Server size={14} className=
|
|
35
|
+
icon={<Server size={14} className={inAgentsRoute && tab === 'mcp' ? 'text-[var(--amber)]' : 'text-muted-foreground'} />}
|
|
36
36
|
title={copy.navMcp}
|
|
37
37
|
href="/agents?tab=mcp"
|
|
38
38
|
active={inAgentsRoute && tab === 'mcp'}
|
|
39
39
|
/>
|
|
40
40
|
<PanelNavRow
|
|
41
|
-
icon={<Zap size={14} className=
|
|
41
|
+
icon={<Zap size={14} className={inAgentsRoute && tab === 'skills' ? 'text-[var(--amber)]' : 'text-muted-foreground'} />}
|
|
42
42
|
title={copy.navSkills}
|
|
43
43
|
href="/agents?tab=skills"
|
|
44
44
|
active={inAgentsRoute && tab === 'skills'}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { Lightbulb, Blocks, Zap, LayoutTemplate, User, Download, RefreshCw, Repeat, Rocket, Search, Handshake, ShieldCheck, ChevronDown } from 'lucide-react';
|
|
4
6
|
import PanelHeader from './PanelHeader';
|
|
5
7
|
import { PanelNavRow, ComingSoonBadge } from './PanelNavRow';
|
|
6
8
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
9
|
import { useCases } from '@/components/explore/use-cases';
|
|
8
10
|
import { openAskModal } from '@/hooks/useAskModal';
|
|
11
|
+
import { getPluginRenderers, isRendererEnabled, setRendererEnabled, loadDisabledState } from '@/lib/renderers/registry';
|
|
12
|
+
import { Toggle } from '../settings/Primitives';
|
|
9
13
|
|
|
10
14
|
interface DiscoverPanelProps {
|
|
11
15
|
active: boolean;
|
|
@@ -56,8 +60,44 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
|
|
|
56
60
|
const { t } = useLocale();
|
|
57
61
|
const d = t.panels.discover;
|
|
58
62
|
const e = t.explore;
|
|
63
|
+
const p = t.panels.plugins;
|
|
64
|
+
const router = useRouter();
|
|
65
|
+
|
|
66
|
+
const [pluginsMounted, setPluginsMounted] = useState(false);
|
|
67
|
+
const [showPlugins, setShowPlugins] = useState(false);
|
|
68
|
+
const [, forceUpdate] = useState(0);
|
|
69
|
+
const [existingFiles, setExistingFiles] = useState<Set<string>>(new Set());
|
|
70
|
+
const fetchedRef = useRef(false);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
loadDisabledState();
|
|
74
|
+
setPluginsMounted(true);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!pluginsMounted || fetchedRef.current) return;
|
|
79
|
+
fetchedRef.current = true;
|
|
80
|
+
const entryPaths = getPluginRenderers().map(r => r.entryPath).filter((ep): ep is string => !!ep);
|
|
81
|
+
if (entryPaths.length === 0) return;
|
|
82
|
+
fetch('/api/files')
|
|
83
|
+
.then(r => r.ok ? r.json() : [])
|
|
84
|
+
.then((allPaths: string[]) => {
|
|
85
|
+
const pathSet = new Set(allPaths);
|
|
86
|
+
setExistingFiles(new Set(entryPaths.filter(ep => pathSet.has(ep))));
|
|
87
|
+
})
|
|
88
|
+
.catch(() => {});
|
|
89
|
+
}, [pluginsMounted]);
|
|
90
|
+
|
|
91
|
+
const handleToggle = useCallback((id: string, enabled: boolean) => {
|
|
92
|
+
setRendererEnabled(id, enabled);
|
|
93
|
+
forceUpdate(n => n + 1);
|
|
94
|
+
window.dispatchEvent(new Event('renderer-state-changed'));
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const handleOpenPlugin = useCallback((entryPath: string) => {
|
|
98
|
+
router.push(`/view/${entryPath.split('/').map(encodeURIComponent).join('/')}`);
|
|
99
|
+
}, [router]);
|
|
59
100
|
|
|
60
|
-
/** Type-safe lookup for use case i18n */
|
|
61
101
|
const getUseCaseText = (id: string): { title: string; prompt: string } | undefined => {
|
|
62
102
|
const map: Record<string, { title: string; desc: string; prompt: string }> = {
|
|
63
103
|
c1: e.c1, c2: e.c2, c3: e.c3, c4: e.c4, c5: e.c5,
|
|
@@ -66,6 +106,9 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
|
|
|
66
106
|
return map[id];
|
|
67
107
|
};
|
|
68
108
|
|
|
109
|
+
const renderers = pluginsMounted ? getPluginRenderers() : [];
|
|
110
|
+
const enabledCount = pluginsMounted ? renderers.filter(r => isRendererEnabled(r.id)).length : 0;
|
|
111
|
+
|
|
69
112
|
return (
|
|
70
113
|
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
71
114
|
<PanelHeader title={d.title} maximized={maximized} onMaximize={onMaximize} />
|
|
@@ -97,6 +140,49 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
|
|
|
97
140
|
|
|
98
141
|
<div className="mx-4 border-t border-border" />
|
|
99
142
|
|
|
143
|
+
{/* Installed extensions (merged from Plugins panel) */}
|
|
144
|
+
<div className="py-2">
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={() => setShowPlugins(v => !v)}
|
|
148
|
+
className="w-full flex items-center gap-1.5 px-4 py-1.5 text-left"
|
|
149
|
+
>
|
|
150
|
+
<ChevronDown size={11} className={`text-muted-foreground transition-transform duration-150 ${showPlugins ? '' : '-rotate-90'}`} />
|
|
151
|
+
<Blocks size={13} className="text-muted-foreground shrink-0" />
|
|
152
|
+
<span className="text-2xs font-medium text-muted-foreground uppercase tracking-wider flex-1">
|
|
153
|
+
{p.title}
|
|
154
|
+
</span>
|
|
155
|
+
<span className="text-2xs text-muted-foreground tabular-nums">{enabledCount}/{renderers.length}</span>
|
|
156
|
+
</button>
|
|
157
|
+
{showPlugins && renderers.map(r => {
|
|
158
|
+
const enabled = isRendererEnabled(r.id);
|
|
159
|
+
const fileExists = r.entryPath ? existingFiles.has(r.entryPath) : false;
|
|
160
|
+
const canOpen = enabled && r.entryPath && fileExists;
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
key={r.id}
|
|
164
|
+
className={`flex items-center gap-2 px-4 py-1.5 mx-1 rounded-sm transition-colors ${canOpen ? 'cursor-pointer hover:bg-muted/50' : ''} ${!enabled ? 'opacity-50' : ''}`}
|
|
165
|
+
onClick={canOpen ? () => handleOpenPlugin(r.entryPath!) : undefined}
|
|
166
|
+
role={canOpen ? 'link' : undefined}
|
|
167
|
+
tabIndex={canOpen ? 0 : undefined}
|
|
168
|
+
onKeyDown={canOpen ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleOpenPlugin(r.entryPath!); } } : undefined}
|
|
169
|
+
>
|
|
170
|
+
<span className="text-sm shrink-0" suppressHydrationWarning>{r.icon}</span>
|
|
171
|
+
<span className="text-xs text-foreground truncate flex-1">{r.name}</span>
|
|
172
|
+
{r.core ? (
|
|
173
|
+
<span className="text-2xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{p.core}</span>
|
|
174
|
+
) : (
|
|
175
|
+
<div onClick={e => e.stopPropagation()}>
|
|
176
|
+
<Toggle checked={enabled} onChange={v => handleToggle(r.id, v)} size="sm" />
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="mx-4 border-t border-border" />
|
|
185
|
+
|
|
100
186
|
{/* Quick try — use case list */}
|
|
101
187
|
<div className="py-2">
|
|
102
188
|
<div className="px-4 py-1.5">
|