hasDetails && setExpanded(!expanded)}
- >
- {/* Tool icon */}
-
{icon}
+
+ {/* Thought */}
+ {message.thought && (
+
+ )}
- {/* Title */}
- {cleanTitle}
+ {/* Tool calls */}
+ {message.toolCalls.map((tc) => (
+
+ ))}
- {/* Status */}
-
- {status.icon ? (
- {status.icon}
- ) : (
-
- )}
- {toolCall.status}
-
-
- {/* Expand icon */}
- {hasDetails && (
-
- ▼
-
- )}
-
-
- {/* Details - collapsible */}
- {expanded && hasDetails && (
-
- {toolCall.input && (
-
-
Input
-
- {toolCall.input}
-
-
- )}
- {toolCall.output && (
-
-
Output
-
- {toolCall.output.slice(0, 500)}
- {toolCall.output.length > 500 && '...'}
-
-
- )}
+ {/* Text content */}
+ {message.content && (
+
+ {message.content}
)}
)
}
-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 '🔧'
+// Thought line - inline display
+function ThoughtLine({ text }: { text: string }) {
+ const truncated = text.length > 80 ? text.slice(0, 77) + '...' : text
+ return (
+
+ ⊛
+ Thinking
+
+ {truncated}
+
+
+ )
}
-function getCleanTitle(title?: string, kind?: string): string {
- if (!title) return kind || 'Tool Call'
+// Tool call line - inline display
+function ToolCallLine({ toolCall }: { toolCall: ToolCall }) {
+ const { icon, action, detail } = parseToolCall(toolCall)
- // If title is too long or looks like a command, try to extract meaningful part
- const cleanTitle = title.trim()
+ const isRunning = toolCall.status === 'running' || toolCall.status === 'in_progress' || toolCall.status === 'pending'
+ const isFailed = toolCall.status === 'failed'
- // 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}`
+ return (
+
+ {/* Icon */}
+ {icon}
+
+ {/* Action */}
+ {action}
+
+ {/* Detail in code pill */}
+ {detail && (
+
+ {detail}
+
+ )}
+
+ {/* Running indicator */}
+ {isRunning && }
+
+ )
+}
+
+interface ParsedToolCall {
+ icon: string
+ action: string
+ detail?: string
+}
+
+function parseToolCall(toolCall: ToolCall): ParsedToolCall {
+ const title = toolCall.title?.toLowerCase() || ''
+ const kind = toolCall.kind?.toLowerCase() || ''
+
+ // Try to extract file path from input
+ let filePath: string | undefined
+ if (toolCall.input) {
+ try {
+ const parsed = JSON.parse(toolCall.input)
+ filePath = parsed.file_path || parsed.path || parsed.filePath || parsed.pattern
+ } catch {
+ // Not JSON, might be a direct path
+ if (toolCall.input.startsWith('/') || toolCall.input.includes('.')) {
+ filePath = toolCall.input.split('\n')[0].trim()
+ }
+ }
}
- // 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
+ // Determine icon and action based on kind/title
+ if (kind === 'search' || title.includes('glob') || title.includes('search') || title.includes('grep')) {
+ return {
+ icon: '◎',
+ action: title.includes('glob') ? 'Search files' : 'Search',
+ detail: filePath || extractPathFromTitle(toolCall.title),
+ }
}
- // Truncate if too long
- if (cleanTitle.length > 60) {
- return cleanTitle.slice(0, 57) + '...'
+ if (title.includes('list') || title.startsWith('ls')) {
+ return {
+ icon: '▤',
+ action: 'List',
+ detail: extractPathFromTitle(toolCall.title),
+ }
}
- return cleanTitle
+ if (title.includes('read')) {
+ return {
+ icon: '◔',
+ action: 'Read',
+ detail: filePath || extractPathFromTitle(toolCall.title),
+ }
+ }
+
+ if (title.includes('write') || title.includes('create')) {
+ // Try to get line count from output
+ let lineCount = ''
+ if (toolCall.output) {
+ const lines = toolCall.output.split('\n').length
+ if (lines > 1) lineCount = `${lines} lines`
+ }
+ return {
+ icon: '▤',
+ action: lineCount ? `Write ${lineCount}` : 'Write',
+ detail: filePath || extractPathFromTitle(toolCall.title),
+ }
+ }
+
+ if (title.includes('edit') || title.includes('replace')) {
+ return {
+ icon: '✎',
+ action: 'Edit',
+ detail: filePath || extractPathFromTitle(toolCall.title),
+ }
+ }
+
+ if (title.startsWith('run') || kind === 'bash' || kind === 'shell') {
+ const cmd = extractCommandFromTitle(toolCall.title)
+ return {
+ icon: '>_',
+ action: getCommandDescription(toolCall.title),
+ detail: cmd,
+ }
+ }
+
+ // Default
+ return {
+ icon: '◇',
+ action: toolCall.title || toolCall.kind || 'Tool',
+ detail: filePath,
+ }
+}
+
+function extractPathFromTitle(title?: string): string | undefined {
+ if (!title) return undefined
+ // Look for path-like strings
+ const match = title.match(/\/[\w\-./]+/)
+ return match ? match[0] : undefined
+}
+
+function extractCommandFromTitle(title?: string): string | undefined {
+ if (!title) return undefined
+ // Remove "Run " prefix and get the command
+ if (title.toLowerCase().startsWith('run ')) {
+ const cmd = title.slice(4).trim()
+ // Truncate if too long
+ return cmd.length > 50 ? cmd.slice(0, 47) + '...' : cmd
+ }
+ return undefined
+}
+
+function getCommandDescription(title?: string): string {
+ if (!title) return 'Run command'
+ const lower = title.toLowerCase()
+
+ if (lower.includes('mkdir')) return 'Create directory'
+ if (lower.includes('rm ')) return 'Remove'
+ if (lower.includes('mv ')) return 'Move'
+ if (lower.includes('cp ')) return 'Copy'
+ if (lower.includes('npm') || lower.includes('pnpm') || lower.includes('yarn')) return 'Package manager'
+ if (lower.includes('git')) return 'Git'
+ if (lower.includes('python')) return 'Run Python'
+ if (lower.includes('node')) return 'Run Node'
+ if (lower.includes('perl')) return 'Run Perl'
+
+ return 'Run command'
}
function LoadingDots() {