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:
2025-09-30 01:07:00 +02:00
parent 004fc0ba5e
commit 8409bf646a
3 changed files with 194 additions and 13 deletions

View File

@@ -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) ",