From c66c3b926ca72bd3f099f40d5ac58f62bf696bc6 Mon Sep 17 00:00:00 2001 From: LuoPengcheng <2653972504@qq.com> Date: Fri, 21 Nov 2025 01:44:49 +0800 Subject: [PATCH 01/14] feat: add-task hover --- .../GroupedHistoryView/ProjectDialog.tsx | 15 +++++++++------ .../GroupedHistoryView/ProjectGroup.tsx | 12 +++++++----- .../GroupedHistoryView/TaskItem.tsx | 6 ++++-- src/components/GroupedHistoryView/index.tsx | 19 +++++++++++-------- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/components/GroupedHistoryView/ProjectDialog.tsx b/src/components/GroupedHistoryView/ProjectDialog.tsx index 55d63536..abfadd8b 100644 --- a/src/components/GroupedHistoryView/ProjectDialog.tsx +++ b/src/components/GroupedHistoryView/ProjectDialog.tsx @@ -20,6 +20,7 @@ import { LoaderCircle, } from "lucide-react"; import { useProjectStore } from "@/store/projectStore"; +import { TooltipSimple } from "@/components/ui/tooltip"; interface ProjectDialogProps { open: boolean; @@ -182,12 +183,14 @@ export default function ProjectDialog({ {t("layout.total-tasks")} -
- - - {project.task_count} - -
+ +
+ + + {project.task_count} + +
+
diff --git a/src/components/GroupedHistoryView/ProjectGroup.tsx b/src/components/GroupedHistoryView/ProjectGroup.tsx index 4418d9a7..5c02016f 100644 --- a/src/components/GroupedHistoryView/ProjectGroup.tsx +++ b/src/components/GroupedHistoryView/ProjectGroup.tsx @@ -335,10 +335,12 @@ export default function ProjectGroup({ {project.total_tokens ? project.total_tokens.toLocaleString() : "0"} - - - {project.task_count} - + + + + {project.task_count} + +
{/* End: Status and menu */} @@ -401,4 +403,4 @@ export default function ProjectGroup({ /> ); -} \ No newline at end of file +} diff --git a/src/components/GroupedHistoryView/TaskItem.tsx b/src/components/GroupedHistoryView/TaskItem.tsx index e93f7a89..8d92a7ad 100644 --- a/src/components/GroupedHistoryView/TaskItem.tsx +++ b/src/components/GroupedHistoryView/TaskItem.tsx @@ -88,7 +88,9 @@ export default function TaskItem({ `} >
- + + +
); -} \ No newline at end of file +} diff --git a/src/components/GroupedHistoryView/index.tsx b/src/components/GroupedHistoryView/index.tsx index 9f926435..f5e1fd73 100644 --- a/src/components/GroupedHistoryView/index.tsx +++ b/src/components/GroupedHistoryView/index.tsx @@ -6,6 +6,7 @@ import ProjectGroup from "./ProjectGroup"; import { useTranslation } from "react-i18next"; import { Loader2, FolderOpen, Pin, Hash, LayoutGrid, List, Sparkles, Sparkle } from "lucide-react"; import { Tag } from "@/components/ui/tag"; +import { TooltipSimple } from "@/components/ui/tooltip"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useGlobalStore } from "@/store/globalStore"; import { proxyFetchDelete, proxyFetchPut } from "@/api/http"; @@ -264,13 +265,15 @@ export default function GroupedHistoryView({ - - - {t("layout.total-tasks")} - - {allProjects.reduce((total, project) => total + project.task_count, 0)} - - + + + + {t("layout.total-tasks")} + + {allProjects.reduce((total, project) => total + project.task_count, 0)} + + +
); -} \ No newline at end of file +} From f56302626e3b63c8689449d5e2ed638879167284 Mon Sep 17 00:00:00 2001 From: Sun Tao <2605127667@qq.com> Date: Fri, 21 Nov 2025 01:53:07 +0800 Subject: [PATCH 02/14] update --- src/components/IntegrationList/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/IntegrationList/index.tsx b/src/components/IntegrationList/index.tsx index bba91d94..e52b367a 100644 --- a/src/components/IntegrationList/index.tsx +++ b/src/components/IntegrationList/index.tsx @@ -239,6 +239,7 @@ export default function IntegrationList({ ); const COMING_SOON_ITEMS = [ + "Slack", "X(Twitter)", "WhatsApp", "LinkedIn", From 137d5845552aca3bca95a61e980df9587150dde3 Mon Sep 17 00:00:00 2001 From: LuoPengcheng <2653972504@qq.com> Date: Fri, 21 Nov 2025 01:55:32 +0800 Subject: [PATCH 03/14] feat: remove top hover --- src/components/GroupedHistoryView/index.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/GroupedHistoryView/index.tsx b/src/components/GroupedHistoryView/index.tsx index f5e1fd73..34c63996 100644 --- a/src/components/GroupedHistoryView/index.tsx +++ b/src/components/GroupedHistoryView/index.tsx @@ -6,7 +6,6 @@ import ProjectGroup from "./ProjectGroup"; import { useTranslation } from "react-i18next"; import { Loader2, FolderOpen, Pin, Hash, LayoutGrid, List, Sparkles, Sparkle } from "lucide-react"; import { Tag } from "@/components/ui/tag"; -import { TooltipSimple } from "@/components/ui/tooltip"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useGlobalStore } from "@/store/globalStore"; import { proxyFetchDelete, proxyFetchPut } from "@/api/http"; @@ -265,15 +264,13 @@ export default function GroupedHistoryView({ - - - - {t("layout.total-tasks")} - - {allProjects.reduce((total, project) => total + project.task_count, 0)} - - - + + + {t("layout.total-tasks")} + + {allProjects.reduce((total, project) => total + project.task_count, 0)} + +
Date: Fri, 21 Nov 2025 02:00:26 +0800 Subject: [PATCH 04/14] minor format --- src/components/GroupedHistoryView/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/GroupedHistoryView/index.tsx b/src/components/GroupedHistoryView/index.tsx index 34c63996..9f926435 100644 --- a/src/components/GroupedHistoryView/index.tsx +++ b/src/components/GroupedHistoryView/index.tsx @@ -429,4 +429,4 @@ export default function GroupedHistoryView({
); -} +} \ No newline at end of file From 8774db71e870bef035148c7e24b398676c5e51dc Mon Sep 17 00:00:00 2001 From: LuoPengcheng <2653972504@qq.com> Date: Fri, 21 Nov 2025 02:04:27 +0800 Subject: [PATCH 05/14] minor format --- .../GroupedHistoryView/ProjectDialog.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/GroupedHistoryView/ProjectDialog.tsx b/src/components/GroupedHistoryView/ProjectDialog.tsx index abfadd8b..55d63536 100644 --- a/src/components/GroupedHistoryView/ProjectDialog.tsx +++ b/src/components/GroupedHistoryView/ProjectDialog.tsx @@ -20,7 +20,6 @@ import { LoaderCircle, } from "lucide-react"; import { useProjectStore } from "@/store/projectStore"; -import { TooltipSimple } from "@/components/ui/tooltip"; interface ProjectDialogProps { open: boolean; @@ -183,14 +182,12 @@ export default function ProjectDialog({ {t("layout.total-tasks")} - -
- - - {project.task_count} - -
-
+
+ + + {project.task_count} + +
From 8cd96e55d36ae3f85a958581a924cad3f3d739ee Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Fri, 21 Nov 2025 02:21:21 +0800 Subject: [PATCH 06/14] fix --- src/components/GroupedHistoryView/ProjectGroup.tsx | 2 +- src/components/GroupedHistoryView/TaskItem.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/GroupedHistoryView/ProjectGroup.tsx b/src/components/GroupedHistoryView/ProjectGroup.tsx index 5c02016f..1410ac3a 100644 --- a/src/components/GroupedHistoryView/ProjectGroup.tsx +++ b/src/components/GroupedHistoryView/ProjectGroup.tsx @@ -335,7 +335,7 @@ export default function ProjectGroup({ {project.total_tokens ? project.total_tokens.toLocaleString() : "0"} - + {project.task_count} diff --git a/src/components/GroupedHistoryView/TaskItem.tsx b/src/components/GroupedHistoryView/TaskItem.tsx index 8d92a7ad..8ec8f7e5 100644 --- a/src/components/GroupedHistoryView/TaskItem.tsx +++ b/src/components/GroupedHistoryView/TaskItem.tsx @@ -88,7 +88,7 @@ export default function TaskItem({ `} >
- + From 24e0961986ca41b08254366733c1bba7b0bce5c7 Mon Sep 17 00:00:00 2001 From: Sun Tao <2605127667@qq.com> Date: Fri, 21 Nov 2025 02:21:27 +0800 Subject: [PATCH 07/14] Update index.tsx --- src/components/update/index.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/update/index.tsx b/src/components/update/index.tsx index 124012b5..3a649491 100644 --- a/src/components/update/index.tsx +++ b/src/components/update/index.tsx @@ -8,9 +8,21 @@ const Update = () => { const [downloadProgress, setDownloadProgress] = useState(0); const [isDownloading, setIsDownloading] = useState(false); const { t } = useTranslation(); + + // Some updater errors (e.g. GitHub 503 / missing release) are noisy and not actionable for users. + const shouldSuppressError = (message?: string) => { + if (!message) return false; + const lower = message.toLowerCase(); + return ( + lower.includes("cannot parse releases feed") || + lower.includes("unable to find latest version on github") || + lower.includes("httperror: 503") + ); + }; + const checkUpdate = async () => { const result = await window.ipcRenderer.invoke("check-update"); - if (result?.error) { + if (result?.error && !shouldSuppressError(result.error.message)) { toast.error(t("update.update-check-failed"), { description: result.error.message, }); @@ -40,11 +52,15 @@ const Update = () => { const onUpdateError = useCallback( (_event: Electron.IpcRendererEvent, err: ErrorType) => { - toast.error(t("update.update-error"), { + if (shouldSuppressError(err.message)) { + console.warn("[update] suppressed updater error:", err.message); + return; + } + toast.error(t("update.update-error"), { description: err.message, }); }, - [] + [t] ); const onDownloadProgress = useCallback( From de258f1bb63316b4d22e1d85a55ab7dc0f214c43 Mon Sep 17 00:00:00 2001 From: Sun Tao <2605127667@qq.com> Date: Fri, 21 Nov 2025 02:25:08 +0800 Subject: [PATCH 08/14] Update index.tsx --- src/components/update/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/update/index.tsx b/src/components/update/index.tsx index 3a649491..0d0537c1 100644 --- a/src/components/update/index.tsx +++ b/src/components/update/index.tsx @@ -14,9 +14,7 @@ const Update = () => { if (!message) return false; const lower = message.toLowerCase(); return ( - lower.includes("cannot parse releases feed") || - lower.includes("unable to find latest version on github") || - lower.includes("httperror: 503") + lower.includes("unable to find latest version on github") ); }; From 4fab89270f2450678012aa1801bef3e56ce778f3 Mon Sep 17 00:00:00 2001 From: Sun Tao <2605127667@qq.com> Date: Fri, 21 Nov 2025 02:27:43 +0800 Subject: [PATCH 09/14] Update index.tsx --- src/components/IntegrationList/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/IntegrationList/index.tsx b/src/components/IntegrationList/index.tsx index e52b367a..24098f96 100644 --- a/src/components/IntegrationList/index.tsx +++ b/src/components/IntegrationList/index.tsx @@ -8,7 +8,7 @@ import { TooltipSimple } from "@/components/ui/tooltip"; import { CircleAlert, Settings2 } from "lucide-react"; import { fetchGet, fetchPost } from "@/api/http"; -import React, { useState, useCallback } from "react"; +import React, { useState, useCallback, useMemo } from "react"; import ellipseIcon from "@/assets/mcp/Ellipse-25.svg"; import { MCPEnvDialog } from "@/pages/Setting/components/MCPEnvDialog"; import { OAuth } from "@/lib/oauth"; @@ -247,6 +247,12 @@ export default function IntegrationList({ "Github", ]; + const sortedItems = useMemo(() => { + const available = items.filter((item) => !COMING_SOON_ITEMS.includes(item.name)); + const comingSoon = items.filter((item) => COMING_SOON_ITEMS.includes(item.name)); + return [...available, ...comingSoon]; + }, [items]); + // Determine container and item styles based on variant const containerClassName = isSelectMode ? "space-y-3" @@ -268,7 +274,7 @@ export default function IntegrationList({ onConnect={onConnect} activeMcp={activeMcp} > - {items.map((item) => { + {sortedItems.map((item) => { const isInstalled = !!installed[item.key]; const isComingSoon = COMING_SOON_ITEMS.includes(item.name); From 52b432aeab0ae226e7124ee93956a454f3ad888e Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Fri, 21 Nov 2025 02:33:37 +0800 Subject: [PATCH 10/14] update --- electron/preload/index.ts | 1 + src/hooks/useInstallationSetup.ts | 56 ++++++++++++++++++++++++++++++- src/pages/Setting/General.tsx | 3 ++ src/store/authStore.ts | 3 +- src/types/electron.d.ts | 12 ++++--- 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 7ef37abf..0ef7933a 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -69,6 +69,7 @@ contextBridge.exposeInMainWorld('electronAPI', { checkInstallBrowser: () => ipcRenderer.invoke('check-install-browser'), getInstallationStatus: () => ipcRenderer.invoke('get-installation-status'), restartBackend: () => ipcRenderer.invoke('restart-backend'), + getBackendPort: () => ipcRenderer.invoke('get-backend-port'), onInstallDependenciesStart: (callback: () => void) => { ipcRenderer.on('install-dependencies-start', callback); }, diff --git a/src/hooks/useInstallationSetup.ts b/src/hooks/useInstallationSetup.ts index b7bca2e3..28f9945b 100644 --- a/src/hooks/useInstallationSetup.ts +++ b/src/hooks/useInstallationSetup.ts @@ -7,11 +7,12 @@ import { useAuthStore } from '@/store/authStore'; * This should be called once in your App component or Layout component */ export const useInstallationSetup = () => { - const { initState, setInitState } = useAuthStore(); + const { initState, setInitState, email } = useAuthStore(); const hasCheckedOnMount = useRef(false); const installationCompleted = useRef(false); const backendReady = useRef(false); + const previousEmail = useRef(null); const startInstallation = useInstallationStore(state => state.startInstallation); const performInstallation = useInstallationStore(state => state.performInstallation); const addLog = useInstallationStore(state => state.addLog); @@ -20,6 +21,59 @@ export const useInstallationSetup = () => { const setBackendError = useInstallationStore(state => state.setBackendError); const setWaitingBackend = useInstallationStore(state => state.setWaitingBackend); + // Monitor email changes to detect account switching + useEffect(() => { + // Detect new login: email changed from null/different to a new value + if (previousEmail.current !== email && email !== null) { + console.log('[useInstallationSetup] Account switch detected:', previousEmail.current, '->', email); + + // For account switching, tools are already installed, only backend needs restart + // So we mark installation as completed and only wait for backend + installationCompleted.current = true; + backendReady.current = false; + + // Set to waiting-backend state since backend is restarting + if (initState === 'carousel') { + console.log('[useInstallationSetup] New account login detected, waiting for backend restart'); + setWaitingBackend(); + + // Poll backend status every 2 seconds to ensure we catch when it's ready + // This is a fallback in case the backend-ready event is missed + const pollInterval = setInterval(async () => { + try { + const backendPort = await window.electronAPI.getBackendPort(); + if (backendPort && backendPort > 0) { + console.log('[useInstallationSetup] Backend poll detected ready on port:', backendPort); + + // Verify backend is actually responding + const response = await fetch(`http://localhost:${backendPort}/health`).catch(() => null); + if (response && response.ok) { + console.log('[useInstallationSetup] Backend health check passed'); + clearInterval(pollInterval); + + if (!backendReady.current) { + backendReady.current = true; + setSuccess(); + setInitState('done'); + } + } + } + } catch (error) { + console.log('[useInstallationSetup] Backend poll check failed:', error); + } + }, 2000); + + // Clear polling after 30 seconds to prevent infinite polling + setTimeout(() => { + clearInterval(pollInterval); + }, 30000); + } + } + + // Update previous email + previousEmail.current = email; + }, [email, initState, setWaitingBackend, setSuccess, setInitState]); + useEffect(() => { if (hasCheckedOnMount.current) { return; diff --git a/src/pages/Setting/General.tsx b/src/pages/Setting/General.tsx index 85fc8947..5c3236f5 100644 --- a/src/pages/Setting/General.tsx +++ b/src/pages/Setting/General.tsx @@ -6,6 +6,7 @@ import light from "@/assets/light.png"; import dark from "@/assets/dark.png"; import transparent from "@/assets/transparent.png"; import { useAuthStore } from "@/store/authStore"; +import { useInstallationStore } from "@/store/installationStore"; import { useNavigate } from "react-router-dom"; import { proxyFetchPut, proxyFetchGet } from "@/api/http"; import { createRef, RefObject } from "react"; @@ -29,6 +30,7 @@ import useChatStoreAdapter from "@/hooks/useChatStoreAdapter"; export default function SettingGeneral() { const { t } = useTranslation(); const authStore = useAuthStore(); + const resetInstallation = useInstallationStore(state => state.reset); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); const setAppearance = authStore.setAppearance; @@ -159,6 +161,7 @@ export default function SettingGeneral() { size="xs" onClick={() => { chatStore.clearTasks(); + resetInstallation(); // Reset installation state for new account authStore.logout(); navigate("/login"); }} diff --git a/src/store/authStore.ts b/src/store/authStore.ts index aa7d3ff5..7024a2a7 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -80,7 +80,8 @@ const authStore = create()( token: null, username: null, email: null, - user_id: null + user_id: null, + initState: 'carousel' // Reset to carousel state to wait for backend restart }), // set related methods diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 116a06a8..78e1870a 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -56,14 +56,16 @@ interface ElectronAPI { error?: string }>; restartBackend: () => Promise<{ success: boolean; error?: string }>; + getBackendPort: () => Promise; onInstallDependenciesStart: (callback: () => void) => void; onInstallDependenciesLog: (callback: (data: { type: string; data: string }) => void) => void; onInstallDependenciesComplete: (callback: (data: { success: boolean; code?: number; error?: string }) => void) => void; - onUpdateNotification: (callback: (data: { - type: string; - currentVersion: string; - previousVersion: string; - reason: string; + onBackendReady: (callback: (data: { success: boolean; port?: number; error?: string }) => void) => void; + onUpdateNotification: (callback: (data: { + type: string; + currentVersion: string; + previousVersion: string; + reason: string; }) => void) => void; removeAllListeners: (channel: string) => void; getEmailFolderPath: (email: string) => Promise<{ From 5ad2cdf46070f7770f9a47b487bd8f1eb8cc8206 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Fri, 21 Nov 2025 02:38:10 +0800 Subject: [PATCH 11/14] fix: pause issue --- src/components/ChatBox/index.tsx | 45 ++++++--- src/store/chatStore.ts | 157 +++++++++++++++++++++++++++++-- 2 files changed, 180 insertions(+), 22 deletions(-) diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index 78cec6aa..6365ee5b 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -162,11 +162,16 @@ export default function ChatBox(): JSX.Element { const isFinished = chatStore.tasks[_taskId as string].status === "finished"; const hasWaitComfirm = chatStore.tasks[_taskId as string]?.hasWaitComfirm; + // Check if this task was manually stopped (finished but without natural completion) + const wasTaskStopped = isFinished && !chatStore.tasks[_taskId as string].messages.some( + m => m.step === "end" // Natural completion has an "end" step message + ); + // Continue conversation if: - // 1. Has wait confirm (simple query response) - // 2. Task is finished (complex task completed) + // 1. Has wait confirm (simple query response) - but not if task was stopped + // 2. Task is naturally finished (complex task completed) - but not if task was stopped // 3. Has any messages but pending (ongoing conversation) - const shouldContinueConversation = hasWaitComfirm || isFinished || (hasMessages && chatStore.tasks[_taskId as string].status === "pending"); + const shouldContinueConversation = (hasWaitComfirm && !wasTaskStopped) || (isFinished && !wasTaskStopped) || (hasMessages && chatStore.tasks[_taskId as string].status === "pending"); if (shouldContinueConversation) { // Check if this is the very first message and task hasn't started @@ -416,28 +421,38 @@ export default function ChatBox(): JSX.Element { const handleSkip = async () => { const taskId = chatStore.activeTaskId as string; setIsPauseResumeLoading(true); - + try { - // Skip the current task + // First, try to notify backend to skip the task await fetchPost(`/chat/${projectStore.activeProjectId}/skip-task`, { project_id: projectStore.activeProjectId }); - // Update task status to finished - chatStore.setStatus(taskId, 'finished'); + // Only stop local task if backend call succeeds + chatStore.stopTask(taskId); chatStore.setIsPending(taskId, false); - - // toast.success("Task skipped successfully", { - // closeButton: true, - // }); + toast.success("Task stopped successfully", { closeButton: true, }); } catch (error) { console.error("Failed to skip task:", error); - toast.error("Failed to skip task", { - closeButton: true, - }); + + // If backend call failed, still try to stop local task as fallback + // but with different messaging to user + try { + chatStore.stopTask(taskId); + chatStore.setIsPending(taskId, false); + toast.warning("Task stopped locally, but backend notification failed. Backend task may continue running.", { + closeButton: true, + duration: 5000, + }); + } catch (localError) { + console.error("Failed to stop task locally:", localError); + toast.error("Failed to stop task completely. Please refresh the page.", { + closeButton: true, + }); + } } finally { setIsPauseResumeLoading(false); } @@ -897,4 +912,4 @@ export default function ChatBox(): JSX.Element { )}
); -} +} \ No newline at end of file diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 146f6d9c..83fd79a8 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -51,6 +51,7 @@ export interface ChatStore { tasks: { [key: string]: Task }; create: (id?: string, type?: any) => string; removeTask: (taskId: string) => void; + stopTask: (taskId: string) => void; setStatus: (taskId: string, status: 'running' | 'finished' | 'pending' | 'pause') => void; setActiveTaskId: (taskId: string) => void; replay: (taskId: string, question: string, time: number) => Promise; @@ -114,6 +115,9 @@ export type VanillaChatStore = { // Track auto-confirm timers per task to avoid reusing stale timers across rounds const autoConfirmTimers: Record> = {}; +// Track active SSE connections for proper cleanup +const activeSSEControllers: Record = {}; + const chatStore = (initial?: Partial) => createStore()( (set, get) => ({ activeTaskId: null, @@ -189,6 +193,16 @@ const chatStore = (initial?: Partial) => createStore()( console.warn('Error clearing auto-confirm timer in removeTask:', error); } + // Clean up SSE connection if it exists + try { + if (activeSSEControllers[taskId]) { + activeSSEControllers[taskId].abort(); + delete activeSSEControllers[taskId]; + } + } catch (error) { + console.warn('Error aborting SSE connection in removeTask:', error); + } + set((state) => { delete state.tasks[taskId]; return ({ @@ -198,6 +212,58 @@ const chatStore = (initial?: Partial) => createStore()( }) }) }, + stopTask(taskId: string) { + // Abort the SSE connection for this task + try { + if (activeSSEControllers[taskId]) { + console.log(`Stopping SSE connection for task ${taskId}`); + activeSSEControllers[taskId].abort(); + delete activeSSEControllers[taskId]; + } + } catch (error) { + console.warn('Error aborting SSE connection in stopTask:', error); + // Even if abort fails, still clean up the reference + try { + delete activeSSEControllers[taskId]; + } catch (cleanupError) { + console.warn('Error cleaning up SSE controller reference:', cleanupError); + } + } + + // Clean up any pending auto-confirm timers + try { + if (autoConfirmTimers[taskId]) { + clearTimeout(autoConfirmTimers[taskId]); + delete autoConfirmTimers[taskId]; + } + } catch (error) { + console.warn('Error clearing auto-confirm timer in stopTask:', error); + } + + // Update task status to finished - ensure this happens even if cleanup fails + try { + set((state) => { + // Check if task exists before updating + if (!state.tasks[taskId]) { + console.warn(`Task ${taskId} not found when trying to stop it`); + return state; + } + + return { + ...state, + tasks: { + ...state.tasks, + [taskId]: { + ...state.tasks[taskId], + status: 'finished' + }, + }, + }; + }); + } catch (error) { + console.error('Error updating task status to finished in stopTask:', error); + } + }, startTask: async (taskId: string, type?: string, shareToken?: string, delayTime?: number, messageContent?: string, messageAttaches?: File[]) => { // ✅ Wait for backend to be ready before starting task (except for replay/share) if (!type || type === 'normal') { @@ -209,7 +275,7 @@ const chatStore = (initial?: Partial) => createStore()( const { addMessages } = get(); addMessages(taskId, { id: generateUniqueId(), - role: 'system', + role: 'agent', content: '❌ Backend service is not ready. Please wait a moment and try again, or restart the application if the problem persists.', }); return; @@ -421,26 +487,42 @@ const chatStore = (initial?: Partial) => createStore()( // during active message processing let lockedChatStore = targetChatStore; let lockedTaskId = newTaskId; - + + // Create AbortController for this task's SSE connection + // First check if there's already an active SSE connection for this task + if (activeSSEControllers[newTaskId]) { + console.warn(`Task ${newTaskId} already has an active SSE connection, aborting old one`); + try { + activeSSEControllers[newTaskId].abort(); + } catch (error) { + console.warn('Error aborting existing SSE connection:', error); + } + delete activeSSEControllers[newTaskId]; + } + + const abortController = new AbortController(); + activeSSEControllers[newTaskId] = abortController; + // Getter functions that use the locked references instead of dynamic ones const getCurrentChatStore = () => { return lockedChatStore.getState(); }; - + // Get the locked task ID - this won't change during the SSE session const getCurrentTaskId = () => { return lockedTaskId; }; - + // Function to update locked references (only for special cases like replay) const updateLockedReferences = (newChatStore: VanillaChatStore, newTaskId: string) => { lockedChatStore = newChatStore; lockedTaskId = newTaskId; }; - + fetchEventSource(api, { method: !type ? "POST" : "GET", openWhenHidden: true, + signal: abortController.signal, // Add abort signal for proper cleanup headers: { "Content-Type": "application/json", "Authorization": type == 'replay' ? `Bearer ${token}` : undefined as unknown as string }, body: !type ? JSON.stringify({ project_id: project_id, @@ -485,6 +567,32 @@ const chatStore = (initial?: Partial) => createStore()( return; } + // Check if this task has been stopped before processing any message + // But allow messages that switch to new tasks (like confirmed events) + const lockedTaskId = getCurrentTaskId(); + const currentTask = getCurrentChatStore().tasks[lockedTaskId]; + + // Only ignore messages if: + // 1. The task doesn't exist, OR + // 2. The task is finished AND it's not a task-switching event + const isTaskSwitchingEvent = agentMessages.step === "confirmed" || + agentMessages.step === "new_task_state" || + agentMessages.step === "end"; + + // More robust check - only ignore if task doesn't exist OR + // task is finished and it's not a legitimate flow-control event + if (!currentTask) { + console.log(`Task ${lockedTaskId} not found, ignoring SSE message for step: ${agentMessages.step}`); + return; + } + + if (currentTask.status === 'finished' && !isTaskSwitchingEvent) { + // Only ignore non-essential messages for finished tasks + // Allow flow control messages through even for finished tasks + console.log(`Ignoring SSE message for finished task ${lockedTaskId}, step: ${agentMessages.step}`); + return; + } + console.log("agentMessages", agentMessages); const agentNameMap = { developer_agent: "Developer Agent", @@ -1629,12 +1737,31 @@ const chatStore = (initial?: Partial) => createStore()( // For other errors, log and throw to stop retrying console.error('[fetchEventSource] Fatal error, stopping connection:', err); + + // Clean up AbortController on error with robust error handling + try { + if (activeSSEControllers[newTaskId]) { + delete activeSSEControllers[newTaskId]; + console.log(`Cleaned up SSE controller for task ${newTaskId} after error`); + } + } catch (cleanupError) { + console.warn('Error cleaning up AbortController on SSE error:', cleanupError); + } throw err; }, // Server closes connection onclose() { - console.log("server closed"); + console.log("SSE connection closed"); + // Clean up AbortController when connection closes with robust error handling + try { + if (activeSSEControllers[newTaskId]) { + delete activeSSEControllers[newTaskId]; + console.log(`Cleaned up SSE controller for task ${newTaskId} after connection close`); + } + } catch (cleanupError) { + console.warn('Error cleaning up AbortController on SSE close:', cleanupError); + } }, }); @@ -2253,6 +2380,22 @@ const chatStore = (initial?: Partial) => createStore()( console.error('Error during timer cleanup in clearTasks:', error); } + // Clean up all active SSE connections + try { + Object.keys(activeSSEControllers).forEach(taskId => { + try { + if (activeSSEControllers[taskId]) { + activeSSEControllers[taskId].abort(); + delete activeSSEControllers[taskId]; + } + } catch (error) { + console.warn(`Error aborting SSE connection for task ${taskId}:`, error); + } + }); + } catch (error) { + console.error('Error during SSE cleanup in clearTasks:', error); + } + window.ipcRenderer.invoke('restart-backend') .then((res) => { console.log('restart-backend', res) @@ -2315,4 +2458,4 @@ const filterMessage = (message: AgentMessage) => { export const useChatStore = chatStore; -export const getToolStore = () => chatStore().getState(); +export const getToolStore = () => chatStore().getState(); \ No newline at end of file From 692cc64d15b85f2a8d315bf7fa9d1104d849bea1 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Fri, 21 Nov 2025 02:52:06 +0800 Subject: [PATCH 12/14] Revert "fix: login (#739)" This reverts commit e0f867d26e4f306ddf159453c939d13a5241284e, reversing changes made to e3c61a958b159762a8597838c56a6628b2694369. --- electron/preload/index.ts | 1 - src/hooks/useInstallationSetup.ts | 56 +------------------------------ src/pages/Setting/General.tsx | 3 -- src/store/authStore.ts | 3 +- src/types/electron.d.ts | 12 +++---- 5 files changed, 7 insertions(+), 68 deletions(-) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 0ef7933a..7ef37abf 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -69,7 +69,6 @@ contextBridge.exposeInMainWorld('electronAPI', { checkInstallBrowser: () => ipcRenderer.invoke('check-install-browser'), getInstallationStatus: () => ipcRenderer.invoke('get-installation-status'), restartBackend: () => ipcRenderer.invoke('restart-backend'), - getBackendPort: () => ipcRenderer.invoke('get-backend-port'), onInstallDependenciesStart: (callback: () => void) => { ipcRenderer.on('install-dependencies-start', callback); }, diff --git a/src/hooks/useInstallationSetup.ts b/src/hooks/useInstallationSetup.ts index 28f9945b..b7bca2e3 100644 --- a/src/hooks/useInstallationSetup.ts +++ b/src/hooks/useInstallationSetup.ts @@ -7,12 +7,11 @@ import { useAuthStore } from '@/store/authStore'; * This should be called once in your App component or Layout component */ export const useInstallationSetup = () => { - const { initState, setInitState, email } = useAuthStore(); + const { initState, setInitState } = useAuthStore(); const hasCheckedOnMount = useRef(false); const installationCompleted = useRef(false); const backendReady = useRef(false); - const previousEmail = useRef(null); const startInstallation = useInstallationStore(state => state.startInstallation); const performInstallation = useInstallationStore(state => state.performInstallation); const addLog = useInstallationStore(state => state.addLog); @@ -21,59 +20,6 @@ export const useInstallationSetup = () => { const setBackendError = useInstallationStore(state => state.setBackendError); const setWaitingBackend = useInstallationStore(state => state.setWaitingBackend); - // Monitor email changes to detect account switching - useEffect(() => { - // Detect new login: email changed from null/different to a new value - if (previousEmail.current !== email && email !== null) { - console.log('[useInstallationSetup] Account switch detected:', previousEmail.current, '->', email); - - // For account switching, tools are already installed, only backend needs restart - // So we mark installation as completed and only wait for backend - installationCompleted.current = true; - backendReady.current = false; - - // Set to waiting-backend state since backend is restarting - if (initState === 'carousel') { - console.log('[useInstallationSetup] New account login detected, waiting for backend restart'); - setWaitingBackend(); - - // Poll backend status every 2 seconds to ensure we catch when it's ready - // This is a fallback in case the backend-ready event is missed - const pollInterval = setInterval(async () => { - try { - const backendPort = await window.electronAPI.getBackendPort(); - if (backendPort && backendPort > 0) { - console.log('[useInstallationSetup] Backend poll detected ready on port:', backendPort); - - // Verify backend is actually responding - const response = await fetch(`http://localhost:${backendPort}/health`).catch(() => null); - if (response && response.ok) { - console.log('[useInstallationSetup] Backend health check passed'); - clearInterval(pollInterval); - - if (!backendReady.current) { - backendReady.current = true; - setSuccess(); - setInitState('done'); - } - } - } - } catch (error) { - console.log('[useInstallationSetup] Backend poll check failed:', error); - } - }, 2000); - - // Clear polling after 30 seconds to prevent infinite polling - setTimeout(() => { - clearInterval(pollInterval); - }, 30000); - } - } - - // Update previous email - previousEmail.current = email; - }, [email, initState, setWaitingBackend, setSuccess, setInitState]); - useEffect(() => { if (hasCheckedOnMount.current) { return; diff --git a/src/pages/Setting/General.tsx b/src/pages/Setting/General.tsx index 5c3236f5..85fc8947 100644 --- a/src/pages/Setting/General.tsx +++ b/src/pages/Setting/General.tsx @@ -6,7 +6,6 @@ import light from "@/assets/light.png"; import dark from "@/assets/dark.png"; import transparent from "@/assets/transparent.png"; import { useAuthStore } from "@/store/authStore"; -import { useInstallationStore } from "@/store/installationStore"; import { useNavigate } from "react-router-dom"; import { proxyFetchPut, proxyFetchGet } from "@/api/http"; import { createRef, RefObject } from "react"; @@ -30,7 +29,6 @@ import useChatStoreAdapter from "@/hooks/useChatStoreAdapter"; export default function SettingGeneral() { const { t } = useTranslation(); const authStore = useAuthStore(); - const resetInstallation = useInstallationStore(state => state.reset); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); const setAppearance = authStore.setAppearance; @@ -161,7 +159,6 @@ export default function SettingGeneral() { size="xs" onClick={() => { chatStore.clearTasks(); - resetInstallation(); // Reset installation state for new account authStore.logout(); navigate("/login"); }} diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 7024a2a7..aa7d3ff5 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -80,8 +80,7 @@ const authStore = create()( token: null, username: null, email: null, - user_id: null, - initState: 'carousel' // Reset to carousel state to wait for backend restart + user_id: null }), // set related methods diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 78e1870a..116a06a8 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -56,16 +56,14 @@ interface ElectronAPI { error?: string }>; restartBackend: () => Promise<{ success: boolean; error?: string }>; - getBackendPort: () => Promise; onInstallDependenciesStart: (callback: () => void) => void; onInstallDependenciesLog: (callback: (data: { type: string; data: string }) => void) => void; onInstallDependenciesComplete: (callback: (data: { success: boolean; code?: number; error?: string }) => void) => void; - onBackendReady: (callback: (data: { success: boolean; port?: number; error?: string }) => void) => void; - onUpdateNotification: (callback: (data: { - type: string; - currentVersion: string; - previousVersion: string; - reason: string; + onUpdateNotification: (callback: (data: { + type: string; + currentVersion: string; + previousVersion: string; + reason: string; }) => void) => void; removeAllListeners: (channel: string) => void; getEmailFolderPath: (email: string) => Promise<{ From f3f73a4e9fa43629bc83957f547af0e0ecd589e7 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Fri, 21 Nov 2025 02:58:49 +0800 Subject: [PATCH 13/14] chore: update camel version --- backend/pyproject.toml | 2 +- backend/uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 3a713297..4b4dc445 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -5,7 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = "==3.10.16" dependencies = [ - "camel-ai[eigent]==0.2.80a0", + "camel-ai[eigent]==0.2.80a3", "fastapi>=0.115.12", "fastapi-babel>=1.0.0", "uvicorn[standard]>=0.34.2", diff --git a/backend/uv.lock b/backend/uv.lock index 150cc816..972efe55 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -249,7 +249,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiofiles", specifier = ">=24.1.0" }, - { name = "camel-ai", extras = ["eigent"], specifier = "==0.2.80a0" }, + { name = "camel-ai", extras = ["eigent"], specifier = "==0.2.80a3" }, { name = "debugpy", specifier = ">=1.8.17" }, { name = "fastapi", specifier = ">=0.115.12" }, { name = "fastapi-babel", specifier = ">=1.0.0" }, @@ -333,7 +333,7 @@ wheels = [ [[package]] name = "camel-ai" -version = "0.2.80a0" +version = "0.2.80a3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astor" }, @@ -349,9 +349,9 @@ dependencies = [ { name = "tiktoken" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/b8/0af67d136edb1d2e598986c9cfaa0e616e2fd337765a40dac60809860698/camel_ai-0.2.80a0.tar.gz", hash = "sha256:a7425ecfebc7d5713e058ae5c04d0b108aa934d14fc2bc48786cd9afa84c9a53", size = 1004526, upload-time = "2025-11-19T08:25:44.579Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/72/691a6126e062b5c5a24b7c5a116f690be1b50127a359c7d53d13435d27ef/camel_ai-0.2.80a3.tar.gz", hash = "sha256:edda7cb0466a63c4d8f92ae4ea2d11ee6946b69146336176346db3227de747d9", size = 1013184, upload-time = "2025-11-20T18:52:25.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/2e/4e0d0d55c13abf7e6ce9699d8639516e879b966bc6b843be1c1704567f9b/camel_ai-0.2.80a0-py3-none-any.whl", hash = "sha256:eae23435fefe8813c8de3f250a449e435593ead651c93f530fa4ce2377109095", size = 1465918, upload-time = "2025-11-19T08:25:42.331Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/a1203a2fa8e432deb4e6a7740a4dff532b79e0129bbf2e60626fca8f4271/camel_ai-0.2.80a3-py3-none-any.whl", hash = "sha256:2f398e648edae57bf23372a9cc812c82bf64f007178cdaf7ba113f2fde6761a6", size = 1473469, upload-time = "2025-11-20T18:52:22.853Z" }, ] [package.optional-dependencies] From 74092cd1176a7f96424aa5cec453a8e6847d05af Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Fri, 21 Nov 2025 03:16:59 +0800 Subject: [PATCH 14/14] fix: code issue --- electron/main/index.ts | 2 +- electron/main/update.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 02cedbb8..f1ca0037 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1116,7 +1116,7 @@ const startBackendAfterInstall = async () => { // ==================== installation lock ==================== let isInstallationInProgress = false; -let installationLock = Promise.resolve(); +let installationLock: Promise = Promise.resolve({ message: "No installation needed", success: true }); // ==================== window create ==================== async function createWindow() { diff --git a/electron/main/update.ts b/electron/main/update.ts index a5fc726e..c1f75685 100644 --- a/electron/main/update.ts +++ b/electron/main/update.ts @@ -50,7 +50,7 @@ export function update(win: Electron.BrowserWindow) { if (!app.isPackaged) { console.log('[DEV] setFeedURL:', feed) // In development, check for updates but don't fail if it errors - autoUpdater.checkForUpdates().catch(err => { + autoUpdater.checkForUpdates().catch((err: Error) => { console.log('[DEV] Update check failed (expected in dev environment):', err.message) }) }