From 7921b6648fb1b167ccc3753d1d8c873ec9d9a005 Mon Sep 17 00:00:00 2001 From: David Kaya Date: Sat, 28 Mar 2026 23:59:06 +0100 Subject: [PATCH] fix: auto-complete previous pending messages when a new assistant message starts When the agent produces multiple intermediate responses during a turn, each gets a unique messageId but all stay pending until finalizeTurn(). This caused multiple stacked 'Thinking' bubbles in the chat transcript. Now, when a new messageId arrives in applyTurnDelta, any previously pending assistant messages are marked complete and message-complete events are emitted before the new message-delta. The renderer reducer mirrors this logic for defensive consistency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/main/AryxAppService.ts | 19 ++++++++++++ src/renderer/lib/sessionWorkspace.ts | 10 ++++++- tests/renderer/sessionWorkspace.test.ts | 40 +++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/main/AryxAppService.ts b/src/main/AryxAppService.ts index 5746d66..ac53fa5 100644 --- a/src/main/AryxAppService.ts +++ b/src/main/AryxAppService.ts @@ -1471,11 +1471,20 @@ export class AryxAppService extends EventEmitter { ? mergeStreamingText(existing.content, event.contentDelta) : (event.content ?? event.contentDelta); + // When a new assistant message begins, auto-complete any previously pending + // assistant messages so only the latest one shows the "Thinking" indicator. + const completedMessages: ChatMessageRecord[] = []; if (existing) { existing.content = content; existing.pending = true; existing.authorName = event.authorName; } else { + for (const message of session.messages) { + if (message.pending && message.role === 'assistant') { + message.pending = false; + completedMessages.push(message); + } + } session.messages.push({ id: event.messageId, role: 'assistant', @@ -1498,6 +1507,16 @@ export class AryxAppService extends EventEmitter { session.updatedAt = occurredAt; await this.workspaceRepository.save(workspace); + for (const completed of completedMessages) { + this.emitSessionEvent({ + sessionId, + kind: 'message-complete', + occurredAt, + messageId: completed.id, + authorName: completed.authorName, + content: completed.content, + }); + } this.emitSessionEvent({ sessionId, kind: 'message-delta', diff --git a/src/renderer/lib/sessionWorkspace.ts b/src/renderer/lib/sessionWorkspace.ts index 9f1a10f..57b3db1 100644 --- a/src/renderer/lib/sessionWorkspace.ts +++ b/src/renderer/lib/sessionWorkspace.ts @@ -112,10 +112,18 @@ function applyMessageDeltaEvent(session: SessionRecord, event: SessionEventRecor }; } + // Auto-complete any previously pending assistant messages so only + // the new message shows the "Thinking" indicator. + const completedMessages = session.messages.map((message) => + message.pending && message.role === 'assistant' + ? { ...message, pending: false } + : message, + ); + return { ...session, messages: [ - ...session.messages, + ...completedMessages, { id: event.messageId, role: 'assistant', diff --git a/tests/renderer/sessionWorkspace.test.ts b/tests/renderer/sessionWorkspace.test.ts index be47175..c3e4239 100644 --- a/tests/renderer/sessionWorkspace.test.ts +++ b/tests/renderer/sessionWorkspace.test.ts @@ -228,6 +228,46 @@ describe('session workspace helpers', () => { expect(updated?.sessions[0].runs).toEqual([run]); }); + test('auto-completes previous pending assistant messages when a new message starts', () => { + const first = applySessionEventWorkspace(createWorkspace(), { + sessionId: 'session-1', + kind: 'message-delta', + occurredAt: '2026-03-23T00:00:01.000Z', + messageId: 'assistant-1', + authorName: 'Primary Agent', + contentDelta: 'Exploring the codebase...', + content: 'Exploring the codebase...', + } satisfies SessionEventRecord); + + expect(first?.sessions[0].messages).toHaveLength(1); + expect(first?.sessions[0].messages[0]).toMatchObject({ + id: 'assistant-1', + pending: true, + }); + + const second = applySessionEventWorkspace(first, { + sessionId: 'session-1', + kind: 'message-delta', + occurredAt: '2026-03-23T00:00:02.000Z', + messageId: 'assistant-2', + authorName: 'Primary Agent', + contentDelta: 'Still exploring...', + content: 'Still exploring...', + } satisfies SessionEventRecord); + + expect(second?.sessions[0].messages).toHaveLength(2); + expect(second?.sessions[0].messages[0]).toMatchObject({ + id: 'assistant-1', + content: 'Exploring the codebase...', + pending: false, + }); + expect(second?.sessions[0].messages[1]).toMatchObject({ + id: 'assistant-2', + content: 'Still exploring...', + pending: true, + }); + }); + test('ignores events for unknown sessions', () => { const workspace = createWorkspace(); expect(