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 (