Преглед на файлове

fix(app): enable auto-accept keybind regardless of permission config (#16259)

Luis Felipe Cordeiro Sena преди 1 месец
родител
ревизия
b7605add58

+ 2 - 10
packages/app/src/components/prompt-input.tsx

@@ -244,7 +244,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     draggingType: "image" | "@mention" | null
     mode: "normal" | "shell"
     applyingHistory: boolean
-    pendingAutoAccept: boolean
   }>({
     popover: null,
     historyIndex: -1,
@@ -253,7 +252,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     draggingType: null,
     mode: "normal",
     applyingHistory: false,
-    pendingAutoAccept: false,
   })
 
   const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
@@ -306,12 +304,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     }),
   )
 
-  createEffect(
-    on(sessionKey, () => {
-      setStore("pendingAutoAccept", false)
-    }),
-  )
-
   const historyComments = () => {
     const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const))
     return prompt.context.items().flatMap((item) => {
@@ -961,7 +953,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   const variants = createMemo(() => ["default", ...local.model.variant.list()])
   const accepting = createMemo(() => {
     const id = params.id
-    if (!id) return store.pendingAutoAccept
+    if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
     return permission.isAutoAccepting(id, sdk.directory)
   })
 
@@ -1336,7 +1328,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                   variant="ghost"
                   onClick={() => {
                     if (!params.id) {
-                      setStore("pendingAutoAccept", (value) => !value)
+                      permission.toggleAutoAcceptDirectory(sdk.directory)
                       return
                     }
                     permission.toggleAutoAccept(params.id, sdk.directory)

+ 40 - 1
packages/app/src/context/permission-auto-respond.test.ts

@@ -1,7 +1,7 @@
 import { describe, expect, test } from "bun:test"
 import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
 import { base64Encode } from "@opencode-ai/util/encode"
-import { autoRespondsPermission } from "./permission-auto-respond"
+import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
 
 const session = (input: { id: string; parentID?: string }) =>
   ({
@@ -60,4 +60,43 @@ describe("autoRespondsPermission", () => {
 
     expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true)
   })
+
+  test("falls back to directory-level auto-accept", () => {
+    const directory = "/tmp/project"
+    const sessions = [session({ id: "root" })]
+    const autoAccept = {
+      [`${base64Encode(directory)}/*`]: true,
+    }
+
+    expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(true)
+  })
+
+  test("session-level override takes precedence over directory-level", () => {
+    const directory = "/tmp/project"
+    const sessions = [session({ id: "root" })]
+    const autoAccept = {
+      [`${base64Encode(directory)}/*`]: true,
+      [`${base64Encode(directory)}/root`]: false,
+    }
+
+    expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(false)
+  })
+})
+
+describe("isDirectoryAutoAccepting", () => {
+  test("returns true when directory key is set", () => {
+    const directory = "/tmp/project"
+    const autoAccept = { [`${base64Encode(directory)}/*`]: true }
+    expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(true)
+  })
+
+  test("returns false when directory key is not set", () => {
+    expect(isDirectoryAutoAccepting({}, "/tmp/project")).toBe(false)
+  })
+
+  test("returns false when directory key is explicitly false", () => {
+    const directory = "/tmp/project"
+    const autoAccept = { [`${base64Encode(directory)}/*`]: false }
+    expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(false)
+  })
 })

+ 11 - 1
packages/app/src/context/permission-auto-respond.ts

@@ -5,9 +5,19 @@ export function acceptKey(sessionID: string, directory?: string) {
   return `${base64Encode(directory)}/${sessionID}`
 }
 
+export function directoryAcceptKey(directory: string) {
+  return `${base64Encode(directory)}/*`
+}
+
 function accepted(autoAccept: Record<string, boolean>, sessionID: string, directory?: string) {
   const key = acceptKey(sessionID, directory)
-  return autoAccept[key] ?? autoAccept[sessionID]
+  const directoryKey = directory ? directoryAcceptKey(directory) : undefined
+  return autoAccept[key] ?? autoAccept[sessionID] ?? (directoryKey ? autoAccept[directoryKey] : undefined)
+}
+
+export function isDirectoryAutoAccepting(autoAccept: Record<string, boolean>, directory: string) {
+  const key = directoryAcceptKey(directory)
+  return autoAccept[key] ?? false
 }
 
 function sessionLineage(session: { id: string; parentID?: string }[], sessionID: string) {

+ 68 - 2
packages/app/src/context/permission.tsx

@@ -1,4 +1,4 @@
-import { createMemo, onCleanup } from "solid-js"
+import { createEffect, createMemo, onCleanup } from "solid-js"
 import { createStore, produce } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
@@ -7,7 +7,7 @@ import { useGlobalSDK } from "@/context/global-sdk"
 import { useGlobalSync } from "./global-sync"
 import { useParams } from "@solidjs/router"
 import { decode64 } from "@/utils/base64"
-import { acceptKey, autoRespondsPermission } from "./permission-auto-respond"
+import { acceptKey, directoryAcceptKey, isDirectoryAutoAccepting, autoRespondsPermission } from "./permission-auto-respond"
 
 type PermissionRespondFn = (input: {
   sessionID: string
@@ -76,6 +76,25 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
       }),
     )
 
+    // When config has permission: "allow", auto-enable directory-level auto-accept
+    createEffect(() => {
+      if (!ready()) return
+      const directory = decode64(params.dir)
+      if (!directory) return
+      const [childStore] = globalSync.child(directory)
+      const perm = childStore.config.permission
+      if (typeof perm === "string" && perm === "allow") {
+        const key = directoryAcceptKey(directory)
+        if (store.autoAccept[key] === undefined) {
+          setStore(
+            produce((draft) => {
+              draft.autoAccept[key] = true
+            }),
+          )
+        }
+      }
+    })
+
     const MAX_RESPONDED = 1000
     const RESPONDED_TTL_MS = 60 * 60 * 1000
     const responded = new Map<string, number>()
@@ -119,6 +138,10 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
       return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
     }
 
+    function isAutoAcceptingDirectory(directory: string) {
+      return isDirectoryAutoAccepting(store.autoAccept, directory)
+    }
+
     function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
       const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
       return autoRespondsPermission(store.autoAccept, session, permission, directory)
@@ -142,6 +165,36 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
     })
     onCleanup(unsubscribe)
 
