Skip to content

feat(pr-review): server inbox, honest recents, and stale-submit recovery (w5a) - #5362

Open
iscekic wants to merge 13 commits into
mainfrom
audit-w5a-pr-review-ae21
Open

feat(pr-review): server inbox, honest recents, and stale-submit recovery (w5a)#5362
iscekic wants to merge 13 commits into
mainfrom
audit-w5a-pr-review-ae21

Conversation

@iscekic

@iscekic iscekic commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • The PR Review home now lists pull requests that are waiting for your review, loaded from the server with paging. You can still open any pull request by pasting its link.
  • If a pull request changes after you queue comments, the outdated comments are marked and stay in your queue so you can edit or remove them.
  • When you submit a review, only the comments that still match the current pull request are sent. Outdated comments stay in your queue and are not sent.
  • The file navigator now loads files as you scroll instead of loading every file up front, so large pull requests open faster.
  • The organization Members list now scrolls smoothly for large organizations.
  • Recent pull requests are saved only after they open successfully. A failed entry shows a Retry button, and every entry has a Remove button.
  • Diff context, file content, and viewed-file state are cached during review, so revisiting files is faster.
  • The pull request overview and its checks now load their parts at the same time, so the screen appears sooner.

Reviewer Notes

The PR review entry screen now shows a server-backed inbox of pull requests requesting the viewer's review, paginated through a new listInbox query. The search string is fixed server-side, so a caller can only ever see pull requests their own GitHub token can already see. Direct URL open is unchanged, and pagination is capped at 20 pages to stay inside GraphQL search rate limits.

Files
  • apps/web/src/lib/github-pr-review/mappers.ts — adds buildInboxResult and the GraphQlInboxNode type; drops non-PR search nodes.
  • apps/web/src/lib/github-pr-review/dtos.ts — adds the inbox item and result schemas plus INBOX_PAGE_SIZE.
  • apps/mobile/src/lib/pr-review/use-pr-inbox.ts — infinite-query hook; 20-page cap and 30-second stale time.
  • apps/mobile/src/components/pr-review/pr-review-inbox-list.tsx — FlashList body composing the paste header, inbox rows, and recents footer.
  • apps/mobile/src/components/pr-review/pr-review-inbox-view.ts — pure state selector for the seven inbox states.
  • apps/mobile/src/components/pr-review/pr-review-entry-screen.tsx — mounts the inbox list; renders the recents Retry and Remove buttons.
  • apps/mobile/src/components/ui/icons.ts — adds the Inbox icon.

The overview and check queries now start their sub-requests in parallel and settle them with Promise.allSettled, rethrowing the first rejection in leg order. This removes the sequential round-trips that previously serialized the overview's REST and GraphQL legs and the checks' two paginations.

Files
  • apps/web/src/routers/github-pr-review-router.ts — adds listInbox, INBOX_QUERY, and INBOX_SEARCH_QUERY; parallelizes getPullRequest and listChecks.

The submit sheet now partitions queued comments into fresh and stale by comparing each comment's recorded commit SHA against the current head. Only fresh comments are sent in the batched submit; stale comments stay queued, are flagged outdated, and the sheet reports a partial result instead of dismissing. This replaces the previous behavior of submitting everything against the latest head and surfacing per-item 422s.

Files
  • apps/mobile/src/lib/pr-review/partition-pending-items.ts — splits the queue into fresh and stale; an empty head makes every item fresh.
  • apps/mobile/src/components/pr-review/pr-review-submit.tsx — submits fresh items only, removes them via removeComments, and shows the partial result.
  • apps/mobile/src/components/pr-review/pr-review-submit-view.ts — CTA label and partial-result copy selectors.
  • apps/mobile/src/components/pr-review/pr-review-pending-comment-row.tsx — stale flag, Outdated badge, and updated hint copy.
  • apps/mobile/src/lib/pr-review/build-submit-review-input.ts — documents the fresh-only submission contract.
  • apps/mobile/src/lib/pr-review/pending-review-provider.tsx — adds removeComments with the hydration-generation guard.

Recent PRs now carry a last-load outcome and are written only after the first authorized payload loads; a failed open marks an existing entry failed rather than creating one. The entry screen shows a Retry button for failed entries and a Remove button for every entry.

Files
  • apps/mobile/src/lib/pr-review/recent-prs.ts — adds lastResult, removeRecentPr, and markRecentPrFailed.
  • apps/mobile/src/components/pr-review/pr-review-screen.tsx — backfills with lastResult: 'ok'; marks an existing entry failed on error.
  • apps/mobile/src/components/pr-review/recent-pr-row-state.ts — row-state selector for title, identity, and failed marker.

