Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Background service worker for tracking CodeRabbit comment statistics across PRs

const STATS_KEY = "coderabbitStats";

const DEFAULT_STATS = {
totalScanned: 0,
totalHidden: 0,
bySeverity: { nitpick: 0, suggestion: 0, warning: 0, issue: 0 },
byRepo: {},
lastUpdated: null,
};

async function getStats() {
const result = await chrome.storage.local.get(STATS_KEY);
return result[STATS_KEY] || structuredClone(DEFAULT_STATS);
}

async function saveStats(stats) {
stats.lastUpdated = new Date().toISOString();
await chrome.storage.local.set({ [STATS_KEY]: stats });
}

async function resetStats() {
await chrome.storage.local.set({ [STATS_KEY]: structuredClone(DEFAULT_STATS) });
}

function extractRepoFromUrl(url) {
const match = url.match(/github\.com\/([^/]+\/[^/]+)/);
return match ? match[1] : null;
}

// Update badge with hidden comment count
async function updateBadge(tabId, hiddenCount) {
const text = hiddenCount > 0 ? String(hiddenCount) : "";
const color = hiddenCount > 0 ? "#cf222e" : "#1a7f37";

await chrome.action.setBadgeText({ text, tabId });
await chrome.action.setBadgeBackgroundColor({ color, tabId });
}

// Handle messages from content script and popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "RECORD_STATS") {
(async () => {
const stats = await getStats();
const { scanned, hidden, severityCounts, url } = message.data;

stats.totalScanned += scanned;
stats.totalHidden += hidden;

for (const [severity, count] of Object.entries(severityCounts)) {
stats.bySeverity[severity] = (stats.bySeverity[severity] || 0) + count;
}

const repo = extractRepoFromUrl(url || "");
if (repo) {
if (!stats.byRepo[repo]) {
stats.byRepo[repo] = { scanned: 0, hidden: 0 };
}
stats.byRepo[repo].scanned += scanned;
stats.byRepo[repo].hidden += hidden;
}

await saveStats(stats);
sendResponse({ ok: true });
})();
return true;
}

if (message.type === "GET_STATS") {
getStats().then((stats) => sendResponse(stats));
return true;
}

if (message.type === "RESET_STATS") {
resetStats().then(() => sendResponse({ ok: true }));
return true;
}

if (message.type === "UPDATE_BADGE") {
const tabId = sender.tab?.id;
if (tabId) {
updateBadge(tabId, message.hiddenCount);
}
sendResponse({ ok: true });
return true;
}
});

// Handle keyboard shortcut commands
chrome.commands.onCommand.addListener(async (command) => {
if (!command.startsWith("toggle-")) return;

const severity = command.replace("toggle-", "");
const result = await chrome.storage.sync.get("severitySettings");
const settings = result.severitySettings || {
nitpick: true,
suggestion: true,
warning: true,
issue: true,
};

settings[severity] = !settings[severity];
await chrome.storage.sync.set({ severitySettings: settings });

const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab?.id) {
chrome.tabs.sendMessage(tab.id, {
type: "UPDATE_SEVERITY_FILTER",
settings,
});
}
});

// Set default badge on install
chrome.runtime.onInstalled.addListener(() => {
chrome.action.setBadgeText({ text: "" });
});
47 changes: 47 additions & 0 deletions content.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,50 @@
.coderabbit-hidden {
display: none !important;
}

/* Collapsed mode styles */
.coderabbit-collapsed > *:not(.coderabbit-collapse-bar) {
display: none !important;
}

.coderabbit-collapsed.coderabbit-expanded > * {
display: revert !important;
}

.coderabbit-collapse-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 6px;
cursor: pointer;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 13px;
color: #656d76;
transition: background 0.15s;
margin: 4px 0;
}

.coderabbit-collapse-bar:hover {
background: #eaeef2;
}

.coderabbit-collapse-badge {
display: inline-block;
padding: 1px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
color: #fff;
}

