1use super::model::{TaskId, TaskStatus};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::sync::Arc;
16use std::time::SystemTime;
17use tokio::sync::RwLock;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ThreadMessage {
22 pub role: MessageRole,
23 pub content: String,
24 pub timestamp: SystemTime,
25 #[serde(default, skip_serializing_if = "Vec::is_empty")]
27 pub tool_interactions: Vec<ToolInteraction>,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum MessageRole {
34 System,
35 User,
36 Assistant,
37 Tool,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ToolInteraction {
43 pub tool_name: String,
44 pub arguments: String,
45 pub result: Option<String>,
46 pub success: bool,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct TaskThread {
52 pub task_id: TaskId,
54
55 pub label: String,
57
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub description: Option<String>,
61
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub status: Option<TaskStatus>,
65
66 pub messages: Vec<ThreadMessage>,
68
69 pub compact_summary: Option<String>,
71
72 pub is_foreground: bool,
74
75 pub created_at: SystemTime,
77
78 pub last_activity: SystemTime,
80
81 pub model: Option<String>,
83
84 pub share_context: bool,
86}
87
88impl TaskThread {
89 pub fn new(label: impl Into<String>) -> Self {
91 let now = SystemTime::now();
92 Self {
93 task_id: TaskId::new(),
94 label: label.into(),
95 description: None,
96 status: None,
97 messages: Vec::new(),
98 compact_summary: None,
99 is_foreground: true,
100 created_at: now,
101 last_activity: now,
102 model: None,
103 share_context: true,
104 }
105 }
106
107 pub fn new_task(label: impl Into<String>, description: impl Into<String>) -> Self {
109 let mut thread = Self::new(label);
110 thread.description = Some(description.into());
111 thread.status = Some(TaskStatus::Running {
112 progress: None,
113 message: None,
114 });
115 thread
116 }
117
118 pub fn add_message(&mut self, role: MessageRole, content: impl Into<String>) {
120 self.messages.push(ThreadMessage {
121 role,
122 content: content.into(),
123 timestamp: SystemTime::now(),
124 tool_interactions: Vec::new(),
125 });
126 self.last_activity = SystemTime::now();
127 }
128
129 pub fn message_count(&self) -> usize {
131 self.messages.len()
132 }
133
134 pub fn estimated_tokens(&self) -> usize {
136 self.messages.iter().map(|m| m.content.len() / 4).sum()
137 }
138
139 pub fn compaction_prompt(&self) -> String {
141 let mut history = String::new();
142 for msg in &self.messages {
143 let role = match msg.role {
144 MessageRole::System => "System",
145 MessageRole::User => "User",
146 MessageRole::Assistant => "Assistant",
147 MessageRole::Tool => "Tool",
148 };
149 history.push_str(&format!("{}: {}\n\n", role, msg.content));
150 }
151
152 format!(
153 r#"Summarize the following conversation thread titled "{}".
154
155Focus on:
156- Key decisions made
157- Important information discovered
158- Current state of the task
159- Any pending actions or blockers
160
161Keep the summary concise but complete enough for context continuity.
162
163---
164
165{}
166
167---
168
169Summary:"#,
170 self.label, history
171 )
172 }
173
174 pub fn apply_compaction(&mut self, summary: String) {
176 self.compact_summary = Some(summary);
177 let keep_recent = 3;
179 if self.messages.len() > keep_recent {
180 self.messages = self.messages.split_off(self.messages.len() - keep_recent);
181 }
182 }
183
184 pub fn build_context(&self) -> String {
186 let mut ctx = String::new();
187
188 if let Some(ref summary) = self.compact_summary {
189 ctx.push_str(&format!(
190 "## Thread Summary: {}\n\n{}\n\n",
191 self.label, summary
192 ));
193 }
194
195 ctx.push_str("## Recent Messages\n\n");
196 for msg in &self.messages {
197 let role = match msg.role {
198 MessageRole::System => "System",
199 MessageRole::User => "User",
200 MessageRole::Assistant => "Assistant",
201 MessageRole::Tool => "Tool",
202 };
203 ctx.push_str(&format!("**{}:** {}\n\n", role, msg.content));
204 }
205
206 ctx
207 }
208}
209
210#[derive(Debug, Default)]
212pub struct ThreadManager {
213 threads: HashMap<TaskId, TaskThread>,
215
216 foreground_id: Option<TaskId>,
218}
219
220impl ThreadManager {
221 pub fn new() -> Self {
223 Self::default()
224 }
225
226 pub fn create_thread(&mut self, label: impl Into<String>) -> TaskId {
228 let thread = TaskThread::new(label);
229 let id = thread.task_id;
230
231 if let Some(fg_id) = self.foreground_id {
233 if let Some(fg) = self.threads.get_mut(&fg_id) {
234 fg.is_foreground = false;
235 }
236 }
237
238 self.threads.insert(id, thread);
239 self.foreground_id = Some(id);
240 id
241 }
242
243 pub fn foreground(&self) -> Option<&TaskThread> {
245 self.foreground_id.and_then(|id| self.threads.get(&id))
246 }
247
248 pub fn foreground_mut(&mut self) -> Option<&mut TaskThread> {
250 self.foreground_id.and_then(|id| self.threads.get_mut(&id))
251 }
252
253 pub fn set_foreground_description(&mut self, description: &str) {
255 if let Some(thread) = self.foreground_mut() {
256 thread.description = Some(description.to_string());
257 }
258 }
259
260 pub fn switch_to(&mut self, id: TaskId) -> Option<TaskId> {
262 if !self.threads.contains_key(&id) {
263 return None;
264 }
265
266 let old_fg = self.foreground_id;
267
268 if let Some(fg_id) = old_fg {
270 if fg_id != id {
271 if let Some(fg) = self.threads.get_mut(&fg_id) {
272 fg.is_foreground = false;
273 }
274 }
275 }
276
277 if let Some(thread) = self.threads.get_mut(&id) {
279 thread.is_foreground = true;
280 thread.last_activity = SystemTime::now();
281 }
282
283 self.foreground_id = Some(id);
284 old_fg
285 }
286
287 pub fn all_threads(&self) -> Vec<&TaskThread> {
289 self.threads.values().collect()
290 }
291
292 pub fn get(&self, id: TaskId) -> Option<&TaskThread> {
294 self.threads.get(&id)
295 }
296
297 pub fn get_mut(&mut self, id: TaskId) -> Option<&mut TaskThread> {
299 self.threads.get_mut(&id)
300 }
301
302 pub fn remove(&mut self, id: TaskId) -> Option<TaskThread> {
304 if self.foreground_id == Some(id) {
305 self.foreground_id = None;
306 }
307 self.threads.remove(&id)
308 }
309
310 pub fn rename(&mut self, id: TaskId, new_label: impl Into<String>) -> bool {
312 if let Some(thread) = self.threads.get_mut(&id) {
313 thread.label = new_label.into();
314 true
315 } else {
316 false
317 }
318 }
319
320 pub fn build_global_context(&self) -> String {
322 let mut ctx = String::new();
323
324 for thread in self.threads.values() {
325 if thread.share_context && !thread.is_foreground {
326 if let Some(ref summary) = thread.compact_summary {
327 ctx.push_str(&format!(
328 "## Background Task: {}\n\n{}\n\n---\n\n",
329 thread.label, summary
330 ));
331 }
332 }
333 }
334
335 ctx
336 }
337
338 pub fn find_best_match(&self, message: &str) -> Option<TaskId> {
342 let message_lower = message.to_lowercase();
343 let foreground = self.foreground_id;
344
345 let mut best_match: Option<(TaskId, usize)> = None;
346 let mut foreground_score = 0usize;
347
348 for (id, thread) in &self.threads {
349 if thread.label.eq_ignore_ascii_case("main") {
351 continue;
352 }
353
354 let score: usize = thread
356 .label
357 .split_whitespace()
358 .filter(|word| word.len() >= 3) .filter(|word| message_lower.contains(&word.to_lowercase()))
360 .count();
361
362 if Some(*id) == foreground {
363 foreground_score = score;
364 } else if score > 0 {
365 if best_match.is_none() || score > best_match.unwrap().1 {
366 best_match = Some((*id, score));
367 }
368 }
369 }
370
371 match best_match {
373 Some((id, score)) if score > foreground_score => Some(id),
374 _ => None,
375 }
376 }
377
378 pub fn count(&self) -> usize {
380 self.threads.len()
381 }
382
383 pub fn list_info(&self) -> Vec<ThreadInfo> {
385 self.threads
386 .values()
387 .map(|t| ThreadInfo {
388 id: t.task_id,
389 label: t.label.clone(),
390 description: t.description.clone(),
391 status: t.status.clone(),
392 is_foreground: t.is_foreground,
393 message_count: t.messages.len(),
394 has_summary: t.compact_summary.is_some(),
395 })
396 .collect()
397 }
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct ThreadInfo {
403 pub id: TaskId,
404 pub label: String,
405 pub description: Option<String>,
407 pub status: Option<TaskStatus>,
409 pub is_foreground: bool,
410 pub message_count: usize,
411 pub has_summary: bool,
412}
413
414pub type SharedThreadManager = Arc<RwLock<ThreadManager>>;
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct ThreadManagerState {
422 pub threads: Vec<TaskThread>,
423 pub foreground_id: Option<u64>,
424}
425
426impl ThreadManager {
427 pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
429 let state = ThreadManagerState {
430 threads: self.threads.values().cloned().collect(),
431 foreground_id: self.foreground_id.map(|id| id.0),
432 };
433 let json = serde_json::to_string_pretty(&state)
434 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
435 std::fs::write(path, json)
436 }
437
438 pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
440 let json = std::fs::read_to_string(path)?;
441 let state: ThreadManagerState = serde_json::from_str(&json)
442 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
443
444 let mut threads = HashMap::new();
445 for thread in state.threads {
446 threads.insert(thread.task_id, thread);
447 }
448
449 Ok(Self {
450 threads,
451 foreground_id: state.foreground_id.map(TaskId),
452 })
453 }
454
455 pub fn load_or_default(path: &std::path::Path) -> Self {
457 match Self::load_from_file(path) {
458 Ok(mgr) if !mgr.threads.is_empty() => mgr,
459 _ => {
460 let mut mgr = Self::new();
461 mgr.create_thread("Main");
462 mgr
463 }
464 }
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn test_thread_creation() {
474 let thread = TaskThread::new("Test task");
475 assert_eq!(thread.label, "Test task");
476 assert!(thread.messages.is_empty());
477 assert!(thread.is_foreground);
478 }
479
480 #[test]
481 fn test_thread_manager() {
482 let mut mgr = ThreadManager::new();
483
484 let id1 = mgr.create_thread("Task 1");
485 assert!(mgr.foreground().is_some());
486 assert_eq!(mgr.foreground().unwrap().label, "Task 1");
487
488 let id2 = mgr.create_thread("Task 2");
489 assert_eq!(mgr.foreground().unwrap().label, "Task 2");
490 assert!(!mgr.get(id1).unwrap().is_foreground);
491
492 mgr.switch_to(id1);
493 assert_eq!(mgr.foreground().unwrap().label, "Task 1");
494 }
495
496 #[test]
497 fn test_message_adding() {
498 let mut thread = TaskThread::new("Test");
499 thread.add_message(MessageRole::User, "Hello");
500 thread.add_message(MessageRole::Assistant, "Hi there!");
501 assert_eq!(thread.message_count(), 2);
502 }
503}