Skip to content

Simplify the desktop pull request reader - #1158

Merged
alexeyzimarev merged 12 commits into
mainfrom
norton/ai-3216-simplify-pr-reader
Sep 25, 2026
Merged

alexeyzimarev merged 12 commits into
mainfrom
norton/ai-3216-simplify-pr-reader

Conversation

@nortonandreev

@nortonandreev nortonandreev commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1157 — AI-3216

What & why

The reader showed each status in three places, used Fluent chrome where the app has its own, and could disagree with itself. Each status now lives in one place: Checks and Reviews tabs carry an outcome dot, and the header is lifecycle, repo #n and the GitHub link. Buttons use the app's kcapGhost / kcapPrimary classes, tooltips follow the rail footer's text-plus-caption pattern, and every tab shares one content inset. Loading and empty states share one centred slot, and the footer reports only when the open tab was last updated; refresh lives on the About this work pane, which reloads the reader too. A session that switches branch finds the PR opened on the new one. The open tab reloads on the 30s poll unless it was paged past page one, and a tab reopened after 30s reloads, so rows cannot fall behind the checks summary; a new PR head reloads the checks without a prompt. The conversation tab is named Comments.

Where to look

  • GitRepository.CurrentBranch reads the checkout's HEAD without a git process. The daemon still reports the branch the worktree was cut on, and PR discovery and the Session pane now prefer HEAD.
  • RequestRefresh: a click waits out a 3s gap instead of the poll's 15s. Progress (ShowsRefreshing, the pane's IsRefreshing) shows only for a click; the poll reads silently.
  • PulseClock drives every pending marker from one timer, which runs only while one is on screen.
  • RemoteAvatar fetches https://{host}/{login}.png through MarkdownImages, the same credential-free fetch markdown images use.

Verification

  • dotnet run --project test/Capacitor.App.Tests.Unit: 2761 passed, 0 failed.
  • GitRepository* tests in Capacitor.Cli.Core.Tests.Unit: 20 passed. AOT publish of Capacitor.Cli: 0 IL warnings.
  • Rendered every reader tab headlessly with Skia and compared spacing and button chrome.

Visuals

Screenshot 2026-09-25 at 17 24 00 Screenshot 2026-09-25 at 17 23 42 Screenshot 2026-09-25 at 17 23 39 Screenshot 2026-09-25 at 17 23 35 Screenshot 2026-09-25 at 17 23 21 Screenshot 2026-09-25 at 17 23 19

@linear-code

linear-code Bot commented Sep 25, 2026

Copy link
Copy Markdown

AI-3216

@nortonandreev nortonandreev self-assigned this Sep 25, 2026
A requested reviewer with no review carries no state from GitHub, so it now reads
as awaiting rather than unknown. Avatars load through MarkdownImages, the app's one
credential-free https image fetch; a failed load leaves the initial in place.
A style animation starts when its control appears, so each copy of a pending marker
faded on its own offset. PulseClock drives every marker from one phase instead.
…1157)

Fluent's CheckBox has a 32px minimum height, taller than the sub-tab pills, so
opening Threads pushed the content down. The row now has a fixed height.
The daemon reports the branch a worktree was cut on, so a PR opened after a switch
was never discovered; HEAD is read instead. Check rows reload while any is pending,
so the summary cannot move on beside a stale snapshot.
)

Refresh lives on the About this work pane alone, which already reloads the reader,
so the footer only reports when the open tab was last updated.
A tab paged past its first page is left alone, since a reload restarts at page one.
Progress shows only for a refresh someone clicked; the poll reads silently.
The poll reloads only the open tab, and the checks summary distrusts rows older than
30s, so rows kept past that beside a moved-on summary read as out of sync.
A page read that reports the new head first recovers through one fresh overview; an
overview still on the old head leaves the retry to the next poll rather than looping.
@nortonandreev
nortonandreev force-pushed the norton/ai-3216-simplify-pr-reader branch from de54170 to fee2033 Compare September 25, 2026 11:52
@nortonandreev

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fee203335c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

string? _reportedBranch;
// The daemon reports the branch the worktree was cut on; HEAD knows a switch made since.
void ResolveBranch() {
var branch = GitRepository.CurrentBranch(WorktreePath) ?? _reportedBranch;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve the branch from the agent's actual worktree

For a borrowed reviewer whose snapshot path differs from BorrowedFrom, WorktreePath has already been rewritten by CheckoutLabel.CheckoutPathFor to the checkout being reviewed. The status DTO documents Branch as belonging to the checkout the agent runs in, so this reads HEAD from the wrong repository and can display a branch unrelated to the session whenever those two checkouts differ. Retain the raw dto.WorktreePath for this lookup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The branch fact is HEAD of the checkout the pane already names. CheckoutLabel.CheckoutPathFor prefers BorrowedFrom, and that is the path ResolveBranch reads. A borrowed snapshot's daemon Branch is empty, so that HEAD is the label beside the borrowed checkout. The PR list keeps the raw snapshot path because it has to follow the checkout the agent runs in; using it here would show the snapshot's branch next to the borrowed checkout.

Comment on lines +148 to +152
} else if (read.Kind == PullRequestReadKind.Restart && read.Reason == "head_changed" && !_headRestarted) {
// Learn the new head from a fresh overview, whose apply reloads the checks. Once only:
// an overview still on the old head would bounce straight back here.
_headRestarted = true;
_sections.Remove(key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset head-change recovery for each pull request

After one check request returns head_changed, this global flag remains true until a successful checks page clears it. If the user switches PRs or sessions while the recovery overview is pending and the newly selected PR also returns head_changed, this branch is skipped and the generic restart path marks its checks section stopped instead of fetching a fresh overview. Reset this state when changing the selected subject/session, or scope it to the subject.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

_headRestarted is cleared in CancelReads. Choosing another pull request and switching session both cancel the in-flight recovery, so the next head_changed can load a fresh overview. A recovery that was not cancelled still refuses a second bounce from an overview on the old head. Covered by Switching_pull_request_during_a_head_change_lets_the_next_one_recover.

The once-only guard stops an overview on the old head from bouncing. It has to die with the cancelled read, or the next pull request's checks stop instead of reloading.
@nortonandreev
nortonandreev marked this pull request as ready for review September 25, 2026 12:19
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Simplify and synchronize the desktop pull request reader

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Consolidates pull request outcomes, navigation, loading states, and refresh feedback.
• Keeps branch discovery, open tabs, and check rows synchronized with current repository state.
• Adds reviewer avatars, shared pending animations, and comprehensive reader behavior tests.
Diagram

graph TD
  HEAD["Git HEAD"] --> VM["PR View Model"] --> SRC["PR Reader Source"] --> GH["GitHub"]
  WORK["Work Context"] --> VM
  VM --> PROJ["Row Projections"] --> UI["Reader UI"]
  SVC["UI Services"] --> UI
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Daemon-pushed pull request invalidation
  • ➕ Could refresh changed tabs immediately instead of waiting for the 30-second poll.
  • ➕ Would reduce periodic reads when pull request state remains unchanged.
  • ➖ Requires cross-process protocol and daemon changes beyond the desktop reader.
  • ➖ Still needs pagination-preservation and head-change recovery rules.
  • ➖ Adds connection and event-ordering failure modes to a currently request-driven feature.

Recommendation: Keep the PR's hybrid polling and freshness-based reload strategy. It resolves stale summaries and branch switches within the existing reader contract while deliberately preserving multi-page browsing; push-based invalidation could be explored separately if polling traffic or latency becomes significant.

Files changed (20) +792 / -255

Enhancement (8) +312 / -107
App.axamlAdd shared ghost-button sizes and refresh spinner styling +36/-0

Add shared ghost-button sizes and refresh spinner styling

• Defines reusable icon and quiet variants for the app's ghost buttons. Adds a rotating path animation for visible refresh feedback.

src/Capacitor.App/App.axaml

PullRequestContextViewModel.Projections.csRefine reader labels, empty states, reviewer statuses, and avatars +51/-25

Refine reader labels, empty states, reviewer statuses, and avatars

• Separates loading, empty, page-error, and freshness projections for consistent rendering. Improves reviewer and row metadata, including awaiting-review semantics, re-request detection, concise details, and GitHub avatar URLs.

src/Capacitor.App/ViewModels/PullRequestContextViewModel.Projections.cs

PullRequestRow.csExtend pull request rows with avatars and re-request state +3/-2

Extend pull request rows with avatars and re-request state

• Replaces the generic requested-review flag with explicit re-request semantics and adds an optional avatar URL. Includes avatar data in the row memory budget.

src/Capacitor.App/ViewModels/PullRequestRow.cs

PullRequestReader.axamlSimplify reader navigation, states, rows, and footer +96/-74

Simplify reader navigation, states, rows, and footer

• Moves checks and review outcomes onto their tabs, renames Activity to Comments, and makes review subtabs a fixed-height segmented row. Adds centered loading and empty states, reviewer avatars, shared button styles, and a status-only freshness footer.

src/Capacitor.App/Views/PullRequestReader.axaml

PulseClock.csIntroduce a shared pending-marker animation clock +66/-0

Introduce a shared pending-marker animation clock

• Adds an attached Avalonia property that synchronizes opacity across all visible pending markers. The render timer starts only while at least one active marker is attached.

src/Capacitor.App/Views/PulseClock.cs

RemoteAvatar.csAdd resilient credential-free reviewer avatar loading +38/-0

Add resilient credential-free reviewer avatar loading

• Loads remote avatars through the existing MarkdownImages service and displays them only after successful completion. Guards against stale asynchronous results when rows are rebound.

src/Capacitor.App/Views/RemoteAvatar.cs

SessionRailView.axamlSynchronize session rail pending markers +1/-1

Synchronize session rail pending markers

• Connects the rail's running-check branch glyph to the shared pulse clock.

src/Capacitor.App/Views/SessionRailView.axaml

WorkContextView.axamlShow explicit refresh progress in the work context pane +21/-5

Show explicit refresh progress in the work context pane

• Disables the refresh action during its own read while preserving full-strength spinner feedback. Replaces the simple tooltip with the application's text-and-caption pattern.

src/Capacitor.App/Views/WorkContextView.axaml

Bug fix (5) +97 / -20
PullRequestContextViewModel.Reads.csSynchronize discovery and section reloads with current state +35/-9

Synchronize discovery and section reloads with current state

• Discovers pull requests from the checkout's current HEAD branch and gives manual refreshes a shorter queue delay. Reloads eligible open tabs during polling and automatically recovers checks after rollup or head changes.

src/Capacitor.App/ViewModels/PullRequestContextViewModel.Reads.cs

PullRequestContextViewModel.csTrack worktree, user refresh, and stale tab state +19/-4

Track worktree, user refresh, and stale tab state

• Stores the session worktree and distinguishes user-requested progress from silent polling. Refreshes reopened sections after 30 seconds while retaining tabs that have paged beyond the first page.

src/Capacitor.App/ViewModels/PullRequestContextViewModel.cs

WorkContextViewModel.csResolve live branches and expose manual refresh progress +22/-3

Resolve live branches and expose manual refresh progress

• Prefers the checkout's current HEAD over the daemon-reported creation branch. Tracks user-triggered refreshes across queued reads and updates refresh tooltip messaging.

src/Capacitor.App/ViewModels/WorkContextViewModel.cs

PullRequestStatusLabel.axamlPulse only pending status markers +6/-4

Pulse only pending status markers

• Connects pending dots to the shared pulse clock rather than fading the entire status control. Keeps status text fully readable while pending.

src/Capacitor.App/Views/PullRequestStatusLabel.axaml

GitRepository.csRead the current branch directly from Git HEAD +15/-0

Read the current branch directly from Git HEAD

• Adds a process-free branch resolver supporting main and linked worktrees. Returns null for detached, missing, or unreadable HEAD state.

src/Capacitor.Cli.Core/GitRepository.cs

Refactor (3) +71 / -56
PullRequestContextViewModel.Presentation.csUse the shared row freshness interval for check status +1/-1

Use the shared row freshness interval for check status

• Replaces the inline 30-second check-row validity threshold with the view model's shared freshness constant.

src/Capacitor.App/ViewModels/PullRequestContextViewModel.Presentation.cs

PullRequestCard.axamlAdopt application-standard button styles in the PR card +5/-5

Adopt application-standard button styles in the PR card

• Replaces reader-specific button chrome with shared ghost and primary application styles.

src/Capacitor.App/Views/PullRequestCard.axaml

PullRequestStyles.axamlRestyle tabs, outcome dots, rows, and section states +65/-50

Restyle tabs, outcome dots, rows, and section states

• Reworks secondary tabs as a segmented control and adds tab outcome-dot styling. Removes obsolete reader button styles while standardizing row spacing, content insets, and loading or empty layouts.

src/Capacitor.App/Views/PullRequestStyles.axaml

Tests (4) +312 / -72
FakePullRequestSource.csSupport empty, delayed, restarted, and changing-head reads +16/-5

Support empty, delayed, restarted, and changing-head reads

• Extends the fake source with configurable head SHAs, empty pages, gated page completion, and one-shot restart responses. These controls exercise the reader's new asynchronous recovery paths.

test/Capacitor.App.Tests.Unit/FakePullRequestSource.cs

PullRequestContextViewModelRegistryTests.csVerify discovery uses the checkout's current branch +16/-1

Verify discovery uses the checkout's current branch

• Adds coverage proving that pull request discovery prefers worktree HEAD after a branch switch. Updates the harness to provide a worktree path.

test/Capacitor.App.Tests.Unit/PullRequestContextViewModelRegistryTests.cs

PullRequestPresentationTests.csCover simplified reader presentation and refresh behavior +243/-66

Cover simplified reader presentation and refresh behavior

• Adds headless tests for tab outcomes, reviewer states, avatars, shared pulses, loading and empty states, and consistent freshness labels. Also verifies polling, stale-tab reloads, manual progress, pagination preservation, and automatic head-change recovery.

test/Capacitor.App.Tests.Unit/PullRequestPresentationTests.cs

GitRepositoryCurrentBranchTests.csTest branch resolution for standard and linked worktrees +37/-0

Test branch resolution for standard and linked worktrees

• Covers normal branches, linked-worktree HEAD files, detached HEAD state, and non-repository paths.

test/Capacitor.Cli.Core.Tests.Unit/GitRepositoryCurrentBranchTests.cs

@nortonandreev

Copy link
Copy Markdown
Contributor Author

Changelog

Pull request reader

Changed

  • Checks and Reviews tabs show a coloured dot for their outcome (failed, needs review, passing, running). The status buttons that repeated this in the header are gone.
  • The header is one line: state, owner/repo #number, and a GitHub link. The branch line is hidden when GitHub doesn't report the branches.
  • Selected tabs highlight in the app's selection blue. Review sub-tabs (Reviewers, Submitted, Threads) are compact pills, so they no longer look like a second row of main tabs.
  • Sections no longer repeat their tab name as a heading.
  • "Activity" is now Comments, and "Published reviews" is now Submitted.
  • "Show resolved" on Threads is a toggle switch in the app's style, placed beside the sub-tabs.
  • Reviewers show their GitHub avatar, falling back to their initial.
  • A reviewer who has been asked but hasn't reviewed yet shows Awaiting review, not "Unknown review state". "Review re-requested" appears only when someone who already reviewed is asked again.
  • Check and thread rows leave out details GitHub didn't report ("Unknown source", "Resolution unknown").
  • Loading and empty sections share one centred state, with a spinner while loading and an icon and message when empty.
  • The bottom bar reads "Updated HH:MM:SS" on every tab. On Checks, hovering it shows the commit the checks ran on.
  • Buttons, links and tooltips use the app's own styles, and every tab uses the same spacing.

Fixed

  • A pull request opened after the agent switched branch now appears without restarting the app. The Session pane's branch also updates.
  • Checks can no longer show a green summary beside rows still marked pending, or a failure without the failed job in the list. The open tab reloads every 30 seconds, and a tab left for more than 30 seconds reloads when reopened.
  • A new commit on the PR reloads the checks automatically. You no longer have to refresh by hand.
  • The pending-checks pulse is gentler (only the small dot fades, slowly) and stays in step across the sidebar, reader and session rail.
  • "Unknown" states have a readable outline instead of a near-invisible ring.
  • Unknown review states no longer wrap onto two lines and misalign their icon.
  • Opening Threads no longer shifts the content down.

About this work pane

  • Its refresh button is the only refresh. It reloads the work item, the plan and the pull request, including the open tab.
  • Clicking refresh spins the icon in place and disables the button until the refresh finishes. Background refreshes run silently.
  • A click within 15 seconds of the last automatic refresh now runs within 3 seconds, instead of being held until that 15-second gap passed.

@qodo-code-review

qodo-code-review Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 🔗 Cross-repo conflicts (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Snapshot reviewers show wrong branch 🐞 Bug ≡ Correctness
Description
ResolveBranch calls GitRepository.CurrentBranch(WorktreePath), but UpdateFacts assigns
WorktreePath from CheckoutLabel.CheckoutPathFor(dto), which prefers BorrowedFrom. For snapshot
reviewers, BorrowedFrom is the source checkout while the agent runs in its separate
dto.WorktreePath, so the Session pane reads and displays the source branch after a switch instead
of the running agent’s branch.
Code

src/Capacitor.App/ViewModels/WorkContextViewModel.cs[R182-183]

+        var branch = GitRepository.CurrentBranch(WorktreePath) ?? _reportedBranch;
+        Branch = string.IsNullOrWhiteSpace(branch) ? "—" : branch;
Relevance

●●● Strong

Branch resolution must use the running snapshot worktree, not the borrowed source checkout.

PR-#766
PR-#653

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new resolver reads from the WorktreePath property, while the existing checkout helper
explicitly prefers the borrowed source path. The daemon distinguishes a snapshot reviewer’s running
worktree from its borrowed source, so these values are different in the affected case.

src/Capacitor.App/ViewModels/WorkContextViewModel.cs[179-184]
src/Capacitor.App/ViewModels/WorkContextViewModel.cs[299-305]
src/Capacitor.App/ViewModels/TrayModels.cs[17-20]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[25-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

Issue description
`ResolveBranch` reads `HEAD` through the UI-facing `WorktreePath`, which intentionally prefers `BorrowedFrom` for borrowed snapshot reviewers. That path can differ from the checkout in which the agent runs, producing an incorrect branch display.

Fix Focus Areas
- src/Capacitor.App/ViewModels/WorkContextViewModel.cs[179-184]
- src/Capacitor.App/ViewModels/WorkContextViewModel.cs[301-305]

Recommended Fix
Keep the actual agent worktree path separately from the presentation/grouping checkout path, and pass the actual `dto.WorktreePath` to `GitRepository.CurrentBranch`. Continue using `CheckoutLabel.CheckoutPathFor(dto)` only for the displayed worktree label and grouping.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. New reviewers appear re-requested 🔗 Cross-repo conflict ≡ Correctness
Description
ReviewRowStatus treats only a null review state as awaiting, while ReRequested treats every
non-null state as evidence of an earlier review. The server sends review_state: "unknown" with no
submission time for a newly requested reviewer, so these reviewers display as unknown and
re-requested instead of awaiting review.
Code

src/Capacitor.App/ViewModels/PullRequestContextViewModel.Projections.cs[R153-154]

+        null when requested => new("Awaiting review", Detail: "Review requested; this reviewer has not submitted one yet."),
+        _ => new("Unknown", Detail: "GitHub did not report this reviewer's state."),
Relevance

●●● Strong

Server payload semantics support the misclassification; reviewer-state handling should distinguish
unknown from no prior submission.

PR-#1029

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR interprets a requested reviewer as awaiting only when ReviewState is null and marks any
requested reviewer with a non-null state as re-requested. The server explicitly emits `ReviewState =
"unknown"` for requested reviewers with no prior review, while reviewed users carry a submission
timestamp, proving that the new client logic misclassifies the server’s normal payload.

kcap-cli -> kcap-server
src/Capacitor.App/ViewModels/PullRequestContextViewModel.Projections.cs[135-154]
External repo: kurrent-io/kcap-server, src/Capacitor.Api.Public/PullRequests/GitHubPullRequestProvider.Reviewers.cs [11-27]
External repo: kurrent-io/kcap-server, src/Capacitor.Api.Public.Abstractions/PullRequests/PullRequestReviewerDto.cs [13-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The server represents a newly requested reviewer with `Requested = true`, `ReviewState = "unknown"`, and no `SubmittedAt`, but the desktop reader interprets that combination as an unknown, re-requested review.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/PullRequestContextViewModel.Projections.cs[135-154]
- test/Capacitor.App.Tests.Unit/PullRequestPresentationTests.cs[96-127]

## Recommended Fix
Use `SubmittedAt` to distinguish initial requests from re-requests. When `Requested` is true and `SubmittedAt` is null, show `Awaiting review` and keep `ReRequested` false; only mark a request as re-requested when a prior submitted review exists, and update the fake-data test to use the server’s `"unknown"` state.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Borrowed reviewers miss pull requests ✓ Resolved 🐞 Bug ≡ Correctness
Description
PullRequestContextViewModel stores dto.WorktreePath instead of the presented checkout selected
by CheckoutLabel.CheckoutPathFor. For independent borrowed reviewers, that path is the branchless
private snapshot rather than BorrowedFrom, so live discovery receives no branch and cannot find a
pull request opened on the reviewed checkout.
Code

src/Capacitor.App/ViewModels/PullRequestContextViewModel.cs[154]

+            _worktree = dto.WorktreePath;
Relevance

●●● Strong

Using the runtime path bypasses borrowed-checkout selection and can prevent discovery on the
reviewed branch.

PR-#653
PR-#766

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The status contract distinguishes the runtime worktree from the checkout a borrowed reviewer
reviews, and the existing checkout helper deliberately prefers BorrowedFrom. The new assignment
bypasses that helper, while pull-request discovery only invokes providers when it receives a
non-empty branch.

src/Capacitor.App/ViewModels/PullRequestContextViewModel.cs[151-154]
src/Capacitor.App/ViewModels/TrayModels.cs[14-20]
src/Capacitor.Cli.Core/LocalIpc/StatusIpc.cs[65-80]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[25-35]
src/Capacitor.Cli.Core/PullRequests/Readers/PullRequestReaderRegistry.cs[57-63]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Pull-request discovery reads HEAD from `dto.WorktreePath`, which is a private snapshot for independent borrowed reviewers rather than the checkout being reviewed.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/PullRequestContextViewModel.cs[154-154]

## Recommended Fix
Set `_worktree` using `CheckoutLabel.CheckoutPathFor(dto)` so borrowed sessions prefer `BorrowedFrom`, and add a discovery test where `WorktreePath` is a branchless snapshot while `BorrowedFrom` has the active branch.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Repeated head changes stop threads ✓ Resolved 🐞 Bug ≡ Correctness
Description
Page clears _headRestarted only after a successful checks read even though the generic restart
branch also handles thread reads. After threads recover from one head change, a later paginated read
on another new head skips recovery, clears the rows, and stops the section with a checks-specific
error.
Code

src/Capacitor.App/ViewModels/PullRequestContextViewModel.Reads.cs[140]

+                    if (section == "checks") _headRestarted = false;
Relevance

●●● Strong

Global restart state is cleared only for checks, leaving thread recovery broken on later head
changes.

PR-#979
PR-#730

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
All sections share the generic page completion handler, while the GitHub thread reader specifically
returns head_changed when a cursor's recorded head no longer matches. The first restart sets the
global latch, but a successful replacement threads page cannot clear it because the new reset is
restricted to checks; the second restart therefore enters the stopped-state branch.

src/Capacitor.App/ViewModels/PullRequestContextViewModel.Reads.cs[117-140]
src/Capacitor.App/ViewModels/PullRequestContextViewModel.Reads.cs[148-159]
src/Capacitor.Cli.Core/PullRequests/Readers/GitHubCli/GitHubCliReaderProvider.cs[141-157]
src/Capacitor.App/ViewModels/PullRequestContextViewModel.cs[299-304]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The shared head-restart latch remains set after a recovered threads page because only successful checks pages clear it, preventing subsequent thread head changes from recovering.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/PullRequestContextViewModel.Reads.cs[140-140]
- src/Capacitor.App/ViewModels/PullRequestContextViewModel.Reads.cs[148-159]

## Recommended Fix
Track head-change recovery by section key and clear the matching latch when that section successfully reloads. Add a test that pages threads across two successive pull-request head changes and verifies both recover automatically.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Hidden markers keep animation running 🐞 Bug ➹ Performance
Description
PulseClock.Start retains every attached active visual and starts its timer without checking
effective visibility, while Stop runs only when the visual detaches or IsActive becomes false. A
pending marker under a hidden reader or other hidden ancestor remains attached and active, so the
dispatcher continues updating it every 50 ms and cannot stop the shared timer even when no marker is
on screen.
Code

src/Capacitor.App/Views/PulseClock.cs[R25-26]

+                visual.DetachedFromVisualTree += OnDetached;
+                if (TopLevel.GetTopLevel(visual) is not null) Start(visual);
Relevance

●●● Strong

Active hidden visuals can retain the shared timer; visibility-aware cleanup prevents unnecessary
periodic work.

PR-#1030
PR-#985

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new clock’s active set is strong-reference-backed and is only changed by attachment, detachment,
or attached-property changes; it has no effective-visibility handling. The reader uses IsVisible
extensively for state transitions, which does not require its child visuals to detach.

src/Capacitor.App/Views/PulseClock.cs[21-31]
src/Capacitor.App/Views/PulseClock.cs[48-65]
src/Capacitor.App/Views/PullRequestReader.axaml[60-75]
src/Capacitor.App/Views/PullRequestReader.axaml[87-94]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

Issue description
`PulseClock` treats visual-tree attachment as visibility and retains hidden active markers in its global set. Hidden controls remain attached when an ancestor's `IsVisible` changes, so they keep the 20 Hz timer alive and receive opacity updates.

Fix Focus Areas
- src/Capacitor.App/Views/PulseClock.cs[21-31]
- src/Capacitor.App/Views/PulseClock.cs[48-65]

Recommended Fix
Track effective-visibility changes in addition to attachment and `IsActive`, adding a visual to `Active` only while it is effectively visible and removing it when it becomes hidden. Start or stop the shared timer after each membership change, and preserve opacity clearing when a marker stops pulsing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. A test comment narrates old behavior ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The documentation above A_refresh_click_shows_progress_at_once_and_runs_within_seconds says the
click used to wait it out, recording change history instead of the current immediate-progress
invariant. Because it describes only a past symptom, later readers cannot tell which refresh
behavior the test is intended to preserve.
Code

test/Capacitor.App.Tests.Unit/PullRequestPresentationTests.cs[237]

+    /// A click inside the poll's gap used to wait it out with nothing on screen.
Relevance

●●● Strong

Recent precedents accept rewriting historical comments into current behavioral invariants.

PR-#1029
PR-#1030
PR-#666

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added comment explicitly uses the historical phrase used to. Rule 2897915 prohibits
change-history narration in comments, while rule 2762993 requires comments to express a current,
non-obvious behavioral constraint rather than historical context.

Rule 2897915: Avoid time-sensitive or process-reference metadata in code comments
Rule 2762993: Restrict comments to documenting non-obvious, behavior‑critical constraints
test/Capacitor.App.Tests.Unit/PullRequestPresentationTests.cs[237-237]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test comment narrates how refresh previously behaved rather than documenting the current behavior-critical constraint.

## Fix Focus Areas
- test/Capacitor.App.Tests.Unit/PullRequestPresentationTests.cs[237-237]

## Recommended Fix
Rewrite the comment to state that a manual refresh must show progress immediately and execute within the shorter manual-refresh interval, without referring to historical behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Cross-repo context — repo relationships
  Explored: repo: kurrent-io/kcap-server (sha: cc5bcc9a) — View relationship
Review mode: 🧠 Deep: This is a broad, behavior-heavy desktop/UI and data-refresh change spanning many independent code paths, including branch discovery, asynchronous polling/recovery, rendering, remote image loading, and public interaction behavior.

Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread test/Capacitor.App.Tests.Unit/PullRequestPresentationTests.cs Outdated
Comment thread src/Capacitor.App/ViewModels/PullRequestContextViewModel.cs Outdated
Comment thread src/Capacitor.App/ViewModels/PullRequestContextViewModel.Reads.cs Outdated
Comment on lines +182 to +183
var branch = GitRepository.CurrentBranch(WorktreePath) ?? _reportedBranch;
Branch = string.IsNullOrWhiteSpace(branch) ? "—" : branch;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Snapshot reviewers show wrong branch 🐞 Bug ≡ Correctness

ResolveBranch calls GitRepository.CurrentBranch(WorktreePath), but UpdateFacts assigns
WorktreePath from CheckoutLabel.CheckoutPathFor(dto), which prefers BorrowedFrom. For snapshot
reviewers, BorrowedFrom is the source checkout while the agent runs in its separate
dto.WorktreePath, so the Session pane reads and displays the source branch after a switch instead
of the running agent’s branch.
Agent Prompt
Issue description
`ResolveBranch` reads `HEAD` through the UI-facing `WorktreePath`, which intentionally prefers `BorrowedFrom` for borrowed snapshot reviewers. That path can differ from the checkout in which the agent runs, producing an incorrect branch display.

Fix Focus Areas
- src/Capacitor.App/ViewModels/WorkContextViewModel.cs[179-184]
- src/Capacitor.App/ViewModels/WorkContextViewModel.cs[301-305]

Recommended Fix
Keep the actual agent worktree path separately from the presentation/grouping checkout path, and pass the actual `dto.WorktreePath` to `GitRepository.CurrentBranch`. Continue using `CheckoutLabel.CheckoutPathFor(dto)` only for the displayed worktree label and grouping.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The branch fact stays on the checkout the pane names. CheckoutLabel.CheckoutPathFor prefers BorrowedFrom, and a borrowed snapshot is detached, so reading dto.WorktreePath would show no branch. Discovery now uses that same presented checkout.

Comment on lines +25 to +26
visual.DetachedFromVisualTree += OnDetached;
if (TopLevel.GetTopLevel(visual) is not null) Start(visual);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

6. Hidden markers keep animation running 🐞 Bug ➹ Performance

PulseClock.Start retains every attached active visual and starts its timer without checking
effective visibility, while Stop runs only when the visual detaches or IsActive becomes false. A
pending marker under a hidden reader or other hidden ancestor remains attached and active, so the
dispatcher continues updating it every 50 ms and cannot stop the shared timer even when no marker is
on screen.
Agent Prompt
Issue description
`PulseClock` treats visual-tree attachment as visibility and retains hidden active markers in its global set. Hidden controls remain attached when an ancestor's `IsVisible` changes, so they keep the 20 Hz timer alive and receive opacity updates.

Fix Focus Areas
- src/Capacitor.App/Views/PulseClock.cs[21-31]
- src/Capacitor.App/Views/PulseClock.cs[48-65]

Recommended Fix
Track effective-visibility changes in addition to attachment and `IsActive`, adding a visual to `Active` only while it is effectively visible and removing it when it becomes hidden. Start or stop the shared timer after each membership change, and preserve opacity clearing when a marker stops pulsing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A hidden reader stays attached, and a pending marker on it remains in the clock. The work-context card shows the same marker and is the one on screen, which is why the timer runs. When checks are no longer pending, IsActive goes false and the clock stops. Effective visibility would also drop the card's marker whenever that card is covered.

Comment on lines +153 to +154
null when requested => new("Awaiting review", Detail: "Review requested; this reviewer has not submitted one yet."),
_ => new("Unknown", Detail: "GitHub did not report this reviewer's state."),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. New reviewers appear re-requested 🔗 Cross-repo conflict ≡ Correctness

ReviewRowStatus treats only a null review state as awaiting, while ReRequested treats every
non-null state as evidence of an earlier review. The server sends review_state: "unknown" with no
submission time for a newly requested reviewer, so these reviewers display as unknown and
re-requested instead of awaiting review.
Agent Prompt
## Issue description
The server represents a newly requested reviewer with `Requested = true`, `ReviewState = "unknown"`, and no `SubmittedAt`, but the desktop reader interprets that combination as an unknown, re-requested review.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/PullRequestContextViewModel.Projections.cs[135-154]
- test/Capacitor.App.Tests.Unit/PullRequestPresentationTests.cs[96-127]

## Recommended Fix
Use `SubmittedAt` to distinguish initial requests from re-requests. When `Requested` is true and `SubmittedAt` is null, show `Awaiting review` and keep `ReRequested` false; only mark a request as re-requested when a prior submitted review exists, and update the fake-data test to use the server’s `"unknown"` state.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A newly requested reviewer from the local reader has ReviewState null. That already reads as Awaiting review, and re-requested stays off. ReviewState is set only from an actual GitHub review state. This client does not send "unknown" for that row; A_requested_reviewer_without_a_review_reads_as_awaiting pins the null case.

A borrowed snapshot is detached, so discovery reads the reviewed checkout.
Head recovery is per section, because threads are what report a moved head.
@alexeyzimarev
alexeyzimarev merged commit d5f07d2 into main Sep 25, 2026
8 checks passed
@alexeyzimarev
alexeyzimarev deleted the norton/ai-3216-simplify-pr-reader branch September 25, 2026 12:52
alexeyzimarev pushed a commit that referenced this pull request Sep 25, 2026
#1157 — AI-3216

#1157 is already closed by #1158, so this line has no closing keyword.

## What & why

Continuation of #1158, which merged before the reader spacing was
finished. Lists sit on the section edge. Only Overview, which has no row
padding, keeps a small top inset. The Reviewers, Submitted, and Threads
row keeps a little space under the pills, and the loading notice sizes
to its text.

## Where to look

The review row's bottom margin is the gap under the pills. The scroll
area has no top inset except the overview block.

## Visuals
<img width="1819" height="1110" alt="Screenshot 2026-09-25 at 18 26 42"
src="https://github.com/user-attachments/assets/048e51cc-4c1f-4c41-9205-ae4cac8d7322"
/>
<img width="1819" height="1110" alt="Screenshot 2026-09-25 at 18 26 40"
src="https://github.com/user-attachments/assets/bca18ff4-0b04-4c16-a8f9-4b2d7e0ee1bb"
/>
<img width="1819" height="1110" alt="Screenshot 2026-09-25 at 18 26 38"
src="https://github.com/user-attachments/assets/c53cbae9-34af-40fe-858a-bdaf64cec93d"
/>
<img width="1819" height="1110" alt="Screenshot 2026-09-25 at 18 26 36"
src="https://github.com/user-attachments/assets/7265013f-2071-4583-a8d6-af903dd583fb"
/>
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.

Simplify the pull request reader in the desktop app

2 participants