Merge branch 'main' into fix_workforce_subagent_summary

This commit is contained in:
Wendong-Fan 2026-01-23 11:07:34 +08:00 committed by GitHub
commit 364dec473d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 584 additions and 322 deletions

View file

@ -12,7 +12,7 @@ body:
id: version
attributes:
label: What version of eigent are you using?
placeholder: E.g., 0.0.80
placeholder: E.g., 0.0.81
validations:
required: true

View file

@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
include:
- os: [self-hosted, macOS, ARM64]
- os: macos-latest
arch: arm64
artifact_name: macos-arm64
- os: windows-latest
@ -85,7 +85,13 @@ jobs:
- name: Build Release Files (macOS with signing)
if: runner.os == 'macOS'
timeout-minutes: 90
run: npm run build -- --arch ${{ matrix.arch }}
run: |
# Increase file descriptor limit to prevent EMFILE errors during signing
# This is needed because electron-builder signs all files recursively,
# and Python venvs contain thousands of files
ulimit -n 65536 || ulimit -n 10240
echo "File descriptor limit set to: $(ulimit -n)"
npm run build -- --arch ${{ matrix.arch }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.CERT_P12 }}

View file

@ -92,7 +92,11 @@ jobs:
# Step for macOS builds with signing
- name: Build Release Files (macOS with signing)
if: runner.os == 'macOS'
run: npm run build -- --arch ${{ matrix.arch }}
run: |
# Increase file descriptor limit to prevent EMFILE errors during signing
ulimit -n 65536 || ulimit -n 10240
echo "File descriptor limit set to: $(ulimit -n)"
npm run build -- --arch ${{ matrix.arch }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.CERT_P12 }}

View file

@ -28,7 +28,7 @@
</div>
<br/>
**Eigent** is the open source cowork desktop application, empowering you to build, manage, and deploy a custom AI workforce that can turn your most complex workflows into automated tasks.
**Eigent** is the open source Cowork desktop application, empowering you to build, manage, and deploy a custom AI workforce that can turn your most complex workflows into automated tasks. As a leading open-source Cowork product, Eigent brings together the best of open-source collaboration and AI-powered automation.
Built on [CAMEL-AI][camel-site]'s acclaimed open-source project, our system introduces a **Multi-Agent Workforce** that **boosts productivity** through parallel execution, customization, and privacy protection.
@ -51,23 +51,23 @@ Built on [CAMEL-AI][camel-site]'s acclaimed open-source project, our system intr
#### TOC
- [🚀 Getting Started](#-getting-started)
- [🚀 Getting Started with Open Source Cowork](#-getting-started-with-open-source-Cowork)
- [🏠 Local Deployment (Recommended)](#-local-deployment-recommended)
- [⚡ Quick Start (Cloud-Connected)](#-quick-start-cloud-connected)
- [🏢 Enterprise](#-enterprise)
- [☁️ Cloud Version](#-cloud-version)
- [✨ Key features](#-key-features)
- [✨ Key features - Open Source Cowork](#-key-features---open-source-Cowork)
- [🏭 Workforce](#-workforce)
- [🧠 Comprehensive Model Support](#-comprehensive-model-support)
- [🔌 MCP Tools Integration (MCP)](#-mcp-tools-integration-mcp)
- [✋ Human-in-the-Loop](#-human-in-the-loop)
- [👐 100% Open Source](#-100-open-source)
- [🧩 Use Cases](#-use-cases)
- [🧩 Use Cases - Open Source Cowork](#-use-cases---open-source-Cowork)
- [🛠️ Tech Stack](#-tech-stack)
- [Backend](#backend)
- [Frontend](#frontend)
- [🌟 Staying ahead](#staying-ahead)
- [🗺️ Roadmap](#-roadmap)
- [🌟 Staying ahead - Open Source Cowork](#-staying-ahead---open-source-Cowork)
- [🗺️ Roadmap - Open Source Cowork](#-roadmap---open-source-Cowork)
- [📖 Contributing](#-contributing)
- [Main Contributors](#main-contributors)
- [Distinguished amabssador](#distinguished-amabssador)
@ -81,7 +81,7 @@ Built on [CAMEL-AI][camel-site]'s acclaimed open-source project, our system intr
</details>
## **🚀 Getting Started**
## **🚀 Getting Started with Open Source Cowork**
> **🔓 Build in Public** — Eigent is **100% open source** from day one. Every feature, every commit, every decision is transparent. We believe the best AI tools should be built openly with the community, not behind closed doors.
@ -155,11 +155,11 @@ For teams who prefer managed infrastructure, we also offer a cloud platform. The
<a href="https://www.eigent.ai/download">Get started at Eigent.ai →</a>
</div>
## **✨ Key features**
Unlock the full potential of exceptional productivity with Eigents powerful features—built for seamless integration, smarter task execution, and boundless automation.
## **✨ Key features - Open Source Cowork**
Unlock the full potential of exceptional productivity with Eigent's open-source Cowork powerful features — built for seamless integration, smarter task execution, and boundless automation.
### 🏭 Workforce
Employs a team of specialized AI agents that collaborate to solve complex tasks. Eigent dynamically breaks down tasks and activates multiple agents to work **in parallel.**
Employs a team of specialized AI agents that collaborate to solve complex tasks. Eigent's open-source Cowork dynamically breaks down tasks and activates multiple agents to work **in parallel.**
Eigent pre-defined the following agent workers:
@ -173,7 +173,7 @@ Eigent pre-defined the following agent workers:
<br/>
### 🧠 Comprehensive Model Support
Deploy Eigent locally with your preferred models.
Deploy the Eigent open-source Cowork desktop locally with your preferred models.
![Model](https://eigent-ai.github.io/.github/assets/gif/feature_local_model.gif)
@ -200,7 +200,9 @@ Eigent is completely open-sourced. You can download, inspect, and modify the cod
<br/>
## 🧩 Use Cases
## 🧩 Use Cases - Open Source Cowork
Discover how developers worldwide leverage Eigent's open-source Cowork capabilities to automate complex workflows and boost productivity across diverse industries.
### 1. Palm Springs Tennis Trip Itinerary with Slack Summary [Replay ▶️](https://www.eigent.ai/download?share_token=IjE3NTM0MzUxNTEzMzctNzExMyI.aIeysw.MUeG6ZcBxI1GqvPDvn4dcv-CDWw__1753435151337-7113)
@ -279,6 +281,8 @@ Please add this signature image to the Signature Areas in the PDF. You could ins
## 🛠️ Tech Stack
Eigent open-source Cowork desktop is built on modern, reliable technologies that ensure scalability, performance, and extensibility.
### Backend
- **Framework:** FastAPI
- **Package Manager:** uv
@ -295,7 +299,7 @@ Please add this signature image to the Signature Areas in the PDF. You could ins
- **State Management:** Zustand
- **Flow Editor:** React Flow
## 🌟 Staying ahead
## 🌟 Staying ahead - Open Source Cowork
> \[!IMPORTANT]
>
@ -303,7 +307,9 @@ Please add this signature image to the Signature Areas in the PDF. You could ins
![][image-star-us]
## 🗺️ Roadmap
## 🗺️ Roadmap - Open Source Cowork
Our open-source Cowork continues to evolve with input from the community. Here's what's coming next:
| Topics | Issues | Discord Channel |
| ------------------------ | -- |-- |

View file

@ -28,7 +28,7 @@
</div>
<br/>
**Eigent** 是全球首个 **多智能体工作流** 桌面应用程序,帮助您构建、管理和部署定制化的 AI 工作团队,将最复杂的工作流程转化为自动化任务。
**Eigent** 是全球首个 **多智能体工作流** 桌面应用程序,帮助您构建、管理和部署定制化的 AI 工作团队,将最复杂的工作流程转化为自动化任务。作为领先的开源 Cowork产品Eigent融合了开源 Cowork 和AI驱动自动化的优势。
基于 [CAMEL-AI][camel-site] 广受赞誉的开源项目,我们的系统引入了 **多智能体工作流**,通过并行执行、定制化和隐私保护 **提升生产力**
@ -51,22 +51,22 @@
#### 目录
- [🚀 快速开始](#-快速开始)
- [🚀 快速开始 - 开源 Cowork](#-快速开始---开源 Cowork)
- [☁️ 云版本](#-云版本)
- [🏠 自托管(社区版)](#-自托管社区版)
- [🏢 企业版](#-企业版)
- [✨ 核心功能](#-核心功能)
- [✨ 核心功能 - 开源 Cowork](#-核心功能---开源 Cowork)
- [🏭 工作流](#-工作流)
- [🧠 全面模型支持](#-全面模型支持)
- [🔌 MCP 工具集成](#-mcp-工具集成)
- [✋ 人工介入](#-人工介入)
- [👐 100% 开源](#-100-开源)
- [🧩 使用案例](#-使用案例)
- [🧩 使用案例 - 开源 Cowork](#-使用案例---开源 Cowork)
- [🛠️ 技术栈](#-技术栈)
- [后端](#后端)
- [前端](#前端)
- [🌟 保持领先](#保持领先)
- [🗺️ 路线图](#-路线图)
- [🌟 保持领先 - 开源 Cowork](#保持领先---开源 Cowork)
- [🗺️ 路线图 - 开源 Cowork](#-路线图---开源 Cowork)
- [📖 贡献](#-贡献)
- [核心贡献者](#核心贡献者)
- [杰出大使](#杰出大使)
@ -80,7 +80,7 @@
</details>
## **🚀 快速开始**
## **🚀 快速开始 - 开源 Cowork**
有三种方式开始使用 Eigent
@ -148,11 +148,11 @@ uv sync
📧 更多详情,请联系 [info@eigent.ai](mailto:info@eigent.ai)。
## **✨ 核心功能**
通过 Eigent 的强大功能释放卓越生产力的全部潜力——专为无缝集成、智能任务执行和无边界自动化而设计。
## **✨ 核心功能 - 开源 Cowork**
通过 Eigent 开源 Cowork的强大功能释放卓越生产力的全部潜力——专为无缝集成、智能任务执行和无边界自动化而设计。
### 🏭 工作流
部署一支专业 AI 智能体团队协作解决复杂任务。Eigent 动态分解任务并激活多个智能体 **并行工作**
### 🏭 工作流
部署一支专业 AI 智能体团队协作解决复杂任务。Eigent 开源 Cowork动态分解任务并激活多个智能体 **并行工作**
Eigent 预定义了以下智能体工作者:
@ -165,8 +165,8 @@ Eigent 预定义了以下智能体工作者:
<br/>
### 🧠 全面模型支持
使用您偏好的模型本地部署 Eigent。
### 🧠 全面模型支持
使用您偏好的模型本地部署 Eigent 开源 Cowork桌面应用
![Model](https://eigent-ai.github.io/.github/assets/gif/feature_local_model.gif)
@ -193,7 +193,9 @@ Eigent 完全开源。您可以下载、检查和修改代码,确保透明度
<br/>
## 🧩 使用案例
## 🧩 使用案例 - 开源 Cowork
了解全球开发者如何利用 Eigent 的开源 Cowork能力在各行各业自动化复杂工作流程并提升生产力。
### 1. 棕榈泉网球旅行行程与 Slack 摘要 [回放 ▶️](https://www.eigent.ai/download?share_token=IjE3NTM0MzUxNTEzMzctNzExMyI.aIeysw.MUeG6ZcBxI1GqvPDvn4dcv-CDWw__1753435151337-7113)
@ -267,6 +269,8 @@ Eigent 完全开源。您可以下载、检查和修改代码,确保透明度
## 🛠️ 技术栈
Eigent 开源 Cowork桌面应用基于现代、可靠的技术构建确保可扩展性、性能和可扩展性。
### 后端
- **框架:** FastAPI
- **包管理器:** uv
@ -282,7 +286,7 @@ Eigent 完全开源。您可以下载、检查和修改代码,确保透明度
- **状态管理:** Zustand
- **流程编辑器:** React Flow
## 🌟 保持领先
## 🌟 保持领先 - 开源 Cowork
> \[!重要]
>
@ -290,7 +294,9 @@ Eigent 完全开源。您可以下载、检查和修改代码,确保透明度
![][image-star-us]
## 🗺️ 路线图
## 🗺️ 路线图 - 开源 Cowork
我们的开源 Cowork将继续在社区的参与下发展。以下是接下来的计划
| 主题 | 问题 | Discord 频道 |
| ------------------------ | -- |-- |
@ -304,7 +310,7 @@ Eigent 完全开源。您可以下载、检查和修改代码,确保透明度
## [🤝 贡献][contribution-link]
我们相信通过开源协作建立信任。您的创意贡献将推动 `Eigent` 的创新。探索我们的 GitHub 问题与项目,加入我们 🤝❤️ [贡献指南][contribution-link]
我们相信通过开源 Cowork建立信任。您的创意贡献将推动 `Eigent` 的创新。探索我们的 GitHub 问题与项目,加入我们 🤝❤️ [贡献指南][contribution-link]
## Contributors

View file

@ -28,7 +28,7 @@
</div>
<br/>
**Eigent**は、オープンソースのコワークデスクトップアプリケーションです。複雑なワークフローを自動化タスクに変換できるカスタムAIワークフォースを構築、管理、デプロイする力を提供します。
**Eigent**は、オープンソースのコワークデスクトップアプリケーションです。複雑なワークフローを自動化タスクに変換できるカスタムAIワークフォースを構築、管理、デプロイする力を提供します。先進的なオープンソース Cowork製品として、EigentはオープンソースコラボレーションとAI駆動の自動化の最良の部分を組み合わせています。
[CAMEL-AI][camel-site]の評価の高いオープンソースプロジェクトを基盤として構築されており、**マルチエージェントワークフォース**を導入し、並列実行、カスタマイズ、プライバシー保護を通じて**生産性を向上**させます。
@ -51,23 +51,23 @@
#### TOC
- [🚀 はじめに](#-はじめに)
- [🚀 はじめに - オープンソース Cowork](#-はじめに---オープンソース Cowork)
- [🏠 ローカルデプロイメント(推奨)](#-ローカルデプロイメント推奨)
- [⚡ クイックスタート(クラウド接続)](#-クイックスタートクラウド接続)
- [🏢 エンタープライズ](#-エンタープライズ)
- [☁️ クラウドバージョン](#-クラウドバージョン)
- [✨ 主な機能](#-主な機能)
- [✨ 主な機能 - オープンソース Cowork](#-主な機能---オープンソース Cowork)
- [🏭 ワークフォース](#-ワークフォース)
- [🧠 包括的なモデルサポート](#-包括的なモデルサポート)
- [🔌 MCPツール統合](#-mcpツール統合)
- [✋ ヒューマンインザループ](#-ヒューマンインザループ)
- [👐 100%オープンソース](#-100オープンソース)
- [🧩 ユースケース](#-ユースケース)
- [🧩 ユースケース - オープンソース Cowork](#-ユースケース---オープンソース Cowork)
- [🛠️ 技術スタック](#-技術スタック)
- [バックエンド](#バックエンド)
- [フロントエンド](#フロントエンド)
- [🌟 最新情報を入手](#最新情報を入手)
- [🗺️ ロードマップ](#-ロードマップ)
- [🌟 最新情報を入手 - オープンソース Cowork](#最新情報を入手---オープンソース Cowork)
- [🗺️ ロードマップ - オープンソース Cowork](#-ロードマップ---オープンソース Cowork)
- [📖 コントリビューション](#-コントリビューション)
- [エコシステム](#エコシステム)
- [📄 オープンソースライセンス](#-オープンソースライセンス)
@ -79,7 +79,7 @@
</details>
## **🚀 はじめに**
## **🚀 はじめに - オープンソース Cowork**
> **🔓 オープンに開発** — Eigentは初日から**100%オープンソース**です。すべての機能、すべてのコミット、すべての決定が透明です。最高のAIツールは、閉じられたドアの後ろではなく、コミュニティと共にオープンに構築されるべきだと信じています。
@ -153,11 +153,11 @@ uv sync
<a href="https://www.eigent.ai/download">Eigent.aiで始める →</a>
</div>
## **✨ 主な機能**
Eigentの強力な機能で卓越した生産性の可能性を最大限に引き出しましょう — シームレスな統合、よりスマートなタスク実行、無限の自動化のために構築されています。
## **✨ 主な機能 - オープンソース Cowork**
Eigentのオープンソース Coworkの強力な機能で卓越した生産性の可能性を最大限に引き出しましょう — シームレスな統合、よりスマートなタスク実行、無限の自動化のために構築されています。
### 🏭 ワークフォース
複雑なタスクを解決するために協力する専門AIエージェントのチームを活用します。Eigentは動的にタスクを分解し、複数のエージェントを**並列で**動作させます。
複雑なタスクを解決するために協力する専門AIエージェントのチームを活用します。Eigentのオープンソース Coworkは動的にタスクを分解し、複数のエージェントを**並列で**動作させます。
Eigentは以下のエージェントワーカーを事前定義しています
@ -171,7 +171,7 @@ Eigentは以下のエージェントワーカーを事前定義しています
<br/>
### 🧠 包括的なモデルサポート
お好みのモデルでEigentをローカルにデプロイできます。
お好みのモデルでEigent オープンソース Coworkデスクトップをローカルにデプロイできます。
![Model](https://eigent-ai.github.io/.github/assets/gif/feature_local_model.gif)
@ -198,7 +198,9 @@ Eigentは完全にオープンソースです。コードをダウンロード
<br/>
## 🧩 ユースケース
## 🧩 ユースケース - オープンソース Cowork
世界中の開発者がEigentのオープンソース Cowork機能を活用して、さまざまな業界で複雑なワークフローを自動化し、生産性を向上させている方法をご覧ください。
### 1. パームスプリングステニス旅行の旅程とSlackサマリー [リプレイ ▶️](https://www.eigent.ai/download?share_token=IjE3NTM0MzUxNTEzMzctNzExMyI.aIeysw.MUeG6ZcBxI1GqvPDvn4dcv-CDWw__1753435151337-7113)
@ -277,6 +279,8 @@ Documentsディレクトリにmydocsというフォルダがあります。ス
## 🛠️ 技術スタック
Eigent オープンソース Coworkデスクトップは、スケーラビリティ、パフォーマンス、拡張性を確保する最新の信頼性の高いテクロジーで構築されています。
### バックエンド
- **フレームワーク:** FastAPI
- **パッケージマネージャー:** uv
@ -293,7 +297,7 @@ Documentsディレクトリにmydocsというフォルダがあります。ス
- **状態管理:** Zustand
- **フローエディター:** React Flow
## 🌟 最新情報を入手
## 🌟 最新情報を入手 - オープンソース Cowork
> \[!IMPORTANT]
>
@ -301,7 +305,9 @@ Documentsディレクトリにmydocsというフォルダがあります。ス
![][image-star-us]
## 🗺️ ロードマップ
## 🗺️ ロードマップ - オープンソース Cowork
私たちのオープンソース Coworkはコミュニティからのフィードバックを取り入れながら進化を続けています。次に予定されている内容は以下の通りです
| トピック | 課題 | Discordチャンネル |
| ------------------------ | -- |-- |

View file

@ -28,7 +28,7 @@
</div>
<br/>
**Eigent** é a aplicação desktop cowork open source que capacita você a construir, gerenciar e implantar uma força de trabalho de IA personalizada, capaz de transformar seus fluxos de trabalho mais complexos em tarefas automatizadas.
**Eigent** é a aplicação desktop Cowork código aberto que capacita você a construir, gerenciar e implantar uma força de trabalho de IA personalizada, capaz de transformar seus fluxos de trabalho mais complexos em tarefas automatizadas. Como um produto líder de Cowork código aberto, o Eigent reúne o melhor da colaboração open source e da automação impulsionada por IA.
Construído sobre o aclamado projeto open source da [CAMEL-AI][camel-site], nosso sistema introduz uma **Força de Trabalho Multiagente** que **aumenta a produtividade** por meio de execução paralela, personalização e proteção de privacidade.
@ -51,23 +51,23 @@ Construído sobre o aclamado projeto open source da [CAMEL-AI][camel-site], noss
#### TOC
- [🚀 Primeiros Passos](#-primeiros-passos)
- [🚀 Primeiros Passos com Cowork Open Source](#-primeiros-passos-com-Cowork-open-source)
- [🏠 Implantação Local (Recomendado)](#-implantação-local-recomendado)
- [⚡ Início Rápido (Conectado à Nuvem)](#-início-rápido-conectado-à-nuvem)
- [🏢 Empresarial](#-empresarial)
- [☁️ Versão em Nuvem](#-versão-em-nuvem)
- [✨ Principais Recursos](#-principais-recursos)
- [✨ Principais Recursos - Cowork Open Source](#-principais-recursos---Cowork-open-source)
- [🏭 Força de Trabalho](#-força-de-trabalho)
- [🧠 Suporte Abrangente a Modelos](#-suporte-abrangente-a-modelos)
- [🔌 Integração de Ferramentas MCP (MCP)](#-integração-de-ferramentas-mcp-mcp)
- [✋ Humano no Circuito](#-humano-no-circuito)
- [👐 100% Código Aberto](#-100-código-aberto)
- [🧩 Casos de Uso](#-casos-de-uso)
- [🧩 Casos de Uso - Cowork Open Source](#-casos-de-uso---Cowork-open-source)
- [🛠️ Stack Tecnológica](#-stack-tecnológica)
- [Backend](#backend)
- [Frontend](#frontend)
- [🌟 Mantendo-se à Frente](#-mantendo-se-à-frente)
- [🗺️ Roadmap](#-roadmap)
- [🌟 Mantendo-se à Frente - Cowork Open Source](#-mantendo-se-à-frente---Cowork-open-source)
- [🗺️ Roadmap - Cowork Open Source](#-roadmap---Cowork-open-source)
- [🤝 Contribuição](#-contribuição)
- [Contribuidores](#contribuidores)
- [❤️ Patrocínio](#-patrocínio)
@ -80,7 +80,7 @@ Construído sobre o aclamado projeto open source da [CAMEL-AI][camel-site], noss
</details>
## **🚀 Primeiros Passos**
## **🚀 Primeiros Passos com Cowork Open Source**
> **🔓 Construído em Público** — Eigent é **100% open source** desde o primeiro dia. Cada funcionalidade, cada commit e cada decisão são transparentes. Acreditamos que as melhores ferramentas de IA devem ser construídas abertamente com a comunidade, e não a portas fechadas.
@ -154,11 +154,11 @@ Para equipes que preferem infraestrutura gerenciada, também oferecemos uma plat
<a href="https://www.eigent.ai/download">Comece em Eigent.ai →</a>
</div>
## **✨ Principais recursos**
Desbloqueie todo o potencial de produtividade excepcional com os poderosos recursos do Eigent—construídos para integração perfeita, execução de tarefas mais inteligente e automação ilimitada.
## **✨ Principais recursos - Cowork Open Source**
Desbloqueie todo o potencial de produtividade excepcional com os poderosos recursos do Eigent Cowork código aberto—construídos para integração perfeita, execução de tarefas mais inteligente e automação ilimitada.
### 🏭 Força de Trabalho
Emprega uma equipe de agentes de IA especializados que colaboram para resolver tarefas complexas. O Eigent divide dinamicamente as tarefas e ativa múltiplos agentes para trabalhar **em paralelo.**
Emprega uma equipe de agentes de IA especializados que colaboram para resolver tarefas complexas. O Eigent Cowork código aberto divide dinamicamente as tarefas e ativa múltiplos agentes para trabalhar **em paralelo.**
O Eigent pré-definiu os seguintes agentes trabalhadores:
@ -172,7 +172,7 @@ O Eigent pré-definiu os seguintes agentes trabalhadores:
<br/>
### 🧠 Suporte Abrangente a Modelos
Implante o Eigent localmente com seus modelos preferidos.
Implante o desktop Eigent Cowork código aberto localmente com seus modelos preferidos.
![Model](https://eigent-ai.github.io/.github/assets/gif/feature_local_model.gif)
@ -199,7 +199,9 @@ O Eigent é completamente de código aberto. Você pode baixar, inspecionar e mo
<br/>
## 🧩 Casos de Uso
## 🧩 Casos de Uso - Cowork Open Source
Descubra como desenvolvedores em todo o mundo aproveitam as capacidades de Cowork código aberto do Eigent para automatizar fluxos de trabalho complexos e aumentar a produtividade em diversos setores.
### 1. Itinerário de Viagem de Tênis em Palm Springs com Resumo no Slack [Replay ▶️](https://www.eigent.ai/download?share_token=IjE3NTM0MzUxNTEzMzctNzExMyI.aIeysw.MUeG6ZcBxI1GqvPDvn4dcv-CDWw__1753435151337-7113)
@ -274,6 +276,8 @@ Por favor, adicione esta imagem de assinatura às áreas de assinatura no PDF. V
## 🛠️ Stack Tecnológica
O desktop Eigent Cowork código aberto é construído com tecnologias modernas e confiáveis que garantem escalabilidade, desempenho e extensibilidade.
### Backend
- **Framework:** FastAPI
- **Gerenciador de Pacotes:** uv
@ -290,7 +294,7 @@ Por favor, adicione esta imagem de assinatura às áreas de assinatura no PDF. V
- **Gerenciamento de Estado:** Zustand
- **Editor de Fluxo:** React Flow
## 🌟 Mantendo-se à Frente
## 🌟 Mantendo-se à Frente - Cowork Open Source
> \[!IMPORTANT]
>
@ -298,7 +302,9 @@ Por favor, adicione esta imagem de assinatura às áreas de assinatura no PDF. V
![][image-star-us]
## 🗺️ Roadmap
## 🗺️ Roadmap - Cowork Open Source
Nosso Cowork código aberto continua a evoluir com feedback da comunidade. Aqui está o que vem a seguir:
| Tópicos | Issues | Canal do Discord |
| ------------------------- | -- |-- |

View file

@ -21,7 +21,7 @@ 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"
APP_VERSION = "0.0.81"
def get_terminal_base_venv_path() -> str:

View file

@ -6,7 +6,7 @@ readme = "README.md"
requires-python = ">=3.10,<3.11"
dependencies = [
"pip>=23.0",
"camel-ai[eigent]==0.2.84",
"camel-ai[eigent]==0.2.85a0",
"fastapi>=0.115.12",
"fastapi-babel>=1.0.0",
"uvicorn[standard]>=0.34.2",

8
backend/uv.lock generated
View file

@ -261,7 +261,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiofiles", specifier = ">=24.1.0" },
{ name = "camel-ai", extras = ["eigent"], specifier = "==0.2.84" },
{ name = "camel-ai", extras = ["eigent"], specifier = "==0.2.85a0" },
{ name = "debugpy", specifier = ">=1.8.17" },
{ name = "fastapi", specifier = ">=0.115.12" },
{ name = "fastapi-babel", specifier = ">=1.0.0" },
@ -337,7 +337,7 @@ wheels = [
[[package]]
name = "camel-ai"
version = "0.2.84"
version = "0.2.85a0"
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/9b/62/96d922750b304ab2dd0ac1ef35dd3fbbf0e9a44f147e08a1ddfeb94c51c6/camel_ai-0.2.84.tar.gz", hash = "sha256:173c79755fc986e3fa8e27523606222c5f8816fc085abb24831912dcd4a0dec3", size = 1125724, upload-time = "2026-01-20T17:23:13.524Z" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ab/7d305f80e868a60c7097ab510063a171e1798d163b5f8fd7fe7c16553e13/camel_ai-0.2.85a0.tar.gz", hash = "sha256:432de9bac1e40bd4ebf434ca80eaf3993121f87924820e26ad2bad69c1fb5cf5", size = 1126159, upload-time = "2026-01-23T02:24:08.868Z" }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/ea/0c/35d73b5d648413844bdfeaf95172a6b7c19802150829f5f907753a773d19/camel_ai-0.2.85a0-py3-none-any.whl", hash = "sha256:6045e9af72fee918ca3acc92f3b4af8af084af7b0cf6435c01a1252bd04ae6b3", size = 1599866, upload-time = "2026-01-23T02:24:06.78Z" },
]
[package.optional-dependencies]

View file

@ -65,6 +65,26 @@
"hardenedRuntime": true,
"gatekeeperAssess": false,
"notarize": false,
"signIgnore": [
".*\\.py$",
".*\\.pyc$",
".*\\.pyo$",
".*\\.pyi$",
".*\\.typed$",
".*\\.txt$",
".*\\.md$",
".*\\.rst$",
".*\\.json$",
".*\\.yaml$",
".*\\.yml$",
".*\\.toml$",
".*\\.cfg$",
".*\\.ini$",
".*\\.csv$",
".*\\.html$",
".*\\.css$",
".*\\.map$"
],
"extendInfo": {
"CFBundleURLTypes": [
{

View file

@ -36,7 +36,6 @@ import axios from 'axios';
import FormData from 'form-data';
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');
@ -1277,7 +1276,9 @@ async function createWindow() {
frame: false,
show: false, // Don't show until content is ready to avoid white screen
transparent: true,
backgroundColor: '#00000000',
vibrancy: 'sidebar',
visualEffectState: 'active',
backgroundColor: '#f5f5f580',
titleBarStyle: isMac ? 'hidden' : undefined,
trafficLightPosition: isMac ? { x: 10, y: 10 } : undefined,
icon: path.join(VITE_PUBLIC, 'favicon.ico'),
@ -1295,28 +1296,6 @@ 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);
}
}
});
}
// ==================== Handle renderer crashes and failed loads ====================
win.webContents.on('render-process-gone', (event, details) => {
log.error('[RENDERER] Process gone:', details.reason, details.exitCode);

View file

@ -173,11 +173,6 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
if (!data) return;
const msg = data.toString().trimEnd();
// REMOVED: detectInstallationLogs(msg)
// Reason: Removed keyword-based detection to avoid false positives when backend
// outputs logs containing keywords like "Installing", "Updating", "Syncing" etc.
// Installation is now only handled through the explicit installation flow.
if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) {
log.error(`BACKEND: ${msg}`);
} else if (msg.toLowerCase().includes("warn")) {
@ -297,7 +292,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
cwd: backendPath,
env: env,
detached: process.platform !== 'win32',
stdio: ['ignore', 'pipe', 'pipe']
stdio: ['ignore', 'ignore', 'pipe'] // stdin=ignore, stdout=ignore, stderr=pipe (Python logs to stderr)
}
);
@ -415,13 +410,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
}, intervalMs);
};
node_process.stdout.on('data', (data) => {
log.debug(`Backend stdout received ${data.length} bytes`);
displayFilteredLogs(data);
});
node_process.stderr.on('data', (data) => {
log.debug(`Backend stderr received ${data.length} bytes`);
displayFilteredLogs(data);
if (data.toString().includes("Address already in use") ||

View file

@ -1,171 +0,0 @@
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;

View file

@ -1,6 +1,6 @@
{
"name": "eigent",
"version": "0.0.80",
"version": "0.0.81",
"main": "dist-electron/main/index.js",
"description": "Eigent",
"author": "Eigent.AI",

View file

@ -7,7 +7,7 @@ requires-python = ">=3.12,<3.13"
dependencies = [
"alembic>=1.15.2",
"openai>=1.99.3,<2",
"camel-ai==0.2.84",
"camel-ai==0.2.85a0",
"pydantic[email]>=2.11.1",
"click>=8.1.8",
"fastapi>=0.115.12",

8
server/uv.lock generated
View file

@ -145,7 +145,7 @@ wheels = [
[[package]]
name = "camel-ai"
version = "0.2.84"
version = "0.2.85a0"
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/9b/62/96d922750b304ab2dd0ac1ef35dd3fbbf0e9a44f147e08a1ddfeb94c51c6/camel_ai-0.2.84.tar.gz", hash = "sha256:173c79755fc986e3fa8e27523606222c5f8816fc085abb24831912dcd4a0dec3", size = 1125724, upload-time = "2026-01-20T17:23:13.524Z" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ab/7d305f80e868a60c7097ab510063a171e1798d163b5f8fd7fe7c16553e13/camel_ai-0.2.85a0.tar.gz", hash = "sha256:432de9bac1e40bd4ebf434ca80eaf3993121f87924820e26ad2bad69c1fb5cf5", size = 1126159, upload-time = "2026-01-23T02:24:08.868Z" }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/ea/0c/35d73b5d648413844bdfeaf95172a6b7c19802150829f5f907753a773d19/camel_ai-0.2.85a0-py3-none-any.whl", hash = "sha256:6045e9af72fee918ca3acc92f3b4af8af084af7b0cf6435c01a1252bd04ae6b3", size = 1599866, upload-time = "2026-01-23T02:24:06.78Z" },
]
[[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.84" },
{ name = "camel-ai", specifier = "==0.2.85a0" },
{ name = "click", specifier = ">=8.1.8" },
{ name = "convert-case", specifier = ">=1.2.3" },
{ name = "cryptography", specifier = ">=45.0.4" },

View file

@ -48,6 +48,7 @@ export interface BoxHeaderConfirmProps {
onStartTask?: () => void;
onEdit?: () => void;
className?: string;
loading?: boolean;
}
export const BoxHeaderConfirm = ({
@ -55,6 +56,7 @@ export const BoxHeaderConfirm = ({
onStartTask,
onEdit,
className,
loading = false,
}: BoxHeaderConfirmProps) => {
return (
<div
@ -88,6 +90,7 @@ export const BoxHeaderConfirm = ({
size="sm"
className="rounded-full"
onClick={onStartTask}
disabled={loading}
>
Start Task
</Button>

View file

@ -93,6 +93,7 @@ export default function BottomBox({
subtitle={subtitle}
onStartTask={onStartTask}
onEdit={onEdit}
loading={loading}
/>
)}

View file

@ -16,9 +16,10 @@ interface Props {
onOpenChange: (open: boolean) => void;
trigger?: React.ReactNode;
onConfirm: () => void;
loading?: boolean;
}
export default function EndNoticeDialog({ open, onOpenChange, trigger, onConfirm }: Props) {
export default function EndNoticeDialog({ open, onOpenChange, trigger, onConfirm, loading = false }: Props) {
const { t } = useTranslation();
const onSubmit = useCallback(() => {
onConfirm();
@ -36,9 +37,9 @@ export default function EndNoticeDialog({ open, onOpenChange, trigger, onConfirm
</div>
<DialogFooter className="bg-white-100% !rounded-b-xl p-md">
<DialogClose asChild>
<Button variant="ghost" size="md">{t("layout.cancel")}</Button>
<Button variant="ghost" size="md" disabled={loading}>{t("layout.cancel")}</Button>
</DialogClose>
<Button size="md" onClick={onSubmit} variant="cuation">{t("layout.yes-end-project")}</Button>
<Button size="md" onClick={onSubmit} variant="cuation" disabled={loading}>{t("layout.yes-end-project")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View file

@ -0,0 +1,48 @@
import { ZoomOut, ZoomIn, RotateCcw } from "lucide-react";
import { Button } from "../ui/button";
// Zoom Controls Component
interface ZoomControlsProps {
zoom: number;
onZoomIn: () => void;
onZoomOut: () => void;
onZoomReset: () => void;
}
export const ZoomControls = ({ zoom, onZoomIn, onZoomOut, onZoomReset }: ZoomControlsProps) => {
return (
<div className="absolute top-0 left-1/2 -translate-x-1/2 z-10 group">
<div className="flex items-center gap-1 px-3 py-1.5 bg-gray-100/90 backdrop-blur-xl rounded-full shadow-lg border border-gray-300/50 translate-y-[calc(-100%-8px)] group-hover:translate-y-[20px] transition-transform duration-300 ease-out">
<Button
size="icon"
variant="ghost"
onClick={onZoomOut}
title="Zoom Out"
className="h-7 w-7 hover:bg-gray-200/60 text-gray-700 hover:text-gray-900"
>
<ZoomOut className="w-3.5 h-3.5" />
</Button>
<span className="text-xs text-gray-800 min-w-[2.5rem] text-center font-medium tabular-nums">{zoom}%</span>
<Button
size="icon"
variant="ghost"
onClick={onZoomIn}
title="Zoom In"
className="h-7 w-7 hover:bg-gray-200/60 text-gray-700 hover:text-gray-900"
>
<ZoomIn className="w-3.5 h-3.5" />
</Button>
<div className="w-px h-4 bg-gray-300/60 mx-0.5" />
<Button
size="icon"
variant="ghost"
onClick={onZoomReset}
title="Reset Zoom"
className="h-7 w-7 hover:bg-gray-200/60 text-gray-700 hover:text-gray-900"
>
<RotateCcw className="w-3.5 h-3.5" />
</Button>
</div>
</div>
);
}

View file

@ -9,7 +9,9 @@ import {
Folder as FolderIcon,
ChevronRight,
ChevronDown,
AlertTriangle,
} from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import FolderComponent from './FolderComponent';
@ -19,6 +21,7 @@ import { proxyFetchGet } from '@/api/http';
import { useTranslation } from 'react-i18next';
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
import DOMPurify from 'dompurify';
import { ZoomControls } from './ZoomControls';
// Type definitions
interface FileTreeNode {
@ -221,6 +224,16 @@ export default function Folder({ data }: { data?: Agent }) {
setIsShowSourceCode(!isShowSourceCode);
};
// State for HTML script approval (lifted from HtmlRenderer)
const [htmlHasScripts, setHtmlHasScripts] = useState(false);
const [htmlScriptsApproved, setHtmlScriptsApproved] = useState(false);
// Reset script approval when file changes
useEffect(() => {
setHtmlScriptsApproved(false);
setHtmlHasScripts(false);
}, [selectedFile?.path]);
const [isCollapsed, setIsCollapsed] = useState(false);
const buildFileTree = (files: FileInfo[]): FileTreeNode => {
@ -235,17 +248,22 @@ export default function Folder({ data }: { data?: Agent }) {
nodeMap.set('', root);
const sortedFiles = [...files].sort((a, b) => {
const depthA = (a.relativePath || '').split('/').filter(Boolean).length;
const depthB = (b.relativePath || '').split('/').filter(Boolean).length;
// Normalize paths to use forward slashes for cross-platform compatibility
const normalizedPathA = (a.relativePath || '').replace(/\\/g, '/');
const normalizedPathB = (b.relativePath || '').replace(/\\/g, '/');
const depthA = normalizedPathA.split('/').filter(Boolean).length;
const depthB = normalizedPathB.split('/').filter(Boolean).length;
return depthA - depthB;
});
for (const file of sortedFiles) {
const fullRelativePath = file.relativePath
? `${file.relativePath}/${file.name}`
// Normalize paths to use forward slashes for cross-platform compatibility
const normalizedRelativePath = (file.relativePath || '').replace(/\\/g, '/');
const fullRelativePath = normalizedRelativePath
? `${normalizedRelativePath}/${file.name}`
: file.name;
const parentPath = file.relativePath || '';
const parentPath = normalizedRelativePath;
const parentNode = nodeMap.get(parentPath) || root;
const node: FileTreeNode = {
@ -530,6 +548,28 @@ export default function Folder({ data }: { data?: Agent }) {
<Download className="w-4 h-4 text-zinc-500" />
</Button>
</div>
{/* Safe to Run button for HTML files with scripts */}
{selectedFile?.type === 'html' && !isShowSourceCode && htmlHasScripts && !htmlScriptsApproved && (
<Button
size="sm"
variant="success"
className="flex-shrink-0"
onClick={() => {
setHtmlScriptsApproved(true);
toast.success(t('chat.scripts-approved'), {
duration: 3000,
});
}}
>
{t('chat.safe-to-run')}
</Button>
)}
{selectedFile?.type === 'html' && !isShowSourceCode && htmlScriptsApproved && htmlHasScripts && (
<span className="text-xs text-yellow-600 flex items-center gap-1 flex-shrink-0">
<AlertTriangle className="w-3 h-3" />
{t('chat.scripts-running')}
</span>
)}
<Button
variant="ghost"
size="icon"
@ -543,8 +583,8 @@ export default function Folder({ data }: { data?: Agent }) {
)}
{/* content */}
<div className="flex-1 overflow-y-auto min-h-0 scrollbar">
<div className="p-6 h-full">
<div className={`flex-1 min-h-0 ${selectedFile?.type === 'html' && !isShowSourceCode ? 'overflow-hidden' : 'overflow-y-auto scrollbar'}`}>
<div className={`h-full ${selectedFile?.type === 'html' && !isShowSourceCode ? '' : 'p-6'}`}>
{selectedFile ? (
!loading ? (
selectedFile.type === 'md' && !isShowSourceCode ? (
@ -568,7 +608,12 @@ export default function Folder({ data }: { data?: Agent }) {
isShowSourceCode ? (
<>{selectedFile.content}</>
) : (
<HtmlRenderer selectedFile={selectedFile} />
<HtmlRenderer
selectedFile={selectedFile}
projectFiles={fileGroups[0]?.files || []}
scriptsApproved={htmlScriptsApproved}
onScriptsDetected={setHtmlHasScripts}
/>
)
) : selectedFile.type === 'zip' ? (
<div className="flex items-center justify-center h-full text-zinc-500">
@ -660,19 +705,133 @@ function joinPath(...paths: string[]): string {
.replace(/\/+/g, '/');
}
// Helper function to resolve relative paths (handles ../ and ./)
function resolveRelativePath(basePath: string, relativePath: string): string {
// Normalize paths
const normalizedBase = basePath.replace(/\\/g, '/');
const normalizedRelative = relativePath.replace(/\\/g, '/');
// If it's not a relative path, return as-is
if (!normalizedRelative.startsWith('./') && !normalizedRelative.startsWith('../')) {
// It's a simple relative path like "script.js" or "js/script.js"
return joinPath(normalizedBase, normalizedRelative);
}
const baseParts = normalizedBase.split('/').filter(Boolean);
const relativeParts = normalizedRelative.split('/').filter(Boolean);
for (const part of relativeParts) {
if (part === '.') {
// Current directory, skip
continue;
} else if (part === '..') {
// Parent directory, go up one level
baseParts.pop();
} else {
// Regular path segment
baseParts.push(part);
}
}
return baseParts.join('/');
}
// Component to render HTML with relative image paths resolved
function HtmlRenderer({ selectedFile }: { selectedFile: FileInfo }) {
function HtmlRenderer({
selectedFile,
projectFiles,
scriptsApproved,
onScriptsDetected,
}: {
selectedFile: FileInfo;
projectFiles: FileInfo[];
scriptsApproved: boolean;
onScriptsDetected: (hasScripts: boolean) => void;
}) {
const { t } = useTranslation();
const [processedHtml, setProcessedHtml] = useState<string>('');
const [hasScripts, setHasScripts] = useState<boolean>(false);
const [rawHtmlWithScriptsCache, setRawHtmlWithScriptsCache] = useState<string>('');
const hasShownWarningRef = useRef<string | null>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
const processHtml = async () => {
if (!selectedFile.content) {
setProcessedHtml('');
setHasScripts(false);
return;
}
let html = selectedFile.content;
// Get the directory of the HTML file
const htmlDir = getDirPath(selectedFile.path);
// Parse HTML to find referenced JS and CSS files via relative paths
const scriptSrcRegex = /<script[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
const linkHrefRegex = /<link[^>]*href\s*=\s*["']([^"']+\.css)["'][^>]*>/gi;
const referencedPaths: Set<string> = new Set();
// Helper to extract and resolve paths
const addReferencedPath = (url: string) => {
if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('//')) {
const resolvedPath = resolveRelativePath(htmlDir, url);
referencedPaths.add(resolvedPath.toLowerCase());
}
};
// Extract script sources
let scriptMatch;
while ((scriptMatch = scriptSrcRegex.exec(html)) !== null) {
addReferencedPath(scriptMatch[1]);
}
// Extract CSS link hrefs
let linkMatch;
while ((linkMatch = linkHrefRegex.exec(html)) !== null) {
addReferencedPath(linkMatch[1]);
}
// Find matching files (exact path match only)
const relatedFiles = projectFiles.filter((file) => {
if (file.isFolder || !['js', 'css'].includes(file.type?.toLowerCase() || '')) return false;
const normalizedFilePath = file.path.replace(/\\/g, '/').toLowerCase();
return referencedPaths.has(normalizedFilePath);
});
const jsFiles = relatedFiles.filter((f) => f.type?.toLowerCase() === 'js');
const cssFiles = relatedFiles.filter((f) => f.type?.toLowerCase() === 'css');
// Detect inline scripts in HTML content (handle malformed closing tags like </script > or </script foo="bar">)
const inlineScriptRegex = /<script\b[^>]*>[\s\S]*?<\/\s*script\b[^>]*>/gi;
const hasInlineScripts = inlineScriptRegex.test(html);
// Detect inline event handlers (onclick, onload, etc.)
const eventHandlerRegex = /\s+on\w+\s*=\s*["'][^"']*["']/gi;
const hasEventHandlers = eventHandlerRegex.test(html);
const hasAnyScripts = jsFiles.length > 0 || hasInlineScripts || hasEventHandlers;
// Show warning if any scripts are found (external JS files, inline scripts, or event handlers)
if (hasAnyScripts && hasShownWarningRef.current !== selectedFile.path) {
hasShownWarningRef.current = selectedFile.path;
setHasScripts(true);
onScriptsDetected(true);
toast.warning(t('chat.scripts-warning'), {
duration: 5000,
icon: <AlertTriangle className="w-4 h-4" />,
});
} else if (!hasAnyScripts) {
setHasScripts(false);
onScriptsDetected(false);
} else if (hasAnyScripts) {
// Scripts exist but warning already shown for this file
setHasScripts(true);
onScriptsDetected(true);
}
// Strict dangerous content detection to prevent various bypass techniques
const dangerousPatterns = [
/ipcRenderer/gi,
@ -779,9 +938,6 @@ function HtmlRenderer({ selectedFile }: { selectedFile: FileInfo }) {
return;
}
// Get the directory of the HTML file
const htmlDir = getDirPath(selectedFile.path);
// Find all img tags with relative paths (match various formats)
const imgRegex = /<img\s+([^>]*?)(?:\s*\/\s*>|>)/gi;
const matches = Array.from(html.matchAll(imgRegex));
@ -848,7 +1004,70 @@ function HtmlRenderer({ selectedFile }: { selectedFile: FileInfo }) {
);
});
// Sanitize the processed HTML
// Load and inject CSS files, replacing external link tags
for (const cssFile of cssFiles) {
try {
const cssContent = await window.ipcRenderer.invoke(
'open-file',
'css',
cssFile.path,
false
);
if (cssContent) {
const styleTag = `<style data-source="${cssFile.name}">${cssContent}</style>`;
// Try to replace the external link tag with inline style
const linkRegex = new RegExp(
`<link[^>]*href=["'](?:[^"']*[/\\\\])?${cssFile.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["'][^>]*>`,
'gi'
);
const replacedCss = processedHtmlContent.replace(linkRegex, styleTag);
if (replacedCss !== processedHtmlContent) {
processedHtmlContent = replacedCss;
} else {
// Fallback: inject CSS at the beginning of the HTML
if (processedHtmlContent.includes('<head>')) {
processedHtmlContent = processedHtmlContent.replace(
'<head>',
`<head>${styleTag}`
);
} else {
processedHtmlContent = styleTag + processedHtmlContent;
}
}
}
} catch (error) {
console.error(`Failed to load CSS file: ${cssFile.path}`, error);
}
}
// Load JS files content and replace external script tags
for (const jsFile of jsFiles) {
try {
const jsContent = await window.ipcRenderer.invoke(
'open-file',
'js',
jsFile.path,
false
);
if (jsContent) {
// Replace external script tag with inline script
const scriptRegex = new RegExp(
`<script[^>]*src=["'](?:[^"']*[/\\\\])?${jsFile.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["'][^>]*>\\s*</script>`,
'gi'
);
const inlineScriptTag = `<script data-source="${jsFile.name}">${jsContent}</script>`;
processedHtmlContent = processedHtmlContent.replace(scriptRegex, inlineScriptTag);
}
} catch (error) {
console.error(`Failed to load JS file: ${jsFile.path}`, error);
}
}
// Store raw HTML with inline scripts for iframe rendering (before sanitization)
const rawHtmlWithScripts = processedHtmlContent;
// Sanitize the processed HTML (for non-iframe rendering)
const sanitized = DOMPurify.sanitize(processedHtmlContent, {
USE_PROFILES: { html: true },
ALLOWED_TAGS: [
@ -881,6 +1100,12 @@ function HtmlRenderer({ selectedFile }: { selectedFile: FileInfo }) {
'h5',
'h6',
'style',
'canvas',
'html',
'head',
'body',
'title',
'meta',
],
ALLOWED_ATTR: [
'href',
@ -928,16 +1153,75 @@ function HtmlRenderer({ selectedFile }: { selectedFile: FileInfo }) {
KEEP_CONTENT: false,
});
setProcessedHtml(sanitized);
// Inject JS content after sanitization (only if user is aware of scripts)
let finalHtml = sanitized;
// Cache raw HTML with scripts for iframe rendering (before sanitization stripped them)
setRawHtmlWithScriptsCache(rawHtmlWithScripts);
setProcessedHtml(finalHtml);
};
processHtml();
}, [selectedFile]);
}, [selectedFile, projectFiles, onScriptsDetected, t]);
// Zoom state and controls
const [zoom, setZoom] = useState(100);
const handleZoomIn = () => setZoom((prev) => Math.min(prev + 10, 200));
const handleZoomOut = () => setZoom((prev) => Math.max(prev - 10, 50));
const handleZoomReset = () => setZoom(100);
// Handle scroll wheel zoom (Ctrl+scroll or pinch)
const handleWheel = (e: React.WheelEvent) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
const delta = e.deltaY > 0 ? -10 : 10;
setZoom((prev) => Math.min(Math.max(prev + delta, 50), 200));
}
};
return (
<div
className="w-full overflow-auto"
dangerouslySetInnerHTML={{ __html: processedHtml }}
/>
<div className="w-full h-full flex flex-col relative">
{/* Floating notch-style zoom controls */}
<ZoomControls
zoom={zoom}
onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut}
onZoomReset={handleZoomReset}
/>
{/* Content area with zoom */}
<div
className={`flex-1 min-h-0 bg-zinc-100 ${scriptsApproved && hasScripts ? 'overflow-hidden' : 'overflow-auto'}`}
onWheel={handleWheel}
>
<div
className="origin-top-left transition-transform duration-150 h-full"
style={{
transform: `scale(${zoom / 100})`,
width: `${10000 / zoom}%`,
height: `${10000 / zoom}%`,
}}
>
{scriptsApproved && hasScripts ? (
<iframe
ref={iframeRef}
srcDoc={rawHtmlWithScriptsCache}
className="w-full h-full border-0 bg-white"
sandbox="allow-scripts allow-forms allow-same-origin"
title={selectedFile.name}
tabIndex={0}
onLoad={() => iframeRef.current?.focus()}
/>
) : (
<div
className="w-full bg-white p-4"
dangerouslySetInnerHTML={{ __html: processedHtml }}
/>
)}
</div>
</div>
</div>
);
}

View file

@ -48,6 +48,7 @@ function HeaderWin() {
const [isFullscreen, setIsFullscreen] = useState(false);
const { token } = getAuthStore();
const [endDialogOpen, setEndDialogOpen] = useState(false);
const [endProjectLoading, setEndProjectLoading] = useState(false);
useEffect(() => {
const p = window.electronAPI.getPlatform();
setPlatform(p);
@ -150,6 +151,7 @@ function HeaderWin() {
const historyId = projectId ? projectStore.getHistoryId(projectId) : null;
setEndProjectLoading(true);
try {
const task = chatStore.tasks[taskId];
@ -197,6 +199,7 @@ function HeaderWin() {
closeButton: true,
});
} finally {
setEndProjectLoading(false);
setEndDialogOpen(false);
}
};
@ -414,6 +417,7 @@ function HeaderWin() {
open={endDialogOpen}
onOpenChange={setEndDialogOpen}
onConfirm={handleEndProject}
loading={endProjectLoading}
/>
</div>
);

View file

@ -104,7 +104,7 @@ export function Node({ id, data }: NodeProps) {
if (!chatStore) {
return <div>Loading...</div>;
}
const { setCenter, getNode, setViewport, setNodes } = useReactFlow();
const workerList = useWorkerList();
const { setWorkerList } = useAuthStore();
@ -115,21 +115,47 @@ export function Node({ id, data }: NodeProps) {
setIsExpanded(data.isExpanded);
}, [data.isExpanded]);
// Auto-expand when a task is running with toolkits
useEffect(() => {
const runningTask = data.agent?.tasks?.find(
const tasks = data.agent?.tasks || [];
// Find running task with active toolkits
const runningTaskWithToolkits = tasks.find(
(task) =>
task.status === "running" && task.toolkits && task.toolkits.length > 0
task.status === "running" &&
task.toolkits &&
task.toolkits.length > 0
);
if (runningTask && runningTask.id !== lastAutoExpandedTaskIdRef.current) {
// Reset tracking when no tasks are running
const hasRunningTasks = tasks.some((task) => task.status === "running");
if (!hasRunningTasks && lastAutoExpandedTaskIdRef.current) {
lastAutoExpandedTaskIdRef.current = null;
}
// Auto-expand for new running task
if (runningTaskWithToolkits && runningTaskWithToolkits.id !== lastAutoExpandedTaskIdRef.current) {
// Always select the new task
setSelectedTask(runningTaskWithToolkits);
// Expand if not already expanded
if (!isExpanded) {
setIsExpanded(true);
data.onExpandChange(id, true);
setSelectedTask(runningTask);
}
lastAutoExpandedTaskIdRef.current = runningTask.id;
lastAutoExpandedTaskIdRef.current = runningTaskWithToolkits.id;
}
}, [data.agent?.tasks, id, data.onExpandChange, isExpanded]);
}, [
data.agent?.tasks,
// Add specific dependencies that actually change
data.agent?.tasks?.length,
data.agent?.tasks?.find((t) => t.status === "running")?.id,
data.agent?.tasks?.find((t) => t.status === "running")?.toolkits?.length,
id,
data.onExpandChange,
isExpanded,
]);
// manually control node size
useEffect(() => {

View file

@ -47,5 +47,9 @@
"your-cloud-storage-has-reached-the-limit-of-your-current-plan": ".لقد وصل تخزينك السحابي إلى الحد الأقصى لخطة الاشتراك الحالية الخاصة بك",
"we-re-experiencing-high-traffic-please-try-again-in-a-few-moments": ".نواجه حركة مرور عالية. يرجى المحاولة مرة أخرى بعد وقت قصيرة",
"new-project": "مشروع جديد",
"no-reply-received-task-continue": "لم يتم استلام رد، تستمر المهمة"
"no-reply-received-task-continue": "لم يتم استلام رد، تستمر المهمة",
"safe-to-run": "آمن للتشغيل",
"scripts-running": "البرامج النصية قيد التشغيل",
"scripts-warning": "وجد عارض HTML برامج نصية ذات صلة. تأكد من أن البرامج النصية آمنة للتشغيل.",
"scripts-approved": "تمت الموافقة على البرامج النصية. يتم العرض بكامل الوظائف."
}

View file

@ -47,6 +47,10 @@
"your-cloud-storage-has-reached-the-limit-of-your-current-plan": "Ihr Cloud-Speicher hat das Limit Ihres aktuellen Plans erreicht.",
"we-re-experiencing-high-traffic-please-try-again-in-a-few-moments": "Wir erleben hohen Datenverkehr. Bitte versuchen Sie es in wenigen Augenblicken erneut.",
"new-project": "Neues Projekt",
"no-reply-received-task-continue": "Keine Antwort erhalten, Aufgabe wird fortgesetzt"
"no-reply-received-task-continue": "Keine Antwort erhalten, Aufgabe wird fortgesetzt",
"safe-to-run": "Sicher ausführen",
"scripts-running": "Skripte werden ausgeführt",
"scripts-warning": "HTML-Renderer hat verwandte Skripte gefunden. Stellen Sie sicher, dass die Skripte sicher sind.",
"scripts-approved": "Skripte genehmigt. Rendering mit voller Funktionalität."
}

View file

@ -47,6 +47,10 @@
"your-cloud-storage-has-reached-the-limit-of-your-current-plan": "Your cloud storage has reached the limit of your current plan.",
"we-re-experiencing-high-traffic-please-try-again-in-a-few-moments": "We're experiencing high traffic. Please try again in a few moments.",
"new-project": "Untitled Project",
"no-reply-received-task-continue": "No reply received, task continue"
"no-reply-received-task-continue": "No reply received, task continue",
"safe-to-run": "Safe to Run",
"scripts-running": "Scripts running",
"scripts-warning": "HTML render found related scripts. Make sure scripts are safe to run.",
"scripts-approved": "Scripts approved. Rendering with full functionality."
}

View file

@ -47,6 +47,10 @@
"your-cloud-storage-has-reached-the-limit-of-your-current-plan": "Tu almacenamiento en la nube ha alcanzado el límite de tu plan actual.",
"we-re-experiencing-high-traffic-please-try-again-in-a-few-moments": "Estamos experimentando mucho tráfico. Inténtalo de nuevo en unos momentos.",
"new-project": "Nuevo proyecto",
"no-reply-received-task-continue": "No se recibió respuesta, la tarea continúa"
"no-reply-received-task-continue": "No se recibió respuesta, la tarea continúa",
"safe-to-run": "Ejecutar de forma segura",
"scripts-running": "Scripts en ejecución",
"scripts-warning": "El renderizador HTML encontró scripts relacionados. Asegúrese de que los scripts sean seguros.",
"scripts-approved": "Scripts aprobados. Renderizando con funcionalidad completa."
}

View file

@ -47,5 +47,9 @@
"your-cloud-storage-has-reached-the-limit-of-your-current-plan": "Votre stockage cloud a atteint la limite de votre plan actuel.",
"we-re-experiencing-high-traffic-please-try-again-in-a-few-moments": "Nous connaissons un trafic élevé. Veuillez réessayer dans quelques instants.",
"new-project": "Nouveau projet",
"no-reply-received-task-continue": "Aucune réponse reçue, la tâche continue"
"no-reply-received-task-continue": "Aucune réponse reçue, la tâche continue",
"safe-to-run": "Exécuter en toute sécurité",
"scripts-running": "Scripts en cours d'exécution",
"scripts-warning": "Le rendu HTML a trouvé des scripts associés. Assurez-vous que les scripts sont sûrs.",
"scripts-approved": "Scripts approuvés. Rendu avec toutes les fonctionnalités."
}

View file

@ -47,6 +47,10 @@
"your-cloud-storage-has-reached-the-limit-of-your-current-plan": "Il tuo spazio di archiviazione cloud ha raggiunto il limite del tuo piano attuale.",
"we-re-experiencing-high-traffic-please-try-again-in-a-few-moments": "Stiamo riscontrando un traffico elevato. Riprova tra qualche istante.",
"new-project": "Nuovo Progetto",
"no-reply-received-task-continue": "Nessuna risposta ricevuta, il compito continua"
"no-reply-received-task-continue": "Nessuna risposta ricevuta, il compito continua",
"safe-to-run": "Esegui in sicurezza",
"scripts-running": "Script in esecuzione",
"scripts-warning": "Il renderer HTML ha trovato script correlati. Assicurati che gli script siano sicuri.",
"scripts-approved": "Script approvati. Rendering con funzionalità complete."
}

View file

@ -47,6 +47,10 @@
"your-cloud-storage-has-reached-the-limit-of-your-current-plan": "クラウドストレージが現在のプランの制限に達しました。",
"we-re-experiencing-high-traffic-please-try-again-in-a-few-moments": "トラフィックが多いため、しばらくしてからもう一度お試しください。",
"new-project": "新規プロジェクト",
"no-reply-received-task-continue": "応答がないため、タスクを続行します"
"no-reply-received-task-continue": "応答がないため、タスクを続行します",
"safe-to-run": "安全に実行",
"scripts-running": "スクリプト実行中",
"scripts-warning": "HTMLレンダラーが関連スクリプトを検出しました。スクリプトが安全であることを確認してください。",
"scripts-approved": "スクリプトが承認されました。完全な機能でレンダリングしています。"
}

View file

@ -47,6 +47,10 @@
"your-cloud-storage-has-reached-the-limit-of-your-current-plan": "클라우드 스토리지가 현재 플랜의 한도에 도달했습니다.",
"we-re-experiencing-high-traffic-please-try-again-in-a-few-moments": "트래픽이 많습니다. 잠시 후 다시 시도해 주세요.",
"new-project": "새 프로젝트",
"no-reply-received-task-continue": "응답이 없으므로 작업을 계속합니다."
"no-reply-received-task-continue": "응답이 없으므로 작업을 계속합니다.",
"safe-to-run": "안전하게 실행",
"scripts-running": "스크립트 실행 중",
"scripts-warning": "HTML 렌더러가 관련 스크립트를 발견했습니다. 스크립트가 안전한지 확인하세요.",
"scripts-approved": "스크립트가 승인되었습니다. 전체 기능으로 렌더링 중입니다."
}

View file

@ -47,6 +47,10 @@
"your-cloud-storage-has-reached-the-limit-of-your-current-plan": "Ваше облачное хранилище достигло лимита вашего текущего плана.",
"we-re-experiencing-high-traffic-please-try-again-in-a-few-moments": "Мы испытываем высокую нагрузку. Пожалуйста, попробуйте еще раз через несколько минут.",
"new-project": "Новый проект",
"no-reply-received-task-continue": "Ответ не получен, задача продолжается"
"no-reply-received-task-continue": "Ответ не получен, задача продолжается",
"safe-to-run": "Безопасно запустить",
"scripts-running": "Скрипты выполняются",
"scripts-warning": "HTML-рендерер обнаружил связанные скрипты. Убедитесь, что скрипты безопасны.",
"scripts-approved": "Скрипты одобрены. Рендеринг с полной функциональностью."
}

View file

@ -47,5 +47,9 @@
"your-cloud-storage-has-reached-the-limit-of-your-current-plan": "您的云存储已达到当前计划的限制。",
"we-re-experiencing-high-traffic-please-try-again-in-a-few-moments": "我们正在经历高流量。请稍后再试。",
"new-project": "新项目",
"no-reply-received-task-continue": "没有收到回复,任务继续"
"no-reply-received-task-continue": "没有收到回复,任务继续",
"safe-to-run": "安全运行",
"scripts-running": "脚本运行中",
"scripts-warning": "HTML渲染器发现相关脚本。请确保脚本可以安全运行。",
"scripts-approved": "脚本已批准。正在以完整功能渲染。"
}

View file

@ -47,5 +47,9 @@
"your-cloud-storage-has-reached-the-limit-of-your-current-plan": "您的雲存儲已達到當前計劃的限制。",
"we-re-experiencing-high-traffic-please-try-again-in-a-few-moments": "我們正在經歷高流量。請稍後再試。",
"new-project": "新項目",
"no-reply-received-task-continue": "沒有收到回复,任務繼續"
"no-reply-received-task-continue": "沒有收到回复,任務繼續",
"safe-to-run": "安全運行",
"scripts-running": "腳本運行中",
"scripts-warning": "HTML渲染器發現相關腳本。請確保腳本可以安全運行。",
"scripts-approved": "腳本已批准。正在以完整功能渲染。"
}