// Artifact panel views: Board grid + Timeline list + Evolution chain. // // The compact L0 row (density-spec §2, artifacts.js) is the default List // view. This module adds two more projections of the same event stream: // // * Board — group by kind (md / html / svg / json / code / other), // each group a tile grid with thumbnails. Good when the // front-end task is producing several files and you want to // see "what's ready" at a glance rather than scroll rows. // * Timeline — chronological, one row per artifact-version tuple, so // you can watch "which file changed when" over the session. // // Plus the version-evolution chain: clicking a `.artifact-version` chip // on any List row expands an inline strip listing every version this // artifact has seen, with a per-hop diff pane. The chain is fed from // state.history[artifactId] which artifacts.js maintains as events arrive. // // Runtime constraint: the artifact server only serves the CURRENT file // contents (see src/main/artifact-server.js — no per-version blob store). // This module accepts an optional `blob` field on history entries so the // fixture can demo real diffs; when absent, the pre-latest hops render a // "content not preserved for older versions" placeholder — an honest // signal that the diff is fixture-tier until the runtime seam grows a // snapshot store (RFC follow-up). 'use strict' ;(function () { const KINDS = ['md', 'html', 'svg', 'json', 'code', 'other'] const KIND_LABEL = { md: 'Markdown', html: 'HTML', svg: 'SVG', json: 'JSON', code: 'Code', other: 'Other', } // Mirrors artifacts.js's ICON_SVG shape — inline stroke icons keep the // Board tiles consistent with the row icons on the List view. const KIND_ICON = { md: '', html: '', svg: '', json: '', code: '', other: '', } // Normalize the free-form `kind` value each event carries into one of // the six Board buckets — HTML from real artifacts sometimes arrives as // 'html', 'htm', 'text/html'; keep the mapping generous. function bucketOf(kind) { if (!kind) return 'other' const k = String(kind).toLowerCase() if (k.includes('md') || k.includes('markdown')) return 'md' if (k.includes('svg')) return 'svg' if (k.includes('html') || k.includes('htm')) return 'html' if (k.includes('json')) return 'json' if (['js', 'ts', 'py', 'go', 'rs', 'sh', 'c', 'cpp', 'rb', 'java'].some((x) => k === x || k.includes(x))) return 'code' return 'other' } function groupByKind(entries) { const groups = new Map() for (const k of KINDS) groups.set(k, []) for (const e of entries) { const b = bucketOf(e.kind) groups.get(b).push(e) } // Drop empty buckets so the Board doesn't render empty group headers // when a session has only produced markdown, etc. for (const k of KINDS) { if (groups.get(k).length === 0) groups.delete(k) } return groups } function firstLine(text) { if (!text) return '' const nl = text.indexOf('\n') return (nl < 0 ? text : text.slice(0, nl)).trim() } // Build a small text preview for the tile: MD → first non-blank line; // JSON → first two keys; code → first two non-blank lines; SVG/HTML // fall back to the filename since a code preview isn't legible. function thumbnailPreview(entry) { const b = bucketOf(entry.kind) const blob = entry.blob || '' if (b === 'md') return firstLine(blob) || entry.artifactId if (b === 'json') { try { const parsed = JSON.parse(blob) const keys = Object.keys(parsed).slice(0, 3) return keys.length ? '{ ' + keys.join(', ') + ' }' : entry.artifactId } catch { return firstLine(blob) || entry.artifactId } } if (b === 'code') { const lines = blob.split('\n').filter((l) => l.trim()).slice(0, 3) return lines.join('\n') || entry.artifactId } if (b === 'html') return firstLine(blob.replace(/<[^>]+>/g, ' ')) || entry.artifactId return '' } // Board renderer: one
per kind // bucket, containing a grid of `.artifact-tile` cards. Clicking a tile // opens the artifact in the system browser (same seam as the L0 // `open ↗` link on the List view). function renderBoard(entries, opts) { const openArtifact = (opts && opts.openArtifact) || (() => {}) const el = document.createElement('div') el.className = 'artifact-board' const groups = groupByKind(entries) for (const [kind, group] of groups) { const section = document.createElement('section') section.className = 'artifact-board-group' section.dataset.kind = kind const head = document.createElement('header') head.className = 'artifact-board-group-head' const icon = document.createElement('span') icon.className = 'artifact-board-group-icon' icon.innerHTML = KIND_ICON[kind] || KIND_ICON.other const title = document.createElement('span') title.className = 'artifact-board-group-title' title.textContent = KIND_LABEL[kind] || kind const count = document.createElement('span') count.className = 'artifact-board-group-count' count.textContent = String(group.length) head.append(icon, title, count) section.append(head) const grid = document.createElement('div') grid.className = 'artifact-board-grid' for (const entry of group) { grid.appendChild(renderTile(entry, openArtifact)) } section.append(grid) el.append(section) } return el } function renderTile(entry, openArtifact) { // Board reuse decision (lane-artifact-inline, 2026-07-20): the List row // grew an expandable inline md/html preview, but the Board tile does NOT // reuse it. A tile is a single