claude-mission-control 1.13.0 → 1.14.0
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 -0
- package/lib/demo.js +25 -1
- package/lib/gitlog.js +42 -1
- package/package.json +1 -1
- package/public/index.html +15 -1
- package/server.js +6 -10
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.14.0**
|
|
39
|
+
|
|
40
|
+
- Cost per commit in each project's slide-over: every commit made during a Claude session shows its share of that session's cost, with a summary of cost per commit and spend in sessions that committed nothing.
|
|
41
|
+
|
|
38
42
|
**New in 1.13.0**
|
|
39
43
|
|
|
40
44
|
- Background sessions from `claude --bg` and Agent View carry a `BG` chip, the name Agent View gives them, and its one-line summary of what they did.
|
|
@@ -194,6 +198,7 @@ If something looks off, the log is at `~/Library/Logs/claude-dashboard.log`. To
|
|
|
194
198
|

|
|
195
199
|
|
|
196
200
|
- **Project details** — click any project's name for a slide-over with its full session list, recent git commits, rendered CLAUDE.md, per-project memory files, skills/agents/commands from `.claude/`, and settings (permissions, MCP servers, allowed tools), plus the resolved **Toolset** below. Read-only; also a quick audit of which projects are missing instructions or memory. Esc closes.
|
|
201
|
+
- **Cost per commit** — in each project's slide-over, every commit made during a Claude session carries a `≈$` figure: that session's cost split evenly over the commits it made (±5 minutes of its activity). Hover for which session it came from. The line above the list sums it up: cost per commit, how many of the recent commits came from a Claude session, and what was spent in sessions that committed nothing. It's an even split, so a one-line version bump counts the same as a feature.
|
|
197
202
|
- **Toolset** — inside the project slide-over: what Claude Code would actually use in that project. Every installed plugin, every agent and skill from the project, your user dir, and enabled plugins, and every MCP server that applies, each marked on or off with where it came from and which settings scope decided it, resolved with Claude Code's own precedence (managed › local › project › user, deny beats allow, project agents shadow user agents shadow plugin agents). An amber list at the top flags things worth a look: a plugin enabled but not installed or gone stale, a deny rule naming an agent that no longer exists, a server that needs auth, a settings file that doesn't parse. The `⧉` on a row copies the exact snippet to flip it, aimed at `.claude/settings.local.json`; MCP toggles live in Claude's own registry, so that recipe tells you to run `/mcp` in a session instead (servers from a project's `.mcp.json` also honour the settings keys `enabledMcpjsonServers`, `disabledMcpjsonServers` and `enableAllProjectMcpServers`, and their recipes use those). Buttons at the foot reveal the project's settings files in Finder. Read-only — the dashboard never edits Claude configuration.
|
|
198
203
|
|
|
199
204
|

