Skip to content

Commit a78759f

Browse files
Apply PR #12633: feat(tui): add auto-accept mode for permission requests
2 parents 18804b9 + 5792a80 commit a78759f

9 files changed

Lines changed: 67 additions & 14 deletions

File tree

packages/opencode/src/agent/agent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export namespace Agent {
9393
question: "deny",
9494
plan_enter: "deny",
9595
plan_exit: "deny",
96+
edit: "ask",
9697
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
9798
read: {
9899
"*": "allow",

packages/opencode/src/cli/cmd/run.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,11 @@ export const RunCommand = cmd({
375375
action: "deny",
376376
pattern: "*",
377377
},
378+
{
379+
permission: "edit",
380+
action: "allow",
381+
pattern: "*",
382+
},
378383
]
379384

380385
function title() {

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
569569
{
570570
title: "Toggle MCPs",
571571
value: "mcp.list",
572+
search: "toggle mcps",
572573
category: "Agent",
573574
slash: {
574575
name: "mcps",
@@ -673,8 +674,9 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
673674
category: "System",
674675
},
675676
{
676-
title: "Toggle theme mode",
677+
title: mode() === "dark" ? "Light mode" : "Dark mode",
677678
value: "theme.switch_mode",
679+
search: "toggle appearance",
678680
onSelect: (dialog) => {
679681
setMode(mode() === "dark" ? "light" : "dark")
680682
dialog.clear()
@@ -723,6 +725,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
723725
},
724726
{
725727
title: "Toggle debug panel",
728+
search: "toggle debug",
726729
category: "System",
727730
value: "app.debug",
728731
onSelect: (dialog) => {
@@ -732,6 +735,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
732735
},
733736
{
734737
title: "Toggle console",
738+
search: "toggle console",
735739
category: "System",
736740
value: "app.console",
737741
onSelect: (dialog) => {
@@ -773,6 +777,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
773777
{
774778
title: terminalTitleEnabled() ? "Disable terminal title" : "Enable terminal title",
775779
value: "terminal.title.toggle",
780+
search: "toggle terminal title",
776781
keybind: "terminal_title_toggle",
777782
category: "System",
778783
onSelect: (dialog) => {
@@ -788,6 +793,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
788793
{
789794
title: kv.get("animations_enabled", true) ? "Disable animations" : "Enable animations",
790795
value: "app.toggle.animations",
796+
search: "toggle animations",
791797
category: "System",
792798
onSelect: (dialog) => {
793799
kv.set("animations_enabled", !kv.get("animations_enabled", true))
@@ -797,6 +803,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
797803
{
798804
title: kv.get("diff_wrap_mode", "word") === "word" ? "Disable diff wrapping" : "Enable diff wrapping",
799805
value: "app.toggle.diffwrap",
806+
search: "toggle diff wrapping",
800807
category: "System",
801808
onSelect: (dialog) => {
802809
const current = kv.get("diff_wrap_mode", "word")
@@ -805,7 +812,9 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
805812
},
806813
},
807814
{
808-
title: kv.get("clear_prompt_save_history", false) ? "Don't include cleared prompts in history" : "Include cleared prompts in history",
815+
title: kv.get("clear_prompt_save_history", false)
816+
? "Don't include cleared prompts in history"
817+
: "Include cleared prompts in history",
809818
value: "app.toggle.clear_prompt_history",
810819
category: "System",
811820
onSelect: (dialog) => {

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export function Prompt(props: PromptProps) {
9797
const [auto, setAuto] = createSignal<AutocompleteRef>()
9898
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
9999
const hasRightContent = createMemo(() => Boolean(props.right))
100+
const [autoaccept, setAutoaccept] = kv.signal<"none" | "edit">("permission_auto_accept", "edit")
100101

101102
function promptModelWarning() {
102103
toast.show({
@@ -211,6 +212,17 @@ export function Prompt(props: PromptProps) {
211212

212213
command.register(() => {
213214
return [
215+
{
216+
title: autoaccept() === "none" ? "Enable autoedit" : "Disable autoedit",
217+
value: "permission.auto_accept.toggle",
218+
search: "toggle permissions",
219+
keybind: "permission_auto_accept_toggle",
220+
category: "Agent",
221+
onSelect: (dialog) => {
222+
setAutoaccept(() => (autoaccept() === "none" ? "edit" : "none"))
223+
dialog.clear()
224+
},
225+
},
214226
{
215227
title: "Clear prompt",
216228
value: "prompt.clear",
@@ -1115,11 +1127,14 @@ export function Prompt(props: PromptProps) {
11151127
</box>
11161128
</Show>
11171129
</box>
1118-
<Show when={hasRightContent()}>
1119-
<box flexDirection="row" gap={1} alignItems="center">
1120-
{props.right}
1121-
</box>
1122-
</Show>
1130+
<box flexDirection="row" gap={1} alignItems="center">
1131+
<Show when={autoaccept() === "edit"}>
1132+
<text>
1133+
<span style={{ fg: theme.warning }}>autoedit</span>
1134+
</text>
1135+
</Show>
1136+
<Show when={hasRightContent()}>{props.right}</Show>
1137+
</box>
11231138
</box>
11241139
</box>
11251140
</box>

packages/opencode/src/cli/cmd/tui/context/sync.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { createSimpleContext } from "./helper"
2525
import type { Snapshot } from "@/snapshot"
2626
import { useExit } from "./exit"
2727
import { useArgs } from "./args"
28+
import { useKV } from "./kv"
2829
import { batch, onMount } from "solid-js"
2930
import { Log } from "@/util/log"
3031
import type { Path } from "@opencode-ai/sdk"
@@ -109,6 +110,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
109110
})
110111

111112
const sdk = useSDK()
113+
const kv = useKV()
114+
const [autoaccept] = kv.signal<"none" | "edit">("permission_auto_accept", "edit")
112115

113116
async function syncWorkspaces() {
114117
const result = await sdk.client.experimental.workspace.list().catch(() => undefined)
@@ -139,6 +142,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
139142

140143
case "permission.asked": {
141144
const request = event.properties
145+
if (autoaccept() === "edit" && request.permission === "edit") {
146+
sdk.client.permission.reply({
147+
reply: "once",
148+
requestID: request.id,
149+
})
150+
break
151+
}
142152
const requests = store.permission[request.sessionID]
143153
if (!requests) {
144154
setStore("permission", request.sessionID, [request])
@@ -463,6 +473,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
463473
get ready() {
464474
return store.status !== "loading"
465475
},
476+
466477
session: {
467478
get(sessionID: string) {
468479
const match = Binary.search(store.session, sessionID, (s) => s.id)

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ export function Session() {
577577
{
578578
title: sidebarVisible() ? "Hide sidebar" : "Show sidebar",
579579
value: "session.sidebar.toggle",
580+
search: "toggle sidebar",
580581
keybind: "sidebar_toggle",
581582
category: "Session",
582583
onSelect: (dialog) => {
@@ -591,6 +592,7 @@ export function Session() {
591592
{
592593
title: conceal() ? "Disable code concealment" : "Enable code concealment",
593594
value: "session.toggle.conceal",
595+
search: "toggle code concealment",
594596
keybind: "messages_toggle_conceal" as any,
595597
category: "Session",
596598
onSelect: (dialog) => {
@@ -601,6 +603,7 @@ export function Session() {
601603
{
602604
title: showTimestamps() ? "Hide timestamps" : "Show timestamps",
603605
value: "session.toggle.timestamps",
606+
search: "toggle timestamps",
604607
category: "Session",
605608
slash: {
606609
name: "timestamps",
@@ -614,6 +617,7 @@ export function Session() {
614617
{
615618
title: showThinking() ? "Hide thinking" : "Show thinking",
616619
value: "session.toggle.thinking",
620+
search: "toggle thinking",
617621
keybind: "display_thinking",
618622
category: "Session",
619623
slash: {
@@ -628,6 +632,7 @@ export function Session() {
628632
{
629633
title: showDetails() ? "Hide tool details" : "Show tool details",
630634
value: "session.toggle.actions",
635+
search: "toggle tool details",
631636
keybind: "tool_details",
632637
category: "Session",
633638
onSelect: (dialog) => {
@@ -636,8 +641,9 @@ export function Session() {
636641
},
637642
},
638643
{
639-
title: "Toggle session scrollbar",
644+
title: showScrollbar() ? "Hide session scrollbar" : "Show session scrollbar",
640645
value: "session.toggle.scrollbar",
646+
search: "toggle session scrollbar",
641647
keybind: "scrollbar_toggle",
642648
category: "Session",
643649
onSelect: (dialog) => {

packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export interface DialogSelectOption<T = any> {
3636
title: string
3737
value: T
3838
description?: string
39+
search?: string
3940
footer?: JSX.Element | string
4041
category?: string
4142
categoryView?: JSX.Element
@@ -91,8 +92,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
9192
// users typically search by the item name, and not its category.
9293
const result = fuzzysort
9394
.go(needle, options, {
94-
keys: ["title", "category"],
95-
scoreFn: (r) => r[0].score * 2 + r[1].score,
95+
keys: ["title", "category", "search"],
96+
scoreFn: (r) => r[0].score * 2 + r[1].score + r[2].score,
9697
})
9798
.map((x) => x.obj)
9899

packages/opencode/src/config/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,12 @@ export namespace Config {
671671
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
672672
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
673673
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
674-
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
674+
agent_cycle_reverse: z.string().optional().default("none").describe("Previous agent"),
675+
permission_auto_accept_toggle: z
676+
.string()
677+
.optional()
678+
.default("shift+tab")
679+
.describe("Toggle auto-accept mode for permissions"),
675680
variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
676681
variant_list: z.string().optional().default("none").describe("List model variants"),
677682
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),

packages/opencode/test/agent/agent.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ test("build agent has correct default properties", async () => {
4242
expect(build).toBeDefined()
4343
expect(build?.mode).toBe("primary")
4444
expect(build?.native).toBe(true)
45-
expect(evalPerm(build, "edit")).toBe("allow")
45+
expect(evalPerm(build, "edit")).toBe("ask")
4646
expect(evalPerm(build, "bash")).toBe("allow")
4747
},
4848
})
@@ -219,8 +219,8 @@ test("agent permission config merges with defaults", async () => {
219219
expect(build).toBeDefined()
220220
// Specific pattern is denied
221221
expect(Permission.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny")
222-
// Edit still allowed
223-
expect(evalPerm(build, "edit")).toBe("allow")
222+
// Edit still asks (default behavior)
223+
expect(evalPerm(build, "edit")).toBe("ask")
224224
},
225225
})
226226
})

0 commit comments

Comments
 (0)