feat: detect stale local server and notify developer to restart (#1517)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Pre-commit / pre-commit (push) Has been cancelled
Test / Run Python Tests (push) Has been cancelled

This commit is contained in:
Tong Chen 2026-03-27 23:06:00 +08:00 committed by GitHub
parent 199b013044
commit 26cc5c4604
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 358 additions and 33 deletions

View file

@ -61,6 +61,11 @@ RUN sed -i 's/\r$//' /app/start.sh && chmod +x /app/start.sh
RUN sed -i 's/\r$//' /app/celery/worker/start && chmod +x /app/celery/worker/start
RUN sed -i 's/\r$//' /app/celery/beat/start && chmod +x /app/celery/beat/start
# Bake the latest server/ commit into the image for stale-server detection.
# Uses --mount=type=bind to access .git without adding it to a layer.
RUN --mount=type=bind,source=.git,target=/tmp/.git \
echo "EIGENT_SERVER_GIT_COMMIT=$(git --git-dir=/tmp/.git log -1 --format=%H -- server/ 2>/dev/null || echo unknown)" > /app/.image_env
# Reset the entrypoint, don't invoke `uv`
ENTRYPOINT []

View file

@ -14,7 +14,7 @@
- 配置中心 Config保存各类工具/能力所需的密钥或参数)
- `GET /configs``POST /configs``PUT /configs/{id}``DELETE /configs/{id}``GET /config/info`
- 聊天与数据
- 历史、快照、分享等接口位于 `app/controller/chat/`,数据全部落在本地数据库
- 历史、快照、分享等接口位于 `app/domains/chat/api/`,数据全部落在本地数据库
- MCP 服务管理(导入本地/远程 MCP 服务器)
- `GET /mcps``POST /mcp/install``POST /mcp/import/{Local|Remote}`

View file

@ -14,7 +14,7 @@
- Config Center (store secrets/params required by tools/capabilities)
- `GET /configs`, `POST /configs`, `PUT /configs/{id}`, `DELETE /configs/{id}`, `GET /config/info`
- Chat & Data
- History, snapshots, sharing, etc. in `app/controller/chat/`, all persisted to local DB
- History, snapshots, sharing, etc. in `app/domains/chat/api/`, all persisted to local DB
- MCP Management (import local/remote MCP servers)
- `GET /mcps`, `POST /mcp/install`, `POST /mcp/import/{Local|Remote}`, etc.

View file

@ -0,0 +1,79 @@
# Server Refactor v1 - Upgrade Guide
> Applies to: v0.0.89+
> PR: #1509
## What Changed
The server codebase has been restructured from a flat layout to a **domain-driven architecture**. No API endpoints or database schemas were changed — this is a code organization refactor only.
### Directory Mapping
| Before | After | Description |
|---|---|---|
| `app/component/` | `app/core/` | Infrastructure utilities (database, encryption, celery, etc.) |
| `app/controller/` | `app/domains/*/api/` | API controllers, grouped by domain |
| `app/service/` | `app/domains/*/service/` | Business logic, grouped by domain |
| `app/exception/` | `app/shared/exception/` | Exception handling |
| `app/type/` | `app/shared/types/` | Shared type definitions |
| _(new)_ | `app/shared/auth/` | Authentication & authorization |
| _(new)_ | `app/shared/middleware/` | CORS, rate limiting, trace ID |
| _(new)_ | `app/shared/http/` | HTTP client utilities |
| _(new)_ | `app/shared/logging/` | Logging & sensitive data filtering |
### Domain Structure
Each domain (`chat`, `config`, `mcp`, `model_provider`, `oauth`, `trigger`, `user`) follows the same layout:
```
app/domains/<domain>/
api/ # Controllers (route handlers)
service/ # Business logic
schema/ # Request/response schemas
```
## Upgrade Action Required
**This is a breaking change for local deployments.** The old server code will fail to start due to changed import paths.
### Docker Users
```bash
cd server
docker-compose up --build -d
```
You **must** include `--build` to rebuild the image. Running `docker-compose up -d` without `--build` will use the stale old image and fail.
### Non-Docker Users (Local Development)
If you are running the server directly (via `start_server.sh` or `uv run uvicorn`):
1. Stop the running server process
2. Pull the latest code
3. Restart the server
```bash
# If using start_server.sh
cd server
./start_server.sh
# If running uvicorn directly
cd server
uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0
```
### Electron App Users
If you are running Eigent as a desktop app, simply restart the application. The server will be restarted automatically.
## FAQ
**Q: Will I lose my data?**
A: No. Database volumes and schemas are not affected. Only the Python source code layout changed.
**Q: Do I need to re-run database migrations?**
A: No. There are no new migrations in this change.
**Q: I see import errors like `ModuleNotFoundError: No module named 'app.component'`**
A: This means you are running an old server binary/image. Follow the upgrade steps above.

View file

@ -0,0 +1,79 @@
# Server 重构 v1 - 升级指南
> 适用版本: v0.0.89+
> PR: #1509
## 改动概述
Server 代码从扁平结构重构为**领域驱动架构 (Domain-Driven)**。API 接口和数据库结构均未变更,这是一次纯代码组织层面的重构。
### 目录变更对照
| 重构前 | 重构后 | 说明 |
|---|---|---|
| `app/component/` | `app/core/` | 基础设施数据库、加密、celery 等) |
| `app/controller/` | `app/domains/*/api/` | 按领域分组的 API 控制器 |
| `app/service/` | `app/domains/*/service/` | 按领域分组的业务逻辑 |
| `app/exception/` | `app/shared/exception/` | 异常处理 |
| `app/type/` | `app/shared/types/` | 共享类型定义 |
| _(新增)_ | `app/shared/auth/` | 认证与授权 |
| _(新增)_ | `app/shared/middleware/` | CORS、限流、Trace ID |
| _(新增)_ | `app/shared/http/` | HTTP 客户端工具 |
| _(新增)_ | `app/shared/logging/` | 日志与敏感信息过滤 |
### 领域结构
每个领域(`chat``config``mcp``model_provider``oauth``trigger``user`)遵循统一结构:
```
app/domains/<领域>/
api/ # 控制器(路由处理)
service/ # 业务逻辑
schema/ # 请求/响应模型
```
## 升级操作(必须)
**此改动对本地部署是 breaking change。** 旧版 server 代码因 import 路径变更将无法启动。
### Docker 用户
```bash
cd server
docker-compose up --build -d
```
**必须**加 `--build` 参数重新构建镜像。直接 `docker-compose up -d` 会使用旧镜像导致启动失败。
### 非 Docker 用户(本地开发)
如果你通过 `start_server.sh``uv run uvicorn` 直接运行 server
1. 停止正在运行的 server 进程
2. 拉取最新代码
3. 重新启动 server
```bash
# 使用 start_server.sh
cd server
./start_server.sh
# 直接运行 uvicorn
cd server
uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0
```
### Electron 桌面应用用户
重启应用即可server 会自动重启。
## 常见问题
**Q: 数据会丢失吗?**
A: 不会。数据库卷和表结构未受影响,仅 Python 源码目录结构发生了变化。
**Q: 需要重新执行数据库迁移吗?**
A: 不需要。此次改动没有新增数据库迁移。
**Q: 出现 `ModuleNotFoundError: No module named 'app.component'`**
A: 说明正在运行旧版 server。请按上述升级步骤操作。

View file

@ -107,7 +107,7 @@ services:
test:
[
'CMD-SHELL',
'celery -A app.component.celery inspect ping -d celery@$$HOSTNAME',
'celery -A app.core.celery inspect ping -d celery@$$HOSTNAME',
]
interval: 30s
timeout: 10s

View file

