feat(web): drop "none" from the offered thinking levels (#34)

Co-authored-by: Alice <alice@prismshadow.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-07-23 00:35:37 +08:00
1 parent 1514cd66b2
commit 78a8b35793
9 files changed
+141 -26

No files matched your search

@@ -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 (
@@ -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({
<div className="border-b border-gray-100 px-3 pb-1.5 pt-0.5 text-xs font-semibold text-gray-500 dark:border-gray-800 dark:text-gray-400">
{S.chat.thinkingLevel}
</div>
{THINKING_LEVELS.map((level) => (
{SELECTABLE_THINKING_LEVELS.map((level) => (
<button
key={level}
type="button"
@@ -5,18 +5,29 @@
* it shows the selected Agent's current level and writes a picked level straight through
* to the Agent config, so the session created on first send — which reads systemConfig
* fresh — runs with it, and it becomes the Agent's new default (switch-becomes-default).
* Per review: the menu lists exactly the five levels with short names only (no
* descriptions, no "default" row) under a title bar naming the control.
* Per review: the menu lists the levels with short names only (no descriptions, no
* "default" row) under a title bar naming the control — and it does **not** offer "none"
* (many models cannot disable thinking): "none" stays a valid stored/wire value, so a
* legacy config or session that carries it still displays via the label table below.
*/
/** The five levels, in menu order (mirrors core's ThinkingLevelName). */
/** All five stored/wire levels, for display lookup (mirrors core's ThinkingLevelName). */
export const THINKING_LEVELS = ["none", "low", "medium", "high", "xhigh"] as const;
/**
* The levels the picker offers, in menu order: "none" is deliberately excluded (many models
* don't support disabling thinking) — it can still be displayed (a stored legacy value) but
* never picked.
*/
export const SELECTABLE_THINKING_LEVELS = ["low", "medium", "high", "xhigh"] as const;
/**
* Short display label for a level from the localized name table (S.chat.thinkingLevelNames).
* Returns null for anything outside the five levels — including "" (an Agent without an
* explicit override) and session_meta's "default" — so callers can render a placeholder on
* the trigger and hide the session read-only tag instead of showing a raw internal value.
* Covers all five stored levels including "none" (so a legacy value displays sanely instead
* of being rewritten or hidden). Returns null for anything else — including "" (an Agent
* without an explicit override) and session_meta's "default" — so callers can render a
* placeholder on the trigger and hide the session read-only tag instead of showing a raw
* internal value.
*/
export function thinkingLevelLabel(
names: Readonly<Record<string, string>>,
@@ -26,3 +37,46 @@ export function thinkingLevelLabel(
? (names[level] ?? level)
: null;
}
/** One dropdown row of the agent-settings thinking-level menu (shape of OptionMenuChoice<string>). */
export interface ThinkingLevelOptionRow {
value: string;
/** Compact text on the trigger button ("" renders the localized default tag). */
triggerLabel: string;
/** Panel row title. */
label: string;
/** Panel row description. */
description: string;
}
/**
* Assembles the agent-settings thinking-level dropdown rows from the dictionary's
* [value, description] pairs (which no longer include `none`). Backward compatibility:
* when the **persisted** config already stores `none` (a legacy value), a display-only
* row for it is appended — the trigger shows the real stored state instead of "—" and
* nothing is silently rewritten. Gating on the persisted value (not the local edit
* state) means a misclick onto another tier keeps the row until the change is actually
* saved, so the user can always click back to the value still stored on disk.
*/
export function thinkingLevelOptionsFor(
options: ReadonlyArray<readonly [string, string]>,
defaultTag: string,
noneKeptDescription: string,
storedLevel: string | undefined,
): ThinkingLevelOptionRow[] {
const rows = options.map(([value, description]) => ({
value,
triggerLabel: value || defaultTag,
label: value || defaultTag,
description,
}));
if (storedLevel === "none") {
rows.push({
value: "none",
triggerLabel: "none",
label: "none",
description: noneKeptDescription,
});
}
return rows;
}
+2 -1
View File
@@ -197,7 +197,6 @@ export const en: Strings = {
thinkingLevel: "model.thinking_level",
thinkingLevelOptions: [
["", "Send no override — keep whatever is currently configured."],
["none", "Disables extended reasoning; the fastest responses."],
["low", "Enables a lower tier of extended reasoning."],
[
"medium",
@@ -209,6 +208,8 @@ export const en: Strings = {
"Enables the highest tier of extended reasoning; identical to high on some models.",
],
] as ReadonlyArray<readonly [string, string]>,
thinkingLevelNoneKept:
"Stored legacy tier: new selections no longer offer the off tier (many models cannot disable thinking).",
timeoutMs: "model.timeoutMs",
timeoutMsHint: "Per-request timeout, ms",
compaction: "Context compaction",
+4 -2
View File
@@ -188,14 +188,16 @@ export const zh = {
maxTurns: "max_turns(单 Task 最大轮次,-1 不限制)",
maxTokens: "model.max_tokens",
thinkingLevel: "model.thinking_level",
/** Selectable tiers exclude `none` (many models cannot disable thinking); a stored `none` still displays — see `thinkingLevelNoneKept`. */
thinkingLevelOptions: [
["", "不提交覆盖值,沿用当前生效的配置。"],
["none", "关闭扩展推理,响应最快。"],
["low", "开启较低强度的扩展推理。"],
["medium", "开启中等强度的扩展推理(新建 Agent 的缺省档位)。"],
["high", "开启较高强度的扩展推理,响应更慢。"],
["xhigh", "开启最高强度的扩展推理,部分模型上效果与 high 相同。"],
] as ReadonlyArray<readonly [string, string]>,
/** Row description shown only while the stored config is `none`: displayed as-is, never rewritten, and no longer offered as a choice. */
thinkingLevelNoneKept: "已存的历史档位:新选择不再提供关闭档(多数模型不支持关闭思考)。",
timeoutMs: "model.timeoutMs",
timeoutMsHint: "单次 Request 超时,毫秒",
compaction: "上下文压缩(compaction)",
@@ -463,7 +465,7 @@ export const zh = {
chooseAgent: "选择 Agent",
chooseModel: "选择模型",
thinkingLevel: "思考等级",
/** 会话前拾取器的档位短名(评审要求:只写短名、不带说明、无“缺省”项)。 */
/** Short tier names for the pre-conversation picker (per review: short names only, no descriptions, no "default" row). `none` exists purely to display a stored legacy value — it is never offered as a choice (many models cannot disable thinking). */
thinkingLevelNames: {
none: "无",
low: "低",