From e2e7e9f35e0f27f8d8999f1e82a9ad5e1a50bbbf Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 20 Dec 2025 11:11:12 +0100 Subject: [PATCH 1/8] Remove Anthropic SDK type dependencies from models and parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove anthropic.types imports (Message, Usage, StopReason) - Remove unused methods: to_anthropic_usage(), from_anthropic_usage(), from_anthropic_message() - Simplify normalize_usage_info() to handle dicts and object-like access without depending on Anthropic SDK validation - Change stop_reason type from StopReason to Optional[str] - Remove no-op AnthropicMessage.model_validate() call in parser Our models are the canonical types for parsing JSONL transcripts - we don't need SDK types for validation. This simplifies dependencies and ownership. Added dev-docs documenting the rationale and future refactoring plans. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/models.py | 60 ++--------------------- claude_code_log/parser.py | 62 +++++++++--------------- dev-docs/MESSAGE_REFACTORING2.md | 62 ++++++++++++++++++++++++ dev-docs/REMOVE_ANTHROPIC_TYPES.md | 76 ++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 97 deletions(-) create mode 100644 dev-docs/MESSAGE_REFACTORING2.md create mode 100644 dev-docs/REMOVE_ANTHROPIC_TYPES.md diff --git a/claude_code_log/models.py b/claude_code_log/models.py index cf60e772..e858131b 100644 --- a/claude_code_log/models.py +++ b/claude_code_log/models.py @@ -1,15 +1,9 @@ -"""Pydantic models for Claude Code transcript JSON structures. - -Enhanced to leverage official Anthropic types where beneficial. -""" +"""Pydantic models for Claude Code transcript JSON structures.""" from dataclasses import dataclass, field from enum import Enum from typing import Any, Union, Optional, Literal -from anthropic.types import Message as AnthropicMessage -from anthropic.types import StopReason -from anthropic.types import Usage as AnthropicUsage from pydantic import BaseModel, PrivateAttr @@ -699,7 +693,7 @@ class ExitPlanModeInput(BaseModel): class UsageInfo(BaseModel): - """Token usage information that extends Anthropic's Usage type to handle optional fields.""" + """Token usage information for tracking API consumption.""" input_tokens: Optional[int] = None cache_creation_input_tokens: Optional[int] = None @@ -708,33 +702,6 @@ class UsageInfo(BaseModel): service_tier: Optional[str] = None server_tool_use: Optional[dict[str, Any]] = None - def to_anthropic_usage(self) -> Optional[AnthropicUsage]: - """Convert to Anthropic Usage type if both required fields are present.""" - if self.input_tokens is not None and self.output_tokens is not None: - return AnthropicUsage( - input_tokens=self.input_tokens, - output_tokens=self.output_tokens, - cache_creation_input_tokens=self.cache_creation_input_tokens, - cache_read_input_tokens=self.cache_read_input_tokens, - service_tier=self.service_tier, # type: ignore - server_tool_use=self.server_tool_use, # type: ignore - ) - return None - - @classmethod - def from_anthropic_usage(cls, usage: AnthropicUsage) -> "UsageInfo": - """Create UsageInfo from Anthropic Usage.""" - return cls( - input_tokens=usage.input_tokens, - output_tokens=usage.output_tokens, - cache_creation_input_tokens=usage.cache_creation_input_tokens, - cache_read_input_tokens=usage.cache_read_input_tokens, - service_tier=usage.service_tier, - server_tool_use=usage.server_tool_use.model_dump() - if usage.server_tool_use - else None, - ) - class ToolUseContent(BaseModel, MessageContent): type: Literal["tool_use"] @@ -793,36 +760,17 @@ class UserMessage(BaseModel): class AssistantMessage(BaseModel): - """Assistant message model compatible with Anthropic's Message type.""" + """Assistant message model.""" id: str type: Literal["message"] role: Literal["assistant"] model: str content: list[ContentItem] - stop_reason: Optional[StopReason] = None + stop_reason: Optional[str] = None stop_sequence: Optional[str] = None usage: Optional[UsageInfo] = None - @classmethod - def from_anthropic_message( - cls, anthropic_msg: AnthropicMessage - ) -> "AssistantMessage": - """Create AssistantMessage from official Anthropic Message.""" - from .parser import normalize_usage_info - - # Convert Anthropic Message to our format, preserving official types where possible - return cls( - id=anthropic_msg.id, - type=anthropic_msg.type, - role=anthropic_msg.role, - model=anthropic_msg.model, - content=list(anthropic_msg.content), # type: ignore[arg-type] - stop_reason=anthropic_msg.stop_reason, - stop_sequence=anthropic_msg.stop_sequence, - usage=normalize_usage_info(anthropic_msg.usage), - ) - # Tool result type - flexible to accept various result formats from JSONL # The specific parsing/formatting happens in tool_formatters.py using diff --git a/claude_code_log/parser.py b/claude_code_log/parser.py index 19596778..a0528849 100644 --- a/claude_code_log/parser.py +++ b/claude_code_log/parser.py @@ -6,8 +6,6 @@ from typing import Any, Callable, Optional, Union, cast from datetime import datetime -from anthropic.types import Message as AnthropicMessage -from anthropic.types import Usage as AnthropicUsage from pydantic import BaseModel from .models import ( @@ -712,7 +710,7 @@ def parse_tool_input(tool_name: str, input_data: dict[str, Any]) -> ToolInput: def normalize_usage_info(usage_data: Any) -> Optional[UsageInfo]: - """Normalize usage data to be compatible with both custom and Anthropic formats.""" + """Normalize usage data from various formats to UsageInfo.""" if usage_data is None: return None @@ -720,35 +718,28 @@ def normalize_usage_info(usage_data: Any) -> Optional[UsageInfo]: if isinstance(usage_data, UsageInfo): return usage_data - # If it's an Anthropic Usage instance, convert using our method - if isinstance(usage_data, AnthropicUsage): - return UsageInfo.from_anthropic_usage(usage_data) - - # If it has the shape of an Anthropic Usage, try to construct it first - if hasattr(usage_data, "input_tokens") and hasattr(usage_data, "output_tokens"): - try: - # Try to create an Anthropic Usage first - anthropic_usage = AnthropicUsage.model_validate(usage_data) - return UsageInfo.from_anthropic_usage(anthropic_usage) - except Exception: - # Fall back to direct conversion - return UsageInfo( - input_tokens=getattr(usage_data, "input_tokens", None), - cache_creation_input_tokens=getattr( - usage_data, "cache_creation_input_tokens", None - ), - cache_read_input_tokens=getattr( - usage_data, "cache_read_input_tokens", None - ), - output_tokens=getattr(usage_data, "output_tokens", None), - service_tier=getattr(usage_data, "service_tier", None), - server_tool_use=getattr(usage_data, "server_tool_use", None), - ) - - # If it's a dict, validate and convert to our format + # If it's a dict, validate and convert if isinstance(usage_data, dict): return UsageInfo.model_validate(usage_data) + # Handle object-like access (e.g., from SDK types) + if hasattr(usage_data, "input_tokens"): + server_tool_use = getattr(usage_data, "server_tool_use", None) + if server_tool_use is not None and hasattr(server_tool_use, "model_dump"): + server_tool_use = server_tool_use.model_dump() + return UsageInfo( + input_tokens=getattr(usage_data, "input_tokens", None), + output_tokens=getattr(usage_data, "output_tokens", None), + cache_creation_input_tokens=getattr( + usage_data, "cache_creation_input_tokens", None + ), + cache_read_input_tokens=getattr( + usage_data, "cache_read_input_tokens", None + ), + service_tier=getattr(usage_data, "service_tier", None), + server_tool_use=server_tool_use, + ) + return None @@ -923,20 +914,9 @@ def parse_transcript_entry(data: dict[str, Any]) -> TranscriptEntry: return UserTranscriptEntry.model_validate(data_copy) elif entry_type == "assistant": - # Enhanced assistant message parsing with optional Anthropic types data_copy = data.copy() - # Validate compatibility with official Anthropic Message type - if "message" in data_copy: - try: - message_data = data_copy["message"] - AnthropicMessage.model_validate(message_data) - # Successfully validated - our data is compatible with official Anthropic types - except Exception: - # Validation failed - continue with standard parsing - pass - - # Standard parsing path using assistant-specific parser + # Parse assistant message content if "message" in data_copy and "content" in data_copy["message"]: message_copy = data_copy["message"].copy() message_copy["content"] = parse_message_content( diff --git a/dev-docs/MESSAGE_REFACTORING2.md b/dev-docs/MESSAGE_REFACTORING2.md new file mode 100644 index 00000000..d83ec70e --- /dev/null +++ b/dev-docs/MESSAGE_REFACTORING2.md @@ -0,0 +1,62 @@ +# Message Refactoring Phase 2 + +## Vision + +The goal is to achieve a cleaner, type-driven architecture where: +1. **MessageContent type is the source of truth** - No need for separate `MessageModifiers` or `MessageType` checks +2. **Inverted relationship** - Instead of `TemplateMessage.content: MessageContent`, have `MessageContent.meta: MessageMetadata` +3. **Leaner models** - Remove derived/redundant fields like `has_children`, `has_markdown`, `is_session_header`, `raw_text_content` +4. **Modular organization** - Split into `user_models.py`, `assistant_models.py`, `tools_models.py` with corresponding parsers + +## Current State Analysis + +### What we've achieved so far +- Content types now determine behavior (e.g., `UserSlashCommandContent` vs `UserTextContent`) +- Dispatcher pattern routes formatting based on content type +- Removed `ContentBlock` from `ContentItem` union - using our own types +- Simplified `_process_regular_message` - content type detection drives modifiers + +### Remaining issues +- `MessageModifiers` still exists with `is_slash_command`, `is_compacted`, `is_sidechain` flags +- These are redundant with type information (e.g., `is_slash_command` ↔ `isinstance(content, UserSlashCommandContent)`) +- `TemplateMessage` still owns `content` rather than the reverse + +## Cache Considerations + +**Good news**: The cache stores `TranscriptEntry` objects (raw parsed data), not `TemplateMessage`: +```python +class CacheManager: + def load_cached_entries(...) -> Optional[list[TranscriptEntry]] + def save_cached_entries(...) +``` + +This means: +- Cache is at the parsing layer, not rendering layer +- Changing `TemplateMessage` structure won't break cache compatibility +- If we store `MessageContent` class names for deserialization, it's a parsing concern + +**Feasibility of the inversion**: Yes, because: +1. Cache stores raw transcript entries, not TemplateMessages +2. TemplateMessage is generated fresh from entries on each render +3. The relationship between MessageContent and its metadata is internal to rendering + +## Modular Organization Plan + +### Models split +- `models.py` - Base classes (`MessageContent`, `TranscriptEntry`, etc.) +- `user_models.py` - User message content types +- `assistant_models.py` - Assistant message content types +- `tools_models.py` - Tool use/result models + +### Parser split +- `parser.py` - Base parsing, entry point +- `user_parser.py` - User message parsing +- `assistant_parser.py` - Assistant message parsing + +### Renderer reorganization +- `renderer.py` - Main message reorganization (`_render_messages`, tree building) +- Move `_process_*` functions to appropriate parser modules + +## Related Work + +See [REMOVE_ANTHROPIC_TYPES.md](REMOVE_ANTHROPIC_TYPES.md) for simplifying Anthropic SDK dependencies. diff --git a/dev-docs/REMOVE_ANTHROPIC_TYPES.md b/dev-docs/REMOVE_ANTHROPIC_TYPES.md new file mode 100644 index 00000000..1b7f96b1 --- /dev/null +++ b/dev-docs/REMOVE_ANTHROPIC_TYPES.md @@ -0,0 +1,76 @@ +# Remove Anthropic Types Dependency + +## Current Usage + +### Imports + +| Import | File | Purpose | +|--------|------|---------| +| `AnthropicMessage` | models.py, parser.py | Validate message compatibility | +| `AnthropicUsage` | models.py, parser.py | Convert usage info | +| `StopReason` | models.py | Type alias | + +### Methods + +| Method | Location | Used? | +|--------|----------|-------| +| `to_anthropic_usage()` | UsageInfo (models.py:711) | **Never** | +| `from_anthropic_usage()` | UsageInfo (models.py:725) | Yes, in `normalize_usage_info()` | +| `from_anthropic_message()` | AssistantMessage (models.py:808) | **Never** | + +### Call Sites + +| Call | Location | Purpose | Needed? | +|------|----------|---------|---------| +| `AnthropicMessage.model_validate()` | parser.py:933 | Validate JSONL data is compatible | **No** - result unused | +| `AnthropicUsage.model_validate()` | parser.py:731 | Parse usage dict as Anthropic type | **No** - can use UsageInfo directly | +| `UsageInfo.from_anthropic_usage()` | parser.py:725, 732 | Convert Anthropic Usage to ours | **Partially** - simplify | + +## Simplification Plan + +### Phase 1: Remove dead code +1. Remove `to_anthropic_usage()` - never used +2. Remove `from_anthropic_message()` - never used +3. Remove `AnthropicMessage.model_validate()` validation in parser.py - no-op +4. Remove `StopReason` import - just use `Optional[str]` + +### Phase 2: Simplify usage parsing +Replace the Anthropic-aware `normalize_usage_info()` with direct dict-to-UsageInfo conversion: +```python +def normalize_usage_info(usage_data: Any) -> Optional[UsageInfo]: + if usage_data is None: + return None + if isinstance(usage_data, UsageInfo): + return usage_data + if isinstance(usage_data, dict): + return UsageInfo.model_validate(usage_data) + # Handle object-like access for backwards compatibility + return UsageInfo( + input_tokens=getattr(usage_data, "input_tokens", None), + ... + ) +``` + +### Phase 3: Remove Anthropic imports +After Phase 1-2, remove from models.py and parser.py: +- `from anthropic.types import Message as AnthropicMessage` +- `from anthropic.types import Usage as AnthropicUsage` +- `from anthropic.types import StopReason` + +## Benefits + +1. **Simpler dependency** - Don't need anthropic SDK types for parsing our own JSONL +2. **Clearer ownership** - Our models are the canonical types, not wrappers +3. **Easier maintenance** - No need to track Anthropic SDK type changes +4. **Smaller models.py** - Less code, clearer structure + +## Open Questions + +1. **Was there ever a use case for `from_anthropic_message()`?** + - Possibly for direct SDK integration, but we only parse JSONL files + +2. **Why validate against `AnthropicMessage`?** + - Historical artifact from when we considered using SDK types directly + +3. **Could Anthropic types return in content arrays?** + - We already removed `ContentBlock` from `ContentItem` - no SDK types in content From e051c85cd91feb8e7a810005c265803605972e5a Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 20 Dec 2025 11:42:51 +0100 Subject: [PATCH 2/8] Add CSS_CLASS_REGISTRY to derive CSS classes from content types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create CSS_CLASS_REGISTRY mapping content types to CSS class lists - Rewrite css_class_from_message() to use registry with MRO walk - Dynamic modifiers (system-{level}, error) added based on content attributes - Cross-cutting modifiers (steering, sidechain) still from MessageModifiers This is a step toward eliminating redundant MessageModifiers fields that duplicate information already present in content types. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/html/utils.py | 97 +++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 17 deletions(-) diff --git a/claude_code_log/html/utils.py b/claude_code_log/html/utils.py index 72e1f903..3dedee72 100644 --- a/claude_code_log/html/utils.py +++ b/claude_code_log/html/utils.py @@ -22,26 +22,92 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape from .renderer_code import highlight_code_with_pygments, truncate_highlighted_preview +from ..models import ( + AssistantTextContent, + BashInputContent, + BashOutputContent, + CommandOutputContent, + CompactedSummaryContent, + HookSummaryContent, + MessageContent, + SessionHeaderContent, + SlashCommandContent, + SystemContent, + ThinkingContentModel, + ToolResultContentModel, + ToolUseContent, + UnknownContent, + UserMemoryContent, + UserSlashCommandContent, + UserTextContent, +) from ..renderer_timings import timing_stat if TYPE_CHECKING: from ..renderer import TemplateMessage +# -- CSS Class Registry ------------------------------------------------------- +# Maps content types to their CSS classes. +# The first class is typically the base type (user, assistant, system, etc.), +# followed by any static modifiers. + +CSS_CLASS_REGISTRY: dict[type[MessageContent], list[str]] = { + # System message types + SystemContent: ["system"], # level added dynamically + HookSummaryContent: ["system", "system-hook"], + # User message types + UserTextContent: ["user"], + SlashCommandContent: ["user", "slash-command"], + UserSlashCommandContent: ["user", "slash-command"], + UserMemoryContent: ["user"], + CompactedSummaryContent: ["user", "compacted"], + CommandOutputContent: ["user", "command-output"], + # Assistant message types + AssistantTextContent: ["assistant"], + # Tool message types + ToolUseContent: ["tool_use"], + ToolResultContentModel: ["tool_result"], # error added dynamically + # Other message types + ThinkingContentModel: ["thinking"], + SessionHeaderContent: ["session_header"], + BashInputContent: ["bash-input"], + BashOutputContent: ["bash-output"], + UnknownContent: ["unknown"], +} + + +def _get_css_classes_from_content(content: MessageContent) -> list[str]: + """Get CSS classes from content type using the registry. + + Walks the MRO to find a matching registry entry, then adds + any dynamic modifiers based on content attributes. + """ + for cls in type(content).__mro__: + if classes := CSS_CLASS_REGISTRY.get(cls): + result = list(classes) + # Dynamic modifiers based on content attributes + if isinstance(content, SystemContent): + result.append(f"system-{content.level}") + elif isinstance(content, ToolResultContentModel) and content.is_error: + result.append("error") + return result + return [] + + # -- CSS and Message Display -------------------------------------------------- def css_class_from_message(msg: "TemplateMessage") -> str: """Generate CSS class string from message type and modifiers. - This reconstructs the original css_class format for backward - compatibility with existing CSS and JavaScript. + Uses CSS_CLASS_REGISTRY to derive classes from content type, + with fallback to msg.type for messages without registered content. The order of classes follows the original pattern: - 1. Message type (required) - 2. Modifier flags in order: slash-command, command-output, compacted, - error, steering, sidechain - 3. System level suffix (e.g., "system-info", "system-warning") + 1. Message type (from content type or msg.type fallback) + 2. Content-derived modifiers (e.g., slash-command, compacted, error) + 3. Cross-cutting modifier flags: steering, sidechain Args: msg: The template message to generate CSS classes for @@ -49,23 +115,20 @@ def css_class_from_message(msg: "TemplateMessage") -> str: Returns: Space-separated CSS class string (e.g., "user slash-command sidechain") """ - parts = [msg.type] + # Get base classes and content-derived modifiers from content type + if msg.content: + parts = _get_css_classes_from_content(msg.content) + if not parts: + parts = [msg.type] # Fallback if content type not in registry + else: + parts = [msg.type] + # Cross-cutting modifier flags (not derivable from content type alone) mods = msg.modifiers - if mods.is_slash_command: - parts.append("slash-command") - if mods.is_command_output: - parts.append("command-output") - if mods.is_compacted: - parts.append("compacted") - if mods.is_error: - parts.append("error") if mods.is_steering: parts.append("steering") if mods.is_sidechain: parts.append("sidechain") - if mods.system_level: - parts.append(f"system-{mods.system_level}") return " ".join(parts) From 0f2c7b9abd1b4ab906a4b4f418e22480209c7dc2 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 20 Dec 2025 11:55:01 +0100 Subject: [PATCH 3/8] Remove redundant MessageModifiers fields, derive from content types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MessageModifiers simplified from 7 fields to just is_sidechain: - Removed: is_slash_command, is_command_output, is_compacted, is_error, is_steering, system_level - These are now derived from content types via CSS_CLASS_REGISTRY or content attributes Changes: - Create UserSteeringContent for queue-operation "remove" messages - Set has_markdown flag based on content type (AssistantTextContent, ThinkingContentModel, CompactedSummaryContent) - Template uses message.has_markdown instead of type checks - Replace modifier checks with isinstance() on content types - CSS classes now fully derived from content types (except is_sidechain) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../html/templates/transcript.html | 2 +- claude_code_log/html/utils.py | 13 ++-- claude_code_log/models.py | 29 ++++---- claude_code_log/renderer.py | 68 ++++++++++++------- 4 files changed, 65 insertions(+), 47 deletions(-) diff --git a/claude_code_log/html/templates/transcript.html b/claude_code_log/html/templates/transcript.html index b531c688..3957cfe7 100644 --- a/claude_code_log/html/templates/transcript.html +++ b/claude_code_log/html/templates/transcript.html @@ -102,7 +102,7 @@

