(
message:
| Message
| TombstoneMessage
| StreamEvent
| RequestStartEvent
| ToolUseSummaryMessage,
onMessage: (message: Message) => void,
onUpdateLength: (newContent: string) => void,
onSetStreamMode: (mode: SpinnerMode) => void,
onStreamingToolUses: (
f: (streamingToolUse: StreamingToolUse[]) => StreamingToolUse[],
) => void,
onTombstone?: (message: Message) => void,
onStreamingThinking?: (
f: (current: StreamingThinking | null) => StreamingThinking | null,
) => void,
onApiMetrics?: (metrics: { ttftMs: number }) => void,
onStreamingText?: (f: (current: string | null) => string | null) => void,
)
| 3283 | * Handles messages from a stream, updating response length for deltas and appending completed messages |
| 3284 | */ |
| 3285 | export function handleMessageFromStream( |
| 3286 | message: |
| 3287 | | Message |
| 3288 | | TombstoneMessage |
| 3289 | | StreamEvent |
| 3290 | | RequestStartEvent |
| 3291 | | ToolUseSummaryMessage, |
| 3292 | onMessage: (message: Message) => void, |
| 3293 | onUpdateLength: (newContent: string) => void, |
| 3294 | onSetStreamMode: (mode: SpinnerMode) => void, |
| 3295 | onStreamingToolUses: ( |
| 3296 | f: (streamingToolUse: StreamingToolUse[]) => StreamingToolUse[], |
| 3297 | ) => void, |
| 3298 | onTombstone?: (message: Message) => void, |
| 3299 | onStreamingThinking?: ( |
| 3300 | f: (current: StreamingThinking | null) => StreamingThinking | null, |
| 3301 | ) => void, |
| 3302 | onApiMetrics?: (metrics: { ttftMs: number }) => void, |
| 3303 | onStreamingText?: (f: (current: string | null) => string | null) => void, |
| 3304 | ): void { |
| 3305 | if ( |
| 3306 | message.type !== 'stream_event' && |
| 3307 | message.type !== 'stream_request_start' |
| 3308 | ) { |
| 3309 | // Handle tombstone messages - remove the targeted message instead of adding |
| 3310 | if (message.type === 'tombstone') { |
| 3311 | onTombstone?.(message.message as unknown as Message) |
| 3312 | return |
| 3313 | } |
| 3314 | // Tool use summary messages are SDK-only, ignore them in stream handling |
| 3315 | if (message.type === 'tool_use_summary') { |
| 3316 | return |
| 3317 | } |
| 3318 | // Capture complete thinking blocks for real-time display in transcript mode |
| 3319 | if (message.type === 'assistant') { |
| 3320 | const assistMsg = message as Message |
| 3321 | const contentArr = Array.isArray(assistMsg.message?.content) |
| 3322 | ? assistMsg.message.content |
| 3323 | : [] |
| 3324 | const thinkingBlock = contentArr.find( |
| 3325 | block => typeof block !== 'string' && block.type === 'thinking', |
| 3326 | ) |
| 3327 | if ( |
| 3328 | thinkingBlock && |
| 3329 | typeof thinkingBlock !== 'string' && |
| 3330 | thinkingBlock.type === 'thinking' |
| 3331 | ) { |
| 3332 | const tb = thinkingBlock as ThinkingBlock |
| 3333 | onStreamingThinking?.(() => ({ |
| 3334 | thinking: tb.thinking, |
| 3335 | isStreaming: false, |
| 3336 | streamingEndedAt: Date.now(), |
| 3337 | })) |
| 3338 | } |
| 3339 | } |
| 3340 | // Clear streaming text NOW so the render can switch displayedMessages |
| 3341 | // from deferredMessages to messages in the same batch, making the |
| 3342 | // transition from streaming text → final message atomic (no gap, no duplication). |
no test coverage detected