//! Session export as Markdown. use crate::types::{Message, Part, Role, Session}; fn fence(s: &str) -> String { // A fence longer than any backtick run inside the content. let mut longest = 0; let mut run = 0; for c in s.chars() { if c == '`' { run += 1; longest = longest.max(run); } else { run = 0; } } "`".repeat((longest + 1).max(3)) } fn clip(s: &str, max: usize) -> String { if s.len() <= max { return s.to_string(); } let mut end = max; while !s.is_char_boundary(end) { end -= 1; } format!("{}\n… ({} more bytes)", &s[..end], s.len() - end) } fn time(ms: i64) -> String { chrono::DateTime::from_timestamp_millis(ms).map(|t| t.format("%Y-%m-%d %H:%M UTC").to_string()).unwrap_or_default() } pub fn markdown(s: &Session, messages: &[Message]) -> String { let title = if s.title.is_empty() { "Untitled session" } else { s.title.as_str() }; let mut out = format!( "# {title}\n\n- Directory: `{}`\n- Model: `{}`\n- Mode: {}\n- Created: {}\n- Exported from CodeCraft\n\n---\n\n", s.cwd, s.model, s.agent, time(s.created_at) ); for m in messages { let is_tool_results = m.parts.iter().all(|p| matches!(p, Part::ToolResult { .. })); if !is_tool_results { let who = match m.role { Role::User => "User".to_string(), _ => format!("Assistant{}", m.model.as_deref().map(|x| format!(" ({x})")).unwrap_or_default()), }; out.push_str(&format!("## {who} · {}\n\n", time(m.created_at))); } for p in &m.parts { match p { Part::Text { text } => out.push_str(&format!("{}\n\n", text.trim())), Part::Summary { text, .. } => out.push_str(&format!("
Summary of the earlier conversation\n\n{}\n\n
\n\n", text.trim())), Part::Reasoning { text, .. } if !text.trim().is_empty() => { out.push_str(&format!("
Thinking\n\n{}\n\n
\n\n", text.trim())) } Part::Reasoning { .. } => {} Part::Image { image } => out.push_str(&format!("*[image: {}]*\n\n", image.media_type)), Part::ToolCall { name, input, .. } => { let body = match name.as_str() { "bash" => input["command"].as_str().unwrap_or("").to_string(), _ => serde_json::to_string_pretty(input).unwrap_or_default(), }; let body = clip(&body, 4000); let f = fence(&body); out.push_str(&format!("**Tool `{name}`**\n\n{f}{}\n{body}\n{f}\n\n", if name == "bash" { "sh" } else { "json" })); } Part::ToolResult { name, content, is_error, .. } => { let body = clip(content.trim(), 4000); let f = fence(&body); out.push_str(&format!( "
{} of {name}\n\n{f}\n{body}\n{f}\n\n
\n\n", if *is_error { "Error" } else { "Result" } )); } } } } out }