Detail levels and compact rendering of conversations#96
Conversation
📝 WalkthroughWalkthroughAdds a DetailLevel enum and compact mode, exposes Changes
Sequence DiagramsequenceDiagram
participant User as User/CLI
participant CLI as CLI Module
participant Converter as Converter
participant RendererFactory as Renderer Factory
participant FilterPre as Pre-Render Filter
participant TemplateGen as Template Generator
participant FilterPost as Post-Render Filter
participant Output as Output (HTML/MD)
User->>CLI: invoke with --detail=low --compact
CLI->>Converter: convert_jsonl_to(detail=LOW, compact=True)
Converter->>RendererFactory: get_renderer(detail=LOW, compact=True)
RendererFactory->>RendererFactory: set renderer.detail & renderer.compact
Converter->>FilterPre: _filter_by_detail(messages, LOW)
FilterPre-->>Converter: filtered messages
Converter->>TemplateGen: generate_template_messages(filtered, detail=LOW)
TemplateGen->>FilterPost: _filter_template_by_detail(templates, LOW)
FilterPost-->>TemplateGen: filtered templates
TemplateGen-->>RendererFactory: template messages
RendererFactory->>Output: render templates (apply compact behavior if MD)
Output-->>User: final rendered files
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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: 2
🧹 Nitpick comments (2)
test/test_shallow_mode.py (1)
561-639: Add a cache mode-toggle regression test (full → shallowandshallow → full).Current tests mostly generate from fresh outputs, so they won’t catch stale reuse when the same cached target is rendered with different modes.
Also applies to: 751-774
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/test_shallow_mode.py` around lines 561 - 639, Add a regression test that exercises cache-mode toggling by rendering the same output target twice with different modes: first generate a cached HTML/MD with --shallow and assert tool messages are omitted, then re-run the CLI for the same output path without --shallow (full) and assert tool messages are present (and vice versa: start with full then run --shallow) to ensure cached artifacts are not incorrectly reused across modes; implement this using the existing test helpers and CliRunner.invoke(main, ...) and reuse the same output_file path to force cache reuse, checking presence/absence of "class='message tool_use" / "class='message tool_result" (or tool name like "Bash") and exit_code == 0 for each invocation.claude_code_log/renderer.py (1)
527-544: Missing docstring forshallowparameter.The
shallowparameter was added to the function signature but not documented in the Args section of the docstring.📝 Add documentation for the shallow parameter
"""Generate root messages and session navigation from transcript messages. This is the format-neutral rendering step that produces data structures ready for template rendering by any format-specific renderer. Args: messages: List of transcript entries to process. + shallow: If True, filter to only user and assistant text messages, + excluding tools, thinking, system messages, and sidechains. Returns:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/renderer.py` around lines 527 - 544, The docstring for generate_template_messages is missing documentation for the shallow parameter; update the Args section to include a one-line description of shallow (its type bool, default False) and what it controls (e.g., whether to perform a shallow rendering that skips deep parsing/child population or only generates top-level session headers), so readers understand its effect when calling generate_template_messages(messages, shallow=True/False).
🤖 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/converter.py`:
- Around line 701-702: Staleness checks and early exits are ignoring the new
shallow render mode, so add the shallow flag into the cache/staleness identity
used wherever staleness is computed for combined, paginated, and session
outputs: include shallow in the metadata written to and read from cache
files/marker files and incorporate it into whatever key/hash/compute_staleness
function is used by the early-exit logic (the code paths that currently accept
shallow: bool and return Path). Update the marker read/write and the
is_stale/check_staleness logic to compare the shallow value as well as
timestamps/inputs so switching shallow vs full on the same output path won't
return the wrong artifact.
In `@test/test_shallow_mode.py`:
- Around line 126-130: Rename the outdated references to _filter_compact to the
current function name _filter_shallow in the test header and docstring: update
the section comment and the TestFilterShallow class docstring so they mention
_filter_shallow (and any other lingering _filter_compact mentions) to accurately
reflect the tested symbol and intent.
---
Nitpick comments:
In `@claude_code_log/renderer.py`:
- Around line 527-544: The docstring for generate_template_messages is missing
documentation for the shallow parameter; update the Args section to include a
one-line description of shallow (its type bool, default False) and what it
controls (e.g., whether to perform a shallow rendering that skips deep
parsing/child population or only generates top-level session headers), so
readers understand its effect when calling generate_template_messages(messages,
shallow=True/False).
In `@test/test_shallow_mode.py`:
- Around line 561-639: Add a regression test that exercises cache-mode toggling
by rendering the same output target twice with different modes: first generate a
cached HTML/MD with --shallow and assert tool messages are omitted, then re-run
the CLI for the same output path without --shallow (full) and assert tool
messages are present (and vice versa: start with full then run --shallow) to
ensure cached artifacts are not incorrectly reused across modes; implement this
using the existing test helpers and CliRunner.invoke(main, ...) and reuse the
same output_file path to force cache reuse, checking presence/absence of
"class='message tool_use" / "class='message tool_result" (or tool name like
"Bash") and exit_code == 0 for each invocation.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
claude_code_log/cli.pyclaude_code_log/converter.pyclaude_code_log/html/renderer.pyclaude_code_log/markdown/renderer.pyclaude_code_log/renderer.pytest/test_shallow_mode.py
| shallow: bool = False, | ||
| ) -> Path: |
There was a problem hiding this comment.
Cache invalidation does not account for render mode (shallow vs full).
shallow is now threaded through generation, but staleness checks/early exits are still mode-agnostic. That can serve the wrong artifact when users switch modes on the same output path.
🛠️ Minimum mitigation direction
@@ def convert_jsonl_to(...):
- if (
+ if (
cache_manager is not None
and not cache_was_updated
and from_date is None
and to_date is None
+ and not shallow
):
...
@@
- should_regenerate = (
+ should_regenerate = shallow or (
is_stale
or renderer.is_outdated(output_path)
or from_date is not None
or to_date is not None
or not output_path.exists()
)
@@
- if format == "html" and cache_manager is not None:
+ if format == "html" and cache_manager is not None and not shallow:
cache_manager.update_html_cache(...)A complete fix should include render mode in staleness identity (cache metadata and/or file marker checks) for combined, paginated, and session outputs.
Also applies to: 921-922, 1026-1027, 1073-1074, 1126-1127, 1489-1490, 1525-1526, 1673-1674, 1830-1831
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@claude_code_log/converter.py` around lines 701 - 702, Staleness checks and
early exits are ignoring the new shallow render mode, so add the shallow flag
into the cache/staleness identity used wherever staleness is computed for
combined, paginated, and session outputs: include shallow in the metadata
written to and read from cache files/marker files and incorporate it into
whatever key/hash/compute_staleness function is used by the early-exit logic
(the code paths that currently accept shallow: bool and return Path). Update the
marker read/write and the is_stale/check_staleness logic to compare the shallow
value as well as timestamps/inputs so switching shallow vs full on the same
output path won't return the wrong artifact.
|
After a little brainstorming session with ChatGPT: ✅ Final polished version Does that sound right? |
Yes, I think these sound great! Please put those comments as details in the CLI help too! |
57abed6 to
0418998
Compare
Filters out tool use/result, thinking, and system messages early in the rendering pipeline via _filter_compact() in renderer.py. Threaded through CLI, converter, and both HTML/Markdown renderers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
27 tests covering unit (_filter_compact), integration (generate_template_messages), HTML/Markdown rendering, CLI flag, real project data, and bundled test data files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-derived types Use isinstance checks on content classes instead of message_type strings, since SlashCommandMessage, CommandOutputMessage, and CompactedSummaryMessage all return "user" as their message_type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add explicit ContentItem annotation and cast for narrowing the UserTranscriptEntry/AssistantTranscriptEntry union on .message assignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add isinstance narrowing for TranscriptEntry union types before accessing .message and .isSidechain attributes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename CLI flag, parameters, functions, constants, test file, and all references from "compact" to "shallow" to avoid confusion with the existing CompactedSummary message type and future compact styling mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues from rebasing dev/shallow onto main: 1. generate_single_session_export() called load_directory_transcripts() which now returns (messages, session_tree), but only assigned to messages — session ID lookup iterated a tuple instead of entries. 2. Shallow mode skipped regeneration when pre-existing HTML matched the current version. Added shallow flag to should_regenerate conditions so --shallow always produces fresh output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement four detail levels: full, high, low, minimal. - full: everything (default, no change from current behavior) - high: detailed but cleaned (no system/hook/slash-command noise) - low: interaction-focused + key signals (keeps WebSearch, WebFetch, Task tool_use/result; drops other tools, bash, thinking, sidechains) - minimal: user + assistant text only (equivalent to old --shallow) Infrastructure: - Add DetailLevel enum to models.py - Replace shallow: bool with detail: DetailLevel throughout pipeline (cli.py, converter.py, renderer.py, html/renderer.py, markdown/renderer.py) - Level-specific filtering via _filter_by_detail (pre-render) and _filter_template_by_detail (post-render) with per-level exclude sets - CLI: --detail full|high|low|minimal with help text per daaain's request Rename test/test_shallow_mode.py -> test/test_detail_levels.py. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per monk review feedback: existing tests only covered MINIMAL level. HIGH level tests (4): - Keeps tool_use and tool_result messages - Keeps ThinkingMessage (not filtered at HIGH) - Drops SystemMessage - Drops slash command messages LOW level tests (5): - Keeps WebSearch tool_use/result (key signal) - Drops Read tool_use/result (not a key signal) - Drops ThinkingMessage - Keeps user and assistant text - Drops system messages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When --compact is set, consecutive messages of the same type share a single heading instead of repeating it. Most useful with --detail low/minimal where tool stripping creates runs of Assistant sections. Implementation: MarkdownRenderer._render_message tracks _last_message_type and suppresses the heading when the current message matches. Resets on session header boundaries. Markdown-only — HTML ignores the flag (no heading-per-message there). 6 tests covering: consecutive merge, type boundary, session reset, off-by-default, no effect on HTML. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per monk review: without this, cached non-compact output would be served when --compact is requested. Same pattern as detail level. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two related fixes for DAG-rendered transcripts with many branches: 1. Branch nav items whose first message was filtered out (e.g. a passthrough attachment entry) rendered as href='#msg-d-None' links pointing nowhere. Drop such branches, and drop the surrounding fork point if all its branches are dead. 2. Clicking a #msg-d-X anchor from the session index landed on an element that was display:none because its ancestor fold-bars were initially folded. Add a hashchange listener (also runs on initial load with a hash) that walks the target's d-X ancestor classes, unfolds each ancestor's fold-all-levels control, then re-scrolls the now-visible target into view. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related corrections to DAG fork detection: 1. Variant 1 of `_stitch_tool_results` used to treat any user child with a same-session descendant as "live", which missed the common parallel-tool_use pattern where the tool_result has only a hook_success attachment leaf. Replace the "no immediate children" check with `_is_structural_subtree()`, which walks descendants looking for user/assistant content and returns True for subtrees that contain only passthrough/system entries. 2. A parent whose multiple children are all passthrough (e.g. hook_success + SessionStart:resume at far-apart timestamps) used to produce a fake fork with two dead branches. Handle this directly in `_walk_session_with_forks` by collapsing all children into the chain and terminating — no fork, no branches. Variant 2 of `_stitch_tool_results` is unchanged. Existing tests for both variants still pass; three new tests cover the parallel-tool_use+attachment, attachment-only, and real-rewind cases. On the 1594-entry BCT Teamcenter test file this eliminates 22 fake fork points and 42 dead branch links with no content loss. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Within-session branch-headers used to restart the fold hierarchy at level 0, making them siblings of their parent session. Clicking the parent session's 'fold all levels' button therefore did not cascade into branch content: descendants of branches had no 'd-<parent>' ancestor class and were skipped by the `.message.d-X` selector. Assign branch-headers a fractional level of 0.5 so they nest under their parent session while staying above user messages. Descendants now carry both the parent session's ancestor class and the branch- header's, so the existing anchor-unfold JS and the fold-bar cascade work uniformly across branches and non-branch content. Only within-session forks (`is_branch_header`) move — resumed-session headers stay at level 0. Three companion changes: - Template: emit ancestor `d-X` classes on session-header <div> too, so branch-headers carry their parent session's ancestor class. - Test: walk the full flat/tree of template messages instead of expecting branch-headers to live at the top of `root_messages`. - dag.py: tighten the all-passthrough fork early-exit to also check `_is_structural_subtree` per monk's review — defense-in-depth for any future passthrough type that might carry conversational descendants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`/compact` writes a parentless `system/compact_boundary` entry followed by a user entry carrying the summary — the existing `CompactedSummaryMessage` content type. The transition is a real content discontinuity (115k+ tokens of context replaced by a summary), so it deserves a navigational landmark rather than being silently merged into the main trunk during DAG walking. Three related changes: 1. `build_session_nav` emits a `is_compaction_point` nav item for each `CompactedSummaryMessage` in a session, indented under its parent session (depth + 1). The anchor points at the summary's own `#msg-d-X` so clicking jumps to the rendered content. Ordering is chronological by message timestamp. 2. `session_nav.html` + `session_nav_styles.css` render the new item with a 📦 glyph and a solid (vs dashed for forks/branches) border on the nav card. 3. `extract_session_dag_lines` now classifies the roots of multi-root sessions. `system/compact_boundary` and `system/local_command` roots are the two known expected cases — when all roots are expected, demote the warning to debug. When anything else shows up (an orphan user/assistant hinting at a missing parent), keep the warning at its original level. The underlying multi-root walking behavior is unchanged. Tests: - `TestRootClassification`: two compact_boundary + one local_command root produces no warning; an orphan user entry does. - `TestCompactBoundaryNav`: single/multiple/no-boundary cases for `is_compaction_point` nav emission. On the 1594-entry BCT Teamcenter file: previously noisy "3 roots found" warning is gone, and the two manual `/compact` points are surfaced in the session index as navigable landmarks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Align dag.md with the current dag.py/renderer.py behavior after the
alice-{fix-nav-anchors,real-fork-fix,branch-hierarchy,compact-boundaries}
series.
Changes:
- Phase 3 ("Extract Session DAG-lines") now reflects multi-root handling,
`_walk_session_with_forks`, trunk merging, and the coverage-fallback.
The "verify linearity" claim is gone — within-session forks are
supported intentionally.
- "Tool-Result Side-Branches" section now uses Mermaid diagrams for
Variant 1, Variant 2, and the structural-only fork case, each with a
detection/action summary. Variant 1 diagrams show the pre-fix fake
fork and the post-fix stitched chain side-by-side. Added a summary
table covering all five multi-child dispositions (variant 1/2,
structural-only, real rewind, compaction replay).
- New section "Compact Boundaries and Multi-Root Sessions": diagrams a
3-root session produced by two `/compact` runs; documents the
`_EXPECTED_ROOT_SYSTEM_SUBTYPES` classifier (`compact_boundary`,
`local_command`) that demotes the multi-root warning to debug; notes
the `is_compaction_point` nav landmarks built from
`CompactedSummaryMessage` entries.
- Assertions/Invariants rewritten to state "trunk is linear after
stitching" and enumerate the three legitimate sources of intra-session
branching. Added multi-root tolerance and DAG walk coverage as
separate invariants.
No code changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The alice-real-fork-fix branch handled two structural-collapse cases: the passthrough-with-attachment tool_result variant (via updated `_is_structural_subtree` check in `_stitch_tool_results`), and the all-passthrough early-exit (e.g. hook_success + SessionStart:resume attachments on the same parent). It missed the general mixed case: a conversational child alongside a passthrough-only sibling (e.g. a `<user>` next to a bare `<progress>` entry, or `<progress>`→`<progress>` chain). `_stitch_tool_results` requires both a user and an assistant child, so this fell through to the different-timestamps real-fork path, producing spurious "Fork point (1 branches)" entries in the nav. Replace the all-passthrough early-exit with a slightly more general test: if at most one non-structural child remains after isolating the PassthroughTranscriptEntry subtrees whose descendants are `_is_structural_subtree`, the chain continues through that single child (or terminates if none remain) and the structural children are stitched chronologically as dead-end side entries. Three new tests cover: - `<user>` + `<progress>` leaf sibling → linear chain, 0 branches - `<user>` + `<progress>`→`<progress>` chain → linear, skipped subtree - two conversational siblings → real fork still preserved On the -experiments-ideas fixture the two previously-spurious 1-branch forks are gone; the single real 2-branch fork is kept. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The compact_boundary system entry carries `compactMetadata` (preTokens, trigger, postTokens, durationMs) that the nav landmark label previously ignored, producing bare "Conversation compacted" entries with no signal about how much context was actually dropped. Surface preTokens and trigger through the factory: - `SystemTranscriptEntry` gains `compactMetadata: Optional[dict]` so the raw JSONL field lands on the parsed entry. - `SystemMessage` gains two optional typed fields — `compact_pre_tokens` and `compact_trigger` — populated only for subtype="compact_boundary" by `create_system_message`. Other system entries leave them None. - `_compact_nav_label` looks up the landmark's parent TemplateMessage by parent_uuid and reads these fields off the SystemMessage, producing labels like "Conversation compacted (115k tokens) • 2026-04-14 09:09:28". Falls back to the bare label when metadata is missing (e.g. older transcripts). Tested against the BCT Teamcenter session: the two existing landmarks now show "(115k tokens)" and "(126k tokens)" alongside their timestamps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to monk-dag-docs (867d81f), picking up the two alice branches
that have since landed on dev/shallow:
- alice-fix-structural-collapse (2676a56) generalized the passthrough
early-exit from the all-children-passthrough case to any parent with
at most one non-structural child. Rename the subsection to
"Structural-side-branch collapse", describe the
structural_kids / non_structural partition, and add a second Mermaid
diagram for the mixed user + `<progress>` sibling shape. Note
explicitly that Variant 1 and the collapse path don't overlap
(UserTranscriptEntry with a passthrough-only subtree stays in
non_structural and goes through `_stitch_tool_results`). Summary
table row updated accordingly.
- alice-compact-landmark-enrichment (86264d4) surfaced
`compactMetadata.preTokens` and `trigger` through
SystemTranscriptEntry → SystemMessage. Extend the "Nav landmarks"
subsection with an example label ("Conversation compacted (115k
tokens) • 2026-04-14 09:09:28"), document the plumbing and the
graceful-degradation fallbacks (missing parent_uuid, filtered parent
at HIGH detail, non-SystemMessage parent, None/zero tokens), and note
that `compact_trigger` is plumbed but not rendered yet.
No code changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
claude_code_log/converter.py (1)
1129-1145:⚠️ Potential issue | 🟡 MinorDocstring missing
compactparameter.The
convert_jsonl_todocstring documentsdetailbut not the newcompactparameter added alongside it. Same applies to_generate_individual_session_filesandprocess_projects_hierarchydocstrings.📝 Proposed addition
detail: Output detail level (full, high, low, minimal). + compact: If True, merge consecutive same-type headings (Markdown only). """🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/converter.py` around lines 1129 - 1145, Update the docstrings to document the new boolean compact parameter: add a line describing "compact: Whether to produce compact output (e.g., omit verbose metadata) and how it affects output" inside the convert_jsonl_to(...) docstring and the _generate_individual_session_files(...) and process_projects_hierarchy(...) docstrings so each lists the parameter alongside existing ones like detail, include its type (bool) and default behavior/impact on output generation; ensure the parameter name exactly matches "compact" to align with the function signatures (_generate_individual_session_files, process_projects_hierarchy, and convert_jsonl_to) and keep wording consistent across all three docstrings.
♻️ Duplicate comments (1)
claude_code_log/converter.py (1)
1295-1297:⚠️ Potential issue | 🟠 MajorDetail/compact not incorporated into paginated or per-session staleness checks.
detail/compactare threaded through, and the single-file branch forces regenerate when they are non-default (Lines 1309-1310, 1320-1321). But the paginated path (_generate_paginated_htmlusescache_manager.is_page_stale(...)at Line 972) and_generate_individual_session_filesstaleness checks (Lines 1781-1787, 1790-1796) don't know about these modes. Two resulting bugs:
- Running with
--detail low --compactthen again with defaults (or vice versa) on the same output path returns the cached prior-mode artifact in both the paginated and session-file cases.- Even in the non-paginated branch,
detail != FULL or compactonly forces regeneration going into those modes — cached low/compact outputs are treated as up-to-date for subsequent--detail fullruns.A complete fix needs to include
detailandcompactin the cache identity (marker/metadata) so page and session staleness flip when modes change. At minimum, mirror the force-regen guard into the paginated and session-file branches.Also applies to: 1345-1358
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/converter.py` around lines 1295 - 1297, The paginated and per-session staleness logic doesn't consider the detail and compact modes, so update the cache identity used by cache_manager.is_page_stale and the staleness checks in _generate_paginated_html and _generate_individual_session_files to include the detail and compact values (or add them to the metadata/marker passed into the cache manager), or at minimum mirror the existing force-regen guard (the detail != FULL || compact condition used in the single-file branch) into those branches so changes to detail/compact flip page/session staleness; refer to the functions _generate_paginated_html, _generate_individual_session_files and the cache_manager.is_page_stale calls and ensure the mode tuple (detail, compact) is included in the cache key/metadata used for staleness decisions.
🧹 Nitpick comments (4)
claude_code_log/markdown/renderer.py (1)
841-856: Compact mode: heading-suppression logic is sound; note one subtle state scope.The ordering is correct: reset on session headers → check suppression against previous type → update
self._last_message_type. Because the update lives insideif title:, messages that produce an empty title (other thanpair_last, which already returns early at line 826) won't update the tracker and thus don't "break" a run of same-type headings. That should be fine in practice given current title methods, but it's a behavior worth keeping in mind if future title methods start returning empty strings for non-paired messages.Also, tracking is global across the recursive tree walk rather than per-parent/per-sibling. For the current message shape (tool results as children, not repeated top-level message types) this is a non-issue, but a future nested same-type scenario could see a deeper heading accidentally suppressed.
🤖 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 841 - 856, The suppression logic can miss updating the tracker when a message yields an empty title (because _last_message_type is currently set only after building the heading), so always assign self._last_message_type = msg_type unconditionally (i.e., move the update out of the title/heading conditional) so runs of same-type messages are tracked even when a specific message produces no title; keep the existing session-reset (is_session_header) and suppress_heading calculation using compact and msg_type intact, and add a brief comment near the _last_message_type update to note global-tracking behavior across the recursive walk.test/test_detail_levels.py (2)
656-684: Fragile substring assertion for tool-name absence.
"Write" not in md.split("File created!")[0].split("Creating the file.")[1]depends on exact formatting and substring layout in the Markdown output; any future heading/prefix change (e.g., a rendered "Write" anywhere in the slice) silently breaks or masks regressions. Prefer the_collect_typespattern used elsewhere, or check for concrete tool markers.♻️ Proposed alternative
- assert ( - "Write" not in md.split("File created!")[0].split("Creating the file.")[1] - ) + # No tool_use block markers should be rendered in minimal Markdown. + _, _, ctx = generate_template_messages(messages, detail=DetailLevel.MINIMAL) + types = {m.content.message_type for m in ctx.messages} + assert "tool_use" not in types + assert "tool_result" not in types🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/test_detail_levels.py` around lines 656 - 684, The assertion in test_minimal_markdown_no_tool_content is fragile because it slices md by literal strings; update the test to assert absence of tool-specific content using the same robust helper/pattern used elsewhere: call the _collect_types(md) utility (or parse md for concrete tool markers generated from _tool_use_item/_tool_result_item) and assert that tool names like "Write" or tool markers (e.g., entries created by _tool_use_item/_tool_result_item) are not present in the collected types/output; replace the substring-slice assertion with checks against _collect_types(md) or explicit absence of tool markers on the rendered Markdown.
721-740: Non-deterministic fixture selection.
jsonl_files[0]depends on filesystem iteration order sincereal_projects.iterdir()andproject_dir.glob("*.jsonl")are unordered. On different filesystems/CI runners the chosen file can differ, producing flaky behaviour on edge-case transcripts. Sort the collection before picking an element.♻️ Proposed fix
- jsonl_files = [] - for project_dir in real_projects.iterdir(): - if project_dir.is_dir(): - jsonl_files.extend(project_dir.glob("*.jsonl")) + jsonl_files = [] + for project_dir in sorted(real_projects.iterdir()): + if project_dir.is_dir(): + jsonl_files.extend(sorted(project_dir.glob("*.jsonl")))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/test_detail_levels.py` around lines 721 - 740, The test test_minimal_markdown_on_real_projects is picking jsonl_files[0] from an unsorted jsonl_files list built via real_projects.iterdir() and project_dir.glob("*.jsonl"), which yields non-deterministic order; fix by sorting the collected jsonl_files (e.g., sort by path name) before selecting the first element so the test always uses a deterministic file—update the code that builds jsonl_files (the loop using real_projects.iterdir() / project_dir.glob) to append then call jsonl_files.sort() (or use a sorted() wrapper) prior to load_transcript(...) and generating with MarkdownRenderer/DetailLevel.MINIMAL.claude_code_log/converter.py (1)
920-921: Bypass ofget_renderer(...)creates a second renderer construction path.
_generate_paginated_htmlimportsHtmlRendererdirectly and assignsdetail/compactas attributes post-init, while the rest of the file consistently usesget_renderer(format, image_export_mode, detail=detail, compact=compact)(Lines 1243, 1750). Ifget_rendererever gains additional wiring (e.g., defaultimage_export_mode, validation, instrumentation), this path will silently diverge.♻️ Proposed alignment
- from .html.renderer import HtmlRenderer from .utils import format_timestamp @@ - page_renderer = HtmlRenderer() - page_renderer.detail = detail - page_renderer.compact = compact + page_renderer = get_renderer( + "html", image_export_mode=None, detail=detail, compact=compact + )(Adjust signature if
image_export_modeshould be threaded through from the caller.)Also applies to: 1059-1062
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/converter.py` around lines 920 - 921, The helper _generate_paginated_html currently imports and constructs HtmlRenderer directly (assigning detail/compact attributes afterwards), creating a second construction path that diverges from the rest of the module which uses get_renderer(format, image_export_mode, detail=detail, compact=compact); change _generate_paginated_html to call get_renderer(...) instead of importing HtmlRenderer directly, passing through the caller's image_export_mode (or add an image_export_mode parameter to _generate_paginated_html if needed) so all renderer wiring/validation/instrumentation stays centralized in get_renderer and remove the direct HtmlRenderer usage.
🤖 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/dag.py`:
- Around line 539-563: The current logic treats any non-expected root as
"unexpected" and logs a warning even when there's a single ordinary primary root
plus expected compact_boundary/local_command roots; update the check around
roots/unexpected in the block that uses _is_expected_root_type so that one
non-expected root is allowed as the session primary and only additional
non-expected roots trigger the warning—e.g., compute unexpected as you do, then
treat the first non-expected as the primary (if any) and only call
logger.warning when len(unexpected) > 1 (or when len(unexpected[1:]) > 0),
adjusting the logged counts/messages to reflect how many unexpected extras were
found and keeping the existing debug path when there are no extra non-expected
roots.
In `@claude_code_log/html/templates/transcript.html`:
- Around line 862-867: The code only tries to un-fold ancestor messages by
clicking the '.fold-all-levels' button, so single-level fold bars
('.fold-one-level.full-width') are left folded; update the ancestor handling in
the block that queries foldBar/ foldAll (using the ancestorMsg variable) to also
look for a fallback button: query
foldBar.querySelector('.fold-one-level.full-width') and if it exists and has the
'folded' class, call click() on it (fall back to this when foldAll is null) so
single-level folded ancestors are expanded too.
In `@dev-docs/dag.md`:
- Around line 460-462: The fenced code block containing the label "📦
Conversation compacted (115k tokens) • 2026-04-14 09:09:28" is missing a
language identifier; update that markdown block to include the `text` language
specifier (i.e., change the opening backticks to ```text) so markdownlint MD040
is satisfied and the content is treated as plain text.
In `@test/test_dag_integration.py`:
- Around line 1378-1398: The compact-boundary fixture returned by
_make_compact_boundary is missing the system message "level" field required by
SystemMessage construction, which breaks uuid_to_msg and the "100k tokens" label
path; update the dict returned by _make_compact_boundary to include a suitable
"level" value (e.g., "system" or the expected enum/string used elsewhere)
alongside the existing keys like "type", "subtype", "uuid", and
"compactMetadata" so the fixture produces a valid SystemMessage.
In `@test/test_detail_levels.py`:
- Around line 130-134: The test file's section header and class docstring
incorrectly reference _filter_compact while the tests actually import and
exercise _filter_by_detail; update the section comment text and the
TestFilterMinimal class docstring to reference _filter_by_detail (or otherwise
clarify that the tests target _filter_by_detail) so the documented intent
matches the imported symbol _filter_by_detail used in the tests.
---
Outside diff comments:
In `@claude_code_log/converter.py`:
- Around line 1129-1145: Update the docstrings to document the new boolean
compact parameter: add a line describing "compact: Whether to produce compact
output (e.g., omit verbose metadata) and how it affects output" inside the
convert_jsonl_to(...) docstring and the _generate_individual_session_files(...)
and process_projects_hierarchy(...) docstrings so each lists the parameter
alongside existing ones like detail, include its type (bool) and default
behavior/impact on output generation; ensure the parameter name exactly matches
"compact" to align with the function signatures
(_generate_individual_session_files, process_projects_hierarchy, and
convert_jsonl_to) and keep wording consistent across all three docstrings.
---
Duplicate comments:
In `@claude_code_log/converter.py`:
- Around line 1295-1297: The paginated and per-session staleness logic doesn't
consider the detail and compact modes, so update the cache identity used by
cache_manager.is_page_stale and the staleness checks in _generate_paginated_html
and _generate_individual_session_files to include the detail and compact values
(or add them to the metadata/marker passed into the cache manager), or at
minimum mirror the existing force-regen guard (the detail != FULL || compact
condition used in the single-file branch) into those branches so changes to
detail/compact flip page/session staleness; refer to the functions
_generate_paginated_html, _generate_individual_session_files and the
cache_manager.is_page_stale calls and ensure the mode tuple (detail, compact) is
included in the cache key/metadata used for staleness decisions.
---
Nitpick comments:
In `@claude_code_log/converter.py`:
- Around line 920-921: The helper _generate_paginated_html currently imports and
constructs HtmlRenderer directly (assigning detail/compact attributes
afterwards), creating a second construction path that diverges from the rest of
the module which uses get_renderer(format, image_export_mode, detail=detail,
compact=compact); change _generate_paginated_html to call get_renderer(...)
instead of importing HtmlRenderer directly, passing through the caller's
image_export_mode (or add an image_export_mode parameter to
_generate_paginated_html if needed) so all renderer
wiring/validation/instrumentation stays centralized in get_renderer and remove
the direct HtmlRenderer usage.
In `@claude_code_log/markdown/renderer.py`:
- Around line 841-856: The suppression logic can miss updating the tracker when
a message yields an empty title (because _last_message_type is currently set
only after building the heading), so always assign self._last_message_type =
msg_type unconditionally (i.e., move the update out of the title/heading
conditional) so runs of same-type messages are tracked even when a specific
message produces no title; keep the existing session-reset (is_session_header)
and suppress_heading calculation using compact and msg_type intact, and add a
brief comment near the _last_message_type update to note global-tracking
behavior across the recursive walk.
In `@test/test_detail_levels.py`:
- Around line 656-684: The assertion in test_minimal_markdown_no_tool_content is
fragile because it slices md by literal strings; update the test to assert
absence of tool-specific content using the same robust helper/pattern used
elsewhere: call the _collect_types(md) utility (or parse md for concrete tool
markers generated from _tool_use_item/_tool_result_item) and assert that tool
names like "Write" or tool markers (e.g., entries created by
_tool_use_item/_tool_result_item) are not present in the collected types/output;
replace the substring-slice assertion with checks against _collect_types(md) or
explicit absence of tool markers on the rendered Markdown.
- Around line 721-740: The test test_minimal_markdown_on_real_projects is
picking jsonl_files[0] from an unsorted jsonl_files list built via
real_projects.iterdir() and project_dir.glob("*.jsonl"), which yields
non-deterministic order; fix by sorting the collected jsonl_files (e.g., sort by
path name) before selecting the first element so the test always uses a
deterministic file—update the code that builds jsonl_files (the loop using
real_projects.iterdir() / project_dir.glob) to append then call
jsonl_files.sort() (or use a sorted() wrapper) prior to load_transcript(...) and
generating with MarkdownRenderer/DetailLevel.MINIMAL.
🪄 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: f4f349a2-b5f5-4478-b1c6-a1ce8a44a64b
📒 Files selected for processing (16)
claude_code_log/cli.pyclaude_code_log/converter.pyclaude_code_log/dag.pyclaude_code_log/factories/system_factory.pyclaude_code_log/html/renderer.pyclaude_code_log/html/templates/components/session_nav.htmlclaude_code_log/html/templates/components/session_nav_styles.cssclaude_code_log/html/templates/transcript.htmlclaude_code_log/markdown/renderer.pyclaude_code_log/models.pyclaude_code_log/renderer.pydev-docs/dag.mdtest/__snapshots__/test_snapshot_html.ambrtest/test_dag.pytest/test_dag_integration.pytest/test_detail_levels.py
🚧 Files skipped from review as they are similar to previous changes (1)
- claude_code_log/renderer.py
| if len(roots) > 1: | ||
| logger.warning( | ||
| "Session %s: %d roots found, walking all from earliest (%s)", | ||
| session_id, | ||
| len(roots), | ||
| roots[0].uuid, | ||
| ) | ||
| # Roots that arise from expected Claude Code mechanisms — | ||
| # `/compact` writes a compact_boundary system entry with no | ||
| # parentUuid; early `local_command` entries like `/memory` | ||
| # sometimes land as orphans too. Warn only when an unexpected | ||
| # root shows up (e.g. an orphan user/assistant that hints at a | ||
| # missing parent); otherwise log at debug level. | ||
| unexpected = [n for n in roots if not _is_expected_root_type(n.entry)] | ||
| if unexpected: | ||
| logger.warning( | ||
| "Session %s: %d roots found (%d unexpected), " | ||
| "walking all from earliest (%s)", | ||
| session_id, | ||
| len(roots), | ||
| len(unexpected), | ||
| roots[0].uuid, | ||
| ) | ||
| else: | ||
| logger.debug( | ||
| "Session %s: %d expected roots (compact_boundary / " | ||
| "local_command), walking all from earliest (%s)", | ||
| session_id, | ||
| len(roots), | ||
| roots[0].uuid, | ||
| ) |
There was a problem hiding this comment.
Allow one ordinary primary root before warning on extra roots.
With a normal initial user root plus later compact_boundary roots, unexpected still includes that primary user root and emits the warning this change is trying to suppress. Treat one non-expected root as the session’s primary root, then warn only on additional non-expected roots.
🐛 Proposed root classification fix
- unexpected = [n for n in roots if not _is_expected_root_type(n.entry)]
+ ordinary_roots = [n for n in roots if not _is_expected_root_type(n.entry)]
+ unexpected = ordinary_roots[1:]
if unexpected:
logger.warning(
"Session %s: %d roots found (%d unexpected), "
@@
else:
logger.debug(
- "Session %s: %d expected roots (compact_boundary / "
- "local_command), walking all from earliest (%s)",
+ "Session %s: %d roots found with only expected extra "
+ "roots, walking all from earliest (%s)",
session_id,
len(roots),
roots[0].uuid,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@claude_code_log/dag.py` around lines 539 - 563, The current logic treats any
non-expected root as "unexpected" and logs a warning even when there's a single
ordinary primary root plus expected compact_boundary/local_command roots; update
the check around roots/unexpected in the block that uses _is_expected_root_type
so that one non-expected root is allowed as the session primary and only
additional non-expected roots trigger the warning—e.g., compute unexpected as
you do, then treat the first non-expected as the primary (if any) and only call
logger.warning when len(unexpected) > 1 (or when len(unexpected[1:]) > 0),
adjusting the logged counts/messages to reflect how many unexpected extras were
found and keeping the existing debug path when there are no extra non-expected
roots.
| ``` | ||
| 📦 Conversation compacted (115k tokens) • 2026-04-14 09:09:28 | ||
| ``` |
There was a problem hiding this comment.
Add a language identifier to the fenced code block (MD040).
markdownlint flagged this block as missing a language specifier. Since the content is a formatted label example (not code), text is appropriate.
📝 Proposed fix
-```
+```text
📦 Conversation compacted (115k tokens) • 2026-04-14 09:09:28</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **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.
```suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)
[warning] 460-460: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@dev-docs/dag.md` around lines 460 - 462, The fenced code block containing the
label "📦 Conversation compacted (115k tokens) • 2026-04-14 09:09:28" is missing
a language identifier; update that markdown block to include the `text` language
specifier (i.e., change the opening backticks to ```text) so markdownlint MD040
is satisfied and the content is treated as plain text.
| def _make_compact_boundary( | ||
| uuid: str, | ||
| session_id: str, | ||
| timestamp: str, | ||
| pre_tokens: int = 100_000, | ||
| ) -> dict[str, Any]: | ||
| """Synthesize a system/compact_boundary entry (always parent-null).""" | ||
| return { | ||
| "type": "system", | ||
| "subtype": "compact_boundary", | ||
| "timestamp": timestamp, | ||
| "parentUuid": None, | ||
| "isSidechain": False, | ||
| "userType": "human", | ||
| "cwd": "/tmp", | ||
| "sessionId": session_id, | ||
| "version": "1.0.0", | ||
| "uuid": uuid, | ||
| "content": "Conversation compacted", | ||
| "compactMetadata": {"trigger": "manual", "preTokens": pre_tokens}, | ||
| } |
There was a problem hiding this comment.
Add level to compact-boundary fixtures.
SystemMessage creation requires a system level, so this fixture can leave the compact-boundary parent out of uuid_to_msg and break the "100k tokens" label path.
🧪 Proposed fixture fix
"uuid": uuid,
+ "level": "info",
"content": "Conversation compacted",
"compactMetadata": {"trigger": "manual", "preTokens": pre_tokens},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/test_dag_integration.py` around lines 1378 - 1398, The compact-boundary
fixture returned by _make_compact_boundary is missing the system message "level"
field required by SystemMessage construction, which breaks uuid_to_msg and the
"100k tokens" label path; update the dict returned by _make_compact_boundary to
include a suitable "level" value (e.g., "system" or the expected enum/string
used elsewhere) alongside the existing keys like "type", "subtype", "uuid", and
"compactMetadata" so the fixture produces a valid SystemMessage.
| # -- Unit tests for _filter_compact ------------------------------------------ | ||
|
|
||
|
|
||
| class TestFilterMinimal: | ||
| """Test the _filter_compact function directly on parsed TranscriptEntry lists.""" |
There was a problem hiding this comment.
Stale _filter_compact references — tested symbol is _filter_by_detail.
The section header and class docstring both reference _filter_compact, but the tests call _filter_by_detail (imported at Line 32). This mismatches the tested symbol and makes the test intent harder to follow.
📝 Proposed fix
-# -- Unit tests for _filter_compact ------------------------------------------
+# -- Unit tests for _filter_by_detail ----------------------------------------
class TestFilterMinimal:
- """Test the _filter_compact function directly on parsed TranscriptEntry lists."""
+ """Test the _filter_by_detail function directly on parsed TranscriptEntry lists."""📝 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.
| # -- Unit tests for _filter_compact ------------------------------------------ | |
| class TestFilterMinimal: | |
| """Test the _filter_compact function directly on parsed TranscriptEntry lists.""" | |
| # -- Unit tests for _filter_by_detail ---------------------------------------- | |
| class TestFilterMinimal: | |
| """Test the _filter_by_detail function directly on parsed TranscriptEntry lists.""" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/test_detail_levels.py` around lines 130 - 134, The test file's section
header and class docstring incorrectly reference _filter_compact while the tests
actually import and exercise _filter_by_detail; update the section comment text
and the TestFilterMinimal class docstring to reference _filter_by_detail (or
otherwise clarify that the tests target _filter_by_detail) so the documented
intent matches the imported symbol _filter_by_detail used in the tests.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (5)
claude_code_log/html/templates/transcript.html (1)
862-867:⚠️ Potential issue | 🟠 MajorFallback to the single-level fold control when unfolding ancestors.
Line 864 only looks for
.fold-all-levels, but one-level fold bars render only.fold-one-level.full-width, so anchors under those folded ancestors can remain hidden.🐛 Proposed fix
- const foldAll = foldBar.querySelector('.fold-all-levels'); - if (foldAll && foldAll.classList.contains('folded')) { - foldAll.click(); + const foldAll = foldBar.querySelector('.fold-all-levels'); + const unfoldButton = foldAll || foldBar.querySelector('.fold-one-level.full-width'); + if (unfoldButton && unfoldButton.classList.contains('folded')) { + unfoldButton.click(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/html/templates/transcript.html` around lines 862 - 867, When expanding ancestor messages, the code only checks for a `.fold-all-levels` control and misses single-level fold bars (`.fold-one-level.full-width`), leaving anchors hidden; update the ancestor un-folding logic in the block that queries `foldBar` and `foldAll` so it first looks for `.fold-all-levels` and if not found falls back to selecting `.fold-one-level.full-width` (e.g., a `foldOne` variable) and triggers a click on whichever control exists and is in a folded state, using the existing `ancestorMsg.querySelector` approach and reusing the `foldBar` element/logic.claude_code_log/dag.py (1)
539-563:⚠️ Potential issue | 🟡 MinorAllow one ordinary primary root before warning on expected extra roots.
Line 546 still counts the normal initial user/assistant root as unexpected, so a common session shape like
user root + compact_boundary rootwill warn even though only the extra compact root is expected noise.🐛 Proposed root-classification fix
- unexpected = [n for n in roots if not _is_expected_root_type(n.entry)] + ordinary_roots = [n for n in roots if not _is_expected_root_type(n.entry)] + # Allow one ordinary root as the session's primary chain; warn only + # when additional ordinary roots indicate disconnected conversation. + unexpected = ordinary_roots[1:] if unexpected: logger.warning( "Session %s: %d roots found (%d unexpected), " @@ else: logger.debug( - "Session %s: %d expected roots (compact_boundary / " - "local_command), walking all from earliest (%s)", + "Session %s: %d roots found with only expected extra " + "roots, walking all from earliest (%s)", session_id, len(roots), roots[0].uuid,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/dag.py` around lines 539 - 563, The current unexpected-root check treats the normal initial user/assistant root as unexpected; change the logic in the roots-handling block so one ordinary primary root is allowed before flagging others: compute unexpected by skipping the first root when present (e.g. unexpected = [n for n in roots[1:] if not _is_expected_root_type(n.entry)]) or more explicitly allow roots[0] even if _is_expected_root_type(root[0].entry) is false, then use that unexpected list in the existing logger.warning/debug branches (symbols to update: roots, unexpected, _is_expected_root_type, session_id, logger).test/test_detail_levels.py (1)
130-134:⚠️ Potential issue | 🟡 MinorRename the stale
_filter_compactreferences.These tests exercise
_filter_by_detail, so the section header and class docstring should match the actual symbol under test.📝 Proposed fix
-# -- Unit tests for _filter_compact ------------------------------------------ +# -- Unit tests for _filter_by_detail ---------------------------------------- class TestFilterMinimal: - """Test the _filter_compact function directly on parsed TranscriptEntry lists.""" + """Test the _filter_by_detail function directly on parsed TranscriptEntry lists."""🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/test_detail_levels.py` around lines 130 - 134, The test section and class docstring incorrectly refer to _filter_compact; update the section comment header and the TestFilterMinimal class docstring to reference the actual symbol under test, _filter_by_detail (replace any occurrences of "_filter_compact" with "_filter_by_detail" in this block), and ensure the class name TestFilterMinimal still maps to tests exercising _filter_by_detail.claude_code_log/converter.py (1)
971-977:⚠️ Potential issue | 🟠 MajorInclude
detail/compactin cache identity before reusing cached artifacts.Render-mode invalidation is only partial: directory early exit, paginated page reuse, per-session reuse, and hierarchy
needs_workcan still skip generation for non-default output. Also, non-default renders still update the same HTML/page cache keys, so a later default render can be treated as current while the file contains filtered output.Until cache metadata stores
detail/compact, avoid cache reuse and cache updates for non-default render modes.🛠️ Minimum mitigation direction
+ non_default_render = detail != DetailLevel.FULL or ( + format in ("md", "markdown") and compact + ) + if ( cache_manager is not None and not cache_was_updated and from_date is None and to_date is None + and not non_default_render ): @@ - if format == "html" and cache_manager is not None: + if ( + format == "html" + and cache_manager is not None + and detail == DetailLevel.FULL + ): cache_manager.update_html_cache( output_path.name, None, total_message_count )For
_generate_paginated_html()and_generate_individual_session_files(), apply the same rule: do not skip or update cached page/session entries whendetail != DetailLevel.FULLuntil those cache tables compare render-mode metadata.Also applies to: 1071-1084, 1192-1210, 1303-1337, 1776-1829, 2128-2134
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/converter.py` around lines 971 - 977, Cached-page reuse and cache updates are incorrectly applied for non-default render modes; change the early-return/skip logic that relies on cache_manager.is_page_stale (and analogous session/hierarchy checks) to also consider render mode by refusing to reuse or update cached artifacts when detail != DetailLevel.FULL (i.e., when using compact/detail modes) in _generate_paginated_html() and _generate_individual_session_files(); until cache metadata is extended to store render-mode, ensure any branch that currently skips generation or updates the same HTML/page cache keys also bypasses reuse/update for non-default modes so cached keys aren’t incorrectly reused for different render outputs.dev-docs/dag.md (1)
460-462:⚠️ Potential issue | 🟡 MinorAdd a language identifier to this fenced block.
This is plain text, so
textkeeps markdownlint MD040 satisfied.📝 Proposed fix
-``` +```text 📦 Conversation compacted (115k tokens) • 2026-04-14 09:09:28</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@dev-docs/dag.mdaround lines 460 - 462, The fenced code block containing "📦
Conversation compacted (115k tokens) • 2026-04-14 09:09:28" lacks a language
identifier; update that fenced block to include thetextlanguage identifier
(i.e., change the opening backticks to ```text) so markdownlint MD040 is
satisfied and the block is explicitly marked as plain text.</details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against the current code and only fix it if needed.
Inline comments:
In@claude_code_log/cli.py:
- Around line 516-532: The --detail and --compact CLI options are never
forwarded when exporting a single session because generate_single_session_file()
doesn't accept them and the CLI returns before calling convert_jsonl_to; update
generate_single_session_file() in converter.py to add detail and compact
parameters, pass those through to get_renderer() where it's invoked (around the
get_renderer(...) call), and then update the CLI call site that invokes
generate_single_session_file() (the --session-id branch in cli.py) to call it
with detail=detail_level and compact=compact so the renderer receives and
applies those options.In
@claude_code_log/factories/system_factory.py:
- Around line 93-99: The code accepts boolean values for preTokens because
isinstance(raw_pre, int) returns True for booleans; update the check in the
block handling transcript.compactMetadata (inspect variables raw_pre and
pre_tokens in system_factory.py) to use a strict type comparison (type(raw_pre)
is int) before assigning pre_tokens, so True/False are rejected, leaving the
raw_trigger handling unchanged for strings.In
@claude_code_log/markdown/renderer.py:
- Around line 841-856: The compact-heading suppression currently compares
msg_type but should compare the actual rendered heading category; update the
logic around msg_type/title/suppress_heading so suppression and the stored
tracker use a normalized rendered-heading key (e.g., derive heading_key =
title.lower().strip() or another stable normalization of title) instead of
msg_type, then set self._last_message_type = heading_key and compare msg_type ==
self._last_message_type to use heading_key in the suppress_heading condition
(reference symbols: msg_type, title, suppress_heading, self._last_message_type,
compact). Ensure session-header reset still clears the tracker the same way.In
@claude_code_log/renderer.py:
- Around line 693-697: The current detail-level filtering replaces ctx.messages
with a filtered list via _filter_template_by_detail but leaves
RenderingContext.message_index values unchanged, causing RenderingContext.get()
and nav/parent indices to point to wrong messages; after filtering (or instead
of assigning directly) rebuild the RenderingContext indices by remapping
message_index values to the new positions (or re-register messages) so every
stored index references the correct message. Locate the block that calls
_filter_template_by_detail and update it to either perform the filter before
messages are registered in RenderingContext or, immediately after ctx.messages
is replaced, iterate the context's registration (or message_index map) and
recompute indices based on the filtered ctx.messages so RenderingContext.get()
returns consistent results.In
@test/__snapshots__/test_snapshot_html.ambr:
- Around line 6454-6471: unfoldAncestorsOf currently only opens the ancestor's
.fold-all-levelscontrol so messages still stay hidden if an ancestor is
collapsed via the.fold-one-levelcontrol; update unfoldAncestorsOf to also
find the.fold-one-levelelement on each ancestor's foldBar (the same place
you look up.fold-all-levels) and, if it exists and has thefoldedclass,
call its click() as well so both fold controls are unfolded when navigating to
an anchor (references: function unfoldAncestorsOf, variables ancestorMsg and
foldBar, selectors '.fold-all-levels' and '.fold-one-level').
Duplicate comments:
In@claude_code_log/converter.py:
- Around line 971-977: Cached-page reuse and cache updates are incorrectly
applied for non-default render modes; change the early-return/skip logic that
relies on cache_manager.is_page_stale (and analogous session/hierarchy checks)
to also consider render mode by refusing to reuse or update cached artifacts
when detail != DetailLevel.FULL (i.e., when using compact/detail modes) in
_generate_paginated_html() and _generate_individual_session_files(); until cache
metadata is extended to store render-mode, ensure any branch that currently
skips generation or updates the same HTML/page cache keys also bypasses
reuse/update for non-default modes so cached keys aren’t incorrectly reused for
different render outputs.In
@claude_code_log/dag.py:
- Around line 539-563: The current unexpected-root check treats the normal
initial user/assistant root as unexpected; change the logic in the
roots-handling block so one ordinary primary root is allowed before flagging
others: compute unexpected by skipping the first root when present (e.g.
unexpected = [n for n in roots[1:] if not _is_expected_root_type(n.entry)]) or
more explicitly allow roots[0] even if _is_expected_root_type(root[0].entry) is
false, then use that unexpected list in the existing logger.warning/debug
branches (symbols to update: roots, unexpected, _is_expected_root_type,
session_id, logger).In
@claude_code_log/html/templates/transcript.html:
- Around line 862-867: When expanding ancestor messages, the code only checks
for a.fold-all-levelscontrol and misses single-level fold bars
(.fold-one-level.full-width), leaving anchors hidden; update the ancestor
un-folding logic in the block that queriesfoldBarandfoldAllso it first
looks for.fold-all-levelsand if not found falls back to selecting
.fold-one-level.full-width(e.g., afoldOnevariable) and triggers a click
on whichever control exists and is in a folded state, using the existing
ancestorMsg.querySelectorapproach and reusing thefoldBarelement/logic.In
@dev-docs/dag.md:
- Around line 460-462: The fenced code block containing "📦 Conversation
compacted (115k tokens) • 2026-04-14 09:09:28" lacks a language identifier;
update that fenced block to include thetextlanguage identifier (i.e., change
the opening backticks to ```text) so markdownlint MD040 is satisfied and the
block is explicitly marked as plain text.In
@test/test_detail_levels.py:
- Around line 130-134: The test section and class docstring incorrectly refer to
_filter_compact; update the section comment header and the TestFilterMinimal
class docstring to reference the actual symbol under test, _filter_by_detail
(replace any occurrences of "_filter_compact" with "_filter_by_detail" in this
block), and ensure the class name TestFilterMinimal still maps to tests
exercising _filter_by_detail.</details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `a4ea6165-9eaf-40a2-b486-779e762dfe92` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 269b984e4b1daf5ca64c0992d1f9b07d10973e33 and 176a6ed35ba252dbf834a07cb44861adad112e29. </details> <details> <summary>📒 Files selected for processing (16)</summary> * `claude_code_log/cli.py` * `claude_code_log/converter.py` * `claude_code_log/dag.py` * `claude_code_log/factories/system_factory.py` * `claude_code_log/html/renderer.py` * `claude_code_log/html/templates/components/session_nav.html` * `claude_code_log/html/templates/components/session_nav_styles.css` * `claude_code_log/html/templates/transcript.html` * `claude_code_log/markdown/renderer.py` * `claude_code_log/models.py` * `claude_code_log/renderer.py` * `dev-docs/dag.md` * `test/__snapshots__/test_snapshot_html.ambr` * `test/test_dag.py` * `test/test_dag_integration.py` * `test/test_detail_levels.py` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
| @click.option( | ||
| "--detail", | ||
| type=click.Choice(["full", "high", "low", "minimal"], case_sensitive=False), | ||
| default="full", | ||
| help=( | ||
| "Detail level for output. " | ||
| "full: everything; " | ||
| "high: detailed but cleaned (no system/hook noise); " | ||
| "low: interaction-focused + key signals; " | ||
| "minimal: user + assistant messages only." | ||
| ), | ||
| ) | ||
| @click.option( | ||
| "--compact", | ||
| is_flag=True, | ||
| help="Merge consecutive same-type headings in Markdown output.", | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect generate_single_session_file signature and all call sites.
# Expectation: either generate_single_session_file accepts detail/compact, or it needs to be extended before cli.py passes them.
rg -nP -C5 '\bdef\s+generate_single_session_file\s*\('
rg -nP -C5 '\bgenerate_single_session_file\s*\('Repository: daaain/claude-code-log
Length of output: 10029
🏁 Script executed:
# Get the full signature of generate_single_session_file
sed -n '1838,1850p' claude_code_log/converter.pyRepository: daaain/claude-code-log
Length of output: 478
🏁 Script executed:
# Check the --session-id code path around line 732
sed -n '700,750p' claude_code_log/cli.py | cat -nRepository: daaain/claude-code-log
Length of output: 2552
🏁 Script executed:
# Search for convert_jsonl_to calls within generate_single_session_file
sed -n '1838,2000p' claude_code_log/converter.py | rg -C3 'convert_jsonl_to'Repository: daaain/claude-code-log
Length of output: 48
🏁 Script executed:
# Get the signature of convert_jsonl_to
rg -nP -C2 'def\s+convert_jsonl_to\s*\(' claude_code_log/converter.py | head -30Repository: daaain/claude-code-log
Length of output: 153
🏁 Script executed:
# Get the full signature of convert_jsonl_to
sed -n '1116,1130p' claude_code_log/converter.pyRepository: daaain/claude-code-log
Length of output: 553
🏁 Script executed:
# Get the full body of generate_single_session_file
sed -n '1838,1920p' claude_code_log/converter.pyRepository: daaain/claude-code-log
Length of output: 3031
🏁 Script executed:
# Continue reading generate_single_session_file to see what it calls
sed -n '1920,2000p' claude_code_log/converter.pyRepository: daaain/claude-code-log
Length of output: 2795
🏁 Script executed:
# Search for where the actual conversion happens in generate_single_session_file
rg -n 'def generate_single_session_file' -A 150 claude_code_log/converter.py | tail -100Repository: daaain/claude-code-log
Length of output: 3913
🏁 Script executed:
# Check if renderer.generate_session has any reference to detail/compact
rg -n 'def generate_session' claude_code_log/converter.pyRepository: daaain/claude-code-log
Length of output: 48
🏁 Script executed:
# Check the CLI to understand the detail and compact options better
sed -n '516,550p' claude_code_log/cli.py | cat -nRepository: daaain/claude-code-log
Length of output: 1216
🏁 Script executed:
# See the full main function signature including detail and compact parameters
sed -n '516,580p' claude_code_log/cli.py | cat -nRepository: daaain/claude-code-log
Length of output: 2492
🏁 Script executed:
# Search for where detail_level is used in cli.py
rg -n 'detail_level\|detail:' claude_code_log/cli.py | head -20Repository: daaain/claude-code-log
Length of output: 48
🏁 Script executed:
# Search for convert_jsonl_to calls in cli.py to see if detail_level is passed
rg -n 'convert_jsonl_to' claude_code_log/cli.py -A 10Repository: daaain/claude-code-log
Length of output: 1352
🏁 Script executed:
# Check line 732 and surrounding context to find the early return for --session-id
sed -n '690,740p' claude_code_log/cli.py | cat -nRepository: daaain/claude-code-log
Length of output: 2608
🏁 Script executed:
# Find the renderer classes and generate_session method
rg -n 'class.*Renderer' claude_code_log/ -A 5Repository: daaain/claude-code-log
Length of output: 1297
🏁 Script executed:
# Search for generate_session implementations
rg -n 'def generate_session' claude_code_log/ -B 2 -A 10 | head -50Repository: daaain/claude-code-log
Length of output: 3350
🏁 Script executed:
# Check how convert_jsonl_to calls the renderer methods
rg -n 'def convert_jsonl_to' -A 100 claude_code_log/converter.py | grep -A 20 'renderer\.' | head -40Repository: daaain/claude-code-log
Length of output: 48
🏁 Script executed:
# Check the full generate_session signature in MarkdownRenderer to see if it accepts detail/compact
sed -n '920,960p' claude_code_log/markdown/renderer.pyRepository: daaain/claude-code-log
Length of output: 1643
🏁 Script executed:
# Check if generate_session calls generate() and if generate() accepts/uses detail or compact
sed -n '920,945p' claude_code_log/markdown/renderer.pyRepository: daaain/claude-code-log
Length of output: 1027
🏁 Script executed:
# Look at the generate method signature
rg -n 'def generate\(' claude_code_log/markdown/renderer.py -A 8 | head -30Repository: daaain/claude-code-log
Length of output: 436
🏁 Script executed:
# Search for where detail or compact is actually used in the rendering
rg -n 'detail|compact' claude_code_log/renderer.py | head -30Repository: daaain/claude-code-log
Length of output: 1999
🏁 Script executed:
# Check how convert_jsonl_to uses detail_level and compact
sed -n '1116,1200p' claude_code_log/converter.pyRepository: daaain/claude-code-log
Length of output: 3701
🏁 Script executed:
# Check if renderer.generate_session actually uses detail/compact by searching for where these would be used
rg -n 'generate_individual_sessions' claude_code_log/converter.py -B 5 -A 15 | head -50Repository: daaain/claude-code-log
Length of output: 1805
🏁 Script executed:
# Check the full flow in convert_jsonl_to to see how it uses detail/compact with individual sessions
sed -n '1170,1230p' claude_code_log/converter.pyRepository: daaain/claude-code-log
Length of output: 2796
🏁 Script executed:
# Check if detail is used in renderer.generate() method
sed -n '879,950p' claude_code_log/markdown/renderer.py | grep -i 'detail\|compact'Repository: daaain/claude-code-log
Length of output: 133
🏁 Script executed:
# Check how convert_jsonl_to passes detail to the rendering process
sed -n '1220,1280p' claude_code_log/converter.pyRepository: daaain/claude-code-log
Length of output: 2612
🏁 Script executed:
# Check how get_renderer is called in the session path vs main path
rg -n 'get_renderer' claude_code_log/Repository: daaain/claude-code-log
Length of output: 757
🏁 Script executed:
# Check how the Renderer is initialized with detail/compact
sed -n '2633,2700p' claude_code_log/renderer.py | head -60Repository: daaain/claude-code-log
Length of output: 2413
🏁 Script executed:
# Get the full get_renderer signature
sed -n '2923,2960p' claude_code_log/renderer.pyRepository: daaain/claude-code-log
Length of output: 1484
🏁 Script executed:
# Confirm the issue: Line 1951 in converter.py (generate_single_session_file)
sed -n '1948,1958p' claude_code_log/converter.pyRepository: daaain/claude-code-log
Length of output: 478
Wire --detail and --compact into --session-id exports.
The --session-id branch returns at line 733 before reaching the convert_jsonl_to(...) calls that receive these options, so claude-code-log --session-id ... --detail minimal and --compact are silently ignored. The generate_single_session_file() function needs to accept these parameters and pass them to get_renderer().
🔎 Required changes
- Extend
generate_single_session_file()signature inconverter.pyto acceptdetailandcompactparameters - Pass these to
get_renderer()at line 1951 - Update the call site in
cli.py(lines 721–729) to passdetail=detail_levelandcompact=compact
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@claude_code_log/cli.py` around lines 516 - 532, The --detail and --compact
CLI options are never forwarded when exporting a single session because
generate_single_session_file() doesn't accept them and the CLI returns before
calling convert_jsonl_to; update generate_single_session_file() in converter.py
to add detail and compact parameters, pass those through to get_renderer() where
it's invoked (around the get_renderer(...) call), and then update the CLI call
site that invokes generate_single_session_file() (the --session-id branch in
cli.py) to call it with detail=detail_level and compact=compact so the renderer
receives and applies those options.
CodeRabbit flagged that `_filter_template_by_detail` returns a shorter list that was assigned directly to `ctx.messages`, while every surviving TemplateMessage kept its original `message_index`. `RenderingContext.get()` indexes into the list by position, so post-filter: - `ctx.get(idx)` returns the wrong message when idx sits past the new length, or refers to a neighbour that happened to slide into that slot. - `session_first_message[sid]` can point at an index that now resolves to a different session's first survivor, breaking nav anchors. - `pair_first` / `pair_last` hold stale references that make the Markdown renderer's `ctx.get(message.pair_first)` dereference wrong messages (the pair-identification pass below re-runs, but only after nav has already been built). Add `_reindex_filtered_context` to rewrite `ctx.messages` to the filtered list, remap every surviving message's `message_index` (and mirror it on `content.message_index`) to its new position, drop stale `session_first_message` entries whose target was filtered out, and clear the pair pointers so `_identify_message_pairs` can rebuild them cleanly. Ancestry is already rebuilt from scratch by `_build_message_hierarchy` after this point, so it doesn't need explicit remapping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`generate_single_session_file` called `get_renderer(format, image_export_mode)` with defaults, so `--session-id --detail low` or `--compact` silently produced a full-detail session file. CodeRabbit flagged this as a parameter threading gap on the single-session code path. Accept `detail` and `compact` on `generate_single_session_file` (defaulting to FULL / False to preserve the public signature) and pass both through to `get_renderer`. Update the CLI's `--session-id` branch to forward the CLI's already-parsed `detail_level` and `compact` flags. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Python's `bool` is a subclass of `int`, so `isinstance(True, int)` is True — meaning a JSON `true` or `false` in `compactMetadata.preTokens` would be copied onto `SystemMessage.compact_pre_tokens` as 1 or 0 and surface in the nav label as "1 tokens". CodeRabbit noted the pattern. Narrow the guard to `isinstance(raw_pre, int) and not isinstance(raw_pre, bool)` so only genuine integer token counts survive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Markdown compact mode suppressed headings for consecutive same-type messages using `msg.content.message_type`, but that raw type doesn't always match the rendered title category. A paired `ThinkingMessage` (is_first_in_pair, paired with an AssistantTextMessage) renders an "🤖 Assistant: ..." heading via `title_ThinkingMessage`, yet the tracking key stored "thinking". A following standalone AssistantText then failed to compact even though the visible headings were both Assistant. Track the rendered category derived from the title itself — everything before the first ":" (e.g. "🤖 Assistant", "🤷 User", "💭 Thinking"). Rename the tracking field from `_last_message_type` to `_last_heading_category` so the intent is clear. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A parent message with `immediate_children_count == total_descendants_count` only renders a single full-width `.fold-one-level` button (per transcript.html lines 106-111) — `.fold-all-levels` isn't emitted at all. The anchor-unfold JS queried `.fold-all-levels` unconditionally, so anchors sitting under one of those single-level ancestors stayed folded even when we'd navigated directly to them. CodeRabbit caught this on the merged alice-fix-nav-anchors flow. Fall back to `.fold-one-level.full-width` when `.fold-all-levels` is absent on a given fold-bar. Both controls cover the same descendant-hiding behaviour at their respective levels, so the same .folded guard + .click() pattern applies uniformly. Snapshots regenerate mechanically (pure JS comment + logic tweak, no structural change to output). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pyright flagged the `isinstance(detail, str) and not isinstance(detail, DetailLevel)` guard at renderer.py:595 as unnecessary because DetailLevel inherits from str. The guard was actually load-bearing for accepting raw strings from the CLI, so the fix is to reflect that in the annotation rather than removing the check. - Widen `detail` param to `DetailLevel | str` in generate_template_messages - Simplify normalization to `if not isinstance(detail, DetailLevel)` - Docstring clarified to mention both accepted forms Resolves 2 pre-existing pyright warnings monk flagged on the alice-coderabbit-fixes review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
claude_code_log/converter.py (2)
1781-1787:⚠️ Potential issue | 🟠 MajorPer-session regeneration check does not consider
detail/compact.
_generate_individual_session_filesnow acceptsdetail/compactand passes them toget_renderer, butshould_regenerate_sessionhere only checks cache staleness, version, date filters, and file existence. If a user previously generated sessions with--detail fulland now requests--detail low(or vice-versa), existing session files will be served as-is.Consider adding
or detail != DetailLevel.FULL or compactto the HTML branch (mirroring Lines 1309-1310) as an interim mitigation, and ideally factoringdetail/compactinto the html_cache staleness key for correctness in both directions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/converter.py` around lines 1781 - 1787, The per-session regeneration check in _generate_individual_session_files fails to account for detail/compact, so update the should_regenerate_session logic (the variable computed where renderer.is_outdated(session_file_path) is checked) to also consider the current rendering mode — e.g., add a condition that compares detail/compact with the previous/full state (mirroring the earlier check: or detail != DetailLevel.FULL or compact) so files generated under a different detail level are regenerated; also, for a robust fix, include the detail/compact values in the html_cache staleness key used by get_renderer/html_cache so different detail levels produce distinct cache entries and stale detection works correctly.
1059-1068:⚠️ Potential issue | 🟠 MajorPaginated generation bypasses
is_page_staleinvalidation for non-defaultdetail/compact.Inside
_generate_paginated_html,is_page_stale(Line 972) is the sole gate for regeneration and does not account fordetail/compact. Pages previously rendered with--detail fullwill be reused when the user requests--detail low(or vice-versa), returning the wrong content. The single-file path was hardened at Lines 1309–1310 and 1320–1321 but the paginated path has no equivalent guard.At minimum, short-circuit
is_stalewhendetail != DetailLevel.FULL or compactso pages always regenerate under non-default flags; longer-term, include these flags in the page_cache staleness identity.🛠️ Interim mitigation
- is_stale, reason = cache_manager.is_page_stale(page_num, page_size) - - if not is_stale and page_file.exists(): + is_stale, reason = cache_manager.is_page_stale(page_num, page_size) + force_regen = detail != DetailLevel.FULL or compact + + if not is_stale and not force_regen and page_file.exists():🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/converter.py` around lines 1059 - 1068, The paginated path in _generate_paginated_html reuses cached pages via is_page_stale but doesn't consider the detail or compact flags, causing mismatched renders; update _generate_paginated_html to force regeneration when detail != DetailLevel.FULL or compact is True (i.e., short-circuit/override is_page_stale to treat page as stale) before calling HtmlRenderer.generate, and as a longer-term fix include detail/compact in the page cache/staleness identity so page_cache keys consider those flags.
♻️ Duplicate comments (1)
claude_code_log/converter.py (1)
1192-1210:⚠️ Potential issue | 🟠 MajorEarly-exit ignores
detail/compact— can serve stale artifact when flags change.The Phase 1b early-exit at Lines 1192–1210 returns
output_pathbased only on cache freshness andis_html_outdated, without consideringdetailorcompact. If a user first runs with--detail low(which forces regeneration via Lines 1309-1310/1320-1321) and then re-runs with the default--detail full,is_html_stalewill report not-stale and the filtered LOW artifact will be returned for a FULL request.The
should_regenerateguard added for the single-file path (Lines 1303–1311, 1314–1322) only forces regeneration whendetail != FULL or compact; it cannot detect the inverse direction (switching back to FULL after a non-FULL run). A complete fix requires includingdetail/compactin the cache/staleness identity (e.g., stored in html_cache metadata, page cache, and session html_cache), and also checking them in this early-exit and in the paginated path (is_page_stale) and individual-session path (Lines 1781–1787).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/converter.py` around lines 1192 - 1210, The early-exit in converter.py (the block using cache_manager, is_html_stale(output_path.name, None), is_html_outdated, get_stale_sessions, generate_individual_sessions and returning output_path) ignores the detail and compact flags so it can return a filtered LOW/compact artifact for a FULL request; fix by including detail and compact in the cache identity and staleness checks: when calling is_html_stale (and is_page_stale) pass/derive a cache key that incorporates detail and compact (and update html_cache/page/session cache metadata to store these flags), and amend the early-exit condition to require regeneration when the cached artifact's detail/compact differ from the current request (also ensure session-level cache checks in get_stale_sessions and individual-session logic account for detail/compact).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@claude_code_log/converter.py`:
- Around line 1781-1787: The per-session regeneration check in
_generate_individual_session_files fails to account for detail/compact, so
update the should_regenerate_session logic (the variable computed where
renderer.is_outdated(session_file_path) is checked) to also consider the current
rendering mode — e.g., add a condition that compares detail/compact with the
previous/full state (mirroring the earlier check: or detail != DetailLevel.FULL
or compact) so files generated under a different detail level are regenerated;
also, for a robust fix, include the detail/compact values in the html_cache
staleness key used by get_renderer/html_cache so different detail levels produce
distinct cache entries and stale detection works correctly.
- Around line 1059-1068: The paginated path in _generate_paginated_html reuses
cached pages via is_page_stale but doesn't consider the detail or compact flags,
causing mismatched renders; update _generate_paginated_html to force
regeneration when detail != DetailLevel.FULL or compact is True (i.e.,
short-circuit/override is_page_stale to treat page as stale) before calling
HtmlRenderer.generate, and as a longer-term fix include detail/compact in the
page cache/staleness identity so page_cache keys consider those flags.
---
Duplicate comments:
In `@claude_code_log/converter.py`:
- Around line 1192-1210: The early-exit in converter.py (the block using
cache_manager, is_html_stale(output_path.name, None), is_html_outdated,
get_stale_sessions, generate_individual_sessions and returning output_path)
ignores the detail and compact flags so it can return a filtered LOW/compact
artifact for a FULL request; fix by including detail and compact in the cache
identity and staleness checks: when calling is_html_stale (and is_page_stale)
pass/derive a cache key that incorporates detail and compact (and update
html_cache/page/session cache metadata to store these flags), and amend the
early-exit condition to require regeneration when the cached artifact's
detail/compact differ from the current request (also ensure session-level cache
checks in get_stale_sessions and individual-session logic account for
detail/compact).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f1b69a7b-2fe6-4e31-a651-25d00ac69a01
📒 Files selected for processing (7)
claude_code_log/cli.pyclaude_code_log/converter.pyclaude_code_log/factories/system_factory.pyclaude_code_log/html/templates/transcript.htmlclaude_code_log/markdown/renderer.pyclaude_code_log/renderer.pytest/__snapshots__/test_snapshot_html.ambr
🚧 Files skipped from review as they are similar to previous changes (3)
- claude_code_log/cli.py
- claude_code_log/factories/system_factory.py
- claude_code_log/renderer.py
OK, noted. Will be done in a follow-up PR. |
The detail-level filter and compact-Markdown mode landed in #96 and #114 but weren't surfaced in the README. Add: - Key Features bullet calling out the pairing with --format md for feeding past conversations to an LLM - New Usage subsection "Feeding Past Conversations to an LLM" with the --detail low --format md --compact combo and a short level reference - Problem-statement entry for LLM-based analysis/experience building - Markdown Output Features bullet for --compact Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…115) * Preserve agentId anchors in parallel-Task stitch Parallel `Task` tool_uses emit a chain of assistant tool_use entries whose sibling tool_result anchors each reference a different subagent via agentId. Variant 2 of `_stitch_tool_results` classified the intermediate assistant subtrees as "dead-end" and let the walker mark their descendants — including agentId-bearing tool_results — as skipped, so only the outermost subagent session had an attachment point the traversal could find. Fix: add `_collect_agent_anchors` to walk an assistant subtree for UserTranscriptEntry descendants with agentId, and surface those anchors into the stitch result alongside the assistant dead-ends. The anchors still end up in `skipped` via `_collect_descendants` (since they're descendants of the assistant), but now they also live in the DAG-line's chain — which is what `build_session_tree` reads when choosing attachment points for subagent sessions. Regression tests cover the 2-teammate case, 3-teammate case (the experiments/worktrees shape), and an end-to-end splice verifying a subagent entry is actually reached through the anchor. Fixes issue #91 symptom: only 2 of 6 subagents rendering in a teammates session. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Restore collapse affordance on expanded tool-param details The generic tool renderer (render_params_table) wraps long parameter values in <details class='tool-param-collapsible'>. The CSS hid the entire <summary> on open, which also removed the browser's default disclosure arrow — leaving no visible way to collapse the expanded value. Wrap the preview text in <span class='tool-param-preview'> so the [open] rule can hide just the preview, not the whole summary. The summary (with its list-item marker / arrow) stays visible, and a "collapse" hint via ::after makes the click target obvious. Covers both call sites in tool_formatters.py: structured (JSON) and simple (string) long values. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Advertise --detail / --compact in README The detail-level filter and compact-Markdown mode landed in #96 and #114 but weren't surfaced in the README. Add: - Key Features bullet calling out the pairing with --format md for feeding past conversations to an LLM - New Usage subsection "Feeding Past Conversations to an LLM" with the --detail low --format md --compact combo and a short level reference - Problem-statement entry for LLM-based analysis/experience building - Markdown Output Features bullet for --compact Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
We add a
--detail <level>CLI flag that controls how much of a conversation gets rendered, and a--compactflag that merges consecutive same-type headings in Markdown output.Summary
--detail <level>— controls message-type filteringfull(default): everythinghigh: detailed but cleaned (drops SystemMessage, HookSummaryMessage, SlashCommandMessage, CommandOutputMessage, CompactedSummaryMessage, UserMemoryMessage, UnknownMessage). Keeps tools, bash, thinking.low: interaction-focused + key signals (drops Read/Edit/Bash/etc. tool_use/result, thinking, sidechains). Keeps WebSearch, WebFetch, and Task (agent delegation) tool_use/result pairs.minimal: user + assistant text only (equivalent to the original--shallowproposal).Implemented via a cumulative per-level exclude-class system (
_HIGH_EXCLUDE_CLASSES,_LOW_EXCLUDE_CLASSES,_MINIMAL_EXCLUDE_CLASSES).lowuses an additional keep-set for tool_use by name (_LOW_KEEP_TOOLS = {"WebSearch", "WebFetch", "Task"}).--compact— merges consecutive same-type headings (Markdown only)When
--detail lowor--detail minimalstrips tools, you often get runs of consecutive User or Assistant sections, each with its own###heading.--compactmerges them: the first heading stays, subsequent same-type headings are suppressed. Flow-of-reading is preserved.Markdown-only (HTML output has its own fold/expand UI). Works at any
--detaillevel, most useful withlow/minimal.Test plan
uv run pytest -n auto test/test_detail_levels.py -v— 40+ tests pass (4 HIGH + 5 LOW + 6 compact-markdown + existing minimal coverage)uv run pytest -n auto— full suite passes (897 unit tests)uv run pyright— 0 errorsuv run ty check— cleanclaude-code-log --detail minimal path/to/file.jsonlproduces clean user/assistant-only outputclaude-code-log --detail low --compact --format md path/to/file.jsonlmerges consecutive assistants into a single headingRelease Notes
New Features
--detail <level>CLI flag (full/high/low/minimal) replacing the older binary--shallowidea with a four-level filtering system.--compactCLI flag that merges consecutive same-type headings in Markdown output, improving readability of stripped transcripts.--detail lowpreserves WebSearch/WebFetch/Task tool calls as key conversation signals.Tests
Summary by CodeRabbit
New Features
Improvements
Documentation
Tests