Skip to content

Commit d7b9fc7

Browse files
committed
feat(app): enable server-wide permission auto-accept
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fc19188-72f7-45c9-9cb3-fc3aad38a17c
1 parent 393798a commit d7b9fc7

8 files changed

Lines changed: 122 additions & 62 deletions

File tree

packages/app/e2e/regression/remote-session-settings.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ test("session settings use the remote server context", async ({ page }) => {
3131
.poll(() =>
3232
permissionRequests.some((request) => {
3333
const url = new URL(request)
34-
return url.origin === serverB && url.searchParams.get("directory") === directoryB
34+
return url.origin === serverB && url.searchParams.get("directory") === null
3535
}),
3636
)
3737
.toBe(true)

packages/app/src/components/settings-dialog.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import { useParams } from "@solidjs/router"
21
import { onCleanup } from "solid-js"
32
import { useCommand } from "@/context/command"
43
import { useLanguage } from "@/context/language"
54
import { useDialog } from "@opencode-ai/ui/context/dialog"
65

76
export function useSettingsDialog(defaultValue?: string) {
87
const dialog = useDialog()
9-
const params = useParams<{ id?: string }>()
108
let run = 0
119
let dead = false
1210

@@ -16,10 +14,9 @@ export function useSettingsDialog(defaultValue?: string) {
1614

1715
return () => {
1816
const current = ++run
19-
const sessionID = params.id
2017
void import("@/components/settings-v2").then((module) => {
2118
if (dead || run !== current) return
22-
void dialog.show(() => <module.DialogSettings sessionID={sessionID} defaultValue={defaultValue} />)
19+
void dialog.show(() => <module.DialogSettings defaultValue={defaultValue} />)
2320
})
2421
}
2522
}

packages/app/src/components/settings-general.tsx

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
88
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
99
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
1010
import { useDialog } from "@opencode-ai/ui/context/dialog"
11-
import { useParams } from "@solidjs/router"
1211
import { useLanguage } from "@/context/language"
1312
import { usePermission } from "@/context/permission"
1413
import { usePlatform, type DisplayBackend } from "@/context/platform"
@@ -27,7 +26,6 @@ import {
2726
terminalInput,
2827
useSettings,
2928
} from "@/context/settings"
30-
import { decode64 } from "@/utils/base64"
3129
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
3230
import { Link } from "./link"
3331
import { SettingsList } from "./settings-list"
@@ -88,36 +86,16 @@ export const SettingsGeneral: Component = () => {
8886
const permission = usePermission()
8987
const platform = usePlatform()
9088
const dialog = useDialog()
91-
const params = useParams()
9289
const settings = useSettings()
9390

9491
const updater = useUpdaterAction()
9592

9693
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
97-
const dir = createMemo(() => decode64(params.dir))
98-
const accepting = createMemo(() => {
99-
const value = dir()
100-
if (!value) return false
101-
if (!params.id) return permission.isAutoAcceptingDirectory(value)
102-
return permission.isAutoAccepting(params.id, value)
103-
})
94+
const accepting = createMemo(() => permission.isAutoAcceptingServer())
10495

10596
const toggleAccept = (checked: boolean) => {
106-
const value = dir()
107-
if (!value) return
108-
109-
if (!params.id) {
110-
if (permission.isAutoAcceptingDirectory(value) === checked) return
111-
permission.toggleAutoAcceptDirectory(value)
112-
return
113-
}
114-
115-
if (checked) {
116-
permission.enableAutoAccept(params.id, value)
117-
return
118-
}
119-
120-
permission.disableAutoAccept(params.id, value)
97+
if (permission.isAutoAcceptingServer() === checked) return
98+
permission.toggleAutoAcceptServer()
12199
}
122100
const desktop = createMemo(() => platform.platform === "desktop")
123101

@@ -318,7 +296,7 @@ export const SettingsGeneral: Component = () => {
318296
description={language.t("toast.permissions.autoaccept.on.description")}
319297
>
320298
<div data-action="settings-auto-accept-permissions">
321-
<Switch checked={accepting()} disabled={!dir()} onChange={toggleAccept} />
299+
<Switch checked={accepting()} onChange={toggleAccept} />
322300
</div>
323301
</SettingsRow>
324302

packages/app/src/components/settings-v2/dialog-settings-v2.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,14 @@ import "./settings-v2.css"
1212
import { SettingsServersV2 } from "./servers"
1313
import { useDialog } from "@opencode-ai/ui/context/dialog"
1414

15-
export const DialogSettings: Component<{
16-
sessionID?: string
17-
defaultValue?: string
18-
}> = (props) => {
15+
export const DialogSettings: Component<{ defaultValue?: string }> = (props) => {
1916
const language = useLanguage()
2017
const platform = usePlatform()
2118
const dialog = useDialog()
2219
const [tab, setTab] = createSignal(props.defaultValue ?? "general")
2320

2421
const showProviders = () => {
25-
void dialog.show(() => <DialogSettings sessionID={props.sessionID} defaultValue="providers" />)
22+
void dialog.show(() => <DialogSettings defaultValue="providers" />)
2623
}
2724

2825
return (
@@ -78,7 +75,7 @@ export const DialogSettings: Component<{
7875
</div>
7976
</TabsV2.List>
8077
<TabsV2.Content value="general" class="settings-v2-panel">
81-
<SettingsGeneralV2 sessionID={props.sessionID} />
78+
<SettingsGeneralV2 />
8279
</TabsV2.Content>
8380
<TabsV2.Content value="shortcuts" class="settings-v2-panel">
8481
<SettingsKeybinds v2 />

packages/app/src/components/settings-v2/general.tsx

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,7 @@ const playDemoSound = (id: string | undefined) => {
8181
}, 100)
8282
}
8383

84-
export const SettingsGeneralV2: Component<{
85-
sessionID?: string
86-
}> = (props) => {
84+
export const SettingsGeneralV2: Component = () => {
8785
const theme = useTheme()
8886
const language = useLanguage()
8987
const permission = usePermission()
@@ -96,26 +94,11 @@ export const SettingsGeneralV2: Component<{
9694

9795
const updater = useUpdaterAction()
9896

99-
const dir = createMemo(() => {
100-
if (!props.sessionID) return undefined
101-
return serverSync().session.lineage.peek(props.sessionID)?.session.directory
102-
})
103-
const accepting = createMemo(() => {
104-
const value = dir()
105-
if (!value || !props.sessionID) return false
106-
return permission.isAutoAccepting(props.sessionID, value)
107-
})
97+
const accepting = createMemo(() => permission.isAutoAcceptingServer())
10898

10999
const toggleAccept = (checked: boolean) => {
110-
const value = dir()
111-
if (!value || !props.sessionID) return
112-
113-
if (checked) {
114-
permission.enableAutoAccept(props.sessionID, value)
115-
return
116-
}
117-
118-
permission.disableAutoAccept(props.sessionID, value)
100+
if (permission.isAutoAcceptingServer() === checked) return
101+
permission.toggleAutoAcceptServer()
119102
}
120103
const desktop = createMemo(() => platform.platform === "desktop")
121104

@@ -277,7 +260,7 @@ export const SettingsGeneralV2: Component<{
277260
description={language.t("toast.permissions.autoaccept.on.description")}
278261
>
279262
<div data-action="settings-auto-accept-permissions">
280-
<Switch checked={accepting()} disabled={!dir()} onChange={toggleAccept} />
263+
<Switch checked={accepting()} onChange={toggleAccept} />
281264
</div>
282265
</SettingsRowV2>
283266

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
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/core/util/encode"
4-
import { autoRespondsPermission, isDirectoryAutoAccepting, sessionAutoAccept } from "./permission-auto-respond"
4+
import {
5+
SERVER_ACCEPT_KEY,
6+
autoRespondsPermission,
7+
isDirectoryAutoAccepting,
8+
isServerAutoAccepting,
9+
sessionAutoAccept,
10+
} from "./permission-auto-respond"
511

612
const session = (input: { id: string; parentID?: string }) =>
713
({
@@ -40,6 +46,24 @@ describe("autoRespondsPermission", () => {
4046
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), "/tmp/project")).toBe(false)
4147
})
4248

49+
test("falls back to server-wide auto-accept", () => {
50+
const sessions = [session({ id: "root" })]
51+
const autoAccept = { [SERVER_ACCEPT_KEY]: true }
52+
expect(autoRespondsPermission(autoAccept, sessions, permission("root"), "/tmp/project")).toBe(true)
53+
expect(autoRespondsPermission(autoAccept, sessions, permission("root"))).toBe(true)
54+
})
55+
56+
test("session override takes precedence over server-wide auto-accept", () => {
57+
const directory = "/tmp/project"
58+
const sessions = [session({ id: "root" })]
59+
const autoAccept = {
60+
[SERVER_ACCEPT_KEY]: true,
61+
[`${base64Encode(directory)}/root`]: false,
62+
}
63+
64+
expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(false)
65+
})
66+
4367
test("inherits a parent session's false override", () => {
4468
const directory = "/tmp/project"
4569
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
@@ -122,4 +146,25 @@ describe("isDirectoryAutoAccepting", () => {
122146
const autoAccept = { [`${base64Encode(directory)}/*`]: false }
123147
expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(false)
124148
})
149+
150+
test("falls back to server-wide auto-accept", () => {
151+
expect(isDirectoryAutoAccepting({ [SERVER_ACCEPT_KEY]: true }, "/tmp/project")).toBe(true)
152+
})
153+
154+
test("explicit false overrides server-wide auto-accept", () => {
155+
const directory = "/tmp/project"
156+
const autoAccept = {
157+
[SERVER_ACCEPT_KEY]: true,
158+
[`${base64Encode(directory)}/*`]: false,
159+
}
160+
expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(false)
161+
})
162+
})
163+
164+
describe("isServerAutoAccepting", () => {
165+
test("reflects the server-wide setting", () => {
166+
expect(isServerAutoAccepting({ [SERVER_ACCEPT_KEY]: true })).toBe(true)
167+
expect(isServerAutoAccepting({ [SERVER_ACCEPT_KEY]: false })).toBe(false)
168+
expect(isServerAutoAccepting({})).toBe(false)
169+
})
125170
})

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { base64Encode } from "@opencode-ai/core/util/encode"
22

3+
export const SERVER_ACCEPT_KEY = "*/*"
4+
35
export function acceptKey(sessionID: string, directory?: string) {
46
if (!directory) return sessionID
57
return `${base64Encode(directory)}/${sessionID}`
@@ -16,7 +18,11 @@ function accepted(autoAccept: Record<string, boolean>, sessionID: string, direct
1618

1719
export function isDirectoryAutoAccepting(autoAccept: Record<string, boolean>, directory: string) {
1820
const key = directoryAcceptKey(directory)
19-
return autoAccept[key] ?? false
21+
return autoAccept[key] ?? isServerAutoAccepting(autoAccept)
22+
}
23+
24+
export function isServerAutoAccepting(autoAccept: Record<string, boolean>) {
25+
return autoAccept[SERVER_ACCEPT_KEY] ?? false
2026
}
2127

2228
function sessionLineage(session: { id: string; parentID?: string }[], sessionID: string) {
@@ -45,7 +51,7 @@ export function autoRespondsPermission(
4551
) {
4652
const value = sessionAutoAccept(autoAccept, session, permission, directory)
4753
if (value !== undefined) return value
48-
return directory ? isDirectoryAutoAccepting(autoAccept, directory) : false
54+
return directory ? isDirectoryAutoAccepting(autoAccept, directory) : isServerAutoAccepting(autoAccept)
4955
}
5056

5157
export function sessionAutoAccept(

packages/app/src/context/permission.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {
1717
acceptKey,
1818
directoryAcceptKey,
1919
isDirectoryAutoAccepting,
20+
isServerAutoAccepting,
2021
autoRespondsPermission,
22+
SERVER_ACCEPT_KEY,
2123
sessionAutoAccept,
2224
} from "./permission-auto-respond"
2325

@@ -163,12 +165,18 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
163165
isAutoAcceptingDirectory(directory: string) {
164166
return selected().isAutoAcceptingDirectory(directory)
165167
},
168+
isAutoAcceptingServer() {
169+
return selected().isAutoAcceptingServer()
170+
},
166171
toggleAutoAccept(sessionID: string, directory: string) {
167172
selected().toggleAutoAccept(sessionID, directory)
168173
},
169174
toggleAutoAcceptDirectory(directory: string) {
170175
selected().toggleAutoAcceptDirectory(directory)
171176
},
177+
toggleAutoAcceptServer() {
178+
selected().toggleAutoAcceptServer()
179+
},
172180
enableAutoAccept(sessionID: string, directory: string) {
173181
selected().enableAutoAccept(sessionID, directory)
174182
},
@@ -277,6 +285,10 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }
277285
return isDirectoryAutoAccepting(store.autoAccept, directory)
278286
}
279287

288+
function isAutoAcceptingServer() {
289+
return isServerAutoAccepting(store.autoAccept)
290+
}
291+
280292
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
281293
return autoRespondsPermission(store.autoAccept, sessions(directory), permission, directory)
282294
}
@@ -334,6 +346,36 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }
334346
unsubscribe()
335347
})
336348

349+
function enableServer() {
350+
if (meta.disposed) return
351+
setStore(
352+
produce((draft) => {
353+
draft.autoAccept[SERVER_ACCEPT_KEY] = true
354+
}),
355+
)
356+
357+
input.sdk.client.permission
358+
.list()
359+
.then((x) => {
360+
if (meta.disposed || !isAutoAcceptingServer()) return
361+
for (const perm of x.data ?? []) {
362+
if (!perm?.id) continue
363+
const directory = sessions().find((session) => session.id === perm.sessionID)?.directory
364+
void respondPending(perm, directory, isAutoAcceptingServer)
365+
}
366+
})
367+
.catch(() => undefined)
368+
}
369+
370+
function disableServer() {
371+
if (meta.disposed) return
372+
setStore(
373+
produce((draft) => {
374+
draft.autoAccept[SERVER_ACCEPT_KEY] = false
375+
}),
376+
)
377+
}
378+
337379
function enableDirectory(directory: string) {
338380
if (meta.disposed) return
339381
const key = directoryAcceptKey(directory)
@@ -423,6 +465,10 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }
423465
if (meta.disposed) return false
424466
return isAutoAcceptingDirectory(directory)
425467
},
468+
isAutoAcceptingServer() {
469+
if (meta.disposed) return false
470+
return isAutoAcceptingServer()
471+
},
426472
toggleAutoAccept(sessionID: string, directory: string) {
427473
if (meta.disposed) return
428474
if (isAutoAccepting(sessionID, directory)) {
@@ -440,6 +486,14 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }
440486
}
441487
enableDirectory(directory)
442488
},
489+
toggleAutoAcceptServer() {
490+
if (meta.disposed) return
491+
if (isAutoAcceptingServer()) {
492+
disableServer()
493+
return
494+
}
495+
enableServer()
496+
},
443497
enableAutoAccept(sessionID: string, directory: string) {
444498
if (meta.disposed) return
445499
if (isAutoAccepting(sessionID, directory)) return

0 commit comments

Comments
 (0)