From 46d1f1fed15887822669e0e285da28c9874fe237 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:56:14 +1000 Subject: [PATCH] refactor(app): make auto-accept permissions an app-level setting (#44608) --- .../remote-session-settings.spec.ts | 164 ++++++++- packages/app/src/home/sessions/controller.tsx | 1 - .../app/src/new-session/composer-adapter.ts | 5 - packages/app/src/runtime/server/runtime.tsx | 5 +- .../session/commands/use-session-commands.tsx | 20 +- .../app/src/session/requests/auto-approve.ts | 136 +++++++ .../src/session/requests/auto-respond.test.ts | 129 ------- .../app/src/session/requests/auto-respond.ts | 79 ---- packages/app/src/session/requests/model.ts | 9 +- .../app/src/session/requests/permission.ts | 3 - .../session/requests/server-permission.tsx | 348 ------------------ packages/app/src/settings/command.tsx | 5 +- .../app/src/settings/general/controllers.ts | 36 +- packages/app/src/settings/general/general.tsx | 17 +- packages/app/src/settings/shell.tsx | 5 +- .../src/shell/layout/project-avatar-state.ts | 9 +- .../src/shell/layout/session-tab-avatar.tsx | 1 - packages/storybook/.storybook/main.ts | 1 - .../mocks/app/context/permission.ts | 27 -- 19 files changed, 315 insertions(+), 685 deletions(-) create mode 100644 packages/app/src/session/requests/auto-approve.ts delete mode 100644 packages/app/src/session/requests/auto-respond.test.ts delete mode 100644 packages/app/src/session/requests/auto-respond.ts delete mode 100644 packages/app/src/session/requests/permission.ts delete mode 100644 packages/app/src/session/requests/server-permission.tsx delete mode 100644 packages/storybook/.storybook/mocks/app/context/permission.ts diff --git a/packages/app/e2e/regression/remote-session-settings.spec.ts b/packages/app/e2e/regression/remote-session-settings.spec.ts index 251fd3f27eb..3ae2cedd50c 100644 --- a/packages/app/e2e/regression/remote-session-settings.spec.ts +++ b/packages/app/e2e/regression/remote-session-settings.spec.ts @@ -13,9 +13,14 @@ const sessionB = session("ses_server_b", directoryB, "Server B session") test("session settings use the remote server context", async ({ page }) => { const permissionRequests: string[] = [] + const permissionResponses: PermissionResponse[] = [] await installSseTransport(page, { server: serverA }) await installSseTransport(page, { server: serverB }) - await mockServers(page, permissionRequests) + // Server A has no tab and is never visited: a pending request there proves + // one toggle sweeps every connected server, not just the focused one. + await mockServers(page, permissionRequests, permissionResponses, { + pending: { [serverA]: [pendingPermission("permission-pending-a", sessionA.id)] }, + }) await configureServers(page) await page.goto(`/server/${base64Encode(serverB)}/session/${sessionB.id}`) @@ -38,7 +43,17 @@ test("session settings use the remote server context", async ({ page }) => { }), ) .toBe(true) - expect(permissionRequests.every((request) => new URL(request).origin === serverB)).toBe(true) + await expect + .poll(() => permissionResponses) + .toEqual([ + { + origin: serverA, + directory: undefined, + sessionID: sessionA.id, + permissionID: "permission-pending-a", + body: { reply: "once" }, + }, + ]) await dialog.getByRole("tab", { name: "Models" }).click() await expect(dialog.getByRole("switch", { name: "Server B Model" })).toBeEnabled() @@ -143,6 +158,99 @@ test("auto-accept responds for an unfocused server session", async ({ page }) => ]) }) +test("auto-accept sweeps again after a reconnect", async ({ page }) => { + const permissionRequests: string[] = [] + const permissionResponses: PermissionResponse[] = [] + const pendingA: MockPermission[] = [] + const listFailures: Record = {} + const sessionGets: string[] = [] + await installSseTransport(page, { server: serverB }) + const transport = await installSseTransport(page, { server: serverA, retry: 20 }) + await mockServers(page, permissionRequests, permissionResponses, { + pending: { [serverA]: pendingA }, + listFailures, + sessionGets, + }) + await configureServers(page, [{ type: "session", server: serverA, sessionId: sessionA.id }]) + + await page.goto(`/server/${base64Encode(serverA)}/session/${sessionA.id}`) + await expect(page.getByRole("heading", { name: sessionA.title, exact: true })).toBeVisible() + const first = await transport.waitForConnection() + + await page.keyboard.press("Control+,") + const autoAccept = page.locator(".settings-dialog").locator('[data-action="settings-auto-accept-permissions"]') + await autoAccept.locator('[data-slot="switch-control"]').click() + await expect(autoAccept.getByRole("switch")).toBeChecked() + await expect + .poll(() => + permissionRequests.some((request) => { + const url = new URL(request) + return url.origin === serverA && url.searchParams.get("location[directory]") === directoryA + }), + ) + .toBe(true) + await page.keyboard.press("Escape") + + // This request is asked while the client is disconnected, so it is never + // delivered as an event and only a reconnect sweep can find it. The first + // listing after the reconnect fails, so only the bounded sweep retry can + // deliver the reply. + pendingA.push(pendingPermission("permission-offline-a", sessionA.id)) + listFailures[serverA] = 1 + const syncsBeforeReconnect = sessionGets.length + await transport.disconnect() + await transport.waitForConnection({ after: first.id }) + + await expect + .poll(() => permissionResponses) + .toEqual([ + { + origin: serverA, + directory: undefined, + sessionID: sessionA.id, + permissionID: "permission-offline-a", + body: { reply: "once" }, + }, + ]) + // The reconnect sweep must resync active sessions instead of trusting + // cached locations, since another client may have moved them meanwhile. + expect(sessionGets.slice(syncsBeforeReconnect)).toContain(sessionA.id) +}) + +test("auto-accept approves a request discovered by opening a session", async ({ page }) => { + const permissionRequests: string[] = [] + const permissionResponses: PermissionResponse[] = [] + await installSseTransport(page, { server: serverA }) + await installSseTransport(page, { server: serverB }) + // The request is only served from the per-session permission list, so it + // reaches the client through the store sync when the session view opens, + // never through a location sweep or an event. + await mockServers(page, permissionRequests, permissionResponses, { + sessionPending: { [sessionA.id]: [pendingPermission("permission-synced-a", sessionA.id)] }, + }) + await configureServers(page, [{ type: "session", server: serverA, sessionId: sessionA.id }]) + + await page.goto(`/server/${base64Encode(serverA)}/session/${sessionA.id}`) + await expect(page.getByRole("heading", { name: sessionA.title, exact: true })).toBeVisible() + + await page.keyboard.press("Control+,") + const autoAccept = page.locator(".settings-dialog").locator('[data-action="settings-auto-accept-permissions"]') + await autoAccept.locator('[data-slot="switch-control"]').click() + await expect(autoAccept.getByRole("switch")).toBeChecked() + + await expect + .poll(() => permissionResponses) + .toEqual([ + { + origin: serverA, + directory: undefined, + sessionID: sessionA.id, + permissionID: "permission-synced-a", + body: { reply: "once" }, + }, + ]) +}) + type PermissionResponse = { origin: string directory?: string @@ -151,6 +259,19 @@ type PermissionResponse = { body: unknown } +type MockPermission = { + id: string + sessionID: string + action: string + resources: string[] + metadata: Record + save: unknown[] +} + +function pendingPermission(id: string, sessionID: string): MockPermission { + return { id, sessionID, action: "shell", resources: ["git status"], metadata: {}, save: [] } +} + async function configureServers(page: Page, tabs: { type: "session"; server: string; sessionId: string }[] = []) { await page.addInitScript( ({ serverB, tabs }) => { @@ -161,7 +282,23 @@ async function configureServers(page: Page, tabs: { type: "session"; server: str ) } -async function mockServers(page: Page, permissionRequests: string[], permissionResponses: PermissionResponse[] = []) { +type MockServerOptions = { + // Pending requests served from /api/permission/request, keyed by origin. + pending?: Record + // Pending requests served from /api/session/:id/permission, keyed by session ID. + sessionPending?: Record + // Counts of /api/permission/request calls to fail with a 500, keyed by origin. + listFailures?: Record + // Records /api/session/:id GETs so tests can assert session resyncs. + sessionGets?: string[] +} + +async function mockServers( + page: Page, + permissionRequests: string[], + permissionResponses: PermissionResponse[] = [], + options: MockServerOptions = {}, +) { await page.route("**/*", async (route) => { const url = new URL(route.request().url()) if (url.origin !== serverA && url.origin !== serverB) return route.fallback() @@ -178,8 +315,12 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR permissionID: response[2]!, body: route.request().postDataJSON(), }) - return json(route, true) + // The generated client requires exactly 204 for a successful reply. + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) } + const sessionPermission = url.pathname.match(/^\/api\/session\/([^/]+)\/permission$/) + if (route.request().method() === "GET" && sessionPermission) + return json(route, { data: options.sessionPending?.[sessionPermission[1]!] ?? [] }) if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500) if (url.pathname === "/api/provider") return json(route, { @@ -197,7 +338,12 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR if (url.pathname === "/api/agent") return json(route, { location: { directory }, data: [] }) if (url.pathname === "/api/permission/request") { permissionRequests.push(url.toString()) - return json(route, { location: { directory }, data: [] }) + const failures = options.listFailures?.[url.origin] ?? 0 + if (failures > 0) { + options.listFailures![url.origin] = failures - 1 + return json(route, { name: "Internal" }, 500) + } + return json(route, { location: { directory }, data: options.pending?.[url.origin] ?? [] }) } if (["/api/command", "/api/reference", "/api/question/request"].includes(url.pathname)) return json(route, { location: { directory }, data: [] }) @@ -219,9 +365,13 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR return json(route, { id: remote ? sessionB.projectID : "project-server-a", directory, canonical: directory }) if (url.pathname === "/api/session") return json(route, { data: sessions.map((session) => currentSession(session)), cursor: {} }) - if (url.pathname === "/api/session/active") return json(route, { data: {} }) + if (url.pathname === "/api/session/active") + return json(route, { data: Object.fromEntries(sessions.map((session) => [session.id, { type: "running" }])) }) const currentSessionInfo = sessions.find((session) => url.pathname === `/api/session/${session.id}`) - if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) }) + if (currentSessionInfo) { + options.sessionGets?.push(currentSessionInfo.id) + return json(route, { data: currentSession(currentSessionInfo) }) + } if (sessions.some((session) => url.pathname === `/api/session/${session.id}/message`)) return json(route, { data: [], cursor: {} }) if (sessions.some((session) => url.pathname === `/api/session/${session.id}/inbox`)) diff --git a/packages/app/src/home/sessions/controller.tsx b/packages/app/src/home/sessions/controller.tsx index 0772b97fc26..8d2497b017c 100644 --- a/packages/app/src/home/sessions/controller.tsx +++ b/packages/app/src/home/sessions/controller.tsx @@ -373,7 +373,6 @@ export function HomeSessionStatusController(props: { }) { const avatar = useSessionTabAvatarState( () => props.server, - () => props.record.session.location.directory, () => props.record.session.id, () => true, ) diff --git a/packages/app/src/new-session/composer-adapter.ts b/packages/app/src/new-session/composer-adapter.ts index 7b4f953a0f2..6dac74e77fe 100644 --- a/packages/app/src/new-session/composer-adapter.ts +++ b/packages/app/src/new-session/composer-adapter.ts @@ -8,7 +8,6 @@ import { createComposerControls, createComposerModelSelection } from "@/composer import { createComposerProjectControls } from "./project/controller" import { useLanguage } from "@/runtime/i18n/language" import { useLocal } from "@/providers/models/selection" -import { usePermission } from "@/session/requests/permission" import { useData, useServer } from "@/runtime/server/current" import { type ServerSDK, useServerSDK } from "@/runtime/server/client" import { useTabs } from "@/shell/tabs/tabs" @@ -30,7 +29,6 @@ export function createNewSessionComposerAdapter(props: { const data = useData() const server = useServer() const serverSDK = useServerSDK() - const permission = usePermission() const tabs = useTabs() const location = useWorkspaceLocation() const language = useLanguage() @@ -86,9 +84,6 @@ export function createNewSessionComposerAdapter(props: { ) const cleanupReady = startTransition(() => { tabs.updateDraft(props.draftID, { worktree: undefined }) - if (permission.isAutoAcceptingDirectory(projectDirectory)) { - permission.enableAutoAccept(created.id, sessionDirectory) - } local.session.promote(sessionDirectory, created.id, { agent: selection.agent, model: selection.model, diff --git a/packages/app/src/runtime/server/runtime.tsx b/packages/app/src/runtime/server/runtime.tsx index 34e7edf6635..152065e9357 100644 --- a/packages/app/src/runtime/server/runtime.tsx +++ b/packages/app/src/runtime/server/runtime.tsx @@ -8,7 +8,7 @@ import { createServerSdkContext } from "./client" import { createServerSyncContext } from "./sync" import { createData } from "@opencode-ai/client/solid" import type { ServerScope } from "@/runtime/server/scope" -import { createServerPermissionState } from "@/session/requests/server-permission" +import { createPermissionAutoApprover } from "@/session/requests/auto-approve" import { createServerNotificationState } from "@/shell/notifications/notification" import { Persist, persisted } from "@/runtime/persistence/storage" @@ -144,7 +144,7 @@ function createServerController( directory: "", }) const sync = createServerSyncContext(sdk, data) - const permission = createServerPermissionState({ sdk, sync, data }) + createPermissionAutoApprover({ sdk, data }) const notification = createServerNotificationState({ sdk, data, key: connKey }) function enrich(project: { worktree: string; expanded: boolean }) { @@ -187,7 +187,6 @@ function createServerController( list: projectsList, recentlyClosed: recentlyClosedList, }, - permission, notification, } } diff --git a/packages/app/src/session/commands/use-session-commands.tsx b/packages/app/src/session/commands/use-session-commands.tsx index 89f2a62ef4d..a2ab6c83bcb 100644 --- a/packages/app/src/session/commands/use-session-commands.tsx +++ b/packages/app/src/session/commands/use-session-commands.tsx @@ -4,9 +4,7 @@ import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-b import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/workspaces/files/model" import { useLanguage } from "@/runtime/i18n/language" import { useLayout } from "@/shell/state/layout" -import { usePermission } from "@/session/requests/permission" import { useComposerState } from "@/composer/persistence" -import { useWorkspaceLocation } from "@/workspaces/location" import { useServerSDK } from "@/runtime/server/client" import { useSettings } from "@/settings/model" import { useTerminal } from "@/session/terminal/context" @@ -48,9 +46,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const dialog = useDialog() const file = useFile() const language = useLanguage() - const permission = usePermission() const prompt = useComposerState() - const sdk = useWorkspaceLocation() const serverSDK = useServerSDK() const settings = useSettings() const terminal = useTerminal() @@ -99,11 +95,6 @@ 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 = actions.session.identity.params.id - if (sessionID) return permission.isAutoAccepting(sessionID, sdk().directory) - return permission.isAutoAcceptingDirectory(sdk().directory) - } const exportSession = async () => { const sessionID = actions.session.identity.params.id if (!sessionID) return @@ -223,13 +214,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => { } const toggleAutoAccept = () => { - const sessionID = actions.session.identity.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 = !settings.permissions.autoApprove() + settings.permissions.setAutoApprove(active) showToast({ title: active ? language.t("toast.permissions.autoaccept.on.title") @@ -454,7 +440,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const permissionsCmds = () => [ permissionsCommand({ id: "permissions.autoaccept", - title: isAutoAcceptActive() + title: settings.permissions.autoApprove() ? language.t("command.permissions.autoaccept.disable") : language.t("command.permissions.autoaccept.enable"), keybind: "mod+shift+a", diff --git a/packages/app/src/session/requests/auto-approve.ts b/packages/app/src/session/requests/auto-approve.ts new file mode 100644 index 00000000000..1b81bc9ca0a --- /dev/null +++ b/packages/app/src/session/requests/auto-approve.ts @@ -0,0 +1,136 @@ +import { createEffect, onCleanup } from "solid-js" +import type { PermissionRequest } from "@opencode-ai/client/promise" +import type { Data } from "@opencode-ai/client/solid" +import type { ServerSDK } from "@/runtime/server/client" +import { useSettings } from "@/settings/model" + +const respondedLimit = 1000 +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. +export function createPermissionAutoApprover(input: { sdk: ServerSDK; data: Data }) { + const enabled = useSettings().permissions.autoApprove + const state = { disposed: false, generation: 0, responded: new Set() } + + const unsubscribe = input.sdk.event.on("permission.asked", (event) => { + if (enabled()) approve(event.data) + }) + onCleanup(() => { + state.disposed = true + unsubscribe() + }) + + // The event stream does not replay requests asked while this client was + // disconnected, and requests may already be pending before the setting turns + // on, so sweep on every connect while the setting is on. + createEffect(() => { + if (!enabled() || input.sdk.connection.status() !== "connected") return + const generation = ++state.generation + void sweepWithRetry(generation, 0) + }) + + // Approves pending requests that reach the local store, which is how a + // previously unknown idle session's requests surface when its view opens + // and syncs them. Store changes cannot re-trigger the network sweep: it + // deliberately reads them after an await, outside Solid tracking. + createEffect(() => { + if (!enabled()) return + for (const session of input.data.session.list()) { + for (const request of input.data.session.permission.list(session.id) ?? []) approve(request) + } + }) + + // An incomplete sweep leaves pending requests hidden with no later trigger + // to recover them, so retry it a bounded number of times. A newer sweep + // supersedes scheduled retries. + async function sweepWithRetry(generation: number, attempt: number) { + const complete = await sweep() + if (complete || attempt >= retryLimit) return + setTimeout(() => { + if (state.disposed || !enabled() || generation !== state.generation) return + void sweepWithRetry(generation, attempt + 1) + }, retryDelayMs * (attempt + 1)) + } + + async function sweep() { + const inventory = await sweepLocations() + const listed = await Promise.all( + inventory.locations.map((location) => + input.sdk.api.permission.request + .list({ location: { directory: location.directory, workspace: location.workspaceID } }) + .then((pending) => { + if (!state.disposed) pending.data.forEach((request) => approve(request)) + return true + }) + .catch(() => false), + ), + ) + return inventory.complete && listed.every(Boolean) + } + + // Active sessions are the primary inventory: session.active is server-wide, + // so it covers sessions no tab has loaded, and a request blocking a tool + // call always belongs to one (Permission.assert clears its entry when the + // awaiting fiber dies). Locally known sessions are swept too because the + // external session.permission.create API can park a request on an idle + // session. A detached request on a session this client never loaded is the + // one case that stays uncovered. + async function sweepLocations() { + const active = await input.sdk.api.session.active().catch(() => undefined) + const ids = Object.keys(active ?? {}) + // Resync every active session rather than trusting cached info: another + // client may have moved one while this client was disconnected, and the + // cached location would list permissions from the old location. A failed + // resync falls back to the cached location and marks the sweep incomplete. + const synced = await Promise.all( + ids.map((id) => { + input.data.session.invalidate(id) + return input.data.session.sync(id).then( + () => true, + () => false, + ) + }), + ) + const locations = [ + ...ids.flatMap((id) => { + const location = input.data.session.get(id)?.location + return location ? [location] : [] + }), + ...input.data.session.list().map((session) => session.location), + ] + return { + locations: [ + ...new Map(locations.map((item) => [`${item.directory}\u0000${item.workspaceID ?? ""}`, item])).values(), + ], + complete: active !== undefined && synced.every(Boolean), + } + } + + function approve(permission: PermissionRequest, attempt = 0) { + // enabled() 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 + remember(permission.id) + input.sdk.api.permission + .reply({ sessionID: permission.sessionID, requestID: permission.id, reply: "once" }) + .catch(() => { + // A reply failure leaves the request pending but invisible (the UI + // hides prompts while auto-approve is on), so retry a bounded number + // of times. Later sweeps retry it after that. + state.responded.delete(permission.id) + if (state.disposed || attempt >= retryLimit) return + setTimeout(() => approve(permission, attempt + 1), retryDelayMs * (attempt + 1)) + }) + } + + function remember(id: string) { + state.responded.add(id) + for (const oldest of state.responded) { + if (state.responded.size <= respondedLimit) break + state.responded.delete(oldest) + } + } +} diff --git a/packages/app/src/session/requests/auto-respond.test.ts b/packages/app/src/session/requests/auto-respond.test.ts deleted file mode 100644 index 55e26a4e0bd..00000000000 --- a/packages/app/src/session/requests/auto-respond.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { PermissionRequest, SessionInfo } from "@opencode-ai/client/promise" -import { base64Encode } from "@opencode-ai/util/encode" -import { autoRespondsPermission, isDirectoryAutoAccepting, relocateAutoAccept, sessionAutoAccept } from "./auto-respond" - -const session = (input: { id: string; parentID?: string }) => - ({ - id: input.id, - parentID: input.parentID, - }) as SessionInfo - -const permission = (sessionID: string) => - ({ - sessionID, - }) as Pick - -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("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) - expect(sessionAutoAccept(autoAccept, sessions, permission("root"), directory)).toBeUndefined() - }) - - 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) - }) - - test("parent false override takes precedence over directory-level auto-accept", () => { - const directory = "/tmp/project" - const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] - const autoAccept = { - [`${base64Encode(directory)}/*`]: true, - [`${base64Encode(directory)}/root`]: false, - } - - expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(false) - }) - - test("parent true override takes precedence over disabled directory fallback", () => { - const directory = "/tmp/project" - const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] - const autoAccept = { - [`${base64Encode(directory)}/*`]: false, - [`${base64Encode(directory)}/root`]: true, - } - - expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true) - }) -}) - -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) - }) -}) - -test("relocates bare session settings when the directory becomes known", () => { - const directory = "/tmp/project" - expect(relocateAutoAccept({ root: true }, [{ id: "root" }], directory)).toEqual({ - [`${base64Encode(directory)}/root`]: true, - }) - expect( - relocateAutoAccept({ root: true, [`${base64Encode(directory)}/root`]: false }, [{ id: "root" }], directory), - ).toEqual({ [`${base64Encode(directory)}/root`]: false }) -}) diff --git a/packages/app/src/session/requests/auto-respond.ts b/packages/app/src/session/requests/auto-respond.ts deleted file mode 100644 index 15192dbd1c0..00000000000 --- a/packages/app/src/session/requests/auto-respond.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { base64Encode } from "@opencode-ai/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, sessionID: string, directory?: string) { - return autoAccept[acceptKey(sessionID, directory)] -} - -export function isDirectoryAutoAccepting(autoAccept: Record, directory: string) { - const key = directoryAcceptKey(directory) - return autoAccept[key] ?? false -} - -export function relocateAutoAccept( - autoAccept: Record, - sessions: readonly { id: string }[], - directory: string, -) { - const moves = sessions.flatMap((session) => { - const value = autoAccept[session.id] - if (value === undefined) return [] - return [{ source: session.id, target: acceptKey(session.id, directory), value }] - }) - if (moves.length === 0) return autoAccept - - const next = { ...autoAccept } - for (const move of moves) { - if (next[move.target] === undefined) next[move.target] = move.value - delete next[move.source] - } - return next -} - -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()) - 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, - session: { id: string; parentID?: string }[], - permission: { sessionID: string }, - directory?: string, -) { - const value = sessionAutoAccept(autoAccept, session, permission, directory) - if (value !== undefined) return value - return directory ? isDirectoryAutoAccepting(autoAccept, directory) : false -} - -export function sessionAutoAccept( - autoAccept: Record, - session: { id: string; parentID?: string }[], - permission: { sessionID: string }, - directory?: string, -) { - return sessionLineage(session, permission.sessionID) - .map((id) => accepted(autoAccept, id, directory)) - .find((item): item is boolean => item !== undefined) -} diff --git a/packages/app/src/session/requests/model.ts b/packages/app/src/session/requests/model.ts index 8356fd27db7..2f618ee18b5 100644 --- a/packages/app/src/session/requests/model.ts +++ b/packages/app/src/session/requests/model.ts @@ -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 { usePermission } from "@/session/requests/permission" +import { useSettings } from "@/settings/model" import { useWorkspaceLocation } from "@/workspaces/location" import { sessionPermissionRequest, sessionQuestionForm } from "@/session/requests/session-request-tree" import { useData } from "@/runtime/server/current" @@ -16,7 +16,7 @@ export function createSessionRequestModel() { const serverSDK = useServerSDK() const data = useData() const language = useLanguage() - const permission = usePermission() + const settings = useSettings() createEffect(() => { const id = params.id if (!id || serverSDK.connection.status() !== "connected") return @@ -32,9 +32,8 @@ export function createSessionRequestModel() { }) const permissionRequest = createMemo((): PermissionRequest | undefined => { - return sessionPermissionRequest(data.session.list(), data.session.permission.list, params.id, (item) => { - return !permission.autoResponds(item, sdk().directory) - }) + if (settings.permissions.autoApprove()) return undefined + return sessionPermissionRequest(data.session.list(), data.session.permission.list, params.id) }) const blocked = createMemo(() => { diff --git a/packages/app/src/session/requests/permission.ts b/packages/app/src/session/requests/permission.ts deleted file mode 100644 index df9a831a421..00000000000 --- a/packages/app/src/session/requests/permission.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { useServer } from "@/runtime/server/current" - -export const usePermission = () => useServer().ctx.permission diff --git a/packages/app/src/session/requests/server-permission.tsx b/packages/app/src/session/requests/server-permission.tsx deleted file mode 100644 index 8a5ebfdc4a4..00000000000 --- a/packages/app/src/session/requests/server-permission.tsx +++ /dev/null @@ -1,348 +0,0 @@ -import { createEffect, createMemo, createRoot, getOwner, onCleanup } from "solid-js" -import { createStore, produce, reconcile } from "solid-js/store" -import { createSimpleContext } from "@opencode-ai/ui/context" -import type { PermissionRequest } from "@opencode-ai/client/promise" -import { Persist, persisted } from "@/runtime/persistence/storage" -import type { ServerSDK } from "@/runtime/server/client" -import type { ServerSync } from "@/runtime/server/sync" -import type { Data } from "@opencode-ai/client/solid" -import { useParams, useSearchParams } from "@solidjs/router" -import { decode64 } from "@/runtime/persistence/base64" -import { useGlobal } from "@/runtime/server/runtime" -import { ServerConnection, useServers } from "@/runtime/server/registry" -import { type DraftTab, useTabs } from "@/shell/tabs/tabs" -import { useSettings } from "@/settings/model" -import { requireServerKey } from "@/shell/routes/session" -import { ServerScope } from "@/runtime/server/scope" -import { - acceptKey, - directoryAcceptKey, - isDirectoryAutoAccepting, - autoRespondsPermission, - relocateAutoAccept, - sessionAutoAccept, -} from "./auto-respond" - -type PermissionRespondFn = (input: { - sessionID: string - permissionID: string - response: "once" | "always" | "reject" - directory?: string -}) => void - -function isNonAllowRule(rule: unknown) { - if (!rule) return false - if (typeof rule === "string") return rule !== "allow" - if (typeof rule !== "object") return false - if (Array.isArray(rule)) return false - - for (const action of Object.values(rule)) { - if (action !== "allow") return true - } - - return false -} - -function hasPermissionPromptRules(permission: unknown) { - if (!permission) return false - if (typeof permission === "string") return permission !== "allow" - if (typeof permission !== "object") return false - if (Array.isArray(permission)) return false - - const config = permission as Record - return Object.values(config).some(isNonAllowRule) -} - -export function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync; data: Data }) { - const [store, setStore, _, ready] = persisted( - { - ...Persist.serverGlobal(input.sdk.scope, "permission"), - ...(input.sdk.scope === ServerScope.local ? { previousKey: "permission.v3" } : {}), - migrate(value) { - if (!value || typeof value !== "object" || Array.isArray(value)) return value - - const data = value as Record - if (data.autoAccept) return value - - return { - ...data, - autoAccept: - typeof data.autoAcceptEdits === "object" && data.autoAcceptEdits && !Array.isArray(data.autoAcceptEdits) - ? data.autoAcceptEdits - : {}, - } - }, - }, - createStore({ - autoAccept: {} as Record, - }), - ) - - function enableConfiguredDirectory(directory: string) { - if (meta.disposed || !ready()) return - const [childStore] = input.sync.child(directory) - if (childStore.config.permission !== "allow") return - const key = directoryAcceptKey(directory) - if (store.autoAccept[key] !== undefined) return - setStore( - produce((draft) => { - draft.autoAccept[key] = true - }), - ) - } - - const MAX_RESPONDED = 1000 - const RESPONDED_TTL_MS = 60 * 60 * 1000 - const responded = new Map() - const enableVersion = new Map() - const meta = { disposed: false } - - 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 = (request) => { - if (meta.disposed) return - input.sdk.api.permission - .reply({ - sessionID: request.sessionID, - requestID: request.permissionID, - reply: request.response, - }) - .catch(() => { - responded.delete(request.permissionID) - }) - } - - const list = async (directory: string) => { - return input.sdk.api.permission.request.list({ location: { directory } }).then((result) => result.data) - } - - 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", - directory, - }) - } - - function sessions(_directory?: string) { - return input.data.session.list() - } - - function autoAccept(directory?: string) { - if (!directory) return store.autoAccept - const next = relocateAutoAccept(store.autoAccept, sessions(directory), directory) - if (next !== store.autoAccept) setStore("autoAccept", reconcile(next)) - return next - } - - function isAutoAccepting(sessionID: string, directory?: string) { - return autoRespondsPermission(autoAccept(directory), sessions(directory), { sessionID }, directory) - } - - function isAutoAcceptingDirectory(directory: string) { - return isDirectoryAutoAccepting(store.autoAccept, directory) - } - - function shouldAutoRespond(permission: PermissionRequest, directory?: string) { - return autoRespondsPermission(autoAccept(directory), sessions(directory), permission, directory) - } - - function isPending(permission: PermissionRequest) { - const pending = input.data.session.permission.list(permission.sessionID) - return pending === undefined || pending.some((item) => item.id === permission.id) - } - - async function shouldAutoRespondResolved(permission: PermissionRequest, directory?: string) { - const override = sessionAutoAccept(autoAccept(directory), sessions(directory), permission, directory) - if (override !== undefined) return override - const loaded = new Set() - while (!loaded.has(input.data.session.root(permission.sessionID))) { - const root = input.data.session.root(permission.sessionID) - loaded.add(root) - if (input.data.session.get(root)) break - await input.data.session.sync(root).catch(() => undefined) - } - if (meta.disposed || !input.data.session.get(permission.sessionID)) return false - return shouldAutoRespond(permission, directory) - } - - async function respondPending( - permission: PermissionRequest, - directory?: string, - current: () => boolean = () => true, - ) { - if (!current() || !isPending(permission)) return - if (!(await shouldAutoRespondResolved(permission, directory))) return - if (meta.disposed || !current() || !isPending(permission)) return - respondOnce(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 = input.sdk.event.on("permission.asked", (event) => { - if (ready()) { - void respondPending(event.data, event.location?.directory) - return - } - void ready.promise?.then(() => { - if (meta.disposed) return - void respondPending(event.data, event.location?.directory) - }) - }) - onCleanup(() => { - meta.disposed = true - unsubscribe() - }) - - function enableDirectory(directory: string) { - if (meta.disposed) return - const key = directoryAcceptKey(directory) - setStore( - produce((draft) => { - draft.autoAccept[key] = true - }), - ) - - list(directory) - .then((permissions) => { - if (meta.disposed) return - if (!isAutoAcceptingDirectory(directory)) return - for (const permission of permissions) { - void respondPending(permission, directory, () => isAutoAcceptingDirectory(directory)) - } - }) - .catch(() => undefined) - } - - function disableDirectory(directory: string) { - if (meta.disposed) return - const key = directoryAcceptKey(directory) - setStore( - produce((draft) => { - draft.autoAccept[key] = false - }), - ) - } - - function enable(sessionID: string, directory: string) { - if (meta.disposed) return - const key = acceptKey(sessionID, directory) - const version = bumpEnableVersion(sessionID, directory) - setStore( - produce((draft) => { - draft.autoAccept[key] = true - delete draft.autoAccept[sessionID] - }), - ) - - list(directory) - .then((permissions) => { - if (meta.disposed) return - if (enableVersion.get(key) !== version) return - if (!isAutoAccepting(sessionID, directory)) return - for (const permission of permissions) { - void respondPending( - permission, - directory, - () => enableVersion.get(key) === version && isAutoAccepting(sessionID, directory), - ) - } - }) - .catch(() => undefined) - } - - function disable(sessionID: string, directory?: string) { - if (meta.disposed) return - bumpEnableVersion(sessionID, directory) - const key = directory ? acceptKey(sessionID, directory) : sessionID - setStore( - produce((draft) => { - draft.autoAccept[key] = false - if (!directory) return - delete draft.autoAccept[sessionID] - }), - ) - } - - const api = { - ready: () => !meta.disposed && ready(), - respond, - autoResponds(permission: PermissionRequest, directory?: string) { - if (meta.disposed) return false - return shouldAutoRespond(permission, directory) - }, - isAutoAccepting(sessionID: string, directory?: string) { - if (meta.disposed) return false - return isAutoAccepting(sessionID, directory) - }, - isAutoAcceptingDirectory(directory: string) { - if (meta.disposed) return false - return isAutoAcceptingDirectory(directory) - }, - toggleAutoAccept(sessionID: string, directory: string) { - if (meta.disposed) return - if (isAutoAccepting(sessionID, directory)) { - disable(sessionID, directory) - return - } - - enable(sessionID, directory) - }, - toggleAutoAcceptDirectory(directory: string) { - if (meta.disposed) return - if (isAutoAcceptingDirectory(directory)) { - disableDirectory(directory) - return - } - enableDirectory(directory) - }, - enableAutoAccept(sessionID: string, directory: string) { - if (meta.disposed) return - if (isAutoAccepting(sessionID, directory)) return - enable(sessionID, directory) - }, - disableAutoAccept(sessionID: string, directory?: string) { - if (meta.disposed) return - disable(sessionID, directory) - }, - isPermissionAllowAll(directory: string) { - if (meta.disposed) return false - const [childStore] = input.sync.child(directory) - return childStore.config.permission === "allow" - }, - } - - return { - ...api, - api, - sync: input.sync, - enableConfiguredDirectory, - permissionsEnabled(directory: string) { - if (meta.disposed) return false - const [childStore] = input.sync.child(directory) - return hasPermissionPromptRules(childStore.config.permission) - }, - } -} diff --git a/packages/app/src/settings/command.tsx b/packages/app/src/settings/command.tsx index 7ffad2ff917..0869821c23c 100644 --- a/packages/app/src/settings/command.tsx +++ b/packages/app/src/settings/command.tsx @@ -1,4 +1,3 @@ -import { useParams } from "@solidjs/router" import { onCleanup } from "solid-js" import { useCommand } from "@/shell/commands/command" import { useLanguage } from "@/runtime/i18n/language" @@ -6,7 +5,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" export function useSettingsDialog(defaultValue?: string) { const dialog = useDialog() - const params = useParams<{ id?: string }>() let run = 0 let dead = false @@ -16,10 +14,9 @@ export function useSettingsDialog(defaultValue?: string) { return () => { const current = ++run - const sessionID = params.id void import("@/settings/shell").then((module) => { if (dead || run !== current) return - void dialog.show(() => ) + void dialog.show(() => ) }) } } diff --git a/packages/app/src/settings/general/controllers.ts b/packages/app/src/settings/general/controllers.ts index b2c2987a9c7..c67e3ce8d47 100644 --- a/packages/app/src/settings/general/controllers.ts +++ b/packages/app/src/settings/general/controllers.ts @@ -1,7 +1,6 @@ import { createMemo, createResource, onMount, type Accessor } from "solid-js" import type { ColorScheme } from "@opencode-ai/ui/theme/context" import { useTheme } from "@opencode-ai/ui/theme/context" -import { usePermission } from "@/session/requests/permission" import { monoDefault, monoFontFamily, @@ -17,43 +16,11 @@ import { import { playSoundById, SOUND_OPTIONS } from "@/shell/notifications/sound" import { createSoundPreviewController, type ShellOption } from "./behavior" import { ServerConnection } from "@/runtime/server/registry" -import { useGlobal, useServerCtx } from "@/runtime/server/runtime" +import { useServerCtx } from "@/runtime/server/runtime" export { createShellOptions, createSoundPreviewController } from "./behavior" export type { ShellOption, ShellSelectOption } from "./behavior" -export function createPermissionScopeController( - server: Accessor, - sessionID: Accessor, -) { - const serverCtx = useServerCtx(server) - const permission = () => serverCtx()?.permission - - const directory = createMemo(() => { - const s = server() - const id = sessionID() - if (!s || !id) return undefined - return serverCtx()?.data.session.get(id)?.location.directory - }) - - return { - accepting: createMemo(() => { - const id = sessionID() - const dir = directory() - if (!id || !dir) return false - return permission()?.isAutoAccepting(id, dir) - }), - enabled: createMemo(() => !!directory()), - set: (checked: boolean) => { - const id = sessionID() - const dir = directory() - if (!id || !dir) return - if (checked) return permission()?.enableAutoAccept(id, dir) - permission()?.disableAutoAccept(id, dir) - }, - } -} - export function createShellSettingsController(server: Accessor) { const serverCtx = useServerCtx(server) const [shells] = createResource( @@ -172,7 +139,6 @@ export function createSoundSettingsController() { } } -export type PermissionScopeController = ReturnType export type ShellSettingsController = ReturnType export type AppearanceSettingsController = ReturnType export type SoundSettingsController = ReturnType diff --git a/packages/app/src/settings/general/general.tsx b/packages/app/src/settings/general/general.tsx index 6b664fa7da0..d14c14622e0 100644 --- a/packages/app/src/settings/general/general.tsx +++ b/packages/app/src/settings/general/general.tsx @@ -13,11 +13,9 @@ import { SettingsList } from "@/settings/list" import { SettingsRow } from "@/settings/row" import { createAppearanceSettingsController, - createPermissionScopeController, createShellOptions, createShellSettingsController, type AppearanceSettingsController, - type PermissionScopeController, type ShellSettingsController, } from "./controllers" import "@/settings/settings.css" @@ -47,8 +45,9 @@ const fontSettings = { input: "setTerminal", }, } as const -const PermissionScopeSetting: Component<{ controller: PermissionScopeController }> = (props) => { +const AutoApprovePermissionsSetting: Component = () => { const language = useLanguage() + const settings = useSettings() return (
settings.permissions.setAutoApprove(checked)} />
@@ -262,7 +260,6 @@ const LanguageSetting = () => { } export const SettingsGeneral: Component<{ - sessionID?: string server?: ServerConnection.Any }> = (props) => { const language = useLanguage() @@ -270,10 +267,6 @@ export const SettingsGeneral: Component<{ const settings = useSettings() const mobile = createMediaQuery("(max-width: 767px)") const updater = useUpdaterAction() - const permissionScope = createPermissionScopeController( - () => props.server, - () => props.sessionID, - ) const shell = createShellSettingsController(() => props.server) const desktop = createMemo(() => platform.platform === "desktop") @@ -297,7 +290,7 @@ export const SettingsGeneral: Component<{ - + diff --git a/packages/app/src/settings/shell.tsx b/packages/app/src/settings/shell.tsx index 17b153ab241..4f1debbb11a 100644 --- a/packages/app/src/settings/shell.tsx +++ b/packages/app/src/settings/shell.tsx @@ -23,7 +23,6 @@ import { ServerConnection, useServers } from "@/runtime/server/registry" import "@/settings/settings.css" export const DialogSettings: Component<{ - sessionID?: string defaultValue?: string }> = (props) => { const language = useLanguage() @@ -68,7 +67,7 @@ export const DialogSettings: Component<{ }) const showProviders = () => { - void dialog.show(() => ) + void dialog.show(() => ) } return ( @@ -144,7 +143,7 @@ export const DialogSettings: Component<{ - + diff --git a/packages/app/src/shell/layout/project-avatar-state.ts b/packages/app/src/shell/layout/project-avatar-state.ts index 6fc5fd27379..7df71d1db5a 100644 --- a/packages/app/src/shell/layout/project-avatar-state.ts +++ b/packages/app/src/shell/layout/project-avatar-state.ts @@ -2,14 +2,15 @@ import { createMemo, type Accessor } from "solid-js" import { useGlobal, useServerCtx } from "@/runtime/server/runtime" import { sessionPermissionRequest, sessionQuestionForm } from "@/session/requests/session-request-tree" import { ServerConnection } from "@/runtime/server/registry" +import { useSettings } from "@/settings/model" export function useSessionTabAvatarState( server: Accessor, - directory: Accessor, sessionId: Accessor, root?: Accessor, ) { const global = useGlobal() + const settings = useSettings() const connection = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === server())) const serverCtx = useServerCtx(connection) const sessions = createMemo(() => { @@ -23,12 +24,10 @@ export function useSessionTabAvatarState( }) }) const hasPermissions = createMemo(() => { + if (settings.permissions.autoApprove()) return false const ctx = serverCtx() if (!ctx) return false - const permission = ctx.permission - return !!sessionPermissionRequest(sessions(), ctx.data.session.permission.list, sessionId(), (item) => { - return !permission.autoResponds(item, directory()) - }) + return !!sessionPermissionRequest(sessions(), ctx.data.session.permission.list, sessionId()) }) const hasQuestions = createMemo(() => { const data = serverCtx()?.data diff --git a/packages/app/src/shell/layout/session-tab-avatar.tsx b/packages/app/src/shell/layout/session-tab-avatar.tsx index 86cc35f9f99..b196a0e2862 100644 --- a/packages/app/src/shell/layout/session-tab-avatar.tsx +++ b/packages/app/src/shell/layout/session-tab-avatar.tsx @@ -15,7 +15,6 @@ export function SessionTabAvatar(props: { }) { const state = useSessionTabAvatarState( () => props.server, - () => props.directory, () => props.sessionId, ) return ( diff --git a/packages/storybook/.storybook/main.ts b/packages/storybook/.storybook/main.ts index 9aa5099b41d..5d5daa7a014 100644 --- a/packages/storybook/.storybook/main.ts +++ b/packages/storybook/.storybook/main.ts @@ -42,7 +42,6 @@ export default defineMain({ { find: /^@\/workspaces\/location$/, replacement: path.resolve(mocks, "app/context/location.ts") }, { find: /^@\/composer\/comments$/, replacement: path.resolve(mocks, "app/context/comments.ts") }, { find: /^@\/shell\/commands\/command$/, replacement: path.resolve(mocks, "app/context/command.ts") }, - { find: /^@\/session\/requests\/permission$/, replacement: path.resolve(mocks, "app/context/permission.ts") }, { find: /^@\/runtime\/platform\/platform$/, replacement: path.resolve(mocks, "app/context/platform.ts") }, { find: /^@\/runtime\/server\/global-sync$/, replacement: path.resolve(mocks, "app/context/global-sync.ts") }, { find: /^@\/runtime\/server\/sync$/, replacement: path.resolve(mocks, "app/context/server-sync.ts") }, diff --git a/packages/storybook/.storybook/mocks/app/context/permission.ts b/packages/storybook/.storybook/mocks/app/context/permission.ts deleted file mode 100644 index bfae134a7fd..00000000000 --- a/packages/storybook/.storybook/mocks/app/context/permission.ts +++ /dev/null @@ -1,27 +0,0 @@ -const accepted = new Set() - -function key(sessionID: string, directory?: string) { - return `${directory ?? ""}:${sessionID}` -} - -export function usePermission() { - return { - autoResponds() { - return false - }, - isAutoAccepting(sessionID: string, directory?: string) { - return accepted.has(key(sessionID, directory)) - }, - isAutoAcceptingDirectory() { - return false - }, - toggleAutoAccept(sessionID: string, directory?: string) { - const next = key(sessionID, directory) - if (accepted.has(next)) { - accepted.delete(next) - return - } - accepted.add(next) - }, - } -}