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