Merge pull request #874 from escapade-mckv/fix-tool-views-sharepage

fix: fix tool call views in share page
This commit is contained in:
Bobbie 2025-07-03 09:43:45 +05:30 committed by GitHub
commit 20dbf95725
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 11 additions and 70 deletions

View File

@ -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">