@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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import { History, Play, Trash2 } from 'lucide-react';
|
|
5
|
+
import type { AcpSession } from '@/lib/acp/types';
|
|
6
|
+
|
|
7
|
+
interface SessionEntry {
|
|
8
|
+
id: string;
|
|
9
|
+
agentId: string;
|
|
10
|
+
state: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
lastActivityAt: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function AgentsPanelSessionsTab() {
|
|
17
|
+
const [sessions, setSessions] = useState<SessionEntry[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [error, setError] = useState<string | null>(null);
|
|
20
|
+
|
|
21
|
+
const fetchSessions = useCallback(async () => {
|
|
22
|
+
try {
|
|
23
|
+
setLoading(true);
|
|
24
|
+
const res = await fetch('/api/acp/session');
|
|
25
|
+
if (!res.ok) throw new Error(`Failed to fetch sessions: ${res.status}`);
|
|
26
|
+
const data = await res.json();
|
|
27
|
+
setSessions(data.sessions ?? []);
|
|
28
|
+
setError(null);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
setError((err as Error).message);
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => { fetchSessions(); }, [fetchSessions]);
|
|
37
|
+
|
|
38
|
+
const handleClose = useCallback(async (sessionId: string) => {
|
|
39
|
+
try {
|
|
40
|
+
await fetch('/api/acp/session', {
|
|
41
|
+
method: 'DELETE',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ sessionId }),
|
|
44
|
+
});
|
|
45
|
+
setSessions(prev => prev.filter(s => s.id !== sessionId));
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error('Failed to close session:', err);
|
|
48
|
+
}
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
if (loading) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="space-y-3 animate-pulse">
|
|
54
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
55
|
+
<div key={i} className="rounded-lg border border-border bg-card p-4">
|
|
56
|
+
<div className="flex items-center gap-3">
|
|
57
|
+
<div className="w-8 h-8 rounded-full bg-muted" />
|
|
58
|
+
<div className="flex-1 space-y-2">
|
|
59
|
+
<div className="h-4 w-32 bg-muted rounded" />
|
|
60
|
+
<div className="h-3 w-48 bg-muted rounded" />
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (error) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="rounded-lg border border-border bg-card p-6 text-center">
|
|
72
|
+
<p className="text-sm text-muted-foreground mb-3">{error}</p>
|
|
73
|
+
<button
|
|
74
|
+
onClick={fetchSessions}
|
|
75
|
+
className="text-sm text-[var(--amber)] hover:underline"
|
|
76
|
+
>
|
|
77
|
+
Retry
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (sessions.length === 0) {
|
|
84
|
+
return (
|
|
85
|
+
<div className="rounded-lg border border-border bg-card p-8 text-center">
|
|
86
|
+
<History size={32} className="mx-auto mb-3 text-muted-foreground/40" />
|
|
87
|
+
<p className="text-sm font-medium text-foreground mb-1">No active sessions</p>
|
|
88
|
+
<p className="text-xs text-muted-foreground">
|
|
89
|
+
Sessions appear here when you chat with ACP agents.
|
|
90
|
+
<br />
|
|
91
|
+
Select an agent from the Network tab and send a message to start.
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="space-y-2">
|
|
99
|
+
<div className="flex items-center justify-between mb-3">
|
|
100
|
+
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wider">
|
|
101
|
+
Active Sessions ({sessions.length})
|
|
102
|
+
</p>
|
|
103
|
+
<button
|
|
104
|
+
onClick={fetchSessions}
|
|
105
|
+
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
106
|
+
>
|
|
107
|
+
Refresh
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{sessions.map(session => (
|
|
112
|
+
<div
|
|
113
|
+
key={session.id}
|
|
114
|
+
className="group rounded-lg border border-border bg-card hover:border-[var(--amber)]/30 transition-colors p-3.5"
|
|
115
|
+
>
|
|
116
|
+
<div className="flex items-start justify-between gap-2">
|
|
117
|
+
<div className="flex-1 min-w-0">
|
|
118
|
+
<div className="flex items-center gap-2 mb-1">
|
|
119
|
+
<span className={`inline-block w-2 h-2 rounded-full ${
|
|
120
|
+
session.state === 'active' ? 'bg-[var(--success)]' :
|
|
121
|
+
session.state === 'error' ? 'bg-[var(--error)]' :
|
|
122
|
+
'bg-muted-foreground/40'
|
|
123
|
+
}`} />
|
|
124
|
+
<span className="text-sm font-medium text-foreground truncate">
|
|
125
|
+
{session.agentId}
|
|
126
|
+
</span>
|
|
127
|
+
<span className="text-2xs text-muted-foreground/60 px-1.5 py-0.5 rounded bg-muted/40">
|
|
128
|
+
{session.state}
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
{session.cwd && (
|
|
132
|
+
<p className="text-xs text-muted-foreground truncate font-mono">
|
|
133
|
+
{session.cwd}
|
|
134
|
+
</p>
|
|
135
|
+
)}
|
|
136
|
+
<p className="text-2xs text-muted-foreground/60 mt-1">
|
|
137
|
+
{formatRelativeTime(session.lastActivityAt)}
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
142
|
+
<button
|
|
143
|
+
onClick={() => handleClose(session.id)}
|
|
144
|
+
className="p-1.5 rounded-md hover:bg-muted/60 text-muted-foreground hover:text-[var(--error)] transition-colors"
|
|
145
|
+
title="Close session"
|
|
146
|
+
>
|
|
147
|
+
<Trash2 size={14} />
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatRelativeTime(isoString: string): string {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
const then = new Date(isoString).getTime();
|
|
160
|
+
const diff = now - then;
|
|
161
|
+
|
|
162
|
+
if (diff < 60_000) return 'just now';
|
|
163
|
+
if (diff < 3600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
|
164
|
+
if (diff < 86400_000) return `${Math.floor(diff / 3600_000)}h ago`;
|
|
165
|
+
return `${Math.floor(diff / 86400_000)}d ago`;
|
|
166
|
+
}
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
BookOpen,
|
|
6
6
|
Code2,
|
|
7
7
|
Copy,
|
|
8
|
-
Check,
|
|
9
8
|
FileText,
|
|
10
9
|
Loader2,
|
|
11
10
|
Plus,
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
} from 'lucide-react';
|
|
20
19
|
import { apiFetch } from '@/lib/api';
|
|
21
20
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
21
|
+
import { toast } from '@/lib/toast';
|
|
22
22
|
import type { SkillInfo } from '@/components/settings/types';
|
|
23
23
|
import { Toggle } from '@/components/settings/Primitives';
|
|
24
24
|
import { AgentAvatar, ConfirmDialog } from './AgentsPrimitives';
|
|
@@ -187,7 +187,6 @@ export default function SkillDetailPopover({
|
|
|
187
187
|
const [nativeDesc, setNativeDesc] = useState<string>('');
|
|
188
188
|
const [loading, setLoading] = useState(false);
|
|
189
189
|
const [loadError, setLoadError] = useState(false);
|
|
190
|
-
const [copied, setCopied] = useState(false);
|
|
191
190
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
192
191
|
const [deleting, setDeleting] = useState(false);
|
|
193
192
|
const [deleteMsg, setDeleteMsg] = useState<string | null>(null);
|
|
@@ -228,7 +227,6 @@ export default function SkillDetailPopover({
|
|
|
228
227
|
setContent(null);
|
|
229
228
|
setNativeDesc('');
|
|
230
229
|
setLoadError(false);
|
|
231
|
-
setCopied(false);
|
|
232
230
|
setDeleteMsg(null);
|
|
233
231
|
setDeleting(false);
|
|
234
232
|
setToggleBusy(false);
|
|
@@ -250,10 +248,7 @@ export default function SkillDetailPopover({
|
|
|
250
248
|
const handleCopy = useCallback(async () => {
|
|
251
249
|
if (!content) return;
|
|
252
250
|
const ok = await copyToClipboard(content);
|
|
253
|
-
if (ok)
|
|
254
|
-
setCopied(true);
|
|
255
|
-
setTimeout(() => setCopied(false), 1500);
|
|
256
|
-
}
|
|
251
|
+
if (ok) toast.copy();
|
|
257
252
|
}, [content]);
|
|
258
253
|
|
|
259
254
|
const handleToggle = useCallback(async (enabled: boolean) => {
|
|
@@ -442,8 +437,8 @@ export default function SkillDetailPopover({
|
|
|
442
437
|
className="inline-flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground cursor-pointer transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1.5 py-0.5"
|
|
443
438
|
aria-label={copy.copyContent}
|
|
444
439
|
>
|
|
445
|
-
|
|
446
|
-
{
|
|
440
|
+
<Copy size={11} />
|
|
441
|
+
{copy.copyContent}
|
|
447
442
|
</button>
|
|
448
443
|
)}
|
|
449
444
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentInfo, SkillInfo } from '@/components/settings/types';
|
|
2
2
|
|
|
3
|
-
export type AgentsDashboardTab = 'overview' | 'mcp' | 'skills';
|
|
3
|
+
export type AgentsDashboardTab = 'overview' | 'mcp' | 'skills' | 'a2a' | 'sessions';
|
|
4
4
|
export type AgentResolvedStatus = 'connected' | 'detected' | 'notFound';
|
|
5
5
|
export type SkillCapability = 'research' | 'coding' | 'docs' | 'ops' | 'memory';
|
|
6
6
|
export type SkillSourceFilter = 'all' | 'builtin' | 'user';
|
|
@@ -24,7 +24,7 @@ export interface AgentBuckets {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function parseAgentsTab(tab: string | undefined): AgentsDashboardTab {
|
|
27
|
-
if (tab === 'mcp' || tab === 'skills') return tab;
|
|
27
|
+
if (tab === 'mcp' || tab === 'skills' || tab === 'a2a' || tab === 'sessions') return tab;
|
|
28
28
|
return 'overview';
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { Bot, ChevronDown, X, Check } from 'lucide-react';
|
|
6
|
+
import type { AcpAgentSelection } from '@/hooks/useAskModal';
|
|
7
|
+
import type { DetectedAgent } from '@/hooks/useAcpDetection';
|
|
8
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
9
|
+
|
|
10
|
+
interface AgentSelectorCapsuleProps {
|
|
11
|
+
selectedAgent: AcpAgentSelection | null;
|
|
12
|
+
onSelect: (agent: AcpAgentSelection | null) => void;
|
|
13
|
+
installedAgents: DetectedAgent[];
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface DropdownPos {
|
|
18
|
+
top: number;
|
|
19
|
+
left: number;
|
|
20
|
+
direction: 'up' | 'down';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function AgentSelectorCapsule({
|
|
24
|
+
selectedAgent,
|
|
25
|
+
onSelect,
|
|
26
|
+
installedAgents,
|
|
27
|
+
loading = false,
|
|
28
|
+
}: AgentSelectorCapsuleProps) {
|
|
29
|
+
const { t } = useLocale();
|
|
30
|
+
const p = t.panels.agents;
|
|
31
|
+
const [open, setOpen] = useState(false);
|
|
32
|
+
const [pos, setPos] = useState<DropdownPos | null>(null);
|
|
33
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
34
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
|
|
36
|
+
// Position the dropdown relative to the trigger, rendered via portal
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!open || !triggerRef.current) return;
|
|
39
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
40
|
+
const spaceAbove = rect.top;
|
|
41
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
42
|
+
const estimatedH = 200; // approximate dropdown height
|
|
43
|
+
const direction: 'up' | 'down' = spaceAbove > spaceBelow && spaceAbove > estimatedH ? 'up' : 'down';
|
|
44
|
+
|
|
45
|
+
setPos({
|
|
46
|
+
left: rect.left,
|
|
47
|
+
top: direction === 'up' ? rect.top - 6 : rect.bottom + 6,
|
|
48
|
+
direction,
|
|
49
|
+
});
|
|
50
|
+
}, [open]);
|
|
51
|
+
|
|
52
|
+
// Close dropdown on outside click
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!open) return;
|
|
55
|
+
const handler = (e: MouseEvent) => {
|
|
56
|
+
const target = e.target as Node;
|
|
57
|
+
if (
|
|
58
|
+
triggerRef.current && !triggerRef.current.contains(target) &&
|
|
59
|
+
dropdownRef.current && !dropdownRef.current.contains(target)
|
|
60
|
+
) {
|
|
61
|
+
setOpen(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
document.addEventListener('mousedown', handler);
|
|
65
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
66
|
+
}, [open]);
|
|
67
|
+
|
|
68
|
+
// Close on Escape
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!open) return;
|
|
71
|
+
const handler = (e: KeyboardEvent) => {
|
|
72
|
+
if (e.key === 'Escape') setOpen(false);
|
|
73
|
+
};
|
|
74
|
+
document.addEventListener('keydown', handler);
|
|
75
|
+
return () => document.removeEventListener('keydown', handler);
|
|
76
|
+
}, [open]);
|
|
77
|
+
|
|
78
|
+
// Reposition on scroll/resize while open
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!open || !triggerRef.current) return;
|
|
81
|
+
const reposition = () => {
|
|
82
|
+
if (!triggerRef.current) return;
|
|
83
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
84
|
+
const spaceAbove = rect.top;
|
|
85
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
86
|
+
const estimatedH = 200;
|
|
87
|
+
const direction: 'up' | 'down' = spaceAbove > spaceBelow && spaceAbove > estimatedH ? 'up' : 'down';
|
|
88
|
+
setPos({
|
|
89
|
+
left: rect.left,
|
|
90
|
+
top: direction === 'up' ? rect.top - 6 : rect.bottom + 6,
|
|
91
|
+
direction,
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
window.addEventListener('scroll', reposition, true);
|
|
95
|
+
window.addEventListener('resize', reposition);
|
|
96
|
+
return () => {
|
|
97
|
+
window.removeEventListener('scroll', reposition, true);
|
|
98
|
+
window.removeEventListener('resize', reposition);
|
|
99
|
+
};
|
|
100
|
+
}, [open]);
|
|
101
|
+
|
|
102
|
+
const handleSelectDefault = useCallback(() => {
|
|
103
|
+
onSelect(null);
|
|
104
|
+
setOpen(false);
|
|
105
|
+
}, [onSelect]);
|
|
106
|
+
|
|
107
|
+
const handleSelectAgent = useCallback((agent: DetectedAgent) => {
|
|
108
|
+
onSelect({ id: agent.id, name: agent.name });
|
|
109
|
+
setOpen(false);
|
|
110
|
+
}, [onSelect]);
|
|
111
|
+
|
|
112
|
+
const handleClear = useCallback((e: React.MouseEvent) => {
|
|
113
|
+
e.stopPropagation();
|
|
114
|
+
onSelect(null);
|
|
115
|
+
}, [onSelect]);
|
|
116
|
+
|
|
117
|
+
const isDefault = !selectedAgent;
|
|
118
|
+
const displayName = selectedAgent?.name ?? p.acpDefaultAgent;
|
|
119
|
+
|
|
120
|
+
// Only show if there are installed agents to choose from
|
|
121
|
+
if (!loading && installedAgents.length === 0 && !selectedAgent) return null;
|
|
122
|
+
|
|
123
|
+
const dropdown = open && pos ? (
|
|
124
|
+
<div
|
|
125
|
+
ref={dropdownRef}
|
|
126
|
+
role="listbox"
|
|
127
|
+
aria-label={p.acpSelectAgent}
|
|
128
|
+
className="fixed z-50 min-w-[180px] max-w-[240px] rounded-lg border border-border bg-card shadow-lg py-1 animate-in fade-in-0 zoom-in-95 duration-100"
|
|
129
|
+
style={{
|
|
130
|
+
left: pos.left,
|
|
131
|
+
...(pos.direction === 'up'
|
|
132
|
+
? { bottom: window.innerHeight - pos.top }
|
|
133
|
+
: { top: pos.top }),
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{/* Default MindOS Agent option */}
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
role="option"
|
|
140
|
+
aria-selected={isDefault}
|
|
141
|
+
onClick={handleSelectDefault}
|
|
142
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs text-left transition-colors hover:bg-muted"
|
|
143
|
+
>
|
|
144
|
+
<Bot size={12} className="shrink-0 text-muted-foreground" />
|
|
145
|
+
<span className="flex-1 truncate font-medium">{p.acpDefaultAgent}</span>
|
|
146
|
+
{isDefault && <Check size={11} className="shrink-0 text-[var(--amber)]" />}
|
|
147
|
+
</button>
|
|
148
|
+
|
|
149
|
+
{/* Divider */}
|
|
150
|
+
{installedAgents.length > 0 && (
|
|
151
|
+
<div className="mx-2 my-1 border-t border-border/60" />
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Installed ACP agents */}
|
|
155
|
+
{installedAgents.map((agent) => {
|
|
156
|
+
const isSelected = selectedAgent?.id === agent.id;
|
|
157
|
+
return (
|
|
158
|
+
<button
|
|
159
|
+
key={agent.id}
|
|
160
|
+
type="button"
|
|
161
|
+
role="option"
|
|
162
|
+
aria-selected={isSelected}
|
|
163
|
+
onClick={() => handleSelectAgent(agent)}
|
|
164
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs text-left transition-colors hover:bg-muted"
|
|
165
|
+
>
|
|
166
|
+
<span className="w-2 h-2 rounded-full bg-[var(--success)] shrink-0" />
|
|
167
|
+
<span className="flex-1 truncate">{agent.name}</span>
|
|
168
|
+
{isSelected && <Check size={11} className="shrink-0 text-[var(--amber)]" />}
|
|
169
|
+
</button>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
</div>
|
|
173
|
+
) : null;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
<button
|
|
178
|
+
ref={triggerRef}
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={() => setOpen(v => !v)}
|
|
181
|
+
className={`
|
|
182
|
+
inline-flex items-center gap-1 rounded-full px-2.5 py-0.5
|
|
183
|
+
text-2xs font-medium transition-colors
|
|
184
|
+
border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
|
185
|
+
${isDefault
|
|
186
|
+
? 'bg-muted/50 border-border/50 text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
187
|
+
: 'bg-[var(--amber)]/10 border-[var(--amber)]/25 text-foreground hover:bg-[var(--amber)]/15'
|
|
188
|
+
}
|
|
189
|
+
`}
|
|
190
|
+
title={p.acpChangeAgent}
|
|
191
|
+
aria-expanded={open}
|
|
192
|
+
aria-haspopup="listbox"
|
|
193
|
+
>
|
|
194
|
+
{isDefault ? (
|
|
195
|
+
<Bot size={11} className="shrink-0 text-muted-foreground" />
|
|
196
|
+
) : (
|
|
197
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--success)] shrink-0" />
|
|
198
|
+
)}
|
|
199
|
+
<span className="truncate max-w-[120px]">{displayName}</span>
|
|
200
|
+
{selectedAgent ? (
|
|
201
|
+
<span
|
|
202
|
+
role="button"
|
|
203
|
+
tabIndex={0}
|
|
204
|
+
onClick={handleClear}
|
|
205
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleClear(e as unknown as React.MouseEvent); } }}
|
|
206
|
+
className="p-0.5 -mr-1 rounded-full text-muted-foreground hover:text-foreground transition-colors"
|
|
207
|
+
aria-label={`Remove ${selectedAgent.name}`}
|
|
208
|
+
>
|
|
209
|
+
<X size={9} />
|
|
210
|
+
</span>
|
|
211
|
+
) : (
|
|
212
|
+
<ChevronDown size={10} className="shrink-0 text-muted-foreground" />
|
|
213
|
+
)}
|
|
214
|
+
</button>
|
|
215
|
+
{typeof document !== 'undefined' && dropdown && createPortal(dropdown, document.body)}
|
|
216
|
+
</>
|
|
217
|
+
);
|
|
218
|
+
}
|