11"use client" ;
22
3- import React , { useState , useEffect , useRef } from "react" ;
3+ import React , { useState , useEffect , useRef , useMemo } from "react" ;
44import { createPortal } from "react-dom" ;
55import { useTranslation } from "react-i18next" ;
6- import { ChevronDown , MousePointerClick } from "lucide-react" ;
6+ import { ChevronDown , MousePointerClick , AlertCircle } from "lucide-react" ;
77
88import { fetchAllAgents } from "@/services/agentConfigService" ;
99import { getUrlParam } from "@/lib/utils" ;
@@ -34,6 +34,39 @@ export function ChatAgentSelector({
3434 ( agent ) => agent . agent_id === selectedAgentId
3535 ) ;
3636
37+ // Detect duplicate agent names and mark later-added agents as disabled
38+ // For agents with the same name, keep the first one (smallest ID) enabled, disable the rest
39+ const duplicateAgentInfo = useMemo ( ( ) => {
40+ // Create a map to track agents by name
41+ const nameToAgents = new Map < string , Agent [ ] > ( ) ;
42+
43+ agents . forEach ( ( agent ) => {
44+ const agentName = agent . name ;
45+ if ( ! nameToAgents . has ( agentName ) ) {
46+ nameToAgents . set ( agentName , [ ] ) ;
47+ }
48+ nameToAgents . get ( agentName ) ! . push ( agent ) ;
49+ } ) ;
50+
51+ // For each group of agents with the same name, sort by ID (smallest first)
52+ // Mark all except the first one as disabled
53+ const disabledAgentIds = new Set < number > ( ) ;
54+
55+ nameToAgents . forEach ( ( agents , name ) => {
56+ if ( agents . length > 1 ) {
57+ // Sort by agent_id (smallest first)
58+ const sortedAgents = [ ...agents ] . sort ( ( a , b ) => a . agent_id - b . agent_id ) ;
59+
60+ // Mark all except the first one as disabled
61+ for ( let i = 1 ; i < sortedAgents . length ; i ++ ) {
62+ disabledAgentIds . add ( sortedAgents [ i ] . agent_id ) ;
63+ }
64+ }
65+ } ) ;
66+
67+ return { disabledAgentIds, nameToAgents } ;
68+ } , [ agents ] ) ;
69+
3770 /**
3871 * Handle URL parameter auto-selection logic for Agent
3972 */
@@ -46,11 +79,17 @@ export function ChatAgentSelector({
4679 ) ;
4780 if ( agentId === null ) return ;
4881
49- // Check if agentId is a valid agent
82+ // Check if agentId is a valid and effectively available agent
5083 const agent = agents . find ( ( a ) => a . agent_id === agentId ) ;
51- if ( agent && agent . is_available ) {
52- handleAgentSelect ( agentId ) ;
53- setIsAutoSelectInit ( true ) ;
84+ if ( agent ) {
85+ const isAvailableTool = agent . is_available !== false ;
86+ const isDuplicateDisabled = duplicateAgentInfo . disabledAgentIds . has ( agent . agent_id ) ;
87+ const isEffectivelyAvailable = isAvailableTool && ! isDuplicateDisabled ;
88+
89+ if ( isEffectivelyAvailable ) {
90+ handleAgentSelect ( agentId ) ;
91+ setIsAutoSelectInit ( true ) ;
92+ }
5493 }
5594 } ;
5695
@@ -61,7 +100,8 @@ export function ChatAgentSelector({
61100 // Execute auto-selection logic when agents are loaded
62101 useEffect ( ( ) => {
63102 handleAutoSelectAgent ( ) ;
64- } , [ agents ] ) ;
103+ // eslint-disable-next-line react-hooks/exhaustive-deps
104+ } , [ agents , duplicateAgentInfo ] ) ;
65105
66106 // Calculate dropdown position
67107 useEffect ( ( ) => {
@@ -157,11 +197,17 @@ export function ChatAgentSelector({
157197 } ;
158198
159199 const handleAgentSelect = ( agentId : number | null ) => {
160- // Only available agents can be selected
200+ // Only effectively available agents can be selected
161201 if ( agentId !== null ) {
162202 const agent = agents . find ( ( a ) => a . agent_id === agentId ) ;
163- if ( agent && ! agent . is_available ) {
164- return ; // Unavailable agents cannot be selected
203+ if ( agent ) {
204+ const isAvailableTool = agent . is_available !== false ;
205+ const isDuplicateDisabled = duplicateAgentInfo . disabledAgentIds . has ( agent . agent_id ) ;
206+ const isEffectivelyAvailable = isAvailableTool && ! isDuplicateDisabled ;
207+
208+ if ( ! isEffectivelyAvailable ) {
209+ return ; // Unavailable agents cannot be selected
210+ }
165211 }
166212 }
167213
@@ -301,90 +347,108 @@ export function ChatAgentSelector({
301347 ) }
302348 </ div >
303349 ) : (
304- allAgents . map ( ( agent , idx ) => (
305- < div
306- key = { agent . agent_id }
307- className = { `
308- flex items-start gap-3 px-3.5 py-2.5 text-sm
309- transition-all duration-150 ease-in-out
310- ${
311- agent . is_available
312- ? `hover:bg-slate-50 cursor-pointer ${
313- selectedAgentId === agent . agent_id
314- ? "bg-blue-50/70 text-blue-600 hover:bg-blue-50/70"
315- : ""
316- } `
317- : "cursor-not-allowed bg-slate-50/50"
318- }
319- ${
320- selectedAgentId === agent . agent_id
321- ? "shadow-[inset_2px_0_0_0] shadow-blue-500"
322- : ""
350+ allAgents . map ( ( agent , idx ) => {
351+ const isAvailableTool = agent . is_available !== false ;
352+ const isDuplicateDisabled = duplicateAgentInfo . disabledAgentIds . has ( agent . agent_id ) ;
353+ const isEffectivelyAvailable = isAvailableTool && ! isDuplicateDisabled ;
354+
355+ // Determine the reason for unavailability
356+ let unavailableReason : string | null = null ;
357+ if ( ! isEffectivelyAvailable ) {
358+ if ( isDuplicateDisabled ) {
359+ unavailableReason = t ( "subAgentPool.tooltip.duplicateNameDisabled" ) ;
360+ } else if ( ! isAvailableTool ) {
361+ unavailableReason = t ( "subAgentPool.tooltip.hasUnavailableTools" ) ;
323362 }
324- ${ idx !== 0 ? "border-t border-slate-100" : "" }
325- ` }
326- onClick = { ( ) =>
327- agent . is_available && handleAgentSelect ( agent . agent_id )
328- }
329- >
330- { /* Agent Icon */ }
331- < div className = "flex-shrink-0 mt-0.5" >
332- < MousePointerClick
333- className = { `h-4 w-4 ${
334- agent . is_available
335- ? selectedAgentId === agent . agent_id
336- ? "text-blue-500"
337- : "text-slate-500"
338- : "text-slate-300"
339- } `}
340- />
341- </ div >
363+ }
364+
365+ return (
366+ < div
367+ key = { agent . agent_id }
368+ className = { `
369+ flex items-start gap-3 px-3.5 py-2.5 text-sm
370+ transition-all duration-150 ease-in-out
371+ ${
372+ isEffectivelyAvailable
373+ ? `hover:bg-slate-50 cursor-pointer ${
374+ selectedAgentId === agent . agent_id
375+ ? "bg-blue-50/70 text-blue-600 hover:bg-blue-50/70"
376+ : ""
377+ } `
378+ : "opacity-60 cursor-not-allowed bg-slate-50/50"
379+ }
380+ ${
381+ selectedAgentId === agent . agent_id
382+ ? "shadow-[inset_2px_0_0_0] shadow-blue-500"
383+ : ""
384+ }
385+ ${ idx !== 0 ? "border-t border-slate-100" : "" }
386+ ` }
387+ onClick = { ( ) =>
388+ isEffectivelyAvailable && handleAgentSelect ( agent . agent_id )
389+ }
390+ >
391+ { /* Agent Icon */ }
392+ < div className = "flex-shrink-0 mt-0.5" >
393+ { isEffectivelyAvailable ? (
394+ < MousePointerClick
395+ className = { `h-4 w-4 ${
396+ selectedAgentId === agent . agent_id
397+ ? "text-blue-500"
398+ : "text-slate-500"
399+ } `}
400+ />
401+ ) : (
402+ < AlertCircle className = "h-4 w-4 text-amber-500" />
403+ ) }
404+ </ div >
342405
343- { /* Agent Info */ }
344- < div className = "flex-1 min-w-0" >
345- < div
346- className = { `font-medium truncate ${
347- agent . is_available
348- ? selectedAgentId === agent . agent_id
349- ? "text-blue-600"
350- : "text-slate-700 hover:text-slate-900"
351- : "text-slate-400"
352- } `}
353- >
354- < div className = "flex items-center" >
355- { agent . display_name && (
356- < span className = "text-sm leading-none" >
357- { agent . display_name }
406+ { /* Agent Info */ }
407+ < div className = "flex-1 min-w-0" >
408+ < div
409+ className = { `font-medium truncate ${
410+ isEffectivelyAvailable
411+ ? selectedAgentId === agent . agent_id
412+ ? "text-blue-600"
413+ : "text-slate-700 hover:text-slate-900"
414+ : "text-slate-400"
415+ } `}
416+ >
417+ < div className = "flex items-center gap-1.5" >
418+ { agent . display_name && (
419+ < span className = "text-sm leading-none" >
420+ { agent . display_name }
421+ </ span >
422+ ) }
423+ < span
424+ className = { `text-sm leading-none align-baseline ${
425+ agent . display_name ? "ml-2" : "text-sm"
426+ } `}
427+ >
428+ { agent . name }
429+ </ span >
430+ </ div >
431+ </ div >
432+ < div
433+ className = { `text-xs mt-1 leading-relaxed ${
434+ isEffectivelyAvailable
435+ ? selectedAgentId === agent . agent_id
436+ ? "text-blue-500"
437+ : "text-slate-500"
438+ : "text-slate-400"
439+ } `}
440+ >
441+ { agent . description }
442+ { unavailableReason && (
443+ < span className = "block mt-1.5 text-amber-600 font-medium" >
444+ { unavailableReason }
358445 </ span >
359446 ) }
360- < span
361- className = { `text-sm leading-none align-baseline ${
362- agent . display_name ? "ml-2" : "text-sm"
363- } `}
364- >
365- { agent . name }
366- </ span >
367447 </ div >
368448 </ div >
369- < div
370- className = { `text-xs mt-1 leading-relaxed ${
371- agent . is_available
372- ? selectedAgentId === agent . agent_id
373- ? "text-blue-500"
374- : "text-slate-500"
375- : "text-slate-300"
376- } `}
377- >
378- { agent . description }
379- { ! agent . is_available && (
380- < span className = "block mt-1 text-red-400" >
381- { t ( "agentSelector.agentUnavailable" ) }
382- </ span >
383- ) }
384- </ div >
385449 </ div >
386- </ div >
387- ) )
450+ ) ;
451+ } )
388452 ) }
389453 </ div >
390454 </ div >
0 commit comments