Skip to content

feat: add a repo to watch by name - #26

Merged
facundofarias merged 1 commit into
mainfrom
feat/add-repo-by-name
Jul 1, 2026
Merged

facundofarias merged 1 commit into
mainfrom
feat/add-repo-by-name

Conversation

@facundofarias

@facundofarias facundofarias commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

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:

  • The available-repo fetch is paginated — GitHub caps at MAX_PAGINATED_PAGES × 100 = ~2000 repos per list — so repos beyond the cap never appear.
  • The filter is client-side over the already-loaded set, so searching can't surface a repo that wasn't fetched.

Caching (#24) fixed the slowness but not these ceilings. This adds a way to watch a specific repo directly, without enumerating everything.

What

  • "Add a repo by name" form on the Repos page: type owner/repo (or paste a repo URL — normalized to the path form), pick the platform if more than one is connected, and Add.
  • Single-call verification via a new VERIFY_REPO service-worker message backed by new API helpers: github.getRepo, gitlab.getProject, bitbucket.getRepository. Each returns the canonical name or null on 404; the form shows an inline error if the repo isn't found/accessible.
  • On success the repo is enabled, saved, and a poll is triggered so it shows up immediately.
  • buildRepoList now 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

  • New Features
    • Added an “Add a repo by name” flow, letting you enter a repository in name or URL form and verify it before adding it to your watched list.
    • Improved support for GitHub, GitLab, and Bitbucket, with platform-specific repository lookup and clearer input hints.
  • Bug Fixes
    • Watched repositories now rebuild more reliably, avoiding reintroducing disabled entries while keeping enabled or pinned ones visible.

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>
@coderabbitai

coderabbitai Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a "verify and add repo by name" feature: a new VERIFY_REPO message type, platform-specific verification helpers (GitHub, GitLab, Bitbucket) returning canonical repo names or null on 404, a background handler orchestrating verification, and popup UI/state for entering, validating, and persisting a new watched repo. Documentation updated accordingly.

Changes

Add Repo By Name

Layer / File(s) Summary
Message contract and platform verification APIs
src/shared/types.ts, src/shared/api/github.ts, src/shared/api/gitlab.ts, src/shared/api/bitbucket.ts
Adds VERIFY_REPO message variant and getRepo/getProject/getRepository functions that fetch a repo by canonical path, returning null on 404 and rethrowing other errors.
Background message handling
src/background/service-worker.ts
Handles VERIFY_REPO messages by resolving the connected account, calling the platform-specific verification API, and responding with success/failure and canonical repo name.
Popup add-by-name UI and state
src/popup/pages/Repos.tsx
Adds input normalization, a resilient buildRepoList merge, new component state, handleAddRepo submission flow (verify, persist, poll), and a conditional add-repo form UI.
Documentation updates
CLAUDE.md
Documents the add-repo-by-name capability in architecture notes, design decisions, and features lists.

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
Loading

Suggested reviewers: thdurante, MartaKar

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the new repo-by-name watch feature.
Description check ✅ Passed It covers the why, what, and verification steps, though the checklist section from the template is not filled out.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-repo-by-name

Comment @coderabbitai help to get the list of available commands.

@facundofarias
facundofarias merged commit 7e756a9 into main Jul 1, 2026
2 of 3 checks passed
@facundofarias
facundofarias deleted the feat/add-repo-by-name branch July 1, 2026 09:37

@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: 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".

Comment thread src/popup/pages/Repos.tsx
Comment on lines +147 to +151
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,

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 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 👍 / 👎.

@coderabbitai coderabbitai 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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4726e52 and b16735d.

📒 Files selected for processing (7)
  • CLAUDE.md
  • src/background/service-worker.ts
  • src/popup/pages/Repos.tsx
  • src/shared/api/bitbucket.ts
  • src/shared/api/github.ts
  • src/shared/api/gitlab.ts
  • src/shared/types.ts

Comment on lines +140 to +167
} 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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.

Suggested change
} 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.

Comment thread src/popup/pages/Repos.tsx
Comment on lines +126 to +163
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);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.

Suggested change
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.

Comment thread src/popup/pages/Repos.tsx
Comment on lines +274 to +283
{!showAdd ? (
<button
onClick={() => {
setShowAdd(true);
setAddError(null);
}}
className="text-[11px] text-radar-400 hover:underline"
>
+ Add a repo by name
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
{!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

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.

1 participant