Skip to content

Commit e9a7c71

Browse files
committed
fix(app): permission notifications
1 parent 4205fbd commit e9a7c71

6 files changed

Lines changed: 131 additions & 16 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, test } from "bun:test"
2+
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
3+
import { base64Encode } from "@opencode-ai/util/encode"
4+
import { autoRespondsPermission } from "./permission-auto-respond"
5+
6+
const session = (input: { id: string; parentID?: string }) =>
7+
({
8+
id: input.id,
9+
parentID: input.parentID,
10+
}) as Session
11+
12+
const permission = (sessionID: string) =>
13+
({
14+
sessionID,
15+
}) as Pick<PermissionRequest, "sessionID">
16+
17+
describe("autoRespondsPermission", () => {
18+
test("uses a parent session's directory-scoped auto-accept", () => {
19+
const directory = "/tmp/project"
20+
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
21+
const autoAccept = {
22+
[`${base64Encode(directory)}/root`]: true,
23+
}
24+
25+
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true)
26+
})
27+
28+
test("uses a parent session's legacy auto-accept key", () => {
29+
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
30+
31+
expect(autoRespondsPermission({ root: true }, sessions, permission("child"), "/tmp/project")).toBe(true)
32+
})
33+
34+
test("ignores auto-accept from unrelated sessions", () => {
35+
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" }), session({ id: "other" })]
36+
const autoAccept = {
37+
other: true,
38+
}
39+
40+
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), "/tmp/project")).toBe(false)
41+
})
42+
})
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { base64Encode } from "@opencode-ai/util/encode"
2+
3+
export function acceptKey(sessionID: string, directory?: string) {
4+
if (!directory) return sessionID
5+
return `${base64Encode(directory)}/${sessionID}`
6+
}
7+
8+
function sessionLineage(session: { id: string; parentID?: string }[], sessionID: string) {
9+
const parent = session.reduce((acc, item) => {
10+
if (item.parentID) acc.set(item.id, item.parentID)
11+
return acc
12+
}, new Map<string, string>())
13+
const seen = new Set([sessionID])
14+
const ids = [sessionID]
15+
16+
for (const id of ids) {
17+
const parentID = parent.get(id)
18+
if (!parentID || seen.has(parentID)) continue
19+
seen.add(parentID)
20+
ids.push(parentID)
21+
}
22+
23+
return ids
24+
}
25+
26+
export function autoRespondsPermission(
27+
autoAccept: Record<string, boolean>,
28+
session: { id: string; parentID?: string }[],
29+
permission: { sessionID: string },
30+
directory?: string,
31+
) {
32+
return sessionLineage(session, permission.sessionID).some((id) => {
33+
const key = acceptKey(id, directory)
34+
return autoAccept[key] ?? autoAccept[id] ?? false
35+
})
36+
}

packages/app/src/context/permission.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { Persist, persisted } from "@/utils/persist"
66
import { useGlobalSDK } from "@/context/global-sdk"
77
import { useGlobalSync } from "./global-sync"
88
import { useParams } from "@solidjs/router"
9-
import { base64Encode } from "@opencode-ai/util/encode"
109
import { decode64 } from "@/utils/base64"
10+
import { acceptKey, autoRespondsPermission } from "./permission-auto-respond"
1111

1212
type PermissionRespondFn = (input: {
1313
sessionID: string
@@ -114,16 +114,16 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
114114
})
115115
}
116116

117-
function acceptKey(sessionID: string, directory?: string) {
118-
if (!directory) return sessionID
119-
return `${base64Encode(directory)}/${sessionID}`
120-
}
121-
122117
function isAutoAccepting(sessionID: string, directory?: string) {
123118
const key = acceptKey(sessionID, directory)
124119
return store.autoAccept[key] ?? store.autoAccept[sessionID] ?? false
125120
}
126121

