Merge branch 'main' into chatbox-ux

This commit is contained in:
Douglas Lai 2025-11-06 11:07:12 +00:00 committed by GitHub
commit eb0dfaff82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 295 additions and 73 deletions

View file

@ -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)

View file

@ -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")

View file

@ -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):

View file

@ -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")

View file

@ -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")

64
backend/app/router.py Normal file
View file

@ -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)}")

View file

@ -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")

View file

@ -1 +0,0 @@
# Utils package

View file

@ -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<an
let started = false;
let healthCheckInterval: NodeJS.Timeout | null = null;
const startTimeout = setTimeout(() => {
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<an
displayFilteredLogs(data);
if (!started && data.toString().includes("Uvicorn running on")) {
started = true;
clearTimeout(startTimeout);
resolve(node_process);
log.info('Uvicorn startup detected (stderr), starting health check polling...');
pollHealthEndpoint();
}
// Check for port binding errors
@ -227,6 +283,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
data.toString().includes("bind() failed")) {
started = true; // Prevent multiple rejections
clearTimeout(startTimeout);
if (healthCheckInterval) clearInterval(healthCheckInterval);
node_process.kill();
reject(new Error(`Port ${port} is already in use`));
}
@ -234,6 +291,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
node_process.on('close', (code) => {
clearTimeout(startTimeout);
if (healthCheckInterval) clearInterval(healthCheckInterval);
if (!started) {
reject(new Error(`fastapi exited with code ${code}`));
}

View file

@ -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`

View file

@ -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

View file

@ -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})

View file

@ -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):

View file

@ -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",

View file

@ -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"

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Before After
Before After

View file

@ -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<McpItem | null>(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<void>;
} | 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({
</Button>
)}
</DialogTrigger>
<DialogContent size="sm" className="p-0 gap-0">
<DialogContent
size="sm"
className="p-0 gap-0"
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
<DialogHeader
title={showEnvConfig ? t("workforce.configure-mcp-server") : t("workforce.add-your-agent")}
tooltip={t("layout.configure-your-mcp-worker-node-here")}
@ -364,18 +379,24 @@ export function AddWorker({
{Object.keys(activeMcp?.install_command?.env || {}).map(
(key) => (
<div key={key}>
<div className="text-text-body text-sm leading-normal font-bold">
{key}*
</div>
<Input
placeholder=""
className="h-7 rounded-sm border border-solid border-input-border-default bg-input-bg-default !shadow-none text-sm leading-normal !ring-0 !ring-offset-0 resize-none"
size="default"
title={key}
required
placeholder={envValues[key]?.tip || `Enter ${key}`}
type={isSensitiveKey(key) && !secretVisible[key] ? "password" : "text"}
value={envValues[key]?.value || ""}
onChange={(e) => updateEnvValue(key, e.target.value)}
note={envValues[key]?.tip}
backIcon={isSensitiveKey(key) ? (
secretVisible[key] ? (
<EyeOff size={16} className="text-button-transparent-icon-disabled" />
) : (
<Eye size={16} className="text-button-transparent-icon-disabled" />
)
) : undefined}
onBackIconClick={isSensitiveKey(key) ? () => toggleSecretVisibility(key) : undefined}
/>
<div className="text-input-label-default text-xs leading-normal">
{envValues[key]?.tip}
</div>
</div>
)
)}
@ -392,7 +413,6 @@ export function AddWorker({
cancelButtonVariant="ghost"
confirmButtonVariant="primary"
>
<ArrowRight size={16} />
</DialogFooter>
{/* hidden but keep rendering ToolSelect component */}
<div style={{ display: "none" }}>
@ -425,11 +445,6 @@ export function AddWorker({
}}
state={nameError ? "error" : "default"}
note={nameError || ""}
backIcon={<RefreshCw size={16} className="text-button-transparent-icon-disabled" />}
onBackIconClick={() => {
// Handle refresh/regenerate logic here
console.log("Refresh agent name");
}}
required
/>
</div>

View file

@ -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]
};
}
}
});

View file

@ -41,10 +41,10 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
// 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<UserQueryGroupProps> = ({
>
<UserMessageCard
id={queryGroup.userMessage.id}
role={queryGroup.userMessage.role}
content={queryGroup.userMessage.content}
onTyping={() => {}}
attaches={queryGroup.userMessage.attaches}
/>
</motion.div>

View file

@ -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);
}

View file

@ -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,

View file

@ -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<HTMLDivElement | null>(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(() => {

View file

@ -1 +1,3 @@
from . import traceroot_wrapper
__all__ = ['traceroot_wrapper']

View file

@ -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']