diff --git a/bun.lock b/bun.lock index 3cf2d9ce99..4f40c58c78 100644 --- a/bun.lock +++ b/bun.lock @@ -265,6 +265,7 @@ "name": "@opencode-ai/desktop-electron", "version": "1.14.34", "dependencies": { + "@effect/platform-node": "catalog:", "drizzle-orm": "catalog:", "effect": "catalog:", "electron-context-menu": "4.1.2", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 8a6fcf5786..c3c95ec46b 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -25,6 +25,7 @@ "main": "./out/main/index.js", "dependencies": { "effect": "catalog:", + "@effect/platform-node": "catalog:", "electron-context-menu": "4.1.2", "electron-log": "^5", "electron-store": "^10", diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 463989e0d4..4f9857833a 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -4,6 +4,8 @@ import { createServer } from "node:net" import { homedir } from "node:os" import { join } from "node:path" import { app, BrowserWindow, dialog } from "electron" +import * as NodeRuntime from "@effect/platform-node/NodeRuntime" +import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient" import pkg from "electron-updater" import { Data, Deferred, Effect, Fiber, Option, PubSub, Queue, Ref, Stream, SubscriptionRef } from "effect" @@ -312,7 +314,7 @@ const main = Effect.gen(function* () { ), ) -void Effect.runPromise(main) +main.pipe(Effect.provide(NodeHttpClient.layerFetch), NodeRuntime.runMain()) const wireMenu = (win: BrowserWindow) => { createMenu({ @@ -364,10 +366,10 @@ const registerIpcHandlersImpl = () => Effect.tap(() => Deferred.succeed(deepLinksConsumed, undefined)), ), ), - getDefaultServerUrl: () => Effect.runPromise(getDefaultServerUrl), - setDefaultServerUrl: (url) => Effect.runPromise(setDefaultServerUrl(url)), - getWslConfig: () => Effect.runPromise(getWslConfig), - setWslConfig: (config: WslConfig) => Effect.runPromise(setWslConfig(config)), + getDefaultServerUrl: () => getDefaultServerUrl(), + setDefaultServerUrl: (url) => setDefaultServerUrl(url), + getWslConfig: () => Promise.resolve(getWslConfig()), + setWslConfig: (config: WslConfig) => setWslConfig(config), getDisplayBackend: () => Promise.resolve(null), setDisplayBackend: () => Promise.resolve(undefined), parseMarkdown: (markdown) => Promise.resolve(parseMarkdown(markdown)), diff --git a/packages/desktop-electron/src/main/server.ts b/packages/desktop-electron/src/main/server.ts index 75c62e7b58..2e8ac536e7 100644 --- a/packages/desktop-electron/src/main/server.ts +++ b/packages/desktop-electron/src/main/server.ts @@ -1,81 +1,83 @@ import { app } from "electron" -import { Effect } from "effect" +import { Effect, Option } from "effect" import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants" import { getUserShell, loadShellEnv } from "./shell-env" import { getStore } from "./store" +import { HttpClient, HttpClientResponse } from "effect/unstable/http" export type WslConfig = { enabled: boolean } -export const getDefaultServerUrl = Effect.sync((): string | null => { +export const getDefaultServerUrl = (): string | null => { const value = getStore().get(DEFAULT_SERVER_URL_KEY) return typeof value === "string" ? value : null -}) +} -export const setDefaultServerUrl = (url: string | null) => - Effect.sync(() => { - if (url) { - getStore().set(DEFAULT_SERVER_URL_KEY, url) - return - } - getStore().delete(DEFAULT_SERVER_URL_KEY) - }) +export const setDefaultServerUrl = (url: string | null) => { + if (url) { + getStore().set(DEFAULT_SERVER_URL_KEY, url) + return + } + getStore().delete(DEFAULT_SERVER_URL_KEY) +} -export const getWslConfig = Effect.sync((): WslConfig => { +export const getWslConfig = (): WslConfig => { const value = getStore().get(WSL_ENABLED_KEY) return { enabled: typeof value === "boolean" ? value : false } -}) +} -export const setWslConfig = (config: WslConfig) => - Effect.sync(() => getStore().set(WSL_ENABLED_KEY, config.enabled)) +export const setWslConfig = (config: WslConfig) => getStore().set(WSL_ENABLED_KEY, config.enabled) -export const spawnLocalServerEffect = Effect.fn("Server.spawnLocalServer")( - function* (hostname: string, port: number, password: string) { - yield* prepareServerEnv(password) - const { Log, Server } = yield* Effect.promise(() => - import("virtual:opencode-server") as Promise, - ) - yield* Effect.promise(() => Log.init({ level: "WARN" })) - const listener = yield* Effect.promise(() => - Server.listen({ - port, - hostname, - username: "opencode", - password, - cors: ["oc://renderer"], - }), - ) +export const spawnLocalServerEffect = Effect.fn("Server.spawnLocalServer")(function* ( + hostname: string, + port: number, + password: string, +) { + prepareServerEnv(password) + const { Log, Server } = yield* Effect.promise( + () => import("virtual:opencode-server") as Promise, + ) + yield* Effect.promise(() => Log.init({ level: "WARN" })) + const listener = yield* Effect.promise(() => + Server.listen({ + port, + hostname, + username: "opencode", + password, + cors: ["oc://renderer"], + }), + ) - const healthCheck = Effect.gen(function* () { - const url = `http://${hostname}:${port}` - while (true) { - const healthy = yield* checkHealthEffect(url, password) - if (healthy) return - yield* Effect.sleep("100 millis") - } - }) - - return { listener, health: healthCheck } - }, -) - -const prepareServerEnv = (password: string) => - Effect.sync(() => { - const shell = process.platform === "win32" ? null : getUserShell() - const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {} - const env = { - ...process.env, - ...shellEnv, - OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true", - OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", - OPENCODE_CLIENT: "desktop", - OPENCODE_SERVER_USERNAME: "opencode", - OPENCODE_SERVER_PASSWORD: password, - XDG_STATE_HOME: app.getPath("userData"), + const healthCheck = Effect.gen(function* () { + const url = `http://${hostname}:${port}` + while (true) { + const healthy = yield* checkHealthEffect(url, password) + if (healthy) return + yield* Effect.sleep("100 millis") } - Object.assign(process.env, env) }) + return { listener, health: healthCheck } +}) + +const prepareServerEnv = (password: string) => () => { + const shell = process.platform === "win32" ? null : getUserShell() + const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {} + const env = { + ...process.env, + ...shellEnv, + OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true", + OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", + OPENCODE_CLIENT: "desktop", + OPENCODE_SERVER_USERNAME: "opencode", + OPENCODE_SERVER_PASSWORD: password, + XDG_STATE_HOME: app.getPath("userData"), + } + Object.assign(process.env, env) +} + export const checkHealthEffect = Effect.fn("Server.checkHealth")(function* (url: string, password?: string | null) { + const httpClient = yield* HttpClient.HttpClient + let healthUrl: URL try { healthUrl = new URL("/global/health", url) @@ -89,16 +91,12 @@ export const checkHealthEffect = Effect.fn("Server.checkHealth")(function* (url: headers.set("authorization", `Basic ${auth}`) } - try { - const res = yield* Effect.promise(() => - fetch(healthUrl, { - method: "GET", - headers, - signal: AbortSignal.timeout(3000), - }), + return yield* httpClient + .get(healthUrl, { headers }) + .pipe( + Effect.timeout("3 seconds"), + Effect.flatMap(HttpClientResponse.filterStatusOk), + Effect.option, + Effect.map(Option.isSome), ) - return res.ok - } catch { - return false - } })