Просмотр исходного кода

fix(app): permission notifications

Adam 1 месяц назад
Родитель
Сommit
e9a7c71141

+ 42 - 0
packages/app/src/context/permission-auto-respond.test.ts

@@ -0,0 +1,42 @@
+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"
+
+const session = (input: { id: string; parentID?: string }) =>
+  ({
+    id: input.id,
+    parentID: input.parentID,
+  }) as Session
+
+const permission = (sessionID: string) =>
+  ({
+    sessionID,
+  }) as Pick<PermissionRequest, "sessionID">
+
+describe("autoRespondsPermission", () => {
+  test("uses a parent session's directory-scoped auto-accept", () => {
+    const directory = "/tmp/project"
+    const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
+    const autoAccept = {
+      [`${base64Encode(directory)}/root`]: true,
+    }
+
+    expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true)
+  })
+
+  test("uses a parent session's legacy auto-accept key", () => {
+    const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
+
+    expect(autoRespondsPermission({ root: true }, sessions, permission("child"), "/tmp/project")).toBe(true)
+  })
+
+  test("ignores auto-accept from unrelated sessions", () => {
+    const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" }), session({ id: "other" })]
+    const autoAccept = {
+      other: true,
+    }
+
+    expect(autoRespondsPermission(autoAccept, sessions, permission("child"), "/tmp/project")).toBe(false)
+  })
+})

+ 36 - 0
packages/app/src/context/permission-auto-respond.ts

@@ -0,0 +1,36 @@
+import { base64Encode } from "@opencode-ai/util/encode"
+
+export function acceptKey(sessionID: string, directory?: string) {
+  if (!directory) return sessionID
+  return `${base64Encode(directory)}/${sessionID}`
+}
+
+function sessionLineage(session: { id: string; parentID?: string }[], sessionID: string) {
+  const parent = session.reduce((acc, item) => {
+    if (item.parentID) acc.set(item.id, item.parentID)
+    return acc
+  }, new Map<string, string>())
+  const seen = new Set([sessionID])
+  const ids = [sessionID]
+
+  for (const id of ids) {
+    const parentID = parent.get(id)
+    if (!parentID || seen.has(parentID)) continue
+    seen.add(parentID)
+    ids.push(parentID)
+  }
+
+  return ids
+}
+
+export function autoRespondsPermission(
+  autoAccept: Record<string, boolean>,
+  session: { id: string; parentID?: string }[],
+  permission: { sessionID: string },
+  directory?: string,
+) {
+  return sessionLineage(session, permission.sessionID).some((id) => {
+    const key = acceptKey(id, directory)
+    return autoAccept[key] ?? autoAccept[id] ?? false
+  })
+}

+ 9 - 9
packages/app/src/context/permission.tsx

@@ -6,8 +6,8 @@ import { Persist, persisted } from "@/utils/persist"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { useGlobalSync } from "./global-sync"
 import { useParams } from "@solidjs/router"
-import { base64Encode } from "@opencode-ai/util/encode"
 import { decode64 } from "@/utils/base64"
+import { acceptKey, autoRespondsPermission } from "./permission-auto-respond"
 
 type PermissionRespondFn = (input: {
   sessionID: string
@@ -114,16 +114,16 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
       })
     }
 
-    function acceptKey(sessionID: string, directory?: string) {
-      if (!directory) return sessionID
-      return `${base64Encode(directory)}/${sessionID}`
-    }
-
     function isAutoAccepting(sessionID: string, directory?: string) {
       const key = acceptKey(sessionID, directory)
       return store.autoAccept[key] ?? store.autoAccept[sessionID] ?? false
     }
 
+    function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
+      const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
+      return autoRespondsPermission(store.autoAccept, session, permission, directory)
+    }
+
     function bumpEnableVersion(sessionID: string, directory?: string) {
       const key = acceptKey(sessionID, directory)
       const next = (enableVersion.get(key) ?? 0) + 1
@@ -136,7 +136,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
       if (event?.type !== "permission.asked") return
 
       const perm = event.properties
-      if (!isAutoAccepting(perm.sessionID, e.name)) return
+      if (!shouldAutoRespond(perm, e.name)) return
 
       respondOnce(perm, e.name)
     })
@@ -159,7 +159,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
           if (!isAutoAccepting(sessionID, directory)) return
           for (const perm of x.data ?? []) {
             if (!perm?.id) continue
-            if (perm.sessionID !== sessionID) continue
+            if (!shouldAutoRespond(perm, directory)) continue
             respondOnce(perm, directory)
           }
         })
