From 16c615fd477f98c882af005835fb8f2a2d9c07f9 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:24:48 -0600 Subject: [PATCH] wip: app permissions changes --- packages/app/src/context/global-sync.tsx | 10 ++--- packages/app/src/context/permission.tsx | 14 +++---- packages/app/src/pages/layout.tsx | 2 +- packages/opencode/src/permission/next.ts | 4 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 2 + packages/sdk/js/src/v2/gen/types.gen.ts | 45 ++++----------------- packages/ui/src/components/message-part.tsx | 19 ++++----- packages/ui/src/components/session-turn.tsx | 22 +++++----- packages/ui/src/context/data.tsx | 4 +- 9 files changed, 47 insertions(+), 75 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index dd040d8d5d..92de0a6368 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -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 = {} + const grouped: Record = {} 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", diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx index a0ad1ee05b..0614703611 100644 --- a/packages/app/src/context/permission.tsx +++ b/packages/app/src/context/permission.tsx @@ -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, diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 0bcf0f7a2d..7aa1e24485 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -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 diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index 83e98a70fa..4f7d831e75 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -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(), diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 9ab91b7e9d..f56e836779 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -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( parameters: { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 69276f996f..10764bebee 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -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 - 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 + 200: Array } export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index ac80dada73..d0e8afefd6 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -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 }) diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 0ef1f135c7..8285b98229 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -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 }) diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx index 3292ba579f..9f7ec813f9 100644 --- a/packages/ui/src/context/data.tsx +++ b/packages/ui/src/context/data.tsx @@ -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[] } permission?: { - [sessionID: string]: Permission[] + [sessionID: string]: PermissionRequest[] } message: { [sessionID: string]: Message[]