summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components/settings-permissions.tsx
blob: f5ee76650179be1ae59802a2552fd9b10d6f7d3f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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"

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: Array<{ value: PermissionAction; label: string }> = [
  { value: "allow", label: "Allow" },
  { value: "ask", label: "Ask" },
  { value: "deny", label: "Deny" },
]

const ITEMS: PermissionItem[] = [
  { id: "read", title: "Read", description: "Reading a file (matches the file path)" },
  { id: "edit", title: "Edit", description: "Modify files, including edits, writes, patches, and multi-edits" },
  { id: "glob", title: "Glob", description: "Match files using glob patterns" },
  { id: "grep", title: "Grep", description: "Search file contents using regular expressions" },
  { id: "list", title: "List", description: "List files within a directory" },
  { id: "bash", title: "Bash", description: "Run shell commands" },
  { id: "task", title: "Task", description: "Launch sub-agents" },
  { id: "skill", title: "Skill", description: "Load a skill by name" },
  { id: "lsp", title: "LSP", description: "Run language server queries" },
  { id: "todoread", title: "Todo Read", description: "Read the todo list" },
  { id: "todowrite", title: "Todo Write", description: "Update the todo list" },
  { id: "webfetch", title: "Web Fetch", description: "Fetch content from a URL" },
  { id: "websearch", title: "Web Search", description: "Search the web" },
  { id: "codesearch", title: "Code Search", description: "Search code on the web" },
  { id: "external_directory", title: "External Directory", description: "Access files outside the project directory" },
  { id: "doom_loop", title: "Doom Loop", description: "Detect repeated tool calls with identical input" },
]

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 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: "Failed to update permissions", description: message })
    })
  }

  return (
    <div class="flex flex-col h-full overflow-y-auto no-scrollbar">
      <div class="sticky top-0 z-10 bg-background-base border-b border-border-weak-base">
        <div class="flex flex-col gap-1 p-8 max-w-[720px]">
          <h2 class="text-16-medium text-text-strong">Permissions</h2>
          <p class="text-14-regular text-text-weak">Control what tools the server can use by default.</p>
        </div>
      </div>

      <div class="flex flex-col gap-6 p-8 pt-6 max-w-[720px]">
        <div class="flex flex-col gap-2">
          <h3 class="text-14-medium text-text-strong">Appearance</h3>
          <div class="border border-border-weak-base rounded-lg overflow-hidden">
            <For each={ITEMS}>
              {(item) => (
                <SettingsRow title={item.title} description={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"
                  />
                </SettingsRow>
              )}
            </For>
          </div>
        </div>
      </div>
    </div>
  )
}

interface SettingsRowProps {
  title: string
  description: string
  children: JSX.Element
}

const SettingsRow: Component<SettingsRowProps> = (props) => {
  return (
    <div class="flex 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">
        <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>
  )
}