@@ -887,6 +887,7 @@ function ChatView({ turns }: { turns: Turn[] }) {
887887 const rawExpanded = rawText != null && expandedRaw . has ( index ) ;
888888 const hasActions = isTool || rawText != null ;
889889 const bubbleClass = isTool ? "chat-bubble-tool" : chatBubbleClass ( turn . role ) ;
890+ const parts = rawExpanded || collapsed ? null : visualContentParts ( turn ) ;
890891 return (
891892 < div
892893 key = { index }
@@ -925,17 +926,16 @@ function ChatView({ turns }: { turns: Turn[] }) {
925926 < pre className = "chat-raw-json" > { rawText } </ pre >
926927 ) : collapsed ? (
927928 < div className = "chat-preview" > { chatPreview ( turn ) } </ div >
929+ ) : parts ? (
930+ < >
931+ < StructuredContentParts parts = { parts } fallbackText = { turnText ( turn ) } />
932+ { turn . attachments ?. length ? (
933+ < div className = "chat-attachments" > attachments: { turn . attachments . join ( "; " ) } </ div >
934+ ) : null }
935+ </ >
928936 ) : (
929937 < >
930- < div className = "chat-body prose-md" >
931- < ReactMarkdown
932- remarkPlugins = { [ remarkGfm ] }
933- urlTransform = { markdownUrlTransform }
934- components = { { strong : MarkdownStrong } }
935- >
936- { turnText ( turn ) }
937- </ ReactMarkdown >
938- </ div >
938+ < MarkdownContent text = { turnText ( turn ) } className = "chat-body prose-md" />
939939 { turn . attachments ?. length ? (
940940 < div className = "chat-attachments" > attachments: { turn . attachments . join ( "; " ) } </ div >
941941 ) : null }
@@ -949,6 +949,231 @@ function ChatView({ turns }: { turns: Turn[] }) {
949949 ) ;
950950}
951951
952+ function MarkdownContent ( { text, className } : { text : string ; className : string } ) {
953+ return (
954+ < div className = { className } >
955+ < ReactMarkdown
956+ remarkPlugins = { [ remarkGfm ] }
957+ urlTransform = { markdownUrlTransform }
958+ components = { { strong : MarkdownStrong } }
959+ >
960+ { text }
961+ </ ReactMarkdown >
962+ </ div >
963+ ) ;
964+ }
965+
966+ function StructuredContentParts ( {
967+ parts,
968+ fallbackText,
969+ } : {
970+ parts : Record < string , unknown > [ ] ;
971+ fallbackText : string ;
972+ } ) {
973+ return (
974+ < div className = "chat-parts" >
975+ { parts . map ( ( part , index ) => (
976+ < StructuredContentPart key = { index } part = { part } index = { index } />
977+ ) ) }
978+ { parts . length === 0 && < MarkdownContent text = { fallbackText } className = "chat-body prose-md" /> }
979+ </ div >
980+ ) ;
981+ }
982+
983+ function StructuredContentPart ( { part, index } : { part : Record < string , unknown > ; index : number } ) {
984+ const type = stringRecordValue ( part . type ) ;
985+ if ( type === "text" || type === "input_text" || type === "output_text" ) {
986+ const text = stringRecordValue ( part . text ) ;
987+ return text ? < MarkdownContent text = { text } className = "chat-part-text prose-md" /> : null ;
988+ }
989+ if ( type === "thinking" || type === "reasoning" ) {
990+ return < ThinkingPart part = { part } type = { type } /> ;
991+ }
992+ if ( type === "redacted_thinking" ) {
993+ return (
994+ < div className = "chat-hidden-part" >
995+ < span className = "part-type-badge" > redacted_thinking</ span >
996+ < span > Encrypted reasoning block</ span >
997+ </ div >
998+ ) ;
999+ }
1000+ if ( type === "tool_use" ) {
1001+ return < ToolUsePart part = { part } index = { index } /> ;
1002+ }
1003+ if ( type === "tool_result" ) {
1004+ return < ToolResultPart part = { part } /> ;
1005+ }
1006+ return < GenericPart part = { part } type = { type || `part ${ index + 1 } ` } /> ;
1007+ }
1008+
1009+ function ThinkingPart ( { part, type } : { part : Record < string , unknown > ; type : string } ) {
1010+ const text = stringRecordValue ( part . thinking ) || stringRecordValue ( part . text ) ;
1011+ return (
1012+ < div className = "thinking-part" >
1013+ < div className = "part-header" >
1014+ < span className = "part-type-badge" > { type } </ span >
1015+ { ! text && < span className = "part-muted" > encrypted or redacted</ span > }
1016+ </ div >
1017+ { text ? < div className = "thinking-text" > { text } </ div > : null }
1018+ </ div >
1019+ ) ;
1020+ }
1021+
1022+ function ToolUsePart ( { part, index } : { part : Record < string , unknown > ; index : number } ) {
1023+ const name = stringRecordValue ( part . name ) || `tool_${ index + 1 } ` ;
1024+ const id = stringRecordValue ( part . id ) || stringRecordValue ( part . tool_use_id ) || stringRecordValue ( part . call_id ) ;
1025+ const input = part . input ?? { } ;
1026+ const questions = askUserQuestions ( input ) ;
1027+ return (
1028+ < div className = "tool-use-card" >
1029+ < div className = "tool-use-header" >
1030+ < div className = "tool-use-title" >
1031+ < span className = "part-type-badge" > tool_use</ span >
1032+ < span className = "tool-name" > { name } </ span >
1033+ </ div >
1034+ { id && < span className = "tool-id" > { id } </ span > }
1035+ </ div >
1036+ { questions ? (
1037+ < AskUserQuestionView questions = { questions } />
1038+ ) : (
1039+ < JsonValueBlock label = "Input" value = { input } />
1040+ ) }
1041+ </ div >
1042+ ) ;
1043+ }
1044+
1045+ function ToolResultPart ( { part } : { part : Record < string , unknown > } ) {
1046+ const id = stringRecordValue ( part . tool_use_id ) || stringRecordValue ( part . id ) || stringRecordValue ( part . call_id ) ;
1047+ return (
1048+ < div className = "tool-result-card" >
1049+ < div className = "tool-use-header" >
1050+ < div className = "tool-use-title" >
1051+ < span className = "part-type-badge" > tool_result</ span >
1052+ { id && < span className = "tool-id" > { id } </ span > }
1053+ </ div >
1054+ </ div >
1055+ < ToolResultContent value = { part . content } />
1056+ </ div >
1057+ ) ;
1058+ }
1059+
1060+ function ToolResultContent ( { value } : { value : unknown } ) {
1061+ if ( typeof value === "string" ) {
1062+ return < pre className = "tool-json-block" > { value || "—" } </ pre > ;
1063+ }
1064+ if ( Array . isArray ( value ) ) {
1065+ const objectParts = value . filter ( isRecord ) ;
1066+ if ( objectParts . length === value . length && objectParts . length > 0 ) {
1067+ return < StructuredContentParts parts = { objectParts } fallbackText = "" /> ;
1068+ }
1069+ }
1070+ return < JsonValueBlock label = "Content" value = { value ?? null } /> ;
1071+ }
1072+
1073+ interface VisualQuestion {
1074+ header : string ;
1075+ question : string ;
1076+ multiSelect : boolean ;
1077+ options : VisualQuestionOption [ ] ;
1078+ }
1079+
1080+ interface VisualQuestionOption {
1081+ label : string ;
1082+ description : string ;
1083+ }
1084+
1085+ function AskUserQuestionView ( { questions } : { questions : VisualQuestion [ ] } ) {
1086+ return (
1087+ < div className = "ask-user-questions" >
1088+ { questions . map ( ( q , index ) => (
1089+ < div key = { `${ q . header } -${ index } ` } className = "ask-question-card" >
1090+ < div className = "ask-question-meta" >
1091+ { q . header && < span > { q . header } </ span > }
1092+ < span > { q . multiSelect ? "multi select" : "single select" } </ span >
1093+ </ div >
1094+ < div className = "ask-question-text" > { q . question || "Question" } </ div >
1095+ { q . options . length > 0 && (
1096+ < div className = "ask-options" >
1097+ { q . options . map ( ( option , optionIndex ) => (
1098+ < div key = { `${ option . label } -${ optionIndex } ` } className = "ask-option" >
1099+ < div className = "ask-option-label" > { option . label || `Option ${ optionIndex + 1 } ` } </ div >
1100+ { option . description && (
1101+ < div className = "ask-option-description" > { option . description } </ div >
1102+ ) }
1103+ </ div >
1104+ ) ) }
1105+ </ div >
1106+ ) }
1107+ </ div >
1108+ ) ) }
1109+ </ div >
1110+ ) ;
1111+ }
1112+
1113+ function GenericPart ( { part, type } : { part : Record < string , unknown > ; type : string } ) {
1114+ return (
1115+ < div className = "generic-part" >
1116+ < div className = "part-header" >
1117+ < span className = "part-type-badge" > { type } </ span >
1118+ </ div >
1119+ < JsonValueBlock label = "Payload" value = { part } />
1120+ </ div >
1121+ ) ;
1122+ }
1123+
1124+ function JsonValueBlock ( { label, value } : { label : string ; value : unknown } ) {
1125+ return (
1126+ < div className = "tool-json-wrap" >
1127+ < div className = "tool-json-label" > { label } </ div >
1128+ < pre className = "tool-json-block" > { rawValueText ( value ) } </ pre >
1129+ </ div >
1130+ ) ;
1131+ }
1132+
1133+ function visualContentParts ( turn : Turn ) : Record < string , unknown > [ ] | null {
1134+ const raw = isRecord ( turn . raw ) ? turn . raw : null ;
1135+ const content = raw ?. content ;
1136+ if ( ! Array . isArray ( content ) ) return null ;
1137+ const parts = content . filter ( isRecord ) ;
1138+ if ( parts . length === 0 ) return null ;
1139+ return parts . some ( ( part ) => ! isPlainTextPart ( part ) ) ? parts : null ;
1140+ }
1141+
1142+ function isPlainTextPart ( part : Record < string , unknown > ) : boolean {
1143+ const type = stringRecordValue ( part . type ) ;
1144+ return ( type === "text" || type === "input_text" || type === "output_text" ) && typeof part . text === "string" ;
1145+ }
1146+
1147+ function askUserQuestions ( input : unknown ) : VisualQuestion [ ] | null {
1148+ const obj = isRecord ( input ) ? input : null ;
1149+ const rawQuestions = obj ?. questions ;
1150+ if ( ! Array . isArray ( rawQuestions ) ) return null ;
1151+ const questions = rawQuestions . filter ( isRecord ) . map ( ( q ) : VisualQuestion => {
1152+ const rawOptions = q . options ;
1153+ const options = Array . isArray ( rawOptions )
1154+ ? rawOptions . filter ( isRecord ) . map ( ( option ) => ( {
1155+ label : stringRecordValue ( option . label ) ,
1156+ description : stringRecordValue ( option . description ) ,
1157+ } ) )
1158+ : [ ] ;
1159+ return {
1160+ header : stringRecordValue ( q . header ) ,
1161+ question : stringRecordValue ( q . question ) ,
1162+ multiSelect : q . multiSelect === true || q . multi_select === true ,
1163+ options,
1164+ } ;
1165+ } ) ;
1166+ return questions . length > 0 ? questions : null ;
1167+ }
1168+
1169+ function isRecord ( value : unknown ) : value is Record < string , unknown > {
1170+ return ! ! value && typeof value === "object" && ! Array . isArray ( value ) ;
1171+ }
1172+
1173+ function stringRecordValue ( value : unknown ) : string {
1174+ return typeof value === "string" ? value : "" ;
1175+ }
1176+
9521177function turnRawText ( turn : Turn ) : string | null {
9531178 if ( ! Object . prototype . hasOwnProperty . call ( turn , "raw" ) ) return null ;
9541179 return rawValueText ( turn . raw ) ;
0 commit comments