use std::path::PathBuf;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
#[derive(Debug, Clone)]
pub struct ConversationMessage {
pub entry_id: String,
pub role: String,
pub model: Option<String>,
pub timestamp: Option<DateTime<Utc>>,
pub parts: Vec<Part>,
}
impl ConversationMessage {
#[must_use]
pub fn new(entry_id: impl Into<String>, role: impl Into<String>, parts: Vec<Part>) -> Self {
Self {
entry_id: entry_id.into(),
role: role.into(),
model: None,
timestamp: None,
parts,
}
}
#[must_use]
pub fn role(&self) -> &str {
&self.role
}
#[must_use]
pub fn role_label(&self) -> String {
if let [part] = self.parts.as_slice() {
match part {
Part::ToolResult(tr) => return format!("tool/{}", tr.tool_name),
Part::Bash(_) => return "bash".to_string(),
_ => {}
}
}
if self.role.is_empty() {
"unknown".to_string()
} else {
self.role.clone()
}
}
#[must_use]
pub fn is_assistant(&self) -> bool {
self.role == "assistant"
}
#[must_use]
pub fn text(&self) -> String {
self.parts
.iter()
.filter_map(|part| match part {
Part::Text(text) => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
#[must_use]
pub fn thinking(&self) -> Vec<String> {
self.parts
.iter()
.filter_map(|part| match part {
Part::Reasoning(r) => Some(r.text.clone()),
_ => None,
})
.collect()
}
#[must_use]
pub fn tool_calls(&self) -> Vec<&ToolCall> {
self.parts
.iter()
.filter_map(|part| match part {
Part::ToolCall(call) => Some(call),
_ => None,
})
.collect()
}
#[must_use]
pub fn view(&self) -> MessageView<'_> {
if self.role == "assistant" {
return MessageView::Assistant {
thinking: self.thinking(),
text: self.text(),
tool_calls: self.tool_calls(),
};
}
match self.parts.as_slice() {
[Part::ToolResult(result)] => MessageView::ToolResult(result),
[Part::Bash(bash)] => MessageView::Bash(bash),
_ => MessageView::Text {
role: &self.role,
text: self.text(),
},
}
}
}
pub enum MessageView<'a> {
Text {
role: &'a str,
text: String,
},
Assistant {
thinking: Vec<String>,
text: String,
tool_calls: Vec<&'a ToolCall>,
},
ToolResult(&'a ToolResultData),
Bash(&'a BashOutput),
}
#[derive(Debug, Clone)]
pub enum Part {
Text(String),
Reasoning(Reasoning),
ToolCall(ToolCall),
ToolResult(ToolResultData),
Image(Image),
Bash(BashOutput),
Passthrough(Passthrough),
}
#[derive(Debug, Clone)]
pub struct Reasoning {
pub text: String,
pub signature: String,
pub redacted: bool,
}
impl Reasoning {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
signature: String::new(),
redacted: false,
}
}
}
#[derive(Debug, Clone)]
pub struct Image {
pub mime_type: String,
pub data: String,
}
#[derive(Debug, Clone)]
pub struct Passthrough {
pub kind: String,
pub raw: JsonValue,
}
#[derive(Debug, Clone)]
pub struct ToolResultData {
pub call_id: String,
pub tool_name: String,
pub content: String,
pub is_error: bool,
}
#[derive(Debug, Clone)]
pub struct BashOutput {
pub command: String,
pub output: String,
}
#[derive(Debug, Clone)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: JsonValue,
}
#[derive(Debug, Clone)]
pub struct Entry {
pub id: String,
pub parent_id: String,
}
#[derive(Debug, Clone)]
pub struct Context {
pub entries: Vec<Entry>,
pub messages: Vec<ConversationMessage>,
pub cwd: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SearchHit {
pub entry_id: String,
pub score: f64,
}
#[derive(Debug, Clone)]
pub struct ContextListing {
pub id: String,
pub provider_id: ProviderId,
pub path: PathBuf,
pub parent_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderId {
pub label: Option<String>,
pub cwd: PathBuf,
pub count: u32,
pub from: Option<DateTime<Utc>>,
pub until: Option<DateTime<Utc>>,
}