use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;
use std::time::Instant;
use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::types::GeneratedMessage;
use super::events::{ContentType, EventSource, TaskType, TimestampedEvent};
const MAX_CHAT_MESSAGES: usize = 500;
const MAX_CONTENT_VERSIONS: usize = 50;
use super::state::Mode;
use super::utils::truncate_chars;
#[derive(Debug, Clone)]
pub struct SessionMetadata {
pub session_id: Uuid,
pub created_at: DateTime<Utc>,
pub last_activity: DateTime<Utc>,
pub repo_path: Option<PathBuf>,
pub title: Option<String>,
pub branch: Option<String>,
}
impl Default for SessionMetadata {
fn default() -> Self {
Self::new()
}
}
impl SessionMetadata {
pub fn new() -> Self {
let now = Utc::now();
Self {
session_id: Uuid::new_v4(),
created_at: now,
last_activity: now,
repo_path: None,
title: None,
branch: None,
}
}
pub fn with_repo(repo_path: PathBuf, branch: Option<String>) -> Self {
let mut meta = Self::new();
meta.repo_path = Some(repo_path);
meta.branch = branch;
meta
}
pub fn touch(&mut self) {
self.last_activity = Utc::now();
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.title = Some(title.into());
}
}
#[derive(Debug, Clone)]
pub struct History {
pub metadata: SessionMetadata,
events: VecDeque<HistoryEntry>,
max_events: usize,
chat_messages: Vec<ChatMessage>,
content_versions: HashMap<ContentKey, Vec<ContentVersion>>,
next_id: u64,
}
impl Default for History {
fn default() -> Self {
Self::new()
}
}
impl History {
pub fn new() -> Self {
Self {
metadata: SessionMetadata::new(),
events: VecDeque::with_capacity(1000),
max_events: 1000,
chat_messages: Vec::new(),
content_versions: HashMap::new(),
next_id: 1,
}
}
pub fn with_repo(repo_path: PathBuf, branch: Option<String>) -> Self {
Self {
metadata: SessionMetadata::with_repo(repo_path, branch),
events: VecDeque::with_capacity(1000),
max_events: 1000,
chat_messages: Vec::new(),
content_versions: HashMap::new(),
next_id: 1,
}
}
pub fn session_id(&self) -> Uuid {
self.metadata.session_id
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.metadata.set_title(title);
}
fn touch(&mut self) {
self.metadata.touch();
}
pub fn record_event(&mut self, event: &TimestampedEvent) {
self.touch();
let entry = HistoryEntry {
id: self.next_id(),
timestamp: event.timestamp,
source: event.source,
change: HistoryChange::Event(format!("{:?}", event.event)),
};
self.push_entry(entry);
}
pub fn record_content(
&mut self,
mode: Mode,
content_type: ContentType,
content: &ContentData,
source: EventSource,
trigger: &str,
) {
self.touch();
let key = ContentKey { mode, content_type };
let previous = self
.content_versions
.get(&key)
.and_then(|versions| versions.last())
.map(|v| v.content.clone());
let version = ContentVersion {
id: self.next_id(),
timestamp: Instant::now(),
source,
trigger: trigger.to_string(),
content: content.clone(),
previous_id: self
.content_versions
.get(&key)
.and_then(|v| v.last())
.map(|v| v.id),
};
self.content_versions
.entry(key.clone())
.or_default()
.push(version);
self.trim_content_versions(&key);
let entry = HistoryEntry {
id: self.next_id(),
timestamp: Instant::now(),
source,
change: HistoryChange::ContentUpdated {
mode,
content_type,
trigger: trigger.to_string(),
preview: content.preview(50),
previous_preview: previous.map(|p| p.preview(50)),
},
};
self.push_entry(entry);
}
fn trim_chat_messages(&mut self) {
while self.chat_messages.len() > MAX_CHAT_MESSAGES {
self.chat_messages.remove(0);
}
}
fn trim_content_versions(&mut self, key: &ContentKey) {
if let Some(versions) = self.content_versions.get_mut(key) {
while versions.len() > MAX_CONTENT_VERSIONS {
versions.remove(0);
}
}
}
pub fn add_chat_message(&mut self, role: ChatRole, content: &str) {
self.touch();
let message = ChatMessage {
id: self.next_id(),
timestamp: Instant::now(),
role,
content: content.to_string(),
mode_context: None,
};
self.chat_messages.push(message);
self.trim_chat_messages();
let entry = HistoryEntry {
id: self.next_id(),
timestamp: Instant::now(),
source: match role {
ChatRole::User => EventSource::User,
ChatRole::Iris => EventSource::Agent,
},
change: HistoryChange::ChatMessage {
role,
preview: truncate_chars(content, 100),
},
};
self.push_entry(entry);
}
pub fn add_chat_message_with_context(
&mut self,
role: ChatRole,
content: &str,
mode: Mode,
related_content: Option<String>,
) {
self.touch();
let message = ChatMessage {
id: self.next_id(),
timestamp: Instant::now(),
role,
content: content.to_string(),
mode_context: Some(ModeContext {
mode,
related_content,
}),
};
self.chat_messages.push(message);
self.trim_chat_messages();
let entry = HistoryEntry {
id: self.next_id(),
timestamp: Instant::now(),
source: match role {
ChatRole::User => EventSource::User,
ChatRole::Iris => EventSource::Agent,
},
change: HistoryChange::ChatMessage {
role,
preview: truncate_chars(content, 100),
},
};
self.push_entry(entry);
}
pub fn record_mode_switch(&mut self, from: Mode, to: Mode) {
let entry = HistoryEntry {
id: self.next_id(),
timestamp: Instant::now(),
source: EventSource::User,
change: HistoryChange::ModeSwitch { from, to },
};
self.push_entry(entry);
}
pub fn record_agent_start(&mut self, task_type: TaskType) {
let entry = HistoryEntry {
id: self.next_id(),
timestamp: Instant::now(),
source: EventSource::System,
change: HistoryChange::AgentTaskStarted { task_type },
};
self.push_entry(entry);
}
pub fn record_agent_complete(&mut self, task_type: TaskType, success: bool) {
let entry = HistoryEntry {
id: self.next_id(),
timestamp: Instant::now(),
source: EventSource::Agent,
change: HistoryChange::AgentTaskCompleted { task_type, success },
};
self.push_entry(entry);
}
pub fn chat_messages(&self) -> &[ChatMessage] {
&self.chat_messages
}
pub fn recent_chat_messages(&self, n: usize) -> &[ChatMessage] {
let start = self.chat_messages.len().saturating_sub(n);
&self.chat_messages[start..]
}
pub fn content_versions(&self, mode: Mode, content_type: ContentType) -> &[ContentVersion] {
let key = ContentKey { mode, content_type };
self.content_versions.get(&key).map_or(&[], Vec::as_slice)
}
pub fn latest_content(&self, mode: Mode, content_type: ContentType) -> Option<&ContentVersion> {
let key = ContentKey { mode, content_type };
self.content_versions.get(&key).and_then(|v| v.last())
}
pub fn content_version_count(&self, mode: Mode, content_type: ContentType) -> usize {
let key = ContentKey { mode, content_type };
self.content_versions.get(&key).map_or(0, Vec::len)
}
pub fn events(&self) -> impl Iterator<Item = &HistoryEntry> {
self.events.iter()
}
pub fn recent_events(&self, n: usize) -> impl Iterator<Item = &HistoryEntry> {
let skip = self.events.len().saturating_sub(n);
self.events.iter().skip(skip)
}
pub fn events_since(&self, since: Instant) -> impl Iterator<Item = &HistoryEntry> {
self.events.iter().filter(move |e| e.timestamp >= since)
}
pub fn event_count(&self) -> usize {
self.events.len()
}
pub fn clear(&mut self) {
self.events.clear();
self.chat_messages.clear();
self.content_versions.clear();
}
pub fn clear_chat(&mut self) {
self.chat_messages.clear();
}
fn next_id(&mut self) -> u64 {
let id = self.next_id;
self.next_id += 1;
id
}
fn push_entry(&mut self, entry: HistoryEntry) {
self.events.push_back(entry);
while self.events.len() > self.max_events {
self.events.pop_front();
}
}
}
#[derive(Debug, Clone)]
pub struct HistoryEntry {
pub id: u64,
pub timestamp: Instant,
pub source: EventSource,
pub change: HistoryChange,
}
#[derive(Debug, Clone)]
pub enum HistoryChange {
Event(String),
ContentUpdated {
mode: Mode,
content_type: ContentType,
trigger: String,
preview: String,
previous_preview: Option<String>,
},
ChatMessage { role: ChatRole, preview: String },
ModeSwitch { from: Mode, to: Mode },
AgentTaskStarted { task_type: TaskType },
AgentTaskCompleted { task_type: TaskType, success: bool },
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ContentKey {
mode: Mode,
content_type: ContentType,
}
#[derive(Debug, Clone)]
pub struct ContentVersion {
pub id: u64,
pub timestamp: Instant,
pub source: EventSource,
pub trigger: String,
pub content: ContentData,
pub previous_id: Option<u64>,
}
#[derive(Debug, Clone)]
pub enum ContentData {
Commit(GeneratedMessage),
Markdown(String),
}
impl ContentData {
pub fn preview(&self, max_len: usize) -> String {
match self {
Self::Commit(msg) => {
let full = format!("{} {}", msg.emoji.as_deref().unwrap_or(""), msg.title);
if full.len() > max_len {
format!("{}...", &full[..max_len])
} else {
full
}
}
Self::Markdown(content) => {
let first_line = content.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
if first_line.len() > max_len {
format!("{}...", &first_line[..max_len])
} else {
first_line.to_string()
}
}
}
}
pub fn as_string(&self) -> String {
match self {
Self::Commit(msg) => {
let emoji = msg.emoji.as_deref().unwrap_or("");
if emoji.is_empty() {
format!("{}\n\n{}", msg.title, msg.message)
} else {
format!("{} {}\n\n{}", emoji, msg.title, msg.message)
}
}
Self::Markdown(content) => content.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct ChatMessage {
pub id: u64,
pub timestamp: Instant,
pub role: ChatRole,
pub content: String,
pub mode_context: Option<ModeContext>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChatRole {
User,
Iris,
}
#[derive(Debug, Clone)]
pub struct ModeContext {
pub mode: Mode,
pub related_content: Option<String>,
}