@monoes/monomindcli 1.10.38 → 1.10.40
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/context-persistence-hook.mjs +1 -1
- package/dist/src/browser/batch.d.ts +0 -2
- package/dist/src/browser/batch.d.ts.map +1 -1
- package/dist/src/browser/batch.js +1 -10
- package/dist/src/browser/batch.js.map +1 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +1 -0
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/dialog.d.ts.map +1 -1
- package/dist/src/browser/dialog.js +4 -1
- package/dist/src/browser/dialog.js.map +1 -1
- package/dist/src/browser/network.js +1 -1
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/screenshot.d.ts.map +1 -1
- package/dist/src/browser/screenshot.js +1 -0
- package/dist/src/browser/screenshot.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 +6 -9
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/trace.d.ts.map +1 -1
- package/dist/src/browser/trace.js +10 -4
- package/dist/src/browser/trace.js.map +1 -1
- 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 +38 -5
- package/dist/src/commands/browse.js.map +1 -1
- package/dist/src/ui/.monomind/loops/loop-1779095996252-mdjpp.json +11 -0
- package/dist/src/ui/dashboard-v2.html +880 -19
- package/dist/src/ui/server.mjs +101 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -389,6 +389,69 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
389
389
|
/* ── forecast row ────────────────────────────────────────── */
|
|
390
390
|
.m-val.forecast { color:var(--text-lo); font-size:11px; font-weight:500; }
|
|
391
391
|
|
|
392
|
+
/* ── session date groups ─────────────────────────────────── */
|
|
393
|
+
.sg-section { margin-bottom:4px; }
|
|
394
|
+
.sg-header { display:flex; align-items:center; gap:6px; padding:5px 2px 3px; cursor:pointer; user-select:none; }
|
|
395
|
+
.sg-title { font-size:10px; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-xs); font-weight:600; }
|
|
396
|
+
.sg-count { font-size:10px; color:var(--text-lo); background:var(--surface-hi); border-radius:8px; padding:1px 6px; }
|
|
397
|
+
.sg-toggle { font-size:10px; color:var(--text-xs); margin-left:auto; }
|
|
398
|
+
.sg-header:hover .sg-title { color:var(--text-lo); }
|
|
399
|
+
.sg-body.collapsed { display:none; }
|
|
400
|
+
|
|
401
|
+
/* ── session heatmap calendar ─────────────────────────────── */
|
|
402
|
+
#sess-heatmap { margin-bottom:14px; }
|
|
403
|
+
.shm-grid { display:grid; grid-template-rows:repeat(7,10px); grid-auto-flow:column; grid-auto-columns:10px; gap:2px; margin-top:6px; }
|
|
404
|
+
.shm-cell { border-radius:2px; background:var(--surface-hi); cursor:pointer; transition:outline 0.1s; }
|
|
405
|
+
.shm-cell:hover { outline:1px solid var(--accent); outline-offset:-1px; }
|
|
406
|
+
.shm-cell.shm-1 { background:oklch(72% 0.18 75 / 0.22); }
|
|
407
|
+
.shm-cell.shm-2 { background:oklch(72% 0.18 75 / 0.42); }
|
|
408
|
+
.shm-cell.shm-3 { background:oklch(72% 0.18 75 / 0.65); }
|
|
409
|
+
.shm-cell.shm-4 { background:oklch(72% 0.18 75 / 0.85); }
|
|
410
|
+
.shm-cell.shm-active { outline:2px solid var(--accent); outline-offset:-1px; }
|
|
411
|
+
.shm-label { font-size:10px; color:var(--text-xs); display:flex; align-items:center; justify-content:space-between; }
|
|
412
|
+
#shm-clear { font-size:10px; color:var(--accent); background:none; border:none; cursor:pointer; padding:0; display:none; }
|
|
413
|
+
#shm-clear.show { display:inline; }
|
|
414
|
+
|
|
415
|
+
/* ── cost period toggle ───────────────────────────────────── */
|
|
416
|
+
.period-toggles { display:flex; gap:4px; margin-bottom:10px; flex-wrap:wrap; }
|
|
417
|
+
.period-btn { font-size:11px; padding:3px 10px; border-radius:8px; border:1px solid var(--border); background:transparent; color:var(--text-lo); cursor:pointer; font-family:var(--sans); transition:color 0.1s, background 0.1s; }
|
|
418
|
+
.period-btn:hover { color:var(--text-hi); }
|
|
419
|
+
.period-btn.active { background:var(--accent-dim); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
|
|
420
|
+
|
|
421
|
+
/* ── bulk session actions ─────────────────────────────────── */
|
|
422
|
+
.sess-row.bulk-sel { background:oklch(72% 0.18 75 / 0.08); outline:1px solid oklch(72% 0.18 75 / 0.3); outline-offset:-1px; }
|
|
423
|
+
#bulk-toolbar { display:none; position:sticky; top:0; z-index:10; background:oklch(18% 0.009 55); border:1px solid oklch(72% 0.18 75 / 0.3); border-radius:var(--r); padding:8px 14px; margin-bottom:10px; gap:10px; align-items:center; }
|
|
424
|
+
#bulk-toolbar.show { display:flex; }
|
|
425
|
+
.bulk-count { font-size:12px; color:var(--text-mid); flex:1; }
|
|
426
|
+
.bulk-btn { font-size:11px; padding:4px 12px; border-radius:6px; border:1px solid var(--border); background:var(--surface); color:var(--text-mid); cursor:pointer; font-family:var(--sans); transition:color 0.1s; }
|
|
427
|
+
.bulk-btn:hover { color:var(--text-hi); }
|
|
428
|
+
.bulk-btn.danger { color:oklch(65% 0.15 25); border-color:oklch(65% 0.15 25 / 0.3); }
|
|
429
|
+
|
|
430
|
+
/* ── files touched ────────────────────────────────────────── */
|
|
431
|
+
.sr-files { font-size:10px; color:var(--text-xs); margin-top:2px; display:flex; flex-wrap:wrap; gap:3px; }
|
|
432
|
+
.sr-file-chip { font-family:var(--mono); background:var(--surface-hi); border-radius:3px; padding:1px 5px; color:var(--text-lo); }
|
|
433
|
+
|
|
434
|
+
/* ── memory age bars ──────────────────────────────────────── */
|
|
435
|
+
.dr-age-bar { height:2px; border-radius:1px; margin-top:4px; background:oklch(72% 0.18 75 / 0.25); }
|
|
436
|
+
.dr-age-bar-fill { height:100%; border-radius:1px; background:var(--accent); }
|
|
437
|
+
|
|
438
|
+
/* ── loop creation form ───────────────────────────────────── */
|
|
439
|
+
#loop-create-form { background:var(--surface); border:1px solid var(--border); border-radius:var(--r); padding:14px 16px; margin-bottom:16px; }
|
|
440
|
+
.lcf-title { font-size:12px; font-weight:600; color:var(--text-hi); margin-bottom:10px; }
|
|
441
|
+
.lcf-row { display:flex; flex-direction:column; gap:3px; margin-bottom:10px; }
|
|
442
|
+
.lcf-label { font-size:10px; color:var(--text-xs); text-transform:uppercase; letter-spacing:0.06em; }
|
|
443
|
+
.lcf-input, .lcf-textarea { background:var(--bg); border:1px solid var(--border); border-radius:4px; color:var(--text-hi); font-family:var(--sans); font-size:12px; padding:6px 10px; transition:border-color 0.15s; }
|
|
444
|
+
.lcf-textarea { resize:vertical; min-height:56px; }
|
|
445
|
+
.lcf-input:focus, .lcf-textarea:focus { outline:none; border-color:oklch(72% 0.18 75 / 0.5); }
|
|
446
|
+
.lcf-row-inline { display:flex; gap:8px; }
|
|
447
|
+
.lcf-row-inline .lcf-row { flex:1; margin-bottom:0; }
|
|
448
|
+
.lcf-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:4px; }
|
|
449
|
+
.lcf-submit { background:var(--accent); color:oklch(11% 0.009 55); border:none; border-radius:6px; font-size:12px; font-weight:600; padding:5px 16px; cursor:pointer; font-family:var(--sans); }
|
|
450
|
+
.lcf-submit:hover { background:oklch(78% 0.18 75); }
|
|
451
|
+
.lcf-cancel { background:transparent; border:1px solid var(--border); border-radius:6px; font-size:12px; color:var(--text-lo); padding:5px 12px; cursor:pointer; font-family:var(--sans); }
|
|
452
|
+
#btn-new-loop { font-size:11px; color:var(--text-lo); background:transparent; border:1px solid var(--border); border-radius:8px; padding:3px 10px; cursor:pointer; transition:color 0.1s; font-family:var(--sans); margin-bottom:12px; }
|
|
453
|
+
#btn-new-loop:hover { color:var(--accent); border-color:oklch(72% 0.18 75 / 0.4); }
|
|
454
|
+
|
|
392
455
|
/* ── auto-tags ───────────────────────────────────────────── */
|
|
393
456
|
.sr-autotag { font-size:10px; padding:1px 6px; border-radius:8px; background:oklch(72% 0.18 75 / 0.1); color:oklch(78% 0.18 75); border:1px solid oklch(72% 0.18 75 / 0.2); }
|
|
394
457
|
.tag-filter-bar { display:flex; flex-wrap:wrap; gap:5px; margin-bottom:12px; }
|
|
@@ -651,6 +714,70 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
651
714
|
|
|
652
715
|
/* ── streak badge ─────────────────────────────────────────── */
|
|
653
716
|
.streak-chip { display:inline-flex; align-items:center; gap:3px; font-size:11px; padding:2px 7px; border-radius:8px; background:oklch(65% 0.18 75 / 0.12); color:oklch(78% 0.18 75); white-space:nowrap; }
|
|
717
|
+
|
|
718
|
+
/* ── f47: 30-day daily cost trend ────────────────────────── */
|
|
719
|
+
#daily-trend-chart { display:flex; align-items:flex-end; gap:2px; height:40px; margin-top:6px; }
|
|
720
|
+
.dt-bar { flex:1; min-width:4px; border-radius:2px 2px 0 0; background:oklch(72% 0.18 75 / 0.5); cursor:pointer; transition:background 0.15s; }
|
|
721
|
+
.dt-bar:hover,.dt-bar.active { background:oklch(72% 0.18 75); }
|
|
722
|
+
.dt-bar.has-filter { background:oklch(65% 0.15 150 / 0.7); }
|
|
723
|
+
|
|
724
|
+
/* ── f48: live cost ticker ────────────────────────────────── */
|
|
725
|
+
#live-cost-ticker { font-size:11px; font-family:var(--mono); font-weight:600; color:oklch(78% 0.18 75); opacity:0; transition:opacity 0.3s; white-space:nowrap; }
|
|
726
|
+
#live-cost-ticker.show { opacity:1; }
|
|
727
|
+
#live-cost-ticker .lct-change { font-size:10px; color:var(--green); margin-left:3px; }
|
|
728
|
+
|
|
729
|
+
/* ── f49: hourly productivity heatmap ────────────────────── */
|
|
730
|
+
#hourly-heatmap-grid { display:grid; grid-template-columns:20px repeat(24,1fr); grid-template-rows:auto; gap:2px; margin-top:6px; font-size:9px; }
|
|
731
|
+
.hh-hour-lbl { color:var(--text-xs); text-align:center; padding-bottom:1px; }
|
|
732
|
+
.hh-day-lbl { color:var(--text-xs); line-height:12px; }
|
|
733
|
+
.hh-cell { height:10px; border-radius:2px; background:var(--surface-hi); cursor:default; }
|
|
734
|
+
.hh-cell.hh-1 { background:oklch(72% 0.18 75 / 0.25); }
|
|
735
|
+
.hh-cell.hh-2 { background:oklch(72% 0.18 75 / 0.5); }
|
|
736
|
+
.hh-cell.hh-3 { background:oklch(72% 0.18 75 / 0.75); }
|
|
737
|
+
.hh-cell.hh-4 { background:oklch(72% 0.18 75); }
|
|
738
|
+
|
|
739
|
+
/* ── f50: custom tag editor ──────────────────────────────── */
|
|
740
|
+
.sr-custom-tags { display:flex; flex-wrap:wrap; gap:4px; align-items:center; margin-top:4px; }
|
|
741
|
+
.sr-ctag { display:inline-flex; align-items:center; gap:3px; font-size:10px; padding:1px 6px; border-radius:8px; background:oklch(65% 0.15 200 / 0.15); color:oklch(72% 0.15 200); }
|
|
742
|
+
.sr-ctag .ctag-del { cursor:pointer; opacity:0.6; font-size:9px; line-height:1; }
|
|
743
|
+
.sr-ctag .ctag-del:hover { opacity:1; }
|
|
744
|
+
.ctag-add-btn { font-size:10px; color:var(--text-xs); background:none; border:1px dashed var(--border); border-radius:8px; padding:1px 6px; cursor:pointer; }
|
|
745
|
+
.ctag-add-btn:hover { color:var(--text-lo); border-color:var(--text-xs); }
|
|
746
|
+
.ctag-input-wrap { display:flex; gap:4px; margin-top:4px; }
|
|
747
|
+
.ctag-input { font-size:11px; background:var(--surface-hi); border:1px solid var(--border); border-radius:var(--r); padding:2px 6px; color:var(--text-hi); outline:none; width:100px; }
|
|
748
|
+
.ctag-input:focus { border-color:oklch(72% 0.18 75 / 0.5); }
|
|
749
|
+
.ctag-ok { font-size:10px; background:none; border:1px solid var(--border); border-radius:var(--r); padding:2px 6px; cursor:pointer; color:var(--text-lo); }
|
|
750
|
+
.ctag-ok:hover { color:var(--text-hi); }
|
|
751
|
+
.sr-autotag.ctag { background:oklch(65% 0.15 200 / 0.12); color:oklch(72% 0.15 200); }
|
|
752
|
+
|
|
753
|
+
/* ── f51: tool error drawer ──────────────────────────────── */
|
|
754
|
+
.sess-anomaly.anom-err.clickable { cursor:pointer; }
|
|
755
|
+
.sess-anomaly.anom-err.clickable:hover { opacity:0.8; }
|
|
756
|
+
.err-drawer { display:none; margin-top:6px; border:1px solid oklch(60% 0.18 25 / 0.3); border-radius:var(--r); overflow:hidden; }
|
|
757
|
+
.err-drawer.open { display:block; }
|
|
758
|
+
.err-drawer-head { display:flex; align-items:center; gap:8px; padding:6px 10px; background:oklch(60% 0.18 25 / 0.08); font-size:11px; font-weight:600; color:oklch(70% 0.18 25); }
|
|
759
|
+
.err-drawer-head .err-close { margin-left:auto; cursor:pointer; font-size:12px; background:none; border:none; color:var(--text-xs); }
|
|
760
|
+
.err-drawer-body { max-height:160px; overflow-y:auto; padding:8px 10px; }
|
|
761
|
+
.err-item { font-size:11px; font-family:var(--mono); color:oklch(80% 0.1 25); border-bottom:1px solid var(--border); padding:4px 0; white-space:pre-wrap; word-break:break-word; }
|
|
762
|
+
.err-item:last-child { border:none; }
|
|
763
|
+
|
|
764
|
+
/* ── f52: prompt copy button ─────────────────────────────── */
|
|
765
|
+
.sr-copy-btn { font-size:10px; background:none; border:none; cursor:pointer; color:var(--text-xs); padding:0 4px; opacity:0; transition:opacity 0.15s; line-height:1; }
|
|
766
|
+
.sess-row:hover .sr-copy-btn { opacity:1; }
|
|
767
|
+
.sr-copy-btn:hover { color:var(--text-hi); }
|
|
768
|
+
.sr-copy-btn.copied { color:var(--green); opacity:1; }
|
|
769
|
+
|
|
770
|
+
/* ── f53: session cost histogram ─────────────────────────── */
|
|
771
|
+
#cost-histogram-panel { display:none; margin-bottom:16px; padding:12px; background:var(--surface); border:1px solid var(--border); border-radius:var(--r); }
|
|
772
|
+
.ch-title { font-size:11px; font-weight:600; color:var(--text-lo); letter-spacing:0.06em; text-transform:uppercase; margin-bottom:8px; }
|
|
773
|
+
.ch-bars { display:flex; align-items:flex-end; gap:4px; height:50px; }
|
|
774
|
+
.ch-bar-wrap { flex:1; display:flex; flex-direction:column; align-items:center; gap:2px; }
|
|
775
|
+
.ch-bar { width:100%; border-radius:2px 2px 0 0; background:oklch(72% 0.18 75 / 0.4); min-height:2px; }
|
|
776
|
+
.ch-lbl { font-size:8px; color:var(--text-xs); white-space:nowrap; }
|
|
777
|
+
.ch-cnt { font-size:9px; color:var(--text-lo); }
|
|
778
|
+
|
|
779
|
+
/* ── f54: URL param indicator ────────────────────────────── */
|
|
780
|
+
.url-param-active { background:oklch(72% 0.18 75 / 0.08); }
|
|
654
781
|
</style>
|
|
655
782
|
</head>
|
|
656
783
|
<body>
|
|
@@ -708,6 +835,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
708
835
|
<span id="view-title">Now</span>
|
|
709
836
|
<span class="pill"><span class="live-dot"></span> live</span>
|
|
710
837
|
<span id="topbar-cost"></span>
|
|
838
|
+
<span id="live-cost-ticker" title="Current session accumulated cost"></span>
|
|
711
839
|
<span id="topbar-activity"></span>
|
|
712
840
|
<div id="tb-right">
|
|
713
841
|
<button class="btn" id="btn-budget" onclick="openBudgetModal()" title="Set daily/monthly cost budget">⚑ Budget</button>
|
|
@@ -852,6 +980,14 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
852
980
|
<div class="m-group-title">Token Velocity</div>
|
|
853
981
|
<div class="loading-txt">—</div>
|
|
854
982
|
</div>
|
|
983
|
+
<div id="m-daily-trend">
|
|
984
|
+
<div class="m-group-title">30-Day Cost Trend</div>
|
|
985
|
+
<div class="loading-txt">—</div>
|
|
986
|
+
</div>
|
|
987
|
+
<div id="m-hourly-heatmap">
|
|
988
|
+
<div class="m-group-title">Peak Work Hours</div>
|
|
989
|
+
<div class="loading-txt">—</div>
|
|
990
|
+
</div>
|
|
855
991
|
</div>
|
|
856
992
|
</div>
|
|
857
993
|
|
|
@@ -893,6 +1029,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
893
1029
|
<th class="lb-rank">#</th><th>Session</th><th class="lb-cost">Cost</th><th class="lb-dur">Duration</th>
|
|
894
1030
|
</tr></thead><tbody id="lb-body"></tbody></table>
|
|
895
1031
|
</div>
|
|
1032
|
+
<div id="cost-histogram-panel"></div>
|
|
896
1033
|
<div id="model-mix-panel" style="display:none;margin-bottom:16px">
|
|
897
1034
|
<div id="model-mix-body"></div>
|
|
898
1035
|
</div>
|
|
@@ -908,6 +1045,23 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
908
1045
|
<div id="patterns-panel" style="display:none;margin-bottom:16px">
|
|
909
1046
|
<div id="patterns-body"></div>
|
|
910
1047
|
</div>
|
|
1048
|
+
<div id="sess-heatmap" style="margin-bottom:14px;display:none">
|
|
1049
|
+
<div class="shm-label"><span>12-week activity</span><button id="shm-clear" onclick="clearHeatmapFilter()">✕ Clear filter</button></div>
|
|
1050
|
+
<div class="shm-grid" id="shm-grid"></div>
|
|
1051
|
+
</div>
|
|
1052
|
+
<div class="period-toggles" id="period-toggles">
|
|
1053
|
+
<span style="font-size:10px;color:var(--text-xs);align-self:center;text-transform:uppercase;letter-spacing:0.06em">Period:</span>
|
|
1054
|
+
<button class="period-btn active" data-period="day" onclick="setPeriod('day')">Day</button>
|
|
1055
|
+
<button class="period-btn" data-period="week" onclick="setPeriod('week')">Week</button>
|
|
1056
|
+
<button class="period-btn" data-period="month" onclick="setPeriod('month')">Month</button>
|
|
1057
|
+
<button class="period-btn" data-period="all" onclick="setPeriod('all')">All</button>
|
|
1058
|
+
</div>
|
|
1059
|
+
<div id="bulk-toolbar">
|
|
1060
|
+
<span class="bulk-count" id="bulk-count">0 selected</span>
|
|
1061
|
+
<button class="bulk-btn" onclick="bulkExport()">⬇ Export</button>
|
|
1062
|
+
<button class="bulk-btn" onclick="bulkBookmark()">☆ Bookmark all</button>
|
|
1063
|
+
<button class="bulk-btn danger" onclick="clearBulkSelection()">✕ Clear</button>
|
|
1064
|
+
</div>
|
|
911
1065
|
<div id="sess-filter-wrap">
|
|
912
1066
|
<input id="sess-filter-input" type="text" placeholder="Filter sessions by prompt…" oninput="filterSessions(this.value)" autocomplete="off">
|
|
913
1067
|
<span id="sess-filter-count"></span>
|
|
@@ -921,6 +1075,32 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
921
1075
|
<div class="vscroll">
|
|
922
1076
|
<div class="pg-title">Loops</div>
|
|
923
1077
|
<div class="pg-sub">Scheduled automation loops</div>
|
|
1078
|
+
<button id="btn-new-loop" onclick="showLoopForm()">+ New Loop</button>
|
|
1079
|
+
<div id="loop-create-form" style="display:none">
|
|
1080
|
+
<div class="lcf-title">Create Loop</div>
|
|
1081
|
+
<div class="lcf-row">
|
|
1082
|
+
<label class="lcf-label">Prompt</label>
|
|
1083
|
+
<textarea class="lcf-textarea" id="lcf-prompt" placeholder="What should the agent do each iteration?"></textarea>
|
|
1084
|
+
</div>
|
|
1085
|
+
<div class="lcf-row">
|
|
1086
|
+
<label class="lcf-label">Name (optional)</label>
|
|
1087
|
+
<input class="lcf-input" id="lcf-name" type="text" placeholder="My loop">
|
|
1088
|
+
</div>
|
|
1089
|
+
<div class="lcf-row-inline">
|
|
1090
|
+
<div class="lcf-row">
|
|
1091
|
+
<label class="lcf-label">Interval</label>
|
|
1092
|
+
<input class="lcf-input" id="lcf-interval" type="text" placeholder="1h" value="1h">
|
|
1093
|
+
</div>
|
|
1094
|
+
<div class="lcf-row">
|
|
1095
|
+
<label class="lcf-label">Max reps (blank = ∞)</label>
|
|
1096
|
+
<input class="lcf-input" id="lcf-maxreps" type="number" placeholder="∞" min="1">
|
|
1097
|
+
</div>
|
|
1098
|
+
</div>
|
|
1099
|
+
<div class="lcf-actions">
|
|
1100
|
+
<button class="lcf-cancel" onclick="hideLoopForm()">Cancel</button>
|
|
1101
|
+
<button class="lcf-submit" onclick="createLoop()">Create Loop</button>
|
|
1102
|
+
</div>
|
|
1103
|
+
</div>
|
|
924
1104
|
<div id="loops-content" class="loop-list"><div class="loading-txt">Loading…</div></div>
|
|
925
1105
|
</div>
|
|
926
1106
|
</div>
|
|
@@ -964,6 +1144,24 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
964
1144
|
</div><!-- /app -->
|
|
965
1145
|
<div id="toast-rack"></div>
|
|
966
1146
|
|
|
1147
|
+
<!-- shortcut help modal -->
|
|
1148
|
+
<div id="shortcut-modal" onclick="if(event.target===this)closeShortcutHelp()">
|
|
1149
|
+
<div id="shortcut-box">
|
|
1150
|
+
<div class="sk-title">Keyboard Shortcuts <button class="sk-close" onclick="closeShortcutHelp()">✕</button></div>
|
|
1151
|
+
<div class="sk-section">Feed (Now view)</div>
|
|
1152
|
+
<div class="sk-row"><span class="sk-desc">Navigate entries</span><span class="sk-keys"><kbd>J</kbd><kbd>K</kbd></span></div>
|
|
1153
|
+
<div class="sk-row"><span class="sk-desc">Open detail drawer</span><span class="sk-keys"><kbd>↵</kbd></span></div>
|
|
1154
|
+
<div class="sk-row"><span class="sk-desc">Search in feed</span><span class="sk-keys"><kbd>/</kbd></span></div>
|
|
1155
|
+
<div class="sk-row"><span class="sk-desc">Jump to live session</span><span class="sk-keys"><kbd>G</kbd></span></div>
|
|
1156
|
+
<div class="sk-row"><span class="sk-desc">Refresh current view</span><span class="sk-keys"><kbd>R</kbd></span></div>
|
|
1157
|
+
<div class="sk-row"><span class="sk-desc">Toggle ambient mode</span><span class="sk-keys"><kbd>A</kbd></span></div>
|
|
1158
|
+
<div class="sk-section">Global</div>
|
|
1159
|
+
<div class="sk-row"><span class="sk-desc">Command palette</span><span class="sk-keys"><kbd>⌘</kbd><kbd>K</kbd></span></div>
|
|
1160
|
+
<div class="sk-row"><span class="sk-desc">Close / dismiss</span><span class="sk-keys"><kbd>Esc</kbd></span></div>
|
|
1161
|
+
<div class="sk-row"><span class="sk-desc">This help</span><span class="sk-keys"><kbd>?</kbd></span></div>
|
|
1162
|
+
</div>
|
|
1163
|
+
</div>
|
|
1164
|
+
|
|
967
1165
|
<!-- budget modal (fixed overlay, outside app) -->
|
|
968
1166
|
<div id="budget-modal" onclick="if(event.target===this)closeBudgetModal()">
|
|
969
1167
|
<div id="budget-box">
|
|
@@ -1053,10 +1251,21 @@ async function init() {
|
|
|
1053
1251
|
document.getElementById('sb-path').textContent = DIR;
|
|
1054
1252
|
document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
|
|
1055
1253
|
} catch (_) {}
|
|
1056
|
-
|
|
1254
|
+
// deep-link: ?sess=<id>&proj=<path>
|
|
1255
|
+
const params = new URLSearchParams(window.location.search);
|
|
1256
|
+
const projParam = params.get('proj');
|
|
1257
|
+
const sessParam = params.get('sess');
|
|
1258
|
+
if (projParam) {
|
|
1259
|
+
DIR = projParam;
|
|
1260
|
+
document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
|
|
1261
|
+
document.getElementById('sb-path').textContent = DIR;
|
|
1262
|
+
}
|
|
1263
|
+
restoreURLParams();
|
|
1264
|
+
viewRendered['now'] = true;
|
|
1057
1265
|
updateBudgetBtnStyle();
|
|
1058
1266
|
await refreshNow();
|
|
1059
|
-
|
|
1267
|
+
if (sessParam) setTimeout(() => jumpToSession(sessParam), 300);
|
|
1268
|
+
initSSE();
|
|
1060
1269
|
}
|
|
1061
1270
|
|
|
1062
1271
|
function startPolling() {
|
|
@@ -1064,6 +1273,20 @@ function startPolling() {
|
|
|
1064
1273
|
pollTimer = setInterval(() => { if (currentView === 'now') refreshNowSilent(); }, 30000);
|
|
1065
1274
|
}
|
|
1066
1275
|
|
|
1276
|
+
let _sseSource = null;
|
|
1277
|
+
function initSSE() {
|
|
1278
|
+
if (_sseSource) { try { _sseSource.close(); } catch {} _sseSource = null; }
|
|
1279
|
+
if (!DIR || !window.EventSource) { startPolling(); return; }
|
|
1280
|
+
try {
|
|
1281
|
+
const src = new EventSource('/api/events-stream?dir=' + enc(DIR));
|
|
1282
|
+
src.addEventListener('update', () => { if (currentView === 'now') refreshNowSilent(); });
|
|
1283
|
+
src.addEventListener('connected', () => {});
|
|
1284
|
+
src.onerror = () => { src.close(); _sseSource = null; startPolling(); };
|
|
1285
|
+
_sseSource = src;
|
|
1286
|
+
clearInterval(pollTimer); // SSE replaces polling
|
|
1287
|
+
} catch { startPolling(); }
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1067
1290
|
async function apiFetch(url) {
|
|
1068
1291
|
const r = await fetch(url);
|
|
1069
1292
|
if (!r.ok) throw new Error(r.status);
|
|
@@ -1105,7 +1328,7 @@ function toggleLiveTail() {
|
|
|
1105
1328
|
if (sessionIdx !== 0 && allSessions.length) { sessionIdx = 0; userScrolled = false; loadFeedForSession(allSessions[0]); }
|
|
1106
1329
|
liveTailTimer = setInterval(() => { if (currentView === 'now') refreshNowSilent(); }, 5000);
|
|
1107
1330
|
} else {
|
|
1108
|
-
|
|
1331
|
+
initSSE();
|
|
1109
1332
|
}
|
|
1110
1333
|
}
|
|
1111
1334
|
|
|
@@ -1159,6 +1382,9 @@ async function loadFeedForSession(sess) {
|
|
|
1159
1382
|
document.getElementById('btn-prev-sess').style.opacity = sessionIdx < allSessions.length - 1 ? '1' : '0.3';
|
|
1160
1383
|
document.getElementById('btn-next-sess').style.opacity = sessionIdx > 0 ? '1' : '0.3';
|
|
1161
1384
|
showSessCtx(sess);
|
|
1385
|
+
// f48: live cost ticker
|
|
1386
|
+
const sessCost = typeof sess.totalCost === 'number' ? sess.totalCost : (typeof sess.cost === 'number' ? sess.cost : null);
|
|
1387
|
+
updateLiveTicker(sessCost);
|
|
1162
1388
|
if (!sess.file) { setFeedContent('<div class="feed-empty">Session file path unavailable.</div>'); return; }
|
|
1163
1389
|
try {
|
|
1164
1390
|
const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=120');
|
|
@@ -1275,6 +1501,8 @@ function renderFeedEvents(events, silent) {
|
|
|
1275
1501
|
updateBurnRate(filtered);
|
|
1276
1502
|
// session recap card
|
|
1277
1503
|
buildRecap(filtered, allSessions[sessionIdx]);
|
|
1504
|
+
// infer current activity from most recent tool
|
|
1505
|
+
updateCurrentActivity(visible);
|
|
1278
1506
|
}
|
|
1279
1507
|
|
|
1280
1508
|
function renderGroupRow(g) {
|
|
@@ -1642,17 +1870,49 @@ async function renderSessions() {
|
|
|
1642
1870
|
}
|
|
1643
1871
|
let toShow = showStarredOnly ? sessions.filter(s => bookmarks.has(s.id)) : sessions;
|
|
1644
1872
|
if (activeTagFilter) toShow = toShow.filter(s => (allTags.sessionTags.get(s.id) || []).includes(activeTagFilter));
|
|
1873
|
+
if (heatmapDateFilter) toShow = toShow.filter(s => {
|
|
1874
|
+
const t = s.lastTs || s.mtime; if (!t) return false;
|
|
1875
|
+
return new Date(typeof t === 'number' ? t : t).toDateString() === heatmapDateFilter;
|
|
1876
|
+
});
|
|
1645
1877
|
if (!toShow.length) {
|
|
1646
1878
|
el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
|
|
1879
|
+
buildSessionHeatmap(sessions);
|
|
1647
1880
|
return;
|
|
1648
1881
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1882
|
+
// compute median cost for anomaly detection
|
|
1883
|
+
const costsForMedian = sessions.map(s => s.totalCost || 0).filter(c => c > 0).sort((a, b) => a - b);
|
|
1884
|
+
const medianCost = costsForMedian.length ? costsForMedian[Math.floor(costsForMedian.length / 2)] : 0;
|
|
1885
|
+
|
|
1886
|
+
// group by date
|
|
1887
|
+
const now = Date.now(); const DAY = 86400000;
|
|
1888
|
+
function sessDateGroup(s) {
|
|
1889
|
+
const t = s.lastTs || s.mtime; if (!t) return 'Older';
|
|
1890
|
+
const age = now - (typeof t === 'number' ? t : new Date(t).getTime());
|
|
1891
|
+
if (age < DAY) return 'Today';
|
|
1892
|
+
if (age < 2 * DAY) return 'Yesterday';
|
|
1893
|
+
if (age < 8 * DAY) return 'This week';
|
|
1894
|
+
return 'Older';
|
|
1895
|
+
}
|
|
1896
|
+
const GROUP_ORDER = ['Today', 'Yesterday', 'This week', 'Older'];
|
|
1897
|
+
const groups = {};
|
|
1898
|
+
for (const s of toShow) {
|
|
1899
|
+
const g = sessDateGroup(s); if (!groups[g]) groups[g] = [];
|
|
1900
|
+
groups[g].push(s);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function renderSessRow(s, idx) {
|
|
1651
1904
|
const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
|
|
1652
1905
|
const msgs = s.totalMessages ? s.totalMessages + ' msg' : '';
|
|
1653
1906
|
const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2)
|
|
1654
1907
|
: typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '';
|
|
1655
1908
|
const meta = [dur, msgs, cost].filter(Boolean).join(' · ') || s.id.slice(0, 16);
|
|
1909
|
+
const sCost = s.totalCost || 0;
|
|
1910
|
+
let anomBadge = '';
|
|
1911
|
+
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>`;
|
|
1913
|
+
} else if (s.toolCalls > 0 && (s.errorCount || 0) / s.toolCalls > 0.3) {
|
|
1914
|
+
anomBadge = `<span class="sess-anomaly anom-err" title="${s.errorCount} tool errors">! errors</span>`;
|
|
1915
|
+
}
|
|
1656
1916
|
const satPct = Math.min(100, Math.round((s.totalMessages || 0) / 200 * 100));
|
|
1657
1917
|
const satColor = satPct > 80 ? 'oklch(65% 0.2 25)' : satPct > 50 ? 'oklch(70% 0.18 80)' : 'var(--accent)';
|
|
1658
1918
|
const satBar = satPct > 0 ? `<div class="ctx-sat-wrap" title="Context saturation ~${satPct}% (${s.totalMessages||0} turns / 200 est. max)">
|
|
@@ -1664,16 +1924,29 @@ async function renderSessions() {
|
|
|
1664
1924
|
const sData = JSON.stringify(s).replace(/'/g, ''');
|
|
1665
1925
|
const note = getSessNote(s.id);
|
|
1666
1926
|
const hasNote = !!note;
|
|
1667
|
-
|
|
1927
|
+
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>` : '';
|
|
1929
|
+
// f51: error badge — clickable if errors exist
|
|
1930
|
+
const errBadge = (s.errorCount > 0 && s.toolCalls > 0 && (s.errorCount / s.toolCalls) > 0.3)
|
|
1931
|
+
? `<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>`
|
|
1932
|
+
: anomBadge;
|
|
1933
|
+
// f50: custom tags
|
|
1934
|
+
const ctags = getCustomTags(s.id);
|
|
1935
|
+
const ctagsHtml = renderCustomTagsInline(s.id, ctags);
|
|
1936
|
+
return `<div class="sess-row" data-sess-idx="${idx}" data-sess-id="${esc(s.id)}" onclick="handleSessRowClick(event,this,'${esc(s.id)}')" data-sess-data='${sData}'>
|
|
1668
1937
|
<div class="sr-top">
|
|
1669
1938
|
<div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
|
|
1670
1939
|
<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>
|
|
1671
1941
|
<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>
|
|
1672
1942
|
<span class="sr-view">→ view</span>
|
|
1673
1943
|
</div>
|
|
1674
|
-
<div class="sr-meta">${esc(meta)}</div>
|
|
1944
|
+
<div class="sr-meta">${esc(meta)}${errBadge}</div>
|
|
1675
1945
|
${(summaries || autoTags) ? `<div class="sr-tags">${summaries}${autoTags}</div>` : ''}
|
|
1946
|
+
${ctagsHtml}
|
|
1947
|
+
${filesHtml}
|
|
1676
1948
|
${satBar}
|
|
1949
|
+
<div class="err-drawer" id="err-drawer-${esc(s.id)}"></div>
|
|
1677
1950
|
<div class="sess-notes-wrap" onclick="event.stopPropagation()">
|
|
1678
1951
|
<button class="sess-notes-toggle${hasNote ? ' has-note' : ''}" onclick="toggleSessNote('${esc(s.id)}',this)">✎ ${hasNote ? 'Note' : 'Add note'}</button>
|
|
1679
1952
|
<div class="sess-notes-area" id="snote-${esc(s.id)}">
|
|
@@ -1682,17 +1955,36 @@ async function renderSessions() {
|
|
|
1682
1955
|
</div>
|
|
1683
1956
|
</div>
|
|
1684
1957
|
</div>`;
|
|
1685
|
-
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
let html = '';
|
|
1961
|
+
let flatIdx = 0;
|
|
1962
|
+
for (const grp of GROUP_ORDER) {
|
|
1963
|
+
const items = groups[grp];
|
|
1964
|
+
if (!items || !items.length) continue;
|
|
1965
|
+
const gid = 'sg-' + grp.replace(/\s+/g, '-').toLowerCase();
|
|
1966
|
+
html += `<div class="sg-section"><div class="sg-header" onclick="toggleSessGroup('${gid}')">
|
|
1967
|
+
<span class="sg-title">${grp}</span><span class="sg-count">${items.length}</span><span class="sg-toggle">▾</span>
|
|
1968
|
+
</div><div class="sg-body" id="${gid}">`;
|
|
1969
|
+
for (const s of items) { html += renderSessRow(s, flatIdx++); }
|
|
1970
|
+
html += '</div></div>';
|
|
1971
|
+
}
|
|
1972
|
+
el.innerHTML = html;
|
|
1686
1973
|
// prepend tag filter bar if there are common tags
|
|
1687
1974
|
if (allTags.common.size > 1) {
|
|
1688
1975
|
el.innerHTML = buildTagFilterBar(toShow) + el.innerHTML;
|
|
1689
1976
|
}
|
|
1977
|
+
buildSessionHeatmap(sessions);
|
|
1690
1978
|
buildDigest();
|
|
1691
1979
|
buildWeeklyRecap();
|
|
1692
1980
|
buildEfficiencyPanel();
|
|
1693
1981
|
buildActivityHeatmap();
|
|
1694
1982
|
buildTokenVelocity();
|
|
1983
|
+
buildDailyCostTrend();
|
|
1984
|
+
buildHourlyHeatmap();
|
|
1985
|
+
buildCostHistogram();
|
|
1695
1986
|
if (leaderboardOpen) renderLeaderboard();
|
|
1987
|
+
syncURLParams();
|
|
1696
1988
|
// check budget thresholds and fire toasts
|
|
1697
1989
|
const todayCost = allSessions.filter(s => {
|
|
1698
1990
|
const t = s.firstTs || s.mtime;
|
|
@@ -1716,6 +2008,7 @@ async function renderSessions() {
|
|
|
1716
2008
|
|
|
1717
2009
|
function jumpToSession(id) {
|
|
1718
2010
|
switchView('now');
|
|
2011
|
+
history.replaceState(null, '', '?sess=' + encodeURIComponent(id) + '&proj=' + encodeURIComponent(DIR));
|
|
1719
2012
|
setTimeout(() => {
|
|
1720
2013
|
const i = allSessions.findIndex(x => x.id === id);
|
|
1721
2014
|
if (i >= 0) { sessionIdx = i; userScrolled = false; loadFeedForSession(allSessions[i]); }
|
|
@@ -1740,10 +2033,10 @@ function toggleSessStarFilter() {
|
|
|
1740
2033
|
showStarredOnly = !showStarredOnly;
|
|
1741
2034
|
const btn = document.getElementById('sess-star-filter');
|
|
1742
2035
|
btn.classList.toggle('on', showStarredOnly);
|
|
1743
|
-
// re-render with filter
|
|
1744
2036
|
viewRendered['sessions'] = false;
|
|
1745
2037
|
renderSessions();
|
|
1746
2038
|
viewRendered['sessions'] = true;
|
|
2039
|
+
syncURLParams();
|
|
1747
2040
|
}
|
|
1748
2041
|
|
|
1749
2042
|
// ── feature 1: auto-tags ───────────────────────────────────
|
|
@@ -1791,6 +2084,7 @@ function setTagFilter(tag) {
|
|
|
1791
2084
|
viewRendered['sessions'] = false;
|
|
1792
2085
|
renderSessions();
|
|
1793
2086
|
viewRendered['sessions'] = true;
|
|
2087
|
+
syncURLParams();
|
|
1794
2088
|
}
|
|
1795
2089
|
|
|
1796
2090
|
// ── feature 2: session recap ───────────────────────────────
|
|
@@ -2099,8 +2393,21 @@ function buildDigest() {
|
|
|
2099
2393
|
...themes.map(t => `#${t}`),
|
|
2100
2394
|
].filter(Boolean);
|
|
2101
2395
|
|
|
2396
|
+
// cost forecast: project monthly spend from daily average
|
|
2397
|
+
const today2 = new Date();
|
|
2398
|
+
const daysInMonth = new Date(today2.getFullYear(), today2.getMonth() + 1, 0).getDate();
|
|
2399
|
+
const dayOfMonth = today2.getDate();
|
|
2400
|
+
const monthCostSoFar = allSessions.filter(s => {
|
|
2401
|
+
const t = s.firstTs || s.mtime;
|
|
2402
|
+
if (!t) return false;
|
|
2403
|
+
const d = new Date(typeof t === 'number' ? t : t);
|
|
2404
|
+
return d.getFullYear() === today2.getFullYear() && d.getMonth() === today2.getMonth();
|
|
2405
|
+
}).reduce((a, s) => a + (s.totalCost || 0), 0);
|
|
2406
|
+
const dailyAvg = dayOfMonth > 0 ? monthCostSoFar / dayOfMonth : 0;
|
|
2407
|
+
const projected = dailyAvg * daysInMonth;
|
|
2408
|
+
const forecastHtml = projected > 0.05 ? `<span class="digest-stat" title="Projected monthly spend at current pace" style="color:oklch(72% 0.14 200)">~$${projected.toFixed(2)}/mo</span>` : '';
|
|
2102
2409
|
document.getElementById('digest-stats').innerHTML =
|
|
2103
|
-
stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('');
|
|
2410
|
+
stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('') + forecastHtml;
|
|
2104
2411
|
document.getElementById('digest-card').classList.add('show');
|
|
2105
2412
|
}
|
|
2106
2413
|
|
|
@@ -2519,6 +2826,42 @@ function toggleLoop(row) {
|
|
|
2519
2826
|
row.classList.toggle('open');
|
|
2520
2827
|
}
|
|
2521
2828
|
|
|
2829
|
+
function showLoopForm() {
|
|
2830
|
+
document.getElementById('loop-create-form').style.display = 'block';
|
|
2831
|
+
document.getElementById('btn-new-loop').style.display = 'none';
|
|
2832
|
+
document.getElementById('lcf-prompt').focus();
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
function hideLoopForm() {
|
|
2836
|
+
document.getElementById('loop-create-form').style.display = 'none';
|
|
2837
|
+
document.getElementById('btn-new-loop').style.display = '';
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
async function createLoop() {
|
|
2841
|
+
const prompt = document.getElementById('lcf-prompt').value.trim();
|
|
2842
|
+
if (!prompt) { showToast('Required', 'Prompt is required', 'warn'); return; }
|
|
2843
|
+
const name = document.getElementById('lcf-name').value.trim();
|
|
2844
|
+
const interval = document.getElementById('lcf-interval').value.trim() || '1h';
|
|
2845
|
+
const maxRepsVal = document.getElementById('lcf-maxreps').value;
|
|
2846
|
+
const maxReps = maxRepsVal ? parseInt(maxRepsVal) : null;
|
|
2847
|
+
try {
|
|
2848
|
+
const r = await fetch('/api/loops/create?dir=' + enc(DIR), {
|
|
2849
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2850
|
+
body: JSON.stringify({ name, prompt, interval, maxReps }),
|
|
2851
|
+
});
|
|
2852
|
+
const d = await r.json();
|
|
2853
|
+
if (!d.ok) { showToast('Error', d.error || 'Failed to create loop', 'err'); return; }
|
|
2854
|
+
showToast('Created', `Loop created: ${name || prompt.slice(0, 30)}`, 'ok');
|
|
2855
|
+
hideLoopForm();
|
|
2856
|
+
document.getElementById('lcf-prompt').value = '';
|
|
2857
|
+
document.getElementById('lcf-name').value = '';
|
|
2858
|
+
document.getElementById('lcf-interval').value = '1h';
|
|
2859
|
+
document.getElementById('lcf-maxreps').value = '';
|
|
2860
|
+
viewRendered['loops'] = false;
|
|
2861
|
+
renderLoops();
|
|
2862
|
+
} catch (err) { showToast('Error', err.message, 'err'); }
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2522
2865
|
// ── feature 19: cache efficiency panel ────────────────────
|
|
2523
2866
|
function buildEfficiencyPanel() {
|
|
2524
2867
|
const el = document.getElementById('m-efficiency');
|
|
@@ -2610,6 +2953,7 @@ function buildWeeklyRecap() {
|
|
|
2610
2953
|
return new Date(typeof t === 'number' ? t : t).toDateString();
|
|
2611
2954
|
})).size;
|
|
2612
2955
|
const longestMs = Math.max(...weekSess.map(s => s.totalDurationMs||0));
|
|
2956
|
+
const streak = calcStreak();
|
|
2613
2957
|
const stats = [
|
|
2614
2958
|
`${weekSess.length} session${weekSess.length!==1?'s':''}`,
|
|
2615
2959
|
`${days} day${days!==1?'s':''}`,
|
|
@@ -2617,7 +2961,8 @@ function buildWeeklyRecap() {
|
|
|
2617
2961
|
totalTools > 0 ? `${totalTools.toLocaleString()} tool calls` : null,
|
|
2618
2962
|
longestMs > 0 ? `${fmtDur(longestMs)} longest` : null,
|
|
2619
2963
|
].filter(Boolean);
|
|
2620
|
-
|
|
2964
|
+
const streakHtml = streak >= 2 ? `<span class="streak-chip" title="${streak} consecutive days with sessions">🔥 ${streak}d streak</span>` : '';
|
|
2965
|
+
document.getElementById('weekly-stats').innerHTML = stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('') + streakHtml;
|
|
2621
2966
|
document.getElementById('weekly-card').classList.add('show');
|
|
2622
2967
|
}
|
|
2623
2968
|
function dismissWeekly() {
|
|
@@ -2724,12 +3069,21 @@ async function renderMemory() {
|
|
|
2724
3069
|
).join('');
|
|
2725
3070
|
}
|
|
2726
3071
|
|
|
2727
|
-
const
|
|
2728
|
-
|
|
3072
|
+
const tsValues = allDrawers.map(d => d.timestamp ? new Date(d.timestamp).getTime() : 0).filter(Boolean);
|
|
3073
|
+
const oldestTs = tsValues.length ? Math.min(...tsValues) : 0;
|
|
3074
|
+
const newestTs = tsValues.length ? Math.max(...tsValues) : 0;
|
|
3075
|
+
const tsRange = newestTs - oldestTs || 1;
|
|
3076
|
+
const items = allDrawers.map((d, i) => {
|
|
3077
|
+
const ts = d.timestamp ? new Date(d.timestamp).getTime() : 0;
|
|
3078
|
+
const agePct = ts && oldestTs ? Math.round(((ts - oldestTs) / tsRange) * 100) : 0;
|
|
3079
|
+
const ageBar = ts ? `<div class="dr-age-bar"><div class="dr-age-bar-fill" style="width:${agePct}%"></div></div>` : '';
|
|
3080
|
+
return `<div class="drawer-item" data-idx="${i}" data-ns="${esc(d.namespace || 'default')}">
|
|
2729
3081
|
<div class="dr-key">${esc(d.key || d.namespace || '—')}</div>
|
|
2730
3082
|
<div class="dr-val">${esc(String(d.value || d.text || '').slice(0, 300))}</div>
|
|
2731
3083
|
${d.timestamp ? `<div class="dr-ts">${relTime(d.timestamp)}</div>` : ''}
|
|
2732
|
-
|
|
3084
|
+
${ageBar}
|
|
3085
|
+
</div>`;
|
|
3086
|
+
}).join('');
|
|
2733
3087
|
html += `<div class="mem-section" id="drawers-section">
|
|
2734
3088
|
<div class="mem-title">Drawers (${allDrawers.length})</div>
|
|
2735
3089
|
<div id="drawers-list">${items}</div>
|
|
@@ -3118,7 +3472,8 @@ document.addEventListener('keydown', e => {
|
|
|
3118
3472
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
3119
3473
|
if (document.getElementById('cmd-palette').classList.contains('open')) return;
|
|
3120
3474
|
|
|
3121
|
-
if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); }
|
|
3475
|
+
if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); closeShortcutHelp(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); }
|
|
3476
|
+
if (e.key === '?') { e.preventDefault(); openShortcutHelp(); return; }
|
|
3122
3477
|
if (e.key === 'a' || e.key === 'A') { if (currentView === 'now') { e.preventDefault(); toggleAmbient(); } }
|
|
3123
3478
|
|
|
3124
3479
|
if (currentView === 'now') {
|
|
@@ -3147,6 +3502,108 @@ document.addEventListener('keydown', e => {
|
|
|
3147
3502
|
}
|
|
3148
3503
|
});
|
|
3149
3504
|
|
|
3505
|
+
// ── feature 31: inline session filter ─────────────────────
|
|
3506
|
+
function filterSessions(q) {
|
|
3507
|
+
const rows = document.querySelectorAll('#sess-content .sess-row');
|
|
3508
|
+
const lq = q.toLowerCase().trim();
|
|
3509
|
+
let visible = 0;
|
|
3510
|
+
rows.forEach(row => {
|
|
3511
|
+
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);
|
|
3515
|
+
row.style.display = match ? '' : 'none';
|
|
3516
|
+
if (match) visible++;
|
|
3517
|
+
});
|
|
3518
|
+
const countEl = document.getElementById('sess-filter-count');
|
|
3519
|
+
if (countEl) countEl.textContent = lq && rows.length ? `${visible} / ${rows.length}` : '';
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
// ── feature 32: keyboard shortcut help modal ──────────────
|
|
3523
|
+
function openShortcutHelp() { document.getElementById('shortcut-modal').classList.add('open'); }
|
|
3524
|
+
function closeShortcutHelp() { document.getElementById('shortcut-modal').classList.remove('open'); }
|
|
3525
|
+
|
|
3526
|
+
// ── feature 33: "currently working on" inference ──────────
|
|
3527
|
+
function updateCurrentActivity(events) {
|
|
3528
|
+
const el = document.getElementById('topbar-activity');
|
|
3529
|
+
if (!el) return;
|
|
3530
|
+
if (!events?.length) { el.textContent = ''; el.classList.remove('loaded'); return; }
|
|
3531
|
+
const recent = [...events].reverse().find(ev => ev.kind === 'tool');
|
|
3532
|
+
if (!recent) { el.textContent = ''; el.classList.remove('loaded'); return; }
|
|
3533
|
+
const name = recent.name || '';
|
|
3534
|
+
let activity = '';
|
|
3535
|
+
if (['Write', 'Edit', 'Read', 'MultiEdit'].includes(name)) {
|
|
3536
|
+
const lbl = recent.label || '';
|
|
3537
|
+
const match = lbl.match(/([^/\\]+\.[a-zA-Z0-9]+)/) ;
|
|
3538
|
+
activity = match ? match[1] : (lbl.split('/').pop() || name);
|
|
3539
|
+
} else if (name === 'Bash') {
|
|
3540
|
+
activity = 'bash: ' + (recent.label || '').slice(0, 24);
|
|
3541
|
+
} else if (name === 'WebSearch' || name === 'WebFetch') {
|
|
3542
|
+
activity = 'web: ' + (recent.label || '').slice(0, 20);
|
|
3543
|
+
} else if (name) {
|
|
3544
|
+
activity = name;
|
|
3545
|
+
}
|
|
3546
|
+
if (activity) { el.textContent = '⤷ ' + activity; el.classList.add('loaded'); }
|
|
3547
|
+
else { el.textContent = ''; el.classList.remove('loaded'); }
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
// ── feature 34: prompt pattern analysis ───────────────────
|
|
3551
|
+
let patternsOpen = false;
|
|
3552
|
+
function togglePatterns() {
|
|
3553
|
+
patternsOpen = !patternsOpen;
|
|
3554
|
+
document.getElementById('btn-patterns').classList.toggle('on', patternsOpen);
|
|
3555
|
+
const p = document.getElementById('patterns-panel');
|
|
3556
|
+
p.style.display = patternsOpen ? '' : 'none';
|
|
3557
|
+
if (patternsOpen) buildPatterns();
|
|
3558
|
+
}
|
|
3559
|
+
function buildPatterns() {
|
|
3560
|
+
const el = document.getElementById('patterns-body');
|
|
3561
|
+
if (!allSessions.length) { el.innerHTML = '<div class="loading-txt">No sessions loaded</div>'; return; }
|
|
3562
|
+
const freq = {};
|
|
3563
|
+
for (const s of allSessions) {
|
|
3564
|
+
const words = (s.lastPrompt || '').toLowerCase().match(/\b[a-z]{4,}\b/g) || [];
|
|
3565
|
+
const seen = new Set();
|
|
3566
|
+
for (const w of words) {
|
|
3567
|
+
if (!STOP_WORDS.has(w) && !seen.has(w)) { freq[w] = (freq[w] || 0) + 1; seen.add(w); }
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
const sorted = Object.entries(freq).filter(([,c]) => c >= 2).sort((a, b) => b[1] - a[1]).slice(0, 20);
|
|
3571
|
+
if (!sorted.length) { el.innerHTML = '<div class="loading-txt">Not enough prompt data</div>'; return; }
|
|
3572
|
+
const maxCount = sorted[0][1];
|
|
3573
|
+
const rows = sorted.map(([word, count], i) => {
|
|
3574
|
+
const barW = Math.round((count / maxCount) * 100);
|
|
3575
|
+
return `<tr><td class="lb-rank">${i + 1}</td>
|
|
3576
|
+
<td style="font-size:12px;color:var(--text-mid)">${esc(word)}</td>
|
|
3577
|
+
<td style="width:100px;padding:4px 6px"><div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
|
|
3578
|
+
<div style="height:100%;width:${barW}%;background:oklch(70% 0.15 300);border-radius:2px"></div></div></td>
|
|
3579
|
+
<td class="lb-cost" style="color:var(--text-mid)">${count}</td></tr>`;
|
|
3580
|
+
}).join('');
|
|
3581
|
+
el.innerHTML = `<table class="lb-table"><thead><tr>
|
|
3582
|
+
<th class="lb-rank">#</th><th>Term</th><th></th><th class="lb-cost">Sessions</th>
|
|
3583
|
+
</tr></thead><tbody>${rows}</tbody></table>`;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
// ── feature 35: session streak tracker ────────────────────
|
|
3587
|
+
function calcStreak() {
|
|
3588
|
+
const dates = new Set(allSessions.map(s => {
|
|
3589
|
+
const t = s.firstTs || s.mtime;
|
|
3590
|
+
if (!t) return null;
|
|
3591
|
+
return new Date(typeof t === 'number' ? t : t).toDateString();
|
|
3592
|
+
}).filter(Boolean));
|
|
3593
|
+
let streak = 0;
|
|
3594
|
+
const today = new Date();
|
|
3595
|
+
for (let i = 0; i <= 365; i++) {
|
|
3596
|
+
const d = new Date(today);
|
|
3597
|
+
d.setDate(d.getDate() - i);
|
|
3598
|
+
if (dates.has(d.toDateString())) {
|
|
3599
|
+
streak++;
|
|
3600
|
+
} else if (i > 0) {
|
|
3601
|
+
break;
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
return streak;
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3150
3607
|
// ── feature 25: notification toasts ──────────────────────
|
|
3151
3608
|
let _toastLastBudgetKey = '';
|
|
3152
3609
|
function showToast(title, msg, type = 'info', duration = 5000) {
|
|
@@ -3187,14 +3644,169 @@ function checkBudgetToast(todayCost, monthCost) {
|
|
|
3187
3644
|
}
|
|
3188
3645
|
}
|
|
3189
3646
|
|
|
3647
|
+
// ── feature 39: cost period toggle ────────────────────────
|
|
3648
|
+
let activePeriod = 'day';
|
|
3649
|
+
let heatmapDateFilter = null;
|
|
3650
|
+
|
|
3651
|
+
function setPeriod(p) {
|
|
3652
|
+
activePeriod = p;
|
|
3653
|
+
document.querySelectorAll('.period-btn').forEach(b => b.classList.toggle('active', b.dataset.period === p));
|
|
3654
|
+
buildTokenVelocity();
|
|
3655
|
+
syncURLParams();
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
function periodFilteredSessions() {
|
|
3659
|
+
const now = Date.now();
|
|
3660
|
+
const DAY = 86400000;
|
|
3661
|
+
const windows = { day: DAY, week: 7 * DAY, month: 30 * DAY, all: Infinity };
|
|
3662
|
+
const w = windows[activePeriod] || DAY;
|
|
3663
|
+
if (w === Infinity) return allSessions;
|
|
3664
|
+
return allSessions.filter(s => {
|
|
3665
|
+
const t = s.firstTs || s.mtime; if (!t) return false;
|
|
3666
|
+
const ts = typeof t === 'number' ? t : new Date(t).getTime();
|
|
3667
|
+
return (now - ts) <= w;
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
// ── feature 40: session heatmap ────────────────────────────
|
|
3672
|
+
function buildSessionHeatmap(sessions) {
|
|
3673
|
+
const el = document.getElementById('shm-grid');
|
|
3674
|
+
const wrap = document.getElementById('sess-heatmap');
|
|
3675
|
+
if (!el || !wrap || !sessions.length) return;
|
|
3676
|
+
wrap.style.display = 'block';
|
|
3677
|
+
const DAY = 86400000;
|
|
3678
|
+
const now = Date.now();
|
|
3679
|
+
const WEEKS = 12; const DAYS = WEEKS * 7;
|
|
3680
|
+
const buckets = new Array(DAYS).fill(null).map(() => ({ count:0, date:null }));
|
|
3681
|
+
for (let i = 0; i < DAYS; i++) {
|
|
3682
|
+
buckets[i].date = new Date(now - (DAYS - 1 - i) * DAY).toDateString();
|
|
3683
|
+
}
|
|
3684
|
+
for (const s of sessions) {
|
|
3685
|
+
const ts = s.lastTs || s.mtime; if (!ts) continue;
|
|
3686
|
+
const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
|
|
3687
|
+
const idx = DAYS - 1 - Math.floor(age / DAY);
|
|
3688
|
+
if (idx >= 0 && idx < DAYS) buckets[idx].count++;
|
|
3689
|
+
}
|
|
3690
|
+
const max = Math.max(...buckets.map(b => b.count), 1);
|
|
3691
|
+
el.innerHTML = buckets.map(b => {
|
|
3692
|
+
const level = b.count === 0 ? 0 : Math.min(4, Math.ceil(b.count / max * 4));
|
|
3693
|
+
const isActive = b.date === heatmapDateFilter;
|
|
3694
|
+
return `<div class="shm-cell shm-${level}${isActive ? ' shm-active' : ''}" title="${b.date}: ${b.count} session${b.count !== 1 ? 's' : ''}" onclick="setHeatmapFilter('${b.date}',${b.count})"></div>`;
|
|
3695
|
+
}).join('');
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
function setHeatmapFilter(dateStr, count) {
|
|
3699
|
+
if (!count) return;
|
|
3700
|
+
heatmapDateFilter = heatmapDateFilter === dateStr ? null : dateStr;
|
|
3701
|
+
const clearBtn = document.getElementById('shm-clear');
|
|
3702
|
+
if (clearBtn) clearBtn.classList.toggle('show', !!heatmapDateFilter);
|
|
3703
|
+
viewRendered['sessions'] = false;
|
|
3704
|
+
renderSessions();
|
|
3705
|
+
syncURLParams();
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
function clearHeatmapFilter() {
|
|
3709
|
+
heatmapDateFilter = null;
|
|
3710
|
+
const clearBtn = document.getElementById('shm-clear');
|
|
3711
|
+
if (clearBtn) clearBtn.classList.remove('show');
|
|
3712
|
+
viewRendered['sessions'] = false;
|
|
3713
|
+
renderSessions();
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
function toggleSessGroup(id) {
|
|
3717
|
+
const body = document.getElementById(id);
|
|
3718
|
+
if (!body) return;
|
|
3719
|
+
body.classList.toggle('collapsed');
|
|
3720
|
+
const hdr = body.previousElementSibling;
|
|
3721
|
+
if (hdr) { const tog = hdr.querySelector('.sg-toggle'); if (tog) tog.textContent = body.classList.contains('collapsed') ? '▸' : '▾'; }
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
// ── feature 41: bulk session actions ───────────────────────
|
|
3725
|
+
let bulkSelected = new Set();
|
|
3726
|
+
let lastClickedSessIdx = null;
|
|
3727
|
+
|
|
3728
|
+
function handleSessRowClick(evt, row, sessId) {
|
|
3729
|
+
if (diffMode) { diffSelectSession(JSON.parse(row.dataset.sessData || '{}'), row); return; }
|
|
3730
|
+
if (evt.shiftKey && lastClickedSessIdx !== null) {
|
|
3731
|
+
// range-select
|
|
3732
|
+
const rows = [...document.querySelectorAll('#sess-content .sess-row')];
|
|
3733
|
+
const curIdx = rows.indexOf(row);
|
|
3734
|
+
const lo = Math.min(lastClickedSessIdx, curIdx);
|
|
3735
|
+
const hi = Math.max(lastClickedSessIdx, curIdx);
|
|
3736
|
+
for (let i = lo; i <= hi; i++) {
|
|
3737
|
+
const r = rows[i]; if (!r) continue;
|
|
3738
|
+
const id = r.dataset.sessId;
|
|
3739
|
+
if (id) { bulkSelected.add(id); r.classList.add('bulk-sel'); }
|
|
3740
|
+
}
|
|
3741
|
+
updateBulkToolbar();
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
if (bulkSelected.size > 0) {
|
|
3745
|
+
// toggle this row in bulk selection
|
|
3746
|
+
if (bulkSelected.has(sessId)) { bulkSelected.delete(sessId); row.classList.remove('bulk-sel'); }
|
|
3747
|
+
else { bulkSelected.add(sessId); row.classList.add('bulk-sel'); }
|
|
3748
|
+
const rows = [...document.querySelectorAll('#sess-content .sess-row')];
|
|
3749
|
+
lastClickedSessIdx = rows.indexOf(row);
|
|
3750
|
+
updateBulkToolbar();
|
|
3751
|
+
return;
|
|
3752
|
+
}
|
|
3753
|
+
lastClickedSessIdx = [...document.querySelectorAll('#sess-content .sess-row')].indexOf(row);
|
|
3754
|
+
jumpToSession(sessId);
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
function updateBulkToolbar() {
|
|
3758
|
+
const tb = document.getElementById('bulk-toolbar');
|
|
3759
|
+
const cnt = document.getElementById('bulk-count');
|
|
3760
|
+
if (!tb) return;
|
|
3761
|
+
tb.classList.toggle('show', bulkSelected.size > 0);
|
|
3762
|
+
if (cnt) cnt.textContent = bulkSelected.size + ' selected';
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
function clearBulkSelection() {
|
|
3766
|
+
bulkSelected.clear();
|
|
3767
|
+
document.querySelectorAll('#sess-content .sess-row.bulk-sel').forEach(r => r.classList.remove('bulk-sel'));
|
|
3768
|
+
updateBulkToolbar();
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
function bulkBookmark() {
|
|
3772
|
+
for (const id of bulkSelected) bookmarks.add(id);
|
|
3773
|
+
localStorage.setItem('mm-bookmarks', JSON.stringify([...bookmarks]));
|
|
3774
|
+
clearBulkSelection();
|
|
3775
|
+
showToast('Bookmarked', `${bulkSelected.size || 'Selected'} sessions bookmarked`, 'ok');
|
|
3776
|
+
viewRendered['sessions'] = false;
|
|
3777
|
+
renderSessions();
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
function bulkExport() {
|
|
3781
|
+
const toExport = allSessions.filter(s => bulkSelected.has(s.id));
|
|
3782
|
+
if (!toExport.length) return;
|
|
3783
|
+
const headers = ['Date', 'Session ID', 'Prompt', 'Cost ($)', 'Duration (s)', 'Tool Calls', 'Files Touched'];
|
|
3784
|
+
const rows = toExport.map(s => {
|
|
3785
|
+
const dt = new Date(s.firstTs || s.mtime || 0).toISOString().slice(0, 19).replace('T', ' ');
|
|
3786
|
+
const cost = typeof s.totalCost === 'number' ? s.totalCost.toFixed(4) : '';
|
|
3787
|
+
const dur = s.totalDurationMs ? Math.round(s.totalDurationMs / 1000) : '';
|
|
3788
|
+
const prompt = (s.lastPrompt || '').replace(/"/g, '""');
|
|
3789
|
+
const files = (s.filesTouched || []).join(';');
|
|
3790
|
+
return [dt, s.id, prompt, cost, dur, s.toolCalls || '', files];
|
|
3791
|
+
});
|
|
3792
|
+
const csv = [headers, ...rows].map(r => r.map(c => `"${c}"`).join(',')).join('\n');
|
|
3793
|
+
const blob = new Blob([csv], { type: 'text/csv' });
|
|
3794
|
+
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
|
|
3795
|
+
a.download = `sessions-bulk-${new Date().toISOString().slice(0,10)}.csv`; a.click();
|
|
3796
|
+
URL.revokeObjectURL(a.href);
|
|
3797
|
+
showToast('Exported', `${toExport.length} sessions saved`, 'ok');
|
|
3798
|
+
clearBulkSelection();
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3190
3801
|
// ── feature 26: token velocity sparkline ──────────────────
|
|
3191
3802
|
function buildTokenVelocity() {
|
|
3192
3803
|
const el = document.getElementById('m-velocity');
|
|
3193
3804
|
if (!el || !allSessions.length) return;
|
|
3194
3805
|
const now = Date.now();
|
|
3195
3806
|
const HOUR = 3600000;
|
|
3807
|
+
const filtered = periodFilteredSessions();
|
|
3196
3808
|
const buckets = new Array(24).fill(0);
|
|
3197
|
-
for (const s of
|
|
3809
|
+
for (const s of filtered) {
|
|
3198
3810
|
const t = s.firstTs || s.mtime;
|
|
3199
3811
|
if (!t) continue;
|
|
3200
3812
|
const ts = typeof t === 'number' ? t : new Date(t).getTime();
|
|
@@ -3212,10 +3824,11 @@ function buildTokenVelocity() {
|
|
|
3212
3824
|
const hrsAgo = 23 - i;
|
|
3213
3825
|
return `<div class="vel-bar ${cls}" style="height:${h}px" title="${fmt(v)} tokens — ${hrsAgo}h ago"></div>`;
|
|
3214
3826
|
}).join('');
|
|
3215
|
-
const totalCost =
|
|
3216
|
-
|
|
3827
|
+
const totalCost = filtered.reduce((a, s) => a + (s.totalCost || 0), 0);
|
|
3828
|
+
const periodLabel = { day:'24h', week:'7d', month:'30d', all:'all time' }[activePeriod] || '24h';
|
|
3829
|
+
el.innerHTML = `<div class="m-group-title">Token Velocity <span style="font-size:10px;color:var(--text-xs);font-weight:400">${periodLabel} · ${fmt(totalTok)}</span></div>
|
|
3217
3830
|
<div id="vel-chart">${bars}</div>
|
|
3218
|
-
<div style="font-size:10px;color:var(--text-xs);margin-top:4px">
|
|
3831
|
+
<div style="font-size:10px;color:var(--text-xs);margin-top:4px">Cost <span style="color:oklch(78% 0.18 75)">$${totalCost.toFixed(2)}</span> <span style="color:var(--text-xs)">· ${filtered.length} sessions</span></div>`;
|
|
3219
3832
|
}
|
|
3220
3833
|
|
|
3221
3834
|
// ── feature 27: export sessions CSV ───────────────────────
|
|
@@ -3317,6 +3930,254 @@ async function loadProjCosts() {
|
|
|
3317
3930
|
}
|
|
3318
3931
|
}
|
|
3319
3932
|
|
|
3933
|
+
// ── feature 47: 30-day daily cost trend ───────────────────
|
|
3934
|
+
function buildDailyCostTrend() {
|
|
3935
|
+
const el = document.getElementById('m-daily-trend');
|
|
3936
|
+
if (!el || !allSessions.length) return;
|
|
3937
|
+
const now = Date.now(); const DAY = 86400000;
|
|
3938
|
+
const buckets = new Array(30).fill(0);
|
|
3939
|
+
const dates = new Array(30).fill(null).map((_, i) => new Date(now - (29 - i) * DAY).toDateString());
|
|
3940
|
+
for (const s of allSessions) {
|
|
3941
|
+
const t = s.firstTs || s.mtime; if (!t) continue;
|
|
3942
|
+
const ts = typeof t === 'number' ? t : new Date(t).getTime();
|
|
3943
|
+
const daysAgo = Math.floor((now - ts) / DAY);
|
|
3944
|
+
const idx = 29 - daysAgo;
|
|
3945
|
+
if (idx >= 0 && idx < 30) buckets[idx] += s.totalCost || 0;
|
|
3946
|
+
}
|
|
3947
|
+
const maxCost = Math.max(0.001, ...buckets);
|
|
3948
|
+
const totalCost = buckets.reduce((a, b) => a + b, 0);
|
|
3949
|
+
const bars = buckets.map((v, i) => {
|
|
3950
|
+
const h = Math.max(2, Math.round((v / maxCost) * 38));
|
|
3951
|
+
const isActive = dates[i] === heatmapDateFilter;
|
|
3952
|
+
const label = i === 29 ? 'Today' : (i === 28 ? 'Yday' : '');
|
|
3953
|
+
return `<div class="dt-bar${isActive ? ' active' : ''}" style="height:${h}px" title="${dates[i]}: $${v.toFixed(3)}" onclick="setDailyTrendFilter('${dates[i]}',${v})"></div>`;
|
|
3954
|
+
}).join('');
|
|
3955
|
+
el.innerHTML = `<div class="m-group-title">30-Day Cost <span style="font-size:10px;color:var(--text-xs);font-weight:400">$${totalCost.toFixed(2)} total</span></div>
|
|
3956
|
+
<div id="daily-trend-chart">${bars}</div>
|
|
3957
|
+
<div style="display:flex;justify-content:space-between;font-size:9px;color:var(--text-xs);margin-top:2px"><span>30d ago</span><span>Today</span></div>`;
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
function setDailyTrendFilter(dateStr, cost) {
|
|
3961
|
+
if (!cost) return;
|
|
3962
|
+
heatmapDateFilter = heatmapDateFilter === dateStr ? null : dateStr;
|
|
3963
|
+
const clearBtn = document.getElementById('shm-clear');
|
|
3964
|
+
if (clearBtn) clearBtn.classList.toggle('show', !!heatmapDateFilter);
|
|
3965
|
+
viewRendered['sessions'] = false;
|
|
3966
|
+
renderSessions();
|
|
3967
|
+
syncURLParams();
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
// ── feature 48: live cost ticker ──────────────────────────
|
|
3971
|
+
let _liveTickerCost = 0;
|
|
3972
|
+
let _liveTickerPrev = 0;
|
|
3973
|
+
|
|
3974
|
+
function updateLiveTicker(cost) {
|
|
3975
|
+
const el = document.getElementById('live-cost-ticker');
|
|
3976
|
+
if (!el) return;
|
|
3977
|
+
if (cost == null || cost === 0) { el.classList.remove('show'); return; }
|
|
3978
|
+
_liveTickerPrev = _liveTickerCost;
|
|
3979
|
+
_liveTickerCost = cost;
|
|
3980
|
+
const delta = _liveTickerCost - _liveTickerPrev;
|
|
3981
|
+
const deltaHtml = delta > 0.0001 ? `<span class="lct-change">+$${delta.toFixed(4)}</span>` : '';
|
|
3982
|
+
el.innerHTML = `$${cost.toFixed(3)}${deltaHtml}`;
|
|
3983
|
+
el.classList.add('show');
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
// ── feature 49: hourly productivity heatmap ───────────────
|
|
3987
|
+
function buildHourlyHeatmap() {
|
|
3988
|
+
const el = document.getElementById('m-hourly-heatmap');
|
|
3989
|
+
if (!el || !allSessions.length) return;
|
|
3990
|
+
// 24 hours × 7 days-of-week matrix
|
|
3991
|
+
const grid = Array.from({ length: 7 }, () => new Array(24).fill(0));
|
|
3992
|
+
for (const s of allSessions) {
|
|
3993
|
+
const t = s.firstTs || s.mtime; if (!t) continue;
|
|
3994
|
+
const d = new Date(typeof t === 'number' ? t : t);
|
|
3995
|
+
const dow = d.getDay(); // 0=Sun
|
|
3996
|
+
const hour = d.getHours();
|
|
3997
|
+
grid[dow][hour]++;
|
|
3998
|
+
}
|
|
3999
|
+
const maxVal = Math.max(1, ...grid.flatMap(r => r));
|
|
4000
|
+
const dayNames = ['Su','Mo','Tu','We','Th','Fr','Sa'];
|
|
4001
|
+
let html = '<div id="hourly-heatmap-grid">';
|
|
4002
|
+
// Header row: empty corner + hour labels (0,6,12,18,23)
|
|
4003
|
+
html += '<div></div>';
|
|
4004
|
+
for (let h = 0; h < 24; h++) {
|
|
4005
|
+
const lbl = h % 6 === 0 ? String(h) : '';
|
|
4006
|
+
html += `<div class="hh-hour-lbl">${lbl}</div>`;
|
|
4007
|
+
}
|
|
4008
|
+
// Data rows
|
|
4009
|
+
for (let d = 0; d < 7; d++) {
|
|
4010
|
+
html += `<div class="hh-day-lbl">${dayNames[d]}</div>`;
|
|
4011
|
+
for (let h = 0; h < 24; h++) {
|
|
4012
|
+
const v = grid[d][h];
|
|
4013
|
+
const level = v === 0 ? 0 : Math.min(4, Math.ceil(v / maxVal * 4));
|
|
4014
|
+
html += `<div class="hh-cell hh-${level}" title="${dayNames[d]} ${h}:00 — ${v} session${v !== 1 ? 's' : ''}"></div>`;
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
html += '</div>';
|
|
4018
|
+
const peakHour = grid.flatMap((r, d) => r.map((v, h) => ({ d, h, v }))).sort((a, b) => b.v - a.v)[0];
|
|
4019
|
+
const peakLabel = peakHour ? `${dayNames[peakHour.d]} ${peakHour.h}:00` : '';
|
|
4020
|
+
el.innerHTML = `<div class="m-group-title">Peak Work Hours${peakLabel ? `<span style="font-size:10px;color:var(--text-xs);font-weight:400;margin-left:4px">peak: ${peakLabel}</span>` : ''}</div>${html}`;
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
// ── feature 50: custom tag editor ─────────────────────────
|
|
4024
|
+
const _customTagsKey = 'mm-custom-tags';
|
|
4025
|
+
let _customTagsMap = new Map(Object.entries(JSON.parse(localStorage.getItem(_customTagsKey) || '{}')));
|
|
4026
|
+
|
|
4027
|
+
function getCustomTags(sessId) {
|
|
4028
|
+
return _customTagsMap.get(sessId) || [];
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
function saveCustomTags(sessId, tags) {
|
|
4032
|
+
if (tags.length === 0) _customTagsMap.delete(sessId);
|
|
4033
|
+
else _customTagsMap.set(sessId, tags);
|
|
4034
|
+
localStorage.setItem(_customTagsKey, JSON.stringify(Object.fromEntries(_customTagsMap)));
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
function addCustomTag(sessId, tag, event) {
|
|
4038
|
+
if (event) event.stopPropagation();
|
|
4039
|
+
const t = tag.trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
4040
|
+
if (!t) return;
|
|
4041
|
+
const tags = getCustomTags(sessId);
|
|
4042
|
+
if (!tags.includes(t)) { tags.push(t); saveCustomTags(sessId, tags); }
|
|
4043
|
+
const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
|
|
4044
|
+
if (wrap) wrap.outerHTML = renderCustomTagsInline(sessId, tags);
|
|
4045
|
+
// rebuild tag filter if this tag is new globally
|
|
4046
|
+
initTags(); buildTagFilterBar(allSessions);
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
function removeCustomTag(sessId, tag, event) {
|
|
4050
|
+
if (event) event.stopPropagation();
|
|
4051
|
+
const tags = getCustomTags(sessId).filter(t => t !== tag);
|
|
4052
|
+
saveCustomTags(sessId, tags);
|
|
4053
|
+
const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
|
|
4054
|
+
if (wrap) wrap.outerHTML = renderCustomTagsInline(sessId, tags);
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
function showCustomTagInput(sessId, event) {
|
|
4058
|
+
if (event) event.stopPropagation();
|
|
4059
|
+
const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
|
|
4060
|
+
if (!wrap) return;
|
|
4061
|
+
const existing = wrap.querySelector('.ctag-input-wrap');
|
|
4062
|
+
if (existing) { existing.remove(); return; }
|
|
4063
|
+
const iw = document.createElement('div');
|
|
4064
|
+
iw.className = 'ctag-input-wrap';
|
|
4065
|
+
iw.onclick = e => e.stopPropagation();
|
|
4066
|
+
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>`;
|
|
4068
|
+
wrap.appendChild(iw);
|
|
4069
|
+
const inp = iw.querySelector('input');
|
|
4070
|
+
inp.focus();
|
|
4071
|
+
inp.addEventListener('keydown', e => {
|
|
4072
|
+
if (e.key === 'Enter') { addCustomTag(sessId, inp.value, e); inp.value = ''; }
|
|
4073
|
+
if (e.key === 'Escape') iw.remove();
|
|
4074
|
+
});
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
function renderCustomTagsInline(sessId, tags) {
|
|
4078
|
+
const tagHtml = tags.map(t =>
|
|
4079
|
+
`<span class="sr-ctag">${esc(t)}<span class="ctag-del" onclick="removeCustomTag('${esc(sessId)}','${esc(t)}',event)" title="Remove tag">✕</span></span>`
|
|
4080
|
+
).join('');
|
|
4081
|
+
return `<div class="sr-custom-tags" data-sess="${esc(sessId)}" onclick="event.stopPropagation()">
|
|
4082
|
+
${tagHtml}
|
|
4083
|
+
<button class="ctag-add-btn" onclick="showCustomTagInput('${esc(sessId)}',event)" title="Add tag">+ tag</button>
|
|
4084
|
+
</div>`;
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
// ── feature 51: tool error drawer ─────────────────────────
|
|
4088
|
+
async function toggleErrDrawer(sessId, event) {
|
|
4089
|
+
if (event) event.stopPropagation();
|
|
4090
|
+
const drawer = document.getElementById('err-drawer-' + sessId);
|
|
4091
|
+
if (!drawer) return;
|
|
4092
|
+
if (drawer.classList.contains('open')) { drawer.classList.remove('open'); drawer.innerHTML = ''; return; }
|
|
4093
|
+
drawer.classList.add('open');
|
|
4094
|
+
drawer.innerHTML = '<div class="err-drawer-head">Loading errors…</div>';
|
|
4095
|
+
try {
|
|
4096
|
+
const data = await apiFetch('/api/session-errors?dir=' + enc(DIR) + '&id=' + enc(sessId));
|
|
4097
|
+
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>`;
|
|
4099
|
+
return;
|
|
4100
|
+
}
|
|
4101
|
+
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>
|
|
4103
|
+
<div class="err-drawer-body">${items}</div>`;
|
|
4104
|
+
} 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>`;
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
// ── feature 52: prompt copy button ────────────────────────
|
|
4110
|
+
function copyPrompt(text, event) {
|
|
4111
|
+
if (event) event.stopPropagation();
|
|
4112
|
+
const btn = event?.currentTarget || event?.target;
|
|
4113
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
4114
|
+
if (btn) { btn.textContent = '✓'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = '⎘'; btn.classList.remove('copied'); }, 1500); }
|
|
4115
|
+
}).catch(() => {});
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
// ── feature 53: session cost histogram ────────────────────
|
|
4119
|
+
function buildCostHistogram() {
|
|
4120
|
+
const el = document.getElementById('cost-histogram-panel');
|
|
4121
|
+
if (!el) return;
|
|
4122
|
+
const costs = allSessions.map(s => s.totalCost || 0).filter(c => c > 0);
|
|
4123
|
+
if (costs.length < 2) { el.style.display = 'none'; return; }
|
|
4124
|
+
el.style.display = 'block';
|
|
4125
|
+
const minC = Math.min(...costs); const maxC = Math.max(...costs);
|
|
4126
|
+
const BUCKETS = 10;
|
|
4127
|
+
const range = maxC - minC || 0.01;
|
|
4128
|
+
const bucketSize = range / BUCKETS;
|
|
4129
|
+
const counts = new Array(BUCKETS).fill(0);
|
|
4130
|
+
for (const c of costs) { const i = Math.min(BUCKETS - 1, Math.floor((c - minC) / bucketSize)); counts[i]++; }
|
|
4131
|
+
const maxCount = Math.max(1, ...counts);
|
|
4132
|
+
const fmt = v => v < 0.01 ? v.toFixed(4) : v < 1 ? '$' + v.toFixed(2) : '$' + v.toFixed(1);
|
|
4133
|
+
const bars = counts.map((n, i) => {
|
|
4134
|
+
const h = Math.max(2, Math.round((n / maxCount) * 46));
|
|
4135
|
+
const lo = minC + i * bucketSize; const hi = lo + bucketSize;
|
|
4136
|
+
return `<div class="ch-bar-wrap" title="${fmt(lo)}–${fmt(hi)}: ${n} session${n !== 1 ? 's' : ''}">
|
|
4137
|
+
<div class="ch-cnt">${n || ''}</div>
|
|
4138
|
+
<div class="ch-bar" style="height:${h}px"></div>
|
|
4139
|
+
<div class="ch-lbl">${i === 0 ? fmt(lo) : i === BUCKETS - 1 ? fmt(hi) : ''}</div>
|
|
4140
|
+
</div>`;
|
|
4141
|
+
}).join('');
|
|
4142
|
+
el.innerHTML = `<div class="ch-title">Cost Distribution — ${costs.length} sessions</div>
|
|
4143
|
+
<div class="ch-bars">${bars}</div>`;
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
// ── feature 54: persistent filter URL params ──────────────
|
|
4147
|
+
function syncURLParams() {
|
|
4148
|
+
const p = new URLSearchParams(window.location.search);
|
|
4149
|
+
if (DIR) p.set('proj', DIR); else p.delete('proj');
|
|
4150
|
+
if (activePeriod && activePeriod !== 'day') p.set('period', activePeriod); else p.delete('period');
|
|
4151
|
+
if (activeTagFilter) p.set('tag', activeTagFilter); else p.delete('tag');
|
|
4152
|
+
if (heatmapDateFilter) p.set('date', heatmapDateFilter); else p.delete('date');
|
|
4153
|
+
if (showStarredOnly) p.set('starred', '1'); else p.delete('starred');
|
|
4154
|
+
const newUrl = window.location.pathname + (p.toString() ? '?' + p.toString() : '');
|
|
4155
|
+
history.replaceState(null, '', newUrl);
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
function restoreURLParams() {
|
|
4159
|
+
const p = new URLSearchParams(window.location.search);
|
|
4160
|
+
const period = p.get('period');
|
|
4161
|
+
if (period && ['day','week','month','all'].includes(period)) {
|
|
4162
|
+
activePeriod = period;
|
|
4163
|
+
document.querySelectorAll('.period-btn').forEach(b => b.classList.toggle('active', b.dataset.period === period));
|
|
4164
|
+
}
|
|
4165
|
+
const tag = p.get('tag');
|
|
4166
|
+
if (tag) activeTagFilter = tag;
|
|
4167
|
+
const date = p.get('date');
|
|
4168
|
+
if (date) {
|
|
4169
|
+
heatmapDateFilter = date;
|
|
4170
|
+
const clearBtn = document.getElementById('shm-clear');
|
|
4171
|
+
if (clearBtn) clearBtn.classList.add('show');
|
|
4172
|
+
}
|
|
4173
|
+
const starred = p.get('starred');
|
|
4174
|
+
if (starred === '1') {
|
|
4175
|
+
showStarredOnly = true;
|
|
4176
|
+
const btn = document.getElementById('sess-star-filter');
|
|
4177
|
+
if (btn) btn.classList.add('on');
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
|
|
3320
4181
|
// ── helpers ────────────────────────────────────────────────
|
|
3321
4182
|
function enc(s) { return encodeURIComponent(s); }
|
|
3322
4183
|
function esc(s) {
|