diff --git a/packages/docs/content/models.en.md b/packages/docs/content/models.en.md
index 4fb5e61..f065c3c 100644
--- a/packages/docs/content/models.en.md
+++ b/packages/docs/content/models.en.md
@@ -77,7 +77,7 @@ Some models in the preset catalog: deepseek-v4-pro / deepseek-v4-flash, gemini-3
## Thinking levels
-Five levels: `none | low | medium | high | xhigh`, configured per Agent as `model.thinking_level` in `system_config.yaml`, default medium. The chat draft view offers a quick picker next to the model selector: a picked level is written back to the selected Agent's setting immediately (the switched-to level becomes that Agent's new default and applies from the next session; a running session keeps the level it was created with). See [Configuration](/configuration).
+Five levels: `none | low | medium | high | xhigh`, configured per Agent as `model.thinking_level` in `system_config.yaml`, default medium. The Web pickers offer `low` and above only (many models cannot disable thinking; `none` stays a valid stored value and still displays). The chat draft view offers a quick picker next to the model selector: a picked level is written back to the selected Agent's setting immediately (the switched-to level becomes that Agent's new default and applies from the next session; a running session keeps the level it was created with). See [Configuration](/configuration).
## Models decoupled from Agents
diff --git a/packages/docs/content/models.zh.md b/packages/docs/content/models.zh.md
index e296e56..c6578b6 100644
--- a/packages/docs/content/models.zh.md
+++ b/packages/docs/content/models.zh.md
@@ -77,7 +77,7 @@ api_key = "sk-..."
## 思考等级
-思考等级共五档:`none | low | medium | high | xhigh`,按 Agent 在 `system_config.yaml` 的 `model.thinking_level` 配置,默认 medium。对话草稿页在模型选择器旁提供快捷拾取器:选定档位立即写回所选 Agent 的该项配置(切换后的档位即成为该 Agent 的新默认,自下一个 Session 生效;进行中的 Session 沿用创建时的档位)。见 [配置参考](/configuration)。
+思考等级共五档:`none | low | medium | high | xhigh`,按 Agent 在 `system_config.yaml` 的 `model.thinking_level` 配置,默认 medium。Web 拾取器只提供 `low` 及以上档位(多数模型不支持关闭思考;`none` 仍是合法的已存值,能正常显示)。对话草稿页在模型选择器旁提供快捷拾取器:选定档位立即写回所选 Agent 的该项配置(切换后的档位即成为该 Agent 的新默认,自下一个 Session 生效;进行中的 Session 沿用创建时的档位)。见 [配置参考](/configuration)。
## 模型与 Agent 解耦
diff --git a/packages/web/e2e/draft.spec.mjs b/packages/web/e2e/draft.spec.mjs
index d127361..56b2fa7 100644
--- a/packages/web/e2e/draft.spec.mjs
+++ b/packages/web/e2e/draft.spec.mjs
@@ -87,14 +87,16 @@ test("draft: pick model/approval -> reload restores them -> send creates the ses
await expect(page.getByRole("button", { name: "审批模式" })).toContainText("放行只读");
// Conversation-time thinking level (backed by the Agent settings): the picker shows the
- // seeded default (medium, short name 中); the menu carries a title bar and exactly the five
- // short-name rows (no descriptions, no default row); picking 高 writes straight through to
- // the Agent config, so the session created on send runs with it and it becomes the Agent's
- // new default.
+ // seeded default (medium, short name 中); the menu carries a title bar and the short-name
+ // rows 低/中/高/极高 only — no descriptions, no default row, and no 无 (many models cannot
+ // disable thinking); picking 高 writes straight through to the Agent config, so the session
+ // created on send runs with it and it becomes the Agent's new default.
const thinkingBtn = page.getByRole("button", { name: "思考等级" });
await expect(thinkingBtn).toContainText("中");
await thinkingBtn.click();
await expect(page.getByText("思考等级", { exact: true })).toBeVisible(); // menu title bar
+ await expect(page.getByRole("button", { name: "低", exact: true })).toBeVisible();
+ await expect(page.getByRole("button", { name: "无", exact: true })).toHaveCount(0);
await page.getByRole("button", { name: "高", exact: true }).click();
await expect(thinkingBtn).toContainText("高");
await expect
diff --git a/packages/web/src/features/agents/agent-settings-page.tsx b/packages/web/src/features/agents/agent-settings-page.tsx
index bb68477..1d0702e 100644
--- a/packages/web/src/features/agents/agent-settings-page.tsx
+++ b/packages/web/src/features/agents/agent-settings-page.tsx
@@ -31,6 +31,7 @@ import { Modal } from "../../components/ui/modal";
import { Skeleton } from "../../components/ui/skeleton";
import { VaultTab } from "./vault-tab";
import { SchedulesTab } from "./schedules-tab";
+import { thinkingLevelOptionsFor } from "../chat/thinking-level";
type TabKey = "overview" | "prompt" | "runtime" | "tools" | "vault" | "schedules";
@@ -500,7 +501,16 @@ function RuntimeTab({ data, onSave }: { data: AgentConfigResponse; onSave: SaveF
};
// S is reassigned on language switch (live binding), so read it during render rather than hoisting to a module-level constant.
- const thinkingLevelOptions = withDefaultOption(S.agent.thinkingLevelOptions);
+ // "none" is no longer offered (many models cannot disable thinking) but stays a valid stored
+ // value: assembly lives in thinkingLevelOptionsFor, gated on the **persisted** config — a
+ // misclick onto another tier keeps the legacy row until the change is actually saved, so the
+ // stored value stays reachable (see thinking-level.ts).
+ const thinkingLevelOptions = thinkingLevelOptionsFor(
+ S.agent.thinkingLevelOptions,
+ S.agent.defaultValue,
+ S.agent.thinkingLevelNoneKept,
+ cfg.model?.thinkingLevel,
+ );
const compactionModeOptions = withDefaultOption(S.agent.compactionModeOptions);
return (
diff --git a/packages/web/src/features/chat/chat-input.tsx b/packages/web/src/features/chat/chat-input.tsx
index adb8ff6..6a4f0d4 100644
--- a/packages/web/src/features/chat/chat-input.tsx
+++ b/packages/web/src/features/chat/chat-input.tsx
@@ -64,7 +64,7 @@ import { ProviderLogo } from "../../components/ui/provider-logo";
import { hasConfiguredKey, sameModelRef, visibleChatModels } from "../models/model-grouping";
import { filterAgents, matchMention, splitLeadingMention } from "./agent-mentions";
import { matchSlash, removeSlashToken } from "./slash-token";
-import { THINKING_LEVELS, thinkingLevelLabel } from "./thinking-level";
+import { SELECTABLE_THINKING_LEVELS, thinkingLevelLabel } from "./thinking-level";
import {
BOOK_ICON,
buildSkillsMessage,
@@ -393,8 +393,10 @@ const SPARK_ICON = "M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12
* level straight through to the Agent settings — llmConfig is assembled once per session, so
* the level applies to the session created on first send and becomes the Agent's new default
* (switch-becomes-default). Per review: a title bar names the control, and the menu lists
- * exactly the five levels with short names only (no descriptions, no "default" row) — an
- * Agent without an explicit override shows an em dash until a level is picked.
+ * the selectable levels with short names only (no descriptions, no "default" row, and no
+ * "none" — many models cannot disable thinking); an Agent without an explicit override shows
+ * an em dash until a level is picked, and a stored legacy "none" still displays via the
+ * label table (just never offered).
*/
function ThinkingLevelSelect({
value,
@@ -449,7 +451,7 @@ function ThinkingLevelSelect({