Przeglądaj źródła

feat(desktop): auto-accept edits toggle

Adam 1 miesiąc temu
rodzic
commit
3109214900

+ 130 - 0
packages/app/src/context/permission.tsx

@@ -0,0 +1,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)
+      },
+    }
+  },
+})

+ 12 - 9
packages/app/src/pages/directory-layout.tsx

@@ -3,6 +3,7 @@ import { useParams } from "@solidjs/router"
 import { SDKProvider, useSDK } from "@/context/sdk"
 import { SyncProvider, useSync } from "@/context/sync"
 import { LocalProvider } from "@/context/local"
+import { PermissionProvider } from "@/context/permission"
 import { base64Decode } from "@opencode-ai/util/encode"
 import { DataProvider } from "@opencode-ai/ui/context"
 import { iife } from "@opencode-ai/util/iife"
@@ -19,16 +20,18 @@ export default function Layout(props: ParentProps) {
           {iife(() => {
             const sync = useSync()
             const sdk = useSDK()
+            const respond = (input: {
+              sessionID: string
+              permissionID: string
+              response: "once" | "always" | "reject"
+            }) => sdk.client.permission.respond(input)
+
             return (
-              <DataProvider
-                data={sync.data}
-                directory={directory()}
-                onPermissionRespond={(input) => {
-                  sdk.client.permission.respond(input)
-                }}
-              >
-                <LocalProvider>{props.children}</LocalProvider>
-              </DataProvider>
+              <PermissionProvider permissions={sync.data.permission} onRespond={respond}>
+                <DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
+                  <LocalProvider>{props.children}</LocalProvider>
+                </DataProvider>
+              </PermissionProvider>
             )
           })}
         </SyncProvider>

+ 19 - 0
packages/app/src/pages/session.tsx

@@ -60,6 +60,8 @@ import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
 import { StatusBar } from "@/components/status-bar"
 import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
 import { SessionLspIndicator } from "@/components/session-lsp-indicator"
+import { usePermission } from "@/context/permission"
+import { showToast } from "@opencode-ai/ui/toast"
 
 export default function Page() {
   const layout = useLayout()
@@ -74,6 +76,7 @@ export default function Page() {
   const sdk = useSDK()
   const prompt = usePrompt()
 
+  const permission = usePermission()
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
   const tabs = createMemo(() => layout.tabs(sessionKey()))
   const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
@@ -312,6 +315,22 @@ export default function Page() {
       keybind: "shift+mod+.",
       onSelect: () => local.agent.move(-1),
     },
+    {
+      id: "permissions.autoaccept",
+      title: params.id && permission.isAutoAccepting(params.id) ? "Stop auto-accepting edits" : "Auto-accept edits",
+      category: "Permissions",
+      disabled: !params.id,
+      onSelect: () => {
+        if (!params.id) return
+        permission.toggleAutoAccept(params.id)
+        showToast({
+          title: permission.isAutoAccepting(params.id) ? "Auto-accepting edits" : "Stopped auto-accepting edits",
+          description: permission.isAutoAccepting(params.id)
+            ? "Edit and write permissions will be automatically approved"
+            : "Edit and write permissions will require approval",
+        })
+      },
+    },
     {
       id: "session.undo",
       title: "Undo",