feat: add AgentSettingsPanel component

This commit is contained in:
KimiSwitch Dev
2026-07-09 01:01:13 +08:00
parent a0ec266129
commit 70c52aeaca
+376
View File
@@ -0,0 +1,376 @@
import type { ReactNode } from "react";
import { useTranslation } from "../i18n";
import type { TranslationKey } from "../i18n/zh";
import { getAgentSettings, setAgentSettings } from "../lib/agent-settings";
import type { AgentSettings, Hook, PermissionRule } from "../types";
interface AgentSettingsPanelProps {
rawOther: unknown;
onChange: (nextRawOther: unknown) => void;
}
const THINKING_LEVELS = ["low", "medium", "high", "max"] as const;
const THINKING_LABELS: Record<(typeof THINKING_LEVELS)[number], TranslationKey> = {
low: "thinkingLow",
medium: "thinkingMedium",
high: "thinkingHigh",
max: "thinkingMax",
};
const PERMISSION_DECISIONS = ["allow", "deny", "ask"] as const;
const PERMISSION_LABELS: Record<(typeof PERMISSION_DECISIONS)[number], TranslationKey> = {
allow: "permissionAllow",
deny: "permissionDeny",
ask: "permissionAsk",
};
const COMMON_EVENTS = ["PreToolUse", "PostToolUse"] as const;
export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelProps) {
const { t } = useTranslation();
const settings = getAgentSettings(rawOther);
const update = (patch: Partial<AgentSettings>) => {
onChange(setAgentSettings(rawOther, patch));
};
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 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-gray-400 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-gray-400">{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 AgentSettings["thinking"]["effort"] })
}
disabled={!thinkingEnabled}
/>
</div>
<Checkbox
label={t("thinkingKeep")}
checked={settings.thinking?.keep === "all"}
disabled={!thinkingEnabled}
onChange={(checked) => updateThinking({ keep: checked ? "all" : false })}
/>
<p className="text-xs text-gray-500">{t("thinkingContextHint")}</p>
</Card>
<Card title={t("loopControlSettings")}>
<NumberField
label={t("maxRetriesPerStep")}
value={settings.loop_control?.max_retries_per_step ?? 3}
onChange={(v) => updateLoopControl({ max_retries_per_step: v })}
/>
<NumberField
label={t("reservedContextSize")}
value={settings.loop_control?.reserved_context_size ?? 50000}
onChange={(v) => updateLoopControl({ reserved_context_size: v })}
/>
</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("permissionRules")}>
<div className="space-y-2">
{(settings.permission?.rules ?? []).map((rule, idx) => (
<div key={idx} className="flex items-center gap-2">
<select
className="bg-[#1f1f23] border border-[#2a2a2e] 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-[#1f1f23] border border-[#2a2a2e] 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-[#2a2a2e] rounded hover:bg-[#252529]"
>
{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-[#2a2a2e] rounded hover:bg-[#252529]"
>
{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-[#1f1f23] border border-[#2a2a2e] 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-[#1f1f23] border border-[#2a2a2e] 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-[#1f1f23] border border-[#2a2a2e] 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-[#1f1f23] border border-[#2a2a2e] 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-[#2a2a2e] rounded hover:bg-[#252529]"
>
{t("addHook")}
</button>
</Card>
</div>
);
}
function Card({ title, children }: { title: string; children: ReactNode }) {
return (
<div className="bg-[#16161a] border border-[#2a2a2e] rounded-xl p-4 space-y-3">
<h4 className="text-gray-400 text-sm font-medium">{title}</h4>
{children}
</div>
);
}
function Checkbox({
label,
checked,
disabled,
onChange,
}: {
label: string;
checked: boolean;
disabled?: boolean;
onChange: (checked: boolean) => void;
}) {
return (
<label
className={`flex items-center gap-2 text-sm ${
disabled ? "text-gray-600" : "text-[#e5e5e7]"
}`}
>
<input
type="checkbox"
checked={checked}
disabled={disabled}
onChange={(e) => onChange(e.target.checked)}
className="rounded border-[#2a2a2e] bg-[#1f1f23] text-blue-600 focus:ring-blue-500"
/>
{label}
</label>
);
}
function NumberField({
label,
value,
onChange,
}: {
label: string;
value?: number;
onChange: (value: number | undefined) => void;
}) {
return (
<label className="flex items-center gap-3 text-sm text-[#e5e5e7]">
<span className="text-gray-400">{label}</span>
<input
type="number"
value={value ?? ""}
onChange={(e) => {
const raw = e.target.value;
onChange(raw === "" ? undefined : Number(raw));
}}
className="w-32 bg-[#1f1f23] border border-[#2a2a2e] rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
</label>
);
}
function Segmented({
options,
value,
onChange,
disabled,
}: {
options: { key: string; label: string }[];
value: string;
onChange: (value: string) => void;
disabled?: boolean;
}) {
return (
<div
className={`flex items-center rounded overflow-hidden border border-[#2a2a2e] ${
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-[#1f1f23] text-gray-400 hover:bg-[#252529]"
}`}
>
{opt.label}
</button>
))}
</div>
);
}