+const sessionListeners = new Map();
+
+// Parking host for terminal DOM when view unmounts
+let parkingHost = null;
+
+// Export function to get current session ID (for backward compatibility)
+export const getSessionId = () => {
+ // Return the first active session ID if any
+ if (terminalInstances.size > 0) {
+ return Array.from(terminalInstances.keys())[0];
+ }
+ return null;
+};
+
+const ensureParkingHost = () => {
+ if (parkingHost && document.body.contains(parkingHost)) return parkingHost;
+ parkingHost = document.createElement('div');
+ parkingHost.style.display = 'none';
+ parkingHost.setAttribute('data-terminal-parking-host', 'true');
+ document.body.appendChild(parkingHost);
+ return parkingHost;
+};
+
+const createTerminalForSession = (sessionId) => {
+ if (terminalInstances.has(sessionId)) {
+ return terminalInstances.get(sessionId);
+ }
+
+ const terminal = new Terminal({
+ cursorBlink: true,
+ fontSize: 14,
+ fontFamily: 'Menlo, Monaco, "Courier New", monospace',
+ theme: {
+ background: '#1e1e1e',
+ foreground: '#d4d4d4',
+ cursor: '#d4d4d4',
+ selection: '#264f78',
+ black: '#1e1e1e',
+ red: '#f14c4c',
+ green: '#23d18b',
+ yellow: '#f5f543',
+ blue: '#3b8eea',
+ magenta: '#d670d6',
+ cyan: '#29b8db',
+ white: '#e5e5e5',
+ brightBlack: '#666666',
+ brightRed: '#f14c4c',
+ brightGreen: '#23d18b',
+ brightYellow: '#f5f543',
+ brightBlue: '#3b8eea',
+ brightMagenta: '#d670d6',
+ brightCyan: '#29b8db',
+ brightWhite: '#e5e5e5'
+ },
+ allowProposedApi: true
+ });
+
+ const fitAddon = new FitAddon();
+ terminal.loadAddon(fitAddon);
+
+ const inputDisposable = terminal.onData((data) => {
+ if (data && sessionId && window.ipcRenderer) {
+ window.ipcRenderer.send('terminal:input', sessionId, data);
+ }
+ });
+
+ const resizeDisposable = terminal.onResize(({ cols, rows }) => {
+ if (sessionId && window.ipcRenderer) {
+ window.ipcRenderer.send('terminal:resize', sessionId, { cols, rows });
+ }
+ });
+
+ const instance = {
+ terminal,
+ fitAddon,
+ inputDisposable,
+ resizeDisposable
+ };
+
+ terminalInstances.set(sessionId, instance);
+
+ // Setup IPC listeners for this session
+ if (window.ipcRenderer && !sessionListeners.has(sessionId)) {
+ const onData = (data) => {
+ if (!data) return;
+ const instance = terminalInstances.get(sessionId);
+ if (instance && instance.terminal) {
+ try {
+ instance.terminal.write(data);
+ } catch (err) {
+ console.warn('Failed to write terminal data:', err);
+ }
+ }
+ };
+
+ const onExit = ({ exitCode, signal } = {}) => {
+ const msg = `\r\n[Process exited with code ${exitCode ?? ''} ${signal ? `(signal ${signal})` : ''}]\r\n`;
+ const instance = terminalInstances.get(sessionId);
+ if (instance && instance.terminal) {
+ try {
+ instance.terminal.write(msg);
+ } catch (err) {
+ console.warn('Failed to write terminal exit message:', err);
+ }
+ }
+ // Cleanup on exit
+ cleanupTerminalInstance(sessionId);
+ };
+
+ window.ipcRenderer.on(`terminal:data:${sessionId}`, onData);
+ window.ipcRenderer.on(`terminal:exit:${sessionId}`, onExit);
+
+ sessionListeners.set(sessionId, { onData, onExit });
+ }
+
+ return instance;
+};
+
+const cleanupTerminalInstance = (sessionId) => {
+ const instance = terminalInstances.get(sessionId);
+ if (instance) {
+ try {
+ if (instance.inputDisposable) instance.inputDisposable.dispose();
+ if (instance.resizeDisposable) instance.resizeDisposable.dispose();
+ if (instance.terminal) {
+ instance.terminal.dispose();
+ }
+ } catch (err) {
+ console.warn('Error disposing terminal instance:', err);
+ }
+ terminalInstances.delete(sessionId);
+ }
+
+ // Remove IPC listeners
+ const listeners = sessionListeners.get(sessionId);
+ if (listeners && window.ipcRenderer) {
+ try {
+ window.ipcRenderer.removeAllListeners(`terminal:data:${sessionId}`);
+ window.ipcRenderer.removeAllListeners(`terminal:exit:${sessionId}`);
+ } catch (err) {
+ console.warn('Error removing IPC listeners:', err);
+ }
+ sessionListeners.delete(sessionId);
+ }
+};
+
+const openTerminalIntoContainer = async (container, sessionId) => {
+ if (!container || !sessionId) return;
+
+ const instance = createTerminalForSession(sessionId);
+ const { terminal, fitAddon } = instance;
+
+ if (!terminal.element) {
+ terminal.open(container);
+ } else {
+ // Move terminal element to new container
+ if (terminal.element.parentElement !== container) {
+ container.appendChild(terminal.element);
+ }
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ try {
+ fitAddon.fit();
+ const { cols, rows } = terminal;
+ if (cols && rows && window.ipcRenderer) {
+ window.ipcRenderer.send('terminal:resize', sessionId, { cols, rows });
+ }
+ } catch (e) {
+ console.warn('Error fitting terminal:', e);
+ }
+};
+
+const TerminalTab = () => {
+ const terminalRef = useRef(null);
+ const [sessions, setSessions] = useState([]);
+ const [activeSessionId, setActiveSessionId] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ // Load sessions list
+ const loadSessions = useCallback(async (currentActiveSessionId = null) => {
+ if (!window.ipcRenderer) return [];
+
+ try {
+ const sessionList = await window.ipcRenderer.invoke('terminal:list-sessions');
+ setSessions(sessionList);
+
+ // Use functional state updates to get the current activeSessionId
+ setActiveSessionId((prevActiveSessionId) => {
+ const activeId = currentActiveSessionId !== null ? currentActiveSessionId : prevActiveSessionId;
+
+ // Auto-select first session if none selected
+ if (!activeId && sessionList.length > 0) {
+ return sessionList[0].sessionId;
+ }
+
+ // If active session no longer exists, select first available
+ if (activeId && !sessionList.find((s) => s.sessionId === activeId)) {
+ return sessionList.length > 0 ? sessionList[0].sessionId : null;
+ }
+
+ // Keep current selection if it still exists
+ return activeId;
+ });
+
+ return sessionList;
+ } catch (err) {
+ console.error('Failed to load sessions:', err);
+ return [];
+ }
+ }, []);
+
+ // Create new terminal session
+ const createNewSession = useCallback(async (cwd = null) => {
+ if (!window.ipcRenderer) return null;
+
+ try {
+ const options = cwd ? { cwd } : {};
+ const newSessionId = await window.ipcRenderer.invoke('terminal:create', options);
+ if (newSessionId) {
+ await loadSessions(newSessionId);
+ setActiveSessionId(newSessionId);
+ return newSessionId;
+ }
+ } catch (err) {
+ console.error('Failed to create terminal session:', err);
+ }
+ return null;
+ }, [loadSessions]);
+
+ // Listen for requests to open terminal at specific CWD
+ useEffect(() => {
+ const normalizePath = (path) => {
+ if (!path) return '';
+ // Normalize path separators and remove trailing separators for comparison
+ return path.replace(/\\/g, '/').replace(/\/$/, '') || '/';
+ };
+
+ const handleOpenTerminalAtCwd = async (event) => {
+ const { cwd } = event.detail;
+ if (!cwd) return;
+
+ const normalizedCwd = normalizePath(cwd);
+
+ // Check if session already exists at this CWD
+ const sessionList = await window.ipcRenderer.invoke('terminal:list-sessions');
+ const existingSession = sessionList.find((s) => normalizePath(s.cwd) === normalizedCwd);
+
+ if (existingSession) {
+ // Switch to existing session
+ await loadSessions(existingSession.sessionId);
+ setActiveSessionId(existingSession.sessionId);
+ } else {
+ // Create new session at this CWD
+ await createNewSession(cwd);
+ }
+ };
+
+ window.addEventListener('terminal:open-at-cwd', handleOpenTerminalAtCwd);
+
+ return () => {
+ window.removeEventListener('terminal:open-at-cwd', handleOpenTerminalAtCwd);
+ };
+ }, [loadSessions, createNewSession]);
+
+ // Close terminal session
+ const closeSession = async (sessionId) => {
+ if (!window.ipcRenderer) return;
+
+ try {
+ window.ipcRenderer.send('terminal:kill', sessionId);
+ cleanupTerminalInstance(sessionId);
+
+ // Load updated sessions (this will also handle active session switching)
+ const updatedSessions = await loadSessions();
+
+ // If we closed the active session and there are no sessions left, clear selection
+ if (activeSessionId === sessionId && updatedSessions.length === 0) {
+ setActiveSessionId(null);
+ }
+ } catch (err) {
+ console.error('Failed to close terminal session:', err);
+ }
+ };
+
+ // Load sessions on mount and set up polling
+ useEffect(() => {
+ if (!window.ipcRenderer) {
+ setIsLoading(false);
+ return;
+ }
+
+ let mounted = true;
+
+ const initialLoad = async () => {
+ const sessionList = await loadSessions();
+ if (mounted) {
+ setIsLoading(false);
+ }
+ };
+
+ initialLoad();
+
+ // Poll for session updates every 2 seconds
+ // Note: We don't pass currentActiveSessionId here to avoid stale closures
+ // The functional update inside loadSessions will use the current state
+ const pollInterval = setInterval(() => {
+ if (mounted) {
+ loadSessions();
+ }
+ }, 2000);
+
+ return () => {
+ mounted = false;
+ clearInterval(pollInterval);
+ };
+ }, []);
+
+ // Handle terminal display for active session
+ useEffect(() => {
+ if (!activeSessionId || !terminalRef.current) return;
+
+ let mounted = true;
+
+ const setupTerminal = async () => {
+ await openTerminalIntoContainer(terminalRef.current, activeSessionId);
+
+ if (mounted) {
+ const instance = terminalInstances.get(activeSessionId);
+ if (instance && instance.fitAddon) {
+ const onResize = () => {
+ try {
+ instance.fitAddon.fit();
+ } catch (e) {}
+ };
+
+ window.addEventListener('resize', onResize);
+
+ // Initial resize
+ setTimeout(() => {
+ try {
+ instance.fitAddon.fit();
+ const { cols, rows } = instance.terminal;
+ if (cols && rows && window.ipcRenderer) {
+ window.ipcRenderer.send('terminal:resize', activeSessionId, { cols, rows });
+ }
+ } catch (err) {
+ console.warn('Failed to perform initial resize:', err);
+ }
+ }, 100);
+
+ return () => {
+ window.removeEventListener('resize', onResize);
+
+ // Park terminal element when switching sessions
+ if (instance.terminal && instance.terminal.element) {
+ const host = ensureParkingHost();
+ if (instance.terminal.element.parentElement !== host) {
+ host.appendChild(instance.terminal.element);
+ }
+ }
+ };
+ }
+ }
+ };
+
+ const cleanup = setupTerminal();
+
+ return () => {
+ mounted = false;
+ Promise.resolve(cleanup).then((fn) => {
+ if (typeof fn === 'function') fn();
+ });
+ };
+ }, [activeSessionId]);
+
+ return (
+
+
+ {/* Left Sidebar */}
+
+
+ Sessions
+ {
+ e.stopPropagation();
+ createNewSession();
+ }}
+ title="New Terminal Session"
+ />
+
+
+ {isLoading ? (
+
+ Loading sessions...
+
+ ) : sessions.length === 0 ? (
+
+ No active sessions
+
+ ) : (
+
+ )}
+
+
+
+ {/* Right Terminal Display */}
+
+ {!activeSessionId && window.ipcRenderer && (
+
+
+ No terminal session selected
+
+ )}
+
+
+
+
+ );
+};
+
+export default TerminalTab;
diff --git a/packages/bruno-app/src/components/Devtools/Console/index.js b/packages/bruno-app/src/components/Devtools/Console/index.js
index 669fbf925..558acc79c 100644
--- a/packages/bruno-app/src/components/Devtools/Console/index.js
+++ b/packages/bruno-app/src/components/Devtools/Console/index.js
@@ -27,6 +27,7 @@ import {
} from 'providers/ReduxStore/slices/logs';
import NetworkTab from './NetworkTab';
+import TerminalTab from './TerminalTab';
import RequestDetailsPanel from './RequestDetailsPanel';
// import DebugTab from './DebugTab';
import ErrorDetailsPanel from './ErrorDetailsPanel';
@@ -389,6 +390,8 @@ const Console = () => {
return ;
case 'performance':
return ;
+ case 'terminal':
+ return ;
// case 'debug':
// return ;
default:
@@ -442,6 +445,8 @@ const Console = () => {
);
+ case 'terminal':
+ return null; // No controls needed for terminal
// case 'debug':
// return (
//
@@ -497,6 +502,14 @@ const Console = () => {
Performance
+
+
{/*
)}
+ {isFolder && (
+ {
+ dropdownTippyRef.current.hide();
+ // Get folder pathname
+ const folderCwd = item.pathname || collectionPathname;
+ await openDevtoolsAndSwitchToTerminal(dispatch, folderCwd);
+ }}
+ >
+
+
+
+ Open in Terminal
+
+ )}
{
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
index 3c8e07987..c8a534444 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
@@ -17,7 +17,8 @@ import {
IconShare,
IconFoldDown,
IconX,
- IconSettings
+ IconSettings,
+ IconTerminal2
} from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { toggleCollection, collapseFullCollection } from 'providers/ReduxStore/slices/collections';
@@ -42,6 +43,7 @@ import { scrollToTheActiveTab } from 'utils/tabs';
import ShareCollection from 'components/ShareCollection/index';
import { CollectionItemDragPreview } from './CollectionItem/CollectionItemDragPreview/index';
import { sortByNameThenSequence } from 'utils/common/index';
+import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';
const Collection = ({ collection, searchText }) => {
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
@@ -422,6 +424,19 @@ const Collection = ({ collection, searchText }) => {
Settings
+ {
+ menuDropdownTippyRef.current.hide();
+ const collectionCwd = collection.pathname;
+ await openDevtoolsAndSwitchToTerminal(dispatch, collectionCwd);
+ }}
+ >
+
+
+
+ Open in Terminal
+
{
diff --git a/packages/bruno-app/src/utils/terminal.js b/packages/bruno-app/src/utils/terminal.js
new file mode 100644
index 000000000..0933f56f3
--- /dev/null
+++ b/packages/bruno-app/src/utils/terminal.js
@@ -0,0 +1,32 @@
+import { openConsole, setActiveTab } from 'providers/ReduxStore/slices/logs';
+import { getSessionId } from 'components/Devtools/Console/TerminalTab';
+
+/**
+ * Opens the devtools console and switches to the terminal tab
+ * Optionally opens/switches to a terminal session at a specific CWD
+ * @param {Function} dispatch - Redux dispatch function
+ * @param {string} [cwd] - Optional CWD path. If provided, checks for existing session at that CWD or creates new one
+ */
+export const openDevtoolsAndSwitchToTerminal = async (dispatch, cwd = null) => {
+ // Open console if closed
+ dispatch(openConsole());
+
+ // Switch to terminal tab
+ dispatch(setActiveTab('terminal'));
+
+ // If CWD is provided, dispatch event to TerminalTab to handle session selection/creation
+ if (cwd) {
+ // Small delay to ensure terminal tab is mounted
+ setTimeout(() => {
+ window.dispatchEvent(new CustomEvent('terminal:open-at-cwd', { detail: { cwd } }));
+ }, 100);
+ }
+};
+
+/**
+ * Gets the current terminal session ID if a terminal session is running
+ * @returns {string|null} The session ID if terminal session exists, null otherwise
+ */
+export const getSessionID = () => {
+ return getSessionId();
+};
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index a38233fe8..4e042394d 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -32,6 +32,7 @@
"@aws-sdk/credential-providers": "3.750.0",
"@grpc/grpc-js": "^1.13.2",
"@grpc/proto-loader": "^0.7.13",
+ "@lydell/node-pty": "^1.1.0",
"@usebruno/common": "0.1.0",
"@usebruno/converters": "^0.1.0",
"@usebruno/filestore": "^0.1.0",
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index 47453fbea..5b891ec0e 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -42,6 +42,7 @@ const collectionWatcher = require('./app/collection-watcher');
const { loadWindowState, saveBounds, saveMaximized } = require('./utils/window');
const registerNotificationsIpc = require('./ipc/notifications');
const registerGlobalEnvironmentsIpc = require('./ipc/global-environments');
+const TerminalManager = require('./ipc/terminal');
const { safeParseJSON, safeStringifyJSON } = require('./utils/common');
const { getDomainsWithCookies } = require('./utils/cookies');
const { cookiesStore } = require('./store/cookies');
@@ -51,6 +52,7 @@ const { getIsRunningInRosetta } = require('./utils/arch');
const lastOpenedCollections = new LastOpenedCollections();
const systemMonitor = new SystemMonitor();
+const terminalManager = new TerminalManager();
// Reference: https://content-security-policy.com/
const contentSecurityPolicy = [
@@ -161,6 +163,7 @@ app.on('ready', async () => {
mainWindow.on('unmaximize', () => saveMaximized(false));
mainWindow.on('close', (e) => {
e.preventDefault();
+ terminalManager.cleanup(mainWindow.webContents);
ipcMain.emit('main:start-quit-flow');
});
@@ -230,6 +233,12 @@ app.on('before-quit', () => {
// Stop system monitoring
systemMonitor.stop();
+
+ try {
+ terminalManager.killAll();
+ } catch (err) {
+ console.error('Failed to kill all terminals on quit', err);
+ }
});
app.on('window-all-closed', app.quit);
diff --git a/packages/bruno-electron/src/ipc/terminal.js b/packages/bruno-electron/src/ipc/terminal.js
new file mode 100644
index 000000000..b6d8cc1ca
--- /dev/null
+++ b/packages/bruno-electron/src/ipc/terminal.js
@@ -0,0 +1,182 @@
+const { ipcMain } = require('electron');
+const pty = require('@lydell/node-pty');
+const os = require('os');
+const path = require('path');
+const isDev = require('electron-is-dev');
+
+class TerminalManager {
+ constructor() {
+ this.terminals = new Map();
+ this.setupIpcHandlers();
+ }
+
+ setupIpcHandlers() {
+ // Create a new terminal session
+ ipcMain.handle('terminal:create', (event, options = {}) => {
+ try {
+ const sessionId = this.generateSessionId();
+ const shell = this.getDefaultShell();
+ // Use provided cwd or default to home directory
+ const cwd = options.cwd || this.getDefaultCwd();
+
+ if (isDev) {
+ console.log(`Creating new terminal session: ${sessionId} at ${cwd}`);
+ }
+
+ const ptyProcess = pty.spawn(shell, [], {
+ name: 'xterm-color',
+ cols: 80,
+ rows: 24,
+ cwd: cwd,
+ env: process.env
+ });
+
+ // Store terminal session
+ this.terminals.set(sessionId, {
+ pty: ptyProcess,
+ webContents: event.sender,
+ cwd: cwd // Store initial cwd
+ });
+
+ // Handle terminal output
+ ptyProcess.onData((data) => {
+ try {
+ if (data && event.sender && !event.sender.isDestroyed()) {
+ event.sender.send(`terminal:data:${sessionId}`, data);
+ }
+ } catch (error) {
+ console.warn('Failed to send terminal data:', error);
+ }
+ });
+
+ // Handle terminal exit
+ ptyProcess.onExit(({ exitCode, signal }) => {
+ try {
+ this.terminals.delete(sessionId);
+ if (event.sender && !event.sender.isDestroyed()) {
+ event.sender.send(`terminal:exit:${sessionId}`, { exitCode, signal });
+ }
+ } catch (error) {
+ console.warn('Failed to handle terminal exit:', error);
+ }
+ });
+
+ return sessionId;
+ } catch (error) {
+ console.error('Failed to create terminal session:', error);
+ return null;
+ }
+ });
+
+ // Send input to terminal
+ ipcMain.on('terminal:input', (event, sessionId, data) => {
+ try {
+ const terminal = this.terminals.get(sessionId);
+ if (terminal && terminal.pty && data) {
+ terminal.pty.write(data);
+ }
+ } catch (error) {
+ console.warn('Failed to send input to terminal:', error);
+ }
+ });
+
+ // Resize terminal
+ ipcMain.on('terminal:resize', (event, sessionId, { cols, rows }) => {
+ try {
+ const terminal = this.terminals.get(sessionId);
+ if (terminal && terminal.pty && cols > 0 && rows > 0) {
+ terminal.pty.resize(cols, rows);
+ }
+ } catch (error) {
+ console.warn('Failed to resize terminal:', error);
+ }
+ });
+
+ // Kill terminal session
+ ipcMain.on('terminal:kill', (event, sessionId) => {
+ const terminal = this.terminals.get(sessionId);
+ if (terminal && terminal.pty) {
+ try {
+ terminal.pty.kill();
+ this.terminals.delete(sessionId);
+ } catch (error) {
+ console.error('Failed to kill terminal:', error);
+ }
+ }
+ });
+
+ // Get list of all active terminal sessions
+ ipcMain.handle('terminal:list-sessions', (event) => {
+ try {
+ const sessions = [];
+ for (const [sessionId, terminal] of this.terminals.entries()) {
+ if (terminal && terminal.pty) {
+ // Check if process is still alive
+ try {
+ const pid = terminal.pty.pid;
+ process.kill(pid, 0); // Signal 0 just checks if process exists
+ sessions.push({
+ sessionId,
+ cwd: terminal.cwd || '', // Use stored cwd
+ pid: pid
+ });
+ } catch (error) {
+ // Process doesn't exist, remove it
+ this.terminals.delete(sessionId);
+ }
+ }
+ }
+ return sessions;
+ } catch (error) {
+ console.error('Failed to list terminal sessions:', error);
+ return [];
+ }
+ });
+ }
+
+ getDefaultShell() {
+ if (process.platform === 'win32') {
+ // Try PowerShell Core first (pwsh.exe), then fall back to Windows PowerShell (powershell.exe)
+ return process.env.PWSH || 'powershell.exe';
+ } else {
+ return process.env.SHELL || '/bin/bash';
+ }
+ }
+
+ getDefaultCwd() {
+ // Try to use user's home directory as default
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
+ }
+
+ generateSessionId() {
+ return `terminal_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
+ }
+
+ // Clean up terminals when window closes
+ cleanup(webContents) {
+ for (const [sessionId, terminal] of this.terminals.entries()) {
+ if (terminal.webContents === webContents) {
+ try {
+ terminal.pty.kill();
+ this.terminals.delete(sessionId);
+ } catch (error) {
+ console.error('Failed to cleanup terminal:', error);
+ }
+ }
+ }
+ }
+
+ // Kill all terminals
+ killAll() {
+ for (const [sessionId, terminal] of this.terminals.entries()) {
+ try {
+ terminal.pty.kill();
+ } catch (error) {
+ console.error('Failed to kill terminal:', error);
+ }
+ }
+ this.terminals.clear();
+ }
+}
+
+module.exports = TerminalManager;
diff --git a/packages/bruno-electron/src/preload.js b/packages/bruno-electron/src/preload.js
index 49c9c1b8d..6ba81c073 100644
--- a/packages/bruno-electron/src/preload.js
+++ b/packages/bruno-electron/src/preload.js
@@ -2,9 +2,14 @@ const { ipcRenderer, contextBridge, webUtils, shell } = require('electron');
contextBridge.exposeInMainWorld('ipcRenderer', {
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
+ send: (channel, ...args) => ipcRenderer.send(channel, ...args),
on: (channel, handler) => {
// Deliberately strip event as it includes `sender`
- const subscription = (event, ...args) => handler(...args);
+ const subscription = (event, ...args) => {
+ // Ensure args is always an array to prevent undefined errors
+ const safeArgs = args && args.length ? args : [];
+ handler(...safeArgs);
+ };
ipcRenderer.on(channel, subscription);
return () => {