Skip to content

Instantly share code, notes, and snippets.

@shuymn
Last active March 3, 2026 15:49
Show Gist options
  • Select an option

  • Save shuymn/2bcc1232ae6f764ddec4ffd20901b0b1 to your computer and use it in GitHub Desktop.

Select an option

Save shuymn/2bcc1232ae6f764ddec4ffd20901b0b1 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Gemini Force Pro
// @namespace https://shuymn.me
// @updateURL https://gist.githubusercontent.com/shuymn/2bcc1232ae6f764ddec4ffd20901b0b1/raw/gemini-force-pro.user.js
// @downloadURL https://gist.githubusercontent.com/shuymn/2bcc1232ae6f764ddec4ffd20901b0b1/raw/gemini-force-pro.user.js
// @version 0.4.0
// @description On /app (root) ensure Pro mode. Boost frequency for first 5s. If user interacts with mode UI, pause until URL changes.
// @match https://gemini.google.com/*
// @run-at document-idle
// @grant none
// ==/UserScript==
(() => {
'use strict';
const CFG = {
// Selectors
menuButtonSel: '[data-test-id="bard-mode-menu-button"]',
proOptionSel: '[data-test-id="bard-mode-option-pro"]',
anyOptionSel: '[data-test-id^="bard-mode-option-"]',
anyItemSel: '[role="menuitemradio"], [data-test-id^="bard-mode-option-"]',
proTextRe: /pro/i,
// Frequency boost (first 5s)
initialBoostMs: 5000,
minIntervalBoostMs: 250,
minIntervalNormalMs: 1500,
mutationDebounceMsBoost: 80,
mutationDebounceMsNormal: 250,
// Action tuning
openTimeoutMs: 2500,
verifyTimeoutMs: 2500,
// Debug
debug: false,
};
const log = (...a) => CFG.debug && console.log('[force-pro]', ...a);
const t0 = Date.now();
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
const firstVisible = (list) => list.find(visible) || list[0] || null;
// --- URL gating (per your requirement) ---
function shouldHandleUrl(urlStr = location.href) {
const u = new URL(urlStr, location.origin);
return u.pathname === '/app' || u.pathname === '/app/';
}
// --- Manual lock ---
// If user interacts with mode UI on /app, we pause automation until URL changes.
let manualLock = false;
let lastHref = location.href;
function setManualLock(reason) {
if (!shouldHandleUrl()) return;
if (!manualLock) log('manualLock ON:', reason);
manualLock = true;
}
function clearManualLock(reason) {
if (manualLock) log('manualLock OFF:', reason);
manualLock = false;
}
function inBoostWindow() {
return (Date.now() - t0) < CFG.initialBoostMs;
}
function minIntervalMs() {
return inBoostWindow() ? CFG.minIntervalBoostMs : CFG.minIntervalNormalMs;
}
function mutationDebounceMs() {
return inBoostWindow() ? CFG.mutationDebounceMsBoost : CFG.mutationDebounceMsNormal;
}
// --- “human-like click” (works in your environment) ---
function humanClick(el) {
if (!el) return false;
try {
el.scrollIntoView?.({ block: 'center', inline: 'center' });
el.focus?.();
const opts = { bubbles: true, cancelable: true, composed: true, view: window };
const p = (type) => el.dispatchEvent(new PointerEvent(type, { ...opts, pointerType: 'mouse', isPrimary: true, buttons: 1 }));
const m = (type) => el.dispatchEvent(new MouseEvent(type, { ...opts, buttons: 1 }));
p('pointerover'); p('pointerenter'); m('mouseover'); m('mouseenter');
p('pointerdown'); m('mousedown');
p('pointerup'); m('mouseup');
m('click');
return true;
} catch {
try { el.click(); return true; } catch { return false; }
}
}
async function waitFor(fn, timeoutMs, pollMs = 120) {
const start = performance.now();
while (performance.now() - start < timeoutMs) {
const v = fn();
if (v) return v;
await sleep(pollMs);
}
return null;
}
function isDisabled(el) {
if (!el) return true;
if (el.getAttribute('aria-disabled') === 'true') return true;
if (el.hasAttribute('disabled')) return true;
return /disabled|is-disabled/i.test(String(el.className || ''));
}
function closeMenu() {
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
}
// --- Detect USER interactions with mode UI ---
function isModeUiTarget(target) {
if (!(target instanceof Element)) return false;
// Direct hit
if (target.matches(CFG.menuButtonSel) || target.matches(CFG.anyOptionSel)) return true;
// Inside mode button / option
if (target.closest(CFG.menuButtonSel) || target.closest(CFG.anyOptionSel)) return true;
return false;
}
// Capture phase: only trusted events => user actions
document.addEventListener('pointerdown', (e) => {
if (!e.isTrusted) return;
if (!shouldHandleUrl()) return;
if (isModeUiTarget(e.target)) setManualLock('user pointerdown on mode UI');
}, true);
document.addEventListener('click', (e) => {
if (!e.isTrusted) return;
if (!shouldHandleUrl()) return;
if (isModeUiTarget(e.target)) setManualLock('user click on mode UI');
}, true);
document.addEventListener('keydown', (e) => {
if (!e.isTrusted) return;
if (!shouldHandleUrl()) return;
// Mode UI is often operated by Enter/Space
if (e.key !== 'Enter' && e.key !== ' ') return;
const active = document.activeElement;
if (isModeUiTarget(active)) setManualLock(`user keydown(${e.key}) on mode UI`);
}, true);
// --- Core action: ensure Pro ---
async function ensureProOnce(reason) {
if (!shouldHandleUrl()) return { ok: false, stop: true, why: 'not /app root' };
if (manualLock) return { ok: false, stop: true, why: 'manualLock active' };
const btn = firstVisible(Array.from(document.querySelectorAll(CFG.menuButtonSel)));
if (!btn) return { ok: false, stop: false, why: 'menu button not found yet' };
// Already Pro? (cheap shortcut)
const label = (btn.textContent || '').toLowerCase();
if (label.includes('pro')) return { ok: true, stop: true, why: 'already pro (label)' };
// Open menu (2 tries)
humanClick(btn);
let items = await waitFor(() => {
const list = Array.from(document.querySelectorAll(CFG.anyItemSel));
return list.length ? list : null;
}, CFG.openTimeoutMs);
if (!items) {
humanClick(btn);
items = await waitFor(() => {
const list = Array.from(document.querySelectorAll(CFG.anyItemSel));
return list.length ? list : null;
}, CFG.openTimeoutMs);
}
if (!items) {
closeMenu();
return { ok: false, stop: false, why: 'menu did not open' };
}
// Find Pro
let pro = document.querySelector(CFG.proOptionSel);
if (!pro) {
pro = items.find(el => CFG.proTextRe.test((el.querySelector('.mode-title')?.textContent || el.textContent || '').trim()));
}
// No-op if unavailable
if (!pro) {
closeMenu();
return { ok: false, stop: true, why: 'pro not found (no-op)' };
}
if (isDisabled(pro)) {
closeMenu();
return { ok: false, stop: true, why: 'pro disabled (no-op)' };
}
// Already checked?
if (pro.getAttribute('aria-checked') === 'true' || pro.classList.contains('is-selected')) {
closeMenu();
return { ok: true, stop: true, why: 'already pro (checked)' };
}
// Select Pro
humanClick(pro);
// Verify (prefer aria-checked)
const ok = await waitFor(() => {
const p = document.querySelector(CFG.proOptionSel);
if (p && p.getAttribute('aria-checked') === 'true') return true;
const b = firstVisible(Array.from(document.querySelectorAll(CFG.menuButtonSel)));
if (b && (b.textContent || '').toLowerCase().includes('pro')) return true;
return null;
}, CFG.verifyTimeoutMs, 150);
closeMenu();
return { ok: !!ok, stop: !!ok, why: ok ? 'selected' : 'verify failed' };
}
// --- Scheduler (boost + throttle) ---
let lastRunAt = 0;
let token = 0;
let mutationTimer = null;
function schedule(reason) {
if (!shouldHandleUrl()) return;
if (manualLock) {
log('schedule skipped (manualLock)', reason);
return;
}
const now = Date.now();
const minInt = minIntervalMs();
if (now - lastRunAt < minInt) {
log('throttled', reason, { minInt });
return;
}
lastRunAt = now;
const myToken = ++token;
log('scheduled', reason, 'href=', location.href, { boost: inBoostWindow() });
(async () => {
const base = inBoostWindow() ? 120 : 250;
for (let i = 0; i < 6; i++) {
if (myToken !== token) return;
if (manualLock) return; // if user clicked during retries, stop immediately
const r = await ensureProOnce(reason);
log('attempt', i, r);
if (r.stop) return;
await sleep(base + i * (inBoostWindow() ? 120 : 300));
}
})();
}
// --- URL change detection => clear manualLock ---
function onMaybeUrlChange(reason) {
const href = location.href;
if (href !== lastHref) {
lastHref = href;
clearManualLock(`url changed (${reason})`);
}
// If we arrived at /app root, try
if (shouldHandleUrl()) schedule(`url-check:${reason}`);
}
function hookHistoryMethod(name) {
const orig = history[name];
history[name] = function (...args) {
const ret = orig.apply(this, args);
onMaybeUrlChange(`history.${name}`);
return ret;
};
}
hookHistoryMethod('pushState');
hookHistoryMethod('replaceState');
window.addEventListener('popstate', () => onMaybeUrlChange('popstate'));
// Fallback: some navigations may not call history hooks (rare) => periodic href check (cheap)
setInterval(() => onMaybeUrlChange('interval'), 500);
// DOM rebuild watchdog (debounced)
const mo = new MutationObserver(() => {
if (!shouldHandleUrl()) return;
if (manualLock) return;
if (mutationTimer) return;
mutationTimer = setTimeout(() => {
mutationTimer = null;
if (manualLock) return;
if (document.querySelector(CFG.menuButtonSel)) schedule('mutation(button present)');
}, mutationDebounceMs());
});
mo.observe(document.documentElement, { childList: true, subtree: true });
// Initial run (no delay; boost window handles “early” timing)
schedule('initial');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment