Skip to content

Render hook attachment entries at FULL detail (#128)#149

Merged
cboos merged 7 commits into
mainfrom
dev/improve-hook-support
May 10, 2026
Merged

Render hook attachment entries at FULL detail (#128)#149
cboos merged 7 commits into
mainfrom
dev/improve-hook-support

Conversation

@cboos
Copy link
Copy Markdown
Collaborator

@cboos cboos commented May 10, 2026

Summary

Closes #128. Promotes Claude Code's type: "attachment" JSONL entries (hook callbacks, deferred-tool deltas, queued commands, file references, todo reminders, etc.) from a structurally-only PassthroughTranscriptEntry into a typed AttachmentTranscriptEntry, and surfaces the four hook flavours — hook_success, hook_additional_context, hook_blocking_error, hook_non_blocking_error — as a renderable HookAttachmentMessage at --detail full. Non-hook flavours stay structural (DAG continuity preserved, dropped from the rendered output). Anchored on parentUuid per the issue, since the example UserPromptSubmit hook attachment carries a toolUseID matching nothing in the project.

What's in the diff

  • New entry + content type. AttachmentTranscriptEntry(BaseTranscriptEntry) in models.py, dispatched via transcript_factory.ENTRY_CREATORS; HookAttachmentMessage dataclass with kind discriminator (success / additional_context / blocking_error / non_blocking_error) carrying hook event/name, command, exit code, duration, content/stdout/stderr, blocking_error.
  • Factory. factories/attachment_factory.py returns HookAttachmentMessage for the four hook attachment.type values; returns None for everything else (silent drop, mirroring pre-existing Passthrough semantics).
  • DAG. New _StructuralEntry = (PassthroughTranscriptEntry, AttachmentTranscriptEntry) tuple covers fork-collapse, parallel-tool_use chain detection, and orphan-root classification. _is_expected_root_type accepts orphan attachment roots — SessionStart hooks legitimately fire pre-prompt.
  • Detail filtering. HookAttachmentMessage joins _HIGH_EXCLUDE_CLASSES (drops at HIGH and below alongside HookSummaryMessage); pre-render _filter_by_detail drops attachments at non-FULL since they're not in allowed_types.
  • Hierarchy and pairing. Hook content types (both flavours) sit at hierarchy level 3 alongside system info — without this they default to level 2 and claim subsequent system-info entries as descendants. Both are also excluded from the system→system parent/child uuid-pairing path (_try_pair_by_index) so a stop_hook_summary whose parentUuid is a hook attachment doesn't pair the hook as its parent and produce a spurious "▼ 1 system" fold-bar on every hook in dense transcripts.
  • Rendering. HTML formatter renders a collapsible <details> block (header summary + per-stream fenced bodies). Markdown mirrors the structure with adaptive code fences. JSON gets the new content type for free via dataclasses.asdict.
  • Visual register. Hook entries appear in dense bursts (plugins firing on every turn). Reduced to 75% font-size + 65% opacity with 500-weight headers so they read as secondary signal without losing the affordance. Title-level 🪝 glyph is the single hook indicator; body summary drops the redundant icon.

Test plan

  • 26 targeted tests in test/test_hook_attachment_rendering.py covering parsing of the issue's example payload, all four hook flavours, non-hook structural carve-out, formatter output, parent-uuid anchoring, hierarchy-level isolation (no claiming of system-info children), and pair-link isolation (no spurious system→system pairing) — plus end-to-end across detail levels (FULL renders, HIGH/LOW drop).
  • Fixture test/test_data/hook_attachments.jsonl covering all four hook flavours + a non-hook attachment for the structural carve-out.
  • Existing test/test_silent_skip.py updated and extended to assert attachments now parse as AttachmentTranscriptEntry; existing test/test_hook_summary.py updated to match the trimmed body shape.
  • Snapshot tests regenerated (CSS + HTML structure changes).
  • just ci clean: 1609 unit + 66 TUI + 39 browser + format + lint + pyright + ty.
  • Real-world smoke check: 2998-message transcript with 990 attachment entries renders 867 hook <details> at FULL, 0 at HIGH (matches the population's hook/non-hook ratio).
  • Verified no spurious fold-bars on the alice fixture transcript: 1022 hook divs, 0 with pair_first/pair_last, 0 with a fold-bar inside their own message div.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Attachments are now recognized and rendered as rich hook messages in HTML and Markdown (commands, exit codes, duration, blocking errors, stdout/stderr, metadata), with dedicated summaries and titles.
  • Bug Fixes

    • Attachments no longer affect session/message/token counts, deduplication, or system pairing; orphaned attachments are promoted silently without mispairing.
  • Style

    • New styling for hook attachments improves visual hierarchy, collapsible summaries, and output formatting.
  • Tests

    • Added fixtures and comprehensive unit/end-to-end tests for attachment parsing and rendering.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15a0649e-9b83-4074-80f3-4e033b549f47

📥 Commits

Reviewing files that changed from the base of the PR and between 0ef9e5f and dda4479.

📒 Files selected for processing (16)
  • claude_code_log/converter.py
  • claude_code_log/dag.py
  • claude_code_log/factories/attachment_factory.py
  • claude_code_log/factories/transcript_factory.py
  • claude_code_log/html/renderer.py
  • claude_code_log/html/system_formatters.py
  • claude_code_log/html/templates/components/message_styles.css
  • claude_code_log/html/utils.py
  • claude_code_log/markdown/renderer.py
  • claude_code_log/models.py
  • claude_code_log/renderer.py
  • test/__snapshots__/test_snapshot_html.ambr
  • test/test_data/hook_attachments.jsonl
  • test/test_hook_attachment_rendering.py
  • test/test_hook_summary.py
  • test/test_silent_skip.py
✅ Files skipped from review due to trivial changes (1)
  • test/test_data/hook_attachments.jsonl
🚧 Files skipped from review as they are similar to previous changes (13)
  • test/test_hook_summary.py
  • claude_code_log/markdown/renderer.py
  • claude_code_log/factories/attachment_factory.py
  • claude_code_log/factories/transcript_factory.py
  • claude_code_log/html/renderer.py
  • claude_code_log/models.py
  • claude_code_log/html/system_formatters.py
  • claude_code_log/html/utils.py
  • claude_code_log/converter.py
  • test/test_silent_skip.py
  • claude_code_log/renderer.py
  • claude_code_log/dag.py
  • test/snapshots/test_snapshot_html.ambr

📝 Walkthrough

Walkthrough

This PR models Claude Code attachment entries, converts hook-flavoured attachments to HookAttachmentMessage, treats attachments as structural DAG nodes, integrates them into rendering, adds HTML/Markdown formatters and CSS, and expands tests and snapshots.

Changes

Hook Attachment Support

Layer / File(s) Summary
Data Models & Message Contracts
claude_code_log/models.py
Adds PassthroughTranscriptEntry, AttachmentTranscriptEntry, updates TranscriptEntry union, and adds HookAttachmentMessage.
Entry Parsing & Message Factory
claude_code_log/factories/transcript_factory.py, claude_code_log/factories/attachment_factory.py
Registers "attachment" in ENTRY_CREATORS and implements create_attachment_message to convert hook-flavoured attachments to HookAttachmentMessage or return None for non-hook attachments.
Transcript Loading & Session Aggregation
claude_code_log/converter.py
load_transcript() recognizes "attachment"; deduplication uses message.uuid for attachments; session/cache/project-collection and team-name extraction skip attachments.
DAG Structural Handling
claude_code_log/dag.py
Classifies attachments as structural entries alongside passthroughs; suppresses orphan warnings for attachments; widens fork-collapse/pruning and expected-root detection.
Rendering Pipeline Integration
claude_code_log/renderer.py
Includes AttachmentTranscriptEntry in filtered stream; converts via factory during render; excludes hook messages from system UUID pairing; assigns level‑3 hierarchy; suppresses HookAttachmentMessage at HIGH/LOW detail; adds title dispatch.
Output Formatters & Utilities
claude_code_log/html/renderer.py, claude_code_log/html/system_formatters.py, claude_code_log/markdown/renderer.py, claude_code_log/html/utils.py
Adds format_hook_attachment_content, Html/Markdown formatters, HtmlRenderer hook formatting method, updates hook-summary rendering, and maps CSS class/emoji for hook attachments.
CSS Styling & Snapshots
claude_code_log/html/templates/components/message_styles.css, test/__snapshots__/test_snapshot_html.ambr
Introduces .system-hook-attachment and .hook-attachment rules, adjusts .system-hook typography/opacity, and updates snapshots.
Tests & Test Data
test/test_hook_attachment_rendering.py, test/test_data/hook_attachments.jsonl, test/test_hook_summary.py, test/test_silent_skip.py
Adds comprehensive tests: parsing/factory dispatch, non-hook structural verification, formatter unit tests, Markdown escaping/regression tests, detail-level suppression checks, DAG anchoring/pairing regressions, and fixture-based end-to-end rendering.

Sequence Diagram(s)

sequenceDiagram
  participant JSONL
  participant TranscriptFactory
  participant AttachmentFactory
  participant Renderer
  participant Formatter

  JSONL->>TranscriptFactory: parse entry type="attachment"
  TranscriptFactory->>AttachmentFactory: AttachmentTranscriptEntry
  AttachmentFactory-->>TranscriptFactory: HookAttachmentMessage or None
  TranscriptFactory->>Renderer: pass through message stream
  Renderer->>Formatter: format_hook_attachment_content / format_HookAttachmentMessage
  Formatter-->>Renderer: HTML/Markdown fragment
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

"I hop through JSONL fields with glee,
Attachments stitched into the tree,
Hooks unfurl their folded light,
Rendered, anchored, snug and right,
A rabbit nods — the DAG feels free." 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Render hook attachment entries at FULL detail (#128)' directly describes the main change: enabling hook attachment entries to be rendered at full detail level.
Linked Issues check ✅ Passed All objectives from #128 are implemented: hook attachments are rendered at FULL detail, payload fields are surfaced in HookAttachmentMessage, anchoring uses parentUuid, non-hook attachments remain structural, hierarchy placement is at level 3, and system pairing is prevented.
Out of Scope Changes check ✅ Passed All changes align with #128 requirements. File modifications introduce necessary transcript entry models, factories, rendering logic, styling, and comprehensive tests for hook attachment support without introducing unrelated functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 dev/improve-hook-support

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
test/test_hook_attachment_rendering.py (2)

326-326: 💤 Low value

Nit: del to silence unused-variable warnings is unusual.

Idiomatic Python uses _-prefixed names (already done for _nav) or simply doesn't bind the values. Since roots is destructured out of the tuple, you could rename it to _roots upfront and drop the del. Not a correctness concern.

♻️ Suggested refactor
-        roots, _nav, ctx = generate_template_messages(
+        _roots, _nav, ctx = generate_template_messages(
             _hook_messages(), detail=DetailLevel.FULL
         )
-        del roots, _nav  # only inspect ctx.messages

Also applies to: 436-436, 478-478

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_hook_attachment_rendering.py` at line 326, The test currently uses
"del roots, _nav" to silence unused-variable warnings; instead, rename the
destructured "roots" binding to "_roots" where the tuple is unpacked (keeping
"_nav" as-is) and remove the "del" statement(s); update the other occurrences
that use the same pattern (the other spots that delete roots/_nav) so they also
bind to "_roots" and drop the del lines, leaving only the variables you actually
inspect (e.g., ctx.messages).

113-115: 💤 Low value

Optional: replace # type: ignore[arg-type] with an isinstance narrow.

create_transcript_entry returns TranscriptEntry, so the type checker can't see that the parsed result is concretely an AttachmentTranscriptEntry. The first test (line 78) already does the right thing with an isinstance assert before invoking the factory. Mirroring that here would let you drop the three # type: ignore[arg-type] comments and add a useful invariant check at the same time.

♻️ Suggested refactor
         entry = create_transcript_entry(raw)
-        msg = create_attachment_message(entry)  # type: ignore[arg-type]
+        assert isinstance(entry, AttachmentTranscriptEntry)
+        msg = create_attachment_message(entry)

Also applies to: 136-138, 159-161

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_hook_attachment_rendering.py` around lines 113 - 115, Replace the
ad-hoc type ignores by narrowing the transcript entry with an isinstance check
before calling create_attachment_message: assert isinstance(entry,
AttachmentTranscriptEntry) where entry comes from create_transcript_entry(raw),
then call msg = create_attachment_message(entry) without # type:
ignore[arg-type] and assert isinstance(msg, HookAttachmentMessage); apply the
same change to the other two occurrences that currently use # type:
ignore[arg-type] so the type checker sees the concrete AttachmentTranscriptEntry
prior to invoking create_attachment_message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@claude_code_log/html/templates/components/message_styles.css`:
- Around line 399-408: The CSS blocks for .hook-attachment-output and
.hook-attachment-content use the deprecated word-wrap property; replace
word-wrap: break-word; with overflow-wrap: break-word; in those selector blocks
(and likewise update other occurrences you noted) so the behavior remains the
same but the deprecation warning is cleared—search for the symbol names
.hook-attachment-output and .hook-attachment-content (and other selectors where
word-wrap appears) and swap each word-wrap line to overflow-wrap: break-word;.

In `@claude_code_log/markdown/renderer.py`:
- Around line 478-503: The hook inline fields are currently inserted using raw
backticks which breaks if values contain backticks; update the places that
interpolate inline hook values to use the helper `_inline_code(...)` instead of
raw backticks: in the earlier hook-list block replace f"`{info.command}`" with
`_inline_code(info.command)` (where info.command is used), and inside
format_HookAttachmentMessage replace f"`{content.hook_name}`" with
`_inline_code(content.hook_name)`, f"`{content.hook_event}`" with
`_inline_code(content.hook_event)`, format the exit code as `f"exit
{_inline_code(content.exit_code)}"` and the duration as
`f"{_inline_code(content.duration_ms)} ms"` so all inline code fragments use the
central sanitizer.

---

Nitpick comments:
In `@test/test_hook_attachment_rendering.py`:
- Line 326: The test currently uses "del roots, _nav" to silence unused-variable
warnings; instead, rename the destructured "roots" binding to "_roots" where the
tuple is unpacked (keeping "_nav" as-is) and remove the "del" statement(s);
update the other occurrences that use the same pattern (the other spots that
delete roots/_nav) so they also bind to "_roots" and drop the del lines, leaving
only the variables you actually inspect (e.g., ctx.messages).
- Around line 113-115: Replace the ad-hoc type ignores by narrowing the
transcript entry with an isinstance check before calling
create_attachment_message: assert isinstance(entry, AttachmentTranscriptEntry)
where entry comes from create_transcript_entry(raw), then call msg =
create_attachment_message(entry) without # type: ignore[arg-type] and assert
isinstance(msg, HookAttachmentMessage); apply the same change to the other two
occurrences that currently use # type: ignore[arg-type] so the type checker sees
the concrete AttachmentTranscriptEntry prior to invoking
create_attachment_message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9a2880bc-8b17-4f08-98b5-8683ca8ce0e3

📥 Commits

Reviewing files that changed from the base of the PR and between ca5afa3 and 7373a53.

📒 Files selected for processing (16)
  • claude_code_log/converter.py
  • claude_code_log/dag.py
  • claude_code_log/factories/attachment_factory.py
  • claude_code_log/factories/transcript_factory.py
  • claude_code_log/html/renderer.py
  • claude_code_log/html/system_formatters.py
  • claude_code_log/html/templates/components/message_styles.css
  • claude_code_log/html/utils.py
  • claude_code_log/markdown/renderer.py
  • claude_code_log/models.py
  • claude_code_log/renderer.py
  • test/__snapshots__/test_snapshot_html.ambr
  • test/test_data/hook_attachments.jsonl
  • test/test_hook_attachment_rendering.py
  • test/test_hook_summary.py
  • test/test_silent_skip.py

Comment on lines +399 to +408
.hook-attachment-output,
.hook-attachment-content {
margin: 0;
padding: 0.5em;
background-color: var(--code-bg);
border-radius: 4px;
font-size: 0.85em;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify deprecated property usage in the changed hook attachment style block.
rg -n 'hook-attachment-(output|content)|word-wrap|overflow-wrap' claude_code_log/html/templates/components/message_styles.css

Repository: daaain/claude-code-log

Length of output: 313


Replace deprecated word-wrap with overflow-wrap in hook attachment blocks.

The word-wrap property is deprecated. Replace with overflow-wrap: break-word; at line 407 to preserve behavior and clear the deprecation warning. Note: the same deprecated property appears at lines 470, 492, 746, 900, and 1006 elsewhere in the file—consider addressing all instances for consistency.

Suggested patch
 .hook-attachment-output,
 .hook-attachment-content {
-    word-wrap: break-word;
+    overflow-wrap: break-word;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.hook-attachment-output,
.hook-attachment-content {
margin: 0;
padding: 0.5em;
background-color: var(--code-bg);
border-radius: 4px;
font-size: 0.85em;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
.hook-attachment-output,
.hook-attachment-content {
margin: 0;
padding: 0.5em;
background-color: var(--code-bg);
border-radius: 4px;
font-size: 0.85em;
white-space: pre-wrap;
overflow-wrap: break-word;
overflow-x: auto;
🧰 Tools
🪛 Stylelint (17.10.0)

[error] 407-407: Expected "word-wrap" to be "overflow-wrap" (property-no-deprecated)

(property-no-deprecated)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/html/templates/components/message_styles.css` around lines
399 - 408, The CSS blocks for .hook-attachment-output and
.hook-attachment-content use the deprecated word-wrap property; replace
word-wrap: break-word; with overflow-wrap: break-word; in those selector blocks
(and likewise update other occurrences you noted) so the behavior remains the
same but the deprecation warning is cleared—search for the symbol names
.hook-attachment-output and .hook-attachment-content (and other selectors where
word-wrap appears) and swap each word-wrap line to overflow-wrap: break-word;.

Comment thread claude_code_log/markdown/renderer.py Outdated
@cboos cboos force-pushed the dev/improve-hook-support branch from 7373a53 to b8f3b81 Compare May 10, 2026 11:20
@cboos
Copy link
Copy Markdown
Collaborator Author

cboos commented May 10, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

cboos added a commit that referenced this pull request May 10, 2026
CodeRabbit review on PR #149 flagged five places in the Markdown
hook formatters where inline fields are wrapped in raw backticks via
``f"`{value}`"``. If any value contains a backtick (a hook name like
``PostToolUse:`weird``` or a command like ``echo `pwd```), the
naive span closes at the inner tick and the surrounding Markdown
breaks.

Switched all five to ``_inline_code(...)``, the existing helper that
widens the fence past the longest internal backtick run and pads with
a space when the value starts/ends with a backtick:

- ``format_HookSummaryMessage``: ``info.command``.
- ``format_HookAttachmentMessage``: ``content.hook_name``,
  ``content.hook_event``, ``content.exit_code``,
  ``content.duration_ms``.

``exit_code`` and ``duration_ms`` are typed ``Optional[int]`` so they
go through ``str()`` first to keep the helper's ``str``-only contract
intact.

Two regression tests added under
``TestHookAttachmentMarkdownInlineCode``: a hook name and a hook
command containing backticks both render with the widened fence + space
padding pattern (``"`` echo `pwd` ``"``) instead of breaking the span.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cboos and others added 7 commits May 10, 2026 18:18
Hook callbacks recorded as ``type: "attachment"`` JSONL entries
(``hook_success``, ``hook_additional_context``, ``hook_blocking_error``,
``hook_non_blocking_error``) were previously dropped at parse time as
``PassthroughTranscriptEntry``, which meant the user couldn't inspect any
hook output at any detail level.

Promote attachment entries into a typed ``AttachmentTranscriptEntry`` so
their payload survives parsing. A new ``HookAttachmentMessage`` content
type carries the hook event/name, command, exit code, duration and
stdout/stderr/blocking-error text, and renders as a collapsible
``<details>`` block at ``DetailLevel.FULL``. ``HIGH`` and below drop it
alongside the existing ``HookSummaryMessage`` noise.

Anchoring follows ``parentUuid`` (not ``toolUseID``) per the issue —
``UserPromptSubmit`` hook attachments carry ``toolUseID`` values that
match nothing in the transcript, while ``parentUuid`` correctly anchors
on the prompt that fired them.

Non-hook attachment flavours (``deferred_tools_delta``, ``queued_command``,
``skill_listing``, ``task_reminder``, ``edited_text_file``, etc.) keep the
historical "structural in DAG, hidden from rendering" behaviour by
returning ``None`` from the factory; future flavours can grow their own
factory branch without touching the dispatch.

DAG-level structural-subtree detection now treats ``AttachmentTranscriptEntry``
the same as ``PassthroughTranscriptEntry`` so fork-collapse and orphan-root
classification still apply to chains of hook attachments.

Includes a fixture ``test/test_data/hook_attachments.jsonl`` covering
all four hook flavours plus a non-hook attachment (deferred_tools_delta)
to lock in the structural carve-out, and 16 unit/integration tests
verifying parsing, factory dispatch, parent-uuid anchoring, full-detail
rendering, and HIGH/LOW filtering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual feedback after #128 landed:

1. **Hook attachments** (the new ``HookAttachmentMessage``): the body
   summary doubled the title's 🪝 icon. Kept the label bold ("Hook
   output" / "Hook blocked" / "Hook errored" / "Hook added context")
   plus the event/exit/duration metadata, dropped the icon — the
   message header carries it once at the title level.

2. **Hook summaries** (the legacy ``HookSummaryMessage``): same
   treatment, plus a stronger trim. The body's generic
   "🪝 Hook output" / "🪝 Hook failed" subhead repeated the title with
   no extra signal. Replaced with the actual command preview (first
   ``hookInfo`` command, with ``(+N more)`` when multiple). Returns
   empty when there's neither command nor error so a content-free
   hook renders as a single-line title (matches the request to keep
   "only that title").

3. **HookSummaryMessage title icon**: ``get_message_emoji`` now
   returns 🪝 for both ``HookAttachmentMessage`` and
   ``HookSummaryMessage`` instead of falling back to ⚙️ for the
   summary case. Aligns the two hook-flavoured content types under
   the same title glyph.

4. **stderr padding**: the warning-bar ``border-left`` on
   ``.hook-attachment-stderr`` butted up against the text. Bumped
   ``padding-left`` to ``calc(0.5em + 3px)`` so the border breathes.

Markdown side gets the matching trim: ``format_HookSummaryMessage``
no longer emits "Hook produced output" — the body is just the
command list and any error text.

Tests updated: the previous "Hook failed" / "Hook output" body-label
assertions on ``test_hook_summary.py`` now look for the rendered
command preview instead, and ``test_hook_attachment_rendering.py``
asserts the body deliberately omits the icon (header carries it).
Snapshots regenerated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues surfaced when reviewing transcripts where hooks fire on
every turn (ClMail-style plugins).

## Visual weight (CSS)

Hook entries (both ``HookAttachmentMessage`` from #128 and the
legacy ``HookSummaryMessage``) accumulate in dense bursts and were
drowning out actual conversation. Dropped them from 90% font-size to
70%, added 0.9 opacity, and de-bolded the message header — they now
read as secondary signal without losing the affordance:

```css
div.system-hook-attachment, div.system-hook {
    font-size: 70%;
    opacity: 0.9;
}
div.system-hook-attachment .header, div.system-hook .header {
    font-weight: 400;
}
```

## Hierarchy-level mis-anchor (regression fix)

``HookAttachmentMessage`` carries ``message_type == "system"`` but
isn't a ``SystemMessage`` instance, so
``_get_message_hierarchy_level`` skipped the system info/warning
level-3 branch (which gates on ``isinstance(msg.content,
SystemMessage)``) and fell through to level 2 — same as
``stop_hook_summary`` entries.

At level 2, a hook attachment claimed any subsequent
``system-info`` (level 3) entries as descendants. Symptom: a
``UserPromptSubmit`` hook ended up parenting a ``/color`` system
entry from a *later* turn, AND the ``/color`` →
``Session color set to: green`` pair (which should anchor under
their own user turn) was split because ``/color`` was already
"claimed" by the hook.

Added an explicit branch placing both ``HookAttachmentMessage`` and
``HookSummaryMessage`` at level 3. Both are out-of-band callbacks,
not conversation turns; they sit alongside other system noise
rather than acting as containers for it. Regression test
``test_hook_does_not_claim_system_info_as_child`` builds the exact
shape that triggered the bug (user → hook → /color → Session color
set) and asserts the hook stays a leaf and the system_infos
anchor on the user turn.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Iteration on the visual-impact reduction:

- font-size 70% → 75%, opacity 0.9 → 65% on hook entries — readable
  at lower density, still clearly secondary content.
- ``.header`` font-weight 400 → 500 — distinct enough from body text
  to anchor the eye without competing with conversation turns.
- Dropped the ``<strong>`` around the hook attachment's body summary
  label ("Hook output" / "Hook blocked" / etc.). The 500-weight
  header already gives it the right level of emphasis; the extra
  bold made it feel double-weighted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hierarchy-level fix in c0cefe1 stopped hooks from claiming
system-info entries as ancestry children, but a separate pairing
pass kept producing the same visual symptom: every hook attachment
in a dense transcript rendered with a "▼ 1 system" fold-bar
because ``_try_pair_by_index`` paired it with a chained system
entry by uuid.

The mechanism: ``_build_pairing_indices`` indexes every
``type == "system"`` message by uuid for the parent/child pairing
path; ``_try_pair_by_index`` then looks up ``current.parent_uuid``
in that index. ``HookAttachmentMessage`` and ``HookSummaryMessage``
both carry ``type == "system"`` (their ``message_type`` is
``"system"``), so any chained system entry whose ``parentUuid``
points at a hook ended up pairing the hook as its parent.

In the alice fixture transcript, 132 ``stop_hook_summary`` entries
have a hook attachment as their ``parentUuid`` — every one of them
triggered the bug. Real-world rendering: spurious fold-bar on
hundreds of hooks per session.

Fix: exclude ``HookAttachmentMessage`` and ``HookSummaryMessage``
from both sides of the system→system pairing — the index build
and the lookup. Hook flavours stand on their own; they don't
participate in the conversational system-entry chain pairing
(which is intended for ``/context``-style multi-step output and
similar cases).

Verified on the alice fixture: 1022 hook divs, 0 with
``pair_first``/``pair_last``, 0 with a fold-bar inside their own
message div. Regression test
``test_hook_does_not_pair_with_chained_system_entry`` builds the
exact shape (hook → stop_hook_summary parented on the hook) and
asserts the hook has no pair links.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeRabbit review on PR #149 flagged five places in the Markdown
hook formatters where inline fields are wrapped in raw backticks via
``f"`{value}`"``. If any value contains a backtick (a hook name like
``PostToolUse:`weird``` or a command like ``echo `pwd```), the
naive span closes at the inner tick and the surrounding Markdown
breaks.

Switched all five to ``_inline_code(...)``, the existing helper that
widens the fence past the longest internal backtick run and pads with
a space when the value starts/ends with a backtick:

- ``format_HookSummaryMessage``: ``info.command``.
- ``format_HookAttachmentMessage``: ``content.hook_name``,
  ``content.hook_event``, ``content.exit_code``,
  ``content.duration_ms``.

``exit_code`` and ``duration_ms`` are typed ``Optional[int]`` so they
go through ``str()`` first to keep the helper's ``str``-only contract
intact.

Two regression tests added under
``TestHookAttachmentMarkdownInlineCode``: a hook name and a hook
command containing backticks both render with the widened fence + space
padding pattern (``"`` echo `pwd` ``"``) instead of breaking the span.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the .filter-toggle[data-type="sidechain"] block landed in
`7c2e6f6` directly on main (CSS-only, no snapshot refresh in that
commit). 28 lines added across the 7 affected fixtures (4 lines per
insertion); no other drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cboos cboos force-pushed the dev/improve-hook-support branch from 0ef9e5f to dda4479 Compare May 10, 2026 16:20
@cboos cboos merged commit 6ad4b0b into main May 10, 2026
11 checks passed
This was referenced May 10, 2026
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.

Improve hook support

1 participant