Skip to content

Filter auto-routed review requests (CODEOWNERS-style)#44

Merged
AntonNiklasson merged 5 commits into
mainfrom
claude/filter-code-owners-reviews-jua5c
May 19, 2026
Merged

Filter auto-routed review requests (CODEOWNERS-style)#44
AntonNiklasson merged 5 commits into
mainfrom
claude/filter-code-owners-reviews-jua5c

Conversation

@AntonNiklasson
Copy link
Copy Markdown
Owner

@AntonNiklasson AntonNiklasson commented May 18, 2026

Summary

Hide CODEOWNERS auto-attached review requests when the "Code owners" toggle in the Reviews column is off. Manual user and manual team requests stay visible.

Detection

A review_requested event counts as automatic when either:

  • the actor is a bot (type === "Bot" or login ends in [bot]), or
  • the actor is the PR author AND the event fires within 2s of PR creation.

This was reached empirically. Real timelines in sana-labs/sana-ai show two patterns:

  • A GitHub Action attribution: actor=github-actions[bot] (any timing).
  • Native CODEOWNERS attribution: actor=<PR author>, event timestamp exactly matches PR created_at.

Manual requests by the PR author (clicked later, including 13s after open) and any request by a non-author land in the "manual" bucket and stay visible.

Known limitation

CODEOWNERS can also re-evaluate on a push that adds owned files. The resulting event fires at push time (not PR creation), so we'd miss those (false negative). Detecting them would require pairing the request event with a nearby push event — not worth the complexity for now.

Caching

Each fresh fetch would otherwise add one timeline API call per review-requested PR. autoAssigned only changes when the PR changes (push, comment, review — all bump updated_at), so we cache {value, updatedAt} per (instance, repo, prNumber) and skip the timeline fetch when the cached updatedAt still matches. Transient timeline failures don't poison the cache.

Changes

  • Server (fetchers.ts): each review gets an autoAssigned flag (replaces the prior requestedDirectly). Adds one timeline fetch per review-requested PR, in parallel with the existing CI/reviews/PR calls. Cached per PR keyed on updated_at.
  • Web (filters.ts): atom renamed to showCodeOwnerRequestsAtom, new localStorage key, default true (same UX).
  • Web (App.tsx): filter predicate updated; switch label "Code owners" lives between the column header and sort dimensions. f keyboard shortcut toggles it (Reviews column only).
  • Dashboard polish (came up while wiring this): "My PRs" → "My work", "Review Requests" → "Reviews"; My work count now reflects open PRs only; SortControl renders just the active dimension.

Tests

Server suite covers the autoAssigned branches and the cache end-to-end (fetchers.integration.test.ts):

  • direct path: PR-author at creation, PR-author within 2s (boundary), PR-author >2s, PR-author much later, non-author human, Bot-type actor, [bot]-suffix actor without type, mixed events, empty timeline
  • team path: PR-author at creation, PR-author >2s, non-author human, Bot actor
  • fallbacks: timeline 404, event missing created_at
  • caching: hit short-circuits timeline fetch, miss refetches, failure doesn't write, miss-then-populate

107 server tests pass (was 91 on main).

Follow-up

Surfaced #46 — keyboard-shortcut scope is implicit today and the help overlay doesn't communicate it.

Test plan

  • pnpm typecheck, pnpm test, pnpm lint, pnpm fmt:check
  • Toggle in column header changes count against real data
  • f shortcut fires only when Reviews column is focused
  • Atom default is true (existing users see all reviews unchanged)
  • s still cycles sort dimensions

…ests

