diff.ts
typescript
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef
debug(push/stream): instrument O-frame decode path with INF…
Sonnet 4.6
patch
121 days ago
| 1 | /** |
| 2 | * diff.ts — Muse semantic diff page. |
| 3 | * |
| 4 | * Unlike Git, Muse knows *what semantically changed* (structured_delta) and |
| 5 | * *which files truly changed* (snapshot_diff). We use both to render: |
| 6 | * |
| 7 | * 1. A stats bar — symbol counts, file counts (things Git cannot show) |
| 8 | * 2. Per-file cards — semantic symbol changes above the line diff |
| 9 | * 3. Real line diff — comparing parent vs current content, not just dumping |
| 10 | * the whole file green |
| 11 | */ |
| 12 | |
| 13 | import hljs from 'highlight.js/lib/core'; |
| 14 | import { extToLang } from '../lang-detect.ts'; |
| 15 | import python from 'highlight.js/lib/languages/python'; |
| 16 | import typescript from 'highlight.js/lib/languages/typescript'; |
| 17 | import javascript from 'highlight.js/lib/languages/javascript'; |
| 18 | import rust from 'highlight.js/lib/languages/rust'; |
| 19 | import go from 'highlight.js/lib/languages/go'; |
| 20 | import swift from 'highlight.js/lib/languages/swift'; |
| 21 | import kotlin from 'highlight.js/lib/languages/kotlin'; |
| 22 | import java from 'highlight.js/lib/languages/java'; |
| 23 | import ruby from 'highlight.js/lib/languages/ruby'; |
| 24 | import cpp from 'highlight.js/lib/languages/cpp'; |
| 25 | import json from 'highlight.js/lib/languages/json'; |
| 26 | import yaml from 'highlight.js/lib/languages/yaml'; |
| 27 | import toml from 'highlight.js/lib/languages/ini'; |
| 28 | import bash from 'highlight.js/lib/languages/bash'; |
| 29 | import xml from 'highlight.js/lib/languages/xml'; |
| 30 | import css from 'highlight.js/lib/languages/css'; |
| 31 | import sql from 'highlight.js/lib/languages/sql'; |
| 32 | import markdown from 'highlight.js/lib/languages/markdown'; |
| 33 | import plaintext from 'highlight.js/lib/languages/plaintext'; |
| 34 | |
| 35 | hljs.registerLanguage('python', python); |
| 36 | hljs.registerLanguage('typescript', typescript); |
| 37 | hljs.registerLanguage('javascript', javascript); |
| 38 | hljs.registerLanguage('rust', rust); |
| 39 | hljs.registerLanguage('go', go); |
| 40 | hljs.registerLanguage('swift', swift); |
| 41 | hljs.registerLanguage('kotlin', kotlin); |
| 42 | hljs.registerLanguage('java', java); |
| 43 | hljs.registerLanguage('ruby', ruby); |
| 44 | hljs.registerLanguage('cpp', cpp); |
| 45 | hljs.registerLanguage('json', json); |
| 46 | hljs.registerLanguage('yaml', yaml); |
| 47 | hljs.registerLanguage('toml', toml); |
| 48 | hljs.registerLanguage('bash', bash); |
| 49 | hljs.registerLanguage('xml', xml); |
| 50 | hljs.registerLanguage('css', css); |
| 51 | hljs.registerLanguage('sql', sql); |
| 52 | hljs.registerLanguage('markdown', markdown); |
| 53 | hljs.registerLanguage('plaintext', plaintext); |
| 54 | |
| 55 | // ── Types ───────────────────────────────────────────────────────────────────── |
| 56 | |
| 57 | interface ChildOp { |
| 58 | address: string; |
| 59 | op: string; |
| 60 | content_summary: string; |
| 61 | } |
| 62 | |
| 63 | interface FileOp { |
| 64 | address: string; |
| 65 | op: string; |
| 66 | child_summary: string; |
| 67 | child_ops: ChildOp[]; |
| 68 | } |
| 69 | |
| 70 | interface StructuredDelta { |
| 71 | domain: string; |
| 72 | ops: FileOp[]; |
| 73 | } |
| 74 | |
| 75 | interface SnapshotDiff { |
| 76 | added: string[]; |
| 77 | modified: string[]; |
| 78 | removed: string[]; |
| 79 | total_files: number; |
| 80 | } |
| 81 | |
| 82 | interface CommitData { |
| 83 | commitId: string; |
| 84 | message: string; |
| 85 | author: string; |
| 86 | timestamp: string; |
| 87 | branch: string; |
| 88 | parentIds: string[]; |
| 89 | } |
| 90 | |
| 91 | interface PageData { |
| 92 | page: string; |
| 93 | repoId: string; |
| 94 | commitId: string; |
| 95 | shortId: string; |
| 96 | owner: string; |
| 97 | repoSlug: string; |
| 98 | baseUrl: string; |
| 99 | parentId: string | null; |
| 100 | structuredDelta: StructuredDelta | null; |
| 101 | snapshotDiff: SnapshotDiff; |
| 102 | commit: CommitData | null; |
| 103 | viewerType: string; |
| 104 | domainName: string; |
| 105 | } |
| 106 | |
| 107 | // ── Helpers ─────────────────────────────────────────────────────────────────── |
| 108 | |
| 109 | function esc(s: string): string { |
| 110 | return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); |
| 111 | } |
| 112 | |
| 113 | |
| 114 | function highlight(code: string, lang: string): string { |
| 115 | try { |
| 116 | return hljs.highlight(code, { language: lang, ignoreIllegals: true }).value; |
| 117 | } catch { |
| 118 | return esc(code); |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | /** Determine op class from operation string. */ |
| 123 | function opClass(op: string): string { |
| 124 | if (op === 'insert') return 'df3-op-add'; |
| 125 | if (op === 'delete') return 'df3-op-del'; |
| 126 | return 'df3-op-mod'; |
| 127 | } |
| 128 | |
| 129 | function opSign(op: string): string { |
| 130 | if (op === 'insert') return '+'; |
| 131 | if (op === 'delete') return '−'; |
| 132 | return '~'; |
| 133 | } |
| 134 | |
| 135 | /** Detect kind from content_summary string. */ |
| 136 | function kindChip(summary: string): string { |
| 137 | const s = summary.toLowerCase(); |
| 138 | if (s.includes('class')) return '<span class="df3-kind df3-k-class">class</span>'; |
| 139 | if (s.includes('method')) return '<span class="df3-kind df3-k-method">method</span>'; |
| 140 | if (s.includes('function') || s.includes('func')) return '<span class="df3-kind df3-k-func">func</span>'; |
| 141 | if (s.includes('import')) return '<span class="df3-kind df3-k-import">import</span>'; |
| 142 | if (s.includes('variable') || s.includes('constant')) return '<span class="df3-kind df3-k-var">var</span>'; |
| 143 | return ''; |
| 144 | } |
| 145 | |
| 146 | // ── Myers line diff ─────────────────────────────────────────────────────────── |
| 147 | // Simple O(ND) diff — sufficient for typical commit sizes. |
| 148 | |
| 149 | type LineKind = 'add' | 'del' | 'ctx'; |
| 150 | interface DiffLine { kind: LineKind; text: string; oldN: number; newN: number; } |
| 151 | |
| 152 | function diffLines(oldLines: string[], newLines: string[]): DiffLine[] { |
| 153 | const m = oldLines.length, n = newLines.length; |
| 154 | const max = m + n; |
| 155 | const v: number[] = new Array(2 * max + 1).fill(0); |
| 156 | const trace: number[][] = []; |
| 157 | |
| 158 | outer: for (let d = 0; d <= max; d++) { |
| 159 | trace.push([...v]); |
| 160 | for (let k = -d; k <= d; k += 2) { |
| 161 | const ki = k + max; |
| 162 | let x: number; |
| 163 | if (k === -d || (k !== d && v[ki - 1] < v[ki + 1])) { |
| 164 | x = v[ki + 1]; |
| 165 | } else { |
| 166 | x = v[ki - 1] + 1; |
| 167 | } |
| 168 | let y = x - k; |
| 169 | while (x < m && y < n && oldLines[x] === newLines[y]) { x++; y++; } |
| 170 | v[ki] = x; |
| 171 | if (x >= m && y >= n) break outer; |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | // Backtrack |
| 176 | const result: DiffLine[] = []; |
| 177 | let x = m, y = n; |
| 178 | for (let d = trace.length - 1; d >= 0; d--) { |
| 179 | const vd = trace[d]; |
| 180 | const k = x - y; |
| 181 | const ki = k + max; |
| 182 | const prevK = (k === -d || (k !== d && vd[ki - 1] < vd[ki + 1])) ? k + 1 : k - 1; |
| 183 | const prevX = vd[prevK + max]; |
| 184 | const prevY = prevX - prevK; |
| 185 | while (x > prevX && y > prevY) { |
| 186 | result.unshift({ kind: 'ctx', text: oldLines[x - 1], oldN: x, newN: y }); |
| 187 | x--; y--; |
| 188 | } |
| 189 | if (d > 0) { |
| 190 | if (x === prevX) { |
| 191 | result.unshift({ kind: 'add', text: newLines[y - 1], oldN: 0, newN: y }); |
| 192 | y--; |
| 193 | } else { |
| 194 | result.unshift({ kind: 'del', text: oldLines[x - 1], oldN: x, newN: 0 }); |
| 195 | x--; |
| 196 | } |
| 197 | } |
| 198 | } |
| 199 | return result; |
| 200 | } |
| 201 | |
| 202 | /** Render a diff as an HTML table with syntax-highlighted tokens. */ |
| 203 | function renderDiffTable(lines: DiffLine[], lang: string): string { |
| 204 | // Highlight the full old and new texts then split, to preserve multi-line spans. |
| 205 | const oldText = lines.filter(l => l.kind !== 'add').map(l => l.text).join('\n'); |
| 206 | const newText = lines.filter(l => l.kind !== 'del').map(l => l.text).join('\n'); |
| 207 | const oldHl = highlight(oldText, lang).split('\n'); |
| 208 | const newHl = highlight(newText, lang).split('\n'); |
| 209 | |
| 210 | let oldI = 0, newI = 0; |
| 211 | const rows = lines.map(l => { |
| 212 | let hlCode: string; |
| 213 | let rowCls: string; |
| 214 | let sign: string; |
| 215 | if (l.kind === 'del') { |
| 216 | hlCode = oldHl[oldI++] ?? esc(l.text); |
| 217 | rowCls = 'df3-dl-del'; sign = '−'; |
| 218 | } else if (l.kind === 'add') { |
| 219 | hlCode = newHl[newI++] ?? esc(l.text); |
| 220 | rowCls = 'df3-dl-add'; sign = '+'; |
| 221 | } else { |
| 222 | hlCode = oldHl[oldI++] ?? esc(l.text); newI++; |
| 223 | rowCls = 'df3-dl-ctx'; sign = ' '; |
| 224 | } |
| 225 | const ln = l.kind === 'del' ? l.oldN : l.kind === 'add' ? l.newN : l.oldN; |
| 226 | return `<tr class="${rowCls}"><td class="df3-ln-sign">${sign}</td><td class="df3-ln-num">${ln}</td><td class="df3-ln-code"><span>${hlCode}</span></td></tr>`; |
| 227 | }); |
| 228 | return `<table class="df3-table hljs"><tbody>${rows.join('')}</tbody></table>`; |
| 229 | } |
| 230 | |
| 231 | /** Collapse context lines — show only ±5 lines around changes. */ |
| 232 | function collapseContext(lines: DiffLine[], ctx = 5): DiffLine[] { |
| 233 | const changed = new Set<number>(); |
| 234 | lines.forEach((l, i) => { if (l.kind !== 'ctx') { for (let j = Math.max(0, i - ctx); j <= Math.min(lines.length - 1, i + ctx); j++) changed.add(j); } }); |
| 235 | const result: DiffLine[] = []; |
| 236 | let skipped = 0; |
| 237 | lines.forEach((l, i) => { |
| 238 | if (changed.has(i)) { |
| 239 | if (skipped > 0) { |
| 240 | result.push({ kind: 'ctx', text: `⋯ ${skipped} unchanged lines`, oldN: 0, newN: 0 }); |
| 241 | skipped = 0; |
| 242 | } |
| 243 | result.push(l); |
| 244 | } else { |
| 245 | skipped++; |
| 246 | } |
| 247 | }); |
| 248 | if (skipped > 0) result.push({ kind: 'ctx', text: `⋯ ${skipped} unchanged lines`, oldN: 0, newN: 0 }); |
| 249 | return result; |
| 250 | } |
| 251 | |
| 252 | // ── Fetch file content ──────────────────────────────────────────────────────── |
| 253 | |
| 254 | async function fetchFile(owner: string, repoSlug: string, ref: string, path: string): Promise<string | null> { |
| 255 | try { |
| 256 | const resp = await fetch(`/${owner}/${repoSlug}/raw/${ref}/${path}`); |
| 257 | if (!resp.ok) return null; |
| 258 | return await resp.text(); |
| 259 | } catch { |
| 260 | return null; |
| 261 | } |
| 262 | } |
| 263 | |
| 264 | // ── Symbol changes sidebar ──────────────────────────────────────────────────── |
| 265 | |
| 266 | function renderSymbolChanges(fileOp: FileOp): string { |
| 267 | if (!fileOp.child_ops?.length) return ''; |
| 268 | |
| 269 | const rows = fileOp.child_ops.map(sym => { |
| 270 | const addr = sym.address.includes('::') ? sym.address.split('::').slice(1).join('::') : sym.address; |
| 271 | const isChild = addr.includes('.'); |
| 272 | const label = isChild ? addr.split('.').pop()! : addr; |
| 273 | const indent = isChild ? 'df3-sym-child' : 'df3-sym-top'; |
| 274 | const desc = sym.content_summary |
| 275 | ? sym.content_summary.replace(/^(added |modified |removed )/, '') |
| 276 | : ''; |
| 277 | return ` |
| 278 | <div class="df3-sym-row ${indent}"> |
| 279 | ${isChild ? '<span class="df3-sym-indent"><svg width="8" height="8" aria-hidden="true"><use href="#icon-corner-right-down"></use></svg></span>' : ''} |
| 280 | <span class="df3-sym-dot ${opClass(sym.op)}">${opSign(sym.op)}</span> |
| 281 | <span class="df3-sym-name">${esc(label)}</span> |
| 282 | ${kindChip(sym.content_summary || '')} |
| 283 | ${desc ? `<span class="df3-sym-desc">${esc(desc)}</span>` : ''} |
| 284 | </div>`; |
| 285 | }).join(''); |
| 286 | |
| 287 | const total = fileOp.child_ops.length; |
| 288 | return ` |
| 289 | <div class="df3-sym-panel"> |
| 290 | <div class="df3-sym-hd"> |
| 291 | <svg width="11" height="11" style="color:var(--color-accent)" aria-hidden="true"><use href="#icon-code"></use></svg> |
| 292 | <span class="df3-sym-title">Symbols</span> |
| 293 | <span class="df3-sym-count">${total} change${total !== 1 ? 's' : ''}</span> |
| 294 | </div> |
| 295 | <div class="df3-sym-body">${rows}</div> |
| 296 | </div>`; |
| 297 | } |
| 298 | |
| 299 | // ── File card ───────────────────────────────────────────────────────────────── |
| 300 | |
| 301 | async function renderFileCard( |
| 302 | path: string, |
| 303 | fileType: 'added' | 'modified' | 'removed', |
| 304 | pd: PageData, |
| 305 | fileOp: FileOp | null, |
| 306 | ): Promise<string> { |
| 307 | const lang = extToLang(path); |
| 308 | const ext = path.split('.').pop()?.toLowerCase() ?? ''; |
| 309 | const fname = path.split('/').pop() ?? path; |
| 310 | const opLabel = fileType === 'added' ? 'added' : fileType === 'removed' ? 'removed' : 'modified'; |
| 311 | const opDotCls = fileType === 'added' ? 'df3-op-add' : fileType === 'removed' ? 'df3-op-del' : 'df3-op-mod'; |
| 312 | const opSign2 = fileType === 'added' ? '+' : fileType === 'removed' ? '−' : '~'; |
| 313 | const blobUrl = `${pd.baseUrl}/blob/${pd.shortId}/${path}`; |
| 314 | const rawUrl = `/${pd.owner}/${pd.repoSlug}/raw/${pd.commitId}/${path}`; |
| 315 | |
| 316 | // Fetch file content |
| 317 | let tableHtml = ''; |
| 318 | let lineStats = ''; |
| 319 | |
| 320 | if (fileType === 'added') { |
| 321 | const text = await fetchFile(pd.owner, pd.repoSlug, pd.commitId, path); |
| 322 | if (text !== null) { |
| 323 | const lines = text.split('\n'); |
| 324 | const hl = highlight(text, lang).split('\n'); |
| 325 | const rows = lines.map((_, i) => |
| 326 | `<tr class="df3-dl-add"><td class="df3-ln-sign">+</td><td class="df3-ln-num">${i + 1}</td><td class="df3-ln-code"><span>${hl[i] ?? ''}</span></td></tr>` |
| 327 | ).join(''); |
| 328 | tableHtml = `<table class="df3-table hljs"><tbody>${rows}</tbody></table>`; |
| 329 | lineStats = `+${lines.length} lines`; |
| 330 | } |
| 331 | } else if (fileType === 'removed') { |
| 332 | const ref = pd.parentId ?? pd.commitId; |
| 333 | const text = await fetchFile(pd.owner, pd.repoSlug, ref, path); |
| 334 | if (text !== null) { |
| 335 | const lines = text.split('\n'); |
| 336 | const hl = highlight(text, lang).split('\n'); |
| 337 | const rows = lines.map((_, i) => |
| 338 | `<tr class="df3-dl-del"><td class="df3-ln-sign">−</td><td class="df3-ln-num">${i + 1}</td><td class="df3-ln-code"><span>${hl[i] ?? ''}</span></td></tr>` |
| 339 | ).join(''); |
| 340 | tableHtml = `<table class="df3-table hljs"><tbody>${rows}</tbody></table>`; |
| 341 | lineStats = `−${lines.length} lines`; |
| 342 | } |
| 343 | } else { |
| 344 | // Modified: compute real line diff |
| 345 | const [oldText, newText] = await Promise.all([ |
| 346 | fetchFile(pd.owner, pd.repoSlug, pd.parentId ?? pd.commitId, path), |
| 347 | fetchFile(pd.owner, pd.repoSlug, pd.commitId, path), |
| 348 | ]); |
| 349 | if (oldText !== null && newText !== null) { |
| 350 | const oldLines = oldText.split('\n'); |
| 351 | const newLines = newText.split('\n'); |
| 352 | const rawDiff = diffLines(oldLines, newLines); |
| 353 | const collapsed = collapseContext(rawDiff); |
| 354 | const added = rawDiff.filter(l => l.kind === 'add').length; |
| 355 | const removed = rawDiff.filter(l => l.kind === 'del').length; |
| 356 | lineStats = `<span class="df3-stat-add">+${added}</span> <span class="df3-stat-del">−${removed}</span>`; |
| 357 | tableHtml = renderDiffTable(collapsed, lang); |
| 358 | } |
| 359 | } |
| 360 | |
| 361 | const symHtml = fileOp ? renderSymbolChanges(fileOp) : ''; |
| 362 | |
| 363 | return ` |
| 364 | <div class="df3-file-card df3-file-${fileType}" id="df3-file-${CSS.escape(path)}"> |
| 365 | <div class="df3-file-hd"> |
| 366 | <span class="df3-op-dot ${opDotCls}">${opSign2}</span> |
| 367 | <a href="${blobUrl}" class="df3-file-path">${esc(path)}</a> |
| 368 | ${ext ? `<span class="df3-ext">.${esc(ext)}</span>` : ''} |
| 369 | <div class="df3-file-hd-right"> |
| 370 | ${lineStats ? `<span class="df3-line-stat">${lineStats}</span>` : ''} |
| 371 | <a href="${rawUrl}" class="df3-raw-link" target="_blank" rel="noopener">Raw ↗</a> |
| 372 | </div> |
| 373 | </div> |
| 374 | ${symHtml} |
| 375 | ${tableHtml ? `<div class="df3-code-wrap">${tableHtml}</div>` : '<p class="df3-no-content">Could not load file content.</p>'} |
| 376 | </div>`; |
| 377 | } |
| 378 | |
| 379 | // ── Stats bar ───────────────────────────────────────────────────────────────── |
| 380 | |
| 381 | function renderStatsBar(pd: PageData): string { |
| 382 | const { snapshotDiff: sd, structuredDelta: delta } = pd; |
| 383 | |
| 384 | let symAdded = 0, symModified = 0, symRemoved = 0; |
| 385 | if (delta) { |
| 386 | for (const fop of delta.ops ?? []) { |
| 387 | for (const cop of fop.child_ops ?? []) { |
| 388 | if (cop.op === 'insert') symAdded++; |
| 389 | else if (cop.op === 'delete') symRemoved++; |
| 390 | else symModified++; |
| 391 | } |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | const filesChanged = sd.added.length + sd.modified.length + sd.removed.length; |
| 396 | const isRoot = !pd.parentId; |
| 397 | |
| 398 | const stats: string[] = []; |
| 399 | if (symAdded) stats.push(`<div class="df3-stat df3-stat-add"><div class="df3-stat-n">+${symAdded}</div><div class="df3-stat-l">symbol${symAdded !== 1 ? 's' : ''} added</div></div>`); |
| 400 | if (symModified) stats.push(`<div class="df3-stat df3-stat-mod"><div class="df3-stat-n">~${symModified}</div><div class="df3-stat-l">symbol${symModified !== 1 ? 's' : ''} modified</div></div>`); |
| 401 | if (symRemoved) stats.push(`<div class="df3-stat df3-stat-del"><div class="df3-stat-n">−${symRemoved}</div><div class="df3-stat-l">symbol${symRemoved !== 1 ? 's' : ''} removed</div></div>`); |
| 402 | if (filesChanged) stats.push(`<div class="df3-stat df3-stat-files"><div class="df3-stat-n">${filesChanged}</div><div class="df3-stat-l">file${filesChanged !== 1 ? 's' : ''} changed</div></div>`); |
| 403 | if (sd.total_files) stats.push(`<div class="df3-stat df3-stat-snap"><div class="df3-stat-n">${sd.total_files}</div><div class="df3-stat-l">in snapshot</div></div>`); |
| 404 | if (symAdded + symModified + symRemoved > 0) stats.push(`<div class="df3-stat df3-stat-clean"><div class="df3-stat-n">0</div><div class="df3-stat-l">dead code</div></div>`); |
| 405 | |
| 406 | const parentHtml = isRoot |
| 407 | ? `<span class="df3-root-pill">root commit</span>` |
| 408 | : `<span class="df3-vs">vs parent <a href="${pd.baseUrl}/commits/${pd.parentId}" class="df3-parent-sha">${(pd.parentId ?? '').slice(0, 8)}</a></span>`; |
| 409 | |
| 410 | return ` |
| 411 | <div class="df3-stats-bar"> |
| 412 | <div class="df3-stats-row">${stats.join('')}</div> |
| 413 | <div class="df3-stats-meta">${parentHtml}</div> |
| 414 | </div>`; |
| 415 | } |
| 416 | |
| 417 | // ── Entry point ─────────────────────────────────────────────────────────────── |
| 418 | |
| 419 | export async function initDiff(): Promise<void> { |
| 420 | const el = document.getElementById('page-data'); |
| 421 | if (!el) return; |
| 422 | let pd: PageData; |
| 423 | try { pd = JSON.parse(el.textContent ?? '{}') as PageData; } catch { return; } |
| 424 | if (pd.page !== 'diff') return; |
| 425 | |
| 426 | const container = document.getElementById('df3-content'); |
| 427 | if (!container) return; |
| 428 | |
| 429 | const { snapshotDiff: sd, structuredDelta: delta } = pd; |
| 430 | const allFiles: Array<[string, 'added' | 'modified' | 'removed']> = [ |
| 431 | ...sd.added.map(p => [p, 'added'] as [string, 'added']), |
| 432 | ...sd.modified.map(p => [p, 'modified'] as [string, 'modified']), |
| 433 | ...sd.removed.map(p => [p, 'removed'] as [string, 'removed']), |
| 434 | ]; |
| 435 | |
| 436 | if (allFiles.length === 0) { |
| 437 | container.innerHTML = `<p class="df3-empty">No file changes in this commit.</p>`; |
| 438 | return; |
| 439 | } |
| 440 | |
| 441 | // Build lookup: file path → FileOp from structured delta |
| 442 | const deltaByFile = new Map<string, FileOp>(); |
| 443 | for (const fop of delta?.ops ?? []) { |
| 444 | deltaByFile.set(fop.address, fop); |
| 445 | } |
| 446 | |
| 447 | // Render stats bar immediately |
| 448 | container.innerHTML = renderStatsBar(pd) + |
| 449 | `<div id="df3-files" class="df3-files"><div class="df3-loading"><span class="spinner"></span> Loading file diffs…</div></div>`; |
| 450 | |
| 451 | // Render file cards async (in parallel, then stitch in order) |
| 452 | const filesDiv = document.getElementById('df3-files'); |
| 453 | if (!filesDiv) return; |
| 454 | |
| 455 | const cards = await Promise.all( |
| 456 | allFiles.map(([path, ft]) => renderFileCard(path, ft, pd, deltaByFile.get(path) ?? null)) |
| 457 | ); |
| 458 | |
| 459 | filesDiv.innerHTML = cards.join(''); |
| 460 | } |
File History
1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef
debug(push/stream): instrument O-frame decode path with INF…
Sonnet 4.6
patch
121 days ago