diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx index 79b08ed6554..5ec66b77da4 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx @@ -1,28 +1,9 @@ -import { Component, For, createMemo } from "solid-js" +import { Component, For, Show, createMemo, createSignal } from "solid-js" import { Select } from "@kilocode/kilo-ui/select" -import { Card } from "@kilocode/kilo-ui/card" +import { IconButton } from "@kilocode/kilo-ui/icon-button" import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" -import type { PermissionLevel } from "../../types/messages" - -const TOOLS = [ - "read", - "edit", - "glob", - "grep", - "list", - "bash", - "task", - "skill", - "lsp", - "todoread", - "todowrite", - "webfetch", - "websearch", - "codesearch", - "external_directory", - "doom_loop", -] as const +import type { PermissionLevel, PermissionRule } from "../../types/messages" interface LevelOption { value: PermissionLevel @@ -35,104 +16,470 @@ const LEVEL_OPTIONS: LevelOption[] = [ { value: "deny", labelKey: "settings.autoApprove.level.deny" }, ] +interface ToolDef { + id: string + descriptionKey: string + granular?: { + wildcardKey: string + addKey: string + placeholderKey: string + } +} + +/** Grouped tool: maps a single UI row to multiple config keys */ +interface GroupedToolDef { + ids: string[] + label: string + descriptionKey: string +} + +const GRANULAR_TOOLS: ToolDef[] = [ + { + id: "external_directory", + descriptionKey: "settings.autoApprove.tool.external_directory", + granular: { + wildcardKey: "settings.autoApprove.wildcardLabel.paths", + addKey: "settings.autoApprove.addPath", + placeholderKey: "settings.autoApprove.placeholder.path", + }, + }, + { + id: "bash", + descriptionKey: "settings.autoApprove.tool.bash", + granular: { + wildcardKey: "settings.autoApprove.wildcardLabel.commands", + addKey: "settings.autoApprove.addCommand", + placeholderKey: "settings.autoApprove.placeholder.command", + }, + }, + { + id: "read", + descriptionKey: "settings.autoApprove.tool.read", + granular: { + wildcardKey: "settings.autoApprove.wildcardLabel.paths", + addKey: "settings.autoApprove.addPath", + placeholderKey: "settings.autoApprove.placeholder.path", + }, + }, + { + id: "edit", + descriptionKey: "settings.autoApprove.tool.edit", + granular: { + wildcardKey: "settings.autoApprove.wildcardLabel.paths", + addKey: "settings.autoApprove.addPath", + placeholderKey: "settings.autoApprove.placeholder.path", + }, + }, +] + +const SIMPLE_TOOLS: ToolDef[] = [ + { id: "glob", descriptionKey: "settings.autoApprove.tool.glob" }, + { id: "grep", descriptionKey: "settings.autoApprove.tool.grep" }, + { id: "list", descriptionKey: "settings.autoApprove.tool.list" }, + { id: "task", descriptionKey: "settings.autoApprove.tool.task" }, + { id: "skill", descriptionKey: "settings.autoApprove.tool.skill" }, + { id: "lsp", descriptionKey: "settings.autoApprove.tool.lsp" }, +] + +const GROUPED_TOOLS: GroupedToolDef[] = [ + { + ids: ["todoread", "todowrite"], + label: "todoread / todowrite", + descriptionKey: "settings.autoApprove.tool.todoreadwrite", + }, + { + ids: ["websearch", "codesearch"], + label: "websearch / codesearch", + descriptionKey: "settings.autoApprove.tool.websearchcodesearch", + }, +] + +const TRAILING_TOOLS: ToolDef[] = [ + { id: "webfetch", descriptionKey: "settings.autoApprove.tool.webfetch" }, + { id: "doom_loop", descriptionKey: "settings.autoApprove.tool.doom_loop" }, +] + +function wildcardAction(rule: PermissionRule | undefined, fallback: PermissionLevel): PermissionLevel { + if (!rule) return fallback + if (typeof rule === "string") return rule + return rule["*"] ?? fallback +} + +function exceptions(rule: PermissionRule | undefined): Array<{ pattern: string; action: PermissionLevel }> { + if (!rule || typeof rule === "string") return [] + return Object.entries(rule) + .filter(([key]) => key !== "*") + .map(([pattern, action]) => ({ pattern, action })) +} + const AutoApproveTab: Component = () => { const { config, updateConfig } = useConfig() const language = useLanguage() const permissions = createMemo(() => config().permission ?? {}) - const getLevel = (tool: string): PermissionLevel => { - return permissions()[tool] ?? permissions()["*"] ?? "ask" + const globalFallback = createMemo((): PermissionLevel => { + const star = permissions()["*"] + if (typeof star === "string") return star + return "ask" + }) + + const levelFor = (tool: string): PermissionLevel => wildcardAction(permissions()[tool], globalFallback()) + + const ruleFor = (tool: string): PermissionRule | undefined => permissions()[tool] + + const setSimple = (tool: string, level: PermissionLevel) => { + updateConfig({ permission: { [tool]: level } }) } - const setPermission = (tool: string, level: PermissionLevel) => { - updateConfig({ - permission: { ...permissions(), [tool]: level }, - }) + const setGrouped = (ids: string[], level: PermissionLevel) => { + const patch: Record = {} + for (const id of ids) patch[id] = level + updateConfig({ permission: patch }) } - const setAll = (level: PermissionLevel) => { - const updated: Record = {} - for (const tool of TOOLS) { - updated[tool] = level + const setWildcard = (tool: string, level: PermissionLevel) => { + const current = ruleFor(tool) + const excs = exceptions(current) + if (excs.length === 0) { + updateConfig({ permission: { [tool]: level } }) + return + } + const obj: Record = { "*": level } + for (const exc of excs) obj[exc.pattern] = exc.action + updateConfig({ permission: { [tool]: obj } }) + } + + const setException = (tool: string, pattern: string, level: PermissionLevel) => { + const current = ruleFor(tool) + const base: Record = + typeof current === "string" ? { "*": current } : { ...(current ?? {}) } + base[pattern] = level + updateConfig({ permission: { [tool]: base } }) + } + + const addException = (tool: string, pattern: string) => { + const current = ruleFor(tool) + const base: Record = + typeof current === "string" ? { "*": current } : { ...(current ?? {}) } + base[pattern] = "allow" + updateConfig({ permission: { [tool]: base } }) + } + + const removeException = (tool: string, pattern: string) => { + const current = ruleFor(tool) + if (!current || typeof current === "string") return + const rebuilt: Record = {} + for (const [k, v] of Object.entries(current)) { + if (k !== pattern) rebuilt[k] = v + } + const keys = Object.keys(rebuilt) + const value: PermissionRule = + keys.length === 0 ? "ask" : keys.length === 1 && keys[0] === "*" ? rebuilt["*"]! : rebuilt + // patchJsonc only sets keys present in the patch — it won't remove the deleted key + // from the JSONC file. To work around this, first set the tool to a string (which + // replaces the entire JSONC node), then set the rebuilt object if needed. + const wildcard = rebuilt["*"] ?? "ask" + updateConfig({ permission: { [tool]: wildcard } }) + if (typeof value === "object") { + updateConfig({ permission: { [tool]: value } }) } - updateConfig({ permission: updated }) } return (
- {/* Set All control */} - -
- {language.t("settings.autoApprove.setAll")} - o.value === getLevel(tool))} - value={(o) => o.value} - label={(o) => language.t(o.labelKey)} - onSelect={(option) => option && setPermission(tool, option.value)} - variant="secondary" - size="small" - triggerVariant="settings" - /> -
- )} - -
+ + {(group) => ( + setGrouped(group.ids, level)} + /> + )} + + + + {(tool) => ( + setSimple(tool.id, level)} + /> + )} +
) } +const SimpleToolRow: Component<{ + id: string + descriptionKey: string + level: PermissionLevel + onChange: (level: PermissionLevel) => void +}> = (props) => { + const language = useLanguage() + return ( +
+
+
{props.id}
+
+ {language.t(props.descriptionKey)} +
+
+ +
+ ) +} + +const GranularToolRow: Component<{ + tool: ToolDef + rule: PermissionRule | undefined + fallback: PermissionLevel + onWildcardChange: (level: PermissionLevel) => void + onExceptionChange: (pattern: string, level: PermissionLevel) => void + onExceptionAdd: (pattern: string) => void + onExceptionRemove: (pattern: string) => void +}> = (props) => { + const language = useLanguage() + const [adding, setAdding] = createSignal(false) + const [input, setInput] = createSignal("") + + const excs = createMemo(() => exceptions(props.rule)) + const level = createMemo(() => wildcardAction(props.rule, props.fallback)) + + const submit = () => { + const val = input().trim() + if (val) { + props.onExceptionAdd(val) + setInput("") + } + setAdding(false) + } + + const cancel = () => { + setInput("") + setAdding(false) + } + + return ( +
+ {/* Tool header with name and description */} +
+
+
{props.tool.id}
+
+ {language.t(props.tool.descriptionKey)} +
+
+
+ + {/* Wildcard row */} +
+
+
+ {language.t(props.tool.granular!.wildcardKey)} +
+
+ +
+ + {/* Exceptions */} + 0}> +
+
+ {language.t("settings.autoApprove.exceptions")} +
+ + {(exc) => ( +
+
+ {exc.pattern} +
+
+ props.onExceptionChange(exc.pattern, level)} /> + props.onExceptionRemove(exc.pattern)} + /> +
+
+ )} +
+
+
+ + {/* Add button / inline input */} + setAdding(true)} + > + + + {language.t(props.tool.granular!.addKey)} + + } + > +
+ setTimeout(() => el.focus(), 0)} + type="text" + value={input()} + onInput={(e) => setInput(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === "Enter") submit() + if (e.key === "Escape") cancel() + }} + onBlur={() => { + if (!input().trim()) cancel() + }} + placeholder={language.t(props.tool.granular!.placeholderKey)} + style={{ + flex: 1, + "min-width": 0, + background: "var(--surface-strong-base, #252526)", + border: "1px solid var(--border-base, #434443)", + "border-radius": "2px", + color: "var(--text-base, #ccc)", + "font-size": "13px", + "font-family": "var(--vscode-editor-font-family, monospace)", + padding: "4px 8px", + outline: "none", + }} + /> + +
+
+
+ ) +} + +const ActionSelect: Component<{ + level: PermissionLevel + onChange: (level: PermissionLevel) => void +}> = (props) => { + const language = useLanguage() + return ( +