mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 12:10:45 +03:00
feat(app): per-project auto-accept permissions overriding a default (#54839)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
e65080dd08
commit
b8f925e51c
@@ -74,14 +74,32 @@ async function setup(page: Page, input: { tabs: TabSeed[]; a?: Partial<MockServe
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
const enableAutoAccept = async () => {
|
||||
await page.keyboard.press("Control+,")
|
||||
const autoAccept = page.getByTestId("settings-screen").locator('[data-action="settings-auto-accept-permissions"]')
|
||||
await autoAccept.locator('[data-slot="switch-control"]').click()
|
||||
await expect(autoAccept.getByRole("switch")).toBeChecked()
|
||||
return {
|
||||
replies,
|
||||
sessionGets,
|
||||
transport: servers[SERVER]!.transport,
|
||||
listed,
|
||||
enableAutoAccept: () => enableAutoAccept(page),
|
||||
}
|
||||
}
|
||||
|
||||
return { replies, sessionGets, transport: servers[SERVER]!.transport, listed, enableAutoAccept }
|
||||
async function enableAutoAccept(page: Page) {
|
||||
await page.keyboard.press("Control+,")
|
||||
const autoAccept = page.getByTestId("settings-screen").locator('[data-action="settings-auto-accept-permissions"]')
|
||||
await autoAccept.locator('[data-slot="switch-control"]').click()
|
||||
await expect(autoAccept.getByRole("switch")).toBeChecked()
|
||||
}
|
||||
|
||||
async function chooseProjectAutoAccept(page: Page, project: string, current: string, choice: "On" | "Off") {
|
||||
const settings = page.getByTestId("settings-screen")
|
||||
await settings.getByRole("tab", { name: "Projects", exact: true }).click()
|
||||
await settings.getByRole("button", { name: project, exact: true }).click()
|
||||
const autoAccept = settings.locator('[data-action="settings-project-auto-accept-permissions"]')
|
||||
await expect(autoAccept).toHaveText(current)
|
||||
await autoAccept.click()
|
||||
await page.getByRole("option", { name: choice, exact: true }).click()
|
||||
await expect(autoAccept).toHaveText(choice)
|
||||
await settings.getByRole("button", { name: "Back to projects", exact: true }).click()
|
||||
}
|
||||
|
||||
const reply = (sessionID: string, permissionID: string) => ({
|
||||
@@ -201,3 +219,60 @@ test("auto-accept approves a request discovered by opening a session", async ({
|
||||
await view.enableAutoAccept()
|
||||
await expect.poll(() => view.replies).toEqual([reply(sessionA.id, "permission-synced-a")])
|
||||
})
|
||||
|
||||
test("a project's auto-accept choice overrides the default in both directions", async ({ page }) => {
|
||||
const api = session({ id: "ses_project_api", directory: "/work/api", title: "API session", projectID: "proj_api" })
|
||||
const web = session({ id: "ses_project_web", directory: "/work/web", title: "Web session", projectID: "proj_web" })
|
||||
const requests = { [api.id]: [pending("permission-api", api.id)], [web.id]: [pending("permission-web", web.id)] }
|
||||
const replies: string[] = []
|
||||
await mockServers(page, {
|
||||
[SERVER]: {
|
||||
directory: "/work/api",
|
||||
project: project({ id: "proj_api", directory: "/work/api" }),
|
||||
projects: [
|
||||
project({ id: "proj_api", directory: "/work/api" }),
|
||||
project({ id: "proj_web", directory: "/work/web" }),
|
||||
],
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
sessions: [api, web],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
sessionStatus: { [api.id]: { type: "running" }, [web.id]: { type: "running" } },
|
||||
permissions: () => Object.values(requests).flat(),
|
||||
sessionPermissions: requests,
|
||||
onPermissionReply: (reply) => replies.push(reply.permissionID),
|
||||
},
|
||||
})
|
||||
await seed(page, {
|
||||
projects: { local: [{ worktree: "/work/api" }, { worktree: "/work/web" }] },
|
||||
tabs: [api.id, web.id],
|
||||
})
|
||||
const dock = page.locator('[data-component="dock-prompt"][data-kind="permission"]')
|
||||
const settings = page.getByTestId("settings-screen")
|
||||
|
||||
await page.goto(sessionHref(web.id))
|
||||
await expectSessionTitle(page, web.title)
|
||||
await expect(dock.getByText("git status")).toBeVisible()
|
||||
|
||||
// On for api while the default is off; off for web before the default turns on.
|
||||
await page.keyboard.press("Control+,")
|
||||
await chooseProjectAutoAccept(page, "api", "Use default (Off)", "On")
|
||||
await expect.poll(() => replies).toEqual(["permission-api"])
|
||||
await chooseProjectAutoAccept(page, "web", "Use default (Off)", "Off")
|
||||
await settings.getByRole("tab", { name: "Preferences", exact: true }).click()
|
||||
const autoAccept = settings.locator('[data-action="settings-auto-accept-permissions"]')
|
||||
await autoAccept.locator('[data-slot="switch-control"]').click()
|
||||
await expect(autoAccept.getByRole("switch")).toBeChecked()
|
||||
await settings.getByRole("button", { name: "Back to app" }).click()
|
||||
|
||||
await expectSessionTitle(page, web.title)
|
||||
await expect(dock.getByText("git status")).toBeVisible()
|
||||
expect(replies).toEqual(["permission-api"])
|
||||
|
||||
// Back on the default, web follows it.
|
||||
await page.keyboard.press("Control+,")
|
||||
await settings.getByRole("tab", { name: "Projects", exact: true }).click()
|
||||
await settings.getByRole("button", { name: "web", exact: true }).click()
|
||||
await settings.locator('[data-action="settings-project-auto-accept-permissions"]').click()
|
||||
await page.getByRole("option", { name: "Use default (On)" }).click()
|
||||
await expect.poll(() => replies).toEqual(["permission-api", "permission-web"])
|
||||
})
|
||||
|
||||
@@ -68,6 +68,7 @@ export const dict = {
|
||||
"command.prompt.mode.normal": "Prompt",
|
||||
"command.permissions.autoaccept.enable": "Auto-accept permissions",
|
||||
"command.permissions.autoaccept.disable": "Stop auto-accepting permissions",
|
||||
"command.permissions.autoaccept.project": "Auto-accept permissions for this project",
|
||||
"command.session.undo": "Undo",
|
||||
"command.session.undo.description": "Undo the last message",
|
||||
"command.session.redo": "Redo",
|
||||
@@ -397,6 +398,9 @@ export const dict = {
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Auto-accepting permissions",
|
||||
"toast.permissions.autoaccept.on.description": "Permission requests will be automatically approved",
|
||||
"toast.permissions.autoaccept.project.title": "Auto-accepting permissions for this project",
|
||||
"toast.permissions.autoaccept.project.description":
|
||||
"Permission requests in this project will be automatically approved",
|
||||
"toast.permissions.autoaccept.off.title": "Stopped auto-accepting permissions",
|
||||
"toast.permissions.autoaccept.off.description": "Permission requests will require approval",
|
||||
|
||||
@@ -720,6 +724,11 @@ export const dict = {
|
||||
"settings.extensions.manageConfig": "Manage in opencode.json",
|
||||
"settings.extensions.addSkills": "How to add skills",
|
||||
"project.settings.title": "Edit project",
|
||||
"project.settings.autoAccept.description": "Approve this project's permission requests without asking",
|
||||
"project.settings.autoAccept.default.on": "Use default (On)",
|
||||
"project.settings.autoAccept.default.off": "Use default (Off)",
|
||||
"project.settings.autoAccept.on": "On",
|
||||
"project.settings.autoAccept.off": "Off",
|
||||
"project.settings.name.title": "Project name",
|
||||
"project.settings.name.description": "The name shown for this project throughout OpenCode",
|
||||
"project.settings.icon.description": "Recommended: 128×128px. Click or drag to upload an image.",
|
||||
@@ -997,6 +1006,8 @@ export const dict = {
|
||||
"settings.workspaces.delete.blocked.active": "The active worktree cannot be deleted.",
|
||||
"settings.workspaces.delete.blocked.linked":
|
||||
"Linked sessions will remain, but their working directory will no longer exist.",
|
||||
"settings.permissions.autoAccept.default.description":
|
||||
"Approve permission requests without asking. Projects can override this in their settings.",
|
||||
"settings.workspaces.default.title": "Default environment",
|
||||
"settings.workspaces.default.description": "Choose where new sessions start",
|
||||
"settings.workspaces.default.lastUsed": "Last used per project",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useComposerState } from "@/composer/persistence"
|
||||
import { useServerSDK } from "@/runtime/server/client"
|
||||
import { useData } from "@/runtime/server/current"
|
||||
import { formatServerError } from "@/runtime/server/errors"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { projectSettingsKey, useSettings } from "@/settings/model"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import { fetchSessionExport, saveSessionExport, sessionExportFilename } from "@/session/commands/export"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
@@ -229,16 +229,38 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
)
|
||||
}
|
||||
|
||||
const sessionProject = () => projectSettingsKey(serverSDK.scope, actions.session.data.info()?.projectID)
|
||||
|
||||
const autoAccepting = () => settings.permissions.autoApproves(sessionProject())
|
||||
|
||||
// From a session the toggle sets this project's choice, falling back to the default when they match.
|
||||
const toggleAutoAccept = () => {
|
||||
const active = !settings.permissions.autoApprove()
|
||||
settings.permissions.setAutoApprove(active)
|
||||
const project = sessionProject()
|
||||
const active = !autoAccepting()
|
||||
|
||||
if (!project) settings.permissions.setAutoApproveDefault(active)
|
||||
else
|
||||
settings.permissions.setAutoApproveOverride(
|
||||
project,
|
||||
active === settings.permissions.autoApproveDefault() ? undefined : active,
|
||||
)
|
||||
|
||||
if (!active) {
|
||||
showToast({
|
||||
title: language.t("toast.permissions.autoaccept.off.title"),
|
||||
description: language.t("toast.permissions.autoaccept.off.description"),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
showToast({
|
||||
title: active
|
||||
? language.t("toast.permissions.autoaccept.on.title")
|
||||
: language.t("toast.permissions.autoaccept.off.title"),
|
||||
description: active
|
||||
? language.t("toast.permissions.autoaccept.on.description")
|
||||
: language.t("toast.permissions.autoaccept.off.description"),
|
||||
title: project
|
||||
? language.t("toast.permissions.autoaccept.project.title")
|
||||
: language.t("toast.permissions.autoaccept.on.title"),
|
||||
description: project
|
||||
? language.t("toast.permissions.autoaccept.project.description")
|
||||
: language.t("toast.permissions.autoaccept.on.description"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -429,9 +451,11 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const permissionsCmds = () => [
|
||||
permissionsCommand({
|
||||
id: "permissions.autoaccept",
|
||||
title: settings.permissions.autoApprove()
|
||||
title: autoAccepting()
|
||||
? language.t("command.permissions.autoaccept.disable")
|
||||
: language.t("command.permissions.autoaccept.enable"),
|
||||
: sessionProject()
|
||||
? language.t("command.permissions.autoaccept.project")
|
||||
: language.t("command.permissions.autoaccept.enable"),
|
||||
keybind: "mod+shift+a",
|
||||
disabled: false,
|
||||
onSelect: toggleAutoAccept,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createEffect, onCleanup } from "solid-js"
|
||||
import type { PermissionRequest } from "@opencode/client/promise"
|
||||
import type { Data } from "@opencode/client/solid"
|
||||
import type { ServerSDK } from "@/runtime/server/client"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { projectSettingsKey, useSettings } from "@/settings/model"
|
||||
|
||||
const respondedLimit = 1000
|
||||
|
||||
@@ -10,15 +10,19 @@ const retryLimit = 2
|
||||
|
||||
const retryDelayMs = 1000
|
||||
|
||||
// Auto-approves permission requests on one server connection whenever the
|
||||
// app-level auto-approve setting is on. The setting lives in the client-local
|
||||
// settings store, so it applies to every session, tab, and server at once.
|
||||
// Auto-approves permission requests on one server connection for the projects
|
||||
// the client-local settings cover: the app-wide default, unless a project overrides it.
|
||||
export function createPermissionAutoApprover(input: { sdk: ServerSDK; data: Data }) {
|
||||
const enabled = useSettings().permissions.autoApprove
|
||||
const permissions = useSettings().permissions
|
||||
const enabled = () => permissions.autoApprovesOnServer(input.sdk.scope)
|
||||
|
||||
const covers = (projectID: string | undefined) =>
|
||||
permissions.autoApproves(projectSettingsKey(input.sdk.scope, projectID))
|
||||
|
||||
const state = { disposed: false, generation: 0, responded: new Set<string>() }
|
||||
|
||||
const unsubscribe = input.sdk.event.on("permission.asked", (event) => {
|
||||
if (enabled()) approve(event.data)
|
||||
if (enabled()) consider(event.data)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
@@ -68,7 +72,7 @@ export function createPermissionAutoApprover(input: { sdk: ServerSDK; data: Data
|
||||
input.sdk.api.permission.request
|
||||
.list({ location: { directory: location.directory } })
|
||||
.then((pending) => {
|
||||
if (!state.disposed) pending.data.forEach((request) => approve(request))
|
||||
if (!state.disposed) pending.data.forEach((request) => consider(request))
|
||||
|
||||
return true
|
||||
})
|
||||
@@ -122,10 +126,24 @@ export function createPermissionAutoApprover(input: { sdk: ServerSDK; data: Data
|
||||
}
|
||||
}
|
||||
|
||||
function approve(permission: PermissionRequest, attempt = 0) {
|
||||
// enabled() guards the retry timer path: the user may disable the setting
|
||||
// A request from a session this client has not loaded needs that session's
|
||||
// project before a per-project choice can apply to it.
|
||||
function consider(permission: PermissionRequest) {
|
||||
if (covers(undefined) || input.data.session.get(permission.sessionID)) return approve(permission)
|
||||
void input.sdk.api.session
|
||||
.get({ sessionID: permission.sessionID })
|
||||
.then((session) => approve(permission, session.projectID))
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
function approve(
|
||||
permission: PermissionRequest,
|
||||
projectID = input.data.session.get(permission.sessionID)?.projectID,
|
||||
attempt = 0,
|
||||
) {
|
||||
// covers() guards the retry timer path: the user may disable the setting
|
||||
// between a failed reply and its scheduled retry.
|
||||
if (state.disposed || !enabled() || state.responded.has(permission.id)) return
|
||||
if (state.disposed || !covers(projectID) || state.responded.has(permission.id)) return
|
||||
remember(permission.id)
|
||||
input.sdk.api.permission
|
||||
.reply({ sessionID: permission.sessionID, requestID: permission.id, decision: "once" })
|
||||
@@ -136,7 +154,7 @@ export function createPermissionAutoApprover(input: { sdk: ServerSDK; data: Data
|
||||
state.responded.delete(permission.id)
|
||||
|
||||
if (state.disposed || attempt >= retryLimit) return
|
||||
setTimeout(() => approve(permission, attempt + 1), retryDelayMs * (attempt + 1))
|
||||
setTimeout(() => approve(permission, projectID, attempt + 1), retryDelayMs * (attempt + 1))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useParams } from "@solidjs/router"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import { useServerSDK } from "@/runtime/server/client"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { projectSettingsKey, useSettings } from "@/settings/model"
|
||||
import { useWorkspaceLocation } from "@/workspaces/location"
|
||||
import { sessionPermissionRequest, sessionFormRequest, sessionTreeIDs } from "@/session/requests/session-request-tree"
|
||||
import { createWebSearchRequest } from "./websearch"
|
||||
@@ -69,9 +69,13 @@ export function createSessionRequestModel() {
|
||||
})
|
||||
|
||||
const permissionRequest = createMemo((): PermissionRequest | undefined => {
|
||||
if (settings.permissions.autoApprove()) return undefined
|
||||
const id = params.id
|
||||
|
||||
return sessionPermissionRequest(data.session.list(), data.session.permission.list, params.id)
|
||||
const project = projectSettingsKey(serverSDK.scope, id ? data.session.get(id)?.projectID : undefined)
|
||||
|
||||
if (settings.permissions.autoApproves(project)) return
|
||||
|
||||
return sessionPermissionRequest(data.session.list(), data.session.permission.list, id)
|
||||
})
|
||||
|
||||
const blocked = createMemo(() => {
|
||||
|
||||
@@ -27,12 +27,12 @@ const AutoApprovePermissionsSetting: Component = () => {
|
||||
return (
|
||||
<SettingsRow
|
||||
title={language.t("command.permissions.autoaccept.enable")}
|
||||
description={language.t("toast.permissions.autoaccept.on.description")}
|
||||
description={language.t("settings.permissions.autoAccept.default.description")}
|
||||
>
|
||||
<div data-action="settings-auto-accept-permissions">
|
||||
<Switch
|
||||
checked={settings.permissions.autoApprove()}
|
||||
onChange={(checked) => settings.permissions.setAutoApprove(checked)}
|
||||
checked={settings.permissions.autoApproveDefault()}
|
||||
onChange={(checked) => settings.permissions.setAutoApproveDefault(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
@@ -105,7 +105,7 @@ describe("settings schema", () => {
|
||||
tabLayout: "horizontal",
|
||||
},
|
||||
keybinds: {},
|
||||
permissions: { autoApprove: false },
|
||||
permissions: { autoApprove: false, autoApproveProjects: {} },
|
||||
workspaces: { defaultDestination: "last-used", lastUsed: {} },
|
||||
notifications: { agent: true, permissions: true, errors: false },
|
||||
sounds: {
|
||||
@@ -129,7 +129,7 @@ describe("settings schema", () => {
|
||||
followUpBehavior: "invalid",
|
||||
},
|
||||
appearance: { fontSize: "large", mono: "Custom Mono", tabLayout: "vertical", showProjectName: true },
|
||||
permissions: { autoApprove: true },
|
||||
permissions: { autoApprove: true, autoApproveProjects: { on: true, off: false, bad: "yes" } },
|
||||
workspaces: { defaultDestination: "new", lastUsed: { good: "workspace", bad: true } },
|
||||
keybinds: { good: "ctrl+k", bad: 3 },
|
||||
notifications: { agent: false, permissions: "yes", errors: true },
|
||||
@@ -149,7 +149,7 @@ describe("settings schema", () => {
|
||||
terminal: "",
|
||||
tabLayout: "vertical",
|
||||
})
|
||||
expect(settings.permissions.autoApprove).toBe(true)
|
||||
expect(settings.permissions).toEqual({ autoApprove: true, autoApproveProjects: { on: true, off: false } })
|
||||
expect(settings.workspaces).toEqual({ defaultDestination: "new", lastUsed: { good: "workspace" } })
|
||||
expect(settings.keybinds).toEqual({ good: "ctrl+k" })
|
||||
expect(settings.notifications).toEqual({ agent: false, permissions: true, errors: true })
|
||||
|
||||
@@ -23,6 +23,11 @@ export type NotificationSettings = Settings["notifications"]
|
||||
|
||||
export type SoundSettings = Settings["sounds"]
|
||||
|
||||
// Names a project for per-project settings: project IDs are only unique within one server.
|
||||
export function projectSettingsKey(scope: ServerScope, projectID: string | undefined) {
|
||||
return projectID ? ScopedKey.from(scope, projectID) : undefined
|
||||
}
|
||||
|
||||
export const monoDefault = "IBM Plex Mono"
|
||||
|
||||
export const sansDefault = "Inter"
|
||||
@@ -127,7 +132,12 @@ const appearanceSchema = Persistence.struct({
|
||||
})
|
||||
|
||||
const permissionsSchema = Persistence.struct({
|
||||
// The default for every project.
|
||||
autoApprove: Schema.Boolean,
|
||||
// Projects that override the default, keyed by server scope and project ID. Absent projects use the default.
|
||||
autoApproveProjects: Persistence.record(
|
||||
Schema.Boolean.pipe(Schema.catchDecoding(() => Effect.succeed(Option.none()))),
|
||||
),
|
||||
})
|
||||
|
||||
const workspacesSchema = Persistence.struct({
|
||||
@@ -274,7 +284,7 @@ export const defaultSettings: Settings = {
|
||||
},
|
||||
appearance: { fontSize: 14, mono: "", sans: "", terminal: "", tabLayout: "horizontal" },
|
||||
keybinds: {},
|
||||
permissions: { autoApprove: false },
|
||||
permissions: { autoApprove: false, autoApproveProjects: {} },
|
||||
workspaces: { defaultDestination: "last-used", lastUsed: {} },
|
||||
notifications: { agent: true, permissions: true, errors: false },
|
||||
sounds: {
|
||||
@@ -314,6 +324,15 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
void document.fonts?.load(`440 13px ${mono}`).catch(() => undefined)
|
||||
})
|
||||
|
||||
const autoApproveDefault = withFallback(
|
||||
() => store.permissions?.autoApprove,
|
||||
defaultSettings.permissions.autoApprove,
|
||||
)
|
||||
|
||||
// A project's explicit choice, or undefined when it uses the default.
|
||||
const autoApproveOverride = (project: ScopedKey | undefined) =>
|
||||
project ? store.permissions?.autoApproveProjects?.[project] : undefined
|
||||
|
||||
return {
|
||||
ready,
|
||||
get current() {
|
||||
@@ -396,10 +415,30 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
autoApprove: withFallback(() => store.permissions?.autoApprove, defaultSettings.permissions.autoApprove),
|
||||
setAutoApprove(value: boolean) {
|
||||
autoApproveDefault,
|
||||
setAutoApproveDefault(value: boolean) {
|
||||
setStore("permissions", "autoApprove", value)
|
||||
},
|
||||
autoApproveOverride,
|
||||
// Undefined makes the project use the default again.
|
||||
setAutoApproveOverride(project: ScopedKey, value: boolean | undefined) {
|
||||
const projects = { ...store.permissions?.autoApproveProjects }
|
||||
|
||||
if (value === undefined) delete projects[project]
|
||||
else projects[project] = value
|
||||
setStore("permissions", "autoApproveProjects", reconcile(projects))
|
||||
},
|
||||
// Whether a project's requests are approved without asking.
|
||||
autoApproves: (project: ScopedKey | undefined) => autoApproveOverride(project) ?? autoApproveDefault(),
|
||||
// Whether any request on the server may be approved without asking.
|
||||
autoApprovesOnServer(scope: ServerScope) {
|
||||
if (autoApproveDefault()) return true
|
||||
const prefix = ScopedKey.prefix(scope)
|
||||
|
||||
return Object.entries(store.permissions?.autoApproveProjects ?? {}).some(
|
||||
([key, value]) => value && key.startsWith(prefix),
|
||||
)
|
||||
},
|
||||
},
|
||||
workspaces: {
|
||||
defaultDestination: withFallback(
|
||||
|
||||
@@ -39,7 +39,8 @@ export const clientSettings: Entry<SettingsRootTab>[] = [
|
||||
tab: "general",
|
||||
label: "command.permissions.autoaccept.enable",
|
||||
target: "settings-auto-accept-permissions",
|
||||
keywords: "permissions approve allow auto accept",
|
||||
description: "settings.permissions.autoAccept.default.description",
|
||||
keywords: "permissions approve allow auto accept default",
|
||||
},
|
||||
{
|
||||
tab: "general",
|
||||
@@ -187,6 +188,12 @@ export const projectSettings: Entry<SettingsProjectTab>[] = [
|
||||
{ tab: "general", label: "project.settings.name.title", target: "settings-project-name", keywords: "rename" },
|
||||
{ tab: "general", label: "dialog.project.edit.icon", target: "settings-project-icon" },
|
||||
{ tab: "general", label: "dialog.project.edit.color", target: "settings-project-color" },
|
||||
{
|
||||
tab: "general",
|
||||
label: "command.permissions.autoaccept.enable",
|
||||
target: "settings-project-auto-accept-permissions",
|
||||
keywords: "permissions approve allow auto accept",
|
||||
},
|
||||
{ tab: "workspaces", label: "settings.tab.workspaces", keywords: "workspaces disk usage cleanup delete" },
|
||||
{ tab: "extensions", label: "settings.tab.extensions" },
|
||||
{
|
||||
|
||||
@@ -5,13 +5,16 @@ import {
|
||||
ProjectAvatar,
|
||||
PROJECT_AVATAR_VARIANTS,
|
||||
} from "@opencode/ui/project-avatar"
|
||||
import { Select } from "@opencode/ui/select"
|
||||
import { Textarea } from "@opencode/ui/textarea"
|
||||
import { TextInput } from "@opencode/ui/text-input"
|
||||
import { For, Show, type Component } from "solid-js"
|
||||
import { For, Show, createMemo, type Component } from "solid-js"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import type { LocalProject } from "@/shell/state/layout"
|
||||
import { ServerConnection, serverName } from "@/runtime/server/registry"
|
||||
import { useSettingsServers } from "@/settings/servers/inventory"
|
||||
import { useServerCtx } from "@/runtime/server/runtime"
|
||||
import { projectSettingsKey, useSettings } from "@/settings/model"
|
||||
import { ProjectIcon } from "@/shell/layout/project-icon"
|
||||
import { ProjectOptions } from "./project-options"
|
||||
import { SettingsList } from "@/settings/list"
|
||||
@@ -156,6 +159,8 @@ export const SettingsProjectGeneral: Component<{
|
||||
</SettingsRow>
|
||||
</Show>
|
||||
|
||||
<AutoAcceptPermissionsSetting project={props.project} server={props.server} />
|
||||
|
||||
<div class="project-settings-startup">
|
||||
<div class="project-settings-startup-copy">
|
||||
<span class="project-settings-startup-title">{language.t("dialog.project.edit.worktree.startup")}</span>
|
||||
@@ -184,6 +189,52 @@ export const SettingsProjectGeneral: Component<{
|
||||
)
|
||||
}
|
||||
|
||||
// A project uses the app-wide default unless it is explicitly turned on or off here.
|
||||
const AutoAcceptPermissionsSetting: Component<{ project: LocalProject; server: ServerConnection.Any }> = (props) => {
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
const serverCtx = useServerCtx(() => props.server)
|
||||
|
||||
const key = createMemo(() => {
|
||||
const scope = serverCtx()?.sdk.scope
|
||||
|
||||
return scope ? projectSettingsKey(scope, props.project.id) : undefined
|
||||
})
|
||||
|
||||
const options = createMemo(() => [
|
||||
{
|
||||
value: undefined,
|
||||
label: settings.permissions.autoApproveDefault()
|
||||
? language.t("project.settings.autoAccept.default.on")
|
||||
: language.t("project.settings.autoAccept.default.off"),
|
||||
},
|
||||
{ value: true, label: language.t("project.settings.autoAccept.on") },
|
||||
{ value: false, label: language.t("project.settings.autoAccept.off") },
|
||||
])
|
||||
|
||||
return (
|
||||
<Show when={key()}>
|
||||
{(project) => (
|
||||
<SettingsRow
|
||||
title={language.t("command.permissions.autoaccept.enable")}
|
||||
description={language.t("project.settings.autoAccept.description")}
|
||||
>
|
||||
<Select
|
||||
data-action="settings-project-auto-accept-permissions"
|
||||
options={options()}
|
||||
current={options().find((option) => option.value === settings.permissions.autoApproveOverride(project()))}
|
||||
value={(option) => String(option.value)}
|
||||
label={(option) => option.label}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
onSelect={(option) => option && settings.permissions.setAutoApproveOverride(project(), option.value)}
|
||||
/>
|
||||
</SettingsRow>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function inlineVariables(text: string) {
|
||||
return text
|
||||
.split(/(\$[A-Z][A-Z0-9_]*)/g)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createMemo, type Accessor } from "solid-js"
|
||||
import { useGlobal, useServerCtx } from "@/runtime/server/runtime"
|
||||
import { sessionPermissionRequest, sessionFormRequest } from "@/session/requests/session-request-tree"
|
||||
import { ServerConnection } from "@/runtime/server/registry"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { projectSettingsKey, useSettings } from "@/settings/model"
|
||||
|
||||
export function useSessionTabAvatarState(
|
||||
server: Accessor<ServerConnection.Key>,
|
||||
@@ -30,11 +30,14 @@ export function useSessionTabAvatarState(
|
||||
})
|
||||
|
||||
const hasPermissions = createMemo(() => {
|
||||
if (settings.permissions.autoApprove()) return false
|
||||
const ctx = serverCtx()
|
||||
|
||||
if (!ctx) return false
|
||||
|
||||
const project = projectSettingsKey(ctx.sdk.scope, ctx.data.session.get(sessionId())?.projectID)
|
||||
|
||||
if (settings.permissions.autoApproves(project)) return false
|
||||
|
||||
return !!sessionPermissionRequest(sessions(), ctx.data.session.permission.list, sessionId())
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user