🔍 Search & Filter

{% else %} {%- set msg_css_class = css_class_from_message(message) %} - {% set markdown = message.type in ['assistant', 'thinking'] or message.modifiers.is_compacted %} + {% set markdown = message.has_markdown %}
{% set msg_emoji = get_message_emoji(message) -%} diff --git a/claude_code_log/html/utils.py b/claude_code_log/html/utils.py index 3dedee72..c0a50caa 100644 --- a/claude_code_log/html/utils.py +++ b/claude_code_log/html/utils.py @@ -39,6 +39,7 @@ UnknownContent, UserMemoryContent, UserSlashCommandContent, + UserSteeringContent, UserTextContent, ) from ..renderer_timings import timing_stat @@ -58,6 +59,7 @@ HookSummaryContent: ["system", "system-hook"], # User message types UserTextContent: ["user"], + UserSteeringContent: ["user", "steering"], SlashCommandContent: ["user", "slash-command"], UserSlashCommandContent: ["user", "slash-command"], UserMemoryContent: ["user"], @@ -84,6 +86,8 @@ def _get_css_classes_from_content(content: MessageContent) -> list[str]: any dynamic modifiers based on content attributes. """ for cls in type(content).__mro__: + if not issubclass(cls, MessageContent): + continue if classes := CSS_CLASS_REGISTRY.get(cls): result = list(classes) # Dynamic modifiers based on content attributes @@ -124,10 +128,7 @@ def css_class_from_message(msg: "TemplateMessage") -> str: parts = [msg.type] # Cross-cutting modifier flags (not derivable from content type alone) - mods = msg.modifiers - if mods.is_steering: - parts.append("steering") - if mods.is_sidechain: + if msg.modifiers.is_sidechain: parts.append("sidechain") return " ".join(parts) @@ -148,7 +149,7 @@ def get_message_emoji(msg: "TemplateMessage") -> str: return "📋" elif msg_type == "user": # Command output has no emoji (neutral - can be from built-in or user command) - if msg.modifiers.is_command_output: + if isinstance(msg.content, CommandOutputContent): return "" return "🤷" elif msg_type == "bash-input": @@ -162,7 +163,7 @@ def get_message_emoji(msg: "TemplateMessage") -> str: elif msg_type == "tool_use": return "🛠️" elif msg_type == "tool_result": - if msg.modifiers.is_error: + if isinstance(msg.content, ToolResultContentModel) and msg.content.is_error: return "🚨" return "🧰" elif msg_type == "thinking": diff --git a/claude_code_log/models.py b/claude_code_log/models.py index e858131b..83c06036 100644 --- a/claude_code_log/models.py +++ b/claude_code_log/models.py @@ -47,24 +47,14 @@ class MessageType(str, Enum): @dataclass class MessageModifiers: - """Semantic modifiers that affect message display. + """Cross-cutting modifier for sidechain messages. - These are format-neutral flags that renderers can use to determine - how to display a message. HTML renderer converts these to CSS classes, - text renderer might use them for indentation or formatting. - - The modifiers capture traits that were previously encoded in the - css_class string (e.g., "user sidechain slash-command"). + Most display properties are now derived from content type (via CSS_CLASS_REGISTRY). + Only is_sidechain remains as it's a cross-cutting concern that can apply to + any message type. """ is_sidechain: bool = False - is_slash_command: bool = False - is_command_output: bool = False - is_compacted: bool = False - is_error: bool = False - is_steering: bool = False - # System message level (mutually exclusive: info, warning, error, hook) - system_level: Optional[str] = None # ============================================================================= @@ -317,6 +307,17 @@ class UserTextContent(MessageContent): ] = field(default_factory=list) +@dataclass +class UserSteeringContent(UserTextContent): + """Content for user steering prompts (queue-operation "remove"). + + These are user messages that steer the conversation by removing + items from the queue. Inherits from UserTextContent. + """ + + pass + + # ============================================================================= # Assistant Message Content Models # ============================================================================= diff --git a/claude_code_log/renderer.py b/claude_code_log/renderer.py index 1f10a730..89bd52b3 100644 --- a/claude_code_log/renderer.py +++ b/claude_code_log/renderer.py @@ -2,7 +2,7 @@ """Render Claude transcript data to HTML format.""" import time -from dataclasses import dataclass, replace +from dataclasses import dataclass from pathlib import Path from typing import Any, Callable, Optional, Tuple, TYPE_CHECKING @@ -28,15 +28,19 @@ ThinkingContentModel, # Structured content types AssistantTextContent, + CommandOutputContent, CompactedSummaryContent, DedupNoticeContent, HookInfo, HookSummaryContent, SessionHeaderContent, + SlashCommandContent, SystemContent, UnknownContent, UserMemoryContent, UserSlashCommandContent, + UserSteeringContent, + UserTextContent, ) from .parser import ( as_assistant_entry, @@ -645,7 +649,7 @@ def _process_command_message( These are user messages containing slash command invocations (e.g., /context, /model). The JSONL type is "user", not "system". """ - modifiers = MessageModifiers(is_slash_command=True) + modifiers = MessageModifiers() # Type info is in SlashCommandContent # Parse to content model (formatting happens in HtmlRenderer) content = parse_slash_command(text_content) @@ -664,7 +668,7 @@ def _process_local_command_output( These are user messages containing the output from slash commands (e.g., /context, /model). The JSONL type is "user", not "system". """ - modifiers = MessageModifiers(is_command_output=True) + modifiers = MessageModifiers() # Type info is in CommandOutputContent # Parse to content model (formatting happens in HtmlRenderer) content = parse_command_output(text_content) @@ -724,8 +728,6 @@ def _process_regular_message( is_meta: True for slash command expanded prompts (isMeta=True in JSONL) """ message_title = message_type.title() # Default title - is_compacted = False - is_slash_command = False content_model: Optional["MessageContent"] = None # Handle user-specific preprocessing @@ -734,12 +736,10 @@ def _process_regular_message( # Parse user content (is_meta triggers UserSlashCommandContent creation) content_model = parse_user_message_content(items, is_slash_command=is_meta) - # Determine message_title and modifiers from content type + # Determine message_title from content type if isinstance(content_model, UserSlashCommandContent): - is_slash_command = True message_title = "User (slash command)" elif isinstance(content_model, CompactedSummaryContent): - is_compacted = True message_title = "User (compacted conversation)" elif isinstance(content_model, UserMemoryContent): message_title = "Memory" @@ -754,14 +754,10 @@ def _process_regular_message( if is_sidechain: # Update message title for display (only non-user types reach here) - if not is_compacted: + if not isinstance(content_model, CompactedSummaryContent): message_title = "Sub-assistant" - modifiers = MessageModifiers( - is_sidechain=is_sidechain, - is_slash_command=is_slash_command, - is_compacted=is_compacted, - ) + modifiers = MessageModifiers(is_sidechain=is_sidechain) return modifiers, content_model, message_type, message_title @@ -823,7 +819,7 @@ def _process_system_message( ancestry=[], # Will be assigned by _build_message_hierarchy uuid=message.uuid, parent_uuid=parent_uuid, - modifiers=MessageModifiers(system_level=level), + modifiers=MessageModifiers(), # Level info is in SystemContent content=content, ) @@ -1094,7 +1090,9 @@ def _build_pairing_indices(messages: list[TemplateMessage]) -> PairingIndices: uuid_index[msg.uuid] = i # Index slash-command user messages by parent_uuid - if msg.parent_uuid and msg.modifiers.is_slash_command: + if msg.parent_uuid and isinstance( + msg.content, (SlashCommandContent, UserSlashCommandContent) + ): slash_command_by_parent[msg.parent_uuid] = i return PairingIndices( @@ -1127,7 +1125,9 @@ def _try_pair_adjacent( - thinking + assistant """ # Slash command + command output (both are user messages) - if current.modifiers.is_slash_command and next_msg.modifiers.is_command_output: + if isinstance( + current.content, (SlashCommandContent, UserSlashCommandContent) + ) and isinstance(next_msg.content, CommandOutputContent): _mark_pair(current, next_msg) return True @@ -1251,7 +1251,7 @@ def _reorder_paired_messages(messages: list[TemplateMessage]) -> list[TemplateMe msg.is_paired and msg.pair_role == "pair_last" and msg.parent_uuid - and msg.modifiers.is_slash_command + and isinstance(msg.content, (SlashCommandContent, UserSlashCommandContent)) ): slash_command_pair_index[msg.parent_uuid] = i @@ -1346,7 +1346,6 @@ def _get_message_hierarchy_level(msg: TemplateMessage) -> int: """ msg_type = msg.type is_sidechain = msg.modifiers.is_sidechain - system_level = msg.modifiers.system_level # User messages at level 1 (under session) # Note: sidechain user messages are skipped before reaching this function @@ -1354,6 +1353,8 @@ def _get_message_hierarchy_level(msg: TemplateMessage) -> int: return 1 # System info/warning at level 3 (tool-related, e.g., hook notifications) + # Get level from SystemContent if available + system_level = msg.content.level if isinstance(msg.content, SystemContent) else None if ( msg_type == "system" and system_level in ("info", "warning") @@ -2129,12 +2130,13 @@ def _render_messages( ) ) - # Add 'steering' modifier for queue-operation 'remove' messages + # Convert to UserSteeringContent for queue-operation 'remove' messages if ( isinstance(message, QueueOperationTranscriptEntry) and message.operation == "remove" + and isinstance(content_model, UserTextContent) ): - modifiers = replace(modifiers, is_steering=True) + content_model = UserSteeringContent(items=content_model.items) message_title = "User (steering)" # Skip empty chunks @@ -2150,6 +2152,16 @@ def _render_messages( if chunk_uuid and len(chunks) > 1: chunk_uuid = f"{chunk_uuid}-chunk-{chunk_idx}" + # Markdown rendering for assistant, thinking, and compacted content + has_markdown = isinstance( + content_model, + ( + AssistantTextContent, + ThinkingContentModel, + CompactedSummaryContent, + ), + ) + template_message = TemplateMessage( message_type=chunk_message_type, formatted_timestamp=formatted_timestamp, @@ -2165,6 +2177,7 @@ def _render_messages( parent_uuid=getattr(message, "parentUuid", None), modifiers=modifiers, content=content_model, + has_markdown=has_markdown, ) # Store raw text content for potential future use @@ -2209,11 +2222,8 @@ def _render_messages( # Preserve sidechain context for tool/thinking content tool_is_sidechain = getattr(message, "isSidechain", False) - # Build modifiers directly from tool_result properties - tool_modifiers = MessageModifiers( - is_sidechain=tool_is_sidechain, - is_error=tool_result.is_error, - ) + # Build modifiers (is_error is in ToolResultContentModel) + tool_modifiers = MessageModifiers(is_sidechain=tool_is_sidechain) # Generate unique UUID for this tool message # Use tool_use_id if available, otherwise fall back to msg UUID + index @@ -2223,6 +2233,11 @@ def _render_messages( else f"{msg_uuid}-tool-{len(template_messages)}" ) + # Thinking content uses markdown + tool_has_markdown = isinstance( + tool_result.content, ThinkingContentModel + ) + tool_template_message = TemplateMessage( message_type=tool_result.message_type, formatted_timestamp=tool_formatted_timestamp, @@ -2238,6 +2253,7 @@ def _render_messages( uuid=tool_uuid, modifiers=tool_modifiers, content=tool_result.content, # Structured content model + has_markdown=tool_has_markdown, ) # Store raw text for Task result deduplication From 72661977771e638b5bce78f69751cfca8c2e86ad Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 20 Dec 2025 13:00:21 +0100 Subject: [PATCH 4/8] Remove MessageModifiers class, use is_sidechain flag directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the MessageModifiers simplification by: - Remove MessageModifiers class from models.py entirely - Store is_sidechain as a bool directly on TemplateMessage - Update _process_regular_message to return (is_sidechain, content, type, title) - Update all TemplateMessage creation sites to use is_sidechain= - Replace msg.modifiers.is_sidechain with msg.is_sidechain All display properties are now derived from content type via CSS_CLASS_REGISTRY, making MessageModifiers obsolete. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/html/utils.py | 4 +- claude_code_log/models.py | 12 ----- claude_code_log/renderer.py | 96 ++++++++++++++--------------------- 3 files changed, 40 insertions(+), 72 deletions(-) diff --git a/claude_code_log/html/utils.py b/claude_code_log/html/utils.py index c0a50caa..c62657a8 100644 --- a/claude_code_log/html/utils.py +++ b/claude_code_log/html/utils.py @@ -128,7 +128,7 @@ def css_class_from_message(msg: "TemplateMessage") -> str: parts = [msg.type] # Cross-cutting modifier flags (not derivable from content type alone) - if msg.modifiers.is_sidechain: + if msg.is_sidechain: parts.append("sidechain") return " ".join(parts) @@ -155,7 +155,7 @@ def get_message_emoji(msg: "TemplateMessage") -> str: elif msg_type == "bash-input": return "💻" elif msg_type == "assistant": - if msg.modifiers.is_sidechain: + if msg.is_sidechain: return "🔗" return "🤖" elif msg_type == "system": diff --git a/claude_code_log/models.py b/claude_code_log/models.py index 83c06036..620d3133 100644 --- a/claude_code_log/models.py +++ b/claude_code_log/models.py @@ -45,18 +45,6 @@ class MessageType(str, Enum): SYSTEM_ERROR = "system-error" -@dataclass -class MessageModifiers: - """Cross-cutting modifier for sidechain messages. - - Most display properties are now derived from content type (via CSS_CLASS_REGISTRY). - Only is_sidechain remains as it's a cross-cutting concern that can apply to - any message type. - """ - - is_sidechain: bool = False - - # ============================================================================= # Message Content Models # ============================================================================= diff --git a/claude_code_log/renderer.py b/claude_code_log/renderer.py index 89bd52b3..3e5209d9 100644 --- a/claude_code_log/renderer.py +++ b/claude_code_log/renderer.py @@ -12,7 +12,6 @@ from datetime import datetime from .models import ( - MessageModifiers, MessageType, TranscriptEntry, AssistantTranscriptEntry, @@ -189,14 +188,14 @@ def __init__( uuid: Optional[str] = None, parent_uuid: Optional[str] = None, agent_id: Optional[str] = None, - modifiers: Optional[MessageModifiers] = None, + is_sidechain: bool = False, content: Optional["MessageContent"] = None, ): self.type = message_type # Structured content for rendering self.content = content self.formatted_timestamp = formatted_timestamp - self.modifiers = modifiers if modifiers is not None else MessageModifiers() + self.is_sidechain = is_sidechain self.raw_timestamp = raw_timestamp # Display title for message header (capitalized, with decorations) self.message_title = ( @@ -643,70 +642,54 @@ def prepare_session_navigation( def _process_command_message( text_content: str, -) -> tuple[MessageModifiers, Optional["MessageContent"], str, str]: - """Process a slash command message and return (modifiers, content, message_type, message_title). +) -> tuple[Optional["MessageContent"], str, str]: + """Process a slash command message and return (content, message_type, message_title). These are user messages containing slash command invocations (e.g., /context, /model). The JSONL type is "user", not "system". """ - modifiers = MessageModifiers() # Type info is in SlashCommandContent - # Parse to content model (formatting happens in HtmlRenderer) content = parse_slash_command(text_content) # If parsing fails, content will be None and caller will handle fallback - message_type = "user" - message_title = "Slash Command" - return modifiers, content, message_type, message_title + return content, "user", "Slash Command" def _process_local_command_output( text_content: str, -) -> tuple[MessageModifiers, Optional["MessageContent"], str, str]: - """Process slash command output and return (modifiers, content, message_type, message_title). +) -> tuple[Optional["MessageContent"], str, str]: + """Process slash command output and return (content, message_type, message_title). These are user messages containing the output from slash commands (e.g., /context, /model). The JSONL type is "user", not "system". """ - modifiers = MessageModifiers() # Type info is in CommandOutputContent - # Parse to content model (formatting happens in HtmlRenderer) content = parse_command_output(text_content) # If parsing fails, content will be None and caller will handle fallback - message_type = "user" - message_title = "" - return modifiers, content, message_type, message_title + return content, "user", "" def _process_bash_input( text_content: str, -) -> tuple[MessageModifiers, Optional["MessageContent"], str, str]: - """Process bash input command and return (modifiers, content, message_type, message_title).""" - modifiers = MessageModifiers() # bash-input is a message type, not a modifier - +) -> tuple[Optional["MessageContent"], str, str]: + """Process bash input command and return (content, message_type, message_title).""" # Parse to content model (formatting happens in HtmlRenderer) content = parse_bash_input(text_content) # If parsing fails, content will be None and caller will handle fallback - message_type = "bash-input" - message_title = "Bash command" - return modifiers, content, message_type, message_title + return content, "bash-input", "Bash command" def _process_bash_output( text_content: str, -) -> tuple[MessageModifiers, Optional["MessageContent"], str, str]: - """Process bash output and return (modifiers, content, message_type, message_title).""" - modifiers = MessageModifiers() # bash-output is a message type, not a modifier - +) -> tuple[Optional["MessageContent"], str, str]: + """Process bash output and return (content, message_type, message_title).""" # Parse to content model (formatting happens in HtmlRenderer) content = parse_bash_output(text_content) # If parsing fails, content will be None - caller/renderer handles empty output - message_type = "bash-output" - message_title = "" - return modifiers, content, message_type, message_title + return content, "bash-output", "" def _process_regular_message( @@ -714,8 +697,8 @@ def _process_regular_message( message_type: str, is_sidechain: bool, is_meta: bool = False, -) -> tuple[MessageModifiers, Optional["MessageContent"], str, str]: - """Process regular message and return (modifiers, content_model, message_type, message_title). +) -> tuple[bool, Optional["MessageContent"], str, str]: + """Process regular message and return (is_sidechain, content_model, message_type, message_title). Returns content_model for user messages, None for non-user messages. Non-user messages (assistant) are handled by the legacy render_message_content path. @@ -757,9 +740,7 @@ def _process_regular_message( if not isinstance(content_model, CompactedSummaryContent): message_title = "Sub-assistant" - modifiers = MessageModifiers(is_sidechain=is_sidechain) - - return modifiers, content_model, message_type, message_title + return is_sidechain, content_model, message_type, message_title def _process_system_message( @@ -819,8 +800,7 @@ def _process_system_message( ancestry=[], # Will be assigned by _build_message_hierarchy uuid=message.uuid, parent_uuid=parent_uuid, - modifiers=MessageModifiers(), # Level info is in SystemContent - content=content, + content=content, # Level info is in SystemContent ) @@ -1345,7 +1325,7 @@ def _get_message_hierarchy_level(msg: TemplateMessage) -> int: Integer hierarchy level (1-5, session headers are 0) """ msg_type = msg.type - is_sidechain = msg.modifiers.is_sidechain + is_sidechain = msg.is_sidechain # User messages at level 1 (under session) # Note: sidechain user messages are skipped before reaching this function @@ -1628,7 +1608,7 @@ def _reorder_sidechain_template_messages( sidechain_map: dict[str, list[TemplateMessage]] = {} for message in messages: - is_sidechain = message.modifiers.is_sidechain + is_sidechain = message.is_sidechain agent_id = message.agent_id if is_sidechain and agent_id: @@ -2037,7 +2017,6 @@ def _render_messages( is_session_header=True, message_id=None, ancestry=[], - modifiers=MessageModifiers(), content=SessionHeaderContent( title=session_title, session_id=session_id, @@ -2094,24 +2073,25 @@ def _render_messages( is_bash_cmd = is_bash_input(chunk_text) is_bash_result = is_bash_output(chunk_text) - # Determine modifiers and content based on message type + # Determine is_sidechain and content based on message type content_model: Optional[MessageContent] = None chunk_message_type = message_type + chunk_is_sidechain = getattr(message, "isSidechain", False) if is_command: - modifiers, content_model, chunk_message_type, message_title = ( + content_model, chunk_message_type, message_title = ( _process_command_message(chunk_text) ) elif is_local_output: - modifiers, content_model, chunk_message_type, message_title = ( + content_model, chunk_message_type, message_title = ( _process_local_command_output(chunk_text) ) elif is_bash_cmd: - modifiers, content_model, chunk_message_type, message_title = ( + content_model, chunk_message_type, message_title = ( _process_bash_input(chunk_text) ) elif is_bash_result: - modifiers, content_model, chunk_message_type, message_title = ( + content_model, chunk_message_type, message_title = ( _process_bash_output(chunk_text) ) else: @@ -2121,13 +2101,16 @@ def _render_messages( else: effective_type = message_type - modifiers, content_model, chunk_message_type, message_title = ( - _process_regular_message( - chunk, # Pass the chunk items - effective_type, - getattr(message, "isSidechain", False), - getattr(message, "isMeta", False), - ) + ( + chunk_is_sidechain, + content_model, + chunk_message_type, + message_title, + ) = _process_regular_message( + chunk, # Pass the chunk items + effective_type, + chunk_is_sidechain, + getattr(message, "isMeta", False), ) # Convert to UserSteeringContent for queue-operation 'remove' messages @@ -2175,7 +2158,7 @@ def _render_messages( agent_id=getattr(message, "agentId", None), uuid=chunk_uuid, parent_uuid=getattr(message, "parentUuid", None), - modifiers=modifiers, + is_sidechain=chunk_is_sidechain, content=content_model, has_markdown=has_markdown, ) @@ -2222,9 +2205,6 @@ def _render_messages( # Preserve sidechain context for tool/thinking content tool_is_sidechain = getattr(message, "isSidechain", False) - # Build modifiers (is_error is in ToolResultContentModel) - tool_modifiers = MessageModifiers(is_sidechain=tool_is_sidechain) - # Generate unique UUID for this tool message # Use tool_use_id if available, otherwise fall back to msg UUID + index tool_uuid = ( @@ -2251,7 +2231,7 @@ def _render_messages( ancestry=[], # Will be assigned by _build_message_hierarchy agent_id=getattr(message, "agentId", None), uuid=tool_uuid, - modifiers=tool_modifiers, + is_sidechain=tool_is_sidechain, content=tool_result.content, # Structured content model has_markdown=tool_has_markdown, ) From d6f917eee9531aaf29e1731bf3c1151ec91d48ec Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 20 Dec 2025 14:05:33 +0100 Subject: [PATCH 5/8] Update messages.md for UserSteeringContent and CSS registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the MessageModifiers removal and new architecture: - Add UserSteeringContent to data flow diagram and user variants - Update TemplateMessage fields (is_sidechain, has_markdown) - Replace MessageModifiers section with CSS_CLASS_REGISTRY docs - Update CSS class table to show content type derivation - Update Queue Operation section to reference UserSteeringContent 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- dev-docs/messages.md | 100 ++++++++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/dev-docs/messages.md b/dev-docs/messages.md index cfa881d5..09f9e9af 100644 --- a/dev-docs/messages.md +++ b/dev-docs/messages.md @@ -25,6 +25,7 @@ JSONL Parsing (parser.py) │ │ ├── CommandOutputContent ( tags) │ │ ├── BashInputContent ( tags) │ │ ├── CompactedSummaryContent (compacted conversation) +│ │ ├── UserSteeringContent (queue-operation "remove") │ │ └── Plain user text │ ├── ToolResultContent → Tool result messages: │ │ ├── ReadOutput (cat-n formatted file content) @@ -48,7 +49,7 @@ JSONL Parsing (parser.py) ├── SummaryTranscriptEntry → Session metadata (not rendered) │ └── QueueOperationTranscriptEntry - └── "remove" operation → Steering message (rendered as user) + └── "remove" operation → UserSteeringContent (rendered as user) ``` --- @@ -72,8 +73,9 @@ class TemplateMessage: # Display message_title: str # Display title (e.g., "User", "Assistant") - css_class: str # CSS classes (derived from type + modifiers) - modifiers: MessageModifiers # Format-neutral display traits + is_sidechain: bool # Sub-agent message flag + has_markdown: bool # Content should be rendered as markdown + # Note: CSS classes are derived from content type via CSS_CLASS_REGISTRY # Metadata raw_timestamp: str # ISO 8601 timestamp @@ -91,30 +93,30 @@ class TemplateMessage: tool_use_id: Optional[str] # ID linking tool_use to tool_result ``` -### MessageModifiers → CSS Classes - -Display traits are stored in `MessageModifiers` (see [Part 6](#part-6-infrastructure-models)) and converted to CSS classes for HTML rendering: - -| css_class | Base Type | MessageModifiers Field | -|-----------|-----------|------------------------| -| `"user"` | user | (none) | -| `"user compacted"` | user | `is_compacted=True` | -| `"user slash-command"` | user | `is_slash_command=True` | -| `"user command-output"` | user | `is_command_output=True` | -| `"user sidechain"` | user | `is_sidechain=True` | -| `"user steering"` | user | `is_steering=True` | -| `"assistant"` | assistant | (none) | -| `"assistant sidechain"` | assistant | `is_sidechain=True` | -| `"tool_use"` | tool_use | (none) | -| `"tool_use sidechain"` | tool_use | `is_sidechain=True` | -| `"tool_result"` | tool_result | (none) | -| `"tool_result error"` | tool_result | `is_error=True` | -| `"tool_result sidechain"` | tool_result | `is_sidechain=True` | -| `"thinking"` | thinking | (none) | -| `"system system-info"` | system | `system_level="info"` | -| `"system system-warning"` | system | `system_level="warning"` | -| `"system system-error"` | system | `system_level="error"` | -| `"system system-hook"` | system | `system_level="hook"` | +### Content Type → CSS Classes + +CSS classes are derived from the content type using `CSS_CLASS_REGISTRY` (in `html/utils.py`). This ensures the content type is the single source of truth for display styling. + +| css_class | Content Type | Dynamic Modifier | +|-----------|--------------|------------------| +| `"user"` | `UserTextContent` | — | +| `"user compacted"` | `CompactedSummaryContent` | — | +| `"user slash-command"` | `SlashCommandContent`, `UserSlashCommandContent` | — | +| `"user command-output"` | `CommandOutputContent` | — | +| `"user steering"` | `UserSteeringContent` | — | +| `"assistant"` | `AssistantTextContent` | — | +| `"tool_use"` | `ToolUseContent` | — | +| `"tool_result"` | `ToolResultContentModel` | — | +| `"tool_result error"` | `ToolResultContentModel` | `is_error=True` | +| `"thinking"` | `ThinkingContentModel` | — | +| `"bash-input"` | `BashInputContent` | — | +| `"bash-output"` | `BashOutputContent` | — | +| `"system system-info"` | `SystemContent` | `level="info"` | +| `"system system-warning"` | `SystemContent` | `level="warning"` | +| `"system system-error"` | `SystemContent` | `level="error"` | +| `"system system-hook"` | `HookSummaryContent` | — | + +The `sidechain` modifier is added when `msg.is_sidechain=True` (a cross-cutting concern that applies to any message type). **Note**: See [css-classes.md](css-classes.md) for complete CSS support status. @@ -245,6 +247,22 @@ class CompactedSummaryContent(MessageContent): summary_text: str # The compacted conversation summary ``` +### User Steering (Queue Remove) + +- **Condition**: `QueueOperationTranscriptEntry` with `operation: "remove"` +- **Content Model**: `UserSteeringContent` (extends `UserTextContent`) +- **CSS Class**: `user steering` +- **Title**: "User (steering)" + +```python +@dataclass +class UserSteeringContent(UserTextContent): + """Content for user steering prompts (queue-operation 'remove').""" + pass # Inherits items from UserTextContent +``` + +Steering messages represent user interrupts that cancel queued operations. + ### Sidechain User (Sub-agent) - **Condition**: `isSidechain: true` @@ -612,7 +630,8 @@ The `leafUuid` links the summary to the last message of the session. ## 4.2 Queue Operation (QueueOperationTranscriptEntry) - **Purpose**: User interrupts and steering during assistant responses -- **Rendered**: Only `remove` operations (as `user steering`) +- **Rendered**: Only `remove` operations (as `UserSteeringContent`) +- **CSS Class**: `user steering` - **Files**: [queue_operation.json](messages/system/queue_operation.json) ## 4.3 File History Snapshot @@ -653,22 +672,25 @@ class DedupNoticeContent(MessageContent): # Part 6: Infrastructure Models -## 6.1 MessageModifiers +## 6.1 CSS Class Registry -Semantic modifiers that affect message display. These are format-neutral flags that renderers use to determine how to display a message: +Display styling is derived from content types using `CSS_CLASS_REGISTRY` in `html/utils.py`. This registry maps `MessageContent` subclasses to their CSS classes: ```python -@dataclass -class MessageModifiers: - is_sidechain: bool = False # Sub-agent message - is_slash_command: bool = False # Slash command invocation - is_command_output: bool = False # Command output - is_compacted: bool = False # Compacted conversation - is_error: bool = False # Error message - is_steering: bool = False # Queue remove (steering) - system_level: Optional[str] = None # "info", "warning", "error", "hook" +CSS_CLASS_REGISTRY: dict[type[MessageContent], list[str]] = { + # User message types + UserTextContent: ["user"], + UserSteeringContent: ["user", "steering"], + SlashCommandContent: ["user", "slash-command"], + CompactedSummaryContent: ["user", "compacted"], + # ... more content types +} ``` +The `_get_css_classes_from_content()` function walks the content type's MRO to find the matching registry entry, then adds dynamic modifiers (e.g., `system-{level}` for `SystemContent`). + +The only cross-cutting modifier is `is_sidechain`, which is stored directly on `TemplateMessage` and appended to CSS classes when true. + ## 6.2 UsageInfo Token usage tracking for assistant messages: From 47753e21c133d532582d2c5b3353e0e08f28e173 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 20 Dec 2025 14:05:55 +0100 Subject: [PATCH 6/8] Update MESSAGE_REFACTORING2.md to reflect completed work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark MessageModifiers removal, CSS_CLASS_REGISTRY, and UserSteeringContent as achieved goals. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- dev-docs/MESSAGE_REFACTORING2.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dev-docs/MESSAGE_REFACTORING2.md b/dev-docs/MESSAGE_REFACTORING2.md index d83ec70e..87a29c57 100644 --- a/dev-docs/MESSAGE_REFACTORING2.md +++ b/dev-docs/MESSAGE_REFACTORING2.md @@ -14,12 +14,13 @@ The goal is to achieve a cleaner, type-driven architecture where: - Content types now determine behavior (e.g., `UserSlashCommandContent` vs `UserTextContent`) - Dispatcher pattern routes formatting based on content type - Removed `ContentBlock` from `ContentItem` union - using our own types -- Simplified `_process_regular_message` - content type detection drives modifiers +- Simplified `_process_regular_message` - content type detection drives rendering +- **CSS_CLASS_REGISTRY** derives CSS classes from content types (in `html/utils.py`) +- **MessageModifiers removed** - only `is_sidechain` remains as a flag on `TemplateMessage` +- **UserSteeringContent** created for queue-operation "remove" messages -### Remaining issues -- `MessageModifiers` still exists with `is_slash_command`, `is_compacted`, `is_sidechain` flags -- These are redundant with type information (e.g., `is_slash_command` ↔ `isinstance(content, UserSlashCommandContent)`) -- `TemplateMessage` still owns `content` rather than the reverse +### Remaining goals +- `TemplateMessage` still owns `content` rather than the reverse (inverted relationship) ## Cache Considerations From 179c2303431f102b4c23d8520f2094a466612bce Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 20 Dec 2025 14:07:42 +0100 Subject: [PATCH 7/8] Fix docstring for format_assistant_text_content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove incorrect claim about fallback behavior when items is None. The code unconditionally iterates over content.items. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claude_code_log/html/assistant_formatters.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/claude_code_log/html/assistant_formatters.py b/claude_code_log/html/assistant_formatters.py index c4a420e5..cbfe41d0 100644 --- a/claude_code_log/html/assistant_formatters.py +++ b/claude_code_log/html/assistant_formatters.py @@ -31,12 +31,10 @@ def format_assistant_text_content( ) -> str: """Format assistant text content as HTML. - When `items` is set, iterates through the content items preserving order: + Iterates through content.items preserving order: - TextContent: Rendered as markdown with collapsible support - ImageContent: Rendered as inline tag with base64 data URL - Falls back to legacy text-only behavior when `items` is None. - Args: content: AssistantTextContent with text/items to render line_threshold: Number of lines before content becomes collapsible From 6bfbb90e9c131ae98c62dcb1ba2a730c9060d3ff Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Sat, 20 Dec 2025 16:09:30 +0100 Subject: [PATCH 8/8] Remove anthropic dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The anthropic package was only used for types which have been replaced with local Pydantic models. No code imports anthropic anymore. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- pyproject.toml | 1 - uv.lock | 196 ------------------------------------------------- 2 files changed, 197 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 31b733b2..903e3ba3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ dependencies = [ "pydantic>=2.12.0", "jinja2>=3.1.6", "mistune>=3.1.4", - "anthropic>=0.72.0", "toml>=0.10.2", "textual==6.5.0", "packaging>=25.0", diff --git a/uv.lock b/uv.lock index fb9b9e46..2cd60a97 100644 --- a/uv.lock +++ b/uv.lock @@ -11,39 +11,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] -[[package]] -name = "anthropic" -version = "0.75.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "docstring-parser" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" }, -] - -[[package]] -name = "anyio" -version = "4.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, -] - [[package]] name = "backports-asyncio-runner" version = "1.2.0" @@ -156,7 +123,6 @@ name = "claude-code-log" version = "0.9.0" source = { editable = "." } dependencies = [ - { name = "anthropic" }, { name = "click" }, { name = "dateparser" }, { name = "gitpython" }, @@ -186,7 +152,6 @@ dev = [ [package.metadata] requires-dist = [ - { name = "anthropic", specifier = ">=0.72.0" }, { name = "click", specifier = ">=8.3.0" }, { name = "dateparser", specifier = ">=1.2.2" }, { name = "gitpython", specifier = ">=3.1.45" }, @@ -354,24 +319,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/22/f020c047ae1346613db9322638186468238bcfa8849b4668a22b97faad65/dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482", size = 315453, upload-time = "2025-06-26T09:29:21.412Z" }, ] -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - -[[package]] -name = "docstring-parser" -version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, -] - [[package]] name = "exceptiongroup" version = "1.3.1" @@ -478,43 +425,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - [[package]] name = "idna" version = "3.11" @@ -545,103 +455,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "jiter" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" }, - { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" }, - { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" }, - { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" }, - { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" }, - { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" }, - { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" }, - { url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368, upload-time = "2025-11-09T20:46:58.638Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847, upload-time = "2025-11-09T20:47:00.295Z" }, - { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, - { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, - { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, - { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, - { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, - { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, - { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, - { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, - { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, - { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, - { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, - { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, - { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, - { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, - { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, - { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, - { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, - { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, - { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, - { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, - { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, - { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, - { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, - { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, - { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, - { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, - { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, - { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, - { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, - { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, - { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, - { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, - { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, - { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, - { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, - { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, - { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, -] - [[package]] name = "linkify-it-py" version = "2.0.3" @@ -1341,15 +1154,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, ] -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - [[package]] name = "syrupy" version = "5.0.0"