diff options
| author | Adam <[email protected]> | 2025-12-31 13:12:24 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-31 13:12:31 -0600 |
| commit | 93845db4623f157dff96a3a1ef440e21ab865769 (patch) | |
| tree | 9545ed6d2139e20c53a2c3f32f2b89691b3e7da4 | |
| parent | 65bc72098b737bee12f388215913808fa1629a4d (diff) | |
| download | opencode-93845db4623f157dff96a3a1ef440e21ab865769.tar.gz opencode-93845db4623f157dff96a3a1ef440e21ab865769.zip | |
fix(desktop): don't show notifs if auto-accepting
| -rw-r--r-- | packages/app/src/app.tsx | 59 | ||||
| -rw-r--r-- | packages/app/src/context/permission.tsx | 121 | ||||
| -rw-r--r-- | packages/app/src/pages/directory-layout.tsx | 10 | ||||
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 39 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 10 |
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", }) |
