Render a single message into a list of owned ListItems. Returns owned items plus message-local clickable regions so results can be cached across frames.
(&mut self, message: &ChatMessage, available_width: u16)
| 365 | /// Render a single message into a list of owned ListItems. |
| 366 | /// Returns owned items plus message-local clickable regions so results can be cached across frames. |
| 367 | fn render_message(&mut self, message: &ChatMessage, available_width: u16) -> MessageRenderResult { |
| 368 | let mut items: Vec<ListItem<'static>> = Vec::new(); |
| 369 | let mut plain_lines: Vec<String> = Vec::new(); |
| 370 | let mut tool_regions: Vec<(String, u16, u16)> = Vec::new(); |
| 371 | let mut thinking_regions: Vec<(String, u16, u16)> = Vec::new(); |
| 372 | let mut thinking_block_index: usize = 0; |
| 373 | |
| 374 | // Match opencode's TUI style: no explicit "You:" / "Assistant:" prefixes. |
| 375 | // Instead, differentiate user messages via background color (and a subtle left border). |
| 376 | let user_bg_style = Style::default().bg(self.theme.background_panel); |
| 377 | let user_border_style = self |
| 378 | .theme |
| 379 | .style(StyleKind::Success) |
| 380 | .add_modifier(Modifier::BOLD); |
| 381 | |
| 382 | fn blank_line() -> ListItem<'static> { |
| 383 | ListItem::new(Line::from(Span::raw(String::new()))) |
| 384 | } |
| 385 | |
| 386 | fn user_padding_line(user_bg_style: Style, user_border_style: Style) -> ListItem<'static> { |
| 387 | ListItem::new(Line::from(vec![ |
| 388 | Span::raw(" ".to_string()), |
| 389 | Span::styled("\u{258f}".to_string(), user_border_style), // ▏ |
| 390 | Span::raw(" ".to_string()), |
| 391 | ])) |
| 392 | .style(user_bg_style) |
| 393 | } |
| 394 | |
| 395 | fn close_user_bubble( |
| 396 | items: &mut Vec<ListItem<'static>>, |
| 397 | plain_lines: &mut Vec<String>, |
| 398 | open: &mut bool, |
| 399 | user_bg_style: Style, |
| 400 | user_border_style: Style, |
| 401 | ) { |
| 402 | if *open { |
| 403 | items.push(user_padding_line(user_bg_style, user_border_style)); |
| 404 | plain_lines.push(" | ".to_string()); |
| 405 | *open = false; |
| 406 | } |
| 407 | } |
| 408 | |
| 409 | fn wrap_hard_display_width(s: &str, max_width: usize) -> Vec<String> { |
| 410 | if max_width == 0 { |
| 411 | return vec![String::new()]; |
| 412 | } |
| 413 | if UnicodeWidthStr::width(s) <= max_width { |
| 414 | return vec![s.to_string()]; |
| 415 | } |
| 416 | |
| 417 | let mut lines: Vec<String> = Vec::new(); |
| 418 | let mut current = String::new(); |
| 419 | let mut current_width = 0usize; |
| 420 | |
| 421 | for ch in s.chars() { |
| 422 | let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0); |
| 423 | |
| 424 | if !current.is_empty() && current_width + ch_width > max_width { |