Skip to content

Render the built-in Monitor tool with Task-end backlink (#142)#147

Open
cboos wants to merge 2 commits intomainfrom
dev/monitor-tool-rendering
Open

Render the built-in Monitor tool with Task-end backlink (#142)#147
cboos wants to merge 2 commits intomainfrom
dev/monitor-tool-rendering

Conversation

@cboos
Copy link
Copy Markdown
Collaborator

@cboos cboos commented May 8, 2026

Summary

Closes #142. Two related pieces in one commit:

  • Specialised rendering for the built-in Monitor tool (the shell-stream watcher with description / command / timeout_ms / persistent). New MonitorInput (Pydantic) + MonitorOutput (dataclass), factory parser, HTML title 🔭 Monitor <description> + 4-row params grid + collapsible command via the existing render_collapsible_code helper, mirrored Markdown.
  • Task-end → originating tool_use backlink. When a <task-notification> carries <tool-use-id> (e.g. the harness emits this when a Monitor stream ends), the notification's Task ID value is wrapped in an anchor pointing at the originating tool_use card. Extended task_notification_factory to extract <tool-use-id> (older notifications without the field still parse — tool_use_id defaults to None). New renderer pass _link_tool_use_notifications runs after _link_async_notifications and sets spawning_task_message_index only on virgin notifications carrying a matching id. The HTML formatter now wraps the Task ID's <code> in <a class='task-notification-backlink'> whenever the index is set; the previous separate "Spawn" row is replaced by the Task-ID-as-link affordance. This unifies the async-agent and Monitor flows behind one rendering shape on the HTML side.

The Markdown side keeps the existing two-row shape — the markdown renderer's own comment notes that "Markdown only has session-level <a id=…> anchors", so message-level backlinks aren't structurally available in MD output.

Out of scope: mcp__plugin_clmail_clmail__control with action="monitor" (the actor-monitoring MCP tool) — distinct shape, can be a follow-up issue.

Test plan

  • just test: 1626 pass, 7 skipped (was 1607 on main; +19 new tests)
  • just test-tui: 66 pass
  • just test-browser: 39 pass, 1 skipped
  • just ci end-to-end clean (ruff + pyright + ty all green)
  • New regression suite test/test_monitor_rendering.py (20 cases) covers:
    • TestMonitorInputModel / TestMonitorOutputParser — direct factory tests including the missing-task-id fallback
    • TestMonitorHtmlFormatter — short-command-inline, long-command-collapsible, optional-fields-omitted, output paragraph
    • TestMonitorFixtureRendering — drives the real renderers against a small JSONL fixture (Monitor call + result + matching task-notification linked by toolu_test_monitor_42); asserts the backlink anchor matches the Monitor tool_use's div id
    • TestTaskNotificationToolUseIdBacklink — orthogonal contract: factory extracts tool-use-id, legacy notifications without it still parse, an orphan tool-use-id (no matching tool_use) renders as plain <code> rather than a dangling anchor
  • Snapshot drift: one line in test_snapshot_html.ambr — the async-agent fixture's notification card shifting from Task ID:<code> + Spawn:<a> to Task ID:<a><code>. Content-equivalent.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Monitor tool support: start monitors with command, description, optional timeout and persistence; results render in HTML/Markdown including collapsible long commands.
    • Task notifications can include a tool-use id and now backlink Task IDs to the originating tool call for improved traceability.
  • Tests

    • Added unit and end-to-end tests covering Monitor parsing, rendering, and backlink behavior.

Two changes addressing #142:

## 1. Monitor tool — typed input/output models, formatters, title

The built-in ``Monitor`` tool (shell-stream watcher; ``description`` /
``command`` / ``timeout_ms`` / ``persistent``) used to fall through to
the generic params-table render. Add full specialised rendering:

- ``MonitorInput`` (Pydantic) and ``MonitorOutput`` (dataclass) on
  ``models.py``, registered in the ``ToolInput`` and ``ToolOutput``
  unions.
- Factory: ``"Monitor"`` registered in ``TOOL_INPUT_MODELS`` so
  ``ToolUseMessage.input`` deserialises into ``MonitorInput``;
  ``parse_monitor_output`` registered in ``TOOL_OUTPUT_PARSERS`` and
  pulls the task id out of the start-confirmation paragraph for
  optional cross-reference (the body text is what actually renders).
- HTML formatter (``html/tool_formatters.py``):
  - Title: ``🔭 Monitor <description>``.
  - Input body: 4-row ``tool-params-table`` with ``description``,
    ``command``, ``timeout_ms``, ``persistent``. The ``command`` cell
    uses the existing ``render_collapsible_code`` helper when the body
    is multi-line / long, matching the line-count-badge affordance
    already used for other tool bodies; short single-line commands
    render in a plain ``<pre class='monitor-command'>``.
  - Output body: start-confirmation paragraph verbatim inside a
    ``<div class='monitor-output'>``.
- Markdown formatter mirrors the shape: bullet list of the three
  scalar fields, command in a fenced ``bash`` block, output text
  passes through.

## 2. Task-end → originating tool_use backlink

When a ``<task-notification>`` carries ``<tool-use-id>`` pointing back
at the originating tool_use (built-in Monitor's start-confirmation
returns the task id; later, when the stream ends, Claude Code injects
a notification carrying both the task id and the tool_use id), the
notification's Task ID value should link back to the originating
tool_use card.

- ``task_notification_factory`` now extracts ``<tool-use-id>`` from
  the notification block alongside ``<task-id>`` / ``<status>`` /
  ``<summary>``. Older notifications without the field still parse —
  the new ``tool_use_id`` is just ``None``.
- New renderer pass ``_link_tool_use_notifications`` (in
  ``renderer.py``, runs after ``_link_async_notifications``) builds
  a ``tool_use_id → message_index`` map and sets
  ``spawning_task_message_index`` on each notification carrying a
  matching id. Reuses the existing field name to keep the formatter
  single-shape — both flows now wire the same backlink slot.
- HTML formatter: when ``spawning_task_message_index`` is set, the
  Task ID's ``<code>`` is wrapped in
  ``<a class='task-notification-backlink' href='#msg-d-{N}'>``. The
  separate "Spawn" row that used to carry the backlink is removed —
  the Task ID value itself is the more discoverable affordance and
  matches the spec in #142.

This unifies the async-agent and Monitor flows behind one rendering
path. Async-agent notifications gain the same Task-ID-as-link shape,
which is a small intentional improvement to that flow as well.

## Tests

New ``test/test_monitor_rendering.py`` (20 cases):

- ``TestMonitorInputModel`` / ``TestMonitorOutputParser`` — direct
  factory tests including the missing-task-id fallback.
- ``TestMonitorHtmlFormatter`` — input grid, short-command-inline,
  long-command-collapsible, optional-fields-omitted, output paragraph.
- ``TestMonitorFixtureRendering`` — drives the real renderers against
  a small JSONL fixture (Monitor call + result + matching
  task-notification). Asserts: telescope icon and description in the
  title, four-row grid, collapsible command with line-count badge,
  result paragraph, and (most importantly) the backlink — the Task ID
  ``<a>`` href matches the Monitor tool_use's ``msg-d-N`` anchor.
- ``TestTaskNotificationToolUseIdBacklink`` — orthogonal to the
  Monitor flow: factory extracts ``tool-use-id``, legacy notifications
  without the field still parse, an orphan ``tool-use-id`` (no
  matching tool_use in the transcript) renders as plain ``<code>``
  rather than an anchor pointing at nothing.

Snapshot drift is one line in ``test_snapshot_html.ambr`` — exactly
the async-agent fixture's notification card shifting from the Spawn
row + plain-Task-ID shape to the Task-ID-as-link shape.

## CI

- ``just test``: 1610 pass, 7 skipped (was 1591 before; +19 new)
- ``just test-tui``: 66 pass
- ``just test-browser``: 39 pass, 1 skipped
- ruff + pyright clean on production files.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7a3e01a3-9a86-44f2-913d-b59f994afa1a

📥 Commits

Reviewing files that changed from the base of the PR and between 19f08dc and 9bfd1b5.

📒 Files selected for processing (3)
  • claude_code_log/html/tool_formatters.py
  • claude_code_log/markdown/renderer.py
  • test/test_monitor_rendering.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • claude_code_log/markdown/renderer.py
  • claude_code_log/html/tool_formatters.py
  • test/test_monitor_rendering.py

📝 Walkthrough

Walkthrough

Adds Monitor tool support and task-notification backlinking: new MonitorInput/MonitorOutput models, factory parsing (including ), HTML/Markdown formatters, renderer backlink pass linking notifications to tool_use messages, and tests/fixture updates.

Changes

Monitor Tool Support

Layer / File(s) Summary
Data Models & Contracts
claude_code_log/models.py
Adds MonitorInput (description, command, optional timeout_ms, persistent), MonitorOutput (text, optional task_id), extends ToolInput/ToolOutput, and adds tool_use_id: Optional[str] to TaskNotificationMessage.
Parsing & Factories
claude_code_log/factories/task_notification_factory.py, claude_code_log/factories/tool_factory.py
Task notification parser captures <tool-use-id> and passes it into TaskNotificationMessage; TOOL_INPUT_MODELS includes "Monitor": MonitorInput; adds parse_monitor_output to extract Monitor startup task id and registers it in TOOL_OUTPUT_PARSERS.
HTML Formatters
claude_code_log/html/tool_formatters.py
Adds format_monitor_input (tool-params table; collapsible code for long commands) and format_monitor_output (escaped monitor text); registers formatters in __all__.
HTML Renderer & Async Formatter
claude_code_log/html/renderer.py, claude_code_log/html/async_formatter.py
HtmlRenderer adds format_MonitorInput/format_MonitorOutput and title_MonitorInput; async formatter embeds backlink anchor into the "Task ID" cell when spawning_task_message_index is set and removes the old separate "Spawn" row.
Renderer Backlink Pass
claude_code_log/renderer.py
Adds _link_tool_use_notifications(ctx) and a post-processing pass that indexes tool_use messages by tool_use_id and links unmatched TaskNotificationMessage entries to their originating tool_use card.
Markdown Rendering
claude_code_log/markdown/renderer.py
Adds format_MonitorInput (bulleted metadata + fenced bash command), format_MonitorOutput (verbatim text), and title_MonitorInput (🔭 Monitor with description).
Tests, Fixtures & Snapshot
test/test_monitor_rendering.py, test/test_data/monitor_tool.jsonl, test/__snapshots__/test_snapshot_html.ambr
New unit and end-to-end tests for Monitor models/parsers/formatters and backlinking; adds fixture JSONL; updates HTML snapshot to show Task ID backlink and remove prior "Spawn" row.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • daaain/claude-code-log#132: Both PRs modify task-notification parsing and renderer backlinking logic; this PR extends the parser to capture <tool-use-id> and links notifications to tool_use messages.
  • daaain/claude-code-log#87: Similar extension of tool registries, formatters, and renderer plumbing for new tool types.
  • daaain/claude-code-log#37: Related work on propagating and linking tool-use metadata through the renderer pipeline.

Suggested reviewers

  • daaain

Poem

🐇🔭 I stitched a monitor, quiet and spry,
It starts, it links, a task id flies by,
Collapsible commands tucked in a view,
Backlinks lead readers to what tools do—
Hooray, small hops for logging delight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% 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 clearly and concisely summarizes the main changes: rendering the Monitor tool and adding a backlink from task-end notifications to the originating tool_use.
Linked Issues check ✅ Passed All coding requirements from #142 are met: Monitor tool rendering with description/command/timeout/persistent fields, collapsible commands for long/multiline text, and task-end → tool_use backlink via tool-use-id extraction.
Out of Scope Changes check ✅ Passed All changes directly support the Monitor tool rendering and task-notification backlink requirements; no unrelated modifications detected.

✏️ 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/monitor-tool-rendering

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 (1)
claude_code_log/html/tool_formatters.py (1)

700-724: ⚡ Quick win

Remove the unused params dict — it's dead code that contradicts the comment.

The params dict is populated (lines 702, 722–724) but is never referenced in the final output. The table rows are composed manually (lines 728–746), so params is unreachable. The comment at line 703 ("pass a placeholder through the table builder, then replace") describes an approach that was replaced by the manual row-building strategy — the dict is a leftover.

🧹 Proposed cleanup
 def format_monitor_input(monitor_input: MonitorInput) -> str:
-    params: dict[str, Any] = {"description": monitor_input.description}
-    # Command rendered separately for adaptive collapsibility — pass
-    # a placeholder through the table builder, then replace with the
-    # specialised cell. This keeps the row layout consistent with
-    # ``render_params_table`` (same CSS hooks downstream) while letting
-    # us keep the line-count badge for the multi-line case.
     command = monitor_input.command
     line_count = command.count("\n") + 1
     escaped_command = escape_html(command)
     if line_count > 5 or len(command) > 300:
         preview_lines = "\n".join(command.splitlines()[:3])
         preview_html = f"<pre>{escape_html(preview_lines)}</pre>"
         full_html = f"<pre>{escaped_command}</pre>"
         command_cell = render_collapsible_code(preview_html, full_html, line_count)
     else:
         command_cell = f"<pre class='monitor-command'>{escaped_command}</pre>"
 
-    if monitor_input.timeout_ms is not None:
-        params["timeout_ms"] = monitor_input.timeout_ms
-    if monitor_input.persistent is not None:
-        params["persistent"] = monitor_input.persistent
-
     # Compose the table by hand so the ``command`` row carries the
     # specialised cell instead of a generic stringification.
🤖 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/tool_formatters.py` around lines 700 - 724, The params
dict created in tool_formatters.py (params) is dead code — remove the params =
{"description": monitor_input.description} declaration and the later conditional
assignments to params["timeout_ms"] and params["persistent"], and also delete or
update the outdated comment about "pass a placeholder through the table
builder"; verify that nothing else references params and that the code continues
to use the manually built command_cell (render_collapsible_code / escape_html
usage remains unchanged).
🤖 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/markdown/renderer.py`:
- Around line 1421-1423: The title_MonitorInput method currently injects
input.description raw into the markdown heading; sanitize it by escaping
markdown control characters before interpolation. Update title_MonitorInput to
pass input.description through an escaping helper (e.g., escape_markdown or
similar utility) or implement one that escapes characters like *, _, `, [, ], (,
), #, +, -, !, >, | so the returned string returns f"🔭 Monitor
{escaped_description}" instead of the raw input.description.

In `@test/test_monitor_rendering.py`:
- Around line 362-366: The current module-level autouse fixture
_ensure_fixture_present skips the entire module if FIXTURE is missing; change it
to not be autouse and limit its scope to the test class that requires the
fixture. Modify the fixture signature to remove autouse and set scope="class"
(pytest.fixture(scope="class")), keep the same body referencing FIXTURE, and
then add `@pytest.mark.usefixtures`("_ensure_fixture_present") to the
TestMonitorFixtureRendering test class so only that class is skipped when the
JSONL fixture is absent.

---

Nitpick comments:
In `@claude_code_log/html/tool_formatters.py`:
- Around line 700-724: The params dict created in tool_formatters.py (params) is
dead code — remove the params = {"description": monitor_input.description}
declaration and the later conditional assignments to params["timeout_ms"] and
params["persistent"], and also delete or update the outdated comment about "pass
a placeholder through the table builder"; verify that nothing else references
params and that the code continues to use the manually built command_cell
(render_collapsible_code / escape_html usage remains unchanged).
🪄 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: 911df1eb-0932-44d0-ad3d-94f914d9e94e

📥 Commits

Reviewing files that changed from the base of the PR and between 4d0ecd1 and 19f08dc.

📒 Files selected for processing (11)
  • claude_code_log/factories/task_notification_factory.py
  • claude_code_log/factories/tool_factory.py
  • claude_code_log/html/async_formatter.py
  • claude_code_log/html/renderer.py
  • claude_code_log/html/tool_formatters.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/monitor_tool.jsonl
  • test/test_monitor_rendering.py

Comment thread claude_code_log/markdown/renderer.py Outdated
Comment thread test/test_monitor_rendering.py Outdated
Three findings, all applied. None blocking; all small.

## 1. Markdown title — escape via inline-code wrap

`title_MonitorInput` in `markdown/renderer.py` interpolated
`input.description` raw into the title heading. A description
containing markdown emphasis / heading metacharacters (``*``,
``_``, ``` ` ```, ``[``, ...) would leak into the rendered title and
either italicise unwanted runs or open code spans that swallow
following text.

Fix: wrap the description in inline code via the existing
`_inline_code` helper, matching the recipe already used by
`title_WebSearchInput` / `title_WebFetchInput` / `title_SkillInput`.
The helper widens the fence past any backtick run in the value and
pads when the value starts/ends with a tick — same protection,
established convention. Title becomes ``🔭 Monitor `<description>` ``.

The HTML side was already safe (`_tool_title` routes through
`escape_html`); the asymmetry is because HTML and Markdown have
different metacharacter sets.

## 2. Test fixture scope — class-only via `@pytest.mark.usefixtures`

`_ensure_fixture_present` was `scope="module", autouse=True`, which
means the *entire* test module skips when `monitor_tool.jsonl` is
missing — including the model-parsing / formatter-unit /
backlink-contract tests that don't need the fixture at all.

Fix: drop `autouse`, set `scope="class"`, and apply via
`@pytest.mark.usefixtures("_ensure_fixture_present")` on
`TestMonitorFixtureRendering` only. Now only the fixture-driven
end-to-end tests skip when the JSONL is absent; the rest of the
suite still runs.

(Pyright is added to the ignore list for `reportUnusedFunction` on
the fixture itself — pytest accesses it by name string via the mark,
which the static checker can't see. Standard pytest fixture pattern.)

## 3. Dead `params` dict removed (nitpick)

`format_monitor_input` populated a `params` dict but never used it —
leftover from an earlier draft that routed through
`render_params_table`. The final shape composes rows by hand to
keep the specialised collapsible-command cell. Dropped the dict, the
two later assignments, and the stale comment that mentioned the
discarded approach.

## Tests

- `just test`: 1626 pass, 7 skipped (unchanged count — no test added,
  just adjusted assertions and fixture wiring).
- `test_markdown_title_and_command_fence` updated to expect the new
  inline-code-wrapped title.
- ruff + pyright clean on the touched files.
@cboos
Copy link
Copy Markdown
Collaborator Author

cboos commented May 8, 2026

(Claude) Status update on the latest CodeRabbit review (round 1 on 19f08dc):

All three findings applied9bfd1b5.

  1. Markdown title escape (actionable): title_MonitorInput now wraps the description in inline code via the existing _inline_code helper, matching the recipe used by title_WebSearchInput / title_WebFetchInput / title_SkillInput. The helper widens the fence past any backtick run in the value, so descriptions containing markdown emphasis / heading metacharacters can no longer leak into the rendered title. The HTML side was already safe via escape_html in _tool_title.

  2. Test fixture scope (actionable): _ensure_fixture_present is no longer autouse=True / scope="module". Now scope="class" and applied via @pytest.mark.usefixtures("_ensure_fixture_present") on TestMonitorFixtureRendering only. The model-parsing / formatter-unit / backlink-contract tests now run independently of whether monitor_tool.jsonl is present.

  3. Dead params dict (nitpick): removed. Leftover from an earlier draft that routed through render_params_table; the final shape composes rows by hand to keep the specialised collapsible-command cell, so the dict was never read.

CI on alice (pre-push): just test 1626 pass / 7 skip; ruff + pyright clean on touched files. Snapshot fixtures untouched (no new tests, just adjusted assertions).

Thanks — the markdown-escape catch in particular would have been a real bug the first time someone passed a description containing a backtick.

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.

Add support for Monitor tool

1 participant