From 7fd4a38a2b7cfe72dc3d6a15e009c96cebeee490 Mon Sep 17 00:00:00 2001 From: Krishna Date: Thu, 27 Nov 2025 20:41:36 -0600 Subject: [PATCH 1/9] Update quickstart.mdx --- docs/quickstart.mdx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index 9d8e59a3b..560663921 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -143,15 +143,21 @@ Sandboxes are the **easiest way to run Browser-Use in production**. We handle ag To run in production with authentication, just add `@sandbox` to your function: ```python +import asyncio from browser_use import Browser, sandbox, ChatBrowserUse from browser_use.agent.service import Agent @sandbox(cloud_profile_id='your-profile-id') async def production_task(browser: Browser): - agent = Agent(task="Your authenticated task", browser=browser, llm=ChatBrowserUse()) + agent = Agent( + task="Your authenticated task", + browser=browser, + llm=ChatBrowserUse(), + ) await agent.run() -await production_task() +if __name__ == "__main__": + asyncio.run(production_task()) ``` See [Going to Production](/production) for how to sync your cookies to the cloud. From 02cf38e864e1a095310e0abbbeea062b13d8a148 Mon Sep 17 00:00:00 2001 From: Mert Unsal Date: Fri, 28 Nov 2025 04:00:22 -0800 Subject: [PATCH 2/9] Revert "allowing MUCH MORE with coordinates" --- browser_use/agent/message_manager/views.py | 2 +- browser_use/agent/prompts.py | 9 +- browser_use/agent/service.py | 12 +- browser_use/agent/system_prompt.md | 2 +- browser_use/agent/system_prompt_anthropic.md | 34 ------ .../system_prompt_anthropic_no_thinking.md | 33 ------ .../agent/system_prompt_flash_anthropic.md | 2 - browser_use/browser/events.py | 11 -- .../watchdogs/default_action_watchdog.py | 41 ------- browser_use/tools/service.py | 108 ++++-------------- browser_use/tools/views.py | 10 +- pyproject.toml | 3 - 12 files changed, 31 insertions(+), 236 deletions(-) delete mode 100644 browser_use/agent/system_prompt_anthropic.md delete mode 100644 browser_use/agent/system_prompt_anthropic_no_thinking.md diff --git a/browser_use/agent/message_manager/views.py b/browser_use/agent/message_manager/views.py index 3f8fe7bc0..88076ba52 100644 --- a/browser_use/agent/message_manager/views.py +++ b/browser_use/agent/message_manager/views.py @@ -32,7 +32,7 @@ class HistoryItem(BaseModel): def to_string(self) -> str: """Get string representation of the history item""" - step_str = f'step_{self.step_number}' if self.step_number is not None else 'step_unknown' + step_str = 'step' if self.step_number is not None else 'step_unknown' if self.error: return f"""<{step_str}> diff --git a/browser_use/agent/prompts.py b/browser_use/agent/prompts.py index 3a002f08b..b2729b248 100644 --- a/browser_use/agent/prompts.py +++ b/browser_use/agent/prompts.py @@ -43,13 +43,8 @@ class SystemPrompt: """Load the prompt template from the markdown file.""" try: # Choose the appropriate template based on flash_mode, use_thinking, and is_anthropic - if self.is_anthropic: - if self.flash_mode: - template_filename = 'system_prompt_flash_anthropic.md' - elif self.use_thinking: - template_filename = 'system_prompt_anthropic.md' - else: - template_filename = 'system_prompt_no_thinking.md' + if self.flash_mode and self.is_anthropic: + template_filename = 'system_prompt_flash_anthropic.md' elif self.flash_mode: template_filename = 'system_prompt_flash.md' elif self.use_thinking: diff --git a/browser_use/agent/service.py b/browser_use/agent/service.py index df3dc6c63..8b36d6a64 100644 --- a/browser_use/agent/service.py +++ b/browser_use/agent/service.py @@ -213,16 +213,12 @@ class Agent(Generic[Context, AgentStructuredOutput]): if llm.provider == 'browser-use': flash_mode = True - # Auto-configure llm_screenshot_size for specific models + # Auto-configure llm_screenshot_size for Claude Sonnet models if llm_screenshot_size is None: model_name = getattr(llm, 'model', '') - if isinstance(model_name, str): - if model_name.startswith('claude-sonnet'): - llm_screenshot_size = (1400, 850) - logger.info('🖼️ Auto-configured LLM screenshot size for Claude Sonnet: 1400x850') - elif 'gemini' in model_name.lower(): - llm_screenshot_size = (1024, 720) - logger.info('🖼️ Auto-configured LLM screenshot size for Gemini: 1024x720') + if isinstance(model_name, str) and model_name.startswith('claude-sonnet'): + llm_screenshot_size = (1400, 850) + logger.info('🖼️ Auto-configured LLM screenshot size for Claude Sonnet: 1400x850') if page_extraction_llm is None: page_extraction_llm = llm diff --git a/browser_use/agent/system_prompt.md b/browser_use/agent/system_prompt.md index d8f2c7c59..058849cf2 100644 --- a/browser_use/agent/system_prompt.md +++ b/browser_use/agent/system_prompt.md @@ -178,7 +178,7 @@ You must ALWAYS respond with a valid JSON in this exact format: "thinking": "A structured -style reasoning block that applies the provided above.", "evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.", "memory": "1-3 sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.", - "next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.", + "next_goal": "State the next immediate goal and action to achieve it, in one clear sentence." "action":[{{"navigate": {{ "url": "url_value"}}}}, // ... more actions in sequence] }} Action list should NEVER be empty. diff --git a/browser_use/agent/system_prompt_anthropic.md b/browser_use/agent/system_prompt_anthropic.md deleted file mode 100644 index fdd20df93..000000000 --- a/browser_use/agent/system_prompt_anthropic.md +++ /dev/null @@ -1,34 +0,0 @@ -You are an AI agent designed to operate in an iterative loop to automate browser tasks. Your ultimate goal is accomplishing the task provided in . - -User request is the ultimate objective. For tasks with specific instructions, follow each step. For open-ended tasks, plan your own approach. - - -Elements: [index]text. Only [indexed] are interactive. Indentation=child. *[=new. - - -PDFs are auto-downloaded to available_file_paths - use read_file to read the doc or scroll and look at screenshot. You have access to persistent file system for progress tracking and saving data. Long tasks >10 steps: use todo.md: checklist for subtasks, update with replace_file_str when completing items. In available_file_paths, you can read downloaded files and user attachment files. - - -You are allowed to use a maximum of {max_actions} actions per step. Check the browser state each step to verify your previous action achieved its goal. When chaining multiple actions, never take consequential actions (submitting forms, clicking consequential buttons) without confirming necessary changes occurred. - -Default to element indices for browser interaction. If the target index is missing or a prior index-based action failed, use screenshot coordinates instead—DOM extraction doesn't capture everything. Coordinate interaction is useful when DOM extraction fails such as interacting with Canvas, scrolling on sidebars, etc. - -You must call the AgentOutput tool with the following schema for the arguments: - -{{ - "thinking": "A structured -style reasoning block to analyze the current state, agent history, and plan the next goals. Analyze what happened in the last few steps.", - "evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.", - "memory": "1-3 sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.", - "next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.", - "action": [ - {{ - "action_name": {{ - "parameter1": "value1", - "parameter2": "value2" - }} - }} - ] -}} - -Action list should NEVER be empty. - diff --git a/browser_use/agent/system_prompt_anthropic_no_thinking.md b/browser_use/agent/system_prompt_anthropic_no_thinking.md deleted file mode 100644 index 09136a212..000000000 --- a/browser_use/agent/system_prompt_anthropic_no_thinking.md +++ /dev/null @@ -1,33 +0,0 @@ -You are an AI agent designed to operate in an iterative loop to automate browser tasks. Your ultimate goal is accomplishing the task provided in . - -User request is the ultimate objective. For tasks with specific instructions, follow each step. For open-ended tasks, plan your own approach. - - -Elements: [index]text. Only [indexed] are interactive. Indentation=child. *[=new. - - -PDFs are auto-downloaded to available_file_paths - use read_file to read the doc or scroll and look at screenshot. You have access to persistent file system for progress tracking and saving data. Long tasks >10 steps: use todo.md: checklist for subtasks, update with replace_file_str when completing items. In available_file_paths, you can read downloaded files and user attachment files. - - -You are allowed to use a maximum of {max_actions} actions per step. Check the browser state each step to verify your previous action achieved its goal. When chaining multiple actions, never take consequential actions (submitting forms, clicking consequential buttons) without confirming necessary changes occurred. - -Default to element indices for browser interaction. If the target index is missing or a prior index-based action failed, use screenshot coordinates instead—DOM extraction doesn't capture everything. Coordinate interaction is useful when DOM extraction fails such as interacting with Canvas, scrolling on sidebars, etc. - -You must call the AgentOutput tool with the following schema for the arguments: - -{{ - "evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.", - "memory": "1-3 sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.", - "next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.", - "action": [ - {{ - "action_name": {{ - "parameter1": "value1", - "parameter2": "value2" - }} - }} - ] -}} - -Action list should NEVER be empty. - diff --git a/browser_use/agent/system_prompt_flash_anthropic.md b/browser_use/agent/system_prompt_flash_anthropic.md index 82b3a65ec..01182c55b 100644 --- a/browser_use/agent/system_prompt_flash_anthropic.md +++ b/browser_use/agent/system_prompt_flash_anthropic.md @@ -10,8 +10,6 @@ PDFs are auto-downloaded to available_file_paths - use read_file to read the doc You are allowed to use a maximum of {max_actions} actions per step. Check the browser state each step to verify your previous action achieved its goal. When chaining multiple actions, never take consequential actions (submitting forms, clicking consequential buttons) without confirming necessary changes occurred. - -Default to element indices for browser interaction. If the target index is missing or a prior index-based action failed, use screenshot coordinates instead—DOM extraction doesn't capture everything. Coordinate interaction is useful when DOM extraction fails such as interacting with Canvas, scrolling on sidebars, etc. You must call the AgentOutput tool with the following schema for the arguments: diff --git a/browser_use/browser/events.py b/browser_use/browser/events.py index af99cf31a..4c40cb428 100644 --- a/browser_use/browser/events.py +++ b/browser_use/browser/events.py @@ -166,17 +166,6 @@ class ScrollEvent(ElementSelectedEvent[None]): event_timeout: float | None = Field(default_factory=lambda: _get_timeout('TIMEOUT_ScrollEvent', 8.0)) # seconds -class ScrollAtCoordinateEvent(BaseEvent[None]): - """Scroll at specific coordinates using mouse wheel.""" - - coordinate_x: int - coordinate_y: int - scroll_x: int = 0 # deltaX (positive=right, negative=left) - scroll_y: int = 0 # deltaY (positive=down, negative=up) - - event_timeout: float | None = _get_timeout('TIMEOUT_ScrollAtCoordinateEvent', 8.0) # seconds - - class SwitchTabEvent(BaseEvent[TargetID]): """Switch to a different tab.""" diff --git a/browser_use/browser/watchdogs/default_action_watchdog.py b/browser_use/browser/watchdogs/default_action_watchdog.py index 90745b968..a87664fe8 100644 --- a/browser_use/browser/watchdogs/default_action_watchdog.py +++ b/browser_use/browser/watchdogs/default_action_watchdog.py @@ -13,7 +13,6 @@ from browser_use.browser.events import ( GoBackEvent, GoForwardEvent, RefreshEvent, - ScrollAtCoordinateEvent, ScrollEvent, ScrollToTextEvent, SelectDropdownOptionEvent, @@ -384,46 +383,6 @@ class DefaultActionWatchdog(BaseWatchdog): except Exception as e: raise - async def on_ScrollAtCoordinateEvent(self, event: ScrollAtCoordinateEvent) -> None: - """Handle scroll at specific coordinates using CDP synthesizeScrollGesture.""" - # Check if we have a current target for scrolling - if not self.browser_session.agent_focus_target_id: - error_msg = 'No active target for scrolling' - raise BrowserError(error_msg) - - try: - # Get focused CDP session - cdp_session = await self.browser_session.get_or_create_cdp_session() - cdp_client = cdp_session.cdp_client - session_id = cdp_session.session_id - - # Convert scroll deltas to gesture distances - # Note: synthesizeScrollGesture uses opposite directions: - # - positive yDistance = scroll UP (opposite of mouseWheel deltaY) - # - positive xDistance = scroll LEFT (opposite of mouseWheel deltaX) - # So we negate the values to maintain the same behavior as before - params: dict[str, float] = { - 'x': float(event.coordinate_x), - 'y': float(event.coordinate_y), - } - if event.scroll_x != 0: - params['xDistance'] = float(-event.scroll_x) - if event.scroll_y != 0: - params['yDistance'] = float(-event.scroll_y) - - # Synthesize scroll gesture at the specified coordinates - await cdp_client.send.Input.synthesizeScrollGesture( - params=params, # type: ignore[arg-type] - session_id=session_id, - ) - - self.logger.debug( - f'📄 Scrolled at ({event.coordinate_x}, {event.coordinate_y}) by deltaX={event.scroll_x}, deltaY={event.scroll_y}' - ) - return None - except Exception as e: - raise - # ========== Implementation Methods ========== async def _check_element_occlusion(self, backend_node_id: int, x: float, y: float, cdp_session) -> bool: diff --git a/browser_use/tools/service.py b/browser_use/tools/service.py index 74590b0f9..95739f49b 100644 --- a/browser_use/tools/service.py +++ b/browser_use/tools/service.py @@ -19,7 +19,6 @@ from browser_use.browser.events import ( GetDropdownOptionsEvent, GoBackEvent, NavigateToUrlEvent, - ScrollAtCoordinateEvent, ScrollEvent, ScrollToTextEvent, SendKeysEvent, @@ -45,7 +44,6 @@ from browser_use.tools.views import ( NavigateAction, NoParamsAction, ScrollAction, - ScrollAtCoordinateAction, SearchAction, SelectDropdownOptionAction, SendKeysAction, @@ -62,7 +60,6 @@ logger = logging.getLogger(__name__) ClickElementEvent.model_rebuild() TypeTextEvent.model_rebuild() ScrollEvent.model_rebuild() -ScrollAtCoordinateEvent.model_rebuild() UploadFileEvent.model_rebuild() Context = TypeVar('Context') @@ -254,25 +251,6 @@ class Tools(Generic[Context]): return actual_x, actual_y return llm_x, llm_y - def _convert_llm_scroll_deltas_to_viewport( - llm_scroll_x: int, llm_scroll_y: int, browser_session: BrowserSession - ) -> tuple[int, int]: - """Convert scroll deltas from LLM screenshot size to original viewport size.""" - if browser_session.llm_screenshot_size and browser_session._original_viewport_size: - original_width, original_height = browser_session._original_viewport_size - llm_width, llm_height = browser_session.llm_screenshot_size - - # Scale scroll deltas using the same ratio as coordinates - actual_scroll_x = int((llm_scroll_x / llm_width) * original_width) - actual_scroll_y = int((llm_scroll_y / llm_height) * original_height) - - logger.info( - f'🔄 Scaling scroll deltas: LLM ({llm_scroll_x}, {llm_scroll_y}) @ {llm_width}x{llm_height} ' - f'→ Viewport ({actual_scroll_x}, {actual_scroll_y}) @ {original_width}x{original_height}' - ) - return actual_scroll_x, actual_scroll_y - return llm_scroll_x, llm_scroll_y - # Element Interaction Actions async def _click_by_coordinate(params: ClickElementAction, browser_session: BrowserSession) -> ActionResult: # Ensure coordinates are provided (type safety) @@ -374,7 +352,7 @@ class Tools(Generic[Context]): return ActionResult(error=error_msg) @self.registry.action( - 'Click element by index or coordinates', + 'Click element by index or coordinates. Prefer index over coordinates when possible. Either provide coordinates or index.', param_model=ClickElementAction, ) async def click(params: ClickElementAction, browser_session: BrowserSession): @@ -810,14 +788,25 @@ You will be given a query and the markdown of a webpage that has been filtered t raise RuntimeError(str(e)) @self.registry.action( - """Scroll by pages where one page = viewport height. Set down=True to scroll down, down=False to scroll up. Defaults to scrolling down one page.""", + """Scroll by pages. REQUIRED: down=True/False (True=scroll down, False=scroll up, default=True). Optional: pages=0.5-10.0 (default 1.0). Use index for scroll containers (dropdowns/custom UI). High pages (10) reaches bottom. Multi-page scrolls sequentially. Viewport-based height, fallback 1000px/page.""", param_model=ScrollAction, ) async def scroll(params: ScrollAction, browser_session: BrowserSession): try: - direction = 'down' if params.down else 'up' + # Look up the node from the selector map if index is provided + # Special case: index 0 means scroll the whole page (root/body element) + node = None + if params.index is not None and params.index != 0: + node = await browser_session.get_element_by_index(params.index) + if node is None: + # Element does not exist + msg = f'Element index {params.index} not found in browser state' + return ActionResult(error=msg) - # Get actual viewport height for scrolling + direction = 'down' if params.down else 'up' + target = f'element {params.index}' if params.index is not None and params.index != 0 else '' + + # Get actual viewport height for more accurate scrolling try: cdp_session = await browser_session.get_or_create_cdp_session() metrics = await cdp_session.cdp_client.send.Page.getLayoutMetrics(session_id=cdp_session.session_id) @@ -834,12 +823,6 @@ You will be given a query and the markdown of a webpage that has been filtered t viewport_height = 1000 # Fallback to 1000px logger.debug(f'Failed to get viewport height, using fallback 1000px: {e}') - # For reporting to LLM, use LLM screenshot height if available (so LLM's mental model matches) - if browser_session.llm_screenshot_size: - _, llm_viewport_height = browser_session.llm_screenshot_size - else: - llm_viewport_height = viewport_height - # For multiple pages (>=1.0), scroll one page at a time to ensure each scroll completes if params.pages >= 1.0: import asyncio @@ -857,7 +840,7 @@ You will be given a query and the markdown of a webpage that has been filtered t pixels = -pixels event = browser_session.event_bus.dispatch( - ScrollEvent(direction=direction, amount=abs(pixels), node=None) + ScrollEvent(direction=direction, amount=abs(pixels), node=node) ) await event await event.event_result(raise_if_any=True, raise_if_none=False) @@ -878,7 +861,7 @@ You will be given a query and the markdown of a webpage that has been filtered t pixels = -pixels event = browser_session.event_bus.dispatch( - ScrollEvent(direction=direction, amount=abs(pixels), node=None) + ScrollEvent(direction=direction, amount=abs(pixels), node=node) ) await event await event.event_result(raise_if_any=True, raise_if_none=False) @@ -888,19 +871,18 @@ You will be given a query and the markdown of a webpage that has been filtered t logger.warning(f'Fractional scroll failed: {e}') if params.pages == 1.0: - long_term_memory = f'Scrolled {direction} {llm_viewport_height}px' + long_term_memory = f'Scrolled {direction} {target} {viewport_height}px'.replace(' ', ' ') else: - long_term_memory = f'Scrolled {direction} {completed_scrolls:.1f} pages' + long_term_memory = f'Scrolled {direction} {target} {completed_scrolls:.1f} pages'.replace(' ', ' ') else: # For fractional pages <1.0, do single scroll pixels = int(params.pages * viewport_height) event = browser_session.event_bus.dispatch( - ScrollEvent(direction='down' if params.down else 'up', amount=pixels, node=None) + ScrollEvent(direction='down' if params.down else 'up', amount=pixels, node=node) ) await event await event.event_result(raise_if_any=True, raise_if_none=False) - llm_pixels = int(params.pages * llm_viewport_height) - long_term_memory = f'Scrolled {direction} {llm_pixels}px' + long_term_memory = f'Scrolled {direction} {target} {params.pages} pages'.replace(' ', ' ') msg = f'🔍 {long_term_memory}' logger.info(msg) @@ -910,54 +892,6 @@ You will be given a query and the markdown of a webpage that has been filtered t error_msg = 'Failed to execute scroll action.' return ActionResult(error=error_msg) - @self.registry.action( - 'Scroll at specific coordinates. Use when you need to scroll within a specific area (e.g., scrollable containers, maps, modals) not reachable via element index.', - param_model=ScrollAtCoordinateAction, - ) - async def scroll_at_coordinates(params: ScrollAtCoordinateAction, browser_session: BrowserSession): - try: - # Convert coordinates from LLM screenshot size to viewport size - actual_x, actual_y = _convert_llm_coordinates_to_viewport( - params.coordinate_x, params.coordinate_y, browser_session - ) - - # Convert scroll deltas from LLM screenshot size to viewport size - actual_scroll_x, actual_scroll_y = _convert_llm_scroll_deltas_to_viewport( - params.scroll_x, params.scroll_y, browser_session - ) - - # Dispatch scroll at coordinate event - event = browser_session.event_bus.dispatch( - ScrollAtCoordinateEvent( - coordinate_x=actual_x, - coordinate_y=actual_y, - scroll_x=actual_scroll_x, - scroll_y=actual_scroll_y, - ) - ) - await event - await event.event_result(raise_if_any=True, raise_if_none=False) - - # Build memory with scroll amounts - direction_parts = [] - if params.scroll_y > 0: - direction_parts.append(f'down {params.scroll_y}px') - elif params.scroll_y < 0: - direction_parts.append(f'up {abs(params.scroll_y)}px') - if params.scroll_x > 0: - direction_parts.append(f'right {params.scroll_x}px') - elif params.scroll_x < 0: - direction_parts.append(f'left {abs(params.scroll_x)}px') - scroll_desc = ' and '.join(direction_parts) if direction_parts else 'zero' - - memory = f'Scrolled {scroll_desc} at ({params.coordinate_x}, {params.coordinate_y})' - msg = f'🔍 {memory}' - logger.info(msg) - return ActionResult(extracted_content=msg, long_term_memory=memory) - except Exception as e: - logger.error(f'Failed to scroll at coordinates: {type(e).__name__}: {e}') - return ActionResult(error=f'Failed to scroll at ({params.coordinate_x}, {params.coordinate_y})') - @self.registry.action( '', param_model=SendKeysAction, diff --git a/browser_use/tools/views.py b/browser_use/tools/views.py index ac657388e..fc3eaea93 100644 --- a/browser_use/tools/views.py +++ b/browser_use/tools/views.py @@ -73,7 +73,8 @@ class CloseTabAction(BaseModel): class ScrollAction(BaseModel): down: bool = Field(default=True, description='down=True=scroll down, down=False scroll up') - pages: float = Field(default=1.0, description='0.5=half page, 1=full page, 2=two pages, etc.') + pages: float = Field(default=1.0, description='0.5=half page, 1=full page, 10=to bottom/top') + index: int | None = Field(default=None, description='Optional element index to scroll within specific container') class SendKeysAction(BaseModel): @@ -96,10 +97,3 @@ class GetDropdownOptionsAction(BaseModel): class SelectDropdownOptionAction(BaseModel): index: int text: str = Field(description='exact text/value') - - -class ScrollAtCoordinateAction(BaseModel): - coordinate_x: int = Field(description='Horizontal coordinate relative to viewport left edge') - coordinate_y: int = Field(description='Vertical coordinate relative to viewport top edge') - scroll_x: int = Field(default=0, description='Horizontal scroll delta (positive=right, negative=left)') - scroll_y: int = Field(default=0, description='Vertical scroll delta (positive=down, negative=up)') diff --git a/pyproject.toml b/pyproject.toml index d3c6bfe8a..34500544c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -166,9 +166,6 @@ include = [ "browser_use/agent/system_prompt_no_thinking.md", "browser_use/agent/system_prompt_flash.md", "browser_use/agent/system_prompt_flash_anthropic.md", - "browser-use/agent/system_prompt_anthropic_no_thinking.md", - "browser_use/agent/system_prompt_flash_anthropic.md", - "browser-use/agent/system_prompt_anthropic.md", "browser_use/code_use/system_prompt.md", "browser_use/cli_templates/*.py", "browser_use/py.typed", From 5c581c642ea32ed7085ccdacddcdc72683196988 Mon Sep 17 00:00:00 2001 From: AntonVishal Date: Sat, 29 Nov 2025 12:27:32 +0530 Subject: [PATCH 3/9] Add provider options to ChatVercel for enhanced model routing --- browser_use/llm/vercel/chat.py | 20 +++++++++++++++++--- docs/supported-models.mdx | 14 +++++++++++++- examples/models/vercel_ai_gateway.py | 19 +++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/browser_use/llm/vercel/chat.py b/browser_use/llm/vercel/chat.py index bba86a026..2d963d401 100644 --- a/browser_use/llm/vercel/chat.py +++ b/browser_use/llm/vercel/chat.py @@ -189,6 +189,8 @@ class ChatVercel(BaseChatModel): prompt-based JSON extraction. Auto-detects common reasoning models by default. timeout: Request timeout in seconds max_retries: Maximum number of retries for failed requests + provider_options: Provider routing options for the gateway. Use this to control which + providers are used and in what order. Example: {'gateway': {'order': ['vertex', 'anthropic']}} """ # Model configuration @@ -218,6 +220,7 @@ class ChatVercel(BaseChatModel): default_query: Mapping[str, object] | None = None http_client: httpx.AsyncClient | None = None _strict_response_validation: bool = False + provider_options: dict[str, Any] | None = None # Static @property @@ -382,6 +385,8 @@ class ChatVercel(BaseChatModel): model_params['max_tokens'] = self.max_tokens if self.top_p is not None: model_params['top_p'] = self.top_p + if self.provider_options: + model_params['extra_body'] = {'providerOptions': self.provider_options} if output_format is None: # Return string response @@ -400,11 +405,12 @@ class ChatVercel(BaseChatModel): else: is_google_model = self.model.startswith('google/') + is_anthropic_model = self.model.startswith('anthropic/') is_reasoning_model = self.reasoning_models and any( str(pattern).lower() in str(self.model).lower() for pattern in self.reasoning_models ) - if is_google_model or is_reasoning_model: + if is_google_model or is_anthropic_model or is_reasoning_model: modified_messages = [m.model_copy(deep=True) for m in messages] schema = SchemaOptimizer.create_gemini_optimized_schema(output_format) @@ -431,10 +437,14 @@ class ChatVercel(BaseChatModel): vercel_messages = VercelMessageSerializer.serialize_messages(modified_messages) + request_params = model_params.copy() + if self.provider_options: + request_params['extra_body'] = {'providerOptions': self.provider_options} + response = await self.get_client().chat.completions.create( model=self.model, messages=vercel_messages, - **model_params, + **request_params, ) content = response.choices[0].message.content if response.choices else None @@ -479,6 +489,10 @@ class ChatVercel(BaseChatModel): 'schema': schema, } + request_params = model_params.copy() + if self.provider_options: + request_params['extra_body'] = {'providerOptions': self.provider_options} + response = await self.get_client().chat.completions.create( model=self.model, messages=vercel_messages, @@ -486,7 +500,7 @@ class ChatVercel(BaseChatModel): json_schema=response_format_schema, type='json_schema', ), - **model_params, + **request_params, ) content = response.choices[0].message.content if response.choices else None diff --git a/docs/supported-models.mdx b/docs/supported-models.mdx index 4783402a9..37a851bc7 100644 --- a/docs/supported-models.mdx +++ b/docs/supported-models.mdx @@ -358,12 +358,24 @@ api_key = os.getenv('VERCEL_API_KEY') if not api_key: raise ValueError('VERCEL_API_KEY is not set') -# Use Vercel AI Gateway +# Basic usage llm = ChatVercel( model='openai/gpt-4o', api_key=api_key, ) +# With provider options - control which providers are used and in what order +# This will try Vertex AI first, then fall back to Anthropic if Vertex fails +llm_with_provider_options = ChatVercel( + model='anthropic/claude-sonnet-4', + api_key=api_key, + provider_options={ + 'gateway': { + 'order': ['vertex', 'anthropic'] # Try Vertex AI first, then Anthropic + } + }, +) + agent = Agent( task="Your task here", llm=llm diff --git a/examples/models/vercel_ai_gateway.py b/examples/models/vercel_ai_gateway.py index 977539a6c..8f2f15979 100644 --- a/examples/models/vercel_ai_gateway.py +++ b/examples/models/vercel_ai_gateway.py @@ -24,19 +24,38 @@ api_key = os.getenv('VERCEL_API_KEY') if not api_key: raise ValueError('VERCEL_API_KEY is not set') +# Basic usage llm = ChatVercel( model='openai/gpt-4o', api_key=api_key, ) +# Example with provider options - control which providers are used and in what order +# This will try Vertex AI first, then fall back to Anthropic if Vertex fails +llm_with_provider_options = ChatVercel( + model='anthropic/claude-sonnet-4', + api_key=api_key, + provider_options={ + 'gateway': { + 'order': ['vertex', 'anthropic'] # Try Vertex AI first, then Anthropic + } + }, +) + agent = Agent( task='Go to example.com and summarize the main content', llm=llm, ) +agent_with_provider_options = Agent( + task='Go to example.com and summarize the main content', + llm=llm_with_provider_options, +) + async def main(): await agent.run(max_steps=10) + await agent_with_provider_options.run(max_steps=10) if __name__ == '__main__': From a742645416c4a09f756fc340552fa5333c9439f0 Mon Sep 17 00:00:00 2001 From: Mert Unsal Date: Sat, 29 Nov 2025 13:09:20 -0800 Subject: [PATCH 4/9] Revert "Improve demo mode" --- browser_use/agent/service.py | 8 + browser_use/browser/demo_mode.py | 904 +++++++++-- browser_use/browser/demo_panel_scripts.py | 1691 --------------------- browser_use/browser/profile.py | 4 - browser_use/browser/session.py | 52 +- browser_use/browser/session_manager.py | 32 +- browser_use/code_use/service.py | 1 + pyproject.toml | 2 +- 8 files changed, 830 insertions(+), 1864 deletions(-) delete mode 100644 browser_use/browser/demo_panel_scripts.py diff --git a/browser_use/agent/service.py b/browser_use/agent/service.py index 8b36d6a64..cb96129e1 100644 --- a/browser_use/agent/service.py +++ b/browser_use/agent/service.py @@ -930,6 +930,8 @@ class Agent(Generic[Context, AgentStructuredOutput]): # Log step completion summary summary_message = self._log_step_completion_summary(self.step_start_time, self.state.last_result) + if summary_message: + await self._demo_mode_log(summary_message, 'info', {'step': self.state.n_steps}) # Save file system state after step completion self.save_file_system_state() @@ -1768,6 +1770,12 @@ class Agent(Generic[Context, AgentStructuredOutput]): if on_step_start is not None: await on_step_start(self) + await self._demo_mode_log( + f'Starting step {step + 1}/{max_steps}', + 'info', + {'step': step + 1, 'total_steps': max_steps}, + ) + self.logger.debug(f'🚶 Starting step {step + 1}/{max_steps}...') try: diff --git a/browser_use/browser/demo_mode.py b/browser_use/browser/demo_mode.py index 94743154d..3eb4eb2f5 100644 --- a/browser_use/browser/demo_mode.py +++ b/browser_use/browser/demo_mode.py @@ -5,47 +5,824 @@ from __future__ import annotations import asyncio import json import logging -from collections import deque from datetime import datetime, timezone from typing import Any -from browser_use.browser.demo_panel_scripts import get_full_panel_script, get_last_panel_script from browser_use.browser.session import BrowserSession +# Embedded JavaScript for demo panel (injected into browser pages) +_DEMO_PANEL_SCRIPT = r"""(function () { + // SESSION_ID_PLACEHOLDER will be replaced by DemoMode with actual session ID + const SESSION_ID = '__BROWSER_USE_SESSION_ID_PLACEHOLDER__'; + const EXCLUDE_ATTR = 'data-browser-use-exclude-' + SESSION_ID; + const PANEL_ID = 'browser-use-demo-panel'; + const STYLE_ID = 'browser-use-demo-panel-style'; + const STORAGE_KEY = '__browserUseDemoLogs__'; + const STORAGE_HTML_KEY = '__browserUseDemoLogsHTML__'; + const PANEL_STATE_KEY = '__browserUseDemoPanelState__'; + const TOGGLE_BUTTON_ID = 'browser-use-demo-toggle'; + const MAX_MESSAGES = 100; + const EXPANDED_IDS_KEY = '__browserUseExpandedEntries__'; + const LEVEL_ICONS = { + info: 'ℹ️', + action: '▶️', + thought: '💭', + success: '✅', + warning: '⚠️', + error: '❌', + }; + const LEVEL_LABELS = { + info: 'info', + action: 'action', + thought: 'thought', + success: 'success', + warning: 'warning', + error: 'error', + }; + + if (window.__browserUseDemoPanelLoaded) { + const existingPanel = document.getElementById(PANEL_ID); + if (!existingPanel) { + initializePanel(); + } + return; + } + window.__browserUseDemoPanelLoaded = true; + + const state = { + panel: null, + list: null, + messages: [], + isOpen: true, + toggleButton: null, + }; + state.messages = restoreMessages(); + + function initializePanel() { + console.log('Browser-use demo panel initialized with session ID:', SESSION_ID); + addStyles(); + state.isOpen = loadPanelState(); + state.panel = buildPanel(); + state.list = state.panel.querySelector('[data-role="log-list"]'); + appendToHost(state.panel); + state.toggleButton = buildToggleButton(); + appendToHost(state.toggleButton); + const savedWidth = loadPanelWidth(); + if (savedWidth) { + document.documentElement.style.setProperty('--browser-use-demo-panel-width', `${savedWidth}px`); + } + + if (!hydrateFromStoredMarkup()) { + state.messages.forEach((entry) => appendEntry(entry, false)); + } + attachCloseHandler(); + if (state.isOpen) { + openPanel(false); + } else { + closePanel(false); + } + adjustLayout(); + window.addEventListener('resize', debounce(adjustLayout, 150)); + } + + function appendToHost(node) { + if (!node) { + return; + } + + const host = document.body || document.documentElement; + if (!host.contains(node)) { + host.appendChild(node); + } + + if (!document.body) { + document.addEventListener( + 'DOMContentLoaded', + () => { + if (document.body && node.parentNode !== document.body) { + document.body.appendChild(node); + } + }, + { once: true } + ); + } + } + + function addStyles() { + if (document.getElementById(STYLE_ID)) { + return; + } + const style = document.createElement('style'); + style.id = STYLE_ID; + style.setAttribute(EXCLUDE_ATTR, 'true'); + style.textContent = ` + #${PANEL_ID} { + position: fixed; + top: 0; + right: 0; + width: var(--browser-use-demo-panel-width, 340px); + max-width: calc(100vw - 64px); + height: 100vh; + display: flex; + flex-direction: column; + background: #05070d; + color: #f8f9ff; + font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', monospace; + font-size: 13px; + line-height: 1.4; + box-shadow: -6px 0 25px rgba(0, 0, 0, 0.35); + z-index: 2147480000; + border-left: 1px solid rgba(255, 255, 255, 0.14); + backdrop-filter: blur(10px); + pointer-events: auto; + transform: translateX(0); + opacity: 1; + transition: transform 0.25s ease, opacity 0.25s ease; + } + + #${PANEL_ID}[data-open="false"] { + transform: translateX(110%); + opacity: 0; + pointer-events: none; + } + + #${PANEL_ID} .browser-use-demo-header { + padding: 16px 18px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.14); + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; + } + + #${PANEL_ID} .browser-use-demo-header h1 { + font-size: 15px; + text-transform: uppercase; + letter-spacing: 0.12em; + margin: 0; + color: #f8f9ff; + } + + #${PANEL_ID} .browser-use-badge { + font-size: 11px; + padding: 2px 10px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.4); + text-transform: uppercase; + letter-spacing: 0.08em; + color: #f8f9ff; + } + + #${PANEL_ID} .browser-use-logo img { + height: 36px; + } + + #${PANEL_ID} .browser-use-header-actions { + margin-left: auto; + display: flex; + align-items: center; + gap: 8px; + } + + #${PANEL_ID} .browser-use-close-btn { + width: 28px; + height: 28px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.05); + color: #f8f9ff; + cursor: pointer; + font-size: 16px; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease, border 0.2s ease; + } + + #${PANEL_ID} .browser-use-close-btn:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.35); + } + + #${PANEL_ID} .browser-use-demo-body { + flex: 1; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.3) transparent; + padding: 8px 0 12px; + } + + #${PANEL_ID} .browser-use-demo-body::-webkit-scrollbar { + width: 8px; + } + + #${PANEL_ID} .browser-use-demo-body::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.25); + border-radius: 999px; + } + + .browser-use-demo-entry { + display: flex; + gap: 12px; + padding: 10px 18px; + border-left: 2px solid transparent; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); + animation: browser-use-fade-in 0.25s ease; + background: #000000; + } + + .browser-use-demo-entry:last-child { + border-bottom-color: transparent; + } + + .browser-use-entry-icon { + font-size: 16px; + line-height: 1.2; + width: 20px; + } + + .browser-use-entry-content { + flex: 1; + min-width: 0; + } + + .browser-use-entry-meta { + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: white; + margin-bottom: 4px; + display: flex; + justify-content: space-between; + gap: 12px; + } + + .browser-use-entry-message { + margin: 0; + word-break: break-word; + font-size: 12px; + color: #f8f9ff; + display: flex; + flex-direction: column; + gap: 6px; + } + + .browser-use-markdown-content { + margin: 0; + line-height: 1.5; + } + + .browser-use-markdown-content p { + margin: 0 0 8px 0; + } + + .browser-use-markdown-content p:last-child { + margin-bottom: 0; + } + + .browser-use-markdown-content h1, + .browser-use-markdown-content h2, + .browser-use-markdown-content h3 { + margin: 8px 0 4px 0; + font-weight: 600; + color: #f8f9ff; + } + + .browser-use-markdown-content h1 { + font-size: 16px; + } + + .browser-use-markdown-content h2 { + font-size: 14px; + } + + .browser-use-markdown-content h3 { + font-size: 13px; + } + + .browser-use-markdown-content code { + background: rgba(255, 255, 255, 0.1); + padding: 2px 6px; + border-radius: 3px; + font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', monospace; + font-size: 11px; + color: #60a5fa; + } + + .browser-use-markdown-content pre { + background: rgba(0, 0, 0, 0.3); + padding: 8px 12px; + border-radius: 4px; + overflow-x: auto; + margin: 8px 0; + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .browser-use-markdown-content pre code { + background: transparent; + padding: 0; + color: #f8f9ff; + font-size: 11px; + white-space: pre; + } + + .browser-use-markdown-content ul, + .browser-use-markdown-content ol { + margin: 4px 0 4px 16px; + padding: 0; + } + + .browser-use-markdown-content li { + margin: 2px 0; + } + + .browser-use-markdown-content a { + color: #60a5fa; + text-decoration: underline; + } + + .browser-use-markdown-content a:hover { + color: #93c5fd; + } + + .browser-use-markdown-content strong { + font-weight: 600; + color: #f8f9ff; + } + + .browser-use-markdown-content em { + font-style: italic; + } + + .browser-use-demo-entry:not(.expanded) .browser-use-markdown-content { + max-height: 120px; + overflow: hidden; + mask-image: linear-gradient(to bottom, rgba(0,0,0,1), rgba(0,0,0,0)); + } + + .browser-use-entry-toggle { + align-self: flex-start; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #f8f9ff; + padding: 2px 10px; + font-size: 11px; + border-radius: 999px; + cursor: pointer; + } + + .browser-use-demo-entry.level-info { border-left-color: #60a5fa; } + .browser-use-demo-entry.level-action { border-left-color: #34d399; } + .browser-use-demo-entry.level-thought { border-left-color: #f97316; } + .browser-use-demo-entry.level-warning { border-left-color: #fbbf24; } + .browser-use-demo-entry.level-success { border-left-color: #22c55e; } + .browser-use-demo-entry.level-error { border-left-color: #f87171; } + + @keyframes browser-use-fade-in { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } + } + + @media (max-width: 1024px) { + #${PANEL_ID} { + font-size: 12px; + } + #${PANEL_ID} .browser-use-demo-header { + padding: 12px 16px 10px; + } + } + + #${TOGGLE_BUTTON_ID} { + position: fixed; + top: 20px; + right: 20px; + width: 44px; + height: 44px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(5, 7, 13, 0.92); + color: #f8f9ff; + font-size: 18px; + display: none; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 2147480001; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); + transition: transform 0.2s ease, background 0.2s ease; + } + + #${TOGGLE_BUTTON_ID}:hover { + transform: scale(1.05); + background: rgba(5, 7, 13, 0.98); + } + + #${TOGGLE_BUTTON_ID} img { + display: block; + width: 24px; + height: auto; + max-width: 100%; + max-height: 100%; + object-fit: contain; + pointer-events: none; + user-select: none; + } + `; + document.head.appendChild(style); + } + + function buildPanel() { + const panel = document.createElement('section'); + panel.id = PANEL_ID; + panel.setAttribute('role', 'complementary'); + panel.setAttribute('aria-label', 'Browser-use demo panel'); + panel.setAttribute(EXCLUDE_ATTR, 'true'); + + const header = document.createElement('header'); + header.className = 'browser-use-demo-header'; + const title = document.createElement('div'); + title.className = 'browser-use-logo'; + const logo = document.createElement('img'); + logo.src = 'https://raw.githubusercontent.com/browser-use/browser-use/main/static/browser-use-dark.png'; + logo.alt = 'Browser-use'; + logo.loading = 'lazy'; + title.appendChild(logo); + const actions = document.createElement('div'); + actions.className = 'browser-use-header-actions'; + const closeBtn = document.createElement('button'); + closeBtn.type = 'button'; + closeBtn.className = 'browser-use-close-btn'; + closeBtn.setAttribute(EXCLUDE_ATTR, 'true'); + closeBtn.setAttribute('aria-label', 'Hide demo panel'); + closeBtn.dataset.role = 'close-toggle'; + closeBtn.innerHTML = '×'; + actions.appendChild(closeBtn); + header.appendChild(title); + header.appendChild(actions); + + const body = document.createElement('div'); + body.className = 'browser-use-demo-body'; + body.setAttribute('data-role', 'log-list'); + + panel.appendChild(header); + panel.appendChild(body); + panel.setAttribute('data-open', 'true'); + return panel; + } + + function buildToggleButton() { + const button = document.createElement('button'); + button.id = TOGGLE_BUTTON_ID; + button.type = 'button'; + button.setAttribute(EXCLUDE_ATTR, 'true'); + button.setAttribute('aria-label', 'Open demo panel'); + const img = document.createElement('img'); + img.alt = 'Browser-use'; + img.loading = 'eager'; + // Use embedded SVG as data URI to avoid CSP issues + const logoSvg = ''; + img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(logoSvg); + button.appendChild(img); + button.addEventListener('click', () => openPanel(true)); + return button; + } + + function attachCloseHandler() { + const closeBtn = state.panel?.querySelector('[data-role="close-toggle"]'); + if (closeBtn) { + closeBtn.addEventListener('click', () => closePanel(true)); + } + } + + function openPanel(saveState = true) { + state.isOpen = true; + if (state.panel) { + state.panel.setAttribute('data-open', 'true'); + } + if (state.toggleButton) { + state.toggleButton.style.display = 'none'; + } + adjustLayout(); + if (saveState) { + persistPanelState(); + } + } + + function closePanel(saveState = true) { + state.isOpen = false; + if (state.panel) { + state.panel.setAttribute('data-open', 'false'); + } + document.body.style.marginRight = ''; + if (state.toggleButton) { + state.toggleButton.style.display = 'flex'; + } + if (saveState) { + persistPanelState(); + } + } + + function persistPanelState() { + try { + sessionStorage.setItem(PANEL_STATE_KEY, state.isOpen ? 'open' : 'closed'); + } catch (err) { + // Ignore storage errors + } + } + + function loadPanelState() { + try { + const stored = sessionStorage.getItem(PANEL_STATE_KEY); + if (!stored) return true; + return stored === 'open'; + } catch (err) { + return true; + } + } + + function adjustLayout() { + const width = computePanelWidth(); + document.documentElement.style.setProperty('--browser-use-demo-panel-width', `${width}px`); + if (state.isOpen) { + document.body.style.marginRight = `${width + 16}px`; + if (state.toggleButton) { + state.toggleButton.style.display = 'none'; + } + } else { + document.body.style.marginRight = ''; + if (state.toggleButton) { + state.toggleButton.style.display = 'flex'; + } + } + } + + function computePanelWidth() { + const viewport = Math.max(window.innerWidth, 320); + const maxAvailable = Math.max(220, viewport - 240); + const target = Math.min(380, Math.max(260, viewport * 0.3)); + const width = Math.max(220, Math.min(target, maxAvailable)); + try { + sessionStorage.setItem('__browserUsePanelWidth__', String(width)); + } catch { + // fallthrough + } + return width; + } + + function loadPanelWidth() { + try { + const saved = sessionStorage.getItem('__browserUsePanelWidth__'); + return saved ? Number(saved) : null; + } catch { + return null; + } + } + + function restoreMessages() { + try { + const raw = sessionStorage.getItem(STORAGE_KEY); + if (!raw) return []; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch (err) { + return []; + } + } + + function persistMessages() { + try { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state.messages.slice(-MAX_MESSAGES))); + if (state.list) { + sessionStorage.setItem(STORAGE_HTML_KEY, state.list.innerHTML); + } + } catch (err) { + // Ignore sessionStorage errors (private mode, etc.) + } + } + + function hydrateFromStoredMarkup() { + if (!state.list) return false; + try { + const html = sessionStorage.getItem(STORAGE_HTML_KEY); + if (html) { + state.list.innerHTML = html; + for (const entryNode of state.list.querySelectorAll('.browser-use-demo-entry')) { + const toggle = entryNode.querySelector('.browser-use-entry-toggle'); + if (toggle) { + toggle.addEventListener('click', () => + toggleEntryExpansion(entryNode, toggle, entryNode.getAttribute('data-id')) + ); + } + applyPersistedExpansion(entryNode); + } + state.list.scrollTop = state.list.scrollHeight; + return true; + } + } catch (err) { + // ignore hydration failures + } + return false; + } + + function normalizeEntry(detail) { + if (!detail) return null; + const entry = typeof detail === 'string' ? { message: detail } : { ...detail }; + entry.message = typeof entry.message === 'string' ? entry.message : JSON.stringify(entry.message ?? ''); + entry.level = (entry.level || 'info').toLowerCase(); + if (!LEVEL_ICONS[entry.level]) { + entry.level = 'info'; + } + + if (!entry.metadata || typeof entry.metadata !== 'object') { + entry.metadata = {}; + } + + entry.timestamp = entry.timestamp || new Date().toISOString(); + entry.id = entry.id || `${Date.now()}-${Math.random().toString(16).slice(2)}`; + return entry; + } + + function appendEntry(entry, shouldPersist = true) { + if (shouldPersist) { + state.messages.push(entry); + if (state.messages.length > MAX_MESSAGES) { + state.messages = state.messages.slice(-MAX_MESSAGES); + } + persistMessages(); + } + + if (!state.list) { + return; + } + + const node = createEntryNode(entry); + applyPersistedExpansion(node); + state.list.appendChild(node); + state.list.scrollTop = state.list.scrollHeight; + } + + function createEntryNode(entry) { + const row = document.createElement('article'); + row.className = `browser-use-demo-entry level-${entry.level}`; + row.setAttribute('data-id', entry.id); + + const icon = document.createElement('span'); + icon.className = 'browser-use-entry-icon'; + icon.textContent = LEVEL_ICONS[entry.level] || LEVEL_ICONS.info; + + const content = document.createElement('div'); + content.className = 'browser-use-entry-content'; + + const meta = document.createElement('div'); + meta.className = 'browser-use-entry-meta'; + const time = formatTime(entry.timestamp); + const label = LEVEL_LABELS[entry.level] || entry.level; + meta.innerHTML = `${time}${label}`; + + const messageWrapper = document.createElement('div'); + messageWrapper.className = 'browser-use-entry-message'; + const messageText = entry.message.trim(); + const messageHtml = messageText; + const message = document.createElement('div'); + message.className = 'browser-use-markdown-content'; + message.textContent = messageHtml; + messageWrapper.appendChild(message); + + if (messageText.length > 160) { + const toggle = document.createElement('button'); + toggle.type = 'button'; + toggle.className = 'browser-use-entry-toggle'; + toggle.setAttribute(EXCLUDE_ATTR, 'true'); + toggle.textContent = 'Expand'; + toggle.addEventListener('click', () => toggleEntryExpansion(row, toggle, entry.id)); + messageWrapper.appendChild(toggle); + } else { + row.classList.add('expanded'); + } + + content.appendChild(meta); + content.appendChild(messageWrapper); + row.appendChild(icon); + row.appendChild(content); + return row; + } + + function applyPersistedExpansion(node) { + if (!node) return; + try { + const expanded = new Set(JSON.parse(sessionStorage.getItem(EXPANDED_IDS_KEY) || '[]')); + const id = node.getAttribute('data-id'); + if (id && expanded.has(id)) { + node.classList.add('expanded'); + const toggle = node.querySelector('.browser-use-entry-toggle'); + if (toggle) { + toggle.textContent = 'Collapse'; + } + } + } catch { + // ignore + } + } + + function toggleEntryExpansion(row, toggle, entryId) { + if (!row) return; + const isExpanded = row.classList.toggle('expanded'); + if (toggle) { + toggle.textContent = isExpanded ? 'Collapse' : 'Expand'; + } + try { + const expanded = new Set(JSON.parse(sessionStorage.getItem(EXPANDED_IDS_KEY) || '[]')); + if (isExpanded) { + expanded.add(entryId); + } else { + expanded.delete(entryId); + } + sessionStorage.setItem(EXPANDED_IDS_KEY, JSON.stringify(Array.from(expanded))); + } catch { + // ignore persistence issues + } + } + + function formatTime(value) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return new Date().toLocaleTimeString(); + } + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + } + + function debounce(fn, delay) { + let frame; + return (...args) => { + cancelAnimationFrame(frame); + frame = requestAnimationFrame(() => fn.apply(null, args)); + }; + } + + function handleLogEvent(event) { + const entry = normalizeEntry(event?.detail); + if (!entry) return; + appendEntry(entry, true); + } + + const boot = () => { + if (window.__browserUseDemoPanelBootstrapped) { + return; + } + + const start = () => { + if (window.__browserUseDemoPanelBootstrapped) { + return; + } + if (!document.body) { + requestAnimationFrame(start); + return; + } + window.__browserUseDemoPanelBootstrapped = true; + initializePanel(); + }; + + start(); + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', boot, { once: true }); + } else { + boot(); + } + window.addEventListener('browser-use-log', handleLogEvent); +})(); +""" + class DemoMode: + """Encapsulates browser overlay injection and log broadcasting for demo mode.""" + VALID_LEVELS = {'info', 'action', 'thought', 'error', 'success', 'warning'} - MAX_BUFFERED_MESSAGES = 100 def __init__(self, session: BrowserSession): self.session = session self.logger = logging.getLogger(f'{__name__}.DemoMode') + self._script_identifier: str | None = None self._script_source: str | None = None self._panel_ready = False self._lock = asyncio.Lock() - self._script_identifiers: dict[str, str] = {} - self._message_buffer: deque[dict[str, Any]] = deque(maxlen=self.MAX_BUFFERED_MESSAGES) def reset(self) -> None: - self._script_source = None - self._script_identifiers.clear() - self._message_buffer.clear() + self._script_identifier = None self._panel_ready = False def _load_script(self) -> str: if self._script_source is None: - session_id = self.session.id - accent_color = '#fe750e' # Default accent color - display_mode = self.session.browser_profile.demo_mode_display + self._script_source = _DEMO_PANEL_SCRIPT - if display_mode == 'last': - self._script_source = get_last_panel_script(session_id, accent_color) - else: - self._script_source = get_full_panel_script(session_id, accent_color) - - self.logger.debug(f'Loaded {display_mode} mode script for session {session_id}') - - return self._script_source + # Replace placeholder with actual session ID + session_id = self.session.id + script_with_session_id = self._script_source.replace('__BROWSER_USE_SESSION_ID_PLACEHOLDER__', session_id) + self.logger.debug(f'Injecting session ID {session_id} into demo panel script') + return script_with_session_id async def ensure_ready(self) -> None: """Add init script and inject overlay into currently open pages.""" @@ -56,36 +833,15 @@ class DemoMode: async with self._lock: script = self._load_script() - target_ids = await self._get_relevant_target_ids() - for target_id in target_ids: - await self._ensure_script_for_target(target_id, script) + if self._script_identifier is None: + self._script_identifier = await self.session._cdp_add_init_script(script) + self.logger.debug('Added auto-injection script for demo overlay') + + await self._inject_into_open_pages(script) self._panel_ready = True self.logger.debug('Demo overlay injected successfully') - async def register_new_target(self, target_id: str) -> None: - """Ensure demo overlay is attached to a newly created target.""" - await self.refresh_target(target_id) - - async def refresh_target(self, target_id: str) -> None: - """Reinstate the overlay and replay logs for the given target.""" - if not self.session.browser_profile.demo_mode: - return - if self.session._cdp_client_root is None: - return - - async with self._lock: - script = self._load_script() - if target_id not in self._script_identifiers: - await self._ensure_script_for_target(target_id, script) - else: - await self._reinstate_target(target_id, script) - self._panel_ready = True - - def unregister_target(self, target_id: str) -> None: - """Stop tracking a target after it closes.""" - self._script_identifiers.pop(target_id, None) - async def send_log(self, message: str, level: str = 'info', metadata: dict[str, Any] | None = None) -> None: """Send a log entry to the in-browser panel.""" if not message or not self.session.browser_profile.demo_mode: @@ -105,17 +861,13 @@ class DemoMode: if level_value not in self.VALID_LEVELS: level_value = 'info' - metadata = dict(metadata) if metadata else {} - payload = { 'message': message, 'level': level_value, - 'metadata': metadata, + 'metadata': metadata or {}, 'timestamp': datetime.now(timezone.utc).isoformat(), } - self._message_buffer.append(payload) - script = self._build_event_expression(json.dumps(payload, ensure_ascii=False)) try: @@ -140,7 +892,7 @@ class DemoMode: }})(); """.strip() - async def _get_relevant_target_ids(self) -> list[str]: + async def _inject_into_open_pages(self, script: str) -> None: targets = await self.session._cdp_get_all_pages( # - intentional private access include_http=True, include_about=True, @@ -155,32 +907,12 @@ class DemoMode: target_ids = [t['targetId'] for t in targets] if not target_ids and self.session.agent_focus_target_id: target_ids = [self.session.agent_focus_target_id] - return target_ids - async def _ensure_script_for_target(self, target_id: str, script: str) -> None: - if target_id in self._script_identifiers: - return - - try: - identifier = await self.session._cdp_add_init_script(script, target_id=target_id) - self._script_identifiers[target_id] = identifier - except Exception as exc: - self.logger.debug(f'Failed to register demo overlay script for {target_id}: {exc}') - return - - await self._reinstate_target(target_id, script) - - async def _reinstate_target(self, target_id: str, script: str) -> None: - try: - await self._inject_into_target(target_id, script) - except Exception as exc: - self.logger.debug(f'Failed to inject demo overlay into {target_id}: {exc}') - return - - try: - await self._replay_buffer_to_target(target_id) - except Exception as exc: - self.logger.debug(f'Failed to replay demo logs into {target_id}: {exc}') + for target_id in target_ids: + try: + await self._inject_into_target(target_id, script) + except Exception as exc: + self.logger.debug(f'Failed to inject demo overlay into {target_id}: {exc}') async def _inject_into_target(self, target_id: str, script: str) -> None: session = await self.session.get_or_create_cdp_session(target_id=target_id, focus=False) @@ -188,23 +920,3 @@ class DemoMode: params={'expression': script, 'awaitPromise': False}, session_id=session.session_id, ) - - async def _replay_buffer_to_target(self, target_id: str) -> None: - if not self._message_buffer: - return - - try: - session = await self.session.get_or_create_cdp_session(target_id=target_id, focus=False) - except Exception as exc: - self.logger.debug(f'Cannot replay demo logs to {target_id}: {exc}') - return - - for payload in self._message_buffer: - script = self._build_event_expression(json.dumps(payload, ensure_ascii=False)) - try: - await session.cdp_client.send.Runtime.evaluate( - params={'expression': script, 'awaitPromise': False}, - session_id=session.session_id, - ) - except Exception as exc: - self.logger.debug(f'Failed to replay demo log to {target_id}: {exc}') diff --git a/browser_use/browser/demo_panel_scripts.py b/browser_use/browser/demo_panel_scripts.py deleted file mode 100644 index 7c115f30b..000000000 --- a/browser_use/browser/demo_panel_scripts.py +++ /dev/null @@ -1,1691 +0,0 @@ -"""Demo panel script generators for different display modes.""" - - -def get_full_panel_script(session_id: str, accent_color: str = '#fe750e') -> str: - """Generate JavaScript for the full side panel display mode.""" - script = r"""(function () { - // SESSION_ID_PLACEHOLDER will be replaced by DemoMode with actual session ID - const SESSION_ID = '__BROWSER_USE_SESSION_ID_PLACEHOLDER__'; - const EXCLUDE_ATTR = 'data-browser-use-exclude-' + SESSION_ID; - const PANEL_ID = 'browser-use-demo-panel'; - const STYLE_ID = 'browser-use-demo-panel-style'; - const STORAGE_KEY = '__browserUseDemoLogs__'; - const STORAGE_HTML_KEY = '__browserUseDemoLogsHTML__'; - const PANEL_STATE_KEY = '__browserUseDemoPanelState__'; - const TOGGLE_BUTTON_ID = 'browser-use-demo-toggle'; - const ACCENT_COLOR = '#fe750e'; - const FINAL_RESULT_KEY = '__browserUseDemoFinalResult__'; - const DEFAULT_LOGO_SVG = ''; - const MAX_MESSAGES = 100; - const EXPANDED_IDS_KEY = '__browserUseExpandedEntries__'; - const TOGGLE_POSITION_KEY = '__browserUseTogglePosition__'; - const LEVEL_LABELS = { - info: 'info', - action: 'action', - thought: 'thought', - success: 'success', - warning: 'warning', - error: 'error', - }; - - if (window.__browserUseDemoPanelLoaded) { - const existingPanel = document.getElementById(PANEL_ID); - if (!existingPanel) { - initializePanel(); - } - return; - } - window.__browserUseDemoPanelLoaded = true; - - const state = { - panel: null, - list: null, - messages: [], - isOpen: true, - toggleButton: null, - finalResult: loadFinalResult(), - finalResultSection: null, - finalResultContent: null, - }; - state.messages = restoreMessages(); - if (!state.finalResult) { - const storedFinal = findFinalResultInMessages(state.messages); - if (storedFinal) { - state.finalResult = storedFinal; - } - } - - function initializePanel() { - console.log('Browser-use demo panel initialized with session ID:', SESSION_ID); - addStyles(); - state.isOpen = loadPanelState(); - state.panel = buildPanel(); - state.list = state.panel.querySelector('[data-role="log-list"]'); - state.finalResultSection = state.panel.querySelector('[data-role="final-result"]'); - state.finalResultContent = state.panel.querySelector('[data-role="final-result-content"]'); - appendToHost(state.panel); - state.toggleButton = buildToggleButton(); - appendToHost(state.toggleButton); - updateFinalResultDisplay(state.finalResult || '', false); - const savedWidth = loadPanelWidth(); - if (savedWidth) { - document.documentElement.style.setProperty('--browser-use-demo-panel-width', `${savedWidth}px`); - } - - if (!hydrateFromStoredMarkup()) { - state.messages.forEach((entry) => appendEntry(entry, false)); - } - highlightLatestEntry(state.list?.lastElementChild || null); - attachCloseHandler(); - if (state.isOpen) { - openPanel(false); - } else { - closePanel(false); - } - adjustLayout(); - window.addEventListener('resize', debounce(adjustLayout, 150)); - } - - function appendToHost(node) { - if (!node) { - return; - } - - const host = document.body || document.documentElement; - if (!host.contains(node)) { - host.appendChild(node); - } - - if (!document.body) { - document.addEventListener( - 'DOMContentLoaded', - () => { - if (document.body && node.parentNode !== document.body) { - document.body.appendChild(node); - } - }, - { once: true } - ); - } - } - - function addStyles() { - if (document.getElementById(STYLE_ID)) { - return; - } - const style = document.createElement('style'); - style.id = STYLE_ID; - style.setAttribute(EXCLUDE_ATTR, 'true'); - style.textContent = ` - #${PANEL_ID} { - --browser-use-demo-accent: ${ACCENT_COLOR}; - position: fixed; - top: 0; - right: 0; - width: var(--browser-use-demo-panel-width, 340px); - max-width: calc(100vw - 64px); - height: 100vh; - display: flex; - flex-direction: column; - background: #000000; - color: #f8f9ff; - font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', monospace; - font-size: 13px; - line-height: 1.4; - box-shadow: -6px 0 25px rgba(0, 0, 0, 0.35); - z-index: 2147480000; - border-left: 1px solid rgba(255, 255, 255, 0.14); - backdrop-filter: blur(10px); - pointer-events: auto; - transform: translateX(0); - opacity: 1; - transition: transform 0.25s ease, opacity 0.25s ease; - } - - #${PANEL_ID}[data-open="false"] { - transform: translateX(110%); - opacity: 0; - pointer-events: none; - } - - #${PANEL_ID} .browser-use-demo-header { - padding: 16px 18px 12px; - border-bottom: 1px solid rgba(255, 255, 255, 0.14); - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 8px; - flex-wrap: wrap; - } - - #${PANEL_ID} .browser-use-demo-header h1 { - font-size: 15px; - text-transform: uppercase; - letter-spacing: 0.12em; - margin: 0; - color: #f8f9ff; - } - - #${PANEL_ID} .browser-use-badge { - font-size: 11px; - padding: 2px 10px; - border-radius: 999px; - border: 1px solid rgba(255, 255, 255, 0.4); - text-transform: uppercase; - letter-spacing: 0.08em; - color: #f8f9ff; - } - - #${PANEL_ID} .browser-use-logo svg { - height: 36px; - width: auto; - display: block; - } - - #${PANEL_ID} .browser-use-header-actions { - margin-left: auto; - display: flex; - align-items: center; - gap: 8px; - } - - #${PANEL_ID} .browser-use-final-result { - margin: 12px 18px 0; - padding: 14px 16px; - border-radius: 14px; - border: 1px solid rgba(255, 255, 255, 0.12); - background: radial-gradient(circle at top, rgba(255, 255, 255, 0.05), transparent 70%); - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04); - position: relative; - overflow: hidden; - display: none; - opacity: 0; - transform: translateY(-8px); - } - - #${PANEL_ID} .browser-use-final-result[data-visible="true"] { - display: block; - opacity: 1; - transform: translateY(0); - } - - #${PANEL_ID} .browser-use-final-result.is-revealed { - animation: browser-use-final-pop 0.65s cubic-bezier(0.23, 1, 0.32, 1); - opacity: 1; - transform: translateY(0); - } - - #${PANEL_ID} .browser-use-final-result::after { - content: ''; - position: absolute; - inset: 0; - border-radius: 14px; - background: linear-gradient(120deg, rgba(255, 117, 14, 0.18), transparent 60%); - opacity: 0.45; - pointer-events: none; - } - - #${PANEL_ID} .browser-use-final-result-label { - font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; - color: rgba(255, 255, 255, 0.82); - margin-bottom: 6px; - position: relative; - z-index: 2; - } - - #${PANEL_ID} .browser-use-final-result-body { - position: relative; - z-index: 2; - font-size: 12px; - color: #fefefe; - max-height: 180px; - overflow-y: auto; - padding-right: 6px; - white-space: pre-wrap; - word-break: break-word; - } - - #${PANEL_ID} .browser-use-final-result-body::-webkit-scrollbar { - width: 6px; - } - - #${PANEL_ID} .browser-use-final-result-body::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.25); - border-radius: 999px; - } - - #${PANEL_ID} .browser-use-close-btn { - width: 28px; - height: 28px; - border-radius: 50%; - border: 1px solid rgba(255, 255, 255, 0.2); - background: #000000; - color: #f8f9ff; - cursor: pointer; - font-size: 16px; - line-height: 1; - display: flex; - align-items: center; - justify-content: center; - transition: border 0.2s ease, box-shadow 0.2s ease; - } - - #${PANEL_ID} .browser-use-close-btn:hover { - border-color: rgba(255, 255, 255, 0.35); - box-shadow: 0 0 8px rgba(255, 255, 255, 0.2); - } - - #${PANEL_ID} .browser-use-demo-body { - flex: 1; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: rgba(255, 255, 255, 0.3) transparent; - padding: 8px 0 12px; - } - - #${PANEL_ID} .browser-use-demo-body::-webkit-scrollbar { - width: 8px; - } - - #${PANEL_ID} .browser-use-demo-body::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.25); - border-radius: 999px; - } - - .browser-use-demo-entry { - display: flex; - gap: 14px; - padding: 16px 22px; - border-bottom: 1px solid rgba(255, 255, 255, 0.04); - animation: browser-use-fade-in 0.25s ease; - background: #000000; - position: relative; - overflow: hidden; - } - - .browser-use-demo-entry:last-child { - border-bottom-color: transparent; - } - - .browser-use-entry-content { - flex: 1; - min-width: 0; - position: relative; - z-index: 2; - } - - .browser-use-entry-meta { - font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; - color: white; - margin-bottom: 4px; - display: flex; - justify-content: space-between; - gap: 12px; - } - - .browser-use-entry-message { - margin: 0; - word-break: break-word; - font-size: 12px; - color: #f8f9ff; - display: flex; - flex-direction: column; - gap: 6px; - } - - .browser-use-corner { - position: absolute; - width: min(24px, 12%); - height: min(24px, 30%); - border: 2px solid var(--browser-use-demo-accent); - pointer-events: none; - z-index: 1; - display: none; - opacity: 0.3; - } - - .browser-use-corner.corner-top-left { - top: 6px; - left: 6px; - border-right: none; - border-bottom: none; - } - - .browser-use-corner.corner-top-right { - top: 6px; - right: 6px; - border-left: none; - border-bottom: none; - } - - .browser-use-corner.corner-bottom-left { - bottom: 6px; - left: 6px; - border-right: none; - border-top: none; - } - - .browser-use-corner.corner-bottom-right { - bottom: 6px; - right: 6px; - border-left: none; - border-top: none; - } - - .browser-use-demo-entry.highlighted .browser-use-corner { - display: block; - animation: browser-use-corner-glow 1.8s ease-in-out infinite; - } - - .browser-use-demo-entry.highlighted { - animation: browser-use-highlight-in 0.45s ease; - } - - @keyframes browser-use-corner-glow { - 0% { opacity: 0.2; } - 50% { opacity: 0.85; } - 100% { opacity: 0.2; } - } - - @keyframes browser-use-highlight-in { - from { opacity: 0.65; } - to { opacity: 1; } - } - - .browser-use-markdown-content { - margin: 0; - line-height: 1.5; - } - - .browser-use-markdown-content p { - margin: 0 0 8px 0; - } - - .browser-use-markdown-content p:last-child { - margin-bottom: 0; - } - - .browser-use-markdown-content h1, - .browser-use-markdown-content h2, - .browser-use-markdown-content h3 { - margin: 8px 0 4px 0; - font-weight: 600; - color: #f8f9ff; - } - - .browser-use-markdown-content h1 { - font-size: 16px; - } - - .browser-use-markdown-content h2 { - font-size: 14px; - } - - .browser-use-markdown-content h3 { - font-size: 13px; - } - - .browser-use-markdown-content code { - background: rgba(255, 255, 255, 0.1); - padding: 2px 6px; - border-radius: 3px; - font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', monospace; - font-size: 11px; - color: #60a5fa; - } - - .browser-use-markdown-content pre { - background: rgba(0, 0, 0, 0.3); - padding: 8px 12px; - border-radius: 4px; - overflow-x: auto; - margin: 8px 0; - border: 1px solid rgba(255, 255, 255, 0.1); - } - - .browser-use-markdown-content pre code { - background: transparent; - padding: 0; - color: #f8f9ff; - font-size: 11px; - white-space: pre; - } - - .browser-use-markdown-content ul, - .browser-use-markdown-content ol { - margin: 4px 0 4px 16px; - padding: 0; - } - - .browser-use-markdown-content li { - margin: 2px 0; - } - - .browser-use-markdown-content a { - color: #60a5fa; - text-decoration: underline; - } - - .browser-use-markdown-content a:hover { - color: #93c5fd; - } - - .browser-use-markdown-content strong { - font-weight: 600; - color: #f8f9ff; - } - - .browser-use-markdown-content em { - font-style: italic; - } - - .browser-use-demo-entry:not(.expanded) .browser-use-markdown-content { - max-height: 150px; - overflow: hidden; - mask-image: linear-gradient(to bottom, rgba(0,0,0,1), rgba(0,0,0,0)); - -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,1), rgba(0,0,0,0)); - } - - .browser-use-demo-entry:not(.expanded) .browser-use-markdown-content.is-scrollable { - max-height: 220px; - overflow-y: auto; - mask-image: none; - -webkit-mask-image: none; - padding-right: 6px; - } - - .browser-use-markdown-content.is-scrollable::-webkit-scrollbar { - width: 6px; - } - - .browser-use-markdown-content.is-scrollable::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.25); - border-radius: 999px; - } - - .browser-use-entry-toggle { - align-self: flex-start; - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - color: #f8f9ff; - padding: 2px 10px; - font-size: 11px; - border-radius: 999px; - cursor: pointer; - } - - @keyframes browser-use-fade-in { - from { opacity: 0; transform: translateY(6px); } - to { opacity: 1; transform: translateY(0); } - } - - @keyframes browser-use-final-pop { - 0% { opacity: 0; transform: translateY(12px) scale(0.96); box-shadow: none; } - 60% { opacity: 1; transform: translateY(0) scale(1.02); box-shadow: 0 18px 32px rgba(0, 0, 0, 0.35); } - 100% { opacity: 1; transform: translateY(0) scale(1); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.35); } - } - - @media (max-width: 1024px) { - #${PANEL_ID} { - font-size: 12px; - } - #${PANEL_ID} .browser-use-demo-header { - padding: 12px 16px 10px; - } - } - - #${TOGGLE_BUTTON_ID} { - position: fixed; - bottom: 20px; - right: 20px; - width: 44px; - height: 44px; - border-radius: 50%; - border: 1px solid rgba(255, 255, 255, 0.2); - background: #000000; - color: #f8f9ff; - font-size: 18px; - display: none; - align-items: center; - justify-content: center; - cursor: grab; - z-index: 2147480001; - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); - transition: transform 0.2s ease, box-shadow 0.2s ease; - user-select: none; - } - - #${TOGGLE_BUTTON_ID}:hover { - transform: scale(1.05); - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.55); - } - - #${TOGGLE_BUTTON_ID}:active { - cursor: grabbing; - } - - #${TOGGLE_BUTTON_ID} img { - display: block; - width: 24px; - height: auto; - max-width: 100%; - max-height: 100%; - object-fit: contain; - pointer-events: none; - user-select: none; - } - `; - document.head.appendChild(style); - } - - function buildPanel() { - const panel = document.createElement('section'); - panel.id = PANEL_ID; - panel.setAttribute('role', 'complementary'); - panel.setAttribute('aria-label', 'Browser-use demo panel'); - panel.setAttribute(EXCLUDE_ATTR, 'true'); - - const header = document.createElement('header'); - header.className = 'browser-use-demo-header'; - const title = document.createElement('div'); - title.className = 'browser-use-logo'; - title.innerHTML = DEFAULT_LOGO_SVG; - const inlineSvg = title.querySelector('svg'); - if (inlineSvg) { - inlineSvg.setAttribute('role', 'img'); - inlineSvg.setAttribute('aria-label', 'Browser-use'); - } - const actions = document.createElement('div'); - actions.className = 'browser-use-header-actions'; - const closeBtn = document.createElement('button'); - closeBtn.type = 'button'; - closeBtn.className = 'browser-use-close-btn'; - closeBtn.setAttribute(EXCLUDE_ATTR, 'true'); - closeBtn.setAttribute('aria-label', 'Hide demo panel'); - closeBtn.dataset.role = 'close-toggle'; - closeBtn.innerHTML = '×'; - actions.appendChild(closeBtn); - header.appendChild(title); - header.appendChild(actions); - - const body = document.createElement('div'); - body.className = 'browser-use-demo-body'; - body.setAttribute('data-role', 'log-list'); - - panel.appendChild(header); - panel.appendChild(buildFinalResultSection()); - panel.appendChild(body); - panel.setAttribute('data-open', 'true'); - return panel; - } - - function buildFinalResultSection() { - const section = document.createElement('section'); - section.className = 'browser-use-final-result'; - section.setAttribute('data-role', 'final-result'); - section.setAttribute('aria-live', 'polite'); - section.dataset.visible = 'false'; - - const label = document.createElement('div'); - label.className = 'browser-use-final-result-label'; - label.textContent = 'Final Result'; - - const body = document.createElement('div'); - body.className = 'browser-use-final-result-body'; - body.setAttribute('data-role', 'final-result-content'); - - section.appendChild(label); - section.appendChild(body); - return section; - } - - function updateFinalResultDisplay(value, animate = true) { - if (!state.finalResultSection || !state.finalResultContent) { - return; - } - const finalValue = typeof value === 'string' ? value.trim() : ''; - const hasValue = Boolean(finalValue); - state.finalResultSection.dataset.visible = hasValue ? 'true' : 'false'; - if (!hasValue) { - state.finalResultContent.textContent = ''; - state.finalResultSection.classList.remove('is-revealed'); - return; - } - state.finalResultContent.textContent = finalValue; - if (animate) { - state.finalResultSection.classList.remove('is-revealed'); - void state.finalResultSection.offsetWidth; - state.finalResultSection.classList.add('is-revealed'); - } else { - state.finalResultSection.classList.remove('is-revealed'); - } - } - - function buildToggleButton() { - const button = document.createElement('button'); - button.id = TOGGLE_BUTTON_ID; - button.type = 'button'; - button.setAttribute(EXCLUDE_ATTR, 'true'); - button.setAttribute('aria-label', 'Open demo panel'); - const img = document.createElement('img'); - img.alt = 'Browser-use'; - img.loading = 'eager'; - img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(DEFAULT_LOGO_SVG); - button.appendChild(img); - - // Restore saved position - const savedPos = loadTogglePosition(); - if (savedPos) { - button.style.left = savedPos.x + 'px'; - button.style.top = savedPos.y + 'px'; - button.style.right = 'auto'; - button.style.bottom = 'auto'; - } - - // Drag functionality - let isDragging = false; - let dragStartX = 0; - let dragStartY = 0; - let initialX = 0; - let initialY = 0; - - button.addEventListener('mousedown', (e) => { - if (e.button !== 0) return; // Only left mouse button - isDragging = false; - const rect = button.getBoundingClientRect(); - dragStartX = e.clientX; - dragStartY = e.clientY; - initialX = rect.left; - initialY = rect.top; - - const handleMouseMove = (moveEvent) => { - const deltaX = moveEvent.clientX - dragStartX; - const deltaY = moveEvent.clientY - dragStartY; - const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - - if (distance > 5) { - isDragging = true; - } - - if (isDragging) { - const newX = initialX + deltaX; - const newY = initialY + deltaY; - - // Constrain to viewport - const maxX = window.innerWidth - rect.width; - const maxY = window.innerHeight - rect.height; - const constrainedX = Math.max(0, Math.min(newX, maxX)); - const constrainedY = Math.max(0, Math.min(newY, maxY)); - - button.style.left = constrainedX + 'px'; - button.style.top = constrainedY + 'px'; - button.style.right = 'auto'; - button.style.bottom = 'auto'; - } - }; - - const handleMouseUp = () => { - if (isDragging) { - const rect = button.getBoundingClientRect(); - saveTogglePosition(rect.left, rect.top); - } - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - }); - - // Click handler (only if not dragging) - button.addEventListener('click', (e) => { - if (!isDragging) { - openPanel(true); - } - isDragging = false; - }); - - return button; - } - - function attachCloseHandler() { - const closeBtn = state.panel?.querySelector('[data-role="close-toggle"]'); - if (closeBtn) { - closeBtn.addEventListener('click', () => closePanel(true)); - } - } - - function openPanel(saveState = true) { - state.isOpen = true; - if (state.panel) { - state.panel.setAttribute('data-open', 'true'); - } - if (state.toggleButton) { - state.toggleButton.style.display = 'none'; - } - adjustLayout(); - if (saveState) { - persistPanelState(); - } - } - - function closePanel(saveState = true) { - state.isOpen = false; - if (state.panel) { - state.panel.setAttribute('data-open', 'false'); - } - document.body.style.marginRight = ''; - if (state.toggleButton) { - state.toggleButton.style.display = 'flex'; - } - if (saveState) { - persistPanelState(); - } - } - - function persistPanelState() { - try { - sessionStorage.setItem(PANEL_STATE_KEY, state.isOpen ? 'open' : 'closed'); - } catch (err) { - // Ignore storage errors - } - } - - function loadPanelState() { - try { - const stored = sessionStorage.getItem(PANEL_STATE_KEY); - if (!stored) return true; - return stored === 'open'; - } catch (err) { - return false; - } - } - - function adjustLayout() { - const width = computePanelWidth(); - document.documentElement.style.setProperty('--browser-use-demo-panel-width', `${width}px`); - if (state.isOpen) { - document.body.style.marginRight = `${width + 16}px`; - if (state.toggleButton) { - state.toggleButton.style.display = 'none'; - } - } else { - document.body.style.marginRight = ''; - if (state.toggleButton) { - state.toggleButton.style.display = 'flex'; - } - } - } - - function computePanelWidth() { - const viewport = Math.max(window.innerWidth, 320); - const maxAvailable = Math.max(220, viewport - 240); - const target = Math.min(380, Math.max(260, viewport * 0.3)); - const width = Math.max(220, Math.min(target, maxAvailable)); - try { - sessionStorage.setItem('__browserUsePanelWidth__', String(width)); - } catch { - // fallthrough - } - return width; - } - - function loadPanelWidth() { - try { - const saved = sessionStorage.getItem('__browserUsePanelWidth__'); - return saved ? Number(saved) : null; - } catch { - return null; - } - } - - function saveTogglePosition(x, y) { - try { - localStorage.setItem(TOGGLE_POSITION_KEY, JSON.stringify({ x, y })); - } catch { - // Ignore storage errors - } - } - - function loadTogglePosition() { - try { - const saved = localStorage.getItem(TOGGLE_POSITION_KEY); - if (!saved) return null; - const parsed = JSON.parse(saved); - if (typeof parsed.x === 'number' && typeof parsed.y === 'number') { - return { x: parsed.x, y: parsed.y }; - } - } catch { - // Ignore parse errors - } - return null; - } - - function persistFinalResult(value) { - try { - if (value) { - sessionStorage.setItem(FINAL_RESULT_KEY, value); - } else { - sessionStorage.removeItem(FINAL_RESULT_KEY); - } - } catch { - // ignore storage errors - } - } - - function loadFinalResult() { - try { - return sessionStorage.getItem(FINAL_RESULT_KEY) || ''; - } catch { - return ''; - } - } - - function restoreMessages() { - try { - const raw = sessionStorage.getItem(STORAGE_KEY); - if (!raw) return []; - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : []; - } catch (err) { - return []; - } - } - - function persistMessages() { - try { - sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state.messages.slice(-MAX_MESSAGES))); - if (state.list) { - sessionStorage.setItem(STORAGE_HTML_KEY, state.list.innerHTML); - } - } catch (err) { - // Ignore sessionStorage errors (private mode, etc.) - } - } - - function hydrateFromStoredMarkup() { - if (!state.list) return false; - try { - const html = sessionStorage.getItem(STORAGE_HTML_KEY); - if (html) { - state.list.innerHTML = html; - for (const entryNode of state.list.querySelectorAll('.browser-use-demo-entry')) { - const toggle = entryNode.querySelector('.browser-use-entry-toggle'); - if (toggle) { - toggle.addEventListener('click', () => - toggleEntryExpansion(entryNode, toggle, entryNode.getAttribute('data-id')) - ); - } - applyPersistedExpansion(entryNode); - } - state.list.scrollTop = state.list.scrollHeight; - return true; - } - } catch (err) { - // ignore hydration failures - } - return false; - } - - function findFinalResultInMessages(messages) { - if (!Array.isArray(messages)) { - return ''; - } - for (let index = messages.length - 1; index >= 0; index--) { - const entry = normalizeEntry(messages[index]); - if (!entry) { - continue; - } - const result = extractFinalResult(entry); - if (result) { - return result; - } - } - return ''; - } - - function normalizeEntry(detail) { - if (!detail) return null; - const entry = typeof detail === 'string' ? { message: detail } : { ...detail }; - entry.message = typeof entry.message === 'string' ? entry.message : JSON.stringify(entry.message ?? ''); - entry.level = (entry.level || 'info').toLowerCase(); - if (!LEVEL_LABELS[entry.level]) { - entry.level = 'info'; - } - - if (!entry.metadata || typeof entry.metadata !== 'object') { - entry.metadata = {}; - } - - entry.timestamp = entry.timestamp || new Date().toISOString(); - entry.id = entry.id || `${Date.now()}-${Math.random().toString(16).slice(2)}`; - return entry; - } - - function extractFinalResult(entry) { - if (!entry || typeof entry.message !== 'string') { - return ''; - } - const metadata = entry.metadata || {}; - const messageText = entry.message.trim(); - const hasMetadataFlag = metadata.final === true || metadata.tag === 'final-result'; - if (!hasMetadataFlag && !/^final result\b/i.test(messageText)) { - return ''; - } - const cleaned = messageText.replace(/^final result\s*:?\s*/i, '').trim(); - return cleaned || messageText; - } - - function appendEntry(entry, shouldPersist = true) { - if (shouldPersist) { - state.messages.push(entry); - if (state.messages.length > MAX_MESSAGES) { - state.messages = state.messages.slice(-MAX_MESSAGES); - } - persistMessages(); - } - - if (!state.list) { - return; - } - - const node = createEntryNode(entry); - applyPersistedExpansion(node); - state.list.appendChild(node); - highlightLatestEntry(node); - state.list.scrollTop = state.list.scrollHeight; - handlePotentialFinalResult(entry); - } - - function createEntryNode(entry) { - const row = document.createElement('article'); - row.className = `browser-use-demo-entry level-${entry.level}`; - row.setAttribute('data-id', entry.id); - - const content = document.createElement('div'); - content.className = 'browser-use-entry-content'; - - const meta = document.createElement('div'); - meta.className = 'browser-use-entry-meta'; - const time = formatTime(entry.timestamp); - const label = LEVEL_LABELS[entry.level] || entry.level; - meta.innerHTML = `${time}${label}`; - - const messageWrapper = document.createElement('div'); - messageWrapper.className = 'browser-use-entry-message'; - const messageText = entry.message.trim(); - const messageHtml = messageText; - const message = document.createElement('div'); - message.className = 'browser-use-markdown-content'; - message.textContent = messageHtml; - if (messageText.length > 280) { - message.classList.add('is-scrollable'); - } - messageWrapper.appendChild(message); - - if (messageText.length > 160) { - const toggle = document.createElement('button'); - toggle.type = 'button'; - toggle.className = 'browser-use-entry-toggle'; - toggle.setAttribute(EXCLUDE_ATTR, 'true'); - toggle.textContent = 'Expand'; - toggle.addEventListener('click', () => toggleEntryExpansion(row, toggle, entry.id)); - messageWrapper.appendChild(toggle); - } else { - row.classList.add('expanded'); - } - - content.appendChild(meta); - content.appendChild(messageWrapper); - row.appendChild(content); - return row; - } - - function addCornerHighlights(row) { - if (!row) return; - removeCornerHighlights(row); - ['top-left', 'top-right', 'bottom-left', 'bottom-right'].forEach((position) => { - const corner = document.createElement('span'); - corner.className = `browser-use-corner corner-${position}`; - corner.setAttribute(EXCLUDE_ATTR, 'true'); - row.appendChild(corner); - }); - } - - function removeCornerHighlights(row) { - if (!row) return; - row.querySelectorAll('.browser-use-corner').forEach((corner) => corner.remove()); - } - - function highlightLatestEntry(latestNode) { - if (!state.list) return; - state.list.querySelectorAll('.browser-use-demo-entry.highlighted').forEach((entry) => { - entry.classList.remove('highlighted'); - removeCornerHighlights(entry); - }); - const target = latestNode || state.list.lastElementChild; - if (!target) return; - target.classList.add('highlighted'); - addCornerHighlights(target); - } - - function applyPersistedExpansion(node) { - if (!node) return; - try { - const expanded = new Set(JSON.parse(sessionStorage.getItem(EXPANDED_IDS_KEY) || '[]')); - const id = node.getAttribute('data-id'); - if (id && expanded.has(id)) { - node.classList.add('expanded'); - const toggle = node.querySelector('.browser-use-entry-toggle'); - if (toggle) { - toggle.textContent = 'Collapse'; - } - } - } catch { - // ignore - } - } - - function toggleEntryExpansion(row, toggle, entryId) { - if (!row) return; - const isExpanded = row.classList.toggle('expanded'); - if (toggle) { - toggle.textContent = isExpanded ? 'Collapse' : 'Expand'; - } - try { - const expanded = new Set(JSON.parse(sessionStorage.getItem(EXPANDED_IDS_KEY) || '[]')); - if (isExpanded) { - expanded.add(entryId); - } else { - expanded.delete(entryId); - } - sessionStorage.setItem(EXPANDED_IDS_KEY, JSON.stringify(Array.from(expanded))); - } catch { - // ignore persistence issues - } - } - - function handlePotentialFinalResult(entry) { - const resultText = extractFinalResult(entry); - if (!resultText) { - return; - } - state.finalResult = resultText; - persistFinalResult(resultText); - updateFinalResultDisplay(resultText); - } - - function formatTime(value) { - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return new Date().toLocaleTimeString(); - } - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - } - - function debounce(fn, delay) { - let frame; - return (...args) => { - cancelAnimationFrame(frame); - frame = requestAnimationFrame(() => fn.apply(null, args)); - }; - } - - function handleLogEvent(event) { - const entry = normalizeEntry(event?.detail); - if (!entry) return; - appendEntry(entry, true); - } - - const boot = () => { - if (window.__browserUseDemoPanelBootstrapped) { - return; - } - - const start = () => { - if (window.__browserUseDemoPanelBootstrapped) { - return; - } - if (!document.body) { - requestAnimationFrame(start); - return; - } - window.__browserUseDemoPanelBootstrapped = true; - initializePanel(); - }; - - start(); - }; - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', boot, { once: true }); - } else { - boot(); - } - window.addEventListener('browser-use-log', handleLogEvent); -})(); -""" - script = script.replace('__BROWSER_USE_SESSION_ID_PLACEHOLDER__', session_id) - script = script.replace("const ACCENT_COLOR = '#fe750e';", f"const ACCENT_COLOR = '{accent_color}';") - return script - - -def get_last_panel_script(session_id: str, accent_color: str = '#fe750e') -> str: - """Generate JavaScript for the compact draggable display mode.""" - script = r"""(function () { - const SESSION_ID = '__BROWSER_USE_SESSION_ID__'; - const EXCLUDE_ATTR = 'data-browser-use-exclude-' + SESSION_ID; - const BOX_ID = 'browser-use-demo-last-box'; - const STYLE_ID = 'browser-use-demo-last-style'; - const ACCENT_COLOR = '__BROWSER_USE_ACCENT__'; - const STORAGE_KEY = '__browserUseDemoLastState__' + SESSION_ID; - const POSITION_KEY = '__browserUseDemoLastPosition__' + SESSION_ID; - const THOUGHT_ID = 'browser-use-last-thought'; - const MEMORY_ID = 'browser-use-last-memory'; - const FINAL_ID = 'browser-use-last-final'; - const PLACEHOLDER_ID = 'browser-use-last-placeholder'; - const LOGO_HTML = ` - `; - - if (window.__browserUseDemoLastBoxLoaded) { - return; - } - window.__browserUseDemoLastBoxLoaded = true; - - const state = { - box: null, - lastThought: null, - lastMemory: null, - lastFinal: null, - }; - - function restoreState() { - try { - const raw = sessionStorage.getItem(STORAGE_KEY); - if (!raw) return; - const parsed = JSON.parse(raw); - if (parsed.lastThought) { - state.lastThought = parsed.lastThought; - } - if (parsed.lastMemory) { - state.lastMemory = parsed.lastMemory; - } - if (parsed.lastFinal) { - state.lastFinal = parsed.lastFinal; - } - } catch (err) { - // ignore storage errors - } - } - - function persistState() { - try { - sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ - lastThought: state.lastThought, - lastMemory: state.lastMemory, - lastFinal: state.lastFinal, - })); - } catch (err) { - // ignore storage errors - } - } - - function savePosition(pos) { - try { - sessionStorage.setItem(POSITION_KEY, JSON.stringify(pos)); - } catch (err) { - // ignore storage errors - } - } - - function loadPosition() { - try { - const raw = sessionStorage.getItem(POSITION_KEY); - if (!raw) return null; - const parsed = JSON.parse(raw); - if (typeof parsed.x === 'number' && typeof parsed.y === 'number') { - return parsed; - } - } catch (err) { - // ignore storage errors - } - return null; - } - - function addStyles() { - if (document.getElementById(STYLE_ID)) return; - const style = document.createElement('style'); - style.id = STYLE_ID; - style.setAttribute(EXCLUDE_ATTR, 'true'); - style.textContent = ` - #${BOX_ID} { - --browser-use-demo-accent: ${ACCENT_COLOR}; - position: fixed; - top: auto; - left: auto; - bottom: calc(24px + env(safe-area-inset-bottom, 0px)); - right: calc(24px + env(safe-area-inset-right, 0px)); - min-width: 240px; - min-height: 150px; - max-width: min(380px, calc(100vw - 48px)); - max-height: 360px; - overflow: hidden; - background: rgba(5, 5, 5, 0.94); - color: #f8f9ff; - font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', monospace; - font-size: 12px; - line-height: 1.4; - box-shadow: 0 22px 40px rgba(0, 0, 0, 0.55); - padding: 18px 20px; - display: flex; - flex-direction: column; - gap: 12px; - box-sizing: border-box; - cursor: grab; - pointer-events: auto; - z-index: 2147480000; - transition: box-shadow 0.25s ease; - } - - #${BOX_ID}.dragging { - cursor: grabbing; - box-shadow: 0 12px 25px rgba(0, 0, 0, 0.65); - } - - #${BOX_ID}.highlighted { - animation: browser-use-last-pulse 0.85s ease; - } - - @keyframes browser-use-last-pulse { - 0% { box-shadow: 0 22px 40px rgba(0, 0, 0, 0.55); } - 50% { box-shadow: 0 22px 48px rgba(255, 117, 14, 0.28); } - 100% { box-shadow: 0 22px 40px rgba(0, 0, 0, 0.55); } - } - - @keyframes browser-use-last-final-pop { - 0% { opacity: 0; transform: translateY(12px) scale(0.95); } - 60% { opacity: 1; transform: translateY(0) scale(1.02); } - 100% { opacity: 1; transform: translateY(0) scale(1); } - } - - #${BOX_ID} .browser-use-last-content { - position: relative; - z-index: 2; - font-size: 12px; - word-break: break-word; - color: #fefefe; - padding: 0; - min-height: 48px; - max-height: 200px; - overflow-y: auto; - background: transparent; - border: none; - border-radius: 0; - box-shadow: none; - } - - #${BOX_ID} .browser-use-last-content::-webkit-scrollbar { - width: 6px; - } - - #${BOX_ID} .browser-use-last-content::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.25); - border-radius: 999px; - } - - #${BOX_ID} .browser-use-last-empty { - color: rgba(255, 255, 255, 0.35); - font-style: italic; - } - - #${BOX_ID} .browser-use-last-content.browser-use-last-final { - max-height: none; - padding: 14px 16px; - border: 1px solid rgba(255, 117, 14, 0.35); - border-radius: 14px; - background: rgba(255, 255, 255, 0.03); - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02); - } - - #${BOX_ID} .browser-use-last-content.browser-use-last-final::-webkit-scrollbar { - width: 6px; - } - - #${BOX_ID} .browser-use-last-content.browser-use-last-final::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.35); - border-radius: 999px; - } - - #${BOX_ID} .browser-use-last-content.browser-use-last-final[data-visible="false"] { - display: none; - } - - #${BOX_ID} .browser-use-last-content.browser-use-last-final.is-revealed { - animation: browser-use-last-final-pop 0.6s ease; - } - - #${BOX_ID} .browser-use-last-placeholder { - position: absolute; - inset: 18px 20px; - border-radius: 18px; - display: flex; - align-items: center; - justify-content: center; - opacity: 0.55; - pointer-events: none; - transition: opacity 0.35s ease, transform 0.35s ease; - z-index: 1; - background: rgba(0, 0, 0, 0.35); - backdrop-filter: blur(2px); - } - - #${BOX_ID} .browser-use-last-placeholder[data-visible="false"] { - opacity: 0; - transform: scale(0.9); - visibility: hidden; - } - - #${BOX_ID} .browser-use-last-logo { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - opacity: 0.85; - } - - #${BOX_ID} .browser-use-last-logo svg { - width: 70px; - height: 70px; - } - - .browser-use-corner { - position: absolute; - width: 26px; - height: 26px; - border: 2px solid var(--browser-use-demo-accent); - opacity: 0.45; - pointer-events: none; - z-index: 4; - } - - .browser-use-corner.corner-top-left { - top: 6px; - left: 6px; - border-right: none; - border-bottom: none; - } - - .browser-use-corner.corner-top-right { - top: 6px; - right: 6px; - border-left: none; - border-bottom: none; - } - - .browser-use-corner.corner-bottom-left { - bottom: 6px; - left: 6px; - border-right: none; - border-top: none; - } - - .browser-use-corner.corner-bottom-right { - bottom: 6px; - right: 6px; - border-left: none; - border-top: none; - } - `; - document.head.appendChild(style); - } - - function createContent(id) { - const node = document.createElement('div'); - node.id = id; - node.className = 'browser-use-last-content browser-use-last-empty'; - node.setAttribute(EXCLUDE_ATTR, 'true'); - return node; - } - - function buildBox() { - const box = document.createElement('div'); - box.id = BOX_ID; - box.setAttribute(EXCLUDE_ATTR, 'true'); - box.classList.add('highlighted'); - - const thought = createContent(THOUGHT_ID); - const memory = createContent(MEMORY_ID); - const finalResult = createContent(FINAL_ID); - finalResult.classList.add('browser-use-last-final'); - finalResult.dataset.visible = 'false'; - - box.appendChild(thought); - box.appendChild(memory); - box.appendChild(finalResult); - - const placeholder = document.createElement('div'); - placeholder.id = PLACEHOLDER_ID; - placeholder.className = 'browser-use-last-placeholder'; - placeholder.innerHTML = LOGO_HTML; - placeholder.dataset.visible = 'true'; - placeholder.setAttribute(EXCLUDE_ATTR, 'true'); - box.appendChild(placeholder); - - ['top-left', 'top-right', 'bottom-left', 'bottom-right'].forEach((position) => { - const corner = document.createElement('span'); - corner.className = 'browser-use-corner corner-' + position; - corner.setAttribute(EXCLUDE_ATTR, 'true'); - box.appendChild(corner); - }); - - return box; - } - - function applyStoredPosition(node) { - const stored = loadPosition(); - if (!stored) return; - node.style.right = 'auto'; - node.style.bottom = 'auto'; - node.style.left = stored.x + 'px'; - node.style.top = stored.y + 'px'; - } - - function initDrag(node) { - let dragData = null; - - function onPointerDown(event) { - if (event.button !== undefined && event.button !== 0) return; - const rect = node.getBoundingClientRect(); - dragData = { - startX: event.clientX, - startY: event.clientY, - origX: rect.left + window.scrollX, - origY: rect.top + window.scrollY, - }; - node.classList.add('dragging'); - node.setPointerCapture && node.setPointerCapture(event.pointerId); - window.addEventListener('pointermove', onPointerMove); - window.addEventListener('pointerup', onPointerUp); - } - - function onPointerMove(event) { - if (!dragData) return; - const dx = event.clientX - dragData.startX; - const dy = event.clientY - dragData.startY; - let nextX = dragData.origX + dx; - let nextY = dragData.origY + dy; - const maxX = window.scrollX + window.innerWidth - node.offsetWidth - 12; - const maxY = window.scrollY + window.innerHeight - node.offsetHeight - 12; - nextX = Math.min(Math.max(window.scrollX + 8, nextX), Math.max(window.scrollX + 8, maxX)); - nextY = Math.min(Math.max(window.scrollY + 8, nextY), Math.max(window.scrollY + 8, maxY)); - node.style.right = 'auto'; - node.style.bottom = 'auto'; - node.style.left = nextX + 'px'; - node.style.top = nextY + 'px'; - savePosition({ x: nextX, y: nextY }); - } - - function onPointerUp(event) { - if (dragData && node.releasePointerCapture) { - node.releasePointerCapture(event.pointerId); - } - dragData = null; - node.classList.remove('dragging'); - window.removeEventListener('pointermove', onPointerMove); - window.removeEventListener('pointerup', onPointerUp); - } - - node.addEventListener('pointerdown', onPointerDown); - } - - function ensureBox() { - if (!document.body) return; - addStyles(); - let node = document.getElementById(BOX_ID); - if (!node) { - node = buildBox(); - document.body.appendChild(node); - initDrag(node); - } - state.box = node; - applyStoredPosition(node); - updateDisplay(); - } - - function updateDisplay() { - const thoughtEl = document.getElementById(THOUGHT_ID); - const memoryEl = document.getElementById(MEMORY_ID); - const finalEl = document.getElementById(FINAL_ID); - - const hasFinal = Boolean(state.lastFinal); - - if (thoughtEl) { - const showThought = Boolean(state.lastThought) && !hasFinal; - if (showThought) { - thoughtEl.textContent = state.lastThought; - thoughtEl.classList.remove('browser-use-last-empty'); - } else { - thoughtEl.textContent = ''; - thoughtEl.classList.add('browser-use-last-empty'); - } - thoughtEl.style.display = showThought ? '' : 'none'; - } - - if (memoryEl) { - const showMemory = Boolean(state.lastMemory) && !hasFinal; - if (showMemory) { - memoryEl.textContent = state.lastMemory; - memoryEl.classList.remove('browser-use-last-empty'); - } else { - memoryEl.textContent = ''; - memoryEl.classList.add('browser-use-last-empty'); - } - memoryEl.style.display = showMemory ? '' : 'none'; - } - - if (finalEl) { - const wasVisible = finalEl.dataset.visible === 'true'; - finalEl.dataset.visible = hasFinal ? 'true' : 'false'; - finalEl.style.display = hasFinal ? '' : 'none'; - if (hasFinal) { - finalEl.textContent = state.lastFinal; - finalEl.classList.remove('browser-use-last-empty'); - if (!wasVisible) { - finalEl.classList.remove('is-revealed'); - void finalEl.offsetWidth; - finalEl.classList.add('is-revealed'); - } - } else { - finalEl.textContent = ''; - finalEl.classList.add('browser-use-last-empty'); - finalEl.classList.remove('is-revealed'); - } - } - - updatePlaceholderVisibility(); - - if (state.box) { - state.box.classList.remove('highlighted'); - void state.box.offsetWidth; - state.box.classList.add('highlighted'); - } - } - - function updatePlaceholderVisibility() { - const placeholder = document.getElementById(PLACEHOLDER_ID); - if (!placeholder) return; - const hasContent = Boolean(state.lastThought || state.lastMemory || state.lastFinal); - placeholder.dataset.visible = hasContent ? 'false' : 'true'; - } - - function normalizeEntry(detail) { - if (!detail) return null; - const entry = typeof detail === 'string' ? { message: detail } : { ...detail }; - entry.message = typeof entry.message === 'string' ? entry.message : JSON.stringify(entry.message ?? ''); - entry.level = (entry.level || 'info').toLowerCase(); - if (!entry.metadata || typeof entry.metadata !== 'object') { - entry.metadata = {}; - } - return entry; - } - - function extractFinalResult(entry) { - if (!entry || typeof entry.message !== 'string') { - return ''; - } - const metadata = entry.metadata || {}; - const text = entry.message.trim(); - const hasFlag = metadata.final === true || metadata.tag === 'final-result'; - if (!hasFlag && !/^final result\b/i.test(text)) { - return ''; - } - const cleaned = text.replace(/^final result\s*:?\s*/i, '').trim(); - return cleaned || text; - } - - function handleLogEvent(event) { - const entry = normalizeEntry(event && event.detail); - if (!entry || !entry.message) return; - - let updated = false; - if (entry.level === 'thought') { - state.lastThought = entry.message; - updated = true; - } - const messageText = entry.message.trim(); - if (entry.level === 'info' && messageText.toLowerCase().startsWith('memory:')) { - state.lastMemory = messageText.replace(/^memory:\s*/i, ''); - updated = true; - } - const finalResult = extractFinalResult(entry); - if (finalResult) { - state.lastFinal = finalResult; - updated = true; - } - - if (updated) { - persistState(); - updateDisplay(); - } - } - - restoreState(); - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', ensureBox, { once: true }); - } else { - ensureBox(); - } - - window.addEventListener('browser-use-log', handleLogEvent); - })(); -""" - script = script.replace('__BROWSER_USE_SESSION_ID__', session_id) - script = script.replace('__BROWSER_USE_ACCENT__', accent_color) - return script diff --git a/browser_use/browser/profile.py b/browser_use/browser/profile.py index 419c30f68..d2df6403f 100644 --- a/browser_use/browser/profile.py +++ b/browser_use/browser/profile.py @@ -595,10 +595,6 @@ class BrowserProfile(BrowserConnectArgs, BrowserLaunchPersistentContextArgs, Bro default=False, description='Enable demo mode side panel that streams agent logs directly inside the browser window (requires headless=False).', ) - demo_mode_display: Literal['full', 'last'] = Field( - default='last', - description="Display mode for demo panel: 'full' shows complete log panel, 'last' shows only latest action and memory in bottom-right corner", - ) cookie_whitelist_domains: list[str] = Field( default_factory=lambda: ['nature.com', 'qatarairways.com'], description='List of domains to whitelist in the "I still don\'t care about cookies" extension, preventing automatic cookie banner handling on these sites.', diff --git a/browser_use/browser/session.py b/browser_use/browser/session.py index 16cafb557..b8b8d4419 100644 --- a/browser_use/browser/session.py +++ b/browser_use/browser/session.py @@ -436,7 +436,6 @@ class BrowserSession(BaseModel): _cloud_browser_client: CloudBrowserClient = PrivateAttr(default_factory=lambda: CloudBrowserClient()) _demo_mode: 'DemoMode | None' = PrivateAttr(default=None) - _demo_nav_handler_event_bus: EventBus | None = PrivateAttr(default=None) _logger: Any = PrivateAttr(default=None) @@ -518,13 +517,15 @@ class BrowserSession(BaseModel): self._demo_mode.reset() self._demo_mode = None - self.logger.debug('✅ Browser session reset complete') + self.logger.info('✅ Browser session reset complete') def model_post_init(self, __context) -> None: """Register event handlers after model initialization.""" self._connection_lock = asyncio.Lock() # Check if handlers are already registered to prevent duplicates + from browser_use.browser.watchdog_base import BaseWatchdog + start_handlers = self.event_bus.handlers.get('BrowserStartEvent', []) start_handler_names = [getattr(h, '__name__', str(h)) for h in start_handlers] @@ -535,16 +536,9 @@ class BrowserSession(BaseModel): 'This likely means BrowserSession was initialized multiple times with the same EventBus.' ) - self._register_essential_handlers() - - def _register_essential_handlers(self) -> None: - """Register all essential event handlers on the current event bus.""" - from browser_use.browser.watchdog_base import BaseWatchdog - BaseWatchdog.attach_handler_to_session(self, BrowserStartEvent, self.on_BrowserStartEvent) BaseWatchdog.attach_handler_to_session(self, BrowserStopEvent, self.on_BrowserStopEvent) BaseWatchdog.attach_handler_to_session(self, NavigateToUrlEvent, self.on_NavigateToUrlEvent) - self._ensure_demo_mode_handlers() BaseWatchdog.attach_handler_to_session(self, SwitchTabEvent, self.on_SwitchTabEvent) BaseWatchdog.attach_handler_to_session(self, TabCreatedEvent, self.on_TabCreatedEvent) BaseWatchdog.attach_handler_to_session(self, TabClosedEvent, self.on_TabClosedEvent) @@ -552,14 +546,6 @@ class BrowserSession(BaseModel): BaseWatchdog.attach_handler_to_session(self, FileDownloadedEvent, self.on_FileDownloadedEvent) BaseWatchdog.attach_handler_to_session(self, CloseTabEvent, self.on_CloseTabEvent) - def _ensure_demo_mode_handlers(self) -> None: - """Ensure demo mode handlers are attached to the active event bus.""" - if self._demo_nav_handler_event_bus is self.event_bus: - return - - self.event_bus.on(NavigationCompleteEvent, self._on_demo_mode_navigation_complete) - self._demo_nav_handler_event_bus = self.event_bus - @observe_debug(ignore_input=True, ignore_output=True, name='browser_session_start') async def start(self) -> None: """Start the browser session.""" @@ -586,8 +572,6 @@ class BrowserSession(BaseModel): await self.reset() # Create fresh event bus self.event_bus = EventBus() - # Re-register all essential handlers on the new event bus - self._register_essential_handlers() async def stop(self) -> None: """Stop the browser session without killing the browser process. @@ -612,8 +596,6 @@ class BrowserSession(BaseModel): await self.reset() # Create fresh event bus self.event_bus = EventBus() - # Re-register all essential handlers on the new event bus - self._register_essential_handlers() @observe_debug(ignore_input=True, ignore_output=True, name='browser_start_event_handler') async def on_BrowserStartEvent(self, event: BrowserStartEvent) -> dict[str, str]: @@ -904,20 +886,6 @@ class BrowserSession(BaseModel): else: self.logger.warning(f'⚠️ Page readiness timeout ({timeout}s, {duration_ms:.0f}ms) for {url}') - async def _on_demo_mode_navigation_complete(self, event: NavigationCompleteEvent) -> None: - """Rehydrate the demo overlay and logs after navigation.""" - if not self.browser_profile.demo_mode: - return - - demo = self.demo_mode - if not demo: - return - - try: - await demo.refresh_target(event.target_id) - except Exception as exc: - self.logger.debug(f'[DemoMode] Failed to refresh overlay for target {event.target_id[:8]}...: {exc}') - async def on_SwitchTabEvent(self, event: SwitchTabEvent) -> TargetID: """Handle tab switching - core browser functionality.""" if not self.agent_focus_target_id: @@ -1075,7 +1043,7 @@ class BrowserSession(BaseModel): self.logger.debug(f'Failed to cleanup cloud browser session: {e}') # Clear CDP session cache before stopping - self.logger.debug( + self.logger.info( f'📢 on_BrowserStopEvent - Calling reset() (force={event.force}, keep_alive={self.browser_profile.keep_alive})' ) await self.reset() @@ -2957,19 +2925,19 @@ class BrowserSession(BaseModel): """Clear geolocation override using CDP.""" await self.cdp_client.send.Emulation.clearGeolocationOverride() - async def _cdp_add_init_script(self, script: str, target_id: TargetID | None = None) -> str: - """Add script to evaluate on new document for a specific target.""" + async def _cdp_add_init_script(self, script: str) -> str: + """Add script to evaluate on new document using CDP Page.addScriptToEvaluateOnNewDocument.""" assert self._cdp_client_root is not None - cdp_session = await self.get_or_create_cdp_session(target_id=target_id, focus=target_id is None) + cdp_session = await self.get_or_create_cdp_session() result = await cdp_session.cdp_client.send.Page.addScriptToEvaluateOnNewDocument( params={'source': script, 'runImmediately': True}, session_id=cdp_session.session_id ) return result['identifier'] - async def _cdp_remove_init_script(self, identifier: str, target_id: TargetID | None = None) -> None: - """Remove script added with addScriptToEvaluateOnNewDocument for a target.""" - cdp_session = await self.get_or_create_cdp_session(target_id=target_id, focus=target_id is None) + async def _cdp_remove_init_script(self, identifier: str) -> None: + """Remove script added with addScriptToEvaluateOnNewDocument.""" + cdp_session = await self.get_or_create_cdp_session(target_id=None) await cdp_session.cdp_client.send.Page.removeScriptToEvaluateOnNewDocument( params={'identifier': identifier}, session_id=cdp_session.session_id ) diff --git a/browser_use/browser/session_manager.py b/browser_use/browser/session_manager.py index 20d005ea3..91ccdadab 100644 --- a/browser_use/browser/session_manager.py +++ b/browser_use/browser/session_manager.py @@ -181,7 +181,7 @@ class SessionManager: self._target_sessions.clear() self._session_to_target.clear() - self.logger.debug('[SessionManager] Cleared all owned data (targets, sessions, mappings)') + self.logger.info('[SessionManager] Cleared all owned data (targets, sessions, mappings)') async def is_target_valid(self, target_id: TargetID) -> bool: """Check if a target is still valid and has active sessions. @@ -458,14 +458,6 @@ class SessionManager: except Exception as e: self.logger.warning(f'[SessionManager] Failed to resume execution: {e}') - if target_type in ('page', 'tab') and self.browser_session.browser_profile.demo_mode: - demo = self.browser_session.demo_mode - if demo: - try: - await demo.register_new_target(target_id) - except Exception as exc: - self.logger.debug(f'[SessionManager] Failed to register demo overlay for {target_id[:8]}...: {exc}') - async def _handle_target_info_changed(self, event: dict) -> None: """Handle Target.targetInfoChanged event. @@ -478,30 +470,13 @@ class SessionManager: if not target_id: return - url_changed = False - target_type = None - async with self._lock: # Update target if it exists (source of truth for url/title) if target_id in self._targets: target = self._targets[target_id] - target_type = target.target_type - previous_url = target.url - new_url = target_info.get('url', previous_url) target.title = target_info.get('title', target.title) - target.url = new_url - url_changed = previous_url != new_url - - if url_changed and target_type in ('page', 'tab') and self.browser_session.browser_profile.demo_mode: - demo = self.browser_session.demo_mode - if demo: - try: - await demo.refresh_target(target_id) - except Exception as exc: - self.logger.debug( - f'[SessionManager] Failed to refresh demo overlay after URL change for {target_id[:8]}...: {exc}' - ) + target.url = target_info.get('url', target.url) async def _handle_target_detached(self, event: DetachedFromTargetEvent) -> None: """Handle Target.detachedFromTarget event. @@ -592,9 +567,6 @@ class SessionManager: self.browser_session.event_bus.dispatch(TabClosedEvent(target_id=target_id)) self.logger.debug(f'[SessionManager] Dispatched TabClosedEvent for page target {target_id[:8]}...') - demo = self.browser_session.demo_mode - if demo: - demo.unregister_target(target_id) elif target_type: self.logger.debug( f'[SessionManager] Target {target_id[:8]}... fully removed (type={target_type}) - not dispatching TabClosedEvent' diff --git a/browser_use/code_use/service.py b/browser_use/code_use/service.py index d86bbbe02..e4f5b54d3 100644 --- a/browser_use/code_use/service.py +++ b/browser_use/code_use/service.py @@ -295,6 +295,7 @@ class CodeAgent: # Main execution loop for step in range(self.max_steps): logger.info(f'\n\n\n\n\n\n\nStep {step + 1}/{self.max_steps}') + await self._demo_mode_log(f'Starting step {step + 1}/{self.max_steps}', 'info', {'step': step + 1}) # Start timing this step self._step_start_time = datetime.datetime.now().timestamp() diff --git a/pyproject.toml b/pyproject.toml index 34500544c..8a46963b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ build-backend = "hatchling.build" [tool.codespell] -ignore-words-list = "bu,wit,dont,cant,wont,re-use,re-used,re-using,re-usable,thats,doesnt,doubleclick,finaly,finalY,initialY" +ignore-words-list = "bu,wit,dont,cant,wont,re-use,re-used,re-using,re-usable,thats,doesnt,doubleclick,finaly,finalY" skip = "*.json" [tool.ruff] From 3fe94db8f28499cd1c75bb604490fa031df217c3 Mon Sep 17 00:00:00 2001 From: mertunsall Date: Sat, 29 Nov 2025 13:16:14 -0800 Subject: [PATCH 5/9] try to fix stuff --- browser_use/browser/watchdogs/default_action_watchdog.py | 5 +++++ browser_use/browser/watchdogs/screenshot_watchdog.py | 2 +- browser_use/tools/service.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/browser_use/browser/watchdogs/default_action_watchdog.py b/browser_use/browser/watchdogs/default_action_watchdog.py index a87664fe8..1af2f4b72 100644 --- a/browser_use/browser/watchdogs/default_action_watchdog.py +++ b/browser_use/browser/watchdogs/default_action_watchdog.py @@ -920,6 +920,8 @@ class DefaultActionWatchdog(BaseWatchdog): }, session_id=cdp_session.session_id, ) + # Add 10ms delay between keystrokes + await asyncio.sleep(0.010) except Exception as e: raise Exception(f'Failed to type to page: {str(e)}') @@ -2222,6 +2224,9 @@ class DefaultActionWatchdog(BaseWatchdog): session_id=cdp_session.session_id, ) + # Small delay between characters (10ms) + await asyncio.sleep(0.010) + self.logger.info(f'⌨️ Sent keys: {event.keys}') # Note: We don't clear cached state on Enter; multi_act will detect DOM changes diff --git a/browser_use/browser/watchdogs/screenshot_watchdog.py b/browser_use/browser/watchdogs/screenshot_watchdog.py index e4a9089ae..e41db4a18 100644 --- a/browser_use/browser/watchdogs/screenshot_watchdog.py +++ b/browser_use/browser/watchdogs/screenshot_watchdog.py @@ -50,7 +50,7 @@ class ScreenshotWatchdog(BaseWatchdog): raise BrowserError('[ScreenshotWatchdog] No page targets available for screenshot') target_id = page_targets[-1].target_id - cdp_session = await self.browser_session.get_or_create_cdp_session(target_id, focus=False) + cdp_session = await self.browser_session.get_or_create_cdp_session(target_id, focus=True) # Prepare screenshot parameters params = CaptureScreenshotParameters(format='png', captureBeyondViewport=False) diff --git a/browser_use/tools/service.py b/browser_use/tools/service.py index 95739f49b..0e05cc119 100644 --- a/browser_use/tools/service.py +++ b/browser_use/tools/service.py @@ -368,7 +368,7 @@ class Tools(Generic[Context]): return await _click_by_coordinate(params, browser_session) @self.registry.action( - 'Input text into element with index.', + 'Input text into element with index. Only works with index, NEVER use coordinates for inputting text.', param_model=InputTextAction, ) async def input( From 63292a26b52649451333e49c99c821a75b5d9e1f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 29 Nov 2025 21:43:12 +0000 Subject: [PATCH 6/9] Fix: Update Claude Code in README Co-authored-by: mailmertunsal --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64be04249..8d31e6672 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ # 🤖 LLM Quickstart -1. Direct your favorite coding agent (Cursor, ClaudeS, etc) to [Agents.md](https://docs.browser-use.com/llms-full.txt) +1. Direct your favorite coding agent (Cursor, Claude Code, etc) to [Agents.md](https://docs.browser-use.com/llms-full.txt) 2. Prompt away!
From 879960a16a06e4f9c065db20d6d6546570d05d4c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 29 Nov 2025 22:00:16 +0000 Subject: [PATCH 7/9] Bump version to 0.10.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8a46963b7..7ab926bf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "browser-use" description = "Make websites accessible for AI agents" authors = [{ name = "Gregor Zunic" }] -version = "0.9.7" +version = "0.10.0" readme = "README.md" requires-python = ">=3.11,<4.0" classifiers = [ From 7f4f5cacbc36bea10cdcae610fd7ba6e4efa4fc9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 29 Nov 2025 22:08:07 +0000 Subject: [PATCH 8/9] Bump version to 0.10.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7ab926bf5..018e12720 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "browser-use" description = "Make websites accessible for AI agents" authors = [{ name = "Gregor Zunic" }] -version = "0.10.0" +version = "0.10.1" readme = "README.md" requires-python = ">=3.11,<4.0" classifiers = [ From fce911140e21317e47ca0817481c6f05fc7ce162 Mon Sep 17 00:00:00 2001 From: mertunsall Date: Sat, 29 Nov 2025 14:22:44 -0800 Subject: [PATCH 9/9] Add cloud --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8d31e6672..047cb0159 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@
+🌤️ Want to skip the setup? Use our [cloud](https://cloud.browser-use.com) for faster, scalable, stealth-enabled browser automation! + # 🤖 LLM Quickstart 1. Direct your favorite coding agent (Cursor, Claude Code, etc) to [Agents.md](https://docs.browser-use.com/llms-full.txt)