From d30087a27fbf22b2433b65e5b4925d94b27e96d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Mar 2026 18:15:20 +0000 Subject: [PATCH 1/2] Use file creation date instead of last modified date for filenames When the session JSON lacks a dateCreated field, fall back to the file's creation time rather than its last modified time. This prevents duplicate output files when revisiting sessions and provides more intuitive chronological ordering based on when work started. Bump version to 0.14.0. https://claude.ai/code/session_01YQGzNyfmvVtAe7XAistLRA --- Cargo.toml | 2 +- README.md | 8 ++++---- pyproject.toml | 2 +- python/continue_transcripts/__init__.py | 2 +- src/main.rs | 16 ++++++++-------- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a5c602a..8fc3210 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "continue-transcripts" -version = "0.13.1" +version = "0.14.0" edition = "2021" description = "Convert continue.dev session files to readable HTML transcripts" license = "MIT" diff --git a/README.md b/README.md index d430e36..8aba57f 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Install globally so the `continue-transcripts` command is always available: ```sh uv tool install continue-transcripts \ --no-index \ - --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.13.1 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.14.0 ``` The `continue-transcripts` command is then available on your `PATH`. @@ -83,7 +83,7 @@ To upgrade later (update the version in the URL): ```sh uv tool install --upgrade continue-transcripts \ --no-index \ - --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.13.1 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.14.0 ``` To uninstall: @@ -100,7 +100,7 @@ Run without installing: uvx \ --no-index \ --from continue-transcripts \ - --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.13.1 \ + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.14.0 \ continue-transcripts ./sessions ``` @@ -115,7 +115,7 @@ uv venv .venv source .venv/bin/activate uv pip install continue-transcripts \ --no-index \ - --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.13.1 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.14.0 ``` ### Building from source diff --git a/pyproject.toml b/pyproject.toml index 32d4d9f..fd1c61a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "continue-transcripts" -version = "0.13.1" +version = "0.14.0" description = "Convert continue.dev session files to readable HTML transcripts" readme = "README.md" license = { text = "MIT" } diff --git a/python/continue_transcripts/__init__.py b/python/continue_transcripts/__init__.py index 154bfb9..2574675 100644 --- a/python/continue_transcripts/__init__.py +++ b/python/continue_transcripts/__init__.py @@ -1,3 +1,3 @@ """continue-transcripts - Convert continue.dev session files to readable HTML transcripts.""" -__version__ = "0.13.1" +__version__ = "0.14.0" diff --git a/src/main.rs b/src/main.rs index b02df4e..5df8f6d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1190,10 +1190,10 @@ fn collect_tool_names(history: &[ChatHistoryItem]) -> Vec { names } -fn file_modified_date(path: &Path) -> Option { +fn file_created_date(path: &Path) -> Option { let meta = fs::metadata(path).ok()?; - let modified = meta.modified().ok()?; - let dt: chrono::DateTime = modified.into(); + let created = meta.created().ok()?; + let dt: chrono::DateTime = created.into(); Some(dt.format("%Y-%m-%dT%H:%M:%S").to_string()) } @@ -1907,7 +1907,7 @@ fn render_session(session: &Session, source_path: Option<&Path>) -> String { .date_created .as_deref() .map(|s| format_date_display(s)) - .or_else(|| source_path.and_then(file_modified_date)) + .or_else(|| source_path.and_then(file_created_date)) .unwrap_or_else(|| "Unknown date".to_string()); let workspace_decoded = decode_path(&session.workspace_directory); @@ -3637,9 +3637,9 @@ fn main() { .as_deref() .and_then(parse_session_date) .or_else(|| { - // Fall back to file modification time + // Fall back to file creation time file.metadata().ok() - .and_then(|m| m.modified().ok()) + .and_then(|m| m.created().ok()) .map(|t| { let dt: chrono::DateTime = t.into(); dt.date_naive() @@ -3677,7 +3677,7 @@ fn main() { .date_created .clone() .or_else(|| sessions_meta.get(&session.session_id).cloned()) - .or_else(|| file_modified_date(file.as_path())) + .or_else(|| file_created_date(file.as_path())) .unwrap_or_default(); ProcessResult::Success(html, title, date, file.clone()) @@ -4090,7 +4090,7 @@ mod tests { extract_datetime_prefix("2025-01-01"), Some("2025-01-01".to_string()) ); - // Format produced by file_modified_date fallback (no timezone) + // Format produced by file_created_date fallback (no timezone) assert_eq!( extract_datetime_prefix("2025-06-15T10:30:00"), Some("2025-06-15_1030".to_string()) From 907e2cd2ef9d760d578f782fc76acbb93043cb31 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Mar 2026 18:17:07 +0000 Subject: [PATCH 2/2] Fix clippy warnings, apply rustfmt, document lint requirements Resolve three clippy warnings: use char array for split, simplify closure to function reference, replace map_or with is_some_and. Apply rustfmt across the codebase. Add linting/formatting instructions to CLAUDE.md so clippy and rustfmt are run before committing. https://claude.ai/code/session_01YQGzNyfmvVtAe7XAistLRA --- CLAUDE.md | 11 ++ src/main.rs | 314 ++++++++++++++++++++++++++-------------------------- 2 files changed, 167 insertions(+), 158 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5d406c8..e39c295 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,6 +44,17 @@ cargo test ``` Tests use fixture files in `tests/fixtures/`: + +### Linting and formatting + +Before committing, ensure clippy and rustfmt are clean: + +```sh +cargo clippy +cargo fmt --check +``` + +To auto-fix formatting: `cargo fmt` - `sample-session.json` — basic conversation - `sample-session-rich.json` — complex session with system prompt, tool calls, ANSI output, thinking blocks diff --git a/src/main.rs b/src/main.rs index 5df8f6d..581de1f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -140,7 +140,6 @@ struct SystemMessageDescription { example_args: Option>>, } - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct ChatMessage { @@ -297,9 +296,8 @@ fn highlight_code(lang: &str, code: &str) -> Option { // --------------------------------------------------------------------------- fn markdown_to_html(md: &str) -> String { - let options = Options::ENABLE_TABLES - | Options::ENABLE_STRIKETHROUGH - | Options::ENABLE_TASKLISTS; + let options = + Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TASKLISTS; let parser = MdParser::new_ext(md, options); let mut html_output = String::new(); @@ -492,9 +490,15 @@ fn ansi_to_html(input: &str) -> String { // Find the terminating letter let start = i + 2; let mut end = start; - while end < len && bytes[end] != b'm' && bytes[end] != b'A' && bytes[end] != b'B' - && bytes[end] != b'C' && bytes[end] != b'D' && bytes[end] != b'H' - && bytes[end] != b'J' && bytes[end] != b'K' + while end < len + && bytes[end] != b'm' + && bytes[end] != b'A' + && bytes[end] != b'B' + && bytes[end] != b'C' + && bytes[end] != b'D' + && bytes[end] != b'H' + && bytes[end] != b'J' + && bytes[end] != b'K' { end += 1; } @@ -508,10 +512,8 @@ fn ansi_to_html(input: &str) -> String { if params.is_empty() || params == "0" { style.reset(); } else { - let codes: Vec = params - .split(';') - .filter_map(|s| s.parse().ok()) - .collect(); + let codes: Vec = + params.split(';').filter_map(|s| s.parse().ok()).collect(); let mut ci = 0; while ci < codes.len() { match codes[ci] { @@ -527,22 +529,18 @@ fn ansi_to_html(input: &str) -> String { 23 => style.italic = false, 24 => style.underline = false, 30..=37 => { - style.fg = - Some(FG_COLORS[(codes[ci] - 30) as usize].to_string()); + style.fg = Some(FG_COLORS[(codes[ci] - 30) as usize].to_string()); } 39 => style.fg = None, 40..=47 => { - style.bg = - Some(BG_COLORS[(codes[ci] - 40) as usize].to_string()); + style.bg = Some(BG_COLORS[(codes[ci] - 40) as usize].to_string()); } 49 => style.bg = None, 90..=97 => { - style.fg = - Some(BRIGHT_FG[(codes[ci] - 90) as usize].to_string()); + style.fg = Some(BRIGHT_FG[(codes[ci] - 90) as usize].to_string()); } 100..=107 => { - style.bg = - Some(BG_COLORS[(codes[ci] - 100) as usize].to_string()); + style.bg = Some(BG_COLORS[(codes[ci] - 100) as usize].to_string()); } // 256-color: 38;5;N or 48;5;N — skip for now 38 | 48 => { @@ -708,7 +706,9 @@ fn render_tool_args(name: &str, arguments: &str) -> String { } // Multiple keys — render as key: value pairs - let all_simple = obj.values().all(|v| v.is_string() || v.is_number() || v.is_boolean()); + let all_simple = obj + .values() + .all(|v| v.is_string() || v.is_number() || v.is_boolean()); if all_simple && obj.len() <= 8 { let mut html = String::new(); html.push_str("
"); @@ -793,7 +793,9 @@ fn extract_copyable_command(tool_name: &str, arguments: &str) -> Option let parsed: serde_json::Value = serde_json::from_str(arguments).ok()?; let obj = parsed.as_object()?; if tool_name == "Bash" || tool_name == "bash" || obj.contains_key("command") { - obj.get("command").and_then(|v| v.as_str()).map(|s| s.to_string()) + obj.get("command") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) } else { None } @@ -1018,11 +1020,7 @@ fn render_message( /// `tool_name` is the name of the tool call this result responds to (if known). /// `tool_args` is the JSON arguments string from the corresponding tool call, /// used to infer file extensions for syntax highlighting. -fn render_tool_result_inline( - item: &ChatHistoryItem, - tool_name: &str, - tool_args: &str, -) -> String { +fn render_tool_result_inline(item: &ChatHistoryItem, tool_name: &str, tool_args: &str) -> String { let raw_content = item.message.content.text(); let content_text = decode_file_uris_in_text(&raw_content); let label = if tool_name.is_empty() { @@ -1144,7 +1142,9 @@ fn extract_tool_descriptions(system_content: &str) -> Vec<(String, String, Strin while i < lines.len() { let next = lines[i].trim(); // Stop at next ### or ## header - if next.starts_with("### ") || (next.starts_with("## ") && !next.starts_with("### ")) { + if next.starts_with("### ") + || (next.starts_with("## ") && !next.starts_with("### ")) + { break; } desc_lines.push(lines[i]); @@ -1306,9 +1306,7 @@ fn extract_tool_defs(session: &Session) -> Vec { system_message_prefix: smd .as_ref() .map_or(String::new(), |s| s.prefix.clone()), - example_args: smd - .as_ref() - .and_then(|s| s.example_args.clone()), + example_args: smd.as_ref().and_then(|s| s.example_args.clone()), default_tool_policy: tool_def.default_tool_policy.clone(), parameters: func.parameters.clone(), }); @@ -1365,10 +1363,8 @@ fn render_tools_reference_from_defs( } // Build lookup from structured tool defs - let def_map: std::collections::HashMap<&str, &ExtractedTool> = tool_defs - .iter() - .map(|t| (t.name.as_str(), t)) - .collect(); + let def_map: std::collections::HashMap<&str, &ExtractedTool> = + tool_defs.iter().map(|t| (t.name.as_str(), t)).collect(); // Build fallback lookup from text-parsed descriptions let text_short_map: std::collections::HashMap<&str, &str> = text_descriptions @@ -1436,7 +1432,9 @@ fn render_tools_reference_from_defs( // Function description (from function.description in the tool schema) if !tool.description.is_empty() { html.push_str("
\n"); - html.push_str("
Function Description
\n"); + html.push_str( + "
Function Description
\n", + ); html.push_str(&format!( " {}\n", markdown_to_html(&tool.description) @@ -1452,7 +1450,9 @@ fn render_tools_reference_from_defs( // System message description (the instruction in the system prompt) if !tool.system_message_prefix.is_empty() { html.push_str("
\n"); - html.push_str("
System Message
\n"); + html.push_str( + "
System Message
\n", + ); html.push_str(&format!( "

