use std::path::Path;
mod render;
pub use render::render;
const INBOX_DIR: &str = "inbox";
const MESSAGE_EXT: &str = ".md";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Epitaph {
FinalResponse,
Stopped,
BudgetExhausted,
Died,
Unknown(String),
}
impl Epitaph {
fn parse(value: &str) -> Epitaph {
match value {
"final-response" => Epitaph::FinalResponse,
"stopped" => Epitaph::Stopped,
"budget-exhausted" => Epitaph::BudgetExhausted,
"died" => Epitaph::Died,
other => Epitaph::Unknown(other.to_string()),
}
}
pub fn label(&self) -> String {
match self {
Epitaph::FinalResponse => "final-response".to_string(),
Epitaph::Stopped => "stopped".to_string(),
Epitaph::BudgetExhausted => "budget-exhausted".to_string(),
Epitaph::Died => "died".to_string(),
Epitaph::Unknown(value) => value.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Deposit {
pub sender: Option<String>,
pub deposited_at: Option<String>,
pub epitaph: Option<Epitaph>,
pub terminal_ref: Option<String>,
pub body: String,
}
pub fn parse_deposit(bytes: &[u8]) -> Deposit {
let text = String::from_utf8_lossy(bytes);
match split_frontmatter(&text) {
Some((frontmatter, body)) => Deposit {
sender: field(frontmatter, "from"),
deposited_at: field(frontmatter, "deposited_at"),
epitaph: field(frontmatter, "epitaph").map(|v| Epitaph::parse(&v)),
terminal_ref: field(frontmatter, "terminal_ref"),
body: body.to_string(),
},
None => Deposit {
body: text.into_owned(),
..Deposit::default()
},
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct InboxEntry {
pub name: String,
pub raw: Vec<u8>,
pub deposit: Deposit,
}
impl InboxEntry {
pub fn in_memory(&self) -> bool {
self.name.is_empty()
}
}
pub fn header_line(deposit: &Deposit) -> String {
let from = deposit.sender.as_deref().unwrap_or("?");
let at = deposit.deposited_at.as_deref().unwrap_or("?");
format!("✉ {from} · {at}")
}
pub fn list_inbox(workspace: &Path, agent_id: &str) -> Vec<InboxEntry> {
let dir = workspace.join(INBOX_DIR).join(agent_id);
let Ok(entries) = std::fs::read_dir(&dir) else {
return Vec::new();
};
let mut files: Vec<std::path::PathBuf> = entries
.flatten()
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.ends_with(MESSAGE_EXT))
})
.collect();
files.sort();
files
.iter()
.filter_map(|p| {
let raw = std::fs::read(p).ok()?;
Some(InboxEntry {
name: p.file_name()?.to_string_lossy().into_owned(),
deposit: parse_deposit(&raw),
raw,
})
})
.collect()
}
fn split_frontmatter(text: &str) -> Option<(&str, &str)> {
text.strip_prefix("---\n")?.split_once("\n---\n")
}
fn field(frontmatter: &str, key: &str) -> Option<String> {
frontmatter
.lines()
.filter_map(|line| line.split_once(": "))
.find(|(k, _)| *k == key)
.map(|(_, v)| v.to_string())
}
#[cfg(test)]
mod tests;