Skip to content

Render background task completions as their own panel, not user prompts - #109

Open
dchaudhari7177 wants to merge 1 commit into
simonw:mainfrom
dchaudhari7177:fix/render-task-notifications
Open

Render background task completions as their own panel, not user prompts#109
dchaudhari7177 wants to merge 1 commit into
simonw:mainfrom
dchaudhari7177:fix/render-task-notifications

Conversation

@dchaudhari7177

Copy link
Copy Markdown

Closes #99.

The problem

is_tool_result_message() only recognises the list of tool_result blocks form. A background task reports back as a type: user entry whose message.content is a plain string of XML, so it fell through to the User branch — and a long session read as though the human had been typing system XML at intervals.

Result

Running your repro JSONL verbatim, the fourth entry now renders:

<div class="message task-notification" id="msg-2026-01-01T10-00-31-000Z">
  <div class="message-header"><span class="role-label">Background task</span></div>
  <div class="message-content">
    <div class="task-notification-body">
      <span class="task-status task-status-completed">completed</span>
      <span class="task-summary">Background command &quot;Long task&quot; completed (exit code 0)</span>
      <div class="task-meta">task <code>bash_AAA</code> &middot; tool <code>toolu_AAA</code></div>
    </div>
  </div>
</div>

No class="message user" anywhere in the output. The <tool-use-id> is surfaced because it is what ties the notification back to the tool_use that started it, as @Necmttn suggested.

Its own class rather than reusing tool-reply: this is a lifecycle event with no tool output attached, and slate grey reads as quieter than the orange, which seemed right for something the human did not ask for at that moment. Easy to change if you'd rather it share the tool-reply styling.

Regex, not an XML parser

<summary> carries free text, which can itself contain markup, so the payload is not guaranteed well-formed — an ElementTree parse would raise on exactly the inputs most worth displaying.

More importantly the fallback still renders as a notification: a payload yielding no fields shows its raw text under the same panel. Falling back to the user branch would reintroduce the bug for precisely the malformed cases. test_unparseable_xml_still_renders_as_a_notification pins that.

Fields are escaped on the way in, so <summary><script>alert(1)</script></summary> is inert.

Detection

Only a string content whose stripped form starts with <task-notification>. test_a_real_user_prompt_is_not pins the negative side, including a prompt that merely mentions the tag in passing — a human writing "I asked <task-notification> about it" must still be a user message.

Tests

8 tests in TestTaskNotifications, written first and watched fail (collection error on the missing import, then the behaviour).

uv run pytest: 130 passed, 18 snapshots passed. uv run black . clean.

Four snapshots are updated. The diff is 8 added CSS lines each and nothing removed — no rendered message changed. I checked the diff before regenerating rather than after.

Also verified end to end by running claude-code-transcripts json repro.jsonl -o /tmp/out on your repro and reading the HTML, which is the output quoted above.

AI disclosure

Written with Claude Code (Claude Opus 5), following AGENTS.md: failing tests first, then implementation, then Black. The end-to-end repro run and the snapshot-diff review were done by hand.

A tool started with run_in_background=true reports back as a `type: user`
entry whose message.content is a plain XML string starting with
<task-notification> -- not the usual list of tool_result blocks. Since
is_tool_result_message() only recognises the list form, these fell through
to the "User" branch, and a long session read as though the human had been
typing system-generated XML at intervals.

They now classify as a "Background task" panel showing the status, the
summary, and the task and tool-use ids that tie the entry back to the
tool_use that started it.

Parsed with a regex rather than an XML parser: <summary> carries free text
which may itself contain markup, so the payload is not guaranteed
well-formed. A payload that yields no fields still renders as a
notification with its raw text -- falling back to the user branch would
reintroduce exactly the bug being fixed. Fields are escaped on the way in.

The four snapshot updates are the added CSS only; no rendered message
changed.

Closes simonw#99
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.

Background <task-notification> entries are rendered as User messages

1 participant