Skip to content

Commit 0085d2c

Browse files
committed
fix(chat): find a running turn's saved answer past its steering rows
A refetch mid-turn can return the saved answer after a steering row. Only the row right after the owner was checked, so the live answer was added beside it and the turn showed two answers until it ended. It now walks past steering rows with findAssistantAfterOwner, as the reconcile path does.
1 parent 0cdb5c9 commit 0085d2c

3 files changed

Lines changed: 47 additions & 4 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/hooks/message-reconcile.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,10 @@ export function findStreamOwnerIndex(messages: PersistedMessage[], streamId: str
248248
}
249249

250250
/** The turn's response after its owner, past any steering the user sent into that turn. */
251-
export function findAssistantAfterOwner(messages: PersistedMessage[], ownerIndex: number): number {
251+
export function findAssistantAfterOwner(
252+
messages: ReadonlyArray<Pick<PersistedMessage, 'role' | 'steering'>>,
253+
ownerIndex: number
254+
): number {
252255
for (let index = ownerIndex + 1; index < messages.length; index++) {
253256
const message = messages[index]
254257
if (message.role === 'user' && !message.steering) return -1

‎apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.dom.test.tsx‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4560,6 +4560,44 @@ describe('useChat remount send recovery', () => {
45604560
}
45614561
}, 30_000)
45624562

4563+
/** The server saved the turn's answer, after the steering row, before this surface saw it end. */
4564+
it('shows one answer when a refetch saves it after a steering row mid-turn', async () => {
4565+
const { view, turn } = await startTurn()
4566+
const at = '2026-11-05T00:00:00.000Z'
4567+
const saved: MothershipChatHistory = {
4568+
...history,
4569+
messages: [
4570+
{ id: turn.firstStreamId(), role: 'user', content: 'Count to fifty', timestamp: at },
4571+
{
4572+
id: 'steering-row',
4573+
role: 'user',
4574+
content: 'Stop at two',
4575+
timestamp: at,
4576+
steering: { streamId: turn.firstStreamId() },
4577+
},
4578+
{
4579+
id: 'saved-answer',
4580+
streamId: turn.firstStreamId(),
4581+
role: 'assistant',
4582+
content: '1 2.',
4583+
timestamp: at,
4584+
},
4585+
],
4586+
}
4587+
await act(async () => {
4588+
queryClient.setQueryData(mothershipChatKeys.detail(chatId), saved)
4589+
await settle()
4590+
})
4591+
4592+
expect(view.getResult().isSending).toBe(true)
4593+
expect(
4594+
view
4595+
.getResult()
4596+
.messages.filter((message) => message.role === 'assistant')
4597+
.map((message) => message.id)
4598+
).toEqual(['saved-answer'])
4599+
})
4600+
45634601
const SUMMARY_SCOPE = { startDate: '2026-11-01T00:00:00.000Z' }
45644602

45654603
it('holds a message with a search scope for its own turn, keeping the scope', async () => {

‎apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ import type {
196196
import {
197197
buildAssistantSnapshotMessage,
198198
buildChatHistoryHydrationKey,
199+
findAssistantAfterOwner,
199200
getReplayCompletedWorkflowToolCallIds,
200201
hasTerminalPersistedAssistantForStream,
201202
markMessageStopped,
@@ -1438,15 +1439,16 @@ export function useChat(
14381439
ownerIndex = assistantIndex < 0 ? source.length : assistantIndex
14391440
source.splice(ownerIndex, 0, pendingTurn.optimisticUserMessage)
14401441
}
1441-
const assistant = source[ownerIndex + 1]
1442+
const assistantIndex = findAssistantAfterOwner(source, ownerIndex)
1443+
const assistant = assistantIndex < 0 ? undefined : source[assistantIndex]
14421444
const liveAssistant = {
14431445
...pendingTurn.optimisticAssistantMessage,
14441446
content: liveContent,
14451447
contentBlocks: liveBlocks,
14461448
}
14471449
if (assistant?.id === pendingTurn.assistantMessageId) {
1448-
source[ownerIndex + 1] = { ...assistant, ...liveAssistant }
1449-
} else if (assistant?.role !== 'assistant') {
1450+
source[assistantIndex] = { ...assistant, ...liveAssistant }
1451+
} else if (!assistant) {
14501452
source.splice(ownerIndex + 1, 0, liveAssistant)
14511453
}
14521454
}

0 commit comments

Comments
 (0)