fix(frontend): reset active chat after deletion (#3519)

This commit is contained in:
zgenu
2026-06-14 22:06:19 +08:00
committed by GitHub
parent ec520e6427
commit 34e126ee4b
7 changed files with 481 additions and 51 deletions
@@ -5,6 +5,25 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { uuid } from "@/core/utils/uuid";
export const THREAD_CHAT_RESET_EVENT = "deer-flow:thread-chat-reset";
type ThreadChatResetDetail = {
deletedThreadId: string;
nextPath: string;
force?: boolean;
};
export function resetThreadChatAfterDelete(detail: ThreadChatResetDetail) {
if (typeof window === "undefined") {
return;
}
window.dispatchEvent(
new CustomEvent<ThreadChatResetDetail>(THREAD_CHAT_RESET_EVENT, {
detail,
}),
);
}
export function useThreadChat() {
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
const pathname = usePathname();
@@ -30,6 +49,13 @@ export function useThreadChat() {
() => threadIdFromPath === "new",
);
const resetToNewThread = useCallback(() => {
const nextThreadId = uuid();
newThreadIdRef.current = nextThreadId;
setIsNewThreadState(true);
setThreadIdState(nextThreadId);
}, []);
useEffect(() => {
if (isNewPath) {
const nextThreadId = newThreadIdRef.current ?? uuid();
@@ -51,6 +77,35 @@ export function useThreadChat() {
setThreadIdState(threadIdFromPath);
}, [isNewPath, threadIdFromPath]);
useEffect(() => {
const handleReset = (event: Event) => {
const detail = (event as CustomEvent<ThreadChatResetDetail>).detail;
if (!detail?.nextPath) {
return;
}
const currentPathname = window.location.pathname;
const isDeletingCurrentThread =
detail.force === true ||
detail.deletedThreadId === threadId ||
detail.deletedThreadId === threadIdFromPath ||
currentPathname.endsWith(`/${detail.deletedThreadId}`);
if (!isDeletingCurrentThread) {
return;
}
// URL replacement is owned by the caller's Next router action; this hook
// only resets local chat state so the router state and browser URL stay
// in sync.
resetToNewThread();
};
window.addEventListener(THREAD_CHAT_RESET_EVENT, handleReset);
return () =>
window.removeEventListener(THREAD_CHAT_RESET_EVENT, handleReset);
}, [resetToNewThread, threadId, threadIdFromPath]);
const setThreadId = useCallback((nextThreadId: string) => {
newThreadIdRef.current = null;
setThreadIdState(nextThreadId);
@@ -42,6 +42,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { resetThreadChatAfterDelete } from "@/components/workspace/chats/use-thread-chat";
import { getAPIClient } from "@/core/api";
import { writeTextToClipboard } from "@/core/clipboard";
import { useI18n } from "@/core/i18n/hooks";
@@ -112,24 +113,41 @@ export function RecentChatList() {
const [renameValue, setRenameValue] = useState("");
const handleDelete = useCallback(
(threadId: string) => {
deleteThread({ threadId });
if (threadId === threadIdFromPath) {
const threadIndex = threads.findIndex((t) => t.thread_id === threadId);
let nextThreadPath = pathOfThread("new", {
agent_name: agentNameFromPath,
});
if (threadIndex > -1) {
if (threads[threadIndex + 1]) {
nextThreadPath = pathOfThread(threads[threadIndex + 1]!);
} else if (threads[threadIndex - 1]) {
nextThreadPath = pathOfThread(threads[threadIndex - 1]!);
}
}
void router.push(nextThreadPath);
}
(thread: AgentThread) => {
const currentPathname =
typeof window === "undefined" ? pathname : window.location.pathname;
const threadPath = pathOfThread(thread);
const nextThreadPath = pathOfThread("new", {
agent_name: agentNameFromPath,
});
const isNewThreadPath = currentPathname === nextThreadPath;
const isCurrentThread =
thread.thread_id === threadIdFromPath ||
threadPath === currentPathname ||
(isNewThreadPath && threads[0]?.thread_id === thread.thread_id);
deleteThread({
threadId: thread.thread_id,
onRemoteDeleted: isCurrentThread
? () => {
resetThreadChatAfterDelete({
deletedThreadId: thread.thread_id,
nextPath: nextThreadPath,
force: true,
});
void router.replace(nextThreadPath);
}
: undefined,
});
},
[agentNameFromPath, deleteThread, router, threadIdFromPath, threads],
[
agentNameFromPath,
deleteThread,
pathname,
router,
threadIdFromPath,
threads,
],
);
const handleRenameClick = useCallback(
@@ -302,7 +320,7 @@ export function RecentChatList() {
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => handleDelete(thread.thread_id)}
onSelect={() => handleDelete(thread)}
>
<Trash2 className="text-muted-foreground" />
<span>{t.common.delete}</span>