@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.
- package/AGENTS.md +4 -0
- package/README.md +19 -0
- package/electron-builder.config.test.ts +48 -0
- package/electron-builder.config.ts +146 -0
- package/electron.vite.config.ts +106 -0
- package/icons/README.md +14 -0
- package/icons/beta/128x128.png +0 -0
- package/icons/beta/128x128@2x.png +0 -0
- package/icons/beta/32x32.png +0 -0
- package/icons/beta/64x64.png +0 -0
- package/icons/beta/Square107x107Logo.png +0 -0
- package/icons/beta/Square142x142Logo.png +0 -0
- package/icons/beta/Square150x150Logo.png +0 -0
- package/icons/beta/Square284x284Logo.png +0 -0
- package/icons/beta/Square30x30Logo.png +0 -0
- package/icons/beta/Square310x310Logo.png +0 -0
- package/icons/beta/Square44x44Logo.png +0 -0
- package/icons/beta/Square71x71Logo.png +0 -0
- package/icons/beta/Square89x89Logo.png +0 -0
- package/icons/beta/StoreLogo.png +0 -0
- package/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/icons/beta/android/mipmap-hdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-mdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-xhdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/values/ic_launcher_background.xml +4 -0
- package/icons/beta/dock.png +0 -0
- package/icons/beta/icon.icns +0 -0
- package/icons/beta/icon.ico +0 -0
- package/icons/beta/icon.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@1x.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@2x-1.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@2x.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@3x.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@1x.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@2x-1.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@2x.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@3x.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@1x.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@2x-1.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@2x.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@3x.png +0 -0
- package/icons/beta/ios/AppIcon-512@2x.png +0 -0
- package/icons/beta/ios/AppIcon-60x60@2x.png +0 -0
- package/icons/beta/ios/AppIcon-60x60@3x.png +0 -0
- package/icons/beta/ios/AppIcon-76x76@1x.png +0 -0
- package/icons/beta/ios/AppIcon-76x76@2x.png +0 -0
- package/icons/beta/ios/AppIcon-83.5x83.5@2x.png +0 -0
- package/icons/dev/128x128.png +0 -0
- package/icons/dev/128x128@2x.png +0 -0
- package/icons/dev/32x32.png +0 -0
- package/icons/dev/64x64.png +0 -0
- package/icons/dev/Square107x107Logo.png +0 -0
- package/icons/dev/Square142x142Logo.png +0 -0
- package/icons/dev/Square150x150Logo.png +0 -0
- package/icons/dev/Square284x284Logo.png +0 -0
- package/icons/dev/Square30x30Logo.png +0 -0
- package/icons/dev/Square310x310Logo.png +0 -0
- package/icons/dev/Square44x44Logo.png +0 -0
- package/icons/dev/Square71x71Logo.png +0 -0
- package/icons/dev/Square89x89Logo.png +0 -0
- package/icons/dev/StoreLogo.png +0 -0
- package/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/icons/dev/android/mipmap-hdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-mdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-xhdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/values/ic_launcher_background.xml +4 -0
- package/icons/dev/dock.png +0 -0
- package/icons/dev/icon.icns +0 -0
- package/icons/dev/icon.ico +0 -0
- package/icons/dev/icon.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@1x.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@2x-1.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@2x.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@3x.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@1x.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@2x-1.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@2x.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@3x.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@1x.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@2x-1.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@2x.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@3x.png +0 -0
- package/icons/dev/ios/AppIcon-512@2x.png +0 -0
- package/icons/dev/ios/AppIcon-60x60@2x.png +0 -0
- package/icons/dev/ios/AppIcon-60x60@3x.png +0 -0
- package/icons/dev/ios/AppIcon-76x76@1x.png +0 -0
- package/icons/dev/ios/AppIcon-76x76@2x.png +0 -0
- package/icons/dev/ios/AppIcon-83.5x83.5@2x.png +0 -0
- package/icons/prod/128x128.png +0 -0
- package/icons/prod/128x128@2x.png +0 -0
- package/icons/prod/32x32.png +0 -0
- package/icons/prod/64x64.png +0 -0
- package/icons/prod/Square107x107Logo.png +0 -0
- package/icons/prod/Square142x142Logo.png +0 -0
- package/icons/prod/Square150x150Logo.png +0 -0
- package/icons/prod/Square284x284Logo.png +0 -0
- package/icons/prod/Square30x30Logo.png +0 -0
- package/icons/prod/Square310x310Logo.png +0 -0
- package/icons/prod/Square44x44Logo.png +0 -0
- package/icons/prod/Square71x71Logo.png +0 -0
- package/icons/prod/Square89x89Logo.png +0 -0
- package/icons/prod/StoreLogo.png +0 -0
- package/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/icons/prod/android/mipmap-hdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-mdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-xhdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/values/ic_launcher_background.xml +4 -0
- package/icons/prod/dock.png +0 -0
- package/icons/prod/icon.icns +0 -0
- package/icons/prod/icon.ico +0 -0
- package/icons/prod/icon.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@1x.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@2x-1.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@2x.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@3x.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@1x.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@2x-1.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@2x.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@3x.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@1x.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@2x-1.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@2x.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@3x.png +0 -0
- package/icons/prod/ios/AppIcon-512@2x.png +0 -0
- package/icons/prod/ios/AppIcon-60x60@2x.png +0 -0
- package/icons/prod/ios/AppIcon-60x60@3x.png +0 -0
- package/icons/prod/ios/AppIcon-76x76@1x.png +0 -0
- package/icons/prod/ios/AppIcon-76x76@2x.png +0 -0
- package/icons/prod/ios/AppIcon-83.5x83.5@2x.png +0 -0
- package/package.json +75 -0
- package/resources/entitlements.plist +18 -0
- package/resources/linux/opencode-desktop.desktop +10 -0
- package/scripts/copy-bundles.ts +12 -0
- package/scripts/copy-icons.ts +12 -0
- package/scripts/copy-metainfo.ts +47 -0
- package/scripts/finalize-latest-json.ts +219 -0
- package/scripts/finalize-latest-yml.ts +124 -0
- package/scripts/prebuild.ts +10 -0
- package/scripts/predev.ts +5 -0
- package/scripts/prepare.ts +9 -0
- package/scripts/utils.ts +77 -0
- package/src/main/apps.ts +136 -0
- package/src/main/attachment-picker.test.ts +87 -0
- package/src/main/attachment-picker.ts +56 -0
- package/src/main/constants.ts +7 -0
- package/src/main/debug.ts +95 -0
- package/src/main/desktop-menu-actions.ts +84 -0
- package/src/main/env.d.ts +19 -0
- package/src/main/index.test.ts +37 -0
- package/src/main/index.ts +399 -0
- package/src/main/initialization.ts +6 -0
- package/src/main/install-state.test.ts +19 -0
- package/src/main/install-state.ts +8 -0
- package/src/main/ipc.ts +274 -0
- package/src/main/logging.ts +205 -0
- package/src/main/markdown.ts +16 -0
- package/src/main/menu.ts +67 -0
- package/src/main/migrate.ts +91 -0
- package/src/main/onboarding.ts +45 -0
- package/src/main/server.ts +238 -0
- package/src/main/shell-env.test.ts +50 -0
- package/src/main/shell-env.ts +101 -0
- package/src/main/sidecar.ts +157 -0
- package/src/main/store-cleanup.test.ts +93 -0
- package/src/main/store-cleanup.ts +94 -0
- package/src/main/store-keys.ts +7 -0
- package/src/main/store.ts +35 -0
- package/src/main/unresponsive.ts +70 -0
- package/src/main/updater-controller.test.ts +111 -0
- package/src/main/updater-controller.ts +97 -0
- package/src/main/updater-subscriptions.test.ts +16 -0
- package/src/main/updater-subscriptions.ts +20 -0
- package/src/main/updater.ts +85 -0
- package/src/main/window-registry.test.ts +91 -0
- package/src/main/window-registry.ts +47 -0
- package/src/main/window-state.ts +29 -0
- package/src/main/windows.ts +496 -0
- package/src/main/wsl/ipc.ts +103 -0
- package/src/main/wsl/policy.ts +33 -0
- package/src/main/wsl/runtime.ts +400 -0
- package/src/main/wsl/servers.test.ts +231 -0
- package/src/main/wsl/servers.ts +521 -0
- package/src/main/wsl/sidecar.ts +129 -0
- package/src/main/wsl/startup.ts +31 -0
- package/src/preload/index.ts +128 -0
- package/src/preload/types.ts +109 -0
- package/src/renderer/cli.ts +12 -0
- package/src/renderer/env.d.ts +10 -0
- package/src/renderer/html.test.ts +62 -0
- package/src/renderer/i18n/ar.ts +26 -0
- package/src/renderer/i18n/br.ts +27 -0
- package/src/renderer/i18n/bs.ts +28 -0
- package/src/renderer/i18n/da.ts +28 -0
- package/src/renderer/i18n/de.ts +28 -0
- package/src/renderer/i18n/en.ts +27 -0
- package/src/renderer/i18n/es.ts +27 -0
- package/src/renderer/i18n/fr.ts +28 -0
- package/src/renderer/i18n/index.ts +194 -0
- package/src/renderer/i18n/ja.ts +28 -0
- package/src/renderer/i18n/ko.ts +27 -0
- package/src/renderer/i18n/no.ts +28 -0
- package/src/renderer/i18n/pl.ts +28 -0
- package/src/renderer/i18n/ru.ts +27 -0
- package/src/renderer/i18n/uk.ts +28 -0
- package/src/renderer/i18n/zh.ts +26 -0
- package/src/renderer/i18n/zht.ts +26 -0
- package/src/renderer/index.html +21 -0
- package/src/renderer/index.tsx +464 -0
- package/src/renderer/initialization.test.ts +73 -0
- package/src/renderer/initialization.ts +22 -0
- package/src/renderer/onboarding.tsx +53 -0
- package/src/renderer/styles.css +0 -0
- package/src/renderer/webview-zoom.ts +138 -0
- package/src/renderer/wsl/connections.test.ts +43 -0
- package/src/renderer/wsl/connections.ts +28 -0
- package/sst-env.d.ts +10 -0
- 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
|