Skip to content

Cross-link TaskOutput / TaskUpdate headers back to their spawn (#154)#158

Open
cboos wants to merge 2 commits into
mainfrom
dev/link-taskoutput-tasks
Open

Cross-link TaskOutput / TaskUpdate headers back to their spawn (#154)#158
cboos wants to merge 2 commits into
mainfrom
dev/link-taskoutput-tasks

Conversation

@cboos
Copy link
Copy Markdown
Collaborator

@cboos cboos commented May 14, 2026

Summary

TaskOutput polls and TaskUpdate calls reference a task_id
minted by an earlier tool_use. The polled / updated card showed
the id verbatim but readers had no way to jump back to the
originating spawn — they had to scroll-and-grep.

Mirror the affordance already in place for async-agent notifications
(#142), the Monitor tool (#142 / #147), and the Cron* family
(#148 / #152): wire a renderer pass (structural twin of
_link_cron_jobs_by_id, two-pass index → stamp) that indexes each
id-minting tool_use, then stamps consumer cards with a
creating_call_message_index. The title formatter wraps #<id> in
<a class='task-id-backlink' href='#msg-d-N'>, same dotted-underline
visual as .cron-id-backlink.

Three flows share the pass:

  1. Bash with run_in_background=trueTaskOutput polls
    carrying taskType: local_bash. Background id sourced from
    the structured toolUseResult.backgroundTaskId field (not
    text parsing).
  2. Async-agent Task launch → TaskOutput polls carrying
    taskType: local_agent. Agent id sourced via the existing
    _async_agent_id_from_tool_result helper.
  3. TaskCreateTaskUpdate. Todo-list #N ids form a parallel
    id space; the consumer's TaskUpdateInput carries it
    unambiguously so the same pass handles both shapes.

Markdown stays plain-text — the #<id> form surfaces in the title
so the reader can grep across the document; clickable backlinks
remain HTML-only.

Snapshot delta: the new .task-id-backlink CSS rule everywhere +
two existing async-agents fixture cards picking up real backlinks
(cccc333 → spawn, #1 → create card) — no new fixture cards.

Closes #154.

Test plan

  • uv run pytest -m "not (tui or browser)" — 1697 passed,
    7 skipped, no regressions
  • ruff format / ruff check clean
  • pyright — 0 errors, 0 warnings
  • 8 new tests in test/test_task_id_linking.py pin anchor
    href against the originating spawn for all three flows;
    _spawn_anchor locates the spawn div by the title-tooltip's
    API id (stable across message-index renumbering)
  • Snapshot regen (pytest test/test_snapshot_html.py -n0 --snapshot-update) verified against expected delta

Follow-up (optional, non-blocking)

The else: agent_id = _async_agent_id_from_tool_result(...)
branch in step 1 of _link_task_id_consumers runs for every
non-Bash tool_result without a background_task_id, including
TaskCreate / TaskList / generic tool_results whose raw text
won't match the agent-id regex. Functionally correct, just wasted
work on transcripts dominated by todo activity. A one-line gate
on tool_name in {"Task"} would skip the regex when it can't
match. Not worth doing speculatively — leaving for a future
profile-driven cleanup.

Summary by CodeRabbit

  • New Features

    • Task IDs in output and update headers now link back to their originating creation/spawn messages for easier navigation.
    • Background task identifiers from local runs are captured and used to connect later polling outputs to their originating run.
  • Style

    • Added visual styling for task ID backlinks with dotted underlines and hover effects.
  • Tests

    • Added end-to-end fixtures and tests covering task-id backlinking and cross-session isolation.

Review Change Stack

`TaskOutput` polls and `TaskUpdate` calls reference a `task_id`
minted by an earlier tool_use. The polled card showed the id
verbatim, but readers had no way to jump back to the originating
spawn — they had to scroll-and-grep.

Mirror the affordance already in place for async-agent notifications
(#142), the `Monitor` tool (#142 / #147), and the Cron* family
(#148 / #152): wire a renderer pass that walks the tool_result
stream, indexes each id-minting tool_use, then stamps consumer
cards with a `creating_call_message_index` so the title formatter
can wrap `#<id>` in `<a class='task-id-backlink' href='#msg-d-N'>`.

Three flows share the pass:

1. `Bash` with `run_in_background=true` → `TaskOutput` polls
   carrying `taskType: local_bash`. Background id sourced from
   `toolUseResult.backgroundTaskId` (structured field, not text
   parsing).
2. Async-agent `Task` launch → `TaskOutput` polls carrying
   `taskType: local_agent`. Agent id sourced via the existing
   `_async_agent_id_from_tool_result` helper (already handles both
   the structured `agentId` and the raw-text fallback).
3. `TaskCreate` → `TaskUpdate`. Todo-list `#N` ids form a parallel
   id space; the consumer's `TaskUpdateInput` carries it
   unambiguously so the same pass handles both shapes.

Markdown stays plain-text — the same `#<id>` form surfaces in the
title (the renderer omits the anchor wrapper there) so the reader
can grep across the document; clickable backlinks remain
HTML-only.

Tests cover all three paths against a synthetic JSONL fixture and
pin the anchor href against the originating tool_use's `msg-d-N`
id by locating it via the title-tooltip's API id (stable across
message-index renumbering). The async-agents snapshot now picks
up the cccc333 backlink alongside the new CSS.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 11de4846-5baf-4ec4-9780-2a505e36b2f7

📥 Commits

Reviewing files that changed from the base of the PR and between 79ffc8c and cc75d90.

📒 Files selected for processing (3)
  • claude_code_log/renderer.py
  • test/test_data/task_id_linking_cross_session.jsonl
  • test/test_task_id_linking.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • claude_code_log/renderer.py

📝 Walkthrough

Walkthrough

This PR wires task-id backlinking end-to-end: Bash parsers extract backgroundTaskId, models gain backlink fields, a renderer post-pass maps originating tool_use/TaskCreate messages to consumers, and HTML rendering wraps task ids with anchors using the stamped message indices.

Changes

Task ID Backlinking

Layer / File(s) Summary
Bash output parser enhancement
claude_code_log/factories/tool_factory.py
parse_bash_output accepts an optional tool_use_result, extracts toolUseResult.backgroundTaskId when present, and returns BashOutput.background_task_id. PARSERS_WITH_TOOL_USE_RESULT now includes "Bash".
Data model cross-linking fields
claude_code_log/models.py
Added creating_call_message_index: Optional[int] to TaskUpdateInput and TaskOutputInput, and background_task_id: Optional[str] to BashOutput.
Core linking engine in renderer
claude_code_log/renderer.py
Added a _link_task_id_consumers post-render pass (invoked from generate_template_messages) that builds per-session maps from originating tool-result cards (Bash background ids, async agent ids, TaskCreate.task_id) to call-card message indices and stamps creating_call_message_index onto TaskOutputInput and TaskUpdateInput consumers.
HTML rendering for backlinks
claude_code_log/html/renderer.py
HtmlRenderer._task_title accepts linked_creating_call_index and conditionally wraps #<id> in an <a class="task-id-backlink"> pointing to #msg-d-{index}; title_TaskUpdateInput and title_TaskOutputInput pass through creating_call_message_index.
Backlink CSS styling & snapshots
claude_code_log/html/templates/components/message_styles.css, test/__snapshots__/test_snapshot_html.ambr
Added .task-id-backlink styles (dotted underline, hover solid underline and --user-color) and updated snapshots to show task-id anchors in TaskOutput/TaskUpdate headers.
Test suite and fixtures
test/test_task_id_linking.py, test/test_data/task_id_linking.jsonl, test/test_data/task_id_linking_cross_session.jsonl
Unit tests confirm Bash parser extracts backgroundTaskId; end-to-end tests render fixtures and assert HTML backlinks for Bash background tasks, async-agent launches, and TaskCreate -> TaskUpdate links; cross-session fixture ensures backlinking is session-scoped; Markdown output assertions verify plain IDs without anchors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • daaain/claude-code-log#42: Touches Task/tool-result rendering and post-processing in renderer.py, related to message generation pipeline changes this PR extends.

Poem

🐰 I found a task id, bright and small,
I hopped its trail from card to call,
An anchor loop, a dotted bow,
Back to the spawn, the links now go —
Happy hops, the backlog stands tall.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding cross-linking from TaskOutput/TaskUpdate headers back to their spawn/creation points, which is the core feature implemented across multiple files.
Linked Issues check ✅ Passed The PR fully implements issue #154 requirements: cross-links from TaskOutput to background Bash tasks via backgroundTaskId, to async-agent tasks via agentId, and from TaskUpdate back to TaskCreate via task IDs.
Out of Scope Changes check ✅ Passed All changes directly support the stated objective of cross-linking task IDs. No unrelated refactoring, style changes, or auxiliary features were introduced outside the scope of issue #154.
Docstring Coverage ✅ Passed Docstring coverage is 88.46% which is sufficient. The required threshold is 80.00%.

✏️ 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/link-taskoutput-tasks

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

🤖 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/renderer.py`:
- Around line 2457-2458: The two global maps bg_task_id_to_call_index and
todo_task_id_to_call_index are keyed only by task ID which can collide across
sessions; change their keys to include session scope (for example use a
composite key like (session_id, task_id) or make the maps nested per session)
and update every access site that uses bg_task_id_to_call_index or
todo_task_id_to_call_index (including the lookup/manipulation sites referenced
near the current diff and the other occurrences) to construct/look up with the
session-aware key (or index into the per-session sub-dict) so task IDs from
different sessions cannot cross-link.

In `@test/test_task_id_linking.py`:
- Around line 208-215: The fixture _ensure_fixture_present currently calls
pytest.skip when FIXTURE is absent, which silently removes the backlink E2E
class from runs; change the behavior to fail fast by raising or calling
pytest.fail with a clear message (e.g., "Fixture missing: {FIXTURE}") instead of
pytest.skip so CI surfaces missing-fixture regressions; update the
_ensure_fixture_present fixture (and any uses via `@pytest.mark.usefixtures`) to
use pytest.fail when not FIXTURE.exists() and keep the same scope and docstring.
🪄 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: 0b21937b-ab4e-4b81-92a1-baee523cc6b8

📥 Commits

Reviewing files that changed from the base of the PR and between 20a61d5 and 79ffc8c.

📒 Files selected for processing (8)
  • claude_code_log/factories/tool_factory.py
  • claude_code_log/html/renderer.py
  • claude_code_log/html/templates/components/message_styles.css
  • claude_code_log/models.py
  • claude_code_log/renderer.py
  • test/__snapshots__/test_snapshot_html.ambr
  • test/test_data/task_id_linking.jsonl
  • test/test_task_id_linking.py

Comment thread claude_code_log/renderer.py Outdated
Comment thread test/test_task_id_linking.py Outdated
CodeRabbit findings on #158:

1. Cross-session collision on todo `#N` ids. Two sessions in a
   combined-transcripts render each minting `TaskCreate #1` would
   collapse onto a single map entry under id-only keys — the
   second `TaskUpdate #1` would backlink to the FIRST session's
   `TaskCreate` card. Fix: key the maps on `(session_id, task_id)`
   tuples on both index and lookup. Background ids are random
   alphanumeric so collision is practically nil; they ride the
   same shape for symmetry.

2. Silent skip on missing fixture would mask a fixture-deletion
   regression in CI. Switch the `_ensure_fixture_present` helper
   to `pytest.fail` so the loud failure surfaces in the run.

Adds a cross-session regression test (`task_id_linking_cross_session`)
that builds two sessions both minting `#1` and asserts each
`TaskUpdate` backlinks to its OWN session's `TaskCreate` — the
test would fail loudly under the id-only-keyed regression.
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.

Link TaskOutput to their tasks (background or otherwise)

1 participant