diff --git a/src/components/chat/hooks/useAgentChatSession.test.tsx b/src/components/chat/hooks/useAgentChatSession.test.tsx index 10a239d..8e3d84d 100644 --- a/src/components/chat/hooks/useAgentChatSession.test.tsx +++ b/src/components/chat/hooks/useAgentChatSession.test.tsx @@ -353,6 +353,66 @@ describe("useAgentChatSession", () => { expect(abortAgentChat).toHaveBeenCalledWith("session-loaded"); }); + it("finalizes running progress when aborting an active prompt", async () => { + listChatSessions.mockResolvedValue([]); + jest.mocked(streamAgentChat).mockImplementationOnce( + ({ onEvent, signal }) => + new Promise((_, reject) => { + onEvent({ + type: "progress", + sessionId: "session-1", + id: "request-received", + phase: "start", + status: "running", + title: "开始分析", + startedAt: 1000, + } satisfies StreamEvent); + + signal.addEventListener("abort", () => { + reject(new Error("aborted")); + }); + }), + ); + + const { result } = renderHook(() => + useAgentChatSession({ + projectId: "project-1", + onToolCall: jest.fn(), + }), + ); + + await waitFor(() => expect(result.current.isHydrating).toBe(false)); + + act(() => { + void result.current.sendPrompt("测试中断"); + }); + + await waitFor(() => expect(result.current.isStreaming).toBe(true)); + + act(() => { + result.current.abort(); + }); + + await waitFor(() => expect(result.current.isStreaming).toBe(false)); + + expect(result.current.messages.at(-1)).toEqual( + expect.objectContaining({ + role: "assistant", + content: "⚠️ **请求已中断**", + isError: true, + progress: [ + expect.objectContaining({ + id: "request-received", + status: "completed", + durationMs: expect.any(Number), + endedAt: expect.any(Number), + }), + ], + }), + ); + expect(abortAgentChat).toHaveBeenCalledWith("session-1"); + }); + it("ignores generated session titles after the title was edited manually", async () => { listChatSessions.mockResolvedValue([]); jest.mocked(streamAgentChat).mockImplementationOnce(async ({ onEvent }) => { diff --git a/src/components/chat/hooks/useAgentChatSession.ts b/src/components/chat/hooks/useAgentChatSession.ts index cb8150a..7694f6e 100644 --- a/src/components/chat/hooks/useAgentChatSession.ts +++ b/src/components/chat/hooks/useAgentChatSession.ts @@ -130,6 +130,25 @@ const completeRunningProgress = (progress: ChatProgress[] | undefined) => }; }); +const finalizeAssistantMessageAfterAbort = (message: Message): Message => { + const completedProgress = completeRunningProgress(message.progress); + const hasVisibleOutput = + message.content.trim().length > 0 || + Boolean(message.artifacts?.length) || + Boolean(completedProgress?.length); + + if (!hasVisibleOutput) { + return message; + } + + return { + ...message, + content: message.content || "⚠️ **请求已中断**", + isError: true, + progress: completedProgress, + }; +}; + const createUserMessage = (content: string, branchRootId?: string): Message => { const id = createId(); return { @@ -605,6 +624,17 @@ export const useAgentChatSession = ({ const controller = abortRef.current; controller?.abort(); setIsStreaming(false); + const assistantMessageId = getLastAssistantMessageId(); + + if (assistantMessageId) { + setMessages((prev) => + prev.map((message) => + message.id === assistantMessageId + ? finalizeAssistantMessageAfterAbort(message) + : message, + ), + ); + } const cancelPromise = abortAgentChat(sessionIdRef.current).catch((error) => { console.error("[GlobalChatbox] Failed to abort agent session:", error); @@ -615,7 +645,7 @@ export const useAgentChatSession = ({ } }); cancelPromiseRef.current = trackedCancelPromise; - }, []); + }, [getLastAssistantMessageId]); const createSession = useCallback(() => { if (isHydrating || isStreaming) return;