From 33a734e11070ea8ac77fd0eb2e41dfd96cec8375 Mon Sep 17 00:00:00 2001 From: Yiliu Date: Fri, 15 May 2026 12:48:21 +0800 Subject: [PATCH] fix(app): isLocal() should only return true for sidecar/base connections - Change isLocal() to only recognize sidecar variant 'base' as local - Align getTerminalServerScope() with the same semantics - Extract isLocalConnection() pure function for testability - Add decision matrix tests for both predicates Fixes: connecting Desktop to WSL via localhost HTTP was incorrectly treated as local, causing Windows native Explorer to open instead of the in-app DialogSelectDirectory for browsing remote filesystem. --- .../app/src/context/server-islocal.test.ts | 136 ++++++++++++++++++ packages/app/src/context/server.tsx | 6 +- packages/app/src/context/terminal.test.ts | 18 +-- packages/app/src/context/terminal.tsx | 14 -- 4 files changed, 150 insertions(+), 24 deletions(-) create mode 100644 packages/app/src/context/server-islocal.test.ts diff --git a/packages/app/src/context/server-islocal.test.ts b/packages/app/src/context/server-islocal.test.ts new file mode 100644 index 000000000000..1b839d4af1a3 --- /dev/null +++ b/packages/app/src/context/server-islocal.test.ts @@ -0,0 +1,136 @@ +import { beforeAll, describe, expect, mock, test } from "bun:test" +import type { ServerConnection } from "./server" + +let isLocalConnection: (conn: ServerConnection.Any) => boolean + +beforeAll(async () => { + mock.module("@opencode-ai/ui/context", () => ({ + createSimpleContext: () => ({ + use: () => undefined, + provider: () => undefined, + }), + })) + mock.module("solid-js", () => { + const noop = () => () => {} + return { + $DEVCOMP: false, + $PROXY: false, + $TRACK: false, + DEV: {}, + ErrorBoundary: noop, + For: noop, + Index: noop, + Match: noop, + Show: noop, + Suspense: noop, + SuspenseList: noop, + Switch: noop, + batch: (fn: () => void) => fn(), + catchError: noop, + children: noop, + createComponent: noop, + createComputed: noop, + createContext: noop, + createDeferred: noop, + createEffect: noop, + createMemo: (fn: () => unknown) => fn, + createReaction: noop, + createRenderEffect: noop, + createResource: () => [undefined, { refetch: () => {} }], + createRoot: (fn: () => unknown) => fn(), + createSelector: noop, + createSignal: () => [() => undefined, () => {}], + createUniqueId: () => "", + enableExternalSource: noop, + enableHydration: noop, + enableScheduling: noop, + equalFn: noop, + from: noop, + getListener: noop, + getOwner: () => null, + indexArray: noop, + lazy: noop, + mapArray: noop, + mergeProps: () => ({}), + observable: noop, + on: noop, + onCleanup: noop, + onError: noop, + onMount: noop, + requestCallback: noop, + resetErrorBoundaries: noop, + runWithOwner: noop, + sharedConfig: {}, + splitProps: () => [], + startTransition: noop, + untrack: (fn: () => unknown) => fn(), + useContext: noop, + useTransition: () => [false, noop], + } + }) + mock.module("solid-js/store", () => ({ + createStore: () => [{}, () => {}], + })) + mock.module("@/utils/persist", () => ({ + Persist: { global: () => "" }, + persisted: () => [{}, () => {}, () => {}, () => true], + })) + mock.module("@/utils/server-health", () => ({ + useCheckServerHealth: () => () => ({ subscribe: () => () => {} }), + })) + const mod = await import("./server") + isLocalConnection = mod.isLocalConnection +}) + +describe("isLocalConnection", () => { + test("returns true for sidecar base variant", () => { + const conn: ServerConnection.Sidecar = { + type: "sidecar", + variant: "base", + http: { url: "http://127.0.0.1:4096" }, + } + expect(isLocalConnection(conn)).toBe(true) + }) + + test("returns false for sidecar wsl variant", () => { + const conn: ServerConnection.Sidecar = { + type: "sidecar", + variant: "wsl", + distro: "Ubuntu", + http: { url: "http://127.0.0.1:4096" }, + } + expect(isLocalConnection(conn)).toBe(false) + }) + + test("returns false for http localhost", () => { + const conn: ServerConnection.Http = { + type: "http", + http: { url: "http://localhost:4096" }, + } + expect(isLocalConnection(conn)).toBe(false) + }) + + test("returns false for http 127.0.0.1", () => { + const conn: ServerConnection.Http = { + type: "http", + http: { url: "http://127.0.0.1:4096" }, + } + expect(isLocalConnection(conn)).toBe(false) + }) + + test("returns false for http remote IP", () => { + const conn: ServerConnection.Http = { + type: "http", + http: { url: "http://192.168.1.100:4096" }, + } + expect(isLocalConnection(conn)).toBe(false) + }) + + test("returns false for http remote hostname", () => { + const conn: ServerConnection.Http = { + type: "http", + http: { url: "http://remote.example.com:4096" }, + } + expect(isLocalConnection(conn)).toBe(false) + }) +}) diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index a981d99fa1d9..be16357ec654 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -120,6 +120,10 @@ export namespace ServerConnection { export const Key = { make: (v: string) => v as Key } } +export function isLocalConnection(conn: ServerConnection.Any): boolean { + return conn.type === "sidecar" && conn.variant === "base" +} + export const { use: useServer, provider: ServerProvider } = createSimpleContext({ name: "Server", init: (props: { @@ -230,7 +234,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( ) const isLocal = createMemo(() => { const c = current() - return (c?.type === "sidecar" && c.variant === "base") || (c?.type === "http" && isLocalHost(c.http.url)) + return !!c && isLocalConnection(c) }) return { diff --git a/packages/app/src/context/terminal.test.ts b/packages/app/src/context/terminal.test.ts index 5bca1b4b7edd..3799f9a74af2 100644 --- a/packages/app/src/context/terminal.test.ts +++ b/packages/app/src/context/terminal.test.ts @@ -43,15 +43,6 @@ describe("getTerminalServerScope", () => { "sidecar" as ServerKey, ), ).toBeUndefined() - expect( - getTerminalServerScope( - { type: "http", http: { url: "http://localhost:4096" } }, - "http://localhost:4096" as ServerKey, - ), - ).toBeUndefined() - expect( - getTerminalServerScope({ type: "http", http: { url: "http://[::1]:4096" } }, "http://[::1]:4096" as ServerKey), - ).toBeUndefined() }) test("scopes non-local server keys", () => { @@ -61,6 +52,15 @@ describe("getTerminalServerScope", () => { "wsl:Debian" as ServerKey, ), ).toBe("wsl:Debian" as ServerKey) + expect( + getTerminalServerScope( + { type: "http", http: { url: "http://localhost:4096" } }, + "http://localhost:4096" as ServerKey, + ), + ).toBe("http://localhost:4096" as ServerKey) + expect( + getTerminalServerScope({ type: "http", http: { url: "http://[::1]:4096" } }, "http://[::1]:4096" as ServerKey), + ).toBe("http://[::1]:4096" as ServerKey) expect( getTerminalServerScope( { type: "http", http: { url: "https://example.com" } }, diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index f6751c3f0ec7..e71cdeeb94ba 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -91,20 +91,6 @@ export function getWorkspaceTerminalCacheKey(dir: string, scope?: string) { export function getTerminalServerScope(conn: ServerConnection.Any | undefined, key: ServerConnection.Key) { if (!conn) return if (conn.type === "sidecar" && conn.variant === "base") return - if (conn.type === "http") { - try { - const url = new URL(conn.http.url) - if ( - url.hostname === "localhost" || - url.hostname === "127.0.0.1" || - url.hostname === "::1" || - url.hostname === "[::1]" - ) - return - } catch { - return key - } - } return key }