diff --git a/README.md b/README.md index b2b627b8..877e0525 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,19 @@ npm run dev > Note: This mode connects to Eigent cloud services and requires account registration. For a fully standalone experience, use [Local Deployment](#-local-deployment-recommended) instead. +#### Updating Dependencies + +After pulling new code (`git pull`), update both frontend and backend dependencies: + +```bash +# 1. Update frontend dependencies (in project root) +npm install + +# 2. Update backend/Python dependencies (in backend directory) +cd backend +uv sync +``` + ### 🏢 Enterprise For organizations requiring maximum security, customization, and control: diff --git a/README_CN.md b/README_CN.md index e71d3054..c2c90f16 100644 --- a/README_CN.md +++ b/README_CN.md @@ -124,6 +124,19 @@ npm run dev #### 3. 本地开发(使用完全和云端服务分离的版本) [server/README_CN.md](./server/README_CN.md) +#### 4. 更新依赖 + +拉取新代码(`git pull`)后,需要分别更新前端和后端依赖: + +```bash +# 1. 更新前端依赖(在项目根目录) +npm install + +# 2. 更新后端/Python 依赖(在 backend 目录) +cd backend +uv sync +``` + ### 🏢 企业版 适合需要最高安全性、定制化和控制的组织: diff --git a/README_JA.md b/README_JA.md index 93079536..08f73105 100644 --- a/README_JA.md +++ b/README_JA.md @@ -114,6 +114,19 @@ npm run dev > 注:このモードはEigentクラウドサービスに接続し、アカウント登録が必要です。完全にスタンドアロンで使用する場合は、代わりに[ローカルデプロイメント](#-ローカルデプロイメント推奨)を使用してください。 +#### 依存関係の更新 + +新しいコードを取得(`git pull`)した後、フロントエンドとバックエンドの両方の依存関係を更新します: + +```bash +# 1. フロントエンド依存関係を更新(プロジェクトルートで) +npm install + +# 2. バックエンド/Python依存関係を更新(backendディレクトリで) +cd backend +uv sync +``` + ### 🏢 エンタープライズ 最大限のセキュリティ、カスタマイズ、制御を必要とする組織向け: diff --git a/README_PT-BR.md b/README_PT-BR.md index e727366e..a26c2a2a 100644 --- a/README_PT-BR.md +++ b/README_PT-BR.md @@ -115,6 +115,19 @@ npm run dev > Nota: Este modo se conecta aos serviços em nuvem do Eigent e requer registro de conta. Para uma experiência totalmente independente, utilize a [Implantação Local](#-implantação-local-recomendado) em vez disso. +#### Atualizando Dependências + +Após baixar novo código (`git pull`), atualize as dependências do frontend e do backend: + +```bash +# 1. Atualizar dependências do frontend (no diretório raiz do projeto) +npm install + +# 2. Atualizar dependências do backend/Python (no diretório backend) +cd backend +uv sync +``` + ### 🏢 Empresarial Para organizações que requerem máxima segurança, personalização e controle: diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 6c38c94e..95920af5 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -859,7 +859,8 @@ async def developer_agent(options: Chat): terminal_toolkit = TerminalToolkit( options.project_id, - Agents.document_agent, + Agents.developer_agent, + working_directory=working_directory, safe_mode=True, clone_current_env=True, ) @@ -1069,6 +1070,7 @@ def browser_agent(options: Chat): terminal_toolkit = TerminalToolkit( options.project_id, Agents.browser_agent, + working_directory=working_directory, safe_mode=True, clone_current_env=True, ) @@ -1259,6 +1261,7 @@ async def document_agent(options: Chat): terminal_toolkit = TerminalToolkit( options.project_id, Agents.document_agent, + working_directory=working_directory, safe_mode=True, clone_current_env=True, ) @@ -1482,6 +1485,7 @@ def multi_modal_agent(options: Chat): terminal_toolkit = TerminalToolkit( options.project_id, agent_name=Agents.multi_modal_agent, + working_directory=working_directory, safe_mode=True, clone_current_env=True, ) @@ -1676,6 +1680,7 @@ async def social_medium_agent(options: Chat): *TerminalToolkit( options.project_id, agent_name=Agents.social_medium_agent, + working_directory=working_directory, clone_current_env=True, ).get_tools(), *NoteTakingToolkit( diff --git a/backend/app/utils/toolkit/terminal_toolkit.py b/backend/app/utils/toolkit/terminal_toolkit.py index d3ca259d..07733239 100644 --- a/backend/app/utils/toolkit/terminal_toolkit.py +++ b/backend/app/utils/toolkit/terminal_toolkit.py @@ -1,7 +1,9 @@ import asyncio import logging import os +import platform import shutil +import subprocess import threading import time from concurrent.futures import ThreadPoolExecutor @@ -17,6 +19,20 @@ from utils import traceroot_wrapper as traceroot logger = traceroot.get_logger("terminal_toolkit") +# App version - should match electron app version +# TODO: Consider getting this from a shared config +APP_VERSION = "0.0.80" + + +def get_terminal_base_venv_path() -> str: + """Get the path to the terminal base venv created during app installation.""" + return os.path.join( + os.path.expanduser("~"), + ".eigent", + "venvs", + f"terminal_base-{APP_VERSION}" + ) + @auto_listen_toolkit(BaseTerminalToolkit) class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): @@ -40,13 +56,18 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): self.api_task_id = api_task_id if agent_name is not None: self.agent_name = agent_name + + # Get base directory from environment + base_dir = env("file_save_path", os.path.expanduser("~/.eigent/terminal/")) + if working_directory is None: - working_directory = env("file_save_path", os.path.expanduser("~/.eigent/terminal/")) + working_directory = base_dir + self._agent_venv_dir = os.path.join(base_dir, self.agent_name) logger.debug(f"Initializing TerminalToolkit for agent={self.agent_name}", extra={ "api_task_id": api_task_id, "working_directory": working_directory, - "clone_current_env": clone_current_env + "agent_venv_dir": self._agent_venv_dir, }) if TerminalToolkit._thread_pool is None: @@ -63,16 +84,10 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): session_logs_dir=session_logs_dir, safe_mode=safe_mode, allowed_commands=allowed_commands, - clone_current_env=clone_current_env, - install_dependencies=[ - "pandas", - "numpy", - "matplotlib", - "requests", - "openpyxl", - ], + clone_current_env=True, + install_dependencies=[], ) - + # Auto-register with TaskLock for cleanup when task ends from app.service.task import get_task_lock_if_exists task_lock = get_task_lock_if_exists(api_task_id) @@ -83,6 +98,116 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): "working_directory": working_directory }) + def _setup_cloned_environment(self): + """Override to clone from terminal_base venv instead of current process venv. + + Creates a lightweight clone using symlinks to the terminal_base venv, + which contains pre-installed packages (pandas, numpy, matplotlib, etc.). + """ + self.cloned_env_path = os.path.join(self._agent_venv_dir, ".venv") + terminal_base_path = get_terminal_base_venv_path() + + # Check if terminal_base exists + if platform.system() == 'Windows': + base_python = os.path.join(terminal_base_path, "Scripts", "python.exe") + else: + base_python = os.path.join(terminal_base_path, "bin", "python") + + if not os.path.exists(base_python): + logger.warning( + f"Terminal base venv not found at {terminal_base_path}, " + "falling back to system Python" + ) + return + + # Check if cloned env already exists + if platform.system() == 'Windows': + cloned_python = os.path.join(self.cloned_env_path, "Scripts", "python.exe") + else: + cloned_python = os.path.join(self.cloned_env_path, "bin", "python") + + if os.path.exists(cloned_python): + logger.info(f"Using existing cloned environment: {self.cloned_env_path}") + self.python_executable = cloned_python + return + + logger.info(f"Cloning terminal_base venv to: {self.cloned_env_path}") + + try: + # Create the cloned venv directory + os.makedirs(self.cloned_env_path, exist_ok=True) + + # Clone using symlinks for efficiency + # We need to create proper venv structure with symlinks to terminal_base + self._clone_venv_with_symlinks(terminal_base_path, self.cloned_env_path) + + self.python_executable = cloned_python + logger.info(f"Successfully cloned environment to: {self.cloned_env_path}") + + except Exception as e: + logger.error(f"Failed to clone terminal_base venv: {e}", exc_info=True) + # Cleanup partial clone + if os.path.exists(self.cloned_env_path): + shutil.rmtree(self.cloned_env_path, ignore_errors=True) + logger.warning("Falling back to system Python") + + def _get_venv_path(self): + return None + + def _clone_venv_with_symlinks(self, source_venv: str, target_venv: str): + """Clone a venv using symlinks for efficiency. + + Only creates the minimum structure needed: pyvenv.cfg, bin/python, and lib symlink. + Activation scripts are not needed since we use python_executable directly. + """ + is_windows = platform.system() == 'Windows' + + # Read source pyvenv.cfg to get Python home + source_cfg = os.path.join(source_venv, "pyvenv.cfg") + python_home = None + + with open(source_cfg, 'r') as f: + for line in f: + if line.startswith('home = '): + python_home = line.split('=', 1)[1].strip() + break + + if not python_home: + raise RuntimeError(f"Could not determine Python home from {source_cfg}") + + # Copy pyvenv.cfg (simpler than recreating) + shutil.copy2(source_cfg, os.path.join(target_venv, "pyvenv.cfg")) + + if is_windows: + # Windows: copy executables from source + target_bin = os.path.join(target_venv, "Scripts") + os.makedirs(target_bin, exist_ok=True) + source_scripts = os.path.join(source_venv, "Scripts") + for exe in ["python.exe", "pythonw.exe"]: + src = os.path.join(source_scripts, exe) + if os.path.exists(src): + shutil.copy2(src, os.path.join(target_bin, exe)) + # Use directory junction for Lib (no admin rights needed, unlike symlink) + source_lib = os.path.join(source_venv, "Lib") + target_lib = os.path.join(target_venv, "Lib") + subprocess.run(["cmd", "/c", "mklink", "/J", target_lib, source_lib], + check=True, capture_output=True) + else: + # Unix: symlink python executable and lib directory + target_bin = os.path.join(target_venv, "bin") + os.makedirs(target_bin, exist_ok=True) + + # Symlink python to the base Python + python_exe = os.path.join(python_home, "python3") + if not os.path.exists(python_exe): + python_exe = os.path.join(python_home, "python") + os.symlink(python_exe, os.path.join(target_bin, "python")) + os.symlink("python", os.path.join(target_bin, "python3")) + + # Symlink lib directory + source_lib = os.path.join(source_venv, "lib") + os.symlink(source_lib, os.path.join(target_venv, "lib")) + def _write_to_log(self, log_file: str, content: str) -> None: r"""Write content to log file with optional ANSI stripping. @@ -198,32 +323,34 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): return # Remove cloned env (.venv) if it exists - if self.cloned_env_path and os.path.exists(self.cloned_env_path): + cloned_env_path = getattr(self, 'cloned_env_path', None) + if cloned_env_path and os.path.exists(cloned_env_path): try: - shutil.rmtree(self.cloned_env_path) + shutil.rmtree(cloned_env_path) logger.info("Removed cloned venv", extra={ "api_task_id": self.api_task_id, - "path": self.cloned_env_path + "path": cloned_env_path }) except Exception as e: logger.warning("Failed to remove cloned venv", extra={ "api_task_id": self.api_task_id, - "path": self.cloned_env_path, + "path": cloned_env_path, "error": str(e) }) # Remove initial env (.initial_env) if it exists - if self.initial_env_path and os.path.exists(self.initial_env_path): + initial_env_path = getattr(self, 'initial_env_path', None) + if initial_env_path and os.path.exists(initial_env_path): try: - shutil.rmtree(self.initial_env_path) + shutil.rmtree(initial_env_path) logger.info("Removed initial env", extra={ "api_task_id": self.api_task_id, - "path": self.initial_env_path + "path": initial_env_path }) except Exception as e: logger.warning("Failed to remove initial env", extra={ "api_task_id": self.api_task_id, - "path": self.initial_env_path, + "path": initial_env_path, "error": str(e) }) diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index daaebb0f..e5babcc7 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -8,10 +8,12 @@ import { getBinaryPath, getCachePath, getVenvPath, + getTerminalVenvPath, getUvEnv, cleanupOldVenvs, isBinaryExists, runInstallScript, + TERMINAL_BASE_PACKAGES, } from './utils/process'; import { spawn } from 'child_process'; import { safeMainWindowSend } from './utils/safeWebContentsSend'; @@ -482,6 +484,124 @@ const runInstall = (extraArgs: string[], version: string) => { }); }; +/** + * Install terminal base venv with common packages for terminal tasks. + * This is a lightweight venv separate from the backend venv. + */ +async function installTerminalBaseVenv(version: string): Promise { + const terminalVenvPath = getTerminalVenvPath(version); + const pythonPath = process.platform === 'win32' + ? path.join(terminalVenvPath, 'Scripts', 'python.exe') + : path.join(terminalVenvPath, 'bin', 'python'); + + // Check if terminal base venv already exists and is valid + if (fs.existsSync(pythonPath)) { + log.info('[DEPS INSTALL] Terminal base venv already exists, skipping creation'); + return { message: 'Terminal base venv already exists', success: true }; + } + + log.info('[DEPS INSTALL] Creating terminal base venv...'); + safeMainWindowSend('install-dependencies-log', { + type: 'stdout', + data: 'Creating terminal base environment...\n', + }); + + try { + // Create the venv using uv + await new Promise((resolve, reject) => { + const createVenv = spawn( + uv_path, + ['venv', '--python', '3.10', terminalVenvPath], + { + env: { + ...process.env, + UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'), + }, + } + ); + + createVenv.stdout.on('data', (data) => { + log.info(`[DEPS INSTALL] terminal venv: ${data}`); + }); + + createVenv.stderr.on('data', (data) => { + log.info(`[DEPS INSTALL] terminal venv: ${data}`); + }); + + createVenv.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Failed to create terminal venv, exit code: ${code}`)); + } + }); + + createVenv.on('error', reject); + }); + + // Install base packages + log.info('[DEPS INSTALL] Installing terminal base packages...'); + safeMainWindowSend('install-dependencies-log', { + type: 'stdout', + data: `Installing packages: ${TERMINAL_BASE_PACKAGES.join(', ')}...\n`, + }); + + await new Promise((resolve, reject) => { + const installPkgs = spawn( + uv_path, + [ + 'pip', + 'install', + '--python', + pythonPath, + ...TERMINAL_BASE_PACKAGES, + ], + { + env: { + ...process.env, + UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'), + }, + } + ); + + installPkgs.stdout.on('data', (data) => { + log.info(`[DEPS INSTALL] terminal packages: ${data}`); + safeMainWindowSend('install-dependencies-log', { + type: 'stdout', + data: data.toString(), + }); + }); + + installPkgs.stderr.on('data', (data) => { + log.info(`[DEPS INSTALL] terminal packages: ${data}`); + safeMainWindowSend('install-dependencies-log', { + type: 'stdout', + data: data.toString(), + }); + }); + + installPkgs.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Failed to install terminal packages, exit code: ${code}`)); + } + }); + + installPkgs.on('error', reject); + }); + + log.info('[DEPS INSTALL] Terminal base venv created successfully'); + return { message: 'Terminal base venv created successfully', success: true }; + } catch (error) { + log.error('[DEPS INSTALL] Failed to create terminal base venv:', error); + return { + message: `Failed to create terminal base venv: ${error}`, + success: false, + }; + } +} + export async function installDependencies( version: string ): Promise { @@ -890,6 +1010,13 @@ export async function installDependencies( // try default install const installSuccess = await runInstall([], version); if (installSuccess.success) { + // Install terminal base venv (lightweight venv for terminal tasks) + log.info('[DEPS INSTALL] Installing terminal base venv...'); + const terminalResult = await installTerminalBaseVenv(version); + if (!terminalResult.success) { + log.warn('[DEPS INSTALL] Terminal base venv installation failed, but continuing...', terminalResult.message); + } + // Install hybrid_browser_toolkit npm dependencies after Python packages are installed log.info( '[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...' @@ -922,6 +1049,13 @@ export async function installDependencies( : await runInstall([], version); if (mirrorInstallSuccess.success) { + // Install terminal base venv (lightweight venv for terminal tasks) + log.info('[DEPS INSTALL] Installing terminal base venv...'); + const terminalResult = await installTerminalBaseVenv(version); + if (!terminalResult.success) { + log.warn('[DEPS INSTALL] Terminal base venv installation failed, but continuing...', terminalResult.message); + } + // Install hybrid_browser_toolkit npm dependencies after Python packages are installed log.info( '[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...' diff --git a/electron/main/utils/process.ts b/electron/main/utils/process.ts index c8647e68..c6d861e3 100644 --- a/electron/main/utils/process.ts +++ b/electron/main/utils/process.ts @@ -185,6 +185,43 @@ export function getVenvsBaseDir(): string { return path.join(os.homedir(), '.eigent', 'venvs'); } +/** + * Packages to install in the terminal base venv. + * These are commonly used packages for terminal tasks (data processing, visualization, etc.) + * Keep this list minimal - users can install additional packages as needed. + */ +export const TERMINAL_BASE_PACKAGES = [ + 'pandas', + 'numpy', + 'matplotlib', + 'requests', + 'openpyxl', + 'beautifulsoup4', + 'pillow', +]; + +/** + * Get path to the terminal base venv. + * This is a lightweight venv with common packages for terminal tasks, + * separate from the backend venv. + */ +export function getTerminalVenvPath(version: string): string { + const venvDir = path.join( + os.homedir(), + '.eigent', + 'venvs', + `terminal_base-${version}` + ); + + // Ensure venvs directory exists + const venvsBaseDir = path.dirname(venvDir); + if (!fs.existsSync(venvsBaseDir)) { + fs.mkdirSync(venvsBaseDir, { recursive: true }); + } + + return venvDir; +} + export async function cleanupOldVenvs(currentVersion: string): Promise { const venvsBaseDir = getVenvsBaseDir(); @@ -193,23 +230,34 @@ export async function cleanupOldVenvs(currentVersion: string): Promise { return; } + // Patterns to match: backend-{version} and terminal_base-{version} + const venvPatterns = [ + { prefix: 'backend-', regex: /^backend-(.+)$/ }, + { prefix: 'terminal_base-', regex: /^terminal_base-(.+)$/ }, + ]; + try { const entries = fs.readdirSync(venvsBaseDir, { withFileTypes: true }); for (const entry of entries) { - if (entry.isDirectory() && entry.name.startsWith('backend-')) { - const versionMatch = entry.name.match(/^backend-(.+)$/); - if (versionMatch && versionMatch[1] !== currentVersion) { - const oldVenvPath = path.join(venvsBaseDir, entry.name); - console.log(`Cleaning up old venv: ${oldVenvPath}`); + if (!entry.isDirectory()) continue; - try { - // Remove old venv directory recursively - fs.rmSync(oldVenvPath, { recursive: true, force: true }); - console.log(`Successfully removed old venv: ${entry.name}`); - } catch (err) { - console.error(`Failed to remove old venv ${entry.name}:`, err); + for (const pattern of venvPatterns) { + if (entry.name.startsWith(pattern.prefix)) { + const versionMatch = entry.name.match(pattern.regex); + if (versionMatch && versionMatch[1] !== currentVersion) { + const oldVenvPath = path.join(venvsBaseDir, entry.name); + console.log(`Cleaning up old venv: ${oldVenvPath}`); + + try { + // Remove old venv directory recursively + fs.rmSync(oldVenvPath, { recursive: true, force: true }); + console.log(`Successfully removed old venv: ${entry.name}`); + } catch (err) { + console.error(`Failed to remove old venv ${entry.name}:`, err); + } } + break; // Found matching pattern, no need to check others } } } diff --git a/server/app/component/permission.py b/server/app/component/permission.py index 1cae0db8..f9997ef8 100644 --- a/server/app/component/permission.py +++ b/server/app/component/permission.py @@ -10,7 +10,7 @@ def permissions(): return [ { "name": _("User"), - "description": _("User manger"), + "description": _("User manager"), "children": [ { "identity": "user:view", @@ -26,7 +26,7 @@ def permissions(): }, { "name": _("Admin"), - "description": _("Admin manger"), + "description": _("Admin manager"), "children": [ { "identity": "admin:view", @@ -42,7 +42,7 @@ def permissions(): }, { "name": _("Role"), - "description": _("Role manger"), + "description": _("Role manager"), "children": [ { "identity": "role:view", @@ -58,7 +58,7 @@ def permissions(): }, { "name": _("Mcp"), - "description": _("Mcp manger"), + "description": _("Mcp manager"), "children": [ { "identity": "mcp:edit", diff --git a/server/lang/zh_CN/LC_MESSAGES/messages.po b/server/lang/zh_CN/LC_MESSAGES/messages.po index 9c5f8ba1..3438d4d6 100644 --- a/server/lang/zh_CN/LC_MESSAGES/messages.po +++ b/server/lang/zh_CN/LC_MESSAGES/messages.po @@ -31,7 +31,7 @@ msgid "User" msgstr "" #: app/component/permission.py:13 -msgid "User manger" +msgid "User manager" msgstr "" #: app/component/permission.py:17 @@ -55,7 +55,7 @@ msgid "Admin" msgstr "" #: app/component/permission.py:29 -msgid "Admin manger" +msgid "Admin manager" msgstr "" #: app/component/permission.py:33 @@ -79,7 +79,7 @@ msgid "Role" msgstr "" #: app/component/permission.py:45 -msgid "Role manger" +msgid "Role manager" msgstr "" #: app/component/permission.py:49 @@ -103,7 +103,7 @@ msgid "Mcp" msgstr "" #: app/component/permission.py:61 -msgid "Mcp manger" +msgid "Mcp manager" msgstr "" #: app/component/permission.py:65 diff --git a/server/messages.pot b/server/messages.pot index d86ee0be..f9fb4611 100644 --- a/server/messages.pot +++ b/server/messages.pot @@ -30,7 +30,7 @@ msgid "User" msgstr "" #: app/component/permission.py:13 -msgid "User manger" +msgid "User manager" msgstr "" #: app/component/permission.py:17 @@ -54,7 +54,7 @@ msgid "Admin" msgstr "" #: app/component/permission.py:29 -msgid "Admin manger" +msgid "Admin manager" msgstr "" #: app/component/permission.py:33 @@ -78,7 +78,7 @@ msgid "Role" msgstr "" #: app/component/permission.py:45 -msgid "Role manger" +msgid "Role manager" msgstr "" #: app/component/permission.py:49 @@ -102,7 +102,7 @@ msgid "Mcp" msgstr "" #: app/component/permission.py:61 -msgid "Mcp manger" +msgid "Mcp manager" msgstr "" #: app/component/permission.py:65 diff --git a/src/components/WorkFlow/index.tsx b/src/components/WorkFlow/index.tsx index f23c7975..b5f6ea46 100644 --- a/src/components/WorkFlow/index.tsx +++ b/src/components/WorkFlow/index.tsx @@ -233,13 +233,22 @@ export default function Workflow({ // console.log("workerList ", workerList); setNodes((prev: CustomNode[]) => { if (!taskAssigning) return prev; + // Agents not yet in taskAssigning (from baseWorker or workerList) const base = [...baseWorker, ...workerList].filter( (worker) => !taskAssigning.find((agent) => agent.type === worker.type) ); let targetData = [...prev]; - taskAssigning = [...base, ...taskAssigning]; - // taskAssigning = taskAssigning.filter((agent) => agent.tasks.length > 0); - targetData = taskAssigning.map((agent, index) => { + // Merge all agents + const allAgents = [...taskAssigning, ...base]; + // Sort: agents with tasks come first, then agents without tasks + const sortedAgents = allAgents.sort((a, b) => { + const aHasTasks = a.tasks && a.tasks.length > 0; + const bHasTasks = b.tasks && b.tasks.length > 0; + if (aHasTasks && !bHasTasks) return -1; + if (!aHasTasks && bHasTasks) return 1; + return 0; + }); + targetData = sortedAgents.map((agent, index) => { const node = targetData.find((node) => node.id === agent.agent_id); if (node) { return { diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index a855f955..523fbe1e 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -211,11 +211,11 @@ const chatStore = (initial?: Partial) => createStore()( const { task, setProgressValue } = get() if (!task) return; const taskRunning = [...task.taskRunning] - const finshedTask = taskRunning?.filter( + const finishedTask = taskRunning?.filter( (t) => t.status === "completed" || t.status === "failed" ).length; const taskProgress = ( - ((finshedTask || 0) / (taskRunning?.length || 0)) * + ((finishedTask || 0) / (taskRunning?.length || 0)) * 100 ).toFixed(2); setProgressValue(Number(taskProgress)); @@ -1061,7 +1061,7 @@ const chatStore = (initial?: Partial) => createStore()( if (agentMessages.step === "new_task_state") { const { task_id, content, state, result, failure_count } = agentMessages.data; //new chatStore logic is handled along side "confirmed" event - console.log(`Recieved new task: ${task_id} with content: ${content}`); + console.log(`Received new task: ${task_id} with content: ${content}`); return; } diff --git a/test/integration/chatStore/deadWorkforce.test.tsx b/test/integration/chatStore/deadWorkforce.test.tsx index d3ecc458..da7bb51a 100644 --- a/test/integration/chatStore/deadWorkforce.test.tsx +++ b/test/integration/chatStore/deadWorkforce.test.tsx @@ -169,7 +169,7 @@ describe('Integration Test: Case 2 - same session new chat', () => { console.log("Progress test - task status:", task?.status); }, { timeout: 1500 }) - // Test 3: Rerender untill status is "finished" + // Test 3: Rerender until status is "finished" await waitFor(() => { rerender() const {chatStore: newChatStore} = result.current; @@ -380,7 +380,7 @@ describe('Integration Test: Case 2 - same session new chat', () => { }) }) - //TODO: Don't let new startTask untill newChatStore appended + //TODO: Don't let new startTask until newChatStore appended it("Parallel startTask calls with separate chatStores (startTask -> wait for append -> startTask)", async () => { const { result, rerender } = renderHook(() => useChatStoreAdapter())