diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index c812ffb1..d1011764 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -66,9 +66,6 @@ export default function ChatPage() { !isNewThread || searchParams.get("xclaw_used")?.trim().toLowerCase() === "true"; - // Submission strategy: always reuse the thread id from query; never create new. - const createNewSession = false; - /* // Original strategy: // - isnew=false + thread_id: reuse existing thread (explicit request from URL) // - xclaw_used=true: follow `isnew` (isnew=false => reuse existing thread) @@ -87,9 +84,13 @@ export default function ChatPage() { if (searchParams.get("xclaw_used")?.trim().toLowerCase() !== "true") { return true; } - return searchParams.get("isnew")?.trim().toLowerCase() !== "false"; + return searchParams.get("isnew")?.trim().toLowerCase() === "true"; }, [isNewThread, searchParams]); - */ + console.log(createNewSession, "createNewSession"); + const shouldStayOnNewRoute = useMemo( + () => searchParams.get("isnew")?.trim().toLowerCase() === "true", + [searchParams], + ); const streamThreadId = useMemo(() => { return isNewThread && createNewSession ? undefined : threadId; }, [createNewSession, isNewThread, threadId]); @@ -105,11 +106,14 @@ export default function ChatPage() { const [thread, sendMessage, isUploading] = useThreadStream({ threadId: streamThreadId, context: settings.context, + createNewSession, isMock, onStart: (currentThreadId) => { setIsNewThread(false); - // Keep /new in history so router.back() can return to it. - router.replace(`/workspace/chats/${currentThreadId}`); + if (!shouldStayOnNewRoute) { + // Keep /new in history so router.back() can return to it. + router.replace(`/workspace/chats/${currentThreadId}`); + } // history.pushState(null, "", pathOfThread(currentThreadId)); }, onFinish: (state) => { diff --git a/frontend/src/components/workspace/chats/use-thread-chat.ts b/frontend/src/components/workspace/chats/use-thread-chat.ts index c4f68868..8ac0761c 100644 --- a/frontend/src/components/workspace/chats/use-thread-chat.ts +++ b/frontend/src/components/workspace/chats/use-thread-chat.ts @@ -60,9 +60,8 @@ export function useThreadChat() { } return value === "new" ? queryThreadIdFromParams : value; }; - const xClawUsedFromQuery = searchParams.get("xclaw_used"); - const isNewFromQuery = - searchParams.get("isnew")?.trim().toLowerCase() === "false"; + const isNewRequested = + searchParams.get("isnew")?.trim().toLowerCase() === "true"; const effectiveThreadIdFromPath = normalizeThreadId(threadIdFromPath) ?? readStoredThreadId(); // console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath); @@ -71,28 +70,18 @@ export function useThreadChat() { return effectiveThreadIdFromPath ?? undefined; }); - // /new 或缺少 query 的 thread_id 时,视为新会话状态。 但是这个并不是新会话的意思,而是说当前处在对话状态。 - const [isNewThread, setIsNewThread] = useState( - () => threadIdFromPath === "new" || !queryThreadIdFromParams, - ); + // New session is only controlled by `isnew=true`. + const [isNewThread, setIsNewThread] = useState(() => isNewRequested); useEffect(() => { // 记住最近一次有效的 thread_id,供下次加载兜底使用。 if (threadId && threadId !== "new" && typeof window !== "undefined") { window.sessionStorage.setItem("workspace.thread_id", threadId); } - if (pathname.endsWith("/new")) { - setIsNewThread(true); - const nextQueryThreadId = readQueryThreadId(); - const nextIsNewFromQuery = - searchParams.get("isnew")?.trim().toLowerCase() === "false"; - const nextXClawUsed = searchParams.get("xclaw_used"); - setThreadId(nextQueryThreadId ?? undefined); - return; - } - setIsNewThread(false); - // console.log("threadIdFromPath", threadIdFromPath, "normalized", normalizeThreadId(threadIdFromPath)); - + setIsNewThread( + searchParams.get("isnew")?.trim().toLowerCase() === "true", + ); + // Prefer path thread id, fall back to query thread_id when path is /new. setThreadId(normalizeThreadId(threadIdFromPath)); }, [pathname, router, searchParams, threadIdFromPath]); const isMock = searchParams.get("mock") === "true"; diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index e96696a8..a55b1c5b 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -31,6 +31,7 @@ export type ToolEndEvent = { export type ThreadStreamOptions = { threadId?: string | null | undefined; context: LocalSettings["context"]; + createNewSession?: boolean; isMock?: boolean; onStart?: (threadId: string) => void; onFinish?: (state: AgentThreadState) => void; @@ -128,6 +129,7 @@ export function useThreadStreamLegacy({ export function useThreadStream({ threadId, context, + createNewSession = false, isMock, onStart, onFinish, @@ -179,9 +181,10 @@ export function useThreadStream({ const queryClient = useQueryClient(); const updateSubtask = useUpdateSubtask(); + const apiClient = getAPIClient(isMock); const thread = useStream({ - client: getAPIClient(isMock), + client: apiClient, assistantId: "lead_agent", threadId: onStreamThreadId, reconnectOnMount: true, @@ -329,6 +332,11 @@ export function useThreadStream({ let uploadedFileInfo: UploadedFileInfo[] = []; try { + // isnew 为 true 时,删除旧线程并创建同名新线程 + if (createNewSession && resolvedThreadId) { + await apiClient.threads.delete(resolvedThreadId).catch(() => undefined); + } + // Upload files first if any if (message.files && message.files.length > 0) { setIsUploading(true); @@ -474,7 +482,15 @@ export function useThreadStream({ sendInFlightRef.current = false; } }, - [thread, _handleOnStart, t.uploads.uploadingFiles, context, queryClient], + [ + thread, + _handleOnStart, + t.uploads.uploadingFiles, + context, + queryClient, + apiClient, + createNewSession, + ], ); // Merge thread with optimistic messages for display @@ -524,11 +540,11 @@ export function useSubmitThread({ } if (createNewSession && threadId) { - try { - await apiClient.threads.delete(threadId); - } catch { - // Ignore delete errors - } + await apiClient.threads.delete(threadId).catch(() => undefined); + await apiClient.threads.create({ + threadId, + ifExists: "do_nothing", + }); } if (message.files && message.files.length > 0) { diff --git a/frontend/src/hooks/use-iframe-skill.ts b/frontend/src/hooks/use-iframe-skill.ts index f33bdabb..bb491ba7 100644 --- a/frontend/src/hooks/use-iframe-skill.ts +++ b/frontend/src/hooks/use-iframe-skill.ts @@ -29,6 +29,7 @@ export function useIframeSkill(): UseIframeSkillReturn { const titleFromQuery = searchParams.get("title"); const threadIdFromQuery = searchParams.get("thread_id"); const xClawUsedFromQuery = searchParams.get("xclaw_used"); + const isNewFromQuery = searchParams.get("isnew")?.trim().toLowerCase() === "true"; const lastThreadIdRef = useRef(null); const [selectedSkill, setSelectedSkill] = useState(null); @@ -45,10 +46,11 @@ export function useIframeSkill(): UseIframeSkillReturn { if (!threadIdFromQuery) return; if (xClawUsedFromQuery !== "true") return; + if (isNewFromQuery) return; if (lastThreadIdRef.current === threadIdFromQuery) return; lastThreadIdRef.current = threadIdFromQuery; router.replace(`/workspace/chats/${threadIdFromQuery}`); - }, [router, threadIdFromQuery, xClawUsedFromQuery]); + }, [isNewFromQuery, router, threadIdFromQuery, xClawUsedFromQuery]); // 2. 监听宿主页 postMessage useEffect(() => {