- 1
//! Markdown transcript renderer (docs/design/29-personal-os.md P4). One - 2
//! shared renderer for the TUI export and the server-side export so both - 3
//! surfaces emit byte-identical markdown for the same session. Pure: - 4
//! messages in, string out. - 5
- 6
use vak_llm::{ContentBlock, Message, Role}; - 7
- 8
/// Render a projected message list exactly as `export_transcript` has - 9
/// always written it: `# Vakyartha transcript`, then one `## {idx} · {role}` - 10
/// section per message with block-level lines between. - 11
pub fn render_markdown(msgs: &[Message]) -> String { - 12
let mut out = String::from("# Vakyartha transcript\n\n"); - 13
for (idx, m) in msgs.iter().enumerate() { - 14
let role = match m.role { - 15
Role::User => "user", - 16
_ => "assistant", - 17
}; - 18
out.push_str(&format!("## {idx} · {role}\n\n")); - 19
for block in &m.content { - 20
match block { - 21
ContentBlock::Text { text } => { - 22
out.push_str(text.trim()); - 23
out.push_str("\n\n"); - 24
} - 25
ContentBlock::Thinking { text, .. } => { - 26
out.push_str(&format!("> thinking: {}\n\n", text.trim())); - 27
} - 28
ContentBlock::ToolUse { name, input, .. } => { - 29
out.push_str(&format!("- tool `{name}` `{input}`\n")); - 30
} - 31
ContentBlock::ToolResult { - 32
content, is_error, .. - 33
} => { - 34
let mark = if *is_error { "✗" } else { "→" }; - 35
out.push_str(&format!( - 36
"- {mark} result: {}\n", - 37
content.replace('\n', " ") - 38
)); - 39
} - 40
ContentBlock::Image { source } => { - 41
out.push_str(&format!("- image ({})\n", source.media_type)); - 42
} - 43
ContentBlock::Provider { kind, .. } => { - 44
out.push_str(&format!("- provider block `{kind}`\n")); - 45
} - 46
} - 47
} - 48
out.push('\n'); - 49
} - 50
out - 51
} - 52
- 53
#[cfg(test)] - 54
mod tests { - 55
#![allow(clippy::unwrap_used, clippy::expect_used)] - 56
use super::*; - 57
- 58
#[test] - 59
fn output_equals_legacy_tui_export_byte_for_byte() { - 60
let msgs = vec![ - 61
Message { - 62
role: Role::User, - 63
content: vec![ - 64
ContentBlock::text(" fix the flaky test \nsecond line "), - 65
ContentBlock::image_base64("image/png", "aGVsbG8="), - 66
], - 67
}, - 68
Message { - 69
role: Role::Assistant, - 70
content: vec![ - 71
ContentBlock::Thinking { - 72
text: " consider retries ".into(), - 73
signature: Some("sig".into()), - 74
}, - 75
ContentBlock::ToolUse { - 76
id: "t1".into(), - 77
name: "bash".into(), - 78
input: serde_json::json!({"command": "cargo test"}), - 79
}, - 80
ContentBlock::ToolResult { - 81
tool_use_id: "t1".into(), - 82
content: "ok\nwith newline".into(), - 83
is_error: false, - 84
}, - 85
ContentBlock::tool_error("t2", "boom"), - 86
ContentBlock::text("done."), - 87
], - 88
}, - 89
]; - 90
- 91
// Expected string transcribed from the TUI export path this module - 92
// replaces (note: `text.trim()` keeps interior line-trailing - 93
// spaces, and each message section ends with a blank line); any - 94
// divergence from it is a parity bug. - 95
let expected = "# Vakyartha transcript\n\ - 96
\n\ - 97
## 0 · user\n\ - 98
\n\ - 99
fix the flaky test \nsecond line\n\ - 100
\n\ - 101
- image (image/png)\n\ - 102
\n\ - 103
## 1 · assistant\n\ - 104
\n\ - 105
> thinking: consider retries\n\ - 106
\n\ - 107
- tool `bash` `{\"command\":\"cargo test\"}`\n\ - 108
- → result: ok with newline\n\ - 109
- ✗ result: boom\n\ - 110
done.\n\ - 111
\n\ - 112
\n"; - 113
assert_eq!(render_markdown(&msgs), expected); - 114
} - 115
- 116
#[test] - 117
fn empty_transcript_renders_header_only() { - 118
assert_eq!(render_markdown(&[]), "# Vakyartha transcript\n\n"); - 119
} - 120
} - 121
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.