mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-26 21:03:58 +02:00
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>
This commit is contained in:
@@ -1471,11 +1471,20 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
? mergeStreamingText(existing.content, event.contentDelta)
|
? mergeStreamingText(existing.content, event.contentDelta)
|
||||||
: (event.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) {
|
if (existing) {
|
||||||
existing.content = content;
|
existing.content = content;
|
||||||
existing.pending = true;
|
existing.pending = true;
|
||||||
existing.authorName = event.authorName;
|
existing.authorName = event.authorName;
|
||||||
} else {
|
} else {
|
||||||
|
for (const message of session.messages) {
|
||||||
|
if (message.pending && message.role === 'assistant') {
|
||||||
|
message.pending = false;
|
||||||
|
completedMessages.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
session.messages.push({
|
session.messages.push({
|
||||||
id: event.messageId,
|
id: event.messageId,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -1498,6 +1507,16 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
session.updatedAt = occurredAt;
|
session.updatedAt = occurredAt;
|
||||||
await this.workspaceRepository.save(workspace);
|
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({
|
this.emitSessionEvent({
|
||||||
sessionId,
|
sessionId,
|
||||||
kind: 'message-delta',
|
kind: 'message-delta',
|
||||||
|
|||||||
@@ -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 {
|
return {
|
||||||
...session,
|
...session,
|
||||||
messages: [
|
messages: [
|
||||||
...session.messages,
|
...completedMessages,
|
||||||
{
|
{
|
||||||
id: event.messageId,
|
id: event.messageId,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
|
|||||||
@@ -228,6 +228,46 @@ describe('session workspace helpers', () => {
|
|||||||
expect(updated?.sessions[0].runs).toEqual([run]);
|
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', () => {
|
test('ignores events for unknown sessions', () => {
|
||||||
const workspace = createWorkspace();
|
const workspace = createWorkspace();
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
Reference in New Issue
Block a user