Compare commits

...

10 Commits

Author SHA1 Message Date
Dax 5792a80a8c
Merge branch 'dev' into feat/auto-accept-permissions 2026-03-20 10:46:31 -04:00
Dax Raad db039db7f5 regen js sdk 2026-03-20 10:21:10 -04:00
Dax Raad c1a3936b61 Merge remote-tracking branch 'origin/dev' into feat/auto-accept-permissions
# Conflicts:
#	packages/sdk/js/src/v2/gen/types.gen.ts
2026-03-20 10:20:26 -04:00
Dax Raad a531f3f36d core: run command build agent now auto-accepts file edits to reduce workflow interruptions while still requiring confirmation for bash commands 2026-02-07 20:00:09 -05:00
Dax Raad bb3382311d tui: standardize autoedit indicator text styling to match other status labels 2026-02-07 19:57:45 -05:00
Dax Raad ad545d0cc9 tui: allow auto-accepting only edit permissions instead of all permissions 2026-02-07 19:52:53 -05:00
Dax Raad ac244b1458 tui: add searchable 'toggle' keywords to command palette and show current state in toggle titles 2026-02-07 17:03:34 -05:00
Dax Raad f202536b65 tui: show enable/disable state in permission toggle and make it searchable by 'toggle permissions' 2026-02-07 16:57:48 -05:00
Dax Raad 405cc3f610 tui: streamline permission toggle command naming and add keyboard shortcut support
Rename 'Toggle autoaccept permissions' to 'Toggle permissions' for clarity
and move the command to the Agent category for better discoverability.
Add permission_auto_accept_toggle keybind to enable keyboard shortcut
toggling of auto-accept mode for permission requests.
2026-02-07 16:51:55 -05:00
Dax Raad 878c1b8c2d feat(tui): add auto-accept mode for permission requests
Add a toggleable auto-accept mode that automatically accepts all incoming
permission requests with a 'once' reply. This is useful for users who want
to streamline their workflow when they trust the agent's actions.

Changes:
- Add permission_auto_accept keybind (default: shift+tab) to config
- Remove default for agent_cycle_reverse (was shift+tab)
- Add auto-accept logic in sync.tsx to auto-reply when enabled
- Add command bar action to toggle auto-accept mode (copy: "Toggle autoaccept permissions")
- Add visual indicator showing 'auto-accept' when active
- Store auto-accept state in KV for persistence across sessions
2026-02-07 16:44:39 -05:00
10 changed files with 80 additions and 24 deletions

View File

