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.

lmata 2026-04-05 17:56 trunk
Commit 118f5a0f5c77a7465af388d9a37a7dede09581f97f456e421b38e8f4c98bd3b8
1 file changed +141 -27
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -195,10 +195,39 @@
195195
.msg-meta .meta-kv { display:grid; grid-template-columns:auto 1fr; gap:2px 10px; }
196196
.msg-meta .meta-kv dt { color:#8b949e; }
197197
.msg-meta .meta-kv dd { color:#e6edf3; word-break:break-all; }
198198
.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; }
199199
.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; }
200229
.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; }
201230
202231
/* channels tab */
203232
.chan-card { display:flex; align-items:center; gap:12px; padding:12px 16px; border-bottom:1px solid #21262d; }
204233
.chan-card:last-child { border-bottom:none; }
@@ -2336,51 +2365,136 @@
23362365
case 'image': return renderImage(meta.data);
23372366
default: return renderGeneric(meta);
23382367
}
23392368
}
23402369
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>`;
23512461
}
23522462
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>`;
23572468
}
23582469
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>`;
23632476
}
23642477
function renderStatus(d) {
23652478
const state = (d.state || 'running').toLowerCase();
23662479
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>';
23692481
if (d.message) html += ' <span>' + esc(d.message) + '</span>';
23702482
return html;
23712483
}
23722484
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>`;
23772492
}
23782493
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>`;
23822496
}
23832497
function renderGeneric(meta) {
23842498
return '<div class="meta-type">' + esc(meta.type) + '</div><pre>' + esc(JSON.stringify(meta.data, null, 2)) + '</pre>';
23852499
}
23862500
23872501
--- 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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button