Skip to content

Instantly share code, notes, and snippets.

@dbowling
Last active November 19, 2025 21:14
Show Gist options
  • Select an option

  • Save dbowling/80baff8bad5950177c894e0e133248db to your computer and use it in GitHub Desktop.

Select an option

Save dbowling/80baff8bad5950177c894e0e133248db to your computer and use it in GitHub Desktop.
github-issue-comments-hide-by-username
// ==UserScript==
// @name GitHub Issue Hide Comments by Username + Slash Commands
// @namespace https://github.com/dbowling
// @version 0.5
// @description Toggle/hide selected usernames and command-only comments
// @include https://github.com/*
// @match https://github.com/*/*/issues/*
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @icon https://github.githubassets.com/pinned-octocat.svg
// ==/UserScript==
(function () {
'use strict';
const DEFAULT_HIDDEN_USERNAMES = ['k8s-triage-robot'];
const processedComments = new WeakSet();
const hiddenUsernames = new Set();
// --- Settings helpers ----------------------------------------------------
function parseHiddenUsernames(raw) {
return String(raw)
.split(',')
.map((s) => s.trim())
.filter(Boolean);
}
function loadHiddenUsernames() {
const raw = GM_getValue('hiddenUsernames', DEFAULT_HIDDEN_USERNAMES.join(','));
hiddenUsernames.clear();
for (const name of parseHiddenUsernames(raw)) {
hiddenUsernames.add(name);
}
console.debug('[GitHub Issue Comment User] Hidden usernames:', [...hiddenUsernames]);
}
function configureHiddenUsernames() {
const current = [...hiddenUsernames].join(', ');
const input = window.prompt(
'Enter comma-separated GitHub usernames to auto-collapse:',
current
);
if (input === null) {
return; // user cancelled
}
GM_setValue('hiddenUsernames', input);
loadHiddenUsernames();
window.alert('Hidden usernames updated. Reload the page to re-process existing comments.');
}
GM_registerMenuCommand('Configure hidden usernames', configureHiddenUsernames);
loadHiddenUsernames();
// --- DOM helpers ---------------------------------------------------------
function findBodyContainer(commentEl) {
let bodyContainer =
commentEl.querySelector('.IssueCommentViewer-module__IssueCommentBody--xvkt3') || null;
if (!bodyContainer) {
const markdownBody = commentEl.querySelector('[data-testid="markdown-body"], .markdown-body');
if (markdownBody) {
bodyContainer = markdownBody.closest('div') || markdownBody;
}
}
return bodyContainer;
}
function extractUsername(commentEl) {
// Primary: explicit author link in the header
const headerAuthorLink = commentEl.querySelector('[data-testid="avatar-link"]');
if (headerAuthorLink && headerAuthorLink.textContent) {
return headerAuthorLink.textContent.trim();
}
// Fallback: avatar/profile link
const profileLink = commentEl.querySelector('a[aria-label*="\'s profile"]');
if (profileLink) {
const img = profileLink.querySelector('img[alt^="@"]');
if (img) {
return (img.getAttribute('alt') || '').replace(/^@/, '').trim();
}
const href = profileLink.getAttribute('href') || '';
const parts = href.split('/').filter(Boolean);
if (parts.length > 0) {
return parts[parts.length - 1];
}
}
return null;
}
// Detect “command-only” comments like `/remove-lifecycle stale`, `/triage accepted`
function isCommandOnlyComment(commentEl) {
const bodyContainer = findBodyContainer(commentEl);
if (!bodyContainer) return false;
const text = bodyContainer.textContent || '';
const trimmed = text.trim();
if (!trimmed) return false;
const lines = trimmed
.split('\n')
.map((l) => l.trim())
.filter((l) => l.length > 0);
if (!lines.length) return false;
// Heuristic: all non-empty lines must start with '/'
const allCommands = lines.every((line) => line.startsWith('/'));
if (allCommands) {
console.debug('[GitHub Issue Comment User] Detected command-only comment:', lines);
}
return allCommands;
}
// Generic toggle setup for both “hidden users” and “command-only” comments
function setupToggle(commentEl, labelBase) {
if (commentEl.dataset.hiddenCommentToggleInit === '1') return;
commentEl.dataset.hiddenCommentToggleInit = '1';
const bodyContainer = findBodyContainer(commentEl);
if (!bodyContainer) {
console.debug(
'[GitHub Issue Comment User] Could not find body container to toggle for:',
labelBase
);
return;
}
const headerLeft =
commentEl.querySelector('[data-testid="comment-header-left-side-items"]') ||
commentEl.querySelector('[data-testid="comment-header"]') ||
commentEl;
const toggleButton = document.createElement('button');
toggleButton.type = 'button';
toggleButton.textContent = `Show ${labelBase} comment`;
toggleButton.setAttribute(
'aria-label',
`Toggle visibility of ${labelBase} comment`
);
toggleButton.setAttribute('aria-expanded', 'false');
// Light GitHub-ish styling
toggleButton.style.marginLeft = '8px';
toggleButton.style.fontSize = '12px';
toggleButton.style.border = 'none';
toggleButton.style.padding = '2px 4px';
toggleButton.style.background = 'transparent';
toggleButton.style.cursor = 'pointer';
toggleButton.style.color = 'var(--fgColor-muted, #57606a)';
toggleButton.style.textDecoration = 'underline';
let collapsed = true;
bodyContainer.style.display = 'none';
toggleButton.addEventListener('click', () => {
collapsed = !collapsed;
bodyContainer.style.display = collapsed ? 'none' : '';
toggleButton.textContent = collapsed
? `Show ${labelBase} comment`
: `Hide ${labelBase} comment`;
toggleButton.setAttribute('aria-expanded', String(!collapsed));
});
headerLeft.appendChild(toggleButton);
}
// --- Core logic ----------------------------------------------------------
function handleComment(comment) {
if (processedComments.has(comment)) return;
processedComments.add(comment);
const username = extractUsername(comment);
const commandOnly = isCommandOnlyComment(comment);
if (username) {
console.debug('[GitHub Issue Comment User]', username);
} else {
console.debug('[GitHub Issue Comment User] Username not found for comment:', comment);
}
const shouldHideByUser = username && hiddenUsernames.has(username);
const shouldHideByCommand = commandOnly;
if (!shouldHideByUser && !shouldHideByCommand) {
return;
}
let labelBase;
if (shouldHideByUser && username) {
labelBase = username;
} else if (shouldHideByCommand) {
labelBase = 'command';
} else {
labelBase = 'hidden';
}
console.debug(
'[GitHub Issue Comment User] Setting up toggle for comment:',
{ username, commandOnly }
);
setupToggle(comment, labelBase);
}
function scanComments() {
const comments = document.querySelectorAll('div.react-issue-comment');
comments.forEach(handleComment);
}
// Initial scan
scanComments();
// Watch for dynamically loaded comments
const observer = new MutationObserver((mutations) => {
let foundAny = false;
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue;
const el = /** @type {Element} */ (node);
if (el.matches && el.matches('div.react-issue-comment')) {
handleComment(el);
foundAny = true;
} else if (el.querySelector && el.querySelector('div.react-issue-comment')) {
foundAny = true;
}
}
}
if (foundAny) {
scanComments();
}
});
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment