desktop: add electron version (#15663)
This commit is contained in:
parent
e4f0825c56
commit
5cf235fa6c
223 changed files with 4292 additions and 46 deletions
312
packages/desktop-electron/src/renderer/index.tsx
Normal file
312
packages/desktop-electron/src/renderer/index.tsx
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
// @refresh reload
|
||||
|
||||
import {
|
||||
AppBaseProviders,
|
||||
AppInterface,
|
||||
handleNotificationClick,
|
||||
type Platform,
|
||||
PlatformProvider,
|
||||
ServerConnection,
|
||||
useCommand,
|
||||
} from "@opencode-ai/app"
|
||||
import { Splash } from "@opencode-ai/ui/logo"
|
||||
import type { AsyncStorage } from "@solid-primitives/storage"
|
||||
import { type Accessor, createResource, type JSX, onCleanup, onMount, Show } from "solid-js"
|
||||
import { render } from "solid-js/web"
|
||||
import { MemoryRouter } from "@solidjs/router"
|
||||
import pkg from "../../package.json"
|
||||
import { initI18n, t } from "./i18n"
|
||||
import { UPDATER_ENABLED } from "./updater"
|
||||
import { webviewZoom } from "./webview-zoom"
|
||||
import "./styles.css"
|
||||
import type { ServerReadyData } from "../preload/types"
|
||||
|
||||
const root = document.getElementById("root")
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
throw new Error(t("error.dev.rootNotFound"))
|
||||
}
|
||||
|
||||
void initI18n()
|
||||
|
||||
const deepLinkEvent = "opencode:deep-link"
|
||||
|
||||
const emitDeepLinks = (urls: string[]) => {
|
||||
if (urls.length === 0) return
|
||||
window.__OPENCODE__ ??= {}
|
||||
const pending = window.__OPENCODE__.deepLinks ?? []
|
||||
window.__OPENCODE__.deepLinks = [...pending, ...urls]
|
||||
window.dispatchEvent(new CustomEvent(deepLinkEvent, { detail: { urls } }))
|
||||
}
|
||||
|
||||
const listenForDeepLinks = () => {
|
||||
const startUrls = window.__OPENCODE__?.deepLinks ?? []
|
||||
if (startUrls.length) emitDeepLinks(startUrls)
|
||||
return window.api.onDeepLink((urls) => emitDeepLinks(urls))
|
||||
}
|
||||
|
||||
const createPlatform = (): Platform => {
|
||||
const os = (() => {
|
||||
const ua = navigator.userAgent
|
||||
if (ua.includes("Mac")) return "macos"
|
||||
if (ua.includes("Windows")) return "windows"
|
||||
if (ua.includes("Linux")) return "linux"
|
||||
return undefined
|
||||
})()
|
||||
|
||||
const wslHome = async () => {
|
||||
if (os !== "windows" || !window.__OPENCODE__?.wsl) return undefined
|
||||
return window.api.wslPath("~", "windows").catch(() => undefined)
|
||||
}
|
||||
|
||||
const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
|
||||
if (!result || !window.__OPENCODE__?.wsl) return result
|
||||
if (Array.isArray(result)) {
|
||||
return Promise.all(result.map((path) => window.api.wslPath(path, "linux").catch(() => path))) as any
|
||||
}
|
||||
return window.api.wslPath(result, "linux").catch(() => result) as any
|
||||
}
|
||||
|
||||
const storage = (() => {
|
||||
const cache = new Map<string, AsyncStorage>()
|
||||
|
||||
const createStorage = (name: string) => {
|
||||
const api: AsyncStorage = {
|
||||
getItem: (key: string) => window.api.storeGet(name, key),
|
||||
setItem: (key: string, value: string) => window.api.storeSet(name, key, value),
|
||||
removeItem: (key: string) => window.api.storeDelete(name, key),
|
||||
clear: () => window.api.storeClear(name),
|
||||
key: async (index: number) => (await window.api.storeKeys(name))[index],
|
||||
getLength: () => window.api.storeLength(name),
|
||||
get length() {
|
||||
return api.getLength()
|
||||
},
|
||||
}
|
||||
return api
|
||||
}
|
||||
|
||||
return (name = "default.dat") => {
|
||||
const cached = cache.get(name)
|
||||
if (cached) return cached
|
||||
const api = createStorage(name)
|
||||
cache.set(name, api)
|
||||
return api
|
||||
}
|
||||
})()
|
||||
|
||||
return {
|
||||
platform: "desktop",
|
||||
os,
|
||||
version: pkg.version,
|
||||
|
||||
async openDirectoryPickerDialog(opts) {
|
||||
const defaultPath = await wslHome()
|
||||
const result = await window.api.openDirectoryPicker({
|
||||
multiple: opts?.multiple ?? false,
|
||||
title: opts?.title ?? t("desktop.dialog.chooseFolder"),
|
||||
defaultPath,
|
||||
})
|
||||
return await handleWslPicker(result)
|
||||
},
|
||||
|
||||
async openFilePickerDialog(opts) {
|
||||
const result = await window.api.openFilePicker({
|
||||
multiple: opts?.multiple ?? false,
|
||||
title: opts?.title ?? t("desktop.dialog.chooseFile"),
|
||||
})
|
||||
return handleWslPicker(result)
|
||||
},
|
||||
|
||||
async saveFilePickerDialog(opts) {
|
||||
const result = await window.api.saveFilePicker({
|
||||
title: opts?.title ?? t("desktop.dialog.saveFile"),
|
||||
defaultPath: opts?.defaultPath,
|
||||
})
|
||||
return handleWslPicker(result)
|
||||
},
|
||||
|
||||
openLink(url: string) {
|
||||
window.api.openLink(url)
|
||||
},
|
||||
async openPath(path: string, app?: string) {
|
||||
if (os === "windows") {
|
||||
const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
|
||||
const resolvedPath = await (async () => {
|
||||
if (window.__OPENCODE__?.wsl) {
|
||||
const converted = await window.api.wslPath(path, "windows").catch(() => null)
|
||||
if (converted) return converted
|
||||
}
|
||||
return path
|
||||
})()
|
||||
return window.api.openPath(resolvedPath, resolvedApp ?? undefined)
|
||||
}
|
||||
return window.api.openPath(path, app)
|
||||
},
|
||||
|
||||
back() {
|
||||
window.history.back()
|
||||
},
|
||||
|
||||
forward() {
|
||||
window.history.forward()
|
||||
},
|
||||
|
||||
storage,
|
||||
|
||||
checkUpdate: async () => {
|
||||
if (!UPDATER_ENABLED) return { updateAvailable: false }
|
||||
return window.api.checkUpdate()
|
||||
},
|
||||
|
||||
update: async () => {
|
||||
if (!UPDATER_ENABLED) return
|
||||
await window.api.installUpdate()
|
||||
},
|
||||
|
||||
restart: async () => {
|
||||
await window.api.killSidecar().catch(() => undefined)
|
||||
window.api.relaunch()
|
||||
},
|
||||
|
||||
notify: async (title, description, href) => {
|
||||
const focused = await window.api.getWindowFocused().catch(() => document.hasFocus())
|
||||
if (focused) return
|
||||
|
||||
const notification = new Notification(title, {
|
||||
body: description ?? "",
|
||||
icon: "https://opencode.ai/favicon-96x96-v3.png",
|
||||
})
|
||||
notification.onclick = () => {
|
||||
void window.api.showWindow()
|
||||
void window.api.setWindowFocus()
|
||||
handleNotificationClick(href)
|
||||
notification.close()
|
||||
}
|
||||
},
|
||||
|
||||
fetch: (input, init) => {
|
||||
if (input instanceof Request) return fetch(input)
|
||||
return fetch(input, init)
|
||||
},
|
||||
|
||||
getWslEnabled: async () => {
|
||||
const next = await window.api.getWslConfig().catch(() => null)
|
||||
if (next) return next.enabled
|
||||
return window.__OPENCODE__!.wsl ?? false
|
||||
},
|
||||
|
||||
setWslEnabled: async (enabled) => {
|
||||
await window.api.setWslConfig({ enabled })
|
||||
},
|
||||
|
||||
getDefaultServerUrl: async () => {
|
||||
return window.api.getDefaultServerUrl().catch(() => null)
|
||||
},
|
||||
|
||||
setDefaultServerUrl: async (url: string | null) => {
|
||||
await window.api.setDefaultServerUrl(url)
|
||||
},
|
||||
|
||||
getDisplayBackend: async () => {
|
||||
return window.api.getDisplayBackend().catch(() => null)
|
||||
},
|
||||
|
||||
setDisplayBackend: async (backend) => {
|
||||
await window.api.setDisplayBackend(backend)
|
||||
},
|
||||
|
||||
parseMarkdown: (markdown: string) => window.api.parseMarkdownCommand(markdown),
|
||||
|
||||
webviewZoom,
|
||||
|
||||
checkAppExists: async (appName: string) => {
|
||||
return window.api.checkAppExists(appName)
|
||||
},
|
||||
|
||||
async readClipboardImage() {
|
||||
const image = await window.api.readClipboardImage().catch(() => null)
|
||||
if (!image) return null
|
||||
const blob = new Blob([image.buffer], { type: "image/png" })
|
||||
return new File([blob], `pasted-image-${Date.now()}.png`, { type: "image/png" })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let menuTrigger = null as null | ((id: string) => void)
|
||||
window.api.onMenuCommand((id) => {
|
||||
menuTrigger?.(id)
|
||||
})
|
||||
listenForDeepLinks()
|
||||
|
||||
render(() => {
|
||||
const platform = createPlatform()
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
|
||||
if (link?.href) {
|
||||
e.preventDefault()
|
||||
platform.openLink(link.href)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("click", handleClick)
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("click", handleClick)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<PlatformProvider value={platform}>
|
||||
<AppBaseProviders>
|
||||
<ServerGate>
|
||||
{(data) => {
|
||||
const server: ServerConnection.Sidecar = {
|
||||
displayName: "Local Server",
|
||||
type: "sidecar",
|
||||
variant: "base",
|
||||
http: {
|
||||
url: data().url,
|
||||
username: "opencode",
|
||||
password: data().password ?? undefined,
|
||||
},
|
||||
}
|
||||
|
||||
function Inner() {
|
||||
const cmd = useCommand()
|
||||
|
||||
menuTrigger = (id) => cmd.trigger(id)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<AppInterface defaultServer={ServerConnection.key(server)} servers={[server]} router={MemoryRouter}>
|
||||
<Inner />
|
||||
</AppInterface>
|
||||
)
|
||||
}}
|
||||
</ServerGate>
|
||||
</AppBaseProviders>
|
||||
</PlatformProvider>
|
||||
)
|
||||
}, root!)
|
||||
|
||||
// Gate component that waits for the server to be ready
|
||||
function ServerGate(props: { children: (data: Accessor<ServerReadyData>) => JSX.Element }) {
|
||||
const [serverData] = createResource(() => window.api.awaitInitialization(() => undefined))
|
||||
console.log({ serverData })
|
||||
if (serverData.state === "errored") throw serverData.error
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={serverData.state !== "pending" && serverData()}
|
||||
fallback={
|
||||
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{(data) => props.children(data)}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue