From 69c83e705952e83b8d2ca48d0119e165d07fc838 Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Sun, 2 Aug 2026 10:38:24 +0200 Subject: [PATCH] Improve chat Markdown exports --- src/app.rs | 102 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 10 deletions(-) diff --git a/src/app.rs b/src/app.rs index 78b59db..8f4b456 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1004,7 +1004,7 @@ impl App { return Task::none(); }; let file_name = format!("{}.md", export_file_stem(&title)); - let content = export_markdown(&title, &self.conversation); + let content = export_markdown(&title, self.config.model, &self.conversation); return Task::perform( async move { AsyncFileDialog::new() @@ -2263,11 +2263,40 @@ fn export_file_stem(title: &str) -> String { } } -fn export_markdown(title: &str, conversation: &[ChatMessage]) -> String { +fn write_block_quote(output: &mut String, text: &str) { + use std::fmt::Write; + + for line in text.lines() { + let _ = writeln!(output, "> {line}"); + } +} + +fn write_code_fence(output: &mut String, language: &str, text: &str) { + use std::fmt::Write; + + let length = text + .lines() + .map(|line| { + line.trim_start() + .chars() + .take_while(|character| *character == '`') + .count() + + 1 + }) + .max() + .unwrap_or(3) + .max(3); + let fence = "`".repeat(length); + let _ = write!(output, "\n{fence}{language}\n{text}\n{fence}\n"); +} + +fn export_markdown(title: &str, model: ModelChoice, conversation: &[ChatMessage]) -> String { use std::fmt::Write; let mut output = format!("# {title}\n"); - for message in conversation { + let mut index = 0; + while let Some(message) = conversation.get(index) { + index += 1; if message.system { continue; } @@ -2288,14 +2317,52 @@ fn export_markdown(title: &str, conversation: &[ChatMessage]) -> String { }; let _ = write!(output, "\n## {label}\n"); if let Some(reasoning) = message.reasoning.as_deref().filter(|text| !text.is_empty()) { - let _ = write!( - output, - "\n
\nReasoning\n\n{reasoning}\n\n
\n" - ); + output.push('\n'); + write_block_quote(&mut output, reasoning); } let visible = crate::agent::visible_content(&message.content); let visible = crate::a2ui::transcript_fallback(visible); let _ = write!(output, "\n{}\n", visible.trim()); + + if message.user || message.tool { + continue; + } + let tool_result = conversation.get(index).filter(|message| message.tool); + let cards = crate::agent::stored_tool_cards( + model, + &message.content, + tool_result.map(|message| message.content.as_str()), + &message.tool_approval_reasons, + ); + if cards.is_empty() { + continue; + } + index += usize::from(tool_result.is_some()); + for (tool_index, card) in cards.into_iter().enumerate() { + if card.call.name == "bash" + && let Some(command) = card + .call + .arguments + .get("command") + .and_then(|value| value.as_str()) + { + if let Some(reason) = card.approval_reason { + output.push('\n'); + write_block_quote(&mut output, &reason); + } + write_code_fence(&mut output, "bash", command.trim()); + if let Some(result) = card.result { + write_code_fence(&mut output, "text", &result); + } + } else if let Some(result) = card.result { + let _ = write!( + output, + "\n## Tool\n\nTool result {} ({}):\n{result}\n", + tool_index + 1, + card.call.name + ); + } + } } output } @@ -2747,7 +2814,7 @@ mod tests { } #[test] - fn chat_export_keeps_visible_turns_and_omits_system_messages() { + fn chat_export_formats_visible_turns_reasoning_and_bash_tools() { let message = |user, tool, system, content: &str| ChatMessage { id: 1, user, @@ -2769,17 +2836,32 @@ mod tests { a2ui_replies: Vec::new(), a2ui_open_urls: Vec::new(), }; + let mut assistant = message( + false, + false, + false, + r#"I will check. +<|DSML|tool_calls> +<|DSML|invoke name="bash"><|DSML|parameter name="command" string="true">cat <<'EOF' +``` +EOF +"#, + ); + assistant.reasoning = Some("First thought.\nSecond thought.".into()); + assistant.tool_approval_reasons = vec![Some("This only prints text.".into())]; let exported = export_markdown( "A chat", + ModelChoice::DeepSeekV4Flash, &[ message(false, false, true, "private"), message(true, false, false, "hello"), - message(false, false, false, "**hi**"), + assistant, + message(false, true, false, "Tool result 1 (bash):\n```\none\ntwo\n"), ], ); assert_eq!( exported, - "# A chat\n\n## You\n\nhello\n\n## DS4\n\n**hi**\n" + "# A chat\n\n## You\n\nhello\n\n## DS4\n\n> First thought.\n> Second thought.\n\nI will check.\n\n> This only prints text.\n\n````bash\ncat <<'EOF'\n```\nEOF\n````\n\n````text\n```\none\ntwo\n````\n" ); assert!(!exported.contains("private")); }