Files
SuperCharged-Claude-Code-Up…/dexto/packages/webui/components/AgentSelector/AgentSelector.tsx
admin b52318eeae feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing
- Add AUTO-TRIGGER-SUMMARY.md documentation
- Add FINAL-INTEGRATION-SUMMARY.md documentation
- Complete Prometheus integration (6 commands + 4 tools)
- Complete Dexto integration (12 commands + 5 tools)
- Enhanced Ralph with access to all agents
- Fix /clawd command (removed disable-model-invocation)
- Update hooks.json to v5 with intelligent routing
- 291 total skills now available
- All 21 commands with automatic routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-28 00:27:56 +04:00

644 lines
34 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { useNavigate } from '@tanstack/react-router';
import { useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/queryKeys.js';
import { cn } from '@/lib/utils';
import {
useAgents,
useAgentPath,
useSwitchAgent,
useInstallAgent,
useUninstallAgent,
} from '../hooks/useAgents';
import { useRecentAgentsStore } from '@/lib/stores/recentAgentsStore';
import { useSessionStore } from '@/lib/stores/sessionStore';
import { Button } from '../ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
import {
ChevronDown,
Check,
DownloadCloud,
Sparkles,
Trash2,
BadgeCheck,
Plus,
} from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
import CreateAgentModal from './CreateAgentModal';
import { useAnalytics } from '@/lib/analytics/index.js';
type AgentItem = {
id: string;
name: string;
description: string;
author?: string;
tags?: string[];
type: 'builtin' | 'custom';
};
type AgentsResponse = {
installed: AgentItem[];
available: AgentItem[];
current: { id: string | null; name: string | null };
};
type AgentSelectorProps = {
mode?: 'default' | 'badge' | 'title';
};
export default function AgentSelector({ mode = 'default' }: AgentSelectorProps) {
const navigate = useNavigate();
const currentSessionId = useSessionStore((s) => s.currentSessionId);
const analytics = useAnalytics();
const analyticsRef = useRef(analytics);
const recentAgents = useRecentAgentsStore((state) => state.recentAgents);
const addToRecentAgents = useRecentAgentsStore((state) => state.addRecentAgent);
const [switching, setSwitching] = useState(false);
const [open, setOpen] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
// Keep analytics ref up to date to avoid stale closure issues
useEffect(() => {
analyticsRef.current = analytics;
}, [analytics]);
const queryClient = useQueryClient();
// Invalidate all agent-specific queries when switching agents
// This replaces the DOM event pattern (dexto:agentSwitched)
const invalidateAgentSpecificQueries = useCallback(() => {
queryClient.invalidateQueries({ queryKey: queryKeys.sessions.all });
queryClient.invalidateQueries({ queryKey: ['sessions', 'history'] }); // All session histories
queryClient.invalidateQueries({ queryKey: queryKeys.memories.all });
queryClient.invalidateQueries({ queryKey: queryKeys.servers.all });
queryClient.invalidateQueries({ queryKey: ['servers', 'tools'] });
queryClient.invalidateQueries({ queryKey: queryKeys.resources.all });
queryClient.invalidateQueries({ queryKey: ['greeting'] }); // Hierarchical invalidation
queryClient.invalidateQueries({ queryKey: queryKeys.prompts.all });
queryClient.invalidateQueries({ queryKey: queryKeys.agent.config }); // Agent config (CustomizePanel)
}, [queryClient]);
// Check if an agent path is from the global ~/.dexto directory
// Global pattern: /Users/<user>/.dexto/agents or /home/<user>/.dexto/agents
// Also handles Windows: C:\Users\<user>\.dexto\agents
const isGlobalAgent = useCallback((path: string): boolean => {
// Match paths where .dexto appears within first 4 segments (home directory level)
// POSIX: /Users/username/.dexto/agents/... (index 2)
// Windows: C:/Users/username/.dexto/agents/... (index 3, drive letter adds extra segment)
// Project: /Users/username/Projects/my-project/.dexto/agents/... (5+ segments)
// Normalize Windows backslashes to forward slashes for consistent parsing
const normalized = path.replace(/\\/g, '/');
const segments = normalized.split('/').filter(Boolean);
const dextoIndex = segments.findIndex((s) => s === '.dexto');
return dextoIndex >= 0 && dextoIndex <= 3;
}, []);
// Fetch agents list and path using typed hooks
const { data: agentsData, isLoading: agentsLoading, refetch: refetchAgents } = useAgents();
const { data: currentAgentPathData } = useAgentPath();
const installed = useMemo(() => agentsData?.installed || [], [agentsData?.installed]);
const available = useMemo(() => agentsData?.available || [], [agentsData?.available]);
const currentId = agentsData?.current.id || null;
const currentAgentPath = currentAgentPathData ?? null;
// Agent mutations using typed hooks
const switchAgentMutation = useSwitchAgent();
const installAgentMutation = useInstallAgent();
const deleteAgentMutation = useUninstallAgent();
// Sync current agent path to recent agents when it loads
useEffect(() => {
if (currentAgentPath?.path && currentAgentPath?.name) {
addToRecentAgents({
id: currentAgentPath.name,
name: currentAgentPath.name,
path: currentAgentPath.path,
});
}
}, [currentAgentPath, addToRecentAgents]);
const loading = agentsLoading;
const handleSwitch = useCallback(
async (agentId: string) => {
try {
setSwitching(true);
// Check if the agent exists in the installed list
const agent = installed.find((agent) => agent.id === agentId);
if (!agent) {
console.error(`Agent not found in installed list: ${agentId}`);
throw new Error(
`Agent '${agentId}' not found. Please refresh the agents list.`
);
}
// Capture current agent ID before switch
const fromAgentId = currentId;
await switchAgentMutation.mutateAsync({ id: agentId });
setOpen(false); // Close dropdown after successful switch
// Track agent switch using ref to avoid stale closure
analyticsRef.current.trackAgentSwitched({
fromAgentId,
toAgentId: agentId,
toAgentName: agent.name,
sessionId: currentSessionId || undefined,
});
// Invalidate all agent-specific queries
invalidateAgentSpecificQueries();
// Navigate back to home after switching agents
// The ChatApp component will automatically handle returnToWelcome when sessionId prop is undefined
navigate({ to: '/' });
} catch (err) {
console.error(
`Switch agent failed: ${err instanceof Error ? err.message : String(err)}`
);
const errorMessage = err instanceof Error ? err.message : 'Failed to switch agent';
alert(`Failed to switch agent: ${errorMessage}`);
} finally {
setSwitching(false);
}
},
[
installed,
navigate,
currentId,
currentSessionId,
switchAgentMutation,
invalidateAgentSpecificQueries,
]
);
const handleSwitchToPath = useCallback(
async (agent: { id: string; name: string; path: string }) => {
try {
setSwitching(true);
// Capture current agent ID before switch
const fromAgentId = currentId;
await switchAgentMutation.mutateAsync({ id: agent.id, path: agent.path });
setOpen(false); // Close dropdown after successful switch
// Add to recent agents
addToRecentAgents(agent);
// Track agent switch using ref to avoid stale closure
analyticsRef.current.trackAgentSwitched({
fromAgentId,
toAgentId: agent.id,
toAgentName: agent.name,
sessionId: currentSessionId || undefined,
});
// Invalidate all agent-specific queries
invalidateAgentSpecificQueries();
// Navigate back to home after switching agents
// The ChatApp component will automatically handle returnToWelcome when sessionId prop is undefined
navigate({ to: '/' });
} catch (err) {
console.error(
`Switch agent failed: ${err instanceof Error ? err.message : String(err)}`
);
const errorMessage = err instanceof Error ? err.message : 'Failed to switch agent';
alert(`Failed to switch agent: ${errorMessage}`);
} finally {
setSwitching(false);
}
},
[
addToRecentAgents,
navigate,
currentId,
currentSessionId,
switchAgentMutation,
invalidateAgentSpecificQueries,
]
);
const handleInstall = useCallback(
async (agentId: string) => {
try {
setSwitching(true);
// Capture current agent ID before operations
const fromAgentId = currentId;
// Step 1: Install the agent
await installAgentMutation.mutateAsync({ id: agentId });
// Step 2: Refetch agents list to ensure cache has fresh data
await queryClient.refetchQueries({ queryKey: queryKeys.agents.all });
// Step 3: Verify agent is now in installed list
const freshData = queryClient.getQueryData<AgentsResponse>(queryKeys.agents.all);
const agent = freshData?.installed.find((a) => a.id === agentId);
if (!agent) {
throw new Error(
`Agent '${agentId}' not found after installation. Please refresh.`
);
}
// Step 4: Switch to the newly installed agent
await switchAgentMutation.mutateAsync({ id: agentId });
setOpen(false);
// Step 5: Track the switch analytics
analyticsRef.current.trackAgentSwitched({
fromAgentId,
toAgentId: agentId,
toAgentName: agent.name,
sessionId: currentSessionId || undefined,
});
// Step 6: Invalidate all agent-specific queries
invalidateAgentSpecificQueries();
// Step 7: Navigate to home
// The ChatApp component will automatically handle returnToWelcome when sessionId prop is undefined
navigate({ to: '/' });
} catch (err) {
console.error(
`Install/switch agent failed: ${err instanceof Error ? err.message : String(err)}`
);
const errorMessage =
err instanceof Error ? err.message : 'Failed to install/switch agent';
alert(`Failed to install/switch agent: ${errorMessage}`);
} finally {
setSwitching(false);
}
},
[
navigate,
currentId,
currentSessionId,
queryClient,
installAgentMutation,
switchAgentMutation,
invalidateAgentSpecificQueries,
]
);
const handleDelete = useCallback(
async (agent: AgentItem, e: React.MouseEvent) => {
e.stopPropagation(); // Prevent triggering switch when clicking delete
if (!confirm(`Are you sure you want to delete the custom agent "${agent.name}"?`)) {
return;
}
try {
setSwitching(true);
await deleteAgentMutation.mutateAsync({ id: agent.id });
} catch (err) {
console.error(
`Delete agent failed: ${err instanceof Error ? err.message : String(err)}`
);
const errorMessage = err instanceof Error ? err.message : 'Failed to delete agent';
alert(`Failed to delete agent: ${errorMessage}`);
} finally {
setSwitching(false);
}
},
[deleteAgentMutation]
);
const currentLabel = useMemo(() => {
if (!currentId) return 'Choose Agent';
const match =
installed.find((agent) => agent.id === currentId) ||
available.find((agent) => agent.id === currentId);
return match?.name ?? currentId;
}, [available, currentId, installed]);
const handleAgentCreated = useCallback(
async (_agentName: string) => {
await refetchAgents();
},
[refetchAgents]
);
const getButtonClassName = (mode: string) => {
switch (mode) {
case 'badge':
// Teal text, transparent bg
return `h-9 px-4 text-lg font-medium rounded-lg bg-transparent text-teal-600 hover:bg-muted/50 hover:text-teal-700 dark:text-teal-400 dark:hover:text-teal-300 transition-colors min-w-[120px] max-w-[180px] md:min-w-[160px] md:max-w-[280px] lg:max-w-[400px] xl:max-w-[500px]`;
case 'title':
return `h-11 px-4 text-lg font-bold rounded-lg bg-gradient-to-r from-teal-500/30 to-teal-500/40 text-teal-600 hover:from-teal-500/50 hover:to-teal-500/60 hover:text-teal-700 focus-visible:ring-2 focus-visible:ring-teal-500/50 focus-visible:ring-offset-2 border border-teal-500/40 dark:text-teal-400 dark:hover:text-teal-300 dark:border-teal-400 transition-all duration-200 shadow-lg hover:shadow-xl`;
default:
return `h-10 px-3 text-sm rounded-lg bg-teal-500/40 text-teal-600 hover:bg-teal-500/50 hover:text-teal-700 focus-visible:ring-2 focus-visible:ring-teal-500/50 focus-visible:ring-offset-2 border border-teal-500/50 dark:text-teal-400 dark:hover:text-teal-300 dark:border-teal-400 transition-all duration-200 shadow-lg hover:shadow-xl`;
}
};
return (
<>
<DropdownMenu open={open} onOpenChange={setOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant={mode === 'badge' ? 'ghost' : 'default'}
size="sm"
className={getButtonClassName(mode)}
disabled={switching}
>
<div className="flex items-center justify-between w-full min-w-0">
{mode !== 'badge' && (
<Sparkles className="w-4 h-4 mr-2 flex-shrink-0" />
)}
<span
className={cn(
'truncate min-w-0',
mode === 'badge'
? 'flex-1 text-left'
: 'flex-1 text-center px-1'
)}
>
{switching
? 'Switching...'
: mode === 'title'
? `Agent: ${currentLabel}`
: currentLabel}
</span>
<ChevronDown className="w-4 h-4 ml-1.5 flex-shrink-0 opacity-60" />
</div>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom">Select agent</TooltipContent>
</Tooltip>
<DropdownMenuContent
align="start"
className="w-80 max-h-96 overflow-y-auto shadow-xl border-border/30"
>
{loading && (
<DropdownMenuItem disabled className="text-center text-muted-foreground">
Loading agents...
</DropdownMenuItem>
)}
{!loading && (
<>
{/* Create New Agent Button */}
<DropdownMenuItem
onClick={() => {
setCreateModalOpen(true);
setOpen(false);
}}
disabled={switching}
className="cursor-pointer py-3 px-3 mx-1 my-1 bg-gradient-to-r from-primary/10 to-primary/5 hover:from-primary/15 hover:to-primary/10 border border-primary/20 hover:border-primary/30 transition-all rounded-md shadow-sm"
>
<div className="flex items-center gap-2 w-full">
<Plus className="w-4 h-4 text-primary" />
<span className="font-semibold text-primary">New Agent</span>
</div>
</DropdownMenuItem>
{/* Current Agent (if loaded from file and not in installed list) */}
{currentAgentPath &&
!installed.some((a) => a.id === currentAgentPath.name) && (
<>
<div className="px-3 py-2 mt-1 text-xs font-bold text-teal-600 dark:text-teal-400 uppercase tracking-wider border-b border-border/20">
Currently Active
</div>
<DropdownMenuItem
onClick={() =>
handleSwitchToPath({
id: currentAgentPath.name,
name: currentAgentPath.name,
path: currentAgentPath.path,
})
}
disabled={
switching || currentId === currentAgentPath.name
}
className="cursor-pointer py-3 px-3 hover:bg-muted/60 transition-colors rounded-md mx-1"
>
<div className="flex items-center justify-between w-full">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">
{currentAgentPath.name}
</span>
{currentId === currentAgentPath.name && (
<Check className="w-4 h-4 text-green-600 flex-shrink-0 animate-in fade-in duration-200" />
)}
</div>
<p className="text-xs text-muted-foreground mt-0.5 truncate">
Loaded from file
</p>
</div>
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
{/* Recent Agents */}
{recentAgents.length > 0 && (
<>
<div className="px-3 py-2 mt-1 text-xs font-bold text-foreground/70 uppercase tracking-wider border-b border-border/20">
Recent
</div>
{recentAgents
.filter(
(ra) =>
!installed.some((a) => a.id === ra.id) &&
ra.id !== currentAgentPath?.name &&
!isGlobalAgent(ra.path) // Filter out global dexto directory agents
)
.slice(0, 3)
.map((agent) => (
<DropdownMenuItem
key={agent.path}
onClick={() => handleSwitchToPath(agent)}
disabled={switching || agent.id === currentId}
className="cursor-pointer py-3 px-3 hover:bg-muted/60 transition-colors rounded-md mx-1"
>
<div className="flex items-center justify-between w-full">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">
{agent.name}
</span>
{agent.id === currentId && (
<Check className="w-4 h-4 text-green-600 flex-shrink-0 animate-in fade-in duration-200" />
)}
</div>
<p
className="text-xs text-muted-foreground mt-0.5 truncate"
title={agent.path}
>
{agent.path}
</p>
</div>
</div>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
</>
)}
{/* Installed Custom Agents */}
{installed.filter((a) => a.type === 'custom').length > 0 && (
<>
<div className="px-3 py-2 mt-1 text-xs font-bold text-primary uppercase tracking-wider flex items-center gap-1 border-b border-border/20">
<BadgeCheck className="w-3 h-3" />
Custom Agents
</div>
{installed
.filter((a) => a.type === 'custom')
.map((agent) => (
<DropdownMenuItem
key={agent.id}
onClick={() => handleSwitch(agent.id)}
disabled={switching || agent.id === currentId}
className="cursor-pointer py-3 px-3 hover:bg-muted/60 transition-colors rounded-md mx-1"
>
<div className="flex items-center justify-between w-full gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">
{agent.name}
</span>
{agent.id === currentId && (
<Check className="w-4 h-4 text-green-600 flex-shrink-0 animate-in fade-in duration-200" />
)}
</div>
<p className="text-xs text-muted-foreground mt-0.5 truncate">
{agent.description}
</p>
{agent.author && (
<p className="text-xs text-muted-foreground/80 mt-0.5">
by {agent.author}
</p>
)}
</div>
<button
onClick={(e) => handleDelete(agent, e)}
disabled={switching}
className="p-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded transition-colors"
title="Delete custom agent"
>
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
</button>
</div>
</DropdownMenuItem>
))}
</>
)}
{/* Installed Builtin Agents */}
{installed.filter((a) => a.type === 'builtin').length > 0 && (
<>
{installed.filter((a) => a.type === 'custom').length > 0 && (
<DropdownMenuSeparator />
)}
<div className="px-3 py-2 mt-1 text-xs font-bold text-foreground/70 uppercase tracking-wider border-b border-border/20">
Installed
</div>
{installed
.filter((a) => a.type === 'builtin')
.map((agent) => (
<DropdownMenuItem
key={agent.id}
onClick={() => handleSwitch(agent.id)}
disabled={switching || agent.id === currentId}
className="cursor-pointer py-3 px-3 hover:bg-muted/60 transition-colors rounded-md mx-1"
>
<div className="flex items-center justify-between w-full">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">
{agent.name}
</span>
{agent.id === currentId && (
<Check className="w-4 h-4 text-green-600 flex-shrink-0 animate-in fade-in duration-200" />
)}
</div>
<p className="text-xs text-muted-foreground mt-0.5 truncate">
{agent.description}
</p>
{agent.author && (
<p className="text-xs text-muted-foreground/80 mt-0.5">
by {agent.author}
</p>
)}
</div>
</div>
</DropdownMenuItem>
))}
</>
)}
{/* Available Builtin Agents */}
{available.filter((a) => a.type === 'builtin').length > 0 && (
<>
{installed.length > 0 && <DropdownMenuSeparator />}
<div className="px-3 py-2 mt-1 text-xs font-bold text-foreground/70 uppercase tracking-wider border-b border-border/20">
Available
</div>
{available
.filter((a) => a.type === 'builtin')
.map((agent) => (
<DropdownMenuItem
key={agent.id}
onClick={() => handleInstall(agent.id)}
disabled={switching}
className="cursor-pointer py-3 px-3 hover:bg-muted/60 transition-colors rounded-md mx-1"
>
<div className="flex items-center justify-between w-full">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">
{agent.name}
</span>
<DownloadCloud className="w-4 h-4 text-blue-600 flex-shrink-0" />
</div>
<p className="text-xs text-muted-foreground mt-0.5 truncate">
{agent.description}
</p>
{agent.author && (
<p className="text-xs text-muted-foreground/80 mt-0.5">
by {agent.author}
</p>
)}
</div>
</div>
</DropdownMenuItem>
))}
</>
)}
{!loading && installed.length === 0 && available.length === 0 && (
<DropdownMenuItem
disabled
className="text-center text-muted-foreground"
>
No agents found
</DropdownMenuItem>
)}
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<CreateAgentModal
open={createModalOpen}
onOpenChange={setCreateModalOpen}
onAgentCreated={handleAgentCreated}
/>
</>
);
}