CodeNomad/packages/electron-app/electron/preload/index.cjs
Pascal André b8e7b16350
feat(desktop): open workspaces in native apps (#638)
## Summary
- add project folder, terminal, and explicit editor actions to the
command palette and native File menus
- replace the Files-row copy button with desktop context actions while
preserving internal file navigation and web/remote copy fallback
- resolve trusted workspace roots through the authenticated local server
and enforce canonical path containment in Electron and Tauri
- use safe platform launch policies, including Windows Open/Edit/Open
With handling and executable rejection on Unix
- localize all new UI actions across supported locales

## Validation
- npm run typecheck
- npm run test:native --workspace @neuralnomads/codenomad-electron-app
(122 passed)
- cargo test (86 passed)
- npm run build
- git diff --check
- independent security, platform, and UX reviews: zero findings

Closes #570
2026-08-13 09:22:30 +01:00

66 lines
2.9 KiB
JavaScript

const { contextBridge, ipcRenderer, webUtils } = require("electron")
function resolveWindowContext() {
const prefix = "--codenomad-window-context="
const arg = process.argv.find((value) => typeof value === "string" && value.startsWith(prefix))
const context = arg ? arg.slice(prefix.length) : "local"
return context === "remote" ? "remote" : "local"
}
function resolveRuntimeHost(windowContext) {
return "electron"
}
const windowContext = resolveWindowContext()
const localElectronAPI = {
onCliStatus: (callback) => {
ipcRenderer.on("cli:status", (_, data) => callback(data))
return () => ipcRenderer.removeAllListeners("cli:status")
},
onCliError: (callback) => {
ipcRenderer.on("cli:error", (_, data) => callback(data))
return () => ipcRenderer.removeAllListeners("cli:error")
},
getCliStatus: () => ipcRenderer.invoke("cli:getStatus"),
restartCli: () => ipcRenderer.invoke("cli:restart"),
openDialog: (options) => ipcRenderer.invoke("dialog:open", options),
getDirectoryPaths: (paths) => ipcRenderer.invoke("filesystem:getDirectoryPaths", paths),
openWorkspaceTarget: (payload) => ipcRenderer.invoke("workspace:openTarget", payload),
setWorkspaceMenuEnabled: (enabled) => ipcRenderer.invoke("workspace:setMenuEnabled", Boolean(enabled)),
onMenuAction: (callback) => {
const handler = (_event, action) => callback(action)
ipcRenderer.on("menu:action", handler)
return () => ipcRenderer.removeListener("menu:action", handler)
},
getPathForFile: (file) => {
try {
return webUtils.getPathForFile(file)
} catch {
return null
}
},
requestMicrophoneAccess: () => ipcRenderer.invoke("media:requestMicrophoneAccess"),
setWakeLock: (enabled) => ipcRenderer.invoke("power:setWakeLock", Boolean(enabled)),
showNotification: (payload) => ipcRenderer.invoke("notifications:show", payload),
openRemoteWindow: (payload) => ipcRenderer.invoke("remote:openWindow", payload),
claimClientStateAccess: (token) => ipcRenderer.invoke("client-state:claimAccess", token),
loadClientState: (token) => ipcRenderer.invoke("client-state:load", token),
saveClientState: (token, snapshot) => ipcRenderer.invoke("client-state:save", token, snapshot),
setClientStateRestoreEnabled: (token, enabled) =>
ipcRenderer.invoke("client-state:setRestoreEnabled", token, Boolean(enabled)),
clearClientState: (token) => ipcRenderer.invoke("client-state:clear", token),
}
const remoteElectronAPI = {
requestMicrophoneAccess: localElectronAPI.requestMicrophoneAccess,
setWakeLock: localElectronAPI.setWakeLock,
showNotification: localElectronAPI.showNotification,
}
contextBridge.exposeInMainWorld(
"electronAPI",
windowContext === "local" ? localElectronAPI : remoteElectronAPI,
)
contextBridge.exposeInMainWorld("__CODENOMAD_WINDOW_CONTEXT__", windowContext)
contextBridge.exposeInMainWorld("__CODENOMAD_RUNTIME_HOST__", resolveRuntimeHost(windowContext))