@neurocode-ai/desktop 1.18.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +19 -0
  3. package/electron-builder.config.test.ts +48 -0
  4. package/electron-builder.config.ts +146 -0
  5. package/electron.vite.config.ts +106 -0
  6. package/icons/README.md +14 -0
  7. package/icons/beta/128x128.png +0 -0
  8. package/icons/beta/128x128@2x.png +0 -0
  9. package/icons/beta/32x32.png +0 -0
  10. package/icons/beta/64x64.png +0 -0
  11. package/icons/beta/Square107x107Logo.png +0 -0
  12. package/icons/beta/Square142x142Logo.png +0 -0
  13. package/icons/beta/Square150x150Logo.png +0 -0
  14. package/icons/beta/Square284x284Logo.png +0 -0
  15. package/icons/beta/Square30x30Logo.png +0 -0
  16. package/icons/beta/Square310x310Logo.png +0 -0
  17. package/icons/beta/Square44x44Logo.png +0 -0
  18. package/icons/beta/Square71x71Logo.png +0 -0
  19. package/icons/beta/Square89x89Logo.png +0 -0
  20. package/icons/beta/StoreLogo.png +0 -0
  21. package/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  22. package/icons/beta/android/mipmap-hdpi/ic_launcher.png +0 -0
  23. package/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  24. package/icons/beta/android/mipmap-hdpi/ic_launcher_round.png +0 -0
  25. package/icons/beta/android/mipmap-mdpi/ic_launcher.png +0 -0
  26. package/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  27. package/icons/beta/android/mipmap-mdpi/ic_launcher_round.png +0 -0
  28. package/icons/beta/android/mipmap-xhdpi/ic_launcher.png +0 -0
  29. package/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  30. package/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
  31. package/icons/beta/android/mipmap-xxhdpi/ic_launcher.png +0 -0
  32. package/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  33. package/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  34. package/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
  35. package/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  36. package/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  37. package/icons/beta/android/values/ic_launcher_background.xml +4 -0
  38. package/icons/beta/dock.png +0 -0
  39. package/icons/beta/icon.icns +0 -0
  40. package/icons/beta/icon.ico +0 -0
  41. package/icons/beta/icon.png +0 -0
  42. package/icons/beta/ios/AppIcon-20x20@1x.png +0 -0
  43. package/icons/beta/ios/AppIcon-20x20@2x-1.png +0 -0
  44. package/icons/beta/ios/AppIcon-20x20@2x.png +0 -0
  45. package/icons/beta/ios/AppIcon-20x20@3x.png +0 -0
  46. package/icons/beta/ios/AppIcon-29x29@1x.png +0 -0
  47. package/icons/beta/ios/AppIcon-29x29@2x-1.png +0 -0
  48. package/icons/beta/ios/AppIcon-29x29@2x.png +0 -0
  49. package/icons/beta/ios/AppIcon-29x29@3x.png +0 -0
  50. package/icons/beta/ios/AppIcon-40x40@1x.png +0 -0
  51. package/icons/beta/ios/AppIcon-40x40@2x-1.png +0 -0
  52. package/icons/beta/ios/AppIcon-40x40@2x.png +0 -0
  53. package/icons/beta/ios/AppIcon-40x40@3x.png +0 -0
  54. package/icons/beta/ios/AppIcon-512@2x.png +0 -0
  55. package/icons/beta/ios/AppIcon-60x60@2x.png +0 -0
  56. package/icons/beta/ios/AppIcon-60x60@3x.png +0 -0
  57. package/icons/beta/ios/AppIcon-76x76@1x.png +0 -0
  58. package/icons/beta/ios/AppIcon-76x76@2x.png +0 -0
  59. package/icons/beta/ios/AppIcon-83.5x83.5@2x.png +0 -0
  60. package/icons/dev/128x128.png +0 -0
  61. package/icons/dev/128x128@2x.png +0 -0
  62. package/icons/dev/32x32.png +0 -0
  63. package/icons/dev/64x64.png +0 -0
  64. package/icons/dev/Square107x107Logo.png +0 -0
  65. package/icons/dev/Square142x142Logo.png +0 -0
  66. package/icons/dev/Square150x150Logo.png +0 -0
  67. package/icons/dev/Square284x284Logo.png +0 -0
  68. package/icons/dev/Square30x30Logo.png +0 -0
  69. package/icons/dev/Square310x310Logo.png +0 -0
  70. package/icons/dev/Square44x44Logo.png +0 -0
  71. package/icons/dev/Square71x71Logo.png +0 -0
  72. package/icons/dev/Square89x89Logo.png +0 -0
  73. package/icons/dev/StoreLogo.png +0 -0
  74. package/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  75. package/icons/dev/android/mipmap-hdpi/ic_launcher.png +0 -0
  76. package/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  77. package/icons/dev/android/mipmap-hdpi/ic_launcher_round.png +0 -0
  78. package/icons/dev/android/mipmap-mdpi/ic_launcher.png +0 -0
  79. package/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  80. package/icons/dev/android/mipmap-mdpi/ic_launcher_round.png +0 -0
  81. package/icons/dev/android/mipmap-xhdpi/ic_launcher.png +0 -0
  82. package/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  83. package/icons/dev/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
  84. package/icons/dev/android/mipmap-xxhdpi/ic_launcher.png +0 -0
  85. package/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  86. package/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  87. package/icons/dev/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
  88. package/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  89. package/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  90. package/icons/dev/android/values/ic_launcher_background.xml +4 -0
  91. package/icons/dev/dock.png +0 -0
  92. package/icons/dev/icon.icns +0 -0
  93. package/icons/dev/icon.ico +0 -0
  94. package/icons/dev/icon.png +0 -0
  95. package/icons/dev/ios/AppIcon-20x20@1x.png +0 -0
  96. package/icons/dev/ios/AppIcon-20x20@2x-1.png +0 -0
  97. package/icons/dev/ios/AppIcon-20x20@2x.png +0 -0
  98. package/icons/dev/ios/AppIcon-20x20@3x.png +0 -0
  99. package/icons/dev/ios/AppIcon-29x29@1x.png +0 -0
  100. package/icons/dev/ios/AppIcon-29x29@2x-1.png +0 -0
  101. package/icons/dev/ios/AppIcon-29x29@2x.png +0 -0
  102. package/icons/dev/ios/AppIcon-29x29@3x.png +0 -0
  103. package/icons/dev/ios/AppIcon-40x40@1x.png +0 -0
  104. package/icons/dev/ios/AppIcon-40x40@2x-1.png +0 -0
  105. package/icons/dev/ios/AppIcon-40x40@2x.png +0 -0
  106. package/icons/dev/ios/AppIcon-40x40@3x.png +0 -0
  107. package/icons/dev/ios/AppIcon-512@2x.png +0 -0
  108. package/icons/dev/ios/AppIcon-60x60@2x.png +0 -0
  109. package/icons/dev/ios/AppIcon-60x60@3x.png +0 -0
  110. package/icons/dev/ios/AppIcon-76x76@1x.png +0 -0
  111. package/icons/dev/ios/AppIcon-76x76@2x.png +0 -0
  112. package/icons/dev/ios/AppIcon-83.5x83.5@2x.png +0 -0
  113. package/icons/prod/128x128.png +0 -0
  114. package/icons/prod/128x128@2x.png +0 -0
  115. package/icons/prod/32x32.png +0 -0
  116. package/icons/prod/64x64.png +0 -0
  117. package/icons/prod/Square107x107Logo.png +0 -0
  118. package/icons/prod/Square142x142Logo.png +0 -0
  119. package/icons/prod/Square150x150Logo.png +0 -0
  120. package/icons/prod/Square284x284Logo.png +0 -0
  121. package/icons/prod/Square30x30Logo.png +0 -0
  122. package/icons/prod/Square310x310Logo.png +0 -0
  123. package/icons/prod/Square44x44Logo.png +0 -0
  124. package/icons/prod/Square71x71Logo.png +0 -0
  125. package/icons/prod/Square89x89Logo.png +0 -0
  126. package/icons/prod/StoreLogo.png +0 -0
  127. package/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  128. package/icons/prod/android/mipmap-hdpi/ic_launcher.png +0 -0
  129. package/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  130. package/icons/prod/android/mipmap-hdpi/ic_launcher_round.png +0 -0
  131. package/icons/prod/android/mipmap-mdpi/ic_launcher.png +0 -0
  132. package/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  133. package/icons/prod/android/mipmap-mdpi/ic_launcher_round.png +0 -0
  134. package/icons/prod/android/mipmap-xhdpi/ic_launcher.png +0 -0
  135. package/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  136. package/icons/prod/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
  137. package/icons/prod/android/mipmap-xxhdpi/ic_launcher.png +0 -0
  138. package/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  139. package/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  140. package/icons/prod/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
  141. package/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  142. package/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  143. package/icons/prod/android/values/ic_launcher_background.xml +4 -0
  144. package/icons/prod/dock.png +0 -0
  145. package/icons/prod/icon.icns +0 -0
  146. package/icons/prod/icon.ico +0 -0
  147. package/icons/prod/icon.png +0 -0
  148. package/icons/prod/ios/AppIcon-20x20@1x.png +0 -0
  149. package/icons/prod/ios/AppIcon-20x20@2x-1.png +0 -0
  150. package/icons/prod/ios/AppIcon-20x20@2x.png +0 -0
  151. package/icons/prod/ios/AppIcon-20x20@3x.png +0 -0
  152. package/icons/prod/ios/AppIcon-29x29@1x.png +0 -0
  153. package/icons/prod/ios/AppIcon-29x29@2x-1.png +0 -0
  154. package/icons/prod/ios/AppIcon-29x29@2x.png +0 -0
  155. package/icons/prod/ios/AppIcon-29x29@3x.png +0 -0
  156. package/icons/prod/ios/AppIcon-40x40@1x.png +0 -0
  157. package/icons/prod/ios/AppIcon-40x40@2x-1.png +0 -0
  158. package/icons/prod/ios/AppIcon-40x40@2x.png +0 -0
  159. package/icons/prod/ios/AppIcon-40x40@3x.png +0 -0
  160. package/icons/prod/ios/AppIcon-512@2x.png +0 -0
  161. package/icons/prod/ios/AppIcon-60x60@2x.png +0 -0
  162. package/icons/prod/ios/AppIcon-60x60@3x.png +0 -0
  163. package/icons/prod/ios/AppIcon-76x76@1x.png +0 -0
  164. package/icons/prod/ios/AppIcon-76x76@2x.png +0 -0
  165. package/icons/prod/ios/AppIcon-83.5x83.5@2x.png +0 -0
  166. package/package.json +75 -0
  167. package/resources/entitlements.plist +18 -0
  168. package/resources/linux/opencode-desktop.desktop +10 -0
  169. package/scripts/copy-bundles.ts +12 -0
  170. package/scripts/copy-icons.ts +12 -0
  171. package/scripts/copy-metainfo.ts +47 -0
  172. package/scripts/finalize-latest-json.ts +219 -0
  173. package/scripts/finalize-latest-yml.ts +124 -0
  174. package/scripts/prebuild.ts +10 -0
  175. package/scripts/predev.ts +5 -0
  176. package/scripts/prepare.ts +9 -0
  177. package/scripts/utils.ts +77 -0
  178. package/src/main/apps.ts +136 -0
  179. package/src/main/attachment-picker.test.ts +87 -0
  180. package/src/main/attachment-picker.ts +56 -0
  181. package/src/main/constants.ts +7 -0
  182. package/src/main/debug.ts +95 -0
  183. package/src/main/desktop-menu-actions.ts +84 -0
  184. package/src/main/env.d.ts +19 -0
  185. package/src/main/index.test.ts +37 -0
  186. package/src/main/index.ts +399 -0
  187. package/src/main/initialization.ts +6 -0
  188. package/src/main/install-state.test.ts +19 -0
  189. package/src/main/install-state.ts +8 -0
  190. package/src/main/ipc.ts +274 -0
  191. package/src/main/logging.ts +205 -0
  192. package/src/main/markdown.ts +16 -0
  193. package/src/main/menu.ts +67 -0
  194. package/src/main/migrate.ts +91 -0
  195. package/src/main/onboarding.ts +45 -0
  196. package/src/main/server.ts +238 -0
  197. package/src/main/shell-env.test.ts +50 -0
  198. package/src/main/shell-env.ts +101 -0
  199. package/src/main/sidecar.ts +157 -0
  200. package/src/main/store-cleanup.test.ts +93 -0
  201. package/src/main/store-cleanup.ts +94 -0
  202. package/src/main/store-keys.ts +7 -0
  203. package/src/main/store.ts +35 -0
  204. package/src/main/unresponsive.ts +70 -0
  205. package/src/main/updater-controller.test.ts +111 -0
  206. package/src/main/updater-controller.ts +97 -0
  207. package/src/main/updater-subscriptions.test.ts +16 -0
  208. package/src/main/updater-subscriptions.ts +20 -0
  209. package/src/main/updater.ts +85 -0
  210. package/src/main/window-registry.test.ts +91 -0
  211. package/src/main/window-registry.ts +47 -0
  212. package/src/main/window-state.ts +29 -0
  213. package/src/main/windows.ts +496 -0
  214. package/src/main/wsl/ipc.ts +103 -0
  215. package/src/main/wsl/policy.ts +33 -0
  216. package/src/main/wsl/runtime.ts +400 -0
  217. package/src/main/wsl/servers.test.ts +231 -0
  218. package/src/main/wsl/servers.ts +521 -0
  219. package/src/main/wsl/sidecar.ts +129 -0
  220. package/src/main/wsl/startup.ts +31 -0
  221. package/src/preload/index.ts +128 -0
  222. package/src/preload/types.ts +109 -0
  223. package/src/renderer/cli.ts +12 -0
  224. package/src/renderer/env.d.ts +10 -0
  225. package/src/renderer/html.test.ts +62 -0
  226. package/src/renderer/i18n/ar.ts +26 -0
  227. package/src/renderer/i18n/br.ts +27 -0
  228. package/src/renderer/i18n/bs.ts +28 -0
  229. package/src/renderer/i18n/da.ts +28 -0
  230. package/src/renderer/i18n/de.ts +28 -0
  231. package/src/renderer/i18n/en.ts +27 -0
  232. package/src/renderer/i18n/es.ts +27 -0
  233. package/src/renderer/i18n/fr.ts +28 -0
  234. package/src/renderer/i18n/index.ts +194 -0
  235. package/src/renderer/i18n/ja.ts +28 -0
  236. package/src/renderer/i18n/ko.ts +27 -0
  237. package/src/renderer/i18n/no.ts +28 -0
  238. package/src/renderer/i18n/pl.ts +28 -0
  239. package/src/renderer/i18n/ru.ts +27 -0
  240. package/src/renderer/i18n/uk.ts +28 -0
  241. package/src/renderer/i18n/zh.ts +26 -0
  242. package/src/renderer/i18n/zht.ts +26 -0
  243. package/src/renderer/index.html +21 -0
  244. package/src/renderer/index.tsx +464 -0
  245. package/src/renderer/initialization.test.ts +73 -0
  246. package/src/renderer/initialization.ts +22 -0
  247. package/src/renderer/onboarding.tsx +53 -0
  248. package/src/renderer/styles.css +0 -0
  249. package/src/renderer/webview-zoom.ts +138 -0
  250. package/src/renderer/wsl/connections.test.ts +43 -0
  251. package/src/renderer/wsl/connections.ts +28 -0
  252. package/sst-env.d.ts +10 -0
  253. package/tsconfig.json +23 -0
