feat(models): add toggle to hide chat model selector

Expose a model-management setting to control chat-page model dropdown visibility, persist it in app settings, and apply it in chat UI rendering.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-05-06 18:58:01 +08:00
parent 61d2b87a30
commit 6628cd1220
3 changed files with 52 additions and 0 deletions

View file

@ -33,6 +33,16 @@ _LLM_MODE_OPTIONS = frozenset({"openai", "openai_responses", "anthropic", "googl
_LLM_USER_CREATE_MODES = frozenset({"openai", "anthropic", "google", "ollama", "rule"})
_OPS_AI_ACTIVE_KEY = "ops_ai_active_llm_profile_id"
_OPS_AI_BINDINGS_KEY = "ops_ai_agent_profile_bindings"
_CHAT_MODEL_SELECTOR_VISIBLE_KEY = "AIA_CHAT_MODEL_SELECTOR_VISIBLE"
def _truthy(v: Any, *, default: bool = False) -> bool:
if v is None:
return bool(default)
s = str(v).strip().lower()
if not s:
return bool(default)
return s in {"1", "true", "yes", "on"}
def _require_permission(ctx: dict[str, Any], permission: str) -> None:
@ -188,6 +198,7 @@ def include_model_mgmt_routes(
"role_ids": list(agent_role_ids()),
"profile_secret": secret,
"can_manage_llm_grants": _can_manage_llm_grants(ctx),
"chat_model_selector_visible": _truthy(store.get_setting(_CHAT_MODEL_SELECTOR_VISIBLE_KEY), default=True),
# 便于核对「浏览器连的是哪台网关、网关读的是哪个库文件」
"db_path": db_path(),
}
@ -294,6 +305,19 @@ def include_model_mgmt_routes(
store.set_setting(_OPS_AI_BINDINGS_KEY, dump_agent_profile_bindings(cur))
return {"ok": True, "ops_ai_bindings": cur}
@mg.post("/chat-ui")
def api_models_set_chat_ui(
payload: dict[str, Any] | None = Body(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
ctx = resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
visible = _truthy(payload.get("chat_model_selector_visible"), default=True)
store.set_setting(_CHAT_MODEL_SELECTOR_VISIBLE_KEY, "1" if visible else "0")
return {"ok": True, "chat_model_selector_visible": visible}
@mg.post("/profiles")
def api_models_create_profile(
payload: dict[str, Any] | None = Body(default=None),

View file

@ -322,6 +322,8 @@ const I18N = {
"models.sectionBindings": "智能体与模型绑定",
"models.sectionNew": "新建模型配置",
"models.sectionApi": "API 与密钥",
"models.chatModelSelector": "聊天页模型选择器",
"models.chatModelSelectorHint": "控制 Chat 页底部的模型下拉是否显示。",
"models.sectionEval": "Agent Evaluation",
"models.pickModel": "选用配置",
"models.agentBindingHelp": "可为不同角色指定模型配置;留空则跟随上方全局选用。",
@ -763,6 +765,8 @@ const I18N = {
"models.sectionBindings": "Agent ↔ profile bindings",
"models.sectionNew": "New profile",
"models.sectionApi": "API & secret",
"models.chatModelSelector": "Chat model selector",
"models.chatModelSelectorHint": "Control whether the model dropdown is visible on the Chat page.",
"models.sectionEval": "Agent Evaluation",
"models.pickModel": "Profile",
"models.agentBindingHelp": "Optional per-role profile; leave empty to use the global selection above.",
@ -3460,6 +3464,7 @@ async function renderApiGrants() {
async function renderModels() {
const canPickActive = hasPermission("admin:read");
const canConfigureBindings = canPickActive;
const canConfigureChatUi = hasPermission("admin:tenant:write");
const status = el("div", { class: "muted", text: "" });
const isAdminPool = String((authSession && authSession.username) || "").trim().toLowerCase() === "administrator";
// 与后端 _require_models_mutate 对齐:administrator 写全局池需 tenant:write;普通用户写自己的配置只需 admin:read
@ -3469,6 +3474,7 @@ async function renderModels() {
text: isAdminPool && !canMutateProfiles ? t("models.readonly") : "",
});
const activeSelect = el("select", { class: "input", disabled: !canPickActive });
const chatModelSelectorVisibleCb = el("input", { type: "checkbox", disabled: !canConfigureChatUi });
const bindingWrap = el("div", {});
const opsAiSpecialistSelect = el("select", { class: "input", disabled: !canConfigureBindings });
const opsAiProfileSelect = el("select", { class: "input", disabled: !canConfigureBindings });
@ -3882,6 +3888,8 @@ async function renderModels() {
const aid = String(state.active_llm_profile_id || "");
if (profiles.some((p) => String(p.id) === aid)) activeSelect.value = aid;
activeSelect.disabled = !canPickActive;
chatModelSelectorVisibleCb.disabled = !canConfigureChatUi;
chatModelSelectorVisibleCb.checked = state.chat_model_selector_visible !== false;
opsAiSpecialistSelect.disabled = !canConfigureBindings;
opsAiProfileSelect.disabled = !canConfigureBindings;
@ -3963,6 +3971,17 @@ async function renderModels() {
status.textContent = String(e.message || e);
}
});
chatModelSelectorVisibleCb.addEventListener("change", async () => {
if (!canConfigureChatUi) return;
try {
await apiPost("/admin/api/models/chat-ui", {
chat_model_selector_visible: !!chatModelSelectorVisibleCb.checked,
});
await refresh();
} catch (e) {
status.textContent = String(e.message || e);
}
});
opsAiSpecialistSelect.addEventListener("change", async () => {
paintOpsAiBindings();
});
@ -4206,6 +4225,8 @@ async function renderModels() {
el("div", { class: "page-grid page-grid--two" }, [
renderSectionCard(t("models.sectionActive"), "", [
el("div", { class: "row" }, [el("label", { text: t("models.pickModel") }), activeSelect]),
el("div", { class: "row" }, [el("label", { text: t("models.chatModelSelector") }), chatModelSelectorVisibleCb]),
el("div", { class: "muted", text: t("models.chatModelSelectorHint") }),
], { id: "models-overview" }),
renderSectionCard(t("models.sectionNew"), "", [
el("div", { class: "row" }, [el("label", { text: t("models.profileName") }), newName]),

View file

@ -2845,9 +2845,15 @@ async function renderChatUi() {
};
await loadSpecialistCatalog();
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;
};
const refreshActiveModelText = async () => {
try {
const ms = await apiGet("/admin/api/models");
applyModelSelectorVisibility(ms);
const profiles = Array.isArray(ms.profiles) ? ms.profiles : [];
const aid = String(ms.active_llm_profile_id || "");
const p = profiles.find((x) => String(x.id || "") === aid) || null;
@ -2883,6 +2889,7 @@ async function renderChatUi() {
modelSelect.value = String(modelSelect.options[0].value || "");
}
} catch (_) {
applyModelSelectorVisibility(null);
activeModelText.textContent = `${t("chat.activeModelLabel")}: -`;
modelSelect.innerHTML = "";
modelSelect.appendChild(el("option", { value: "", text: "-" }));