fix: prevent silent refresh from overwriting newly created conversations in EditorAgentConversation

This commit is contained in:
2026-07-23 14:43:57 +08:00
parent d05ac49d07
commit 9ae0e93407
3 changed files with 104 additions and 14 deletions
@@ -450,6 +450,98 @@ describe('useEditorAgentConversation', () => {
);
});
it('stops the first send while conversation creation is pending', async () => {
const client = createClient();
let resolveCreate!: (detail: EditorAgentConversationDetail) => void;
vi.mocked(client.listConversations).mockResolvedValueOnce([]);
vi.mocked(client.createConversation).mockImplementationOnce(
() =>
new Promise<EditorAgentConversationDetail>((resolve) => {
resolveCreate = resolve;
}),
);
const { result } = renderHook(() =>
useEditorAgentConversation({ projectId: 'project-1', client }),
);
await waitFor(() => {
expect(result.current.isLoadingConversations).toBe(false);
});
let sendPromise!: Promise<void>;
act(() => {
sendPromise = result.current.sendMessage('首次创建时停止');
});
await waitFor(() => {
expect(client.createConversation).toHaveBeenCalledWith('project-1', {});
});
expect(result.current.isWaiting).toBe(true);
act(() => {
result.current.stopCurrentTurn();
});
expect(result.current.isAborting).toBe(true);
await act(async () => {
resolveCreate({
conversationId: 'conversation-2',
projectId: 'project-1',
title: '新对话',
messages: [],
createdAt: '2026-07-03T00:00:00.000Z',
updatedAt: '2026-07-03T00:01:00.000Z',
});
await sendPromise;
});
expect(client.sendMessage).not.toHaveBeenCalled();
expect(result.current.activeConversationId).toBe('conversation-2');
expect(result.current.messages).toEqual([]);
expect(result.current.isWaiting).toBe(false);
expect(result.current.isAborting).toBe(false);
});
it('keeps a stopped first send silent when conversation creation fails', async () => {
const client = createClient();
let rejectCreate!: (error: Error) => void;
vi.mocked(client.listConversations).mockResolvedValueOnce([]);
vi.mocked(client.createConversation).mockImplementationOnce(
() =>
new Promise<EditorAgentConversationDetail>((_resolve, reject) => {
rejectCreate = reject;
}),
);
const { result } = renderHook(() =>
useEditorAgentConversation({ projectId: 'project-1', client }),
);
await waitFor(() => {
expect(result.current.isLoadingConversations).toBe(false);
});
let sendPromise!: Promise<void>;
act(() => {
sendPromise = result.current.sendMessage('首次创建失败前停止');
});
await waitFor(() => {
expect(client.createConversation).toHaveBeenCalledWith('project-1', {});
});
act(() => {
result.current.stopCurrentTurn();
});
await act(async () => {
rejectCreate(new Error('创建会话失败'));
await sendPromise;
});
expect(client.sendMessage).not.toHaveBeenCalled();
expect(result.current.errorMessage).toBeNull();
expect(result.current.isWaiting).toBe(false);
expect(result.current.isAborting).toBe(false);
});
it('does not send or apply a stale conversation created after switching projects', async () => {
const client = createClient();
let resolveCreate!: (detail: EditorAgentConversationDetail) => void;
@@ -10,7 +10,6 @@ import type {
EditorAgentMessageResponse,
} from '@/packages/shared/src/contracts';
import { isAbortError } from '../../../services/apiClient.ts';
import {
cancelEditorAgentToolCall,
confirmEditorAgentToolCall,
@@ -61,8 +60,6 @@ export type EditorAgentToolCallActionState = {
type ActiveEditorAgentSend = {
requestId: number;
conversationId: string;
clientMessageId: string;
controller: AbortController;
};
@@ -447,7 +444,12 @@ export function useEditorAgentConversation({
isWaitingRef.current = true;
const requestedProjectId = normalizedProjectId;
const requestId = pendingSendRequestIdRef.current + 1;
const controller = new AbortController();
pendingSendRequestIdRef.current = requestId;
activeSendRef.current = {
requestId,
controller,
};
setErrorMessage(null);
setIsWaiting(true);
setPatienceNoticeConversationId(null);
@@ -458,12 +460,12 @@ export function useEditorAgentConversation({
conversationId = await ensureConversationForSend();
if (
pendingSendRequestIdRef.current !== requestId ||
normalizedProjectIdRef.current !== requestedProjectId
normalizedProjectIdRef.current !== requestedProjectId ||
controller.signal.aborted
) {
return;
}
const clientMessageId = createEditorAgentClientMessageId();
const controller = new AbortController();
const nextOptimisticMessage = createLocalUserMessage({
id: -1,
clientMessageId,
@@ -471,12 +473,6 @@ export function useEditorAgentConversation({
attachments,
});
optimisticMessage = nextOptimisticMessage;
activeSendRef.current = {
requestId,
conversationId,
clientMessageId,
controller,
};
setMessages((currentMessages) => [
...currentMessages,
nextOptimisticMessage,
@@ -516,8 +512,10 @@ export function useEditorAgentConversation({
}
} catch (error) {
const wasStoppedByUser =
stoppedSendRequestIdRef.current === requestId && isAbortError(error);
stoppedSendRequestIdRef.current === requestId &&
controller.signal.aborted;
if (wasStoppedByUser) {
setErrorMessage(null);
if (
conversationId &&
activeConversationIdRef.current === conversationId