11import type { AssistantMessage } from "@earendil-works/pi-ai" ;
22import { type Component , Container , Markdown , type MarkdownTheme , Spacer , Text } from "@earendil-works/pi-tui" ;
33import type { MarkdownTransformer } from "../../../core/extensions/types.ts" ;
4- import { formatDuration } from "../../../utils/duration.ts" ;
5- import { formatProviderNativeBody , formatProviderNativeSummary } from "../../provider-native-rendering.ts" ;
64import { getMarkdownTheme , theme } from "../theme/theme.ts" ;
5+ import { type AssistantRenderDescriptor , createAssistantRenderDescriptors } from "./assistant-render-descriptors.ts" ;
76import { createMarkdownTransform } from "./markdown-transform.ts" ;
87import { createBoundedRenderSignature } from "./render-signature.ts" ;
98
109const OSC133_ZONE_START = "\x1b]133;A\x07" ;
1110const OSC133_ZONE_END = "\x1b]133;B\x07" ;
1211const OSC133_ZONE_FINAL = "\x1b]133;C\x07" ;
1312
14- type MarkdownDescriptorKind = "text-md" | "thinking-md" ;
15- type TextDescriptorKind = "thinking-label" | "provider-native-summary" | "provider-native-body" | "error-text" ;
16- type RenderDescriptorKind = "spacer" | MarkdownDescriptorKind | TextDescriptorKind ;
17- type RenderDescriptor = { readonly kind : RenderDescriptorKind ; readonly text : string } ;
18-
19- const SPACER_DESCRIPTOR = { kind : "spacer" , text : "" } as const satisfies RenderDescriptor ;
20-
2113function assertNever ( value : never ) : never {
2214 throw new TypeError ( `Unexpected assistant render variant: ${ String ( value ) } ` ) ;
2315}
2416
25- function isVisibleContent ( content : AssistantMessage [ "content" ] [ number ] , providerNativeVisible : boolean ) : boolean {
26- switch ( content . type ) {
27- case "text" :
28- return Boolean ( content . text . trim ( ) ) ;
29- case "thinking" :
30- return Boolean ( content . thinking . trim ( ) ) ;
31- case "providerNative" :
32- return providerNativeVisible ;
33- case "toolCall" :
34- return false ;
35- default :
36- return assertNever ( content ) ;
37- }
38- }
39-
4017export class AssistantMessageComponent extends Container {
4118 private renderCache ?: { readonly lines : string [ ] ; readonly signature : string ; readonly width : number } ;
4219 private contentContainer : Container ;
@@ -47,7 +24,7 @@ export class AssistantMessageComponent extends Container {
4724 private markdownTransformers : readonly MarkdownTransformer [ ] ;
4825 private lastMessage ?: AssistantMessage ;
4926 private lastMessageSignature ?: string ;
50- private renderDescriptors : readonly RenderDescriptor [ ] = [ ] ;
27+ private renderDescriptors : readonly AssistantRenderDescriptor [ ] = [ ] ;
5128 private hasToolCalls = false ;
5229 private expanded = false ;
5330 private isStreaming = false ;
@@ -137,122 +114,16 @@ export class AssistantMessageComponent extends Container {
137114 this . renderCache = undefined ;
138115 if ( streamingChanged ) this . renderDescriptors = [ ] ;
139116 this . hasToolCalls = message . content . some ( ( content ) => content . type === "toolCall" ) ;
140- const descriptors = this . createRenderDescriptors ( message ) ;
117+ const descriptors = createAssistantRenderDescriptors ( message , {
118+ expanded : this . expanded ,
119+ hiddenThinkingLabel : this . hiddenThinkingLabel ,
120+ hideThinkingBlock : this . hideThinkingBlock ,
121+ hasToolCalls : this . hasToolCalls ,
122+ } ) ;
141123 this . reconcileRenderDescriptors ( descriptors ) ;
142124 }
143125
144- private createRenderDescriptors ( message : AssistantMessage ) : readonly RenderDescriptor [ ] {
145- const descriptors : RenderDescriptor [ ] = [ ] ;
146- if ( message . content . some ( ( content ) => isVisibleContent ( content , true ) ) ) descriptors . push ( SPACER_DESCRIPTOR ) ;
147- for ( let i = 0 ; i < message . content . length ; i ++ ) {
148- const content = message . content [ i ] ;
149- switch ( content . type ) {
150- case "text" : {
151- const text = content . text . trim ( ) ;
152- if ( text ) descriptors . push ( { kind : "text-md" , text } ) ;
153- break ;
154- }
155- case "thinking" : {
156- const thinkingBlocks : string [ ] = [ ] ;
157- let hasTiming = false ;
158- let isDone = true ;
159- let minStart = Number . POSITIVE_INFINITY ;
160- let maxEnd = Number . NEGATIVE_INFINITY ;
161- for ( ; i < message . content . length ; i ++ ) {
162- const thinkingContent = message . content [ i ] ;
163- if ( thinkingContent . type !== "thinking" ) break ;
164- const startedAt = thinkingContent . startedAt ;
165- if ( startedAt !== undefined ) {
166- hasTiming = true ;
167- minStart = Math . min ( minStart , startedAt ) ;
168- const endedAt = thinkingContent . endedAt ;
169- if ( endedAt === undefined ) {
170- isDone = false ;
171- } else {
172- maxEnd = Math . max ( maxEnd , endedAt ) ;
173- }
174- }
175- const thinking = thinkingContent . thinking . trim ( ) ;
176- if ( thinking ) thinkingBlocks . push ( thinking ) ;
177- }
178- i -- ;
179- if ( thinkingBlocks . length === 0 ) break ;
180- if ( ! hasTiming ) {
181- const text = this . hideThinkingBlock
182- ? theme . italic ( theme . fg ( "thinkingText" , this . hiddenThinkingLabel ) )
183- : thinkingBlocks . join ( "\n\n" ) ;
184- descriptors . push ( { kind : this . hideThinkingBlock ? "thinking-label" : "thinking-md" , text } ) ;
185- } else {
186- const label = isDone
187- ? theme . italic (
188- theme . fg ( "thinkingText" , `Thought: ${ formatDuration ( Math . max ( 0 , maxEnd - minStart ) ) } ` ) ,
189- )
190- : theme . italic ( theme . fg ( "thinkingText" , this . hiddenThinkingLabel ) ) ;
191- if ( this . hideThinkingBlock ) {
192- descriptors . push ( { kind : "thinking-label" , text : label } ) ;
193- } else {
194- descriptors . push (
195- { kind : "thinking-label" , text : label } ,
196- { kind : "thinking-md" , text : thinkingBlocks . join ( "\n\n" ) } ,
197- ) ;
198- }
199- }
200- if ( message . content . slice ( i + 1 ) . some ( ( following ) => isVisibleContent ( following , false ) ) )
201- descriptors . push ( SPACER_DESCRIPTOR ) ;
202- break ;
203- }
204- case "providerNative" :
205- descriptors . push (
206- {
207- kind : "provider-native-summary" ,
208- text : theme . fg ( "muted" , formatProviderNativeSummary ( message , content , this . expanded ) ) ,
209- } ,
210- {
211- kind : "provider-native-body" ,
212- text : theme . fg ( "dim" , formatProviderNativeBody ( content , this . expanded ) ) ,
213- } ,
214- ) ;
215- if ( message . content . slice ( i + 1 ) . some ( ( following ) => isVisibleContent ( following , true ) ) )
216- descriptors . push ( SPACER_DESCRIPTOR ) ;
217- break ;
218- case "toolCall" :
219- break ;
220- default :
221- assertNever ( content ) ;
222- }
223- }
224- const addError = ( text : string ) : void => {
225- descriptors . push ( SPACER_DESCRIPTOR , { kind : "error-text" , text : theme . fg ( "error" , text ) } ) ;
226- } ;
227- switch ( message . stopReason ) {
228- case "length" :
229- addError (
230- "Error: Model stopped because it reached the maximum output token limit. The response may be incomplete." ,
231- ) ;
232- break ;
233- case "aborted" : {
234- if ( this . hasToolCalls ) break ;
235- const abortMessage =
236- message . errorMessage && message . errorMessage !== "Request was aborted"
237- ? message . errorMessage
238- : "Operation aborted" ;
239- addError ( abortMessage ) ;
240- break ;
241- }
242- case "error" :
243- if ( ! this . hasToolCalls ) addError ( `Error: ${ message . errorMessage || "Unknown error" } ` ) ;
244- break ;
245- case "pending" :
246- case "stop" :
247- case "toolUse" :
248- break ;
249- default :
250- assertNever ( message . stopReason ) ;
251- }
252- return descriptors ;
253- }
254-
255- private reconcileRenderDescriptors ( descriptors : readonly RenderDescriptor [ ] ) : void {
126+ private reconcileRenderDescriptors ( descriptors : readonly AssistantRenderDescriptor [ ] ) : void {
256127 let divergentIndex = 0 ;
257128 const sharedLength = Math . min ( this . renderDescriptors . length , descriptors . length ) ;
258129 while ( divergentIndex < sharedLength ) {
@@ -273,7 +144,7 @@ export class AssistantMessageComponent extends Container {
273144 this . renderDescriptors = descriptors ;
274145 }
275146
276- private createRenderChild ( descriptor : RenderDescriptor ) : Component {
147+ private createRenderChild ( descriptor : AssistantRenderDescriptor ) : Component {
277148 switch ( descriptor . kind ) {
278149 case "spacer" :
279150 return new Spacer ( 1 ) ;
@@ -312,7 +183,7 @@ export class AssistantMessageComponent extends Container {
312183 content : message . content ,
313184 hiddenThinkingLabel : this . hiddenThinkingLabel ,
314185 hideThinkingBlock : this . hideThinkingBlock ,
315- errorMessage : message . errorMessage ,
186+ errorState : [ message . diagnostics , message . errorMessage ] ,
316187 stopReason : message . stopReason ,
317188 } ) ;
318189 }
0 commit comments