Replaces Gemini model selection dropdown with easily accessible buttons, and reapplies your selection on conversation reload
// ==UserScript==
// @name Gemini Model Switcher Buttons
// @namespace http://tampermonkey.net/
// @version 2.4
// @description Replaces Gemini model selection dropdown with easily accessible buttons, and reapplies your selection on conversation reload
// @author treescandal & Gemini 3.0 Pro
// @match https://gemini.google.com/*
// @license MIT
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// ============ CONFIGURATION ============
const MODE_CONFIG = {
modes: [
{ icon: '⚡', label: 'Flash', index: 0 },
{ icon: '🧠', label: 'Think', index: 1 },
{ icon: '✨', label: 'Pro', index: 2 }
],
containerSelector: '.leading-actions-wrapper',
checkInterval: 300,
compactBreakpoint: 620,
reapplyDelay: 1500,
minReapplyInterval: 4000,
maxReapplyAttempts: 3
};
// ============ STATE MANAGEMENT ============
let savedModeIndex = -1;
let languageMap = null;
let isSwitching = false;
let switchTimeout = null;
let reapplyAttempts = 0;
let lastReapplyTime = 0;
let conversationId = null;
let lastContainerCheck = null;
let resizeObserver = null;
try {
const savedMap = localStorage.getItem('gqs_language_map');
if (savedMap) {
languageMap = JSON.parse(savedMap);
}
const savedIndex = localStorage.getItem('gqs_last_index');
if (savedIndex !== null) {
savedModeIndex = parseInt(savedIndex, 10);
console.log('Gemini Switcher: Loaded saved preference:', savedModeIndex);
}
} catch (e) {
console.warn('Gemini Switcher: Could not load saved state', e);
}
// ============ STYLES ============
const styles = `
#gemini-quick-switch-bar {
display: flex;
gap: 6px;
align-items: center;
margin-left: auto;
margin-right: 8px;
height: 40px;
z-index: 999;
}
.gqs-btn {
background: rgba(0,0,0,0.05);
border: 1px solid transparent;
border-radius: 100px;
padding: 0 14px 0 10px;
height: 32px;
font-size: 13px;
font-weight: 500;
color: #444746;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.gqs-btn:hover {
background: rgba(0,0,0,0.1);
}
.gqs-btn.active {
background: #c2e7ff;
color: #001d35;
}
.gqs-btn.compact {
padding: 0 8px;
min-width: 32px;
justify-content: center;
}
.gqs-btn.compact .gqs-label {
display: none;
}
.gqs-btn .gqs-icon {
font-size: 16px;
line-height: 1;
}
.gqs-btn .gqs-label {
font-size: 13px;
}
@media (prefers-color-scheme: dark) {
.gqs-btn {
background: rgba(255,255,255,0.1);
color: #e3e3e3;
}
.gqs-btn:hover {
background: rgba(255,255,255,0.2);
}
.gqs-btn.active {
background: #004a77;
color: #c2e7ff;
}
}
.model-picker-container {
width: 0 !important;
height: 0 !important;
opacity: 0 !important;
overflow: hidden !important;
pointer-events: none !important;
position: absolute !important;
}
.gds-mode-switch-menu {
opacity: 0 !important;
pointer-events: none !important;
visibility: hidden !important;
}
.cdk-overlay-pane:has(> .gds-mode-switch-menu),
.cdk-overlay-pane:has(> * > .gds-mode-switch-menu) {
opacity: 0 !important;
pointer-events: none !important;
visibility: hidden !important;
}
.mat-bottom-sheet-container.gds-mode-switch-menu,
.mat-bottom-sheet-container:has(.gds-mode-switch-menu) {
opacity: 0 !important;
pointer-events: none !important;
visibility: hidden !important;
}
.mat-bottom-sheet-container:has([data-test-id^="bard-mode-option"]) {
opacity: 0 !important;
pointer-events: none !important;
visibility: hidden !important;
}
.cdk-overlay-backdrop {
transition: none !important;
}
.cdk-overlay-container:has(.gds-mode-switch-menu) .cdk-overlay-backdrop,
.cdk-overlay-container:has([data-test-id^="bard-mode-option"]) .cdk-overlay-backdrop {
opacity: 0 !important;
pointer-events: none !important;
visibility: hidden !important;
}
@supports not (selector(:has(*))) {
.gds-mode-switch-menu * {
opacity: 0 !important;
pointer-events: none !important;
}
}
@keyframes gqs-pulse {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(0.98); }
100% { opacity: 1; transform: scale(1); }
}
@keyframes gqs-success-flash {
0% { background-color: #c2e7ff; }
50% { background-color: #b7f7d6; border-color: #26a668; }
100% { background-color: #c2e7ff; }
}
@media (prefers-color-scheme: dark) {
@keyframes gqs-success-flash {
0% { background-color: transparent; }
50% { background-color: #22c55e; color: #ffffff; }
100% { background-color: transparent; }
}
}
.gqs-btn.working {
animation: gqs-pulse 3s infinite ease-in-out !important;
pointer-events: none;
cursor: wait;
}
.gqs-btn.success-flash {
animation: gqs-success-flash 0.6s ease-out;
}
`;
const styleSheet = document.createElement('style');
styleSheet.innerText = styles;
if (document.head.firstChild) {
document.head.insertBefore(styleSheet, document.head.firstChild);
} else {
document.head.appendChild(styleSheet);
}
// ============ URL DETECTION ============
function getConversationId() {
const pathParts = location.pathname.split('/').filter(p => p);
let startIndex = 0;
if (pathParts[0] === 'u' && !isNaN(pathParts[1])) {
startIndex = 2;
}
const relevantParts = pathParts.slice(startIndex);
if (relevantParts.length === 1) {
return null;
}
if (relevantParts.length >= 2) {
return relevantParts.slice(1).join('/');
}
return null;
}
function isOnConversationPage() {
const path = location.pathname;
return path.includes('/app/') || path.includes('/gem/') ||
path.endsWith('/app') || path.endsWith('/gem');
}
// ============ MENU TRIGGER DETECTION ============
function findMenuTrigger() {
let trigger = document.querySelector('[data-test-id*="mode-menu"]');
if (trigger) return { element: trigger, strategy: 'test-id' };
const candidates = Array.from(document.querySelectorAll('button[aria-haspopup="true"]'));
trigger = candidates.find(btn => {
const hasRelevantContent = btn.querySelector('svg') ||
btn.className.includes('model') ||
btn.className.includes('mode');
return hasRelevantContent;
});
if (trigger) return { element: trigger, strategy: 'aria-haspopup' };
const structureMatch = Array.from(document.querySelectorAll('button')).find(btn => {
const hasChevron = btn.querySelector('svg[viewBox*="24"]');
return hasChevron;
});
if (structureMatch) return { element: structureMatch, strategy: 'structure' };
return null;
}
// ============ MENU ITEMS DETECTION ============
function findMenuItems() {
const menuSelectors = [
'[data-test-id^="bard-mode-option"]',
'.gds-mode-switch-menu [role="menuitem"]',
'.mat-bottom-sheet-container [role="menuitem"]',
'[role="menu"] [role="menuitem"]',
'[role="listbox"] [role="option"]',
];
for (const selector of menuSelectors) {
const items = Array.from(document.querySelectorAll(selector));
if (items.length >= 2) {
return items;
}
}
const overlays = Array.from(document.querySelectorAll('.cdk-overlay-pane'));
for (const overlay of overlays) {
const rect = overlay.getBoundingClientRect();
if (rect.height > 0) {
const buttons = Array.from(overlay.querySelectorAll('button, [role="menuitem"], [role="option"]'));
const validItems = buttons.filter(btn => {
const text = btn.innerText?.trim();
const btnRect = btn.getBoundingClientRect();
return text && text.length > 0 && btnRect.height > 20;
});
if (validItems.length >= 2) {
return validItems;
}
}
}
return [];
}
// ============ POLLING HELPER ============
function waitForMenu(callback, maxWait = 500) {
const startTime = Date.now();
const check = () => {
const items = findMenuItems();
if (items.length >= 3) {
callback(items);
} else if (Date.now() - startTime < maxWait) {
requestAnimationFrame(check);
} else {
console.warn('Gemini Switcher: Menu timeout');
callback([]);
}
};
requestAnimationFrame(check);
}
// ============ LANGUAGE MAP ============
function buildLanguageMapFromMenu(menuItems) {
const tempMap = {};
menuItems.forEach((item, index) => {
const titleElement = item.querySelector('.gds-title-m, [class*="title"]');
if (titleElement) {
const text = titleElement.innerText.trim().toLowerCase();
if (text) {
tempMap[text] = index;
}
}
});
if (Object.keys(tempMap).length > 0) {
languageMap = tempMap;
try {
localStorage.setItem('gqs_language_map', JSON.stringify(languageMap));
console.log('Gemini Switcher: Language map built:', languageMap);
} catch (e) {
console.warn('Gemini Switcher: Could not save language map', e);
}
}
}
// ============ ACTIVE MODE DETECTION ============
function detectActiveModeFromUI() {
const triggerResult = findMenuTrigger();
if (!triggerResult) return -1;
const trigger = triggerResult.element;
const text = trigger.innerText.toLowerCase().replace(/\s+/g, ' ').trim();
if (!text || text.length < 2) return -1;
if (languageMap) {
for (const [keyword, index] of Object.entries(languageMap)) {
if (text.includes(keyword)) {
return index;
}
}
}
return -1;
}
// ============ MODE SWITCHING ============
function performModeSwitch(modeIndex, isAutoReapply = false) {
if (isSwitching) {
console.log('Gemini Switcher: Already switching, skipping');
return;
}
const triggerExists = !!findMenuTrigger();
console.log(`Gemini Switcher: performModeSwitch - mode: ${modeIndex}, isAutoReapply: ${isAutoReapply}, trigger exists: ${triggerExists}`);
const targetBtn = document.querySelector(`.gqs-btn[data-mode-index="${modeIndex}"]`);
if (targetBtn) targetBtn.classList.add('working');
if (!isAutoReapply) {
savedModeIndex = modeIndex;
reapplyAttempts = 0;
try {
localStorage.setItem('gqs_last_index', modeIndex.toString());
} catch (e) {
console.warn('Gemini Switcher: Could not save preference', e);
}
} else {
reapplyAttempts++;
lastReapplyTime = Date.now();
}
isSwitching = true;
clearTimeout(switchTimeout);
// Update UI optimistically
document.querySelectorAll('.gqs-btn').forEach((btn, idx) => {
btn.classList.toggle('active', idx === modeIndex);
});
const triggerResult = findMenuTrigger();
if (!triggerResult) {
console.error('Gemini Switcher: Trigger not found');
if (targetBtn) targetBtn.classList.remove('working');
isSwitching = false;
return;
}
console.log('Gemini Switcher: Clicking trigger with strategy:', triggerResult.strategy);
triggerResult.element.click();
waitForMenu((menuItems) => {
if (menuItems.length === 0) {
console.error('Gemini Switcher: Menu not found');
if (targetBtn) targetBtn.classList.remove('working');
isSwitching = false;
return;
}
console.log('Gemini Switcher: Menu found with', menuItems.length, 'items');
if (menuItems.length >= 3 && !languageMap) {
buildLanguageMapFromMenu(menuItems);
}
if (menuItems.length > modeIndex) {
console.log('Gemini Switcher: Clicking menu item', modeIndex);
menuItems[modeIndex].click();
if (targetBtn) {
targetBtn.classList.remove('working');
targetBtn.classList.add('success-flash');
setTimeout(() => targetBtn.classList.remove('success-flash'), 800);
}
switchTimeout = setTimeout(() => {
isSwitching = false;
console.log('Gemini Switcher: Switch completed');
}, 800);
} else {
console.error(`Gemini Switcher: Only found ${menuItems.length} menu items, cannot switch to index ${modeIndex}`);
if (targetBtn) targetBtn.classList.remove('working');
isSwitching = false;
}
});
}
function checkAndReapply() {
if (savedModeIndex === -1) {
console.log('Gemini Switcher: No saved preference, skipping reapply');
return;
}
if (isSwitching) {
console.log('Gemini Switcher: Currently switching, skipping reapply');
return;
}
if (reapplyAttempts >= MODE_CONFIG.maxReapplyAttempts) {
console.log('Gemini Switcher: Max reapply attempts reached');
return;
}
const now = Date.now();
if (now - lastReapplyTime < MODE_CONFIG.minReapplyInterval) {
console.log('Gemini Switcher: Rate limit - skipping reapply (too soon)');
return;
}
const trigger = findMenuTrigger();
if (!trigger) {
console.log('Gemini Switcher: UI not ready for reapply (no trigger), skipping');
return;
}
const currentMode = detectActiveModeFromUI();
if (currentMode === -1) {
console.log('Gemini Switcher: Cannot detect current mode, will build language map');
performModeSwitch(savedModeIndex, true);
return;
}
if (currentMode !== savedModeIndex) {
console.log(`Gemini Switcher: Drift detected (current: ${currentMode}, saved: ${savedModeIndex})`);
performModeSwitch(savedModeIndex, true);
} else {
console.log(`Gemini Switcher: Mode already correct (${currentMode}), no reapply needed`);
}
}
// ============ UI STATE UPDATE ============
function updateActiveState() {
if (isSwitching) return;
let activeIndex = savedModeIndex;
if (activeIndex === -1) {
activeIndex = detectActiveModeFromUI();
}
document.querySelectorAll('.gqs-btn').forEach((btn, idx) => {
btn.classList.toggle('active', idx === activeIndex);
});
}
// ============ RESPONSIVE LAYOUT ============
function updateButtonLayout() {
const container = document.querySelector(MODE_CONFIG.containerSelector);
const buttons = document.querySelectorAll('.gqs-btn');
if (!container || buttons.length === 0) return;
const containerWidth = container.offsetWidth;
const isCompact = containerWidth < MODE_CONFIG.compactBreakpoint;
buttons.forEach(btn => {
if (isCompact) {
btn.classList.add('compact');
btn.title = btn.querySelector('.gqs-label')?.textContent || '';
} else {
btn.classList.remove('compact');
btn.removeAttribute('title');
}
});
}
// ============ UI INJECTION ============
function injectButtons(container) {
const bar = document.createElement('div');
bar.id = 'gemini-quick-switch-bar';
MODE_CONFIG.modes.forEach((mode, idx) => {
const btn = document.createElement('button');
btn.className = 'gqs-btn';
const icon = document.createElement('span');
icon.className = 'gqs-icon';
icon.textContent = mode.icon;
const label = document.createElement('span');
label.className = 'gqs-label';
label.textContent = mode.label;
btn.appendChild(icon);
btn.appendChild(label);
btn.dataset.modeIndex = idx;
btn.onclick = () => performModeSwitch(mode.index, false);
bar.appendChild(btn);
});
container.appendChild(bar);
console.log('Gemini Switcher: Buttons injected');
setTimeout(() => {
updateButtonLayout();
updateActiveState();
}, 100);
}
// ============ INITIALIZATION & MONITORING ============
setInterval(() => {
const container = document.querySelector(MODE_CONFIG.containerSelector);
const existingBar = document.getElementById('gemini-quick-switch-bar');
if (container && !existingBar) {
injectButtons(container);
lastContainerCheck = container;
if (resizeObserver) {
resizeObserver.disconnect();
}
resizeObserver = new ResizeObserver(updateButtonLayout);
resizeObserver.observe(container);
}
if (existingBar && !document.body.contains(existingBar)) {
lastContainerCheck = null;
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
}
if (existingBar) {
updateActiveState();
}
}, MODE_CONFIG.checkInterval);
window.addEventListener('resize', updateButtonLayout);
// ============ NAVIGATION HANDLING ============
let lastUrl = location.href;
let navigationDebounceTimeout = null;
conversationId = getConversationId();
function handleNavigationChange() {
const currentConversationId = getConversationId();
if (currentConversationId !== conversationId) {
conversationId = currentConversationId;
console.log('Gemini Switcher: Conversation changed:', {
id: conversationId,
isOnConversationPage: isOnConversationPage()
});
if (isOnConversationPage() && savedModeIndex !== -1) {
console.log('Gemini Switcher: On conversation page, will reapply preference');
reapplyAttempts = 0;
lastReapplyTime = 0;
setTimeout(() => {
checkAndReapply();
}, MODE_CONFIG.reapplyDelay);
}
}
}
new MutationObserver(() => {
const currentUrl = location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
lastContainerCheck = null;
isSwitching = false;
clearTimeout(switchTimeout);
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
console.log('Gemini Switcher: Navigation detected');
clearTimeout(navigationDebounceTimeout);
navigationDebounceTimeout = setTimeout(() => {
handleNavigationChange();
}, 500);
}
}).observe(document.body, { childList: true, subtree: true });
// ============ INITIAL LOAD HANDLING ============
if (isOnConversationPage() && savedModeIndex !== -1) {
console.log('Gemini Switcher: Initial load on conversation page, checking state...');
const retryDelays = [1500, 2500, 4000];
retryDelays.forEach(delay => {
setTimeout(() => {
if (reapplyAttempts < MODE_CONFIG.maxReapplyAttempts) {
const trigger = findMenuTrigger();
if (trigger) {
console.log(`Gemini Switcher: Trigger found at ${delay}ms, attempting reapply`);
checkAndReapply();
} else {
console.log(`Gemini Switcher: Trigger not found at ${delay}ms, will retry`);
}
}
}, delay);
});
}
})();