The file navigator now renders file rows in a virtualized list and loads pages on scroll instead of fetching the full file set on mount. An active search still drives fetch-to-completion so filtering covers the full listed set. The memoized rows read the latest select and toggle handlers through refs, so the rows stay identity-stable across a search re-render yet never go stale when a head-SHA change swaps the toggle handler.

Files
  • apps/mobile/src/components/pr-review/diff/pr-diff-file-navigator.tsx — FlashList with onEndReached and memoized rows; per-path callbacks read the latest handlers through refs.
  • apps/mobile/src/lib/pr-review/diff/navigator-file-filter.ts — extracted path filter and load-all decision.

The organization Members screen now renders a single virtualized list built from a pure item builder, with loading, error, and empty states selected by a dedicated error-view selector. Invited members now render alongside active members in the same list.

Files
  • apps/mobile/src/components/organization/members-screen.tsx — FlashList with section, member, and invite items.
  • apps/mobile/src/components/organization/members-list-items.ts — item builder with exact composition rules.
  • apps/mobile/src/components/organization/org-list-error-view.ts — error-view selector; no reconnect state.

Diff context expansion now fetches through the React Query cache with a 5-minute stale time and 10-minute GC time instead of a raw tRPC client call, so repeated context loads reuse cached lines. Viewed-file state adds a process-lifetime cache with a generation guard; when a read observes a generation change, it returns the authoritative current map rather than the stale parse, so a toggle or clear never races a re-read.

Files
  • apps/mobile/src/lib/pr-review/diff/use-pr-diff-context-loader.tsqueryClient.fetchQuery with stale and GC times.
  • apps/mobile/src/lib/pr-review/diff/context-window.ts — extracted window arithmetic.
  • apps/mobile/src/lib/pr-review/viewed-files.ts — cached map plus a generation guard; the guard now protects the caller's return value, not only the cache.

The Discussion tab's state selection is extracted into a pure selectDiscussionTabView function so the seven outcomes can be unit-tested. The tab renders exactly what it rendered before; only the decision is moved.

Files
  • apps/mobile/src/components/pr-review/pr-review-discussion-tab-view.ts — pure state selector for the seven tab states.
  • apps/mobile/src/components/pr-review/pr-review-discussion-tab.tsx — uses the selector instead of inline branches.

Satisfied on main, not reassigned and not reimplemented: P1-F-45, P2-GH-52d, P1-F-46c, P2-E-30a, P2-E-33b.

Two assumptions were corrected during the build:

  1. The server does not fence submitReview on expectedHeadSha, and it does not return stale_head in the review path; stale_head exists only in the merge reconcile path. Stale handling is solved entirely on the client.
  2. The highlight module already has a bounded content-addressed LRU cache (5,000 entries, keyed by language and line text). No highlight change was made.

Tests: 20 test files added or updated.
Generated: none.


Verification

  • No manual test paths were recorded for this change; verification relies on the automated test suite added in this branch.

Visual Changes

PR Review entry screen with the server-backed inbox

entry-screen.png

Submit sheet showing an outdated comment kept in the queue

now.png

Human steps: none known.

Notes: none.

Add , a server-fixed GraphQL search over review-requested
open PRs, so the mobile inbox reads only PRs the caller's token can see.
 drops non-PR and identity-less nodes instead of
throwing, and the schema guard now validates the inbox query.
Start the pulls, repos, and GraphQL legs of getPullRequest, and the two
paginate legs of listChecks, before awaiting. Await with Promise.allSettled
and rethrow the first rejection in leg order, so a second failure never
becomes an unhandled rejection. Authorization stays inside the token retry.
Route context expansion through the react-query cache so re-expanding a gap
is a hit, and hold the parsed viewed-files map in a process cache that a
generation guard keeps from being overwritten by a late first read. Sign-out
still drops the private content.
Replace the entry screen's ScrollView with a single FlashList whose header
and footer stay mounted in every inbox state, so direct URL open and Recents
survive an inbox failure. Recents now write only after the first authorized
payload, and failed entries expose Retry and Remove.
Partition queued comments by head SHA and send only the fresh ones, so a
stale item never blocks the valid ones. Stale items stay queued with an
Outdated badge and edit/delete, and removeComments carries the hydration
guard so an in-flight loadDraft cannot re-add just-posted comments.
Replace the ScrollView with a FlashList and page on scroll, so a large PR
does not mount every file row. A search still fetches the full listed set,
and a failed next page shows a retry CTA.
Replace the members ScrollView with a FlashList and drive it from a pure
item builder, with loading/error/empty evaluated in that order so an org
error never renders No members yet. Extract the discussion tab's state
selection into a pure, tested selector.
Use the repo's deferred-promise pattern (undefined as unknown as) and
mockReturnValueOnce so the two late-read tests pass oxlint.
The mapper receives only fresh items; stale items are never sent.
@iscekic iscekic self-assigned this Aug 19, 2026
Comment thread apps/mobile/src/lib/pr-review/viewed-files.ts
Comment thread apps/mobile/src/components/pr-review/diff/pr-diff-file-navigator.tsx Outdated
@kilo-code-bot

