feat(thinking): 思考等级扩展五档(Max/XHigh)+ 按默认模型 support_efforts 能力感知;删除过时的 max→high 迁移;Segmented 支持逐选项禁用/tooltip — bump to v0.7.23
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s

This commit is contained in:
KimiSwitch Dev committed 2026-10-02 13:00:35 +08:00
1 parent 50f59cac79
commit 26cb88ad0e
17 files changed
+453 -53

No files matched your search

+80 -14
View File
@@ -1,12 +1,20 @@
import { useEffect, useState } from "react";
import { useEffect, useReducer, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { useTranslation } from "../i18n";
import type { TranslationKey } from "../i18n/zh";
import { getAgentSettings, setAgentSettings } from "../lib/agent-settings";
import { getModelRef, modelsDevReady } from "../lib/models-dev";
import {
THINKING_EFFORTS,
readSupportEfforts,
resolveThinkingEffortSupport,
type EffortAvailability,
} from "../lib/thinking-efforts";
import type {
AgentSettings,
ExperimentalEnvStatus,
Hook,
Model,
PermissionRule,
} from "../types";
import { Card, Checkbox, NumberField, Segmented } from "./ui/controls";
@@ -14,14 +22,18 @@ import { Card, Checkbox, NumberField, Segmented } from "./ui/controls";
interface AgentSettingsPanelProps {
rawOther: unknown;
onChange: (nextRawOther: unknown) => void;
/** Configured models (alias → entry) — used to resolve effort support. */
models?: Record<string, Model>;
/** Alias of the model the effort tier actually applies to. */
defaultModel?: string | null;
}
// Upstream removed the "max" effort tier (auto-migrates to "high").
const THINKING_LEVELS = ["low", "medium", "high"] as const;
const THINKING_LABELS: Record<(typeof THINKING_LEVELS)[number], TranslationKey> = {
const THINKING_LABELS: Record<(typeof THINKING_EFFORTS)[number], TranslationKey> = {
low: "thinkingLow",
medium: "thinkingMedium",
high: "thinkingHigh",
max: "thinkingMax",
xhigh: "thinkingXHigh",
};
const PERMISSION_DECISIONS = ["allow", "deny", "ask"] as const;
@@ -44,7 +56,7 @@ const COMMON_EVENTS = [
"SessionEnd",
] as const;
export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelProps) {
export function AgentSettingsPanel({ rawOther, onChange, models, defaultModel }: AgentSettingsPanelProps) {
const { t } = useTranslation();
const settings = getAgentSettings(rawOther);
/**
@@ -52,6 +64,9 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
* v1-engine users; the default (v2) writes only max_attempts_per_step.
*/
const [legacyV1, setLegacyV1] = useState(false);
// The models.dev index loads in the background; re-render once it lands so
// the reasoning flag can disable the thinking area.
const [, forceModelsDevReady] = useReducer((x: number) => x + 1, 0);
useEffect(() => {
invoke<ExperimentalEnvStatus>("get_experimental_env_status")
@@ -62,6 +77,18 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
.catch(() => setLegacyV1(false));
}, []);
useEffect(() => {
// The panel only needs the flag once; App owns the permanent
// onModelsDevReady listener for later hot swaps.
let alive = true;
modelsDevReady().then(() => {
if (alive) forceModelsDevReady();
});
return () => {
alive = false;
};
}, []);
const update = (patch: Partial<AgentSettings>) => {
onChange(setAgentSettings(rawOther, patch, { legacyV1 }));
};
@@ -92,6 +119,42 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
const thinkingEnabled = settings.thinking?.enabled ?? true;
// Which tiers the current default model accepts: `[models."<alias>"]
// support_efforts` first, then the models.dev `reasoning` flag.
const defaultEntry = defaultModel ? models?.[defaultModel] : undefined;
const effortSupport = resolveThinkingEffortSupport({
supportEfforts: readSupportEfforts(defaultEntry),
reasoning: defaultEntry ? getModelRef(defaultEntry.model)?.reasoning : undefined,
});
const effortNote = (availability: EffortAvailability): string | undefined => {
const note = availability.note;
if (!note) return undefined;
if (note.kind === "thinkingUnsupported") return t("thinkingEffortUnsupported");
if (note.kind === "tierUnsupported") {
return t("thinkingEffortTierUnsupported", { levels: note.supported.join(", ") });
}
return t("thinkingEffortTierUndeclared");
};
// Upstream takes a free-form string: keep a hand-written tier visible and
// selectable instead of dropping it from the control.
const effort = settings.thinking?.effort ?? "medium";
const effortOptions: {
key: string;
label: string;
disabled?: boolean;
title?: string;
}[] = THINKING_EFFORTS.map((tier) => ({
key: tier,
label: t(THINKING_LABELS[tier]),
disabled: !effortSupport.levels[tier].enabled,
title: effortNote(effortSupport.levels[tier]),
}));
if (!THINKING_EFFORTS.includes(effort as (typeof THINKING_EFFORTS)[number])) {
effortOptions.push({ key: effort, label: effort });
}
return (
<div className="mt-6 space-y-4">
<h3 className="text-content-muted text-sm font-medium">{t("agentSettings")}</h3>
@@ -105,17 +168,20 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
<div className="flex items-center gap-3 flex-wrap">
<span className="text-sm text-content-muted">{t("thinkingLevel")}</span>
<Segmented
options={THINKING_LEVELS.map((lvl) => ({
key: lvl,
label: t(THINKING_LABELS[lvl]),
}))}
value={settings.thinking?.effort ?? "medium"}
onChange={(effort) =>
updateThinking({ effort: effort as NonNullable<AgentSettings["thinking"]>["effort"] })
}
disabled={!thinkingEnabled}
options={effortOptions}
value={effort}
onChange={(next) => updateThinking({ effort: next })}
disabled={!thinkingEnabled || !effortSupport.thinkingSupported}
/>
</div>
{!effortSupport.thinkingSupported && (
<p className="text-xs text-amber-500 dark:text-amber-400">
{t("thinkingEffortUnsupported")}
</p>
)}
<p className="text-xs text-content-muted">
{t("thinkingEffortDependsOnDefaultModel")}
</p>
<Checkbox
label={t("thinkingKeep")}
checked={settings.thinking?.keep === "all"}
+10 -1
View File
@@ -1,4 +1,4 @@
import { useEffect, useId, useState } from "react";
import { useEffect, useId, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { invoke } from "@tauri-apps/api/core";
import { useTranslation } from "../i18n";
@@ -142,6 +142,13 @@ export function ProviderEdit({
const apiKeyEnvSupported = agent === "kimi_code";
const apiKeyEnvMode = apiKeyEnvSupported && provider.api_key_env != null;
// Alias → entry lookup for panels that resolve a model by alias
// (AgentSettingsPanel reads the default model's support_efforts).
const modelsByAlias = useMemo(
() => Object.fromEntries(models.map((m) => [m.alias, m])),
[models]
);
useEffect(() => {
const def = defaultBaseUrl(agent, provider.provider_type);
if (!def) return;
@@ -462,6 +469,8 @@ export function ProviderEdit({
<AgentSettingsPanel
rawOther={rawOther}
onChange={onRawOtherChange}
models={modelsByAlias}
defaultModel={defaultModel}
/>
)}
</>
+7 -5
View File
@@ -43,12 +43,15 @@ interface SubagentSettingsPageProps {
onBack: () => void;
}
// Upstream removed the "max" effort tier (auto-migrates to "high").
const EFFORTS = ["low", "medium", "high"] as const;
// Effort tiers the CLI accepts; the value is forwarded verbatim upstream, and
// per-model support comes from `[models."<alias>"] support_efforts`.
const EFFORTS = ["low", "medium", "high", "max", "xhigh"] as const;
const EFFORT_LABELS: Record<(typeof EFFORTS)[number], TranslationKey> = {
low: "thinkingLow",
medium: "thinkingMedium",
high: "thinkingHigh",
max: "thinkingMax",
xhigh: "thinkingXHigh",
};
const FLAG_LABELS: Record<string, { name: TranslationKey; desc: TranslationKey }> = {
@@ -281,9 +284,8 @@ export function SubagentSettingsPage({
? `${Math.round(n / 1000)}K`
: String(n);
const effortLabel = (e: string): string => {
// Stored "max" tiers from old configs are shown as "high" (upstream
// removed the tier; it auto-migrates to "high").
const key = EFFORT_LABELS[e === "max" ? "high" : (e as (typeof EFFORTS)[number])];
// Unknown values (hand-written config) are shown verbatim.
const key = EFFORT_LABELS[e as (typeof EFFORTS)[number]];
return key ? t(key) : e;
};
+22 -16
View File
@@ -106,7 +106,7 @@ export function Segmented({
onChange,
disabled,
}: {
options: { key: string; label: string }[];
options: { key: string; label: string; disabled?: boolean; title?: string }[];
value: string;
onChange: (value: string) => void;
disabled?: boolean;
@@ -117,21 +117,27 @@ export function Segmented({
disabled ? "opacity-50" : ""
}`}
>
{options.map((opt) => (
<button
key={opt.key}
type="button"
disabled={disabled}
onClick={() => onChange(opt.key)}
className={`px-3 py-1 text-sm ${
value === opt.key
? "bg-blue-600 text-white"
: "bg-input text-content-muted hover:bg-hover-2"
}`}
>
{opt.label}
</button>
))}
{options.map((opt) => {
const optDisabled = disabled || opt.disabled === true;
return (
// The title lives on the wrapper: a disabled button swallows pointer
// events in Chromium, so its own tooltip would never show.
<span key={opt.key} title={opt.title} className="inline-flex">
<button
type="button"
disabled={optDisabled}
onClick={() => onChange(opt.key)}
className={`px-3 py-1 text-sm ${
value === opt.key
? "bg-blue-600 text-white"
: "bg-input text-content-muted hover:bg-hover-2"
} ${optDisabled ? "cursor-not-allowed opacity-40 hover:bg-input" : ""}`}
>
{opt.label}
</button>
</span>
);
})}
</div>
);
}