claude-mission-control 1.20.0 → 1.22.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
@@ -621,6 +621,7 @@ class Collector {
621
621
  const sessionDays = [];
622
622
  let sessionTotal = 0;
623
623
  let costTotal = 0;
624
+ const liveIds = new Set(this.raw.live.map((s) => s.sessionId));
624
625
  const dayCut = Date.now() - 24 * 3600000;
625
626
  const timeline = [];
626
627
  const weekDayKey = dayKey(weekCut);
@@ -634,12 +635,14 @@ class Collector {
634
635
  let weekCacheAll = 0;
635
636
  for (const g of this.raw.transcriptGroups.values()) {
636
637
  if (isIgnored(g.path)) continue;
637
- sessionTotal += g.sessions.length;
638
638
  const projModels = {};
639
639
  for (const m of g.sessions) {
640
640
  const mDays = combinedDays(m);
641
641
  sessionDays.push(mDays);
642
- if (m.lastActivityAt >= dayCut) {
642
+ // Automated and empty sessions count toward spend, not toward sessions.
643
+ const real = !sessionKind(m, liveIds.has(m.sessionId));
644
+ if (real) sessionTotal++;
645
+ if (real && m.lastActivityAt >= dayCut) {
643
646
  timeline.push({
644
647
  project: friendlyName(g.path),
645
648
  sessionId: m.sessionId,
@@ -649,7 +652,7 @@ class Collector {
649
652
  });
650
653
  }
651
654
  if (m.lastActivityAt >= weekCut) {
652
- weekSessions++;
655
+ if (real) weekSessions++;
653
656
  if (m.waits) for (let i = 0; i < 4; i++) weekWaits[i] += m.waits[i];
654
657
  let mWeekCost = 0;
655
658
  for (const [day, byM] of Object.entries(mDays)) {
@@ -675,7 +678,7 @@ class Collector {
675
678
  mCost += cost;
676
679
  projModels[model] = (projModels[model] || 0) + cost;
677
680
  }
678
- topCandidates.push({
681
+ if (real) topCandidates.push({
679
682
  sessionId: m.sessionId,
680
683
  title: this.titleOf(m).title,
681
684
  projectName: friendlyName(g.path),
@@ -726,9 +729,12 @@ class Collector {
726
729
  fameSessions() {
727
730
  const pricing = readPricing();
728
731
  const out = [];
732
+ const liveIds = new Set(this.raw.live.map((s) => s.sessionId));
729
733
  for (const g of this.raw.transcriptGroups.values()) {
730
734
  if (isIgnored(g.path)) continue;
731
735
  for (const m of g.sessions) {
736
+ // A one-shot claude -p run that starts as a session ends would claim its commits.
737
+ if (sessionKind(m, liveIds.has(m.sessionId))) continue;
732
738
  const ts = toolSummary(m.tools);
733
739
  out.push({
734
740
  sessionId: m.sessionId,
@@ -154,7 +154,7 @@ async function scanFile(abs, sessionIdFromName) {
154
154
  }
155
155
  if (meta.firstUserPrompt === null) {
156
156
  const text = extractUserText(rec);
157
- if (text) meta.firstUserPrompt = text.slice(0, 120);
157
+ if (text) meta.firstUserPrompt = text.slice(0, 300); // room for tidyPromptTitle to drop a preamble
158
158
  }
159
159
  if (meta.cwd && meta.startedAt && meta.firstUserPrompt) break;
160
160
  }
@@ -655,12 +655,48 @@ function dailyCostSeries(daysList, numDays, today = Date.now(), pricing = null)
655
655
  // Best display title for a session, with its provenance.
656
656
  // `custom` is a user-set name from config.json (sessionNames); it wins over
657
657
  // anything derived from the transcript.
658
+ // A prompt reads badly as a title when it opens with where the work is:
659
+ // "In /Users/x/site/deploy.sh, the rsync flag…" or "In the Foo monorepo at
660
+ // `path` (branch y), the…". The card already names the project, so drop that
661
+ // clause, keep a file name it pointed at, and shorten home paths to ~/.
662
+ const LOCATION_WORDS = /\b(repo|repository|monorepo|project|folder|directory|codebase|theme|plugin|package|workspace)\b/i;
663
+ const homePaths = (t) => t.replace(/\/(?:Users|home)\/[^/\s]+\//g, '~/');
664
+
665
+ function leadingClauseEnd(t) {
666
+ let depth = 0, quote = null;
667
+ for (let i = 3; i < t.length; i++) {
668
+ const c = t[i];
669
+ if (quote) { if (c === quote) quote = null; continue; }
670
+ if (c === '`' || c === '"') quote = c;
671
+ else if ('([{'.includes(c)) depth++;
672
+ else if (')]}'.includes(c)) depth = Math.max(0, depth - 1);
673
+ else if ((c === ',' || c === ':') && depth === 0 && (i + 1 === t.length || t[i + 1] === ' ')) return i;
674
+ }
675
+ return -1;
676
+ }
677
+
678
+ function tidyPromptTitle(text) {
679
+ const t = String(text || '');
680
+ if (/^In /.test(t)) {
681
+ const end = leadingClauseEnd(t);
682
+ const clause = end > 0 ? t.slice(3, end) : '';
683
+ if (clause && (clause.includes('/') || LOCATION_WORDS.test(clause))) {
684
+ const rest = t.slice(end + 1).trim();
685
+ if (rest.length >= 8) {
686
+ const file = /\/([\w.-]+\.[a-z][a-z0-9]{0,5})(?=[\s`"'),]|$)/i.exec(clause);
687
+ return homePaths(file ? `${file[1]}: ${rest}` : rest);
688
+ }
689
+ }
690
+ }
691
+ return homePaths(t);
692
+ }
693
+
658
694
  function sessionTitle(meta, customNames) {
659
695
  const custom = customNames && customNames[meta.sessionId];
660
696
  if (custom) return { title: custom, source: 'custom' };
661
697
  if (meta.aiTitle) return { title: meta.aiTitle, source: 'ai-title' };
662
- if (meta.firstUserPrompt) return { title: meta.firstUserPrompt, source: 'first-prompt' };
663
- if (meta.lastPrompt) return { title: meta.lastPrompt, source: 'last-prompt' };
698
+ if (meta.firstUserPrompt) return { title: tidyPromptTitle(meta.firstUserPrompt).slice(0, 120), source: 'first-prompt' };
699
+ if (meta.lastPrompt) return { title: tidyPromptTitle(meta.lastPrompt).slice(0, 120), source: 'last-prompt' };
664
700
  return { title: meta.sessionId.slice(0, 8), source: 'session-id' };
665
701
  }
666
702
 
@@ -685,7 +721,7 @@ function groupByProject(sessions) {
685
721
  }
686
722
 
687
723
  module.exports = {
688
- scanAllTranscripts, scanFile, groupByProject, sessionTitle, sessionKind, combinedUsage, mergeUsage,
724
+ scanAllTranscripts, scanFile, groupByProject, sessionTitle, tidyPromptTitle, sessionKind, combinedUsage, mergeUsage,
689
725
  scanLine, mergeDays, combinedDays, dailyCostSeries, subagentSummary, usageBreakdown, dayKey, contextSummary, toolSummary,
690
726
  waitingReason, reasonText, shouldReadMeta, TOOL_RING,
691
727
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mission-control",
3
- "version": "1.20.0",
3
+ "version": "1.22.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
@@ -950,6 +950,34 @@
950
950
  #palette-list li.sel .pl { color: var(--accent); }
951
951
  #palette-empty { color: var(--faint); font-size: 12px; padding: 12px 16px; }
952
952
 
953
+ /* ---------- first-run guide ---------- */
954
+ #guide {
955
+ background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
956
+ padding: 16px 18px 14px; margin-bottom: 22px;
957
+ }
958
+ #guide .g-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 12px; }
959
+ #guide .g-head h2 { font-size: 14px; font-weight: 600; }
960
+ #guide .g-head .g-sub { color: var(--faint); font-size: 12.5px; }
961
+ #guide .g-head .spacer { flex: 1; }
962
+ #guide .g-done {
963
+ font: 500 12.5px var(--ui-font); color: var(--amber-ink); background: var(--accent);
964
+ border: none; border-radius: 6px; padding: 5px 12px; cursor: pointer;
965
+ }
966
+ #guide .g-done:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
967
+ #guide .g-grid { display: grid; gap: 12px 24px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
968
+ #guide .g-item { font-size: 13px; color: var(--muted); line-height: 1.5; }
969
+ #guide .g-item b { display: block; color: var(--ink); font-weight: 600; margin-bottom: 4px; }
970
+ #guide .g-lamps { gap: 6px; }
971
+ #guide .g-lamps li { align-items: flex-start; }
972
+ #guide .g-lamps .flap { flex: none; min-width: 84px; justify-content: center; }
973
+ #guide kbd {
974
+ font-size: 11.5px; color: var(--ink); background: var(--surface2);
975
+ border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 4px; padding: 0 5px;
976
+ }
977
+ #guide .g-icons { list-style: none; padding: 0; margin: 0; display: grid; gap: 3px; }
978
+ #guide .g-icons li { display: flex; align-items: center; gap: 8px; }
979
+ #guide .g-icons svg { flex: none; color: var(--ink); }
980
+
953
981
  /* welcome card for fresh installs */
954
982
  #welcome {
955
983
  grid-column: 1 / -1; background: var(--surface); border: 1px solid var(--line);
@@ -976,7 +1004,7 @@
976
1004
  header .spacer, #open-pref-wrap { display: none; } /* opening terminals is a desk thing; still in settings */
977
1005
  #quota { order: 10; flex: 1 1 100%; flex-wrap: wrap; gap: 8px 14px; }
978
1006
  .meter .bar { width: 52px; }
979
- #live { padding: 12px 16px; }
1007
+ #live { padding: 12px 16px; position: static; } /* a sticky rail would cover most of a phone screen */
980
1008
  main { padding: 14px 16px; }
981
1009
  .dep-row { grid-template-columns: 1fr; padding: 10px 12px; }
982
1010
  .dep-proj { font-size: 15px; }
@@ -1031,6 +1059,7 @@
1031
1059
 
1032
1060
  <section id="live"></section>
1033
1061
  <main>
1062
+ <section id="guide" hidden aria-label="How to read this board"></section>
1034
1063
  <div id="pinned" hidden></div>
1035
1064
  <div id="git-attn" hidden></div>
1036
1065
  <details id="digest" open>
@@ -1623,7 +1652,7 @@ function render(state) {
1623
1652
  : `<div id="welcome">
1624
1653
  <h2><span class="tilde">❯</span> Welcome to Claude Mission Control</h2>
1625
1654
  <p>No Claude Code projects found yet. Run <code>claude</code> in any folder and it appears here automatically — live status, transcripts, git state, costs, the lot.</p>
1626
- <p>Tip: press <code>⌘K</code> to jump anywhere, <code>/</code> to search, and click ⚙ for settings.</p>
1655
+ <p>Tip: press <code>⌘K</code> to jump anywhere, <code>/</code> to search, and <code>?</code> for a quick guide.</p>
1627
1656
  </div>`);
1628
1657
  const d = $('#dormant');
1629
1658
  d.hidden = dormant.length === 0;
@@ -3761,6 +3790,7 @@ function paletteItems(q) {
3761
3790
  const items = [];
3762
3791
  const st = lastState || { projects: [], liveSessions: [] };
3763
3792
  items.push({ k: '⚙', label: 'Settings', hint: 'rename, hide, terminal, notifications', run: openSettings });
3793
+ items.push({ k: '?', label: 'Show the guide', hint: 'what the lamps, rows and buttons do', run: () => { showGuide(true, true); window.scrollTo({ top: 0 }); } });
3764
3794
  items.push({ k: '▦', label: 'Mission Control', hint: 'every live session at once', run: openMissionControl });
3765
3795
  items.push({ k: '⬢', label: 'Hive', hint: 'live sessions as characters · who is winning', run: openHive });
3766
3796
  items.push({ k: '★', label: 'Hall of Fame', hint: 'your best sessions by award · share cards', run: () => openFame() });
@@ -3864,6 +3894,58 @@ document.addEventListener('keydown', (e) => {
3864
3894
  }
3865
3895
  });
3866
3896
 
3897
+ // ---------- first-run guide: shown until dismissed, back with ? or the palette ----------
3898
+ const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
3899
+ function guideSeen() { try { return localStorage.getItem('guideSeen') === '1'; } catch { return true; } }
3900
+ function renderGuide() {
3901
+ const icon = (id) => $(`#${id} svg`)?.outerHTML || '';
3902
+ const mod = isMac ? '⌘' : 'Ctrl';
3903
+ return `<div class="g-head">
3904
+ <h2>How to read this board</h2>
3905
+ <span class="g-sub">Press <kbd>?</kbd> to bring this back.</span>
3906
+ <span class="spacer"></span>
3907
+ <button class="g-done" id="guide-done">Got it</button>
3908
+ </div>
3909
+ <div class="g-grid">
3910
+ <div class="g-item"><b>Status lamps</b>
3911
+ <ul class="g-icons g-lamps">
3912
+ <li><span class="flap st st-waiting">needs you</span> Claude is waiting on you. What it's asking shows under the title.</li>
3913
+ <li><span class="flap st st-busy">busy</span> Claude is working.</li>
3914
+ </ul></div>
3915
+ <div class="g-item"><b>Sessions</b>
3916
+ Click a session title to read its transcript, live if it's running. Hover a row to rename it, open it in your terminal, or copy its resume command.</div>
3917
+ <div class="g-item"><b>Find anything</b>
3918
+ <kbd>${mod}</kbd> <kbd>K</kbd> jumps to any project, session or view. <kbd>/</kbd> searches inside every transcript. Click a project name for its details.</div>
3919
+ <div class="g-item"><b>Top-right buttons</b>
3920
+ <ul class="g-icons">
3921
+ <li>${icon('mc-btn')} Mission Control: every live session side by side</li>
3922
+ <li>${icon('hive-btn')} Hive: live sessions ranked, plus Hall of Fame</li>
3923
+ <li>${icon('bell-btn')} Catch up on what happened while you were away</li>
3924
+ <li>${icon('gear')} Settings, themes and your phone view</li>
3925
+ </ul></div>
3926
+ </div>`;
3927
+ }
3928
+ function showGuide(show, focus = false) {
3929
+ const g = $('#guide');
3930
+ if (show) {
3931
+ g.innerHTML = renderGuide();
3932
+ g.hidden = false;
3933
+ if (focus) $('#guide-done').focus({ preventScroll: true });
3934
+ } else g.hidden = true;
3935
+ }
3936
+ document.addEventListener('click', (e) => {
3937
+ if (!e.target.closest('#guide-done')) return;
3938
+ try { localStorage.setItem('guideSeen', '1'); } catch {}
3939
+ showGuide(false);
3940
+ });
3941
+ document.addEventListener('keydown', (e) => {
3942
+ if (e.key !== '?' || e.metaKey || e.ctrlKey || /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName || '')) return;
3943
+ e.preventDefault();
3944
+ showGuide($('#guide').hidden, true);
3945
+ if (!$('#guide').hidden) window.scrollTo({ top: 0 });
3946
+ });
3947
+ if (!guideSeen()) showGuide(true);
3948
+
3867
3949
  poll();
3868
3950
  connect();
3869
3951
  setInterval(poll, 60_000);