+    function enableDirectory(directory: string) {
+      const key = directoryAcceptKey(directory)
+      setStore(
+        produce((draft) => {
+          draft.autoAccept[key] = true
+        }),
+      )
+
+      globalSDK.client.permission
+        .list({ directory })
+        .then((x) => {
+          if (!isAutoAcceptingDirectory(directory)) return
+          for (const perm of x.data ?? []) {
+            if (!perm?.id) continue
+            if (!shouldAutoRespond(perm, directory)) continue
+            respondOnce(perm, directory)
+          }
+        })
+        .catch(() => undefined)
+    }
+
+    function disableDirectory(directory: string) {
+      const key = directoryAcceptKey(directory)
+      setStore(
+        produce((draft) => {
+          draft.autoAccept[key] = false
+        }),
+      )
+    }
+
     function enable(sessionID: string, directory: string) {
       const key = acceptKey(sessionID, directory)
       const version = bumpEnableVersion(sessionID, directory)
@@ -185,6 +238,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
         return shouldAutoRespond(permission, directory)
       },
       isAutoAccepting,
+      isAutoAcceptingDirectory,
       toggleAutoAccept(sessionID: string, directory: string) {
         if (isAutoAccepting(sessionID, directory)) {
           disable(sessionID, directory)
@@ -193,6 +247,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
 
         enable(sessionID, directory)
       },
+      toggleAutoAcceptDirectory(directory: string) {
+        if (isAutoAcceptingDirectory(directory)) {
+          disableDirectory(directory)
+          return
+        }
+        enableDirectory(directory)
+      },
       enableAutoAccept(sessionID: string, directory: string) {
         if (isAutoAccepting(sessionID, directory)) return
         enable(sessionID, directory)
@@ -201,6 +262,11 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
         disable(sessionID, directory)
       },
       permissionsEnabled,
+      isPermissionAllowAll(directory: string) {
+        const [childStore] = globalSync.child(directory)
+        const perm = childStore.config.permission
+        return typeof perm === "string" && perm === "allow"
+      },
     }
   },
 })

+ 20 - 9
packages/app/src/pages/session/use-session-commands.tsx

@@ -261,24 +261,35 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     }),
   ])
 
+  const isAutoAcceptActive = () => {
+    const sessionID = params.id
+    if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory)
+    return permission.isAutoAcceptingDirectory(sdk.directory)
+  }
+
   const permissionCommands = createMemo(() => [
     permissionsCommand({
       id: "permissions.autoaccept",
-      title:
-        params.id && permission.isAutoAccepting(params.id, sdk.directory)
-          ? language.t("command.permissions.autoaccept.disable")
-          : language.t("command.permissions.autoaccept.enable"),
+      title: isAutoAcceptActive()
+        ? language.t("command.permissions.autoaccept.disable")
+        : language.t("command.permissions.autoaccept.enable"),
       keybind: "mod+shift+a",
-      disabled: !params.id || !permission.permissionsEnabled(),
+      disabled: false,
       onSelect: () => {
         const sessionID = params.id
-        if (!sessionID) return
-        permission.toggleAutoAccept(sessionID, sdk.directory)
+        if (sessionID) {
+          permission.toggleAutoAccept(sessionID, sdk.directory)
+        } else {
+          permission.toggleAutoAcceptDirectory(sdk.directory)
+        }
+        const active = sessionID
+          ? permission.isAutoAccepting(sessionID, sdk.directory)
+          : permission.isAutoAcceptingDirectory(sdk.directory)
         showToast({
-          title: permission.isAutoAccepting(sessionID, sdk.directory)
+          title: active
             ? language.t("toast.permissions.autoaccept.on.title")
             : language.t("toast.permissions.autoaccept.off.title"),
-          description: permission.isAutoAccepting(sessionID, sdk.directory)
+          description: active
             ? language.t("toast.permissions.autoaccept.on.description")
             : language.t("toast.permissions.autoaccept.off.description"),
         })

+ 4 - 2
packages/opencode/script/build.ts

@@ -4,7 +4,7 @@ import { $ } from "bun"
 import fs from "fs"
 import path from "path"
 import { fileURLToPath } from "url"
-import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin"
+import solidPlugin from "@opentui/solid/bun-plugin"
 
 const __filename = fileURLToPath(import.meta.url)
 const __dirname = path.dirname(__filename)
@@ -161,7 +161,9 @@ for (const item of targets) {
   console.log(`building ${name}`)
   await $`mkdir -p dist/${name}/bin`
 
-  const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"))
+  const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
+  const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
+  const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
   const workerPath = "./src/cli/cmd/tui/worker.ts"
 
   // Use platform-specific bunfs root path based on target OS