After entering text and pressing Enter, you can select a model before sending.
// ==UserScript==
// @name ChatGPT Model Selector
// @namespace https://openai.com/
// @version 2.1.1
// @description After entering text and pressing Enter, you can select a model before sending.
// @author 81standard
// @match https://chatgpt.com/*
// @match https://chat.openai.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const STATE = {
overlay: null,
panel: null,
selectedIndex: 0,
isOpen: false,
applying: false,
actionTaken: false,
lastComposer: null,
hotkeyCooldownUntil: 0,
toastTimer: null,
dialogThreadId: null,
lastKnownHref: location.href,
threadSyncToken: 0,
pendingThreadModel: null,
};
const I18N = {
thought: ['思考', 'thinking'],
extendedThought: ['じっくり思考', 'extended thinking'],
standard: ['標準', 'standard'],
extended: ['拡張', 'extended'],
};
const OPTIONS = [
{ key: 'instant', label: 'GPT-5', modelDataTestId: 'model-switcher-gpt-5-3' },
{ key: 'thinking-standard', label: 'GPT Thinking 標準', modelDataTestId: 'model-switcher-gpt-5-4-thinking', effortLabels: I18N.standard },
{ key: 'thinking-extended', label: 'GPT Thinking 拡張', modelDataTestId: 'model-switcher-gpt-5-4-thinking', effortLabels: I18N.extended },
];
const STORAGE_KEY = 'tm-chatgpt-model-selector.thread-models.v1';
const DEFAULT_MODEL_KEY = OPTIONS[0].key;
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function visible(el) {
if (!el || !el.isConnected) return false;
const st = getComputedStyle(el);
if (st.display === 'none' || st.visibility === 'hidden') return false;
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}
function norm(s) {
return (s || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function textOf(el) {
return norm((el?.innerText || el?.textContent || '') + ' ' + (el?.getAttribute?.('aria-label') || ''));
}
function includesAny(text, words) {
const t = norm(text);
return (words || []).some(word => t.includes(norm(word)));
}
function thoughtLike(text) {
return includesAny(text, I18N.thought) || includesAny(text, I18N.extendedThought);
}
function extendedLike(text) {
return includesAny(text, I18N.extended) || includesAny(text, I18N.extendedThought);
}
function normalizeModel(model) {
const key = typeof model === 'string' ? model : model?.key;
return OPTIONS.some(option => option.key === key) ? key : null;
}
function getOptionByModel(model) {
const key = normalizeModel(model) || DEFAULT_MODEL_KEY;
return OPTIONS.find(option => option.key === key) || OPTIONS[0];
}
function getOptionIndexByModel(model) {
const key = normalizeModel(model) || DEFAULT_MODEL_KEY;
const index = OPTIONS.findIndex(option => option.key === key);
return index >= 0 ? index : 0;
}
function readThreadModelStore() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return {};
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
function writeThreadModelStore(store) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(store || {}));
} catch {}
}
function getCurrentThreadId() {
try {
const url = new URL(location.href);
const pathMatch = url.pathname.match(/\/c\/([A-Za-z0-9-]+)/);
if (pathMatch?.[1]) return `thread:${pathMatch[1]}`;
for (const key of ['conversationId', 'conversation_id', 'threadId', 'thread_id']) {
const value = url.searchParams.get(key);
if (value) return `thread:${value}`;
}
const normalizedPath = url.pathname.replace(/\/+$/, '') || '/';
return `route:${normalizedPath}${url.search}`;
} catch {
return 'route:/';
}
}
function isEphemeralThreadId(threadId) {
return !String(threadId || '').startsWith('thread:');
}
function loadThreadModel(threadId) {
if (!threadId) return null;
const store = readThreadModelStore();
return normalizeModel(store[threadId]);
}
function saveThreadModel(threadId, model) {
const key = normalizeModel(model);
if (!threadId || !key) return key;
const store = readThreadModelStore();
store[threadId] = key;
writeThreadModelStore(store);
return key;
}
function getComposer() {
const selectors = [
'form textarea',
'textarea[placeholder]',
'textarea',
'[contenteditable="true"][data-lexical-editor="true"]',
'[contenteditable="true"][role="textbox"]',
'[contenteditable="true"]',
];
for (const sel of selectors) {
const els = Array.from(document.querySelectorAll(sel)).filter(visible);
if (els.length) return els[els.length - 1];
}
const ae = document.activeElement;
if (ae && ae.matches?.('textarea,[contenteditable="true"],[role="textbox"]')) return ae;
return null;
}
function getComposerForm() {
const composer = getComposer();
return composer ? composer.closest('form') : null;
}
function focusComposer() {
const el = STATE.lastComposer || getComposer();
if (!el) return false;
try {
el.focus({ preventScroll: true });
if (typeof el.value === 'string' && typeof el.setSelectionRange === 'function') {
const end = el.value.length;
el.setSelectionRange(end, end);
} else if (el.isContentEditable) {
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
return true;
} catch {
try { el.focus(); return true; } catch {}
}
return false;
}
function getUiLang() {
const htmlLang = (document.documentElement.getAttribute('lang') || '').toLowerCase();
const navLang = (navigator.language || '').toLowerCase();
const lang = htmlLang || navLang;
return lang.startsWith('ja') ? 'ja' : 'en';
}
const UI_TEXT = {
ja: {
title: '送信前にモデルを選択',
hint: 'Enter: 送信 / Esc: 反映のみ / 外クリック: 反映のみ',
option_gpt5: 'GPT-5',
option_thinking_standard: 'GPT Thinking 標準',
option_thinking_extended: 'GPT Thinking 拡張',
toast_apply: '反映',
toast_send: '送信',
toast_failed: '切り替え失敗',
},
en: {
title: 'Select model before sending',
hint: 'Enter: Send / Esc: Apply only / Click outside: Apply only',
option_gpt5: 'GPT-5',
option_thinking_standard: 'GPT Thinking Standard',
option_thinking_extended: 'GPT Thinking Extended',
toast_apply: 'Applied',
toast_send: 'Sent',
toast_failed: 'Switch failed',
}
};
function ui(key) {
const lang = getUiLang();
return (UI_TEXT[lang] && UI_TEXT[lang][key]) || UI_TEXT.en[key] || key;
}
function getOptionLabel(option) {
if (option.key === 'instant') return ui('option_gpt5');
if (option.key === 'thinking-standard') return ui('option_thinking_standard');
if (option.key === 'thinking-extended') return ui('option_thinking_extended');
return option.label || option.key;
}
function showToast(msg, ok = true) {
clearTimeout(STATE.toastTimer);
let el = document.getElementById('tm-chatgpt-model-toast');
if (!el) {
el = document.createElement('div');
el.id = 'tm-chatgpt-model-toast';
Object.assign(el.style, {
position: 'fixed',
right: '16px',
bottom: '16px',
zIndex: '2147483647',
padding: '10px 14px',
borderRadius: '12px',
color: '#fff',
fontSize: '14px',
lineHeight: '1.3',
boxShadow: '0 8px 24px rgba(0,0,0,.25)',
backdropFilter: 'blur(6px)',
pointerEvents: 'none',
});
document.body.appendChild(el);
}
el.style.background = ok ? 'rgba(24,24,27,.92)' : 'rgba(127,29,29,.96)';
el.textContent = msg;
STATE.toastTimer = setTimeout(() => { try { el.remove(); } catch {} }, 1800);
}
function clickSequence(el) {
if (!el) return false;
const r = el.getBoundingClientRect();
const cx = r.left + Math.max(1, r.width / 2);
const cy = r.top + Math.max(1, r.height / 2);
const dispatch = (target, type, Ctor, extra = {}) => {
const ev = new Ctor(type, {
bubbles: true,
cancelable: true,
composed: true,
view: window,
clientX: cx,
clientY: cy,
button: 0,
buttons: (type === 'mouseup' || type === 'click' || type === 'pointerup') ? 0 : 1,
...extra,
});
return target.dispatchEvent(ev);
};
try { el.focus?.({ preventScroll: true }); } catch {}
try { dispatch(el, 'pointerdown', PointerEvent, { pointerId: 1, pointerType: 'mouse', isPrimary: true }); } catch {}
try { dispatch(el, 'mousedown', MouseEvent); } catch {}
try { dispatch(el, 'pointerup', PointerEvent, { pointerId: 1, pointerType: 'mouse', isPrimary: true }); } catch {}
try { dispatch(el, 'mouseup', MouseEvent); } catch {}
try { dispatch(el, 'click', MouseEvent); } catch {}
return true;
}
function clickSingle(el) {
if (!el) return false;
try { el.focus?.({ preventScroll: true }); } catch {}
try { el.click(); return true; } catch {}
try {
el.dispatchEvent(new MouseEvent('click', {
bubbles: true,
cancelable: true,
composed: true,
view: window,
button: 0,
buttons: 0,
}));
return true;
} catch {}
return false;
}
function clickElementAtCenter(el) {
if (!el) return false;
const r = el.getBoundingClientRect();
const cx = r.left + Math.max(1, r.width / 2);
const cy = r.top + Math.max(1, r.height / 2);
const target = document.elementFromPoint(cx, cy);
if (!target) return false;
return clickSequence(target);
}
function findHeaderModelButton() {
return Array.from(document.querySelectorAll('button[data-testid="model-switcher-dropdown-button"]'))
.find(visible) || null;
}
function getModelMenuItems() {
return Array.from(document.querySelectorAll('[data-testid^="model-switcher-"]'))
.filter(visible)
.filter(el => (el.getAttribute('data-testid') || '') !== 'model-switcher-dropdown-button');
}
function getEffortMenuItems() {
return Array.from(document.querySelectorAll('[role="menuitemradio"]')).filter(visible);
}
async function openModelMenu() {
const btn = findHeaderModelButton();
if (!btn) throw new Error('model button not found');
clickSequence(btn);
for (let i = 0; i < 10; i++) {
if (getModelMenuItems().length) return btn;
await sleep(80);
}
throw new Error('model menu not opened');
}
async function chooseModel(dataTestId) {
for (let i = 0; i < 8; i++) {
const item = getModelMenuItems().find(el => (el.getAttribute('data-testid') || '') === dataTestId);
if (item) {
clickSingle(item);
await sleep(120);
return true;
}
await sleep(60);
}
return false;
}
function getThoughtPillButtons() {
const form = getComposerForm();
const scope = form || document;
return Array.from(scope.querySelectorAll('button'))
.filter(visible)
.filter(el => {
const cls = String(el.className || '');
if (cls.includes('__composer-pill-remove')) return false;
if (!cls.includes('__composer-pill')) return false;
const t = textOf(el);
return thoughtLike(t) || includesAny(t, I18N.standard) || includesAny(t, I18N.extended);
});
}
function findThoughtPillButton() {
const candidates = getThoughtPillButtons();
candidates.sort((a, b) => {
const ar = a.getBoundingClientRect();
const br = b.getBoundingClientRect();
const topDiff = br.top - ar.top;
if (topDiff !== 0) return topDiff;
return br.width * br.height - ar.width * ar.height;
});
return candidates[0] || null;
}
function effortAlreadyApplied(labels) {
const t = norm(getComposerForm()?.innerText || '');
const wantsStandard = includesAny(labels?.join(' '), I18N.standard);
const wantsExtended = includesAny(labels?.join(' '), I18N.extended);
if (wantsStandard) {
return includesAny(t, I18N.standard) || (thoughtLike(t) && !extendedLike(t));
}
if (wantsExtended) {
return extendedLike(t);
}
return false;
}
async function waitForThoughtPill() {
for (let i = 0; i < 18; i++) {
const pill = findThoughtPillButton();
if (pill) return pill;
await sleep(80);
}
return null;
}
async function openThoughtMenu() {
if (getEffortMenuItems().length) return true;
for (let i = 0; i < 10; i++) {
const pill = await waitForThoughtPill();
if (!pill) {
await sleep(80);
continue;
}
clickSingle(pill);
for (let j = 0; j < 8; j++) {
if (getEffortMenuItems().length) return true;
await sleep(60);
}
clickSequence(pill);
for (let j = 0; j < 8; j++) {
if (getEffortMenuItems().length) return true;
await sleep(60);
}
clickElementAtCenter(pill);
for (let j = 0; j < 8; j++) {
if (getEffortMenuItems().length) return true;
await sleep(60);
}
await sleep(80);
}
throw new Error('thought menu not opened');
}
async function chooseEffort(labels) {
for (let i = 0; i < 10; i++) {
const item = getEffortMenuItems().find(el => includesAny(textOf(el), labels));
if (item) {
clickSingle(item);
await sleep(120);
return true;
}
await sleep(60);
}
return false;
}
function detectCurrentModelFromUi() {
const texts = [
textOf(findThoughtPillButton()),
textOf(getComposerForm()),
textOf(findHeaderModelButton()),
].filter(Boolean);
if (!texts.length) return null;
const combined = norm(texts.join(' '));
if (extendedLike(combined)) return 'thinking-extended';
if (includesAny(combined, I18N.standard) || thoughtLike(combined)) return 'thinking-standard';
if (combined.includes('gpt-5')) return 'instant';
return null;
}
function getCurrentModelForThread(threadId) {
return loadThreadModel(threadId) || detectCurrentModelFromUi() || DEFAULT_MODEL_KEY;
}
async function waitForApplied(option) {
for (let i = 0; i < 12; i++) {
const detected = detectCurrentModelFromUi();
if (detected === option.key) return true;
const t = norm(getComposerForm()?.innerText || '');
if (option.key === 'instant') {
if (!thoughtLike(t) && !includesAny(t, I18N.standard) && !includesAny(t, I18N.extended)) return true;
} else if (option.key === 'thinking-standard') {
if (includesAny(t, I18N.standard) || (thoughtLike(t) && !extendedLike(t))) return true;
} else if (option.key === 'thinking-extended') {
if (extendedLike(t)) return true;
}
await sleep(80);
}
return false;
}
async function applyOption(option) {
STATE.applying = true;
try {
await openModelMenu();
const okModel = await chooseModel(option.modelDataTestId);
if (!okModel) throw new Error('モデル切り替え失敗');
if (option.effortLabels) {
await waitForThoughtPill();
if (!effortAlreadyApplied(option.effortLabels)) {
await openThoughtMenu();
const okEffort = await chooseEffort(option.effortLabels);
if (!okEffort) throw new Error('思考モード切り替え失敗');
}
}
await waitForApplied(option);
return true;
} finally {
STATE.applying = false;
}
}
async function applyModelToComposer(threadId, model) {
const option = getOptionByModel(model);
await applyOption(option);
saveThreadModel(threadId, option.key);
STATE.selectedIndex = getOptionIndexByModel(option.key);
return option.key;
}
function findSendButton() {
const form = getComposerForm();
const scope = form || document;
const selectors = [
'button[data-testid="send-button"]',
'button[data-testid="composer-submit-button"]',
'button[aria-label*="プロンプトを送信"]',
'button[aria-label*="送信"]',
'button[aria-label*="Send"]',
'button[aria-label*="send"]',
'button[type="submit"]',
];
for (const sel of selectors) {
const buttons = Array.from(scope.querySelectorAll(sel)).filter(visible);
const enabled = buttons.filter(b => !b.disabled && b.getAttribute('aria-disabled') !== 'true');
if (enabled.length) return enabled[enabled.length - 1];
}
return null;
}
async function sendOnce() {
const btn = findSendButton();
if (btn) {
clickSingle(btn);
return true;
}
const form = getComposerForm();
if (form && typeof form.requestSubmit === 'function') {
form.requestSubmit();
return true;
}
return false;
}
function cleanupDialogListeners() {
if (STATE.overlay && typeof STATE.overlay._cleanup === 'function') {
STATE.overlay._cleanup();
}
}
function closeDialog() {
cleanupDialogListeners();
try { STATE.overlay?.remove(); } catch {}
STATE.overlay = null;
STATE.panel = null;
STATE.isOpen = false;
STATE.actionTaken = false;
STATE.dialogThreadId = null;
requestAnimationFrame(() => setTimeout(() => focusComposer(), 10));
}
async function closeWithoutSendingButSaveModel() {
if (STATE.actionTaken) return;
STATE.actionTaken = true;
const option = OPTIONS[STATE.selectedIndex];
const threadId = STATE.dialogThreadId || getCurrentThreadId();
try {
await applyModelToComposer(threadId, option.key);
showToast(`${ui('toast_apply')}: ${getOptionLabel(option)}`, true);
} catch (err) {
console.error(err);
showToast(`${ui('toast_failed')}: ${getOptionLabel(option)}`, false);
} finally {
closeDialog();
}
}
async function sendWithSelectedModel() {
if (STATE.actionTaken) return;
STATE.actionTaken = true;
const option = OPTIONS[STATE.selectedIndex];
const threadId = STATE.dialogThreadId || getCurrentThreadId();
try {
await applyModelToComposer(threadId, option.key);
if (isEphemeralThreadId(threadId)) {
STATE.pendingThreadModel = option.key;
}
const sent = await sendOnce();
if (!sent) throw new Error('送信失敗');
STATE.hotkeyCooldownUntil = Date.now() + 1500;
showToast(`${ui('toast_send')}: ${getOptionLabel(option)}`, true);
} catch (err) {
console.error(err);
showToast(`${ui('toast_failed')}: ${getOptionLabel(option)}`, false);
} finally {
closeDialog();
}
}
function renderDialog() {
if (STATE.isOpen) return;
STATE.lastComposer = getComposer() || document.activeElement;
STATE.isOpen = true;
STATE.actionTaken = false;
const overlay = document.createElement('div');
STATE.overlay = overlay;
Object.assign(overlay.style, {
position: 'fixed',
inset: '0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(0,0,0,.22)',
zIndex: '2147483646',
});
const panel = document.createElement('div');
STATE.panel = panel;
Object.assign(panel.style, {
width: '360px',
maxWidth: 'calc(100vw - 32px)',
background: '#fff',
color: '#111',
borderRadius: '16px',
boxShadow: '0 18px 60px rgba(0,0,0,.30)',
padding: '14px',
fontFamily: 'system-ui, sans-serif',
});
overlay.appendChild(panel);
const title = document.createElement('div');
title.textContent = ui('title');
Object.assign(title.style, {
fontSize: '18px',
fontWeight: '700',
marginBottom: '8px',
});
panel.appendChild(title);
const hint = document.createElement('div');
hint.textContent = ui('hint');
Object.assign(hint.style, {
fontSize: '12px',
color: '#555',
marginBottom: '10px',
});
panel.appendChild(hint);
function paintSelection() {
Array.from(panel.querySelectorAll('[data-index]')).forEach((el, i) => {
const active = i === STATE.selectedIndex;
el.style.background = active ? '#e8f0fe' : '#fff';
el.style.borderColor = active ? '#8ab4f8' : '#e5e7eb';
});
}
OPTIONS.forEach((opt, idx) => {
const row = document.createElement('div');
row.dataset.index = String(idx);
row.textContent = getOptionLabel(opt);
Object.assign(row.style, {
padding: '12px 14px',
borderRadius: '12px',
cursor: 'pointer',
userSelect: 'none',
marginTop: idx ? '6px' : '0',
border: '1px solid #e5e7eb',
});
row.addEventListener('mouseenter', () => {
STATE.selectedIndex = idx;
paintSelection();
});
row.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
STATE.selectedIndex = idx;
paintSelection();
await sendWithSelectedModel();
});
panel.appendChild(row);
});
paintSelection();
overlay.addEventListener('mousedown', async (e) => {
if (e.target !== overlay) return;
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
await closeWithoutSendingButSaveModel();
}, true);
function onDialogKeydown(e) {
if (!STATE.isOpen || STATE.applying) return;
if (e.key === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
STATE.selectedIndex = (STATE.selectedIndex + OPTIONS.length - 1) % OPTIONS.length;
paintSelection();
} else if (e.key === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
STATE.selectedIndex = (STATE.selectedIndex + 1) % OPTIONS.length;
paintSelection();
} else if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
sendWithSelectedModel();
} else if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
closeWithoutSendingButSaveModel();
}
}
document.addEventListener('keydown', onDialogKeydown, true);
overlay._cleanup = () => document.removeEventListener('keydown', onDialogKeydown, true);
document.body.appendChild(overlay);
}
function openDialogWithThreadState() {
if (STATE.isOpen) return;
const threadId = getCurrentThreadId();
STATE.dialogThreadId = threadId;
STATE.selectedIndex = getOptionIndexByModel(getCurrentModelForThread(threadId));
renderDialog();
}
async function waitForComposerSurface() {
for (let i = 0; i < 25; i++) {
if (findHeaderModelButton() && getComposer()) return true;
await sleep(120);
}
return false;
}
async function syncThreadModelState() {
if (STATE.isOpen || STATE.applying) return;
const token = ++STATE.threadSyncToken;
const threadId = getCurrentThreadId();
if (STATE.pendingThreadModel && !isEphemeralThreadId(threadId)) {
saveThreadModel(threadId, STATE.pendingThreadModel);
STATE.pendingThreadModel = null;
}
const savedModel = loadThreadModel(threadId);
const fallbackModel = savedModel || detectCurrentModelFromUi() || DEFAULT_MODEL_KEY;
STATE.selectedIndex = getOptionIndexByModel(fallbackModel);
if (!savedModel) return;
const ready = await waitForComposerSurface();
if (!ready) return;
if (token !== STATE.threadSyncToken) return;
if (STATE.isOpen || STATE.applying) return;
if (getCurrentThreadId() !== threadId) return;
const currentUiModel = detectCurrentModelFromUi();
if (currentUiModel === savedModel) return;
if (!currentUiModel && savedModel === DEFAULT_MODEL_KEY) return;
try {
await applyModelToComposer(threadId, savedModel);
} catch (err) {
console.error(err);
}
}
function startThreadWatcher() {
setInterval(() => {
if (location.href === STATE.lastKnownHref) return;
STATE.lastKnownHref = location.href;
syncThreadModelState().catch(err => console.error(err));
}, 300);
}
window.tmChatGPTModelSelectorDebug = {
dumpAll() {
const obj = {
threadId: getCurrentThreadId(),
savedThreadModel: loadThreadModel(getCurrentThreadId()),
detectedThreadModel: detectCurrentModelFromUi(),
modelTrigger: findHeaderModelButton(),
thoughtPill: findThoughtPillButton(),
thoughtPillButtons: getThoughtPillButtons().map(el => ({
text: (el.innerText || '').trim(),
ariaLabel: el.getAttribute('aria-label'),
className: String(el.className || ''),
visible: visible(el),
})),
sendButton: findSendButton(),
modelMenuItems: getModelMenuItems().map(el => ({
text: (el.innerText || '').trim(),
dataTestid: el.getAttribute('data-testid'),
role: el.getAttribute('role'),
visible: visible(el),
})),
effortMenuItems: getEffortMenuItems().map(el => ({
text: (el.innerText || '').trim(),
ariaChecked: el.getAttribute('aria-checked'),
role: el.getAttribute('role'),
visible: visible(el),
})),
};
console.log(obj);
return obj;
}
};
document.addEventListener('keydown', (e) => {
if (e.defaultPrevented) return;
if (STATE.isOpen) return;
if (Date.now() < STATE.hotkeyCooldownUntil) return;
if (e.key === 'Enter' && e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
const composer = getComposer();
if (!composer) return;
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
openDialogWithThreadState();
}
}, true);
startThreadWatcher();
syncThreadModelState().catch(err => console.error(err));
})();