From e15a6fff02415bdbe6dd332c965aeca4845f08ca Mon Sep 17 00:00:00 2001
From: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Date: Tue, 14 Jul 2026 08:55:05 +1000
Subject: [PATCH 1/2] fix(app): enable remote session auto-accept
---
.../e2e/regression/remote-tab-busy.spec.ts | 22 +++++++++++++++++++
packages/app/src/pages/home.tsx | 4 ++--
packages/app/src/pages/layout-new.tsx | 2 --
packages/app/src/pages/new-session.tsx | 3 ++-
packages/app/src/pages/session.tsx | 21 +++++++++++++-----
5 files changed, 42 insertions(+), 10 deletions(-)
diff --git a/packages/app/e2e/regression/remote-tab-busy.spec.ts b/packages/app/e2e/regression/remote-tab-busy.spec.ts
index 119fc7ee2da..523065e8ef6 100644
--- a/packages/app/e2e/regression/remote-tab-busy.spec.ts
+++ b/packages/app/e2e/regression/remote-tab-busy.spec.ts
@@ -38,6 +38,28 @@ test("tab busy indicator reflects the tab server's own session status", async ({
await expect(tabA.locator('[data-component="session-progress-indicator-v2"]')).toHaveCount(0)
})
+test("auto-accept can be enabled for a session on a remote server", async ({ page }) => {
+ await mockServers(page)
+ await page.addInitScript(
+ ({ serverB }) => {
+ localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
+ localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
+ },
+ { serverB },
+ )
+
+ await page.goto(`/server/${base64Encode(serverB)}/session/${sessionB.id}`)
+ await expect(page.getByText(sessionB.title).first()).toBeVisible()
+ await page.keyboard.press("Control+,")
+
+ const autoAccept = page.locator('[data-action="settings-auto-accept-permissions"]')
+ const input = autoAccept.locator('[data-slot="switch-input"]')
+ await expect(autoAccept).toBeVisible()
+ await expect(input).toBeEnabled()
+ await autoAccept.locator('[data-slot="switch-control"]').click()
+ await expect(input).toBeChecked()
+})
+
function session(id: string, directory: string, title: string) {
return {
id,
diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx
index d697e53ae71..07035c585f1 100644
--- a/packages/app/src/pages/home.tsx
+++ b/packages/app/src/pages/home.tsx
@@ -35,7 +35,7 @@ import { usePlatform } from "@/context/platform"
import { DateTime } from "luxon"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useDirectoryPicker } from "@/components/directory-picker"
-import { useSettingsDialog } from "@/components/settings-dialog"
+import { useSettingsCommand } from "@/components/settings-dialog"
import { DialogSelectServer, useServerManagementController } from "@/components/dialog-select-server"
import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2"
import { ServerConnection, serverName, useServer } from "@/context/server"
@@ -269,7 +269,7 @@ export function NewHome() {
const command = useCommand()
const notification = useNotification()
const marked = useMarked()
- const openSettings = useSettingsDialog()
+ const openSettings = useSettingsCommand()
let focusSessionSearch: (() => void) | undefined
const [state, setState] = createStore({
search: "",
diff --git a/packages/app/src/pages/layout-new.tsx b/packages/app/src/pages/layout-new.tsx
index 095d1ebd7ea..45844320cb8 100644
--- a/packages/app/src/pages/layout-new.tsx
+++ b/packages/app/src/pages/layout-new.tsx
@@ -6,13 +6,11 @@ import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
import { usePlatform } from "@/context/platform"
import { setNavigate } from "@/utils/notification-click"
import { setV2Toast, ToastRegion } from "@/utils/toast"
-import { useSettingsCommand } from "@/components/settings-dialog"
export default function NewLayout(props: ParentProps) {
const platform = usePlatform()
const navigate = useNavigate()
setNavigate(navigate)
- useSettingsCommand()
createEffect(() => setV2Toast(true))
diff --git a/packages/app/src/pages/new-session.tsx b/packages/app/src/pages/new-session.tsx
index 19c90b9a384..3cb5a0d4594 100644
--- a/packages/app/src/pages/new-session.tsx
+++ b/packages/app/src/pages/new-session.tsx
@@ -28,7 +28,7 @@ import { PromptWorkspaceSelector } from "@/components/prompt-workspace-selector"
import { useTitlebarRightMount } from "@/components/titlebar"
import { useCommand } from "@/context/command"
import { useProviders } from "@/hooks/use-providers"
-import { useSettingsDialog } from "@/components/settings-dialog"
+import { useSettingsCommand, useSettingsDialog } from "@/components/settings-dialog"
import { Persist, persisted } from "@/utils/persist"
import createPresence from "solid-presence"
import { useLocal } from "@/context/local"
@@ -54,6 +54,7 @@ export default function NewSessionPage() {
const command = useCommand()
const providers = useProviders(() => sdk().directory)
const openProviderSettings = useSettingsDialog("providers")
+ useSettingsCommand()
const route = useSessionKey()
const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>()
const local = useLocal()
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 7ce4627135e..e24ae415560 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -58,6 +58,7 @@ import { useSync } from "@/context/sync"
import { useTabs } from "@/context/tabs"
import { TerminalProvider, useTerminal } from "@/context/terminal"
import { PromptInput } from "@/components/prompt-input"
+import { useSettingsCommand } from "@/components/settings-dialog"
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
import {
createPromptInputController,
@@ -153,13 +154,23 @@ export function SessionPage() {
// workspace-scoped state (terminal, directory providers) lives below.
export function TargetSessionRouteContent() {
const params = useParams<{ serverKey: string; id: string }>()
+ const serverSync = useServerSync()
+ const directory = createMemo(() => serverSync().session.lineage.peek(params.id)?.session.directory)
return (
-
-
-
+
+
+
+
+
+
)
}
+function TargetSessionSettingsCommand() {
+ useSettingsCommand()
+ return null
+}
+
export function SessionRouteErrorBoundary(
props: ParentProps<{ sessionID?: string; serverKey?: ServerConnection.Key; padded?: boolean }>,
) {
@@ -281,10 +292,10 @@ function TargetServerScopedProviders(
props: ParentProps<{ directory?: () => string | undefined; sessionID?: () => string | undefined }>,
) {
return (
-
+ <>
{props.children}
-
+ >
)
}
From 56878236101d4a8b88be4df8e00f06a66915bdcd Mon Sep 17 00:00:00 2001
From: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Date: Tue, 14 Jul 2026 09:29:28 +1000
Subject: [PATCH 2/2] fix(app): scope remote settings models
---
.../remote-session-settings.spec.ts | 138 ++++++++++++++++++
.../e2e/regression/remote-tab-busy.spec.ts | 22 ---
packages/app/src/pages/session.tsx | 34 ++---
3 files changed, 155 insertions(+), 39 deletions(-)
create mode 100644 packages/app/e2e/regression/remote-session-settings.spec.ts
diff --git a/packages/app/e2e/regression/remote-session-settings.spec.ts b/packages/app/e2e/regression/remote-session-settings.spec.ts
new file mode 100644
index 00000000000..320cc2c1c62
--- /dev/null
+++ b/packages/app/e2e/regression/remote-session-settings.spec.ts
@@ -0,0 +1,138 @@
+import { base64Encode } from "@opencode-ai/core/util/encode"
+import { expect, test, type Page, type Route } from "@playwright/test"
+
+const serverA = "http://127.0.0.1:4096"
+const serverB = "http://127.0.0.1:4097"
+const directoryA = "C:/server-a"
+const directoryB = "/home/server-b"
+const sessionB = {
+ id: "ses_server_b",
+ slug: "ses_server_b",
+ projectID: "project-server-b",
+ directory: directoryB,
+ title: "Server B session",
+ version: "dev",
+ time: { created: 1, updated: 1 },
+}
+
+test("session settings use the remote server context", async ({ page }) => {
+ const permissionRequests: string[] = []
+ await mockServers(page, permissionRequests)
+ await page.addInitScript(
+ ({ serverB }) => {
+ localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
+ localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
+ },
+ { serverB },
+ )
+
+ await page.goto(`/server/${base64Encode(serverB)}/session/${sessionB.id}`)
+ await expect(page.getByText(sessionB.title).first()).toBeVisible()
+ await page.keyboard.press(process.platform === "darwin" ? "Meta+," : "Control+,")
+
+ const dialog = page.locator(".settings-v2-dialog")
+ const autoAccept = dialog.locator('[data-action="settings-auto-accept-permissions"]')
+ const input = autoAccept.getByRole("switch")
+ await expect(autoAccept).toBeVisible()
+ await expect(input).toBeEnabled()
+ permissionRequests.length = 0
+ await autoAccept.locator('[data-slot="switch-control"]').click()
+ await expect(input).toBeChecked()
+ await expect
+ .poll(() =>
+ permissionRequests.some((request) => {
+ const url = new URL(request)
+ return url.origin === serverB && url.searchParams.get("directory") === directoryB
+ }),
+ )
+ .toBe(true)
+ expect(permissionRequests.every((request) => new URL(request).origin === serverB)).toBe(true)
+
+ await dialog.getByRole("tab", { name: "Models" }).click()
+ await expect(dialog.getByRole("switch", { name: "Server B Model" })).toBeEnabled()
+ await expect(dialog.getByRole("switch", { name: "Server A Model" })).toHaveCount(0)
+})
+
+async function mockServers(page: Page, permissionRequests: string[]) {
+ await page.route("**/*", async (route) => {
+ const url = new URL(route.request().url())
+ if (url.origin !== serverA && url.origin !== serverB) return route.fallback()
+ const remote = url.origin === serverB
+ const directory = remote ? directoryB : directoryA
+ const requestDirectory = url.searchParams.get("directory")
+ if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500)
+ if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route)
+ if (url.pathname === "/global/health") return json(route, { healthy: true })
+ if (url.pathname === "/session/status") return json(route, {})
+ if (url.pathname === "/session") return json(route, remote ? [sessionB] : [])
+ if (url.pathname === `/session/${sessionB.id}` && remote) return json(route, sessionB)
+ if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404)
+ if (url.pathname === `/session/${sessionB.id}/message`) return json(route, [])
+ if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, [])
+ if (url.pathname === "/permission") {
+ permissionRequests.push(url.toString())
+ return json(route, [])
+ }
+ if (["/skill", "/command", "/lsp", "/formatter", "/question", "/vcs/diff", "/pty/shells"].includes(url.pathname))
+ return json(route, [])
+ if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {})
+ if (url.pathname === "/provider") return json(route, provider(remote ? "server-b" : "server-a"))
+ if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }])
+ if (url.pathname === "/project" || url.pathname === "/project/current") {
+ const project = {
+ id: remote ? sessionB.projectID : "project-server-a",
+ worktree: directory,
+ vcs: "git",
+ time: { created: 1, updated: 1 },
+ sandboxes: [],
+ }
+ return json(route, url.pathname === "/project" ? [project] : project)
+ }
+ if (url.pathname === "/path")
+ return json(route, {
+ state: directory,
+ config: directory,
+ worktree: directory,
+ directory,
+ home: directory,
+ })
+ if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" })
+ return json(route, {})
+ })
+}
+
+function provider(id: string) {
+ const name = id === "server-b" ? "Server B" : "Server A"
+ return {
+ all: [
+ {
+ id,
+ name: `${name} Provider`,
+ models: {
+ [id]: {
+ id,
+ name: `${name} Model`,
+ family: id,
+ release_date: "2026-01-01",
+ limit: { context: 200_000 },
+ },
+ },
+ },
+ ],
+ connected: [id],
+ default: { providerID: id, modelID: id },
+ }
+}
+
+function json(route: Route, body: unknown, status = 200) {
+ return route.fulfill({
+ status,
+ contentType: "application/json",
+ headers: { "access-control-allow-origin": "*" },
+ body: JSON.stringify(body),
+ })
+}
+
+function sse(route: Route) {
+ return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" })
+}
diff --git a/packages/app/e2e/regression/remote-tab-busy.spec.ts b/packages/app/e2e/regression/remote-tab-busy.spec.ts
index 523065e8ef6..119fc7ee2da 100644
--- a/packages/app/e2e/regression/remote-tab-busy.spec.ts
+++ b/packages/app/e2e/regression/remote-tab-busy.spec.ts
@@ -38,28 +38,6 @@ test("tab busy indicator reflects the tab server's own session status", async ({
await expect(tabA.locator('[data-component="session-progress-indicator-v2"]')).toHaveCount(0)
})
-test("auto-accept can be enabled for a session on a remote server", async ({ page }) => {
- await mockServers(page)
- await page.addInitScript(
- ({ serverB }) => {
- localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
- localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
- },
- { serverB },
- )
-
- await page.goto(`/server/${base64Encode(serverB)}/session/${sessionB.id}`)
- await expect(page.getByText(sessionB.title).first()).toBeVisible()
- await page.keyboard.press("Control+,")
-
- const autoAccept = page.locator('[data-action="settings-auto-accept-permissions"]')
- const input = autoAccept.locator('[data-slot="switch-input"]')
- await expect(autoAccept).toBeVisible()
- await expect(input).toBeEnabled()
- await autoAccept.locator('[data-slot="switch-control"]').click()
- await expect(input).toBeChecked()
-})
-
function session(id: string, directory: string, title: string) {
return {
id,
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index e24ae415560..ca415d77c46 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -157,12 +157,14 @@ export function TargetSessionRouteContent() {
const serverSync = useServerSync()
const directory = createMemo(() => serverSync().session.lineage.peek(params.id)?.session.directory)
return (
-
+ // Settings must keep the complete target-server context and remain registered
+ // when session content falls back to the route error boundary.
+ params.id}>
-
+
)
}
@@ -259,19 +261,17 @@ function ResolvedTargetSessionRoute() {
})
return (
- params.id}>
- {/* Non-keyed: closes only while the target's directory is unknown (uncached
- lineage mid-resolution), which tears down the workspace subtree including
- the terminal. Same-workspace tab switches keep it open because warm
- targets resolve synchronously from the sync cache. */}
-
-
-
-
-
-
-
-
+ // Non-keyed: closes only while the target's directory is unknown (uncached
+ // lineage mid-resolution), which tears down the workspace subtree including
+ // the terminal. Same-workspace tab switches keep it open because warm
+ // targets resolve synchronously from the sync cache.
+
+
+
+
+
+
+
)
}
@@ -292,10 +292,10 @@ function TargetServerScopedProviders(
props: ParentProps<{ directory?: () => string | undefined; sessionID?: () => string | undefined }>,
) {
return (
- <>
+
{props.children}
- >
+
)
}