From 613dec90f0b77c7148978583d6d6c585e578a5e7 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 11:33:54 +0530 Subject: [PATCH 01/23] build: src-node will be directly used from phoenix repo when development not needing rebuild on node changes --- src-build/serveForPlatform.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src-build/serveForPlatform.js b/src-build/serveForPlatform.js index a1e1e21..bc4d92d 100644 --- a/src-build/serveForPlatform.js +++ b/src-build/serveForPlatform.js @@ -1,6 +1,7 @@ import {getPlatformDetails} from "./utils.js"; import {execa} from "execa"; import chalk from "chalk"; +import {resolve} from "path"; const {platform} = getPlatformDetails(); @@ -44,14 +45,18 @@ console.log(`Platform: ${platform}, target: ${target}`); console.log('\nEnsure to start phoenix server at http://localhost:8000 for development.'); console.log('Follow https://github.com/phcode-dev/phoenix#running-phoenix for instructions.\n'); -console.log('Setting up src-node...'); -await execa("npm", ["run", "_make_src-node"], {stdio: "inherit"}); - // Run platform-specific command if (target === "tauri") { + console.log('Setting up src-node...'); + await execa("npm", ["run", "_make_src-node"], {stdio: "inherit"}); + console.log('Starting Tauri dev server...'); await execa("npx", ["tauri", "dev"], {stdio: "inherit"}); } else { + const srcNodePath = resolve("../phoenix/src-node"); + console.log(`Running "npm ci" in ${srcNodePath}`); + await execa("npm", ["ci"], {cwd: srcNodePath, stdio: "inherit"}); + console.log('Starting Electron...'); await execa("./src-electron/node_modules/.bin/electron", ["src-electron/main.js"], {stdio: "inherit"}); } From 41513d7c6506645838488caf8b924ca3d4bad710 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 11:42:20 +0530 Subject: [PATCH 02/23] chore: add getPhNodePath and getSrcNodePath electron api --- src-electron/main-app-ipc.js | 20 ++++++++++++++++++++ src-electron/preload.js | 9 ++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src-electron/main-app-ipc.js b/src-electron/main-app-ipc.js index f85e547..e872492 100644 --- a/src-electron/main-app-ipc.js +++ b/src-electron/main-app-ipc.js @@ -1,6 +1,8 @@ const { app, ipcMain } = require('electron'); const { spawn } = require('child_process'); const readline = require('readline'); +const path = require('path'); +const fs = require('fs'); const { productName } = require('./package.json'); let processInstanceId = 0; @@ -134,6 +136,24 @@ function registerAppIpcHandlers() { ipcMain.handle('get-all-items', () => { return Object.fromEntries(sharedStorageMap); }); + + // Get path to phnode binary + ipcMain.handle('get-phnode-path', () => { + const phNodePath = path.resolve(__dirname, 'bin', 'phnode'); + if (!fs.existsSync(phNodePath)) { + throw new Error(`phnode binary does not exist: ${phNodePath}`); + } + return phNodePath; + }); + + // Get path to src-node (for development) + ipcMain.handle('get-src-node-path', () => { + const srcNodePath = path.resolve(__dirname, '..', '..', 'phoenix', 'src-node'); + if (!fs.existsSync(srcNodePath)) { + throw new Error(`src-node path does not exist: ${srcNodePath}`); + } + return srcNodePath; + }); } module.exports = { diff --git a/src-electron/preload.js b/src-electron/preload.js index 9f9e29e..b64f256 100644 --- a/src-electron/preload.js +++ b/src-electron/preload.js @@ -69,5 +69,12 @@ contextBridge.exposeInMainWorld('electronAPI', { // In-memory storage for multi-window sync (mirrors Tauri's put_item/get_all_items) putItem: (key, value) => ipcRenderer.invoke('put-item', key, value), - getAllItems: () => ipcRenderer.invoke('get-all-items') + getAllItems: () => ipcRenderer.invoke('get-all-items'), + + // Path to phnode binary (src-electron/bin/phnode) + getPhNodePath: () => ipcRenderer.invoke('get-phnode-path'), + + // Path to src-node for development (../phoenix/src-node) + // Throws if path does not exist + getSrcNodePath: () => ipcRenderer.invoke('get-src-node-path') }); From 2f6be15a938a936ba2b9cd36fd69381514418a6a Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 14:49:18 +0530 Subject: [PATCH 03/23] refacotor: put apis properly --- src-electron/main-app-ipc.js | 43 ++++------------------------ src-electron/main-fs-ipc.js | 5 ++++ src-electron/main.js | 55 +++++++++++++++++++++++++++++++++--- src-electron/preload.js | 3 ++ 4 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src-electron/main-app-ipc.js b/src-electron/main-app-ipc.js index e872492..9e2664c 100644 --- a/src-electron/main-app-ipc.js +++ b/src-electron/main-app-ipc.js @@ -1,18 +1,17 @@ +/** + * IPC handlers for electronAppAPI + * Preload location: contextBridge.exposeInMainWorld('electronAppAPI', { ... }) + */ + const { app, ipcMain } = require('electron'); const { spawn } = require('child_process'); const readline = require('readline'); -const path = require('path'); -const fs = require('fs'); const { productName } = require('./package.json'); let processInstanceId = 0; // Map of instanceId -> { process, terminated } const spawnedProcesses = new Map(); -// In-memory key-value store shared across all windows (mirrors Tauri's put_item/get_all_items) -// Used for multi-window storage synchronization -const sharedStorageMap = new Map(); - function waitForTrue(fn, timeout) { return new Promise((resolve) => { const startTime = Date.now(); @@ -122,38 +121,6 @@ function registerAppIpcHandlers() { ipcMain.handle('get-app-name', () => { return productName; }); - - // Set zoom factor on the webview (mirrors Tauri's zoom_window) - ipcMain.handle('zoom-window', (event, scaleFactor) => { - event.sender.setZoomFactor(scaleFactor); - }); - - // In-memory storage for multi-window sync (mirrors Tauri's put_item/get_all_items) - ipcMain.handle('put-item', (event, key, value) => { - sharedStorageMap.set(key, value); - }); - - ipcMain.handle('get-all-items', () => { - return Object.fromEntries(sharedStorageMap); - }); - - // Get path to phnode binary - ipcMain.handle('get-phnode-path', () => { - const phNodePath = path.resolve(__dirname, 'bin', 'phnode'); - if (!fs.existsSync(phNodePath)) { - throw new Error(`phnode binary does not exist: ${phNodePath}`); - } - return phNodePath; - }); - - // Get path to src-node (for development) - ipcMain.handle('get-src-node-path', () => { - const srcNodePath = path.resolve(__dirname, '..', '..', 'phoenix', 'src-node'); - if (!fs.existsSync(srcNodePath)) { - throw new Error(`src-node path does not exist: ${srcNodePath}`); - } - return srcNodePath; - }); } module.exports = { diff --git a/src-electron/main-fs-ipc.js b/src-electron/main-fs-ipc.js index d2eeb6f..ebaf0da 100644 --- a/src-electron/main-fs-ipc.js +++ b/src-electron/main-fs-ipc.js @@ -1,3 +1,8 @@ +/** + * IPC handlers for electronFSAPI + * Preload location: contextBridge.exposeInMainWorld('electronFSAPI', { ... }) + */ + const { ipcMain, dialog, BrowserWindow } = require('electron'); const path = require('path'); const fsp = require('fs/promises'); diff --git a/src-electron/main.js b/src-electron/main.js index 161f573..e621799 100644 --- a/src-electron/main.js +++ b/src-electron/main.js @@ -1,9 +1,14 @@ -const { app, BrowserWindow, protocol } = require('electron'); +const { app, BrowserWindow, protocol, Menu, ipcMain } = require('electron'); const path = require('path'); +const fs = require('fs'); const { registerAppIpcHandlers, terminateAllProcesses } = require('./main-app-ipc'); const { registerFsIpcHandlers, getAppDataDir } = require('./main-fs-ipc'); +// In-memory key-value store shared across all windows (mirrors Tauri's put_item/get_all_items) +// Used for multi-window storage synchronization +const sharedStorageMap = new Map(); + let mainWindow; async function createWindow() { @@ -21,9 +26,6 @@ async function createWindow() { // Load the test page from the http-server mainWindow.loadURL('http://localhost:8000/src/'); - // Open DevTools for debugging - mainWindow.webContents.openDevTools(); - mainWindow.on('closed', () => { mainWindow = null; }); @@ -39,12 +41,57 @@ async function gracefulShutdown(exitCode = 0) { registerAppIpcHandlers(); registerFsIpcHandlers(); +/** + * IPC handlers for electronAPI + * Preload location: contextBridge.exposeInMainWorld('electronAPI', { ... }) + */ + +// Set zoom factor on the webview (mirrors Tauri's zoom_window) +ipcMain.handle('zoom-window', (event, scaleFactor) => { + event.sender.setZoomFactor(scaleFactor); +}); + +// In-memory storage for multi-window sync (mirrors Tauri's put_item/get_all_items) +ipcMain.handle('put-item', (event, key, value) => { + sharedStorageMap.set(key, value); +}); + +ipcMain.handle('get-all-items', () => { + return Object.fromEntries(sharedStorageMap); +}); + +// Toggle DevTools +ipcMain.handle('toggle-dev-tools', (event) => { + event.sender.toggleDevTools(); +}); + +// Get path to phnode binary +ipcMain.handle('get-phnode-path', () => { + const phNodePath = path.resolve(__dirname, 'bin', 'phnode'); + if (!fs.existsSync(phNodePath)) { + throw new Error(`phnode binary does not exist: ${phNodePath}`); + } + return phNodePath; +}); + +// Get path to src-node (for development) +ipcMain.handle('get-src-node-path', () => { + const srcNodePath = path.resolve(__dirname, '..', '..', 'phoenix', 'src-node'); + if (!fs.existsSync(srcNodePath)) { + throw new Error(`src-node path does not exist: ${srcNodePath}`); + } + return srcNodePath; +}); + // Handle quit request from renderer app.on('quit-requested', (exitCode) => { gracefulShutdown(exitCode); }); app.whenReady().then(async () => { + // Remove default menu bar + Menu.setApplicationMenu(null); + // Register asset:// protocol for serving local files from appLocalData/assets/ const appDataDir = getAppDataDir(); const assetsDir = path.join(appDataDir, 'assets'); diff --git a/src-electron/preload.js b/src-electron/preload.js index b64f256..2e63f7f 100644 --- a/src-electron/preload.js +++ b/src-electron/preload.js @@ -71,6 +71,9 @@ contextBridge.exposeInMainWorld('electronAPI', { putItem: (key, value) => ipcRenderer.invoke('put-item', key, value), getAllItems: () => ipcRenderer.invoke('get-all-items'), + // Toggle DevTools + toggleDevTools: () => ipcRenderer.invoke('toggle-dev-tools'), + // Path to phnode binary (src-electron/bin/phnode) getPhNodePath: () => ipcRenderer.invoke('get-phnode-path'), From 5bd8292d6f97d1ade2274b6a38954c2ab5a44d63 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 15:40:09 +0530 Subject: [PATCH 04/23] chore: spawn error handling --- src-electron/main-app-ipc.js | 4 ++++ src-electron/preload.js | 1 + 2 files changed, 5 insertions(+) diff --git a/src-electron/main-app-ipc.js b/src-electron/main-app-ipc.js index 9e2664c..0ba5e2f 100644 --- a/src-electron/main-app-ipc.js +++ b/src-electron/main-app-ipc.js @@ -83,7 +83,11 @@ function registerAppIpcHandlers() { }); childProcess.on('error', (err) => { + instance.terminated = true; console.error(`Failed to start process (instance ${instanceId}):`, err); + if (!sender.isDestroyed()) { + sender.send('process-error', instanceId, { message: err.message, code: err.code }); + } }); return instanceId; diff --git a/src-electron/preload.js b/src-electron/preload.js index 2e63f7f..3bb8e8d 100644 --- a/src-electron/preload.js +++ b/src-electron/preload.js @@ -11,6 +11,7 @@ contextBridge.exposeInMainWorld('electronAppAPI', { onProcessStdout: (callback) => ipcRenderer.on('process-stdout', (_event, instanceId, line) => callback(instanceId, line)), onProcessStderr: (callback) => ipcRenderer.on('process-stderr', (_event, instanceId, line) => callback(instanceId, line)), onProcessClose: (callback) => ipcRenderer.on('process-close', (_event, instanceId, data) => callback(instanceId, data)), + onProcessError: (callback) => ipcRenderer.on('process-error', (_event, instanceId, err) => callback(instanceId, err)), // Quit the app with an exit code (for CI) quitApp: (exitCode) => ipcRenderer.invoke('quit-app', exitCode), From 14374963859b0b085df68df7f4a0b36332d68152 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 16:12:42 +0530 Subject: [PATCH 05/23] feat: trust rin apis in electron edge --- src-electron/main-cred-ipc.js | 112 ++++++++++++++++++++++++++++++++++ src-electron/main.js | 6 ++ src-electron/package.json | 3 + src-electron/preload.js | 9 ++- 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src-electron/main-cred-ipc.js diff --git a/src-electron/main-cred-ipc.js b/src-electron/main-cred-ipc.js new file mode 100644 index 0000000..9f82354 --- /dev/null +++ b/src-electron/main-cred-ipc.js @@ -0,0 +1,112 @@ +const { ipcMain } = require('electron'); +const crypto = require('crypto'); + +// Per-window AES trust map (mirrors Tauri's WindowAesTrust) +// Uses webContents.id which persists across page reloads but changes when window is destroyed +const windowTrustMap = new Map(); // webContentsId -> { key, iv } + +// Keytar for system keychain (libsecret on Linux, Keychain on macOS, Credential Vault on Windows) +let keytar = null; +try { + keytar = require('keytar'); +} catch (e) { + console.warn('keytar not available, credential storage will not work'); +} + +const PHOENIX_CRED_PREFIX = 'phcode_electron_'; + +function registerCredIpcHandlers() { + // Trust window AES key - can only be called once per window + ipcMain.handle('trust-window-aes-key', (event, key, iv) => { + const webContentsId = event.sender.id; + + if (windowTrustMap.has(webContentsId)) { + throw new Error('Trust has already been established for this window.'); + } + + // Validate key (64 hex chars = 32 bytes for AES-256) + if (!/^[0-9a-fA-F]{64}$/.test(key)) { + throw new Error('Invalid AES key. Must be 64 hex characters.'); + } + // Validate IV (24 hex chars = 12 bytes for AES-GCM) + if (!/^[0-9a-fA-F]{24}$/.test(iv)) { + throw new Error('Invalid IV. Must be 24 hex characters.'); + } + + windowTrustMap.set(webContentsId, { key, iv }); + console.log(`AES trust established for webContents: ${webContentsId}`); + }); + + // Remove trust - requires matching key/iv + ipcMain.handle('remove-trust-window-aes-key', (event, key, iv) => { + const webContentsId = event.sender.id; + const stored = windowTrustMap.get(webContentsId); + + if (!stored) { + throw new Error('No trust established for this window.'); + } + if (stored.key !== key || stored.iv !== iv) { + throw new Error('Provided key and IV do not match.'); + } + + windowTrustMap.delete(webContentsId); + console.log(`AES trust removed for webContents: ${webContentsId}`); + }); + + // Store credential in system keychain + ipcMain.handle('store-credential', async (event, scopeName, secretVal) => { + if (!keytar) { + throw new Error('keytar module not available.'); + } + const service = PHOENIX_CRED_PREFIX + scopeName; + await keytar.setPassword(service, process.env.USER || 'user', secretVal); + }); + + // Get credential (encrypted with window's AES key) + ipcMain.handle('get-credential', async (event, scopeName) => { + if (!keytar) { + throw new Error('keytar module not available.'); + } + + const webContentsId = event.sender.id; + const trustData = windowTrustMap.get(webContentsId); + if (!trustData) { + throw new Error('Trust needs to be established first.'); + } + + const service = PHOENIX_CRED_PREFIX + scopeName; + const credential = await keytar.getPassword(service, process.env.USER || 'user'); + if (!credential) { + return null; + } + + // Encrypt with AES-256-GCM (same as Tauri) + const keyBytes = Buffer.from(trustData.key, 'hex'); + const ivBytes = Buffer.from(trustData.iv, 'hex'); + const cipher = crypto.createCipheriv('aes-256-gcm', keyBytes, ivBytes); + let encrypted = cipher.update(credential, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + const authTag = cipher.getAuthTag().toString('hex'); + + return encrypted + authTag; // Return ciphertext + authTag as hex string + }); + + // Delete credential from system keychain + ipcMain.handle('delete-credential', async (event, scopeName) => { + if (!keytar) { + throw new Error('keytar module not available.'); + } + const service = PHOENIX_CRED_PREFIX + scopeName; + await keytar.deletePassword(service, process.env.USER || 'user'); + }); +} + +// Clean up trust when window closes +function cleanupWindowTrust(webContentsId) { + if (windowTrustMap.has(webContentsId)) { + windowTrustMap.delete(webContentsId); + console.log(`AES trust auto-removed for closed webContents: ${webContentsId}`); + } +} + +module.exports = { registerCredIpcHandlers, cleanupWindowTrust }; diff --git a/src-electron/main.js b/src-electron/main.js index e621799..8123cfc 100644 --- a/src-electron/main.js +++ b/src-electron/main.js @@ -4,6 +4,7 @@ const fs = require('fs'); const { registerAppIpcHandlers, terminateAllProcesses } = require('./main-app-ipc'); const { registerFsIpcHandlers, getAppDataDir } = require('./main-fs-ipc'); +const { registerCredIpcHandlers, cleanupWindowTrust } = require('./main-cred-ipc'); // In-memory key-value store shared across all windows (mirrors Tauri's put_item/get_all_items) // Used for multi-window storage synchronization @@ -26,6 +27,10 @@ async function createWindow() { // Load the test page from the http-server mainWindow.loadURL('http://localhost:8000/src/'); + mainWindow.webContents.on('destroyed', () => { + cleanupWindowTrust(mainWindow.webContents.id); + }); + mainWindow.on('closed', () => { mainWindow = null; }); @@ -40,6 +45,7 @@ async function gracefulShutdown(exitCode = 0) { // Register all IPC handlers registerAppIpcHandlers(); registerFsIpcHandlers(); +registerCredIpcHandlers(); /** * IPC handlers for electronAPI diff --git a/src-electron/package.json b/src-electron/package.json index 4ecda03..53583ac 100644 --- a/src-electron/package.json +++ b/src-electron/package.json @@ -5,6 +5,9 @@ "productName": "Phoenix Code Experimental Build", "description": "Phoenix Code Experimental Build", "main": "main.js", + "dependencies": { + "keytar": "^7.9.0" + }, "devDependencies": { "electron": "^40.0.0" } diff --git a/src-electron/preload.js b/src-electron/preload.js index 3bb8e8d..cf08516 100644 --- a/src-electron/preload.js +++ b/src-electron/preload.js @@ -80,5 +80,12 @@ contextBridge.exposeInMainWorld('electronAPI', { // Path to src-node for development (../phoenix/src-node) // Throws if path does not exist - getSrcNodePath: () => ipcRenderer.invoke('get-src-node-path') + getSrcNodePath: () => ipcRenderer.invoke('get-src-node-path'), + + // Trust ring / credential APIs + trustWindowAesKey: (key, iv) => ipcRenderer.invoke('trust-window-aes-key', key, iv), + removeTrustWindowAesKey: (key, iv) => ipcRenderer.invoke('remove-trust-window-aes-key', key, iv), + storeCredential: (scopeName, secretVal) => ipcRenderer.invoke('store-credential', scopeName, secretVal), + getCredential: (scopeName) => ipcRenderer.invoke('get-credential', scopeName), + deleteCredential: (scopeName) => ipcRenderer.invoke('delete-credential', scopeName) }); From b3446e672c7cfc0c2e9a2e61412d6785e7d2bf32 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 16:16:17 +0530 Subject: [PATCH 06/23] chore: pull in missing deps --- package-lock.json | 4 +- src-electron/package-lock.json | 397 ++++++++++++++++++++++++++++++++- 2 files changed, 388 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 485f754..99aa4e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "phoenix-code-ide", - "version": "5.0.5", + "version": "5.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "phoenix-code-ide", - "version": "5.0.5", + "version": "5.1.0", "hasInstallScript": true, "devDependencies": { "@tauri-apps/cli": "1.6.3", diff --git a/src-electron/package-lock.json b/src-electron/package-lock.json index e8f4879..ef18303 100644 --- a/src-electron/package-lock.json +++ b/src-electron/package-lock.json @@ -1,12 +1,15 @@ { - "name": "phoenix-fs-electron-shell", - "version": "1.0.0", + "name": "phoenix-code-electron-shell", + "version": "5.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "phoenix-fs-electron-shell", - "version": "1.0.0", + "name": "phoenix-code-electron-shell", + "version": "5.0.5", + "dependencies": { + "keytar": "^7.9.0" + }, "devDependencies": { "electron": "^40.0.0" } @@ -111,6 +114,37 @@ "@types/node": "*" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -119,6 +153,30 @@ "dev": true, "optional": true }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -155,6 +213,12 @@ "node": ">=8" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -188,7 +252,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -203,7 +266,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -211,6 +273,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -256,6 +327,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -285,7 +365,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -339,6 +418,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -368,6 +456,12 @@ "pend": "~1.2.0" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -397,6 +491,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", @@ -521,6 +621,38 @@ "node": ">=10.19.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -543,6 +675,17 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -583,12 +726,63 @@ "node": ">=4" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "license": "MIT" + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -615,7 +809,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -635,6 +828,32 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -648,7 +867,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -666,6 +884,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -702,6 +949,26 @@ "node": ">=8.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -734,6 +1001,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -741,6 +1053,24 @@ "dev": true, "optional": true }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -753,6 +1083,46 @@ "node": ">= 8.0" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -781,11 +1151,16 @@ "node": ">= 4.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/yauzl": { "version": "2.10.0", From 8b078c2c6e178110da6a6e55e744d6feaae4765b Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 16:40:54 +0530 Subject: [PATCH 07/23] feat: window management/label apis for electron --- src-electron/main-window-ipc.js | 97 +++++++++++++++++++++++++++++++++ src-electron/main.js | 5 ++ src-electron/preload.js | 10 +++- 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src-electron/main-window-ipc.js diff --git a/src-electron/main-window-ipc.js b/src-electron/main-window-ipc.js new file mode 100644 index 0000000..1485a1c --- /dev/null +++ b/src-electron/main-window-ipc.js @@ -0,0 +1,97 @@ +const { ipcMain, BrowserWindow } = require('electron'); +const path = require('path'); + +const PHOENIX_WINDOW_PREFIX = 'phcode-'; +const PHOENIX_EXTENSION_WINDOW_PREFIX = 'extn-'; +const MAX_WINDOWS = 30; + +// Window registry: windowLabel -> BrowserWindow +const windowRegistry = new Map(); +// Reverse lookup: webContentsId -> windowLabel +const webContentsToLabel = new Map(); + +function getNextLabel(prefix) { + for (let i = 1; i <= MAX_WINDOWS; i++) { + const label = `${prefix}${i}`; + if (!windowRegistry.has(label)) { + return label; + } + } + throw new Error(`No free window label available for prefix: ${prefix}`); +} + +function registerWindow(win, label) { + windowRegistry.set(label, win); + webContentsToLabel.set(win.webContents.id, label); + + win.on('closed', () => { + windowRegistry.delete(label); + webContentsToLabel.delete(win.webContents.id); + }); +} + +function registerWindowIpcHandlers() { + // Get all window labels (mirrors Tauri's _get_window_labels) + ipcMain.handle('get-window-labels', () => { + return Array.from(windowRegistry.keys()); + }); + + // Get current window's label + ipcMain.handle('get-current-window-label', (event) => { + return webContentsToLabel.get(event.sender.id) || null; + }); + + // Create new window (mirrors openURLInPhoenixWindow for Electron) + ipcMain.handle('create-phoenix-window', async (event, url, options) => { + const { windowTitle, fullscreen, resizable, height, minHeight, width, minWidth, isExtension } = options || {}; + + const prefix = isExtension ? PHOENIX_EXTENSION_WINDOW_PREFIX : PHOENIX_WINDOW_PREFIX; + const label = getNextLabel(prefix); + + const win = new BrowserWindow({ + width: width || 1366, + height: height || 900, + minWidth: minWidth || 800, + minHeight: minHeight || 600, + fullscreen: fullscreen || false, + resizable: resizable !== false, + title: windowTitle || label, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false + } + }); + + registerWindow(win, label); + await win.loadURL(url); + + return label; + }); + + // Close current window + ipcMain.handle('close-window', async (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + win.close(); + } + }); + + // Quit app (for last window scenario) + ipcMain.handle('quit-app', (event, exitCode) => { + const { app } = require('electron'); + app.exit(exitCode || 0); + }); + + // Focus current window + ipcMain.handle('focus-window', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + win.setAlwaysOnTop(true); + win.focus(); + win.setAlwaysOnTop(false); + } + }); +} + +module.exports = { registerWindowIpcHandlers, registerWindow, windowRegistry }; diff --git a/src-electron/main.js b/src-electron/main.js index 8123cfc..517c825 100644 --- a/src-electron/main.js +++ b/src-electron/main.js @@ -5,6 +5,7 @@ const fs = require('fs'); const { registerAppIpcHandlers, terminateAllProcesses } = require('./main-app-ipc'); const { registerFsIpcHandlers, getAppDataDir } = require('./main-fs-ipc'); const { registerCredIpcHandlers, cleanupWindowTrust } = require('./main-cred-ipc'); +const { registerWindowIpcHandlers, registerWindow } = require('./main-window-ipc'); // In-memory key-value store shared across all windows (mirrors Tauri's put_item/get_all_items) // Used for multi-window storage synchronization @@ -24,6 +25,9 @@ async function createWindow() { icon: path.join(__dirname, '..', 'src-tauri', 'icons', 'icon.png') }); + // Register main window with label 'main' (mirrors Tauri's window labeling) + registerWindow(mainWindow, 'main'); + // Load the test page from the http-server mainWindow.loadURL('http://localhost:8000/src/'); @@ -46,6 +50,7 @@ async function gracefulShutdown(exitCode = 0) { registerAppIpcHandlers(); registerFsIpcHandlers(); registerCredIpcHandlers(); +registerWindowIpcHandlers(); /** * IPC handlers for electronAPI diff --git a/src-electron/preload.js b/src-electron/preload.js index cf08516..d09e6a0 100644 --- a/src-electron/preload.js +++ b/src-electron/preload.js @@ -87,5 +87,13 @@ contextBridge.exposeInMainWorld('electronAPI', { removeTrustWindowAesKey: (key, iv) => ipcRenderer.invoke('remove-trust-window-aes-key', key, iv), storeCredential: (scopeName, secretVal) => ipcRenderer.invoke('store-credential', scopeName, secretVal), getCredential: (scopeName) => ipcRenderer.invoke('get-credential', scopeName), - deleteCredential: (scopeName) => ipcRenderer.invoke('delete-credential', scopeName) + deleteCredential: (scopeName) => ipcRenderer.invoke('delete-credential', scopeName), + + // Window management APIs (mirrors Tauri's window labeling scheme) + getWindowLabels: () => ipcRenderer.invoke('get-window-labels'), + getCurrentWindowLabel: () => ipcRenderer.invoke('get-current-window-label'), + createPhoenixWindow: (url, options) => ipcRenderer.invoke('create-phoenix-window', url, options), + closeWindow: () => ipcRenderer.invoke('close-window'), + quitApp: (exitCode) => ipcRenderer.invoke('quit-app', exitCode), + focusWindow: () => ipcRenderer.invoke('focus-window') }); From cae954ce170b6a26d7ce7f652102220bb502d10f Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 16:46:21 +0530 Subject: [PATCH 08/23] chore: focus window the electron way --- src-electron/main-window-ipc.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src-electron/main-window-ipc.js b/src-electron/main-window-ipc.js index 1485a1c..4a53d01 100644 --- a/src-electron/main-window-ipc.js +++ b/src-electron/main-window-ipc.js @@ -83,13 +83,16 @@ function registerWindowIpcHandlers() { app.exit(exitCode || 0); }); - // Focus current window + // Focus current window and bring to front ipcMain.handle('focus-window', (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win) { - win.setAlwaysOnTop(true); + if (win.isMinimized()) { + win.restore(); + } + win.moveTop(); + win.show(); win.focus(); - win.setAlwaysOnTop(false); } }); } From 9c6bc1d45dd02dedafa13bf321eea992c99cb3a0 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 17:13:14 +0530 Subject: [PATCH 09/23] shell api migration to electron --- src-electron/main-window-ipc.js | 151 +++++++++++++++++++++++++++++++- src-electron/preload.js | 31 ++++++- 2 files changed, 179 insertions(+), 3 deletions(-) diff --git a/src-electron/main-window-ipc.js b/src-electron/main-window-ipc.js index 4a53d01..79fcec4 100644 --- a/src-electron/main-window-ipc.js +++ b/src-electron/main-window-ipc.js @@ -1,5 +1,6 @@ -const { ipcMain, BrowserWindow } = require('electron'); +const { ipcMain, BrowserWindow, shell, clipboard } = require('electron'); const path = require('path'); +const { spawn } = require('child_process'); const PHOENIX_WINDOW_PREFIX = 'phcode-'; const PHOENIX_EXTENSION_WINDOW_PREFIX = 'extn-'; @@ -20,6 +21,9 @@ function getNextLabel(prefix) { throw new Error(`No free window label available for prefix: ${prefix}`); } +// Track close handlers per window +const windowCloseHandlers = new Map(); + function registerWindow(win, label) { windowRegistry.set(label, win); webContentsToLabel.set(win.webContents.id, label); @@ -27,6 +31,17 @@ function registerWindow(win, label) { win.on('closed', () => { windowRegistry.delete(label); webContentsToLabel.delete(win.webContents.id); + windowCloseHandlers.delete(win.webContents.id); + }); +} + +function setupCloseHandler(win) { + win.on('close', (event) => { + const hasHandler = windowCloseHandlers.get(win.webContents.id); + if (hasHandler && !win.forceClose) { + event.preventDefault(); + win.webContents.send('close-requested'); + } }); } @@ -95,6 +110,138 @@ function registerWindowIpcHandlers() { win.focus(); } }); + + // Get process ID + ipcMain.handle('get-process-id', () => { + return process.pid; + }); + + // Get platform architecture + ipcMain.handle('get-platform-arch', () => { + return process.arch; + }); + + // Get current working directory + ipcMain.handle('get-cwd', () => { + return process.cwd(); + }); + + // Fullscreen APIs + ipcMain.handle('is-fullscreen', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + return win ? win.isFullScreen() : false; + }); + + ipcMain.handle('set-fullscreen', (event, enable) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + win.setFullScreen(enable); + } + }); + + // Window title APIs + ipcMain.handle('set-window-title', (event, title) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + win.setTitle(title); + } + }); + + ipcMain.handle('get-window-title', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + return win ? win.getTitle() : ''; + }); + + // Clipboard APIs + ipcMain.handle('clipboard-read-text', () => { + return clipboard.readText(); + }); + + ipcMain.handle('clipboard-write-text', (event, text) => { + clipboard.writeText(text); + }); + + // Read file paths from clipboard (platform-specific) + ipcMain.handle('clipboard-read-files', () => { + const formats = clipboard.availableFormats(); + + // Windows: FileNameW format contains file paths + if (process.platform === 'win32' && formats.includes('FileNameW')) { + const buffer = clipboard.readBuffer('FileNameW'); + // FileNameW is null-terminated UTF-16LE string + const paths = buffer.toString('utf16le').split('\0').filter(p => p.length > 0); + return paths; + } + + // macOS: public.file-url format + if (process.platform === 'darwin') { + // Try reading as file URLs + const text = clipboard.read('public.file-url'); + if (text) { + // Convert file:// URLs to paths + const paths = text.split('\n') + .filter(url => url.startsWith('file://')) + .map(url => decodeURIComponent(url.replace('file://', ''))); + if (paths.length > 0) { + return paths; + } + } + } + + // Linux: text/uri-list format + if (process.platform === 'linux' && formats.includes('text/uri-list')) { + const text = clipboard.read('text/uri-list'); + if (text) { + const paths = text.split('\n') + .filter(url => url.startsWith('file://')) + .map(url => decodeURIComponent(url.replace('file://', ''))); + if (paths.length > 0) { + return paths; + } + } + } + + return null; + }); + + // Shell APIs + ipcMain.handle('move-to-trash', async (event, platformPath) => { + await shell.trashItem(platformPath); + }); + + ipcMain.handle('show-in-folder', (event, platformPath) => { + shell.showItemInFolder(platformPath); + }); + + ipcMain.handle('open-external', async (event, url) => { + await shell.openExternal(url); + }); + + // Windows-only: open URL in specific browser (fire and forget) + ipcMain.handle('open-url-in-browser-win', (event, url, browser) => { + if (process.platform !== 'win32') { + throw new Error('open-url-in-browser-win is only supported on Windows'); + } + spawn('cmd', ['/c', 'start', browser, url], { shell: true, detached: true, stdio: 'ignore' }); + }); + + // Register close handler for current window + ipcMain.handle('register-close-handler', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + windowCloseHandlers.set(win.webContents.id, true); + setupCloseHandler(win); + } + }); + + // Allow close after handler approves + ipcMain.handle('allow-close', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + win.forceClose = true; + win.close(); + } + }); } -module.exports = { registerWindowIpcHandlers, registerWindow, windowRegistry }; +module.exports = { registerWindowIpcHandlers, registerWindow, setupCloseHandler, windowRegistry }; diff --git a/src-electron/preload.js b/src-electron/preload.js index d09e6a0..2cc4aea 100644 --- a/src-electron/preload.js +++ b/src-electron/preload.js @@ -95,5 +95,34 @@ contextBridge.exposeInMainWorld('electronAPI', { createPhoenixWindow: (url, options) => ipcRenderer.invoke('create-phoenix-window', url, options), closeWindow: () => ipcRenderer.invoke('close-window'), quitApp: (exitCode) => ipcRenderer.invoke('quit-app', exitCode), - focusWindow: () => ipcRenderer.invoke('focus-window') + focusWindow: () => ipcRenderer.invoke('focus-window'), + + // Process and platform info + getProcessId: () => ipcRenderer.invoke('get-process-id'), + getPlatformArch: () => ipcRenderer.invoke('get-platform-arch'), + getCwd: () => ipcRenderer.invoke('get-cwd'), + + // Fullscreen APIs + isFullscreen: () => ipcRenderer.invoke('is-fullscreen'), + setFullscreen: (enable) => ipcRenderer.invoke('set-fullscreen', enable), + + // Window title APIs + setWindowTitle: (title) => ipcRenderer.invoke('set-window-title', title), + getWindowTitle: () => ipcRenderer.invoke('get-window-title'), + + // Clipboard APIs + clipboardReadText: () => ipcRenderer.invoke('clipboard-read-text'), + clipboardWriteText: (text) => ipcRenderer.invoke('clipboard-write-text', text), + clipboardReadFiles: () => ipcRenderer.invoke('clipboard-read-files'), + + // Shell APIs + moveToTrash: (platformPath) => ipcRenderer.invoke('move-to-trash', platformPath), + showInFolder: (platformPath) => ipcRenderer.invoke('show-in-folder', platformPath), + openExternal: (url) => ipcRenderer.invoke('open-external', url), + openUrlInBrowserWin: (url, browser) => ipcRenderer.invoke('open-url-in-browser-win', url, browser), + + // Close requested handler + onCloseRequested: (callback) => ipcRenderer.on('close-requested', () => callback()), + registerCloseHandler: () => ipcRenderer.invoke('register-close-handler'), + allowClose: () => ipcRenderer.invoke('allow-close') }); From caeb1fec1dc5f979e6db41f6406cb88f9786f0dd Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 17:23:22 +0530 Subject: [PATCH 10/23] docs: for keeping sync with phoenix-fs repo --- src-electron/main-app-ipc.js | 3 +++ src-electron/main-fs-ipc.js | 3 +++ src-electron/preload.js | 11 ++++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src-electron/main-app-ipc.js b/src-electron/main-app-ipc.js index 0ba5e2f..d4b7151 100644 --- a/src-electron/main-app-ipc.js +++ b/src-electron/main-app-ipc.js @@ -1,6 +1,9 @@ /** * IPC handlers for electronAppAPI * Preload location: contextBridge.exposeInMainWorld('electronAppAPI', { ... }) + * + * NOTE: This file is copied from phoenix-fs library. Do not modify without + * updating the source library. Only add new Phoenix-specific handlers to main-window-ipc.js. */ const { app, ipcMain } = require('electron'); diff --git a/src-electron/main-fs-ipc.js b/src-electron/main-fs-ipc.js index ebaf0da..0c32c1a 100644 --- a/src-electron/main-fs-ipc.js +++ b/src-electron/main-fs-ipc.js @@ -1,6 +1,9 @@ /** * IPC handlers for electronFSAPI * Preload location: contextBridge.exposeInMainWorld('electronFSAPI', { ... }) + * + * NOTE: This file is copied from phoenix-fs library. Do not modify without + * updating the source library. Only add new Phoenix-specific handlers to main-window-ipc.js. */ const { ipcMain, dialog, BrowserWindow } = require('electron'); diff --git a/src-electron/preload.js b/src-electron/preload.js index 2cc4aea..a316385 100644 --- a/src-electron/preload.js +++ b/src-electron/preload.js @@ -1,5 +1,10 @@ const { contextBridge, ipcRenderer } = require('electron'); +/** + * electronAppAPI - Process lifecycle and app info APIs + * NOTE: This API block is copied from phoenix-fs library. Do not modify without + * updating the source library. Only add new Phoenix-specific APIs to electronAPI below. + */ contextBridge.exposeInMainWorld('electronAppAPI', { // App info getAppName: () => ipcRenderer.invoke('get-app-name'), @@ -26,7 +31,11 @@ contextBridge.exposeInMainWorld('electronAppAPI', { getCliArgs: () => ipcRenderer.invoke('get-cli-args') }); -// the electronFSAPI is the fn that you need to copy to your election app impl for the fs to work. +/** + * electronFSAPI - File system APIs + * NOTE: This API block is copied from phoenix-fs library. Do not modify without + * updating the source library. Only add new Phoenix-specific APIs to electronAPI below. + */ contextBridge.exposeInMainWorld('electronFSAPI', { // Path utilities path: { From aad6edb16f12acc3ecfb4d24dec1929aeb1861fe Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 17:44:07 +0530 Subject: [PATCH 11/23] chore: single window handling in electron --- src-electron/main.js | 26 ++++++++++++++++++++++++++ src-electron/preload.js | 5 ++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src-electron/main.js b/src-electron/main.js index 517c825..60ba172 100644 --- a/src-electron/main.js +++ b/src-electron/main.js @@ -7,6 +7,14 @@ const { registerFsIpcHandlers, getAppDataDir } = require('./main-fs-ipc'); const { registerCredIpcHandlers, cleanupWindowTrust } = require('./main-cred-ipc'); const { registerWindowIpcHandlers, registerWindow } = require('./main-window-ipc'); +// Request single instance lock - only one instance of the app should run at a time +const gotTheLock = app.requestSingleInstanceLock(); + +if (!gotTheLock) { + // Another instance is running, quit this one immediately + app.quit(); +} + // In-memory key-value store shared across all windows (mirrors Tauri's put_item/get_all_items) // Used for multi-window storage synchronization const sharedStorageMap = new Map(); @@ -99,6 +107,20 @@ app.on('quit-requested', (exitCode) => { gracefulShutdown(exitCode); }); +// Handle second instance attempts - forward args to existing windows +app.on('second-instance', (event, commandLine, workingDirectory) => { + // Forward to all windows via IPC + // Window focusing is handled by the renderer's singleInstanceHandler + BrowserWindow.getAllWindows().forEach(win => { + if (!win.isDestroyed()) { + win.webContents.send('single-instance', { + args: commandLine, + cwd: workingDirectory + }); + } + }); +}); + app.whenReady().then(async () => { // Remove default menu bar Menu.setApplicationMenu(null); @@ -136,6 +158,10 @@ app.on('window-all-closed', () => { gracefulShutdown(0); }); +// macOS: When dock icon is clicked and no windows are open, create a new window. +// Currently this won't fire because window-all-closed quits the app. If macOS support +// is added and we want apps to stay running with no windows, change window-all-closed +// to not quit on macOS (process.platform !== 'darwin'). app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); diff --git a/src-electron/preload.js b/src-electron/preload.js index a316385..7f9ed57 100644 --- a/src-electron/preload.js +++ b/src-electron/preload.js @@ -133,5 +133,8 @@ contextBridge.exposeInMainWorld('electronAPI', { // Close requested handler onCloseRequested: (callback) => ipcRenderer.on('close-requested', () => callback()), registerCloseHandler: () => ipcRenderer.invoke('register-close-handler'), - allowClose: () => ipcRenderer.invoke('allow-close') + allowClose: () => ipcRenderer.invoke('allow-close'), + + // Single instance event listener (mirrors Tauri's single-instance event) + onSingleInstance: (callback) => ipcRenderer.on('single-instance', (_event, payload) => callback(payload)) }); From 990710c42a2d4cfa21cbe4ebff95301d895f7558 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 17:49:36 +0530 Subject: [PATCH 12/23] chore: release aes trust on window close --- src-electron/main-window-ipc.js | 10 +++++++--- src-electron/main.js | 19 +++++-------------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src-electron/main-window-ipc.js b/src-electron/main-window-ipc.js index 79fcec4..2ab7dfa 100644 --- a/src-electron/main-window-ipc.js +++ b/src-electron/main-window-ipc.js @@ -1,6 +1,7 @@ const { ipcMain, BrowserWindow, shell, clipboard } = require('electron'); const path = require('path'); const { spawn } = require('child_process'); +const { cleanupWindowTrust } = require('./main-cred-ipc'); const PHOENIX_WINDOW_PREFIX = 'phcode-'; const PHOENIX_EXTENSION_WINDOW_PREFIX = 'extn-'; @@ -25,13 +26,16 @@ function getNextLabel(prefix) { const windowCloseHandlers = new Map(); function registerWindow(win, label) { + const webContentsId = win.webContents.id; windowRegistry.set(label, win); - webContentsToLabel.set(win.webContents.id, label); + webContentsToLabel.set(webContentsId, label); win.on('closed', () => { windowRegistry.delete(label); - webContentsToLabel.delete(win.webContents.id); - windowCloseHandlers.delete(win.webContents.id); + webContentsToLabel.delete(webContentsId); + windowCloseHandlers.delete(webContentsId); + // Clean up AES trust for closing window (mirrors Tauri's on_window_event CloseRequested handler) + cleanupWindowTrust(webContentsId); }); } diff --git a/src-electron/main.js b/src-electron/main.js index 60ba172..3abba02 100644 --- a/src-electron/main.js +++ b/src-electron/main.js @@ -4,7 +4,7 @@ const fs = require('fs'); const { registerAppIpcHandlers, terminateAllProcesses } = require('./main-app-ipc'); const { registerFsIpcHandlers, getAppDataDir } = require('./main-fs-ipc'); -const { registerCredIpcHandlers, cleanupWindowTrust } = require('./main-cred-ipc'); +const { registerCredIpcHandlers } = require('./main-cred-ipc'); const { registerWindowIpcHandlers, registerWindow } = require('./main-window-ipc'); // Request single instance lock - only one instance of the app should run at a time @@ -19,10 +19,8 @@ if (!gotTheLock) { // Used for multi-window storage synchronization const sharedStorageMap = new Map(); -let mainWindow; - async function createWindow() { - mainWindow = new BrowserWindow({ + const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { @@ -34,18 +32,11 @@ async function createWindow() { }); // Register main window with label 'main' (mirrors Tauri's window labeling) - registerWindow(mainWindow, 'main'); + // Trust cleanup is handled by registerWindow's closed handler + registerWindow(win, 'main'); // Load the test page from the http-server - mainWindow.loadURL('http://localhost:8000/src/'); - - mainWindow.webContents.on('destroyed', () => { - cleanupWindowTrust(mainWindow.webContents.id); - }); - - mainWindow.on('closed', () => { - mainWindow = null; - }); + win.loadURL('http://localhost:8000/src/'); } async function gracefulShutdown(exitCode = 0) { From 69435fd1cf22b05dcc076b8e199fb63beb008ca6 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 18:16:33 +0530 Subject: [PATCH 13/23] fix: electron app launching but not working --- src-electron/main-window-ipc.js | 6 ------ src-electron/main.js | 3 +++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src-electron/main-window-ipc.js b/src-electron/main-window-ipc.js index 2ab7dfa..daf4bcb 100644 --- a/src-electron/main-window-ipc.js +++ b/src-electron/main-window-ipc.js @@ -96,12 +96,6 @@ function registerWindowIpcHandlers() { } }); - // Quit app (for last window scenario) - ipcMain.handle('quit-app', (event, exitCode) => { - const { app } = require('electron'); - app.exit(exitCode || 0); - }); - // Focus current window and bring to front ipcMain.handle('focus-window', (event) => { const win = BrowserWindow.fromWebContents(event.sender); diff --git a/src-electron/main.js b/src-electron/main.js index 3abba02..4ece3f1 100644 --- a/src-electron/main.js +++ b/src-electron/main.js @@ -35,6 +35,9 @@ async function createWindow() { // Trust cleanup is handled by registerWindow's closed handler registerWindow(win, 'main'); + // uncomment line below if you want to open dev tools at app start + // win.webContents.openDevTools(); + // Load the test page from the http-server win.loadURL('http://localhost:8000/src/'); } From 91ddd9abf8461921de2fb348fdf22eaaf6df9361 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 18:52:02 +0530 Subject: [PATCH 14/23] chore: add file/folder drop support apis --- src-electron/preload.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src-electron/preload.js b/src-electron/preload.js index 7f9ed57..c32409c 100644 --- a/src-electron/preload.js +++ b/src-electron/preload.js @@ -1,4 +1,4 @@ -const { contextBridge, ipcRenderer } = require('electron'); +const { contextBridge, ipcRenderer, webUtils } = require('electron'); /** * electronAppAPI - Process lifecycle and app info APIs @@ -136,5 +136,8 @@ contextBridge.exposeInMainWorld('electronAPI', { allowClose: () => ipcRenderer.invoke('allow-close'), // Single instance event listener (mirrors Tauri's single-instance event) - onSingleInstance: (callback) => ipcRenderer.on('single-instance', (_event, payload) => callback(payload)) + onSingleInstance: (callback) => ipcRenderer.on('single-instance', (_event, payload) => callback(payload)), + + // Drag and drop: get native file path from a dropped File object + getPathForFile: (file) => webUtils.getPathForFile(file) }); From 47d82fc9427878df4b960c61fe226f1b490beedd Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 21:53:13 +0530 Subject: [PATCH 15/23] ci: update cargo deps --- src-tauri/Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0960b6f..d0cd12a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3407,7 +3407,7 @@ dependencies = [ [[package]] name = "phoenix-code-ide" -version = "5.0.5" +version = "5.1.0" dependencies = [ "aes-gcm", "backtrace", From 5bf09717b2a64dce0426596483811a76ed6d986b Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 22:36:04 +0530 Subject: [PATCH 16/23] fix: electron main files always open when starting app in editor due to cli args parsing --- src-electron/main-app-ipc.js | 30 ++++++++++++++++++++++++++++-- src-electron/main.js | 6 ++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src-electron/main-app-ipc.js b/src-electron/main-app-ipc.js index d4b7151..cdc74c0 100644 --- a/src-electron/main-app-ipc.js +++ b/src-electron/main-app-ipc.js @@ -9,9 +9,33 @@ const { app, ipcMain } = require('electron'); const { spawn } = require('child_process'); const readline = require('readline'); +const path = require('path'); const { productName } = require('./package.json'); let processInstanceId = 0; + +// Path to main.js - used to filter it out from CLI args in dev mode +const mainScriptPath = path.resolve(__dirname, 'main.js'); + +/** + * Filter CLI args to remove internal Electron arguments. + * In dev mode, process.argv includes: [electron, main.js, ...userArgs] + * In production, it includes: [app, ...userArgs] + * This function filters out the main.js entry point in dev mode. + */ +function filterCliArgs(args) { + if (!args || args.length === 0) { + return args; + } + + const normalizedMainScript = mainScriptPath.toLowerCase(); + + return args.filter(arg => { + // Resolve to handle both absolute and relative paths + const resolvedArg = path.resolve(arg).toLowerCase(); + return resolvedArg !== normalizedMainScript; + }); +} // Map of instanceId -> { process, terminated } const spawnedProcesses = new Map(); @@ -115,8 +139,9 @@ function registerAppIpcHandlers() { }); // CLI args (mirrors Tauri's cli.getMatches for --quit-when-done / -q) + // Filter out internal Electron args (main.js in dev mode) ipcMain.handle('get-cli-args', () => { - return process.argv; + return filterCliArgs(process.argv); }); // App path (repo root when running from source) @@ -132,5 +157,6 @@ function registerAppIpcHandlers() { module.exports = { registerAppIpcHandlers, - terminateAllProcesses + terminateAllProcesses, + filterCliArgs }; diff --git a/src-electron/main.js b/src-electron/main.js index 4ece3f1..6b7105d 100644 --- a/src-electron/main.js +++ b/src-electron/main.js @@ -2,7 +2,7 @@ const { app, BrowserWindow, protocol, Menu, ipcMain } = require('electron'); const path = require('path'); const fs = require('fs'); -const { registerAppIpcHandlers, terminateAllProcesses } = require('./main-app-ipc'); +const { registerAppIpcHandlers, terminateAllProcesses, filterCliArgs } = require('./main-app-ipc'); const { registerFsIpcHandlers, getAppDataDir } = require('./main-fs-ipc'); const { registerCredIpcHandlers } = require('./main-cred-ipc'); const { registerWindowIpcHandlers, registerWindow } = require('./main-window-ipc'); @@ -105,10 +105,12 @@ app.on('quit-requested', (exitCode) => { app.on('second-instance', (event, commandLine, workingDirectory) => { // Forward to all windows via IPC // Window focusing is handled by the renderer's singleInstanceHandler + // Filter out internal electron args (executable and main.js script) + const filteredArgs = filterCliArgs(commandLine); BrowserWindow.getAllWindows().forEach(win => { if (!win.isDestroyed()) { win.webContents.send('single-instance', { - args: commandLine, + args: filteredArgs, cwd: workingDirectory }); } From 72f1c1dbb95ad5f4190a12381d50c5e8db57e864 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 28 Jan 2026 22:44:44 +0530 Subject: [PATCH 17/23] chore: move to the actual dev location --- src-electron/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-electron/package.json b/src-electron/package.json index 53583ac..7f2ace9 100644 --- a/src-electron/package.json +++ b/src-electron/package.json @@ -1,6 +1,6 @@ { "name": "phoenix-code-electron-shell", - "identifier": "io.phcode.dev-electon-migration", + "identifier": "io.phcode.dev", "version": "5.0.5", "productName": "Phoenix Code Experimental Build", "description": "Phoenix Code Experimental Build", From c8e920750de6cb03f7554853d56aed2a0569a9b4 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 29 Jan 2026 08:34:34 +0530 Subject: [PATCH 18/23] chore: during dev npm install is preferred to pull in node deps in src-node --- src-build/serveForPlatform.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-build/serveForPlatform.js b/src-build/serveForPlatform.js index bc4d92d..dc8db30 100644 --- a/src-build/serveForPlatform.js +++ b/src-build/serveForPlatform.js @@ -54,8 +54,8 @@ if (target === "tauri") { await execa("npx", ["tauri", "dev"], {stdio: "inherit"}); } else { const srcNodePath = resolve("../phoenix/src-node"); - console.log(`Running "npm ci" in ${srcNodePath}`); - await execa("npm", ["ci"], {cwd: srcNodePath, stdio: "inherit"}); + console.log(`Running "npm install" in ${srcNodePath}`); + await execa("npm", ["install"], {cwd: srcNodePath, stdio: "inherit"}); console.log('Starting Electron...'); await execa("./src-electron/node_modules/.bin/electron", ["src-electron/main.js"], {stdio: "inherit"}); From f242f3995bb5f77cc560e4e186a5e08cdc1d5d39 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 29 Jan 2026 09:12:20 +0530 Subject: [PATCH 19/23] chore: harden electron security --- src-electron/config.js | 51 +++++++++++++++++++ src-electron/ipc-security.js | 90 +++++++++++++++++++++++++++++++++ src-electron/main-app-ipc.js | 16 ++++-- src-electron/main-cred-ipc.js | 6 +++ src-electron/main-fs-ipc.js | 54 +++++++++++++++----- src-electron/main-window-ipc.js | 67 +++++++++++++++++++----- src-electron/main.js | 13 +++-- src-electron/package.json | 2 + 8 files changed, 268 insertions(+), 31 deletions(-) create mode 100644 src-electron/config.js create mode 100644 src-electron/ipc-security.js diff --git a/src-electron/config.js b/src-electron/config.js new file mode 100644 index 0000000..4d3047f --- /dev/null +++ b/src-electron/config.js @@ -0,0 +1,51 @@ +/** + * Centralized Configuration Module + * + * This module provides a single source of truth for all configuration values. + * It reads from package.json and can apply stage-wise transforms as needed. + * + * Usage: + * const { stage, trustedElectronDomains, productName } = require('./config'); + */ + +const packageJson = require('./package.json'); + +// Core package.json values +const name = packageJson.name; +const identifier = packageJson.identifier; +const stage = packageJson.stage; +const version = packageJson.version; +const productName = packageJson.productName; +const description = packageJson.description; + +// Security configuration +const trustedElectronDomains = packageJson.trustedElectronDomains || []; + +/** + * Initialize configuration (call once at app startup if needed). + * Currently a no-op but can be extended for async config loading, + * environment variable overrides, or stage-wise transforms. + */ +function initConfig() { + // Future: Add stage-wise transforms, env overrides, etc. + // Example: + // if (stage === 'prod') { + // // Apply production-specific config + // } +} + +module.exports = { + // Package info + name, + identifier, + stage, + version, + productName, + description, + + // Security + trustedElectronDomains, + + // Initialization + initConfig +}; diff --git a/src-electron/ipc-security.js b/src-electron/ipc-security.js new file mode 100644 index 0000000..d92365e --- /dev/null +++ b/src-electron/ipc-security.js @@ -0,0 +1,90 @@ +/** + * IPC Security - Trusted Domain Validation + * + * This module implements security measures to ensure Electron APIs are only + * accessible from trusted origins. Trust is evaluated at window load/navigation + * time (not on every IPC call) for optimal performance. + * + * Trust rules: + * - Dev stage: trustedElectronDomains + all localhost URLs + * - Other stages (staging/prod): only trustedElectronDomains + */ + +const { stage, trustedElectronDomains } = require('./config'); + +// Track trusted webContents IDs (Set for O(1) lookup) +const _trustedWebContents = new Set(); + +/** + * Check if a URL is trusted based on stage configuration. + * - Dev stage: trustedElectronDomains + all localhost URLs + * - Other stages: only trustedElectronDomains + */ +function _isTrustedOrigin(url) { + if (!url) return false; + + // Check against trustedElectronDomains + for (const domain of trustedElectronDomains) { + if (url.startsWith(domain)) { + return true; + } + } + + // In dev stage, also allow localhost URLs + if (stage === 'dev') { + try { + const parsed = new URL(url); + if (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') { + return true; + } + } catch { + return false; + } + } + + return false; +} + +/** + * Mark a webContents as trusted/untrusted based on its current URL. + * Call this when window loads or navigates. + */ +function updateTrustStatus(webContents) { + const url = webContents.getURL(); + if (_isTrustedOrigin(url)) { + _trustedWebContents.add(webContents.id); + } else { + _trustedWebContents.delete(webContents.id); + } +} + +/** + * Remove trust tracking when webContents is destroyed. + */ +function cleanupTrust(webContentsId) { + _trustedWebContents.delete(webContentsId); +} + +/** + * Fast check if webContents is trusted (O(1) lookup). + */ +function _isWebContentsTrusted(webContentsId) { + return _trustedWebContents.has(webContentsId); +} + +/** + * Assert that IPC event comes from trusted webContents. + * Throws error if not trusted. + */ +function assertTrusted(event) { + if (!_isWebContentsTrusted(event.sender.id)) { + const url = event.senderFrame?.url || event.sender.getURL() || 'unknown'; + throw new Error(`Blocked IPC from untrusted origin: ${url}`); + } +} + +module.exports = { + updateTrustStatus, + cleanupTrust, + assertTrusted +}; diff --git a/src-electron/main-app-ipc.js b/src-electron/main-app-ipc.js index cdc74c0..e890e2a 100644 --- a/src-electron/main-app-ipc.js +++ b/src-electron/main-app-ipc.js @@ -10,7 +10,8 @@ const { app, ipcMain } = require('electron'); const { spawn } = require('child_process'); const readline = require('readline'); const path = require('path'); -const { productName } = require('./package.json'); +const { productName } = require('./config'); +const { assertTrusted } = require('./ipc-security'); let processInstanceId = 0; @@ -73,6 +74,7 @@ function registerAppIpcHandlers() { // Spawn a child process and forward stdio to the calling renderer. // Returns an instanceId so the renderer can target the correct process. ipcMain.handle('spawn-process', async (event, command, args) => { + assertTrusted(event); const instanceId = ++processInstanceId; const sender = event.sender; console.log(`Spawning: ${command} ${args.join(' ')} (instance ${instanceId})`); @@ -122,6 +124,7 @@ function registerAppIpcHandlers() { // Write data to a specific spawned process stdin ipcMain.handle('write-to-process', (event, instanceId, data) => { + assertTrusted(event); const instance = spawnedProcesses.get(instanceId); if (instance && !instance.terminated) { instance.process.stdin.write(data); @@ -129,28 +132,33 @@ function registerAppIpcHandlers() { }); ipcMain.handle('quit-app', (event, exitCode) => { + assertTrusted(event); console.log('Quit requested with exit code:', exitCode); // This will be handled by the main module's gracefulShutdown app.emit('quit-requested', exitCode); }); ipcMain.on('console-log', (event, message) => { + assertTrusted(event); console.log('Renderer:', message); }); // CLI args (mirrors Tauri's cli.getMatches for --quit-when-done / -q) // Filter out internal Electron args (main.js in dev mode) - ipcMain.handle('get-cli-args', () => { + ipcMain.handle('get-cli-args', (event) => { + assertTrusted(event); return filterCliArgs(process.argv); }); // App path (repo root when running from source) - ipcMain.handle('get-app-path', () => { + ipcMain.handle('get-app-path', (event) => { + assertTrusted(event); return app.getAppPath(); }); // App name from package.json - ipcMain.handle('get-app-name', () => { + ipcMain.handle('get-app-name', (event) => { + assertTrusted(event); return productName; }); } diff --git a/src-electron/main-cred-ipc.js b/src-electron/main-cred-ipc.js index 9f82354..6b2a1c8 100644 --- a/src-electron/main-cred-ipc.js +++ b/src-electron/main-cred-ipc.js @@ -1,5 +1,6 @@ const { ipcMain } = require('electron'); const crypto = require('crypto'); +const { assertTrusted } = require('./ipc-security'); // Per-window AES trust map (mirrors Tauri's WindowAesTrust) // Uses webContents.id which persists across page reloads but changes when window is destroyed @@ -18,6 +19,7 @@ const PHOENIX_CRED_PREFIX = 'phcode_electron_'; function registerCredIpcHandlers() { // Trust window AES key - can only be called once per window ipcMain.handle('trust-window-aes-key', (event, key, iv) => { + assertTrusted(event); const webContentsId = event.sender.id; if (windowTrustMap.has(webContentsId)) { @@ -39,6 +41,7 @@ function registerCredIpcHandlers() { // Remove trust - requires matching key/iv ipcMain.handle('remove-trust-window-aes-key', (event, key, iv) => { + assertTrusted(event); const webContentsId = event.sender.id; const stored = windowTrustMap.get(webContentsId); @@ -55,6 +58,7 @@ function registerCredIpcHandlers() { // Store credential in system keychain ipcMain.handle('store-credential', async (event, scopeName, secretVal) => { + assertTrusted(event); if (!keytar) { throw new Error('keytar module not available.'); } @@ -64,6 +68,7 @@ function registerCredIpcHandlers() { // Get credential (encrypted with window's AES key) ipcMain.handle('get-credential', async (event, scopeName) => { + assertTrusted(event); if (!keytar) { throw new Error('keytar module not available.'); } @@ -93,6 +98,7 @@ function registerCredIpcHandlers() { // Delete credential from system keychain ipcMain.handle('delete-credential', async (event, scopeName) => { + assertTrusted(event); if (!keytar) { throw new Error('keytar module not available.'); } diff --git a/src-electron/main-fs-ipc.js b/src-electron/main-fs-ipc.js index 0c32c1a..39e0861 100644 --- a/src-electron/main-fs-ipc.js +++ b/src-electron/main-fs-ipc.js @@ -10,7 +10,8 @@ const { ipcMain, dialog, BrowserWindow } = require('electron'); const path = require('path'); const fsp = require('fs/promises'); const os = require('os'); -const { identifier: APP_IDENTIFIER } = require('./package.json'); +const { identifier: APP_IDENTIFIER } = require('./config'); +const { assertTrusted } = require('./ipc-security'); // Electron IPC only preserves Error.message when errors cross the IPC boundary (see // https://github.com/electron/electron/issues/24427). To preserve error.code for FS @@ -47,25 +48,32 @@ function getAppDataDir() { function registerFsIpcHandlers() { // Directory APIs - ipcMain.handle('get-documents-dir', () => { + ipcMain.handle('get-documents-dir', (event) => { + assertTrusted(event); // Match Tauri's documentDir which ends with a trailing slash return path.join(os.homedir(), 'Documents') + path.sep; }); - ipcMain.handle('get-home-dir', () => { + ipcMain.handle('get-home-dir', (event) => { + assertTrusted(event); // Match Tauri's homeDir which ends with a trailing slash const home = os.homedir(); return home.endsWith(path.sep) ? home : home + path.sep; }); - ipcMain.handle('get-temp-dir', () => { + ipcMain.handle('get-temp-dir', (event) => { + assertTrusted(event); return os.tmpdir(); }); - ipcMain.handle('get-app-data-dir', () => getAppDataDir()); + ipcMain.handle('get-app-data-dir', (event) => { + assertTrusted(event); + return getAppDataDir(); + }); // Get Windows drive letters (returns null on non-Windows platforms) - ipcMain.handle('get-windows-drives', async () => { + ipcMain.handle('get-windows-drives', async (event) => { + assertTrusted(event); if (process.platform !== 'win32') { return null; } @@ -86,12 +94,14 @@ function registerFsIpcHandlers() { // Dialogs ipcMain.handle('show-open-dialog', async (event, options) => { + assertTrusted(event); const win = BrowserWindow.fromWebContents(event.sender); const result = await dialog.showOpenDialog(win, options); return result.filePaths; }); ipcMain.handle('show-save-dialog', async (event, options) => { + assertTrusted(event); const win = BrowserWindow.fromWebContents(event.sender); const result = await dialog.showSaveDialog(win, options); return result.filePath; @@ -99,6 +109,7 @@ function registerFsIpcHandlers() { // FS operations ipcMain.handle('fs-readdir', async (event, dirPath) => { + assertTrusted(event); return fsResult( fsp.readdir(dirPath, { withFileTypes: true }) .then(entries => entries.map(e => ({ name: e.name, isDirectory: e.isDirectory() }))) @@ -106,6 +117,7 @@ function registerFsIpcHandlers() { }); ipcMain.handle('fs-stat', async (event, filePath) => { + assertTrusted(event); return fsResult( fsp.stat(filePath).then(stats => ({ isFile: stats.isFile(), @@ -122,12 +134,30 @@ function registerFsIpcHandlers() { ); }); - ipcMain.handle('fs-mkdir', (event, dirPath, options) => fsResult(fsp.mkdir(dirPath, options))); - ipcMain.handle('fs-unlink', (event, filePath) => fsResult(fsp.unlink(filePath))); - ipcMain.handle('fs-rmdir', (event, dirPath, options) => fsResult(fsp.rm(dirPath, options))); - ipcMain.handle('fs-rename', (event, oldPath, newPath) => fsResult(fsp.rename(oldPath, newPath))); - ipcMain.handle('fs-read-file', (event, filePath) => fsResult(fsp.readFile(filePath))); - ipcMain.handle('fs-write-file', (event, filePath, data) => fsResult(fsp.writeFile(filePath, Buffer.from(data)))); + ipcMain.handle('fs-mkdir', (event, dirPath, options) => { + assertTrusted(event); + return fsResult(fsp.mkdir(dirPath, options)); + }); + ipcMain.handle('fs-unlink', (event, filePath) => { + assertTrusted(event); + return fsResult(fsp.unlink(filePath)); + }); + ipcMain.handle('fs-rmdir', (event, dirPath, options) => { + assertTrusted(event); + return fsResult(fsp.rm(dirPath, options)); + }); + ipcMain.handle('fs-rename', (event, oldPath, newPath) => { + assertTrusted(event); + return fsResult(fsp.rename(oldPath, newPath)); + }); + ipcMain.handle('fs-read-file', (event, filePath) => { + assertTrusted(event); + return fsResult(fsp.readFile(filePath)); + }); + ipcMain.handle('fs-write-file', (event, filePath, data) => { + assertTrusted(event); + return fsResult(fsp.writeFile(filePath, Buffer.from(data))); + }); } module.exports = { diff --git a/src-electron/main-window-ipc.js b/src-electron/main-window-ipc.js index daf4bcb..ab58422 100644 --- a/src-electron/main-window-ipc.js +++ b/src-electron/main-window-ipc.js @@ -2,6 +2,7 @@ const { ipcMain, BrowserWindow, shell, clipboard } = require('electron'); const path = require('path'); const { spawn } = require('child_process'); const { cleanupWindowTrust } = require('./main-cred-ipc'); +const { updateTrustStatus, cleanupTrust, assertTrusted } = require('./ipc-security'); const PHOENIX_WINDOW_PREFIX = 'phcode-'; const PHOENIX_EXTENSION_WINDOW_PREFIX = 'extn-'; @@ -26,16 +27,30 @@ function getNextLabel(prefix) { const windowCloseHandlers = new Map(); function registerWindow(win, label) { - const webContentsId = win.webContents.id; + const webContents = win.webContents; + const webContentsId = webContents.id; windowRegistry.set(label, win); webContentsToLabel.set(webContentsId, label); + // Initial trust evaluation + updateTrustStatus(webContents); + + // Re-evaluate trust on navigation + webContents.on('did-navigate', () => { + updateTrustStatus(webContents); + }); + webContents.on('did-navigate-in-page', () => { + updateTrustStatus(webContents); + }); + win.on('closed', () => { windowRegistry.delete(label); webContentsToLabel.delete(webContentsId); windowCloseHandlers.delete(webContentsId); // Clean up AES trust for closing window (mirrors Tauri's on_window_event CloseRequested handler) cleanupWindowTrust(webContentsId); + // Clean up security trust + cleanupTrust(webContentsId); }); } @@ -51,22 +66,36 @@ function setupCloseHandler(win) { function registerWindowIpcHandlers() { // Get all window labels (mirrors Tauri's _get_window_labels) - ipcMain.handle('get-window-labels', () => { + ipcMain.handle('get-window-labels', (event) => { + assertTrusted(event); return Array.from(windowRegistry.keys()); }); // Get current window's label ipcMain.handle('get-current-window-label', (event) => { + assertTrusted(event); return webContentsToLabel.get(event.sender.id) || null; }); // Create new window (mirrors openURLInPhoenixWindow for Electron) ipcMain.handle('create-phoenix-window', async (event, url, options) => { + assertTrusted(event); const { windowTitle, fullscreen, resizable, height, minHeight, width, minWidth, isExtension } = options || {}; const prefix = isExtension ? PHOENIX_EXTENSION_WINDOW_PREFIX : PHOENIX_WINDOW_PREFIX; const label = getNextLabel(prefix); + const webPreferences = { + contextIsolation: true, + nodeIntegration: false, + sandbox: true + }; + + // Only inject preload for Phoenix windows, not extensions + if (!isExtension) { + webPreferences.preload = path.join(__dirname, 'preload.js'); + } + const win = new BrowserWindow({ width: width || 1366, height: height || 900, @@ -75,11 +104,7 @@ function registerWindowIpcHandlers() { fullscreen: fullscreen || false, resizable: resizable !== false, title: windowTitle || label, - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - contextIsolation: true, - nodeIntegration: false - } + webPreferences }); registerWindow(win, label); @@ -90,6 +115,7 @@ function registerWindowIpcHandlers() { // Close current window ipcMain.handle('close-window', async (event) => { + assertTrusted(event); const win = BrowserWindow.fromWebContents(event.sender); if (win) { win.close(); @@ -98,6 +124,7 @@ function registerWindowIpcHandlers() { // Focus current window and bring to front ipcMain.handle('focus-window', (event) => { + assertTrusted(event); const win = BrowserWindow.fromWebContents(event.sender); if (win) { if (win.isMinimized()) { @@ -110,27 +137,32 @@ function registerWindowIpcHandlers() { }); // Get process ID - ipcMain.handle('get-process-id', () => { + ipcMain.handle('get-process-id', (event) => { + assertTrusted(event); return process.pid; }); // Get platform architecture - ipcMain.handle('get-platform-arch', () => { + ipcMain.handle('get-platform-arch', (event) => { + assertTrusted(event); return process.arch; }); // Get current working directory - ipcMain.handle('get-cwd', () => { + ipcMain.handle('get-cwd', (event) => { + assertTrusted(event); return process.cwd(); }); // Fullscreen APIs ipcMain.handle('is-fullscreen', (event) => { + assertTrusted(event); const win = BrowserWindow.fromWebContents(event.sender); return win ? win.isFullScreen() : false; }); ipcMain.handle('set-fullscreen', (event, enable) => { + assertTrusted(event); const win = BrowserWindow.fromWebContents(event.sender); if (win) { win.setFullScreen(enable); @@ -139,6 +171,7 @@ function registerWindowIpcHandlers() { // Window title APIs ipcMain.handle('set-window-title', (event, title) => { + assertTrusted(event); const win = BrowserWindow.fromWebContents(event.sender); if (win) { win.setTitle(title); @@ -146,21 +179,25 @@ function registerWindowIpcHandlers() { }); ipcMain.handle('get-window-title', (event) => { + assertTrusted(event); const win = BrowserWindow.fromWebContents(event.sender); return win ? win.getTitle() : ''; }); // Clipboard APIs - ipcMain.handle('clipboard-read-text', () => { + ipcMain.handle('clipboard-read-text', (event) => { + assertTrusted(event); return clipboard.readText(); }); ipcMain.handle('clipboard-write-text', (event, text) => { + assertTrusted(event); clipboard.writeText(text); }); // Read file paths from clipboard (platform-specific) - ipcMain.handle('clipboard-read-files', () => { + ipcMain.handle('clipboard-read-files', (event) => { + assertTrusted(event); const formats = clipboard.availableFormats(); // Windows: FileNameW format contains file paths @@ -204,19 +241,23 @@ function registerWindowIpcHandlers() { // Shell APIs ipcMain.handle('move-to-trash', async (event, platformPath) => { + assertTrusted(event); await shell.trashItem(platformPath); }); ipcMain.handle('show-in-folder', (event, platformPath) => { + assertTrusted(event); shell.showItemInFolder(platformPath); }); ipcMain.handle('open-external', async (event, url) => { + assertTrusted(event); await shell.openExternal(url); }); // Windows-only: open URL in specific browser (fire and forget) ipcMain.handle('open-url-in-browser-win', (event, url, browser) => { + assertTrusted(event); if (process.platform !== 'win32') { throw new Error('open-url-in-browser-win is only supported on Windows'); } @@ -225,6 +266,7 @@ function registerWindowIpcHandlers() { // Register close handler for current window ipcMain.handle('register-close-handler', (event) => { + assertTrusted(event); const win = BrowserWindow.fromWebContents(event.sender); if (win) { windowCloseHandlers.set(win.webContents.id, true); @@ -234,6 +276,7 @@ function registerWindowIpcHandlers() { // Allow close after handler approves ipcMain.handle('allow-close', (event) => { + assertTrusted(event); const win = BrowserWindow.fromWebContents(event.sender); if (win) { win.forceClose = true; diff --git a/src-electron/main.js b/src-electron/main.js index 6b7105d..1027211 100644 --- a/src-electron/main.js +++ b/src-electron/main.js @@ -6,6 +6,7 @@ const { registerAppIpcHandlers, terminateAllProcesses, filterCliArgs } = require const { registerFsIpcHandlers, getAppDataDir } = require('./main-fs-ipc'); const { registerCredIpcHandlers } = require('./main-cred-ipc'); const { registerWindowIpcHandlers, registerWindow } = require('./main-window-ipc'); +const { assertTrusted } = require('./ipc-security'); // Request single instance lock - only one instance of the app should run at a time const gotTheLock = app.requestSingleInstanceLock(); @@ -61,25 +62,30 @@ registerWindowIpcHandlers(); // Set zoom factor on the webview (mirrors Tauri's zoom_window) ipcMain.handle('zoom-window', (event, scaleFactor) => { + assertTrusted(event); event.sender.setZoomFactor(scaleFactor); }); // In-memory storage for multi-window sync (mirrors Tauri's put_item/get_all_items) ipcMain.handle('put-item', (event, key, value) => { + assertTrusted(event); sharedStorageMap.set(key, value); }); -ipcMain.handle('get-all-items', () => { +ipcMain.handle('get-all-items', (event) => { + assertTrusted(event); return Object.fromEntries(sharedStorageMap); }); // Toggle DevTools ipcMain.handle('toggle-dev-tools', (event) => { + assertTrusted(event); event.sender.toggleDevTools(); }); // Get path to phnode binary -ipcMain.handle('get-phnode-path', () => { +ipcMain.handle('get-phnode-path', (event) => { + assertTrusted(event); const phNodePath = path.resolve(__dirname, 'bin', 'phnode'); if (!fs.existsSync(phNodePath)) { throw new Error(`phnode binary does not exist: ${phNodePath}`); @@ -88,7 +94,8 @@ ipcMain.handle('get-phnode-path', () => { }); // Get path to src-node (for development) -ipcMain.handle('get-src-node-path', () => { +ipcMain.handle('get-src-node-path', (event) => { + assertTrusted(event); const srcNodePath = path.resolve(__dirname, '..', '..', 'phoenix', 'src-node'); if (!fs.existsSync(srcNodePath)) { throw new Error(`src-node path does not exist: ${srcNodePath}`); diff --git a/src-electron/package.json b/src-electron/package.json index 7f2ace9..2f0f364 100644 --- a/src-electron/package.json +++ b/src-electron/package.json @@ -1,6 +1,8 @@ { "name": "phoenix-code-electron-shell", "identifier": "io.phcode.dev", + "stage": "dev", + "trustedElectronDomains": ["phtauri://localhost/", "https://phcode.dev/"], "version": "5.0.5", "productName": "Phoenix Code Experimental Build", "description": "Phoenix Code Experimental Build", From 67870a74af1001ff5af816c628c02bbdd3867f7a Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 29 Jan 2026 09:30:30 +0530 Subject: [PATCH 20/23] fix: newly spawned untrusted windows shouldnt have electon api injection at all --- src-electron/ipc-security.js | 5 +++-- src-electron/main-window-ipc.js | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src-electron/ipc-security.js b/src-electron/ipc-security.js index d92365e..6d75afd 100644 --- a/src-electron/ipc-security.js +++ b/src-electron/ipc-security.js @@ -20,7 +20,7 @@ const _trustedWebContents = new Set(); * - Dev stage: trustedElectronDomains + all localhost URLs * - Other stages: only trustedElectronDomains */ -function _isTrustedOrigin(url) { +function isTrustedOrigin(url) { if (!url) return false; // Check against trustedElectronDomains @@ -51,7 +51,7 @@ function _isTrustedOrigin(url) { */ function updateTrustStatus(webContents) { const url = webContents.getURL(); - if (_isTrustedOrigin(url)) { + if (isTrustedOrigin(url)) { _trustedWebContents.add(webContents.id); } else { _trustedWebContents.delete(webContents.id); @@ -84,6 +84,7 @@ function assertTrusted(event) { } module.exports = { + isTrustedOrigin, updateTrustStatus, cleanupTrust, assertTrusted diff --git a/src-electron/main-window-ipc.js b/src-electron/main-window-ipc.js index ab58422..87cc529 100644 --- a/src-electron/main-window-ipc.js +++ b/src-electron/main-window-ipc.js @@ -2,7 +2,7 @@ const { ipcMain, BrowserWindow, shell, clipboard } = require('electron'); const path = require('path'); const { spawn } = require('child_process'); const { cleanupWindowTrust } = require('./main-cred-ipc'); -const { updateTrustStatus, cleanupTrust, assertTrusted } = require('./ipc-security'); +const { isTrustedOrigin, updateTrustStatus, cleanupTrust, assertTrusted } = require('./ipc-security'); const PHOENIX_WINDOW_PREFIX = 'phcode-'; const PHOENIX_EXTENSION_WINDOW_PREFIX = 'extn-'; @@ -91,8 +91,8 @@ function registerWindowIpcHandlers() { sandbox: true }; - // Only inject preload for Phoenix windows, not extensions - if (!isExtension) { + // Only inject preload for Phoenix windows with trusted URLs, not extensions + if (!isExtension && isTrustedOrigin(url)) { webPreferences.preload = path.join(__dirname, 'preload.js'); } From 5e156c111f8cc8eed433801f6c59e6e061a2f46f Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 29 Jan 2026 11:24:56 +0530 Subject: [PATCH 21/23] chore: better logging --- src-electron/main-cred-ipc.js | 11 +++++++---- src-electron/main-window-ipc.js | 8 ++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src-electron/main-cred-ipc.js b/src-electron/main-cred-ipc.js index 6b2a1c8..cbe613c 100644 --- a/src-electron/main-cred-ipc.js +++ b/src-electron/main-cred-ipc.js @@ -36,7 +36,9 @@ function registerCredIpcHandlers() { } windowTrustMap.set(webContentsId, { key, iv }); - console.log(`AES trust established for webContents: ${webContentsId}`); + // Lazy require to avoid circular dependency + const { getWindowLabel } = require('./main-window-ipc'); + console.log(`AES trust established for window: ${getWindowLabel(webContentsId)} (webContentsId: ${webContentsId})`); }); // Remove trust - requires matching key/iv @@ -53,7 +55,8 @@ function registerCredIpcHandlers() { } windowTrustMap.delete(webContentsId); - console.log(`AES trust removed for webContents: ${webContentsId}`); + const { getWindowLabel } = require('./main-window-ipc'); + console.log(`AES trust removed for window: ${getWindowLabel(webContentsId)} (webContentsId: ${webContentsId})`); }); // Store credential in system keychain @@ -108,10 +111,10 @@ function registerCredIpcHandlers() { } // Clean up trust when window closes -function cleanupWindowTrust(webContentsId) { +function cleanupWindowTrust(webContentsId, windowLabel) { if (windowTrustMap.has(webContentsId)) { windowTrustMap.delete(webContentsId); - console.log(`AES trust auto-removed for closed webContents: ${webContentsId}`); + console.log(`AES trust auto-removed for closed window: ${windowLabel} (webContentsId: ${webContentsId})`); } } diff --git a/src-electron/main-window-ipc.js b/src-electron/main-window-ipc.js index 87cc529..821302f 100644 --- a/src-electron/main-window-ipc.js +++ b/src-electron/main-window-ipc.js @@ -48,7 +48,7 @@ function registerWindow(win, label) { webContentsToLabel.delete(webContentsId); windowCloseHandlers.delete(webContentsId); // Clean up AES trust for closing window (mirrors Tauri's on_window_event CloseRequested handler) - cleanupWindowTrust(webContentsId); + cleanupWindowTrust(webContentsId, label); // Clean up security trust cleanupTrust(webContentsId); }); @@ -285,4 +285,8 @@ function registerWindowIpcHandlers() { }); } -module.exports = { registerWindowIpcHandlers, registerWindow, setupCloseHandler, windowRegistry }; +function getWindowLabel(webContentsId) { + return webContentsToLabel.get(webContentsId) || 'unknown'; +} + +module.exports = { registerWindowIpcHandlers, registerWindow, setupCloseHandler, windowRegistry, getWindowLabel }; From f404a3826577c264b811fda6b465e3069bed9f6d Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 29 Jan 2026 12:16:35 +0530 Subject: [PATCH 22/23] feat: restore window sizes --- src-electron/main-window-ipc.js | 27 ++++- src-electron/main.js | 19 +++- src-electron/window-state.js | 189 ++++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 src-electron/window-state.js diff --git a/src-electron/main-window-ipc.js b/src-electron/main-window-ipc.js index 821302f..c1491b7 100644 --- a/src-electron/main-window-ipc.js +++ b/src-electron/main-window-ipc.js @@ -3,6 +3,7 @@ const path = require('path'); const { spawn } = require('child_process'); const { cleanupWindowTrust } = require('./main-cred-ipc'); const { isTrustedOrigin, updateTrustStatus, cleanupTrust, assertTrusted } = require('./ipc-security'); +const { DEFAULTS } = require('./window-state'); const PHOENIX_WINDOW_PREFIX = 'phcode-'; const PHOENIX_EXTENSION_WINDOW_PREFIX = 'extn-'; @@ -96,11 +97,29 @@ function registerWindowIpcHandlers() { webPreferences.preload = path.join(__dirname, 'preload.js'); } + let windowConfig; + if (isExtension) { + // Extensions manage their own sizing + windowConfig = { + width: width || 800, + height: height || 600, + minWidth: minWidth, + minHeight: minHeight + }; + } else { + // Phoenix windows: use defaults and ensure dimensions are at least the minimums + const actualMinWidth = Math.max(minWidth || DEFAULTS.minWidth, DEFAULTS.minWidth); + const actualMinHeight = Math.max(minHeight || DEFAULTS.minHeight, DEFAULTS.minHeight); + windowConfig = { + width: Math.max(width || DEFAULTS.width, actualMinWidth), + height: Math.max(height || DEFAULTS.height, actualMinHeight), + minWidth: actualMinWidth, + minHeight: actualMinHeight + }; + } + const win = new BrowserWindow({ - width: width || 1366, - height: height || 900, - minWidth: minWidth || 800, - minHeight: minHeight || 600, + ...windowConfig, fullscreen: fullscreen || false, resizable: resizable !== false, title: windowTitle || label, diff --git a/src-electron/main.js b/src-electron/main.js index 1027211..873a4a5 100644 --- a/src-electron/main.js +++ b/src-electron/main.js @@ -7,6 +7,7 @@ const { registerFsIpcHandlers, getAppDataDir } = require('./main-fs-ipc'); const { registerCredIpcHandlers } = require('./main-cred-ipc'); const { registerWindowIpcHandlers, registerWindow } = require('./main-window-ipc'); const { assertTrusted } = require('./ipc-security'); +const { getWindowOptions, trackWindowState, DEFAULTS } = require('./window-state'); // Request single instance lock - only one instance of the app should run at a time const gotTheLock = app.requestSingleInstanceLock(); @@ -21,9 +22,15 @@ if (!gotTheLock) { const sharedStorageMap = new Map(); async function createWindow() { + // Get window options with restored state or defaults + const windowOptions = getWindowOptions(); + const wasMaximized = windowOptions._wasMaximized; + delete windowOptions._wasMaximized; + const win = new BrowserWindow({ - width: 1200, - height: 800, + ...windowOptions, + minWidth: DEFAULTS.minWidth, + minHeight: DEFAULTS.minHeight, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, @@ -32,6 +39,14 @@ async function createWindow() { icon: path.join(__dirname, '..', 'src-tauri', 'icons', 'icon.png') }); + // Track window state for persistence + trackWindowState(win); + + // Restore maximized state after window is ready + if (wasMaximized) { + win.maximize(); + } + // Register main window with label 'main' (mirrors Tauri's window labeling) // Trust cleanup is handled by registerWindow's closed handler registerWindow(win, 'main'); diff --git a/src-electron/window-state.js b/src-electron/window-state.js new file mode 100644 index 0000000..b0ee560 --- /dev/null +++ b/src-electron/window-state.js @@ -0,0 +1,189 @@ +/** + * Window State Manager + * + * Persists and restores window position, size, and maximized state. + * Handles multi-monitor setups and gracefully handles disconnected monitors. + */ + +const { screen } = require('electron'); +const path = require('path'); +const fs = require('fs'); +const { getAppDataDir } = require('./main-fs-ipc'); + +const STATE_FILE = 'window-state.json'; + +// Default window dimensions +const DEFAULTS = { + width: 1366, + height: 900, + minWidth: 800, + minHeight: 600 +}; + +/** + * Get the path to the window state file. + */ +function getStateFilePath() { + return path.join(getAppDataDir(), STATE_FILE); +} + +/** + * Load saved window state from disk. + * Returns null if no saved state or file is corrupted. + */ +function loadWindowState() { + try { + const filePath = getStateFilePath(); + if (!fs.existsSync(filePath)) { + return null; + } + const data = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(data); + } catch (err) { + console.warn('Failed to load window state:', err.message); + return null; + } +} + +/** + * Save window state to disk. + */ +function saveWindowState(state) { + try { + const filePath = getStateFilePath(); + // Ensure directory exists + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + } catch (err) { + console.warn('Failed to save window state:', err.message); + } +} + +/** + * Check if a rectangle is visible on any display. + * Returns true if at least a portion of the window would be visible. + */ +function isVisibleOnAnyDisplay(bounds) { + const displays = screen.getAllDisplays(); + const minVisibleArea = 100; // At least 100px visible + + for (const display of displays) { + const { x, y, width, height } = display.workArea; + + // Calculate overlap between window bounds and display work area + const overlapX = Math.max(0, Math.min(bounds.x + bounds.width, x + width) - Math.max(bounds.x, x)); + const overlapY = Math.max(0, Math.min(bounds.y + bounds.height, y + height) - Math.max(bounds.y, y)); + const overlapArea = overlapX * overlapY; + + if (overlapArea >= minVisibleArea) { + return true; + } + } + return false; +} + +/** + * Get the display nearest to a point. + */ +function getNearestDisplay(x, y) { + return screen.getDisplayNearestPoint({ x, y }); +} + +/** + * Get window options with restored state or defaults. + * Validates saved position is on a visible display. + */ +function getWindowOptions() { + const savedState = loadWindowState(); + + const options = { + width: DEFAULTS.width, + height: DEFAULTS.height, + minWidth: DEFAULTS.minWidth, + minHeight: DEFAULTS.minHeight + }; + + if (savedState) { + // Restore size (clamped to minimums) + options.width = Math.max(savedState.width || DEFAULTS.width, DEFAULTS.minWidth); + options.height = Math.max(savedState.height || DEFAULTS.height, DEFAULTS.minHeight); + + // Check if saved position is visible on any current display + if (savedState.x !== undefined && savedState.y !== undefined) { + const bounds = { + x: savedState.x, + y: savedState.y, + width: options.width, + height: options.height + }; + + if (isVisibleOnAnyDisplay(bounds)) { + // Position is valid, use it + options.x = savedState.x; + options.y = savedState.y; + } else { + // Position is off-screen (monitor disconnected?), center on nearest display + const nearestDisplay = getNearestDisplay(savedState.x, savedState.y); + const { x, y, width, height } = nearestDisplay.workArea; + options.x = x + Math.round((width - options.width) / 2); + options.y = y + Math.round((height - options.height) / 2); + console.log('Window position was off-screen, repositioned to nearest display'); + } + } + + // Track if we need to maximize after window is created + options._wasMaximized = savedState.isMaximized || false; + } + + return options; +} + +/** + * Track window state changes and save on close. + * Call this after creating the BrowserWindow. + */ +function trackWindowState(win) { + let windowState = { + width: DEFAULTS.width, + height: DEFAULTS.height, + x: undefined, + y: undefined, + isMaximized: false + }; + + // Update state from current window bounds + function updateState() { + if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) { + const bounds = win.getBounds(); + windowState.width = bounds.width; + windowState.height = bounds.height; + windowState.x = bounds.x; + windowState.y = bounds.y; + } + windowState.isMaximized = win.isMaximized(); + } + + // Listen for state changes + win.on('resize', updateState); + win.on('move', updateState); + win.on('maximize', updateState); + win.on('unmaximize', updateState); + + // Save state before window closes + win.on('close', () => { + updateState(); + saveWindowState(windowState); + }); + + // Initialize state + updateState(); +} + +module.exports = { + DEFAULTS, + getWindowOptions, + trackWindowState +}; From 4ff612f29129977772378904f457b5418b0e7c11 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 29 Jan 2026 12:27:25 +0530 Subject: [PATCH 23/23] chore: window size and position persistance --- src-electron/main-window-ipc.js | 7 +++++- src-electron/window-state.js | 42 ++++++++------------------------- 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/src-electron/main-window-ipc.js b/src-electron/main-window-ipc.js index c1491b7..4acd3fc 100644 --- a/src-electron/main-window-ipc.js +++ b/src-electron/main-window-ipc.js @@ -3,7 +3,7 @@ const path = require('path'); const { spawn } = require('child_process'); const { cleanupWindowTrust } = require('./main-cred-ipc'); const { isTrustedOrigin, updateTrustStatus, cleanupTrust, assertTrusted } = require('./ipc-security'); -const { DEFAULTS } = require('./window-state'); +const { DEFAULTS, trackWindowState } = require('./window-state'); const PHOENIX_WINDOW_PREFIX = 'phcode-'; const PHOENIX_EXTENSION_WINDOW_PREFIX = 'extn-'; @@ -126,6 +126,11 @@ function registerWindowIpcHandlers() { webPreferences }); + // Track window state for Phoenix windows (not extensions) + if (!isExtension) { + trackWindowState(win); + } + registerWindow(win, label); await win.loadURL(url); diff --git a/src-electron/window-state.js b/src-electron/window-state.js index b0ee560..5fdc8ec 100644 --- a/src-electron/window-state.js +++ b/src-electron/window-state.js @@ -142,44 +142,22 @@ function getWindowOptions() { } /** - * Track window state changes and save on close. + * Track window state and save on close. * Call this after creating the BrowserWindow. */ function trackWindowState(win) { - let windowState = { - width: DEFAULTS.width, - height: DEFAULTS.height, - x: undefined, - y: undefined, - isMaximized: false - }; - - // Update state from current window bounds - function updateState() { - if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) { - const bounds = win.getBounds(); - windowState.width = bounds.width; - windowState.height = bounds.height; - windowState.x = bounds.x; - windowState.y = bounds.y; - } - windowState.isMaximized = win.isMaximized(); - } - - // Listen for state changes - win.on('resize', updateState); - win.on('move', updateState); - win.on('maximize', updateState); - win.on('unmaximize', updateState); - - // Save state before window closes win.on('close', () => { - updateState(); + // getNormalBounds() returns the non-maximized bounds even when maximized + const bounds = win.getNormalBounds(); + const windowState = { + width: bounds.width, + height: bounds.height, + x: bounds.x, + y: bounds.y, + isMaximized: win.isMaximized() + }; saveWindowState(windowState); }); - - // Initialize state - updateState(); } module.exports = {