Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ with pointers to the deep-dive docs:
- [dev-docs/teammates.md](dev-docs/teammates.md) - Teammates feature deep-dive
- [dev-docs/message-hierarchy.md](dev-docs/message-hierarchy.md) - Fold/unfold state machine
- [dev-docs/implementing-a-tool-renderer.md](dev-docs/implementing-a-tool-renderer.md) - How-to: add a new tool
- [dev-docs/plugins.md](dev-docs/plugins.md) - Plugin system reference + author guide

User-facing docs live in [docs/](docs/); plans and TODOs live in [work/](work/).

Expand Down
46 changes: 46 additions & 0 deletions claude_code_log/factories/priorities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Priority constants for plugin transformer ordering.

Built-in factory detectors run in a fixed sequence; these constants
expose their notional positions on a numeric priority scale so plugin
transformers can declare where they sit relative to the built-ins
without renumbering on every core change. Gaps of 100 leave room for
plugin insertion.

Convention (lower number = higher priority = runs first):

- ``0`` through ``999`` — user/system entry classification
- ``1000`` — generic text fallback (UserTextMessage)
- ``5000`` through ``9999`` — tool input / output classification
- ``10000`` and up — never used by built-ins; reserved for plugin fallbacks

See ``work/tool-renderer-plugins.md`` §Priority + ordering for the
RFC discussion and worked examples.
"""

# User-entry detector chain (see factories/user_factory.py::create_user_message)
COMMAND_MESSAGE: int = 100
LOCAL_COMMAND_OUTPUT: int = 200
BASH_INPUT_OUTPUT: int = 300
TEAMMATE_MESSAGE: int = 400
TASK_NOTIFICATION: int = 500
HOOK_NOTIFICATION: int = 600 # PR #167's seat
SLASH_COMMAND_ISMETA: int = 700
TEXT_FALLBACK: int = 1000 # generic UserTextMessage

# Tool-entry classification
TOOL_INPUT_GENERIC: int = 5000
TOOL_OUTPUT_GENERIC: int = 5100


__all__ = [
"BASH_INPUT_OUTPUT",
"COMMAND_MESSAGE",
"HOOK_NOTIFICATION",
"LOCAL_COMMAND_OUTPUT",
"SLASH_COMMAND_ISMETA",
"TASK_NOTIFICATION",
"TEAMMATE_MESSAGE",
"TEXT_FALLBACK",
"TOOL_INPUT_GENERIC",
"TOOL_OUTPUT_GENERIC",
]
14 changes: 12 additions & 2 deletions claude_code_log/factories/tool_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pydantic import BaseModel

from .agent_metadata_factory import parse_agent_result_metadata
from ..plugins import apply_transformers
from ..models import (
# Tool input models
AskUserQuestionInput,
Expand Down Expand Up @@ -1349,13 +1350,18 @@ def create_tool_use_message(

# Create ToolUseMessage wrapper with parsed input for specialized formatting
# Use ToolUseContent as fallback when no specialized parser exists
tool_use_message = ToolUseMessage(
tool_use_message: MessageContent = ToolUseMessage(
meta,
input=parsed if parsed is not None else tool_use,
tool_use_id=tool_use.id,
tool_name=tool_use.name,
)

# Plugin transformer pass: lets a plugin rewrite the ToolUseMessage
# into a specialized subclass (e.g. ClmailCommunicateInputMessage)
# carrying its own format/title methods. No-op when no plugin matches.
tool_use_message = apply_transformers(tool_use_message, meta)

return ToolItemResult(
message_type="tool_use",
content=tool_use_message,
Expand Down Expand Up @@ -1401,7 +1407,7 @@ def create_tool_result_message(
)

# Create content model with rendering context
content_model = ToolResultMessage(
content_model: MessageContent = ToolResultMessage(
meta,
tool_use_id=tool_result.tool_use_id,
output=parsed_output,
Expand All @@ -1410,6 +1416,10 @@ def create_tool_result_message(
file_path=result_file_path,
)

# Plugin transformer pass: lets a plugin rewrite the ToolResultMessage
# into a specialized subclass with its own format/title methods.
content_model = apply_transformers(content_model, meta)

return ToolItemResult(
message_type="tool_result",
content=content_model,
Expand Down
25 changes: 25 additions & 0 deletions claude_code_log/factories/user_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
UserSlashCommandMessage,
UserTextMessage,
)
from ..plugins import apply_transformers
from .task_notification_factory import (
create_task_notification_message,
has_task_notification,
Expand Down Expand Up @@ -467,6 +468,30 @@ def create_user_message(
content_list: list[ContentItem],
text_content: str,
is_slash_command: bool = False,
) -> Optional[UserMessageContent]:
"""Wrapper: build the candidate, then run plugin transformers.

The body lives in :func:`_classify_user_message`; this wrapper
applies the plugin transformer pass to the result so every
classification path (slash-command, bash, teammate, ...) becomes
rewriteable by a plugin, not just the generic-text fallback.
Transformers whose ``applies_to`` doesn't subclass-match the
candidate's type pass through with no-op cost.
"""
candidate = _classify_user_message(
meta, content_list, text_content, is_slash_command
)
if candidate is None:
return None
transformed = apply_transformers(candidate, meta)
return cast("UserMessageContent", transformed)


def _classify_user_message(
meta: MessageMeta,
content_list: list[ContentItem],
text_content: str,
is_slash_command: bool = False,
) -> Optional[UserMessageContent]:
"""Create a user message content model from content items.

Expand Down
8 changes: 8 additions & 0 deletions claude_code_log/html/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,14 @@ def _to_posix(s: str) -> str:
class HtmlRenderer(Renderer):
"""HTML renderer for Claude Code transcripts."""

# Consulted by Renderer._dispatch_format Strategy 2: plugin-defined
# content classes contributing a ``format_html`` method get picked up
# here. See work/tool-renderer-plugins.md §`_dispatch_format`
# resolution order. Class-side ``format_html`` may return None to
# opt for mistune-derived HTML; that fallback is handled by callers
# of format_content, not by the dispatcher itself.
_class_dispatch_format: str = "html"

def __init__(self, image_export_mode: str = "embedded"):
"""Initialize the HTML renderer.

Expand Down
Loading
Loading