Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1300e59c1a |
@@ -0,0 +1,167 @@
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
const directory = "C:/OpenCode/ServerAutoApprove"
|
||||
const projectID = "proj_server_auto_approve"
|
||||
const sourceID = "ses_server_auto_approve_source"
|
||||
const targetID = "ses_server_auto_approve_target"
|
||||
const sourceTitle = "Configure server auto-approve"
|
||||
const targetTitle = "Use server auto-approve"
|
||||
|
||||
type EventPayload = {
|
||||
directory: string
|
||||
payload: Record<string, unknown>
|
||||
}
|
||||
|
||||
test("auto-approves permission requests across sessions on the same server", async ({ page }) => {
|
||||
const events: EventPayload[] = []
|
||||
const replies: Array<{ path: string; body: unknown }> = []
|
||||
|
||||
await mockOpenCodeServer(page, {
|
||||
directory,
|
||||
project: {
|
||||
id: projectID,
|
||||
worktree: directory,
|
||||
vcs: "git",
|
||||
name: "server-auto-approve",
|
||||
time: { created: 1700000000000, updated: 1700000000000 },
|
||||
sandboxes: [],
|
||||
},
|
||||
provider: {
|
||||
all: [
|
||||
{
|
||||
id: "opencode",
|
||||
name: "OpenCode",
|
||||
models: {
|
||||
"claude-opus-4-6": {
|
||||
id: "claude-opus-4-6",
|
||||
name: "Claude Opus 4.6",
|
||||
limit: { context: 200_000 },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connected: ["opencode"],
|
||||
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
|
||||
},
|
||||
sessions: [session(sourceID, sourceTitle, 1700000000000), session(targetID, targetTitle, 1700000001000)],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
events: () => events.splice(0, 1),
|
||||
eventRetry: 16,
|
||||
})
|
||||
await page.route("**/pty/shells*", (route) =>
|
||||
route.fulfill({ status: 200, contentType: "application/json", body: "[]" }),
|
||||
)
|
||||
await configurePage(page)
|
||||
|
||||
page.on("request", (request) => {
|
||||
const url = new URL(request.url())
|
||||
if (request.method() !== "POST" || !url.pathname.includes("/permissions/")) return
|
||||
replies.push({ path: url.pathname, body: request.postDataJSON() })
|
||||
const requestID = url.pathname.split("/").at(-1)!
|
||||
events.push(permissionReplied(targetID, requestID))
|
||||
})
|
||||
|
||||
await page.goto(sessionHref(sourceID))
|
||||
await expectSessionTitle(page, sourceTitle)
|
||||
await page.keyboard.press("Control+Comma")
|
||||
const settings = page.getByRole("dialog")
|
||||
await expect(settings).toBeVisible()
|
||||
const autoApprove = settings.getByRole("switch").first()
|
||||
await expect(autoApprove).not.toBeChecked()
|
||||
await autoApprove.focus()
|
||||
await page.keyboard.press("Space")
|
||||
await expect(autoApprove).toBeChecked()
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(settings).toHaveCount(0)
|
||||
|
||||
await page.getByRole("button", { name: "Home" }).click()
|
||||
await expect(page).toHaveURL("/")
|
||||
await page.getByText(targetTitle, { exact: true }).last().click()
|
||||
await expect(page).toHaveURL(sessionHref(targetID))
|
||||
await expectSessionTitle(page, targetTitle)
|
||||
events.push(permissionAsked(targetID, "permission-auto", "git status"))
|
||||
|
||||
await expect
|
||||
.poll(() => replies)
|
||||
.toEqual([
|
||||
{
|
||||
path: `/session/${targetID}/permissions/permission-auto`,
|
||||
body: { response: "once" },
|
||||
},
|
||||
])
|
||||
await expect(page.locator('[data-component="dock-prompt"][data-kind="permission"]')).toHaveCount(0)
|
||||
|
||||
await page.keyboard.press("Control+Comma")
|
||||
await expect(settings).toBeVisible()
|
||||
await autoApprove.focus()
|
||||
await page.keyboard.press("Space")
|
||||
await expect(autoApprove).not.toBeChecked()
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(settings).toHaveCount(0)
|
||||
|
||||
events.push(permissionAsked(targetID, "permission-manual", "git diff"))
|
||||
const dock = page.locator('[data-component="dock-prompt"][data-kind="permission"]')
|
||||
await expect(dock).toBeVisible()
|
||||
await expect(dock.getByText("git diff", { exact: true })).toBeVisible()
|
||||
expect(replies).toHaveLength(1)
|
||||
})
|
||||
|
||||
function session(id: string, title: string, created: number) {
|
||||
return {
|
||||
id,
|
||||
slug: id,
|
||||
projectID,
|
||||
directory,
|
||||
title,
|
||||
version: "dev",
|
||||
time: { created, updated: created },
|
||||
}
|
||||
}
|
||||
|
||||
function permissionAsked(sessionID: string, id: string, pattern: string): EventPayload {
|
||||
return {
|
||||
directory,
|
||||
payload: {
|
||||
type: "permission.asked",
|
||||
properties: { id, sessionID, permission: "bash", patterns: [pattern], metadata: {}, always: [] },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function permissionReplied(sessionID: string, requestID: string): EventPayload {
|
||||
return {
|
||||
directory,
|
||||
payload: { type: "permission.replied", properties: { sessionID, requestID, reply: "once" } },
|
||||
}
|
||||
}
|
||||
|
||||
function sessionHref(sessionID: string) {
|
||||
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||
return `/server/${base64Encode(server)}/session/${sessionID}`
|
||||
}
|
||||
|
||||
async function configurePage(page: Parameters<typeof mockOpenCodeServer>[0]) {
|
||||
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||
await page.addInitScript(
|
||||
({ directory, server, sessionIDs }) => {
|
||||
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:server",
|
||||
JSON.stringify({
|
||||
projects: { local: [{ worktree: directory, expanded: true }] },
|
||||
lastProject: { local: directory },
|
||||
}),
|
||||
)
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:tabs",
|
||||
JSON.stringify(
|
||||
sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64: base64Encode(directory), sessionId })),
|
||||
),
|
||||
)
|
||||
},
|
||||
{ directory, server, sessionIDs: [sourceID, targetID] },
|
||||
)
|
||||
}
|
||||
@@ -41,7 +41,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
@@ -204,7 +203,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const comments = useComments()
|
||||
const dialog = useDialog()
|
||||
const command = useCommand()
|
||||
const permission = usePermission()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const tabs = () => props.controls.session.tabs
|
||||
@@ -1147,12 +1145,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const variants = createMemo(() => ["default", ...props.controls.model.selection.variant.list()])
|
||||
// Check provider variants directly: `variants` also includes the UI-only default option.
|
||||
const showVariantControl = createMemo(() => props.controls.model.selection.variant.list().length > 0)
|
||||
const accepting = createMemo(() => {
|
||||
const id = props.controls.session.id
|
||||
if (!id) return permission.isAutoAcceptingDirectory(sdk().directory)
|
||||
return permission.isAutoAccepting(id, sdk().directory)
|
||||
})
|
||||
|
||||
const { abort, handleSubmit } =
|
||||
props.submission ??
|
||||
createPromptSubmit({
|
||||
@@ -1160,7 +1152,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
info,
|
||||
imageAttachments,
|
||||
commentCount,
|
||||
autoAccept: () => accepting(),
|
||||
mode: () => store.mode,
|
||||
working,
|
||||
editor: () => editorRef,
|
||||
|
||||
@@ -5,7 +5,6 @@ let createPromptSubmit: typeof import("./submit").createPromptSubmit
|
||||
|
||||
const createdClients: string[] = []
|
||||
const createdSessions: string[] = []
|
||||
const enabledAutoAccept: Array<{ sessionID: string; directory: string }> = []
|
||||
const optimistic: Array<{
|
||||
directory?: string
|
||||
sessionID?: string
|
||||
@@ -117,14 +116,6 @@ beforeAll(async () => {
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module("@/context/permission", () => ({
|
||||
usePermission: () => ({
|
||||
enableAutoAccept(sessionID: string, directory: string) {
|
||||
enabledAutoAccept.push({ sessionID, directory })
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module("@/context/server", () => ({
|
||||
useServer: () => ({ key: "server-key" }),
|
||||
}))
|
||||
@@ -235,7 +226,6 @@ beforeAll(async () => {
|
||||
beforeEach(() => {
|
||||
createdClients.length = 0
|
||||
createdSessions.length = 0
|
||||
enabledAutoAccept.length = 0
|
||||
optimistic.length = 0
|
||||
optimisticSeeded.length = 0
|
||||
promoted.length = 0
|
||||
@@ -256,7 +246,6 @@ describe("prompt submit worktree selection", () => {
|
||||
info: () => undefined,
|
||||
imageAttachments: () => [],
|
||||
commentCount: () => 0,
|
||||
autoAccept: () => false,
|
||||
mode: () => "shell",
|
||||
working: () => false,
|
||||
editor: () => undefined,
|
||||
@@ -288,34 +277,6 @@ describe("prompt submit worktree selection", () => {
|
||||
expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"])
|
||||
})
|
||||
|
||||
test("applies auto-accept to newly created sessions", async () => {
|
||||
const submit = createPromptSubmit({
|
||||
prompt,
|
||||
info: () => undefined,
|
||||
imageAttachments: () => [],
|
||||
commentCount: () => 0,
|
||||
autoAccept: () => true,
|
||||
mode: () => "shell",
|
||||
working: () => false,
|
||||
editor: () => undefined,
|
||||
queueScroll: () => undefined,
|
||||
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||
addToHistory: () => undefined,
|
||||
resetHistoryNavigation: () => undefined,
|
||||
setMode: () => undefined,
|
||||
setPopover: () => undefined,
|
||||
newSessionWorktree: () => selected,
|
||||
onNewSessionWorktreeReset: () => undefined,
|
||||
onSubmit: () => undefined,
|
||||
})
|
||||
|
||||
const event = { preventDefault: () => undefined } as unknown as Event
|
||||
|
||||
await submit.handleSubmit(event)
|
||||
|
||||
expect(enabledAutoAccept).toEqual([{ sessionID: "session-1", directory: "/repo/worktree-a" }])
|
||||
})
|
||||
|
||||
test("promotes drafts using the selected project's server", async () => {
|
||||
search = { draftId: "draft-1" }
|
||||
const submit = createPromptSubmit({
|
||||
@@ -323,7 +284,6 @@ describe("prompt submit worktree selection", () => {
|
||||
info: () => undefined,
|
||||
imageAttachments: () => [],
|
||||
commentCount: () => 0,
|
||||
autoAccept: () => false,
|
||||
mode: () => "normal",
|
||||
working: () => false,
|
||||
editor: () => undefined,
|
||||
@@ -352,7 +312,6 @@ describe("prompt submit worktree selection", () => {
|
||||
info: () => ({ id: "session-1" }),
|
||||
imageAttachments: () => [],
|
||||
commentCount: () => 0,
|
||||
autoAccept: () => false,
|
||||
mode: () => "normal",
|
||||
working: () => false,
|
||||
editor: () => undefined,
|
||||
@@ -384,7 +343,6 @@ describe("prompt submit worktree selection", () => {
|
||||
info: () => undefined,
|
||||
imageAttachments: () => [],
|
||||
commentCount: () => 0,
|
||||
autoAccept: () => false,
|
||||
mode: () => "normal",
|
||||
working: () => false,
|
||||
editor: () => undefined,
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useServerSync, type ServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { type ContextItem, type ImageAttachmentPart, type Prompt, type usePrompt } from "@/context/prompt"
|
||||
import { useSDK, type DirectorySDK } from "@/context/sdk"
|
||||
import { useSync, type DirectorySync } from "@/context/sync"
|
||||
@@ -175,7 +174,6 @@ type PromptSubmitInput = {
|
||||
info: Accessor<{ id: string } | undefined>
|
||||
imageAttachments: Accessor<ImageAttachmentPart[]>
|
||||
commentCount: Accessor<number>
|
||||
autoAccept: Accessor<boolean>
|
||||
mode: Accessor<"normal" | "shell">
|
||||
working: Accessor<boolean>
|
||||
editor: () => HTMLDivElement | undefined
|
||||
@@ -199,7 +197,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const sync = useSync()
|
||||
const serverSync = useServerSync()
|
||||
const local = useLocal()
|
||||
const permission = usePermission()
|
||||
const prompt = input.prompt
|
||||
const layout = useLayout()
|
||||
const language = useLanguage()
|
||||
@@ -314,7 +311,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
|
||||
const projectDirectory = sdk().directory
|
||||
const isNewSession = !params.id
|
||||
const shouldAutoAccept = isNewSession && input.autoAccept()
|
||||
const worktreeSelection = input.newSessionWorktree?.() || "main"
|
||||
|
||||
let sessionDirectory = projectDirectory
|
||||
@@ -374,7 +370,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
if (created) {
|
||||
seed(sessionDirectory, created)
|
||||
session = created
|
||||
if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
|
||||
local.session.promote(sessionDirectory, session.id)
|
||||
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
|
||||
const draftID = search.draftId
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { onCleanup } from "solid-js"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -6,7 +5,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
|
||||
export function useSettingsDialog() {
|
||||
const dialog = useDialog()
|
||||
const params = useParams<{ id?: string }>()
|
||||
let run = 0
|
||||
let dead = false
|
||||
|
||||
@@ -16,10 +14,9 @@ export function useSettingsDialog() {
|
||||
|
||||
return () => {
|
||||
const current = ++run
|
||||
const sessionID = params.id
|
||||
void import("@/components/settings-v2").then((module) => {
|
||||
if (dead || run !== current) return
|
||||
void dialog.show(() => <module.DialogSettings sessionID={sessionID} />)
|
||||
void dialog.show(() => <module.DialogSettings />)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
||||
@@ -26,7 +25,6 @@ import {
|
||||
terminalInput,
|
||||
useSettings,
|
||||
} from "@/context/settings"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
|
||||
import { Link } from "./link"
|
||||
import { SettingsList } from "./settings-list"
|
||||
@@ -87,37 +85,11 @@ export const SettingsGeneral: Component = () => {
|
||||
const permission = usePermission()
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
const params = useParams()
|
||||
const settings = useSettings()
|
||||
|
||||
const updater = useUpdaterAction()
|
||||
|
||||
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
||||
const dir = createMemo(() => decode64(params.dir))
|
||||
const accepting = createMemo(() => {
|
||||
const value = dir()
|
||||
if (!value) return false
|
||||
if (!params.id) return permission.isAutoAcceptingDirectory(value)
|
||||
return permission.isAutoAccepting(params.id, value)
|
||||
})
|
||||
|
||||
const toggleAccept = (checked: boolean) => {
|
||||
const value = dir()
|
||||
if (!value) return
|
||||
|
||||
if (!params.id) {
|
||||
if (permission.isAutoAcceptingDirectory(value) === checked) return
|
||||
permission.toggleAutoAcceptDirectory(value)
|
||||
return
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
permission.enableAutoAccept(params.id, value)
|
||||
return
|
||||
}
|
||||
|
||||
permission.disableAutoAccept(params.id, value)
|
||||
}
|
||||
const desktop = createMemo(() => platform.platform === "desktop")
|
||||
|
||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||
@@ -273,7 +245,7 @@ export const SettingsGeneral: Component = () => {
|
||||
description={language.t("toast.permissions.autoaccept.on.description")}
|
||||
>
|
||||
<div data-action="settings-auto-accept-permissions">
|
||||
<Switch checked={accepting()} disabled={!dir()} onChange={toggleAccept} />
|
||||
<Switch checked={permission.autoApprove()} onChange={permission.setAutoApprove} />
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ import { SettingsModelsV2 } from "./models"
|
||||
import "./settings-v2.css"
|
||||
import { SettingsServersV2 } from "./servers"
|
||||
|
||||
export const DialogSettings: Component<{
|
||||
sessionID?: string
|
||||
}> = (props) => {
|
||||
export const DialogSettings: Component = () => {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
|
||||
@@ -64,7 +62,7 @@ export const DialogSettings: Component<{
|
||||
</div>
|
||||
</TabsV2.List>
|
||||
<TabsV2.Content value="general" class="settings-v2-panel">
|
||||
<SettingsGeneralV2 sessionID={props.sessionID} />
|
||||
<SettingsGeneralV2 />
|
||||
</TabsV2.Content>
|
||||
<TabsV2.Content value="shortcuts" class="settings-v2-panel">
|
||||
<SettingsKeybinds v2 />
|
||||
|
||||
@@ -80,9 +80,7 @@ const playDemoSound = (id: string | undefined) => {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
export const SettingsGeneralV2: Component<{
|
||||
sessionID?: string
|
||||
}> = (props) => {
|
||||
export const SettingsGeneralV2: Component = () => {
|
||||
const theme = useTheme()
|
||||
const language = useLanguage()
|
||||
const permission = usePermission()
|
||||
@@ -95,27 +93,6 @@ export const SettingsGeneralV2: Component<{
|
||||
|
||||
const updater = useUpdaterAction()
|
||||
|
||||
const dir = createMemo(() => {
|
||||
if (!props.sessionID) return undefined
|
||||
return serverSync().session.lineage.peek(props.sessionID)?.session.directory
|
||||
})
|
||||
const accepting = createMemo(() => {
|
||||
const value = dir()
|
||||
if (!value || !props.sessionID) return false
|
||||
return permission.isAutoAccepting(props.sessionID, value)
|
||||
})
|
||||
|
||||
const toggleAccept = (checked: boolean) => {
|
||||
const value = dir()
|
||||
if (!value || !props.sessionID) return
|
||||
|
||||
if (checked) {
|
||||
permission.enableAutoAccept(props.sessionID, value)
|
||||
return
|
||||
}
|
||||
|
||||
permission.disableAutoAccept(props.sessionID, value)
|
||||
}
|
||||
const desktop = createMemo(() => platform.platform === "desktop")
|
||||
|
||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||
@@ -251,7 +228,7 @@ export const SettingsGeneralV2: Component<{
|
||||
description={language.t("toast.permissions.autoaccept.on.description")}
|
||||
>
|
||||
<div data-action="settings-auto-accept-permissions">
|
||||
<Switch checked={accepting()} disabled={!dir()} onChange={toggleAccept} />
|
||||
<Switch checked={permission.autoApprove()} onChange={permission.setAutoApprove} />
|
||||
</div>
|
||||
</SettingsRowV2>
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
|
||||
|
||||
const session = (input: { id: string; parentID?: string }) =>
|
||||
({
|
||||
id: input.id,
|
||||
parentID: input.parentID,
|
||||
}) as Session
|
||||
|
||||
const permission = (sessionID: string) =>
|
||||
({
|
||||
sessionID,
|
||||
}) as Pick<PermissionRequest, "sessionID">
|
||||
|
||||
describe("autoRespondsPermission", () => {
|
||||
test("uses a parent session's directory-scoped auto-accept", () => {
|
||||
const directory = "/tmp/project"
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
||||
const autoAccept = {
|
||||
[`${base64Encode(directory)}/root`]: true,
|
||||
}
|
||||
|
||||
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true)
|
||||
})
|
||||
|
||||
test("uses a parent session's legacy auto-accept key", () => {
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
||||
|
||||
expect(autoRespondsPermission({ root: true }, sessions, permission("child"), "/tmp/project")).toBe(true)
|
||||
})
|
||||
|
||||
test("defaults to requiring approval when no lineage override exists", () => {
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" }), session({ id: "other" })]
|
||||
const autoAccept = {
|
||||
other: true,
|
||||
}
|
||||
|
||||
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), "/tmp/project")).toBe(false)
|
||||
})
|
||||
|
||||
test("inherits a parent session's false override", () => {
|
||||
const directory = "/tmp/project"
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
||||
const autoAccept = {
|
||||
[`${base64Encode(directory)}/root`]: false,
|
||||
}
|
||||
|
||||
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(false)
|
||||
})
|
||||
|
||||
test("prefers a child override over parent override", () => {
|
||||
const directory = "/tmp/project"
|
||||
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
|
||||
const autoAccept = {
|
||||
[`${base64Encode(directory)}/root`]: false,
|
||||
[`${base64Encode(directory)}/child`]: true,
|
||||
}
|
||||
|
||||
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true)
|
||||
})
|
||||
|
||||
test("falls back to directory-level auto-accept", () => {
|
||||
const directory = "/tmp/project"
|
||||
const sessions = [session({ id: "root" })]
|
||||
const autoAccept = {
|
||||
[`${base64Encode(directory)}/*`]: true,
|
||||
}
|
||||
|
||||
expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(true)
|
||||
})
|
||||
|
||||
test("session-level override takes precedence over directory-level", () => {
|
||||
const directory = "/tmp/project"
|
||||
const sessions = [session({ id: "root" })]
|
||||
const autoAccept = {
|
||||
[`${base64Encode(directory)}/*`]: true,
|
||||
[`${base64Encode(directory)}/root`]: false,
|
||||
}
|
||||
|
||||
expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isDirectoryAutoAccepting", () => {
|
||||
test("returns true when directory key is set", () => {
|
||||
const directory = "/tmp/project"
|
||||
const autoAccept = { [`${base64Encode(directory)}/*`]: true }
|
||||
expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false when directory key is not set", () => {
|
||||
expect(isDirectoryAutoAccepting({}, "/tmp/project")).toBe(false)
|
||||
})
|
||||
|
||||
test("returns false when directory key is explicitly false", () => {
|
||||
const directory = "/tmp/project"
|
||||
const autoAccept = { [`${base64Encode(directory)}/*`]: false }
|
||||
expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,51 +0,0 @@
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
|
||||
export function acceptKey(sessionID: string, directory?: string) {
|
||||
if (!directory) return sessionID
|
||||
return `${base64Encode(directory)}/${sessionID}`
|
||||
}
|
||||
|
||||
export function directoryAcceptKey(directory: string) {
|
||||
return `${base64Encode(directory)}/*`
|
||||
}
|
||||
|
||||
function accepted(autoAccept: Record<string, boolean>, sessionID: string, directory?: string) {
|
||||
const key = acceptKey(sessionID, directory)
|
||||
const directoryKey = directory ? directoryAcceptKey(directory) : undefined
|
||||
return autoAccept[key] ?? autoAccept[sessionID] ?? (directoryKey ? autoAccept[directoryKey] : undefined)
|
||||
}
|
||||
|
||||
export function isDirectoryAutoAccepting(autoAccept: Record<string, boolean>, directory: string) {
|
||||
const key = directoryAcceptKey(directory)
|
||||
return autoAccept[key] ?? false
|
||||
}
|
||||
|
||||
function sessionLineage(session: { id: string; parentID?: string }[], sessionID: string) {
|
||||
const parent = session.reduce((acc, item) => {
|
||||
if (item.parentID) acc.set(item.id, item.parentID)
|
||||
return acc
|
||||
}, new Map<string, string>())
|
||||
const seen = new Set([sessionID])
|
||||
const ids = [sessionID]
|
||||
|
||||
for (const id of ids) {
|
||||
const parentID = parent.get(id)
|
||||
if (!parentID || seen.has(parentID)) continue
|
||||
seen.add(parentID)
|
||||
ids.push(parentID)
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
export function autoRespondsPermission(
|
||||
autoAccept: Record<string, boolean>,
|
||||
session: { id: string; parentID?: string }[],
|
||||
permission: { sessionID: string },
|
||||
directory?: string,
|
||||
) {
|
||||
const value = sessionLineage(session, permission.sessionID)
|
||||
.map((id) => accepted(autoAccept, id, directory))
|
||||
.find((item): item is boolean => item !== undefined)
|
||||
return value ?? false
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { beforeEach, describe, expect, test } from "bun:test"
|
||||
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { ServerScope } from "@/utils/server-scope"
|
||||
import { respondPermissionOnce } from "./permission-response"
|
||||
|
||||
const permission = (id: string): PermissionRequest => ({
|
||||
id,
|
||||
sessionID: "session",
|
||||
permission: "bash",
|
||||
patterns: ["git status"],
|
||||
metadata: {},
|
||||
always: [],
|
||||
})
|
||||
|
||||
const pending: Array<Parameters<Parameters<typeof respondPermissionOnce>[0]["respond"]>[0]> = []
|
||||
const respond = async (input: (typeof pending)[number]) => {
|
||||
pending.push(input)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
pending.length = 0
|
||||
})
|
||||
|
||||
describe("respondPermissionOnce", () => {
|
||||
test("deduplicates the same request across provider instances", () => {
|
||||
const request = permission(`permission-dedupe-${Date.now()}`)
|
||||
const input = { scope: ServerScope.local, permission: request, directory: "/repo", respond }
|
||||
|
||||
respondPermissionOnce(input)
|
||||
respondPermissionOnce(input)
|
||||
|
||||
expect(pending).toEqual([{ sessionID: "session", permissionID: request.id, response: "once", directory: "/repo" }])
|
||||
})
|
||||
|
||||
test("keeps requests on different servers independent", () => {
|
||||
const request = permission(`permission-server-${Date.now()}`)
|
||||
|
||||
respondPermissionOnce({ scope: ServerScope.local, permission: request, respond })
|
||||
respondPermissionOnce({
|
||||
scope: ServerScope.fromServerKey(ServerConnection.Key.make("remote")),
|
||||
permission: request,
|
||||
respond,
|
||||
})
|
||||
|
||||
expect(pending).toHaveLength(2)
|
||||
})
|
||||
|
||||
test("allows a retry after a failed response", async () => {
|
||||
const request = permission(`permission-retry-${Date.now()}`)
|
||||
let attempts = 0
|
||||
const fail = async () => {
|
||||
attempts += 1
|
||||
throw new Error("failed")
|
||||
}
|
||||
|
||||
respondPermissionOnce({ scope: ServerScope.local, permission: request, respond: fail })
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
respondPermissionOnce({ scope: ServerScope.local, permission: request, respond: fail })
|
||||
|
||||
expect(attempts).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
|
||||
import type { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
const MAX_RESPONDED = 1000
|
||||
const RESPONDED_TTL_MS = 60 * 60 * 1000
|
||||
const responded = new Map<string, number>()
|
||||
|
||||
function key(scope: ServerScope, permissionID: string) {
|
||||
return `${scope}\0${permissionID}`
|
||||
}
|
||||
|
||||
function prune(now: number) {
|
||||
for (const [id, ts] of responded) {
|
||||
if (now - ts < RESPONDED_TTL_MS) break
|
||||
responded.delete(id)
|
||||
}
|
||||
|
||||
for (const id of responded.keys()) {
|
||||
if (responded.size <= MAX_RESPONDED) break
|
||||
responded.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
export function respondPermissionOnce(input: {
|
||||
scope: ServerScope
|
||||
permission: PermissionRequest
|
||||
directory?: string
|
||||
respond: (input: {
|
||||
sessionID: string
|
||||
permissionID: string
|
||||
response: "once"
|
||||
directory?: string
|
||||
}) => Promise<unknown>
|
||||
}) {
|
||||
const id = key(input.scope, input.permission.id)
|
||||
const now = Date.now()
|
||||
const hit = responded.has(id)
|
||||
responded.delete(id)
|
||||
responded.set(id, now)
|
||||
prune(now)
|
||||
if (hit) return
|
||||
|
||||
void input
|
||||
.respond({
|
||||
sessionID: input.permission.sessionID,
|
||||
permissionID: input.permission.id,
|
||||
response: "once",
|
||||
directory: input.directory,
|
||||
})
|
||||
.catch(() => responded.delete(id))
|
||||
}
|
||||
@@ -1,25 +1,12 @@
|
||||
import { type Accessor, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { type Accessor, createMemo, onCleanup } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "./server-sync"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import {
|
||||
acceptKey,
|
||||
directoryAcceptKey,
|
||||
isDirectoryAutoAccepting,
|
||||
autoRespondsPermission,
|
||||
} from "./permission-auto-respond"
|
||||
|
||||
type PermissionRespondFn = (input: {
|
||||
sessionID: string
|
||||
permissionID: string
|
||||
response: "once" | "always" | "reject"
|
||||
directory?: string
|
||||
}) => void
|
||||
import { useSettings } from "./settings"
|
||||
import { respondPermissionOnce } from "./permission-response"
|
||||
|
||||
function isNonAllowRule(rule: unknown) {
|
||||
if (!rule) return false
|
||||
@@ -51,6 +38,8 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
const params = useParams()
|
||||
const serverSDK = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const settings = useSettings()
|
||||
const autoApprove = () => settings.permissions.autoApprove(serverSDK().scope)
|
||||
|
||||
const permissionsEnabled = createMemo(() => {
|
||||
const directory = props.directory?.() ?? decode64(params.dir)
|
||||
@@ -59,215 +48,39 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
return hasPermissionPromptRules(store.config.permission)
|
||||
})
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
{
|
||||
...Persist.serverGlobal(serverSDK().scope, "permission", ["permission.v3"]),
|
||||
migrate(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return value
|
||||
|
||||
const data = value as Record<string, unknown>
|
||||
if (data.autoAccept) return value
|
||||
|
||||
return {
|
||||
...data,
|
||||
autoAccept:
|
||||
typeof data.autoAcceptEdits === "object" && data.autoAcceptEdits && !Array.isArray(data.autoAcceptEdits)
|
||||
? data.autoAcceptEdits
|
||||
: {},
|
||||
}
|
||||
},
|
||||
},
|
||||
createStore({
|
||||
autoAccept: {} as Record<string, boolean>,
|
||||
}),
|
||||
)
|
||||
|
||||
// When config has permission: "allow", auto-enable directory-level auto-accept
|
||||
createEffect(() => {
|
||||
if (!ready()) return
|
||||
const directory = props.directory?.() ?? decode64(params.dir)
|
||||
if (!directory) return
|
||||
const [childStore] = serverSync().child(directory)
|
||||
const perm = childStore.config.permission
|
||||
if (typeof perm === "string" && perm === "allow") {
|
||||
const key = directoryAcceptKey(directory)
|
||||
if (store.autoAccept[key] === undefined) {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.autoAccept[key] = true
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const MAX_RESPONDED = 1000
|
||||
const RESPONDED_TTL_MS = 60 * 60 * 1000
|
||||
const responded = new Map<string, number>()
|
||||
const enableVersion = new Map<string, number>()
|
||||
|
||||
function pruneResponded(now: number) {
|
||||
for (const [id, ts] of responded) {
|
||||
if (now - ts < RESPONDED_TTL_MS) break
|
||||
responded.delete(id)
|
||||
}
|
||||
|
||||
for (const id of responded.keys()) {
|
||||
if (responded.size <= MAX_RESPONDED) break
|
||||
responded.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
const respond: PermissionRespondFn = (input) => {
|
||||
serverSDK()
|
||||
.client.permission.respond(input)
|
||||
.catch(() => {
|
||||
responded.delete(input.permissionID)
|
||||
})
|
||||
}
|
||||
|
||||
function respondOnce(permission: PermissionRequest, directory?: string) {
|
||||
const now = Date.now()
|
||||
const hit = responded.has(permission.id)
|
||||
responded.delete(permission.id)
|
||||
responded.set(permission.id, now)
|
||||
pruneResponded(now)
|
||||
if (hit) return
|
||||
respond({
|
||||
sessionID: permission.sessionID,
|
||||
permissionID: permission.id,
|
||||
response: "once",
|
||||
respondPermissionOnce({
|
||||
scope: serverSDK().scope,
|
||||
permission,
|
||||
directory,
|
||||
respond: (input) => serverSDK().client.permission.respond(input),
|
||||
})
|
||||
}
|
||||
|
||||
function isAutoAccepting(sessionID: string, directory?: string) {
|
||||
const session = directory ? serverSync().child(directory, { bootstrap: false })[0].session : []
|
||||
return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
|
||||
}
|
||||
|
||||
function isAutoAcceptingDirectory(directory: string) {
|
||||
return isDirectoryAutoAccepting(store.autoAccept, directory)
|
||||
}
|
||||
|
||||
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
|
||||
const session = directory ? serverSync().child(directory, { bootstrap: false })[0].session : []
|
||||
return autoRespondsPermission(store.autoAccept, session, permission, directory)
|
||||
}
|
||||
|
||||
function bumpEnableVersion(sessionID: string, directory?: string) {
|
||||
const key = acceptKey(sessionID, directory)
|
||||
const next = (enableVersion.get(key) ?? 0) + 1
|
||||
enableVersion.set(key, next)
|
||||
return next
|
||||
}
|
||||
|
||||
const unsubscribe = serverSDK().event.listen((e) => {
|
||||
const event = e.details
|
||||
if (event?.type !== "permission.asked") return
|
||||
|
||||
const perm = event.properties
|
||||
if (!shouldAutoRespond(perm, e.name)) return
|
||||
|
||||
respondOnce(perm, e.name)
|
||||
if (!autoApprove()) return
|
||||
respondOnce(event.properties, e.name)
|
||||
})
|
||||
onCleanup(unsubscribe)
|
||||
|
||||
function enableDirectory(directory: string) {
|
||||
const key = directoryAcceptKey(directory)
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.autoAccept[key] = true
|
||||
}),
|
||||
)
|
||||
|
||||
serverSDK()
|
||||
.client.permission.list({ directory })
|
||||
.then((x) => {
|
||||
if (!isAutoAcceptingDirectory(directory)) return
|
||||
for (const perm of x.data ?? []) {
|
||||
if (!perm?.id) continue
|
||||
if (!shouldAutoRespond(perm, directory)) continue
|
||||
respondOnce(perm, directory)
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
function disableDirectory(directory: string) {
|
||||
const key = directoryAcceptKey(directory)
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.autoAccept[key] = false
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function enable(sessionID: string, directory: string) {
|
||||
const key = acceptKey(sessionID, directory)
|
||||
const version = bumpEnableVersion(sessionID, directory)
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.autoAccept[key] = true
|
||||
delete draft.autoAccept[sessionID]
|
||||
}),
|
||||
)
|
||||
|
||||
serverSDK()
|
||||
.client.permission.list({ directory })
|
||||
.then((x) => {
|
||||
if (enableVersion.get(key) !== version) return
|
||||
if (!isAutoAccepting(sessionID, directory)) return
|
||||
for (const perm of x.data ?? []) {
|
||||
if (!perm?.id) continue
|
||||
if (!shouldAutoRespond(perm, directory)) continue
|
||||
respondOnce(perm, directory)
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
function disable(sessionID: string, directory?: string) {
|
||||
bumpEnableVersion(sessionID, directory)
|
||||
const key = directory ? acceptKey(sessionID, directory) : sessionID
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.autoAccept[key] = false
|
||||
if (!directory) return
|
||||
delete draft.autoAccept[sessionID]
|
||||
}),
|
||||
)
|
||||
function setAutoApprove(value: boolean) {
|
||||
settings.permissions.setAutoApprove(serverSDK().scope, value)
|
||||
if (!value) return
|
||||
for (const requests of Object.values(serverSync().session.data.permission)) {
|
||||
for (const permission of requests ?? []) {
|
||||
const directory = serverSync().session.get(permission.sessionID)?.directory
|
||||
if (directory) respondOnce(permission, directory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ready,
|
||||
respond,
|
||||
autoResponds(permission: PermissionRequest, directory?: string) {
|
||||
return shouldAutoRespond(permission, directory)
|
||||
},
|
||||
isAutoAccepting,
|
||||
isAutoAcceptingDirectory,
|
||||
toggleAutoAccept(sessionID: string, directory: string) {
|
||||
if (isAutoAccepting(sessionID, directory)) {
|
||||
disable(sessionID, directory)
|
||||
return
|
||||
}
|
||||
|
||||
enable(sessionID, directory)
|
||||
},
|
||||
toggleAutoAcceptDirectory(directory: string) {
|
||||
if (isAutoAcceptingDirectory(directory)) {
|
||||
disableDirectory(directory)
|
||||
return
|
||||
}
|
||||
enableDirectory(directory)
|
||||
},
|
||||
enableAutoAccept(sessionID: string, directory: string) {
|
||||
if (isAutoAccepting(sessionID, directory)) return
|
||||
enable(sessionID, directory)
|
||||
},
|
||||
disableAutoAccept(sessionID: string, directory?: string) {
|
||||
disable(sessionID, directory)
|
||||
autoApprove,
|
||||
setAutoApprove,
|
||||
autoResponds(_permission: PermissionRequest, _directory?: string) {
|
||||
return autoApprove()
|
||||
},
|
||||
permissionsEnabled,
|
||||
isPermissionAllowAll(directory: string) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createStore, reconcile } from "solid-js/store"
|
||||
import { createEffect, createMemo } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { persisted } from "@/utils/persist"
|
||||
import type { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
export interface NotificationSettings {
|
||||
agent: boolean
|
||||
@@ -43,7 +44,7 @@ export interface Settings {
|
||||
}
|
||||
keybinds: Record<string, string>
|
||||
permissions: {
|
||||
autoApprove: boolean
|
||||
autoApprove: Record<string, boolean>
|
||||
}
|
||||
notifications: NotificationSettings
|
||||
sounds: SoundSettings
|
||||
@@ -127,7 +128,7 @@ const defaultSettings: Settings = {
|
||||
},
|
||||
keybinds: {},
|
||||
permissions: {
|
||||
autoApprove: false,
|
||||
autoApprove: {},
|
||||
},
|
||||
notifications: {
|
||||
agent: true,
|
||||
@@ -175,6 +176,12 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setStore("general", "followup", "steer")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const current: unknown = store.permissions?.autoApprove
|
||||
if (typeof current !== "boolean") return
|
||||
setStore("permissions", "autoApprove", {})
|
||||
})
|
||||
|
||||
return {
|
||||
ready,
|
||||
get current() {
|
||||
@@ -295,9 +302,11 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
autoApprove: withFallback(() => store.permissions?.autoApprove, defaultSettings.permissions.autoApprove),
|
||||
setAutoApprove(value: boolean) {
|
||||
setStore("permissions", "autoApprove", value)
|
||||
autoApprove(scope: ServerScope) {
|
||||
return store.permissions?.autoApprove?.[scope] ?? false
|
||||
},
|
||||
setAutoApprove(scope: ServerScope, value: boolean) {
|
||||
setStore("permissions", "autoApprove", scope, value)
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
|
||||
@@ -139,11 +139,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const mcpCommand = withCategory(language.t("command.category.mcp"))
|
||||
const permissionsCommand = withCategory(language.t("command.category.permissions"))
|
||||
|
||||
const isAutoAcceptActive = () => {
|
||||
const sessionID = params.id
|
||||
if (sessionID) return permission.isAutoAccepting(sessionID, sdk().directory)
|
||||
return permission.isAutoAcceptingDirectory(sdk().directory)
|
||||
}
|
||||
const isAutoAcceptActive = permission.autoApprove
|
||||
const write = async (value: string) => {
|
||||
const body = typeof document === "undefined" ? undefined : document.body
|
||||
if (body) {
|
||||
@@ -277,13 +273,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
}
|
||||
|
||||
const toggleAutoAccept = () => {
|
||||
const sessionID = params.id
|
||||
if (sessionID) permission.toggleAutoAccept(sessionID, sdk().directory)
|
||||
else permission.toggleAutoAcceptDirectory(sdk().directory)
|
||||
|
||||
const active = sessionID
|
||||
? permission.isAutoAccepting(sessionID, sdk().directory)
|
||||
: permission.isAutoAcceptingDirectory(sdk().directory)
|
||||
const active = !permission.autoApprove()
|
||||
permission.setAutoApprove(active)
|
||||
showToast({
|
||||
title: active
|
||||
? language.t("toast.permissions.autoaccept.on.title")
|
||||
|
||||
Reference in New Issue
Block a user