Skip to content

plugins: dev-docs gaps + public helper API + ToolResult example (Phase 2)#173

Merged
cboos merged 4 commits into
mainfrom
review/plugin-system-impl
May 25, 2026
Merged

plugins: dev-docs gaps + public helper API + ToolResult example (Phase 2)#173
cboos merged 4 commits into
mainfrom
review/plugin-system-impl

Conversation

@cboos
Copy link
Copy Markdown
Collaborator

@cboos cboos commented May 25, 2026

Summary

Phase 2 follow-up on the plugin system landed in #169. Addresses the seven doc/code gaps clmail/main and clmail/carol surfaced after their v1 plugin shipped (federation mails #3218 + #3241), plus the long-deferred work/ shrink for the now-graduated RFC.

Production code

  • Public plugin API in claude_code_log/plugins.py — re-export render_markdown and render_markdown_collapsible from html/utils.py under the stable claude_code_log.plugins namespace. Resolved lazily via PEP-562 __getattr__ (the html subpackage closes a circular loop with factories / utils at module-init time); TYPE_CHECKING block makes the names visible to ruff F822 and pyright reportUnsupportedDunderAll. _PUBLIC_HELPERS keeps the surface narrow on purpose.

Test-embedded reference plugin

  • tool_communicate_result.py — new transformer mirroring tool_communicate.py (same fixture tool name, companion applies_to=(ToolResultMessage,)). Demonstrates the long-Markdown-body pattern via the new public helper. Single addition closes both the original "no worked example for ToolResultMessage" gap (#3218 gap 4) AND the long-body Request 2 from #3241.
  • Three new tests in TestToolResultTransformerWithCollapsibleBody: short-body inline / long-body collapsed / Strategy-2 class-side dispatch.

dev-docs/plugins.md expansion

Section What's new
§2 Quick start Constructor-signature reference block for ToolUseMessage (with skill_body call-out) and ToolResultMessage; reference-plugin list grows to three entries.
§4.1 Plugin-facing helpers New subsection documenting render_markdown / render_markdown_collapsible signatures + use-when guidance + the explicit "add only on concrete demand" disclaimer.
§6 Practical guide HIGH vs FULL choice for hook-style content.
§8.2 Priority ordering Per-tool offset convention for multi-transformer plugins.
§10 Common patterns CSS-wrap note for direct Markdown-shaped HTML returns; render_markdown_collapsible already handles it.
§12 v2 directions Dispatch-auto-wrap design question.

work/tool-renderer-plugins.md shrink

The RFC's main content has graduated to dev-docs/plugins.md per the project's "work → dev-docs on shipping" lifecycle convention. Retained Open questions (deferred to v2) + Future extensions as the v2 backlog (drop -784 lines, keep 11 deferred items + 4 future extensions).

Test plan

  • just ci clean (format / test-all / ruff / pyright / ty)
  • Full unit suite: 1914 passed, 7 skipped (+3 from this PR)
  • All 12 inter-doc links resolve against actual files on disk
  • Smoke-test the public re-exports: python -c "from claude_code_log.plugins import render_markdown, render_markdown_collapsible; ..." works

Standing instruction

Per user's standing instruction: do not auto-merge. Once approved, monk will request a read-pass from clmail/carol (the original plugin author) as the acceptance-test loop the user named in #3185, then await user ack.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Plugin system now exposes HTML formatting utilities for plugin developers
    • Enhanced rendering support for tool result messages with collapsible content
  • Documentation

    • Expanded plugin development guide with clarified rendering contracts and usage examples

Review Change Stack

Phase 2 of the plugin-system follow-up — addresses the seven
doc/code gaps clmail/main + clmail/carol surfaced after their
v1 plugin shipped (mails #3218 + #3241), plus the long-deferred
``work/`` shrink for the now-graduated RFC.

**Public plugin API (`claude_code_log/plugins.py`)** — re-export
``render_markdown`` and ``render_markdown_collapsible`` from
``html/utils.py`` under the stable ``claude_code_log.plugins``
namespace. Resolved lazily via PEP-562 ``__getattr__`` because the
``html`` subpackage closes a circular loop with ``factories`` /
``utils`` at module-init time; ``TYPE_CHECKING`` block makes the
names visible to static checkers (ruff F822, pyright dunder-all)
so the re-export reads as a real surface declaration.

The stable namespace lets ``html/utils.py`` churn without
breaking plugin authors. ``_PUBLIC_HELPERS`` keeps the surface
narrow on purpose — every entry is an API commitment.

**ToolResultMessage worked example** —
``test/_plugins/clmail/.../tool_communicate_result.py`` mirrors the
existing ``tool_communicate.py`` (same fixture tool name, companion
``applies_to=(ToolResultMessage,)`` transformer) and demonstrates
the long-Markdown-body pattern via the new public helper. Three
tests in ``test_plugin_system.py::TestToolResultTransformerWith
CollapsibleBody`` cover short-body inline / long-body collapsed /
Strategy-2 class-side dispatch.

This single addition closes both #3218 gap 4 ("no worked example
for ToolResultMessage") and the long-Markdown-body Request 2 from
#3241 — same example, both audiences.

**`dev-docs/plugins.md` expansion**:

- §2 Quick start gains a constructor-signature reference block
  spelling out ``ToolUseMessage`` (with the kw-only ``skill_body``
  call-out) and ``ToolResultMessage`` field-copy patterns. The
  three-bullet reference-plugin list grows to include the new
  ``tool_communicate_result.py``.
- §4.1 "Plugin-facing helpers" is the new home for the
  ``render_markdown`` / ``render_markdown_collapsible`` signatures
  and use-when guidance; the section explicitly disclaims a wider
  surface ("add only on concrete plugin-author demand").
- §6 "Practical guide" picks up the ``HIGH`` vs ``FULL`` distinction
  for hook-style content, with the right rule-of-thumb question
  ("would a reviewer skimming at HIGH want to know this happened?").
- §8.2 picks up the per-tool-offset convention for multi-transformer
  plugins ("base ± single-digit offset to silence self-collisions
  on shared ``applies_to``").
- §10 gains the CSS-wrap note (``<div class="markdown">…</div>``
  for direct Markdown-shaped HTML returns) and the
  ``render_markdown_collapsible``-already-handles-it caveat.
- §12 picks up the dispatch-auto-wrap design question as an
  informational v2 direction so the conversation has a home.

**`work/tool-renderer-plugins.md` shrink** — the RFC's main content
has graduated to ``dev-docs/plugins.md`` per the project's "work →
dev-docs on shipping" lifecycle convention. Retained ``Open
questions (deferred to v2)`` + ``Future extensions`` as the v2
backlog (drop -784 lines, keep the 11 deferred items + 4 future
extensions). Reframed to align with the v1-shipped reality.

Test summary: 1914 passed, 7 skipped — +3 from this PR
(``TestToolResultTransformerWithCollapsibleBody``). ``just ci``
clean across format / test-all / ruff / pyright / ty.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f5c3441-5a29-4b05-9051-263b66bc4bc5

📥 Commits

Reviewing files that changed from the base of the PR and between e352670 and af1a9f1.

📒 Files selected for processing (9)
  • claude_code_log/html/renderer.py
  • claude_code_log/plugins.py
  • dev-docs/plugins.md
  • test/_plugins/clmail/pyproject.toml
  • test/_plugins/clmail/src/claude_code_log_clmail_test/transformers/hook_demotion.py
  • test/_plugins/clmail/src/claude_code_log_clmail_test/transformers/tool_communicate.py
  • test/_plugins/clmail/src/claude_code_log_clmail_test/transformers/tool_communicate_result.py
  • test/test_plugin_system.py
  • work/tool-renderer-plugins.md

📝 Walkthrough

Walkthrough

This PR implements the final plugin rendering dispatch system: HtmlRenderer._dispatch_format resolves plugin formatting methods by concrete class and synthesizes HTML from Markdown when format_html is absent, while the plugins module exports rendering helpers (render_markdown, render_markdown_collapsible) for plugin authors via lazy loading.

Changes

Plugin Rendering Dispatch System

Layer / File(s) Summary
HtmlRenderer dispatch and helper re-exports
claude_code_log/html/renderer.py, claude_code_log/plugins.py
HtmlRenderer._dispatch_format checks concrete class __dict__ for format_html (used as-is), else synthesizes HTML by wrapping format_markdown output in <div class="markdown">, else defers to base dispatcher. Module-level __getattr__ lazily imports render_markdown and render_markdown_collapsible from claude_code_log.html.utils and exposes them via __all__ for plugin authors.
Plugin rendering behavior documentation
dev-docs/plugins.md
Documented dispatch contract branches, method signatures, class-side precedence, helper APIs, error handling, multi-transformer collision-avoidance conventions, and .markdown CSS scoping rules (including has_markdown wrapping behavior and synthesis warnings).
Reference ToolResultMessage transformer with collapsible rendering
test/_plugins/clmail/pyproject.toml, test/_plugins/clmail/src/claude_code_log_clmail_test/transformers/tool_communicate_result.py
Defined TestClmailCommunicateResultMessage (applies DetailLevel.LOW, always has_markdown=True, uses render_markdown_collapsible for long bodies with preview/full content separation) and ClmailCommunicateResultTransformer to filter and rewrite ToolResultMessage entries for a specific MCP tool.
Existing test plugins updated for dispatch synthesis
test/_plugins/clmail/src/claude_code_log_clmail_test/transformers/hook_demotion.py, test/_plugins/clmail/src/claude_code_log_clmail_test/transformers/tool_communicate.py
Removed explicit format_html overrides (which returned None to force fallback) from TestHookNotificationMessage and TestClmailCommunicateInputMessage; both now rely on HtmlRenderer synthesizing HTML from format_markdown.
Integration tests for rendering dispatch and synthesis
test/test_plugin_system.py
New TestToolResultTransformerWithCollapsibleBody validates short/long body collapsible rendering, class-side dispatch Strategy 2 selection, Markdown→HTML synthesis without None leaks, and has_markdown=True CSS scoping.
Archive RFC work document into v2 backlog
work/tool-renderer-plugins.md
Consolidated full design/RFC into a compact v2 backlog note retaining deferred questions and extension pointers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • daaain/claude-code-log#166: Documents the plugin and rendering contracts that this PR's code implements, especially the _dispatch_format class-resolution order and Markdown→HTML synthesis behavior.
  • daaain/claude-code-log#83: Adds format_WebSearchInput/Output and title_WebSearchInput/Output handlers for HtmlRenderer that depend on the new _dispatch_format resolution behavior.
  • daaain/claude-code-log#169: Earlier PR extending the unified plugin system's renderer-side _dispatch_format/_dispatch_title resolution logic, which this PR now completes with Markdown→HTML synthesis and helper exports.

Suggested labels

plugin

Poem

🐰 Hop! The renderer springs to action,
Class methods caught with clear extraction,
Markdown flows to HTML's grace,
Plugins play with newfound space!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch review/plugin-system-impl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

cboos and others added 2 commits May 25, 2026 20:46
Three findings from clmail/carol's read pass against e6218eb
(mail #3249):

**Wall A — `format_html(...) -> None` doesn't fall back to
mistune.** The dispatcher returns `None` verbatim and the host
template renders the literal string "None" in the card body. The
reference test plugins' ``return None  # fall back to mistune``
pattern was misleading — a copy-paste user inherits the bug.

Fix in two places:
- Honest doc note in `dev-docs/plugins.md` §4 explaining the v1
  state and pointing at the v2 dispatch auto-wrap (§12) for the
  long-term fix.
- Reference plugins (`hook_demotion.py`, `tool_communicate.py`)
  switch to the explicit
  ``render_markdown(self.format_markdown(...))`` pattern with a
  comment explaining why ``return None`` doesn't work today.

The dispatcher fix itself (have the MRO walk continue on
class-side None, with HTML re-attempting via the markdown path)
is deliberately deferred to a separate PR — broader renderer
behaviour change with snapshot implications.

**Wall B — `is_error=True` on ToolResultMessage subclasses.**
Setting it triggers the host's standard error chrome (🚨 emoji +
red `.tool_result.error` CSS class) via `html/utils.py`'s
`isinstance(content, ToolResultMessage) and content.is_error`
gate. Bash errors use the same primitive. Documented in §4 right
after the v1 caveat so plugin authors writing result-side
transformers discover it without grepping the renderer.

**Cleaner CSS-scope alternative — `has_markdown = True`.** The
host template at `html/templates/transcript.html:146` reads
`message.content.has_markdown` and flips the `markdown` class
onto the wrapping `<div class='content'>` automatically.
Built-ins (`AwaySummaryMessage`, `TeammateMessage`,
`AssistantTextMessage`) use this exclusively — no inline
`<div class="markdown">` wrapping in their `format_html`.

§10 now presents `has_markdown = True` as the preferred path
with the inline-wrap recipe as the fallback for fine-grained
scope control. `tool_communicate_result.py` picks up the
property as a worked example. One new test
(`test_has_markdown_property_pins_markdown_css_scope`) pins
the contract.

Test summary: 1915 passed, 7 skipped (+1 from this fixup,
+4 from PR #173 cumulatively). `just ci` clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Design pivot from user (relayed via main): with ``render_markdown``
and ``render_markdown_collapsible`` now public on
``claude_code_log.plugins``, the ``None``-as-fall-back-sentinel for
``format_X`` is dead complexity. New v1 contract:

- **Plugin class defines ``format_html``** → dispatcher uses the
  return value as-is. Must be a real string; no None sentinel.
- **Plugin class does NOT define ``format_html``** → dispatcher
  synthesizes HTML by rendering the class's ``format_markdown``
  through mistune, wrapped in ``<div class="markdown">``.

Supersedes the doc-only Wall A fix in b55e187 (which had plugin
authors write a ``render_markdown(self.format_markdown(...))``
shim explicitly). The shim is now the dispatcher's job.

**`HtmlRenderer._dispatch_format` override** (``html/renderer.py``):
adds two actual-class precedence rules before deferring to the base
MRO walk:

1. ``type(obj).__dict__["format_html"]`` exists → use it verbatim.
2. ``type(obj).__dict__["format_markdown"]`` exists (but not
   ``format_html``) → synthesize via mistune + wrap.
3. Otherwise → ``super()._dispatch_format(obj, message)``.

Rule 2 deliberately wins over an ancestor's renderer-side
``format_<ClassName>``: a plugin author who defined
``format_markdown`` on a subclass meant for their Markdown to
drive the rendering, not for the parent class's built-in
renderer behaviour to take over. Without this, plugin subclasses
of ``UserTextMessage`` etc. would silently fall through to the
parent's HTML output.

**Reference plugins**:
- ``hook_demotion.py`` + ``tool_communicate.py`` drop the redundant
  ``format_html`` shim — they're now the canonical "Markdown only;
  HTML inherits via synthesis" demo. The comment block explains
  when to add ``format_html`` vs leave it absent.
- ``tool_communicate_result.py`` keeps ``format_html`` because it
  legitimately does something different (collapsible ``<details>``
  via ``render_markdown_collapsible`` for long bodies). Tightened
  the return annotation to ``str`` (was ``Optional[str]``) per
  the new contract.

**`dev-docs/plugins.md`**:
- §4 contract rewritten — table updated, the v1 caveat about
  None-rendering-as-literal-string replaced with the new clean
  rule. ``Error-shaped results`` paragraph kept (Wall B —
  ``is_error=True`` chrome).
- §5.1 new subsection — HtmlRenderer extension explaining
  actual-class precedence + the Markdown synthesis path.
- §10 picks up a note clarifying ``has_markdown`` semantics:
  synthesis path always wraps in ``<div class="markdown">`` (so
  ``has_markdown = True`` is not needed for that path); the
  property only matters for explicit ``format_html`` callers.
- §12 ``Dispatch auto-wrap for Markdown-shaped returns`` removed
  — the pivot subsumes it.

**`work/tool-renderer-plugins.md`**: companion v2-backlog entry
dropped (same rationale).

**Tests**: one new regression
(``test_absent_format_html_synthesizes_from_format_markdown``)
proves the synthesis path fires for a plugin subclass of
``UserTextMessage`` that defines only ``format_markdown`` — the
actual-class precedence rule (synthesis wins over inherited
Strategy 1) is the load-bearing behaviour. Asserts the rendered
HTML wraps in ``<div class="markdown">``, contains the
mistune-rendered ``<em>`` from the Markdown italics, and crucially
that the literal string "None" does NOT appear (the old bug).

Test summary: 1916 passed, 7 skipped (+1 from this commit). ``just
ci`` clean.

Worth re-pinging clmail/carol for a second-pass acceptance read
since this changes the contract she validated against in #3249.
Will do after pushing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cboos cboos mentioned this pull request May 25, 2026
3 tasks
From clmail/carol's second-pass acceptance read on c65cfdd
(mail #3285): the prior phrasing said `has_markdown = True`
"doesn't matter" for the synthesis path — technically correct
but under-prescriptive. The empirical pitfall: combining the
two produces a nested-wrap DOM (`<div class="content markdown">
<div class="markdown">…</div></div>`) because the synthesizer
wraps unconditionally AND the host template flips on the outer
`markdown` class via `has_markdown`.

Cache-masked-but-no-cache-visible — carol hit it during her
shipped-render simplification audit. Benign for CSS (selectors
don't care about depth), visible in the DOM, surprising on
view-source.

Sharpened from "doesn't matter" to "should NOT be set" for
synthesis classes; explicit rule of thumb added. One paragraph
under the existing §10 synthesis-path note, no contract change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cboos cboos marked this pull request as ready for review May 25, 2026 22:13
@cboos cboos merged commit 6392821 into main May 25, 2026
11 checks passed
@cboos cboos deleted the review/plugin-system-impl branch May 25, 2026 22:13
@cboos
Copy link
Copy Markdown
Collaborator Author

cboos commented May 25, 2026

Follow-ups needed... not the final word on the topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant