Skip to content

S-143608: Add Clean Choice, a Chromium extension that equalizes lopsided upsell choices - #4

Draft
EllAchE wants to merge 1 commit into
mainfrom
s-143608-clean-choice-extension
Draft

EllAchE wants to merge 1 commit into
mainfrom
s-143608-clean-choice-extension

Conversation

@EllAchE

@EllAchE EllAchE commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Why

Checkout pages often put a large filled "Upgrade" button next to a small grey "Continue" link, so declining the upsell takes more effort than accepting it. Clean Choice finds those pairs and, when asked, shows both options as equal buttons.

Summary

  • New clean-choice/ package: a Manifest V3 extension (activeTab, scripting, storage) that runs only when its toolbar button is clicked.
  • It pairs an offer control with a decline control that share a container, form or dialog, and flags the pair when at least two signals hold: filled vs unfilled, a link-styled decline, 3× size, larger or bolder text, low contrast, or a decline that Tab can't reach.
  • Show both equally replaces the pair with two same-size buttons that call the originals' click(). Undo and Off for this site restore the page.
  • HTML fixtures and Playwright tests cover asymmetric, balanced, disabled, replaced, no-price and unrelated pages.

Test plan

  • bun test in clean-choice/: 13 pass, 0 fail
  • tsc --noEmit is clean

🤖 Generated with Claude Code

… choices

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@EllAchE EllAchE added the claude PR primarily authored by Claude label Sep 25, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T09:39:48.687930Z 95daa76 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95daa761a3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +296 to +298
function syncDisabled(entry) {
entry.offerButton.disabled = isDisabled(entry.pair.offer);
entry.declineButton.disabled = isDisabled(entry.pair.decline);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid observing writes to the replacement buttons

When either original control remains disabled and any observed DOM mutation invokes reconcile, syncDisabled writes disabled = true to the already-disabled replacement button. The observer covers the entire body, including that button, so the attribute write schedules another callback and can create an endless mutation cycle that starves a dynamic checkout page. Compare the current state before writing or exclude the generated panel from observation.

Useful? React with 👍 / 👎.

return;
}
unmark(pair);
entry.pair = replacement;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refresh displayed labels after a control rerender

When a checkout rerenders the same-named controls after changing the price, replacement contains the new price but assigning it here never updates offerButton.textContent, declineButton.textContent, or the title initialized in apply. The panel can therefore continue displaying the old price while clicking the newly rendered control at a different price; recompute the generated labels whenever the pair is replaced.

Useful? React with 👍 / 👎.

return;
}
entry.observer.disconnect();
const replacement = findPairs().find((p) => p.offerName === pair.offerName && p.declineName === pair.declineName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match rerendered controls within their original scope

When a page contains multiple add-ons with identical generic labels, such as several Add/No thanks pairs, rerendering a later applied pair makes this global first-match select an earlier still-live pair. The extension then hides and forwards clicks to the wrong add-on while leaving the actual replacement untouched; constrain reconciliation using the original container or another stable positional identity rather than names alone.

Useful? React with 👍 / 👎.

@EllAchE
EllAchE marked this pull request as draft September 25, 2026 12:48
@EllAchE EllAchE self-assigned this Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude PR primarily authored by Claude

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant