Enhance TUI thinking panel: add dynamic height calculation, implement real-time updates from assistant messages, and refine thinking content rendering logic.
This commit is contained in:
@@ -11,6 +11,9 @@ use crate::chat_app::{ChatApp, InputMode};
|
||||
use owlen_core::types::Role;
|
||||
|
||||
pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
|
||||
// Update thinking content from last message
|
||||
app.update_thinking_from_last_message();
|
||||
|
||||
// Calculate dynamic input height based on textarea content
|
||||
let available_width = frame.area().width;
|
||||
let input_height = if matches!(app.mode(), InputMode::Editing) {
|
||||
@@ -30,20 +33,51 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
|
||||
(visual_lines as u16).min(10) + 2 // +2 for borders
|
||||
};
|
||||
|
||||
// Calculate thinking section height
|
||||
let thinking_height = if let Some(thinking) = app.current_thinking() {
|
||||
let content_width = available_width.saturating_sub(4);
|
||||
let visual_lines = calculate_wrapped_line_count(
|
||||
thinking.lines(),
|
||||
content_width,
|
||||
);
|
||||
(visual_lines as u16).min(6) + 2 // +2 for borders, max 6 lines
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let mut constraints = vec![
|
||||
Constraint::Length(4), // Header
|
||||
Constraint::Min(8), // Messages
|
||||
];
|
||||
|
||||
if thinking_height > 0 {
|
||||
constraints.push(Constraint::Length(thinking_height)); // Thinking
|
||||
}
|
||||
|
||||
constraints.push(Constraint::Length(input_height)); // Input
|
||||
constraints.push(Constraint::Length(3)); // Status
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(4), // Header
|
||||
Constraint::Min(8), // Messages
|
||||
Constraint::Length(input_height), // Input
|
||||
Constraint::Length(3), // Status
|
||||
])
|
||||
.constraints(constraints)
|
||||
.split(frame.area());
|
||||
|
||||
render_header(frame, layout[0], app);
|
||||
render_messages(frame, layout[1], app);
|
||||
render_input(frame, layout[2], app);
|
||||
render_status(frame, layout[3], app);
|
||||
let mut idx = 0;
|
||||
render_header(frame, layout[idx], app);
|
||||
idx += 1;
|
||||
|
||||
render_messages(frame, layout[idx], app);
|
||||
idx += 1;
|
||||
|
||||
if thinking_height > 0 {
|
||||
render_thinking(frame, layout[idx], app);
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
render_input(frame, layout[idx], app);
|
||||
idx += 1;
|
||||
|
||||
render_status(frame, layout[idx], app);
|
||||
|
||||
match app.mode() {
|
||||
InputMode::ProviderSelection => render_provider_selector(frame, app),
|
||||
@@ -420,7 +454,15 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
Role::System => ("⚙️ ", "System: "),
|
||||
};
|
||||
|
||||
let formatted = formatter.format_message(message);
|
||||
// Extract content without thinking tags for assistant messages
|
||||
let content_to_display = if matches!(role, Role::Assistant) {
|
||||
let (content_without_think, _) = formatter.extract_thinking(&message.content);
|
||||
content_without_think
|
||||
} else {
|
||||
message.content.clone()
|
||||
};
|
||||
|
||||
let formatted: Vec<String> = content_to_display.trim().lines().map(|s| s.to_string()).collect();
|
||||
let is_streaming = message
|
||||
.metadata
|
||||
.get("streaming")
|
||||
@@ -532,6 +574,53 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn render_thinking(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
if let Some(thinking) = app.current_thinking().cloned() {
|
||||
let viewport_height = area.height.saturating_sub(2) as usize; // subtract borders
|
||||
let content_width = area.width.saturating_sub(4);
|
||||
|
||||
app.set_thinking_viewport_height(viewport_height);
|
||||
|
||||
let chunks = wrap(&thinking, content_width as usize);
|
||||
|
||||
let lines: Vec<Line> = chunks
|
||||
.into_iter()
|
||||
.map(|seg| {
|
||||
Line::from(Span::styled(
|
||||
seg.into_owned(),
|
||||
Style::default()
|
||||
.fg(Color::DarkGray)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Update AutoScroll state with accurate content length
|
||||
let thinking_scroll = app.thinking_scroll_mut();
|
||||
thinking_scroll.content_len = lines.len();
|
||||
thinking_scroll.on_viewport(viewport_height);
|
||||
|
||||
let scroll_position = app.thinking_scroll_position().min(u16::MAX as usize) as u16;
|
||||
|
||||
let paragraph = Paragraph::new(lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
" 💭 Thinking ",
|
||||
Style::default()
|
||||
.fg(Color::DarkGray)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::DarkGray)),
|
||||
)
|
||||
.scroll((scroll_position, 0))
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_input(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
||||
let title = match app.mode() {
|
||||
InputMode::Editing => " Input (Enter=send · Ctrl+J=newline · Esc=exit input mode) ",
|
||||
|
||||
Reference in New Issue
Block a user