.coderabbit-badge-nitpick { background-color: #818b98; }
.coderabbit-badge-suggestion { background-color: #0969da; }
.coderabbit-badge-warning { background-color: #bf8700; }
.coderabbit-badge-issue { background-color: #cf222e; }

.coderabbit-collapse-text {
color: #656d76;
}
96 changes: 89 additions & 7 deletions content.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const SEVERITY_PATTERNS = {
};

let currentSettings = { nitpick: true, suggestion: true, warning: true, issue: true };
let options = { autoCollapse: false, showBadge: true, trackStats: true };

function isCodeRabbitComment(commentEl) {
const authorEl =
Expand Down Expand Up @@ -39,32 +40,96 @@ function getCommentElements() {

function applyFilters() {
let hiddenCount = 0;
const severityCounts = { nitpick: 0, suggestion: 0, warning: 0, issue: 0 };
let totalScanned = 0;

getCommentElements().forEach((el) => {
if (!isCodeRabbitComment(el)) return;

const severity = detectSeverity(el);
if (!severity) return;

totalScanned++;
severityCounts[severity]++;

const shouldShow = currentSettings[severity] !== false;

// Walk up to the nearest hideable container (inline comment thread, timeline item, etc.)
const container = el.closest(".js-timeline-item") || el.closest(".js-resolvable-timeline-thread-container") || el;
// Walk up to the nearest hideable container
const container =
el.closest(".js-timeline-item") ||
el.closest(".js-resolvable-timeline-thread-container") ||
el;

if (shouldShow) {
container.style.display = "";
container.classList.remove("coderabbit-hidden");
container.classList.remove("coderabbit-collapsed");
// Remove collapse overlay if present
const overlay = container.querySelector(".coderabbit-collapse-bar");
if (overlay) overlay.remove();
} else {
container.style.display = "none";
container.classList.add("coderabbit-hidden");
if (options.autoCollapse) {
// Collapse mode: show a minimal bar instead of fully hiding
container.style.display = "";
container.classList.remove("coderabbit-hidden");
container.classList.add("coderabbit-collapsed");

if (!container.querySelector(".coderabbit-collapse-bar")) {
const bar = document.createElement("div");
bar.className = "coderabbit-collapse-bar";
bar.innerHTML = `<span class="coderabbit-collapse-badge coderabbit-badge-${severity}">${severity}</span> <span class="coderabbit-collapse-text">CodeRabbit comment (click to expand)</span>`;
bar.addEventListener("click", () => {
container.classList.toggle("coderabbit-expanded");
});
container.prepend(bar);
}
} else {
// Full hide mode
container.style.display = "none";
container.classList.add("coderabbit-hidden");
container.classList.remove("coderabbit-collapsed");
const overlay = container.querySelector(".coderabbit-collapse-bar");
if (overlay) overlay.remove();
}
hiddenCount++;
}
});

// Report to background for badge and stats
if (options.showBadge) {
chrome.runtime.sendMessage({ type: "UPDATE_BADGE", hiddenCount }).catch(() => {});
}

if (options.trackStats && totalScanned > 0) {
chrome.runtime
.sendMessage({
type: "RECORD_STATS",
data: {
scanned: totalScanned,
hidden: hiddenCount,
severityCounts,
url: window.location.href,
},
})
.catch(() => {});
}

return hiddenCount;
}

// Listen for settings changes from popup
// Load options from storage
async function loadOptions() {
try {
const result = await chrome.storage.sync.get("coderabbitOptions");
if (result.coderabbitOptions) {
options = { ...options, ...result.coderabbitOptions };
}
} catch {
// Use defaults
}
}

// Listen for settings changes from popup or keyboard shortcuts
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === "UPDATE_SEVERITY_FILTER") {
currentSettings = message.settings;
Expand All @@ -77,13 +142,30 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
return true;
});

// Listen for options changes
chrome.storage.onChanged.addListener((changes) => {
if (changes.coderabbitOptions) {
options = { ...options, ...changes.coderabbitOptions.newValue };
applyFilters();
}
if (changes.severitySettings) {
currentSettings = { ...currentSettings, ...changes.severitySettings.newValue };
applyFilters();
}
});

// Load saved settings on page load
chrome.storage.sync.get("severitySettings", (result) => {
async function init() {
await loadOptions();

const result = await chrome.storage.sync.get("severitySettings");
if (result.severitySettings) {
currentSettings = result.severitySettings;
}
applyFilters();
});
}

init();

// Re-apply when GitHub dynamically loads content (SPA navigation, lazy-loaded comments)
const observer = new MutationObserver(() => {
Expand Down
Loading