summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-29 12:57:48 -0600
committerAdam <[email protected]>2025-12-29 14:23:41 -0600
commit31092149002eb69111f4b94a16f875e101c61432 (patch)
treee3826bb81c8cbdd667cbdc11ffa8cfc84b25a69d
parent86ccc3409b1d3d5cca70c4aec52e5cf0792aa9dd (diff)
downloadopencode-31092149002eb69111f4b94a16f875e101c61432.tar.gz
opencode-31092149002eb69111f4b94a16f875e101c61432.zip
feat(desktop): auto-accept edits toggle
-rw-r--r--packages/app/src/context/permission.tsx130
-rw-r--r--packages/app/src/pages/directory-layout.tsx21
-rw-r--r--packages/app/src/pages/session.tsx19
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",