diff options
| author | Adam <[email protected]> | 2025-12-29 12:57:48 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-29 14:23:41 -0600 |
| commit | 31092149002eb69111f4b94a16f875e101c61432 (patch) | |
| tree | e3826bb81c8cbdd667cbdc11ffa8cfc84b25a69d | |
| parent | 86ccc3409b1d3d5cca70c4aec52e5cf0792aa9dd (diff) | |
| download | opencode-31092149002eb69111f4b94a16f875e101c61432.tar.gz opencode-31092149002eb69111f4b94a16f875e101c61432.zip | |
feat(desktop): auto-accept edits toggle
| -rw-r--r-- | packages/app/src/context/permission.tsx | 130 | ||||
| -rw-r--r-- | packages/app/src/pages/directory-layout.tsx | 21 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 19 |
3 files changed, 161 insertions, 9 deletions
diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx new file mode 100644 index 000000000..6d7b335ad --- /dev/null +++ b/packages/app/src/context/permission.tsx @@ -0,0 +1,130 @@ +import { createEffect, createRoot, 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 { persisted } from "@/utils/persist" + +type PermissionsBySession = { + [sessionID: string]: Permission[] +} + +type PermissionRespondFn = (input: { + sessionID: string + permissionID: string + response: "once" | "always" | "reject" +}) => void + +const AUTO_ACCEPT_TYPES = new Set(["edit", "write"]) + +function shouldAutoAccept(perm: Permission) { + return AUTO_ACCEPT_TYPES.has(perm.type) +} + +export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({ + name: "Permission", + init: (props: { permissions: PermissionsBySession; onRespond: PermissionRespondFn }) => { + const [store, setStore, _, ready] = persisted( + "permission.v1", + createStore({ + autoAcceptEdits: {} as Record<string, boolean>, + }), + ) + + const responded = new Set<string>() + const watches = new Map<string, () => void>() + + function respond(perm: Permission) { + if (responded.has(perm.id)) return + responded.add(perm.id) + props.onRespond({ + sessionID: perm.sessionID, + permissionID: perm.id, + response: "once", + }) + } + + function watch(sessionID: string) { + if (watches.has(sessionID)) return + + const dispose = createRoot((dispose) => { + createEffect(() => { + if (!store.autoAcceptEdits[sessionID]) return + + const permissions = props.permissions[sessionID] ?? [] + permissions.length + + for (const perm of permissions) { + if (!shouldAutoAccept(perm)) continue + respond(perm) + } + }) + + return dispose + }) + + watches.set(sessionID, dispose) + } + + function unwatch(sessionID: string) { + const dispose = watches.get(sessionID) + if (!dispose) return + dispose() + watches.delete(sessionID) + } + + createEffect(() => { + if (!ready()) return + + for (const sessionID in store.autoAcceptEdits) { + if (!store.autoAcceptEdits[sessionID]) continue + watch(sessionID) + } + }) + + onCleanup(() => { + for (const dispose of watches.values()) dispose() + watches.clear() + }) + + function enable(sessionID: string) { + setStore("autoAcceptEdits", sessionID, true) + watch(sessionID) + + const permissions = props.permissions[sessionID] ?? [] + for (const perm of permissions) { + if (!shouldAutoAccept(perm)) continue + respond(perm) + } + } + + function disable(sessionID: string) { + setStore("autoAcceptEdits", sessionID, false) + unwatch(sessionID) + } + + return { + get permissions() { + return props.permissions + }, + respond: props.onRespond, + isAutoAccepting(sessionID: string) { + return store.autoAcceptEdits[sessionID] ?? false + }, + toggleAutoAccept(sessionID: string) { + if (store.autoAcceptEdits[sessionID]) { + disable(sessionID) + return + } + + enable(sessionID) + }, + enableAutoAccept(sessionID: string) { + if (store.autoAcceptEdits[sessionID]) return + enable(sessionID) + }, + disableAutoAccept(sessionID: string) { + disable(sessionID) + }, + } + }, +}) diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 04f90bdcb..473dcd8e1 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -3,6 +3,7 @@ import { useParams } from "@solidjs/router" import { SDKProvider, useSDK } from "@/context/sdk" import { SyncProvider, useSync } from "@/context/sync" import { LocalProvider } from "@/context/local" +import { PermissionProvider } from "@/context/permission" import { base64Decode } from "@opencode-ai/util/encode" import { DataProvider } from "@opencode-ai/ui/context" import { iife } from "@opencode-ai/util/iife" @@ -19,16 +20,18 @@ export default function Layout(props: ParentProps) { {iife(() => { const sync = useSync() const sdk = useSDK() + const respond = (input: { + sessionID: string + permissionID: string + response: "once" | "always" | "reject" + }) => sdk.client.permission.respond(input) + return ( - <DataProvider - data={sync.data} - directory={directory()} - onPermissionRespond={(input) => { - sdk.client.permission.respond(input) - }} - > - <LocalProvider>{props.children}</LocalProvider> - </DataProvider> + <PermissionProvider permissions={sync.data.permission} onRespond={respond}> + <DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}> + <LocalProvider>{props.children}</LocalProvider> + </DataProvider> + </PermissionProvider> ) })} </SyncProvider> diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 6bc39daca..7be5552de 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -60,6 +60,8 @@ import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { StatusBar } from "@/components/status-bar" import { SessionMcpIndicator } from "@/components/session-mcp-indicator" import { SessionLspIndicator } from "@/components/session-lsp-indicator" +import { usePermission } from "@/context/permission" +import { showToast } from "@opencode-ai/ui/toast" export default function Page() { const layout = useLayout() @@ -74,6 +76,7 @@ export default function Page() { const sdk = useSDK() const prompt = usePrompt() + const permission = usePermission() const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) @@ -313,6 +316,22 @@ export default function Page() { onSelect: () => local.agent.move(-1), }, { + id: "permissions.autoaccept", + title: params.id && permission.isAutoAccepting(params.id) ? "Stop auto-accepting edits" : "Auto-accept edits", + category: "Permissions", + disabled: !params.id, + onSelect: () => { + if (!params.id) return + permission.toggleAutoAccept(params.id) + showToast({ + title: permission.isAutoAccepting(params.id) ? "Auto-accepting edits" : "Stopped auto-accepting edits", + description: permission.isAutoAccepting(params.id) + ? "Edit and write permissions will be automatically approved" + : "Edit and write permissions will require approval", + }) + }, + }, + { id: "session.undo", title: "Undo", description: "Undo the last message", |
