settings-permissions.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import { Select } from "@opencode-ai/ui/select"
  2. import { showToast } from "@opencode-ai/ui/toast"
  3. import { Component, For, createMemo, type JSX } from "solid-js"
  4. import { useGlobalSync } from "@/context/global-sync"
  5. import { useLanguage } from "@/context/language"
  6. type PermissionAction = "allow" | "ask" | "deny"
  7. type PermissionObject = Record<string, PermissionAction>
  8. type PermissionValue = PermissionAction | PermissionObject | string[] | undefined
  9. type PermissionMap = Record<string, PermissionValue>
  10. type PermissionItem = {
  11. id: string
  12. title: string
  13. description: string
  14. }
  15. const ACTIONS = [
  16. { value: "allow", label: "settings.permissions.action.allow" },
  17. { value: "ask", label: "settings.permissions.action.ask" },
  18. { value: "deny", label: "settings.permissions.action.deny" },
  19. ] as const
  20. const ITEMS = [
  21. {
  22. id: "read",
  23. title: "settings.permissions.tool.read.title",
  24. description: "settings.permissions.tool.read.description",
  25. },
  26. {
  27. id: "edit",
  28. title: "settings.permissions.tool.edit.title",
  29. description: "settings.permissions.tool.edit.description",
  30. },
  31. {
  32. id: "glob",
  33. title: "settings.permissions.tool.glob.title",
  34. description: "settings.permissions.tool.glob.description",
  35. },
  36. {
  37. id: "grep",
  38. title: "settings.permissions.tool.grep.title",
  39. description: "settings.permissions.tool.grep.description",
  40. },
  41. {
  42. id: "list",
  43. title: "settings.permissions.tool.list.title",
  44. description: "settings.permissions.tool.list.description",
  45. },
  46. {
  47. id: "bash",
  48. title: "settings.permissions.tool.bash.title",
  49. description: "settings.permissions.tool.bash.description",
  50. },
  51. {
  52. id: "task",
  53. title: "settings.permissions.tool.task.title",
  54. description: "settings.permissions.tool.task.description",
  55. },
  56. {
  57. id: "skill",
  58. title: "settings.permissions.tool.skill.title",
  59. description: "settings.permissions.tool.skill.description",
  60. },
  61. {
  62. id: "lsp",
  63. title: "settings.permissions.tool.lsp.title",
  64. description: "settings.permissions.tool.lsp.description",
  65. },
  66. {
  67. id: "todoread",
  68. title: "settings.permissions.tool.todoread.title",
  69. description: "settings.permissions.tool.todoread.description",
  70. },
  71. {
  72. id: "todowrite",
  73. title: "settings.permissions.tool.todowrite.title",
  74. description: "settings.permissions.tool.todowrite.description",
  75. },
  76. {
  77. id: "webfetch",
  78. title: "settings.permissions.tool.webfetch.title",
  79. description: "settings.permissions.tool.webfetch.description",
  80. },
  81. {
  82. id: "websearch",
  83. title: "settings.permissions.tool.websearch.title",
  84. description: "settings.permissions.tool.websearch.description",
  85. },
  86. {
  87. id: "codesearch",
  88. title: "settings.permissions.tool.codesearch.title",
  89. description: "settings.permissions.tool.codesearch.description",
  90. },
  91. {
  92. id: "external_directory",
  93. title: "settings.permissions.tool.external_directory.title",
  94. description: "settings.permissions.tool.external_directory.description",
  95. },
  96. {
  97. id: "doom_loop",
  98. title: "settings.permissions.tool.doom_loop.title",
  99. description: "settings.permissions.tool.doom_loop.description",
  100. },
  101. ] as const
  102. const VALID_ACTIONS = new Set<PermissionAction>(["allow", "ask", "deny"])
  103. function toMap(value: unknown): PermissionMap {
  104. if (value && typeof value === "object" && !Array.isArray(value)) return value as PermissionMap
  105. const action = getAction(value)
  106. if (action) return { "*": action }
  107. return {}
  108. }
  109. function getAction(value: unknown): PermissionAction | undefined {
  110. if (typeof value === "string" && VALID_ACTIONS.has(value as PermissionAction)) return value as PermissionAction
  111. return
  112. }
  113. function getRuleDefault(value: unknown): PermissionAction | undefined {
  114. const action = getAction(value)
  115. if (action) return action
  116. if (!value || typeof value !== "object" || Array.isArray(value)) return
  117. return getAction((value as Record<string, unknown>)["*"])
  118. }
  119. export const SettingsPermissions: Component = () => {
  120. const globalSync = useGlobalSync()
  121. const language = useLanguage()
  122. const actions = createMemo(
  123. (): Array<{ value: PermissionAction; label: string }> =>
  124. ACTIONS.map((action) => ({
  125. value: action.value,
  126. label: language.t(action.label),
  127. })),
  128. )
  129. const permission = createMemo(() => {
  130. return toMap(globalSync.data.config.permission)
  131. })
  132. const actionFor = (id: string): PermissionAction => {
  133. const value = permission()[id]
  134. const direct = getRuleDefault(value)
  135. if (direct) return direct
  136. const wildcard = getRuleDefault(permission()["*"])
  137. if (wildcard) return wildcard
  138. return "allow"
  139. }
  140. const setPermission = async (id: string, action: PermissionAction) => {
  141. const before = globalSync.data.config.permission
  142. const map = toMap(before)
  143. const existing = map[id]
  144. const nextValue =
  145. existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing, "*": action } : action
  146. globalSync.set("config", "permission", { ...map, [id]: nextValue })
  147. globalSync.updateConfig({ permission: { [id]: nextValue } }).catch((err: unknown) => {
  148. globalSync.set("config", "permission", before)
  149. const message = err instanceof Error ? err.message : String(err)
  150. showToast({ title: language.t("settings.permissions.toast.updateFailed.title"), description: message })
  151. })
  152. }
  153. return (
  154. <div class="flex flex-col h-full overflow-y-auto no-scrollbar">
  155. <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
  156. <div class="flex flex-col gap-1 px-4 py-8 sm:p-8 max-w-[720px]">
  157. <h2 class="text-16-medium text-text-strong">{language.t("settings.permissions.title")}</h2>
  158. <p class="text-14-regular text-text-weak">{language.t("settings.permissions.description")}</p>
  159. </div>
  160. </div>
  161. <div class="flex flex-col gap-6 px-4 py-6 sm:p-8 sm:pt-6 max-w-[720px]">
  162. <div class="flex flex-col gap-2">
  163. <h3 class="text-14-medium text-text-strong">{language.t("settings.permissions.section.tools")}</h3>
  164. <div class="border border-border-weak-base rounded-lg overflow-hidden">
  165. <For each={ITEMS}>
  166. {(item) => (
  167. <SettingsRow title={language.t(item.title)} description={language.t(item.description)}>
  168. <Select
  169. options={actions()}
  170. current={actions().find((o) => o.value === actionFor(item.id))}
  171. value={(o) => o.value}
  172. label={(o) => o.label}
  173. onSelect={(option) => option && setPermission(item.id, option.value)}
  174. variant="secondary"
  175. size="small"
  176. triggerVariant="settings"
  177. />
  178. </SettingsRow>
  179. )}
  180. </For>
  181. </div>
  182. </div>
  183. </div>
  184. </div>
  185. )
  186. }
  187. interface SettingsRowProps {
  188. title: string
  189. description: string
  190. children: JSX.Element
  191. }
  192. const SettingsRow: Component<SettingsRowProps> = (props) => {
  193. return (
  194. <div class="flex flex-wrap items-center justify-between gap-4 px-4 py-3 border-b border-border-weak-base last:border-none">
  195. <div class="flex flex-col gap-0.5 min-w-0">
  196. <span class="text-14-medium text-text-strong">{props.title}</span>
  197. <span class="text-12-regular text-text-weak">{props.description}</span>
  198. </div>
  199. <div class="flex-shrink-0">{props.children}</div>
  200. </div>
  201. )
  202. }