mirror of https://github.com/kortix-ai/suna.git
fix: fix tool call views in share page
This commit is contained in:
parent
4ef84453d9
commit
0c66b05706
|
@ -31,8 +31,8 @@ import { useAgentStream } from '@/hooks/useAgentStream';
|
|||
import { threadErrorCodeMessages } from '@/lib/constants/errorCodeMessages';
|
||||
import { ThreadSkeleton } from '@/components/thread/content/ThreadSkeleton';
|
||||
import { useAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { extractToolName } from '@/components/thread/tool-views/xml-parser';
|
||||
|
||||
// Extend the base Message type with the expected database fields
|
||||
interface ApiMessageType extends BaseApiMessageType {
|
||||
message_id?: string;
|
||||
thread_id?: string;
|
||||
|
@ -48,7 +48,6 @@ interface ApiMessageType extends BaseApiMessageType {
|
|||
};
|
||||
}
|
||||
|
||||
// Add a simple interface for streaming tool calls
|
||||
interface StreamingToolCall {
|
||||
id?: string;
|
||||
name?: string;
|
||||
|
@ -78,7 +77,6 @@ export default function ThreadPage({
|
|||
const [currentToolIndex, setCurrentToolIndex] = useState<number>(0);
|
||||
const [autoOpenedPanel, setAutoOpenedPanel] = useState(false);
|
||||
|
||||
// Playback control states
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
|
||||
const [streamingText, setStreamingText] = useState('');
|
||||
|
@ -194,7 +192,7 @@ export default function ThreadPage({
|
|||
(toolCall: StreamingToolCall | null) => {
|
||||
if (!toolCall) return;
|
||||
|
||||
// Normalize the tool name by replacing underscores with hyphens
|
||||
// Normalize the tool name like the project thread page does
|
||||
const rawToolName = toolCall.name || toolCall.xml_tag_name || 'Unknown Tool';
|
||||
const toolName = rawToolName.replace(/_/g, '-').toLowerCase();
|
||||
|
||||
|
@ -210,6 +208,7 @@ export default function ThreadPage({
|
|||
// Format the arguments in a way that matches the expected XML format for each tool
|
||||
// This ensures the specialized tool views render correctly
|
||||
let formattedContent = toolArguments;
|
||||
|
||||
if (
|
||||
toolName.includes('command') &&
|
||||
!toolArguments.includes('<execute-command>')
|
||||
|
@ -432,17 +431,10 @@ export default function ThreadPage({
|
|||
return assistantMsg.content;
|
||||
}
|
||||
})();
|
||||
|
||||
// Try to extract tool name from content
|
||||
const xmlMatch = assistantContent.match(
|
||||
/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/,
|
||||
);
|
||||
if (xmlMatch) {
|
||||
// Normalize tool name: replace underscores with hyphens and lowercase
|
||||
const rawToolName = xmlMatch[1] || xmlMatch[2] || 'unknown';
|
||||
toolName = rawToolName.replace(/_/g, '-').toLowerCase();
|
||||
const extractedToolName = extractToolName(assistantContent);
|
||||
if (extractedToolName) {
|
||||
toolName = extractedToolName;
|
||||
} else {
|
||||
// Fallback to checking for tool_calls JSON structure
|
||||
const assistantContentParsed = safeJsonParse<{
|
||||
tool_calls?: Array<{ function?: { name?: string }; name?: string }>;
|
||||
}>(assistantMsg.content, {});
|
||||
|
@ -452,7 +444,6 @@ export default function ThreadPage({
|
|||
) {
|
||||
const firstToolCall = assistantContentParsed.tool_calls[0];
|
||||
const rawName = firstToolCall.function?.name || firstToolCall.name || 'unknown';
|
||||
// Normalize tool name here too
|
||||
toolName = rawName.replace(/_/g, '-').toLowerCase();
|
||||
}
|
||||
}
|
||||
|
@ -460,7 +451,6 @@ export default function ThreadPage({
|
|||
|
||||
let isSuccess = true;
|
||||
try {
|
||||
// Parse tool result content
|
||||
const toolResultContent = (() => {
|
||||
try {
|
||||
const parsed = safeJsonParse<{ content?: string }>(resultMessage.content, {});
|
||||
|
@ -469,15 +459,11 @@ export default function ThreadPage({
|
|||
return resultMessage.content;
|
||||
}
|
||||
})();
|
||||
|
||||
// Check for ToolResult pattern first
|
||||
if (toolResultContent && typeof toolResultContent === 'string') {
|
||||
// Look for ToolResult(success=True/False) pattern
|
||||
const toolResultMatch = toolResultContent.match(/ToolResult\s*\(\s*success\s*=\s*(True|False|true|false)/i);
|
||||
if (toolResultMatch) {
|
||||
isSuccess = toolResultMatch[1].toLowerCase() === 'true';
|
||||
} else {
|
||||
// Fallback: only check for error keywords if no ToolResult pattern found
|
||||
const toolContent = toolResultContent.toLowerCase();
|
||||
isSuccess = !(toolContent.includes('failed') ||
|
||||
toolContent.includes('error') ||
|
||||
|
@ -489,19 +475,17 @@ export default function ThreadPage({
|
|||
historicalToolPairs.push({
|
||||
assistantCall: {
|
||||
name: toolName,
|
||||
content: assistantMsg.content, // Store original content
|
||||
content: assistantMsg.content,
|
||||
timestamp: assistantMsg.created_at,
|
||||
},
|
||||
toolResult: {
|
||||
content: resultMessage.content, // Store original content
|
||||
content: resultMessage.content,
|
||||
isSuccess: isSuccess,
|
||||
timestamp: resultMessage.created_at,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sort the tool calls chronologically by timestamp
|
||||
historicalToolPairs.sort((a, b) => {
|
||||
const timeA = new Date(a.assistantCall.timestamp || '').getTime();
|
||||
const timeB = new Date(b.assistantCall.timestamp || '').getTime();
|
||||
|
@ -509,8 +493,6 @@ export default function ThreadPage({
|
|||
});
|
||||
|
||||
setToolCalls(historicalToolPairs);
|
||||
|
||||
// When loading is complete, prepare for playback
|
||||
initialLoadCompleted.current = true;
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -525,9 +507,7 @@ export default function ThreadPage({
|
|||
if (isMounted) setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadData();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
|
@ -546,7 +526,6 @@ export default function ThreadPage({
|
|||
messagesEndRef.current?.scrollIntoView({ behavior });
|
||||
};
|
||||
|
||||
// Handle tool clicks
|
||||
const handleToolClick = useCallback(
|
||||
(clickedAssistantMessageId: string | null, clickedToolName: string) => {
|
||||
if (!clickedAssistantMessageId) {
|
||||
|
@ -557,7 +536,6 @@ export default function ThreadPage({
|
|||
return;
|
||||
}
|
||||
|
||||
// Reset user closed state when explicitly clicking a tool
|
||||
userClosedPanelRef.current = false;
|
||||
|
||||
console.log(
|
||||
|
@ -567,21 +545,16 @@ export default function ThreadPage({
|
|||
clickedToolName,
|
||||
);
|
||||
|
||||
// Find the index of the tool call associated with the clicked assistant message
|
||||
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
|
||||
return false;
|
||||
|
||||
// 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 {
|
||||
|
@ -593,9 +566,6 @@ export default function ThreadPage({
|
|||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if the current toolCall 'tc' corresponds to this assistant/tool message pair
|
||||
// Compare the original content directly without parsing
|
||||
return (
|
||||
tc.assistantCall?.content === assistantMessage.content &&
|
||||
tc.toolResult?.content === toolMessage?.content
|
||||
|
@ -608,7 +578,7 @@ export default function ThreadPage({
|
|||
);
|
||||
setExternalNavIndex(toolIndex);
|
||||
setCurrentToolIndex(toolIndex);
|
||||
setIsSidePanelOpen(true); // Explicitly open the panel
|
||||
setIsSidePanelOpen(true);
|
||||
|
||||
setTimeout(() => setExternalNavIndex(undefined), 100);
|
||||
} else {
|
||||
|
@ -630,7 +600,6 @@ export default function ThreadPage({
|
|||
setFileViewerOpen(true);
|
||||
}, []);
|
||||
|
||||
// Initialize PlaybackControls
|
||||
const playbackController: PlaybackController = PlaybackControls({
|
||||
messages,
|
||||
isSidePanelOpen,
|
||||
|
@ -641,7 +610,6 @@ export default function ThreadPage({
|
|||
projectName: projectName || 'Shared Conversation',
|
||||
});
|
||||
|
||||
// Extract the playback state and functions
|
||||
const {
|
||||
playbackState,
|
||||
renderHeader,
|
||||
|
@ -652,21 +620,17 @@ export default function ThreadPage({
|
|||
skipToEnd,
|
||||
} = playbackController;
|
||||
|
||||
// Connect playbackState to component state
|
||||
useEffect(() => {
|
||||
// Keep the isPlaying state in sync with playbackState
|
||||
setIsPlaying(playbackState.isPlaying);
|
||||
setCurrentMessageIndex(playbackState.currentMessageIndex);
|
||||
}, [playbackState.isPlaying, playbackState.currentMessageIndex]);
|
||||
|
||||
// Auto-scroll when new messages appear during playback
|
||||
useEffect(() => {
|
||||
if (playbackState.visibleMessages.length > 0 && !userHasScrolled) {
|
||||
scrollToBottom('smooth');
|
||||
}
|
||||
}, [playbackState.visibleMessages, userHasScrolled]);
|
||||
|
||||
// Scroll button visibility
|
||||
useEffect(() => {
|
||||
if (!latestMessageRef.current || playbackState.visibleMessages.length === 0)
|
||||
return;
|
||||
|
@ -683,7 +647,6 @@ export default function ThreadPage({
|
|||
`[PAGE] 🔄 Page AgentStatus: ${agentStatus}, Hook Status: ${streamHookStatus}, Target RunID: ${agentRunId || 'none'}, Hook RunID: ${currentHookRunId || 'none'}`,
|
||||
);
|
||||
|
||||
// If the stream hook reports completion/stopping but our UI hasn't updated
|
||||
if (
|
||||
(streamHookStatus === 'completed' ||
|
||||
streamHookStatus === 'stopped' ||
|
||||
|
@ -700,7 +663,6 @@ export default function ThreadPage({
|
|||
}
|
||||
}, [agentStatus, streamHookStatus, agentRunId, currentHookRunId]);
|
||||
|
||||
// Auto-scroll function for use throughout the component
|
||||
const autoScrollToBottom = useCallback(
|
||||
(behavior: ScrollBehavior = 'smooth') => {
|
||||
if (!userHasScrolled && messagesEndRef.current) {
|
||||
|
@ -710,31 +672,21 @@ export default function ThreadPage({
|
|||
[userHasScrolled],
|
||||
);
|
||||
|
||||
// Very direct approach to update the tool index during message playback
|
||||
useEffect(() => {
|
||||
if (!isPlaying || currentMessageIndex <= 0 || !messages.length) return;
|
||||
|
||||
// Check if current message is a tool message
|
||||
const currentMsg = messages[currentMessageIndex - 1]; // Look at previous message that just played
|
||||
|
||||
const currentMsg = messages[currentMessageIndex - 1];
|
||||
if (currentMsg?.type === 'tool' && currentMsg.metadata) {
|
||||
try {
|
||||
const metadata = safeJsonParse<ParsedMetadata>(currentMsg.metadata, {});
|
||||
const assistantId = metadata.assistant_message_id;
|
||||
|
||||
if (assistantId) {
|
||||
// Find the tool call that matches this assistant message
|
||||
const toolIndex = toolCalls.findIndex((tc) => {
|
||||
// Find the assistant message
|
||||
const assistantMessage = messages.find(
|
||||
(m) => m.message_id === assistantId && m.type === 'assistant'
|
||||
);
|
||||
if (!assistantMessage) return false;
|
||||
|
||||
// Check if this tool call matches
|
||||
return tc.assistantCall?.content === assistantMessage.content;
|
||||
});
|
||||
|
||||
if (toolIndex !== -1) {
|
||||
console.log(
|
||||
`Direct mapping: Setting tool index to ${toolIndex} for message ${assistantId}`,
|
||||
|
@ -748,28 +700,19 @@ export default function ThreadPage({
|
|||
}
|
||||
}, [currentMessageIndex, isPlaying, messages, toolCalls]);
|
||||
|
||||
// Force an explicit update to the tool panel based on the current message index
|
||||
useEffect(() => {
|
||||
// Skip if not playing or no messages
|
||||
if (!isPlaying || messages.length === 0 || currentMessageIndex <= 0) return;
|
||||
|
||||
// Get all messages up to the current index
|
||||
const currentMessages = messages.slice(0, currentMessageIndex);
|
||||
|
||||
// Find the most recent tool message to determine which panel to show
|
||||
for (let i = currentMessages.length - 1; i >= 0; i--) {
|
||||
const msg = currentMessages[i];
|
||||
if (msg.type === 'tool' && msg.metadata) {
|
||||
try {
|
||||
const metadata = safeJsonParse<ParsedMetadata>(msg.metadata, {});
|
||||
const assistantId = metadata.assistant_message_id;
|
||||
|
||||
if (assistantId) {
|
||||
console.log(
|
||||
`Looking for tool panel for assistant message ${assistantId}`,
|
||||
);
|
||||
|
||||
// Scan for matching tool call
|
||||
for (let j = 0; j < toolCalls.length; j++) {
|
||||
const content = toolCalls[j].assistantCall?.content || '';
|
||||
if (content.includes(assistantId)) {
|
||||
|
@ -788,14 +731,12 @@ export default function ThreadPage({
|
|||
}
|
||||
}, [currentMessageIndex, isPlaying, messages, toolCalls]);
|
||||
|
||||
// Loading skeleton UI
|
||||
if (isLoading && !initialLoadCompleted.current) {
|
||||
return (
|
||||
<ThreadSkeleton isSidePanelOpen={isSidePanelOpen} showHeader={true} />
|
||||
);
|
||||
}
|
||||
|
||||
// Error state UI
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
|
|
Loading…
Reference in New Issue