-
Notifications
You must be signed in to change notification settings - Fork 0
Add markdown export for session logs and enhance UI #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Add wkhtmltopdf and pulldown-cmark dependencies for PDF/markdown generation - Implement export_session_markdown() and export_session_pdf() API endpoints - Add comprehensive markdown export with formatted user/assistant messages, tool usage, and timestamps - Add PDF export with HTML-to-PDF conversion and professional styling - Add export buttons in session view header (📄 Markdown, 📋 PDF) - Add hover-revealed export buttons (MD, PDF) for each session in sessions list - Implement proper click event handling to prevent conflicts with session selection - Files download with descriptive names like ''project-name-session-id.md'' Closes #3 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Harper Reed <[email protected]>
- Change wkhtmltopdf dependency from 0.5 to 0.4.0 (0.5 not available) - Fix formatting issues in src/lib.rs to pass rustfmt checks Co-authored-by: Harper Reed <[email protected]>
- Combined TUI functionality with markdown export capabilities - Resolved conflicts in Cargo.toml by including ratatui/crossterm dependencies - Resolved conflicts in src/main.rs by supporting both --tui flag and markdown export endpoint - Removed PDF export functionality to simplify dependencies - Applied pre-commit hook fixes for formatting and linting - Preserves all features from both branches except PDF export 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Remove PDF export button from session view header - Remove PDF export button from sessions list hover actions - Remove PDF-specific CSS styles (.export-btn.pdf, .small-export-btn.pdf) - Simplify exportSession function to only handle markdown format - Now only supports markdown export as intended 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Replace push_str("\n") with push('\n') for single character strings - Satisfies clippy::single_char_add_str lint 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Add complete tool_renderer.rs module with 20+ handlers - Support for both Markdown and HTML output formats - Trait-based architecture with ToolHandler implementations - Replace old export functions with new tool renderer - Clean up dead code and fix clippy warnings - All tests passing with enhanced tool rendering capabilities 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
WalkthroughThe changes introduce a comprehensive session log export feature in Markdown format. This includes backend support for exporting logs via both HTTP API and CLI, a modular rendering framework for formatting tool interactions, and UI enhancements to trigger exports from the web interface. The implementation is additive and modular, affecting the backend, CLI, rendering logic, and frontend. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Browser
participant WebServer
participant AppState
participant FileSystem
participant ToolRenderer
User->>Browser: Click "Export Markdown"
Browser->>WebServer: GET /api/projects/:project/sessions/:session/export/markdown
WebServer->>AppState: Retrieve session log file
AppState->>FileSystem: Read .jsonl log
FileSystem-->>AppState: Log data
AppState->>ToolRenderer: Render entries to Markdown
ToolRenderer-->>AppState: Markdown content
AppState->>WebServer: Return Markdown file response
WebServer-->>Browser: Download .md file
Browser-->>User: Save Markdown file
sequenceDiagram
participant User
participant CLI
participant FileSystem
participant ToolRenderer
User->>CLI: Run with --export flags
CLI->>FileSystem: Discover projects/sessions
loop For each session
FileSystem->>CLI: Read .jsonl log
CLI->>ToolRenderer: Render entries to Markdown
ToolRenderer-->>CLI: Markdown content
CLI->>FileSystem: Write .md file
end
CLI-->>User: Export complete
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. ✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (6)
static/index.html (1)
563-569
: Inline HTML event handlers hurt maintainability & accessibility
onclick="exportSession('markdown')"
mixes behaviour into markup, makes it harder
to audit, and bypasses ESLint/TS checks.
Prefer unobtrusive JS:document .getElementById('export-markdown-btn') .addEventListener('click', e => { e.preventDefault(); exportSession('markdown'); });Bonus: you can add
aria-label="Export current session as markdown"
to the anchor for screen-reader clarity.src/main.rs (3)
35-108
: Well-structured CLI export feature implementation.The CLI flags are properly configured with dependencies, and the export mode logic is clean with appropriate error handling. Consider applying the static analysis suggestion for string formatting on line 82.
- .map_err(|e| format!("Failed to initialize watch manager: {}", e))?; + .map_err(|e| format!("Failed to initialize watch manager: {e}"))?;
149-223
: Export functions look good with minor improvements needed.The export logic is well-structured, but consider:
- More specific error handling instead of propagating all errors with
?
- Applying format string interpolation improvements as suggested by static analysis
Consider adding more context to errors:
- let content = fs::read_to_string(&session_file)?; + let content = fs::read_to_string(&session_file) + .map_err(|e| format!("Failed to read session file {}: {}", session_file.display(), e))?;Apply format string improvements on lines 158, 174, 177-180, 206, 207, and 219.
225-275
: Consider handling edge cases in discovery functions.The helper functions work correctly but silently skip certain failures:
- Files/directories with non-UTF8 names are ignored
- Malformed JSON lines are silently skipped
Consider logging skipped entries for debugging:
fn discover_projects(projects_dir: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let mut projects = Vec::new(); for entry in fs::read_dir(projects_dir)? { let entry = entry?; if entry.file_type()?.is_dir() { - if let Some(name) = entry.file_name().to_str() { - projects.push(name.to_string()); - } + match entry.file_name().to_str() { + Some(name) => projects.push(name.to_string()), + None => eprintln!("Warning: Skipping directory with non-UTF8 name"), + } } }Note that
parse_log_entries
duplicates logic from lib.rs and could potentially use a shared implementation.src/tool_renderer.rs (2)
1279-1369
: Format utilities are well-implemented with room for enhancement.The utilities provide good cross-format support. Consider:
- The
diff_block
function shows all old lines then all new lines, which isn't a true line-by-line diff- Apply format string interpolation as suggested by static analysis (lines 1350, 1357)
For better diff visualization, consider using a line-by-line comparison:
pub fn diff_block(old_content: &str, new_content: &str, format: OutputFormat) -> String { // Use a proper diff algorithm or at least show changes line by line // Current implementation shows all removals then all additions }
1416-1439
: Consider expanding test coverage.The current tests provide basic validation but could be more comprehensive.
Consider adding tests for:
- Actual tool rendering with sample inputs/outputs
- Edge cases (missing fields, malformed JSON)
- Registry operations (registering custom handlers)
- Output format differences between Markdown and HTML
Example:
#[test] fn test_bash_handler_rendering() { let renderer = ToolRenderer::new(); let input = json!({ "command": "echo hello", "description": "Test command" }); let context = RenderContext { tool_name: "Bash".to_string(), tool_id: Some("123".to_string()), timestamp: None, session_id: "test".to_string(), project_name: "test".to_string(), }; let result = renderer.render_tool("Bash", &input, None, OutputFormat::Markdown, &context); assert!(result.input.contains("echo hello")); assert!(result.header.contains("💻")); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/lib.rs
(3 hunks)src/main.rs
(5 hunks)src/tool_renderer.rs
(1 hunks)static/index.html
(4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/lib.rs (1)
src/tool_renderer.rs (1)
new
(62-72)
🪛 GitHub Check: clippy
src/main.rs
[warning] 82-82: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/main.rs:82:22
|
82 | .map_err(|e| format!("Failed to initialize watch manager: {}", e))?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
= note: #[warn(clippy::uninlined_format_args)]
on by default
help: change this to
|
82 - .map_err(|e| format!("Failed to initialize watch manager: {}", e))?;
82 + .map_err(|e| format!("Failed to initialize watch manager: {e}"))?;
|
[warning] 219-219: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/main.rs:219:9
|
219 | println!(" ✅ {}.md", session_id);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
219 - println!(" ✅ {}.md", session_id);
219 + println!(" ✅ {session_id}.md");
|
[warning] 207-207: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/main.rs:207:51
|
207 | let export_file = project_export_dir.join(format!("{}.md", session_id));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
207 - let export_file = project_export_dir.join(format!("{}.md", session_id));
207 + let export_file = project_export_dir.join(format!("{session_id}.md"));
|
[warning] 206-206: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/main.rs:206:45
|
206 | let session_file = project_dir.join(format!("{}.jsonl", session_id));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
206 - let session_file = project_dir.join(format!("{}.jsonl", session_id));
206 + let session_file = project_dir.join(format!("{session_id}.jsonl"));
|
[warning] 177-180: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/main.rs:177:13
|
177 | / eprintln!(
178 | | "
179 | | project_name
180 | | );
| |_____________^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
[warning] 174-174: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/main.rs:174:13
|
174 | println!("📖 Exporting project: {}", project_name);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
174 - println!("📖 Exporting project: {}", project_name);
174 + println!("📖 Exporting project: {project_name}");
|
[warning] 158-158: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/main.rs:158:9
|
158 | println!("📖 Exporting project: {}", project_name);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
158 - println!("📖 Exporting project: {}", project_name);
158 + println!("📖 Exporting project: {project_name}");
|
src/lib.rs
[warning] 529-529: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:529:44
|
529 | markdown.push_str(&format!("## 🤖 Assistant\n\n{}\n\n", content));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
529 - markdown.push_str(&format!("## 🤖 Assistant\n\n{}\n\n", content));
529 + markdown.push_str(&format!("## 🤖 Assistant\n\n{content}\n\n"));
|
[warning] 522-522: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:522:44
|
522 | markdown.push_str(&format!("## 👤 User\n\n{}\n\n", content));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
522 - markdown.push_str(&format!("## 👤 User\n\n{}\n\n", content));
522 + markdown.push_str(&format!("## 👤 User\n\n{content}\n\n"));
|
[warning] 516-516: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:516:40
|
516 | markdown.push_str(&format!("## 📋 Session Summary\n\n{}\n\n", summary));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
516 - markdown.push_str(&format!("## 📋 Session Summary\n\n{}\n\n", summary));
516 + markdown.push_str(&format!("## 📋 Session Summary\n\n{summary}\n\n"));
|
[warning] 497-497: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:497:24
|
497 | markdown.push_str(&format!("Project: {}\n", project_name));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
497 - markdown.push_str(&format!("Project: {}\n", project_name));
497 + markdown.push_str(&format!("Project: {project_name}\n"));
|
[warning] 496-496: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:496:24
|
496 | markdown.push_str(&format!("# Claude Code Session: {}\n\n", session_id));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
496 - markdown.push_str(&format!("# Claude Code Session: {}\n\n", session_id));
496 + markdown.push_str(&format!("# Claude Code Session: {session_id}\n\n"));
|
[warning] 479-479: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:479:13
|
479 | format!("attachment; filename="{}"", filename),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
479 - format!("attachment; filename="{}"", filename),
479 + format!("attachment; filename="{filename}""),
|
[warning] 472-472: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:472:20
|
472 | let filename = format!("{}-{}.md", project_name, session_id);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
472 - let filename = format!("{}-{}.md", project_name, session_id);
472 + let filename = format!("{project_name}-{session_id}.md");
|
[warning] 455-455: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:455:15
|
455 | .join(format!("{}.jsonl", session_id));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
455 - .join(format!("{}.jsonl", session_id));
455 + .join(format!("{session_id}.jsonl"));
|
src/tool_renderer.rs
[warning] 1357-1357: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/tool_renderer.rs:1357:39
|
1357 | OutputFormat::Markdown => format!("{}
", text),
| ^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
1357 - OutputFormat::Markdown => format!("{}
", text),
1357 + OutputFormat::Markdown => format!("{text}
"),
|
[warning] 1350-1350: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/tool_renderer.rs:1350:39
|
1350 | OutputFormat::Markdown => format!("{}", text),
| ^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
1350 - OutputFormat::Markdown => format!("{}", text),
1350 + OutputFormat::Markdown => format!("{text}"),
|
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Generate Coverage Report
- GitHub Check: Run cargo test
🔇 Additional comments (4)
src/lib.rs (1)
5-5
: LGTM!The new imports and module declaration are appropriate for the markdown export functionality.
Also applies to: 10-10, 23-23
src/main.rs (1)
128-131
: LGTM!The new markdown export route is properly registered and follows REST conventions.
src/tool_renderer.rs (2)
1-176
: Excellent architecture for extensible tool rendering.The registry-based design with the
ToolHandler
trait provides a clean, extensible architecture. The separation of concerns between tool discovery, rendering, and formatting is well thought out.
1371-1414
: LGTM!The default handler provides a sensible fallback for unknown tools by preserving all information in JSON format.
) | ||
.body(Body::from(markdown_content)) | ||
.unwrap()) | ||
} | ||
|
||
pub fn generate_markdown_export( | ||
entries: &[LogEntry], | ||
session_id: &str, | ||
project_name: &str, | ||
) -> String { | ||
use tool_renderer::{OutputFormat, RenderContext, ToolRenderer}; | ||
|
||
let mut markdown = String::new(); | ||
let renderer = ToolRenderer::new(); | ||
|
||
// Header | ||
markdown.push_str(&format!("# Claude Code Session: {}\n\n", session_id)); | ||
markdown.push_str(&format!("**Project:** {}\n", project_name)); | ||
|
||
if let Some(first_entry) = entries.first() { | ||
if let Some(timestamp) = &first_entry.timestamp { | ||
markdown.push_str(&format!( | ||
"**Date:** {}\n", | ||
timestamp.format("%Y-%m-%d %H:%M:%S UTC") | ||
)); | ||
} | ||
} | ||
|
||
markdown.push_str("\n---\n\n"); | ||
|
||
let mut current_tool_use: Option<&LogEntry> = None; | ||
|
||
for entry in entries { | ||
match entry.entry_type.as_deref() { | ||
Some("summary") => { | ||
if let Some(summary) = &entry.summary { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Extract common log reading logic to reduce duplication.
This function duplicates the log reading and parsing logic from get_session_logs
(lines 427-443). Consider extracting a shared helper function.
Add a helper function to reduce duplication:
+async fn read_session_entries(
+ projects_dir: &Path,
+ project_name: &str,
+ session_id: &str,
+) -> Result<Vec<LogEntry>, StatusCode> {
+ let log_path = projects_dir
+ .join(project_name)
+ .join(format!("{session_id}.jsonl"));
+
+ if !log_path.exists() {
+ return Err(StatusCode::NOT_FOUND);
+ }
+
+ let content = fs::read_to_string(&log_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+ let mut entries = Vec::new();
+ for line in content.lines() {
+ if let Ok(entry) = serde_json::from_str::<LogEntry>(line) {
+ entries.push(entry);
+ }
+ }
+
+ Ok(entries)
+}
Then update both functions to use it:
pub async fn get_session_logs(
Path((project_name, session_id)): Path<(String, String)>,
State(state): State<AppState>,
) -> Result<Json<Vec<LogEntry>>, StatusCode> {
- let log_path = state
- .projects_dir
- .join(&project_name)
- .join(format!("{}.jsonl", session_id));
-
- if !log_path.exists() {
- return Err(StatusCode::NOT_FOUND);
- }
-
- let content = fs::read_to_string(&log_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
-
- let mut entries = Vec::new();
- for line in content.lines() {
- if let Ok(entry) = serde_json::from_str::<LogEntry>(line) {
- entries.push(entry);
- }
- }
-
- Ok(Json(entries))
+ let entries = read_session_entries(&state.projects_dir, &project_name, &session_id).await?;
+ Ok(Json(entries))
}
pub async fn export_session_markdown(
Path((project_name, session_id)): Path<(String, String)>,
State(state): State<AppState>,
) -> Result<Response, StatusCode> {
- let log_path = state
- .projects_dir
- .join(&project_name)
- .join(format!("{}.jsonl", session_id));
-
- if !log_path.exists() {
- return Err(StatusCode::NOT_FOUND);
- }
-
- let content = fs::read_to_string(&log_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
-
- let mut entries = Vec::new();
- for line in content.lines() {
- if let Ok(entry) = serde_json::from_str::<LogEntry>(line) {
- entries.push(entry);
- }
- }
-
+ let entries = read_session_entries(&state.projects_dir, &project_name, &session_id).await?;
let markdown_content = generate_markdown_export(&entries, &session_id, &project_name);
Also apply the static analysis suggestions for string formatting in lines 455, 472, and 479.
🧰 Tools
🪛 GitHub Check: clippy
[warning] 479-479: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:479:13
|
479 | format!("attachment; filename="{}"", filename),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
479 - format!("attachment; filename="{}"", filename),
479 + format!("attachment; filename="{filename}""),
|
[warning] 472-472: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:472:20
|
472 | let filename = format!("{}-{}.md", project_name, session_id);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
472 - let filename = format!("{}-{}.md", project_name, session_id);
472 + let filename = format!("{project_name}-{session_id}.md");
|
[warning] 455-455: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:455:15
|
455 | .join(format!("{}.jsonl", session_id));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
455 - .join(format!("{}.jsonl", session_id));
455 + .join(format!("{session_id}.jsonl"));
|
🤖 Prompt for AI Agents
In src/lib.rs around lines 427 to 483, the log reading and parsing logic is
duplicated in export_session_markdown and get_session_logs. Extract this common
logic into a new helper function that takes project_name and session_id, reads
the log file, parses each line into LogEntry objects, and returns the entries or
an error. Then update both export_session_markdown and get_session_logs to call
this helper function instead of duplicating the code. Additionally, revise the
string formatting in lines 455, 472, and 479 to use more efficient or idiomatic
Rust formatting methods as suggested by static analysis.
src/lib.rs
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Potential duplicate timestamps and opportunities for refactoring.
Issues identified:
- When a
toolUse
entry is immediately followed by itstoolResult
, timestamps might be duplicated (lines 608-610) since timestamps are added after every entry. - The function is quite long and would benefit from extraction into smaller helpers.
- Format strings can use inline interpolation as suggested by static analysis.
Consider skipping the timestamp for toolUse
entries when they're paired with results:
Some("toolUse") => {
current_tool_use = Some(entry);
if let Some(message) = &entry.message {
if let Some(tool_name) = message.get("name").and_then(|n| n.as_str()) {
if let Some(input) = message.get("input") {
// ... existing rendering code ...
markdown.push_str(&rendered.header);
markdown.push_str(&rendered.input);
}
}
}
+ // Skip adding timestamp here - it will be added with the result
+ continue;
}
For better maintainability, consider extracting helper functions:
fn render_summary_entry(entry: &LogEntry, markdown: &mut String) {
if let Some(summary) = &entry.summary {
markdown.push_str(&format!("## 📋 Session Summary\n\n{summary}\n\n"));
}
}
fn render_user_entry(entry: &LogEntry, markdown: &mut String) {
if let Some(message) = &entry.message {
if let Some(content) = message.get("content").and_then(|c| c.as_str()) {
markdown.push_str(&format!("## 👤 User\n\n{content}\n\n"));
}
}
}
// Similar helpers for other entry types...
🧰 Tools
🪛 GitHub Check: clippy
[warning] 529-529: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:529:44
|
529 | markdown.push_str(&format!("## 🤖 Assistant\n\n{}\n\n", content));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
529 - markdown.push_str(&format!("## 🤖 Assistant\n\n{}\n\n", content));
529 + markdown.push_str(&format!("## 🤖 Assistant\n\n{content}\n\n"));
|
[warning] 522-522: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:522:44
|
522 | markdown.push_str(&format!("## 👤 User\n\n{}\n\n", content));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
522 - markdown.push_str(&format!("## 👤 User\n\n{}\n\n", content));
522 + markdown.push_str(&format!("## 👤 User\n\n{content}\n\n"));
|
[warning] 516-516: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:516:40
|
516 | markdown.push_str(&format!("## 📋 Session Summary\n\n{}\n\n", summary));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
516 - markdown.push_str(&format!("## 📋 Session Summary\n\n{}\n\n", summary));
516 + markdown.push_str(&format!("## 📋 Session Summary\n\n{summary}\n\n"));
|
[warning] 497-497: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:497:24
|
497 | markdown.push_str(&format!("Project: {}\n", project_name));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
497 - markdown.push_str(&format!("Project: {}\n", project_name));
497 + markdown.push_str(&format!("Project: {project_name}\n"));
|
[warning] 496-496: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:496:24
|
496 | markdown.push_str(&format!("# Claude Code Session: {}\n\n", session_id));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
496 - markdown.push_str(&format!("# Claude Code Session: {}\n\n", session_id));
496 + markdown.push_str(&format!("# Claude Code Session: {session_id}\n\n"));
|
🤖 Prompt for AI Agents
In src/lib.rs from lines 485 to 614, the generate_markdown_export function has
duplicated timestamps for toolUse entries followed by toolResult entries and is
quite long. To fix this, modify the timestamp appending logic to skip adding a
timestamp after toolUse entries when they are immediately followed by a
toolResult entry to avoid duplication. Additionally, refactor the function by
extracting rendering logic for each entry type (summary, user, assistant,
toolUse, toolResult) into separate helper functions that take a LogEntry and
mutable markdown string reference, using inline string interpolation for
formatting. This will improve readability and maintainability.
let priority_icon = match priority { | ||
"high" => "🟢", | ||
"medium" => "🟠", | ||
"low" => "🔴", | ||
_ => "⚪", | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Priority icons appear to be inverted.
The priority icons seem counterintuitive:
- High priority = 🟢 (green)
- Low priority = 🔴 (red)
Typically, red indicates high priority/urgency while green indicates low priority/completed.
Consider swapping the icons:
let priority_icon = match priority {
- "high" => "🟢",
+ "high" => "🔴",
"medium" => "🟠",
- "low" => "🔴",
+ "low" => "🟢",
_ => "⚪",
};
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
let priority_icon = match priority { | |
"high" => "🟢", | |
"medium" => "🟠", | |
"low" => "🔴", | |
_ => "⚪", | |
}; | |
let priority_icon = match priority { | |
"high" => "🔴", | |
"medium" => "🟠", | |
"low" => "🟢", | |
_ => "⚪", | |
}; |
🤖 Prompt for AI Agents
In src/tool_renderer.rs around lines 474 to 479, the priority icons are inverted
with high priority showing green and low priority showing red, which is
counterintuitive. Swap the icons so that "high" priority maps to the red circle
emoji, "medium" to orange, and "low" to green, aligning with common color
conventions for urgency.
sessionItem.onclick = (e) => { | ||
// Only trigger if we didn't click on an export button | ||
if (!e.target.classList.contains('small-export-btn')) { | ||
loadSession(projectName, session.id, true); | ||
} | ||
}; | ||
|
||
sessionItem.innerHTML = ` | ||
<div class="session-title">${session.summary}</div> | ||
<div class="session-meta"> | ||
<span class="timestamp">${new Date(session.timestamp).toLocaleString()}</span> | ||
<span>${session.message_count} entries</span> | ||
<div class="session-meta-left"> | ||
<span class="timestamp">${new Date(session.timestamp).toLocaleString()}</span> | ||
<span>${session.message_count} entries</span> | ||
</div> | ||
<div class="session-actions"> | ||
<a href="/api/projects/${encodeURIComponent(projectName)}/sessions/${encodeURIComponent(session.id)}/export/markdown" | ||
class="small-export-btn" | ||
download="${projectName}-${session.id}.md" | ||
onclick="event.stopPropagation()">MD</a> | ||
</div> | ||
</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential XSS/Content-injection via unsanitised download
filename
projectName
and session.id
are inserted verbatim into the download
attribute.
If either of these strings ever contains quotes, angle brackets, line-breaks, etc. the generated HTML becomes syntactically unsafe and opens the door for DOM-based injection.
download="${projectName}-${session.id}.md"
Small change, big win: escape/normalise the filename before embedding it.
- download="${projectName}-${session.id}.md"
+ download="${encodeURIComponent(projectName)}-${encodeURIComponent(session.id)}.md"
At minimum, run them through a conservative whitelist (alphanumerics, dash, underscore).
🤖 Prompt for AI Agents
In static/index.html around lines 2468 to 2488, the download attribute for the
export link uses projectName and session.id directly, which can lead to XSS or
content injection if these values contain unsafe characters. To fix this,
sanitize projectName and session.id by allowing only alphanumeric characters,
dashes, and underscores before embedding them in the download attribute. This
can be done by applying a whitelist filter or escaping unsafe characters to
ensure the generated HTML is safe and syntactically correct.
function exportSession(format) { | ||
if (!currentProject || !currentSession) { | ||
alert('No session currently loaded'); | ||
return; | ||
} | ||
|
||
const url = `/api/projects/${encodeURIComponent(currentProject)}/sessions/${encodeURIComponent(currentSession)}/export/${format}`; | ||
const filename = `${currentProject}-${currentSession}.md`; | ||
|
||
// Create a temporary link and trigger download | ||
const link = document.createElement('a'); | ||
link.href = url; | ||
link.download = filename; | ||
document.body.appendChild(link); | ||
link.click(); | ||
document.body.removeChild(link); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
exportSession()
hard-codes .md
extension & lacks input validation
-
filename
is always suffixed with.md
, yetformat
is a free argument.
CallingexportSession('json')
would still download a file ending in.md
. -
format
is echoed straight into the URL without validation – a rogue caller could
craftexportSession('../../etc/passwd')
.
The backend might be safe, but validating client-side prevents pointless requests.
function exportSession(format) {
- if (!currentProject || !currentSession) { … }
+ const allowed = new Set(['markdown']);
+ if (!allowed.has(format)) {
+ console.warn(`Unsupported export format: ${format}`);
+ return;
+ }
const url =
- `/api/projects/${encodeURIComponent(currentProject)}/sessions/${encodeURIComponent(currentSession)}/export/${format}`;
- const filename = `${currentProject}-${currentSession}.md`;
+ `/api/projects/${encodeURIComponent(currentProject)}/sessions/${encodeURIComponent(currentSession)}/export/${format}`;
+ const filename = `${currentProject}-${currentSession}.${format === 'markdown' ? 'md' : format}`;
This keeps the front-end consistent and closes a trivial injection vector.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In static/index.html around lines 2501 to 2517, the exportSession function
always appends a .md extension to the filename regardless of the format
argument, and it does not validate the format input before using it in the URL.
To fix this, validate the format argument against a whitelist of allowed formats
(e.g., 'md', 'json', 'txt') before proceeding. Then, dynamically set the
filename extension based on the validated format. This prevents invalid or
malicious format values from being used and ensures the downloaded file has the
correct extension.
Major improvements to markdown export functionality: **Core Fix - Empty markdown exports**: - Fixed markdown export function that was looking for non-existent "toolUse" and "toolResult" entry types - Rewrote to parse assistant messages for nested tool_use items matching real Claude Code JSONL structure - Implemented HashMap-based tool use/result matching via tool_use_id **Tool Rendering Consistency**: - Added render_data_tool_output helper function for consistent edge case handling - Refactored BashHandler, ReadHandler, LSHandler, GrepHandler, GlobHandler, DefaultHandler to use new helper - Enhanced WebFetchHandler, WebSearchHandler, TaskHandler, NotebookReadHandler with better empty content handling - All 13+ tool handlers now consistently handle empty/missing content **Testing and Validation**: - Created comprehensive audit system to verify markdown/HTML consistency - Built test cases covering normal, empty, and missing content fields - Ensured format_utils helpers work consistently across output formats The markdown export now properly renders the same rich tool content that appears in the HTML web interface, resolving the issue where tool sections were completely missing from markdown exports. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/tool_renderer.rs (1)
450-455
: Priority icons appear to be inverted.The priority icons seem counterintuitive:
- High priority = 🟢 (green)
- Low priority = 🔴 (red)
Typically, red indicates high priority/urgency while green indicates low priority/completed.
Consider swapping the icons:
let priority_icon = match priority { - "high" => "🟢", + "high" => "🔴", "medium" => "🟠", - "low" => "🔴", + "low" => "🟢", _ => "⚪", };src/lib.rs (1)
448-483
: Extract common log reading logic to reduce duplication.This function duplicates the log reading and parsing logic from
get_session_logs
(lines 427-443). Consider extracting a shared helper function.Add a helper function to reduce duplication:
+async fn read_session_entries( + projects_dir: &Path, + project_name: &str, + session_id: &str, +) -> Result<Vec<LogEntry>, StatusCode> { + let log_path = projects_dir + .join(project_name) + .join(format!("{session_id}.jsonl")); + + if !log_path.exists() { + return Err(StatusCode::NOT_FOUND); + } + + let content = fs::read_to_string(&log_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let mut entries = Vec::new(); + for line in content.lines() { + if let Ok(entry) = serde_json::from_str::<LogEntry>(line) { + entries.push(entry); + } + } + + Ok(entries) +}Then update both functions to use it and apply the string formatting improvements.
🧹 Nitpick comments (3)
src/tool_renderer.rs (3)
149-160
: Remove redundant wrapper method.The
render_tool_with_result
method simply wraps the result inSome()
, but sincerender_tool
always returns a value (never fails), this wrapper adds no value and may confuse users about when it might returnNone
.Consider removing this method entirely:
- /// Render a complete tool interaction (convenience method) - pub fn render_tool_with_result( - &self, - tool_name: &str, - input: &Value, - output: Option<&Value>, - format: OutputFormat, - context: &RenderContext, - ) -> Option<RenderedTool> { - Some(self.render_tool(tool_name, input, output, format, context)) - }
1394-1401
: Apply inline string formatting for cleaner code.The static analysis correctly identifies opportunities to use inline formatting:
pub fn italic(text: &str, format: OutputFormat) -> String { match format { - OutputFormat::Markdown => format!("*{}*", text), + OutputFormat::Markdown => format!("*{text}*"), OutputFormat::Html => format!("<em>{}</em>", html_escape(text)), } } pub fn inline_code(text: &str, format: OutputFormat) -> String { match format { - OutputFormat::Markdown => format!("`{}`", text), + OutputFormat::Markdown => format!("`{text}`"), OutputFormat::Html => format!("<code>{}</code>", html_escape(text)), } }
1452-1476
: Consider expanding test coverage.While the basic tests verify creation and tool support, consider adding tests for:
- Actual rendering output for different tools
- Format-specific output (Markdown vs HTML)
- Edge cases like empty inputs or malformed data
Would you like me to generate comprehensive test cases for the tool renderer?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/lib.rs
(3 hunks)src/tool_renderer.rs
(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/lib.rs (1)
src/tool_renderer.rs (1)
new
(62-72)
🪛 GitHub Check: clippy
src/lib.rs
[warning] 532-532: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:532:48
|
532 | ... markdown.push_str(&format!("## 🤖 Assistant\n\n{}\n\n", content));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
532 - markdown.push_str(&format!("## 🤖 Assistant\n\n{}\n\n", content));
532 + markdown.push_str(&format!("## 🤖 Assistant\n\n{content}\n\n"));
|
[warning] 523-523: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:523:44
|
523 | markdown.push_str(&format!("## 👤 User\n\n{}\n\n", content));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
523 - markdown.push_str(&format!("## 👤 User\n\n{}\n\n", content));
523 + markdown.push_str(&format!("## 👤 User\n\n{content}\n\n"));
|
[warning] 517-517: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:517:40
|
517 | markdown.push_str(&format!("## 📋 Session Summary\n\n{}\n\n", summary));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
517 - markdown.push_str(&format!("## 📋 Session Summary\n\n{}\n\n", summary));
517 + markdown.push_str(&format!("## 📋 Session Summary\n\n{summary}\n\n"));
|
[warning] 497-497: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:497:24
|
497 | markdown.push_str(&format!("Project: {}\n", project_name));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
497 - markdown.push_str(&format!("Project: {}\n", project_name));
497 + markdown.push_str(&format!("Project: {project_name}\n"));
|
[warning] 496-496: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:496:24
|
496 | markdown.push_str(&format!("# Claude Code Session: {}\n\n", session_id));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
496 - markdown.push_str(&format!("# Claude Code Session: {}\n\n", session_id));
496 + markdown.push_str(&format!("# Claude Code Session: {session_id}\n\n"));
|
[warning] 479-479: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:479:13
|
479 | format!("attachment; filename="{}"", filename),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
479 - format!("attachment; filename="{}"", filename),
479 + format!("attachment; filename="{filename}""),
|
[warning] 472-472: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:472:20
|
472 | let filename = format!("{}-{}.md", project_name, session_id);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
472 - let filename = format!("{}-{}.md", project_name, session_id);
472 + let filename = format!("{project_name}-{session_id}.md");
|
[warning] 455-455: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:455:15
|
455 | .join(format!("{}.jsonl", session_id));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
455 - .join(format!("{}.jsonl", session_id));
455 + .join(format!("{session_id}.jsonl"));
|
src/tool_renderer.rs
[warning] 1401-1401: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/tool_renderer.rs:1401:39
|
1401 | OutputFormat::Markdown => format!("{}
", text),
| ^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
1401 - OutputFormat::Markdown => format!("{}
", text),
1401 + OutputFormat::Markdown => format!("{text}
"),
|
[warning] 1394-1394: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/tool_renderer.rs:1394:39
|
1394 | OutputFormat::Markdown => format!("{}", text),
| ^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
1394 - OutputFormat::Markdown => format!("{}", text),
1394 + OutputFormat::Markdown => format!("{text}"),
|
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Generate Coverage Report
- GitHub Check: Run cargo test
🔇 Additional comments (4)
src/tool_renderer.rs (2)
1-60
: Well-structured foundation for the tool rendering system.The file structure, trait definitions, and data models are well-designed. The use of trait-based polymorphism for different tool handlers provides excellent extensibility.
213-319
: Solid implementations for file operation handlers.The ReadHandler and EditHandler implementations properly handle optional fields, format-specific rendering, and edge cases like empty content.
src/lib.rs (2)
5-23
: Import additions are appropriate for the new export functionality.The added imports and public module declaration properly support the markdown export feature.
536-579
: Well-structured handling of complex assistant message formats.The implementation correctly handles both simple text content and complex tool_use arrays within assistant messages, with proper tracking of pending tool uses.
src/lib.rs
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider refactoring for maintainability and fixing timestamp duplication.
Issues identified:
- The function is quite long and would benefit from extraction into smaller helpers
- Timestamps might be duplicated when processing tool results immediately after tool uses
- String formatting can use inline interpolation
Consider extracting helper functions and fixing the timestamp logic:
fn render_summary_entry(entry: &LogEntry, markdown: &mut String) {
if let Some(summary) = &entry.summary {
markdown.push_str(&format!("## 📋 Session Summary\n\n{summary}\n\n"));
}
}
fn render_user_entry(entry: &LogEntry, markdown: &mut String) {
if let Some(message) = &entry.message {
if let Some(content) = message.get("content").and_then(|c| c.as_str()) {
markdown.push_str(&format!("## 👤 User\n\n{content}\n\n"));
}
}
}
// In the main loop, track whether we just processed a tool use
let mut skip_next_timestamp = false;
// After processing entries, conditionally add timestamp:
if !skip_next_timestamp {
if let Some(timestamp) = &entry.timestamp {
markdown.push_str(&format!("*Time: {}*\n\n", timestamp.format("%H:%M:%S")));
}
}
skip_next_timestamp = matches!(entry.entry_type.as_deref(), Some("toolUse"));
🧰 Tools
🪛 GitHub Check: clippy
[warning] 532-532: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:532:48
|
532 | ... markdown.push_str(&format!("## 🤖 Assistant\n\n{}\n\n", content));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
532 - markdown.push_str(&format!("## 🤖 Assistant\n\n{}\n\n", content));
532 + markdown.push_str(&format!("## 🤖 Assistant\n\n{content}\n\n"));
|
[warning] 523-523: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:523:44
|
523 | markdown.push_str(&format!("## 👤 User\n\n{}\n\n", content));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
523 - markdown.push_str(&format!("## 👤 User\n\n{}\n\n", content));
523 + markdown.push_str(&format!("## 👤 User\n\n{content}\n\n"));
|
[warning] 517-517: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:517:40
|
517 | markdown.push_str(&format!("## 📋 Session Summary\n\n{}\n\n", summary));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
517 - markdown.push_str(&format!("## 📋 Session Summary\n\n{}\n\n", summary));
517 + markdown.push_str(&format!("## 📋 Session Summary\n\n{summary}\n\n"));
|
[warning] 497-497: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:497:24
|
497 | markdown.push_str(&format!("Project: {}\n", project_name));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
497 - markdown.push_str(&format!("Project: {}\n", project_name));
497 + markdown.push_str(&format!("Project: {project_name}\n"));
|
[warning] 496-496: variables can be used directly in the format!
string
warning: variables can be used directly in the format!
string
--> src/lib.rs:496:24
|
496 | markdown.push_str(&format!("# Claude Code Session: {}\n\n", session_id));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
help: change this to
|
496 - markdown.push_str(&format!("# Claude Code Session: {}\n\n", session_id));
496 + markdown.push_str(&format!("# Claude Code Session: {session_id}\n\n"));
|
🤖 Prompt for AI Agents
In src/lib.rs between lines 485 and 615, the generate_markdown_export function
is too long and has duplicated timestamps when processing tool uses and their
results. Refactor by extracting rendering logic for summary and user entries
into separate helper functions that take a LogEntry and mutable markdown string.
Also, introduce a flag to track if the last processed entry was a tool use and
skip adding a timestamp after tool use entries to avoid duplication. Replace
explicit format! calls with inline string interpolation for cleaner code. Adjust
the main loop to set and check this flag accordingly before appending
timestamps.
Pull Request: Session Markdown Export Feature
This pull request introduces a new feature that allows users to export their session logs in Markdown format, enhancing the usability for users interested in documenting their activities. It covers back-end logic for processing exports and front-end update for triggering downloads.
Changes Made
In total, several files were modified to integrate this feature. Below is an overview of the changes:
src/lib.rs
export_session_markdown
that handles the logic for exporting session logs to Markdown format.generate_markdown_export
function to create the actual Markdown content from session entries.src/main.rs
--export
,--export-all
,--export-projects
,--export-dir
) to manage various export scenarios.src/tool_renderer.rs
static/index.html
Why These Changes Are Necessary
The primary goal behind these enhancements is improving session documentation for users. Many users prefer Markdown for its simplicity and wide support across text editors and platforms. Exporting directly from the application saves time and eliminates manual processes, improving overall user experience. The tool rendering aspect is also designed to support coherent generation of Markdown documents, avoiding any clutter.
Of course, while trying to get the exports up and running, I encountered a timeline bug that Elvis probably would have fixed in a jiffy if he were around. Seriously, who does that guy think he is, rockin' and rollin' without checking his code?
Summary of Changes
src/lib.rs
: Introduced session export functions.src/main.rs
: Enhanced CLI for export operations.src/tool_renderer.rs
: Added tool rendering logic dedicated to Markdown.static/index.html
: Updated UI to support exporting functionality.Closed Issues
Here's a little haiku to sum up the changes:
Session logs now saved,
In Markdown format with pride,
No Elvis in sight.
Summary by CodeRabbit
New Features
Enhancements