@ -64,6 +64,7 @@ export namespace Agent {
question: "deny", question: "deny",
plan_enter: "deny", plan_enter: "deny",
plan_exit: "deny", plan_exit: "deny",
edit: "ask",
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files // mirrors github.com/github/gitignore Node.gitignore pattern for .env files
read: { read: {
"*": "allow", "*": "allow",

View File

@ -370,6 +370,11 @@ export const RunCommand = cmd({
action: "deny", action: "deny",
pattern: "*", pattern: "*",
}, },
{
permission: "edit",
action: "allow",
pattern: "*",
},
] ]
function title() { function title() {

View File

@ -480,6 +480,7 @@ function App() {
{ {
title: "Toggle MCPs", title: "Toggle MCPs",
value: "mcp.list", value: "mcp.list",
search: "toggle mcps",
category: "Agent", category: "Agent",
slash: { slash: {
name: "mcps", name: "mcps",
@ -555,8 +556,9 @@ function App() {
category: "System", category: "System",
}, },
{ {
title: "Toggle appearance", title: mode() === "dark" ? "Light mode" : "Dark mode",
value: "theme.switch_mode", value: "theme.switch_mode",
search: "toggle appearance",
onSelect: (dialog) => { onSelect: (dialog) => {
setMode(mode() === "dark" ? "light" : "dark") setMode(mode() === "dark" ? "light" : "dark")
dialog.clear() dialog.clear()
@ -595,6 +597,7 @@ function App() {
}, },
{ {
title: "Toggle debug panel", title: "Toggle debug panel",
search: "toggle debug",
category: "System", category: "System",
value: "app.debug", value: "app.debug",
onSelect: (dialog) => { onSelect: (dialog) => {
@ -604,6 +607,7 @@ function App() {
}, },
{ {
title: "Toggle console", title: "Toggle console",
search: "toggle console",
category: "System", category: "System",
value: "app.console", value: "app.console",
onSelect: (dialog) => { onSelect: (dialog) => {
@ -644,6 +648,7 @@ function App() {
{ {
title: terminalTitleEnabled() ? "Disable terminal title" : "Enable terminal title", title: terminalTitleEnabled() ? "Disable terminal title" : "Enable terminal title",
value: "terminal.title.toggle", value: "terminal.title.toggle",
search: "toggle terminal title",
keybind: "terminal_title_toggle", keybind: "terminal_title_toggle",
category: "System", category: "System",
onSelect: (dialog) => { onSelect: (dialog) => {
@ -659,6 +664,7 @@ function App() {
{ {
title: kv.get("animations_enabled", true) ? "Disable animations" : "Enable animations", title: kv.get("animations_enabled", true) ? "Disable animations" : "Enable animations",
value: "app.toggle.animations", value: "app.toggle.animations",
search: "toggle animations",
category: "System", category: "System",
onSelect: (dialog) => { onSelect: (dialog) => {
kv.set("animations_enabled", !kv.get("animations_enabled", true)) 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", title: kv.get("diff_wrap_mode", "word") === "word" ? "Disable diff wrapping" : "Enable diff wrapping",
value: "app.toggle.diffwrap", value: "app.toggle.diffwrap",
search: "toggle diff wrapping",
category: "System", category: "System",
onSelect: (dialog) => { onSelect: (dialog) => {
const current = kv.get("diff_wrap_mode", "word") const current = kv.get("diff_wrap_mode", "word")

View File

@ -79,6 +79,7 @@ export function Prompt(props: PromptProps) {
const renderer = useRenderer() const renderer = useRenderer()
const { theme, syntax } = useTheme() const { theme, syntax } = useTheme()
const kv = useKV() const kv = useKV()
const [autoaccept, setAutoaccept] = kv.signal<"none" | "edit">("permission_auto_accept", "edit")
function promptModelWarning() { function promptModelWarning() {
toast.show({ toast.show({
@ -172,6 +173,17 @@ export function Prompt(props: PromptProps) {
command.register(() => { command.register(() => {
return [ 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", title: "Clear prompt",
value: "prompt.clear", value: "prompt.clear",
@ -1010,23 +1022,30 @@ export function Prompt(props: PromptProps) {
cursorColor={theme.text} cursorColor={theme.text}
syntaxStyle={syntax()} syntaxStyle={syntax()}
/> />
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}> <box flexDirection="row" flexShrink={0} paddingTop={1} gap={1} justifyContent="space-between">
<text fg={highlight()}> <box flexDirection="row" gap={1}>
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} <text fg={highlight()}>
</text> {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
<Show when={store.mode === "normal"}> </text>
<box flexDirection="row" gap={1}> <Show when={store.mode === "normal"}>
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}> <box flexDirection="row" gap={1}>
{local.model.parsed().model} <text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
</text> {local.model.parsed().model}
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
<Show when={showVariant()}>
<text fg={theme.textMuted}>·</text>
<text>
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
</text> </text>
</Show> <text fg={theme.textMuted}>{local.model.parsed().provider}</text>
</box> <Show when={showVariant()}>
<text fg={theme.textMuted}>·</text>
<text>
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
</text>
</Show>
</box>
</Show>
</box>
<Show when={autoaccept() === "edit"}>
<text>
<span style={{ fg: theme.warning }}>autoedit</span>
</text>
</Show> </Show>
</box> </box>
</box> </box>

View File

@ -25,6 +25,7 @@ import { createSimpleContext } from "./helper"
import type { Snapshot } from "@/snapshot" import type { Snapshot } from "@/snapshot"
import { useExit } from "./exit" import { useExit } from "./exit"
import { useArgs } from "./args" import { useArgs } from "./args"
import { useKV } from "./kv"
import { batch, onMount } from "solid-js" import { batch, onMount } from "solid-js"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import type { Path } from "@opencode-ai/sdk" import type { Path } from "@opencode-ai/sdk"
@ -106,6 +107,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}) })
const sdk = useSDK() const sdk = useSDK()
const kv = useKV()
const [autoaccept] = kv.signal<"none" | "edit">("permission_auto_accept", "edit")
async function syncWorkspaces() { async function syncWorkspaces() {
const result = await sdk.client.experimental.workspace.list().catch(() => undefined) const result = await sdk.client.experimental.workspace.list().catch(() => undefined)
@ -136,6 +139,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
case "permission.asked": { case "permission.asked": {
const request = event.properties 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] const requests = store.permission[request.sessionID]
if (!requests) { if (!requests) {
setStore("permission", request.sessionID, [request]) setStore("permission", request.sessionID, [request])
@ -451,6 +461,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
get ready() { get ready() {
return store.status !== "loading" return store.status !== "loading"
}, },
session: { session: {
get(sessionID: string) { get(sessionID: string) {
const match = Binary.search(store.session, sessionID, (s) => s.id) const match = Binary.search(store.session, sessionID, (s) => s.id)

View File

@ -47,6 +47,7 @@ export function Home() {
{ {
title: tipsHidden() ? "Show tips" : "Hide tips", title: tipsHidden() ? "Show tips" : "Hide tips",
value: "tips.toggle", value: "tips.toggle",
search: "toggle tips",
keybind: "tips_toggle", keybind: "tips_toggle",
category: "System", category: "System",
onSelect: (dialog) => { onSelect: (dialog) => {

View File

@ -568,6 +568,7 @@ export function Session() {
{ {
title: sidebarVisible() ? "Hide sidebar" : "Show sidebar", title: sidebarVisible() ? "Hide sidebar" : "Show sidebar",
value: "session.sidebar.toggle", value: "session.sidebar.toggle",
search: "toggle sidebar",
keybind: "sidebar_toggle", keybind: "sidebar_toggle",
category: "Session", category: "Session",
onSelect: (dialog) => { onSelect: (dialog) => {
@ -582,6 +583,7 @@ export function Session() {
{ {
title: conceal() ? "Disable code concealment" : "Enable code concealment", title: conceal() ? "Disable code concealment" : "Enable code concealment",
value: "session.toggle.conceal", value: "session.toggle.conceal",
search: "toggle code concealment",
keybind: "messages_toggle_conceal" as any, keybind: "messages_toggle_conceal" as any,
category: "Session", category: "Session",
onSelect: (dialog) => { onSelect: (dialog) => {
@ -592,6 +594,7 @@ export function Session() {
{ {
title: showTimestamps() ? "Hide timestamps" : "Show timestamps", title: showTimestamps() ? "Hide timestamps" : "Show timestamps",
value: "session.toggle.timestamps", value: "session.toggle.timestamps",
search: "toggle timestamps",
category: "Session", category: "Session",
slash: { slash: {
name: "timestamps", name: "timestamps",
@ -605,6 +608,7 @@ export function Session() {
{ {
title: showThinking() ? "Hide thinking" : "Show thinking", title: showThinking() ? "Hide thinking" : "Show thinking",
value: "session.toggle.thinking", value: "session.toggle.thinking",
search: "toggle thinking",
keybind: "display_thinking", keybind: "display_thinking",
category: "Session", category: "Session",
slash: { slash: {
@ -619,6 +623,7 @@ export function Session() {
{ {
title: showDetails() ? "Hide tool details" : "Show tool details", title: showDetails() ? "Hide tool details" : "Show tool details",
value: "session.toggle.actions", value: "session.toggle.actions",
search: "toggle tool details",
keybind: "tool_details", keybind: "tool_details",
category: "Session", category: "Session",
onSelect: (dialog) => { 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", value: "session.toggle.scrollbar",
search: "toggle session scrollbar",
keybind: "scrollbar_toggle", keybind: "scrollbar_toggle",
category: "Session", category: "Session",
onSelect: (dialog) => { onSelect: (dialog) => {

View File

@ -34,6 +34,7 @@ export interface DialogSelectOption<T = any> {
title: string title: string
value: T value: T
description?: string description?: string
search?: string
footer?: JSX.Element | string footer?: JSX.Element | string
category?: string category?: string
disabled?: boolean disabled?: boolean
@ -85,8 +86,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
// users typically search by the item name, and not its category. // users typically search by the item name, and not its category.
const result = fuzzysort const result = fuzzysort
.go(needle, options, { .go(needle, options, {
keys: ["title", "category"], keys: ["title", "category", "search"],
scoreFn: (r) => r[0].score * 2 + r[1].score, scoreFn: (r) => r[0].score * 2 + r[1].score + r[2].score,
}) })
.map((x) => x.obj) .map((x) => x.obj)

View File

@ -858,7 +858,12 @@ export namespace Config {
command_list: z.string().optional().default("ctrl+p").describe("List available commands"), command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
agent_list: z.string().optional().default("<leader>a").describe("List agents"), agent_list: z.string().optional().default("<leader>a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"), 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_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"), input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),

View File

@ -38,7 +38,7 @@ test("build agent has correct default properties", async () => {
expect(build).toBeDefined() expect(build).toBeDefined()
expect(build?.mode).toBe("primary") expect(build?.mode).toBe("primary")
expect(build?.native).toBe(true) expect(build?.native).toBe(true)
expect(evalPerm(build, "edit")).toBe("allow") expect(evalPerm(build, "edit")).toBe("ask")
expect(evalPerm(build, "bash")).toBe("allow") expect(evalPerm(build, "bash")).toBe("allow")
}, },
}) })
@ -217,8 +217,8 @@ test("agent permission config merges with defaults", async () => {
expect(build).toBeDefined() expect(build).toBeDefined()
// Specific pattern is denied // Specific pattern is denied
expect(PermissionNext.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny") expect(PermissionNext.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny")
// Edit still allowed // Edit still asks (default behavior)
expect(evalPerm(build, "edit")).toBe("allow") expect(evalPerm(build, "edit")).toBe("ask")
}, },
}) })
}) })