diff --git a/backend/app/controller/electron_browser.cjs b/backend/app/controller/electron_browser.cjs new file mode 100644 index 00000000..5b4fc698 --- /dev/null +++ b/backend/app/controller/electron_browser.cjs @@ -0,0 +1,393 @@ +const { app, BrowserWindow, ipcMain } = require('electron'); +const path = require('path'); + +// Parse command line arguments +const args = process.argv.slice(2); +const userDataDir = args[0]; +const cdpPort = args[1]; +const startUrl = args[2] || 'https://www.google.com'; + +// This must be called before app.ready +app.commandLine.appendSwitch('remote-debugging-port', cdpPort); + +console.log('[ELECTRON BROWSER] Starting with:'); +console.log(' Chrome version:', process.versions.chrome); +console.log(' User data dir (requested):', userDataDir); +console.log(' CDP port:', cdpPort); +console.log(' Start URL:', startUrl); + +// Set app paths - must be done before app.ready +// Do NOT use commandLine.appendSwitch('user-data-dir') as it conflicts with setPath +app.setPath('userData', userDataDir); +app.setPath('sessionData', userDataDir); + +app.whenReady().then(async () => { + const { session } = require('electron'); + const fs = require('fs'); + const path = require('path'); + + // Log actual paths being used + console.log('[ELECTRON BROWSER] Actual paths:'); + console.log(' app.getPath("userData"):', app.getPath('userData')); + console.log(' app.getPath("sessionData"):', app.getPath('sessionData')); + console.log(' app.getPath("cache"):', app.getPath('cache')); + console.log(' app.getPath("temp"):', app.getPath('temp')); + console.log(' process.argv:', process.argv); + + // Check command line switches + console.log('[ELECTRON BROWSER] Command line switches:'); + console.log(' user-data-dir:', app.commandLine.getSwitchValue('user-data-dir')); + console.log(' remote-debugging-port:', app.commandLine.getSwitchValue('remote-debugging-port')); + + // Log partition session info + const userLoginSession = session.fromPartition('persist:user_login'); + console.log('[ELECTRON BROWSER] Session info:'); + console.log(' Partition: persist:user_login'); + console.log(' Session storage path:', userLoginSession.getStoragePath()); + + // Check if Cookies file exists + const cookiesPath = path.join(app.getPath('userData'), 'Partitions', 'user_login', 'Cookies'); + console.log('[ELECTRON BROWSER] Cookies path:', cookiesPath); + console.log('[ELECTRON BROWSER] Cookies exists:', fs.existsSync(cookiesPath)); + if (fs.existsSync(cookiesPath)) { + const stats = fs.statSync(cookiesPath); + console.log('[ELECTRON BROWSER] Cookies file size:', stats.size, 'bytes'); + } + const win = new BrowserWindow({ + width: 1400, + height: 900, + title: 'Eigent Browser - Login', + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + webviewTag: true + } + }); + + // Create navigation bar and webview HTML + const html = ` + + + + + + + + + + + + + + +`; + + win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(html)); + + // Show window when ready + win.once('ready-to-show', () => { + win.show(); + + // Log cookies periodically to track changes + setInterval(async () => { + try { + const cookies = await userLoginSession.cookies.get({}); + console.log('[ELECTRON BROWSER] Current cookies count:', cookies.length); + if (cookies.length > 0) { + console.log('[ELECTRON BROWSER] Cookie domains:', [...new Set(cookies.map(c => c.domain))]); + } + } catch (error) { + console.error('[ELECTRON BROWSER] Failed to get cookies:', error); + } + }, 5000); // Check every 5 seconds + }); + + win.on('closed', async () => { + console.log('[ELECTRON BROWSER] Window closed, preparing to quit...'); + + // Flush storage data before quitting to ensure cookies are saved + try { + const { session } = require('electron'); + const fs = require('fs'); + const path = require('path'); + const userLoginSession = session.fromPartition('persist:user_login'); + + // Log cookies before flush + const cookiesBeforeFlush = await userLoginSession.cookies.get({}); + console.log('[ELECTRON BROWSER] Cookies count before flush:', cookiesBeforeFlush.length); + + // Flush storage + console.log('[ELECTRON BROWSER] Flushing storage data...'); + await userLoginSession.flushStorageData(); + console.log('[ELECTRON BROWSER] Storage data flushed successfully'); + + // Check cookies file after flush + const cookiesPath = path.join(app.getPath('userData'), 'Partitions', 'user_login', 'Cookies'); + if (fs.existsSync(cookiesPath)) { + const stats = fs.statSync(cookiesPath); + console.log('[ELECTRON BROWSER] Cookies file size after flush:', stats.size, 'bytes'); + } else { + console.log('[ELECTRON BROWSER] WARNING: Cookies file does not exist after flush!'); + } + } catch (error) { + console.error('[ELECTRON BROWSER] Failed to flush storage data:', error); + } + app.quit(); + }); +}); + +let isQuitting = false; + +app.on('before-quit', async (event) => { + if (isQuitting) return; + + // Prevent immediate quit to allow storage flush and cookie sync + event.preventDefault(); + isQuitting = true; + + console.log('[ELECTRON BROWSER] before-quit event triggered'); + + try { + const { session } = require('electron'); + const fs = require('fs'); + const path = require('path'); + const userLoginSession = session.fromPartition('persist:user_login'); + + // Log cookies before flush + const cookiesBeforeQuit = await userLoginSession.cookies.get({}); + console.log('[ELECTRON BROWSER] Cookies count before quit:', cookiesBeforeQuit.length); + if (cookiesBeforeQuit.length > 0) { + console.log('[ELECTRON BROWSER] Cookie domains before quit:', [...new Set(cookiesBeforeQuit.map(c => c.domain))]); + } + + // Flush storage + console.log('[ELECTRON BROWSER] Flushing storage on quit...'); + await userLoginSession.flushStorageData(); + console.log('[ELECTRON BROWSER] Storage data flushed on quit'); + } catch (error) { + console.error('[ELECTRON BROWSER] Failed to sync cookies:', error); + } finally { + console.log('[ELECTRON BROWSER] Exiting now...'); + // Force quit after sync + app.exit(0); + } +}); + +app.on('window-all-closed', () => { + if (!isQuitting) { + app.quit(); + } +}); diff --git a/backend/app/controller/tool_controller.py b/backend/app/controller/tool_controller.py index 29320da5..b9cb7ffd 100644 --- a/backend/app/controller/tool_controller.py +++ b/backend/app/controller/tool_controller.py @@ -347,410 +347,13 @@ async def open_browser_login(): "note": "Your login data will be saved in the profile." } - # Create Electron browser script with .cjs extension for CommonJS + # Use static Electron browser script electron_script_path = os.path.join(os.path.dirname(__file__), "electron_browser.cjs") - electron_script_content = ''' -const { app, BrowserWindow, ipcMain } = require('electron'); -const path = require('path'); -// Parse command line arguments -const args = process.argv.slice(2); -const userDataDir = args[0]; -const cdpPort = args[1]; -const startUrl = args[2] || 'https://www.google.com'; + # Verify script exists + if not os.path.exists(electron_script_path): + raise FileNotFoundError(f"Electron browser script not found: {electron_script_path}") -// This must be called before app.ready -app.commandLine.appendSwitch('remote-debugging-port', cdpPort); - -console.log('[ELECTRON BROWSER] Starting with:'); -console.log(' Chrome version:', process.versions.chrome); -console.log(' User data dir (requested):', userDataDir); -console.log(' CDP port:', cdpPort); -console.log(' Start URL:', startUrl); - -// Set app paths - must be done before app.ready -// Do NOT use commandLine.appendSwitch('user-data-dir') as it conflicts with setPath -app.setPath('userData', userDataDir); -app.setPath('sessionData', userDataDir); - -app.whenReady().then(async () => { - const { session } = require('electron'); - const fs = require('fs'); - const path = require('path'); - - // Log actual paths being used - console.log('[ELECTRON BROWSER] Actual paths:'); - console.log(' app.getPath("userData"):', app.getPath('userData')); - console.log(' app.getPath("sessionData"):', app.getPath('sessionData')); - console.log(' app.getPath("cache"):', app.getPath('cache')); - console.log(' app.getPath("temp"):', app.getPath('temp')); - console.log(' process.argv:', process.argv); - - // Check command line switches - console.log('[ELECTRON BROWSER] Command line switches:'); - console.log(' user-data-dir:', app.commandLine.getSwitchValue('user-data-dir')); - console.log(' remote-debugging-port:', app.commandLine.getSwitchValue('remote-debugging-port')); - - // Log partition session info - const userLoginSession = session.fromPartition('persist:user_login'); - console.log('[ELECTRON BROWSER] Session info:'); - console.log(' Partition: persist:user_login'); - console.log(' Session storage path:', userLoginSession.getStoragePath()); - - // Check if Cookies file exists - const cookiesPath = path.join(app.getPath('userData'), 'Partitions', 'user_login', 'Cookies'); - console.log('[ELECTRON BROWSER] Cookies path:', cookiesPath); - console.log('[ELECTRON BROWSER] Cookies exists:', fs.existsSync(cookiesPath)); - if (fs.existsSync(cookiesPath)) { - const stats = fs.statSync(cookiesPath); - console.log('[ELECTRON BROWSER] Cookies file size:', stats.size, 'bytes'); - } - const win = new BrowserWindow({ - width: 1400, - height: 900, - title: 'Eigent Browser - Login', - webPreferences: { - nodeIntegration: true, - contextIsolation: false, - webviewTag: true - } - }); - - // Create navigation bar and webview - const html = ` - - - - - - - - - - - - - - -`; - - win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(html)); - - // Show window when ready - win.once('ready-to-show', () => { - win.show(); - - // Log cookies periodically to track changes - setInterval(async () => { - try { - const cookies = await userLoginSession.cookies.get({}); - console.log('[ELECTRON BROWSER] Current cookies count:', cookies.length); - if (cookies.length > 0) { - console.log('[ELECTRON BROWSER] Cookie domains:', [...new Set(cookies.map(c => c.domain))]); - } - } catch (error) { - console.error('[ELECTRON BROWSER] Failed to get cookies:', error); - } - }, 5000); // Check every 5 seconds - }); - - win.on('closed', async () => { - console.log('[ELECTRON BROWSER] Window closed, preparing to quit...'); - - // Flush storage data before quitting to ensure cookies are saved - try { - const { session } = require('electron'); - const fs = require('fs'); - const path = require('path'); - const userLoginSession = session.fromPartition('persist:user_login'); - - // Log cookies before flush - const cookiesBeforeFlush = await userLoginSession.cookies.get({}); - console.log('[ELECTRON BROWSER] Cookies count before flush:', cookiesBeforeFlush.length); - - // Flush storage - console.log('[ELECTRON BROWSER] Flushing storage data...'); - await userLoginSession.flushStorageData(); - console.log('[ELECTRON BROWSER] Storage data flushed successfully'); - - // Check cookies file after flush - const cookiesPath = path.join(app.getPath('userData'), 'Partitions', 'user_login', 'Cookies'); - if (fs.existsSync(cookiesPath)) { - const stats = fs.statSync(cookiesPath); - console.log('[ELECTRON BROWSER] Cookies file size after flush:', stats.size, 'bytes'); - } else { - console.log('[ELECTRON BROWSER] WARNING: Cookies file does not exist after flush!'); - } - } catch (error) { - console.error('[ELECTRON BROWSER] Failed to flush storage data:', error); - } - app.quit(); - }); -}); - -let isQuitting = false; - -app.on('before-quit', async (event) => { - if (isQuitting) return; - - // Prevent immediate quit to allow storage flush and cookie sync - event.preventDefault(); - isQuitting = true; - - console.log('[ELECTRON BROWSER] before-quit event triggered'); - - try { - const { session } = require('electron'); - const fs = require('fs'); - const path = require('path'); - const userLoginSession = session.fromPartition('persist:user_login'); - - // Log cookies before flush - const cookiesBeforeQuit = await userLoginSession.cookies.get({}); - console.log('[ELECTRON BROWSER] Cookies count before quit:', cookiesBeforeQuit.length); - if (cookiesBeforeQuit.length > 0) { - console.log('[ELECTRON BROWSER] Cookie domains before quit:', [...new Set(cookiesBeforeQuit.map(c => c.domain))]); - } - - // Flush storage - console.log('[ELECTRON BROWSER] Flushing storage on quit...'); - await userLoginSession.flushStorageData(); - console.log('[ELECTRON BROWSER] Storage data flushed on quit'); - } catch (error) { - console.error('[ELECTRON BROWSER] Failed to sync cookies:', error); - } finally { - console.log('[ELECTRON BROWSER] Exiting now...'); - // Force quit after sync - app.exit(0); - } -}); - -app.on('window-all-closed', () => { - if (!isQuitting) { - app.quit(); - } -}); -''' - - # Write the Electron script - with open(electron_script_path, 'w') as f: - f.write(electron_script_content) - - # Find Electron executable - # Try to use the same Electron version as the main app electron_cmd = "npx" electron_args = [ electron_cmd, @@ -775,7 +378,9 @@ app.on('window-all-closed', () => { cwd=app_dir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # Redirect stderr to stdout - universal_newlines=True, + text=True, + encoding='utf-8', + errors='replace', # Replace undecodable chars instead of crashing bufsize=1 # Line buffered ) @@ -791,17 +396,7 @@ app.on('window-all-closed', () => { # Wait a bit for Electron to start import asyncio await asyncio.sleep(3) - - # Clean up the script file after a delay - async def cleanup_script(): - await asyncio.sleep(10) - try: - os.remove(electron_script_path) - except: - pass - - asyncio.create_task(cleanup_script()) - + logger.info(f"[PROFILE USER LOGIN] Electron browser launched with PID {process.pid}") return { diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 404ab646..b9a95f82 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -517,7 +517,14 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): if not sub_tasks: sub_tasks = getattr(task_lock, "decompose_sub_tasks", []) sub_tasks = update_sub_tasks(sub_tasks, update_tasks) - add_sub_tasks(camel_task, item.data.task) + # Also update camel_task.subtasks to remove deleted tasks (used by to_sub_tasks) + update_sub_tasks(camel_task.subtasks, update_tasks) + # Add new tasks (with empty id) to both camel_task and sub_tasks + new_tasks = add_sub_tasks(camel_task, item.data.task) + # Also add new tasks to sub_tasks so workforce.eigent_start uses correct list + sub_tasks.extend(new_tasks) + # Save updated sub_tasks back to task_lock so Action.start uses the correct list + setattr(task_lock, "decompose_sub_tasks", sub_tasks) summary_task_content_local = getattr(task_lock, "summary_task_content", summary_task_content) yield to_sub_tasks(camel_task, summary_task_content_local) elif item.action == Action.add_task: @@ -1103,15 +1110,18 @@ def update_sub_tasks(sub_tasks: list[Task], update_tasks: dict[str, TaskContent] return sub_tasks -def add_sub_tasks(camel_task: Task, update_tasks: list[TaskContent]): +def add_sub_tasks(camel_task: Task, update_tasks: list[TaskContent]) -> list[Task]: + """Add new tasks (with empty id) to camel_task and return the list of added tasks.""" + added_tasks = [] for item in update_tasks: - if item.id == "": # - camel_task.add_subtask( - Task( - content=item.content, - id=f"{camel_task.id}.{len(camel_task.subtasks) + 1}", - ) + if item.id == "": + new_task = Task( + content=item.content, + id=f"{camel_task.id}.{len(camel_task.subtasks) + 1}", ) + camel_task.add_subtask(new_task) + added_tasks.append(new_task) + return added_tasks async def question_confirm(agent: ListenChatAgent, prompt: str, task_lock: TaskLock | None = None) -> bool: diff --git a/backend/app/utils/toolkit/terminal_toolkit.py b/backend/app/utils/toolkit/terminal_toolkit.py index 9109dcab..659decd5 100644 --- a/backend/app/utils/toolkit/terminal_toolkit.py +++ b/backend/app/utils/toolkit/terminal_toolkit.py @@ -169,7 +169,7 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): source_cfg = os.path.join(source_venv, "pyvenv.cfg") python_home = None - with open(source_cfg, 'r') as f: + with open(source_cfg, 'r', encoding='utf-8') as f: for line in f: if line.startswith('home = '): python_home = line.split('=', 1)[1].strip() diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index 363e6462..9579bec2 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -129,6 +129,9 @@ class Workforce(BaseWorkforce): logger.debug(f"[WF-LIFECYCLE] eigent_start called with {len(subtasks)} subtasks", extra={ "api_task_id": self.api_task_id }) + # Clear existing pending tasks to use the user-edited task list + # (tasks may have been added during decomposition before user edits) + self._pending_tasks.clear() self._pending_tasks.extendleft(reversed(subtasks)) self.save_snapshot("Initial task decomposition") diff --git a/electron/main/index.ts b/electron/main/index.ts index 7b3c1d45..041e8975 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1275,6 +1275,7 @@ async function createWindow() { minWidth: 1050, minHeight: 650, frame: false, + show: false, // Don't show until content is ready to avoid white screen transparent: true, backgroundColor: '#00000000', titleBarStyle: isMac ? 'hidden' : undefined, @@ -1316,6 +1317,41 @@ async function createWindow() { }); } + // ==================== Handle renderer crashes and failed loads ==================== + win.webContents.on('render-process-gone', (event, details) => { + log.error('[RENDERER] Process gone:', details.reason, details.exitCode); + if (win && !win.isDestroyed()) { + // Reload the window after a brief delay + setTimeout(() => { + if (win && !win.isDestroyed()) { + log.info('[RENDERER] Attempting to reload after crash...'); + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL); + } else { + win.loadFile(indexHtml); + } + } + }, 1000); + } + }); + + win.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => { + log.error(`[RENDERER] Failed to load: ${errorCode} - ${errorDescription} - ${validatedURL}`); + // Retry loading after a delay + if (errorCode !== -3) { // -3 is USER_CANCELLED, don't retry + setTimeout(() => { + if (win && !win.isDestroyed()) { + log.info('[RENDERER] Retrying load after failure...'); + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL); + } else { + win.loadFile(indexHtml); + } + } + }, 2000); + } + }); + // Main window now uses default userData directly with partition 'persist:main_window' // No migration needed - data is already persistent @@ -1563,9 +1599,15 @@ async function createWindow() { win.loadFile(indexHtml); } - // Wait for window to be ready + // Wait for window to be ready with timeout await new Promise((resolve) => { + const loadTimeout = setTimeout(() => { + log.warn('Window content load timeout (10s), showing window anyway...'); + resolve(); + }, 10000); + win!.webContents.once('did-finish-load', () => { + clearTimeout(loadTimeout); log.info( 'Window content loaded, starting dependency check immediately...' ); @@ -1573,6 +1615,12 @@ async function createWindow() { }); }); + // Show window now that content is loaded (or timeout reached) + if (win && !win.isDestroyed()) { + win.show(); + log.info('Window shown after content loaded'); + } + // Mark window as ready and process any queued protocol URLs isWindowReady = true; log.info('Window is ready, processing queued protocol URLs...'); diff --git a/package.json b/package.json index b1ffee24..bf47fee7 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "csv-parser": "^3.2.0", "dompurify": "^3.2.7", "electron-log": "^5.4.0", - "electron-updater": "^6.7.3", + "electron-updater": "^6.3.9", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.17.0", @@ -118,11 +118,11 @@ "@vitejs/plugin-react": "^4.3.3", "@vitest/coverage-v8": "^2.1.9", "autoprefixer": "^10.4.20", - "electron": "^33.4.11", - "electron-builder": "^26.4.0", + "electron": "^33.2.0", + "electron-builder": "^24.13.3", "electron-devtools-installer": "^4.0.0", "i18next": "^25.4.2", - "jsdom": "^27.4.0", + "jsdom": "^26.1.0", "postcss": "^8.4.49", "postcss-import": "^16.1.0", "react": "^18.3.1", @@ -140,16 +140,10 @@ "@storybook/addon-a11y": "^10.1.11", "@storybook/addon-docs": "^10.1.11" }, - "overrides": { - "glob": "^10.4.5" - }, "pnpm": { - "neverBuiltDependencies": [], - "overrides": { - "glob": "^10.4.5" - } + "neverBuiltDependencies": [] }, "engines": { - "node": ">=20.0.0 <23.0.0" + "node": ">=18.0.0 <23.0.0" } } diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index f44e2639..04872073 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -870,6 +870,13 @@ const chatStore = (initial?: Partial) => createStore()( clearStreamingDecomposeText(currentTaskId); // Clean up TTFT tracking delete ttftTracking[currentTaskId]; + + // Check if task is already confirmed - don't overwrite user edits + const existingToSubTasksMessage = tasks[currentTaskId].messages.findLast((m: Message) => m.step === 'to_sub_tasks'); + if (existingToSubTasksMessage?.isConfirm) { + return; + } + // Check if this is a multi-turn scenario after task completion const isMultiTurnAfterCompletion = tasks[currentTaskId].status === 'finished'; @@ -2171,20 +2178,14 @@ const chatStore = (initial?: Partial) => createStore()( // record task start time setTaskTime(taskId, Date.now()); + // Filter out empty tasks from the user-edited taskInfo const taskInfo = tasks[taskId].taskInfo.filter((task) => task.content !== '') setTaskInfo(taskId, taskInfo) - // Also update taskRunning with the filtered tasks to keep counts consistent - const taskRunning = tasks[taskId].taskRunning.filter((task) => task.content !== '') - setTaskRunning(taskId, taskRunning) - if (!type) { - await fetchPut(`/task/${project_id}`, { - task: taskInfo, - }); - await fetchPost(`/task/${project_id}/start`, {}); + // Sync taskRunning with the filtered taskInfo (user edits should be reflected + setTaskRunning(taskId, taskInfo.map(task => ({ ...task }))) - setActiveWorkSpace(taskId, 'workflow') - setStatus(taskId, 'running') - } + // IMPORTANT: Set isConfirm BEFORE sending API requests to prevent race condition + // where backend sends to_sub_tasks SSE event before we mark task as confirmed let messages = [...tasks[taskId].messages] const cardTaskIndex = messages.findLastIndex((message) => message.step === 'to_sub_tasks') if (cardTaskIndex !== -1) { @@ -2196,6 +2197,16 @@ const chatStore = (initial?: Partial) => createStore()( setMessages(taskId, messages) } + if (!type) { + await fetchPut(`/task/${project_id}`, { + task: taskInfo, + }); + await fetchPost(`/task/${project_id}/start`, {}); + + setActiveWorkSpace(taskId, 'workflow') + setStatus(taskId, 'running') + } + // Reset editing state after manual confirmation so next round can auto-start setIsTaskEdit(taskId, false); }, @@ -2348,10 +2359,10 @@ const chatStore = (initial?: Partial) => createStore()( updateTaskInfo(index: number, content: string) { const { tasks, activeTaskId, setTaskInfo } = get() if (!activeTaskId) return - let targetTaskInfo = [...tasks[activeTaskId].taskInfo] - if (targetTaskInfo) { - targetTaskInfo[index].content = content - } + // Deep copy the array with updated item to ensure React detects the change + const targetTaskInfo = tasks[activeTaskId].taskInfo.map((item, i) => + i === index ? { ...item, content } : item + ) setTaskInfo(activeTaskId, targetTaskInfo) }, deleteTaskInfo(index: number) {