Skip to content

Commit 38c1d11

Browse files
authored
[verified] fix(web): coalesce full replay reattach events (#6926)
1 parent b20d3c6 commit 38c1d11

2 files changed

Lines changed: 71 additions & 1 deletion

File tree

apps/web/src/components/ProjectView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5591,7 +5591,7 @@ export function ProjectView({
55915591
genericDisconnectRetriesRef.current.delete(runId);
55925592
}
55935593
genericDisconnectBackoffUntilRef.current.delete(runId);
5594-
replayedEvents = [...replayedEvents, ev];
5594+
replayedEvents = appendCoalescedAgentEvent(replayedEvents, ev);
55955595
textBuffer.appendEvent(ev);
55965596
},
55975597
onDone: async () => {

apps/web/tests/components/ProjectView.reattach-restore.test.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,76 @@ describe('ProjectView daemon reattach restore', () => {
940940
});
941941
});
942942

943+
it('coalesces adjacent thinking events while saving a full reattach replay', async () => {
944+
const startedAt = Date.now();
945+
listConversations.mockResolvedValue([{ id: 'conv-1', title: 'Conversation' }]);
946+
listMessages.mockResolvedValue([
947+
{
948+
id: 'msg-reattach-full-replay-thinking',
949+
role: 'assistant',
950+
content: '',
951+
createdAt: startedAt,
952+
startedAt,
953+
runId: 'run-full-replay-thinking',
954+
runStatus: 'running',
955+
preTurnFileNames: [],
956+
events: [],
957+
} satisfies ChatMessage,
958+
]);
959+
fetchPreviewComments.mockResolvedValue([]);
960+
loadTabs.mockResolvedValue({ tabs: [], activeTabId: null });
961+
fetchProjectFiles.mockResolvedValue([]);
962+
fetchLiveArtifacts.mockResolvedValue([]);
963+
fetchSkill.mockResolvedValue(null);
964+
fetchDesignSystem.mockResolvedValue(null);
965+
getTemplate.mockResolvedValue(null);
966+
fetchChatRunStatus.mockResolvedValue({
967+
id: 'run-full-replay-thinking',
968+
status: 'running',
969+
createdAt: startedAt,
970+
updatedAt: startedAt,
971+
exitCode: null,
972+
signal: null,
973+
});
974+
listActiveChatRuns.mockResolvedValue([]);
975+
976+
let captured: {
977+
onAgentEvent: (ev: unknown) => void;
978+
onDone: () => void;
979+
} | null = null;
980+
reattachDaemonRun.mockImplementation(async (options: any) => {
981+
captured = {
982+
onAgentEvent: options.handlers.onAgentEvent,
983+
onDone: options.handlers.onDone,
984+
};
985+
return new Promise<void>(() => {});
986+
});
987+
988+
renderProjectView();
989+
990+
await waitFor(() => expect(reattachDaemonRun).toHaveBeenCalledTimes(1));
991+
expect(captured).not.toBeNull();
992+
for (let index = 0; index < 1_500; index += 1) {
993+
captured!.onAgentEvent({ kind: 'thinking', text: 'thought ' });
994+
}
995+
captured!.onDone();
996+
997+
await waitFor(() => {
998+
const finalMessage = saveMessage.mock.calls
999+
.map((call) => call[2] as ChatMessage)
1000+
.filter(
1001+
(message) =>
1002+
message?.id === 'msg-reattach-full-replay-thinking' &&
1003+
message.runStatus === 'succeeded',
1004+
)
1005+
.at(-1);
1006+
expect(finalMessage?.events).toHaveLength(1);
1007+
expect(finalMessage?.events).toEqual([
1008+
{ kind: 'thinking', text: 'thought '.repeat(1_500) },
1009+
]);
1010+
});
1011+
});
1012+
9431013
it('clears touched-file paths after a failed run before the next successful run finalizes', async () => {
9441014
listConversations.mockResolvedValue([{ id: 'conv-1', title: 'Conversation' }]);
9451015
listMessages.mockResolvedValue([]);

0 commit comments

Comments
 (0)