1use std::io::Write;
3
4use anyhow::Result;
5use serde::Serialize;
6
7use crate::models::{ContentBlock, MessageContent};
8use crate::output::Emitter;
9use crate::util::discover::SessionFile;
10
11pub struct ExportOpts {
14 pub session: String,
15 pub to_stdout: bool,
17 pub md_path: Option<String>,
19}
20
21#[derive(Serialize, Debug)]
24struct ExportDone {
25 #[serde(rename = "type")]
26 record_type: &'static str,
27 session_id: String,
28 project: String,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 output_file: Option<String>,
31 messages: usize,
32}
33
34pub fn run<W: Write>(opts: &ExportOpts, file: &SessionFile, em: &mut Emitter<W>) -> Result<()> {
37 let records = crate::cmd::parse_records(file)?;
38
39 let mut md = String::new();
40 md.push_str(&format!(
41 "# Session: {}\n\n**Project:** {} \n**Size:** {}\n\n---\n\n",
42 file.session_id, file.project_name, file.size_human()
43 ));
44
45 let mut msg_count = 0usize;
46
47 for record in &records {
48 let Some(msg) = record.as_message() else { continue };
49 msg_count += 1;
50
51 let role = record.role();
52 let ts = msg.timestamp.as_deref().unwrap_or("unknown");
53 let ts_short = ts.get(..19).unwrap_or(ts);
54
55 md.push_str(&format!("## {} ({})\n\n", role.to_uppercase(), ts_short));
56
57 match &msg.message.content {
58 MessageContent::Text(s) => {
59 md.push_str(s);
60 md.push_str("\n\n");
61 }
62 MessageContent::Blocks(blocks) => {
63 for block in blocks {
64 match block {
65 ContentBlock::Text { text } => {
66 md.push_str(text);
67 md.push_str("\n\n");
68 }
69 ContentBlock::Thinking { thinking } => {
70 md.push_str(&format!(
71 "<details>\n<summary>Thinking</summary>\n\n{}\n\n</details>\n\n",
72 thinking
73 ));
74 }
75 ContentBlock::ToolUse { name, input, .. } => {
76 let pretty = serde_json::to_string_pretty(input)
77 .unwrap_or_else(|_| input.to_string());
78 md.push_str(&format!("**Tool: {}**\n```json\n{}\n```\n\n", name, pretty));
79 }
80 ContentBlock::ToolResult { content: Some(c), .. } => {
81 let s = c.to_string();
82 let preview: String = s.chars().take(2000).collect();
83 md.push_str(&format!("**Result:**\n```\n{}\n```\n\n", preview));
84 }
85 _ => {}
86 }
87 }
88 }
89 }
90
91 md.push_str("---\n\n");
92 }
93
94 if opts.to_stdout {
96 for line in md.lines() {
98 em.raw(line)?;
99 }
100 }
101
102 let output_file = if let Some(p) = &opts.md_path {
103 std::fs::write(p, &md)?;
104 Some(p.clone())
105 } else if !opts.to_stdout {
106 let path = format!("{}.md", &file.session_id[..8.min(file.session_id.len())]);
107 std::fs::write(&path, &md)?;
108 Some(path)
109 } else {
110 None
111 };
112
113 if !opts.to_stdout {
114 let done = ExportDone {
115 record_type: "export",
116 session_id: file.session_id.clone(),
117 project: file.project_name.clone(),
118 output_file,
119 messages: msg_count,
120 };
121 em.emit(&done)?;
122 }
123
124 em.flush()?;
125 Ok(())
126}