diff --git a/dashboard/components/MessageBubble.js b/dashboard/components/MessageBubble.js index ef3e6d6..88956d5 100644 --- a/dashboard/components/MessageBubble.js +++ b/dashboard/components/MessageBubble.js @@ -44,11 +44,11 @@ export function MessageBubble({ msg, userBg, compact = false, formatTime }) { } /** - * Filter messages for display — removes tool-call-only messages - * that have no text or thinking (would render as empty bubbles). + * Filter messages for display — removes empty assistant messages + * (no content, thinking, or tool_calls) that would render as empty bubbles. */ export function filterDisplayMessages(messages) { return messages.filter(msg => - msg.content || msg.thinking || msg.role === 'user' + msg.content || msg.thinking || msg.tool_calls?.length || msg.role === 'user' ); } diff --git a/dashboard/lib/markdown.js b/dashboard/lib/markdown.js index 8136ad5..25ef49b 100644 --- a/dashboard/lib/markdown.js +++ b/dashboard/lib/markdown.js @@ -61,19 +61,65 @@ export function renderContent(content) { return html`
`; } -// Generate a short summary for a tool call based on name + input +// Generate a short summary for a tool call based on name + input. +// Uses heuristics to extract meaningful info from common input patterns +// rather than hardcoding specific tool names. function getToolSummary(name, input) { - if (!input) return name; - switch (name) { - case 'Bash': return input.description || input.command?.slice(0, 60) || 'Bash'; - case 'Read': return input.file_path?.split('/').slice(-2).join('/') || 'Read'; - case 'Write': return input.file_path?.split('/').slice(-2).join('/') || 'Write'; - case 'Edit': return input.file_path?.split('/').slice(-2).join('/') || 'Edit'; - case 'Grep': return `/${input.pattern?.slice(0, 40) || ''}/ ${input.glob || ''}`.trim(); - case 'Glob': return input.pattern?.slice(0, 50) || 'Glob'; - case 'Task': return input.description || 'Task'; - default: return name; + if (!input || typeof input !== 'object') return name; + + // Helper to safely get string value and slice it + const str = (val, len) => typeof val === 'string' ? val.slice(0, len) : null; + + // Try to extract a meaningful summary from common input patterns + // Priority order matters - more specific/useful fields first + + // 1. Explicit description or summary + let s = str(input.description, 60) || str(input.summary, 60); + if (s) return s; + + // 2. Command/shell execution + s = str(input.command, 60) || str(input.cmd, 60); + if (s) return s; + + // 3. File paths - show last 2 segments for context + const pathKeys = ['file_path', 'path', 'file', 'filename', 'filepath']; + for (const key of pathKeys) { + if (typeof input[key] === 'string' && input[key]) { + return input[key].split('/').slice(-2).join('/'); + } } + + // 4. Search patterns + if (typeof input.pattern === 'string' && input.pattern) { + const glob = typeof input.glob === 'string' ? ` ${input.glob}` : ''; + return `/${input.pattern.slice(0, 40)}/${glob}`.trim(); + } + s = str(input.query, 50) || str(input.search, 50); + if (s) return s; + if (typeof input.regex === 'string' && input.regex) return `/${input.regex.slice(0, 40)}/`; + + // 5. URL/endpoint + s = str(input.url, 60) || str(input.endpoint, 60); + if (s) return s; + + // 6. Name/title fields + if (typeof input.name === 'string' && input.name && input.name !== name) return input.name.slice(0, 50); + s = str(input.title, 50); + if (s) return s; + + // 7. Message/content (for chat/notification tools) + s = str(input.message, 50) || str(input.content, 50); + if (s) return s; + + // 8. First string value as fallback (skip very long values) + for (const [key, value] of Object.entries(input)) { + if (typeof value === 'string' && value.length > 0 && value.length < 100) { + return value.slice(0, 50); + } + } + + // No useful summary found + return name; } // Render tool call pills (summary mode)