122+
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
123+
const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
124+
return autoRespondsPermission(store.autoAccept, session, permission, directory)
125+
}
126+
127127
function bumpEnableVersion(sessionID: string, directory?: string) {
128128
const key = acceptKey(sessionID, directory)
129129
const next = (enableVersion.get(key) ?? 0) + 1
@@ -136,7 +136,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
136136
if (event?.type !== "permission.asked") return
137137

138138
const perm = event.properties
139-
if (!isAutoAccepting(perm.sessionID, e.name)) return
139+
if (!shouldAutoRespond(perm, e.name)) return
140140

141141
respondOnce(perm, e.name)
142142
})
@@ -159,7 +159,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
159159
if (!isAutoAccepting(sessionID, directory)) return
160160
for (const perm of x.data ?? []) {
161161
if (!perm?.id) continue
162-
if (perm.sessionID !== sessionID) continue
162+
if (!shouldAutoRespond(perm, directory)) continue
163163
respondOnce(perm, directory)
164164
}
165165
})
@@ -181,7 +181,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
181181
ready,
182182
respond,
183183
autoResponds(permission: PermissionRequest, directory?: string) {
184-
return isAutoAccepting(permission.sessionID, directory)
184+
return shouldAutoRespond(permission, directory)
185185
},
186186
isAutoAccepting,
187187
toggleAutoAccept(sessionID: string, directory: string) {

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,28 @@ describe("sessionPermissionRequest", () => {
5555

5656
expect(sessionPermissionRequest(sessions, permissions, "root")).toBeUndefined()
5757
})
58+
59+
test("skips filtered permissions in the current tree", () => {
60+
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
61+
const permissions = {
62+
root: [permission("perm-root", "root")],
63+
child: [permission("perm-child", "child")],
64+
}
65+
66+
expect(sessionPermissionRequest(sessions, permissions, "root", (item) => item.id !== "perm-root"))?.toMatchObject({
67+
id: "perm-child",
68+
})
69+
})
70+
71+
test("returns undefined when all tree permissions are filtered out", () => {
72+
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
73+
const permissions = {
74+
root: [permission("perm-root", "root")],
75+
child: [permission("perm-child", "child")],
76+
}
77+
78+
expect(sessionPermissionRequest(sessions, permissions, "root", () => false)).toBeUndefined()
79+
})
5880
})
5981

6082
describe("sessionQuestionRequest", () => {

packages/app/src/pages/session/composer/session-composer-state.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@ import { useParams } from "@solidjs/router"
55
import { showToast } from "@opencode-ai/ui/toast"
66
import { useGlobalSync } from "@/context/global-sync"
77
import { useLanguage } from "@/context/language"
8+
import { usePermission } from "@/context/permission"
89
import { useSDK } from "@/context/sdk"
910
import { useSync } from "@/context/sync"
1011
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
1112

1213
export function createSessionComposerBlocked() {
1314
const params = useParams()
15+
const permission = usePermission()
16+
const sdk = useSDK()
1417
const sync = useSync()
1518
const permissionRequest = createMemo(() =>
16-
sessionPermissionRequest(sync.data.session, sync.data.permission, params.id),
19+
sessionPermissionRequest(sync.data.session, sync.data.permission, params.id, (item) => {
20+
return !permission.autoResponds(item, sdk.directory)
21+
}),
1722
)
1823
const questionRequest = createMemo(() => sessionQuestionRequest(sync.data.session, sync.data.question, params.id))
1924

@@ -30,13 +35,16 @@ export function createSessionComposerState() {
3035
const sync = useSync()
3136
const globalSync = useGlobalSync()
3237
const language = useLanguage()
38+
const permission = usePermission()
3339

3440
const questionRequest = createMemo((): QuestionRequest | undefined => {
3541
return sessionQuestionRequest(sync.data.session, sync.data.question, params.id)
3642
})
3743

3844
const permissionRequest = createMemo((): PermissionRequest | undefined => {
39-
return sessionPermissionRequest(sync.data.session, sync.data.permission, params.id)
45+
return sessionPermissionRequest(sync.data.session, sync.data.permission, params.id, (item) => {
46+
return !permission.autoResponds(item, sdk.directory)
47+
})
4048
})
4149

4250
const blocked = createMemo(() => {

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
22

3-
function sessionTreeRequest<T>(session: Session[], request: Record<string, T[] | undefined>, sessionID?: string) {
3+
function sessionTreeRequest<T>(
4+
session: Session[],
5+
request: Record<string, T[] | undefined>,
6+
sessionID?: string,
7+
include: (item: T) => boolean = () => true,
8+
) {
49
if (!sessionID) return
510

611
const map = session.reduce((acc, item) => {
@@ -23,23 +28,25 @@ function sessionTreeRequest<T>(session: Session[], request: Record<string, T[] |
2328
}
2429
}
2530

26-
const id = ids.find((id) => !!request[id]?.[0])
31+
const id = ids.find((id) => request[id]?.some(include))
2732
if (!id) return
28-
return request[id]?.[0]
33+
return request[id]?.find(include)
2934
}
3035

3136
export function sessionPermissionRequest(
3237
session: Session[],
3338
request: Record<string, PermissionRequest[] | undefined>,
3439
sessionID?: string,
40+
include?: (item: PermissionRequest) => boolean,
3541
) {
36-
return sessionTreeRequest(session, request, sessionID)
42+
return sessionTreeRequest(session, request, sessionID, include)
3743
}
3844

3945
export function sessionQuestionRequest(
4046
session: Session[],
4147
request: Record<string, QuestionRequest[] | undefined>,
4248
sessionID?: string,
49+
include?: (item: QuestionRequest) => boolean,
4350
) {
44-
return sessionTreeRequest(session, request, sessionID)
51+
return sessionTreeRequest(session, request, sessionID, include)
4552
}

0 commit comments

Comments
 (0)