ChatGPT Modelswitcher
Adds hotkeys to switch between ChatGPT models (macos only, alt+t, alt+a).
Show source
// ==UserScript==
// @name ChatGPT Reasoning Mode Hotkeys (Auto/Think) — CSP-safe + Focus Restore
// @description ⌥A=Auto, ⌥T=Think, ⌥`=Toggle. ⌥⇧D dump; ⌥⇧H highlight; ⌥⇧O open menu. Restores focus to composer after mode change. Works under strict CSP.
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
// @inject-into content
// @run-at document-start
// @noframes
// @version 1.3
// ==/UserScript==
(() => {
const DEBUG = false;
// Use e.code so macOS Option combos don't turn into å/† and miss-match.
const KEY_AUTO = { alt:true, code:'KeyA' };
const KEY_THINK = { alt:true, code:'KeyT' };
const KEY_TOGGLE = { alt:true, code:'Backquote' };
const KEY_DUMP = { alt:true, shift:true, code:'KeyD' };
const KEY_HILITE = { alt:true, shift:true, code:'KeyH' };
const KEY_OPEN = { alt:true, shift:true, code:'KeyO' };
const labels = {
auto: ['auto','automático','automatic'],
think: ['think','thinking','reasoning','razonamiento','pensar','pensamiento']
};
const dbg = (...a) => { if (DEBUG) console.log('[ModeDbg]', ...a); };
const toastBoxId = '__cgpt_mode_toast__';
function toast(msg) {
let box = document.getElementById(toastBoxId);
if (!box) {
box = document.createElement('div');
box.id = toastBoxId;
Object.assign(box.style, {
position:'fixed', top:'10px', right:'12px', zIndex: 2147483647,
font:'12px system-ui,-apple-system,Segoe UI,Roboto,sans-serif'
});
document.body.appendChild(box);
}
const el = document.createElement('div');
Object.assign(el.style, {
background:'#111', color:'#fff', padding:'6px 8px', marginTop:'8px',
borderRadius:'6px', boxShadow:'0 2px 8px rgba(0,0,0,.35)', opacity:'0.95'
});
el.textContent = '[Mode] ' + msg;
box.appendChild(el);
setTimeout(() => el.remove(), 2200);
}
const norm = s => (s||'').replace(/\s+/g,' ').trim().toLowerCase();
const vis = el => el && el.isConnected && (()=>{const r=el.getBoundingClientRect(),cs=getComputedStyle(el);return r.width>0&&r.height>0&&cs.visibility!=='hidden'&&cs.display!=='none'})();
// Boundary-insensitive "startsWith": matches "autoDecides..." too
const startsWith = (text, lbl) => norm(text).startsWith(lbl);
const matchAnyStart = (text, arr) => arr.some(lbl => startsWith(text, lbl));
function pointerClick(el){
try{
const o={bubbles:true,cancelable:true,composed:true};
el.dispatchEvent(new PointerEvent('pointerdown',o));
el.dispatchEvent(new MouseEvent('mousedown',o));
el.dispatchEvent(new PointerEvent('pointerup',o));
el.dispatchEvent(new MouseEvent('mouseup',o));
el.dispatchEvent(new MouseEvent('click',o));
return true;
}catch(e){ try{ el.click(); return true; }catch{} }
return false;
}
function topFixedOrZ(){
return Array.from(document.querySelectorAll('body *')).filter(n=>{
const cs=getComputedStyle(n);
return vis(n) && (cs.position==='fixed' || parseInt(cs.zIndex||'0',10)>1000);
});
}
function allClickable(scope=document){
const sel = [
'button',
'[role="button"]',
'[role^="menuitem"]',
'[role="option"]',
'[role="tab"]',
'[role="radio"]',
// Radix-like menu items:
'div.__menu-item','div.group.__menu-item','[class*="__menu-item"]','[class*="menu-item"]'
].join(',');
return Array.from(scope.querySelectorAll(sel)).filter(vis);
}
function findByLabels(arr){
// Try high z-index popovers first
const pops = topFixedOrZ();
for(const p of pops){
const hit = allClickable(p).find(el => matchAnyStart(el.textContent||'', arr));
if (hit) return hit;
}
return allClickable(document).find(el => matchAnyStart(el.textContent||'', arr)) || null;
}
// === Focus restore helpers ===
function focusComposer(retries=10){
const tryFocus = () => {
const selectors = [
'#prompt-textarea',
'textarea[placeholder*="Message"]',
'form textarea',
'[data-testid*="composer"] [contenteditable="true"]',
'[contenteditable="true"][role="textbox"]',
'#composer [contenteditable="true"]',
'textarea'
];
let candidates=[];
selectors.forEach(q => candidates.push(...document.querySelectorAll(q)));
const el = candidates.find(vis);
if (!el) return false;
// Focus + move caret to end
try {
if (el.matches('textarea,input')) {
el.focus({ preventScroll:true });
const n = el.value?.length ?? 0;
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(n, n);
} else {
el.focus({ preventScroll:true });
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
return true;
} catch { return false; }
};
if (tryFocus()) return;
const id = setInterval(() => {
if (tryFocus() || --retries <= 0) clearInterval(id);
}, 120);
}
function queueFocus(){
// Focus after UI settles; do a couple of attempts.
setTimeout(focusComposer, 100);
setTimeout(focusComposer, 400);
}
// Visual highlight
const HL_CLS='__cgpt_mode_hl__', HL_STYLE_ID='__cgpt_mode_hl_style__';
function ensureHLStyle(){
if(document.getElementById(HL_STYLE_ID)) return;
const st = document.createElement('style');
st.id = HL_STYLE_ID;
st.textContent = `
.${HL_CLS}{outline:3px solid rgba(0,160,255,.95)!important;border-radius:8px!important;animation:cgptPulse 1s ease-in-out infinite}
@keyframes cgptPulse{0%{outline-offset:0}50%{outline-offset:3px}100%{outline-offset:0}}
`;
document.documentElement.appendChild(st);
}
function mark(el,label=''){ ensureHLStyle(); try{ el.classList.add(HL_CLS); setTimeout(()=>el.classList.remove(HL_CLS),1500);}catch{} if(label)dbg('HL:',label,el); }
function openModeMenu(){
// Try icon/aria buttons near composer first
const cands = Array.from(document.querySelectorAll('button,[role="button"],[aria-haspopup="menu"],[aria-controls]'))
.filter(vis)
.filter(el=>{
const t = `${el.textContent||''} ${el.getAttribute('aria-label')||''}`.toLowerCase();
return /auto|think|reason|mode|instant|thinking|reasoning/.test(t);
});
// Prefer the one closest to the composer
const composer = document.querySelector('#composer,[data-testid*="composer"],form textarea,[contenteditable="true"]');
if (composer) {
cands.sort((a,b)=>distance(a,composer)-distance(b,composer));
}
const btn = cands[0];
if(btn){ mark(btn,'open-menu'); pointerClick(btn); dbg('OpenMenu via',btn); return true; }
dbg('OpenMenu: no trigger found');
return false;
}
function distance(a,b){ try{const ra=a.getBoundingClientRect(),rb=b.getBoundingClientRect();return Math.hypot((ra.left+ra.right)/2-(rb.left+rb.right)/2,(ra.top+ra.bottom)/2-(rb.top+rb.bottom)/2);}catch{return 1e9;} }
async function selectMode(which){
const want = which==='auto'?labels.auto:labels.think;
dbg('selectMode', which);
// Segmented toggle case
const nodes = allClickable(document);
const autoBtn = nodes.find(n=>matchAnyStart(n.textContent||'',labels.auto));
const thinkBtn = nodes.find(n=>matchAnyStart(n.textContent||'',labels.think));
if (autoBtn && thinkBtn && autoBtn.parentElement && autoBtn.parentElement===thinkBtn.parentElement){
const target = which==='auto'?autoBtn:thinkBtn;
toast(`Selecting ${which} (segmented)`);
mark(target,'segmented');
pointerClick(target);
queueFocus();
return true;
}
// Dropdown/menu case
const tryMenu = () => {
const item = findByLabels(want);
if (item) {
toast(`Selecting ${which} (menu)`);
mark(item,'menu-item');
pointerClick(item);
queueFocus();
return true;
}
return false;
};
if (tryMenu()) return true;
openModeMenu();
// Poll briefly
return await new Promise(res=>{
let n=0, id=setInterval(()=>{
if (tryMenu()){ clearInterval(id); res(true); }
else if(++n>=15){ clearInterval(id); toast(`Failed to find "${which}"`); res(false); }
}, 90);
});
}
function dumpCandidates(highlight=false){
const scopes = [document, ...topFixedOrZ()];
let total = 0;
scopes.forEach((s,idx)=>{
const nodes = allClickable(s); total += nodes.length;
dbg(`--- DUMP scope[${idx}] nodes=${nodes.length} ---`);
nodes.forEach((n,i)=>{
const text=(n.textContent||'').replace(/\s+/g,' ').trim();
console.log(`[${idx}:${i}]`, { text, node:n });
if (highlight && (matchAnyStart(text, labels.auto) || matchAnyStart(text, labels.think))) mark(n,text);
});
});
toast(`Dumped ${total} nodes`);
}
function matchHotkey(e,s){
return (!!s.alt === !!e.altKey) &&
(!!s.ctrl === !!e.ctrlKey) &&
(!!s.shift === !!e.shiftKey) &&
(!!s.meta === !!e.metaKey) &&
(s.code ? e.code === s.code : (s.key && e.key.toLowerCase()===s.key.toLowerCase()));
}
// Capture even in the composer; stop propagation so characters don't appear.
const handler = async (e) => {
const isCombo =
matchHotkey(e,KEY_AUTO) || matchHotkey(e,KEY_THINK) || matchHotkey(e,KEY_TOGGLE) ||
matchHotkey(e,KEY_DUMP) || matchHotkey(e,KEY_HILITE) || matchHotkey(e,KEY_OPEN);
if (!isCombo) return;
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
if (matchHotkey(e,KEY_AUTO)) { dbg('HK AUTO'); await selectMode('auto'); }
else if (matchHotkey(e,KEY_THINK)){ dbg('HK THINK'); await selectMode('think'); }
else if (matchHotkey(e,KEY_TOGGLE)){
dbg('HK TOGGLE');
// heuristic toggle: open menu, then pick the other item
openModeMenu();
setTimeout(async ()=>{
const thinkItem = findByLabels(labels.think);
if (thinkItem && thinkItem.getAttribute('data-state')!=='on') await selectMode('think');
else await selectMode('auto');
}, 120);
}
else if (matchHotkey(e,KEY_DUMP)) { dbg('HK DUMP'); dumpCandidates(false); }
else if (matchHotkey(e,KEY_HILITE)){ dbg('HK HILITE');dumpCandidates(true); }
else if (matchHotkey(e,KEY_OPEN)) { dbg('HK OPEN'); openModeMenu(); toast('Tried to open menu'); }
};
window.addEventListener('keydown', handler, { capture:true });
document.addEventListener('keydown', handler, { capture:true });
// First probe (after DOM ready enough to have body)
const ready = () => document.body ? dumpCandidates(false) : setTimeout(ready, 300);
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ready, { once:true });
else ready();
// Log when popovers appear
new MutationObserver(() => {
const pops = topFixedOrZ();
if (pops.length) dbg('Mutation: high-z containers present:', pops.length);
}).observe(document.documentElement, { childList:true, subtree:true });
})();