From 081688ff39e30eeeab6cd4d302821c58a83a1542 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 11:21:36 +0000 Subject: [PATCH 01/10] add GitHub PR Approve Helper userscript Auto-fills the review comment with a random LGTM-style text when selecting Approve in the GitHub pull request review dialog. Supports both the new react review dialog and the legacy review dropdown. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JL2YxajmCPuDRWyejSpCJM --- README.md | 9 ++- github-pr-approve-helper.user.js | 125 +++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 github-pr-approve-helper.user.js diff --git a/README.md b/README.md index 6cb0f2a..3f3bda1 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,15 @@ Misc Userscripts 2. Get information or install: - Install a script directly from GitHub by clicking on the "install" link in the table below. -| Userscript Wiki | Direct Install | Created | Updated | -| ----------------------------- | :----------------: | :--------: | :--------: | -| [CloudWatch Helper][cwh-wiki] | [install][cwh-raw] | 30.03.2021 | 13.12.2021 | +| Userscript Wiki | Direct Install | Created | Updated | +| ------------------------------------- | :-----------------: | :--------: | :--------: | +| [CloudWatch Helper][cwh-wiki] | [install][cwh-raw] | 30.03.2021 | 13.12.2021 | +| [GitHub PR Approve Helper][gpah-wiki] | [install][gpah-raw] | 04.08.2026 | 04.08.2026 | [cwh-wiki]: https://github.com/MishaKav/userscripts/wiki/CloudWatch-Helper [cwh-raw]: https://raw.githubusercontent.com/MishaKav/userscripts/main/cloudwatch-helper.user.js +[gpah-wiki]: https://github.com/MishaKav/userscripts/wiki/GitHub-PR-Approve-Helper +[gpah-raw]: https://raw.githubusercontent.com/MishaKav/userscripts/main/github-pr-approve-helper.user.js ## Updating diff --git a/github-pr-approve-helper.user.js b/github-pr-approve-helper.user.js new file mode 100644 index 0000000..66f3f16 --- /dev/null +++ b/github-pr-approve-helper.user.js @@ -0,0 +1,125 @@ +// ==UserScript== +// @name GitHub PR Approve Helper +// @namespace https://github.com/MishaKav/userscripts/github-pr-approve-helper +// @version 1.0.0 +// @description A userscript that auto-fills the review comment with LGTM when you select Approve in the GitHub pull request review dialog +// @author Misha Kav +// @copyright 2026, Misha Kav +// @match https://github.com/* +// @icon https://github.com/favicon.ico +// @grant none +// @run-at document-end +// @updateURL https://raw.githubusercontent.com/MishaKav/userscripts/main/github-pr-approve-helper.user.js +// @downloadURL https://raw.githubusercontent.com/MishaKav/userscripts/main/github-pr-approve-helper.user.js +// @supportURL https://github.com/MishaKav/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + // one of these is picked randomly on every approve, add/remove as you like + const APPROVE_COMMENTS = ['LGTM', 'LGTM πŸ‘', 'Looks good to me!']; + + // marker for text we inserted, so we never delete anything the user typed + const AUTO_FILL_ATTRIBUTE = 'data-approve-helper-text'; + + // 'pull_request_review[event]' - legacy "Review changes" dropdown + // 'reviewEvent' - new react "Finish your review" dialog + const REVIEW_RADIO_NAMES = ['pull_request_review[event]', 'reviewEvent']; + + const SELECTORS = { + REVIEW_CONTAINER: + '#review-changes-modal, form[action*="/reviews"], dialog, [role="dialog"]', + REVIEW_TEXTAREAS: [ + 'textarea#pull_request_review_body', // legacy dropdown + 'textarea[name="pull_request_review[body]"]', // legacy fallback + 'textarea[aria-label="Markdown value"]', // new react dialog + 'textarea[placeholder="Leave a comment"]', // react fallback + 'textarea', // last resort, scoped to the review container only + ], + }; + + const isPullRequestPage = () => /\/pull\/\d+/.test(location.pathname); + + const isReviewRadio = (el) => + el.matches?.('input[type="radio"]') && + (REVIEW_RADIO_NAMES.includes(el.name) || + /^(approve|comment|reject|request[ _-]?changes)$/i.test(el.value)); + + const isApprove = (radio) => /^approve$/i.test(radio.value); + + const pickComment = () => + APPROVE_COMMENTS[Math.floor(Math.random() * APPROVE_COMMENTS.length)]; + + const getReviewTextarea = (container) => { + for (const selector of SELECTORS.REVIEW_TEXTAREAS) { + const textarea = container.querySelector(selector); + if (textarea) { + return textarea; + } + } + return null; + }; + + // react-controlled textarea ignores a plain `.value =`, so assign through + // the native prototype setter and fire bubbled events for react to notice + const setNativeValue = (textarea, text) => { + Object.getOwnPropertyDescriptor( + HTMLTextAreaElement.prototype, + 'value', + ).set.call(textarea, text); + textarea.dispatchEvent(new Event('input', { bubbles: true })); + textarea.dispatchEvent(new Event('change', { bubbles: true })); + }; + + const fillComment = (textarea) => { + // never overwrite anything the user already typed + if (textarea.value.trim() !== '') { + return; + } + + const comment = pickComment(); + setNativeValue(textarea, comment); + textarea.setAttribute(AUTO_FILL_ATTRIBUTE, comment); + console.log(`[GitHub PR Approve Helper] filled review comment: "${comment}"`); + }; + + const clearAutoComment = (textarea) => { + const autoText = textarea.getAttribute(AUTO_FILL_ATTRIBUTE); + + // not inserted by us, or edited by the user since - keep it + if (autoText === null || textarea.value !== autoText) { + return; + } + + setNativeValue(textarea, ''); + textarea.removeAttribute(AUTO_FILL_ATTRIBUTE); + console.log('[GitHub PR Approve Helper] cleared auto comment'); + }; + + const onReviewOptionChange = (event) => { + const radio = event.target; + + if (!isPullRequestPage() || !isReviewRadio(radio) || !radio.checked) { + return; + } + + const container = radio.closest(SELECTORS.REVIEW_CONTAINER); + const textarea = container && getReviewTextarea(container); + + if (!textarea) { + return; + } + + if (isApprove(radio)) { + fillComment(textarea); + } else { + clearAutoComment(textarea); + } + }; + + // capture-phase listener on document survives github's soft navigation + // and the review dialog being re-created every time it opens + document.addEventListener('change', onReviewOptionChange, true); + console.log('[GitHub PR Approve Helper] ready'); +})(); From 23bf2f5df8d05a48ef99c14919df95157fba481a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 11:23:26 +0000 Subject: [PATCH 02/10] limit GitHub PR Approve Helper to linear-b org, bump version 1.1.0 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JL2YxajmCPuDRWyejSpCJM --- github-pr-approve-helper.user.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/github-pr-approve-helper.user.js b/github-pr-approve-helper.user.js index 66f3f16..07c8c69 100644 --- a/github-pr-approve-helper.user.js +++ b/github-pr-approve-helper.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name GitHub PR Approve Helper // @namespace https://github.com/MishaKav/userscripts/github-pr-approve-helper -// @version 1.0.0 +// @version 1.1.0 // @description A userscript that auto-fills the review comment with LGTM when you select Approve in the GitHub pull request review dialog // @author Misha Kav // @copyright 2026, Misha Kav @@ -20,6 +20,11 @@ // one of these is picked randomly on every approve, add/remove as you like const APPROVE_COMMENTS = ['LGTM', 'LGTM πŸ‘', 'Looks good to me!']; + // the script only fills comments on PRs of these orgs/users, add as you like + // (kept as a runtime check instead of @match, so it survives github's + // soft navigation between orgs) + const ALLOWED_ORGS = ['linear-b']; + // marker for text we inserted, so we never delete anything the user typed const AUTO_FILL_ATTRIBUTE = 'data-approve-helper-text'; @@ -41,6 +46,11 @@ const isPullRequestPage = () => /\/pull\/\d+/.test(location.pathname); + const isAllowedOrgPage = () => + ALLOWED_ORGS.some((org) => + location.pathname.toLowerCase().startsWith(`/${org.toLowerCase()}/`), + ); + const isReviewRadio = (el) => el.matches?.('input[type="radio"]') && (REVIEW_RADIO_NAMES.includes(el.name) || @@ -100,7 +110,12 @@ const onReviewOptionChange = (event) => { const radio = event.target; - if (!isPullRequestPage() || !isReviewRadio(radio) || !radio.checked) { + if ( + !isPullRequestPage() || + !isAllowedOrgPage() || + !isReviewRadio(radio) || + !radio.checked + ) { return; } From e6f3fecc3770f75ef8100eccb3f1ae79ada65236 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 11:27:48 +0000 Subject: [PATCH 03/10] restrict approve helper to linear-b via @match, add comment dropdown - narrow @match to https://github.com/linear-b/* - expand the list of approve comments - inject a dropdown above the review textarea to pick a specific comment (or a random one) on demand - bump version 1.2.0 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JL2YxajmCPuDRWyejSpCJM --- github-pr-approve-helper.user.js | 95 ++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/github-pr-approve-helper.user.js b/github-pr-approve-helper.user.js index 07c8c69..a376d21 100644 --- a/github-pr-approve-helper.user.js +++ b/github-pr-approve-helper.user.js @@ -1,11 +1,11 @@ // ==UserScript== // @name GitHub PR Approve Helper // @namespace https://github.com/MishaKav/userscripts/github-pr-approve-helper -// @version 1.1.0 +// @version 1.2.0 // @description A userscript that auto-fills the review comment with LGTM when you select Approve in the GitHub pull request review dialog // @author Misha Kav // @copyright 2026, Misha Kav -// @match https://github.com/* +// @match https://github.com/linear-b/* // @icon https://github.com/favicon.ico // @grant none // @run-at document-end @@ -18,16 +18,30 @@ 'use strict'; // one of these is picked randomly on every approve, add/remove as you like - const APPROVE_COMMENTS = ['LGTM', 'LGTM πŸ‘', 'Looks good to me!']; + const APPROVE_COMMENTS = [ + 'LGTM', + 'LGTM πŸ‘', + 'LGTM πŸš€', + 'Looks good to me!', + 'Looks great, approved βœ…', + 'Nice work! πŸ‘', + 'Great job! πŸŽ‰', + 'Ship it! 🚒', + 'Well done πŸ’ͺ', + 'Clean and simple, LGTM πŸ”₯', + ]; // the script only fills comments on PRs of these orgs/users, add as you like - // (kept as a runtime check instead of @match, so it survives github's - // soft navigation between orgs) + // (checked at runtime in addition to @match, so it stays correct when + // github soft-navigates between orgs without a full page load) const ALLOWED_ORGS = ['linear-b']; // marker for text we inserted, so we never delete anything the user typed const AUTO_FILL_ATTRIBUTE = 'data-approve-helper-text'; + const DROPDOWN_ID = 'gpah-comment-select'; + const RANDOM_OPTION_VALUE = '__random__'; + // 'pull_request_review[event]' - legacy "Review changes" dropdown // 'reviewEvent' - new react "Finish your review" dialog const REVIEW_RADIO_NAMES = ['pull_request_review[event]', 'reviewEvent']; @@ -35,6 +49,9 @@ const SELECTORS = { REVIEW_CONTAINER: '#review-changes-modal, form[action*="/reviews"], dialog, [role="dialog"]', + REVIEW_RADIOS: REVIEW_RADIO_NAMES.map( + (name) => `input[type="radio"][name="${name}"]`, + ).join(', '), REVIEW_TEXTAREAS: [ 'textarea#pull_request_review_body', // legacy dropdown 'textarea[name="pull_request_review[body]"]', // legacy fallback @@ -133,8 +150,76 @@ } }; + const createCommentDropdown = () => { + const select = document.createElement('select'); + select.id = DROPDOWN_ID; + select.className = 'form-select'; + select.style.cssText = 'width: 100%; margin-bottom: 8px;'; + + const options = [ + { value: '', text: 'πŸ’¬ Insert approve comment…' }, + { value: RANDOM_OPTION_VALUE, text: '🎲 Random' }, + ...APPROVE_COMMENTS.map((comment) => ({ value: comment, text: comment })), + ]; + + for (const { value, text } of options) { + const option = document.createElement('option'); + option.value = value; + option.textContent = text; + select.appendChild(option); + } + + select.addEventListener('change', () => { + const comment = + select.value === RANDOM_OPTION_VALUE ? pickComment() : select.value; + // back to the placeholder, so the same option can be picked again + select.selectedIndex = 0; + + const container = select.closest(SELECTORS.REVIEW_CONTAINER); + const textarea = container && getReviewTextarea(container); + + if (!comment || !textarea) { + return; + } + + // explicit pick from the dropdown replaces whatever is in the box + setNativeValue(textarea, comment); + textarea.setAttribute(AUTO_FILL_ATTRIBUTE, comment); + textarea.focus(); + console.log(`[GitHub PR Approve Helper] inserted comment: "${comment}"`); + }); + + return select; + }; + + const injectCommentDropdowns = () => { + if (!isPullRequestPage() || !isAllowedOrgPage()) { + return; + } + + const containers = document.querySelectorAll(SELECTORS.REVIEW_CONTAINER); + + for (const container of containers) { + // only real review dialogs (they contain the approve/comment radios) + const isReviewDialog = container.querySelector(SELECTORS.REVIEW_RADIOS); + const textarea = isReviewDialog && getReviewTextarea(container); + + if (!textarea || container.querySelector(`#${DROPDOWN_ID}`)) { + continue; + } + + textarea.before(createCommentDropdown()); + } + }; + + // the review dialog is created on demand (and react re-creates it on every + // open), so watch the page and add the dropdown whenever it shows up + const observer = new MutationObserver(injectCommentDropdowns); + observer.observe(document.body, { childList: true, subtree: true }); + // capture-phase listener on document survives github's soft navigation // and the review dialog being re-created every time it opens document.addEventListener('change', onReviewOptionChange, true); + injectCommentDropdowns(); console.log('[GitHub PR Approve Helper] ready'); })(); From 8126f972bad4c1c7a095c8f02616809fe01956e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 11:35:00 +0000 Subject: [PATCH 04/10] handle pre-selected approve and click fallback in approve helper - fill the comment when the review dialog opens with approve already selected (no change event fires in that case) - add a capture-phase click fallback for clicks on the label or on an already-selected approve radio - resolve event targets through composedPath for shadow dom safety - more console diagnostics, bump version 1.3.0 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JL2YxajmCPuDRWyejSpCJM --- github-pr-approve-helper.user.js | 89 +++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/github-pr-approve-helper.user.js b/github-pr-approve-helper.user.js index a376d21..117ac8d 100644 --- a/github-pr-approve-helper.user.js +++ b/github-pr-approve-helper.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name GitHub PR Approve Helper // @namespace https://github.com/MishaKav/userscripts/github-pr-approve-helper -// @version 1.2.0 +// @version 1.3.0 // @description A userscript that auto-fills the review comment with LGTM when you select Approve in the GitHub pull request review dialog // @author Misha Kav // @copyright 2026, Misha Kav @@ -39,6 +39,10 @@ // marker for text we inserted, so we never delete anything the user typed const AUTO_FILL_ATTRIBUTE = 'data-approve-helper-text'; + // marker for dialogs we already processed, so the "approve pre-selected on + // open" fill happens once per open and never fights the user + const SEEN_ATTRIBUTE = 'data-approve-helper-seen'; + const DROPDOWN_ID = 'gpah-comment-select'; const RANDOM_OPTION_VALUE = '__random__'; @@ -69,7 +73,7 @@ ); const isReviewRadio = (el) => - el.matches?.('input[type="radio"]') && + el?.matches?.('input[type="radio"]') && (REVIEW_RADIO_NAMES.includes(el.name) || /^(approve|comment|reject|request[ _-]?changes)$/i.test(el.value)); @@ -124,15 +128,8 @@ console.log('[GitHub PR Approve Helper] cleared auto comment'); }; - const onReviewOptionChange = (event) => { - const radio = event.target; - - if ( - !isPullRequestPage() || - !isAllowedOrgPage() || - !isReviewRadio(radio) || - !radio.checked - ) { + const handleReviewRadio = (radio) => { + if (!radio.checked) { return; } @@ -150,6 +147,44 @@ } }; + // use composedPath so the real target is found even inside shadow dom + const getEventTarget = (event) => { + const target = event.composedPath?.()[0] ?? event.target; + return target instanceof Element ? target : null; + }; + + const onReviewOptionChange = (event) => { + if (!isPullRequestPage() || !isAllowedOrgPage()) { + return; + } + + const radio = getEventTarget(event); + + if (isReviewRadio(radio)) { + handleReviewRadio(radio); + } + }; + + // fallback for clicks that don't produce a change event, e.g. clicking the + // approve option when it's already selected, or clicking its label + const onReviewOptionClick = (event) => { + if (!isPullRequestPage() || !isAllowedOrgPage()) { + return; + } + + const target = getEventTarget(event); + const radio = target?.matches?.('input[type="radio"]') + ? target + : target?.closest('label')?.control; + + if (!isReviewRadio(radio)) { + return; + } + + // let the browser/react finish updating the checked state first + setTimeout(() => handleReviewRadio(radio), 0); + }; + const createCommentDropdown = () => { const select = document.createElement('select'); select.id = DROPDOWN_ID; @@ -192,7 +227,7 @@ return select; }; - const injectCommentDropdowns = () => { + const processReviewContainers = () => { if (!isPullRequestPage() || !isAllowedOrgPage()) { return; } @@ -201,25 +236,39 @@ for (const container of containers) { // only real review dialogs (they contain the approve/comment radios) - const isReviewDialog = container.querySelector(SELECTORS.REVIEW_RADIOS); - const textarea = isReviewDialog && getReviewTextarea(container); + const radios = [...container.querySelectorAll(SELECTORS.REVIEW_RADIOS)]; + const textarea = radios.length > 0 && getReviewTextarea(container); - if (!textarea || container.querySelector(`#${DROPDOWN_ID}`)) { + if (!textarea) { continue; } - textarea.before(createCommentDropdown()); + if (!container.querySelector(`#${DROPDOWN_ID}`)) { + textarea.before(createCommentDropdown()); + console.log('[GitHub PR Approve Helper] review dialog found, dropdown added'); + } + + // the dialog can open with approve already pre-selected (github + // remembers the last choice), which fires no change event - fill once + if (!container.hasAttribute(SEEN_ATTRIBUTE)) { + container.setAttribute(SEEN_ATTRIBUTE, 'true'); + + if (radios.find(isApprove)?.checked) { + fillComment(textarea); + } + } } }; // the review dialog is created on demand (and react re-creates it on every - // open), so watch the page and add the dropdown whenever it shows up - const observer = new MutationObserver(injectCommentDropdowns); + // open), so watch the page and process it whenever it shows up + const observer = new MutationObserver(processReviewContainers); observer.observe(document.body, { childList: true, subtree: true }); - // capture-phase listener on document survives github's soft navigation + // capture-phase listeners on document survive github's soft navigation // and the review dialog being re-created every time it opens document.addEventListener('change', onReviewOptionChange, true); - injectCommentDropdowns(); + document.addEventListener('click', onReviewOptionClick, true); + processReviewContainers(); console.log('[GitHub PR Approve Helper] ready'); })(); From 381620a137bae60e2f14b14cf30935a6cd1eafbe Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 11:57:23 +0000 Subject: [PATCH 05/10] flash an on-page active badge in approve helper, bump 1.3.1 Shows a small self-removing badge on PR pages, so it's visible the script is injected and running without opening devtools. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JL2YxajmCPuDRWyejSpCJM --- github-pr-approve-helper.user.js | 39 +++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/github-pr-approve-helper.user.js b/github-pr-approve-helper.user.js index 117ac8d..03419a7 100644 --- a/github-pr-approve-helper.user.js +++ b/github-pr-approve-helper.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name GitHub PR Approve Helper // @namespace https://github.com/MishaKav/userscripts/github-pr-approve-helper -// @version 1.3.0 +// @version 1.3.1 // @description A userscript that auto-fills the review comment with LGTM when you select Approve in the GitHub pull request review dialog // @author Misha Kav // @copyright 2026, Misha Kav @@ -43,7 +43,12 @@ // open" fill happens once per open and never fights the user const SEEN_ATTRIBUTE = 'data-approve-helper-seen'; + // flash a small badge on PR pages, so it's visible the script is running + // without opening devtools - set to false to disable + const SHOW_ACTIVE_BADGE = true; + const DROPDOWN_ID = 'gpah-comment-select'; + const BADGE_ID = 'gpah-active-badge'; const RANDOM_OPTION_VALUE = '__random__'; // 'pull_request_review[event]' - legacy "Review changes" dropdown @@ -227,6 +232,37 @@ return select; }; + const showActiveBadge = () => { + if ( + !SHOW_ACTIVE_BADGE || + !isPullRequestPage() || + !isAllowedOrgPage() || + document.getElementById(BADGE_ID) + ) { + return; + } + + const badge = document.createElement('div'); + badge.id = BADGE_ID; + badge.textContent = 'βœ… Approve Helper active'; + badge.style.cssText = [ + 'position: fixed', + 'bottom: 16px', + 'right: 16px', + 'padding: 6px 12px', + 'background: #1f883d', + 'color: #fff', + 'font: 12px -apple-system, sans-serif', + 'border-radius: 6px', + 'box-shadow: 0 3px 12px rgba(0, 0, 0, 0.3)', + 'pointer-events: none', + 'z-index: 2147483647', + ].join(';'); + + document.body.appendChild(badge); + setTimeout(() => badge.remove(), 2500); + }; + const processReviewContainers = () => { if (!isPullRequestPage() || !isAllowedOrgPage()) { return; @@ -270,5 +306,6 @@ document.addEventListener('change', onReviewOptionChange, true); document.addEventListener('click', onReviewOptionClick, true); processReviewContainers(); + showActiveBadge(); console.log('[GitHub PR Approve Helper] ready'); })(); From efc1e9ccb6cb35bc4cf71265ad0d1d7d5f687f9f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 12:04:37 +0000 Subject: [PATCH 06/10] run approve helper in isolated world via @sandbox DOM, bump 1.3.2 GitHub's trusted-types CSP breaks tampermonkey's page-context (MAIN world) injection with a mangled SyntaxError from appendChild. The script only needs DOM access, so run it in the extension's isolated world instead, which bypasses the page CSP entirely. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JL2YxajmCPuDRWyejSpCJM --- github-pr-approve-helper.user.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/github-pr-approve-helper.user.js b/github-pr-approve-helper.user.js index 03419a7..61ad492 100644 --- a/github-pr-approve-helper.user.js +++ b/github-pr-approve-helper.user.js @@ -1,13 +1,14 @@ // ==UserScript== // @name GitHub PR Approve Helper // @namespace https://github.com/MishaKav/userscripts/github-pr-approve-helper -// @version 1.3.1 +// @version 1.3.2 // @description A userscript that auto-fills the review comment with LGTM when you select Approve in the GitHub pull request review dialog // @author Misha Kav // @copyright 2026, Misha Kav // @match https://github.com/linear-b/* // @icon https://github.com/favicon.ico // @grant none +// @sandbox DOM // @run-at document-end // @updateURL https://raw.githubusercontent.com/MishaKav/userscripts/main/github-pr-approve-helper.user.js // @downloadURL https://raw.githubusercontent.com/MishaKav/userscripts/main/github-pr-approve-helper.user.js From ed6a2cf42aac9300645eaeced4b6832bce8f150e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 12:06:38 +0000 Subject: [PATCH 07/10] add debug logging to approve helper, bump 1.4.0 Logs every radio change/click with full attribute details and dumps the radios/textareas of each dialog once, so real github markup can be compared against the selectors. DEBUG const to silence later. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JL2YxajmCPuDRWyejSpCJM --- github-pr-approve-helper.user.js | 62 ++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/github-pr-approve-helper.user.js b/github-pr-approve-helper.user.js index 61ad492..756611c 100644 --- a/github-pr-approve-helper.user.js +++ b/github-pr-approve-helper.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name GitHub PR Approve Helper // @namespace https://github.com/MishaKav/userscripts/github-pr-approve-helper -// @version 1.3.2 +// @version 1.4.0 // @description A userscript that auto-fills the review comment with LGTM when you select Approve in the GitHub pull request review dialog // @author Misha Kav // @copyright 2026, Misha Kav @@ -18,6 +18,12 @@ (function () { 'use strict'; + // keep in sync with @version above, shown in the logs and the badge + const VERSION = '1.4.0'; + + // verbose console logs for debugging - set to false to silence + const DEBUG = true; + // one of these is picked randomly on every approve, add/remove as you like const APPROVE_COMMENTS = [ 'LGTM', @@ -71,6 +77,41 @@ ], }; + const debug = (...args) => DEBUG && console.log('[GPAH debug]', ...args); + + // compact description of an input/textarea for the debug logs + const describeField = (el) => ({ + tag: el.tagName.toLowerCase(), + type: el.type || null, + name: el.name || null, + value: el.tagName === 'TEXTAREA' ? `(${el.value.length} chars)` : el.value, + checked: el.checked ?? null, + id: el.id || null, + ariaLabel: el.getAttribute('aria-label'), + placeholder: el.getAttribute('placeholder'), + }); + + // dump every radio/textarea of each dialog-ish container once, so real + // github markup can be compared against our selectors + const dumpedContainers = new WeakSet(); + const dumpContainer = (container) => { + if (!DEBUG || dumpedContainers.has(container)) { + return; + } + dumpedContainers.add(container); + debug( + `container <${container.tagName.toLowerCase()}> id="${container.id}" role="${container.getAttribute('role')}"`, + JSON.stringify( + { + radios: [...container.querySelectorAll('input[type="radio"]')].map(describeField), + textareas: [...container.querySelectorAll('textarea')].map(describeField), + }, + null, + 2, + ), + ); + }; + const isPullRequestPage = () => /\/pull\/\d+/.test(location.pathname); const isAllowedOrgPage = () => @@ -136,6 +177,7 @@ const handleReviewRadio = (radio) => { if (!radio.checked) { + debug('radio not checked, skip', describeField(radio)); return; } @@ -143,6 +185,10 @@ const textarea = container && getReviewTextarea(container); if (!textarea) { + debug( + container ? 'no textarea in container, skip' : 'no container for radio, skip', + describeField(radio), + ); return; } @@ -166,6 +212,10 @@ const radio = getEventTarget(event); + if (radio?.matches?.('input[type="radio"]')) { + debug('change on radio', describeField(radio), isReviewRadio(radio) ? 'IS review radio' : 'NOT review radio'); + } + if (isReviewRadio(radio)) { handleReviewRadio(radio); } @@ -183,6 +233,10 @@ ? target : target?.closest('label')?.control; + if (radio) { + debug('click resolved to radio', describeField(radio), isReviewRadio(radio) ? 'IS review radio' : 'NOT review radio'); + } + if (!isReviewRadio(radio)) { return; } @@ -245,7 +299,7 @@ const badge = document.createElement('div'); badge.id = BADGE_ID; - badge.textContent = 'βœ… Approve Helper active'; + badge.textContent = `βœ… Approve Helper v${VERSION} active`; badge.style.cssText = [ 'position: fixed', 'bottom: 16px', @@ -272,6 +326,8 @@ const containers = document.querySelectorAll(SELECTORS.REVIEW_CONTAINER); for (const container of containers) { + dumpContainer(container); + // only real review dialogs (they contain the approve/comment radios) const radios = [...container.querySelectorAll(SELECTORS.REVIEW_RADIOS)]; const textarea = radios.length > 0 && getReviewTextarea(container); @@ -308,5 +364,5 @@ document.addEventListener('click', onReviewOptionClick, true); processReviewContainers(); showActiveBadge(); - console.log('[GitHub PR Approve Helper] ready'); + console.log(`[GitHub PR Approve Helper] v${VERSION} ready on ${location.href}`); })(); From 2fdba3c7db5d189e1289700775ea97056042aea6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 12:11:43 +0000 Subject: [PATCH 08/10] auto-select approve, default LGTM text, header dropdown, bump 1.5.0 - automatically select the approve option when the review dialog opens (AUTO_SELECT_APPROVE const to disable) - auto-fill always inserts the deterministic DEFAULT_COMMENT ('LGTM'); the dropdown replaces it with an alternative on demand - restyle the dropdown as a compact pill floating in the dialog header next to the close button, instead of squeezing into the editor Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JL2YxajmCPuDRWyejSpCJM --- github-pr-approve-helper.user.js | 62 ++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/github-pr-approve-helper.user.js b/github-pr-approve-helper.user.js index 756611c..837137a 100644 --- a/github-pr-approve-helper.user.js +++ b/github-pr-approve-helper.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name GitHub PR Approve Helper // @namespace https://github.com/MishaKav/userscripts/github-pr-approve-helper -// @version 1.4.0 +// @version 1.5.0 // @description A userscript that auto-fills the review comment with LGTM when you select Approve in the GitHub pull request review dialog // @author Misha Kav // @copyright 2026, Misha Kav @@ -19,12 +19,18 @@ 'use strict'; // keep in sync with @version above, shown in the logs and the badge - const VERSION = '1.4.0'; + const VERSION = '1.5.0'; // verbose console logs for debugging - set to false to silence const DEBUG = true; - // one of these is picked randomly on every approve, add/remove as you like + // automatically select the approve option when the review dialog opens + const AUTO_SELECT_APPROVE = true; + + // always inserted on approve - pick from the dropdown to replace it + const DEFAULT_COMMENT = 'LGTM'; + + // the alternatives offered in the dropdown ('🎲 Random' picks one of these) const APPROVE_COMMENTS = [ 'LGTM', 'LGTM πŸ‘', @@ -156,10 +162,9 @@ return; } - const comment = pickComment(); - setNativeValue(textarea, comment); - textarea.setAttribute(AUTO_FILL_ATTRIBUTE, comment); - console.log(`[GitHub PR Approve Helper] filled review comment: "${comment}"`); + setNativeValue(textarea, DEFAULT_COMMENT); + textarea.setAttribute(AUTO_FILL_ATTRIBUTE, DEFAULT_COMMENT); + console.log(`[GitHub PR Approve Helper] filled review comment: "${DEFAULT_COMMENT}"`); }; const clearAutoComment = (textarea) => { @@ -248,11 +253,26 @@ const createCommentDropdown = () => { const select = document.createElement('select'); select.id = DROPDOWN_ID; - select.className = 'form-select'; - select.style.cssText = 'width: 100%; margin-bottom: 8px;'; + // compact pill floating in the dialog header, next to the close button - + // it never disturbs the layout of the react-rendered dialog content + select.style.cssText = [ + 'position: absolute', + 'top: 12px', + 'right: 48px', + 'max-width: 200px', + 'padding: 4px 8px', + 'font-size: 12px', + 'font-weight: 500', + 'color: #1f2328', + 'background: #f6f8fa', + 'border: 1px solid #d0d7de', + 'border-radius: 6px', + 'cursor: pointer', + 'z-index: 100', + ].join(';'); const options = [ - { value: '', text: 'πŸ’¬ Insert approve comment…' }, + { value: '', text: `πŸ’¬ ${DEFAULT_COMMENT}…` }, { value: RANDOM_OPTION_VALUE, text: '🎲 Random' }, ...APPROVE_COMMENTS.map((comment) => ({ value: comment, text: comment })), ]; @@ -337,16 +357,30 @@ } if (!container.querySelector(`#${DROPDOWN_ID}`)) { - textarea.before(createCommentDropdown()); + // anchor the absolutely-positioned dropdown to the dialog itself + if (getComputedStyle(container).position === 'static') { + container.style.position = 'relative'; + } + container.appendChild(createCommentDropdown()); console.log('[GitHub PR Approve Helper] review dialog found, dropdown added'); } - // the dialog can open with approve already pre-selected (github - // remembers the last choice), which fires no change event - fill once + // once per dialog open: select approve (github opens with the last + // used option, and a pre-selected radio fires no change event) if (!container.hasAttribute(SEEN_ATTRIBUTE)) { container.setAttribute(SEEN_ATTRIBUTE, 'true'); + const approveRadio = radios.find(isApprove); + + if (!approveRadio) { + continue; + } + + if (AUTO_SELECT_APPROVE && !approveRadio.checked) { + debug('auto-selecting approve'); + approveRadio.click(); + } - if (radios.find(isApprove)?.checked) { + if (approveRadio.checked) { fillComment(textarea); } } From f00e4bb4b782c1095fb8ffbb6bee90d5ca303a30 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 12:18:36 +0000 Subject: [PATCH 09/10] clean debug logs, trim comment list, reset version to 1.0.0 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JL2YxajmCPuDRWyejSpCJM --- github-pr-approve-helper.user.js | 65 ++------------------------------ 1 file changed, 3 insertions(+), 62 deletions(-) diff --git a/github-pr-approve-helper.user.js b/github-pr-approve-helper.user.js index 837137a..0b71cb6 100644 --- a/github-pr-approve-helper.user.js +++ b/github-pr-approve-helper.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name GitHub PR Approve Helper // @namespace https://github.com/MishaKav/userscripts/github-pr-approve-helper -// @version 1.5.0 +// @version 1.0.0 // @description A userscript that auto-fills the review comment with LGTM when you select Approve in the GitHub pull request review dialog // @author Misha Kav // @copyright 2026, Misha Kav @@ -19,10 +19,7 @@ 'use strict'; // keep in sync with @version above, shown in the logs and the badge - const VERSION = '1.5.0'; - - // verbose console logs for debugging - set to false to silence - const DEBUG = true; + const VERSION = '1.0.0'; // automatically select the approve option when the review dialog opens const AUTO_SELECT_APPROVE = true; @@ -33,13 +30,9 @@ // the alternatives offered in the dropdown ('🎲 Random' picks one of these) const APPROVE_COMMENTS = [ 'LGTM', - 'LGTM πŸ‘', - 'LGTM πŸš€', + 'Nice', 'Looks good to me!', - 'Looks great, approved βœ…', - 'Nice work! πŸ‘', 'Great job! πŸŽ‰', - 'Ship it! 🚒', 'Well done πŸ’ͺ', 'Clean and simple, LGTM πŸ”₯', ]; @@ -83,41 +76,6 @@ ], }; - const debug = (...args) => DEBUG && console.log('[GPAH debug]', ...args); - - // compact description of an input/textarea for the debug logs - const describeField = (el) => ({ - tag: el.tagName.toLowerCase(), - type: el.type || null, - name: el.name || null, - value: el.tagName === 'TEXTAREA' ? `(${el.value.length} chars)` : el.value, - checked: el.checked ?? null, - id: el.id || null, - ariaLabel: el.getAttribute('aria-label'), - placeholder: el.getAttribute('placeholder'), - }); - - // dump every radio/textarea of each dialog-ish container once, so real - // github markup can be compared against our selectors - const dumpedContainers = new WeakSet(); - const dumpContainer = (container) => { - if (!DEBUG || dumpedContainers.has(container)) { - return; - } - dumpedContainers.add(container); - debug( - `container <${container.tagName.toLowerCase()}> id="${container.id}" role="${container.getAttribute('role')}"`, - JSON.stringify( - { - radios: [...container.querySelectorAll('input[type="radio"]')].map(describeField), - textareas: [...container.querySelectorAll('textarea')].map(describeField), - }, - null, - 2, - ), - ); - }; - const isPullRequestPage = () => /\/pull\/\d+/.test(location.pathname); const isAllowedOrgPage = () => @@ -182,7 +140,6 @@ const handleReviewRadio = (radio) => { if (!radio.checked) { - debug('radio not checked, skip', describeField(radio)); return; } @@ -190,10 +147,6 @@ const textarea = container && getReviewTextarea(container); if (!textarea) { - debug( - container ? 'no textarea in container, skip' : 'no container for radio, skip', - describeField(radio), - ); return; } @@ -217,10 +170,6 @@ const radio = getEventTarget(event); - if (radio?.matches?.('input[type="radio"]')) { - debug('change on radio', describeField(radio), isReviewRadio(radio) ? 'IS review radio' : 'NOT review radio'); - } - if (isReviewRadio(radio)) { handleReviewRadio(radio); } @@ -238,10 +187,6 @@ ? target : target?.closest('label')?.control; - if (radio) { - debug('click resolved to radio', describeField(radio), isReviewRadio(radio) ? 'IS review radio' : 'NOT review radio'); - } - if (!isReviewRadio(radio)) { return; } @@ -346,8 +291,6 @@ const containers = document.querySelectorAll(SELECTORS.REVIEW_CONTAINER); for (const container of containers) { - dumpContainer(container); - // only real review dialogs (they contain the approve/comment radios) const radios = [...container.querySelectorAll(SELECTORS.REVIEW_RADIOS)]; const textarea = radios.length > 0 && getReviewTextarea(container); @@ -362,7 +305,6 @@ container.style.position = 'relative'; } container.appendChild(createCommentDropdown()); - console.log('[GitHub PR Approve Helper] review dialog found, dropdown added'); } // once per dialog open: select approve (github opens with the last @@ -376,7 +318,6 @@ } if (AUTO_SELECT_APPROVE && !approveRadio.checked) { - debug('auto-selecting approve'); approveRadio.click(); } From 8b7b6ae1fcc563f669621727a5fc6799a92ca7a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 12:23:10 +0000 Subject: [PATCH 10/10] address copilot review: native-setter fallback, throttle observer - fall back to a plain value assignment when the native prototype setter is unavailable - coalesce mutation bursts into one scan per animation frame - re-show the active badge once per soft navigation Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JL2YxajmCPuDRWyejSpCJM --- github-pr-approve-helper.user.js | 35 ++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/github-pr-approve-helper.user.js b/github-pr-approve-helper.user.js index 0b71cb6..0ed4e3a 100644 --- a/github-pr-approve-helper.user.js +++ b/github-pr-approve-helper.user.js @@ -106,10 +106,17 @@ // react-controlled textarea ignores a plain `.value =`, so assign through // the native prototype setter and fire bubbled events for react to notice const setNativeValue = (textarea, text) => { - Object.getOwnPropertyDescriptor( + const nativeSetter = Object.getOwnPropertyDescriptor( HTMLTextAreaElement.prototype, 'value', - ).set.call(textarea, text); + )?.set; + + if (nativeSetter) { + nativeSetter.call(textarea, text); + } else { + textarea.value = text; + } + textarea.dispatchEvent(new Event('input', { bubbles: true })); textarea.dispatchEvent(new Event('change', { bubbles: true })); }; @@ -252,16 +259,22 @@ return select; }; + // shown once per page/soft-navigation, tracked by pathname + let badgeShownFor = null; + const showActiveBadge = () => { if ( !SHOW_ACTIVE_BADGE || !isPullRequestPage() || !isAllowedOrgPage() || + badgeShownFor === location.pathname || document.getElementById(BADGE_ID) ) { return; } + badgeShownFor = location.pathname; + const badge = document.createElement('div'); badge.id = BADGE_ID; badge.textContent = `βœ… Approve Helper v${VERSION} active`; @@ -329,8 +342,22 @@ }; // the review dialog is created on demand (and react re-creates it on every - // open), so watch the page and process it whenever it shows up - const observer = new MutationObserver(processReviewContainers); + // open), so watch the page and process it whenever it shows up - mutation + // bursts are coalesced into one scan per animation frame + let scanScheduled = false; + const scheduleScan = () => { + if (scanScheduled) { + return; + } + scanScheduled = true; + requestAnimationFrame(() => { + scanScheduled = false; + processReviewContainers(); + showActiveBadge(); + }); + }; + + const observer = new MutationObserver(scheduleScan); observer.observe(document.body, { childList: true, subtree: true }); // capture-phase listeners on document survive github's soft navigation