fix(report): address CodeRabbit review — pricing match, sidechain filter, UX guards

- getPricing: use tokenized order-insensitive matching so Claude 3 era
  models (e.g. "claude-3-opus-20240229") correctly match pricing key "opus-3"
- sessionAnalyzer: skip isSidechain messages in parentCost loop to prevent
  double-counting with processSubagentCost
- CostSection: disable row expansion when token stats are missing
- tabSlice: guard against undefined sessionId/projectId in openSessionReport

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Holstein 2026-02-22 08:43:33 -05:00
parent 4fda90bc3e
commit 586cb00174
3 changed files with 11 additions and 7 deletions

View file

@ -182,17 +182,17 @@ export const CostSection = ({
<tbody>
{modelEntries.map(([model, cost]) => {
const stats = tokensByModel[model];
const isExpanded = expandedModel === model;
const isExpanded = expandedModel === model && !!stats;
const pricing = getPricing(model);
return (
<Fragment key={model}>
<tr
className="border-border/50 hover:bg-surface-raised/50 cursor-pointer border-b"
onClick={() => setExpandedModel(isExpanded ? null : model)}
className={`border-border/50 border-b ${stats ? 'hover:bg-surface-raised/50 cursor-pointer' : ''}`}
onClick={() => stats && setExpandedModel(isExpanded ? null : model)}
>
<td className="py-1.5 pr-4 text-text">
<span className="mr-1.5 inline-block w-3 text-text-muted">
{isExpanded ? '\u25BC' : '\u25B6'}
{stats ? (isExpanded ? '\u25BC' : '\u25B6') : ''}
</span>
{model}
</td>

View file

@ -387,6 +387,7 @@ export const createTabSlice: StateCreator<AppState, [], [], TabSlice> = (set, ge
const allTabs = getAllTabs(state.paneLayout);
const sourceTab = allTabs.find((t) => t.id === sourceTabId);
if (sourceTab?.type !== 'session') return;
if (!sourceTab.sessionId || !sourceTab.projectId) return;
const tabData = state.tabSessionData[sourceTabId];
const firstMsg = tabData?.sessionDetail?.session.firstMessage;

View file

@ -104,9 +104,10 @@ const DEFAULT_PRICING: ModelPricing = {
};
export function getPricing(modelName: string): ModelPricing {
const name = modelName.toLowerCase();
const nameTokens: string[] = modelName.toLowerCase().match(/[a-z0-9]+/g) ?? [];
for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
if (name.includes(key)) return pricing;
const keyTokens: string[] = key.match(/[a-z0-9]+/g) ?? [];
if (keyTokens.every((t) => nameTokens.includes(t))) return pricing;
}
return DEFAULT_PRICING;
}
@ -454,7 +455,9 @@ export function analyzeSession(detail: SessionDetail): SessionReport {
}
// --- Token usage, cache economics, and cost ---
if (m.usage && m.model) {
// Skip sidechain messages to avoid double-counting (subagent costs are
// accounted for separately via processSubagentCost).
if (m.usage && m.model && !m.isSidechain) {
const model = m.model;
const u = m.usage;
const inpTok = u.input_tokens ?? 0;