Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 64 additions & 4 deletions packages/app/e2e/regression/remote-session-settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,34 @@ const sessionA = session("ses_server_a", directoryA, "Server A session")
const childSessionA = { ...session("ses_server_a_child", directoryA, "Server A child session"), parentID: sessionA.id }
const sessionB = session("ses_server_b", directoryB, "Server B session")

test("session settings use the remote server context", async ({ page }) => {
test("auto-accept setting works without a session", async ({ page }) => {
const permissionRequests: string[] = []
await mockServers(page, permissionRequests)
await configureServers(page)

await page.goto("/")
await page.keyboard.press("Control+,")

const input = page
.locator(".settings-v2-dialog")
.locator('[data-action="settings-auto-accept-permissions"]')
.getByRole("switch")
await expect(input).toBeEnabled()
await input.click()
await expect(input).toBeChecked()
})

test("session settings use the remote server context", async ({ page }) => {
const permissionRequests: string[] = []
const permissionResponses: PermissionResponse[] = []
const pendingB: MockPermission[] = []
const permissionID = "permission-pending-b"
await mockServers(page, permissionRequests, permissionResponses, {
pending: { [serverB]: pendingB },
replyFailures: { [permissionID]: 1 },
})
await configureServers(page)

await page.goto(`/server/${base64Encode(serverB)}/session/${sessionB.id}`)
await expect(page.getByText(sessionB.title).first()).toBeVisible()
await page.keyboard.press("Control+,")
Expand All @@ -26,6 +49,7 @@ test("session settings use the remote server context", async ({ page }) => {
await expect(autoAccept).toBeVisible()
await expect(input).toBeEnabled()
permissionRequests.length = 0
pendingB.push(pendingPermission(permissionID, sessionB.id))
await autoAccept.locator('[data-slot="switch-control"]').click()
await expect(input).toBeChecked()
await expect
Expand All @@ -36,7 +60,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: serverB,
directory: directoryB,
sessionID: sessionB.id,
permissionID,
body: { response: "once" },
},
])

await dialog.getByRole("tab", { name: "Models" }).click()
await expect(dialog.getByRole("switch", { name: "Server B Model" })).toBeEnabled()
Expand Down Expand Up @@ -150,6 +184,19 @@ type PermissionResponse = {
body: unknown
}

type MockPermission = {
id: string
sessionID: string
permission: string
patterns: string[]
metadata: Record<string, unknown>
always: string[]
}

function pendingPermission(id: string, sessionID: string): MockPermission {
return { id, sessionID, permission: "bash", patterns: ["git status"], metadata: {}, always: [] }
}

async function configureServers(page: Page, tabs: { type: "session"; server: string; sessionId: string }[] = []) {
await page.addInitScript(
({ serverB, tabs }) => {
Expand All @@ -161,7 +208,15 @@ async function configureServers(page: Page, tabs: { type: "session"; server: str
)
}

async function mockServers(page: Page, permissionRequests: string[], permissionResponses: PermissionResponse[] = []) {
async function mockServers(
page: Page,
permissionRequests: string[],
permissionResponses: PermissionResponse[] = [],
options: {
pending?: Record<string, MockPermission[]>
replyFailures?: Record<string, number>
} = {},
) {
await page.route("**/*", async (route) => {
const url = new URL(route.request().url())
if (url.origin !== serverA && url.origin !== serverB) return route.fallback()
Expand All @@ -171,6 +226,11 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR
const requestDirectory = url.searchParams.get("directory")
const response = url.pathname.match(/^\/session\/([^/]+)\/permissions\/([^/]+)$/)
if (route.request().method() === "POST" && response) {
const failures = options.replyFailures?.[response[2]!] ?? 0
if (failures > 0) {
options.replyFailures![response[2]!] = failures - 1
return json(route, { name: "Internal" }, 500)
}
permissionResponses.push({
origin: url.origin,
directory: requestDirectory ?? undefined,
Expand Down Expand Up @@ -218,7 +278,7 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, [])
if (url.pathname === "/permission") {
permissionRequests.push(url.toString())
return json(route, [])
return json(route, options.pending?.[url.origin] ?? [])
}
if (["/skill", "/command", "/lsp", "/formatter", "/question", "/vcs/diff", "/pty/shells"].includes(url.pathname))
return json(route, [])
Expand Down
35 changes: 4 additions & 31 deletions packages/app/src/components/settings-general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
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"
import { useServerSync } from "@/context/server-sync"
import { useServerSDK } from "@/context/server-sdk"
Expand All @@ -27,7 +25,6 @@ import {
terminalInput,
useSettings,
} from "@/context/settings"
import { decode64 } from "@/utils/base64"
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
import { ExternalLink } from "./external-link"
import { SettingsList } from "./settings-list"
Expand Down Expand Up @@ -85,40 +82,13 @@ const playDemoSound = (id: string | undefined) => {
export const SettingsGeneral: Component = () => {
const theme = useTheme()
const language = useLanguage()
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) })))
Expand Down Expand Up @@ -321,7 +291,10 @@ 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={settings.permissions.autoApprove()}
onChange={(checked) => settings.permissions.setAutoApprove(checked)}
/>
</div>
</SettingsRow>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,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 />
Expand Down
29 changes: 0 additions & 29 deletions packages/app/src/components/settings-v2/general-controllers.ts
Original file line number Diff line number Diff line change
@@ -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 "@/context/permission"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import {
Expand All @@ -22,33 +21,6 @@ import { createSoundPreviewController, type ShellOption } from "./general-contro
export { createShellOptions, createSoundPreviewController } from "./general-controller-behavior"
export type { ShellOption, ShellSelectOption } from "./general-controller-behavior"

export function createPermissionScopeController(sessionID: Accessor<string | undefined>) {
const permission = usePermission()
const serverSync = useServerSync()
const directory = createMemo(() => {
const id = sessionID()
if (!id) return undefined
return serverSync().session.lineage.peek(id)?.session.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() {
const serverSdk = useServerSDK()
const serverSync = useServerSync()
Expand Down Expand Up @@ -167,7 +139,6 @@ export function createSoundSettingsController() {
}
}

export type PermissionScopeController = ReturnType<typeof createPermissionScopeController>
export type ShellSettingsController = ReturnType<typeof createShellSettingsController>
export type AppearanceSettingsController = ReturnType<typeof createAppearanceSettingsController>
export type SoundSettingsController = ReturnType<typeof createSoundSettingsController>
17 changes: 6 additions & 11 deletions packages/app/src/components/settings-v2/general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ import { SettingsRowV2 } from "./parts/row"
import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition"
import {
createAppearanceSettingsController,
createPermissionScopeController,
createShellOptions,
createShellSettingsController,
createSoundSettingsController,
soundOptions,
type AppearanceSettingsController,
type PermissionScopeController,
type ShellSettingsController,
type SoundSettingsController,
} from "./general-controllers"
Expand Down Expand Up @@ -69,18 +67,18 @@ const soundSettings = {
},
} as const

const PermissionScopeSetting: Component<{ controller: PermissionScopeController }> = (props) => {
const AutoApprovePermissionsSetting: Component = () => {
const language = useLanguage()
const settings = useSettings()
return (
<SettingsRowV2
title={language.t("command.permissions.autoaccept.enable")}
description={language.t("toast.permissions.autoaccept.on.description")}
>
<div data-action="settings-auto-accept-permissions">
<Switch
checked={props.controller.accepting()}
disabled={!props.controller.enabled()}
onChange={props.controller.set}
checked={settings.permissions.autoApprove()}
onChange={(checked) => settings.permissions.setAutoApprove(checked)}
/>
</div>
</SettingsRowV2>
Expand Down Expand Up @@ -271,16 +269,13 @@ const LanguageSetting = () => {
)
}

export const SettingsGeneralV2: Component<{
sessionID?: string
}> = (props) => {
export const SettingsGeneralV2: Component = () => {
const language = useLanguage()
const platform = usePlatform()
const dialog = useDialog()
const settings = useSettings()
const mobile = createMediaQuery("(max-width: 767px)")
const updater = useUpdaterAction()
const permissionScope = createPermissionScopeController(() => props.sessionID)
const shell = createShellSettingsController()
const appearance = createAppearanceSettingsController()
const sounds = createSoundSettingsController()
Expand Down Expand Up @@ -329,7 +324,7 @@ export const SettingsGeneralV2: Component<{
<SettingsListV2>
<LanguageSetting />

<PermissionScopeSetting controller={permissionScope} />
<AutoApprovePermissionsSetting />

<ShellSetting controller={shell} />

Expand Down
Loading
Loading