claude-mission-control 1.19.0 → 1.21.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 +124 -10
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
|
|
|
@@ -939,6 +950,34 @@
|
|
|
939
950
|
#palette-list li.sel .pl { color: var(--accent); }
|
|
940
951
|
#palette-empty { color: var(--faint); font-size: 12px; padding: 12px 16px; }
|
|
941
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
|
+
|
|
942
981
|
/* welcome card for fresh installs */
|
|
943
982
|
#welcome {
|
|
944
983
|
grid-column: 1 / -1; background: var(--surface); border: 1px solid var(--line);
|
|
@@ -965,7 +1004,7 @@
|
|
|
965
1004
|
header .spacer, #open-pref-wrap { display: none; } /* opening terminals is a desk thing; still in settings */
|
|
966
1005
|
#quota { order: 10; flex: 1 1 100%; flex-wrap: wrap; gap: 8px 14px; }
|
|
967
1006
|
.meter .bar { width: 52px; }
|
|
968
|
-
#live { padding: 12px 16px; }
|
|
1007
|
+
#live { padding: 12px 16px; position: static; } /* a sticky rail would cover most of a phone screen */
|
|
969
1008
|
main { padding: 14px 16px; }
|
|
970
1009
|
.dep-row { grid-template-columns: 1fr; padding: 10px 12px; }
|
|
971
1010
|
.dep-proj { font-size: 15px; }
|
|
@@ -1020,6 +1059,7 @@
|
|
|
1020
1059
|
|
|
1021
1060
|
<section id="live"></section>
|
|
1022
1061
|
<main>
|
|
1062
|
+
<section id="guide" hidden aria-label="How to read this board"></section>
|
|
1023
1063
|
<div id="pinned" hidden></div>
|
|
1024
1064
|
<div id="git-attn" hidden></div>
|
|
1025
1065
|
<details id="digest" open>
|
|
@@ -1373,6 +1413,13 @@ function liveCard(s) {
|
|
|
1373
1413
|
</div>`;
|
|
1374
1414
|
}
|
|
1375
1415
|
|
|
1416
|
+
// "3 automated runs, 1 empty session" — the rows a card or drawer folds away.
|
|
1417
|
+
function hiddenLabel(h) {
|
|
1418
|
+
const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
|
|
1419
|
+
return [h.automated && plural(h.automated, 'automated run'), h.empty && plural(h.empty, 'empty session')]
|
|
1420
|
+
.filter(Boolean).join(', ');
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1376
1423
|
function projectCard(p) {
|
|
1377
1424
|
const sessions = p.sessions.length
|
|
1378
1425
|
? `<ul class="sessions">${p.sessions.map((sess) => `
|
|
@@ -1383,7 +1430,12 @@ function projectCard(p) {
|
|
|
1383
1430
|
<span class="s-when">${relTime(sess.lastActivityAt)}</span>
|
|
1384
1431
|
<span class="acts">${renameBtn(sess.sessionId, sess.title)}${openBtn(sess.sessionId)}${copyBtn(sess.resumeCommand)}</span>
|
|
1385
1432
|
</li>`).join('')}</ul>`
|
|
1386
|
-
: '
|
|
1433
|
+
: '';
|
|
1434
|
+
const hiddenTxt = p.hidden ? hiddenLabel(p.hidden) : '';
|
|
1435
|
+
const hiddenNote = hiddenTxt
|
|
1436
|
+
? `<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>`
|
|
1437
|
+
: '';
|
|
1438
|
+
const empty = !p.sessions.length && !hiddenTxt ? '<div class="none-yet">No sessions recorded.</div>' : '';
|
|
1387
1439
|
return `<div class="card">
|
|
1388
1440
|
<div class="head">
|
|
1389
1441
|
${p.isLive ? '<span class="live-dot" title="session running"></span>' : ''}
|
|
@@ -1396,7 +1448,7 @@ function projectCard(p) {
|
|
|
1396
1448
|
${costChip(p.spend7d, { cls: 'badge', after: ' in 7d' })}
|
|
1397
1449
|
<span class="when" title="${esc(p.path)}">${relTime(p.lastActivityAt)}</span>
|
|
1398
1450
|
</div>
|
|
1399
|
-
${sessions}
|
|
1451
|
+
${sessions}${empty}${hiddenNote}
|
|
1400
1452
|
</div>`;
|
|
1401
1453
|
}
|
|
1402
1454
|
|
|
@@ -1591,7 +1643,8 @@ function render(state) {
|
|
|
1591
1643
|
const now = Date.now();
|
|
1592
1644
|
const active = [], dormant = [];
|
|
1593
1645
|
for (const p of state.projects || []) {
|
|
1594
|
-
const
|
|
1646
|
+
const noiseOnly = !p.sessions.length && p.hidden && (p.hidden.automated || p.hidden.empty);
|
|
1647
|
+
const isDormant = !p.isLive && (noiseOnly || !p.lastActivityAt || now - p.lastActivityAt > DORMANT_MS);
|
|
1595
1648
|
(isDormant ? dormant : active).push(p);
|
|
1596
1649
|
}
|
|
1597
1650
|
patch($('#grid'), active.length || dormant.length
|
|
@@ -1599,7 +1652,7 @@ function render(state) {
|
|
|
1599
1652
|
: `<div id="welcome">
|
|
1600
1653
|
<h2><span class="tilde">❯</span> Welcome to Claude Mission Control</h2>
|
|
1601
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>
|
|
1602
|
-
<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>
|
|
1603
1656
|
</div>`);
|
|
1604
1657
|
const d = $('#dormant');
|
|
1605
1658
|
d.hidden = dormant.length === 0;
|
|
@@ -1851,8 +1904,7 @@ function toolsetHtml(t, projectPath) {
|
|
|
1851
1904
|
function renderDetail(d, name) {
|
|
1852
1905
|
$('#d-title').textContent = name;
|
|
1853
1906
|
$('#d-path').textContent = d.path;
|
|
1854
|
-
const
|
|
1855
|
-
? `<ul class="d-sessions">${d.sessions.map((s) => `
|
|
1907
|
+
const sessionRow = (s) => `
|
|
1856
1908
|
<li>
|
|
1857
1909
|
<span class="s-title" data-session="${esc(s.sessionId)}" title="${esc(titleTip(s.model))}">${
|
|
1858
1910
|
s.worktree ? `<span class="wt mono">⎇ ${esc(s.worktree)}</span> ` : ''}${esc(s.title)}</span>${noteMark(s)}
|
|
@@ -1860,7 +1912,16 @@ function renderDetail(d, name) {
|
|
|
1860
1912
|
${costChip(s.estCost, { assumed: s.assumed })}
|
|
1861
1913
|
<span class="s-when">${relTime(s.lastActivityAt)}</span>
|
|
1862
1914
|
<span class="acts">${renameBtn(s.sessionId, s.title)}${openBtn(s.sessionId)}${copyBtn(s.resumeCommand)}</span>
|
|
1863
|
-
</li
|
|
1915
|
+
</li>`;
|
|
1916
|
+
const real = d.sessions.filter((s) => !s.kind);
|
|
1917
|
+
const noise = d.sessions.filter((s) => s.kind);
|
|
1918
|
+
const noiseTxt = hiddenLabel({
|
|
1919
|
+
automated: noise.filter((s) => s.kind === 'automated').length,
|
|
1920
|
+
empty: noise.filter((s) => s.kind === 'empty').length,
|
|
1921
|
+
});
|
|
1922
|
+
const sessions = d.sessions.length
|
|
1923
|
+
? (real.length ? `<ul class="d-sessions">${real.map(sessionRow).join('')}</ul>` : '') +
|
|
1924
|
+
(noise.length ? `<details class="d-noise"><summary>${esc(noiseTxt)}</summary><ul class="d-sessions">${noise.map(sessionRow).join('')}</ul></details>` : '')
|
|
1864
1925
|
: null;
|
|
1865
1926
|
const caps = [...d.skills, ...d.agents.map((a) => ({ ...a, name: `agent: ${a.name}` })),
|
|
1866
1927
|
...d.commands.map((c) => ({ ...c, name: `/${c.name}` }))];
|
|
@@ -1897,7 +1958,7 @@ function renderDetail(d, name) {
|
|
|
1897
1958
|
const tsOff = ts ? KINDS.reduce((n, k) => n + (ts[k] || []).filter((r) => !r.enabled).length, 0) : 0;
|
|
1898
1959
|
$('#d-body').innerHTML =
|
|
1899
1960
|
muteBtn +
|
|
1900
|
-
dSection(`Sessions — ${d.sessions.length}`, sessions, 'No sessions recorded.') +
|
|
1961
|
+
dSection(`Sessions — ${d.sessions.filter((s) => !s.kind).length}`, sessions, 'No sessions recorded.') +
|
|
1901
1962
|
dSection(`What changed — last ${(d.commits || []).length} commits`, commitRows ? commitSummaryLine(d.commitSummary) + `<ul class="d-sessions">${commitRows}</ul>` : null, 'No git history here.') +
|
|
1902
1963
|
dSection('CLAUDE.md', d.claudeMd.length ? dFiles(d.claudeMd, 'name') : null, 'No CLAUDE.md — sessions here start without project instructions.') +
|
|
1903
1964
|
dSection(`Memory — ${d.memory.length} file${d.memory.length === 1 ? '' : 's'}`, d.memory.length ? dFiles(d.memory, 'name') : null, 'No memory yet.') +
|
|
@@ -3147,7 +3208,7 @@ document.addEventListener('click', async (e) => {
|
|
|
3147
3208
|
|
|
3148
3209
|
document.addEventListener('click', (e) => {
|
|
3149
3210
|
const el = e.target.closest('[data-detail]');
|
|
3150
|
-
if (el) openDetail(el.dataset.detail, el.textContent);
|
|
3211
|
+
if (el) openDetail(el.dataset.detail, el.dataset.detailName || el.textContent);
|
|
3151
3212
|
});
|
|
3152
3213
|
$('#d-close').addEventListener('click', closeDetail);
|
|
3153
3214
|
$('#drawer-backdrop').addEventListener('click', closeDetail);
|
|
@@ -3729,6 +3790,7 @@ function paletteItems(q) {
|
|
|
3729
3790
|
const items = [];
|
|
3730
3791
|
const st = lastState || { projects: [], liveSessions: [] };
|
|
3731
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 }); } });
|
|
3732
3794
|
items.push({ k: '▦', label: 'Mission Control', hint: 'every live session at once', run: openMissionControl });
|
|
3733
3795
|
items.push({ k: '⬢', label: 'Hive', hint: 'live sessions as characters · who is winning', run: openHive });
|
|
3734
3796
|
items.push({ k: '★', label: 'Hall of Fame', hint: 'your best sessions by award · share cards', run: () => openFame() });
|
|
@@ -3832,6 +3894,58 @@ document.addEventListener('keydown', (e) => {
|
|
|
3832
3894
|
}
|
|
3833
3895
|
});
|
|
3834
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
|
+
|
|
3835
3949
|
poll();
|
|
3836
3950
|
connect();
|
|
3837
3951
|
setInterval(poll, 60_000);
|