claude-mission-control 1.17.0 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/lib/collector.js +7 -2
- package/lib/pricing.js +30 -10
- package/package.json +1 -1
- package/public/index.html +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,10 @@ Claude Code runs in a terminal. Once you have two or three sessions going, you l
|
|
|
35
35
|
|
|
36
36
|
*Every screenshot here is demo data (`CLAUDE_DASH_DEMO=1`). The projects are invented.*
|
|
37
37
|
|
|
38
|
+
**New in 1.17.1**
|
|
39
|
+
|
|
40
|
+
- Burn rate is now your pace over the last 30 minutes. It used to divide lifetime cost by process age, which read in the thousands per hour right after resuming an old session.
|
|
41
|
+
|
|
38
42
|
**New in 1.17.0**
|
|
39
43
|
|
|
40
44
|
- Hive view (⬢ in the header): every live session as a character card. Each gets a generated face, a personality earned from what it has done ("the delegator", "the big spender", "the new kid"), a rank by spend, and its latest tool call on a live ticker.
|
|
@@ -226,7 +230,7 @@ If something looks off, the log is at `~/Library/Logs/claude-dashboard.log`. To
|
|
|
226
230
|
- Worktree sessions (`.claude/worktrees/…`) are folded into their parent project and badged `⎇`.
|
|
227
231
|
- **Notifications** — the moment any session flips to "waiting for input", you get a macOS notification (with sound) naming the project and what it needs — the question, the command awaiting permission, or Claude's last line. Fires once per wait, never on server restart. Mute a single noisy project from its slide-over (the bell button at the top), or disable everything with `CLAUDE_DASH_NOTIFY=0` in the plist. Notifications arrive via Script Editor/osascript — if you don't see them, allow it under System Settings → Notifications.
|
|
228
232
|
- **Cost estimates** — every session, project, and live row carries a `≈$` figure computed from the token counts in its transcript. On an API-key login it is labelled *estimated cost*; on a subscription it is *list-price value*, a relative weight rather than a bill (hover a cost chip for the wording). Rates are Anthropic list prices bundled with the dashboard (cached 2026-10-06). If your organization deploys Claude Code's `modelPricing` managed setting, the dashboard reads it and reports at your contracted rates; you can also put the same `modelPricing` block in `config.json` (⚙ settings → *copy skeleton*). A model the table doesn't know is priced at the Opus tier and marked with an amber `?` so a stale table is never silent. Subagent tokens are included.
|
|
229
|
-
- **Live cost** — running sessions show their cost so far on the departures board and in Mission Control, with a `$/h` burn rate
|
|
233
|
+
- **Live cost** — running sessions show their cost so far on the departures board and in Mission Control, with a `$/h` burn rate: the pace over the last 30 minutes, shown after five minutes of watching. A resumed session's earlier history doesn't count toward it.
|
|
230
234
|
- **Session cost breakdown** — the `≈$ ▾` button in a transcript's header opens a table by model (input, output, cache read, cache write), the subagent share, and the session's cache hit rate.
|
|
231
235
|
- **Context and tools** — each live session shows how full its context window was on the last turn (`412k / 1M`, amber from 70%, red from 90%), so you can see a compaction or a fresh session coming. Mission Control panes add the session's tool-call count with failures in amber, and the transcript's cost panel shows the same count in its footer. Read from the same incremental transcript scan as everything else; subagents are not included.
|
|
232
236
|
- **Most expensive sessions** — in the stats view: the ten costliest sessions all time, click to open.
|
package/lib/collector.js
CHANGED
|
@@ -15,7 +15,7 @@ const { worktreeRoot } = require('./paths');
|
|
|
15
15
|
const { sendNotification, newlyWaiting, overdueWaits, isProjectMuted, trackWaits } = require('./notify');
|
|
16
16
|
const { readConfig, readPricing } = require('./config');
|
|
17
17
|
const { hiveRoster } = require('./hive');
|
|
18
|
-
const { estimateCost, planValue, costReport, budgetLevel, typicalWait, burnRate, topByCost } = require('./pricing');
|
|
18
|
+
const { estimateCost, planValue, costReport, budgetLevel, typicalWait, burnRate, recordCostSample, topByCost } = require('./pricing');
|
|
19
19
|
const { readPlan } = require('./plan');
|
|
20
20
|
const { readTeams, agentStatus, resolveTeam } = require('./teams');
|
|
21
21
|
const { WINDOWS, recordSample, forecastWindow, shouldAlert, clockLabel, durationLabel } = require('./forecast');
|
|
@@ -39,6 +39,7 @@ class Collector {
|
|
|
39
39
|
this.state = { generatedAt: 0, quota: null, liveSessions: [], projects: [], errors: [] };
|
|
40
40
|
this.raw = { live: [], transcriptGroups: new Map(), history: new Map(), git: new Map(), quota: null, metaBySession: new Map(), teams: [] };
|
|
41
41
|
this.waitingSince = new Map(); // sessionId -> when it entered 'waiting'
|
|
42
|
+
this.costSamples = new Map(); // sessionId -> [[t, usd], ...] for burn rate
|
|
42
43
|
this.idleNotified = new Set(); // nagged once for the current wait
|
|
43
44
|
this.listeners = new Set();
|
|
44
45
|
this.events = []; // notification history for the catch-up bell, newest first
|
|
@@ -402,6 +403,8 @@ class Collector {
|
|
|
402
403
|
// displayed digit would. Null until the transcript has been scanned.
|
|
403
404
|
const report = costReport(usageBySession.get(s.sessionId) || {}, pricing);
|
|
404
405
|
const estCost = report.usd >= 0.005 ? Math.round(report.usd * 100) / 100 : null;
|
|
406
|
+
const samples = this.costSamples.get(s.sessionId) || [];
|
|
407
|
+
this.costSamples.set(s.sessionId, recordCostSample(samples, report.usd));
|
|
405
408
|
return {
|
|
406
409
|
quietMin,
|
|
407
410
|
waitingMin: (() => {
|
|
@@ -419,7 +422,7 @@ class Collector {
|
|
|
419
422
|
name: s.name,
|
|
420
423
|
model: modelBySession.get(s.sessionId) || null,
|
|
421
424
|
estCost,
|
|
422
|
-
burnRate: burnRate(
|
|
425
|
+
burnRate: burnRate(samples),
|
|
423
426
|
assumed: report.assumedModels.length > 0,
|
|
424
427
|
context: contextBySession.get(s.sessionId) || null,
|
|
425
428
|
tools: toolsBySession.get(s.sessionId) || null,
|
|
@@ -441,6 +444,8 @@ class Collector {
|
|
|
441
444
|
resumeCommand: resumeCommand(s.cwd, s.sessionId),
|
|
442
445
|
};
|
|
443
446
|
});
|
|
447
|
+
const stillLive = new Set(liveSessions.map((s) => s.sessionId));
|
|
448
|
+
for (const id of this.costSamples.keys()) if (!stillLive.has(id)) this.costSamples.delete(id);
|
|
444
449
|
const roster = hiveRoster(liveSessions);
|
|
445
450
|
for (const s of liveSessions) s.hive = roster[s.sessionId];
|
|
446
451
|
|
package/lib/pricing.js
CHANGED
|
@@ -165,16 +165,36 @@ function typicalWait(buckets) {
|
|
|
165
165
|
return null;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
168
|
+
// Spend per hour at the current pace: the cost added over the last 30
|
|
169
|
+
// minutes of samples, not lifetime cost over process age. A resumed session
|
|
170
|
+
// carries its whole history in its transcript, so lifetime/age read in the
|
|
171
|
+
// thousands right after a resume and near zero for a long-lived process.
|
|
172
|
+
// Null until five minutes of samples exist, and under $1/h. Whole dollars so
|
|
173
|
+
// the SSE state fingerprint doesn't churn.
|
|
174
|
+
const BURN_WINDOW_MS = 30 * 60 * 1000;
|
|
172
175
|
const BURN_MIN_MS = 5 * 60 * 1000;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
const BURN_SAMPLE_MS = 30 * 1000;
|
|
177
|
+
|
|
178
|
+
// Appends [now, usd] to a session's samples (at most one per 30s unless the
|
|
179
|
+
// cost moved) and drops what has aged out of the window. A cost that went
|
|
180
|
+
// down means the transcript was rewritten: start over.
|
|
181
|
+
function recordCostSample(samples, usd, now = Date.now()) {
|
|
182
|
+
if (usd == null || !Number.isFinite(usd)) return samples;
|
|
183
|
+
const last = samples[samples.length - 1];
|
|
184
|
+
if (last && usd < last[1]) samples.length = 0;
|
|
185
|
+
const prev = samples[samples.length - 1];
|
|
186
|
+
if (!prev || usd !== prev[1] || now - prev[0] >= BURN_SAMPLE_MS) samples.push([now, usd]);
|
|
187
|
+
while (samples.length > 2 && samples[1][0] <= now - BURN_WINDOW_MS) samples.shift();
|
|
188
|
+
return samples;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function burnRate(samples, now = Date.now()) {
|
|
192
|
+
if (!samples || samples.length < 2) return null;
|
|
193
|
+
const first = samples[0];
|
|
194
|
+
const last = samples[samples.length - 1];
|
|
195
|
+
const span = last[0] - first[0];
|
|
196
|
+
if (span < BURN_MIN_MS || now - last[0] > BURN_WINDOW_MS) return null;
|
|
197
|
+
const perHour = Math.round((last[1] - first[1]) / (span / 3600000));
|
|
178
198
|
return perHour >= 1 ? perHour : null;
|
|
179
199
|
}
|
|
180
200
|
|
|
@@ -226,4 +246,4 @@ function planValue(plan, weekCost) {
|
|
|
226
246
|
};
|
|
227
247
|
}
|
|
228
248
|
|
|
229
|
-
module.exports = { estimateCost, planValue, costReport, totalTokens, rateFor, normalizeModel, loadPricing, overrideFor, budgetLevel, typicalWait, burnRate, cacheHitRatio, topByCost, contextWindow };
|
|
249
|
+
module.exports = { estimateCost, planValue, costReport, totalTokens, rateFor, normalizeModel, loadPricing, overrideFor, budgetLevel, typicalWait, burnRate, recordCostSample, cacheHitRatio, topByCost, contextWindow };
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -1312,7 +1312,7 @@ function liveCard(s) {
|
|
|
1312
1312
|
<div class="dep-right">
|
|
1313
1313
|
${modelChip(s.model)}
|
|
1314
1314
|
${s.estCost != null ? `<span class="dep-cost">${costChip(s.estCost, { cls: 'dep-cost-main', assumed: s.assumed })}${
|
|
1315
|
-
s.burnRate ? `<span class="dep-burn" title="
|
|
1315
|
+
s.burnRate ? `<span class="dep-burn" title="Spend per hour over the last 30 minutes">$${s.burnRate}/h</span>` : ''}</span>` : ''}
|
|
1316
1316
|
${contextMeter(s.context)}
|
|
1317
1317
|
${s.waitingMin != null
|
|
1318
1318
|
? `<span class="dep-elapsed overdue" title="No reply for ${s.waitingMin} minutes — session started ${elapsed(s.startedAt)} ago">${waitLabel(s.waitingMin)}</span>`
|