feat: add a repo to watch by name - #26
Conversation
The repo picker only lists what the paginated APIs return (GitHub caps at ~2000 repos per list), and its filter is client-side over that loaded set. In a very large org, a repo may never appear in the list — so "select the ones you want" can't reach it. Add an "Add a repo by name" form to the Repos page: enter owner/repo (or paste a URL), which is verified with a single API call via a new VERIFY_REPO service-worker message (github.getRepo / gitlab.getProject / bitbucket.getRepository) and added straight to the watched list. buildRepoList now unions watched-but-not-enumerated repos (enabled or pinned only) so an added repo survives cache refreshes without resurrecting stale disabled ones. Addresses the large-org follow-up on #23. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WalkthroughAdds a "verify and add repo by name" feature: a new ChangesAdd Repo By Name
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant ReposUI
participant ServiceWorker
participant PlatformAPI
User->>ReposUI: Enter owner/repo or URL, submit
ReposUI->>ReposUI: normalizeRepoInput, validate contains "/"
ReposUI->>ServiceWorker: sendMessage VERIFY_REPO {platform, fullName}
ServiceWorker->>ServiceWorker: getAccounts, find matching platform account
alt no account
ServiceWorker-->>ReposUI: {success:false, message:"Not connected"}
else account found
ServiceWorker->>PlatformAPI: verify repo (getRepo/getProject/getRepository)
PlatformAPI-->>ServiceWorker: canonical name or null
alt not found
ServiceWorker-->>ReposUI: {success:false, message:"Repo not found"}
else found
ServiceWorker-->>ReposUI: {success:true, fullName}
ReposUI->>ReposUI: update/prepend WatchedRepo, saveWatchedRepos
ReposUI->>ServiceWorker: sendMessage POLL_NOW
end
end
ReposUI-->>User: show result or error, close form
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b16735de42
ℹ️ 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".
| const updated = exists | ||
| ? repos.map((r) => (`${r.platform}:${r.fullName}` === key ? { ...r, enabled: true } : r)) | ||
| : [ | ||
| { platform: addPlatform, fullName: canonical, enabled: true, pinned: false } satisfies WatchedRepo, | ||
| ...repos, |
There was a problem hiding this comment.
Preserve existing watched repos when adding before cache loads
When the available-repos cache is empty, the page does not initialize repos from the stored watched list before awaiting FETCH_AVAILABLE_REPOS, but the add form is still visible. If a user adds a repo by name during that first or slow refresh, this updated array is built from the current repos state, which can be empty/stale, and saveWatchedRepos(updated) then replaces all stored watched repos with only the new entry, so existing selections stop being polled. Merge with getWatchedRepos() at save time or disable the form until initialization completes.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/background/service-worker.ts`:
- Around line 140-167: The VERIFY_REPO branch in serviceWorker leaves the
message unresolved if getAccounts() throws before the existing try/catch. Move
getAccounts() and the account lookup into the same try/catch used for the
repository verification so any failure still calls sendResponse with an error.
Keep the fix localized to the VERIFY_REPO handler and preserve the async
IIFE/channel-opening return behavior.
In `@src/popup/pages/Repos.tsx`:
- Around line 274-283: The add-by-name toggle button in Repos should expose its
open/closed state to assistive tech. Update the button in the showAdd toggle
block to include an aria-expanded attribute that reflects the showAdd state, so
screen readers can tell whether the add form is currently visible. Keep the
change localized to the button that calls setShowAdd and clears addError.
- Around line 126-163: handleAddRepo in Repos.tsx is using a stale repos
snapshot after an awaited VERIFY_REPO call, which can overwrite concurrent
changes like toggles, pin updates, or Select all. Update the add flow to derive
the next repos state from the latest state at write time, using a functional
state update or equivalent merge logic around setRepos/saveWatchedRepos, so the
new repo is added without clobbering intervening changes. If needed, review
handleToggle, handleTogglePin, and handleSelectAll for the same read-then-write
pattern and make them consistent.
🪄 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: 8056ddb8-d2ef-4646-9377-36f9270dfabb
📒 Files selected for processing (7)
CLAUDE.mdsrc/background/service-worker.tssrc/popup/pages/Repos.tsxsrc/shared/api/bitbucket.tssrc/shared/api/github.tssrc/shared/api/gitlab.tssrc/shared/types.ts
| } else if (message.type === 'VERIFY_REPO') { | ||
| const { platform, fullName } = message.payload; | ||
| (async () => { | ||
| const accounts = await getAccounts(); | ||
| const account = accounts.find((a) => a.platform === platform); | ||
| if (!account) { | ||
| sendResponse({ success: false, message: `Not connected to ${platform}` }); | ||
| return; | ||
| } | ||
| try { | ||
| let canonical: string | null = null; | ||
| if (platform === 'github') { | ||
| canonical = (await github.getRepo(account.token, fullName))?.full_name ?? null; | ||
| } else if (platform === 'gitlab') { | ||
| canonical = (await gitlab.getProject(account.token, fullName))?.path_with_namespace ?? null; | ||
| } else { | ||
| canonical = (await bitbucket.getRepository(account.token, fullName))?.full_name ?? null; | ||
| } | ||
| if (!canonical) { | ||
| sendResponse({ success: false, message: 'Repo not found or not accessible with your token' }); | ||
| return; | ||
| } | ||
| sendResponse({ success: true, fullName: canonical }); | ||
| } catch (err) { | ||
| sendResponse({ success: false, message: err instanceof Error ? err.message : 'Verification failed' }); | ||
| } | ||
| })(); | ||
| return true; // keep channel open for async sendResponse |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Wrap getAccounts() in the same try/catch as the verification call.
If getAccounts() throws, this branch never calls sendResponse, leaving the popup's pending chrome.runtime.sendMessage promise unresolved and the "Adding…" UI state stuck.
🔧 Proposed fix
} else if (message.type === 'VERIFY_REPO') {
const { platform, fullName } = message.payload;
(async () => {
- const accounts = await getAccounts();
- const account = accounts.find((a) => a.platform === platform);
- if (!account) {
- sendResponse({ success: false, message: `Not connected to ${platform}` });
- return;
- }
try {
+ const accounts = await getAccounts();
+ const account = accounts.find((a) => a.platform === platform);
+ if (!account) {
+ sendResponse({ success: false, message: `Not connected to ${platform}` });
+ return;
+ }
let canonical: string | null = null;
if (platform === 'github') {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } else if (message.type === 'VERIFY_REPO') { | |
| const { platform, fullName } = message.payload; | |
| (async () => { | |
| const accounts = await getAccounts(); | |
| const account = accounts.find((a) => a.platform === platform); | |
| if (!account) { | |
| sendResponse({ success: false, message: `Not connected to ${platform}` }); | |
| return; | |
| } | |
| try { | |
| let canonical: string | null = null; | |
| if (platform === 'github') { | |
| canonical = (await github.getRepo(account.token, fullName))?.full_name ?? null; | |
| } else if (platform === 'gitlab') { | |
| canonical = (await gitlab.getProject(account.token, fullName))?.path_with_namespace ?? null; | |
| } else { | |
| canonical = (await bitbucket.getRepository(account.token, fullName))?.full_name ?? null; | |
| } | |
| if (!canonical) { | |
| sendResponse({ success: false, message: 'Repo not found or not accessible with your token' }); | |
| return; | |
| } | |
| sendResponse({ success: true, fullName: canonical }); | |
| } catch (err) { | |
| sendResponse({ success: false, message: err instanceof Error ? err.message : 'Verification failed' }); | |
| } | |
| })(); | |
| return true; // keep channel open for async sendResponse | |
| } else if (message.type === 'VERIFY_REPO') { | |
| const { platform, fullName } = message.payload; | |
| (async () => { | |
| try { | |
| const accounts = await getAccounts(); | |
| const account = accounts.find((a) => a.platform === platform); | |
| if (!account) { | |
| sendResponse({ success: false, message: `Not connected to ${platform}` }); | |
| return; | |
| } | |
| let canonical: string | null = null; | |
| if (platform === 'github') { | |
| canonical = (await github.getRepo(account.token, fullName))?.full_name ?? null; | |
| } else if (platform === 'gitlab') { | |
| canonical = (await gitlab.getProject(account.token, fullName))?.path_with_namespace ?? null; | |
| } else { | |
| canonical = (await bitbucket.getRepository(account.token, fullName))?.full_name ?? null; | |
| } | |
| if (!canonical) { | |
| sendResponse({ success: false, message: 'Repo not found or not accessible with your token' }); | |
| return; | |
| } | |
| sendResponse({ success: true, fullName: canonical }); | |
| } catch (err) { | |
| sendResponse({ success: false, message: err instanceof Error ? err.message : 'Verification failed' }); | |
| } | |
| })(); | |
| return true; // keep channel open for async sendResponse |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/background/service-worker.ts` around lines 140 - 167, The VERIFY_REPO
branch in serviceWorker leaves the message unresolved if getAccounts() throws
before the existing try/catch. Move getAccounts() and the account lookup into
the same try/catch used for the repository verification so any failure still
calls sendResponse with an error. Keep the fix localized to the VERIFY_REPO
handler and preserve the async IIFE/channel-opening return behavior.
| async function handleAddRepo(e: React.FormEvent) { | ||
| e.preventDefault(); | ||
| const fullName = normalizeRepoInput(addValue); | ||
| if (!fullName.includes('/')) { | ||
| setAddError('Enter a repo as owner/name'); | ||
| return; | ||
| } | ||
| setAdding(true); | ||
| setAddError(null); | ||
| try { | ||
| const res = await chrome.runtime.sendMessage({ | ||
| type: 'VERIFY_REPO', | ||
| payload: { platform: addPlatform, fullName }, | ||
| }); | ||
| if (!res?.success) { | ||
| setAddError(res?.message ?? 'Could not add repo'); | ||
| return; | ||
| } | ||
| const canonical: string = res.fullName; | ||
| const key = `${addPlatform}:${canonical}`; | ||
| const exists = repos.some((r) => `${r.platform}:${r.fullName}` === key); | ||
| const updated = exists | ||
| ? repos.map((r) => (`${r.platform}:${r.fullName}` === key ? { ...r, enabled: true } : r)) | ||
| : [ | ||
| { platform: addPlatform, fullName: canonical, enabled: true, pinned: false } satisfies WatchedRepo, | ||
| ...repos, | ||
| ]; | ||
| setRepos(updated); | ||
| await saveWatchedRepos(updated); | ||
| chrome.runtime.sendMessage({ type: 'POLL_NOW' }); | ||
| setAddValue(''); | ||
| setShowAdd(false); | ||
| } catch { | ||
| setAddError('Could not reach the extension background'); | ||
| } finally { | ||
| setAdding(false); | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Stale repos closure can lose a concurrent toggle while verification is in flight.
handleAddRepo computes updated from the repos snapshot captured when the function was created, after awaiting a full network round trip (VERIFY_REPO). If the user toggles/pins another repo (or hits "Select all") while "Adding…" is pending, that write is silently reverted once handleAddRepo's saveWatchedRepos(updated) lands, since updated doesn't include the intervening change. handleToggle/handleTogglePin/handleSelectAll have the same read-then-write pattern, but their await window (a fast chrome.storage write) is much shorter than this new network-bound verification call.
🔧 Proposed fix using functional state update
const canonical: string = res.fullName;
const key = `${addPlatform}:${canonical}`;
- const exists = repos.some((r) => `${r.platform}:${r.fullName}` === key);
- const updated = exists
- ? repos.map((r) => (`${r.platform}:${r.fullName}` === key ? { ...r, enabled: true } : r))
- : [
- { platform: addPlatform, fullName: canonical, enabled: true, pinned: false } satisfies WatchedRepo,
- ...repos,
- ];
- setRepos(updated);
+ let updated: WatchedRepo[] = repos;
+ setRepos((prev) => {
+ const exists = prev.some((r) => `${r.platform}:${r.fullName}` === key);
+ updated = exists
+ ? prev.map((r) => (`${r.platform}:${r.fullName}` === key ? { ...r, enabled: true } : r))
+ : [
+ { platform: addPlatform, fullName: canonical, enabled: true, pinned: false } satisfies WatchedRepo,
+ ...prev,
+ ];
+ return updated;
+ });
await saveWatchedRepos(updated);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function handleAddRepo(e: React.FormEvent) { | |
| e.preventDefault(); | |
| const fullName = normalizeRepoInput(addValue); | |
| if (!fullName.includes('/')) { | |
| setAddError('Enter a repo as owner/name'); | |
| return; | |
| } | |
| setAdding(true); | |
| setAddError(null); | |
| try { | |
| const res = await chrome.runtime.sendMessage({ | |
| type: 'VERIFY_REPO', | |
| payload: { platform: addPlatform, fullName }, | |
| }); | |
| if (!res?.success) { | |
| setAddError(res?.message ?? 'Could not add repo'); | |
| return; | |
| } | |
| const canonical: string = res.fullName; | |
| const key = `${addPlatform}:${canonical}`; | |
| const exists = repos.some((r) => `${r.platform}:${r.fullName}` === key); | |
| const updated = exists | |
| ? repos.map((r) => (`${r.platform}:${r.fullName}` === key ? { ...r, enabled: true } : r)) | |
| : [ | |
| { platform: addPlatform, fullName: canonical, enabled: true, pinned: false } satisfies WatchedRepo, | |
| ...repos, | |
| ]; | |
| setRepos(updated); | |
| await saveWatchedRepos(updated); | |
| chrome.runtime.sendMessage({ type: 'POLL_NOW' }); | |
| setAddValue(''); | |
| setShowAdd(false); | |
| } catch { | |
| setAddError('Could not reach the extension background'); | |
| } finally { | |
| setAdding(false); | |
| } | |
| } | |
| async function handleAddRepo(e: React.FormEvent) { | |
| e.preventDefault(); | |
| const fullName = normalizeRepoInput(addValue); | |
| if (!fullName.includes('/')) { | |
| setAddError('Enter a repo as owner/name'); | |
| return; | |
| } | |
| setAdding(true); | |
| setAddError(null); | |
| try { | |
| const res = await chrome.runtime.sendMessage({ | |
| type: 'VERIFY_REPO', | |
| payload: { platform: addPlatform, fullName }, | |
| }); | |
| if (!res?.success) { | |
| setAddError(res?.message ?? 'Could not add repo'); | |
| return; | |
| } | |
| const canonical: string = res.fullName; | |
| const key = `${addPlatform}:${canonical}`; | |
| let updated: WatchedRepo[] = repos; | |
| setRepos((prev) => { | |
| const exists = prev.some((r) => `${r.platform}:${r.fullName}` === key); | |
| updated = exists | |
| ? prev.map((r) => (`${r.platform}:${r.fullName}` === key ? { ...r, enabled: true } : r)) | |
| : [ | |
| { platform: addPlatform, fullName: canonical, enabled: true, pinned: false } satisfies WatchedRepo, | |
| ...prev, | |
| ]; | |
| return updated; | |
| }); | |
| await saveWatchedRepos(updated); | |
| chrome.runtime.sendMessage({ type: 'POLL_NOW' }); | |
| setAddValue(''); | |
| setShowAdd(false); | |
| } catch { | |
| setAddError('Could not reach the extension background'); | |
| } finally { | |
| setAdding(false); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/popup/pages/Repos.tsx` around lines 126 - 163, handleAddRepo in Repos.tsx
is using a stale repos snapshot after an awaited VERIFY_REPO call, which can
overwrite concurrent changes like toggles, pin updates, or Select all. Update
the add flow to derive the next repos state from the latest state at write time,
using a functional state update or equivalent merge logic around
setRepos/saveWatchedRepos, so the new repo is added without clobbering
intervening changes. If needed, review handleToggle, handleTogglePin, and
handleSelectAll for the same read-then-write pattern and make them consistent.
| {!showAdd ? ( | ||
| <button | ||
| onClick={() => { | ||
| setShowAdd(true); | ||
| setAddError(null); | ||
| }} | ||
| className="text-[11px] text-radar-400 hover:underline" | ||
| > | ||
| + Add a repo by name | ||
| </button> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Add aria-expanded to the add-by-name toggle button.
The button that reveals the form doesn't expose its expanded/collapsed state to assistive tech.
Based on learnings, "Keep accessibility requirements broad: ARIA labels/roles, live regions, expanded state, hidden decorative icons, and labeled form inputs."
🔧 Proposed fix
<button
onClick={() => {
setShowAdd(true);
setAddError(null);
}}
+ aria-expanded={showAdd}
className="text-[11px] text-radar-400 hover:underline"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {!showAdd ? ( | |
| <button | |
| onClick={() => { | |
| setShowAdd(true); | |
| setAddError(null); | |
| }} | |
| className="text-[11px] text-radar-400 hover:underline" | |
| > | |
| + Add a repo by name | |
| </button> | |
| {!showAdd ? ( | |
| <button | |
| onClick={() => { | |
| setShowAdd(true); | |
| setAddError(null); | |
| }} | |
| aria-expanded={showAdd} | |
| className="text-[11px] text-radar-400 hover:underline" | |
| > | |
| Add a repo by name | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/popup/pages/Repos.tsx` around lines 274 - 283, The add-by-name toggle
button in Repos should expose its open/closed state to assistive tech. Update
the button in the showAdd toggle block to include an aria-expanded attribute
that reflects the showAdd state, so screen readers can tell whether the add form
is currently visible. Keep the change localized to the button that calls
setShowAdd and clears addError.
Source: Learnings
Why
Follow-up on #23. The reporter is in an org with thousands of repos. Two limits mean the picker can't reach all of them:
MAX_PAGINATED_PAGES × 100 = ~2000repos per list — so repos beyond the cap never appear.Caching (#24) fixed the slowness but not these ceilings. This adds a way to watch a specific repo directly, without enumerating everything.
What
owner/repo(or paste a repo URL — normalized to the path form), pick the platform if more than one is connected, and Add.VERIFY_REPOservice-worker message backed by new API helpers:github.getRepo,gitlab.getProject,bitbucket.getRepository. Each returns the canonical name ornullon 404; the form shows an inline error if the repo isn't found/accessible.buildRepoListnow unions watched repos that aren't in the enumerated list (enabled or pinned only), so an added repo survives background cache refreshes and we don't resurrect stale, no-longer-accessible disabled entries.Verification
npm run typecheck✅npm run lint✅npm run build✅Scales to any org size (no enumeration), and works identically across GitHub, GitLab, and Bitbucket.
🤖 Generated with Claude Code
Summary by CodeRabbit