diff --git a/Cargo.toml b/Cargo.toml index c195097..a5c602a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "continue-transcripts" -version = "0.13.0" +version = "0.13.1" edition = "2021" description = "Convert continue.dev session files to readable HTML transcripts" license = "MIT" diff --git a/README.md b/README.md index b90377c..d430e36 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.0 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.13.1 ``` 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.0 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.13.1 ``` 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.0 \ + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.13.1 \ 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.0 + --find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.13.1 ``` ### Building from source diff --git a/pyproject.toml b/pyproject.toml index 5f875e9..32d4d9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "continue-transcripts" -version = "0.13.0" +version = "0.13.1" 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 3fcfefa..154bfb9 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.0" +__version__ = "0.13.1" diff --git a/src/main.rs b/src/main.rs index 337e03c..b02df4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3123,8 +3123,8 @@ document.addEventListener('DOMContentLoaded', function() { } }); - // Copy buttons for all code blocks (fenced, highlighted, and tool-result) - document.querySelectorAll('.message-content pre, pre.highlighted-code').forEach(function(pre) { + // Copy buttons for all code blocks (fenced, highlighted, tool-result, and context items) + document.querySelectorAll('.message-content pre, pre.highlighted-code, pre.tool-result-pre, pre.context-content').forEach(function(pre) { var wrapper = document.createElement('div'); wrapper.className = 'code-block-wrapper'; pre.parentNode.insertBefore(wrapper, pre); @@ -5358,6 +5358,335 @@ mod tests { assert!(html.contains("data-copy-text=\"npm test\"")); } + // ----------------------------------------------------------------------- + // Copy button coverage for all block types + // ----------------------------------------------------------------------- + + #[test] + fn test_copy_button_plain_text_tool_result() { + // A tool result with no syntax highlighting (unknown extension) + // should get class "tool-result-pre" so the copy button JS matches it. + let session = Session { + session_id: "copy-plain-tool".to_string(), + title: "Copy Plain Tool".to_string(), + workspace_directory: "/tmp".to_string(), + history: vec![ + ChatHistoryItem { + message: ChatMessage { + role: "assistant".to_string(), + content: MessageContent::Text("Reading file.".to_string()), + tool_calls: Some(vec![ToolCallDelta { + function: Some(ToolCallFunction { + name: "Read".to_string(), + arguments: r#"{"file_path": "/tmp/data"}"#.to_string(), + }), + }]), + usage: None, + }, + context_items: vec![], + prompt_logs: None, + tool_call_states: None, + }, + ChatHistoryItem { + message: ChatMessage { + role: "tool".to_string(), + content: MessageContent::Text( + "some plain text content here".to_string(), + ), + tool_calls: None, + usage: None, + }, + context_items: vec![], + prompt_logs: None, + tool_call_states: None, + }, + ], + date_created: Some("2025-01-01".to_string()), + }; + + let html = render_session(&session, None); + // The tool result should be in a
block
+ assert!(
+ html.contains("tool-result-pre"),
+ "plain text tool result should have tool-result-pre class"
+ );
+ assert!(
+ html.contains(""),
+ "plain text tool result should render as "
+ );
+ }
+
+ #[test]
+ fn test_copy_button_ansi_tool_result() {
+ // Terminal output with ANSI escape codes should get "tool-result-pre"
+ // class so the copy button JS matches it.
+ let session = Session {
+ session_id: "copy-ansi".to_string(),
+ title: "Copy ANSI".to_string(),
+ workspace_directory: "/tmp".to_string(),
+ history: vec![
+ ChatHistoryItem {
+ message: ChatMessage {
+ role: "assistant".to_string(),
+ content: MessageContent::Text("Running tests.".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,
+ },
+ ChatHistoryItem {
+ message: ChatMessage {
+ role: "tool".to_string(),
+ content: MessageContent::Text(
+ "\x1b[32m✓ pass\x1b[0m \x1b[31m✕ fail\x1b[0m".to_string(),
+ ),
+ tool_calls: None,
+ usage: None,
+ },
+ context_items: vec![],
+ prompt_logs: None,
+ tool_call_states: None,
+ },
+ ],
+ date_created: Some("2025-01-01".to_string()),
+ };
+
+ let html = render_session(&session, None);
+ assert!(
+ html.contains(""),
+ "ANSI tool result should render as "
+ );
+ // Should contain converted ANSI spans, not raw escape codes
+ assert!(
+ html.contains(""),
+ "plain text context item should render as "
+ );
+ }
+
+ #[test]
+ fn test_copy_button_highlighted_context_item() {
+ // Context items with a known extension should get both
+ // "highlighted-code" and "context-content" classes.
+ let session = Session {
+ session_id: "copy-ctx-hl".to_string(),
+ title: "Copy Ctx HL".to_string(),
+ workspace_directory: "/tmp".to_string(),
+ history: vec![ChatHistoryItem {
+ message: ChatMessage {
+ role: "user".to_string(),
+ content: MessageContent::Text("Check this file".to_string()),
+ tool_calls: None,
+ usage: None,
+ },
+ context_items: vec![ContextItem {
+ name: "src/app.py".to_string(),
+ description: "Python source".to_string(),
+ content: "import os\nprint('hello')".to_string(),
+ }],
+ prompt_logs: None,
+ tool_call_states: None,
+ }],
+ date_created: Some("2025-01-01".to_string()),
+ };
+
+ let html = render_session(&session, None);
+ assert!(
+ html.contains("highlighted-code context-content"),
+ "highlighted context item should have both highlighted-code and context-content classes"
+ );
+ }
+
+ #[test]
+ fn test_copy_button_fenced_code_block() {
+ // Markdown fenced code blocks inside message-content should have
+ // a that the JS selector ".message-content pre" matches.
+ // Syntect-highlighted blocks get class="highlighted-code"; unknown
+ // languages fall back to .
+ let session = Session {
+ session_id: "copy-fenced".to_string(),
+ title: "Copy Fenced".to_string(),
+ workspace_directory: "/tmp".to_string(),
+ history: vec![ChatHistoryItem {
+ message: ChatMessage {
+ role: "assistant".to_string(),
+ content: MessageContent::Text(
+ "Here is some code:\n```rust\nfn main() {}\n```".to_string(),
+ ),
+ tool_calls: None,
+ usage: None,
+ },
+ context_items: vec![],
+ prompt_logs: None,
+ tool_call_states: None,
+ }],
+ date_created: Some("2025-01-01".to_string()),
+ };
+
+ let html = render_session(&session, None);
+ // The code block should be inside a message-content div
+ assert!(
+ html.contains("message-content"),
+ "assistant message should have message-content class"
+ );
+ // Syntect highlights known languages →
+ // which lives inside the message-content div, matched by both
+ // ".message-content pre" and "pre.highlighted-code"
+ assert!(
+ html.contains(" inside message-content,
+ // matched by ".message-content pre".
+ let session = Session {
+ session_id: "copy-fenced-unk".to_string(),
+ title: "Copy Fenced Unk".to_string(),
+ workspace_directory: "/tmp".to_string(),
+ history: vec![ChatHistoryItem {
+ message: ChatMessage {
+ role: "assistant".to_string(),
+ content: MessageContent::Text(
+ "Example:\n```xyzlang\nfoo bar\n```".to_string(),
+ ),
+ tool_calls: None,
+ usage: None,
+ },
+ context_items: vec![],
+ prompt_logs: None,
+ tool_call_states: None,
+ }],
+ date_created: Some("2025-01-01".to_string()),
+ };
+
+ let html = render_session(&session, None);
+ assert!(
+ html.contains("message-content"),
+ "assistant message should have message-content class"
+ );
+ // Unknown language → plain
+ assert!(
+ html.contains("language-xyzlang"),
+ "unknown-language fenced code block should have language-* class"
+ );
+ }
+
+ #[test]
+ fn test_copy_button_js_selector_covers_all_classes() {
+ // The JS querySelectorAll for copy buttons must include selectors
+ // for all block types: message-content pre, highlighted-code,
+ // tool-result-pre, and context-content.
+ let session = Session {
+ session_id: "selector-test".to_string(),
+ title: "Selector Test".to_string(),
+ workspace_directory: "/tmp".to_string(),
+ history: vec![],
+ date_created: Some("2025-01-01".to_string()),
+ };
+
+ let html = render_session(&session, None);
+ // Verify the JS selector string covers all needed classes
+ assert!(
+ html.contains(
+ "querySelectorAll('.message-content pre, pre.highlighted-code, pre.tool-result-pre, pre.context-content')"
+ ),
+ "JS copy button selector must cover message-content pre, highlighted-code, tool-result-pre, and context-content"
+ );
+ }
+
#[test]
fn test_container_width_expanded() {
let session = Session {