mirror of https://github.com/kortix-ai/suna.git
sidebar hover states
This commit is contained in:
parent
c4cb7bc920
commit
296a68e8d1
|
@ -16,6 +16,9 @@ type ThreadParams = { id: string; threadId: string };
|
||||||
interface ApiMessage {
|
interface ApiMessage {
|
||||||
role: string;
|
role: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
type?: 'content' | 'tool_call';
|
||||||
|
name?: string;
|
||||||
|
arguments?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiAgentRun {
|
interface ApiAgentRun {
|
||||||
|
@ -71,14 +74,37 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
|
|
||||||
// Start streaming the agent's responses with improved implementation
|
// Start streaming the agent's responses with improved implementation
|
||||||
const cleanup = streamAgent(runId, {
|
const cleanup = streamAgent(runId, {
|
||||||
onMessage: (content: string) => {
|
onMessage: (rawData: string) => {
|
||||||
// Skip empty content chunks
|
try {
|
||||||
if (!content.trim()) return;
|
// Parse the outer data structure
|
||||||
|
const data = JSON.parse(rawData);
|
||||||
|
|
||||||
// Improved stream update with requestAnimationFrame for smoother UI updates
|
// Handle the nested data structure
|
||||||
window.requestAnimationFrame(() => {
|
if (data.content?.startsWith('data: ')) {
|
||||||
setStreamContent(prev => prev + content);
|
try {
|
||||||
});
|
const innerJson = data.content.replace('data: ', '');
|
||||||
|
const innerData = JSON.parse(innerJson);
|
||||||
|
|
||||||
|
// Skip empty messages
|
||||||
|
if (!innerData.content && !innerData.arguments) return;
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (innerData.type === 'tool_call') {
|
||||||
|
const toolContent = innerData.name
|
||||||
|
? `Tool: ${innerData.name}\n${innerData.arguments || ''}`
|
||||||
|
: innerData.arguments || '';
|
||||||
|
setStreamContent(prev => prev + (prev ? '\n' : '') + toolContent);
|
||||||
|
} else if (innerData.type === 'content' && innerData.content) {
|
||||||
|
setStreamContent(prev => prev + innerData.content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (innerError) {
|
||||||
|
console.warn('[PAGE] Failed to parse inner data:', innerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[PAGE] Failed to parse message:', error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onToolCall: (name: string, args: Record<string, unknown>) => {
|
onToolCall: (name: string, args: Record<string, unknown>) => {
|
||||||
console.log('[PAGE] Tool call received:', name, args);
|
console.log('[PAGE] Tool call received:', name, args);
|
||||||
|
@ -564,7 +590,16 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
: 'bg-muted'
|
: 'bg-muted'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="whitespace-pre-wrap break-words">{message.content}</div>
|
<div className="whitespace-pre-wrap break-words">
|
||||||
|
{message.type === 'tool_call' ? (
|
||||||
|
<div className="font-mono text-xs">
|
||||||
|
<div className="text-muted-foreground">Tool: {message.name}</div>
|
||||||
|
<div className="mt-1">{message.arguments}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
message.content
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -164,11 +164,55 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sidebarLogoMotion = {
|
||||||
|
tap: { scale: 0.97 },
|
||||||
|
hover: {
|
||||||
|
scale: 1.02,
|
||||||
|
transition: { type: "spring", stiffness: 400, damping: 17 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectItemVariants = {
|
||||||
|
initial: { opacity: 0, y: -5 },
|
||||||
|
animate: { opacity: 1, y: 0 },
|
||||||
|
exit: { opacity: 0, y: -5 },
|
||||||
|
transition: { duration: 0.2 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const threadListVariants = {
|
||||||
|
hidden: { opacity: 0, height: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
height: "auto",
|
||||||
|
transition: {
|
||||||
|
height: {
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 500,
|
||||||
|
damping: 30
|
||||||
|
},
|
||||||
|
opacity: { duration: 0.2, delay: 0.05 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
height: 0,
|
||||||
|
transition: {
|
||||||
|
height: { duration: 0.2 },
|
||||||
|
opacity: { duration: 0.1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="offcanvas" {...props}>
|
<Sidebar collapsible="offcanvas" {...props}>
|
||||||
<SidebarHeader className="border-b-0 h-14 px-4 py-3">
|
<SidebarHeader className="border-b-0 h-14 px-4 py-3">
|
||||||
<Link href="/dashboard" className="flex items-center">
|
<Link href="/dashboard" className="flex items-center group">
|
||||||
<div className="group/logo flex items-center">
|
<motion.div
|
||||||
|
className="flex items-center"
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
variants={sidebarLogoMotion}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
@ -177,12 +221,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className="mr-2.5 h-5 w-5 text-black transition-transform duration-200 group-hover/logo:scale-110"
|
className="mr-2.5 h-5 w-5 text-black transition-colors group-hover:text-zinc-800"
|
||||||
>
|
>
|
||||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-lg font-medium tracking-tight">AgentPress</span>
|
<span className="text-lg font-medium tracking-tight group-hover:text-zinc-800">AgentPress</span>
|
||||||
</div>
|
</motion.div>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent className="py-0 px-2">
|
<SidebarContent className="py-0 px-2">
|
||||||
|
@ -191,18 +235,24 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
<div className="flex items-center justify-between px-2 py-1.5">
|
<div className="flex items-center justify-between px-2 py-1.5">
|
||||||
<h3 className="text-xs uppercase tracking-wider text-zinc-500 font-medium">Projects</h3>
|
<h3 className="text-xs uppercase tracking-wider text-zinc-500 font-medium">Projects</h3>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip delayDuration={200}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<motion.div
|
||||||
variant="ghost"
|
whileHover={{ scale: 1.05 }}
|
||||||
size="icon"
|
whileTap={{ scale: 0.95 }}
|
||||||
className="h-6 w-6 text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 rounded-md cursor-pointer transition-colors duration-200"
|
className="cursor-pointer"
|
||||||
onClick={() => setIsDialogOpen(true)}
|
|
||||||
>
|
>
|
||||||
<IconPlus className="size-3.5" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 rounded-md cursor-pointer transition-all duration-150"
|
||||||
|
onClick={() => setIsDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<IconPlus className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent side="right" className="text-xs">
|
||||||
<p>New Project</p>
|
<p>New Project</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -223,93 +273,149 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
projects.map((project) => (
|
projects.map((project) => (
|
||||||
<React.Fragment key={project.id}>
|
<motion.div
|
||||||
|
key={project.id}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
variants={projectItemVariants}
|
||||||
|
>
|
||||||
<li className="relative">
|
<li className="relative">
|
||||||
<div
|
<motion.div
|
||||||
className={`flex items-center justify-between px-2 py-1.5 text-sm rounded-md transition-all duration-200 ${
|
className={`flex items-center justify-between px-2 py-1.5 text-sm rounded-md transition-all duration-200 cursor-pointer ${
|
||||||
expandedProjectId === project.id
|
expandedProjectId === project.id
|
||||||
? 'text-zinc-900 font-medium bg-zinc-50'
|
? 'text-zinc-900 font-medium bg-zinc-50 hover:bg-zinc-100'
|
||||||
: 'text-zinc-700 hover:bg-zinc-50 hover:text-zinc-900'
|
: 'text-zinc-700 hover:bg-zinc-50 hover:text-zinc-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
whileHover={{
|
||||||
<Link
|
scale: 1.01,
|
||||||
href={`/projects/${project.id}`}
|
x: 1
|
||||||
className="flex-1 truncate transition-colors duration-200"
|
}}
|
||||||
>
|
transition={{ duration: 0.15 }}
|
||||||
{project.name}
|
style={{
|
||||||
</Link>
|
backgroundColor: expandedProjectId === project.id ? 'rgb(249 250 251)' : 'transparent'
|
||||||
<Button
|
}}
|
||||||
variant="ghost"
|
initial={false}
|
||||||
size="icon"
|
animate={{
|
||||||
className={`h-5 w-5 p-0 ml-1 transition-all duration-200 ${
|
backgroundColor: expandedProjectId === project.id
|
||||||
expandedProjectId === project.id
|
? 'rgb(249 250 251)'
|
||||||
? 'text-zinc-900 hover:bg-zinc-100'
|
: 'transparent'
|
||||||
: 'text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100'
|
}}
|
||||||
}`}
|
whileTap={{ scale: 0.99 }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
// Either toggle expansion or navigate to project
|
||||||
|
if (expandedProjectId === project.id) {
|
||||||
|
router.push(`/projects/${project.id}`)
|
||||||
|
} else {
|
||||||
toggleProjectExpanded(project.id)
|
toggleProjectExpanded(project.id)
|
||||||
}}
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
aria-expanded={expandedProjectId === project.id}
|
||||||
|
>
|
||||||
|
<div className="flex-1 truncate transition-colors duration-200">
|
||||||
|
{project.name}
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
{expandedProjectId === project.id ? (
|
<Button
|
||||||
<IconChevronDown className="size-3.5" />
|
variant="ghost"
|
||||||
) : (
|
size="icon"
|
||||||
<IconChevronRight className="size-3.5" />
|
className={`h-5 w-5 p-0 ml-1 rounded-full transition-all duration-200 cursor-pointer ${
|
||||||
)}
|
expandedProjectId === project.id
|
||||||
</Button>
|
? 'text-zinc-900 hover:bg-zinc-200 hover:text-zinc-950'
|
||||||
</div>
|
: 'text-zinc-500 hover:text-zinc-900 hover:bg-zinc-200'
|
||||||
|
}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation() // Prevent parent's onClick from firing
|
||||||
|
toggleProjectExpanded(project.id)
|
||||||
|
}}
|
||||||
|
aria-label={expandedProjectId === project.id ? "Collapse project" : "Expand project"}
|
||||||
|
>
|
||||||
|
{expandedProjectId === project.id ? (
|
||||||
|
<IconChevronDown className="size-3.5" />
|
||||||
|
) : (
|
||||||
|
<IconChevronRight className="size-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
</li>
|
</li>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{expandedProjectId === project.id && (
|
{expandedProjectId === project.id && (
|
||||||
<motion.li
|
<motion.li
|
||||||
initial={{ height: 0, opacity: 0 }}
|
variants={threadListVariants}
|
||||||
animate={{ height: "auto", opacity: 1 }}
|
initial="hidden"
|
||||||
exit={{ height: 0, opacity: 0 }}
|
animate="visible"
|
||||||
transition={{ duration: 0.2 }}
|
exit="exit"
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<ul className="pl-4 space-y-0.5">
|
<ul className="pl-4 space-y-0.5 mt-0.5">
|
||||||
{threads[project.id]?.map((thread) => (
|
{threads[project.id]?.map((thread) => (
|
||||||
<li key={thread.thread_id}>
|
<motion.li
|
||||||
|
key={thread.thread_id}
|
||||||
|
whileHover={{
|
||||||
|
x: 2,
|
||||||
|
}}
|
||||||
|
className={`rounded-md ${
|
||||||
|
pathname?.includes(`/threads/${thread.thread_id}`)
|
||||||
|
? 'bg-zinc-50 text-zinc-900 font-medium'
|
||||||
|
: 'text-zinc-700 hover:bg-zinc-50/70 hover:text-zinc-900'
|
||||||
|
}`}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 400,
|
||||||
|
damping: 25
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`/projects/${project.id}/threads/${thread.thread_id}`}
|
href={`/projects/${project.id}/threads/${thread.thread_id}`}
|
||||||
className={`block px-2 py-1.5 text-sm rounded-md transition-all duration-200 ${
|
className="block px-2 py-1.5 text-sm rounded-md transition-all duration-200 cursor-pointer w-full"
|
||||||
pathname?.includes(`/threads/${thread.thread_id}`)
|
|
||||||
? 'text-zinc-900 font-medium bg-zinc-50'
|
|
||||||
: 'text-zinc-700 hover:bg-zinc-50 hover:text-zinc-900'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{thread.messages[0]?.content || 'New Conversation'}
|
<span className="block truncate">
|
||||||
{thread.messages[0]?.content && thread.messages[0].content.length > 30 ? '...' : ''}
|
{thread.messages[0]?.content || 'New Conversation'}
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</motion.li>
|
||||||
))}
|
))}
|
||||||
<li>
|
<motion.li
|
||||||
<Button
|
whileHover={{ x: 2 }}
|
||||||
variant="ghost"
|
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||||
size="sm"
|
className="rounded-md hover:bg-zinc-50/70"
|
||||||
className="w-full justify-start px-2 py-1.5 text-sm text-zinc-700 hover:text-zinc-900 hover:bg-zinc-50 transition-all duration-200"
|
>
|
||||||
onClick={() => handleCreateThread(project.id)}
|
<motion.div
|
||||||
disabled={isCreatingThread[project.id]}
|
whileTap={{ scale: 0.98 }}
|
||||||
|
className="w-full cursor-pointer"
|
||||||
>
|
>
|
||||||
{isCreatingThread[project.id] ? (
|
<Button
|
||||||
<div className="flex items-center gap-2">
|
variant="ghost"
|
||||||
<div className="h-3 w-3 border-2 border-zinc-500 border-t-transparent rounded-full animate-spin" />
|
size="sm"
|
||||||
<span>Creating...</span>
|
className="w-full justify-start px-2 py-1.5 text-sm text-zinc-700 hover:text-zinc-900 transition-all duration-200 cursor-pointer rounded-md"
|
||||||
</div>
|
onClick={() => handleCreateThread(project.id)}
|
||||||
) : (
|
disabled={isCreatingThread[project.id]}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<IconPlus className="size-3.5" />
|
{isCreatingThread[project.id] ? (
|
||||||
<span>New Conversation</span>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<div className="h-3 w-3 border-2 border-zinc-500 border-t-transparent rounded-full animate-spin" />
|
||||||
)}
|
<span>Creating...</span>
|
||||||
</Button>
|
</div>
|
||||||
</li>
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconPlus className="size-3.5" />
|
||||||
|
<span>New Conversation</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.li>
|
||||||
</ul>
|
</ul>
|
||||||
</motion.li>
|
</motion.li>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</React.Fragment>
|
</motion.div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -317,11 +423,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter className="border-t border-border/40">
|
<SidebarFooter className="border-t border-border/40">
|
||||||
{user && (
|
{user && (
|
||||||
<NavUser user={{
|
<motion.div
|
||||||
name: user.email?.split('@')[0] || 'Guest',
|
whileHover={{ y: -1 }}
|
||||||
email: user.email || '',
|
transition={{ type: "spring", stiffness: 500 }}
|
||||||
avatar: '/avatars/user.jpg',
|
className="cursor-pointer"
|
||||||
}} />
|
>
|
||||||
|
<NavUser user={{
|
||||||
|
name: user.email?.split('@')[0] || 'Guest',
|
||||||
|
email: user.email || '',
|
||||||
|
avatar: '/avatars/user.jpg',
|
||||||
|
}} />
|
||||||
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<CreateProjectDialog
|
<CreateProjectDialog
|
||||||
|
|
|
@ -66,7 +66,7 @@ export function NavUser({
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size="lg"
|
size="lg"
|
||||||
className="hover:bg-zinc-50 rounded-md transition-colors w-full py-1"
|
className="hover:bg-zinc-50 rounded-md transition-all duration-200 w-full py-1 group cursor-pointer active:bg-zinc-100"
|
||||||
>
|
>
|
||||||
<Avatar className="h-7 w-7 rounded-full mr-2">
|
<Avatar className="h-7 w-7 rounded-full mr-2">
|
||||||
<AvatarImage src={user.avatar} alt={user.name} />
|
<AvatarImage src={user.avatar} alt={user.name} />
|
||||||
|
@ -80,7 +80,9 @@ export function NavUser({
|
||||||
{user.email}
|
{user.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<IconDotsVertical className="ml-auto size-3.5 text-zinc-400" />
|
<div className="ml-auto p-0.5 rounded-full transition-all duration-200 group-hover:bg-zinc-100 group-active:bg-zinc-200">
|
||||||
|
<IconDotsVertical className="size-3.5 text-zinc-400 transition-all duration-200 group-hover:text-zinc-600 group-active:text-zinc-700" />
|
||||||
|
</div>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
|
|
|
@ -372,51 +372,27 @@ export const streamAgent = (agentRunId: string, callbacks: {
|
||||||
// Log raw data for debugging
|
// Log raw data for debugging
|
||||||
console.log(`[STREAM] Received data: ${rawData.substring(0, 100)}${rawData.length > 100 ? '...' : ''}`);
|
console.log(`[STREAM] Received data: ${rawData.substring(0, 100)}${rawData.length > 100 ? '...' : ''}`);
|
||||||
|
|
||||||
const data = JSON.parse(rawData);
|
// Pass the raw data directly to onMessage for handling in the component
|
||||||
|
callbacks.onMessage(rawData);
|
||||||
|
|
||||||
if (data.type === 'content' && data.content) {
|
// Try to parse for tool calls
|
||||||
if (data.content.startsWith('data: {')) {
|
try {
|
||||||
try {
|
const data = JSON.parse(rawData);
|
||||||
const innerData = JSON.parse(data.content.substring(6));
|
if (data.content?.startsWith('data: ')) {
|
||||||
if (innerData.type === 'content' && innerData.content) {
|
const innerJson = data.content.replace('data: ', '');
|
||||||
callbacks.onMessage(innerData.content);
|
const innerData = JSON.parse(innerJson);
|
||||||
} else if (innerData.type === 'tool_call') {
|
|
||||||
callbacks.onToolCall(innerData.name, innerData.arguments);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
callbacks.onMessage(data.content);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callbacks.onMessage(data.content);
|
|
||||||
}
|
|
||||||
} else if (data.type === 'tool_call') {
|
|
||||||
callbacks.onToolCall(data.name, data.arguments);
|
|
||||||
} else if (data.type === 'error') {
|
|
||||||
console.error(`[STREAM] Error from server: ${data.message}`);
|
|
||||||
callbacks.onError(data.message instanceof Error ? data.message : new Error(data.message));
|
|
||||||
} else if (data.type === 'status') {
|
|
||||||
console.log(`[STREAM] Status update: ${data.status}`);
|
|
||||||
|
|
||||||
if (data.status === 'completed') {
|
if (innerData.type === 'tool_call') {
|
||||||
console.log(`[STREAM] Agent run completed - closing stream for ${agentRunId}`);
|
callbacks.onToolCall(innerData.name || '', innerData.arguments || '');
|
||||||
|
|
||||||
// Close connection first before handling completion
|
|
||||||
if (eventSourceInstance) {
|
|
||||||
console.log(`[STREAM] Closing EventSource for ${agentRunId}`);
|
|
||||||
eventSourceInstance.close();
|
|
||||||
eventSourceInstance = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then notify completion (once)
|
|
||||||
if (!isClosing) {
|
|
||||||
console.log(`[STREAM] Calling onClose for ${agentRunId}`);
|
|
||||||
isClosing = true;
|
|
||||||
callbacks.onClose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
// Ignore parsing errors for tool calls
|
||||||
|
console.debug('[STREAM] Could not parse tool call data:', parseError);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[STREAM] Error parsing message:`, error);
|
console.error(`[STREAM] Error handling message:`, error);
|
||||||
callbacks.onError(error instanceof Error ? error : String(error));
|
callbacks.onError(error instanceof Error ? error : String(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue