Refactor codebase: improve formatting consistency, simplify message rendering, and optimize cursor and visual selection handling logic across panels.
This commit is contained in:
@@ -11,7 +11,7 @@ use tokio::sync::mpsc;
|
|||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{DisableMouseCapture, EnableMouseCapture, DisableBracketedPaste, EnableBracketedPaste},
|
event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
@@ -57,7 +57,12 @@ async fn main() -> Result<()> {
|
|||||||
// Terminal setup
|
// Terminal setup
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste)?;
|
execute!(
|
||||||
|
stdout,
|
||||||
|
EnterAlternateScreen,
|
||||||
|
EnableMouseCapture,
|
||||||
|
EnableBracketedPaste
|
||||||
|
)?;
|
||||||
let backend = CrosstermBackend::new(stdout);
|
let backend = CrosstermBackend::new(stdout);
|
||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ impl MessageFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_message(&self, message: &Message) -> Vec<String> {
|
pub fn format_message(&self, message: &Message) -> Vec<String> {
|
||||||
message.content.trim().lines().map(|s| s.to_string()).collect()
|
message
|
||||||
|
.content
|
||||||
|
.trim()
|
||||||
|
.lines()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract thinking content from <think> tags, returning (content_without_think, thinking_content)
|
/// Extract thinking content from <think> tags, returning (content_without_think, thinking_content)
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ impl SessionController {
|
|||||||
|
|
||||||
self.conversation.push_user_message(content);
|
self.conversation.push_user_message(content);
|
||||||
|
|
||||||
self.send_request_with_current_conversation(parameters).await
|
self.send_request_with_current_conversation(parameters)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a request using the current conversation without adding a new user message
|
/// Send a request using the current conversation without adding a new user message
|
||||||
|
|||||||
@@ -422,4 +422,4 @@ mod tests {
|
|||||||
assert_eq!(find_prev_word_boundary(line, 11), Some(6));
|
assert_eq!(find_prev_word_boundary(line, 11), Some(6));
|
||||||
assert_eq!(find_prev_word_boundary(line, 6), Some(0));
|
assert_eq!(find_prev_word_boundary(line, 6), Some(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,10 +51,10 @@ pub struct ChatApp {
|
|||||||
clipboard: String, // Vim-style clipboard for yank/paste
|
clipboard: String, // Vim-style clipboard for yank/paste
|
||||||
command_buffer: String, // Buffer for command mode input
|
command_buffer: String, // Buffer for command mode input
|
||||||
visual_start: Option<(usize, usize)>, // Visual mode selection start (row, col) for Input panel
|
visual_start: Option<(usize, usize)>, // Visual mode selection start (row, col) for Input panel
|
||||||
visual_end: Option<(usize, usize)>, // Visual mode selection end (row, col) for scrollable panels
|
visual_end: Option<(usize, usize)>, // Visual mode selection end (row, col) for scrollable panels
|
||||||
focused_panel: FocusedPanel, // Currently focused panel for scrolling
|
focused_panel: FocusedPanel, // Currently focused panel for scrolling
|
||||||
chat_cursor: (usize, usize), // Cursor position in Chat panel (row, col)
|
chat_cursor: (usize, usize), // Cursor position in Chat panel (row, col)
|
||||||
thinking_cursor: (usize, usize), // Cursor position in Thinking panel (row, col)
|
thinking_cursor: (usize, usize), // Cursor position in Thinking panel (row, col)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatApp {
|
impl ChatApp {
|
||||||
@@ -364,9 +364,7 @@ impl ChatApp {
|
|||||||
self.sync_buffer_to_textarea();
|
self.sync_buffer_to_textarea();
|
||||||
// Set a visible selection style
|
// Set a visible selection style
|
||||||
self.textarea.set_selection_style(
|
self.textarea.set_selection_style(
|
||||||
Style::default()
|
Style::default().bg(Color::LightBlue).fg(Color::Black),
|
||||||
.bg(Color::LightBlue)
|
|
||||||
.fg(Color::Black)
|
|
||||||
);
|
);
|
||||||
// Start visual selection at current cursor position
|
// Start visual selection at current cursor position
|
||||||
self.textarea.start_selection();
|
self.textarea.start_selection();
|
||||||
@@ -374,7 +372,8 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
||||||
// For scrollable panels, start selection at cursor position
|
// For scrollable panels, start selection at cursor position
|
||||||
let cursor = if matches!(self.focused_panel, FocusedPanel::Chat) {
|
let cursor = if matches!(self.focused_panel, FocusedPanel::Chat)
|
||||||
|
{
|
||||||
self.chat_cursor
|
self.chat_cursor
|
||||||
} else {
|
} else {
|
||||||
self.thinking_cursor
|
self.thinking_cursor
|
||||||
@@ -463,7 +462,8 @@ impl ChatApp {
|
|||||||
if self.chat_cursor.0 + 1 < max_lines {
|
if self.chat_cursor.0 + 1 < max_lines {
|
||||||
self.chat_cursor.0 += 1;
|
self.chat_cursor.0 += 1;
|
||||||
// Scroll if cursor moves below viewport
|
// Scroll if cursor moves below viewport
|
||||||
let viewport_bottom = self.auto_scroll.scroll + self.viewport_height;
|
let viewport_bottom =
|
||||||
|
self.auto_scroll.scroll + self.viewport_height;
|
||||||
if self.chat_cursor.0 >= viewport_bottom {
|
if self.chat_cursor.0 >= viewport_bottom {
|
||||||
self.on_scroll(1);
|
self.on_scroll(1);
|
||||||
}
|
}
|
||||||
@@ -473,7 +473,8 @@ impl ChatApp {
|
|||||||
let max_lines = self.thinking_scroll.content_len;
|
let max_lines = self.thinking_scroll.content_len;
|
||||||
if self.thinking_cursor.0 + 1 < max_lines {
|
if self.thinking_cursor.0 + 1 < max_lines {
|
||||||
self.thinking_cursor.0 += 1;
|
self.thinking_cursor.0 += 1;
|
||||||
let viewport_bottom = self.thinking_scroll.scroll + self.thinking_viewport_height;
|
let viewport_bottom = self.thinking_scroll.scroll
|
||||||
|
+ self.thinking_viewport_height;
|
||||||
if self.thinking_cursor.0 >= viewport_bottom {
|
if self.thinking_cursor.0 >= viewport_bottom {
|
||||||
self.on_scroll(1);
|
self.on_scroll(1);
|
||||||
}
|
}
|
||||||
@@ -486,133 +487,135 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
// Horizontal cursor movement
|
// Horizontal cursor movement
|
||||||
(KeyCode::Left, KeyModifiers::NONE)
|
(KeyCode::Left, KeyModifiers::NONE)
|
||||||
| (KeyCode::Char('h'), KeyModifiers::NONE) => {
|
| (KeyCode::Char('h'), KeyModifiers::NONE) => match self.focused_panel {
|
||||||
match self.focused_panel {
|
FocusedPanel::Chat => {
|
||||||
FocusedPanel::Chat => {
|
if self.chat_cursor.1 > 0 {
|
||||||
if self.chat_cursor.1 > 0 {
|
self.chat_cursor.1 -= 1;
|
||||||
self.chat_cursor.1 -= 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
|
||||||
if self.thinking_cursor.1 > 0 {
|
|
||||||
self.thinking_cursor.1 -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
FocusedPanel::Thinking => {
|
||||||
|
if self.thinking_cursor.1 > 0 {
|
||||||
|
self.thinking_cursor.1 -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
(KeyCode::Right, KeyModifiers::NONE)
|
(KeyCode::Right, KeyModifiers::NONE)
|
||||||
| (KeyCode::Char('l'), KeyModifiers::NONE) => {
|
| (KeyCode::Char('l'), KeyModifiers::NONE) => match self.focused_panel {
|
||||||
match self.focused_panel {
|
FocusedPanel::Chat => {
|
||||||
FocusedPanel::Chat => {
|
if let Some(line) = self.get_line_at_row(self.chat_cursor.0) {
|
||||||
if let Some(line) = self.get_line_at_row(self.chat_cursor.0) {
|
let max_col = line.chars().count();
|
||||||
let max_col = line.chars().count();
|
if self.chat_cursor.1 < max_col {
|
||||||
if self.chat_cursor.1 < max_col {
|
self.chat_cursor.1 += 1;
|
||||||
self.chat_cursor.1 += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
|
||||||
if let Some(line) = self.get_line_at_row(self.thinking_cursor.0) {
|
|
||||||
let max_col = line.chars().count();
|
|
||||||
if self.thinking_cursor.1 < max_col {
|
|
||||||
self.thinking_cursor.1 += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
FocusedPanel::Thinking => {
|
||||||
|
if let Some(line) = self.get_line_at_row(self.thinking_cursor.0) {
|
||||||
|
let max_col = line.chars().count();
|
||||||
|
if self.thinking_cursor.1 < max_col {
|
||||||
|
self.thinking_cursor.1 += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
// Word movement
|
// Word movement
|
||||||
(KeyCode::Char('w'), KeyModifiers::NONE) => {
|
(KeyCode::Char('w'), KeyModifiers::NONE) => match self.focused_panel {
|
||||||
match self.focused_panel {
|
FocusedPanel::Chat => {
|
||||||
FocusedPanel::Chat => {
|
if let Some(new_col) = self
|
||||||
if let Some(new_col) = self.find_next_word_boundary(self.chat_cursor.0, self.chat_cursor.1) {
|
.find_next_word_boundary(self.chat_cursor.0, self.chat_cursor.1)
|
||||||
self.chat_cursor.1 = new_col;
|
{
|
||||||
}
|
self.chat_cursor.1 = new_col;
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
|
||||||
if let Some(new_col) = self.find_next_word_boundary(self.thinking_cursor.0, self.thinking_cursor.1) {
|
|
||||||
self.thinking_cursor.1 = new_col;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
FocusedPanel::Thinking => {
|
||||||
(KeyCode::Char('e'), KeyModifiers::NONE) => {
|
if let Some(new_col) = self.find_next_word_boundary(
|
||||||
match self.focused_panel {
|
self.thinking_cursor.0,
|
||||||
FocusedPanel::Chat => {
|
self.thinking_cursor.1,
|
||||||
if let Some(new_col) = self.find_word_end(self.chat_cursor.0, self.chat_cursor.1) {
|
) {
|
||||||
self.chat_cursor.1 = new_col;
|
self.thinking_cursor.1 = new_col;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
|
||||||
if let Some(new_col) = self.find_word_end(self.thinking_cursor.0, self.thinking_cursor.1) {
|
|
||||||
self.thinking_cursor.1 = new_col;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
_ => {}
|
||||||
(KeyCode::Char('b'), KeyModifiers::NONE) => {
|
},
|
||||||
match self.focused_panel {
|
(KeyCode::Char('e'), KeyModifiers::NONE) => match self.focused_panel {
|
||||||
FocusedPanel::Chat => {
|
FocusedPanel::Chat => {
|
||||||
if let Some(new_col) = self.find_prev_word_boundary(self.chat_cursor.0, self.chat_cursor.1) {
|
if let Some(new_col) =
|
||||||
self.chat_cursor.1 = new_col;
|
self.find_word_end(self.chat_cursor.0, self.chat_cursor.1)
|
||||||
}
|
{
|
||||||
|
self.chat_cursor.1 = new_col;
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
|
||||||
if let Some(new_col) = self.find_prev_word_boundary(self.thinking_cursor.0, self.thinking_cursor.1) {
|
|
||||||
self.thinking_cursor.1 = new_col;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
FocusedPanel::Thinking => {
|
||||||
(KeyCode::Char('^'), KeyModifiers::SHIFT) => {
|
if let Some(new_col) = self
|
||||||
match self.focused_panel {
|
.find_word_end(self.thinking_cursor.0, self.thinking_cursor.1)
|
||||||
FocusedPanel::Chat => {
|
{
|
||||||
if let Some(line) = self.get_line_at_row(self.chat_cursor.0) {
|
self.thinking_cursor.1 = new_col;
|
||||||
let first_non_blank = line.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
|
|
||||||
self.chat_cursor.1 = first_non_blank;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
|
||||||
if let Some(line) = self.get_line_at_row(self.thinking_cursor.0) {
|
|
||||||
let first_non_blank = line.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
|
|
||||||
self.thinking_cursor.1 = first_non_blank;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
_ => {}
|
||||||
|
},
|
||||||
|
(KeyCode::Char('b'), KeyModifiers::NONE) => match self.focused_panel {
|
||||||
|
FocusedPanel::Chat => {
|
||||||
|
if let Some(new_col) = self
|
||||||
|
.find_prev_word_boundary(self.chat_cursor.0, self.chat_cursor.1)
|
||||||
|
{
|
||||||
|
self.chat_cursor.1 = new_col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FocusedPanel::Thinking => {
|
||||||
|
if let Some(new_col) = self.find_prev_word_boundary(
|
||||||
|
self.thinking_cursor.0,
|
||||||
|
self.thinking_cursor.1,
|
||||||
|
) {
|
||||||
|
self.thinking_cursor.1 = new_col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
(KeyCode::Char('^'), KeyModifiers::SHIFT) => match self.focused_panel {
|
||||||
|
FocusedPanel::Chat => {
|
||||||
|
if let Some(line) = self.get_line_at_row(self.chat_cursor.0) {
|
||||||
|
let first_non_blank =
|
||||||
|
line.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
|
||||||
|
self.chat_cursor.1 = first_non_blank;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FocusedPanel::Thinking => {
|
||||||
|
if let Some(line) = self.get_line_at_row(self.thinking_cursor.0) {
|
||||||
|
let first_non_blank =
|
||||||
|
line.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
|
||||||
|
self.thinking_cursor.1 = first_non_blank;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
// Line start/end navigation
|
// Line start/end navigation
|
||||||
(KeyCode::Char('0'), KeyModifiers::NONE) | (KeyCode::Home, KeyModifiers::NONE) => {
|
(KeyCode::Char('0'), KeyModifiers::NONE)
|
||||||
match self.focused_panel {
|
| (KeyCode::Home, KeyModifiers::NONE) => match self.focused_panel {
|
||||||
FocusedPanel::Chat => {
|
FocusedPanel::Chat => {
|
||||||
self.chat_cursor.1 = 0;
|
self.chat_cursor.1 = 0;
|
||||||
}
|
|
||||||
FocusedPanel::Thinking => {
|
|
||||||
self.thinking_cursor.1 = 0;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
FocusedPanel::Thinking => {
|
||||||
(KeyCode::Char('$'), KeyModifiers::NONE) | (KeyCode::End, KeyModifiers::NONE) => {
|
self.thinking_cursor.1 = 0;
|
||||||
match self.focused_panel {
|
|
||||||
FocusedPanel::Chat => {
|
|
||||||
if let Some(line) = self.get_line_at_row(self.chat_cursor.0) {
|
|
||||||
self.chat_cursor.1 = line.chars().count();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FocusedPanel::Thinking => {
|
|
||||||
if let Some(line) = self.get_line_at_row(self.thinking_cursor.0) {
|
|
||||||
self.thinking_cursor.1 = line.chars().count();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
_ => {}
|
||||||
|
},
|
||||||
|
(KeyCode::Char('$'), KeyModifiers::NONE)
|
||||||
|
| (KeyCode::End, KeyModifiers::NONE) => match self.focused_panel {
|
||||||
|
FocusedPanel::Chat => {
|
||||||
|
if let Some(line) = self.get_line_at_row(self.chat_cursor.0) {
|
||||||
|
self.chat_cursor.1 = line.chars().count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FocusedPanel::Thinking => {
|
||||||
|
if let Some(line) = self.get_line_at_row(self.thinking_cursor.0) {
|
||||||
|
self.thinking_cursor.1 = line.chars().count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
// Half-page scrolling
|
// Half-page scrolling
|
||||||
(KeyCode::Char('d'), KeyModifiers::CONTROL) => {
|
(KeyCode::Char('d'), KeyModifiers::CONTROL) => {
|
||||||
self.scroll_half_page_down();
|
self.scroll_half_page_down();
|
||||||
@@ -647,7 +650,8 @@ impl ChatApp {
|
|||||||
if !self.clipboard.is_empty() {
|
if !self.clipboard.is_empty() {
|
||||||
// Always paste into Input panel
|
// Always paste into Input panel
|
||||||
let current_lines = self.textarea.lines().to_vec();
|
let current_lines = self.textarea.lines().to_vec();
|
||||||
let clipboard_lines: Vec<String> = self.clipboard.lines().map(|s| s.to_string()).collect();
|
let clipboard_lines: Vec<String> =
|
||||||
|
self.clipboard.lines().map(|s| s.to_string()).collect();
|
||||||
|
|
||||||
// Append clipboard content to current input
|
// Append clipboard content to current input
|
||||||
let mut new_lines = current_lines;
|
let mut new_lines = current_lines;
|
||||||
@@ -692,7 +696,7 @@ impl ChatApp {
|
|||||||
self.pending_key = None;
|
self.pending_key = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
InputMode::Editing => match (key.code, key.modifiers) {
|
InputMode::Editing => match (key.code, key.modifiers) {
|
||||||
(KeyCode::Esc, KeyModifiers::NONE) => {
|
(KeyCode::Esc, KeyModifiers::NONE) => {
|
||||||
// Sync textarea content to input buffer before leaving edit mode
|
// Sync textarea content to input buffer before leaving edit mode
|
||||||
@@ -733,10 +737,12 @@ impl ChatApp {
|
|||||||
self.textarea.move_cursor(tui_textarea::CursorMove::End);
|
self.textarea.move_cursor(tui_textarea::CursorMove::End);
|
||||||
}
|
}
|
||||||
(KeyCode::Char('w'), m) if m.contains(KeyModifiers::CONTROL) => {
|
(KeyCode::Char('w'), m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
self.textarea.move_cursor(tui_textarea::CursorMove::WordForward);
|
self.textarea
|
||||||
|
.move_cursor(tui_textarea::CursorMove::WordForward);
|
||||||
}
|
}
|
||||||
(KeyCode::Char('b'), m) if m.contains(KeyModifiers::CONTROL) => {
|
(KeyCode::Char('b'), m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
self.textarea.move_cursor(tui_textarea::CursorMove::WordBack);
|
self.textarea
|
||||||
|
.move_cursor(tui_textarea::CursorMove::WordBack);
|
||||||
}
|
}
|
||||||
(KeyCode::Char('r'), m) if m.contains(KeyModifiers::CONTROL) => {
|
(KeyCode::Char('r'), m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
// Redo - history next
|
// Redo - history next
|
||||||
@@ -774,7 +780,8 @@ impl ChatApp {
|
|||||||
let (row, _) = self.textarea.cursor();
|
let (row, _) = self.textarea.cursor();
|
||||||
if let Some(line) = self.textarea.lines().get(row) {
|
if let Some(line) = self.textarea.lines().get(row) {
|
||||||
self.clipboard = line.clone();
|
self.clipboard = line.clone();
|
||||||
self.status = format!("Yanked line ({} chars)", self.clipboard.len());
|
self.status =
|
||||||
|
format!("Yanked line ({} chars)", self.clipboard.len());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.textarea.cancel_selection();
|
self.textarea.cancel_selection();
|
||||||
@@ -812,7 +819,10 @@ impl ChatApp {
|
|||||||
// Can't delete from read-only panels, just yank
|
// Can't delete from read-only panels, just yank
|
||||||
if let Some(yanked) = self.yank_from_panel() {
|
if let Some(yanked) = self.yank_from_panel() {
|
||||||
self.clipboard = yanked;
|
self.clipboard = yanked;
|
||||||
self.status = format!("Yanked {} chars (read-only panel)", self.clipboard.len());
|
self.status = format!(
|
||||||
|
"Yanked {} chars (read-only panel)",
|
||||||
|
self.clipboard.len()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
self.status = "Nothing to yank".to_string();
|
self.status = "Nothing to yank".to_string();
|
||||||
}
|
}
|
||||||
@@ -877,11 +887,12 @@ impl ChatApp {
|
|||||||
// Move selection down (increase end row)
|
// Move selection down (increase end row)
|
||||||
if let Some((row, col)) = self.visual_end {
|
if let Some((row, col)) = self.visual_end {
|
||||||
// Get max lines for the current panel
|
// Get max lines for the current panel
|
||||||
let max_lines = if matches!(self.focused_panel, FocusedPanel::Chat) {
|
let max_lines =
|
||||||
self.auto_scroll.content_len
|
if matches!(self.focused_panel, FocusedPanel::Chat) {
|
||||||
} else {
|
self.auto_scroll.content_len
|
||||||
self.thinking_scroll.content_len
|
} else {
|
||||||
};
|
self.thinking_scroll.content_len
|
||||||
|
};
|
||||||
if row + 1 < max_lines {
|
if row + 1 < max_lines {
|
||||||
self.visual_end = Some((row + 1, col));
|
self.visual_end = Some((row + 1, col));
|
||||||
// Scroll if needed to keep selection visible
|
// Scroll if needed to keep selection visible
|
||||||
@@ -894,7 +905,8 @@ impl ChatApp {
|
|||||||
(KeyCode::Char('w'), KeyModifiers::NONE) => {
|
(KeyCode::Char('w'), KeyModifiers::NONE) => {
|
||||||
match self.focused_panel {
|
match self.focused_panel {
|
||||||
FocusedPanel::Input => {
|
FocusedPanel::Input => {
|
||||||
self.textarea.move_cursor(tui_textarea::CursorMove::WordForward);
|
self.textarea
|
||||||
|
.move_cursor(tui_textarea::CursorMove::WordForward);
|
||||||
}
|
}
|
||||||
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
||||||
// Move selection forward by word
|
// Move selection forward by word
|
||||||
@@ -909,7 +921,8 @@ impl ChatApp {
|
|||||||
(KeyCode::Char('b'), KeyModifiers::NONE) => {
|
(KeyCode::Char('b'), KeyModifiers::NONE) => {
|
||||||
match self.focused_panel {
|
match self.focused_panel {
|
||||||
FocusedPanel::Input => {
|
FocusedPanel::Input => {
|
||||||
self.textarea.move_cursor(tui_textarea::CursorMove::WordBack);
|
self.textarea
|
||||||
|
.move_cursor(tui_textarea::CursorMove::WordBack);
|
||||||
}
|
}
|
||||||
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
||||||
// Move selection backward by word
|
// Move selection backward by word
|
||||||
@@ -997,7 +1010,8 @@ impl ChatApp {
|
|||||||
self.command_buffer.clear();
|
self.command_buffer.clear();
|
||||||
self.mode = InputMode::Normal;
|
self.mode = InputMode::Normal;
|
||||||
}
|
}
|
||||||
(KeyCode::Char(c), KeyModifiers::NONE) | (KeyCode::Char(c), KeyModifiers::SHIFT) => {
|
(KeyCode::Char(c), KeyModifiers::NONE)
|
||||||
|
| (KeyCode::Char(c), KeyModifiers::SHIFT) => {
|
||||||
self.command_buffer.push(c);
|
self.command_buffer.push(c);
|
||||||
self.status = format!(":{}", self.command_buffer);
|
self.status = format!(":{}", self.command_buffer);
|
||||||
}
|
}
|
||||||
@@ -1282,7 +1296,9 @@ impl ChatApp {
|
|||||||
|
|
||||||
// Step 1: Add user message to conversation immediately (synchronous)
|
// Step 1: Add user message to conversation immediately (synchronous)
|
||||||
let message = self.controller.input_buffer_mut().commit_to_history();
|
let message = self.controller.input_buffer_mut().commit_to_history();
|
||||||
self.controller.conversation_mut().push_user_message(message.clone());
|
self.controller
|
||||||
|
.conversation_mut()
|
||||||
|
.push_user_message(message.clone());
|
||||||
|
|
||||||
// Auto-scroll to bottom when sending a message
|
// Auto-scroll to bottom when sending a message
|
||||||
self.auto_scroll.stick_to_bottom = true;
|
self.auto_scroll.stick_to_bottom = true;
|
||||||
@@ -1308,7 +1324,11 @@ impl ChatApp {
|
|||||||
parameters.stream = self.controller.config().general.enable_streaming;
|
parameters.stream = self.controller.config().general.enable_streaming;
|
||||||
|
|
||||||
// Step 2: Start the actual request
|
// Step 2: Start the actual request
|
||||||
match self.controller.send_request_with_current_conversation(parameters).await {
|
match self
|
||||||
|
.controller
|
||||||
|
.send_request_with_current_conversation(parameters)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(SessionOutcome::Complete(_response)) => {
|
Ok(SessionOutcome::Complete(_response)) => {
|
||||||
self.stop_loading_animation();
|
self.stop_loading_animation();
|
||||||
self.status = "Ready".to_string();
|
self.status = "Ready".to_string();
|
||||||
@@ -1323,10 +1343,7 @@ impl ChatApp {
|
|||||||
self.status = "Generating response...".to_string();
|
self.status = "Generating response...".to_string();
|
||||||
|
|
||||||
self.spawn_stream(response_id, stream);
|
self.spawn_stream(response_id, stream);
|
||||||
match self
|
match self.controller.mark_stream_placeholder(response_id, "▌") {
|
||||||
.controller
|
|
||||||
.mark_stream_placeholder(response_id, "▌")
|
|
||||||
{
|
|
||||||
Ok(_) => self.error = None,
|
Ok(_) => self.error = None,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.error = Some(format!("Could not set response placeholder: {}", err));
|
self.error = Some(format!("Could not set response placeholder: {}", err));
|
||||||
@@ -1354,7 +1371,6 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn sync_selected_model_index(&mut self) {
|
fn sync_selected_model_index(&mut self) {
|
||||||
let current_model_id = self.controller.selected_model().to_string();
|
let current_model_id = self.controller.selected_model().to_string();
|
||||||
let filtered_models: Vec<&ModelInfo> = self
|
let filtered_models: Vec<&ModelInfo> = self
|
||||||
@@ -1409,7 +1425,8 @@ impl ChatApp {
|
|||||||
|
|
||||||
pub fn advance_loading_animation(&mut self) {
|
pub fn advance_loading_animation(&mut self) {
|
||||||
if self.is_loading {
|
if self.is_loading {
|
||||||
self.loading_animation_frame = (self.loading_animation_frame + 1) % 8; // 8-frame animation
|
self.loading_animation_frame = (self.loading_animation_frame + 1) % 8;
|
||||||
|
// 8-frame animation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1452,7 +1469,8 @@ impl ChatApp {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let content_to_display = if matches!(role, Role::Assistant) {
|
let content_to_display = if matches!(role, Role::Assistant) {
|
||||||
let (content_without_think, _) = formatter.extract_thinking(&message.content);
|
let (content_without_think, _) =
|
||||||
|
formatter.extract_thinking(&message.content);
|
||||||
content_without_think
|
content_without_think
|
||||||
} else {
|
} else {
|
||||||
message.content.clone()
|
message.content.clone()
|
||||||
@@ -1505,7 +1523,8 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn yank_from_panel(&self) -> Option<String> {
|
fn yank_from_panel(&self) -> Option<String> {
|
||||||
let (start_pos, end_pos) = if let (Some(s), Some(e)) = (self.visual_start, self.visual_end) {
|
let (start_pos, end_pos) = if let (Some(s), Some(e)) = (self.visual_start, self.visual_end)
|
||||||
|
{
|
||||||
// Normalize selection
|
// Normalize selection
|
||||||
if s.0 < e.0 || (s.0 == e.0 && s.1 <= e.1) {
|
if s.0 < e.0 || (s.0 == e.0 && s.1 <= e.1) {
|
||||||
(s, e)
|
(s, e)
|
||||||
@@ -1522,7 +1541,13 @@ impl ChatApp {
|
|||||||
|
|
||||||
pub fn update_thinking_from_last_message(&mut self) {
|
pub fn update_thinking_from_last_message(&mut self) {
|
||||||
// Extract thinking from the last assistant message
|
// Extract thinking from the last assistant message
|
||||||
if let Some(last_msg) = self.conversation().messages.iter().rev().find(|m| matches!(m.role, Role::Assistant)) {
|
if let Some(last_msg) = self
|
||||||
|
.conversation()
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|m| matches!(m.role, Role::Assistant))
|
||||||
|
{
|
||||||
let (_, thinking) = self.formatter().extract_thinking(&last_msg.content);
|
let (_, thinking) = self.formatter().extract_thinking(&last_msg.content);
|
||||||
// Only set stick_to_bottom if content actually changed (to enable auto-scroll during streaming)
|
// Only set stick_to_bottom if content actually changed (to enable auto-scroll during streaming)
|
||||||
let content_changed = self.current_thinking != thinking;
|
let content_changed = self.current_thinking != thinking;
|
||||||
@@ -1540,8 +1565,6 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn spawn_stream(&mut self, message_id: Uuid, mut stream: owlen_core::provider::ChatStream) {
|
fn spawn_stream(&mut self, message_id: Uuid, mut stream: owlen_core::provider::ChatStream) {
|
||||||
let sender = self.session_tx.clone();
|
let sender = self.session_tx.clone();
|
||||||
self.streaming.insert(message_id);
|
self.streaming.insert(message_id);
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use tui_textarea::TextArea;
|
|||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use crate::chat_app::ChatApp;
|
use crate::chat_app::ChatApp;
|
||||||
use owlen_core::ui::{FocusedPanel, InputMode};
|
|
||||||
use owlen_core::types::Role;
|
use owlen_core::types::Role;
|
||||||
|
use owlen_core::ui::{FocusedPanel, InputMode};
|
||||||
|
|
||||||
pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
|
pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
|
||||||
// Update thinking content from last message
|
// Update thinking content from last message
|
||||||
@@ -37,18 +37,15 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
|
|||||||
// Calculate thinking section height
|
// Calculate thinking section height
|
||||||
let thinking_height = if let Some(thinking) = app.current_thinking() {
|
let thinking_height = if let Some(thinking) = app.current_thinking() {
|
||||||
let content_width = available_width.saturating_sub(4);
|
let content_width = available_width.saturating_sub(4);
|
||||||
let visual_lines = calculate_wrapped_line_count(
|
let visual_lines = calculate_wrapped_line_count(thinking.lines(), content_width);
|
||||||
thinking.lines(),
|
|
||||||
content_width,
|
|
||||||
);
|
|
||||||
(visual_lines as u16).min(6) + 2 // +2 for borders, max 6 lines
|
(visual_lines as u16).min(6) + 2 // +2 for borders, max 6 lines
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut constraints = vec![
|
let mut constraints = vec![
|
||||||
Constraint::Length(4), // Header
|
Constraint::Length(4), // Header
|
||||||
Constraint::Min(8), // Messages
|
Constraint::Min(8), // Messages
|
||||||
];
|
];
|
||||||
|
|
||||||
if thinking_height > 0 {
|
if thinking_height > 0 {
|
||||||
@@ -56,7 +53,7 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constraints.push(Constraint::Length(input_height)); // Input
|
constraints.push(Constraint::Length(input_height)); // Input
|
||||||
constraints.push(Constraint::Length(3)); // Status
|
constraints.push(Constraint::Length(3)); // Status
|
||||||
|
|
||||||
let layout = Layout::default()
|
let layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
@@ -437,89 +434,100 @@ fn render_header(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
|||||||
frame.render_widget(paragraph, inner_area);
|
frame.render_widget(paragraph, inner_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_visual_selection(
|
||||||
fn apply_visual_selection(lines: Vec<Line>, selection: Option<((usize, usize), (usize, usize))>) -> Vec<Line> {
|
lines: Vec<Line>,
|
||||||
|
selection: Option<((usize, usize), (usize, usize))>,
|
||||||
|
) -> Vec<Line> {
|
||||||
if let Some(((start_row, start_col), (end_row, end_col))) = selection {
|
if let Some(((start_row, start_col), (end_row, end_col))) = selection {
|
||||||
// Normalize selection (ensure start is before end)
|
// Normalize selection (ensure start is before end)
|
||||||
let ((start_r, start_c), (end_r, end_c)) = if start_row < end_row || (start_row == end_row && start_col <= end_col) {
|
let ((start_r, start_c), (end_r, end_c)) =
|
||||||
((start_row, start_col), (end_row, end_col))
|
if start_row < end_row || (start_row == end_row && start_col <= end_col) {
|
||||||
} else {
|
((start_row, start_col), (end_row, end_col))
|
||||||
((end_row, end_col), (start_row, start_col))
|
} else {
|
||||||
};
|
((end_row, end_col), (start_row, start_col))
|
||||||
|
};
|
||||||
|
|
||||||
lines.into_iter().enumerate().map(|(idx, line)| {
|
lines
|
||||||
if idx < start_r || idx > end_r {
|
.into_iter()
|
||||||
// Line not in selection
|
.enumerate()
|
||||||
return line;
|
.map(|(idx, line)| {
|
||||||
}
|
if idx < start_r || idx > end_r {
|
||||||
|
// Line not in selection
|
||||||
// Convert line to plain text for character indexing
|
|
||||||
let line_text = line.to_string();
|
|
||||||
let char_count = line_text.chars().count();
|
|
||||||
|
|
||||||
if idx == start_r && idx == end_r {
|
|
||||||
// Selection within single line
|
|
||||||
let sel_start = start_c.min(char_count);
|
|
||||||
let sel_end = end_c.min(char_count);
|
|
||||||
|
|
||||||
if sel_start >= sel_end {
|
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_byte = char_to_byte_index(&line_text, sel_start);
|
// Convert line to plain text for character indexing
|
||||||
let end_byte = char_to_byte_index(&line_text, sel_end);
|
let line_text = line.to_string();
|
||||||
|
let char_count = line_text.chars().count();
|
||||||
|
|
||||||
let mut spans = Vec::new();
|
if idx == start_r && idx == end_r {
|
||||||
if start_byte > 0 {
|
// Selection within single line
|
||||||
spans.push(Span::raw(line_text[..start_byte].to_string()));
|
let sel_start = start_c.min(char_count);
|
||||||
}
|
let sel_end = end_c.min(char_count);
|
||||||
spans.push(Span::styled(
|
|
||||||
line_text[start_byte..end_byte].to_string(),
|
|
||||||
Style::default().bg(Color::LightBlue).fg(Color::Black)
|
|
||||||
));
|
|
||||||
if end_byte < line_text.len() {
|
|
||||||
spans.push(Span::raw(line_text[end_byte..].to_string()));
|
|
||||||
}
|
|
||||||
Line::from(spans)
|
|
||||||
} else if idx == start_r {
|
|
||||||
// First line of multi-line selection
|
|
||||||
let sel_start = start_c.min(char_count);
|
|
||||||
let start_byte = char_to_byte_index(&line_text, sel_start);
|
|
||||||
|
|
||||||
let mut spans = Vec::new();
|
if sel_start >= sel_end {
|
||||||
if start_byte > 0 {
|
return line;
|
||||||
spans.push(Span::raw(line_text[..start_byte].to_string()));
|
}
|
||||||
}
|
|
||||||
spans.push(Span::styled(
|
|
||||||
line_text[start_byte..].to_string(),
|
|
||||||
Style::default().bg(Color::LightBlue).fg(Color::Black)
|
|
||||||
));
|
|
||||||
Line::from(spans)
|
|
||||||
} else if idx == end_r {
|
|
||||||
// Last line of multi-line selection
|
|
||||||
let sel_end = end_c.min(char_count);
|
|
||||||
let end_byte = char_to_byte_index(&line_text, sel_end);
|
|
||||||
|
|
||||||
let mut spans = Vec::new();
|
let start_byte = char_to_byte_index(&line_text, sel_start);
|
||||||
spans.push(Span::styled(
|
let end_byte = char_to_byte_index(&line_text, sel_end);
|
||||||
line_text[..end_byte].to_string(),
|
|
||||||
Style::default().bg(Color::LightBlue).fg(Color::Black)
|
let mut spans = Vec::new();
|
||||||
));
|
if start_byte > 0 {
|
||||||
if end_byte < line_text.len() {
|
spans.push(Span::raw(line_text[..start_byte].to_string()));
|
||||||
spans.push(Span::raw(line_text[end_byte..].to_string()));
|
}
|
||||||
|
spans.push(Span::styled(
|
||||||
|
line_text[start_byte..end_byte].to_string(),
|
||||||
|
Style::default().bg(Color::LightBlue).fg(Color::Black),
|
||||||
|
));
|
||||||
|
if end_byte < line_text.len() {
|
||||||
|
spans.push(Span::raw(line_text[end_byte..].to_string()));
|
||||||
|
}
|
||||||
|
Line::from(spans)
|
||||||
|
} else if idx == start_r {
|
||||||
|
// First line of multi-line selection
|
||||||
|
let sel_start = start_c.min(char_count);
|
||||||
|
let start_byte = char_to_byte_index(&line_text, sel_start);
|
||||||
|
|
||||||
|
let mut spans = Vec::new();
|
||||||
|
if start_byte > 0 {
|
||||||
|
spans.push(Span::raw(line_text[..start_byte].to_string()));
|
||||||
|
}
|
||||||
|
spans.push(Span::styled(
|
||||||
|
line_text[start_byte..].to_string(),
|
||||||
|
Style::default().bg(Color::LightBlue).fg(Color::Black),
|
||||||
|
));
|
||||||
|
Line::from(spans)
|
||||||
|
} else if idx == end_r {
|
||||||
|
// Last line of multi-line selection
|
||||||
|
let sel_end = end_c.min(char_count);
|
||||||
|
let end_byte = char_to_byte_index(&line_text, sel_end);
|
||||||
|
|
||||||
|
let mut spans = Vec::new();
|
||||||
|
spans.push(Span::styled(
|
||||||
|
line_text[..end_byte].to_string(),
|
||||||
|
Style::default().bg(Color::LightBlue).fg(Color::Black),
|
||||||
|
));
|
||||||
|
if end_byte < line_text.len() {
|
||||||
|
spans.push(Span::raw(line_text[end_byte..].to_string()));
|
||||||
|
}
|
||||||
|
Line::from(spans)
|
||||||
|
} else {
|
||||||
|
// Middle line - fully selected
|
||||||
|
let styled_spans: Vec<Span> = line
|
||||||
|
.spans
|
||||||
|
.into_iter()
|
||||||
|
.map(|span| {
|
||||||
|
Span::styled(
|
||||||
|
span.content,
|
||||||
|
span.style.bg(Color::LightBlue).fg(Color::Black),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Line::from(styled_spans)
|
||||||
}
|
}
|
||||||
Line::from(spans)
|
})
|
||||||
} else {
|
.collect()
|
||||||
// Middle line - fully selected
|
|
||||||
let styled_spans: Vec<Span> = line.spans.into_iter().map(|span| {
|
|
||||||
Span::styled(
|
|
||||||
span.content,
|
|
||||||
span.style.bg(Color::LightBlue).fg(Color::Black)
|
|
||||||
)
|
|
||||||
}).collect();
|
|
||||||
Line::from(styled_spans)
|
|
||||||
}
|
|
||||||
}).collect()
|
|
||||||
} else {
|
} else {
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
@@ -569,7 +577,11 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
message.content.clone()
|
message.content.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatted: Vec<String> = content_to_display.trim().lines().map(|s| s.to_string()).collect();
|
let formatted: Vec<String> = content_to_display
|
||||||
|
.trim()
|
||||||
|
.lines()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
let is_streaming = message
|
let is_streaming = message
|
||||||
.metadata
|
.metadata
|
||||||
.get("streaming")
|
.get("streaming")
|
||||||
@@ -586,10 +598,11 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Add loading indicator if applicable
|
// Add loading indicator if applicable
|
||||||
if matches!(role, Role::Assistant) &&
|
if matches!(role, Role::Assistant)
|
||||||
app.get_loading_indicator() != "" &&
|
&& app.get_loading_indicator() != ""
|
||||||
message_index == conversation.messages.len() - 1 &&
|
&& message_index == conversation.messages.len() - 1
|
||||||
is_streaming {
|
&& is_streaming
|
||||||
|
{
|
||||||
role_line_spans.push(Span::styled(
|
role_line_spans.push(Span::styled(
|
||||||
format!(" {}", app.get_loading_indicator()),
|
format!(" {}", app.get_loading_indicator()),
|
||||||
Style::default().fg(Color::Yellow),
|
Style::default().fg(Color::Yellow),
|
||||||
@@ -640,7 +653,9 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
|
|
||||||
// Add loading indicator ONLY if we're loading and there are no messages at all,
|
// Add loading indicator ONLY if we're loading and there are no messages at all,
|
||||||
// or if the last message is from the user (no Assistant response started yet)
|
// or if the last message is from the user (no Assistant response started yet)
|
||||||
let last_message_is_user = conversation.messages.last()
|
let last_message_is_user = conversation
|
||||||
|
.messages
|
||||||
|
.last()
|
||||||
.map(|msg| matches!(msg.role, Role::User))
|
.map(|msg| matches!(msg.role, Role::User))
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
|
|
||||||
@@ -649,7 +664,9 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
Span::raw("🤖 "),
|
Span::raw("🤖 "),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
"Assistant:",
|
"Assistant:",
|
||||||
Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD),
|
Style::default()
|
||||||
|
.fg(Color::LightMagenta)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!(" {}", app.get_loading_indicator()),
|
format!(" {}", app.get_loading_indicator()),
|
||||||
@@ -664,7 +681,8 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply visual selection highlighting if in visual mode and Chat panel is focused
|
// Apply visual selection highlighting if in visual mode and Chat panel is focused
|
||||||
if matches!(app.mode(), InputMode::Visual) && matches!(app.focused_panel(), FocusedPanel::Chat) {
|
if matches!(app.mode(), InputMode::Visual) && matches!(app.focused_panel(), FocusedPanel::Chat)
|
||||||
|
{
|
||||||
if let Some(selection) = app.visual_selection() {
|
if let Some(selection) = app.visual_selection() {
|
||||||
lines = apply_visual_selection(lines, Some(selection));
|
lines = apply_visual_selection(lines, Some(selection));
|
||||||
}
|
}
|
||||||
@@ -695,13 +713,16 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
frame.render_widget(paragraph, area);
|
frame.render_widget(paragraph, area);
|
||||||
|
|
||||||
// Render cursor if Chat panel is focused and in Normal mode
|
// Render cursor if Chat panel is focused and in Normal mode
|
||||||
if matches!(app.focused_panel(), FocusedPanel::Chat) && matches!(app.mode(), InputMode::Normal) {
|
if matches!(app.focused_panel(), FocusedPanel::Chat) && matches!(app.mode(), InputMode::Normal)
|
||||||
|
{
|
||||||
let cursor = app.chat_cursor();
|
let cursor = app.chat_cursor();
|
||||||
let cursor_row = cursor.0;
|
let cursor_row = cursor.0;
|
||||||
let cursor_col = cursor.1;
|
let cursor_col = cursor.1;
|
||||||
|
|
||||||
// Calculate visible cursor position (accounting for scroll)
|
// Calculate visible cursor position (accounting for scroll)
|
||||||
if cursor_row >= scroll_position as usize && cursor_row < (scroll_position as usize + viewport_height) {
|
if cursor_row >= scroll_position as usize
|
||||||
|
&& cursor_row < (scroll_position as usize + viewport_height)
|
||||||
|
{
|
||||||
let visible_row = cursor_row - scroll_position as usize;
|
let visible_row = cursor_row - scroll_position as usize;
|
||||||
let cursor_y = area.y + 1 + visible_row as u16; // +1 for border
|
let cursor_y = area.y + 1 + visible_row as u16; // +1 for border
|
||||||
|
|
||||||
@@ -742,7 +763,9 @@ fn render_thinking(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Apply visual selection highlighting if in visual mode and Thinking panel is focused
|
// Apply visual selection highlighting if in visual mode and Thinking panel is focused
|
||||||
if matches!(app.mode(), InputMode::Visual) && matches!(app.focused_panel(), FocusedPanel::Thinking) {
|
if matches!(app.mode(), InputMode::Visual)
|
||||||
|
&& matches!(app.focused_panel(), FocusedPanel::Thinking)
|
||||||
|
{
|
||||||
if let Some(selection) = app.visual_selection() {
|
if let Some(selection) = app.visual_selection() {
|
||||||
lines = apply_visual_selection(lines, Some(selection));
|
lines = apply_visual_selection(lines, Some(selection));
|
||||||
}
|
}
|
||||||
@@ -780,13 +803,17 @@ fn render_thinking(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
frame.render_widget(paragraph, area);
|
frame.render_widget(paragraph, area);
|
||||||
|
|
||||||
// Render cursor if Thinking panel is focused and in Normal mode
|
// Render cursor if Thinking panel is focused and in Normal mode
|
||||||
if matches!(app.focused_panel(), FocusedPanel::Thinking) && matches!(app.mode(), InputMode::Normal) {
|
if matches!(app.focused_panel(), FocusedPanel::Thinking)
|
||||||
|
&& matches!(app.mode(), InputMode::Normal)
|
||||||
|
{
|
||||||
let cursor = app.thinking_cursor();
|
let cursor = app.thinking_cursor();
|
||||||
let cursor_row = cursor.0;
|
let cursor_row = cursor.0;
|
||||||
let cursor_col = cursor.1;
|
let cursor_col = cursor.1;
|
||||||
|
|
||||||
// Calculate visible cursor position (accounting for scroll)
|
// Calculate visible cursor position (accounting for scroll)
|
||||||
if cursor_row >= scroll_position as usize && cursor_row < (scroll_position as usize + viewport_height) {
|
if cursor_row >= scroll_position as usize
|
||||||
|
&& cursor_row < (scroll_position as usize + viewport_height)
|
||||||
|
{
|
||||||
let visible_row = cursor_row - scroll_position as usize;
|
let visible_row = cursor_row - scroll_position as usize;
|
||||||
let cursor_y = area.y + 1 + visible_row as u16; // +1 for border
|
let cursor_y = area.y + 1 + visible_row as u16; // +1 for border
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user