Merge remote-tracking branch 'origin/fix/change_async_in_terminal_toolkit' into fix/change_async_in_terminal_toolkit

This commit is contained in:
LuoPengcheng 2025-10-24 02:00:10 +08:00
commit 47bf341cc7
13 changed files with 150 additions and 48 deletions

7
.gitignore vendored
View file

@ -47,3 +47,10 @@ public/
# Testing
coverage/
.traceroot-config.yaml
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python

View file

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

View file

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

View file

@ -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<PromiseReturnType> => {
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<PromiseReturnType>((resolveInner, rejectInner) => {

View file

@ -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<HTMLDivElement>(null);
const [contentHeight, setContentHeight] = useState<number | "auto">("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 <div>Loading...</div>;
}
@ -329,6 +331,20 @@ export function TaskCard({
<div
onClick={() => {
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"

View file

@ -38,14 +38,14 @@ 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
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<UserQueryGroupProps> = ({
>
<TaskCard
key={`task-${activeTaskId}-${queryGroup.queryId}`}
chatId={chatId}
taskInfo={task?.taskInfo || []}
taskType={queryGroup.taskMessage?.taskType || 1}
taskAssigning={task?.taskAssigning || []}

View file

@ -224,6 +224,19 @@ function HeaderWin() {
<img className="w-6 h-6" src={folderIcon} alt="folder-icon" />
</Button>
</div>
{location.pathname === "/history" && (
<div className="flex items-center mr-1">
<Button
variant="ghost"
size="xs"
className="no-drag"
onClick={() => navigate("/")}
>
<ChevronLeft className="w-4 h-4" />
{t("layout.back")}
</Button>
</div>
)}
{location.pathname !== "/history" && (
<div className="flex items-center mr-1">
<TooltipSimple content={t("layout.home")} side="bottom" align="center">
@ -282,7 +295,7 @@ function HeaderWin() {
<div
className={`${
platform === "darwin" && "pr-2"
} flex h-full items-center space-x-1 z-50 relative no-drag gap-1`}
} flex h-full items-center z-50 relative no-drag gap-1`}
>
{chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && (
<>
@ -314,6 +327,17 @@ function HeaderWin() {
{t("layout.refer-friends")}
</Button>
</TooltipSimple>
<TooltipSimple content={t("layout.settings")} side="bottom" align="center">
<Button
onClick={() => navigate("/history?tab=settings")}
variant="ghost"
size="xs"
className="no-drag"
>
<Settings className="w-4 h-4" />
{t("layout.settings")}
</Button>
</TooltipSimple>
{chatStore.activeTaskId &&
chatStore.tasks[chatStore.activeTaskId as string]?.status === 'finished' && (
<TooltipSimple content={t("layout.share")} side="bottom" align="end">
@ -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'
) && (
<TooltipSimple content={t("layout.end-project")} side="bottom" align="end">
<Button
@ -352,7 +376,7 @@ function HeaderWin() {
<div
className={`${
platform === "darwin" && "pr-2"
} flex h-full items-center space-x-1 z-50 relative no-drag`}
} flex h-full items-center z-50 relative no-drag gap-1`}
>
</div>
)}
@ -392,4 +416,4 @@ function HeaderWin() {
);
}
export default HeaderWin;
export default HeaderWin;

View file

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

View file

@ -98,6 +98,7 @@
"are-you-sure-you-want-to-delete": "您确定要删除此任务吗?此操作无法撤销。",
"share": "分享",
"home": "首页",
"back": "返回",
"developer-agent": "开发者智能体",
"search-agent": "搜索智能体",
"document-agent": "文档智能体",

View file

@ -98,6 +98,7 @@
"are-you-sure-you-want-to-delete": "您確定要刪除此任務嗎?此操作無法撤銷。",
"share": "分享",
"home": "首頁",
"back": "返回",
"developer-agent": "開發者智能體",
"search-agent": "搜尋智能體",
"document-agent": "文件智能體",

View file

@ -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 <div>Loading...</div>;
}
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<HTMLDivElement | null>(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)) {

View file

@ -202,7 +202,7 @@ export default function Home() {
<div className="w-full h-full flex-1 flex flex-col animate-in fade-in-0 pr-2 slide-in-from-right-2 duration-300">
{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() {
<Folder
data={chatStore.tasks[
chatStore.activeTaskId as string
]?.taskAssigning.find(
]?.taskAssigning?.find(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]

View file

@ -551,7 +551,8 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
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 = {