diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 316ae22e5..3f024a1db 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -5,6 +5,6 @@ "node licenses/update_license.js" ], "*.{js,jsx}": ["eslint --fix --no-warn-ignored", "prettier --write"], - "*.{json,css,md}": ["prettier --write"], + "*.{json,css}": ["prettier --write"], "*.py": ["node licenses/update_license.js"] } diff --git a/backend/app/agent/factory/browser.py b/backend/app/agent/factory/browser.py index 385ae167f..4500b4afb 100644 --- a/backend/app/agent/factory/browser.py +++ b/backend/app/agent/factory/browser.py @@ -13,6 +13,7 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import platform +import threading import uuid from camel.messages import BaseMessage @@ -35,6 +36,116 @@ from app.utils.file_utils import get_working_directory +def _get_browser_port(browser: dict) -> int: + """Extract port from a browser config dict, with fallback to env default.""" + return int(browser.get("port", env("browser_port", "9222"))) + + +class CdpBrowserPoolManager: + """Manages CDP browser pool occupation to ensure + parallel tasks use different browsers.""" + + def __init__(self): + self._occupied_ports: dict[int, str] = {} + self._session_to_port: dict[str, int] = {} + self._session_to_task: dict[str, str | None] = {} + self._lock = threading.Lock() + + def acquire_browser( + self, + cdp_browsers: list[dict], + session_id: str, + task_id: str | None = None, + ) -> dict | None: + """Acquire an available browser from the pool. + + Args: + cdp_browsers: List of browser configurations. + session_id: Unique session identifier. + task_id: Optional task identifier for ownership tracking. + + Returns: + Browser configuration dict or None if all occupied. + """ + with self._lock: + for browser in cdp_browsers: + port = browser.get("port") + if port and port not in self._occupied_ports: + self._occupied_ports[port] = session_id + self._session_to_port[session_id] = port + self._session_to_task[session_id] = task_id + logger.info( + f"Acquired browser on port {port} for session " + f"{session_id}. Occupied: " + f"{list(self._occupied_ports.keys())}" + ) + return browser + logger.warning( + f"No available browsers for session {session_id}. " + f"All occupied: {list(self._occupied_ports.keys())}" + ) + return None + + def release_browser(self, port: int, session_id: str): + """Release a browser back to the pool.""" + with self._lock: + if ( + port in self._occupied_ports + and self._occupied_ports[port] == session_id + ): + del self._occupied_ports[port] + self._session_to_port.pop(session_id, None) + self._session_to_task.pop(session_id, None) + logger.info( + f"Released browser on port {port} from session " + f"{session_id}. Occupied: " + f"{list(self._occupied_ports.keys())}" + ) + else: + logger.warning( + f"Attempted to release browser on port {port} " + f"but it was not occupied by {session_id}" + ) + + def release_by_task(self, task_id: str) -> list[int]: + """Release all browsers associated with a task_id. + + Returns: + List of released ports. + """ + released_ports = [] + with self._lock: + sessions = [ + s for s, t in self._session_to_task.items() if t == task_id + ] + for session_id in sessions: + port = self._session_to_port.get(session_id) + if ( + port is not None + and self._occupied_ports.get(port) == session_id + ): + del self._occupied_ports[port] + released_ports.append(port) + self._session_to_port.pop(session_id, None) + self._session_to_task.pop(session_id, None) + if released_ports: + logger.info( + f"Released {len(released_ports)} browser(s) for " + f"task {task_id}. Occupied: " + f"{list(self._occupied_ports.keys())}" + ) + return released_ports + + def get_occupied_ports(self) -> list[int]: + """Get list of currently occupied ports.""" + with self._lock: + return list(self._occupied_ports.keys()) + + +# Global CDP browser pool manager instance +_cdp_pool_manager = CdpBrowserPoolManager() + + def browser_agent(options: Chat): working_directory = get_working_directory(options) logger.info( @@ -47,14 +158,43 @@ def browser_agent(options: Chat): ).send_message_to_user ) + # Acquire CDP browser from pool or use default port + toolkit_session_id = str(uuid.uuid4())[:8] + selected_port = None + selected_is_external = False + + if options.cdp_browsers: + selected_browser = _cdp_pool_manager.acquire_browser( + options.cdp_browsers, toolkit_session_id, options.task_id + ) + if selected_browser: + selected_port = _get_browser_port(selected_browser) + selected_is_external = selected_browser.get("isExternal", False) + logger.info( + f"Acquired CDP browser from pool (initial): " + f"port={selected_port}, isExternal={selected_is_external}, " + f"session_id={toolkit_session_id}" + ) + else: + selected_port = _get_browser_port(options.cdp_browsers[0]) + selected_is_external = options.cdp_browsers[0].get( + "isExternal", False + ) + logger.warning( + f"No available browsers in pool (initial), using first: " + f"port={selected_port}, session_id={toolkit_session_id}" + ) + else: + selected_port = env("browser_port", "9222") + web_toolkit_custom = HybridBrowserToolkit( options.project_id, headless=False, browser_log_to_file=True, stealth=True, - session_id=str(uuid.uuid4())[:8], + session_id=toolkit_session_id, default_start_url="about:blank", - cdp_url=f"http://localhost:{env('browser_port', '9222')}", + cdp_url=f"http://localhost:{selected_port}", enabled_tools=[ "browser_click", "browser_type", @@ -113,14 +253,28 @@ def browser_agent(options: Chat): *search_tools, ] + # Build external browser notice + external_browser_notice = "" + if selected_is_external: + external_browser_notice = ( + "\n\n" + "**IMPORTANT**: You are connected to an external browser instance. " + "The browser may already be open with active sessions and logged-in " + "websites. When you use browser tools, you will connect to this " + "existing browser and can immediately access its current state and " + "pages.\n" + "\n" + ) + system_message = BROWSER_SYS_PROMPT.format( platform_system=platform.system(), platform_machine=platform.machine(), working_directory=working_directory, now_str=NOW_STR, + external_browser_notice=external_browser_notice, ) - return agent_model( + agent = agent_model( Agents.browser_agent, BaseMessage.make_assistant_message( role_name="Browser Agent", @@ -139,3 +293,45 @@ def browser_agent(options: Chat): toolkits_to_register_agent=[web_toolkit_for_agent_registration], enable_snapshot_clean=True, ) + + # Attach CDP management callbacks and info to the agent + def acquire_cdp_for_agent(agent_instance): + """Acquire a CDP browser from pool for a cloned agent.""" + if not options.cdp_browsers: + return + session_id = str(uuid.uuid4())[:8] + selected = _cdp_pool_manager.acquire_browser( + options.cdp_browsers, session_id, options.task_id + ) + if selected: + agent_instance._cdp_port = _get_browser_port(selected) + else: + agent_instance._cdp_port = _get_browser_port( + options.cdp_browsers[0] + ) + agent_instance._cdp_session_id = session_id + logger.info( + f"Acquired CDP for cloned agent {agent_instance.agent_id}: " + f"port={agent_instance._cdp_port}, session={session_id}" + ) + + def release_cdp_from_agent(agent_instance): + """Release CDP browser back to pool.""" + port = getattr(agent_instance, "_cdp_port", None) + session_id = getattr(agent_instance, "_cdp_session_id", None) + if port is not None and session_id is not None: + _cdp_pool_manager.release_browser(port, session_id) + logger.info( + f"Released CDP for agent {agent_instance.agent_id}: " + f"port={port}, session={session_id}" + ) + + agent._cdp_acquire_callback = acquire_cdp_for_agent + agent._cdp_release_callback = release_cdp_from_agent + agent._cdp_port = selected_port + agent._cdp_session_id = toolkit_session_id + agent._cdp_task_id = options.task_id + agent._cdp_options = options + agent._browser_toolkit = web_toolkit_for_agent_registration + + return agent diff --git a/backend/app/agent/listen_chat_agent.py b/backend/app/agent/listen_chat_agent.py index 6bdb61ca5..7fbaddc6a 100644 --- a/backend/app/agent/listen_chat_agent.py +++ b/backend/app/agent/listen_chat_agent.py @@ -15,6 +15,7 @@ import asyncio import json import logging +import threading from collections.abc import Callable from threading import Event from typing import Any @@ -52,6 +53,10 @@ class ListenChatAgent(ChatAgent): + _cdp_clone_lock = ( + threading.Lock() + ) # Protects CDP URL mutation during clone + def __init__( self, api_task_id: str, @@ -692,8 +697,62 @@ def clone(self, with_memory: bool = False) -> ChatAgent: """Please see super.clone()""" system_message = None if with_memory else self._original_system_message - # Clone tools and collect toolkits that need registration - cloned_tools, toolkits_to_register = self._clone_tools() + # If this agent has CDP acquire callback, acquire CDP BEFORE cloning + # tools so that HybridBrowserToolkit clones with the correct CDP port + new_cdp_port = None + new_cdp_session = None + has_cdp = hasattr(self, "_cdp_acquire_callback") and callable( + getattr(self, "_cdp_acquire_callback", None) + ) + + need_cdp_clone = False + if has_cdp and hasattr(self, "_cdp_options"): + options = self._cdp_options + cdp_browsers = getattr(options, "cdp_browsers", []) + if cdp_browsers and hasattr(self, "_browser_toolkit"): + need_cdp_clone = True + import uuid as _uuid + + from app.agent.factory.browser import _cdp_pool_manager + + new_cdp_session = str(_uuid.uuid4())[:8] + selected = _cdp_pool_manager.acquire_browser( + cdp_browsers, + new_cdp_session, + getattr(self, "_cdp_task_id", None), + ) + from app.agent.factory.browser import _get_browser_port + + if selected: + new_cdp_port = _get_browser_port(selected) + else: + new_cdp_port = _get_browser_port(cdp_browsers[0]) + + if need_cdp_clone: + # Temporarily override the browser toolkit's CDP URL. + # Lock prevents concurrent clones from clobbering each + # other's cdp_url on the shared parent toolkit. + toolkit = self._browser_toolkit + with ListenChatAgent._cdp_clone_lock: + original_cdp_url = ( + toolkit.config_loader.get_browser_config().cdp_url + ) + toolkit.config_loader.get_browser_config().cdp_url = ( + f"http://localhost:{new_cdp_port}" + ) + try: + cloned_tools, toolkits_to_register = self._clone_tools() + except Exception: + _cdp_pool_manager.release_browser( + new_cdp_port, new_cdp_session + ) + raise + finally: + toolkit.config_loader.get_browser_config().cdp_url = ( + original_cdp_url + ) + else: + cloned_tools, toolkits_to_register = self._clone_tools() new_agent = ListenChatAgent( api_task_id=self.api_task_id, @@ -726,6 +785,31 @@ def clone(self, with_memory: bool = False) -> ChatAgent: new_agent.process_task_id = self.process_task_id + # Copy CDP management data to cloned agent + if has_cdp: + new_agent._cdp_acquire_callback = self._cdp_acquire_callback + new_agent._cdp_release_callback = self._cdp_release_callback + if hasattr(self, "_cdp_options"): + new_agent._cdp_options = self._cdp_options + if hasattr(self, "_cdp_task_id"): + new_agent._cdp_task_id = self._cdp_task_id + + # Find and store the cloned browser toolkit on the new agent + for tk in toolkits_to_register: + if tk.__class__.__name__ == "HybridBrowserToolkit": + new_agent._browser_toolkit = tk + break + + # Set CDP info on cloned agent + if new_cdp_port is not None and new_cdp_session is not None: + new_agent._cdp_port = new_cdp_port + new_agent._cdp_session_id = new_cdp_session + else: + if hasattr(self, "_cdp_port"): + new_agent._cdp_port = self._cdp_port + if hasattr(self, "_cdp_session_id"): + new_agent._cdp_session_id = self._cdp_session_id + # Copy memory if requested if with_memory: # Get all records from the current memory diff --git a/backend/app/agent/prompt.py b/backend/app/agent/prompt.py index ee1d782d0..ba8327820 100644 --- a/backend/app/agent/prompt.py +++ b/backend/app/agent/prompt.py @@ -596,7 +596,7 @@ -Your approach depends on available search tools: +{external_browser_notice}Your approach depends on available search tools: **If Google Search is Available:** - Initial Search: Start with `search_google` to get a list of relevant URLs diff --git a/backend/app/agent/toolkit/hybrid_browser_python_toolkit.py b/backend/app/agent/toolkit/hybrid_browser_python_toolkit.py index 096f0d7cb..e4635e178 100644 --- a/backend/app/agent/toolkit/hybrid_browser_python_toolkit.py +++ b/backend/app/agent/toolkit/hybrid_browser_python_toolkit.py @@ -174,6 +174,7 @@ def __init__( enabled_tools: list[str] | None = None, browser_log_to_file: bool = False, session_id: str | None = None, + log_base_dir: str | None = None, default_start_url: str = "https://google.com/", default_timeout: int | None = None, short_timeout: int | None = None, @@ -189,6 +190,7 @@ def __init__( self._stealth = stealth self._cache_dir = cache_dir self._browser_log_to_file = browser_log_to_file + self._log_base_dir = log_base_dir self._default_start_url = default_start_url self._session_id = session_id or "default" @@ -224,7 +226,12 @@ def __init__( # Set up log file if needed if self.log_to_file: # Create log directory if it doesn't exist - log_dir = "browser_log" + # If log_base_dir is provided, use task-specific directory; otherwise use default backend/browser_log + if log_base_dir: + log_dir = os.path.join(log_base_dir, "browser_logs") + else: + log_dir = "browser_log" # Backward compatibility: use default location + os.makedirs(log_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") @@ -351,6 +358,8 @@ def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]: FunctionTool(browser.browser_visit_page), FunctionTool(browser.browser_scroll), FunctionTool(browser.browser_get_som_screenshot), + FunctionTool(browser.browser_sheet_read), + FunctionTool(browser.browser_sheet_input), # FunctionTool(browser.select), # FunctionTool(browser.wait_user), ] @@ -376,6 +385,7 @@ def clone_for_new_session( enabled_tools=self.enabled_tools.copy(), browser_log_to_file=self._browser_log_to_file, session_id=new_session_id, + log_base_dir=self._log_base_dir, default_start_url=self._default_start_url, default_timeout=self._default_timeout, short_timeout=self._short_timeout, diff --git a/backend/app/agent/toolkit/hybrid_browser_toolkit.py b/backend/app/agent/toolkit/hybrid_browser_toolkit.py index dd9b8d716..988893d91 100644 --- a/backend/app/agent/toolkit/hybrid_browser_toolkit.py +++ b/backend/app/agent/toolkit/hybrid_browser_toolkit.py @@ -456,7 +456,7 @@ def __init__( page_stability_timeout: int | None = None, dom_content_loaded_timeout: int | None = None, viewport_limit: bool = False, - connect_over_cdp: bool = True, + connect_over_cdp: bool = True, # Deprecated: auto-set to True when cdp_url is provided, kept for compatibility cdp_url: str | None = "http://localhost:9222", cdp_keep_current_page: bool = False, full_visual_mode: bool = False, @@ -588,7 +588,7 @@ def clone_for_new_session( dom_content_loaded_timeout=self._dom_content_loaded_timeout, viewport_limit=self._viewport_limit, connect_over_cdp=self.config_loader.get_browser_config().connect_over_cdp, - cdp_url=f"http://localhost:{env('browser_port', '9222')}", + cdp_url=self.config_loader.get_browser_config().cdp_url, cdp_keep_current_page=self.config_loader.get_browser_config().cdp_keep_current_page, full_visual_mode=self._full_visual_mode, ) diff --git a/backend/app/model/chat.py b/backend/app/model/chat.py index 58d0e8bfe..24cfa79cf 100644 --- a/backend/app/model/chat.py +++ b/backend/app/model/chat.py @@ -63,6 +63,7 @@ class Chat(BaseModel): api_url: str | None = None language: str = "en" browser_port: int = 9222 + cdp_browsers: list[dict] = Field(default_factory=list) max_retries: int = 3 allow_local_system: bool = False installed_mcp: McpServers = {"mcpServers": {}} diff --git a/backend/app/utils/listen/toolkit_listen.py b/backend/app/utils/listen/toolkit_listen.py index bd411b86b..1c2c9bbc6 100644 --- a/backend/app/utils/listen/toolkit_listen.py +++ b/backend/app/utils/listen/toolkit_listen.py @@ -171,6 +171,24 @@ def _log_deactivate( ) +def _filter_kwargs_for_callable( + func: Callable[..., Any], kwargs: dict +) -> dict: + """Drop unexpected kwargs unless the callable accepts **kwargs.""" + if not kwargs: + return kwargs + try: + sig = signature(func) + except (TypeError, ValueError): + return kwargs + if any( + param.kind == param.VAR_KEYWORD for param in sig.parameters.values() + ): + return kwargs + allowed = set(sig.parameters.keys()) + return {k: v for k, v in kwargs.items() if k in allowed} + + def _safe_put_queue(task_lock, data): """Safely put data to the queue, handling both sync and async contexts""" try: @@ -308,7 +326,8 @@ async def async_wrapper(*args, **kwargs): error = None res = None try: - res = await func(*args, **kwargs) + safe_kwargs = _filter_kwargs_for_callable(func, kwargs) + res = await func(*args, **safe_kwargs) except Exception as e: error = e diff --git a/backend/app/utils/single_agent_worker.py b/backend/app/utils/single_agent_worker.py index 7a4ec4818..dd96b832b 100644 --- a/backend/app/utils/single_agent_worker.py +++ b/backend/app/utils/single_agent_worker.py @@ -36,7 +36,7 @@ def __init__( description: str, worker: ListenChatAgent, use_agent_pool: bool = True, - pool_initial_size: int = 1, + pool_initial_size: int = 0, # Changed from 1 to 0 to avoid pre-creating clones that waste CDP resources pool_max_size: int = 10, auto_scale_pool: bool = True, use_structured_output_handler: bool = True, @@ -86,6 +86,16 @@ async def _process_task( TaskState: `TaskState.DONE` if processed successfully, otherwise `TaskState.FAILED`. """ + # Log task details before getting agent (for clone tracking) + task_content_preview = ( + task.content[:100] + "..." + if len(task.content) > 100 + else task.content + ) + logger.debug( + f"[TASK REQUEST] Requesting agent for task_id={task.id}, content_preview='{task_content_preview}'" + ) + # Get agent efficiently (from pool or by cloning) worker_agent = await self._get_worker_agent() worker_agent.process_task_id = task.id # type: ignore rewrite line diff --git a/backend/app/utils/telemetry/__init__.py b/backend/app/utils/telemetry/__init__.py index e69de29bb..fa7455a0c 100644 --- a/backend/app/utils/telemetry/__init__.py +++ b/backend/app/utils/telemetry/__init__.py @@ -0,0 +1,13 @@ +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index d9f70f2fe..39be31e79 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -922,12 +922,94 @@ def stop_gracefully(self) -> None: f"{self._state.name}, _running: {self._running}" ) logger.info("=" * 80) + self._cleanup_all_agents() super().stop_gracefully() logger.info( f"[WF-LIFECYCLE] ✅ super().stop_gracefully() completed, " f"new state: {self._state.name}, _running: {self._running}" ) + def _cleanup_all_agents(self) -> None: + """Release CDP browser resources for all agents.""" + cleanup_count = 0 + + if hasattr(self, "_children") and self._children: + for child in self._children: + # Cleanup base worker agent + if hasattr(child, "worker_agent"): + agent = child.worker_agent + cb = getattr(agent, "_cdp_release_callback", None) + if callable(cb): + try: + cb(agent) + cleanup_count += 1 + except Exception as e: + logger.error( + f"[WF-CLEANUP] Error releasing CDP for " + f"agent: {e}" + ) + + # Cleanup agents in AgentPool + if hasattr(child, "agent_pool") and child.agent_pool: + pool = child.agent_pool + for agent in list(getattr(pool, "_available_agents", [])): + cb = getattr(agent, "_cdp_release_callback", None) + if callable(cb): + try: + cb(agent) + cleanup_count += 1 + except Exception as e: + logger.error( + f"[WF-CLEANUP] Error releasing CDP for " + f"pooled agent: {e}" + ) + + # Force-clear all occupied ports as a safety net + try: + from app.agent.factory.browser import _cdp_pool_manager + + task_ids: set[str] = set() + if hasattr(self, "_children") and self._children: + for child in self._children: + if hasattr(child, "worker_agent"): + tid = getattr(child.worker_agent, "_cdp_task_id", None) + if tid is not None: + task_ids.add(tid) + if hasattr(child, "agent_pool") and child.agent_pool: + for agent in list(child.agent_pool._available_agents): + tid = getattr(agent, "_cdp_task_id", None) + if tid is not None: + task_ids.add(tid) + if hasattr(self, "coordinator_agent") and self.coordinator_agent: + tid = getattr(self.coordinator_agent, "_cdp_task_id", None) + if tid is not None: + task_ids.add(tid) + + if not task_ids: + logger.debug( + "[WF-CLEANUP] No task_id found for CDP release; skipping pool cleanup" + ) + else: + logger.info( + f"[WF-CLEANUP] Force releasing CDP resources for task_ids: {sorted(task_ids)}" + ) + released_ports = [] + for task_id in task_ids: + released_ports.extend( + _cdp_pool_manager.release_by_task(task_id) + ) + + logger.info( + f"[WF-CLEANUP] Released {len(released_ports)} CDP browser(s), remaining: {_cdp_pool_manager.get_occupied_ports()}" + ) + except Exception as e: + logger.error(f"[WF-CLEANUP] Error clearing CDP pool: {e}") + + logger.info( + f"[WF-CLEANUP] Cleanup completed, " + f"{cleanup_count} agent(s) released" + ) + def skip_gracefully(self) -> None: logger.info("=" * 80) logger.info( diff --git a/electron/main/index.ts b/electron/main/index.ts index b2001bbf0..a757c3ad4 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -83,8 +83,36 @@ let fileReader: FileReader | null = null; let python_process: ChildProcessWithoutNullStreams | null = null; let backendPort: number = 5001; let browser_port = 9222; +let use_external_cdp = false; let proxyUrl: string | null = null; +// CDP Browser Pool +interface CdpBrowser { + id: string; + port: number; + isExternal: boolean; + name?: string; + addedAt: number; +} +let cdp_browser_pool: CdpBrowser[] = []; + +// Map to store multiple browser processes by port +let cdp_browser_processes: Map = + new Map(); + +/** Remove a non-external browser from the pool by port (used on process error/exit). */ +function removeFromPoolByPort(port: number, reason: string): void { + const idx = cdp_browser_pool.findIndex( + (b) => b.port === port && !b.isExternal + ); + if (idx !== -1) { + const removed = cdp_browser_pool.splice(idx, 1)[0]; + log.warn( + `[CDP POOL] Auto-removed port=${port} (${reason}), id=${removed.id}, pool_size=${cdp_browser_pool.length}` + ); + } +} + // Protocol URL queue for handling URLs before window is ready let protocolUrlQueue: string[] = []; let isWindowReady = false; @@ -387,6 +415,461 @@ function registerIpcHandlers() { log.info('Getting browser port'); return browser_port; }); + + // Set browser port + ipcMain.handle( + 'set-browser-port', + (event, port: number, isExternal: boolean = false) => { + log.info(`Setting browser port to ${port}, external: ${isExternal}`); + browser_port = port; + use_external_cdp = isExternal; + return { success: true, port: browser_port, use_external_cdp }; + } + ); + + // Get external CDP flag + ipcMain.handle('get-use-external-cdp', () => { + log.info(`Getting use_external_cdp: ${use_external_cdp}`); + return use_external_cdp; + }); + + // ==================== CDP Browser Pool Management ==================== + + // Get all browsers in the pool + ipcMain.handle('get-cdp-browsers', () => { + log.debug(`[CDP POOL] GET pool (size=${cdp_browser_pool.length})`); + return cdp_browser_pool; + }); + + // Get running browser processes + ipcMain.handle('get-running-browser-ports', () => { + return Array.from(cdp_browser_processes.keys()); + }); + + // Add browser to pool + ipcMain.handle( + 'add-cdp-browser', + (event, port: number, isExternal: boolean, name?: string) => { + // Check if browser with this port already exists + const existing = cdp_browser_pool.find((b) => b.port === port); + if (existing) { + log.warn( + `[CDP POOL] ADD rejected: port ${port} already exists (id=${existing.id})` + ); + return { + success: false, + error: 'Browser with this port already exists', + }; + } + + const newBrowser: CdpBrowser = { + id: `cdp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + port, + isExternal, + name, + addedAt: Date.now(), + }; + + cdp_browser_pool.push(newBrowser); + log.info( + `[CDP POOL] ADD: port=${port}, isExternal=${isExternal}, id=${newBrowser.id}, pool_size=${cdp_browser_pool.length}` + ); + + return { success: true, browser: newBrowser }; + } + ); + + // Remove browser from pool + ipcMain.handle('remove-cdp-browser', (event, browserId: string) => { + const index = cdp_browser_pool.findIndex((b) => b.id === browserId); + if (index === -1) { + log.warn(`[CDP POOL] REMOVE: browser not found: ${browserId}`); + return { success: false, error: 'Browser not found' }; + } + + const removed = cdp_browser_pool.splice(index, 1)[0]; + + // If it's a launched browser, kill the process + if (!removed.isExternal && cdp_browser_processes.has(removed.port)) { + try { + const process = cdp_browser_processes.get(removed.port); + process?.kill(); + cdp_browser_processes.delete(removed.port); + } catch (error) { + log.warn( + `[CDP POOL] Failed to kill browser process on port ${removed.port}: ${error}` + ); + } + } + + log.info( + `[CDP POOL] REMOVE: port=${removed.port}, id=${removed.id}, pool_size=${cdp_browser_pool.length}` + ); + return { success: true, browser: removed }; + }); + + // Update browser in pool + ipcMain.handle( + 'update-cdp-browser', + (event, browserId: string, updates: Partial) => { + log.info(`Updating CDP browser: ${browserId}`); + + const browser = cdp_browser_pool.find((b) => b.id === browserId); + if (!browser) { + return { success: false, error: 'Browser not found' }; + } + + // Update allowed fields + if (updates.name !== undefined) browser.name = updates.name; + + log.info(`Browser updated in pool`); + return { success: true, browser }; + } + ); + + // Check if CDP port is available + ipcMain.handle('check-cdp-port', async (event, port: number) => { + log.info(`Checking CDP port availability: ${port}`); + try { + const response = await axios.get( + `http://localhost:${port}/json/version`, + { + timeout: 3000, + } + ); + + if (response.status === 200 && response.data) { + log.info(`CDP port ${port} is available and responsive`); + return { + available: true, + data: response.data, + }; + } + return { available: false, error: 'Invalid response from CDP' }; + } catch (error: any) { + log.warn(`CDP port ${port} is not available: ${error.message}`); + return { + available: false, + error: + error.code === 'ECONNREFUSED' + ? 'Connection refused - no browser running on this port' + : error.message, + }; + } + }); + + // Launch CDP browser with custom port + ipcMain.handle('launch-cdp-browser', async (event, port: number) => { + log.info(`[CDP LAUNCH] Launching browser on port ${port}`); + + try { + const platform = process.platform; + let chromeExecutable: string | null = null; + + // Use Playwright's Chromium + let playwrightCacheDir: string; + + if (platform === 'darwin') { + playwrightCacheDir = path.join( + app.getPath('home'), + 'Library/Caches/ms-playwright' + ); + } else if (platform === 'win32') { + playwrightCacheDir = path.join( + app.getPath('home'), + 'AppData/Local/ms-playwright' + ); + } else if (platform === 'linux') { + playwrightCacheDir = path.join( + app.getPath('home'), + '.cache/ms-playwright' + ); + } else { + return { + success: false, + error: `Unsupported platform: ${platform}`, + }; + } + + log.info(`Looking for Playwright Chromium in: ${playwrightCacheDir}`); + + // Find the latest chromium directory + try { + if (!existsSync(playwrightCacheDir)) { + return { + success: false, + error: + 'Playwright Chromium not found. Please run: npx playwright install chromium', + }; + } + + const chromiumDirs = fs + .readdirSync(playwrightCacheDir) + .filter((dir) => dir.startsWith('chromium-')) + .sort() + .reverse(); + + if (chromiumDirs.length === 0) { + return { + success: false, + error: + 'No Playwright Chromium installations found. Please run: npx playwright install chromium', + }; + } + + // Prioritize versions that have Chromium.app over Google Chrome for Testing + let selectedChromiumDir = chromiumDirs[0]; + if (platform === 'darwin') { + for (const dir of chromiumDirs) { + const chromiumAppPaths = [ + path.join( + playwrightCacheDir, + dir, + 'chrome-mac-arm64', + 'Chromium.app' + ), + path.join(playwrightCacheDir, dir, 'chrome-mac', 'Chromium.app'), + ]; + if (chromiumAppPaths.some((p) => existsSync(p))) { + selectedChromiumDir = dir; + log.info(`Selected Chromium version with Chromium.app: ${dir}`); + break; + } + } + } + + const latestChromiumDir = selectedChromiumDir; + log.info(`Using Playwright Chromium version: ${latestChromiumDir}`); + + // Build path to Chromium executable based on platform + if (platform === 'darwin') { + // Try to find Chromium executable in both arm64 and regular directories + // Priority: Chromium.app (older versions) > Google Chrome for Testing (newer versions) + const possiblePaths = [ + // ARM64 paths + path.join( + playwrightCacheDir, + latestChromiumDir, + 'chrome-mac-arm64', + 'Chromium.app/Contents/MacOS/Chromium' + ), + path.join( + playwrightCacheDir, + latestChromiumDir, + 'chrome-mac-arm64', + 'Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing' + ), + // Intel/Universal paths + path.join( + playwrightCacheDir, + latestChromiumDir, + 'chrome-mac', + 'Chromium.app/Contents/MacOS/Chromium' + ), + path.join( + playwrightCacheDir, + latestChromiumDir, + 'chrome-mac', + 'Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing' + ), + ]; + + // Find the first path that exists + chromeExecutable = possiblePaths.find((p) => existsSync(p)) || null; + } else if (platform === 'win32') { + // Windows: Try to find chrome.exe in possible directories + const possiblePaths = [ + // 64-bit paths + path.join( + playwrightCacheDir, + latestChromiumDir, + 'chrome-win64', + 'chrome.exe' + ), + // 32-bit or older versions + path.join( + playwrightCacheDir, + latestChromiumDir, + 'chrome-win', + 'chrome.exe' + ), + ]; + + chromeExecutable = possiblePaths.find((p) => existsSync(p)) || null; + } else if (platform === 'linux') { + // Linux: Try to find chrome in possible directories + const possiblePaths = [ + path.join( + playwrightCacheDir, + latestChromiumDir, + 'chrome-linux', + 'chrome' + ), + ]; + + chromeExecutable = possiblePaths.find((p) => existsSync(p)) || null; + } + + if (!chromeExecutable || !existsSync(chromeExecutable)) { + return { + success: false, + error: `Chromium executable not found at: ${chromeExecutable}`, + }; + } + + log.info(`Using Chromium at: ${chromeExecutable}`); + } catch (error: any) { + log.error(`Error finding Playwright Chromium: ${error}`); + return { + success: false, + error: `Failed to locate Playwright Chromium: ${error.message}`, + }; + } + + // Create user data directory with port number in name + // This allows multiple browsers on different ports to maintain separate profiles + const userDataDir = path.join( + app.getPath('userData'), + `cdp_browser_profile_${port}` + ); + + // Create directory if it doesn't exist (preserve existing data) + if (!existsSync(userDataDir)) { + await fsp.mkdir(userDataDir, { recursive: true }); + log.info(`Created new user data directory: ${userDataDir}`); + } else { + log.info(`Using existing user data directory: ${userDataDir}`); + } + + // Check if browser on this port is already running + if (cdp_browser_processes.has(port)) { + log.warn(`[CDP LAUNCH] Browser process already exists on port ${port}`); + return { + success: false, + error: `Browser already running on port ${port}`, + }; + } + + // Chrome launch arguments + const args = [ + `--remote-debugging-port=${port}`, + `--user-data-dir=${userDataDir}`, + '--no-first-run', + '--no-default-browser-check', + '--disable-blink-features=AutomationControlled', + 'about:blank', + ]; + + log.info(`[CDP LAUNCH] Spawning: ${chromeExecutable} on port ${port}`); + + // Spawn Chrome process + const browserProcess = spawn(chromeExecutable, args, { + detached: false, + stdio: 'ignore', + }); + + browserProcess.on('error', (error) => { + log.error( + `[CDP LAUNCH] Browser process error on port ${port}: ${error}` + ); + cdp_browser_processes.delete(port); + removeFromPoolByPort(port, 'process error'); + }); + + browserProcess.on('exit', (code) => { + log.info( + `[CDP LAUNCH] Browser process on port ${port} exited with code ${code}` + ); + cdp_browser_processes.delete(port); + removeFromPoolByPort(port, `exit code ${code}`); + }); + + // Store the process in the Map + cdp_browser_processes.set(port, browserProcess); + log.info( + `[CDP LAUNCH] Browser process stored in map, PID: ${browserProcess.pid}` + ); + + // Poll for browser to become ready (max 5 seconds) + log.info( + `[CDP LAUNCH] Polling for browser to become ready (max 5 seconds)...` + ); + const maxWaitTime = 5000; // 5 seconds + const pollInterval = 300; // Check every 300ms + const startTime = Date.now(); + let attempt = 0; + let lastError = null; + + while (Date.now() - startTime < maxWaitTime) { + attempt++; + try { + log.info( + `[CDP LAUNCH] Attempt ${attempt}: Checking http://localhost:${port}/json/version` + ); + const response = await axios.get( + `http://localhost:${port}/json/version`, + { + timeout: 1000, // Short timeout for each attempt + } + ); + + if (response.status === 200 && response.data) { + const elapsedTime = Date.now() - startTime; + log.info( + `[CDP LAUNCH] ✅ SUCCESS - Browser ready on port ${port} after ${elapsedTime}ms (${attempt} attempts)` + ); + log.info( + `[CDP LAUNCH] Browser info: ${JSON.stringify(response.data)}` + ); + log.info( + `[CDP LAUNCH] ⚠️ NOTE: Browser launched but NOT added to pool yet` + ); + // This is our own launched browser, not external + use_external_cdp = false; + return { + success: true, + port, + data: response.data, + }; + } + } catch (pollError: any) { + lastError = pollError; + // Log only every 3rd attempt to avoid spam + if (attempt % 3 === 0) { + log.info( + `[CDP LAUNCH] Attempt ${attempt}: Not ready yet (${pollError.code || pollError.message})` + ); + } + } + + // Wait before next attempt + await new Promise((resolve) => setTimeout(resolve, pollInterval)); + } + + // If we get here, browser didn't respond within max wait time + // Kill the orphaned process to avoid resource leak + const proc = cdp_browser_processes.get(port); + if (proc) { + proc.kill(); + cdp_browser_processes.delete(port); + } + const totalTime = Date.now() - startTime; + log.warn( + `[CDP LAUNCH] Verification failed after ${totalTime}ms (${attempt} attempts), last error: ${lastError?.code || lastError?.message || 'Unknown'}` + ); + return { + success: false, + error: `Browser launched but not responding on CDP port after ${totalTime}ms`, + }; + } catch (error: any) { + log.error(`[CDP LAUNCH] Failed to launch browser: ${error}`); + return { + success: false, + error: error.message, + }; + } + }); + ipcMain.handle('get-app-version', () => app.getVersion()); ipcMain.handle('get-backend-port', () => backendPort); diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 5d7896db7..f84991572 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -155,6 +155,20 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('get-project-folder-path', email, projectId), openInIDE: (folderPath: string, ide: string) => ipcRenderer.invoke('open-in-ide', folderPath, ide), + checkCdpPort: (port: number) => ipcRenderer.invoke('check-cdp-port', port), + launchCdpBrowser: (port: number) => + ipcRenderer.invoke('launch-cdp-browser', port), + setBrowserPort: (port: number, isExternal?: boolean) => + ipcRenderer.invoke('set-browser-port', port, isExternal), + getBrowserPort: () => ipcRenderer.invoke('get-browser-port'), + getCdpBrowsers: () => ipcRenderer.invoke('get-cdp-browsers'), + getRunningBrowserPorts: () => ipcRenderer.invoke('get-running-browser-ports'), + addCdpBrowser: (port: number, isExternal: boolean, name?: string) => + ipcRenderer.invoke('add-cdp-browser', port, isExternal, name), + removeCdpBrowser: (browserId: string) => + ipcRenderer.invoke('remove-cdp-browser', browserId), + updateCdpBrowser: (browserId: string, updates: any) => + ipcRenderer.invoke('update-cdp-browser', browserId, updates), }); // --------- Preload scripts loading --------- diff --git a/src/i18n/locales/ar/layout.json b/src/i18n/locales/ar/layout.json index 75f758ab6..73c8e1a27 100644 --- a/src/i18n/locales/ar/layout.json +++ b/src/i18n/locales/ar/layout.json @@ -166,5 +166,42 @@ "days-ago": "أيام مضت", "delete-project": "حذف المشروع", "delete-project-confirmation": "هل أنت متأكد من أنك تريد حذف هذا المشروع وجميع مهامه؟ لا يمكن التراجع عن هذا الإجراء.", - "please-select-model": "يرجى اختيار نموذج في الإعدادات > النماذج للمتابعة." + "please-select-model": "يرجى اختيار نموذج في الإعدادات > النماذج للمتابعة.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/de/layout.json b/src/i18n/locales/de/layout.json index 4cc133adb..9b379792d 100644 --- a/src/i18n/locales/de/layout.json +++ b/src/i18n/locales/de/layout.json @@ -166,5 +166,42 @@ "days-ago": "Tage zuvor", "delete-project": "Projekt löschen", "delete-project-confirmation": "Sind Sie sicher, dass Sie dieses Projekt und alle seine Aufgaben löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", - "please-select-model": "Bitte wählen Sie ein Modell unter Einstellungen > Modelle aus, um fortzufahren." + "please-select-model": "Bitte wählen Sie ein Modell unter Einstellungen > Modelle aus, um fortzufahren.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/en-us/layout.json b/src/i18n/locales/en-us/layout.json index 786103b67..260405fc8 100644 --- a/src/i18n/locales/en-us/layout.json +++ b/src/i18n/locales/en-us/layout.json @@ -168,5 +168,42 @@ "days-ago": "days ago", "delete-project": "Delete Project", "delete-project-confirmation": "Are you sure you want to delete this project and all its tasks? This action cannot be undone.", - "please-select-model": "Please select a model in Settings > Models to continue." + "please-select-model": "Please select a model in Settings > Models to continue.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Electron Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9223)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/es/layout.json b/src/i18n/locales/es/layout.json index 3f700d514..f0813c016 100644 --- a/src/i18n/locales/es/layout.json +++ b/src/i18n/locales/es/layout.json @@ -166,5 +166,42 @@ "days-ago": "días atrás", "delete-project": "Eliminar Proyecto", "delete-project-confirmation": "¿Estás seguro de que quieres eliminar este proyecto y todas sus tareas? Esta acción no se puede deshacer.", - "please-select-model": "Por favor, selecciona un modelo en Configuración > Modelos para continuar." + "please-select-model": "Por favor, selecciona un modelo en Configuración > Modelos para continuar.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/fr/layout.json b/src/i18n/locales/fr/layout.json index d2415b021..a3fda3079 100644 --- a/src/i18n/locales/fr/layout.json +++ b/src/i18n/locales/fr/layout.json @@ -166,5 +166,42 @@ "days-ago": "jours auparavant", "delete-project": "Supprimer le Projet", "delete-project-confirmation": "Êtes-vous sûr de vouloir supprimer ce projet et toutes ses tâches ? Cette action ne peut pas être annulée.", - "please-select-model": "Veuillez sélectionner un modèle dans Paramètres > Modèles pour continuer." + "please-select-model": "Veuillez sélectionner un modèle dans Paramètres > Modèles pour continuer.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/it/layout.json b/src/i18n/locales/it/layout.json index 1b5b365d0..be3fdda6d 100644 --- a/src/i18n/locales/it/layout.json +++ b/src/i18n/locales/it/layout.json @@ -166,5 +166,42 @@ "days-ago": "giorni fa", "delete-project": "Elimina Progetto", "delete-project-confirmation": "Sei sicuro di voler eliminare questo progetto e tutte le sue attività? Questa azione non può essere annullata.", - "please-select-model": "Seleziona un modello in Impostazioni > Modelli per continuare." + "please-select-model": "Seleziona un modello in Impostazioni > Modelli per continuare.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/ja/layout.json b/src/i18n/locales/ja/layout.json index 6c0e9e45c..c0b67175f 100644 --- a/src/i18n/locales/ja/layout.json +++ b/src/i18n/locales/ja/layout.json @@ -166,5 +166,42 @@ "days-ago": "日前", "delete-project": "プロジェクトを削除", "delete-project-confirmation": "このプロジェクトとそのすべてのタスクを削除してもよろしいですか?この操作は元に戻せません。", - "please-select-model": "続行するには、設定 > モデルでモデルを選択してください。" + "please-select-model": "続行するには、設定 > モデルでモデルを選択してください。", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/ko/layout.json b/src/i18n/locales/ko/layout.json index d6678e942..3e8d67447 100644 --- a/src/i18n/locales/ko/layout.json +++ b/src/i18n/locales/ko/layout.json @@ -166,5 +166,42 @@ "days-ago": "일 전", "delete-project": "프로젝트 삭제", "delete-project-confirmation": "이 프로젝트와 모든 작업을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", - "please-select-model": "계속하려면 설정 > 모델에서 모델을 선택하세요." + "please-select-model": "계속하려면 설정 > 모델에서 모델을 선택하세요.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/ru/layout.json b/src/i18n/locales/ru/layout.json index bc93cfc18..0d33fd3cf 100644 --- a/src/i18n/locales/ru/layout.json +++ b/src/i18n/locales/ru/layout.json @@ -166,5 +166,42 @@ "days-ago": "дней назад", "delete-project": "Удалить проект", "delete-project-confirmation": "Вы уверены, что хотите удалить этот проект и все его задачи? Это действие нельзя отменить.", - "please-select-model": "Пожалуйста, выберите модель в Настройки > Модели, чтобы продолжить." + "please-select-model": "Пожалуйста, выберите модель в Настройки > Модели, чтобы продолжить.", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/zh-Hans/layout.json b/src/i18n/locales/zh-Hans/layout.json index 4a28cc55e..15932d4ac 100644 --- a/src/i18n/locales/zh-Hans/layout.json +++ b/src/i18n/locales/zh-Hans/layout.json @@ -168,5 +168,42 @@ "days-ago": "天前", "delete-project": "删除项目", "delete-project-confirmation": "您确定要删除此项目及其所有任务吗?此操作无法撤销。", - "please-select-model": "请在设置 > 模型中选择一个模型以继续。" + "please-select-model": "请在设置 > 模型中选择一个模型以继续。", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/i18n/locales/zh-Hant/layout.json b/src/i18n/locales/zh-Hant/layout.json index 1a33236cd..07dd2ae7c 100644 --- a/src/i18n/locales/zh-Hant/layout.json +++ b/src/i18n/locales/zh-Hant/layout.json @@ -168,5 +168,42 @@ "days-ago": "天前", "delete-project": "刪除專案", "delete-project-confirmation": "您確定要刪除此專案及其所有任務嗎?此操作無法撤銷。", - "please-select-model": "請在設定 > 模型中選擇一個模型以繼續。" + "please-select-model": "請在設定 > 模型中選擇一個模型以繼續。", + "cdp-browser-connection": "CDP Browser Connection", + "cdp-browser-connection-description": "Connect to a Chrome browser with remote debugging enabled", + "current-port": "Current Port:", + "cdp-port-check-description": "Check if a browser is available on a specific port", + "port-placeholder": "Port number (e.g., 9222)", + "checking": "Checking", + "check-port": "Check Port", + "browser-available": "Browser Available", + "browser-not-available": "Browser Not Available", + "cdp-browser-pool": "CDP Browser Pool", + "cdp-browser-pool-description": "Manage multiple CDP browsers for task execution", + "external": "External", + "launched": "Launched", + "stopped": "Stopped", + "port": "Port:", + "no-browsers-in-pool": "No browsers in pool", + "add-browsers-hint": "Add browsers using the check port tool above", + "invalid-port": "Please enter a valid port number (1-65535)", + "cdp-port-check-not-available": "CDP port check not available", + "failed-to-check-port": "Failed to check port", + "added-browser-to-pool": "Added external browser on port {{port}} to pool", + "failed-to-add-browser": "Failed to add browser to pool", + "launch-not-available": "Launch CDP browser not available", + "launching-browser": "Launching browser on port {{port}}...", + "browser-launched": "Browser launched successfully on port {{port}}", + "failed-to-launch-browser": "Failed to launch browser", + "browser-removed": "Browser removed from pool", + "failed-to-remove-browser": "Failed to remove browser", + "browser-opened": "Browser opened successfully for login", + "restart-not-available": "Restart function not available", + "browser-found": "Browser Found", + "browser-found-description": "A browser is running on port {{port}}. Would you like to use it for browser operations?", + "yes-use-browser": "Yes, Use This Browser", + "no-browser-found": "No Browser Found", + "no-browser-found-description": "No browser is running on port {{port}}. Would you like to launch a new Chrome browser with CDP enabled on this port?", + "yes-launch-browser": "Yes, Launch Browser", + "for-more-info": "For more information, check out our" } diff --git a/src/pages/Dashboard/Browser.tsx b/src/pages/Dashboard/Browser.tsx index 0cd575fc8..df2585268 100644 --- a/src/pages/Dashboard/Browser.tsx +++ b/src/pages/Dashboard/Browser.tsx @@ -15,7 +15,17 @@ import { fetchDelete, fetchGet, fetchPost } from '@/api/http'; import AlertDialog from '@/components/ui/alertDialog'; import { Button } from '@/components/ui/button'; -import { Cookie, Plus, RefreshCw, Trash2 } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { + CheckCircle2, + Cookie, + Globe, + Loader2, + Plus, + RefreshCw, + Trash2, + XCircle, +} from 'lucide-react'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; @@ -32,6 +42,21 @@ interface GroupedDomain { totalCookies: number; } +interface CdpPortStatus { + checking: boolean; + available: boolean | null; + error?: string; + data?: any; +} + +interface CdpBrowser { + id: string; + port: number; + isExternal: boolean; + name?: string; + addedAt: number; +} + export default function Browser() { const { t } = useTranslation(); const [loginLoading, setLoginLoading] = useState(false); @@ -40,9 +65,26 @@ export default function Browser() { const [deletingDomain, setDeletingDomain] = useState(null); const [deletingAll, setDeletingAll] = useState(false); const [showRestartDialog, setShowRestartDialog] = useState(false); - const [_cookiesBeforeBrowser, setCookiesBeforeBrowser] = useState(0); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + // CDP port configuration + const [cdpPort, setCdpPort] = useState(9223); + const [customPort, setCustomPort] = useState('9223'); + const [portStatus, setPortStatus] = useState({ + checking: false, + available: null, + }); + + // Dialog states + const [showUseExistingDialog, setShowUseExistingDialog] = useState(false); + const [showLaunchNewDialog, setShowLaunchNewDialog] = useState(false); + const [pendingPort, setPendingPort] = useState(null); + + // CDP Browser Pool + const [cdpBrowsers, setCdpBrowsers] = useState([]); + const [deletingBrowser, setDeletingBrowser] = useState(null); + const [runningPorts, setRunningPorts] = useState([]); + // Extract main domain (e.g., "aa.bb.cc" -> "bb.cc", "www.google.com" -> "google.com") const getMainDomain = (domain: string): string => { // Remove leading dot if present @@ -85,21 +127,218 @@ export default function Browser() { // Auto-load cookies on component mount useEffect(() => { handleLoadCookies(); + // Load current browser port on mount + loadCurrentBrowserPort(); + // Load CDP browser pool + loadCdpBrowsers(); + }, []); + + const loadCurrentBrowserPort = async () => { + if (window.electronAPI?.getBrowserPort) { + const port = await window.electronAPI.getBrowserPort(); + setCdpPort(port); + } + }; + + const loadCdpBrowsers = async () => { + if (window.electronAPI?.getCdpBrowsers) { + try { + const browsers = await window.electronAPI.getCdpBrowsers(); + setCdpBrowsers(browsers); + + // Also load running browser ports + if (window.electronAPI?.getRunningBrowserPorts) { + const ports = await window.electronAPI.getRunningBrowserPorts(); + setRunningPorts(ports); + } + } catch (error) { + console.error('Failed to load CDP browsers:', error); + } + } + }; + + // Periodically refresh running browser ports + useEffect(() => { + const interval = setInterval(async () => { + if (window.electronAPI?.getRunningBrowserPorts) { + try { + const ports = await window.electronAPI.getRunningBrowserPorts(); + setRunningPorts(ports); + } catch (error) { + console.error('Failed to refresh running ports:', error); + } + } + }, 3000); // Refresh every 3 seconds + + return () => clearInterval(interval); }, []); + const handleCheckPort = async () => { + const portNumber = parseInt(customPort); + + if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) { + toast.error(t('layout.invalid-port')); + return; + } + + setPortStatus({ checking: true, available: null }); + + try { + if (!window.electronAPI?.checkCdpPort) { + toast.error(t('layout.cdp-port-check-not-available')); + setPortStatus({ + checking: false, + available: false, + error: 'Not available', + }); + return; + } + + const result = await window.electronAPI.checkCdpPort(portNumber); + + if (result.available) { + setPortStatus({ + checking: false, + available: true, + data: result.data, + }); + // Browser exists, ask if user wants to use it + setPendingPort(portNumber); + setShowUseExistingDialog(true); + } else { + setPortStatus({ + checking: false, + available: false, + error: result.error, + }); + // No browser on this port, ask if user wants to launch one + setPendingPort(portNumber); + setShowLaunchNewDialog(true); + } + } catch (error: any) { + setPortStatus({ + checking: false, + available: false, + error: error.message, + }); + toast.error(error.message || t('layout.failed-to-check-port')); + } + }; + + const handleUseExistingBrowser = async () => { + setShowUseExistingDialog(false); + if (pendingPort) { + try { + if (window.electronAPI?.addCdpBrowser) { + const result = await window.electronAPI.addCdpBrowser( + pendingPort, + true, + `External Browser (${pendingPort})` + ); + if (result.success) { + toast.success( + t('layout.added-browser-to-pool', { port: pendingPort }) + ); + await loadCdpBrowsers(); + } else { + toast.error(result.error || t('layout.failed-to-add-browser')); + } + } + } catch (error: any) { + toast.error(error.message || t('layout.failed-to-add-browser')); + } + } + setPendingPort(null); + }; + + const handleLaunchNewBrowser = async () => { + setShowLaunchNewDialog(false); + + if (!pendingPort) { + return; + } + + const port = pendingPort; + setPendingPort(null); + + try { + if (!window.electronAPI?.launchCdpBrowser) { + toast.error(t('layout.launch-not-available')); + return; + } + + toast.loading(t('layout.launching-browser', { port }), { + id: 'launch-browser', + }); + + const result = await window.electronAPI.launchCdpBrowser(port); + + if (result.success) { + toast.success(t('layout.browser-launched', { port }), { + id: 'launch-browser', + }); + + // Add launched browser to pool + if (window.electronAPI?.addCdpBrowser) { + const addResult = await window.electronAPI.addCdpBrowser( + port, + false, + `Launched Browser (${port})` + ); + if (addResult.success) { + await loadCdpBrowsers(); + } else { + toast.error(addResult.error || t('layout.failed-to-add-browser')); + } + } + + // Update port status + setPortStatus({ + checking: false, + available: true, + data: result.data, + }); + } else { + toast.error(result.error || t('layout.failed-to-launch-browser'), { + id: 'launch-browser', + }); + } + } catch (error: any) { + toast.error(error.message || t('layout.failed-to-launch-browser'), { + id: 'launch-browser', + }); + } + }; + + const handleRemoveBrowser = async (browserId: string) => { + setDeletingBrowser(browserId); + try { + if (window.electronAPI?.removeCdpBrowser) { + const result = await window.electronAPI.removeCdpBrowser(browserId); + if (result.success) { + toast.success(t('layout.browser-removed')); + await loadCdpBrowsers(); + } else { + toast.error(result.error || t('layout.failed-to-remove-browser')); + } + } + } catch (error: any) { + toast.error(error.message || t('layout.failed-to-remove-browser')); + } finally { + setDeletingBrowser(null); + } + }; + const handleBrowserLogin = async () => { setLoginLoading(true); + const currentCookieCount = cookieDomains.reduce( + (sum, item) => sum + item.cookie_count, + 0 + ); try { - // Record current cookie count before opening browser - const currentCookieCount = cookieDomains.reduce( - (sum, item) => sum + item.cookie_count, - 0 - ); - setCookiesBeforeBrowser(currentCookieCount); - const response = await fetchPost('/browser/login'); if (response) { - toast.success('Browser opened successfully for login'); + toast.success(t('layout.browser-opened')); // Listen for browser close event to reload cookies const checkInterval = setInterval(async () => { try { @@ -135,7 +374,6 @@ export default function Browser() { } } catch (error) { // Browser might be closed - console.error(error); clearInterval(checkInterval); await handleLoadCookies(); } @@ -197,7 +435,7 @@ export default function Browser() { setDeletingDomain(null); } }; - 4; + const handleDeleteAll = async () => { setDeletingAll(true); try { @@ -220,7 +458,7 @@ export default function Browser() { if (window.electronAPI && window.electronAPI.restartApp) { window.electronAPI.restartApp(); } else { - toast.error('Restart function not available'); + toast.error(t('layout.restart-not-available')); } }; @@ -243,8 +481,40 @@ export default function Browser() { confirmVariant="information" /> + {/* Use Existing Browser Dialog */} + { + setShowUseExistingDialog(false); + setPendingPort(null); + }} + onConfirm={handleUseExistingBrowser} + title={t('layout.browser-found')} + message={t('layout.browser-found-description', { port: pendingPort })} + confirmText={t('layout.yes-use-browser')} + cancelText={t('layout.cancel')} + confirmVariant="information" + /> + + {/* Launch New Browser Dialog */} + { + setShowLaunchNewDialog(false); + setPendingPort(null); + }} + onConfirm={handleLaunchNewBrowser} + title={t('layout.no-browser-found')} + message={t('layout.no-browser-found-description', { + port: pendingPort, + })} + confirmText={t('layout.yes-launch-browser')} + cancelText={t('layout.cancel')} + confirmVariant="information" + /> + {/* Header Section */} -
+
@@ -261,7 +531,7 @@ export default function Browser() { {/* Content Section */}
-
+
+
+ + {portStatus.available !== null && ( +
+ {portStatus.available ? ( + <> + +
+
+ {t('layout.browser-available')} +
+ {portStatus.data && ( +
+ {portStatus.data['Browser']} -{' '} + {portStatus.data['User-Agent']?.split(' ')[0]} +
+ )} +
+ + ) : ( + <> + +
+
+ {t('layout.browser-not-available')} +
+
+ {portStatus.error} +
+
+ + )} +
+ )} +
+
+ + {/* CDP Browser Pool Section */} +
+
+
+
+
+ {t('layout.cdp-browser-pool')} +
+ + {runningPorts.length} / {cdpBrowsers.length}{' '} + {t('layout.running')} + +
+

+ {t('layout.cdp-browser-pool-description')} +

+
+
+ + {cdpBrowsers.length > 0 ? ( +
+ {cdpBrowsers.map((browser) => ( +
+
+
+ + {browser.name || `Browser ${browser.port}`} + + + {browser.isExternal + ? t('layout.external') + : t('layout.launched')} + + {/* Running status indicator */} + {runningPorts.includes(browser.port) ? ( + + + {t('layout.running')} + + ) : ( + !browser.isExternal && ( + + + {t('layout.stopped')} + + ) + )} +
+ + {t('layout.port')} {browser.port} + +
+ +
+ ))} +
+ ) : ( +
+ +
+ {t('layout.no-browsers-in-pool')} +
+

+ {t('layout.add-browsers-hint')} +

+
+ )} +
+ {/* Cookies Section */}