mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-09 08:34:37 +00:00
Addresses the review findings on the per-turn PR attribution and adds the model and task-category surfaces. Correctness: - Cache migration: the 5 -> 6 session-cache bump now adopts the prior v5 file's expired-source PR entries instead of abandoning them, and the claude scan preserves and surfaces PR-bearing orphans, so a session whose transcript was deleted still appears as a legacy even-split instead of vanishing. The daily cache is untouched. - Date-range carry: the parser captures the PR set active at the start of the in-range turn slice and seeds the state machine with it, so a PR referenced before the window still owns its later, in-range, ref-less turns instead of the session falling back to a whole-session approx split. - Calls are split across a multi-PR turn by largest-remainder, keeping per-PR counts whole (a 1-call, 2-PR turn no longer renders as 2 calls). - The CLI and app footers reconcile to the rounded row values actually shown. - Distinct sessions are keyed by project + sessionId, not sessionId alone. - The app tolerates an older by-reference payload (no attributedCost): it keeps the old non-summable footer and never renders NaN. Features: - Each PR contribution records the models of its calls and the turn's task category (split by the same share on a multi-PR turn); legacy even-split rows carry the model union but no category breakdown. - Payload rows gain models (short names, cost-desc) and categories (label + cost, cost-desc, omitted when empty); the payload gains otherPrCount/otherPrCost for the PRs beyond the sent top 20. - CLI --by-pr gains a Models column. The desktop table is now full-width with a Models column and click-to-expand rows showing a per-category cost breakdown with proportional bars, keyboard accessible, with an "Other (N more PRs)" row when capped. Tests: state-machine seed/models/categories/largest-remainder/dedup, the prRefs round-trip through the real incremental-append path (continuation + straddle), v5 adoption of an expired PR session, payload round-trip, and the desktop expansion/models/old-payload cases.
278 lines
9.5 KiB
TypeScript
278 lines
9.5 KiB
TypeScript
export type TokenUsage = {
|
|
inputTokens: number
|
|
outputTokens: number
|
|
cacheCreationInputTokens: number
|
|
cacheReadInputTokens: number
|
|
cachedInputTokens: number
|
|
reasoningTokens: number
|
|
webSearchRequests: number
|
|
}
|
|
|
|
export type ToolUseBlock = {
|
|
type: 'tool_use'
|
|
id: string
|
|
name: string
|
|
input: Record<string, unknown>
|
|
}
|
|
|
|
export type ContentBlock =
|
|
| { type: 'text'; text: string }
|
|
| { type: 'thinking'; thinking: string }
|
|
| ToolUseBlock
|
|
| { type: string; [key: string]: unknown }
|
|
|
|
export type ApiUsage = {
|
|
input_tokens: number
|
|
output_tokens: number
|
|
cache_creation_input_tokens?: number
|
|
cache_creation?: {
|
|
ephemeral_5m_input_tokens?: number
|
|
ephemeral_1h_input_tokens?: number
|
|
}
|
|
cache_read_input_tokens?: number
|
|
server_tool_use?: {
|
|
web_search_requests?: number
|
|
web_fetch_requests?: number
|
|
}
|
|
speed?: 'standard' | 'fast'
|
|
// Claude Code advisor tool (/advisor): per-turn sub-usage records. A record
|
|
// with type 'advisor_message' carries the advisor model's own tokens and is
|
|
// NOT included in the top-level totals above; type 'message' records mirror
|
|
// the main model and are already covered by the top-level totals.
|
|
iterations?: ApiUsageIteration[]
|
|
}
|
|
|
|
export type ApiUsageIteration = {
|
|
type?: string
|
|
model?: string
|
|
input_tokens?: number
|
|
output_tokens?: number
|
|
cache_creation_input_tokens?: number
|
|
cache_creation?: {
|
|
ephemeral_5m_input_tokens?: number
|
|
ephemeral_1h_input_tokens?: number
|
|
}
|
|
cache_read_input_tokens?: number
|
|
server_tool_use?: {
|
|
web_search_requests?: number
|
|
web_fetch_requests?: number
|
|
}
|
|
speed?: 'standard' | 'fast'
|
|
}
|
|
|
|
export type AssistantMessageContent = {
|
|
model: string
|
|
id?: string
|
|
type: 'message'
|
|
role: 'assistant'
|
|
content: ContentBlock[]
|
|
usage: ApiUsage
|
|
stop_reason?: string
|
|
}
|
|
|
|
export type JournalEntry = {
|
|
type: string
|
|
uuid?: string
|
|
parentUuid?: string | null
|
|
timestamp?: string
|
|
sessionId?: string
|
|
cwd?: string
|
|
version?: string
|
|
gitBranch?: string
|
|
promptId?: string
|
|
message?: AssistantMessageContent | { role: 'user'; content: string | ContentBlock[] }
|
|
isSidechain?: boolean
|
|
[key: string]: unknown
|
|
}
|
|
|
|
export type ParsedTurn = {
|
|
userMessage: string
|
|
assistantCalls: ParsedApiCall[]
|
|
timestamp: string
|
|
sessionId: string
|
|
// Claude Code: the git branch active for this turn (top-level `gitBranch` on
|
|
// the turn's entries). Captured for cost-per-branch reporting; deduped at the
|
|
// cache boundary (stored per-turn only when it changes). Optional; Claude only.
|
|
gitBranch?: string
|
|
// Claude Code: GitHub PR URLs referenced during this turn (`pr-link` entries
|
|
// that landed inside the turn's span), sorted and deduplicated. Drives
|
|
// turn-level PR spend attribution; the session-level `prLinks` union is built
|
|
// separately. Absent when the turn referenced no PR. Optional; Claude only.
|
|
prRefs?: string[]
|
|
}
|
|
|
|
export type ParsedApiCall = {
|
|
provider: string
|
|
model: string
|
|
usage: TokenUsage
|
|
costUSD: number
|
|
tools: string[]
|
|
mcpTools: string[]
|
|
skills: string[]
|
|
subagentTypes: string[]
|
|
hasAgentSpawn: boolean
|
|
hasPlanMode: boolean
|
|
speed: 'standard' | 'fast'
|
|
timestamp: string
|
|
bashCommands: string[]
|
|
deduplicationKey: string
|
|
cacheCreationOneHourTokens?: number
|
|
toolSequence?: ToolCall[][]
|
|
/// When set, `costUSD` is the actual local call (forced to 0) and
|
|
/// `savingsUSD` is the counterfactual cost the same tokens would have
|
|
/// incurred against `savingsBaselineModel`. Set by the savings
|
|
/// normalization step in `src/parser.ts`.
|
|
savingsUSD?: number
|
|
savingsBaselineModel?: string
|
|
isLocalSavings?: boolean
|
|
/// True when this call's `costUSD` is priced from estimated token counts or
|
|
/// otherwise synthesized by the provider (e.g. Warp/Kiro/Cursor derive tokens
|
|
/// from content length). Carried from `ParsedProviderCall.costIsEstimated`
|
|
/// across the parser/cache boundary. Aggregates roll the estimated portion up
|
|
/// as `estimatedCostUSD`; it is display/metadata only and never changes totals.
|
|
isEstimated?: boolean
|
|
/// Lines added/removed by this call's edits, counted from tool-result diffs
|
|
/// (Claude: `toolUseResult.structuredPatch`). Numbers only, never patch text;
|
|
/// omitted when zero. Rich-session-capture (capture-only; no report yet).
|
|
locAdded?: number
|
|
locRemoved?: number
|
|
/// True only when at least one of this call's tool results was interrupted or
|
|
/// had its edit modified by the user (Claude `toolUseResult.interrupted` /
|
|
/// `userModified`). Omitted when false.
|
|
interrupted?: boolean
|
|
userModified?: boolean
|
|
/// Count of this call's tool results flagged `is_error` (Claude tool_result
|
|
/// blocks). Bash stderr alone is NOT counted (warnings go there). Omitted at 0.
|
|
toolErrors?: number
|
|
}
|
|
|
|
export type ToolCall = {
|
|
tool: string
|
|
file?: string
|
|
command?: string
|
|
}
|
|
|
|
export type TaskCategory =
|
|
| 'coding'
|
|
| 'debugging'
|
|
| 'feature'
|
|
| 'refactoring'
|
|
| 'testing'
|
|
| 'exploration'
|
|
| 'planning'
|
|
| 'delegation'
|
|
| 'git'
|
|
| 'build/deploy'
|
|
| 'conversation'
|
|
| 'brainstorming'
|
|
| 'general'
|
|
|
|
export type ClassifiedTurn = ParsedTurn & {
|
|
category: TaskCategory
|
|
subCategory?: string
|
|
retries: number
|
|
hasEdits: boolean
|
|
}
|
|
|
|
export type SessionSourceMetadata = {
|
|
id: string
|
|
label: string
|
|
path: string
|
|
kind: 'claude-config' | 'claude-desktop'
|
|
}
|
|
|
|
export type SessionSummary = {
|
|
sessionId: string
|
|
project: string
|
|
source?: SessionSourceMetadata
|
|
// Claude Code only: agent type of a subagent transcript session
|
|
// (`workflow-subagent`, `Explore`, `general-purpose`, …); undefined for
|
|
// ordinary sessions. Drives the Claude-scoped agent-type breakdown.
|
|
agentType?: string
|
|
firstTimestamp: string
|
|
lastTimestamp: string
|
|
totalCostUSD: number
|
|
totalSavingsUSD: number
|
|
/// Portion of `totalCostUSD` contributed by calls whose price is estimated
|
|
/// (see `ParsedApiCall.isEstimated`). Optional so SessionSummary fixtures and
|
|
/// producers predating the field keep compiling; the parser always sets it.
|
|
totalEstimatedCostUSD?: number
|
|
totalInputTokens: number
|
|
totalOutputTokens: number
|
|
totalReasoningTokens: number
|
|
totalCacheReadTokens: number
|
|
totalCacheWriteTokens: number
|
|
apiCalls: number
|
|
turns: ClassifiedTurn[]
|
|
/// GitHub PR URLs captured from the session transcript (session-level,
|
|
/// deduplicated). Absent when none were observed.
|
|
prLinks?: string[]
|
|
/// The PR set active at the start of the in-range turn slice: the refs of the
|
|
/// last turn BEFORE the report's range start that referenced any PR. Captured
|
|
/// pre-filter (like `everHadBranch`) so per-turn PR attribution can carry a
|
|
/// reference made before the window into its later, in-range, ref-less turns.
|
|
/// Absent when no PR was referenced before the range (or no range filter).
|
|
prRefsAtRangeStart?: string[]
|
|
/// Human session title captured from the transcript (last ai-title entry).
|
|
/// Absent when the transcript never produced one.
|
|
title?: string
|
|
/// True when the session observed a git branch on ANY turn of its FULL
|
|
/// (pre-date-filter) transcript. Set before turns are sliced to a range so the
|
|
/// by-branch report can still tell a branch-bearing Claude session (whose
|
|
/// in-range turns may all predate its first branch → the `null` bucket) apart
|
|
/// from a provider that never captures branches (→ contributes nothing).
|
|
/// Claude only; absent otherwise.
|
|
everHadBranch?: boolean
|
|
modelBreakdown: Record<string, { calls: number; costUSD: number; tokens: TokenUsage; savingsUSD: number; estimatedCostUSD?: number }>
|
|
toolBreakdown: Record<string, { calls: number }>
|
|
mcpBreakdown: Record<string, { calls: number }>
|
|
bashBreakdown: Record<string, { calls: number }>
|
|
categoryBreakdown: Record<TaskCategory, { turns: number; costUSD: number; savingsUSD: number; retries: number; editTurns: number; oneShotTurns: number }>
|
|
skillBreakdown: Record<string, { turns: number; costUSD: number; savingsUSD: number; editTurns: number; oneShotTurns: number }>
|
|
subagentBreakdown: Record<string, { calls: number; costUSD: number; savingsUSD: number }>
|
|
// Observed MCP tools available in this session, captured from
|
|
// `attachment.deferred_tools_delta.addedNames` entries. Union across all
|
|
// turns. Each name is a fully-qualified `mcp__<server>__<tool>` identifier.
|
|
// Built-in tools (Bash, Edit, etc.) are filtered out. Provider-agnostic field;
|
|
// currently populated only by the Claude parser.
|
|
mcpInventory?: string[]
|
|
}
|
|
|
|
export type ProjectSummary = {
|
|
project: string
|
|
projectPath: string
|
|
sessions: SessionSummary[]
|
|
totalCostUSD: number
|
|
totalSavingsUSD: number
|
|
/// Portion of `totalCostUSD` priced from estimated tokens (see
|
|
/// `SessionSummary.totalEstimatedCostUSD`). Optional for the same reason.
|
|
totalEstimatedCostUSD?: number
|
|
totalApiCalls: number
|
|
// Portion of `totalCostUSD` served through a subscription-backed proxy
|
|
// (config `proxyPaths`). `totalCostUSD` is left at the full API rate (the
|
|
// billable / would-be figure); this is the subscription-covered amount, so
|
|
// net out-of-pocket for the project is `totalCostUSD - totalProxiedCostUSD`.
|
|
// 0 when the project is not under a configured proxy path.
|
|
totalProxiedCostUSD: number
|
|
}
|
|
|
|
export type DateRange = {
|
|
start: Date
|
|
end: Date
|
|
}
|
|
|
|
export const CATEGORY_LABELS: Record<TaskCategory, string> = {
|
|
coding: 'Coding',
|
|
debugging: 'Debugging',
|
|
feature: 'Feature Dev',
|
|
refactoring: 'Refactoring',
|
|
testing: 'Testing',
|
|
exploration: 'Exploration',
|
|
planning: 'Planning',
|
|
delegation: 'Delegation',
|
|
git: 'Git Ops',
|
|
'build/deploy': 'Build/Deploy',
|
|
conversation: 'Conversation',
|
|
brainstorming: 'Brainstorming',
|
|
general: 'General',
|
|
}
|