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..5a7bc761f --- /dev/null +++ b/packages/app/src/components/titlebar-tauri-compat.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "bun:test" + +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", () => { + 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..0935e2781 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/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..9421e40bd --- /dev/null +++ b/packages/desktop-electron/src/main/tauri-cleanup.test.ts @@ -0,0 +1,20 @@ +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) + }) +}) 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..cb510af95 --- /dev/null +++ b/packages/opencode/test/server/cors-middleware.test.ts @@ -0,0 +1,51 @@ +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", () => { + 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") + }), + ) + + 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 = 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() + } + }), + ) +}) 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; }