Adds a `requestedDirectly` signal — set from `requested_reviewers` on the PR —
and a Settings toggle to hide reviews where the user was only attached via team
membership. Closes the noisy-CODEOWNERS case from #42 at the cost of also
hiding manual team requests (the API doesn't distinguish them).
Copilot AI review requested due to automatic review settings May 18, 2026 19:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a requestedDirectly flag to review-request items so the UI can optionally hide review requests that were routed via team membership (common with CODEOWNERS), reducing noise while keeping explicitly requested reviews visible.

Changes:

  • Server: compute and return requestedDirectly in fetchReviews() based on whether the authenticated user is in requested_reviewers.
  • Web: persist a new “Hide requests routed via a team” setting and apply it when rendering the review list.
  • Tests/demo: add integration tests for fetchReviews() and update demo cache data with requestedDirectly.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/web/src/types.ts Extends ReviewRequest with optional requestedDirectly.
packages/web/src/filters.ts Adds hideTeamReviewRequestsAtom persisted via atomWithStorage.
packages/web/src/components/SettingsModal.tsx Adds a settings toggle UI for hiding team-routed review requests.
packages/web/src/App.tsx Applies the new filter when building the review-request list.
packages/server/src/fetchers.ts Computes and returns requestedDirectly in fetchReviews().
packages/server/src/fetchers.integration.test.ts Adds integration tests validating requestedDirectly behavior.
.cache-demo/data.json Updates demo review-request entries to include requestedDirectly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/server/src/fetchers.ts Outdated
Move the "show team-routed reviews" toggle out of the settings modal
into the Reviews column header, between the label and sort dimensions.
Default on; off filters CODEOWNERS-style auto-requests.

Also:
- `f` keyboard shortcut to toggle, gated to the Reviews column.
- Rename columns: "My PRs" → "My work", "Review Requests" → "Reviews".
- "My work" count shows open PRs only (Last-7-days list excluded from
  the badge; keyboard nav still spans both).
@AntonNiklasson AntonNiklasson changed the title Add requestedDirectly flag to filter team-routed review requests Filter auto-routed review requests (CODEOWNERS-style) May 18, 2026
…label

SortControl now renders only the active dimension (label + direction
arrow); s still cycles, click flips direction. Trims the visual noise
now that the column header carries more controls.

Also: switch reads "Auto requests" rather than "Show auto requests".
Copilot AI review requested due to automatic review settings May 19, 2026 06:38
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread packages/server/src/fetchers.ts Outdated
Comment thread packages/server/src/fetchers.integration.test.ts Outdated
The previous heuristic conflated "user in requested_reviewers" with
"asked of me directly" — it filtered out anyone routed via team
membership, including manual team requests. Replace it with a
timeline-based check that targets only automatic attachments:

- bot actor (login ends in [bot] or actor type is Bot), OR
- PR author as actor AND event fires within 2s of PR creation.

Manual requests (whether by the author themselves later, or by any
non-author at any time) stay visible.

API shape changes:
- field `requestedDirectly` -> `autoAssigned` (flipped meaning)
- atom `showTeamReviewRequestsAtom` -> `showCodeOwnerRequestsAtom`
  (new localStorage key, default true — same UX)
- switch label "Auto requests" -> "Code owners", tooltip rewritten

Adds one timeline API call per review-requested PR (in parallel with
the existing CI/reviews/PR fetches). Verified against real PRs in
sana-labs/sana-ai: github-actions[bot] and PR-author-at-creation
patterns both detected; manual late requests stay visible.

Tests cover bot actor (both type and [bot]-suffix detection), timing
boundaries (1.5s / 3s), team and direct paths, multi-event scenarios,
and fallback when the timeline fetch fails.
The timeline fetch is the one new API call this branch introduced.
Each review-requested PR triggers it on every fresh fetch even though
the auto/manual classification rarely changes — CODEOWNERS attaches at
PR creation, and the only way it'd change is a push touching newly-
owned files. Both kinds of activity bump the PR's updated_at, so we
can safely cache the result and refetch only when updated_at moves.

Cache value is {value, updatedAt}; on hit we return the cached value
and skip the timeline fetch entirely (Promise.resolve(null) inside the
parallel batch so the rest of the data still fetches in parallel).
We don't persist when timeline fetch fails — a transient 404 / throttle
shouldn't burn a "false" into the cache.

Adds 4 tests covering hit, miss, miss-after-failure, and miss-then-populate.
@AntonNiklasson AntonNiklasson merged commit 635e9da into main May 19, 2026
2 checks passed
@AntonNiklasson AntonNiklasson deleted the claude/filter-code-owners-reviews-jua5c branch May 19, 2026 13:06
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

packages/server/src/fetchers.ts:388

  • autoAssigned team-path detection treats any review_requested event that targets any team as relevant (requested_team != null). On PRs that have multiple requested teams, this can mark a review as auto-assigned due to an unrelated team’s event and incorrectly hide a manual request. Consider restricting matches to teams that are currently requested on the PR (e.g., compare ev.requested_team.slug against prData.requested_teams) before applying isAutoActor.
        const isAutoActor = (
          actor: { login?: string; type?: string } | null | undefined,
          eventCreatedAt?: string,
        ) => {
          if (!actor?.login) return false;
          if (actor.type === "Bot" || actor.login.endsWith("[bot]"))

Comment on lines +376 to +382
const timelineEvents = (timelineRes?.data ?? []) as Array<{
event?: string;
created_at?: string;
actor?: { login?: string; type?: string } | null;
requested_reviewer?: { login?: string } | null;
requested_team?: { slug?: string } | null;
}>;
@@ -12,39 +11,26 @@ export function SortControl<F extends string>({
value,
onChange,
}: Props<F>) {
["a", "Approve PR"],
["c", "Close PR"],
["e", "Dismiss review / notification"],
["f", "Toggle CODEOWNERS-attached reviews"],
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.

3 participants