@@ -181,7 +181,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
       ready,
       respond,
       autoResponds(permission: PermissionRequest, directory?: string) {
-        return isAutoAccepting(permission.sessionID, directory)
+        return shouldAutoRespond(permission, directory)
       },
       isAutoAccepting,
       toggleAutoAccept(sessionID: string, directory: string) {

+ 22 - 0
packages/app/src/pages/session/composer/session-composer-state.test.ts

@@ -55,6 +55,28 @@ describe("sessionPermissionRequest", () => {
 
     expect(sessionPermissionRequest(sessions, permissions, "root")).toBeUndefined()
   })
+
+  test("skips filtered permissions in the current tree", () => {
+    const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
+    const permissions = {
+      root: [permission("perm-root", "root")],
+      child: [permission("perm-child", "child")],
+    }
+
+    expect(sessionPermissionRequest(sessions, permissions, "root", (item) => item.id !== "perm-root"))?.toMatchObject({
+      id: "perm-child",
+    })
+  })
+
+  test("returns undefined when all tree permissions are filtered out", () => {
+    const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
+    const permissions = {
+      root: [permission("perm-root", "root")],
+      child: [permission("perm-child", "child")],
+    }
+
+    expect(sessionPermissionRequest(sessions, permissions, "root", () => false)).toBeUndefined()
+  })
 })
 
 describe("sessionQuestionRequest", () => {

+ 10 - 2
packages/app/src/pages/session/composer/session-composer-state.ts

@@ -5,15 +5,20 @@ import { useParams } from "@solidjs/router"
 import { showToast } from "@opencode-ai/ui/toast"
 import { useGlobalSync } from "@/context/global-sync"
 import { useLanguage } from "@/context/language"
+import { usePermission } from "@/context/permission"
 import { useSDK } from "@/context/sdk"
 import { useSync } from "@/context/sync"
 import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
 
 export function createSessionComposerBlocked() {
   const params = useParams()
+  const permission = usePermission()
+  const sdk = useSDK()
   const sync = useSync()
   const permissionRequest = createMemo(() =>
-    sessionPermissionRequest(sync.data.session, sync.data.permission, params.id),
+    sessionPermissionRequest(sync.data.session, sync.data.permission, params.id, (item) => {
+      return !permission.autoResponds(item, sdk.directory)
+    }),
   )
   const questionRequest = createMemo(() => sessionQuestionRequest(sync.data.session, sync.data.question, params.id))
 
@@ -30,13 +35,16 @@ export function createSessionComposerState() {
   const sync = useSync()
   const globalSync = useGlobalSync()
   const language = useLanguage()
+  const permission = usePermission()
 
   const questionRequest = createMemo((): QuestionRequest | undefined => {
     return sessionQuestionRequest(sync.data.session, sync.data.question, params.id)
   })
 
   const permissionRequest = createMemo((): PermissionRequest | undefined => {
-    return sessionPermissionRequest(sync.data.session, sync.data.permission, params.id)
+    return sessionPermissionRequest(sync.data.session, sync.data.permission, params.id, (item) => {
+      return !permission.autoResponds(item, sdk.directory)
+    })
   })
 
   const blocked = createMemo(() => {

+ 12 - 5
packages/app/src/pages/session/composer/session-request-tree.ts

@@ -1,6 +1,11 @@
 import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
 
-function sessionTreeRequest<T>(session: Session[], request: Record<string, T[] | undefined>, sessionID?: string) {
+function sessionTreeRequest<T>(
+  session: Session[],
+  request: Record<string, T[] | undefined>,
+  sessionID?: string,
+  include: (item: T) => boolean = () => true,
+) {
   if (!sessionID) return
 
   const map = session.reduce((acc, item) => {
@@ -23,23 +28,25 @@ function sessionTreeRequest<T>(session: Session[], request: Record<string, T[] |
     }
   }
 
-  const id = ids.find((id) => !!request[id]?.[0])
+  const id = ids.find((id) => request[id]?.some(include))
   if (!id) return
-  return request[id]?.[0]
+  return request[id]?.find(include)
 }
 
 export function sessionPermissionRequest(
   session: Session[],
   request: Record<string, PermissionRequest[] | undefined>,
   sessionID?: string,
+  include?: (item: PermissionRequest) => boolean,
 ) {
-  return sessionTreeRequest(session, request, sessionID)
+  return sessionTreeRequest(session, request, sessionID, include)
 }
 
 export function sessionQuestionRequest(
   session: Session[],
   request: Record<string, QuestionRequest[] | undefined>,
   sessionID?: string,
+  include?: (item: QuestionRequest) => boolean,
 ) {
-  return sessionTreeRequest(session, request, sessionID)
+  return sessionTreeRequest(session, request, sessionID, include)
 }