@@ -486,29 +486,18 @@ impl<'a> MinimalSessionView<'a> {
486486
487487 let mut all_lines: Vec < Line < ' static > > = Vec :: new ( ) ;
488488
489- // Determine if we need to insert tool calls before the last assistant message
489+ // Determine what content we have for display
490490 let has_tool_calls = !self . app_state . tool_calls . is_empty ( ) ;
491+ let has_content_segments = !self . app_state . content_segments . is_empty ( ) ;
491492 let last_is_assistant = self
492493 . app_state
493494 . messages
494495 . last ( )
495496 . map ( |m| m. role == cortex_core:: widgets:: MessageRole :: Assistant )
496497 . unwrap_or ( false ) ;
497498
498- // Determine if all tool calls are completed (have results)
499- // This indicates we're in a continuation phase (2nd turn after tool execution)
500- let all_tools_completed = has_tool_calls
501- && self
502- . app_state
503- . tool_calls
504- . iter ( )
505- . all ( |c| c. status == ToolStatus :: Completed || c. status == ToolStatus :: Failed ) ;
506-
507- // Render messages (except the last assistant message if we have content segments)
508- // Content segments will replace the last assistant message rendering when present
509- let has_content_segments = !self . app_state . content_segments . is_empty ( ) ;
510-
511499 // If we have content segments, skip the last assistant message (it's in the segments)
500+ // Content segments replace the last assistant message rendering when present
512501 let messages_to_render = if has_content_segments && last_is_assistant {
513502 // Skip the last message if it's an assistant message and we have segments
514503 let len = self . app_state . messages . len ( ) ;
@@ -532,12 +521,14 @@ impl<'a> MinimalSessionView<'a> {
532521 None
533522 } ;
534523
535- // Check if tool is currently executing (for real-time display)
536- let is_tool_executing = self . app_state . streaming . is_tool_executing ( ) ;
537-
538524 // Render interleaved content using segments timeline
539525 // This properly interleaves text and tool calls in arrival order
540526 // IMPORTANT: Use content_segments even during streaming to preserve correct order
527+ //
528+ // The ordering logic is simple:
529+ // 1. If we have content_segments, use them (sorted by sequence) - this ensures correct order
530+ // 2. Only the pending_text_segment (not yet committed) gets the streaming cursor
531+ // 3. Finalized text segments use render_text_content (no cursor)
541532 if has_content_segments {
542533 // Sort segments by sequence to ensure correct order
543534 let mut sorted_segments: Vec < _ > = self . app_state . content_segments . iter ( ) . collect ( ) ;
@@ -546,8 +537,8 @@ impl<'a> MinimalSessionView<'a> {
546537 for segment in sorted_segments {
547538 match segment {
548539 ContentSegment :: Text { content, .. } => {
549- // Render text segment as streaming content
550- all_lines. extend ( self . render_streaming_content ( content, area. width ) ) ;
540+ // Render finalized text segment (no cursor)
541+ all_lines. extend ( self . render_text_content ( content, area. width ) ) ;
551542 }
552543 ContentSegment :: ToolCall { tool_call_id, .. } => {
553544 // Find and render the corresponding tool call
@@ -564,40 +555,34 @@ impl<'a> MinimalSessionView<'a> {
564555 }
565556
566557 // If still streaming, also render the pending text that hasn't been segmented yet
567- // This is the text that arrived after the last tool call
558+ // This is the text that arrived after the last tool call - show with cursor
568559 if self . app_state . streaming . is_streaming {
569560 let pending_text = & self . app_state . pending_text_segment ;
570561 if !pending_text. is_empty ( ) {
571562 all_lines. extend ( self . render_streaming_content ( pending_text, area. width ) ) ;
572563 }
573564 }
574565 } else if has_tool_calls {
575- // No content segments yet but have tool calls - means we're in the middle of first response
576- // ALWAYS render tool calls when they exist (for real-time display)
566+ // No content segments but have tool calls
567+ // This is a fallback case that shouldn't normally happen since add_tool_call()
568+ // always creates content segments. But handle it gracefully.
569+ //
570+ // Use consistent ordering: always sort tool calls by sequence (arrival order)
571+ // and render them after any streaming content
577572 let mut sorted_calls: Vec < _ > = self . app_state . tool_calls . iter ( ) . collect ( ) ;
578573 sorted_calls. sort_by_key ( |c| c. sequence ) ;
579574
580- if all_tools_completed {
581- // 2nd turn: tools first (sorted by arrival), then streaming response
582- for call in & sorted_calls {
583- all_lines. extend ( self . render_tool_call ( call) ) ;
584- }
585- // Then streaming (the response after tool execution)
586- if let Some ( ref content) = streaming_content {
587- all_lines. extend ( self . render_streaming_content ( content, area. width ) ) ;
588- }
589- } else {
590- // 1st turn or during execution: streaming first, then tools
591- if let Some ( ref content) = streaming_content {
592- all_lines. extend ( self . render_streaming_content ( content, area. width ) ) ;
593- }
594- // Always show tool calls (including those being executed)
595- for call in & sorted_calls {
596- all_lines. extend ( self . render_tool_call ( call) ) ;
597- }
575+ // Render any streaming content first (with cursor)
576+ if let Some ( ref content) = streaming_content {
577+ all_lines. extend ( self . render_streaming_content ( content, area. width ) ) ;
578+ }
579+
580+ // Then render tool calls in arrival order
581+ for call in & sorted_calls {
582+ all_lines. extend ( self . render_tool_call ( call) ) ;
598583 }
599584 } else if let Some ( ref content) = streaming_content {
600- // No tool calls - just render streaming
585+ // No tool calls - just render streaming content with cursor
601586 all_lines. extend ( self . render_streaming_content ( content, area. width ) ) ;
602587 }
603588
@@ -851,7 +836,21 @@ impl<'a> MinimalSessionView<'a> {
851836 . render ( area, buf, & mut scrollbar_state) ;
852837 }
853838
839+ /// Renders finalized text content (without streaming cursor).
840+ /// Used for text segments that are already committed in content_segments.
841+ fn render_text_content ( & self , content : & str , width : u16 ) -> Vec < Line < ' static > > {
842+ use cortex_core:: markdown:: MarkdownRenderer ;
843+
844+ let content_width = width. saturating_sub ( 4 ) as u16 ;
845+ let renderer = MarkdownRenderer :: new ( ) . with_width ( content_width) ;
846+ let rendered_lines = renderer. render ( content) ;
847+
848+ // No cursor for finalized content
849+ rendered_lines
850+ }
851+
854852 /// Renders streaming content with cursor.
853+ /// Used only for actively streaming content (pending_text_segment).
855854 fn render_streaming_content ( & self , content : & str , width : u16 ) -> Vec < Line < ' static > > {
856855 use cortex_core:: markdown:: MarkdownRenderer ;
857856
0 commit comments