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
================================================
[](https://github.com/browser-use/desktop/stargazers)
[](https://link.browser-use.com/discord)
[](https://docs.browser-use.com)
[](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.
## 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({});