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
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:
1 parent
50f59cac79
commit
26cb88ad0e
17 files changed
+453
-53
No files matched your search
@@ -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"}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user