Improve chat Markdown exports
This commit is contained in:
102
src/app.rs
102
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<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.
|
||||
<|DSML|tool_calls>
|
||||
<|DSML|invoke name="bash"><|DSML|parameter name="command" string="true">cat <<'EOF'
|
||||
```
|
||||
EOF</|DSML|parameter></|DSML|invoke>
|
||||
</|DSML|tool_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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user