diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index e30d05e935..43e7a2de34 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -64,6 +64,7 @@ export namespace Agent { 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 85b5689daa..ee75bcb933 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -370,6 +370,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 8bb17ff133..4b997418bf 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -480,6 +480,7 @@ function App() { { title: "Toggle MCPs", value: "mcp.list", + search: "toggle mcps", category: "Agent", slash: { name: "mcps", @@ -555,8 +556,9 @@ function App() { category: "System", }, { - title: "Toggle appearance", + title: mode() === "dark" ? "Light mode" : "Dark mode", value: "theme.switch_mode", + search: "toggle appearance", onSelect: (dialog) => { setMode(mode() === "dark" ? "light" : "dark") dialog.clear() @@ -595,6 +597,7 @@ function App() { }, { title: "Toggle debug panel", + search: "toggle debug", category: "System", value: "app.debug", onSelect: (dialog) => { @@ -604,6 +607,7 @@ function App() { }, { title: "Toggle console", + search: "toggle console", category: "System", value: "app.console", onSelect: (dialog) => { @@ -644,6 +648,7 @@ function App() { { title: terminalTitleEnabled() ? "Disable terminal title" : "Enable terminal title", value: "terminal.title.toggle", + search: "toggle terminal title", keybind: "terminal_title_toggle", category: "System", onSelect: (dialog) => { @@ -659,6 +664,7 @@ function App() { { 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)) @@ -668,6 +674,7 @@ function App() { { 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 249a48d358..2142df063f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -79,6 +79,7 @@ export function Prompt(props: PromptProps) { const renderer = useRenderer() const { theme, syntax } = useTheme() const kv = useKV() + const [autoaccept, setAutoaccept] = kv.signal<"none" | "edit">("permission_auto_accept", "edit") function promptModelWarning() { toast.show({ @@ -172,6 +173,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", @@ -1010,23 +1022,30 @@ export function Prompt(props: PromptProps) { cursorColor={theme.text} syntaxStyle={syntax()} /> - - - {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} - - - - - {local.model.parsed().model} - - {local.model.parsed().provider} - - · - - {local.model.variant.current()} + + + + {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} + + + + + {local.model.parsed().model} - - + {local.model.parsed().provider} + + · + + {local.model.variant.current()} + + + + + + + + autoedit + diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 3b296a927a..b629d3f8a0 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" @@ -106,6 +107,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) @@ -136,6 +139,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]) @@ -451,6 +461,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/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index e76e165b26..39b9b3326f 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -47,6 +47,7 @@ export function Home() { { title: tipsHidden() ? "Show tips" : "Hide tips", value: "tips.toggle", + search: "toggle tips", keybind: "tips_toggle", category: "System", onSelect: (dialog) => { 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 7b54ec2875..f8efba06e5 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -568,6 +568,7 @@ export function Session() { { title: sidebarVisible() ? "Hide sidebar" : "Show sidebar", value: "session.sidebar.toggle", + search: "toggle sidebar", keybind: "sidebar_toggle", category: "Session", onSelect: (dialog) => { @@ -582,6 +583,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) => { @@ -592,6 +594,7 @@ export function Session() { { title: showTimestamps() ? "Hide timestamps" : "Show timestamps", value: "session.toggle.timestamps", + search: "toggle timestamps", category: "Session", slash: { name: "timestamps", @@ -605,6 +608,7 @@ export function Session() { { title: showThinking() ? "Hide thinking" : "Show thinking", value: "session.toggle.thinking", + search: "toggle thinking", keybind: "display_thinking", category: "Session", slash: { @@ -619,6 +623,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) => { @@ -627,8 +632,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 151f73cf7c..e3152bbd70 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -34,6 +34,7 @@ export interface DialogSelectOption { title: string value: T description?: string + search?: string footer?: JSX.Element | string category?: string disabled?: boolean @@ -85,8 +86,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 47afdfd7d0..abb772ab22 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -858,7 +858,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"), input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"), input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 60c8e57c92..c298e0d9eb 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -38,7 +38,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") }, }) @@ -217,8 +217,8 @@ test("agent permission config merges with defaults", async () => { expect(build).toBeDefined() // Specific pattern is denied expect(PermissionNext.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") }, }) }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index ec797f2ba8..20ecf168aa 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1009,6 +1009,392 @@ export type GlobalEvent = { payload: Event } +/** + * Custom keybind configurations + */ +export type KeybindsConfig = { + /** + * Leader key for keybind combinations + */ + leader?: string + /** + * Exit the application + */ + app_exit?: string + /** + * Open external editor + */ + editor_open?: string + /** + * List available themes + */ + theme_list?: string + /** + * Toggle sidebar + */ + sidebar_toggle?: string + /** + * Toggle session scrollbar + */ + scrollbar_toggle?: string + /** + * Toggle username visibility + */ + username_toggle?: string + /** + * View status + */ + status_view?: string + /** + * Export session to editor + */ + session_export?: string + /** + * Create a new session + */ + session_new?: string + /** + * List all sessions + */ + session_list?: string + /** + * Show session timeline + */ + session_timeline?: string + /** + * Fork session from message + */ + session_fork?: string + /** + * Rename session + */ + session_rename?: string + /** + * Delete session + */ + session_delete?: string + /** + * Delete stash entry + */ + stash_delete?: string + /** + * Open provider list from model dialog + */ + model_provider_list?: string + /** + * Toggle model favorite status + */ + model_favorite_toggle?: string + /** + * Share current session + */ + session_share?: string + /** + * Unshare current session + */ + session_unshare?: string + /** + * Interrupt current session + */ + session_interrupt?: string + /** + * Compact the session + */ + session_compact?: string + /** + * Scroll messages up by one page + */ + messages_page_up?: string + /** + * Scroll messages down by one page + */ + messages_page_down?: string + /** + * Scroll messages up by one line + */ + messages_line_up?: string + /** + * Scroll messages down by one line + */ + messages_line_down?: string + /** + * Scroll messages up by half page + */ + messages_half_page_up?: string + /** + * Scroll messages down by half page + */ + messages_half_page_down?: string + /** + * Navigate to first message + */ + messages_first?: string + /** + * Navigate to last message + */ + messages_last?: string + /** + * Navigate to next message + */ + messages_next?: string + /** + * Navigate to previous message + */ + messages_previous?: string + /** + * Navigate to last user message + */ + messages_last_user?: string + /** + * Copy message + */ + messages_copy?: string + /** + * Undo message + */ + messages_undo?: string + /** + * Redo message + */ + messages_redo?: string + /** + * Toggle code block concealment in messages + */ + messages_toggle_conceal?: string + /** + * Toggle tool details visibility + */ + tool_details?: string + /** + * List available models + */ + model_list?: string + /** + * Next recently used model + */ + model_cycle_recent?: string + /** + * Previous recently used model + */ + model_cycle_recent_reverse?: string + /** + * Next favorite model + */ + model_cycle_favorite?: string + /** + * Previous favorite model + */ + model_cycle_favorite_reverse?: string + /** + * List available commands + */ + command_list?: string + /** + * List agents + */ + agent_list?: string + /** + * Next agent + */ + agent_cycle?: string + /** + * Previous agent + */ + agent_cycle_reverse?: string + /** + * Toggle auto-accept mode for permissions + */ + permission_auto_accept_toggle?: string + /** + * Cycle model variants + */ + variant_cycle?: string + /** + * Clear input field + */ + input_clear?: string + /** + * Paste from clipboard + */ + input_paste?: string + /** + * Submit input + */ + input_submit?: string + /** + * Insert newline in input + */ + input_newline?: string + /** + * Move cursor left in input + */ + input_move_left?: string + /** + * Move cursor right in input + */ + input_move_right?: string + /** + * Move cursor up in input + */ + input_move_up?: string + /** + * Move cursor down in input + */ + input_move_down?: string + /** + * Select left in input + */ + input_select_left?: string + /** + * Select right in input + */ + input_select_right?: string + /** + * Select up in input + */ + input_select_up?: string + /** + * Select down in input + */ + input_select_down?: string + /** + * Move to start of line in input + */ + input_line_home?: string + /** + * Move to end of line in input + */ + input_line_end?: string + /** + * Select to start of line in input + */ + input_select_line_home?: string + /** + * Select to end of line in input + */ + input_select_line_end?: string + /** + * Move to start of visual line in input + */ + input_visual_line_home?: string + /** + * Move to end of visual line in input + */ + input_visual_line_end?: string + /** + * Select to start of visual line in input + */ + input_select_visual_line_home?: string + /** + * Select to end of visual line in input + */ + input_select_visual_line_end?: string + /** + * Move to start of buffer in input + */ + input_buffer_home?: string + /** + * Move to end of buffer in input + */ + input_buffer_end?: string + /** + * Select to start of buffer in input + */ + input_select_buffer_home?: string + /** + * Select to end of buffer in input + */ + input_select_buffer_end?: string + /** + * Delete line in input + */ + input_delete_line?: string + /** + * Delete to end of line in input + */ + input_delete_to_line_end?: string + /** + * Delete to start of line in input + */ + input_delete_to_line_start?: string + /** + * Backspace in input + */ + input_backspace?: string + /** + * Delete character in input + */ + input_delete?: string + /** + * Undo in input + */ + input_undo?: string + /** + * Redo in input + */ + input_redo?: string + /** + * Move word forward in input + */ + input_word_forward?: string + /** + * Move word backward in input + */ + input_word_backward?: string + /** + * Select word forward in input + */ + input_select_word_forward?: string + /** + * Select word backward in input + */ + input_select_word_backward?: string + /** + * Delete word forward in input + */ + input_delete_word_forward?: string + /** + * Delete word backward in input + */ + input_delete_word_backward?: string + /** + * Previous history item + */ + history_previous?: string + /** + * Next history item + */ + history_next?: string + /** + * Next child session + */ + session_child_cycle?: string + /** + * Previous child session + */ + session_child_cycle_reverse?: string + /** + * Go to parent session + */ + session_parent?: string + /** + * Suspend terminal + */ + terminal_suspend?: string + /** + * Toggle terminal title + */ + terminal_title_toggle?: string + /** + * Toggle tips on home screen + */ + tips_toggle?: string + /** + * Toggle thinking blocks visibility + */ + display_thinking?: string +} + /** * Log level */