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

fix(desktop): don't show notifs if auto-accepting

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

+ 31 - 28
packages/app/src/app.tsx

@@ -10,6 +10,7 @@ import { Diff } from "@opencode-ai/ui/diff"
 import { Code } from "@opencode-ai/ui/code"
 import { Code } from "@opencode-ai/ui/code"
 import { ThemeProvider } from "@opencode-ai/ui/theme"
 import { ThemeProvider } from "@opencode-ai/ui/theme"
 import { GlobalSyncProvider } from "@/context/global-sync"
 import { GlobalSyncProvider } from "@/context/global-sync"
+import { PermissionProvider } from "@/context/permission"
 import { LayoutProvider } from "@/context/layout"
 import { LayoutProvider } from "@/context/layout"
 import { GlobalSDKProvider } from "@/context/global-sdk"
 import { GlobalSDKProvider } from "@/context/global-sdk"
 import { ServerProvider, useServer } from "@/context/server"
 import { ServerProvider, useServer } from "@/context/server"
@@ -66,34 +67,36 @@ export function App() {
                     <ServerKey>
                     <ServerKey>
                       <GlobalSDKProvider>
                       <GlobalSDKProvider>
                         <GlobalSyncProvider>
                         <GlobalSyncProvider>
-                          <LayoutProvider>
-                            <NotificationProvider>
-                              <Router
-                                root={(props) => (
-                                  <CommandProvider>
-                                    <Layout>{props.children}</Layout>
-                                  </CommandProvider>
-                                )}
-                              >
-                                <Route path="/" component={Home} />
-                                <Route path="/:dir" component={DirectoryLayout}>
-                                  <Route path="/" component={() => <Navigate href="session" />} />
-                                  <Route
-                                    path="/session/:id?"
-                                    component={(p) => (
-                                      <Show when={p.params.id ?? "new"} keyed>
-                                        <TerminalProvider>
-                                          <PromptProvider>
-                                            <Session />
-                                          </PromptProvider>
-                                        </TerminalProvider>
-                                      </Show>
-                                    )}
-                                  />
-                                </Route>
-                              </Router>
-                            </NotificationProvider>
-                          </LayoutProvider>
+                          <PermissionProvider>
+                            <LayoutProvider>
+                              <NotificationProvider>
+                                <Router
+                                  root={(props) => (
+                                    <CommandProvider>
+                                      <Layout>{props.children}</Layout>
+                                    </CommandProvider>
+                                  )}
+                                >
+                                  <Route path="/" component={Home} />
+                                  <Route path="/:dir" component={DirectoryLayout}>
+                                    <Route path="/" component={() => <Navigate href="session" />} />
+                                    <Route
+                                      path="/session/:id?"
+                                      component={(p) => (
+                                        <Show when={p.params.id ?? "new"} keyed>
+                                          <TerminalProvider>
+                                            <PromptProvider>
+                                              <Session />
+                                            </PromptProvider>
+                                          </TerminalProvider>
+                                        </Show>
+                                      )}
+                                    />
+                                  </Route>
+                                </Router>
+                              </NotificationProvider>
+                            </LayoutProvider>
+                          </PermissionProvider>
                         </GlobalSyncProvider>
                         </GlobalSyncProvider>
                       </GlobalSDKProvider>
                       </GlobalSDKProvider>
                     </ServerKey>
                     </ServerKey>

+ 51 - 70
packages/app/src/context/permission.tsx

@@ -1,17 +1,15 @@
-import { createEffect, createRoot, onCleanup } from "solid-js"
+import { onCleanup } from "solid-js"
 import { createStore } from "solid-js/store"
 import { createStore } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import type { Permission } from "@opencode-ai/sdk/v2/client"
 import type { Permission } from "@opencode-ai/sdk/v2/client"
 import { persisted } from "@/utils/persist"
 import { persisted } from "@/utils/persist"
-
-type PermissionsBySession = {
-  [sessionID: string]: Permission[]
-}
+import { useGlobalSDK } from "@/context/global-sdk"
 
 
 type PermissionRespondFn = (input: {
 type PermissionRespondFn = (input: {
   sessionID: string
   sessionID: string
   permissionID: string
   permissionID: string
   response: "once" | "always" | "reject"
   response: "once" | "always" | "reject"
+  directory?: string
 }) => void
 }) => void
 
 
 const AUTO_ACCEPT_TYPES = new Set(["edit", "write"])
 const AUTO_ACCEPT_TYPES = new Set(["edit", "write"])
@@ -22,105 +20,88 @@ function shouldAutoAccept(perm: Permission) {
 
 
 export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
 export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
   name: "Permission",
   name: "Permission",
-  init: (props: { permissions: PermissionsBySession; onRespond: PermissionRespondFn }) => {
+  init: () => {
+    const globalSDK = useGlobalSDK()
     const [store, setStore, _, ready] = persisted(
     const [store, setStore, _, ready] = persisted(
-      "permission.v1",
+      "permission.v3",
       createStore({
       createStore({
         autoAcceptEdits: {} as Record<string, boolean>,
         autoAcceptEdits: {} as Record<string, boolean>,
       }),
       }),
     )
     )
 
 
     const responded = new Set<string>()
     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",
+
+    const respond: PermissionRespondFn = (input) => {
+      globalSDK.client.permission.respond(input).catch(() => {
+        responded.delete(input.permissionID)
       })
       })
     }
     }
 
 
-    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
+    function respondOnce(permission: Permission, directory?: string) {
+      if (responded.has(permission.id)) return
+      responded.add(permission.id)
+      respond({
+        sessionID: permission.sessionID,
+        permissionID: permission.id,
+        response: "once",
+        directory,
       })
       })
