fix(codex): keep the reasoning levels column from stretching the model catalog

Picking many reasoning levels for a Codex model catalog row pushed the
levels column wider, and the rows stopped lining up with each other and
with the header.

- Each catalog row is its own grid, and its `1fr` tracks could not shrink
  below their content, so a long label such as
  "none,low,medium,high,xhigh,max" widened its own row. The header and
  row templates now use `minmax(0,1fr)`, so every row splits the width the
  same way and the label's existing truncation takes effect.
- The trigger label collapses each run of three or more adjacent levels
  into "first → last": "low → max", or "none, low → max" when minimal is
  skipped. A range never spans a skipped level, so it cannot suggest a
  level that was not picked. Shorter runs, gapped picks such as
  "low, high, max" and lists that are not in canonical order are shown in
  full.
- Hovering or keyboard-focusing the trigger shows every picked level and,
  when one is set, the default level (new codexConfig.defaultReasoningLevelTip
  key in all four locales).
This commit is contained in:
Jason committed 2026-10-05 21:03:10 +08:00
1 parent 20222460a1
commit db8069ef7d
6 files changed
+119 -19

No files matched your search

@@ -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 ? (
<>
<div>{selected.join(", ")}</div>
{defaultLevel && (
<div>
{t("codexConfig.defaultReasoningLevelTip", {
level: defaultLevel,
defaultValue: "Default level: {{level}}",
})}
</div>
)}
</>
) : undefined;
return (
<Popover modal open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
role="combobox"
aria-expanded={open}
className="flex h-9 w-full items-center justify-between gap-1 rounded-md border border-border bg-surface px-3 py-1 text-sm shadow-sm focus:outline-none focus-visible:outline-none focus:border-border focus-visible:border-border focus:ring-0 focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50"
>
<span
className={cn("truncate", selected.length === 0 && "text-fg-2")}
<HoverTip content={triggerTip}>
<PopoverTrigger asChild>
<button
type="button"
role="combobox"
aria-expanded={open}
className="flex h-9 w-full items-center justify-between gap-1 rounded-md border border-border bg-surface px-3 py-1 text-sm shadow-sm focus:outline-none focus-visible:outline-none focus:border-border focus-visible:border-border focus:ring-0 focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50"
>
{triggerLabel}
</span>
<ChevronsUpDown className="h-3.5 w-3.5 shrink-0 opacity-50" />
</button>
</PopoverTrigger>
<span
className={cn("truncate", selected.length === 0 && "text-fg-2")}
>
{triggerLabel}
</span>
<ChevronsUpDown className="h-3.5 w-3.5 shrink-0 opacity-50" />
</button>
</PopoverTrigger>
</HoverTip>
<PopoverContent
side="bottom"
align="start"
@@ -1167,8 +1219,8 @@ export function CodexFormFields({
className={cn(
"hidden gap-2 px-1 text-xs font-medium text-fg-2 md:grid",
withDefault
? "grid-cols-[36px_1fr_1fr_140px_1fr_36px]"
: "grid-cols-[1fr_1fr_140px_1fr_36px]",
? "grid-cols-[36px_minmax(0,1fr)_minmax(0,1fr)_140px_minmax(0,1fr)_36px]"
: "grid-cols-[minmax(0,1fr)_minmax(0,1fr)_140px_minmax(0,1fr)_36px]",
)}
>
{withDefault && <span />}
@@ -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)}
+1
View File
@@ -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",
+1
View File
@@ -1824,6 +1824,7 @@
"reasoningLevelsEmpty": "レベルなし",
"defaultReasoningLevelLabel": "デフォルトレベル",
"defaultReasoningLevelPlaceholder": "自動",
"defaultReasoningLevelTip": "デフォルトレベル:{{level}}",
"reasoningModeToggle": "思考モードに対応",
"reasoningModeHint": "上流の Chat Completions API が思考(thinking)のオン/オフ切り替えに対応している場合に有効化します。Kimi、GLM、Qwen などが通常このタイプに該当します。",
"reasoningEffortToggle": "思考レベルに対応",
+1
View File
@@ -1817,6 +1817,7 @@
"reasoningLevelsEmpty": "無等級",
"defaultReasoningLevelLabel": "預設等級",
"defaultReasoningLevelPlaceholder": "自動",
"defaultReasoningLevelTip": "預設等級:{{level}}",
"catalogDisplayNamePlaceholder": "例如: DeepSeek V4 Flash",
"catalogModelPlaceholder": "例如: deepseek-v4-flash",
"contextWindow": "上下文視窗",
+1
View File
@@ -1824,6 +1824,7 @@
"reasoningLevelsEmpty": "无等级",
"defaultReasoningLevelLabel": "默认等级",
"defaultReasoningLevelPlaceholder": "自动",
"defaultReasoningLevelTip": "默认等级:{{level}}",
"reasoningModeToggle": "支持思考模式",
"reasoningModeHint": "上游 Chat Completions 接口支持开启或关闭 thinking 时启用。Kimi、GLM、Qwen 等通常属于这一类。",
"reasoningEffortToggle": "支持思考等级",
@@ -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",
);
});
});