@@ -140,6 +140,20 @@ function mapToolResult(msg: SDKUserMessage): ContentBlock[] {
140140 return blocks ;
141141}
142142
143+ // User messages from the Claude SDK carry either tool_result blocks (assistant
144+ // attribution) or plain text (real human input — initial prompt or chat reply).
145+ // Extract the latter so history can surface it as a role:user message.
146+ function extractUserText ( msg : SDKUserMessage ) : string {
147+ const content = msg . message . content ;
148+ if ( typeof content === "string" ) return content . trim ( ) ;
149+ if ( ! Array . isArray ( content ) ) return "" ;
150+ return content
151+ . filter ( ( b ) : b is { type : "text" ; text : string } => b . type === "text" && typeof b . text === "string" )
152+ . map ( ( b ) => b . text )
153+ . join ( "\n" )
154+ . trim ( ) ;
155+ }
156+
143157/**
144158 * Map SDK system task_* messages → our subtask.* events.
145159 * SDK subtypes: task_started | task_progress | task_notification.
@@ -173,8 +187,16 @@ function mapTaskSystemMessage(msg: any): AgentEvent | null {
173187 }
174188}
175189
176- /** Map a single SDK message to an AgentEvent (1:1, used for history fallback). */
177- export function mapSDKMessage ( msg : SDKMessage ) : AgentEvent | null {
190+ /**
191+ * Map a single SDK message to a list of AgentEvents (0..N).
192+ *
193+ * Most SDK message types produce a single event, but `user` messages can
194+ * contain both plain text (human input) and tool_result blocks (assistant
195+ * attribution). Those must emit two events in order — user text first, then
196+ * the tool_result-bearing message — so the UI can render human turn before
197+ * the tool output that follows it.
198+ */
199+ export function mapSDKMessage ( msg : SDKMessage ) : AgentEvent [ ] {
178200 switch ( msg . type ) {
179201 case "rate_limit_event" : {
180202 const info = msg . rate_limit_info ;
@@ -186,24 +208,26 @@ export function mapSDKMessage(msg: SDKMessage): AgentEvent | null {
186208 resetAt : info . overageResetsAt ? new Date ( info . overageResetsAt * 1000 ) . toISOString ( ) : undefined ,
187209 }
188210 : undefined ;
189- return {
190- type : "turn.rate_limit" ,
191- status : info . status ,
192- resetAt,
193- rateLimitType : info . rateLimitType ,
194- isUsingOverage : info . isUsingOverage ,
195- overage,
196- } ;
211+ return [
212+ {
213+ type : "turn.rate_limit" ,
214+ status : info . status ,
215+ resetAt,
216+ rateLimitType : info . rateLimitType ,
217+ isUsingOverage : info . isUsingOverage ,
218+ overage,
219+ } ,
220+ ] ;
197221 }
198222 if ( info . status === "allowed_warning" ) {
199223 logger . warn ( `Rate limit warning: ${ info . rateLimitType } at ${ ( ( info . utilization ?? 0 ) * 100 ) . toFixed ( 0 ) } %` ) ;
200224 }
201- return null ;
225+ return [ ] ;
202226 }
203227
204228 case "assistant" : {
205229 if ( msg . error === "rate_limit" ) {
206- return { type : "turn.rate_limit" , status : "rejected" , resetAt : new Date ( Date . now ( ) + 60 * 60 * 1000 ) . toISOString ( ) } ;
230+ return [ { type : "turn.rate_limit" , status : "rejected" , resetAt : new Date ( Date . now ( ) + 60 * 60 * 1000 ) . toISOString ( ) } ] ;
207231 }
208232 if ( msg . error ) {
209233 const contentText = ( msg . message ?. content ?? [ ] )
@@ -212,28 +236,36 @@ export function mapSDKMessage(msg: SDKMessage): AgentEvent | null {
212236 . join ( " " )
213237 . slice ( 0 , 500 ) ;
214238 const detail = contentText || msg . error ;
215- return { type : "turn.error" , code : msg . error , detail } ;
239+ return [ { type : "turn.error" , code : msg . error , detail } ] ;
216240 }
217241 const parentId = msg . parent_tool_use_id ;
218242 const blocks = ( msg . message . content ?? [ ] ) . map ( ( b ) => mapContentBlock ( b , parentId ) ) . filter ( ( b ) : b is ContentBlock => b !== null ) ;
219- return blocks . length > 0 ? { type : "message" , blocks } : null ;
243+ return blocks . length > 0 ? [ { type : "message" , blocks } ] : [ ] ;
220244 }
221245
222246 case "user" : {
247+ // Order matters: the human text precedes any tool_result blocks from
248+ // the same SDK message so consumers render the user turn first.
249+ const events : AgentEvent [ ] = [ ] ;
250+ const userText = extractUserText ( msg ) ;
251+ if ( userText ) events . push ( { type : "message.user" , text : userText } ) ;
223252 const blocks = mapToolResult ( msg ) ;
224- return blocks . length > 0 ? { type : "message" , blocks } : null ;
253+ if ( blocks . length > 0 ) events . push ( { type : "message" , blocks } ) ;
254+ return events ;
225255 }
226256
227257 case "result" : {
228258 const text = msg . subtype === "success" ? msg . result : undefined ;
229- return { type : "turn.end" , text, cost : msg . total_cost_usd || 0 , usage : msg . usage as Record < string , any > } ;
259+ return [ { type : "turn.end" , text, cost : msg . total_cost_usd || 0 , usage : msg . usage as Record < string , any > } ] ;
230260 }
231261
232- case "system" :
233- return mapTaskSystemMessage ( msg ) ;
262+ case "system" : {
263+ const event = mapTaskSystemMessage ( msg ) ;
264+ return event ? [ event ] : [ ] ;
265+ }
234266
235267 default :
236- return null ;
268+ return [ ] ;
237269 }
238270}
239271
@@ -295,8 +327,7 @@ export function* mapSDKMessageStream(msg: SDKMessage, turnOpen: { value: boolean
295327 if ( assistantMsg . error === "rate_limit" && rateLimitSeen ?. value ) {
296328 return ;
297329 }
298- const event = mapSDKMessage ( msg ) ;
299- if ( event ) yield event ;
330+ yield * mapSDKMessage ( msg ) ;
300331 return ;
301332 }
302333
@@ -335,8 +366,7 @@ export function* mapSDKMessageStream(msg: SDKMessage, turnOpen: { value: boolean
335366 }
336367
337368 // Everything else (rate_limit_event, etc.) — delegate
338- const event = mapSDKMessage ( msg ) ;
339- if ( event ) {
369+ for ( const event of mapSDKMessage ( msg ) ) {
340370 if ( event . type === "turn.rate_limit" && rateLimitSeen !== undefined ) {
341371 rateLimitSeen . value = true ;
342372 }
@@ -445,9 +475,15 @@ export const claudeProvider: AgentProvider = {
445475 let counter = 0 ;
446476 for ( const msg of messages ) {
447477 const sdkLike = { ...msg , message : msg . message } as unknown as SDKMessage ;
448- const event = mapSDKMessage ( sdkLike ) ;
449- if ( event ) {
450- events . push ( { id : msg . uuid || `claude-hist-${ ++ counter } ` , event, timestamp : new Date ( ) . toISOString ( ) } ) ;
478+ const baseId = msg . uuid || `claude-hist-${ ++ counter } ` ;
479+ const timestamp = new Date ( ) . toISOString ( ) ;
480+ // User SDK messages can expand into two events (message.user + message
481+ // with tool_result). Suffix their ids so both are stable even when only
482+ // one of the two is emitted.
483+ const isUser = sdkLike . type === "user" ;
484+ for ( const event of mapSDKMessage ( sdkLike ) ) {
485+ const id = isUser ? `${ baseId } -${ event . type === "message.user" ? "user" : "tool" } ` : baseId ;
486+ events . push ( { id, event, timestamp } ) ;
451487 }
452488 }
453489 return events ;
0 commit comments