@ -22,7 +22,8 @@ if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
import logging
import sys
import subprocess
from importlib.metadata import version as pkg_version
from fastapi.staticfiles import StaticFiles
from fastapi_pagination import add_pagination
@ -50,10 +51,51 @@ auto_include_routers(router, "", "app/domains")
auto_include_routers(router, "", "app/api")
api.include_router(router, prefix=f"{prefix}/v1")
# Server version — read once at import time so it reflects the running code
try:
SERVER_VERSION = pkg_version("Eigent")
except Exception:
SERVER_VERSION = "unknown"
# Git hash of the last commit that touched server/ — used for stale-server detection.
# Captured once at startup; stays constant while the process lives.
# 1) Try git directly (works in local dev)
# 2) Fall back to .image_env baked by Dockerfile (works in Docker)
def _read_server_code_hash() -> str:
# Try git first (local dev)
try:
h = subprocess.check_output(
["git", "log", "-1", "--format=%H", "--", "server/"],
cwd=str(_project_root), text=True, stderr=subprocess.DEVNULL,
).strip()
if h:
return h
except Exception:
pass
# Fallback: read from Docker-baked .image_env
try:
env_file = pathlib.Path(__file__).parent / ".image_env"
for line in env_file.read_text().splitlines():
if line.startswith("EIGENT_SERVER_GIT_COMMIT="):
v = line.split("=", 1)[1].strip()
if v:
return v
except Exception:
pass
return "unknown"
SERVER_CODE_HASH = _read_server_code_hash()
# Health check at root level for Docker healthcheck (GET /health)
@api.get("/health", tags=["Health"])
async def health_check():
return {"status": "ok", "service": "eigent-server"}
return {
"status": "ok",
"service": "eigent-server",
"version": SERVER_VERSION,
"server_hash": SERVER_CODE_HASH,
}
# Backward-compatible webhook route (/api/webhook/...)
from app.domains.trigger.api.webhook_controller import router as webhook_router

27
server/uv.lock generated
View file

