@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,399 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { mkdirSync, rmSync } from "node:fs"
3
+ import * as http from "node:http"
4
+ import { createServer } from "node:net"
5
+ import { homedir, tmpdir } from "node:os"
6
+ import { join } from "node:path"
7
+ import { getCACertificates, setDefaultCACertificates } from "node:tls"
8
+ import type { Event } from "electron"
9
+ import { app } from "electron"
10
+
11
+ import { Deferred, Effect, Fiber } from "effect"
12
+ import contextMenu from "electron-context-menu"
13
+
14
+ import type { ServerReadyData } from "../preload/types"
15
+ import { checkAppExists, resolveAppPath } from "./apps"
16
+ import { CHANNEL } from "./constants"
17
+ import { registerIpcHandlers, sendDeepLinks, sendMenuCommand } from "./ipc"
18
+ import { forwardInitializationFailure } from "./initialization"
19
+ import { exportDebugLogs, initCrashReporter, initLogging, startNetLog, write as writeLog } from "./logging"
20
+ import { parseMarkdown } from "./markdown"
21
+ import { createMenu } from "./menu"
22
+ import {
23
+ finishFirstLaunchOnboarding,
24
+ initializeOldLayoutEligibility,
25
+ isFirstLaunchOnboardingPending,
26
+ isOldLayoutEligible,
27
+ } from "./onboarding"
28
+ import {
29
+ getDefaultServerUrl,
30
+ preferAppEnv,
31
+ setDefaultServerUrl,
32
+ spawnLocalServer,
33
+ type SidecarListener,
34
+ } from "./server"
35
+ import { setupAutoUpdater, showUpdaterDialog } from "./updater"
36
+ import { safeWebContentsURL } from "./window-state"
37
+ import {
38
+ getLastFocusedWindow,
39
+ registerRendererProtocol,
40
+ setRelaunchHandler,
41
+ setAppQuitting,
42
+ setBackgroundColor,
43
+ setDockIcon,
44
+ restoreMainWindows,
45
+ } from "./windows"
46
+ import { createWslServersController } from "./wsl/servers"
47
+ import { registerWslIpcHandlers } from "./wsl/ipc"
48
+ import { spawnWslSidecar } from "./wsl/sidecar"
49
+ import { migrate } from "./migrate"
50
+ import { cleanupStoreFiles } from "./store-cleanup"
51
+
52
+ const APP_NAMES: Record<string, string> = {
53
+ dev: "OpenCode Dev",
54
+ beta: "OpenCode Beta",
55
+ prod: "OpenCode",
56
+ }
57
+ const APP_IDS: Record<string, string> = {
58
+ dev: "ai.neurocode.desktop.dev",
59
+ beta: "ai.neurocode.desktop.beta",
60
+ prod: "ai.neurocode.desktop",
61
+ }
62
+ const TEST_ONBOARDING = process.env.NEUROCODE_TEST_ONBOARDING === "1"
63
+ const jsCallStackFeature = "DocumentPolicyIncludeJSCallStacksInCrashReports"
64
+
65
+ let logger: ReturnType<typeof initLogging>
66
+ let server: SidecarListener | null = null
67
+
68
+ const pendingDeepLinks: string[] = []
69
+
70
+ function useEnvProxy() {
71
+ try {
72
+ // Electron 41.2 runs Node 24.14.1; latest @types/node@24 is 24.12.2.
73
+ ;(http as any).setGlobalProxyFromEnv()
74
+ } catch (error) {
75
+ logger.warn("failed to load proxy environment", error)
76
+ }
77
+ }
78
+
79
+ function emitDeepLinks(urls: string[]) {
80
+ if (urls.length === 0) return
81
+ pendingDeepLinks.push(...urls)
82
+ const win = getLastFocusedWindow()
83
+ if (win) sendDeepLinks(win, urls)
84
+ }
85
+
86
+ async function killSidecar() {
87
+ if (!server) return
88
+ const current = server
89
+ server = null
90
+ await current.stop()
91
+ }
92
+
93
+ function ensureLoopbackNoProxy() {
94
+ const loopback = ["127.0.0.1", "localhost", "::1"]
95
+ const upsert = (key: string) => {
96
+ const items = (process.env[key] ?? "")
97
+ .split(",")
98
+ .map((value: string) => value.trim())
99
+ .filter((value: string) => Boolean(value))
100
+
101
+ for (const host of loopback) {
102
+ if (items.some((value: string) => value.toLowerCase() === host)) continue
103
+ items.push(host)
104
+ }
105
+
106
+ process.env[key] = items.join(",")
107
+ }
108
+
109
+ upsert("NO_PROXY")
110
+ upsert("no_proxy")
111
+ }
112
+
113
+ const main = Effect.gen(function* () {
114
+ contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false })
115
+
116
+ // on macOS apps run in `/` which can cause issues with ripgrep
117
+ try {
118
+ process.chdir(homedir())
119
+ } catch {}
120
+
121
+ process.env.NEUROCODE_DISABLE_EMBEDDED_WEB_UI = "true"
122
+
123
+ const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.neurocode.desktop.dev"
124
+ const onboardingTestRoot = ((): string | undefined => {
125
+ if (!TEST_ONBOARDING) return
126
+
127
+ const root = join(tmpdir(), `opencode-onboarding-${randomUUID()}`)
128
+ rmSync(root, { recursive: true, force: true })
129
+ ;["data", "config", "cache", "state", "desktop", "session"].forEach((dir) =>
130
+ mkdirSync(join(root, dir), { recursive: true }),
131
+ )
132
+ process.env.NEUROCODE_DB = ":memory:"
133
+ process.env.XDG_DATA_HOME = join(root, "data")
134
+ process.env.XDG_CONFIG_HOME = join(root, "config")
135
+ process.env.XDG_CACHE_HOME = join(root, "cache")
136
+ process.env.XDG_STATE_HOME = join(root, "state")
137
+ return root
138
+ })()
139
+ app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev")
140
+ app.setAppUserModelId(appId)
141
+ app.setPath(
142
+ "userData",
143
+ onboardingTestRoot ? join(onboardingTestRoot, "desktop") : join(app.getPath("appData"), appId),
144
+ )
145
+ if (onboardingTestRoot) app.setPath("sessionData", join(onboardingTestRoot, "session"))
146
+ initializeOldLayoutEligibility(app.getPath("userData"))
147
+ logger = initLogging()
148
+ initCrashReporter()
149
+
150
+ const wslServers = createWslServersController(
151
+ app.getVersion(),
152
+ async (distro) => {
153
+ logger.log("spawning wsl sidecar", { distro })
154
+ return spawnWslSidecar(distro, {
155
+ onLine: (line) => logger.log("wsl sidecar", { distro, stream: line.stream, text: line.text }),
156
+ })
157
+ },
158
+ {
159
+ logger: {
160
+ log: (message, meta) => logger.log(message, meta),
161
+ error: (message, meta) => logger.error(message, meta),
162
+ },
163
+ },
164
+ )
165
+ const stopSidecars = async () => {
166
+ await killSidecar()
167
+ wslServers.stopAll()
168
+ }
169
+ const relaunch = () => {
170
+ setAppQuitting()
171
+ void stopSidecars().finally(() => {
172
+ app.relaunch()
173
+ app.exit(0)
174
+ })
175
+ }
176
+
177
+ try {
178
+ setDefaultCACertificates([...new Set([...getCACertificates("default"), ...getCACertificates("system")])])
179
+ } catch (error) {
180
+ logger.warn("failed to load system certificates", error)
181
+ }
182
+
183
+ logger.log("app starting", {
184
+ version: app.getVersion(),
185
+ packaged: app.isPackaged,
186
+ onboardingTest: Boolean(onboardingTestRoot),
187
+ })
188
+
189
+ ensureLoopbackNoProxy()
190
+ useEnvProxy()
191
+ app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")
192
+ const features = app.commandLine.getSwitchValue("enable-features")
193
+ app.commandLine.appendSwitch("enable-features", features ? `${jsCallStackFeature},${features}` : jsCallStackFeature)
194
+ if (!app.isPackaged) app.commandLine.appendSwitch("remote-debugging-port", "9222")
195
+
196
+ if (!app.requestSingleInstanceLock()) {
197
+ app.quit()
198
+ return
199
+ }
200
+
201
+ preferAppEnv(app.getPath("userData"))
202
+
203
+ app.on("second-instance", (_event: Event, argv: string[]) => {
204
+ const urls = argv.filter((arg: string) => arg.startsWith("opencode://"))
205
+ if (urls.length) {
206
+ logger.log("deep link received via second-instance", { urls })
207
+ emitDeepLinks(urls)
208
+ }
209
+ const win = getLastFocusedWindow()
210
+ if (win) {
211
+ win.show()
212
+ win.focus()
213
+ }
214
+ })
215
+
216
+ app.on("open-url", (event: Event, url: string) => {
217
+ event.preventDefault()
218
+ logger.log("deep link received via open-url", { url })
219
+ emitDeepLinks([url])
220
+ })
221
+
222
+ app.on("before-quit", () => {
223
+ setAppQuitting()
224
+ void stopSidecars()
225
+ })
226
+
227
+ app.on("will-quit", () => {
228
+ setAppQuitting()
229
+ void stopSidecars()
230
+ })
231
+
232
+ app.on("child-process-gone", (_event, details) => {
233
+ writeLog("utility", "child process gone", { details }, "error")
234
+ })
235
+
236
+ app.on("render-process-gone", (_event, webContents, details) => {
237
+ writeLog("window", "app render process gone", { url: safeWebContentsURL(webContents), details }, "error")
238
+ })
239
+
240
+ setRelaunchHandler(() => {
241
+ relaunch()
242
+ })
243
+
244
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
245
+ process.on(signal, () => {
246
+ setAppQuitting()
247
+ void stopSidecars().finally(() => app.exit(0))
248
+ })
249
+ }
250
+
251
+ const serverReady = Deferred.makeUnsafe<ServerReadyData, unknown>()
252
+
253
+ yield* Effect.promise(() => app.whenReady())
254
+
255
+ if (!TEST_ONBOARDING) migrate()
256
+ yield* Effect.promise(() => cleanupStoreFiles(app.getPath("userData"))).pipe(
257
+ Effect.tap((result) =>
258
+ Effect.sync(() => {
259
+ if (result.deleted.length === 0) return
260
+ logger.log("cleaned scoped store files", { count: result.deleted.length, scanned: result.scanned })
261
+ }),
262
+ ),
263
+ Effect.catch((error) =>
264
+ Effect.sync(() => {
265
+ logger.warn("failed to clean scoped store files", error)
266
+ }),
267
+ ),
268
+ )
269
+ app.setAsDefaultProtocolClient("opencode")
270
+ registerRendererProtocol()
271
+ setDockIcon()
272
+ const updater = setupAutoUpdater(stopSidecars)
273
+ registerIpcHandlers({
274
+ killSidecar: () => killSidecar(),
275
+ relaunch,
276
+ awaitInitialization: Effect.fnUntraced(
277
+ function* () {
278
+ logger.log("awaiting server ready")
279
+ const res = yield* Deferred.await(serverReady)
280
+ logger.log("server ready", { url: res.url })
281
+ return res
282
+ },
283
+ (e) => Effect.runPromise(e),
284
+ ),
285
+ consumeInitialDeepLinks: () => pendingDeepLinks.splice(0),
286
+ getDefaultServerUrl: () => getDefaultServerUrl(),
287
+ setDefaultServerUrl: (url) => setDefaultServerUrl(url),
288
+ isFirstLaunchOnboardingPending,
289
+ finishFirstLaunchOnboarding,
290
+ isOldLayoutEligible,
291
+ getDisplayBackend: async () => null,
292
+ setDisplayBackend: async () => undefined,
293
+ parseMarkdown: async (markdown) => parseMarkdown(markdown),
294
+ checkAppExists: (appName) => checkAppExists(appName),
295
+ resolveAppPath: async (appName) => resolveAppPath(appName),
296
+ updater,
297
+ showUpdater: () => showUpdaterDialog(updater, true),
298
+ setBackgroundColor: (color) => setBackgroundColor(color),
299
+ exportDebugLogs: () => exportDebugLogs(),
300
+ recordFatalRendererError: (error) => writeLog("renderer", "fatal renderer error", { ...error }, "error"),
301
+ })
302
+ registerWslIpcHandlers(wslServers)
303
+ void updater.start()
304
+ const updateTimer = setInterval(() => void updater.check(), 10 * 60 * 1000)
305
+ updateTimer.unref()
306
+ app.once("will-quit", () => clearInterval(updateTimer))
307
+ yield* Effect.promise(() => startNetLog()).pipe(
308
+ Effect.catch((error) =>
309
+ Effect.sync(() => {
310
+ logger.warn("failed to start net log", error)
311
+ }),
312
+ ),
313
+ )
314
+
315
+ const port = yield* Effect.gen(function* () {
316
+ const fromEnv = process.env.NEUROCODE_PORT
317
+ if (fromEnv) {
318
+ const parsed = Number.parseInt(fromEnv, 10)
319
+ if (!Number.isNaN(parsed)) return parsed
320
+ }
321
+
322
+ const res = yield* Deferred.make<number, unknown>()
323
+ const server = createServer()
324
+ server.on("error", (e) => Deferred.failSync(res, () => e))
325
+ server.listen(0, "127.0.0.1", () => {
326
+ const address = server.address()
327
+ if (typeof address !== "object" || !address) {
328
+ server.close()
329
+ Deferred.failSync(res, () => new Error("Failed to get port"))
330
+ return
331
+ }
332
+ const port = address.port
333
+ server.close(() => Effect.runSync(Deferred.succeed(res, port)))
334
+ })
335
+
336
+ return yield* Deferred.await(res)
337
+ })
338
+ const hostname = "127.0.0.1"
339
+ const url = `http://${hostname}:${port}`
340
+ const password = randomUUID()
341
+
342
+ const loadingTask = yield* Effect.gen(function* () {
343
+ logger.log("sidecar connection started", { url })
344
+
345
+ ensureLoopbackNoProxy()
346
+ useEnvProxy()
347
+
348
+ logger.log("spawning sidecar", { url })
349
+ const { listener, health } = yield* Effect.promise(() =>
350
+ spawnLocalServer(hostname, port, password, {
351
+ userDataPath: app.getPath("userData"),
352
+ onStdout: (message) => writeLog("server", "stdout", { message }),
353
+ onStderr: (message) => writeLog("server", "stderr", { message }, "warn"),
354
+ onExit: (code) => writeLog("utility", "sidecar exited", { code }, "warn"),
355
+ }),
356
+ )
357
+ server = listener
358
+ yield* Deferred.succeed(serverReady, {
359
+ url,
360
+ username: "opencode",
361
+ password,
362
+ })
363
+
364
+ if (process.platform === "win32") {
365
+ void wslServers.initialize().catch((error) => logger.error("wsl server initialization failed", error))
366
+ }
367
+
368
+ yield* Effect.promise(() => health.wait).pipe(
369
+ Effect.timeout("30 seconds"),
370
+ Effect.catch((e) =>
371
+ Effect.sync(() => {
372
+ logger.error("sidecar health check failed", e.toString())
373
+ }),
374
+ ),
375
+ )
376
+
377
+ logger.log("loading task finished")
378
+ }).pipe(forwardInitializationFailure(serverReady), Effect.forkChild)
379
+
380
+ yield* Fiber.await(loadingTask)
381
+
382
+ const windows = restoreMainWindows()
383
+ if (windows.length) {
384
+ createMenu({
385
+ trigger: (id) => {
386
+ const win = getLastFocusedWindow()
387
+ if (win) sendMenuCommand(win, id)
388
+ },
389
+ checkForUpdates: () => {
390
+ void showUpdaterDialog(updater, true)
391
+ },
392
+ relaunch: () => {
393
+ relaunch()
394
+ },
395
+ })
396
+ }
397
+ })
398
+
399
+ Effect.runFork(main)
@@ -0,0 +1,6 @@
1
+ import { Deferred, Effect } from "effect"
2
+
3
+ export function forwardInitializationFailure<A>(initialization: Deferred.Deferred<A, unknown>) {
4
+ return <B, E, R>(effect: Effect.Effect<B, E, R>) =>
5
+ effect.pipe(Effect.tapCause((cause) => Deferred.failCause(initialization, cause)))
6
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { hasExistingAppState } from "./install-state"
3
+
4
+ const file = (name: string) => ({ name, isDirectory: () => false })
5
+ const directory = (name: string) => ({ name, isDirectory: () => true })
6
+
7
+ describe("hasExistingAppState", () => {
8
+ test("ignores files Electron may create on a fresh install", () => {
9
+ expect(hasExistingAppState([])).toBe(false)
10
+ expect(hasExistingAppState([file("Local State"), directory("Crashpad")])).toBe(false)
11
+ })
12
+
13
+ test("recognizes state written by an earlier OpenCode launch", () => {
14
+ expect(hasExistingAppState([file("neurocode.settings")])).toBe(true)
15
+ expect(hasExistingAppState([file("neurocode.global.dat")])).toBe(true)
16
+ expect(hasExistingAppState([file("window-state-abc.json")])).toBe(true)
17
+ expect(hasExistingAppState([directory("opencode")])).toBe(true)
18
+ })
19
+ })
@@ -0,0 +1,8 @@
1
+ export function hasExistingAppState(entries: Array<{ name: string; isDirectory: () => boolean }>) {
2
+ return entries.some((entry) => {
3
+ if (entry.name === "neurocode.settings") return true
4
+ if (entry.name.endsWith(".dat")) return true
5
+ if (/^window-state-.+\.json$/.test(entry.name)) return true
6
+ return entry.isDirectory() && entry.name === "opencode"
7
+ })
8
+ }