diff --git a/AGENTS.md b/AGENTS.md index a4072f0..823930f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # GitNexus MCP -This project is indexed by GitNexus as **claude-session-intelligence** (698 symbols, 2165 relationships, 57 execution flows). +This project is indexed by GitNexus as **claude-session-intelligence** (704 symbols, 2183 relationships, 58 execution flows). ## Always Start Here diff --git a/CLAUDE.md b/CLAUDE.md index f790620..face0d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,7 +71,7 @@ Disable with `toolArchive.enabled=false` or `CLAUDE_TOOL_ARCHIVE=0` if you don't # GitNexus MCP -This project is indexed by GitNexus as **claude-session-intelligence** (698 symbols, 2165 relationships, 57 execution flows). +This project is indexed by GitNexus as **claude-session-intelligence** (704 symbols, 2183 relationships, 58 execution flows). ## Always Start Here diff --git a/README.md b/README.md index 3f9379c..b00cc3a 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ Set `fields` in `~/.claude/statusline-intel.json` to the list + order you want. | `compactCost` | `c$12.34 / s$68.20` | Cost + cache-savings accrued **since last /compact**. Incrementally cached at `/tmp/claude-cost-sincecompact-`, auto-invalidated when compact mtime advances. Shows `c$` only when ≥$0.01, `s$` only when ≥$0.10; hidden when both trivial (dim) | | `blockUsage` | `b:47% r:1hr 12m` | Claude's 5-hour block quota used + reset countdown; leads line 2 next to session duration + time until reset. Sourced from statusline stdin `rate_limits` (CC ≥2.1.90); cached `/api/oauth/usage` fallback for older builds, rendering last-good values with a `~` marker during API outages | | `weekUsage` | `w:31% r:4d 12hr` | Weekly (7-day) quota used + time until reset. Same sourcing as `blockUsage`; % zone-coloured: green <60%, yellow 60-85%, orange 85-95%, red ≥95% | -| `modelSplit` | `fable-5:sc$80·sp99%·wr:~85%` | Per model: session cost (`sc$`), share of session spend (`sp%`), and estimated weekly window remaining (`wr:~%`). `wr` attributes the account weekly used% across models by their share of this week's tracked spend — an SI estimate (hence `~`), since Anthropic exposes no per-model weekly quota. Parent-session splits are recorded to `session_model_costs` on each redraw, so the weekly attribution gets richer as sessions run | +| `modelSplit` | `fable-5:$1.7k·61% opus-4-8:$938·33%` | Per model: session spend (`$`, abbreviated to `k` past $1000) and share of session spend (`%`). Merges parent-conversation turns with subagent runs from the events DB; top 4 models, ones rounding to 0% dropped. No per-model quota is shown — Anthropic exposes only the account-wide weekly window (the `w:` cell), so a per-model "weekly remaining" would be fabricated. Per-model session spend is still recorded to `session_model_costs` for `/si stats` | | `tokenSpeed` | `tps:238` | Output-token generation speed (tokens/sec) — median of the last few streamed turns, measured from per-message chunk-timestamp spans (pure generation time). Falls back to session average (total output / total API time) when the tail has no streamed turn | | `modelQuota` | `opus:12/88%` | Per-model QUOTA used/available — dormant until Anthropic populates per-model limits (stdin extra `rate_limits` keys or `seven_day_` buckets in the usage payload; both are null on plans without per-model limits) | | `cwd` | `~/DWS/claude-session-intelligence` | Full working directory, ccstatusline-style. `$HOME` collapses to `~`; middle-ellipsis when longer than 60 chars (keeps the leaf) (dim) | diff --git a/plugins/session-intelligence/statusline/statusline-intel.js b/plugins/session-intelligence/statusline/statusline-intel.js index 8d9bd5e..04092cd 100755 --- a/plugins/session-intelligence/statusline/statusline-intel.js +++ b/plugins/session-intelligence/statusline/statusline-intel.js @@ -712,6 +712,17 @@ function fmtUsd(n) { return `$${n.toFixed(2)}`; } +// Denser USD for the per-model split, where a long session stacks several +// figures on one line: roll to `k` past $1000 so `$1727` reads `$1.7k`, +// `$12480` reads `$12k`. Small values keep cents. +function fmtUsdShort(n) { + if (!Number.isFinite(n) || n <= 0) return '$0'; + if (n >= 10000) return `$${Math.round(n / 1000)}k`; + if (n >= 1000) return `$${(n / 1000).toFixed(1)}k`; + if (n >= 10) return `$${Math.round(n)}`; + return `$${n.toFixed(2)}`; +} + // Single-letter duration formatter for tight cells (compactAge, session). // `3hr 23m` -> `3h23m`, `11hr` -> `11h`, `45m` -> `45m`. Spans over 1d // keep the day component and skip minutes — the precision isn't useful at @@ -1519,7 +1530,7 @@ function buildRenderers(C) { return `${C.dim}tps:${v}${C.reset}`; }, - modelSplit: (input, ctx) => { + modelSplit: (_input, ctx) => { // Parent-conversation turns from the transcript accumulator... const byModel = (ctx.tokenTotals && ctx.tokenTotals.byModel) || {}; const merged = { ...byModel }; @@ -1534,45 +1545,28 @@ function buildRenderers(C) { if (!entries.length || total <= 0) return ''; entries.sort((a, b) => b[1] - a[1]); - const events = loadEventsLib(); - // Persist this session's per-model split so weeklyModelSpend can see - // parent-session spend across sessions (agents are already in the DB). + // Persist this session's per-model split for cross-session analytics + // (/si stats) — the only surviving record of parent-session splits + // once a transcript ages out. try { + const events = loadEventsLib(); if (events && typeof events.upsertSessionModelCosts === 'function' && ctx.sessionId) { events.upsertSessionModelCosts(ctx.sessionId, byModel); } } catch { /* telemetry is best-effort */ } - // Per-model weekly-remaining ESTIMATE (`wr:~61%`): Anthropic exposes - // no per-model weekly quota (seven_day_ buckets are null on - // plans without per-model limits — verified 2026-07-17), so attribute - // the account weekly used% across models by their share of this - // week's tracked spend: wr(model) = 100 − weeklyUsed × spendShare. - // `~` marks it as SI's estimate, not an Anthropic number. - let weeklyUsed = null; - const sd = input && input.rate_limits && input.rate_limits.seven_day; - if (sd && typeof sd.used_percentage === 'number') weeklyUsed = sd.used_percentage; - let weekSpend = null; - if (weeklyUsed !== null && events && typeof events.weeklyModelSpend === 'function') { - try { weekSpend = events.weeklyModelSpend(); } catch { weekSpend = null; } - } - const weekTotal = weekSpend ? weekSpend.reduce((sum, r) => sum + (r.cost_usd || 0), 0) : 0; - const weekByModel = weekSpend - ? Object.fromEntries(weekSpend.map((r) => [r.model, r.cost_usd || 0])) - : {}; - - const parts = entries.slice(0, 4).map(([m, c]) => { + // Self-labelling `$·%`: session spend + share of session spend. No + // per-model quota shown — Anthropic exposes only the account-wide + // weekly window (the separate `w:` cell), so a per-model "weekly + // remaining" would be fabricated. Models rounding to 0% are dropped. + const parts = []; + for (const [m, c] of entries.slice(0, 4)) { + const share = Math.round((c / total) * 100); + if (share < 1) continue; const name = m.replace(/^claude-/, '').replace(/-20\d{6,}$/, ''); - const usd = fmtUsd(c); - let cell = `${name}:sc${usd || '$0'}\u00b7sp${Math.round((c / total) * 100)}%`; - if (weeklyUsed !== null && weekTotal > 0) { - const share = (weekByModel[m] || 0) / weekTotal; - const wr = Math.max(0, Math.min(100, Math.round(100 - weeklyUsed * share))); - cell += `\u00b7wr:~${wr}%`; - } - return cell; - }); - return `${C.dim}${parts.join(' ')}${C.reset}`; + parts.push(`${name}:${fmtUsdShort(c)}·${share}%`); + } + return parts.length ? `${C.dim}${parts.join(' ')}${C.reset}` : ''; }, /** Per-model QUOTA used/available — dormant until real data exists.