@monoes/monomindcli 1.10.33 → 1.10.35
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/.claude/helpers/intelligence.cjs +3 -2
- package/.claude/helpers/session.cjs +2 -1
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +16 -12
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts +1 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +4 -2
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/find.d.ts +1 -0
- package/dist/src/browser/find.d.ts.map +1 -1
- package/dist/src/browser/find.js +12 -0
- package/dist/src/browser/find.js.map +1 -1
- package/dist/src/browser/har.d.ts +26 -0
- package/dist/src/browser/har.d.ts.map +1 -0
- package/dist/src/browser/har.js +130 -0
- package/dist/src/browser/har.js.map +1 -0
- package/dist/src/browser/index.d.ts +5 -0
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +5 -0
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/network.d.ts +15 -0
- package/dist/src/browser/network.d.ts.map +1 -1
- package/dist/src/browser/network.js +54 -0
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/profiler.d.ts +10 -0
- package/dist/src/browser/profiler.d.ts.map +1 -0
- package/dist/src/browser/profiler.js +55 -0
- package/dist/src/browser/profiler.js.map +1 -0
- package/dist/src/browser/record.d.ts +21 -0
- package/dist/src/browser/record.d.ts.map +1 -0
- package/dist/src/browser/record.js +48 -0
- package/dist/src/browser/record.js.map +1 -0
- package/dist/src/browser/snapshot.d.ts.map +1 -1
- package/dist/src/browser/snapshot.js +23 -2
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/trace.d.ts +10 -0
- package/dist/src/browser/trace.d.ts.map +1 -0
- package/dist/src/browser/trace.js +72 -0
- package/dist/src/browser/trace.js.map +1 -0
- package/dist/src/browser/types.d.ts +1 -0
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/browser/vitals.d.ts +15 -0
- package/dist/src/browser/vitals.d.ts.map +1 -0
- package/dist/src/browser/vitals.js +116 -0
- package/dist/src/browser/vitals.js.map +1 -0
- package/dist/src/browser/wait.js +1 -1
- package/dist/src/browser/wait.js.map +1 -1
- package/dist/src/commands/browse.d.ts.map +1 -1
- package/dist/src/commands/browse.js +426 -23
- package/dist/src/commands/browse.js.map +1 -1
- package/dist/src/ui/.monomind/sessions/current.json +1 -1
- package/dist/src/ui/dashboard-v2.html +245 -40
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -434,7 +434,8 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
434
434
|
#app.ambient #metrics-pane,
|
|
435
435
|
#app.ambient #replay-bar,
|
|
436
436
|
#app.ambient #feed-recap,
|
|
437
|
-
#app.ambient #feed-timeline
|
|
437
|
+
#app.ambient #feed-timeline,
|
|
438
|
+
#app.ambient #digest-card { display:none !important; }
|
|
438
439
|
#app.ambient #main { background:var(--bg); }
|
|
439
440
|
#app.ambient #view-now { height:100vh; }
|
|
440
441
|
#app.ambient #feed-pane { border:none; }
|
|
@@ -504,6 +505,53 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
504
505
|
.sess-row.diff-sel-a { background:oklch(60% 0.12 220 / 0.08); outline:1px solid oklch(60% 0.12 220 / 0.3); }
|
|
505
506
|
.sess-row.diff-sel-b { background:oklch(72% 0.18 75 / 0.06); outline:1px solid oklch(72% 0.18 75 / 0.3); }
|
|
506
507
|
|
|
508
|
+
/* ── burn rate gauge ─────────────────────────────────────── */
|
|
509
|
+
.burn-gauge-wrap { margin-top:6px; }
|
|
510
|
+
.burn-gauge-row { display:flex; align-items:center; gap:8px; margin-bottom:4px; }
|
|
511
|
+
.burn-gauge-label { font-size:10px; color:var(--text-lo); width:60px; flex-shrink:0; }
|
|
512
|
+
.burn-gauge-track { flex:1; height:6px; background:var(--surface-hi); border-radius:3px; overflow:hidden; }
|
|
513
|
+
.burn-gauge-fill { height:100%; border-radius:3px; transition:width 0.4s, background 0.4s; }
|
|
514
|
+
.burn-val { font-size:10px; font-family:var(--mono); color:var(--text-xs); white-space:nowrap; }
|
|
515
|
+
.burn-rate-ok { background:oklch(65% 0.15 150); }
|
|
516
|
+
.burn-rate-warn { background:oklch(72% 0.18 75); }
|
|
517
|
+
.burn-rate-hot { background:oklch(60% 0.18 25); }
|
|
518
|
+
|
|
519
|
+
/* ── swimlane timeline ────────────────────────────────────── */
|
|
520
|
+
#swimlane-wrap { padding:4px 0; }
|
|
521
|
+
.sw-row { display:flex; align-items:center; gap:6px; margin-bottom:3px; height:14px; }
|
|
522
|
+
.sw-lbl { font-size:10px; color:var(--text-xs); width:60px; flex-shrink:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:right; cursor:pointer; }
|
|
523
|
+
.sw-lbl:hover { color:var(--text-lo); }
|
|
524
|
+
.sw-track { flex:1; height:10px; background:var(--surface-hi); border-radius:3px; position:relative; overflow:hidden; }
|
|
525
|
+
.sw-bar { position:absolute; top:0; height:100%; border-radius:2px; opacity:0.8; }
|
|
526
|
+
.sw-gap { position:absolute; top:0; height:100%; background:var(--bg); width:2px; }
|
|
527
|
+
|
|
528
|
+
/* ── focus mode ──────────────────────────────────────────── */
|
|
529
|
+
#feed-pane.focus-mode .feed-entry.k-tool:not(.errored) { display:none; }
|
|
530
|
+
#feed-pane.focus-mode .feed-group { display:none; }
|
|
531
|
+
.focus-btn { font-size:10px; color:var(--text-lo); background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 7px; cursor:pointer; transition:color 0.15s, border-color 0.15s; line-height:1.4; white-space:nowrap; }
|
|
532
|
+
.focus-btn:hover { color:var(--text-hi); }
|
|
533
|
+
.focus-btn.on { color:oklch(60% 0.12 220); border-color:oklch(60% 0.12 220 / 0.5); }
|
|
534
|
+
|
|
535
|
+
/* ── session notes ───────────────────────────────────────── */
|
|
536
|
+
.sess-notes-wrap { margin-top:8px; }
|
|
537
|
+
.sess-notes-toggle { font-size:11px; color:var(--text-xs); background:none; border:none; cursor:pointer; padding:0; display:flex; align-items:center; gap:5px; }
|
|
538
|
+
.sess-notes-toggle:hover { color:var(--text-lo); }
|
|
539
|
+
.sess-notes-toggle.has-note { color:oklch(72% 0.14 220); }
|
|
540
|
+
.sess-notes-area { display:none; margin-top:6px; }
|
|
541
|
+
.sess-notes-area.open { display:block; }
|
|
542
|
+
textarea.sess-note-input { width:100%; background:var(--surface); border:1px solid var(--border); border-radius:var(--r); color:var(--text-mid); font-size:12px; font-family:var(--sans); padding:6px 8px; resize:vertical; min-height:56px; outline:none; transition:border-color 0.1s; }
|
|
543
|
+
textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
544
|
+
.sess-note-saved { font-size:10px; color:var(--text-xs); margin-top:3px; height:14px; }
|
|
545
|
+
|
|
546
|
+
/* ── export button ───────────────────────────────────────── */
|
|
547
|
+
.sess-copy-btn.dl { }
|
|
548
|
+
|
|
549
|
+
/* ── loop run history sparkline ──────────────────────────── */
|
|
550
|
+
.loop-sparkline { display:flex; gap:2px; align-items:flex-end; height:20px; margin-top:4px; }
|
|
551
|
+
.lsp-bar { width:5px; border-radius:2px 2px 0 0; background:var(--accent); opacity:0.6; min-height:3px; }
|
|
552
|
+
.lsp-bar.err { background:var(--red); opacity:0.8; }
|
|
553
|
+
.le-spark { display:flex; align-items:center; gap:8px; }
|
|
554
|
+
|
|
507
555
|
/* ── budget cap ──────────────────────────────────────────── */
|
|
508
556
|
#budget-modal { display:none; position:fixed; inset:0; z-index:200; background:oklch(5% 0 0 / 0.6); align-items:center; justify-content:center; }
|
|
509
557
|
#budget-modal.open { display:flex; }
|
|
@@ -612,6 +660,8 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
612
660
|
<button class="live-tail-btn" id="btn-live-tail" onclick="toggleLiveTail()" title="Toggle live tail (auto-scroll + 5s refresh)">⬤ Tail</button>
|
|
613
661
|
<button class="sess-copy-btn" id="btn-replay" onclick="replayToggle()" title="Replay session event-by-event">⏵ Replay</button>
|
|
614
662
|
<button class="sess-copy-btn" id="btn-copy-sess" onclick="copySession()" title="Copy session as markdown">⎘ Copy</button>
|
|
663
|
+
<button class="sess-copy-btn" id="btn-export-sess" onclick="exportSession()" title="Download session as .md file">⬇ Export</button>
|
|
664
|
+
<button class="focus-btn" id="btn-focus" onclick="toggleFocusMode()" title="Focus mode: show only user messages + errors">⊡ Focus</button>
|
|
615
665
|
<button class="density-btn" id="btn-density" onclick="toggleDensity()" title="Toggle compact view">⊟</button>
|
|
616
666
|
<button class="sess-btn" onclick="toggleFeedSearch()" title="Search in feed (/)">⌕</button>
|
|
617
667
|
<button class="sess-btn" id="btn-prev-sess" onclick="prevSession()" title="Older session">‹</button>
|
|
@@ -688,6 +738,14 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
688
738
|
<div class="m-group-title">Tool Usage</div>
|
|
689
739
|
<div class="loading-txt">—</div>
|
|
690
740
|
</div>
|
|
741
|
+
<div id="m-burn">
|
|
742
|
+
<div class="m-group-title">Burn Rate</div>
|
|
743
|
+
<div class="loading-txt">—</div>
|
|
744
|
+
</div>
|
|
745
|
+
<div id="m-swimlane">
|
|
746
|
+
<div class="m-group-title">Session Lanes</div>
|
|
747
|
+
<div class="loading-txt">—</div>
|
|
748
|
+
</div>
|
|
691
749
|
</div>
|
|
692
750
|
</div>
|
|
693
751
|
|
|
@@ -1082,6 +1140,7 @@ function renderFeedEvents(events, silent) {
|
|
|
1082
1140
|
buildTimeline(filtered);
|
|
1083
1141
|
buildBreakdownByName(filtered);
|
|
1084
1142
|
buildMinimap(filtered);
|
|
1143
|
+
updateBurnRate(filtered);
|
|
1085
1144
|
// session recap card
|
|
1086
1145
|
buildRecap(filtered, allSessions[sessionIdx]);
|
|
1087
1146
|
}
|
|
@@ -1354,7 +1413,7 @@ async function loadLoopMetrics() {
|
|
|
1354
1413
|
const TWO_HOURS = 2 * 3600 * 1000;
|
|
1355
1414
|
const now = Date.now();
|
|
1356
1415
|
alertState.longLoops = loops
|
|
1357
|
-
.filter(l => l.
|
|
1416
|
+
.filter(l => l.status !== 'stopped' && l.status !== 'paused' && l.startedAt && (now - new Date(l.startedAt).getTime()) > TWO_HOURS)
|
|
1358
1417
|
.map(l => (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 30));
|
|
1359
1418
|
updateAlerts();
|
|
1360
1419
|
|
|
@@ -1383,6 +1442,7 @@ function renderMiniSessions(sessions) {
|
|
|
1383
1442
|
<div class="ms-meta">${relTime(s.lastTs || s.mtime)}</div>
|
|
1384
1443
|
</div>`).join('');
|
|
1385
1444
|
document.getElementById('m-sessions').innerHTML = `<div class="m-group-title">Recent Sessions</div>${items}`;
|
|
1445
|
+
buildSwimlane();
|
|
1386
1446
|
}
|
|
1387
1447
|
|
|
1388
1448
|
// ── projects ───────────────────────────────────────────────
|
|
@@ -1465,6 +1525,8 @@ async function renderSessions() {
|
|
|
1465
1525
|
const autoTags = (allTags.sessionTags.get(s.id) || []).map(t => `<span class="sr-autotag">${esc(t)}</span>`).join('');
|
|
1466
1526
|
const isStarred = bookmarks.has(s.id);
|
|
1467
1527
|
const sData = JSON.stringify(s).replace(/'/g, ''');
|
|
1528
|
+
const note = getSessNote(s.id);
|
|
1529
|
+
const hasNote = !!note;
|
|
1468
1530
|
return `<div class="sess-row" data-sess-idx="${idx}" onclick="diffMode ? diffSelectSession(JSON.parse(this.dataset.sessData || '{}'), this) : jumpToSession('${esc(s.id)}')" data-sess-data='${sData}'>
|
|
1469
1531
|
<div class="sr-top">
|
|
1470
1532
|
<div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
|
|
@@ -1474,6 +1536,13 @@ async function renderSessions() {
|
|
|
1474
1536
|
</div>
|
|
1475
1537
|
<div class="sr-meta">${esc(meta)}</div>
|
|
1476
1538
|
${(summaries || autoTags) ? `<div class="sr-tags">${summaries}${autoTags}</div>` : ''}
|
|
1539
|
+
<div class="sess-notes-wrap" onclick="event.stopPropagation()">
|
|
1540
|
+
<button class="sess-notes-toggle${hasNote ? ' has-note' : ''}" onclick="toggleSessNote('${esc(s.id)}',this)">✎ ${hasNote ? 'Note' : 'Add note'}</button>
|
|
1541
|
+
<div class="sess-notes-area" id="snote-${esc(s.id)}">
|
|
1542
|
+
<textarea class="sess-note-input" rows="2" placeholder="Session note…" oninput="saveSessNote('${esc(s.id)}',this.value,this.closest('.sess-notes-wrap').querySelector('.sess-notes-toggle'),this.closest('.sess-row').querySelector('.sess-note-saved'))">${esc(note)}</textarea>
|
|
1543
|
+
<div class="sess-note-saved"></div>
|
|
1544
|
+
</div>
|
|
1545
|
+
</div>
|
|
1477
1546
|
</div>`;
|
|
1478
1547
|
}).join('');
|
|
1479
1548
|
// prepend tag filter bar if there are common tags
|
|
@@ -1856,8 +1925,12 @@ function buildDigest() {
|
|
|
1856
1925
|
const totalMsgs = todaySessions.reduce((a, s) => a + (s.userMessages || 0), 0);
|
|
1857
1926
|
const longestMs = Math.max(...todaySessions.map(s => s.totalDurationMs || 0));
|
|
1858
1927
|
|
|
1859
|
-
//
|
|
1860
|
-
const
|
|
1928
|
+
// top tags from today's sessions using the auto-tag system
|
|
1929
|
+
const tagFreq = {};
|
|
1930
|
+
for (const s of todaySessions) {
|
|
1931
|
+
for (const t of (allTags.sessionTags.get(s.id) || [])) tagFreq[t] = (tagFreq[t] || 0) + 1;
|
|
1932
|
+
}
|
|
1933
|
+
const themes = Object.entries(tagFreq).sort((a,b) => b[1]-a[1]).slice(0, 3).map(e => e[0]);
|
|
1861
1934
|
|
|
1862
1935
|
const stats = [
|
|
1863
1936
|
`${todaySessions.length} session${todaySessions.length > 1 ? 's' : ''}`,
|
|
@@ -1886,7 +1959,8 @@ function buildMinimap(events) {
|
|
|
1886
1959
|
const scroll = document.getElementById('feed-scroll');
|
|
1887
1960
|
if (!track || !thumb || !scroll) return;
|
|
1888
1961
|
|
|
1889
|
-
|
|
1962
|
+
// feed renders newest-first (reversed), so reverse events to match visual order
|
|
1963
|
+
const tools = [...events].reverse().filter(ev => ev.kind === 'tool' || ev.kind === 'user');
|
|
1890
1964
|
if (!tools.length) { track.innerHTML = ''; return; }
|
|
1891
1965
|
|
|
1892
1966
|
const CAT_CLS = { file:'mp-file', bash:'mp-bash', agent:'mp-agent', mcp:'mp-mcp', user:'mp-user' };
|
|
@@ -2052,6 +2126,168 @@ function renderDiff() {
|
|
|
2052
2126
|
cols.innerHTML = colHtml(0) + colHtml(1);
|
|
2053
2127
|
}
|
|
2054
2128
|
|
|
2129
|
+
// ── feature 13: export session as .md file ────────────────
|
|
2130
|
+
async function exportSession() {
|
|
2131
|
+
const btn = document.getElementById('btn-export-sess');
|
|
2132
|
+
const sess = allSessions[sessionIdx];
|
|
2133
|
+
if (!sess?.file) return;
|
|
2134
|
+
try {
|
|
2135
|
+
btn.textContent = '…';
|
|
2136
|
+
const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=300');
|
|
2137
|
+
const events = data.events || [];
|
|
2138
|
+
const lines = [
|
|
2139
|
+
`# Session: ${sess.lastPrompt || sess.id}`,
|
|
2140
|
+
`> ${new Date(sess.lastTs || sess.mtime).toLocaleString()}`,
|
|
2141
|
+
sess.totalCost != null ? `> Cost: $${sess.totalCost.toFixed(2)}` : '',
|
|
2142
|
+
sess.totalDurationMs ? `> Duration: ${fmtDur(sess.totalDurationMs)}` : '',
|
|
2143
|
+
'',
|
|
2144
|
+
].filter(s => s !== null);
|
|
2145
|
+
for (const ev of events) {
|
|
2146
|
+
if (ev.kind === 'user' && ev.text?.trim()) {
|
|
2147
|
+
lines.push(`\n## ${ev.text.trim().slice(0, 80)}`);
|
|
2148
|
+
if (ev.ts) lines.push(`_${new Date(ev.ts).toLocaleTimeString()}_`);
|
|
2149
|
+
} else if (ev.kind === 'tool') {
|
|
2150
|
+
const label = ev.label || ev.name || ev.cat;
|
|
2151
|
+
lines.push(`- \`${ev.name || ev.cat}\`${label ? ': ' + label : ''}${ev._errored ? ' ⚠ error' : ''}`);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
lines.push('', `---`, `_Exported from monomind dashboard_`);
|
|
2155
|
+
const blob = new Blob([lines.join('\n')], { type: 'text/markdown' });
|
|
2156
|
+
const a = document.createElement('a');
|
|
2157
|
+
a.href = URL.createObjectURL(blob);
|
|
2158
|
+
a.download = `session-${sess.id.slice(0, 8)}.md`;
|
|
2159
|
+
a.click();
|
|
2160
|
+
URL.revokeObjectURL(a.href);
|
|
2161
|
+
btn.textContent = '✓ Saved';
|
|
2162
|
+
setTimeout(() => { btn.textContent = '⬇ Export'; }, 1800);
|
|
2163
|
+
} catch (err) {
|
|
2164
|
+
btn.textContent = '✕ Error';
|
|
2165
|
+
setTimeout(() => { btn.textContent = '⬇ Export'; }, 1500);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// ── feature 14: focus mode ────────────────────────────────
|
|
2170
|
+
let focusMode = false;
|
|
2171
|
+
function toggleFocusMode() {
|
|
2172
|
+
focusMode = !focusMode;
|
|
2173
|
+
document.getElementById('feed-pane').classList.toggle('focus-mode', focusMode);
|
|
2174
|
+
document.getElementById('btn-focus').classList.toggle('on', focusMode);
|
|
2175
|
+
document.getElementById('btn-focus').title = focusMode ? 'Exit focus mode' : 'Focus mode: user messages + errors only';
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// ── feature 15: session notes ─────────────────────────────
|
|
2179
|
+
const NOTES_KEY = 'mm-sess-notes';
|
|
2180
|
+
function getAllNotes() {
|
|
2181
|
+
try { return JSON.parse(localStorage.getItem(NOTES_KEY) || '{}'); } catch { return {}; }
|
|
2182
|
+
}
|
|
2183
|
+
function getSessNote(id) { return getAllNotes()[id] || ''; }
|
|
2184
|
+
function saveSessNote(id, text, toggleBtn, savedEl) {
|
|
2185
|
+
const all = getAllNotes();
|
|
2186
|
+
if (text.trim()) all[id] = text; else delete all[id];
|
|
2187
|
+
localStorage.setItem(NOTES_KEY, JSON.stringify(all));
|
|
2188
|
+
if (toggleBtn) {
|
|
2189
|
+
toggleBtn.classList.toggle('has-note', !!text.trim());
|
|
2190
|
+
toggleBtn.textContent = '✎ ' + (text.trim() ? 'Note' : 'Add note');
|
|
2191
|
+
}
|
|
2192
|
+
if (savedEl) {
|
|
2193
|
+
savedEl.textContent = 'Saved';
|
|
2194
|
+
clearTimeout(savedEl._t);
|
|
2195
|
+
savedEl._t = setTimeout(() => { savedEl.textContent = ''; }, 1200);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
function toggleSessNote(id, btn) {
|
|
2199
|
+
const area = document.getElementById('snote-' + id);
|
|
2200
|
+
if (!area) return;
|
|
2201
|
+
const open = area.classList.toggle('open');
|
|
2202
|
+
if (open) { const ta = area.querySelector('textarea'); if (ta) { ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); } }
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// ── feature 16: burn rate gauge ───────────────────────────
|
|
2206
|
+
let burnRateEvents = [];
|
|
2207
|
+
let burnRateTimer = null;
|
|
2208
|
+
|
|
2209
|
+
function updateBurnRate(events) {
|
|
2210
|
+
burnRateEvents = events;
|
|
2211
|
+
renderBurnGauge();
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
function renderBurnGauge() {
|
|
2215
|
+
const el = document.getElementById('m-burn');
|
|
2216
|
+
if (!el) return;
|
|
2217
|
+
const tools = burnRateEvents.filter(ev => ev.kind === 'tool' && ev.ts);
|
|
2218
|
+
if (tools.length < 2) {
|
|
2219
|
+
el.innerHTML = '<div class="m-group-title">Burn Rate</div><div class="loading-txt" style="padding:4px 0">—</div>';
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
const now = Date.now();
|
|
2223
|
+
// calls in last 5 min, 15 min, 60 min
|
|
2224
|
+
const t5 = tools.filter(e => now - new Date(e.ts).getTime() < 300000).length;
|
|
2225
|
+
const t15 = tools.filter(e => now - new Date(e.ts).getTime() < 900000).length;
|
|
2226
|
+
const t60 = tools.filter(e => now - new Date(e.ts).getTime() < 3600000).length;
|
|
2227
|
+
const rate5 = (t5 / 5).toFixed(1); // calls/min
|
|
2228
|
+
const rate15 = (t15 / 15).toFixed(1);
|
|
2229
|
+
const rate60 = (t60 / 60).toFixed(1);
|
|
2230
|
+
const maxRate = 20; // calls/min considered "hot"
|
|
2231
|
+
const pct = v => Math.min(100, Math.round((parseFloat(v) / maxRate) * 100));
|
|
2232
|
+
const cls = v => parseFloat(v) < 4 ? 'burn-rate-ok' : parseFloat(v) < 10 ? 'burn-rate-warn' : 'burn-rate-hot';
|
|
2233
|
+
el.innerHTML = `<div class="m-group-title">Burn Rate <span style="font-size:10px;color:var(--text-xs);font-weight:400">calls/min</span></div>
|
|
2234
|
+
<div class="burn-gauge-wrap">
|
|
2235
|
+
<div class="burn-gauge-row"><div class="burn-gauge-label">5 min</div><div class="burn-gauge-track"><div class="burn-gauge-fill ${cls(rate5)}" style="width:${pct(rate5)}%"></div></div><div class="burn-val">${rate5}</div></div>
|
|
2236
|
+
<div class="burn-gauge-row"><div class="burn-gauge-label">15 min</div><div class="burn-gauge-track"><div class="burn-gauge-fill ${cls(rate15)}" style="width:${pct(rate15)}%"></div></div><div class="burn-val">${rate15}</div></div>
|
|
2237
|
+
<div class="burn-gauge-row"><div class="burn-gauge-label">60 min</div><div class="burn-gauge-track"><div class="burn-gauge-fill ${cls(rate60)}" style="width:${pct(rate60)}%"></div></div><div class="burn-val">${rate60}</div></div>
|
|
2238
|
+
</div>`;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// ── feature 17: session swimlane ──────────────────────────
|
|
2242
|
+
function buildSwimlane() {
|
|
2243
|
+
const el = document.getElementById('m-swimlane');
|
|
2244
|
+
if (!el) return;
|
|
2245
|
+
const recent = allSessions.slice(0, 8);
|
|
2246
|
+
if (!recent.length) { el.innerHTML = '<div class="m-group-title">Session Lanes</div><div class="loading-txt" style="padding:4px 0">—</div>'; return; }
|
|
2247
|
+
const now = Date.now();
|
|
2248
|
+
const DAY = 86400000;
|
|
2249
|
+
const windowMs = DAY; // 24-hour window
|
|
2250
|
+
const windowStart = now - windowMs;
|
|
2251
|
+
// hues spread across sessions for visual distinction
|
|
2252
|
+
const LANE_HUES = [75, 200, 300, 150, 25, 220, 340, 120];
|
|
2253
|
+
const rows = recent.map((s, si) => {
|
|
2254
|
+
const start = s.firstTs || s.startTs || s.mtime || now;
|
|
2255
|
+
const startMs = typeof start === 'number' ? start : new Date(start).getTime();
|
|
2256
|
+
const dur = s.totalDurationMs || 60000;
|
|
2257
|
+
const endMs = startMs + dur;
|
|
2258
|
+
const leftPct = Math.max(0, Math.min(100, ((startMs - windowStart) / windowMs) * 100));
|
|
2259
|
+
const rightPct = Math.max(0, Math.min(100, ((now - endMs) / windowMs) * 100));
|
|
2260
|
+
const widthPct = Math.max(2, 100 - leftPct - rightPct);
|
|
2261
|
+
const hue = LANE_HUES[si % LANE_HUES.length];
|
|
2262
|
+
const color = `oklch(65% 0.14 ${hue})`;
|
|
2263
|
+
const name = (s.lastPrompt || s.id).slice(0, 16);
|
|
2264
|
+
return `<div class="sw-row">
|
|
2265
|
+
<div class="sw-lbl" onclick="jumpToSession('${esc(s.id)}')" title="${esc(s.lastPrompt || s.id)}">${esc(name)}</div>
|
|
2266
|
+
<div class="sw-track">
|
|
2267
|
+
<div class="sw-bar" style="left:${leftPct}%;width:${widthPct}%;background:${color}" title="${esc(name)} · ${fmtDur(dur)}"></div>
|
|
2268
|
+
</div>
|
|
2269
|
+
</div>`;
|
|
2270
|
+
}).join('');
|
|
2271
|
+
el.innerHTML = `<div class="m-group-title">Session Lanes <span style="font-size:10px;color:var(--text-xs);font-weight:400">24h</span></div><div id="swimlane-wrap">${rows}</div>`;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// ── feature 18: loop run history ──────────────────────────
|
|
2275
|
+
function buildLoopSparkline(l) {
|
|
2276
|
+
// Synthesize a run history from currentRep if actual history unavailable
|
|
2277
|
+
const runHistory = l.runHistory || [];
|
|
2278
|
+
if (!runHistory.length && l.currentRep) {
|
|
2279
|
+
for (let i = 0; i < Math.min(l.currentRep, 10); i++) {
|
|
2280
|
+
runHistory.push({ ok: true });
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
if (!runHistory.length) return '';
|
|
2284
|
+
const bars = runHistory.slice(-10).map(r => {
|
|
2285
|
+
const h = r.durationMs ? Math.max(4, Math.min(20, Math.round(r.durationMs / 2000))) : 10;
|
|
2286
|
+
return `<div class="lsp-bar${r.error || r.ok === false ? ' err' : ''}" style="height:${h}px" title="${r.error ? 'error' : 'ok'}${r.durationMs ? ' · ' + fmtDur(r.durationMs) : ''}"></div>`;
|
|
2287
|
+
}).join('');
|
|
2288
|
+
return `<div class="le-spark"><span style="font-size:10px;color:var(--text-xs)">last ${runHistory.slice(-10).length} runs</span><div class="loop-sparkline">${bars}</div></div>`;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2055
2291
|
// ── loops ──────────────────────────────────────────────────
|
|
2056
2292
|
async function renderLoops() {
|
|
2057
2293
|
const el = document.getElementById('loops-content');
|
|
@@ -2065,13 +2301,13 @@ async function renderLoops() {
|
|
|
2065
2301
|
return;
|
|
2066
2302
|
}
|
|
2067
2303
|
el.innerHTML = loops.map((l, idx) => {
|
|
2068
|
-
const running = l.
|
|
2304
|
+
const running = l.status !== 'stopped' && l.status !== 'paused';
|
|
2069
2305
|
const name = l.name || (l.prompt || 'loop').split('--')[0].trim().slice(0, 60);
|
|
2070
2306
|
const interval = l.interval || l.schedule || '';
|
|
2071
2307
|
const fullPrompt = l.prompt || l.command || '';
|
|
2072
2308
|
const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
|
|
2073
|
-
const lastRun = l.
|
|
2074
|
-
const runs = l.
|
|
2309
|
+
const lastRun = l.lastRunAt ? relTime(l.lastRunAt) : (l.startedAt ? relTime(l.startedAt) : '—');
|
|
2310
|
+
const runs = l.currentRep != null ? l.currentRep : '—';
|
|
2075
2311
|
return `<div class="loop-row" onclick="toggleLoop(this)">
|
|
2076
2312
|
<div class="loop-ico">↺</div>
|
|
2077
2313
|
<div class="loop-body">
|
|
@@ -2087,6 +2323,7 @@ async function renderLoops() {
|
|
|
2087
2323
|
<div class="le-row"><div class="le-lbl">Started</div><div class="le-val mono">${esc(startedAt)}</div></div>
|
|
2088
2324
|
<div class="le-row"><div class="le-lbl">Last run</div><div class="le-val">${esc(lastRun)}</div></div>
|
|
2089
2325
|
<div class="le-row"><div class="le-lbl">Run count</div><div class="le-val">${esc(String(runs))}</div></div>
|
|
2326
|
+
${buildLoopSparkline(l)}
|
|
2090
2327
|
</div>`;
|
|
2091
2328
|
}).join('');
|
|
2092
2329
|
} catch (err) {
|
|
@@ -2226,38 +2463,6 @@ function buildTimeline(events) {
|
|
|
2226
2463
|
tl.innerHTML = segs.join('');
|
|
2227
2464
|
}
|
|
2228
2465
|
|
|
2229
|
-
// ── tool breakdown ──────────────────────────────────────────
|
|
2230
|
-
const TB_COLORS = TL_COLORS;
|
|
2231
|
-
const TB_ORDER = ['file','bash','mcp','agent','search','skill','task','mem','other'];
|
|
2232
|
-
|
|
2233
|
-
function buildBreakdown(events) {
|
|
2234
|
-
const counts = {};
|
|
2235
|
-
for (const ev of events) {
|
|
2236
|
-
if (ev.kind !== 'tool') continue;
|
|
2237
|
-
const c = ev.cat || 'other';
|
|
2238
|
-
counts[c] = (counts[c] || 0) + 1;
|
|
2239
|
-
}
|
|
2240
|
-
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
2241
|
-
if (!total) {
|
|
2242
|
-
document.getElementById('m-breakdown').innerHTML =
|
|
2243
|
-
'<div class="m-group-title">Tool Usage</div><div class="loading-txt" style="padding:6px 0">—</div>';
|
|
2244
|
-
return;
|
|
2245
|
-
}
|
|
2246
|
-
const rows = TB_ORDER.filter(c => counts[c])
|
|
2247
|
-
.sort((a, b) => counts[b] - counts[a])
|
|
2248
|
-
.map(c => {
|
|
2249
|
-
const pct = Math.round(counts[c] / total * 100);
|
|
2250
|
-
const color = TB_COLORS[c] || TB_COLORS.other;
|
|
2251
|
-
return `<div class="tb-row">
|
|
2252
|
-
<div class="tb-lbl">${c}</div>
|
|
2253
|
-
<div class="tb-bar-wrap"><div class="tb-bar" style="width:${pct}%;background:${color}"></div></div>
|
|
2254
|
-
<div class="tb-count">${counts[c]}</div>
|
|
2255
|
-
</div>`;
|
|
2256
|
-
}).join('');
|
|
2257
|
-
document.getElementById('m-breakdown').innerHTML =
|
|
2258
|
-
`<div class="m-group-title">Tool Usage</div><div class="m-breakdown">${rows}</div>`;
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
2466
|
// ── error jump ──────────────────────────────────────────────
|
|
2262
2467
|
function jumpToErrors() {
|
|
2263
2468
|
// find first errored feed entry and scroll to it
|