@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,29 @@
|
|
|
1
|
+
export const destroyedWindowURL = "<destroyed>"
|
|
2
|
+
|
|
3
|
+
type WebContentsURLState = {
|
|
4
|
+
isDestroyed(): boolean
|
|
5
|
+
getURL(): string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type WindowURLState = {
|
|
9
|
+
isDestroyed(): boolean
|
|
10
|
+
readonly webContents: WebContentsURLState
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function safeWebContentsURL(webContents: WebContentsURLState) {
|
|
14
|
+
try {
|
|
15
|
+
if (webContents.isDestroyed()) return destroyedWindowURL
|
|
16
|
+
return webContents.getURL()
|
|
17
|
+
} catch {
|
|
18
|
+
return destroyedWindowURL
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function safeWindowURL(win: WindowURLState) {
|
|
23
|
+
try {
|
|
24
|
+
if (win.isDestroyed()) return destroyedWindowURL
|
|
25
|
+
return safeWebContentsURL(win.webContents)
|
|
26
|
+
} catch {
|
|
27
|
+
return destroyedWindowURL
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import windowState from "electron-window-state"
|
|
2
|
+
import { resolveThemeVariant } from "@neurocode-ai/ui/theme/resolve"
|
|
3
|
+
import type { DesktopTheme } from "@neurocode-ai/ui/theme/types"
|
|
4
|
+
import oc2ThemeJson from "../../../ui/src/theme/themes/oc-2.json"
|
|
5
|
+
import { randomUUID } from "node:crypto"
|
|
6
|
+
import { rmSync } from "node:fs"
|
|
7
|
+
import { app, BrowserWindow, dialog, net, nativeImage, nativeTheme, protocol } from "electron"
|
|
8
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
10
|
+
import type { TitlebarTheme } from "../preload/types"
|
|
11
|
+
import { exportDebugLogs, write as writeLog } from "./logging"
|
|
12
|
+
import { getStore, removeStoreFile } from "./store"
|
|
13
|
+
import { PINCH_ZOOM_ENABLED_KEY, WINDOW_IDS_KEY } from "./store-keys"
|
|
14
|
+
import { createUnresponsiveSampler } from "./unresponsive"
|
|
15
|
+
import { createWindowRegistry } from "./window-registry"
|
|
16
|
+
import { safeWindowURL } from "./window-state"
|
|
17
|
+
|
|
18
|
+
const root = dirname(fileURLToPath(import.meta.url))
|
|
19
|
+
const rendererRoot = join(root, "../renderer")
|
|
20
|
+
const rendererProtocol = "oc"
|
|
21
|
+
const rendererHost = "renderer"
|
|
22
|
+
const clipboardWritePermission = "clipboard-sanitized-write"
|
|
23
|
+
const notificationPermission = "notifications"
|
|
24
|
+
const rendererPermissions = new Set([clipboardWritePermission, notificationPermission])
|
|
25
|
+
const oc2Theme = oc2ThemeJson as DesktopTheme
|
|
26
|
+
const oc2Background = {
|
|
27
|
+
light: resolveThemeVariant(oc2Theme.light, false)["background-base"],
|
|
28
|
+
dark: resolveThemeVariant(oc2Theme.dark, true)["background-base"],
|
|
29
|
+
}
|
|
30
|
+
const documentPolicyHeader = "Document-Policy"
|
|
31
|
+
const jsCallStacksDocumentPolicy = "include-js-call-stacks-in-crash-reports"
|
|
32
|
+
|
|
33
|
+
protocol.registerSchemesAsPrivileged([
|
|
34
|
+
{
|
|
35
|
+
scheme: rendererProtocol,
|
|
36
|
+
privileges: {
|
|
37
|
+
secure: true,
|
|
38
|
+
standard: true,
|
|
39
|
+
supportFetchAPI: true,
|
|
40
|
+
stream: true,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
let backgroundColor: string | undefined
|
|
46
|
+
let relaunchHandler = () => {
|
|
47
|
+
setAppQuitting()
|
|
48
|
+
app.relaunch()
|
|
49
|
+
app.exit(0)
|
|
50
|
+
}
|
|
51
|
+
const titlebarThemes = new WeakMap<BrowserWindow, Partial<TitlebarTheme>>()
|
|
52
|
+
const pinchZoomEnabled = new WeakMap<BrowserWindow, boolean>()
|
|
53
|
+
const windowIDs = new WeakMap<BrowserWindow, string>()
|
|
54
|
+
const registry = createWindowRegistry<BrowserWindow>({
|
|
55
|
+
read: () => getStore().get(WINDOW_IDS_KEY),
|
|
56
|
+
write: (ids) => getStore().set(WINDOW_IDS_KEY, ids),
|
|
57
|
+
cleanup: (id) => {
|
|
58
|
+
rmSync(join(app.getPath("userData"), windowStateFile(id)), { force: true })
|
|
59
|
+
removeStoreFile(windowDataFile(id))
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
const titlebarHeight = 40
|
|
63
|
+
const maxZoomLevel = 10
|
|
64
|
+
const minZoomLevel = 0.2
|
|
65
|
+
|
|
66
|
+
export function setRelaunchHandler(handler: () => void) {
|
|
67
|
+
relaunchHandler = handler
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function setAppQuitting(quitting = true) {
|
|
71
|
+
registry.setQuitting(quitting)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function setBackgroundColor(color: string) {
|
|
75
|
+
backgroundColor = color
|
|
76
|
+
BrowserWindow.getAllWindows().forEach((win) => {
|
|
77
|
+
win.setBackgroundColor(color)
|
|
78
|
+
if (process.platform === "darwin") win.invalidateShadow()
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getBackgroundColor(): string | undefined {
|
|
83
|
+
return backgroundColor
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function iconsDir() {
|
|
87
|
+
return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function iconPath() {
|
|
91
|
+
const ext = process.platform === "win32" ? "ico" : "png"
|
|
92
|
+
return join(iconsDir(), `icon.${ext}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tone() {
|
|
96
|
+
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function defaultBackgroundColor() {
|
|
100
|
+
return oc2Background[tone()]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function overlay(theme: Partial<TitlebarTheme> = {}, zoom = 1) {
|
|
104
|
+
const mode = theme.mode ?? tone()
|
|
105
|
+
return {
|
|
106
|
+
color: "#00000000",
|
|
107
|
+
symbolColor: mode === "dark" ? "white" : "black",
|
|
108
|
+
height: Math.max(titlebarHeight, Math.round(titlebarHeight * zoom)),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) {
|
|
113
|
+
titlebarThemes.set(win, theme)
|
|
114
|
+
// macOS draws the window frame hairline and shadow using the NSWindow
|
|
115
|
+
// appearance, which follows nativeTheme rather than the rendered content.
|
|
116
|
+
// Align it with the app theme so a light app on a dark system does not get
|
|
117
|
+
// the dark-appearance border and shadow. A "system" scheme must map to
|
|
118
|
+
// "system" (not the resolved mode) or prefers-color-scheme stops tracking
|
|
119
|
+
// OS appearance changes in the renderer.
|
|
120
|
+
if (process.platform === "darwin") nativeTheme.themeSource = theme.scheme ?? theme.mode ?? "system"
|
|
121
|
+
updateTitlebar(win)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function updateTitlebar(win: BrowserWindow) {
|
|
125
|
+
if (process.platform !== "win32") return
|
|
126
|
+
win.setTitleBarOverlay(overlay(titlebarThemes.get(win), win.webContents.getZoomFactor()))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function setPinchZoomEnabled(enabled: boolean) {
|
|
130
|
+
getStore().set(PINCH_ZOOM_ENABLED_KEY, enabled)
|
|
131
|
+
for (const win of BrowserWindow.getAllWindows()) {
|
|
132
|
+
pinchZoomEnabled.set(win, enabled)
|
|
133
|
+
win.webContents.send("pinch-zoom-enabled-changed", enabled)
|
|
134
|
+
if (!enabled && win.webContents.getZoomFactor() !== 1) win.webContents.setZoomFactor(1)
|
|
135
|
+
updateZoom(win)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getPinchZoomEnabled() {
|
|
140
|
+
return getStore().get(PINCH_ZOOM_ENABLED_KEY) === true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getWindowID(win: BrowserWindow) {
|
|
144
|
+
return windowIDs.get(win)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getLastFocusedWindow() {
|
|
148
|
+
const focused = BrowserWindow.getFocusedWindow()
|
|
149
|
+
if (focused) return focused
|
|
150
|
+
const win = registry.lastFocused()
|
|
151
|
+
if (!win || win.isDestroyed()) return null
|
|
152
|
+
return win
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function restoreMainWindows() {
|
|
156
|
+
const ids = registry.persisted()
|
|
157
|
+
return (ids.length ? ids : [randomUUID()]).map((id) => createMainWindow(id))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function setDockIcon() {
|
|
161
|
+
if (process.platform !== "darwin") return
|
|
162
|
+
const icon = nativeImage.createFromPath(join(iconsDir(), "dock.png"))
|
|
163
|
+
if (!icon.isEmpty()) app.dock?.setIcon(icon)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createMainWindow(id: string = randomUUID()) {
|
|
167
|
+
const state = windowState({
|
|
168
|
+
file: windowStateFile(id),
|
|
169
|
+
defaultWidth: 1280,
|
|
170
|
+
defaultHeight: 800,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const mode = tone()
|
|
174
|
+
const win = new BrowserWindow({
|
|
175
|
+
x: state.x,
|
|
176
|
+
y: state.y,
|
|
177
|
+
width: state.width,
|
|
178
|
+
height: state.height,
|
|
179
|
+
show: false,
|
|
180
|
+
autoHideMenuBar: true,
|
|
181
|
+
title: "OpenCode",
|
|
182
|
+
icon: iconPath(),
|
|
183
|
+
backgroundColor: backgroundColor ?? defaultBackgroundColor(),
|
|
184
|
+
...(process.platform === "darwin"
|
|
185
|
+
? {
|
|
186
|
+
titleBarStyle: "hidden" as const,
|
|
187
|
+
trafficLightPosition: { x: 14, y: 14 },
|
|
188
|
+
}
|
|
189
|
+
: {}),
|
|
190
|
+
...(process.platform === "win32"
|
|
191
|
+
? {
|
|
192
|
+
frame: false,
|
|
193
|
+
titleBarStyle: "hidden" as const,
|
|
194
|
+
titleBarOverlay: overlay({ mode }),
|
|
195
|
+
}
|
|
196
|
+
: {}),
|
|
197
|
+
webPreferences: {
|
|
198
|
+
preload: join(root, "../preload/index.js"),
|
|
199
|
+
contextIsolation: true,
|
|
200
|
+
nodeIntegration: false,
|
|
201
|
+
sandbox: true,
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
allowRendererPermissions(win)
|
|
206
|
+
wireWindowRecovery(win, id)
|
|
207
|
+
|
|
208
|
+
win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
|
|
209
|
+
const { requestHeaders } = details
|
|
210
|
+
upsertKeyValue(requestHeaders, "Access-Control-Allow-Origin", ["*"])
|
|
211
|
+
callback({ requestHeaders })
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
|
215
|
+
const { responseHeaders = {} } = details
|
|
216
|
+
addRendererHeaders(details.url, responseHeaders)
|
|
217
|
+
callback({ responseHeaders })
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
state.manage(win)
|
|
221
|
+
registerWindow(win, id)
|
|
222
|
+
loadWindow(win, "index.html")
|
|
223
|
+
wireZoom(win)
|
|
224
|
+
|
|
225
|
+
win.once("ready-to-show", () => {
|
|
226
|
+
win.show()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
return win
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function registerWindow(win: BrowserWindow, id: string) {
|
|
233
|
+
windowIDs.set(win, id)
|
|
234
|
+
registry.register(id, win)
|
|
235
|
+
|
|
236
|
+
win.on("focus", () => registry.focused(id))
|
|
237
|
+
// Windows never emits before-quit on OS shutdown/logoff, but each window
|
|
238
|
+
// gets session-end before it closes; flag the quit so ids stay persisted.
|
|
239
|
+
win.on("session-end", () => registry.setQuitting())
|
|
240
|
+
win.on("closed", () => registry.closed(id))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function windowStateFile(id: string) {
|
|
244
|
+
return `window-state-${id.replace(/[^a-zA-Z0-9._-]/g, "-")}.json`
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Mirrors windowStorage() in packages/app/src/utils/persist.ts, which names
|
|
248
|
+
// the per-window renderer store this window persists its tabs into.
|
|
249
|
+
function windowDataFile(id: string) {
|
|
250
|
+
return `neurocode.window.${id.replace(/[^a-zA-Z0-9._-]/g, "-")}.dat`
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function registerRendererProtocol() {
|
|
254
|
+
if (protocol.isProtocolHandled(rendererProtocol)) return
|
|
255
|
+
|
|
256
|
+
protocol.handle(rendererProtocol, async (request) => {
|
|
257
|
+
const url = new URL(request.url)
|
|
258
|
+
if (url.host !== rendererHost) {
|
|
259
|
+
writeLog("protocol", "rejected host", { url: request.url }, "warn")
|
|
260
|
+
return new Response("Not found", { status: 404 })
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const file = resolve(rendererRoot, `.${decodeURIComponent(url.pathname)}`)
|
|
264
|
+
const rel = relative(rendererRoot, file)
|
|
265
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
266
|
+
writeLog("protocol", "rejected path", { url: request.url, file }, "warn")
|
|
267
|
+
return new Response("Not found", { status: 404 })
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const range = request.headers.get("range")
|
|
272
|
+
const response = await net.fetch(pathToFileURL(file).toString(), {
|
|
273
|
+
headers: range ? { range } : undefined,
|
|
274
|
+
})
|
|
275
|
+
if (response.status >= 400) {
|
|
276
|
+
writeLog(
|
|
277
|
+
"protocol",
|
|
278
|
+
"fetch failed",
|
|
279
|
+
{
|
|
280
|
+
url: request.url,
|
|
281
|
+
file,
|
|
282
|
+
status: response.status,
|
|
283
|
+
statusText: response.statusText,
|
|
284
|
+
},
|
|
285
|
+
"error",
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
return addDocumentPolicy(response, file)
|
|
289
|
+
} catch (error) {
|
|
290
|
+
writeLog("protocol", "fetch error", { url: request.url, file, error }, "error")
|
|
291
|
+
return new Response("Not found", { status: 404 })
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function loadWindow(win: BrowserWindow, html: string) {
|
|
297
|
+
const devUrl = process.env.ELECTRON_RENDERER_URL
|
|
298
|
+
if (devUrl) {
|
|
299
|
+
const url = new URL(html, devUrl)
|
|
300
|
+
void win.loadURL(url.toString())
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function wireWindowRecovery(win: BrowserWindow, name: string) {
|
|
308
|
+
let showing = false
|
|
309
|
+
const sampler = createUnresponsiveSampler(win, name)
|
|
310
|
+
|
|
311
|
+
const handle = async (button: string | undefined, wait: boolean) => {
|
|
312
|
+
if (button === "Export Logs") {
|
|
313
|
+
const sampling = sampler.stopAndFlush()
|
|
314
|
+
await exportDebugLogs().catch((error) => writeLog("main", "failed to export debug logs", { error }, "error"))
|
|
315
|
+
if (wait && sampling) sampler.start()
|
|
316
|
+
return true
|
|
317
|
+
}
|
|
318
|
+
if (button === "Relaunch") {
|
|
319
|
+
sampler.stopAndFlush()
|
|
320
|
+
relaunchHandler()
|
|
321
|
+
return false
|
|
322
|
+
}
|
|
323
|
+
if (button === "Quit") {
|
|
324
|
+
sampler.stopAndFlush()
|
|
325
|
+
app.quit()
|
|
326
|
+
}
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const show = async (message: string, detail: string, wait: boolean) => {
|
|
331
|
+
if (showing || win.isDestroyed()) return
|
|
332
|
+
showing = true
|
|
333
|
+
try {
|
|
334
|
+
while (!win.isDestroyed()) {
|
|
335
|
+
const buttons = wait ? ["Relaunch", "Export Logs", "Keep Waiting"] : ["Relaunch", "Export Logs", "Quit"]
|
|
336
|
+
const result = await dialog.showMessageBox(win, {
|
|
337
|
+
type: "warning",
|
|
338
|
+
buttons,
|
|
339
|
+
defaultId: 0,
|
|
340
|
+
cancelId: 2,
|
|
341
|
+
message,
|
|
342
|
+
detail,
|
|
343
|
+
})
|
|
344
|
+
if (await handle(buttons[result.response], wait)) continue
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
} finally {
|
|
348
|
+
showing = false
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const failed = (
|
|
353
|
+
event: string,
|
|
354
|
+
errorCode: number,
|
|
355
|
+
errorDescription: string,
|
|
356
|
+
validatedURL: string,
|
|
357
|
+
isMainFrame: boolean,
|
|
358
|
+
) => {
|
|
359
|
+
writeLog(
|
|
360
|
+
"window",
|
|
361
|
+
"renderer load failed",
|
|
362
|
+
{
|
|
363
|
+
window: name,
|
|
364
|
+
event,
|
|
365
|
+
errorCode,
|
|
366
|
+
errorDescription,
|
|
367
|
+
validatedURL,
|
|
368
|
+
currentURL: safeWindowURL(win),
|
|
369
|
+
isMainFrame,
|
|
370
|
+
},
|
|
371
|
+
"error",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if (!isMainFrame || errorCode === -3) return
|
|
375
|
+
void show(
|
|
376
|
+
"OpenCode failed to load",
|
|
377
|
+
[`Window: ${name}`, `URL: ${validatedURL}`, `Error: ${errorCode} ${errorDescription}`].join("\n"),
|
|
378
|
+
false,
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
win.webContents.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
|
383
|
+
failed("did-fail-load", errorCode, errorDescription, validatedURL, isMainFrame)
|
|
384
|
+
})
|
|
385
|
+
win.webContents.on("did-fail-provisional-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
|
386
|
+
failed("did-fail-provisional-load", errorCode, errorDescription, validatedURL, isMainFrame)
|
|
387
|
+
})
|
|
388
|
+
win.webContents.on("render-process-gone", (_event, details) => {
|
|
389
|
+
sampler.stopAndFlush()
|
|
390
|
+
writeLog("window", "renderer process gone", { window: name, currentURL: safeWindowURL(win), details }, "error")
|
|
391
|
+
void show(
|
|
392
|
+
"OpenCode window terminated unexpectedly",
|
|
393
|
+
[`Window: ${name}`, `Reason: ${details.reason}`, `Code: ${details.exitCode ?? "<unknown>"}`].join("\n"),
|
|
394
|
+
false,
|
|
395
|
+
)
|
|
396
|
+
})
|
|
397
|
+
win.on("unresponsive", () => {
|
|
398
|
+
writeLog("window", "renderer unresponsive", { window: name, currentURL: safeWindowURL(win) }, "error")
|
|
399
|
+
sampler.start()
|
|
400
|
+
void show("OpenCode is not responding", "You can relaunch the app, open the logs, or keep waiting.", true)
|
|
401
|
+
})
|
|
402
|
+
win.on("responsive", () => {
|
|
403
|
+
writeLog("window", "renderer responsive", { window: name, currentURL: safeWindowURL(win) }, "error")
|
|
404
|
+
sampler.stopAndFlush()
|
|
405
|
+
})
|
|
406
|
+
win.webContents.on("console-message", (_event, level, message, line, sourceId) => {
|
|
407
|
+
if (message.toLowerCase().includes("terminal") || sourceId.toLowerCase().includes("terminal")) {
|
|
408
|
+
writeLog("pty", "console", { window: name, level, message, line, sourceId })
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
win.webContents.on("preload-error", (_event, preloadPath, error) => {
|
|
412
|
+
writeLog("preload", "preload error", { window: name, preloadPath, error }, "error")
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function addDocumentPolicy(response: Response, file: string) {
|
|
417
|
+
if (!file.toLowerCase().endsWith(".html")) return response
|
|
418
|
+
const headers = new Headers(response.headers)
|
|
419
|
+
headers.set(documentPolicyHeader, jsCallStacksDocumentPolicy)
|
|
420
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers })
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function allowRendererPermissions(win: BrowserWindow) {
|
|
424
|
+
const webContentsId = win.webContents.id
|
|
425
|
+
|
|
426
|
+
win.webContents.session.setPermissionRequestHandler((webContents, permission, callback, details) => {
|
|
427
|
+
callback(
|
|
428
|
+
rendererPermissions.has(permission) &&
|
|
429
|
+
isTrustedRendererUrl(details.requestingUrl) &&
|
|
430
|
+
webContents.id === webContentsId,
|
|
431
|
+
)
|
|
432
|
+
})
|
|
433
|
+
win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
|
|
434
|
+
if (!rendererPermissions.has(permission)) return false
|
|
435
|
+
if (webContents && webContents.id !== webContentsId) return false
|
|
436
|
+
return isTrustedRendererUrl(details.requestingUrl) || isTrustedRendererUrl(requestingOrigin)
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function isTrustedRendererUrl(value?: string) {
|
|
441
|
+
return isRendererUrl(value)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function addRendererHeaders(value: string, headers: Record<string, any>) {
|
|
445
|
+
upsertKeyValue(headers, "Access-Control-Allow-Origin", ["*"])
|
|
446
|
+
upsertKeyValue(headers, "Access-Control-Allow-Headers", ["*"])
|
|
447
|
+
if (isRendererUrl(value, true)) upsertKeyValue(headers, documentPolicyHeader, [jsCallStacksDocumentPolicy])
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isRendererUrl(value?: string, html = false) {
|
|
451
|
+
if (!value || !URL.canParse(value)) return false
|
|
452
|
+
const url = new URL(value)
|
|
453
|
+
if (html && !url.pathname.endsWith(".html")) return false
|
|
454
|
+
if (url.protocol === `${rendererProtocol}:` && url.host === rendererHost) return true
|
|
455
|
+
const devUrl = process.env.ELECTRON_RENDERER_URL
|
|
456
|
+
if (!devUrl || !URL.canParse(devUrl)) return false
|
|
457
|
+
return url.origin === new URL(devUrl).origin
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function wireZoom(win: BrowserWindow) {
|
|
461
|
+
pinchZoomEnabled.set(win, getPinchZoomEnabled())
|
|
462
|
+
win.webContents.setZoomFactor(1)
|
|
463
|
+
win.webContents.on("zoom-changed", (event, zoomDirection) => {
|
|
464
|
+
event.preventDefault()
|
|
465
|
+
if (pinchZoomEnabled.get(win)) {
|
|
466
|
+
win.webContents.setZoomFactor(clampZoom(win.webContents.getZoomFactor() + (zoomDirection === "in" ? 0.2 : -0.2)))
|
|
467
|
+
updateZoom(win)
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
if (win.webContents.getZoomFactor() !== 1) win.webContents.setZoomFactor(1)
|
|
471
|
+
updateZoom(win)
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function clampZoom(value: number) {
|
|
476
|
+
return Math.min(Math.max(value, minZoomLevel), maxZoomLevel)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function updateZoom(win: BrowserWindow) {
|
|
480
|
+
updateTitlebar(win)
|
|
481
|
+
win.webContents.send("zoom-factor-changed", win.webContents.getZoomFactor())
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function upsertKeyValue(obj: Record<string, any>, keyToChange: string, value: any) {
|
|
485
|
+
const keyToChangeLower = keyToChange.toLowerCase()
|
|
486
|
+
for (const key of Object.keys(obj)) {
|
|
487
|
+
if (key.toLowerCase() === keyToChangeLower) {
|
|
488
|
+
// Reassign old key
|
|
489
|
+
obj[key] = value
|
|
490
|
+
// Done
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Insert at end instead
|
|
495
|
+
obj[keyToChange] = value
|
|
496
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { app, ipcMain } from "electron"
|
|
2
|
+
import type { IpcMainInvokeEvent } from "electron"
|
|
3
|
+
import type { WslServersController } from "./servers"
|
|
4
|
+
import { requireWslIpcString, requireWslIpcStrings } from "./policy"
|
|
5
|
+
import type { WslServersState } from "../../preload/types"
|
|
6
|
+
|
|
7
|
+
export function registerWslIpcHandlers(controller: WslServersController) {
|
|
8
|
+
if (process.platform !== "win32") {
|
|
9
|
+
registerUnavailableWslIpcHandlers()
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const subscriptions = new Map<number, () => void>()
|
|
14
|
+
const unsubscribe = (id: number) => {
|
|
15
|
+
const off = subscriptions.get(id)
|
|
16
|
+
if (!off) return
|
|
17
|
+
off()
|
|
18
|
+
subscriptions.delete(id)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
app.once("will-quit", () => {
|
|
22
|
+
subscriptions.forEach((off) => off())
|
|
23
|
+
subscriptions.clear()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
ipcMain.handle("wsl-servers-subscribe", (event) => {
|
|
27
|
+
const id = event.sender.id
|
|
28
|
+
if (subscriptions.has(id)) return
|
|
29
|
+
subscriptions.set(
|
|
30
|
+
id,
|
|
31
|
+
controller.subscribe((payload) => {
|
|
32
|
+
if (event.sender.isDestroyed()) {
|
|
33
|
+
unsubscribe(id)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
event.sender.send("wsl-servers-event", payload)
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
event.sender.once("destroyed", () => unsubscribe(id))
|
|
40
|
+
})
|
|
41
|
+
ipcMain.handle("wsl-servers-unsubscribe", (event) => unsubscribe(event.sender.id))
|
|
42
|
+
ipcMain.handle("wsl-servers-get-state", () => controller.getState())
|
|
43
|
+
ipcMain.handle("wsl-servers-probe-runtime", () => controller.probeRuntime())
|
|
44
|
+
ipcMain.handle("wsl-servers-refresh-distros", () => controller.refreshDistros())
|
|
45
|
+
ipcMain.handle("wsl-servers-install-wsl", () => controller.installWsl())
|
|
46
|
+
ipcMain.handle("wsl-servers-install-distro", (_event: IpcMainInvokeEvent, name: string) =>
|
|
47
|
+
controller.installDistro(requireWslIpcString("distro", name)),
|
|
48
|
+
)
|
|
49
|
+
ipcMain.handle("wsl-servers-probe-addable", (_event: IpcMainInvokeEvent, distros: string[]) =>
|
|
50
|
+
controller.probeAddable(requireWslIpcStrings("distro", distros)),
|
|
51
|
+
)
|
|
52
|
+
ipcMain.handle("wsl-servers-install-neurocode", (_event: IpcMainInvokeEvent, name: string) =>
|
|
53
|
+
controller.installNeurocode(requireWslIpcString("distro", name)),
|
|
54
|
+
)
|
|
55
|
+
ipcMain.handle("wsl-servers-open-terminal", (_event: IpcMainInvokeEvent, name: string) =>
|
|
56
|
+
controller.openTerminal(requireWslIpcString("distro", name)),
|
|
57
|
+
)
|
|
58
|
+
ipcMain.handle("wsl-servers-add", (_event: IpcMainInvokeEvent, distro: string) =>
|
|
59
|
+
controller.addServer(requireWslIpcString("distro", distro)),
|
|
60
|
+
)
|
|
61
|
+
ipcMain.handle("wsl-servers-remove", (_event: IpcMainInvokeEvent, id: string) =>
|
|
62
|
+
controller.removeServer(requireWslIpcString("server id", id)),
|
|
63
|
+
)
|
|
64
|
+
ipcMain.handle("wsl-servers-start", (_event: IpcMainInvokeEvent, id: string) =>
|
|
65
|
+
controller.startServer(requireWslIpcString("server id", id)),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function registerUnavailableWslIpcHandlers() {
|
|
70
|
+
const unavailable = () => {
|
|
71
|
+
throw new Error("WSL is only available on Windows")
|
|
72
|
+
}
|
|
73
|
+
const state = (): WslServersState => ({
|
|
74
|
+
runtime: {
|
|
75
|
+
available: false,
|
|
76
|
+
version: null,
|
|
77
|
+
error: "WSL is only available on Windows",
|
|
78
|
+
},
|
|
79
|
+
installed: [],
|
|
80
|
+
online: [],
|
|
81
|
+
distroProbes: {},
|
|
82
|
+
neurocodeChecks: {},
|
|
83
|
+
pendingRestart: false,
|
|
84
|
+
servers: [],
|
|
85
|
+
job: null,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
ipcMain.handle("wsl-servers-subscribe", (event) => {
|
|
89
|
+
event.sender.send("wsl-servers-event", { type: "state", state: state() })
|
|
90
|
+
})
|
|
91
|
+
ipcMain.handle("wsl-servers-unsubscribe", () => undefined)
|
|
92
|
+
ipcMain.handle("wsl-servers-get-state", () => state())
|
|
93
|
+
ipcMain.handle("wsl-servers-probe-runtime", unavailable)
|
|
94
|
+
ipcMain.handle("wsl-servers-refresh-distros", unavailable)
|
|
95
|
+
ipcMain.handle("wsl-servers-install-wsl", unavailable)
|
|
96
|
+
ipcMain.handle("wsl-servers-install-distro", unavailable)
|
|
97
|
+
ipcMain.handle("wsl-servers-probe-addable", unavailable)
|
|
98
|
+
ipcMain.handle("wsl-servers-install-neurocode", unavailable)
|
|
99
|
+
ipcMain.handle("wsl-servers-open-terminal", unavailable)
|
|
100
|
+
ipcMain.handle("wsl-servers-add", unavailable)
|
|
101
|
+
ipcMain.handle("wsl-servers-remove", unavailable)
|
|
102
|
+
ipcMain.handle("wsl-servers-start", unavailable)
|
|
103
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { WslDistroProbe, WslNeurocodeCheck, WslServerItem } from "../../preload/types"
|
|
2
|
+
|
|
3
|
+
export function wslServerIdToRestart(servers: WslServerItem[], distro: string) {
|
|
4
|
+
return servers.find((item) => item.config.distro === distro)?.config.id
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function clearWslDistroState(
|
|
8
|
+
distroProbes: Record<string, WslDistroProbe>,
|
|
9
|
+
neurocodeChecks: Record<string, WslNeurocodeCheck>,
|
|
10
|
+
distro: string,
|
|
11
|
+
) {
|
|
12
|
+
const nextDistroProbes = { ...distroProbes }
|
|
13
|
+
const nextNeurocodeChecks = { ...neurocodeChecks }
|
|
14
|
+
delete nextDistroProbes[distro]
|
|
15
|
+
delete nextNeurocodeChecks[distro]
|
|
16
|
+
return { distroProbes: nextDistroProbes, neurocodeChecks: nextNeurocodeChecks }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function wslTerminalArgs(distro?: string | null) {
|
|
20
|
+
return ["/c", "start", "", "wsl", ...(distro ? ["-d", distro] : [])]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function requireWslIpcString(name: string, value: unknown) {
|
|
24
|
+
if (typeof value === "string" && value.length > 0) return value
|
|
25
|
+
throw new Error(`Invalid ${name}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function requireWslIpcStrings(name: string, value: unknown) {
|
|
29
|
+
if (!Array.isArray(value)) throw new Error(`Invalid ${name}`)
|
|
30
|
+
const values = value.map((item) => requireWslIpcString(name, item))
|
|
31
|
+
if (values.length > 0) return values
|
|
32
|
+
throw new Error(`Invalid ${name}`)
|
|
33
|
+
}
|