@@ -13,6 +13,7 @@ function createMessage({
1313 parallelGroupId = null ,
1414 parallelIndex = null ,
1515 activeStreamId = null ,
16+ text = "" ,
1617} : {
1718 id : string ;
1819 role : ChatMessage [ "role" ] ;
@@ -21,11 +22,12 @@ function createMessage({
2122 parallelGroupId ?: string | null ;
2223 parallelIndex ?: number | null ;
2324 activeStreamId ?: string | null ;
25+ text ?: string ;
2426} ) : ChatMessage {
2527 return {
2628 id,
2729 role,
28- parts : [ ] ,
30+ parts : text ? [ { type : "text" , text } ] : [ ] ,
2931 metadata : {
3032 createdAt : new Date ( createdAt ) ,
3133 parentMessageId,
@@ -44,14 +46,82 @@ function createThreadStore(initialMessages: ChatMessage[]) {
4446 withThreads < ChatMessage , BaseChatStoreState < ChatMessage > > (
4547 ( set ) =>
4648 ( {
49+ _messageIndex : { update : ( ) => undefined } ,
50+ _memoizedSelectors : new Map ( ) ,
51+ _throttledMessages : initialMessages ,
4752 messages : initialMessages ,
4853 setMessages : ( messages : ChatMessage [ ] ) => set ( { messages } ) ,
49- } ) as BaseChatStoreState < ChatMessage >
54+ } ) as unknown as BaseChatStoreState < ChatMessage >
5055 )
5156 ) ;
5257}
5358
5459describe ( "withThreads" , ( ) => {
60+ it ( "preserves hidden branches when ThreadChat publishes an active-path snapshot" , ( ) => {
61+ const userA = createMessage ( {
62+ id : "user-a" ,
63+ role : "user" ,
64+ createdAt : "2024-01-01T00:00:00.000Z" ,
65+ } ) ;
66+ const assistantA = createMessage ( {
67+ id : "assistant-a" ,
68+ role : "assistant" ,
69+ createdAt : "2024-01-01T00:00:01.000Z" ,
70+ parentMessageId : userA . id ,
71+ } ) ;
72+ const userB = createMessage ( {
73+ id : "user-b" ,
74+ role : "user" ,
75+ createdAt : "2024-01-01T00:00:02.000Z" ,
76+ } ) ;
77+ const assistantB = createMessage ( {
78+ id : "assistant-b" ,
79+ role : "assistant" ,
80+ createdAt : "2024-01-01T00:00:03.000Z" ,
81+ parentMessageId : userB . id ,
82+ } ) ;
83+ const store = createThreadStore ( [ userB , assistantB ] ) ;
84+
85+ store . getState ( ) . setAllMessages ( [ userA , assistantA , userB , assistantB ] ) ;
86+ store . getState ( ) . setTreeSnapshot ( {
87+ childrenByParentId : {
88+ __root__ : [ userB . id ] ,
89+ [ userB . id ] : [ assistantB . id ] ,
90+ } ,
91+ cursorId : assistantB . id ,
92+ messagesById : {
93+ [ userB . id ] : userB ,
94+ [ assistantB . id ] : assistantB ,
95+ } ,
96+ parentById : {
97+ [ userB . id ] : null ,
98+ [ assistantB . id ] : userB . id ,
99+ } ,
100+ rootIds : [ userB . id ] ,
101+ version : 1 ,
102+ } ) ;
103+
104+ const siblingInfo = store . getState ( ) . getMessageSiblingInfo ( userB . id ) ;
105+ assert . deepEqual (
106+ siblingInfo ?. siblings . map ( ( message ) => message . id ) ,
107+ [ userA . id , userB . id ]
108+ ) ;
109+ const previousThread = store . getState ( ) . switchToSibling ( userB . id , "prev" ) ;
110+ const previousThreadIds = [ userA . id , assistantA . id ] ;
111+ assert . deepEqual (
112+ previousThread ?. map ( ( message ) => message . id ) ,
113+ previousThreadIds
114+ ) ;
115+ assert . deepEqual (
116+ store . getState ( ) . messages . map ( ( message ) => message . id ) ,
117+ previousThreadIds
118+ ) ;
119+ assert . deepEqual (
120+ store . getState ( ) . _throttledMessages ?. map ( ( message ) => message . id ) ,
121+ previousThreadIds
122+ ) ;
123+ } ) ;
124+
55125 it ( "preserves local-only optimistic branch nodes across server syncs" , ( ) => {
56126 const rootUser = createMessage ( {
57127 id : "user-root" ,
@@ -118,9 +188,9 @@ describe("withThreads", () => {
118188 assert . deepEqual ( allMessageIds , [
119189 "user-root" ,
120190 "assistant-a" ,
121- "assistant-b" ,
122191 "user-nested" ,
123192 "assistant-nested-a" ,
193+ "assistant-b" ,
124194 "assistant-nested-b" ,
125195 ] ) ;
126196
@@ -134,4 +204,182 @@ describe("withThreads", () => {
134204 "pending:assistant-nested-b"
135205 ) ;
136206 } ) ;
207+
208+ it ( "preserves a pending stream marker when a run inserts its assistant shell" , ( ) => {
209+ const rootUser = createMessage ( {
210+ id : "user-root" ,
211+ role : "user" ,
212+ createdAt : "2024-01-01T00:00:00.000Z" ,
213+ } ) ;
214+ const assistant = createMessage ( {
215+ id : "assistant-a" ,
216+ role : "assistant" ,
217+ createdAt : "2024-01-01T00:00:01.000Z" ,
218+ parentMessageId : rootUser . id ,
219+ activeStreamId : "pending:assistant-a" ,
220+ } ) ;
221+ const assistantWithoutMetadata = {
222+ id : assistant . id ,
223+ parts : [ ] ,
224+ role : "assistant" ,
225+ } as unknown as ChatMessage ;
226+
227+ const store = createThreadStore ( [ rootUser , assistant ] ) ;
228+ store . getState ( ) . addMessageToTree ( assistantWithoutMetadata ) ;
229+
230+ const updatedAssistant = store . getState ( ) . allMessages . at ( - 1 ) ;
231+ assert . equal (
232+ updatedAssistant ?. metadata . activeStreamId ,
233+ "pending:assistant-a"
234+ ) ;
235+ assert . equal (
236+ updatedAssistant ?. metadata . selectedModel ,
237+ assistant . metadata . selectedModel
238+ ) ;
239+ } ) ;
240+
241+ it ( "replaces a selected placeholder with completed server content when ready" , ( ) => {
242+ const rootUser = createMessage ( {
243+ id : "user-root" ,
244+ role : "user" ,
245+ createdAt : "2024-01-01T00:00:00.000Z" ,
246+ } ) ;
247+ const placeholder = createMessage ( {
248+ id : "assistant-b" ,
249+ role : "assistant" ,
250+ createdAt : "2024-01-01T00:00:01.000Z" ,
251+ parentMessageId : rootUser . id ,
252+ parallelGroupId : "group-root" ,
253+ parallelIndex : 1 ,
254+ activeStreamId : "pending:assistant-b" ,
255+ } ) ;
256+ const completed = createMessage ( {
257+ id : placeholder . id ,
258+ role : "assistant" ,
259+ createdAt : "2024-01-01T00:00:01.000Z" ,
260+ parentMessageId : rootUser . id ,
261+ parallelGroupId : "group-root" ,
262+ parallelIndex : 1 ,
263+ text : "Completed secondary response" ,
264+ } ) ;
265+ const store = createThreadStore ( [ rootUser , placeholder ] ) ;
266+
267+ store . getState ( ) . setAllMessages ( [ rootUser , completed ] ) ;
268+
269+ const completedPart = store . getState ( ) . messages . at ( - 1 ) ?. parts . at ( 0 ) ;
270+ assert . equal ( completedPart ?. type , "text" ) ;
271+ assert . equal (
272+ completedPart ?. type === "text" ? completedPart . text : null ,
273+ "Completed secondary response"
274+ ) ;
275+ assert . equal (
276+ store . getState ( ) . messages . at ( - 1 ) ?. metadata . activeStreamId ,
277+ null
278+ ) ;
279+ const throttledPart = store
280+ . getState ( )
281+ . _throttledMessages ?. at ( - 1 )
282+ ?. parts . at ( 0 ) ;
283+ assert . equal (
284+ throttledPart ?. type === "text" ? throttledPart . text : null ,
285+ "Completed secondary response"
286+ ) ;
287+ } ) ;
288+
289+ it ( "fills metadata for ThreadChat assistant shells in tree snapshots" , ( ) => {
290+ const rootUser = createMessage ( {
291+ id : "user-root" ,
292+ role : "user" ,
293+ createdAt : "2024-01-01T00:00:00.000Z" ,
294+ } ) ;
295+ const assistantWithoutMetadata = {
296+ id : "assistant-a" ,
297+ parts : [ ] ,
298+ role : "assistant" ,
299+ } as unknown as ChatMessage ;
300+
301+ const store = createThreadStore ( [ rootUser ] ) ;
302+
303+ store . getState ( ) . setTreeSnapshot ( {
304+ childrenByParentId : {
305+ __root__ : [ rootUser . id ] ,
306+ [ rootUser . id ] : [ assistantWithoutMetadata . id ] ,
307+ } ,
308+ cursorId : assistantWithoutMetadata . id ,
309+ messagesById : {
310+ [ rootUser . id ] : rootUser ,
311+ [ assistantWithoutMetadata . id ] : assistantWithoutMetadata ,
312+ } ,
313+ parentById : {
314+ [ rootUser . id ] : null ,
315+ [ assistantWithoutMetadata . id ] : rootUser . id ,
316+ } ,
317+ rootIds : [ rootUser . id ] ,
318+ version : 1 ,
319+ } ) ;
320+
321+ const assistant = store . getState ( ) . messages . at ( - 1 ) ;
322+ assert . equal ( assistant ?. metadata . parentMessageId , rootUser . id ) ;
323+ assert . equal (
324+ assistant ?. metadata . selectedModel ,
325+ rootUser . metadata . selectedModel
326+ ) ;
327+ } ) ;
328+
329+ it ( "adds exactly one user sibling when editing after branch navigation" , ( ) => {
330+ const userA = createMessage ( {
331+ id : "user-a" ,
332+ role : "user" ,
333+ createdAt : "2024-01-01T00:00:00.000Z" ,
334+ } ) ;
335+ const assistantA = createMessage ( {
336+ id : "assistant-a" ,
337+ role : "assistant" ,
338+ createdAt : "2024-01-01T00:00:01.000Z" ,
339+ parentMessageId : userA . id ,
340+ } ) ;
341+ const userB = createMessage ( {
342+ id : "user-b" ,
343+ role : "user" ,
344+ createdAt : "2024-01-01T00:00:02.000Z" ,
345+ } ) ;
346+ const assistantB = createMessage ( {
347+ id : "assistant-b" ,
348+ role : "assistant" ,
349+ createdAt : "2024-01-01T00:00:03.000Z" ,
350+ parentMessageId : userB . id ,
351+ } ) ;
352+ const userC = createMessage ( {
353+ id : "user-c" ,
354+ role : "user" ,
355+ createdAt : "2024-01-01T00:00:04.000Z" ,
356+ } ) ;
357+ const assistantC = createMessage ( {
358+ id : "assistant-c" ,
359+ role : "assistant" ,
360+ createdAt : "2024-01-01T00:00:05.000Z" ,
361+ parentMessageId : userC . id ,
362+ } ) ;
363+
364+ const store = createThreadStore ( [ userA , assistantA ] ) ;
365+ store . getState ( ) . addMessageToTree ( userB ) ;
366+ store . getState ( ) . addMessageToTree ( assistantB ) ;
367+
368+ assert . equal (
369+ store . getState ( ) . getMessageSiblingInfo ( userB . id ) ?. siblings . length ,
370+ 2
371+ ) ;
372+
373+ store . getState ( ) . switchToSibling ( userB . id , "prev" ) ;
374+ store . getState ( ) . setMessages ( [ ] ) ;
375+ store . getState ( ) . addMessageToTree ( userC ) ;
376+ store . getState ( ) . addMessageToTree ( assistantC ) ;
377+
378+ const rootSiblingIds = store
379+ . getState ( )
380+ . getMessageSiblingInfo ( userC . id )
381+ ?. siblings . map ( ( message : ChatMessage ) => message . id ) ;
382+
383+ assert . deepEqual ( rootSiblingIds , [ userA . id , userB . id , userC . id ] ) ;
384+ } ) ;
137385} ) ;
0 commit comments