From 0407d588dd943ededdab4c4568e9a8872ec4fd27 Mon Sep 17 00:00:00 2001 From: sw3205933776 <3205933776@qq.com> Date: Thu, 16 Oct 2025 14:58:09 +0800 Subject: [PATCH 01/14] fix: correct ordered list rendering in Markdown where all items appeared as "1." --- src/components/ChatBox/MarkDown.tsx | 16 ++++++++-------- src/components/WorkFlow/MarkDown.tsx | 16 ++++++++-------- src/style/index.css | 5 +++++ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/components/ChatBox/MarkDown.tsx b/src/components/ChatBox/MarkDown.tsx index fd5d6ce9f..c0ce71331 100644 --- a/src/components/ChatBox/MarkDown.tsx +++ b/src/components/ChatBox/MarkDown.tsx @@ -49,7 +49,7 @@ export const MarkDown = memo( }, [content, speed, enableTypewriter, onTyping]); return ( -
+ {title} +
+ + {/* Content */} ++ {content} +
+ + {/* Action buttons */} +{children}
), ul: ({ children }) => (
+
{children}
),
pre: ({ children }) => (
-
+
{children}
),
@@ -117,7 +123,8 @@ export const MarkDown = memo(
a: ({ children, href }) => (
diff --git a/src/components/ChatBox/NoticeCard.tsx b/src/components/ChatBox/MessageItem/NoticeCard.tsx
similarity index 96%
rename from src/components/ChatBox/NoticeCard.tsx
rename to src/components/ChatBox/MessageItem/NoticeCard.tsx
index ca093ae50..ab606a85b 100644
--- a/src/components/ChatBox/NoticeCard.tsx
+++ b/src/components/ChatBox/MessageItem/NoticeCard.tsx
@@ -1,10 +1,8 @@
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
-import { TaskType } from "./TaskType";
-import { TaskItem } from "./TaskItem";
+import { TaskType } from "../TaskBox/TaskType";
+import { TaskItem } from "../TaskBox/TaskItem";
import ShinyText from "@/components/ui/ShinyText/ShinyText";
-
-
import { ChevronDown, SquareCode } from "lucide-react";
import { useMemo, useState, useRef, useEffect } from "react";
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
diff --git a/src/components/ChatBox/SummaryMarkDown.tsx b/src/components/ChatBox/MessageItem/SummaryMarkDown.tsx
similarity index 100%
rename from src/components/ChatBox/SummaryMarkDown.tsx
rename to src/components/ChatBox/MessageItem/SummaryMarkDown.tsx
diff --git a/src/components/ChatBox/MessageItem/UserMessageCard.tsx b/src/components/ChatBox/MessageItem/UserMessageCard.tsx
new file mode 100644
index 000000000..b42cc33b1
--- /dev/null
+++ b/src/components/ChatBox/MessageItem/UserMessageCard.tsx
@@ -0,0 +1,157 @@
+import { Copy, FileText, X, Image } from "lucide-react";
+import { Button } from "../../ui/button";
+import { cn } from "@/lib/utils";
+import { useState, useRef, useEffect } from "react";
+
+interface UserMessageCardProps {
+ id: string;
+ content: string;
+ className?: string;
+ attaches?: File[];
+}
+
+export function UserMessageCard({
+ id,
+ content,
+ className,
+ attaches,
+}: UserMessageCardProps) {
+ const [hoveredFilePath, setHoveredFilePath] = useState(null);
+ const [isRemainingOpen, setIsRemainingOpen] = useState(false);
+ const remainingRef = useRef(null);
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(content);
+ };
+
+ useEffect(() => {
+ const onDocClick = (e: MouseEvent) => {
+ if (!remainingRef.current) return;
+ if (!remainingRef.current.contains(e.target as Node)) {
+ setIsRemainingOpen(false);
+ }
+ };
+ document.addEventListener("mousedown", onDocClick);
+ return () => document.removeEventListener("mousedown", onDocClick);
+ }, []);
+
+ const getFileIcon = (fileName: string) => {
+ const ext = fileName.split(".").pop()?.toLowerCase() || "";
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(ext)) {
+ return ;
+ }
+ return ;
+ };
+
+ return (
+
+
+
+
+
+ {content}
+
+ {attaches && attaches.length > 0 && (
+
+ {(() => {
+ // Show max 4 files + count indicator
+ const maxVisibleFiles = 4;
+ const visibleFiles = attaches.slice(0, maxVisibleFiles);
+ const remainingCount = attaches.length > maxVisibleFiles ? attaches.length - maxVisibleFiles : 0;
+
+ return (
+ <>
+ {visibleFiles.map((file) => {
+ const isHovered = hoveredFilePath === file.filePath;
+ return (
+ setHoveredFilePath(file.filePath)}
+ onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
+ onClick={(e) => {
+ e.stopPropagation();
+ window.ipcRenderer.invoke("reveal-in-folder", file.filePath);
+ }}
+ >
+ {/* File icon */}
+
+ {getFileIcon(file.fileName)}
+
+
+ {/* File Name */}
+
+ {file.fileName}
+
+
+ );
+ })}
+
+ {/* Show remaining count if more than 4 files */}
+ {remainingCount > 0 && (
+
+
+ {isRemainingOpen && (
+
+
+ {attaches.slice(maxVisibleFiles, maxVisibleFiles + 5).map((file) => {
+ const isHovered = hoveredFilePath === file.filePath;
+ return (
+ setHoveredFilePath(file.filePath)}
+ onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
+ onClick={(e) => {
+ e.stopPropagation();
+ window.ipcRenderer.invoke("reveal-in-folder", file.filePath);
+ setIsRemainingOpen(false);
+ }}
+ >
+
+ {getFileIcon(file.fileName)}
+
+
+ {file.fileName}
+
+
+ );
+ })}
+
+
+ )}
+
+ )}
+ >
+ );
+ })()}
+
+ )}
+
+ );
+}
+
diff --git a/src/components/ChatBox/ProjectChatContainer.tsx b/src/components/ChatBox/ProjectChatContainer.tsx
index 1f4f0e7b8..15a3fcb94 100644
--- a/src/components/ChatBox/ProjectChatContainer.tsx
+++ b/src/components/ChatBox/ProjectChatContainer.tsx
@@ -134,7 +134,7 @@ export const ProjectChatContainer: React.FC = ({
return (
{chatStores.map(({ chatId, chatStore }) => {
@@ -146,9 +146,12 @@ export const ProjectChatContainer: React.FC = ({
}
const task = chatState.tasks[activeTaskId];
- const hasMessages = task.messages.length > 0 || task.hasMessages;
-
- if (!hasMessages) {
+ const messages = task.messages || [];
+
+ // Only render if there are actual user messages (not just empty or system messages)
+ const hasUserMessages = messages.some((msg: any) => msg.role === 'user' && msg.content);
+
+ if (!hasUserMessages) {
return null;
}
diff --git a/src/components/ChatBox/ProjectSection.tsx b/src/components/ChatBox/ProjectSection.tsx
index c48c00f90..ae361674a 100644
--- a/src/components/ChatBox/ProjectSection.tsx
+++ b/src/components/ChatBox/ProjectSection.tsx
@@ -43,7 +43,7 @@ export const ProjectSection = React.forwardRef
{/* User Query Groups */}
diff --git a/src/components/ChatBox/TaskCard.tsx b/src/components/ChatBox/TaskBox/TaskCard.tsx
similarity index 95%
rename from src/components/ChatBox/TaskCard.tsx
rename to src/components/ChatBox/TaskBox/TaskCard.tsx
index 26d7663f7..1815c5b0a 100644
--- a/src/components/ChatBox/TaskCard.tsx
+++ b/src/components/ChatBox/TaskBox/TaskCard.tsx
@@ -21,7 +21,7 @@ import {
CircleSlash,
} from "lucide-react";
import { useMemo, useState, useRef, useEffect } from "react";
-import { TaskState, TaskStateType } from "../TaskState";
+import { TaskState, TaskStateType } from "@/components/TaskState";
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
interface TaskCardProps {
@@ -171,16 +171,16 @@ export function TaskCard({
return (
-
-
+
+
-
- {summaryTask
- ? summaryTask.split("|")[0].replace(/"/g, "")
- : "Thinking hard..."}
-
+ {summaryTask && (
+
+ {summaryTask.split("|")[0].replace(/"/g, "")}
+
+ )}
{summaryTask && (
@@ -405,7 +405,7 @@ export function TaskCard({
-
+
diff --git a/src/components/ChatBox/UserQueryGroup.tsx b/src/components/ChatBox/UserQueryGroup.tsx
index 7776bba68..3a3380309 100644
--- a/src/components/ChatBox/UserQueryGroup.tsx
+++ b/src/components/ChatBox/UserQueryGroup.tsx
@@ -1,10 +1,12 @@
import React, { useRef, useEffect, useState } from 'react';
import { motion, useMotionValue, useTransform } from 'framer-motion';
-import { MessageCard } from './MessageCard';
-import { NoticeCard } from './NoticeCard';
-import { TypeCardSkeleton } from './TypeCardSkeleton';
-import { TaskCard } from './TaskCard';
+import { UserMessageCard } from './MessageItem/UserMessageCard';
+import { AgentMessageCard } from './MessageItem/AgentMessageCard';
+import { NoticeCard } from './MessageItem/NoticeCard';
+import { TypeCardSkeleton } from './TaskBox/TypeCardSkeleton';
+import { TaskCard } from './TaskBox/TaskCard';
import { VanillaChatStore } from '@/store/chatStore';
+import { FileText } from 'lucide-react';
interface QueryGroup {
queryId: string;
@@ -36,17 +38,19 @@ export const UserQueryGroup: React.FC = ({
const chatState = chatStore.getState();
const activeTaskId = chatState.activeTaskId;
- // 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 &&
- chatState.tasks[activeTaskId] &&
- 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';
+ // Get the active task
+ const activeTask = activeTaskId ? chatState.tasks[activeTaskId] : null;
- const task = (queryGroup.taskMessage || isLastUserQuery) && activeTaskId ? chatState.tasks[activeTaskId] : null;
+ // Check if this query group's user message matches the first user message in the active task
+ // This handles the splitting state before taskMessage is added to the group
+ const isQueryGroupForActiveTask = activeTask &&
+ activeTask.messages.length > 0 &&
+ activeTask.messages[0].id === queryGroup.userMessage?.id;
+
+ // Show task if this query group has a task message OR if it's the query for the active task
+ const task = (queryGroup.taskMessage || isQueryGroupForActiveTask) && activeTaskId
+ ? chatState.tasks[activeTaskId]
+ : null;
// Set up intersection observer for this query group
useEffect(() => {
@@ -113,10 +117,13 @@ export const UserQueryGroup: React.FC = ({
}, [task]);
// Check if we're in skeleton phase
+ const anyToSubTasksMessage = task?.messages.find((m: any) => m.step === "to_sub_tasks");
const isSkeletonPhase = task && (
- (!task.messages.find((m: any) => m.step === "to_sub_tasks") &&
- !task.hasWaitComfirm && task.messages.length > 0) ||
- task.isTakeControl
+ (task.status !== 'finished' &&
+ !anyToSubTasksMessage &&
+ !task.hasWaitComfirm &&
+ task.messages.length > 0) ||
+ (task.isTakeControl && !anyToSubTasksMessage)
);
return (
@@ -136,19 +143,17 @@ export const UserQueryGroup: React.FC = ({
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
- className="px-2 py-sm"
+ className="pl-sm py-sm"
>
- {}}
attaches={queryGroup.userMessage.attaches}
/>
- {/* Sticky Task Box - Show for each query group that has a task */}
- {task && (
+ {/* Sticky Task Box - Show only when task exists and NOT in skeleton phase */}
+ {task && !isSkeletonPhase && (
= ({
initial={{ opacity: 0, y: 20 }}
animate={{
opacity: 1,
- y: 0,
- paddingTop: isTaskBoxSticky ? 0 : 8,
- paddingBottom: isTaskBoxSticky ? 0 : 8,
- paddingLeft: isTaskBoxSticky ? 0 : 8,
- paddingRight: isTaskBoxSticky ? 0 : 8
+ y: 0
}}
transition={{
duration: 0.3,
- delay: 0.1, // Slight delay for sequencing
- paddingTop: { duration: 0.3, ease: "easeInOut" },
- paddingBottom: { duration: 0.3, ease: "easeInOut" },
- paddingLeft: { duration: 0.3, ease: "easeInOut" },
- paddingRight: { duration: 0.3, ease: "easeInOut" }
+ delay: 0.1 // Slight delay for sequencing
}}
>
= ({
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
- className="flex flex-col gap-4"
+ className="flex flex-col pl-3 gap-4"
>
- {}}
/>
{/* File List */}
{message.fileList && (
-
+
{message.fileList.map((file: any) => (
= ({
);
} else if (message.content === "skip") {
return (
-
+ {}}
/>
+
);
} else {
return (
-
+ {}}
attaches={message.attaches}
/>
+
);
}
} else if (message.step === "end" && message.content === "") {
@@ -294,7 +304,7 @@ export const UserQueryGroup: React.FC = ({
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
- className="flex flex-col gap-4"
+ className="flex flex-col pl-3 gap-4"
>
{message.fileList && (
@@ -308,10 +318,11 @@ export const UserQueryGroup: React.FC = ({
chatState.setSelectedFile(activeTaskId as string, file);
chatState.setActiveWorkSpace(activeTaskId as string, "documentWorkSpace");
}}
- className="flex items-center gap-2 bg-message-fill-default rounded-sm px-2 py-1 w-[140px] cursor-pointer hover:bg-message-fill-hover transition-colors"
- >
+ className="flex items-center gap-2 bg-message-fill-default rounded-2xl px-2 py-1 w-[120px] cursor-pointer hover:bg-message-fill-hover transition-colors"
+ >
+
-
+
{file.name.split(".")[0]}
diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx
index 7e3f8af3f..224ca0d18 100644
--- a/src/components/ChatBox/index.tsx
+++ b/src/components/ChatBox/index.tsx
@@ -611,10 +611,11 @@ export default function ChatBox(): JSX.Element {
// Check if any chat store in the project has messages
const hasAnyMessages = useMemo(() => {
// First check current active chat store
- if (chatStore.activeTaskId &&
- (chatStore.tasks[chatStore.activeTaskId].messages.length > 0 ||
- chatStore.tasks[chatStore.activeTaskId as string]?.hasMessages)) {
- return true;
+ if (chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId]) {
+ const activeTask = chatStore.tasks[chatStore.activeTaskId];
+ if ((activeTask.messages && activeTask.messages.length > 0) || activeTask.hasMessages) {
+ return true;
+ }
}
// Then check all other chat stores in the project
@@ -628,11 +629,9 @@ export default function ChatBox(): JSX.Element {
}, [chatStore, getAllChatStoresMemoized]);
return (
-
+
{hasAnyMessages ? (
-
-
-
+
{/* New Project Chat Container */}
) : (
// Init ChatBox
-
-
+
+
diff --git a/src/components/Folder/index.tsx b/src/components/Folder/index.tsx
index a8e734bf1..aed6ae65f 100644
--- a/src/components/Folder/index.tsx
+++ b/src/components/Folder/index.tsx
@@ -13,7 +13,7 @@ import {
import { Button } from "@/components/ui/button";
import FolderComponent from "./FolderComponent";
-import { MarkDown } from "@/components/ChatBox/MarkDown";
+import { MarkDown } from "@/components/ChatBox/MessageItem/MarkDown";
import { useAuthStore } from "@/store/authStore";
import { proxyFetchGet } from "@/api/http";
import { useTranslation } from "react-i18next";
diff --git a/src/components/HistorySidebar/index.tsx b/src/components/HistorySidebar/index.tsx
index 26686915e..4d238749b 100644
--- a/src/components/HistorySidebar/index.tsx
+++ b/src/components/HistorySidebar/index.tsx
@@ -330,7 +330,7 @@ export default function HistorySidebar() {
/>
- {task?.messages[0]?.content || t("layout.new-project")}
+ {task?.messages?.[0]?.content || t("layout.new-project")}
diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx
index e20aea664..54053ea94 100644
--- a/src/components/TopBar/index.tsx
+++ b/src/components/TopBar/index.tsx
@@ -14,10 +14,17 @@ import {
ChevronLeft,
House,
Share,
+ MoreHorizontal,
} from "lucide-react";
import "./index.css";
import folderIcon from "@/assets/Folder.svg";
import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
import { useLocation, useNavigate } from "react-router-dom";
import { useSidebarStore } from "@/store/sidebarStore";
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
@@ -140,6 +147,8 @@ function HeaderWin() {
const handleEndProject = async () => {
const taskId = chatStore.activeTaskId;
+ const currentProjectId = projectStore.activeProjectId;
+
if (!taskId) {
toast.error(t("layout.no-active-project-to-end"));
return;
@@ -172,9 +181,9 @@ function HeaderWin() {
// Remove from local store
chatStore.removeTask(taskId);
- // Create a new project
- const newTaskId = chatStore.create();
- chatStore.setActiveTaskId(newTaskId);
+ // Create a completely new project instead of just a new task
+ // This ensures we start fresh without any residual state
+ projectStore.createProject("new project");
// Navigate to home
navigate("/");
@@ -284,49 +293,6 @@ function HeaderWin() {
platform === "darwin" && "pr-2"
} flex h-full items-center space-x-1 z-50 relative no-drag gap-1`}
>
- {chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && (
- <>
-
-
-
- >
- )}
-
-
-
- {chatStore.activeTaskId &&
- chatStore.tasks[chatStore.activeTaskId as string]?.status === 'finished' && (
-
-
-
- )}
{chatStore.activeTaskId &&
chatStore.tasks[chatStore.activeTaskId as string] &&
(
@@ -346,6 +312,50 @@ function HeaderWin() {
)}
+ {chatStore.activeTaskId &&
+ chatStore.tasks[chatStore.activeTaskId as string]?.status === 'finished' && (
+
+
+
+ )}
+
+
+
+
+
+ {chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && (
+
+
+ {t("layout.report-bug")}
+
+ )}
+
+
+ {t("layout.refer-friends")}
+
+ navigate("/history?tab=settings")} className="cursor-pointer">
+
+ {t("layout.settings")}
+
+
+
)}
{location.pathname === "/history" && (
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
index 86f5bcc82..648a44e0c 100644
--- a/src/components/ui/dropdown-menu.tsx
+++ b/src/components/ui/dropdown-menu.tsx
@@ -63,10 +63,15 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
- "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border border-solid border-zinc-200 bg-white-100% p-xs text-popover-foreground shadow-md",
+ "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden p-xs text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
+ style={{
+ borderRadius: 'var(--borderRadius-rounded-xl, 0.75rem)',
+ border: '1px solid var(--dropdown-border, #CCC)',
+ background: 'var(--dropdown-bg, #FFF)'
+ }}
{...props}
/>
@@ -82,7 +87,7 @@ const DropdownMenuItem = React.forwardRef<
svg]:size-4 [&>svg]:shrink-0",
+ "relative flex cursor-default select-none items-center gap-2 rounded-lg px-2 py-1.5 text-sm outline-none transition-colors bg-menubutton-fill-default data-[highlighted]:bg-menubutton-fill-hover focus:bg-menubutton-fill-active focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
diff --git a/src/pages/Dashboard/Project.tsx b/src/pages/Dashboard/Project.tsx
index 0153c7954..7512a54f4 100644
--- a/src/pages/Dashboard/Project.tsx
+++ b/src/pages/Dashboard/Project.tsx
@@ -379,11 +379,11 @@ export default function Project() {
0 &&
+ task.taskAssigning && task.taskAssigning.length > 0 &&
"border-x border-white-100%"
}`}
>
- {task.taskAssigning.map((taskAssigning) => (
+ {task.taskAssigning && task.taskAssigning.map((taskAssigning) => (
Loading...;
}
- const [activeTab, setActiveTab] = useState<"projects" | "workers" | "trigger" | "settings" | "mcp_tools">("projects");
+
+ // Get initial tab from URL parameter, default to "projects"
+ const getInitialTab = () => {
+ const tabFromUrl = searchParams.get('tab');
+ const validTabs = ["projects", "workers", "trigger", "settings", "mcp_tools"];
+ return validTabs.includes(tabFromUrl || "") ? tabFromUrl as typeof activeTab : "projects";
+ };
+
+ const [activeTab, setActiveTab] = useState<"projects" | "workers" | "trigger" | "settings" | "mcp_tools">(getInitialTab);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const scrollContainerRef = useRef(null);
const HAS_STACK_KEYS = hasStackKeys();
@@ -70,7 +79,14 @@ export default function Home() {
navigate("/");
};
- useEffect(() => {}, []);
+ useEffect(() => {
+ // Update active tab when URL parameter changes
+ const tabFromUrl = searchParams.get('tab');
+ const validTabs = ["projects", "workers", "trigger", "settings", "mcp_tools"];
+ if (tabFromUrl && validTabs.includes(tabFromUrl)) {
+ setActiveTab(tabFromUrl as typeof activeTab);
+ }
+ }, [searchParams]);
return (
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index dea644d6d..414e9f89e 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -75,19 +75,18 @@ export default function Home() {
// capture webview
const captureWebview = async () => {
- if (
- chatStore.tasks[chatStore.activeTaskId as string].status === "finished"
- ) {
+ const activeTask = chatStore.tasks[chatStore.activeTaskId as string];
+ if (!activeTask || activeTask.status === "finished") {
return;
}
webviews.map((webview) => {
window.ipcRenderer
.invoke("capture-webview", webview.id)
.then((base64: string) => {
- if (chatStore.tasks[chatStore.activeTaskId as string].type) return;
+ const currentTask = chatStore.tasks[chatStore.activeTaskId as string];
+ if (!currentTask || currentTask.type) return;
let taskAssigning = [
- ...chatStore.tasks[chatStore.activeTaskId as string]
- .taskAssigning,
+ ...currentTask.taskAssigning,
];
const searchAgentIndex = taskAssigning.findIndex(
(agent) => agent.agent_id === webview.agent_id
@@ -186,14 +185,12 @@ export default function Home() {
};
return (
-
+
-
+
-
-
diff --git a/src/routers/index.tsx b/src/routers/index.tsx
index a7dfdcb7b..193766bd5 100644
--- a/src/routers/index.tsx
+++ b/src/routers/index.tsx
@@ -8,14 +8,7 @@ const Login = lazy(() => import("@/pages/Login"));
const Signup = lazy(() => import("@/pages/SignUp"));
const Home = lazy(() => import("@/pages/Home"));
const History = lazy(() => import("@/pages/History"));
-const Setting = lazy(() => import("@/pages/Setting"));
const NotFound = lazy(() => import("@/pages/NotFound"));
-const SettingGeneral = lazy(() => import("@/pages/Setting/General"));
-const SettingPrivacy = lazy(() => import("@/pages/Setting/Privacy"));
-const SettingModels = lazy(() => import("@/pages/Setting/Models"));
-const SettingAPI = lazy(() => import("@/pages/Setting/API"));
-const SettingMCP = lazy(() => import("@/pages/Setting/MCP"));
-const MCPMarket = lazy(() => import("@/pages/Setting/MCPMarket"));
// Route guard: Check if user is logged in
const ProtectedRoute = () => {
@@ -49,16 +42,8 @@ const AppRoutes = () => (
}>
} />
} />
- }>
- {/* Setting sub-routes */}
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
+ } />
+ } />
} />
diff --git a/src/style/index.css b/src/style/index.css
index 1be6367b5..1f3e1c235 100644
--- a/src/style/index.css
+++ b/src/style/index.css
@@ -39,7 +39,6 @@ body {
width: 100%;
height: 100%;
overflow: hidden;
- border-radius: 8px;
}
@@ -92,17 +91,16 @@ body {
/* Custom ResizableHandle Styles */
.custom-resizable-handle {
- width: 4px;
+ width: 1px;
height: 100%;
- background: transparent;
+ background: rgba(200,200,200,0.3);
transition: all 0.2s ease;
position: relative;
}
.custom-resizable-handle:hover {
- background: var(--border-primary);
- width: 4px;
- border-radius: 20px;
+ background: var(--border-information);
+ width: 1px;
height: 100%;
transform: none;
}
@@ -260,15 +258,15 @@ code {
}
.scrollbar.scrolling::-webkit-scrollbar-thumb {
- background-color: rgba(156, 163, 175, 0.8);
+ background-color: rgba(156, 163, 175, 0.2);
}
.scrollbar::-webkit-scrollbar-thumb:hover {
- background-color: rgba(156, 163, 175, 0.8);
+ background-color: rgba(156, 163, 175, 0.2);
}
.scrollbar::-webkit-scrollbar-track {
- background: rgba(0, 0, 0, 0.05);
+ background: transparent;
}
.scrollbar-always-visible {
diff --git a/tailwind.config.js b/tailwind.config.js
index 791ea77a4..1293711ab 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -363,7 +363,7 @@ module.exports = {
},
menubutton: {
"fill-default": "var(--menubutton-fill-default)",
- "fill-hover": "var(--menubutton-fill-active)",
+ "fill-hover": "var(--menubutton-fill-hover)",
"fill-active": "var(--menubutton-fill-active)",
"border-active": "var(--menubutton-border-active)",
"border-default": "var(--menubutton-border-default)",
diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc
index 664fc47bd9826a3e449c88dcd75e10bb0081579c..ff3c84b3e30be08256dc9dc392a9678165af8d8a 100644
GIT binary patch
delta 55
zcmbQuIE|4jpO=@50SJNuCUV&-TIz=urxq3Kr{tHW=Oh;EyQCIpm*f}dJLjjQDmWz;
Jr%ue(0|1Zq5Pkpv
delta 60
zcmbQnIGd3xpO=@50SIEYPvo*yv(pbPPAw|dPt7Yz&D3{EEzT~e)=$YVP0vXz)^|xQ&MwI>(09&HNmX!4
MEKc40kSUA}0OV&9Jpcdz
delta 63
zcmdlav|WfRpO=@50SIEYZ{*s@q~@+4TAW%`te=`!l$xpUl3JWyl3$=-lv
Date: Thu, 23 Oct 2025 10:12:00 +0100
Subject: [PATCH 03/14] top bar fixed
---
src/components/TopBar/index.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx
index a1e099c77..dfaa2f929 100644
--- a/src/components/TopBar/index.tsx
+++ b/src/components/TopBar/index.tsx
@@ -242,7 +242,6 @@ function HeaderWin() {
onClick={() => navigate("/")}
>
- {t("layout.back")}
)}
From 86748275171de642eba612077cda40194b18a196 Mon Sep 17 00:00:00 2001
From: luo <479933015@qq.com>
Date: Thu, 6 Nov 2025 11:08:14 +0800
Subject: [PATCH 04/14] feat(chat_controller): add SSE timeout handling and
improve logging structure
---
backend/app/controller/chat_controller.py | 93 ++++++++++++++++++-----
1 file changed, 72 insertions(+), 21 deletions(-)
diff --git a/backend/app/controller/chat_controller.py b/backend/app/controller/chat_controller.py
index d9afc4acd..338537845 100644
--- a/backend/app/controller/chat_controller.py
+++ b/backend/app/controller/chat_controller.py
@@ -1,6 +1,7 @@
import asyncio
import os
import re
+import time
from pathlib import Path
from dotenv import load_dotenv
from fastapi import APIRouter, HTTPException, Request, Response
@@ -8,7 +9,7 @@ from fastapi.responses import StreamingResponse
from utils import traceroot_wrapper as traceroot
from app.component import code
from app.exception.exception import UserException
-from app.model.chat import Chat, HumanReply, McpServers, Status, SupplementChat, AddTaskRequest
+from app.model.chat import Chat, HumanReply, McpServers, Status, SupplementChat, AddTaskRequest, sse_json
from app.service.chat_service import step_solve
from app.service.task import (
Action,
@@ -30,13 +31,51 @@ from camel.tasks.task import Task
router = APIRouter(tags=["chat"])
# Create traceroot logger for chat controller
-chat_logger = traceroot.get_logger('chat_controller')
+chat_logger = traceroot.get_logger("chat_controller")
+
+# SSE timeout configuration (10 minutes in seconds)
+SSE_TIMEOUT_SECONDS = 10 * 60
+
+
+async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TIMEOUT_SECONDS):
+ last_data_time = [time.time()]
+ generator = stream_generator.__aiter__()
+ should_stop = False
+
+ try:
+ while not should_stop:
+ elapsed = time.time() - last_data_time[0]
+ remaining_timeout = max(0, timeout_seconds - elapsed)
+
+ if elapsed >= timeout_seconds:
+ chat_logger.warning(f"SSE timeout: No data received for {elapsed:.1f} seconds, closing connection")
+ yield sse_json("timeout", {"message": "Connection timeout: No data received for 10 minutes"})
+ break
+ try:
+ data = await asyncio.wait_for(generator.__anext__(), timeout=remaining_timeout)
+ last_data_time[0] = time.time()
+ yield data
+ except asyncio.TimeoutError:
+ chat_logger.warning(f"SSE timeout: No data received for {timeout_seconds} seconds, closing connection")
+ yield sse_json("timeout", {"message": "Connection timeout: No data received for 10 minutes"})
+ break
+ except StopAsyncIteration:
+ break
+
+ except asyncio.CancelledError:
+ chat_logger.info("Stream cancelled")
+ raise
+ except Exception as e:
+ chat_logger.error(f"Error in stream wrapper: {e}", exc_info=True)
+ raise
@router.post("/chat", name="start chat")
@traceroot.trace()
async def post(data: Chat, request: Request):
- chat_logger.info("Starting new chat session", extra={"project_id": data.project_id, "task_id": data.task_id, "user": data.email})
+ chat_logger.info(
+ "Starting new chat session", extra={"project_id": data.project_id, "task_id": data.task_id, "user": data.email}
+ )
task_lock = get_or_create_task_lock(data.project_id)
# Set user-specific environment path for this thread
@@ -50,7 +89,14 @@ 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 / ("project_" + data.project_id) / ("task_" + data.task_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)
@@ -61,8 +107,13 @@ async def post(data: Chat, request: Request):
# Put initial action in queue to start processing
await task_lock.put_queue(ActionImproveData(data=data.question))
- chat_logger.info("Chat session initialized, starting streaming response", extra={"project_id": data.project_id, "task_id": data.task_id, "log_dir": str(camel_log)})
- return StreamingResponse(step_solve(data, request, task_lock), media_type="text/event-stream")
+ chat_logger.info(
+ "Chat session initialized, starting streaming response",
+ extra={"project_id": data.project_id, "task_id": data.task_id, "log_dir": str(camel_log)},
+ )
+ return StreamingResponse(
+ timeout_stream_wrapper(step_solve(data, request, task_lock)), media_type="text/event-stream"
+ )
@router.post("/chat/{id}", name="improve chat")
@@ -77,14 +128,14 @@ def improve(id: str, data: SupplementChat):
# Reset status to allow processing new messages
task_lock.status = Status.confirming
# Clear any existing background tasks since workforce was stopped
- if hasattr(task_lock, 'background_tasks'):
+ if hasattr(task_lock, "background_tasks"):
task_lock.background_tasks.clear()
# Note: conversation_history and last_task_result are preserved
# Log context preservation
- if hasattr(task_lock, 'conversation_history'):
+ if hasattr(task_lock, "conversation_history"):
chat_logger.info(f"[CONTEXT] Preserved {len(task_lock.conversation_history)} conversation entries")
- if hasattr(task_lock, 'last_task_result'):
+ if hasattr(task_lock, "last_task_result"):
chat_logger.info(f"[CONTEXT] Preserved task result: {len(task_lock.last_task_result)} chars")
# Update file save path if task_id is provided
@@ -93,7 +144,7 @@ def improve(id: str, data: SupplementChat):
try:
# Get current environment values needed to construct new path
current_email = None
-
+
# Extract email from current file_save_path if available
current_file_save_path = os.environ.get("file_save_path", "")
if current_file_save_path:
@@ -102,7 +153,7 @@ def improve(id: str, data: SupplementChat):
eigent_index = path_parts.index("eigent")
if eigent_index + 1 < len(path_parts):
current_email = path_parts[eigent_index + 1]
-
+
# If we have the necessary information, update the file_save_path
if current_email and id:
# Create new path using the existing pattern: email/project_{project_id}/task_{task_id}
@@ -110,12 +161,12 @@ def improve(id: str, data: SupplementChat):
new_folder_path.mkdir(parents=True, exist_ok=True)
os.environ["file_save_path"] = str(new_folder_path)
chat_logger.info(f"Updated file_save_path to: {new_folder_path}")
-
+
# Store the new folder path in task_lock for potential cleanup and persistence
task_lock.new_folder_path = new_folder_path
else:
chat_logger.warning(f"Could not update file_save_path - email: {current_email}, project_id: {id}")
-
+
except Exception as e:
chat_logger.error(f"Error updating file path for project_id: {id}, task_id: {data.task_id}: {e}")
@@ -160,7 +211,7 @@ def human_reply(id: str, data: HumanReply):
@router.post("/chat/{id}/install-mcp")
@traceroot.trace()
def install_mcp(id: str, data: McpServers):
- chat_logger.info("Installing MCP servers", extra={"task_id": id, "servers_count": len(data.get('mcpServers', {}))})
+ chat_logger.info("Installing MCP servers", extra={"task_id": id, "servers_count": len(data.get("mcpServers", {}))})
task_lock = get_task_lock(id)
asyncio.run(task_lock.put_queue(ActionInstallMcpData(action=Action.install_mcp, data=data)))
chat_logger.info("MCP installation queued", extra={"task_id": id})
@@ -173,7 +224,7 @@ def add_task(id: str, data: AddTaskRequest):
"""Add a new task to the workforce"""
chat_logger.info(f"Adding task to workforce for task_id: {id}, content: {data.content[:100]}...")
task_lock = get_task_lock(id)
-
+
try:
# Queue the add task action
add_task_action = ActionAddTaskData(
@@ -181,11 +232,11 @@ def add_task(id: str, data: AddTaskRequest):
project_id=data.project_id,
task_id=data.task_id,
additional_info=data.additional_info,
- insert_position=data.insert_position
+ insert_position=data.insert_position,
)
asyncio.run(task_lock.put_queue(add_task_action))
return Response(status_code=201)
-
+
except Exception as e:
chat_logger.error(f"Error adding task for task_id: {id}: {e}")
raise UserException(code.error, f"Failed to add task: {str(e)}")
@@ -197,7 +248,7 @@ def remove_task(project_id: str, task_id: str):
"""Remove a task from the workforce"""
chat_logger.info(f"Removing task {task_id} from workforce for project_id: {project_id}")
task_lock = get_task_lock(project_id)
-
+
try:
# Queue the remove task action
remove_task_action = ActionRemoveTaskData(task_id=task_id, project_id=project_id)
@@ -205,7 +256,7 @@ def remove_task(project_id: str, task_id: str):
chat_logger.info(f"Task removal request queued for project_id: {project_id}, removing task: {task_id}")
return Response(status_code=204)
-
+
except Exception as e:
chat_logger.error(f"Error removing task {task_id} for project_id: {project_id}: {e}")
raise UserException(code.error, f"Failed to remove task: {str(e)}")
@@ -217,7 +268,7 @@ def skip_task(project_id: str):
"""Skip a task in the workforce"""
chat_logger.info(f"Skipping task in workforce for project_id: {project_id}")
task_lock = get_task_lock(project_id)
-
+
try:
# Queue the skip task action
skip_task_action = ActionSkipTaskData(project_id=project_id)
@@ -225,7 +276,7 @@ def skip_task(project_id: str):
chat_logger.info(f"Task skip request queued for project_id: {project_id}")
return Response(status_code=201)
-
+
except Exception as e:
chat_logger.error(f"Error skipping task for project_id: {project_id}: {e}")
raise UserException(code.error, f"Failed to skip task: {str(e)}")
From cbaa477fe8240617a448f177a70f38dcab5b31c2 Mon Sep 17 00:00:00 2001
From: luo <479933015@qq.com>
Date: Thu, 6 Nov 2025 12:45:40 +0800
Subject: [PATCH 05/14] fix(chat_controller): change SSE timeout response type
from 'timeout' to 'error'
---
backend/app/controller/chat_controller.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/backend/app/controller/chat_controller.py b/backend/app/controller/chat_controller.py
index 338537845..7500ea248 100644
--- a/backend/app/controller/chat_controller.py
+++ b/backend/app/controller/chat_controller.py
@@ -49,7 +49,7 @@ async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TI
if elapsed >= timeout_seconds:
chat_logger.warning(f"SSE timeout: No data received for {elapsed:.1f} seconds, closing connection")
- yield sse_json("timeout", {"message": "Connection timeout: No data received for 10 minutes"})
+ yield sse_json("error", {"message": "Connection timeout: No data received for 10 minutes"})
break
try:
data = await asyncio.wait_for(generator.__anext__(), timeout=remaining_timeout)
@@ -57,7 +57,7 @@ async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TI
yield data
except asyncio.TimeoutError:
chat_logger.warning(f"SSE timeout: No data received for {timeout_seconds} seconds, closing connection")
- yield sse_json("timeout", {"message": "Connection timeout: No data received for 10 minutes"})
+ yield sse_json("error", {"message": "Connection timeout: No data received for 10 minutes"})
break
except StopAsyncIteration:
break
From 57dca46e9806f5c1f55e6dc9c68f70fc0b84adb7 Mon Sep 17 00:00:00 2001
From: puzhen <1303385763@qq.com>
Date: Thu, 6 Nov 2025 10:30:55 +0100
Subject: [PATCH 06/14] fix: add isHumanReply logic to prevent duplicate task
boxes
Add isHumanReply check from main branch (#577, #602) to prevent
duplicate task boxes in human_toolkit multi-turn conversations.
This resolves the conflict with main branch by preserving the bug fix
while keeping chatbox-ux's UI improvements.
---
src/components/ChatBox/UserQueryGroup.tsx | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/components/ChatBox/UserQueryGroup.tsx b/src/components/ChatBox/UserQueryGroup.tsx
index 7f2748d33..4fba6dd31 100644
--- a/src/components/ChatBox/UserQueryGroup.tsx
+++ b/src/components/ChatBox/UserQueryGroup.tsx
@@ -40,7 +40,25 @@ 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 &&
+ activeTaskId &&
+ chatState.tasks[activeTaskId] &&
+ (chatState.tasks[activeTaskId].activeAsk ||
+ // Check if this user message follows an 'ask' message in the message sequence
+ (() => {
+ const messages = chatState.tasks[activeTaskId].messages;
+ const userMessageIndex = messages.findIndex((m: any) => m.id === queryGroup.userMessage.id);
+ if (userMessageIndex > 0) {
+ // Check the previous message - if it's an agent message with step 'ask', this is a human-reply
+ const prevMessage = messages[userMessageIndex - 1];
+ return prevMessage?.role === 'agent' && prevMessage?.step === 'ask';
+ }
+ return false;
+ })());
+
const isLastUserQuery = !queryGroup.taskMessage &&
+ !isHumanReply &&
activeTaskId &&
chatState.tasks[activeTaskId] &&
queryGroup.userMessage &&
@@ -151,7 +169,7 @@ export const UserQueryGroup: React.FC = ({
{/* Sticky Task Box - Show only when task exists and NOT in skeleton phase */}
- {task && !isSkeletonPhase && (
+ {task && !isSkeletonPhase && !isHumanReply && (
Date: Thu, 6 Nov 2025 10:39:40 +0100
Subject: [PATCH 07/14] update
---
utils/__pycache__/__init__.cpython-310.pyc | Bin 150 -> 193 bytes
.../traceroot_wrapper.cpython-310.pyc | Bin 2354 -> 2523 bytes
2 files changed, 0 insertions(+), 0 deletions(-)
diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc
index ff3c84b3e30be08256dc9dc392a9678165af8d8a..10d1d4a69c028b0fa2e5b31e6afd01340e2b871d 100644
GIT binary patch
literal 193
zcmd1j<>g`kg7uOdnT|mEF^Gc>lA4US1WSq%lT(ZG^Go8(ixLY8Qj3Z}`u#MSZ!v&bx7g$36LWIn<5w~iu>xhl
z#4ig`kf}nu!8H_;sF^Gcs
diff --git a/utils/__pycache__/traceroot_wrapper.cpython-310.pyc b/utils/__pycache__/traceroot_wrapper.cpython-310.pyc
index 8a7166a1508ae29d727cf39e1190149d77a36c7a..d695ee21dc2c1d82d5dfa3d82d7694ce6bef45e3 100644
GIT binary patch
delta 693
zcmY+B&ubGw6vyZ7&*^59G@CZgrFqw3Sinl&(
zRT6~!jLqes$}FnAH-9o)JTf}wuEi`MhZG6w9h=!`GWwVVNrUjA=d(cKg%+czGkcQ+
z@RXp9^q4S*15fmz?+!>ii96$8q$vhPjs-kf4ZT
zIIae{fSt4a|5y_jETf-CZvd>IhC1Pe{v_rHYQ(@d&rKmVI|L`+0GmSnAt_DC8&J}!
zE2o>#fQh-Kg<82Q!ZvS4HQw6cK~&q1TAjU`{G}BkEi?KTD9P{o3z(A)_?a)T2<`mR)l}J}^h*C(}ybihsjU;%SB^W=_AF0^qJ(v1?Z=
zDk};~4p*z9-sBr$7){p?>a9+_(c#mLefi$L=^rlqD+ZSv
zQX~OS9%S&QkoYImKf;@TKm;$Iym|BLtccV%%!m2DdGqD*=4<85-;N7~Jkhat|JUGK
zj*#z}I3G-ln|deY?RMwXh!|%`(2T5Oo7+H;p*17i5g>9?nCBN{w1RenpwlAU6}E7v
z07S5PVL{}+ewmAk$0c6QawCs>!aoAci^>tj6?A8em#`pKwJKt1X7Z|Vri9l-8Ou}P
z^#f5Kt)Yj$sA1(j#nm?y$aoX0Skq0MH>>|=w)WS|oHww%0NxbMc?%m0-CCbu^CR$e
zG#-+mr3MguQg8@iH+-B3nS>ah^rK!9-WJKTG@gXFViXs;y+nj!*can8JV}R95~`BX
z0Y~i`Utmiy^A&W|Yx47|2ccqpmXODlab
z8IJohvz46;n}5<6r(*nEX1}XGJ9nY2yxa}Aq#oq%?OfJLm$=~QrD@bWhdR^+i~6)^
RTFil(&iYo-&9gkX#&56+ef
Date: Thu, 6 Nov 2025 11:43:34 +0100
Subject: [PATCH 08/14] fix: add conditional rendering for userMessage to
resolve conflict with main
Added null check for queryGroup.userMessage to prevent errors when
rendering orphan messages (fix #593 from main branch).
---
src/components/ChatBox/UserQueryGroup.tsx | 28 ++++++++++++-----------
1 file changed, 15 insertions(+), 13 deletions(-)
diff --git a/src/components/ChatBox/UserQueryGroup.tsx b/src/components/ChatBox/UserQueryGroup.tsx
index 4fba6dd31..6bd2ac2f6 100644
--- a/src/components/ChatBox/UserQueryGroup.tsx
+++ b/src/components/ChatBox/UserQueryGroup.tsx
@@ -154,19 +154,21 @@ export const UserQueryGroup: React.FC = ({
}}
className="relative"
>
- {/* User Query */}
-
-
-
+ {/* User Query (render only if exists) */}
+ {queryGroup.userMessage && (
+
+
+
+ )}
{/* Sticky Task Box - Show only when task exists and NOT in skeleton phase */}
{task && !isSkeletonPhase && !isHumanReply && (
From 4424b4d7fcc17baa6ae6524d0d4442870db18aa3 Mon Sep 17 00:00:00 2001
From: Douglasymlai
Date: Thu, 6 Nov 2025 11:13:16 +0000
Subject: [PATCH 09/14] update bugs on the user message card
---
src/components/ChatBox/UserQueryGroup.tsx | 2 --
utils/__pycache__/__init__.cpython-310.pyc | Bin 193 -> 214 bytes
.../traceroot_wrapper.cpython-310.pyc | Bin 2523 -> 2544 bytes
3 files changed, 2 deletions(-)
diff --git a/src/components/ChatBox/UserQueryGroup.tsx b/src/components/ChatBox/UserQueryGroup.tsx
index 69a4b3d64..7990739b7 100644
--- a/src/components/ChatBox/UserQueryGroup.tsx
+++ b/src/components/ChatBox/UserQueryGroup.tsx
@@ -164,9 +164,7 @@ export const UserQueryGroup: React.FC = ({
>
{}}
attaches={queryGroup.userMessage.attaches}
/>
diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc
index 10d1d4a69c028b0fa2e5b31e6afd01340e2b871d..57ecb1ad7b408569a80294e611f22351efca17cb 100644
GIT binary patch
delta 70
zcmX@ec#V-epO=@50SFE^@=WBe(znzPElw>e)=$YVP0vXz)^|xQ&MwI>(09&HNmX!4
YEKb!=%}h_tE7314$;>I%pEzF$0P#E*KL7v#
delta 49
zcmcb{c#x4hpO=@50SMMha!ll|l2UQDig7F`%FjwoE{RFaOi#@#i773~%qfnUxJ3y7
DNuv*y
diff --git a/utils/__pycache__/traceroot_wrapper.cpython-310.pyc b/utils/__pycache__/traceroot_wrapper.cpython-310.pyc
index d695ee21dc2c1d82d5dfa3d82d7694ce6bef45e3..b8d9747f6f81fdae3f1f8ee704ee23bf51b8513c 100644
GIT binary patch
delta 73
zcmcaD{6UyIpO=@50SFE^@@(XO#iZ|~A6lGRRIHzpUz(niSgh}oTAW>yU!d=tpOUKJ
blvtdqpPHGTnpdJyHFvk=Vy
From 03f8b1d246caf7ad0f9b63faa63683e836c7dc90 Mon Sep 17 00:00:00 2001
From: Wendong-Fan
Date: Fri, 7 Nov 2025 14:46:27 +0800
Subject: [PATCH 10/14] enhance: add SSE timeout handling and improve logging
PR614
---
backend/app/controller/chat_controller.py | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/backend/app/controller/chat_controller.py b/backend/app/controller/chat_controller.py
index 3f0b96ce9..9342b20d6 100644
--- a/backend/app/controller/chat_controller.py
+++ b/backend/app/controller/chat_controller.py
@@ -38,22 +38,22 @@ SSE_TIMEOUT_SECONDS = 10 * 60
async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TIMEOUT_SECONDS):
- last_data_time = [time.time()]
+ """
+ Wraps a stream generator with timeout handling.
+
+ Closes the SSE connection if no data is received within the timeout period.
+ """
+ last_data_time = time.time()
generator = stream_generator.__aiter__()
- should_stop = False
try:
- while not should_stop:
- elapsed = time.time() - last_data_time[0]
- remaining_timeout = max(0, timeout_seconds - elapsed)
+ while True:
+ elapsed = time.time() - last_data_time
+ remaining_timeout = timeout_seconds - elapsed
- if elapsed >= timeout_seconds:
- chat_logger.warning(f"SSE timeout: No data received for {elapsed:.1f} seconds, closing connection")
- yield sse_json("error", {"message": "Connection timeout: No data received for 10 minutes"})
- break
try:
data = await asyncio.wait_for(generator.__anext__(), timeout=remaining_timeout)
- last_data_time[0] = time.time()
+ last_data_time = time.time()
yield data
except asyncio.TimeoutError:
chat_logger.warning(f"SSE timeout: No data received for {timeout_seconds} seconds, closing connection")
From d0d15ee66dd7f1f9d3415adb918010361980419e Mon Sep 17 00:00:00 2001
From: Wendong-Fan
Date: Sat, 8 Nov 2025 14:35:47 +0800
Subject: [PATCH 11/14] enhance: Reorganize ChatBox components and enhance UX
PR567
---
src/components/ChatBox/MessageItem/MarkDown.tsx | 6 +++---
.../ChatBox/MessageItem/UserMessageCard.tsx | 14 ++++++--------
src/components/ChatBox/UserQueryGroup.tsx | 4 ++--
3 files changed, 11 insertions(+), 13 deletions(-)
diff --git a/src/components/ChatBox/MessageItem/MarkDown.tsx b/src/components/ChatBox/MessageItem/MarkDown.tsx
index 06ed27347..0dfb54d94 100644
--- a/src/components/ChatBox/MessageItem/MarkDown.tsx
+++ b/src/components/ChatBox/MessageItem/MarkDown.tsx
@@ -23,6 +23,9 @@ export const MarkDown = memo(
useEffect(() => {
if (!enableTypewriter) {
setDisplayedContent(content);
+ if (onTyping) {
+ onTyping();
+ }
return;
}
@@ -33,9 +36,6 @@ export const MarkDown = memo(
if (index < content.length) {
setDisplayedContent(content.slice(0, index + 1));
index++;
- if (onTyping) {
- onTyping();
- }
} else {
clearInterval(timer);
// when typewriter effect is completed, call callback
diff --git a/src/components/ChatBox/MessageItem/UserMessageCard.tsx b/src/components/ChatBox/MessageItem/UserMessageCard.tsx
index b42cc33b1..c83b83626 100644
--- a/src/components/ChatBox/MessageItem/UserMessageCard.tsx
+++ b/src/components/ChatBox/MessageItem/UserMessageCard.tsx
@@ -67,12 +67,11 @@ export function UserMessageCard({
return (
<>
{visibleFiles.map((file) => {
- const isHovered = hoveredFilePath === file.filePath;
return (
setHoveredFilePath(file.filePath)}
onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
@@ -82,7 +81,7 @@ export function UserMessageCard({
}}
>
{/* File icon */}
-
+
{getFileIcon(file.fileName)}
@@ -98,7 +97,7 @@ export function UserMessageCard({
);
})}
-
+
{/* Show remaining count if more than 4 files */}
{remainingCount > 0 && (
@@ -118,12 +117,11 @@ export function UserMessageCard({
{isRemainingOpen && (
- {attaches.slice(maxVisibleFiles, maxVisibleFiles + 5).map((file) => {
- const isHovered = hoveredFilePath === file.filePath;
+ {attaches.slice(maxVisibleFiles).map((file) => {
return (
setHoveredFilePath(file.filePath)}
onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
onClick={(e) => {
@@ -132,7 +130,7 @@ export function UserMessageCard({
setIsRemainingOpen(false);
}}
>
-
+
{getFileIcon(file.fileName)}
diff --git a/src/components/ChatBox/UserQueryGroup.tsx b/src/components/ChatBox/UserQueryGroup.tsx
index 7990739b7..71ae69d9a 100644
--- a/src/components/ChatBox/UserQueryGroup.tsx
+++ b/src/components/ChatBox/UserQueryGroup.tsx
@@ -279,7 +279,7 @@ export const UserQueryGroup: React.FC = ({
} else if (message.content === "skip") {
return (
= ({
} else {
return (
Date: Mon, 10 Nov 2025 10:19:08 +0100
Subject: [PATCH 12/14] fixed undefined function for file upload & file display
popover
---
src/components/ChatBox/BottomBox/InputBox.tsx | 133 ++++++++++--------
src/components/ChatBox/BottomBox/index.tsx | 4 +-
.../ChatBox/MessageItem/UserMessageCard.tsx | 126 +++++++++--------
3 files changed, 147 insertions(+), 116 deletions(-)
diff --git a/src/components/ChatBox/BottomBox/InputBox.tsx b/src/components/ChatBox/BottomBox/InputBox.tsx
index 29a606da5..0a6fefd26 100644
--- a/src/components/ChatBox/BottomBox/InputBox.tsx
+++ b/src/components/ChatBox/BottomBox/InputBox.tsx
@@ -1,6 +1,7 @@
import { useState, useRef, useEffect } from "react";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
+import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { Paperclip, ArrowRight, X, Image, FileText, UploadCloud, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
@@ -108,18 +109,25 @@ export const Inputbox = ({
const dragCounter = useRef(0);
const [hoveredFilePath, setHoveredFilePath] = useState(null);
const [isRemainingOpen, setIsRemainingOpen] = useState(false);
- const remainingRef = useRef(null);
+ const hoverCloseTimerRef = useRef(null);
- useEffect(() => {
- const onDocClick = (e: MouseEvent) => {
- if (!remainingRef.current) return;
- if (!remainingRef.current.contains(e.target as Node)) {
- setIsRemainingOpen(false);
- }
- };
- document.addEventListener("mousedown", onDocClick);
- return () => document.removeEventListener("mousedown", onDocClick);
- }, []);
+ const openRemainingPopover = () => {
+ if (hoverCloseTimerRef.current) {
+ window.clearTimeout(hoverCloseTimerRef.current);
+ hoverCloseTimerRef.current = null;
+ }
+ setIsRemainingOpen(true);
+ };
+
+ const scheduleCloseRemainingPopover = () => {
+ if (hoverCloseTimerRef.current) {
+ window.clearTimeout(hoverCloseTimerRef.current);
+ }
+ hoverCloseTimerRef.current = window.setTimeout(() => {
+ setIsRemainingOpen(false);
+ hoverCloseTimerRef.current = null;
+ }, 150);
+ };
// Auto-resize textarea on value changes (hug content up to max height)
useEffect(() => {
@@ -330,57 +338,64 @@ export const Inputbox = ({
})}
{/* Show remaining count if more than 5 files */}
{remainingCount > 0 && (
-
-
- {isRemainingOpen && (
-
-
- {files.slice(maxVisibleFiles, maxVisibleFiles + 5).map((file) => {
- const isHovered = hoveredFilePath === file.filePath;
- return (
- setHoveredFilePath(file.filePath)}
- onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
+
+ {files.slice(maxVisibleFiles).map((file) => {
+ const isHovered = hoveredFilePath === file.filePath;
+ return (
+ setHoveredFilePath(file.filePath)}
+ onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
+ >
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ handleRemoveFile(file.filePath);
+ setIsRemainingOpen(false);
+ }}
+ title={isHovered ? "Remove file" : file.fileName}
>
- {
- e.preventDefault();
- e.stopPropagation();
- handleRemoveFile(file.filePath);
- setIsRemainingOpen(false);
- }}
- title={isHovered ? "Remove file" : file.fileName}
- >
- {isHovered ? : getFileIcon(file.fileName)}
-
-
- {file.fileName}
-
-
- );
- })}
-
+ {isHovered ? : getFileIcon(file.fileName)}
+
+
+ {file.fileName}
+
+
+ );
+ })}
- )}
-
+
+
)}
)}
diff --git a/src/components/ChatBox/BottomBox/index.tsx b/src/components/ChatBox/BottomBox/index.tsx
index 6d1275c24..18ae00dc5 100644
--- a/src/components/ChatBox/BottomBox/index.tsx
+++ b/src/components/ChatBox/BottomBox/index.tsx
@@ -71,7 +71,7 @@ export default function BottomBox({
else if (state === "confirm") backgroundClass = "bg-input-bg-confirm";
return (
-
+
{/* QueuedBox overlay (should not affect BoxMain layout) */}
{queuedMessages.length > 0 && (
@@ -82,7 +82,7 @@ export default function BottomBox({
)}
{/* BoxMain */}
-
+
{/* BoxHeader variants */}
{state === "splitting" && (
diff --git a/src/components/ChatBox/MessageItem/UserMessageCard.tsx b/src/components/ChatBox/MessageItem/UserMessageCard.tsx
index b42cc33b1..bdbc1eb38 100644
--- a/src/components/ChatBox/MessageItem/UserMessageCard.tsx
+++ b/src/components/ChatBox/MessageItem/UserMessageCard.tsx
@@ -1,5 +1,6 @@
import { Copy, FileText, X, Image } from "lucide-react";
import { Button } from "../../ui/button";
+import { Popover, PopoverTrigger, PopoverContent } from "../../ui/popover";
import { cn } from "@/lib/utils";
import { useState, useRef, useEffect } from "react";
@@ -17,23 +18,31 @@ export function UserMessageCard({
attaches,
}: UserMessageCardProps) {
const [hoveredFilePath, setHoveredFilePath] = useState(null);
- const [isRemainingOpen, setIsRemainingOpen] = useState(false);
- const remainingRef = useRef(null);
-
- const handleCopy = () => {
+ const [isRemainingOpen, setIsRemainingOpen] = useState(false);
+ const hoverCloseTimerRef = useRef(null);
+
+ const handleCopy = () => {
navigator.clipboard.writeText(content);
};
- useEffect(() => {
- const onDocClick = (e: MouseEvent) => {
- if (!remainingRef.current) return;
- if (!remainingRef.current.contains(e.target as Node)) {
- setIsRemainingOpen(false);
+ // Popover handles outside clicks; no manual listener needed
+ const openRemainingPopover = () => {
+ if (hoverCloseTimerRef.current) {
+ window.clearTimeout(hoverCloseTimerRef.current);
+ hoverCloseTimerRef.current = null;
}
+ setIsRemainingOpen(true);
+ };
+
+ const scheduleCloseRemainingPopover = () => {
+ if (hoverCloseTimerRef.current) {
+ window.clearTimeout(hoverCloseTimerRef.current);
+ }
+ hoverCloseTimerRef.current = window.setTimeout(() => {
+ setIsRemainingOpen(false);
+ hoverCloseTimerRef.current = null;
+ }, 150);
};
- document.addEventListener("mousedown", onDocClick);
- return () => document.removeEventListener("mousedown", onDocClick);
- }, []);
const getFileIcon = (fileName: string) => {
const ext = fileName.split(".").pop()?.toLowerCase() || "";
@@ -46,7 +55,7 @@ export function UserMessageCard({
return (
@@ -72,7 +81,8 @@ export function UserMessageCard({
setHoveredFilePath(file.filePath)}
onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
@@ -101,50 +111,56 @@ export function UserMessageCard({
{/* Show remaining count if more than 4 files */}
{remainingCount > 0 && (
-
- {
- e.stopPropagation();
- setIsRemainingOpen((v) => !v);
- }}
+
+
+ {
+ e.stopPropagation();
+ }}
+ >
+
+ {remainingCount}+
+
+
+
+
-
- {remainingCount}+
-
-
- {isRemainingOpen && (
-
-
- {attaches.slice(maxVisibleFiles, maxVisibleFiles + 5).map((file) => {
- const isHovered = hoveredFilePath === file.filePath;
- return (
- setHoveredFilePath(file.filePath)}
- onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
- onClick={(e) => {
- e.stopPropagation();
- window.ipcRenderer.invoke("reveal-in-folder", file.filePath);
- setIsRemainingOpen(false);
- }}
- >
-
- {getFileIcon(file.fileName)}
-
-
- {file.fileName}
-
+
+ {attaches.slice(maxVisibleFiles).map((file) => {
+ const isHovered = hoveredFilePath === file.filePath;
+ return (
+ setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
+ onClick={(e) => {
+ e.stopPropagation();
+ window.ipcRenderer.invoke("reveal-in-folder", file.filePath);
+ setIsRemainingOpen(false);
+ }}
+ >
+
+ {getFileIcon(file.fileName)}
- );
- })}
-
+
+ {file.fileName}
+
+
+ );
+ })}
- )}
-
+
+
)}
>
);
From 6a6e0ff168a4518c28e3b5ce0ce86e9a1900aef8 Mon Sep 17 00:00:00 2001
From: Douglasymlai
Date: Mon, 10 Nov 2025 10:23:59 +0100
Subject: [PATCH 13/14] fix input box Chinese keyboard return problem
---
src/components/ChatBox/BottomBox/InputBox.tsx | 61 ++++++++++---------
1 file changed, 32 insertions(+), 29 deletions(-)
diff --git a/src/components/ChatBox/BottomBox/InputBox.tsx b/src/components/ChatBox/BottomBox/InputBox.tsx
index 0a6fefd26..c9f332a98 100644
--- a/src/components/ChatBox/BottomBox/InputBox.tsx
+++ b/src/components/ChatBox/BottomBox/InputBox.tsx
@@ -110,6 +110,7 @@ export const Inputbox = ({
const [hoveredFilePath, setHoveredFilePath] = useState(null);
const [isRemainingOpen, setIsRemainingOpen] = useState(false);
const hoverCloseTimerRef = useRef(null);
+ const [isComposing, setIsComposing] = useState(false);
const openRemainingPopover = () => {
if (hoverCloseTimerRef.current) {
@@ -156,7 +157,7 @@ export const Inputbox = ({
};
const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter" && !e.shiftKey && !disabled) {
+ if (e.key === "Enter" && !e.shiftKey && !disabled && !isComposing) {
e.preventDefault();
handleSend();
}
@@ -259,34 +260,36 @@ export const Inputbox = ({
{/* Text Input Area */}
-
From 7807131df64e0218fe8e0447592facd015788a11 Mon Sep 17 00:00:00 2001
From: Douglasymlai
Date: Mon, 10 Nov 2025 10:29:46 +0100
Subject: [PATCH 14/14] fixed return to edit input state files lost issue
---
src/components/ChatBox/index.tsx | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx
index 688c8be56..0ef498a10 100644
--- a/src/components/ChatBox/index.tsx
+++ b/src/components/ChatBox/index.tsx
@@ -479,9 +479,16 @@ export default function ChatBox(): JSX.Element {
const messageIndex = chatStore.tasks[taskId].messages.findLastIndex(
(item) => item.step === "to_sub_tasks"
);
- const question = chatStore.tasks[taskId].messages[messageIndex - 2].content;
+ const questionMessage = chatStore.tasks[taskId].messages[messageIndex - 2];
+ const question = questionMessage.content;
+ // Get the file attachments from the original user message (not from task.attaches which gets cleared after sending)
+ const attachments = questionMessage.attaches || [];
let id = chatStore.create();
chatStore.setHasMessages(id, true);
+ // Copy the file attachments to the new task
+ if (attachments.length > 0) {
+ chatStore.setAttaches(id, attachments);
+ }
chatStore.removeTask(taskId);
proxyFetchDelete(`/api/chat/history/${taskId}`);
setMessage(question);