kilo-code-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (4 files)
  • apps/mobile/src/components/pr-review/diff/pr-diff-file-navigator.test.tsx
  • apps/mobile/src/components/pr-review/diff/pr-diff-file-navigator.tsx
  • apps/mobile/src/lib/pr-review/viewed-files.test.ts
  • apps/mobile/src/lib/pr-review/viewed-files.ts
Previous Review Summaries (2 snapshots, latest commit c48dcae)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit c48dcae)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/pr-review/viewed-files.ts 102 Generation guard still returns a stale parse to the caller
apps/mobile/src/components/pr-review/diff/pr-diff-file-navigator.tsx 161 Cached row callbacks never update when handlers change
Files Reviewed (5 files)
  • apps/mobile/src/components/pr-review/diff/pr-diff-file-navigator.tsx - 1 issue
  • apps/mobile/src/components/pr-review/pr-review-inbox-view.ts
  • apps/mobile/src/lib/pr-review/diff/navigator-file-filter.test.ts
  • apps/mobile/src/lib/pr-review/diff/navigator-file-filter.ts
  • apps/mobile/src/lib/pr-review/viewed-files.test.ts

Fix these issues in Kilo Cloud

Previous review (commit dddb570)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/pr-review/viewed-files.ts 102 Generation guard still returns a stale parse to the caller
apps/mobile/src/components/pr-review/diff/pr-diff-file-navigator.tsx 164 Cached row callbacks never update when handlers change
Files Reviewed (47 files)
  • apps/mobile/src/components/organization/members-list-items.test.ts
  • apps/mobile/src/components/organization/members-list-items.ts
  • apps/mobile/src/components/organization/members-screen.mounted.test.tsx
  • apps/mobile/src/components/organization/members-screen.tsx
  • apps/mobile/src/components/organization/org-list-error-view.test.ts
  • apps/mobile/src/components/organization/org-list-error-view.ts
  • apps/mobile/src/components/pr-review/diff/pr-diff-file-navigator.test.tsx
  • apps/mobile/src/components/pr-review/diff/pr-diff-file-navigator.tsx - 1 issue
  • apps/mobile/src/components/pr-review/pr-review-discussion-tab-view.test.ts
  • apps/mobile/src/components/pr-review/pr-review-discussion-tab-view.ts
  • apps/mobile/src/components/pr-review/pr-review-discussion-tab.tsx
  • apps/mobile/src/components/pr-review/pr-review-entry-screen.tsx
  • apps/mobile/src/components/pr-review/pr-review-inbox-list.tsx
  • apps/mobile/src/components/pr-review/pr-review-inbox-view.test.ts
  • apps/mobile/src/components/pr-review/pr-review-inbox-view.ts
  • apps/mobile/src/components/pr-review/pr-review-pending-comment-row.tsx
  • apps/mobile/src/components/pr-review/pr-review-screen.tsx
  • apps/mobile/src/components/pr-review/pr-review-submit-view.test.ts
  • apps/mobile/src/components/pr-review/pr-review-submit-view.ts
  • apps/mobile/src/components/pr-review/pr-review-submit.test.tsx
  • apps/mobile/src/components/pr-review/pr-review-submit.tsx
  • apps/mobile/src/components/pr-review/recent-pr-row-state.test.ts
  • apps/mobile/src/components/pr-review/recent-pr-row-state.ts
  • apps/mobile/src/components/ui/icons.ts
  • apps/mobile/src/lib/pr-review/build-submit-review-input.test.ts
  • apps/mobile/src/lib/pr-review/build-submit-review-input.ts
  • apps/mobile/src/lib/pr-review/diff/context-window.test.ts
  • apps/mobile/src/lib/pr-review/diff/context-window.ts
  • apps/mobile/src/lib/pr-review/diff/navigator-file-filter.test.ts
  • apps/mobile/src/lib/pr-review/diff/navigator-file-filter.ts
  • apps/mobile/src/lib/pr-review/diff/use-pr-diff-context-loader.mounted.test.tsx
  • apps/mobile/src/lib/pr-review/diff/use-pr-diff-context-loader.ts
  • apps/mobile/src/lib/pr-review/partition-pending-items.test.ts
  • apps/mobile/src/lib/pr-review/partition-pending-items.ts
  • apps/mobile/src/lib/pr-review/pending-review-provider.mounted.test.tsx
  • apps/mobile/src/lib/pr-review/pending-review-provider.tsx
  • apps/mobile/src/lib/pr-review/recent-prs.test.ts
  • apps/mobile/src/lib/pr-review/recent-prs.ts
  • apps/mobile/src/lib/pr-review/use-pr-inbox.test.ts
  • apps/mobile/src/lib/pr-review/use-pr-inbox.ts
  • apps/mobile/src/lib/pr-review/viewed-files.test.ts
  • apps/mobile/src/lib/pr-review/viewed-files.ts - 1 issue
  • apps/web/src/lib/github-pr-review/dtos.ts
  • apps/web/src/lib/github-pr-review/mappers.ts
  • apps/web/src/routers/github-pr-review-graphql-schema.test.ts
  • apps/web/src/routers/github-pr-review-router.test.ts
  • apps/web/src/routers/github-pr-review-router.ts

