entry.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. // @refresh reload
  2. import * as Sentry from "@sentry/solid"
  3. import { render } from "solid-js/web"
  4. import { AppBaseProviders, AppInterface } from "@/app"
  5. import { type Platform, PlatformProvider } from "@/context/platform"
  6. import { dict as en } from "@/i18n/en"
  7. import { dict as zh } from "@/i18n/zh"
  8. import { handleNotificationClick } from "@/utils/notification-click"
  9. import { authFromToken } from "@/utils/server"
  10. import pkg from "../package.json"
  11. import { ServerConnection } from "./context/server"
  12. const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
  13. const getLocale = () => {
  14. if (typeof navigator !== "object") return "en" as const
  15. const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
  16. for (const language of languages) {
  17. if (!language) continue
  18. if (language.toLowerCase().startsWith("zh")) return "zh" as const
  19. }
  20. return "en" as const
  21. }
  22. const getRootNotFoundError = () => {
  23. const key = "error.dev.rootNotFound" as const
  24. const locale = getLocale()
  25. return locale === "zh" ? (zh[key] ?? en[key]) : en[key]
  26. }
  27. const getStorage = (key: string) => {
  28. if (typeof localStorage === "undefined") return null
  29. try {
  30. return localStorage.getItem(key)
  31. } catch {
  32. return null
  33. }
  34. }
  35. const setStorage = (key: string, value: string | null) => {
  36. if (typeof localStorage === "undefined") return
  37. try {
  38. if (value !== null) {
  39. localStorage.setItem(key, value)
  40. return
  41. }
  42. localStorage.removeItem(key)
  43. } catch {
  44. return
  45. }
  46. }
  47. const readDefaultServerUrl = () => getStorage(DEFAULT_SERVER_URL_KEY)
  48. const writeDefaultServerUrl = (url: string | null) => setStorage(DEFAULT_SERVER_URL_KEY, url)
  49. const notify: Platform["notify"] = async (title, description, href) => {
  50. if (!("Notification" in window)) return
  51. const permission =
  52. Notification.permission === "default"
  53. ? await Notification.requestPermission().catch(() => "denied")
  54. : Notification.permission
  55. if (permission !== "granted") return
  56. const inView = document.visibilityState === "visible" && document.hasFocus()
  57. if (inView) return
  58. const notification = new Notification(title, {
  59. body: description ?? "",
  60. icon: "https://opencode.ai/favicon-96x96-v3.png",
  61. })
  62. notification.onclick = () => {
  63. handleNotificationClick(href)
  64. notification.close()
  65. }
  66. }
  67. const openLink: Platform["openLink"] = (url) => {
  68. window.open(url, "_blank")
  69. }
  70. const back: Platform["back"] = () => {
  71. window.history.back()
  72. }
  73. const forward: Platform["forward"] = () => {
  74. window.history.forward()
  75. }
  76. const restart: Platform["restart"] = async () => {
  77. window.location.reload()
  78. }
  79. const root = document.getElementById("root")
  80. if (!(root instanceof HTMLElement) && import.meta.env.DEV) {
  81. throw new Error(getRootNotFoundError())
  82. }
  83. const getCurrentUrl = () => {
  84. if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
  85. if (import.meta.env.DEV)
  86. return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
  87. return location.origin
  88. }
  89. const getDefaultUrl = () => {
  90. const lsDefault = readDefaultServerUrl()
  91. if (lsDefault) return lsDefault
  92. return getCurrentUrl()
  93. }
  94. const clearAuthToken = () => {
  95. const params = new URLSearchParams(location.search)
  96. if (!params.has("auth_token")) return
  97. params.delete("auth_token")
  98. history.replaceState(null, "", location.pathname + (params.size ? `?${params}` : "") + location.hash)
  99. }
  100. const platform: Platform = {
  101. platform: "web",
  102. version: pkg.version,
  103. openLink,
  104. back,
  105. forward,
  106. restart,
  107. notify,
  108. getDefaultServer: async () => {
  109. const stored = readDefaultServerUrl()
  110. return stored ? ServerConnection.Key.make(stored) : null
  111. },
  112. setDefaultServer: writeDefaultServerUrl,
  113. }
  114. if (import.meta.env.VITE_SENTRY_DSN) {
  115. Sentry.init({
  116. dsn: import.meta.env.VITE_SENTRY_DSN,
  117. environment: import.meta.env.VITE_SENTRY_ENVIRONMENT ?? import.meta.env.MODE,
  118. release: import.meta.env.VITE_SENTRY_RELEASE ?? `web@${pkg.version}`,
  119. initialScope: {
  120. tags: {
  121. platform: "web",
  122. },
  123. },
  124. integrations: (integrations) => {
  125. return integrations.filter(
  126. (i) =>
  127. i.name !== "Breadcrumbs" && !(import.meta.env.OPENCODE_CHANNEL === "prod" && i.name === "GlobalHandlers"),
  128. )
  129. },
  130. })
  131. }
  132. if (root instanceof HTMLElement) {
  133. const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
  134. clearAuthToken()
  135. const server: ServerConnection.Http = {
  136. type: "http",
  137. authToken: !!auth,
  138. http: {
  139. url: getCurrentUrl(),
  140. ...auth,
  141. },
  142. }
  143. render(
  144. () => (
  145. <PlatformProvider value={platform}>
  146. <AppBaseProviders>
  147. <AppInterface
  148. defaultServer={ServerConnection.Key.make(getDefaultUrl())}
  149. canonicalLocalServer={ServerConnection.key(server)}
  150. servers={[server]}
  151. disableHealthCheck
  152. />
  153. </AppBaseProviders>
  154. </PlatformProvider>
  155. ),
  156. root,
  157. )
  158. }