Skip to content

Commit e9d4617

Browse files
committed
fix(cortex-tui): fix content ordering and streaming cursor display
- Add render_text_content() for finalized text segments (no cursor) - Use render_text_content() for content_segments Text entries - Keep render_streaming_content() with cursor only for pending_text_segment - Remove inconsistent ordering logic that switched based on all_tools_completed - Use consistent ordering: streaming content first, then tool calls by sequence This fixes visual 'refreshing' issues where content would reorder when tool calls completed or streaming state changed.
1 parent 1358370 commit e9d4617

1 file changed

Lines changed: 40 additions & 41 deletions

File tree

cortex-tui/src/views/minimal_session.rs

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)