Restyle chat model picker as a themed custom dropdown.

Drop the bordered native select chrome and open a soft popup that follows light/dark theme tokens.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-11 05:05:52 +08:00
parent 746570dfc9
commit 2a52349668
3 changed files with 164 additions and 67 deletions

View file

@ -337,55 +337,87 @@
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 2px 4px 8px;
gap: 4px;
padding: 0 2px 6px;
}
.chat-composer-chip,
.chat-composer-chip--select {
appearance: none;
-webkit-appearance: none;
height: 30px;
min-height: 30px;
max-height: 30px;
padding: 0 12px;
height: 28px;
min-height: 28px;
max-height: 28px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.28));
background-color: var(--ds-surface-2, #152033);
color: var(--ds-text, #e2e8f0);
border: none;
background-color: transparent;
color: var(--ds-text-muted, rgba(148, 163, 184, 0.92));
font-size: 12px;
font-weight: 500;
line-height: 1;
box-sizing: border-box;
cursor: pointer;
transition: background-color 0.15s ease, color 0.15s ease;
}
.chat-composer-chip--select {
min-width: 148px;
max-width: 240px;
padding-right: 28px;
background-color: var(--ds-surface-2, #152033);
min-width: 0;
max-width: 220px;
width: auto;
padding-right: 22px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
background-color: transparent;
background-image:
linear-gradient(45deg, transparent 50%, var(--ds-text-muted, rgba(148, 163, 184, 0.85)) 50%),
linear-gradient(135deg, var(--ds-text-muted, rgba(148, 163, 184, 0.85)) 50%, transparent 50%);
linear-gradient(45deg, transparent 50%, var(--ds-text-muted, rgba(148, 163, 184, 0.7)) 50%),
linear-gradient(135deg, var(--ds-text-muted, rgba(148, 163, 184, 0.7)) 50%, transparent 50%);
background-position:
calc(100% - 14px) calc(50% - 2px),
calc(100% - 9px) calc(50% - 2px);
background-size: 5px 5px, 5px 5px;
calc(100% - 10px) calc(50% - 1.5px),
calc(100% - 6px) calc(50% - 1.5px);
background-size: 4px 4px, 4px 4px;
background-repeat: no-repeat;
}
.chat-composer-chip--select option {
background-color: var(--ds-surface, #0f172a);
color: var(--ds-text, #e2e8f0);
}
.chat-composer-chip:hover,
.chat-composer-chip--select:hover {
background-color: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.16));
border-color: var(--ds-accent-border, rgba(94, 179, 255, 0.35));
background-color: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.12));
color: var(--ds-text, #e2e8f0);
}
.chat-composer-chip:focus-visible,
.chat-composer-chip--select:focus-visible {
outline: none;
border-color: var(--ds-accent-border-strong, rgba(94, 179, 255, 0.5));
box-shadow: 0 0 0 2px var(--ds-accent-soft, rgba(94, 179, 255, 0.2));
background-color: var(--ds-accent-soft, rgba(94, 179, 255, 0.14));
color: var(--ds-text, #e2e8f0);
box-shadow: none;
}
.chat-model-select {
position: relative;
display: inline-flex;
max-width: 100%;
}
.chat-model-select-pop {
border-radius: 12px;
padding: 6px;
min-width: 11rem;
max-width: min(320px, calc(100vw - 16px));
max-height: min(320px, 50vh);
overflow-y: auto;
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.18));
background: var(--ds-panel, var(--ds-surface, #0f172a));
box-shadow: var(--ds-pop-shadow, 0 12px 32px rgba(2, 6, 23, 0.45));
}
.chat-model-select-pop .chat-sess-menu-item {
border-radius: 8px;
padding: 9px 12px;
font-size: 13px;
line-height: 1.35;
}
.chat-model-select-pop .chat-sess-menu-item--active {
background: var(--ds-accent-soft, rgba(59, 130, 246, 0.18));
color: var(--ds-text, #e2e8f0);
font-weight: 600;
}
.chat-model-select-pop .chat-sess-menu-item--active:hover {
background: var(--ds-accent-soft, rgba(59, 130, 246, 0.22));
}
.chat-composer-shell {
border-radius: 12px;

View file

@ -3473,11 +3473,71 @@ async function renderChatUi() {
});
let globalMenuModeValue = String(localStorage.getItem(CHAT_USER_MENU_MODE_KEY) || MAIN_MODE_VALUE).toLowerCase();
if (globalMenuModeValue === "comprehensive" || globalMenuModeValue === "main") globalMenuModeValue = MAIN_MODE_VALUE;
const modelSelect = el("select", {
const modelSelectBtn = el("button", {
type: "button",
class: "chat-composer-chip chat-composer-chip--select",
title: t("chat.activeModelLabel"),
text: "-",
});
const modelSelectWrap = el("div", { class: "chat-model-select" }, [modelSelectBtn]);
let modelSelectNameToId = new Map();
let modelSelectOptions = [];
let modelSelectedKey = "";
const syncModelSelectBtn = () => {
const hit = modelSelectOptions.find((o) => o.key === modelSelectedKey);
modelSelectBtn.textContent = String((hit && hit.label) || "-");
};
const openModelSelectMenu = (anchorEl) => {
dismissChatMenus();
const items = modelSelectOptions.length
? modelSelectOptions.map((opt) =>
el("button", {
type: "button",
class:
"chat-sess-menu-item" +
(opt.key === modelSelectedKey ? " chat-sess-menu-item--active" : ""),
text: opt.label,
onclick: async (ev) => {
ev.stopPropagation();
dismissChatMenus();
const key = String(opt.key || "").trim();
if (!key || key === modelSelectedKey) return;
modelSelectedKey = key;
syncModelSelectBtn();
const pid = String(modelSelectNameToId.get(key) || "").trim();
if (!pid) return;
try {
await apiPost("/admin/api/models/active", { profile_id: pid });
await refreshActiveModelText();
} catch (e) {
statusBar.textContent = `${t("chat.error")}: ${String(e)}`;
}
},
}),
)
: [el("div", { class: "chat-sess-menu-item muted", text: "-" })];
const menu = el("div", { class: "chat-sess-menu-pop chat-model-select-pop u-pop-menu" }, items);
menu.style.position = "fixed";
menu.style.zIndex = "300";
const rect = (anchorEl || modelSelectBtn).getBoundingClientRect();
attachChatMenuDismiss(menu);
const mrect = menu.getBoundingClientRect();
const pad = 8;
let left = rect.left;
let top = rect.bottom + 6;
if (top + mrect.height > window.innerHeight - pad) {
top = Math.max(pad, rect.top - 6 - mrect.height);
}
left = Math.max(pad, Math.min(left, window.innerWidth - pad - mrect.width));
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
menu.style.minWidth = `${Math.max(168, Math.ceil(rect.width))}px`;
};
modelSelectBtn.addEventListener("click", (ev) => {
ev.stopPropagation();
if (modelSelectBtn.disabled) return;
openModelSelectMenu(ev.currentTarget);
});
const normalizeExecutionMode = (v) => {
const raw = String(v || "").trim().toLowerCase();
return raw === EXECUTION_MODE_PLAN ? EXECUTION_MODE_PLAN : EXECUTION_MODE_AGENT;
@ -3585,8 +3645,8 @@ async function renderChatUi() {
const activeModelText = el("span", { class: "muted", text: `${t("chat.activeModelLabel")}: -` });
const applyModelSelectorVisibility = (modelsState) => {
const show = !(modelsState && modelsState.chat_model_selector_visible === false);
modelSelect.style.display = show ? "" : "none";
modelSelect.disabled = !show;
modelSelectWrap.style.display = show ? "" : "none";
modelSelectBtn.disabled = !show;
};
const refreshActiveModelText = async () => {
try {
@ -3597,8 +3657,8 @@ async function renderChatUi() {
const p = profiles.find((x) => String(x.id || "") === aid) || null;
const modelName = String((p && p.model) || "").trim() || "-";
activeModelText.textContent = `${t("chat.activeModelLabel")}: ${modelName}`;
modelSelect.innerHTML = "";
modelSelectNameToId = new Map();
modelSelectOptions = [];
const usedKeys = new Set();
profiles.forEach((row) => {
const pid = String(row && row.id ? row.id : "");
@ -3613,38 +3673,30 @@ async function renderChatUi() {
}
usedKeys.add(key);
modelSelectNameToId.set(key, pid);
const display = String(row.model || row.name || pid);
modelSelect.appendChild(el("option", { value: key, text: display }));
modelSelectOptions.push({
key,
id: pid,
label: String(row.model || row.name || pid),
});
});
if (p) {
const rawName = String(p.name || "").trim();
const firstKey = Array.from(modelSelectNameToId.keys()).find((k) => modelSelectNameToId.get(k) === aid) || "";
const key = firstKey || rawName;
if (key && Array.from(modelSelect.options).some((o) => String(o.value || "") === key)) {
modelSelect.value = key;
}
} else if (modelSelect.options.length > 0) {
modelSelect.value = String(modelSelect.options[0].value || "");
modelSelectedKey = firstKey || "";
} else if (modelSelectOptions.length > 0) {
modelSelectedKey = String(modelSelectOptions[0].key || "");
} else {
modelSelectedKey = "";
}
syncModelSelectBtn();
} catch (_) {
applyModelSelectorVisibility(null);
activeModelText.textContent = `${t("chat.activeModelLabel")}: -`;
modelSelect.innerHTML = "";
modelSelect.appendChild(el("option", { value: "", text: "-" }));
modelSelect.value = "";
modelSelectNameToId = new Map();
modelSelectOptions = [];
modelSelectedKey = "";
syncModelSelectBtn();
}
};
modelSelect.addEventListener("change", async () => {
const selectedKey = String(modelSelect.value || "").trim();
const pid = String(modelSelectNameToId.get(selectedKey) || "").trim();
if (!pid) return;
try {
await apiPost("/admin/api/models/active", { profile_id: pid });
await refreshActiveModelText();
} catch (e) {
statusBar.textContent = `${t("chat.error")}: ${String(e)}`;
}
});
const loadSessionModePreference = async () => {
if (!activeId) {
setExecutionMode(currentExecutionMode, { persistLocal: true, saveSession: false });
@ -3781,7 +3833,7 @@ async function renderChatUi() {
});
const composerMetaBar = el("div", { class: "chat-composer-meta" }, [
execSelectWrap,
modelSelect,
modelSelectWrap,
compressBtn,
]);

View file

@ -321,32 +321,45 @@ body.theme-ds-body[data-admin-theme="light"] .chat-composer {
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip,
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select {
background-color: #ffffff;
border-color: rgba(15, 23, 42, 0.16);
color: #0f172a;
background-color: transparent;
border: none;
color: rgba(15, 23, 42, 0.62);
}
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select {
background-color: #ffffff;
background-color: transparent;
background-image:
linear-gradient(45deg, transparent 50%, rgba(15, 23, 42, 0.55) 50%),
linear-gradient(135deg, rgba(15, 23, 42, 0.55) 50%, transparent 50%);
linear-gradient(45deg, transparent 50%, rgba(15, 23, 42, 0.45) 50%),
linear-gradient(135deg, rgba(15, 23, 42, 0.45) 50%, transparent 50%);
background-position:
calc(100% - 14px) calc(50% - 2px),
calc(100% - 9px) calc(50% - 2px);
background-size: 5px 5px, 5px 5px;
calc(100% - 10px) calc(50% - 1.5px),
calc(100% - 6px) calc(50% - 1.5px);
background-size: 4px 4px, 4px 4px;
background-repeat: no-repeat;
}
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select option {
background-color: #ffffff;
body.theme-ds-body[data-admin-theme="light"] .chat-model-select-pop {
background: #ffffff;
border-color: rgba(15, 23, 42, 0.12);
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
}
body.theme-ds-body[data-admin-theme="light"] .chat-model-select-pop .chat-sess-menu-item--active {
background: rgba(37, 99, 235, 0.12);
color: #0f172a;
}
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip:hover,
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select:hover {
background-color: #f8fafc;
border-color: rgba(37, 99, 235, 0.35);
background-color: rgba(15, 23, 42, 0.05);
color: #0f172a;
}
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip:focus-visible,
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select:focus-visible {
background-color: rgba(37, 99, 235, 0.1);
color: #0f172a;
box-shadow: none;
}
body.theme-ds-body[data-admin-theme="light"] .chat-composer-shell {