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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions claude_code_log/factories/task_notification_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,13 @@
re.DOTALL,
)

# Single-tag fields inside the notification.
# Single-tag fields inside the notification. ``tool-use-id`` is the
# originating ``toolu_...`` id from the spawning tool_use; surfaced here
# so the renderer can backlink the notification's Task ID value to the
# original tool_use card (#142). Older notifications didn't carry it,
# so the absence is benign β€” the dict get below returns None.
_FIELD_RE = re.compile(
r"<(?P<tag>task-id|status|summary)>(?P<body>.*?)</(?P=tag)>",
r"<(?P<tag>task-id|tool-use-id|status|summary)>(?P<body>.*?)</(?P=tag)>",
re.DOTALL,
)
_RESULT_RE = re.compile(r"<result>\s*(?P<body>.*?)\s*</result>", re.DOTALL)
Expand Down Expand Up @@ -153,4 +157,5 @@ def create_task_notification_message(
usage=usage,
transcript_path=transcript_path,
raw_text=text,
tool_use_id=fields.get("tool-use-id") or None,
)
32 changes: 32 additions & 0 deletions claude_code_log/factories/tool_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
ToolUseContent,
ToolUseMessage,
ToolUseResult,
MonitorInput,
SkillInput,
WebSearchInput,
WebFetchInput,
Expand All @@ -55,6 +56,7 @@
BashOutput,
EditOutput,
ExitPlanModeOutput,
MonitorOutput,
ReadOutput,
SendMessageOutput,
TaskCreateOutput,
Expand Down Expand Up @@ -99,6 +101,7 @@
"ExitPlanMode": ExitPlanModeInput,
"WebSearch": WebSearchInput,
"WebFetch": WebFetchInput,
"Monitor": MonitorInput,
"Skill": SkillInput,
# Teammates feature tools
"TeamCreate": TeamCreateInput,
Expand Down Expand Up @@ -705,6 +708,34 @@ def parse_webfetch_output(
return None


# Match ``Monitor started (task <id>, …)`` so the task id is available
# to downstream consumers without re-parsing the full body. The id is
# the short alphanumeric form (e.g. ``b07h5t4ng``) the harness echoes
# back; not load-bearing for rendering today (the body text is shown
# verbatim) but useful for the task-end backlink (#142) and any future
# UI that wants to cross-reference the originating Monitor card.
_MONITOR_TASK_ID_RE = re.compile(r"Monitor started \(task ([A-Za-z0-9]+)")


def parse_monitor_output(
tool_result: ToolResultContent,
file_path: Optional[str],
) -> Optional[MonitorOutput]:
"""Parse Monitor tool's start-confirmation result.

The harness emits a single paragraph confirming the monitor was
armed and naming the task id. Capture the raw text for rendering
and extract the task id when the format matches; both fields are
optional from the renderer's perspective.
"""
del file_path # Unused β€” Monitor's result text is self-contained.
text = _extract_tool_result_text(tool_result).strip()
if not text:
return None
task_match = _MONITOR_TASK_ID_RE.search(text)
return MonitorOutput(text=text, task_id=task_match.group(1) if task_match else None)


# =============================================================================
# Teammates feature tool output parsers
# =============================================================================
Expand Down Expand Up @@ -1004,6 +1035,7 @@ def parse_taskoutput_output(
"ExitPlanMode": parse_exitplanmode_output,
"WebSearch": parse_websearch_output,
"WebFetch": parse_webfetch_output,
"Monitor": parse_monitor_output,
# Teammates feature tools
"TeamCreate": parse_teamcreate_output,
"TeamDelete": parse_teamdelete_output,
Expand Down
29 changes: 16 additions & 13 deletions claude_code_log/html/async_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,22 @@ def format_task_notification_content(content: TaskNotificationMessage) -> str:
"""
rows: list[str] = []
if content.task_id:
rows.append(_row("Task ID", _code(content.task_id)))
if content.spawning_task_message_index is not None:
# Backlink anchor format matches the rest of the renderer
# ("d-{N}" β†’ "msg-d-{N}"; the template emits the corresponding
# id on every message div). Wrapping the Task ID itself in
# the anchor (rather than a separate "Spawn" row) keeps the
# affordance close to the value the reader is looking up
# β€” matches issue #142's spec for the Monitor backlink and
# is a cleaner shape for the agent-spawn case too.
anchor = f"msg-d-{content.spawning_task_message_index}"
task_id_html = (
f"<a class='task-notification-backlink' href='#{anchor}'>"
f"<code>{escape_html(content.task_id)}</code></a>"
)
else:
task_id_html = _code(content.task_id)
rows.append(_row("Task ID", task_id_html))
if content.status:
status_class = (
f"status-{content.status}"
Expand All @@ -146,18 +161,6 @@ def format_task_notification_content(content: TaskNotificationMessage) -> str:
rows.extend(_format_usage_rows(content.usage))
if content.transcript_path:
rows.append(_row("Transcript", _code(content.transcript_path)))
if content.spawning_task_message_index is not None:
# Backlink anchor format matches the rest of the renderer
# ("d-{N}" β†’ "msg-d-{N}"; the template emits the corresponding
# id on every message div).
anchor = f"msg-d-{content.spawning_task_message_index}"
rows.append(
_row(
"Spawn",
f"<a class='task-notification-backlink' href='#{anchor}'>"
f"&#x21b1; Task</a>",
)
)

parts: list[str] = []
if rows:
Expand Down
16 changes: 16 additions & 0 deletions claude_code_log/html/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
SkillInput,
WebSearchInput,
WebFetchInput,
MonitorInput,
MonitorOutput,
WriteInput,
# Tool output types
AskUserQuestionOutput,
Expand Down Expand Up @@ -150,6 +152,8 @@
format_websearch_output,
format_webfetch_input,
format_webfetch_output,
format_monitor_input,
format_monitor_output,
format_write_input,
format_write_output,
render_params_table,
Expand Down Expand Up @@ -589,6 +593,14 @@ def format_WebFetchOutput(self, output: WebFetchOutput, _: TemplateMessage) -> s
"""Format β†’ collapsible markdown with metadata badge."""
return format_webfetch_output(output)

def format_MonitorInput(self, input: MonitorInput, _: TemplateMessage) -> str:
"""Format β†’ 4-row params table with collapsible command."""
return format_monitor_input(input)

def format_MonitorOutput(self, output: MonitorOutput, _: TemplateMessage) -> str:
"""Format β†’ start-confirmation paragraph verbatim."""
return format_monitor_output(output)

# -------------------------------------------------------------------------
# Tool Input Title Methods (for Renderer.title_ToolUseMessage dispatch)
# -------------------------------------------------------------------------
Expand Down Expand Up @@ -699,6 +711,10 @@ def title_WebFetchInput(
"""Title β†’ '🌐 WebFetch <url>'."""
return self._tool_title(message, "🌐", input.url)

def title_MonitorInput(self, input: MonitorInput, message: TemplateMessage) -> str:
"""Title β†’ 'πŸ”­ Monitor <description>'."""
return self._tool_title(message, "πŸ”­", input.description)

def title_SkillInput(self, input: SkillInput, message: TemplateMessage) -> str:
"""Title β†’ 'πŸ’‘ Skill <skill_name>'."""
return self._tool_title(message, "πŸ’‘", input.skill)
Expand Down
69 changes: 69 additions & 0 deletions claude_code_log/html/tool_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from .utils import (
escape_html,
render_collapsible_code,
render_file_content_collapsible,
render_markdown_collapsible,
)
Expand All @@ -37,6 +38,8 @@
ExitPlanModeInput,
ExitPlanModeOutput,
GrepInput,
MonitorInput,
MonitorOutput,
MultiEditInput,
ReadInput,
ReadOutput,
Expand Down Expand Up @@ -680,6 +683,70 @@ def format_webfetch_output(output: WebFetchOutput) -> str:
return f"{badge_html}{content_html}"


# -- Monitor Tool -------------------------------------------------------------


def format_monitor_input(monitor_input: MonitorInput) -> str:
"""Format Monitor tool use as a key-value grid.

Renders four rows β€” ``description``, ``command``, ``timeout_ms``,
``persistent`` β€” using the same ``tool-params-table`` shape as
other multi-field tool inputs. The ``command`` value (often a
multi-line bash poll-loop) renders inside a collapsible block so
a long script doesn't dominate the card.

The ``description`` is shown both in the title and the body; the
body row anchors the rendering to the harness's exact field name
and keeps the card useful when a future title format changes.
"""
command = monitor_input.command
line_count = command.count("\n") + 1
escaped_command = escape_html(command)
if line_count > 5 or len(command) > 300:
# Use the collapsible-code helper for the same visual treatment
# other multi-line tool bodies get (line-count badge + preview).
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>"

# Compose the table by hand so the ``command`` row carries the
# specialised cell instead of a generic stringification.
rows: list[str] = []
rows.append(
f"<tr><td class='tool-param-key'>description</td>"
f"<td class='tool-param-value'>{escape_html(monitor_input.description)}</td></tr>"
)
rows.append(
f"<tr><td class='tool-param-key'>command</td>"
f"<td class='tool-param-value'>{command_cell}</td></tr>"
)
if monitor_input.timeout_ms is not None:
rows.append(
f"<tr><td class='tool-param-key'>timeout_ms</td>"
f"<td class='tool-param-value'>{escape_html(str(monitor_input.timeout_ms))}</td></tr>"
)
if monitor_input.persistent is not None:
rows.append(
f"<tr><td class='tool-param-key'>persistent</td>"
f"<td class='tool-param-value'>{escape_html(str(monitor_input.persistent))}</td></tr>"
)
return f"<table class='tool-params-table monitor-input'>{''.join(rows)}</table>"


def format_monitor_output(output: MonitorOutput) -> str:
"""Format Monitor tool result β€” the start-confirmation paragraph.

The harness emits a single paragraph confirming the monitor was
armed and naming the task id. Render verbatim inside a paragraph
block; the body is short enough that no collapsibility is worth
the chrome.
"""
return f"<div class='monitor-output'>{escape_html(output.text)}</div>"


# -- Generic Parameter Table --------------------------------------------------


Expand Down Expand Up @@ -871,6 +938,7 @@ def format_tool_result_content_raw(tool_result: ToolResultContent) -> str:
"format_grep_input",
"format_websearch_input",
"format_webfetch_input",
"format_monitor_input",
# Tool output formatters (called by HtmlRenderer.format_{OutputClass})
"format_read_output",
"format_write_output",
Expand All @@ -881,6 +949,7 @@ def format_tool_result_content_raw(tool_result: ToolResultContent) -> str:
"format_exitplanmode_output",
"format_websearch_output",
"format_webfetch_output",
"format_monitor_output",
# Fallback for ToolResultContent
"format_tool_result_content_raw",
# Legacy formatters (still used)
Expand Down
36 changes: 36 additions & 0 deletions claude_code_log/markdown/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
SkillInput,
WebSearchInput,
WebFetchInput,
MonitorInput,
MonitorOutput,
WriteInput,
# Tool output types
AskUserQuestionOutput,
Expand Down Expand Up @@ -837,6 +839,28 @@ def format_WebFetchInput(self, input: WebFetchInput, _: TemplateMessage) -> str:
return self._code_fence(input.prompt)
return ""

def format_MonitorInput(self, input: MonitorInput, _: TemplateMessage) -> str:
"""Format β†’ bullet list of fields with the command in a fenced block.

Mirrors the HTML 4-row grid in plain Markdown. The command's
adaptive fence width comes from ``_code_fence``; the description
is also in the title but kept here so the body stands alone.
"""
lines: list[str] = [f"- **description:** {input.description}"]
if input.timeout_ms is not None:
lines.append(f"- **timeout_ms:** {input.timeout_ms}")
if input.persistent is not None:
lines.append(f"- **persistent:** {input.persistent}")
lines.append("")
lines.append("**command:**")
lines.append("")
lines.append(self._code_fence(input.command, "bash"))
return "\n".join(lines)

def format_MonitorOutput(self, output: MonitorOutput, _: TemplateMessage) -> str:
"""Format β†’ start-confirmation paragraph verbatim."""
return output.text

def format_SkillInput(self, _input: SkillInput, _: TemplateMessage) -> str:
"""Format β†’ '' (skill name in title; body folded in via skill_body)."""
return ""
Expand Down Expand Up @@ -1394,6 +1418,18 @@ def title_WebFetchInput(self, input: WebFetchInput, _: TemplateMessage) -> str:
url = input.url[:60] + "…" if len(input.url) > 60 else input.url
return f"🌐 WebFetch `{url}`"

def title_MonitorInput(self, input: MonitorInput, _: TemplateMessage) -> str:
"""Title β†’ 'πŸ”­ Monitor `<description>`'.

Wraps the description in inline code via ``_inline_code`` β€”
same recipe as ``title_WebSearchInput`` / ``title_WebFetchInput``
/ ``title_SkillInput``. The helper widens the fence past any
backtick run in the value and escapes the value from
Markdown emphasis / heading metacharacters that would otherwise
leak into the rendered title (e.g. ``*`` / ``_`` / ``[``).
"""
return f"πŸ”­ Monitor {_inline_code(input.description)}"

def title_SkillInput(self, input: SkillInput, _: TemplateMessage) -> str:
"""Title β†’ 'πŸ’‘ Skill `<skill_name>`'."""
return f"πŸ’‘ Skill `{input.skill}`"
Expand Down
Loading
Loading