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 +10 -4
- package/lib/transcripts.js +40 -4
- package/package.json +1 -1
- package/public/index.html +84 -2
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
|
-
|
|
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,
|
package/lib/transcripts.js
CHANGED
|
@@ -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,
|
|
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
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
|
|
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);
|