Skip to content

Add diff review mode with before/after comparisons#4

Merged
ritikrishu merged 5 commits intomainfrom
features/diff-review-mode
Feb 16, 2026
Merged

Add diff review mode with before/after comparisons#4
ritikrishu merged 5 commits intomainfrom
features/diff-review-mode

Conversation

@ritikrishu
Copy link
Contributor

@ritikrishu ritikrishu commented Feb 16, 2026

Summary

  • Adds review-mode: 'diff' action input that shows only changed frames instead of rendering everything
  • Fetches base branch .pen files via GitHub API, sends both versions to the screenshot service's /api/v1/diff endpoint
  • Builds PR comments with side-by-side before/after HTML tables for modified frames, screenshots for added frames, strikethrough for removed frames, and collapsed <details> for unchanged frames
  • Fully backward compatible — default review-mode: 'full' behavior is unchanged

Depends on: Service-side PR RemoteState/pencil-screenshot-service#6 (diff endpoint)

Files changed

File Change
action.yml New review-mode input (default 'full')
src/types.ts ReviewMode, DiffResponse, diff comment data types
src/config.ts Read + validate review-mode
src/github/files.ts getFileContentAtRef(), readLocalFileAsBase64(), capture previousPath for renames
src/renderers/service.ts fetchDiff() method, widened pollForCompletion return type
src/comment-builder.ts buildDiffComment(), before/after HTML tables, collapsed unchanged
src/main.ts runDiffMode(), extracted runFullMode(), branching logic

Test plan

  • Trigger PR Test design update #3 with review-mode: 'diff' — verify modified files show before/after
  • Verify added files show all frame screenshots
  • Verify deleted files show deletion notice
  • Switch back to review-mode: 'full' — verify full mode still works unchanged

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a new "review mode" configuration option with "full" and "diff" modes.
    • Introduced diff mode rendering for pull request comments, displaying file and frame changes in a condensed diff format.
    • Enhanced service renderer to support diff computation alongside standard renders.

Introduces `review-mode: 'diff'` option that fetches base branch .pen files
via GitHub API, calls the screenshot service's /api/v1/diff endpoint, and
posts PR comments with side-by-side before/after screenshots for modified
frames, new screenshots for added frames, and strikethrough listings for
removed frames. Unchanged frames are collapsed in a details section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Warning

Rate limit exceeded

@ritikrishu has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 55 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This pull request introduces a "diff mode" review feature alongside the existing full render mode. Users can now select between reviewing complete design changes or comparing frame-level differences. The implementation adds configuration support, new diff-rendering logic in comment builders, file content fetching utilities, and orchestration changes to handle both rendering modes.

Changes

Cohort / File(s) Summary
Configuration & Input Setup
action.yml, src/config.ts
Added review-mode input parameter with 'full' default; config now reads and validates reviewMode against ['full', 'diff'].
Type Definitions
src/types.ts
Introduced ReviewMode type ('full' | 'diff'), extended PenFile with optional previousPath, and added comprehensive diff-related types (DiffFrameScreenshot, DiffModifiedFrame, DiffFrameInfo, DiffSummary, DiffResponse, DiffFileCommentData, DiffCommentData, DiffCommentSummary).
Diff Comment Building
src/comment-builder.ts
Added buildDiffComment and calculateDiffSummary exports; new logic renders diff-focused markdown with per-file sections, frame-level changes (added/modified/removed), and collapsible unchanged frames.
File Content Operations
src/github/files.ts
Added getFileContentAtRef for fetching file content at git refs via GitHub API, readLocalFileAsBase64 for local file encoding, and previousPath tracking in PenFile objects for diff comparisons.
Service Renderer Extension
src/renderers/service.ts
Added fetchDiff method for submitting and polling diff jobs; updated JobStatusResponse.result and pollForCompletion to use generic unknown type instead of ServiceResponse.
Main Workflow Orchestration
src/main.ts
Major refactor introducing dual-mode workflow: runFullMode renders all frames; runDiffMode compares base/head content via service renderer. Integrated diff-specific comment building, output handling, and fallback logic from diff to full mode.