-
-      watches.set(sessionID, dispose)
     }
     }
 
 
-    function unwatch(sessionID: string) {
-      const dispose = watches.get(sessionID)
-      if (!dispose) return
-      dispose()
-      watches.delete(sessionID)
+    function isAutoAccepting(sessionID: string) {
+      return store.autoAcceptEdits[sessionID] ?? false
     }
     }
 
 
-    createEffect(() => {
-      if (!ready()) return
+    const unsubscribe = globalSDK.event.listen((e) => {
+      const event = e.details
+      if (event?.type !== "permission.updated") return
 
 
-      for (const sessionID in store.autoAcceptEdits) {
-        if (!store.autoAcceptEdits[sessionID]) continue
-        watch(sessionID)
-      }
-    })
+      const perm = event.properties
+      if (!isAutoAccepting(perm.sessionID)) return
+      if (!shouldAutoAccept(perm)) return
 
 
-    onCleanup(() => {
-      for (const dispose of watches.values()) dispose()
-      watches.clear()
+      respondOnce(perm, e.name)
     })
     })
+    onCleanup(unsubscribe)
 
 
-    function enable(sessionID: string) {
+    function enable(sessionID: string, directory: string) {
       setStore("autoAcceptEdits", sessionID, true)
       setStore("autoAcceptEdits", sessionID, true)
-      watch(sessionID)
 
 
-      const permissions = props.permissions[sessionID] ?? []
-      for (const perm of permissions) {
-        if (!shouldAutoAccept(perm)) continue
-        respond(perm)
-      }
+      globalSDK.client.permission
+        .list({ directory })
+        .then((x) => {
+          for (const perm of x.data ?? []) {
+            if (!perm?.id) continue
+            if (perm.sessionID !== sessionID) continue
+            if (!shouldAutoAccept(perm)) continue
+            respondOnce(perm, directory)
+          }
+        })
+        .catch(() => undefined)
     }
     }
 
 
     function disable(sessionID: string) {
     function disable(sessionID: string) {
       setStore("autoAcceptEdits", sessionID, false)
       setStore("autoAcceptEdits", sessionID, false)
-      unwatch(sessionID)
     }
     }
 
 
     return {
     return {
-      get permissions() {
-        return props.permissions
-      },
-      respond: props.onRespond,
-      isAutoAccepting(sessionID: string) {
-        return store.autoAcceptEdits[sessionID] ?? false
+      ready,
+      respond,
+      autoResponds(permission: Permission) {
+        return isAutoAccepting(permission.sessionID) && shouldAutoAccept(permission)
       },
       },
-      toggleAutoAccept(sessionID: string) {
-        if (store.autoAcceptEdits[sessionID]) {
+      isAutoAccepting,
+      toggleAutoAccept(sessionID: string, directory: string) {
+        if (isAutoAccepting(sessionID)) {
           disable(sessionID)
           disable(sessionID)
           return
           return
         }
         }
 
 
-        enable(sessionID)
+        enable(sessionID, directory)
       },
       },
-      enableAutoAccept(sessionID: string) {
-        if (store.autoAcceptEdits[sessionID]) return
-        enable(sessionID)
+      enableAutoAccept(sessionID: string, directory: string) {
+        if (isAutoAccepting(sessionID)) return
+        enable(sessionID, directory)
       },
       },
       disableAutoAccept(sessionID: string) {
       disableAutoAccept(sessionID: string) {
         disable(sessionID)
         disable(sessionID)

+ 4 - 6
packages/app/src/pages/directory-layout.tsx

@@ -3,7 +3,7 @@ import { useParams } from "@solidjs/router"
 import { SDKProvider, useSDK } from "@/context/sdk"
 import { SDKProvider, useSDK } from "@/context/sdk"
 import { SyncProvider, useSync } from "@/context/sync"
 import { SyncProvider, useSync } from "@/context/sync"
 import { LocalProvider } from "@/context/local"
 import { LocalProvider } from "@/context/local"
-import { PermissionProvider } from "@/context/permission"
+
 import { base64Decode } from "@opencode-ai/util/encode"
 import { base64Decode } from "@opencode-ai/util/encode"
 import { DataProvider } from "@opencode-ai/ui/context"
 import { DataProvider } from "@opencode-ai/ui/context"
 import { iife } from "@opencode-ai/util/iife"
 import { iife } from "@opencode-ai/util/iife"
@@ -27,11 +27,9 @@ export default function Layout(props: ParentProps) {
             }) => sdk.client.permission.respond(input)
             }) => sdk.client.permission.respond(input)
 
 
             return (
             return (
-              <PermissionProvider permissions={sync.data.permission} onRespond={respond}>
-                <DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
-                  <LocalProvider>{props.children}</LocalProvider>
-                </DataProvider>
-              </PermissionProvider>
+              <DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
+                <LocalProvider>{props.children}</LocalProvider>
+              </DataProvider>
             )
             )
           })}
           })}
         </SyncProvider>
         </SyncProvider>

