This commit is contained in:
marko-kraemer 2025-04-20 01:56:59 +01:00
parent 6ac4213fdc
commit eaced25dda
4 changed files with 184 additions and 74 deletions

View File

@ -17,6 +17,7 @@ import { ToolCallSidePanel, ToolCallInput } from "@/components/thread/tool-call-
import { useSidebar } from "@/components/ui/sidebar"; import { useSidebar } from "@/components/ui/sidebar";
import { useAgentStream } from '@/hooks/useAgentStream'; import { useAgentStream } from '@/hooks/useAgentStream';
import { Markdown } from '@/components/home/ui/markdown'; import { Markdown } from '@/components/home/ui/markdown';
import { cn } from "@/lib/utils";
import { UnifiedMessage, ParsedContent, ParsedMetadata, ThreadParams } from '@/components/thread/types'; import { UnifiedMessage, ParsedContent, ParsedMetadata, ThreadParams } from '@/components/thread/types';
import { getToolIcon, extractPrimaryParam, safeJsonParse } from '@/components/thread/utils'; import { getToolIcon, extractPrimaryParam, safeJsonParse } from '@/components/thread/utils';
@ -185,7 +186,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
const [isSidePanelOpen, setIsSidePanelOpen] = useState(false); const [isSidePanelOpen, setIsSidePanelOpen] = useState(false);
const [toolCalls, setToolCalls] = useState<ToolCallInput[]>([]); const [toolCalls, setToolCalls] = useState<ToolCallInput[]>([]);
const [currentToolIndex, setCurrentToolIndex] = useState<number>(0); const [currentToolIndex, setCurrentToolIndex] = useState<number>(0);
const [autoOpenedPanel, setAutoOpenedPanel] = useState(false); const [autoOpenedPanel, setAutoOpenedPanel] = useState(true);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null); const messagesContainerRef = useRef<HTMLDivElement>(null);
@ -212,6 +213,11 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
const initialLayoutAppliedRef = useRef(false); const initialLayoutAppliedRef = useRef(false);
const userClosedPanelRef = useRef(false); const userClosedPanelRef = useRef(false);
// Initialize as if user already closed panel to prevent auto-opening
useEffect(() => {
userClosedPanelRef.current = true;
}, []);
const toggleSidePanel = useCallback(() => { const toggleSidePanel = useCallback(() => {
setIsSidePanelOpen(prevIsOpen => { setIsSidePanelOpen(prevIsOpen => {
@ -667,7 +673,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
// Automatically detect and populate tool calls from messages // Automatically detect and populate tool calls from messages
useEffect(() => { useEffect(() => {
// Calculate historical tool calls regardless of panel state // Calculate historical tool pairs regardless of panel state
const historicalToolPairs: ToolCallInput[] = []; const historicalToolPairs: ToolCallInput[] = [];
const assistantMessages = messages.filter(m => m.type === 'assistant' && m.message_id); const assistantMessages = messages.filter(m => m.type === 'assistant' && m.message_id);
@ -699,6 +705,11 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
} }
} catch {} } catch {}
// Skip adding <ask> tags to the tool calls
if (toolName === 'ask') {
return;
}
let isSuccess = true; let isSuccess = true;
try { try {
const toolContent = resultMessage.content?.toLowerCase() || ''; const toolContent = resultMessage.content?.toLowerCase() || '';
@ -785,8 +796,13 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
); );
}, []); }, []);
// Update handleToolClick to respect user closing preference // Update handleToolClick to respect user closing preference and navigate correctly
const handleToolClick = useCallback((clickedAssistantMessageId: string | null, clickedToolName: string) => { const handleToolClick = useCallback((clickedAssistantMessageId: string | null, clickedToolName: string) => {
// Explicitly ignore ask tags from opening the side panel
if (clickedToolName === 'ask') {
return;
}
if (!clickedAssistantMessageId) { if (!clickedAssistantMessageId) {
console.warn("Clicked assistant message ID is null. Cannot open side panel."); console.warn("Clicked assistant message ID is null. Cannot open side panel.");
toast.warning("Cannot view details: Assistant message ID is missing."); toast.warning("Cannot view details: Assistant message ID is missing.");
@ -796,22 +812,65 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
// Reset user closed state when explicitly clicking a tool // Reset user closed state when explicitly clicking a tool
userClosedPanelRef.current = false; userClosedPanelRef.current = false;
console.log("Tool Click Triggered. Assistant Message ID:", clickedAssistantMessageId, "Tool Name:", clickedToolName); console.log("[PAGE] Tool Click Triggered. Assistant Message ID:", clickedAssistantMessageId, "Tool Name:", clickedToolName);
// ... rest of existing code ... // Find the index of the tool call associated with the clicked assistant message
}, [messages]); const toolIndex = toolCalls.findIndex(tc => {
// Check if the assistant message ID matches the one stored in the tool result's metadata
if (!tc.toolResult?.content || tc.toolResult.content === "STREAMING") return false; // Skip streaming or incomplete calls
// Directly compare assistant message IDs if available in the structure
// Find the original assistant message based on the ID
const assistantMessage = messages.find(m => m.message_id === clickedAssistantMessageId && m.type === 'assistant');
if (!assistantMessage) return false;
// Find the corresponding tool message using metadata
const toolMessage = messages.find(m => {
if (m.type !== 'tool' || !m.metadata) return false;
try {
const metadata = safeJsonParse<ParsedMetadata>(m.metadata, {});
return metadata.assistant_message_id === assistantMessage.message_id;
} catch {
return false;
}
});
// Check if the current toolCall 'tc' corresponds to this assistant/tool message pair
return tc.assistantCall?.content === assistantMessage.content &&
tc.toolResult?.content === toolMessage?.content;
});
if (toolIndex !== -1) {
console.log(`[PAGE] Found tool call at index ${toolIndex} for assistant message ${clickedAssistantMessageId}`);
setCurrentToolIndex(toolIndex);
setIsSidePanelOpen(true); // Explicitly open the panel
} else {
console.warn(`[PAGE] Could not find matching tool call in toolCalls array for assistant message ID: ${clickedAssistantMessageId}`);
toast.info("Could not find details for this tool call.");
// Optionally, still open the panel but maybe at the last index or show a message?
// setIsSidePanelOpen(true);
}
}, [messages, toolCalls]); // Add toolCalls as a dependency
// Handle streaming tool calls // Handle streaming tool calls
const handleStreamingToolCall = useCallback((toolCall: StreamingToolCall | null) => { const handleStreamingToolCall = useCallback((toolCall: StreamingToolCall | null) => {
if (!toolCall) return; if (!toolCall) return;
console.log("[STREAM] Received tool call:", toolCall.name || toolCall.xml_tag_name);
const toolName = toolCall.name || toolCall.xml_tag_name || 'Unknown Tool';
// Skip <ask> tags from showing in the side panel during streaming
if (toolName === 'ask') {
return;
}
console.log("[STREAM] Received tool call:", toolName);
// If user explicitly closed the panel, don't reopen it for streaming calls // If user explicitly closed the panel, don't reopen it for streaming calls
if (userClosedPanelRef.current) return; if (userClosedPanelRef.current) return;
// Create a properly formatted tool call input for the streaming tool // Create a properly formatted tool call input for the streaming tool
// that matches the format of historical tool calls // that matches the format of historical tool calls
const toolName = toolCall.name || toolCall.xml_tag_name || 'Unknown Tool';
const toolArguments = toolCall.arguments || ''; const toolArguments = toolCall.arguments || '';
// Format the arguments in a way that matches the expected XML format for each tool // Format the arguments in a way that matches the expected XML format for each tool
@ -864,50 +923,110 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
if (isLoading && !initialLoadCompleted.current) { if (isLoading && !initialLoadCompleted.current) {
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
<div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[600px]' : ''}`}> <div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[650px]' : ''}`}>
<SiteHeader {/* Skeleton Header */}
threadId={threadId} <div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
projectName={projectName} <div className="flex h-14 items-center gap-4 px-4">
projectId={project?.id ?? null} <div className="flex-1">
onViewFiles={handleOpenFileViewer} <div className="flex items-center gap-2">
onToggleSidePanel={toggleSidePanel} <Skeleton className="h-6 w-6 rounded-full" />
/> <Skeleton className="h-5 w-40" />
</div>
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-8 w-8 rounded-full" />
</div>
</div>
</div>
{/* Skeleton Chat Messages */}
<div className="flex-1 overflow-y-auto px-6 py-4 pb-[5.5rem]"> <div className="flex-1 overflow-y-auto px-6 py-4 pb-[5.5rem]">
<div className="mx-auto max-w-3xl space-y-4"> <div className="mx-auto max-w-3xl space-y-6">
{/* User message */}
<div className="flex justify-end"> <div className="flex justify-end">
<div className="max-w-[85%] rounded-lg bg-primary/10 px-4 py-3"> <div className="max-w-[85%] rounded-lg bg-primary/10 px-4 py-3">
<Skeleton className="h-4 w-32" /> <div className="space-y-2">
<Skeleton className="h-4 w-48" />
<Skeleton className="h-4 w-32" />
</div>
</div> </div>
</div> </div>
<div className="flex justify-start">
<div className="max-w-[85%] rounded-lg bg-muted px-4 py-3"> {/* Assistant response with tool usage */}
<Skeleton className="h-4 w-48 mb-2" /> <div>
<Skeleton className="h-4 w-40" /> <div className="flex items-start gap-3">
<Skeleton className="flex-shrink-0 w-5 h-5 mt-2 rounded-full" />
<div className="flex-1 space-y-2">
<div className="max-w-[90%] w-full rounded-lg bg-muted px-4 py-3">
<div className="space-y-3">
<div>
<Skeleton className="h-4 w-full max-w-[360px] mb-2" />
<Skeleton className="h-4 w-full max-w-[320px] mb-2" />
<Skeleton className="h-4 w-full max-w-[290px]" />
</div>
{/* Tool call button skeleton */}
<div className="py-1">
<Skeleton className="h-6 w-32 rounded-md" />
</div>
<div>
<Skeleton className="h-4 w-full max-w-[340px] mb-2" />
<Skeleton className="h-4 w-full max-w-[280px]" />
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
{/* User message */}
<div className="flex justify-end"> <div className="flex justify-end">
<div className="max-w-[85%] rounded-lg bg-primary/10 px-4 py-3"> <div className="max-w-[85%] rounded-lg bg-primary/10 px-4 py-3">
<Skeleton className="h-4 w-40" /> <Skeleton className="h-4 w-36" />
</div> </div>
</div> </div>
<div className="flex justify-start">
<div className="max-w-[85%] rounded-lg bg-muted px-4 py-3"> {/* Assistant thinking state */}
<Skeleton className="h-4 w-56 mb-2" /> <div>
<Skeleton className="h-4 w-44" /> <div className="flex items-start gap-3">
<Skeleton className="flex-shrink-0 w-5 h-5 mt-2 rounded-full" />
<div className="flex-1 space-y-2">
<div className="flex items-center gap-1.5 py-1">
<div className="h-1.5 w-1.5 rounded-full bg-gray-400/50 animate-pulse" />
<div className="h-1.5 w-1.5 rounded-full bg-gray-400/50 animate-pulse delay-150" />
<div className="h-1.5 w-1.5 rounded-full bg-gray-400/50 animate-pulse delay-300" />
</div>
</div>
</div>
</div>
</div>
</div>
{/* Skeleton Chat Input */}
<div className="border-t p-4">
<div className="mx-auto max-w-3xl">
<div className="relative">
<Skeleton className="h-10 w-full rounded-md" />
<div className="absolute right-2 top-2">
<Skeleton className="h-6 w-6 rounded-full" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<ToolCallSidePanel
isOpen={isSidePanelOpen} {/* Skeleton Side Panel (closed state) */}
onClose={() => setIsSidePanelOpen(false)} <div className={`hidden ${isSidePanelOpen ? 'block' : ''}`}>
toolCalls={[]} <div className="h-screen w-[450px] border-l">
currentIndex={0} <div className="p-4">
onNavigate={handleSidePanelNavigate} <Skeleton className="h-8 w-32 mb-4" />
project={project} <Skeleton className="h-20 w-full rounded-md mb-4" />
agentStatus="idle" <Skeleton className="h-40 w-full rounded-md" />
/> </div>
</div>
</div>
</div> </div>
); );
} }
@ -915,7 +1034,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
if (error) { if (error) {
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
<div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[700px]' : ''}`}> <div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[650px]' : ''}`}>
<SiteHeader <SiteHeader
threadId={threadId} threadId={threadId}
projectName={projectName} projectName={projectName}
@ -948,7 +1067,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
<div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[700px]' : ''}`}> <div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[650px]' : ''}`}>
<SiteHeader <SiteHeader
threadId={threadId} threadId={threadId}
projectName={projectName} projectName={projectName}
@ -959,7 +1078,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
/> />
<div <div
ref={messagesContainerRef} ref={messagesContainerRef}
className="flex-1 overflow-y-auto px-6 py-4 pb-[0.5rem]" className="flex-1 overflow-y-auto px-6 py-4 pb-24 bg-transparent"
onScroll={handleScroll} onScroll={handleScroll}
> >
<div className="mx-auto max-w-3xl"> <div className="mx-auto max-w-3xl">
@ -1185,33 +1304,29 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
)} )}
<div ref={messagesEndRef} className="h-1" /> <div ref={messagesEndRef} className="h-1" />
</div> </div>
<div
className="sticky bottom-6 flex justify-center transition-opacity duration-300"
style={{ opacity: showScrollButton ? 1 : 0, visibility: showScrollButton ? 'visible' : 'hidden' }}
>
<Button variant="outline" size="icon" className="h-8 w-8 rounded-full" onClick={handleScrollButtonClick}>
<ArrowDown className="h-4 w-4" />
</Button>
</div>
</div> </div>
</div>
<div> {/* Fixed Chat Input Container */}
<div className=""> <div className={cn(
<ChatInput "fixed bottom-0 z-10 bg-gradient-to-t from-background via-background/90 to-transparent px-4 pb-4 pt-8 transition-all duration-200 ease-in-out",
value={newMessage} leftSidebarState === 'expanded' ? 'left-[72px] lg:left-[256px]' : 'left-[72px]',
onChange={setNewMessage} isSidePanelOpen ? 'right-[90%] sm:right-[450px] md:right-[500px] lg:right-[550px] xl:right-[650px]' : 'right-0'
onSubmit={handleSubmitMessage} )}>
placeholder="Ask Suna anything..." <div className="mx-auto max-w-3xl">
loading={isSending} <ChatInput
disabled={isSending || agentStatus === 'running' || agentStatus === 'connecting'} value={newMessage}
isAgentRunning={agentStatus === 'running' || agentStatus === 'connecting'} onChange={setNewMessage}
onStopAgent={handleStopAgent} onSubmit={handleSubmitMessage}
autoFocus={!isLoading} placeholder="Ask Suna anything..."
onFileBrowse={handleOpenFileViewer} loading={isSending}
sandboxId={sandboxId || undefined} disabled={isSending || agentStatus === 'running' || agentStatus === 'connecting'}
/> isAgentRunning={agentStatus === 'running' || agentStatus === 'connecting'}
</div> onStopAgent={handleStopAgent}
autoFocus={!isLoading}
onFileBrowse={handleOpenFileViewer}
sandboxId={sandboxId || undefined}
/>
</div> </div>
</div> </div>

View File

@ -12,17 +12,11 @@ import {
Download, Download,
ChevronRight, ChevronRight,
Home, Home,
ArrowLeft,
Save,
ChevronLeft, ChevronLeft,
PanelLeft,
PanelLeftClose,
Menu,
Loader, Loader,
AlertTriangle, AlertTriangle,
} from "lucide-react"; } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { FileRenderer, getFileTypeFromExtension } from "@/components/file-renderers"; import { FileRenderer, getFileTypeFromExtension } from "@/components/file-renderers";
import { listSandboxFiles, getSandboxFileContent, type FileInfo, Project } from "@/lib/api"; import { listSandboxFiles, getSandboxFileContent, type FileInfo, Project } from "@/lib/api";
import { toast } from "sonner"; import { toast } from "sonner";

View File

@ -7,6 +7,7 @@ import { getToolIcon } from "@/components/thread/utils";
import React from "react"; import React from "react";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/ui/slider";
import { ApiMessageType } from '@/components/thread/types'; import { ApiMessageType } from '@/components/thread/types';
import { cn } from "@/lib/utils";
// Import tool view components from the tool-views directory // Import tool view components from the tool-views directory
import { CommandToolView } from "./tool-views/CommandToolView"; import { CommandToolView } from "./tool-views/CommandToolView";
@ -237,7 +238,7 @@ export function ToolCallSidePanel({
}; };
return ( return (
<div className="fixed inset-y-0 right-0 w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[700px] bg-background border-l flex flex-col z-10"> <div className="fixed inset-y-0 right-0 w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[650px] bg-background border-l flex flex-col z-10">
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<h3 className="text-sm font-semibold"> <h3 className="text-sm font-semibold">
{isStreaming {isStreaming

View File

@ -126,12 +126,12 @@ export function BrowserToolView({
src={vncPreviewUrl} src={vncPreviewUrl}
title="Browser preview (Live)" title="Browser preview (Live)"
className="absolute top-0 left-0 w-full h-full border-0" className="absolute top-0 left-0 w-full h-full border-0"
style={{ maxHeight: '600px' }} style={{ maxHeight: '650px' }}
/> />
</div> </div>
) : screenshotBase64 ? ( ) : screenshotBase64 ? (
// Show screenshot if available (and agent is not running) // Show screenshot if available (and agent is not running)
<div className="bg-black w-full relative overflow-hidden" style={{ maxHeight: '600px' }}> <div className="bg-black w-full relative overflow-hidden" style={{ maxHeight: '650px' }}>
<img <img
src={`data:image/jpeg;base64,${screenshotBase64}`} src={`data:image/jpeg;base64,${screenshotBase64}`}
alt="Browser Screenshot (Final State)" alt="Browser Screenshot (Final State)"
@ -145,7 +145,7 @@ export function BrowserToolView({
src={vncPreviewUrl} src={vncPreviewUrl}
title="Browser preview (VNC Fallback)" title="Browser preview (VNC Fallback)"
className="absolute top-0 left-0 w-full h-full border-0" className="absolute top-0 left-0 w-full h-full border-0"
style={{ maxHeight: '600px' }} style={{ maxHeight: '650px' }}
/> />
</div> </div>
) : ( ) : (