Improve chat Markdown exports

This commit is contained in:
Georg Bauer
2026-08-02 10:38:24 +02:00
parent 0b4e4fcc4a
commit 69c83e7059

View File

@@ -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<details>\n<summary>Reasoning</summary>\n\n{reasoning}\n\n</details>\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.
<DSMLtool_calls>
<DSMLinvoke name="bash"><DSMLparameter name="command" string="true">cat <<'EOF'
```
EOF</DSMLparameter></DSMLinvoke>
</DSMLtool_calls>"#,
);
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"));
}