1use std::collections::VecDeque;
11
12use crate::ui::app::LogLevel;
13
14pub const CHAT_CAPACITY: usize = 5_000;
17
18pub const DRAIN_CAP_PER_FRAME: usize = 500;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum EntryKind {
24 User,
26 Reasoning,
28 Log(LogLevel),
30 System,
32}
33
34#[derive(Debug, Clone)]
35pub struct ChatEntry {
36 pub seq: u64,
37 pub kind: EntryKind,
38 pub text: String,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum ScrollPos {
44 Follow,
45 Up(usize),
47}
48
49pub struct ChatState {
50 pub entries: VecDeque<ChatEntry>,
51 pub scroll: ScrollPos,
52 pub unseen: usize,
54 next_seq: u64,
55}
56
57impl ChatState {
58 pub fn new() -> Self {
59 Self {
60 entries: VecDeque::new(),
61 scroll: ScrollPos::Follow,
62 unseen: 0,
63 next_seq: 1,
64 }
65 }
66
67 pub fn push(&mut self, kind: EntryKind, text: impl Into<String>) {
68 let text = text.into();
69 self.entries.push_back(ChatEntry {
70 seq: self.next_seq,
71 kind,
72 text,
73 });
74 self.next_seq += 1;
75 if self.entries.len() > CHAT_CAPACITY {
76 self.entries.pop_front();
77 }
78 if self.scroll != ScrollPos::Follow {
79 self.unseen += 1;
80 }
81 }
82
83 pub fn push_status_line(&mut self, line: &str) -> bool {
91 match route_status_line(line) {
92 Routed::Control => false,
93 Routed::Activity(level, text) => {
94 self.push(EntryKind::Log(level), text);
95 true
96 }
97 Routed::Chat(kind, text) => {
98 self.push(kind, text);
99 true
100 }
101 }
102 }
103
104 pub fn passes_filter(entry: &ChatEntry, level: LogLevel) -> bool {
106 match entry.kind {
107 EntryKind::Log(entry_level) => entry_level <= level,
108 _ => true,
109 }
110 }
111
112 pub fn scroll_to_follow(&mut self) {
113 self.scroll = ScrollPos::Follow;
114 self.unseen = 0;
115 }
116
117 pub fn scroll_up(&mut self, lines: usize) {
118 self.scroll = match self.scroll {
119 ScrollPos::Follow => ScrollPos::Up(lines),
120 ScrollPos::Up(n) => ScrollPos::Up(n.saturating_add(lines)),
121 };
122 }
123
124 pub fn scroll_down(&mut self, lines: usize) {
125 self.scroll = match self.scroll {
126 ScrollPos::Follow => ScrollPos::Follow,
127 ScrollPos::Up(n) => {
128 let n = n.saturating_sub(lines);
129 if n == 0 {
130 self.unseen = 0;
131 ScrollPos::Follow
132 } else {
133 ScrollPos::Up(n)
134 }
135 }
136 };
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
142pub enum Routed {
143 Control,
145 Activity(LogLevel, String),
149 Chat(EntryKind, String),
151}
152
153pub fn route_status_line(line: &str) -> Routed {
155 if line.starts_with("__") {
156 return Routed::Control;
157 }
158 if let Some(rest) = line.strip_prefix("[ERROR] ") {
159 Routed::Activity(LogLevel::Error, rest.to_string())
160 } else if let Some(rest) = line.strip_prefix("[WARN] ") {
161 Routed::Activity(LogLevel::Warn, rest.to_string())
162 } else if let Some(rest) = line.strip_prefix("[INFO] ") {
163 Routed::Activity(LogLevel::Info, rest.to_string())
164 } else if let Some(rest) = line.strip_prefix("[DEBUG] ") {
165 Routed::Activity(LogLevel::Debug, rest.to_string())
166 } else if let Some(rest) = line.strip_prefix("[TRACE] ") {
167 Routed::Activity(LogLevel::Trace, rest.to_string())
168 } else if let Some(rest) = line.strip_prefix("[REASONING] ") {
169 Routed::Chat(EntryKind::Reasoning, rest.to_string())
170 } else if line.starts_with("[SERVER] ") || line.starts_with("[CLIENT] ") {
171 Routed::Activity(LogLevel::Info, line.to_string())
175 } else {
176 Routed::Chat(EntryKind::System, line.to_string())
177 }
178}