From f53ff758a6dc2d80c815589f3257098d9de684bc Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 21 Apr 2026 19:45:38 +0800 Subject: [PATCH 1/5] chore(desktop): remove Tauri migration leftovers --- packages/desktop-electron/README.md | 28 +++--- packages/desktop-electron/src/main/migrate.ts | 91 ------------------- .../src/main/tauri-cleanup.test.ts | 21 +++++ 3 files changed, 37 insertions(+), 103 deletions(-) delete mode 100644 packages/desktop-electron/src/main/migrate.ts create mode 100644 packages/desktop-electron/src/main/tauri-cleanup.test.ts diff --git a/packages/desktop-electron/README.md b/packages/desktop-electron/README.md index ebaf48822..9993ac1d4 100644 --- a/packages/desktop-electron/README.md +++ b/packages/desktop-electron/README.md @@ -1,6 +1,6 @@ -# OpenCode Desktop +# PawWork Desktop -Native OpenCode desktop app, built with Tauri v2. +Native PawWork desktop app, built with Electron. ## Development @@ -8,25 +8,29 @@ From the repo root: ```bash bun install -bun run --cwd packages/desktop tauri dev +bun run --cwd packages/desktop-electron dev ``` -This starts the Vite dev server on http://localhost:1420 and opens the native window. +This starts the Electron shell and renderer development server. -If you only want the web dev server (no native shell): +## Build + +To build the Electron main process, preload script, and renderer: ```bash -bun run --cwd packages/desktop dev +bun run --cwd packages/desktop-electron build ``` -## Build - -To create a production `dist/` and build the native app bundle: +To create a local desktop package: ```bash -bun run --cwd packages/desktop tauri build +bun run --cwd packages/desktop-electron package ``` -## Prerequisites +For platform-specific packages: -Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions. +```bash +bun run --cwd packages/desktop-electron package:mac +bun run --cwd packages/desktop-electron package:win +bun run --cwd packages/desktop-electron package:linux +``` diff --git a/packages/desktop-electron/src/main/migrate.ts b/packages/desktop-electron/src/main/migrate.ts deleted file mode 100644 index bad1349ee..000000000 --- a/packages/desktop-electron/src/main/migrate.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { app } from "electron" -import log from "electron-log/main.js" -import { existsSync, readdirSync, readFileSync } from "node:fs" -import { homedir } from "node:os" -import { join } from "node:path" -import { CHANNEL } from "./constants" -import { getStore, store } from "./store" - -const TAURI_MIGRATED_KEY = "tauriMigrated" - -// Resolve the directory where Tauri stored its .dat files for the given app identifier. -// Mirrors Tauri's AppLocalData / AppData resolution per OS. -function tauriDir(id: string) { - switch (process.platform) { - case "darwin": - return join(homedir(), "Library", "Application Support", id) - case "win32": - return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), id) - default: - return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), id) - } -} - -// The Tauri app identifier changes between dev/beta/prod builds. -const TAURI_APP_IDS: Record = { - dev: "ai.opencode.desktop.dev", - beta: "ai.opencode.desktop.beta", - prod: "ai.opencode.desktop", -} -function tauriAppId() { - return app.isPackaged ? TAURI_APP_IDS[CHANNEL] : "ai.opencode.desktop.dev" -} - -// Migrate a single Tauri .dat file into the corresponding electron-store. -// `opencode.settings.dat` is special: it maps to the `opencode.settings` store -// (the electron-store name without the `.dat` extension). All other .dat files -// keep their full filename as the electron-store name so they match what the -// renderer already passes via IPC (e.g. `"default.dat"`, `"opencode.global.dat"`). -function migrateFile(datPath: string, filename: string) { - let data: Record - try { - data = JSON.parse(readFileSync(datPath, "utf-8")) - } catch (err) { - log.warn("tauri migration: failed to parse", filename, err) - return - } - - // opencode.settings.dat → the electron settings store ("opencode.settings"). - // All other .dat files keep their full filename as the store name so they match - // what the renderer passes via IPC (e.g. "default.dat", "opencode.global.dat"). - const storeName = filename === "opencode.settings.dat" ? "opencode.settings" : filename - const target = getStore(storeName) - const migrated: string[] = [] - const skipped: string[] = [] - - for (const [key, value] of Object.entries(data)) { - // Don't overwrite values the user has already set in the Electron app. - if (target.has(key)) { - skipped.push(key) - continue - } - target.set(key, value) - migrated.push(key) - } - - log.log("tauri migration: migrated", filename, "→", storeName, { migrated, skipped }) -} - -export function migrate() { - if (store.get(TAURI_MIGRATED_KEY)) { - log.log("tauri migration: already done, skipping") - return - } - - const dir = tauriDir(tauriAppId()) - log.log("tauri migration: starting", { dir }) - - if (!existsSync(dir)) { - log.log("tauri migration: no tauri data directory found, nothing to migrate") - store.set(TAURI_MIGRATED_KEY, true) - return - } - - for (const filename of readdirSync(dir)) { - if (!filename.endsWith(".dat")) continue - migrateFile(join(dir, filename), filename) - } - - log.log("tauri migration: complete") - store.set(TAURI_MIGRATED_KEY, true) -} diff --git a/packages/desktop-electron/src/main/tauri-cleanup.test.ts b/packages/desktop-electron/src/main/tauri-cleanup.test.ts new file mode 100644 index 000000000..d0673c909 --- /dev/null +++ b/packages/desktop-electron/src/main/tauri-cleanup.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test" +import { existsSync, readFileSync } from "node:fs" +import { join } from "node:path" +import { fileURLToPath } from "node:url" + +const packageRoot = fileURLToPath(new URL("../../", import.meta.url)) + +describe("Tauri cleanup", () => { + test("does not keep the old Tauri data migration module", () => { + expect(existsSync(join(packageRoot, "src/main/migrate.ts"))).toBe(false) + }) + + test("desktop README documents Electron commands, not Tauri commands", () => { + const readme = readFileSync(join(packageRoot, "README.md"), "utf8") + + expect(readme).toContain("bun run --cwd packages/desktop-electron dev") + expect(readme).toContain("bun run --cwd packages/desktop-electron package") + expect(readme).not.toMatch(/tauri/i) + expect(readme).not.toMatch(/packages\/desktop\s+tauri/) + }) +}) From ab3f6777672ec1bfa17718afc32f2f3a1e6b37bc Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 21 Apr 2026 19:47:33 +0800 Subject: [PATCH 2/5] chore(app): rename Tauri drag regions --- .../components/titlebar-tauri-compat.test.ts | 21 +++++ packages/app/src/components/titlebar.tsx | 86 +------------------ packages/ui/src/styles/base.css | 18 ++-- 3 files changed, 34 insertions(+), 91 deletions(-) create mode 100644 packages/app/src/components/titlebar-tauri-compat.test.ts diff --git a/packages/app/src/components/titlebar-tauri-compat.test.ts b/packages/app/src/components/titlebar-tauri-compat.test.ts new file mode 100644 index 000000000..fc9a14058 --- /dev/null +++ b/packages/app/src/components/titlebar-tauri-compat.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test" + +const titlebarSource = await Bun.file(new URL("./titlebar.tsx", import.meta.url)).text() +const baseCssSource = await Bun.file(new URL("../../../ui/src/styles/base.css", import.meta.url)).text() + +describe("titlebar Tauri compatibility cleanup", () => { + test("titlebar no longer references Tauri globals or Tauri DOM attributes", () => { + expect(titlebarSource).not.toContain("__TAURI__") + expect(titlebarSource).not.toContain("tauriApi") + expect(titlebarSource).not.toContain("data-tauri") + expect(titlebarSource).not.toContain("Tauri") + }) + + test("titlebar uses shell-neutral drag regions", () => { + expect(titlebarSource).toContain("data-shell-drag-region") + expect(baseCssSource).toContain("[data-shell-drag-region]") + expect(baseCssSource).toContain("app-region: drag") + expect(baseCssSource).toContain("app-region: no-drag") + expect(baseCssSource).not.toContain("data-tauri-drag-region") + }) +}) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index cc983abae..88ff301ee 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -1,11 +1,10 @@ -import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js" +import { createEffect, createMemo, Show, untrack } from "solid-js" import { createStore } from "solid-js/store" import { useLocation, useNavigate, useParams } from "@solidjs/router" import { IconButton } from "@opencode-ai/ui/icon-button" import { Icon } from "@opencode-ai/ui/icon" import { Button } from "@opencode-ai/ui/button" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { useTheme } from "@opencode-ai/ui/theme/context" import { useLayout } from "@/context/layout" import { usePlatform } from "@/context/platform" @@ -13,34 +12,11 @@ import { useCommand } from "@/context/command" import { useLanguage } from "@/context/language" import { applyPath, backPath, forwardPath } from "./titlebar-history" -type TauriDesktopWindow = { - startDragging?: () => Promise - toggleMaximize?: () => Promise -} - -type TauriThemeWindow = { - setTheme?: (theme?: "light" | "dark" | null) => Promise -} - -type TauriApi = { - window?: { - getCurrentWindow?: () => TauriDesktopWindow - } - webviewWindow?: { - getCurrentWebviewWindow?: () => TauriThemeWindow - } -} - -const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__ -const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.() -const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.() - export function Titlebar() { const layout = useLayout() const platform = usePlatform() const command = useCommand() const language = useLanguage() - const theme = useTheme() const navigate = useNavigate() const location = useLocation() const params = useParams() @@ -111,56 +87,6 @@ export function Titlebar() { }, ]) - const getWin = () => { - if (platform.platform !== "desktop") return - return currentDesktopWindow() - } - - createEffect(() => { - if (platform.platform !== "desktop") return - - const scheme = theme.colorScheme() - const value = scheme === "system" ? null : scheme - - const win = currentThemeWindow() - if (!win?.setTheme) return - - void win.setTheme(value).catch(() => undefined) - }) - - const interactive = (target: EventTarget | null) => { - if (!(target instanceof Element)) return false - - const selector = - "button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']" - - return !!target.closest(selector) - } - - const drag = (e: MouseEvent) => { - if (platform.platform !== "desktop") return - if (e.buttons !== 1) return - if (interactive(e.target)) return - - const win = getWin() - if (!win?.startDragging) return - - e.preventDefault() - void win.startDragging().catch(() => undefined) - } - - const maximize = (e: MouseEvent) => { - if (platform.platform !== "desktop") return - if (interactive(e.target)) return - if (e.target instanceof Element && e.target.closest("[data-tauri-decorum-tb]")) return - - const win = getWin() - if (!win?.toggleMaximize) return - - e.preventDefault() - void win.toggleMaximize().catch(() => undefined) - } - return (
- {!tauriApi() &&
} -
+
diff --git a/packages/ui/src/styles/base.css b/packages/ui/src/styles/base.css index a032f9ea2..7becacc1f 100644 --- a/packages/ui/src/styles/base.css +++ b/packages/ui/src/styles/base.css @@ -82,18 +82,18 @@ a { cursor: default; } -#root:not([aria-hidden]) *[data-tauri-drag-region] { +#root:not([aria-hidden]) *[data-shell-drag-region] { app-region: drag; } -*[data-tauri-drag-region] button, -*[data-tauri-drag-region] a, -*[data-tauri-drag-region] input, -*[data-tauri-drag-region] textarea, -*[data-tauri-drag-region] select, -*[data-tauri-drag-region] [role="button"], -*[data-tauri-drag-region] [role="menuitem"], -*[data-tauri-drag-region] [contenteditable] { +*[data-shell-drag-region] button, +*[data-shell-drag-region] a, +*[data-shell-drag-region] input, +*[data-shell-drag-region] textarea, +*[data-shell-drag-region] select, +*[data-shell-drag-region] [role="button"], +*[data-shell-drag-region] [role="menuitem"], +*[data-shell-drag-region] [contenteditable] { app-region: no-drag; } From 1b849f88f80c05946786033d9109dc21d81db1cd Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 21 Apr 2026 19:48:29 +0800 Subject: [PATCH 3/5] chore(server): drop Tauri CORS origins --- packages/opencode/src/server/middleware.ts | 2 -- .../test/server/cors-middleware.test.ts | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 packages/opencode/test/server/cors-middleware.test.ts diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index 54aaa5b0e..3657ecde3 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -73,8 +73,6 @@ export function CorsMiddleware(opts?: { cors?: string[] }): MiddlewareHandler { if (input.startsWith("http://localhost:")) return input if (input.startsWith("http://127.0.0.1:")) return input - if (input === "tauri://localhost" || input === "http://tauri.localhost" || input === "https://tauri.localhost") - return input if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) return input if (opts?.cors?.includes(input)) return input diff --git a/packages/opencode/test/server/cors-middleware.test.ts b/packages/opencode/test/server/cors-middleware.test.ts new file mode 100644 index 000000000..810630421 --- /dev/null +++ b/packages/opencode/test/server/cors-middleware.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test } from "bun:test" +import { Server } from "../../src/server/server" +import { Log } from "../../src/util/log" + +Log.init({ print: false }) + +describe("CORS middleware", () => { + test("allows localhost browser origins", async () => { + const app = Server.Default().app + const response = await app.request("/global/health", { + headers: { + Origin: "http://localhost:5173", + }, + }) + + expect(response.status).toBe(200) + expect(response.headers.get("access-control-allow-origin")).toBe("http://localhost:5173") + }) + + test("does not allow legacy Tauri origins", async () => { + const app = Server.Default().app + const origins = ["tauri://localhost", "http://tauri.localhost", "https://tauri.localhost"] + + for (const origin of origins) { + const response = await app.request("/global/health", { + headers: { + Origin: origin, + }, + }) + + expect(response.status).toBe(200) + expect(response.headers.get("access-control-allow-origin")).toBeNull() + } + }) +}) From ab0573c35e49e0ebca3c1fd8d721f04b17cf2c1f Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 21 Apr 2026 20:04:59 +0800 Subject: [PATCH 4/5] test: address Tauri cleanup review feedback --- .../components/titlebar-tauri-compat.test.ts | 17 ++++- .../src/main/tauri-cleanup.test.ts | 1 - .../test/server/cors-middleware.test.ts | 64 ++++++++++++------- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/packages/app/src/components/titlebar-tauri-compat.test.ts b/packages/app/src/components/titlebar-tauri-compat.test.ts index fc9a14058..5a7bc761f 100644 --- a/packages/app/src/components/titlebar-tauri-compat.test.ts +++ b/packages/app/src/components/titlebar-tauri-compat.test.ts @@ -1,7 +1,20 @@ import { describe, expect, test } from "bun:test" -const titlebarSource = await Bun.file(new URL("./titlebar.tsx", import.meta.url)).text() -const baseCssSource = await Bun.file(new URL("../../../ui/src/styles/base.css", import.meta.url)).text() +async function readSource(path: string, description: string) { + const url = new URL(path, import.meta.url) + + try { + return await Bun.file(url).text() + } catch (err) { + throw new Error(`Expected ${description} at ${url.pathname}. Update this test if the monorepo layout changes.`, { + cause: err, + }) + } +} + +const titlebarSource = await readSource("./titlebar.tsx", "titlebar component source") +// Monorepo layout: packages/app/src/components -> packages/ui/src/styles. +const baseCssSource = await readSource("../../../ui/src/styles/base.css", "shared UI base CSS") describe("titlebar Tauri compatibility cleanup", () => { test("titlebar no longer references Tauri globals or Tauri DOM attributes", () => { diff --git a/packages/desktop-electron/src/main/tauri-cleanup.test.ts b/packages/desktop-electron/src/main/tauri-cleanup.test.ts index d0673c909..9421e40bd 100644 --- a/packages/desktop-electron/src/main/tauri-cleanup.test.ts +++ b/packages/desktop-electron/src/main/tauri-cleanup.test.ts @@ -16,6 +16,5 @@ describe("Tauri cleanup", () => { expect(readme).toContain("bun run --cwd packages/desktop-electron dev") expect(readme).toContain("bun run --cwd packages/desktop-electron package") expect(readme).not.toMatch(/tauri/i) - expect(readme).not.toMatch(/packages\/desktop\s+tauri/) }) }) diff --git a/packages/opencode/test/server/cors-middleware.test.ts b/packages/opencode/test/server/cors-middleware.test.ts index 810630421..cb510af95 100644 --- a/packages/opencode/test/server/cors-middleware.test.ts +++ b/packages/opencode/test/server/cors-middleware.test.ts @@ -1,35 +1,51 @@ -import { describe, expect, test } from "bun:test" +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" import { Server } from "../../src/server/server" import { Log } from "../../src/util/log" +import { testEffect } from "../lib/effect" Log.init({ print: false }) +const it = testEffect(Layer.empty) + describe("CORS middleware", () => { - test("allows localhost browser origins", async () => { - const app = Server.Default().app - const response = await app.request("/global/health", { - headers: { - Origin: "http://localhost:5173", - }, - }) + it.live("allows localhost browser origins", () => + Effect.gen(function* () { + const app = Server.Default().app + const response = yield* Effect.promise(() => + Promise.resolve( + app.request("/global/health", { + headers: { + Origin: "http://localhost:5173", + }, + }), + ), + ) - expect(response.status).toBe(200) - expect(response.headers.get("access-control-allow-origin")).toBe("http://localhost:5173") - }) + expect(response.status).toBe(200) + expect(response.headers.get("access-control-allow-origin")).toBe("http://localhost:5173") + }), + ) - test("does not allow legacy Tauri origins", async () => { - const app = Server.Default().app - const origins = ["tauri://localhost", "http://tauri.localhost", "https://tauri.localhost"] + it.live("does not allow legacy Tauri origins", () => + Effect.gen(function* () { + const app = Server.Default().app + const origins = ["tauri://localhost", "http://tauri.localhost", "https://tauri.localhost"] - for (const origin of origins) { - const response = await app.request("/global/health", { - headers: { - Origin: origin, - }, - }) + for (const origin of origins) { + const response = yield* Effect.promise(() => + Promise.resolve( + app.request("/global/health", { + headers: { + Origin: origin, + }, + }), + ), + ) - expect(response.status).toBe(200) - expect(response.headers.get("access-control-allow-origin")).toBeNull() - } - }) + expect(response.status).toBe(200) + expect(response.headers.get("access-control-allow-origin")).toBeNull() + } + }), + ) }) From 68afc727e1ccd42813c6d926fa88e0b48f38ce72 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 21 Apr 2026 20:06:26 +0800 Subject: [PATCH 5/5] chore(app): remove redundant drag attribute --- packages/app/src/components/titlebar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 88ff301ee..0935e2781 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -227,7 +227,6 @@ export function Titlebar() { "flex items-center min-w-0 justify-end": true, "pr-2": !windows(), }} - data-shell-drag-region >