optexity-browser-use 0.9.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. browser_use/__init__.py +157 -0
  2. browser_use/actor/__init__.py +11 -0
  3. browser_use/actor/element.py +1175 -0
  4. browser_use/actor/mouse.py +134 -0
  5. browser_use/actor/page.py +561 -0
  6. browser_use/actor/playground/flights.py +41 -0
  7. browser_use/actor/playground/mixed_automation.py +54 -0
  8. browser_use/actor/playground/playground.py +236 -0
  9. browser_use/actor/utils.py +176 -0
  10. browser_use/agent/cloud_events.py +282 -0
  11. browser_use/agent/gif.py +424 -0
  12. browser_use/agent/judge.py +170 -0
  13. browser_use/agent/message_manager/service.py +473 -0
  14. browser_use/agent/message_manager/utils.py +52 -0
  15. browser_use/agent/message_manager/views.py +98 -0
  16. browser_use/agent/prompts.py +413 -0
  17. browser_use/agent/service.py +2316 -0
  18. browser_use/agent/system_prompt.md +185 -0
  19. browser_use/agent/system_prompt_flash.md +10 -0
  20. browser_use/agent/system_prompt_no_thinking.md +183 -0
  21. browser_use/agent/views.py +743 -0
  22. browser_use/browser/__init__.py +41 -0
  23. browser_use/browser/cloud/cloud.py +203 -0
  24. browser_use/browser/cloud/views.py +89 -0
  25. browser_use/browser/events.py +578 -0
  26. browser_use/browser/profile.py +1158 -0
  27. browser_use/browser/python_highlights.py +548 -0
  28. browser_use/browser/session.py +3225 -0
  29. browser_use/browser/session_manager.py +399 -0
  30. browser_use/browser/video_recorder.py +162 -0
  31. browser_use/browser/views.py +200 -0
  32. browser_use/browser/watchdog_base.py +260 -0
  33. browser_use/browser/watchdogs/__init__.py +0 -0
  34. browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
  35. browser_use/browser/watchdogs/crash_watchdog.py +335 -0
  36. browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
  37. browser_use/browser/watchdogs/dom_watchdog.py +817 -0
  38. browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
  39. browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
  40. browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
  41. browser_use/browser/watchdogs/popups_watchdog.py +143 -0
  42. browser_use/browser/watchdogs/recording_watchdog.py +126 -0
  43. browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
  44. browser_use/browser/watchdogs/security_watchdog.py +280 -0
  45. browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
  46. browser_use/cli.py +2359 -0
  47. browser_use/code_use/__init__.py +16 -0
  48. browser_use/code_use/formatting.py +192 -0
  49. browser_use/code_use/namespace.py +665 -0
  50. browser_use/code_use/notebook_export.py +276 -0
  51. browser_use/code_use/service.py +1340 -0
  52. browser_use/code_use/system_prompt.md +574 -0
  53. browser_use/code_use/utils.py +150 -0
  54. browser_use/code_use/views.py +171 -0
  55. browser_use/config.py +505 -0
  56. browser_use/controller/__init__.py +3 -0
  57. browser_use/dom/enhanced_snapshot.py +161 -0
  58. browser_use/dom/markdown_extractor.py +169 -0
  59. browser_use/dom/playground/extraction.py +312 -0
  60. browser_use/dom/playground/multi_act.py +32 -0
  61. browser_use/dom/serializer/clickable_elements.py +200 -0
  62. browser_use/dom/serializer/code_use_serializer.py +287 -0
  63. browser_use/dom/serializer/eval_serializer.py +478 -0
  64. browser_use/dom/serializer/html_serializer.py +212 -0
  65. browser_use/dom/serializer/paint_order.py +197 -0
  66. browser_use/dom/serializer/serializer.py +1170 -0
  67. browser_use/dom/service.py +825 -0
  68. browser_use/dom/utils.py +129 -0
  69. browser_use/dom/views.py +906 -0
  70. browser_use/exceptions.py +5 -0
  71. browser_use/filesystem/__init__.py +0 -0
  72. browser_use/filesystem/file_system.py +619 -0
  73. browser_use/init_cmd.py +376 -0
  74. browser_use/integrations/gmail/__init__.py +24 -0
  75. browser_use/integrations/gmail/actions.py +115 -0
  76. browser_use/integrations/gmail/service.py +225 -0
  77. browser_use/llm/__init__.py +155 -0
  78. browser_use/llm/anthropic/chat.py +242 -0
  79. browser_use/llm/anthropic/serializer.py +312 -0
  80. browser_use/llm/aws/__init__.py +36 -0
  81. browser_use/llm/aws/chat_anthropic.py +242 -0
  82. browser_use/llm/aws/chat_bedrock.py +289 -0
  83. browser_use/llm/aws/serializer.py +257 -0
  84. browser_use/llm/azure/chat.py +91 -0
  85. browser_use/llm/base.py +57 -0
  86. browser_use/llm/browser_use/__init__.py +3 -0
  87. browser_use/llm/browser_use/chat.py +201 -0
  88. browser_use/llm/cerebras/chat.py +193 -0
  89. browser_use/llm/cerebras/serializer.py +109 -0
  90. browser_use/llm/deepseek/chat.py +212 -0
  91. browser_use/llm/deepseek/serializer.py +109 -0
  92. browser_use/llm/exceptions.py +29 -0
  93. browser_use/llm/google/__init__.py +3 -0
  94. browser_use/llm/google/chat.py +542 -0
  95. browser_use/llm/google/serializer.py +120 -0
  96. browser_use/llm/groq/chat.py +229 -0
  97. browser_use/llm/groq/parser.py +158 -0
  98. browser_use/llm/groq/serializer.py +159 -0
  99. browser_use/llm/messages.py +238 -0
  100. browser_use/llm/models.py +271 -0
  101. browser_use/llm/oci_raw/__init__.py +10 -0
  102. browser_use/llm/oci_raw/chat.py +443 -0
  103. browser_use/llm/oci_raw/serializer.py +229 -0
  104. browser_use/llm/ollama/chat.py +97 -0
  105. browser_use/llm/ollama/serializer.py +143 -0
  106. browser_use/llm/openai/chat.py +264 -0
  107. browser_use/llm/openai/like.py +15 -0
  108. browser_use/llm/openai/serializer.py +165 -0
  109. browser_use/llm/openrouter/chat.py +211 -0
  110. browser_use/llm/openrouter/serializer.py +26 -0
  111. browser_use/llm/schema.py +176 -0
  112. browser_use/llm/views.py +48 -0
  113. browser_use/logging_config.py +330 -0
  114. browser_use/mcp/__init__.py +18 -0
  115. browser_use/mcp/__main__.py +12 -0
  116. browser_use/mcp/client.py +544 -0
  117. browser_use/mcp/controller.py +264 -0
  118. browser_use/mcp/server.py +1114 -0
  119. browser_use/observability.py +204 -0
  120. browser_use/py.typed +0 -0
  121. browser_use/sandbox/__init__.py +41 -0
  122. browser_use/sandbox/sandbox.py +637 -0
  123. browser_use/sandbox/views.py +132 -0
  124. browser_use/screenshots/__init__.py +1 -0
  125. browser_use/screenshots/service.py +52 -0
  126. browser_use/sync/__init__.py +6 -0
  127. browser_use/sync/auth.py +357 -0
  128. browser_use/sync/service.py +161 -0
  129. browser_use/telemetry/__init__.py +51 -0
  130. browser_use/telemetry/service.py +112 -0
  131. browser_use/telemetry/views.py +101 -0
  132. browser_use/tokens/__init__.py +0 -0
  133. browser_use/tokens/custom_pricing.py +24 -0
  134. browser_use/tokens/mappings.py +4 -0
  135. browser_use/tokens/service.py +580 -0
  136. browser_use/tokens/views.py +108 -0
  137. browser_use/tools/registry/service.py +572 -0
  138. browser_use/tools/registry/views.py +174 -0
  139. browser_use/tools/service.py +1675 -0
  140. browser_use/tools/utils.py +82 -0
  141. browser_use/tools/views.py +100 -0
  142. browser_use/utils.py +670 -0
  143. optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
  144. optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
  145. optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
  146. optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
  147. optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
