mirror of https://github.com/kortix-ai/suna.git
328 lines
12 KiB
TypeScript
328 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useMemo } from 'react';
|
|
import { AlertTriangle, ExternalLink, X, Square, Loader2 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
import Link from 'next/link';
|
|
import { useStopAgentMutation } from '@/hooks/react-query/threads/use-agent-run';
|
|
import { AgentRun, getAgentRuns } from '@/lib/api';
|
|
import { toast } from 'sonner';
|
|
import { useQueries, useQueryClient } from '@tanstack/react-query';
|
|
import { getThread, getProject } from '@/hooks/react-query/threads/utils';
|
|
import { threadKeys } from '@/hooks/react-query/threads/keys';
|
|
|
|
interface RunningThreadInfo {
|
|
threadId: string;
|
|
name: string;
|
|
projectId: string | null;
|
|
projectName: string;
|
|
agentRun: AgentRun | null;
|
|
isLoading: boolean;
|
|
error?: boolean;
|
|
}
|
|
|
|
interface RunningThreadItemProps {
|
|
threadInfo: RunningThreadInfo;
|
|
onThreadStopped: () => void;
|
|
}
|
|
|
|
const RunningThreadItem: React.FC<RunningThreadItemProps> = ({
|
|
threadInfo,
|
|
onThreadStopped,
|
|
}) => {
|
|
const stopAgentMutation = useStopAgentMutation();
|
|
const queryClient = useQueryClient();
|
|
|
|
const handleStop = async () => {
|
|
if (!threadInfo.agentRun?.id) return;
|
|
|
|
try {
|
|
await stopAgentMutation.mutateAsync(threadInfo.agentRun.id);
|
|
|
|
// Invalidate relevant queries to refetch updated data
|
|
await Promise.all([
|
|
// Refetch agent runs for this thread to update the running status
|
|
queryClient.invalidateQueries({
|
|
queryKey: threadKeys.agentRuns(threadInfo.threadId)
|
|
}),
|
|
// Refetch thread details in case the status affects the thread
|
|
queryClient.invalidateQueries({
|
|
queryKey: threadKeys.details(threadInfo.threadId)
|
|
})
|
|
]);
|
|
|
|
toast.success('Agent stopped successfully');
|
|
onThreadStopped();
|
|
} catch (error) {
|
|
console.error('Failed to stop agent:', error);
|
|
toast.error('Failed to stop agent');
|
|
}
|
|
};
|
|
|
|
const getThreadDisplayName = () => {
|
|
if (threadInfo.name && threadInfo.name.trim()) {
|
|
return threadInfo.name;
|
|
}
|
|
return `Thread ${threadInfo.threadId.slice(0, 8)}...`;
|
|
};
|
|
|
|
const getProjectDisplayName = () => {
|
|
if (threadInfo.projectName && threadInfo.projectName.trim()) {
|
|
return threadInfo.projectName;
|
|
}
|
|
return threadInfo.projectId ? `Project ${threadInfo.projectId.slice(0, 8)}...` : 'No Project';
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center justify-between rounded-lg border bg-muted/50 px-3 py-2 gap-2">
|
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
|
<div className="min-w-0 flex-1">
|
|
<div className="font-medium text-sm truncate">
|
|
{getProjectDisplayName()}
|
|
</div>
|
|
<code className="text-xs text-muted-foreground/70 truncate block">
|
|
{threadInfo.threadId}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
{threadInfo.agentRun && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 w-7 p-0 hover:bg-red-100 hover:text-red-600"
|
|
onClick={handleStop}
|
|
disabled={stopAgentMutation.isPending || threadInfo.isLoading}
|
|
>
|
|
{stopAgentMutation.isPending ? (
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
) : (
|
|
<Square className="h-3 w-3" />
|
|
)}
|
|
<span className="sr-only">Stop agent</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Stop this agent</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
|
|
{threadInfo.projectId && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
asChild
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 w-7 p-0 hover:bg-background"
|
|
>
|
|
<Link href={`/projects/${threadInfo.projectId}/thread/${threadInfo.threadId}`} target="_blank">
|
|
<ExternalLink className="h-3 w-3" />
|
|
<span className="sr-only">Open thread</span>
|
|
</Link>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Open thread</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface AgentRunLimitDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
runningCount: number;
|
|
runningThreadIds: string[];
|
|
projectId?: string;
|
|
}
|
|
|
|
export const AgentRunLimitDialog: React.FC<AgentRunLimitDialogProps> = ({
|
|
open,
|
|
onOpenChange,
|
|
runningCount,
|
|
runningThreadIds,
|
|
projectId,
|
|
}) => {
|
|
// Use the exact same query keys as existing hooks for perfect cache consistency with sidebar
|
|
// This means data fetched by the sidebar is immediately available here without additional requests
|
|
const threadQueries = useQueries({
|
|
queries: runningThreadIds.map(threadId => ({
|
|
queryKey: threadKeys.details(threadId),
|
|
queryFn: () => getThread(threadId),
|
|
enabled: open && !!threadId,
|
|
retry: 1,
|
|
}))
|
|
});
|
|
|
|
const agentRunQueries = useQueries({
|
|
queries: runningThreadIds.map(threadId => ({
|
|
queryKey: threadKeys.agentRuns(threadId), // This matches useAgentRunsQuery exactly
|
|
queryFn: () => getAgentRuns(threadId),
|
|
enabled: open && !!threadId,
|
|
retry: 1,
|
|
}))
|
|
});
|
|
|
|
// Use the same query keys as useProjectQuery for cache consistency
|
|
const projectQueries = useQueries({
|
|
queries: runningThreadIds.map(threadId => {
|
|
const threadQuery = threadQueries.find((_, index) => runningThreadIds[index] === threadId);
|
|
const projectId = threadQuery?.data?.project_id;
|
|
|
|
return {
|
|
queryKey: threadKeys.project(projectId || ""),
|
|
queryFn: () => projectId ? getProject(projectId) : null,
|
|
enabled: open && !!projectId,
|
|
retry: 1,
|
|
};
|
|
})
|
|
});
|
|
|
|
// Process the React Query results into our thread info structure
|
|
const runningThreadsInfo: RunningThreadInfo[] = useMemo(() => {
|
|
return runningThreadIds.map((threadId, index) => {
|
|
const threadQuery = threadQueries[index];
|
|
const agentRunQuery = agentRunQueries[index];
|
|
const projectQuery = projectQueries[index];
|
|
|
|
const isLoading = threadQuery.isLoading || agentRunQuery.isLoading || projectQuery.isLoading;
|
|
const hasError = threadQuery.isError || agentRunQuery.isError || projectQuery.isError;
|
|
|
|
// Find the running agent run for this thread
|
|
const runningAgentRun = agentRunQuery.data?.find((run: AgentRun) => run.status === 'running') || null;
|
|
|
|
// Get thread name from first user message if available
|
|
let threadName = '';
|
|
if (threadQuery.data?.messages?.length > 0) {
|
|
const firstUserMessage = threadQuery.data.messages.find((msg: any) => msg.type === 'user');
|
|
if (firstUserMessage?.content) {
|
|
threadName = firstUserMessage.content.substring(0, 50);
|
|
if (firstUserMessage.content.length > 50) threadName += '...';
|
|
}
|
|
}
|
|
|
|
// Get project information
|
|
const projectId = threadQuery.data?.project_id || null;
|
|
const projectName = projectQuery.data?.name || '';
|
|
|
|
return {
|
|
threadId,
|
|
name: threadName,
|
|
projectId,
|
|
projectName,
|
|
agentRun: runningAgentRun,
|
|
isLoading,
|
|
error: hasError,
|
|
};
|
|
});
|
|
}, [runningThreadIds, threadQueries, agentRunQueries, projectQueries]);
|
|
|
|
const isLoadingThreads = threadQueries.some(q => q.isLoading) || agentRunQueries.some(q => q.isLoading) || projectQueries.some(q => q.isLoading);
|
|
|
|
const handleClose = () => {
|
|
onOpenChange(false);
|
|
};
|
|
|
|
const handleThreadStopped = () => {
|
|
// No need to close the dialog - the queries will automatically refetch
|
|
// and update the UI to show the current running state
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900">
|
|
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
|
</div>
|
|
<DialogTitle className="text-lg font-semibold">
|
|
Parallel Runs Limit Reached
|
|
</DialogTitle>
|
|
</div>
|
|
<DialogDescription className="text-sm text-muted-foreground">
|
|
You've reached the maximum parallel agent runs allowed.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{(runningThreadIds.length > 0 || runningCount > 0) && (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<h4 className="text-sm font-medium">Currently Running Agents</h4>
|
|
</div>
|
|
|
|
{isLoadingThreads ? (
|
|
<div className="flex items-center justify-center py-4">
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
<span className="ml-2 text-sm text-muted-foreground">Loading threads...</span>
|
|
</div>
|
|
) : runningThreadIds.length === 0 ? (
|
|
<div className="text-center py-4 text-sm text-muted-foreground">
|
|
<p>Found {runningCount} running agents but unable to load thread details.</p>
|
|
<p className="text-xs mt-1">Thread IDs: {JSON.stringify(runningThreadIds)}</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2 max-h-64 overflow-y-auto">
|
|
{runningThreadsInfo.slice(0, 5).map((threadInfo) => (
|
|
<RunningThreadItem
|
|
key={threadInfo.threadId}
|
|
threadInfo={threadInfo}
|
|
onThreadStopped={handleThreadStopped}
|
|
/>
|
|
))}
|
|
|
|
{runningThreadsInfo.length > 5 && (
|
|
<div className="text-center py-2">
|
|
<Badge variant="outline" className="text-xs">
|
|
+{runningThreadsInfo.length - 5} more running
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-3">
|
|
<h4 className="text-sm font-medium">What can you do?</h4>
|
|
<ul className="space-y-2 text-sm text-muted-foreground">
|
|
<li className="flex items-start gap-2">
|
|
<div className="h-1.5 w-1.5 rounded-full bg-muted-foreground mt-2 flex-shrink-0" />
|
|
<span>Click the <Square className="h-3 w-3 inline mx-1" /> button to stop running agents</span>
|
|
</li>
|
|
<li className="flex items-start gap-2">
|
|
<div className="h-1.5 w-1.5 rounded-full bg-muted-foreground mt-2 flex-shrink-0" />
|
|
<span>Wait for an agent to complete automatically</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-2">
|
|
<Button variant="outline" onClick={handleClose}>
|
|
Got it
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}; |