@@ -48,6 +48,38 @@ export type ServiceHub = {
4848 }
4949}
5050
51+ /**
52+ * Wraps a UIMessageChunk stream so that when the first `text-start` chunk
53+ * arrives, a `text-delta` carrying `prefixText` is immediately injected into
54+ * the same text block. This makes the new message show the partial content
55+ * right away while continuation tokens stream in after it.
56+ */
57+ function prependTextDeltaToUIStream (
58+ stream : ReadableStream < UIMessageChunk > ,
59+ prefixText : string
60+ ) : ReadableStream < UIMessageChunk > {
61+ const reader = stream . getReader ( )
62+ let prefixEmitted = false
63+ return new ReadableStream < UIMessageChunk > ( {
64+ async pull ( controller ) {
65+ const { done, value } = await reader . read ( )
66+ if ( done ) {
67+ controller . close ( )
68+ return
69+ }
70+ controller . enqueue ( value )
71+ if ( ! prefixEmitted && ( value as { type : string } ) . type === 'text-start' ) {
72+ prefixEmitted = true
73+ const id = ( value as { type : 'text-start' ; id : string } ) . id
74+ controller . enqueue ( { type : 'text-delta' , id, delta : prefixText } as UIMessageChunk )
75+ }
76+ } ,
77+ cancel ( ) {
78+ reader . cancel ( )
79+ } ,
80+ } )
81+ }
82+
5183export class CustomChatTransport implements ChatTransport < UIMessage > {
5284 public model : LanguageModel | null = null
5385 private tools : Record < string , Tool > = { }
@@ -58,6 +90,7 @@ export class CustomChatTransport implements ChatTransport<UIMessage> {
5890 private systemMessage ?: string
5991 private serviceHub : ServiceHub | null
6092 private threadId ?: string
93+ private continueFromContent : string | null = null
6194
6295 constructor ( systemMessage ?: string , threadId ?: string ) {
6396 this . systemMessage = systemMessage
@@ -213,6 +246,14 @@ export class CustomChatTransport implements ChatTransport<UIMessage> {
213246 return this . tools
214247 }
215248
249+ /**
250+ * Set partial assistant content to send as a prefill on the next request,
251+ * so the model continues generation from where it left off.
252+ */
253+ setContinueFromContent ( content : string ) {
254+ this . continueFromContent = content
255+ }
256+
216257 async sendMessages (
217258 options : {
218259 chatId : string
@@ -258,10 +299,18 @@ export class CustomChatTransport implements ChatTransport<UIMessage> {
258299 }
259300
260301 // Convert UI messages to model messages
261- const modelMessages = convertToModelMessages (
302+ const baseMessages = convertToModelMessages (
262303 this . mapUserInlineAttachments ( options . messages )
263304 )
264305
306+ // If continuing a truncated response, append the partial assistant content as a
307+ // prefill so the model resumes from where it left off rather than regenerating.
308+ const continueContent = this . continueFromContent
309+ this . continueFromContent = null
310+ const modelMessages = continueContent
311+ ? [ ...baseMessages , { role : 'assistant' as const , content : continueContent } ]
312+ : baseMessages
313+
265314 // Include tools only if we have tools loaded AND model supports them
266315 const hasTools = Object . keys ( this . tools ) . length > 0
267316 const selectedModel = useModelProvider . getState ( ) . selectedModel
@@ -282,7 +331,7 @@ export class CustomChatTransport implements ChatTransport<UIMessage> {
282331
283332 let tokensPerSecond = 0
284333
285- return result . toUIMessageStream ( {
334+ const uiStream = result . toUIMessageStream ( {
286335 messageMetadata : ( { part } ) => {
287336 // Track stream start time on start
288337 if ( part . type === 'start' && ! streamStartTime ) {
@@ -320,6 +369,7 @@ export class CustomChatTransport implements ChatTransport<UIMessage> {
320369 }
321370
322371 return {
372+ finishReason : finishPart . finishReason ,
323373 usage : {
324374 inputTokens : inputTokens ,
325375 outputTokens : outputTokens ,
@@ -364,6 +414,13 @@ export class CustomChatTransport implements ChatTransport<UIMessage> {
364414 }
365415 } ,
366416 } )
417+
418+ // When continuing a truncated response, inject the partial content as the
419+ // very first text-delta so the new message immediately shows it and the
420+ // user sees a seamless continuation rather than an empty box.
421+ return continueContent
422+ ? prependTextDeltaToUIStream ( uiStream , continueContent )
423+ : uiStream
367424 }
368425
369426 async reconnectToStream (
0 commit comments