diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index 83cea131f5d..65549b79191 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -8,6 +8,7 @@ import { SettingsGeneral } from "./settings-general" import { SettingsKeybinds } from "./settings-keybinds" import { SettingsProviders } from "./settings-providers" import { SettingsModels } from "./settings-models" +import { SettingsPermissions } from "./settings-permissions" export const DialogSettings: Component = () => { const language = useLanguage() @@ -41,6 +42,10 @@ export const DialogSettings: Component = () => { {language.t("settings.providers.title")} + + + {language.t("settings.permissions.title")} + {language.t("settings.models.title")} @@ -64,6 +69,9 @@ export const DialogSettings: Component = () => { + + + diff --git a/packages/app/src/components/settings-permissions.tsx b/packages/app/src/components/settings-permissions.tsx new file mode 100644 index 00000000000..398bb08a2ec --- /dev/null +++ b/packages/app/src/components/settings-permissions.tsx @@ -0,0 +1,247 @@ +import { Select } from "@opencode-ai/ui/select" +import { Switch } from "@opencode-ai/ui/switch" +import { showToast } from "@opencode-ai/ui/toast" +import { Component, For, createMemo, type JSX } from "solid-js" +import { useGlobalSync } from "@/context/global-sync" +import { useLanguage } from "@/context/language" +import { usePermission } from "@/context/permission" + +type PermissionAction = "allow" | "ask" | "deny" + +type PermissionObject = Record +type PermissionValue = PermissionAction | PermissionObject | string[] | undefined +type PermissionMap = Record + +type PermissionItem = { + id: string + title: string + description: string +} + +const ACTIONS = [ + { value: "allow", label: "settings.permissions.action.allow" }, + { value: "ask", label: "settings.permissions.action.ask" }, + { value: "deny", label: "settings.permissions.action.deny" }, +] as const + +const ITEMS = [ + { + id: "read", + title: "settings.permissions.tool.read.title", + description: "settings.permissions.tool.read.description", + }, + { + id: "edit", + title: "settings.permissions.tool.edit.title", + description: "settings.permissions.tool.edit.description", + }, + { + id: "glob", + title: "settings.permissions.tool.glob.title", + description: "settings.permissions.tool.glob.description", + }, + { + id: "grep", + title: "settings.permissions.tool.grep.title", + description: "settings.permissions.tool.grep.description", + }, + { + id: "list", + title: "settings.permissions.tool.list.title", + description: "settings.permissions.tool.list.description", + }, + { + id: "bash", + title: "settings.permissions.tool.bash.title", + description: "settings.permissions.tool.bash.description", + }, + { + id: "task", + title: "settings.permissions.tool.task.title", + description: "settings.permissions.tool.task.description", + }, + { + id: "skill", + title: "settings.permissions.tool.skill.title", + description: "settings.permissions.tool.skill.description", + }, + { + id: "lsp", + title: "settings.permissions.tool.lsp.title", + description: "settings.permissions.tool.lsp.description", + }, + { + id: "todoread", + title: "settings.permissions.tool.todoread.title", + description: "settings.permissions.tool.todoread.description", + }, + { + id: "todowrite", + title: "settings.permissions.tool.todowrite.title", + description: "settings.permissions.tool.todowrite.description", + }, + { + id: "webfetch", + title: "settings.permissions.tool.webfetch.title", + description: "settings.permissions.tool.webfetch.description", + }, + { + id: "websearch", + title: "settings.permissions.tool.websearch.title", + description: "settings.permissions.tool.websearch.description", + }, + { + id: "codesearch", + title: "settings.permissions.tool.codesearch.title", + description: "settings.permissions.tool.codesearch.description", + }, + { + id: "external_directory", + title: "settings.permissions.tool.external_directory.title", + description: "settings.permissions.tool.external_directory.description", + }, + { + id: "doom_loop", + title: "settings.permissions.tool.doom_loop.title", + description: "settings.permissions.tool.doom_loop.description", + }, +] as const + +const VALID_ACTIONS = new Set(["allow", "ask", "deny"]) + +function toMap(value: unknown): PermissionMap { + if (value && typeof value === "object" && !Array.isArray(value)) return value as PermissionMap + + const action = getAction(value) + if (action) return { "*": action } + + return {} +} + +function getAction(value: unknown): PermissionAction | undefined { + if (typeof value === "string" && VALID_ACTIONS.has(value as PermissionAction)) return value as PermissionAction + return +} + +function getRuleDefault(value: unknown): PermissionAction | undefined { + const action = getAction(value) + if (action) return action + + if (!value || typeof value !== "object" || Array.isArray(value)) return + + return getAction((value as Record)["*"]) +} + +export const SettingsPermissions: Component = () => { + const globalSync = useGlobalSync() + const language = useLanguage() + const perms = usePermission() + + const actions = createMemo( + (): Array<{ value: PermissionAction; label: string }> => + ACTIONS.map((action) => ({ + value: action.value, + label: language.t(action.label), + })), + ) + + const rules = createMemo(() => { + return toMap(globalSync.data.config.permission) + }) + + const actionFor = (id: string): PermissionAction => { + const value = rules()[id] + const direct = getRuleDefault(value) + if (direct) return direct + + const wildcard = getRuleDefault(rules()["*"]) + if (wildcard) return wildcard + + return "allow" + } + + const setPermission = async (id: string, action: PermissionAction) => { + const before = globalSync.data.config.permission + const map = toMap(before) + const existing = map[id] + + const nextValue = + existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing, "*": action } : action + + const rollback = (err: unknown) => { + globalSync.set("config", "permission", before) + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("settings.permissions.toast.updateFailed.title"), description: message }) + } + + globalSync.set("config", "permission", { ...map, [id]: nextValue }) + globalSync.updateConfig({ permission: { [id]: nextValue } }).catch(rollback) + } + + return ( +
+
+
+

{language.t("settings.permissions.title")}

+

{language.t("settings.permissions.description")}

+
+
+ +
+
+

{language.t("command.permissions.autoaccept.enable")}

+
+ +
+ perms.toggleAutoAcceptGlobal()} /> +
+
+
+
+ +
+

{language.t("settings.permissions.section.tools")}

+
+ + {(item) => ( + +