summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-26 20:36:10 -0600
committerAdam <[email protected]>2026-02-26 20:36:10 -0600
commite9a7c7114184d0092c114ce7a7d9446cf0d366cc (patch)
tree9255b5a23422d4b5ecdc9df77da17993aaab7cfd /packages/app/src
parent4205fbd2aa98c6f62c8caae94e909a6048afbf53 (diff)
downloadopencode-e9a7c7114184d0092c114ce7a7d9446cf0d366cc.tar.gz
opencode-e9a7c7114184d0092c114ce7a7d9446cf0d366cc.zip
fix(app): permission notifications
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/context/permission-auto-respond.test.ts42
-rw-r--r--packages/app/src/context/permission-auto-respond.ts36
-rw-r--r--packages/app/src/context/permission.tsx18
-rw-r--r--packages/app/src/pages/session/composer/session-composer-state.test.ts22
-rw-r--r--packages/app/src/pages/session/composer/session-composer-state.ts12
-rw-r--r--packages/app/src/pages/session/composer/session-request-tree.ts17
6 files changed, 131 insertions, 16 deletions
diff --git a/packages/app/src/context/permission-auto-respond.test.ts b/packages/app/src/context/permission-auto-respond.test.ts
new file mode 100644
index 000000000..1fa1ff3de
--- /dev/null
+++ b/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)
+ })
+})
diff --git a/packages/app/src/context/permission-auto-respond.ts b/packages/app/src/context/permission-auto-respond.ts
new file mode 100644
index 000000000..e45e5f51c
--- /dev/null
+++ b/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
+ })
+}
diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx
index ccfda5e69..d63d4d568 100644
--- a/packages/app/src/context/permission.tsx
+++ b/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) {
diff --git a/packages/app/src/pages/session/composer/session-composer-state.test.ts b/packages/app/src/pages/session/composer/session-composer-state.test.ts
index 7b6029eb3..934d3152a 100644
--- a/packages/app/src/pages/session/composer/session-composer-state.test.ts
+++ b/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", () => {
diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts
index ed65867ef..201846177 100644
--- a/packages/app/src/pages/session/composer/session-composer-state.ts
+++ b/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(() => {
diff --git a/packages/app/src/pages/session/composer/session-request-tree.ts b/packages/app/src/pages/session/composer/session-request-tree.ts
index f9673e254..03872c091 100644
--- a/packages/app/src/pages/session/composer/session-request-tree.ts
+++ b/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)
}