8.7 KiB
v0.2.1 Message Rendering Polish Implementation Plan
Goal
Improve Cass transcript readability by rendering user/assistant messages as Markdown and making tool calls/results easier to scan. Keep the conversation record and provider payloads unchanged; this release is primarily a presentation-layer improvement.
Scope
In scope
- Render
UserandAssistanttranscript blocks as Markdown in the TUI. - Preserve existing transcript wrapping, scrolling, and sanitization behavior.
- Improve tool invocation summaries with familiar developer-facing language.
- Improve collapsed tool result display so hidden output still gives useful context.
- Normalize tool titles/statuses for successful, failed, pending, cancelled, and approval blocks.
- Add focused rendering tests.
Out of scope
- Changing stored conversation format.
- Changing provider message format.
- Rich Markdown support for tables, images, raw HTML, or nested block-level edge cases.
- Interactive expand/collapse per individual tool call.
- Syntax highlighting for code fences.
Current State
Rendering is centralized in src/ui/render.rs:
transcript_lines_from()convertsTranscriptBlocks into ratatuiLines.- User and assistant content is displayed as sanitized plain text.
- Tool output is hidden when
show_full_tools == false, except for live streamed tool output. max_transcript_scroll()depends ontranscript_lines_from()output and wrapped row counting.
Conversation-to-transcript conversion happens in src/app.rs:
blocks_from_conversation()turns records intoTranscriptBlocks.- Assistant tool calls currently show pretty-printed JSON arguments.
- Tool result blocks show full raw content when expanded.
Design Principles
-
Presentation-only where possible
- Keep
TranscriptBlockand conversation records stable unless a small field addition is clearly worth it. - Prefer helper functions in rendering/conversion code over protocol/model changes.
- Keep
-
Developer-native wording
- Use common dev terms:
file,lines,command,query,matches,edits,diff,exit,duration. - Avoid implementation-specific or awkward terms like
replacements,operations,mutations, ormodifications.
- Use common dev terms:
-
Readable collapsed state
- Collapsed tools should not vanish entirely.
- Show compact metadata such as line count and byte size.
-
Markdown subset first
- Support common chat Markdown well.
- Do not attempt complete Markdown terminal fidelity in v0.2.1.
Implementation Steps
1. Add Markdown parser dependency
Update Cargo.toml:
pulldown-cmark = "0.12"
Rationale: pulldown-cmark is mature, lightweight, and suitable for converting Markdown events into ratatui lines.
2. Add Markdown rendering helpers
File: src/ui/render.rs
Add helper functions:
fn render_markdown_content(content: &str, base_style: Style) -> Vec<Line<'static>>
fn render_plain_content(content: &str) -> Vec<Line<'static>>
fn indent_rendered_lines(lines: Vec<Line<'static>>) -> Vec<Line<'static>>
Use Markdown rendering for:
TranscriptKind::UserTranscriptKind::Assistant
Use plain rendering for:
TranscriptKind::ToolTranscriptKind::ReasoningTranscriptKind::StatusTranscriptKind::Error
Markdown subset
Support these elements:
- Paragraphs
- Soft/hard breaks
- Headings
- Bullet lists
- Ordered lists
- Fenced/indented code blocks
- Inline code
- Emphasis
- Strong text
- Blockquotes
- Links as visible text, optionally followed by dim URL if useful
Suggested visual treatment:
- Heading: bold, maybe same role color
- Bullet:
• - Ordered item:
1.,2. - Code block: preserve text lines, dim or gray style, with indentation
- Inline code: contrasting style, e.g. yellow or gray
- Blockquote: prefix
│, dim style
Important: continue sanitizing rendered text with sanitize_line() or equivalent character filtering.
3. Integrate Markdown renderer into transcript rendering
Current pattern in transcript_lines_from():
for line in content.lines() {
lines.push(Line::raw(format!(" {}", sanitize_line(line))));
}
Replace with logic like:
let rendered = match block.kind {
TranscriptKind::User | TranscriptKind::Assistant => {
render_markdown_content(&content, style_for(&block.kind))
}
_ => render_plain_content(&content),
};
lines.extend(indent_rendered_lines(rendered));
Ensure empty content still produces no body lines.
4. Improve collapsed tool result summaries
File: src/ui/render.rs
Change display_content() so completed tool blocks do not disappear when tools are collapsed.
Add:
fn collapsed_tool_summary(content: &str) -> String
Suggested output:
42 lines · 3.1 KB · tool output hidden
Rules:
- Count lines with
content.lines().count(). - Count bytes with
content.len(). - Use human-readable byte formatting.
- If content is empty, show
no output. - Preserve current live streamed tool behavior: live output should remain visible even when full tools are hidden.
Example behavior:
Collapsed:
· read ✓ (abc123)
120 lines · 8.4 KB · tool output hidden
Expanded:
· read ✓ (abc123)
<full content>
5. Add tool argument summarization
File: src/app.rs
Add:
fn summarize_tool_arguments(name: &str, args: &serde_json::Value) -> String
Use this in blocks_from_conversation() for assistant tool-call blocks instead of always pretty-printing raw JSON.
Fallback to pretty JSON if a tool is unknown or arguments do not match expected shape.
Recommended summaries
read
file: src/ui/render.rs
lines: 1–120
If no range:
file: src/ui/render.rs
write
file: src/lib.rs
bytes: 1.8 KB
If content length is unavailable, omit bytes.
edit
Use edits, not replacements.
file: src/ui/render.rs
edits: 2
If only one edit:
file: src/ui/render.rs
edits: 1
shell
command: cargo test
grep
query: transcript
path: src
If include/exclude globs exist, include them only if concise.
ls
path: src/ui
Unknown tool fallback
Pretty JSON:
serde_json::to_string_pretty(args).unwrap_or_else(|_| args.to_string())
6. Normalize tool titles
File: src/app.rs
Current loaded transcript titles are close to good:
- Invocation:
{tool} … ({id}) - Success:
{tool} ✓ ({id}) - Failure:
{tool} ✗ ({id})
Keep these, but audit live event handling to ensure live-created blocks match loaded blocks after reload.
Preferred title forms:
· read … (abc123) # pending/running invocation
· read ✓ (abc123) # successful result
! read ✗ (abc123) # failed result, via TranscriptKind::Error
· shell cancelled (abc123)
· approval required (abc123)
Do not include verbose implementation terms in titles.
7. Tests
Add or update tests in src/ui/render.rs:
- Assistant Markdown heading/list/code renders into multiple lines.
- User Markdown uses Markdown rendering.
- Tool output collapsed summary appears when
show_full_tools == false. - Tool output is fully visible when
show_full_tools == true. - Live streamed tool output remains visible even when tools are collapsed.
- Scroll calculation still counts rendered Markdown lines.
Add tests in src/app.rs for summarize_tool_arguments():
editsummary usesedits, notreplacements.shellsummary usescommand.readsummary usesfileandlines.- Unknown tool falls back to pretty JSON.
8. Manual QA checklist
Run:
cargo fmt
cargo test
Then manually verify in the TUI:
- User Markdown renders cleanly.
- Assistant Markdown renders cleanly.
- Bullets and code blocks look acceptable in narrow terminals.
- Tool calls are scannable without expanding full output.
/toolsor equivalent full-tool toggle still shows complete output.- Reloaded conversations and live conversations show consistent tool formatting.
- Error tool results are visually distinct.
Suggested Commit Breakdown
- Add Markdown rendering dependency and helpers.
- Switch user/assistant transcript blocks to Markdown rendering.
- Add collapsed tool result summaries.
- Add tool argument summaries with developer-native wording.
- Normalize live/reloaded tool titles.
- Add tests and polish.
Acceptance Criteria
- User and assistant messages render common Markdown elements in the transcript.
- Tool invocations no longer default to noisy JSON for known built-in tools.
edittool summaries sayedits, notreplacements.- Collapsed tool outputs show useful metadata instead of disappearing.
- Existing tool expansion behavior remains available.
cargo fmtandcargo testpass.