summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-31 13:12:24 -0600
committerAdam <[email protected]>2025-12-31 13:12:31 -0600
commit93845db4623f157dff96a3a1ef440e21ab865769 (patch)
tree9545ed6d2139e20c53a2c3f32f2b89691b3e7da4
parent65bc72098b737bee12f388215913808fa1629a4d (diff)
downloadopencode-93845db4623f157dff96a3a1ef440e21ab865769.tar.gz
opencode-93845db4623f157dff96a3a1ef440e21ab865769.zip
fix(desktop): don't show notifs if auto-accepting
-rw-r--r--packages/app/src/app.tsx59
-rw-r--r--packages/app/src/context/permission.tsx121
-rw-r--r--packages/app/src/pages/directory-layout.tsx10
-rw-r--r--packages/app/src/pages/layout.tsx39
-rw-r--r--packages/app/src/pages/session.tsx10
5 files changed, 119 insertions, 120 deletions
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index ff9b8b4db..cc1f052f6 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -10,6 +10,7 @@ import { Diff } from "@opencode-ai/ui/diff"
import { Code } from "@opencode-ai/ui/code"
import { ThemeProvider } from "@opencode-ai/ui/theme"
import { GlobalSyncProvider } from "@/context/global-sync"
+import { PermissionProvider } from "@/context/permission"
import { LayoutProvider } from "@/context/layout"
import { GlobalSDKProvider } from "@/context/global-sdk"
import { ServerProvider, useServer } from "@/context/server"
@@ -66,34 +67,36 @@ export function App() {
<ServerKey>
<GlobalSDKProvider>
<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>
</GlobalSDKProvider>
</ServerKey>
diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx
index 6d7b335ad..d82a8392c 100644
--- a/packages/app/src/context/permission.tsx
+++ b/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 { 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[]
-}
+import { useGlobalSDK } from "@/context/global-sdk"
type PermissionRespondFn = (input: {
sessionID: string
permissionID: string
response: "once" | "always" | "reject"
+ directory?: string
}) => void
const AUTO_ACCEPT_TYPES = new Set(["edit", "write"])
@@ -22,105 +20,88 @@ function shouldAutoAccept(perm: Permission) {
export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
name: "Permission",
- init: (props: { permissions: PermissionsBySession; onRespond: PermissionRespondFn }) => {
+ init: () => {
+ const globalSDK = useGlobalSDK()
const [store, setStore, _, ready] = persisted(
- "permission.v1",
+ "permission.v3",
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",
+
+ 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)
- 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) {
setStore("autoAcceptEdits", sessionID, false)
- unwatch(sessionID)
}
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)
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) {
disable(sessionID)
diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx
index 473dcd8e1..6aac3c384 100644
--- a/packages/app/src/pages/directory-layout.tsx
+++ b/packages/app/src/pages/directory-layout.tsx
@@ -3,7 +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"
@@ -27,11 +27,9 @@ export default function Layout(props: ParentProps) {
}) => sdk.client.permission.respond(input)
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>
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index ce753ecc3..d6d24e9ad 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/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 { useGlobalSDK } from "@/context/global-sdk"
import { useNotification } from "@/context/notification"
+import { usePermission } from "@/context/permission"
import { Binary } from "@opencode-ai/util/binary"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -92,6 +93,7 @@ export default function Layout(props: ParentProps) {
const platform = usePlatform()
const server = useServer()
const notification = useNotification()
+ const permission = usePermission()
const navigate = useNavigate()
const providers = useProviders()
const dialog = useDialog()
@@ -160,28 +162,41 @@ export default function Layout(props: ParentProps) {
})
onMount(() => {
- const seenSessions = new Set<string>()
const toastBySession = new Map<string, number>()
+ const alertedAtBySession = new Map<string, number>()
+ const permissionAlertCooldownMs = 5000
+
const unsub = globalSDK.event.listen((e) => {
if (e.details?.type !== "permission.updated") return
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 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 projectName = getFilename(directory)
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)
- 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
- 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({
persistent: true,
@@ -214,7 +229,7 @@ export default function Layout(props: ParentProps) {
if (toastId !== undefined) {
toaster.dismiss(toastId)
toastBySession.delete(sessionKey)
- seenSessions.delete(sessionKey)
+ alertedAtBySession.delete(sessionKey)
}
const [store] = globalSync.child(currentDir)
const childSessions = store.session.filter((s) => s.parentID === currentSession)
@@ -224,7 +239,7 @@ export default function Layout(props: ParentProps) {
if (childToastId !== undefined) {
toaster.dismiss(childToastId)
toastBySession.delete(childKey)
- seenSessions.delete(childKey)
+ alertedAtBySession.delete(childKey)
}
}
})
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 125dc19d3..1cea23de4 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -556,11 +556,13 @@ export default function Page() {
category: "Permissions",
disabled: !params.id,
onSelect: () => {
- if (!params.id) return
- permission.toggleAutoAccept(params.id)
+ const sessionID = params.id
+ if (!sessionID) return
+
+ permission.toggleAutoAccept(sessionID, sdk.directory)
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 require approval",
})