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>
This commit is contained in:
admin
2026-01-28 00:27:56 +04:00
Unverified
parent 3b128ba3bd
commit b52318eeae
1724 changed files with 351216 additions and 0 deletions

View File

@@ -0,0 +1,688 @@
import React, { useState, useEffect } from 'react';
import {
useSessions,
useCreateSession,
useDeleteSession,
useRenameSession,
type Session,
} from './hooks/useSessions';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from './ui/dialog';
import { ScrollArea } from './ui/scroll-area';
import {
Trash2,
AlertTriangle,
RefreshCw,
History,
Search,
Plus,
MoreHorizontal,
Pencil,
Copy,
Check,
ChevronLeft,
Settings,
FlaskConical,
Moon,
Sun,
} from 'lucide-react';
import { Alert, AlertDescription } from './ui/alert';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
} from './ui/dropdown-menu';
import { cn } from '@/lib/utils';
interface SessionPanelProps {
isOpen: boolean;
onClose: () => void;
onExpand?: () => void;
currentSessionId?: string | null;
onSessionChange: (sessionId: string) => void;
returnToWelcome: () => void;
variant?: 'inline' | 'overlay';
onSearchOpen?: () => void;
onNewChat?: () => void;
// App-level actions
onSettingsOpen?: () => void;
onPlaygroundOpen?: () => void;
onThemeToggle?: () => void;
theme?: 'light' | 'dark';
}
function sortSessions(sessions: Session[]): Session[] {
return sessions.sort((a, b) => {
const timeA = a.lastActivity ? new Date(a.lastActivity).getTime() : 0;
const timeB = b.lastActivity ? new Date(b.lastActivity).getTime() : 0;
return timeB - timeA;
});
}
export default function SessionPanel({
isOpen,
onClose,
onExpand,
currentSessionId,
onSessionChange,
returnToWelcome,
variant = 'overlay',
onSearchOpen,
onNewChat,
onSettingsOpen,
onPlaygroundOpen,
onThemeToggle,
theme,
}: SessionPanelProps) {
const [isNewSessionOpen, setNewSessionOpen] = useState(false);
const [newSessionId, setNewSessionId] = useState('');
const [isDeleteConversationDialogOpen, setDeleteConversationDialogOpen] = useState(false);
const [selectedSessionForAction, setSelectedSessionForAction] = useState<string | null>(null);
const [isRenameDialogOpen, setRenameDialogOpen] = useState(false);
const [renameValue, setRenameValue] = useState('');
const [copiedSessionId, setCopiedSessionId] = useState<string | null>(null);
const { data: sessionsData = [], isLoading: loading, error } = useSessions(isOpen);
// Sort sessions by last activity for display
const sessions = sortSessions([...sessionsData]);
const createSessionMutation = useCreateSession();
const deleteSessionMutation = useDeleteSession();
const renameSessionMutation = useRenameSession();
// Note: Agent switch invalidation is now handled centrally in AgentSelector
// Message/response/title events are handled in useChat via direct cache updates
const handleCreateSession = async () => {
const newSession = await createSessionMutation.mutateAsync({
sessionId: newSessionId.trim() || undefined,
});
setNewSessionId('');
setNewSessionOpen(false);
onSessionChange(newSession.id);
};
const handleDeleteSession = async (sessionId: string) => {
await deleteSessionMutation.mutateAsync({ sessionId });
const isDeletingCurrentSession = currentSessionId === sessionId;
if (isDeletingCurrentSession) {
returnToWelcome();
}
};
const handleDeleteConversation = async () => {
if (!selectedSessionForAction) return;
await deleteSessionMutation.mutateAsync({ sessionId: selectedSessionForAction });
const isDeletingCurrentSession = currentSessionId === selectedSessionForAction;
if (isDeletingCurrentSession) {
returnToWelcome();
}
setDeleteConversationDialogOpen(false);
setSelectedSessionForAction(null);
};
const handleOpenRenameDialog = (sessionId: string, currentTitle: string | null) => {
setSelectedSessionForAction(sessionId);
setRenameValue(currentTitle || '');
setRenameDialogOpen(true);
};
const handleRenameSession = async () => {
if (!selectedSessionForAction || !renameValue.trim()) return;
try {
await renameSessionMutation.mutateAsync({
sessionId: selectedSessionForAction,
title: renameValue.trim(),
});
setRenameDialogOpen(false);
setSelectedSessionForAction(null);
setRenameValue('');
} catch (error) {
// Error is already logged by React Query, keep dialog open for retry
console.error(`Failed to rename session: ${error}`);
}
};
const handleCopySessionId = async (sessionId: string) => {
try {
await navigator.clipboard.writeText(sessionId);
setCopiedSessionId(sessionId);
} catch (error) {
console.error(`Failed to copy session ID: ${error}`);
}
};
// Clean up copy feedback timeout
useEffect(() => {
if (copiedSessionId) {
const timeoutId = setTimeout(() => setCopiedSessionId(null), 2000);
return () => clearTimeout(timeoutId);
}
}, [copiedSessionId]);
const formatRelativeTime = (timestamp: number | null) => {
if (!timestamp) return 'Unknown';
const now = Date.now();
const diff = now - timestamp;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
return `${days}d ago`;
};
const content = (
<div className="flex flex-col h-full">
{/* Header with Dexto branding */}
<div className="px-4 py-5">
<div className="flex items-center justify-between">
{/* Dexto logo */}
<div id="sessionpanel-title" className="flex items-center px-2">
{/* Light mode logo */}
<img
src="/logos/dexto/dexto_logo_light.svg"
alt="Dexto"
className="h-6 w-auto dark:hidden"
/>
{/* Dark mode logo */}
<img
src="/logos/dexto/dexto_logo.svg"
alt="Dexto"
className="h-6 w-auto hidden dark:block"
/>
</div>
{/* Collapse button */}
<button
onClick={onClose}
className="flex items-center justify-center w-8 h-8 rounded-lg text-muted-foreground hover:bg-muted/40 hover:text-foreground transition-colors"
aria-label="Collapse panel"
>
<ChevronLeft className="h-5 w-5" />
</button>
</div>
</div>
{/* Error Display */}
{error && (
<div className="p-4">
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{error.message}</AlertDescription>
</Alert>
</div>
)}
{/* Sessions List */}
<ScrollArea className="flex-1 scrollbar-thin">
{/* Action items at top of list */}
<div className="px-3 pt-2 pb-1 space-y-0.5">
{onNewChat && (
<button
onClick={onNewChat}
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:bg-muted/40 hover:text-foreground transition-colors"
>
<Plus className="h-4 w-4" />
<span>New Chat</span>
</button>
)}
{onSearchOpen && (
<button
onClick={onSearchOpen}
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:bg-muted/40 hover:text-foreground transition-colors"
>
<Search className="h-4 w-4" />
<span>Search</span>
</button>
)}
</div>
{/* Spacer */}
{(onNewChat || onSearchOpen) && <div className="h-2" />}
{/* History Header */}
{!loading && sessions.length > 0 && (
<div className="px-4 py-2">
<h2 className="text-xs font-semibold text-muted-foreground/70 uppercase tracking-wider">
History
</h2>
</div>
)}
{loading ? (
<div className="flex items-center justify-center py-12">
<RefreshCw className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : sessions.length === 0 ? (
<div className="text-center py-12 px-6">
<History className="h-10 w-10 mx-auto mb-3 text-muted-foreground/30" />
<p className="text-sm text-muted-foreground">No conversations yet</p>
<p className="text-xs text-muted-foreground/60 mt-1">
Start chatting to see your history
</p>
</div>
) : (
<div className="px-3 py-2 space-y-0.5">
{sessions.map((session) => {
const title =
session.title && session.title.trim().length > 0
? session.title
: session.id;
const isActive = currentSessionId === session.id;
return (
<div
key={session.id}
className={cn(
'group relative px-3 py-1.5 rounded-lg transition-all cursor-pointer',
isActive
? 'bg-primary/5 before:absolute before:left-0 before:top-1.5 before:bottom-1.5 before:w-0.5 before:bg-primary before:rounded-full'
: 'hover:bg-muted/40'
)}
role="button"
tabIndex={0}
aria-current={isActive ? 'page' : undefined}
onClick={() => onSessionChange(session.id)}
onKeyDown={(e) => {
if (e.target !== e.currentTarget) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onSessionChange(session.id);
}
}}
>
<div className="flex items-center justify-between gap-2">
<h3
className={cn(
'text-sm truncate flex-1 min-w-0',
isActive
? 'font-medium text-foreground'
: 'text-muted-foreground'
)}
>
{title}
</h3>
{/* Timestamp - hidden on hover */}
<span className="text-[10px] text-muted-foreground/50 shrink-0 group-hover:opacity-0 transition-opacity">
{formatRelativeTime(session.lastActivity)}
</span>
{/* Dropdown - shown on hover, positioned to overlap timestamp */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={(e) => e.stopPropagation()}
className="h-7 w-7 p-0 absolute right-2 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity"
aria-label="Session options"
>
<MoreHorizontal className="h-4 w-4 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-48"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuItem
onClick={() =>
handleOpenRenameDialog(
session.id,
session.title ?? null
)
}
>
<Pencil className="h-4 w-4 mr-2" />
Rename
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleCopySessionId(session.id)}
>
{copiedSessionId === session.id ? (
<Check className="h-4 w-4 mr-2 text-green-500" />
) : (
<Copy className="h-4 w-4 mr-2" />
)}
{copiedSessionId === session.id
? 'Copied!'
: 'Copy Session ID'}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
if (session.messageCount > 0) {
setSelectedSessionForAction(session.id);
setDeleteConversationDialogOpen(true);
} else {
handleDeleteSession(session.id);
}
}}
className="text-destructive focus:text-destructive"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
})}
</div>
)}
</ScrollArea>
{/* Footer with app-level actions */}
<div className="border-t border-border/30 p-3 space-y-1">
{/* Developer Tools */}
{onPlaygroundOpen && (
<button
onClick={onPlaygroundOpen}
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:bg-muted/40 hover:text-foreground transition-colors"
>
<FlaskConical className="h-4 w-4" />
<span>MCP Playground</span>
</button>
)}
{/* Separator */}
{onPlaygroundOpen && (onThemeToggle || onSettingsOpen) && (
<div className="h-px bg-border/30 my-1" />
)}
{/* Theme Toggle */}
{onThemeToggle && (
<button
onClick={onThemeToggle}
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:bg-muted/40 hover:text-foreground transition-colors"
>
{theme === 'dark' ? (
<Sun className="h-4 w-4 transition-transform duration-200 hover:rotate-180" />
) : (
<Moon className="h-4 w-4 transition-transform duration-200 hover:rotate-12" />
)}
<span>{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span>
</button>
)}
{/* Settings */}
{onSettingsOpen && (
<button
onClick={onSettingsOpen}
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:bg-muted/40 hover:text-foreground transition-colors"
>
<Settings className="h-4 w-4" />
<span>Settings</span>
</button>
)}
</div>
{/* New Chat Dialog */}
<Dialog open={isNewSessionOpen} onOpenChange={setNewSessionOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Start New Chat</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="sessionId">Chat ID</Label>
<Input
id="sessionId"
value={newSessionId}
onChange={(e) => setNewSessionId(e.target.value)}
placeholder="e.g., user-123, project-alpha"
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
Leave empty to auto-generate a unique ID
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setNewSessionOpen(false)}>
Cancel
</Button>
<Button onClick={handleCreateSession}>Start Chat</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Conversation Confirmation Dialog */}
<Dialog
open={isDeleteConversationDialogOpen}
onOpenChange={setDeleteConversationDialogOpen}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<Trash2 className="h-5 w-5 text-destructive" />
<span>Delete Conversation</span>
</DialogTitle>
<DialogDescription>
This will permanently delete this conversation and all its messages.
This action cannot be undone.
{selectedSessionForAction && (
<span className="block mt-2 font-medium">
Session:{' '}
<span className="font-mono">{selectedSessionForAction}</span>
</span>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeleteConversationDialogOpen(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteConversation}
disabled={
deleteSessionMutation.isPending &&
deleteSessionMutation.variables?.sessionId ===
selectedSessionForAction
}
className="flex items-center space-x-2"
>
<Trash2 className="h-4 w-4" />
<span>
{deleteSessionMutation.isPending &&
deleteSessionMutation.variables?.sessionId ===
selectedSessionForAction
? 'Deleting...'
: 'Delete Conversation'}
</span>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Rename Session Dialog */}
<Dialog open={isRenameDialogOpen} onOpenChange={setRenameDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<Pencil className="h-5 w-5" />
<span>Rename Chat</span>
</DialogTitle>
<DialogDescription>
Enter a new name for this conversation.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="renameTitle">Chat Name</Label>
<Input
id="renameTitle"
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
placeholder="Enter chat name..."
onKeyDown={(e) => {
if (e.key === 'Enter' && renameValue.trim()) {
handleRenameSession();
}
}}
autoFocus
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setRenameDialogOpen(false)}>
Cancel
</Button>
<Button
onClick={handleRenameSession}
disabled={!renameValue.trim() || renameSessionMutation.isPending}
>
{renameSessionMutation.isPending ? 'Saving...' : 'Save'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
// Collapsed sidebar content - thin bar with icon buttons
const collapsedContent = (
<div className="flex flex-col h-full py-3 px-2 items-center">
{/* Dexto icon - click to expand */}
<button
onClick={onExpand}
className="flex items-center justify-center w-10 h-10 rounded-lg hover:bg-muted/40 transition-colors mb-3"
aria-label="Expand panel"
>
<img src="/logos/dexto/dexto_logo_icon.svg" alt="Dexto" className="h-7 w-7" />
</button>
{/* Action items with subtle spacing */}
<div className="flex flex-col gap-1 flex-1">
{/* New Chat */}
{onNewChat && (
<button
onClick={onNewChat}
className="flex items-center justify-center w-10 h-10 rounded-lg text-muted-foreground hover:bg-muted/40 hover:text-foreground transition-colors"
aria-label="New chat"
>
<Plus className="h-5 w-5" />
</button>
)}
{/* Search */}
{onSearchOpen && (
<button
onClick={onSearchOpen}
className="flex items-center justify-center w-10 h-10 rounded-lg text-muted-foreground hover:bg-muted/40 hover:text-foreground transition-colors"
aria-label="Search"
>
<Search className="h-5 w-5" />
</button>
)}
</div>
{/* Footer actions - playground, theme, settings */}
<div className="flex flex-col gap-1 pt-2 border-t border-border/30">
{/* Playground */}
{onPlaygroundOpen && (
<button
onClick={onPlaygroundOpen}
className="flex items-center justify-center w-10 h-10 rounded-lg text-muted-foreground hover:bg-muted/40 hover:text-foreground transition-colors"
aria-label="MCP Playground"
>
<FlaskConical className="h-5 w-5" />
</button>
)}
{/* Theme Toggle */}
{onThemeToggle && (
<button
onClick={onThemeToggle}
className="flex items-center justify-center w-10 h-10 rounded-lg text-muted-foreground hover:bg-muted/40 hover:text-foreground transition-colors"
aria-label={theme === 'dark' ? 'Light mode' : 'Dark mode'}
>
{theme === 'dark' ? (
<Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />
)}
</button>
)}
{/* Settings */}
{onSettingsOpen && (
<button
onClick={onSettingsOpen}
className="flex items-center justify-center w-10 h-10 rounded-lg text-muted-foreground hover:bg-muted/40 hover:text-foreground transition-colors"
aria-label="Settings"
>
<Settings className="h-5 w-5" />
</button>
)}
</div>
</div>
);
// For inline variant, show collapsed or expanded
if (variant === 'inline') {
if (!isOpen) {
// Collapsed state - thin bar
return (
<div className="h-full flex flex-col bg-card border-r border-border/30">
{collapsedContent}
</div>
);
}
// Expanded state - full panel
return <div className="h-full w-full flex flex-col bg-card">{content}</div>;
}
// Overlay variant with slide animation
return (
<>
{/* Backdrop */}
<div
className={cn(
'fixed inset-0 bg-black/50 z-30 transition-opacity duration-300',
isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
)}
onClick={onClose}
/>
{/* Panel */}
<aside
role="dialog"
aria-modal="true"
aria-labelledby="sessionpanel-title"
tabIndex={-1}
className={cn(
'fixed top-0 left-0 z-40 h-screen w-72 bg-card border-r border-border shadow-xl transition-transform duration-300 ease-in-out flex flex-col',
isOpen ? 'translate-x-0' : '-translate-x-full'
)}
>
{content}
</aside>
</>
);
}