diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 5e839ead5c..9c9a2a7c75 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -93,6 +93,7 @@ export const layer = Layer.effect( question: "deny", plan_enter: "deny", plan_exit: "deny", + edit: "ask", // mirrors github.com/github/gitignore Node.gitignore pattern for .env files read: { "*": "allow", diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 4c60023b76..bb8486d1a1 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -367,6 +367,11 @@ export const RunCommand = cmd({ action: "deny", pattern: "*", }, + { + permission: "edit", + action: "allow", + pattern: "*", + }, ] function title() { diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index d103d725a2..650e8d6c5e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -138,6 +138,7 @@ export function Prompt(props: PromptProps) { const [auto, setAuto] = createSignal() const currentProviderLabel = createMemo(() => local.model.parsed().provider) const hasRightContent = createMemo(() => Boolean(props.right)) + const [autoaccept, setAutoaccept] = kv.signal<"none" | "edit">("permission_auto_accept", "edit") function promptModelWarning() { toast.show({ @@ -254,6 +255,17 @@ export function Prompt(props: PromptProps) { command.register(() => { return [ + { + title: autoaccept() === "none" ? "Enable autoedit" : "Disable autoedit", + value: "permission.auto_accept.toggle", + search: "toggle permissions", + keybind: "permission_auto_accept_toggle", + category: "Agent", + onSelect: (dialog) => { + setAutoaccept(() => (autoaccept() === "none" ? "edit" : "none")) + dialog.clear() + }, + }, { title: "Clear prompt", value: "prompt.clear", @@ -1280,9 +1292,14 @@ export function Prompt(props: PromptProps) { )} - + - {props.right} + + + autoedit + + + {props.right} diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 7b18d7f4ee..a11bf59257 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -27,6 +27,7 @@ import { createSimpleContext } from "./helper" import type { Snapshot } from "@/snapshot" import { useExit } from "./exit" import { useArgs } from "./args" +import { useKV } from "./kv" import { batch, onMount } from "solid-js" import * as Log from "@opencode-ai/core/util/log" import { emptyConsoleState, type ConsoleState } from "@/config/console-state" @@ -107,6 +108,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const event = useEvent() const project = useProject() const sdk = useSDK() + const kv = useKV() + const [autoaccept] = kv.signal<"none" | "edit">("permission_auto_accept", "edit") const fullSyncedSessions = new Set() let syncedWorkspace = project.workspace.current() @@ -133,6 +136,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ case "permission.asked": { const request = event.properties + if (autoaccept() === "edit" && request.permission === "edit") { + sdk.client.permission.reply({ + reply: "once", + requestID: request.id, + }) + break + } const requests = store.permission[request.sessionID] if (!requests) { setStore("permission", request.sessionID, [request]) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 183ce52cde..913bb03422 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -619,6 +619,7 @@ export function Session() { { title: conceal() ? "Disable code concealment" : "Enable code concealment", value: "session.toggle.conceal", + search: "toggle code concealment", keybind: "messages_toggle_conceal", category: "Session", onSelect: (dialog) => { diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index b6c937f411..40d84e19bc 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -37,6 +37,7 @@ export interface DialogSelectOption { title: string value: T description?: string + search?: string footer?: JSX.Element | string category?: string categoryView?: JSX.Element @@ -93,8 +94,8 @@ export function DialogSelect(props: DialogSelectProps) { // users typically search by the item name, and not its category. const result = fuzzysort .go(needle, options, { - keys: ["title", "category"], - scoreFn: (r) => r[0].score * 2 + r[1].score, + keys: ["title", "category", "search"], + scoreFn: (r) => r[0].score * 2 + r[1].score + r[2].score, }) .map((x) => x.obj) diff --git a/packages/opencode/src/config/keybinds.ts b/packages/opencode/src/config/keybinds.ts index a84fc0b37d..a1a6211614 100644 --- a/packages/opencode/src/config/keybinds.ts +++ b/packages/opencode/src/config/keybinds.ts @@ -61,7 +61,8 @@ const KeybindsSchema = Schema.Struct({ command_list: keybind("ctrl+p", "List available commands"), agent_list: keybind("a", "List agents"), agent_cycle: keybind("tab", "Next agent"), - agent_cycle_reverse: keybind("shift+tab", "Previous agent"), + agent_cycle_reverse: keybind("none", "Previous agent"), + permission_auto_accept_toggle: keybind("shift+tab", "Toggle auto-accept mode for permissions"), variant_cycle: keybind("ctrl+t", "Cycle model variants"), variant_list: keybind("none", "List model variants"), input_clear: keybind("ctrl+c", "Clear input field"), diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index ec384709da..86f633b9e6 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -224,8 +224,8 @@ test("agent permission config merges with defaults", async () => { expect(build).toBeDefined() // Specific pattern is denied expect(Permission.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny") - // Edit still allowed - expect(evalPerm(build, "edit")).toBe("allow") + // Edit still asks (default behavior) + expect(evalPerm(build, "edit")).toBe("ask") }, }) })