Skip to content

feat(pr-history): Report Glitch action and REQ→PR back-link (#573)#586

Merged
frankbria merged 3 commits intomainfrom
feature/issue-573-report-glitch-pr-backlink
Apr 15, 2026
Merged

feat(pr-history): Report Glitch action and REQ→PR back-link (#573)#586
frankbria merged 3 commits intomainfrom
feature/issue-573-report-glitch-pr-backlink

Conversation

@frankbria
Copy link
Copy Markdown
Owner

@frankbria frankbria commented Apr 15, 2026

Summary

  • Adds [Report Glitch] button to each merged PR row in the PR History panel on the Review page
  • Clicking it fetches the PR's changed files via a new GET /api/v2/pr/{number}/files endpoint, then opens the Capture Glitch modal pre-populated with scope, source, and a PR reference note
  • REQs created from a PR store the GitHub PR URL as source_issue, which now renders as a clickable link on the REQ detail page

Closes #573

Changes

Backend (Python)

  • GitHubIntegration.get_pr_files(pr_number) — fetches changed filenames from GitHub API
  • GET /api/v2/pr/{pr_number}/files — new endpoint returning PRFilesResponse { files: string[] }

Frontend (TypeScript)

  • CaptureGlitchModal — new optional props prNumber, prTitle, prUrl, initialScope for pre-population; includes source_issue in submission payload
  • PRHistoryPanel — [Report Glitch] button on each row with loading spinner during file fetch
  • proof/[req_id]/page.tsxsource_issue renders as clickable GitHub link when applicable
  • prApi.getFiles() and PRFilesResponse type added

Test plan

  • Backend: 3 unit tests for get_pr_files (pass, empty, error propagation)
  • Backend: 4 endpoint tests for GET /api/v2/pr/{number}/files (200, empty, 404, close)
  • Frontend: 4 tests for CaptureGlitchModal pre-population (description, scope, source_issue, no source_issue)
  • Frontend: 3 tests for PRHistoryPanel Report Glitch button (render, fetch, modal open)
  • Frontend: 3 tests for REQ detail source_issue rendering (link, plain text, null)
  • All 32 backend tests pass (uv run pytest)
  • All 783 frontend tests pass (npm test)
  • npm run build succeeds
  • ruff check . passes

Summary by CodeRabbit

  • New Features
    • Added "Report Glitch" button to each PR row in PR history with loading state while fetching changed files.
    • Glitch report modal now pre-populates description and scope with PR number, title, and fetched file list; falls back to a placeholder scope if file fetch fails.
    • Submissions include the PR URL as the source when available, and PR-originating GitHub links render as clickable "Source PR" links.

Add "Report Glitch" button to each merged PR row in the PR History panel.
Clicking it fetches the PR's changed files and opens the Capture Glitch
modal pre-populated with scope, source, and a PR reference note. REQs
created this way store the GitHub PR URL as source_issue, which now
renders as a clickable link on the REQ detail page.

Backend:
- GitHubIntegration.get_pr_files() fetches changed files for a PR
- GET /api/v2/pr/{pr_number}/files endpoint exposes PR changed files

Frontend:
- CaptureGlitchModal accepts optional prNumber/prTitle/prUrl/initialScope
  props for pre-population and includes source_issue in the payload
- PRHistoryPanel rows include [Report Glitch] button with loading state
- REQ detail page renders source_issue as a GitHub link when applicable
- PRFilesResponse type and prApi.getFiles() API method added
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 604e95fb-048b-4f69-980d-1b4455a2aac2

📥 Commits

Reviewing files that changed from the base of the PR and between 2d24529 and 40503e6.

📒 Files selected for processing (4)
  • codeframe/git/github_integration.py
  • tests/unit/test_github_integration.py
  • web-ui/src/app/proof/[req_id]/page.tsx
  • web-ui/src/components/review/PRHistoryPanel.tsx
✅ Files skipped from review due to trivial changes (1)
  • tests/unit/test_github_integration.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • web-ui/src/app/proof/[req_id]/page.tsx
  • web-ui/src/components/review/PRHistoryPanel.tsx
  • codeframe/git/github_integration.py

Walkthrough

Adds a backend endpoint to fetch filenames changed in a GitHub PR, a GitHub client method, frontend API and UI wiring to fetch those files from PR history, pre-fill the Capture Glitch modal (scope, description, optional source PR), and conditional rendering of a Source PR link on requirement detail pages.

Changes

Cohort / File(s) Summary
GitHub integration & tests
codeframe/git/github_integration.py, tests/unit/test_github_integration.py
Add GitHubIntegration.get_pr_files(pr_number) that paginates GitHub's pulls/{pr}/files endpoint and returns filenames; unit tests for success, empty list, and error propagation.
PR files API & tests
codeframe/ui/routers/pr_v2.py, tests/ui/test_pr_history.py
Add GET /api/v2/pr/{pr_number}/files returning PRFilesResponse(files: list[str]); robust error mapping (404 → 404, others → propagated codes) and client cleanup; UI-level tests covering 200/404 and close behavior.
Frontend API & types
web-ui/src/lib/api.ts, web-ui/src/types/index.ts
Add prApi.getFiles(workspacePath, prNumber): Promise<string[]>, PRFilesResponse type, and CaptureGlitchRequest.source_issue?: string.
PR history panel & tests
web-ui/src/components/review/PRHistoryPanel.tsx, web-ui/src/__tests__/components/review/PRHistoryPanel.test.tsx
Add per-PR "Report Glitch" button, per-PR loading state, async handler that calls prApi.getFiles and opens CaptureGlitchModal with fetched files (or fallback scope). Tests assert button rendering, API call, and modal opening.
Capture Glitch modal & tests
web-ui/src/components/proof/CaptureGlitchModal.tsx, web-ui/src/__tests__/components/proof/CaptureGlitchModal.test.tsx
Extend modal props (prNumber, prTitle, prUrl, initialScope); pre-fill description and scope on open; include source_issue in payload only when prUrl provided; tests for pre-population and payload content.
Proof detail rendering & tests
web-ui/src/app/proof/[req_id]/page.tsx, web-ui/src/__tests__/components/proof/ProofDetailPage.test.tsx
Render source_issue as an external "Source PR:" link when it's a GitHub URL; otherwise show plain text or omit. Tests cover link vs. text vs. absence.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant PRHistory as PR History Panel
    participant FrontendAPI as Frontend API (prApi)
    participant Backend as Backend Router (/api/v2)
    participant GitHub as GitHub API
    participant Modal as Capture Glitch Modal

    User->>PRHistory: Click "Report Glitch"
    PRHistory->>FrontendAPI: prApi.getFiles(workspace, prNumber)
    FrontendAPI->>Backend: GET /api/v2/pr/{pr_number}/files
    Backend->>GitHub: GET /repos/.../pulls/{pr_number}/files?per_page=100
    GitHub-->>Backend: PR file list
    Backend-->>FrontendAPI: PRFilesResponse(files)
    FrontendAPI-->>PRHistory: string[] (filenames)
    PRHistory->>Modal: Open with {prNumber, prTitle, prUrl, initialScope: files}
    Modal->>Modal: Pre-fill description and scope
    User->>Modal: Submit
    Modal->>FrontendAPI: proofApi.capture({... , source_issue?: prUrl})
    FrontendAPI-->>Modal: Success
    Modal->>PRHistory: Close
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I hopped from commit to PR with glee,
Collected changed files for scope, safe and free,
A note: "Reported from PR #…" in the form,
Source link trimmed and opened in a storm,
Hop, report, and patch — carrot-powered QA!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the two main changes: adding a Report Glitch action to PR history and implementing a REQ→PR back-link, matching the core functionality in the changeset.
Linked Issues check ✅ Passed All coding requirements from issue #573 are met: Report Glitch button with pre-populated scope/source/reference [PRHistoryPanel, CaptureGlitchModal], PR file fetching endpoint [github_integration, pr_v2.py], and source_issue rendering on REQ detail page [proof page, tests].
Out of Scope Changes check ✅ Passed All changes directly support the linked issue objectives: backend PR file fetching, frontend API integration, modal pre-population, REQ detail source_issue rendering, and comprehensive test coverage. No unrelated changes detected.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-573-report-glitch-pr-backlink

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

@claude
Copy link
Copy Markdown

claude Bot commented Apr 15, 2026

Review posted via file

@claude
Copy link
Copy Markdown

claude Bot commented Apr 15, 2026

Review: feat(pr-history): Report Glitch action and REQ-to-PR back-link

The PR delivers a clean, well-scoped feature. Architecture follows existing patterns, test coverage is solid, and the CI checklist checks out. A few items worth addressing before merge.

What is working well

  • Backend endpoint, modal pre-population, and API client all follow the established patterns.
  • Error handling in the router (404 NOT_FOUND, generic 500, finally: client.close()) mirrors other PR endpoints correctly.
  • rel=noopener noreferrer is present on the external link.
  • Test coverage hits the key paths: happy path, empty list, 404, and client.close() assertion.

Issue 1: GitHub API pagination -- get_pr_files() is incomplete for large PRs

The GitHub API returns at most 30 files per page by default. A PR touching 31+ files will silently return a truncated list, leaving the pre-populated scope in the Capture Glitch modal incomplete with no user indication. Quick fix: add per_page=100 to the request params. Full pagination via the Link header is more thorough but likely overkill for this use case.

Issue 2: Silent error swallowed in handleReportGlitch

When the files fetch fails, the modal opens silently with empty scope. The user cannot distinguish a network error from a PR with no changed files. A toast or inline error banner would prevent glitches being captured with empty scope due to transient API failures.

Issue 3: Redundant open prop

The outer conditional already gates on glitchTarget being truthy, so open={!!glitchTarget} is always true inside that block. Either drop the outer conditional and let the open prop control visibility (the idiomatic pattern for controlled modals), or change it to open={true}.

Issue 4: Unused workspace dependency in the /files endpoint

The injected workspace is never referenced in the function body. This forces callers to supply a valid workspace_path for a purely GitHub-side operation. If kept intentionally (auth context, future use), a brief comment would clarify intent -- otherwise it can be removed.

Minor

  • Unused ProofRequirement import in PRHistoryPanel.tsx: the onSuccess lambda ignores its argument so the type is never referenced. TypeScript does not error but it is a dead import.
  • source_issue URL check via startsWith is intentionally narrow and correct for now. Worth a comment if other source URL formats are added later.

Summary

Severity Item
Should fix GitHub API pagination -- silent truncation on PRs with 30+ files
Should fix Silent catch -- empty scope with no error feedback on failure
Nit Redundant open prop double-check
Nit Unused workspace param (or add comment if intentional)
Nit Unused ProofRequirement import

The pagination gap is the only item with correctness impact. The rest is polish. Solid implementation overall -- this cleanly closes the feedback loop between merged PRs and glitch capture.

- Add per_page=100 to get_pr_files() to avoid silent truncation on PRs
  with 30+ changed files (GitHub API default is 30)
- Show fallback note in scope when file fetch fails so user knows to
  enter scope manually (was silently empty)
- Simplify redundant open={!!glitchTarget} to open (always true inside
  the conditional render block)
- Remove unused ProofRequirement import from PRHistoryPanel
Copy link
Copy Markdown
Contributor

@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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@codeframe/git/github_integration.py`:
- Around line 393-395: The current call to self._make_request for endpoint
"/repos/{self.owner}/{self.repo_name}/pulls/{pr_number}/files" only fetches a
single page; update the method (where endpoint and pr_number are used) to
paginate until all files are retrieved by either iterating page numbers with
per_page=100 query param or following the Link header from self._make_request
responses, accumulating filenames from each page into a single list before
returning; ensure you handle empty pages and stop when no next page exists.

In `@tests/unit/test_github_integration.py`:
- Around line 302-347: The new tests in TestGetPrFiles (including
test_returns_list_of_filenames, test_returns_empty_list_for_no_files,
test_propagates_api_error) are missing the required v2 marker; add the marker by
either decorating the class or each test with `@pytest.mark.v2` (or add pytestmark
= pytest.mark.v2 at the module top) so these tests run under the enforced pytest
-m v2 profile; ensure the marker import (pytest) remains available and update
only the test file containing TestGetPrFiles and its test methods.

In `@web-ui/src/app/proof/`[req_id]/page.tsx:
- Around line 264-274: Replace the fragile startsWith check for req.source_issue
with strict URL parsing and host validation: in the conditional that renders the
"Source PR" link (where req.source_issue is used in page.tsx), wrap new
URL(req.source_issue) in a try/catch and only render the link if url.protocol
=== 'https:' and url.hostname === 'github.com'; otherwise treat it as untrusted
(do not render or render plain text). This ensures lookalike hosts like
"github.com.evil.tld" are rejected while preserving valid https://github.com
links.

In `@web-ui/src/components/review/PRHistoryPanel.tsx`:
- Around line 68-77: Concurrent clicks can cause stale getFiles responses to
overwrite glitchTarget; fix handleReportGlitch by associating each fetch with
the PR number and ignoring responses that don't match the latest request:
capture const requestNumber = pr.number, setLoadingFiles(requestNumber), then
after awaiting prApi.getFiles check that the current request equals
requestNumber (use an up-to-date source such as a loadingFilesRef mirrored from
loadingFiles state or compare against loadingFiles state read via a ref) before
calling setGlitchTarget; apply the same guard to the catch branch so only the
most recent request updates setGlitchTarget and ensure finally still clears
loading using setLoadingFiles(null).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 190cae3d-ebf3-41d9-bb51-83be6d53a513

📥 Commits

Reviewing files that changed from the base of the PR and between 2df7918 and 111cb3e.

📒 Files selected for processing (12)
  • codeframe/git/github_integration.py
  • codeframe/ui/routers/pr_v2.py
  • tests/ui/test_pr_history.py
  • tests/unit/test_github_integration.py
  • web-ui/src/__tests__/components/proof/CaptureGlitchModal.test.tsx
  • web-ui/src/__tests__/components/proof/ProofDetailPage.test.tsx
  • web-ui/src/__tests__/components/review/PRHistoryPanel.test.tsx
  • web-ui/src/app/proof/[req_id]/page.tsx
  • web-ui/src/components/proof/CaptureGlitchModal.tsx
  • web-ui/src/components/review/PRHistoryPanel.tsx
  • web-ui/src/lib/api.ts
  • web-ui/src/types/index.ts

Comment thread codeframe/git/github_integration.py Outdated
Comment thread tests/unit/test_github_integration.py
Comment thread web-ui/src/app/proof/[req_id]/page.tsx Outdated
Comment on lines +68 to +77
const handleReportGlitch = async (pr: PRHistoryItem) => {
setLoadingFiles(pr.number);
try {
const files = await prApi.getFiles(workspacePath, pr.number);
setGlitchTarget({ pr, files });
} catch {
setGlitchTarget({ pr, files: [] });
} finally {
setLoadingFiles(null);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against out-of-order getFiles responses (stale modal data).

Concurrent clicks across different rows can race; the slower earlier request can overwrite glitchTarget and open the modal for the wrong PR.

Suggested fix
-import { useState } from 'react';
+import { useRef, useState } from 'react';
@@
 export function PRHistoryPanel({ workspacePath }: PRHistoryPanelProps) {
   const [expandedPR, setExpandedPR] = useState<number | null>(null);
   const [loadingFiles, setLoadingFiles] = useState<number | null>(null);
+  const latestRequestId = useRef(0);
@@
   const handleReportGlitch = async (pr: PRHistoryItem) => {
+    const requestId = ++latestRequestId.current;
     setLoadingFiles(pr.number);
     try {
       const files = await prApi.getFiles(workspacePath, pr.number);
-      setGlitchTarget({ pr, files });
+      if (requestId === latestRequestId.current) {
+        setGlitchTarget({ pr, files });
+      }
     } catch {
-      setGlitchTarget({ pr, files: [] });
+      if (requestId === latestRequestId.current) {
+        setGlitchTarget({ pr, files: [] });
+      }
     } finally {
-      setLoadingFiles(null);
+      if (requestId === latestRequestId.current) {
+        setLoadingFiles(null);
+      }
     }
   };
@@
-                  disabled={loadingFiles === pr.number}
+                  disabled={loadingFiles !== null}

Also applies to: 148-149, 204-214

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/review/PRHistoryPanel.tsx` around lines 68 - 77,
Concurrent clicks can cause stale getFiles responses to overwrite glitchTarget;
fix handleReportGlitch by associating each fetch with the PR number and ignoring
responses that don't match the latest request: capture const requestNumber =
pr.number, setLoadingFiles(requestNumber), then after awaiting prApi.getFiles
check that the current request equals requestNumber (use an up-to-date source
such as a loadingFilesRef mirrored from loadingFiles state or compare against
loadingFiles state read via a ref) before calling setGlitchTarget; apply the
same guard to the catch branch so only the most recent request updates
setGlitchTarget and ensure finally still clears loading using
setLoadingFiles(null).

@claude
Copy link
Copy Markdown

claude Bot commented Apr 15, 2026

Follow-up correction to my previous review

On re-reading the diff more carefully, two items in my prior review were inaccurate and I want to correct them:

Issue 1 (Pagination) — correction
I said per_page=100 was missing — it is already in the implementation:

endpoint = f"/repos/{self.owner}/{self.repo_name}/pulls/{pr_number}/files?per_page=100"

The actual concern is that GitHub's max for this endpoint is 300 files across multiple pages, so a PR touching more than 100 files will be silently truncated. The current code handles the common case (≤100 files) correctly. For completeness, full pagination via the Link header would close the gap, but this is an edge case and the current behavior is acceptable.

Issue 2 (Error handling) — correction
I said the error was swallowed silently. It is not — the catch block sets a clear placeholder:

setGlitchTarget({ pr, files: ['# Could not load changed files — enter scope manually'] });

This surfaces in the scope textarea and gives the user actionable instruction. It is reasonable error handling. A toast could complement it, but the current implementation is not silent.

The remaining items (redundant open prop, unused workspace dependency, unused import) stand as originally noted. Apologies for the inaccuracies.

- Paginate get_pr_files() to handle PRs with 100+ changed files
  (loop with per_page=100 until all pages exhausted)
- Add @pytest.mark.v2 to TestGetPrFiles so tests run in enforced v2 profile
- Use URL() constructor for strict host validation on source_issue links
  (rejects lookalike hosts like github.com.evil.tld)
- Disable all Report Glitch buttons during any file fetch to prevent
  race conditions from concurrent clicks
@claude
Copy link
Copy Markdown

claude Bot commented Apr 15, 2026

Follow-up review (addressing prior feedback)

The commit at 111cb3e picks up all three of the substantive issues raised in my first review (after I corrected myself) and by CodeRabbit. Summary of what landed:

All critical items resolved

Item Status
GitHub API pagination ✅ Fully implemented — page loop with per_page=100, terminates on len(data) < 100
Error handling ✅ Not silent — catch block sets a descriptive placeholder in scope textarea
v2 pytest marker on TestGetPrFiles @pytest.mark.v2 on the class
URL host validation new URL() with try/catch, checks protocol === 'https:' and hostname === 'github.com'
Race condition on concurrent clicks disabled={loadingFiles !== null} prevents a second click while any fetch is in progress

Two nits still open from the original review

These were flagged before and are unchanged. Neither blocks merge, but they're worth a quick note for housekeeping:

  1. Redundant open prop — In PRHistoryPanel.tsx, the modal is mounted inside {glitchTarget && (<CaptureGlitchModal open …/>)}. The outer conditional already gates rendering, so open is always true inside that block. Either drop the outer conditional (let open={!!glitchTarget} control visibility, the standard controlled-modal pattern) or keep the outer guard and use open={true} explicitly — but the current mix of both is redundant.

  2. Unused workspace dependency in /files endpointworkspace: Workspace = Depends(get_v2_workspace) is injected but never referenced in the function body. If it's there for future auth context, a one-line comment would clarify intent; otherwise it can be removed to avoid requiring callers to supply workspace_path for a purely GitHub-side call.


Minor coverage gap worth knowing

test_returns_list_of_filenames in TestGetPrFiles mocks a 2-item response and confirms the loop terminates. There's no test for the multi-page path (first page returns exactly 100 items, triggering a second call). The logic is straightforward, but a test for that branch would fully exercise the pagination loop. Not required for merge, just a gap to be aware of.


Overall this is a clean, well-scoped implementation. The pagination fix is correct, the URL validation is solid, and the test coverage across backend, router, and frontend is thorough. The two open nits are minor polish items.

@frankbria frankbria merged commit 429bf11 into main Apr 15, 2026
11 checks passed
@frankbria frankbria deleted the feature/issue-573-report-glitch-pr-backlink branch April 15, 2026 16:29
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.

[Phase 4B] Post-merge: Report Glitch action and REQ→PR back-link

1 participant