Repository: browser-use/desktop Branch: main Commit: 600a8b3c88e2 Files: 17 Total size: 53.5 KB Directory structure: gitextract_ja6d5fvp/ ├── .eslintrc.json ├── .gitignore ├── README.md ├── forge.config.ts ├── forge.env.d.ts ├── index.html ├── package.json ├── src/ │ ├── config.ts │ ├── index.css │ ├── main.ts │ ├── preload.ts │ ├── renderer.ts │ └── types/ │ └── electron-squirrel-startup.d.ts ├── tsconfig.json ├── vite.main.config.ts ├── vite.preload.config.ts └── vite.renderer.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "env": { "browser": true, "es6": true, "node": true }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:import/recommended", "plugin:import/electron", "plugin:import/typescript" ], "parser": "@typescript-eslint/parser" } ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock .DS_Store # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # Webpack .webpack/ # Vite .vite/ # Electron-Forge out/ ================================================ FILE: README.md ================================================ Browser Use Desktop App
[![GitHub stars](https://img.shields.io/github/stars/browser-use/desktop?style=social)](https://github.com/browser-use/desktop/stargazers) [![Discord](https://img.shields.io/discord/1303749220842340412?color=7289DA&label=Discord&logo=discord&logoColor=white)](https://link.browser-use.com/discord) [![Documentation](https://img.shields.io/badge/Documentation-📕-blue)](https://docs.browser-use.com) [![Browser-Use](https://img.shields.io/twitter/follow/browser_use?style=social)](https://x.com/browser_use) This project is designed to make websites accessible for AI agents and builds upon the foundation of [browser-use](https://github.com/browser-use/browser-use) + [web-ui](https://github.com/browser-use/web-ui). **UI:** is built on Electron and Gradio and supports most `browser-use` functionalities. This UI is designed to be user-friendly and enables easy interaction with the browser agent. **Expanded LLM Support:** We've integrated support for various Large Language Models (LLMs), including: Google, OpenAI, Azure OpenAI, Anthropic, DeepSeek, Ollama etc. And we plan to add support for even more models in the future. **Custom Browser Support:** The desktop app uses your existing Google Chrome browser, eliminating the need to re-login to sites or deal with other authentication challenges. This feature also supports high-definition screen recording. Screenshot of Browser Use Desktop running on macOS ## Get Started ```bash git clone https://github.com/browser-use/desktop cd desktop npm install vite dev ``` ## Download - Download for macOS *(Coming soon...)* - Download for Windows *(Coming soon...)* - Download for Linux *(Coming soon...)* ================================================ FILE: forge.config.ts ================================================ import type { ForgeConfig } from '@electron-forge/shared-types'; import { MakerSquirrel } from '@electron-forge/maker-squirrel'; import { MakerZIP } from '@electron-forge/maker-zip'; import { MakerDeb } from '@electron-forge/maker-deb'; import { MakerRpm } from '@electron-forge/maker-rpm'; import { VitePlugin } from '@electron-forge/plugin-vite'; import { FusesPlugin } from '@electron-forge/plugin-fuses'; import { FuseV1Options, FuseVersion } from '@electron/fuses'; const config: ForgeConfig = { packagerConfig: { asar: true, }, rebuildConfig: {}, makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin']), new MakerRpm({}), new MakerDeb({})], plugins: [ new VitePlugin({ // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc. // If you are familiar with Vite configuration, it will look really familiar. build: [ { // `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`. entry: 'src/main.ts', config: 'vite.main.config.ts', target: 'main', }, { entry: 'src/preload.ts', config: 'vite.preload.config.ts', target: 'preload', }, ], renderer: [ { name: 'main_window', config: 'vite.renderer.config.ts', }, ], }), // Fuses are used to enable/disable various Electron functionality // at package time, before code signing the application new FusesPlugin({ version: FuseVersion.V1, [FuseV1Options.RunAsNode]: false, [FuseV1Options.EnableCookieEncryption]: true, [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, [FuseV1Options.EnableNodeCliInspectArguments]: false, [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, [FuseV1Options.OnlyLoadAppFromAsar]: true, }), ], }; export default config; ================================================ FILE: forge.env.d.ts ================================================ /// ================================================ FILE: index.html ================================================ Browser-Use Desktop

Browser-Use Desktop

Starting Python web server...

Console Output

×
================================================ FILE: package.json ================================================ { "name": "browser-use-desktop", "productName": "browser-use-desktop", "version": "1.0.0", "description": "My Electron application description", "main": ".vite/build/main.js", "scripts": { "start": "electron-forge start", "package": "electron-forge package", "make": "electron-forge make", "publish": "electron-forge publish", "lint": "eslint --ext .ts,.tsx ." }, "keywords": [], "author": { "name": "Nick Sweeting", "email": "git@sweeting.me" }, "license": "MIT", "devDependencies": { "@electron-forge/cli": "^7.8.0", "@electron-forge/maker-deb": "^7.8.0", "@electron-forge/maker-rpm": "^7.8.0", "@electron-forge/maker-squirrel": "^7.8.0", "@electron-forge/maker-zip": "^7.8.0", "@electron-forge/plugin-auto-unpack-natives": "^7.8.0", "@electron-forge/plugin-fuses": "^7.8.0", "@electron-forge/plugin-vite": "^7.8.0", "@electron/fuses": "^1.8.0", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "electron": "35.1.2", "eslint": "^8.57.1", "eslint-plugin-import": "^2.31.0", "ts-node": "^10.9.2", "typescript": "~4.5.4", "vite": "^5.4.15" }, "dependencies": { "electron-squirrel-startup": "^1.0.1" } } ================================================ FILE: src/config.ts ================================================ // Configuration for processes export const pythonCommand = { path: './.venv/bin/python', args: ['webui.py', '--ip', '127.0.0.1', '--port', '7788'], workingDir: 'lib/web-ui', // Full command string for display get display(): string { return `${this.path} ${this.args.join(' ')}`; } }; // Platform type for OS detection type Platform = 'darwin' | 'win32' | 'linux' | string; // Define the nodeAPI interface that's injected by the preload script declare global { interface Window { nodeAPI?: { platform: Platform; homedir: string; env: { HOME?: string; USERPROFILE?: string; LOCALAPPDATA?: string; }; pathSep: string; }; } } // Get platform info from nodeAPI bridge or fallback to browser detection function getPlatform(): Platform { if (typeof window !== 'undefined' && window.nodeAPI) { return window.nodeAPI.platform; } // Fallback to browser detection if nodeAPI is not available if (typeof navigator !== 'undefined') { const userAgent = navigator.userAgent; if (userAgent.includes('Win')) return 'win32'; if (userAgent.includes('Mac')) return 'darwin'; if (userAgent.includes('Linux')) return 'linux'; } return 'unknown'; } // Get home directory from nodeAPI bridge function getHomeDir(): string { if (typeof window !== 'undefined' && window.nodeAPI) { return window.nodeAPI.homedir || ''; } return ''; } // Get environment variables from nodeAPI bridge function getEnvVar(name: string): string { if (typeof window !== 'undefined' && window.nodeAPI && window.nodeAPI.env) { // Use type assertion to bypass TypeScript's index signature check return (window.nodeAPI.env as Record)[name] || ''; } return ''; } // Default Chrome paths by platform const CHROME_PATHS: Record = { darwin: [ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta', '/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev', '/Applications/Chromium.app/Contents/MacOS/Chromium', ], win32: [ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', `${getEnvVar('LOCALAPPDATA')}\\Google\\Chrome\\Application\\chrome.exe`, 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe', // Edge as fallback ], linux: [ '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/opt/google/chrome/chrome', '/usr/bin/chromium-browser', '/usr/bin/chromium', '/snap/bin/chromium', ] }; // Default user data directories by platform const USER_DATA_DIRS: Record string> = { darwin: (homeDir: string) => `${homeDir}/Library/Application Support/Google/Chrome`, win32: (homeDir: string) => `${homeDir}\\AppData\\Local\\Google\\Chrome\\User Data`, linux: (homeDir: string) => `${homeDir}/.config/google-chrome` }; // Determine Chrome path based on OS function getChromePath(): string { // Get platform from nodeAPI const currentPlatform = getPlatform(); console.log('Detected platform:', currentPlatform); const paths = CHROME_PATHS[currentPlatform] || CHROME_PATHS.linux; console.log('Potential Chrome paths:', paths); // Try direct check in Node environment if (typeof window === 'undefined' || typeof require !== 'undefined') { try { // Import fs dynamically to avoid linter errors // eslint-disable-next-line @typescript-eslint/no-var-requires const fs = require('fs'); console.log('Checking if Chrome paths exist using Node.js fs module'); for (const path of paths) { try { if (fs.existsSync(path)) { console.log('Found Chrome at:', path); return path; } } catch (e) { console.warn(`Error checking Chrome path ${path}:`, e); } } } catch (e) { console.warn('Could not use fs module to check paths:', e); } } // In browser context or if no valid path found, return the first path for the platform console.log('Using default Chrome path for platform:', paths[0]); return paths[0]; } // Get default user data directory based on OS function getUserDataDir(): string { const homeDir = getHomeDir() || getEnvVar('HOME') || getEnvVar('USERPROFILE') || ''; const currentPlatform = getPlatform(); const dirFn = USER_DATA_DIRS[currentPlatform] || USER_DATA_DIRS.linux; return dirFn(homeDir); } // Basic Chrome command class - actual initialization happens in main.ts class ChromeCommand { private _path = ''; private _args = [ '--remote-debugging-port=9222', '--window-position=0,0', '--disable-web-security' ]; get path(): string { return this._path; } set path(value: string) { this._path = value; } get args(): string[] { return this._args; } set args(value: string[]) { this._args = value; } get display(): string { return `${this.path} ${this.args.join(' ')}`; } } export const chromeCommand = new ChromeCommand(); ================================================ FILE: src/index.css ================================================ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; margin: auto; max-width: 38rem; padding: 2rem; } ================================================ FILE: src/main.ts ================================================ import { app, BrowserWindow, ipcMain, screen } from 'electron'; import path from 'node:path'; import { spawn, ChildProcess } from 'child_process'; import started from 'electron-squirrel-startup'; import { pythonCommand, chromeCommand } from './config'; import fs from 'fs'; import os from 'os'; import http from 'http'; // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) { app.quit(); } let pyProcess: ChildProcess | null = null; let chromeProcess: ChildProcess | null = null; // Clear any potentially existing IPC handlers function clearIPCHandlers() { ipcMain.removeAllListeners('restart-python'); ipcMain.removeAllListeners('restart-chrome'); } // Create required directories function createRequiredDirectories() { const baseDir = path.join(os.homedir(), 'Downloads', 'browser-use'); const dirs = ['recordings', 'traces', 'history']; dirs.forEach(dir => { const dirPath = path.join(baseDir, dir); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } }); } // Load default settings from JSON function loadDefaultSettings() { const settingsPath = path.join(app.getAppPath(), 'lib/web-ui/tmp/webui_settings/31ccfe5a-ef3b-4064-836c-6910ab3a3281.json'); try { if (fs.existsSync(settingsPath)) { const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); return settings; } } catch (e) { console.error('Error loading default settings:', e); } return null; } const createWindow = () => { // Make sure we clean up any existing handlers first clearIPCHandlers(); // Get the primary display dimensions const primaryDisplay = screen.getPrimaryDisplay(); const { width, height } = primaryDisplay.workAreaSize; // Calculate half width for positioning const halfWidth = Math.floor(width / 2); // Create the browser window positioned on left half const mainWindow = new BrowserWindow({ width: halfWidth, height: height, x: 0, y: 0, frame: false, // Remove window frame titleBarStyle: 'hidden', // Hide title bar on macOS backgroundColor: '#000000', // Set black background webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, webviewTag: true, // Enable webview tag sandbox: false, // Disable sandbox for preload script }, }); // Set up dark mode mainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents.insertCSS(` body { background-color: #000000; color: #ffffff; } * { color-scheme: dark; } `); }); // Clean up IPC handlers when window is closed mainWindow.on('closed', () => { clearIPCHandlers(); }); // Set up IPC handlers for restarting processes ipcMain.on('restart-python', () => { startPyProcess(mainWindow); }); ipcMain.on('restart-chrome', () => { startChromeProcess(mainWindow); }); // and load the index.html of the app. if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); } else { mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)); } // Open the DevTools. mainWindow.webContents.openDevTools(); // Load default settings and pass them to the web UI const defaultSettings = loadDefaultSettings(); if (defaultSettings) { mainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents.send('load-default-settings', defaultSettings); }); } // Start the Python process startPyProcess(mainWindow); // Wait a bit before starting Chrome to ensure Python is running first setTimeout(() => { startChromeProcess(mainWindow); }, 1000); }; function startPyProcess(mainWindow: BrowserWindow) { // Find Chrome path first - needed for environment variables const chromePath = findBestChromeExecutable(); if (!chromePath) { console.error('Could not find Chrome for Python process environment'); } // Ensure user data directory exists const userDataDir = ensureChromeUserDataDir(); // Working directory for the subprocess const options = { cwd: path.join(app.getAppPath(), pythonCommand.workingDir), shell: false, env: { ...process.env, BROWSER_USE_DESKTOP_APP: 'true', CHROME_PATH: chromePath || '', CHROME_CDP: 'http://localhost:9222', CHROME_USER_DATA: userDataDir } }; // Clear existing process if it exists if (pyProcess) { try { // Remove all listeners first to prevent callback after destroy pyProcess.stdout.removeAllListeners(); pyProcess.stderr.removeAllListeners(); pyProcess.removeAllListeners(); pyProcess.kill(); } catch (e) { console.error('Error killing Python process:', e); } pyProcess = null; } // Spawn the Python process pyProcess = spawn(pythonCommand.path, pythonCommand.args, options); // Send process output to the renderer pyProcess.stdout.on('data', (data) => { const output = data.toString(); if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('py-output', { type: 'stdout', data: output }); } }); pyProcess.stderr.on('data', (data) => { const output = data.toString(); if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('py-output', { type: 'stderr', data: output }); } }); pyProcess.on('error', (error) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('py-output', { type: 'error', data: `Failed to start subprocess: ${error.message}` }); } }); pyProcess.on('close', (code) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('py-output', { type: 'info', data: `Python process exited with code ${code}` }); } pyProcess = null; }); // Notify the renderer the process has started if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('py-started'); } // We'll let the renderer detect when the server is ready from the stdout logs // but still send the ready signal after a longer timeout as a fallback setTimeout(() => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('py-ready', 'http://127.0.0.1:7788'); } }, 10000); // 10 seconds delay as fallback } // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(() => { createRequiredDirectories(); createWindow(); app.on('activate', function () { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); // Kill the Python and Chrome processes before the app quits app.on('before-quit', (event) => { // Prevent the default quit behavior so we can clean up first event.preventDefault(); // Counter to track cleanup completion let cleanupCounter = 0; const processesToCleanup = (pyProcess ? 1 : 0) + (chromeProcess ? 1 : 0); if (processesToCleanup === 0) { // No processes to clean up, safe to quit immediately app.exit(0); return; } // Set a safety timeout to force quit after 1 second if processes don't exit properly const forceQuitTimeout = setTimeout(() => { console.warn('Force quitting after timeout'); app.exit(0); }, 1000); // Function to check if cleanup is done const checkCleanup = () => { cleanupCounter++; if (cleanupCounter >= processesToCleanup) { // All processes cleaned up, now safe to exit clearTimeout(forceQuitTimeout); // Clear the force quit timeout app.exit(0); } }; // Clean up Python process if (pyProcess) { try { // Kill the process immediately pyProcess.kill('SIGKILL'); pyProcess = null; checkCleanup(); } catch (e) { console.error('Error killing Python process:', e); pyProcess = null; checkCleanup(); } } // Clean up Chrome process if (chromeProcess) { try { // Kill the process immediately chromeProcess.kill('SIGKILL'); chromeProcess = null; checkCleanup(); } catch (e) { console.error('Error killing Chrome process:', e); chromeProcess = null; checkCleanup(); } } }); function startChromeProcess(mainWindow: BrowserWindow) { // Clear existing process if it exists if (chromeProcess) { try { // Remove all listeners first to prevent callback after destroy chromeProcess.stdout.removeAllListeners(); chromeProcess.stderr.removeAllListeners(); chromeProcess.removeAllListeners(); chromeProcess.kill(); } catch (e) { console.error('Error killing Chrome process:', e); } chromeProcess = null; } // Find the best Chrome executable for the current OS const chromePath = findBestChromeExecutable(); if (!chromePath) { const errorMsg = 'Could not find a valid Chrome executable. Please install Google Chrome and try again.'; console.error(errorMsg); mainWindow.webContents.send('chrome-output', { type: 'error', data: errorMsg }); return; } // Update chromeCommand with the found path chromeCommand.path = chromePath; // Ensure Chrome user data directory exists const userDataDir = ensureChromeUserDataDir(); // Get screen dimensions for window positioning const primaryDisplay = screen.getPrimaryDisplay(); const { width, height } = primaryDisplay.workAreaSize; const halfWidth = Math.floor(width / 2); // Compose Chrome arguments with the validated user data directory const args = [ '--remote-debugging-port=9222', `--window-position=${halfWidth},0`, `--window-size=${halfWidth},${height}`, '--install-autogenerated-theme=0,0,0', // Theme setting '--disable-web-security', `--user-data-dir=${userDataDir}`, '--profile-directory=Default', '--no-first-run', '--no-default-browser-check' ]; // Add compatibility flags args.push('--disable-features=TranslateUI'); args.push('--disable-extensions'); // Add minimal necessary flags for stability based on platform if (process.platform === 'linux') { // Linux often needs these flags args.push('--no-sandbox'); args.push('--disable-gpu'); } // Store the args in chromeCommand for other parts of the app chromeCommand.args = args; // Log Chrome launch information console.log('Launching Chrome with path:', chromePath); console.log('Chrome arguments:', args); try { // Spawn the Chrome process chromeProcess = spawn(chromePath, args); if (!chromeProcess || !chromeProcess.pid) { const errorMsg = `Failed to launch Chrome process - could not start the process`; console.error(errorMsg); mainWindow.webContents.send('chrome-output', { type: 'error', data: errorMsg }); return; } console.log(`Chrome process started with PID: ${chromeProcess.pid}`); // Set up Chrome process event listeners chromeProcess.stdout.on('data', (data) => { const output = data.toString(); console.log('Chrome stdout:', output); if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('chrome-output', { type: 'stdout', data: output }); } }); chromeProcess.stderr.on('data', (data) => { const output = data.toString(); console.log('Chrome stderr:', output); if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('chrome-output', { type: 'stderr', data: output }); } }); chromeProcess.on('error', (error) => { console.error('Chrome process error:', error.message); if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('chrome-output', { type: 'error', data: `Failed to start Chrome: ${error.message}` }); } }); chromeProcess.on('close', (code) => { console.log(`Chrome process exited with code ${code}`); if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('chrome-output', { type: 'info', data: `Chrome process exited with code ${code}` }); } chromeProcess = null; }); // Notify the renderer the process has started if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('chrome-started'); } // Verify Chrome is listening on debug port setTimeout(() => { verifyChromeLaunched(mainWindow); }, 2000); } catch (error) { const errorMsg = `Unexpected error launching Chrome: ${error}`; console.error(errorMsg); if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('chrome-output', { type: 'error', data: errorMsg }); } } } /** * Verifies that Chrome launched successfully and is accessible via debug port */ function verifyChromeLaunched(mainWindow: BrowserWindow): void { console.log('Verifying Chrome is accessible on debug port...'); try { // Try to connect to Chrome's debug port const options = { host: '127.0.0.1', port: 9222, path: '/json/version', timeout: 2000 }; const req = http.get(options, (res) => { if (res.statusCode === 200) { console.log('Chrome debug port connection successful'); let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const info = JSON.parse(data); console.log('Chrome debug info:', info); if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('chrome-output', { type: 'info', data: `Chrome ready - ${info.Browser || 'Chrome'} [${info.Protocol || 'CDP'}]` }); } } catch (e) { console.error('Error parsing Chrome debug info:', e); } }); } else { console.error(`Chrome debug port returned status code: ${res.statusCode}`); if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('chrome-output', { type: 'warning', data: `Chrome may not be fully initialized (status: ${res.statusCode})` }); } } }); req.on('error', (e) => { console.error('Error connecting to Chrome debug port:', e.message); if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('chrome-output', { type: 'warning', data: `Chrome debug port not accessible: ${e.message}. The browser may still be starting.` }); } }); req.end(); } catch (e) { console.error('Error verifying Chrome launch:', e); } } /** * Finds the best Chrome executable for the current OS * @returns The path to the Chrome executable or null if not found */ function findBestChromeExecutable(): string | null { // Define platform-specific paths in order of preference const paths: string[] = []; if (process.platform === 'darwin') { // macOS paths paths.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'); paths.push('/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta'); paths.push('/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev'); paths.push('/Applications/Chromium.app/Contents/MacOS/Chromium'); // Add fallback for homebrew installations paths.push('/opt/homebrew/bin/chromium'); } else if (process.platform === 'win32') { // Windows paths paths.push('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'); paths.push('C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'); // Add LocalAppData path if available const localAppData = process.env.LOCALAPPDATA; if (localAppData) { paths.push(`${localAppData}\\Google\\Chrome\\Application\\chrome.exe`); } // Microsoft Edge as fallback (Chromium-based) paths.push('C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe'); paths.push('C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'); } else { // Linux paths paths.push('/usr/bin/google-chrome'); paths.push('/usr/bin/google-chrome-stable'); paths.push('/opt/google/chrome/chrome'); paths.push('/usr/bin/chromium-browser'); paths.push('/usr/bin/chromium'); paths.push('/snap/bin/chromium'); } console.log('Checking for Chrome at these paths:', paths); // Find first existing path for (const path of paths) { if (fs.existsSync(path)) { console.log(`Found Chrome executable at: ${path}`); return path; } } console.error('No Chrome executable found in common locations'); return null; } /** * Ensures the Chrome user data directory exists * @returns The path to the Chrome user data directory */ function ensureChromeUserDataDir(): string { // Determine user data directory based on platform let userDataDir: string; const homeDir = os.homedir(); const appName = 'browser-use-desktop'; if (process.platform === 'darwin') { userDataDir = `${homeDir}/Library/Application Support/${appName}/ChromeProfile`; } else if (process.platform === 'win32') { userDataDir = `${homeDir}\\AppData\\Local\\${appName}\\ChromeProfile`; } else { userDataDir = `${homeDir}/.config/${appName}/ChromeProfile`; } // Log the user data directory path console.log(`Using Chrome user data directory: ${userDataDir}`); // Ensure the directory exists try { if (!fs.existsSync(userDataDir)) { fs.mkdirSync(userDataDir, { recursive: true }); console.log(`Created Chrome user data directory: ${userDataDir}`); // Create first_run file to prevent first run experience const firstRunPath = path.join(userDataDir, 'First Run'); fs.writeFileSync(firstRunPath, ''); console.log('Created First Run file to disable welcome screen'); // Create an empty Preferences file with basic settings const prefsPath = path.join(userDataDir, 'Default', 'Preferences'); // Create Default directory if it doesn't exist const defaultDir = path.join(userDataDir, 'Default'); if (!fs.existsSync(defaultDir)) { fs.mkdirSync(defaultDir, { recursive: true }); } // Basic preferences to disable welcome page, etc. const defaultPrefs = { browser: { custom_chrome_frame: false, check_default_browser: false }, profile: { default_content_setting_values: { notifications: 2 // Block notifications: 1=allow, 2=block, 3=ask } }, session: { restore_on_startup: 5 // Don't restore anything: 5=open new tab }, bookmark_bar: { show_on_all_tabs: false }, distribution: { import_bookmarks: false, import_history: false, import_search_engine: false, make_chrome_default: false, show_welcome_page: false, skip_first_run_ui: true } }; fs.writeFileSync(prefsPath, JSON.stringify(defaultPrefs, null, 2)); console.log('Created default Chrome preferences file'); } else { console.log(`Using existing Chrome user data directory: ${userDataDir}`); } } catch (err) { console.error(`Error creating Chrome user data directory: ${err}`); // Fallback to a temporary directory userDataDir = path.join(os.tmpdir(), `${appName}-chrome-profile`); fs.mkdirSync(userDataDir, { recursive: true }); console.log(`Using fallback Chrome user data directory: ${userDataDir}`); } return userDataDir; } // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and import them here. ================================================ FILE: src/preload.ts ================================================ // See the Electron documentation for details on how to use preload scripts: // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts import { contextBridge, ipcRenderer } from 'electron'; import * as os from 'os'; import * as fs from 'fs'; // Define types for the bridges type PyOutputCallback = (data: { type: string; data: string }) => void; type PyStartedCallback = () => void; type PyReadyCallback = (url: string) => void; type ChromeOutputCallback = (data: { type: string; data: string }) => void; type ChromeStartedCallback = () => void; type SettingsCallback = (settings: Record) => void; // Expose protected methods that allow the renderer process to use // the ipcRenderer without exposing the entire object contextBridge.exposeInMainWorld('pyBridge', { // Listen for messages from the main process onPyOutput: (callback: PyOutputCallback) => { ipcRenderer.on('py-output', (_, data) => callback(data)); }, onPyStarted: (callback: PyStartedCallback) => { ipcRenderer.on('py-started', () => callback()); }, onPyReady: (callback: PyReadyCallback) => { ipcRenderer.on('py-ready', (_, url) => callback(url)); }, // Restart the Python process restartPython: () => { ipcRenderer.send('restart-python'); }, // Clean up event listeners removeAllListeners: () => { ipcRenderer.removeAllListeners('py-output'); ipcRenderer.removeAllListeners('py-started'); ipcRenderer.removeAllListeners('py-ready'); } }); // Expose Chrome bridge for communicating Chrome process information contextBridge.exposeInMainWorld('chromeBridge', { // Listen for messages from the main process onChromeOutput: (callback: ChromeOutputCallback) => { ipcRenderer.on('chrome-output', (_, data) => callback(data)); }, onChromeStarted: (callback: ChromeStartedCallback) => { ipcRenderer.on('chrome-started', () => callback()); }, // Restart the Chrome process restartChrome: () => { ipcRenderer.send('restart-chrome'); }, // Clean up event listeners removeAllListeners: () => { ipcRenderer.removeAllListeners('chrome-output'); ipcRenderer.removeAllListeners('chrome-started'); } }); // Expose settings bridge for handling default settings contextBridge.exposeInMainWorld('settingsBridge', { // Listen for default settings from the main process onDefaultSettings: (callback: SettingsCallback) => { ipcRenderer.on('load-default-settings', (_, settings) => callback(settings)); }, // Clean up event listeners removeAllListeners: () => { ipcRenderer.removeAllListeners('load-default-settings'); } }); // Expose Node.js platform-specific APIs needed by the renderer contextBridge.exposeInMainWorld('nodeAPI', { platform: os.platform(), homedir: os.homedir(), env: { HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE, LOCALAPPDATA: process.env.LOCALAPPDATA }, pathSep: process.platform === 'win32' ? '\\' : '/' }); ================================================ FILE: src/renderer.ts ================================================ /** * This file will automatically be loaded by vite and run in the "renderer" context. * To learn more about the differences between the "main" and the "renderer" context in * Electron, visit: * * https://electronjs.org/docs/tutorial/process-model */ import './index.css'; import { pythonCommand, chromeCommand } from './config'; // Define interface for Python bridge interface PyBridge { onPyOutput: (callback: (data: { type: string; data: string }) => void) => void; onPyStarted: (callback: () => void) => void; onPyReady: (callback: (url: string) => void) => void; restartPython: () => void; removeAllListeners: () => void; } // Define interface for Chrome bridge interface ChromeBridge { onChromeOutput: (callback: (data: { type: string; data: string }) => void) => void; onChromeStarted: (callback: () => void) => void; restartChrome: () => void; removeAllListeners: () => void; } // Access the exposed API from preload script declare global { interface Window { pyBridge: PyBridge; chromeBridge: ChromeBridge; settingsBridge: { onDefaultSettings: (callback: (settings: any) => void) => void; }; } } // DOM Elements const consoleOutput = document.getElementById('console-output') as HTMLDivElement; const consoleContent = document.getElementById('console-content') as HTMLDivElement; const chromeConsoleContent = document.getElementById('chrome-console-content') as HTMLDivElement; const loadingProgress = document.getElementById('loading-progress') as HTMLDivElement; const loadingContainer = document.getElementById('loading-container') as HTMLDivElement; const webviewContainer = document.getElementById('webview-container') as HTMLDivElement; const webview = document.getElementById('webview') as HTMLElement; const launchButton = document.getElementById('launch-button') as HTMLButtonElement; const controls = document.getElementById('controls') as HTMLDivElement; const toggleConsoleButton = document.getElementById('toggle-console') as HTMLButtonElement; const closeConsoleButton = document.getElementById('close-console') as HTMLSpanElement; const tabPython = document.getElementById('tab-python') as HTMLButtonElement; const tabChrome = document.getElementById('tab-chrome') as HTMLButtonElement; const restartPythonButton = document.getElementById('restart-python') as HTMLButtonElement; const restartChromeButton = document.getElementById('restart-chrome') as HTMLButtonElement; const pythonCommandDisplay = document.getElementById('python-command') as HTMLDivElement; const chromeCommandDisplay = document.getElementById('chrome-command') as HTMLDivElement; const actionRows = document.querySelectorAll('.action-row') as NodeListOf; // Variables to track loading let progressValue = 0; let progressInterval: number | null = null; let serverUrl = ''; // Function to append output to the console function appendToConsole(message: string, type: string, target: HTMLElement = consoleContent): void { const element = document.createElement('div'); element.className = type; element.textContent = message; target.appendChild(element); target.scrollTop = target.scrollHeight; } // Function to simulate loading progress function simulateProgress(): void { progressInterval = window.setInterval(() => { if (progressValue < 90) { progressValue += Math.random() * 10; loadingProgress.style.width = `${progressValue}%`; } }, 300); } // Function to complete the loading progress function completeProgress(): void { clearInterval(progressInterval!); progressValue = 100; loadingProgress.style.width = '100%'; // Hide loading container after a short delay setTimeout(() => { loadingContainer.style.display = 'none'; // Don't show the launch button if we're auto-loading the UI if (!serverUrl) { launchButton.style.display = 'block'; } }, 500); } // Toggle console visibility function toggleConsole(): void { // Use classList to toggle visibility const isConsoleVisible = consoleOutput.classList.contains('visible'); if (isConsoleVisible) { consoleOutput.classList.remove('visible'); toggleConsoleButton.textContent = 'Show Console'; } else { consoleOutput.classList.add('visible'); toggleConsoleButton.textContent = 'Hide Console'; // Scroll to the latest output const activeTab = tabPython.classList.contains('active') ? consoleContent : chromeConsoleContent; activeTab.scrollTop = activeTab.scrollHeight; } } // Function to switch between console tabs function switchConsoleTab(tab: 'python' | 'chrome'): void { if (tab === 'python') { tabPython.classList.add('active'); tabChrome.classList.remove('active'); consoleContent.style.display = 'block'; chromeConsoleContent.style.display = 'none'; consoleContent.scrollTop = consoleContent.scrollHeight; // Show Python action row, hide Chrome action row actionRows[0].style.display = 'flex'; actionRows[1].style.display = 'none'; } else { tabPython.classList.remove('active'); tabChrome.classList.add('active'); consoleContent.style.display = 'none'; chromeConsoleContent.style.display = 'block'; chromeConsoleContent.scrollTop = chromeConsoleContent.scrollHeight; // Hide Python action row, show Chrome action row actionRows[0].style.display = 'none'; actionRows[1].style.display = 'flex'; } } // Function to load the web UI function loadWebUI(url: string): void { // Force the correct type for webview element const webviewElement = document.querySelector('webview') as Electron.WebviewTag; if (webviewElement) { webviewElement.src = url; // Handle webview events webviewElement.addEventListener('dom-ready', () => { console.log('WebView DOM ready'); }); webviewElement.addEventListener('did-start-loading', () => { console.log('WebView started loading'); }); webviewElement.addEventListener('did-finish-load', () => { console.log('WebView finished loading'); // Remove loading class when webview is loaded document.body.classList.remove('loading'); }); webviewElement.addEventListener('did-fail-load', (e) => { console.error('WebView failed to load:', e); // Show error and console if webview fails to load appendToConsole(`Failed to load webview: ${JSON.stringify(e)}`, 'error'); consoleOutput.classList.add('visible'); toggleConsoleButton.textContent = 'Hide Console'; // Scroll to the error consoleContent.scrollTop = consoleContent.scrollHeight; }); } // Show the webview and controls, hide other elements webviewContainer.style.display = 'block'; controls.style.display = 'block'; loadingContainer.style.display = 'none'; launchButton.style.display = 'none'; // Keep console visible until the UI fully loads, then hide it with a delay // This is handled in the event listeners in the init function } // Initialize the app function init(): void { // Start simulating progress simulateProgress(); // Show console automatically during loading consoleOutput.classList.add('visible'); // Set the command displays pythonCommandDisplay.textContent = pythonCommand.display; chromeCommandDisplay.textContent = chromeCommand.display; // Listen for Python process output window.pyBridge.onPyOutput((data) => { appendToConsole(data.data, data.type); // Automatically detect when server is ready from the output if (data.data.includes('Server is ready') || data.data.includes('Running on http://')) { if (!serverUrl) { serverUrl = 'http://127.0.0.1:7788'; appendToConsole(`Detected server is ready at: ${serverUrl}`, 'info'); completeProgress(); loadWebUI(serverUrl); // Hide console after server is ready and web UI is loaded setTimeout(() => { consoleOutput.classList.remove('visible'); toggleConsoleButton.textContent = 'Show Console'; }, 1000); // Small delay to allow users to see the "Server is ready" message } } }); // Listen for Python process started event window.pyBridge.onPyStarted(() => { appendToConsole('Python process started', 'info'); }); // Listen for Python ready event window.pyBridge.onPyReady((url) => { serverUrl = url; appendToConsole(`Server is ready at: ${url}`, 'info'); completeProgress(); // Automatically load the web UI loadWebUI(url); // Hide console after server is ready and web UI is loaded setTimeout(() => { consoleOutput.classList.remove('visible'); toggleConsoleButton.textContent = 'Show Console'; }, 1000); // Small delay to allow users to see the "Server is ready" message }); // Listen for Chrome process output window.chromeBridge.onChromeOutput((data) => { appendToConsole(data.data, data.type, chromeConsoleContent); }); // Listen for Chrome process started event window.chromeBridge.onChromeStarted(() => { appendToConsole('Chrome process started', 'info', chromeConsoleContent); }); // Setup tab switching tabPython.addEventListener('click', () => { switchConsoleTab('python'); restartPythonButton.style.display = 'block'; restartChromeButton.style.display = 'none'; }); tabChrome.addEventListener('click', () => { switchConsoleTab('chrome'); restartPythonButton.style.display = 'none'; restartChromeButton.style.display = 'block'; }); // Setup restart buttons restartPythonButton.addEventListener('click', () => { // Clear console consoleContent.innerHTML = ''; // Add message about restart appendToConsole('Restarting Python process...', 'info'); // Restart the process window.pyBridge.restartPython(); }); restartChromeButton.addEventListener('click', () => { // Clear console chromeConsoleContent.innerHTML = ''; // Add message about restart appendToConsole('Restarting Chrome process...', 'info', chromeConsoleContent); // Restart the process window.chromeBridge.restartChrome(); }); // Setup launch button click handler launchButton.addEventListener('click', () => { loadWebUI(serverUrl); }); // Setup toggle console button toggleConsoleButton.addEventListener('click', toggleConsole); // Setup close console button closeConsoleButton.addEventListener('click', () => { consoleOutput.classList.remove('visible'); toggleConsoleButton.textContent = 'Show Console'; }); } // Initialize the app when the DOM is loaded document.addEventListener('DOMContentLoaded', init); // Clean up event listeners when window is closed window.addEventListener('beforeunload', () => { window.pyBridge.removeAllListeners(); window.chromeBridge.removeAllListeners(); if (progressInterval) { clearInterval(progressInterval); } }); ================================================ FILE: src/types/electron-squirrel-startup.d.ts ================================================ declare module 'electron-squirrel-startup' { const started: boolean; export default started; } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "module": "commonjs", "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "noImplicitAny": true, "sourceMap": true, "baseUrl": ".", "outDir": "dist", "moduleResolution": "node", "resolveJsonModule": true } } ================================================ FILE: vite.main.config.ts ================================================ import { defineConfig } from 'vite'; // https://vitejs.dev/config export default defineConfig({}); ================================================ FILE: vite.preload.config.ts ================================================ import { defineConfig } from 'vite'; // https://vitejs.dev/config export default defineConfig({}); ================================================ FILE: vite.renderer.config.ts ================================================ import { defineConfig } from 'vite'; // https://vitejs.dev/config export default defineConfig({});