|
package/lib/demo.js
CHANGED
|
@@ -463,5 +463,29 @@ function demoToolset() {
|
|
|
463
463
|
};
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
-
|
|
466
|
+
// Project slide-over for the demo: commits with their cost split from the
|
|
467
|
+
// sessions that made them (see commitCosts in lib/gitlog.js).
|
|
468
|
+
function demoProject() {
|
|
469
|
+
const now = Date.now();
|
|
470
|
+
const c = (sha, ago, subject, stat, cost, title, n) => ({
|
|
471
|
+
sha, ts: now - ago, subject, author: 'Demo', claude: cost != null, stat,
|
|
472
|
+
...(cost != null ? { cost, costFrom: [{ sessionId: S1, title, commits: n }], sessionId: S1, sessionTitle: title } : {}),
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
path: '/demo/acme-storefront', sessions: [], muted: false,
|
|
476
|
+
claudeMd: [], memory: [], skills: [], agents: [], commands: [], settings: {},
|
|
477
|
+
toolset: demoToolset(),
|
|
478
|
+
commits: [
|
|
479
|
+
c('9f3c2a1', 6 * MIN, 'Checkout: create the payment intent server-side', '3 files changed, 64 insertions(+), 9 deletions(-)', 8.2, 'Wire Stripe checkout into the cart', 3),
|
|
480
|
+
c('41be07d', 22 * MIN, 'Cart: call the checkout endpoint and redirect', '2 files changed, 31 insertions(+), 4 deletions(-)', 8.2, 'Wire Stripe checkout into the cart', 3),
|
|
481
|
+
c('c0d9e55', 41 * MIN, 'Remove the legacy checkout page', '1 file changed, 0 insertions(+), 212 deletions(-)', 8.2, 'Wire Stripe checkout into the cart', 3),
|
|
482
|
+
c('77a1f30', 3 * HOUR, 'Onboarding: account and store screens', '6 files changed, 240 insertions(+), 12 deletions(-)', 9.1, 'Design the onboarding flow', 2),
|
|
483
|
+
c('e52b8c4', 3 * HOUR + 40 * MIN, 'Onboarding: first-product screen', '3 files changed, 118 insertions(+), 3 deletions(-)', 9.1, 'Design the onboarding flow', 2),
|
|
484
|
+
c('1a6d9f2', 26 * HOUR, 'Bump dependencies', '2 files changed, 14 insertions(+), 14 deletions(-)', null),
|
|
485
|
+
],
|
|
486
|
+
commitSummary: { total: 6, withSession: 5, perCommit: 8.56, noCommitSpend: 3.4 },
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
module.exports = { demoState, demoStats, demoSession, demoChanges, demoInventory, demoToolset, demoProject };
|
|
467
491
|
|
package/lib/gitlog.js
CHANGED
|
@@ -44,6 +44,47 @@ function linkCommitsToSessions(commits, sessions) {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Cost per commit: each session's cost is split evenly across the commits
|
|
48
|
+
// made inside its activity window (same padded window as above). A commit
|
|
49
|
+
// inside two overlapping sessions gets a share of both. Sessions that cost
|
|
50
|
+
// something but committed nothing, since the oldest listed commit, are
|
|
51
|
+
// reported separately rather than spread over unrelated commits.
|
|
52
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
53
|
+
function commitCosts(commits, sessions) {
|
|
54
|
+
const cost = new Map(); // sha -> usd
|
|
55
|
+
const from = new Map(); // sha -> [{ sessionId, title, commits }]
|
|
56
|
+
const oldest = commits.length ? Math.min(...commits.map((c) => c.ts)) : Infinity;
|
|
57
|
+
let attributed = 0;
|
|
58
|
+
let noCommit = 0;
|
|
59
|
+
for (const s of sessions || []) {
|
|
60
|
+
const usd = s.estCost || 0;
|
|
61
|
+
if (usd < 0.005 || !s.lastActivityAt) continue;
|
|
62
|
+
const start = (s.startedAt || s.lastActivityAt) - PAD;
|
|
63
|
+
const end = s.lastActivityAt + PAD;
|
|
64
|
+
const inside = commits.filter((c) => c.ts >= start && c.ts <= end);
|
|
65
|
+
if (!inside.length) {
|
|
66
|
+
if (s.lastActivityAt >= oldest) noCommit += usd;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
attributed += usd;
|
|
70
|
+
for (const c of inside) {
|
|
71
|
+
cost.set(c.sha, (cost.get(c.sha) || 0) + usd / inside.length);
|
|
72
|
+
from.set(c.sha, [...(from.get(c.sha) || []), { sessionId: s.sessionId, title: s.title || null, commits: inside.length }]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const out = commits.map((c) => (cost.has(c.sha) ? { ...c, cost: round2(cost.get(c.sha)), costFrom: from.get(c.sha) } : c));
|
|
76
|
+
const withSession = cost.size;
|
|
77
|
+
return {
|
|
78
|
+
commits: out,
|
|
79
|
+
summary: {
|
|
80
|
+
total: commits.length,
|
|
81
|
+
withSession,
|
|
82
|
+
perCommit: withSession ? round2(attributed / withSession) : null,
|
|
83
|
+
noCommitSpend: round2(noCommit),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
47
88
|
function recentCommits(projectPath, limit = 30) {
|
|
48
89
|
return new Promise((resolve) => {
|
|
49
90
|
execFile(
|
|
@@ -55,4 +96,4 @@ function recentCommits(projectPath, limit = 30) {
|
|
|
55
96
|
});
|
|
56
97
|
}
|
|
57
98
|
|
|
58
|
-
module.exports = { parseGitLog, linkCommitsToSessions, recentCommits };
|
|
99
|
+
module.exports = { parseGitLog, linkCommitsToSessions, commitCosts, recentCommits };
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -1607,6 +1607,18 @@ function renderMd(raw) {
|
|
|
1607
1607
|
return `<div class="md">${out.join('')}</div>`;
|
|
1608
1608
|
}
|
|
1609
1609
|
|
|
1610
|
+
// "≈$3.40 per commit · 18 of 30 made during a Claude session · ≈$12 in sessions with no commit"
|
|
1611
|
+
function commitSummaryLine(s) {
|
|
1612
|
+
if (!s || !s.withSession) return '';
|
|
1613
|
+
const w = costWording();
|
|
1614
|
+
const parts = [
|
|
1615
|
+
`${costChip(s.perCommit, { cls: '' })} per commit`,
|
|
1616
|
+
`${s.withSession} of ${s.total} made during a Claude session`,
|
|
1617
|
+
fmtCost(s.noCommitSpend) ? `${costChip(s.noCommitSpend, { cls: '' })} in sessions with no commit` : '',
|
|
1618
|
+
].filter(Boolean);
|
|
1619
|
+
return `<div class="set-sub" style="margin:0 0 8px" title="${esc(`Each session's ${w.noun} is split evenly over the commits made while it was active (±5 minutes). Commits outside any session are left out.`)}">${parts.join(' · ')}</div>`;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1610
1622
|
function dSection(title, inner, empty) {
|
|
1611
1623
|
return `<details class="d-section" open><summary>${esc(title)}</summary>${
|
|
1612
1624
|
inner || `<div class="d-empty">${esc(empty)}</div>`}</details>`;
|
|
@@ -1697,6 +1709,8 @@ function renderDetail(d, name) {
|
|
|
1697
1709
|
${esc(c.subject)}
|
|
1698
1710
|
</span>
|
|
1699
1711
|
${c.sessionId ? `<button class="copy" data-session="${esc(c.sessionId)}" title="${esc(c.sessionTitle || '')}">session →</button>` : ''}
|
|
1712
|
+
${c.cost != null ? `<span title="${esc((c.costFrom || []).map((f) =>
|
|
1713
|
+
`${f.title || 'session'}: split over ${f.commits} commit${f.commits === 1 ? '' : 's'}`).join('\n'))}">${costChip(c.cost)}</span>` : ''}
|
|
1700
1714
|
<span class="s-when">${esc(statCompact(c.stat))}</span>
|
|
1701
1715
|
<span class="s-when">${relTime(c.ts)}</span>
|
|
1702
1716
|
</li>`).join('');
|
|
@@ -1708,7 +1722,7 @@ function renderDetail(d, name) {
|
|
|
1708
1722
|
$('#d-body').innerHTML =
|
|
1709
1723
|
muteBtn +
|
|
1710
1724
|
dSection(`Sessions — ${d.sessions.length}`, sessions, 'No sessions recorded.') +
|
|
1711
|
-
dSection(`What changed — last ${(d.commits || []).length} commits`, commitRows ? `<ul class="d-sessions">${commitRows}</ul>` : null, 'No git history here.') +
|
|
1725
|
+
dSection(`What changed — last ${(d.commits || []).length} commits`, commitRows ? commitSummaryLine(d.commitSummary) + `<ul class="d-sessions">${commitRows}</ul>` : null, 'No git history here.') +
|
|
1712
1726
|
dSection('CLAUDE.md', d.claudeMd.length ? dFiles(d.claudeMd, 'name') : null, 'No CLAUDE.md — sessions here start without project instructions.') +
|
|
1713
1727
|
dSection(`Memory — ${d.memory.length} file${d.memory.length === 1 ? '' : 's'}`, d.memory.length ? dFiles(d.memory, 'name') : null, 'No memory yet.') +
|
|
1714
1728
|
dSection(`Skills, agents & commands — ${caps.length}`, caps.length ? dFiles(caps, 'name') : null, 'None in .claude/skills, .claude/agents, or .claude/commands.') +
|
package/server.js
CHANGED
|
@@ -15,9 +15,9 @@ const { sessionTitle } = require('./lib/transcripts');
|
|
|
15
15
|
const { friendlyName } = require('./lib/names');
|
|
16
16
|
const cfg = require('./lib/config');
|
|
17
17
|
const { isProjectMuted } = require('./lib/notify');
|
|
18
|
-
const { recentCommits, linkCommitsToSessions } = require('./lib/gitlog');
|
|
18
|
+
const { recentCommits, linkCommitsToSessions, commitCosts } = require('./lib/gitlog');
|
|
19
19
|
const { sessionChangeList, sessionFileDiff } = require('./lib/changes');
|
|
20
|
-
const { demoState, demoStats, demoSession, demoChanges, demoInventory,
|
|
20
|
+
const { demoState, demoStats, demoSession, demoChanges, demoInventory, demoProject } = require('./lib/demo');
|
|
21
21
|
const { runSelfUpdate } = require('./lib/update');
|
|
22
22
|
const { readInventory } = require('./lib/inventory');
|
|
23
23
|
const { readToolset } = require('./lib/toolset');
|
|
@@ -133,13 +133,7 @@ const server = http.createServer((req, res) => {
|
|
|
133
133
|
}
|
|
134
134
|
if (url === '/api/search') return json(res, 200, { q: '', prompts: [], titles: [], notes: [], transcripts: null });
|
|
135
135
|
if (url === '/api/reveal-file') return json(res, 200, { ok: true });
|
|
136
|
-
if (url === '/api/project')
|
|
137
|
-
return json(res, 200, {
|
|
138
|
-
path: '/demo/acme-storefront', sessions: [], commits: [], muted: false,
|
|
139
|
-
claudeMd: [], memory: [], skills: [], agents: [], commands: [], settings: {},
|
|
140
|
-
toolset: demoToolset(),
|
|
141
|
-
});
|
|
142
|
-
}
|
|
136
|
+
if (url === '/api/project') return json(res, 200, demoProject());
|
|
143
137
|
if (url === '/api/config' && req.method === 'GET') {
|
|
144
138
|
return json(res, 200, { demo: true, notifications: true, usageApi: false, weeklyBudget: 200, idleNagMinutes: 60, terminals: [], resolvedTerminal: { id: 'terminal', label: 'Terminal' }, claudeApp: false, names: {}, ignores: [], themes: cfg.THEMES, version: VERSION, errors: [], pricingSource: null, unpricedModels: [] });
|
|
145
139
|
}
|
|
@@ -258,7 +252,9 @@ const server = http.createServer((req, res) => {
|
|
|
258
252
|
Promise.all([projectDetail(known), recentCommits(known), readInventory(collector.projectPaths())])
|
|
259
253
|
.then(async ([detail, commits, inventory]) => {
|
|
260
254
|
detail.sessions = collector.allSessions(known);
|
|
261
|
-
|
|
255
|
+
const costed = commitCosts(linkCommitsToSessions(commits, detail.sessions), detail.sessions);
|
|
256
|
+
detail.commits = costed.commits;
|
|
257
|
+
detail.commitSummary = costed.summary;
|
|
262
258
|
detail.muted = isProjectMuted(known, cfg.readConfig().mutedProjects);
|
|
263
259
|
detail.toolset = await readToolset(known, inventory);
|
|
264
260
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|