@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.
Files changed (113) hide show
  1. package/README.md +10 -4
  2. package/app/app/api/a2a/agents/route.ts +9 -0
  3. package/app/app/api/a2a/delegations/route.ts +9 -0
  4. package/app/app/api/a2a/discover/route.ts +2 -0
  5. package/app/app/api/a2a/route.ts +6 -6
  6. package/app/app/api/acp/config/route.ts +82 -0
  7. package/app/app/api/acp/detect/route.ts +114 -0
  8. package/app/app/api/acp/install/route.ts +51 -0
  9. package/app/app/api/acp/registry/route.ts +31 -0
  10. package/app/app/api/acp/session/route.ts +185 -0
  11. package/app/app/api/ask/route.ts +116 -13
  12. package/app/app/api/workflows/route.ts +156 -0
  13. package/app/app/layout.tsx +2 -0
  14. package/app/app/page.tsx +7 -2
  15. package/app/components/ActivityBar.tsx +12 -4
  16. package/app/components/AskModal.tsx +4 -1
  17. package/app/components/DirView.tsx +64 -2
  18. package/app/components/FileTree.tsx +40 -10
  19. package/app/components/GuideCard.tsx +7 -17
  20. package/app/components/HomeContent.tsx +1 -0
  21. package/app/components/MarkdownView.tsx +2 -0
  22. package/app/components/Panel.tsx +1 -0
  23. package/app/components/RightAskPanel.tsx +5 -1
  24. package/app/components/SearchModal.tsx +234 -80
  25. package/app/components/SidebarLayout.tsx +6 -0
  26. package/app/components/agents/AgentDetailContent.tsx +266 -52
  27. package/app/components/agents/AgentsContentPage.tsx +32 -6
  28. package/app/components/agents/AgentsPanelA2aTab.tsx +684 -0
  29. package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
  30. package/app/components/agents/SkillDetailPopover.tsx +4 -9
  31. package/app/components/agents/agents-content-model.ts +2 -2
  32. package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
  33. package/app/components/ask/AskContent.tsx +197 -239
  34. package/app/components/ask/FileChip.tsx +82 -17
  35. package/app/components/ask/MentionPopover.tsx +21 -3
  36. package/app/components/ask/MessageList.tsx +30 -9
  37. package/app/components/ask/SlashCommandPopover.tsx +21 -3
  38. package/app/components/help/HelpContent.tsx +9 -9
  39. package/app/components/panels/AgentsPanel.tsx +2 -0
  40. package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
  41. package/app/components/panels/AgentsPanelHubNav.tsx +16 -2
  42. package/app/components/panels/EchoPanel.tsx +5 -1
  43. package/app/components/panels/EchoSidebarStats.tsx +136 -0
  44. package/app/components/panels/WorkflowsPanel.tsx +206 -0
  45. package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
  46. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
  47. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
  48. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
  49. package/app/components/renderers/workflow-yaml/execution.ts +177 -0
  50. package/app/components/renderers/workflow-yaml/index.ts +6 -0
  51. package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
  52. package/app/components/renderers/workflow-yaml/parser.ts +172 -0
  53. package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
  54. package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
  55. package/app/components/renderers/workflow-yaml/types.ts +46 -0
  56. package/app/components/settings/KnowledgeTab.tsx +3 -6
  57. package/app/components/settings/McpSkillsSection.tsx +4 -5
  58. package/app/components/settings/McpTab.tsx +6 -8
  59. package/app/components/setup/StepSecurity.tsx +4 -5
  60. package/app/components/setup/index.tsx +5 -11
  61. package/app/components/ui/Toaster.tsx +39 -0
  62. package/app/hooks/useA2aRegistry.ts +6 -1
  63. package/app/hooks/useAcpConfig.ts +96 -0
  64. package/app/hooks/useAcpDetection.ts +120 -0
  65. package/app/hooks/useAcpRegistry.ts +86 -0
  66. package/app/hooks/useAskModal.ts +12 -5
  67. package/app/hooks/useAskPanel.ts +8 -5
  68. package/app/hooks/useAskSession.ts +19 -2
  69. package/app/hooks/useDelegationHistory.ts +49 -0
  70. package/app/hooks/useImageUpload.ts +152 -0
  71. package/app/lib/a2a/client.ts +49 -5
  72. package/app/lib/a2a/orchestrator.ts +0 -1
  73. package/app/lib/a2a/task-handler.ts +4 -4
  74. package/app/lib/a2a/types.ts +15 -0
  75. package/app/lib/acp/acp-tools.ts +95 -0
  76. package/app/lib/acp/agent-descriptors.ts +274 -0
  77. package/app/lib/acp/bridge.ts +144 -0
  78. package/app/lib/acp/index.ts +40 -0
  79. package/app/lib/acp/registry.ts +202 -0
  80. package/app/lib/acp/session.ts +717 -0
  81. package/app/lib/acp/subprocess.ts +495 -0
  82. package/app/lib/acp/types.ts +274 -0
  83. package/app/lib/agent/model.ts +18 -3
  84. package/app/lib/agent/to-agent-messages.ts +25 -2
  85. package/app/lib/agent/tools.ts +2 -1
  86. package/app/lib/i18n/_core.ts +22 -0
  87. package/app/lib/i18n/index.ts +35 -0
  88. package/app/lib/i18n/modules/ai-chat.ts +215 -0
  89. package/app/lib/i18n/modules/common.ts +71 -0
  90. package/app/lib/i18n/modules/features.ts +153 -0
  91. package/app/lib/i18n/modules/knowledge.ts +429 -0
  92. package/app/lib/i18n/modules/navigation.ts +153 -0
  93. package/app/lib/i18n/modules/onboarding.ts +523 -0
  94. package/app/lib/i18n/modules/panels.ts +1196 -0
  95. package/app/lib/i18n/modules/settings.ts +585 -0
  96. package/app/lib/i18n-en.ts +2 -1518
  97. package/app/lib/i18n-zh.ts +2 -1542
  98. package/app/lib/i18n.ts +3 -6
  99. package/app/lib/pi-integration/skills.ts +21 -6
  100. package/app/lib/renderers/index.ts +2 -2
  101. package/app/lib/settings.ts +10 -0
  102. package/app/lib/toast.ts +79 -0
  103. package/app/lib/types.ts +12 -1
  104. package/app/next-env.d.ts +1 -1
  105. package/app/package.json +3 -1
  106. package/bin/cli.js +25 -25
  107. package/bin/commands/file.js +29 -2
  108. package/bin/commands/space.js +249 -91
  109. package/package.json +1 -1
  110. package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
  111. package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
  112. package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
  113. 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
- {copied ? <Check size={11} /> : <Copy size={11} />}
446
- {copied ? copy.copied : copy.copyContent}
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
+ }