From d1b08c9bfa5fc0968637253c0942ebfc99fcc6bf Mon Sep 17 00:00:00 2001
From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Date: Mon, 22 Jun 2026 16:54:32 +0530
Subject: [PATCH 1/2] #115: wsl server button dropdown
---
.../src/components/settings-v2/servers.tsx | 8 +-
packages/app/src/entry.tsx | 2 +
packages/app/src/index.ts | 1 +
packages/app/src/wsl/mock.test.ts | 29 ++
packages/app/src/wsl/mock.ts | 308 ++++++++++++++++++
packages/app/src/wsl/settings.tsx | 27 +-
packages/desktop/src/renderer/index.tsx | 3 +-
packages/ui/src/v2/components/button-v2.css | 17 +-
.../ui/src/v2/components/icon-button-v2.css | 9 +-
9 files changed, 387 insertions(+), 17 deletions(-)
create mode 100644 packages/app/src/wsl/mock.test.ts
create mode 100644 packages/app/src/wsl/mock.ts
diff --git a/packages/app/src/components/settings-v2/servers.tsx b/packages/app/src/components/settings-v2/servers.tsx
index 3a7203bc7647..b93f700d24cf 100644
--- a/packages/app/src/components/settings-v2/servers.tsx
+++ b/packages/app/src/components/settings-v2/servers.tsx
@@ -1,4 +1,3 @@
-import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
@@ -14,7 +13,7 @@ import { ServerConnection, serverName } from "@/context/server"
import { useServerManagementController } from "../dialog-select-server"
import { DialogServerV2 } from "./dialog-server-v2"
import { SettingsListV2 } from "./parts/list"
-import { isWslServer, useFilteredWslServers, WslAddServerButton, WslServerSettings } from "@/wsl/settings"
+import { AddServerMenu, isWslServer, useFilteredWslServers, WslServerSettings } from "@/wsl/settings"
import "./settings-v2.css"
export const SettingsServersV2: Component = () => {
@@ -55,10 +54,7 @@ export const SettingsServersV2: Component = () => {
>
diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx
index c41c1bb4b63e..1eba2f49c845 100644
--- a/packages/app/src/entry.tsx
+++ b/packages/app/src/entry.tsx
@@ -10,6 +10,7 @@ import { handleNotificationClick } from "@/utils/notification-click"
import { authFromToken } from "@/utils/server"
import pkg from "../package.json"
import { ServerConnection } from "./context/server"
+import { devWslServers } from "./wsl/mock"
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
@@ -132,6 +133,7 @@ const platform: Platform = {
return stored ? ServerConnection.Key.make(stored) : null
},
setDefaultServer: writeDefaultServerUrl,
+ wslServers: devWslServers(),
}
if (import.meta.env.VITE_SENTRY_DSN) {
diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts
index 2bc9262fde63..46695e6fc1b3 100644
--- a/packages/app/src/index.ts
+++ b/packages/app/src/index.ts
@@ -5,6 +5,7 @@ export { loadLocaleDict, normalizeLocale, type Locale } from "./context/language
export { useWslServers } from "./wsl/context"
export { type DisplayBackend, type FatalRendererErrorLog, type Platform, PlatformProvider } from "./context/platform"
export { type UpdaterPlatform, type UpdaterState } from "./updater"
+export { createMockWslServers, devWslServers, wslMockScenario, type WslMockScenario } from "./wsl/mock"
export {
type WslDistroProbe,
type WslInstalledDistro,
diff --git a/packages/app/src/wsl/mock.test.ts b/packages/app/src/wsl/mock.test.ts
new file mode 100644
index 000000000000..3a329f9852f0
--- /dev/null
+++ b/packages/app/src/wsl/mock.test.ts
@@ -0,0 +1,29 @@
+import { describe, expect, test } from "bun:test"
+import { createMockWslServers } from "./mock"
+
+describe("createMockWslServers", () => {
+ test("returns onboarding state for onboarding scenario", async () => {
+ const api = createMockWslServers("onboarding")
+ const state = await api.getState()
+ expect(state.runtime?.available).toBe(false)
+ expect(state.servers).toEqual([])
+ })
+
+ test("adds a server after startup delay", async () => {
+ const api = createMockWslServers("fresh")
+ const config = await api.addServer("Ubuntu-24.04")
+ expect(config.id).toBe("wsl:Ubuntu-24.04")
+ const ready = await api.getState()
+ expect(ready.servers.some((item) => item.config.id === config.id && item.runtime.kind === "ready")).toBe(true)
+ })
+
+ test("notifies subscribers when state changes", async () => {
+ const api = createMockWslServers("onboarding")
+ let count = 0
+ api.subscribe(() => {
+ count++
+ })
+ await api.probeRuntime()
+ expect(count).toBeGreaterThan(1)
+ })
+})
diff --git a/packages/app/src/wsl/mock.ts b/packages/app/src/wsl/mock.ts
new file mode 100644
index 000000000000..dd04ab7bf916
--- /dev/null
+++ b/packages/app/src/wsl/mock.ts
@@ -0,0 +1,308 @@
+import type {
+ WslDistroProbe,
+ WslOpencodeCheck,
+ WslServerConfig,
+ WslServerItem,
+ WslServersEvent,
+ WslServersPlatform,
+ WslServersState,
+} from "./types"
+
+export type WslMockScenario = "default" | "onboarding" | "servers" | "fresh"
+
+const MOCK_VERSION = "1.17.9"
+const DEFAULT_DISTRO = "Ubuntu-24.04"
+const SECONDARY_DISTRO = "Debian"
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms))
+}
+
+function distroProbe(name: string, ready = true): WslDistroProbe {
+ return {
+ name,
+ canExecute: ready,
+ hasBash: ready,
+ hasCurl: ready,
+ error: ready ? null : `${name} is not ready yet`,
+ }
+}
+
+function opencodeCheck(distro: string, ready: boolean, outdated = false): WslOpencodeCheck {
+ if (!ready) {
+ return {
+ distro,
+ resolvedPath: null,
+ version: null,
+ expectedVersion: MOCK_VERSION,
+ matchesDesktop: null,
+ error: null,
+ }
+ }
+ const version = outdated ? "1.14.0" : MOCK_VERSION
+ return {
+ distro,
+ resolvedPath: `/home/dev/.opencode/bin/opencode`,
+ version,
+ expectedVersion: MOCK_VERSION,
+ matchesDesktop: !outdated,
+ error: null,
+ }
+}
+
+function readyRuntime(url: string): WslServerItem["runtime"] {
+ return { kind: "ready", url, username: "opencode", password: "mock" }
+}
+
+function initialState(scenario: WslMockScenario): WslServersState {
+ if (scenario === "onboarding") {
+ return {
+ runtime: { available: false, version: null, error: "WSL is not installed" },
+ installed: [],
+ online: [],
+ distroProbes: {},
+ opencodeChecks: {},
+ pendingRestart: false,
+ servers: [],
+ job: null,
+ }
+ }
+
+ if (scenario === "fresh") {
+ return {
+ runtime: { available: true, version: "2.4.13.0", error: null },
+ installed: [
+ { name: DEFAULT_DISTRO, version: 2, isDefault: true },
+ { name: SECONDARY_DISTRO, version: 2, isDefault: false },
+ ],
+ online: [
+ { name: DEFAULT_DISTRO, label: "Ubuntu 24.04 LTS" },
+ { name: SECONDARY_DISTRO, label: "Debian" },
+ { name: "FedoraLinux-42", label: "Fedora Linux 42" },
+ ],
+ distroProbes: {
+ [DEFAULT_DISTRO]: distroProbe(DEFAULT_DISTRO),
+ [SECONDARY_DISTRO]: distroProbe(SECONDARY_DISTRO),
+ },
+ opencodeChecks: {
+ [DEFAULT_DISTRO]: opencodeCheck(DEFAULT_DISTRO, false),
+ [SECONDARY_DISTRO]: opencodeCheck(SECONDARY_DISTRO, true, true),
+ },
+ pendingRestart: false,
+ servers: [],
+ job: null,
+ }
+ }
+
+ if (scenario === "servers") {
+ return {
+ runtime: { available: true, version: "2.4.13.0", error: null },
+ installed: [
+ { name: DEFAULT_DISTRO, version: 2, isDefault: true },
+ { name: SECONDARY_DISTRO, version: 2, isDefault: false },
+ ],
+ online: [],
+ distroProbes: {
+ [DEFAULT_DISTRO]: distroProbe(DEFAULT_DISTRO),
+ [SECONDARY_DISTRO]: distroProbe(SECONDARY_DISTRO),
+ },
+ opencodeChecks: {
+ [DEFAULT_DISTRO]: opencodeCheck(DEFAULT_DISTRO, true),
+ [SECONDARY_DISTRO]: opencodeCheck(SECONDARY_DISTRO, true, true),
+ },
+ pendingRestart: false,
+ servers: [
+ {
+ config: { id: `wsl:${DEFAULT_DISTRO}`, distro: DEFAULT_DISTRO },
+ runtime: readyRuntime("http://127.0.0.1:4097"),
+ },
+ {
+ config: { id: `wsl:${SECONDARY_DISTRO}`, distro: SECONDARY_DISTRO },
+ runtime: { kind: "starting" },
+ },
+ {
+ config: { id: "wsl:FedoraLinux-42", distro: "FedoraLinux-42" },
+ runtime: { kind: "failed", message: "Sidecar failed to start" },
+ },
+ ],
+ job: null,
+ }
+ }
+
+ return {
+ runtime: { available: true, version: "2.4.13.0", error: null },
+ installed: [
+ { name: DEFAULT_DISTRO, version: 2, isDefault: true },
+ { name: SECONDARY_DISTRO, version: 2, isDefault: false },
+ ],
+ online: [{ name: "FedoraLinux-42", label: "Fedora Linux 42" }],
+ distroProbes: {
+ [DEFAULT_DISTRO]: distroProbe(DEFAULT_DISTRO),
+ [SECONDARY_DISTRO]: distroProbe(SECONDARY_DISTRO),
+ },
+ opencodeChecks: {
+ [DEFAULT_DISTRO]: opencodeCheck(DEFAULT_DISTRO, true),
+ [SECONDARY_DISTRO]: opencodeCheck(SECONDARY_DISTRO, true, true),
+ },
+ pendingRestart: false,
+ servers: [
+ {
+ config: { id: `wsl:${SECONDARY_DISTRO}`, distro: SECONDARY_DISTRO },
+ runtime: readyRuntime("http://127.0.0.1:4098"),
+ },
+ ],
+ job: null,
+ }
+}
+
+export function wslMockScenario(): WslMockScenario {
+ if (typeof window === "undefined") return "default"
+ const value = new URLSearchParams(window.location.search).get("wslMock")
+ if (value === "onboarding" || value === "servers" || value === "fresh") return value
+ return "default"
+}
+
+export function createMockWslServers(scenario: WslMockScenario = wslMockScenario()): WslServersPlatform {
+ let state = initialState(scenario)
+ const listeners = new Set<(event: WslServersEvent) => void>()
+
+ const emit = () => {
+ const snapshot = structuredClone(state)
+ listeners.forEach((listener) => listener({ type: "state", state: snapshot }))
+ }
+
+ const setState = (next: Partial) => {
+ state = { ...state, ...next }
+ emit()
+ }
+
+ const withJob = async (job: WslServersState["job"], run: () => Promise) => {
+ setState({ job })
+ await delay(600)
+ const result = await run()
+ setState({ job: null })
+ return result
+ }
+
+ const serverId = (distro: string) => `wsl:${distro}`
+
+ return {
+ getState: async () => structuredClone(state),
+
+ subscribe(cb) {
+ listeners.add(cb)
+ cb({ type: "state", state: structuredClone(state) })
+ return () => listeners.delete(cb)
+ },
+
+ probeRuntime: () =>
+ withJob({ kind: "runtime", startedAt: Date.now() }, async () => {
+ setState({
+ runtime: { available: true, version: "2.4.13.0", error: null },
+ pendingRestart: false,
+ })
+ }),
+
+ refreshDistros: () =>
+ withJob({ kind: "distros", startedAt: Date.now() }, async () => {
+ setState({
+ installed: [
+ { name: DEFAULT_DISTRO, version: 2, isDefault: true },
+ { name: SECONDARY_DISTRO, version: 2, isDefault: false },
+ ],
+ online: [
+ { name: DEFAULT_DISTRO, label: "Ubuntu 24.04 LTS" },
+ { name: SECONDARY_DISTRO, label: "Debian" },
+ { name: "FedoraLinux-42", label: "Fedora Linux 42" },
+ ],
+ })
+ }),
+
+ installWsl: () =>
+ withJob({ kind: "install-wsl", startedAt: Date.now() }, async () => {
+ setState({ runtime: { available: true, version: "2.4.13.0", error: null } })
+ }),
+
+ installDistro: (name) =>
+ withJob({ kind: "install-distro", distro: name, startedAt: Date.now() }, async () => {
+ const installed = state.installed.some((item) => item.name === name)
+ ? state.installed
+ : [...state.installed, { name, version: 2, isDefault: state.installed.length === 0 }]
+ setState({ installed })
+ }),
+
+ probeDistro: (name) =>
+ withJob({ kind: "probe-distro", distro: name, startedAt: Date.now() }, async () => {
+ setState({
+ distroProbes: { ...state.distroProbes, [name]: distroProbe(name) },
+ })
+ }),
+
+ probeOpencode: (name) =>
+ withJob({ kind: "probe-opencode", distro: name, startedAt: Date.now() }, async () => {
+ const existing = state.opencodeChecks[name]
+ setState({
+ opencodeChecks: {
+ ...state.opencodeChecks,
+ [name]: existing?.resolvedPath ? existing : opencodeCheck(name, false),
+ },
+ })
+ }),
+
+ installOpencode: (name) =>
+ withJob({ kind: "install-opencode", distro: name, startedAt: Date.now() }, async () => {
+ setState({
+ opencodeChecks: { ...state.opencodeChecks, [name]: opencodeCheck(name, true) },
+ })
+ }),
+
+ openTerminal: async (name) => {
+ console.info("[opencode] mock WSL openTerminal", name)
+ },
+
+ addServer: async (distro) => {
+ const config: WslServerConfig = { id: serverId(distro), distro }
+ setState({
+ servers: [...state.servers.filter((item) => item.config.id !== config.id), { config, runtime: { kind: "starting" } }],
+ })
+ await delay(800)
+ const servers = state.servers.map((item) =>
+ item.config.id === config.id
+ ? { ...item, runtime: readyRuntime(`http://127.0.0.1:${4096 + state.servers.length}`) }
+ : item,
+ )
+ setState({ servers })
+ return config
+ },
+
+ removeServer: async (id) => {
+ setState({ servers: state.servers.filter((item) => item.config.id !== id) })
+ },
+
+ startServer: async (id) => {
+ const servers = state.servers.map((item) =>
+ item.config.id === id ? { ...item, runtime: { kind: "starting" } as const } : item,
+ )
+ setState({ servers })
+ await delay(800)
+ setState({
+ servers: state.servers.map((item) =>
+ item.config.id === id ? { ...item, runtime: readyRuntime("http://127.0.0.1:4099") } : item,
+ ),
+ })
+ },
+ }
+}
+
+export function devWslServers(input?: { os?: "macos" | "windows" | "linux" }) {
+ if (!import.meta.env.DEV) return undefined
+ if (input?.os === "windows") return undefined
+ if (typeof window !== "undefined" && new URLSearchParams(window.location.search).get("wslMock") === "0") {
+ return undefined
+ }
+ const scenario = wslMockScenario()
+ console.info(
+ `[opencode] Mock WSL servers enabled (scenario: ${scenario}). Use ?wslMock=onboarding|fresh|servers|default or ?wslMock=0 to disable.`,
+ )
+ return createMockWslServers(scenario)
+}
diff --git a/packages/app/src/wsl/settings.tsx b/packages/app/src/wsl/settings.tsx
index 746a5861c52c..ca8e650f9bd5 100644
--- a/packages/app/src/wsl/settings.tsx
+++ b/packages/app/src/wsl/settings.tsx
@@ -24,11 +24,11 @@ export function isWslServer(server: ServerConnection.Any) {
return server.type === "sidecar" && server.variant === "wsl"
}
-export function WslAddServerButton() {
+export function AddServerMenu(props: { onAddServer: () => void }) {
const platform = usePlatform()
const dialog = useDialog()
const language = useLanguage()
- const openAdd = () => {
+ const openAddWsl = () => {
dialog.push(() => (