(
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,
)
| 2928 | * Handles messages from a stream, updating response length for deltas and appending completed messages |
| 2929 | */ |
| 2930 | export function handleMessageFromStream( |
| 2931 | message: |
| 2932 | | Message |
| 2933 | | TombstoneMessage |
| 2934 | | StreamEvent |
| 2935 | | RequestStartEvent |
| 2936 | | ToolUseSummaryMessage, |
| 2937 | onMessage: (message: Message) => void, |
| 2938 | onUpdateLength: (newContent: string) => void, |
| 2939 | onSetStreamMode: (mode: SpinnerMode) => void, |
| 2940 | onStreamingToolUses: ( |
| 2941 | f: (streamingToolUse: StreamingToolUse[]) => StreamingToolUse[], |
| 2942 | ) => void, |
| 2943 | onTombstone?: (message: Message) => void, |
| 2944 | onStreamingThinking?: ( |
| 2945 | f: (current: StreamingThinking | null) => StreamingThinking | null, |
| 2946 | ) => void, |
| 2947 | onApiMetrics?: (metrics: { ttftMs: number }) => void, |
| 2948 | onStreamingText?: (f: (current: string | null) => string | null) => void, |
| 2949 | ): void { |
| 2950 | if ( |
| 2951 | message.type !== 'stream_event' && |
| 2952 | message.type !== 'stream_request_start' |
| 2953 | ) { |
| 2954 | // Handle tombstone messages - remove the targeted message instead of adding |
| 2955 | if (message.type === 'tombstone') { |
| 2956 | onTombstone?.(message.message) |
| 2957 | return |
| 2958 | } |
| 2959 | // Tool use summary messages are SDK-only, ignore them in stream handling |
| 2960 | if (message.type === 'tool_use_summary') { |
| 2961 | return |
| 2962 | } |
| 2963 | // Capture complete thinking blocks for real-time display in transcript mode |
| 2964 | if (message.type === 'assistant') { |
| 2965 | const thinkingBlock = message.message.content.find( |
| 2966 | block => block.type === 'thinking', |
| 2967 | ) |
| 2968 | if (thinkingBlock && thinkingBlock.type === 'thinking') { |
| 2969 | onStreamingThinking?.(() => ({ |
| 2970 | thinking: thinkingBlock.thinking, |
| 2971 | isStreaming: false, |
| 2972 | streamingEndedAt: Date.now(), |
| 2973 | })) |
| 2974 | } |
| 2975 | } |
| 2976 | // Clear streaming text NOW so the render can switch displayedMessages |
| 2977 | // from deferredMessages to messages in the same batch, making the |
| 2978 | // transition from streaming text → final message atomic (no gap, no duplication). |
| 2979 | onStreamingText?.(() => null) |
| 2980 | onMessage(message) |
| 2981 | return |
| 2982 | } |
| 2983 | |
| 2984 | if (message.type === 'stream_request_start') { |
| 2985 | onSetStreamMode('requesting') |
| 2986 | return |
| 2987 | } |
no test coverage detected