feat: add AgentSettingsPanel component
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user