Adds a quick toggle button for Extended Thinking in ChatGPT
// ==UserScript==
// @name ChatGPT Extended Thinking Toggle
// @namespace http://tampermonkey.net/
// @version 5.0
// @description Adds a quick toggle button for Extended Thinking in ChatGPT
// @author You
// @match https://chatgpt.com/*
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
let thinkingEnabled = false;
let observerTimeout = null;
function createToggleButton() {
if (document.getElementById('ext-thinking-toggle')) return;
const btn = document.createElement('button');
btn.id = 'ext-thinking-toggle';
btn.textContent = '🧠 OFF';
btn.title = 'Toggle Extended Thinking';
Object.assign(btn.style, {
position: 'fixed',
bottom: '140px',
right: '20px',
zIndex: '9999',
padding: '8px 14px',
borderRadius: '20px',
border: '1px solid #555',
backgroundColor: '#1a1a2e',
color: '#ccc',
fontSize: '13px',
cursor: 'pointer',
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
transition: 'all 0.2s ease',
fontFamily: 'sans-serif',
});
btn.addEventListener('click', toggleExtendedThinking);
document.body.appendChild(btn);
}
function realClick(el) {
el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true, isPrimary: true }));
el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
el.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, cancelable: true, isPrimary: true }));
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
}
function waitForElement(selector, textMatch, timeout = 3000) {
return new Promise((resolve, reject) => {
const deadline = Date.now() + timeout;
// Use a MutationObserver scoped only to document.body direct children
// instead of polling with setInterval
const obs = new MutationObserver(() => {
const els = [...document.querySelectorAll(selector)];
const match = textMatch
? els.find(el => el.textContent.trim().toLowerCase().includes(textMatch.toLowerCase()))
: els[0];
if (match) {
obs.disconnect();
resolve(match);
} else if (Date.now() > deadline) {
obs.disconnect();
reject(new Error(`Timed out finding: "${textMatch}"`));
}
});
obs.observe(document.body, { childList: true, subtree: true });
// Also check immediately in case it's already there
const els = [...document.querySelectorAll(selector)];
const match = textMatch
? els.find(el => el.textContent.trim().toLowerCase().includes(textMatch.toLowerCase()))
: els[0];
if (match) {
obs.disconnect();
resolve(match);
}
});
}
async function toggleExtendedThinking() {
const btn = document.getElementById('ext-thinking-toggle');
btn.textContent = '🧠 ...';
btn.style.opacity = '0.6';
try {
const plusBtn = document.querySelector('[data-testid="composer-plus-btn"]');
if (!plusBtn) throw new Error('+ button not found');
realClick(plusBtn);
const thinkingItem = await waitForElement('[role="menuitemradio"]', 'thinking', 3000);
const wasChecked = thinkingItem.getAttribute('aria-checked') === 'true' ||
thinkingItem.getAttribute('data-state') === 'checked';
realClick(thinkingItem);
await sleep(150);
thinkingEnabled = !wasChecked;
updateButtonState();
showToast(`🧠 Thinking ${thinkingEnabled ? 'ON ✅' : 'OFF ❌'}`);
} catch (err) {
console.error('[Thinking Toggle]', err);
showToast(`❌ ${err.message}`);
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
updateButtonState();
}
}
function updateButtonState() {
const btn = document.getElementById('ext-thinking-toggle');
if (!btn) return;
btn.style.opacity = '1';
if (thinkingEnabled) {
btn.textContent = '🧠 ON';
btn.style.backgroundColor = '#0f3460';
btn.style.color = '#00d4ff';
btn.style.borderColor = '#00d4ff';
} else {
btn.textContent = '🧠 OFF';
btn.style.backgroundColor = '#1a1a2e';
btn.style.color = '#ccc';
btn.style.borderColor = '#555';
}
}
function showToast(message) {
const existing = document.getElementById('ext-thinking-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.id = 'ext-thinking-toast';
toast.textContent = message;
Object.assign(toast.style, {
position: 'fixed',
bottom: '195px',
right: '20px',
zIndex: '99999',
padding: '8px 14px',
borderRadius: '10px',
backgroundColor: '#222',
color: '#fff',
fontSize: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.5)',
fontFamily: 'sans-serif',
transition: 'opacity 0.5s',
});
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 500);
}, 2500);
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
// Lightweight SPA navigation detection using URL polling
// instead of a permanent MutationObserver on the whole DOM
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(createToggleButton, 500);
}
}, 1000);
if (document.body) {
createToggleButton();
} else {
document.addEventListener('DOMContentLoaded', createToggleButton);
}
})();