Refined GitHub Comments (TJ)

Remove clutter in the comments view

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Refined GitHub Comments (TJ)
// @license      MIT
// @homepageURL  https://github.com/tjx666/user-scripts
// @supportURL   https://github.com/tjx666/user-scripts/issues
// @namespace    https://github.com/tjx666/user-scripts
// @version      0.4.3
// @description  Remove clutter in the comments view
// @author       YuTengjing
// @match        https://github.com/*/issues/*
// @match        https://github.com/*/pull/*
// @match        https://github.com/*/discussions/*
// @match        https://github.com/*/commits/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant        none
// ==/UserScript==

// common bots that i already know what they do
const authorsToMinimize = [
    'changeset-bot',
    'codeflowapp',
    'netlify',
    // 'vercel',
    'pkg-pr-new',
    'codecov',
    'astrobot-houston',
    'codspeed-hq',
    'lobehubbot',
];

// common comments that don't really add value
const commentMatchToMinimize = [
    /^![a-z]/, // commands that start with !
    /^\/[a-z]/, // commands that start with /
    /^> [email protected]/, // astro preview release bot
    /^👍[\s\S]*Thank you for raising your pull request/, // lobehubbot PR thanks
    /^👀[\s\S]*Thank you for raising an issue/, // lobehubbot issue thanks
    /^✅[\s\S]*This issue is closed/, // lobehubbot issue closed
    /^❤️[\s\S]*Great PR/, // lobehubbot PR merged thanks
    /Bot detected the issue body's language is not English/, // lobehubbot translation
];

// DOM selectors
const SELECTORS = {
    TIMELINE_ELEMENT: '.LayoutHelpers-module__timelineElement--IsjVR, [data-wrapper-timeline-id]',
    COMMENT_BODY:
        '[data-testid="markdown-body"] .markdown-body, .IssueCommentViewer-module__IssueCommentBody--xvkt3 .markdown-body',
    COMMENT_CONTENT:
        '.IssueCommentViewer-module__IssueCommentBody--xvkt3, [data-testid="markdown-body"]',
    COMMENT_HEADER: '[data-testid="comment-header"]',
    AUTHOR_LINK: '.ActivityHeader-module__AuthorLink--D7Ojk, [data-testid="avatar-link"]',
    COMMENT_ACTIONS:
        '[data-testid="comment-header-hamburger"], .CommentActions-module__CommentActionsIconButton--EOXv7',
    TITLE_CONTAINER: '.ActivityHeader-module__TitleContainer--pa99A',
    FOOTER_CONTAINER:
        '.ActivityHeader-module__footer--ssKOW, .ActivityHeader-module__FooterContainer--FHEpM',
    ACTIONS_CONTAINER: '.ActivityHeader-module__ActionsButtonsContainer--L7GUK',
};

// Used by `minimizeDiscussionThread`
let expandedThread = false;
const maxParentThreadHeight = 185;

// Used by `minimizeReactBlockquote` to track seen comments
const seenReactComments = [];

(function () {
    'use strict';

    run();

    // listen to github page loaded event
    document.addEventListener('pjax:end', () => run());
    document.addEventListener('turbo:render', () => run());
})();

function run() {
    injectCSS();

    setTimeout(() => {
        // Handle React version comments
        const reactComments = document.querySelectorAll('.react-issue-comment');
        reactComments.forEach((comment) => {
            minimizeReactComment(comment);
            minimizeReactBlockquote(comment, seenReactComments);
        });

        // Handle PR comments
        const timelineItems = document.querySelectorAll('.js-timeline-item');
        timelineItems.forEach((timelineItem) => {
            minimizePRComment(timelineItem);
        });

        // Discussion threads view
        if (location.pathname.includes('/discussions/')) {
            minimizeDiscussionThread();
        }

        setupDOMObserver();
    }, 1000);
}

