@@ -18,7 +18,11 @@ import {
1818 workEntryDisplayIndicatesToolFailure ,
1919 type TimelineEntry ,
2020} from "../../session-logic" ;
21- import { makeStreamingTimelineFixture } from "../../test-fixtures" ;
21+ import {
22+ makeStreamingTimelineFixture ,
23+ makeStreamingWorkTimelineFixture ,
24+ withStreamedText ,
25+ } from "../../test-fixtures" ;
2226import type { TurnDiffSummary } from "../../types" ;
2327import { describe , expect , it } from "vite-plus/test" ;
2428import { EnvironmentId , MessageId , RunId } from "@t3tools/contracts" ;
@@ -3817,6 +3821,219 @@ describe("streaming v2 row projection", () => {
38173821 } ) ) ,
38183822 } ) ;
38193823 } ) ;
3824+
3825+ it . each ( [ "reasoning" , "proposed_plan" ] as const ) (
3826+ "keeps every row not holding streaming %s text when it changes" ,
3827+ ( kind ) => {
3828+ const source = makeStreamingWorkTimelineFixture ( kind ) ;
3829+ const rowsInput = ( timelineEntries : ReadonlyArray < TimelineEntry > ) => ( {
3830+ timelineEntries,
3831+ latestRun : {
3832+ runId : source . runId ,
3833+ status : "running" as const ,
3834+ startedAt : source . time ( 5 ) ,
3835+ completedAt : null ,
3836+ } ,
3837+ runningRunId : source . runId ,
3838+ isWorking : true ,
3839+ activeTurnStartedAt : source . time ( 5 ) ,
3840+ turnDiffSummaries : [ ] ,
3841+ supportsConversationRollback : true ,
3842+ } ) ;
3843+ let timelineInput = {
3844+ visibleTurnItems : withStreamedText (
3845+ source . visibleTurnItems ,
3846+ source . streamingItemId ,
3847+ "Let" ,
3848+ source . time ( 20 ) ,
3849+ ) ,
3850+ optimisticMessages : [ ] ,
3851+ } ;
3852+ let timeline = deriveTimelineEntriesFromVisibleTurnItemsWithState ( timelineInput ) ;
3853+ let projection = deriveMessagesTimelineRowsWithState ( rowsInput ( timeline . entries ) ) ;
3854+ for ( const [ step , text ] of [ "Let me" , "Let me check the tests" ] . entries ( ) ) {
3855+ timelineInput = {
3856+ visibleTurnItems : withStreamedText (
3857+ timelineInput . visibleTurnItems ,
3858+ source . streamingItemId ,
3859+ text ,
3860+ source . time ( 21 + step ) ,
3861+ ) ,
3862+ optimisticMessages : [ ] ,
3863+ } ;
3864+ const previous = projection ;
3865+ timeline = deriveTimelineEntriesFromVisibleTurnItemsWithState ( timelineInput , timeline ) ;
3866+ projection = deriveMessagesTimelineRowsWithState ( rowsInput ( timeline . entries ) , previous ) ;
3867+ expect ( projection . rows ) . toEqual (
3868+ deriveMessagesTimelineRows (
3869+ rowsInput ( deriveTimelineEntriesFromVisibleTurnItems ( timelineInput ) ) ,
3870+ ) ,
3871+ ) ;
3872+ expect ( projection . rows ) . toHaveLength ( previous . rows . length ) ;
3873+ for ( const [ index , row ] of previous . rows . entries ( ) ) {
3874+ const holdsItem =
3875+ row . id === source . streamingItemId ||
3876+ ( ( row . kind === "work" || row . kind === "work-live" ) &&
3877+ row . groupedEntries . some ( ( entry ) => entry . id === source . streamingItemId ) ) ;
3878+ if ( ! holdsItem ) expect ( projection . rows [ index ] ) . toBe ( row ) ;
3879+ }
3880+ }
3881+ } ,
3882+ ) ;
3883+
3884+ it . each ( [ "reasoning" , "proposed_plan" ] as const ) (
3885+ "matches a full derivation when streaming %s text turns blank or completes" ,
3886+ ( kind ) => {
3887+ const source = makeStreamingWorkTimelineFixture ( kind ) ;
3888+ const rowsInput = ( timelineEntries : ReadonlyArray < TimelineEntry > ) => ( {
3889+ timelineEntries,
3890+ latestRun : {
3891+ runId : source . runId ,
3892+ status : "running" as const ,
3893+ startedAt : source . time ( 5 ) ,
3894+ completedAt : null ,
3895+ } ,
3896+ runningRunId : source . runId ,
3897+ isWorking : true ,
3898+ activeTurnStartedAt : source . time ( 5 ) ,
3899+ turnDiffSummaries : [ ] ,
3900+ supportsConversationRollback : true ,
3901+ } ) ;
3902+ let visibleTurnItems = source . visibleTurnItems ;
3903+ let timeline = deriveTimelineEntriesFromVisibleTurnItemsWithState ( {
3904+ visibleTurnItems,
3905+ optimisticMessages : [ ] ,
3906+ } ) ;
3907+ let projection = deriveMessagesTimelineRowsWithState ( rowsInput ( timeline . entries ) ) ;
3908+ const steps = [
3909+ ...[ "Let" , " " , "Let me" ] . map (
3910+ ( text , step ) => ( rows : typeof visibleTurnItems ) =>
3911+ withStreamedText ( rows , source . streamingItemId , text , source . time ( 20 + step ) ) ,
3912+ ) ,
3913+ ( rows : typeof visibleTurnItems ) =>
3914+ rows . map ( ( row ) =>
3915+ row . item . id === source . streamingItemId &&
3916+ ( row . item . type === "reasoning" || row . item . type === "proposed_plan" )
3917+ ? {
3918+ ...row ,
3919+ item : {
3920+ ...row . item ,
3921+ status : "completed" as const ,
3922+ streaming : false ,
3923+ completedAt : DateTime . makeUnsafe ( source . time ( 30 ) ) ,
3924+ updatedAt : DateTime . makeUnsafe ( source . time ( 30 ) ) ,
3925+ } ,
3926+ }
3927+ : row ,
3928+ ) ,
3929+ ] ;
3930+ for ( const step of steps ) {
3931+ visibleTurnItems = step ( visibleTurnItems ) ;
3932+ const timelineInput = { visibleTurnItems, optimisticMessages : [ ] } ;
3933+ timeline = deriveTimelineEntriesFromVisibleTurnItemsWithState ( timelineInput , timeline ) ;
3934+ expect ( timeline . entries ) . toEqual ( deriveTimelineEntriesFromVisibleTurnItems ( timelineInput ) ) ;
3935+ projection = deriveMessagesTimelineRowsWithState ( rowsInput ( timeline . entries ) , projection ) ;
3936+ expect ( projection . rows ) . toEqual (
3937+ deriveMessagesTimelineRows (
3938+ rowsInput ( deriveTimelineEntriesFromVisibleTurnItems ( timelineInput ) ) ,
3939+ ) ,
3940+ ) ;
3941+ }
3942+ } ,
3943+ ) ;
3944+
3945+ it . each ( [
3946+ "expanded live group" ,
3947+ "thinking superseded by a running tool" ,
3948+ "settled turn" ,
3949+ ] as const ) ( "matches a full derivation when streaming thinking sits in a %s" , ( scenario ) => {
3950+ const source = makeStreamingWorkTimelineFixture ( "reasoning" ) ;
3951+ const running = scenario !== "settled turn" ;
3952+ const rowsInput = (
3953+ timelineEntries : ReadonlyArray < TimelineEntry > ,
3954+ expandedWorkGroupIds ?: ReadonlySet < string > ,
3955+ ) => ( {
3956+ timelineEntries,
3957+ latestRun : {
3958+ runId : source . runId ,
3959+ status : running ? ( "running" as const ) : ( "completed" as const ) ,
3960+ startedAt : source . time ( 5 ) ,
3961+ completedAt : running ? null : source . time ( 40 ) ,
3962+ } ,
3963+ runningRunId : running ? source . runId : null ,
3964+ isWorking : running ,
3965+ activeTurnStartedAt : source . time ( 5 ) ,
3966+ turnDiffSummaries : [ ] ,
3967+ supportsConversationRollback : true ,
3968+ ...( expandedWorkGroupIds ? { expandedWorkGroupIds } : { } ) ,
3969+ ...( running ? { } : { expandedRunIds : new Set ( [ source . runId ] ) } ) ,
3970+ } ) ;
3971+ const tool = source . visibleTurnItems . find ( ( row ) => row . item . id === "live-work" ) ! ;
3972+ let visibleTurnItems =
3973+ scenario === "thinking superseded by a running tool"
3974+ ? [
3975+ ...source . visibleTurnItems ,
3976+ {
3977+ ...tool ,
3978+ position : source . visibleTurnItems . length ,
3979+ sourceItemId : TurnItemId . make ( "live-running-tool" ) ,
3980+ item : {
3981+ ...tool . item ,
3982+ id : TurnItemId . make ( "live-running-tool" ) ,
3983+ ordinal : 8 ,
3984+ status : "running" as const ,
3985+ completedAt : null ,
3986+ } ,
3987+ } ,
3988+ ]
3989+ : // Alone in a settled group, the thought's text becomes the group summary.
3990+ source . visibleTurnItems . filter ( ( row ) => running || row !== tool ) ;
3991+ visibleTurnItems = withStreamedText (
3992+ visibleTurnItems ,
3993+ source . streamingItemId ,
3994+ "Let" ,
3995+ source . time ( 20 ) ,
3996+ ) ;
3997+ let timeline = deriveTimelineEntriesFromVisibleTurnItemsWithState ( {
3998+ visibleTurnItems,
3999+ optimisticMessages : [ ] ,
4000+ } ) ;
4001+ const liveRow = deriveMessagesTimelineRows ( rowsInput ( timeline . entries ) ) . find (
4002+ ( row ) => row . kind === "work-live" ,
4003+ ) ;
4004+ const expanded =
4005+ scenario === "expanded live group" && liveRow ?. kind === "work-live"
4006+ ? new Set ( [ liveRow . groupId ] )
4007+ : undefined ;
4008+ let projection = deriveMessagesTimelineRowsWithState ( rowsInput ( timeline . entries , expanded ) ) ;
4009+ for ( const [ step , text ] of [ "Let me" , "Let me check the tests" ] . entries ( ) ) {
4010+ visibleTurnItems = withStreamedText (
4011+ visibleTurnItems ,
4012+ source . streamingItemId ,
4013+ text ,
4014+ source . time ( 21 + step ) ,
4015+ ) ;
4016+ const timelineInput = { visibleTurnItems, optimisticMessages : [ ] } ;
4017+ const previous = projection ;
4018+ timeline = deriveTimelineEntriesFromVisibleTurnItemsWithState ( timelineInput , timeline ) ;
4019+ projection = deriveMessagesTimelineRowsWithState (
4020+ rowsInput ( timeline . entries , expanded ) ,
4021+ previous ,
4022+ ) ;
4023+ expect ( projection . rows ) . toEqual (
4024+ deriveMessagesTimelineRows (
4025+ rowsInput ( deriveTimelineEntriesFromVisibleTurnItems ( timelineInput ) , expanded ) ,
4026+ ) ,
4027+ ) ;
4028+ if ( ! running ) continue ;
4029+ for ( const [ index , row ] of previous . rows . entries ( ) ) {
4030+ const holdsItem =
4031+ ( row . kind === "work" || row . kind === "work-live" ) &&
4032+ row . groupedEntries . some ( ( entry ) => entry . id === source . streamingItemId ) ;
4033+ if ( ! holdsItem ) expect ( projection . rows [ index ] ) . toBe ( row ) ;
4034+ }
4035+ }
4036+ } ) ;
38204037} ) ;
38214038
38224039describe ( "linked timeline resources" , ( ) => {
0 commit comments