@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = "==3.12.*"
resolution-markers = [
"sys_platform == 'win32'",
@ -479,6 +479,7 @@ dependencies = [
{ name = "fastapi-pagination" },
{ name = "httpx" },
{ name = "itsdangerous" },
{ name = "loguru" },
{ name = "openai" },
{ name = "openpyxl" },
{ name = "pandas" },
@ -523,6 +524,7 @@ requires-dist = [
{ name = "fastapi-pagination", specifier = ">=0.12.34" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "itsdangerous", specifier = ">=2.2.0" },
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "openai", specifier = ">=1.99.3,<2" },
{ name = "openpyxl", specifier = ">=3.1.5" },
{ name = "pandas", specifier = ">=2.2.3" },
@ -699,7 +701,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" },
{ url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" },
{ url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" },
{ url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" },
{ url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" },
{ url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" },
{ url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" },
@ -847,6 +848,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" },
]
[[package]]
name = "loguru"
version = "0.7.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
]
[[package]]
name = "mako"
version = "1.3.10"
@ -1675,6 +1689,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
]
[[package]]
name = "win32-setctime"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
]
[[package]]
name = "yarl"
version = "1.23.0"

View file

@ -311,6 +311,80 @@ export async function checkBackendHealth(): Promise<boolean> {
}
}
// =============== Local Server Stale Detection ===============
/**
* Git hash of the last commit that touched server/, injected by Vite at build
* time. When the running server reports a different hash it means the server
* process is stale and needs to be restarted / rebuilt.
*/
const EXPECTED_SERVER_HASH: string =
import.meta.env.VITE_SERVER_CODE_HASH || '';
let serverStaleChecked = false;
/**
* One-time check: when VITE_USE_LOCAL_PROXY is enabled, fetch the local
* server's /health and compare its server_hash against the expected hash
* baked into this build. Shows a persistent toast if they differ.
*/
export async function checkLocalServerStale(): Promise<void> {
if (serverStaleChecked || !EXPECTED_SERVER_HASH) return;
serverStaleChecked = true;
const useLocalProxy = import.meta.env.VITE_USE_LOCAL_PROXY === 'true';
if (!useLocalProxy) return;
const serverUrl = import.meta.env.VITE_PROXY_URL || 'http://localhost:3001';
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const res = await fetch(`${serverUrl}/health`, {
signal: controller.signal,
method: 'GET',
});
clearTimeout(timeoutId);
let staleReason = '';
if (res.status === 404) {
// /health endpoint doesn't exist — server predates v0.0.89
staleReason = 'Server does not have /health endpoint (pre-v0.0.89)';
} else if (res.ok) {
const data = await res.json();
const serverHash: string | undefined = data?.server_hash;
if (!serverHash) {
staleReason = 'Server does not report version info (pre-v0.0.89)';
} else if (
serverHash !== 'unknown' &&
serverHash !== EXPECTED_SERVER_HASH
) {
staleReason = `Server hash ${serverHash} != expected ${EXPECTED_SERVER_HASH}`;
}
} else {
// Other HTTP errors — skip
return;
}
if (staleReason) {
const { toast } = await import('sonner');
toast.warning('Server code has been updated', {
description:
'Server is outdated. Please restart it or rebuild: docker-compose up --build -d',
duration: Infinity,
closeButton: true,
});
console.warn(`[Server Check] ${staleReason}. Please restart the server.`);
}
} catch {
// server not reachable — skip silently
}
}
/**
* Simple backend health check with retries
* @param maxWaitMs - Maximum time to wait in milliseconds (default: 10000ms)
@ -331,6 +405,10 @@ export async function waitForBackendReady(
console.log(
`[Backend Health Check] Backend is ready after ${Date.now() - startTime}ms`
);
// Fire-and-forget: check local server version when using local proxy
checkLocalServerStale();
return true;
}

View file

@ -12,6 +12,7 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { checkLocalServerStale } from '@/api/http';
import ChatBox from '@/components/ChatBox';
import Folder from '@/components/Folder';
import UpdateElectron from '@/components/update';
@ -180,6 +181,11 @@ export default function Home() {
e.target.value = '';
};
// One-time check: warn if local server is outdated after a git pull
useEffect(() => {
checkLocalServerStale();
}, []);
// Detect files and triggers when project loads
useEffect(() => {
const detectAgentFiles = async () => {
@ -393,7 +399,7 @@ export default function Home() {
return (
<div className="flex h-full w-full flex-1 items-center justify-center">
<div className="relative flex h-full w-full flex-col">
<div className="inset-0 rounded-xl pointer-events-none absolute bg-transparent"></div>
<div className="pointer-events-none absolute inset-0 rounded-xl bg-transparent"></div>
<div className="relative z-10 h-full w-full">
<Workflow taskAssigning={[]} />
</div>
@ -407,7 +413,7 @@ export default function Home() {
{activeTask.taskAssigning?.find(
(agent) => agent.agent_id === activeWorkSpace
)?.type === 'browser_agent' && (
<div className="animate-in fade-in-0 slide-in-from-right-2 flex h-full w-full flex-1 duration-300">
<div className="flex h-full w-full flex-1 duration-300 animate-in fade-in-0 slide-in-from-right-2">
<BrowserAgentWorkspace />
</div>
)}
@ -415,7 +421,7 @@ export default function Home() {
<div className="flex h-full w-full flex-1 items-center justify-center">
<div className="relative flex h-full w-full flex-col">
{/*filter blur */}
<div className="inset-0 rounded-xl pointer-events-none absolute bg-transparent"></div>
<div className="pointer-events-none absolute inset-0 rounded-xl bg-transparent"></div>
<div className="relative z-10 h-full w-full">
<Workflow taskAssigning={activeTask.taskAssigning || []} />
</div>
@ -434,7 +440,7 @@ export default function Home() {
<div className="flex h-full w-full flex-1 items-center justify-center">
<div className="relative flex h-full w-full flex-col">
{/*filter blur */}
<div className="blur-bg inset-0 rounded-xl bg-surface-secondary pointer-events-none absolute"></div>
<div className="blur-bg pointer-events-none absolute inset-0 rounded-xl bg-surface-secondary"></div>
<div className="relative z-10 h-full w-full">
<Folder />
</div>
@ -447,7 +453,7 @@ export default function Home() {
<div className="flex h-full w-full flex-1 items-center justify-center">
<div className="relative flex h-full w-full flex-col">
{/*filter blur */}
<div className="blur-bg inset-0 rounded-xl bg-surface-secondary pointer-events-none absolute"></div>
<div className="blur-bg pointer-events-none absolute inset-0 rounded-xl bg-surface-secondary"></div>
<div className="relative z-10 h-full w-full">
<Folder
data={activeTask.taskAssigning?.find(
@ -463,7 +469,7 @@ export default function Home() {
<div className="flex h-full w-full flex-1 items-center justify-center">
<div className="relative flex h-full w-full flex-col">
{/*filter blur */}
<div className="blur-bg inset-0 rounded-xl bg-surface-secondary pointer-events-none absolute"></div>
<div className="blur-bg pointer-events-none absolute inset-0 rounded-xl bg-surface-secondary"></div>
<div className="relative z-10 h-full w-full">
<Folder />
</div>
@ -478,12 +484,12 @@ export default function Home() {
// Render Tasks tab content (default)
return (
<ReactFlowProvider>
<div className="min-h-0 px-2 pb-2 pt-10 flex h-full flex-row overflow-hidden">
<div className="min-h-0 min-w-0 gap-4 relative flex h-full flex-1 items-center justify-center overflow-hidden">
<div className="flex h-full min-h-0 flex-row overflow-hidden px-2 pb-2 pt-10">
<div className="relative flex h-full min-h-0 min-w-0 flex-1 items-center justify-center gap-4 overflow-hidden">
<ResizablePanelGroup
direction="horizontal"
key={`${isChatBoxVisible}-${chatPanelPosition}`}
className="gap-0.5 w-full items-center justify-center"
className="w-full items-center justify-center gap-0.5"
>
{/* ChatBox Panel - Left side */}
{isChatBoxVisible && chatPanelPosition === 'left' && (
@ -504,10 +510,10 @@ export default function Home() {
<ResizablePanel className="h-full w-full min-w-[600px]">
{chatStore.activeTaskId &&
chatStore.tasks[chatStore.activeTaskId]?.activeWorkspace ? (
<div className="rounded-2xl border-border-tertiary bg-surface-secondary flex h-full w-full flex-col border-solid">
<div className="flex h-full w-full flex-col rounded-2xl border-solid border-border-tertiary bg-surface-secondary">
{/* Header with workspace tabs */}
<div className="px-2 py-2 flex w-full items-center justify-between">
<div className="gap-4 flex w-full flex-row items-center justify-start">
<div className="flex w-full items-center justify-between px-2 py-2">
<div className="flex w-full flex-row items-center justify-start gap-4">
<MenuToggleGroup
type="single"
variant="info"
@ -538,7 +544,7 @@ export default function Home() {
icon={<Inbox />}
showSubIcon={unviewedTabs.has('inbox')}
subIcon={
<span className="h-2 w-2 bg-red-500 rounded-full" />
<span className="h-2 w-2 rounded-full bg-red-500" />
}
className="w-32"
>
@ -553,14 +559,14 @@ export default function Home() {
}
showSubIcon={unviewedTabs.has('triggers')}
subIcon={
<span className="h-2 w-2 bg-text-error rounded-full" />
<span className="h-2 w-2 rounded-full bg-text-error" />
}
className="w-32"
rightElement={
wsConnectionStatus !== 'connected' && (
<Popover>
<PopoverPrimitive.Trigger asChild>
<div className="h-6 w-6 rounded-md hover:bg-surface-tertiary flex cursor-pointer items-center justify-center transition-colors">
<div className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md transition-colors hover:bg-surface-tertiary">
<RefreshCw
className={`h-3 w-3 ${wsConnectionStatus === 'connecting' ? 'animate-spin' : ''}`}
/>
@ -571,7 +577,7 @@ export default function Home() {
side="bottom"
align="end"
>
<div className="gap-3 flex flex-col">
<div className="flex flex-col gap-3">
<p className="text-body-sm text-text-body">
Reconnect to trigger listener
</p>
@ -596,12 +602,12 @@ export default function Home() {
</MenuToggleItem>
</MenuToggleGroup>
</div>
<div className="gap-2 flex items-center">
<div className="flex items-center gap-2">
{activeWorkspaceTab !== 'inbox' && (
<Button
variant="primary"
size="sm"
className="w-24 rounded-lg items-center justify-center"
className="w-24 items-center justify-center rounded-lg"
onClick={() => {
if (activeWorkspaceTab === 'workforce') {
setAddWorkerDialogOpen(true);
@ -664,10 +670,10 @@ export default function Home() {
</div>
) : (
// Show default workspace when activeTaskId is null or task doesn't exist
<div className="rounded-2xl border-border-tertiary bg-surface-secondary flex h-full w-full flex-col border-solid">
<div className="flex h-full w-full flex-col rounded-2xl border-solid border-border-tertiary bg-surface-secondary">
{/* Header with workspace tabs */}
<div className="px-2 py-2 flex w-full items-center justify-between">
<div className="gap-4 flex w-full flex-row items-center justify-start">
<div className="flex w-full items-center justify-between px-2 py-2">
<div className="flex w-full flex-row items-center justify-start gap-4">
<MenuToggleGroup
type="single"
variant="info"
@ -698,7 +704,7 @@ export default function Home() {
icon={<Inbox />}
showSubIcon={unviewedTabs.has('inbox')}
subIcon={
<span className="h-2 w-2 bg-red-500 rounded-full" />
<span className="h-2 w-2 rounded-full bg-red-500" />
}
className="w-32"
>
@ -713,14 +719,14 @@ export default function Home() {
}
showSubIcon={unviewedTabs.has('triggers')}
subIcon={
<span className="h-2 w-2 bg-red-500 rounded-full" />
<span className="h-2 w-2 rounded-full bg-red-500" />
}
className="w-32"
rightElement={
wsConnectionStatus !== 'connected' && (
<Popover>
<PopoverPrimitive.Trigger asChild>
<div className="h-6 w-6 rounded-md hover:bg-surface-tertiary flex cursor-pointer items-center justify-center transition-colors">
<div className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md transition-colors hover:bg-surface-tertiary">
<RefreshCw
className={`h-3 w-3 ${wsConnectionStatus === 'connecting' ? 'animate-spin' : ''}`}
/>
@ -731,7 +737,7 @@ export default function Home() {
side="bottom"
align="end"
>
<div className="gap-3 flex flex-col">
<div className="flex flex-col gap-3">
<p className="text-sm text-text-body">
Reconnect to trigger listener
</p>
@ -756,7 +762,7 @@ export default function Home() {
</MenuToggleItem>
</MenuToggleGroup>
</div>
<div className="gap-2 flex items-center">
<div className="flex items-center gap-2">
{activeWorkspaceTab !== 'inbox' && (
<Button
variant="primary"

View file

@ -13,12 +13,25 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import react from '@vitejs/plugin-react';
import { execSync } from 'node:child_process';
import { readFileSync, rmSync } from 'node:fs';
import path from 'node:path';
import { defineConfig, loadEnv } from 'vite';
import electron from 'vite-plugin-electron/simple';
import pkg from './package.json';
// Git hash of the last commit that touched server/ — used for stale-server detection.
// Set as VITE_ env var so Vite exposes it to import.meta.env automatically.
try {
process.env.VITE_SERVER_CODE_HASH = execSync(
'git log -1 --format=%H -- server/'
)
.toString()
.trim();
} catch {
// git not available (CI, packaged build, etc.)
}
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
rmSync('dist-electron', { recursive: true, force: true });