diff --git a/src/renderer/src/components/ChatView.tsx b/src/renderer/src/components/ChatView.tsx index bbe8a65de7..dea247c9e8 100644 --- a/src/renderer/src/components/ChatView.tsx +++ b/src/renderer/src/components/ChatView.tsx @@ -1,7 +1,7 @@ /** * Chat view component - displays messages and tool calls */ -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' import type { StoredSessionUpdate } from '../../../shared/types' interface ChatViewProps { @@ -254,34 +254,155 @@ interface ToolCallCardProps { } function ToolCallCard({ toolCall }: ToolCallCardProps) { - const statusIcon = - toolCall.status === 'completed' ? ( - - ) : toolCall.status === 'running' ? ( - - ) : ( - - ) + const [expanded, setExpanded] = useState(false) + + // Get icon based on tool kind + const icon = getToolIcon(toolCall.kind, toolCall.title) + + // Get clean title + const cleanTitle = getCleanTitle(toolCall.title, toolCall.kind) + + // Status styling + const statusConfig = { + completed: { icon: '✓', color: 'text-green-500', bg: 'bg-green-500/10' }, + failed: { icon: '✗', color: 'text-red-500', bg: 'bg-red-500/10' }, + running: { icon: null, color: 'text-blue-500', bg: 'bg-blue-500/10' }, + in_progress: { icon: null, color: 'text-blue-500', bg: 'bg-blue-500/10' }, + pending: { icon: '○', color: 'text-yellow-500', bg: 'bg-yellow-500/10' }, + } + const status = statusConfig[toolCall.status as keyof typeof statusConfig] || statusConfig.pending + + const hasDetails = toolCall.input || toolCall.output return ( -
-
- {statusIcon} - {toolCall.title} - {toolCall.kind && ( - ({toolCall.kind}) +
+ {/* Header - always visible */} +
hasDetails && setExpanded(!expanded)} + > + {/* Tool icon */} + {icon} + + {/* Title */} + {cleanTitle} + + {/* Status */} + + {status.icon ? ( + {status.icon} + ) : ( + + )} + {toolCall.status} + + + {/* Expand icon */} + {hasDetails && ( + + ▼ + )}
- {toolCall.input && ( -
-          {toolCall.input.slice(0, 200)}
-          {toolCall.input.length > 200 && '...'}
-        
+ + {/* Details - collapsible */} + {expanded && hasDetails && ( +
+ {toolCall.input && ( +
+
Input
+
+                {toolCall.input}
+              
+
+ )} + {toolCall.output && ( +
+
Output
+
+                {toolCall.output.slice(0, 500)}
+                {toolCall.output.length > 500 && '...'}
+              
+
+ )} +
)}
) } +function getToolIcon(kind?: string, title?: string): string { + const lowerKind = kind?.toLowerCase() || '' + const lowerTitle = title?.toLowerCase() || '' + + // Match by kind first + if (lowerKind.includes('search') || lowerKind.includes('grep') || lowerKind.includes('glob')) { + return '🔍' + } + if (lowerKind.includes('file') || lowerKind.includes('read') || lowerKind.includes('write')) { + return '📄' + } + if (lowerKind.includes('edit')) { + return '✏️' + } + if (lowerKind.includes('bash') || lowerKind.includes('shell') || lowerKind.includes('command')) { + return '⌨️' + } + + // Match by title + if (lowerTitle.includes('list') || lowerTitle.includes('ls')) { + return '📁' + } + if (lowerTitle.includes('read')) { + return '📖' + } + if (lowerTitle.includes('write') || lowerTitle.includes('create')) { + return '📝' + } + if (lowerTitle.includes('edit') || lowerTitle.includes('replace')) { + return '✏️' + } + if (lowerTitle.includes('run') || lowerTitle.includes('exec') || lowerTitle.includes('bash')) { + return '⌨️' + } + if (lowerTitle.includes('search') || lowerTitle.includes('find') || lowerTitle.includes('grep')) { + return '🔍' + } + if (lowerTitle.includes('glob')) { + return '🔍' + } + + return '🔧' +} + +function getCleanTitle(title?: string, kind?: string): string { + if (!title) return kind || 'Tool Call' + + // If title is too long or looks like a command, try to extract meaningful part + const cleanTitle = title.trim() + + // If it starts with "Run " and has a long command, shorten it + if (cleanTitle.startsWith('Run ') && cleanTitle.length > 50) { + const cmd = cleanTitle.slice(4).split(' ')[0] + return `Run ${cmd}` + } + + // If it's a file path, show just the filename + if (cleanTitle.includes('/') && !cleanTitle.includes(' ')) { + const parts = cleanTitle.split('/') + return parts[parts.length - 1] || cleanTitle + } + + // Truncate if too long + if (cleanTitle.length > 60) { + return cleanTitle.slice(0, 57) + '...' + } + + return cleanTitle +} + function LoadingDots() { return (