Move chat code to chat.rs

This commit is contained in:
Kent Overstreet 2026-04-05 20:08:18 -04:00
parent 65d23692fb
commit f29b4be09c
2 changed files with 226 additions and 228 deletions

View file

@ -21,8 +21,6 @@ use ratatui::crossterm::{
};
use ratatui::{
backend::CrosstermBackend,
style::{Color, Style},
text::{Line, Span},
};
use std::io;
@ -48,210 +46,6 @@ pub(crate) fn screen_legend() -> String {
SCREEN_LEGEND.get().cloned().unwrap_or_default()
}
pub(crate) fn strip_ansi(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next();
while let Some(&c) = chars.peek() {
if c.is_ascii() && (0x20..=0x3F).contains(&(c as u8)) {
chars.next();
} else {
break;
}
}
if let Some(&c) = chars.peek() {
if c.is_ascii() && (0x40..=0x7E).contains(&(c as u8)) {
chars.next();
}
}
} else if let Some(&c) = chars.peek() {
if c.is_ascii() && (0x40..=0x5F).contains(&(c as u8)) {
chars.next();
}
}
} else {
out.push(ch);
}
}
out
}
pub(crate) fn is_zero_width(ch: char) -> bool {
matches!(ch,
'\u{200B}'..='\u{200F}' |
'\u{2028}'..='\u{202F}' |
'\u{2060}'..='\u{2069}' |
'\u{FEFF}'
)
}
/// Which pane receives scroll keys.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ActivePane {
Autonomous,
Conversation,
Tools,
}
const MAX_PANE_LINES: usize = 10_000;
/// Turn marker for the conversation pane gutter.
#[derive(Clone, Copy, PartialEq, Default)]
pub(crate) enum Marker {
#[default]
None,
User,
Assistant,
}
pub(crate) struct PaneState {
pub(crate) lines: Vec<Line<'static>>,
pub(crate) markers: Vec<Marker>,
pub(crate) current_line: String,
pub(crate) current_color: Color,
pub(crate) md_buffer: String,
pub(crate) use_markdown: bool,
pub(crate) pending_marker: Marker,
pub(crate) scroll: u16,
pub(crate) pinned: bool,
pub(crate) last_total_lines: u16,
pub(crate) last_height: u16,
}
impl PaneState {
fn new(use_markdown: bool) -> Self {
Self {
lines: Vec::new(), markers: Vec::new(),
current_line: String::new(), current_color: Color::Reset,
md_buffer: String::new(), use_markdown,
pending_marker: Marker::None, scroll: 0, pinned: false,
last_total_lines: 0, last_height: 20,
}
}
fn evict(&mut self) {
if self.lines.len() > MAX_PANE_LINES {
let excess = self.lines.len() - MAX_PANE_LINES;
self.lines.drain(..excess);
self.markers.drain(..excess);
self.scroll = self.scroll.saturating_sub(excess as u16);
}
}
fn append_text(&mut self, text: &str) {
let clean = strip_ansi(text);
if self.use_markdown {
self.md_buffer.push_str(&clean);
} else {
for ch in clean.chars() {
if ch == '\n' {
let line = std::mem::take(&mut self.current_line);
self.lines.push(Line::styled(line, Style::default().fg(self.current_color)));
self.markers.push(Marker::None);
} else if ch == '\t' {
self.current_line.push_str(" ");
} else if ch.is_control() || is_zero_width(ch) {
} else {
self.current_line.push(ch);
}
}
}
self.evict();
}
pub(crate) fn flush_pending(&mut self) {
if self.use_markdown && !self.md_buffer.is_empty() {
let parsed = parse_markdown(&self.md_buffer);
for (i, line) in parsed.into_iter().enumerate() {
let marker = if i == 0 { std::mem::take(&mut self.pending_marker) } else { Marker::None };
self.lines.push(line);
self.markers.push(marker);
}
self.md_buffer.clear();
}
if !self.current_line.is_empty() {
let line = std::mem::take(&mut self.current_line);
self.lines.push(Line::styled(line, Style::default().fg(self.current_color)));
self.markers.push(std::mem::take(&mut self.pending_marker));
}
}
fn push_line(&mut self, line: String, color: Color) {
self.push_line_with_marker(line, color, Marker::None);
}
fn push_line_with_marker(&mut self, line: String, color: Color, marker: Marker) {
self.flush_pending();
self.lines.push(Line::styled(strip_ansi(&line), Style::default().fg(color)));
self.markers.push(marker);
self.evict();
}
pub(crate) fn pop_line(&mut self) {
self.lines.pop();
self.markers.pop();
}
fn scroll_up(&mut self, n: u16) {
self.scroll = self.scroll.saturating_sub(n);
self.pinned = true;
}
fn scroll_down(&mut self, n: u16) {
let max = self.last_total_lines.saturating_sub(self.last_height);
self.scroll = (self.scroll + n).min(max);
if self.scroll >= max { self.pinned = false; }
}
pub(crate) fn all_lines(&self) -> Vec<Line<'static>> {
let (lines, _) = self.all_lines_with_markers();
lines
}
pub(crate) fn all_lines_with_markers(&self) -> (Vec<Line<'static>>, Vec<Marker>) {
let mut lines: Vec<Line<'static>> = self.lines.clone();
let mut markers: Vec<Marker> = self.markers.clone();
if self.use_markdown && !self.md_buffer.is_empty() {
let parsed = parse_markdown(&self.md_buffer);
let count = parsed.len();
lines.extend(parsed);
if count > 0 {
markers.push(self.pending_marker);
markers.extend(std::iter::repeat(Marker::None).take(count - 1));
}
} else if !self.current_line.is_empty() {
lines.push(Line::styled(self.current_line.clone(), Style::default().fg(self.current_color)));
markers.push(self.pending_marker);
}
(lines, markers)
}
}
pub(crate) fn new_textarea(lines: Vec<String>) -> tui_textarea::TextArea<'static> {
let mut ta = tui_textarea::TextArea::new(lines);
ta.set_cursor_line_style(Style::default());
ta.set_wrap_mode(tui_textarea::WrapMode::Word);
ta
}
pub(crate) fn parse_markdown(md: &str) -> Vec<Line<'static>> {
tui_markdown::from_str(md)
.lines
.into_iter()
.map(|line| {
let spans: Vec<Span<'static>> = line.spans.into_iter()
.map(|span| Span::styled(span.content.into_owned(), span.style))
.collect();
let mut result = Line::from(spans).style(line.style);
result.alignment = line.alignment;
result
})
.collect()
}
/// Action returned from a screen's tick method.
pub enum ScreenAction {
/// Switch to screen at this index