Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/app/src/components/titlebar-tauri-compat.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
85 changes: 3 additions & 82 deletions packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,22 @@
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"
import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { applyPath, backPath, forwardPath } from "./titlebar-history"

type TauriDesktopWindow = {
startDragging?: () => Promise<void>
toggleMaximize?: () => Promise<void>
}

type TauriThemeWindow = {
setTheme?: (theme?: "light" | "dark" | null) => Promise<void>
}

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()
Expand Down Expand Up @@ -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 (
<header
data-component="titlebar-shell"
Expand All @@ -169,9 +95,7 @@ export function Titlebar() {
class="shrink-0 bg-background-base relative grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
classList={{ "h-11": platform.platform === "desktop" && !mac() }}
style={{ height: currentTitlebarHeight(), "min-height": currentTitlebarHeight() }}
data-tauri-drag-region
onMouseDown={drag}
onDblClick={maximize}
data-shell-drag-region
>
<div
classList={{
Expand Down Expand Up @@ -303,17 +227,14 @@ export function Titlebar() {
"flex items-center min-w-0 justify-end": true,
"pr-2": !windows(),
}}
data-tauri-drag-region
onMouseDown={drag}
>
<div
id="opencode-titlebar-right"
data-shell-slot="right-portal"
class="flex items-center gap-1 shrink-0 justify-end"
/>
<Show when={windows()}>
{!tauriApi() && <div class="w-36 shrink-0" />}
<div data-tauri-decorum-tb class="flex flex-row" />
<div class="w-36 shrink-0" />
</Show>
</div>
</header>
Expand Down
28 changes: 16 additions & 12 deletions packages/desktop-electron/README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
# OpenCode Desktop
# PawWork Desktop

Native OpenCode desktop app, built with Tauri v2.
Native PawWork desktop app, built with Electron.

## Development

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
```
91 changes: 0 additions & 91 deletions packages/desktop-electron/src/main/migrate.ts

This file was deleted.

20 changes: 20 additions & 0 deletions packages/desktop-electron/src/main/tauri-cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
2 changes: 0 additions & 2 deletions packages/opencode/src/server/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions packages/opencode/test/server/cors-middleware.test.ts
Original file line number Diff line number Diff line change
@@ -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",
},
}),
),
)
Comment thread
Astro-Han marked this conversation as resolved.

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()
}
}),
)
})
Loading
Loading