fix(dashboard): robust tool call display and filter logic
Two fixes for tool call display in the dashboard: 1. **filterDisplayMessages includes tool_calls** (MessageBubble.js) Previously filtered out messages with only tool_calls (no content/thinking). Now correctly keeps messages that have tool_calls. 2. **Type-safe getToolSummary** (markdown.js) The heuristic tool summary extractor was calling .slice() without type checks. If a tool input had a non-string value (e.g., number), it would throw TypeError. Now uses a helper function to safely check types before calling string methods. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,11 +44,11 @@ export function MessageBubble({ msg, userBg, compact = false, formatTime }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter messages for display — removes tool-call-only messages
|
* Filter messages for display — removes empty assistant messages
|
||||||
* that have no text or thinking (would render as empty bubbles).
|
* (no content, thinking, or tool_calls) that would render as empty bubbles.
|
||||||
*/
|
*/
|
||||||
export function filterDisplayMessages(messages) {
|
export function filterDisplayMessages(messages) {
|
||||||
return messages.filter(msg =>
|
return messages.filter(msg =>
|
||||||
msg.content || msg.thinking || msg.role === 'user'
|
msg.content || msg.thinking || msg.tool_calls?.length || msg.role === 'user'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,19 +61,65 @@ export function renderContent(content) {
|
|||||||
return html`<div class="md-content" dangerouslySetInnerHTML=${{ __html: safeHtml }} />`;
|
return html`<div class="md-content" dangerouslySetInnerHTML=${{ __html: safeHtml }} />`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
function getToolSummary(name, input) {
|
||||||
if (!input) return name;
|
if (!input || typeof input !== 'object') return name;
|
||||||
switch (name) {
|
|
||||||
case 'Bash': return input.description || input.command?.slice(0, 60) || 'Bash';
|
// Helper to safely get string value and slice it
|
||||||
case 'Read': return input.file_path?.split('/').slice(-2).join('/') || 'Read';
|
const str = (val, len) => typeof val === 'string' ? val.slice(0, len) : null;
|
||||||
case 'Write': return input.file_path?.split('/').slice(-2).join('/') || 'Write';
|
|
||||||
case 'Edit': return input.file_path?.split('/').slice(-2).join('/') || 'Edit';
|
// Try to extract a meaningful summary from common input patterns
|
||||||
case 'Grep': return `/${input.pattern?.slice(0, 40) || ''}/ ${input.glob || ''}`.trim();
|
// Priority order matters - more specific/useful fields first
|
||||||
case 'Glob': return input.pattern?.slice(0, 50) || 'Glob';
|
|
||||||
case 'Task': return input.description || 'Task';
|
// 1. Explicit description or summary
|
||||||
default: return name;
|
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)
|
// Render tool call pills (summary mode)
|
||||||
|
|||||||
Reference in New Issue
Block a user