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 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
- sessions: sessionMetas.slice(0, SESSIONS_PER_PROJECT).map((m) => {
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,
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mission-control",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "Local dashboard for Claude Code: live sessions, transcripts, costs, git status — all your projects in one place.",
5
5
  "main": "server.js",
6
6
  "bin": {
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
- : '<div class="none-yet">No sessions recorded.</div>';
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 isDormant = !p.isLive && (!p.lastActivityAt || now - p.lastActivityAt > DORMANT_MS);
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 sessions = d.sessions.length
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>`).join('')}</ul>`
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);