diff --git a/CLAUDE.md b/CLAUDE.md index 4292c5f..5d406c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,7 @@ cargo build --release # Or install as a Python tool via uv (recommended for end users) uv tool install continue-transcripts \ --no-index \ - --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.7.0 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.8.0 ``` ### Running tests diff --git a/Cargo.toml b/Cargo.toml index 1afdac3..147a1f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "continue-transcripts" -version = "0.7.0" +version = "0.8.0" edition = "2021" description = "Convert continue.dev session files to readable HTML transcripts" license = "MIT" diff --git a/README.md b/README.md index 946407d..9e56369 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.7.0 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.8.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.7.0 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.8.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.7.0 \ + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.8.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.7.0 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.8.0 ``` ### Building from source diff --git a/pyproject.toml b/pyproject.toml index c791bf9..42773ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "continue-transcripts" -version = "0.7.0" +version = "0.8.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 8325296..4d7a385 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.7.0" +__version__ = "0.8.0" diff --git a/src/main.rs b/src/main.rs index 86bf4fc..661f8d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -993,7 +993,8 @@ fn render_tool_result_inline( tool_name: &str, tool_args: &str, ) -> String { - let content_text = item.message.content.text(); + let raw_content = item.message.content.text(); + let content_text = decode_file_uris_in_text(&raw_content); let label = if tool_name.is_empty() { "Tool Result".to_string() } else { @@ -1016,13 +1017,9 @@ fn render_tool_result_inline( html.push_str(&ansi_to_html(&content_text)); html.push_str("\n"); } else { - // For Read/read_file results, try to syntax-highlight based on - // the file extension from the tool arguments. - let highlighted = if tool_name == "Read" || tool_name == "read_file" { - try_highlight_read_result(tool_args, &content_text) - } else { - None - }; + // Try to syntax-highlight based on the file extension from + // the tool arguments (works for any tool with a file path arg). + let highlighted = try_highlight_tool_result(tool_args, &content_text); if let Some(hl) = highlighted { html.push_str(" "); html.push_str(&hl); @@ -1040,15 +1037,15 @@ fn render_tool_result_inline( html } -/// Attempt to syntax-highlight the content of a Read tool result. -/// Extracts the file path from the JSON arguments, strips line-number prefixes -/// (e.g. " 1\t..."), and applies syntax highlighting based on file extension. -fn try_highlight_read_result(tool_args: &str, content: &str) -> Option { +/// Attempt to syntax-highlight the content of a tool result. +/// Extracts a file path from the JSON arguments (checking common key names), +/// strips line-number prefixes (e.g. " 1\t..."), and applies syntax +/// highlighting based on the file extension. +fn try_highlight_tool_result(tool_args: &str, content: &str) -> Option { let parsed: serde_json::Value = serde_json::from_str(tool_args).ok()?; - let file_path = parsed - .get("file_path") - .or_else(|| parsed.get("filepath")) - .and_then(|v| v.as_str())?; + let file_path = ["file_path", "filepath", "notebook_path", "path", "file"] + .iter() + .find_map(|key| parsed.get(*key).and_then(|v| v.as_str()))?; let lang = lang_from_filename(file_path); if lang.is_empty() { return None; @@ -2798,6 +2795,28 @@ fn is_path_key(key: &str) -> bool { ) } +/// Find all `file:///...` URIs in free-form text and decode them into +/// normal paths. A URI ends at the first whitespace, `"`, `'`, `)`, `]`, +/// `>`, or end-of-string. +fn decode_file_uris_in_text(text: &str) -> String { + const PREFIX: &str = "file:///"; + let mut out = String::with_capacity(text.len()); + let mut rest = text; + while let Some(start) = rest.find(PREFIX) { + out.push_str(&rest[..start]); + let uri_start = &rest[start..]; + // Find the end of the URI + let end = uri_start + .find(|c: char| c.is_whitespace() || matches!(c, '"' | '\'' | ')' | ']' | '>')) + .unwrap_or(uri_start.len()); + let uri = &uri_start[..end]; + out.push_str(&decode_path(uri)); + rest = &uri_start[end..]; + } + out.push_str(rest); + out +} + /// Decode a value for display: strips `file:///` URI prefixes and /// decodes percent-encoded bytes (e.g. `%3A` → `:`). fn decode_path(input: &str) -> String { @@ -3160,7 +3179,7 @@ fn render_index(entries: &[(String, String, String)]) -> String { let mut rows = String::new(); for (title, filename, date) in entries { rows.push_str(&format!( - " \n {title}\n {date}\n \n", + " \n {title}\n {date}\n \n", filename = encode_text(filename), title = encode_text(title), date = encode_text(date), @@ -3197,7 +3216,7 @@ fn render_index(entries: &[(String, String, String)]) -> String { - + {rows} @@ -3473,6 +3492,37 @@ mod tests { assert_eq!(decode_path("/home/user/file.rs"), "/home/user/file.rs"); } + #[test] + fn test_decode_file_uris_in_text() { + // Windows-style file URI with percent-encoded colon + assert_eq!( + decode_file_uris_in_text("Successfully edited file:///c%3A/Users/test/file.rs"), + "Successfully edited c:/Users/test/file.rs" + ); + // Multiple URIs in one string + assert_eq!( + decode_file_uris_in_text( + "Moved file:///c%3A/old%20dir/a.rs to file:///c%3A/new%20dir/b.rs" + ), + "Moved c:/old dir/a.rs to c:/new dir/b.rs" + ); + // Unix file URI + assert_eq!( + decode_file_uris_in_text("Read file:///home/user/file.rs done"), + "Read /home/user/file.rs done" + ); + // No file URIs — unchanged + 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"), + "Edited c:/Users/test.rs" + ); + } + #[test] fn test_markdown_to_html() { let html = markdown_to_html("**bold** text"); @@ -3802,23 +3852,34 @@ mod tests { // ----------------------------------------------------------------------- #[test] - fn test_try_highlight_read_result() { + fn test_try_highlight_tool_result() { let args = r#"{"file_path": "/home/user/test.rs"}"#; let content = " 1\tfn main() {\n 2\t println!(\"hello\");\n 3\t}"; - let result = try_highlight_read_result(args, content); + let result = try_highlight_tool_result(args, content); assert!(result.is_some()); let hl = result.unwrap(); assert!(hl.contains("highlighted-code")); } #[test] - fn test_try_highlight_read_result_no_extension() { + fn test_try_highlight_tool_result_no_extension() { let args = r#"{"file_path": "/home/user/README"}"#; let content = "just some text"; - let result = try_highlight_read_result(args, content); + let result = try_highlight_tool_result(args, content); assert!(result.is_none()); } + #[test] + fn test_try_highlight_tool_result_path_key() { + // Grep-style tool with "path" key pointing to a Python file + let args = r#"{"pattern": "import", "path": "/home/user/app.py"}"#; + let content = "import os\nimport sys"; + let result = try_highlight_tool_result(args, content); + assert!(result.is_some()); + let hl = result.unwrap(); + assert!(hl.contains("highlighted-code")); + } + // ----------------------------------------------------------------------- // Model badge // -----------------------------------------------------------------------
SessionDate
SessionDate