function injectCSS() {
    // Remove existing style if any
    const existingStyle = document.getElementById('refined-github-comments-style');
    if (existingStyle) {
        existingStyle.remove();
    }

    const style = document.createElement('style');
    style.id = 'refined-github-comments-style';
    style.textContent = `
        /* Layout for minimized React comments */
        .refined-github-comments-minimized .ActivityHeader-module__CommentHeaderContentContainer--OOrIN {
            display: flex !important;
            flex-direction: row !important;
            align-items: center !important;
            flex-wrap: nowrap !important;
            gap: 4px !important;
            flex: 1 !important;
        }
        
        .refined-github-comments-minimized .ActivityHeader-module__FooterContainer--FHEpM {
            display: flex !important;
            flex-direction: row !important;
            align-items: center !important;
            flex: 1 !important;
            overflow: hidden !important;
        }
        
        .refined-github-comments-minimized .ActivityHeader-module__narrowViewportWrapper--k4ncm.ActivityHeader-module__ActionsContainer--Ebsux {
            flex-grow: 0 !important;
        }
        
        .refined-github-comments-minimized .ActivityHeader-module__HeaderMutedText--aJAo0 {
            flex-shrink: 0 !important;
        }
        
        /* Excerpt text styling */
        .refined-github-comments-minimized .ActivityHeader-module__FooterContainer--FHEpM .color-fg-muted,
        .timeline-comment-header .css-truncate-overflow {
            white-space: nowrap !important;
            overflow: hidden !important;
            text-overflow: ellipsis !important;
            display: inline-block !important;
        }
        
        /* Toggle button styling */
        .refined-github-comments-toggle.timeline-comment-action {
            padding: 0 6px !important;
            margin: 0 !important;
        }
        
        /* Hidden elements */
        .refined-github-comments-hidden {
            display: none !important;
        }
    `;

    document.head.appendChild(style);
}