Sequence Diagram

sequenceDiagram
    participant GH as GitHub API
    participant Main as Main Workflow
    participant ServiceR as Service Renderer
    participant CommentB as Comment Builder
    participant Output as PR Output

    alt Review Mode: Diff
        Main->>GH: getFileContentAtRef(base ref)
        GH-->>Main: base content (base64)
        
        Main->>GH: getChangedPenFiles(head)
        GH-->>Main: changed files
        
        Main->>Main: readLocalFileAsBase64(head file)
        Main-->>Main: head content (base64)
        
        Main->>ServiceR: fetchDiff(basePenBase64, headPenBase64)
        ServiceR->>ServiceR: submit diff job
        ServiceR->>ServiceR: poll for completion
        ServiceR-->>Main: DiffResponse
        
        Main->>CommentB: buildDiffComment(diffData)
        CommentB-->>Main: markdown comment
        
        Main->>Output: post comment + outputs
    else Review Mode: Full
        Main->>GH: getChangedPenFiles
        GH-->>Main: changed files
        
        Main->>ServiceR: renderFullFile(penContent)
        ServiceR->>ServiceR: fetch all frames
        ServiceR-->>Main: rendered frames
        
        Main->>CommentB: buildComment(fullData)
        CommentB-->>Main: markdown comment
        
        Main->>Output: post comment + outputs
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A diff mode hops into view,
Frame-by-frame comparisons, fresh and new,
Base meets head in the renderer's care,
Diffs and summaries floating through air,
Design reviews now have a choice so true!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add diff review mode with before/after comparisons' clearly and specifically describes the main change—the introduction of a new diff review mode with comparative rendering—which is the primary objective of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch features/diff-review-mode

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Add example-diff.yml showing the diff review mode configuration.
Modify Dashboard, Analytics, and Settings frames in random.pen to
demonstrate before/after comparison in diff mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (7)
src/github/files.ts (2)

125-162: Solid implementation with proper blob fallback for large files.

The two-tier approach (content API → blob API) handles GitHub's content truncation well. One minor robustness note: the 404 detection on line 157 uses (error as any).status — this works with Octokit's RequestError, but consider checking error more specifically if Octokit typing is available (e.g., importing RequestError from @octokit/request-error).


167-169: No error handling for missing files.

readLocalFileAsBase64 will throw an unhandled ENOENT if the file doesn't exist at the local path. The callers in main.ts do wrap this in try/catch, so it won't crash the action — but a more descriptive error message here would improve debuggability.

💡 Optional: wrap with a friendlier error
 export function readLocalFileAsBase64(filePath: string): string {
+  if (!fs.existsSync(filePath)) {
+    throw new Error(`Local file not found: ${filePath}`);
+  }
   const content = fs.readFileSync(filePath);
   return content.toString('base64');
 }
src/renderers/service.ts (1)

200-253: fetchDiff follows established patterns but inherits the unsafe-cast concern.

The method mirrors the existing fetchAllFrames flow (submit → poll → cast). Line 239 performs as DiffResponse on unvalidated JSON, which could produce confusing runtime errors if the service returns an unexpected shape. This is the same pattern as line 333 (as ServiceResponse), so it's not a regression, but worth noting.

Consider adding a lightweight runtime guard (e.g., checking result.type === 'diff' and result.summary) before returning, to fail fast with a clear message if the service contract changes.

💡 Suggested guard after polling
     // Poll for completion (reuses existing polling logic)
-    const result = await this.pollForCompletion(submitData.jobId) as DiffResponse;
+    const raw = await this.pollForCompletion(submitData.jobId);
+    const result = raw as DiffResponse;
+    if (!result || result.type !== 'diff' || !result.summary) {
+      throw new Error(`Unexpected diff response shape from job ${submitData.jobId}`);
+    }
src/comment-builder.ts (2)

362-377: Image URLs in HTML are not escaped.

mod.base.imageUrl and mod.head.imageUrl are interpolated directly into <img src="..."> without HTML-attribute escaping. If the service ever returns a URL with " or other special characters, it could break the HTML. GitHub does sanitize PR comment HTML, so this is not exploitable in practice, but escaping quotes would be more robust.


