diff --git a/.github/workflows/build-view.yml b/.github/workflows/build-view.yml
index 3546e9fe..178fa43e 100644
--- a/.github/workflows/build-view.yml
+++ b/.github/workflows/build-view.yml
@@ -20,8 +20,72 @@ jobs:
arch: x64
- os: windows-latest
arch: x64
+ - os: ubuntu-latest
+ arch: x64
steps:
+ - name: Free Disk Space (macOS)
+ if: runner.os == 'macOS'
+ run: |
+ echo "Disk space before cleanup:"
+ df -h
+ # Remove Xcode completely (not needed for Electron builds) - saves ~15GB
+ sudo rm -rf /Applications/Xcode.app || true
+ sudo rm -rf /Applications/Xcode_*.app || true
+ # Note: Keep /Library/Developer/CommandLineTools as codesign needs it
+ # Remove iOS simulators
+ sudo rm -rf ~/Library/Developer/CoreSimulator || true
+ # Remove all Xcode Developer files
+ sudo rm -rf ~/Library/Developer/Xcode || true
+ sudo rm -rf /Library/Developer/Xcode || true
+ # Remove provisioning profiles
+ sudo rm -rf ~/Library/MobileDevice/Provisioning\ Profiles || true
+ # Remove Android SDK if present
+ sudo rm -rf ~/Library/Android/sdk || true
+ sudo rm -rf /usr/local/lib/android || true
+ # Remove .NET
+ sudo rm -rf /usr/local/share/dotnet || true
+ # Remove Go
+ sudo rm -rf /usr/local/go || true
+ sudo rm -rf ~/go || true
+ # Remove Ruby
+ sudo rm -rf /usr/local/lib/ruby || true
+ sudo rm -rf ~/.gem || true
+ # Remove Swift toolchains
+ sudo rm -rf /Library/Developer/Toolchains || true
+ # Remove Homebrew cache
+ rm -rf ~/Library/Caches/Homebrew/* || true
+ brew cleanup --prune=all 2>/dev/null || true
+ # Remove npm cache
+ npm cache clean --force || true
+ # Remove pip cache
+ pip cache purge 2>/dev/null || true
+ # Note: Don't delete ~/Library/Caches/* as subsequent steps may need it
+
+ # Additional cleanup for more disk space
+ # Remove hosted tool cache (can be several GB)
+ sudo rm -rf /Users/runner/hostedtoolcache || true
+ sudo rm -rf /opt/hostedtoolcache || true
+ # Remove browsers (not needed for Electron builds)
+ sudo rm -rf "/Applications/Google Chrome.app" || true
+ sudo rm -rf "/Applications/Firefox.app" || true
+ sudo rm -rf "/Applications/Safari Technology Preview.app" || true
+ # Remove PowerShell
+ sudo rm -rf /usr/local/microsoft/powershell || true
+ sudo rm -rf /usr/local/share/powershell || true
+ # Remove more from /usr/local
+ sudo rm -rf /usr/local/aws-cli || true
+ sudo rm -rf /usr/local/julia* || true
+ sudo rm -rf /usr/local/miniconda || true
+ # Remove unused large directories
+ sudo rm -rf /usr/share/swift || true
+ sudo rm -rf /usr/share/miniconda || true
+ # Remove Docker images if present
+ docker system prune -af 2>/dev/null || true
+
+ echo "Disk space after cleanup:"
+ df -h
+
- name: Checkout Code
uses: actions/checkout@v4
@@ -46,6 +110,23 @@ jobs:
- name: Install Dependencies
run: npm install
+ # Install libfuse2 for Linux AppImage builds
+ - name: Install libfuse2 (Linux)
+ if: runner.os == 'Linux'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libfuse2
+
+ # Verify disk space before build
+ - name: Check Disk Space Before Build (macOS)
+ if: runner.os == 'macOS'
+ run: |
+ echo "Disk space available before build:"
+ df -h
+ echo ""
+ echo "Largest directories in home:"
+ du -sh ~/* 2>/dev/null | sort -rh | head -10 || true
+
# Step for macOS builds with signing
- name: Build Release Files (macOS with signing)
if: runner.os == 'macOS'
@@ -78,6 +159,19 @@ jobs:
VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
USE_NPM_INSTALL_BUN: 'true'
+ # Step for Linux builds
+ - name: Build Release Files (Linux)
+ if: runner.os == 'Linux'
+ timeout-minutes: 90
+ run: npm run build:linux
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }}
+ VITE_STACK_PROJECT_ID: ${{ secrets.VITE_STACK_PROJECT_ID }}
+ VITE_STACK_PUBLISHABLE_CLIENT_KEY: ${{ secrets.VITE_STACK_PUBLISHABLE_CLIENT_KEY }}
+ VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
+ USE_NPM_INSTALL_BUN: 'true'
+
- name: Upload Artifact (macOS - dmg only)
if: runner.os == 'macOS'
uses: actions/upload-artifact@v6
@@ -95,13 +189,22 @@ jobs:
path: |
release/*.exe
retention-days: 5
+
+ - name: Upload Artifact (Linux - AppImage only)
+ if: runner.os == 'Linux'
+ uses: actions/upload-artifact@v6
+ with:
+ name: release-${{ matrix.os }}-${{ matrix.arch }}
+ path: |
+ release/*.AppImage
+ retention-days: 5
merge-release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Create directories
run: |
- mkdir -p release/mac-x64 release/mac-arm64 release/win-x64
+ mkdir -p release/mac-x64 release/mac-arm64 release/win-x64 release/linux-x64
# Download all artifacts with correct names
- name: Download mac-x64 artifact
@@ -122,7 +225,13 @@ jobs:
name: release-windows-latest-x64
path: temp-win-x64
- # Move only dmg files for macOS and exe files for Windows
+ - name: Download linux-x64 artifact
+ uses: actions/download-artifact@v7
+ with:
+ name: release-ubuntu-latest-x64
+ path: temp-linux-x64
+
+ # Move only dmg files for macOS, exe files for Windows, and AppImage for Linux
- name: Move files to clean folders
shell: bash
run: |
@@ -146,3 +255,10 @@ jobs:
else
find temp-win-x64 -name "*.exe" -exec mv {} release/win-x64/ \; || true
fi
+
+ # linux-x64 - only move AppImage files
+ if [ -d "temp-linux-x64/release" ]; then
+ find temp-linux-x64/release -name "*.AppImage" -exec mv {} release/linux-x64/ \; || true
+ else
+ find temp-linux-x64 -name "*.AppImage" -exec mv {} release/linux-x64/ \; || true
+ fi
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a52d762f..60b93cb0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -31,8 +31,72 @@ jobs:
arch: x64
- os: windows-latest
arch: x64
+ - os: ubuntu-latest
+ arch: x64
steps:
+ - name: Free Disk Space (macOS)
+ if: runner.os == 'macOS'
+ run: |
+ echo "Disk space before cleanup:"
+ df -h
+ # Remove Xcode completely (not needed for Electron builds) - saves ~15GB
+ sudo rm -rf /Applications/Xcode.app || true
+ sudo rm -rf /Applications/Xcode_*.app || true
+ # Note: Keep /Library/Developer/CommandLineTools as codesign needs it
+ # Remove iOS simulators
+ sudo rm -rf ~/Library/Developer/CoreSimulator || true
+ # Remove all Xcode Developer files
+ sudo rm -rf ~/Library/Developer/Xcode || true
+ sudo rm -rf /Library/Developer/Xcode || true
+ # Remove provisioning profiles
+ sudo rm -rf ~/Library/MobileDevice/Provisioning\ Profiles || true
+ # Remove Android SDK if present
+ sudo rm -rf ~/Library/Android/sdk || true
+ sudo rm -rf /usr/local/lib/android || true
+ # Remove .NET
+ sudo rm -rf /usr/local/share/dotnet || true
+ # Remove Go
+ sudo rm -rf /usr/local/go || true
+ sudo rm -rf ~/go || true
+ # Remove Ruby
+ sudo rm -rf /usr/local/lib/ruby || true
+ sudo rm -rf ~/.gem || true
+ # Remove Swift toolchains
+ sudo rm -rf /Library/Developer/Toolchains || true
+ # Remove Homebrew cache
+ rm -rf ~/Library/Caches/Homebrew/* || true
+ brew cleanup --prune=all 2>/dev/null || true
+ # Remove npm cache
+ npm cache clean --force || true
+ # Remove pip cache
+ pip cache purge 2>/dev/null || true
+ # Note: Don't delete ~/Library/Caches/* as subsequent steps may need it
+
+ # Additional cleanup for more disk space
+ # Remove hosted tool cache (can be several GB)
+ sudo rm -rf /Users/runner/hostedtoolcache || true
+ sudo rm -rf /opt/hostedtoolcache || true
+ # Remove browsers (not needed for Electron builds)
+ sudo rm -rf "/Applications/Google Chrome.app" || true
+ sudo rm -rf "/Applications/Firefox.app" || true
+ sudo rm -rf "/Applications/Safari Technology Preview.app" || true
+ # Remove PowerShell
+ sudo rm -rf /usr/local/microsoft/powershell || true
+ sudo rm -rf /usr/local/share/powershell || true
+ # Remove more from /usr/local
+ sudo rm -rf /usr/local/aws-cli || true
+ sudo rm -rf /usr/local/julia* || true
+ sudo rm -rf /usr/local/miniconda || true
+ # Remove unused large directories
+ sudo rm -rf /usr/share/swift || true
+ sudo rm -rf /usr/share/miniconda || true
+ # Remove Docker images if present
+ docker system prune -af 2>/dev/null || true
+
+ echo "Disk space after cleanup:"
+ df -h
+
- name: Checkout Code
uses: actions/checkout@v4
@@ -54,6 +118,23 @@ jobs:
- name: Install Dependencies
run: npm install
+ # Install libfuse2 for Linux AppImage builds
+ - name: Install libfuse2 (Linux)
+ if: runner.os == 'Linux'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libfuse2
+
+ # Verify disk space before build
+ - name: Check Disk Space Before Build (macOS)
+ if: runner.os == 'macOS'
+ run: |
+ echo "Disk space available before build:"
+ df -h
+ echo ""
+ echo "Largest directories in home:"
+ du -sh ~/* 2>/dev/null | sort -rh | head -10 || true
+
# Step for macOS builds with signing
- name: Build Release Files (macOS with signing)
if: runner.os == 'macOS'
@@ -82,6 +163,17 @@ jobs:
VITE_STACK_PUBLISHABLE_CLIENT_KEY: ${{ secrets.VITE_STACK_PUBLISHABLE_CLIENT_KEY }}
VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
+ # Step for Linux builds
+ - name: Build Release Files (Linux)
+ if: runner.os == 'Linux'
+ run: npm run build:linux
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }}
+ VITE_STACK_PROJECT_ID: ${{ secrets.VITE_STACK_PROJECT_ID }}
+ VITE_STACK_PUBLISHABLE_CLIENT_KEY: ${{ secrets.VITE_STACK_PUBLISHABLE_CLIENT_KEY }}
+ VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
+
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
@@ -97,7 +189,7 @@ jobs:
steps:
- name: Create directories
run: |
- mkdir -p release/mac-x64 release/mac-arm64 release/win-x64
+ mkdir -p release/mac-x64 release/mac-arm64 release/win-x64 release/linux-x64
# Download all artifacts with correct names
- name: Download mac-x64 artifact
@@ -118,6 +210,12 @@ jobs:
name: release-windows-latest-x64
path: temp-win-x64
+ - name: Download linux-x64 artifact
+ uses: actions/download-artifact@v7
+ with:
+ name: release-ubuntu-latest-x64
+ path: temp-linux-x64
+
# Move files to final release directory, removing any nested release/ directory
- name: Move files to clean folders
shell: bash
@@ -143,6 +241,13 @@ jobs:
mv temp-win-x64/* release/win-x64/ || true
fi
+ # linux-x64
+ if [ -d "temp-linux-x64/release" ]; then
+ mv temp-linux-x64/release/* release/linux-x64/ || true
+ else
+ mv temp-linux-x64/* release/linux-x64/ || true
+ fi
+
- name: Rename duplicate files
run: |
mv release/mac-x64/latest-mac.yml release/mac-x64/latest-x64-mac.yml || true
@@ -157,5 +262,6 @@ jobs:
release/mac-x64/*
release/mac-arm64/*
release/win-x64/*
+ release/linux-x64/*
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 6b6bff22..719583e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,3 +59,6 @@ __pycache__/
resources/prebuilt/bin/
resources/prebuilt/venv/
resources/prebuilt/cache/
+
+*storybook.log
+storybook-static
diff --git a/.storybook/main.ts b/.storybook/main.ts
new file mode 100644
index 00000000..301640e5
--- /dev/null
+++ b/.storybook/main.ts
@@ -0,0 +1,13 @@
+import type { StorybookConfig } from '@storybook/react-vite'
+
+const config: StorybookConfig = {
+ stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ addons: ['@storybook/addon-docs', '@storybook/addon-a11y'],
+ framework: '@storybook/react-vite',
+ viteFinal: async (config) => {
+ // Reuse project's vite config for path aliases
+ return config
+ },
+}
+
+export default config
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
new file mode 100644
index 00000000..4142d6db
--- /dev/null
+++ b/.storybook/preview.tsx
@@ -0,0 +1,47 @@
+import type { Preview } from '@storybook/react-vite'
+import React from 'react'
+import '@fontsource/inter/400.css'
+import '@fontsource/inter/500.css'
+import '@fontsource/inter/600.css'
+import '@fontsource/inter/700.css'
+import '@fontsource/inter/800.css'
+import '../src/style/index.css'
+import './storybook.css' // Storybook-specific overrides
+import { Toaster } from 'sonner'
+
+// Apply theme immediately via script
+if (typeof document !== 'undefined') {
+ document.documentElement.setAttribute('data-theme', 'light')
+ document.documentElement.classList.add('root')
+}
+
+const preview: Preview = {
+ tags: ['autodocs'],
+ parameters: {
+ layout: 'centered',
+ controls: {
+ expanded: true,
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/i,
+ },
+ },
+ backgrounds: {
+ default: 'light',
+ values: [
+ { name: 'light', value: '#f5f5f5' },
+ { name: 'dark', value: '#1d1c1b' },
+ ],
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+
+ ),
+ ],
+}
+
+export default preview
diff --git a/.storybook/storybook.css b/.storybook/storybook.css
new file mode 100644
index 00000000..6e0e8f28
--- /dev/null
+++ b/.storybook/storybook.css
@@ -0,0 +1 @@
+/* Storybook-specific CSS overrides - currently empty as component handles styling */
diff --git a/README.md b/README.md
index b2b627b8..d244627b 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:
@@ -294,13 +307,13 @@ Please add this signature image to the Signature Areas in the PDF. You could ins
| Topics | Issues | Discord Channel |
| ------------------------ | -- |-- |
-| **Context Engineering** | - Prompt caching
- System prompt optimize
- Toolkit docstring optimize
- Context compression | [**Join Discord →**](https://discord.gg/D2e3rBWD) |
-| **Multi-modal Enhancement** | - More accurate image understanding when using browser
- Advanced video generation | [**Join Discord →**](https://discord.gg/kyapNCeJ) |
-| **Multi-agent system** | - Workforce support fixed workflow
- Workforce support multi-round conversion | [**Join Discord →**](https://discord.gg/bFRmPuDB) |
-| **Browser Toolkit** | - BrowseComp integration
- Benchmark improvement
- Forbid repeated page visiting
- Automatic cache button clicking | [**Join Discord →**](https://discord.gg/NF73ze5v) |
-| **Document Toolkit** | - Support dynamic file editing | [**Join Discord →**](https://discord.gg/4yAWJxYr) |
-| **Terminal Toolkit** | - Benchmark improvement
- Terminal-Bench integration | [**Join Discord →**](https://discord.gg/FjQfnsrV) |
-| **Environment & RL** | - Environment design
- Data-generation
- RL framework integration (VERL, TRL, OpenRLHF) | [**Join Discord →**](https://discord.gg/MaVZXEn8) |
+| **Context Engineering** | - Prompt caching
- System prompt optimize
- Toolkit docstring optimize
- Context compression | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Multi-modal Enhancement** | - More accurate image understanding when using browser
- Advanced video generation | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Multi-agent system** | - Workforce support fixed workflow
- Workforce support multi-round conversion | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Browser Toolkit** | - BrowseComp integration
- Benchmark improvement
- Forbid repeated page visiting
- Automatic cache button clicking | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Document Toolkit** | - Support dynamic file editing | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Terminal Toolkit** | - Benchmark improvement
- Terminal-Bench integration | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Environment & RL** | - Environment design
- Data-generation
- RL framework integration (VERL, TRL, OpenRLHF) | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
## [🤝 Contributing][contribution-link]
diff --git a/README_CN.md b/README_CN.md
index e71d3054..1bd95f97 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
+```
+
### 🏢 企业版
适合需要最高安全性、定制化和控制的组织:
@@ -281,13 +294,13 @@ Eigent 完全开源。您可以下载、检查和修改代码,确保透明度
| 主题 | 问题 | Discord 频道 |
| ------------------------ | -- |-- |
-| **上下文工程** | - 提示缓存
- 系统提示优化
- 工具包文档优化
- 上下文压缩 | [**加入 Discord →**](https://discord.gg/D2e3rBWD) |
-| **多模态增强** | - 使用浏览器时更准确的图像理解
- 高级视频生成 | [**加入 Discord →**](https://discord.gg/kyapNCeJ) |
-| **多智能体系统** | - 工作流支持固定流程
- 工作流支持多轮对话 | [**加入 Discord →**](https://discord.gg/bFRmPuDB) |
-| **浏览器工具包** | - BrowseComp 集成
- 基准测试改进
- 禁止重复访问页面
- 自动缓存按钮点击 | [**加入 Discord →**](https://discord.gg/NF73ze5v) |
-| **文档工具包** | - 支持动态文件编辑 | [**加入 Discord →**](https://discord.gg/4yAWJxYr) |
-| **终端工具包** | - 基准测试改进
- Terminal-Bench 集成 | [**加入 Discord →**](https://discord.gg/FjQfnsrV) |
-| **环境与强化学习** | - 环境设计
- 数据生成
- 强化学习框架集成(VERL, TRL, OpenRLHF) | [**加入 Discord →**](https://discord.gg/MaVZXEn8) |
+| **上下文工程** | - 提示缓存
- 系统提示优化
- 工具包文档优化
- 上下文压缩 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **多模态增强** | - 使用浏览器时更准确的图像理解
- 高级视频生成 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **多智能体系统** | - 工作流支持固定流程
- 工作流支持多轮对话 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **浏览器工具包** | - BrowseComp 集成
- 基准测试改进
- 禁止重复访问页面
- 自动缓存按钮点击 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **文档工具包** | - 支持动态文件编辑 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **终端工具包** | - 基准测试改进
- Terminal-Bench 集成 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **环境与强化学习** | - 环境设计
- 数据生成
- 强化学习框架集成(VERL, TRL, OpenRLHF) | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
## [🤝 贡献][contribution-link]
diff --git a/README_JA.md b/README_JA.md
index 93079536..92f887a8 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
+```
+
### 🏢 エンタープライズ
最大限のセキュリティ、カスタマイズ、制御を必要とする組織向け:
@@ -292,13 +305,13 @@ Documentsディレクトリにmydocsというフォルダがあります。ス
| トピック | 課題 | Discordチャンネル |
| ------------------------ | -- |-- |
-| **コンテキストエンジニアリング** | - プロンプトキャッシング
- システムプロンプト最適化
- ツールキットdocstring最適化
- コンテキスト圧縮 | [**Discordに参加 →**](https://discord.gg/D2e3rBWD) |
-| **マルチモーダル強化** | - ブラウザ使用時のより正確な画像理解
- 高度な動画生成 | [**Discordに参加 →**](https://discord.gg/kyapNCeJ) |
-| **マルチエージェントシステム** | - 固定ワークフローをサポートするワークフォース
- マルチラウンド変換をサポートするワークフォース | [**Discordに参加 →**](https://discord.gg/bFRmPuDB) |
-| **ブラウザツールキット** | - BrowseComp統合
- ベンチマーク改善
- 繰り返しページ訪問の禁止
- 自動キャッシュボタンクリック | [**Discordに参加 →**](https://discord.gg/NF73ze5v) |
-| **ドキュメントツールキット** | - 動的ファイル編集のサポート | [**Discordに参加 →**](https://discord.gg/4yAWJxYr) |
-| **ターミナルツールキット** | - ベンチマーク改善
- Terminal-Bench統合 | [**Discordに参加 →**](https://discord.gg/FjQfnsrV) |
-| **環境 & RL** | - 環境設計
- データ生成
- RLフレームワーク統合(VERL、TRL、OpenRLHF) | [**Discordに参加 →**](https://discord.gg/MaVZXEn8) |
+| **コンテキストエンジニアリング** | - プロンプトキャッシング
- システムプロンプト最適化
- ツールキットdocstring最適化
- コンテキスト圧縮 | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
+| **マルチモーダル強化** | - ブラウザ使用時のより正確な画像理解
- 高度な動画生成 | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
+| **マルチエージェントシステム** | - 固定ワークフローをサポートするワークフォース
- マルチラウンド変換をサポートするワークフォース | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
+| **ブラウザツールキット** | - BrowseComp統合
- ベンチマーク改善
- 繰り返しページ訪問の禁止
- 自動キャッシュボタンクリック | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
+| **ドキュメントツールキット** | - 動的ファイル編集のサポート | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
+| **ターミナルツールキット** | - ベンチマーク改善
- Terminal-Bench統合 | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
+| **環境 & RL** | - 環境設計
- データ生成
- RLフレームワーク統合(VERL、TRL、OpenRLHF) | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
## [🤝 コントリビューション][contribution-link]
diff --git a/README_PT-BR.md b/README_PT-BR.md
index 4369c355..12aee44d 100644
--- a/README_PT-BR.md
+++ b/README_PT-BR.md
@@ -88,7 +88,7 @@ Construído sobre o aclamado projeto open source da [CAMEL-AI][camel-site], noss
A forma recomendada de executar o Eigent — totalmente independente, com controle completo sobre seus dados, sem necessidade de conta em nuvem.
-👉 **[Guia Completo de Implantação Local](./server/README_EN.md)**
+👉 **[Guia Completo de Implantação Local](./server/README_PT-BR.md)**
Esta configuração inclui:
- Servidor backend local com API completa
@@ -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:
@@ -289,13 +302,13 @@ Por favor, adicione esta imagem de assinatura às áreas de assinatura no PDF. V
| Tópicos | Issues | Canal do Discord |
| ------------------------- | -- |-- |
-| **Engenharia de Contexto** | - Cache de prompts
- Otimização de prompt do sistema
- Otimização de docstrings do toolkit
- Compressão de contexto | [**Entrar no Discord →**](https://discord.gg/D2e3rBWD) |
-| **Aprimoramento Multimodal** | - Compreensão de imagens mais precisa ao usar o navegador
- Geração avançada de vídeo | [**Entrar no Discord →**](https://discord.gg/kyapNCeJ) |
-| **Sistema Multiagente** | - Suporte do Workforce a fluxos fixos
- Suporte do Workforce a conversas em múltiplas rodadas | [**Entrar no Discord →**](https://discord.gg/bFRmPuDB) |
-| **Toolkit de Navegador** | - Integração com BrowseComp
- Melhoria de benchmark
- Proibir visitas repetidas a páginas
- Clique automático em botões de cache | [**Entrar no Discord →**](https://discord.gg/NF73ze5v) |
-| **Toolkit de Documentos** | - Suporte à edição dinâmica de arquivos | [**Entrar no Discord →**](https://discord.gg/4yAWJxYr) |
-| **Toolkit de Terminal** | - Melhoria de benchmark
- Integração com Terminal-Bench | [**Entrar no Discord →**](https://discord.gg/FjQfnsrV) |
-| **Ambiente & RL** | - Design de ambiente
- Geração de dados
- Integração de frameworks de RL (VERL, TRL, OpenRLHF) | [**Entrar no Discord →**](https://discord.gg/MaVZXEn8) |
+| **Engenharia de Contexto** | - Cache de prompts
- Otimização de prompt do sistema
- Otimização de docstrings do toolkit
- Compressão de contexto | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Aprimoramento Multimodal** | - Compreensão de imagens mais precisa ao usar o navegador
- Geração avançada de vídeo | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Sistema Multiagente** | - Suporte do Workforce a fluxos fixos
- Suporte do Workforce a conversas em múltiplas rodadas | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Toolkit de Navegador** | - Integração com BrowseComp
- Melhoria de benchmark
- Proibir visitas repetidas a páginas
- Clique automático em botões de cache | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Toolkit de Documentos** | - Suporte à edição dinâmica de arquivos | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Toolkit de Terminal** | - Melhoria de benchmark
- Integração com Terminal-Bench | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
+| **Ambiente & RL** | - Design de ambiente
- Geração de dados
- Integração de frameworks de RL (VERL, TRL, OpenRLHF) | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
## [🤝 Contribuição][contribution-link]
diff --git a/backend/app/component/model_validation.py b/backend/app/component/model_validation.py
index d734ab92..c0b52ea6 100644
--- a/backend/app/component/model_validation.py
+++ b/backend/app/component/model_validation.py
@@ -29,7 +29,7 @@ def create_agent(
model_type=mtype,
api_key=api_key,
url=url,
- timeout=10,
+ timeout=60, # 1 minute for validation
model_config_dict=model_config_dict,
**kwargs,
)
@@ -37,6 +37,6 @@ def create_agent(
system_message="You are a helpful assistant that must use the tool get_website_content to get the content of a website.",
model=model,
tools=[get_website_content],
- step_timeout=900,
+ step_timeout=1800, # 30 minutes
)
return agent
diff --git a/backend/app/controller/chat_controller.py b/backend/app/controller/chat_controller.py
index b00fda92..b3b8db9a 100644
--- a/backend/app/controller/chat_controller.py
+++ b/backend/app/controller/chat_controller.py
@@ -23,6 +23,8 @@ from app.service.task import (
get_or_create_task_lock,
get_task_lock,
set_current_task_id,
+ delete_task_lock,
+ task_locks,
)
from app.component.environment import set_user_env_path
from app.utils.workforce import Workforce
@@ -34,18 +36,50 @@ router = APIRouter()
# Create traceroot logger for chat controller
chat_logger = traceroot.get_logger("chat_controller")
-# SSE timeout configuration (30 minutes in seconds)
-SSE_TIMEOUT_SECONDS = 30 * 60
+# SSE timeout configuration (60 minutes in seconds)
+SSE_TIMEOUT_SECONDS = 60 * 60
-async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TIMEOUT_SECONDS):
+async def _cleanup_task_lock_safe(task_lock, reason: str) -> bool:
+ """Safely cleanup task lock with existence check.
+
+ Args:
+ task_lock: The task lock to cleanup
+ reason: Reason for cleanup (for logging)
+
+ Returns:
+ True if cleanup was performed, False otherwise
"""
- Wraps a stream generator with timeout handling.
+ if not task_lock:
+ return False
+
+ # Check if task_lock still exists before attempting cleanup
+ if task_lock.id not in task_locks:
+ chat_logger.debug(f"[{reason}] Task lock already removed, skipping cleanup",
+ extra={"task_id": task_lock.id})
+ return False
+
+ try:
+ task_lock.status = Status.done
+ await delete_task_lock(task_lock.id)
+ chat_logger.info(f"[{reason}] Task lock cleanup completed",
+ extra={"task_id": task_lock.id})
+ return True
+ except Exception as e:
+ chat_logger.error(f"[{reason}] Failed to cleanup task lock",
+ extra={"task_id": task_lock.id, "error": str(e)}, exc_info=True)
+ return False
+
+
+async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TIMEOUT_SECONDS, task_lock=None):
+ """Wraps a stream generator with timeout handling.
Closes the SSE connection if no data is received within the timeout period.
+ Triggers cleanup if timeout occurs to prevent resource leaks.
"""
last_data_time = time.time()
generator = stream_generator.__aiter__()
+ cleanup_triggered = False
try:
while True:
@@ -57,17 +91,24 @@ async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TI
last_data_time = time.time()
yield data
except asyncio.TimeoutError:
- chat_logger.warning(f"SSE timeout: No data received for {timeout_seconds} seconds, closing connection")
+ chat_logger.warning("SSE timeout: No data received, closing connection",
+ extra={"timeout_seconds": timeout_seconds})
yield sse_json("error", {"message": f"Connection timeout: No data received for {timeout_seconds // 60} minutes"})
+ cleanup_triggered = await _cleanup_task_lock_safe(task_lock, "TIMEOUT")
break
except StopAsyncIteration:
break
except asyncio.CancelledError:
- chat_logger.info("Stream cancelled")
+ chat_logger.info("[STREAM-CANCELLED] Stream cancelled, triggering cleanup")
+ if not cleanup_triggered:
+ await _cleanup_task_lock_safe(task_lock, "CANCELLED")
raise
except Exception as e:
- chat_logger.error(f"Error in stream wrapper: {e}", exc_info=True)
+ chat_logger.error("[STREAM-ERROR] Unexpected error in stream wrapper",
+ extra={"error": str(e)}, exc_info=True)
+ if not cleanup_triggered:
+ await _cleanup_task_lock_safe(task_lock, "ERROR")
raise
@@ -75,8 +116,10 @@ async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TI
@traceroot.trace()
async def post(data: Chat, request: Request):
chat_logger.info(
- "Starting new chat session", extra={"project_id": data.project_id, "task_id": data.task_id, "user": data.email}
+ "Starting new chat session",
+ extra={"project_id": data.project_id, "task_id": data.task_id, "user": data.email}
)
+
task_lock = get_or_create_task_lock(data.project_id)
# Set user-specific environment path for this thread
@@ -92,9 +135,9 @@ async def post(data: Chat, request: Request):
# Set user-specific search engine configuration if provided
if data.search_config:
for key, value in data.search_config.items():
- if value: # Only set non-empty values
+ if value:
os.environ[key] = value
- chat_logger.info(f"Set search config: {key}", extra={"project_id": data.project_id})
+ chat_logger.debug(f"Set search config: {key}", extra={"project_id": data.project_id})
email_sanitized = re.sub(r'[\\/*?:"<>|\s]', "_", data.email.split("@")[0]).strip(".")
camel_log = (
@@ -119,11 +162,11 @@ async def post(data: Chat, request: Request):
await task_lock.put_queue(ActionImproveData(data=data.question, new_task_id=data.task_id))
chat_logger.info(
- "Chat session initialized, starting streaming response",
+ "Chat session initialized",
extra={"project_id": data.project_id, "task_id": data.task_id, "log_dir": str(camel_log)},
)
return StreamingResponse(
- timeout_stream_wrapper(step_solve(data, request, task_lock)), media_type="text/event-stream"
+ timeout_stream_wrapper(step_solve(data, request, task_lock), task_lock=task_lock), media_type="text/event-stream"
)
@@ -314,5 +357,5 @@ def skip_task(project_id: str):
return Response(status_code=201)
except Exception as e:
- chat_logger.error(f"[STOP-BUTTON] ❌ Error skipping task for project_id: {project_id}: {e}")
+ chat_logger.error(f"[STOP-BUTTON] Error skipping task for project_id: {project_id}: {e}")
raise UserException(code.error, f"Failed to skip task: {str(e)}")
diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py
index 63b85dc7..04accf8b 100644
--- a/backend/app/service/chat_service.py
+++ b/backend/app/service/chat_service.py
@@ -40,6 +40,7 @@ from app.utils.agent import (
social_medium_agent,
task_summary_agent,
question_confirm_agent,
+ set_main_event_loop,
)
from app.service.task import Action, Agents
from app.utils.server.sync_step import sync_step
@@ -153,7 +154,7 @@ def collect_previous_task_context(working_directory: str, previous_task_content:
return "\n".join(context_parts)
-def check_conversation_history_length(task_lock: TaskLock, max_length: int = 100000) -> tuple[bool, int]:
+def check_conversation_history_length(task_lock: TaskLock, max_length: int = 200000) -> tuple[bool, int]:
"""
Check if conversation history exceeds maximum length
@@ -237,15 +238,9 @@ def build_context_for_workforce(task_lock: TaskLock, options: Chat) -> str:
@sync_step
@traceroot.trace()
async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
- # if True:
- # import faulthandler
-
- # faulthandler.enable()
- # for second in [5, 10, 20, 30, 60, 120, 240]:
- # faulthandler.dump_traceback_later(second)
-
start_event_loop = True
+ # Initialize task_lock attributes
if not hasattr(task_lock, 'conversation_history'):
task_lock.conversation_history = []
if not hasattr(task_lock, 'last_task_result'):
@@ -258,15 +253,15 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
# Create or reuse persistent question_agent
if task_lock.question_agent is None:
task_lock.question_agent = question_confirm_agent(options)
- logger.info(f"Created new persistent question_agent for project {options.project_id}")
else:
- logger.info(f"Reusing existing question_agent with {len(task_lock.conversation_history)} history entries")
+ logger.debug(f"Reusing existing question_agent with {len(task_lock.conversation_history)} history entries")
question_agent = task_lock.question_agent
# Other variables
camel_task = None
workforce = None
+ mcp = None
last_completed_task_result = "" # Track the last completed task result
summary_task_content = "" # Track task summary
loop_iteration = 0
@@ -339,14 +334,12 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
})
continue
- # Simplified logic: attachments mean workforce, otherwise let agent decide
+ # Determine task complexity: attachments mean workforce, otherwise let agent decide
is_complex_task: bool
if len(options.attaches) > 0:
- # Questions with attachments always need workforce
is_complex_task = True
logger.info(f"[NEW-QUESTION] Has attachments, treating as complex task")
else:
- logger.info(f"[NEW-QUESTION] Calling question_confirm to determine complexity")
is_complex_task = await question_confirm(question_agent, question, task_lock)
logger.info(f"[NEW-QUESTION] question_confirm result: is_complex={is_complex_task}")
@@ -386,56 +379,35 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
except Exception as e:
logger.error(f"Error cleaning up folder: {e}")
else:
- logger.info(f"[NEW-QUESTION] 🔧 Complex task, creating workforce and decomposing")
+ logger.info(f"[NEW-QUESTION] Complex task, creating workforce and decomposing")
# Update the sync_step with new task_id
if hasattr(item, 'new_task_id') and item.new_task_id:
set_current_task_id(options.project_id, item.new_task_id)
- # Reset summary generation flag for new tasks to ensure proper summaries
task_lock.summary_generated = False
- logger.info("[NEW-QUESTION] Reset summary_generated flag for new task", extra={"project_id": options.project_id, "new_task_id": item.new_task_id})
- logger.info(f"[NEW-QUESTION] Sending 'confirmed' SSE to frontend")
yield sse_json("confirmed", {"question": question})
- logger.info(f"[NEW-QUESTION] Building context for coordinator")
context_for_coordinator = build_context_for_workforce(task_lock, options)
- # Check if workforce exists - if so, reuse it (agents are preserved)
- # Otherwise create new workforce
+ # Check if workforce exists - if so, reuse it; otherwise create new workforce
if workforce is not None:
- logger.info(f"[NEW-QUESTION] 🔄 Workforce exists (id={id(workforce)}), state={workforce._state.name}, _running={workforce._running}")
- logger.info(f"[NEW-QUESTION] ✅ Reusing existing workforce with preserved agents")
- # Workforce is already stopped from skip_task, ready for new decomposition
+ logger.debug(f"[NEW-QUESTION] Reusing existing workforce (id={id(workforce)})")
else:
- logger.info(f"[NEW-QUESTION] 🏭 Creating NEW workforce instance (workforce=None)")
+ logger.info(f"[NEW-QUESTION] Creating NEW workforce instance")
(workforce, mcp) = await construct_workforce(options)
- logger.info(f"[NEW-QUESTION] ✅ NEW Workforce instance created, id={id(workforce)}")
for new_agent in options.new_agents:
workforce.add_single_agent_worker(
format_agent_description(new_agent), await new_agent_model(new_agent, options)
)
task_lock.status = Status.confirmed
- # If camel_task already exists (from previous paused task), add new question as subtask
- # Otherwise, create a new camel_task
- if camel_task is not None:
- logger.info(f"[NEW-QUESTION] 🔄 camel_task exists (id={camel_task.id}), adding new question as context")
- # Update the task content with new question
- clean_task_content = question + options.summary_prompt
- logger.info(f"[NEW-QUESTION] Updating existing camel_task content with new question")
- # We keep the existing task structure but update content for new decomposition
- camel_task = Task(content=clean_task_content, id=options.task_id)
- if len(options.attaches) > 0:
- camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
- else:
- clean_task_content = question + options.summary_prompt
- logger.info(f"[NEW-QUESTION] Creating NEW camel_task with id={options.task_id}")
- camel_task = Task(content=clean_task_content, id=options.task_id)
- if len(options.attaches) > 0:
- camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
+ # Create camel_task for the question
+ clean_task_content = question + options.summary_prompt
+ camel_task = Task(content=clean_task_content, id=options.task_id)
+ if len(options.attaches) > 0:
+ camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
- # Stream decomposition in background so queue items (decompose_text) are processed immediately
- logger.info(f"[NEW-QUESTION] 🧩 Starting task decomposition via workforce.eigent_make_sub_tasks")
+ # Stream decomposition in background
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": ""}
state_holder: dict[str, Any] = {"sub_tasks": [], "summary_task": ""}
@@ -447,8 +419,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
def on_stream_text(chunk):
try:
- # With task_agent using stream_accumulate=True, chunk.msg.content is accumulated content
- # We need to calculate the delta to send only new content to frontend
accumulated_content = chunk.msg.content if hasattr(chunk, 'msg') and chunk.msg else str(chunk)
last_content = stream_state["last_content"]
@@ -486,52 +456,45 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
on_stream_batch,
on_stream_text,
)
+
if stream_state["subtasks"]:
sub_tasks = stream_state["subtasks"]
state_holder["sub_tasks"] = sub_tasks
- logger.info(f"[NEW-QUESTION] ✅ Task decomposed into {len(sub_tasks)} subtasks")
+ logger.info(f"Task decomposed into {len(sub_tasks)} subtasks")
try:
setattr(task_lock, "decompose_sub_tasks", sub_tasks)
except Exception:
pass
- logger.info(f"[NEW-QUESTION] Generating task summary")
+ # Generate task summary
summary_task_agent = task_summary_agent(options)
try:
summary_task_content = await asyncio.wait_for(
summary_task(summary_task_agent, camel_task), timeout=10
)
task_lock.summary_generated = True
- logger.info("[NEW-QUESTION] ✅ Summary generated successfully", extra={"project_id": options.project_id})
except asyncio.TimeoutError:
logger.warning("summary_task timeout", extra={"project_id": options.project_id, "task_id": options.task_id})
task_lock.summary_generated = True
- fallback_name = "Task"
content_preview = camel_task.content if hasattr(camel_task, "content") else ""
if content_preview is None:
content_preview = ""
- summary_task_content = (
- (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
- )
- summary_task_content = f"{fallback_name}|{summary_task_content}"
+ summary_task_content = (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
+ summary_task_content = f"Task|{summary_task_content}"
except Exception:
task_lock.summary_generated = True
- fallback_name = "Task"
content_preview = camel_task.content if hasattr(camel_task, "content") else ""
if content_preview is None:
content_preview = ""
- summary_task_content = (
- (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
- )
- summary_task_content = f"{fallback_name}|{summary_task_content}"
+ summary_task_content = (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
+ summary_task_content = f"Task|{summary_task_content}"
state_holder["summary_task"] = summary_task_content
try:
setattr(task_lock, "summary_task_content", summary_task_content)
except Exception:
pass
- logger.info(f"[NEW-QUESTION] 📤 Sending to_sub_tasks SSE to frontend (task card)")
- logger.info(f"[NEW-QUESTION] to_sub_tasks data: task_id={camel_task.id}, summary={summary_task_content[:50]}..., subtasks_count={len(camel_task.subtasks)}")
+
payload = {
"project_id": options.project_id,
"task_id": options.task_id,
@@ -541,7 +504,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
"summary_task": summary_task_content,
}
await task_lock.put_queue(ActionDecomposeProgressData(data=payload))
- logger.info(f"[NEW-QUESTION] ✅ to_sub_tasks SSE sent")
except Exception as e:
logger.error(f"Error in background decomposition: {e}", exc_info=True)
@@ -781,7 +743,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
logger.info(f"[LIFECYCLE] Multi-turn: building context for workforce")
context_for_multi_turn = build_context_for_workforce(task_lock, options)
- logger.info(f"[LIFECYCLE] Multi-turn: calling workforce.handle_decompose_append_task for new task decomposition")
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": ""}
def on_stream_batch(new_tasks: list[Task], is_final: bool = False):
@@ -792,8 +753,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
def on_stream_text(chunk):
try:
- # With task_agent using stream_accumulate=True, chunk.msg.content is accumulated content
- # We need to calculate the delta to send only new content to frontend
accumulated_content = chunk.msg.content if hasattr(chunk, 'msg') and chunk.msg else str(chunk)
last_content = stream_state["last_content"]
@@ -907,6 +866,10 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
elif item.action == Action.search_mcp:
yield sse_json("search_mcp", item.data)
elif item.action == Action.install_mcp:
+ if mcp is None:
+ logger.error(f"Cannot install MCP: mcp agent not initialized for project {options.project_id}")
+ yield sse_json("error", {"message": "MCP agent not initialized. Please start a complex task first."})
+ continue
task = asyncio.create_task(install_mcp(mcp, item))
task_lock.add_background_task(task)
elif item.action == Action.terminal:
@@ -1300,89 +1263,132 @@ async def get_task_result_with_optional_summary(task: Task, options: Chat) -> st
@traceroot.trace()
async def construct_workforce(options: Chat) -> tuple[Workforce, ListenChatAgent]:
- logger.info("Constructing workforce", extra={"project_id": options.project_id, "task_id": options.task_id})
+ """Construct a workforce with all required agents.
+
+ This function creates all agents in PARALLEL to minimize startup time.
+ Sync functions are run in thread pool, async functions are awaited concurrently.
+ """
+ logger.debug("construct_workforce started", extra={"project_id": options.project_id, "task_id": options.task_id})
+
+ # Store main event loop reference for thread-safe async task scheduling
+ # This allows agent_model() to schedule tasks when called from worker threads
+ set_main_event_loop(asyncio.get_running_loop())
+
working_directory = get_working_directory(options)
- logger.debug("Working directory set", extra={"working_directory": working_directory})
- [coordinator_agent, task_agent] = [
- agent_model(
- key,
- prompt,
- options,
- [
- *(
- ToolkitMessageIntegration(
- message_handler=HumanToolkit(options.project_id, key).send_message_to_user
- ).register_toolkits(NoteTakingToolkit(options.project_id, working_directory=working_directory))
- ).get_tools()
- ],
- )
- for key, prompt in {
- Agents.coordinator_agent: f"""
+
+ # ========================================================================
+ # Define agent creation functions
+ # ========================================================================
+
+ def _create_coordinator_and_task_agents() -> list[ListenChatAgent]:
+ """Create coordinator and task agents (sync, runs in thread pool)."""
+ return [
+ agent_model(
+ key,
+ prompt,
+ options,
+ [
+ *(
+ ToolkitMessageIntegration(
+ message_handler=HumanToolkit(options.project_id, key).send_message_to_user
+ ).register_toolkits(NoteTakingToolkit(options.project_id, working_directory=working_directory))
+ ).get_tools()
+ ],
+ )
+ for key, prompt in {
+ Agents.coordinator_agent: f"""
You are a helpful coordinator.
- You are now working in system {platform.system()} with architecture
{platform.machine()} at working directory `{working_directory}`. All local file operations must occur here, but you can access files from any place in the file system. For all file system operations, you MUST use absolute paths to ensure precision and avoid ambiguity.
The current date is {datetime.date.today()}. For any date-related tasks, you MUST use this as the current date.
-
-- If a task assigned to another agent fails, you should re-assign it to the
-`Developer_Agent`. The `Developer_Agent` is a powerful agent with terminal
-access and can resolve a wide range of issues.
""",
- Agents.task_agent: f"""
+ Agents.task_agent: f"""
You are a helpful task planner.
- You are now working in system {platform.system()} with architecture
{platform.machine()} at working directory `{working_directory}`. All local file operations must occur here, but you can access files from any place in the file system. For all file system operations, you MUST use absolute paths to ensure precision and avoid ambiguity.
The current date is {datetime.date.today()}. For any date-related tasks, you MUST use this as the current date.
""",
- }.items()
- ]
- new_worker_agent = agent_model(
- Agents.new_worker_agent,
- f"""
+ }.items()
+ ]
+
+ def _create_new_worker_agent() -> ListenChatAgent:
+ """Create new worker agent (sync, runs in thread pool)."""
+ return agent_model(
+ Agents.new_worker_agent,
+ f"""
You are a helpful assistant.
- You are now working in system {platform.system()} with architecture
{platform.machine()} at working directory `{working_directory}`. All local file operations must occur here, but you can access files from any place in the file system. For all file system operations, you MUST use absolute paths to ensure precision and avoid ambiguity.
The current date is {datetime.date.today()}. For any date-related tasks, you MUST use this as the current date.
""",
- options,
- [
- *HumanToolkit.get_can_use_tools(options.project_id, Agents.new_worker_agent),
- *(
- ToolkitMessageIntegration(
- message_handler=HumanToolkit(options.project_id, Agents.new_worker_agent).send_message_to_user
- ).register_toolkits(NoteTakingToolkit(options.project_id, working_directory=working_directory))
- ).get_tools(),
- ],
- )
- # msg_toolkit = AgentCommunicationToolkit(max_message_history=100)
+ options,
+ [
+ *HumanToolkit.get_can_use_tools(options.project_id, Agents.new_worker_agent),
+ *(
+ ToolkitMessageIntegration(
+ message_handler=HumanToolkit(options.project_id, Agents.new_worker_agent).send_message_to_user
+ ).register_toolkits(NoteTakingToolkit(options.project_id, working_directory=working_directory))
+ ).get_tools(),
+ ],
+ )
- searcher = browser_agent(options)
- developer = await developer_agent(options)
- documenter = await document_agent(options)
- multi_modaler = multi_modal_agent(options)
+ # ========================================================================
+ # Execute all agent creations in PARALLEL
+ # ========================================================================
- # msg_toolkit.register_agent("Worker", new_worker_agent)
- # msg_toolkit.register_agent("Browser_Agent", searcher)
- # msg_toolkit.register_agent("Developer_Agent", developer)
- # msg_toolkit.register_agent("Document_Agent", documenter)
- # msg_toolkit.register_agent("Multi_Modal_Agent", multi_modaler)
+ try:
+ # asyncio.gather runs all coroutines concurrently
+ # asyncio.to_thread runs sync functions in thread pool without blocking event loop
+ results = await asyncio.gather(
+ asyncio.to_thread(_create_coordinator_and_task_agents),
+ asyncio.to_thread(_create_new_worker_agent),
+ asyncio.to_thread(browser_agent, options),
+ developer_agent(options),
+ document_agent(options),
+ asyncio.to_thread(multi_modal_agent, options),
+ mcp_agent(options),
+ )
+ except Exception as e:
+ logger.error(f"Failed to create agents in parallel: {e}", exc_info=True)
+ raise
+ finally:
+ # Always clear event loop reference after parallel agent creation completes
+ # This prevents stale references and potential cross-request interference
+ set_main_event_loop(None)
+
+ # Unpack results
+ (
+ coord_task_agents,
+ new_worker_agent,
+ searcher,
+ developer,
+ documenter,
+ multi_modaler,
+ mcp,
+ ) = results
+
+ coordinator_agent, task_agent = coord_task_agents
+
+ # ========================================================================
+ # Create Workforce instance and add workers (must be sequential)
+ # ========================================================================
- # Convert string model_platform to enum for comparison
try:
model_platform_enum = ModelPlatformType(options.model_platform.lower())
except (ValueError, AttributeError):
- # If conversion fails, default to non-OpenAI behavior
model_platform_enum = None
workforce = Workforce(
options.project_id,
"A workforce",
- graceful_shutdown_timeout=3, # 30 seconds for debugging
+ graceful_shutdown_timeout=3,
share_memory=False,
coordinator_agent=coordinator_agent,
task_agent=task_agent,
new_worker_agent=new_worker_agent,
use_structured_output_handler=False if model_platform_enum == ModelPlatformType.OPENAI else True,
)
+
workforce.add_single_agent_worker(
"Developer Agent: A master-level coding assistant with a powerful "
"terminal. It can write and execute code, manage files, automate "
@@ -1410,18 +1416,7 @@ The current date is {datetime.date.today()}. For any date-related tasks, you MUS
"generate new images from text prompts.",
multi_modaler,
)
- # workforce.add_single_agent_worker(
- # "Social Media Agent: A social media management assistant for "
- # "handling tasks related to WhatsApp, Twitter, LinkedIn, Reddit, "
- # "Notion, Slack, and other social platforms.",
- # await social_medium_agent(options),
- # )
- mcp = await mcp_agent(options)
- # workforce.add_single_agent_worker(
- # "MCP Agent: A Model Context Protocol agent that provides access "
- # "to external tools and services through MCP integrations.",
- # mcp,
- # )
+
return workforce, mcp
diff --git a/backend/app/service/task.py b/backend/app/service/task.py
index 0788dfac..d4958dd9 100644
--- a/backend/app/service/task.py
+++ b/backend/app/service/task.py
@@ -277,6 +277,8 @@ class TaskLock:
last_accessed: datetime
background_tasks: set[asyncio.Task]
"""Track all background tasks for cleanup"""
+ registered_toolkits: list[Any]
+ """Track toolkits for cleanup (e.g., TerminalToolkit venvs)"""
# Context management fields
conversation_history: List[Dict[str, Any]]
@@ -297,6 +299,7 @@ class TaskLock:
self.created_at = datetime.now()
self.last_accessed = datetime.now()
self.background_tasks = set()
+ self.registered_toolkits = []
# Initialize context management fields
self.conversation_history = []
@@ -346,8 +349,42 @@ class TaskLock:
except asyncio.CancelledError:
pass
self.background_tasks.clear()
+
+ # Clean up registered toolkits (e.g., remove TerminalToolkit venvs)
+ for toolkit in self.registered_toolkits:
+ try:
+ if hasattr(toolkit, 'cleanup'):
+ toolkit.cleanup()
+ logger.info("Toolkit cleanup completed", extra={"task_id": self.id, "toolkit": type(toolkit).__name__})
+ except Exception as e:
+ logger.warning(f"Failed to cleanup toolkit: {e}", extra={"task_id": self.id, "toolkit": type(toolkit).__name__})
+ self.registered_toolkits.clear()
+
logger.info("Task lock cleanup completed", extra={"task_id": self.id})
+ def register_toolkit(self, toolkit: Any) -> None:
+ """Register a toolkit for cleanup when task ends.
+
+ This is used to track toolkits that create resources (like venvs) that
+ should be cleaned up when the task is complete.
+
+ Note: Duplicate registrations of the same toolkit instance are ignored.
+ """
+ # Prevent duplicate registration of the same toolkit instance
+ if any(t is toolkit for t in self.registered_toolkits):
+ logger.debug("Toolkit already registered, skipping", extra={
+ "task_id": self.id,
+ "toolkit": type(toolkit).__name__
+ })
+ return
+
+ self.registered_toolkits.append(toolkit)
+ logger.debug("Toolkit registered for cleanup", extra={
+ "task_id": self.id,
+ "toolkit": type(toolkit).__name__,
+ "total_registered": len(self.registered_toolkits)
+ })
+
def add_conversation(self, role: str, content: str | dict):
"""Add a conversation entry to history"""
logger.debug("Adding conversation entry", extra={"task_id": self.id, "role": role, "content_length": len(str(content))})
@@ -466,7 +503,7 @@ async def _periodic_cleanup():
await asyncio.sleep(300) # Run every 5 minutes
current_time = datetime.now()
- stale_timeout = timedelta(hours=2) # Consider tasks stale after 2 hours
+ stale_timeout = timedelta(hours=4) # Consider tasks stale after 4 hours
stale_ids = []
for task_id, task_lock in task_locks.items():
diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py
index e238c01b..91ef926e 100644
--- a/backend/app/utils/agent.py
+++ b/backend/app/utils/agent.py
@@ -1,12 +1,64 @@
import asyncio
+import contextvars
import json
import os
import platform
-from threading import Event
+from threading import Event, Lock
import traceback
from typing import Any, Callable, Dict, List, Tuple
import uuid
from utils import traceroot_wrapper as traceroot
+
+# Thread-safe reference to main event loop using contextvars
+# This ensures each request has its own event loop reference, avoiding race conditions
+_main_event_loop_var: contextvars.ContextVar[asyncio.AbstractEventLoop | None] = contextvars.ContextVar(
+ '_main_event_loop', default=None
+)
+
+# Global fallback for main event loop reference
+# Used when contextvars don't propagate to worker threads (e.g., asyncio.to_thread)
+_GLOBAL_MAIN_LOOP: asyncio.AbstractEventLoop | None = None
+_GLOBAL_MAIN_LOOP_LOCK = Lock()
+
+
+def set_main_event_loop(loop: asyncio.AbstractEventLoop | None):
+ """Set the main event loop reference for thread-safe task scheduling.
+
+ This should be called from the main async context before spawning threads
+ that need to schedule async tasks. Uses both contextvars (for request isolation)
+ and a global fallback (for thread pool workers where contextvars may not propagate).
+ """
+ global _GLOBAL_MAIN_LOOP
+ _main_event_loop_var.set(loop)
+ with _GLOBAL_MAIN_LOOP_LOCK:
+ _GLOBAL_MAIN_LOOP = loop
+
+
+def _schedule_async_task(coro):
+ """Schedule an async coroutine as a task, thread-safe.
+
+ This function handles scheduling from both the main event loop thread
+ and from worker threads (e.g., when using asyncio.to_thread).
+ """
+ try:
+ # Try to get the running loop (works in main event loop thread)
+ loop = asyncio.get_running_loop()
+ loop.create_task(coro)
+ except RuntimeError:
+ # No running loop in this thread (we're in a worker thread)
+ # First try contextvars, then fallback to global reference
+ main_loop = _main_event_loop_var.get()
+ if main_loop is None:
+ with _GLOBAL_MAIN_LOOP_LOCK:
+ main_loop = _GLOBAL_MAIN_LOOP
+ if main_loop is not None and main_loop.is_running():
+ asyncio.run_coroutine_threadsafe(coro, main_loop)
+ else:
+ # This should not happen in normal operation - log error and skip
+ traceroot.get_logger("agent").error(
+ "No event loop available for async task scheduling, task skipped. "
+ "Ensure set_main_event_loop() is called before parallel agent creation."
+ )
from camel.agents import ChatAgent
from camel.agents.chat_agent import (
StreamingChatAgentResponse,
@@ -121,7 +173,7 @@ class ListenChatAgent(ChatAgent):
pause_event: asyncio.Event | None = None,
prune_tool_calls_from_memory: bool = False,
enable_snapshot_clean: bool = False,
- step_timeout: float | None = 900,
+ step_timeout: float | None = 1800, # 30 minutes
**kwargs: Any,
) -> None:
super().__init__(
@@ -373,7 +425,9 @@ class ListenChatAgent(ChatAgent):
# Check if tool is wrapped by @listen_toolkit decorator
# If so, the decorator will handle activate/deactivate events
- has_listen_decorator = hasattr(tool.func, "__wrapped__")
+ # TODO: Refactor - current marker detection is a workaround. The proper fix is to
+ # unify event sending: remove activate/deactivate from @listen_toolkit, only send here
+ has_listen_decorator = getattr(tool.func, "__listen_toolkit__", False)
try:
task_lock = get_task_lock(self.api_task_id)
@@ -511,19 +565,23 @@ class ListenChatAgent(ChatAgent):
f"Agent {self.agent_name} executing async tool: {func_name} from toolkit: {toolkit_name} with args: {json.dumps(args, ensure_ascii=False)}"
)
- # Always send activate event from agent to ensure consistent logging
- # This ensures all tool calls are logged, regardless of decorator detection issues
- await task_lock.put_queue(
- ActionActivateToolkitData(
- data={
- "agent_name": self.agent_name,
- "process_task_id": self.process_task_id,
- "toolkit_name": toolkit_name,
- "method_name": func_name,
- "message": json.dumps(args, ensure_ascii=False),
- },
+ # Check if tool is wrapped by @listen_toolkit decorator
+ # If so, the decorator will handle activate/deactivate events
+ has_listen_decorator = getattr(tool.func, "__listen_toolkit__", False)
+
+ # Only send activate event if tool is NOT wrapped by @listen_toolkit
+ if not has_listen_decorator:
+ await task_lock.put_queue(
+ ActionActivateToolkitData(
+ data={
+ "agent_name": self.agent_name,
+ "process_task_id": self.process_task_id,
+ "toolkit_name": toolkit_name,
+ "method_name": func_name,
+ "message": json.dumps(args, ensure_ascii=False),
+ },
+ )
)
- )
try:
# Set process_task context for all tool executions
with set_process_task(self.process_task_id):
@@ -591,18 +649,19 @@ class ListenChatAgent(ChatAgent):
else:
result_msg = result_str
- # Always send deactivate event from agent to ensure consistent logging
- await task_lock.put_queue(
- ActionDeactivateToolkitData(
- data={
- "agent_name": self.agent_name,
- "process_task_id": self.process_task_id,
- "toolkit_name": toolkit_name,
- "method_name": func_name,
- "message": result_msg,
- },
+ # Only send deactivate event if tool is NOT wrapped by @listen_toolkit
+ if not has_listen_decorator:
+ await task_lock.put_queue(
+ ActionDeactivateToolkitData(
+ data={
+ "agent_name": self.agent_name,
+ "process_task_id": self.process_task_id,
+ "toolkit_name": toolkit_name,
+ "method_name": func_name,
+ "message": result_msg,
+ },
+ )
)
- )
return self._record_tool_calling(
func_name,
args,
@@ -639,7 +698,9 @@ class ListenChatAgent(ChatAgent):
mask_tool_output=self.mask_tool_output,
pause_event=self.pause_event,
prune_tool_calls_from_memory=self.prune_tool_calls_from_memory,
+ enable_snapshot_clean=self._enable_snapshot_clean,
step_timeout=self.step_timeout,
+ stream_accumulate=self.stream_accumulate,
)
new_agent.process_task_id = self.process_task_id
@@ -668,10 +729,11 @@ def agent_model(
):
task_lock = get_task_lock(options.project_id)
agent_id = str(uuid.uuid4())
- traceroot_logger.info(
+ traceroot_logger.debug(
f"Creating agent: {agent_name} with id: {agent_id} for project: {options.project_id}"
)
- asyncio.create_task(
+ # Use thread-safe scheduling to support parallel agent creation
+ _schedule_async_task(
task_lock.put_queue(
ActionCreateAgentData(
data={
@@ -737,19 +799,21 @@ def agent_model(
)
model_platform_enum = None
+ model = ModelFactory.create(
+ model_platform=options.model_platform,
+ model_type=options.model_type,
+ api_key=options.api_key,
+ url=options.api_url,
+ model_config_dict=model_config or None,
+ timeout=600, # 10 minutes
+ **init_params,
+ )
+
return ListenChatAgent(
options.project_id,
agent_name,
system_message,
- model=ModelFactory.create(
- model_platform=options.model_platform,
- model_type=options.model_type,
- api_key=options.api_key,
- url=options.api_url,
- model_config_dict=model_config or None,
- **init_params,
- ),
- # output_language=options.language,
+ model=model,
tools=tools,
agent_id=agent_id,
prune_tool_calls_from_memory=prune_tool_calls_from_memory,
@@ -803,9 +867,10 @@ 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=False,
+ clone_current_env=True,
)
terminal_toolkit = message_integration.register_toolkits(terminal_toolkit)
@@ -970,9 +1035,10 @@ these tips to maximize your effectiveness:
@traceroot.trace()
def browser_agent(options: Chat):
working_directory = get_working_directory(options)
- traceroot_logger.info(
+ traceroot_logger.debug(
f"Creating browser agent for project: {options.project_id} in directory: {working_directory}"
)
+
message_integration = ToolkitMessageIntegration(
message_handler=HumanToolkit(
options.project_id, Agents.browser_agent
@@ -998,19 +1064,23 @@ def browser_agent(options: Chat):
"browser_switch_tab",
"browser_enter",
"browser_visit_page",
+ "browser_scroll",
+ "browser_sheet_read",
+ "browser_sheet_input",
"browser_get_page_snapshot",
- # "browser_get_som_screenshot",
],
)
# Save reference before registering for toolkits_to_register_agent
web_toolkit_for_agent_registration = web_toolkit_custom
web_toolkit_custom = message_integration.register_toolkits(web_toolkit_custom)
+
terminal_toolkit = TerminalToolkit(
options.project_id,
Agents.browser_agent,
+ working_directory=working_directory,
safe_mode=True,
- clone_current_env=False,
+ clone_current_env=True,
)
terminal_toolkit = message_integration.register_functions(
[terminal_toolkit.shell_exec]
@@ -1020,8 +1090,8 @@ def browser_agent(options: Chat):
options.project_id, Agents.browser_agent, working_directory=working_directory
)
note_toolkit = message_integration.register_toolkits(note_toolkit)
+
search_tools = SearchToolkit.get_can_use_tools(options.project_id)
- # Only register search tools if any are available
if search_tools:
search_tools = message_integration.register_functions(search_tools)
else:
@@ -1171,9 +1241,10 @@ Your approach depends on available search tools:
@traceroot.trace()
async def document_agent(options: Chat):
working_directory = get_working_directory(options)
- traceroot_logger.info(
+ traceroot_logger.debug(
f"Creating document agent for project: {options.project_id} in directory: {working_directory}"
)
+
message_integration = ToolkitMessageIntegration(
message_handler=HumanToolkit(
options.project_id, Agents.task_agent
@@ -1194,13 +1265,20 @@ async def document_agent(options: Chat):
options.project_id, Agents.document_agent, working_directory=working_directory
)
note_toolkit = message_integration.register_toolkits(note_toolkit)
+
terminal_toolkit = TerminalToolkit(
options.project_id,
Agents.document_agent,
+ working_directory=working_directory,
safe_mode=True,
- clone_current_env=False,
+ clone_current_env=True,
)
terminal_toolkit = message_integration.register_toolkits(terminal_toolkit)
+
+ google_drive_tools = await GoogleDriveMCPToolkit.get_can_use_tools(
+ options.project_id, options.get_bun_env()
+ )
+
tools = [
*file_write_toolkit.get_tools(),
*pptx_toolkit.get_tools(),
@@ -1209,9 +1287,7 @@ async def document_agent(options: Chat):
*excel_toolkit.get_tools(),
*note_toolkit.get_tools(),
*terminal_toolkit.get_tools(),
- *await GoogleDriveMCPToolkit.get_can_use_tools(
- options.project_id, options.get_bun_env()
- ),
+ *google_drive_tools,
]
# if env("EXA_API_KEY") or options.is_cloud():
# search_toolkit = SearchToolkit(options.project_id, Agents.document_agent).search_exa
@@ -1394,9 +1470,10 @@ supported formats including advanced spreadsheet functionality.
@traceroot.trace()
def multi_modal_agent(options: Chat):
working_directory = get_working_directory(options)
- traceroot_logger.info(
+ traceroot_logger.debug(
f"Creating multi-modal agent for project: {options.project_id} in directory: {working_directory}"
)
+
message_integration = ToolkitMessageIntegration(
message_handler=HumanToolkit(
options.project_id, Agents.multi_modal_agent
@@ -1416,10 +1493,12 @@ 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=False,
+ clone_current_env=True,
)
terminal_toolkit = message_integration.register_toolkits(terminal_toolkit)
+
note_toolkit = NoteTakingToolkit(
options.project_id,
Agents.multi_modal_agent,
@@ -1609,7 +1688,8 @@ async def social_medium_agent(options: Chat):
*TerminalToolkit(
options.project_id,
agent_name=Agents.social_medium_agent,
- clone_current_env=False,
+ working_directory=working_directory,
+ clone_current_env=True,
).get_tools(),
*NoteTakingToolkit(
options.project_id,
@@ -1776,6 +1856,7 @@ async def mcp_agent(options: Chat):
if options.is_cloud()
else None
),
+ timeout=600, # 10 minutes
**{
k: v
for k, v in (options.extra_params or {}).items()
diff --git a/backend/app/utils/listen/toolkit_listen.py b/backend/app/utils/listen/toolkit_listen.py
index f8cff0c3..beb7f9d7 100644
--- a/backend/app/utils/listen/toolkit_listen.py
+++ b/backend/app/utils/listen/toolkit_listen.py
@@ -185,6 +185,8 @@ def listen_toolkit(
raise error
return res
+ # Mark this wrapper as decorated by @listen_toolkit for detection in agent.py
+ async_wrapper.__listen_toolkit__ = True
return async_wrapper
else:
@@ -291,6 +293,8 @@ def listen_toolkit(
raise error
return res
+ # Mark this wrapper as decorated by @listen_toolkit for detection in agent.py
+ sync_wrapper.__listen_toolkit__ = True
return sync_wrapper
return decorator
@@ -350,14 +354,9 @@ def auto_listen_toolkit(base_toolkit_class: Type[T]) -> Callable[[Type[T]], Type
# Method is overridden, check if it already has @listen_toolkit decorator
overridden_method = cls.__dict__[method_name]
- # Check if already decorated by looking for the wrapper attributes
- # that listen_toolkit adds (like __wrapped__ or specific markers)
- is_already_decorated = (
- hasattr(overridden_method, '__wrapped__') or
- (hasattr(overridden_method, '__name__') and
- hasattr(getattr(overridden_method, '__code__', None), 'co_freevars') and
- 'toolkit' in getattr(overridden_method.__code__, 'co_freevars', []))
- )
+ # Check if already decorated by looking for the __listen_toolkit__ marker
+ # that listen_toolkit adds to its wrappers
+ is_already_decorated = getattr(overridden_method, '__listen_toolkit__', False)
if is_already_decorated:
# Already has @listen_toolkit, skip
diff --git a/backend/app/utils/toolkit/hybrid_browser_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_toolkit.py
index f6136d61..67705222 100644
--- a/backend/app/utils/toolkit/hybrid_browser_toolkit.py
+++ b/backend/app/utils/toolkit/hybrid_browser_toolkit.py
@@ -1,7 +1,9 @@
import os
import asyncio
import json
+import uuid
from typing import Any, Dict, List, Optional
+from typing_extensions import TypedDict
import websockets
import websockets.exceptions
@@ -18,12 +20,29 @@ from utils import traceroot_wrapper as traceroot
logger = traceroot.get_logger("hybrid_browser_toolkit")
+# Global navigation lock to prevent concurrent visit_page conflicts (ERR_ABORTED)
+# This is needed because multiple sessions may share the same browser via CDP
+_global_navigation_lock = asyncio.Lock()
+
+# Global registry: tab_id -> session_id (ensures each tab belongs to only one session)
+_global_tab_registry: Dict[str, str] = {}
+_global_tab_registry_lock = asyncio.Lock()
+
+
+class SheetCell(TypedDict):
+ row: int
+ col: int
+ text: str
+
class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper):
def __init__(self, config: Optional[Dict[str, Any]] = None):
"""Initialize wrapper."""
super().__init__(config)
logger.info(f"WebSocketBrowserWrapper using ts_dir: {self.ts_dir}")
+ # Track tabs opened by this session for isolation
+ self._session_tab_ids: set = set()
+ self._wrapper_session_id: str = str(uuid.uuid4())
def _ensure_local_no_proxy(self) -> None:
local_hosts = ["localhost", "127.0.0.1", "::1"]
@@ -136,6 +155,100 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper):
logger.error(f"Unexpected error sending command '{command}': {type(e).__name__}: {e}")
raise
+ async def visit_page(self, url: str) -> Dict[str, Any]:
+ """Override visit_page to add global navigation lock preventing ERR_ABORTED.
+
+ Multiple sessions sharing the same browser via CDP can cause conflicts
+ when they try to navigate simultaneously (e.g., both trying to use a
+ blank page). This lock serializes navigation operations at the WebSocket
+ wrapper level.
+ """
+ global _global_navigation_lock
+
+ async with _global_navigation_lock:
+ logger.debug(f"[visit_page] Acquired navigation lock, navigating to {url}")
+ try:
+ result = await super().visit_page(url)
+ logger.debug(f"[visit_page] Navigation completed, releasing lock")
+ return result
+ except Exception as e:
+ logger.error(f"[visit_page] Navigation failed: {e}")
+ raise
+
+ async def get_tab_info(self) -> List[Dict[str, Any]]:
+ """Override get_tab_info to track and filter tabs for session isolation.
+
+ Automatically tracks the current tab (is_current=true) as belonging to
+ this session, then filters to only return tabs owned by this session.
+ Uses global registry to ensure each tab belongs to only one session.
+ """
+ global _global_tab_registry, _global_tab_registry_lock
+
+ all_tabs = await super().get_tab_info()
+ session_id = self._wrapper_session_id # Stable UUID for this wrapper
+
+ # Auto-track: add current tab to this session's tracked tabs (with global lock)
+ current_tab = next((t for t in all_tabs if t.get('is_current')),
+ None)
+ if current_tab and current_tab.get('tab_id'):
+ tab_id = current_tab['tab_id']
+ async with _global_tab_registry_lock:
+ # Only track if not already owned by another session
+ if tab_id not in _global_tab_registry:
+ _global_tab_registry[tab_id] = session_id
+ self._session_tab_ids.add(tab_id)
+ logger.info(
+ f"[Session Tab Tracking] Auto-tracked current tab: {tab_id}, session {session_id} now has tabs: {self._session_tab_ids}")
+ elif _global_tab_registry[tab_id] == session_id:
+ # Already owned by this session, ensure local tracking
+ self._session_tab_ids.add(tab_id)
+
+ # Filter: only return tabs belonging to this session
+ filtered_tabs = [tab for tab in all_tabs if
+ tab.get('tab_id') in self._session_tab_ids]
+ logger.info(
+ f"[Session Tab Filtering] Session {session_id}: Returning {len(filtered_tabs)}/{len(all_tabs)} tabs, tracked: {self._session_tab_ids}")
+
+ return filtered_tabs
+
+ async def close_tab(self, tab_id: str) -> Dict[str, Any]:
+ """Override close_tab to update tracking."""
+ global _global_tab_registry, _global_tab_registry_lock
+
+ result = await super().close_tab(tab_id)
+
+ # Remove from tracking if it was ours
+ if tab_id in self._session_tab_ids:
+ self._session_tab_ids.discard(tab_id)
+ async with _global_tab_registry_lock:
+ if tab_id in _global_tab_registry:
+ del _global_tab_registry[tab_id]
+ logger.info(
+ f"[Session Tab Tracking] Removed closed tab: {tab_id}, session now has tabs: {self._session_tab_ids}")
+
+ return result
+
+ async def cleanup_tab_tracking(self):
+ """Clean up all tab tracking for this session from the global registry.
+
+ Should be called when the wrapper is being stopped/destroyed to prevent
+ memory leaks and stale entries in the global registry.
+ """
+ global _global_tab_registry, _global_tab_registry_lock
+
+ if not self._session_tab_ids:
+ return
+
+ async with _global_tab_registry_lock:
+ cleaned_count = len(self._session_tab_ids)
+ for tab_id in list(self._session_tab_ids):
+ if tab_id in _global_tab_registry:
+ del _global_tab_registry[tab_id]
+ # Clear inside lock to prevent race with concurrent get_tab_info
+ self._session_tab_ids.clear()
+ logger.info(
+ f"[Session Tab Tracking] Cleaned up {cleaned_count} tabs for session {self._wrapper_session_id}")
+
# WebSocket connection pool
class WebSocketConnectionPool:
@@ -183,6 +296,7 @@ class WebSocketConnectionPool:
# Connection is unhealthy, clean it up
logger.info(f"Removing unhealthy WebSocket connection for session {session_id}")
try:
+ await wrapper.cleanup_tab_tracking()
await wrapper.stop()
except Exception as e:
logger.debug(f"Error stopping unhealthy wrapper: {e}")
@@ -202,6 +316,7 @@ class WebSocketConnectionPool:
if session_id in self._connections:
wrapper = self._connections[session_id]
try:
+ await wrapper.cleanup_tab_tracking()
await wrapper.stop()
except Exception as e:
logger.error(f"Error closing WebSocket connection for session {session_id}: {e}")
@@ -213,6 +328,7 @@ class WebSocketConnectionPool:
if session_id in self._connections:
wrapper = self._connections[session_id]
try:
+ await wrapper.cleanup_tab_tracking()
await wrapper.stop()
except Exception as e:
logger.error(f"Error closing WebSocket connection for session {session_id}: {e}")
@@ -230,6 +346,7 @@ class WebSocketConnectionPool:
# Global connection pool instance
websocket_connection_pool = WebSocketConnectionPool()
+
@auto_listen_toolkit(BaseHybridBrowserToolkit)
class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
agent_name: str = Agents.browser_agent
@@ -360,6 +477,10 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
full_visual_mode=self._full_visual_mode,
)
+ async def browser_sheet_input(self, *, cells: List[SheetCell]) -> Dict[str, Any]:
+ # Use typing_extensions.TypedDict for Pydantic <3.12 compatibility.
+ return await super().browser_sheet_input(cells=cells)
+
@classmethod
def toolkit_name(cls) -> str:
return "Browser Toolkit"
diff --git a/backend/app/utils/toolkit/terminal_toolkit.py b/backend/app/utils/toolkit/terminal_toolkit.py
index 7eff1ccd..9109dcab 100644
--- a/backend/app/utils/toolkit/terminal_toolkit.py
+++ b/backend/app/utils/toolkit/terminal_toolkit.py
@@ -1,7 +1,11 @@
import asyncio
import logging
import os
+import platform
+import shutil
+import subprocess
import threading
+import time
from concurrent.futures import ThreadPoolExecutor
from typing import Optional
from camel.toolkits.terminal_toolkit import TerminalToolkit as BaseTerminalToolkit
@@ -15,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):
@@ -33,20 +51,23 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
session_logs_dir: str | None = None,
safe_mode: bool = True,
allowed_commands: list[str] | None = None,
- clone_current_env: bool = False,
+ clone_current_env: bool = True,
):
self.api_task_id = api_task_id
if agent_name is not None:
self.agent_name = agent_name
- if working_directory is None:
- working_directory = env("file_save_path", os.path.expanduser("~/.eigent/terminal/"))
- logger.info("Initializing TerminalToolkit", extra={
+ # Get base directory from environment
+ base_dir = env("file_save_path", os.path.expanduser("~/.eigent/terminal/"))
+
+ if working_directory is None:
+ 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,
- "agent_name": self.agent_name,
"working_directory": working_directory,
- "safe_mode": safe_mode,
- "use_docker_backend": use_docker_backend
+ "agent_venv_dir": self._agent_venv_dir,
})
if TerminalToolkit._thread_pool is None:
@@ -54,7 +75,6 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
max_workers=1,
thread_name_prefix="terminal_toolkit"
)
- logger.debug("Created terminal toolkit thread pool")
super().__init__(
timeout=timeout,
@@ -64,16 +84,156 @@ 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)
+ if task_lock:
+ task_lock.register_toolkit(self)
+ logger.info("TerminalToolkit registered for cleanup", extra={
+ "api_task_id": api_task_id,
+ "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 the cloned venv path for shell activation."""
+ cloned_env_path = getattr(self, 'cloned_env_path', None)
+ if cloned_env_path and os.path.exists(cloned_env_path):
+ return cloned_env_path
+ return None
+
+ def _clone_venv_with_symlinks(self, source_venv: str, target_venv: str):
+ """Clone a venv using symlinks for efficiency.
+
+ Creates the structure needed: pyvenv.cfg, bin/python, lib symlink, and activate scripts.
+ """
+ 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))
+ # Copy activate scripts (need to modify VIRTUAL_ENV path)
+ for script in ["activate.bat", "activate.ps1", "deactivate.bat"]:
+ src = os.path.join(source_scripts, script)
+ if os.path.exists(src):
+ with open(src, 'r', encoding='utf-8') as f:
+ content = f.read()
+ content = content.replace(source_venv, target_venv)
+ dst = os.path.join(target_bin, script)
+ with open(dst, 'w', encoding='utf-8') as f:
+ f.write(content)
+ # 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"))
+
+ # Copy activate scripts (need to modify VIRTUAL_ENV path)
+ source_bin = os.path.join(source_venv, "bin")
+ for script in ["activate", "activate.csh", "activate.fish"]:
+ src = os.path.join(source_bin, script)
+ if os.path.exists(src):
+ with open(src, 'r') as f:
+ content = f.read()
+ # Replace source venv path with target venv path
+ content = content.replace(source_venv, target_venv)
+ dst = os.path.join(target_bin, script)
+ with open(dst, 'w') as f:
+ f.write(content)
+
+ # 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.
@@ -175,6 +335,51 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
return result
+ def cleanup(self, remove_venv: bool = True):
+ """Clean up all active sessions and optionally remove the virtual environment.
+
+ Args:
+ remove_venv: If True, removes the .venv or .initial_env folder created
+ by this toolkit. Defaults to True to prevent disk bloat.
+ """
+ # First call parent cleanup to kill all shell sessions
+ super().cleanup()
+
+ if not remove_venv:
+ return
+
+ # Remove cloned env (.venv) if it exists
+ cloned_env_path = getattr(self, 'cloned_env_path', None)
+ if cloned_env_path and os.path.exists(cloned_env_path):
+ try:
+ shutil.rmtree(cloned_env_path)
+ logger.info("Removed cloned venv", extra={
+ "api_task_id": self.api_task_id,
+ "path": cloned_env_path
+ })
+ except Exception as e:
+ logger.warning("Failed to remove cloned venv", extra={
+ "api_task_id": self.api_task_id,
+ "path": cloned_env_path,
+ "error": str(e)
+ })
+
+ # Remove initial env (.initial_env) if it exists
+ initial_env_path = getattr(self, 'initial_env_path', None)
+ if initial_env_path and os.path.exists(initial_env_path):
+ try:
+ shutil.rmtree(initial_env_path)
+ logger.info("Removed initial env", extra={
+ "api_task_id": self.api_task_id,
+ "path": initial_env_path
+ })
+ except Exception as e:
+ logger.warning("Failed to remove initial env", extra={
+ "api_task_id": self.api_task_id,
+ "path": initial_env_path,
+ "error": str(e)
+ })
+
@classmethod
def shutdown(cls):
if cls._thread_pool:
diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py
index 0b5bde45..363e6462 100644
--- a/backend/app/utils/workforce.py
+++ b/backend/app/utils/workforce.py
@@ -61,7 +61,7 @@ class Workforce(BaseWorkforce):
graceful_shutdown_timeout=graceful_shutdown_timeout,
share_memory=share_memory,
use_structured_output_handler=use_structured_output_handler,
- task_timeout_seconds=1800, # 30 minutes
+ task_timeout_seconds=3600, # 60 minutes
failure_handling_config=FailureHandlingConfig(
enabled_strategies=["retry", "replan"],
),
@@ -87,85 +87,66 @@ class Workforce(BaseWorkforce):
on_stream_batch: Optional callback for streaming batches signature (List[Task], bool)
on_stream_text: Optional callback for raw streaming text chunks
"""
- logger.info("=" * 80)
- logger.info("🧩 [DECOMPOSE] eigent_make_sub_tasks CALLED", extra={
+ logger.debug("[DECOMPOSE] eigent_make_sub_tasks called", extra={
"api_task_id": self.api_task_id,
- "workforce_id": id(self),
"task_id": task.id
})
- logger.info(f"[DECOMPOSE] Task content preview: '{task.content[:200]}...'")
- logger.info(f"[DECOMPOSE] Has coordinator context: {bool(coordinator_context)}")
- logger.info(f"[DECOMPOSE] Current workforce state: {self._state.name}, _running: {self._running}")
- logger.info("=" * 80)
if not validate_task_content(task.content, task.id):
task.state = TaskState.FAILED
task.result = "Task failed: Invalid or empty content provided"
- logger.warning("❌ [DECOMPOSE] Task rejected: Invalid or empty content", extra={
+ logger.warning("[DECOMPOSE] Task rejected: Invalid or empty content", extra={
"task_id": task.id,
"content_preview": task.content[:50] + "..." if len(task.content) > 50 else task.content
})
raise UserException(code.error, task.result)
- logger.info(f"[DECOMPOSE] Resetting workforce state")
self.reset()
self._task = task
self.set_channel(TaskChannel())
self._state = WorkforceState.RUNNING
task.state = TaskState.OPEN
- logger.info(f"[DECOMPOSE] Workforce reset complete, state: {self._state.name}")
- logger.info(f"[DECOMPOSE] Calling handle_decompose_append_task")
subtasks = asyncio.run(
self.handle_decompose_append_task(
- task,
- reset=False,
+ task,
+ reset=False,
coordinator_context=coordinator_context,
- on_stream_batch=on_stream_batch,
+ on_stream_batch=on_stream_batch,
on_stream_text=on_stream_text
)
)
- logger.info("=" * 80)
- logger.info(f"✅ [DECOMPOSE] Task decomposition COMPLETED", extra={
+
+ logger.info(f"[DECOMPOSE] Task decomposition completed", extra={
"api_task_id": self.api_task_id,
"task_id": task.id,
"subtasks_count": len(subtasks)
})
- logger.info("=" * 80)
return subtasks
async def eigent_start(self, subtasks: list[Task]):
"""start the workforce"""
- logger.info("=" * 80)
- logger.info("▶️ [WF-LIFECYCLE] eigent_start CALLED", extra={"api_task_id": self.api_task_id, "workforce_id": id(self)})
- logger.info(f"[WF-LIFECYCLE] Starting workforce execution with {len(subtasks)} subtasks")
- logger.info(f"[WF-LIFECYCLE] Current workforce state: {self._state.name}, _running: {self._running}")
- logger.info("=" * 80)
+ logger.debug(f"[WF-LIFECYCLE] eigent_start called with {len(subtasks)} subtasks", extra={
+ "api_task_id": self.api_task_id
+ })
self._pending_tasks.extendleft(reversed(subtasks))
- # Save initial snapshot
self.save_snapshot("Initial task decomposition")
try:
- logger.info(f"[WF-LIFECYCLE] Calling base class start() method")
await self.start()
- logger.info(f"[WF-LIFECYCLE] ✅ Base class start() method completed")
except Exception as e:
- logger.error(f"[WF-LIFECYCLE] ❌ Error in workforce execution: {e}", extra={
+ logger.error(f"[WF-LIFECYCLE] Error in workforce execution: {e}", extra={
"api_task_id": self.api_task_id,
"error": str(e)
}, exc_info=True)
self._state = WorkforceState.STOPPED
- logger.info(f"[WF-LIFECYCLE] Workforce state set to STOPPED after error")
raise
finally:
- logger.info(f"[WF-LIFECYCLE] eigent_start finally block, current state: {self._state.name}")
if self._state != WorkforceState.STOPPED:
self._state = WorkforceState.IDLE
- logger.info(f"[WF-LIFECYCLE] Workforce state set to IDLE")
def _decompose_task(self, task: Task, stream_callback=None):
"""Decompose task with optional streaming text callback."""
-
decompose_prompt = str(
TASK_DECOMPOSE_PROMPT.format(
content=task.content,
@@ -173,6 +154,7 @@ class Workforce(BaseWorkforce):
additional_info=task.additional_info,
)
)
+
self.task_agent.reset()
result = task.decompose(
self.task_agent, decompose_prompt, stream_callback=stream_callback
@@ -217,7 +199,7 @@ class Workforce(BaseWorkforce):
Returns:
List[Task]: The decomposed subtasks or the original task
"""
- logger.info(f"[DECOMPOSE] handle_decompose_append_task CALLED, task_id={task.id}, reset={reset}")
+ logger.debug(f"[DECOMPOSE] handle_decompose_append_task called, task_id={task.id}, reset={reset}")
if not validate_task_content(task.content, task.id):
task.state = TaskState.FAILED
@@ -229,31 +211,20 @@ class Workforce(BaseWorkforce):
return [task]
if reset and self._state != WorkforceState.RUNNING:
- logger.info(f"[DECOMPOSE] Resetting workforce (reset={reset}, state={self._state.name})")
self.reset()
- logger.info("[DECOMPOSE] Workforce reset complete")
self._task = task
task.state = TaskState.FAILED
if coordinator_context:
- logger.info(f"[DECOMPOSE] Adding coordinator context to task")
original_content = task.content
- task_with_context = coordinator_context
- if coordinator_context:
- task_with_context += "\n=== CURRENT TASK ===\n"
- task_with_context += original_content
+ task_with_context = coordinator_context + "\n=== CURRENT TASK ===\n" + original_content
task.content = task_with_context
-
- logger.info(f"[DECOMPOSE] Calling _decompose_task with context")
subtasks_result = self._decompose_task(task, stream_callback=on_stream_text)
-
task.content = original_content
else:
- logger.info(f"[DECOMPOSE] Calling _decompose_task without context")
subtasks_result = self._decompose_task(task, stream_callback=on_stream_text)
- logger.info(f"[DECOMPOSE] _decompose_task returned, processing results")
if isinstance(subtasks_result, Generator):
subtasks = []
for new_tasks in subtasks_result:
@@ -263,18 +234,15 @@ class Workforce(BaseWorkforce):
on_stream_batch(new_tasks, False)
except Exception as e:
logger.warning(f"Streaming callback failed: {e}")
- logger.info(f"[DECOMPOSE] Collected {len(subtasks)} subtasks from generator")
# After consuming the generator, check task.subtasks for final result as fallback
if not subtasks and task.subtasks:
subtasks = task.subtasks
else:
subtasks = subtasks_result
- logger.info(f"[DECOMPOSE] Got {len(subtasks) if subtasks else 0} subtasks directly")
if subtasks:
self._pending_tasks.extendleft(reversed(subtasks))
- logger.info(f"[DECOMPOSE] ✅ Appended {len(subtasks)} subtasks to pending tasks")
if not subtasks:
logger.warning(f"[DECOMPOSE] No subtasks returned, creating fallback task")
@@ -285,7 +253,6 @@ class Workforce(BaseWorkforce):
)
task.subtasks = [fallback_task]
subtasks = [fallback_task]
- logger.info(f"[DECOMPOSE] Created fallback task: {fallback_task.id}")
if on_stream_batch:
try:
@@ -293,6 +260,7 @@ class Workforce(BaseWorkforce):
except Exception as e:
logger.warning(f"Final streaming callback failed: {e}")
+ logger.debug(f"[DECOMPOSE] handle_decompose_append_task completed, returned {len(subtasks)} subtasks")
return subtasks
def _get_agent_id_from_node_id(self, node_id: str) -> str | None:
@@ -387,6 +355,7 @@ class Workforce(BaseWorkforce):
f"Task {task.id} will not be properly tracked on frontend. "
f"Available workers: {[c.node_id for c in self._children if hasattr(c, 'node_id')]}"
)
+ else:
await task_lock.put_queue(
ActionAssignTaskData(
action=Action.assign_task,
@@ -423,7 +392,7 @@ class Workforce(BaseWorkforce):
worker=worker,
pool_max_size=pool_max_size,
use_structured_output_handler=self.use_structured_output_handler,
- context_utility=None, # Will be set during save/load operations
+ context_utility=None,
enable_workflow_memory=enable_workflow_memory,
)
self._children.append(worker_node)
@@ -444,6 +413,7 @@ class Workforce(BaseWorkforce):
role=worker_node.description,
)
metrics_callbacks[0].log_worker_created(event)
+
return self
async def _handle_completed_task(self, task: Task) -> None:
@@ -480,6 +450,11 @@ class Workforce(BaseWorkforce):
result = await super()._handle_failed_task(task)
+ # Only send completion report to frontend when all retries are exhausted
+ max_retries = self.failure_handling_config.max_retries
+ if task.failure_count < max_retries:
+ return result
+
error_message = ""
# Use proper CAMEL pattern for metrics logging
metrics_callbacks = [cb for cb in self._callbacks if isinstance(cb, WorkforceMetrics)]
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 9dbe8c24..4a54a93a 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -6,7 +6,7 @@ readme = "README.md"
requires-python = ">=3.10,<3.11"
dependencies = [
"pip>=23.0",
- "camel-ai[eigent]==0.2.83a9",
+ "camel-ai[eigent]==0.2.84",
"fastapi>=0.115.12",
"fastapi-babel>=1.0.0",
"uvicorn[standard]>=0.34.2",
diff --git a/backend/uv.lock b/backend/uv.lock
index fdde31a4..7a2095e9 100644
--- a/backend/uv.lock
+++ b/backend/uv.lock
@@ -192,15 +192,15 @@ wheels = [
[[package]]
name = "azure-core"
-version = "1.37.0"
+version = "1.38.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ef/83/41c9371c8298999c67b007e308a0a3c4d6a59c6908fa9c62101f031f886f/azure_core-1.37.0.tar.gz", hash = "sha256:7064f2c11e4b97f340e8e8c6d923b822978be3016e46b7bc4aa4b337cfb48aee", size = 357620, upload-time = "2025-12-11T20:05:13.518Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/e503e08e755ea94e7d3419c9242315f888fc664211c90d032e40479022bf/azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993", size = 363033, upload-time = "2026-01-12T17:03:05.535Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ee/34/a9914e676971a13d6cc671b1ed172f9804b50a3a80a143ff196e52f4c7ee/azure_core-1.37.0-py3-none-any.whl", hash = "sha256:b3abe2c59e7d6bb18b38c275a5029ff80f98990e7c90a5e646249a56630fcc19", size = 214006, upload-time = "2025-12-11T20:05:14.96Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825, upload-time = "2026-01-12T17:03:07.291Z" },
]
[[package]]
@@ -261,7 +261,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiofiles", specifier = ">=24.1.0" },
- { name = "camel-ai", extras = ["eigent"], specifier = "==0.2.83a9" },
+ { name = "camel-ai", extras = ["eigent"], specifier = "==0.2.84" },
{ name = "debugpy", specifier = ">=1.8.17" },
{ name = "fastapi", specifier = ">=0.115.12" },
{ name = "fastapi-babel", specifier = ">=1.0.0" },
@@ -309,35 +309,35 @@ wheels = [
[[package]]
name = "boto3"
-version = "1.42.24"
+version = "1.42.30"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ee/21/8be0e3685c3a4868be48d8d2f6e5b4641727e1d8a5d396b8b401d2b5f06e/boto3-1.42.24.tar.gz", hash = "sha256:c47a2f40df933e3861fc66fd8d6b87ee36d4361663a7e7ba39a87f5a78b2eae1", size = 112788, upload-time = "2026-01-07T20:30:51.019Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/42/79/2dac8b7cb075cfa43908ee9af3f8ee06880d84b86013854c5cca8945afac/boto3-1.42.30.tar.gz", hash = "sha256:ba9cd2f7819637d15bfbeb63af4c567fcc8a7dcd7b93dd12734ec58601169538", size = 112809, upload-time = "2026-01-16T20:37:23.636Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/75/bbfccb268f9faa4f59030888e859dca9797a980b77d6a074113af73bd4bf/boto3-1.42.24-py3-none-any.whl", hash = "sha256:8ed6ad670a5a2d7f66c1b0d3362791b48392c7a08f78479f5d8ab319a4d9118f", size = 140572, upload-time = "2026-01-07T20:30:49.431Z" },
+ { url = "https://files.pythonhosted.org/packages/52/b3/2c0d828c9f668292e277ca5232e6160dd5b4b660a3f076f20dd5378baa1e/boto3-1.42.30-py3-none-any.whl", hash = "sha256:d7e548bea65e0ae2c465c77de937bc686b591aee6a352d5a19a16bc751e591c1", size = 140573, upload-time = "2026-01-16T20:37:22.089Z" },
]
[[package]]
name = "botocore"
-version = "1.42.24"
+version = "1.42.30"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/12/d7/bb4a4e839b238ffb67b002d7326b328ebe5eb23ed5180f2ca10399a802de/botocore-1.42.24.tar.gz", hash = "sha256:be8d1bea64fb91eea08254a1e5fea057e4428d08e61f4e11083a02cafc1f8cc6", size = 14878455, upload-time = "2026-01-07T20:30:40.379Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/38/23862628a0eb044c8b8b3d7a9ad1920b3bfd6bce6d746d5a871e8382c7e4/botocore-1.42.30.tar.gz", hash = "sha256:9bf1662b8273d5cc3828a49f71ca85abf4e021011c1f0a71f41a2ea5769a5116", size = 14891439, upload-time = "2026-01-16T20:37:13.77Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ff/d4/f2655d777eed8b069ecab3761454cb83f830f8be8b5b0d292e4b3a980d00/botocore-1.42.24-py3-none-any.whl", hash = "sha256:8fca9781d7c84f7ad070fceffaff7179c4aa7a5ffb27b43df9d1d957801e0a8d", size = 14551806, upload-time = "2026-01-07T20:30:38.103Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/8d/6d7b016383b1f74dd93611b1c5078bbaddaca901553ab886dcda87cae365/botocore-1.42.30-py3-none-any.whl", hash = "sha256:97070a438cac92430bb7b65f8ebd7075224f4a289719da4ee293d22d1e98db02", size = 14566340, upload-time = "2026-01-16T20:37:10.94Z" },
]
[[package]]
name = "camel-ai"
-version = "0.2.83a9"
+version = "0.2.84"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "astor" },
@@ -354,9 +354,9 @@ dependencies = [
{ name = "tiktoken" },
{ name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/bf/8c/7d8071776ba973bb6e734edb6caaf4fdbdf60ecebdc1c4017948cc67ad48/camel_ai-0.2.83a9.tar.gz", hash = "sha256:2ee560551797b089f9849d3b9d63cd3a2b4eb45d339d17e6bf95eba2b85c4b50", size = 1124774, upload-time = "2026-01-15T21:28:24.51Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9b/62/96d922750b304ab2dd0ac1ef35dd3fbbf0e9a44f147e08a1ddfeb94c51c6/camel_ai-0.2.84.tar.gz", hash = "sha256:173c79755fc986e3fa8e27523606222c5f8816fc085abb24831912dcd4a0dec3", size = 1125724, upload-time = "2026-01-20T17:23:13.524Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/34/77/f7594707571af9c86351a69ff9f7f580602b42ffe8113803153c069b6bff/camel_ai-0.2.83a9-py3-none-any.whl", hash = "sha256:7cfe97b590096c1cc5afddf6dca023c5b9a47d104196c16b4b2b1934931af260", size = 1595808, upload-time = "2026-01-15T21:28:21.068Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/8b/246abd2c47154de6220fd0a286c3fe50f343d49943ae17e26b9f824a1ca0/camel_ai-0.2.84-py3-none-any.whl", hash = "sha256:63bfbd09e605f9087bb73eb9b929e162fbc6778084ce50e43a367fc0e98cbc65", size = 1599378, upload-time = "2026-01-20T17:23:11.216Z" },
]
[package.optional-dependencies]
@@ -990,14 +990,14 @@ wheels = [
[[package]]
name = "httplib2"
-version = "0.31.0"
+version = "0.31.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyparsing" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/52/77/6653db69c1f7ecfe5e3f9726fdadc981794656fcd7d98c4209fecfea9993/httplib2-0.31.0.tar.gz", hash = "sha256:ac7ab497c50975147d4f7b1ade44becc7df2f8954d42b38b3d69c515f531135c", size = 250759, upload-time = "2025-09-11T12:16:03.403Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/77/df/6eb1d485a513776bbdbb1c919b72e59b5acc51c5e7ef28ad1cd444e252a3/httplib2-0.31.1.tar.gz", hash = "sha256:21591655ac54953624c6ab8d587c71675e379e31e2cfe3147c83c11e9ef41f92", size = 250746, upload-time = "2026-01-13T12:14:14.365Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8c/a2/0d269db0f6163be503775dc8b6a6fa15820cc9fdc866f6ba608d86b721f2/httplib2-0.31.0-py3-none-any.whl", hash = "sha256:b9cd78abea9b4e43a7714c6e0f8b6b8561a6fc1e95d5dbd367f5bf0ef35f5d24", size = 91148, upload-time = "2025-09-11T12:16:01.803Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/d8/1b05076441c2f01e4b64f59e5255edc2f0384a711b6d618845c023dc269b/httplib2-0.31.1-py3-none-any.whl", hash = "sha256:d520d22fa7e50c746a7ed856bac298c4300105d01bc2d8c2580a9b57fb9ed617", size = 91101, upload-time = "2026-01-13T12:14:12.676Z" },
]
[[package]]
@@ -1046,7 +1046,7 @@ wheels = [
[[package]]
name = "huggingface-hub"
-version = "1.3.1"
+version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -1060,9 +1060,9 @@ dependencies = [
{ name = "typer-slim" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/dd/dd/1cc985c5dda36298b152f75e82a1c81f52243b78fb7e9cad637a29561ad1/huggingface_hub-1.3.1.tar.gz", hash = "sha256:e80e0cfb4a75557c51ab20d575bdea6bb6106c2f97b7c75d8490642f1efb6df5", size = 622356, upload-time = "2026-01-09T14:08:16.888Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/d6/02d1c505e1d3364230e5fa16d2b58c8f36a39c5efe8e99bc4d03d06fd0ca/huggingface_hub-1.3.2.tar.gz", hash = "sha256:15d7902e154f04174a0816d1e9594adcf15cdad57596920a5dc70fadb5d896c7", size = 624018, upload-time = "2026-01-14T13:57:39.635Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/90/fb/cb8fe5f71d5622427f20bcab9e06a696a5aaf21bfe7bd0a8a0c63c88abf5/huggingface_hub-1.3.1-py3-none-any.whl", hash = "sha256:efbc7f3153cb84e2bb69b62ed90985e21ecc9343d15647a419fc0ee4b85f0ac3", size = 533351, upload-time = "2026-01-09T14:08:14.519Z" },
+ { url = "https://files.pythonhosted.org/packages/88/1d/acd3ef8aabb7813c6ef2f91785d855583ac5cd7c3599e5c1a1a2ed1ec2e5/huggingface_hub-1.3.2-py3-none-any.whl", hash = "sha256:b552b9562a5532102a041fa31a6966bb9de95138fc7aa578bb3703198c25d1b6", size = 534504, upload-time = "2026-01-14T13:57:37.555Z" },
]
[[package]]
@@ -1456,30 +1456,30 @@ wheels = [
[[package]]
name = "nodejs-wheel"
-version = "24.12.0"
+version = "24.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodejs-wheel-binaries" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/30/73/0e8cd7c336f64d3b72c608bc7a7a5074cf6a9721c5d3630b8803f3176a3d/nodejs_wheel-24.12.0.tar.gz", hash = "sha256:edfaa3482bd21a2da03a9e7ebda7d4d738cdc864a2d9ddfe87760994a9644232", size = 2968, upload-time = "2025-12-11T21:12:26.103Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/37/f0/3345c6ec958c96eaa9d59355e59c0e93359aec54634f38bc4cd06baf23aa/nodejs_wheel-24.13.0.tar.gz", hash = "sha256:8c423cbf434b4c853ebac076d563b0899d3c6594ef0f99f6cd368ca4e3a28ca2", size = 2965, upload-time = "2026-01-14T11:05:32.811Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9b/5f/72f857250e54c9dacdfbf35f9d77eefdf954de0679f905e2fd03d8faf980/nodejs_wheel-24.12.0-py3-none-any.whl", hash = "sha256:0234fa0c46902d7efb858d41f5d055948cafa6a824812e9e8eeb64662d8963b6", size = 3988, upload-time = "2025-12-11T21:11:56.287Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/29/f259f6c5d31a0dae8257afd8dd7ab4b60a1b6e03fab793789e5bad480d83/nodejs_wheel-24.13.0-py3-none-any.whl", hash = "sha256:c0fc56a4677f55f7639f306a6381fb253d11ce24189c87a5489ea848f6e2bf24", size = 3986, upload-time = "2026-01-14T11:05:02.807Z" },
]
[[package]]
name = "nodejs-wheel-binaries"
-version = "24.12.0"
+version = "24.13.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b9/35/d806c2ca66072e36dc340ccdbeb2af7e4f1b5bcc33f1481f00ceed476708/nodejs_wheel_binaries-24.12.0.tar.gz", hash = "sha256:f1b50aa25375e264697dec04b232474906b997c2630c8f499f4caf3692938435", size = 8058, upload-time = "2025-12-11T21:12:26.856Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/f1/73182280e2c05f49a7c2c8dbd46144efe3f74f03f798fb90da67b4a93bbf/nodejs_wheel_binaries-24.13.0.tar.gz", hash = "sha256:766aed076e900061b83d3e76ad48bfec32a035ef0d41bd09c55e832eb93ef7a4", size = 8056, upload-time = "2026-01-14T11:05:33.653Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c3/3b/9d6f044319cd5b1e98f07c41e2465b58cadc1c9c04a74c891578f3be6cb5/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:7564ddea0a87eff34e9b3ef71764cc2a476a8f09a5cccfddc4691148b0a47338", size = 55125859, upload-time = "2025-12-11T21:11:58.132Z" },
- { url = "https://files.pythonhosted.org/packages/48/a5/f5722bf15c014e2f476d7c76bce3d55c341d19122d8a5d86454db32a61a4/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:8ff929c4669e64613ceb07f5bbd758d528c3563820c75d5de3249eb452c0c0ab", size = 55309035, upload-time = "2025-12-11T21:12:01.754Z" },
- { url = "https://files.pythonhosted.org/packages/a9/61/68d39a6f1b5df67805969fd2829ba7e80696c9af19537856ec912050a2be/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6ebacefa8891bc456ad3655e6bce0af7e20ba08662f79d9109986faeb703fd6f", size = 59661017, upload-time = "2025-12-11T21:12:05.268Z" },
- { url = "https://files.pythonhosted.org/packages/16/a1/31aad16f55a5e44ca7ea62d1367fc69f4b6e1dba67f58a0a41d0ed854540/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3292649a03682ccbfa47f7b04d3e4240e8c46ef04dc941b708f20e4e6a764f75", size = 60159770, upload-time = "2025-12-11T21:12:08.696Z" },
- { url = "https://files.pythonhosted.org/packages/c4/5e/b7c569aa1862690ca4d4daf3a64cafa1ea6ce667a9e3ae3918c56e127d9b/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7fb83df312955ea355ba7f8cbd7055c477249a131d3cb43b60e4aeb8f8c730b1", size = 61653561, upload-time = "2025-12-11T21:12:12.575Z" },
- { url = "https://files.pythonhosted.org/packages/71/87/567f58d7ba69ff0208be849b37be0f2c2e99c69e49334edd45ff44f00043/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2473c819448fedd7b036dde236b09f3c8bbf39fbbd0c1068790a0498800f498b", size = 62238331, upload-time = "2025-12-11T21:12:16.143Z" },
- { url = "https://files.pythonhosted.org/packages/6a/9d/c6492188ce8de90093c6755a4a63bb6b2b4efb17094cb4f9a9a49c73ed3b/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_amd64.whl", hash = "sha256:2090d59f75a68079fabc9b86b14df8238b9aecb9577966dc142ce2a23a32e9bb", size = 41342076, upload-time = "2025-12-11T21:12:20.618Z" },
- { url = "https://files.pythonhosted.org/packages/df/af/cd3290a647df567645353feed451ef4feaf5844496ced69c4dcb84295ff4/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_arm64.whl", hash = "sha256:d0c2273b667dd7e3f55e369c0085957b702144b1b04bfceb7ce2411e58333757", size = 39048104, upload-time = "2025-12-11T21:12:23.495Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/dc/4d7548aa74a5b446d093f03aff4fb236b570959d793f21c9c42ab6ad870a/nodejs_wheel_binaries-24.13.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:356654baa37bfd894e447e7e00268db403ea1d223863963459a0fbcaaa1d9d48", size = 55133268, upload-time = "2026-01-14T11:05:05.335Z" },
+ { url = "https://files.pythonhosted.org/packages/24/8a/8a4454d28339487240dd2232f42f1090e4a58544c581792d427f6239798c/nodejs_wheel_binaries-24.13.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:92fdef7376120e575f8b397789bafcb13bbd22a1b4d21b060d200b14910f22a5", size = 55314800, upload-time = "2026-01-14T11:05:09.121Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/fb/46c600fcc748bd13bc536a735f11532a003b14f5c4dfd6865f5911672175/nodejs_wheel_binaries-24.13.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3f619ac140e039ecd25f2f71d6e83ad1414017a24608531851b7c31dc140cdfd", size = 59666320, upload-time = "2026-01-14T11:05:12.369Z" },
+ { url = "https://files.pythonhosted.org/packages/85/47/d48f11fc5d1541ace5d806c62a45738a1db9ce33e85a06fe4cd3d9ce83f6/nodejs_wheel_binaries-24.13.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:dfb31ebc2c129538192ddb5bedd3d63d6de5d271437cd39ea26bf3fe229ba430", size = 60162447, upload-time = "2026-01-14T11:05:16.003Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/74/d285c579ae8157c925b577dde429543963b845e69cd006549e062d1cf5b6/nodejs_wheel_binaries-24.13.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fdd720d7b378d5bb9b2710457bbc880d4c4d1270a94f13fbe257198ac707f358", size = 61659994, upload-time = "2026-01-14T11:05:19.68Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/97/88b4254a2ff93ed2eaed725f77b7d3d2d8d7973bf134359ce786db894faf/nodejs_wheel_binaries-24.13.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ad6383613f3485a75b054647a09f1cd56d12380d7459184eebcf4a5d403f35c", size = 62244373, upload-time = "2026-01-14T11:05:23.987Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/c3/0e13a3da78f08cb58650971a6957ac7bfef84164b405176e53ab1e3584e2/nodejs_wheel_binaries-24.13.0-py2.py3-none-win_amd64.whl", hash = "sha256:605be4763e3ef427a3385a55da5a1bcf0a659aa2716eebbf23f332926d7e5f23", size = 41345528, upload-time = "2026-01-14T11:05:27.67Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/f1/0578d65b4e3dc572967fd702221ea1f42e1e60accfb6b0dd8d8f15410139/nodejs_wheel_binaries-24.13.0-py2.py3-none-win_arm64.whl", hash = "sha256:2e3431d869d6b2dbeef1d469ad0090babbdcc8baaa72c01dd3cc2c6121c96af5", size = 39054688, upload-time = "2026-01-14T11:05:30.739Z" },
]
[[package]]
@@ -1933,26 +1933,26 @@ wheels = [
[[package]]
name = "pyarrow"
-version = "22.0.0"
+version = "23.0.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/33/ffd9c3eb087fa41dd79c3cf20c4c0ae3cdb877c4f8e1107a446006344924/pyarrow-23.0.0.tar.gz", hash = "sha256:180e3150e7edfcd182d3d9afba72f7cf19839a497cc76555a8dce998a8f67615", size = 1167185, upload-time = "2026-01-18T16:19:42.218Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968, upload-time = "2025-10-24T10:03:31.21Z" },
- { url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085, upload-time = "2025-10-24T10:03:38.146Z" },
- { url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613, upload-time = "2025-10-24T10:03:46.516Z" },
- { url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059, upload-time = "2025-10-24T10:03:55.353Z" },
- { url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043, upload-time = "2025-10-24T10:04:05.408Z" },
- { url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505, upload-time = "2025-10-24T10:04:15.786Z" },
- { url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641, upload-time = "2025-10-24T10:04:22.57Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/2f/23e042a5aa99bcb15e794e14030e8d065e00827e846e53a66faec73c7cd6/pyarrow-23.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cbdc2bf5947aa4d462adcf8453cf04aee2f7932653cb67a27acd96e5e8528a67", size = 34281861, upload-time = "2026-01-18T16:13:34.332Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/65/1651933f504b335ec9cd8f99463718421eb08d883ed84f0abd2835a16cad/pyarrow-23.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:4d38c836930ce15cd31dce20114b21ba082da231c884bdc0a7b53e1477fe7f07", size = 35825067, upload-time = "2026-01-18T16:13:42.549Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ec/d6fceaec050c893f4e35c0556b77d4cc9973fcc24b0a358a5781b1234582/pyarrow-23.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4222ff8f76919ecf6c716175a0e5fddb5599faeed4c56d9ea41a2c42be4998b2", size = 44458539, upload-time = "2026-01-18T16:13:52.975Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/d9/369f134d652b21db62fe3ec1c5c2357e695f79eb67394b8a93f3a2b2cffa/pyarrow-23.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:87f06159cbe38125852657716889296c83c37b4d09a5e58f3d10245fd1f69795", size = 47535889, upload-time = "2026-01-18T16:14:03.693Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/95/f37b6a252fdbf247a67a78fb3f61a529fe0600e304c4d07741763d3522b1/pyarrow-23.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1675c374570d8b91ea6d4edd4608fa55951acd44e0c31bd146e091b4005de24f", size = 48157777, upload-time = "2026-01-18T16:14:12.483Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/ab/fb94923108c9c6415dab677cf1f066d3307798eafc03f9a65ab4abc61056/pyarrow-23.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:247374428fde4f668f138b04031a7e7077ba5fa0b5b1722fdf89a017bf0b7ee0", size = 50580441, upload-time = "2026-01-18T16:14:20.187Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/78/897ba6337b517fc8e914891e1bd918da1c4eb8e936a553e95862e67b80f6/pyarrow-23.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:de53b1bd3b88a2ee93c9af412c903e57e738c083be4f6392288294513cd8b2c1", size = 27530028, upload-time = "2026-01-18T16:14:27.353Z" },
]
[[package]]
name = "pyasn1"
-version = "0.6.1"
+version = "0.6.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
+ { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" },
]
[[package]]
@@ -2051,14 +2051,14 @@ wheels = [
[[package]]
name = "pydash"
-version = "8.0.5"
+version = "8.0.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/2f/24/91c037f47e434172c2112d65c00c84d475a6715425e3315ba2cbb7a87e66/pydash-8.0.5.tar.gz", hash = "sha256:7cc44ebfe5d362f4f5f06c74c8684143c5ac481376b059ff02570705523f9e2e", size = 164861, upload-time = "2025-01-17T16:08:50.562Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/75/c1/1c55272f49d761cec38ddb80be9817935b9c91ebd6a8988e10f532868d56/pydash-8.0.6.tar.gz", hash = "sha256:b2821547e9723f69cf3a986be4db64de41730be149b2641947ecd12e1e11025a", size = 164338, upload-time = "2026-01-17T16:42:56.576Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/86/e74c978800131c657fc5145f2c1c63e0cea01a49b6216f729cf77a2e1edf/pydash-8.0.5-py3-none-any.whl", hash = "sha256:b2625f8981862e19911daa07f80ed47b315ce20d9b5eb57aaf97aaf570c3892f", size = 102077, upload-time = "2025-01-17T16:08:47.91Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/b7/cc5e7974699db40014d58c7dd7c4ad4ffc244d36930dc9ec7d06ee67d7a9/pydash-8.0.6-py3-none-any.whl", hash = "sha256:ee70a81a5b292c007f28f03a4ee8e75c1f5d7576df5457b836ec7ab2839cc5d0", size = 101561, upload-time = "2026-01-17T16:42:55.448Z" },
]
[[package]]
@@ -2277,38 +2277,40 @@ wheels = [
[[package]]
name = "regex"
-version = "2025.11.3"
+version = "2026.1.15"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8a/d6/d788d52da01280a30a3f6268aef2aa71043bff359c618fea4c5b536654d5/regex-2025.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af", size = 488087, upload-time = "2025-11-03T21:30:47.317Z" },
- { url = "https://files.pythonhosted.org/packages/69/39/abec3bd688ec9bbea3562de0fd764ff802976185f5ff22807bf0a2697992/regex-2025.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313", size = 290544, upload-time = "2025-11-03T21:30:49.912Z" },
- { url = "https://files.pythonhosted.org/packages/39/b3/9a231475d5653e60002508f41205c61684bb2ffbf2401351ae2186897fc4/regex-2025.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56", size = 288408, upload-time = "2025-11-03T21:30:51.344Z" },
- { url = "https://files.pythonhosted.org/packages/c3/c5/1929a0491bd5ac2d1539a866768b88965fa8c405f3e16a8cef84313098d6/regex-2025.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28", size = 781584, upload-time = "2025-11-03T21:30:52.596Z" },
- { url = "https://files.pythonhosted.org/packages/ce/fd/16aa16cf5d497ef727ec966f74164fbe75d6516d3d58ac9aa989bc9cdaad/regex-2025.11.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7", size = 850733, upload-time = "2025-11-03T21:30:53.825Z" },
- { url = "https://files.pythonhosted.org/packages/e6/49/3294b988855a221cb6565189edf5dc43239957427df2d81d4a6b15244f64/regex-2025.11.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32", size = 898691, upload-time = "2025-11-03T21:30:55.575Z" },
- { url = "https://files.pythonhosted.org/packages/14/62/b56d29e70b03666193369bdbdedfdc23946dbe9f81dd78ce262c74d988ab/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391", size = 791662, upload-time = "2025-11-03T21:30:57.262Z" },
- { url = "https://files.pythonhosted.org/packages/15/fc/e4c31d061eced63fbf1ce9d853975f912c61a7d406ea14eda2dd355f48e7/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5", size = 782587, upload-time = "2025-11-03T21:30:58.788Z" },
- { url = "https://files.pythonhosted.org/packages/b2/bb/5e30c7394bcf63f0537121c23e796be67b55a8847c3956ae6068f4c70702/regex-2025.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7", size = 774709, upload-time = "2025-11-03T21:31:00.081Z" },
- { url = "https://files.pythonhosted.org/packages/c5/c4/fce773710af81b0cb37cb4ff0947e75d5d17dee304b93d940b87a67fc2f4/regex-2025.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313", size = 845773, upload-time = "2025-11-03T21:31:01.583Z" },
- { url = "https://files.pythonhosted.org/packages/7b/5e/9466a7ec4b8ec282077095c6eb50a12a389d2e036581134d4919e8ca518c/regex-2025.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9", size = 836164, upload-time = "2025-11-03T21:31:03.244Z" },
- { url = "https://files.pythonhosted.org/packages/95/18/82980a60e8ed1594eb3c89eb814fb276ef51b9af7caeab1340bfd8564af6/regex-2025.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5", size = 779832, upload-time = "2025-11-03T21:31:04.876Z" },
- { url = "https://files.pythonhosted.org/packages/03/cc/90ab0fdbe6dce064a42015433f9152710139fb04a8b81b4fb57a1cb63ffa/regex-2025.11.3-cp310-cp310-win32.whl", hash = "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec", size = 265802, upload-time = "2025-11-03T21:31:06.581Z" },
- { url = "https://files.pythonhosted.org/packages/34/9d/e9e8493a85f3b1ddc4a5014465f5c2b78c3ea1cbf238dcfde78956378041/regex-2025.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd", size = 277722, upload-time = "2025-11-03T21:31:08.144Z" },
- { url = "https://files.pythonhosted.org/packages/15/c4/b54b24f553966564506dbf873a3e080aef47b356a3b39b5d5aba992b50db/regex-2025.11.3-cp310-cp310-win_arm64.whl", hash = "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e", size = 270289, upload-time = "2025-11-03T21:31:10.267Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" },
+ { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" },
+ { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" },
+ { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" },
+ { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" },
+ { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" },
]
[[package]]
name = "reportlab"
-version = "4.4.7"
+version = "4.4.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "charset-normalizer" },
{ name = "pillow" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f8/a7/4600cb1cfc975a06552e8927844ddcb8fd90217e9a6068f5c7aa76c3f221/reportlab-4.4.7.tar.gz", hash = "sha256:41e8287af965e5996764933f3e75e7f363c3b6f252ba172f9429e81658d7b170", size = 3714000, upload-time = "2025-12-21T11:50:11.336Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1a/39/42cf24aee570a80e1903221ae3a92a2e34c324794a392eb036cbb6dc3839/reportlab-4.4.9.tar.gz", hash = "sha256:7cf487764294ee791a4781f5a157bebce262a666ae4bbb87786760a9676c9378", size = 3911246, upload-time = "2026-01-15T10:07:56.08Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e7/bf/a29507386366ab17306b187ad247dd78e4599be9032cb5f44c940f547fc0/reportlab-4.4.7-py3-none-any.whl", hash = "sha256:8fa05cbf468e0e76745caf2029a4770276edb3c8e86a0b71e0398926baf50673", size = 1954263, upload-time = "2025-12-21T11:50:08.93Z" },
+ { url = "https://files.pythonhosted.org/packages/17/77/546e50edfaba6a0e58e8ec5fdc4446510227cec9e8f40172b60941d5a633/reportlab-4.4.9-py3-none-any.whl", hash = "sha256:68e2d103ae8041a37714e8896ec9b79a1c1e911d68c3bd2ea17546568cf17bfd", size = 1954401, upload-time = "2026-01-15T09:27:59.133Z" },
]
[[package]]
@@ -2453,11 +2455,11 @@ wheels = [
[[package]]
name = "soupsieve"
-version = "2.8.1"
+version = "2.8.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" },
+ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
]
[[package]]
@@ -2474,15 +2476,15 @@ wheels = [
[[package]]
name = "sse-starlette"
-version = "3.1.2"
+version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "starlette" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303, upload-time = "2025-12-31T08:02:20.023Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484, upload-time = "2025-12-31T08:02:18.894Z" },
+ { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" },
]
[[package]]
@@ -2540,11 +2542,11 @@ wheels = [
[[package]]
name = "tomli"
-version = "2.3.0"
+version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
]
[[package]]
diff --git a/docs/core/models/byok.md b/docs/core/models/byok.md
index 88a0a86d..99545dc5 100644
--- a/docs/core/models/byok.md
+++ b/docs/core/models/byok.md
@@ -11,8 +11,6 @@ description: "Configure your own API keys to use various LLM providers with Eige
- **Direct access** to the latest models from each provider
- **Privacy** - your requests go directly to the provider
----
-
## OpenAI Configuration (Example)
### Step 1: Get Your API Key
@@ -23,70 +21,70 @@ description: "Configure your own API keys to use various LLM providers with Eige
### Step 2: Configure in Eigent
-1. Launch Eigent and go to **Settings** > **Models**
+1. Launch Eigent and go to **Settings** \> **Models**
2. Find the **OpenAI** card in the Custom Model section
+
+
3. Fill in the following fields:
-| Field | Value | Example |
-|-------|-------|---------|
-| **API Key** | Your OpenAI secret key | `sk-proj-xxxx...` |
-| **API Host** | OpenAI API endpoint | `https://api.openai.com/v1` |
-| **Model Type** | The model you want to use | `gpt-4o`, `gpt-4o-mini` |
+| Field | Value | Example |
+| -------------- | ------------------------- | --------------------------- |
+| **API Key** | Your OpenAI secret key | `sk-proj-xxxx...` |
+| **API Host** | OpenAI API endpoint | `https://api.openai.com/v1` |
+| **Model Type** | The model you want to use | `gpt-4o`, `gpt-4o-mini` |
4. Click **Save** to validate and store your configuration
5. Click **Set as Default** to use this provider for your agents
----
-
## Configuration Fields
-| Field | Description | Required |
-|-------|-------------|----------|
-| **API Key** | Your authentication key from the provider | Yes |
-| **API Host** | The API endpoint URL | Yes (pre-filled for most providers) |
-| **Model Type** | The specific model variant to use | Yes |
-| **External Config** | Provider-specific settings (e.g., Azure deployment name) | Only for certain providers |
+| Field | Description | Required |
+| ------------------- | -------------------------------------------------------- | ----------------------------------- |
+| **API Key** | Your authentication key from the provider | Yes |
+| **API Host** | The API endpoint URL | Yes (pre-filled for most providers) |
+| **Model Type** | The specific model variant to use | Yes |
+| **External Config** | Provider-specific settings (e.g., Azure deployment name) | Only for certain providers |
### Azure-Specific Fields
-| Field | Description | Example |
-|-------|-------------|---------|
-| **API Version** | Azure OpenAI API version | `2024-02-15-preview` |
+| Field | Description | Example |
+| ------------------- | -------------------------- | -------------------- |
+| **API Version** | Azure OpenAI API version | `2024-02-15-preview` |
| **Deployment Name** | Your Azure deployment name | `my-gpt4-deployment` |
----
-
## Common Errors
When saving your configuration, Eigent validates your API key and model. Here are the errors you may encounter:
-| Error | Cause | Solution |
-|-------|-------|----------|
-| **Invalid key. Validation failed.** | API key is incorrect, expired, or malformed | Double-check your API key. Regenerate a new key if needed. |
-| **Invalid model name. Validation failed.** | The specified model does not exist or is not available for your account | Verify the model name is correct. Check if you have access to that model. |
-| **You exceeded your current quota** | API quota exhausted or billing issue | Check your provider's billing dashboard. Add credits or upgrade your plan. |
-
----
+| Error | Cause | Solution |
+| ------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------------- |
+| **Invalid key. Validation failed.** | API key is incorrect, expired, or malformed | Double-check your API key. Regenerate a new key if needed. |
+| **Invalid model name. Validation failed.** | The specified model does not exist or is not available for your account | Verify the model name is correct. Check if you have access to that model. |
+| **You exceeded your current quota** | API quota exhausted or billing issue | Check your provider's billing dashboard. Add credits or upgrade your plan. |
## Supported Providers
Eigent supports the following BYOK providers:
-| Provider | Default API Host | Official Documentation |
-|----------|------------------|------------------------|
-| **OpenAI** | `https://api.openai.com/v1` | [OpenAI API Docs](https://platform.openai.com/docs/api-reference) |
-| **Anthropic** | `https://api.anthropic.com/v1/` | [Anthropic API Docs](https://docs.anthropic.com/en/api/getting-started) |
-| **Google Gemini** | `https://generativelanguage.googleapis.com/v1beta/openai/` | [Gemini API Docs](https://ai.google.dev/gemini-api/docs) |
-| **OpenRouter** | `https://openrouter.ai/api/v1` | [OpenRouter Docs](https://openrouter.ai/docs) |
-| **Qwen (Alibaba)** | `https://dashscope.aliyuncs.com/compatible-mode/v1` | [Qwen API Docs](https://help.aliyun.com/zh/dashscope/developer-reference/api-details) |
-| **DeepSeek** | `https://api.deepseek.com` | [DeepSeek API Docs](https://platform.deepseek.com/api-docs) |
-| **Minimax** | `https://api.minimax.io/v1` | [Minimax API Docs](https://platform.minimaxi.com/document/Announcement) |
-| **Z.ai** | `https://api.z.ai/api/coding/paas/v4/` | [Z.ai Platform](https://z.ai) |
-| **Azure OpenAI** | *(user-provided)* | [Azure OpenAI Docs](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference) |
-| **AWS Bedrock** | *(user-provided)* | [AWS Bedrock Docs](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) |
-| **OpenAI Compatible** | *(user-provided)* | For custom endpoints (e.g., xAI, local servers) |
-
----
+| Provider | Default API Host | Official Documentation |
+| --------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
+| **OpenAI** | `https://api.openai.com/v1` | [OpenAI API Docs](https://platform.openai.com/docs/api-reference) |
+| **Anthropic** | `https://api.anthropic.com/v1/` | [Anthropic API Docs](https://docs.anthropic.com/en/api/getting-started) |
+| **Google Gemini** | `https://generativelanguage.googleapis.com/v1beta/openai/` | [Gemini API Docs](https://ai.google.dev/gemini-api/docs) |
+| **OpenRouter** | `https://openrouter.ai/api/v1` | [OpenRouter Docs](https://openrouter.ai/docs) |
+| **Qwen (Alibaba)** | `https://dashscope.aliyuncs.com/compatible-mode/v1` | [Qwen API Docs](https://help.aliyun.com/zh/dashscope/developer-reference/api-details) |
+| **DeepSeek** | `https://api.deepseek.com` | [DeepSeek API Docs](https://platform.deepseek.com/api-docs) |
+| **Minimax** | `https://api.minimax.io/v1` | [Minimax API Docs](https://platform.minimaxi.com/document/Announcement) |
+| **Z.ai** | `https://api.z.ai/api/coding/paas/v4/` | [Z.ai Platform](https://z.ai) |
+| **Azure OpenAI** | _(user-provided)_ | [Azure OpenAI Docs](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference) |
+| **AWS Bedrock** | _(user-provided)_ | [AWS Bedrock Docs](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) |
+| **OpenAI Compatible** | _(user-provided)_ | For custom endpoints (e.g., xAI, local servers) |
## Tips
diff --git a/docs/core/models/local-model.md b/docs/core/models/local-model.md
index b417745b..dcc7592a 100644
--- a/docs/core/models/local-model.md
+++ b/docs/core/models/local-model.md
@@ -1,7 +1,6 @@
---
title: "Models (Local Model)"
description: "Configure and deploy your preferred LLM models with Eigent."
-icon: "server"
---
## **Self-Host Model**
diff --git a/docs/docs.json b/docs/docs.json
index b6bd714b..b475af81 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -65,10 +65,10 @@
"icon": "brain",
"expanded": true,
"pages": [
+ "/core/models/byok",
+ "/core/models/local-model",
"/core/models/gemini",
- "/core/models/minimax",
- "/core/models/local-model"
- "/core/models/byok"
+ "/core/models/minimax"
]
},
"/core/tools",
diff --git a/docs/images/Screenshot2026-01-20at18.12.10.png b/docs/images/Screenshot2026-01-20at18.12.10.png
new file mode 100644
index 00000000..3ec99143
Binary files /dev/null and b/docs/images/Screenshot2026-01-20at18.12.10.png differ
diff --git a/docs/images/Screenshot2026-01-20at18.13.45.png b/docs/images/Screenshot2026-01-20at18.13.45.png
new file mode 100644
index 00000000..0dab1326
Binary files /dev/null and b/docs/images/Screenshot2026-01-20at18.13.45.png differ
diff --git a/docs/images/Screenshot2026-01-20at18.14.03.png b/docs/images/Screenshot2026-01-20at18.14.03.png
new file mode 100644
index 00000000..fc32b704
Binary files /dev/null and b/docs/images/Screenshot2026-01-20at18.14.03.png differ
diff --git a/electron-builder.json b/electron-builder.json
index 61000693..4028cd46 100644
--- a/electron-builder.json
+++ b/electron-builder.json
@@ -29,7 +29,13 @@
{
"from": "resources/prebuilt",
"to": "prebuilt",
- "filter": ["**/*", "!cache/**/*", "!**/.npm-cache/**/*"]
+ "filter": [
+ "**/*",
+ "!cache/**/*",
+ "!**/.npm-cache/**/*",
+ "!uv_python/**/*.pyc",
+ "!uv_python/**/__pycache__"
+ ]
}
],
"protocols": [
@@ -67,7 +73,6 @@
}
},
"win": {
- "certificateFile": null,
"icon": "build/icon.ico",
"artifactName": "${productName}.Setup.${version}.exe",
"target": [
@@ -77,6 +82,11 @@
}
]
},
+ "linux": {
+ "icon": "build/icon.png",
+ "target": ["AppImage"],
+ "category": "Development"
+ },
"nsis": {
"oneClick": false,
"perMachine": false,
diff --git a/electron/main/fileReader.ts b/electron/main/fileReader.ts
index 91a06977..680a812d 100644
--- a/electron/main/fileReader.ts
+++ b/electron/main/fileReader.ts
@@ -514,6 +514,15 @@ export class FileReader {
})
}
+ // Folders to hide in the Agent Folder view
+ private readonly hiddenFolders = [
+ 'browser_agent',
+ 'developer_agent',
+ 'document_agent',
+ 'multi_modal_agent',
+ 'terminal_logs'
+ ];
+
private getFilesRecursive(dirPath: string, basePath: string): FileInfo[] {
try {
const files = fs.readdirSync(dirPath);
@@ -521,6 +530,8 @@ export class FileReader {
for (const file of files) {
if (file.startsWith(".")) continue;
+ // Skip hidden folders
+ if (this.hiddenFolders.includes(file)) continue;
const filePath = path.join(dirPath, file);
const stats = fs.statSync(filePath);
diff --git a/electron/main/index.ts b/electron/main/index.ts
index 611d5fcd..7b3c1d45 100644
--- a/electron/main/index.ts
+++ b/electron/main/index.ts
@@ -34,12 +34,9 @@ import { zipFolder } from './utils/log';
import mime from 'mime';
import axios from 'axios';
import FormData from 'form-data';
-import {
- checkAndInstallDepsOnUpdate,
- PromiseReturnType,
- getInstallationStatus,
-} from './install-deps';
-import { isBinaryExists, getBackendPath, getVenvPath } from './utils/process';
+import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps'
+import { isBinaryExists, getBackendPath, getVenvPath } from './utils/process'
+import { setVibrancy, setRoundedCorners, setTransparentTitlebar } from './native/macos-window'
const userData = app.getPath('userData');
@@ -112,6 +109,32 @@ app.commandLine.appendSwitch('max_old_space_size', '4096');
app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction');
app.commandLine.appendSwitch('renderer-process-limit', '8');
+// ==================== Anti-fingerprint settings ====================
+// Disable automation controlled indicator to avoid detection
+app.commandLine.appendSwitch(
+ 'disable-blink-features',
+ 'AutomationControlled'
+);
+
+// Override User Agent to remove Electron/eigent identifiers
+// Dynamically generate User Agent based on actual platform and Chrome version
+const getPlatformUA = () => {
+ // Use actual Chrome version from Electron instead of hardcoded value
+ const chromeVersion = process.versions.chrome || '131.0.0.0';
+ switch (process.platform) {
+ case 'darwin':
+ return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
+ case 'win32':
+ return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
+ case 'linux':
+ return `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
+ default:
+ return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
+ }
+};
+const normalUserAgent = getPlatformUA();
+app.userAgentFallback = normalUserAgent;
+
// ==================== protocol privileges ====================
// Register custom protocol privileges before app ready
protocol.registerSchemesAsPrivileged([
@@ -1253,9 +1276,7 @@ async function createWindow() {
minHeight: 650,
frame: false,
transparent: true,
- vibrancy: 'sidebar',
- visualEffectState: 'active',
- backgroundColor: '#f5f5f580',
+ backgroundColor: '#00000000',
titleBarStyle: isMac ? 'hidden' : undefined,
trafficLightPosition: isMac ? { x: 10, y: 10 } : undefined,
icon: path.join(VITE_PUBLIC, 'favicon.ico'),
@@ -1273,6 +1294,28 @@ async function createWindow() {
},
});
+ // Apply native macOS effects
+ if (process.platform === 'darwin') {
+ win.once('ready-to-show', () => {
+ if (win && !win.isDestroyed()) {
+ try {
+ // Apply vibrancy with HUDWindow material (or others like 'Sidebar', 'UnderWindowBackground')
+ setVibrancy(win, 'HUDWindow');
+
+ // Apply rounded corners
+ setRoundedCorners(win, 20);
+
+ // Make titlebar transparent
+ setTransparentTitlebar(win);
+
+ log.info('[MacOS] Applied native visual effects');
+ } catch (error) {
+ log.error('[MacOS] Failed to apply native visual effects:', error);
+ }
+ }
+ });
+ }
+
// Main window now uses default userData directly with partition 'persist:main_window'
// No migration needed - data is already persistent
@@ -1827,6 +1870,15 @@ app.whenReady().then(async () => {
}
}
+ // ==================== Anti-fingerprint: Set User Agent for all sessions ====================
+ // Use the same dynamic User Agent as app.userAgentFallback
+ session.defaultSession.setUserAgent(normalUserAgent);
+ // Also set for the user_login partition used by webviews
+ session.fromPartition('persist:user_login').setUserAgent(normalUserAgent);
+ // And for main_window partition
+ session.fromPartition('persist:main_window').setUserAgent(normalUserAgent);
+ log.info('[ANTI-FINGERPRINT] User Agent set for all sessions');
+
// ==================== download handle ====================
session.defaultSession.on('will-download', (event, item, webContents) => {
item.once('done', (event, state) => {
diff --git a/electron/main/init.ts b/electron/main/init.ts
index 1ee74eaa..2b0ace8c 100644
--- a/electron/main/init.ts
+++ b/electron/main/init.ts
@@ -1,4 +1,4 @@
-import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, getUvEnv, isBinaryExists, runInstallScript, killProcessByName } from "./utils/process";
+import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, getUvEnv, isBinaryExists, runInstallScript, killProcessByName, getPrebuiltPythonDir, getPrebuiltVenvPath } from "./utils/process";
import { spawn, exec } from 'child_process'
import log from 'electron-log'
import fs from 'fs'
@@ -225,21 +225,34 @@ export async function startBackend(setPort?: (port: number) => void): Promise {
});
};
+/**
+ * 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');
+ // Marker file to indicate packages were installed successfully
+ const installedMarker = path.join(terminalVenvPath, '.packages_installed');
+
+ // Check if terminal base venv already exists and packages are installed
+ if (fs.existsSync(pythonPath) && fs.existsSync(installedMarker)) {
+ log.info('[DEPS INSTALL] Terminal base venv already exists with packages, skipping creation');
+ return { message: 'Terminal base venv already exists', success: true };
+ }
+
+ // If python exists but marker doesn't, packages may not be installed - need to reinstall
+ const needsPackageInstall = fs.existsSync(pythonPath) && !fs.existsSync(installedMarker);
+
+ if (needsPackageInstall) {
+ log.info('[DEPS INSTALL] Terminal venv exists but packages not installed, installing packages...');
+ } else {
+ log.info('[DEPS INSTALL] Creating terminal base venv...');
+ }
+ safeMainWindowSend('install-dependencies-log', {
+ type: 'stdout',
+ data: needsPackageInstall
+ ? 'Installing missing packages in terminal environment...\n'
+ : 'Creating terminal base environment...\n',
+ });
+
+ try {
+ // Create the venv using uv (skip if only need package install)
+ if (!needsPackageInstall) {
+ 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);
+ });
+
+ // Create marker file to indicate successful installation
+ fs.writeFileSync(installedMarker, new Date().toISOString());
+ 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 +1025,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 +1064,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/native/macos-window.ts b/electron/main/native/macos-window.ts
new file mode 100644
index 00000000..7a81e01e
--- /dev/null
+++ b/electron/main/native/macos-window.ts
@@ -0,0 +1,171 @@
+import { BrowserWindow } from 'electron';
+import koffi from 'koffi';
+import os from 'os';
+
+// NSVisualEffectView material constants (enum values)
+export const NSVisualEffectMaterial = {
+ Titlebar: 3,
+ Selection: 4,
+ Menu: 5,
+ Popover: 6,
+ Sidebar: 7,
+ HeaderView: 10,
+ Sheet: 11,
+ WindowBackground: 12,
+ HUDWindow: 13,
+ FullScreenUI: 15,
+ ToolTip: 17,
+ ContentBackground: 18,
+ UnderWindowBackground: 21,
+ UnderPageBackground: 22
+} as const;
+
+export type MaterialType = keyof typeof NSVisualEffectMaterial;
+
+// Interface for our module functions
+interface MacWindowUtils {
+ setVibrancy: (window: BrowserWindow, material?: MaterialType) => void;
+ setRoundedCorners: (window: BrowserWindow, radius?: number) => void;
+ setTransparentTitlebar: (window: BrowserWindow) => void;
+}
+
+let utils: MacWindowUtils;
+
+if (os.platform() === 'darwin') {
+ try {
+ const objc = koffi.load('libobjc.A.dylib');
+
+ // Types
+ const Ptr = 'size_t';
+
+ const objc_getClass = objc.func('objc_getClass', Ptr, ['string']);
+ const sel_registerName = objc.func('sel_registerName', Ptr, ['string']);
+ const objc_msgSend = objc.func('objc_msgSend', Ptr, [Ptr, Ptr]);
+ const objc_msgSend_long = objc.func('objc_msgSend', Ptr, [Ptr, Ptr, 'long']);
+ const objc_msgSend_double = objc.func('objc_msgSend', Ptr, [Ptr, Ptr, 'double']);
+ const objc_msgSend_bool = objc.func('objc_msgSend', Ptr, [Ptr, Ptr, 'bool']);
+
+ const NSRect = koffi.struct('NSRect', {
+ x: 'double',
+ y: 'double',
+ width: 'double',
+ height: 'double'
+ });
+
+ const NSVisualEffectBlendingMode = {
+ BehindWindow: 0,
+ WithinWindow: 1
+ };
+
+ utils = {
+ setVibrancy: (window: BrowserWindow, material: MaterialType = 'HUDWindow') => {
+ try {
+ const windowHandle = window.getNativeWindowHandle();
+ if (windowHandle.length === 0) return;
+
+ // Electron calls valid native handle returns the NSView (BridgedContentView) on macOS
+ const nsViewPtr = windowHandle.readBigUInt64LE();
+ if (!nsViewPtr) return;
+
+ // Selectors
+ const selAlloc = sel_registerName('alloc');
+ const selInit = sel_registerName('init');
+ const selSetMaterial = sel_registerName('setMaterial:');
+ const selSetBlendingMode = sel_registerName('setBlendingMode:');
+ const selSetState = sel_registerName('setState:');
+ const selSetAutoresizingMask = sel_registerName('setAutoresizingMask:');
+ const selSetFrame = sel_registerName('setFrame:');
+ const selAddSubview = sel_registerName('addSubview:positioned:relativeTo:');
+
+ const NSVisualEffectViewClass = objc_getClass('NSVisualEffectView');
+ if (!NSVisualEffectViewClass) return;
+
+ // Allocation
+ const visualEffectView = objc_msgSend(NSVisualEffectViewClass, selAlloc);
+ objc_msgSend(visualEffectView, selInit);
+
+ const materialValue = NSVisualEffectMaterial[material] || NSVisualEffectMaterial.HUDWindow;
+
+ // Configuration
+ objc_msgSend_long(visualEffectView, selSetMaterial, materialValue);
+ objc_msgSend_long(visualEffectView, selSetBlendingMode, NSVisualEffectBlendingMode.BehindWindow);
+ objc_msgSend_long(visualEffectView, selSetState, 1);
+ objc_msgSend_long(visualEffectView, selSetAutoresizingMask, 18);
+
+ // Frame
+ const bounds = window.getBounds();
+ const viewFrame = { x: 0, y: 0, width: bounds.width, height: bounds.height };
+
+ const objc_msgSend_frame = objc.func('objc_msgSend', 'void', [Ptr, Ptr, NSRect]);
+ objc_msgSend_frame(visualEffectView, selSetFrame, viewFrame);
+
+ // Add Subview to the CONTENT VIEW (which we already have as nsViewPtr)
+ const objc_msgSend_positioned = objc.func('objc_msgSend', 'void', [Ptr, Ptr, Ptr, 'long', Ptr]);
+ objc_msgSend_positioned(nsViewPtr, selAddSubview, visualEffectView, -1, 0); // -1 = NSWindowBelow
+
+ console.log(`[MacOS] Vibrancy applied successfully`);
+ } catch (error) {
+ console.error('[MacOS] Error applying vibrancy:', error);
+ }
+ },
+
+ setRoundedCorners: (window: BrowserWindow, radius = 20) => {
+ try {
+ const windowHandle = window.getNativeWindowHandle();
+ const nsViewPtr = windowHandle.readBigUInt64LE();
+
+ const selLayer = sel_registerName('layer');
+ const selSetWantsLayer = sel_registerName('setWantsLayer:');
+ const selSetCornerRadius = sel_registerName('setCornerRadius:');
+ const selSetMasksToBounds = sel_registerName('setMasksToBounds:');
+
+ // Ensure layer-backing
+ objc_msgSend_bool(nsViewPtr, selSetWantsLayer, true);
+
+ // Get layer
+ const nsLayer = objc_msgSend(nsViewPtr, selLayer);
+ if (!nsLayer) return console.error('[MacOS] Failed to get layer');
+
+ // Apply Corner Radius
+ objc_msgSend_double(nsLayer, selSetCornerRadius, radius);
+ objc_msgSend_bool(nsLayer, selSetMasksToBounds, true);
+
+ console.log(`[MacOS] Rounded corners applied: ${radius}`);
+ } catch (error) {
+ console.error('[MacOS] Error applying rounded corners:', error);
+ }
+ },
+
+ setTransparentTitlebar: (window: BrowserWindow) => {
+ try {
+ const windowHandle = window.getNativeWindowHandle();
+ const nsViewPtr = windowHandle.readBigUInt64LE();
+
+ // We have the View, we need the Window
+ const selWindow = sel_registerName('window');
+ const nsWindowPtr = objc_msgSend(nsViewPtr, selWindow);
+
+ if (!nsWindowPtr) return console.error('[MacOS] Failed to get NSWindow from NSView');
+
+ const selSetTitlebarAppearsTransparent = sel_registerName('setTitlebarAppearsTransparent:');
+ objc_msgSend_bool(nsWindowPtr, selSetTitlebarAppearsTransparent, true);
+
+ console.log('[MacOS] Transparent titlebar applied');
+ } catch (error) {
+ console.error('[MacOS] Error setting transparent titlebar:', error);
+ }
+ }
+ };
+ } catch (e) {
+ console.error('[MacOS] Failed to load native libraries:', e);
+ utils = { setVibrancy: () => { }, setRoundedCorners: () => { }, setTransparentTitlebar: () => { } };
+ }
+} else {
+ utils = {
+ setVibrancy: () => { },
+ setRoundedCorners: () => { },
+ setTransparentTitlebar: () => { }
+ };
+}
+
+export const { setVibrancy, setRoundedCorners, setTransparentTitlebar } = utils;
diff --git a/electron/main/utils/process.ts b/electron/main/utils/process.ts
index c8647e68..ec620a45 100644
--- a/electron/main/utils/process.ts
+++ b/electron/main/utils/process.ts
@@ -149,8 +149,21 @@ export function getPrebuiltVenvPath(): string | null {
if (fs.existsSync(prebuiltVenvPath)) {
const pyvenvCfg = path.join(prebuiltVenvPath, 'pyvenv.cfg');
if (fs.existsSync(pyvenvCfg)) {
- log.info(`Using prebuilt venv: ${prebuiltVenvPath}`);
- return prebuiltVenvPath;
+ // Verify Python executable exists (Windows: Scripts/python.exe, Unix: bin/python)
+ const isWindows = process.platform === 'win32';
+ const pythonExePath = isWindows
+ ? path.join(prebuiltVenvPath, 'Scripts', 'python.exe')
+ : path.join(prebuiltVenvPath, 'bin', 'python');
+
+ if (fs.existsSync(pythonExePath)) {
+ log.info(`Using prebuilt venv: ${prebuiltVenvPath}`);
+ return prebuiltVenvPath;
+ } else {
+ log.warn(
+ `Prebuilt venv found but Python executable missing at: ${pythonExePath}. ` +
+ `Falling back to user venv.`
+ );
+ }
}
}
return null;
@@ -185,6 +198,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 +243,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
}
}
}
@@ -224,6 +285,23 @@ export async function isBinaryExists(name: string): Promise {
return fs.existsSync(cmd);
}
+/**
+ * Get path to prebuilt Python installation (if available in packaged app)
+ */
+export function getPrebuiltPythonDir(): string | null {
+ if (!app.isPackaged) {
+ return null;
+ }
+
+ const prebuiltPythonDir = path.join(process.resourcesPath, 'prebuilt', 'uv_python');
+ if (fs.existsSync(prebuiltPythonDir)) {
+ log.info(`Using prebuilt Python: ${prebuiltPythonDir}`);
+ return prebuiltPythonDir;
+ }
+
+ return null;
+}
+
/**
* Get unified UV environment variables for consistent Python environment management.
* This ensures both installation and runtime use the same paths.
@@ -231,8 +309,12 @@ export async function isBinaryExists(name: string): Promise {
* @returns Environment variables for UV commands
*/
export function getUvEnv(version: string): Record {
+ // Use prebuilt Python if available (packaged app)
+ const prebuiltPython = getPrebuiltPythonDir();
+ const pythonInstallDir = prebuiltPython || getCachePath('uv_python');
+
return {
- UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
+ UV_PYTHON_INSTALL_DIR: pythonInstallDir,
UV_TOOL_DIR: getCachePath('uv_tool'),
UV_PROJECT_ENVIRONMENT: getVenvPath(version),
UV_HTTP_TIMEOUT: '300',
diff --git a/electron/main/webview.ts b/electron/main/webview.ts
index 8fae65a6..96dd86e1 100644
--- a/electron/main/webview.ts
+++ b/electron/main/webview.ts
@@ -72,13 +72,108 @@ export class WebViewManager {
backgroundThrottling: true,
offscreen: false,
sandbox: true,
- disableBlinkFeatures: 'Accelerated2dCanvas',
+ disableBlinkFeatures: 'Accelerated2dCanvas,AutomationControlled',
enableBlinkFeatures: 'IdleDetection',
autoplayPolicy: 'document-user-activation-required',
},
})
view.webContents.on('did-finish-load', () => {
+ // Inject stealth script to avoid bot detection
view.webContents.executeJavaScript(`
+ // Save original values before overriding to maintain consistency
+ const originalLanguages = navigator.languages ? [...navigator.languages] : ['en-US', 'en'];
+ const originalHardwareConcurrency = navigator.hardwareConcurrency || 8;
+ const originalDeviceMemory = navigator.deviceMemory || 8;
+
+ // Hide webdriver property
+ Object.defineProperty(navigator, 'webdriver', {
+ get: () => undefined,
+ configurable: true
+ });
+
+ // Override plugins with proper PluginArray-like behavior
+ Object.defineProperty(navigator, 'plugins', {
+ get: () => {
+ const plugins = {
+ length: 3,
+ 0: { name: 'Chrome PDF Plugin', description: 'Portable Document Format', filename: 'internal-pdf-viewer' },
+ 1: { name: 'Chrome PDF Viewer', description: '', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
+ 2: { name: 'Native Client', description: '', filename: 'internal-nacl-plugin' },
+ item: function(index) { return this[index] || null; },
+ namedItem: function(name) {
+ for (let i = 0; i < this.length; i++) {
+ if (this[i].name === name) return this[i];
+ }
+ return null;
+ },
+ refresh: function() {},
+ [Symbol.iterator]: function* () {
+ for (let i = 0; i < this.length; i++) {
+ yield this[i];
+ }
+ }
+ };
+ return plugins;
+ },
+ configurable: true
+ });
+
+ // Use original system languages for consistency with other browser data
+ Object.defineProperty(navigator, 'languages', {
+ get: () => originalLanguages,
+ configurable: true
+ });
+
+ // Use original hardwareConcurrency, clamped to common range (4-16) to avoid extreme fingerprints
+ Object.defineProperty(navigator, 'hardwareConcurrency', {
+ get: () => Math.min(Math.max(originalHardwareConcurrency, 4), 16),
+ configurable: true
+ });
+
+ // Use original deviceMemory, clamped to common range (4-16) to avoid extreme fingerprints
+ Object.defineProperty(navigator, 'deviceMemory', {
+ get: () => Math.min(Math.max(originalDeviceMemory, 4), 16),
+ configurable: true
+ });
+
+ // Fix WebGL vendor/renderer for both WebGL and WebGL2
+ const getParameter = WebGLRenderingContext.prototype.getParameter;
+ WebGLRenderingContext.prototype.getParameter = function(parameter) {
+ if (parameter === 37445) return 'Intel Inc.';
+ if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100';
+ return getParameter.call(this, parameter);
+ };
+
+ // Also patch WebGL2RenderingContext
+ if (typeof WebGL2RenderingContext !== 'undefined') {
+ const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
+ WebGL2RenderingContext.prototype.getParameter = function(parameter) {
+ if (parameter === 37445) return 'Intel Inc.';
+ if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100';
+ return getParameter2.call(this, parameter);
+ };
+ }
+
+ // Override chrome runtime - real Chrome has window.chrome but runtime is undefined
+ if (!window.chrome) {
+ window.chrome = {};
+ }
+ // In real Chrome, runtime exists but is undefined outside extensions
+ // Don't set it to an object, that's detectable
+
+ // Hide automation variables
+ const automationVars = ['__webdriver_evaluate', '__selenium_evaluate', '__webdriver_script_fn',
+ '__driver_evaluate', '__fxdriver_evaluate', '__driver_unwrapped', 'domAutomation', 'domAutomationController'];
+ automationVars.forEach(v => {
+ Object.defineProperty(window, v, {
+ get: () => undefined,
+ set: () => {},
+ configurable: true,
+ enumerable: false
+ });
+ });
+
+ // Mouse event handler
window.addEventListener('mousedown', (e) => {
if (!(e.target instanceof HTMLButtonElement || e.target instanceof HTMLInputElement)) {
e.preventDefault();
diff --git a/package.json b/package.json
index 1adb63d4..b1ffee24 100644
--- a/package.json
+++ b/package.json
@@ -23,14 +23,17 @@
"build:mac": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc && vite build && electron-builder --mac",
"build:mac:test": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc && vite build && electron-builder --mac && npm run test-signing",
"build:win": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --win",
- "build:all": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --mac --win",
+ "build:linux": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc && vite build && electron-builder --linux",
+ "build:all": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --mac --win --linux",
"preview": "vite preview",
"pretest": "vite build --mode=test",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "vitest run --config vitest.config.ts",
"test:coverage": "vitest run --coverage",
- "type-check": "tsc --noEmit"
+ "type-check": "tsc --noEmit",
+ "storybook": "storybook dev -p 6006",
+ "build-storybook": "storybook build -o storybook-static"
},
"dependencies": {
"@electron/notarize": "^2.5.0",
@@ -67,11 +70,12 @@
"csv-parser": "^3.2.0",
"dompurify": "^3.2.7",
"electron-log": "^5.4.0",
- "electron-updater": "^6.3.9",
+ "electron-updater": "^6.7.3",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.17.0",
"gsap": "^3.13.0",
+ "koffi": "^2.14.1",
"lodash-es": "^4.17.21",
"lottie-web": "^5.13.0",
"lucide-react": "^0.509.0",
@@ -114,11 +118,11 @@
"@vitejs/plugin-react": "^4.3.3",
"@vitest/coverage-v8": "^2.1.9",
"autoprefixer": "^10.4.20",
- "electron": "^33.2.0",
- "electron-builder": "^24.13.3",
+ "electron": "^33.4.11",
+ "electron-builder": "^26.4.0",
"electron-devtools-installer": "^4.0.0",
"i18next": "^25.4.2",
- "jsdom": "^26.1.0",
+ "jsdom": "^27.4.0",
"postcss": "^8.4.49",
"postcss-import": "^16.1.0",
"react": "^18.3.1",
@@ -130,12 +134,22 @@
"vite": "^5.4.11",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6",
- "vitest": "^2.1.5"
+ "vitest": "^2.1.5",
+ "storybook": "^10.1.11",
+ "@storybook/react-vite": "^10.1.11",
+ "@storybook/addon-a11y": "^10.1.11",
+ "@storybook/addon-docs": "^10.1.11"
+ },
+ "overrides": {
+ "glob": "^10.4.5"
},
"pnpm": {
- "neverBuiltDependencies": []
+ "neverBuiltDependencies": [],
+ "overrides": {
+ "glob": "^10.4.5"
+ }
},
"engines": {
- "node": ">=18.0.0 <23.0.0"
+ "node": ">=20.0.0 <23.0.0"
}
}
diff --git a/scripts/clean-symlinks.js b/scripts/clean-symlinks.js
index e011ba45..716a4c40 100644
--- a/scripts/clean-symlinks.js
+++ b/scripts/clean-symlinks.js
@@ -46,7 +46,7 @@ function isValidSymlink(symlinkPath, bundleRoot) {
}
/**
- * Fix Python symlinks in venv/bin
+ * Fix Python symlinks in venv/bin (Unix) or venv/Scripts (Windows)
* Remove symlinks that point outside the bundle (to cache directory)
*/
function fixPythonSymlinks(venvBinDir, bundleRoot) {
@@ -55,7 +55,10 @@ function fixPythonSymlinks(venvBinDir, bundleRoot) {
}
const bundlePath = path.resolve(bundleRoot);
- const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12'];
+ const isWindows = process.platform === 'win32';
+ const pythonNames = isWindows
+ ? ['python.exe', 'python3.exe', 'python3.10.exe', 'python3.11.exe', 'python3.12.exe']
+ : ['python', 'python3', 'python3.10', 'python3.11', 'python3.12'];
for (const pythonName of pythonNames) {
const pythonSymlink = path.join(venvBinDir, pythonName);
@@ -127,7 +130,8 @@ function main() {
console.log('🧹 Cleaning invalid symbolic links...');
const bundleRoot = path.join(projectRoot, 'resources', 'prebuilt');
- const venvBinDir = path.join(bundleRoot, 'venv', 'bin');
+ const isWindows = process.platform === 'win32';
+ const venvBinDir = path.join(bundleRoot, 'venv', isWindows ? 'Scripts' : 'bin');
// First, try to fix Python symlinks specifically
if (fs.existsSync(venvBinDir)) {
diff --git a/scripts/preinstall-deps.js b/scripts/preinstall-deps.js
index d475ce75..ad04989f 100644
--- a/scripts/preinstall-deps.js
+++ b/scripts/preinstall-deps.js
@@ -17,8 +17,9 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
-const BIN_DIR = path.join(projectRoot, 'resources', 'prebuilt', 'bin');
-const VENV_DIR = path.join(projectRoot, 'resources', 'prebuilt', 'venv');
+const PREBUILT_DIR = path.join(projectRoot, 'resources', 'prebuilt');
+const BIN_DIR = path.join(PREBUILT_DIR, 'bin');
+const VENV_DIR = path.join(PREBUILT_DIR, 'venv');
const BACKEND_DIR = path.join(projectRoot, 'backend');
console.log('🚀 Starting pre-installation of dependencies...');
@@ -197,6 +198,45 @@ async function downloadFileWithValidation(urlsToTry, dest, validateFn, fileType
throw new Error(`Failed to download ${fileType} from all sources`);
}
+/**
+ * Recursively copy directory, handling symlinks properly
+ */
+function copyDirRecursiveSync(src, dest) {
+ if (!fs.existsSync(src)) {
+ return;
+ }
+
+ // Create destination directory
+ fs.mkdirSync(dest, { recursive: true });
+
+ // Get all files and directories
+ const entries = fs.readdirSync(src, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const srcPath = path.join(src, entry.name);
+ const destPath = path.join(dest, entry.name);
+
+ if (entry.isDirectory()) {
+ copyDirRecursiveSync(srcPath, destPath);
+ } else if (entry.isSymbolicLink()) {
+ try {
+ const realPath = fs.realpathSync(srcPath);
+ const realStat = fs.statSync(realPath);
+ if (realStat.isDirectory()) {
+ copyDirRecursiveSync(realPath, destPath);
+ } else {
+ fs.copyFileSync(realPath, destPath);
+ }
+ } catch (err) {
+ // If symlink target doesn't exist, skip it
+ console.log(` Skipping broken symlink: ${srcPath}`);
+ }
+ } else {
+ fs.copyFileSync(srcPath, destPath);
+ }
+ }
+}
+
/**
* Get Bun download URL list
*/
@@ -389,6 +429,28 @@ async function installUv() {
await tar.extract({ file: tempFilename, cwd: BIN_DIR });
}
+ // Handle nested directory from tarball if needed
+ if (!isWindows) {
+ const nestedDir = fs.readdirSync(BIN_DIR).find(f =>
+ fs.statSync(path.join(BIN_DIR, f)).isDirectory() && f.startsWith('uv-')
+ );
+ if (nestedDir) {
+ const nestedUvPath = path.join(BIN_DIR, nestedDir, 'uv');
+ const targetPath = path.join(BIN_DIR, 'uv');
+ if (fs.existsSync(nestedUvPath)) {
+ console.log(` Found uv in ${nestedDir}, moving...`);
+ try {
+ if (fs.existsSync(targetPath)) fs.unlinkSync(targetPath);
+ fs.renameSync(nestedUvPath, targetPath);
+ // Clean up directory
+ fs.rmSync(path.join(BIN_DIR, nestedDir), { recursive: true, force: true });
+ } catch (e) {
+ console.log(` Warning: Failed to move uv from nested dir: ${e.message}`);
+ }
+ }
+ }
+ }
+
const extractedUvPath = path.join(BIN_DIR, isWindows ? 'uv.exe' : 'uv');
if (fs.existsSync(extractedUvPath)) {
if (!isWindows && extractedUvPath !== uvPath) {
@@ -591,11 +653,66 @@ async function installPythonDeps(uvPath) {
console.log('📦 Creating Python venv...');
}
+ // Ensure Python is installed before syncing
+ // This is critical for Windows where Python might not be in the venv
+ console.log('🐍 Ensuring Python is installed...');
+ try {
+ execSync(
+ `"${uvPath}" python install 3.10`,
+ { cwd: BACKEND_DIR, env: env, stdio: 'inherit' }
+ );
+ } catch (error) {
+ console.log('⚠️ Python install command failed, continuing with sync (Python may already be installed)...');
+ }
+
+ // Use --python-preference only-managed to ensure uv uses its own managed Python
+ // This makes the venv more portable
execSync(
- `"${uvPath}" sync --no-dev --cache-dir "${cacheDir}"`,
+ `"${uvPath}" sync --no-dev --cache-dir "${cacheDir}" --python-preference only-managed`,
{ cwd: BACKEND_DIR, env: env, stdio: 'inherit' }
);
+ // Verify Python executable exists in the virtual environment
+ const isWindows = process.platform === 'win32';
+ const pythonExePath = isWindows
+ ? path.join(venvPath, 'Scripts', 'python.exe')
+ : path.join(venvPath, 'bin', 'python');
+
+ if (!fs.existsSync(pythonExePath)) {
+ throw new Error(
+ `Python executable not found in virtual environment at: ${pythonExePath}\n` +
+ `Virtual environment may be corrupted. Please ensure uv sync completed successfully.`
+ );
+ }
+
+ console.log(`✅ Python executable verified: ${pythonExePath}`);
+
+ // Bundle the actual Python installation from UV cache into prebuilt
+ console.log('📦 Bundling Python installation...');
+ try {
+ const uvPythonDir = pythonCacheDir;
+ const prebuiltPythonDir = path.join(PREBUILT_DIR, 'uv_python');
+
+ if (fs.existsSync(uvPythonDir)) {
+ console.log(` Copying from: ${uvPythonDir}`);
+ console.log(` Copying to: ${prebuiltPythonDir}`);
+
+ // Remove existing python dir if it exists
+ if (fs.existsSync(prebuiltPythonDir)) {
+ fs.rmSync(prebuiltPythonDir, { recursive: true, force: true });
+ }
+
+ // Copy the Python installation
+ copyDirRecursiveSync(uvPythonDir, prebuiltPythonDir);
+ console.log('✅ Python installation bundled');
+ } else {
+ console.log('⚠️ UV Python cache not found, venv may not be portable');
+ }
+ } catch (error) {
+ console.log(`⚠️ Failed to bundle Python: ${error.message}`);
+ console.log(' The app may fail to start without internet connection');
+ }
+
console.log('✅ Python dependencies installed');
console.log('📝 Compiling babel...');
diff --git a/scripts/test-notarization.js b/scripts/test-notarization.js
new file mode 100644
index 00000000..672b44a5
--- /dev/null
+++ b/scripts/test-notarization.js
@@ -0,0 +1,296 @@
+#!/usr/bin/env node
+/**
+ * Test script for macOS notarization issues
+ * This script checks for common issues that cause notarization to fail:
+ * 1. .npm-cache directories
+ * 2. flac-mac binary (outdated SDK)
+ * 3. Unsigned native binaries (.node files)
+ * 4. Other problematic files
+ */
+
+import fs from 'fs';
+import path from 'path';
+import { execSync } from 'child_process';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const projectRoot = path.resolve(__dirname, '..');
+
+const RELEASE_DIR = path.join(projectRoot, 'release');
+const APP_BUNDLE_PATTERN = /Eigent\.app$/;
+
+/**
+ * Find the app bundle in release directory
+ */
+function findAppBundle() {
+ if (!fs.existsSync(RELEASE_DIR)) {
+ console.log('❌ Release directory does not exist. Please build the app first.');
+ console.log(' Run: npm run build:mac');
+ return null;
+ }
+
+ const entries = fs.readdirSync(RELEASE_DIR, { withFileTypes: true });
+
+ for (const entry of entries) {
+ if (entry.isDirectory() && entry.name.match(APP_BUNDLE_PATTERN)) {
+ return path.join(RELEASE_DIR, entry.name);
+ }
+
+ // Check subdirectories (e.g., mac-arm64/Eigent.app)
+ if (entry.isDirectory()) {
+ const subDir = path.join(RELEASE_DIR, entry.name);
+ const subEntries = fs.readdirSync(subDir, { withFileTypes: true });
+ for (const subEntry of subEntries) {
+ if (subEntry.isDirectory() && subEntry.name.match(APP_BUNDLE_PATTERN)) {
+ return path.join(subDir, subEntry.name);
+ }
+ }
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Check for .npm-cache directories
+ */
+function checkNpmCache(bundlePath) {
+ console.log('\n🔍 Checking for .npm-cache directories...');
+ const issues = [];
+
+ function scanDir(dir) {
+ if (!fs.existsSync(dir)) {
+ return;
+ }
+
+ try {
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+
+ if (entry.name === '.npm-cache' && entry.isDirectory()) {
+ issues.push(fullPath);
+ } else if (entry.isDirectory()) {
+ // Skip node_modules to avoid deep scanning
+ if (entry.name !== 'node_modules' && entry.name !== '__pycache__') {
+ scanDir(fullPath);
+ }
+ }
+ }
+ } catch (error) {
+ // Ignore errors
+ }
+ }
+
+ const resourcesPath = path.join(bundlePath, 'Contents', 'Resources');
+ const prebuiltPath = path.join(resourcesPath, 'prebuilt');
+
+ if (fs.existsSync(prebuiltPath)) {
+ scanDir(prebuiltPath);
+ }
+
+ if (issues.length > 0) {
+ console.log(`❌ Found ${issues.length} .npm-cache directory(ies):`);
+ issues.forEach(issue => console.log(` - ${issue}`));
+ return false;
+ } else {
+ console.log('✅ No .npm-cache directories found');
+ return true;
+ }
+}
+
+/**
+ * Check for flac-mac binary
+ */
+function checkFlacMac(bundlePath) {
+ console.log('\n🔍 Checking for flac-mac binary...');
+ const issues = [];
+
+ const resourcesPath = path.join(bundlePath, 'Contents', 'Resources');
+ const prebuiltPath = path.join(resourcesPath, 'prebuilt');
+ const venvLibPath = path.join(prebuiltPath, 'venv', 'lib');
+
+ if (fs.existsSync(venvLibPath)) {
+ try {
+ const entries = fs.readdirSync(venvLibPath, { withFileTypes: true });
+ for (const entry of entries) {
+ if (entry.isDirectory() && entry.name.startsWith('python')) {
+ const flacMacPath = path.join(venvLibPath, entry.name, 'site-packages', 'speech_recognition', 'flac-mac');
+ if (fs.existsSync(flacMacPath)) {
+ issues.push(flacMacPath);
+ }
+ }
+ }
+ } catch (error) {
+ // Ignore errors
+ }
+ }
+
+ if (issues.length > 0) {
+ console.log(`❌ Found ${issues.length} flac-mac binary(ies) (outdated SDK):`);
+ issues.forEach(issue => console.log(` - ${issue}`));
+ return false;
+ } else {
+ console.log('✅ No flac-mac binaries found');
+ return true;
+ }
+}
+
+/**
+ * Check for unsigned native binaries
+ */
+function checkUnsignedBinaries(bundlePath) {
+ console.log('\n🔍 Checking for unsigned native binaries (.node files)...');
+ const issues = [];
+
+ function scanForNodeFiles(dir) {
+ if (!fs.existsSync(dir)) {
+ return;
+ }
+
+ try {
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+
+ if (entry.isFile() && entry.name.endsWith('.node')) {
+ // Check if file is signed
+ try {
+ const output = execSync(`codesign -dv "${fullPath}" 2>&1 || true`, { encoding: 'utf-8' });
+ if (output.includes('code object is not signed')) {
+ issues.push({
+ path: fullPath,
+ reason: 'Not signed'
+ });
+ }
+ } catch (error) {
+ // If codesign fails, assume it's not signed
+ issues.push({
+ path: fullPath,
+ reason: 'Could not verify signature'
+ });
+ }
+ } else if (entry.isDirectory()) {
+ // Skip certain directories
+ if (entry.name !== 'node_modules' && entry.name !== '__pycache__' && !entry.name.startsWith('.')) {
+ scanForNodeFiles(fullPath);
+ }
+ }
+ }
+ } catch (error) {
+ // Ignore errors
+ }
+ }
+
+ const resourcesPath = path.join(bundlePath, 'Contents', 'Resources');
+ const prebuiltPath = path.join(resourcesPath, 'prebuilt');
+
+ if (fs.existsSync(prebuiltPath)) {
+ scanForNodeFiles(prebuiltPath);
+ }
+
+ if (issues.length > 0) {
+ console.log(`❌ Found ${issues.length} unsigned .node file(s):`);
+ issues.forEach(issue => {
+ console.log(` - ${issue.path}`);
+ console.log(` Reason: ${issue.reason}`);
+ });
+ return false;
+ } else {
+ console.log('✅ No unsigned .node files found');
+ return true;
+ }
+}
+
+/**
+ * Check app bundle size
+ */
+function checkBundleSize(bundlePath) {
+ console.log('\n🔍 Checking app bundle size...');
+
+ try {
+ function getDirSize(dir) {
+ let size = 0;
+ try {
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+ if (entry.isFile()) {
+ size += fs.statSync(fullPath).size;
+ } else if (entry.isDirectory()) {
+ size += getDirSize(fullPath);
+ }
+ }
+ } catch (error) {
+ // Ignore errors
+ }
+ return size;
+ }
+
+ const size = getDirSize(bundlePath);
+ const sizeInMB = (size / (1024 * 1024)).toFixed(2);
+ console.log(` App bundle size: ${sizeInMB} MB`);
+
+ if (size > 500 * 1024 * 1024) {
+ console.log(` ⚠️ Large bundle size (>500MB) may cause slow notarization (30-60 minutes)`);
+ } else if (size > 200 * 1024 * 1024) {
+ console.log(` ⚠️ Medium bundle size (200-500MB) may take 15-30 minutes to notarize`);
+ } else {
+ console.log(` ✅ Bundle size is reasonable for notarization`);
+ }
+
+ return true;
+ } catch (error) {
+ console.log(` ⚠️ Could not calculate bundle size: ${error.message}`);
+ return true;
+ }
+}
+
+/**
+ * Main function
+ */
+function main() {
+ console.log('🧪 macOS Notarization Test Script\n');
+
+ const appBundle = findAppBundle();
+
+ if (!appBundle) {
+ console.log('\n💡 To build the app for testing:');
+ console.log(' npm run build:mac');
+ process.exit(1);
+ }
+
+ console.log(`📦 Found app bundle: ${appBundle}\n`);
+
+ const results = {
+ npmCache: checkNpmCache(appBundle),
+ flacMac: checkFlacMac(appBundle),
+ unsignedBinaries: checkUnsignedBinaries(appBundle),
+ bundleSize: checkBundleSize(appBundle),
+ };
+
+ console.log('\n📊 Summary:');
+ console.log(` .npm-cache directories: ${results.npmCache ? '✅' : '❌'}`);
+ console.log(` flac-mac binaries: ${results.flacMac ? '✅' : '❌'}`);
+ console.log(` Unsigned .node files: ${results.unsignedBinaries ? '✅' : '❌'}`);
+ console.log(` Bundle size: ${results.bundleSize ? '✅' : '⚠️'}`);
+
+ const allPassed = Object.values(results).every(r => r);
+
+ if (allPassed) {
+ console.log('\n✅ All checks passed! The app should be ready for notarization.');
+ console.log('\n💡 Note: This script only checks for common issues.');
+ console.log(' Actual notarization may still fail for other reasons.');
+ console.log(' To test actual notarization, you need:');
+ console.log(' - Valid Apple Developer ID certificate');
+ console.log(' - APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID environment variables');
+ } else {
+ console.log('\n❌ Some checks failed. Please fix the issues above before notarization.');
+ process.exit(1);
+ }
+}
+
+main();
diff --git a/server/Dockerfile b/server/Dockerfile
index b0969036..284596d1 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -4,8 +4,8 @@ FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
# Install the project into `/app`
WORKDIR /app
-# Enable bytecode compilation
-ENV UV_COMPILE_BYTECODE=1
+# Disable bytecode transfer during compilation to avoid EMFILE during build on low nofile limits
+ENV UV_COMPILE_BYTECODE=0
# Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy
@@ -15,7 +15,7 @@ ENV UV_PYTHON_INSTALL_MIRROR=https://registry.npmmirror.com/-/binary/python-buil
ARG database_url
ENV database_url=$database_url
-RUN apt-get update && apt-get install -y \
+RUN apt-get update -o Acquire::Retries=3 && apt-get install -y --no-install-recommends \
gcc \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
@@ -43,7 +43,7 @@ RUN uv run pybabel extract -F babel.cfg -o messages.pot . && \
# Install netcat for database connectivity check
-RUN apt-get update && apt-get install -y curl netcat-openbsd && rm -rf /var/lib/apt/lists/*
+RUN apt-get update -o Acquire::Retries=3 && apt-get install -y --no-install-recommends curl netcat-openbsd && rm -rf /var/lib/apt/lists/*
# Place executables in the environment at the front of the path
ENV PATH="/app/.venv/bin:$PATH"
@@ -58,4 +58,4 @@ ENTRYPOINT []
EXPOSE 5678
# Use the start script
-CMD ["/app/start.sh"]
\ No newline at end of file
+CMD ["/app/start.sh"]
diff --git a/server/README_PT-BR.md b/server/README_PT-BR.md
new file mode 100644
index 00000000..8b5ef99f
--- /dev/null
+++ b/server/README_PT-BR.md
@@ -0,0 +1,111 @@
+### Propósito
+`server/` fornece um backend local (FastAPI + PostgreSQL) para alcançar separação completa entre ambientes locais e na nuvem. Após implantar este serviço, dados sensíveis como registro de usuários, configurações de provedores de modelos, configurações de ferramentas e histórico de bate-papo são armazenados em sua máquina e não são enviados para nossa nuvem, a menos que você configure explicitamente serviços externos (por exemplo, provedores de modelos na nuvem ou servidores MCP remotos).
+
+### Serviços Fornecidos (Módulos Principais)
+- Usuários e Contas
+ - `POST /register`: Registro por email + senha (apenas banco de dados local)
+ - `POST /login`: Login com email + senha; retorna um token emitido localmente
+ - `GET/PUT /user`, `/user/profile`, `/user/privacy`, `/user/current_credits`, `/user/stat`, etc.
+- Provedores de Modelos (armazenar configurações de acesso a modelos locais/na nuvem)
+ - `GET /providers`, `POST /provider`, `PUT /provider/{id}`, `DELETE /provider/{id}`
+ - `POST /provider/prefer`: Definir um provedor preferido (frontend/backend priorizará)
+- Centro de Configuração (armazenar segredos/parâmetros necessários para ferramentas/capacidades)
+ - `GET /configs`, `POST /configs`, `PUT /configs/{id}`, `DELETE /configs/{id}`, `GET /config/info`
+- Chat e Dados
+ - Histórico, snapshots, compartilhamento, etc. em `app/controller/chat/`, todos persistidos no banco de dados local
+- Gerenciamento de MCP (importar servidores MCP locais/remotos)
+ - `GET /mcps`, `POST /mcp/install`, `POST /mcp/import/{Local|Remote}`, etc.
+
+Nota: Todos os dados acima são armazenados no volume PostgreSQL local no Docker (veja "Persistência de Dados" abaixo). Se você configurar modelos externos ou MCP remoto, as solicitações vão para os serviços de terceiros que você especificar.
+
+---
+
+### Início Rápido (Docker)
+
+#### Pré-requisitos
+- **Docker Desktop**: Instalado e em execução
+- **Python**: 3.10.* (3.10.15 recomendado)
+- **Node.js**: >=18.0.0 <23.0.0
+
+#### Etapas de Configuração
+
+1) Inicie os serviços
+```bash
+cd server
+# Copie .env.example para .env (ou crie .env de acordo com .env.example)
+cp .env.example .env
+docker compose up -d
+```
+
+2) Inicie o Frontend (Modo Local)
+- No diretório raiz do projeto, crie ou modifique `.env.development` para ativar o modo local e apontar para o backend local:
+```bash
+VITE_BASE_URL=/api
+VITE_USE_LOCAL_PROXY=true
+VITE_PROXY_URL=http://localhost:3001
+```
+- Inicie a aplicação frontend:
+```bash
+npm install
+npm run dev
+```
+
+### Abra a documentação da API
+- `http://localhost:3001/docs` (Interface do Swagger)
+
+### Portas
+- API: Host `3001` → Contêiner `5678`
+- PostgreSQL: Host `5432` → Contêiner `5432`
+
+### Persistência de Dados
+- Os dados do banco de dados são armazenados no volume Docker `server_postgres_data` em `/var/lib/postgresql/data` dentro do contêiner
+- As migrações de banco de dados são executadas automaticamente na inicialização do contêiner (veja `start.sh` → `alembic upgrade head`)
+
+### Comandos Comuns
+```bash
+# Listar contêineres em execução
+docker ps
+
+# Parar/Iniciar contêiner da API (manter DB)
+docker stop eigent_api
+docker start eigent_api
+
+# Parar/Iniciar tudo (API + DB)
+docker compose stop
+docker compose start
+
+# Exibir logs
+docker logs -f eigent_api | cat
+docker logs -f eigent_postgres | cat
+```
+
+---
+
+### Modo Desenvolvedor (Opcional)
+Você pode executar a API localmente com hot-reload enquanto mantém o banco de dados no Docker:
+```bash
+# Parar API no contêiner, manter DB
+docker stop eigent_api
+
+# Inicializar banco de dados (primeira execução ou quando o esquema do BD muda)
+cd server
+uv run alembic upgrade head
+
+# Executar localmente (fornecer string de conexão do BD)
+export database_url=postgresql://postgres:123456@localhost:5432/eigent
+uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0
+```
+
+---
+
+### Outros
+- Documentação da API: `http://localhost:3001/docs`
+- Logs de tempo de execução: `/app/runtime/log/app.log` no contêiner
+- i18n (para desenvolvedores)
+```bash
+uv run pybabel extract -F babel.cfg -o messages.pot .
+uv run pybabel init -i messages.pot -d lang -l zh_CN
+uv run pybabel compile -d lang -l zh_CN
+```
+
+Para um ambiente completamente offline, use apenas modelos locais e servidores MCP locais, e evite configurar quaisquer Provedores externos ou endereços MCP remotos.
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/server/pyproject.toml b/server/pyproject.toml
index fb23ec22..8a772ab7 100644
--- a/server/pyproject.toml
+++ b/server/pyproject.toml
@@ -7,7 +7,7 @@ requires-python = ">=3.12,<3.13"
dependencies = [
"alembic>=1.15.2",
"openai>=1.99.3,<2",
- "camel-ai==0.2.83a9",
+ "camel-ai==0.2.84",
"pydantic[email]>=2.11.1",
"click>=8.1.8",
"fastapi>=0.115.12",
diff --git a/server/uv.lock b/server/uv.lock
index 1bcd3d49..25688bc7 100644
--- a/server/uv.lock
+++ b/server/uv.lock
@@ -4,16 +4,16 @@ requires-python = "==3.12.*"
[[package]]
name = "alembic"
-version = "1.17.2"
+version = "1.18.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mako" },
{ name = "sqlalchemy" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064, upload-time = "2025-11-14T20:35:04.057Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/cc/aca263693b2ece99fa99a09b6d092acb89973eb2bb575faef1777e04f8b4/alembic-1.18.1.tar.gz", hash = "sha256:83ac6b81359596816fb3b893099841a0862f2117b2963258e965d70dc62fb866", size = 2044319, upload-time = "2026-01-14T18:53:14.907Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" },
+ { url = "https://files.pythonhosted.org/packages/83/36/cd9cb6101e81e39076b2fbe303bfa3c85ca34e55142b0324fcbf22c5c6e2/alembic-1.18.1-py3-none-any.whl", hash = "sha256:f1c3b0920b87134e851c25f1f7f236d8a332c34b75416802d06971df5d1b7810", size = 260973, upload-time = "2026-01-14T18:53:17.533Z" },
]
[[package]]
@@ -117,35 +117,35 @@ wheels = [
[[package]]
name = "boto3"
-version = "1.42.24"
+version = "1.42.30"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ee/21/8be0e3685c3a4868be48d8d2f6e5b4641727e1d8a5d396b8b401d2b5f06e/boto3-1.42.24.tar.gz", hash = "sha256:c47a2f40df933e3861fc66fd8d6b87ee36d4361663a7e7ba39a87f5a78b2eae1", size = 112788, upload-time = "2026-01-07T20:30:51.019Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/42/79/2dac8b7cb075cfa43908ee9af3f8ee06880d84b86013854c5cca8945afac/boto3-1.42.30.tar.gz", hash = "sha256:ba9cd2f7819637d15bfbeb63af4c567fcc8a7dcd7b93dd12734ec58601169538", size = 112809, upload-time = "2026-01-16T20:37:23.636Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/75/bbfccb268f9faa4f59030888e859dca9797a980b77d6a074113af73bd4bf/boto3-1.42.24-py3-none-any.whl", hash = "sha256:8ed6ad670a5a2d7f66c1b0d3362791b48392c7a08f78479f5d8ab319a4d9118f", size = 140572, upload-time = "2026-01-07T20:30:49.431Z" },
+ { url = "https://files.pythonhosted.org/packages/52/b3/2c0d828c9f668292e277ca5232e6160dd5b4b660a3f076f20dd5378baa1e/boto3-1.42.30-py3-none-any.whl", hash = "sha256:d7e548bea65e0ae2c465c77de937bc686b591aee6a352d5a19a16bc751e591c1", size = 140573, upload-time = "2026-01-16T20:37:22.089Z" },
]
[[package]]
name = "botocore"
-version = "1.42.24"
+version = "1.42.30"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/12/d7/bb4a4e839b238ffb67b002d7326b328ebe5eb23ed5180f2ca10399a802de/botocore-1.42.24.tar.gz", hash = "sha256:be8d1bea64fb91eea08254a1e5fea057e4428d08e61f4e11083a02cafc1f8cc6", size = 14878455, upload-time = "2026-01-07T20:30:40.379Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/38/23862628a0eb044c8b8b3d7a9ad1920b3bfd6bce6d746d5a871e8382c7e4/botocore-1.42.30.tar.gz", hash = "sha256:9bf1662b8273d5cc3828a49f71ca85abf4e021011c1f0a71f41a2ea5769a5116", size = 14891439, upload-time = "2026-01-16T20:37:13.77Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ff/d4/f2655d777eed8b069ecab3761454cb83f830f8be8b5b0d292e4b3a980d00/botocore-1.42.24-py3-none-any.whl", hash = "sha256:8fca9781d7c84f7ad070fceffaff7179c4aa7a5ffb27b43df9d1d957801e0a8d", size = 14551806, upload-time = "2026-01-07T20:30:38.103Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/8d/6d7b016383b1f74dd93611b1c5078bbaddaca901553ab886dcda87cae365/botocore-1.42.30-py3-none-any.whl", hash = "sha256:97070a438cac92430bb7b65f8ebd7075224f4a289719da4ee293d22d1e98db02", size = 14566340, upload-time = "2026-01-16T20:37:10.94Z" },
]
[[package]]
name = "camel-ai"
-version = "0.2.83a9"
+version = "0.2.84"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "astor" },
@@ -162,9 +162,9 @@ dependencies = [
{ name = "tiktoken" },
{ name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/bf/8c/7d8071776ba973bb6e734edb6caaf4fdbdf60ecebdc1c4017948cc67ad48/camel_ai-0.2.83a9.tar.gz", hash = "sha256:2ee560551797b089f9849d3b9d63cd3a2b4eb45d339d17e6bf95eba2b85c4b50", size = 1124774, upload-time = "2026-01-15T21:28:24.51Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9b/62/96d922750b304ab2dd0ac1ef35dd3fbbf0e9a44f147e08a1ddfeb94c51c6/camel_ai-0.2.84.tar.gz", hash = "sha256:173c79755fc986e3fa8e27523606222c5f8816fc085abb24831912dcd4a0dec3", size = 1125724, upload-time = "2026-01-20T17:23:13.524Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/34/77/f7594707571af9c86351a69ff9f7f580602b42ffe8113803153c069b6bff/camel_ai-0.2.83a9-py3-none-any.whl", hash = "sha256:7cfe97b590096c1cc5afddf6dca023c5b9a47d104196c16b4b2b1934931af260", size = 1595808, upload-time = "2026-01-15T21:28:21.068Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/8b/246abd2c47154de6220fd0a286c3fe50f343d49943ae17e26b9f824a1ca0/camel_ai-0.2.84-py3-none-any.whl", hash = "sha256:63bfbd09e605f9087bb73eb9b929e162fbc6778084ce50e43a367fc0e98cbc65", size = 1599378, upload-time = "2026-01-20T17:23:11.216Z" },
]
[[package]]
@@ -364,7 +364,7 @@ requires-dist = [
{ name = "alembic", specifier = ">=1.15.2" },
{ name = "arrow", specifier = ">=1.3.0" },
{ name = "bcrypt", specifier = "==4.0.1" },
- { name = "camel-ai", specifier = "==0.2.83a9" },
+ { name = "camel-ai", specifier = "==0.2.84" },
{ name = "click", specifier = ">=8.1.8" },
{ name = "convert-case", specifier = ">=1.2.3" },
{ name = "cryptography", specifier = ">=45.0.4" },
@@ -473,16 +473,16 @@ wheels = [
[[package]]
name = "fastapi-pagination"
-version = "0.15.5"
+version = "0.15.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "fastapi" },
{ name = "pydantic" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/41/5e/656fa2a88b25f3362d96b838d4858cb10f75ae62a917584375be3dae30fc/fastapi_pagination-0.15.5.tar.gz", hash = "sha256:65871797e53392f5a62eb206b4e1f5494d1f64a8ed4d085a32c4f7c1a1987ee1", size = 572714, upload-time = "2026-01-08T16:19:07.372Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a0/da/ad34e0fc98ca9731b0f76d07faeb39d525cb80440ac5814e270cb379d92a/fastapi_pagination-0.15.6.tar.gz", hash = "sha256:c59ca1aa056dccee3526953357c2d1128f988f83d3034d95ddb8de6f5a68e9f8", size = 573720, upload-time = "2026-01-11T22:15:36.385Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/47/f8/6d3dbbd818a106309d073c81081706541e8ffbf9ed581f799cf525e01e15/fastapi_pagination-0.15.5-py3-none-any.whl", hash = "sha256:a9276ad322d0c85b46f1d5e43b2ef33dce21d1a4dbf5598269752b7542a2b47b", size = 56576, upload-time = "2026-01-08T16:19:05.81Z" },
+ { url = "https://files.pythonhosted.org/packages/01/28/0cf3b51115e98c0b84553b9e11ec07f59ae580bf8585eb7876fa9afe4c7a/fastapi_pagination-0.15.6-py3-none-any.whl", hash = "sha256:5c44bfaa78c1c968ca6f027b01a27c1805194c7cc8776eb84ec78235abcdaece", size = 59624, upload-time = "2026-01-11T22:15:37.746Z" },
]
[[package]]
@@ -515,7 +515,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
- { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
@@ -738,21 +737,21 @@ wheels = [
[[package]]
name = "numpy"
-version = "2.4.0"
+version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720, upload-time = "2025-12-20T16:18:19.023Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/ff/f6400ffec95de41c74b8e73df32e3fff1830633193a7b1e409be7fb1bb8c/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117, upload-time = "2025-12-20T16:16:06.709Z" },
- { url = "https://files.pythonhosted.org/packages/fd/28/6c23e97450035072e8d830a3c411bf1abd1f42c611ff9d29e3d8f55c6252/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711, upload-time = "2025-12-20T16:16:08.758Z" },
- { url = "https://files.pythonhosted.org/packages/bc/af/acbef97b630ab1bb45e6a7d01d1452e4251aa88ce680ac36e56c272120ec/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355, upload-time = "2025-12-20T16:16:10.902Z" },
- { url = "https://files.pythonhosted.org/packages/c1/c8/4e0d436b66b826f2e53330adaa6311f5cac9871a5b5c31ad773b27f25a74/numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6", size = 6545298, upload-time = "2025-12-20T16:16:12.607Z" },
- { url = "https://files.pythonhosted.org/packages/ef/27/e1f5d144ab54eac34875e79037011d511ac57b21b220063310cb96c80fbc/numpy-2.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ddc8f4914466e6fc954c76527aa91aa763682a4f6d73249ef20b418fe6effb", size = 14398387, upload-time = "2025-12-20T16:16:14.257Z" },
- { url = "https://files.pythonhosted.org/packages/67/64/4cb909dd5ab09a9a5d086eff9586e69e827b88a5585517386879474f4cf7/numpy-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc578891de1db95b2a35001b695451767b580bb45753717498213c5ff3c41d63", size = 16363091, upload-time = "2025-12-20T16:16:17.32Z" },
- { url = "https://files.pythonhosted.org/packages/9d/9c/8efe24577523ec6809261859737cf117b0eb6fdb655abdfdc81b2e468ce4/numpy-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98e81648e0b36e325ab67e46b5400a7a6d4a22b8a7c8e8bbfe20e7db7906bf95", size = 16176394, upload-time = "2025-12-20T16:16:19.524Z" },
- { url = "https://files.pythonhosted.org/packages/61/f0/1687441ece7b47a62e45a1f82015352c240765c707928edd8aef875d5951/numpy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d57b5046c120561ba8fa8e4030fbb8b822f3063910fa901ffadf16e2b7128ad6", size = 18287378, upload-time = "2025-12-20T16:16:22.866Z" },
- { url = "https://files.pythonhosted.org/packages/d3/6f/f868765d44e6fc466467ed810ba9d8d6db1add7d4a748abfa2a4c99a3194/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432, upload-time = "2025-12-20T16:16:25.06Z" },
- { url = "https://files.pythonhosted.org/packages/d4/b5/94c1e79fcbab38d1ca15e13777477b2914dd2d559b410f96949d6637b085/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201, upload-time = "2025-12-20T16:16:26.979Z" },
- { url = "https://files.pythonhosted.org/packages/70/09/c39dadf0b13bb0768cd29d6a3aaff1fb7c6905ac40e9aaeca26b1c086e06/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234, upload-time = "2025-12-20T16:16:29.417Z" },
+ { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" },
+ { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" },
+ { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" },
+ { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" },
+ { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" },
+ { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" },
]
[[package]]
@@ -1175,14 +1174,14 @@ wheels = [
[[package]]
name = "pydash"
-version = "8.0.5"
+version = "8.0.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/2f/24/91c037f47e434172c2112d65c00c84d475a6715425e3315ba2cbb7a87e66/pydash-8.0.5.tar.gz", hash = "sha256:7cc44ebfe5d362f4f5f06c74c8684143c5ac481376b059ff02570705523f9e2e", size = 164861, upload-time = "2025-01-17T16:08:50.562Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/75/c1/1c55272f49d761cec38ddb80be9817935b9c91ebd6a8988e10f532868d56/pydash-8.0.6.tar.gz", hash = "sha256:b2821547e9723f69cf3a986be4db64de41730be149b2641947ecd12e1e11025a", size = 164338, upload-time = "2026-01-17T16:42:56.576Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/86/e74c978800131c657fc5145f2c1c63e0cea01a49b6216f729cf77a2e1edf/pydash-8.0.5-py3-none-any.whl", hash = "sha256:b2625f8981862e19911daa07f80ed47b315ce20d9b5eb57aaf97aaf570c3892f", size = 102077, upload-time = "2025-01-17T16:08:47.91Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/b7/cc5e7974699db40014d58c7dd7c4ad4ffc244d36930dc9ec7d06ee67d7a9/pydash-8.0.6-py3-none-any.whl", hash = "sha256:ee70a81a5b292c007f28f03a4ee8e75c1f5d7576df5457b836ec7ab2839cc5d0", size = 101561, upload-time = "2026-01-17T16:42:55.448Z" },
]
[[package]]
@@ -1281,24 +1280,26 @@ wheels = [
[[package]]
name = "regex"
-version = "2025.11.3"
+version = "2026.1.15"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" },
- { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" },
- { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" },
- { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" },
- { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" },
- { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" },
- { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" },
- { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" },
- { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" },
- { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" },
- { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" },
- { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" },
- { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" },
- { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" },
+ { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" },
+ { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" },
+ { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" },
+ { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" },
+ { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" },
+ { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" },
]
[[package]]
@@ -1424,15 +1425,15 @@ wheels = [
[[package]]
name = "sse-starlette"
-version = "3.1.2"
+version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "starlette" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303, upload-time = "2025-12-31T08:02:20.023Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484, upload-time = "2025-12-31T08:02:18.894Z" },
+ { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" },
]
[[package]]
diff --git a/src/assets/wechat_qr.jpg b/src/assets/wechat_qr.jpg
index f495c69b..0d92ed47 100644
Binary files a/src/assets/wechat_qr.jpg and b/src/assets/wechat_qr.jpg differ
diff --git a/src/components/AddWorker/ToolSelect.tsx b/src/components/AddWorker/ToolSelect.tsx
index 24e45502..727aa6bf 100644
--- a/src/components/AddWorker/ToolSelect.tsx
+++ b/src/components/AddWorker/ToolSelect.tsx
@@ -630,7 +630,7 @@ const ToolSelect = forwardRef<
key={item.id + item.key + (item.isLocal + "")}
className="h-5 bg-button-tertiery-fill-default flex items-center gap-1 w-auto flex-shrink-0 px-xs"
>
- {item.name || item.mcp_name}
+ {item.name || item.mcp_name || item.key || `tool_${item.id}`}
tool.name)],
+ tools: [...selectedTools.map((tool) => tool.name || tool.mcp_name || tool.key || `tool_${tool.id}`)],
activeWebviewIds: [],
workerInfo: {
name: workerName,
@@ -310,7 +310,7 @@ export function AddWorker({
type: workerName as AgentNameType,
log: [],
tools: [
- ...selectedTools.map((tool) => tool?.key || tool?.mcp_name || ""),
+ ...selectedTools.map((tool) => tool.name || tool.mcp_name || tool.key || `tool_${tool.id}`),
],
activeWebviewIds: [],
workerInfo: {
diff --git a/src/components/BrowserAgentWrokSpace/index.tsx b/src/components/BrowserAgentWrokSpace/index.tsx
new file mode 100644
index 00000000..ab46b4f1
--- /dev/null
+++ b/src/components/BrowserAgentWrokSpace/index.tsx
@@ -0,0 +1,404 @@
+import { useEffect, useState, useRef } from "react";
+import {
+ ArrowDown,
+ ArrowUp,
+ Bird,
+ Bot,
+ ChevronLeft,
+ CodeXml,
+ FileText,
+ GalleryThumbnails,
+ Globe,
+ Hand,
+ Image,
+ Settings2,
+} from "lucide-react";
+import { Button } from "../ui/button";
+import { fetchPut } from "@/api/http";
+import { TaskState } from "../TaskState";
+import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
+
+export default function Home() {
+ //Get Chatstore for the active project's task
+ const { chatStore, projectStore } = useChatStoreAdapter();
+ if (!chatStore) {
+ return Loading...
;
+ }
+
+ const [isSingleMode, setIsSingleMode] = useState(false);
+ const scrollContainerRef = useRef(null);
+
+ const agentMap = {
+ developer_agent: {
+ name: "Developer Agent",
+ icon: ,
+ textColor: "text-emerald-700",
+ bgColor: "bg-bg-fill-coding-active",
+ shapeColor: "bg-bg-fill-coding-default",
+ borderColor: "border-bg-fill-coding-active",
+ bgColorLight: "bg-emerald-200",
+ },
+ browser_agent: {
+ name: "Browser agent",
+ icon: ,
+ textColor: "text-blue-700",
+ bgColor: "bg-bg-fill-browser-active",
+ shapeColor: "bg-bg-fill-browser-default",
+ borderColor: "border-bg-fill-browser-active",
+ bgColorLight: "bg-blue-200",
+ },
+ document_agent: {
+ name: "Document Agent",
+ icon: ,
+ textColor: "text-yellow-700",
+ bgColor: "bg-bg-fill-writing-active",
+ shapeColor: "bg-bg-fill-writing-default",
+ borderColor: "border-bg-fill-writing-active",
+ bgColorLight: "bg-yellow-200",
+ },
+ multi_modal_agent: {
+ name: "Multi Modal Agent",
+ icon: ,
+ textColor: "text-fuchsia-700",
+ bgColor: "bg-bg-fill-multimodal-active",
+ shapeColor: "bg-bg-fill-multimodal-default",
+ borderColor: "border-bg-fill-multimodal-active",
+ bgColorLight: "bg-fuchsia-200",
+ },
+ social_medium_agent: {
+ name: "Social Media Agent",
+ icon: ,
+ textColor: "text-purple-700",
+ bgColor: "bg-violet-700",
+ shapeColor: "bg-violet-300",
+ borderColor: "border-violet-700",
+ bgColorLight: "bg-purple-50",
+ },
+ };
+ const [activeAgent, setActiveAgent] = useState(null);
+ useEffect(() => {
+ const taskAssigning =
+ chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning;
+ if (taskAssigning) {
+ const activeAgent = taskAssigning.find(
+ (item) =>
+ item.agent_id ===
+ chatStore.tasks[chatStore.activeTaskId as string]?.activeWorkSpace
+ );
+ setActiveAgent(() => {
+ if (activeAgent) {
+ return activeAgent;
+ }
+ return null;
+ });
+ }
+ }, [
+ chatStore.tasks[chatStore.activeTaskId as string].taskAssigning,
+ chatStore.tasks[chatStore.activeTaskId as string].activeWorkSpace,
+ ]);
+
+ const [isTakeControl, setIsTakeControl] = useState(false);
+ const handleTakeControl = (id: string) => {
+ console.log("handleTakeControl", id);
+ fetchPut(`/task/${projectStore.activeProjectId}/take-control`, {
+ action: "pause",
+ });
+
+ setIsTakeControl(true);
+ setTimeout(() => {
+ getSize();
+ // show corresponding webview
+ window.electronAPI.showWebview(id);
+ }, 400);
+ };
+
+ // listen to webview container size
+ useEffect(() => {
+ if (!projectStore.activeProjectId) {
+ projectStore.createProject("new project");
+ console.warn("No active projectId found in WorkSpace, creating a new project");
+ }
+
+ const webviewContainer = document.getElementById("webview-container");
+ if (webviewContainer) {
+ const resizeObserver = new ResizeObserver(() => {
+ getSize();
+ });
+ resizeObserver.observe(webviewContainer);
+
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }
+ }, []);
+ const getSize = () => {
+ const webviewContainer = document.getElementById("webview-container");
+ if (webviewContainer) {
+ const rect = webviewContainer.getBoundingClientRect();
+ window.electronAPI.setSize({
+ x: rect.left,
+ y: rect.top,
+ width: rect.width,
+ height: rect.height,
+ });
+ }
+ };
+
+ const [url, setUrl] = useState("");
+
+ useEffect(() => {
+ window.ipcRenderer?.on("url-updated", (_event, newUrl) => {
+ setUrl(newUrl);
+ });
+
+ // optional: clear listener when uninstall
+ return () => {
+ window.ipcRenderer.removeAllListeners("url-updated");
+ };
+ }, []);
+
+ return isTakeControl ? (
+
+
+
+
+
+ {/*
{url}
*/}
+
+
+
+ ) : (
+
+
+
+
+
+
+ {agentMap[activeAgent?.type as keyof typeof agentMap]?.name}
+
+
task.reAssignTo).length ||
+ 0
+ }
+ done={
+ activeAgent?.tasks?.filter(
+ (task) => task.status === "completed"
+ ).length || 0
+ }
+ progress={
+ activeAgent?.tasks?.filter(
+ (task) =>
+ task.status !== "failed" &&
+ task.status !== "completed" &&
+ task.status !== "skipped" &&
+ task.status !== "waiting"
+ ).length || 0
+ }
+ failed={
+ activeAgent?.tasks?.filter((task) => task.status === "failed")
+ .length || 0
+ }
+ skipped={
+ activeAgent?.tasks?.filter(
+ (task) =>
+ task.status === "skipped" || task.status === "waiting"
+ ).length || 0
+ }
+ />
+ {/*
+ {
+ activeAgent?.tasks?.filter(
+ (task) => task.status && task.status !== "running"
+ ).length
+ }
+ /{activeAgent?.tasks?.length}
+
*/}
+
+ {/*
+
+
*/}
+
+
+ {activeAgent?.activeWebviewIds?.length === 1 ? (
+
+ {activeAgent?.activeWebviewIds[0]?.img && (
+
+ handleTakeControl(
+ activeAgent?.activeWebviewIds?.[0]?.id || ""
+ )
+ }
+ className="cursor-pointer relative h-full w-full group pt-sm rounded-b-2xl"
+ >
+

+
+
+
+
+ )}
+
+ ) : (
+
+ {activeAgent?.activeWebviewIds
+ ?.filter((item) => item?.img)
+ .map((item, index) => {
+ return (
+
handleTakeControl(item.id)}
+ className={`cursor-pointer relative card-box rounded-lg group ${
+ isSingleMode
+ ? "h-[calc(100%)] w-[calc(100%)]"
+ : "h-[calc(50%-8px)] w-[calc(50%-8px)]"
+ }`}
+ >
+ {item.img && (
+

+ )}
+
+ handleTakeControl(
+ activeAgent?.activeWebviewIds?.[0]?.id || ""
+ )
+ }
+ className="flex justify-center items-center opacity-0 transition-all group-hover:opacity-100 rounded-lg absolute inset-0 w-full h-full bg-black/20 pointer-events-none"
+ >
+
+
+
+ );
+ })}
+
+ )}
+ {activeAgent?.activeWebviewIds?.length !== 1 && (
+
+
+
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/InstallStep/InstallDependencies.tsx b/src/components/InstallStep/InstallDependencies.tsx
index 3ea2d3aa..ca70b0a9 100644
--- a/src/components/InstallStep/InstallDependencies.tsx
+++ b/src/components/InstallStep/InstallDependencies.tsx
@@ -16,7 +16,7 @@ export const InstallDependencies: React.FC = () => {
} = useInstallationUI();
return (
-
+
{/* {isInstalling.toString()} */}
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index 8e0a7814..8d82cb42 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -99,7 +99,6 @@ const Layout = () => {
onOpenChange={setNoticeOpen}
open={noticeOpen}
/>
-
);
diff --git a/src/components/Navigation/index.tsx b/src/components/Navigation/index.tsx
index 16077cb5..960c6c25 100644
--- a/src/components/Navigation/index.tsx
+++ b/src/components/Navigation/index.tsx
@@ -53,7 +53,7 @@ export function VerticalNavigation({
>
diff --git a/src/components/TerminalAgentWrokSpace/index.tsx b/src/components/TerminalAgentWrokSpace/index.tsx
index a4111f75..d445627f 100644
--- a/src/components/TerminalAgentWrokSpace/index.tsx
+++ b/src/components/TerminalAgentWrokSpace/index.tsx
@@ -332,4 +332,4 @@ export default function TerminalAgentWrokSpace() {
);
-}
+}
\ No newline at end of file
diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx
index 71ada441..860dc60c 100644
--- a/src/components/TopBar/index.tsx
+++ b/src/components/TopBar/index.tsx
@@ -14,17 +14,10 @@ import {
ChevronLeft,
House,
Share,
- MoreHorizontal,
} from "lucide-react";
import "./index.css";
import folderIcon from "@/assets/Folder.svg";
import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
import { useLocation, useNavigate } from "react-router-dom";
import { useSidebarStore } from "@/store/sidebarStore";
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
@@ -264,16 +257,16 @@ function HeaderWin() {
-
+
+
+
)}
{location.pathname !== "/history" && (
@@ -344,37 +337,42 @@ function HeaderWin() {
)}
-
-
+ {chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && (
+
-
-
- {chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && (
-
-
- {t("layout.report-bug")}
-
- )}
-
-
- {t("layout.refer-friends")}
-
- navigate("/history?tab=settings")} className="cursor-pointer">
-
- {t("layout.settings")}
-
-
-
+
+ )}
+
+
+
+
+
+
)}
{location.pathname === "/history" && (
diff --git a/src/components/WorkFlow/index.tsx b/src/components/WorkFlow/index.tsx
index f23c7975..05d260f2 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 {
@@ -450,4 +459,4 @@ export default function Workflow({
);
-}
+}
\ No newline at end of file
diff --git a/src/components/WorkFlow/node.tsx b/src/components/WorkFlow/node.tsx
index 0f5a5e8f..30b261ab 100644
--- a/src/components/WorkFlow/node.tsx
+++ b/src/components/WorkFlow/node.tsx
@@ -420,7 +420,7 @@ export function Node({ id, data }: NodeProps) {
{/* {JSON.stringify(data.agent)} */}
{agentToolkits[
@@ -894,4 +894,4 @@ export function Node({ id, data }: NodeProps) {
/>
>
);
-}
+}
\ No newline at end of file
diff --git a/src/components/ui/button.stories.tsx b/src/components/ui/button.stories.tsx
new file mode 100644
index 00000000..0ae56ba1
--- /dev/null
+++ b/src/components/ui/button.stories.tsx
@@ -0,0 +1,223 @@
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Button } from './button'
+import { Plus, Download, Trash2 } from 'lucide-react'
+import { expect, fn, userEvent, within } from 'storybook/test'
+
+const meta: Meta
= {
+ title: 'UI/Button',
+ component: Button,
+ argTypes: {
+ variant: {
+ control: 'select',
+ options: [
+ 'primary',
+ 'secondary',
+ 'outline',
+ 'ghost',
+ 'success',
+ 'cuation',
+ 'information',
+ 'warning',
+ ],
+ },
+ size: {
+ control: 'select',
+ options: ['xxs', 'xs', 'sm', 'md', 'lg', 'icon'],
+ },
+ disabled: {
+ control: 'boolean',
+ },
+ asChild: {
+ control: 'boolean',
+ },
+ children: {
+ control: 'text',
+ },
+ },
+ args: {
+ children: 'Button',
+ variant: 'primary',
+ size: 'md',
+ },
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {
+ args: {
+ variant: 'primary',
+ children: 'Primary Button',
+ },
+}
+
+export const Secondary: Story = {
+ args: {
+ variant: 'secondary',
+ children: 'Secondary Button',
+ },
+}
+
+export const Outline: Story = {
+ args: {
+ variant: 'outline',
+ children: 'Outline Button',
+ },
+}
+
+export const Ghost: Story = {
+ args: {
+ variant: 'ghost',
+ children: 'Ghost Button',
+ },
+}
+
+export const Success: Story = {
+ args: {
+ variant: 'success',
+ children: 'Success Button',
+ },
+}
+
+export const Warning: Story = {
+ args: {
+ variant: 'warning',
+ children: 'Warning Button',
+ },
+}
+
+export const Disabled: Story = {
+ args: {
+ variant: 'primary',
+ children: 'Disabled Button',
+ disabled: true,
+ },
+}
+
+export const WithIcon: Story = {
+ render: (args) => (
+
+ ),
+ args: {
+ variant: 'primary',
+ },
+}
+
+export const IconOnly: Story = {
+ render: (args) => (
+
+ ),
+ args: {
+ variant: 'ghost',
+ size: 'icon',
+ },
+}
+
+export const AllVariants: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+ ),
+}
+
+export const AllSizes: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+ ),
+}
+
+// Interaction test stories
+export const ClickInteraction: Story = {
+ args: {
+ variant: 'primary',
+ children: 'Click Me',
+ onClick: fn(),
+ },
+ play: async ({ args, canvasElement }) => {
+ const canvas = within(canvasElement)
+ const button = canvas.getByRole('button', { name: /click me/i })
+
+ // Test that button is visible and enabled
+ await expect(button).toBeVisible()
+ await expect(button).toBeEnabled()
+
+ // Click the button
+ await userEvent.click(button)
+
+ // Verify the onClick handler was called
+ await expect(args.onClick).toHaveBeenCalledTimes(1)
+ },
+}
+
+export const DisabledInteraction: Story = {
+ args: {
+ variant: 'primary',
+ children: 'Disabled Button',
+ disabled: true,
+ onClick: fn(),
+ },
+ play: async ({ args, canvasElement }) => {
+ const canvas = within(canvasElement)
+ const button = canvas.getByRole('button', { name: /disabled button/i })
+
+ // Test that button is visible but disabled
+ await expect(button).toBeVisible()
+ await expect(button).toBeDisabled()
+
+ // Verify the onClick handler was NOT called (disabled buttons block pointer events)
+ await expect(args.onClick).not.toHaveBeenCalled()
+ },
+}
+
+export const HoverInteraction: Story = {
+ args: {
+ variant: 'outline',
+ children: 'Hover Over Me',
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement)
+ const button = canvas.getByRole('button', { name: /hover over me/i })
+
+ // Test initial state
+ await expect(button).toBeVisible()
+
+ // Hover over the button
+ await userEvent.hover(button)
+
+ // The button should still be visible after hover
+ await expect(button).toBeVisible()
+
+ // Unhover
+ await userEvent.unhover(button)
+ },
+}
diff --git a/src/components/ui/dialog.stories.tsx b/src/components/ui/dialog.stories.tsx
new file mode 100644
index 00000000..0a151d4e
--- /dev/null
+++ b/src/components/ui/dialog.stories.tsx
@@ -0,0 +1,512 @@
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { useState } from 'react'
+import {
+ Dialog,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogContentSection,
+ DialogFooter,
+} from './dialog'
+import { Button } from './button'
+import { Input } from './input'
+import { expect, userEvent, within } from 'storybook/test'
+
+const meta: Meta = {
+ title: 'UI/Dialog',
+ component: Dialog,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ render: function DefaultDialog() {
+ const [open, setOpen] = useState(false)
+ return (
+
+ )
+ },
+}
+
+export const SmallSize: Story = {
+ render: function SmallDialog() {
+ const [open, setOpen] = useState(false)
+ return (
+
+ )
+ },
+}
+
+export const LargeSize: Story = {
+ render: function LargeDialog() {
+ const [open, setOpen] = useState(false)
+ return (
+
+ )
+ },
+}
+
+export const WithForm: Story = {
+ render: function FormDialog() {
+ const [open, setOpen] = useState(false)
+ return (
+
+ )
+ },
+}
+
+export const WithTooltip: Story = {
+ render: function TooltipDialog() {
+ const [open, setOpen] = useState(false)
+ return (
+
+ )
+ },
+}
+
+export const WithBackButton: Story = {
+ render: function BackButtonDialog() {
+ const [open, setOpen] = useState(false)
+ return (
+
+ )
+ },
+}
+
+export const DestructiveAction: Story = {
+ render: function DestructiveDialog() {
+ const [open, setOpen] = useState(false)
+ return (
+
+ )
+ },
+}
+
+export const NoCloseButton: Story = {
+ render: function NoCloseDialog() {
+ const [open, setOpen] = useState(false)
+ return (
+
+ )
+ },
+}
+
+export const AllSizes: Story = {
+ render: function AllSizesDialog() {
+ const [openSm, setOpenSm] = useState(false)
+ const [openMd, setOpenMd] = useState(false)
+ const [openLg, setOpenLg] = useState(false)
+ return (
+
+
+
+
+
+
+
+ )
+ },
+}
+
+// Interaction test stories
+export const OpenCloseInteraction: Story = {
+ render: function OpenCloseDialog() {
+ const [open, setOpen] = useState(false)
+ return (
+
+ )
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement)
+ const body = within(document.body)
+
+ // Open the dialog
+ await userEvent.click(canvas.getByRole('button', { name: /open dialog/i }))
+
+ // Verify dialog is visible (renders in portal)
+ await expect(await body.findByText('Interactive Dialog')).toBeVisible()
+
+ // Close the dialog
+ await userEvent.click(body.getByRole('button', { name: /cancel/i }))
+
+ // Verify dialog is closed
+ await expect(body.queryByText('Interactive Dialog')).not.toBeInTheDocument()
+ },
+}
+
+export const FormInteraction: Story = {
+ render: function FormInteractionDialog() {
+ const [open, setOpen] = useState(false)
+ const [submitted, setSubmitted] = useState(false)
+ return (
+
+ )
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement)
+ const body = within(document.body)
+
+ // Open the dialog
+ await userEvent.click(canvas.getByRole('button', { name: /open form/i }))
+
+ // Wait for dialog to appear (renders in portal)
+ await expect(await body.findByText('Sign Up')).toBeVisible()
+
+ // Fill in the form
+ const nameInput = body.getByPlaceholderText('Enter your name')
+ const emailInput = body.getByPlaceholderText('Enter your email')
+
+ await userEvent.type(nameInput, 'John Doe')
+ await userEvent.type(emailInput, 'john@example.com')
+
+ // Verify inputs have values
+ await expect(nameInput).toHaveValue('John Doe')
+ await expect(emailInput).toHaveValue('john@example.com')
+
+ // Submit the form
+ await userEvent.click(body.getByRole('button', { name: /sign up/i }))
+
+ // Verify success message appears
+ await expect(await canvas.findByTestId('success-message')).toBeVisible()
+ },
+}
+
+export const ConfirmDialogInteraction: Story = {
+ render: function ConfirmInteractionDialog() {
+ const [open, setOpen] = useState(false)
+ const [confirmed, setConfirmed] = useState(false)
+ return (
+
+
+ {confirmed &&
Item deleted!
}
+
+ )
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement)
+ const body = within(document.body)
+
+ // Open the confirmation dialog
+ await userEvent.click(canvas.getByRole('button', { name: /delete item/i }))
+
+ // Verify dialog is visible (renders in portal)
+ await expect(await body.findByText('Confirm Delete')).toBeVisible()
+
+ // Click the confirm/delete button
+ await userEvent.click(body.getByRole('button', { name: /^delete$/i }))
+
+ // Verify the deleted message appears
+ const deletedMessage = await canvas.findByTestId('deleted-message')
+ await expect(deletedMessage).toHaveTextContent('Item deleted!')
+ },
+}
diff --git a/src/components/ui/input.stories.tsx b/src/components/ui/input.stories.tsx
new file mode 100644
index 00000000..047affa4
--- /dev/null
+++ b/src/components/ui/input.stories.tsx
@@ -0,0 +1,291 @@
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Input } from './input'
+import { Search, Eye, EyeOff } from 'lucide-react'
+import { useState } from 'react'
+import { expect, fn, userEvent, within } from 'storybook/test'
+
+const meta: Meta = {
+ title: 'UI/Input',
+ component: Input,
+ argTypes: {
+ size: {
+ control: 'select',
+ options: ['default', 'sm'],
+ },
+ state: {
+ control: 'select',
+ options: ['default', 'hover', 'input', 'error', 'success', 'disabled'],
+ },
+ disabled: {
+ control: 'boolean',
+ },
+ required: {
+ control: 'boolean',
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {
+ placeholder: 'Enter text...',
+ },
+}
+
+export const WithTitle: Story = {
+ args: {
+ title: 'Email Address',
+ placeholder: 'name@example.com',
+ type: 'email',
+ },
+}
+
+export const Required: Story = {
+ args: {
+ title: 'Username',
+ placeholder: 'Enter username',
+ required: true,
+ },
+}
+
+export const WithTooltip: Story = {
+ args: {
+ title: 'API Key',
+ placeholder: 'Enter your API key',
+ tooltip: 'Your API key can be found in your account settings',
+ },
+}
+
+export const WithNote: Story = {
+ args: {
+ title: 'Password',
+ type: 'password',
+ placeholder: 'Enter password',
+ note: 'Must be at least 8 characters',
+ },
+}
+
+export const ErrorState: Story = {
+ args: {
+ title: 'Email',
+ placeholder: 'name@example.com',
+ state: 'error',
+ note: 'Please enter a valid email address',
+ defaultValue: 'invalid-email',
+ },
+}
+
+export const SuccessState: Story = {
+ args: {
+ title: 'Username',
+ placeholder: 'Enter username',
+ state: 'success',
+ note: 'Username is available',
+ defaultValue: 'johndoe',
+ },
+}
+
+export const Disabled: Story = {
+ args: {
+ title: 'Locked Field',
+ placeholder: 'This field is disabled',
+ disabled: true,
+ defaultValue: 'Cannot edit',
+ },
+}
+
+export const SmallSize: Story = {
+ args: {
+ size: 'sm',
+ placeholder: 'Small input',
+ },
+}
+
+export const WithLeadingIcon: Story = {
+ args: {
+ placeholder: 'Search...',
+ leadingIcon: ,
+ },
+}
+
+export const WithBackIcon: Story = {
+ render: function PasswordInput() {
+ const [showPassword, setShowPassword] = useState(false)
+ return (
+ : }
+ onBackIconClick={() => setShowPassword(!showPassword)}
+ />
+ )
+ },
+}
+
+export const AllStates: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+ ),
+}
+
+export const FormExample: Story = {
+ render: () => (
+
+
Contact Form
+
+
+
+
+
+ ),
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+}
+
+// Interaction test stories
+export const TypeInteraction: Story = {
+ args: {
+ placeholder: 'Type something...',
+ onChange: fn(),
+ },
+ play: async ({ args, canvasElement }) => {
+ const canvas = within(canvasElement)
+ const input = canvas.getByPlaceholderText('Type something...')
+
+ // Test that input is visible and enabled
+ await expect(input).toBeVisible()
+ await expect(input).toBeEnabled()
+
+ // Clear any existing value and type new text
+ await userEvent.clear(input)
+ await userEvent.type(input, 'Hello World')
+
+ // Verify the input value
+ await expect(input).toHaveValue('Hello World')
+
+ // Verify onChange was called
+ await expect(args.onChange).toHaveBeenCalled()
+ },
+}
+
+export const FocusInteraction: Story = {
+ args: {
+ title: 'Focus Test',
+ placeholder: 'Click to focus...',
+ onFocus: fn(),
+ onBlur: fn(),
+ },
+ play: async ({ args, canvasElement }) => {
+ const canvas = within(canvasElement)
+ const input = canvas.getByPlaceholderText('Click to focus...')
+
+ // Click to focus the input
+ await userEvent.click(input)
+ await expect(args.onFocus).toHaveBeenCalled()
+
+ // Tab away to blur
+ await userEvent.tab()
+ await expect(args.onBlur).toHaveBeenCalled()
+ },
+}
+
+export const ClearAndTypeInteraction: Story = {
+ args: {
+ title: 'Edit Field',
+ placeholder: 'Edit this text',
+ defaultValue: 'Initial value',
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement)
+ const input = canvas.getByPlaceholderText('Edit this text')
+
+ // Verify initial value
+ await expect(input).toHaveValue('Initial value')
+
+ // Select all and replace
+ await userEvent.tripleClick(input)
+ await userEvent.type(input, 'Replaced text')
+
+ // Verify the new value
+ await expect(input).toHaveValue('Replaced text')
+ },
+}
+
+export const PasswordToggleInteraction: Story = {
+ render: function PasswordToggle() {
+ const [showPassword, setShowPassword] = useState(false)
+ return (
+ : }
+ onBackIconClick={() => setShowPassword(!showPassword)}
+ />
+ )
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement)
+ const input = canvas.getByPlaceholderText('Enter password')
+ const toggleButton = canvas.getByRole('button')
+
+ // Initially password should be hidden (type="password")
+ await expect(input).toHaveAttribute('type', 'password')
+
+ // Click toggle to show password
+ await userEvent.click(toggleButton)
+ await expect(input).toHaveAttribute('type', 'text')
+
+ // Click toggle again to hide password
+ await userEvent.click(toggleButton)
+ await expect(input).toHaveAttribute('type', 'password')
+ },
+}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
index a8534db9..12cf6c28 100644
--- a/src/components/ui/input.tsx
+++ b/src/components/ui/input.tsx
@@ -31,47 +31,47 @@ function resolveStateClasses(state: InputState | undefined) {
if (state === "disabled") {
return {
container: "opacity-50 cursor-not-allowed",
- field:
- "border-input-border-default bg-input-bg-default text-input-text-default",
+ field: "border-input-border-default bg-input-bg-default",
+ input: "text-text-heading",
placeholder: "placeholder-input-label-default",
}
}
if (state === "hover") {
return {
container: "",
- field:
- "border-input-border-hover bg-input-bg-default text-input-text-default",
+ field: "border-input-border-hover bg-input-bg-default",
+ input: "text-text-heading",
placeholder: "placeholder-input-label-default",
}
}
if (state === "input") {
return {
container: "",
- field:
- "border-input-border-focus bg-input-bg-input text-input-text-focus",
+ field: "border-input-border-focus bg-input-bg-input",
+ input: "text-text-heading",
placeholder: "placeholder-input-label-default",
}
}
if (state === "error") {
return {
container: "",
- field:
- "border-input-border-cuation bg-input-bg-default text-text-body",
+ field: "border-input-border-cuation bg-input-bg-default",
+ input: "text-text-heading",
placeholder: "placeholder-input-label-default",
}
}
if (state === "success") {
return {
container: "",
- field:
- "border-input-border-success bg-input-bg-confirm text-text-body",
+ field: "border-input-border-success bg-input-bg-confirm",
+ input: "text-text-heading",
placeholder: "placeholder-input-label-default",
}
}
return {
container: "",
- field:
- "border-input-border-default bg-input-bg-default text-input-text-default",
+ field: "border-input-border-default bg-input-bg-default",
+ input: "text-text-heading",
placeholder: "placeholder-input-label-default/10",
}
}
@@ -119,7 +119,7 @@ const Input = React.forwardRef(
(
placeholder={placeholder}
className={cn(
"peer w-full bg-transparent outline-none placeholder:transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium",
+ stateCls.input,
stateCls.placeholder,
hasLeft ? "pl-9" : "pl-3",
hasRight ? "pr-9" : "pr-3",
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
index 78714fbf..24bdf3b3 100644
--- a/src/components/ui/select.tsx
+++ b/src/components/ui/select.tsx
@@ -72,7 +72,7 @@ const SelectTrigger = React.forwardRef<
disabled={disabled}
className={cn(
// Base styles
- "relative flex w-full items-center justify-between rounded-md border border-input-border-default border-solid outline-none transition-colors px-3 gap-2 text-text-body",
+ "relative flex w-full items-center justify-between rounded-lg border border-input-border-default border-solid outline-none transition-colors px-3 gap-2 text-text-body",
sizeClasses[size],
"[&>span]:line-clamp-1 whitespace-nowrap",
// Default state (when no error/success)
@@ -147,7 +147,7 @@ const SelectContent = React.forwardRef<
{/* Header Section */}
-
+
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index 70585e2c..dae2740d 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -208,7 +208,7 @@ export default function Home() {
- )}
+ )}
{chatStore.tasks[chatStore.activeTaskId as string]
?.activeWorkSpace === "workflow" && (
diff --git a/src/pages/Setting/MCP.tsx b/src/pages/Setting/MCP.tsx
index 81949941..ea04991f 100644
--- a/src/pages/Setting/MCP.tsx
+++ b/src/pages/Setting/MCP.tsx
@@ -590,7 +590,7 @@ export default function SettingMCP() {
return (
{/* Header Section */}
-
+
{showMarket ? (
diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts
index 2b475693..6cc93c98 100644
--- a/src/store/chatStore.ts
+++ b/src/store/chatStore.ts
@@ -159,6 +159,8 @@ const resolveProcessTaskIdForToolkitEvent = (
// Throttle streaming decompose text updates to prevent excessive re-renders
const streamingDecomposeTextBuffer: Record = {};
const streamingDecomposeTextTimers: Record> = {};
+// TTFT (Time to First Token) tracking for task decomposition
+const ttftTracking: Record = {};
const chatStore = (initial?: Partial) => createStore()(
(set, get) => ({
@@ -213,11 +215,11 @@ const chatStore = (initial?: Partial) => createStore()(
computedProgressValue(taskId: string) {
const { tasks, setProgressValue, activeTaskId } = get()
const taskRunning = [...tasks[taskId].taskRunning]
- const finshedTask = taskRunning?.filter(
+ const finishedTask = taskRunning?.filter(
(task) => task.status === "completed" || task.status === "failed"
).length;
const taskProgress = (
- ((finshedTask || 0) / (taskRunning?.length || 0)) *
+ ((finishedTask || 0) / (taskRunning?.length || 0)) *
100
).toFixed(2);
setProgressValue(
@@ -757,6 +759,11 @@ const chatStore = (initial?: Partial) => createStore()(
//Enable it for the rest of current SSE session
skipFirstConfirm = false;
+
+ // Record confirmed time for TTFT tracking
+ const ttftTaskId = getCurrentTaskId();
+ ttftTracking[ttftTaskId] = { confirmedAt: performance.now(), firstTokenLogged: false };
+ console.log(`[TTFT] Task ${ttftTaskId} confirmed at ${new Date().toISOString()}, starting TTFT measurement`);
return
}
@@ -796,6 +803,13 @@ const chatStore = (initial?: Partial) => createStore()(
const text = content;
const currentId = getCurrentTaskId();
+ // Log TTFT (Time to First Token) on first decompose_text event
+ if (ttftTracking[currentId] && !ttftTracking[currentId].firstTokenLogged) {
+ ttftTracking[currentId].firstTokenLogged = true;
+ const ttft = performance.now() - ttftTracking[currentId].confirmedAt;
+ console.log(`%c[TTFT] 🚀 Time to First Token: ${ttft.toFixed(2)}ms - First streaming token received for task ${currentId}`, 'color: #4CAF50; font-weight: bold');
+ }
+
// Get current buffer or task state
const currentContent = streamingDecomposeTextBuffer[currentId] ||
getCurrentChatStore().tasks[currentId]?.streamingDecomposeText || "";
@@ -829,6 +843,8 @@ const chatStore = (initial?: Partial) => createStore()(
if (agentMessages.step === "to_sub_tasks") {
// Clear streaming decompose text when task splitting is done
clearStreamingDecomposeText(currentTaskId);
+ // Clean up TTFT tracking
+ delete ttftTracking[currentTaskId];
// Check if this is a multi-turn scenario after task completion
const isMultiTurnAfterCompletion = tasks[currentTaskId].status === 'finished';
@@ -1056,7 +1072,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/src/style/token.css b/src/style/token.css
index 137ce863..6dab9fbb 100644
--- a/src/style/token.css
+++ b/src/style/token.css
@@ -446,7 +446,7 @@
--text-developer: var(--colors-emerald-default);
--text-multimodal: var(--colors-fuchsia-default);
--text-on-hover: var(--colors-primary-2);
- --surface-primary: var(--colors-off-white-80);
+ --surface-primary: var(--colors-off-white-50);
--surface-secondary: var(--colors-off-white-50);
--surface-success: var(--colors-green-50);
--surface-information: var(--colors-blue-50);
diff --git a/tailwind.config.js b/tailwind.config.js
index cb0e2014..29f90933 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,7 +1,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
- content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}", "./.storybook/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
diff --git a/test/integration/chatStore/deadWorkforce.test.tsx b/test/integration/chatStore/deadWorkforce.test.tsx
index 8e362187..e5563530 100644
--- a/test/integration/chatStore/deadWorkforce.test.tsx
+++ b/test/integration/chatStore/deadWorkforce.test.tsx
@@ -172,7 +172,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;
@@ -392,7 +392,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())
diff --git a/test/mocks/sse.mock.ts b/test/mocks/sse.mock.ts
index eb8ff527..b56a0fb7 100644
--- a/test/mocks/sse.mock.ts
+++ b/test/mocks/sse.mock.ts
@@ -324,4 +324,4 @@ export const issue619SseSequence = [
},
delay: 1100
}
-];
+];
\ No newline at end of file