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>
160 lines
6.3 KiB
JavaScript
160 lines
6.3 KiB
JavaScript
// Markdown rendering with syntax highlighting
|
|
import { h } from 'https://esm.sh/preact@10.19.3';
|
|
import { marked } from 'https://esm.sh/marked@15.0.7';
|
|
import DOMPurify from 'https://esm.sh/dompurify@3.2.4';
|
|
import hljs from 'https://esm.sh/highlight.js@11.11.1/lib/core';
|
|
import langJavascript from 'https://esm.sh/highlight.js@11.11.1/lib/languages/javascript';
|
|
import langTypescript from 'https://esm.sh/highlight.js@11.11.1/lib/languages/typescript';
|
|
import langBash from 'https://esm.sh/highlight.js@11.11.1/lib/languages/bash';
|
|
import langJson from 'https://esm.sh/highlight.js@11.11.1/lib/languages/json';
|
|
import langPython from 'https://esm.sh/highlight.js@11.11.1/lib/languages/python';
|
|
import langRust from 'https://esm.sh/highlight.js@11.11.1/lib/languages/rust';
|
|
import langCss from 'https://esm.sh/highlight.js@11.11.1/lib/languages/css';
|
|
import langXml from 'https://esm.sh/highlight.js@11.11.1/lib/languages/xml';
|
|
import langSql from 'https://esm.sh/highlight.js@11.11.1/lib/languages/sql';
|
|
import langYaml from 'https://esm.sh/highlight.js@11.11.1/lib/languages/yaml';
|
|
import htm from 'https://esm.sh/htm@3.1.1';
|
|
|
|
const html = htm.bind(h);
|
|
|
|
// Register highlight.js languages
|
|
hljs.registerLanguage('javascript', langJavascript);
|
|
hljs.registerLanguage('js', langJavascript);
|
|
hljs.registerLanguage('typescript', langTypescript);
|
|
hljs.registerLanguage('ts', langTypescript);
|
|
hljs.registerLanguage('bash', langBash);
|
|
hljs.registerLanguage('sh', langBash);
|
|
hljs.registerLanguage('shell', langBash);
|
|
hljs.registerLanguage('json', langJson);
|
|
hljs.registerLanguage('python', langPython);
|
|
hljs.registerLanguage('py', langPython);
|
|
hljs.registerLanguage('rust', langRust);
|
|
hljs.registerLanguage('css', langCss);
|
|
hljs.registerLanguage('html', langXml);
|
|
hljs.registerLanguage('xml', langXml);
|
|
hljs.registerLanguage('sql', langSql);
|
|
hljs.registerLanguage('yaml', langYaml);
|
|
hljs.registerLanguage('yml', langYaml);
|
|
|
|
// Configure marked with highlight.js using custom renderer (v15 API)
|
|
const renderer = {
|
|
code(token) {
|
|
const code = token.text;
|
|
const lang = token.lang || '';
|
|
let highlighted;
|
|
if (lang && hljs.getLanguage(lang)) {
|
|
highlighted = hljs.highlight(code, { language: lang }).value;
|
|
} else {
|
|
highlighted = hljs.highlightAuto(code).value;
|
|
}
|
|
return `<pre><code class="hljs language-${lang}">${highlighted}</code></pre>`;
|
|
}
|
|
};
|
|
marked.use({ renderer, breaks: false, gfm: true });
|
|
|
|
// Render markdown content with syntax highlighting
|
|
// All HTML is sanitized with DOMPurify before rendering to prevent XSS
|
|
export function renderContent(content) {
|
|
if (!content) return '';
|
|
const rawHtml = marked.parse(content);
|
|
const safeHtml = DOMPurify.sanitize(rawHtml);
|
|
return html`<div class="md-content" dangerouslySetInnerHTML=${{ __html: safeHtml }} />`;
|
|
}
|
|
|
|
// 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 || 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)
|
|
export function renderToolCalls(toolCalls) {
|
|
if (!toolCalls || toolCalls.length === 0) return '';
|
|
return html`
|
|
<div class="flex flex-wrap gap-1.5 mt-1.5">
|
|
${toolCalls.map(tc => {
|
|
const summary = getToolSummary(tc.name, tc.input);
|
|
return html`
|
|
<span class="inline-flex items-center gap-1 rounded-md border border-starting/30 bg-starting/10 px-2 py-0.5 font-mono text-label text-starting">
|
|
<span class="font-medium">${tc.name}</span>
|
|
${summary !== tc.name && html`<span class="text-starting/65 truncate max-w-[200px]">${summary}</span>`}
|
|
</span>
|
|
`;
|
|
})}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Render thinking block (full content, open by default)
|
|
// Content is sanitized with DOMPurify before rendering
|
|
export function renderThinking(thinking) {
|
|
if (!thinking) return '';
|
|
const rawHtml = marked.parse(thinking);
|
|
const safeHtml = DOMPurify.sanitize(rawHtml);
|
|
return html`
|
|
<details class="mt-2 rounded-lg border border-violet-400/25 bg-violet-500/8" open>
|
|
<summary class="cursor-pointer select-none px-3 py-1.5 font-mono text-label uppercase tracking-[0.14em] text-violet-300/80 hover:text-violet-200">
|
|
Thinking
|
|
</summary>
|
|
<div class="border-t border-violet-400/15 px-3 py-2 text-label text-dim/90 font-chat leading-relaxed">
|
|
<div class="md-content" dangerouslySetInnerHTML=${{ __html: safeHtml }} />
|
|
</div>
|
|
</details>
|
|
`;
|
|
}
|