Fix these issues in Kilo Cloud


Reviewed by grok-4.6 · Input: 100.5K · Output: 12.2K · Cached: 337.7K

Review guidance: REVIEW.md from base branch main

@iscekic iscekic added the human-ready The PR is ready for human review. label Aug 19, 2026
@iscekic iscekic changed the title feat(pr-review): server inbox, honest recents, and stale-submit recovery feat(pr-review): server inbox, honest recents, and stale-submit recovery (w5c) Aug 19, 2026
@iscekic iscekic changed the title feat(pr-review): server inbox, honest recents, and stale-submit recovery (w5c) feat(pr-review): server inbox, honest recents, and stale-submit recovery (w5a) Aug 19, 2026
@iscekic
iscekic requested a review from eshurakov August 19, 2026 13:57

@pandemicsyn pandemicsyn left a comment

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.

lgtm, couple bot notes inline and some merge conflicts but preapproved

// items persist even when the invalidated load never resolves. The
// persistence effect writes the remainder (an empty remainder clears the
// stored entry through its empty-state branch), so no direct clearDraft.
hydrationGenerationRef.current += 1;

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.

bot: A successful Approve before draft hydration finishes can erase an existing persisted pending-comment queue.

Before hydration, APPROVE submits with the empty in-memory queue, then calls removeComments([]). That still invalidates the pending load and marks the provider hydrated, so the empty-state persistence effect clears the stored draft and the delayed load is discarded.

Suggested fix: Gate submission until hydration settles, or make an empty removeComments([]) a true no-op. The latter must skip both the hydration-generation increment and setHydrated(true); skipping only the generation bump still enables the empty-state clearDraft. Add a mounted regression test with deferred loadDraft, persisted items, and an immediate APPROVE; resolving the load must retain the queue and must not clear its stored draft.

!fetchAll.isRunning &&
!fetchAll.error
) {
void runRef.current();

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.

bot: An active search can repeatedly request a failed next page without showing a retry.

fetchNextPage() can resolve to an error-state result while retaining prior pages and hasNextPage: true. useFetchToCompletion.run() only stops for missing data or no next page, so it requests that cursor again. Because this does not enter its catch, fetchAll.error remains unset; the normal later-page retry is hidden while search is active.

Suggested fix: Inspect the resolved observer result for an error state and route it through the existing error path (for example, throw result.error before evaluating hasNextPage) so the loop stops and the load-all retry renders. Add coverage using a resolved error result with prior pages and hasNextPage: true: it should make one request, stop, and show the load-all retry.


export async function clearViewedFiles(): Promise<void> {
await deleteAccountMetadata(PR_REVIEW_VIEWED_KEY);
cachedMap = 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.

bot: A populated viewed-file cache remains readable while sign-out deletion is in flight.

clearViewedFiles awaits deleteAccountMetadata before invalidating cachedMap, so a concurrent getViewedFiles can return the prior account's paths. The existing generation guard only protects reads already in flight when invalidation occurs.

Suggested fix: Add a clearing-in-progress fence: invalidate the cache/generation before deletion, and ensure reads during the awaited deletion return or await an empty authoritative state without publishing a fresh SecureStore read of the not-yet-deleted value. Add a test with a populated cache and delayed deletion; a concurrent read during the delay must never return the prior paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants