wip: app permissions changes

pull/6319/head
Adam 2026-01-01 13:24:48 -06:00 committed by Dax Raad
parent 0ea4a769de
commit 16c615fd47
9 changed files with 47 additions and 75 deletions

View File

@ -15,7 +15,7 @@ import {
type McpStatus,
type LspStatus,
type VcsInfo,
type Permission,
type PermissionRequest,
createOpencodeClient,
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
@ -46,7 +46,7 @@ type State = {
[sessionID: string]: Todo[]
}
permission: {
[sessionID: string]: Permission[]
[sessionID: string]: PermissionRequest[]
}
mcp: {
[name: string]: McpStatus
@ -168,7 +168,7 @@ function createGlobalSync() {
vcs: () => sdk.vcs.get().then((x) => setStore("vcs", x.data)),
permission: () =>
sdk.permission.list().then((x) => {
const grouped: Record<string, Permission[]> = {}
const grouped: Record<string, PermissionRequest[]> = {}
for (const perm of x.data ?? []) {
if (!perm?.id || !perm.sessionID) continue
const existing = grouped[perm.sessionID]
@ -349,7 +349,7 @@ function createGlobalSync() {
setStore("vcs", { branch: event.properties.branch })
break
}
case "permission.updated": {
case "permission.asked": {
const sessionID = event.properties.sessionID
const permissions = store.permission[sessionID]
if (!permissions) {
@ -375,7 +375,7 @@ function createGlobalSync() {
case "permission.replied": {
const permissions = store.permission[event.properties.sessionID]
if (!permissions) break
const result = Binary.search(permissions, event.properties.permissionID, (p) => p.id)
const result = Binary.search(permissions, event.properties.requestID, (p) => p.id)
if (!result.found) break
setStore(
"permission",

View File

@ -1,7 +1,7 @@
import { createMemo, 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 type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
import { persisted } from "@/utils/persist"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "./global-sync"
@ -14,10 +14,8 @@ type PermissionRespondFn = (input: {
directory?: string
}) => void
const AUTO_ACCEPT_TYPES = new Set(["edit", "write"])
function shouldAutoAccept(perm: Permission) {
return AUTO_ACCEPT_TYPES.has(perm.type)
function shouldAutoAccept(perm: PermissionRequest) {
return perm.permission === "edit"
}
export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
@ -48,7 +46,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
})
}
function respondOnce(permission: Permission, directory?: string) {
function respondOnce(permission: PermissionRequest, directory?: string) {
if (responded.has(permission.id)) return
responded.add(permission.id)
respond({
@ -65,7 +63,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
const unsubscribe = globalSDK.event.listen((e) => {
const event = e.details
if (event?.type !== "permission.updated") return
if (event?.type !== "permission.asked") return
const perm = event.properties
if (!isAutoAccepting(perm.sessionID)) return
@ -98,7 +96,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
return {
ready,
respond,
autoResponds(permission: Permission) {
autoResponds(permission: PermissionRequest) {
return isAutoAccepting(permission.sessionID) && shouldAutoAccept(permission)
},
isAutoAccepting,

View File

@ -175,7 +175,7 @@ export default function Layout(props: ParentProps) {
const permissionAlertCooldownMs = 5000
const unsub = globalSDK.event.listen((e) => {
if (e.details?.type !== "permission.updated") return
if (e.details?.type !== "permission.asked") return
const directory = e.name
const perm = e.details.properties
if (permission.autoResponds(perm)) return

View File

@ -83,9 +83,9 @@ export namespace PermissionNext {
})
export const Event = {
Asked: BusEvent.define("permission.next.asked", Request),
Asked: BusEvent.define("permission.asked", Request),
Replied: BusEvent.define(
"permission.next.replied",
"permission.replied",
z.object({
sessionID: z.string(),
requestID: z.string(),

View File

@ -1596,6 +1596,8 @@ export class Permission extends HeyApiClient {
* Respond to permission
*
* Approve or deny a permission request from the AI assistant.
*
* @deprecated
*/
public respond<ThrowOnError extends boolean = false>(
parameters: {

View File

@ -466,47 +466,17 @@ export type PermissionRequest = {
}
}
export type EventPermissionNextAsked = {
type: "permission.next.asked"
export type EventPermissionAsked = {
type: "permission.asked"
properties: PermissionRequest
}
export type EventPermissionNextReplied = {
type: "permission.next.replied"
properties: {
sessionID: string
requestID: string
reply: "once" | "always" | "reject"
}
}
export type Permission = {
id: string
type: string
pattern?: string | Array<string>
sessionID: string
messageID: string
callID?: string
message: string
metadata: {
[key: string]: unknown
}
time: {
created: number
}
}
export type EventPermissionUpdated = {
type: "permission.updated"
properties: Permission
}
export type EventPermissionReplied = {
type: "permission.replied"
properties: {
sessionID: string
permissionID: string
response: string
requestID: string
reply: "once" | "always" | "reject"
}
}
@ -796,9 +766,7 @@ export type Event =
| EventMessageRemoved
| EventMessagePartUpdated
| EventMessagePartRemoved
| EventPermissionNextAsked
| EventPermissionNextReplied
| EventPermissionUpdated
| EventPermissionAsked
| EventPermissionReplied
| EventSessionStatus
| EventSessionIdle
@ -1248,6 +1216,7 @@ export type PermissionConfig =
webfetch?: PermissionActionConfig
websearch?: PermissionActionConfig
codesearch?: PermissionActionConfig
lsp?: PermissionRuleConfig
doom_loop?: PermissionActionConfig
[key: string]: PermissionRuleConfig | PermissionActionConfig | undefined
}
@ -3457,7 +3426,7 @@ export type PermissionListResponses = {
/**
* List of pending permissions
*/
200: Array<Permission>
200: Array<PermissionRequest>
}
export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses]

View File

@ -455,8 +455,8 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
const permission = createMemo(() => {
const next = data.store.permission?.[props.message.sessionID]?.[0]
if (!next) return undefined
if (next.callID !== part.callID) return undefined
if (!next || !next.tool) return undefined
if (next.tool!.callID !== part.callID) return undefined
return next
})
@ -732,19 +732,20 @@ ToolRegistry.register({
const childToolPart = createMemo(() => {
const perm = childPermission()
if (!perm) return undefined
if (!perm || !perm.tool) return undefined
const sessionId = childSessionId()
if (!sessionId) return undefined
// Find the tool part that matches the permission's callID
const messages = data.store.message[sessionId] ?? []
for (const msg of messages) {
const parts = data.store.part[msg.id] ?? []
for (const part of parts) {
if (part.type === "tool" && (part as ToolPart).callID === perm.callID) {
return { part: part as ToolPart, message: msg }
}
const message = messages.findLast((m) => m.id === perm.tool!.messageID)
if (!message) return undefined
const parts = data.store.part[message.id] ?? []
for (const part of parts) {
if (part.type === "tool" && (part as ToolPart).callID === perm.tool!.callID) {
return { part: part as ToolPart, message }
}
}
return undefined
})

View File

@ -2,7 +2,7 @@ import {
AssistantMessage,
Message as MessageType,
Part as PartType,
type Permission,
type PermissionRequest,
TextPart,
ToolPart,
} from "@opencode-ai/sdk/v2/client"
@ -132,7 +132,7 @@ export function SessionTurn(
const emptyMessages: MessageType[] = []
const emptyParts: PartType[] = []
const emptyAssistant: AssistantMessage[] = []
const emptyPermissions: Permission[] = []
const emptyPermissions: PermissionRequest[] = []
const emptyPermissionParts: { part: ToolPart; message: AssistantMessage }[] = []
const idle = { type: "idle" as const }
@ -235,16 +235,18 @@ export function SessionTurn(
if (props.stepsExpanded) return emptyPermissionParts
const next = nextPermission()
if (!next) return emptyPermissionParts
if (!next || !next.tool) return emptyPermissionParts
for (const message of assistantMessages()) {
const parts = data.store.part[message.id] ?? emptyParts
for (const part of parts) {
if (part?.type !== "tool") continue
const tool = part as ToolPart
if (tool.callID === next.callID) return [{ part: tool, message }]
}
const message = assistantMessages().findLast((m) => m.id === next.tool!.messageID)
if (!message) return emptyPermissionParts
const parts = data.store.part[message.id] ?? emptyParts
for (const part of parts) {
if (part?.type !== "tool") continue
const tool = part as ToolPart
if (tool.callID === next.tool?.callID) return [{ part: tool, message }]
}
return emptyPermissionParts
})

View File

@ -1,4 +1,4 @@
import type { Message, Session, Part, FileDiff, SessionStatus, Permission } from "@opencode-ai/sdk/v2"
import type { Message, Session, Part, FileDiff, SessionStatus, PermissionRequest } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "./helper"
import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
@ -14,7 +14,7 @@ type Data = {
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
}
permission?: {
[sessionID: string]: Permission[]
[sessionID: string]: PermissionRequest[]
}
message: {
[sessionID: string]: Message[]