413-442: Frame counting for added files may double-count if types aren't mutually exclusive.

Lines 424-429 aggregate from file.diff.summary and lines 432-434 aggregate from file.frames for added files. If a DiffFileCommentData ever has both diff and frames populated, totalAddedFrames would be inflated. Current callers in main.ts never set both — but the types allow it. Consider adding a guard or a comment documenting the mutual exclusivity.

src/main.ts (2)

113-165: runFullMode duplicates the rendering logic already extracted into renderFullFile.

The per-file rendering in runFullMode (lines 113-151) does the same document-load → frame-slice → render-loop → push-results as renderFullFile (lines 324-342). Consider having runFullMode delegate to renderFullFile to reduce duplication. The only additions runFullMode needs on top are the inline totalFramesRendered counter and the log line — both easily handled after the call.

♻️ Sketch of deduplication
       try {
-        // Parse the .pen file to get top-level frames (screens/artboards)
-        const document = await loadPenDocument(file.path);
-        const frames = getTopLevelFrames(document);
-
-        // Limit frames if configured
-        const framesToProcess =
-          inputs.maxFramesPerFile > 0
-            ? frames.slice(0, inputs.maxFramesPerFile)
-            : frames;
-
-        core.info(`Found ${frames.length} top-level frames, processing ${framesToProcess.length}`);
-
-        // Render each frame
-        const frameResults: FrameCommentData[] = [];
-
-        for (const frame of framesToProcess) {
-          const outputPath = getOutputPath(inputs.outputDir, file.path, frame, inputs.imageFormat);
-
-          const result = await renderer.renderFrame(file.path, frame, outputPath);
-
-          frameResults.push({
-            id: frame.id,
-            name: frame.name,
-            screenshotUrl: result.imageUrl,
-            screenshotPath: result.success ? result.screenshotPath : undefined,
-            error: result.error,
-          });
-
-          if (result.success) {
-            totalFramesRendered++;
-          }
-        }
+        const frameResults = await renderFullFile(renderer, file, inputs);
+        totalFramesRendered += frameResults.filter(f => !f.error).length;
 
         fileResults.push({
           path: file.path,

Also applies to: 319-343


222-222: Unsafe as ServiceRenderer cast — safe at runtime but fragile.

This cast is guarded by the inputs.renderer === 'service' check on line 64, so it's correct today. But if initializeRenderer ever returns a different BaseRenderer subtype for the service path, this would silently break. A narrowing check would be more resilient.

💡 Optional: runtime narrowing
-  const renderer = await initializeRenderer(inputs) as ServiceRenderer;
+  const renderer = await initializeRenderer(inputs);
+  if (!(renderer instanceof ServiceRenderer)) {
+    throw new Error('Diff mode requires a ServiceRenderer');
+  }

@github-actions
Copy link

github-actions bot commented Feb 16, 2026

🎨 Design Review

📁 1 design file (1 modified)
🖼️ 9 frames detected
9 failed to render

📝 test-fixtures/designs/random.pen (modified)

Frame ID Status
1. Dashboard J1VeO ❌ Screenshot service returned 429: {"error":"Monthly screenshot limit exceeded for RemoteState/pencil-actions","code":"USAGE_LIMIT_EXCEEDED","usage":{"current":101,"limit":100,"remaining":0}}
2. Interviews iVES0 ❌ Frame iVES0 not found in API response
3. Candidates x7cUJ ❌ Frame x7cUJ not found in API response
4. Questions Bank Mvlj1 ❌ Frame Mvlj1 not found in API response
5. AI Settings lwglP ❌ Frame lwglP not found in API response
6. Analytics pdVTW ❌ Frame pdVTW not found in API response
7. Job Positions pCH9A ❌ Frame pCH9A not found in API response
8. Settings 7b2O7 ❌ Frame 7b2O7 not found in API response
Components 0KJfI ❌ Frame 0KJfI not found in API response

Generated by Pencil Design Review for commit c722298

ritikrishu and others added 2 commits February 16, 2026 02:37
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New `comment-id` input namespaces the HTML comment marker so multiple
  workflows can post separate comments without overwriting each other
- Change default review-mode from 'full' to 'diff'
- Update all example workflows to use `./` and distinct comment-ids
  (basic, visual, diff, test) so PR #4 shows all modes side by side

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Feb 16, 2026

🎨 Design Review

📁 1 design file (1 modified)
🖼️ 9 frames detected
9 rendered successfully

📝 test-fixtures/designs/random.pen (modified)

1. Dashboard

Screenshot saved to: .pencil-screenshots/random/1-dashboard-J1VeO.json

2. Interviews

Screenshot saved to: .pencil-screenshots/random/2-interviews-iVES0.json

3. Candidates

Screenshot saved to: .pencil-screenshots/random/3-candidates-x7cUJ.json

4. Questions Bank

Screenshot saved to: .pencil-screenshots/random/4-questions-bank-Mvlj1.json

5. AI Settings

Screenshot saved to: .pencil-screenshots/random/5-ai-settings-lwglP.json

6. Analytics

Screenshot saved to: .pencil-screenshots/random/6-analytics-pdVTW.json

7. Job Positions

Screenshot saved to: .pencil-screenshots/random/7-job-positions-pCH9A.json

8. Settings

Screenshot saved to: .pencil-screenshots/random/8-settings-7b2O7.json

Components

Screenshot saved to: .pencil-screenshots/random/components-0KJfI.json


Generated by Pencil Design Review for commit c722298

@github-actions
Copy link

github-actions bot commented Feb 16, 2026

🎨 Design Review

📁 1 design file (1 modified)
🖼️ Frames: 3 modified, 6 unchanged

📝 test-fixtures/designs/random.pen (modified)

✏️ Modified Frames

1. Dashboard

BeforeAfter
Before After

6. Analytics

BeforeAfter
Before After

8. Settings

BeforeAfter
Before After
✅ 6 unchanged frames
    1. Interviews (iVES0)
    1. Candidates (x7cUJ)
    1. Questions Bank (Mvlj1)
    1. AI Settings (lwglP)
    1. Job Positions (pCH9A)
  • Components (0KJfI)

Generated by Pencil Design Review for commit c722298

@github-actions
Copy link

github-actions bot commented Feb 16, 2026

🎨 Design Review

📁 1 design file (1 modified)
🖼️ 9 frames detected
9 failed to render

📝 test-fixtures/designs/random.pen (modified)

Frame ID Status
1. Dashboard J1VeO ❌ Failed to download image: 401
2. Interviews iVES0 ❌ Failed to download image: 401
3. Candidates x7cUJ ❌ Failed to download image: 401
4. Questions Bank Mvlj1 ❌ Failed to download image: 401
5. AI Settings lwglP ❌ Failed to download image: 401
6. Analytics pdVTW ❌ Failed to download image: 401
7. Job Positions pCH9A ❌ Failed to download image: 401
8. Settings 7b2O7 ❌ Failed to download image: 401
Components 0KJfI ❌ Failed to download image: 401

Generated by Pencil Design Review for commit c722298

@github-actions
Copy link

github-actions bot commented Feb 16, 2026

🎨 Design Review

📁 1 design file (1 modified)
🖼️ Frames: 3 modified, 6 unchanged

📝 test-fixtures/designs/random.pen (modified)

✏️ Modified Frames

1. Dashboard

BeforeAfter
Before After

6. Analytics

BeforeAfter
Before After

8. Settings

BeforeAfter
Before After
✅ 6 unchanged frames
    1. Interviews (iVES0)
    1. Candidates (x7cUJ)
    1. Questions Bank (Mvlj1)
    1. AI Settings (lwglP)
    1. Job Positions (pCH9A)
  • Components (0KJfI)

Generated by Pencil Design Review for commit c722298

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ritikrishu ritikrishu merged commit e0de18c into main Feb 16, 2026
5 checks passed
@ritikrishu ritikrishu deleted the features/diff-review-mode branch February 17, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant