diff --git a/index.json b/index.json
index 2694c08..08a9bec 100644
--- a/index.json
+++ b/index.json
@@ -1,6 +1,6 @@
{
"version": 1,
- "updated_at": "2026-07-16T08:27:07Z",
+ "updated_at": "2026-07-28T03:17:39Z",
"scripts": [
{
"id": "codex-context-used-meter",
@@ -37,8 +37,8 @@
{
"id": "model-deck",
"name": "Model Deck",
- "description": "悬停官方模型按钮打开模型矩阵,提供 Fast 切换和全局预设",
- "version": "0.1.0",
+ "description": "悬停官方模型按钮打开模型矩阵,自动识别模型能力,支持用户置顶、名称列调宽、Fast 切换和全局预设",
+ "version": "0.3.9",
"author": "0xTotoroX",
"tags": [
"codex",
@@ -50,7 +50,7 @@
],
"homepage": "https://github.com/0xTotoroX/model-deck",
"script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/model-deck.js",
- "sha256": "6a085811d626bbe9ded8e73baf9fbaf719f30297e0ce58c35259a083a056ad2d"
+ "sha256": "381362c2bffbc570a27a1879fd18b59235dd0bf7a7d42817bacf666c466a3fa5"
},
{
"id": "codex-token-usage",
diff --git a/scripts/model-deck.js b/scripts/model-deck.js
index 24306a8..c9ef0af 100644
--- a/scripts/model-deck.js
+++ b/scripts/model-deck.js
@@ -1,11 +1,33 @@
-/* Model Deck v0.1.0 - Codex++ userscript */
+/* Model Deck v0.3.9 - Codex++ userscript */
(() => {
// src/index.js
var API_KEY = "__codexPlusQuickModelPresets";
- var VERSION = "0.1.0";
+ var VERSION = "0.3.9";
var STORAGE_KEY = "codexpp.quickModelPresets.v1";
var PREVIEW_ID = "codexpp-qmp-preset-preview";
var LOADING_MESSAGE = "\u7B49\u5F85\u5B98\u65B9\u6A21\u578B\u6570\u636E\uFF0C\u8BF7\u901A\u8FC7 Codex++ \u91CD\u542F ChatGPT";
+ var PANEL_SAFE_MARGIN = 12;
+ var PANEL_GAP = 8;
+ var PANEL_MIN_WIDTH = 360;
+ var PANEL_MAX_WIDTH = 760;
+ var DEFAULT_MODEL_COLUMN_WIDTH = 112;
+ var MODEL_COLUMN_MIN_WIDTH = 112;
+ var MODEL_COLUMN_MAX_WIDTH = 320;
+ var REASONING_COLUMN_WIDTH = 68;
+ var PANEL_CLOSE_DURATION = 120;
+ var FLOATING_CLOSE_DURATION = 110;
+ var FAST_FEEDBACK_ON_DURATION = 620;
+ var FAST_FEEDBACK_OFF_DURATION = 420;
+ var HOST_FONT_SIZE_FALLBACK = 15;
+ var HOST_FONT_SIZE_MIN = 12;
+ var HOST_FONT_SIZE_MAX = 22;
+ var DECK_FONT_RATIO = 9 / 10;
+ var DECK_FONT_SIZE_MIN = 12;
+ var DECK_FONT_SIZE_MAX = 17;
+ var HOST_FONT_FAMILY_FALLBACK = '-apple-system, "system-ui", "Segoe UI", sans-serif';
+ var HOST_FONT_WEIGHT_FALLBACK = 400;
+ var HOST_FONT_WEIGHT_MIN = 100;
+ var HOST_FONT_WEIGHT_MAX = 900;
var MENU_SELECTOR = '[role="menu"][data-state="open"], [role="listbox"], [data-radix-menu-content][data-state="open"]';
var TRIGGER_OWNER_SELECTOR = "[data-codex-composer-root],[data-codex-composer],footer,form";
var REASONING_LABELS = Object.freeze({
@@ -21,27 +43,41 @@
var REASONING_RANK = new Map(
Object.keys(REASONING_LABELS).map((key, index) => [key, index])
);
- var LEGACY_MODEL_KEYS = /* @__PURE__ */ new Set(["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"]);
+ var VERIFIED_REASONING_CAPABILITIES = Object.freeze({
+ "grok-4.5": Object.freeze({
+ efforts: Object.freeze(["low", "medium", "high"]),
+ defaultEffort: "high",
+ sourceLabel: "xAI Docs",
+ sourceUrl: "https://docs.x.ai/developers/model-capabilities/text/reasoning",
+ verifiedAt: "2026-07-26",
+ unavailableReason: "xAI \u5B98\u65B9\uFF1AGrok 4.5 \u4EC5\u652F\u6301 Low / Medium / High"
+ })
+ });
+ var DEFAULT_PINNED_MODEL_KEYS = Object.freeze([
+ "gpt-5.6-sol",
+ "gpt-5.6-terra",
+ "gpt-5.6-luna"
+ ]);
var REASONING_ALIASES = new Map(
Object.entries({
none: "none",
minimal: "minimal",
low: "low",
light: "low",
- "\u8F7B\u5EA6": "low",
+ \u8F7B\u5EA6: "low",
medium: "medium",
- "\u4E2D": "medium",
+ \u4E2D: "medium",
high: "high",
- "\u9AD8": "high",
+ \u9AD8: "high",
xhigh: "xhigh",
"extra high": "xhigh",
- "\u6781\u9AD8": "xhigh",
+ \u6781\u9AD8: "xhigh",
max: "max",
maximum: "max",
- "\u6700\u5927": "max",
- "\u6700\u9AD8": "max",
+ \u6700\u5927: "max",
+ \u6700\u9AD8: "max",
ultra: "ultra",
- "\u8D85\u9AD8": "ultra"
+ \u8D85\u9AD8: "ultra"
})
);
var SECTION_PATTERNS = {
@@ -49,6 +85,26 @@
reasoning: [/^reasoning(?: effort| level)?(?:\s|$)/i, /^推理强度(?:\s|$)/u],
speed: [/^speed(?:\s|$)/i, /^速度(?:\s|$)/u]
};
+ var ROW_PREFIXES = {
+ model: /^(?:model|模型)\s*/iu,
+ reasoning: /^(?:reasoning(?: effort| level)?|推理强度)\s*/iu,
+ speed: /^(?:speed|速度)\s*/iu
+ };
+ var HTML_ESCAPES = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'"
+ };
+ var ICON_PATHS = {
+ zap: '',
+ plus: '',
+ chevron: '',
+ pin: '',
+ pencil: '',
+ trash: ''
+ };
var inheritedModelList = null;
try {
inheritedModelList = window[API_KEY]?.getModelListCache?.() || null;
@@ -67,17 +123,22 @@
var actionMenuAnchor = null;
var resizeObserver = null;
var mutationObserver = null;
+ var typographyObserver = null;
var triggerLifetimes = /* @__PURE__ */ new Map();
var boundTrigger = null;
var openTimer = null;
var closeTimer = null;
+ var panelHideTimer = null;
var previewTimer = null;
var previewHideTimer = null;
+ var actionMenuHideTimer = null;
+ var fastFeedbackTimer = null;
var visible = false;
var disarmed = false;
var nativeSuspended = false;
var pointerOnTrigger = false;
var pointerOnPanel = false;
+ var focusOnPanel = false;
var holdOpen = false;
var probe = null;
var probeQueue = Promise.resolve();
@@ -98,13 +159,18 @@
var capabilityFingerprint = inheritedModels.length ? JSON.stringify(inheritedModels) : "";
var capability = inheritedModels.length ? { status: "ready", generation: 1, models: inheritedModels } : { status: "loading", generation: 0, models: [] };
var store = readStore();
+ var modelColumnWidth = store.modelColumnWidth;
var renameId = null;
var finishRename = null;
var drag = null;
+ var modelColumnResize = null;
var presetClickSuppression = null;
var presetClickSuppressionTimer = null;
- var legacyExpanded = false;
- var autoExpandedLegacyKey = null;
+ var otherModelsExpanded = false;
+ var autoExpandedOtherKey = null;
+ var panelLeftLock = null;
+ var fastFeedback = null;
+ var hostTypography = fallbackHostTypography();
function unavailableSnapshot(status, text) {
return {
status,
@@ -128,6 +194,196 @@
window.clearTimeout(timer);
timers.delete(timer);
}
+ function clamp(value, minimum, maximum) {
+ return Math.min(maximum, Math.max(minimum, value));
+ }
+ function roundPixel(value) {
+ return Math.round(value * 100) / 100;
+ }
+ function syncFastFeedbackPresentation() {
+ if (!root) return;
+ if (!fastFeedback) {
+ root.removeAttribute("data-fast-feedback");
+ root.style.removeProperty("--qmp-fast-feedback-delay");
+ return;
+ }
+ const elapsed = clamp(
+ performanceRef.now() - fastFeedback.startedAt,
+ 0,
+ fastFeedback.duration
+ );
+ root.dataset.fastFeedback = fastFeedback.mode;
+ root.style.setProperty(
+ "--qmp-fast-feedback-delay",
+ `${-roundPixel(elapsed)}ms`
+ );
+ }
+ function startFastFeedback(enabled) {
+ clearLater(fastFeedbackTimer);
+ fastFeedbackTimer = null;
+ if (root) {
+ root.removeAttribute("data-fast-feedback");
+ root.style.removeProperty("--qmp-fast-feedback-delay");
+ root.getBoundingClientRect();
+ }
+ const duration = enabled ? FAST_FEEDBACK_ON_DURATION : FAST_FEEDBACK_OFF_DURATION;
+ fastFeedback = {
+ mode: enabled ? "on" : "off",
+ startedAt: performanceRef.now(),
+ duration
+ };
+ syncFastFeedbackPresentation();
+ fastFeedbackTimer = later(() => {
+ fastFeedbackTimer = null;
+ fastFeedback = null;
+ syncFastFeedbackPresentation();
+ }, duration + 40);
+ }
+ function buildTypography(source, fontFamily, hostFontSizeRaw, hostFontWeightRaw) {
+ const hostFontSize = clamp(
+ Number.isFinite(hostFontSizeRaw) ? hostFontSizeRaw : HOST_FONT_SIZE_FALLBACK,
+ HOST_FONT_SIZE_MIN,
+ HOST_FONT_SIZE_MAX
+ );
+ const hostFontWeight = Math.round(
+ clamp(
+ Number.isFinite(hostFontWeightRaw) ? hostFontWeightRaw : HOST_FONT_WEIGHT_FALLBACK,
+ HOST_FONT_WEIGHT_MIN,
+ HOST_FONT_WEIGHT_MAX
+ )
+ );
+ const fontSize = roundPixel(
+ clamp(
+ hostFontSize * DECK_FONT_RATIO,
+ DECK_FONT_SIZE_MIN,
+ DECK_FONT_SIZE_MAX
+ )
+ );
+ const smallFontSize = roundPixel(
+ clamp(fontSize - 1, DECK_FONT_SIZE_MIN - 1, DECK_FONT_SIZE_MAX - 1)
+ );
+ return {
+ source,
+ fontFamily: fontFamily || HOST_FONT_FAMILY_FALLBACK,
+ hostFontWeight,
+ hostFontSize: roundPixel(hostFontSize),
+ fontSize,
+ smallFontSize,
+ lineHeight: roundPixel(fontSize * 1.4),
+ smallLineHeight: roundPixel(smallFontSize * 1.4),
+ tightLineHeight: roundPixel(fontSize * 1.36)
+ };
+ }
+ function fallbackHostTypography() {
+ return buildTypography("fallback", "", NaN, NaN);
+ }
+ function hostTypographySource() {
+ const trigger = [
+ ...documentRef.querySelectorAll("[data-codex-intelligence-trigger]")
+ ].find((element) => isVisible(element));
+ if (trigger) return { element: trigger, source: "model-trigger" };
+ const composer = [
+ ...documentRef.querySelectorAll(
+ '[data-codex-composer] .ProseMirror, [data-codex-composer] [contenteditable="true"], .ProseMirror'
+ )
+ ].find((element) => isVisible(element));
+ if (composer) return { element: composer, source: "composer" };
+ if (documentRef.body) return { element: documentRef.body, source: "body" };
+ return { element: documentRef.documentElement, source: "document" };
+ }
+ function readHostTypography() {
+ const { element, source } = hostTypographySource();
+ if (!element) return fallbackHostTypography();
+ const style = window.getComputedStyle(element);
+ return buildTypography(
+ source,
+ style.fontFamily,
+ Number.parseFloat(style.fontSize),
+ Number.parseFloat(style.fontWeight)
+ );
+ }
+ function typographyFingerprint(value) {
+ return [
+ value.source,
+ value.fontFamily,
+ value.hostFontWeight,
+ value.hostFontSize,
+ value.fontSize
+ ].join("|");
+ }
+ function applyTypographyVariables(element) {
+ if (!(element instanceof window.HTMLElement)) return;
+ element.style.setProperty(
+ "--qmp-host-font-family",
+ hostTypography.fontFamily
+ );
+ element.style.setProperty(
+ "--qmp-host-font-weight",
+ String(hostTypography.hostFontWeight)
+ );
+ setCustomPixelStyle(element, "--qmp-font-size", hostTypography.fontSize);
+ setCustomPixelStyle(
+ element,
+ "--qmp-small-font-size",
+ hostTypography.smallFontSize
+ );
+ setCustomPixelStyle(element, "--qmp-line-height", hostTypography.lineHeight);
+ setCustomPixelStyle(
+ element,
+ "--qmp-small-line-height",
+ hostTypography.smallLineHeight
+ );
+ setCustomPixelStyle(
+ element,
+ "--qmp-tight-line-height",
+ hostTypography.tightLineHeight
+ );
+ }
+ function syncHostTypography(force = false) {
+ const next = readHostTypography();
+ if (force || typographyFingerprint(next) !== typographyFingerprint(hostTypography)) {
+ hostTypography = next;
+ }
+ [root, preview, actionMenu].forEach(applyTypographyVariables);
+ }
+ function installTypographyObserver() {
+ if (typographyObserver) return;
+ typographyObserver = new window.MutationObserver(() => syncHostTypography());
+ const options = {
+ attributes: true,
+ attributeFilter: [
+ "class",
+ "style",
+ "data-theme",
+ "data-appearance",
+ "data-color-mode"
+ ]
+ };
+ if (documentRef.documentElement)
+ typographyObserver.observe(documentRef.documentElement, options);
+ if (documentRef.body) typographyObserver.observe(documentRef.body, options);
+ window.addEventListener("resize", () => syncHostTypography(), {
+ passive: true,
+ signal
+ });
+ }
+ function pixels(value) {
+ return `${roundPixel(value)}px`;
+ }
+ function setPixelStyle(element, property, value) {
+ const next = pixels(value);
+ if (element.style[property] !== next) element.style[property] = next;
+ }
+ function setCustomPixelStyle(element, property, value) {
+ const next = pixels(value);
+ if (element.style.getPropertyValue(property) !== next)
+ element.style.setProperty(property, next);
+ }
+ function reducedMotion() {
+ return Boolean(
+ window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches
+ );
+ }
function clearPresetClickSuppression() {
clearLater(presetClickSuppressionTimer);
presetClickSuppressionTimer = null;
@@ -139,7 +395,8 @@
presetClickSuppression = token;
if (!taskBound) return;
const timer = later(() => {
- if (presetClickSuppressionTimer === timer) presetClickSuppressionTimer = null;
+ if (presetClickSuppressionTimer === timer)
+ presetClickSuppressionTimer = null;
if (presetClickSuppression === token) presetClickSuppression = null;
}, 0);
presetClickSuppressionTimer = timer;
@@ -188,12 +445,24 @@
const key = canonicalReasoning(raw);
if (!key) return null;
const officialLabel = typeof value === "object" && value ? value.displayName || value.label || value.name : null;
- return { key, label: REASONING_LABELS[key] || String(officialLabel || raw), enabled: true };
+ return {
+ key,
+ label: REASONING_LABELS[key] || String(officialLabel || raw),
+ enabled: true
+ };
+ }
+ function verifiedReasoningCapability(modelKey) {
+ return VERIFIED_REASONING_CAPABILITIES[String(modelKey ?? "").trim().toLowerCase()] || null;
}
function tierSupportsFast(tier) {
- if (typeof tier === "string") return ["priority", "fast"].includes(tier.toLowerCase());
- const id = String(tier?.id ?? tier?.key ?? tier?.value ?? tier?.tier ?? "").toLowerCase();
- const name = String(tier?.name ?? tier?.displayName ?? tier?.label ?? "").toLowerCase();
+ if (typeof tier === "string")
+ return ["priority", "fast"].includes(tier.toLowerCase());
+ const id = String(
+ tier?.id ?? tier?.key ?? tier?.value ?? tier?.tier ?? ""
+ ).toLowerCase();
+ const name = String(
+ tier?.name ?? tier?.displayName ?? tier?.label ?? ""
+ ).toLowerCase();
return id === "priority" || name === "fast";
}
function projectModels(records) {
@@ -202,19 +471,25 @@
const models = [];
records.forEach((record) => {
if (!record || !Array.isArray(record.supportedReasoningEfforts)) return;
- if (!Array.isArray(record.serviceTiers)) return;
const key = String(record.model ?? "").trim();
const label = String(record.displayName ?? "").trim();
if (!key || !label || used.has(key)) return;
- const reasoningOptions = record.supportedReasoningEfforts.map(reasoningOption).filter(Boolean);
- if (!reasoningOptions.length || new Set(reasoningOptions.map((item) => item.key)).size !== reasoningOptions.length) return;
+ const projectedReasoningOptions = record.supportedReasoningEfforts.map(reasoningOption).filter(Boolean);
+ const reasoningEvidence = verifiedReasoningCapability(key);
+ const reasoningOptions = reasoningEvidence ? projectedReasoningOptions.filter(
+ (option) => reasoningEvidence.efforts.includes(option.key)
+ ) : projectedReasoningOptions;
+ if (!reasoningOptions.length || new Set(reasoningOptions.map((item) => item.key)).size !== reasoningOptions.length)
+ return;
+ const serviceTiers = Array.isArray(record.serviceTiers) ? record.serviceTiers : [];
used.add(key);
models.push({
key,
label,
reasoningOptions,
- defaultReasoningEffort: canonicalReasoning(record.defaultReasoningEffort),
- fastSupported: record.serviceTiers.some(tierSupportsFast)
+ defaultReasoningEffort: reasoningEvidence?.defaultEffort || canonicalReasoning(record.defaultReasoningEffort),
+ reasoningEvidence: reasoningEvidence ? { ...reasoningEvidence, efforts: [...reasoningEvidence.efforts] } : null,
+ fastSupported: serviceTiers.some(tierSupportsFast)
});
});
return models;
@@ -244,11 +519,15 @@
const models = projectModels(records);
requestIds.delete(matchingId);
if (!models.length) return;
- modelListCache = JSON.parse(JSON.stringify(records));
+ modelListCache = window.structuredClone(records);
const fingerprint = JSON.stringify(models);
if (fingerprint === capabilityFingerprint) return;
capabilityFingerprint = fingerprint;
- capability = { status: "ready", generation: ++capabilityGeneration, models };
+ capability = {
+ status: "ready",
+ generation: ++capabilityGeneration,
+ models
+ };
scheduleScan();
if (visible) void syncSnapshot();
} catch {
@@ -256,7 +535,15 @@
}
window.addEventListener("message", onBridgeMessage, { capture: true, signal });
function emptyStore() {
- return { schemaVersion: 1, revision: 0, nextSequence: 1, updatedAt: 0, presets: [] };
+ return {
+ schemaVersion: 2,
+ revision: 0,
+ nextSequence: 1,
+ updatedAt: 0,
+ presets: [],
+ pinnedModelKeys: [...DEFAULT_PINNED_MODEL_KEYS],
+ modelColumnWidth: DEFAULT_MODEL_COLUMN_WIDTH
+ };
}
function nextPresetName(presets) {
const occupied = /* @__PURE__ */ new Set();
@@ -277,7 +564,8 @@
if (!value || typeof value !== "object") return null;
const modelKey = String(value.modelKey ?? "").trim();
const reasoningKey = String(value.reasoningKey ?? "").trim();
- if (!modelKey || !reasoningKey || typeof value.fastEnabled !== "boolean") return null;
+ if (!modelKey || !reasoningKey || typeof value.fastEnabled !== "boolean")
+ return null;
const now = Date.now();
return {
id: String(value.id || uuid()),
@@ -291,37 +579,66 @@
updatedAt: Number(value.updatedAt) || now
};
}
+ function normalizePinnedModelKeys(value) {
+ if (!Array.isArray(value)) return [...DEFAULT_PINNED_MODEL_KEYS];
+ return [
+ ...new Set(value.map((key) => String(key ?? "").trim()).filter(Boolean))
+ ];
+ }
+ function normalizeModelColumnWidth(value) {
+ const width = Number(value);
+ if (!Number.isFinite(width)) return DEFAULT_MODEL_COLUMN_WIDTH;
+ return clamp(width, MODEL_COLUMN_MIN_WIDTH, MODEL_COLUMN_MAX_WIDTH);
+ }
function readStore() {
try {
- const parsed = JSON.parse(window.localStorage.getItem(STORAGE_KEY) || "null");
+ const parsed = JSON.parse(
+ window.localStorage.getItem(STORAGE_KEY) || "null"
+ );
const source = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.presets) ? parsed.presets : [];
const presets = source.map(normalizePreset).filter(Boolean);
- const largest = presets.reduce((max, item) => /^\d+$/.test(item.name) ? Math.max(max, Number(item.name)) : max, 0);
+ const largest = presets.reduce(
+ (max, item) => /^\d+$/.test(item.name) ? Math.max(max, Number(item.name)) : max,
+ 0
+ );
return {
- schemaVersion: 1,
+ schemaVersion: 2,
revision: Number(parsed?.revision) || 0,
nextSequence: Math.max(Number(parsed?.nextSequence) || 1, largest + 1),
updatedAt: Number(parsed?.updatedAt) || 0,
- presets
+ presets,
+ pinnedModelKeys: normalizePinnedModelKeys(parsed?.pinnedModelKeys),
+ modelColumnWidth: normalizeModelColumnWidth(parsed?.modelColumnWidth)
};
} catch {
return emptyStore();
}
}
- function updateStore(change) {
- const next = { ...store, presets: store.presets.map((item) => ({ ...item })) };
+ function updateStore(change, options = {}) {
+ const next = {
+ ...store,
+ presets: store.presets.map((item) => ({ ...item })),
+ pinnedModelKeys: [...store.pinnedModelKeys]
+ };
change(next);
+ next.schemaVersion = 2;
+ next.pinnedModelKeys = normalizePinnedModelKeys(next.pinnedModelKeys);
+ next.modelColumnWidth = normalizeModelColumnWidth(next.modelColumnWidth);
next.revision += 1;
next.updatedAt = Date.now();
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
store = next;
- render();
+ modelColumnWidth = next.modelColumnWidth;
+ if (options.render !== false) render();
}
function accessibleText(element) {
- return String(element?.getAttribute?.("aria-label") || element?.getAttribute?.("title") || element?.textContent || "").replace(/\s+/g, " ").trim();
+ return String(
+ element?.getAttribute?.("aria-label") || element?.getAttribute?.("title") || element?.textContent || ""
+ ).replace(/\s+/g, " ").trim();
}
function isVisible(element, requireBox = true) {
- if (!(element instanceof window.HTMLElement) || !element.isConnected) return false;
+ if (!(element instanceof window.HTMLElement) || !element.isConnected)
+ return false;
const style = window.getComputedStyle(element);
if (style.display === "none" || style.visibility === "hidden") return false;
if (!requireBox) return true;
@@ -334,29 +651,47 @@
function isAutoReviewModel(model) {
return model.key.toLowerCase() === "codex-auto-review" || normalizeModelLabel(model.label) === "codex auto review";
}
- function isLegacyModel(model) {
- return LEGACY_MODEL_KEYS.has(model.key.toLowerCase());
+ function isPinnedModelKey(modelKey) {
+ return store.pinnedModelKeys.includes(modelKey);
}
function compactModelLabel(label) {
- return String(label || "").replace(/^GPT[-\s]*/i, "").replace(/-/g, " ").replace(/\b(sol|terra|luna|mini)\b/gi, (word) => word[0].toUpperCase() + word.slice(1).toLowerCase());
+ return String(label || "").replace(/^GPT[-\s]*/i, "").replace(/-/g, " ").replace(
+ /\b(sol|terra|luna|mini)\b/gi,
+ (word) => word[0].toUpperCase() + word.slice(1).toLowerCase()
+ );
}
function modelLike(text) {
return /\b(?:gpt[-\s]*)?\d+(?:\.\d+)+(?:[-\s]+[a-z][\w.-]*)?/i.test(text);
}
function triggerScore(element) {
- if (element.closest("[data-codexpp-qmp-root]") || !isVisible(element)) return -Infinity;
+ if (element.closest("[data-codexpp-qmp-root]") || !isVisible(element))
+ return -Infinity;
const text = accessibleText(element);
- if (/codex\+\+|microphone|send|stop|attachment|麦克风|发送|停止|附件/i.test(text)) return -Infinity;
+ if (/codex\+\+|microphone|send|stop|attachment|麦克风|发送|停止|附件/i.test(
+ text
+ ))
+ return -Infinity;
let score = element.getAttribute("aria-haspopup") === "menu" ? 100 : 0;
- if (capability.models.some((model) => normalizeModelLabel(text).includes(normalizeModelLabel(model.label)))) score += 60;
+ if (capability.models.some(
+ (model) => normalizeModelLabel(text).includes(normalizeModelLabel(model.label))
+ ))
+ score += 60;
else if (modelLike(text)) score += 45;
- if ([...REASONING_ALIASES.keys()].some((label) => label && text.toLowerCase().includes(label))) score += 25;
- if (element.closest("[data-codex-composer-root],[data-codex-composer],footer")) score += 20;
+ if ([...REASONING_ALIASES.keys()].some(
+ (label) => label && text.toLowerCase().includes(label)
+ ))
+ score += 25;
+ if (element.closest("[data-codex-composer-root],[data-codex-composer],footer"))
+ score += 20;
if (element.querySelector("svg")) score += 10;
return score;
}
function findTriggers() {
- const candidates = [...documentRef.querySelectorAll('button[aria-haspopup="menu"],[role="button"][aria-haspopup="menu"]')].map((element) => ({ element, score: triggerScore(element) })).filter((item) => item.score >= 140);
+ const candidates = [
+ ...documentRef.querySelectorAll(
+ 'button[aria-haspopup="menu"],[role="button"][aria-haspopup="menu"]'
+ )
+ ].map((element) => ({ element, score: triggerScore(element) })).filter((item) => item.score >= 140);
const groups = /* @__PURE__ */ new Map();
const unowned = [];
candidates.forEach((candidate) => {
@@ -374,15 +709,51 @@
return fallback ? [fallback] : [];
}
function uniqueTrigger(candidates) {
- const ranked = [...candidates].sort((left, right) => right.score - left.score);
+ const ranked = [...candidates].sort(
+ (left, right) => right.score - left.score
+ );
if (!ranked.length || ranked[0].score === ranked[1]?.score) return null;
return ranked[0].element;
}
+ function singleMatchingTrigger(triggers, matches) {
+ const candidates = triggers.filter(matches);
+ return candidates.length === 1 ? candidates[0] : null;
+ }
+ function resolveTrigger(preferred) {
+ const triggers = findTriggers();
+ bindTriggers(triggers);
+ const available = (trigger) => Boolean(trigger?.isConnected && triggers.includes(trigger));
+ if (available(preferred)) return preferred;
+ if (available(boundTrigger)) return boundTrigger;
+ const hovered = singleMatchingTrigger(triggers, (trigger) => {
+ try {
+ return trigger.matches(":hover");
+ } catch {
+ return false;
+ }
+ });
+ const activeElement = documentRef.activeElement;
+ const focused = activeElement && singleMatchingTrigger(
+ triggers,
+ (trigger) => trigger.closest(TRIGGER_OWNER_SELECTOR)?.contains(activeElement)
+ );
+ const resolved = hovered || focused || (triggers.length === 1 ? triggers[0] : null);
+ if (!boundTrigger && resolved) boundTrigger = resolved;
+ return resolved;
+ }
function isOwnedUi(element) {
- return Boolean(element?.matches?.("[data-codexpp-qmp-root],[data-codexpp-qmp-preview],[data-codexpp-qmp-actions]") || element?.closest?.("[data-codexpp-qmp-root],[data-codexpp-qmp-preview],[data-codexpp-qmp-actions]"));
+ return Boolean(
+ element?.matches?.(
+ "[data-codexpp-qmp-root],[data-codexpp-qmp-preview],[data-codexpp-qmp-actions]"
+ ) || element?.closest?.(
+ "[data-codexpp-qmp-root],[data-codexpp-qmp-preview],[data-codexpp-qmp-actions]"
+ )
+ );
}
function openMenus() {
- return [...documentRef.querySelectorAll(MENU_SELECTOR)].filter((menu) => isVisible(menu, false) && !isOwnedUi(menu));
+ return [...documentRef.querySelectorAll(MENU_SELECTOR)].filter(
+ (menu) => isVisible(menu, false) && !isOwnedUi(menu)
+ );
}
function claimProbeRoots() {
if (!probe) return [];
@@ -395,16 +766,22 @@
return [...probe.roots].filter((menu) => menu.isConnected);
}
function nativeMenuOpen() {
- return openMenus().some((menu) => menu.getAttribute("data-codexpp-qmp-probe-root") !== "true");
+ return openMenus().some(
+ (menu) => menu.getAttribute("data-codexpp-qmp-probe-root") !== "true"
+ );
}
function yieldProbeToUser() {
if (!probe) return;
- probe.roots.forEach((menu) => menu.removeAttribute("data-codexpp-qmp-probe-root"));
+ probe.roots.forEach(
+ (menu) => menu.removeAttribute("data-codexpp-qmp-probe-root")
+ );
probe = null;
nativeTakeover();
}
function sectionRow(rootElement, section) {
- return [...rootElement.querySelectorAll('[role="menuitem"][aria-haspopup="menu"]')].find((item) => {
+ return [
+ ...rootElement.querySelectorAll('[role="menuitem"][aria-haspopup="menu"]')
+ ].find((item) => {
const text = accessibleText(item);
return SECTION_PATTERNS[section].some((pattern) => pattern.test(text));
});
@@ -429,16 +806,22 @@
pointerType: "mouse",
isPrimary: true
};
- element.dispatchEvent(new window.PointerEvent("pointerdown", { ...pointer, buttons: 1 }));
- element.dispatchEvent(new window.PointerEvent("pointerup", { ...pointer, buttons: 0 }));
- element.dispatchEvent(new window.MouseEvent("click", {
- bubbles: true,
- cancelable: true,
- composed: true,
- button: 0,
- buttons: 0,
- detail: 1
- }));
+ element.dispatchEvent(
+ new window.PointerEvent("pointerdown", { ...pointer, buttons: 1 })
+ );
+ element.dispatchEvent(
+ new window.PointerEvent("pointerup", { ...pointer, buttons: 0 })
+ );
+ element.dispatchEvent(
+ new window.MouseEvent("click", {
+ bubbles: true,
+ cancelable: true,
+ composed: true,
+ button: 0,
+ buttons: 0,
+ detail: 1
+ })
+ );
}
async function closeProbe(localSignal) {
const current = probe;
@@ -457,10 +840,20 @@
});
}
} finally {
- current.roots.forEach((menu) => menu.removeAttribute("data-codexpp-qmp-probe-root"));
+ current.roots.forEach(
+ (menu) => menu.removeAttribute("data-codexpp-qmp-probe-root")
+ );
if (probe === current) probe = null;
}
}
+ async function probing(localSignal, work) {
+ try {
+ return await work();
+ } finally {
+ await closeProbe(localSignal).catch(() => {
+ });
+ }
+ }
async function openMainMenu(localSignal, trigger) {
checkAbort(localSignal);
if (!trigger?.isConnected) throw new Error("\u672A\u627E\u5230\u5B98\u65B9\u6A21\u578B\u6309\u94AE");
@@ -479,21 +872,16 @@
dispatchPointerActivation(row);
return waitFor(() => {
claimProbeRoots();
- const menus = openMenus().filter((menu) => menu !== main && menuItems(menu).length);
+ const menus = openMenus().filter(
+ (menu) => menu !== main && menuItems(menu).length
+ );
const expectedId = row.getAttribute("aria-controls") || controlledId;
if (expectedId) return menus.find((menu) => menu.id === expectedId) || null;
return menus.find((menu) => !before.has(menu));
}, localSignal);
}
function rowValue(row, section) {
- let value = accessibleText(row);
- const prefixes = {
- model: /^(?:model|模型)\s*/iu,
- reasoning: /^(?:reasoning(?: effort| level)?|推理强度)\s*/iu,
- speed: /^(?:speed|速度)\s*/iu
- };
- value = value.replace(prefixes[section], "").trim();
- return value;
+ return accessibleText(row).replace(ROW_PREFIXES[section], "").trim();
}
function matchModel(models, label) {
const wanted = normalizeModelLabel(label);
@@ -503,7 +891,10 @@
return matches.length === 1 ? matches[0] : null;
}
function reasoningFromRow(rowText, model) {
- if (rowText.trim() === "\u6781\u9AD8" && model.reasoningOptions.filter((option) => ["xhigh", "max", "ultra"].includes(option.key)).length > 1) return null;
+ if (rowText.trim() === "\u6781\u9AD8" && model.reasoningOptions.filter(
+ (option) => ["xhigh", "max", "ultra"].includes(option.key)
+ ).length > 1)
+ return null;
const key = canonicalReasoning(rowText);
const matches = model.reasoningOptions.filter(
(option) => option.key === key || option.label.toLowerCase() === rowText.toLowerCase()
@@ -513,13 +904,17 @@
function orderedReasoning(models) {
const seen = /* @__PURE__ */ new Set();
const order = [];
- models.forEach((model) => model.reasoningOptions.forEach((option) => {
- if (!seen.has(option.key)) {
- seen.add(option.key);
- order.push(option.key);
- }
- }));
- return order.sort((left, right) => (REASONING_RANK.get(left) ?? Number.MAX_SAFE_INTEGER) - (REASONING_RANK.get(right) ?? Number.MAX_SAFE_INTEGER));
+ models.forEach(
+ (model) => model.reasoningOptions.forEach((option) => {
+ if (!seen.has(option.key)) {
+ seen.add(option.key);
+ order.push(option.key);
+ }
+ })
+ );
+ return order.sort(
+ (left, right) => (REASONING_RANK.get(left) ?? Number.MAX_SAFE_INTEGER) - (REASONING_RANK.get(right) ?? Number.MAX_SAFE_INTEGER)
+ );
}
function queuedProbe(work) {
const run = probeQueue.then(work, work);
@@ -529,83 +924,105 @@
}
async function readOfficialSnapshot(localSignal, trigger) {
checkAbort(localSignal);
- if (capability.status !== "ready") return unavailableSnapshot("loading", LOADING_MESSAGE);
- let main = null;
+ if (capability.status !== "ready")
+ return unavailableSnapshot("loading", LOADING_MESSAGE);
try {
- main = await openMainMenu(localSignal, trigger);
- const modelRow = sectionRow(main, "model");
- const reasoningRow = sectionRow(main, "reasoning");
- const speedRow = sectionRow(main, "speed");
- if (!modelRow || !reasoningRow) throw new Error("\u5B98\u65B9\u6A21\u578B\u83DC\u5355\u7ED3\u6784\u4E0D\u53EF\u7528");
- const modelText = rowValue(modelRow, "model");
- const reasoningText = rowValue(reasoningRow, "reasoning");
- const speedText = speedRow ? rowValue(speedRow, "speed") : "";
- const modelMenu = await openSubmenu(modelRow, main, localSignal);
- const visibleLabels = menuItems(modelMenu).map(itemLabel).filter(Boolean);
- const visibleModels = capability.models.filter(
- (model) => visibleLabels.some((label) => matchModel([model], label))
- );
- const currentModel = matchModel(visibleModels, modelText);
- if (!visibleModels.length || !currentModel) throw new Error("\u5B98\u65B9\u6A21\u578B\u8EAB\u4EFD\u65E0\u6CD5\u7CBE\u786E\u5339\u914D");
- const selectableModels = visibleModels.filter((model) => !isAutoReviewModel(model));
- const triggerReasoning = trigger?.getAttribute("data-selected-reasoning-effort");
- const triggerMatches = triggerReasoning == null ? [] : currentModel.reasoningOptions.filter(
- (option) => option.key === canonicalReasoning(triggerReasoning)
- );
- const reasoning = triggerReasoning == null ? reasoningFromRow(reasoningText, currentModel) : triggerMatches.length === 1 ? triggerMatches[0] : null;
- if (!reasoning) throw new Error("\u5B98\u65B9\u63A8\u7406\u5F3A\u5EA6\u65E0\u6CD5\u7CBE\u786E\u5339\u914D");
- let fastEnabled = false;
- if (speedRow) {
- if (/^(?:fast|快速)(?:\s|$)/i.test(speedText)) fastEnabled = true;
- else if (!/^(?:standard|标准)(?:\s|$)/i.test(speedText)) throw new Error("\u5B98\u65B9\u901F\u5EA6\u72B6\u6001\u4E0D\u53EF\u7528");
- } else if (currentModel.fastSupported) {
- throw new Error("\u5B98\u65B9\u901F\u5EA6\u83DC\u5355\u4E0D\u53EF\u7528");
- }
- return {
- status: "ready",
- generation: ++snapshotGeneration,
- current: {
- modelKey: currentModel.key,
- modelLabel: compactModelLabel(currentModel.label),
- reasoningKey: reasoning.key,
- reasoningLabel: reasoning.label,
- fastEnabled
- },
- models: selectableModels.map((model) => ({
- ...model,
- label: compactModelLabel(model.label),
- reasoningOptions: model.reasoningOptions.map((option) => ({ ...option }))
- })),
- reasoningOrder: orderedReasoning(selectableModels),
- message: ""
- };
+ return await probing(localSignal, async () => {
+ const main = await openMainMenu(localSignal, trigger);
+ const modelRow = sectionRow(main, "model");
+ const reasoningRow = sectionRow(main, "reasoning");
+ const speedRow = sectionRow(main, "speed");
+ if (!modelRow || !reasoningRow) throw new Error("\u5B98\u65B9\u6A21\u578B\u83DC\u5355\u7ED3\u6784\u4E0D\u53EF\u7528");
+ const modelText = rowValue(modelRow, "model");
+ const reasoningText = rowValue(reasoningRow, "reasoning");
+ const speedText = speedRow ? rowValue(speedRow, "speed") : "";
+ const modelMenu = await openSubmenu(modelRow, main, localSignal);
+ const visibleLabels = menuItems(modelMenu).map(itemLabel).filter(Boolean);
+ const visibleModels = capability.models.filter(
+ (model) => visibleLabels.some((label) => matchModel([model], label))
+ );
+ const currentModel = matchModel(visibleModels, modelText);
+ if (!visibleModels.length || !currentModel)
+ throw new Error("\u5B98\u65B9\u6A21\u578B\u8EAB\u4EFD\u65E0\u6CD5\u7CBE\u786E\u5339\u914D");
+ const selectableModels = visibleModels.filter(
+ (model) => !isAutoReviewModel(model)
+ );
+ const triggerReasoning = trigger?.getAttribute(
+ "data-selected-reasoning-effort"
+ );
+ const observedReasoningKey = canonicalReasoning(
+ triggerReasoning ?? reasoningText
+ );
+ const triggerMatches = triggerReasoning == null ? [] : currentModel.reasoningOptions.filter(
+ (option) => option.key === observedReasoningKey
+ );
+ const unsupportedReasoning = currentModel.reasoningEvidence && REASONING_LABELS[observedReasoningKey] && !currentModel.reasoningEvidence.efforts.includes(observedReasoningKey) ? {
+ key: observedReasoningKey,
+ label: REASONING_LABELS[observedReasoningKey],
+ enabled: false,
+ unsupported: true
+ } : null;
+ const reasoning = triggerReasoning == null ? reasoningFromRow(reasoningText, currentModel) || unsupportedReasoning : triggerMatches.length === 1 ? triggerMatches[0] : unsupportedReasoning;
+ if (!reasoning) throw new Error("\u5B98\u65B9\u63A8\u7406\u5F3A\u5EA6\u65E0\u6CD5\u7CBE\u786E\u5339\u914D");
+ let fastEnabled = false;
+ if (speedRow) {
+ if (/^(?:fast|快速)(?:\s|$)/i.test(speedText)) fastEnabled = true;
+ else if (!/^(?:standard|标准)(?:\s|$)/i.test(speedText))
+ throw new Error("\u5B98\u65B9\u901F\u5EA6\u72B6\u6001\u4E0D\u53EF\u7528");
+ } else if (currentModel.fastSupported) {
+ throw new Error("\u5B98\u65B9\u901F\u5EA6\u83DC\u5355\u4E0D\u53EF\u7528");
+ }
+ return {
+ status: "ready",
+ generation: ++snapshotGeneration,
+ current: {
+ modelKey: currentModel.key,
+ modelLabel: compactModelLabel(currentModel.label),
+ reasoningKey: reasoning.key,
+ reasoningLabel: reasoning.label,
+ reasoningSupported: !reasoning.unsupported,
+ fastEnabled
+ },
+ models: selectableModels.map((model) => ({
+ ...model,
+ label: compactModelLabel(model.label),
+ reasoningOptions: model.reasoningOptions.map((option) => ({
+ ...option
+ }))
+ })),
+ reasoningOrder: orderedReasoning(selectableModels),
+ message: reasoning.unsupported ? currentModel.reasoningEvidence.unavailableReason : ""
+ };
+ });
} catch (error) {
if (error?.name === "AbortError") throw error;
return unavailableSnapshot("unavailable", String(error?.message || error));
- } finally {
- await closeProbe(localSignal).catch(() => {
- });
}
}
async function syncSnapshot(options = {}) {
- if (!boundTrigger?.isConnected) bindTriggers(findTriggers());
- const trigger = options.trigger || boundTrigger;
- const result = await queuedProbe(() => readOfficialSnapshot(options.signal, trigger));
- if (!destroyed && trigger === boundTrigger) {
- snapshot = result;
+ const trigger = resolveTrigger(options.trigger);
+ const result = await queuedProbe(
+ () => readOfficialSnapshot(options.signal, trigger)
+ );
+ if (!destroyed && trigger && trigger === boundTrigger) {
if (result.status === "ready") {
- const currentModel = result.models.find((model) => model.key === result.current.modelKey);
- if (currentModel && isLegacyModel(currentModel)) {
- if (autoExpandedLegacyKey !== currentModel.key) {
- legacyExpanded = true;
- autoExpandedLegacyKey = currentModel.key;
+ snapshot = result;
+ const currentModel = result.models.find(
+ (model) => model.key === result.current.modelKey
+ );
+ if (currentModel && !isPinnedModelKey(currentModel.key)) {
+ if (autoExpandedOtherKey !== currentModel.key) {
+ otherModelsExpanded = true;
+ autoExpandedOtherKey = currentModel.key;
}
- } else if (autoExpandedLegacyKey) {
- legacyExpanded = false;
- autoExpandedLegacyKey = null;
+ } else if (autoExpandedOtherKey) {
+ otherModelsExpanded = false;
+ autoExpandedOtherKey = null;
}
+ } else if (snapshot.status !== "ready") {
+ snapshot = result;
}
- message = result.message;
+ if (options.publishMessage !== false) message = result.message;
render();
}
return result;
@@ -626,77 +1043,93 @@
throw new Error(failureMessage);
}
function openMainMenuForModel(model, localSignal, trigger) {
- return openMainMenuWhen(localSignal, trigger, (main) => {
- const row = sectionRow(main, "model");
- return row && matchModel([model], rowValue(row, "model"));
- }, "\u5B98\u65B9\u6A21\u578B\u5207\u6362\u672A\u751F\u6548");
+ return openMainMenuWhen(
+ localSignal,
+ trigger,
+ (main) => {
+ const row = sectionRow(main, "model");
+ return row && matchModel([model], rowValue(row, "model"));
+ },
+ "\u5B98\u65B9\u6A21\u578B\u5207\u6362\u672A\u751F\u6548"
+ );
}
async function selectCombination(target, localSignal, trigger) {
return queuedProbe(async () => {
- const model = capability.models.find((item) => item.key === target.modelKey);
+ const model = capability.models.find(
+ (item) => item.key === target.modelKey
+ );
if (!model) throw new Error("\u76EE\u6807\u6A21\u578B\u4E0D\u53EF\u7528");
- let main = null;
- try {
- main = await openMainMenu(localSignal, trigger);
+ await probing(localSignal, async () => {
+ const main = await openMainMenu(localSignal, trigger);
const modelRow = sectionRow(main, "model");
const modelMenu = await openSubmenu(modelRow, main, localSignal);
- const modelItem = menuItems(modelMenu).find((item) => matchModel([model], itemLabel(item)));
+ const modelItem = menuItems(modelMenu).find(
+ (item) => matchModel([model], itemLabel(item))
+ );
if (!modelItem) throw new Error("\u5B98\u65B9\u6A21\u578B\u9009\u9879\u4E0D\u53EF\u7528");
checkAbort(localSignal);
dispatchPointerActivation(modelItem);
- } finally {
- await closeProbe(localSignal).catch(() => {
- });
- }
+ });
checkAbort(localSignal);
- try {
- main = await openMainMenuForModel(model, localSignal, trigger);
+ await probing(localSignal, async () => {
+ const main = await openMainMenuForModel(model, localSignal, trigger);
const row = sectionRow(main, "reasoning");
const menu = await openSubmenu(row, main, localSignal);
- const index = model.reasoningOptions.findIndex((item2) => item2.key === target.reasoningKey);
const items = menuItems(menu);
- if (items.length !== model.reasoningOptions.length) throw new Error("\u5B98\u65B9\u63A8\u7406\u83DC\u5355\u4E0E\u6A21\u578B\u80FD\u529B\u4E0D\u4E00\u81F4");
- const item = items[index];
+ const index = model.reasoningOptions.findIndex(
+ (item2) => item2.key === target.reasoningKey
+ );
+ let item = null;
+ if (model.reasoningEvidence) {
+ const targetOption = model.reasoningOptions[index];
+ item = targetOption ? items.find((entry) => {
+ const label = itemLabel(entry);
+ return canonicalReasoning(label) === targetOption.key || label.toLowerCase() === targetOption.label.toLowerCase();
+ }) : null;
+ } else {
+ if (items.length !== model.reasoningOptions.length)
+ throw new Error("\u5B98\u65B9\u63A8\u7406\u83DC\u5355\u4E0E\u6A21\u578B\u80FD\u529B\u4E0D\u4E00\u81F4");
+ item = items[index];
+ }
if (index < 0 || !item) throw new Error("\u5B98\u65B9\u63A8\u7406\u9009\u9879\u4E0D\u53EF\u7528");
checkAbort(localSignal);
dispatchPointerActivation(item);
await waitFor(
- () => canonicalReasoning(trigger?.getAttribute("data-selected-reasoning-effort")) === target.reasoningKey,
+ () => canonicalReasoning(
+ trigger?.getAttribute("data-selected-reasoning-effort")
+ ) === target.reasoningKey,
localSignal
);
- } finally {
- await closeProbe(localSignal).catch(() => {
- });
- }
+ });
});
}
async function selectFast(enabled, localSignal, trigger) {
return queuedProbe(async () => {
- let main = null;
const pattern = enabled ? /^(?:fast|快速)(?:\s|$)/i : /^(?:standard|标准)(?:\s|$)/i;
- try {
- main = await openMainMenu(localSignal, trigger);
+ await probing(localSignal, async () => {
+ const main = await openMainMenu(localSignal, trigger);
const row = sectionRow(main, "speed");
if (!row) throw new Error("\u5F53\u524D\u6A21\u578B\u4E0D\u652F\u6301 Fast");
const menu = await openSubmenu(row, main, localSignal);
- const item = menuItems(menu).find((entry) => pattern.test(itemLabel(entry)));
+ const item = menuItems(menu).find(
+ (entry) => pattern.test(itemLabel(entry))
+ );
if (!item) throw new Error("\u5B98\u65B9\u901F\u5EA6\u9009\u9879\u4E0D\u53EF\u7528");
checkAbort(localSignal);
dispatchPointerActivation(item);
- } finally {
- await closeProbe(localSignal).catch(() => {
- });
- }
+ });
checkAbort(localSignal);
- try {
- await openMainMenuWhen(localSignal, trigger, (rootElement) => {
- const row = sectionRow(rootElement, "speed");
- return row && pattern.test(rowValue(row, "speed"));
- }, "\u5B98\u65B9\u901F\u5EA6\u5207\u6362\u672A\u751F\u6548");
- } finally {
- await closeProbe(localSignal).catch(() => {
- });
- }
+ await probing(localSignal, async () => {
+ await openMainMenuWhen(
+ localSignal,
+ trigger,
+ (rootElement) => {
+ const row = sectionRow(rootElement, "speed");
+ return row && pattern.test(rowValue(row, "speed"));
+ },
+ "\u5B98\u65B9\u901F\u5EA6\u5207\u6362\u672A\u751F\u6548"
+ );
+ });
});
}
function configValid(target, source = snapshot) {
@@ -707,7 +1140,9 @@
);
}
function sameConfig(a, b) {
- return Boolean(a && b && a.modelKey === b.modelKey && a.reasoningKey === b.reasoningKey && a.fastEnabled === b.fastEnabled);
+ return Boolean(
+ a && b && a.modelKey === b.modelKey && a.reasoningKey === b.reasoningKey && a.fastEnabled === b.fastEnabled
+ );
}
function submitTarget(target) {
if (!configValid(target)) {
@@ -716,20 +1151,25 @@
return;
}
const operationId = ++operationSequence;
- const targetModel = snapshot.models.find((model) => model.key === target.modelKey);
- if (targetModel && isLegacyModel(targetModel)) legacyExpanded = true;
+ if (!isPinnedModelKey(target.modelKey)) otherModelsExpanded = true;
pendingIntent = { operationId, target: { ...target }, trigger: boundTrigger };
pendingTarget = { ...target };
activeOperation?.abort();
void pumpIntents();
}
async function applyIntent(intent, localSignal) {
- let current = await syncSnapshot({ signal: localSignal, trigger: intent.trigger });
+ let current = await syncSnapshot({
+ signal: localSignal,
+ trigger: intent.trigger
+ });
if (!configValid(intent.target, current)) throw new Error("\u76EE\u6807\u914D\u7F6E\u5DF2\u4E0D\u53EF\u7528");
if (sameConfig(current.current, intent.target)) return current;
if (current.current.modelKey !== intent.target.modelKey || current.current.reasoningKey !== intent.target.reasoningKey) {
await selectCombination(intent.target, localSignal, intent.trigger);
- current = await syncSnapshot({ signal: localSignal, trigger: intent.trigger });
+ current = await syncSnapshot({
+ signal: localSignal,
+ trigger: intent.trigger
+ });
if (current.current?.modelKey !== intent.target.modelKey || current.current?.reasoningKey !== intent.target.reasoningKey) {
throw new Error("\u6A21\u578B\u6216\u63A8\u7406\u5F3A\u5EA6\u672A\u751F\u6548");
}
@@ -737,9 +1177,13 @@
checkAbort(localSignal);
if (current.current.fastEnabled !== intent.target.fastEnabled) {
await selectFast(intent.target.fastEnabled, localSignal, intent.trigger);
- current = await syncSnapshot({ signal: localSignal, trigger: intent.trigger });
+ current = await syncSnapshot({
+ signal: localSignal,
+ trigger: intent.trigger
+ });
}
- if (!sameConfig(current.current, intent.target)) throw new Error("\u5B98\u65B9\u914D\u7F6E\u6821\u9A8C\u5931\u8D25");
+ if (!sameConfig(current.current, intent.target))
+ throw new Error("\u5B98\u65B9\u914D\u7F6E\u6821\u9A8C\u5931\u8D25");
return current;
}
async function pumpIntents() {
@@ -761,7 +1205,10 @@
} catch (error) {
if (error?.name !== "AbortError" && intent.operationId === operationSequence) {
const failureMessage = String(error?.message || error);
- await syncSnapshot({ trigger: intent.trigger }).catch(() => {
+ await syncSnapshot({
+ trigger: intent.trigger,
+ publishMessage: false
+ }).catch(() => {
});
message = failureMessage;
render();
@@ -792,70 +1239,177 @@
interaction = "idle";
}
function escapeHtml(value) {
- return String(value ?? "").replace(/[&<>"']/g, (char) => ({
- "&": "&",
- "<": "<",
- ">": ">",
- '"': """,
- "'": "'"
- })[char]);
+ return String(value ?? "").replace(/[&<>"']/g, (char) => HTML_ESCAPES[char]);
}
function icon(name, size = 16) {
- const paths = {
- zap: '',
- plus: '',
- chevron: '',
- pencil: '',
- trash: ''
- };
- return ``;
+ return ``;
}
function presetValidity(preset) {
- if (snapshot.status !== "ready") return { valid: false, reason: "\u5B98\u65B9\u6A21\u578B\u6570\u636E\u5C1A\u672A\u5C31\u7EEA" };
+ if (snapshot.status !== "ready")
+ return { valid: false, reason: "\u5B98\u65B9\u6A21\u578B\u6570\u636E\u5C1A\u672A\u5C31\u7EEA" };
const model = snapshot.models.find((item) => item.key === preset.modelKey);
if (!model) return { valid: false, reason: "\u6A21\u578B\u4E0D\u53EF\u7528" };
- if (!model.reasoningOptions.some((item) => item.key === preset.reasoningKey)) return { valid: false, reason: "\u63A8\u7406\u5F3A\u5EA6\u4E0D\u53EF\u7528" };
- if (preset.fastEnabled && !model.fastSupported) return { valid: false, reason: "Fast \u4E0D\u53EF\u7528" };
+ if (!model.reasoningOptions.some((item) => item.key === preset.reasoningKey))
+ return { valid: false, reason: "\u63A8\u7406\u5F3A\u5EA6\u4E0D\u53EF\u7528" };
+ if (preset.fastEnabled && !model.fastSupported)
+ return { valid: false, reason: "Fast \u4E0D\u53EF\u7528" };
return { valid: true, reason: "" };
}
- function matrixHtml() {
- if (snapshot.status !== "ready") return `
${escapeHtml(snapshot.message || LOADING_MESSAGE)}
`;
- const primaryModels = snapshot.models.filter((model2) => !isLegacyModel(model2));
- const legacyModels = snapshot.models.filter(isLegacyModel);
- const effective = pendingTarget || snapshot.current;
- const revealLegacy = legacyExpanded;
- const displayedModels = revealLegacy ? [...primaryModels, ...legacyModels] : primaryModels;
+ function partitionModels(models = snapshot.models) {
+ const byKey = new Map(models.map((model) => [model.key, model]));
+ const pinnedModels = store.pinnedModelKeys.map((key) => byKey.get(key)).filter(Boolean);
+ const pinnedKeys = new Set(pinnedModels.map((model) => model.key));
+ const otherModels = models.filter((model) => !pinnedKeys.has(model.key));
+ return { pinnedModels, otherModels };
+ }
+ function togglePinnedModel(modelKey) {
+ const wasPinned = isPinnedModelKey(modelKey);
+ if (wasPinned) {
+ otherModelsExpanded = true;
+ if (snapshot.current?.modelKey === modelKey || pendingTarget?.modelKey === modelKey) {
+ autoExpandedOtherKey = modelKey;
+ }
+ } else if (autoExpandedOtherKey === modelKey) {
+ otherModelsExpanded = false;
+ autoExpandedOtherKey = null;
+ }
+ updateStore((next) => {
+ next.pinnedModelKeys = wasPinned ? next.pinnedModelKeys.filter((key) => key !== modelKey) : [...next.pinnedModelKeys, modelKey];
+ });
+ const replacement = [...root?.querySelectorAll(".qmp-pin") || []].find(
+ (button) => button.dataset.modelKey === modelKey
+ );
+ replacement?.focus({ preventScroll: true });
+ }
+ function matrixLayout() {
+ const { pinnedModels, otherModels } = partitionModels();
+ const revealOthers = otherModelsExpanded || pinnedModels.length === 0;
+ const displayedModels = revealOthers ? [...pinnedModels, ...otherModels] : pinnedModels;
const columns = orderedReasoning(displayedModels).map((key) => {
- const option = displayedModels.flatMap((model2) => model2.reasoningOptions).find((item) => item.key === key);
+ const option = displayedModels.flatMap((model) => model.reasoningOptions).find((item) => item.key === key);
return { key, label: option?.label || key };
});
- const model = snapshot.models.find((item) => item.key === snapshot.current.modelKey);
+ return {
+ pinnedModels,
+ otherModels,
+ revealOthers,
+ displayedModels,
+ columns,
+ gridWidth: modelColumnWidth + columns.length * REASONING_COLUMN_WIDTH
+ };
+ }
+ function preferredPanelWidth() {
+ if (snapshot.status !== "ready") return PANEL_MIN_WIDTH;
+ return matrixLayout().gridWidth + 2;
+ }
+ function applyModelColumnWidth(value) {
+ modelColumnWidth = normalizeModelColumnWidth(value);
+ const grid = root?.querySelector(".qmp-grid");
+ const handle = root?.querySelector(".qmp-model-resizer");
+ if (grid) {
+ setCustomPixelStyle(grid, "--qmp-model-column", modelColumnWidth);
+ setPixelStyle(grid, "width", matrixLayout().gridWidth);
+ }
+ handle?.setAttribute("aria-valuenow", String(modelColumnWidth));
+ if (visible) positionPanel();
+ }
+ function persistModelColumnWidth() {
+ updateStore(
+ (next) => {
+ next.modelColumnWidth = modelColumnWidth;
+ },
+ { render: false }
+ );
+ }
+ function startModelColumnResize(event, handle) {
+ if (event.button !== 0 || modelColumnResize) return;
+ event.preventDefault();
+ clearLater(closeTimer);
+ finishDrag({ cancelled: true });
+ hidePreview(true);
+ closeActionMenu(false, false, true);
+ const rect = root.getBoundingClientRect();
+ panelLeftLock = rect.left;
+ modelColumnResize = {
+ pointerId: event.pointerId,
+ startX: event.clientX,
+ startWidth: modelColumnWidth,
+ handle
+ };
+ holdOpen = true;
+ interaction = "resizing-model-column";
+ root.dataset.resizingModelColumn = "true";
+ handle.setPointerCapture?.(event.pointerId);
+ }
+ function moveModelColumnResize(event) {
+ if (!modelColumnResize || event.pointerId !== modelColumnResize.pointerId)
+ return;
+ event.preventDefault();
+ applyModelColumnWidth(
+ modelColumnResize.startWidth + event.clientX - modelColumnResize.startX
+ );
+ }
+ function finishModelColumnResize({ cancelled = false, schedule = true } = {}) {
+ if (!modelColumnResize) return;
+ const resize = modelColumnResize;
+ modelColumnResize = null;
+ if (cancelled) applyModelColumnWidth(resize.startWidth);
+ else if (modelColumnWidth !== resize.startWidth) persistModelColumnWidth();
+ try {
+ if (resize.handle.hasPointerCapture?.(resize.pointerId)) {
+ resize.handle.releasePointerCapture(resize.pointerId);
+ }
+ } catch {
+ }
+ root?.removeAttribute("data-resizing-model-column");
+ holdOpen = false;
+ interaction = activeOperation ? "applying" : "idle";
+ if (schedule) scheduleClose();
+ }
+ function setModelColumnWidth(value) {
+ applyModelColumnWidth(value);
+ persistModelColumnWidth();
+ }
+ function matrixHtml() {
+ if (snapshot.status !== "ready")
+ return `${escapeHtml(snapshot.message || LOADING_MESSAGE)}
`;
+ const { pinnedModels, otherModels, revealOthers, columns, gridWidth } = matrixLayout();
+ const effective = pendingTarget || snapshot.current;
+ const model = snapshot.models.find((item) => item.key === effective.modelKey);
const fastDisabled = !model?.fastSupported;
- const header = `${columns.map((item) => `${escapeHtml(item.label)}
`).join("")}`;
- const rows = (models) => models.map((row) => {
- const options = new Map(row.reasoningOptions.map((item) => [item.key, item]));
+ const header = `${columns.map((item) => `${escapeHtml(item.label)}
`).join("")}`;
+ const rows = (models, pinned) => models.map((row) => {
+ const options = new Map(
+ row.reasoningOptions.map((item) => [item.key, item])
+ );
+ const currentModel = snapshot.current.modelKey === row.key;
+ const pendingModel = pendingTarget?.modelKey === row.key;
const cells = columns.map((column) => {
const enabled = options.has(column.key);
const selected = snapshot.current.modelKey === row.key && snapshot.current.reasoningKey === column.key;
+ const unsupportedCurrent = selected && !enabled;
const pending = pendingTarget?.modelKey === row.key && pendingTarget?.reasoningKey === column.key;
- return ``;
+ const unavailableReason = !enabled && row.reasoningEvidence ? row.reasoningEvidence.unavailableReason : "";
+ return ``;
}).join("");
- return `${escapeHtml(row.label)}
${cells}`;
+ const pinLabel = `${pinned ? "\u53D6\u6D88\u7F6E\u9876" : "\u4E00\u952E\u7F6E\u9876"} ${row.label}`;
+ return `${escapeHtml(row.label)}
${cells}`;
}).join("");
- const disclosure = legacyModels.length ? `` : "";
- const legacyRegion = `${revealLegacy ? rows(legacyModels) : ""}
`;
- const width = 104 + columns.length * 64;
- return ``;
+ const disclosure = otherModels.length ? `` : "";
+ const pinnedRegion = `${rows(pinnedModels, true)}
`;
+ const otherRegion = `${revealOthers ? rows(otherModels, false) : ""}
`;
+ return ``;
}
function presetHtml(preset) {
const validity = presetValidity(preset);
const active = sameConfig(snapshot.current, preset);
const pending = sameConfig(pendingTarget, preset);
const invalid = snapshot.status === "ready" && !validity.valid;
- return ``;
+ return ``;
}
function render() {
if (!root || destroyed) return;
+ syncFastFeedbackPresentation();
if (renameId && root.querySelector(".qmp-rename")) {
hidePreview(true);
if (visible) positionPanel();
@@ -865,39 +1419,98 @@
const focusedPresetId = root.contains(active) ? active.closest?.(".qmp-preset")?.dataset.presetId || null : null;
const focusedAdd = root.contains(active) && Boolean(active.closest?.(".qmp-add"));
hidePreview(true);
- if (actionMenu) closeActionMenu(false);
+ if (actionMenu) closeActionMenu(false, false, true);
const cancelledDrag = Boolean(drag);
+ const cancelledColumnResize = Boolean(modelColumnResize);
+ if (modelColumnResize)
+ finishModelColumnResize({ cancelled: true, schedule: false });
if (drag) {
drag.wrapper.classList.remove("is-dragging");
drag = null;
holdOpen = false;
- if (interaction === "dragging") interaction = activeOperation ? "applying" : "idle";
+ if (interaction === "dragging")
+ interaction = activeOperation ? "applying" : "idle";
}
const canSave = snapshot.status === "ready" && configValid(snapshot.current);
- root.innerHTML = `${store.presets.map(presetHtml).join("")}
${message && snapshot.status === "ready" ? `${escapeHtml(message)}
` : ""}${matrixHtml()}`;
+ root.innerHTML = `${store.presets.map(presetHtml).join("")}
${message && snapshot.status === "ready" ? `${escapeHtml(message)}
` : ""}${matrixHtml()}`;
if (focusedPresetId || focusedAdd) {
- const wrapper = focusedPresetId ? [...root.querySelectorAll(".qmp-preset")].find((item) => item.dataset.presetId === focusedPresetId) : null;
+ const wrapper = focusedPresetId ? [...root.querySelectorAll(".qmp-preset")].find(
+ (item) => item.dataset.presetId === focusedPresetId
+ ) : null;
const replacement = wrapper?.querySelector(".qmp-preset-apply") || root.querySelector(".qmp-add");
replacement?.focus({ preventScroll: true });
}
if (visible) positionPanel();
- if (cancelledDrag) scheduleClose();
+ if (cancelledDrag || cancelledColumnResize) scheduleClose();
}
function positionPanel() {
- if (!root || !boundTrigger || root.hidden) return;
+ if (!root || !boundTrigger || root.hidden || !visible) return;
const anchor = boundTrigger.getBoundingClientRect();
- const preferredWidth = snapshot.status === "ready" ? 560 : 360;
- const width = Math.min(preferredWidth, window.innerWidth - 24);
- root.style.width = `${Math.max(320, width)}px`;
+ const viewportWidth = Math.max(
+ 0,
+ window.innerWidth || documentRef.documentElement?.clientWidth || 0
+ );
+ const viewportHeight = Math.max(
+ 0,
+ window.innerHeight || documentRef.documentElement?.clientHeight || 0
+ );
+ const availableWidth = Math.max(0, viewportWidth - PANEL_SAFE_MARGIN * 2);
+ if (!availableWidth || !viewportHeight) return;
+ const maximumWidth = Math.min(PANEL_MAX_WIDTH, availableWidth);
+ const minimumWidth = Math.min(PANEL_MIN_WIDTH, maximumWidth);
+ const width = clamp(preferredPanelWidth(), minimumWidth, maximumWidth);
+ setPixelStyle(root, "width", width);
+ const measured = root.getBoundingClientRect();
+ const naturalHeight = Math.max(measured.height, root.scrollHeight || 0);
+ const aboveSpace = Math.max(0, anchor.top - PANEL_GAP - PANEL_SAFE_MARGIN);
+ const belowSpace = Math.max(
+ 0,
+ viewportHeight - anchor.bottom - PANEL_GAP - PANEL_SAFE_MARGIN
+ );
+ const placement = aboveSpace >= naturalHeight || aboveSpace >= belowSpace ? "top" : "bottom";
+ const panelSpace = Math.max(
+ 96,
+ placement === "top" ? aboveSpace : belowSpace
+ );
+ setCustomPixelStyle(
+ root,
+ "--qmp-panel-max-height",
+ Math.min(viewportHeight - PANEL_SAFE_MARGIN * 2, panelSpace)
+ );
const rect = root.getBoundingClientRect();
- const left = Math.min(window.innerWidth - rect.width - 12, Math.max(12, anchor.left + anchor.width / 2 - rect.width / 2));
- const top = Math.max(12, anchor.top - rect.height - 8);
- root.style.left = `${left}px`;
- root.style.top = `${top}px`;
+ const maximumLeft = Math.max(
+ PANEL_SAFE_MARGIN,
+ viewportWidth - rect.width - PANEL_SAFE_MARGIN
+ );
+ const centeredLeft = anchor.left + anchor.width / 2 - rect.width / 2;
+ const left = clamp(
+ panelLeftLock ?? centeredLeft,
+ PANEL_SAFE_MARGIN,
+ maximumLeft
+ );
+ const preferredTop = placement === "top" ? anchor.top - PANEL_GAP - rect.height : anchor.bottom + PANEL_GAP;
+ const maximumTop = Math.max(
+ PANEL_SAFE_MARGIN,
+ viewportHeight - rect.height - PANEL_SAFE_MARGIN
+ );
+ const top = clamp(preferredTop, PANEL_SAFE_MARGIN, maximumTop);
+ const originX = clamp(
+ anchor.left + anchor.width / 2 - left,
+ 18,
+ Math.max(18, rect.width - 18)
+ );
+ if (root.dataset.placement !== placement) root.dataset.placement = placement;
+ setCustomPixelStyle(root, "--qmp-origin-x", originX);
+ root.style.setProperty("--qmp-origin-y", placement === "top" ? "100%" : "0%");
+ root.style.setProperty("--qmp-enter-y", placement === "top" ? "4px" : "-4px");
+ setPixelStyle(root, "left", left);
+ setPixelStyle(root, "top", top);
}
function showPreview(presetId, anchor) {
clearLater(previewTimer);
clearLater(previewHideTimer);
+ previewTimer = null;
+ previewHideTimer = null;
const show = () => {
const preset = store.presets.find((item) => item.id === presetId);
if (!preset || !visible) return;
@@ -907,45 +1520,115 @@
preview.id = PREVIEW_ID;
preview.setAttribute("role", "tooltip");
preview.setAttribute("data-codexpp-qmp-preview", "");
- preview.innerHTML = `${escapeHtml(preset.name)}
${escapeHtml(validity.valid ? `${preset.modelLabel} \xB7 ${preset.reasoningLabel} \xB7 ${preset.fastEnabled ? "Fast" : "Standard"}` : validity.reason)}`;
+ preview.setAttribute("aria-hidden", "true");
+ preview.dataset.state = "closed";
+ preview.innerHTML = `${escapeHtml(preset.name)}
${escapeHtml(validity.valid ? `${preset.modelLabel} \xB7 ${preset.reasoningLabel} \xB7 ${preset.fastEnabled ? "Fast" : "Standard"}` : validity.reason)}
`;
documentRef.body.append(preview);
+ syncHostTypography();
const apply = anchor.matches?.(".qmp-preset-apply") ? anchor : anchor.querySelector?.(".qmp-preset-apply");
apply?.setAttribute("aria-describedby", PREVIEW_ID);
const anchorRect = anchor.getBoundingClientRect();
const rect = preview.getBoundingClientRect();
- preview.style.left = `${Math.min(window.innerWidth - rect.width - 8, Math.max(8, anchorRect.left + anchorRect.width / 2 - rect.width / 2))}px`;
+ const maximumLeft = Math.max(8, window.innerWidth - rect.width - 8);
+ const left = clamp(
+ anchorRect.left + anchorRect.width / 2 - rect.width / 2,
+ 8,
+ maximumLeft
+ );
+ const originX = clamp(
+ anchorRect.left + anchorRect.width / 2 - left,
+ 12,
+ Math.max(12, rect.width - 12)
+ );
+ setPixelStyle(preview, "left", left);
const above = anchorRect.top - rect.height - 8;
- const below = Math.min(window.innerHeight - rect.height - 8, anchorRect.bottom + 8);
- preview.style.top = `${Math.max(8, above >= 8 ? above : below)}px`;
+ const below = Math.min(
+ window.innerHeight - rect.height - 8,
+ anchorRect.bottom + 8
+ );
+ const placement = above >= 8 ? "top" : "bottom";
+ preview.dataset.placement = placement;
+ setCustomPixelStyle(preview, "--qmp-origin-x", originX);
+ preview.style.setProperty(
+ "--qmp-origin-y",
+ placement === "top" ? "100%" : "0%"
+ );
+ preview.style.setProperty(
+ "--qmp-float-y",
+ placement === "top" ? "3px" : "-3px"
+ );
+ setPixelStyle(
+ preview,
+ "top",
+ Math.max(8, placement === "top" ? above : below)
+ );
+ if (!reducedMotion()) preview.getBoundingClientRect();
+ preview.dataset.state = "open";
+ preview.setAttribute("aria-hidden", "false");
};
previewTimer = preview ? later(show, 0) : later(show, 120);
}
- function clearPreview() {
+ function clearPreview(target = preview) {
root?.querySelectorAll(`[aria-describedby="${PREVIEW_ID}"]`).forEach((element) => {
element.removeAttribute("aria-describedby");
});
- preview?.remove();
- preview = null;
+ target?.remove();
+ if (preview === target) preview = null;
}
function hidePreview(immediate = false) {
clearLater(previewTimer);
clearLater(previewHideTimer);
- if (immediate) clearPreview();
- else previewHideTimer = later(clearPreview, 80);
- }
- function closeActionMenu(shouldSchedule = true, restoreFocus = false) {
+ previewTimer = null;
+ previewHideTimer = null;
+ const target = preview;
+ if (!target) return;
+ root?.querySelectorAll(`[aria-describedby="${PREVIEW_ID}"]`).forEach((element) => {
+ element.removeAttribute("aria-describedby");
+ });
+ if (immediate || reducedMotion()) {
+ clearPreview(target);
+ return;
+ }
+ target.dataset.state = "closed";
+ target.setAttribute("aria-hidden", "true");
+ previewHideTimer = later(() => {
+ previewHideTimer = null;
+ clearPreview(target);
+ }, FLOATING_CLOSE_DURATION);
+ }
+ function closeActionMenu(shouldSchedule = true, restoreFocus = false, immediate = false) {
+ clearLater(actionMenuHideTimer);
+ actionMenuHideTimer = null;
+ const menu = actionMenu;
const anchor = actionMenuAnchor;
- actionMenu?.remove();
- actionMenu = null;
- actionMenuAnchor = null;
- if (!renameId && !drag) holdOpen = false;
- if (shouldSchedule && visible && !holdOpen) scheduleClose();
- if (restoreFocus && anchor?.isConnected) anchor.focus({ preventScroll: true });
+ if (restoreFocus && anchor?.isConnected)
+ anchor.focus({ preventScroll: true });
+ if (!menu) return;
+ const finish = () => {
+ menu.remove();
+ if (actionMenu !== menu) return;
+ actionMenu = null;
+ actionMenuAnchor = null;
+ if (!renameId && !drag) holdOpen = false;
+ if (shouldSchedule && visible && !holdOpen) scheduleClose();
+ };
+ if (immediate || reducedMotion()) {
+ finish();
+ return;
+ }
+ menu.dataset.state = "closed";
+ menu.setAttribute("aria-hidden", "true");
+ actionMenuHideTimer = later(() => {
+ actionMenuHideTimer = null;
+ finish();
+ }, FLOATING_CLOSE_DURATION);
}
function beginRename(id) {
- closeActionMenu(false);
+ closeActionMenu(false, false, true);
hidePreview(true);
- const wrapper = [...root.querySelectorAll(".qmp-preset")].find((item) => item.dataset.presetId === id);
+ const wrapper = [...root.querySelectorAll(".qmp-preset")].find(
+ (item) => item.dataset.presetId === id
+ );
const preset = store.presets.find((item) => item.id === id);
if (!wrapper || !preset) return;
renameId = id;
@@ -965,13 +1648,14 @@
const name = input.value.trim();
renameId = null;
holdOpen = false;
- if (commit && name) updateStore((next) => {
- const item = next.presets.find((entry) => entry.id === id);
- if (item) {
- item.name = name;
- item.updatedAt = Date.now();
- }
- });
+ if (commit && name)
+ updateStore((next) => {
+ const item = next.presets.find((entry) => entry.id === id);
+ if (item) {
+ item.name = name;
+ item.updatedAt = Date.now();
+ }
+ });
else render();
scheduleClose();
};
@@ -986,11 +1670,15 @@
finish(false);
}
});
- input.addEventListener("blur", () => later(() => finish(true), 0), { once: true });
+ input.addEventListener("blur", () => later(() => finish(true), 0), {
+ once: true
+ });
}
function focusPreset(id) {
later(() => {
- const wrapper = id ? [...root?.querySelectorAll(".qmp-preset") || []].find((item) => item.dataset.presetId === id) : null;
+ const wrapper = id ? [...root?.querySelectorAll(".qmp-preset") || []].find(
+ (item) => item.dataset.presetId === id
+ ) : null;
const target = wrapper?.querySelector(".qmp-preset-apply") || root?.querySelector(".qmp-add");
target?.focus({ preventScroll: true });
}, 0);
@@ -999,7 +1687,7 @@
finishRename?.(true);
anchor = [...root.querySelectorAll(".qmp-preset")].find((item) => item.dataset.presetId === id)?.querySelector(".qmp-preset-apply");
if (!anchor) return;
- closeActionMenu(false);
+ closeActionMenu(false, false, true);
hidePreview(true);
clearLater(closeTimer);
holdOpen = true;
@@ -1007,8 +1695,11 @@
actionMenu = documentRef.createElement("div");
actionMenu.setAttribute("data-codexpp-qmp-actions", "");
actionMenu.setAttribute("role", "menu");
+ actionMenu.setAttribute("aria-hidden", "true");
+ actionMenu.dataset.state = "closed";
actionMenu.innerHTML = ``;
documentRef.body.append(actionMenu);
+ syncHostTypography();
const rect = anchor.getBoundingClientRect();
const menuRect = actionMenu.getBoundingClientRect();
const preferredLeft = point ? point.x : rect.left;
@@ -1016,15 +1707,37 @@
const fallbackTop = (point ? point.y : rect.top) - menuRect.height - 4;
const maxLeft = Math.max(8, window.innerWidth - menuRect.width - 8);
const maxTop = Math.max(8, window.innerHeight - menuRect.height - 8);
- actionMenu.style.left = `${Math.min(maxLeft, Math.max(8, preferredLeft))}px`;
- actionMenu.style.top = `${preferredTop <= maxTop ? preferredTop : Math.max(8, fallbackTop)}px`;
- actionMenu.addEventListener("pointerdown", (event) => event.stopPropagation());
+ const left = Math.min(maxLeft, Math.max(8, preferredLeft));
+ const placement = preferredTop <= maxTop ? "bottom" : "top";
+ const top = placement === "bottom" ? preferredTop : Math.max(8, fallbackTop);
+ const anchorX = point ? point.x : rect.left + Math.min(rect.width / 2, 18);
+ const originX = clamp(anchorX - left, 12, Math.max(12, menuRect.width - 12));
+ actionMenu.dataset.placement = placement;
+ setCustomPixelStyle(actionMenu, "--qmp-origin-x", originX);
+ actionMenu.style.setProperty(
+ "--qmp-origin-y",
+ placement === "top" ? "100%" : "0%"
+ );
+ actionMenu.style.setProperty(
+ "--qmp-float-y",
+ placement === "top" ? "3px" : "-3px"
+ );
+ setPixelStyle(actionMenu, "left", left);
+ setPixelStyle(actionMenu, "top", top);
+ if (!reducedMotion()) actionMenu.getBoundingClientRect();
+ actionMenu.dataset.state = "open";
+ actionMenu.setAttribute("aria-hidden", "false");
+ actionMenu.addEventListener(
+ "pointerdown",
+ (event) => event.stopPropagation()
+ );
actionMenu.addEventListener("keydown", (event) => {
const items = [...actionMenu.querySelectorAll('[role="menuitem"]')];
const current = Math.max(0, items.indexOf(documentRef.activeElement));
let next = null;
if (event.key === "ArrowDown") next = (current + 1) % items.length;
- if (event.key === "ArrowUp") next = (current - 1 + items.length) % items.length;
+ if (event.key === "ArrowUp")
+ next = (current - 1 + items.length) % items.length;
if (event.key === "Home") next = 0;
if (event.key === "End") next = items.length - 1;
if (next != null) {
@@ -1051,7 +1764,7 @@
if (action === "delete") {
const index = store.presets.findIndex((item) => item.id === id);
const focusId = store.presets[index + 1]?.id || store.presets[index - 1]?.id || null;
- closeActionMenu(false);
+ closeActionMenu(false, false, true);
updateStore((next) => {
next.presets = next.presets.filter((item) => item.id !== id);
});
@@ -1061,7 +1774,8 @@
actionMenu.querySelector('[role="menuitem"]')?.focus({ preventScroll: true });
}
function startDrag(event, apply) {
- if (event.button !== 0 || event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
+ if (event.button !== 0 || event.ctrlKey || event.metaKey || event.altKey || event.shiftKey)
+ return;
clearPresetClickSuppression();
const wrapper = apply.closest(".qmp-preset");
drag = {
@@ -1077,7 +1791,7 @@
function moveDrag(event) {
if (!drag) return;
if (!drag.active && Math.hypot(event.clientX - drag.x, event.clientY - drag.y) > 5) {
- closeActionMenu(false);
+ closeActionMenu(false, false, true);
hidePreview(true);
drag.active = true;
holdOpen = true;
@@ -1086,8 +1800,12 @@
}
if (!drag.active) return;
event.preventDefault();
- const siblings = [...root.querySelectorAll(".qmp-preset")].filter((item) => item !== drag.wrapper);
- const before = siblings.find((item) => event.clientX < item.getBoundingClientRect().left + item.offsetWidth / 2);
+ const siblings = [...root.querySelectorAll(".qmp-preset")].filter(
+ (item) => item !== drag.wrapper
+ );
+ const before = siblings.find(
+ (item) => event.clientX < item.getBoundingClientRect().left + item.offsetWidth / 2
+ );
const rail = root.querySelector(".qmp-presets");
rail.insertBefore(drag.wrapper, before || null);
const railRect = rail.getBoundingClientRect();
@@ -1104,7 +1822,8 @@
holdOpen = false;
interaction = "idle";
if (!cancelled && completed) armPresetClickSuppression(id, true, pointerId);
- else if (suppressReleasedClick) armPresetClickSuppression(id, false, pointerId);
+ else if (suppressReleasedClick)
+ armPresetClickSuppression(id, false, pointerId);
else clearPresetClickSuppression();
if (!completed) return;
if (cancelled) {
@@ -1112,7 +1831,9 @@
scheduleClose();
return;
}
- const order = [...root.querySelectorAll(".qmp-preset")].map((item) => item.dataset.presetId);
+ const order = [...root.querySelectorAll(".qmp-preset")].map(
+ (item) => item.dataset.presetId
+ );
updateStore((next) => {
const byId = new Map(next.presets.map((item) => [item.id, item]));
next.presets = order.map((item) => byId.get(item)).filter(Boolean);
@@ -1125,7 +1846,10 @@
updateStore((next) => {
const now = Date.now();
const name = nextPresetName(next.presets);
- next.nextSequence = Math.max(Number(next.nextSequence) || 1, Number(name) + 1);
+ next.nextSequence = Math.max(
+ Number(next.nextSequence) || 1,
+ Number(name) + 1
+ );
next.presets.push({
id: uuid(),
name,
@@ -1138,7 +1862,10 @@
updatedAt: now
});
});
- later(() => root?.querySelector(".qmp-preset:last-child")?.scrollIntoView({ block: "nearest", inline: "end" }), 0);
+ later(
+ () => root?.querySelector(".qmp-preset:last-child")?.scrollIntoView({ block: "nearest", inline: "end" }),
+ 0
+ );
} catch (error) {
message = `\u4FDD\u5B58\u5931\u8D25\uFF1A${String(error?.message || error)}`;
render();
@@ -1146,21 +1873,34 @@
}
function handleClick(event) {
const action = event.target.closest("[data-action]")?.dataset.action;
+ if (action === "pin-model") {
+ togglePinnedModel(event.target.closest(".qmp-pin").dataset.modelKey);
+ return;
+ }
if (action === "fast" && snapshot.status === "ready") {
const effective = pendingTarget || snapshot.current;
- submitTarget({ ...effective, fastEnabled: !effective.fastEnabled });
+ const nextFastEnabled = !effective.fastEnabled;
+ submitTarget({ ...effective, fastEnabled: nextFastEnabled });
+ startFastFeedback(nextFastEnabled);
+ return;
}
if (action === "save") void savePreset();
- if (action === "legacy") {
- legacyExpanded = event.target.closest(".qmp-legacy-toggle").getAttribute("aria-expanded") !== "true";
+ if (action === "others") {
+ otherModelsExpanded = event.target.closest(".qmp-other-toggle").getAttribute("aria-expanded") !== "true";
render();
- root?.querySelector(".qmp-legacy-toggle")?.focus({ preventScroll: true });
+ root?.querySelector(".qmp-other-toggle")?.focus({ preventScroll: true });
}
const cell = event.target.closest(".qmp-cell");
if (cell && !cell.disabled && snapshot.status === "ready") {
- const row = snapshot.models.find((model) => model.key === cell.dataset.model);
+ const row = snapshot.models.find(
+ (model) => model.key === cell.dataset.model
+ );
const fastEnabled = (pendingTarget || snapshot.current).fastEnabled && Boolean(row?.fastSupported);
- submitTarget({ modelKey: cell.dataset.model, reasoningKey: cell.dataset.reasoning, fastEnabled });
+ submitTarget({
+ modelKey: cell.dataset.model,
+ reasoningKey: cell.dataset.reasoning,
+ fastEnabled
+ });
}
const presetButton = event.target.closest(".qmp-preset-apply");
if (presetButton) {
@@ -1186,27 +1926,54 @@
}
function scheduleClose() {
clearLater(closeTimer);
- if (holdOpen || pointerOnTrigger || pointerOnPanel) return;
+ if (holdOpen || pointerOnTrigger || pointerOnPanel || focusOnPanel) return;
closeTimer = later(closePanel, 180);
}
- function showPanel() {
+ function showPanel(preferredTrigger) {
if (destroyed || disarmed || nativeMenuOpen()) return;
- if (!boundTrigger?.isConnected) bindTriggers(findTriggers());
- if (!boundTrigger || !root) return;
+ const trigger = resolveTrigger(preferredTrigger);
+ if (!trigger || !root) return;
+ boundTrigger = trigger;
+ clearLater(panelHideTimer);
+ panelHideTimer = null;
+ const wasHidden = root.hidden;
+ if (wasHidden) panelLeftLock = null;
visible = true;
root.hidden = false;
+ root.setAttribute("aria-hidden", "false");
+ if (wasHidden) root.dataset.state = "closed";
render();
positionPanel();
- void syncSnapshot({ trigger: boundTrigger });
+ if (wasHidden && !reducedMotion()) root.getBoundingClientRect();
+ root.dataset.state = "open";
+ void syncSnapshot({ trigger });
}
function closePanel() {
clearLater(openTimer);
clearLater(closeTimer);
+ clearLater(panelHideTimer);
+ panelHideTimer = null;
+ finishModelColumnResize({ cancelled: true, schedule: false });
+ panelLeftLock = null;
visible = false;
pointerOnPanel = false;
- if (root) root.hidden = true;
+ focusOnPanel = false;
hidePreview(true);
- closeActionMenu();
+ closeActionMenu(false, false, true);
+ if (!root || root.hidden) return;
+ root.setAttribute("aria-hidden", "true");
+ if (reducedMotion()) {
+ root.dataset.state = "closed";
+ root.hidden = true;
+ return;
+ }
+ root.dataset.state = "closing";
+ panelHideTimer = later(() => {
+ panelHideTimer = null;
+ if (!root || visible) return;
+ root.hidden = true;
+ root.dataset.state = "closed";
+ }, PANEL_CLOSE_DURATION);
}
function nativeTakeover() {
nativeSuspended = true;
@@ -1218,36 +1985,58 @@
const lifetime2 = new window.AbortController();
triggerLifetimes.set(next, lifetime2);
const local = lifetime2.signal;
- next.addEventListener("pointerenter", () => {
- const alreadyCurrent = boundTrigger === next && pointerOnTrigger;
- boundTrigger = next;
- pointerOnTrigger = true;
- clearLater(closeTimer);
- if (disarmed || alreadyCurrent) return;
- clearLater(openTimer);
- openTimer = later(showPanel, 300);
- }, { signal: local });
- next.addEventListener("pointerleave", () => {
- if (boundTrigger !== next) return;
- pointerOnTrigger = false;
- disarmed = false;
- clearLater(openTimer);
- scheduleClose();
- }, { signal: local });
- next.addEventListener("pointerdown", (event) => {
- boundTrigger = next;
- if (probe && !event.isTrusted) return;
- if (event.isTrusted && probe) yieldProbeToUser();
- else {
- disarmed = true;
- cancelIntents();
- closePanel();
- }
- }, { capture: true, signal: local });
+ next.addEventListener(
+ "pointerenter",
+ () => {
+ const alreadyCurrent = boundTrigger === next && pointerOnTrigger;
+ boundTrigger = next;
+ pointerOnTrigger = true;
+ clearLater(closeTimer);
+ if (disarmed || alreadyCurrent) return;
+ clearLater(openTimer);
+ openTimer = later(() => showPanel(next), 300);
+ },
+ { signal: local }
+ );
+ next.addEventListener(
+ "focus",
+ () => {
+ boundTrigger = next;
+ },
+ { signal: local }
+ );
+ next.addEventListener(
+ "pointerleave",
+ () => {
+ if (boundTrigger !== next) return;
+ pointerOnTrigger = false;
+ disarmed = false;
+ clearLater(openTimer);
+ scheduleClose();
+ },
+ { signal: local }
+ );
+ next.addEventListener(
+ "pointerdown",
+ (event) => {
+ boundTrigger = next;
+ if (probe && !event.isTrusted) return;
+ if (event.isTrusted && probe) yieldProbeToUser();
+ else {
+ disarmed = true;
+ cancelIntents();
+ closePanel();
+ }
+ },
+ { capture: true, signal: local }
+ );
}
function bindTriggers(nextTriggers) {
const next = new Set(nextTriggers);
- const intentTriggerMissing = [pendingIntent?.trigger, activeIntentTrigger].some((trigger) => trigger && !next.has(trigger));
+ const intentTriggerMissing = [
+ pendingIntent?.trigger,
+ activeIntentTrigger
+ ].some((trigger) => trigger && !next.has(trigger));
triggerLifetimes.forEach((lifetime2, trigger) => {
if (next.has(trigger)) return;
lifetime2.abort();
@@ -1265,12 +2054,14 @@
pointerOnTrigger = false;
closePanel();
}
- if (!boundTrigger && nextTriggers.length === 1) boundTrigger = nextTriggers[0];
+ if (!boundTrigger && nextTriggers.length === 1)
+ boundTrigger = nextTriggers[0];
}
function scheduleScan() {
if (scanFrame || destroyed) return;
const scan = () => {
scanFrame = 0;
+ syncHostTypography();
bindTriggers(findTriggers());
if (probe) claimProbeRoots();
if (visible && nativeMenuOpen()) nativeTakeover();
@@ -1283,98 +2074,212 @@
style.id = "codexpp-qmp-style";
style.textContent = `
[data-codexpp-qmp-probe-root="true"]{opacity:0!important;pointer-events:none!important}
-[data-codexpp-qmp-root]{position:fixed;z-index:2147483000;box-sizing:border-box;max-width:calc(100vw - 24px);max-height:calc(100vh - 24px);overflow:hidden;border:1px solid color-mix(in srgb,currentColor 13%,transparent);border-radius:7px;background:var(--color-background-elevated-primary-opaque,var(--main-surface-primary,#202020));color:var(--color-text-foreground,var(--text-primary,#f3f3f3));box-shadow:0 10px 28px rgb(0 0 0 / 28%);font:12.5px/1.35 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:0}
+:where([data-codexpp-qmp-root],[data-codexpp-qmp-preview],[data-codexpp-qmp-actions]){--qmp-surface:var(--color-background-elevated-primary-opaque,var(--color-token-dropdown-background,var(--main-surface-primary,#202020)));--qmp-surface-subtle:var(--color-background-elevated-primary,var(--main-surface-primary,var(--qmp-surface)));--qmp-text:var(--color-token-text-primary,var(--color-token-foreground,var(--color-text-foreground,var(--text-primary,#f3f3f3))));--qmp-muted:var(--color-token-text-tertiary,var(--color-token-description-foreground,var(--color-text-foreground-secondary,var(--text-secondary,#aaa))));--qmp-border:var(--color-token-border,var(--border-default,color-mix(in srgb,var(--qmp-text) 12%,transparent)));--qmp-border-strong:color-mix(in srgb,var(--qmp-text) 20%,transparent);--qmp-hover:var(--color-token-list-hover-background,color-mix(in srgb,var(--qmp-text) 7%,transparent));--qmp-accent:var(--color-token-charts-blue,#4d8dff);--qmp-accent-soft:color-mix(in srgb,var(--qmp-accent) 13%,transparent);--qmp-danger:var(--color-text-danger,#ff6b6b);--qmp-focus:color-mix(in srgb,var(--qmp-accent) 82%,white 18%);--qmp-radius-xs:4px;--qmp-radius-sm:6px;--qmp-radius-md:10px;--qmp-shadow-popover:0 8px 24px rgb(0 0 0 / 24%)}
+[data-codexpp-qmp-root]{position:fixed;z-index:2147483000;box-sizing:border-box;display:flex;flex-direction:column;max-width:calc(100vw - 24px);max-height:min(calc(100vh - 24px),var(--qmp-panel-max-height,calc(100vh - 24px)));overflow:hidden;isolation:isolate;border:1px solid var(--qmp-border);border-radius:var(--qmp-radius-md);background:var(--qmp-surface);color:var(--qmp-text);box-shadow:var(--qmp-shadow-popover);font-family:var(--qmp-host-font-family,-apple-system,"system-ui","Segoe UI",sans-serif);font-size:var(--qmp-font-size,13.5px);font-weight:var(--qmp-host-font-weight,400);font-optical-sizing:auto;line-height:var(--qmp-line-height,18.9px);letter-spacing:0;transform-origin:var(--qmp-origin-x,50%) var(--qmp-origin-y,100%);will-change:opacity,transform}
+[data-codexpp-qmp-root][data-resizing-model-column="true"]{cursor:col-resize;user-select:none}
+[data-codexpp-qmp-root][data-state="closed"],[data-codexpp-qmp-root][data-state="closing"]{opacity:0;transform:translate3d(0,var(--qmp-enter-y,4px),0);pointer-events:none;transition:opacity 100ms linear,transform 120ms cubic-bezier(.32,.72,0,1)}
+[data-codexpp-qmp-root][data-state="open"]{opacity:1;transform:translate3d(0,0,0);pointer-events:auto;transition:opacity 120ms ease-out,transform 160ms cubic-bezier(.23,1,.32,1)}
[data-codexpp-qmp-root][hidden]{display:none!important}
-[data-codexpp-qmp-root] .qmp-rail{box-sizing:border-box;height:38px;padding:6px 8px;display:flex;align-items:center;gap:0;border-bottom:1px solid color-mix(in srgb,currentColor 9%,transparent);background:color-mix(in srgb,currentColor 1.5%,transparent)}
-[data-codexpp-qmp-root] .qmp-presets{min-width:0;flex:1;display:flex;gap:2px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;white-space:nowrap}
+[data-codexpp-qmp-root] .qmp-rail{box-sizing:border-box;min-height:44px;flex:0 0 44px;padding:7px 8px;display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--qmp-border);background:color-mix(in srgb,var(--qmp-surface-subtle) 78%,var(--qmp-surface))}
+[data-codexpp-qmp-root] .qmp-presets{min-width:0;flex:1;display:flex;align-items:center;gap:10px;overflow-x:auto;overflow-y:hidden;overscroll-behavior-inline:contain;scrollbar-width:none;white-space:nowrap}
[data-codexpp-qmp-root] .qmp-presets::-webkit-scrollbar{display:none}
-[data-codexpp-qmp-root] button{box-sizing:border-box;font:inherit;color:inherit;letter-spacing:0}
-[data-codexpp-qmp-root] button:focus-visible{outline:2px solid #4d8dff;outline-offset:-2px}
-[data-codexpp-qmp-root] .qmp-icon{width:26px;height:26px;flex:0 0 26px;display:grid;place-items:center;border:0;border-radius:5px;background:transparent;cursor:pointer}
-[data-codexpp-qmp-root] .qmp-icon:hover:not(:disabled){background:color-mix(in srgb,currentColor 8%,transparent)}
-[data-codexpp-qmp-root] .qmp-icon:disabled{opacity:.34;cursor:not-allowed}
-[data-codexpp-qmp-root] .qmp-add{position:relative;margin-left:8px}
-[data-codexpp-qmp-root] .qmp-add::before{content:"";position:absolute;left:-8px;top:4px;width:1px;height:18px;background:color-mix(in srgb,currentColor 12%,transparent);pointer-events:none}
-[data-codexpp-qmp-root] .qmp-fast[aria-pressed="true"]{color:#78aaff;background:rgb(64 132 255 / 13%)}
-[data-codexpp-qmp-root] .qmp-preset{position:relative;width:fit-content;height:26px;min-width:36px;max-width:112px;flex:0 0 auto;border-radius:4px}
-[data-codexpp-qmp-root] .qmp-preset-apply{position:relative;width:auto;height:26px;min-width:36px;max-width:112px;padding:0 9px;border:0;border-radius:4px;background:transparent;color:var(--color-text-foreground-secondary,var(--text-secondary,#aaa));overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}
-[data-codexpp-qmp-root] .qmp-preset-apply::after{content:"";position:absolute;left:9px;right:9px;bottom:0;height:2px;border-radius:2px;background:transparent}
-[data-codexpp-qmp-root] .qmp-preset:hover .qmp-preset-apply,[data-codexpp-qmp-root] .qmp-preset:focus-within .qmp-preset-apply{background:color-mix(in srgb,currentColor 7%,transparent);color:inherit}
-[data-codexpp-qmp-root] .qmp-preset.is-active .qmp-preset-apply{color:inherit}
-[data-codexpp-qmp-root] .qmp-preset.is-active .qmp-preset-apply::after{background:#4d8dff}
-[data-codexpp-qmp-root] .qmp-preset.is-pending .qmp-preset-apply{color:inherit}
-[data-codexpp-qmp-root] .qmp-preset.is-pending .qmp-preset-apply::after{background:rgb(77 141 255 / 55%)}
-[data-codexpp-qmp-root] .qmp-preset.is-invalid .qmp-preset-apply{color:color-mix(in srgb,var(--color-text-foreground-secondary,var(--text-secondary,#aaa)) 48%,transparent)}
+[data-codexpp-qmp-root] button{box-sizing:border-box;font:inherit;color:inherit;letter-spacing:0;-webkit-tap-highlight-color:transparent}
+[data-codexpp-qmp-root] button:focus-visible,[data-codexpp-qmp-actions] button:focus-visible{outline:2px solid var(--qmp-focus);outline-offset:-2px}
+[data-codexpp-qmp-root] .qmp-icon{width:28px;height:28px;flex:0 0 28px;display:grid;place-items:center;border:0;border-radius:var(--qmp-radius-sm);background:transparent;cursor:pointer;transition:color 100ms ease-out,background-color 100ms ease-out,transform 80ms ease-out}
+[data-codexpp-qmp-root] .qmp-icon:active:not(:disabled){transform:scale(.97)}
+[data-codexpp-qmp-root] .qmp-icon:disabled{opacity:.36;cursor:not-allowed}
+[data-codexpp-qmp-root] .qmp-add{position:relative;margin-left:2px}
+[data-codexpp-qmp-root] .qmp-add::before{content:"";position:absolute;left:-5px;top:5px;width:1px;height:18px;background:var(--qmp-border-strong);pointer-events:none}
+[data-codexpp-qmp-root] .qmp-fast{position:relative;width:26px;height:26px;flex-basis:26px;overflow:visible;isolation:isolate}
+[data-codexpp-qmp-root] .qmp-fast svg{position:relative;z-index:1;transform-origin:50% 50%}
+[data-codexpp-qmp-root] .qmp-fast::before{content:"";position:absolute;inset:-2px;z-index:0;border:1px solid transparent;border-top-color:var(--qmp-accent);border-right-color:color-mix(in srgb,var(--qmp-accent) 52%,transparent);border-radius:8px;opacity:0;transform:scale(.72) rotate(-36deg);pointer-events:none}
+[data-codexpp-qmp-root] .qmp-fast::after{content:"";position:absolute;inset:3px;border:1px solid var(--qmp-accent);border-radius:var(--qmp-radius-sm);opacity:0;pointer-events:none}
+[data-codexpp-qmp-root] .qmp-fast[aria-pressed="true"]{color:var(--qmp-accent);background:var(--qmp-accent-soft)}
+[data-codexpp-qmp-root] .qmp-fast[aria-pressed="true"] svg{animation:qmp-fast-charge 2400ms cubic-bezier(.23,1,.32,1) infinite;will-change:transform,filter}
+[data-codexpp-qmp-root] .qmp-fast[aria-pressed="true"]::after{animation:qmp-fast-pulse 2400ms ease-out infinite;will-change:transform,opacity}
+[data-codexpp-qmp-root][data-fast-feedback="on"] .qmp-fast{color:var(--qmp-accent);background:color-mix(in srgb,var(--qmp-accent) 17%,transparent)}
+[data-codexpp-qmp-root][data-fast-feedback="on"] .qmp-fast svg{animation:qmp-fast-engage 620ms cubic-bezier(.16,1,.3,1) var(--qmp-fast-feedback-delay,0ms) both}
+[data-codexpp-qmp-root][data-fast-feedback="on"] .qmp-fast::before{animation:qmp-fast-arc 620ms cubic-bezier(.16,1,.3,1) var(--qmp-fast-feedback-delay,0ms) both}
+[data-codexpp-qmp-root][data-fast-feedback="on"] .qmp-fast::after{animation:qmp-fast-click-ring 620ms cubic-bezier(.16,1,.3,1) var(--qmp-fast-feedback-delay,0ms) both}
+[data-codexpp-qmp-root][data-fast-feedback="off"] .qmp-fast svg{animation:qmp-fast-disengage 420ms cubic-bezier(.32,.72,0,1) var(--qmp-fast-feedback-delay,0ms) both}
+[data-codexpp-qmp-root][data-fast-feedback="off"] .qmp-fast::after{animation:qmp-fast-collapse 420ms cubic-bezier(.32,.72,0,1) var(--qmp-fast-feedback-delay,0ms) both}
+[data-codexpp-qmp-root] .qmp-preset{position:relative;width:fit-content;height:30px;min-width:42px;max-width:150px;flex:0 0 auto;border-radius:var(--qmp-radius-xs)}
+[data-codexpp-qmp-root] .qmp-preset+.qmp-preset::before{content:"";position:absolute;left:-5px;top:8px;bottom:8px;width:1px;background:color-mix(in srgb,var(--qmp-border-strong) 68%,transparent);pointer-events:none}
+[data-codexpp-qmp-root] .qmp-preset-apply{position:relative;width:auto;height:30px;min-width:42px;max-width:150px;padding:0 11px;border:0;border-radius:var(--qmp-radius-xs);background:transparent;color:var(--qmp-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center;cursor:pointer;transition:color 100ms ease-out,background-color 100ms ease-out,transform 80ms ease-out}
+[data-codexpp-qmp-root] .qmp-preset-apply:active:not([aria-disabled="true"]){transform:scale(.97)}
+[data-codexpp-qmp-root] .qmp-preset-apply::after{content:"";position:absolute;left:11px;right:11px;bottom:1px;height:2px;border-radius:2px;background:transparent}
+[data-codexpp-qmp-root] .qmp-preset:focus-within .qmp-preset-apply{background:var(--qmp-hover);color:var(--qmp-text)}
+[data-codexpp-qmp-root] .qmp-preset.is-active .qmp-preset-apply{color:var(--qmp-text)}
+[data-codexpp-qmp-root] .qmp-preset.is-active .qmp-preset-apply::after{background:var(--qmp-accent)}
+[data-codexpp-qmp-root] .qmp-preset.is-pending .qmp-preset-apply{color:var(--qmp-text);background:var(--qmp-accent-soft)}
+[data-codexpp-qmp-root] .qmp-preset.is-pending .qmp-preset-apply::after{background:repeating-linear-gradient(90deg,var(--qmp-accent) 0 3px,transparent 3px 5px);background-size:10px 2px;animation:qmp-pending-line .7s linear infinite}
+[data-codexpp-qmp-root] .qmp-preset.is-invalid .qmp-preset-apply{color:color-mix(in srgb,var(--qmp-muted) 52%,transparent)}
[data-codexpp-qmp-root] .qmp-preset-apply[aria-disabled="true"]{cursor:not-allowed}
-[data-codexpp-qmp-root] .qmp-preset.is-dragging{opacity:.65}
-[data-codexpp-qmp-root] .qmp-preset.is-dragging .qmp-preset-apply{background:color-mix(in srgb,currentColor 7%,transparent)}
-[data-codexpp-qmp-root] .qmp-preset.is-renaming{width:112px;min-width:112px;max-width:112px}
+[data-codexpp-qmp-root] .qmp-preset.is-dragging{opacity:.72}
+[data-codexpp-qmp-root] .qmp-preset.is-dragging .qmp-preset-apply{background:var(--qmp-hover);transition:none}
+[data-codexpp-qmp-root] .qmp-preset.is-renaming{width:150px;min-width:150px;max-width:150px}
[data-codexpp-qmp-root] .qmp-preset.is-renaming>button{visibility:hidden}
-[data-codexpp-qmp-root] .qmp-rename{box-sizing:border-box;position:absolute;inset:0;width:100%;height:26px;border:1px solid #4d8dff;border-radius:4px;background:var(--color-background-elevated-primary-opaque,var(--main-surface-primary,#202020));color:inherit;padding:0 6px;outline:none}
-[data-codexpp-qmp-root] .qmp-message{padding:4px 8px;color:var(--color-text-foreground-secondary,var(--text-secondary,#aaa));font-size:11.5px;border-bottom:1px solid color-mix(in srgb,currentColor 7%,transparent)}
-[data-codexpp-qmp-root] .qmp-matrix-scroll{max-height:min(360px,58vh);overflow:auto}
-[data-codexpp-qmp-root] .qmp-grid{display:grid;grid-template-columns:104px repeat(var(--qmp-cols),minmax(64px,1fr));align-items:center}
-[data-codexpp-qmp-root] .qmp-column{position:sticky;top:0;z-index:2;height:36px;display:grid;place-items:center;padding:0 4px;background:var(--color-background-elevated-primary-opaque,var(--main-surface-primary,#202020));color:var(--color-text-foreground-secondary,var(--text-secondary,#aaa));font-size:11.5px;font-weight:500;text-align:center}
-[data-codexpp-qmp-root] .qmp-fast{position:sticky;top:5px;z-index:3;margin:5px 0 5px 7px}
-[data-codexpp-qmp-root] .qmp-model{height:40px;display:flex;align-items:center;padding:0 9px;border-top:1px solid color-mix(in srgb,currentColor 7%,transparent);font-size:12.5px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-[data-codexpp-qmp-root] .qmp-cell{height:40px;display:grid;place-items:center;border:0;border-top:1px solid color-mix(in srgb,currentColor 7%,transparent);background:transparent;cursor:pointer}
-[data-codexpp-qmp-root] .qmp-cell:hover:not(:disabled){background:color-mix(in srgb,currentColor 4%,transparent)}
-[data-codexpp-qmp-root] .qmp-cell>span{width:14px;height:14px;border:1.5px solid color-mix(in srgb,currentColor 38%,transparent);border-radius:50%;display:grid;place-items:center}
-[data-codexpp-qmp-root] .qmp-cell.is-selected{background:rgb(77 141 255 / 6%)}
-[data-codexpp-qmp-root] .qmp-cell.is-selected>span:after{content:"";width:5px;height:5px;border-radius:50%;background:#4d8dff}
-[data-codexpp-qmp-root] .qmp-cell.is-selected>span{border-color:#4d8dff}
-[data-codexpp-qmp-root] .qmp-cell.is-pending>span{box-shadow:0 0 0 3px rgb(77 141 255 / 14%)}
+[data-codexpp-qmp-root] .qmp-rename{box-sizing:border-box;position:absolute;inset:0;width:100%;height:30px;border:1px solid var(--qmp-accent);border-radius:var(--qmp-radius-sm);background:var(--qmp-surface);color:var(--qmp-text);padding:0 8px;outline:0;box-shadow:0 0 0 2px var(--qmp-accent-soft);font:inherit}
+[data-codexpp-qmp-root] .qmp-message{flex:0 0 auto;padding:5px 9px;border-bottom:1px solid var(--qmp-border);color:var(--qmp-muted);font-size:var(--qmp-small-font-size,12.5px);line-height:var(--qmp-small-line-height,17.5px)}
+[data-codexpp-qmp-root] .qmp-matrix-scroll{min-height:0;flex:1 1 auto;overflow:auto;overscroll-behavior:contain;scrollbar-color:var(--qmp-border-strong) transparent;scrollbar-width:thin}
+[data-codexpp-qmp-root] .qmp-grid{display:grid;grid-template-columns:var(--qmp-model-column) repeat(var(--qmp-cols),minmax(var(--qmp-reasoning-column),1fr));align-items:stretch;background:var(--qmp-surface)}
+[data-codexpp-qmp-root] .qmp-grid-corner{position:sticky;top:0;left:0;z-index:5;height:38px;display:grid;place-items:center;overflow:visible;background:var(--qmp-surface);box-shadow:1px 1px 0 var(--qmp-border)}
+[data-codexpp-qmp-root] .qmp-model-resizer{position:absolute;top:0;right:-5px;bottom:0;z-index:7;width:10px;cursor:col-resize;touch-action:none;outline:0}
+[data-codexpp-qmp-root] .qmp-model-resizer::after{content:"";position:absolute;top:7px;bottom:7px;left:50%;width:2px;border-radius:2px;background:transparent;transform:translateX(-50%);transition:background-color 100ms ease-out}
+[data-codexpp-qmp-root] .qmp-model-resizer:focus-visible::after,[data-codexpp-qmp-root][data-resizing-model-column="true"] .qmp-model-resizer::after{background:var(--qmp-accent)}
+[data-codexpp-qmp-root] .qmp-column{position:sticky;top:0;z-index:4;height:38px;display:grid;place-items:center;padding:0 4px;background:var(--qmp-surface);box-shadow:0 1px 0 var(--qmp-border);color:var(--qmp-muted);font-size:var(--qmp-small-font-size,12.5px);font-weight:500;line-height:1.2;text-align:center;white-space:nowrap;transition:color 100ms ease-out}
+[data-codexpp-qmp-root] .qmp-column.is-active{color:var(--qmp-text);background:var(--qmp-surface)}
+[data-codexpp-qmp-root] .qmp-column.is-active::after{content:"";position:absolute;left:50%;bottom:0;width:14px;height:2px;border-radius:2px;background:var(--qmp-accent);transform:translateX(-50%)}
+[data-codexpp-qmp-root] .qmp-pinned-models,[data-codexpp-qmp-root] .qmp-other-models{display:contents}
+[data-codexpp-qmp-root] .qmp-model{position:sticky;left:0;z-index:2;height:42px;display:flex;align-items:center;gap:5px;padding:0 11px 0 7px;border-top:1px solid var(--qmp-border);background:var(--qmp-surface);box-shadow:1px 0 0 var(--qmp-border);font-size:var(--qmp-font-size,13.5px);font-weight:var(--qmp-host-font-weight,400);white-space:nowrap;transition:box-shadow 100ms ease-out}
+[data-codexpp-qmp-root] .qmp-model.is-current{background:var(--qmp-surface);box-shadow:inset 2px 0 0 var(--qmp-accent),1px 0 0 var(--qmp-border)}
+[data-codexpp-qmp-root] .qmp-model.is-pending{background:var(--qmp-accent-soft);box-shadow:inset 2px 0 0 var(--qmp-accent),1px 0 0 var(--qmp-border)}
+[data-codexpp-qmp-root] .qmp-model-name{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:var(--qmp-host-font-weight,400);font-optical-sizing:auto}
+[data-codexpp-qmp-root] .qmp-pin{width:24px;height:24px;flex:0 0 24px;display:grid;place-items:center;border:0;border-radius:var(--qmp-radius-sm);background:transparent;color:var(--qmp-muted);opacity:.58;cursor:pointer;transition:color 100ms ease-out,background-color 100ms ease-out,opacity 100ms ease-out,transform 80ms ease-out}
+[data-codexpp-qmp-root] .qmp-pin:active{transform:scale(.94)}
+[data-codexpp-qmp-root] .qmp-pin[aria-pressed="true"]{color:color-mix(in srgb,var(--qmp-accent) 64%,transparent);background:color-mix(in srgb,var(--qmp-accent) 6%,transparent);opacity:.9}
+[data-codexpp-qmp-root] .qmp-cell{height:42px;display:grid;place-items:center;border:0;border-top:1px solid var(--qmp-border);background:transparent;cursor:pointer}
+[data-codexpp-qmp-root] .qmp-cell>span{width:15px;height:15px;border:1.5px solid color-mix(in srgb,var(--qmp-text) 38%,transparent);border-radius:999px;display:grid;place-items:center;transition:border-color 100ms ease-out,background-color 100ms ease-out,transform 80ms ease-out}
+[data-codexpp-qmp-root] .qmp-cell:active:not(:disabled)>span{transform:scale(.86)}
+[data-codexpp-qmp-root] .qmp-cell.is-selected{background:transparent}
+[data-codexpp-qmp-root] .qmp-cell.is-selected>span{border-color:var(--qmp-accent)}
+[data-codexpp-qmp-root] .qmp-cell.is-selected>span::after{content:"";width:6px;height:6px;border-radius:999px;background:var(--qmp-accent)}
+[data-codexpp-qmp-root] .qmp-cell.is-pending>span{border-color:var(--qmp-accent);border-style:dashed;background:var(--qmp-accent-soft);animation:qmp-pending-ring .72s linear infinite}
[data-codexpp-qmp-root] .qmp-cell:disabled{opacity:1;cursor:not-allowed}
-[data-codexpp-qmp-root] .qmp-cell:disabled>span{width:10px;height:1px;border:0;border-radius:1px;background:var(--color-text-foreground-secondary,var(--text-secondary,#aaa));opacity:.32}
-[data-codexpp-qmp-root] .qmp-legacy-toggle{grid-column:1/-1;height:30px;display:flex;align-items:center;justify-content:center;gap:5px;border:0;border-top:1px solid color-mix(in srgb,currentColor 7%,transparent);background:color-mix(in srgb,currentColor 1.5%,transparent);color:var(--color-text-foreground-secondary,var(--text-secondary,#aaa));font-size:11.5px;cursor:pointer}
-[data-codexpp-qmp-root] .qmp-legacy-toggle:hover{background:color-mix(in srgb,currentColor 5%,transparent);color:inherit}
-[data-codexpp-qmp-root] .qmp-legacy-toggle svg{transition:transform 120ms ease}
-[data-codexpp-qmp-root] .qmp-legacy-toggle[aria-expanded="true"] svg{transform:rotate(180deg)}
-[data-codexpp-qmp-root] .qmp-legacy-models{display:contents}
-[data-codexpp-qmp-root] .qmp-empty{min-height:90px;display:grid;place-items:center;padding:18px;color:var(--color-text-foreground-secondary,var(--text-secondary,#aaa));text-align:center}
-[data-codexpp-qmp-preview],[data-codexpp-qmp-actions]{position:fixed;z-index:2147483001;box-sizing:border-box;border:1px solid color-mix(in srgb,currentColor 16%,transparent);border-radius:6px;background:var(--color-background-elevated-primary-opaque,var(--main-surface-primary,#202020));color:var(--color-text-foreground,var(--text-primary,#f3f3f3));box-shadow:0 8px 24px rgb(0 0 0 / 28%);font:13px/18px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:0}
-[data-codexpp-qmp-preview]{max-width:320px;padding:8px 10px;pointer-events:none}
-[data-codexpp-qmp-preview] small{display:block;color:var(--color-text-foreground-secondary,var(--text-secondary,#aaa));font-size:12px;line-height:16px;white-space:nowrap}
-[data-codexpp-qmp-actions]{min-width:110px;padding:4px}
-[data-codexpp-qmp-actions] button{width:100%;height:30px;padding:0 8px;display:flex;align-items:center;gap:7px;border:0;border-radius:4px;background:transparent;color:inherit;font:13px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;text-align:left;cursor:pointer}
+[data-codexpp-qmp-root] .qmp-cell:disabled>span{width:10px;height:2px;border:0;border-radius:999px;background:var(--qmp-muted);opacity:.38;animation:none}
+[data-codexpp-qmp-root] .qmp-cell.is-unsupported-current:disabled>span{width:15px;height:15px;border:1.5px dashed var(--qmp-accent);border-radius:999px;background:transparent;opacity:1}
+[data-codexpp-qmp-root] .qmp-cell.is-unsupported-current:disabled>span::after{content:"";width:5px;height:5px;border-radius:999px;background:var(--qmp-accent)}
+[data-codexpp-qmp-root] .qmp-other-toggle{grid-column:1/-1;height:34px;display:flex;align-items:center;justify-content:center;gap:6px;border:0;border-top:1px solid var(--qmp-border);background:color-mix(in srgb,var(--qmp-surface-subtle) 60%,var(--qmp-surface));color:var(--qmp-muted);font-size:var(--qmp-small-font-size,12.5px);font-weight:500;cursor:pointer;transition:color 100ms ease-out,background-color 100ms ease-out,transform 80ms ease-out}
+[data-codexpp-qmp-root] .qmp-other-toggle:active:not(:disabled){transform:scale(.99)}
+[data-codexpp-qmp-root] .qmp-other-toggle svg{transition:transform 140ms cubic-bezier(.23,1,.32,1)}
+[data-codexpp-qmp-root] .qmp-other-toggle[aria-expanded="true"] svg{transform:rotate(180deg)}
+[data-codexpp-qmp-root] .qmp-other-toggle:disabled{cursor:default;opacity:.72}
+[data-codexpp-qmp-root] .qmp-empty{min-height:96px;display:grid;place-items:center;padding:20px;color:var(--qmp-muted);text-align:center;text-wrap:balance}
+[data-codexpp-qmp-preview],[data-codexpp-qmp-actions]{position:fixed;z-index:2147483001;box-sizing:border-box;border:1px solid var(--qmp-border);background:var(--qmp-surface);color:var(--qmp-text);box-shadow:var(--qmp-shadow-popover);font-family:var(--qmp-host-font-family,-apple-system,"system-ui","Segoe UI",sans-serif);font-size:var(--qmp-font-size,13.5px);font-weight:var(--qmp-host-font-weight,400);font-optical-sizing:auto;line-height:var(--qmp-line-height,18.9px);letter-spacing:0;transform-origin:var(--qmp-origin-x,50%) var(--qmp-origin-y,50%)}
+[data-codexpp-qmp-preview][data-state="closed"],[data-codexpp-qmp-actions][data-state="closed"]{opacity:0;transform:translate3d(0,var(--qmp-float-y,3px),0) scale(.98);pointer-events:none;transition:opacity 80ms linear,transform 110ms cubic-bezier(.32,.72,0,1)}
+[data-codexpp-qmp-preview][data-state="open"],[data-codexpp-qmp-actions][data-state="open"]{opacity:1;transform:translate3d(0,0,0) scale(1);transition:opacity 100ms ease-out,transform 140ms cubic-bezier(.23,1,.32,1)}
+[data-codexpp-qmp-preview]{max-width:min(320px,calc(100vw - 16px));padding:8px 10px;border-radius:var(--qmp-radius-md);pointer-events:none}
+[data-codexpp-qmp-preview] .qmp-preview-title{color:var(--qmp-text);font-weight:600;line-height:var(--qmp-tight-line-height,18.4px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+[data-codexpp-qmp-preview] .qmp-preview-detail{margin-top:1px;color:var(--qmp-muted);font-size:var(--qmp-small-font-size,12.5px);line-height:var(--qmp-small-line-height,17.5px);white-space:nowrap}
+[data-codexpp-qmp-actions]{min-width:132px;padding:4px;border-radius:var(--qmp-radius-md)}
+[data-codexpp-qmp-actions][data-state="open"]{pointer-events:auto}
+[data-codexpp-qmp-actions] button{width:100%;height:32px;padding:0 9px;display:flex;align-items:center;gap:8px;border:0;border-radius:var(--qmp-radius-sm);background:transparent;color:var(--qmp-text);font-family:var(--qmp-host-font-family,-apple-system,"system-ui","Segoe UI",sans-serif);font-size:var(--qmp-font-size,13.5px);font-weight:500;line-height:1;text-align:left;cursor:pointer;transition:color 100ms ease-out,background-color 100ms ease-out,transform 80ms ease-out}
+[data-codexpp-qmp-actions] button:active{transform:scale(.98)}
+[data-codexpp-qmp-actions] button[data-menu-action="delete"]{color:var(--qmp-danger)}
[data-codexpp-qmp-actions] svg{flex:0 0 14px}
-[data-codexpp-qmp-actions] button:hover{background:color-mix(in srgb,currentColor 10%,transparent)}
-@media (prefers-reduced-motion:reduce){[data-codexpp-qmp-root],[data-codexpp-qmp-preview]{transition:none!important}}
+@keyframes qmp-fast-charge{0%,70%,100%{transform:scale(1) rotate(0);filter:drop-shadow(0 0 0 transparent)}76%{transform:scale(1.14) rotate(-5deg);filter:drop-shadow(0 0 4px var(--qmp-accent))}83%{transform:scale(.98) rotate(3deg);filter:drop-shadow(0 0 2px var(--qmp-accent))}90%{transform:scale(1.07) rotate(-2deg);filter:drop-shadow(0 0 3px var(--qmp-accent))}96%{transform:scale(1) rotate(0);filter:drop-shadow(0 0 0 transparent)}}
+@keyframes qmp-fast-pulse{0%,70%,100%{opacity:0;transform:scale(.72)}75%{opacity:.5;transform:scale(.82)}94%{opacity:0;transform:scale(1.24)}}
+@keyframes qmp-fast-engage{0%{transform:scale(.94) rotate(-3deg);filter:drop-shadow(0 0 0 transparent)}24%{transform:scale(.9) rotate(-6deg);filter:drop-shadow(0 0 1px var(--qmp-accent))}48%{transform:scale(1.08) rotate(2deg);filter:brightness(1.28) drop-shadow(0 0 6px var(--qmp-accent))}72%{transform:scale(1.02) rotate(-1deg);filter:brightness(1.08) drop-shadow(0 0 3px var(--qmp-accent))}100%{transform:scale(1) rotate(0);filter:drop-shadow(0 0 0 transparent)}}
+@keyframes qmp-fast-arc{0%{opacity:0;transform:scale(.72) rotate(-36deg)}20%{opacity:.8}70%{opacity:.32}100%{opacity:0;transform:scale(1.26) rotate(112deg)}}
+@keyframes qmp-fast-click-ring{0%{opacity:0;transform:scale(.66)}28%{opacity:.72;transform:scale(.82)}100%{opacity:0;transform:scale(1.42)}}
+@keyframes qmp-fast-disengage{0%{transform:scale(1);filter:drop-shadow(0 0 4px var(--qmp-accent))}45%{transform:scale(.9) rotate(3deg);filter:drop-shadow(0 0 1px var(--qmp-accent))}100%{transform:scale(1) rotate(0);filter:drop-shadow(0 0 0 transparent)}}
+@keyframes qmp-fast-collapse{0%{opacity:.48;transform:scale(1.18)}100%{opacity:0;transform:scale(.72)}}
+@keyframes qmp-pending-ring{to{transform:rotate(360deg)}}
+@keyframes qmp-pending-line{to{background-position:10px 0}}
+@media (hover:hover) and (pointer:fine){[data-codexpp-qmp-root] .qmp-icon:hover:not(:disabled){background:var(--qmp-hover)}[data-codexpp-qmp-root] .qmp-preset:hover .qmp-preset-apply{background:var(--qmp-hover);color:var(--qmp-text)}[data-codexpp-qmp-root] .qmp-cell:hover:not(:disabled){background:var(--qmp-hover)}[data-codexpp-qmp-root] .qmp-cell:hover:not(:disabled)>span{border-color:color-mix(in srgb,var(--qmp-accent) 68%,var(--qmp-text))}[data-codexpp-qmp-root] .qmp-model:hover .qmp-pin,[data-codexpp-qmp-root] .qmp-pin:hover{opacity:1}[data-codexpp-qmp-root] .qmp-pin:hover{background:var(--qmp-hover);color:var(--qmp-text)}[data-codexpp-qmp-root] .qmp-pin[aria-pressed="true"]:hover{color:color-mix(in srgb,var(--qmp-accent) 76%,transparent);background:color-mix(in srgb,var(--qmp-accent) 9%,transparent)}[data-codexpp-qmp-root] .qmp-model-resizer:hover::after{background:var(--qmp-border-strong)}[data-codexpp-qmp-root] .qmp-other-toggle:hover:not(:disabled){background:var(--qmp-hover);color:var(--qmp-text)}[data-codexpp-qmp-actions] button:hover{background:var(--qmp-hover)}}
+@media (prefers-reduced-motion:reduce){[data-codexpp-qmp-root],[data-codexpp-qmp-root] *,[data-codexpp-qmp-root] *::before,[data-codexpp-qmp-root] *::after,[data-codexpp-qmp-preview],[data-codexpp-qmp-preview] *,[data-codexpp-qmp-actions],[data-codexpp-qmp-actions] *{scroll-behavior:auto!important;animation:none!important;transition:none!important}}
+@media (prefers-reduced-transparency:reduce){[data-codexpp-qmp-root],[data-codexpp-qmp-preview],[data-codexpp-qmp-actions]{background:var(--qmp-surface);backdrop-filter:none}}
+@media (prefers-contrast:more){[data-codexpp-qmp-root],[data-codexpp-qmp-preview],[data-codexpp-qmp-actions]{border-color:var(--qmp-border-strong)}[data-codexpp-qmp-root] .qmp-column,[data-codexpp-qmp-root] .qmp-model,[data-codexpp-qmp-root] .qmp-cell,[data-codexpp-qmp-root] .qmp-other-toggle{border-color:var(--qmp-border-strong)}[data-codexpp-qmp-root] .qmp-preset-apply,[data-codexpp-qmp-root] .qmp-column,[data-codexpp-qmp-root] .qmp-message{color:var(--qmp-text)}}
`;
documentRef.head?.append(style);
root = documentRef.createElement("div");
root.setAttribute("data-codexpp-qmp-root", "");
+ root.setAttribute("role", "dialog");
+ root.setAttribute("aria-label", "\u6A21\u578B\u4E0E\u9884\u8BBE");
+ root.setAttribute("aria-hidden", "true");
+ root.dataset.state = "closed";
root.hidden = true;
documentRef.body.append(root);
- root.addEventListener("pointerenter", () => {
- pointerOnPanel = true;
- clearLater(closeTimer);
- }, { signal });
- root.addEventListener("pointerleave", () => {
- pointerOnPanel = false;
- scheduleClose();
- }, { signal });
- root.addEventListener("pointerdown", (event) => {
- event.stopPropagation();
- const apply = event.target.closest(".qmp-preset-apply");
- if (apply) startDrag(event, apply);
- }, { signal });
+ installTypographyObserver();
+ syncHostTypography(true);
+ root.addEventListener(
+ "pointerenter",
+ () => {
+ pointerOnPanel = true;
+ clearLater(closeTimer);
+ },
+ { signal }
+ );
+ root.addEventListener(
+ "pointerleave",
+ () => {
+ pointerOnPanel = false;
+ scheduleClose();
+ },
+ { signal }
+ );
+ root.addEventListener(
+ "focusin",
+ () => {
+ focusOnPanel = true;
+ clearLater(closeTimer);
+ },
+ { signal }
+ );
+ root.addEventListener(
+ "focusout",
+ (event) => {
+ if (root?.contains(event.relatedTarget)) return;
+ focusOnPanel = false;
+ scheduleClose();
+ },
+ { signal }
+ );
+ root.addEventListener(
+ "pointerdown",
+ (event) => {
+ event.stopPropagation();
+ const resizer = event.target.closest(".qmp-model-resizer");
+ if (resizer) {
+ startModelColumnResize(event, resizer);
+ return;
+ }
+ const apply = event.target.closest(".qmp-preset-apply");
+ if (apply) startDrag(event, apply);
+ },
+ { signal }
+ );
root.addEventListener("pointermove", moveDrag, { signal });
root.addEventListener("pointerup", () => finishDrag(), { signal });
- root.addEventListener("pointercancel", (event) => {
- if (drag) finishDrag({ cancelled: true });
- else if (presetClickSuppression?.pointerId === event.pointerId) clearPresetClickSuppression();
- }, { signal });
+ root.addEventListener(
+ "pointercancel",
+ (event) => {
+ if (drag) finishDrag({ cancelled: true });
+ else if (presetClickSuppression?.pointerId === event.pointerId)
+ clearPresetClickSuppression();
+ },
+ { signal }
+ );
root.addEventListener("click", handleClick, { signal });
+ root.addEventListener(
+ "dblclick",
+ (event) => {
+ const resizer = event.target.closest(".qmp-model-resizer");
+ if (!resizer) return;
+ event.preventDefault();
+ setModelColumnWidth(DEFAULT_MODEL_COLUMN_WIDTH);
+ resizer.focus({ preventScroll: true });
+ },
+ { signal }
+ );
root.addEventListener("contextmenu", handleContextMenu, { signal });
- root.addEventListener("pointerover", (event) => {
- const preset = event.target.closest(".qmp-preset");
- if (preset && !preset.contains(event.relatedTarget)) showPreview(preset.dataset.presetId, preset);
- }, { signal });
- root.addEventListener("pointerout", (event) => {
- const preset = event.target.closest(".qmp-preset");
- if (preset && !preset.contains(event.relatedTarget)) hidePreview();
- }, { signal });
+ root.addEventListener(
+ "pointerover",
+ (event) => {
+ const preset = event.target.closest(".qmp-preset");
+ if (preset && !preset.contains(event.relatedTarget))
+ showPreview(preset.dataset.presetId, preset);
+ },
+ { signal }
+ );
+ root.addEventListener(
+ "pointerout",
+ (event) => {
+ const preset = event.target.closest(".qmp-preset");
+ if (preset && !preset.contains(event.relatedTarget)) hidePreview();
+ },
+ { signal }
+ );
resizeObserver = window.ResizeObserver ? new window.ResizeObserver(positionPanel) : null;
resizeObserver?.observe(root);
render();
@@ -1384,6 +2289,17 @@
if (actionMenu && !actionMenu.contains(event.target)) closeActionMenu();
}
function onKeyDown(event) {
+ const resizer = event.target.closest?.(".qmp-model-resizer");
+ if (resizer) {
+ const step = event.shiftKey ? 24 : 8;
+ const next = event.key === "ArrowLeft" ? modelColumnWidth - step : event.key === "ArrowRight" ? modelColumnWidth + step : event.key === "Home" ? DEFAULT_MODEL_COLUMN_WIDTH : null;
+ if (next != null) {
+ event.preventDefault();
+ event.stopPropagation();
+ setModelColumnWidth(next);
+ return;
+ }
+ }
if (probe && !event.isTrusted) return;
const presetButton = event.target.closest?.(".qmp-preset-apply");
const opensContextMenu = event.key === "ContextMenu" || event.key === "F10" && event.shiftKey;
@@ -1398,7 +2314,9 @@
if (actionMenu) {
event.preventDefault();
closeActionMenu(false, true);
- } else if (drag) finishDrag({ cancelled: true, suppressReleasedClick: !drag.active });
+ } else if (modelColumnResize) finishModelColumnResize({ cancelled: true });
+ else if (drag)
+ finishDrag({ cancelled: true, suppressReleasedClick: !drag.active });
else if (renameId) {
finishRename?.(false);
} else {
@@ -1406,22 +2324,67 @@
boundTrigger?.focus?.();
}
}
- documentRef.addEventListener("pointerdown", onDocumentPointerDown, { capture: true, signal });
+ documentRef.addEventListener("pointerdown", onDocumentPointerDown, {
+ capture: true,
+ signal
+ });
documentRef.addEventListener("keydown", onKeyDown, { signal });
window.addEventListener("resize", positionPanel, { signal });
- window.addEventListener("blur", () => closeActionMenu(false), { signal });
- window.addEventListener("focus", () => {
- if (visible) void syncSnapshot();
- }, { signal });
- window.addEventListener("storage", (event) => {
- if (event.key === STORAGE_KEY) {
- store = readStore();
- render();
- }
- }, { signal });
- documentRef.addEventListener("visibilitychange", () => {
- if (documentRef.visibilityState === "visible" && visible) void syncSnapshot();
- }, { signal });
+ window.addEventListener("pointermove", moveModelColumnResize, {
+ capture: true,
+ signal
+ });
+ window.addEventListener(
+ "pointerup",
+ (event) => {
+ if (modelColumnResize?.pointerId === event.pointerId)
+ finishModelColumnResize();
+ },
+ { capture: true, signal }
+ );
+ window.addEventListener(
+ "pointercancel",
+ (event) => {
+ if (modelColumnResize?.pointerId === event.pointerId) {
+ finishModelColumnResize({ cancelled: true });
+ }
+ },
+ { capture: true, signal }
+ );
+ window.addEventListener(
+ "blur",
+ () => {
+ closeActionMenu(false, false, true);
+ finishModelColumnResize({ cancelled: true });
+ },
+ { signal }
+ );
+ window.addEventListener(
+ "focus",
+ () => {
+ if (visible) void syncSnapshot();
+ },
+ { signal }
+ );
+ window.addEventListener(
+ "storage",
+ (event) => {
+ if (event.key === STORAGE_KEY) {
+ store = readStore();
+ modelColumnWidth = store.modelColumnWidth;
+ render();
+ }
+ },
+ { signal }
+ );
+ documentRef.addEventListener(
+ "visibilitychange",
+ () => {
+ if (documentRef.visibilityState === "visible" && visible)
+ void syncSnapshot();
+ },
+ { signal }
+ );
mutationObserver = new window.MutationObserver(() => {
if (probe) claimProbeRoots();
const userMenuOpen = nativeMenuOpen();
@@ -1436,31 +2399,49 @@
subtree: true,
childList: true,
attributes: true,
- attributeFilter: ["aria-expanded", "aria-checked", "aria-selected", "data-state"]
+ attributeFilter: [
+ "aria-expanded",
+ "aria-checked",
+ "aria-selected",
+ "data-state"
+ ]
});
if (documentRef.body) mount();
- else documentRef.addEventListener("DOMContentLoaded", mount, { once: true, signal });
+ else
+ documentRef.addEventListener("DOMContentLoaded", mount, {
+ once: true,
+ signal
+ });
function publicState() {
return {
version: VERSION,
visible,
interaction,
message,
- capability: { status: capability.status, generation: capability.generation },
+ capability: {
+ status: capability.status,
+ generation: capability.generation
+ },
modelCount: capability.models.filter((model) => !isAutoReviewModel(model)).length,
snapshot,
pendingTarget,
- presets: store.presets
+ presets: store.presets,
+ pinnedModelKeys: store.pinnedModelKeys,
+ modelColumnWidth,
+ otherModelsExpanded,
+ typography: { ...hostTypography }
};
}
function destroy() {
if (destroyed) return;
+ finishModelColumnResize({ cancelled: true, schedule: false });
destroyed = true;
cancelIntents();
lifetime.abort();
triggerLifetimes.forEach((triggerLifetime) => triggerLifetime.abort());
triggerLifetimes.clear();
mutationObserver?.disconnect();
+ typographyObserver?.disconnect();
resizeObserver?.disconnect();
timers.forEach((timer) => window.clearTimeout(timer));
timers.clear();
@@ -1468,9 +2449,14 @@
if (window.cancelAnimationFrame) window.cancelAnimationFrame(scanFrame);
else clearLater(scanFrame);
}
- if (window.dispatchEvent === wrappedDispatch) window.dispatchEvent = originalDispatch;
- probe?.roots.forEach((menu) => menu.removeAttribute("data-codexpp-qmp-probe-root"));
- documentRef.querySelectorAll("[data-codexpp-qmp-root],[data-codexpp-qmp-preview],[data-codexpp-qmp-actions],#codexpp-qmp-style").forEach((element) => element.remove());
+ if (window.dispatchEvent === wrappedDispatch)
+ window.dispatchEvent = originalDispatch;
+ probe?.roots.forEach(
+ (menu) => menu.removeAttribute("data-codexpp-qmp-probe-root")
+ );
+ documentRef.querySelectorAll(
+ "[data-codexpp-qmp-root],[data-codexpp-qmp-preview],[data-codexpp-qmp-actions],#codexpp-qmp-style"
+ ).forEach((element) => element.remove());
root = null;
preview = null;
actionMenu = null;
@@ -1487,10 +2473,10 @@
return syncSnapshot();
},
getState() {
- return JSON.parse(JSON.stringify(publicState()));
+ return window.structuredClone(publicState());
},
getModelListCache() {
- return JSON.parse(JSON.stringify(modelListCache));
+ return window.structuredClone(modelListCache);
},
destroy
};