diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index ff7c39d4e..4665bec57 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -232,6 +232,39 @@ const CODEX_REASONING_LEVELS = [ "ultra", ] as const; +// Trigger label for the picked levels, kept narrow for the catalog cell. Each +// run of three or more adjacent canonical levels collapses to "first → last" +// ("none, low → max" when minimal is skipped); a range never spans a gap, so +// it can't imply an unpicked level. Non-canonical order stays a plain list. +export function formatReasoningLevelsLabel(levels: string[]): string { + const canonical = CODEX_REASONING_LEVELS as readonly string[]; + const positions = levels.map((level) => canonical.indexOf(level)); + const ordered = positions.every( + (position, index) => + position >= 0 && (index === 0 || position > positions[index - 1]), + ); + if (!ordered) return levels.join(", "); + + const parts: string[] = []; + let runStart = 0; + for (let index = 1; index <= levels.length; index++) { + if ( + index < levels.length && + positions[index] === positions[index - 1] + 1 + ) { + continue; + } + const run = levels.slice(runStart, index); + if (run.length >= 3) { + parts.push(`${run[0]} → ${run[run.length - 1]}`); + } else { + parts.push(...run); + } + runStart = index; + } + return parts.join(", "); +} + // Sentinel for the default-level Select: Radix Select forbids empty item // values, so "back to Auto" needs a non-empty value mapped to undefined. const AUTO_DEFAULT_REASONING_LEVEL = "__auto__"; @@ -270,28 +303,47 @@ function ReasoningLevelsEditor({ const triggerLabel = selected.length > 0 - ? selected.join(",") + ? formatReasoningLevelsLabel(selected) : t("codexConfig.reasoningLevelsNotSet", { defaultValue: "Not set", }); + // The label may be a range or truncated, so hovering spells out every + // picked level plus the default one. + const triggerTip = + selected.length > 0 ? ( + <> +
{selected.join(", ")}
+ {defaultLevel && ( +
+ {t("codexConfig.defaultReasoningLevelTip", { + level: defaultLevel, + defaultValue: "Default level: {{level}}", + })} +
+ )} + + ) : undefined; + return ( - - - + + {triggerLabel} + + + + + {withDefault && } @@ -1201,8 +1253,8 @@ export function CodexFormFields({ className={cn( "grid grid-cols-1 gap-2", withDefault - ? "md:grid-cols-[36px_1fr_1fr_140px_1fr_36px]" - : "md:grid-cols-[1fr_1fr_140px_1fr_36px]", + ? "md:grid-cols-[36px_minmax(0,1fr)_minmax(0,1fr)_140px_minmax(0,1fr)_36px]" + : "md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_140px_minmax(0,1fr)_36px]", )} > {withDefault && renderDefaultStar(index)} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index edc900639..f3b87e927 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1824,6 +1824,7 @@ "reasoningLevelsEmpty": "No levels", "defaultReasoningLevelLabel": "Default level", "defaultReasoningLevelPlaceholder": "Auto", + "defaultReasoningLevelTip": "Default level: {{level}}", "reasoningModeToggle": "Supports Thinking Mode", "reasoningModeHint": "Enable when the upstream Chat Completions API supports toggling thinking on/off. Providers like Kimi, GLM and Qwen usually fall into this category.", "reasoningEffortToggle": "Supports Reasoning Effort", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 2d83dc3d7..534e72cc3 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1824,6 +1824,7 @@ "reasoningLevelsEmpty": "レベルなし", "defaultReasoningLevelLabel": "デフォルトレベル", "defaultReasoningLevelPlaceholder": "自動", + "defaultReasoningLevelTip": "デフォルトレベル:{{level}}", "reasoningModeToggle": "思考モードに対応", "reasoningModeHint": "上流の Chat Completions API が思考(thinking)のオン/オフ切り替えに対応している場合に有効化します。Kimi、GLM、Qwen などが通常このタイプに該当します。", "reasoningEffortToggle": "思考レベルに対応", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 1671f1d0a..fb0eb4d9d 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -1817,6 +1817,7 @@ "reasoningLevelsEmpty": "無等級", "defaultReasoningLevelLabel": "預設等級", "defaultReasoningLevelPlaceholder": "自動", + "defaultReasoningLevelTip": "預設等級:{{level}}", "catalogDisplayNamePlaceholder": "例如: DeepSeek V4 Flash", "catalogModelPlaceholder": "例如: deepseek-v4-flash", "contextWindow": "上下文視窗", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index bc50c9cfc..616e37e30 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1824,6 +1824,7 @@ "reasoningLevelsEmpty": "无等级", "defaultReasoningLevelLabel": "默认等级", "defaultReasoningLevelPlaceholder": "自动", + "defaultReasoningLevelTip": "默认等级:{{level}}", "reasoningModeToggle": "支持思考模式", "reasoningModeHint": "上游 Chat Completions 接口支持开启或关闭 thinking 时启用。Kimi、GLM、Qwen 等通常属于这一类。", "reasoningEffortToggle": "支持思考等级", diff --git a/tests/components/codexReasoningLevelsLabel.test.ts b/tests/components/codexReasoningLevelsLabel.test.ts new file mode 100644 index 000000000..0fcf178a2 --- /dev/null +++ b/tests/components/codexReasoningLevelsLabel.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { formatReasoningLevelsLabel } from "@/components/providers/forms/CodexFormFields"; + +describe("formatReasoningLevelsLabel", () => { + it("collapses a run of three or more adjacent levels into a range", () => { + expect( + formatReasoningLevelsLabel(["low", "medium", "high", "xhigh", "max"]), + ).toBe("low → max"); + }); + + it("collapses each run separately, so a skipped level stays visible", () => { + // minimal sits between none and low: none must not join the low run. + expect( + formatReasoningLevelsLabel([ + "none", + "low", + "medium", + "high", + "xhigh", + "max", + ]), + ).toBe("none, low → max"); + expect(formatReasoningLevelsLabel(["none", "low", "medium", "high"])).toBe( + "none, low → high", + ); + }); + + it("lists short runs and gapped picks as they are", () => { + expect(formatReasoningLevelsLabel(["high"])).toBe("high"); + expect(formatReasoningLevelsLabel(["none", "high"])).toBe("none, high"); + expect(formatReasoningLevelsLabel(["low", "high", "max"])).toBe( + "low, high, max", + ); + expect(formatReasoningLevelsLabel(["low", "medium", "xhigh"])).toBe( + "low, medium, xhigh", + ); + }); + + it("keeps a non-canonical order as a plain list", () => { + expect(formatReasoningLevelsLabel(["high", "medium", "low"])).toBe( + "high, medium, low", + ); + }); +});