claude-mission-control 1.19.0 → 1.20.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/lib/collector.js +14 -3
- package/lib/transcripts.js +15 -1
- package/package.json +1 -1
- package/public/index.html +40 -8
package/lib/collector.js
CHANGED
|
@@ -7,7 +7,7 @@ const { readQuota } = require('./quota');
|
|
|
7
7
|
const { fetchOauthUsage } = require('./usage');
|
|
8
8
|
const { readRegistry } = require('./registry');
|
|
9
9
|
const { refreshHistory, activityBuckets, weekHourHeat } = require('./history');
|
|
10
|
-
const { scanAllTranscripts, scanFile, groupByProject, sessionTitle, combinedUsage, combinedDays, dailyCostSeries, subagentSummary, usageBreakdown, dayKey, reasonText, contextSummary, toolSummary } = require('./transcripts');
|
|
10
|
+
const { scanAllTranscripts, scanFile, groupByProject, sessionTitle, sessionKind, combinedUsage, combinedDays, dailyCostSeries, subagentSummary, usageBreakdown, dayKey, reasonText, contextSummary, toolSummary } = require('./transcripts');
|
|
11
11
|
const { collectGitStatus } = require('./gitstatus');
|
|
12
12
|
const { friendlyName } = require('./names');
|
|
13
13
|
const { isIgnored } = require('./ignore');
|
|
@@ -277,6 +277,7 @@ class Collector {
|
|
|
277
277
|
const liveByProject = new Set(
|
|
278
278
|
this.raw.live.map((s) => worktreeRoot(s.cwd).root.toLowerCase())
|
|
279
279
|
);
|
|
280
|
+
const liveIds = new Set(this.raw.live.map((s) => s.sessionId));
|
|
280
281
|
|
|
281
282
|
const projects = [];
|
|
282
283
|
for (const p of this.projectPaths()) {
|
|
@@ -287,6 +288,15 @@ class Collector {
|
|
|
287
288
|
const git = this.raw.git.get(p) || null;
|
|
288
289
|
|
|
289
290
|
const sessionMetas = group ? group.sessions : [];
|
|
291
|
+
// Automated and empty sessions still count toward spend and activity;
|
|
292
|
+
// they just don't take up rows in the card.
|
|
293
|
+
const shown = [];
|
|
294
|
+
const hidden = { automated: 0, empty: 0 };
|
|
295
|
+
for (const m of sessionMetas) {
|
|
296
|
+
const kind = sessionKind(m, liveIds.has(m.sessionId));
|
|
297
|
+
if (kind) hidden[kind]++;
|
|
298
|
+
else shown.push(m);
|
|
299
|
+
}
|
|
290
300
|
const lastActivityAt = Math.max(
|
|
291
301
|
sessionMetas[0] ? sessionMetas[0].lastActivityAt : 0,
|
|
292
302
|
hist ? hist.lastTimestamp : 0,
|
|
@@ -309,7 +319,8 @@ class Collector {
|
|
|
309
319
|
Date.now() - m.lastActivityAt < SPEND_WINDOW_MS ? sum + estimateCost(combinedUsage(m), pricing) : sum,
|
|
310
320
|
0
|
|
311
321
|
),
|
|
312
|
-
|
|
322
|
+
hidden,
|
|
323
|
+
sessions: shown.slice(0, SESSIONS_PER_PROJECT).map((m) => {
|
|
313
324
|
const { title, source } = sessionTitle(m, sessionNames);
|
|
314
325
|
return {
|
|
315
326
|
sessionId: m.sessionId,
|
|
@@ -508,7 +519,6 @@ class Collector {
|
|
|
508
519
|
const forecast = this.limitForecast();
|
|
509
520
|
|
|
510
521
|
// Background jobs, live or finished, with any worktree work at risk.
|
|
511
|
-
const liveIds = new Set(this.raw.live.map((s) => s.sessionId));
|
|
512
522
|
const jobWt = this.raw.jobWt || new Map();
|
|
513
523
|
const jobsOut = jobs.map((j) => ({
|
|
514
524
|
jobId: j.jobId,
|
|
@@ -543,6 +553,7 @@ class Collector {
|
|
|
543
553
|
sessionId: m.sessionId,
|
|
544
554
|
title,
|
|
545
555
|
titleSource: source,
|
|
556
|
+
kind: sessionKind(m, this.raw.live.some((s) => s.sessionId === m.sessionId)),
|
|
546
557
|
estCost: estimateCost(combinedUsage(m), pricing),
|
|
547
558
|
assumed: costReport(combinedUsage(m), pricing).assumedModels.length > 0,
|
|
548
559
|
awaySummary: m.awaySummary,
|
package/lib/transcripts.js
CHANGED
|
@@ -128,6 +128,7 @@ async function scanFile(abs, sessionIdFromName) {
|
|
|
128
128
|
awaySummaryAt: null,
|
|
129
129
|
aiTitle: null,
|
|
130
130
|
waitingReason: null,
|
|
131
|
+
entrypoint: null,
|
|
131
132
|
};
|
|
132
133
|
|
|
133
134
|
// Head: first ~40 lines; the first line can be queue-operation without cwd.
|
|
@@ -148,6 +149,9 @@ async function scanFile(abs, sessionIdFromName) {
|
|
|
148
149
|
const t = Date.parse(rec.timestamp);
|
|
149
150
|
if (!Number.isNaN(t)) meta.startedAt = t;
|
|
150
151
|
}
|
|
152
|
+
if (meta.entrypoint === null && rec.type === 'user' && typeof rec.entrypoint === 'string') {
|
|
153
|
+
meta.entrypoint = rec.entrypoint;
|
|
154
|
+
}
|
|
151
155
|
if (meta.firstUserPrompt === null) {
|
|
152
156
|
const text = extractUserText(rec);
|
|
153
157
|
if (text) meta.firstUserPrompt = text.slice(0, 120);
|
|
@@ -573,6 +577,16 @@ function combinedUsage(meta) {
|
|
|
573
577
|
return mergeUsage(mergeUsage({}, meta.usage), meta.subagentUsage);
|
|
574
578
|
}
|
|
575
579
|
|
|
580
|
+
// Sessions nobody needs in a project list. 'automated': a one-shot claude -p
|
|
581
|
+
// run (recap and summarizer tools, plugin probes). 'empty': opened and closed
|
|
582
|
+
// without ever calling the model — only /clear, /exit and the like. A live
|
|
583
|
+
// session is never empty; it may just not have had its first reply yet.
|
|
584
|
+
function sessionKind(meta, isLive = false) {
|
|
585
|
+
if (meta.entrypoint === 'sdk-cli') return 'automated';
|
|
586
|
+
if (!isLive && !Object.keys(combinedUsage(meta)).length) return 'empty';
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
576
590
|
// Cost breakdown for the transcript viewer, from the meta the collector
|
|
577
591
|
// already holds — never re-reads the file. `context` and `tools` are
|
|
578
592
|
// filled by the scanner once the context-pressure batch lands.
|
|
@@ -671,7 +685,7 @@ function groupByProject(sessions) {
|
|
|
671
685
|
}
|
|
672
686
|
|
|
673
687
|
module.exports = {
|
|
674
|
-
scanAllTranscripts, scanFile, groupByProject, sessionTitle, combinedUsage, mergeUsage,
|
|
688
|
+
scanAllTranscripts, scanFile, groupByProject, sessionTitle, sessionKind, combinedUsage, mergeUsage,
|
|
675
689
|
scanLine, mergeDays, combinedDays, dailyCostSeries, subagentSummary, usageBreakdown, dayKey, contextSummary, toolSummary,
|
|
676
690
|
waitingReason, reasonText, shouldReadMeta, TOOL_RING,
|
|
677
691
|
};
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -350,6 +350,17 @@
|
|
|
350
350
|
}
|
|
351
351
|
.sessions li:hover > .acts, .sessions li:focus-within > .acts { pointer-events: auto; }
|
|
352
352
|
}
|
|
353
|
+
.hidden-note {
|
|
354
|
+
align-self: flex-start; margin: 6px 0 4px; padding: 2px 0;
|
|
355
|
+
border: none; background: none; cursor: pointer;
|
|
356
|
+
font: 12px var(--ui-font); color: var(--faint);
|
|
357
|
+
}
|
|
358
|
+
.hidden-note:hover { color: var(--accent); }
|
|
359
|
+
.hidden-note:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
|
|
360
|
+
.sessions + .hidden-note { border-top: 1px solid var(--line); align-self: stretch; text-align: left; padding-top: 8px; margin-top: 0; }
|
|
361
|
+
.d-noise { margin-top: 10px; }
|
|
362
|
+
.d-noise > summary { cursor: pointer; color: var(--faint); font-size: 12.5px; padding: 4px 0; }
|
|
363
|
+
.d-noise > summary:hover { color: var(--ink); }
|
|
353
364
|
.card .new-btn { font-size: 16px; line-height: 1; padding: 2px 7px; color: var(--faint); }
|
|
354
365
|
.card .new-btn:hover { color: var(--accent); }
|
|
355
366
|
|
|
@@ -1373,6 +1384,13 @@ function liveCard(s) {
|
|
|
1373
1384
|
</div>`;
|
|
1374
1385
|
}
|
|
1375
1386
|
|
|
1387
|
+
// "3 automated runs, 1 empty session" — the rows a card or drawer folds away.
|
|
1388
|
+
function hiddenLabel(h) {
|
|
1389
|
+
const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
|
|
1390
|
+
return [h.automated && plural(h.automated, 'automated run'), h.empty && plural(h.empty, 'empty session')]
|
|
1391
|
+
.filter(Boolean).join(', ');
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1376
1394
|
function projectCard(p) {
|
|
1377
1395
|
const sessions = p.sessions.length
|
|
1378
1396
|
? `<ul class="sessions">${p.sessions.map((sess) => `
|
|
@@ -1383,7 +1401,12 @@ function projectCard(p) {
|
|
|
1383
1401
|
<span class="s-when">${relTime(sess.lastActivityAt)}</span>
|
|
1384
1402
|
<span class="acts">${renameBtn(sess.sessionId, sess.title)}${openBtn(sess.sessionId)}${copyBtn(sess.resumeCommand)}</span>
|
|
1385
1403
|
</li>`).join('')}</ul>`
|
|
1386
|
-
: '
|
|
1404
|
+
: '';
|
|
1405
|
+
const hiddenTxt = p.hidden ? hiddenLabel(p.hidden) : '';
|
|
1406
|
+
const hiddenNote = hiddenTxt
|
|
1407
|
+
? `<button class="hidden-note" data-detail="${esc(p.path)}" data-detail-name="${esc(p.name)}" title="One-shot claude -p runs and sessions closed without a prompt. Open the project to see them.">${esc(hiddenTxt)} hidden</button>`
|
|
1408
|
+
: '';
|
|
1409
|
+
const empty = !p.sessions.length && !hiddenTxt ? '<div class="none-yet">No sessions recorded.</div>' : '';
|
|
1387
1410
|
return `<div class="card">
|
|
1388
1411
|
<div class="head">
|
|
1389
1412
|
${p.isLive ? '<span class="live-dot" title="session running"></span>' : ''}
|
|
@@ -1396,7 +1419,7 @@ function projectCard(p) {
|
|
|
1396
1419
|
${costChip(p.spend7d, { cls: 'badge', after: ' in 7d' })}
|
|
1397
1420
|
<span class="when" title="${esc(p.path)}">${relTime(p.lastActivityAt)}</span>
|
|
1398
1421
|
</div>
|
|
1399
|
-
${sessions}
|
|
1422
|
+
${sessions}${empty}${hiddenNote}
|
|
1400
1423
|
</div>`;
|
|
1401
1424
|
}
|
|
1402
1425
|
|
|
@@ -1591,7 +1614,8 @@ function render(state) {
|
|
|
1591
1614
|
const now = Date.now();
|
|
1592
1615
|
const active = [], dormant = [];
|
|
1593
1616
|
for (const p of state.projects || []) {
|
|
1594
|
-
const
|
|
1617
|
+
const noiseOnly = !p.sessions.length && p.hidden && (p.hidden.automated || p.hidden.empty);
|
|
1618
|
+
const isDormant = !p.isLive && (noiseOnly || !p.lastActivityAt || now - p.lastActivityAt > DORMANT_MS);
|
|
1595
1619
|
(isDormant ? dormant : active).push(p);
|
|
1596
1620
|
}
|
|
1597
1621
|
patch($('#grid'), active.length || dormant.length
|
|
@@ -1851,8 +1875,7 @@ function toolsetHtml(t, projectPath) {
|
|
|
1851
1875
|
function renderDetail(d, name) {
|
|
1852
1876
|
$('#d-title').textContent = name;
|
|
1853
1877
|
$('#d-path').textContent = d.path;
|
|
1854
|
-
const
|
|
1855
|
-
? `<ul class="d-sessions">${d.sessions.map((s) => `
|
|
1878
|
+
const sessionRow = (s) => `
|
|
1856
1879
|
<li>
|
|
1857
1880
|
<span class="s-title" data-session="${esc(s.sessionId)}" title="${esc(titleTip(s.model))}">${
|
|
1858
1881
|
s.worktree ? `<span class="wt mono">⎇ ${esc(s.worktree)}</span> ` : ''}${esc(s.title)}</span>${noteMark(s)}
|
|
@@ -1860,7 +1883,16 @@ function renderDetail(d, name) {
|
|
|
1860
1883
|
${costChip(s.estCost, { assumed: s.assumed })}
|
|
1861
1884
|
<span class="s-when">${relTime(s.lastActivityAt)}</span>
|
|
1862
1885
|
<span class="acts">${renameBtn(s.sessionId, s.title)}${openBtn(s.sessionId)}${copyBtn(s.resumeCommand)}</span>
|
|
1863
|
-
</li
|
|
1886
|
+
</li>`;
|
|
1887
|
+
const real = d.sessions.filter((s) => !s.kind);
|
|
1888
|
+
const noise = d.sessions.filter((s) => s.kind);
|
|
1889
|
+
const noiseTxt = hiddenLabel({
|
|
1890
|
+
automated: noise.filter((s) => s.kind === 'automated').length,
|
|
1891
|
+
empty: noise.filter((s) => s.kind === 'empty').length,
|
|
1892
|
+
});
|
|
1893
|
+
const sessions = d.sessions.length
|
|
1894
|
+
? (real.length ? `<ul class="d-sessions">${real.map(sessionRow).join('')}</ul>` : '') +
|
|
1895
|
+
(noise.length ? `<details class="d-noise"><summary>${esc(noiseTxt)}</summary><ul class="d-sessions">${noise.map(sessionRow).join('')}</ul></details>` : '')
|
|
1864
1896
|
: null;
|
|
1865
1897
|
const caps = [...d.skills, ...d.agents.map((a) => ({ ...a, name: `agent: ${a.name}` })),
|
|
1866
1898
|
...d.commands.map((c) => ({ ...c, name: `/${c.name}` }))];
|
|
@@ -1897,7 +1929,7 @@ function renderDetail(d, name) {
|
|
|
1897
1929
|
const tsOff = ts ? KINDS.reduce((n, k) => n + (ts[k] || []).filter((r) => !r.enabled).length, 0) : 0;
|
|
1898
1930
|
$('#d-body').innerHTML =
|
|
1899
1931
|
muteBtn +
|
|
1900
|
-
dSection(`Sessions — ${d.sessions.length}`, sessions, 'No sessions recorded.') +
|
|
1932
|
+
dSection(`Sessions — ${d.sessions.filter((s) => !s.kind).length}`, sessions, 'No sessions recorded.') +
|
|
1901
1933
|
dSection(`What changed — last ${(d.commits || []).length} commits`, commitRows ? commitSummaryLine(d.commitSummary) + `<ul class="d-sessions">${commitRows}</ul>` : null, 'No git history here.') +
|
|
1902
1934
|
dSection('CLAUDE.md', d.claudeMd.length ? dFiles(d.claudeMd, 'name') : null, 'No CLAUDE.md — sessions here start without project instructions.') +
|
|
1903
1935
|
dSection(`Memory — ${d.memory.length} file${d.memory.length === 1 ? '' : 's'}`, d.memory.length ? dFiles(d.memory, 'name') : null, 'No memory yet.') +
|
|
@@ -3147,7 +3179,7 @@ document.addEventListener('click', async (e) => {
|
|
|
3147
3179
|
|
|
3148
3180
|
document.addEventListener('click', (e) => {
|
|
3149
3181
|
const el = e.target.closest('[data-detail]');
|
|
3150
|
-
if (el) openDetail(el.dataset.detail, el.textContent);
|
|
3182
|
+
if (el) openDetail(el.dataset.detail, el.dataset.detailName || el.textContent);
|
|
3151
3183
|
});
|
|
3152
3184
|
$('#d-close').addEventListener('click', closeDetail);
|
|
3153
3185
|
$('#drawer-backdrop').addEventListener('click', closeDetail);
|