@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
package/app/hooks/useAskModal.ts
CHANGED
|
@@ -8,13 +8,19 @@ import { useSyncExternalStore, useCallback } from 'react';
|
|
|
8
8
|
* No external dependencies (no zustand needed).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
export interface AcpAgentSelection {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
interface AskModalState {
|
|
12
17
|
open: boolean;
|
|
13
18
|
initialMessage: string;
|
|
14
19
|
source: 'user' | 'guide' | 'guide-next'; // who triggered the open
|
|
20
|
+
acpAgent: AcpAgentSelection | null;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
let state: AskModalState = { open: false, initialMessage: '', source: 'user' };
|
|
23
|
+
let state: AskModalState = { open: false, initialMessage: '', source: 'user', acpAgent: null };
|
|
18
24
|
const listeners = new Set<() => void>();
|
|
19
25
|
|
|
20
26
|
function emit() { listeners.forEach(l => l()); }
|
|
@@ -24,13 +30,13 @@ function subscribe(listener: () => void) {
|
|
|
24
30
|
}
|
|
25
31
|
function getSnapshot() { return state; }
|
|
26
32
|
|
|
27
|
-
export function openAskModal(message = '', source: AskModalState['source'] = 'user') {
|
|
28
|
-
state = { open: true, initialMessage: message, source };
|
|
33
|
+
export function openAskModal(message = '', source: AskModalState['source'] = 'user', acpAgent: AcpAgentSelection | null = null) {
|
|
34
|
+
state = { open: true, initialMessage: message, source, acpAgent };
|
|
29
35
|
emit();
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export function closeAskModal() {
|
|
33
|
-
state = { open: false, initialMessage: '', source: 'user' };
|
|
39
|
+
state = { open: false, initialMessage: '', source: 'user', acpAgent: null };
|
|
34
40
|
emit();
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -40,7 +46,8 @@ export function useAskModal() {
|
|
|
40
46
|
open: snap.open,
|
|
41
47
|
initialMessage: snap.initialMessage,
|
|
42
48
|
source: snap.source,
|
|
43
|
-
|
|
49
|
+
acpAgent: snap.acpAgent,
|
|
50
|
+
openWith: useCallback((message: string, source: AskModalState['source'] = 'user', acpAgent: AcpAgentSelection | null = null) => openAskModal(message, source, acpAgent), []),
|
|
44
51
|
close: useCallback(() => closeAskModal(), []),
|
|
45
52
|
};
|
|
46
53
|
}
|
package/app/hooks/useAskPanel.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { RIGHT_ASK_DEFAULT_WIDTH, RIGHT_ASK_MIN_WIDTH, RIGHT_ASK_MAX_WIDTH } from '@/components/RightAskPanel';
|
|
5
|
-
import { useAskModal } from './useAskModal';
|
|
5
|
+
import { useAskModal, type AcpAgentSelection } from './useAskModal';
|
|
6
6
|
|
|
7
7
|
export interface AskPanelState {
|
|
8
8
|
askPanelOpen: boolean;
|
|
@@ -12,6 +12,7 @@ export interface AskPanelState {
|
|
|
12
12
|
desktopAskPopupOpen: boolean;
|
|
13
13
|
askInitialMessage: string;
|
|
14
14
|
askOpenSource: 'user' | 'guide' | 'guide-next';
|
|
15
|
+
askAcpAgent: AcpAgentSelection | null;
|
|
15
16
|
toggleAskPanel: () => void;
|
|
16
17
|
closeAskPanel: () => void;
|
|
17
18
|
closeDesktopAskPopup: () => void;
|
|
@@ -33,6 +34,7 @@ export function useAskPanel(): AskPanelState {
|
|
|
33
34
|
const [askInitialMessage, setAskInitialMessage] = useState('');
|
|
34
35
|
const [askMaximized, setAskMaximized] = useState(false);
|
|
35
36
|
const [askOpenSource, setAskOpenSource] = useState<'user' | 'guide' | 'guide-next'>('user');
|
|
37
|
+
const [askAcpAgent, setAskAcpAgent] = useState<AcpAgentSelection | null>(null);
|
|
36
38
|
|
|
37
39
|
const askModal = useAskModal();
|
|
38
40
|
|
|
@@ -62,6 +64,7 @@ export function useAskPanel(): AskPanelState {
|
|
|
62
64
|
if (askModal.open) {
|
|
63
65
|
setAskInitialMessage(askModal.initialMessage);
|
|
64
66
|
setAskOpenSource(askModal.source);
|
|
67
|
+
setAskAcpAgent(askModal.acpAgent);
|
|
65
68
|
if (askMode === 'popup') {
|
|
66
69
|
setDesktopAskPopupOpen(true);
|
|
67
70
|
} else {
|
|
@@ -69,17 +72,17 @@ export function useAskPanel(): AskPanelState {
|
|
|
69
72
|
}
|
|
70
73
|
askModal.close();
|
|
71
74
|
}
|
|
72
|
-
}, [askModal.open, askModal.initialMessage, askModal.source, askModal.close, askMode]);
|
|
75
|
+
}, [askModal.open, askModal.initialMessage, askModal.source, askModal.acpAgent, askModal.close, askMode]);
|
|
73
76
|
|
|
74
77
|
const toggleAskPanel = useCallback(() => {
|
|
75
78
|
if (askMode === 'popup') {
|
|
76
79
|
setDesktopAskPopupOpen(v => {
|
|
77
|
-
if (!v) { setAskInitialMessage(''); setAskOpenSource('user'); }
|
|
80
|
+
if (!v) { setAskInitialMessage(''); setAskOpenSource('user'); setAskAcpAgent(null); }
|
|
78
81
|
return !v;
|
|
79
82
|
});
|
|
80
83
|
} else {
|
|
81
84
|
setAskPanelOpen(v => {
|
|
82
|
-
if (!v) { setAskInitialMessage(''); setAskOpenSource('user'); }
|
|
85
|
+
if (!v) { setAskInitialMessage(''); setAskOpenSource('user'); setAskAcpAgent(null); }
|
|
83
86
|
return !v;
|
|
84
87
|
});
|
|
85
88
|
}
|
|
@@ -114,7 +117,7 @@ export function useAskPanel(): AskPanelState {
|
|
|
114
117
|
|
|
115
118
|
return {
|
|
116
119
|
askPanelOpen, askPanelWidth, askMaximized, askMode, desktopAskPopupOpen,
|
|
117
|
-
askInitialMessage, askOpenSource,
|
|
120
|
+
askInitialMessage, askOpenSource, askAcpAgent,
|
|
118
121
|
toggleAskPanel, closeAskPanel, closeDesktopAskPopup,
|
|
119
122
|
handleAskWidthChange, handleAskWidthCommit, handleAskModeSwitch, toggleAskMaximized,
|
|
120
123
|
};
|
|
@@ -21,7 +21,10 @@ export function sessionTitle(s: ChatSession): string {
|
|
|
21
21
|
const firstUser = s.messages.find((m) => m.role === 'user');
|
|
22
22
|
if (!firstUser) return '(empty session)';
|
|
23
23
|
const line = firstUser.content.replace(/\s+/g, ' ').trim();
|
|
24
|
-
|
|
24
|
+
if (!line && firstUser.images && firstUser.images.length > 0) {
|
|
25
|
+
return `[${firstUser.images.length} image${firstUser.images.length > 1 ? 's' : ''}]`;
|
|
26
|
+
}
|
|
27
|
+
return line.length > 42 ? `${line.slice(0, 42)}...` : line || '(empty session)';
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
async function fetchSessions(): Promise<ChatSession[]> {
|
|
@@ -38,10 +41,24 @@ async function fetchSessions(): Promise<ChatSession[]> {
|
|
|
38
41
|
|
|
39
42
|
async function upsertSession(session: ChatSession): Promise<void> {
|
|
40
43
|
try {
|
|
44
|
+
// Strip base64 image data before persisting (images are session-only, not stored)
|
|
45
|
+
const stripped: ChatSession = {
|
|
46
|
+
...session,
|
|
47
|
+
messages: session.messages.map(m => {
|
|
48
|
+
if (!m.images || m.images.length === 0) return m;
|
|
49
|
+
return {
|
|
50
|
+
...m,
|
|
51
|
+
images: m.images.map(img => ({
|
|
52
|
+
...img,
|
|
53
|
+
data: '', // Strip base64 data — images are ephemeral
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
41
58
|
await fetch('/api/ask-sessions', {
|
|
42
59
|
method: 'POST',
|
|
43
60
|
headers: { 'Content-Type': 'application/json' },
|
|
44
|
-
body: JSON.stringify({ session }),
|
|
61
|
+
body: JSON.stringify({ session: stripped }),
|
|
45
62
|
});
|
|
46
63
|
} catch {
|
|
47
64
|
// ignore persistence errors
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import type { DelegationRecord } from '@/lib/a2a/types';
|
|
5
|
+
|
|
6
|
+
interface DelegationHistory {
|
|
7
|
+
delegations: DelegationRecord[];
|
|
8
|
+
loading: boolean;
|
|
9
|
+
refresh: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
13
|
+
|
|
14
|
+
export function useDelegationHistory(active: boolean): DelegationHistory {
|
|
15
|
+
const [delegations, setDelegations] = useState<DelegationRecord[]>([]);
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const mountedRef = useRef(true);
|
|
18
|
+
|
|
19
|
+
const fetchHistory = useCallback(async () => {
|
|
20
|
+
setLoading(true);
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch('/api/a2a/delegations');
|
|
23
|
+
if (!res.ok) return;
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
if (mountedRef.current) {
|
|
26
|
+
setDelegations(data.delegations ?? []);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// silently ignore fetch errors
|
|
30
|
+
} finally {
|
|
31
|
+
if (mountedRef.current) setLoading(false);
|
|
32
|
+
}
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
mountedRef.current = true;
|
|
37
|
+
return () => { mountedRef.current = false; };
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
// Fetch on mount and poll while active
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!active) return;
|
|
43
|
+
fetchHistory();
|
|
44
|
+
const id = setInterval(fetchHistory, POLL_INTERVAL_MS);
|
|
45
|
+
return () => clearInterval(id);
|
|
46
|
+
}, [active, fetchHistory]);
|
|
47
|
+
|
|
48
|
+
return { delegations, loading, refresh: fetchHistory };
|
|
49
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import type { ImagePart, ImageMimeType } from '@/lib/types';
|
|
5
|
+
|
|
6
|
+
const ALLOWED_IMAGE_TYPES: Set<string> = new Set([
|
|
7
|
+
'image/png', 'image/jpeg', 'image/gif', 'image/webp',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB per image
|
|
11
|
+
const MAX_IMAGES = 4;
|
|
12
|
+
const MAX_DIMENSION = 1920; // Compress to this max width/height
|
|
13
|
+
|
|
14
|
+
function isImageMimeType(type: string): type is ImageMimeType {
|
|
15
|
+
return ALLOWED_IMAGE_TYPES.has(type);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Compress image to JPEG if over size/dimension limits */
|
|
19
|
+
async function compressImage(file: File): Promise<{ data: string; mimeType: ImageMimeType }> {
|
|
20
|
+
const mimeType = file.type as ImageMimeType;
|
|
21
|
+
|
|
22
|
+
// If small enough, use as-is (skip canvas compression to preserve GIF animation)
|
|
23
|
+
if (file.size <= MAX_IMAGE_SIZE) {
|
|
24
|
+
const buffer = await file.arrayBuffer();
|
|
25
|
+
const bytes = new Uint8Array(buffer);
|
|
26
|
+
let binary = '';
|
|
27
|
+
const chunkSize = 0x8000;
|
|
28
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
29
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
|
30
|
+
}
|
|
31
|
+
return { data: btoa(binary), mimeType };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Compress via canvas
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const img = new Image();
|
|
37
|
+
img.onload = () => {
|
|
38
|
+
let { width, height } = img;
|
|
39
|
+
if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
|
40
|
+
const ratio = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height);
|
|
41
|
+
width = Math.round(width * ratio);
|
|
42
|
+
height = Math.round(height * ratio);
|
|
43
|
+
}
|
|
44
|
+
const canvas = document.createElement('canvas');
|
|
45
|
+
canvas.width = width;
|
|
46
|
+
canvas.height = height;
|
|
47
|
+
const ctx = canvas.getContext('2d');
|
|
48
|
+
if (!ctx) { reject(new Error('Canvas not supported')); return; }
|
|
49
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
50
|
+
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
|
|
51
|
+
const base64 = dataUrl.split(',')[1];
|
|
52
|
+
resolve({ data: base64, mimeType: 'image/jpeg' });
|
|
53
|
+
URL.revokeObjectURL(img.src);
|
|
54
|
+
};
|
|
55
|
+
img.onerror = () => { reject(new Error('Failed to load image')); URL.revokeObjectURL(img.src); };
|
|
56
|
+
img.src = URL.createObjectURL(file);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Convert clipboard/DataTransfer items to File[] */
|
|
61
|
+
function extractImageFiles(items: DataTransferItemList | DataTransferItem[]): File[] {
|
|
62
|
+
const files: File[] = [];
|
|
63
|
+
const list = Array.from(items);
|
|
64
|
+
for (const item of list) {
|
|
65
|
+
if (item.kind === 'file' && ALLOWED_IMAGE_TYPES.has(item.type)) {
|
|
66
|
+
const file = item.getAsFile();
|
|
67
|
+
if (file) files.push(file);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useImageUpload() {
|
|
74
|
+
const [images, setImages] = useState<ImagePart[]>([]);
|
|
75
|
+
const [imageError, setImageError] = useState('');
|
|
76
|
+
|
|
77
|
+
const addImages = useCallback(async (files: File[]) => {
|
|
78
|
+
setImageError('');
|
|
79
|
+
const toProcess = files.slice(0, MAX_IMAGES);
|
|
80
|
+
|
|
81
|
+
const results: ImagePart[] = [];
|
|
82
|
+
for (const file of toProcess) {
|
|
83
|
+
if (!isImageMimeType(file.type)) {
|
|
84
|
+
setImageError(`Unsupported image type: ${file.type}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const { data, mimeType } = await compressImage(file);
|
|
89
|
+
results.push({ type: 'image', data, mimeType });
|
|
90
|
+
} catch {
|
|
91
|
+
setImageError(`Failed to process image: ${file.name}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setImages(prev => {
|
|
96
|
+
const merged = [...prev, ...results];
|
|
97
|
+
if (merged.length > MAX_IMAGES) {
|
|
98
|
+
setImageError(`Maximum ${MAX_IMAGES} images allowed`);
|
|
99
|
+
return merged.slice(0, MAX_IMAGES);
|
|
100
|
+
}
|
|
101
|
+
return merged;
|
|
102
|
+
});
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
/** Handle paste event — returns true if images were found */
|
|
106
|
+
const handlePaste = useCallback(async (e: ClipboardEvent | React.ClipboardEvent): Promise<boolean> => {
|
|
107
|
+
const items = e.clipboardData?.items;
|
|
108
|
+
if (!items) return false;
|
|
109
|
+
const files = extractImageFiles(items);
|
|
110
|
+
if (files.length === 0) return false;
|
|
111
|
+
await addImages(files);
|
|
112
|
+
return true;
|
|
113
|
+
}, [addImages]);
|
|
114
|
+
|
|
115
|
+
/** Handle drop event */
|
|
116
|
+
const handleDrop = useCallback(async (e: DragEvent | React.DragEvent) => {
|
|
117
|
+
const items = e.dataTransfer?.items;
|
|
118
|
+
if (!items) return;
|
|
119
|
+
const files = extractImageFiles(items);
|
|
120
|
+
if (files.length > 0) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
await addImages(files);
|
|
123
|
+
}
|
|
124
|
+
}, [addImages]);
|
|
125
|
+
|
|
126
|
+
/** Handle file input change */
|
|
127
|
+
const handleFileSelect = useCallback(async (files: FileList | null) => {
|
|
128
|
+
if (!files) return;
|
|
129
|
+
const imageFiles = Array.from(files).filter(f => isImageMimeType(f.type));
|
|
130
|
+
await addImages(imageFiles);
|
|
131
|
+
}, [addImages]);
|
|
132
|
+
|
|
133
|
+
const removeImage = useCallback((idx: number) => {
|
|
134
|
+
setImages(prev => prev.filter((_, i) => i !== idx));
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
const clearImages = useCallback(() => {
|
|
138
|
+
setImages([]);
|
|
139
|
+
setImageError('');
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
images,
|
|
144
|
+
imageError,
|
|
145
|
+
addImages,
|
|
146
|
+
handlePaste,
|
|
147
|
+
handleDrop,
|
|
148
|
+
handleFileSelect,
|
|
149
|
+
removeImage,
|
|
150
|
+
clearImages,
|
|
151
|
+
};
|
|
152
|
+
}
|
package/app/lib/a2a/client.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
JsonRpcRequest,
|
|
11
11
|
JsonRpcResponse,
|
|
12
12
|
SendMessageParams,
|
|
13
|
+
DelegationRecord,
|
|
13
14
|
} from './types';
|
|
14
15
|
|
|
15
16
|
/* ── Constants ─────────────────────────────────────────────────────────── */
|
|
@@ -22,6 +23,20 @@ const CARD_CACHE_TTL_MS = 5 * 60 * 1000; // 5 min
|
|
|
22
23
|
|
|
23
24
|
const registry = new Map<string, RemoteAgent>();
|
|
24
25
|
|
|
26
|
+
/* ── Delegation History ────────────────────────────────────────────────── */
|
|
27
|
+
|
|
28
|
+
const delegationHistory: DelegationRecord[] = [];
|
|
29
|
+
|
|
30
|
+
/** Get all delegation history records */
|
|
31
|
+
export function getDelegationHistory(): DelegationRecord[] {
|
|
32
|
+
return [...delegationHistory];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Clear all delegation history records */
|
|
36
|
+
export function clearDelegationHistory(): void {
|
|
37
|
+
delegationHistory.length = 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
/** Derive a stable ID from a URL (includes protocol to avoid collisions) */
|
|
26
41
|
function urlToId(url: string): string {
|
|
27
42
|
try {
|
|
@@ -152,6 +167,19 @@ export async function delegateTask(
|
|
|
152
167
|
if (!agent) throw new Error(`Agent not found: ${agentId}`);
|
|
153
168
|
if (!agent.reachable) throw new Error(`Agent not reachable: ${agent.card.name}`);
|
|
154
169
|
|
|
170
|
+
const record: DelegationRecord = {
|
|
171
|
+
id: `del-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
172
|
+
agentId,
|
|
173
|
+
agentName: agent.card.name,
|
|
174
|
+
message,
|
|
175
|
+
status: 'pending',
|
|
176
|
+
startedAt: new Date().toISOString(),
|
|
177
|
+
completedAt: null,
|
|
178
|
+
result: null,
|
|
179
|
+
error: null,
|
|
180
|
+
};
|
|
181
|
+
delegationHistory.push(record);
|
|
182
|
+
|
|
155
183
|
const params: SendMessageParams = {
|
|
156
184
|
message: {
|
|
157
185
|
role: 'ROLE_USER',
|
|
@@ -160,13 +188,29 @@ export async function delegateTask(
|
|
|
160
188
|
configuration: { blocking: true },
|
|
161
189
|
};
|
|
162
190
|
|
|
163
|
-
|
|
191
|
+
try {
|
|
192
|
+
const response = await jsonRpcCall(agent.endpoint, 'SendMessage', params, token);
|
|
164
193
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
194
|
+
if (response.error) {
|
|
195
|
+
record.status = 'failed';
|
|
196
|
+
record.completedAt = new Date().toISOString();
|
|
197
|
+
record.error = `A2A error [${response.error.code}]: ${response.error.message}`;
|
|
198
|
+
throw new Error(record.error);
|
|
199
|
+
}
|
|
168
200
|
|
|
169
|
-
|
|
201
|
+
const task = response.result as A2ATask;
|
|
202
|
+
record.status = 'completed';
|
|
203
|
+
record.completedAt = new Date().toISOString();
|
|
204
|
+
record.result = task.artifacts?.[0]?.parts?.[0]?.text ?? null;
|
|
205
|
+
return task;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (record.status === 'pending') {
|
|
208
|
+
record.status = 'failed';
|
|
209
|
+
record.completedAt = new Date().toISOString();
|
|
210
|
+
record.error = (err as Error).message;
|
|
211
|
+
}
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
170
214
|
}
|
|
171
215
|
|
|
172
216
|
/**
|
|
@@ -200,18 +200,18 @@ export function handleGetTask(params: GetTaskParams): A2ATask | null {
|
|
|
200
200
|
return tasks.get(params.id) ?? null;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
export function handleCancelTask(params: CancelTaskParams): A2ATask | null {
|
|
203
|
+
export function handleCancelTask(params: CancelTaskParams): { task: A2ATask | null; reason: 'not_found' | 'not_cancelable' | 'ok' } {
|
|
204
204
|
const task = tasks.get(params.id);
|
|
205
|
-
if (!task) return null;
|
|
205
|
+
if (!task) return { task: null, reason: 'not_found' };
|
|
206
206
|
|
|
207
207
|
const terminalStates: TaskState[] = ['TASK_STATE_COMPLETED', 'TASK_STATE_FAILED', 'TASK_STATE_CANCELED', 'TASK_STATE_REJECTED'];
|
|
208
|
-
if (terminalStates.includes(task.status.state)) return null
|
|
208
|
+
if (terminalStates.includes(task.status.state)) return { task: null, reason: 'not_cancelable' };
|
|
209
209
|
|
|
210
210
|
task.status = {
|
|
211
211
|
state: 'TASK_STATE_CANCELED',
|
|
212
212
|
timestamp: new Date().toISOString(),
|
|
213
213
|
};
|
|
214
|
-
return task;
|
|
214
|
+
return { task, reason: 'ok' };
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
/* ── Helpers ───────────────────────────────────────────────────────────── */
|
package/app/lib/a2a/types.ts
CHANGED
|
@@ -210,3 +210,18 @@ export interface SkillMatch {
|
|
|
210
210
|
skillName: string;
|
|
211
211
|
confidence: number;
|
|
212
212
|
}
|
|
213
|
+
|
|
214
|
+
/* ── Delegation History (Phase 3 UI) ─────────────────────────────────── */
|
|
215
|
+
|
|
216
|
+
/** A recorded delegation attempt for the history log */
|
|
217
|
+
export interface DelegationRecord {
|
|
218
|
+
id: string;
|
|
219
|
+
agentId: string;
|
|
220
|
+
agentName: string;
|
|
221
|
+
message: string;
|
|
222
|
+
status: 'pending' | 'completed' | 'failed';
|
|
223
|
+
startedAt: string;
|
|
224
|
+
completedAt: string | null;
|
|
225
|
+
result: string | null;
|
|
226
|
+
error: string | null;
|
|
227
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP Agent Tools — Expose ACP capabilities as tools
|
|
3
|
+
* for the MindOS built-in agent to discover and invoke ACP agents.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Type, type Static } from '@sinclair/typebox';
|
|
7
|
+
import type { AgentTool } from '@mariozechner/pi-agent-core';
|
|
8
|
+
import { getAcpAgents, findAcpAgent } from './registry';
|
|
9
|
+
import { createSessionFromEntry, prompt, closeSession } from './session';
|
|
10
|
+
import { getMindRoot } from '../fs';
|
|
11
|
+
|
|
12
|
+
function textResult(text: string) {
|
|
13
|
+
return { content: [{ type: 'text' as const, text }], details: {} };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* ── Parameter Schemas ─────────────────────────────────────────────────── */
|
|
17
|
+
|
|
18
|
+
const ListAcpAgentsParams = Type.Object({
|
|
19
|
+
tag: Type.Optional(Type.String({ description: 'Optional tag to filter agents by (e.g. "coding", "search")' })),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const CallAcpAgentParams = Type.Object({
|
|
23
|
+
agent_id: Type.String({ description: 'ID of the ACP agent from the registry (from list_acp_agents)' }),
|
|
24
|
+
message: Type.String({ description: 'Natural language message to send to the ACP agent' }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/* ── Tool Implementations ──────────────────────────────────────────────── */
|
|
28
|
+
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
export const acpTools: AgentTool<any>[] = [
|
|
31
|
+
{
|
|
32
|
+
name: 'list_acp_agents',
|
|
33
|
+
label: 'List ACP Agents',
|
|
34
|
+
description: 'List available ACP (Agent Client Protocol) agents from the public registry. These are local subprocess-based agents like Gemini CLI, Claude, Copilot, etc. Optionally filter by tag.',
|
|
35
|
+
parameters: ListAcpAgentsParams,
|
|
36
|
+
execute: async (_id: string, params: Static<typeof ListAcpAgentsParams>) => {
|
|
37
|
+
try {
|
|
38
|
+
let agents = await getAcpAgents();
|
|
39
|
+
|
|
40
|
+
if (params.tag) {
|
|
41
|
+
const tag = params.tag.toLowerCase();
|
|
42
|
+
agents = agents.filter(a =>
|
|
43
|
+
a.tags?.some(t => t.toLowerCase().includes(tag))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (agents.length === 0) {
|
|
48
|
+
return textResult(
|
|
49
|
+
params.tag
|
|
50
|
+
? `No ACP agents found with tag "${params.tag}". Try list_acp_agents without a tag filter.`
|
|
51
|
+
: 'No ACP agents found in the registry. The registry may be unavailable.'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const lines = agents.map(a => {
|
|
56
|
+
const tags = a.tags?.join(', ') || 'none';
|
|
57
|
+
return `- **${a.name}** (id: \`${a.id}\`, transport: ${a.transport})\n ${a.description}\n Tags: ${tags}`;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return textResult(`Available ACP agents (${agents.length}):\n\n${lines.join('\n\n')}`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return textResult(`Failed to list ACP agents: ${(err as Error).message}`);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
name: 'call_acp_agent',
|
|
69
|
+
label: 'Call ACP Agent',
|
|
70
|
+
description: 'Spawn an ACP agent, send it a message, and return the result. The agent runs as a local subprocess. Use list_acp_agents first to see available agents.',
|
|
71
|
+
parameters: CallAcpAgentParams,
|
|
72
|
+
execute: async (_id: string, params: Static<typeof CallAcpAgentParams>) => {
|
|
73
|
+
try {
|
|
74
|
+
const entry = await findAcpAgent(params.agent_id);
|
|
75
|
+
if (!entry) {
|
|
76
|
+
return textResult(`ACP agent not found: ${params.agent_id}. Use list_acp_agents to see available agents.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const cwd = getMindRoot();
|
|
80
|
+
const session = await createSessionFromEntry(entry, { cwd });
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const response = await prompt(session.id, params.message);
|
|
84
|
+
return textResult(
|
|
85
|
+
`**${entry.name}** responded:\n\n${response.text || '(empty response)'}`
|
|
86
|
+
);
|
|
87
|
+
} finally {
|
|
88
|
+
await closeSession(session.id).catch(() => {});
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return textResult(`ACP call failed: ${(err as Error).message}`);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
];
|