summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context/permission.tsx
blob: 6d7b335ad96c8c0b8b57e6ff322a7d3ad4f22799 (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
import { createEffect, createRoot, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import type { Permission } from "@opencode-ai/sdk/v2/client"
import { persisted } from "@/utils/persist"

type PermissionsBySession = {
  [sessionID: string]: Permission[]
}

type PermissionRespondFn = (input: {
  sessionID: string
  permissionID: string
  response: "once" | "always" | "reject"
}) => void

const AUTO_ACCEPT_TYPES = new Set(["edit", "write"])

function shouldAutoAccept(perm: Permission) {
  return AUTO_ACCEPT_TYPES.has(perm.type)
}

export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
  name: "Permission",
  init: (props: { permissions: PermissionsBySession; onRespond: PermissionRespondFn }) => {
    const [store, setStore, _, ready] = persisted(
      "permission.v1",
      createStore({
        autoAcceptEdits: {} as Record<string, boolean>,
      }),
    )

    const responded = new Set<string>()
    const watches = new Map<string, () => void>()

    function respond(perm: Permission) {
      if (responded.has(perm.id)) return
      responded.add(perm.id)
      props.onRespond({
        sessionID: perm.sessionID,
        permissionID: perm.id,
        response: "once",
      })
    }

    function watch(sessionID: string) {
      if (watches.has(sessionID)) return

      const dispose = createRoot((dispose) => {
        createEffect(() => {
          if (!store.autoAcceptEdits[sessionID]) return

          const permissions = props.permissions[sessionID] ?? []
          permissions.length

          for (const perm of permissions) {
            if (!shouldAutoAccept(perm)) continue
            respond(perm)
          }
        })

        return dispose
      })

      watches.set(sessionID, dispose)
    }

    function unwatch(sessionID: string) {
      const dispose = watches.get(sessionID)
      if (!dispose) return
      dispose()
      watches.delete(sessionID)
    }

    createEffect(() => {
      if (!ready()) return

      for (const sessionID in store.autoAcceptEdits) {
        if (!store.autoAcceptEdits[sessionID]) continue
        watch(sessionID)
      }
    })

    onCleanup(() => {
      for (const dispose of watches.values()) dispose()
      watches.clear()
    })

    function enable(sessionID: string) {
      setStore("autoAcceptEdits", sessionID, true)
      watch(sessionID)

      const permissions = props.permissions[sessionID] ?? []
      for (const perm of permissions) {
        if (!shouldAutoAccept(perm)) continue
        respond(perm)
      }
    }

    function disable(sessionID: string) {
      setStore("autoAcceptEdits", sessionID, false)
      unwatch(sessionID)
    }

    return {
      get permissions() {
        return props.permissions
      },
      respond: props.onRespond,
      isAutoAccepting(sessionID: string) {
        return store.autoAcceptEdits[sessionID] ?? false
      },
      toggleAutoAccept(sessionID: string) {
        if (store.autoAcceptEdits[sessionID]) {
          disable(sessionID)
          return
        }

        enable(sessionID)
      },
      enableAutoAccept(sessionID: string) {
        if (store.autoAcceptEdits[sessionID]) return
        enable(sessionID)
      },
      disableAutoAccept(sessionID: string) {
        disable(sessionID)
      },
    }
  },
})