@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.
Files changed (55) hide show
  1. package/dist/src/browser/actions.d.ts.map +1 -1
  2. package/dist/src/browser/actions.js +114 -22
  3. package/dist/src/browser/actions.js.map +1 -1
  4. package/dist/src/browser/browser.d.ts +1 -0
  5. package/dist/src/browser/browser.d.ts.map +1 -1
  6. package/dist/src/browser/browser.js +51 -24
  7. package/dist/src/browser/browser.js.map +1 -1
  8. package/dist/src/browser/cdp.d.ts.map +1 -1
  9. package/dist/src/browser/cdp.js +20 -4
  10. package/dist/src/browser/cdp.js.map +1 -1
  11. package/dist/src/browser/console-log.d.ts +1 -0
  12. package/dist/src/browser/console-log.d.ts.map +1 -1
  13. package/dist/src/browser/console-log.js +19 -3
  14. package/dist/src/browser/console-log.js.map +1 -1
  15. package/dist/src/browser/dialog.js +1 -1
  16. package/dist/src/browser/dialog.js.map +1 -1
  17. package/dist/src/browser/find.d.ts.map +1 -1
  18. package/dist/src/browser/find.js +28 -8
  19. package/dist/src/browser/find.js.map +1 -1
  20. package/dist/src/browser/har.d.ts +1 -0
  21. package/dist/src/browser/har.d.ts.map +1 -1
  22. package/dist/src/browser/har.js +7 -5
  23. package/dist/src/browser/har.js.map +1 -1
  24. package/dist/src/browser/network.d.ts +7 -4
  25. package/dist/src/browser/network.d.ts.map +1 -1
  26. package/dist/src/browser/network.js +59 -22
  27. package/dist/src/browser/network.js.map +1 -1
  28. package/dist/src/browser/profiler.d.ts.map +1 -1
  29. package/dist/src/browser/profiler.js +13 -2
  30. package/dist/src/browser/profiler.js.map +1 -1
  31. package/dist/src/browser/screenshot.d.ts.map +1 -1
  32. package/dist/src/browser/screenshot.js +3 -2
  33. package/dist/src/browser/screenshot.js.map +1 -1
  34. package/dist/src/browser/session.d.ts.map +1 -1
  35. package/dist/src/browser/session.js +49 -12
  36. package/dist/src/browser/session.js.map +1 -1
  37. package/dist/src/browser/snapshot.d.ts.map +1 -1
  38. package/dist/src/browser/snapshot.js +26 -14
  39. package/dist/src/browser/snapshot.js.map +1 -1
  40. package/dist/src/browser/storage.d.ts +1 -0
  41. package/dist/src/browser/storage.d.ts.map +1 -1
  42. package/dist/src/browser/storage.js +3 -0
  43. package/dist/src/browser/storage.js.map +1 -1
  44. package/dist/src/browser/tabs.d.ts +2 -2
  45. package/dist/src/browser/tabs.d.ts.map +1 -1
  46. package/dist/src/browser/tabs.js +8 -5
  47. package/dist/src/browser/tabs.js.map +1 -1
  48. package/dist/src/browser/wait.js +23 -13
  49. package/dist/src/browser/wait.js.map +1 -1
  50. package/dist/src/commands/browse.d.ts.map +1 -1
  51. package/dist/src/commands/browse.js +265 -32
  52. package/dist/src/commands/browse.js.map +1 -1
  53. package/dist/src/ui/dashboard-v2.html +377 -12
  54. package/dist/tsconfig.tsbuildinfo +1 -1
  55. 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
- anomBadge = `<span class="sess-anomaly anom-cost" title="Cost ${((sCost/medianCost).toFixed(1))}× above median">! costly</span>`;
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
- const filesHtml = files.length ? `<div class="sr-files">${files.map(f => `<span class="sr-file-chip">${esc(f)}</span>`).join('')}${(s.filesTouched||[]).length > 5 ? `<span class="sr-file-chip">+${(s.filesTouched||[]).length-5}</span>` : ''}</div>` : '';
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" onclick="copyPrompt(${JSON.stringify(s.lastPrompt || s.id)},event)" title="Copy prompt to clipboard">⎘</button>
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 = (row.querySelector('.sr-meta')?.textContent || '').toLowerCase();
3513
- const tags = (row.querySelector('.sr-tags')?.textContent || '').toLowerCase();
3514
- const match = !lq || prompt.includes(lq) || meta.includes(lq) || tags.includes(lq);
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 if this tag is new globally
4046
- initTags(); buildTagFilterBar(allSessions);
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.previousSibling;addCustomTag('${esc(sessId)}',inp.value,event);inp.value='';})()">Add</button>`;
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="toggleErrDrawer('${esc(sessId)}',event)">✕</button></div>`;
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="toggleErrDrawer('${esc(sessId)}',event)">✕</button></div>
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="toggleErrDrawer('${esc(sessId)}',event)">✕</button></div>`;
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 ────────────────────────────────────────────────