fix(app): permission notifications
This commit is contained in:
42
packages/app/src/context/permission-auto-respond.test.ts
Normal file
42
packages/app/src/context/permission-auto-respond.test.ts
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
36
packages/app/src/context/permission-auto-respond.ts
Normal file
36
packages/app/src/context/permission-auto-respond.ts
Normal file
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ import { Persist, persisted } from "@/utils/persist"
|
|||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useGlobalSync } from "./global-sync"
|
import { useGlobalSync } from "./global-sync"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
|
import { acceptKey, autoRespondsPermission } from "./permission-auto-respond"
|
||||||
|
|
||||||
type PermissionRespondFn = (input: {
|
type PermissionRespondFn = (input: {
|
||||||
sessionID: string
|
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) {
|
function isAutoAccepting(sessionID: string, directory?: string) {
|
||||||
const key = acceptKey(sessionID, directory)
|
const key = acceptKey(sessionID, directory)
|
||||||
return store.autoAccept[key] ?? store.autoAccept[sessionID] ?? false
|
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) {
|
function bumpEnableVersion(sessionID: string, directory?: string) {
|
||||||
const key = acceptKey(sessionID, directory)
|
const key = acceptKey(sessionID, directory)
|
||||||
const next = (enableVersion.get(key) ?? 0) + 1
|
const next = (enableVersion.get(key) ?? 0) + 1
|
||||||
@@ -136,7 +136,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
if (event?.type !== "permission.asked") return
|
if (event?.type !== "permission.asked") return
|
||||||
|
|
||||||
const perm = event.properties
|
const perm = event.properties
|
||||||
if (!isAutoAccepting(perm.sessionID, e.name)) return
|
if (!shouldAutoRespond(perm, e.name)) return
|
||||||
|
|
||||||
respondOnce(perm, e.name)
|
respondOnce(perm, e.name)
|
||||||
})
|
})
|
||||||
@@ -159,7 +159,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
if (!isAutoAccepting(sessionID, directory)) return
|
if (!isAutoAccepting(sessionID, directory)) return
|
||||||
for (const perm of x.data ?? []) {
|
for (const perm of x.data ?? []) {
|
||||||
if (!perm?.id) continue
|
if (!perm?.id) continue
|
||||||
if (perm.sessionID !== sessionID) continue
|
if (!shouldAutoRespond(perm, directory)) continue
|
||||||
respondOnce(perm, directory)
|
respondOnce(perm, directory)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -181,7 +181,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
ready,
|
ready,
|
||||||
respond,
|
respond,
|
||||||
autoResponds(permission: PermissionRequest, directory?: string) {
|
autoResponds(permission: PermissionRequest, directory?: string) {
|
||||||
return isAutoAccepting(permission.sessionID, directory)
|
return shouldAutoRespond(permission, directory)
|
||||||
},
|
},
|
||||||
isAutoAccepting,
|
isAutoAccepting,
|
||||||
toggleAutoAccept(sessionID: string, directory: string) {
|
toggleAutoAccept(sessionID: string, directory: string) {
|
||||||
|
|||||||
@@ -55,6 +55,28 @@ describe("sessionPermissionRequest", () => {
|
|||||||
|
|
||||||
expect(sessionPermissionRequest(sessions, permissions, "root")).toBeUndefined()
|
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", () => {
|
describe("sessionQuestionRequest", () => {
|
||||||
|
|||||||
@@ -5,15 +5,20 @@ import { useParams } from "@solidjs/router"
|
|||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePermission } from "@/context/permission"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
|
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
|
||||||
|
|
||||||
export function createSessionComposerBlocked() {
|
export function createSessionComposerBlocked() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
const permission = usePermission()
|
||||||
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const permissionRequest = createMemo(() =>
|
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))
|
const questionRequest = createMemo(() => sessionQuestionRequest(sync.data.session, sync.data.question, params.id))
|
||||||
|
|
||||||
@@ -30,13 +35,16 @@ export function createSessionComposerState() {
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const permission = usePermission()
|
||||||
|
|
||||||
const questionRequest = createMemo((): QuestionRequest | undefined => {
|
const questionRequest = createMemo((): QuestionRequest | undefined => {
|
||||||
return sessionQuestionRequest(sync.data.session, sync.data.question, params.id)
|
return sessionQuestionRequest(sync.data.session, sync.data.question, params.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const permissionRequest = createMemo((): PermissionRequest | undefined => {
|
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(() => {
|
const blocked = createMemo(() => {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
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
|
if (!sessionID) return
|
||||||
|
|
||||||
const map = session.reduce((acc, item) => {
|
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
|
if (!id) return
|
||||||
return request[id]?.[0]
|
return request[id]?.find(include)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sessionPermissionRequest(
|
export function sessionPermissionRequest(
|
||||||
session: Session[],
|
session: Session[],
|
||||||
request: Record<string, PermissionRequest[] | undefined>,
|
request: Record<string, PermissionRequest[] | undefined>,
|
||||||
sessionID?: string,
|
sessionID?: string,
|
||||||
|
include?: (item: PermissionRequest) => boolean,
|
||||||
) {
|
) {
|
||||||
return sessionTreeRequest(session, request, sessionID)
|
return sessionTreeRequest(session, request, sessionID, include)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sessionQuestionRequest(
|
export function sessionQuestionRequest(
|
||||||
session: Session[],
|
session: Session[],
|
||||||
request: Record<string, QuestionRequest[] | undefined>,
|
request: Record<string, QuestionRequest[] | undefined>,
|
||||||
sessionID?: string,
|
sessionID?: string,
|
||||||
|
include?: (item: QuestionRequest) => boolean,
|
||||||
) {
|
) {
|
||||||
return sessionTreeRequest(session, request, sessionID)
|
return sessionTreeRequest(session, request, sessionID, include)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user