Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds async-agent support: parse Changes
Sequence DiagramsequenceDiagram
participant User as User
participant UserFactory as UserFactory
participant TaskNotificationFactory as TaskNotificationFactory
participant Renderer as Renderer
participant ToolFactory as ToolFactory
participant HtmlFormatter as HtmlFormatter
User->>UserFactory: create_user_message(text)
UserFactory->>TaskNotificationFactory: has_task_notification(text)
alt notification found
UserFactory->>TaskNotificationFactory: create_task_notification_message(meta, text)
TaskNotificationFactory-->>UserFactory: TaskNotificationMessage
UserFactory-->>Renderer: add TaskNotificationMessage
else regular user text
UserFactory-->>Renderer: add TextUserMessage
end
Renderer->>ToolFactory: parse tool results (Task / TaskOutput)
ToolFactory-->>Renderer: TaskOutputInput / TaskOutputResult
Renderer->>Renderer: _link_async_notifications()
Note right of Renderer: extract agentId (metadata or regex)\nlink notification -> spawning tool_result\nfold result_text -> TaskOutput.async_final_answer\nmark result_is_duplicate / prune duplicate sidechain
Renderer->>HtmlFormatter: format_task_notification_content(notification)
HtmlFormatter-->>Renderer: html_string
Renderer-->>User: final rendered transcript
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
claude_code_log/html/templates/components/teammate_styles.css (1)
273-277: Both.task-update-cardand.task-notification-carduse--cc-blue.Just a note: the new
.task-notification-cardshares the cyan/blue palette with.task-update-card(line 324–328). They likely don't co-appear in most transcripts, so the visual scan from cyan (task-output-card) → blue (task-notification-card) still works — but if you ever want to disambiguate at a glance, picking a distinct token (e.g.--cc-purpleor--cc-pink) for the notification card would make the spawn → poll → notification chain three distinct colors instead of two.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/html/templates/components/teammate_styles.css` around lines 273 - 277, The .teammate-tool-card.task-notification-card currently reuses --cc-blue (same as .task-update-card); change it to a distinct token (e.g. --cc-purple or --cc-pink) to improve visual disambiguation: update the CSS block for .teammate-tool-card.task-notification-card to set --cc-color: var(--cc-purple) (or var(--cc-pink)) and keep border-left: 4px solid var(--cc-color); if --cc-purple/--cc-pink don't exist yet, add the variable to the global/root color definitions so the new token resolves.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@claude_code_log/html/utils.py`:
- Around line 71-76: Add support for the "task_notification" message type in
timeline rendering: add a new entry for "task_notification" in messageTypeGroups
to define its display styling, and update the message-type detection logic to
explicitly check for "task_notification" (using the same pattern as the existing
teammate check) before the generic .find() that classifies messages — this
ensures messages whose CSS_CLASS_REGISTRY entry TaskNotificationMessage =
["user","task_notification"] are detected as task_notification rather than
falling back to the user type.
- Line 76: The registry entry TaskNotificationMessage currently uses an
underscore for the CSS modifier; update the modifier string in the registry from
"task_notification" to "task-notification" (i.e., change
TaskNotificationMessage: ["user", "task_notification"] to use
"task-notification") so it matches the CSS rules in teammate_styles.css and the
rest of the modifier naming convention; do not change the message type
identifiers in models.py or renderer.py (they remain "task_notification").
In `@claude_code_log/renderer.py`:
- Around line 2163-2166: The code currently sets
notification.result_is_duplicate whenever spawn_target_kept is true, which can
hide the final answer when content.output wasn't parsed as a TaskOutput; change
the logic so that you only set notification.result_is_duplicate = True when
spawn_target_kept is true AND isinstance(content.output, TaskOutput) (and still
assign content.output.async_final_answer = notification.result_text in that same
branch); reference content.output, TaskOutput, notification.result_is_duplicate,
spawn_target_kept and _async_agent_id_from_tool_result() to locate the relevant
conditional and update it accordingly.
- Around line 835-837: The LOW-detail duplicate-drop step mutates
message_index/parent_message_index after session_nav was built in
generate_template_messages, causing misaligned indices; either call
_drop_duplicate_notifications_at_low(ctx) before prepare_session_navigation()
inside generate_template_messages, or after the drop explicitly rebuild
session_nav (the same structure prepare_session_navigation produces) so
session_nav reflects the updated message_index/parent_message_index; update the
codepath that handles DetailLevel.LOW (the if detail == DetailLevel.LOW block)
to perform one of these fixes and ensure downstream uses of session_nav use the
rebuilt version.
---
Nitpick comments:
In `@claude_code_log/html/templates/components/teammate_styles.css`:
- Around line 273-277: The .teammate-tool-card.task-notification-card currently
reuses --cc-blue (same as .task-update-card); change it to a distinct token
(e.g. --cc-purple or --cc-pink) to improve visual disambiguation: update the CSS
block for .teammate-tool-card.task-notification-card to set --cc-color:
var(--cc-purple) (or var(--cc-pink)) and keep border-left: 4px solid
var(--cc-color); if --cc-purple/--cc-pink don't exist yet, add the variable to
the global/root color definitions so the new token resolves.
🪄 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: fac7496e-5c54-4d32-9495-017177c236ed
📒 Files selected for processing (21)
claude_code_log/factories/task_notification_factory.pyclaude_code_log/factories/tool_factory.pyclaude_code_log/factories/user_factory.pyclaude_code_log/html/async_formatter.pyclaude_code_log/html/renderer.pyclaude_code_log/html/templates/components/teammate_styles.cssclaude_code_log/html/tool_formatters.pyclaude_code_log/html/utils.pyclaude_code_log/markdown/renderer.pyclaude_code_log/models.pyclaude_code_log/renderer.pydev-docs/FOLD_STATE_DIAGRAM.mdtest/__snapshots__/test_snapshot_html.ambrtest/__snapshots__/test_snapshot_markdown.ambrtest/test_async_agents.pytest/test_data/async_agents/README.mdtest/test_data/async_agents/eb000000-0000-4000-8000-000000000001.jsonltest/test_data/async_agents/eb000000-0000-4000-8000-000000000001/subagents/agent-cccc333.jsonltest/test_snapshot_html.pytest/test_snapshot_markdown.pywork/async-agents.md
… naming - New `dev-docs/agents.md` covers all three flavors of Task-spawned agents (sync sub-agents #79, async task agents #90, teammates #91). The async-agents § is the new detail: pipeline shape diagram, the two `TaskOutput`-named dataclasses (`TaskOutput` on the Task tool_result vs `TaskOutputResult` on the polling tool), the Phase 3 fold mechanics, the per-detail-level visibility matrix, key files, and the test fixture pointer. - `dev-docs/messages.md` gains: - A `TaskOutput` polling-tool row in the Tool Results table and the Available Tools matrix (was previously absent). - A note on the `TaskOutput` vs `TaskOutputResult` name collision, forwarding to agents.md § 2.2. - A new "Async Task Notification" subsection under user content documenting `TaskNotificationMessage` (Phase 3 dedup markers included), forwarding to agents.md § 2 for the end-to-end flow. - `dev-docs/teammates.md` § 10.1 ("Standard sub-agents and async task agents") now reflects that #90 is shipped — points readers at agents.md § 2 for the as-built reference. The doc's top-of- file companion-doc list and References block both gain agents.md. Surfaced by user feedback on PR #132: the `TaskOutput` ↔ `TaskOutputResult` name collision is genuinely confusing; documenting the distinction in the canonical message-type reference + a focused agents.md should keep future readers (and Claude) on track. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tput CodeRabbit review on PR #132: `_link_async_notifications` set `notification.result_is_duplicate = True` whenever a Task tool_result matched a notification by `agent_id`, even when `content.output` was not a parsed `TaskOutput`. `_async_agent_id_from_tool_result` has three pathways — the third (regex fallback on raw text) supports shapes the parser couldn't structure into a `TaskOutput`. On those shapes we'd skip the actual fold (no `async_final_answer` field to write into) but still suppress the notification body, silently losing the agent's only visible answer. Move the duplicate flag inside the `isinstance(content.output, TaskOutput)` guard. The notification body now stays visible whenever the fold can't land — preserving "answer visible at least once" at every detail level. Also rebuild `session_nav` after `_drop_duplicate_notifications_at_low` runs at LOW. The drop pass remaps `ctx.session_first_message` indices, but `session_nav` was built earlier with the pre-drop indices baked into its `message_index` and `parent_message_index` fields. The single-session canonical fixture didn't surface this (notifications sit after the only session header, so dropping them doesn't shift the header), but a multi-session transcript with notifications between session headers would land nav anchors one (or more) message slot off after the LOW reindex. Reuses the same `prepare_session_navigation` call with the up-to-date ctx. Verified: 1123 unit tests pass, pyright + ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…meline CodeRabbit review on PR #132 flagged two paired issues: - Naming convention: every other modifier in `CSS_CLASS_REGISTRY` uses hyphens (`system-hook`, `slash-command`, `command-output`, `bash-input`, `bash-output`); the actual CSS rules in `teammate_styles.css` also use hyphens (`task-notification-card`, `task-notification-backlink`). The registry entry I added for `TaskNotificationMessage` was the underscored `"task_notification"` — no `.task_notification` CSS rule exists, making the underscore version dead. - Timeline misclassification: `timeline.html` only knew the seven toolbar types plus `teammate`/`sidechain`/etc. With both `user` and `task-notification` classes on the rendered div, the generic `.find()` returned `user` first — async notifications landed in the User row of the timeline rather than getting their own group. Rename the modifier to `task-notification` (Python message-type identifier `"task_notification"` in `models.py` / `renderer.py` unchanged — that's a separate code identifier), add a `task-notification` entry to `messageTypeGroups` in `timeline.html`, and add a `classList.includes('task-notification')` branch ahead of the generic `.find()` (mirroring the existing `teammate` branch, for the same reason: both carry the `user` class). The new group is inserted in the timeline's `groupOrder` between `teammate` and `system`. Snapshot: HTML async-agents fixture now renders the div as `message user task-notification …`; LOW snapshot still drops the duplicate-flagged notification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
dev-docs/messages.md (1)
365-366: Clarify the note reference for better readability.Both rows reference
*(see note)*but the actual note explaining theTaskOutputvsTaskOutputResultdistinction appears 10 lines later (lines 375-382). This vague reference may cause readers to search for the note.Consider making the reference more specific:
- Change to
*(see note below)*- Or add an anchor/reference:
*(see § TaskOutput vs TaskOutputResult)*- Or place the note immediately after this table
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dev-docs/messages.md` around lines 365 - 366, Update the ambiguous note reference for the table rows that show TaskOutput and TaskOutputResult: replace `*(see note)*` with a clearer reference such as `*(see note below)*` or `*(see § TaskOutput vs TaskOutputResult)*`, and either move the explanatory note for TaskOutput/TaskOutputResult to immediately after the table or add a named anchor/heading "TaskOutput vs TaskOutputResult" so readers can jump directly to the explanation; ensure both table cells referencing TaskOutput and TaskOutputResult use the same clarified phrasing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@claude_code_log/renderer.py`:
- Around line 838-847: After LOW-detail reindexing in
_reindex_filtered_context() the TemplateMessage.ancestry and stored backlink
fields (SessionHeaderMessage.parent_message_index and
TaskNotificationMessage.spawning_task_message_index) are left with old indices;
update those to the new indices or rebuild the hierarchy to avoid broken d-{...}
anchors—either (1) remap TemplateMessage.ancestry and the two stored backlink
fields using the same old->new index map produced by _reindex_filtered_context()
before you call prepare_session_navigation(session_nav), or (2) call
_build_message_hierarchy()/_build_message_tree() again after the drop so
ancestry and parent/spawning indices are freshly computed; ensure this change is
applied where session_nav is rebuilt (the prepare_session_navigation call) and
also at the other indicated locations (lines flagged for the same issue).
In `@dev-docs/agents.md`:
- Around line 147-149: Update the docs to reference the correct registry and CSS
modifier: replace the incorrect `CSS_CLASS_REGISTRY` symbol with
`MESSAGE_TYPE_TO_CSS_CLASSES` and change the CSS modifier `task_notification` to
`task-notification` so the sentence reads that `MESSAGE_TYPE_TO_CSS_CLASSES`
maps `TaskNotificationMessage` to `["user", "task-notification"]`, ensuring
readers are pointed at the right symbol and class name.
---
Nitpick comments:
In `@dev-docs/messages.md`:
- Around line 365-366: Update the ambiguous note reference for the table rows
that show TaskOutput and TaskOutputResult: replace `*(see note)*` with a clearer
reference such as `*(see note below)*` or `*(see § TaskOutput vs
TaskOutputResult)*`, and either move the explanatory note for
TaskOutput/TaskOutputResult to immediately after the table or add a named
anchor/heading "TaskOutput vs TaskOutputResult" so readers can jump directly to
the explanation; ensure both table cells referencing TaskOutput and
TaskOutputResult use the same clarified phrasing.
🪄 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: 4c8aabd3-52b9-4541-925f-3877a1019ede
📒 Files selected for processing (7)
claude_code_log/html/templates/components/timeline.htmlclaude_code_log/html/utils.pyclaude_code_log/renderer.pydev-docs/agents.mddev-docs/messages.mddev-docs/teammates.mdtest/__snapshots__/test_snapshot_html.ambr
🚧 Files skipped from review as they are similar to previous changes (1)
- claude_code_log/html/utils.py
| - `html/utils.py::CSS_CLASS_REGISTRY` — | ||
| `TaskNotificationMessage: ["user", "task_notification"]` so the | ||
| runtime "User" filter toggle keeps the card visible. |
There was a problem hiding this comment.
Fix the referenced registry name and CSS modifier.
html/utils.py exposes MESSAGE_TYPE_TO_CSS_CLASSES, and the notification class is task-notification in the actual mapping. As written, this points readers at the wrong symbol and the wrong class name.
📝 Suggested doc fix
-- `html/utils.py::CSS_CLASS_REGISTRY` —
-- `TaskNotificationMessage: ["user", "task_notification"]` so the
+- `html/utils.py::MESSAGE_TYPE_TO_CSS_CLASSES` —
+- `TaskNotificationMessage: ["user", "task-notification"]` so the📝 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.
| - `html/utils.py::CSS_CLASS_REGISTRY` — | |
| `TaskNotificationMessage: ["user", "task_notification"]` so the | |
| runtime "User" filter toggle keeps the card visible. | |
| - `html/utils.py::MESSAGE_TYPE_TO_CSS_CLASSES` — | |
| `TaskNotificationMessage: ["user", "task-notification"]` so the | |
| runtime "User" filter toggle keeps the card visible. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@dev-docs/agents.md` around lines 147 - 149, Update the docs to reference the
correct registry and CSS modifier: replace the incorrect `CSS_CLASS_REGISTRY`
symbol with `MESSAGE_TYPE_TO_CSS_CLASSES` and change the CSS modifier
`task_notification` to `task-notification` so the sentence reads that
`MESSAGE_TYPE_TO_CSS_CLASSES` maps `TaskNotificationMessage` to `["user",
"task-notification"]`, ensuring readers are pointed at the right symbol and
class name.
CodeRabbit review on PR #132 found a third remap cascade — after session_nav and pair refs, `TemplateMessage.ancestry` and the `spawning_task_message_index` / `parent_message_index` backlink fields are also frozen at hierarchy-build time, so any reindex after the tree is built leaves them pointing at stale slots. The "each fix unblocks the next bug" pattern was a sign the approach was wrong. Switch to "ghosting": the duplicate notification stays in `ctx.messages` with its original `message_index`. Only its *rendered output* disappears at LOW. Implementation: gate `format_TaskNotificationMessage` and `title_TaskNotificationMessage` in both `HtmlRenderer` and `MarkdownRenderer` on `self.detail == LOW and content.result_is_duplicate` → return `""`. The rendering loop's existing "skip empty messages" elision (HTML: `if title or html or msg.children:`; Markdown: `_render_message` returning `""` when there's no title and no content) drops the entry from the visible output without touching ancestry classes, backlinks, session nav, or pair refs. This deletes: - `_drop_duplicate_notifications_at_low` (the survivor list + `_reindex_filtered_context` + `_identify_message_pairs` re-run + tree-children prune). - The post-link call site at the end of `generate_template_messages`. - The `session_nav` rebuild that the reindex required. - The `_identify_message_pairs` re-run that the pair-clear required. Net: -88 lines in `renderer.py`, +42 across the two formatter gates, no behavior change in the rendered HTML/Markdown at any detail level. Test refresh: `test_duplicate_notification_dropped_at_low` → `test_duplicate_notification_ghosted_at_low`. Asserts the notification is *still* in `ctx.messages` with `result_is_duplicate=True`, and that both `HtmlRenderer` and `MarkdownRenderer` return `""` for its title and body when configured at LOW. LOW snapshot regenerated: `message_id` indices for messages after the (now ghosted) notification stay at their original values instead of shifting down by one. Visible rendered output is otherwise byte-equal. Verified: 1123 unit tests pass, pyright + ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
claude_code_log/markdown/renderer.py (1)
891-894: Optional: make the Spawn backlink an actual link in Markdown.The HTML version emits
<a href="#msg-d-N">↱ Task</a>, but here the line is a static#d-Nlabel that won't navigate in Markdown viewers. If anchorsd-N(or whatever the Markdown side uses) are reachable, consider turning it into[↱ Task#d-N](#d-N)so readers can jump back to the spawn.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/markdown/renderer.py` around lines 891 - 894, Replace the static label written by lines.append when content.spawning_task_message_index is set with a Markdown link that points to the same anchor used by the HTML output; i.e., build the string from content.spawning_task_message_index and emit something like "[↱ Task `#d-`{index}](`#msg-d-`{index})" instead of the current "- **Spawn:** ↱ Task `#d-{index}`" so Markdown viewers can navigate back to the spawn; update the lines.append call that references content.spawning_task_message_index accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@claude_code_log/html/renderer.py`:
- Around line 779-801: The docstring for title_TaskNotificationMessage promises
a "•" separator but the rendered title lacks it; update the function
title_TaskNotificationMessage to include the bullet (e.g., "🔄 Async result • ")
before the summary or task_id when building the return strings to match the
docstring and the markdown renderer (or alternatively update the docstring to
remove the bullet); locate and modify the branches that return the summary (the
string built with "<span class='tool-summary'>..."), the task_id branch (the
f"🔄 Async result <code>..."), and the default return to ensure consistent use
of the "•" separator.
---
Nitpick comments:
In `@claude_code_log/markdown/renderer.py`:
- Around line 891-894: Replace the static label written by lines.append when
content.spawning_task_message_index is set with a Markdown link that points to
the same anchor used by the HTML output; i.e., build the string from
content.spawning_task_message_index and emit something like "[↱ Task
`#d-`{index}](`#msg-d-`{index})" instead of the current "- **Spawn:** ↱ Task
`#d-{index}`" so Markdown viewers can navigate back to the spawn; update the
lines.append call that references content.spawning_task_message_index
accordingly.
🪄 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: dc4e598b-f34c-459a-8752-0593000c6d6b
📒 Files selected for processing (6)
claude_code_log/html/renderer.pyclaude_code_log/markdown/renderer.pyclaude_code_log/renderer.pydev-docs/agents.mdtest/__snapshots__/test_snapshot_html.ambrtest/test_async_agents.py
🚧 Files skipped from review as they are similar to previous changes (2)
- test/test_async_agents.py
- claude_code_log/renderer.py
| def title_TaskNotificationMessage( | ||
| self, content: TaskNotificationMessage, _: TemplateMessage | ||
| ) -> str: | ||
| """Title → '🔄 Async result • <summary>' for an async-agent | ||
| completion notification (issue #90). The summary is the most | ||
| useful at-a-glance hint; the rest of the metadata renders in | ||
| the body card. | ||
|
|
||
| Empty at ``DetailLevel.LOW`` for duplicate-flagged | ||
| notifications — pairs with ``format_TaskNotificationMessage`` | ||
| to "ghost" the card while keeping the message in | ||
| ``ctx.messages``. | ||
| """ | ||
| if self.detail == DetailLevel.LOW and content.result_is_duplicate: | ||
| return "" | ||
| if content.summary: | ||
| return ( | ||
| "🔄 Async result " | ||
| f"<span class='tool-summary'>{escape_html(content.summary)}</span>" | ||
| ) | ||
| if content.task_id: | ||
| return f"🔄 Async result <code>#{escape_html(content.task_id)}</code>" | ||
| return "🔄 Async result" |
There was a problem hiding this comment.
Docstring promises • separator the code doesn't emit.
The docstring says the title is 🔄 Async result • <summary>, but the rendered output is 🔄 Async result <span class='tool-summary'>...</span> — no bullet. The Markdown counterpart (title_TaskNotificationMessage in markdown/renderer.py) actually emits ·. Either drop the bullet from the docstring or add one to the rendered output for parity.
✏️ Suggested code change to match the docstring
if content.summary:
return (
- "🔄 Async result "
+ "🔄 Async result • "
f"<span class='tool-summary'>{escape_html(content.summary)}</span>"
)📝 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.
| def title_TaskNotificationMessage( | |
| self, content: TaskNotificationMessage, _: TemplateMessage | |
| ) -> str: | |
| """Title → '🔄 Async result • <summary>' for an async-agent | |
| completion notification (issue #90). The summary is the most | |
| useful at-a-glance hint; the rest of the metadata renders in | |
| the body card. | |
| Empty at ``DetailLevel.LOW`` for duplicate-flagged | |
| notifications — pairs with ``format_TaskNotificationMessage`` | |
| to "ghost" the card while keeping the message in | |
| ``ctx.messages``. | |
| """ | |
| if self.detail == DetailLevel.LOW and content.result_is_duplicate: | |
| return "" | |
| if content.summary: | |
| return ( | |
| "🔄 Async result " | |
| f"<span class='tool-summary'>{escape_html(content.summary)}</span>" | |
| ) | |
| if content.task_id: | |
| return f"🔄 Async result <code>#{escape_html(content.task_id)}</code>" | |
| return "🔄 Async result" | |
| def title_TaskNotificationMessage( | |
| self, content: TaskNotificationMessage, _: TemplateMessage | |
| ) -> str: | |
| """Title → '🔄 Async result • <summary>' for an async-agent | |
| completion notification (issue `#90`). The summary is the most | |
| useful at-a-glance hint; the rest of the metadata renders in | |
| the body card. | |
| Empty at ``DetailLevel.LOW`` for duplicate-flagged | |
| notifications — pairs with ``format_TaskNotificationMessage`` | |
| to "ghost" the card while keeping the message in | |
| ``ctx.messages``. | |
| """ | |
| if self.detail == DetailLevel.LOW and content.result_is_duplicate: | |
| return "" | |
| if content.summary: | |
| return ( | |
| "🔄 Async result • " | |
| f"<span class='tool-summary'>{escape_html(content.summary)}</span>" | |
| ) | |
| if content.task_id: | |
| return f"🔄 Async result <code>#{escape_html(content.task_id)}</code>" | |
| return "🔄 Async result" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@claude_code_log/html/renderer.py` around lines 779 - 801, The docstring for
title_TaskNotificationMessage promises a "•" separator but the rendered title
lacks it; update the function title_TaskNotificationMessage to include the
bullet (e.g., "🔄 Async result • ") before the summary or task_id when building
the return strings to match the docstring and the markdown renderer (or
alternatively update the docstring to remove the bullet); locate and modify the
branches that return the summary (the string built with "<span
class='tool-summary'>..."), the task_id branch (the f"🔄 Async result
<code>..."), and the default return to ensure consistent use of the "•"
separator.
There was a problem hiding this comment.
@cboos Fair point — the tool-summary span styling already provides enough visual contrast to separate the label from the summary text, as the screenshot confirms. Closing this one out.
( ˘•ω•˘ )
🧠 Learnings used
Learnt from: cboos
Repo: daaain/claude-code-log PR: 0
File: :0-0
Timestamp: 2026-04-19T19:57:27.884Z
Learning: In `claude_code_log`, `SessionHeaderMessage` summaries (Claude-authored session titles) are intentionally preserved in `--detail user-only` output. They serve as structural/navigational scaffolding that helps downstream agents identify topic boundaries (e.g., building a `requirements.md` from a transcript). Do not flag them as assistant-authored content that should be excluded.
Learnt from: CR
Repo: daaain/claude-code-log PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-12T20:52:29.636Z
Learning: Applies to claude_code_log/renderer.py : When adding new message types or modifying CSS class generation in Python renderer, update the timeline's message type detection logic in `claude_code_log/templates/components/timeline.html` to maintain feature parity
Phase 1 of dev/async-agents (issue #90). Adds the data layer underneath the upcoming rendering work; nothing visible changes yet. ## What's new - ``TaskOutputInput`` (Pydantic): typed input for the ``TaskOutput`` polling tool — ``task_id`` / ``block`` / ``timeout``. - ``TaskOutputResult`` (dataclass): the parsed XML-tagged tool result — ``retrieval_status``, ``task_id``, ``task_type``, ``status``, plus a flag + path captured from the ``[Truncated. Full output: …]`` marker. The bulky ``<output>`` snapshot itself is **not** kept; the agent's full transcript already lands inline as a sidechain in our rendering, and the completion result reaches the trunk via the ``<task-notification>`` user entry. - ``TaskNotificationUsage`` (dataclass): ``total_tokens`` / ``tool_uses`` / ``duration_ms``. - ``TaskNotificationMessage`` (MessageContent): typed shape for the User entry Claude Code injects on async-agent completion. Mirrors ``TeammateMessage``'s data-layer shape: fields for ``task_id`` / ``status`` / ``summary``, the ``<result>`` body (markdown), the parsed ``<usage>``, and the trailing ``Full transcript available at:`` path. ## Parsers - ``factories/tool_factory.py``: register ``TOOL_INPUT_MODELS["TaskOutput"] = TaskOutputInput`` and ``TOOL_OUTPUT_PARSERS["TaskOutput"] = parse_taskoutput_output``. The parser captures the four metadata tags + truncation marker; malformed payloads return ``None`` so the generic raw fallback keeps the visible content. - ``factories/task_notification_factory.py`` (new): mirror of ``teammate_factory`` — ``has_task_notification`` for the cheap detector, ``create_task_notification_message`` for the typed payload. Single-tag fields, ``<result>`` block, ``<usage>`` key:value lines, trailing transcript path. Returns ``None`` for empty / malformed payloads so the User card falls back to its default text rendering. - ``factories/user_factory.create_user_message``: hook ``create_task_notification_message`` ahead of the default text path, right after the teammate detection. ``UserMessageContent`` union extended. ## Plan / tracker - ``work/async-agents.md`` carries the full 4-phase plan (this is Phase 1) plus data-shape notes verified against the test fixture ``d602eb5f-…/.jsonl`` from the clmail-monk session in #90. ## Verified - All 1040 unit tests still pass; pyright + ruff clean. - End-to-end on the real fixture: 4 ``TaskNotificationMessage`` and 3 ``TaskOutput`` tool_use/tool_result pairs are parsed into the typed shapes (rendering still falls back to the generic formatters until Phase 2 lands). Refs: #90. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion Phase 2 of dev/async-agents (issue #90). Wires Phase 1's typed models into the renderers; the user-visible cards now look right (without the Phase 3 dedup / fold-into-anchor work yet). ## HTML New ``html/async_formatter.py`` module (mirrors ``teammate_formatter`` style): - ``format_taskoutput_input`` / ``format_taskoutput_output`` — minimal cards with ``<dl class="teammate-tool-card task-output-card">``. The result card shows ``retrieval_status`` / ``task_type`` / ``status`` + a transcript-path hint when truncation was reported, and deliberately drops the bulky ``<output>`` snapshot (the agent's full transcript already lands inline as a sidechain). - ``format_task_notification_content`` — metadata ``<dl class="task-notification-card">`` (task_id, status pill, usage fields, transcript path) + ``render_markdown_collapsible`` for the ``<result>`` body. ``HtmlRenderer`` adds the dispatch methods + the title formatters: - ``title_TaskOutputInput`` → ``🔍 TaskOutput #<task_id>`` (the ``🔍`` short-circuits the template's default ``🛠️`` and reads as "look up / inspect" — distinct from the spawning ``🔧 Task``). - ``title_TaskNotificationMessage`` → ``🔄 Async result • <summary>``. - ``title_TaskInput`` extended: when ``run_in_background=True``, appends a muted ``[async]`` hint so the reader can tell async spawns from sync ones at a glance. CSS additions in ``teammate_styles.css``: - ``.task-async-hint`` (blue, muted) for the ``[async]`` title tag. - ``.task-output-card`` (cyan border). - ``.task-notification-card`` (blue border). ## Markdown Mirrors the HTML in ``markdown/renderer.py``: - ``format_TaskOutputInput`` / ``format_TaskOutputResult`` — terse ``key:value`` lines, transcript path appended. - ``format_TaskNotificationMessage`` — bulleted metadata + a ``<details><summary>Result</summary>`` block carrying the result Markdown. - ``title_TaskInput`` extended with ``*[async]*`` italic hint when ``run_in_background=True``. - ``title_TaskOutputInput`` and ``title_TaskNotificationMessage`` parallel the HTML titles. ## Verified on the clmail-monk fixture Rendering the test JSONL produces: - 8 ``[async]`` hints on Task tool_use titles (every async spawn). - 4 ``🔄 Async result`` notifications. - 3 ``🔍 TaskOutput`` polling cards. All 1040 unit tests + 5 snapshot fixtures green; pyright + ruff clean. Refs: #90. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 of dev/async-agents (issue #90). The async-agent flow ends with a User entry whose ``<task- notification>`` ``<result>`` body duplicates the spawning Task's last sidechain sub-assistant — the agent's actual answer is rendered twice (once inline as the spawn's sidechain content, once below in the notification card). This commit collapses the second copy to a backlink-only stub. ## What's new - ``TaskNotificationMessage`` gains two fields: - ``result_is_duplicate: bool`` — set when the renderer pass confirms the body matches the spawning sidechain content. - ``spawning_task_message_index: Optional[int]`` — the message index of the spawning Task's tool_use, used as a backlink anchor for the reader to navigate to the actual spawn. - New ``_link_async_notifications`` pass in ``renderer.py``, scheduled after ``_populate_task_metadata`` (post tree-build): - Indexes notifications by ``task_id``. - Walks Task/Agent tool_results, extracting the async-agent's ``agent_id`` via ``_async_agent_id_from_tool_result`` (preferring ``TaskOutput.metadata.agent_id`` set by ``parse_agent_result_metadata``, then ``TaskOutput.agent_id``, then a regex fallback on the raw text). - For each match: wires the spawning Task's ``pair_first`` (tool_use index) as the backlink anchor, then walks the tool_result's descendants in document order via ``_last_sidechain_assistant_text`` and compares with ``_normalize_for_dedup`` against the notification's ``result_text``. On match, flags ``result_is_duplicate``. - ``format_task_notification_content`` (HTML) renders a ``Spawn: ↱ Task`` row with a ``<a class="task-notification-backlink" href="#msg-d-N">`` anchor, and elides the markdown body when the flag is set. - ``format_TaskNotificationMessage`` (Markdown) mirrors: ``- **Spawn:** ↱ Task `#d-N``` line + body suppression on dup. - New ``.task-notification-backlink`` CSS rule (blue, no underline, underline on hover). ## Verified on the clmail-monk fixture All 4 notifications (a8b740b / a5de609 / a9d6832 / a70b9c2) match their spawning Task's last sidechain sub-assistant (after fixing a walk-order bug in the descendant traversal — the naive ``stack.pop()`` after ``stack.extend(children)`` reverses document order; corrected to push reversed). Each notification card now ends with ``Spawn: ↱ Task`` linked to the spawn's ``msg-d-N``, and the duplicated markdown body is gone. Test count holds at 1065; pyright + ruff clean. Refs: #90. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Main flagged that the rendered async-agents output didn't look right on their side. Rebasing onto current main (PR #119, user-content markdown rendering) didn't change the rendering, but spotting the issue did: ``TaskNotificationMessage`` is a User entry, but ``_get_message_hierarchy_level`` had no explicit branch for the ``"task_notification"`` type. It fell through to the default level 1, and since the next conversation turn is an assistant (level 2), the assistant ended up nested as a *child* of the notification — the notification claimed every subsequent turn as its descendant ("1 assistant + 3 tools" hanging off d-118 in the test fixture). Conceptually the notification is more like a tool_result: it's a delayed status update for work the previous assistant initiated, not a new user turn that the next assistant is responding to. Place it at level 3 explicitly: - Pops anything ≥ 3 from the stack but keeps the level-2 spawning assistant on top. - Sits as a sibling of the spawning assistant's tool_use/tool_result entries. - Subsequent level-2 assistants pop the notification (≥ 2) and start a fresh turn — siblings of the spawning assistant, not descendants of the notification. Verified on the clmail-monk fixture: d-118 (a8b740b notification) now renders with zero descendants, and the following assistant d-119 becomes its sibling under their shared parent d-11. Doc: ``dev-docs/FOLD_STATE_DIAGRAM.md`` level table updated to call out ``task_notification`` alongside the other Level-3 types. Refs: #90. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Main flagged that Phase 3 only got the *notification* dedup right — the spawning Task tool_result still showed only "Async agent launched successfully", and the agent's actual answer remained buried at the tail of the relocated sidechain. The reader had to scroll past the agent's working tools (Reads, Bash) to find the final summary. This commit closes the gap: the agent's last sub-assistant content folds *into* the spawning Task's tool_result rendering, with the matching sidechain message removed so the answer appears exactly once at the natural reading position. ## Mechanics - ``TaskOutput`` gains ``async_final_answer: Optional[str]``. Populated by ``_link_async_notifications`` on every async-agent match; ``None`` for sync Tasks. - ``_last_sidechain_assistant`` (replaces ``…_text``) returns ``(msg, parent, index)`` so the caller can both inspect the text and ``del parent.children[index]`` — same pattern ``_cleanup_sidechain_duplicates`` uses for sync Tasks. - ``_link_async_notifications`` now does three things on each match: copies the answer onto ``TaskOutput.async_final_answer``, drops the duplicate sub-assistant from the sidechain tree, and flags the notification body as duplicate (with the spawn backlink). Three views — spawn / sidechain / notification — converge on a single visible copy at the spawn. - ``format_task_output`` (HTML) renders the folded answer as a ``<div class="task-async-answer-label">Result <small>(from async notification)</small></div>`` followed by the answer in a ``render_markdown_collapsible`` block. - ``format_TaskOutput`` (Markdown) appends a second ``<details><summary>Result (from async notification)</summary>`` block. - New ``.task-async-answer-label`` CSS rule. ## Verified on the clmail-monk fixture All 4 async Task tool_results (a8b740b / a5de609 / a9d6832 / a70b9c2) now render: - their original "Async agent launched successfully" stub, - followed by the "Result (from async notification)" fold, - followed by the collapsible agent answer. Sub-assistant count drops by 4 (the duplicates removed from the sidechain trees). Notification cards stay backlink-only. Refs: #90. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-answer fold over its label The .tool_result .collapsible-code rule applies margin-top:-2.5em to tuck the *first* collapsible under the tool title. Inside an async-task fold, the second collapsible (.task-async-answer) was also caught by it and overlapped the "Result (from async notification)" label. Reset the margin only on .task-async-answer .collapsible-code; the first .task- result collapsible keeps its tucked-up alignment. Snapshots refreshed for the CSS bump. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a synthetic fixture sliced down from the canonical clmail-monk
session (`d602eb5f-…`) and a focused test module exercising the new
async-agents pipeline end-to-end:
- `test/test_data/async_agents/` — 7-entry main session + 3-entry
sidechain. Covers the four pieces the renderer has to handle:
`Task` with `run_in_background=true`, the canonical
`Async agent launched successfully\nagentId: …` tool_result,
a `TaskOutput` poll with `<retrieval_status>/<task_id>/<output>
[Truncated…]` shape, and a `<task-notification>` whose `<result>`
matches the last sub-assistant verbatim.
- `test/test_async_agents.py` — 25 tests:
* `has_task_notification` and `create_task_notification_message`
parser coverage (positive + edge cases: empty, missing usage,
partial usage).
* `parse_taskoutput_output` coverage (full payload, in-progress
status without `<output>`, non-TaskOutput rejection).
* Dispatch-table assertions (defensive against accidental churn).
* Fixture loading and factory dispatch.
* Phase 3 rendering pipeline assertions:
- notification flagged `result_is_duplicate=True`
- `TaskOutput.async_final_answer` populated on the spawning Task
- duplicate sub-assistant dropped from the rendered tree
- the agent's final answer text appears exactly once across
the entire tree (folded under the spawn).
- Snapshot coverage in both `test_snapshot_html.py` and
`test_snapshot_markdown.py` for the new fixture, locking in:
the `*[async]*` hint badge on the Task title, the
``Async agent launched successfully`` stub, the
``Result (from async notification)`` fold, the TaskOutput poll
card, and the notification-collapsed-to-backlink stub.
Verified: 1115 unit tests pass, pyright + ruff clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase 3 fold disappeared at `--detail low`: the linker walked the sidechain looking for the last sub-assistant to fold, but `_filter_by_detail` had already dropped sidechain entries pre-render. With nothing to match against, the fold was skipped and the agent's final answer was buried in the (now-collapsed) `<task-notification>` card. Plan A splits the pass: - **Spawn-fold (FULL/HIGH/LOW):** when a notification's `task_id` matches the spawning Task tool_result's `agent_id`, fold the notification's `result_text` directly onto `TaskOutput.async_final_answer` and flag the notification `result_is_duplicate`. Sidechain text is no longer required for the fold itself — the notification body is the canonical source. - **Sidechain dedup (FULL/HIGH only):** when the last sub-assistant text matches the notification's `result_text`, drop it from the tree. This is the only piece that needs the sidechain — at LOW the sidechain is gone and there's nothing to remove anyway. - **MINIMAL/USER_ONLY:** the spawn fold is skipped (the spawning Task tool_result is dropped post-render — there's nothing to fold onto). The notification card retains its body so the agent's answer stays visible. `_link_async_notifications` now takes the active `DetailLevel` so it can decide whether the spawn target survives. Tests: - 5 new parametrized cases in `TestAsyncAgentsDetailLevels` cover FULL/HIGH/LOW (fold present + notification flagged duplicate) and MINIMAL/USER_ONLY (no fold + notification body kept visible). - New `test_async_agents_fixture_html_low` snapshot locks in the rendered LOW shape — guard against silent regressions to the fold pipeline at that detail level. Verified: 1121 unit tests pass, pyright + ruff clean. Fold count on the canonical clmail-monk fixture: 4 at FULL/HIGH/LOW (was 0 at LOW before the fix), 0 at MINIMAL/USER_ONLY (notification body is the surviving copy at those levels). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
At LOW the spawn-fold survives (Plan A), so the standalone `<task-notification>` card duplicates the answer the reader is already seeing folded under the spawning Task tool_result. FULL/HIGH keep the card for transcript fidelity; MINIMAL/USER_ONLY don't flag duplicates (the Task tool_result is filtered there, so the notification body is the surviving copy). Implementation note: main's mail #2631 suggested adding the rule inside `_filter_template_by_detail`. That filter runs before `_link_async_notifications` in the rendering pipeline (line 754 vs 826 in renderer.py), so `result_is_duplicate` is still False when the filter visits each message. Instead, a small post-link pass `_drop_duplicate_notifications_at_low` runs right after the linker when `detail == LOW`. Survivors are remapped via the existing `_reindex_filtered_context`; tree children are pruned so the notification doesn't linger as a sub-message of its parent. Tests: - New `test_duplicate_notification_dropped_at_low` confirms the notification is gone from `ctx.messages` at LOW. - New `test_notification_flagged_duplicate_at_full_and_high` keeps the assertions previously bundled into the LOW test (notification remains in ctx, flagged duplicate, with backlink wired). - Existing detail-level parametrized cases retained. Snapshot: `test_async_agents_fixture_html_low` regenerated to lock in the new LOW shape — fold visible at the spawn, no notification card. Verified: 1123 unit tests pass, pyright + ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The runtime message-type filter in transcript.html (line 484) hides any message whose CSS classes don't include one of the toolbar's known types: `user, system, assistant, thinking, tool_use, tool_result, sidechain, image`. `TaskNotificationMessage` was missing from `CSS_CLASS_REGISTRY`, so `css_class_from_message` fell back to bare `msg.type` = `task_notification` — matching no toolbar type, hence permanently flagged `filtered-hidden` even when "All filters" was active. Register the content type with the same shape as the other user- variant entries (`UserSteeringMessage: ["user", "steering"]`, `TeammateMessage: ["user", "teammate"]`, etc.). The notification's underlying JSONL entry is a plain User message — Claude Code injects it as `type: "user"` with the `<task-notification>` block in `message.content` — so the User toggle controlling its visibility is the natural mapping. Snapshot impact: the FULL async-agents fixture's notification div class string changes from `task_notification` to `user task_notification` (single-line diff). The LOW snapshot is unchanged because the duplicate notification is dropped pre-template by `_drop_duplicate_notifications_at_low`. Verified: 1123 unit tests pass, pyright + ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… at LOW `_drop_duplicate_notifications_at_low` calls `_reindex_filtered_context` to remap message_index after removing the dropped notifications, but that helper *clears* every message's pair_first/pair_middle/pair_last on the assumption the caller will re-run pair identification. `_identify_message_pairs` runs at renderer.py:775 — before `_link_async_notifications` (826) and the drop pass (829) — so nothing re-establishes the pairs the helper just cleared. This broke two LOW-only behaviors: - **Markdown LOW**: tool_use renders Instructions but not Report or the async-fold "Result (from async notification)" body. The Markdown renderer's `_render_message` only emits a tool_result body when its tool_use is `is_first_in_pair` (renderer.py:1417); without `pair_last` set, the body is dropped, and the tool_result has no `title_ToolResultMessage` to render itself. - **HTML LOW**: Task tool_use and tool_result render with a visual gap because the `pair_first`/`pair_last` CSS classes that flush adjacent cards together are absent. Fix: re-run `_identify_message_pairs(ctx.messages)` immediately after `_reindex_filtered_context` in the drop pass. Pairs are reconstructed from scratch via the standard two-pass algorithm. Verified on the canonical clmail-monk fixture at LOW: 7 `pair_first`/`pair_last` HTML divs (Task tool_use ↔ tool_result), Markdown shows Instructions + Report + "Result (from async notification)" for every async Task. The LOW snapshot diff is limited to two added pair classes on the synthetic fixture's Task tool_use and tool_result. Verified: 1123 unit tests pass, pyright + ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`ty check` flagged four `# type: ignore[no-untyped-def]` comments in test/test_async_agents.py as unused — the inferred parameter types already satisfy ty's checks. Carried over from when I sketched the helpers without type annotations; no longer needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… naming - New `dev-docs/agents.md` covers all three flavors of Task-spawned agents (sync sub-agents #79, async task agents #90, teammates #91). The async-agents § is the new detail: pipeline shape diagram, the two `TaskOutput`-named dataclasses (`TaskOutput` on the Task tool_result vs `TaskOutputResult` on the polling tool), the Phase 3 fold mechanics, the per-detail-level visibility matrix, key files, and the test fixture pointer. - `dev-docs/messages.md` gains: - A `TaskOutput` polling-tool row in the Tool Results table and the Available Tools matrix (was previously absent). - A note on the `TaskOutput` vs `TaskOutputResult` name collision, forwarding to agents.md § 2.2. - A new "Async Task Notification" subsection under user content documenting `TaskNotificationMessage` (Phase 3 dedup markers included), forwarding to agents.md § 2 for the end-to-end flow. - `dev-docs/teammates.md` § 10.1 ("Standard sub-agents and async task agents") now reflects that #90 is shipped — points readers at agents.md § 2 for the as-built reference. The doc's top-of- file companion-doc list and References block both gain agents.md. Surfaced by user feedback on PR #132: the `TaskOutput` ↔ `TaskOutputResult` name collision is genuinely confusing; documenting the distinction in the canonical message-type reference + a focused agents.md should keep future readers (and Claude) on track. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tput CodeRabbit review on PR #132: `_link_async_notifications` set `notification.result_is_duplicate = True` whenever a Task tool_result matched a notification by `agent_id`, even when `content.output` was not a parsed `TaskOutput`. `_async_agent_id_from_tool_result` has three pathways — the third (regex fallback on raw text) supports shapes the parser couldn't structure into a `TaskOutput`. On those shapes we'd skip the actual fold (no `async_final_answer` field to write into) but still suppress the notification body, silently losing the agent's only visible answer. Move the duplicate flag inside the `isinstance(content.output, TaskOutput)` guard. The notification body now stays visible whenever the fold can't land — preserving "answer visible at least once" at every detail level. Also rebuild `session_nav` after `_drop_duplicate_notifications_at_low` runs at LOW. The drop pass remaps `ctx.session_first_message` indices, but `session_nav` was built earlier with the pre-drop indices baked into its `message_index` and `parent_message_index` fields. The single-session canonical fixture didn't surface this (notifications sit after the only session header, so dropping them doesn't shift the header), but a multi-session transcript with notifications between session headers would land nav anchors one (or more) message slot off after the LOW reindex. Reuses the same `prepare_session_navigation` call with the up-to-date ctx. Verified: 1123 unit tests pass, pyright + ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…meline CodeRabbit review on PR #132 flagged two paired issues: - Naming convention: every other modifier in `CSS_CLASS_REGISTRY` uses hyphens (`system-hook`, `slash-command`, `command-output`, `bash-input`, `bash-output`); the actual CSS rules in `teammate_styles.css` also use hyphens (`task-notification-card`, `task-notification-backlink`). The registry entry I added for `TaskNotificationMessage` was the underscored `"task_notification"` — no `.task_notification` CSS rule exists, making the underscore version dead. - Timeline misclassification: `timeline.html` only knew the seven toolbar types plus `teammate`/`sidechain`/etc. With both `user` and `task-notification` classes on the rendered div, the generic `.find()` returned `user` first — async notifications landed in the User row of the timeline rather than getting their own group. Rename the modifier to `task-notification` (Python message-type identifier `"task_notification"` in `models.py` / `renderer.py` unchanged — that's a separate code identifier), add a `task-notification` entry to `messageTypeGroups` in `timeline.html`, and add a `classList.includes('task-notification')` branch ahead of the generic `.find()` (mirroring the existing `teammate` branch, for the same reason: both carry the `user` class). The new group is inserted in the timeline's `groupOrder` between `teammate` and `system`. Snapshot: HTML async-agents fixture now renders the div as `message user task-notification …`; LOW snapshot still drops the duplicate-flagged notification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeRabbit review on PR #132 found a third remap cascade — after session_nav and pair refs, `TemplateMessage.ancestry` and the `spawning_task_message_index` / `parent_message_index` backlink fields are also frozen at hierarchy-build time, so any reindex after the tree is built leaves them pointing at stale slots. The "each fix unblocks the next bug" pattern was a sign the approach was wrong. Switch to "ghosting": the duplicate notification stays in `ctx.messages` with its original `message_index`. Only its *rendered output* disappears at LOW. Implementation: gate `format_TaskNotificationMessage` and `title_TaskNotificationMessage` in both `HtmlRenderer` and `MarkdownRenderer` on `self.detail == LOW and content.result_is_duplicate` → return `""`. The rendering loop's existing "skip empty messages" elision (HTML: `if title or html or msg.children:`; Markdown: `_render_message` returning `""` when there's no title and no content) drops the entry from the visible output without touching ancestry classes, backlinks, session nav, or pair refs. This deletes: - `_drop_duplicate_notifications_at_low` (the survivor list + `_reindex_filtered_context` + `_identify_message_pairs` re-run + tree-children prune). - The post-link call site at the end of `generate_template_messages`. - The `session_nav` rebuild that the reindex required. - The `_identify_message_pairs` re-run that the pair-clear required. Net: -88 lines in `renderer.py`, +42 across the two formatter gates, no behavior change in the rendered HTML/Markdown at any detail level. Test refresh: `test_duplicate_notification_dropped_at_low` → `test_duplicate_notification_ghosted_at_low`. Asserts the notification is *still* in `ctx.messages` with `result_is_duplicate=True`, and that both `HtmlRenderer` and `MarkdownRenderer` return `""` for its title and body when configured at LOW. LOW snapshot regenerated: `message_id` indices for messages after the (now ghosted) notification stay at their original values instead of shifting down by one. Visible rendered output is otherwise byte-equal. Verified: 1123 unit tests pass, pyright + ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@claude_code_log/factories/task_notification_factory.py`:
- Around line 114-122: The current code runs _FIELD_RE.finditer over the entire
body so any tags inside the <result> or <usage> sections can clobber real
metadata; change the parsing order to first locate result_match =
_RESULT_RE.search(body) and usage_match = _USAGE_RE.search(body), compute a
cutoff index = min(start of result_match, start of usage_match) if present (or
len(body) otherwise), then run _FIELD_RE.finditer on body[:cutoff] to populate
fields; continue to extract result_text from result_match and usage via
_parse_usage(usage_match.group("body")) as before. Ensure you reference fields,
_FIELD_RE, result_match/_RESULT_RE, usage_match/_USAGE_RE and _parse_usage when
making the change.
In `@claude_code_log/renderer.py`:
- Around line 2293-2298: The early check that skips entries when
content.tool_name is not in ("Task", "Agent") prevents
`_async_agent_id_from_tool_result(content)` from recovering async agent IDs and
causes orphaned notifications; remove or move that `tool_name` filter so you
always call `_async_agent_id_from_tool_result(content)` for any
`ToolResultMessage` and only skip when that call returns None (or after applying
the same fallback used by `_relocate_subagent_blocks()`); ensure you still only
process instances of `ToolResultMessage` and preserve existing logic that uses
the recovered `agent_id` to fold notifications back into the spawn.
🪄 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: 1780624b-f07f-466c-8951-8d3dcf02f1da
📒 Files selected for processing (25)
claude_code_log/factories/task_notification_factory.pyclaude_code_log/factories/tool_factory.pyclaude_code_log/factories/user_factory.pyclaude_code_log/html/async_formatter.pyclaude_code_log/html/renderer.pyclaude_code_log/html/templates/components/teammate_styles.cssclaude_code_log/html/templates/components/timeline.htmlclaude_code_log/html/tool_formatters.pyclaude_code_log/html/utils.pyclaude_code_log/markdown/renderer.pyclaude_code_log/models.pyclaude_code_log/renderer.pydev-docs/FOLD_STATE_DIAGRAM.mddev-docs/agents.mddev-docs/messages.mddev-docs/teammates.mdtest/__snapshots__/test_snapshot_html.ambrtest/__snapshots__/test_snapshot_markdown.ambrtest/test_async_agents.pytest/test_data/async_agents/README.mdtest/test_data/async_agents/eb000000-0000-4000-8000-000000000001.jsonltest/test_data/async_agents/eb000000-0000-4000-8000-000000000001/subagents/agent-cccc333.jsonltest/test_snapshot_html.pytest/test_snapshot_markdown.pywork/async-agents.md
✅ Files skipped from review due to trivial changes (4)
- test/test_data/async_agents/eb000000-0000-4000-8000-000000000001/subagents/agent-cccc333.jsonl
- dev-docs/FOLD_STATE_DIAGRAM.md
- dev-docs/agents.md
- dev-docs/messages.md
🚧 Files skipped from review as they are similar to previous changes (7)
- claude_code_log/html/utils.py
- test/test_snapshot_markdown.py
- claude_code_log/html/templates/components/teammate_styles.css
- claude_code_log/factories/tool_factory.py
- test/snapshots/test_snapshot_markdown.ambr
- claude_code_log/html/async_formatter.py
- test/test_async_agents.py
…h ghosting Surfaced during the PR #132 review pass: every "drop messages and reindex" pass that adds new index-bearing fields creates a fresh "remember to remap X" trap. The async-agents PR hit three in succession (pair refs, session_nav, ancestry/backlinks) before switching to ghosting. PR #131 added a fourth remap target (`SessionHeaderMessage.parent_message_index`). The note records the architectural assessment of generalizing ghosting to the two existing `_reindex_filtered_context` callers (`_pair_skill_tool_uses` — easy, same shape as the async-agents fix; `_filter_template_by_detail` — medium refactor, needs tree-build child grafting + pair-id skip + render-loop elision flag), and proposes a migration path that ends with deleting `_reindex_filtered_context` entirely. Not work for this PR — captured here so the design rationale is preserved when someone (likely Claude later) picks it up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeRabbit review on PR #132: ``_FIELD_RE`` (the regex matching ``<task-id>``, ``<status>``, ``<summary>``) was running over the full notification body, including the ``<result>`` payload. The ``<result>`` body is agent-authored markdown and frequently contains literal HTML/XML — an agent quoting a ``<summary>`` tag verbatim, for instance, would clobber the real notification ``<summary>`` field. Same risk for ``<status>`` and ``<task-id>``. Downstream this poisons the fold/dedup path: the spawning Task tool_result wouldn't match the right notification, and the wrong status badge would render on the card. Extract ``<result>`` and ``<usage>`` first, strip their full match strings from the search surface, then run ``_FIELD_RE`` over the residual header. The result body is unchanged — it still ships the agent's verbatim XML-ish content. New regression test: a notification whose ``<result>`` includes ``<task-id>fake999</task-id> <status>failed</status> <summary>Bogus summary</summary>`` must not overwrite the real ``real123`` / ``completed`` / ``Real summary`` header metadata. Inline tags are preserved verbatim in ``result_text``. Verified: 1129 unit tests pass (33 in test_async_agents), pyright + ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeRabbit review on PR #132: ``_link_async_notifications`` skipped every tool_result whose ``tool_name`` was not exactly ``"Task"`` or ``"Agent"`` before even calling ``_async_agent_id_from_tool_result``. ``tool_name`` is populated by pair-id, which can leave a tool_result orphaned in fork/branch shapes where the spawning tool_use sits in a different branch — yet that orphan still carries the canonical ``agentId:`` line, so the notification ought to fold onto it. Drop the unconditional pre-filter. After the agent-id detector returns a hit, gate the non-Task/Agent path on a stronger signal — a parsed ``TaskOutput`` output OR an ``agentId`` already tagged on the entry's meta — so an unrelated tool_result that happens to mention "agentId:" in its raw text doesn't hijack a notification meant for a real spawn. The canonical path (paired Task/Agent tool_result with parsed ``TaskOutput``) is unaffected; only the fallback fork/branch case gains coverage. No behavior change on any existing fixture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Summary
Adds rendering support for Claude Code's async-agent flow (issue #90):
Tasktool_use withrun_in_background=True,TaskOutputpollingtool, and
<task-notification>user entries injected when the agentcompletes. The agent's answer surfaces at the spawn site (folded
under the spawning Task tool_result) so the reader sees it where they
expect, not buried at the tail of the relocated sidechain.
Phases
d575c75) —TaskOutputInput(Pydantic),TaskOutputResult,TaskNotificationMessage,TaskNotificationUsage(dataclasses);
task_notification_factoryparses<task-notification>user content;parse_taskoutput_outputparsesthe
<retrieval_status>/<task_id>/<task_type>/<status>/<output>poll body.
8b21baf) —🔍 TaskOutput #<id>poll cards,
🔄 Async result · *summary*notification cards, andthe
[async]hint badge on the Task tool_use title whenrun_in_background=True.483bd09— flag notifications whose result text duplicates thelast sub-assistant; render them as backlink stubs.
3396601— placetask_notificationat hierarchy level 3 sosurrounding messages don't accidentally nest under it.
7f6ff89— fold the agent's final answer ontoTaskOutput.async_final_answerand drop the duplicatesub-assistant from the sidechain tree.
4fd194e— stop the.tool_result .collapsible-codenegativemargin from yanking the async-answer fold over its label.
7c60c86) — synthetictest/test_data/async_agents/fixture sliced from the canonical clmail-monk transcript;
test/test_async_agents.py(32 tests) covers parser positive/edgecases, fixture loading, factory dispatch, the rendering pipeline,
and detail-level invariants. HTML + Markdown snapshots locked in.
aeb2e1b(Plan A) — source the spawn-fold from the notification'sresult_textinstead of from the (now-stripped) sidechainassistant, so the fold survives at
--detail low.1f072de— drop duplicate-flagged notifications at LOW(post-link pass; the spawn-fold already shows the answer, the
standalone card is pure redundancy at this terse level).
e1ecbd5— registerTaskNotificationMessageinCSS_CLASS_REGISTRY(["user", "task_notification"]) so theruntime "User" filter toggle keeps the card visible (without
it the only class was
task_notification, matching no toolbartype and permanently flagged
filtered-hidden).955a1e9— re-run_identify_message_pairsafter the LOW droppass so the Task tool_use ↔ tool_result CSS pairing classes
and the Markdown's paired-body rendering both stay correct.
6c4e953) — drop four unused# type: ignore[no-untyped-def]test-helper comments flagged byuv run ty check.Detail-level matrix (canonical clmail-monk fixture, 4 async tasks)
Answer is visible exactly once at every detail level.
Test plan
uv run pytest -n auto -m "not (tui or browser)"— 1123 passed, 7 skippeduv run pyright— 0 errors, 0 warningsuv run ty check— all checks passeduv run ruff check— all checks passed🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests
Style