@monoes/monomindcli 1.10.37 → 1.10.39
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/.claude/helpers/utils/telemetry.cjs +1 -0
- package/dist/src/browser/actions.d.ts +1 -0
- package/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +12 -5
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/batch.d.ts +0 -6
- package/dist/src/browser/batch.d.ts.map +1 -1
- package/dist/src/browser/batch.js.map +1 -1
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +37 -20
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +14 -4
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts +4 -4
- package/dist/src/browser/console-log.d.ts.map +1 -1
- package/dist/src/browser/console-log.js +49 -16
- package/dist/src/browser/console-log.js.map +1 -1
- package/dist/src/browser/dialog.d.ts +2 -1
- package/dist/src/browser/dialog.d.ts.map +1 -1
- package/dist/src/browser/dialog.js +24 -10
- package/dist/src/browser/dialog.js.map +1 -1
- package/dist/src/browser/find.d.ts +1 -1
- package/dist/src/browser/find.d.ts.map +1 -1
- package/dist/src/browser/find.js +29 -10
- package/dist/src/browser/find.js.map +1 -1
- package/dist/src/browser/har.d.ts.map +1 -1
- package/dist/src/browser/har.js +3 -3
- package/dist/src/browser/har.js.map +1 -1
- package/dist/src/browser/network.d.ts +2 -0
- package/dist/src/browser/network.d.ts.map +1 -1
- package/dist/src/browser/network.js +64 -23
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/profiler.d.ts.map +1 -1
- package/dist/src/browser/profiler.js +25 -15
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/browser/record.d.ts.map +1 -1
- package/dist/src/browser/record.js +7 -3
- package/dist/src/browser/record.js.map +1 -1
- package/dist/src/browser/session.d.ts.map +1 -1
- package/dist/src/browser/session.js +11 -7
- package/dist/src/browser/session.js.map +1 -1
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/trace.d.ts.map +1 -1
- package/dist/src/browser/trace.js +32 -17
- package/dist/src/browser/trace.js.map +1 -1
- package/dist/src/browser/wait.js +28 -14
- 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 +8 -14
- package/dist/src/commands/browse.js.map +1 -1
- package/dist/src/ui/dashboard-v2.html +563 -5
- package/dist/src/ui/server.mjs +140 -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; }
|
|
@@ -436,6 +499,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
436
499
|
#app.ambient #feed-recap,
|
|
437
500
|
#app.ambient #feed-timeline,
|
|
438
501
|
#app.ambient #digest-card { display:none !important; }
|
|
502
|
+
#app.ambient #weekly-card { display:none !important; }
|
|
439
503
|
#app.ambient #main { background:var(--bg); }
|
|
440
504
|
#app.ambient #view-now { height:100vh; }
|
|
441
505
|
#app.ambient #feed-pane { border:none; }
|
|
@@ -597,6 +661,59 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
597
661
|
.bm-save:hover { background:oklch(68% 0.18 75); }
|
|
598
662
|
.bm-cancel { background:transparent; border:1px solid var(--border); border-radius:var(--r); padding:7px 12px; font-size:13px; color:var(--text-lo); cursor:pointer; }
|
|
599
663
|
.bm-cancel:hover { color:var(--text-hi); }
|
|
664
|
+
|
|
665
|
+
/* ── toast notifications ──────────────────────────────────── */
|
|
666
|
+
#toast-rack { position:fixed; bottom:20px; right:20px; z-index:9999; display:flex; flex-direction:column; gap:8px; pointer-events:none; }
|
|
667
|
+
.toast { display:flex; align-items:flex-start; gap:10px; padding:10px 14px; border-radius:8px; font-size:12px; max-width:300px; pointer-events:all; background:oklch(18% 0.012 55); border:1px solid var(--border); color:var(--text-mid); box-shadow:0 4px 24px rgba(0,0,0,0.4); animation:toast-in 0.25s var(--ease-out); }
|
|
668
|
+
.toast.t-warn { border-color:oklch(72% 0.18 75 / 0.4); background:oklch(18% 0.015 75); }
|
|
669
|
+
.toast.t-err { border-color:oklch(60% 0.18 25 / 0.4); background:oklch(17% 0.015 25); }
|
|
670
|
+
.toast.t-ok { border-color:oklch(65% 0.15 150 / 0.4); background:oklch(17% 0.012 150); }
|
|
671
|
+
.toast-ico { flex-shrink:0; font-size:14px; }
|
|
672
|
+
.toast-body { flex:1; min-width:0; }
|
|
673
|
+
.toast-title { font-weight:600; color:var(--text-hi); margin-bottom:2px; }
|
|
674
|
+
.toast-msg { color:var(--text-lo); line-height:1.4; }
|
|
675
|
+
.toast-close { background:none; border:none; color:var(--text-xs); cursor:pointer; font-size:12px; line-height:1; padding:0; flex-shrink:0; }
|
|
676
|
+
.toast-close:hover { color:var(--text-lo); }
|
|
677
|
+
@keyframes toast-in { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
|
|
678
|
+
|
|
679
|
+
/* ── token velocity sparkline ─────────────────────────────── */
|
|
680
|
+
#vel-chart { display:flex; align-items:flex-end; gap:2px; height:28px; margin-top:6px; }
|
|
681
|
+
.vel-bar { flex:1; border-radius:2px 2px 0 0; min-height:2px; background:oklch(65% 0.18 200 / 0.55); }
|
|
682
|
+
.vel-bar.vel-hi { background:oklch(65% 0.18 150 / 0.8); }
|
|
683
|
+
.vel-bar.vel-lo { background:oklch(65% 0.08 200 / 0.3); }
|
|
684
|
+
|
|
685
|
+
/* ── shortcut help modal ──────────────────────────────────── */
|
|
686
|
+
#shortcut-modal { display:none; position:fixed; inset:0; z-index:500; background:oklch(0% 0 0 / 0.6); align-items:center; justify-content:center; }
|
|
687
|
+
#shortcut-modal.open { display:flex; }
|
|
688
|
+
#shortcut-box { background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:24px 28px; max-width:460px; width:90%; max-height:80vh; overflow-y:auto; }
|
|
689
|
+
.sk-title { font-size:14px; font-weight:600; color:var(--text-hi); margin-bottom:16px; display:flex; justify-content:space-between; align-items:center; }
|
|
690
|
+
.sk-close { background:none; border:none; color:var(--text-xs); cursor:pointer; font-size:16px; padding:0; }
|
|
691
|
+
.sk-close:hover { color:var(--text-lo); }
|
|
692
|
+
.sk-section { font-size:10px; letter-spacing:0.08em; text-transform:uppercase; color:var(--text-xs); margin:14px 0 6px; }
|
|
693
|
+
.sk-row { display:flex; justify-content:space-between; align-items:center; padding:5px 0; border-bottom:1px solid oklch(25% 0.008 55 / 0.4); }
|
|
694
|
+
.sk-row:last-child { border-bottom:none; }
|
|
695
|
+
.sk-keys { display:flex; gap:3px; }
|
|
696
|
+
.sk-keys kbd { font-size:10px; background:var(--surface-hi); border:1px solid var(--border); border-radius:3px; padding:1px 6px; font-family:var(--mono); color:var(--text-lo); }
|
|
697
|
+
.sk-desc { font-size:11px; color:var(--text-mid); }
|
|
698
|
+
|
|
699
|
+
/* ── session filter ───────────────────────────────────────── */
|
|
700
|
+
#sess-filter-wrap { display:flex; align-items:center; gap:8px; margin-bottom:10px; }
|
|
701
|
+
#sess-filter-input { flex:1; background:var(--surface-hi); border:1px solid var(--border); border-radius:var(--r); padding:5px 10px; font-size:12px; color:var(--text-hi); outline:none; font-family:var(--sans); }
|
|
702
|
+
#sess-filter-input:focus { border-color:oklch(72% 0.18 75 / 0.5); }
|
|
703
|
+
#sess-filter-input::placeholder { color:var(--text-xs); }
|
|
704
|
+
#sess-filter-count { font-size:11px; color:var(--text-xs); white-space:nowrap; min-width:60px; text-align:right; }
|
|
705
|
+
|
|
706
|
+
/* ── topbar activity chip ─────────────────────────────────── */
|
|
707
|
+
#topbar-activity { font-size:10px; color:var(--text-xs); font-family:var(--mono); max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; opacity:0; transition:opacity 0.3s; }
|
|
708
|
+
#topbar-activity.loaded { opacity:1; }
|
|
709
|
+
|
|
710
|
+
/* ── anomaly badge on session rows ────────────────────────── */
|
|
711
|
+
.sess-anomaly { display:inline-flex; align-items:center; font-size:10px; padding:1px 5px; border-radius:5px; font-weight:600; margin-left:4px; vertical-align:middle; }
|
|
712
|
+
.sess-anomaly.anom-cost { background:oklch(60% 0.18 25 / 0.12); color:oklch(70% 0.18 25); }
|
|
713
|
+
.sess-anomaly.anom-err { background:oklch(65% 0.15 300 / 0.12); color:oklch(72% 0.15 300); }
|
|
714
|
+
|
|
715
|
+
/* ── streak badge ─────────────────────────────────────────── */
|
|
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; }
|
|
600
717
|
</style>
|
|
601
718
|
</head>
|
|
602
719
|
<body>
|
|
@@ -654,9 +771,11 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
654
771
|
<span id="view-title">Now</span>
|
|
655
772
|
<span class="pill"><span class="live-dot"></span> live</span>
|
|
656
773
|
<span id="topbar-cost"></span>
|
|
774
|
+
<span id="topbar-activity"></span>
|
|
657
775
|
<div id="tb-right">
|
|
658
776
|
<button class="btn" id="btn-budget" onclick="openBudgetModal()" title="Set daily/monthly cost budget">⚑ Budget</button>
|
|
659
777
|
<button class="btn" onclick="openCmdPalette()">⌕ Search <kbd style="font-size:10px;opacity:0.6;margin-left:3px">⌘K</kbd></button>
|
|
778
|
+
<button class="btn" onclick="openShortcutHelp()" title="Keyboard shortcuts (?)">? Help</button>
|
|
660
779
|
<button class="btn" onclick="refreshCurrent()">↺ Refresh</button>
|
|
661
780
|
</div>
|
|
662
781
|
</div>
|
|
@@ -792,6 +911,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
792
911
|
<div class="m-group-title">Activity Heatmap</div>
|
|
793
912
|
<div class="loading-txt">—</div>
|
|
794
913
|
</div>
|
|
914
|
+
<div id="m-velocity">
|
|
915
|
+
<div class="m-group-title">Token Velocity</div>
|
|
916
|
+
<div class="loading-txt">—</div>
|
|
917
|
+
</div>
|
|
795
918
|
</div>
|
|
796
919
|
</div>
|
|
797
920
|
|
|
@@ -816,6 +939,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
816
939
|
<button class="lb-toggle" id="btn-leaderboard" onclick="toggleLeaderboard()" title="Cost leaderboard">⬆ Leaderboard</button>
|
|
817
940
|
<button class="lb-toggle" id="btn-model-mix" onclick="toggleModelMix()" title="Model usage breakdown">⬡ Models</button>
|
|
818
941
|
<button class="lb-toggle" id="btn-tool-errors" onclick="toggleToolErrors()" title="Tool error rate">⚠ Errors</button>
|
|
942
|
+
<button class="lb-toggle" id="btn-tool-rank" onclick="toggleToolRank()" title="Most-used tools across sessions">⟳ Tools</button>
|
|
943
|
+
<button class="lb-toggle" id="btn-proj-costs" onclick="toggleProjCosts()" title="Cost breakdown by project">$ Projects</button>
|
|
944
|
+
<button class="lb-toggle" id="btn-export-csv" onclick="exportSessionsCSV()" title="Export sessions as CSV">⬇ CSV</button>
|
|
945
|
+
<button class="lb-toggle" id="btn-patterns" onclick="togglePatterns()" title="Prompt word frequency across sessions">⊞ Patterns</button>
|
|
819
946
|
<button class="diff-toggle" id="btn-diff" onclick="toggleDiffMode()" title="Compare two sessions">⇄ Compare</button>
|
|
820
947
|
</div>
|
|
821
948
|
<div class="pg-sub" id="sess-pg-sub">Recent Claude Code sessions for this project</div>
|
|
@@ -835,6 +962,36 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
835
962
|
<div id="tool-errors-panel" style="display:none;margin-bottom:16px">
|
|
836
963
|
<div id="tool-errors-body"></div>
|
|
837
964
|
</div>
|
|
965
|
+
<div id="tool-rank-panel" style="display:none;margin-bottom:16px">
|
|
966
|
+
<div id="tool-rank-body"></div>
|
|
967
|
+
</div>
|
|
968
|
+
<div id="proj-costs-panel" style="display:none;margin-bottom:16px">
|
|
969
|
+
<div id="proj-costs-body"></div>
|
|
970
|
+
</div>
|
|
971
|
+
<div id="patterns-panel" style="display:none;margin-bottom:16px">
|
|
972
|
+
<div id="patterns-body"></div>
|
|
973
|
+
</div>
|
|
974
|
+
<div id="sess-heatmap" style="margin-bottom:14px;display:none">
|
|
975
|
+
<div class="shm-label"><span>12-week activity</span><button id="shm-clear" onclick="clearHeatmapFilter()">✕ Clear filter</button></div>
|
|
976
|
+
<div class="shm-grid" id="shm-grid"></div>
|
|
977
|
+
</div>
|
|
978
|
+
<div class="period-toggles" id="period-toggles">
|
|
979
|
+
<span style="font-size:10px;color:var(--text-xs);align-self:center;text-transform:uppercase;letter-spacing:0.06em">Period:</span>
|
|
980
|
+
<button class="period-btn active" data-period="day" onclick="setPeriod('day')">Day</button>
|
|
981
|
+
<button class="period-btn" data-period="week" onclick="setPeriod('week')">Week</button>
|
|
982
|
+
<button class="period-btn" data-period="month" onclick="setPeriod('month')">Month</button>
|
|
983
|
+
<button class="period-btn" data-period="all" onclick="setPeriod('all')">All</button>
|
|
984
|
+
</div>
|
|
985
|
+
<div id="bulk-toolbar">
|
|
986
|
+
<span class="bulk-count" id="bulk-count">0 selected</span>
|
|
987
|
+
<button class="bulk-btn" onclick="bulkExport()">⬇ Export</button>
|
|
988
|
+
<button class="bulk-btn" onclick="bulkBookmark()">☆ Bookmark all</button>
|
|
989
|
+
<button class="bulk-btn danger" onclick="clearBulkSelection()">✕ Clear</button>
|
|
990
|
+
</div>
|
|
991
|
+
<div id="sess-filter-wrap">
|
|
992
|
+
<input id="sess-filter-input" type="text" placeholder="Filter sessions by prompt…" oninput="filterSessions(this.value)" autocomplete="off">
|
|
993
|
+
<span id="sess-filter-count"></span>
|
|
994
|
+
</div>
|
|
838
995
|
<div id="sess-content" class="sess-list"><div class="loading-txt">Loading…</div></div>
|
|
839
996
|
</div>
|
|
840
997
|
</div>
|
|
@@ -844,6 +1001,32 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
844
1001
|
<div class="vscroll">
|
|
845
1002
|
<div class="pg-title">Loops</div>
|
|
846
1003
|
<div class="pg-sub">Scheduled automation loops</div>
|
|
1004
|
+
<button id="btn-new-loop" onclick="showLoopForm()">+ New Loop</button>
|
|
1005
|
+
<div id="loop-create-form" style="display:none">
|
|
1006
|
+
<div class="lcf-title">Create Loop</div>
|
|
1007
|
+
<div class="lcf-row">
|
|
1008
|
+
<label class="lcf-label">Prompt</label>
|
|
1009
|
+
<textarea class="lcf-textarea" id="lcf-prompt" placeholder="What should the agent do each iteration?"></textarea>
|
|
1010
|
+
</div>
|
|
1011
|
+
<div class="lcf-row">
|
|
1012
|
+
<label class="lcf-label">Name (optional)</label>
|
|
1013
|
+
<input class="lcf-input" id="lcf-name" type="text" placeholder="My loop">
|
|
1014
|
+
</div>
|
|
1015
|
+
<div class="lcf-row-inline">
|
|
1016
|
+
<div class="lcf-row">
|
|
1017
|
+
<label class="lcf-label">Interval</label>
|
|
1018
|
+
<input class="lcf-input" id="lcf-interval" type="text" placeholder="1h" value="1h">
|
|
1019
|
+
</div>
|
|
1020
|
+
<div class="lcf-row">
|
|
1021
|
+
<label class="lcf-label">Max reps (blank = ∞)</label>
|
|
1022
|
+
<input class="lcf-input" id="lcf-maxreps" type="number" placeholder="∞" min="1">
|
|
1023
|
+
</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
<div class="lcf-actions">
|
|
1026
|
+
<button class="lcf-cancel" onclick="hideLoopForm()">Cancel</button>
|
|
1027
|
+
<button class="lcf-submit" onclick="createLoop()">Create Loop</button>
|
|
1028
|
+
</div>
|
|
1029
|
+
</div>
|
|
847
1030
|
<div id="loops-content" class="loop-list"><div class="loading-txt">Loading…</div></div>
|
|
848
1031
|
</div>
|
|
849
1032
|
</div>
|
|
@@ -885,6 +1068,25 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
885
1068
|
</div><!-- /main -->
|
|
886
1069
|
<div id="app-ambient-hint">Press A to exit ambient mode</div>
|
|
887
1070
|
</div><!-- /app -->
|
|
1071
|
+
<div id="toast-rack"></div>
|
|
1072
|
+
|
|
1073
|
+
<!-- shortcut help modal -->
|
|
1074
|
+
<div id="shortcut-modal" onclick="if(event.target===this)closeShortcutHelp()">
|
|
1075
|
+
<div id="shortcut-box">
|
|
1076
|
+
<div class="sk-title">Keyboard Shortcuts <button class="sk-close" onclick="closeShortcutHelp()">✕</button></div>
|
|
1077
|
+
<div class="sk-section">Feed (Now view)</div>
|
|
1078
|
+
<div class="sk-row"><span class="sk-desc">Navigate entries</span><span class="sk-keys"><kbd>J</kbd><kbd>K</kbd></span></div>
|
|
1079
|
+
<div class="sk-row"><span class="sk-desc">Open detail drawer</span><span class="sk-keys"><kbd>↵</kbd></span></div>
|
|
1080
|
+
<div class="sk-row"><span class="sk-desc">Search in feed</span><span class="sk-keys"><kbd>/</kbd></span></div>
|
|
1081
|
+
<div class="sk-row"><span class="sk-desc">Jump to live session</span><span class="sk-keys"><kbd>G</kbd></span></div>
|
|
1082
|
+
<div class="sk-row"><span class="sk-desc">Refresh current view</span><span class="sk-keys"><kbd>R</kbd></span></div>
|
|
1083
|
+
<div class="sk-row"><span class="sk-desc">Toggle ambient mode</span><span class="sk-keys"><kbd>A</kbd></span></div>
|
|
1084
|
+
<div class="sk-section">Global</div>
|
|
1085
|
+
<div class="sk-row"><span class="sk-desc">Command palette</span><span class="sk-keys"><kbd>⌘</kbd><kbd>K</kbd></span></div>
|
|
1086
|
+
<div class="sk-row"><span class="sk-desc">Close / dismiss</span><span class="sk-keys"><kbd>Esc</kbd></span></div>
|
|
1087
|
+
<div class="sk-row"><span class="sk-desc">This help</span><span class="sk-keys"><kbd>?</kbd></span></div>
|
|
1088
|
+
</div>
|
|
1089
|
+
</div>
|
|
888
1090
|
|
|
889
1091
|
<!-- budget modal (fixed overlay, outside app) -->
|
|
890
1092
|
<div id="budget-modal" onclick="if(event.target===this)closeBudgetModal()">
|
|
@@ -975,9 +1177,19 @@ async function init() {
|
|
|
975
1177
|
document.getElementById('sb-path').textContent = DIR;
|
|
976
1178
|
document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
|
|
977
1179
|
} catch (_) {}
|
|
1180
|
+
// deep-link: ?sess=<id>&proj=<path>
|
|
1181
|
+
const params = new URLSearchParams(window.location.search);
|
|
1182
|
+
const projParam = params.get('proj');
|
|
1183
|
+
const sessParam = params.get('sess');
|
|
1184
|
+
if (projParam) {
|
|
1185
|
+
DIR = projParam;
|
|
1186
|
+
document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
|
|
1187
|
+
document.getElementById('sb-path').textContent = DIR;
|
|
1188
|
+
}
|
|
978
1189
|
viewRendered['now'] = true; // prevents switchView from re-rendering NOW on jumpToSession
|
|
979
1190
|
updateBudgetBtnStyle();
|
|
980
1191
|
await refreshNow();
|
|
1192
|
+
if (sessParam) setTimeout(() => jumpToSession(sessParam), 300);
|
|
981
1193
|
startPolling();
|
|
982
1194
|
}
|
|
983
1195
|
|
|
@@ -1197,6 +1409,8 @@ function renderFeedEvents(events, silent) {
|
|
|
1197
1409
|
updateBurnRate(filtered);
|
|
1198
1410
|
// session recap card
|
|
1199
1411
|
buildRecap(filtered, allSessions[sessionIdx]);
|
|
1412
|
+
// infer current activity from most recent tool
|
|
1413
|
+
updateCurrentActivity(visible);
|
|
1200
1414
|
}
|
|
1201
1415
|
|
|
1202
1416
|
function renderGroupRow(g) {
|
|
@@ -1568,6 +1782,9 @@ async function renderSessions() {
|
|
|
1568
1782
|
el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
|
|
1569
1783
|
return;
|
|
1570
1784
|
}
|
|
1785
|
+
// compute median cost for anomaly detection
|
|
1786
|
+
const costsForMedian = sessions.map(s => s.totalCost || 0).filter(c => c > 0).sort((a, b) => a - b);
|
|
1787
|
+
const medianCost = costsForMedian.length ? costsForMedian[Math.floor(costsForMedian.length / 2)] : 0;
|
|
1571
1788
|
const sessData = JSON.stringify(toShow).replace(/'/g, ''');
|
|
1572
1789
|
el.innerHTML = toShow.map((s, idx) => {
|
|
1573
1790
|
const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
|
|
@@ -1575,6 +1792,14 @@ async function renderSessions() {
|
|
|
1575
1792
|
const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2)
|
|
1576
1793
|
: typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '';
|
|
1577
1794
|
const meta = [dur, msgs, cost].filter(Boolean).join(' · ') || s.id.slice(0, 16);
|
|
1795
|
+
// anomaly badge
|
|
1796
|
+
const sCost = s.totalCost || 0;
|
|
1797
|
+
let anomBadge = '';
|
|
1798
|
+
if (medianCost > 0.05 && sCost > medianCost * 3 && sCost > 0.5) {
|
|
1799
|
+
anomBadge = `<span class="sess-anomaly anom-cost" title="Cost ${((sCost/medianCost).toFixed(1))}× above median">! costly</span>`;
|
|
1800
|
+
} else if (s.toolCalls > 0 && (s.errorCount || 0) / s.toolCalls > 0.3) {
|
|
1801
|
+
anomBadge = `<span class="sess-anomaly anom-err" title="${s.errorCount} tool errors">! errors</span>`;
|
|
1802
|
+
}
|
|
1578
1803
|
const satPct = Math.min(100, Math.round((s.totalMessages || 0) / 200 * 100));
|
|
1579
1804
|
const satColor = satPct > 80 ? 'oklch(65% 0.2 25)' : satPct > 50 ? 'oklch(70% 0.18 80)' : 'var(--accent)';
|
|
1580
1805
|
const satBar = satPct > 0 ? `<div class="ctx-sat-wrap" title="Context saturation ~${satPct}% (${s.totalMessages||0} turns / 200 est. max)">
|
|
@@ -1593,7 +1818,7 @@ async function renderSessions() {
|
|
|
1593
1818
|
<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>
|
|
1594
1819
|
<span class="sr-view">→ view</span>
|
|
1595
1820
|
</div>
|
|
1596
|
-
<div class="sr-meta">${esc(meta)}</div>
|
|
1821
|
+
<div class="sr-meta">${esc(meta)}${anomBadge}</div>
|
|
1597
1822
|
${(summaries || autoTags) ? `<div class="sr-tags">${summaries}${autoTags}</div>` : ''}
|
|
1598
1823
|
${satBar}
|
|
1599
1824
|
<div class="sess-notes-wrap" onclick="event.stopPropagation()">
|
|
@@ -1613,7 +1838,24 @@ async function renderSessions() {
|
|
|
1613
1838
|
buildWeeklyRecap();
|
|
1614
1839
|
buildEfficiencyPanel();
|
|
1615
1840
|
buildActivityHeatmap();
|
|
1841
|
+
buildTokenVelocity();
|
|
1616
1842
|
if (leaderboardOpen) renderLeaderboard();
|
|
1843
|
+
// check budget thresholds and fire toasts
|
|
1844
|
+
const todayCost = allSessions.filter(s => {
|
|
1845
|
+
const t = s.firstTs || s.mtime;
|
|
1846
|
+
if (!t) return false;
|
|
1847
|
+
const d = new Date(typeof t === 'number' ? t : t);
|
|
1848
|
+
const now = new Date();
|
|
1849
|
+
return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
|
|
1850
|
+
}).reduce((a, s) => a + (s.totalCost || 0), 0);
|
|
1851
|
+
const monthCost = allSessions.filter(s => {
|
|
1852
|
+
const t = s.firstTs || s.mtime;
|
|
1853
|
+
if (!t) return false;
|
|
1854
|
+
const d = new Date(typeof t === 'number' ? t : t);
|
|
1855
|
+
const now = new Date();
|
|
1856
|
+
return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
|
|
1857
|
+
}).reduce((a, s) => a + (s.totalCost || 0), 0);
|
|
1858
|
+
checkBudgetToast(todayCost, monthCost);
|
|
1617
1859
|
} catch (err) {
|
|
1618
1860
|
el.innerHTML = '<div class="empty">Could not load sessions: ' + esc(err.message) + '</div>';
|
|
1619
1861
|
}
|
|
@@ -1621,6 +1863,7 @@ async function renderSessions() {
|
|
|
1621
1863
|
|
|
1622
1864
|
function jumpToSession(id) {
|
|
1623
1865
|
switchView('now');
|
|
1866
|
+
history.replaceState(null, '', '?sess=' + encodeURIComponent(id) + '&proj=' + encodeURIComponent(DIR));
|
|
1624
1867
|
setTimeout(() => {
|
|
1625
1868
|
const i = allSessions.findIndex(x => x.id === id);
|
|
1626
1869
|
if (i >= 0) { sessionIdx = i; userScrolled = false; loadFeedForSession(allSessions[i]); }
|
|
@@ -2004,8 +2247,21 @@ function buildDigest() {
|
|
|
2004
2247
|
...themes.map(t => `#${t}`),
|
|
2005
2248
|
].filter(Boolean);
|
|
2006
2249
|
|
|
2250
|
+
// cost forecast: project monthly spend from daily average
|
|
2251
|
+
const today2 = new Date();
|
|
2252
|
+
const daysInMonth = new Date(today2.getFullYear(), today2.getMonth() + 1, 0).getDate();
|
|
2253
|
+
const dayOfMonth = today2.getDate();
|
|
2254
|
+
const monthCostSoFar = allSessions.filter(s => {
|
|
2255
|
+
const t = s.firstTs || s.mtime;
|
|
2256
|
+
if (!t) return false;
|
|
2257
|
+
const d = new Date(typeof t === 'number' ? t : t);
|
|
2258
|
+
return d.getFullYear() === today2.getFullYear() && d.getMonth() === today2.getMonth();
|
|
2259
|
+
}).reduce((a, s) => a + (s.totalCost || 0), 0);
|
|
2260
|
+
const dailyAvg = dayOfMonth > 0 ? monthCostSoFar / dayOfMonth : 0;
|
|
2261
|
+
const projected = dailyAvg * daysInMonth;
|
|
2262
|
+
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>` : '';
|
|
2007
2263
|
document.getElementById('digest-stats').innerHTML =
|
|
2008
|
-
stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('');
|
|
2264
|
+
stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('') + forecastHtml;
|
|
2009
2265
|
document.getElementById('digest-card').classList.add('show');
|
|
2010
2266
|
}
|
|
2011
2267
|
|
|
@@ -2331,7 +2587,33 @@ function buildSwimlane() {
|
|
|
2331
2587
|
</div>
|
|
2332
2588
|
</div>`;
|
|
2333
2589
|
}).join('');
|
|
2334
|
-
|
|
2590
|
+
// dead time: find largest gap between consecutive sessions
|
|
2591
|
+
const sorted = recent.slice().sort((a, b) => {
|
|
2592
|
+
const aTs = typeof (a.firstTs || a.mtime) === 'number' ? (a.firstTs || a.mtime) : new Date(a.firstTs || a.mtime).getTime();
|
|
2593
|
+
const bTs = typeof (b.firstTs || b.mtime) === 'number' ? (b.firstTs || b.mtime) : new Date(b.firstTs || b.mtime).getTime();
|
|
2594
|
+
return aTs - bTs;
|
|
2595
|
+
});
|
|
2596
|
+
let maxGapMs = 0; let gapStart = 0;
|
|
2597
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
2598
|
+
const prev = sorted[i - 1];
|
|
2599
|
+
const curr = sorted[i];
|
|
2600
|
+
const prevTs = typeof (prev.firstTs || prev.mtime) === 'number' ? (prev.firstTs || prev.mtime) : new Date(prev.firstTs || prev.mtime).getTime();
|
|
2601
|
+
const prevEnd = prevTs + (prev.totalDurationMs || 60000);
|
|
2602
|
+
const currTs = typeof (curr.firstTs || curr.mtime) === 'number' ? (curr.firstTs || curr.mtime) : new Date(curr.firstTs || curr.mtime).getTime();
|
|
2603
|
+
const gap = currTs - prevEnd;
|
|
2604
|
+
if (gap > maxGapMs) { maxGapMs = gap; gapStart = prevEnd; }
|
|
2605
|
+
}
|
|
2606
|
+
const MIN15 = 15 * 60000;
|
|
2607
|
+
let gapNote = '';
|
|
2608
|
+
if (maxGapMs > MIN15) {
|
|
2609
|
+
const gapMins = Math.round(maxGapMs / 60000);
|
|
2610
|
+
const when = new Date(gapStart);
|
|
2611
|
+
const hr = when.getHours().toString().padStart(2, '0');
|
|
2612
|
+
const mn = when.getMinutes().toString().padStart(2, '0');
|
|
2613
|
+
const label = maxGapMs > 3600000 ? `${Math.floor(maxGapMs/3600000)}h idle` : `${gapMins}m idle`;
|
|
2614
|
+
gapNote = `<div style="font-size:10px;color:var(--text-xs);margin-top:5px;padding:3px 6px;border-radius:4px;background:oklch(40% 0.04 55 / 0.15)">Longest gap: ${label} starting ${hr}:${mn}</div>`;
|
|
2615
|
+
}
|
|
2616
|
+
el.innerHTML = `<div class="m-group-title">Session Lanes <span style="font-size:10px;color:var(--text-xs);font-weight:400">24h</span></div><div id="swimlane-wrap">${rows}</div>${gapNote}`;
|
|
2335
2617
|
}
|
|
2336
2618
|
|
|
2337
2619
|
// ── feature 18: loop run history ──────────────────────────
|
|
@@ -2489,6 +2771,7 @@ function buildWeeklyRecap() {
|
|
|
2489
2771
|
return new Date(typeof t === 'number' ? t : t).toDateString();
|
|
2490
2772
|
})).size;
|
|
2491
2773
|
const longestMs = Math.max(...weekSess.map(s => s.totalDurationMs||0));
|
|
2774
|
+
const streak = calcStreak();
|
|
2492
2775
|
const stats = [
|
|
2493
2776
|
`${weekSess.length} session${weekSess.length!==1?'s':''}`,
|
|
2494
2777
|
`${days} day${days!==1?'s':''}`,
|
|
@@ -2496,7 +2779,8 @@ function buildWeeklyRecap() {
|
|
|
2496
2779
|
totalTools > 0 ? `${totalTools.toLocaleString()} tool calls` : null,
|
|
2497
2780
|
longestMs > 0 ? `${fmtDur(longestMs)} longest` : null,
|
|
2498
2781
|
].filter(Boolean);
|
|
2499
|
-
|
|
2782
|
+
const streakHtml = streak >= 2 ? `<span class="streak-chip" title="${streak} consecutive days with sessions">🔥 ${streak}d streak</span>` : '';
|
|
2783
|
+
document.getElementById('weekly-stats').innerHTML = stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('') + streakHtml;
|
|
2500
2784
|
document.getElementById('weekly-card').classList.add('show');
|
|
2501
2785
|
}
|
|
2502
2786
|
function dismissWeekly() {
|
|
@@ -2997,7 +3281,8 @@ document.addEventListener('keydown', e => {
|
|
|
2997
3281
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
2998
3282
|
if (document.getElementById('cmd-palette').classList.contains('open')) return;
|
|
2999
3283
|
|
|
3000
|
-
if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); }
|
|
3284
|
+
if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); closeShortcutHelp(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); }
|
|
3285
|
+
if (e.key === '?') { e.preventDefault(); openShortcutHelp(); return; }
|
|
3001
3286
|
if (e.key === 'a' || e.key === 'A') { if (currentView === 'now') { e.preventDefault(); toggleAmbient(); } }
|
|
3002
3287
|
|
|
3003
3288
|
if (currentView === 'now') {
|
|
@@ -3026,6 +3311,279 @@ document.addEventListener('keydown', e => {
|
|
|
3026
3311
|
}
|
|
3027
3312
|
});
|
|
3028
3313
|
|
|
3314
|
+
// ── feature 31: inline session filter ─────────────────────
|
|
3315
|
+
function filterSessions(q) {
|
|
3316
|
+
const rows = document.querySelectorAll('#sess-content .sess-row');
|
|
3317
|
+
const lq = q.toLowerCase().trim();
|
|
3318
|
+
let visible = 0;
|
|
3319
|
+
rows.forEach(row => {
|
|
3320
|
+
const prompt = (row.querySelector('.sr-prompt')?.textContent || '').toLowerCase();
|
|
3321
|
+
const meta = (row.querySelector('.sr-meta')?.textContent || '').toLowerCase();
|
|
3322
|
+
const tags = (row.querySelector('.sr-tags')?.textContent || '').toLowerCase();
|
|
3323
|
+
const match = !lq || prompt.includes(lq) || meta.includes(lq) || tags.includes(lq);
|
|
3324
|
+
row.style.display = match ? '' : 'none';
|
|
3325
|
+
if (match) visible++;
|
|
3326
|
+
});
|
|
3327
|
+
const countEl = document.getElementById('sess-filter-count');
|
|
3328
|
+
if (countEl) countEl.textContent = lq && rows.length ? `${visible} / ${rows.length}` : '';
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
// ── feature 32: keyboard shortcut help modal ──────────────
|
|
3332
|
+
function openShortcutHelp() { document.getElementById('shortcut-modal').classList.add('open'); }
|
|
3333
|
+
function closeShortcutHelp() { document.getElementById('shortcut-modal').classList.remove('open'); }
|
|
3334
|
+
|
|
3335
|
+
// ── feature 33: "currently working on" inference ──────────
|
|
3336
|
+
function updateCurrentActivity(events) {
|
|
3337
|
+
const el = document.getElementById('topbar-activity');
|
|
3338
|
+
if (!el) return;
|
|
3339
|
+
if (!events?.length) { el.textContent = ''; el.classList.remove('loaded'); return; }
|
|
3340
|
+
const recent = [...events].reverse().find(ev => ev.kind === 'tool');
|
|
3341
|
+
if (!recent) { el.textContent = ''; el.classList.remove('loaded'); return; }
|
|
3342
|
+
const name = recent.name || '';
|
|
3343
|
+
let activity = '';
|
|
3344
|
+
if (['Write', 'Edit', 'Read', 'MultiEdit'].includes(name)) {
|
|
3345
|
+
const lbl = recent.label || '';
|
|
3346
|
+
const match = lbl.match(/([^/\\]+\.[a-zA-Z0-9]+)/) ;
|
|
3347
|
+
activity = match ? match[1] : (lbl.split('/').pop() || name);
|
|
3348
|
+
} else if (name === 'Bash') {
|
|
3349
|
+
activity = 'bash: ' + (recent.label || '').slice(0, 24);
|
|
3350
|
+
} else if (name === 'WebSearch' || name === 'WebFetch') {
|
|
3351
|
+
activity = 'web: ' + (recent.label || '').slice(0, 20);
|
|
3352
|
+
} else if (name) {
|
|
3353
|
+
activity = name;
|
|
3354
|
+
}
|
|
3355
|
+
if (activity) { el.textContent = '⤷ ' + activity; el.classList.add('loaded'); }
|
|
3356
|
+
else { el.textContent = ''; el.classList.remove('loaded'); }
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
// ── feature 34: prompt pattern analysis ───────────────────
|
|
3360
|
+
const STOP_WORDS = new Set(['the','a','an','and','or','but','in','on','at','to','for','of','with','by','from','is','it','this','that','be','as','i','my','me','we','you','your','can','do','did','have','had','has','was','were','are','will','would','should','could','not','no','if','so','then','what','how','when','where','which','who','all','get','make','add','new','old','use','its','into','out','up','any','just','let','set','also','more','using','used','fix','run','file','please']);
|
|
3361
|
+
let patternsOpen = false;
|
|
3362
|
+
function togglePatterns() {
|
|
3363
|
+
patternsOpen = !patternsOpen;
|
|
3364
|
+
document.getElementById('btn-patterns').classList.toggle('on', patternsOpen);
|
|
3365
|
+
const p = document.getElementById('patterns-panel');
|
|
3366
|
+
p.style.display = patternsOpen ? '' : 'none';
|
|
3367
|
+
if (patternsOpen) buildPatterns();
|
|
3368
|
+
}
|
|
3369
|
+
function buildPatterns() {
|
|
3370
|
+
const el = document.getElementById('patterns-body');
|
|
3371
|
+
if (!allSessions.length) { el.innerHTML = '<div class="loading-txt">No sessions loaded</div>'; return; }
|
|
3372
|
+
const freq = {};
|
|
3373
|
+
for (const s of allSessions) {
|
|
3374
|
+
const words = (s.lastPrompt || '').toLowerCase().match(/\b[a-z]{4,}\b/g) || [];
|
|
3375
|
+
const seen = new Set();
|
|
3376
|
+
for (const w of words) {
|
|
3377
|
+
if (!STOP_WORDS.has(w) && !seen.has(w)) { freq[w] = (freq[w] || 0) + 1; seen.add(w); }
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
const sorted = Object.entries(freq).filter(([,c]) => c >= 2).sort((a, b) => b[1] - a[1]).slice(0, 20);
|
|
3381
|
+
if (!sorted.length) { el.innerHTML = '<div class="loading-txt">Not enough prompt data</div>'; return; }
|
|
3382
|
+
const maxCount = sorted[0][1];
|
|
3383
|
+
const rows = sorted.map(([word, count], i) => {
|
|
3384
|
+
const barW = Math.round((count / maxCount) * 100);
|
|
3385
|
+
return `<tr><td class="lb-rank">${i + 1}</td>
|
|
3386
|
+
<td style="font-size:12px;color:var(--text-mid)">${esc(word)}</td>
|
|
3387
|
+
<td style="width:100px;padding:4px 6px"><div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
|
|
3388
|
+
<div style="height:100%;width:${barW}%;background:oklch(70% 0.15 300);border-radius:2px"></div></div></td>
|
|
3389
|
+
<td class="lb-cost" style="color:var(--text-mid)">${count}</td></tr>`;
|
|
3390
|
+
}).join('');
|
|
3391
|
+
el.innerHTML = `<table class="lb-table"><thead><tr>
|
|
3392
|
+
<th class="lb-rank">#</th><th>Term</th><th></th><th class="lb-cost">Sessions</th>
|
|
3393
|
+
</tr></thead><tbody>${rows}</tbody></table>`;
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
// ── feature 35: session streak tracker ────────────────────
|
|
3397
|
+
function calcStreak() {
|
|
3398
|
+
const dates = new Set(allSessions.map(s => {
|
|
3399
|
+
const t = s.firstTs || s.mtime;
|
|
3400
|
+
if (!t) return null;
|
|
3401
|
+
return new Date(typeof t === 'number' ? t : t).toDateString();
|
|
3402
|
+
}).filter(Boolean));
|
|
3403
|
+
let streak = 0;
|
|
3404
|
+
const today = new Date();
|
|
3405
|
+
for (let i = 0; i <= 365; i++) {
|
|
3406
|
+
const d = new Date(today);
|
|
3407
|
+
d.setDate(d.getDate() - i);
|
|
3408
|
+
if (dates.has(d.toDateString())) {
|
|
3409
|
+
streak++;
|
|
3410
|
+
} else if (i > 0) {
|
|
3411
|
+
break;
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
return streak;
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
// ── feature 25: notification toasts ──────────────────────
|
|
3418
|
+
let _toastLastBudgetKey = '';
|
|
3419
|
+
function showToast(title, msg, type = 'info', duration = 5000) {
|
|
3420
|
+
const rack = document.getElementById('toast-rack');
|
|
3421
|
+
if (!rack) return;
|
|
3422
|
+
const icoMap = { warn: '⚑', err: '⚠', ok: '✓', info: '◉' };
|
|
3423
|
+
const div = document.createElement('div');
|
|
3424
|
+
div.className = 'toast t-' + type;
|
|
3425
|
+
div.innerHTML = `<span class="toast-ico">${icoMap[type] || '◉'}</span>
|
|
3426
|
+
<div class="toast-body">
|
|
3427
|
+
<div class="toast-title">${esc(title)}</div>
|
|
3428
|
+
<div class="toast-msg">${esc(msg)}</div>
|
|
3429
|
+
</div>
|
|
3430
|
+
<button class="toast-close" onclick="this.closest('.toast').remove()">✕</button>`;
|
|
3431
|
+
rack.appendChild(div);
|
|
3432
|
+
if (duration > 0) setTimeout(() => { try { div.remove(); } catch {} }, duration);
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
function checkBudgetToast(todayCost, monthCost) {
|
|
3436
|
+
const budget = JSON.parse(localStorage.getItem('mm-budget') || '{}');
|
|
3437
|
+
const daily = parseFloat(budget.daily) || 0;
|
|
3438
|
+
const monthly = parseFloat(budget.monthly) || 0;
|
|
3439
|
+
if (daily > 0 && todayCost >= daily * 0.9) {
|
|
3440
|
+
const pct = Math.round((todayCost / daily) * 100);
|
|
3441
|
+
const key = 'daily-' + pct;
|
|
3442
|
+
if (key !== _toastLastBudgetKey) {
|
|
3443
|
+
_toastLastBudgetKey = key;
|
|
3444
|
+
showToast('Budget alert', `Daily spend at ${pct}% ($${todayCost.toFixed(2)} of $${daily})`, todayCost >= daily ? 'err' : 'warn');
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
if (monthly > 0 && monthCost >= monthly * 0.9) {
|
|
3448
|
+
const pct = Math.round((monthCost / monthly) * 100);
|
|
3449
|
+
const key = 'monthly-' + pct;
|
|
3450
|
+
if (key !== _toastLastBudgetKey) {
|
|
3451
|
+
_toastLastBudgetKey = key;
|
|
3452
|
+
showToast('Monthly budget', `Monthly spend at ${pct}% ($${monthCost.toFixed(2)} of $${monthly})`, monthCost >= monthly ? 'err' : 'warn');
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
// ── feature 26: token velocity sparkline ──────────────────
|
|
3458
|
+
function buildTokenVelocity() {
|
|
3459
|
+
const el = document.getElementById('m-velocity');
|
|
3460
|
+
if (!el || !allSessions.length) return;
|
|
3461
|
+
const now = Date.now();
|
|
3462
|
+
const HOUR = 3600000;
|
|
3463
|
+
const buckets = new Array(24).fill(0);
|
|
3464
|
+
for (const s of allSessions) {
|
|
3465
|
+
const t = s.firstTs || s.mtime;
|
|
3466
|
+
if (!t) continue;
|
|
3467
|
+
const ts = typeof t === 'number' ? t : new Date(t).getTime();
|
|
3468
|
+
const hoursAgo = (now - ts) / HOUR;
|
|
3469
|
+
if (hoursAgo < 0 || hoursAgo >= 24) continue;
|
|
3470
|
+
const bucket = Math.min(23, Math.floor(23 - hoursAgo));
|
|
3471
|
+
buckets[bucket] += s.totalInputTokens || 0;
|
|
3472
|
+
}
|
|
3473
|
+
const maxTok = Math.max(1, ...buckets);
|
|
3474
|
+
const totalTok = buckets.reduce((a, b) => a + b, 0);
|
|
3475
|
+
const fmt = n => n > 1e6 ? (n/1e6).toFixed(1)+'M' : n > 1e3 ? (n/1e3).toFixed(0)+'k' : String(n);
|
|
3476
|
+
const bars = buckets.map((v, i) => {
|
|
3477
|
+
const h = Math.max(2, Math.round((v / maxTok) * 28));
|
|
3478
|
+
const cls = v > maxTok * 0.66 ? 'vel-hi' : v < maxTok * 0.15 ? 'vel-lo' : '';
|
|
3479
|
+
const hrsAgo = 23 - i;
|
|
3480
|
+
return `<div class="vel-bar ${cls}" style="height:${h}px" title="${fmt(v)} tokens — ${hrsAgo}h ago"></div>`;
|
|
3481
|
+
}).join('');
|
|
3482
|
+
const totalCost = allSessions.reduce((a, s) => a + (s.totalCost || 0), 0);
|
|
3483
|
+
el.innerHTML = `<div class="m-group-title">Token Velocity <span style="font-size:10px;color:var(--text-xs);font-weight:400">24h · ${fmt(totalTok)}</span></div>
|
|
3484
|
+
<div id="vel-chart">${bars}</div>
|
|
3485
|
+
<div style="font-size:10px;color:var(--text-xs);margin-top:4px">Total cost <span style="color:oklch(78% 0.18 75)">$${totalCost.toFixed(2)}</span></div>`;
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
// ── feature 27: export sessions CSV ───────────────────────
|
|
3489
|
+
function exportSessionsCSV() {
|
|
3490
|
+
if (!allSessions.length) { showToast('No data', 'No sessions loaded yet', 'warn'); return; }
|
|
3491
|
+
const headers = ['Date', 'Session ID', 'Prompt', 'Cost ($)', 'Duration (s)', 'Tool Calls', 'User Messages', 'Cache Hit %', 'Input Tokens'];
|
|
3492
|
+
const rows = allSessions.map(s => {
|
|
3493
|
+
const dt = new Date(s.firstTs || s.mtime || 0).toISOString().slice(0, 19).replace('T', ' ');
|
|
3494
|
+
const cost = typeof s.totalCost === 'number' ? s.totalCost.toFixed(4) : '';
|
|
3495
|
+
const dur = s.totalDurationMs ? Math.round(s.totalDurationMs / 1000) : '';
|
|
3496
|
+
const cachePct = s.totalInputTokens > 0 ? Math.round((s.cacheReadTokens || 0) / s.totalInputTokens * 100) : '';
|
|
3497
|
+
const prompt = (s.lastPrompt || '').replace(/"/g, '""');
|
|
3498
|
+
return [dt, s.id, prompt, cost, dur, s.toolCalls || '', s.userMessages || '', cachePct, s.totalInputTokens || ''];
|
|
3499
|
+
});
|
|
3500
|
+
const csv = [headers, ...rows].map(r => r.map(c => `"${c}"`).join(',')).join('\n');
|
|
3501
|
+
const blob = new Blob([csv], { type: 'text/csv' });
|
|
3502
|
+
const a = document.createElement('a');
|
|
3503
|
+
a.href = URL.createObjectURL(blob);
|
|
3504
|
+
a.download = `sessions-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
3505
|
+
a.click();
|
|
3506
|
+
URL.revokeObjectURL(a.href);
|
|
3507
|
+
showToast('Exported', `${allSessions.length} sessions saved as CSV`, 'ok');
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
// ── feature 28: tool usage ranking ────────────────────────
|
|
3511
|
+
let toolRankOpen = false;
|
|
3512
|
+
function toggleToolRank() {
|
|
3513
|
+
toolRankOpen = !toolRankOpen;
|
|
3514
|
+
document.getElementById('btn-tool-rank').classList.toggle('on', toolRankOpen);
|
|
3515
|
+
const p = document.getElementById('tool-rank-panel');
|
|
3516
|
+
p.style.display = toolRankOpen ? '' : 'none';
|
|
3517
|
+
if (toolRankOpen) loadToolRank();
|
|
3518
|
+
}
|
|
3519
|
+
async function loadToolRank() {
|
|
3520
|
+
const el = document.getElementById('tool-rank-body');
|
|
3521
|
+
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
3522
|
+
try {
|
|
3523
|
+
const data = await apiFetch('/api/tool-ranking?dir=' + enc(DIR));
|
|
3524
|
+
if (!data.tools?.length) { el.innerHTML = '<div class="loading-txt">No tool usage data</div>'; return; }
|
|
3525
|
+
const maxCount = data.tools[0].count;
|
|
3526
|
+
const rows = data.tools.slice(0, 15).map((t, i) => {
|
|
3527
|
+
const barW = Math.round((t.count / maxCount) * 100);
|
|
3528
|
+
const errRate = t.errors > 0 ? ((t.errors / t.count) * 100).toFixed(0) + '%' : '—';
|
|
3529
|
+
return `<tr><td class="lb-rank">${i + 1}</td>
|
|
3530
|
+
<td style="font-size:12px;color:var(--text-mid);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t.tool)}</td>
|
|
3531
|
+
<td style="width:80px;padding:4px 6px">
|
|
3532
|
+
<div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
|
|
3533
|
+
<div style="height:100%;width:${barW}%;background:oklch(65% 0.15 200);border-radius:2px"></div>
|
|
3534
|
+
</div>
|
|
3535
|
+
</td>
|
|
3536
|
+
<td class="lb-cost" style="color:var(--text-mid)">${t.count.toLocaleString()}</td>
|
|
3537
|
+
<td class="lb-dur" style="color:${t.errors > 0 ? 'oklch(70% 0.18 25)' : 'var(--text-xs)'}">${errRate}</td>
|
|
3538
|
+
</tr>`;
|
|
3539
|
+
}).join('');
|
|
3540
|
+
el.innerHTML = `<table class="lb-table"><thead><tr>
|
|
3541
|
+
<th class="lb-rank">#</th><th>Tool</th><th></th><th class="lb-cost">Calls</th><th class="lb-dur">Error%</th>
|
|
3542
|
+
</tr></thead><tbody>${rows}</tbody></table>`;
|
|
3543
|
+
} catch (err) {
|
|
3544
|
+
el.innerHTML = '<div class="loading-txt">Could not load: ' + esc(err.message) + '</div>';
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
// ── feature 29: cost breakdown by project ─────────────────
|
|
3549
|
+
let projCostsOpen = false;
|
|
3550
|
+
function toggleProjCosts() {
|
|
3551
|
+
projCostsOpen = !projCostsOpen;
|
|
3552
|
+
document.getElementById('btn-proj-costs').classList.toggle('on', projCostsOpen);
|
|
3553
|
+
const p = document.getElementById('proj-costs-panel');
|
|
3554
|
+
p.style.display = projCostsOpen ? '' : 'none';
|
|
3555
|
+
if (projCostsOpen) loadProjCosts();
|
|
3556
|
+
}
|
|
3557
|
+
async function loadProjCosts() {
|
|
3558
|
+
const el = document.getElementById('proj-costs-body');
|
|
3559
|
+
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
3560
|
+
try {
|
|
3561
|
+
const data = await apiFetch('/api/project-costs');
|
|
3562
|
+
if (!data.projects?.length) { el.innerHTML = '<div class="loading-txt">No cost data across projects</div>'; return; }
|
|
3563
|
+
const maxCost = data.projects[0].cost;
|
|
3564
|
+
const rows = data.projects.slice(0, 10).map((p, i) => {
|
|
3565
|
+
const barW = maxCost > 0 ? Math.round((p.cost / maxCost) * 100) : 0;
|
|
3566
|
+
const name = p.path.split('/').filter(Boolean).pop() || p.path;
|
|
3567
|
+
return `<tr onclick="switchProject('${esc(p.path)}')" style="cursor:pointer" title="${esc(p.path)}">
|
|
3568
|
+
<td class="lb-rank">${i + 1}</td>
|
|
3569
|
+
<td style="font-size:12px;color:var(--text-mid);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(name)}</td>
|
|
3570
|
+
<td class="lb-cost">$${p.cost.toFixed(2)}</td>
|
|
3571
|
+
<td style="width:80px;padding:4px 6px">
|
|
3572
|
+
<div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
|
|
3573
|
+
<div style="height:100%;width:${barW}%;background:oklch(72% 0.18 75 / 0.7);border-radius:2px"></div>
|
|
3574
|
+
</div>
|
|
3575
|
+
</td>
|
|
3576
|
+
<td class="lb-dur">${p.sessions}</td>
|
|
3577
|
+
</tr>`;
|
|
3578
|
+
}).join('');
|
|
3579
|
+
el.innerHTML = `<table class="lb-table"><thead><tr>
|
|
3580
|
+
<th class="lb-rank">#</th><th>Project</th><th class="lb-cost">Cost</th><th></th><th class="lb-dur">Sessions</th>
|
|
3581
|
+
</tr></thead><tbody>${rows}</tbody></table>`;
|
|
3582
|
+
} catch (err) {
|
|
3583
|
+
el.innerHTML = '<div class="loading-txt">Could not load: ' + esc(err.message) + '</div>';
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3029
3587
|
// ── helpers ────────────────────────────────────────────────
|
|
3030
3588
|
function enc(s) { return encodeURIComponent(s); }
|
|
3031
3589
|
function esc(s) {
|