diff --git a/.gitignore b/.gitignore index 38dc4b280..c9280a9e2 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,10 @@ public/ # Testing coverage/ .traceroot-config.yaml + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index a75e0d295..18bab0aca 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -182,7 +182,7 @@ def build_conversation_context(task_lock: TaskLock, header: str = "=== CONVERSAT Formatted context string with task history and files listed once at the end """ context = "" - working_directory = None + working_directories = set() # Collect all unique working directories if task_lock.conversation_history: context = f"{header}\n" @@ -190,37 +190,35 @@ def build_conversation_context(task_lock: TaskLock, header: str = "=== CONVERSAT for entry in task_lock.conversation_history: if entry['role'] == 'task_result': if isinstance(entry['content'], dict): - # Format without file listing formatted_context = format_task_context(entry['content'], skip_files=True) context += formatted_context + "\n\n" - # Remember the working directory from the last task if entry['content'].get('working_directory'): - working_directory = entry['content']['working_directory'] + working_directories.add(entry['content']['working_directory']) else: context += entry['content'] + "\n" elif entry['role'] == 'assistant': context += f"Assistant: {entry['content']}\n\n" - # Add all generated files at the end, only once - if working_directory: - try: - if os.path.exists(working_directory): - generated_files = [] - for root, dirs, files in os.walk(working_directory): - dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['node_modules', '__pycache__', 'venv']] - for file in files: - if not file.startswith('.') and not file.endswith(('.pyc', '.tmp')): - file_path = os.path.join(root, file) - absolute_path = os.path.abspath(file_path) - generated_files.append(absolute_path) + if working_directories: + all_generated_files = set() # Use set to avoid duplicates + for working_directory in working_directories: + try: + if os.path.exists(working_directory): + for root, dirs, files in os.walk(working_directory): + dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['node_modules', '__pycache__', 'venv']] + for file in files: + if not file.startswith('.') and not file.endswith(('.pyc', '.tmp')): + file_path = os.path.join(root, file) + absolute_path = os.path.abspath(file_path) + all_generated_files.add(absolute_path) + except Exception as e: + logger.warning(f"Failed to collect generated files from {working_directory}: {e}") - if generated_files: - context += "Generated Files from Previous Tasks:\n" - for file_path in sorted(generated_files): - context += f" - {file_path}\n" - context += "\n" - except Exception as e: - logger.warning(f"Failed to collect generated files: {e}") + if all_generated_files: + context += "Generated Files from Previous Tasks:\n" + for file_path in sorted(all_generated_files): + context += f" - {file_path}\n" + context += "\n" context += "\n" diff --git a/backend/app/utils/listen/toolkit_listen.py b/backend/app/utils/listen/toolkit_listen.py index fb5b9bf5f..7e98713ba 100644 --- a/backend/app/utils/listen/toolkit_listen.py +++ b/backend/app/utils/listen/toolkit_listen.py @@ -285,11 +285,17 @@ def auto_listen_toolkit(base_toolkit_class: Type[T]) -> Callable[[Type[T]], Type for method_name, base_method in base_methods.items(): if method_name in cls.__dict__: continue - + sig = signature(base_method) - + def create_wrapper(method_name: str, base_method: Callable) -> Callable: - if iscoroutinefunction(base_method): + # Unwrap decorators to check the actual function + unwrapped_method = base_method + while hasattr(unwrapped_method, '__wrapped__'): + unwrapped_method = unwrapped_method.__wrapped__ + + # Check if the unwrapped method is a coroutine function + if iscoroutinefunction(unwrapped_method): async def async_method_wrapper(self, *args, **kwargs): return await getattr(super(cls, self), method_name)(*args, **kwargs) async_method_wrapper.__name__ = method_name @@ -301,12 +307,12 @@ def auto_listen_toolkit(base_toolkit_class: Type[T]) -> Callable[[Type[T]], Type sync_method_wrapper.__name__ = method_name sync_method_wrapper.__signature__ = sig return sync_method_wrapper - + wrapper = create_wrapper(method_name, base_method) decorated_method = listen_toolkit(base_method)(wrapper) - + setattr(cls, method_name, decorated_method) return cls - + return class_decorator diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index c475ab94b..257fca38e 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -6,6 +6,7 @@ import fs from 'node:fs' import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, cleanupOldVenvs, isBinaryExists, runInstallScript } from './utils/process' import { spawn } from 'child_process' import { safeMainWindowSend } from './utils/safeWebContentsSend' +import os from 'node:os' const userData = app.getPath('userData'); const versionFile = path.join(userData, 'version.txt'); @@ -57,6 +58,13 @@ Promise => { return new Promise(async (resolve, reject) => { try { + // Clean up cache in production environment BEFORE any checks + // This ensures users always get fresh dependencies in production + if (app.isPackaged) { + log.info('[CACHE CLEANUP] Production environment detected, cleaning cache before dependency check...'); + cleanupCacheInProduction(); + } + const versionExists:boolean = checkInstallOperations.getSavedVersion(); // Check if command tools are installed @@ -280,6 +288,34 @@ class InstallLogs { } } +/** + * Clean up cache directory + * This ensures users get fresh dependencies + * Note: Only call this in production environment (caller should check app.isPackaged) + */ +function cleanupCacheInProduction(): void { + try { + const cacheBaseDir = path.join(os.homedir(), '.eigent', 'cache'); + + if (!fs.existsSync(cacheBaseDir)) { + log.info('[CACHE CLEANUP] Cache directory does not exist, nothing to clean'); + return; + } + + log.info('[CACHE CLEANUP] Cleaning cache directory:', cacheBaseDir); + + fs.rmSync(cacheBaseDir, { recursive: true, force: true }); + + log.info('[CACHE CLEANUP] Cache directory cleaned successfully'); + + fs.mkdirSync(cacheBaseDir, { recursive: true }); + log.info('[CACHE CLEANUP] Empty cache directory recreated'); + + } catch (error) { + log.error('[CACHE CLEANUP] Failed to clean cache directory:', error); + } +} + const runInstall = (extraArgs: string[], version: string) => { const installLogs = new InstallLogs(extraArgs, version); return new Promise((resolveInner, rejectInner) => { diff --git a/src/components/ChatBox/TaskCard.tsx b/src/components/ChatBox/TaskCard.tsx index 26d7663f7..03a85f8a1 100644 --- a/src/components/ChatBox/TaskCard.tsx +++ b/src/components/ChatBox/TaskCard.tsx @@ -35,6 +35,7 @@ interface TaskCardProps { onUpdateTask: (taskIndex: number, content: string) => void; onDeleteTask: (taskIndex: number) => void; clickable?: boolean; + chatId?: string; } export function TaskCard({ @@ -47,14 +48,15 @@ export function TaskCard({ onUpdateTask, onDeleteTask, clickable = true, + chatId, }: TaskCardProps) { const { t } = useTranslation(); const [isExpanded, setIsExpanded] = useState(true); const contentRef = useRef(null); const [contentHeight, setContentHeight] = useState("auto"); - //Get Chatstore for the active project's task - const { chatStore } = useChatStoreAdapter(); + //Get Chatstore and ProjectStore for the active project's task + const { chatStore, projectStore } = useChatStoreAdapter(); if (!chatStore) { return
Loading...
; } @@ -329,6 +331,20 @@ export function TaskCard({
{ if (task.agent) { + // Switch to the chatStore that owns this task card (for multi-turn conversations) + if (chatId && projectStore.activeProjectId) { + const activeChatStore = projectStore.getActiveChatStore(); + const currentChatId = activeChatStore ? Object.keys(projectStore.projects[projectStore.activeProjectId].chatStores).find( + id => projectStore.projects[projectStore.activeProjectId].chatStores[id] === activeChatStore + ) : null; + + // Only switch if this is a different chat + if (currentChatId !== chatId) { + projectStore.setActiveChatStore(projectStore.activeProjectId, chatId); + } + } + + // Set the active workspace and agent chatStore.setActiveWorkSpace( chatStore.activeTaskId as string, "workflow" diff --git a/src/components/ChatBox/UserQueryGroup.tsx b/src/components/ChatBox/UserQueryGroup.tsx index 7776bba68..a90bc3a35 100644 --- a/src/components/ChatBox/UserQueryGroup.tsx +++ b/src/components/ChatBox/UserQueryGroup.tsx @@ -38,14 +38,14 @@ 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 - const isLastUserQuery = !queryGroup.taskMessage && - activeTaskId && + const isLastUserQuery = !queryGroup.taskMessage && + activeTaskId && chatState.tasks[activeTaskId] && - queryGroup.userMessage && + queryGroup.userMessage && queryGroup.userMessage.id === chatState.tasks[activeTaskId].messages.filter((m: any) => m.role === 'user').pop()?.id && // Only show during active phases (not finished) chatState.tasks[activeTaskId].status !== 'finished'; - + const task = (queryGroup.taskMessage || isLastUserQuery) && activeTaskId ? chatState.tasks[activeTaskId] : null; // Set up intersection observer for this query group @@ -185,6 +185,7 @@ export const UserQueryGroup: React.FC = ({ >
+ {location.pathname === "/history" && ( +
+ +
+ )} {location.pathname !== "/history" && (
@@ -282,7 +295,7 @@ function HeaderWin() {
{chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && ( <> @@ -314,6 +327,17 @@ function HeaderWin() { {t("layout.refer-friends")} + + + {chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string]?.status === 'finished' && ( @@ -330,9 +354,9 @@ function HeaderWin() { {chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && ( - chatStore.tasks[chatStore.activeTaskId as string].messages.length > 0 || - chatStore.tasks[chatStore.activeTaskId as string].hasMessages || - chatStore.tasks[chatStore.activeTaskId as string].status !== 'pending' + (chatStore.tasks[chatStore.activeTaskId as string]?.messages?.length || 0) > 0 || + chatStore.tasks[chatStore.activeTaskId as string]?.hasMessages || + chatStore.tasks[chatStore.activeTaskId as string]?.status !== 'pending' ) && (
)} @@ -392,4 +416,4 @@ function HeaderWin() { ); } -export default HeaderWin; \ No newline at end of file +export default HeaderWin; diff --git a/src/i18n/locales/en-us/layout.json b/src/i18n/locales/en-us/layout.json index adf730ee9..ccfff4ee5 100644 --- a/src/i18n/locales/en-us/layout.json +++ b/src/i18n/locales/en-us/layout.json @@ -96,6 +96,7 @@ "are-you-sure-you-want-to-delete": "Are you sure you want to delete this task? This action cannot be undone.", "share": "Share", "home": "Home", + "back": "Back", "developer-agent": "Developer Agent", "search-agent": "Search Agent", "document-agent": "Document Agent", diff --git a/src/i18n/locales/zh-Hans/layout.json b/src/i18n/locales/zh-Hans/layout.json index 28b27e3ba..33cc970df 100644 --- a/src/i18n/locales/zh-Hans/layout.json +++ b/src/i18n/locales/zh-Hans/layout.json @@ -98,6 +98,7 @@ "are-you-sure-you-want-to-delete": "您确定要删除此任务吗?此操作无法撤销。", "share": "分享", "home": "首页", + "back": "返回", "developer-agent": "开发者智能体", "search-agent": "搜索智能体", "document-agent": "文档智能体", diff --git a/src/i18n/locales/zh-Hant/layout.json b/src/i18n/locales/zh-Hant/layout.json index 3d6eb4850..82122aba8 100644 --- a/src/i18n/locales/zh-Hant/layout.json +++ b/src/i18n/locales/zh-Hant/layout.json @@ -98,6 +98,7 @@ "are-you-sure-you-want-to-delete": "您確定要刪除此任務嗎?此操作無法撤銷。", "share": "分享", "home": "首頁", + "back": "返回", "developer-agent": "開發者智能體", "search-agent": "搜尋智能體", "document-agent": "文件智能體", diff --git a/src/pages/History.tsx b/src/pages/History.tsx index b4cfc7298..d57359658 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import useChatStoreAdapter from "@/hooks/useChatStoreAdapter"; import { Plus } from "lucide-react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { useTranslation } from "react-i18next"; import { useUser } from "@stackframe/react"; @@ -28,11 +28,13 @@ import WordCarousel from "@/components/ui/WordCarousel"; export default function Home() { const { t } = useTranslation(); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const { chatStore, projectStore } = useChatStoreAdapter(); if (!chatStore || !projectStore) { return
Loading...
; } - const [activeTab, setActiveTab] = useState<"projects" | "workers" | "trigger" | "settings" | "mcp_tools">("projects"); + const tabParam = searchParams.get("tab") as "projects" | "workers" | "trigger" | "settings" | "mcp_tools" | null; + 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(); @@ -40,6 +42,14 @@ export default function Home() { const { username, email } = useAuthStore(); const displayName = stackUser?.displayName ?? stackUser?.primaryEmail ?? username ?? email ?? ""; + // Sync activeTab with URL changes + useEffect(() => { + const tab = searchParams.get("tab") as "projects" | "workers" | "trigger" | "settings" | "mcp_tools" | null; + if (tab) { + setActiveTab(tab); + } + }, [searchParams]); + const formatWelcomeName = (raw: string): string => { if (!raw) return ""; if (/^[^@]+@gmail\.com$/i.test(raw)) { diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index dea644d6d..a86d362a8 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -202,7 +202,7 @@ export default function Home() {
{chatStore.tasks[ chatStore.activeTaskId as string - ]?.taskAssigning.find( + ]?.taskAssigning?.find( (agent) => agent.agent_id === chatStore.tasks[chatStore.activeTaskId as string] @@ -231,7 +231,7 @@ export default function Home() { )} {chatStore.tasks[ chatStore.activeTaskId as string - ]?.taskAssigning.find( + ]?.taskAssigning?.find( (agent) => agent.agent_id === chatStore.tasks[chatStore.activeTaskId as string] @@ -256,7 +256,7 @@ export default function Home() { )} {chatStore.tasks[ chatStore.activeTaskId as string - ]?.taskAssigning.find( + ]?.taskAssigning?.find( (agent) => agent.agent_id === chatStore.tasks[chatStore.activeTaskId as string] @@ -270,7 +270,7 @@ export default function Home() { agent.agent_id === chatStore.tasks[chatStore.activeTaskId as string] diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index f405a54a9..1c9394008 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -551,7 +551,8 @@ const chatStore = (initial?: Partial) => createStore()( taskType: type ? 2 : 1, showType: "list", // Don't auto-confirm for multi-turn complex tasks - show workforce splitting panel - isConfirm: shouldAutoConfirm + isConfirm: shouldAutoConfirm, + task_id: currentTaskId }; addMessages(currentTaskId, newMessage) const newTaskInfo = {