Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { expect, it } from 'vitest'

import { type ChatMessage, textPart } from '@/lib/chat-messages'

import { selectBranchMessages } from './utils'

const message = (id: string, role: ChatMessage['role'], text: string, rowId: number): ChatMessage => ({
id,
parts: [textPart(text)],
role,
rowId
})

it('maps a clicked reply through a folded authoritative source row', () => {
const local = [
message('tail-user', 'user', 'latest question', 13),
message('tail-assistant', 'assistant', 'latest answer', 14)
]

const authoritative = [
message('old-user', 'user', 'first question', 11),
message('old-assistant', 'assistant', 'first answer', 12),
message('tail-user', 'user', 'latest question', 13),
{
id: 'folded-assistant',
role: 'assistant',
rowId: 10,
parts: [
{ ...textPart('tool preface'), sourceRowId: 10 },
{ ...textPart('latest answer'), sourceRowId: 14 }
]
}
] as ChatMessage[]

expect(selectBranchMessages(local, authoritative, 'tail-assistant').map(branch => branch.content)).toEqual([
'first question',
'first answer',
'latest question',
'tool prefacelatest answer'
])
})
22 changes: 11 additions & 11 deletions apps/desktop/src/app/session/hooks/use-session-actions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1780,13 +1780,9 @@ export const toBranchMessages = (messages: ChatMessage[]): BranchMessage[] =>

/**
* Choose the transcript used to seed an open-chat branch.
*
* The local renderer can hold a compacted model projection, while the REST
* transcript contains the complete display projection. Use the latter for a
* whole-chat branch. When branching from a clicked bubble, map that bubble by
* durable row id first and by same-role/text ordinal as a legacy fallback; if
* it cannot be mapped, keep the local prefix rather than silently choosing a
* different point in the conversation.
* The local renderer may be compacted; the REST transcript is complete. For a
* clicked bubble, map every durable source row, then same-role/text ordinal.
* If neither works, keep the local prefix rather than choose the wrong point.
*/
export function selectBranchMessages(
localMessages: ChatMessage[],
Expand All @@ -1809,10 +1805,14 @@ export function selectBranchMessages(

const target = localMessages[localIndex]

let authoritativeIndex =
target.rowId === undefined
? -1
: authoritativeMessages.findIndex(message => message.rowId !== undefined && message.rowId === target.rowId)
const sourceRowIds = (message: ChatMessage) =>
[message.rowId, ...message.parts.map(part => part.sourceRowId)].flatMap(rowId => rowId ?? [])

const targetRowIds = new Set(sourceRowIds(target))

let authoritativeIndex = targetRowIds.size
? authoritativeMessages.findIndex(message => sourceRowIds(message).some(rowId => targetRowIds.has(rowId)))
: -1

// Strip `@image:` directive lines the same way the persisted→ChatMessage
// conversion does (extractImageRefs lifts them into attachmentRefs), so a
Expand Down