Skip to content

🧹 tui: hide zero-contribution model rows like <synthetic> from the models breakdown #484

Description

@martinciu

Parent: #475

πŸ” What

The models breakdown panel (m) shows a <synthetic> row that contributes nothing:

Fable 5      $690  3M  39% β”‚ Opus 4.7      $22  167k  1%
Opus 5       $461  2M  26%   Haiku 4.5     $15  637k  1%
Opus 4.8     $433  4M  25%   <synthetic>    $0     0  0%
Sonnet 5     $126  3M   7%

<synthetic> is Claude Code's own marker for assistant turns produced locally rather than by an API call. ccpulse records it verbatim β€” pkg/parse writes model straight from the JSONL envelope with no validation, and the only references in this repo are test fixtures.

Measured against a real state.db:

rows input output cache read cache write cost pricing_unknown
153 0 0 0 0 $0.00 153

Every row is zero across the board, and it is the only model in that cache with zero I/O tokens. It occupies a cell in a panel whose cells are a scarce resource (the box collapses the tail to "…N more").

🎯 Proposed rule

Not "drop <synthetic>" β€” hardcoding a magic id is the tabulation pkg/models deliberately avoided, and it would go stale the moment Claude Code introduces another marker.

Instead: drop canonical rows that contribute zero tokens AND zero cost.

πŸ”Ή Removes <synthetic> today, and any future zero-contribution marker automatically
πŸ”Ή Keeps genuinely unpriced-but-used models (gpt-oss:20b, …) visible at $0 β€” that was the point of the original "unpriced models are kept, not dropped" rule, whose stated premise is "they carry real token counts". <synthetic> fails that premise.
πŸ”Ή Leaves the (unknown model) bucket alone when it carries tokens; an empty-model row with real usage still shows, still forced last.

πŸ”’ Why this cannot break panel reconciliation

The invariant that makes p and m agree is that both aggregates sum the same rows over the same window. Dropped rows sum to zero by construction, so neither the cost total nor the token total moves. ProjectAggregates needs no matching change β€” it would still be summing the same zeros.

⚠️ Implementation note β€” CORRECTED

The original rationale in this issue was wrong. It claimed the filter must run post-fold because "filtering pre-fold could drop a zero-token dated variant that should have merged into a non-zero canonical row." That is mathematically impossible for a SUM aggregate: a raw row contributing (0 tokens, $0) cannot change whether its canonical group is non-zero. A 400-fixture randomized differential found zero cases in the claimed direction.

The real difference, and the only observable one: post-fold placement is strictly more aggressive. It additionally drops canonical groups whose raw rows cancel to zero β€” e.g. a model with +1000/-1000 tokens and +$5.00/-$5.00, which reaches (0, 0) from non-zero inputs and disappears from the panel.

That cancellation behaviour is the deliberate choice (such a row contributes zero to every total, and mixed-sign token counts are already malformed input), and it is the only thing a test can use to pin the placement β€” a "zero-token dated variant" fixture passes under both placements, which is itself the proof the original rationale was false.

πŸ§ͺ Acceptance

βœ… A window containing only zero-contribution rows renders the existing no activity in this window placeholder
βœ… Reconciliation test still passes β€” totals identical before and after
βœ… A model with zero cost but non-zero tokens is still shown
βœ… Reverting the filter makes the new test fail

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestimportance: mediumMeaningful improvement; not blockingsize: sSmall β€” about an hourtuiTUI rendering, layout, or interaction

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions