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();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let file_name = format!("{}.md", export_file_stem(&title));
|
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(
|
return Task::perform(
|
||||||
async move {
|
async move {
|
||||||
AsyncFileDialog::new()
|
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;
|
use std::fmt::Write;
|
||||||
|
|
||||||
let mut output = format!("# {title}\n");
|
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 {
|
if message.system {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -2288,14 +2317,52 @@ fn export_markdown(title: &str, conversation: &[ChatMessage]) -> String {
|
|||||||
};
|
};
|
||||||
let _ = write!(output, "\n## {label}\n");
|
let _ = write!(output, "\n## {label}\n");
|
||||||
if let Some(reasoning) = message.reasoning.as_deref().filter(|text| !text.is_empty()) {
|
if let Some(reasoning) = message.reasoning.as_deref().filter(|text| !text.is_empty()) {
|
||||||
let _ = write!(
|
output.push('\n');
|
||||||
output,
|
write_block_quote(&mut output, reasoning);
|
||||||
"\n<details>\n<summary>Reasoning</summary>\n\n{reasoning}\n\n</details>\n"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let visible = crate::agent::visible_content(&message.content);
|
let visible = crate::agent::visible_content(&message.content);
|
||||||
let visible = crate::a2ui::transcript_fallback(visible);
|
let visible = crate::a2ui::transcript_fallback(visible);
|
||||||
let _ = write!(output, "\n{}\n", visible.trim());
|
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
|
output
|
||||||
}
|
}
|
||||||
@@ -2747,7 +2814,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 {
|
let message = |user, tool, system, content: &str| ChatMessage {
|
||||||
id: 1,
|
id: 1,
|
||||||
user,
|
user,
|
||||||
@@ -2769,17 +2836,32 @@ mod tests {
|
|||||||
a2ui_replies: Vec::new(),
|
a2ui_replies: Vec::new(),
|
||||||
a2ui_open_urls: 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(
|
let exported = export_markdown(
|
||||||
"A chat",
|
"A chat",
|
||||||
|
ModelChoice::DeepSeekV4Flash,
|
||||||
&[
|
&[
|
||||||
message(false, false, true, "private"),
|
message(false, false, true, "private"),
|
||||||
message(true, false, false, "hello"),
|
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!(
|
assert_eq!(
|
||||||
exported,
|
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"));
|
assert!(!exported.contains("private"));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user