+ 27 - 12
packages/app/src/pages/layout.tsx

@@ -45,6 +45,7 @@ import { useProviders } from "@/hooks/use-providers"
 import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
 import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { useNotification } from "@/context/notification"
 import { useNotification } from "@/context/notification"
+import { usePermission } from "@/context/permission"
 import { Binary } from "@opencode-ai/util/binary"
 import { Binary } from "@opencode-ai/util/binary"
 
 
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -92,6 +93,7 @@ export default function Layout(props: ParentProps) {
   const platform = usePlatform()
   const platform = usePlatform()
   const server = useServer()
   const server = useServer()
   const notification = useNotification()
   const notification = useNotification()
+  const permission = usePermission()
   const navigate = useNavigate()
   const navigate = useNavigate()
   const providers = useProviders()
   const providers = useProviders()
   const dialog = useDialog()
   const dialog = useDialog()
@@ -160,28 +162,41 @@ export default function Layout(props: ParentProps) {
   })
   })
 
 
   onMount(() => {
   onMount(() => {
-    const seenSessions = new Set<string>()
     const toastBySession = new Map<string, number>()
     const toastBySession = new Map<string, number>()
+    const alertedAtBySession = new Map<string, number>()
+    const permissionAlertCooldownMs = 5000
+
     const unsub = globalSDK.event.listen((e) => {
     const unsub = globalSDK.event.listen((e) => {
       if (e.details?.type !== "permission.updated") return
       if (e.details?.type !== "permission.updated") return
       const directory = e.name
       const directory = e.name
-      const permission = e.details.properties
-      const currentDir = params.dir ? base64Decode(params.dir) : undefined
-      const currentSession = params.id
+      const perm = e.details.properties
+      if (permission.autoResponds(perm)) return
+
+      const sessionKey = `${directory}:${perm.sessionID}`
       const [store] = globalSync.child(directory)
       const [store] = globalSync.child(directory)
-      const session = store.session.find((s) => s.id === permission.sessionID)
+      const session = store.session.find((s) => s.id === perm.sessionID)
+
       const sessionTitle = session?.title ?? "New session"
       const sessionTitle = session?.title ?? "New session"
       const projectName = getFilename(directory)
       const projectName = getFilename(directory)
       const description = `${sessionTitle} in ${projectName} needs permission`
       const description = `${sessionTitle} in ${projectName} needs permission`
-      const href = `/${base64Encode(directory)}/session/${permission.sessionID}`
+      const href = `/${base64Encode(directory)}/session/${perm.sessionID}`
+
+      const now = Date.now()
+      const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0
+      if (now - lastAlerted < permissionAlertCooldownMs) return
+      alertedAtBySession.set(sessionKey, now)
+
       void platform.notify("Permission required", description, href)
       void platform.notify("Permission required", description, href)
 
 
-      if (directory === currentDir && permission.sessionID === currentSession) return
+      const currentDir = params.dir ? base64Decode(params.dir) : undefined
+      const currentSession = params.id
+      if (directory === currentDir && perm.sessionID === currentSession) return
       if (directory === currentDir && session?.parentID === currentSession) return
       if (directory === currentDir && session?.parentID === currentSession) return
 
 
-      const sessionKey = `${directory}:${permission.sessionID}`
-      if (seenSessions.has(sessionKey)) return
-      seenSessions.add(sessionKey)
+      const existingToastId = toastBySession.get(sessionKey)
+      if (existingToastId !== undefined) {
+        toaster.dismiss(existingToastId)
+      }
 
 
       const toastId = showToast({
       const toastId = showToast({
         persistent: true,
         persistent: true,
@@ -214,7 +229,7 @@ export default function Layout(props: ParentProps) {
       if (toastId !== undefined) {
       if (toastId !== undefined) {
         toaster.dismiss(toastId)
         toaster.dismiss(toastId)
         toastBySession.delete(sessionKey)
         toastBySession.delete(sessionKey)
-        seenSessions.delete(sessionKey)
+        alertedAtBySession.delete(sessionKey)
       }
       }
       const [store] = globalSync.child(currentDir)
       const [store] = globalSync.child(currentDir)
       const childSessions = store.session.filter((s) => s.parentID === currentSession)
       const childSessions = store.session.filter((s) => s.parentID === currentSession)
@@ -224,7 +239,7 @@ export default function Layout(props: ParentProps) {
         if (childToastId !== undefined) {
         if (childToastId !== undefined) {
           toaster.dismiss(childToastId)
           toaster.dismiss(childToastId)
           toastBySession.delete(childKey)
           toastBySession.delete(childKey)
-          seenSessions.delete(childKey)
+          alertedAtBySession.delete(childKey)
         }
         }
       }
       }
     })
     })

+ 6 - 4
packages/app/src/pages/session.tsx

@@ -556,11 +556,13 @@ export default function Page() {
       category: "Permissions",
       category: "Permissions",
       disabled: !params.id,
       disabled: !params.id,
       onSelect: () => {
       onSelect: () => {
-        if (!params.id) return
-        permission.toggleAutoAccept(params.id)
+        const sessionID = params.id
+        if (!sessionID) return
+
+        permission.toggleAutoAccept(sessionID, sdk.directory)
         showToast({
         showToast({
-          title: permission.isAutoAccepting(params.id) ? "Auto-accepting edits" : "Stopped auto-accepting edits",
-          description: permission.isAutoAccepting(params.id)
+          title: permission.isAutoAccepting(sessionID) ? "Auto-accepting edits" : "Stopped auto-accepting edits",
+          description: permission.isAutoAccepting(sessionID)
             ? "Edit and write permissions will be automatically approved"
             ? "Edit and write permissions will be automatically approved"
             : "Edit and write permissions will require approval",
             : "Edit and write permissions will require approval",
         })
         })