function setupDOMObserver() {
    const observer = new MutationObserver((mutations) => {
        const hasNewComments = mutations.some(
            (mutation) =>
                mutation.type === 'childList' &&
                Array.from(mutation.addedNodes).some(
                    (node) =>
                        node.nodeType === Node.ELEMENT_NODE &&
                        (node.classList?.contains('react-issue-comment') ||
                            node.querySelector?.('.react-issue-comment') ||
                            node.classList?.contains('js-timeline-item') ||
                            node.querySelector?.('.js-timeline-item')),
                ),
        );

        if (hasNewComments) {
            setTimeout(() => {
                // Handle React comments
                document.querySelectorAll('.react-issue-comment').forEach((comment) => {
                    minimizeReactComment(comment);
                    minimizeReactBlockquote(comment, seenReactComments);
                });

                // Handle PR comments
                document.querySelectorAll('.js-timeline-item').forEach((timelineItem) => {
                    minimizePRComment(timelineItem);
                });
            }, 500);
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });
}

/**
 * Extract author name from link
 * @param {HTMLElement} authorLink
 * @returns {string}
 */
function getAuthorName(authorLink) {
    let authorName =
        authorLink.getAttribute('href')?.replace('/', '') || authorLink.textContent.trim();
    if (authorName.startsWith('apps/')) {
        authorName = authorName.replace('apps/', '');
    }
    return authorName;
}

/**
 * Check if comment should be minimized
 * @param {string} authorName
 * @param {string} commentText
 * @returns {boolean}
 */
function shouldMinimizeComment(authorName, commentText) {
    const shouldMinimizeByAuthor = authorsToMinimize.includes(authorName);
    const matchingPattern = commentMatchToMinimize.find((match) => match.test(commentText));
    return shouldMinimizeByAuthor || matchingPattern;
}

/**
 * Create comment excerpt element
 * @param {string} text
 * @returns {HTMLElement}
 */
function createExcerpt(text) {
    const excerpt = document.createElement('span');
    excerpt.className = 'css-truncate-overflow text-fg-muted text-italic';
    excerpt.style.fontSize = '12px';
    excerpt.style.opacity = '0.6';
    excerpt.style.whiteSpace = 'nowrap';
    excerpt.style.overflow = 'hidden';
    excerpt.style.textOverflow = 'ellipsis';
    excerpt.style.display = 'inline-block';
    excerpt.style.verticalAlign = 'middle';
    excerpt.textContent = text;
    return excerpt;
}

/**
 * Setup toggle button styling and placement
 * @param {HTMLElement} commentActions
 * @param {HTMLElement} toggleBtn
 * @param {HTMLElement} beforeElement - Optional element to insert before
 */
function setupToggleButton(commentActions, toggleBtn, beforeElement = null) {
    commentActions.style.display = 'flex';
    commentActions.style.alignItems = 'center';
    commentActions.style.gap = '4px';

    if (beforeElement) {
        commentActions.insertBefore(toggleBtn, beforeElement);
    } else {
        commentActions.insertBefore(toggleBtn, commentActions.firstChild);
    }
}

/**
 * Handle PR comments
 * @param {HTMLElement} timelineItem
 */
function minimizePRComment(timelineItem) {
    // Skip if already processed
    if (timelineItem.querySelector('.refined-github-comments-toggle')) {
        return;
    }

    // Find timeline comment
    const timelineComment = timelineItem.querySelector('.timeline-comment');
    if (!timelineComment) return;

    // Find comment header
    const header = timelineComment.querySelector('.timeline-comment-header');
    if (!header) return;

    // Find author in h3 strong a structure
    const authorLink = header.querySelector('h3 strong .author');
    if (!authorLink) return;

    // Find comment body
    const commentBody = timelineComment.querySelector(
        '.comment-body.markdown-body.js-comment-body',
    );
    if (!commentBody) return;

    const authorName = getAuthorName(authorLink);
    const commentBodyText = commentBody.innerText.trim();

    if (shouldMinimizeComment(authorName, commentBodyText)) {
        // Find comment actions container
        const commentActions = header.querySelector('.timeline-comment-actions');
        if (!commentActions) return;

        // Hide comment body content
        const taskLists = timelineComment.querySelector('task-lists');
        if (taskLists) {
            taskLists.style.display = 'none';
        } else {
            const commentBody = timelineComment.querySelector('.comment-body');
            if (commentBody) {
                commentBody.style.display = 'none';
            }
        }

        // Remove border bottom from header
        header.style.borderBottom = 'none';

        // Hide mention buttons
        toggleMentionButtons(timelineItem, false);

        // Add comment excerpt in header
        const titleContainer = header.querySelector('h3.f5.text-normal');
        if (titleContainer) {
            // Find the div inside h3 to add excerpt there (keep it on same line)
            const innerDiv = titleContainer.querySelector('div');
            if (innerDiv) {
                const excerpt = createExcerpt(commentBodyText);

                innerDiv.parentElement.style.overflow = 'hidden';
                innerDiv.style.display = 'flex';
                innerDiv.style.alignItems = 'center';
                innerDiv.style.gap = '4px';
                excerpt.style.flex = '1';

                innerDiv.appendChild(excerpt);
            }

            // Add toggle button
            const toggleBtn = toggleComment((isShow) => {
                const currentTaskLists = timelineComment.querySelector('task-lists');
                const currentCommentBody = timelineComment.querySelector('.comment-body');

                if (isShow) {
                    if (currentTaskLists) {
                        currentTaskLists.style.display = '';
                    } else if (currentCommentBody) {
                        currentCommentBody.style.display = '';
                    }
                    header.style.borderBottom = '';
                    if (innerDiv && innerDiv.querySelector('.css-truncate-overflow')) {
                        innerDiv.querySelector('.css-truncate-overflow').style.display = 'none';
                    }
                    toggleMentionButtons(timelineItem, true);
                } else {
                    if (currentTaskLists) {
                        currentTaskLists.style.display = 'none';
                    } else if (currentCommentBody) {
                        currentCommentBody.style.display = 'none';
                    }
                    header.style.borderBottom = 'none';
                    if (innerDiv && innerDiv.querySelector('.css-truncate-overflow')) {
                        innerDiv.querySelector('.css-truncate-overflow').style.display = '';
                    }
                    toggleMentionButtons(timelineItem, false);
                }
            });

            // Style and insert toggle button
            setupToggleButton(commentActions, toggleBtn);
        }
    }
}

/**
 * Toggle mention buttons visibility
 * @param {HTMLElement} element - Can be either a React comment or PR timeline item
 * @param {boolean} show
 */
function toggleMentionButtons(element, show) {
    let mentionContainer = null;

    // Strategy 1: Find mention container directly within the element (for PR comments)
    mentionContainer = element.querySelector('.avatar-parent-child');

    // Strategy 2: Find via closest timeline element (for React comments)
    if (!mentionContainer) {
        const timelineElement = element.closest(SELECTORS.TIMELINE_ELEMENT);
        if (timelineElement) {
            mentionContainer = timelineElement.querySelector('.avatar-parent-child');
        }
    }

    // Strategy 3: For issue comments, try to find timeline element as sibling container
    if (!mentionContainer) {
        // Look for timeline element that contains both avatar-parent-child and this element
        const timelineElements = document.querySelectorAll(SELECTORS.TIMELINE_ELEMENT);
        for (const timeline of timelineElements) {
            if (timeline.contains(element) && timeline.querySelector('.avatar-parent-child')) {
                mentionContainer = timeline.querySelector('.avatar-parent-child');
                break;
            }
        }
    }

    // Strategy 4: Direct search for mention buttons in nearby containers
    if (!mentionContainer) {
        // Look for mention buttons in the document that might be related to this comment
        const commentId = element.querySelector('[data-testid="comment-header"]')?.id;
        if (commentId) {
            const timelineWrapper = document.querySelector(
                `[data-wrapper-timeline-id="${commentId}"]`,
            );
            if (timelineWrapper) {
                mentionContainer = timelineWrapper.querySelector('.avatar-parent-child');
            }
        }
    }

    if (!mentionContainer) return;

    const mentionBtns = mentionContainer.querySelectorAll('.rgh-quick-mention');
    mentionBtns.forEach((btn) => {
        if (show) {
            btn.classList.remove('refined-github-comments-hidden');
        } else {
            btn.classList.add('refined-github-comments-hidden');
        }
    });
}

/**
 * Handle React version GitHub comments
 * @param {HTMLElement} reactComment
 */
function minimizeReactComment(reactComment) {
    // Skip if already processed
    if (reactComment.querySelector('.refined-github-comments-toggle')) {
        return;
    }

    // Find comment header
    const header = reactComment.querySelector(SELECTORS.COMMENT_HEADER);
    if (!header) return;

    // Find author
    const authorLink = header.querySelector(SELECTORS.AUTHOR_LINK);
    if (!authorLink) return;

    // Find comment body
    const commentBody = reactComment.querySelector(SELECTORS.COMMENT_BODY);
    if (!commentBody) return;

    const authorName = getAuthorName(authorLink);
    const commentBodyText = commentBody.innerText.trim();

    if (shouldMinimizeComment(authorName, commentBodyText)) {
        const commentContent = reactComment.querySelector(SELECTORS.COMMENT_CONTENT);
        if (!commentContent) return;

        const commentActions = header.querySelector(SELECTORS.COMMENT_ACTIONS);
        if (!commentActions) return;

        const titleContainer = header.querySelector(SELECTORS.TITLE_CONTAINER);
        if (!titleContainer) return;

        // Hide comment content
        commentContent.style.display = 'none';

        // Remove border bottom from header
        header.style.borderBottom = 'none';

        // Hide mention buttons
        toggleMentionButtons(reactComment, false);

        // Add CSS class for layout styling
        reactComment.classList.add('refined-github-comments-minimized');

        // Add comment excerpt
        const footerContainer = header.querySelector(SELECTORS.FOOTER_CONTAINER);
        let excerpt = null;
        if (footerContainer) {
            excerpt = document.createElement('span');
            excerpt.setAttribute('class', 'color-fg-muted text-italic');
            excerpt.innerHTML = commentBodyText;
            excerpt.style.opacity = '0.5';
            excerpt.style.fontSize = '12px';
            excerpt.style.marginLeft = '4px';
            footerContainer.appendChild(excerpt);
        }

        // Add toggle button
        const toggleBtn = toggleComment((isShow) => {
            if (isShow) {
                commentContent.style.display = '';
                header.style.borderBottom = '';
                if (excerpt) excerpt.style.display = 'none';
                toggleMentionButtons(reactComment, true);
                reactComment.classList.remove('refined-github-comments-minimized');
            } else {
                commentContent.style.display = 'none';
                header.style.borderBottom = 'none';
                if (excerpt) excerpt.style.display = '';
                toggleMentionButtons(reactComment, false);
                reactComment.classList.add('refined-github-comments-minimized');
            }
        });

        // Find actions container and setup toggle button
        const actionsContainer = header.querySelector(SELECTORS.ACTIONS_CONTAINER);
        if (!actionsContainer) return;

        setupToggleButton(actionsContainer, toggleBtn, commentActions);
    }
}

/**
 * Handle blockquotes in React comments (new GitHub structure)
 * @param {HTMLElement} reactComment
 * @param {{ text: string, id: string, author: string }[]} seenComments
 */
function minimizeReactBlockquote(reactComment, seenComments) {
    const commentBody = reactComment.querySelector('[data-testid="markdown-body"] .markdown-body');
    if (!commentBody) return;

    const commentHeader = reactComment.querySelector('[data-testid="comment-header"]');
    if (!commentHeader) return;

    const commentId = commentHeader.id; // e.g., "issuecomment-1528936387"
    if (!commentId) return;

    const authorLink = commentHeader.querySelector('[data-testid="avatar-link"]');
    if (!authorLink) return;

    const commentAuthor = authorLink.textContent.trim();
    if (!commentAuthor) return;

    const commentText = commentBody.innerText.trim().replace(/\s+/g, ' ');

    // bail early in first comment and if comment is already checked before
    if (
        seenComments.length === 0 ||
        commentBody.querySelector('.refined-github-comments-reply-text')
    ) {
        seenComments.push({
            text: commentText,
            id: commentId,
            author: commentAuthor,
        });
        return;
    }

    const blockquotes = commentBody.querySelectorAll(':scope > blockquote');
    for (const blockquote of blockquotes) {
        const blockquoteText = blockquote.innerText.trim().replace(/\s+/g, ' ');

        const dupIndex = seenComments.findIndex((comment) => comment.text === blockquoteText);
        if (dupIndex >= 0) {
            const dup = seenComments[dupIndex];
            // if replying to the one above, always minimize it
            if (dupIndex === seenComments.length - 1) {
                const summary = `\
  <span class="js-clear text-italic refined-github-comments-reply-text">
    Replying to <strong>@${dup.author}</strong> above
  </span>&nbsp;`;
                blockquote.innerHTML = `<details><summary>${summary}</summary>${blockquote.innerHTML}</details>`;
            }
            // if replying to a long comment, or a comment with code, always minimize it
            else if (blockquoteText.length > 200 || blockquote.querySelector('pre')) {
                const summary = `\
  <span class="js-clear text-italic refined-github-comments-reply-text">
    Replying to <strong>@${dup.author}</strong>'s <a href="#${dup.id}">comment</a>
  </span>&nbsp;`;
                blockquote.innerHTML = `<details><summary>${summary}</summary>${blockquote.innerHTML}</details>`;
            }
            // otherwise, just add a hint so we don't have to navigate away a short sentence
            else {
                const hint = `\
  <span dir="auto" class="js-clear text-italic refined-github-comments-reply-text" style="display: block; margin-top: -0.5rem; opacity: 0.7; font-size: 90%;">
    — <strong>@${dup.author}</strong> said in <a href="#${dup.id}">comment</a>
  </span>`;
                blockquote.insertAdjacentHTML('beforeend', hint);
            }
            continue;
        }

        const partialDupIndex = seenComments.findIndex((comment) =>
            comment.text.includes(blockquoteText),
        );
        if (partialDupIndex >= 0) {
            const dup = seenComments[partialDupIndex];
            // get first four words and last four words, craft a text fragment to highlight
            const splitted = blockquoteText.split(' ');
            const textFragment =
                splitted.length < 9
                    ? `#:~:text=${encodeURIComponent(blockquoteText)}`
                    : `#:~:text=${encodeURIComponent(
                          splitted.slice(0, 4).join(' '),
                      )},${encodeURIComponent(splitted.slice(-4).join(' '))}`;

            // if replying to the one above, prepend hint
            if (partialDupIndex === seenComments.length - 1) {
                const hint = `\
  <span dir="auto" class="js-clear text-italic refined-github-comments-reply-text" style="display: block; margin-top: -0.5rem; opacity: 0.7; font-size: 90%;">
    — <strong>@${dup.author}</strong> <a href="${textFragment}">said</a> above
  </span>`;
                blockquote.insertAdjacentHTML('beforeend', hint);
            }
            // prepend generic hint
            else {
                const hint = `\
  <span dir="auto" class="js-clear text-italic refined-github-comments-reply-text" style="display: block; margin-top: -0.5rem; opacity: 0.7; font-size: 90%;">
    — <strong>@${dup.author}</strong> <a href="${textFragment}">said</a> in <a href="#${dup.id}">comment</a>
  </span>`;
                blockquote.insertAdjacentHTML('beforeend', hint);
            }
        }
    }

    seenComments.push({ text: commentText, id: commentId, author: commentAuthor });
}

// test urls:
// https://github.com/vitejs/vite/discussions/18191
function minimizeDiscussionThread() {
    if (expandedThread) {
        _minimizeDiscussionThread();
        return;
    }

    // Look for the first main timeline comment in discussions
    const firstMainComment = document.querySelector(
        '.timeline-comment:not(.nested-discussion-timeline-comment)',
    );
    if (!firstMainComment) return;

    const tripleDotMenuContainer = firstMainComment.querySelector('.timeline-comment-actions');
    if (!tripleDotMenuContainer) return;

    // Skip if already added
    if (document.getElementById('refined-github-comments-expand-btn') != null) return;

    tripleDotMenuContainer.style.display = 'flex';
    tripleDotMenuContainer.style.alignItems = 'center';

    // Create a "Collapse threads" button to enable this feature
    const expandBtn = document.createElement('button');
    expandBtn.id = 'refined-github-comments-expand-btn';
    expandBtn.setAttribute(
        'class',
        'Button Button--iconOnly Button--invisible Button--medium mr-2',
    );
    expandBtn.innerHTML = `\
  <svg class="Button-visual octicon octicon-zap" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
    <path d="M9.504.43a1.516 1.516 0 0 1 2.437 1.713L10.415 5.5h2.123c1.57 0 2.346 1.909 1.22 3.004l-7.34 7.142a1.249 1.249 0 0 1-.871.354h-.302a1.25 1.25 0 0 1-1.157-1.723L5.633 10.5H3.462c-1.57 0-2.346-1.909-1.22-3.004L9.503.429Zm1.047 1.074L3.286 8.571A.25.25 0 0 0 3.462 9H6.75a.75.75 0 0 1 .694 1.034l-1.713 4.188 6.982-6.793A.25.25 0 0 0 12.538 7H9.25a.75.75 0 0 1-.683-1.06l2.008-4.418.003-.006a.036.036 0 0 0-.004-.009l-.006-.006-.008-.001c-.003 0-.006.002-.009.004Z"></path>
  </svg>
  `;
    expandBtn.title = 'Collapse threads';
    expandBtn.addEventListener('click', () => {
        expandedThread = true;
        _minimizeDiscussionThread();
        expandBtn.remove();
    });
    tripleDotMenuContainer.prepend(expandBtn);
}

function _minimizeDiscussionThread() {
    // Find all main timeline comments (not nested replies)
    const timelineComments = document.querySelectorAll(
        '.timeline-comment:not(.nested-discussion-timeline-comment)',
    );

    for (const timelineComment of timelineComments) {
        // Skip if already handled
        if (timelineComment.querySelector('.refined-github-comments-toggle')) continue;

        // Look for child comments container based on comment ID
        // Find the permalink link which contains the comment ID
        const commentPermalink = timelineComment.querySelector('a[href^="#discussioncomment-"]');
        let childCommentsContainer = null;

        if (commentPermalink) {
            const href = commentPermalink.getAttribute('href'); // e.g., "#discussioncomment-10741444"
            const commentId = href.substring(1); // Remove the # to get "discussioncomment-10741444"
            childCommentsContainer = document.querySelector(`#child-comments-${commentId}`);
        }

        // Fallback: look for any child comments container near this comment
        if (!childCommentsContainer) {
            // Check if there's a child comments container right after this comment
            const parentContainer = timelineComment.closest('.d-flex');
            if (parentContainer && parentContainer.parentElement) {
                const possibleChildContainer =
                    parentContainer.parentElement.querySelector('[data-child-comments]');
                if (possibleChildContainer) {
                    childCommentsContainer = possibleChildContainer;
                }
            }
        }

        // Find replies count text (e.g. "4 replies")
        const repliesTextElement = timelineComment.querySelector(
            '.f6.mr-3 .color-fg-muted.no-wrap',
        );

        if (
            childCommentsContainer &&
            repliesTextElement &&
            repliesTextElement.textContent.includes('replies')
        ) {
            const repliesCount = parseInt(
                repliesTextElement.textContent.match(/(\d+)\s+replies?/)?.[1] || '0',
            );

            // Skip if 0 replies
            if (repliesCount > 0) {
                // Find the parent element to insert toggle button
                const repliesContainer = repliesTextElement.parentElement;
                if (repliesContainer) {
                    const toggleBtn = toggleComment((isShow) => {
                        if (isShow) {
                            childCommentsContainer.style.display = '';
                            repliesTextElement.classList.add('color-fg-muted');
                        } else {
                            childCommentsContainer.style.display = 'none';
                            repliesTextElement.classList.remove('color-fg-muted');
                        }
                    });

                    repliesContainer.insertBefore(toggleBtn, repliesTextElement);
                    childCommentsContainer.style.display = 'none';
                    repliesTextElement.classList.remove('color-fg-muted');

                    // Make replies text clickable too
                    repliesTextElement.style.cursor = 'pointer';
                    repliesTextElement.addEventListener('click', () => {
                        toggleBtn.click();
                    });
                }
            }
        }

        // Handle long comment bodies
        const commentBody = timelineComment.querySelector(
            '.comment-body.markdown-body.js-comment-body',
        );
        if (commentBody && commentBody.clientHeight > maxParentThreadHeight) {
            // Apply height limit and mask
            const css = `max-height:${maxParentThreadHeight}px;mask-image:linear-gradient(180deg, #000 80%, transparent);-webkit-mask-image:linear-gradient(180deg, #000 80%, transparent);`;
            commentBody.style.cssText += css;

            // Add toggle button for comment body
            const commentActions = timelineComment.querySelector('.timeline-comment-actions');
            if (commentActions) {
                const toggleCommentBodyBtn = toggleComment((isShow) => {
                    if (isShow) {
                        commentBody.style.maxHeight = '';
                        commentBody.style.maskImage = '';
                        commentBody.style.webkitMaskImage = '';
                    } else {
                        commentBody.style.cssText += css;
                    }
                });

                commentActions.style.display = 'flex';
                commentActions.style.alignItems = 'center';
                commentActions.prepend(toggleCommentBodyBtn);

                // Auto-expand on first click for nicer UX
                commentBody.style.cursor = 'pointer';
                commentBody.addEventListener('click', () => {
                    if (toggleCommentBodyBtn.dataset.show === 'false') {
                        toggleCommentBodyBtn.click();
                    }
                });
            }
        }
    }
}

// create the toggle comment like github does when you hide a comment
function toggleComment(onClick) {
    const btn = document.createElement('button');
    // copied from github hidden comment style
    btn.innerHTML = `
  <div class="color-fg-muted f6 no-wrap">
    <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-unfold position-relative">
    <path d="m8.177.677 2.896 2.896a.25.25 0 0 1-.177.427H8.75v1.25a.75.75 0 0 1-1.5 0V4H5.104a.25.25 0 0 1-.177-.427L7.823.677a.25.25 0 0 1 .354 0ZM7.25 10.75a.75.75 0 0 1 1.5 0V12h2.146a.25.25 0 0 1 .177.427l-2.896 2.896a.25.25 0 0 1-.354 0l-2.896-2.896A.25.25 0 0 1 5.104 12H7.25v-1.25Zm-5-2a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM6 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 6 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM12 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 12 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5Z"></path>
    </svg>
  </div>
  <div class="color-fg-muted f6 no-wrap" style="display: none">
    <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-fold position-relative">
      <path d="M10.896 2H8.75V.75a.75.75 0 0 0-1.5 0V2H5.104a.25.25 0 0 0-.177.427l2.896 2.896a.25.25 0 0 0 .354 0l2.896-2.896A.25.25 0 0 0 10.896 2ZM8.75 15.25a.75.75 0 0 1-1.5 0V14H5.104a.25.25 0 0 1-.177-.427l2.896-2.896a.25.25 0 0 1 .354 0l2.896 2.896a.25.25 0 0 1-.177.427H8.75v1.25Zm-6.5-6.5a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM6 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 6 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM12 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 12 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5Z"></path>
    </svg>
  </div>
  `;
    const showNode = btn.querySelector('div:nth-child(1)');
    const hideNode = btn.querySelector('div:nth-child(2)');
    let isShow = false;
    btn.setAttribute('type', 'button');
    btn.setAttribute('class', 'refined-github-comments-toggle timeline-comment-action btn-link');
    btn.dataset.show = isShow;
    btn.addEventListener('click', () => {
        isShow = !isShow;
        btn.dataset.show = isShow;
        if (isShow) {
            showNode.style.display = 'none';
            hideNode.style.display = '';
        } else {
            showNode.style.display = '';
            hideNode.style.display = 'none';
        }
        onClick(isShow);
    });
    return btn;
}