Skip to content

Commit b7605ad

Browse files
fix(app): enable auto-accept keybind regardless of permission config (#16259)
1 parent 6c7d968 commit b7605ad

6 files changed

Lines changed: 145 additions & 25 deletions

File tree

packages/app/src/components/prompt-input.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
244244
draggingType: "image" | "@mention" | null
245245
mode: "normal" | "shell"
246246
applyingHistory: boolean
247-
pendingAutoAccept: boolean
248247
}>({
249248
popover: null,
250249
historyIndex: -1,
@@ -253,7 +252,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
253252
draggingType: null,
254253
mode: "normal",
255254
applyingHistory: false,
256-
pendingAutoAccept: false,
257255
})
258256

259257
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
@@ -306,12 +304,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
306304
}),
307305
)
308306

309-
createEffect(
310-
on(sessionKey, () => {
311-
setStore("pendingAutoAccept", false)
312-
}),
313-
)
314-
315307
const historyComments = () => {
316308
const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const))
317309
return prompt.context.items().flatMap((item) => {
@@ -961,7 +953,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
961953
const variants = createMemo(() => ["default", ...local.model.variant.list()])
962954
const accepting = createMemo(() => {
963955
const id = params.id
964-
if (!id) return store.pendingAutoAccept
956+
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
965957
return permission.isAutoAccepting(id, sdk.directory)
966958
})
967959

@@ -1336,7 +1328,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
13361328
variant="ghost"
13371329
onClick={() => {
13381330
if (!params.id) {
1339-
setStore("pendingAutoAccept", (value) => !value)
1331+
permission.toggleAutoAcceptDirectory(sdk.directory)
13401332
return
13411333
}
13421334
permission.toggleAutoAccept(params.id, sdk.directory)

packages/app/src/context/permission-auto-respond.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, test } from "bun:test"
22
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
33
import { base64Encode } from "@opencode-ai/util/encode"
4-
import { autoRespondsPermission } from "./permission-auto-respond"
4+
import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
55

66
const session = (input: { id: string; parentID?: string }) =>
77
({
@@ -60,4 +60,43 @@ describe("autoRespondsPermission", () => {
6060

6161
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true)
6262
})
63+
64+
test("falls back to directory-level auto-accept", () => {
65+
const directory = "/tmp/project"
66+
const sessions = [session({ id: "root" })]
67+
const autoAccept = {
68+
[`${base64Encode(directory)}/*`]: true,
69+
}
70+
71+
expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(true)
72+
})
73+
74+
test("session-level override takes precedence over directory-level", () => {
75+
const directory = "/tmp/project"
76+
const sessions = [session({ id: "root" })]
77+
const autoAccept = {
78+
[`${base64Encode(directory)}/*`]: true,
79+
[`${base64Encode(directory)}/root`]: false,
80+
}
81+
82+
expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(false)
83+
})
84+
})
85+
86+
describe("isDirectoryAutoAccepting", () => {
87+
test("returns true when directory key is set", () => {
88+
const directory = "/tmp/project"
89+
const autoAccept = { [`${base64Encode(directory)}/*`]: true }
90+
expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(true)
91+
})
92+
93+
test("returns false when directory key is not set", () => {
94+
expect(isDirectoryAutoAccepting({}, "/tmp/project")).toBe(false)
95+
})
96+
97+
test("returns false when directory key is explicitly false", () => {
98+
const directory = "/tmp/project"
99+
const autoAccept = { [`${base64Encode(directory)}/*`]: false }
100+
expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(false)
101+
})
63102
})

packages/app/src/context/permission-auto-respond.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,19 @@ export function acceptKey(sessionID: string, directory?: string) {
55
return `${base64Encode(directory)}/${sessionID}`
66
}
77

8+
export function directoryAcceptKey(directory: string) {
9+
return `${base64Encode(directory)}/*`
10+
}
11+
812
function accepted(autoAccept: Record<string, boolean>, sessionID: string, directory?: string) {
913
const key = acceptKey(sessionID, directory)
10-
return autoAccept[key] ?? autoAccept[sessionID]
14+
const directoryKey = directory ? directoryAcceptKey(directory) : undefined
15+
return autoAccept[key] ?? autoAccept[sessionID] ?? (directoryKey ? autoAccept[directoryKey] : undefined)
16+
}
17+
18+
export function isDirectoryAutoAccepting(autoAccept: Record<string, boolean>, directory: string) {
19+
const key = directoryAcceptKey(directory)
20+
return autoAccept[key] ?? false
1121
}
1222

1323
function sessionLineage(session: { id: string; parentID?: string }[], sessionID: string) {

packages/app/src/context/permission.tsx

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createMemo, onCleanup } from "solid-js"
1+
import { createEffect, createMemo, onCleanup } from "solid-js"
22
import { createStore, produce } from "solid-js/store"
33
import { createSimpleContext } from "@opencode-ai/ui/context"
44
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
@@ -7,7 +7,7 @@ import { useGlobalSDK } from "@/context/global-sdk"
77
import { useGlobalSync } from "./global-sync"
88
import { useParams } from "@solidjs/router"
99
import { decode64 } from "@/utils/base64"
10-
import { acceptKey, autoRespondsPermission } from "./permission-auto-respond"
10+
import { acceptKey, directoryAcceptKey, isDirectoryAutoAccepting, autoRespondsPermission } from "./permission-auto-respond"
1111

1212
type PermissionRespondFn = (input: {
1313
sessionID: string
@@ -76,6 +76,25 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
7676
}),
7777
)
7878

79+
// When config has permission: "allow", auto-enable directory-level auto-accept
80+
createEffect(() => {
81+
if (!ready()) return
82+
const directory = decode64(params.dir)
83+
if (!directory) return
84+
const [childStore] = globalSync.child(directory)
85+
const perm = childStore.config.permission
86+
if (typeof perm === "string" && perm === "allow") {
87+
const key = directoryAcceptKey(directory)
88+
if (store.autoAccept[key] === undefined) {
89+
setStore(
90+
produce((draft) => {
91+
draft.autoAccept[key] = true
92+
}),
93+
)
94+
}
95+
}
96+
})
97+
7998
const MAX_RESPONDED = 1000
8099
const RESPONDED_TTL_MS = 60 * 60 * 1000
81100
const responded = new Map<string, number>()
@@ -119,6 +138,10 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
119138
return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
120139
}
121140

