229 lines
7.4 KiB
TypeScript
229 lines
7.4 KiB
TypeScript
import { Select } from "@opencode-ai/ui/select"
|
|
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"
|
|
|
|
type PermissionAction = "allow" | "ask" | "deny"
|
|
|
|
type PermissionObject = Record<string, PermissionAction>
|
|
type PermissionValue = PermissionAction | PermissionObject | string[] | undefined
|
|
type PermissionMap = Record<string, PermissionValue>
|
|
|
|
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<PermissionAction>(["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<string, unknown>)["*"])
|
|
}
|
|
|
|
export const SettingsPermissions: Component = () => {
|
|
const globalSync = useGlobalSync()
|
|
const language = useLanguage()
|
|
|
|
const actions = createMemo(
|
|
(): Array<{ value: PermissionAction; label: string }> =>
|
|
ACTIONS.map((action) => ({
|
|
value: action.value,
|
|
label: language.t(action.label),
|
|
})),
|
|
)
|
|
|
|
const permission = createMemo(() => {
|
|
return toMap(globalSync.data.config.permission)
|
|
})
|
|
|
|
const actionFor = (id: string): PermissionAction => {
|
|
const value = permission()[id]
|
|
const direct = getRuleDefault(value)
|
|
if (direct) return direct
|
|
|
|
const wildcard = getRuleDefault(permission()["*"])
|
|
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
|
|
|
|
globalSync.set("config", "permission", { ...map, [id]: nextValue })
|
|
globalSync.updateConfig({ permission: { [id]: nextValue } }).catch((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 })
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar">
|
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
|
<div class="flex flex-col gap-1 px-4 py-8 sm:p-8 max-w-[720px]">
|
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.permissions.title")}</h2>
|
|
<p class="text-14-regular text-text-weak">{language.t("settings.permissions.description")}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-6 px-4 py-6 sm:p-8 sm:pt-6 max-w-[720px]">
|
|
<div class="flex flex-col gap-2">
|
|
<h3 class="text-14-medium text-text-strong">{language.t("settings.permissions.section.tools")}</h3>
|
|
<div class="border border-border-weak-base rounded-lg overflow-hidden">
|
|
<For each={ITEMS}>
|
|
{(item) => (
|
|
<SettingsRow title={language.t(item.title)} description={language.t(item.description)}>
|
|
<Select
|
|
options={actions()}
|
|
current={actions().find((o) => o.value === actionFor(item.id))}
|
|
value={(o) => o.value}
|
|
label={(o) => o.label}
|
|
onSelect={(option) => option && setPermission(item.id, option.value)}
|
|
variant="secondary"
|
|
size="small"
|
|
triggerVariant="settings"
|
|
/>
|
|
</SettingsRow>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface SettingsRowProps {
|
|
title: string
|
|
description: string
|
|
children: JSX.Element
|
|
}
|
|
|
|
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
|
return (
|
|
<div class="flex flex-wrap items-center justify-between gap-4 px-4 py-3 border-b border-border-weak-base last:border-none">
|
|
<div class="flex flex-col gap-0.5 min-w-0">
|
|
<span class="text-14-medium text-text-strong">{props.title}</span>
|
|
<span class="text-12-regular text-text-weak">{props.description}</span>
|
|
</div>
|
|
<div class="flex-shrink-0">{props.children}</div>
|
|
</div>
|
|
)
|
|
}
|