| |
|
|
| import * as Sentry from "@sentry/solid" |
| import { render } from "solid-js/web" |
| import { AppBaseProviders, AppInterface } from "@/app" |
| import { loadInitialLocale } from "@/context/language" |
| import { type Platform, PlatformProvider } from "@/context/platform" |
| import { createBrowserDraftStore } from "@/utils/draft-store" |
| import { dict as en } from "@/i18n/en" |
| import { dict as zh } from "@/i18n/zh" |
| import { authFromToken } from "@/utils/server" |
| import pkg from "../package.json" |
| import { ServerConnection } from "./context/server" |
|
|
| const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl" |
|
|
| const getLocale = () => { |
| if (typeof navigator !== "object") return "en" as const |
| const languages = navigator.languages?.length ? navigator.languages : [navigator.language] |
| for (const language of languages) { |
| if (!language) continue |
| if (language.toLowerCase().startsWith("zh")) return "zh" as const |
| } |
| return "en" as const |
| } |
|
|
| const getRootNotFoundError = () => { |
| const key = "error.dev.rootNotFound" as const |
| const locale = getLocale() |
| return locale === "zh" ? (zh[key] ?? en[key]) : en[key] |
| } |
|
|
| const getStorage = (key: string) => { |
| if (typeof localStorage === "undefined") return null |
| try { |
| return localStorage.getItem(key) |
| } catch { |
| return null |
| } |
| } |
|
|
| const setStorage = (key: string, value: string | null) => { |
| if (typeof localStorage === "undefined") return |
| try { |
| if (value !== null) { |
| localStorage.setItem(key, value) |
| return |
| } |
| localStorage.removeItem(key) |
| } catch { |
| return |
| } |
| } |
|
|
| const readDefaultServerUrl = () => getStorage(DEFAULT_SERVER_URL_KEY) |
| const writeDefaultServerUrl = (url: string | null) => setStorage(DEFAULT_SERVER_URL_KEY, url) |
|
|
| const notify: Platform["notify"] = async (title, description, onClick) => { |
| if (!("Notification" in window)) return |
|
|
| const permission = |
| Notification.permission === "default" |
| ? await Notification.requestPermission().catch(() => "denied") |
| : Notification.permission |
|
|
| if (permission !== "granted") return |
|
|
| const inView = document.visibilityState === "visible" && document.hasFocus() |
| if (inView) return |
|
|
| const notification = new Notification(title, { |
| body: description ?? "", |
| icon: "https://opencode.ai/favicon-96x96-v3.png", |
| }) |
|
|
| notification.onclick = () => { |
| window.focus() |
| onClick?.() |
| notification.close() |
| } |
| } |
|
|
| const openExternal: Platform["openExternal"] = (value) => { |
| if (!URL.canParse(value)) return |
| const url = new URL(value) |
| if (url.protocol !== "http:" && url.protocol !== "https:" && url.protocol !== "mailto:") return |
| window.open(url.href, "_blank", "noopener,noreferrer") |
| } |
|
|
| const restart: Platform["restart"] = async () => { |
| window.location.reload() |
| } |
|
|
| const root = document.getElementById("root") |
| if (!(root instanceof HTMLElement) && import.meta.env.DEV) { |
| throw new Error(getRootNotFoundError()) |
| } |
|
|
| const getCurrentUrl = () => { |
| if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" |
| if (import.meta.env.DEV) |
| return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` |
| return location.origin |
| } |
|
|
| const getDefaultUrl = () => { |
| const lsDefault = readDefaultServerUrl() |
| if (lsDefault) return lsDefault |
| return getCurrentUrl() |
| } |
|
|
| const clearAuthToken = () => { |
| const params = new URLSearchParams(location.search) |
| if (!params.has("auth_token")) return |
| params.delete("auth_token") |
| history.replaceState(null, "", location.pathname + (params.size ? `?${params}` : "") + location.hash) |
| } |
|
|
| const platform: Platform = { |
| platform: "web", |
| draftStore: createBrowserDraftStore(), |
| version: pkg.version, |
| openExternal, |
| restart, |
| notify, |
| getDefaultServer: async () => { |
| const stored = readDefaultServerUrl() |
| return stored ? ServerConnection.Key.make(stored) : null |
| }, |
| setDefaultServer: writeDefaultServerUrl, |
| } |
|
|
| if (import.meta.env.VITE_SENTRY_DSN) { |
| Sentry.init({ |
| dsn: import.meta.env.VITE_SENTRY_DSN, |
| environment: import.meta.env.VITE_SENTRY_ENVIRONMENT ?? import.meta.env.MODE, |
| release: import.meta.env.VITE_SENTRY_RELEASE ?? `web@${pkg.version}`, |
| initialScope: { |
| tags: { |
| platform: "web", |
| }, |
| }, |
| integrations: (integrations) => { |
| return integrations.filter( |
| (i) => |
| i.name !== "Breadcrumbs" && !(import.meta.env.OPENCODE_CHANNEL === "prod" && i.name === "GlobalHandlers"), |
| ) |
| }, |
| }) |
| } |
|
|
| if (root instanceof HTMLElement) { |
| void loadInitialLocale().then((locale) => { |
| const auth = authFromToken(new URLSearchParams(location.search).get("auth_token")) |
| clearAuthToken() |
| const server: ServerConnection.Http = { |
| type: "http", |
| authToken: !!auth, |
| http: { |
| url: getCurrentUrl(), |
| ...auth, |
| }, |
| } |
| render( |
| () => ( |
| <PlatformProvider value={platform}> |
| <AppBaseProviders locale={locale}> |
| <AppInterface |
| defaultServer={ServerConnection.Key.make(getDefaultUrl())} |
| canonicalLocalServer={ServerConnection.key(server)} |
| servers={[server]} |
| disableHealthCheck |
| /> |
| </AppBaseProviders> |
| </PlatformProvider> |
| ), |
| root, |
| ) |
| }) |
| } |
|
|