@geminilight/mindos 0.6.28 → 0.6.30
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/README.md +10 -4
- package/app/app/api/a2a/agents/route.ts +9 -0
- package/app/app/api/a2a/delegations/route.ts +9 -0
- package/app/app/api/a2a/discover/route.ts +2 -0
- package/app/app/api/a2a/route.ts +6 -6
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +114 -0
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/registry/route.ts +31 -0
- package/app/app/api/acp/session/route.ts +185 -0
- package/app/app/api/ask/route.ts +116 -13
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/layout.tsx +2 -0
- package/app/app/page.tsx +7 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/DirView.tsx +64 -2
- package/app/components/FileTree.tsx +40 -10
- package/app/components/GuideCard.tsx +7 -17
- package/app/components/HomeContent.tsx +1 -0
- package/app/components/MarkdownView.tsx +2 -0
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/SearchModal.tsx +234 -80
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/agents/AgentDetailContent.tsx +266 -52
- package/app/components/agents/AgentsContentPage.tsx +32 -6
- package/app/components/agents/AgentsPanelA2aTab.tsx +684 -0
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- package/app/components/agents/SkillDetailPopover.tsx +4 -9
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
- package/app/components/ask/AskContent.tsx +197 -239
- package/app/components/ask/FileChip.tsx +82 -17
- package/app/components/ask/MentionPopover.tsx +21 -3
- package/app/components/ask/MessageList.tsx +30 -9
- package/app/components/ask/SlashCommandPopover.tsx +21 -3
- package/app/components/help/HelpContent.tsx +9 -9
- package/app/components/panels/AgentsPanel.tsx +2 -0
- package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
- package/app/components/panels/AgentsPanelHubNav.tsx +16 -2
- package/app/components/panels/EchoPanel.tsx +5 -1
- package/app/components/panels/EchoSidebarStats.tsx +136 -0
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +177 -0
- package/app/components/renderers/workflow-yaml/index.ts +6 -0
- package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
- package/app/components/renderers/workflow-yaml/parser.ts +172 -0
- package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
- package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
- package/app/components/renderers/workflow-yaml/types.ts +46 -0
- package/app/components/settings/KnowledgeTab.tsx +3 -6
- package/app/components/settings/McpSkillsSection.tsx +4 -5
- package/app/components/settings/McpTab.tsx +6 -8
- package/app/components/setup/StepSecurity.tsx +4 -5
- package/app/components/setup/index.tsx +5 -11
- package/app/components/ui/Toaster.tsx +39 -0
- package/app/hooks/useA2aRegistry.ts +6 -1
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +120 -0
- package/app/hooks/useAcpRegistry.ts +86 -0
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useDelegationHistory.ts +49 -0
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/a2a/client.ts +49 -5
- package/app/lib/a2a/orchestrator.ts +0 -1
- package/app/lib/a2a/task-handler.ts +4 -4
- package/app/lib/a2a/types.ts +15 -0
- package/app/lib/acp/acp-tools.ts +95 -0
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +144 -0
- package/app/lib/acp/index.ts +40 -0
- package/app/lib/acp/registry.ts +202 -0
- package/app/lib/acp/session.ts +717 -0
- package/app/lib/acp/subprocess.ts +495 -0
- package/app/lib/acp/types.ts +274 -0
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +2 -1
- package/app/lib/i18n/_core.ts +22 -0
- package/app/lib/i18n/index.ts +35 -0
- package/app/lib/i18n/modules/ai-chat.ts +215 -0
- package/app/lib/i18n/modules/common.ts +71 -0
- package/app/lib/i18n/modules/features.ts +153 -0
- package/app/lib/i18n/modules/knowledge.ts +429 -0
- package/app/lib/i18n/modules/navigation.ts +153 -0
- package/app/lib/i18n/modules/onboarding.ts +523 -0
- package/app/lib/i18n/modules/panels.ts +1196 -0
- package/app/lib/i18n/modules/settings.ts +585 -0
- package/app/lib/i18n-en.ts +2 -1518
- package/app/lib/i18n-zh.ts +2 -1542
- package/app/lib/i18n.ts +3 -6
- package/app/lib/pi-integration/skills.ts +21 -6
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/settings.ts +10 -0
- package/app/lib/toast.ts +79 -0
- package/app/lib/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +3 -1
- package/bin/cli.js +25 -25
- package/bin/commands/file.js +29 -2
- package/bin/commands/space.js +249 -91
- package/package.json +1 -1
- package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
- package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
- package/app/components/renderers/workflow/manifest.ts +0 -14
|
@@ -1,31 +1,96 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { X, FileText, Table, Paperclip, ImageIcon, Zap, Bot } from 'lucide-react';
|
|
4
6
|
|
|
5
7
|
interface FileChipProps {
|
|
6
8
|
path: string;
|
|
7
9
|
onRemove: () => void;
|
|
8
|
-
variant?: 'kb' | 'upload';
|
|
10
|
+
variant?: 'kb' | 'upload' | 'image' | 'skill' | 'agent';
|
|
11
|
+
/** Base64 image data for hover preview (variant='image' only) */
|
|
12
|
+
imageData?: string;
|
|
13
|
+
/** MIME type for image preview */
|
|
14
|
+
imageMime?: string;
|
|
9
15
|
}
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
const VARIANT_ICON = {
|
|
18
|
+
kb: { icon: FileText, cls: 'text-muted-foreground' },
|
|
19
|
+
upload: { icon: Paperclip, cls: 'text-muted-foreground' },
|
|
20
|
+
image: { icon: ImageIcon, cls: 'text-muted-foreground' },
|
|
21
|
+
skill: { icon: Zap, cls: 'text-[var(--amber)]' },
|
|
22
|
+
agent: { icon: Bot, cls: 'text-muted-foreground' },
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
const VARIANT_STYLE = {
|
|
26
|
+
kb: 'border-border bg-muted text-foreground',
|
|
27
|
+
upload: 'border-border bg-muted text-foreground',
|
|
28
|
+
image: 'border-border bg-muted text-foreground',
|
|
29
|
+
skill: 'border-[var(--amber)]/25 bg-[var(--amber)]/10 text-foreground',
|
|
30
|
+
agent: 'border-[var(--amber)]/25 bg-[var(--amber)]/10 text-foreground',
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
export default function FileChip({ path, onRemove, variant = 'kb', imageData, imageMime }: FileChipProps) {
|
|
34
|
+
const [showPreview, setShowPreview] = useState(false);
|
|
35
|
+
const [mounted, setMounted] = useState(false);
|
|
36
|
+
const chipRef = useRef<HTMLSpanElement>(null);
|
|
37
|
+
const [previewPos, setPreviewPos] = useState<{ top: number; left: number } | null>(null);
|
|
38
|
+
|
|
39
|
+
useEffect(() => setMounted(true), []);
|
|
40
|
+
|
|
12
41
|
const name = path.split('/').pop() ?? path;
|
|
13
|
-
const isCsv = name.endsWith('.csv');
|
|
14
|
-
const
|
|
15
|
-
|
|
42
|
+
const isCsv = variant === 'kb' && name.endsWith('.csv');
|
|
43
|
+
const { icon: Icon, cls: iconClass } = isCsv
|
|
44
|
+
? { icon: Table, cls: 'text-success' }
|
|
45
|
+
: VARIANT_ICON[variant];
|
|
46
|
+
const style = VARIANT_STYLE[variant];
|
|
47
|
+
|
|
48
|
+
// Position preview above the chip using getBoundingClientRect
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!showPreview || !chipRef.current) { setPreviewPos(null); return; }
|
|
51
|
+
const rect = chipRef.current.getBoundingClientRect();
|
|
52
|
+
setPreviewPos({
|
|
53
|
+
top: rect.top - 8, // 8px gap above chip
|
|
54
|
+
left: rect.left,
|
|
55
|
+
});
|
|
56
|
+
}, [showPreview]);
|
|
57
|
+
|
|
58
|
+
const preview = showPreview && previewPos && imageData && imageMime ? (
|
|
59
|
+
<div
|
|
60
|
+
className="fixed z-50 p-1 rounded-lg border border-border bg-card shadow-lg pointer-events-none"
|
|
61
|
+
style={{
|
|
62
|
+
left: previewPos.left,
|
|
63
|
+
bottom: window.innerHeight - previewPos.top,
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<img
|
|
67
|
+
src={`data:${imageMime};base64,${imageData}`}
|
|
68
|
+
alt={name}
|
|
69
|
+
className="max-h-48 max-w-64 rounded object-contain"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
) : null;
|
|
16
73
|
|
|
17
74
|
return (
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
aria-label={`Remove ${name}`}
|
|
25
|
-
className="p-1 -mr-1 rounded hover:bg-muted hover:text-foreground transition-colors shrink-0"
|
|
75
|
+
<>
|
|
76
|
+
<span
|
|
77
|
+
ref={chipRef}
|
|
78
|
+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs border max-w-[220px] ${style}`}
|
|
79
|
+
onMouseEnter={() => imageData && setShowPreview(true)}
|
|
80
|
+
onMouseLeave={() => setShowPreview(false)}
|
|
26
81
|
>
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
82
|
+
<Icon size={11} className={`${iconClass} shrink-0`} />
|
|
83
|
+
<span className="truncate" title={path}>{name}</span>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={onRemove}
|
|
87
|
+
aria-label={`Remove ${name}`}
|
|
88
|
+
className="p-0.5 -mr-1 rounded hover:text-foreground transition-colors shrink-0 text-muted-foreground"
|
|
89
|
+
>
|
|
90
|
+
<X size={10} />
|
|
91
|
+
</button>
|
|
92
|
+
</span>
|
|
93
|
+
{mounted && preview && createPortal(preview, document.body)}
|
|
94
|
+
</>
|
|
30
95
|
);
|
|
31
96
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { FileText, Table, FolderOpen } from 'lucide-react';
|
|
5
5
|
import HighlightMatch from './HighlightMatch';
|
|
6
6
|
|
|
@@ -11,8 +11,26 @@ interface MentionPopoverProps {
|
|
|
11
11
|
onSelect: (filePath: string) => void;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Popover for @ file mentions. Renders as a flex child within the AskContent
|
|
16
|
+
* column (not absolutely positioned) to avoid clipping by the panel's
|
|
17
|
+
* overflow-hidden. Uses a dynamic max-height that caps at 50% of the
|
|
18
|
+
* viewport so the popover never overflows the visible area.
|
|
19
|
+
*/
|
|
14
20
|
export default function MentionPopover({ results, selectedIndex, query, onSelect }: MentionPopoverProps) {
|
|
15
21
|
const listRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
const [maxListH, setMaxListH] = useState(280);
|
|
24
|
+
|
|
25
|
+
// Dynamically compute max list height based on available space
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const container = containerRef.current;
|
|
28
|
+
if (!container) return;
|
|
29
|
+
const rect = container.getBoundingClientRect();
|
|
30
|
+
// Reserve ~80px for header + footer chrome, and cap at 50vh
|
|
31
|
+
const available = Math.min(rect.top - 8, window.innerHeight * 0.5) - 80;
|
|
32
|
+
setMaxListH(Math.max(120, Math.min(320, available)));
|
|
33
|
+
}, [results.length]);
|
|
16
34
|
|
|
17
35
|
useEffect(() => {
|
|
18
36
|
const container = listRef.current;
|
|
@@ -24,13 +42,13 @@ export default function MentionPopover({ results, selectedIndex, query, onSelect
|
|
|
24
42
|
if (results.length === 0) return null;
|
|
25
43
|
|
|
26
44
|
return (
|
|
27
|
-
<div className="border border-border rounded-lg bg-card shadow-lg overflow-hidden">
|
|
45
|
+
<div ref={containerRef} className="border border-border rounded-lg bg-card shadow-lg overflow-hidden">
|
|
28
46
|
<div className="px-3 py-1.5 border-b border-border flex items-center gap-1.5">
|
|
29
47
|
<FolderOpen size={11} className="text-muted-foreground/50" />
|
|
30
48
|
<span className="text-2xs font-medium text-muted-foreground/70 uppercase tracking-wider">Files</span>
|
|
31
49
|
<span className="text-2xs text-muted-foreground/40 ml-auto">{results.length}</span>
|
|
32
50
|
</div>
|
|
33
|
-
<div ref={listRef} className="
|
|
51
|
+
<div ref={listRef} className="overflow-y-auto" style={{ maxHeight: maxListH }}>
|
|
34
52
|
{results.map((f, idx) => {
|
|
35
53
|
const name = f.split('/').pop() ?? f;
|
|
36
54
|
const dir = f.split('/').slice(0, -1).join('/');
|
|
@@ -4,25 +4,46 @@ import { useRef, useEffect } from 'react';
|
|
|
4
4
|
import { Sparkles, Loader2, AlertCircle, Wrench, WifiOff, Zap } from 'lucide-react';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
|
-
import type { Message } from '@/lib/types';
|
|
7
|
+
import type { Message, ImagePart } from '@/lib/types';
|
|
8
8
|
import { stripThinkingTags } from '@/hooks/useAiOrganize';
|
|
9
9
|
import ToolCallBlock from './ToolCallBlock';
|
|
10
10
|
import ThinkingBlock from './ThinkingBlock';
|
|
11
11
|
|
|
12
12
|
const SKILL_PREFIX_RE = /^Use the skill ([^:]+):\s*/;
|
|
13
13
|
|
|
14
|
-
function UserMessageContent({ content, skillName }: { content: string; skillName?: string }) {
|
|
14
|
+
function UserMessageContent({ content, skillName, images }: { content: string; skillName?: string; images?: ImagePart[] }) {
|
|
15
15
|
const resolved = skillName ?? content.match(SKILL_PREFIX_RE)?.[1];
|
|
16
|
-
if (!resolved) return <>{content}</>;
|
|
17
16
|
const prefixMatch = content.match(SKILL_PREFIX_RE);
|
|
18
17
|
const rest = prefixMatch ? content.slice(prefixMatch[0].length) : content;
|
|
19
18
|
return (
|
|
20
19
|
<>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
{/* Images */}
|
|
21
|
+
{images && images.length > 0 && (
|
|
22
|
+
<div className={`flex flex-wrap gap-1.5${content ? ' mb-2' : ''}`}>
|
|
23
|
+
{images.map((img, idx) => (
|
|
24
|
+
img.data ? (
|
|
25
|
+
<img
|
|
26
|
+
key={idx}
|
|
27
|
+
src={`data:${img.mimeType};base64,${img.data}`}
|
|
28
|
+
alt={`Image ${idx + 1}`}
|
|
29
|
+
className="max-h-48 max-w-full rounded-md object-contain"
|
|
30
|
+
/>
|
|
31
|
+
) : (
|
|
32
|
+
<div key={idx} className="h-12 px-3 rounded-md bg-muted flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
33
|
+
<span>[Image {idx + 1}]</span>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
{/* Skill capsule + text */}
|
|
40
|
+
{resolved && (
|
|
41
|
+
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[11px] font-medium bg-[var(--amber)]/15 text-[var(--amber)] mr-1 align-middle">
|
|
42
|
+
<Zap size={10} className="shrink-0" />
|
|
43
|
+
{resolved}
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
{resolved ? rest : content}
|
|
26
47
|
</>
|
|
27
48
|
);
|
|
28
49
|
}
|
|
@@ -168,7 +189,7 @@ export default function MessageList({
|
|
|
168
189
|
<div
|
|
169
190
|
className="max-w-[85%] px-3 py-2 rounded-xl rounded-br-sm text-sm leading-relaxed whitespace-pre-wrap bg-[var(--amber)] text-[var(--amber-foreground)]"
|
|
170
191
|
>
|
|
171
|
-
<UserMessageContent content={m.content} skillName={m.skillName} />
|
|
192
|
+
<UserMessageContent content={m.content} skillName={m.skillName} images={m.images} />
|
|
172
193
|
</div>
|
|
173
194
|
) : m.content.startsWith('__error__') ? (
|
|
174
195
|
<div className="max-w-[85%] px-3 py-2.5 rounded-xl rounded-bl-sm border border-error/20 bg-error/8 text-sm">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Zap } from 'lucide-react';
|
|
5
5
|
import type { SlashItem } from '@/hooks/useSlashCommand';
|
|
6
6
|
import HighlightMatch from './HighlightMatch';
|
|
@@ -12,8 +12,26 @@ interface SlashCommandPopoverProps {
|
|
|
12
12
|
onSelect: (item: SlashItem) => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Popover for slash commands. Renders as a flex child within the AskContent
|
|
17
|
+
* column (not absolutely positioned) to avoid clipping by the panel's
|
|
18
|
+
* overflow-hidden. Uses a dynamic max-height that caps at 50% of the
|
|
19
|
+
* viewport so the popover never overflows the visible area.
|
|
20
|
+
*/
|
|
15
21
|
export default function SlashCommandPopover({ results, selectedIndex, query, onSelect }: SlashCommandPopoverProps) {
|
|
16
22
|
const listRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
const [maxListH, setMaxListH] = useState(280);
|
|
25
|
+
|
|
26
|
+
// Dynamically compute max list height based on available space
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const container = containerRef.current;
|
|
29
|
+
if (!container) return;
|
|
30
|
+
const rect = container.getBoundingClientRect();
|
|
31
|
+
// Reserve ~80px for header + footer chrome, and cap at 50vh
|
|
32
|
+
const available = Math.min(rect.top - 8, window.innerHeight * 0.5) - 80;
|
|
33
|
+
setMaxListH(Math.max(120, Math.min(320, available)));
|
|
34
|
+
}, [results.length]);
|
|
17
35
|
|
|
18
36
|
useEffect(() => {
|
|
19
37
|
const container = listRef.current;
|
|
@@ -25,13 +43,13 @@ export default function SlashCommandPopover({ results, selectedIndex, query, onS
|
|
|
25
43
|
if (results.length === 0) return null;
|
|
26
44
|
|
|
27
45
|
return (
|
|
28
|
-
<div className="border border-border rounded-lg bg-card shadow-lg overflow-hidden">
|
|
46
|
+
<div ref={containerRef} className="border border-border rounded-lg bg-card shadow-lg overflow-hidden">
|
|
29
47
|
<div className="px-3 py-1.5 border-b border-border flex items-center gap-1.5">
|
|
30
48
|
<Zap size={11} className="text-[var(--amber)]/50" />
|
|
31
49
|
<span className="text-2xs font-medium text-muted-foreground/70 uppercase tracking-wider">Skills</span>
|
|
32
50
|
<span className="text-2xs text-muted-foreground/40 ml-auto">{results.length}</span>
|
|
33
51
|
</div>
|
|
34
|
-
<div ref={listRef} className="
|
|
52
|
+
<div ref={listRef} className="overflow-y-auto" style={{ maxHeight: maxListH }}>
|
|
35
53
|
{results.map((item, idx) => (
|
|
36
54
|
<button
|
|
37
55
|
key={item.name}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
4
4
|
import { BookOpen, Rocket, Brain, Keyboard, HelpCircle, Bot, ChevronDown, Copy, Check } from 'lucide-react';
|
|
5
5
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import { copyToClipboard } from '@/lib/clipboard';
|
|
7
|
+
import { toast } from '@/lib/toast';
|
|
6
8
|
|
|
7
9
|
/* ── Collapsible Section ── */
|
|
8
10
|
function Section({ id, icon, title, defaultOpen = false, children }: {
|
|
@@ -56,15 +58,13 @@ function PromptBlock({ text, copyLabel }: { text: string; copyLabel: string }) {
|
|
|
56
58
|
const [copied, setCopied] = useState(false);
|
|
57
59
|
|
|
58
60
|
const handleCopy = useCallback(() => {
|
|
59
|
-
const clean = text.replace(/^["
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
setCopied(true); // Reuse copied state to show error
|
|
67
|
-
setTimeout(() => setCopied(false), 2000);
|
|
61
|
+
const clean = text.replace(/^["“]|["”]$/g, '');
|
|
62
|
+
copyToClipboard(clean).then((ok) => {
|
|
63
|
+
if (ok) {
|
|
64
|
+
setCopied(true);
|
|
65
|
+
setTimeout(() => setCopied(false), 1500);
|
|
66
|
+
toast.copy();
|
|
67
|
+
}
|
|
68
68
|
});
|
|
69
69
|
}, [text]);
|
|
70
70
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useMemo } from 'react';
|
|
4
|
-
import { ChevronLeft, X, Loader2, CheckCircle2, AlertCircle, Copy,
|
|
4
|
+
import { ChevronLeft, X, Loader2, CheckCircle2, AlertCircle, Copy, Monitor, Globe } from 'lucide-react';
|
|
5
|
+
import { toast } from '@/lib/toast';
|
|
5
6
|
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
6
7
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
7
8
|
import type { AgentInfo, McpStatus } from '../settings/types';
|
|
@@ -48,7 +49,6 @@ export default function AgentsPanelAgentDetail({
|
|
|
48
49
|
const [installing, setInstalling] = useState(false);
|
|
49
50
|
const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
50
51
|
const [transport, setTransport] = useState<'stdio' | 'http'>(() => agent.preferredTransport);
|
|
51
|
-
const [copied, setCopied] = useState(false);
|
|
52
52
|
|
|
53
53
|
const snippet = useMemo(() => {
|
|
54
54
|
if (agentStatus === 'notFound') return null;
|
|
@@ -70,10 +70,7 @@ export default function AgentsPanelAgentDetail({
|
|
|
70
70
|
const handleCopy = async () => {
|
|
71
71
|
if (!snippet) return;
|
|
72
72
|
const ok = await copyToClipboard(snippet.snippet);
|
|
73
|
-
if (ok)
|
|
74
|
-
setCopied(true);
|
|
75
|
-
setTimeout(() => setCopied(false), 2000);
|
|
76
|
-
}
|
|
73
|
+
if (ok) toast.copy();
|
|
77
74
|
};
|
|
78
75
|
|
|
79
76
|
const dot =
|
|
@@ -183,8 +180,8 @@ export default function AgentsPanelAgentDetail({
|
|
|
183
180
|
onClick={handleCopy}
|
|
184
181
|
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
185
182
|
>
|
|
186
|
-
|
|
187
|
-
{
|
|
183
|
+
<Copy size={14} />
|
|
184
|
+
{copy.copyConfig}
|
|
188
185
|
</button>
|
|
189
186
|
</>
|
|
190
187
|
)}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { usePathname, useSearchParams } from 'next/navigation';
|
|
4
|
-
import { LayoutDashboard, Server, Zap } from 'lucide-react';
|
|
4
|
+
import { Globe, History, LayoutDashboard, Server, Zap } from 'lucide-react';
|
|
5
5
|
import { PanelNavRow } from './PanelNavRow';
|
|
6
6
|
|
|
7
7
|
type HubCopy = {
|
|
8
8
|
navOverview: string;
|
|
9
9
|
navMcp: string;
|
|
10
10
|
navSkills: string;
|
|
11
|
+
navNetwork: string;
|
|
12
|
+
navSessions: string;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export function AgentsPanelHubNav({
|
|
@@ -25,7 +27,7 @@ export function AgentsPanelHubNav({
|
|
|
25
27
|
return (
|
|
26
28
|
<div className="py-2">
|
|
27
29
|
<PanelNavRow
|
|
28
|
-
icon={<LayoutDashboard size={14} className=
|
|
30
|
+
icon={<LayoutDashboard size={14} className={inAgentsRoute && (tab === null || tab === 'overview') ? 'text-[var(--amber)]' : 'text-muted-foreground'} />}
|
|
29
31
|
title={copy.navOverview}
|
|
30
32
|
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
33
|
href="/agents"
|
|
@@ -43,6 +45,18 @@ export function AgentsPanelHubNav({
|
|
|
43
45
|
href="/agents?tab=skills"
|
|
44
46
|
active={inAgentsRoute && tab === 'skills'}
|
|
45
47
|
/>
|
|
48
|
+
<PanelNavRow
|
|
49
|
+
icon={<Globe size={14} className={inAgentsRoute && tab === 'a2a' ? 'text-[var(--amber)]' : 'text-muted-foreground'} />}
|
|
50
|
+
title={copy.navNetwork}
|
|
51
|
+
href="/agents?tab=a2a"
|
|
52
|
+
active={inAgentsRoute && tab === 'a2a'}
|
|
53
|
+
/>
|
|
54
|
+
<PanelNavRow
|
|
55
|
+
icon={<History size={14} className={inAgentsRoute && tab === 'sessions' ? 'text-[var(--amber)]' : 'text-muted-foreground'} />}
|
|
56
|
+
title={copy.navSessions}
|
|
57
|
+
href="/agents?tab=sessions"
|
|
58
|
+
active={inAgentsRoute && tab === 'sessions'}
|
|
59
|
+
/>
|
|
46
60
|
</div>
|
|
47
61
|
);
|
|
48
62
|
}
|
|
@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation';
|
|
|
5
5
|
import { UserRound, Bookmark, Sun, History, Brain } from 'lucide-react';
|
|
6
6
|
import PanelHeader from './PanelHeader';
|
|
7
7
|
import { PanelNavRow } from './PanelNavRow';
|
|
8
|
+
import EchoSidebarStats from './EchoSidebarStats';
|
|
8
9
|
import { useLocale } from '@/lib/LocaleContext';
|
|
9
10
|
import { ECHO_SEGMENT_HREF, ECHO_SEGMENT_ORDER, type EchoSegment } from '@/lib/echo-segments';
|
|
10
11
|
|
|
@@ -30,7 +31,7 @@ export default function EchoPanel({ active, maximized, onMaximize }: EchoPanelPr
|
|
|
30
31
|
return (
|
|
31
32
|
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
32
33
|
<PanelHeader title={e.title} maximized={maximized} onMaximize={onMaximize} />
|
|
33
|
-
<div className="flex-1 overflow-y-auto min-h-0">
|
|
34
|
+
<div className="flex-1 overflow-y-auto min-h-0 flex flex-col">
|
|
34
35
|
<div className="flex flex-col gap-0.5 py-1.5">
|
|
35
36
|
{ECHO_SEGMENT_ORDER.map((segment) => {
|
|
36
37
|
const row = rowBySegment[segment];
|
|
@@ -41,6 +42,9 @@ export default function EchoPanel({ active, maximized, onMaximize }: EchoPanelPr
|
|
|
41
42
|
);
|
|
42
43
|
})}
|
|
43
44
|
</div>
|
|
45
|
+
<div className="mt-auto">
|
|
46
|
+
<EchoSidebarStats />
|
|
47
|
+
</div>
|
|
44
48
|
</div>
|
|
45
49
|
</div>
|
|
46
50
|
);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { TrendingUp, MessageSquare, AlertCircle } from 'lucide-react';
|
|
5
|
+
import type { ContentChangeEvent } from '@/lib/fs';
|
|
6
|
+
import { apiFetch } from '@/lib/api';
|
|
7
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
8
|
+
|
|
9
|
+
interface EchoStats {
|
|
10
|
+
fileCount: number;
|
|
11
|
+
unreadChanges: number;
|
|
12
|
+
sessionCount: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function EchoSidebarStats() {
|
|
16
|
+
const [stats, setStats] = useState<EchoStats | null>(null);
|
|
17
|
+
const [recentEvents, setRecentEvents] = useState<ContentChangeEvent[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const { t } = useLocale();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const loadStats = async () => {
|
|
23
|
+
try {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
const [monitoring, changes, sessions] = await Promise.all([
|
|
26
|
+
apiFetch<any>('/api/monitoring'),
|
|
27
|
+
apiFetch<any>('/api/changes?op=list&limit=3'),
|
|
28
|
+
apiFetch<any>('/api/ask-sessions'),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
setStats({
|
|
32
|
+
fileCount: monitoring?.knowledgeBase?.fileCount ?? 0,
|
|
33
|
+
unreadChanges: changes?.events?.length ?? 0,
|
|
34
|
+
sessionCount: Array.isArray(sessions) ? sessions.length : 0,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
setRecentEvents((changes?.events ?? []).slice(0, 3));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.warn('[EchoSidebarStats] Failed to load stats:', err);
|
|
40
|
+
setStats({ fileCount: 0, unreadChanges: 0, sessionCount: 0 });
|
|
41
|
+
setRecentEvents([]);
|
|
42
|
+
} finally {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
loadStats();
|
|
48
|
+
const interval = setInterval(loadStats, 10000); // Refresh every 10s
|
|
49
|
+
return () => clearInterval(interval);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
if (loading || !stats) {
|
|
53
|
+
return (
|
|
54
|
+
<div className="px-3 py-2 text-xs text-muted-foreground">
|
|
55
|
+
<div className="h-2 w-20 bg-muted rounded animate-pulse" />
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex flex-col gap-2 px-3 py-3 border-t border-border">
|
|
62
|
+
{/* Quick Stats */}
|
|
63
|
+
<div className="grid grid-cols-3 gap-2">
|
|
64
|
+
<div className="flex flex-col items-center p-1.5 rounded bg-muted/40 hover:bg-muted/60 transition-colors cursor-default">
|
|
65
|
+
<div className="text-sm font-semibold text-foreground">{stats.fileCount}</div>
|
|
66
|
+
<div className="text-xs text-muted-foreground truncate">Files</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="flex flex-col items-center p-1.5 rounded bg-muted/40 hover:bg-muted/60 transition-colors cursor-default">
|
|
69
|
+
<div className="text-sm font-semibold text-foreground">{stats.unreadChanges}</div>
|
|
70
|
+
<div className="text-xs text-muted-foreground truncate">Changes</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="flex flex-col items-center p-1.5 rounded bg-muted/40 hover:bg-muted/60 transition-colors cursor-default">
|
|
73
|
+
<div className="text-sm font-semibold text-foreground">{stats.sessionCount}</div>
|
|
74
|
+
<div className="text-xs text-muted-foreground truncate">Chats</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Recent Activity */}
|
|
79
|
+
{recentEvents.length > 0 && (
|
|
80
|
+
<div className="flex flex-col gap-1">
|
|
81
|
+
<div className="text-xs font-medium text-muted-foreground px-0.5">Recent</div>
|
|
82
|
+
{recentEvents.map((evt) => {
|
|
83
|
+
const relTime = formatRelativeTime(evt.ts);
|
|
84
|
+
const iconType = getIconForOp(evt.op);
|
|
85
|
+
const fileName = evt.path.split('/').pop() || evt.path;
|
|
86
|
+
return (
|
|
87
|
+
<div key={evt.id} className="flex items-start gap-2 p-1.5 rounded hover:bg-muted/40 transition-colors text-xs">
|
|
88
|
+
<span className="text-muted-foreground shrink-0 mt-0.5">{iconType}</span>
|
|
89
|
+
<div className="min-w-0 flex-1">
|
|
90
|
+
<p className="text-foreground truncate font-medium" title={fileName}>{fileName}</p>
|
|
91
|
+
<p className="text-muted-foreground text-2xs">{relTime}</p>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getIconForOp(op: string): React.ReactNode {
|
|
103
|
+
switch (op) {
|
|
104
|
+
case 'create':
|
|
105
|
+
return <TrendingUp size={12} className="text-success" />;
|
|
106
|
+
case 'write':
|
|
107
|
+
case 'append':
|
|
108
|
+
return <AlertCircle size={12} className="text-foreground" />;
|
|
109
|
+
case 'delete':
|
|
110
|
+
return <AlertCircle size={12} className="text-destructive" />;
|
|
111
|
+
case 'rename':
|
|
112
|
+
case 'move':
|
|
113
|
+
return <MessageSquare size={12} className="text-muted-foreground" />;
|
|
114
|
+
default:
|
|
115
|
+
return <AlertCircle size={12} className="text-muted-foreground" />;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatRelativeTime(isoString: string): string {
|
|
120
|
+
try {
|
|
121
|
+
const date = new Date(isoString);
|
|
122
|
+
const now = new Date();
|
|
123
|
+
const diffMs = now.getTime() - date.getTime();
|
|
124
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
125
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
126
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
127
|
+
|
|
128
|
+
if (diffMins < 1) return 'now';
|
|
129
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
130
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
131
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
132
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
133
|
+
} catch {
|
|
134
|
+
return 'recently';
|
|
135
|
+
}
|
|
136
|
+
}
|