diff --git a/backend/app/controller/chat_controller.py b/backend/app/controller/chat_controller.py index 5bb2229ea..8fb4bcf1a 100644 --- a/backend/app/controller/chat_controller.py +++ b/backend/app/controller/chat_controller.py @@ -27,7 +27,7 @@ from app.utils.workforce import Workforce from camel.tasks.task import Task -router = APIRouter(tags=["chat"]) +router = APIRouter() # Create traceroot logger for chat controller chat_logger = traceroot.get_logger('chat_controller') @@ -50,7 +50,7 @@ async def post(data: Chat, request: Request): os.environ["CAMEL_MODEL_LOG_ENABLED"] = "true" email_sanitized = re.sub(r'[\\/*?:"<>|\s]', "_", data.email.split("@")[0]).strip(".") - camel_log = Path.home() / ".eigent" / email_sanitized / ("task_" + data.project_id) / "camel_logs" + camel_log = Path.home() / ".eigent" / email_sanitized / ("project_" + data.project_id) / ("task_" + data.task_id) / "camel_logs" camel_log.mkdir(parents=True, exist_ok=True) os.environ["CAMEL_LOG_DIR"] = str(camel_log) diff --git a/backend/app/controller/health_controller.py b/backend/app/controller/health_controller.py new file mode 100644 index 000000000..296655bf1 --- /dev/null +++ b/backend/app/controller/health_controller.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter +from pydantic import BaseModel + +router = APIRouter(tags=["Health"]) + + +class HealthResponse(BaseModel): + status: str + service: str + + +@router.get("/health", name="health check", response_model=HealthResponse) +async def health_check(): + """Health check endpoint for verifying backend is ready to accept requests.""" + return HealthResponse(status="ok", service="eigent") + diff --git a/backend/app/controller/model_controller.py b/backend/app/controller/model_controller.py index 1ce4f02e2..692a7fb2a 100644 --- a/backend/app/controller/model_controller.py +++ b/backend/app/controller/model_controller.py @@ -8,7 +8,7 @@ from utils import traceroot_wrapper as traceroot logger = traceroot.get_logger("model_controller") -router = APIRouter(tags=["model"]) +router = APIRouter() class ValidateModelRequest(BaseModel): diff --git a/backend/app/controller/task_controller.py b/backend/app/controller/task_controller.py index 3a7bb2d4f..2bf3fc7f6 100644 --- a/backend/app/controller/task_controller.py +++ b/backend/app/controller/task_controller.py @@ -20,7 +20,7 @@ from utils import traceroot_wrapper as traceroot logger = traceroot.get_logger("task_controller") -router = APIRouter(tags=["task"]) +router = APIRouter() @router.post("/task/{id}/start", name="start task") diff --git a/backend/app/controller/tool_controller.py b/backend/app/controller/tool_controller.py index 405f69b20..22c36292f 100644 --- a/backend/app/controller/tool_controller.py +++ b/backend/app/controller/tool_controller.py @@ -4,7 +4,7 @@ from app.utils.toolkit.google_calendar_toolkit import GoogleCalendarToolkit from utils import traceroot_wrapper as traceroot logger = traceroot.get_logger("tool_controller") -router = APIRouter(tags=["task"]) +router = APIRouter() @router.post("/install/tool/{tool}", name="install tool") diff --git a/backend/app/router.py b/backend/app/router.py new file mode 100644 index 000000000..3fc6cb257 --- /dev/null +++ b/backend/app/router.py @@ -0,0 +1,64 @@ +""" +Centralized router registration for the Eigent API. +All routers are explicitly registered here for better visibility and maintainability. +""" +from fastapi import FastAPI +from app.controller import chat_controller, model_controller, task_controller, tool_controller, health_controller +from utils import traceroot_wrapper as traceroot + +logger = traceroot.get_logger("router") + + +def register_routers(app: FastAPI, prefix: str = "") -> None: + """ + Register all API routers with their respective prefixes and tags. + + This replaces the auto-discovery mechanism for better: + - Visibility: See all routes in one place + - Maintainability: Easy to add/remove routes + - Debugging: Clear registration order and configuration + + Args: + app: FastAPI application instance + prefix: Optional global prefix for all routes (e.g., "/api") + """ + routers_config = [ + { + "router": health_controller.router, + "tags": ["Health"], + "description": "Health check endpoint for service readiness" + }, + { + "router": chat_controller.router, + "tags": ["chat"], + "description": "Chat session management, improvements, and human interactions" + }, + { + "router": model_controller.router, + "tags": ["model"], + "description": "Model validation and configuration" + }, + { + "router": task_controller.router, + "tags": ["task"], + "description": "Task lifecycle management (start, stop, update, control)" + }, + { + "router": tool_controller.router, + "tags": ["tool"], + "description": "Tool installation and management" + }, + ] + + for config in routers_config: + app.include_router( + config["router"], + prefix=prefix, + tags=config["tags"] + ) + route_count = len(config["router"].routes) + logger.info( + f"Registered {config['tags'][0]} router: {route_count} routes - {config['description']}" + ) + + logger.info(f"Total routers registered: {len(routers_config)}") \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 4664ed66a..23421fda9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -20,7 +20,9 @@ if traceroot.is_enabled(): connect_fastapi(api) # 2) Now safe to import modules that use traceroot.get_logger() at import-time -from app.component.environment import auto_include_routers, env +from app.component.environment import env +from app.router import register_routers + os.environ["PYTHONIOENCODING"] = "utf-8" @@ -33,7 +35,7 @@ app_logger.info(f"Environment: {os.environ.get('ENVIRONMENT', 'development')}") prefix = env("url_prefix", "") app_logger.info(f"Loading routers with prefix: '{prefix}'") -auto_include_routers(api, prefix, "app/controller") +register_routers(api, prefix) app_logger.info("All routers loaded successfully") diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py deleted file mode 100644 index dd7ee44cc..000000000 --- a/backend/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Utils package diff --git a/electron/main/init.ts b/electron/main/init.ts index 11ed6ef38..b09db6595 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -4,6 +4,7 @@ import log from 'electron-log' import fs from 'fs' import path from 'path' import * as net from "net"; +import * as http from "http"; import { ipcMain, BrowserWindow, app } from 'electron' import { promisify } from 'util' import { detectInstallationLogs, PromiseReturnType } from "./install-deps"; @@ -195,21 +196,77 @@ export async function startBackend(setPort?: (port: number) => void): Promise { if (!started) { + if (healthCheckInterval) clearInterval(healthCheckInterval); node_process.kill(); reject(new Error('Backend failed to start within timeout')); } }, 30000); // 30 second timeout + // Helper function to poll health endpoint + const pollHealthEndpoint = (): void => { + let attempts = 0; + const maxAttempts = 20; // 5 seconds total (20 * 250ms) + const intervalMs = 250; + + healthCheckInterval = setInterval(() => { + attempts++; + const healthUrl = `http://127.0.0.1:${port}/health`; + + const req = http.get(healthUrl, { timeout: 1000 }, (res) => { + if (res.statusCode === 200) { + log.info(`Backend health check passed after ${attempts} attempts`); + started = true; + clearTimeout(startTimeout); + if (healthCheckInterval) clearInterval(healthCheckInterval); + resolve(node_process); + } else { + // Non-200 status (e.g., 404), continue polling unless max attempts reached + if (attempts >= maxAttempts) { + log.error(`Backend health check failed after ${attempts} attempts with status ${res.statusCode}`); + started = true; + clearTimeout(startTimeout); + if (healthCheckInterval) clearInterval(healthCheckInterval); + node_process.kill(); + reject(new Error(`Backend health check failed: HTTP ${res.statusCode}`)); + } + } + }); + + req.on('error', () => { + // Connection error - backend might not be ready yet, continue polling + if (attempts >= maxAttempts) { + log.error(`Backend health check failed after ${attempts} attempts: unable to connect`); + started = true; + clearTimeout(startTimeout); + if (healthCheckInterval) clearInterval(healthCheckInterval); + node_process.kill(); + reject(new Error('Backend health check failed: unable to connect')); + } + }); + + req.on('timeout', () => { + req.destroy(); + if (attempts >= maxAttempts) { + log.error(`Backend health check timed out after ${attempts} attempts`); + started = true; + clearTimeout(startTimeout); + if (healthCheckInterval) clearInterval(healthCheckInterval); + node_process.kill(); + reject(new Error('Backend health check timed out')); + } + }); + }, intervalMs); + }; node_process.stdout.on('data', (data) => { displayFilteredLogs(data); // check output content, judge if start success if (!started && data.toString().includes("Uvicorn running on")) { - started = true; - clearTimeout(startTimeout); - resolve(node_process); + log.info('Uvicorn startup detected, starting health check polling...'); + pollHealthEndpoint(); } }); @@ -217,9 +274,8 @@ export async function startBackend(setPort?: (port: number) => void): Promise void): Promise void): Promise { clearTimeout(startTimeout); + if (healthCheckInterval) clearInterval(healthCheckInterval); if (!started) { reject(new Error(`fastapi exited with code ${code}`)); } diff --git a/server/Dockerfile b/server/Dockerfile index 17ff0e2d3..b09690365 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # Copy dependency files first -COPY pyproject.toml uv.lock ./ +COPY server/pyproject.toml server/uv.lock ./ # Install the project's dependencies RUN --mount=type=cache,target=/root/.cache/uv \ @@ -29,7 +29,11 @@ RUN --mount=type=cache,target=/root/.cache/uv \ # Then, add the rest of the project source code and install it # Installing separately from its dependencies allows optimal layer caching -COPY . /app +COPY server/ /app + +# Copy the utils directory from the parent project +COPY utils /app/utils + RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --no-dev @@ -45,7 +49,7 @@ RUN apt-get update && apt-get install -y curl netcat-openbsd && rm -rf /var/lib/ ENV PATH="/app/.venv/bin:$PATH" # Copy and make the start script executable -COPY start.sh /app/start.sh +COPY server/start.sh /app/start.sh RUN sed -i 's/\r$//' /app/start.sh && chmod +x /app/start.sh # Reset the entrypoint, don't invoke `uv` diff --git a/server/alembic/env.py b/server/alembic/env.py index 4dccfeb6a..2e5cf7262 100644 --- a/server/alembic/env.py +++ b/server/alembic/env.py @@ -1,4 +1,11 @@ from logging.config import fileConfig +import sys +import pathlib + +# Add project root to Python path to import shared utils +_project_root = pathlib.Path(__file__).parent.parent.parent +if str(_project_root) not in sys.path: + sys.path.insert(0, str(_project_root)) from sqlalchemy import engine_from_config, pool from alembic import context diff --git a/server/app/controller/chat/history_controller.py b/server/app/controller/chat/history_controller.py index f58b29995..9dd6f2a30 100644 --- a/server/app/controller/chat/history_controller.py +++ b/server/app/controller/chat/history_controller.py @@ -3,7 +3,7 @@ from fastapi_pagination import Page from fastapi_pagination.ext.sqlmodel import paginate from app.model.chat.chat_history import ChatHistoryOut, ChatHistoryIn, ChatHistory, ChatHistoryUpdate from fastapi_babel import _ -from sqlmodel import Session, select, desc +from sqlmodel import Session, select, desc, case from app.component.auth import Auth, auth_must from app.component.database import session from utils import traceroot_wrapper as traceroot @@ -38,7 +38,19 @@ def create_chat_history(data: ChatHistoryIn, session: Session = Depends(session) def list_chat_history(session: Session = Depends(session), auth: Auth = Depends(auth_must)) -> Page[ChatHistoryOut]: """List chat histories for current user.""" user_id = auth.user.id - stmt = select(ChatHistory).where(ChatHistory.user_id == user_id).order_by(desc(ChatHistory.created_at)) + + # Order by created_at descending, but fallback to id descending for old records without timestamps + # This ensures newer records with timestamps come first, followed by old records ordered by id + stmt = ( + select(ChatHistory) + .where(ChatHistory.user_id == user_id) + .order_by( + desc(case((ChatHistory.created_at.is_(None), 0), else_=1)), # Non-null created_at first + desc(ChatHistory.created_at), # Then by created_at descending + desc(ChatHistory.id) # Finally by id descending for records with same/null created_at + ) + ) + result = paginate(session, stmt) total = result.total if hasattr(result, 'total') else 0 logger.debug("Chat histories listed", extra={"user_id": user_id, "total": total}) diff --git a/server/app/model/chat/chat_history.py b/server/app/model/chat/chat_history.py index 6dd7775a1..79720d87e 100644 --- a/server/app/model/chat/chat_history.py +++ b/server/app/model/chat/chat_history.py @@ -2,6 +2,7 @@ from sqlalchemy import Float, Integer from sqlmodel import Field, SmallInteger, Column, JSON, String from typing import Optional from enum import IntEnum +from datetime import datetime from sqlalchemy_utils import ChoiceType from app.model.abstract.model import AbstractModel, DefaultTimes from pydantic import BaseModel, model_validator @@ -13,6 +14,16 @@ class ChatStatus(IntEnum): class ChatHistory(AbstractModel, DefaultTimes, table=True): + """ + Chat history model with timestamp tracking. + + Inherits from DefaultTimes which provides: + - created_at: timestamp when record is created (auto-populated) + - updated_at: timestamp when record is last modified (auto-updated) + - deleted_at: timestamp for soft deletion (nullable) + + For legacy records without timestamps, sorting falls back to id ordering. + """ id: int = Field(default=None, primary_key=True) user_id: int = Field(index=True) task_id: str = Field(index=True, unique=True) @@ -70,13 +81,22 @@ class ChatHistoryOut(BaseModel): summary: str | None = None tokens: int status: int + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None @model_validator(mode="after") def fill_project_id_from_task_id(self): - """fill by task_id when project_id is None""" + """Fill project_id from task_id when project_id is None""" if self.project_id is None: self.project_id = self.task_id return self + + @model_validator(mode="after") + def handle_legacy_timestamps(self): + """Handle legacy records that might not have timestamp fields""" + # For old records without timestamps, we rely on database-level defaults + # The sorting in the controller will handle ordering appropriately + return self class ChatHistoryUpdate(BaseModel): diff --git a/server/app/model/config/config.py b/server/app/model/config/config.py index 022a520d5..4677b4e82 100644 --- a/server/app/model/config/config.py +++ b/server/app/model/config/config.py @@ -124,10 +124,10 @@ class ConfigInfo: "env_vars": [], "toolkit": "google_drive_mcp_toolkit", }, - # ConfigGroup.GOOGLE_GMAIL_MCP.value: { - # "env_vars": [], - # "toolkit": "google_gmail_mcp_toolkit", - # }, + ConfigGroup.GOOGLE_GMAIL_MCP.value: { + "env_vars": ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GOOGLE_REFRESH_TOKEN"], + "toolkit": "google_gmail_native_toolkit", + }, ConfigGroup.IMAGE_ANALYSIS.value: { "env_vars": [], "toolkit": "image_analysis_toolkit", diff --git a/server/app/type/config_group.py b/server/app/type/config_group.py index ba7b66050..df5103e64 100644 --- a/server/app/type/config_group.py +++ b/server/app/type/config_group.py @@ -21,7 +21,7 @@ class ConfigGroup(str, Enum): GITHUB = "Github" GOOGLE_CALENDAR = "Google Calendar" GOOGLE_DRIVE_MCP = "Google Drive MCP" - GOOGLE_GMAIL_MCP = "Google Gmail MCP" + GOOGLE_GMAIL_MCP = "Google Gmail" IMAGE_ANALYSIS = "Image Analysis" MCP_SEARCH = "MCP Search" PPTX = "PPTX" diff --git a/server/docker-compose.yml b/server/docker-compose.yml index 193ee5713..a4dbba26e 100644 --- a/server/docker-compose.yml +++ b/server/docker-compose.yml @@ -25,8 +25,8 @@ services: # FastAPI Application api: build: - context: . - dockerfile: Dockerfile + context: .. + dockerfile: server/Dockerfile args: database_url: postgresql://postgres:123456@postgres:5432/eigent container_name: eigent_api diff --git a/server/utils/__init__.py b/server/utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/assets/wechat_qr_1.jpg b/src/assets/wechat_qr_1.jpg index 857ac9d9d..3fe17cb39 100644 Binary files a/src/assets/wechat_qr_1.jpg and b/src/assets/wechat_qr_1.jpg differ diff --git a/src/assets/wechat_qr_2.jpg b/src/assets/wechat_qr_2.jpg index a3d5ee49c..c6596c507 100644 Binary files a/src/assets/wechat_qr_2.jpg and b/src/assets/wechat_qr_2.jpg differ diff --git a/src/assets/wechat_qr_3.jpg b/src/assets/wechat_qr_3.jpg index 5663db8e0..8a373214c 100644 Binary files a/src/assets/wechat_qr_3.jpg and b/src/assets/wechat_qr_3.jpg differ diff --git a/src/assets/wechat_qr_4.jpg b/src/assets/wechat_qr_4.jpg index f1e72d6bd..e38ff30bf 100644 Binary files a/src/assets/wechat_qr_4.jpg and b/src/assets/wechat_qr_4.jpg differ diff --git a/src/components/AddWorker/index.tsx b/src/components/AddWorker/index.tsx index 073b7adab..f8a053046 100644 --- a/src/components/AddWorker/index.tsx +++ b/src/components/AddWorker/index.tsx @@ -1,7 +1,6 @@ import { Button } from "@/components/ui/button"; import { Dialog, - DialogClose, DialogContent, DialogContentSection, DialogFooter, @@ -11,12 +10,10 @@ import { import { Input } from "@/components/ui/input"; import { Bot, - CircleAlert, Plus, - RefreshCw, - ChevronLeft, - ArrowRight, Edit, + Eye, + EyeOff, } from "lucide-react"; import ToolSelect from "./ToolSelect"; import { Textarea } from "@/components/ui/textarea"; @@ -25,7 +22,6 @@ import githubIcon from "@/assets/github.svg"; import { fetchPost } from "@/api/http"; import { useAuthStore, useWorkerList } from "@/store/authStore"; import { useTranslation } from "react-i18next"; -import { TooltipSimple } from "../ui/tooltip"; import useChatStoreAdapter from "@/hooks/useChatStoreAdapter"; interface EnvValue { @@ -68,6 +64,7 @@ export function AddWorker({ const [showEnvConfig, setShowEnvConfig] = useState(false); const [activeMcp, setActiveMcp] = useState(null); const [envValues, setEnvValues] = useState<{ [key: string]: EnvValue }>({}); + const [secretVisible, setSecretVisible] = useState<{ [key: string]: boolean }>({}); const toolSelectRef = useRef<{ installMcp: (id: number, env?: any, activeMcp?: any) => Promise; } | null>(null); @@ -86,6 +83,7 @@ export function AddWorker({ console.log(mcp); if (mcp?.install_command?.env) { const initialValues: { [key: string]: EnvValue } = {}; + const initialVisibility: { [key: string]: boolean } = {}; for(const key of Object.keys(mcp.install_command.env)) { initialValues[key] = { value: "", @@ -95,8 +93,10 @@ export function AddWorker({ ?.replace(/{{/g, "") ?.replace(/}}/g, "") || "", }; + initialVisibility[key] = false; } setEnvValues(initialValues); + setSecretVisible(initialVisibility); } }; @@ -136,12 +136,14 @@ export function AddWorker({ // clean status setActiveMcp(null); setEnvValues({}); + setSecretVisible({}); }; const handleCloseMcpEnvSetting = () => { setShowEnvConfig(false); setActiveMcp(null); setEnvValues({}); + setSecretVisible({}); }; const handleShowEnvConfig = (mcp: McpItem) => { @@ -150,6 +152,11 @@ export function AddWorker({ setShowEnvConfig(true); }; + const isSensitiveKey = (key: string) => /token|key|secret|password|id/i.test(key); + const toggleSecretVisibility = (key: string) => { + setSecretVisible((prev) => ({ ...prev, [key]: !prev[key] })); + }; + const handleSelectedToolsChange = (tools: McpItem[]) => { setSelectedTools(tools); }; @@ -161,6 +168,7 @@ export function AddWorker({ setShowEnvConfig(false); setActiveMcp(null); setEnvValues({}); + setSecretVisible({}); setNameError(""); }; @@ -204,9 +212,11 @@ export function AddWorker({ } }); console.log("mcpLocal.mcpServers", mcpLocal.mcpServers); - for(const key of Object.keys(mcpLocal.mcpServers)) { - if (!mcpList.includes(key)) { - delete mcpLocal.mcpServers[key]; + if (mcpLocal.mcpServers && typeof mcpLocal.mcpServers === 'object') { + for(const key of Object.keys(mcpLocal.mcpServers)) { + if (!mcpList.includes(key)) { + delete mcpLocal.mcpServers[key]; + } } } if (edit) { @@ -319,7 +329,12 @@ export function AddWorker({ )} - + e.preventDefault()} + onEscapeKeyDown={(e) => e.preventDefault()} + > (
-
- {key}* -
updateEnvValue(key, e.target.value)} + note={envValues[key]?.tip} + backIcon={isSensitiveKey(key) ? ( + secretVisible[key] ? ( + + ) : ( + + ) + ) : undefined} + onBackIconClick={isSensitiveKey(key) ? () => toggleSecretVisibility(key) : undefined} /> -
- {envValues[key]?.tip} -
) )} @@ -392,7 +413,6 @@ export function AddWorker({ cancelButtonVariant="ghost" confirmButtonVariant="primary" > - {/* hidden but keep rendering ToolSelect component */}
@@ -425,11 +445,6 @@ export function AddWorker({ }} state={nameError ? "error" : "default"} note={nameError || ""} - backIcon={} - onBackIconClick={() => { - // Handle refresh/regenerate logic here - console.log("Refresh agent name"); - }} required />
diff --git a/src/components/ChatBox/ProjectSection.tsx b/src/components/ChatBox/ProjectSection.tsx index ae361674a..129f9084d 100644 --- a/src/components/ChatBox/ProjectSection.tsx +++ b/src/components/ChatBox/ProjectSection.tsx @@ -152,10 +152,18 @@ function groupMessagesByQuery(messages: any[]) { otherMessages: [] }; } - } else { - // Other messages (assistant responses, etc.) + } else { + // Other messages (assistant responses, errors, etc.) if (currentGroup) { currentGroup.otherMessages.push(message); + } else { + // If there is no current user group yet (e.g., the first message is from agent/error), + // create an anonymous group to ensure the message is rendered. + currentGroup = { + queryId: `orphan-${message.id}`, + userMessage: null, + otherMessages: [message] + }; } } }); diff --git a/src/components/ChatBox/UserQueryGroup.tsx b/src/components/ChatBox/UserQueryGroup.tsx index 6bd2ac2f6..69a4b3d64 100644 --- a/src/components/ChatBox/UserQueryGroup.tsx +++ b/src/components/ChatBox/UserQueryGroup.tsx @@ -41,10 +41,10 @@ export const UserQueryGroup: React.FC = ({ // Show task if this query group has a task message OR if it's the most recent user query during splitting // During splitting phase (no to_sub_tasks yet), show task for the most recent query only // Exclude human-reply scenarios (when user is replying to an activeAsk) - const isHumanReply = queryGroup.userMessage && + const isHumanReply = queryGroup.userMessage && activeTaskId && chatState.tasks[activeTaskId] && - (chatState.tasks[activeTaskId].activeAsk || + (chatState.tasks[activeTaskId].activeAsk || // Check if this user message follows an 'ask' message in the message sequence (() => { const messages = chatState.tasks[activeTaskId].messages; @@ -164,7 +164,9 @@ export const UserQueryGroup: React.FC = ({ > {}} attaches={queryGroup.userMessage.attaches} /> diff --git a/src/components/HistorySidebar/index.tsx b/src/components/HistorySidebar/index.tsx index 4d238749b..b826c869e 100644 --- a/src/components/HistorySidebar/index.tsx +++ b/src/components/HistorySidebar/index.tsx @@ -178,7 +178,7 @@ export default function HistorySidebar() { try { //TODO(file): rename endpoint to use project_id //TODO(history): make sure to sync to projectId when updating endpoint - await (window as any).ipcRenderer.invoke('delete-task-files', email, history.task_id); + await (window as any).ipcRenderer.invoke('delete-task-files', email, history.task_id, history.project_id ?? undefined); } catch (error) { console.warn("Local file cleanup failed:", error); } diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx index dfaa2f929..6efff9996 100644 --- a/src/components/TopBar/index.tsx +++ b/src/components/TopBar/index.tsx @@ -154,9 +154,12 @@ function HeaderWin() { return; } + const projectId = projectStore.activeProjectId; + const historyId = projectId ? projectStore.getHistoryId(projectId) : null; + try { const task = chatStore.tasks[taskId]; - + // Stop the task if it's running if (task && task.status === 'running') { await fetchPut(`/task/${taskId}/take-control`, { @@ -171,11 +174,15 @@ function HeaderWin() { console.log("Task may not exist on backend:", error); } - // Delete from history - try { - await proxyFetchDelete(`/api/chat/history/${taskId}`); - } catch (error) { - console.log("Task may not exist in history:", error); + // Delete from history using historyId + if (historyId) { + try { + await proxyFetchDelete(`/api/chat/history/${historyId}`); + } catch (error) { + console.log("History may not exist:", error); + } + } else { + console.warn("No historyId found for project, skipping history deletion"); } // Remove from local store @@ -185,8 +192,8 @@ function HeaderWin() { // This ensures we start fresh without any residual state projectStore.createProject("new project"); - // Navigate to home - navigate("/"); + // Navigate to home with replace to force refresh + navigate("/", { replace: true }); toast.success(t("layout.project-ended-successfully"), { closeButton: true, diff --git a/src/pages/History.tsx b/src/pages/History.tsx index 5bd67ba8d..cb9bd8934 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -4,8 +4,6 @@ import { Plus } from "lucide-react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { useTranslation } from "react-i18next"; -import { useUser } from "@stackframe/react"; -import { hasStackKeys } from "@/lib"; import { useAuthStore } from "@/store/authStore"; import { MenuToggleGroup, MenuToggleItem } from "@/components/MenuButton/MenuButton"; import Project from "@/pages/Dashboard/Project"; @@ -37,10 +35,8 @@ export default function Home() { const [activeTab, setActiveTab] = useState<"projects" | "workers" | "trigger" | "settings" | "mcp_tools">(tabParam || "projects"); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const scrollContainerRef = useRef(null); - const HAS_STACK_KEYS = hasStackKeys(); - const stackUser = HAS_STACK_KEYS ? useUser({ or: 'anonymous-if-exists' }) : null; const { username, email } = useAuthStore(); - const displayName = stackUser?.displayName ?? stackUser?.primaryEmail ?? username ?? email ?? ""; + const displayName = username ?? email ?? ""; // Sync activeTab with URL changes useEffect(() => { diff --git a/utils/__init__.py b/utils/__init__.py index 8b1378917..a93f712f3 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1 +1,3 @@ +from . import traceroot_wrapper +__all__ = ['traceroot_wrapper'] diff --git a/utils/traceroot_wrapper.py b/utils/traceroot_wrapper.py index 6a1a912e8..5ad9c8e32 100644 --- a/utils/traceroot_wrapper.py +++ b/utils/traceroot_wrapper.py @@ -1,9 +1,16 @@ from pathlib import Path from typing import Callable import logging -import traceroot from dotenv import load_dotenv +# Try to import traceroot, but handle gracefully if not available +try: + import traceroot + TRACEROOT_AVAILABLE = True +except ImportError: + TRACEROOT_AVAILABLE = False + traceroot = None + # Auto-detect module name based on caller's path def _get_module_name(): """Automatically detect if this is being called from backend or server.""" @@ -26,7 +33,7 @@ env_path = Path(__file__).resolve().parents[1] / '.env' load_dotenv(env_path) -if traceroot.init(): +if TRACEROOT_AVAILABLE and traceroot.init(): from traceroot.logger import get_logger as _get_traceroot_logger trace = traceroot.trace @@ -42,7 +49,7 @@ if traceroot.init(): # Log successful initialization module_name = _get_module_name() _init_logger = _get_traceroot_logger("traceroot_wrapper") - _init_logger.info("TraceRoot initialized successfully", extra={"backend": "traceroot", "module": module_name}) + _init_logger.info("TraceRoot initialized successfully", extra={"backend": "traceroot", "service_module": module_name}) else: # No-op implementations when TraceRoot is not configured def trace(*args, **kwargs): @@ -69,7 +76,10 @@ else: # Log fallback mode _fallback_logger = logging.getLogger("traceroot_wrapper") - _fallback_logger.warning("TraceRoot not initialized - using Python logging as fallback") + if TRACEROOT_AVAILABLE: + _fallback_logger.warning("TraceRoot available but not initialized - using Python logging as fallback") + else: + _fallback_logger.warning("TraceRoot not available - using Python logging as fallback") __all__ = ['trace', 'get_logger', 'is_enabled']