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
16 changes: 16 additions & 0 deletions content.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
.coderabbit-hidden {
display: none !important;
}

/* ── Toast notification ─────────────────────────────────────────────── */
#coderabbit-toast {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}

/* ── Scroll-to highlight animation ──────────────────────────────────── */
@keyframes coderabbit-flash {
0% { outline-color: rgba(9, 105, 218, 0.8); }
100% { outline-color: transparent; }
}

/* ── Dark mode support for injected elements ────────────────────────── */
[data-coderabbit-theme="dark"] #coderabbit-toast {
filter: invert(1) hue-rotate(180deg);
}
233 changes: 218 additions & 15 deletions content.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const SEVERITY_PATTERNS = {

let currentSettings = { nitpick: true, suggestion: true, warning: true, issue: true };

// ── Comment detection helpers ──────────────────────────────────────────

function isCodeRabbitComment(commentEl) {
const authorEl =
commentEl.querySelector(".author") ||
Expand All @@ -18,10 +20,7 @@ function isCodeRabbitComment(commentEl) {
}

function detectSeverity(commentEl) {
const body =
commentEl.querySelector(".comment-body") ||
commentEl.querySelector(".review-comment-body") ||
commentEl.querySelector("td.comment-body");
const body = getCommentBody(commentEl);
if (!body) return null;

const text = body.textContent;
Expand All @@ -31,12 +30,158 @@ function detectSeverity(commentEl) {
return null;
}

function getCommentBody(commentEl) {
return (
commentEl.querySelector(".comment-body") ||
commentEl.querySelector(".review-comment-body") ||
commentEl.querySelector("td.comment-body")
);
}

function getCommentElements() {
return document.querySelectorAll(
".timeline-comment, .review-comment, .js-comment, [data-body-version]"
);
}

// ── Severity counting ──────────────────────────────────────────────────

function countSeverities() {
const counts = { nitpick: 0, suggestion: 0, warning: 0, issue: 0 };
getCommentElements().forEach((el) => {
if (!isCodeRabbitComment(el)) return;
const severity = detectSeverity(el);
if (severity) counts[severity]++;
});
return counts;
}

// ── File grouping ──────────────────────────────────────────────────────

function getFileForComment(commentEl) {
// Try to find the file path from the surrounding diff/file context
const threadContainer = commentEl.closest(
".js-resolvable-timeline-thread-container"
);
if (threadContainer) {
const fileLink = threadContainer.querySelector(
".file-info a, .file-header a, a[title]"
);
if (fileLink) return fileLink.textContent.trim() || fileLink.getAttribute("title") || "";
}

// Try file header above the comment
const fileContainer = commentEl.closest("[data-file-path]");
if (fileContainer) {
return fileContainer.getAttribute("data-file-path");
}

return "(general)";
}

function getFileGroups() {
const groups = {};

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

const file = getFileForComment(el);
if (!groups[file]) {
groups[file] = { nitpick: 0, suggestion: 0, warning: 0, issue: 0 };
}
groups[file][severity]++;
});

return Object.entries(groups)
.map(([file, counts]) => ({
file,
counts,
total: Object.values(counts).reduce((s, v) => s + v, 0),
}))
.sort((a, b) => b.total - a.total);
}

// ── Scroll to file ─────────────────────────────────────────────────────

function scrollToFile(fileName) {
// Find the file header element on the page
const fileHeaders = document.querySelectorAll(
"[data-file-path], .file-info a, .file-header a"
);

for (const header of fileHeaders) {
const path =
header.getAttribute("data-file-path") ||
header.textContent.trim() ||
header.getAttribute("title");
if (path === fileName) {
const target = header.closest(".js-file, .file, .js-diff-progressive-container") || header;
target.scrollIntoView({ behavior: "smooth", block: "start" });
// Flash highlight
target.style.transition = "outline 0.3s";
target.style.outline = "3px solid #0969da";
setTimeout(() => {
target.style.outline = "";
}, 2000);
return true;
}
}
return false;
}

// ── Toast notifications ────────────────────────────────────────────────

const TOAST_ID = "coderabbit-toast";

function showToast(message, severity) {
let toast = document.getElementById(TOAST_ID);
if (!toast) {
toast = document.createElement("div");
toast.id = TOAST_ID;
toast.setAttribute("role", "status");
toast.setAttribute("aria-live", "polite");
document.body.appendChild(toast);
}

const colors = {
info: { bg: "#ddf4ff", border: "#54aeff", text: "#0550ae" },
success: { bg: "#dafbe1", border: "#4ac26b", text: "#116329" },
warning: { bg: "#fff8c5", border: "#d4a72c", text: "#633c01" },
};

const style = colors[severity] || colors.info;
toast.textContent = message;
toast.style.cssText = `
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(0);
z-index: 99999;
padding: 10px 20px;
border-radius: 8px;
border: 1px solid ${style.border};
background: ${style.bg};
color: ${style.text};
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 13px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
opacity: 1;
transition: opacity 0.3s, transform 0.3s;
pointer-events: none;
`;

clearTimeout(toast._hideTimeout);
toast._hideTimeout = setTimeout(() => {
toast.style.opacity = "0";
toast.style.transform = "translateX(-50%) translateY(10px)";
}, 2500);
}

// ── Core filter logic ──────────────────────────────────────────────────

function applyFilters() {
let hiddenCount = 0;

Expand All @@ -48,8 +193,11 @@ function applyFilters() {

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 = "";
Expand All @@ -64,20 +212,75 @@ function applyFilters() {
return hiddenCount;
}

// Listen for settings changes from popup
// ── Message handling ───────────────────────────────────────────────────

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === "UPDATE_SEVERITY_FILTER") {
currentSettings = message.settings;
applyFilters();
sendResponse({ ok: true });
} else if (message.type === "GET_HIDDEN_COUNT") {
const hiddenCount = applyFilters();
sendResponse({ hiddenCount });
switch (message.type) {
case "UPDATE_SEVERITY_FILTER": {
const prev = { ...currentSettings };
currentSettings = message.settings;
const hiddenCount = applyFilters();

// Show toast for filter changes
const changed = Object.keys(currentSettings).filter(
(k) => currentSettings[k] !== prev[k]
);
if (changed.length > 0) {
const action = currentSettings[changed[0]] ? "Showing" : "Hiding";
if (changed.length === 4) {
showToast(
currentSettings[changed[0]] ? "All comments visible" : "All comments hidden",
"info"
);
} else {
showToast(`${action} ${changed.join(", ")} comments`, "info");
}
}

sendResponse({ ok: true });
break;
}

case "GET_HIDDEN_COUNT": {
const hiddenCount = applyFilters();
sendResponse({ hiddenCount });
break;
}

case "GET_SEVERITY_COUNTS": {
const counts = countSeverities();
sendResponse({ counts });
break;
}

case "GET_FILE_GROUPS": {
const fileGroups = getFileGroups();
sendResponse({ fileGroups });
break;
}

case "SCROLL_TO_FILE": {
const found = scrollToFile(message.file);
sendResponse({ found });
break;
}

case "SET_THEME": {
// Store theme preference for content-injected elements
document.documentElement.setAttribute(
"data-coderabbit-theme",
message.theme
);
sendResponse({ ok: true });
break;
}
}

return true;
});

// Load saved settings on page load
// ── Initialization ─────────────────────────────────────────────────────

chrome.storage.sync.get("severitySettings", (result) => {
if (result.severitySettings) {
currentSettings = result.severitySettings;
Expand Down
6 changes: 3 additions & 3 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"manifest_version": 3,
"name": "CodeRabbit Comment Filter",
"version": "1.0.0",
"description": "Hide CodeRabbit review comments on GitHub by severity level",
"permissions": ["storage"],
"version": "1.2.0",
"description": "Filter CodeRabbit review comments by severity with dark mode, accessibility, and file grouping",
"permissions": ["storage", "activeTab"],
"host_permissions": ["https://github.com/*"],
"content_scripts": [
{
Expand Down
Loading