diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 05fb3a5798..7a0a5c95bf 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -375,6 +375,11 @@ export const RunCommand = cmd({ action: "deny", pattern: "*", }, + { + permission: "edit", + action: "allow", + pattern: "*", + }, ] function title() { diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 4161c025c1..a27c23a37a 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -569,6 +569,7 @@ function App(props: { onSnapshot?: () => Promise }) { { title: "Toggle MCPs", value: "mcp.list", + search: "toggle mcps", category: "Agent", slash: { name: "mcps", @@ -673,8 +674,9 @@ function App(props: { onSnapshot?: () => Promise }) { category: "System", }, { - title: "Toggle theme mode", + title: mode() === "dark" ? "Light mode" : "Dark mode", value: "theme.switch_mode", + search: "toggle appearance", onSelect: (dialog) => { setMode(mode() === "dark" ? "light" : "dark") dialog.clear() @@ -723,6 +725,7 @@ function App(props: { onSnapshot?: () => Promise }) { }, { title: "Toggle debug panel", + search: "toggle debug", category: "System", value: "app.debug", onSelect: (dialog) => { @@ -732,6 +735,7 @@ function App(props: { onSnapshot?: () => Promise }) { }, { title: "Toggle console", + search: "toggle console", category: "System", value: "app.console", onSelect: (dialog) => { @@ -773,6 +777,7 @@ function App(props: { onSnapshot?: () => Promise }) { { title: terminalTitleEnabled() ? "Disable terminal title" : "Enable terminal title", value: "terminal.title.toggle", + search: "toggle terminal title", keybind: "terminal_title_toggle", category: "System", onSelect: (dialog) => { @@ -788,6 +793,7 @@ function App(props: { onSnapshot?: () => Promise }) { { title: kv.get("animations_enabled", true) ? "Disable animations" : "Enable animations", value: "app.toggle.animations", + search: "toggle animations", category: "System", onSelect: (dialog) => { kv.set("animations_enabled", !kv.get("animations_enabled", true)) @@ -797,6 +803,7 @@ function App(props: { onSnapshot?: () => Promise }) { { title: kv.get("diff_wrap_mode", "word") === "word" ? "Disable diff wrapping" : "Enable diff wrapping", value: "app.toggle.diffwrap", + search: "toggle diff wrapping", category: "System", onSelect: (dialog) => { const current = kv.get("diff_wrap_mode", "word") 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 747c61fd0b..f0af962d49 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -97,6 +97,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({ @@ -211,6 +212,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", @@ -1109,11 +1121,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 11336d5002..84e30e3b48 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -25,6 +25,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 { Log } from "@/util/log" import type { Path } from "@opencode-ai/sdk" @@ -109,6 +110,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }) const sdk = useSDK() + const kv = useKV() + const [autoaccept] = kv.signal<"none" | "edit">("permission_auto_accept", "edit") async function syncWorkspaces() { const result = await sdk.client.experimental.workspace.list().catch(() => undefined) @@ -139,6 +142,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]) @@ -463,6 +473,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ get ready() { return store.status !== "loading" }, + session: { get(sessionID: string) { const match = Binary.search(store.session, sessionID, (s) => s.id) 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 396d756301..4d2f2ab251 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -554,6 +554,7 @@ export function Session() { { title: sidebarVisible() ? "Hide sidebar" : "Show sidebar", value: "session.sidebar.toggle", + search: "toggle sidebar", keybind: "sidebar_toggle", category: "Session", onSelect: (dialog) => { @@ -568,6 +569,7 @@ export function Session() { { title: conceal() ? "Disable code concealment" : "Enable code concealment", value: "session.toggle.conceal", + search: "toggle code concealment", keybind: "messages_toggle_conceal" as any, category: "Session", onSelect: (dialog) => { @@ -578,6 +580,7 @@ export function Session() { { title: showTimestamps() ? "Hide timestamps" : "Show timestamps", value: "session.toggle.timestamps", + search: "toggle timestamps", category: "Session", slash: { name: "timestamps", @@ -591,6 +594,7 @@ export function Session() { { title: showThinking() ? "Hide thinking" : "Show thinking", value: "session.toggle.thinking", + search: "toggle thinking", keybind: "display_thinking", category: "Session", slash: { @@ -605,6 +609,7 @@ export function Session() { { title: showDetails() ? "Hide tool details" : "Show tool details", value: "session.toggle.actions", + search: "toggle tool details", keybind: "tool_details", category: "Session", onSelect: (dialog) => { @@ -613,8 +618,9 @@ export function Session() { }, }, { - title: "Toggle session scrollbar", + title: showScrollbar() ? "Hide session scrollbar" : "Show session scrollbar", value: "session.toggle.scrollbar", + search: "toggle session scrollbar", keybind: "scrollbar_toggle", 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 46821cccec..08d79f09f8 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -36,6 +36,7 @@ export interface DialogSelectOption { title: string value: T description?: string + search?: string footer?: JSX.Element | string category?: string categoryView?: JSX.Element @@ -91,8 +92,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/config.ts b/packages/opencode/src/config/config.ts index efae2ca551..e869062055 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -667,7 +667,12 @@ export namespace Config { command_list: z.string().optional().default("ctrl+p").describe("List available commands"), agent_list: z.string().optional().default("a").describe("List agents"), agent_cycle: z.string().optional().default("tab").describe("Next agent"), - agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"), + agent_cycle_reverse: z.string().optional().default("none").describe("Previous agent"), + permission_auto_accept_toggle: z + .string() + .optional() + .default("shift+tab") + .describe("Toggle auto-accept mode for permissions"), variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"), variant_list: z.string().optional().default("none").describe("List model variants"), input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"), diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 98a0fd4c6e..9b2d523be0 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -42,7 +42,7 @@ test("build agent has correct default properties", async () => { expect(build).toBeDefined() expect(build?.mode).toBe("primary") expect(build?.native).toBe(true) - expect(evalPerm(build, "edit")).toBe("allow") + expect(evalPerm(build, "edit")).toBe("ask") expect(evalPerm(build, "bash")).toBe("allow") }, }) @@ -219,8 +219,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") }, }) })