11import type { ChatItem , MessageGroup , NormalizedMessage , ToolCard } from "../types/chat-types.ts" ;
2+ import type { ChatQueueItem } from "../ui-types.ts" ;
23import {
34 isAssistantHeartbeatAckForDisplay ,
45 stripHeartbeatTokenForDisplay ,
@@ -9,6 +10,7 @@ import { normalizeMessage, stripMessageDisplayMetadataText } from "./message-nor
910import { normalizeRoleForGrouping } from "./role-normalizer.ts" ;
1011import { messageMatchesSearchQuery } from "./search-match.ts" ;
1112import { extractToolCardsCached , extractToolPreview } from "./tool-cards.ts" ;
13+ import { buildUserChatMessageContentBlocks } from "./user-message-content.ts" ;
1214
1315export type BuildChatItemsProps = {
1416 sessionKey : string ;
@@ -17,6 +19,7 @@ export type BuildChatItemsProps = {
1719 streamSegments : Array < { text : string ; ts : number } > ;
1820 stream : string | null ;
1921 streamStartedAt : number | null ;
22+ queue ?: ChatQueueItem [ ] ;
2023 showToolCalls : boolean ;
2124 searchOpen ?: boolean ;
2225 searchQuery ?: string ;
@@ -223,6 +226,10 @@ function groupMessages(items: ChatItem[]): Array<ChatItem | MessageGroup> {
223226}
224227
225228function collapseDuplicateDisplaySignature ( message : unknown ) : string | null {
229+ const marker = asRecord ( message ) ?. [ "__openclaw" ] ;
230+ if ( asRecord ( marker ) ?. kind === "pending-send" ) {
231+ return null ;
232+ }
226233 const normalized = safeNormalizeMessage ( message ) ;
227234 if ( ! normalized ) {
228235 return null ;
@@ -292,6 +299,34 @@ function trimAccumulatedStreamPrefix(text: string, previousText: string | null):
292299 return text . slice ( previousText . length ) . trimStart ( ) ;
293300}
294301
302+ function shouldRenderQueuedSendInThread ( item : ChatQueueItem ) : boolean {
303+ if ( typeof item . sendSubmittedAtMs !== "number" || item . sendState === "failed" ) {
304+ return false ;
305+ }
306+ return (
307+ item . sendState === "waiting-model" ||
308+ item . sendState === "sending" ||
309+ item . sendState === "waiting-reconnect"
310+ ) ;
311+ }
312+
313+ function queuedSendThreadMessage ( item : ChatQueueItem ) : Record < string , unknown > | null {
314+ const content = buildUserChatMessageContentBlocks ( item . text , item . attachments ) ;
315+ if ( content . length === 0 ) {
316+ return null ;
317+ }
318+ return {
319+ role : "user" ,
320+ content,
321+ timestamp : item . createdAt ,
322+ __openclaw : {
323+ kind : "pending-send" ,
324+ id : item . id ,
325+ state : item . sendState ,
326+ } ,
327+ } ;
328+ }
329+
295330function rawMessageTimestamp ( message : unknown ) : number | null {
296331 const timestamp = asRecord ( message ) ?. timestamp ;
297332 return typeof timestamp === "number" && Number . isFinite ( timestamp ) ? timestamp : null ;
@@ -535,6 +570,29 @@ export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | Mes
535570 message : msg ,
536571 } ) ;
537572 }
573+ const queuedSends = Array . isArray ( props . queue ) ? props . queue : [ ] ;
574+ for ( const queued of queuedSends ) {
575+ if ( ! shouldRenderQueuedSendInThread ( queued ) ) {
576+ continue ;
577+ }
578+ const message = queuedSendThreadMessage ( queued ) ;
579+ if ( ! message ) {
580+ continue ;
581+ }
582+ const searchQuery = props . searchQuery ?? "" ;
583+ if (
584+ props . searchOpen &&
585+ searchQuery . trim ( ) &&
586+ ! messageMatchesSearchQuery ( message , searchQuery )
587+ ) {
588+ continue ;
589+ }
590+ items . push ( {
591+ kind : "message" ,
592+ key : `pending-send:${ queued . id } ` ,
593+ message,
594+ } ) ;
595+ }
538596 for ( const liftedCanvasSource of liftedCanvasSources ) {
539597 const assistantIndex = findNearestAssistantMessageIndex ( items , liftedCanvasSource . timestamp ) ;
540598 if ( assistantIndex == null ) {
0 commit comments