@@ -8,8 +8,20 @@ const MARKER = '<!-- nitpix-report -->';
88/**
99 * Build the PR comment / step summary markdown.
1010 *
11- * urls: { baseline(name), snapshot(name), diff(name) } returning a raw image
12- * URL or null (e.g. fork PRs where we couldn't push pending images).
11+ * urls: { baseline(name), snapshot(name), diff(name), thumb(kind, name) }
12+ * returning a raw image URL, or null helpers when images couldn't be pushed
13+ * (e.g. fork PRs without the workflow_run topology). thumb() returns null
14+ * when thumbnails are disabled — full images are embedded instead.
15+ *
16+ * Comment-weight strategy (GitHub loads every <img> in a comment at once,
17+ * even inside closed <details>, and `width=` doesn't reduce the download):
18+ * - inline images are small thumbnails hyperlinked to the full-size files
19+ * - snapshots are ordered by severity and only `maxImages` inline images are
20+ * embedded (changed rows cost 3, added rows cost 1); the rest degrade to
21+ * plain baseline/new/diff links
22+ * - with `inlineImages: false` the whole report is links-only; main.js posts
23+ * that version first so notification emails never embed images, then
24+ * immediately edits in the full version (GitHub only emails on creation)
1325 */
1426function buildReport ( {
1527 results,
@@ -21,13 +33,16 @@ function buildReport({
2133 maxImages,
2234 commandPrefix,
2335 runUrl,
36+ inlineImages = true ,
2437} ) {
2538 const counts = countByStatus ( results ) ;
26- const changed = results . filter ( ( r ) => r . status === 'changed' ) ;
39+ const bySeverity = ( a , b ) =>
40+ ( b . layoutDiff ? 100 : b . diffPercentage || 0 ) - ( a . layoutDiff ? 100 : a . diffPercentage || 0 ) ;
41+ const changed = results . filter ( ( r ) => r . status === 'changed' ) . sort ( bySeverity ) ;
2742 const added = results . filter ( ( r ) => r . status === 'added' ) ;
2843 const missing = results . filter ( ( r ) => r . status === 'missing' ) ;
2944
30- const lines = [ '<!-- nitpix-report -->' , '## 👁 nitpix visual review' , '' ] ;
45+ const lines = [ MARKER , '## 👁 nitpix visual review' , '' ] ;
3146
3247 const needsReview = unapproved . length > 0 ;
3348 const verdict = needsReview
@@ -43,7 +58,23 @@ function buildReport({
4358 ) ;
4459
4560 let imagesUsed = 0 ;
46- const imageBudgetExceeded = ( ) => imagesUsed >= maxImages ;
61+ const canEmbed = ( cost ) => inlineImages && urls . baseline && imagesUsed + cost <= maxImages ;
62+
63+ // thumbnail-if-available <img> wrapped in a link to the full image
64+ const img = ( kind , name ) => {
65+ const full = urls [ kind ] ( name ) ;
66+ const thumb = urls . thumb && urls . thumb ( kind , name ) ;
67+ return `<a href="${ full } "><img src="${ thumb || full } " width="260"></a>` ;
68+ } ;
69+ const linkRow = ( r ) => {
70+ if ( ! urls . baseline ) return '_images unavailable (no write access to the baseline branch)_' ;
71+ const parts = [ `[new](${ urls . snapshot ( r . name ) } )` ] ;
72+ if ( r . status === 'changed' ) {
73+ parts . unshift ( `[baseline](${ urls . baseline ( r . name ) } )` ) ;
74+ if ( ! r . layoutDiff ) parts . push ( `[diff](${ urls . diff ( r . name ) } )` ) ;
75+ }
76+ return parts . join ( ' · ' ) ;
77+ } ;
4778
4879 for ( const r of changed ) {
4980 const approved = approvedNames . has ( r . name ) ? ' — ✅ approved' : '' ;
@@ -53,32 +84,24 @@ function buildReport({
5384 ? `${ r . diffPercentage . toFixed ( 2 ) } % diff`
5485 : 'changed' ;
5586 lines . push ( `<details><summary>🔄 <code>${ escapeHtml ( r . name ) } </code> (${ pct } )${ approved } </summary>` , '' ) ;
56- if ( urls . baseline && ! imageBudgetExceeded ( ) ) {
57- imagesUsed ++ ;
58- const cells = [
59- `<img src="${ urls . baseline ( r . name ) } " width="260">` ,
60- `<img src="${ urls . snapshot ( r . name ) } " width="260">` ,
61- r . layoutDiff ? '—' : `<img src="${ urls . diff ( r . name ) } " width="260">` ,
62- ] ;
87+ if ( canEmbed ( 3 ) ) {
88+ imagesUsed += 3 ;
89+ const cells = [ img ( 'baseline' , r . name ) , img ( 'snapshot' , r . name ) , r . layoutDiff ? '—' : img ( 'diff' , r . name ) ] ;
6390 lines . push ( '| baseline | new | diff |' , '| --- | --- | --- |' , `| ${ cells . join ( ' | ' ) } |` ) ;
64- } else if ( ! urls . baseline ) {
65- lines . push ( '_images unavailable (no write access to the baseline branch — see the report artifact)_' ) ;
6691 } else {
67- lines . push ( '_image budget exceeded — see the run artifacts for this diff_' ) ;
92+ lines . push ( linkRow ( r ) ) ;
6893 }
6994 lines . push ( '' , '</details>' , '' ) ;
7095 }
7196
7297 for ( const r of added ) {
7398 const approved = approvedNames . has ( r . name ) ? ' — ✅ approved' : '' ;
7499 lines . push ( `<details><summary>🆕 <code>${ escapeHtml ( r . name ) } </code> (new snapshot)${ approved } </summary>` , '' ) ;
75- if ( urls . snapshot && ! imageBudgetExceeded ( ) ) {
76- imagesUsed ++ ;
77- lines . push ( `<img src="${ urls . snapshot ( r . name ) } " width="260">` ) ;
78- } else if ( ! urls . snapshot ) {
79- lines . push ( '_image unavailable (no write access to the baseline branch)_' ) ;
100+ if ( canEmbed ( 1 ) ) {
101+ imagesUsed += 1 ;
102+ lines . push ( img ( 'snapshot' , r . name ) ) ;
80103 } else {
81- lines . push ( '_image budget exceeded — see the run artifacts_' ) ;
104+ lines . push ( linkRow ( r ) ) ;
82105 }
83106 lines . push ( '' , '</details>' , '' ) ;
84107 }
@@ -94,6 +117,10 @@ function buildReport({
94117 ) ;
95118 }
96119
120+ if ( inlineImages && urls . baseline && imagesUsed >= maxImages && changed . length + added . length > 0 ) {
121+ lines . push ( `_Inline image budget (${ maxImages } ) reached — remaining snapshots are linked above._` , '' ) ;
122+ }
123+
97124 if ( needsReview ) {
98125 lines . push ( `To approve these changes, comment \`${ commandPrefix } approve\` (write access required).` ) ;
99126 }
@@ -105,16 +132,22 @@ function escapeHtml(s) {
105132 return s . replace ( / & / g, '&' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) ;
106133}
107134
108- /** Build url helpers pointing at a pinned commit on the baseline branch. */
109- function rawUrls ( { repo, commitSha, baseRef, pendingDir } ) {
110- if ( ! commitSha ) return { baseline : null , snapshot : null , diff : null } ;
135+ /**
136+ * Build url helpers pointing at a pinned commit on the baseline branch.
137+ * `hasThumbs` controls whether thumb() resolves into pending/<...>/thumb/.
138+ */
139+ function rawUrls ( { repo, commitSha, baseRef, pendingDir, hasThumbs = false } ) {
140+ if ( ! commitSha ) return { baseline : null , snapshot : null , diff : null , thumb : null } ;
111141 const server = process . env . GITHUB_SERVER_URL || 'https://github.com' ;
112142 const rawHost = server === 'https://github.com' ? 'https://raw.githubusercontent.com' : `${ server } /raw` ;
113143 const base = `${ rawHost } /${ repo } /${ commitSha } ` ;
114144 return {
115145 baseline : ( name ) => `${ base } /baselines/${ encodePath ( baseRef ) } /${ encodePath ( name ) } ` ,
116146 snapshot : ( name ) => `${ base } /${ pendingDir } /new/${ encodePath ( name ) } ` ,
117147 diff : ( name ) => `${ base } /${ pendingDir } /diff/${ encodePath ( name ) } ` ,
148+ thumb : hasThumbs
149+ ? ( kind , name ) => `${ base } /${ pendingDir } /thumb/${ kind } /${ encodePath ( name ) } `
150+ : null ,
118151 } ;
119152}
120153
0 commit comments