browser_use/cli.py ADDED
@@ -0,0 +1,2359 @@
1
+ # pyright: reportMissingImports=false
2
+
3
+ # Check for MCP mode early to prevent logging initialization
4
+ import sys
5
+
6
+ if '--mcp' in sys.argv:
7
+ import logging
8
+ import os
9
+
10
+ os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'critical'
11
+ os.environ['BROWSER_USE_SETUP_LOGGING'] = 'false'
12
+ logging.disable(logging.CRITICAL)
13
+
14
+ # Special case: install command doesn't need CLI dependencies
15
+ if len(sys.argv) > 1 and sys.argv[1] == 'install':
16
+ import platform
17
+ import subprocess
18
+
19
+ print('📦 Installing Chromium browser + system dependencies...')
20
+ print('⏳ This may take a few minutes...\n')
21
+
22
+ # Build command - only use --with-deps on Linux (it fails on Windows/macOS)
23
+ cmd = ['uvx', 'playwright', 'install', 'chromium']
24
+ if platform.system() == 'Linux':
25
+ cmd.append('--with-deps')
26
+ cmd.append('--no-shell')
27
+
28
+ result = subprocess.run(cmd)
29
+
30
+ if result.returncode == 0:
31
+ print('\n✅ Installation complete!')
32
+ print('🚀 Ready to use! Run: uvx browser-use')
33
+ else:
34
+ print('\n❌ Installation failed')
35
+ sys.exit(1)
36
+ sys.exit(0)
37
+
38
+ # Check for init subcommand early to avoid loading TUI dependencies
39
+ if 'init' in sys.argv:
40
+ from browser_use.init_cmd import INIT_TEMPLATES
41
+ from browser_use.init_cmd import main as init_main
42
+
43
+ # Check if --template or -t flag is present without a value
44
+ # If so, just remove it and let init_main handle interactive mode
45
+ if '--template' in sys.argv or '-t' in sys.argv:
46
+ try:
47
+ template_idx = sys.argv.index('--template') if '--template' in sys.argv else sys.argv.index('-t')
48
+ template = sys.argv[template_idx + 1] if template_idx + 1 < len(sys.argv) else None
49
+
50
+ # If template is not provided or is another flag, remove the flag and use interactive mode
51
+ if not template or template.startswith('-'):
52
+ if '--template' in sys.argv:
53
+ sys.argv.remove('--template')
54
+ else:
55
+ sys.argv.remove('-t')
56
+ except (ValueError, IndexError):
57
+ pass
58
+
59
+ # Remove 'init' from sys.argv so click doesn't see it as an unexpected argument
60
+ sys.argv.remove('init')
61
+ init_main()
62
+ sys.exit(0)
63
+
64
+ # Check for --template flag early to avoid loading TUI dependencies
65
+ if '--template' in sys.argv:
66
+ from pathlib import Path
67
+
68
+ import click
69
+
70
+ from browser_use.init_cmd import INIT_TEMPLATES
71
+
72
+ # Parse template and output from sys.argv
73
+ try:
74
+ template_idx = sys.argv.index('--template')
75
+ template = sys.argv[template_idx + 1] if template_idx + 1 < len(sys.argv) else None
76
+ except (ValueError, IndexError):
77
+ template = None
78
+
79
+ # If template is not provided or is another flag, use interactive mode
80
+ if not template or template.startswith('-'):
81
+ # Redirect to init command with interactive template selection
82
+ from browser_use.init_cmd import main as init_main
83
+
84
+ # Remove --template from sys.argv
85
+ sys.argv.remove('--template')
86
+ init_main()
87
+ sys.exit(0)
88
+
89
+ # Validate template name
90
+ if template not in INIT_TEMPLATES:
91
+ click.echo(f'❌ Invalid template. Choose from: {", ".join(INIT_TEMPLATES.keys())}', err=True)
92
+ sys.exit(1)
93
+
94
+ # Check for --output flag
95
+ output = None
96
+ if '--output' in sys.argv or '-o' in sys.argv:
97
+ try:
98
+ output_idx = sys.argv.index('--output') if '--output' in sys.argv else sys.argv.index('-o')
99
+ output = sys.argv[output_idx + 1] if output_idx + 1 < len(sys.argv) else None
100
+ except (ValueError, IndexError):
101
+ pass
102
+
103
+ # Check for --force flag
104
+ force = '--force' in sys.argv or '-f' in sys.argv
105
+
106
+ # Determine output path
107
+ output_path = Path(output) if output else Path.cwd() / f'browser_use_{template}.py'
108
+
109
+ # Read and write template
110
+ try:
111
+ templates_dir = Path(__file__).parent / 'cli_templates'
112
+ template_file = INIT_TEMPLATES[template]['file']
113
+ template_path = templates_dir / template_file
114
+ content = template_path.read_text(encoding='utf-8')
115
+
116
+ # Write file with safety checks
117
+ if output_path.exists() and not force:
118
+ click.echo(f'⚠️ File already exists: {output_path}')
119
+ if not click.confirm('Overwrite?', default=False):
120
+ click.echo('❌ Cancelled')
121
+ sys.exit(1)
122
+
123
+ output_path.parent.mkdir(parents=True, exist_ok=True)
124
+ output_path.write_text(content, encoding='utf-8')
125
+
126
+ click.echo(f'✅ Created {output_path}')
127
+ click.echo('\nNext steps:')
128
+ click.echo(' 1. Install browser-use:')
129
+ click.echo(' uv pip install browser-use')
130
+ click.echo(' 2. Set up your API key in .env file or environment:')
131
+ click.echo(' BROWSER_USE_API_KEY=your-key')
132
+ click.echo(' (Get your key at https://cloud.browser-use.com/new-api-key)')
133
+ click.echo(' 3. Run your script:')
134
+ click.echo(f' python {output_path.name}')
135
+ except Exception as e:
136
+ click.echo(f'❌ Error: {e}', err=True)
137
+ sys.exit(1)
138
+
139
+ sys.exit(0)
140
+
141
+ import asyncio
142
+ import json
143
+ import logging
144
+ import os
145
+ import time
146
+ from pathlib import Path
147
+ from typing import Any
148
+
149
+ from dotenv import load_dotenv
150
+
151
+ from browser_use.llm.anthropic.chat import ChatAnthropic
152
+ from browser_use.llm.google.chat import ChatGoogle
153
+ from browser_use.llm.openai.chat import ChatOpenAI
154
+
155
+ load_dotenv()
156
+
157
+ from browser_use import Agent, Controller
158
+ from browser_use.agent.views import AgentSettings
159
+ from browser_use.browser import BrowserProfile, BrowserSession
160
+ from browser_use.logging_config import addLoggingLevel
161
+ from browser_use.telemetry import CLITelemetryEvent, ProductTelemetry
162
+ from browser_use.utils import get_browser_use_version
163
+
164
+ try:
165
+ import click
166
+ from textual import events
167
+ from textual.app import App, ComposeResult
168
+ from textual.binding import Binding
169
+ from textual.containers import Container, HorizontalGroup, VerticalScroll
170
+ from textual.widgets import Footer, Header, Input, Label, Link, RichLog, Static
171
+ except ImportError:
172
+ print('⚠️ CLI addon is not installed. Please install it with: `pip install "browser-use[cli]"` and try again.')
173
+ sys.exit(1)
174
+
175
+
176
+ try:
177
+ import readline
178
+
179
+ READLINE_AVAILABLE = True
180
+ except ImportError:
181
+ # readline not available on Windows by default
182
+ READLINE_AVAILABLE = False
183
+
184
+
185
+ os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'result'
186
+
187
+ from browser_use.config import CONFIG
188
+
189
+ # Set USER_DATA_DIR now that CONFIG is imported
190
+ USER_DATA_DIR = CONFIG.BROWSER_USE_PROFILES_DIR / 'cli'
191
+
192
+ # Ensure directories exist
193
+ CONFIG.BROWSER_USE_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
194
+ USER_DATA_DIR.mkdir(parents=True, exist_ok=True)
195
+
196
+ # Default User settings
197
+ MAX_HISTORY_LENGTH = 100
198
+
199
+ # Directory setup will happen in functions that need CONFIG
200
+
201
+
202
+ # Logo components with styling for rich panels
203
+ BROWSER_LOGO = """
204
+ [white] ++++++ +++++++++ [/]
205
+ [white] +++ +++++ +++ [/]
206
+ [white] ++ ++++ ++ ++ [/]
207
+ [white] ++ +++ +++ ++ [/]
208
+ [white] ++++ +++ [/]
209
+ [white] +++ +++ [/]
210
+ [white] +++ +++ [/]
211
+ [white] ++ +++ +++ ++ [/]
212
+ [white] ++ ++++ ++ ++ [/]
213
+ [white] +++ ++++++ +++ [/]
214
+ [white] ++++++ +++++++ [/]
215
+
216
+ [white]██████╗ ██████╗ ██████╗ ██╗ ██╗███████╗███████╗██████╗[/] [darkorange]██╗ ██╗███████╗███████╗[/]
217
+ [white]██╔══██╗██╔══██╗██╔═══██╗██║ ██║██╔════╝██╔════╝██╔══██╗[/] [darkorange]██║ ██║██╔════╝██╔════╝[/]
218
+ [white]██████╔╝██████╔╝██║ ██║██║ █╗ ██║███████╗█████╗ ██████╔╝[/] [darkorange]██║ ██║███████╗█████╗[/]
219
+ [white]██╔══██╗██╔══██╗██║ ██║██║███╗██║╚════██║██╔══╝ ██╔══██╗[/] [darkorange]██║ ██║╚════██║██╔══╝[/]
220
+ [white]██████╔╝██║ ██║╚██████╔╝╚███╔███╔╝███████║███████╗██║ ██║[/] [darkorange]╚██████╔╝███████║███████╗[/]
221
+ [white]╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝╚══════╝╚═╝ ╚═╝[/] [darkorange]╚═════╝ ╚══════╝╚══════╝[/]
222
+ """
223
+
224
+
225
+ # Common UI constants
226
+ TEXTUAL_BORDER_STYLES = {'logo': 'blue', 'info': 'blue', 'input': 'orange3', 'working': 'yellow', 'completion': 'green'}
227
+
228
+
229
+ def get_default_config() -> dict[str, Any]:
230
+ """Return default configuration dictionary using the new config system."""
231
+ # Load config from the new config system
232
+ config_data = CONFIG.load_config()
233
+
234
+ # Extract browser profile, llm, and agent configs
235
+ browser_profile = config_data.get('browser_profile', {})
236
+ llm_config = config_data.get('llm', {})
237
+ agent_config = config_data.get('agent', {})
238
+
239
+ return {
240
+ 'model': {
241
+ 'name': llm_config.get('model'),
242
+ 'temperature': llm_config.get('temperature', 0.0),
243
+ 'api_keys': {
244
+ 'OPENAI_API_KEY': llm_config.get('api_key', CONFIG.OPENAI_API_KEY),
245
+ 'ANTHROPIC_API_KEY': CONFIG.ANTHROPIC_API_KEY,
246
+ 'GOOGLE_API_KEY': CONFIG.GOOGLE_API_KEY,
247
+ 'DEEPSEEK_API_KEY': CONFIG.DEEPSEEK_API_KEY,
248
+ 'GROK_API_KEY': CONFIG.GROK_API_KEY,
249
+ },
250
+ },
251
+ 'agent': agent_config,
252
+ 'browser': {
253
+ 'headless': browser_profile.get('headless', True),
254
+ 'keep_alive': browser_profile.get('keep_alive', True),
255
+ 'ignore_https_errors': browser_profile.get('ignore_https_errors', False),
256
+ 'user_data_dir': browser_profile.get('user_data_dir'),
257
+ 'allowed_domains': browser_profile.get('allowed_domains'),
258
+ 'wait_between_actions': browser_profile.get('wait_between_actions'),
259
+ 'is_mobile': browser_profile.get('is_mobile'),
260
+ 'device_scale_factor': browser_profile.get('device_scale_factor'),
261
+ 'disable_security': browser_profile.get('disable_security'),
262
+ },
263
+ 'command_history': [],
264
+ }
265
+
266
+
267
+ def load_user_config() -> dict[str, Any]:
268
+ """Load user configuration using the new config system."""
269
+ # Just get the default config which already loads from the new system
270
+ config = get_default_config()
271
+
272
+ # Load command history from a separate file if it exists
273
+ history_file = CONFIG.BROWSER_USE_CONFIG_DIR / 'command_history.json'
274
+ if history_file.exists():
275
+ try:
276
+ with open(history_file) as f:
277
+ config['command_history'] = json.load(f)
278
+ except (FileNotFoundError, json.JSONDecodeError):
279
+ config['command_history'] = []
280
+
281
+ return config
282
+
283
+
284
+ def save_user_config(config: dict[str, Any]) -> None:
285
+ """Save command history only (config is saved via the new system)."""
286
+ # Only save command history to a separate file
287
+ if 'command_history' in config and isinstance(config['command_history'], list):
288
+ # Ensure command history doesn't exceed maximum length
289
+ history = config['command_history']
290
+ if len(history) > MAX_HISTORY_LENGTH:
291
+ history = history[-MAX_HISTORY_LENGTH:]
292
+
293
+ # Save to separate history file
294
+ history_file = CONFIG.BROWSER_USE_CONFIG_DIR / 'command_history.json'
295
+ with open(history_file, 'w') as f:
296
+ json.dump(history, f, indent=2)
297
+
298
+
299
+ def update_config_with_click_args(config: dict[str, Any], ctx: click.Context) -> dict[str, Any]:
300
+ """Update configuration with command-line arguments."""
301
+ # Ensure required sections exist
302
+ if 'model' not in config:
303
+ config['model'] = {}
304
+ if 'browser' not in config:
305
+ config['browser'] = {}
306
+
307
+ # Update configuration with command-line args if provided
308
+ if ctx.params.get('model'):
309
+ config['model']['name'] = ctx.params['model']
310
+ if ctx.params.get('headless') is not None:
311
+ config['browser']['headless'] = ctx.params['headless']
312
+ if ctx.params.get('window_width'):
313
+ config['browser']['window_width'] = ctx.params['window_width']
314
+ if ctx.params.get('window_height'):
315
+ config['browser']['window_height'] = ctx.params['window_height']
316
+ if ctx.params.get('user_data_dir'):
317
+ config['browser']['user_data_dir'] = ctx.params['user_data_dir']
318
+ if ctx.params.get('profile_directory'):
319
+ config['browser']['profile_directory'] = ctx.params['profile_directory']
320
+ if ctx.params.get('cdp_url'):
321
+ config['browser']['cdp_url'] = ctx.params['cdp_url']
322
+
323
+ # Consolidated proxy dict
324
+ proxy: dict[str, str] = {}
325
+ if ctx.params.get('proxy_url'):
326
+ proxy['server'] = ctx.params['proxy_url']
327
+ if ctx.params.get('no_proxy'):
328
+ # Store as comma-separated list string to match Chrome flag
329
+ proxy['bypass'] = ','.join([p.strip() for p in ctx.params['no_proxy'].split(',') if p.strip()])
330
+ if ctx.params.get('proxy_username'):
331
+ proxy['username'] = ctx.params['proxy_username']
332
+ if ctx.params.get('proxy_password'):
333
+ proxy['password'] = ctx.params['proxy_password']
334
+ if proxy:
335
+ config['browser']['proxy'] = proxy
336
+
337
+ return config
338
+
339
+
340
+ def setup_readline_history(history: list[str]) -> None:
341
+ """Set up readline with command history."""
342
+ if not READLINE_AVAILABLE:
343
+ return
344
+
345
+ # Add history items to readline
346
+ for item in history:
347
+ readline.add_history(item)
348
+
349
+
350
+ def get_llm(config: dict[str, Any]):
351
+ """Get the language model based on config and available API keys."""
352
+ model_config = config.get('model', {})
353
+ model_name = model_config.get('name')
354
+ temperature = model_config.get('temperature', 0.0)
355
+
356
+ # Get API key from config or environment
357
+ api_key = model_config.get('api_keys', {}).get('OPENAI_API_KEY') or CONFIG.OPENAI_API_KEY
358
+
359
+ if model_name:
360
+ if model_name.startswith('gpt'):
361
+ if not api_key and not CONFIG.OPENAI_API_KEY:
362
+ print('⚠️ OpenAI API key not found. Please update your config or set OPENAI_API_KEY environment variable.')
363
+ sys.exit(1)
364
+ return ChatOpenAI(model=model_name, temperature=temperature, api_key=api_key or CONFIG.OPENAI_API_KEY)
365
+ elif model_name.startswith('claude'):
366
+ if not CONFIG.ANTHROPIC_API_KEY:
367
+ print('⚠️ Anthropic API key not found. Please update your config or set ANTHROPIC_API_KEY environment variable.')
368
+ sys.exit(1)
369
+ return ChatAnthropic(model=model_name, temperature=temperature)
370
+ elif model_name.startswith('gemini'):
371
+ if not CONFIG.GOOGLE_API_KEY:
372
+ print('⚠️ Google API key not found. Please update your config or set GOOGLE_API_KEY environment variable.')
373
+ sys.exit(1)
374
+ return ChatGoogle(model=model_name, temperature=temperature)
375
+ elif model_name.startswith('oci'):
376
+ # OCI models require additional configuration
377
+ print(
378
+ '⚠️ OCI models require manual configuration. Please use the ChatOCIRaw class directly with your OCI credentials.'
379
+ )
380
+ sys.exit(1)
381
+
382
+ # Auto-detect based on available API keys
383
+ if api_key or CONFIG.OPENAI_API_KEY:
384
+ return ChatOpenAI(model='gpt-5-mini', temperature=temperature, api_key=api_key or CONFIG.OPENAI_API_KEY)
385
+ elif CONFIG.ANTHROPIC_API_KEY:
386
+ return ChatAnthropic(model='claude-4-sonnet', temperature=temperature)
387
+ elif CONFIG.GOOGLE_API_KEY:
388
+ return ChatGoogle(model='gemini-2.5-pro', temperature=temperature)
389
+ else:
390
+ print(
391
+ '⚠️ No API keys found. Please update your config or set one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_API_KEY.'
392
+ )
393
+ sys.exit(1)
394
+
395
+
396
+ class RichLogHandler(logging.Handler):
397
+ """Custom logging handler that redirects logs to a RichLog widget."""
398
+
399
+ def __init__(self, rich_log: RichLog):
400
+ super().__init__()
401
+ self.rich_log = rich_log
402
+
403
+ def emit(self, record):
404
+ try:
405
+ msg = self.format(record)
406
+ self.rich_log.write(msg)
407
+ except Exception:
408
+ self.handleError(record)
409
+
410
+
411
+ class BrowserUseApp(App):
412
+ """Browser-use TUI application."""
413
+
414
+ # Make it an inline app instead of fullscreen
415
+ # MODES = {"light"} # Ensure app is inline, not fullscreen
416
+
417
+ CSS = """
418
+ #main-container {
419
+ height: 100%;
420
+ layout: vertical;
421
+ }
422
+
423
+ #logo-panel, #links-panel, #paths-panel, #info-panels {
424
+ border: solid $primary;
425
+ margin: 0 0 0 0;
426
+ padding: 0;
427
+ }
428
+
429
+ #info-panels {
430
+ display: none;
431
+ layout: vertical;
432
+ height: auto;
433
+ min-height: 5;
434
+ margin: 0 0 1 0;
435
+ }
436
+
437
+ #top-panels {
438
+ layout: horizontal;
439
+ height: auto;
440
+ width: 100%;
441
+ }
442
+
443
+ #browser-panel, #model-panel {
444
+ width: 1fr;
445
+ height: 100%;
446
+ padding: 1;
447
+ border-right: solid $primary;
448
+ }
449
+
450
+ #model-panel {
451
+ border-right: none;
452
+ }
453
+
454
+ #tasks-panel {
455
+ height: auto;
456
+ max-height: 10;
457
+ overflow-y: scroll;
458
+ padding: 1;
459
+ border-top: solid $primary;
460
+ }
461
+
462
+ #browser-info, #model-info, #tasks-info {
463
+ height: auto;
464
+ margin: 0;
465
+ padding: 0;
466
+ background: transparent;
467
+ overflow-y: auto;
468
+ min-height: 3;
469
+ }
470
+
471
+ #three-column-container {
472
+ height: 1fr;
473
+ layout: horizontal;
474
+ width: 100%;
475
+ display: none;
476
+ }
477
+
478
+ #main-output-column {
479
+ width: 1fr;
480
+ height: 100%;
481
+ border: solid $primary;
482
+ padding: 0;
483
+ margin: 0 1 0 0;
484
+ }
485
+
486
+ #events-column {
487
+ width: 1fr;
488
+ height: 100%;
489
+ border: solid $warning;
490
+ padding: 0;
491
+ margin: 0 1 0 0;
492
+ }
493
+
494
+ #cdp-column {
495
+ width: 1fr;
496
+ height: 100%;
497
+ border: solid $accent;
498
+ padding: 0;
499
+ margin: 0;
500
+ }
501
+
502
+ #main-output-log, #events-log, #cdp-log {
503
+ height: 100%;
504
+ overflow-y: scroll;
505
+ background: $surface;
506
+ color: $text;
507
+ width: 100%;
508
+ padding: 1;
509
+ }
510
+
511
+ #events-log {
512
+ color: $warning;
513
+ }
514
+
515
+ #cdp-log {
516
+ color: $accent-lighten-2;
517
+ }
518
+
519
+ #logo-panel {
520
+ width: 100%;
521
+ height: auto;
522
+ content-align: center middle;
523
+ text-align: center;
524
+ }
525
+
526
+ #links-panel {
527
+ width: 100%;
528
+ padding: 1;
529
+ border: solid $primary;
530
+ height: auto;
531
+ }
532
+
533
+ .link-white {
534
+ color: white;
535
+ }
536
+
537
+ .link-purple {
538
+ color: purple;
539
+ }
540
+
541
+ .link-magenta {
542
+ color: magenta;
543
+ }
544
+
545
+ .link-green {
546
+ color: green;
547
+ }
548
+
549
+ HorizontalGroup {
550
+ height: auto;
551
+ }
552
+
553
+ .link-label {
554
+ width: auto;
555
+ }
556
+
557
+ .link-url {
558
+ width: auto;
559
+ }
560
+
561
+ .link-row {
562
+ width: 100%;
563
+ height: auto;
564
+ }
565
+
566
+ #paths-panel {
567
+ color: $text-muted;
568
+ }
569
+
570
+ #task-input-container {
571
+ border: solid $accent;
572
+ padding: 1;
573
+ margin-bottom: 1;
574
+ height: auto;
575
+ dock: bottom;
576
+ }
577
+
578
+ #task-label {
579
+ color: $accent;
580
+ padding-bottom: 1;
581
+ }
582
+
583
+ #task-input {
584
+ width: 100%;
585
+ }
586
+ """
587
+
588
+ BINDINGS = [
589
+ Binding('ctrl+c', 'quit', 'Quit', priority=True, show=True),
590
+ Binding('ctrl+q', 'quit', 'Quit', priority=True),
591
+ Binding('ctrl+d', 'quit', 'Quit', priority=True),
592
+ Binding('up', 'input_history_prev', 'Previous command', show=False),
593
+ Binding('down', 'input_history_next', 'Next command', show=False),
594
+ ]
595
+
596
+ def __init__(self, config: dict[str, Any], *args, **kwargs):
597
+ super().__init__(*args, **kwargs)
598
+ self.config = config
599
+ self.browser_session: BrowserSession | None = None # Will be set before app.run_async()
600
+ self.controller: Controller | None = None # Will be set before app.run_async()
601
+ self.agent: Agent | None = None
602
+ self.llm: Any | None = None # Will be set before app.run_async()
603
+ self.task_history = config.get('command_history', [])
604
+ # Track current position in history for up/down navigation
605
+ self.history_index = len(self.task_history)
606
+ # Initialize telemetry
607
+ self._telemetry = ProductTelemetry()
608
+ # Store for event bus handler
609
+ self._event_bus_handler_id = None
610
+ self._event_bus_handler_func = None
611
+ # Timer for info panel updates
612
+ self._info_panel_timer = None
613
+
614
+ def setup_richlog_logging(self) -> None:
615
+ """Set up logging to redirect to RichLog widget instead of stdout."""
616
+ # Try to add RESULT level if it doesn't exist
617
+ try:
618
+ addLoggingLevel('RESULT', 35)
619
+ except AttributeError:
620
+ pass # Level already exists, which is fine
621
+
622
+ # Get the main output RichLog widget
623
+ rich_log = self.query_one('#main-output-log', RichLog)
624
+
625
+ # Create and set up the custom handler
626
+ log_handler = RichLogHandler(rich_log)
627
+ log_type = os.getenv('BROWSER_USE_LOGGING_LEVEL', 'result').lower()
628
+
629
+ class BrowserUseFormatter(logging.Formatter):
630
+ def format(self, record):
631
+ # if isinstance(record.name, str) and record.name.startswith('browser_use.'):
632
+ # record.name = record.name.split('.')[-2]
633
+ return super().format(record)
634
+
635
+ # Set up the formatter based on log type
636
+ if log_type == 'result':
637
+ log_handler.setLevel('RESULT')
638
+ log_handler.setFormatter(BrowserUseFormatter('%(message)s'))
639
+ else:
640
+ log_handler.setFormatter(BrowserUseFormatter('%(levelname)-8s [%(name)s] %(message)s'))
641
+
642
+ # Configure root logger - Replace ALL handlers, not just stdout handlers
643
+ root = logging.getLogger()
644
+
645
+ # Clear all existing handlers to prevent output to stdout/stderr
646
+ root.handlers = []
647
+ root.addHandler(log_handler)
648
+
649
+ # Set log level based on environment variable
650
+ if log_type == 'result':
651
+ root.setLevel('RESULT')
652
+ elif log_type == 'debug':
653
+ root.setLevel(logging.DEBUG)
654
+ else:
655
+ root.setLevel(logging.INFO)
656
+
657
+ # Configure browser_use logger and all its sub-loggers
658
+ browser_use_logger = logging.getLogger('browser_use')
659
+ browser_use_logger.propagate = False # Don't propagate to root logger
660
+ browser_use_logger.handlers = [log_handler] # Replace any existing handlers
661
+ browser_use_logger.setLevel(root.level)
662
+
663
+ # Also ensure agent loggers go to the main output
664
+ # Use a wildcard pattern to catch all agent-related loggers
665
+ for logger_name in ['browser_use.Agent', 'browser_use.controller', 'browser_use.agent', 'browser_use.agent.service']:
666
+ agent_logger = logging.getLogger(logger_name)
667
+ agent_logger.propagate = False
668
+ agent_logger.handlers = [log_handler]
669
+ agent_logger.setLevel(root.level)
670
+
671
+ # Also catch any dynamically created agent loggers with task IDs
672
+ for name, logger in logging.Logger.manager.loggerDict.items():
673
+ if isinstance(name, str) and 'browser_use.Agent' in name:
674
+ if isinstance(logger, logging.Logger):
675
+ logger.propagate = False
676
+ logger.handlers = [log_handler]
677
+ logger.setLevel(root.level)
678
+
679
+ # Silence third-party loggers but keep them using our handler
680
+ for logger_name in [
681
+ 'WDM',
682
+ 'httpx',
683
+ 'selenium',
684
+ 'playwright',
685
+ 'urllib3',
686
+ 'asyncio',
687
+ 'openai',
688
+ 'httpcore',
689
+ 'charset_normalizer',
690
+ 'anthropic._base_client',
691
+ 'PIL.PngImagePlugin',
692
+ 'trafilatura.htmlprocessing',
693
+ 'trafilatura',
694
+ 'groq',
695
+ 'portalocker',
696
+ 'portalocker.utils',
697
+ ]:
698
+ third_party = logging.getLogger(logger_name)
699
+ third_party.setLevel(logging.ERROR)
700
+ third_party.propagate = False
701
+ third_party.handlers = [log_handler] # Use our handler to prevent stdout/stderr leakage
702
+
703
+ def on_mount(self) -> None:
704
+ """Set up components when app is mounted."""
705
+ # We'll use a file logger since stdout is now controlled by Textual
706
+ logger = logging.getLogger('browser_use.on_mount')
707
+ logger.debug('on_mount() method started')
708
+
709
+ # Step 1: Set up custom logging to RichLog
710
+ logger.debug('Setting up RichLog logging...')
711
+ try:
712
+ self.setup_richlog_logging()
713
+ logger.debug('RichLog logging set up successfully')
714
+ except Exception as e:
715
+ logger.error(f'Error setting up RichLog logging: {str(e)}', exc_info=True)
716
+ raise RuntimeError(f'Failed to set up RichLog logging: {str(e)}')
717
+
718
+ # Step 2: Set up input history
719
+ logger.debug('Setting up readline history...')
720
+ try:
721
+ if READLINE_AVAILABLE and self.task_history:
722
+ for item in self.task_history:
723
+ readline.add_history(item)
724
+ logger.debug(f'Added {len(self.task_history)} items to readline history')
725
+ else:
726
+ logger.debug('No readline history to set up')
727
+ except Exception as e:
728
+ logger.error(f'Error setting up readline history: {str(e)}', exc_info=False)
729
+ # Non-critical, continue
730
+
731
+ # Step 3: Focus the input field
732
+ logger.debug('Focusing input field...')
733
+ try:
734
+ input_field = self.query_one('#task-input', Input)
735
+ input_field.focus()
736
+ logger.debug('Input field focused')
737
+ except Exception as e:
738
+ logger.error(f'Error focusing input field: {str(e)}', exc_info=True)
739
+ # Non-critical, continue
740
+
741
+ # Step 5: Setup CDP logger and event bus listener if browser session is available
742
+ logger.debug('Setting up CDP logging and event bus listener...')
743
+ try:
744
+ self.setup_cdp_logger()
745
+ if self.browser_session:
746
+ self.setup_event_bus_listener()
747
+ logger.debug('CDP logging and event bus setup complete')
748
+ except Exception as e:
749
+ logger.error(f'Error setting up CDP logging/event bus: {str(e)}', exc_info=True)
750
+ # Non-critical, continue
751
+
752
+ # Capture telemetry for CLI start
753
+ self._telemetry.capture(
754
+ CLITelemetryEvent(
755
+ version=get_browser_use_version(),
756
+ action='start',
757
+ mode='interactive',
758
+ model=self.llm.model if self.llm and hasattr(self.llm, 'model') else None,
759
+ model_provider=self.llm.provider if self.llm and hasattr(self.llm, 'provider') else None,
760
+ )
761
+ )
762
+
763
+ logger.debug('on_mount() completed successfully')
764
+
765
+ def on_input_key_up(self, event: events.Key) -> None:
766
+ """Handle up arrow key in the input field."""
767
+ # For textual key events, we need to check focus manually
768
+ input_field = self.query_one('#task-input', Input)
769
+ if not input_field.has_focus:
770
+ return
771
+
772
+ # Only process if we have history
773
+ if not self.task_history:
774
+ return
775
+
776
+ # Move back in history if possible
777
+ if self.history_index > 0:
778
+ self.history_index -= 1
779
+ task_input = self.query_one('#task-input', Input)
780
+ task_input.value = self.task_history[self.history_index]
781
+ # Move cursor to end of text
782
+ task_input.cursor_position = len(task_input.value)
783
+
784
+ # Prevent default behavior (cursor movement)
785
+ event.prevent_default()
786
+ event.stop()
787
+
788
+ def on_input_key_down(self, event: events.Key) -> None:
789
+ """Handle down arrow key in the input field."""
790
+ # For textual key events, we need to check focus manually
791
+ input_field = self.query_one('#task-input', Input)
792
+ if not input_field.has_focus:
793
+ return
794
+
795
+ # Only process if we have history
796
+ if not self.task_history:
797
+ return
798
+
799
+ # Move forward in history or clear input if at the end
800
+ if self.history_index < len(self.task_history) - 1:
801
+ self.history_index += 1
802
+ task_input = self.query_one('#task-input', Input)
803
+ task_input.value = self.task_history[self.history_index]
804
+ # Move cursor to end of text
805
+ task_input.cursor_position = len(task_input.value)
806
+ elif self.history_index == len(self.task_history) - 1:
807
+ # At the end of history, go to "new line" state
808
+ self.history_index += 1
809
+ self.query_one('#task-input', Input).value = ''
810
+
811
+ # Prevent default behavior (cursor movement)
812
+ event.prevent_default()
813
+ event.stop()
814
+
815
+ async def on_key(self, event: events.Key) -> None:
816
+ """Handle key events at the app level to ensure graceful exit."""
817
+ # Handle Ctrl+C, Ctrl+D, and Ctrl+Q for app exit
818
+ if event.key == 'ctrl+c' or event.key == 'ctrl+d' or event.key == 'ctrl+q':
819
+ await self.action_quit()
820
+ event.stop()
821
+ event.prevent_default()
822
+
823
+ def on_input_submitted(self, event: Input.Submitted) -> None:
824
+ """Handle task input submission."""
825
+ if event.input.id == 'task-input':
826
+ task = event.input.value
827
+ if not task.strip():
828
+ return
829
+
830
+ # Add to history if it's new
831
+ if task.strip() and (not self.task_history or task != self.task_history[-1]):
832
+ self.task_history.append(task)
833
+ self.config['command_history'] = self.task_history
834
+ save_user_config(self.config)
835
+
836
+ # Reset history index to point past the end of history
837
+ self.history_index = len(self.task_history)
838
+
839
+ # Hide logo, links, and paths panels
840
+ self.hide_intro_panels()
841
+
842
+ # Process the task
843
+ self.run_task(task)
844
+
845
+ # Clear the input
846
+ event.input.value = ''
847
+
848
+ def hide_intro_panels(self) -> None:
849
+ """Hide the intro panels, show info panels and the three-column view."""
850
+ try:
851
+ # Get the panels
852
+ logo_panel = self.query_one('#logo-panel')
853
+ links_panel = self.query_one('#links-panel')
854
+ paths_panel = self.query_one('#paths-panel')
855
+ info_panels = self.query_one('#info-panels')
856
+ three_column = self.query_one('#three-column-container')
857
+
858
+ # Hide intro panels if they're visible and show info panels + three-column view
859
+ if logo_panel.display:
860
+ logging.debug('Hiding intro panels and showing info panels + three-column view')
861
+
862
+ logo_panel.display = False
863
+ links_panel.display = False
864
+ paths_panel.display = False
865
+
866
+ # Show info panels and three-column container
867
+ info_panels.display = True
868
+ three_column.display = True
869
+
870
+ # Start updating info panels
871
+ self.update_info_panels()
872
+
873
+ logging.debug('Info panels and three-column view should now be visible')
874
+ except Exception as e:
875
+ logging.error(f'Error in hide_intro_panels: {str(e)}')
876
+
877
+ def setup_event_bus_listener(self) -> None:
878
+ """Setup listener for browser session event bus."""
879
+ if not self.browser_session or not self.browser_session.event_bus:
880
+ return
881
+
882
+ # Clean up any existing handler before registering a new one
883
+ if self._event_bus_handler_func is not None:
884
+ try:
885
+ # Remove handler from the event bus's internal handlers dict
886
+ if hasattr(self.browser_session.event_bus, 'handlers'):
887
+ # Find and remove our handler function from all event patterns
888
+ for event_type, handler_list in list(self.browser_session.event_bus.handlers.items()):
889
+ # Remove our specific handler function object
890
+ if self._event_bus_handler_func in handler_list:
891
+ handler_list.remove(self._event_bus_handler_func)
892
+ logging.debug(f'Removed old handler from event type: {event_type}')
893
+ except Exception as e:
894
+ logging.debug(f'Error cleaning up event bus handler: {e}')
895
+ self._event_bus_handler_func = None
896
+ self._event_bus_handler_id = None
897
+
898
+ try:
899
+ # Get the events log widget
900
+ events_log = self.query_one('#events-log', RichLog)
901
+ except Exception:
902
+ # Widget not ready yet
903
+ return
904
+
905
+ # Create handler to log all events
906
+ def log_event(event):
907
+ event_name = event.__class__.__name__
908
+ # Format event data nicely
909
+ try:
910
+ if hasattr(event, 'model_dump'):
911
+ event_data = event.model_dump(exclude_unset=True)
912
+ # Remove large fields
913
+ if 'screenshot' in event_data:
914
+ event_data['screenshot'] = '<bytes>'
915
+ if 'dom_state' in event_data:
916
+ event_data['dom_state'] = '<truncated>'
917
+ event_str = str(event_data) if event_data else ''
918
+ else:
919
+ event_str = str(event)
920
+
921
+ # Truncate long strings
922
+ if len(event_str) > 200:
923
+ event_str = event_str[:200] + '...'
924
+
925
+ events_log.write(f'[yellow]→ {event_name}[/] {event_str}')
926
+ except Exception as e:
927
+ events_log.write(f'[red]→ {event_name}[/] (error formatting: {e})')
928
+
929
+ # Store the handler function before registering it
930
+ self._event_bus_handler_func = log_event
931
+ self._event_bus_handler_id = id(log_event)
932
+
933
+ # Register wildcard handler for all events
934
+ self.browser_session.event_bus.on('*', log_event)
935
+ logging.debug(f'Registered new event bus handler with id: {self._event_bus_handler_id}')
936
+
937
+ def setup_cdp_logger(self) -> None:
938
+ """Setup CDP message logger to capture already-transformed CDP logs."""
939
+ # No need to configure levels - setup_logging() already handles that
940
+ # We just need to capture the transformed logs and route them to the CDP pane
941
+
942
+ # Get the CDP log widget
943
+ cdp_log = self.query_one('#cdp-log', RichLog)
944
+
945
+ # Create custom handler for CDP logging
946
+ class CDPLogHandler(logging.Handler):
947
+ def __init__(self, rich_log: RichLog):
948
+ super().__init__()
949
+ self.rich_log = rich_log
950
+
951
+ def emit(self, record):
952
+ try:
953
+ msg = self.format(record)
954
+ # Truncate very long messages
955
+ if len(msg) > 300:
956
+ msg = msg[:300] + '...'
957
+ # Color code by level
958
+ if record.levelno >= logging.ERROR:
959
+ self.rich_log.write(f'[red]{msg}[/]')
960
+ elif record.levelno >= logging.WARNING:
961
+ self.rich_log.write(f'[yellow]{msg}[/]')
962
+ else:
963
+ self.rich_log.write(f'[cyan]{msg}[/]')
964
+ except Exception:
965
+ self.handleError(record)
966
+
967
+ # Setup handler for cdp_use loggers
968
+ cdp_handler = CDPLogHandler(cdp_log)
969
+ cdp_handler.setFormatter(logging.Formatter('%(message)s'))
970
+ cdp_handler.setLevel(logging.DEBUG)
971
+
972
+ # Route CDP logs to the CDP pane
973
+ # These are already transformed by cdp_use and at the right level from setup_logging
974
+ for logger_name in ['websockets.client', 'cdp_use', 'cdp_use.client', 'cdp_use.cdp', 'cdp_use.cdp.registry']:
975
+ logger = logging.getLogger(logger_name)
976
+ # Add our handler (don't replace - keep existing console handler too)
977
+ if cdp_handler not in logger.handlers:
978
+ logger.addHandler(cdp_handler)
979
+
980
+ def scroll_to_input(self) -> None:
981
+ """Scroll to the input field to ensure it's visible."""
982
+ input_container = self.query_one('#task-input-container')
983
+ input_container.scroll_visible()
984
+
985
+ def run_task(self, task: str) -> None:
986
+ """Launch the task in a background worker."""
987
+ # Create or update the agent
988
+ agent_settings = AgentSettings.model_validate(self.config.get('agent', {}))
989
+
990
+ # Get the logger
991
+ logger = logging.getLogger('browser_use.app')
992
+
993
+ # Make sure intro is hidden and log is ready
994
+ self.hide_intro_panels()
995
+
996
+ # Clear the main output log to start fresh
997
+ rich_log = self.query_one('#main-output-log', RichLog)
998
+ rich_log.clear()
999
+
1000
+ if self.agent is None:
1001
+ if not self.llm:
1002
+ raise RuntimeError('LLM not initialized')
1003
+ self.agent = Agent(
1004
+ task=task,
1005
+ llm=self.llm,
1006
+ controller=self.controller if self.controller else Controller(),
1007
+ browser_session=self.browser_session,
1008
+ source='cli',
1009
+ **agent_settings.model_dump(),
1010
+ )
1011
+ # Update our browser_session reference to point to the agent's
1012
+ if hasattr(self.agent, 'browser_session'):
1013
+ self.browser_session = self.agent.browser_session
1014
+ # Set up event bus listener (will clean up any old handler first)
1015
+ self.setup_event_bus_listener()
1016
+ else:
1017
+ self.agent.add_new_task(task)
1018
+
1019
+ # Let the agent run in the background
1020
+ async def agent_task_worker() -> None:
1021
+ logger.debug('\n🚀 Working on task: %s', task)
1022
+
1023
+ # Set flags to indicate the agent is running
1024
+ if self.agent:
1025
+ self.agent.running = True # type: ignore
1026
+ self.agent.last_response_time = 0 # type: ignore
1027
+
1028
+ # Panel updates are already happening via the timer in update_info_panels
1029
+
1030
+ task_start_time = time.time()
1031
+ error_msg = None
1032
+
1033
+ try:
1034
+ # Capture telemetry for message sent
1035
+ self._telemetry.capture(
1036
+ CLITelemetryEvent(
1037
+ version=get_browser_use_version(),
1038
+ action='message_sent',
1039
+ mode='interactive',
1040
+ model=self.llm.model if self.llm and hasattr(self.llm, 'model') else None,
1041
+ model_provider=self.llm.provider if self.llm and hasattr(self.llm, 'provider') else None,
1042
+ )
1043
+ )
1044
+
1045
+ # Run the agent task, redirecting output to RichLog through our handler
1046
+ if self.agent:
1047
+ await self.agent.run()
1048
+ except Exception as e:
1049
+ error_msg = str(e)
1050
+ logger.error('\nError running agent: %s', str(e))
1051
+ finally:
1052
+ # Clear the running flag
1053
+ if self.agent:
1054
+ self.agent.running = False # type: ignore
1055
+
1056
+ # Capture telemetry for task completion
1057
+ duration = time.time() - task_start_time
1058
+ self._telemetry.capture(
1059
+ CLITelemetryEvent(
1060
+ version=get_browser_use_version(),
1061
+ action='task_completed' if error_msg is None else 'error',
1062
+ mode='interactive',
1063
+ model=self.llm.model if self.llm and hasattr(self.llm, 'model') else None,
1064
+ model_provider=self.llm.provider if self.llm and hasattr(self.llm, 'provider') else None,
1065
+ duration_seconds=duration,
1066
+ error_message=error_msg,
1067
+ )
1068
+ )
1069
+
1070
+ logger.debug('\n✅ Task completed!')
1071
+
1072
+ # Make sure the task input container is visible
1073
+ task_input_container = self.query_one('#task-input-container')
1074
+ task_input_container.display = True
1075
+
1076
+ # Refocus the input field
1077
+ input_field = self.query_one('#task-input', Input)
1078
+ input_field.focus()
1079
+
1080
+ # Ensure the input is visible by scrolling to it
1081
+ self.call_after_refresh(self.scroll_to_input)
1082
+
1083
+ # Run the worker
1084
+ self.run_worker(agent_task_worker, name='agent_task')
1085
+
1086
+ def action_input_history_prev(self) -> None:
1087
+ """Navigate to the previous item in command history."""
1088
+ # Only process if we have history and input is focused
1089
+ input_field = self.query_one('#task-input', Input)
1090
+ if not input_field.has_focus or not self.task_history:
1091
+ return
1092
+
1093
+ # Move back in history if possible
1094
+ if self.history_index > 0:
1095
+ self.history_index -= 1
1096
+ input_field.value = self.task_history[self.history_index]
1097
+ # Move cursor to end of text
1098
+ input_field.cursor_position = len(input_field.value)
1099
+
1100
+ def action_input_history_next(self) -> None:
1101
+ """Navigate to the next item in command history or clear input."""
1102
+ # Only process if we have history and input is focused
1103
+ input_field = self.query_one('#task-input', Input)
1104
+ if not input_field.has_focus or not self.task_history:
1105
+ return
1106
+
1107
+ # Move forward in history or clear input if at the end
1108
+ if self.history_index < len(self.task_history) - 1:
1109
+ self.history_index += 1
1110
+ input_field.value = self.task_history[self.history_index]
1111
+ # Move cursor to end of text
1112
+ input_field.cursor_position = len(input_field.value)
1113
+ elif self.history_index == len(self.task_history) - 1:
1114
+ # At the end of history, go to "new line" state
1115
+ self.history_index += 1
1116
+ input_field.value = ''
1117
+
1118
+ async def action_quit(self) -> None:
1119
+ """Quit the application and clean up resources."""
1120
+ # Note: We don't need to close the browser session here because:
1121
+ # 1. If an agent exists, it already called browser_session.stop() in its run() method
1122
+ # 2. If keep_alive=True (default), we want to leave the browser running anyway
1123
+ # This prevents the duplicate "stop() called" messages in the logs
1124
+
1125
+ # Flush telemetry before exiting
1126
+ self._telemetry.flush()
1127
+
1128
+ # Exit the application
1129
+ self.exit()
1130
+ print('\nTry running tasks on our cloud: https://browser-use.com')
1131
+
1132
+ def compose(self) -> ComposeResult:
1133
+ """Create the UI layout."""
1134
+ yield Header()
1135
+
1136
+ # Main container for app content
1137
+ with Container(id='main-container'):
1138
+ # Logo panel
1139
+ yield Static(BROWSER_LOGO, id='logo-panel', markup=True)
1140
+
1141
+ # Links panel with URLs
1142
+ with Container(id='links-panel'):
1143
+ with HorizontalGroup(classes='link-row'):
1144
+ yield Static('Run at scale on cloud: [blink]☁️[/] ', markup=True, classes='link-label')
1145
+ yield Link('https://browser-use.com', url='https://browser-use.com', classes='link-white link-url')
1146
+
1147
+ yield Static('') # Empty line
1148
+
1149
+ with HorizontalGroup(classes='link-row'):
1150
+ yield Static('Chat & share on Discord: 🚀 ', markup=True, classes='link-label')
1151
+ yield Link(
1152
+ 'https://discord.gg/ESAUZAdxXY', url='https://discord.gg/ESAUZAdxXY', classes='link-purple link-url'
1153
+ )
1154
+
1155
+ with HorizontalGroup(classes='link-row'):
1156
+ yield Static('Get prompt inspiration: 🦸 ', markup=True, classes='link-label')
1157
+ yield Link(
1158
+ 'https://github.com/browser-use/awesome-prompts',
1159
+ url='https://github.com/browser-use/awesome-prompts',
1160
+ classes='link-magenta link-url',
1161
+ )
1162
+
1163
+ with HorizontalGroup(classes='link-row'):
1164
+ yield Static('[dim]Report any issues:[/] 🐛 ', markup=True, classes='link-label')
1165
+ yield Link(
1166
+ 'https://github.com/browser-use/browser-use/issues',
1167
+ url='https://github.com/browser-use/browser-use/issues',
1168
+ classes='link-green link-url',
1169
+ )
1170
+
1171
+ # Paths panel
1172
+ yield Static(
1173
+ f' ⚙️ Settings saved to: {str(CONFIG.BROWSER_USE_CONFIG_FILE.resolve()).replace(str(Path.home()), "~")}\n'
1174
+ f' 📁 Outputs & recordings saved to: {str(Path(".").resolve()).replace(str(Path.home()), "~")}',
1175
+ id='paths-panel',
1176
+ markup=True,
1177
+ )
1178
+
1179
+ # Info panels (hidden by default, shown when task starts)
1180
+ with Container(id='info-panels'):
1181
+ # Top row with browser and model panels side by side
1182
+ with Container(id='top-panels'):
1183
+ # Browser panel
1184
+ with Container(id='browser-panel'):
1185
+ yield RichLog(id='browser-info', markup=True, highlight=True, wrap=True)
1186
+
1187
+ # Model panel
1188
+ with Container(id='model-panel'):
1189
+ yield RichLog(id='model-info', markup=True, highlight=True, wrap=True)
1190
+
1191
+ # Tasks panel (full width, below browser and model)
1192
+ with VerticalScroll(id='tasks-panel'):
1193
+ yield RichLog(id='tasks-info', markup=True, highlight=True, wrap=True, auto_scroll=True)
1194
+
1195
+ # Three-column container (hidden by default)
1196
+ with Container(id='three-column-container'):
1197
+ # Column 1: Main output
1198
+ with VerticalScroll(id='main-output-column'):
1199
+ yield RichLog(highlight=True, markup=True, id='main-output-log', wrap=True, auto_scroll=True)
1200
+
1201
+ # Column 2: Event bus events
1202
+ with VerticalScroll(id='events-column'):
1203
+ yield RichLog(highlight=True, markup=True, id='events-log', wrap=True, auto_scroll=True)
1204
+
1205
+ # Column 3: CDP messages
1206
+ with VerticalScroll(id='cdp-column'):
1207
+ yield RichLog(highlight=True, markup=True, id='cdp-log', wrap=True, auto_scroll=True)
1208
+
1209
+ # Task input container (now at the bottom)
1210
+ with Container(id='task-input-container'):
1211
+ yield Label('🔍 What would you like me to do on the web?', id='task-label')
1212
+ yield Input(placeholder='Enter your task...', id='task-input')
1213
+
1214
+ yield Footer()
1215
+
1216
+ def update_info_panels(self) -> None:
1217
+ """Update all information panels with current state."""
1218
+ try:
1219
+ # Update actual content
1220
+ self.update_browser_panel()
1221
+ self.update_model_panel()
1222
+ self.update_tasks_panel()
1223
+ except Exception as e:
1224
+ logging.error(f'Error in update_info_panels: {str(e)}')
1225
+ finally:
1226
+ # Always schedule the next update - will update at 1-second intervals
1227
+ # This ensures continuous updates even if agent state changes
1228
+ self.set_timer(1.0, self.update_info_panels)
1229
+
1230
+ def update_browser_panel(self) -> None:
1231
+ """Update browser information panel with details about the browser."""
1232
+ browser_info = self.query_one('#browser-info', RichLog)
1233
+ browser_info.clear()
1234
+
1235
+ # Try to use the agent's browser session if available
1236
+ browser_session = self.browser_session
1237
+ if hasattr(self, 'agent') and self.agent and hasattr(self.agent, 'browser_session'):
1238
+ browser_session = self.agent.browser_session
1239
+
1240
+ if browser_session:
1241
+ try:
1242
+ # Check if browser session has a CDP client
1243
+ if not hasattr(browser_session, 'cdp_client') or browser_session.cdp_client is None:
1244
+ browser_info.write('[yellow]Browser session created, waiting for browser to launch...[/]')
1245
+ return
1246
+
1247
+ # Update our reference if we're using the agent's session
1248
+ if browser_session != self.browser_session:
1249
+ self.browser_session = browser_session
1250
+
1251
+ # Get basic browser info from browser_profile
1252
+ browser_type = 'Chromium'
1253
+ headless = browser_session.browser_profile.headless
1254
+
1255
+ # Determine connection type based on config
1256
+ connection_type = 'playwright' # Default
1257
+ if browser_session.cdp_url:
1258
+ connection_type = 'CDP'
1259
+ elif browser_session.browser_profile.executable_path:
1260
+ connection_type = 'user-provided'
1261
+
1262
+ # Get window size details from browser_profile
1263
+ window_width = None
1264
+ window_height = None
1265
+ if browser_session.browser_profile.viewport:
1266
+ window_width = browser_session.browser_profile.viewport.width
1267
+ window_height = browser_session.browser_profile.viewport.height
1268
+
1269
+ # Try to get browser PID
1270
+ browser_pid = 'Unknown'
1271
+ connected = False
1272
+ browser_status = '[red]Disconnected[/]'
1273
+
1274
+ try:
1275
+ # Check if browser PID is available
1276
+ # Check if we have a CDP client
1277
+ if browser_session.cdp_client is not None:
1278
+ connected = True
1279
+ browser_status = '[green]Connected[/]'
1280
+ browser_pid = 'N/A'
1281
+ except Exception as e:
1282
+ browser_pid = f'Error: {str(e)}'
1283
+
1284
+ # Display browser information
1285
+ browser_info.write(f'[bold cyan]Chromium[/] Browser ({browser_status})')
1286
+ browser_info.write(
1287
+ f'Type: [yellow]{connection_type}[/] [{"green" if not headless else "red"}]{" (headless)" if headless else ""}[/]'
1288
+ )
1289
+ browser_info.write(f'PID: [dim]{browser_pid}[/]')
1290
+ browser_info.write(f'CDP Port: {browser_session.cdp_url}')
1291
+
1292
+ if window_width and window_height:
1293
+ browser_info.write(f'Window: [blue]{window_width}[/] × [blue]{window_height}[/]')
1294
+
1295
+ # Include additional information about the browser if needed
1296
+ if connected and hasattr(self, 'agent') and self.agent:
1297
+ try:
1298
+ # Show when the browser was connected
1299
+ timestamp = int(time.time())
1300
+ current_time = time.strftime('%H:%M:%S', time.localtime(timestamp))
1301
+ browser_info.write(f'Last updated: [dim]{current_time}[/]')
1302
+ except Exception:
1303
+ pass
1304
+
1305
+ # Show the agent's current page URL if available
1306
+ if browser_session.agent_focus:
1307
+ current_url = (
1308
+ browser_session.agent_focus.url.replace('https://', '')
1309
+ .replace('http://', '')
1310
+ .replace('www.', '')[:36]
1311
+ + '…'
1312
+ )
1313
+ browser_info.write(f'👁️ [green]{current_url}[/]')
1314
+ except Exception as e:
1315
+ browser_info.write(f'[red]Error updating browser info: {str(e)}[/]')
1316
+ else:
1317
+ browser_info.write('[red]Browser not initialized[/]')
1318
+
1319
+ def update_model_panel(self) -> None:
1320
+ """Update model information panel with details about the LLM."""
1321
+ model_info = self.query_one('#model-info', RichLog)
1322
+ model_info.clear()
1323
+
1324
+ if self.llm:
1325
+ # Get model details
1326
+ model_name = 'Unknown'
1327
+ if hasattr(self.llm, 'model_name'):
1328
+ model_name = self.llm.model_name
1329
+ elif hasattr(self.llm, 'model'):
1330
+ model_name = self.llm.model
1331
+
1332
+ # Show model name
1333
+ if self.agent:
1334
+ temp_str = f'{self.llm.temperature}ºC ' if self.llm.temperature else ''
1335
+ vision_str = '+ vision ' if self.agent.settings.use_vision else ''
1336
+ model_info.write(
1337
+ f'[white]LLM:[/] [blue]{self.llm.__class__.__name__} [yellow]{model_name}[/] {temp_str}{vision_str}'
1338
+ )
1339
+ else:
1340
+ model_info.write(f'[white]LLM:[/] [blue]{self.llm.__class__.__name__} [yellow]{model_name}[/]')
1341
+
1342
+ # Show token usage statistics if agent exists and has history
1343
+ if self.agent and hasattr(self.agent, 'state') and hasattr(self.agent.state, 'history'):
1344
+ # Calculate tokens per step
1345
+ num_steps = len(self.agent.history.history)
1346
+
1347
+ # Get the last step metadata to show the most recent LLM response time
1348
+ if num_steps > 0 and self.agent.history.history[-1].metadata:
1349
+ last_step = self.agent.history.history[-1]
1350
+ if last_step.metadata:
1351
+ step_duration = last_step.metadata.duration_seconds
1352
+ else:
1353
+ step_duration = 0
1354
+
1355
+ # Show total duration
1356
+ total_duration = self.agent.history.total_duration_seconds()
1357
+ if total_duration > 0:
1358
+ model_info.write(f'[white]Total Duration:[/] [magenta]{total_duration:.2f}s[/]')
1359
+
1360
+ # Calculate response time metrics
1361
+ model_info.write(f'[white]Last Step Duration:[/] [magenta]{step_duration:.2f}s[/]')
1362
+
1363
+ # Add current state information
1364
+ if hasattr(self.agent, 'running'):
1365
+ if getattr(self.agent, 'running', False):
1366
+ model_info.write('[yellow]LLM is thinking[blink]...[/][/]')
1367
+ elif hasattr(self.agent, 'state') and hasattr(self.agent.state, 'paused') and self.agent.state.paused:
1368
+ model_info.write('[orange]LLM paused[/]')
1369
+ else:
1370
+ model_info.write('[red]Model not initialized[/]')
1371
+
1372
+ def update_tasks_panel(self) -> None:
1373
+ """Update tasks information panel with details about the tasks and steps hierarchy."""
1374
+ tasks_info = self.query_one('#tasks-info', RichLog)
1375
+ tasks_info.clear()
1376
+
1377
+ if self.agent:
1378
+ # Check if agent has tasks
1379
+ task_history = []
1380
+ message_history = []
1381
+
1382
+ # Try to extract tasks by looking at message history
1383
+ if hasattr(self.agent, '_message_manager') and self.agent._message_manager:
1384
+ message_history = self.agent._message_manager.state.history.get_messages()
1385
+
1386
+ # Extract original task(s)
1387
+ original_tasks = []
1388
+ for msg in message_history:
1389
+ if hasattr(msg, 'content'):
1390
+ content = msg.content
1391
+ if isinstance(content, str) and 'Your ultimate task is:' in content:
1392
+ task_text = content.split('"""')[1].strip()
1393
+ original_tasks.append(task_text)
1394
+
1395
+ if original_tasks:
1396
+ tasks_info.write('[bold green]TASK:[/]')
1397
+ for i, task in enumerate(original_tasks, 1):
1398
+ # Only show latest task if multiple task changes occurred
1399
+ if i == len(original_tasks):
1400
+ tasks_info.write(f'[white]{task}[/]')
1401
+ tasks_info.write('')
1402
+
1403
+ # Get current state information
1404
+ current_step = self.agent.state.n_steps if hasattr(self.agent, 'state') else 0
1405
+
1406
+ # Get all agent history items
1407
+ history_items = []
1408
+ if hasattr(self.agent, 'state') and hasattr(self.agent.state, 'history'):
1409
+ history_items = self.agent.history.history
1410
+
1411
+ if history_items:
1412
+ tasks_info.write('[bold yellow]STEPS:[/]')
1413
+
1414
+ for idx, item in enumerate(history_items, 1):
1415
+ # Determine step status
1416
+ step_style = '[green]✓[/]'
1417
+
1418
+ # For the current step, show it as in progress
1419
+ if idx == current_step:
1420
+ step_style = '[yellow]⟳[/]'
1421
+
1422
+ # Check if this step had an error
1423
+ if item.result and any(result.error for result in item.result):
1424
+ step_style = '[red]✗[/]'
1425
+
1426
+ # Show step number
1427
+ tasks_info.write(f'{step_style} Step {idx}/{current_step}')
1428
+
1429
+ # Show goal if available
1430
+ if item.model_output and hasattr(item.model_output, 'current_state'):
1431
+ # Show goal for this step
1432
+ goal = item.model_output.current_state.next_goal
1433
+ if goal:
1434
+ # Take just the first line for display
1435
+ goal_lines = goal.strip().split('\n')
1436
+ goal_summary = goal_lines[0]
1437
+ tasks_info.write(f' [cyan]Goal:[/] {goal_summary}')
1438
+
1439
+ # Show evaluation of previous goal (feedback)
1440
+ eval_prev = item.model_output.current_state.evaluation_previous_goal
1441
+ if eval_prev and idx > 1: # Only show for steps after the first
1442
+ eval_lines = eval_prev.strip().split('\n')
1443
+ eval_summary = eval_lines[0]
1444
+ eval_summary = eval_summary.replace('Success', '✅ ').replace('Failed', '❌ ').strip()
1445
+ tasks_info.write(f' [tan]Evaluation:[/] {eval_summary}')
1446
+
1447
+ # Show actions taken in this step
1448
+ if item.model_output and item.model_output.action:
1449
+ tasks_info.write(' [purple]Actions:[/]')
1450
+ for action_idx, action in enumerate(item.model_output.action, 1):
1451
+ action_type = action.__class__.__name__
1452
+ if hasattr(action, 'model_dump'):
1453
+ # For proper actions, show the action type
1454
+ action_dict = action.model_dump(exclude_unset=True)
1455
+ if action_dict:
1456
+ action_name = list(action_dict.keys())[0]
1457
+ tasks_info.write(f' {action_idx}. [blue]{action_name}[/]')
1458
+
1459
+ # Show results or errors from this step
1460
+ if item.result:
1461
+ for result in item.result:
1462
+ if result.error:
1463
+ error_text = result.error
1464
+ tasks_info.write(f' [red]Error:[/] {error_text}')
1465
+ elif result.extracted_content:
1466
+ content = result.extracted_content
1467
+ tasks_info.write(f' [green]Result:[/] {content}')
1468
+
1469
+ # Add a space between steps for readability
1470
+ tasks_info.write('')
1471
+
1472
+ # If agent is actively running, show a status indicator
1473
+ if hasattr(self.agent, 'running') and getattr(self.agent, 'running', False):
1474
+ tasks_info.write('[yellow]Agent is actively working[blink]...[/][/]')
1475
+ elif hasattr(self.agent, 'state') and hasattr(self.agent.state, 'paused') and self.agent.state.paused:
1476
+ tasks_info.write('[orange]Agent is paused (press Enter to resume)[/]')
1477
+ else:
1478
+ tasks_info.write('[dim]Agent not initialized[/]')
1479
+
1480
+ # Force scroll to bottom
1481
+ tasks_panel = self.query_one('#tasks-panel')
1482
+ tasks_panel.scroll_end(animate=False)
1483
+
1484
+
1485
+ async def run_prompt_mode(prompt: str, ctx: click.Context, debug: bool = False):
1486
+ """Run browser-use in non-interactive mode with a single prompt."""
1487
+ # Import and call setup_logging to ensure proper initialization
1488
+ from browser_use.logging_config import setup_logging
1489
+
1490
+ # Set up logging to only show results by default
1491
+ os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'result'
1492
+
1493
+ # Re-run setup_logging to apply the new log level
1494
+ setup_logging()
1495
+
1496
+ # The logging is now properly configured by setup_logging()
1497
+ # No need to manually configure handlers since setup_logging() handles it
1498
+
1499
+ # Initialize telemetry
1500
+ telemetry = ProductTelemetry()
1501
+ start_time = time.time()
1502
+ error_msg = None
1503
+
1504
+ try:
1505
+ # Load config
1506
+ config = load_user_config()
1507
+ config = update_config_with_click_args(config, ctx)
1508
+
1509
+ # Get LLM
1510
+ llm = get_llm(config)
1511
+
1512
+ # Capture telemetry for CLI start in oneshot mode
1513
+ telemetry.capture(
1514
+ CLITelemetryEvent(
1515
+ version=get_browser_use_version(),
1516
+ action='start',
1517
+ mode='oneshot',
1518
+ model=llm.model if hasattr(llm, 'model') else None,
1519
+ model_provider=llm.__class__.__name__ if llm else None,
1520
+ )
1521
+ )
1522
+
1523
+ # Get agent settings from config
1524
+ agent_settings = AgentSettings.model_validate(config.get('agent', {}))
1525
+
1526
+ # Create browser session with config parameters
1527
+ browser_config = config.get('browser', {})
1528
+ # Remove None values from browser_config
1529
+ browser_config = {k: v for k, v in browser_config.items() if v is not None}
1530
+ # Create BrowserProfile with user_data_dir
1531
+ profile = BrowserProfile(user_data_dir=str(USER_DATA_DIR), **browser_config)
1532
+ browser_session = BrowserSession(
1533
+ browser_profile=profile,
1534
+ )
1535
+
1536
+ # Create and run agent
1537
+ agent = Agent(
1538
+ task=prompt,
1539
+ llm=llm,
1540
+ browser_session=browser_session,
1541
+ source='cli',
1542
+ **agent_settings.model_dump(),
1543
+ )
1544
+
1545
+ await agent.run()
1546
+
1547
+ # Ensure the browser session is fully stopped
1548
+ # The agent's close() method only kills the browser if keep_alive=False,
1549
+ # but we need to ensure all background tasks are stopped regardless
1550
+ if browser_session:
1551
+ try:
1552
+ # Kill the browser session to stop all background tasks
1553
+ await browser_session.kill()
1554
+ except Exception:
1555
+ # Ignore errors during cleanup
1556
+ pass
1557
+
1558
+ # Capture telemetry for successful completion
1559
+ telemetry.capture(
1560
+ CLITelemetryEvent(
1561
+ version=get_browser_use_version(),
1562
+ action='task_completed',
1563
+ mode='oneshot',
1564
+ model=llm.model if hasattr(llm, 'model') else None,
1565
+ model_provider=llm.__class__.__name__ if llm else None,
1566
+ duration_seconds=time.time() - start_time,
1567
+ )
1568
+ )
1569
+
1570
+ except Exception as e:
1571
+ error_msg = str(e)
1572
+ # Capture telemetry for error
1573
+ telemetry.capture(
1574
+ CLITelemetryEvent(
1575
+ version=get_browser_use_version(),
1576
+ action='error',
1577
+ mode='oneshot',
1578
+ model=llm.model if hasattr(llm, 'model') else None,
1579
+ model_provider=llm.__class__.__name__ if llm and 'llm' in locals() else None,
1580
+ duration_seconds=time.time() - start_time,
1581
+ error_message=error_msg,
1582
+ )
1583
+ )
1584
+ if debug:
1585
+ import traceback
1586
+
1587
+ traceback.print_exc()
1588
+ else:
1589
+ print(f'Error: {str(e)}', file=sys.stderr)
1590
+ sys.exit(1)
1591
+ finally:
1592
+ # Ensure telemetry is flushed
1593
+ telemetry.flush()
1594
+
1595
+ # Give a brief moment for cleanup to complete
1596
+ await asyncio.sleep(0.1)
1597
+
1598
+ # Cancel any remaining tasks to ensure clean exit
1599
+ tasks = [t for t in asyncio.all_tasks() if t != asyncio.current_task()]
1600
+ for task in tasks:
1601
+ task.cancel()
1602
+
1603
+ # Wait for all tasks to be cancelled
1604
+ if tasks:
1605
+ await asyncio.gather(*tasks, return_exceptions=True)
1606
+
1607
+
1608
+ async def textual_interface(config: dict[str, Any]):
1609
+ """Run the Textual interface."""
1610
+ # Prevent browser_use from setting up logging at import time
1611
+ os.environ['BROWSER_USE_SETUP_LOGGING'] = 'false'
1612
+
1613
+ logger = logging.getLogger('browser_use.startup')
1614
+
1615
+ # Set up logging for Textual UI - prevent any logging to stdout
1616
+ def setup_textual_logging():
1617
+ # Replace all handlers with null handler
1618
+ root_logger = logging.getLogger()
1619
+ for handler in root_logger.handlers:
1620
+ root_logger.removeHandler(handler)
1621
+
1622
+ # Add null handler to ensure no output to stdout/stderr
1623
+ null_handler = logging.NullHandler()
1624
+ root_logger.addHandler(null_handler)
1625
+ logger.debug('Logging configured for Textual UI')
1626
+
1627
+ logger.debug('Setting up Browser, Controller, and LLM...')
1628
+
1629
+ # Step 1: Initialize BrowserSession with config
1630
+ logger.debug('Initializing BrowserSession...')
1631
+ try:
1632
+ # Get browser config from the config dict
1633
+ browser_config = config.get('browser', {})
1634
+
1635
+ logger.info('Browser type: chromium') # BrowserSession only supports chromium
1636
+ if browser_config.get('executable_path'):
1637
+ logger.info(f'Browser binary: {browser_config["executable_path"]}')
1638
+ if browser_config.get('headless'):
1639
+ logger.info('Browser mode: headless')
1640
+ else:
1641
+ logger.info('Browser mode: visible')
1642
+
1643
+ # Create BrowserSession directly with config parameters
1644
+ # Remove None values from browser_config
1645
+ browser_config = {k: v for k, v in browser_config.items() if v is not None}
1646
+ # Create BrowserProfile with user_data_dir
1647
+ profile = BrowserProfile(user_data_dir=str(USER_DATA_DIR), **browser_config)
1648
+ browser_session = BrowserSession(
1649
+ browser_profile=profile,
1650
+ )
1651
+ logger.debug('BrowserSession initialized successfully')
1652
+
1653
+ # Set up FIFO logging pipes for streaming logs to UI
1654
+ try:
1655
+ from browser_use.logging_config import setup_log_pipes
1656
+
1657
+ setup_log_pipes(session_id=browser_session.id)
1658
+ logger.debug(f'FIFO logging pipes set up for session {browser_session.id[-4:]}')
1659
+ except Exception as e:
1660
+ logger.debug(f'Could not set up FIFO logging pipes: {e}')
1661
+
1662
+ # Browser version logging not available with CDP implementation
1663
+ except Exception as e:
1664
+ logger.error(f'Error initializing BrowserSession: {str(e)}', exc_info=True)
1665
+ raise RuntimeError(f'Failed to initialize BrowserSession: {str(e)}')
1666
+
1667
+ # Step 3: Initialize Controller
1668
+ logger.debug('Initializing Controller...')
1669
+ try:
1670
+ controller = Controller()
1671
+ logger.debug('Controller initialized successfully')
1672
+ except Exception as e:
1673
+ logger.error(f'Error initializing Controller: {str(e)}', exc_info=True)
1674
+ raise RuntimeError(f'Failed to initialize Controller: {str(e)}')
1675
+
1676
+ # Step 4: Get LLM
1677
+ logger.debug('Getting LLM...')
1678
+ try:
1679
+ # Ensure setup_logging is not called when importing modules
1680
+ os.environ['BROWSER_USE_SETUP_LOGGING'] = 'false'
1681
+ llm = get_llm(config)
1682
+ # Log LLM details
1683
+ model_name = getattr(llm, 'model_name', None) or getattr(llm, 'model', 'Unknown model')
1684
+ provider = llm.__class__.__name__
1685
+ temperature = getattr(llm, 'temperature', 0.0)
1686
+ logger.info(f'LLM: {provider} ({model_name}), temperature: {temperature}')
1687
+ logger.debug(f'LLM initialized successfully: {provider}')
1688
+ except Exception as e:
1689
+ logger.error(f'Error getting LLM: {str(e)}', exc_info=True)
1690
+ raise RuntimeError(f'Failed to initialize LLM: {str(e)}')
1691
+
1692
+ logger.debug('Initializing BrowserUseApp instance...')
1693
+ try:
1694
+ app = BrowserUseApp(config)
1695
+ # Pass the initialized components to the app
1696
+ app.browser_session = browser_session
1697
+ app.controller = controller
1698
+ app.llm = llm
1699
+
1700
+ # Set up event bus listener now that browser session is available
1701
+ # Note: This needs to be called before run_async() but after browser_session is set
1702
+ # We'll defer this to on_mount() since it needs the widgets to be available
1703
+
1704
+ # Configure logging for Textual UI before going fullscreen
1705
+ setup_textual_logging()
1706
+
1707
+ # Log browser and model configuration that will be used
1708
+ browser_type = 'Chromium' # BrowserSession only supports Chromium
1709
+ model_name = config.get('model', {}).get('name', 'auto-detected')
1710
+ headless = config.get('browser', {}).get('headless', False)
1711
+ headless_str = 'headless' if headless else 'visible'
1712
+
1713
+ logger.info(f'Preparing {browser_type} browser ({headless_str}) with {model_name} LLM')
1714
+
1715
+ logger.debug('Starting Textual app with run_async()...')
1716
+ # No more logging after this point as we're in fullscreen mode
1717
+ await app.run_async()
1718
+ except Exception as e:
1719
+ logger.error(f'Error in textual_interface: {str(e)}', exc_info=True)
1720
+ # Note: We don't close the browser session here to avoid duplicate stop() calls
1721
+ # The browser session will be cleaned up by its __del__ method if needed
1722
+ raise
1723
+
1724
+
1725
+ async def run_auth_command():
1726
+ """Run the authentication command with dummy task in UI."""
1727
+ import asyncio
1728
+ import os
1729
+
1730
+ from browser_use.sync.auth import DeviceAuthClient
1731
+
1732
+ print('🔐 Browser Use Cloud Authentication')
1733
+ print('=' * 40)
1734
+
1735
+ # Ensure cloud sync is enabled (should be default, but make sure)
1736
+ os.environ['BROWSER_USE_CLOUD_SYNC'] = 'true'
1737
+
1738
+ auth_client = DeviceAuthClient()
1739
+
1740
+ print('🔍 Debug: Checking authentication status...')
1741
+ print(f' API Token: {"✅ Present" if auth_client.api_token else "❌ Missing"}')
1742
+ print(f' User ID: {auth_client.user_id}')
1743
+ print(f' Is Authenticated: {auth_client.is_authenticated}')
1744
+ if auth_client.auth_config.authorized_at:
1745
+ print(f' Authorized at: {auth_client.auth_config.authorized_at}')
1746
+ print()
1747
+
1748
+ # Check if already authenticated
1749
+ if auth_client.is_authenticated:
1750
+ print('✅ Already authenticated!')
1751
+ print(f' User ID: {auth_client.user_id}')
1752
+ print(f' Authenticated at: {auth_client.auth_config.authorized_at}')
1753
+
1754
+ # Show cloud URL if possible
1755
+ frontend_url = CONFIG.BROWSER_USE_CLOUD_UI_URL or auth_client.base_url.replace('//api.', '//cloud.')
1756
+ print(f'\n🌐 View your runs at: {frontend_url}')
1757
+ return
1758
+
1759
+ print('🚀 Starting authentication flow...')
1760
+ print(' This will open a browser window for you to sign in.')
1761
+ print()
1762
+
1763
+ # Initialize variables for exception handling
1764
+ task_id = None
1765
+ sync_service = None
1766
+
1767
+ try:
1768
+ # Create authentication flow with dummy task
1769
+ from uuid_extensions import uuid7str
1770
+
1771
+ from browser_use.agent.cloud_events import (
1772
+ CreateAgentSessionEvent,
1773
+ CreateAgentStepEvent,
1774
+ CreateAgentTaskEvent,
1775
+ UpdateAgentTaskEvent,
1776
+ )
1777
+ from browser_use.sync.service import CloudSync
1778
+
1779
+ # IDs for our session and task
1780
+ session_id = uuid7str()
1781
+ task_id = uuid7str()
1782
+
1783
+ # Create special sync service that allows auth events
1784
+ sync_service = CloudSync(allow_session_events_for_auth=True)
1785
+ sync_service.set_auth_flow_active() # Explicitly enable auth flow
1786
+ sync_service.session_id = session_id # Set session ID for auth context
1787
+ sync_service.auth_client = auth_client # Use the same auth client instance!
1788
+
1789
+ # 1. Create session (like main branch does at start)
1790
+ session_event = CreateAgentSessionEvent(
1791
+ id=session_id,
1792
+ user_id=auth_client.temp_user_id,
1793
+ browser_session_id=uuid7str(),
1794
+ browser_session_live_url='',
1795
+ browser_session_cdp_url='',
1796
+ device_id=auth_client.device_id,
1797
+ browser_state={
1798
+ 'viewport': {'width': 1280, 'height': 720},
1799
+ 'user_agent': None,
1800
+ 'headless': True,
1801
+ 'initial_url': None,
1802
+ 'final_url': None,
1803
+ 'total_pages_visited': 0,
1804
+ 'session_duration_seconds': 0,
1805
+ },
1806
+ browser_session_data={
1807
+ 'cookies': [],
1808
+ 'secrets': {},
1809
+ 'allowed_domains': [],
1810
+ },
1811
+ )
1812
+ await sync_service.handle_event(session_event)
1813
+
1814
+ # Brief delay to ensure session is created in backend before sending task
1815
+ await asyncio.sleep(0.5)
1816
+
1817
+ # 2. Create task (like main branch does at start)
1818
+ task_event = CreateAgentTaskEvent(
1819
+ id=task_id,
1820
+ agent_session_id=session_id,
1821
+ llm_model='auth-flow',
1822
+ task='🔐 Complete authentication and join the browser-use community',
1823
+ user_id=auth_client.temp_user_id,
1824
+ device_id=auth_client.device_id,
1825
+ done_output=None,
1826
+ user_feedback_type=None,
1827
+ user_comment=None,
1828
+ gif_url=None,
1829
+ )
1830
+ await sync_service.handle_event(task_event)
1831
+
1832
+ # Longer delay to ensure task is created in backend before sending step event
1833
+ await asyncio.sleep(1.0)
1834
+
1835
+ # 3. Run authentication with timeout
1836
+ print('⏳ Waiting for authentication... (this may take up to 2 minutes for testing)')
1837
+ print(' Complete the authentication in your browser, then this will continue automatically.')
1838
+ print()
1839
+
1840
+ try:
1841
+ print('🔧 Debug: Starting authentication process...')
1842
+ print(f' Original auth client authenticated: {auth_client.is_authenticated}')
1843
+ print(f' Sync service auth client authenticated: {sync_service.auth_client.is_authenticated}')
1844
+ print(f' Same auth client? {auth_client is sync_service.auth_client}')
1845
+ print(f' Session ID: {sync_service.session_id}')
1846
+
1847
+ # Create a task to show periodic status updates
1848
+ async def show_auth_progress():
1849
+ for i in range(1, 25): # Show updates every 5 seconds for 2 minutes
1850
+ await asyncio.sleep(5)
1851
+ fresh_check = DeviceAuthClient()
1852
+ print(f'⏱️ Waiting for authentication... ({i * 5}s elapsed)')
1853
+ print(f' Status: {"✅ Authenticated" if fresh_check.is_authenticated else "⏳ Still waiting"}')
1854
+ if fresh_check.is_authenticated:
1855
+ print('🎉 Authentication detected! Completing...')
1856
+ break
1857
+
1858
+ # Run authentication and progress updates concurrently
1859
+ auth_start_time = asyncio.get_event_loop().time()
1860
+ auth_task = asyncio.create_task(sync_service.authenticate(show_instructions=True))
1861
+ progress_task = asyncio.create_task(show_auth_progress())
1862
+
1863
+ # Wait for authentication to complete, with timeout
1864
+ success = await asyncio.wait_for(auth_task, timeout=120.0) # 2 minutes for initial testing
1865
+ progress_task.cancel() # Stop the progress updates
1866
+
1867
+ auth_duration = asyncio.get_event_loop().time() - auth_start_time
1868
+ print(f'🔧 Debug: Authentication returned: {success} (took {auth_duration:.1f}s)')
1869
+
1870
+ except TimeoutError:
1871
+ print('⏱️ Authentication timed out after 2 minutes.')
1872
+ print(' Checking if authentication completed in background...')
1873
+
1874
+ # Create a fresh auth client to check current status
1875
+ fresh_auth_client = DeviceAuthClient()
1876
+ print('🔧 Debug: Fresh auth client check:')
1877
+ print(f' API Token: {"✅ Present" if fresh_auth_client.api_token else "❌ Missing"}')
1878
+ print(f' Is Authenticated: {fresh_auth_client.is_authenticated}')
1879
+
1880
+ if fresh_auth_client.is_authenticated:
1881
+ print('✅ Authentication was successful!')
1882
+ success = True
1883
+ # Update the sync service's auth client
1884
+ sync_service.auth_client = fresh_auth_client
1885
+ else:
1886
+ print('❌ Authentication not completed. Please try again.')
1887
+ success = False
1888
+ except Exception as e:
1889
+ print(f'❌ Authentication error: {type(e).__name__}: {e}')
1890
+ import traceback
1891
+
1892
+ print(f'📄 Full traceback: {traceback.format_exc()}')
1893
+ success = False
1894
+
1895
+ if success:
1896
+ # 4. Send step event to show progress (like main branch during execution)
1897
+ # Use the sync service's auth client which has the updated user_id
1898
+ step_event = CreateAgentStepEvent(
1899
+ # Remove explicit ID - let it auto-generate to avoid backend validation issues
1900
+ user_id=auth_client.temp_user_id, # Use same temp user_id as task for consistency
1901
+ device_id=auth_client.device_id, # Use consistent device_id
1902
+ agent_task_id=task_id,
1903
+ step=1,
1904
+ actions=[
1905
+ {
1906
+ 'click': {
1907
+ 'coordinate': [800, 400],
1908
+ 'description': 'Click on Star button',
1909
+ 'success': True,
1910
+ },
1911
+ 'done': {
1912
+ 'success': True,
1913
+ 'text': '⭐ Starred browser-use/browser-use repository! Welcome to the community!',
1914
+ },
1915
+ }
1916
+ ],
1917
+ next_goal='⭐ Star browser-use GitHub repository to join the community',
1918
+ evaluation_previous_goal='Authentication completed successfully',
1919
+ memory='User authenticated with Browser Use Cloud and is now part of the community',
1920
+ screenshot_url=None,
1921
+ url='https://github.com/browser-use/browser-use',
1922
+ )
1923
+ print('📤 Sending dummy step event...')
1924
+ await sync_service.handle_event(step_event)
1925
+
1926
+ # Small delay to ensure step is processed before completion
1927
+ await asyncio.sleep(0.5)
1928
+
1929
+ # 5. Complete task (like main branch does at end)
1930
+ completion_event = UpdateAgentTaskEvent(
1931
+ id=task_id,
1932
+ user_id=auth_client.temp_user_id, # Use same temp user_id as task for consistency
1933
+ device_id=auth_client.device_id, # Use consistent device_id
1934
+ done_output="🎉 Welcome to Browser Use! You're now authenticated and part of our community. ⭐ Your future tasks will sync to the cloud automatically.",
1935
+ user_feedback_type=None,
1936
+ user_comment=None,
1937
+ gif_url=None,
1938
+ )
1939
+ await sync_service.handle_event(completion_event)
1940
+
1941
+ print('🎉 Authentication successful!')
1942
+ print(' Future browser-use runs will now sync to the cloud.')
1943
+ else:
1944
+ # Failed - still complete the task with failure message
1945
+ completion_event = UpdateAgentTaskEvent(
1946
+ id=task_id,
1947
+ user_id=auth_client.temp_user_id, # Still temp user since auth failed
1948
+ device_id=auth_client.device_id,
1949
+ done_output='❌ Authentication failed. Please try again.',
1950
+ user_feedback_type=None,
1951
+ user_comment=None,
1952
+ gif_url=None,
1953
+ )
1954
+ await sync_service.handle_event(completion_event)
1955
+
1956
+ print('❌ Authentication failed.')
1957
+ print(' Please try again or check your internet connection.')
1958
+
1959
+ except Exception as e:
1960
+ print(f'❌ Authentication error: {e}')
1961
+ # Still try to complete the task in UI with error message
1962
+ if task_id and sync_service:
1963
+ try:
1964
+ from browser_use.agent.cloud_events import UpdateAgentTaskEvent
1965
+
1966
+ completion_event = UpdateAgentTaskEvent(
1967
+ id=task_id,
1968
+ user_id=auth_client.temp_user_id,
1969
+ device_id=auth_client.device_id,
1970
+ done_output=f'❌ Authentication error: {e}',
1971
+ user_feedback_type=None,
1972
+ user_comment=None,
1973
+ gif_url=None,
1974
+ )
1975
+ await sync_service.handle_event(completion_event)
1976
+ except Exception:
1977
+ pass # Don't fail if we can't send the error event
1978
+ sys.exit(1)
1979
+
1980
+
1981
+ @click.group(invoke_without_command=True)
1982
+ @click.option('--version', is_flag=True, help='Print version and exit')
1983
+ @click.option(
1984
+ '--template',
1985
+ type=click.Choice(['default', 'advanced', 'tools'], case_sensitive=False),
1986
+ help='Generate a template file (default, advanced, or tools)',
1987
+ )
1988
+ @click.option('--output', '-o', type=click.Path(), help='Output file path for template (default: browser_use_<template>.py)')
1989
+ @click.option('--force', '-f', is_flag=True, help='Overwrite existing files without asking')
1990
+ @click.option('--model', type=str, help='Model to use (e.g., gpt-5-mini, claude-4-sonnet, gemini-2.5-flash)')
1991
+ @click.option('--debug', is_flag=True, help='Enable verbose startup logging')
1992
+ @click.option('--headless', is_flag=True, help='Run browser in headless mode', default=None)
1993
+ @click.option('--window-width', type=int, help='Browser window width')
1994
+ @click.option('--window-height', type=int, help='Browser window height')
1995
+ @click.option(
1996
+ '--user-data-dir', type=str, help='Path to Chrome user data directory (e.g. ~/Library/Application Support/Google/Chrome)'
1997
+ )
1998
+ @click.option('--profile-directory', type=str, help='Chrome profile directory name (e.g. "Default", "Profile 1")')
1999
+ @click.option('--cdp-url', type=str, help='Connect to existing Chrome via CDP URL (e.g. http://localhost:9222)')
2000
+ @click.option('--proxy-url', type=str, help='Proxy server for Chromium traffic (e.g. http://host:8080 or socks5://host:1080)')
2001
+ @click.option('--no-proxy', type=str, help='Comma-separated hosts to bypass proxy (e.g. localhost,127.0.0.1,*.internal)')
2002
+ @click.option('--proxy-username', type=str, help='Proxy auth username')
2003
+ @click.option('--proxy-password', type=str, help='Proxy auth password')
2004
+ @click.option('-p', '--prompt', type=str, help='Run a single task without the TUI (headless mode)')
2005
+ @click.option('--mcp', is_flag=True, help='Run as MCP server (exposes JSON RPC via stdin/stdout)')
2006
+ @click.pass_context
2007
+ def main(ctx: click.Context, debug: bool = False, **kwargs):
2008
+ """Browser Use - AI Agent for Web Automation
2009
+
2010
+ Run without arguments to start the interactive TUI.
2011
+
2012
+ Examples:
2013
+ uvx browser-use --template default
2014
+ uvx browser-use --template advanced --output my_script.py
2015
+ """
2016
+
2017
+ # Handle template generation
2018
+ if kwargs.get('template'):
2019
+ _run_template_generation(kwargs['template'], kwargs.get('output'), kwargs.get('force', False))
2020
+ return
2021
+
2022
+ if ctx.invoked_subcommand is None:
2023
+ # No subcommand, run the main interface
2024
+ run_main_interface(ctx, debug, **kwargs)
2025
+
2026
+
2027
+ def run_main_interface(ctx: click.Context, debug: bool = False, **kwargs):
2028
+ """Run the main browser-use interface"""
2029
+
2030
+ if kwargs['version']:
2031
+ from importlib.metadata import version
2032
+
2033
+ print(version('browser-use'))
2034
+ sys.exit(0)
2035
+
2036
+ # Check if MCP server mode is activated
2037
+ if kwargs.get('mcp'):
2038
+ # Capture telemetry for MCP server mode via CLI (suppress any logging from this)
2039
+ try:
2040
+ telemetry = ProductTelemetry()
2041
+ telemetry.capture(
2042
+ CLITelemetryEvent(
2043
+ version=get_browser_use_version(),
2044
+ action='start',
2045
+ mode='mcp_server',
2046
+ )
2047
+ )
2048
+ except Exception:
2049
+ # Ignore telemetry errors in MCP mode to prevent any stdout contamination
2050
+ pass
2051
+ # Run as MCP server
2052
+ from browser_use.mcp.server import main as mcp_main
2053
+
2054
+ asyncio.run(mcp_main())
2055
+ return
2056
+
2057
+ # Check if prompt mode is activated
2058
+ if kwargs.get('prompt'):
2059
+ # Set environment variable for prompt mode before running
2060
+ os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'result'
2061
+ # Run in non-interactive mode
2062
+ asyncio.run(run_prompt_mode(kwargs['prompt'], ctx, debug))
2063
+ return
2064
+
2065
+ # Configure console logging
2066
+ console_handler = logging.StreamHandler(sys.stdout)
2067
+ console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', '%H:%M:%S'))
2068
+
2069
+ # Configure root logger
2070
+ root_logger = logging.getLogger()
2071
+ root_logger.setLevel(logging.INFO if not debug else logging.DEBUG)
2072
+ root_logger.addHandler(console_handler)
2073
+
2074
+ logger = logging.getLogger('browser_use.startup')
2075
+ logger.info('Starting Browser-Use initialization')
2076
+ if debug:
2077
+ logger.debug(f'System info: Python {sys.version.split()[0]}, Platform: {sys.platform}')
2078
+
2079
+ logger.debug('Loading environment variables from .env file...')
2080
+ load_dotenv()
2081
+ logger.debug('Environment variables loaded')
2082
+
2083
+ # Load user configuration
2084
+ logger.debug('Loading user configuration...')
2085
+ try:
2086
+ config = load_user_config()
2087
+ logger.debug(f'User configuration loaded from {CONFIG.BROWSER_USE_CONFIG_FILE}')
2088
+ except Exception as e:
2089
+ logger.error(f'Error loading user configuration: {str(e)}', exc_info=True)
2090
+ print(f'Error loading configuration: {str(e)}')
2091
+ sys.exit(1)
2092
+
2093
+ # Update config with command-line arguments
2094
+ logger.debug('Updating configuration with command line arguments...')
2095
+ try:
2096
+ config = update_config_with_click_args(config, ctx)
2097
+ logger.debug('Configuration updated')
2098
+ except Exception as e:
2099
+ logger.error(f'Error updating config with command line args: {str(e)}', exc_info=True)
2100
+ print(f'Error updating configuration: {str(e)}')
2101
+ sys.exit(1)
2102
+
2103
+ # Save updated config
2104
+ logger.debug('Saving user configuration...')
2105
+ try:
2106
+ save_user_config(config)
2107
+ logger.debug('Configuration saved')
2108
+ except Exception as e:
2109
+ logger.error(f'Error saving user configuration: {str(e)}', exc_info=True)
2110
+ print(f'Error saving configuration: {str(e)}')
2111
+ sys.exit(1)
2112
+
2113
+ # Setup handlers for console output before entering Textual UI
2114
+ logger.debug('Setting up handlers for Textual UI...')
2115
+
2116
+ # Log browser and model configuration that will be used
2117
+ browser_type = 'Chromium' # BrowserSession only supports Chromium
2118
+ model_name = config.get('model', {}).get('name', 'auto-detected')
2119
+ headless = config.get('browser', {}).get('headless', False)
2120
+ headless_str = 'headless' if headless else 'visible'
2121
+
2122
+ logger.info(f'Preparing {browser_type} browser ({headless_str}) with {model_name} LLM')
2123
+
2124
+ try:
2125
+ # Run the Textual UI interface - now all the initialization happens before we go fullscreen
2126
+ logger.debug('Starting Textual UI interface...')
2127
+ asyncio.run(textual_interface(config))
2128
+ except Exception as e:
2129
+ # Restore console logging for error reporting
2130
+ root_logger.setLevel(logging.INFO)
2131
+ for handler in root_logger.handlers:
2132
+ root_logger.removeHandler(handler)
2133
+ root_logger.addHandler(console_handler)
2134
+
2135
+ logger.error(f'Error initializing Browser-Use: {str(e)}', exc_info=debug)
2136
+ print(f'\nError launching Browser-Use: {str(e)}')
2137
+ if debug:
2138
+ import traceback
2139
+
2140
+ traceback.print_exc()
2141
+ sys.exit(1)
2142
+
2143
+
2144
+ @main.command()
2145
+ def auth():
2146
+ """Authenticate with Browser Use Cloud to sync your runs"""
2147
+ asyncio.run(run_auth_command())
2148
+
2149
+
2150
+ @main.command()
2151
+ def install():
2152
+ """Install Chromium browser with system dependencies"""
2153
+ import platform
2154
+ import subprocess
2155
+
2156
+ print('📦 Installing Chromium browser + system dependencies...')
2157
+ print('⏳ This may take a few minutes...\n')
2158
+
2159
+ # Build command - only use --with-deps on Linux (it fails on Windows/macOS)
2160
+ cmd = ['uvx', 'playwright', 'install', 'chromium']
2161
+ if platform.system() == 'Linux':
2162
+ cmd.append('--with-deps')
2163
+ cmd.append('--no-shell')
2164
+
2165
+ result = subprocess.run(cmd)
2166
+
2167
+ if result.returncode == 0:
2168
+ print('\n✅ Installation complete!')
2169
+ print('🚀 Ready to use! Run: uvx browser-use')
2170
+ else:
2171
+ print('\n❌ Installation failed')
2172
+ sys.exit(1)
2173
+
2174
+
2175
+ # ============================================================================
2176
+ # Template Generation - Generate template files
2177
+ # ============================================================================
2178
+
2179
+ # Template metadata
2180
+ INIT_TEMPLATES = {
2181
+ 'default': {
2182
+ 'file': 'default_template.py',
2183
+ 'description': 'Simplest setup - capable of any web task with minimal configuration',
2184
+ },
2185
+ 'advanced': {
2186
+ 'file': 'advanced_template.py',
2187
+ 'description': 'All configuration options shown with defaults',
2188
+ },
2189
+ 'tools': {
2190
+ 'file': 'tools_template.py',
2191
+ 'description': 'Custom action examples - extend the agent with your own functions',
2192
+ },
2193
+ }
2194
+
2195
+
2196
+ def _run_template_generation(template: str, output: str | None, force: bool):
2197
+ """Generate a template file (called from main CLI)."""
2198
+ # Determine output path
2199
+ if output:
2200
+ output_path = Path(output)
2201
+ else:
2202
+ output_path = Path.cwd() / f'browser_use_{template}.py'
2203
+
2204
+ # Read template file
2205
+ try:
2206
+ templates_dir = Path(__file__).parent / 'cli_templates'
2207
+ template_file = INIT_TEMPLATES[template]['file']
2208
+ template_path = templates_dir / template_file
2209
+ content = template_path.read_text(encoding='utf-8')
2210
+ except Exception as e:
2211
+ click.echo(f'❌ Error reading template: {e}', err=True)
2212
+ sys.exit(1)
2213
+
2214
+ # Write file
2215
+ if _write_init_file(output_path, content, force):
2216
+ click.echo(f'✅ Created {output_path}')
2217
+ click.echo('\nNext steps:')
2218
+ click.echo(' 1. Install browser-use:')
2219
+ click.echo(' uv pip install browser-use')
2220
+ click.echo(' 2. Set up your API key in .env file or environment:')
2221
+ click.echo(' BROWSER_USE_API_KEY=your-key')
2222
+ click.echo(' (Get your key at https://cloud.browser-use.com/new-api-key)')
2223
+ click.echo(' 3. Run your script:')
2224
+ click.echo(f' python {output_path.name}')
2225
+ else:
2226
+ sys.exit(1)
2227
+
2228
+
2229
+ def _write_init_file(output_path: Path, content: str, force: bool = False) -> bool:
2230
+ """Write content to a file, with safety checks."""
2231
+ # Check if file already exists
2232
+ if output_path.exists() and not force:
2233
+ click.echo(f'⚠️ File already exists: {output_path}')
2234
+ if not click.confirm('Overwrite?', default=False):
2235
+ click.echo('❌ Cancelled')
2236
+ return False
2237
+
2238
+ # Ensure parent directory exists
2239
+ output_path.parent.mkdir(parents=True, exist_ok=True)
2240
+
2241
+ # Write file
2242
+ try:
2243
+ output_path.write_text(content, encoding='utf-8')
2244
+ return True
2245
+ except Exception as e:
2246
+ click.echo(f'❌ Error writing file: {e}', err=True)
2247
+ return False
2248
+
2249
+
2250
+ @main.command('init')
2251
+ @click.option(
2252
+ '--template',
2253
+ '-t',
2254
+ type=click.Choice(['default', 'advanced', 'tools'], case_sensitive=False),
2255
+ help='Template to use',
2256
+ )
2257
+ @click.option(
2258
+ '--output',
2259
+ '-o',
2260
+ type=click.Path(),
2261
+ help='Output file path (default: browser_use_<template>.py)',
2262
+ )
2263
+ @click.option(
2264
+ '--force',
2265
+ '-f',
2266
+ is_flag=True,
2267
+ help='Overwrite existing files without asking',
2268
+ )
2269
+ @click.option(
2270
+ '--list',
2271
+ '-l',
2272
+ 'list_templates',
2273
+ is_flag=True,
2274
+ help='List available templates',
2275
+ )
2276
+ def init(
2277
+ template: str | None,
2278
+ output: str | None,
2279
+ force: bool,
2280
+ list_templates: bool,
2281
+ ):
2282
+ """
2283
+ Generate a browser-use template file to get started quickly.
2284
+
2285
+ Examples:
2286
+
2287
+ \b
2288
+ # Interactive mode - prompts for template selection
2289
+ uvx browser-use init
2290
+
2291
+ \b
2292
+ # Generate default template
2293
+ uvx browser-use init --template default
2294
+
2295
+ \b
2296
+ # Generate advanced template with custom filename
2297
+ uvx browser-use init --template advanced --output my_script.py
2298
+
2299
+ \b
2300
+ # List available templates
2301
+ uvx browser-use init --list
2302
+ """
2303
+
2304
+ # Handle --list flag
2305
+ if list_templates:
2306
+ click.echo('Available templates:\n')
2307
+ for name, info in INIT_TEMPLATES.items():
2308
+ click.echo(f' {name:12} - {info["description"]}')
2309
+ return
2310
+
2311
+ # Interactive template selection if not provided
2312
+ if not template:
2313
+ click.echo('Available templates:\n')
2314
+ for name, info in INIT_TEMPLATES.items():
2315
+ click.echo(f' {name:12} - {info["description"]}')
2316
+ click.echo()
2317
+
2318
+ template = click.prompt(
2319
+ 'Which template would you like to use?',
2320
+ type=click.Choice(['default', 'advanced', 'tools'], case_sensitive=False),
2321
+ default='default',
2322
+ )
2323
+
2324
+ # Template is guaranteed to be set at this point (either from option or prompt)
2325
+ assert template is not None
2326
+
2327
+ # Determine output path
2328
+ if output:
2329
+ output_path = Path(output)
2330
+ else:
2331
+ output_path = Path.cwd() / f'browser_use_{template}.py'
2332
+
2333
+ # Read template file
2334
+ try:
2335
+ templates_dir = Path(__file__).parent / 'cli_templates'
2336
+ template_file = INIT_TEMPLATES[template]['file']
2337
+ template_path = templates_dir / template_file
2338
+ content = template_path.read_text(encoding='utf-8')
2339
+ except Exception as e:
2340
+ click.echo(f'❌ Error reading template: {e}', err=True)
2341
+ sys.exit(1)
2342
+
2343
+ # Write file
2344
+ if _write_init_file(output_path, content, force):
2345
+ click.echo(f'✅ Created {output_path}')
2346
+ click.echo('\nNext steps:')
2347
+ click.echo(' 1. Install browser-use:')
2348
+ click.echo(' uv pip install browser-use')
2349
+ click.echo(' 2. Set up your API key in .env file or environment:')
2350
+ click.echo(' BROWSER_USE_API_KEY=your-key')
2351
+ click.echo(' (Get your key at https://cloud.browser-use.com/new-api-key)')
2352
+ click.echo(' 3. Run your script:')
2353
+ click.echo(f' python {output_path.name}')
2354
+ else:
2355
+ sys.exit(1)
2356
+
2357
+
2358
+ if __name__ == '__main__':
2359
+ main()