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
================================================
<img src="./assets/desktop.png" alt="Browser Use Desktop App" width="full"/>
<br/>
[](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.
<img width="100%" alt="Screenshot of Browser Use Desktop running on macOS" src="https://github.com/user-attachments/assets/94e9fec1-7b17-453e-af6d-2e35f54b46ab" />
## 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
================================================
/// <reference types="@electron-forge/plugin-vite/forge-vite-env" />
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Browser-Use Desktop</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
h1 {
margin-bottom: 20px;
color: #2c3e50;
}
main {
display: flex;
flex-direction: column;
gap: 20px;
}
.console-output {
background-color: #263238;
color: #eeffff;
padding: 0;
border-radius: 5px 5px 0 0;
height: 300px;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 15;
transform: translateY(100%);
transition: transform 0.3s ease;
box-shadow: 0 -2px 10px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
}
.console-output.visible {
transform: translateY(0);
}
/* Adjust layout for loading state */
body.loading .console-output.visible {
height: 250px;
}
.console-header {
background-color: #1e272c;
padding: 8px 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #37474F;
}
.console-header h3 {
margin: 0;
font-size: 14px;
font-weight: normal;
color: #B0BEC5;
}
.console-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.console-tabs {
display: flex;
gap: 8px;
}
.console-tab {
background-color: #37474F;
border: none;
color: #B0BEC5;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.console-tab.active {
background-color: #546E7A;
color: #ECEFF1;
}
.console-tab:hover {
background-color: #455A64;
}
#console-actions {
background-color: #1e272c;
padding: 8px 15px;
display: flex;
border-bottom: 1px solid #37474F;
}
.restart-btn {
background-color: #455A64;
color: #ECEFF1;
border: none;
border-radius: 4px;
padding: 6px 10px;
font-size: 12px;
cursor: pointer;
margin-right: 10px;
}
.restart-btn:hover {
background-color: #546E7A;
}
.action-row {
display: flex;
align-items: center;
width: 100%;
}
.command-display {
margin-left: 15px;
font-family: monospace;
color: #81C784;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
font-size: 12px;
}
.console-content {
overflow-y: auto;
padding: 15px;
font-family: monospace;
white-space: pre-wrap;
flex: 1;
height: 100%;
}
.command-line {
color: #81C784;
font-weight: bold;
padding: 5px 0;
border-bottom: 1px solid #37474F;
margin-bottom: 10px;
}
.close-btn {
font-size: 24px;
color: #B0BEC5;
cursor: pointer;
user-select: none;
line-height: 1;
}
.close-btn:hover {
color: #ECEFF1;
}
#console-content {
overflow-y: auto;
padding: 15px;
font-family: monospace;
white-space: pre-wrap;
flex: 1;
}
.console-output .stderr {
color: #ff5252;
}
.console-output .info {
color: #69f0ae;
}
.console-output .error {
color: #ff7043;
}
.loading-container {
margin: 20px 0;
}
.loading-bar {
height: 20px;
background-color: #ddd;
border-radius: 10px;
overflow: hidden;
}
.loading-progress {
height: 100%;
background-color: #4caf50;
width: 0%;
transition: width 0.3s ease;
}
.webview-container {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 10;
}
.controls {
position: fixed;
bottom: 10px;
right: 10px;
z-index: 20;
background-color: rgba(0,0,0,0.6);
border-radius: 5px;
padding: 8px;
display: none;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
transition: opacity 0.3s ease;
}
.controls:hover {
opacity: 1;
}
.controls button {
background-color: #2196f3;
color: white;
border: none;
border-radius: 4px;
padding: 10px 15px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.2s ease;
}
.controls button:hover {
background-color: #0d8aee;
}
webview {
width: 100%;
height: 100%;
border: none;
}
.launch-button {
display: none;
padding: 10px 20px;
background-color: #2196f3;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
margin-top: 10px;
}
.launch-button:hover {
background-color: #1976d2;
}
</style>
</head>
<body class="loading">
<h1>Browser-Use Desktop</h1>
<main>
<div id="loading-container" class="loading-container">
<h3>Starting Python web server...</h3>
<div class="loading-bar">
<div id="loading-progress" class="loading-progress"></div>
</div>
</div>
<div id="console-output" class="console-output">
<div class="console-header">
<h3>Console Output</h3>
<div class="console-tabs">
<button id="tab-python" class="console-tab active">Python</button>
<button id="tab-chrome" class="console-tab">Chrome</button>
</div>
<span id="close-console" class="close-btn">×</span>
</div>
<div id="console-actions">
<div class="action-row">
<button id="restart-python" class="restart-btn">Restart Python</button>
<div id="python-command" class="command-display"></div>
</div>
<div class="action-row" style="display: none;">
<button id="restart-chrome" class="restart-btn">Restart Chrome</button>
<div id="chrome-command" class="command-display"></div>
</div>
</div>
<div id="console-content"></div>
<div id="chrome-console-content" class="console-content" style="display: none;"></div>
</div>
<button id="launch-button" class="launch-button">Open Web UI</button>
<div id="webview-container" class="webview-container">
<webview id="webview" src="about:blank" webpreferences="contextIsolation=yes" allowpopups></webview>
</div>
<div id="controls" class="controls">
<button id="toggle-console">Show Console</button>
</div>
</main>
<script type="module" src="/src/renderer.ts"></script>
</body>
</html>
================================================
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<string, string | undefined>)[name] || '';
}
return '';
}
// Default Chrome paths by platform
const CHROME_PATHS: Record<Platform, string[]> = {
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<Platform, (homeDir: string) => 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<string, any>) => 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<HTMLDivElement>;
// 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({});
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
SYMBOL INDEX (40 symbols across 4 files)
FILE: src/config.ts
method display (line 7) | get display(): string {
type Platform (line 13) | type Platform = 'darwin' | 'win32' | 'linux' | string;
type Window (line 17) | interface Window {
function getPlatform (line 32) | function getPlatform(): Platform {
function getHomeDir (line 49) | function getHomeDir(): string {
function getEnvVar (line 57) | function getEnvVar(name: string): string {
constant CHROME_PATHS (line 66) | const CHROME_PATHS: Record<Platform, string[]> = {
constant USER_DATA_DIRS (line 90) | const USER_DATA_DIRS: Record<Platform, (homeDir: string) => string> = {
function getChromePath (line 97) | function getChromePath(): string {
function getUserDataDir (line 133) | function getUserDataDir(): string {
class ChromeCommand (line 142) | class ChromeCommand {
method path (line 150) | get path(): string {
method path (line 154) | set path(value: string) {
method args (line 158) | get args(): string[] {
method args (line 162) | set args(value: string[]) {
method display (line 166) | get display(): string {
FILE: src/main.ts
function clearIPCHandlers (line 19) | function clearIPCHandlers() {
function createRequiredDirectories (line 25) | function createRequiredDirectories() {
function loadDefaultSettings (line 38) | function loadDefaultSettings() {
function startPyProcess (line 134) | function startPyProcess(mainWindow: BrowserWindow) {
function startChromeProcess (line 306) | function startChromeProcess(mainWindow: BrowserWindow) {
function verifyChromeLaunched (line 445) | function verifyChromeLaunched(mainWindow: BrowserWindow): void {
function findBestChromeExecutable (line 509) | function findBestChromeExecutable(): string | null {
function ensureChromeUserDataDir (line 563) | function ensureChromeUserDataDir(): string {
FILE: src/preload.ts
type PyOutputCallback (line 9) | type PyOutputCallback = (data: { type: string; data: string }) => void;
type PyStartedCallback (line 10) | type PyStartedCallback = () => void;
type PyReadyCallback (line 11) | type PyReadyCallback = (url: string) => void;
type ChromeOutputCallback (line 12) | type ChromeOutputCallback = (data: { type: string; data: string }) => void;
type ChromeStartedCallback (line 13) | type ChromeStartedCallback = () => void;
type SettingsCallback (line 14) | type SettingsCallback = (settings: Record<string, any>) => void;
FILE: src/renderer.ts
type PyBridge (line 13) | interface PyBridge {
type ChromeBridge (line 22) | interface ChromeBridge {
type Window (line 31) | interface Window {
function appendToConsole (line 66) | function appendToConsole(message: string, type: string, target: HTMLElem...
function simulateProgress (line 75) | function simulateProgress(): void {
function completeProgress (line 85) | function completeProgress(): void {
function toggleConsole (line 101) | function toggleConsole(): void {
function switchConsoleTab (line 118) | function switchConsoleTab(tab: 'python' | 'chrome'): void {
function loadWebUI (line 141) | function loadWebUI(url: string): void {
function init (line 184) | function init(): void {
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (58K chars).
[
{
"path": ".eslintrc.json",
"chars": 352,
"preview": "{\n \"env\": {\n \"browser\": true,\n \"es6\": true,\n \"node\": true\n },\n \"extends\": [\n \"eslint:recommended\",\n \"p"
},
{
"path": ".gitignore",
"chars": 1215,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
},
{
"path": "README.md",
"chars": 1866,
"preview": "\n<img src=\"./assets/desktop.png\" alt=\"Browser Use Desktop App\" width=\"full\"/>\n\n<br/>\n\n[;\n"
},
{
"path": "vite.preload.config.ts",
"chars": 100,
"preview": "import { defineConfig } from 'vite';\n\n// https://vitejs.dev/config\nexport default defineConfig({});\n"
},
{
"path": "vite.renderer.config.ts",
"chars": 100,
"preview": "import { defineConfig } from 'vite';\n\n// https://vitejs.dev/config\nexport default defineConfig({});\n"
}
]
About this extraction
This page contains the full source code of the browser-use/desktop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (53.5 KB), approximately 13.7k tokens, and a symbol index with 40 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.