{}

\n", encode_text(&tool.system_message_prefix) @@ -1548,9 +1548,7 @@ fn render_parameters_schema(params: &serde_json::Value) -> String { .unwrap_or_default(); html.push_str("
\n"); - html.push_str( - "
Parameters
\n", - ); + html.push_str("
Parameters
\n"); html.push_str("
    \n"); for (prop_name, prop_val) in props { let type_str = prop_val @@ -1574,7 +1572,10 @@ fn render_parameters_schema(params: &serde_json::Value) -> String { if let Some(enum_vals) = prop_val.get("enum").and_then(|e| e.as_array()) { let vals: Vec = enum_vals .iter() - .filter_map(|v| v.as_str().map(|s| format!("{}", encode_text(s)))) + .filter_map(|v| { + v.as_str() + .map(|s| format!("{}", encode_text(s))) + }) .collect(); if !vals.is_empty() { html.push_str(&format!(" [{}]", vals.join(", "))); @@ -1619,7 +1620,9 @@ fn parse_system_prompt_sections(input: &str) -> Vec { && !tag_name.starts_with('/') && !tag_name.starts_with('!') && !tag_name.contains(' ') - && tag_name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') + && tag_name + .chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '_') { // Look for matching closing tag let close_tag = format!("", tag_name); @@ -1650,7 +1653,9 @@ fn parse_system_prompt_sections(input: &str) -> Vec { // and continue searching let next_pos = open_start + 1; // Find the next '<' or end of string - let chunk_end = rest[next_pos..].find('<').map_or(rest.len(), |p| next_pos + p); + let chunk_end = rest[next_pos..] + .find('<') + .map_or(rest.len(), |p| next_pos + p); // Don't emit yet — accumulate into a buffer let chunk = &rest[..chunk_end]; if !chunk.trim().is_empty() { @@ -1680,7 +1685,7 @@ fn parse_system_prompt_sections(input: &str) -> Vec { /// Format an XML tag name for display (e.g. "system-reminder" -> "System Reminder"). fn format_xml_tag_name(tag: &str) -> String { - tag.split(|c: char| c == '-' || c == '_') + tag.split(['-', '_']) .map(|word| { let mut chars = word.chars(); match chars.next() { @@ -1760,7 +1765,10 @@ fn render_session(session: &Session, source_path: Option<&Path>) -> String { render_tools_reference_from_defs(&tool_names, &tool_defs, &text_descriptions); // --- Check if any messages have token usage data --- - let has_any_usage = session.history.iter().any(|item| item.message.usage.is_some()); + let has_any_usage = session + .history + .iter() + .any(|item| item.message.usage.is_some()); // --- Render messages with tool-call/tool-result grouping --- let mut messages_html = String::new(); @@ -1807,15 +1815,10 @@ fn render_session(session: &Session, source_path: Option<&Path>) -> String { } "assistant" => { assistant_count += 1; - let mut msg_html = - render_message(item, running, last_model.as_deref()); + let mut msg_html = render_message(item, running, last_model.as_deref()); // Count how many tool calls this assistant message has - let call_count = item - .message - .tool_calls - .as_ref() - .map_or(0, |c| c.len()); + let call_count = item.message.tool_calls.as_ref().map_or(0, |c| c.len()); if call_count > 0 { // Collect tool call names and args for matching with results @@ -1906,7 +1909,7 @@ fn render_session(session: &Session, source_path: Option<&Path>) -> String { let date_str = session .date_created .as_deref() - .map(|s| format_date_display(s)) + .map(format_date_display) .or_else(|| source_path.and_then(file_created_date)) .unwrap_or_else(|| "Unknown date".to_string()); @@ -3259,9 +3262,12 @@ fn discover_session_files(path: &Path) -> Vec { if path.is_dir() { // Recursively find .json files let pattern = format!("{}/**/*.json", path.display()); - for p in glob::glob(&pattern).expect("Failed to read glob pattern").flatten() { + for p in glob::glob(&pattern) + .expect("Failed to read glob pattern") + .flatten() + { // Skip sessions.json — it contains session metadata, not a session transcript - if p.file_name().map_or(false, |n| n == "sessions.json") { + if p.file_name().is_some_and(|n| n == "sessions.json") { continue; } files.push(p); @@ -3452,14 +3458,7 @@ fn unique_filename( fn is_path_key(key: &str) -> bool { matches!( key, - "file_path" - | "filepath" - | "path" - | "file" - | "directory" - | "dir" - | "notebook_path" - | "cwd" + "file_path" | "filepath" | "path" | "file" | "directory" | "dir" | "notebook_path" | "cwd" ) } @@ -3512,10 +3511,9 @@ fn percent_decode(input: &str) -> String { let mut i = 0; while i < bytes.len() { if bytes[i] == b'%' && i + 2 < bytes.len() { - if let Ok(byte) = u8::from_str_radix( - std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""), - 16, - ) { + if let Ok(byte) = + u8::from_str_radix(std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""), 16) + { decoded.push(byte); i += 3; continue; @@ -3545,20 +3543,29 @@ fn main() { let output_dir = &cli.output; if !input.exists() { - eprintln!("\u{274c} Error: input path does not exist: {}", input.display()); + eprintln!( + "\u{274c} Error: input path does not exist: {}", + input.display() + ); std::process::exit(1); } // Validate date filters let since_date = cli.since.as_deref().map(|s| { parse_date_filter(s).unwrap_or_else(|| { - eprintln!("\u{274c} Invalid --since date: {:?} (expected YYYY-MM-DD)", s); + eprintln!( + "\u{274c} Invalid --since date: {:?} (expected YYYY-MM-DD)", + s + ); std::process::exit(1); }) }); let before_date = cli.before.as_deref().map(|s| { parse_date_filter(s).unwrap_or_else(|| { - eprintln!("\u{274c} Invalid --before date: {:?} (expected YYYY-MM-DD)", s); + eprintln!( + "\u{274c} Invalid --before date: {:?} (expected YYYY-MM-DD)", + s + ); std::process::exit(1); }) }); @@ -3579,7 +3586,10 @@ fn main() { let files = discover_session_files(input); if files.is_empty() { - eprintln!("\u{274c} No session JSON files found at: {}", input.display()); + eprintln!( + "\u{274c} No session JSON files found at: {}", + input.display() + ); std::process::exit(1); } @@ -3602,20 +3612,14 @@ fn main() { let raw = match fs::read_to_string(file) { Ok(s) => s, Err(e) => { - return ProcessResult::Error( - file.clone(), - format!("could not read: {}", e), - ); + return ProcessResult::Error(file.clone(), format!("could not read: {}", e)); } }; let session: Session = match serde_json::from_str(&raw) { Ok(s) => s, Err(e) => { - return ProcessResult::Error( - file.clone(), - format!("could not parse: {}", e), - ); + return ProcessResult::Error(file.clone(), format!("could not parse: {}", e)); } }; @@ -3638,7 +3642,8 @@ fn main() { .and_then(parse_session_date) .or_else(|| { // Fall back to file creation time - file.metadata().ok() + file.metadata() + .ok() .and_then(|m| m.created().ok()) .map(|t| { let dt: chrono::DateTime = t.into(); @@ -3698,7 +3703,11 @@ fn main() { ProcessResult::Success(html, title, date, source) => { let filename = unique_filename( title, - if date.is_empty() { None } else { Some(date.as_str()) }, + if date.is_empty() { + None + } else { + Some(date.as_str()) + }, &mut used_filenames, ); let out_path = output_dir.join(&filename); @@ -3706,12 +3715,10 @@ fn main() { // Skip writing if the file already exists with identical content. // Check file size first to avoid reading large files unnecessarily. let needs_write = match fs::metadata(&out_path) { - Ok(meta) if meta.len() == html.len() as u64 => { - match fs::read(&out_path) { - Ok(existing) => existing != html.as_bytes(), - Err(_) => true, - } - } + Ok(meta) if meta.len() == html.len() as u64 => match fs::read(&out_path) { + Ok(existing) => existing != html.as_bytes(), + Err(_) => true, + }, Ok(_) => true, // size differs — must rewrite Err(_) => true, // file doesn't exist }; @@ -3720,7 +3727,10 @@ fn main() { match fs::File::create(&out_path) { Ok(file) => { let mut writer = BufWriter::new(file); - match writer.write_all(html.as_bytes()).and_then(|_| writer.flush()) { + match writer + .write_all(html.as_bytes()) + .and_then(|_| writer.flush()) + { Ok(_) => { eprintln!(" \u{2705} {}", out_path.display()); index_entries.push((title.clone(), filename, date.clone())); @@ -3738,11 +3748,7 @@ fn main() { } } Err(e) => { - eprintln!( - " \u{274c} Failed to create {}: {}", - out_path.display(), - e - ); + eprintln!(" \u{274c} Failed to create {}: {}", out_path.display(), e); errors.push((source.clone(), format!("write failed: {}", e))); files_errored += 1; } @@ -3774,12 +3780,10 @@ fn main() { let index_html = render_index(&index_entries); let index_path = output_dir.join("index.html"); let index_needs_write = match fs::metadata(&index_path) { - Ok(meta) if meta.len() == index_html.len() as u64 => { - match fs::read(&index_path) { - Ok(existing) => existing != index_html.as_bytes(), - Err(_) => true, - } - } + Ok(meta) if meta.len() == index_html.len() as u64 => match fs::read(&index_path) { + Ok(existing) => existing != index_html.as_bytes(), + Err(_) => true, + }, Ok(_) => true, Err(_) => true, }; @@ -3843,10 +3847,7 @@ fn main() { if files_errored == 0 { eprintln!(" \u{2705} All files processed successfully!"); } else { - eprintln!( - " \u{274c} Completed with {} error(s)", - files_errored - ); + eprintln!(" \u{274c} Completed with {} error(s)", files_errored); std::process::exit(1); } } @@ -4105,14 +4106,8 @@ mod tests { format_date_display("2025-06-15T10:30:00Z"), "Jun 15, 2025 at 10:30 AM" ); - assert_eq!( - format_date_display("2025-01-01"), - "2025-01-01" - ); - assert_eq!( - format_date_display("not-a-date"), - "not-a-date" - ); + assert_eq!(format_date_display("2025-01-01"), "2025-01-01"); + assert_eq!(format_date_display("not-a-date"), "not-a-date"); } // ----------------------------------------------------------------------- @@ -4169,10 +4164,7 @@ mod tests { #[test] fn test_percent_decode_windows_path() { - assert_eq!( - percent_decode("C%3A%5CUsers%5Ctest"), - "C:\\Users\\test" - ); + assert_eq!(percent_decode("C%3A%5CUsers%5Ctest"), "C:\\Users\\test"); } #[test] @@ -4204,7 +4196,10 @@ mod tests { #[test] fn test_decode_path_file_uri() { // Unix file URI - assert_eq!(decode_path("file:///home/user/file.rs"), "/home/user/file.rs"); + assert_eq!( + decode_path("file:///home/user/file.rs"), + "/home/user/file.rs" + ); // Windows file URI assert_eq!(decode_path("file:///C%3A/Users/test"), "C:/Users/test"); // Percent-encoded without file:/// prefix @@ -4233,10 +4228,7 @@ mod tests { "Read /home/user/file.rs done" ); // No file URIs — unchanged - assert_eq!( - decode_file_uris_in_text("No URIs here"), - "No URIs here" - ); + assert_eq!(decode_file_uris_in_text("No URIs here"), "No URIs here"); // URI at end of string (no trailing space) assert_eq!( decode_file_uris_in_text("Edited file:///c%3A/Users/test.rs"), @@ -4470,10 +4462,7 @@ mod tests { assert!(!result.contains("%3A"), "Should not contain encoded colons"); // Single-key (Read tool): file_path with percent-encoded colon - let result = render_tool_args( - "Read", - r#"{"file_path": "/home/user%3Aname/file.rs"}"#, - ); + let result = render_tool_args("Read", r#"{"file_path": "/home/user%3Aname/file.rs"}"#); assert!( result.contains("/home/user:name/file.rs"), "Read file_path should be percent-decoded: {}", @@ -5046,7 +5035,10 @@ mod tests { // Check Bash has correct data let bash = defs.iter().find(|t| t.name == "Bash").unwrap(); assert!(bash.description.contains("Execute a shell command")); - assert_eq!(bash.system_message_prefix, "Execute shell commands and return output"); + assert_eq!( + bash.system_message_prefix, + "Execute shell commands and return output" + ); assert!(bash.parameters.is_some()); // Check parameters have property descriptions @@ -5078,9 +5070,7 @@ mod tests { // Grep has an enum parameter let params = grep.parameters.as_ref().unwrap(); - let output_mode = params - .get("properties") - .and_then(|p| p.get("output_mode")); + let output_mode = params.get("properties").and_then(|p| p.get("output_mode")); assert!(output_mode.is_some()); let enum_vals = output_mode.unwrap().get("enum"); assert!(enum_vals.is_some()); @@ -5104,7 +5094,8 @@ mod tests { #[test] fn test_render_parameters_schema() { - let schema: serde_json::Value = serde_json::from_str(r#"{ + let schema: serde_json::Value = serde_json::from_str( + r#"{ "type": "object", "properties": { "command": { @@ -5117,7 +5108,9 @@ mod tests { } }, "required": ["command"] - }"#).unwrap(); + }"#, + ) + .unwrap(); let html = render_parameters_schema(&schema); assert!(html.contains("Parameters")); @@ -5131,7 +5124,8 @@ mod tests { #[test] fn test_render_parameters_schema_with_enum() { - let schema: serde_json::Value = serde_json::from_str(r#"{ + let schema: serde_json::Value = serde_json::from_str( + r#"{ "type": "object", "properties": { "output_mode": { @@ -5141,7 +5135,9 @@ mod tests { } }, "required": [] - }"#).unwrap(); + }"#, + ) + .unwrap(); let html = render_parameters_schema(&schema); assert!(html.contains("output_mode")); @@ -5189,11 +5185,19 @@ mod tests { // Tool sections should appear in order: Function Description, Parameters, System Message // Search within the tools-reference section (after the CSS which also contains class names) - let tools_ref_start = html.find("class=\"tools-reference\"").expect("tools-reference present"); + let tools_ref_start = html + .find("class=\"tools-reference\"") + .expect("tools-reference present"); let tools_html = &html[tools_ref_start..]; - let func_desc_pos = tools_html.find("Function Description").expect("Function Description present"); - let params_pos = tools_html.find("tool-params-header").expect("Parameters present"); - let sys_msg_pos = tools_html.find("System Message").expect("System Message present"); + let func_desc_pos = tools_html + .find("Function Description") + .expect("Function Description present"); + let params_pos = tools_html + .find("tool-params-header") + .expect("Parameters present"); + let sys_msg_pos = tools_html + .find("System Message") + .expect("System Message present"); assert!( func_desc_pos < params_pos, "Function Description should appear before Parameters" @@ -5331,24 +5335,22 @@ mod tests { session_id: "copy-test".to_string(), title: "Copy Test".to_string(), workspace_directory: "/tmp".to_string(), - history: vec![ - ChatHistoryItem { - message: ChatMessage { - role: "assistant".to_string(), - content: MessageContent::Text("Let me run that.".to_string()), - tool_calls: Some(vec![ToolCallDelta { - function: Some(ToolCallFunction { - name: "Bash".to_string(), - arguments: r#"{"command": "npm test"}"#.to_string(), - }), - }]), - usage: None, - }, - context_items: vec![], - prompt_logs: None, - tool_call_states: None, + history: vec![ChatHistoryItem { + message: ChatMessage { + role: "assistant".to_string(), + content: MessageContent::Text("Let me run that.".to_string()), + tool_calls: Some(vec![ToolCallDelta { + function: Some(ToolCallFunction { + name: "Bash".to_string(), + arguments: r#"{"command": "npm test"}"#.to_string(), + }), + }]), + usage: None, }, - ], + context_items: vec![], + prompt_logs: None, + tool_call_states: None, + }], date_created: Some("2025-01-01".to_string()), }; @@ -5390,9 +5392,7 @@ mod tests { ChatHistoryItem { message: ChatMessage { role: "tool".to_string(), - content: MessageContent::Text( - "some plain text content here".to_string(), - ), + content: MessageContent::Text("some plain text content here".to_string()), tool_calls: None, usage: None, }, @@ -5639,9 +5639,7 @@ mod tests { history: vec![ChatHistoryItem { message: ChatMessage { role: "assistant".to_string(), - content: MessageContent::Text( - "Example:\n```xyzlang\nfoo bar\n```".to_string(), - ), + content: MessageContent::Text("Example:\n```xyzlang\nfoo bar\n```".to_string()), tool_calls: None, usage: None, },