@monoes/monomindcli 1.10.40 → 1.10.41
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/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +114 -22
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/browser.d.ts +1 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +51 -24
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +20 -4
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts +1 -0
- package/dist/src/browser/console-log.d.ts.map +1 -1
- package/dist/src/browser/console-log.js +19 -3
- package/dist/src/browser/console-log.js.map +1 -1
- package/dist/src/browser/dialog.js +1 -1
- package/dist/src/browser/dialog.js.map +1 -1
- package/dist/src/browser/find.d.ts.map +1 -1
- package/dist/src/browser/find.js +28 -8
- package/dist/src/browser/find.js.map +1 -1
- package/dist/src/browser/har.d.ts +1 -0
- package/dist/src/browser/har.d.ts.map +1 -1
- package/dist/src/browser/har.js +7 -5
- package/dist/src/browser/har.js.map +1 -1
- package/dist/src/browser/network.d.ts +7 -4
- package/dist/src/browser/network.d.ts.map +1 -1
- package/dist/src/browser/network.js +59 -22
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/profiler.d.ts.map +1 -1
- package/dist/src/browser/profiler.js +13 -2
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/browser/screenshot.d.ts.map +1 -1
- package/dist/src/browser/screenshot.js +3 -2
- package/dist/src/browser/screenshot.js.map +1 -1
- package/dist/src/browser/session.d.ts.map +1 -1
- package/dist/src/browser/session.js +49 -12
- package/dist/src/browser/session.js.map +1 -1
- package/dist/src/browser/snapshot.d.ts.map +1 -1
- package/dist/src/browser/snapshot.js +26 -14
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/storage.d.ts +1 -0
- package/dist/src/browser/storage.d.ts.map +1 -1
- package/dist/src/browser/storage.js +3 -0
- package/dist/src/browser/storage.js.map +1 -1
- package/dist/src/browser/tabs.d.ts +2 -2
- package/dist/src/browser/tabs.d.ts.map +1 -1
- package/dist/src/browser/tabs.js +8 -5
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/wait.js +23 -13
- 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 +265 -32
- package/dist/src/commands/browse.js.map +1 -1
- package/dist/src/ui/dashboard-v2.html +377 -12
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -778,6 +778,60 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
778
778
|
|
|
779
779
|
/* ── f54: URL param indicator ────────────────────────────── */
|
|
780
780
|
.url-param-active { background:oklch(72% 0.18 75 / 0.08); }
|
|
781
|
+
|
|
782
|
+
/* ── f59: keyboard focus in sessions ────────────────────── */
|
|
783
|
+
.sess-row.kb-focus { outline:1px solid oklch(72% 0.18 75 / 0.4); outline-offset:-1px; }
|
|
784
|
+
|
|
785
|
+
/* ── f55: session timeline ───────────────────────────────── */
|
|
786
|
+
#timeline-panel { display:none; margin-bottom:16px; background:var(--surface); border:1px solid var(--border); border-radius:var(--r); overflow:hidden; }
|
|
787
|
+
#timeline-panel.open { display:block; }
|
|
788
|
+
#timeline-head { padding:8px 12px; font-size:11px; font-weight:600; color:var(--text-lo); letter-spacing:0.06em; text-transform:uppercase; border-bottom:1px solid var(--border); display:flex; align-items:center; gap:8px; }
|
|
789
|
+
#timeline-scroll { overflow-x:auto; padding:10px 12px; }
|
|
790
|
+
.tl-day-row { display:flex; align-items:center; gap:6px; margin-bottom:4px; min-height:18px; }
|
|
791
|
+
.tl-day-lbl { font-size:10px; color:var(--text-xs); width:60px; flex-shrink:0; font-family:var(--mono); }
|
|
792
|
+
.tl-track { flex:1; position:relative; height:14px; min-width:400px; }
|
|
793
|
+
.tl-bar { position:absolute; height:10px; top:2px; border-radius:3px; cursor:pointer; opacity:0.7; transition:opacity 0.15s; }
|
|
794
|
+
.tl-bar:hover { opacity:1; z-index:2; }
|
|
795
|
+
|
|
796
|
+
/* ── f56: daily report card ──────────────────────────────── */
|
|
797
|
+
#report-modal { display:none; position:fixed; inset:0; z-index:200; background:rgba(0,0,0,0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center; }
|
|
798
|
+
#report-modal.open { display:flex; }
|
|
799
|
+
#report-box { background:var(--surface); border:1px solid var(--border); border-radius:10px; width:520px; max-width:90vw; max-height:80vh; display:flex; flex-direction:column; }
|
|
800
|
+
.rp-head { display:flex; align-items:center; gap:8px; padding:14px 16px; border-bottom:1px solid var(--border); }
|
|
801
|
+
.rp-title { font-size:14px; font-weight:600; color:var(--text-hi); flex:1; }
|
|
802
|
+
.rp-copy-btn { font-size:11px; background:none; border:1px solid var(--border); border-radius:var(--r); padding:4px 10px; cursor:pointer; color:var(--text-lo); transition:color 0.1s; }
|
|
803
|
+
.rp-copy-btn:hover { color:var(--text-hi); }
|
|
804
|
+
.rp-close-btn { background:none; border:none; cursor:pointer; color:var(--text-xs); font-size:16px; padding:0 2px; }
|
|
805
|
+
#report-content { flex:1; overflow-y:auto; padding:16px; }
|
|
806
|
+
#report-content pre { font-family:var(--mono); font-size:11px; color:var(--text-mid); white-space:pre-wrap; line-height:1.6; }
|
|
807
|
+
|
|
808
|
+
/* ── f57: file-pivot filter ──────────────────────────────── */
|
|
809
|
+
.sr-file-chip { cursor:pointer; }
|
|
810
|
+
.sr-file-chip:hover { background:oklch(72% 0.18 75 / 0.25); color:oklch(85% 0.18 75); }
|
|
811
|
+
.sr-file-chip.pivot-active { background:oklch(72% 0.18 75 / 0.3); color:oklch(90% 0.18 75); border-color:oklch(72% 0.18 75 / 0.5); }
|
|
812
|
+
#file-pivot-bar { display:none; align-items:center; gap:8px; padding:6px 12px; background:oklch(72% 0.18 75 / 0.08); border:1px solid oklch(72% 0.18 75 / 0.2); border-radius:var(--r); margin-bottom:10px; font-size:11px; }
|
|
813
|
+
#file-pivot-bar.show { display:flex; }
|
|
814
|
+
.fpb-label { color:var(--accent); font-weight:600; }
|
|
815
|
+
.fpb-clear { background:none; border:none; cursor:pointer; color:var(--text-lo); font-size:12px; margin-left:auto; }
|
|
816
|
+
|
|
817
|
+
/* ── f58: model cost donut ───────────────────────────────── */
|
|
818
|
+
#model-donut-panel { display:none; margin-bottom:16px; }
|
|
819
|
+
#model-donut-panel.open { display:block; }
|
|
820
|
+
.donut-wrap { display:flex; align-items:center; gap:16px; padding:10px 0; }
|
|
821
|
+
.donut-svg { flex-shrink:0; }
|
|
822
|
+
.donut-legend { display:flex; flex-direction:column; gap:4px; }
|
|
823
|
+
.donut-item { display:flex; align-items:center; gap:6px; font-size:11px; }
|
|
824
|
+
.donut-swatch { width:8px; height:8px; border-radius:2px; flex-shrink:0; }
|
|
825
|
+
.donut-name { color:var(--text-mid); min-width:80px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
826
|
+
.donut-cost { color:var(--accent); font-family:var(--mono); }
|
|
827
|
+
|
|
828
|
+
/* ── f62: context window pressure gauge ─────────────────── */
|
|
829
|
+
.ctx-pressure-wrap { margin-top:3px; display:flex; align-items:center; gap:5px; }
|
|
830
|
+
.ctx-pressure-bar { flex:1; height:3px; background:var(--surface-hi); border-radius:2px; overflow:hidden; }
|
|
831
|
+
.ctx-pressure-fill { height:100%; border-radius:2px; background:oklch(65% 0.15 150); }
|
|
832
|
+
.ctx-pressure-fill.warn { background:oklch(70% 0.18 80); }
|
|
833
|
+
.ctx-pressure-fill.crit { background:oklch(65% 0.2 25); }
|
|
834
|
+
.ctx-pressure-lbl { font-size:9px; color:var(--text-xs); white-space:nowrap; }
|
|
781
835
|
</style>
|
|
782
836
|
</head>
|
|
783
837
|
<body>
|
|
@@ -1017,6 +1071,9 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1017
1071
|
<button class="lb-toggle" id="btn-export-csv" onclick="exportSessionsCSV()" title="Export sessions as CSV">⬇ CSV</button>
|
|
1018
1072
|
<button class="lb-toggle" id="btn-patterns" onclick="togglePatterns()" title="Prompt word frequency across sessions">⊞ Patterns</button>
|
|
1019
1073
|
<button class="diff-toggle" id="btn-diff" onclick="toggleDiffMode()" title="Compare two sessions">⇄ Compare</button>
|
|
1074
|
+
<button class="lb-toggle" id="btn-timeline" onclick="toggleTimeline()" title="Session timeline (Gantt)">⊟ Timeline</button>
|
|
1075
|
+
<button class="lb-toggle" id="btn-donut" onclick="toggleModelDonut()" title="Model cost donut">◕ Donut</button>
|
|
1076
|
+
<button class="lb-toggle" id="btn-report" onclick="showReportCard()" title="Generate daily/weekly report card">✦ Report</button>
|
|
1020
1077
|
</div>
|
|
1021
1078
|
<div class="pg-sub" id="sess-pg-sub">Recent Claude Code sessions for this project</div>
|
|
1022
1079
|
<div id="diff-panel">
|
|
@@ -1030,6 +1087,9 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1030
1087
|
</tr></thead><tbody id="lb-body"></tbody></table>
|
|
1031
1088
|
</div>
|
|
1032
1089
|
<div id="cost-histogram-panel"></div>
|
|
1090
|
+
<div id="timeline-panel"><div id="timeline-head">Session Timeline <span style="font-weight:400;font-size:10px;color:var(--text-xs)">— each bar = one session, width = duration, color = cost</span></div><div id="timeline-scroll"></div></div>
|
|
1091
|
+
<div id="model-donut-panel"></div>
|
|
1092
|
+
<div id="file-pivot-bar"><span class="fpb-label" id="fpb-label"></span><button class="fpb-clear" onclick="clearFilePivot()">✕ Clear filter</button></div>
|
|
1033
1093
|
<div id="model-mix-panel" style="display:none;margin-bottom:16px">
|
|
1034
1094
|
<div id="model-mix-body"></div>
|
|
1035
1095
|
</div>
|
|
@@ -1144,10 +1204,25 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1144
1204
|
</div><!-- /app -->
|
|
1145
1205
|
<div id="toast-rack"></div>
|
|
1146
1206
|
|
|
1207
|
+
<!-- f56: report card modal -->
|
|
1208
|
+
<div id="report-modal" onclick="if(event.target===this)closeReportCard()">
|
|
1209
|
+
<div id="report-box">
|
|
1210
|
+
<div class="rp-head">
|
|
1211
|
+
<span class="rp-title">Report Card</span>
|
|
1212
|
+
<button class="rp-copy-btn" onclick="copyReportCard()">⎘ Copy</button>
|
|
1213
|
+
<button class="rp-close-btn" onclick="closeReportCard()">✕</button>
|
|
1214
|
+
</div>
|
|
1215
|
+
<div id="report-content"><pre id="report-pre"></pre></div>
|
|
1216
|
+
</div>
|
|
1217
|
+
</div>
|
|
1218
|
+
|
|
1147
1219
|
<!-- shortcut help modal -->
|
|
1148
1220
|
<div id="shortcut-modal" onclick="if(event.target===this)closeShortcutHelp()">
|
|
1149
1221
|
<div id="shortcut-box">
|
|
1150
1222
|
<div class="sk-title">Keyboard Shortcuts <button class="sk-close" onclick="closeShortcutHelp()">✕</button></div>
|
|
1223
|
+
<div class="sk-section">Sessions view</div>
|
|
1224
|
+
<div class="sk-row"><span class="sk-desc">Navigate rows</span><span class="sk-keys"><kbd>J</kbd><kbd>K</kbd></span></div>
|
|
1225
|
+
<div class="sk-row"><span class="sk-desc">Open focused session</span><span class="sk-keys"><kbd>↵</kbd></span></div>
|
|
1151
1226
|
<div class="sk-section">Feed (Now view)</div>
|
|
1152
1227
|
<div class="sk-row"><span class="sk-desc">Navigate entries</span><span class="sk-keys"><kbd>J</kbd><kbd>K</kbd></span></div>
|
|
1153
1228
|
<div class="sk-row"><span class="sk-desc">Open detail drawer</span><span class="sk-keys"><kbd>↵</kbd></span></div>
|
|
@@ -1874,6 +1949,8 @@ async function renderSessions() {
|
|
|
1874
1949
|
const t = s.lastTs || s.mtime; if (!t) return false;
|
|
1875
1950
|
return new Date(typeof t === 'number' ? t : t).toDateString() === heatmapDateFilter;
|
|
1876
1951
|
});
|
|
1952
|
+
// f57: file pivot filter
|
|
1953
|
+
if (filePivot) toShow = toShow.filter(s => (s.filesTouched || []).includes(filePivot));
|
|
1877
1954
|
if (!toShow.length) {
|
|
1878
1955
|
el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
|
|
1879
1956
|
buildSessionHeatmap(sessions);
|
|
@@ -1909,7 +1986,8 @@ async function renderSessions() {
|
|
|
1909
1986
|
const sCost = s.totalCost || 0;
|
|
1910
1987
|
let anomBadge = '';
|
|
1911
1988
|
if (medianCost > 0.05 && sCost > medianCost * 3 && sCost > 0.5) {
|
|
1912
|
-
|
|
1989
|
+
// f60: anomaly badge is clickable for explainer
|
|
1990
|
+
anomBadge = `<span class="sess-anomaly anom-cost clickable" style="cursor:pointer" onclick="showCostExplainer('${esc(s.id)}',event)" title="Cost ${((sCost/medianCost).toFixed(1))}× above median — click for details">! costly</span>`;
|
|
1913
1991
|
} else if (s.toolCalls > 0 && (s.errorCount || 0) / s.toolCalls > 0.3) {
|
|
1914
1992
|
anomBadge = `<span class="sess-anomaly anom-err" title="${s.errorCount} tool errors">! errors</span>`;
|
|
1915
1993
|
}
|
|
@@ -1925,7 +2003,16 @@ async function renderSessions() {
|
|
|
1925
2003
|
const note = getSessNote(s.id);
|
|
1926
2004
|
const hasNote = !!note;
|
|
1927
2005
|
const files = (s.filesTouched || []).slice(0, 5);
|
|
1928
|
-
|
|
2006
|
+
// f57: file chips are clickable for pivot filter — use data-attr to avoid JSON.stringify in onclick
|
|
2007
|
+
const filesHtml = files.length ? `<div class="sr-files">${files.map(f => `<span class="sr-file-chip${filePivot===f?' pivot-active':''}" data-fname="${esc(f)}" onclick="setFilePivot(this.dataset.fname,event)" title="Filter by ${esc(f)}">${esc(f)}</span>`).join('')}${(s.filesTouched||[]).length > 5 ? `<span class="sr-file-chip">+${(s.filesTouched||[]).length-5}</span>` : ''}</div>` : '';
|
|
2008
|
+
// f62: context window pressure gauge
|
|
2009
|
+
const CTX_LIMIT = 200000;
|
|
2010
|
+
const tokPct = s.totalInputTokens ? Math.min(100, Math.round(s.totalInputTokens / CTX_LIMIT * 100)) : 0;
|
|
2011
|
+
const tokCls = tokPct > 80 ? 'crit' : tokPct > 50 ? 'warn' : '';
|
|
2012
|
+
const ctxGauge = tokPct > 5 ? `<div class="ctx-pressure-wrap" title="${(s.totalInputTokens||0).toLocaleString()} input tokens — ${tokPct}% of 200k context">
|
|
2013
|
+
<div class="ctx-pressure-bar"><div class="ctx-pressure-fill ${tokCls}" style="width:${tokPct}%"></div></div>
|
|
2014
|
+
<span class="ctx-pressure-lbl">${tokPct}% ctx</span>
|
|
2015
|
+
</div>` : '';
|
|
1929
2016
|
// f51: error badge — clickable if errors exist
|
|
1930
2017
|
const errBadge = (s.errorCount > 0 && s.toolCalls > 0 && (s.errorCount / s.toolCalls) > 0.3)
|
|
1931
2018
|
? `<span class="sess-anomaly anom-err clickable" onclick="toggleErrDrawer('${esc(s.id)}',event)" title="Click to see ${s.errorCount} tool errors">${s.errorCount} err</span>`
|
|
@@ -1937,7 +2024,7 @@ async function renderSessions() {
|
|
|
1937
2024
|
<div class="sr-top">
|
|
1938
2025
|
<div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
|
|
1939
2026
|
<div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>
|
|
1940
|
-
<button class="sr-copy-btn"
|
|
2027
|
+
<button class="sr-copy-btn" data-prompt="${esc(s.lastPrompt || s.id)}" onclick="copyPrompt(this.dataset.prompt,event)" title="Copy prompt to clipboard">⎘</button>
|
|
1941
2028
|
<button class="sess-star${isStarred ? ' on' : ''}" data-sid="${esc(s.id)}" onclick="toggleBookmark('${esc(s.id)}',event)" title="${isStarred ? 'Remove bookmark' : 'Bookmark session'}">${isStarred ? '★' : '☆'}</button>
|
|
1942
2029
|
<span class="sr-view">→ view</span>
|
|
1943
2030
|
</div>
|
|
@@ -1946,6 +2033,7 @@ async function renderSessions() {
|
|
|
1946
2033
|
${ctagsHtml}
|
|
1947
2034
|
${filesHtml}
|
|
1948
2035
|
${satBar}
|
|
2036
|
+
${ctxGauge}
|
|
1949
2037
|
<div class="err-drawer" id="err-drawer-${esc(s.id)}"></div>
|
|
1950
2038
|
<div class="sess-notes-wrap" onclick="event.stopPropagation()">
|
|
1951
2039
|
<button class="sess-notes-toggle${hasNote ? ' has-note' : ''}" onclick="toggleSessNote('${esc(s.id)}',this)">✎ ${hasNote ? 'Note' : 'Add note'}</button>
|
|
@@ -3500,18 +3588,48 @@ document.addEventListener('keydown', e => {
|
|
|
3500
3588
|
if (sel) openDetail(sel.dataset.ev);
|
|
3501
3589
|
}
|
|
3502
3590
|
}
|
|
3591
|
+
|
|
3592
|
+
// ── f59: J/K navigation in sessions list ─────────────────
|
|
3593
|
+
if (currentView === 'sessions') {
|
|
3594
|
+
if (e.key === 'j' || e.key === 'k') {
|
|
3595
|
+
e.preventDefault();
|
|
3596
|
+
const rows = [...document.querySelectorAll('#sess-content .sess-row:not([style*="display: none"])')];
|
|
3597
|
+
if (!rows.length) return;
|
|
3598
|
+
let cur = rows.findIndex(r => r.classList.contains('kb-focus'));
|
|
3599
|
+
if (e.key === 'j') cur = cur < 0 ? 0 : Math.min(cur + 1, rows.length - 1);
|
|
3600
|
+
else cur = cur < 0 ? 0 : Math.max(cur - 1, 0);
|
|
3601
|
+
rows.forEach((r, i) => r.classList.toggle('kb-focus', i === cur));
|
|
3602
|
+
rows[cur].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
3603
|
+
}
|
|
3604
|
+
if (e.key === 'Enter') {
|
|
3605
|
+
const focused = document.querySelector('#sess-content .sess-row.kb-focus');
|
|
3606
|
+
if (focused) {
|
|
3607
|
+
const sid = focused.dataset.sessId;
|
|
3608
|
+
if (sid) jumpToSession(sid);
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3503
3612
|
});
|
|
3504
3613
|
|
|
3505
3614
|
// ── feature 31: inline session filter ─────────────────────
|
|
3615
|
+
// ── feature 61: extended session search ───────────────────
|
|
3506
3616
|
function filterSessions(q) {
|
|
3507
3617
|
const rows = document.querySelectorAll('#sess-content .sess-row');
|
|
3508
3618
|
const lq = q.toLowerCase().trim();
|
|
3509
3619
|
let visible = 0;
|
|
3510
3620
|
rows.forEach(row => {
|
|
3621
|
+
if (!lq) { row.style.display = ''; visible++; return; }
|
|
3622
|
+
const sessId = row.dataset.sessId || '';
|
|
3623
|
+
// search prompt, meta, tags (rendered text)
|
|
3511
3624
|
const prompt = (row.querySelector('.sr-prompt')?.textContent || '').toLowerCase();
|
|
3512
|
-
const meta
|
|
3513
|
-
const tags
|
|
3514
|
-
const
|
|
3625
|
+
const meta = (row.querySelector('.sr-meta')?.textContent || '').toLowerCase();
|
|
3626
|
+
const tags = (row.querySelector('.sr-tags')?.textContent || '').toLowerCase();
|
|
3627
|
+
const files = (row.querySelector('.sr-files')?.textContent || '').toLowerCase();
|
|
3628
|
+
const ctags = (row.querySelector('.sr-custom-tags')?.textContent || '').toLowerCase();
|
|
3629
|
+
// search summaries from session data
|
|
3630
|
+
const sess = allSessions.find(s => s.id === sessId);
|
|
3631
|
+
const summText = sess ? (sess.summaries || []).map(sm => typeof sm === 'string' ? sm : (sm.summary || sm.text || '')).join(' ').toLowerCase() : '';
|
|
3632
|
+
const match = prompt.includes(lq) || meta.includes(lq) || tags.includes(lq) || files.includes(lq) || ctags.includes(lq) || summText.includes(lq);
|
|
3515
3633
|
row.style.display = match ? '' : 'none';
|
|
3516
3634
|
if (match) visible++;
|
|
3517
3635
|
});
|
|
@@ -3930,6 +4048,235 @@ async function loadProjCosts() {
|
|
|
3930
4048
|
}
|
|
3931
4049
|
}
|
|
3932
4050
|
|
|
4051
|
+
// ── feature 55: session timeline (Gantt) ──────────────────
|
|
4052
|
+
let timelineOpen = false;
|
|
4053
|
+
function toggleTimeline() {
|
|
4054
|
+
timelineOpen = !timelineOpen;
|
|
4055
|
+
document.getElementById('btn-timeline').classList.toggle('on', timelineOpen);
|
|
4056
|
+
const panel = document.getElementById('timeline-panel');
|
|
4057
|
+
panel.classList.toggle('open', timelineOpen);
|
|
4058
|
+
if (timelineOpen) buildGanttTimeline();
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
function buildGanttTimeline() {
|
|
4062
|
+
const el = document.getElementById('timeline-scroll');
|
|
4063
|
+
if (!el || !allSessions.length) return;
|
|
4064
|
+
// Group by date, show last 14 days
|
|
4065
|
+
const now = Date.now(); const DAY = 86400000;
|
|
4066
|
+
const days = {};
|
|
4067
|
+
for (let i = 13; i >= 0; i--) {
|
|
4068
|
+
const d = new Date(now - i * DAY); d.setHours(0,0,0,0);
|
|
4069
|
+
days[d.toDateString()] = [];
|
|
4070
|
+
}
|
|
4071
|
+
for (const s of allSessions) {
|
|
4072
|
+
const t = s.firstTs || s.mtime; if (!t) continue;
|
|
4073
|
+
const d = new Date(typeof t === 'number' ? t : t); d.setHours(0,0,0,0);
|
|
4074
|
+
const key = d.toDateString();
|
|
4075
|
+
if (key in days) days[key].push(s);
|
|
4076
|
+
}
|
|
4077
|
+
// Find day with most sessions to normalize bar widths by session start time within day
|
|
4078
|
+
const allCosts = allSessions.map(s => s.totalCost || 0);
|
|
4079
|
+
const maxCost = Math.max(0.01, ...allCosts);
|
|
4080
|
+
|
|
4081
|
+
const DONUT_COLORS = ['oklch(72% 0.18 75)','oklch(65% 0.15 150)','oklch(65% 0.18 220)','oklch(65% 0.2 300)','oklch(65% 0.2 25)'];
|
|
4082
|
+
|
|
4083
|
+
let html = '';
|
|
4084
|
+
for (const [dateStr, sessions] of Object.entries(days)) {
|
|
4085
|
+
const d = new Date(dateStr);
|
|
4086
|
+
const lbl = d.toLocaleDateString(undefined, { month:'short', day:'numeric' });
|
|
4087
|
+
let track = '';
|
|
4088
|
+
// Sort sessions by start time
|
|
4089
|
+
const sorted = [...sessions].sort((a, b) => {
|
|
4090
|
+
const ta = a.firstTs || a.mtime || 0; const tb = b.firstTs || b.mtime || 0;
|
|
4091
|
+
return (typeof ta === 'number' ? ta : new Date(ta).getTime()) - (typeof tb === 'number' ? tb : new Date(tb).getTime());
|
|
4092
|
+
});
|
|
4093
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
4094
|
+
const s = sorted[i];
|
|
4095
|
+
const startTs = typeof (s.firstTs || s.mtime) === 'number' ? (s.firstTs || s.mtime) : new Date(s.firstTs || s.mtime).getTime();
|
|
4096
|
+
const dayStart = new Date(dateStr).getTime();
|
|
4097
|
+
const startPct = Math.min(95, ((startTs - dayStart) / DAY) * 100);
|
|
4098
|
+
const durPct = s.totalDurationMs ? Math.max(0.5, Math.min(15, (s.totalDurationMs / DAY) * 100)) : 1;
|
|
4099
|
+
const costRatio = Math.max(0.15, (s.totalCost || 0.001) / maxCost);
|
|
4100
|
+
const opacity = 0.3 + costRatio * 0.7;
|
|
4101
|
+
const color = DONUT_COLORS[i % DONUT_COLORS.length];
|
|
4102
|
+
const tip = `${s.lastPrompt ? s.lastPrompt.slice(0,60) : s.id} | $${(s.totalCost||0).toFixed(3)} | ${fmtDur(s.totalDurationMs||0)}`;
|
|
4103
|
+
track += `<div class="tl-bar" style="left:${startPct}%;width:${durPct}%;background:${color};opacity:${opacity}" title="${esc(tip)}" onclick="jumpToSession('${esc(s.id)}')"></div>`;
|
|
4104
|
+
}
|
|
4105
|
+
html += `<div class="tl-day-row"><div class="tl-day-lbl">${lbl}</div><div class="tl-track">${track}</div></div>`;
|
|
4106
|
+
}
|
|
4107
|
+
el.innerHTML = html || '<div class="loading-txt">No sessions in last 14 days</div>';
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
// ── feature 56: daily report card ─────────────────────────
|
|
4111
|
+
function showReportCard() {
|
|
4112
|
+
const modal = document.getElementById('report-modal');
|
|
4113
|
+
const pre = document.getElementById('report-pre');
|
|
4114
|
+
if (!modal || !pre) return;
|
|
4115
|
+
const now = new Date();
|
|
4116
|
+
const todayStr = now.toDateString();
|
|
4117
|
+
const weekMs = 7 * 86400000;
|
|
4118
|
+
const weekAgo = Date.now() - weekMs;
|
|
4119
|
+
|
|
4120
|
+
const todaySess = allSessions.filter(s => {
|
|
4121
|
+
const t = s.firstTs || s.mtime; if (!t) return false;
|
|
4122
|
+
return new Date(typeof t === 'number' ? t : t).toDateString() === todayStr;
|
|
4123
|
+
});
|
|
4124
|
+
const weekSess = allSessions.filter(s => {
|
|
4125
|
+
const t = s.firstTs || s.mtime; if (!t) return false;
|
|
4126
|
+
return (typeof t === 'number' ? t : new Date(t).getTime()) >= weekAgo;
|
|
4127
|
+
});
|
|
4128
|
+
|
|
4129
|
+
function summarize(sessions, label) {
|
|
4130
|
+
if (!sessions.length) return `${label}: no sessions\n`;
|
|
4131
|
+
const totalCost = sessions.reduce((a, s) => a + (s.totalCost||0), 0);
|
|
4132
|
+
const totalDur = sessions.reduce((a, s) => a + (s.totalDurationMs||0), 0);
|
|
4133
|
+
const totalTools = sessions.reduce((a, s) => a + (s.toolCalls||0), 0);
|
|
4134
|
+
const totalErrs = sessions.reduce((a, s) => a + (s.errorCount||0), 0);
|
|
4135
|
+
// top files
|
|
4136
|
+
const fileFreq = {};
|
|
4137
|
+
for (const s of sessions) for (const f of s.filesTouched||[]) fileFreq[f] = (fileFreq[f]||0) + 1;
|
|
4138
|
+
const topFiles = Object.entries(fileFreq).sort((a,b)=>b[1]-a[1]).slice(0,5).map(([f,n])=>` - ${f} (×${n})`).join('\n');
|
|
4139
|
+
// anomalies
|
|
4140
|
+
const costsArr = sessions.map(s => s.totalCost||0).filter(c => c > 0).sort((a,b)=>a-b);
|
|
4141
|
+
const median = costsArr.length ? costsArr[Math.floor(costsArr.length/2)] : 0;
|
|
4142
|
+
const anomalies = sessions.filter(s => (s.totalCost||0) > median * 3 && (s.totalCost||0) > 0.5).map(s => ` - ${s.lastPrompt?.slice(0,50)||s.id}: $${(s.totalCost||0).toFixed(2)}`).join('\n');
|
|
4143
|
+
return `### ${label}
|
|
4144
|
+
- Sessions: ${sessions.length}
|
|
4145
|
+
- Total cost: $${totalCost.toFixed(3)}
|
|
4146
|
+
- Total duration: ${fmtDur(totalDur)}
|
|
4147
|
+
- Tool calls: ${totalTools}${totalErrs ? ` (${totalErrs} errors)` : ''}
|
|
4148
|
+
${topFiles ? `\nTop files touched:\n${topFiles}` : ''}${anomalies ? `\n\nCost anomalies:\n${anomalies}` : ''}
|
|
4149
|
+
`;
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
const report = `# Monomind Report Card
|
|
4153
|
+
Generated: ${now.toLocaleString()}
|
|
4154
|
+
Project: ${DIR.split('/').filter(Boolean).pop() || DIR}
|
|
4155
|
+
|
|
4156
|
+
${summarize(todaySess, 'Today')}
|
|
4157
|
+
${summarize(weekSess, 'This Week')}
|
|
4158
|
+
---
|
|
4159
|
+
Total all-time: ${allSessions.length} sessions | $${allSessions.reduce((a,s)=>a+(s.totalCost||0),0).toFixed(2)}
|
|
4160
|
+
`;
|
|
4161
|
+
pre.textContent = report;
|
|
4162
|
+
modal.classList.add('open');
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
function closeReportCard() {
|
|
4166
|
+
document.getElementById('report-modal').classList.remove('open');
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
function copyReportCard() {
|
|
4170
|
+
const text = document.getElementById('report-pre')?.textContent || '';
|
|
4171
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
4172
|
+
const btn = document.querySelector('.rp-copy-btn');
|
|
4173
|
+
if (btn) { btn.textContent = '✓ Copied'; setTimeout(() => { btn.textContent = '⎘ Copy'; }, 1500); }
|
|
4174
|
+
}).catch(() => {});
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
// ── feature 57: file-pivot cross-filter ───────────────────
|
|
4178
|
+
let filePivot = null;
|
|
4179
|
+
|
|
4180
|
+
function setFilePivot(fname, event) {
|
|
4181
|
+
if (event) event.stopPropagation();
|
|
4182
|
+
filePivot = filePivot === fname ? null : fname;
|
|
4183
|
+
const bar = document.getElementById('file-pivot-bar');
|
|
4184
|
+
const lbl = document.getElementById('fpb-label');
|
|
4185
|
+
if (bar) bar.classList.toggle('show', !!filePivot);
|
|
4186
|
+
if (lbl) lbl.textContent = filePivot ? `Showing sessions that touched: ${filePivot}` : '';
|
|
4187
|
+
viewRendered['sessions'] = false;
|
|
4188
|
+
renderSessions();
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
function clearFilePivot() {
|
|
4192
|
+
filePivot = null;
|
|
4193
|
+
const bar = document.getElementById('file-pivot-bar');
|
|
4194
|
+
if (bar) bar.classList.remove('show');
|
|
4195
|
+
viewRendered['sessions'] = false;
|
|
4196
|
+
renderSessions();
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
// ── feature 58: model cost donut ──────────────────────────
|
|
4200
|
+
let modelDonutOpen = false;
|
|
4201
|
+
const DONUT_PALETTE = ['oklch(72% 0.18 75)','oklch(65% 0.15 150)','oklch(65% 0.18 220)','oklch(65% 0.2 300)','oklch(62% 0.18 25)','oklch(70% 0.12 60)'];
|
|
4202
|
+
|
|
4203
|
+
function toggleModelDonut() {
|
|
4204
|
+
modelDonutOpen = !modelDonutOpen;
|
|
4205
|
+
document.getElementById('btn-donut').classList.toggle('on', modelDonutOpen);
|
|
4206
|
+
const panel = document.getElementById('model-donut-panel');
|
|
4207
|
+
panel.classList.toggle('open', modelDonutOpen);
|
|
4208
|
+
if (modelDonutOpen) buildModelDonut();
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
function buildModelDonut() {
|
|
4212
|
+
const el = document.getElementById('model-donut-panel');
|
|
4213
|
+
if (!el) return;
|
|
4214
|
+
const breakdown = {};
|
|
4215
|
+
for (const s of allSessions) {
|
|
4216
|
+
for (const [model, d] of Object.entries(s.modelBreakdown || {})) {
|
|
4217
|
+
if (!breakdown[model]) breakdown[model] = { calls: 0, cost: 0 };
|
|
4218
|
+
breakdown[model].calls += d.calls || 0;
|
|
4219
|
+
breakdown[model].cost += d.cost || 0;
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
const entries = Object.entries(breakdown).sort((a, b) => b[1].cost - a[1].cost);
|
|
4223
|
+
if (!entries.length) { el.innerHTML = '<div class="loading-txt" style="padding:8px">No model breakdown data</div>'; return; }
|
|
4224
|
+
const totalCost = entries.reduce((a, [, d]) => a + d.cost, 0);
|
|
4225
|
+
|
|
4226
|
+
// Build SVG conic-gradient-style donut using stroke-dasharray
|
|
4227
|
+
const R = 36; const CX = 44; const CY = 44; const CIRCUMFERENCE = 2 * Math.PI * R;
|
|
4228
|
+
let offset = 0;
|
|
4229
|
+
const segments = entries.map(([model, d], i) => {
|
|
4230
|
+
const pct = totalCost > 0 ? d.cost / totalCost : 0;
|
|
4231
|
+
const dash = pct * CIRCUMFERENCE;
|
|
4232
|
+
const seg = `<circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="${DONUT_PALETTE[i % DONUT_PALETTE.length]}" stroke-width="14" stroke-dasharray="${dash} ${CIRCUMFERENCE - dash}" stroke-dashoffset="${-offset}" transform="rotate(-90 ${CX} ${CY})"/>`;
|
|
4233
|
+
offset += dash;
|
|
4234
|
+
return seg;
|
|
4235
|
+
}).join('');
|
|
4236
|
+
const svg = `<svg class="donut-svg" width="88" height="88" viewBox="0 0 88 88">
|
|
4237
|
+
<circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="var(--surface-hi)" stroke-width="14"/>
|
|
4238
|
+
${segments}
|
|
4239
|
+
<text x="${CX}" y="${CY}" text-anchor="middle" dy="0.3em" font-size="9" fill="var(--text-xs)" font-family="var(--mono)">$${totalCost.toFixed(2)}</text>
|
|
4240
|
+
</svg>`;
|
|
4241
|
+
|
|
4242
|
+
const legend = entries.slice(0, 6).map(([model, d], i) => {
|
|
4243
|
+
const short = model.replace(/^claude-/, '').replace(/-\d{8}$/, '');
|
|
4244
|
+
const pct = totalCost > 0 ? Math.round(d.cost / totalCost * 100) : 0;
|
|
4245
|
+
return `<div class="donut-item"><div class="donut-swatch" style="background:${DONUT_PALETTE[i % DONUT_PALETTE.length]}"></div>
|
|
4246
|
+
<span class="donut-name" title="${esc(model)}">${esc(short)}</span>
|
|
4247
|
+
<span class="donut-cost">$${d.cost.toFixed(2)} <span style="color:var(--text-xs)">${pct}%</span></span>
|
|
4248
|
+
</div>`;
|
|
4249
|
+
}).join('');
|
|
4250
|
+
|
|
4251
|
+
el.innerHTML = `<div class="donut-wrap">${svg}<div class="donut-legend">${legend}</div></div>`;
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
// ── feature 60: cost anomaly explainer ────────────────────
|
|
4255
|
+
// Enhance anom-cost badge with onclick that shows explainer panel
|
|
4256
|
+
function showCostExplainer(sessId, event) {
|
|
4257
|
+
if (event) event.stopPropagation();
|
|
4258
|
+
const drawer = document.getElementById('err-drawer-' + sessId);
|
|
4259
|
+
if (!drawer) return;
|
|
4260
|
+
if (drawer.classList.contains('open')) { drawer.classList.remove('open'); drawer.innerHTML = ''; return; }
|
|
4261
|
+
const sess = allSessions.find(s => s.id === sessId);
|
|
4262
|
+
if (!sess) return;
|
|
4263
|
+
drawer.classList.add('open');
|
|
4264
|
+
const costsArr = allSessions.map(s => s.totalCost||0).filter(c=>c>0).sort((a,b)=>a-b);
|
|
4265
|
+
const median = costsArr.length ? costsArr[Math.floor(costsArr.length/2)] : 0;
|
|
4266
|
+
const pct = costsArr.length ? Math.round(costsArr.filter(c=>c<=(sess.totalCost||0)).length/costsArr.length*100) : 0;
|
|
4267
|
+
const ratio = median > 0 ? ((sess.totalCost||0)/median).toFixed(1) : '—';
|
|
4268
|
+
const modelRows = Object.entries(sess.modelBreakdown||{}).sort((a,b)=>b[1].cost-a[1].cost).slice(0,4)
|
|
4269
|
+
.map(([m,d])=>`<div class="err-item">${esc(m.replace(/^claude-/,'').replace(/-\d{8}$/,''))}: $${(d.cost||0).toFixed(4)} · ${d.calls||0} calls</div>`).join('');
|
|
4270
|
+
drawer.innerHTML = `<div class="err-drawer-head" style="color:oklch(70% 0.18 80)">
|
|
4271
|
+
<span>Cost anomaly — $${(sess.totalCost||0).toFixed(3)} (${ratio}× median, top ${100-pct}%)</span>
|
|
4272
|
+
<button class="err-close" onclick="this.closest('.err-drawer').classList.remove('open');this.closest('.err-drawer').innerHTML=''">✕</button>
|
|
4273
|
+
</div>
|
|
4274
|
+
<div class="err-drawer-body">
|
|
4275
|
+
<div class="err-item" style="color:var(--text-lo)">Tool calls: ${sess.toolCalls||0} · Messages: ${sess.totalMessages||0} · Tokens in: ${(sess.totalInputTokens||0).toLocaleString()}</div>
|
|
4276
|
+
${modelRows || '<div class="err-item" style="color:var(--text-xs)">No model breakdown available</div>'}
|
|
4277
|
+
</div>`;
|
|
4278
|
+
}
|
|
4279
|
+
|
|
3933
4280
|
// ── feature 47: 30-day daily cost trend ───────────────────
|
|
3934
4281
|
function buildDailyCostTrend() {
|
|
3935
4282
|
const el = document.getElementById('m-daily-trend');
|
|
@@ -4042,8 +4389,10 @@ function addCustomTag(sessId, tag, event) {
|
|
|
4042
4389
|
if (!tags.includes(t)) { tags.push(t); saveCustomTags(sessId, tags); }
|
|
4043
4390
|
const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
|
|
4044
4391
|
if (wrap) wrap.outerHTML = renderCustomTagsInline(sessId, tags);
|
|
4045
|
-
// rebuild tag filter
|
|
4046
|
-
initTags();
|
|
4392
|
+
// rebuild tag filter bar in the DOM if it exists
|
|
4393
|
+
initTags();
|
|
4394
|
+
const tfBar = document.querySelector('.tag-filter-bar');
|
|
4395
|
+
if (tfBar) tfBar.outerHTML = buildTagFilterBar(allSessions);
|
|
4047
4396
|
}
|
|
4048
4397
|
|
|
4049
4398
|
function removeCustomTag(sessId, tag, event) {
|
|
@@ -4064,7 +4413,7 @@ function showCustomTagInput(sessId, event) {
|
|
|
4064
4413
|
iw.className = 'ctag-input-wrap';
|
|
4065
4414
|
iw.onclick = e => e.stopPropagation();
|
|
4066
4415
|
iw.innerHTML = `<input class="ctag-input" type="text" placeholder="tag name…" maxlength="20" autofocus>
|
|
4067
|
-
<button class="ctag-ok" onclick="(()=>{const inp=this.
|
|
4416
|
+
<button class="ctag-ok" onclick="(()=>{const inp=this.closest('.ctag-input-wrap').querySelector('input');addCustomTag('${esc(sessId)}',inp.value,event);inp.value='';})()">Add</button>`;
|
|
4068
4417
|
wrap.appendChild(iw);
|
|
4069
4418
|
const inp = iw.querySelector('input');
|
|
4070
4419
|
inp.focus();
|
|
@@ -4095,17 +4444,24 @@ async function toggleErrDrawer(sessId, event) {
|
|
|
4095
4444
|
try {
|
|
4096
4445
|
const data = await apiFetch('/api/session-errors?dir=' + enc(DIR) + '&id=' + enc(sessId));
|
|
4097
4446
|
if (!data.errors?.length) {
|
|
4098
|
-
drawer.innerHTML = `<div class="err-drawer-head"><span>No error details found</span><button class="err-close" onclick="
|
|
4447
|
+
drawer.innerHTML = `<div class="err-drawer-head"><span>No error details found</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>`;
|
|
4099
4448
|
return;
|
|
4100
4449
|
}
|
|
4101
4450
|
const items = data.errors.map(e => `<div class="err-item">${esc(e.text)}</div>`).join('');
|
|
4102
|
-
drawer.innerHTML = `<div class="err-drawer-head"><span>${data.errors.length} error${data.errors.length !== 1 ? 's' : ''}</span><button class="err-close" onclick="
|
|
4451
|
+
drawer.innerHTML = `<div class="err-drawer-head"><span>${data.errors.length} error${data.errors.length !== 1 ? 's' : ''}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>
|
|
4103
4452
|
<div class="err-drawer-body">${items}</div>`;
|
|
4104
4453
|
} catch (err) {
|
|
4105
|
-
drawer.innerHTML = `<div class="err-drawer-head"><span>Could not load: ${esc(err.message)}</span><button class="err-close" onclick="
|
|
4454
|
+
drawer.innerHTML = `<div class="err-drawer-head"><span>Could not load: ${esc(err.message)}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>`;
|
|
4106
4455
|
}
|
|
4107
4456
|
}
|
|
4108
4457
|
|
|
4458
|
+
function closeErrDrawer(sessId) {
|
|
4459
|
+
const drawer = document.getElementById('err-drawer-' + sessId);
|
|
4460
|
+
if (!drawer) return;
|
|
4461
|
+
drawer.classList.remove('open');
|
|
4462
|
+
drawer.innerHTML = '';
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4109
4465
|
// ── feature 52: prompt copy button ────────────────────────
|
|
4110
4466
|
function copyPrompt(text, event) {
|
|
4111
4467
|
if (event) event.stopPropagation();
|
|
@@ -4151,6 +4507,7 @@ function syncURLParams() {
|
|
|
4151
4507
|
if (activeTagFilter) p.set('tag', activeTagFilter); else p.delete('tag');
|
|
4152
4508
|
if (heatmapDateFilter) p.set('date', heatmapDateFilter); else p.delete('date');
|
|
4153
4509
|
if (showStarredOnly) p.set('starred', '1'); else p.delete('starred');
|
|
4510
|
+
if (filePivot) p.set('file', filePivot); else p.delete('file');
|
|
4154
4511
|
const newUrl = window.location.pathname + (p.toString() ? '?' + p.toString() : '');
|
|
4155
4512
|
history.replaceState(null, '', newUrl);
|
|
4156
4513
|
}
|
|
@@ -4176,6 +4533,14 @@ function restoreURLParams() {
|
|
|
4176
4533
|
const btn = document.getElementById('sess-star-filter');
|
|
4177
4534
|
if (btn) btn.classList.add('on');
|
|
4178
4535
|
}
|
|
4536
|
+
const file = p.get('file');
|
|
4537
|
+
if (file) {
|
|
4538
|
+
filePivot = file;
|
|
4539
|
+
const bar = document.getElementById('file-pivot-bar');
|
|
4540
|
+
const lbl = document.getElementById('fpb-label');
|
|
4541
|
+
if (bar) bar.classList.add('show');
|
|
4542
|
+
if (lbl) lbl.textContent = `Showing sessions that touched: ${file}`;
|
|
4543
|
+
}
|
|
4179
4544
|
}
|
|
4180
4545
|
|
|
4181
4546
|
// ── helpers ────────────────────────────────────────────────
|