141+
function isAutoAcceptingDirectory(directory: string) {
142+
return isDirectoryAutoAccepting(store.autoAccept, directory)
143+
}
144+
122145
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
123146
const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
124147
return autoRespondsPermission(store.autoAccept, session, permission, directory)
@@ -142,6 +165,36 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
142165
})
143166
onCleanup(unsubscribe)
144167

168+
function enableDirectory(directory: string) {
169+
const key = directoryAcceptKey(directory)
170+
setStore(
171+
produce((draft) => {
172+
draft.autoAccept[key] = true
173+
}),
174+
)
175+
176+
globalSDK.client.permission
177+
.list({ directory })
178+
.then((x) => {
179+
if (!isAutoAcceptingDirectory(directory)) return
180+
for (const perm of x.data ?? []) {
181+
if (!perm?.id) continue
182+
if (!shouldAutoRespond(perm, directory)) continue
183+
respondOnce(perm, directory)
184+
}
185+
})
186+
.catch(() => undefined)
187+
}
188+
189+
function disableDirectory(directory: string) {
190+
const key = directoryAcceptKey(directory)
191+
setStore(
192+
produce((draft) => {
193+
draft.autoAccept[key] = false
194+
}),
195+
)
196+
}
197+
145198
function enable(sessionID: string, directory: string) {
146199
const key = acceptKey(sessionID, directory)
147200
const version = bumpEnableVersion(sessionID, directory)
@@ -185,6 +238,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
185238
return shouldAutoRespond(permission, directory)
186239
},
187240
isAutoAccepting,
241+
isAutoAcceptingDirectory,
188242
toggleAutoAccept(sessionID: string, directory: string) {
189243
if (isAutoAccepting(sessionID, directory)) {
190244
disable(sessionID, directory)
@@ -193,6 +247,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
193247

194248
enable(sessionID, directory)
195249
},
250+
toggleAutoAcceptDirectory(directory: string) {
251+
if (isAutoAcceptingDirectory(directory)) {
252+
disableDirectory(directory)
253+
return
254+
}
255+
enableDirectory(directory)
256+
},
196257
enableAutoAccept(sessionID: string, directory: string) {
197258
if (isAutoAccepting(sessionID, directory)) return
198259
enable(sessionID, directory)
@@ -201,6 +262,11 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
201262
disable(sessionID, directory)
202263
},
203264
permissionsEnabled,
265+
isPermissionAllowAll(directory: string) {
266+
const [childStore] = globalSync.child(directory)
267+
const perm = childStore.config.permission
268+
return typeof perm === "string" && perm === "allow"
269+
},
204270
}
205271
},
206272
})

packages/app/src/pages/session/use-session-commands.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -261,24 +261,35 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
261261
}),
262262
])
263263

264+
const isAutoAcceptActive = () => {
265+
const sessionID = params.id
266+
if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory)
267+
return permission.isAutoAcceptingDirectory(sdk.directory)
268+
}
269+
264270
const permissionCommands = createMemo(() => [
265271
permissionsCommand({
266272
id: "permissions.autoaccept",
267-
title:
268-
params.id && permission.isAutoAccepting(params.id, sdk.directory)
269-
? language.t("command.permissions.autoaccept.disable")
270-
: language.t("command.permissions.autoaccept.enable"),
273+
title: isAutoAcceptActive()
274+
? language.t("command.permissions.autoaccept.disable")
275+
: language.t("command.permissions.autoaccept.enable"),
271276
keybind: "mod+shift+a",
272-
disabled: !params.id || !permission.permissionsEnabled(),
277+
disabled: false,
273278
onSelect: () => {
274279
const sessionID = params.id
275-
if (!sessionID) return
276-
permission.toggleAutoAccept(sessionID, sdk.directory)
280+
if (sessionID) {
281+
permission.toggleAutoAccept(sessionID, sdk.directory)
282+
} else {
283+
permission.toggleAutoAcceptDirectory(sdk.directory)
284+
}
285+
const active = sessionID
286+
? permission.isAutoAccepting(sessionID, sdk.directory)
287+
: permission.isAutoAcceptingDirectory(sdk.directory)
277288
showToast({
278-
title: permission.isAutoAccepting(sessionID, sdk.directory)
289+
title: active
279290
? language.t("toast.permissions.autoaccept.on.title")
280291
: language.t("toast.permissions.autoaccept.off.title"),
281-
description: permission.isAutoAccepting(sessionID, sdk.directory)
292+
description: active
282293
? language.t("toast.permissions.autoaccept.on.description")
283294
: language.t("toast.permissions.autoaccept.off.description"),
284295
})

packages/opencode/script/build.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { $ } from "bun"
44
import fs from "fs"
55
import path from "path"
66
import { fileURLToPath } from "url"
7-
import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin"
7+
import solidPlugin from "@opentui/solid/bun-plugin"
88

99
const __filename = fileURLToPath(import.meta.url)
1010
const __dirname = path.dirname(__filename)
@@ -161,7 +161,9 @@ for (const item of targets) {
161161
console.log(`building ${name}`)
162162
await $`mkdir -p dist/${name}/bin`
163163

164-
const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"))
164+
const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
165+
const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
166+
const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
165167
const workerPath = "./src/cli/cmd/tui/worker.ts"
166168

167169
// Use platform-specific bunfs root path based on target OS

0 commit comments

Comments
 (0)