Files
KimiCodeSwitch/src/components/AgentSettingsPanel.tsx
T
KimiSwitch Dev fcd08eae32
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
feat: 阈值预警/加力钱包/api_key_env/版本跟踪器/一键体检 — bump to v0.7.20
2026-09-20 09:49:49 +08:00

368 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, 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 type {
AgentSettings,
ExperimentalEnvStatus,
Hook,
PermissionRule,
} from "../types";
import { Card, Checkbox, NumberField, Segmented } from "./ui/controls";
interface AgentSettingsPanelProps {
rawOther: unknown;
onChange: (nextRawOther: unknown) => void;
}
// 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> = {
low: "thinkingLow",
medium: "thinkingMedium",
high: "thinkingHigh",
};
const PERMISSION_DECISIONS = ["allow", "deny", "ask"] as const;
const PERMISSION_LABELS: Record<(typeof PERMISSION_DECISIONS)[number], TranslationKey> = {
allow: "permissionAllow",
deny: "permissionDeny",
ask: "permissionAsk",
};
// kimi-code 0.33+ added TurnStarted / UserPromptQueued / TaskStarted /
// SessionHeartbeat hook events (plus SessionStart / SessionEnd).
const COMMON_EVENTS = [
"PreToolUse",
"PostToolUse",
"TurnStarted",
"UserPromptQueued",
"TaskStarted",
"SessionHeartbeat",
"SessionStart",
"SessionEnd",
] as const;
export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelProps) {
const { t } = useTranslation();
const settings = getAgentSettings(rawOther);
/**
* KIMI_CODE_LEGACY_FLAG=1 keeps the v1 loop_control keys dual-written for
* v1-engine users; the default (v2) writes only max_attempts_per_step.
*/
const [legacyV1, setLegacyV1] = useState(false);
useEffect(() => {
invoke<ExperimentalEnvStatus>("get_experimental_env_status")
.then((env) => {
const value = env?.KIMI_CODE_LEGACY_FLAG ?? "";
setLegacyV1(["1", "true", "yes", "on"].includes(value.trim().toLowerCase()));
})
.catch(() => setLegacyV1(false));
}, []);
const update = (patch: Partial<AgentSettings>) => {
onChange(setAgentSettings(rawOther, patch, { legacyV1 }));
};
const updateThinking = (patch: Partial<AgentSettings["thinking"]>) => {
update({ thinking: { ...settings.thinking, ...patch } });
};
const updateLoopControl = (patch: Partial<AgentSettings["loop_control"]>) => {
update({ loop_control: { ...settings.loop_control, ...patch } });
};
const updateBackground = (patch: Partial<AgentSettings["background"]>) => {
update({ background: { ...settings.background, ...patch } });
};
const updateWatch = (patch: Partial<AgentSettings["watch"]>) => {
update({ watch: { ...settings.watch, ...patch } });
};
const setRules = (rules: PermissionRule[]) => {
update({ permission: { rules } });
};
const setHooks = (hooks: Hook[]) => {
update({ hooks });
};
const thinkingEnabled = settings.thinking?.enabled ?? true;
return (
<div className="mt-6 space-y-4">
<h3 className="text-content-muted text-sm font-medium">{t("agentSettings")}</h3>
<Card title={t("enableThinking")}>
<Checkbox
label={t("enableThinking")}
checked={thinkingEnabled}
onChange={(checked) => updateThinking({ enabled: checked })}
/>
<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}
/>
</div>
<Checkbox
label={t("thinkingKeep")}
checked={settings.thinking?.keep === "all"}
disabled={!thinkingEnabled}
onChange={(checked) => updateThinking({ keep: checked ? "all" : "off" })}
/>
<p className="text-xs text-content-muted">{t("thinkingContextHint")}</p>
</Card>
<Card title={t("loopControlSettings")}>
<NumberField
label={t("maxAttemptsPerStep")}
value={
settings.loop_control?.max_attempts_per_step ??
settings.loop_control?.max_retries_per_step ??
3
}
onChange={(v) =>
updateLoopControl({
max_attempts_per_step: v,
max_retries_per_step: v,
})
}
/>
<NumberField
label={t("reservedContextSize")}
value={settings.loop_control?.reserved_context_size ?? 50000}
onChange={(v) => updateLoopControl({ reserved_context_size: v })}
/>
{/* kimi-code 0.43.0+ `loop_control.compaction_max_attempts`. The
upstream default is 5; an empty field or 0 removes the key instead
of writing a literal 0 (see setAgentSettings). */}
<NumberField
label={t("compactionMaxAttempts")}
value={settings.loop_control?.compaction_max_attempts ?? 5}
onChange={(v) => updateLoopControl({ compaction_max_attempts: v })}
/>
<p className="text-xs text-content-muted">{t("compactionMaxAttemptsHint")}</p>
</Card>
<Card title={t("backgroundSettings")}>
<NumberField
label={t("maxRunningTasks")}
value={settings.background?.max_running_tasks}
onChange={(v) => updateBackground({ max_running_tasks: v })}
/>
<Checkbox
label={t("keepAliveOnExit")}
checked={settings.background?.keep_alive_on_exit ?? false}
onChange={(checked) => updateBackground({ keep_alive_on_exit: checked })}
/>
</Card>
<Card title={t("watchSettings")}>
{/* Top-level `[watch] enabled` (kimi-code 2.0.1+; off by default since
2.0.2). KIMI_CODE_WATCH outranks this config at runtime — the env
var is probed by get_experimental_env_status as a non-flag entry
but deliberately locks nothing here, the toggle just documents the
config value that applies when the env var is unset. */}
<Checkbox
label={t("watchEnabled")}
checked={settings.watch?.enabled ?? false}
onChange={(checked) => updateWatch({ enabled: checked })}
/>
<p className="text-xs text-content-muted">{t("watchEnabledDesc")}</p>
</Card>
<Card title={t("permissionRules")}>
{/* kimi-code `[permission] dangerous_command_guard`. The env
var KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false")
outranks this config at runtime; no env-lock UI here (deliberate
minimal scope). Absent key = upstream default on. Since
kimi-code 0.41.0 the guard only gates Manual/YOLO modes —
Auto (never-ask) mode no longer intercepts dangerous
commands. */}
<Checkbox
label={t("permissionDangerousGuard")}
checked={settings.permission?.dangerous_command_guard ?? true}
onChange={(checked) =>
update({
permission: {
...settings.permission,
dangerous_command_guard: checked,
},
})
}
/>
<p className="text-xs text-content-muted">
{t("permissionDangerousGuardDesc")}
</p>
<div className="space-y-2">
{(settings.permission?.rules ?? []).map((rule, idx) => (
<div key={idx} className="flex items-center gap-2">
<select
className="bg-input border border-border rounded px-2 py-1 text-sm"
value={rule.decision ?? "allow"}
onChange={(e) => {
const rules = [...(settings.permission?.rules ?? [])];
rules[idx] = {
...rule,
decision: e.target.value as PermissionRule["decision"],
};
setRules(rules);
}}
>
{PERMISSION_DECISIONS.map((d) => (
<option key={d} value={d}>
{t(PERMISSION_LABELS[d])}
</option>
))}
</select>
<input
type="text"
className="flex-1 min-w-0 bg-input border border-border rounded px-2 py-1 text-sm"
value={rule.pattern ?? ""}
placeholder={t("permissionPattern")}
onChange={(e) => {
const rules = [...(settings.permission?.rules ?? [])];
rules[idx] = { ...rule, pattern: e.target.value };
setRules(rules);
}}
/>
<button
type="button"
onClick={() => {
const rules = [...(settings.permission?.rules ?? [])];
rules.splice(idx, 1);
setRules(rules);
}}
className="px-2 py-1 text-sm text-red-400 hover:bg-red-900/20 rounded"
>
×
</button>
</div>
))}
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
setRules([
...(settings.permission?.rules ?? []),
{ decision: "allow", pattern: "" },
]);
}}
className="px-3 py-1.5 text-sm border border-border rounded hover:bg-hover-2"
>
{t("addRule")}
</button>
<button
type="button"
onClick={() => {
setRules([
...(settings.permission?.rules ?? []),
{ decision: "allow", pattern: "Read" },
{ decision: "deny", pattern: "Bash(rm -rf*)" },
]);
}}
className="px-3 py-1.5 text-sm border border-border rounded hover:bg-hover-2"
>
{t("addCommonRules")}
</button>
</div>
</Card>
<Card title={t("hooks")}>
<div className="space-y-2">
{(settings.hooks ?? []).map((hook, idx) => (
<div key={idx} className="flex items-center gap-2 flex-wrap">
<input
list="hook-events"
type="text"
className="w-32 bg-input border border-border rounded px-2 py-1 text-sm"
value={hook.event ?? ""}
placeholder={t("hookEvent")}
onChange={(e) => {
const hooks = [...(settings.hooks ?? [])];
hooks[idx] = { ...hook, event: e.target.value };
setHooks(hooks);
}}
/>
<datalist id="hook-events">
{COMMON_EVENTS.map((e) => (
<option key={e} value={e} />
))}
</datalist>
<input
type="text"
className="w-24 bg-input border border-border rounded px-2 py-1 text-sm"
value={hook.matcher ?? ""}
placeholder={t("hookMatcher")}
onChange={(e) => {
const hooks = [...(settings.hooks ?? [])];
hooks[idx] = { ...hook, matcher: e.target.value };
setHooks(hooks);
}}
/>
<input
type="text"
className="flex-1 min-w-0 bg-input border border-border rounded px-2 py-1 text-sm"
value={hook.command ?? ""}
placeholder={t("hookCommand")}
onChange={(e) => {
const hooks = [...(settings.hooks ?? [])];
hooks[idx] = { ...hook, command: e.target.value };
setHooks(hooks);
}}
/>
<input
type="number"
className="w-20 bg-input border border-border rounded px-2 py-1 text-sm"
value={hook.timeout ?? ""}
placeholder={t("hookTimeout")}
onChange={(e) => {
const hooks = [...(settings.hooks ?? [])];
hooks[idx] = {
...hook,
timeout: e.target.value === "" ? undefined : Number(e.target.value),
};
setHooks(hooks);
}}
/>
<button
type="button"
onClick={() => {
const hooks = [...(settings.hooks ?? [])];
hooks.splice(idx, 1);
setHooks(hooks);
}}
className="px-2 py-1 text-sm text-red-400 hover:bg-red-900/20 rounded"
>
×
</button>
</div>
))}
</div>
<button
type="button"
onClick={() => {
setHooks([...(settings.hooks ?? []), { event: "", matcher: "", command: "" }]);
}}
className="px-3 py-1.5 text-sm border border-border rounded hover:bg-hover-2"
>
{t("addHook")}
</button>
</Card>
</div>
);
}