ScuttleBot
feat: rich rendering v2 — terminal blocks, diffs, file cards (#89) Replace basic key-value meta renderers with type-specific rich cards: - Bash/exec: terminal-styled block with $ command, output, exit code (green/red status indicator) - Edit/Write: file card with path, language badge, syntax-highlighted diff (red/green line coloring for +/- lines) - Read/Glob/Grep: file browser card with path and results - WebSearch/WebFetch: search result card with URL - Error: red-bordered card with message and collapsible stack - Thinking: dim italic block - Images: inline with click-to-zoom - Status: compact badge (ok/error/running colors) Tool detection is automatic from meta.data.tool field.
Commit
118f5a0f5c77a7465af388d9a37a7dede09581f97f456e421b38e8f4c98bd3b8
Parent
e8d318d9d3a13a7…
1 file changed
+141
-27
+141
-27
| --- internal/api/ui/index.html | ||
| +++ internal/api/ui/index.html | ||
| @@ -195,10 +195,39 @@ | ||
| 195 | 195 | .msg-meta .meta-kv { display:grid; grid-template-columns:auto 1fr; gap:2px 10px; } |
| 196 | 196 | .msg-meta .meta-kv dt { color:#8b949e; } |
| 197 | 197 | .msg-meta .meta-kv dd { color:#e6edf3; word-break:break-all; } |
| 198 | 198 | .msg-meta pre { margin:4px 0 0; padding:6px 8px; background:#161b22; border:1px solid #21262d; border-radius:4px; overflow-x:auto; white-space:pre-wrap; word-break:break-all; color:#e6edf3; font-size:12px; } |
| 199 | 199 | .msg-meta img { max-width:100%; max-height:300px; border-radius:4px; margin-top:4px; } |
| 200 | +/* rich rendering v2 */ | |
| 201 | +.rich-card { border-radius:6px; overflow:hidden; margin-top:4px; font-size:12px; } | |
| 202 | +.rich-card-header { display:flex; align-items:center; gap:8px; padding:6px 10px; font-size:11px; } | |
| 203 | +.rich-card-body { padding:0; } | |
| 204 | +.rich-card-body pre { margin:0; border:0; border-radius:0; padding:8px 10px; font-size:12px; line-height:1.5; } | |
| 205 | +/* terminal block */ | |
| 206 | +.rich-terminal { border:1px solid #30363d; background:#0d1117; } | |
| 207 | +.rich-terminal .rich-card-header { background:#161b22; color:#8b949e; border-bottom:1px solid #21262d; } | |
| 208 | +.rich-terminal .rich-card-header .cmd { color:#a5d6ff; font-family:monospace; font-weight:600; } | |
| 209 | +.rich-terminal .exit-ok { color:#3fb950; } .rich-terminal .exit-fail { color:#f85149; } | |
| 210 | +/* file card */ | |
| 211 | +.rich-file { border:1px solid #30363d; background:#0d1117; } | |
| 212 | +.rich-file .rich-card-header { background:#161b22; border-bottom:1px solid #21262d; } | |
| 213 | +.rich-file .rich-card-header .path { color:#79c0ff; font-family:monospace; font-weight:600; } | |
| 214 | +.rich-file .rich-card-header .lang { background:#1f6feb33; color:#58a6ff; padding:1px 6px; border-radius:3px; font-size:10px; } | |
| 215 | +/* diff rendering */ | |
| 216 | +.rich-diff .line-add { background:#3fb95015; color:#3fb950; } | |
| 217 | +.rich-diff .line-del { background:#f8514915; color:#f85149; } | |
| 218 | +.rich-diff .line-ctx { color:#8b949e; } | |
| 219 | +.rich-diff .line-hdr { color:#d2a8ff; font-weight:600; } | |
| 220 | +/* error card */ | |
| 221 | +.rich-error { border:1px solid #f8514944; background:#f8514910; } | |
| 222 | +.rich-error .rich-card-header { background:#f8514918; color:#f85149; border-bottom:1px solid #f8514944; } | |
| 223 | +/* thinking block */ | |
| 224 | +.rich-thinking { border:1px solid #30363d; background:#0d1117; font-style:italic; color:#8b949e; padding:6px 10px; border-radius:6px; margin-top:4px; font-size:12px; } | |
| 225 | +/* search results */ | |
| 226 | +.rich-search { border:1px solid #30363d; background:#0d1117; } | |
| 227 | +.rich-search .rich-card-header { background:#161b22; border-bottom:1px solid #21262d; } | |
| 228 | +.rich-search .url { color:#58a6ff; word-break:break-all; } | |
| 200 | 229 | .chat-input { padding:9px 13px; padding-bottom:calc(9px + env(safe-area-inset-bottom, 0px)); border-top:1px solid #30363d; display:flex; gap:7px; flex-shrink:0; background:#161b22; } |
| 201 | 230 | |
| 202 | 231 | /* channels tab */ |
| 203 | 232 | .chan-card { display:flex; align-items:center; gap:12px; padding:12px 16px; border-bottom:1px solid #21262d; } |
| 204 | 233 | .chan-card:last-child { border-bottom:none; } |
| @@ -2336,51 +2365,136 @@ | ||
| 2336 | 2365 | case 'image': return renderImage(meta.data); |
| 2337 | 2366 | default: return renderGeneric(meta); |
| 2338 | 2367 | } |
| 2339 | 2368 | } |
| 2340 | 2369 | function renderToolResult(d) { |
| 2341 | - let html = '<div class="meta-type">tool call</div><dl class="meta-kv">'; | |
| 2342 | - html += '<dt>tool</dt><dd class="meta-tool">' + esc(d.tool || '?') + '</dd>'; | |
| 2343 | - if (d.file) html += '<dt>file</dt><dd class="meta-file">' + esc(d.file) + '</dd>'; | |
| 2344 | - if (d.command) html += '<dt>command</dt><dd class="meta-cmd">' + esc(d.command) + '</dd>'; | |
| 2345 | - if (d.pattern) html += '<dt>pattern</dt><dd>' + esc(d.pattern) + '</dd>'; | |
| 2346 | - if (d.query) html += '<dt>query</dt><dd>' + esc(d.query) + '</dd>'; | |
| 2347 | - if (d.url) html += '<dt>url</dt><dd>' + esc(d.url) + '</dd>'; | |
| 2348 | - if (d.result) html += '<dt>result</dt><dd>' + esc(d.result) + '</dd>'; | |
| 2349 | - html += '</dl>'; | |
| 2350 | - return html; | |
| 2370 | + const tool = (d.tool || '').toLowerCase(); | |
| 2371 | + // Bash/exec: terminal card | |
| 2372 | + if (tool === 'bash' || tool === 'exec' || tool === 'execute' || d.command) { | |
| 2373 | + return renderTerminal(d); | |
| 2374 | + } | |
| 2375 | + // Edit/Write/apply_patch: file card with diff | |
| 2376 | + if (tool === 'edit' || tool === 'write' || tool === 'apply_patch' || tool === 'notebookedit') { | |
| 2377 | + return renderFileCard(d); | |
| 2378 | + } | |
| 2379 | + // Read/Glob/Grep: file browser | |
| 2380 | + if (tool === 'read' || tool === 'glob' || tool === 'grep') { | |
| 2381 | + return renderFileSearch(d); | |
| 2382 | + } | |
| 2383 | + // WebSearch/WebFetch | |
| 2384 | + if (tool === 'websearch' || tool === 'webfetch' || tool === 'web_search' || d.url) { | |
| 2385 | + return renderSearchCard(d); | |
| 2386 | + } | |
| 2387 | + // Thinking | |
| 2388 | + if (tool === 'thinking' || d.thinking) { | |
| 2389 | + return '<div class="rich-thinking">' + esc(d.result || d.thinking || '') + '</div>'; | |
| 2390 | + } | |
| 2391 | + // Fallback: generic tool card | |
| 2392 | + return renderGenericTool(d); | |
| 2393 | +} | |
| 2394 | +function renderTerminal(d) { | |
| 2395 | + const cmd = d.command || d.tool || 'command'; | |
| 2396 | + const exitOk = d.exit_code === undefined || d.exit_code === 0; | |
| 2397 | + const exitCls = exitOk ? 'exit-ok' : 'exit-fail'; | |
| 2398 | + const exitText = d.exit_code !== undefined ? ` [exit ${d.exit_code}]` : ''; | |
| 2399 | + let output = d.result || d.output || ''; | |
| 2400 | + if (output.length > 2000) output = output.slice(0, 1997) + '...'; | |
| 2401 | + return `<div class="rich-card rich-terminal"> | |
| 2402 | + <div class="rich-card-header"><span class="cmd">$ ${esc(cmd)}</span><span class="${exitCls}">${exitText}</span></div> | |
| 2403 | + <div class="rich-card-body"><pre>${esc(output)}</pre></div> | |
| 2404 | + </div>`; | |
| 2405 | +} | |
| 2406 | +function renderFileCard(d) { | |
| 2407 | + const path = d.file || d.file_path || d.path || '?'; | |
| 2408 | + const ext = path.split('.').pop().toLowerCase(); | |
| 2409 | + const langMap = {go:'Go',js:'JS',ts:'TS',py:'Python',rb:'Ruby',rs:'Rust',java:'Java',md:'Markdown',yaml:'YAML',json:'JSON',html:'HTML',css:'CSS',sh:'Shell',sql:'SQL'}; | |
| 2410 | + const lang = langMap[ext] || ext; | |
| 2411 | + const tool = d.tool || 'file'; | |
| 2412 | + let body = ''; | |
| 2413 | + if (d.diff || d.hunks) { | |
| 2414 | + body = renderDiffBlock(d.diff || d.hunks); | |
| 2415 | + } else if (d.result) { | |
| 2416 | + body = `<pre>${esc(d.result.length > 2000 ? d.result.slice(0, 1997) + '...' : d.result)}</pre>`; | |
| 2417 | + } | |
| 2418 | + return `<div class="rich-card rich-file"> | |
| 2419 | + <div class="rich-card-header"><span class="path">${esc(path)}</span><span class="lang">${esc(lang)}</span><span style="color:#8b949e;margin-left:auto">${esc(tool)}</span></div> | |
| 2420 | + <div class="rich-card-body">${body}</div> | |
| 2421 | + </div>`; | |
| 2422 | +} | |
| 2423 | +function renderDiffBlock(raw) { | |
| 2424 | + const text = typeof raw === 'string' ? raw : JSON.stringify(raw, null, 2); | |
| 2425 | + const lines = text.split('\n').map(line => { | |
| 2426 | + if (line.startsWith('@@')) return `<span class="line-hdr">${esc(line)}</span>`; | |
| 2427 | + else if (line.startsWith('+')) return `<span class="line-add">${esc(line)}</span>`; | |
| 2428 | + else if (line.startsWith('-')) return `<span class="line-del">${esc(line)}</span>`; | |
| 2429 | + else return `<span class="line-ctx">${esc(line)}</span>`; | |
| 2430 | + }); | |
| 2431 | + return `<pre class="rich-diff">${lines.join('\n')}</pre>`; | |
| 2432 | +} | |
| 2433 | +function renderFileSearch(d) { | |
| 2434 | + const path = d.file || d.pattern || d.path || ''; | |
| 2435 | + const tool = d.tool || 'search'; | |
| 2436 | + let body = d.result || ''; | |
| 2437 | + if (body.length > 2000) body = body.slice(0, 1997) + '...'; | |
| 2438 | + return `<div class="rich-card rich-file"> | |
| 2439 | + <div class="rich-card-header"><span class="path">${esc(path)}</span><span style="color:#8b949e;margin-left:auto">${esc(tool)}</span></div> | |
| 2440 | + <div class="rich-card-body"><pre>${esc(body)}</pre></div> | |
| 2441 | + </div>`; | |
| 2442 | +} | |
| 2443 | +function renderSearchCard(d) { | |
| 2444 | + const url = d.url || ''; | |
| 2445 | + const result = d.result || d.summary || ''; | |
| 2446 | + return `<div class="rich-card rich-search"> | |
| 2447 | + <div class="rich-card-header"><span class="url">${esc(url)}</span></div> | |
| 2448 | + <div class="rich-card-body"><pre>${esc(result.length > 2000 ? result.slice(0, 1997) + '...' : result)}</pre></div> | |
| 2449 | + </div>`; | |
| 2450 | +} | |
| 2451 | +function renderGenericTool(d) { | |
| 2452 | + const tool = d.tool || '?'; | |
| 2453 | + let body = ''; | |
| 2454 | + if (d.result) body = d.result; | |
| 2455 | + else body = JSON.stringify(d, null, 2); | |
| 2456 | + if (body.length > 2000) body = body.slice(0, 1997) + '...'; | |
| 2457 | + return `<div class="rich-card rich-file"> | |
| 2458 | + <div class="rich-card-header"><span style="color:#d2a8ff;font-weight:600">${esc(tool)}</span></div> | |
| 2459 | + <div class="rich-card-body"><pre>${esc(body)}</pre></div> | |
| 2460 | + </div>`; | |
| 2351 | 2461 | } |
| 2352 | 2462 | function renderDiff(d) { |
| 2353 | - let html = '<div class="meta-type">diff</div>'; | |
| 2354 | - if (d.file) html += '<div class="meta-file">' + esc(d.file) + '</div>'; | |
| 2355 | - if (d.hunks) html += '<pre>' + esc(typeof d.hunks === 'string' ? d.hunks : JSON.stringify(d.hunks, null, 2)) + '</pre>'; | |
| 2356 | - return html; | |
| 2463 | + const file = d.file || ''; | |
| 2464 | + return `<div class="rich-card rich-file"> | |
| 2465 | + <div class="rich-card-header"><span class="path">${esc(file)}</span><span style="color:#8b949e;margin-left:auto">diff</span></div> | |
| 2466 | + <div class="rich-card-body">${renderDiffBlock(d.hunks || d.diff || '')}</div> | |
| 2467 | + </div>`; | |
| 2357 | 2468 | } |
| 2358 | 2469 | function renderError(d) { |
| 2359 | - let html = '<div class="meta-type meta-error">error</div>'; | |
| 2360 | - html += '<div class="meta-error">' + esc(d.message || '') + '</div>'; | |
| 2361 | - if (d.stack) html += '<pre>' + esc(d.stack) + '</pre>'; | |
| 2362 | - return html; | |
| 2470 | + const msg = d.message || d.error || ''; | |
| 2471 | + const stack = d.stack || ''; | |
| 2472 | + return `<div class="rich-card rich-error"> | |
| 2473 | + <div class="rich-card-header">error</div> | |
| 2474 | + <div class="rich-card-body"><pre>${esc(msg)}${stack ? '\n\n' + esc(stack) : ''}</pre></div> | |
| 2475 | + </div>`; | |
| 2363 | 2476 | } |
| 2364 | 2477 | function renderStatus(d) { |
| 2365 | 2478 | const state = (d.state || 'running').toLowerCase(); |
| 2366 | 2479 | const cls = state === 'ok' || state === 'success' || state === 'done' ? 'ok' : state === 'error' || state === 'failed' ? 'error' : 'running'; |
| 2367 | - let html = '<div class="meta-type">status</div>'; | |
| 2368 | - html += '<span class="meta-status ' + cls + '">' + esc(d.state || '') + '</span>'; | |
| 2480 | + let html = '<span class="meta-status ' + cls + '">' + esc(d.state || '') + '</span>'; | |
| 2369 | 2481 | if (d.message) html += ' <span>' + esc(d.message) + '</span>'; |
| 2370 | 2482 | return html; |
| 2371 | 2483 | } |
| 2372 | 2484 | function renderArtifact(d) { |
| 2373 | - let html = '<div class="meta-type">artifact</div>'; | |
| 2374 | - html += '<div class="meta-file">' + esc(d.name || d.path || '?') + '</div>'; | |
| 2375 | - if (d.language) html += '<span class="tag perm">' + esc(d.language) + '</span>'; | |
| 2376 | - return html; | |
| 2485 | + const path = d.name || d.path || '?'; | |
| 2486 | + const ext = path.split('.').pop().toLowerCase(); | |
| 2487 | + const langMap = {go:'Go',js:'JS',ts:'TS',py:'Python',rb:'Ruby',rs:'Rust'}; | |
| 2488 | + const lang = d.language || langMap[ext] || ext; | |
| 2489 | + return `<div class="rich-card rich-file"> | |
| 2490 | + <div class="rich-card-header"><span class="path">${esc(path)}</span><span class="lang">${esc(lang)}</span><span style="color:#8b949e;margin-left:auto">artifact</span></div> | |
| 2491 | + </div>`; | |
| 2377 | 2492 | } |
| 2378 | 2493 | function renderImage(d) { |
| 2379 | - let html = '<div class="meta-type">image</div>'; | |
| 2380 | - if (d.url) html += '<img src="' + esc(d.url) + '" alt="' + esc(d.alt || '') + '" loading="lazy">'; | |
| 2381 | - return html; | |
| 2494 | + if (!d.url) return ''; | |
| 2495 | + return `<div style="margin-top:4px"><img src="${esc(d.url)}" alt="${esc(d.alt || '')}" loading="lazy" style="max-width:100%;max-height:400px;border-radius:6px;cursor:pointer" onclick="window.open(this.src)"></div>`; | |
| 2382 | 2496 | } |
| 2383 | 2497 | function renderGeneric(meta) { |
| 2384 | 2498 | return '<div class="meta-type">' + esc(meta.type) + '</div><pre>' + esc(JSON.stringify(meta.data, null, 2)) + '</pre>'; |
| 2385 | 2499 | } |
| 2386 | 2500 | |
| 2387 | 2501 |
| --- internal/api/ui/index.html | |
| +++ internal/api/ui/index.html | |
| @@ -195,10 +195,39 @@ | |
| 195 | .msg-meta .meta-kv { display:grid; grid-template-columns:auto 1fr; gap:2px 10px; } |
| 196 | .msg-meta .meta-kv dt { color:#8b949e; } |
| 197 | .msg-meta .meta-kv dd { color:#e6edf3; word-break:break-all; } |
| 198 | .msg-meta pre { margin:4px 0 0; padding:6px 8px; background:#161b22; border:1px solid #21262d; border-radius:4px; overflow-x:auto; white-space:pre-wrap; word-break:break-all; color:#e6edf3; font-size:12px; } |
| 199 | .msg-meta img { max-width:100%; max-height:300px; border-radius:4px; margin-top:4px; } |
| 200 | .chat-input { padding:9px 13px; padding-bottom:calc(9px + env(safe-area-inset-bottom, 0px)); border-top:1px solid #30363d; display:flex; gap:7px; flex-shrink:0; background:#161b22; } |
| 201 | |
| 202 | /* channels tab */ |
| 203 | .chan-card { display:flex; align-items:center; gap:12px; padding:12px 16px; border-bottom:1px solid #21262d; } |
| 204 | .chan-card:last-child { border-bottom:none; } |
| @@ -2336,51 +2365,136 @@ | |
| 2336 | case 'image': return renderImage(meta.data); |
| 2337 | default: return renderGeneric(meta); |
| 2338 | } |
| 2339 | } |
| 2340 | function renderToolResult(d) { |
| 2341 | let html = '<div class="meta-type">tool call</div><dl class="meta-kv">'; |
| 2342 | html += '<dt>tool</dt><dd class="meta-tool">' + esc(d.tool || '?') + '</dd>'; |
| 2343 | if (d.file) html += '<dt>file</dt><dd class="meta-file">' + esc(d.file) + '</dd>'; |
| 2344 | if (d.command) html += '<dt>command</dt><dd class="meta-cmd">' + esc(d.command) + '</dd>'; |
| 2345 | if (d.pattern) html += '<dt>pattern</dt><dd>' + esc(d.pattern) + '</dd>'; |
| 2346 | if (d.query) html += '<dt>query</dt><dd>' + esc(d.query) + '</dd>'; |
| 2347 | if (d.url) html += '<dt>url</dt><dd>' + esc(d.url) + '</dd>'; |
| 2348 | if (d.result) html += '<dt>result</dt><dd>' + esc(d.result) + '</dd>'; |
| 2349 | html += '</dl>'; |
| 2350 | return html; |
| 2351 | } |
| 2352 | function renderDiff(d) { |
| 2353 | let html = '<div class="meta-type">diff</div>'; |
| 2354 | if (d.file) html += '<div class="meta-file">' + esc(d.file) + '</div>'; |
| 2355 | if (d.hunks) html += '<pre>' + esc(typeof d.hunks === 'string' ? d.hunks : JSON.stringify(d.hunks, null, 2)) + '</pre>'; |
| 2356 | return html; |
| 2357 | } |
| 2358 | function renderError(d) { |
| 2359 | let html = '<div class="meta-type meta-error">error</div>'; |
| 2360 | html += '<div class="meta-error">' + esc(d.message || '') + '</div>'; |
| 2361 | if (d.stack) html += '<pre>' + esc(d.stack) + '</pre>'; |
| 2362 | return html; |
| 2363 | } |
| 2364 | function renderStatus(d) { |
| 2365 | const state = (d.state || 'running').toLowerCase(); |
| 2366 | const cls = state === 'ok' || state === 'success' || state === 'done' ? 'ok' : state === 'error' || state === 'failed' ? 'error' : 'running'; |
| 2367 | let html = '<div class="meta-type">status</div>'; |
| 2368 | html += '<span class="meta-status ' + cls + '">' + esc(d.state || '') + '</span>'; |
| 2369 | if (d.message) html += ' <span>' + esc(d.message) + '</span>'; |
| 2370 | return html; |
| 2371 | } |
| 2372 | function renderArtifact(d) { |
| 2373 | let html = '<div class="meta-type">artifact</div>'; |
| 2374 | html += '<div class="meta-file">' + esc(d.name || d.path || '?') + '</div>'; |
| 2375 | if (d.language) html += '<span class="tag perm">' + esc(d.language) + '</span>'; |
| 2376 | return html; |
| 2377 | } |
| 2378 | function renderImage(d) { |
| 2379 | let html = '<div class="meta-type">image</div>'; |
| 2380 | if (d.url) html += '<img src="' + esc(d.url) + '" alt="' + esc(d.alt || '') + '" loading="lazy">'; |
| 2381 | return html; |
| 2382 | } |
| 2383 | function renderGeneric(meta) { |
| 2384 | return '<div class="meta-type">' + esc(meta.type) + '</div><pre>' + esc(JSON.stringify(meta.data, null, 2)) + '</pre>'; |
| 2385 | } |
| 2386 | |
| 2387 |
| --- internal/api/ui/index.html | |
| +++ internal/api/ui/index.html | |
| @@ -195,10 +195,39 @@ | |
| 195 | .msg-meta .meta-kv { display:grid; grid-template-columns:auto 1fr; gap:2px 10px; } |
| 196 | .msg-meta .meta-kv dt { color:#8b949e; } |
| 197 | .msg-meta .meta-kv dd { color:#e6edf3; word-break:break-all; } |
| 198 | .msg-meta pre { margin:4px 0 0; padding:6px 8px; background:#161b22; border:1px solid #21262d; border-radius:4px; overflow-x:auto; white-space:pre-wrap; word-break:break-all; color:#e6edf3; font-size:12px; } |
| 199 | .msg-meta img { max-width:100%; max-height:300px; border-radius:4px; margin-top:4px; } |
| 200 | /* rich rendering v2 */ |
| 201 | .rich-card { border-radius:6px; overflow:hidden; margin-top:4px; font-size:12px; } |
| 202 | .rich-card-header { display:flex; align-items:center; gap:8px; padding:6px 10px; font-size:11px; } |
| 203 | .rich-card-body { padding:0; } |
| 204 | .rich-card-body pre { margin:0; border:0; border-radius:0; padding:8px 10px; font-size:12px; line-height:1.5; } |
| 205 | /* terminal block */ |
| 206 | .rich-terminal { border:1px solid #30363d; background:#0d1117; } |
| 207 | .rich-terminal .rich-card-header { background:#161b22; color:#8b949e; border-bottom:1px solid #21262d; } |
| 208 | .rich-terminal .rich-card-header .cmd { color:#a5d6ff; font-family:monospace; font-weight:600; } |
| 209 | .rich-terminal .exit-ok { color:#3fb950; } .rich-terminal .exit-fail { color:#f85149; } |
| 210 | /* file card */ |
| 211 | .rich-file { border:1px solid #30363d; background:#0d1117; } |
| 212 | .rich-file .rich-card-header { background:#161b22; border-bottom:1px solid #21262d; } |
| 213 | .rich-file .rich-card-header .path { color:#79c0ff; font-family:monospace; font-weight:600; } |
| 214 | .rich-file .rich-card-header .lang { background:#1f6feb33; color:#58a6ff; padding:1px 6px; border-radius:3px; font-size:10px; } |
| 215 | /* diff rendering */ |
| 216 | .rich-diff .line-add { background:#3fb95015; color:#3fb950; } |
| 217 | .rich-diff .line-del { background:#f8514915; color:#f85149; } |
| 218 | .rich-diff .line-ctx { color:#8b949e; } |
| 219 | .rich-diff .line-hdr { color:#d2a8ff; font-weight:600; } |
| 220 | /* error card */ |
| 221 | .rich-error { border:1px solid #f8514944; background:#f8514910; } |
| 222 | .rich-error .rich-card-header { background:#f8514918; color:#f85149; border-bottom:1px solid #f8514944; } |
| 223 | /* thinking block */ |
| 224 | .rich-thinking { border:1px solid #30363d; background:#0d1117; font-style:italic; color:#8b949e; padding:6px 10px; border-radius:6px; margin-top:4px; font-size:12px; } |
| 225 | /* search results */ |
| 226 | .rich-search { border:1px solid #30363d; background:#0d1117; } |
| 227 | .rich-search .rich-card-header { background:#161b22; border-bottom:1px solid #21262d; } |
| 228 | .rich-search .url { color:#58a6ff; word-break:break-all; } |
| 229 | .chat-input { padding:9px 13px; padding-bottom:calc(9px + env(safe-area-inset-bottom, 0px)); border-top:1px solid #30363d; display:flex; gap:7px; flex-shrink:0; background:#161b22; } |
| 230 | |
| 231 | /* channels tab */ |
| 232 | .chan-card { display:flex; align-items:center; gap:12px; padding:12px 16px; border-bottom:1px solid #21262d; } |
| 233 | .chan-card:last-child { border-bottom:none; } |
| @@ -2336,51 +2365,136 @@ | |
| 2365 | case 'image': return renderImage(meta.data); |
| 2366 | default: return renderGeneric(meta); |
| 2367 | } |
| 2368 | } |
| 2369 | function renderToolResult(d) { |
| 2370 | const tool = (d.tool || '').toLowerCase(); |
| 2371 | // Bash/exec: terminal card |
| 2372 | if (tool === 'bash' || tool === 'exec' || tool === 'execute' || d.command) { |
| 2373 | return renderTerminal(d); |
| 2374 | } |
| 2375 | // Edit/Write/apply_patch: file card with diff |
| 2376 | if (tool === 'edit' || tool === 'write' || tool === 'apply_patch' || tool === 'notebookedit') { |
| 2377 | return renderFileCard(d); |
| 2378 | } |
| 2379 | // Read/Glob/Grep: file browser |
| 2380 | if (tool === 'read' || tool === 'glob' || tool === 'grep') { |
| 2381 | return renderFileSearch(d); |
| 2382 | } |
| 2383 | // WebSearch/WebFetch |
| 2384 | if (tool === 'websearch' || tool === 'webfetch' || tool === 'web_search' || d.url) { |
| 2385 | return renderSearchCard(d); |
| 2386 | } |
| 2387 | // Thinking |
| 2388 | if (tool === 'thinking' || d.thinking) { |
| 2389 | return '<div class="rich-thinking">' + esc(d.result || d.thinking || '') + '</div>'; |
| 2390 | } |
| 2391 | // Fallback: generic tool card |
| 2392 | return renderGenericTool(d); |
| 2393 | } |
| 2394 | function renderTerminal(d) { |
| 2395 | const cmd = d.command || d.tool || 'command'; |
| 2396 | const exitOk = d.exit_code === undefined || d.exit_code === 0; |
| 2397 | const exitCls = exitOk ? 'exit-ok' : 'exit-fail'; |
| 2398 | const exitText = d.exit_code !== undefined ? ` [exit ${d.exit_code}]` : ''; |
| 2399 | let output = d.result || d.output || ''; |
| 2400 | if (output.length > 2000) output = output.slice(0, 1997) + '...'; |
| 2401 | return `<div class="rich-card rich-terminal"> |
| 2402 | <div class="rich-card-header"><span class="cmd">$ ${esc(cmd)}</span><span class="${exitCls}">${exitText}</span></div> |
| 2403 | <div class="rich-card-body"><pre>${esc(output)}</pre></div> |
| 2404 | </div>`; |
| 2405 | } |
| 2406 | function renderFileCard(d) { |
| 2407 | const path = d.file || d.file_path || d.path || '?'; |
| 2408 | const ext = path.split('.').pop().toLowerCase(); |
| 2409 | const langMap = {go:'Go',js:'JS',ts:'TS',py:'Python',rb:'Ruby',rs:'Rust',java:'Java',md:'Markdown',yaml:'YAML',json:'JSON',html:'HTML',css:'CSS',sh:'Shell',sql:'SQL'}; |
| 2410 | const lang = langMap[ext] || ext; |
| 2411 | const tool = d.tool || 'file'; |
| 2412 | let body = ''; |
| 2413 | if (d.diff || d.hunks) { |
| 2414 | body = renderDiffBlock(d.diff || d.hunks); |
| 2415 | } else if (d.result) { |
| 2416 | body = `<pre>${esc(d.result.length > 2000 ? d.result.slice(0, 1997) + '...' : d.result)}</pre>`; |
| 2417 | } |
| 2418 | return `<div class="rich-card rich-file"> |
| 2419 | <div class="rich-card-header"><span class="path">${esc(path)}</span><span class="lang">${esc(lang)}</span><span style="color:#8b949e;margin-left:auto">${esc(tool)}</span></div> |
| 2420 | <div class="rich-card-body">${body}</div> |
| 2421 | </div>`; |
| 2422 | } |
| 2423 | function renderDiffBlock(raw) { |
| 2424 | const text = typeof raw === 'string' ? raw : JSON.stringify(raw, null, 2); |
| 2425 | const lines = text.split('\n').map(line => { |
| 2426 | if (line.startsWith('@@')) return `<span class="line-hdr">${esc(line)}</span>`; |
| 2427 | else if (line.startsWith('+')) return `<span class="line-add">${esc(line)}</span>`; |
| 2428 | else if (line.startsWith('-')) return `<span class="line-del">${esc(line)}</span>`; |
| 2429 | else return `<span class="line-ctx">${esc(line)}</span>`; |
| 2430 | }); |
| 2431 | return `<pre class="rich-diff">${lines.join('\n')}</pre>`; |
| 2432 | } |
| 2433 | function renderFileSearch(d) { |
| 2434 | const path = d.file || d.pattern || d.path || ''; |
| 2435 | const tool = d.tool || 'search'; |
| 2436 | let body = d.result || ''; |
| 2437 | if (body.length > 2000) body = body.slice(0, 1997) + '...'; |
| 2438 | return `<div class="rich-card rich-file"> |
| 2439 | <div class="rich-card-header"><span class="path">${esc(path)}</span><span style="color:#8b949e;margin-left:auto">${esc(tool)}</span></div> |
| 2440 | <div class="rich-card-body"><pre>${esc(body)}</pre></div> |
| 2441 | </div>`; |
| 2442 | } |
| 2443 | function renderSearchCard(d) { |
| 2444 | const url = d.url || ''; |
| 2445 | const result = d.result || d.summary || ''; |
| 2446 | return `<div class="rich-card rich-search"> |
| 2447 | <div class="rich-card-header"><span class="url">${esc(url)}</span></div> |
| 2448 | <div class="rich-card-body"><pre>${esc(result.length > 2000 ? result.slice(0, 1997) + '...' : result)}</pre></div> |
| 2449 | </div>`; |
| 2450 | } |
| 2451 | function renderGenericTool(d) { |
| 2452 | const tool = d.tool || '?'; |
| 2453 | let body = ''; |
| 2454 | if (d.result) body = d.result; |
| 2455 | else body = JSON.stringify(d, null, 2); |
| 2456 | if (body.length > 2000) body = body.slice(0, 1997) + '...'; |
| 2457 | return `<div class="rich-card rich-file"> |
| 2458 | <div class="rich-card-header"><span style="color:#d2a8ff;font-weight:600">${esc(tool)}</span></div> |
| 2459 | <div class="rich-card-body"><pre>${esc(body)}</pre></div> |
| 2460 | </div>`; |
| 2461 | } |
| 2462 | function renderDiff(d) { |
| 2463 | const file = d.file || ''; |
| 2464 | return `<div class="rich-card rich-file"> |
| 2465 | <div class="rich-card-header"><span class="path">${esc(file)}</span><span style="color:#8b949e;margin-left:auto">diff</span></div> |
| 2466 | <div class="rich-card-body">${renderDiffBlock(d.hunks || d.diff || '')}</div> |
| 2467 | </div>`; |
| 2468 | } |
| 2469 | function renderError(d) { |
| 2470 | const msg = d.message || d.error || ''; |
| 2471 | const stack = d.stack || ''; |
| 2472 | return `<div class="rich-card rich-error"> |
| 2473 | <div class="rich-card-header">error</div> |
| 2474 | <div class="rich-card-body"><pre>${esc(msg)}${stack ? '\n\n' + esc(stack) : ''}</pre></div> |
| 2475 | </div>`; |
| 2476 | } |
| 2477 | function renderStatus(d) { |
| 2478 | const state = (d.state || 'running').toLowerCase(); |
| 2479 | const cls = state === 'ok' || state === 'success' || state === 'done' ? 'ok' : state === 'error' || state === 'failed' ? 'error' : 'running'; |
| 2480 | let html = '<span class="meta-status ' + cls + '">' + esc(d.state || '') + '</span>'; |
| 2481 | if (d.message) html += ' <span>' + esc(d.message) + '</span>'; |
| 2482 | return html; |
| 2483 | } |
| 2484 | function renderArtifact(d) { |
| 2485 | const path = d.name || d.path || '?'; |
| 2486 | const ext = path.split('.').pop().toLowerCase(); |
| 2487 | const langMap = {go:'Go',js:'JS',ts:'TS',py:'Python',rb:'Ruby',rs:'Rust'}; |
| 2488 | const lang = d.language || langMap[ext] || ext; |
| 2489 | return `<div class="rich-card rich-file"> |
| 2490 | <div class="rich-card-header"><span class="path">${esc(path)}</span><span class="lang">${esc(lang)}</span><span style="color:#8b949e;margin-left:auto">artifact</span></div> |
| 2491 | </div>`; |
| 2492 | } |
| 2493 | function renderImage(d) { |
| 2494 | if (!d.url) return ''; |
| 2495 | return `<div style="margin-top:4px"><img src="${esc(d.url)}" alt="${esc(d.alt || '')}" loading="lazy" style="max-width:100%;max-height:400px;border-radius:6px;cursor:pointer" onclick="window.open(this.src)"></div>`; |
| 2496 | } |
| 2497 | function renderGeneric(meta) { |
| 2498 | return '<div class="meta-type">' + esc(meta.type) + '</div><pre>' + esc(JSON.stringify(meta.data, null, 2)) + '</pre>'; |
| 2499 | } |
| 2500 | |
| 2501 |