Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(diff): Forge-aware labels in version diff panel (#394)

scott a7b376e5 9ac6ad9b

+120 -4
+17
src/css/app.css
··· 8028 8028 color: var(--color-danger); 8029 8029 } 8030 8030 8031 + .diff-stat-forge { 8032 + margin-left: auto; 8033 + padding: 1px 6px; 8034 + border-radius: 3px; 8035 + background: oklch(0.92 0.04 250); 8036 + color: oklch(0.45 0.1 250); 8037 + font-size: 0.6875rem; 8038 + font-weight: 600; 8039 + text-transform: uppercase; 8040 + letter-spacing: 0.04em; 8041 + } 8042 + 8043 + .diff-option--forge { 8044 + color: oklch(0.45 0.1 250); 8045 + font-weight: 500; 8046 + } 8047 + 8031 8048 .diff-panel-body { 8032 8049 flex: 1; 8033 8050 overflow-y: auto;
+36 -4
src/docs/diff-panel.ts
··· 37 37 metadata: Record<string, unknown> | null; 38 38 } 39 39 40 + /** 41 + * Build a dropdown label for a version. 42 + * 43 + * Forge-authored versions show their `label` metadata (e.g. "Initial plan", 44 + * "Step 2 complete") prefixed with a [forge] tag so the timeline stays 45 + * readable when diffing machine-generated revisions. Human versions keep 46 + * the legacy `name (time)` or `time — author` format. 47 + */ 48 + export function formatDiffOption(meta: Record<string, unknown>, createdAt: string): string { 49 + const timeStr = formatRelativeTime(createdAt); 50 + const author = typeof meta['author'] === 'string' ? meta['author'] : 'Unknown'; 51 + const name = typeof meta['name'] === 'string' ? meta['name'] : null; 52 + const label = typeof meta['label'] === 'string' ? meta['label'] : null; 53 + 54 + if (author === 'forge') { 55 + const forgeLabel = name || label || 'revision'; 56 + return `[forge] ${forgeLabel} (${timeStr})`; 57 + } 58 + return name ? `${name} (${timeStr})` : `${timeStr} — ${author}`; 59 + } 60 + 61 + /** Returns true if a version was authored by Forge. */ 62 + export function isForgeAuthored(meta: Record<string, unknown>): boolean { 63 + return meta['author'] === 'forge'; 64 + } 65 + 40 66 // --- Factory --- 41 67 42 68 export function createDiffPanel(config: DiffPanelConfig): DiffPanel { ··· 140 166 141 167 for (const v of versions) { 142 168 const meta = (v.metadata && typeof v.metadata === 'object') ? v.metadata : {}; 143 - const timeStr = formatRelativeTime(v.created_at); 144 - const author = (typeof meta['author'] === 'string' ? meta['author'] : 'Unknown'); 145 - const name = (typeof meta['name'] === 'string' ? meta['name'] : null); 146 - const label = name ? `${name} (${timeStr})` : `${timeStr} — ${author}`; 169 + const label = formatDiffOption(meta, v.created_at); 170 + const forgeClass = isForgeAuthored(meta) ? 'diff-option--forge' : ''; 147 171 148 172 const optA = document.createElement('option'); 149 173 optA.value = v.id; 150 174 optA.textContent = label; 175 + if (forgeClass) optA.className = forgeClass; 151 176 selectA.appendChild(optA); 152 177 153 178 const optB = document.createElement('option'); 154 179 optB.value = v.id; 155 180 optB.textContent = label; 181 + if (forgeClass) optB.className = forgeClass; 156 182 selectB.appendChild(optB); 157 183 } 158 184 ··· 246 272 bodyEl.innerHTML = '<div class="diff-panel-empty">No differences found</div>'; 247 273 } else { 248 274 statsEl.style.display = ''; 275 + const metaA = (versions.find(v => v.id === idA)?.metadata ?? {}) as Record<string, unknown>; 276 + const metaB = (versions.find(v => v.id === idB)?.metadata ?? {}) as Record<string, unknown>; 277 + const forgeBadge = isForgeAuthored(metaA) && isForgeAuthored(metaB) 278 + ? '<span class="diff-stat-forge" title="Both versions authored by Forge">forge</span>' 279 + : ''; 249 280 statsEl.innerHTML = ` 250 281 <span class="diff-stat-add">+${inserts} word${inserts !== 1 ? 's' : ''}</span> 251 282 <span class="diff-stat-del">&minus;${deletes} word${deletes !== 1 ? 's' : ''}</span> 283 + ${forgeBadge} 252 284 `; 253 285 254 286 const diffHtml = renderDiffHtml(blocks);
+67
tests/diff-panel.test.ts
··· 1 + import { describe, it, expect } from 'vitest'; 2 + 3 + /** 4 + * Tests for the Diff Panel pure helpers. 5 + * 6 + * The `createDiffPanel` factory is DOM-heavy and exercised via e2e tests. 7 + * Here we only cover the stringly helpers that decide how each version 8 + * option is rendered — particularly the Forge-aware branch. 9 + */ 10 + 11 + import { formatDiffOption, isForgeAuthored } from '../src/docs/diff-panel.js'; 12 + 13 + describe('formatDiffOption', () => { 14 + const recent = new Date(Date.now() - 5 * 60 * 1000).toISOString(); 15 + 16 + it('tags forge-authored versions with [forge] prefix', () => { 17 + const out = formatDiffOption({ author: 'forge', label: 'Initial plan' }, recent); 18 + expect(out.startsWith('[forge]')).toBe(true); 19 + expect(out).toContain('Initial plan'); 20 + expect(out).toContain('5 min ago'); 21 + }); 22 + 23 + it('prefers name over label when both present', () => { 24 + const out = formatDiffOption({ author: 'forge', name: 'Milestone 1', label: 'raw' }, recent); 25 + expect(out).toContain('Milestone 1'); 26 + expect(out).not.toContain('raw'); 27 + }); 28 + 29 + it('falls back to "revision" when forge metadata has no label', () => { 30 + const out = formatDiffOption({ author: 'forge' }, recent); 31 + expect(out).toContain('[forge] revision'); 32 + }); 33 + 34 + it('uses legacy "time — author" format for human versions', () => { 35 + const out = formatDiffOption({ author: 'scott' }, recent); 36 + expect(out).toBe('5 min ago — scott'); 37 + expect(out.startsWith('[forge]')).toBe(false); 38 + }); 39 + 40 + it('uses "name (time)" when a human version has a name', () => { 41 + const out = formatDiffOption({ author: 'scott', name: 'Before rewrite' }, recent); 42 + expect(out).toBe('Before rewrite (5 min ago)'); 43 + }); 44 + 45 + it('defaults author to Unknown when missing', () => { 46 + const out = formatDiffOption({}, recent); 47 + expect(out).toContain('Unknown'); 48 + }); 49 + }); 50 + 51 + describe('isForgeAuthored', () => { 52 + it('returns true for forge author', () => { 53 + expect(isForgeAuthored({ author: 'forge' })).toBe(true); 54 + }); 55 + 56 + it('returns false for human authors', () => { 57 + expect(isForgeAuthored({ author: 'scott' })).toBe(false); 58 + }); 59 + 60 + it('returns false when author is missing', () => { 61 + expect(isForgeAuthored({})).toBe(false); 62 + }); 63 + 64 + it('returns false for non-string author values', () => { 65 + expect(isForgeAuthored({ author: 42 as unknown })).toBe(false); 66 + }); 67 + });