@@ -0,0 +1,464 @@
1
+ // @refresh reload
2
+
3
+ import {
4
+ ACCEPTED_FILE_EXTENSIONS,
5
+ AppBaseProviders,
6
+ AppInterface,
7
+ handleNotificationClick,
8
+ loadLocaleDict,
9
+ normalizeLocale,
10
+ type Locale,
11
+ type Platform,
12
+ PlatformProvider,
13
+ ServerConnection,
14
+ useCommand,
15
+ useWslServers,
16
+ } from "@neurocode-ai/app"
17
+ import type { UpdaterState } from "@neurocode-ai/app/updater"
18
+ import * as Sentry from "@sentry/solid"
19
+ import type { AsyncStorage } from "@solid-primitives/storage"
20
+ import { createMemoryHistory, MemoryRouter, type BaseRouterProps } from "@solidjs/router"
21
+ import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, Show } from "solid-js"
22
+ import { render } from "solid-js/web"
23
+ import pkg from "../../package.json"
24
+ import { initI18n, t } from "./i18n"
25
+ import { initializationData, initializationReady } from "./initialization"
26
+ import { DesktopFirstLaunchOnboarding } from "./onboarding"
27
+ import { resetZoom, setPinchZoomEnabled, webviewZoom, zoomIn, zoomOut } from "./webview-zoom"
28
+ import { availableStartupServer, readyWslConnections } from "./wsl/connections"
29
+ import "./styles.css"
30
+ import { Splash } from "@neurocode-ai/ui/logo"
31
+ import { useTheme } from "@neurocode-ai/ui/theme/context"
32
+
33
+ const root = document.getElementById("root")
34
+ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
35
+ throw new Error(t("error.dev.rootNotFound"))
36
+ }
37
+
38
+ if (import.meta.env.VITE_SENTRY_DSN) {
39
+ Sentry.init({
40
+ dsn: import.meta.env.VITE_SENTRY_DSN,
41
+ environment: import.meta.env.VITE_SENTRY_ENVIRONMENT ?? import.meta.env.MODE,
42
+ release: import.meta.env.VITE_SENTRY_RELEASE ?? `desktop@${pkg.version}`,
43
+ initialScope: {
44
+ tags: {
45
+ platform: "desktop",
46
+ },
47
+ },
48
+ integrations: (integrations) => {
49
+ return integrations.filter(
50
+ (i) =>
51
+ i.name !== "Breadcrumbs" &&
52
+ !(
53
+ import.meta.env.NEUROCODE_CHANNEL === "prod" &&
54
+ (i.name === "GlobalHandlers" || i.name === "BrowserApiErrors")
55
+ ),
56
+ )
57
+ },
58
+ })
59
+ }
60
+
61
+ void initI18n()
62
+
63
+ const [updaterState, setUpdaterState] = createSignal<UpdaterState>({ status: "disabled" })
64
+ void window.api.updater.subscribe(setUpdaterState)
65
+
66
+ const deepLinkEvent = "neurocode:deep-link"
67
+
68
+ type DesktopWindowState = {
69
+ id?: string
70
+ }
71
+
72
+ const emitDeepLinks = (urls: string[]) => {
73
+ if (urls.length === 0) return
74
+ window.__NEUROCODE__ ??= {}
75
+ const pending = window.__NEUROCODE__.deepLinks ?? []
76
+ window.__NEUROCODE__.deepLinks = [...pending, ...urls]
77
+ window.dispatchEvent(new CustomEvent(deepLinkEvent, { detail: { urls } }))
78
+ }
79
+
80
+ const listenForDeepLinks = () => {
81
+ void window.api.consumeInitialDeepLinks().then((urls) => emitDeepLinks(urls))
82
+ return window.api.onDeepLink((urls) => emitDeepLinks(urls))
83
+ }
84
+
85
+ function windowLastActiveUrlKey(windowID: string) {
86
+ return `opencode.desktop.window.${windowID}.last-active-url`
87
+ }
88
+
89
+ function getLastActiveUrl(windowID: string) {
90
+ if (typeof localStorage !== "object") return "/"
91
+ try {
92
+ const value = localStorage.getItem(windowLastActiveUrlKey(windowID))
93
+ if (value?.startsWith("/") && !value.startsWith("//")) return value
94
+ } catch {}
95
+ return "/"
96
+ }
97
+
98
+ function setLastActiveUrl(windowID: string, value: string) {
99
+ if (typeof localStorage !== "object") return
100
+ try {
101
+ localStorage.setItem(windowLastActiveUrlKey(windowID), value)
102
+ } catch {}
103
+ }
104
+
105
+ function DesktopMemoryRouter(props: BaseRouterProps & { windowID: string }) {
106
+ const history = createMemoryHistory()
107
+ const initialUrl = getLastActiveUrl(props.windowID)
108
+ if (initialUrl !== "/") history.set({ value: initialUrl, replace: true, scroll: false })
109
+ onCleanup(history.listen((value) => setLastActiveUrl(props.windowID, value)))
110
+ return <MemoryRouter {...props} history={history} />
111
+ }
112
+
113
+ const createPlatform = (windowState: DesktopWindowState): Platform => {
114
+ const attachmentPaths = new WeakMap<File, string>()
115
+ const os = (() => {
116
+ const ua = navigator.userAgent
117
+ if (ua.includes("Mac")) return "macos"
118
+ if (ua.includes("Windows")) return "windows"
119
+ if (ua.includes("Linux")) return "linux"
120
+ return undefined
121
+ })()
122
+
123
+ const runDesktopMenuAction: Platform["runDesktopMenuAction"] = (action) => {
124
+ switch (action) {
125
+ case "view.resetZoom":
126
+ resetZoom()
127
+ return
128
+ case "view.zoomIn":
129
+ zoomIn()
130
+ return
131
+ case "view.zoomOut":
132
+ zoomOut()
133
+ return
134
+ }
135
+
136
+ return window.api.runDesktopMenuAction(action)
137
+ }
138
+
139
+ const storage = (() => {
140
+ const cache = new Map<string, AsyncStorage>()
141
+
142
+ const createStorage = (name: string) => {
143
+ const api: AsyncStorage = {
144
+ getItem: (key: string) => window.api.storeGet(name, key),
145
+ setItem: (key: string, value: string) => window.api.storeSet(name, key, value),
146
+ removeItem: (key: string) => window.api.storeDelete(name, key),
147
+ clear: () => window.api.storeClear(name),
148
+ key: async (index: number) => (await window.api.storeKeys(name))[index],
149
+ getLength: () => window.api.storeLength(name),
150
+ get length() {
151
+ return api.getLength()
152
+ },
153
+ }
154
+ return api
155
+ }
156
+
157
+ return (name = "default.dat") => {
158
+ const cached = cache.get(name)
159
+ if (cached) return cached
160
+ const api = createStorage(name)
161
+ cache.set(name, api)
162
+ return api
163
+ }
164
+ })()
165
+
166
+ const wslServersApi = os === "windows" ? window.api.wslServers : undefined
167
+
168
+ return {
169
+ platform: "desktop",
170
+ os,
171
+ version: pkg.version,
172
+ windowID: windowState.id,
173
+
174
+ async openDirectoryPickerDialog(opts) {
175
+ return window.api.openDirectoryPicker({
176
+ multiple: opts?.multiple ?? false,
177
+ title: opts?.title ?? t("desktop.dialog.chooseFolder"),
178
+ })
179
+ },
180
+
181
+ async openAttachmentPickerDialog(opts, onFile) {
182
+ const result = await window.api.openFilePicker({
183
+ multiple: opts?.multiple ?? false,
184
+ title: opts?.title ?? t("desktop.dialog.chooseFile"),
185
+ defaultPath: opts?.defaultPath,
186
+ extensions: opts?.extensions ?? ACCEPTED_FILE_EXTENSIONS,
187
+ })
188
+ if (!result) return
189
+ try {
190
+ for (const file of result.files) {
191
+ const selected = new File([await window.api.readPickedFile(result.token, file.path)], file.name)
192
+ attachmentPaths.set(selected, file.path)
193
+ await onFile(selected)
194
+ }
195
+ } finally {
196
+ await window.api.releasePickedFiles(result.token)
197
+ }
198
+ },
199
+
200
+ getPathForFile(file) {
201
+ return attachmentPaths.get(file) ?? window.api.getPathForFile(file)
202
+ },
203
+
204
+ async saveFilePickerDialog(opts) {
205
+ return window.api.saveFilePicker({
206
+ title: opts?.title ?? t("desktop.dialog.saveFile"),
207
+ defaultPath: opts?.defaultPath,
208
+ })
209
+ },
210
+
211
+ openLink(url: string) {
212
+ window.api.openLink(url)
213
+ },
214
+ async openPath(path: string, app?: string) {
215
+ if (os === "windows") {
216
+ const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
217
+ return window.api.openPath(path, resolvedApp ?? undefined)
218
+ }
219
+ return window.api.openPath(path, app)
220
+ },
221
+ async revealPath(path: string) {
222
+ return window.api.revealPath(path)
223
+ },
224
+
225
+ back() {
226
+ window.history.back()
227
+ },
228
+
229
+ forward() {
230
+ window.history.forward()
231
+ },
232
+
233
+ storage,
234
+
235
+ updater: {
236
+ state: updaterState,
237
+ check: () => window.api.updater.check(),
238
+ install: () => window.api.updater.install(),
239
+ },
240
+
241
+ exportDebugLogs: () => window.api.exportDebugLogs(),
242
+
243
+ setForceFocus: (enabled) => window.api.setForceFocus(enabled),
244
+
245
+ recordFatalRendererError: (error) => window.api.recordFatalRendererError(error),
246
+
247
+ restart: async () => {
248
+ await window.api.killSidecar().catch(() => undefined)
249
+ window.api.relaunch()
250
+ },
251
+
252
+ notify: async (title, description, href) => {
253
+ const focused = await window.api.getWindowFocused().catch(() => document.hasFocus())
254
+ if (focused) return
255
+
256
+ const notification = new Notification(title, {
257
+ body: description ?? "",
258
+ icon: "https://opencode.ai/favicon-96x96-v3.png",
259
+ })
260
+ notification.onclick = () => {
261
+ void window.api.showWindow()
262
+ void window.api.setWindowFocus()
263
+ handleNotificationClick(href)
264
+ notification.close()
265
+ }
266
+ },
267
+
268
+ fetch: (input, init) => {
269
+ if (input instanceof Request) return fetch(input)
270
+ return fetch(input, init)
271
+ },
272
+
273
+ getDefaultServer: async () => {
274
+ const url = await window.api.getDefaultServerUrl().catch(() => null)
275
+ if (!url) return null
276
+ return ServerConnection.Key.make(url)
277
+ },
278
+
279
+ setDefaultServer: async (url: string | null) => {
280
+ await window.api.setDefaultServerUrl(url)
281
+ },
282
+
283
+ wslServers: wslServersApi,
284
+
285
+ getDisplayBackend: async () => {
286
+ return window.api.getDisplayBackend().catch(() => null)
287
+ },
288
+
289
+ setDisplayBackend: async (backend) => {
290
+ await window.api.setDisplayBackend(backend)
291
+ },
292
+
293
+ parseMarkdown: (markdown: string) => window.api.parseMarkdownCommand(markdown),
294
+
295
+ webviewZoom,
296
+
297
+ getPinchZoomEnabled: () => window.api.getPinchZoomEnabled(),
298
+
299
+ setPinchZoomEnabled,
300
+
301
+ runDesktopMenuAction,
302
+
303
+ checkAppExists: async (appName: string) => {
304
+ return window.api.checkAppExists(appName)
305
+ },
306
+
307
+ async readClipboardImage() {
308
+ const image = await window.api.readClipboardImage().catch(() => null)
309
+ if (!image) return null
310
+ const blob = new Blob([image.buffer], { type: "image/png" })
311
+ return new File([blob], `pasted-image-${Date.now()}.png`, {
312
+ type: "image/png",
313
+ })
314
+ },
315
+ }
316
+ }
317
+
318
+ let menuTrigger = null as null | ((id: string) => void)
319
+ window.api.onMenuCommand((id) => {
320
+ menuTrigger?.(id)
321
+ })
322
+ listenForDeepLinks()
323
+
324
+ function LoadingSplash() {
325
+ return (
326
+ <div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
327
+ <Splash class="w-16 h-20 opacity-50 animate-pulse" />
328
+ </div>
329
+ )
330
+ }
331
+
332
+ function DesktopRoot(props: { windowState: DesktopWindowState }) {
333
+ const platform = createPlatform(props.windowState)
334
+ const loadLocale = async () => {
335
+ const current = await platform.storage?.("neurocode.global.dat").getItem("language")
336
+ const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
337
+ const raw = current ?? legacy
338
+ if (!raw) return
339
+ const locale = raw.match(/"locale"\s*:\s*"([^"]+)"/)?.[1]
340
+ if (!locale) return
341
+ const next = normalizeLocale(locale)
342
+ if (next !== "en") await loadLocaleDict(next)
343
+ return next satisfies Locale
344
+ }
345
+
346
+ const [windowCount] = createResource(() => window.api.getWindowCount())
347
+
348
+ // Fetch sidecar credentials (available immediately, before health check)
349
+ const [sidecar] = createResource(() => window.api.awaitInitialization())
350
+
351
+ const [defaultServer] = createResource(() => platform.getDefaultServer?.())
352
+ const [locale] = createResource(loadLocale)
353
+ const router = (props: BaseRouterProps) => (
354
+ <DesktopMemoryRouter {...props} windowID={platform.windowID ?? "browser"} />
355
+ )
356
+ const onboarding = Promise.withResolvers<void>()
357
+
358
+ function handleClick(e: MouseEvent) {
359
+ const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
360
+ if (link?.href) {
361
+ e.preventDefault()
362
+ platform.openLink(link.href)
363
+ }
364
+ }
365
+
366
+ function Inner() {
367
+ const cmd = useCommand()
368
+ menuTrigger = (id) => cmd.trigger(id)
369
+
370
+ const theme = useTheme()
371
+
372
+ createEffect(() => {
373
+ theme.themeId()
374
+ theme.mode()
375
+ const bg = getComputedStyle(document.documentElement).getPropertyValue("--background-base").trim()
376
+ if (bg) {
377
+ void window.api.setBackgroundColor(bg)
378
+ }
379
+ })
380
+
381
+ return null
382
+ }
383
+
384
+ function App() {
385
+ const wslServers = useWslServers()
386
+ const ready = createMemo(
387
+ () =>
388
+ !defaultServer.loading && !sidecar.loading && !windowCount.loading && !locale.loading && !wslServers.isLoading,
389
+ )
390
+ const servers = createMemo(() => {
391
+ const data = initializationData(sidecar)
392
+ const list: ServerConnection.Any[] = []
393
+ if (data) {
394
+ list.push({
395
+ displayName: "Local Server",
396
+ type: "sidecar",
397
+ variant: "base",
398
+ http: {
399
+ url: data.url,
400
+ username: data.username ?? undefined,
401
+ password: data.password ?? undefined,
402
+ },
403
+ })
404
+ }
405
+ list.push(...readyWslConnections(wslServers.data))
406
+ return list
407
+ })
408
+ const effectiveDefaultServer = createMemo(() =>
409
+ ServerConnection.Key.make(availableStartupServer(defaultServer.latest, wslServers.data)),
410
+ )
411
+ return (
412
+ <Show when={ready()} fallback={<LoadingSplash />}>
413
+ <Show when={effectiveDefaultServer()} keyed>
414
+ {(key) => (
415
+ <AppInterface
416
+ defaultServer={key}
417
+ servers={servers()}
418
+ router={router}
419
+ startup={onboarding.promise}
420
+ serverScoped={
421
+ <DesktopFirstLaunchOnboarding
422
+ initialUrl={getLastActiveUrl(platform.windowID ?? "browser")}
423
+ onLoaded={onboarding.resolve}
424
+ />
425
+ }
426
+ >
427
+ <Inner />
428
+ </AppInterface>
429
+ )}
430
+ </Show>
431
+ </Show>
432
+ )
433
+ }
434
+
435
+ onMount(() => {
436
+ document.addEventListener("click", handleClick)
437
+ onCleanup(() => {
438
+ document.removeEventListener("click", handleClick)
439
+ })
440
+ })
441
+
442
+ return (
443
+ <PlatformProvider value={platform}>
444
+ <AppBaseProviders locale={locale.latest}>
445
+ <Show when={true}>{(_) => <App />}</Show>
446
+ </AppBaseProviders>
447
+ </PlatformProvider>
448
+ )
449
+ }
450
+
451
+ render(() => {
452
+ const [windowState] = createResource(async () => {
453
+ const api = window.api as typeof window.api & {
454
+ getWindowID?: () => Promise<string>
455
+ }
456
+ return { id: await api.getWindowID?.() }
457
+ })
458
+
459
+ return (
460
+ <Show when={windowState.latest} fallback={<LoadingSplash />} keyed>
461
+ {(state) => <DesktopRoot windowState={state} />}
462
+ </Show>
463
+ )
464
+ }, root!)
@@ -0,0 +1,73 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { initializationData, initializationReady } from "./initialization"
3
+
4
+ describe("desktop renderer initialization", () => {
5
+ test("throws the original initialization error before rendering server providers", () => {
6
+ const error = new Error("sidecar startup failed")
7
+
8
+ try {
9
+ initializationData(Object.assign(() => undefined, { error }))
10
+ throw new Error("expected initialization to fail")
11
+ } catch (failure) {
12
+ expect(failure).toBe(error)
13
+ expect((failure as Error & { localServerStartup?: boolean }).localServerStartup).toBe(true)
14
+ }
15
+ })
16
+
17
+ test("removes Electron's remote invocation wrapper from startup errors", () => {
18
+ const error = new Error(
19
+ "Error invoking remote method 'await-initialization': Error: Cannot migrate session_message projections",
20
+ )
21
+
22
+ try {
23
+ initializationData(Object.assign(() => undefined, { error }))
24
+ throw new Error("expected initialization to fail")
25
+ } catch (failure) {
26
+ expect(failure).toBe(error)
27
+ expect((failure as Error).message).toBe("Cannot migrate session_message projections")
28
+ }
29
+ })
30
+
31
+ test("returns initialized sidecar data", () => {
32
+ const sidecar = { url: "http://127.0.0.1:1234", username: "opencode", password: "secret" }
33
+
34
+ expect(initializationData(Object.assign(() => sidecar, { error: undefined }))).toBe(sidecar)
35
+ })
36
+
37
+ test("does not discard falsy initialization errors", () => {
38
+ let caught: unknown
39
+ try {
40
+ initializationData(Object.assign(() => undefined, { error: "" }))
41
+ } catch (error) {
42
+ caught = error
43
+ }
44
+
45
+ expect(caught).toBeInstanceOf(Error)
46
+ if (!(caught instanceof Error)) return
47
+ expect(caught.message).toBe("")
48
+ expect((caught as Error & { localServerStartup?: boolean }).localServerStartup).toBe(true)
49
+ })
50
+
51
+ test("checks initialization errors before rendering server providers", () => {
52
+ const error = new Error("sidecar startup failed")
53
+
54
+ expect(() => initializationReady(Object.assign(() => undefined, { error, loading: false }))).toThrow(error)
55
+ })
56
+
57
+ test("waits for pending initialization without reading it", () => {
58
+ let reads = 0
59
+
60
+ expect(
61
+ initializationReady(
62
+ Object.assign(
63
+ () => {
64
+ reads++
65
+ return undefined
66
+ },
67
+ { error: undefined, loading: true },
68
+ ),
69
+ ),
70
+ ).toBe(false)
71
+ expect(reads).toBe(0)
72
+ })
73
+ })
@@ -0,0 +1,22 @@
1
+ export function initializationData<A>(state: (() => A | undefined) & { error: unknown }) {
2
+ if (state.error !== undefined) throw markLocalServerStartup(state.error)
3
+ return state()
4
+ }
5
+
6
+ function markLocalServerStartup(error: unknown) {
7
+ const failure = error instanceof Error ? error : new Error(String(error))
8
+ const prefix = "Error invoking remote method 'await-initialization': Error: "
9
+ if (failure.message.startsWith(prefix)) {
10
+ const previous = failure.message
11
+ failure.message = failure.message.slice(prefix.length)
12
+ if (failure.stack) failure.stack = failure.stack.replace(`Error: ${previous}`, `Error: ${failure.message}`)
13
+ }
14
+ Object.defineProperty(failure, "localServerStartup", { value: true })
15
+ return failure
16
+ }
17
+
18
+ export function initializationReady<A>(state: (() => A | undefined) & { error: unknown; loading: boolean }) {
19
+ if (state.loading) return false
20
+ initializationData(state)
21
+ return true
22
+ }
@@ -0,0 +1,53 @@
1
+ import { ServerConnection, useServer, useSettings, useTabs } from "@neurocode-ai/app"
2
+ import { onMount } from "solid-js"
3
+
4
+ export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoaded: () => void }) {
5
+ const server = useServer()
6
+ const settings = useSettings()
7
+ const tabs = useTabs()
8
+
9
+ onMount(() => {
10
+ void runFirstLaunchOnboarding().finally(props.onLoaded)
11
+ })
12
+
13
+ async function runFirstLaunchOnboarding() {
14
+ try {
15
+ await Promise.all(
16
+ [server.ready.promise, tabs.ready.promise, tabs.recentReady.promise].map((p) => p ?? Promise.resolve()),
17
+ )
18
+ const existingInstall = await window.api.isOldLayoutEligible()
19
+ settings.general.setOldLayoutEligible(existingInstall)
20
+ if (!server.isLocal()) return
21
+
22
+ const pending = await window.api.isFirstLaunchOnboardingPending()
23
+ if (!pending) return
24
+
25
+ const shouldTrigger =
26
+ !existingInstall &&
27
+ props.initialUrl === "/" &&
28
+ tabs.store.length === 0 &&
29
+ server.list.every(ServerConnection.builtin)
30
+
31
+ console.info("[desktop-onboarding] first launch onboarding evaluated", {
32
+ pending,
33
+ shouldTrigger,
34
+ existingInstall,
35
+ initialUrl: props.initialUrl,
36
+ tabs: tabs.store.length,
37
+ servers: server.list.map(ServerConnection.key),
38
+ })
39
+
40
+ const directory = await window.api.finishFirstLaunchOnboarding(shouldTrigger)
41
+ if (!shouldTrigger || !directory) return
42
+
43
+ console.info("[desktop-onboarding] starting first launch draft", { directory })
44
+ server.projects.open(directory)
45
+ server.projects.touch(directory)
46
+ tabs.select(await tabs.newDraft({ server: server.key, directory }))
47
+ } catch (error) {
48
+ console.error("[desktop-onboarding] first launch onboarding failed", error)
49
+ }
50
+ }
51
+
52
+ return null
53
+ }
File without changes