desktop: add electron version (#15663)

This commit is contained in:
Brendan Allan
2026-03-04 15:12:34 +08:00
committed by GitHub
parent e4f0825c56
commit 5cf235fa6c
223 changed files with 4293 additions and 47 deletions

View File

@@ -7,8 +7,8 @@ import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { Font } from "@opencode-ai/ui/font"
import { ThemeProvider } from "@opencode-ai/ui/theme"
import { MetaProvider } from "@solidjs/meta"
import { Navigate, Route, Router } from "@solidjs/router"
import { ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js"
import { BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
import { Component, ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js"
import { CommandProvider } from "@/context/command"
import { CommentsProvider } from "@/context/comments"
import { FileProvider } from "@/context/file"
@@ -28,6 +28,7 @@ import { TerminalProvider } from "@/context/terminal"
import DirectoryLayout from "@/pages/directory-layout"
import Layout from "@/pages/layout"
import { ErrorPage } from "./pages/error"
import { Dynamic } from "solid-js/web"
const Home = lazy(() => import("@/pages/home"))
const Session = lazy(() => import("@/pages/session"))
@@ -144,13 +145,15 @@ export function AppInterface(props: {
children?: JSX.Element
defaultServer: ServerConnection.Key
servers?: Array<ServerConnection.Any>
router?: Component<BaseRouterProps>
}) {
return (
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
<ServerKey>
<GlobalSDKProvider>
<GlobalSyncProvider>
<Router
<Dynamic
component={props.router ?? Router}
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
>
<Route path="/" component={HomeRoute} />
@@ -158,7 +161,7 @@ export function AppInterface(props: {
<Route path="/" component={SessionIndexRoute} />
<Route path="/session/:id?" component={SessionRoute} />
</Route>
</Router>
</Dynamic>
</GlobalSyncProvider>
</GlobalSDKProvider>
</ServerKey>

View File

@@ -157,6 +157,7 @@ export function Titlebar() {
<header
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
style={{ "min-height": minHeight() }}
data-tauri-drag-region
onMouseDown={drag}
onDblClick={maximize}
>
@@ -276,6 +277,7 @@ 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" class="flex items-center gap-1 shrink-0 justify-end" />

View File

@@ -42,6 +42,7 @@ import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { playSound, soundSrc } from "@/utils/sound"
import { createAim } from "@/utils/aim"
import { setNavigate } from "@/utils/notification-click"
import { Worktree as WorktreeState } from "@/utils/worktree"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -107,6 +108,7 @@ export default function Layout(props: ParentProps) {
const notification = useNotification()
const permission = usePermission()
const navigate = useNavigate()
setNavigate(navigate)
const providers = useProviders()
const dialog = useDialog()
const command = useCommand()

View File

@@ -1,26 +1,27 @@
import { describe, expect, test } from "bun:test"
import { handleNotificationClick } from "./notification-click"
import { afterEach, describe, expect, test } from "bun:test"
import { handleNotificationClick, setNavigate } from "./notification-click"
describe("notification click", () => {
test("focuses and navigates when href exists", () => {
const calls: string[] = []
handleNotificationClick("/abc/session/123", {
focus: () => calls.push("focus"),
location: {
assign: (href) => calls.push(href),
},
})
expect(calls).toEqual(["focus", "/abc/session/123"])
afterEach(() => {
setNavigate(undefined as any)
})
test("only focuses when href is missing", () => {
test("navigates via registered navigate function", () => {
const calls: string[] = []
handleNotificationClick(undefined, {
focus: () => calls.push("focus"),
location: {
assign: (href) => calls.push(href),
},
})
expect(calls).toEqual(["focus"])
setNavigate((href) => calls.push(href))
handleNotificationClick("/abc/session/123")
expect(calls).toEqual(["/abc/session/123"])
})
test("does not navigate when href is missing", () => {
const calls: string[] = []
setNavigate((href) => calls.push(href))
handleNotificationClick(undefined)
expect(calls).toEqual([])
})
test("falls back to location.assign without registered navigate", () => {
handleNotificationClick("/abc/session/123")
// falls back to window.location.assign — no error thrown
})
})

View File

@@ -1,12 +1,12 @@
type WindowTarget = {
focus: () => void
location: {
assign: (href: string) => void
}
let nav: ((href: string) => void) | undefined
export const setNavigate = (fn: (href: string) => void) => {
nav = fn
}
export const handleNotificationClick = (href?: string, target: WindowTarget = window) => {
target.focus()
export const handleNotificationClick = (href?: string) => {
window.focus()
if (!href) return
target.location.assign(href)
if (nav) nav(href)
else window.location.assign(href)
}

28
packages/desktop-electron/.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
out/
resources/opencode-cli*
resources/icons

View File

@@ -0,0 +1,4 @@
# Desktop package notes
- Renderer process should only call `window.api` from `src/preload`.
- Main process should register IPC handlers in `src/main/ipc.ts`.

View File

@@ -0,0 +1,32 @@
# OpenCode Desktop
Native OpenCode desktop app, built with Tauri v2.
## Development
From the repo root:
```bash
bun install
bun run --cwd packages/desktop tauri dev
```
This starts the Vite dev server on http://localhost:1420 and opens the native window.
If you only want the web dev server (no native shell):
```bash
bun run --cwd packages/desktop dev
```
## Build
To create a production `dist/` and build the native app bundle:
```bash
bun run --cwd packages/desktop tauri build
```
## Prerequisites
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.

View File

@@ -0,0 +1,97 @@
import type { Configuration } from "electron-builder"
const channel = (() => {
const raw = process.env.OPENCODE_CHANNEL
if (raw === "dev" || raw === "beta" || raw === "prod") return raw
return "dev"
})()
const getBase = (): Configuration => ({
artifactName: "opencode-electron-${os}-${arch}.${ext}",
directories: {
output: "dist",
buildResources: "resources",
},
files: ["out/**/*", "resources/**/*"],
extraResources: [
{
from: "resources/",
to: "",
filter: ["opencode-cli*"],
},
{
from: "native/",
to: "native/",
filter: ["index.js", "index.d.ts", "build/Release/mac_window.node", "swift-build/**"],
},
],
mac: {
category: "public.app-category.developer-tools",
icon: `resources/icons/icon.icns`,
hardenedRuntime: true,
gatekeeperAssess: false,
entitlements: "resources/entitlements.plist",
entitlementsInherit: "resources/entitlements.plist",
notarize: true,
target: ["dmg", "zip"],
},
dmg: {
sign: true,
},
protocols: {
name: "OpenCode",
schemes: ["opencode"],
},
win: {
icon: `resources/icons/icon.ico`,
target: ["nsis"],
},
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true,
installerIcon: `resources/icons/icon.ico`,
installerHeaderIcon: `resources/icons/icon.ico`,
},
linux: {
icon: `resources/icons`,
category: "Development",
target: ["AppImage", "deb", "rpm"],
},
})
function getConfig() {
const base = getBase()
switch (channel) {
case "dev": {
return {
...base,
appId: "ai.opencode.desktop.dev",
productName: "OpenCode Dev",
rpm: { packageName: "opencode-dev" },
}
}
case "beta": {
return {
...base,
appId: "ai.opencode.desktop.beta",
productName: "OpenCode Beta",
protocols: { name: "OpenCode Beta", schemes: ["opencode"] },
publish: { provider: "github", owner: "anomalyco", repo: "opencode-beta", channel: "latest" },
rpm: { packageName: "opencode-beta" },
}
}
case "prod": {
return {
...base,
appId: "ai.opencode.desktop",
productName: "OpenCode",
protocols: { name: "OpenCode", schemes: ["opencode"] },
publish: { provider: "github", owner: "anomalyco", repo: "opencode", channel: "latest" },
rpm: { packageName: "opencode" },
}
}
}
}
export default getConfig()

View File

@@ -0,0 +1,41 @@
import { defineConfig } from "electron-vite"
import appPlugin from "@opencode-ai/app/vite"
const channel = (() => {
const raw = process.env.OPENCODE_CHANNEL
if (raw === "dev" || raw === "beta" || raw === "prod") return raw
return "dev"
})()
export default defineConfig({
main: {
define: {
"import.meta.env.OPENCODE_CHANNEL": JSON.stringify(channel),
},
build: {
rollupOptions: {
input: { index: "src/main/index.ts" },
},
},
},
preload: {
build: {
rollupOptions: {
input: { index: "src/preload/index.ts" },
},
},
},
renderer: {
plugins: [appPlugin],
publicDir: "../app/public",
root: "src/renderer",
build: {
rollupOptions: {
input: {
main: "src/renderer/index.html",
loading: "src/renderer/loading.html",
},
},
},
},
})

View File

@@ -0,0 +1,11 @@
# Tauri Icons
Here's the process I've been using to create icons:
- Save source image as `app-icon.png` in `packages/desktop`
- `cd` to `packages/desktop`
- Run `bun tauri icon -o src-tauri/icons/{environment}`
- Use [Image2Icon](https://img2icnsapp.com/)'s 'Big Sur Icon' preset to generate an `icon.icns` file and place it in the appropriate icons folder
The Image2Icon step is necessary as the `icon.icns` generated by `app-icon.png` does not apply the shadow/padding expected by macOS,
so app icons appear larger than expected.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Some files were not shown because too many files have changed in this diff Show More