// 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 `
${highlighted}
`; } }; 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`
`; } // 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`
${toolCalls.map(tc => { const summary = getToolSummary(tc.name, tc.input); return html` ${tc.name} ${summary !== tc.name && html`${summary}`} `; })}
`; } // 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`
Thinking
`; }