use std::collections::VecDeque;
use crate::ui::app::LogLevel;
pub const CHAT_CAPACITY: usize = 5_000;
pub const DRAIN_CAP_PER_FRAME: usize = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryKind {
User,
Reasoning,
Log(LogLevel),
System,
}
#[derive(Debug, Clone)]
pub struct ChatEntry {
pub seq: u64,
pub kind: EntryKind,
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollPos {
Follow,
Up(usize),
}
pub struct ChatState {
pub entries: VecDeque<ChatEntry>,
pub scroll: ScrollPos,
pub unseen: usize,
next_seq: u64,
}
impl ChatState {
pub fn new() -> Self {
Self {
entries: VecDeque::new(),
scroll: ScrollPos::Follow,
unseen: 0,
next_seq: 1,
}
}
pub fn push(&mut self, kind: EntryKind, text: impl Into<String>) {
let text = text.into();
self.entries.push_back(ChatEntry {
seq: self.next_seq,
kind,
text,
});
self.next_seq += 1;
if self.entries.len() > CHAT_CAPACITY {
self.entries.pop_front();
}
if self.scroll != ScrollPos::Follow {
self.unseen += 1;
}
}
pub fn push_status_line(&mut self, line: &str) -> bool {
match route_status_line(line) {
Routed::Control => false,
Routed::Activity(level, text) => {
self.push(EntryKind::Log(level), text);
true
}
Routed::Chat(kind, text) => {
self.push(kind, text);
true
}
}
}
pub fn passes_filter(entry: &ChatEntry, level: LogLevel) -> bool {
match entry.kind {
EntryKind::Log(entry_level) => entry_level <= level,
_ => true,
}
}
pub fn scroll_to_follow(&mut self) {
self.scroll = ScrollPos::Follow;
self.unseen = 0;
}
pub fn scroll_up(&mut self, lines: usize) {
self.scroll = match self.scroll {
ScrollPos::Follow => ScrollPos::Up(lines),
ScrollPos::Up(n) => ScrollPos::Up(n.saturating_add(lines)),
};
}
pub fn scroll_down(&mut self, lines: usize) {
self.scroll = match self.scroll {
ScrollPos::Follow => ScrollPos::Follow,
ScrollPos::Up(n) => {
let n = n.saturating_sub(lines);
if n == 0 {
self.unseen = 0;
ScrollPos::Follow
} else {
ScrollPos::Up(n)
}
}
};
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Routed {
Control,
Activity(LogLevel, String),
Chat(EntryKind, String),
}
pub fn route_status_line(line: &str) -> Routed {
if line.starts_with("__") {
return Routed::Control;
}
if let Some(rest) = line.strip_prefix("[ERROR] ") {
Routed::Activity(LogLevel::Error, rest.to_string())
} else if let Some(rest) = line.strip_prefix("[WARN] ") {
Routed::Activity(LogLevel::Warn, rest.to_string())
} else if let Some(rest) = line.strip_prefix("[INFO] ") {
Routed::Activity(LogLevel::Info, rest.to_string())
} else if let Some(rest) = line.strip_prefix("[DEBUG] ") {
Routed::Activity(LogLevel::Debug, rest.to_string())
} else if let Some(rest) = line.strip_prefix("[TRACE] ") {
Routed::Activity(LogLevel::Trace, rest.to_string())
} else if let Some(rest) = line.strip_prefix("[REASONING] ") {
Routed::Chat(EntryKind::Reasoning, rest.to_string())
} else if line.starts_with("[SERVER] ") || line.starts_with("[CLIENT] ") {
Routed::Activity(LogLevel::Info, line.to_string())
} else {
Routed::Chat(EntryKind::System, line.to_string())
}
}