feat: parallel review generation + improved onboarding#76
Conversation
After dismissing the first-run welcome hero, a dialog guides users through selecting repos to automatically watch. Selecting repos enables proactive mode and saves the watched repos list. Users can skip and configure later in Settings. Replay onboarding also re-triggers the repo setup step. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a maxPrsPerRepo preference that limits how many open PRs are auto-reviewed per watched repo. Shown as a dropdown in Settings under the proactive mode section. Defaults to 10 (down from the previous hardcoded 30). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a first-run onboarding dialog to help users pick watched repos immediately after auth, and introduces a new preference to cap how many open PRs per watched repo are auto-reviewed in proactive mode.
Changes:
- Introduces
OnboardingRepoSetupdialog for repo search + selection and wires it into first-run flow. - Adds
maxPrsPerRepoto preferences, a Settings control to edit it, and uses it when listing watched-repo PRs. - Updates proactive polling to respect the per-repo PR cap.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
src/pages/HomePage.tsx |
Opens the new repo-setup onboarding on first run and persists watched repos/proactive mode on completion. |
components/OnboardingRepoSetup.tsx |
New onboarding dialog: repo search autocomplete + chip selection + explanatory copy. |
lib/types.ts |
Extends Preferences with maxPrsPerRepo. |
src/main.ts |
Adds default maxPrsPerRepo and passes it to watched-repo PR listing. |
components/SettingsDialog.tsx |
Adds a dropdown to configure “Max PRs per repo” under proactive mode settings. |
| export function OnboardingRepoSetup({ open, onComplete, onSkip }: Props) { | ||
| const [query, setQuery] = useState(''); | ||
| const [suggestions, setSuggestions] = useState<RepoSearchResult[]>([]); | ||
| const [selectedRepos, setSelectedRepos] = useState<string[]>([]); | ||
| const [loading, setLoading] = useState(false); | ||
| const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
| const inputRef = useRef<HTMLInputElement>(null); | ||
|
|
There was a problem hiding this comment.
Because this component stays mounted and is controlled via the open prop, selectedRepos/query/suggestions will persist across closes/reopens. If the dialog can be reopened later (e.g. via a replay action), users may see stale selections. Consider resetting the local state when open transitions from false → true.
| const search = useCallback((q: string) => { | ||
| if (debounceRef.current) clearTimeout(debounceRef.current); | ||
| if (q.trim().length < 2) { | ||
| setSuggestions([]); | ||
| return; | ||
| } | ||
| setLoading(true); | ||
| debounceRef.current = setTimeout(() => { | ||
| void window.electronAPI.searchRepos(q.trim()).then((results) => { | ||
| setSuggestions(results.filter((r) => !selectedRepos.includes(r.fullName))); | ||
| setLoading(false); | ||
| }); | ||
| }, 300); | ||
| }, [selectedRepos]); |
There was a problem hiding this comment.
search() starts a debounced async request but doesn’t clean up the pending timer (or guard against an in-flight promise) when the component closes/unmounts. This can lead to state updates after unmount and leave loading stuck true. Consider clearing the timeout in an effect cleanup and using a cancelled/request-id guard; also ensure loading is reset in a .catch/.finally path.
| onKeyDown={(e) => { | ||
| if (e.key === 'Enter' && query.includes('/')) { | ||
| e.preventDefault(); | ||
| addRepo(query.trim()); | ||
| } | ||
| }} |
There was a problem hiding this comment.
The Enter-to-add path only checks query.includes('/'), so inputs like owner/repo/extra will be accepted and later parsed incorrectly (split('/') elsewhere only takes the first two segments). Validate the repo ref more strictly (e.g., exactly one / and no whitespace) before calling addRepo.
| debounceRef.current = setTimeout(() => { | ||
| void window.electronAPI.searchRepos(q.trim()).then((results) => { | ||
| setSuggestions(results.filter((r) => !selectedRepos.includes(r.fullName))); | ||
| setLoading(false); | ||
| }); |
There was a problem hiding this comment.
searchRepos(...).then(...) has no error handling. If the IPC call rejects (e.g. transient auth issue), this will produce an unhandled promise rejection and loading won’t be cleared. Add a .catch (or try/await inside the timeout) that sets loading false and clears suggestions as appropriate.
| debounceRef.current = setTimeout(() => { | |
| void window.electronAPI.searchRepos(q.trim()).then((results) => { | |
| setSuggestions(results.filter((r) => !selectedRepos.includes(r.fullName))); | |
| setLoading(false); | |
| }); | |
| debounceRef.current = setTimeout(async () => { | |
| try { | |
| const results = await window.electronAPI.searchRepos(q.trim()); | |
| setSuggestions(results.filter((r) => !selectedRepos.includes(r.fullName))); | |
| } catch { | |
| setSuggestions([]); | |
| } finally { | |
| setLoading(false); | |
| } |
| prefs.watchedRepos.map(async (repoRef) => { | ||
| const [owner, repo] = repoRef.split('/'); | ||
| if (!owner || !repo) return []; | ||
| return listRepoPullRequests(octokit, owner, repo); | ||
| return listRepoPullRequests(octokit, owner, repo, prefs.maxPrsPerRepo); | ||
| }) |
There was a problem hiding this comment.
prefs.maxPrsPerRepo is read from JSON and passed directly to Octokit as per_page. If the stored value is non-numeric, NaN, or outside GitHub’s supported range (1–100), watched-repo PR discovery will fail (silently, due to Promise.allSettled). Consider normalizing/clamping this value when loading preferences or right before calling listRepoPullRequests (e.g., default to 10 when invalid).
| if (!prefs.firstRunSeen) { | ||
| setFirstRunOpen(true); | ||
| setRepoSetupOpen(true); | ||
| } |
There was a problem hiding this comment.
PR description says “Replay first-time welcome” should re-trigger the repo setup dialog, but the first-run gate now opens OnboardingRepoSetup while the replay flow still appears to open the older welcome/about modal. Either update the replay handler to open the repo setup dialog (and re-run its first-run logic) or adjust the PR description/UI text so behavior matches.
| <div className="flex flex-col gap-0.5"> | ||
| <label className="text-sm font-medium text-foreground">Max PRs per repo</label> | ||
| <p className="text-xs text-muted-foreground"> | ||
| How many of the latest open PRs to review per watched repo. |
There was a problem hiding this comment.
The UI copy says “latest open PRs”, but the underlying GitHub API call is sorted by updated (most recently updated), not necessarily newest by creation time. Consider updating this description to avoid misleading users about which PRs get picked.
| How many of the latest open PRs to review per watched repo. | |
| How many recently updated open PRs to review per watched repo. |
| <div className="flex flex-col gap-2 text-sm text-muted-foreground"> | ||
| <p> | ||
| Pick repos to watch and Gnosis will automatically review every open PR — no URL pasting needed. | ||
| </p> | ||
| <p> | ||
| Each review becomes a guided walkthrough: diffs grouped by theme, ordered by dependency, with a short narrative on every slide explaining <em>why</em> the change is there. | ||
| </p> | ||
| <p> | ||
| Reviews refresh when PRs update and you'll get a notification when they're ready. | ||
| </p> |
There was a problem hiding this comment.
Onboarding copy says Gnosis will “automatically review every open PR”, but proactive mode is capped (e.g. PROACTIVE_MAX_CONCURRENT_UPDATES, repo PR limit, and a 24h staleness cutoff). Consider softening/qualifying this text so it matches actual behavior (e.g., “automatically review open PRs (recently updated), up to your configured limits”).
Adds an opt-in "Parallel review" mode that splits review generation into: 1. **Planner phase** — lightweight call with just the hunk index to plan topics, assign hunks, and determine ordering 2. **Writer phase** — parallel calls (3 concurrent) with scoped context per topic to generate individual slide narratives Benefits for large PRs: each writer gets focused context (only relevant hunks + file contents), reducing token waste and improving slide quality. The planner call is cheap (~5-10k tokens vs 150k for single-shot). Behind a feature flag (Settings > Parallel review, off by default). Falls back gracefully — if the planner fails, the existing single-shot path can still be used. New files/functions: - lib/agent.ts: planReview(), generateSlide() - lib/context-builder.ts: buildPlannerContext(), buildTopicContext() - lib/types.ts: TopicPlan, PlannerOutput, WriterSlideOutput Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When GitHub returns 'too_large' for the full PR diff, fall back to assembling a unified diff from individual file patches returned by the listFiles endpoint (which has no such limit). The patch field is now captured per ChangedFile. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The planner now outputs a storyArc (overall narrative thread) and a
narrativeBrief per topic (writing direction for each slide). Each
writer receives:
- The story arc for big-picture context
- Its own narrative brief for angle/emphasis
- Briefs of topics it depends on for continuity references
Writers are instructed to acknowledge dependencies ("Building on the
Token type introduced earlier...") and connect to the broader story.
Zero extra API calls — all context comes from the planner output.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a writer call returns invalid JSON (e.g., unescaped characters in narrative text), retry once with a directive to return raw JSON only. Matches the retry pattern used in single-shot generateReviewGuide. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the batched concurrency limit — all topic writers now run simultaneously via a single Promise.all(). The CLI and API handle their own rate limiting, so artificial batching just adds wall-clock time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OnboardingRepoSetup: - Reset state (query, suggestions, selectedRepos) when dialog reopens - Clean up debounce timer on unmount - Validate repo refs strictly (exactly one slash, no whitespace) via REPO_REF_RE pattern; rejects both Enter and suggestion paths - Add try/catch/finally to searchRepos with request-id guard so stale or failed requests don't leave loading stuck - Soften onboarding copy from "every open PR" to "recently updated open PRs" to match actual behavior main.ts: - Clamp maxPrsPerRepo to GitHub's 1-100 range, default to 10 if the stored value is non-numeric or NaN SettingsDialog: - Change "latest open PRs" to "recently updated open PRs" to match the GitHub API's sort=updated ordering HomePage: - Make replayOnboarding trigger the repo setup dialog (matches the PR description's intent) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Skip buildContextPackage + expandFullDiff in parallel mode (wasted work on the default hot path) - Move mcpConfigPath/allowedTools setup inside single-shot branch; fixes a resource leak when enableTools + parallelReview were both on - Replace plannerSummary/plannerRiskLevel/plannerRiskRationale trio with a single nullable plan reference - Extract resolveDiffHunks helper — eliminates 3 copies of hunk-ID → DiffHunk mapping (parallel writer, single-shot writer, catch-all) - buildTopicContext now takes a pre-built Map<id, hunk> instead of iterating all hunks per call (O(topic.hunkIds) per call instead of O(all_hunks) × topics) - Fix shadowing bug: parallel branch's inner `const plan` was shadowing the outer `let plan` Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Parallel Review Architecture
Behind feature flag: Settings > Parallel review (off by default).
Test plan