@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
package/src/main/ipc.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { execFile } from "node:child_process"
|
|
2
|
+
import { stat } from "node:fs/promises"
|
|
3
|
+
import { basename } from "node:path"
|
|
4
|
+
import { app, BrowserWindow, Notification, clipboard, dialog, ipcMain, shell } from "electron"
|
|
5
|
+
import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
|
|
6
|
+
import type { DesktopMenuAction } from "@neurocode-ai/app/desktop-menu"
|
|
7
|
+
|
|
8
|
+
import type { FatalRendererError, ServerReadyData, TitlebarTheme } from "../preload/types"
|
|
9
|
+
import { runDesktopMenuAction } from "./desktop-menu-actions"
|
|
10
|
+
import { setForceFocus } from "./debug"
|
|
11
|
+
import { assertAttachmentBudget, createPickedFileAuthorizations } from "./attachment-picker"
|
|
12
|
+
import { getStore, removeStoreFileIfEmpty } from "./store"
|
|
13
|
+
import { getPinchZoomEnabled, getWindowID, setPinchZoomEnabled, setTitlebar, updateTitlebar } from "./windows"
|
|
14
|
+
import type { UpdaterController } from "./updater-controller"
|
|
15
|
+
import { createUpdaterSubscriptions } from "./updater-subscriptions"
|
|
16
|
+
|
|
17
|
+
const pickerFilters = (ext?: string[]) => {
|
|
18
|
+
if (!ext || ext.length === 0) return undefined
|
|
19
|
+
return [{ name: "Files", extensions: ext }]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pickedFiles = createPickedFileAuthorizations()
|
|
23
|
+
|
|
24
|
+
type Deps = {
|
|
25
|
+
killSidecar: () => Promise<void> | void
|
|
26
|
+
relaunch: () => void
|
|
27
|
+
awaitInitialization: () => Promise<ServerReadyData>
|
|
28
|
+
consumeInitialDeepLinks: () => Promise<string[]> | string[]
|
|
29
|
+
getDefaultServerUrl: () => Promise<string | null> | string | null
|
|
30
|
+
setDefaultServerUrl: (url: string | null) => Promise<void> | void
|
|
31
|
+
isFirstLaunchOnboardingPending: () => Promise<boolean> | boolean
|
|
32
|
+
finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise<string | null> | string | null
|
|
33
|
+
isOldLayoutEligible: () => Promise<boolean> | boolean
|
|
34
|
+
getDisplayBackend: () => Promise<string | null>
|
|
35
|
+
setDisplayBackend: (backend: string | null) => Promise<void> | void
|
|
36
|
+
parseMarkdown: (markdown: string) => Promise<string> | string
|
|
37
|
+
checkAppExists: (appName: string) => Promise<boolean> | boolean
|
|
38
|
+
resolveAppPath: (appName: string) => Promise<string | null>
|
|
39
|
+
updater: UpdaterController
|
|
40
|
+
showUpdater: () => Promise<void> | void
|
|
41
|
+
setBackgroundColor: (color: string) => void
|
|
42
|
+
exportDebugLogs: () => Promise<string>
|
|
43
|
+
recordFatalRendererError: (error: FatalRendererError) => Promise<void> | void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function registerIpcHandlers(deps: Deps) {
|
|
47
|
+
const updaterSubscriptions = createUpdaterSubscriptions()
|
|
48
|
+
app.once("will-quit", updaterSubscriptions.clear)
|
|
49
|
+
|
|
50
|
+
ipcMain.handle("kill-sidecar", () => deps.killSidecar())
|
|
51
|
+
ipcMain.handle("await-initialization", () => deps.awaitInitialization())
|
|
52
|
+
ipcMain.handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks())
|
|
53
|
+
ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl())
|
|
54
|
+
ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
|
|
55
|
+
deps.setDefaultServerUrl(url),
|
|
56
|
+
)
|
|
57
|
+
ipcMain.handle("is-first-launch-onboarding-pending", () => deps.isFirstLaunchOnboardingPending())
|
|
58
|
+
ipcMain.handle("finish-first-launch-onboarding", (_event: IpcMainInvokeEvent, createDefaultProject: boolean) =>
|
|
59
|
+
deps.finishFirstLaunchOnboarding(createDefaultProject),
|
|
60
|
+
)
|
|
61
|
+
ipcMain.handle("is-old-layout-eligible", () => deps.isOldLayoutEligible())
|
|
62
|
+
ipcMain.handle("get-display-backend", () => deps.getDisplayBackend())
|
|
63
|
+
ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) =>
|
|
64
|
+
deps.setDisplayBackend(backend),
|
|
65
|
+
)
|
|
66
|
+
ipcMain.handle("parse-markdown", (_event: IpcMainInvokeEvent, markdown: string) => deps.parseMarkdown(markdown))
|
|
67
|
+
ipcMain.handle("check-app-exists", (_event: IpcMainInvokeEvent, appName: string) => deps.checkAppExists(appName))
|
|
68
|
+
ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName))
|
|
69
|
+
ipcMain.handle("updater-subscribe", (event) => {
|
|
70
|
+
const id = event.sender.id
|
|
71
|
+
updaterSubscriptions.set(
|
|
72
|
+
id,
|
|
73
|
+
deps.updater.subscribe((state) => {
|
|
74
|
+
if (event.sender.isDestroyed()) return updaterSubscriptions.delete(id)
|
|
75
|
+
event.sender.send("updater-state", state)
|
|
76
|
+
}),
|
|
77
|
+
)
|
|
78
|
+
event.sender.once("destroyed", () => updaterSubscriptions.delete(id))
|
|
79
|
+
})
|
|
80
|
+
ipcMain.handle("updater-unsubscribe", (event) => updaterSubscriptions.delete(event.sender.id))
|
|
81
|
+
ipcMain.handle("updater-check", () => deps.updater.check())
|
|
82
|
+
ipcMain.handle("updater-install", () => deps.updater.install())
|
|
83
|
+
ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color))
|
|
84
|
+
ipcMain.handle("export-debug-logs", () => deps.exportDebugLogs())
|
|
85
|
+
ipcMain.handle("set-force-focus", (event: IpcMainInvokeEvent, enabled: boolean) =>
|
|
86
|
+
setForceFocus(event.sender, enabled),
|
|
87
|
+
)
|
|
88
|
+
ipcMain.handle("record-fatal-renderer-error", (_event: IpcMainInvokeEvent, error: FatalRendererError) =>
|
|
89
|
+
deps.recordFatalRendererError(error),
|
|
90
|
+
)
|
|
91
|
+
ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
|
92
|
+
try {
|
|
93
|
+
const store = getStore(name)
|
|
94
|
+
const value = store.get(key)
|
|
95
|
+
if (value === undefined || value === null) return null
|
|
96
|
+
return typeof value === "string" ? value : JSON.stringify(value)
|
|
97
|
+
} catch {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
ipcMain.handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => {
|
|
102
|
+
getStore(name).set(key, value)
|
|
103
|
+
})
|
|
104
|
+
ipcMain.handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
|
105
|
+
getStore(name).delete(key)
|
|
106
|
+
void removeStoreFileIfEmpty(name)
|
|
107
|
+
})
|
|
108
|
+
ipcMain.handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => {
|
|
109
|
+
getStore(name).clear()
|
|
110
|
+
void removeStoreFileIfEmpty(name)
|
|
111
|
+
})
|
|
112
|
+
ipcMain.handle("store-keys", (_event: IpcMainInvokeEvent, name: string) => {
|
|
113
|
+
const store = getStore(name)
|
|
114
|
+
return Object.keys(store.store)
|
|
115
|
+
})
|
|
116
|
+
ipcMain.handle("store-length", (_event: IpcMainInvokeEvent, name: string) => {
|
|
117
|
+
const store = getStore(name)
|
|
118
|
+
return Object.keys(store.store).length
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
ipcMain.handle(
|
|
122
|
+
"open-directory-picker",
|
|
123
|
+
async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => {
|
|
124
|
+
const result = await dialog.showOpenDialog({
|
|
125
|
+
properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : []), "createDirectory"],
|
|
126
|
+
title: opts?.title ?? "Choose a folder",
|
|
127
|
+
defaultPath: opts?.defaultPath,
|
|
128
|
+
})
|
|
129
|
+
if (result.canceled) return null
|
|
130
|
+
return opts?.multiple ? result.filePaths : result.filePaths[0]
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
ipcMain.handle(
|
|
135
|
+
"open-file-picker",
|
|
136
|
+
async (
|
|
137
|
+
event: IpcMainInvokeEvent,
|
|
138
|
+
opts?: { multiple?: boolean; title?: string; defaultPath?: string; extensions?: string[] },
|
|
139
|
+
) => {
|
|
140
|
+
const result = await dialog.showOpenDialog({
|
|
141
|
+
properties: ["openFile", ...(opts?.multiple ? ["multiSelections" as const] : [])],
|
|
142
|
+
title: opts?.title ?? "Choose a file",
|
|
143
|
+
defaultPath: opts?.defaultPath,
|
|
144
|
+
filters: pickerFilters(opts?.extensions),
|
|
145
|
+
})
|
|
146
|
+
if (result.canceled) return null
|
|
147
|
+
const files = await Promise.all(
|
|
148
|
+
result.filePaths.map(async (filePath) => ({
|
|
149
|
+
path: filePath,
|
|
150
|
+
name: basename(filePath),
|
|
151
|
+
size: (await stat(filePath)).size,
|
|
152
|
+
})),
|
|
153
|
+
)
|
|
154
|
+
assertAttachmentBudget(files)
|
|
155
|
+
const token = pickedFiles.add(event.sender.id, result.filePaths)
|
|
156
|
+
return { token, files }
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
ipcMain.handle("read-picked-file", async (event: IpcMainInvokeEvent, token: string, filePath: string) => {
|
|
161
|
+
return pickedFiles.read(event.sender.id, token, filePath)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
ipcMain.handle("release-picked-files", (event: IpcMainInvokeEvent, token: string) => {
|
|
165
|
+
pickedFiles.release(event.sender.id, token)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
ipcMain.handle(
|
|
169
|
+
"save-file-picker",
|
|
170
|
+
async (_event: IpcMainInvokeEvent, opts?: { title?: string; defaultPath?: string }) => {
|
|
171
|
+
const result = await dialog.showSaveDialog({
|
|
172
|
+
title: opts?.title ?? "Save file",
|
|
173
|
+
defaultPath: opts?.defaultPath,
|
|
174
|
+
})
|
|
175
|
+
if (result.canceled) return null
|
|
176
|
+
return result.filePath ?? null
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
ipcMain.on("open-link", (_event: IpcMainEvent, url: string) => {
|
|
181
|
+
void shell.openExternal(url)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
ipcMain.handle("open-path", async (_event: IpcMainInvokeEvent, path: string, app?: string) => {
|
|
185
|
+
if (!app) return shell.openPath(path)
|
|
186
|
+
await new Promise<void>((resolve, reject) => {
|
|
187
|
+
const [cmd, args] =
|
|
188
|
+
process.platform === "darwin" ? (["open", ["-a", app, path]] as const) : ([app, [path]] as const)
|
|
189
|
+
execFile(cmd, args, (err) => (err ? reject(err) : resolve()))
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
ipcMain.handle("reveal-path", async (_event: IpcMainInvokeEvent, path: string) => {
|
|
194
|
+
const exists = await stat(path).then(
|
|
195
|
+
() => true,
|
|
196
|
+
() => false,
|
|
197
|
+
)
|
|
198
|
+
if (!exists) return false
|
|
199
|
+
shell.showItemInFolder(path)
|
|
200
|
+
return true
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
ipcMain.handle("read-clipboard-image", () => {
|
|
204
|
+
const image = clipboard.readImage()
|
|
205
|
+
if (image.isEmpty()) return null
|
|
206
|
+
const buffer = image.toPNG().buffer
|
|
207
|
+
const size = image.getSize()
|
|
208
|
+
return { buffer, width: size.width, height: size.height }
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
ipcMain.on("show-notification", (_event: IpcMainEvent, title: string, body?: string) => {
|
|
212
|
+
new Notification({ title, body }).show()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
ipcMain.handle("get-window-count", () => BrowserWindow.getAllWindows().length)
|
|
216
|
+
|
|
217
|
+
ipcMain.handle("get-window-id", (event: IpcMainInvokeEvent) => {
|
|
218
|
+
const win = BrowserWindow.fromWebContents(event.sender)
|
|
219
|
+
if (!win) throw new Error("Window not found")
|
|
220
|
+
const id = getWindowID(win)
|
|
221
|
+
if (!id) throw new Error("Window ID not found")
|
|
222
|
+
return id
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => {
|
|
226
|
+
const win = BrowserWindow.fromWebContents(event.sender)
|
|
227
|
+
return win?.isFocused() ?? false
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
ipcMain.handle("set-window-focus", (event: IpcMainInvokeEvent) => {
|
|
231
|
+
const win = BrowserWindow.fromWebContents(event.sender)
|
|
232
|
+
win?.focus()
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
ipcMain.handle("show-window", (event: IpcMainInvokeEvent) => {
|
|
236
|
+
const win = BrowserWindow.fromWebContents(event.sender)
|
|
237
|
+
win?.show()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
ipcMain.on("relaunch", () => {
|
|
241
|
+
deps.relaunch()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
|
|
245
|
+
ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => {
|
|
246
|
+
event.sender.setZoomFactor(factor)
|
|
247
|
+
const win = BrowserWindow.fromWebContents(event.sender)
|
|
248
|
+
if (!win) return
|
|
249
|
+
updateTitlebar(win)
|
|
250
|
+
})
|
|
251
|
+
ipcMain.handle("get-pinch-zoom-enabled", () => getPinchZoomEnabled())
|
|
252
|
+
ipcMain.handle("set-pinch-zoom-enabled", (_event: IpcMainInvokeEvent, enabled: boolean) => {
|
|
253
|
+
setPinchZoomEnabled(enabled)
|
|
254
|
+
})
|
|
255
|
+
ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
|
|
256
|
+
const win = BrowserWindow.fromWebContents(event.sender)
|
|
257
|
+
if (!win) return
|
|
258
|
+
setTitlebar(win, theme)
|
|
259
|
+
})
|
|
260
|
+
ipcMain.handle("run-desktop-menu-action", (event: IpcMainInvokeEvent, action: DesktopMenuAction) => {
|
|
261
|
+
runDesktopMenuAction(BrowserWindow.fromWebContents(event.sender), action, {
|
|
262
|
+
checkForUpdates: () => void deps.showUpdater(),
|
|
263
|
+
relaunch: deps.relaunch,
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function sendMenuCommand(win: BrowserWindow, id: string) {
|
|
269
|
+
win.webContents.send("menu-command", id)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function sendDeepLinks(win: BrowserWindow, urls: string[]) {
|
|
273
|
+
win.webContents.send("deep-link", urls)
|
|
274
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { MainLogger } from "electron-log"
|
|
2
|
+
import log from "electron-log/main.js"
|
|
3
|
+
import { app, crashReporter, netLog, shell } from "electron"
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"
|
|
5
|
+
import { ZipWriter, BlobWriter, BlobReader } from "@zip.js/zip.js"
|
|
6
|
+
import { dirname, join } from "node:path"
|
|
7
|
+
import { homedir } from "node:os"
|
|
8
|
+
|
|
9
|
+
const MAX_LOG_AGE_DAYS = 7
|
|
10
|
+
const TAIL_LINES = 1000
|
|
11
|
+
const EXPORT_WINDOW = 24 * 60 * 60 * 1000
|
|
12
|
+
const MAX_EXPORT_FILE_SIZE = 50 * 1024 * 1024
|
|
13
|
+
const NET_LOG_SIZE = 20 * 1024 * 1024
|
|
14
|
+
|
|
15
|
+
let root = ""
|
|
16
|
+
let run = ""
|
|
17
|
+
let netLogPath: string | undefined
|
|
18
|
+
|
|
19
|
+
let logger: MainLogger
|
|
20
|
+
export const getLogger = () => logger
|
|
21
|
+
|
|
22
|
+
export function initLogging() {
|
|
23
|
+
initRunDirectory()
|
|
24
|
+
log.transports.file.maxSize = 5 * 1024 * 1024
|
|
25
|
+
log.transports.file.resolvePathFn = (_vars, message) =>
|
|
26
|
+
join(
|
|
27
|
+
run,
|
|
28
|
+
`${safeLogName(message?.scope ?? (message?.variables?.processType === "renderer" ? "renderer" : "main"))}.log`,
|
|
29
|
+
)
|
|
30
|
+
log.initialize({ preload: false, spyRendererConsole: true })
|
|
31
|
+
initConsoleTransport()
|
|
32
|
+
cleanup()
|
|
33
|
+
return (logger = log)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function initCrashReporter() {
|
|
37
|
+
const dir = join(app.getPath("userData"), "Crashpad")
|
|
38
|
+
mkdirSync(dir, { recursive: true })
|
|
39
|
+
app.setPath("crashDumps", dir)
|
|
40
|
+
crashReporter.start({ uploadToServer: false, compress: true })
|
|
41
|
+
write("crash", "crash reporter started", { path: dir })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function startNetLog() {
|
|
45
|
+
if (netLog.currentlyLogging) return
|
|
46
|
+
netLogPath = join(run, "network.netlog")
|
|
47
|
+
await netLog.startLogging(netLogPath, { captureMode: "default", maxFileSize: NET_LOG_SIZE })
|
|
48
|
+
write("network", "net log started", { path: netLogPath })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function exportDebugLogs() {
|
|
52
|
+
const restartNetLog = netLog.currentlyLogging
|
|
53
|
+
if (restartNetLog) {
|
|
54
|
+
await netLog.stopLogging().catch((error) => write("network", "failed to stop net log", { error }))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const output = join(app.getPath("downloads"), `opencode-debug-${stamp()}.zip`)
|
|
58
|
+
try {
|
|
59
|
+
write("main", "exporting debug logs", { output })
|
|
60
|
+
await writeZip(output, [
|
|
61
|
+
{ name: "manifest.json", data: Buffer.from(JSON.stringify(manifest(), null, 2)) },
|
|
62
|
+
...collect(root, "desktop"),
|
|
63
|
+
...serverLogRoots().flatMap((dir, i) => collect(dir, `server-${i + 1}`)),
|
|
64
|
+
...collect(app.getPath("crashDumps"), "crashpad"),
|
|
65
|
+
])
|
|
66
|
+
shell.showItemInFolder(output)
|
|
67
|
+
return output
|
|
68
|
+
} finally {
|
|
69
|
+
if (restartNetLog) {
|
|
70
|
+
await startNetLog().catch((error) => write("network", "failed to restart net log", { error }))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function write(
|
|
76
|
+
name: string,
|
|
77
|
+
message: string,
|
|
78
|
+
extra?: Record<string, unknown>,
|
|
79
|
+
level: "info" | "warn" | "error" = "info",
|
|
80
|
+
) {
|
|
81
|
+
if (!run) return
|
|
82
|
+
const scoped = log.scope(safeLogName(name))
|
|
83
|
+
if (extra !== undefined) {
|
|
84
|
+
scoped[level](message, extra)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
scoped[level](message)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function tail(): string {
|
|
91
|
+
try {
|
|
92
|
+
const path = log.transports.file.getFile().path
|
|
93
|
+
const contents = readFileSync(path, "utf8")
|
|
94
|
+
const lines = contents.split("\n")
|
|
95
|
+
return lines.slice(Math.max(0, lines.length - TAIL_LINES)).join("\n")
|
|
96
|
+
} catch {
|
|
97
|
+
return ""
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function initRunDirectory() {
|
|
102
|
+
root = join(app.getPath("userData"), "logs")
|
|
103
|
+
run = join(root, stamp())
|
|
104
|
+
mkdirSync(run, { recursive: true })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stamp() {
|
|
108
|
+
return new Date()
|
|
109
|
+
.toISOString()
|
|
110
|
+
.replace(/[-:]/g, "")
|
|
111
|
+
.replace(/\.\d+Z$/, "")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function safeLogName(name: string) {
|
|
115
|
+
return name.replace(/[^a-z0-9_.-]/gi, "_") || "main"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cleanup() {
|
|
119
|
+
const dir = root || dirname(log.transports.file.getFile().path)
|
|
120
|
+
const cutoff = Date.now() - MAX_LOG_AGE_DAYS * 24 * 60 * 60 * 1000
|
|
121
|
+
|
|
122
|
+
for (const entry of readdirSync(dir)) {
|
|
123
|
+
const file = join(dir, entry)
|
|
124
|
+
try {
|
|
125
|
+
const info = statSync(file)
|
|
126
|
+
if (info.mtimeMs < cutoff) rmSync(file, { recursive: true, force: true })
|
|
127
|
+
} catch {
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function manifest() {
|
|
134
|
+
return {
|
|
135
|
+
generated: new Date().toISOString(),
|
|
136
|
+
version: app.getVersion(),
|
|
137
|
+
name: app.getName(),
|
|
138
|
+
packaged: app.isPackaged,
|
|
139
|
+
platform: process.platform,
|
|
140
|
+
arch: process.arch,
|
|
141
|
+
versions: process.versions,
|
|
142
|
+
uptime: process.uptime(),
|
|
143
|
+
userData: app.getPath("userData"),
|
|
144
|
+
logs: root,
|
|
145
|
+
currentRun: run,
|
|
146
|
+
crashDumps: app.getPath("crashDumps"),
|
|
147
|
+
serverLogs: serverLogRoots(),
|
|
148
|
+
netLog: netLogPath,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function serverLogRoots() {
|
|
153
|
+
const xdgData = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share")
|
|
154
|
+
return [...new Set([join(xdgData, "opencode", "log"), join(app.getPath("userData"), "opencode", "log")])]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type Entry = { name: string; path?: string; data?: Buffer }
|
|
158
|
+
|
|
159
|
+
function collect(dir: string, prefix: string): Entry[] {
|
|
160
|
+
if (!existsSync(dir)) return []
|
|
161
|
+
const cutoff = Date.now() - EXPORT_WINDOW
|
|
162
|
+
const result: Entry[] = []
|
|
163
|
+
const walk = (current: string) => {
|
|
164
|
+
for (const entry of readdirSync(current)) {
|
|
165
|
+
const file = join(current, entry)
|
|
166
|
+
const info = statSync(file)
|
|
167
|
+
if (info.isDirectory()) {
|
|
168
|
+
walk(file)
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (info.mtimeMs < cutoff) continue
|
|
172
|
+
if (info.size > MAX_EXPORT_FILE_SIZE) continue
|
|
173
|
+
if (file.endsWith(".heapsnapshot")) continue
|
|
174
|
+
result.push({ name: join(prefix, file.slice(dir.length + 1)).replace(/\\/g, "/"), path: file })
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
walk(dir)
|
|
178
|
+
return result
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function writeZip(output: string, entries: Entry[]) {
|
|
182
|
+
const writer = new ZipWriter(new BlobWriter("application/zip"))
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
const data = entry.data ?? readFileSync(entry.path!)
|
|
185
|
+
await writer.add(entry.name, new BlobReader(new Blob([new Uint8Array(data)])))
|
|
186
|
+
}
|
|
187
|
+
const zip = await writer.close()
|
|
188
|
+
writeFileSync(output, Buffer.from(await zip.arrayBuffer()))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function initConsoleTransport() {
|
|
192
|
+
const write = log.transports.console.writeFn.bind(log.transports.console)
|
|
193
|
+
log.transports.console.writeFn = (options) => {
|
|
194
|
+
try {
|
|
195
|
+
write(options)
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (!isBrokenPipe(err)) throw err
|
|
198
|
+
log.transports.console.level = false
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isBrokenPipe(err: unknown) {
|
|
204
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "EPIPE"
|
|
205
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { marked, type Tokens } from "marked"
|
|
2
|
+
|
|
3
|
+
const renderer = new marked.Renderer()
|
|
4
|
+
|
|
5
|
+
renderer.link = ({ href, title, text }: Tokens.Link) => {
|
|
6
|
+
const titleAttr = title ? ` title="${title}"` : ""
|
|
7
|
+
return `<a href="${href}"${titleAttr} class="external-link" target="_blank" rel="noopener noreferrer">${text}</a>`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseMarkdown(input: string) {
|
|
11
|
+
return marked(input, {
|
|
12
|
+
renderer,
|
|
13
|
+
breaks: false,
|
|
14
|
+
gfm: true,
|
|
15
|
+
})
|
|
16
|
+
}
|
package/src/main/menu.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BrowserWindow, Menu, shell } from "electron"
|
|
2
|
+
import type { MenuItemConstructorOptions } from "electron"
|
|
3
|
+
import {
|
|
4
|
+
DESKTOP_MENU,
|
|
5
|
+
desktopMenuVisible,
|
|
6
|
+
type DesktopMenuEntry,
|
|
7
|
+
type DesktopMenuRole,
|
|
8
|
+
} from "@neurocode-ai/app/desktop-menu"
|
|
9
|
+
|
|
10
|
+
import { UPDATER_ENABLED } from "./constants"
|
|
11
|
+
import { runDesktopMenuAction } from "./desktop-menu-actions"
|
|
12
|
+
|
|
13
|
+
type Deps = {
|
|
14
|
+
trigger: (id: string) => void
|
|
15
|
+
checkForUpdates: () => void
|
|
16
|
+
relaunch: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createMenu(deps: Deps) {
|
|
20
|
+
if (process.platform !== "darwin") return
|
|
21
|
+
|
|
22
|
+
const template = DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "macos")).map((menu) => {
|
|
23
|
+
if (menu.role) return { role: nativeRole(menu.role) }
|
|
24
|
+
return {
|
|
25
|
+
label: menu.label,
|
|
26
|
+
submenu: menu.items
|
|
27
|
+
?.filter((entry) => desktopMenuVisible(entry, "macos"))
|
|
28
|
+
.map((entry) => nativeItem(entry, deps)),
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nativeItem(entry: DesktopMenuEntry, deps: Deps): MenuItemConstructorOptions {
|
|
36
|
+
if (entry.type === "separator") return { type: "separator" }
|
|
37
|
+
if (entry.role) return { role: nativeRole(entry.role) }
|
|
38
|
+
|
|
39
|
+
const item: MenuItemConstructorOptions = {
|
|
40
|
+
label: entry.label,
|
|
41
|
+
accelerator: entry.accelerator?.macos,
|
|
42
|
+
enabled: entry.enabled === "updater" ? UPDATER_ENABLED : undefined,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (entry.command) {
|
|
46
|
+
const command = entry.command
|
|
47
|
+
item.click = () => deps.trigger(command)
|
|
48
|
+
}
|
|
49
|
+
if (entry.action) {
|
|
50
|
+
const action = entry.action
|
|
51
|
+
item.click = () =>
|
|
52
|
+
runDesktopMenuAction(BrowserWindow.getFocusedWindow(), action, {
|
|
53
|
+
checkForUpdates: deps.checkForUpdates,
|
|
54
|
+
relaunch: deps.relaunch,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
if (entry.href) {
|
|
58
|
+
const href = entry.href
|
|
59
|
+
item.click = () => shell.openExternal(href)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return item
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function nativeRole(role: DesktopMenuRole) {
|
|
66
|
+
return role as NonNullable<MenuItemConstructorOptions["role"]>
|
|
67
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { app } from "electron"
|
|
2
|
+
import log from "electron-log/main.js"
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
4
|
+
import { homedir } from "node:os"
|
|
5
|
+
import { join } from "node:path"
|
|
6
|
+
import { CHANNEL } from "./constants"
|
|
7
|
+
import { getStore } from "./store"
|
|
8
|
+
|
|
9
|
+
const TAURI_MIGRATED_KEY = "tauriMigrated"
|
|
10
|
+
|
|
11
|
+
// Resolve the directory where Tauri stored its .dat files for the given app identifier.
|
|
12
|
+
// Mirrors Tauri's AppLocalData / AppData resolution per OS.
|
|
13
|
+
function tauriDir(id: string) {
|
|
14
|
+
switch (process.platform) {
|
|
15
|
+
case "darwin":
|
|
16
|
+
return join(homedir(), "Library", "Application Support", id)
|
|
17
|
+
case "win32":
|
|
18
|
+
return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), id)
|
|
19
|
+
default:
|
|
20
|
+
return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), id)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The Tauri app identifier changes between dev/beta/prod builds.
|
|
25
|
+
const TAURI_APP_IDS: Record<string, string> = {
|
|
26
|
+
dev: "ai.neurocode.desktop.dev",
|
|
27
|
+
beta: "ai.neurocode.desktop.beta",
|
|
28
|
+
prod: "ai.neurocode.desktop",
|
|
29
|
+
}
|
|
30
|
+
function tauriAppId() {
|
|
31
|
+
return app.isPackaged ? TAURI_APP_IDS[CHANNEL] : "ai.neurocode.desktop.dev"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Migrate a single Tauri .dat file into the corresponding electron-store.
|
|
35
|
+
// `neurocode.settings.dat` is special: it maps to the `neurocode.settings` store
|
|
36
|
+
// (the electron-store name without the `.dat` extension). All other .dat files
|
|
37
|
+
// keep their full filename as the electron-store name so they match what the
|
|
38
|
+
// renderer already passes via IPC (e.g. `"default.dat"`, `"neurocode.global.dat"`).
|
|
39
|
+
function migrateFile(datPath: string, filename: string) {
|
|
40
|
+
let data: Record<string, unknown>
|
|
41
|
+
try {
|
|
42
|
+
data = JSON.parse(readFileSync(datPath, "utf-8"))
|
|
43
|
+
} catch (err) {
|
|
44
|
+
log.warn("tauri migration: failed to parse", filename, err)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// neurocode.settings.dat → the electron settings store ("neurocode.settings").
|
|
49
|
+
// All other .dat files keep their full filename as the store name so they match
|
|
50
|
+
// what the renderer passes via IPC (e.g. "default.dat", "neurocode.global.dat").
|
|
51
|
+
const storeName = filename === "neurocode.settings.dat" ? "neurocode.settings" : filename
|
|
52
|
+
const target = getStore(storeName)
|
|
53
|
+
const migrated: string[] = []
|
|
54
|
+
const skipped: string[] = []
|
|
55
|
+
|
|
56
|
+
for (const [key, value] of Object.entries(data)) {
|
|
57
|
+
// Don't overwrite values the user has already set in the Electron app.
|
|
58
|
+
if (target.has(key)) {
|
|
59
|
+
skipped.push(key)
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
target.set(key, value)
|
|
63
|
+
migrated.push(key)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
log.log("tauri migration: migrated", filename, "→", storeName, { migrated, skipped })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function migrate() {
|
|
70
|
+
if (getStore().get(TAURI_MIGRATED_KEY)) {
|
|
71
|
+
log.log("tauri migration: already done, skipping")
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const dir = tauriDir(tauriAppId())
|
|
76
|
+
log.log("tauri migration: starting", { dir })
|
|
77
|
+
|
|
78
|
+
if (!existsSync(dir)) {
|
|
79
|
+
log.log("tauri migration: no tauri data directory found, nothing to migrate")
|
|
80
|
+
getStore().set(TAURI_MIGRATED_KEY, true)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const filename of readdirSync(dir)) {
|
|
85
|
+
if (!filename.endsWith(".dat")) continue
|
|
86
|
+
migrateFile(join(dir, filename), filename)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
log.log("tauri migration: complete")
|
|
90
|
+
getStore().set(TAURI_MIGRATED_KEY, true)
|
|
91
|
+
}
|