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