@monoes/monomindcli 1.10.30 → 1.10.32
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/commands/browse.md +6 -17
- package/.claude/helpers/handlers/adr-draft-handler.cjs +64 -0
- package/.claude/helpers/handlers/agent-start-handler.cjs +99 -0
- package/.claude/helpers/handlers/budget-status-handler.cjs +14 -0
- package/.claude/helpers/handlers/compact-handler.cjs +33 -0
- package/.claude/helpers/handlers/graph-status-handler.cjs +38 -0
- package/.claude/helpers/handlers/loops-status-handler.cjs +45 -0
- package/.claude/helpers/handlers/session-restore-handler.cjs +66 -55
- package/.claude/helpers/handlers/stats-handler.cjs +14 -0
- package/.claude/helpers/hook-handler.cjs +16 -228
- package/.claude/skills/agent-browser-testing/SKILL.md +149 -151
- package/.claude/skills/monomind/browse-agentcore.md +20 -21
- package/.claude/skills/monomind/browse-electron.md +45 -46
- package/.claude/skills/monomind/browse-qa.md +29 -30
- package/.claude/skills/monomind/browse-references/authentication.md +39 -40
- package/.claude/skills/monomind/browse-references/trust-boundaries.md +1 -2
- package/.claude/skills/monomind/browse-references/video-recording.md +23 -24
- package/.claude/skills/monomind/browse-slack.md +52 -53
- package/.claude/skills/monomind/browse-vercel.md +26 -27
- package/.claude/skills/monomind/browse.md +273 -273
- package/dist/src/browser/actions.d.ts +15 -0
- package/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +91 -0
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/batch.d.ts +13 -0
- package/dist/src/browser/batch.d.ts.map +1 -0
- package/dist/src/browser/batch.js +11 -0
- package/dist/src/browser/batch.js.map +1 -0
- package/dist/src/browser/console-log.d.ts +22 -0
- package/dist/src/browser/console-log.d.ts.map +1 -0
- package/dist/src/browser/console-log.js +55 -0
- package/dist/src/browser/console-log.js.map +1 -0
- package/dist/src/browser/dialog.d.ts +11 -0
- package/dist/src/browser/dialog.d.ts.map +1 -0
- package/dist/src/browser/dialog.js +36 -0
- package/dist/src/browser/dialog.js.map +1 -0
- package/dist/src/browser/emulation.d.ts +15 -0
- package/dist/src/browser/emulation.d.ts.map +1 -0
- package/dist/src/browser/emulation.js +62 -0
- package/dist/src/browser/emulation.js.map +1 -0
- package/dist/src/browser/find.d.ts +21 -0
- package/dist/src/browser/find.d.ts.map +1 -0
- package/dist/src/browser/find.js +118 -0
- package/dist/src/browser/find.js.map +1 -0
- package/dist/src/browser/index.d.ts +7 -0
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +7 -0
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/pdf.d.ts +15 -0
- package/dist/src/browser/pdf.d.ts.map +1 -0
- package/dist/src/browser/pdf.js +27 -0
- package/dist/src/browser/pdf.js.map +1 -0
- package/dist/src/browser/storage.d.ts +11 -0
- package/dist/src/browser/storage.d.ts.map +1 -0
- package/dist/src/browser/storage.js +43 -0
- package/dist/src/browser/storage.js.map +1 -0
- package/dist/src/commands/browse.d.ts.map +1 -1
- package/dist/src/commands/browse.js +939 -18
- package/dist/src/commands/browse.js.map +1 -1
- package/dist/src/ui/dashboard-v2.html +581 -19
- package/dist/src/ui/server.mjs +56 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -359,6 +359,86 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
359
359
|
.wow-up { color:oklch(60% 0.18 25); }
|
|
360
360
|
.wow-down { color:oklch(65% 0.15 150); }
|
|
361
361
|
.wow-flat { color:var(--text-lo); }
|
|
362
|
+
|
|
363
|
+
/* ── live tail ───────────────────────────────────────────── */
|
|
364
|
+
.live-tail-btn { font-size:10px; color:var(--text-lo); background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 7px; cursor:pointer; transition:color 0.15s, border-color 0.15s; line-height:1.4; white-space:nowrap; }
|
|
365
|
+
.live-tail-btn:hover { color:var(--text-hi); }
|
|
366
|
+
.live-tail-btn.on { color:var(--green); border-color:oklch(65% 0.15 150 / 0.5); }
|
|
367
|
+
@keyframes live-pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
|
368
|
+
#btn-live-tail.on { animation: live-pulse 2s ease-in-out infinite; }
|
|
369
|
+
@media (prefers-reduced-motion: reduce) { #btn-live-tail.on { animation:none; } }
|
|
370
|
+
|
|
371
|
+
/* ── calendar heatmap ────────────────────────────────────── */
|
|
372
|
+
.cal-grid { display:grid; grid-template-rows:repeat(7,9px); grid-auto-flow:column; grid-auto-columns:9px; gap:2px; margin-top:6px; }
|
|
373
|
+
.cal-cell { border-radius:2px; background:var(--surface-hi); }
|
|
374
|
+
.cal-cell.cal-1 { background:oklch(72% 0.18 75 / 0.22); }
|
|
375
|
+
.cal-cell.cal-2 { background:oklch(72% 0.18 75 / 0.42); }
|
|
376
|
+
.cal-cell.cal-3 { background:oklch(72% 0.18 75 / 0.65); }
|
|
377
|
+
.cal-cell.cal-4 { background:oklch(72% 0.18 75 / 0.85); }
|
|
378
|
+
.cal-cell.cal-today { outline:1px solid var(--accent); outline-offset:-1px; }
|
|
379
|
+
|
|
380
|
+
/* ── session bookmark ────────────────────────────────────── */
|
|
381
|
+
.sess-star { font-size:13px; background:none; border:none; cursor:pointer; color:var(--text-xs); padding:0 2px; transition:color 0.1s; line-height:1; flex-shrink:0; }
|
|
382
|
+
.sess-star:hover { color:var(--text-lo); }
|
|
383
|
+
.sess-star.on { color:oklch(78% 0.18 75); }
|
|
384
|
+
.sr-starred { font-size:10px; color:oklch(78% 0.18 75); margin-left:auto; }
|
|
385
|
+
#sess-star-filter { font-size:11px; color:var(--text-lo); background:transparent; border:1px solid transparent; border-radius:8px; padding:2px 9px; cursor:pointer; transition:color 0.1s, background 0.1s; font-family:var(--sans); margin-left:auto; }
|
|
386
|
+
#sess-star-filter:hover { color:var(--text-hi); }
|
|
387
|
+
#sess-star-filter.on { background:oklch(72% 0.18 75 / 0.1); color:oklch(78% 0.18 75); border-color:oklch(72% 0.18 75 / 0.3); }
|
|
388
|
+
|
|
389
|
+
/* ── forecast row ────────────────────────────────────────── */
|
|
390
|
+
.m-val.forecast { color:var(--text-lo); font-size:11px; font-weight:500; }
|
|
391
|
+
|
|
392
|
+
/* ── auto-tags ───────────────────────────────────────────── */
|
|
393
|
+
.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
|
+
.tag-filter-bar { display:flex; flex-wrap:wrap; gap:5px; margin-bottom:12px; }
|
|
395
|
+
.tag-chip { 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; }
|
|
396
|
+
.tag-chip:hover { color:var(--text-hi); }
|
|
397
|
+
.tag-chip.active { background:var(--accent-dim); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
|
|
398
|
+
|
|
399
|
+
/* ── session recap ───────────────────────────────────────── */
|
|
400
|
+
#feed-recap { display:none; flex-shrink:0; padding:8px 18px; border-bottom:1px solid var(--border); background:oklch(14% 0.009 55); }
|
|
401
|
+
#feed-recap.show { display:block; }
|
|
402
|
+
.recap-text { font-size:12px; color:var(--text-mid); line-height:1.6; }
|
|
403
|
+
.recap-stat { display:inline-flex; align-items:center; gap:4px; font-size:11px; padding:1px 7px; border-radius:8px; margin-right:5px; }
|
|
404
|
+
.recap-stat.rs-tool { background:oklch(65% 0.15 150 / 0.1); color:oklch(65% 0.15 150); }
|
|
405
|
+
.recap-stat.rs-cost { background:oklch(72% 0.18 75 / 0.1); color:oklch(78% 0.18 75); }
|
|
406
|
+
.recap-stat.rs-err { background:oklch(60% 0.18 25 / 0.1); color:oklch(70% 0.18 25); }
|
|
407
|
+
.recap-stat.rs-user { background:var(--surface-hi); color:var(--text-lo); }
|
|
408
|
+
|
|
409
|
+
/* ── replay mode ─────────────────────────────────────────── */
|
|
410
|
+
#replay-bar { display:none; flex-shrink:0; align-items:center; gap:8px; padding:5px 18px; border-bottom:1px solid var(--border); background:oklch(13% 0.009 55); }
|
|
411
|
+
#replay-bar.show { display:flex; }
|
|
412
|
+
.rp-btn { font-size:11px; background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 8px; cursor:pointer; color:var(--text-lo); transition:color 0.1s; }
|
|
413
|
+
.rp-btn:hover { color:var(--text-hi); }
|
|
414
|
+
.rp-btn.active { color:var(--accent); border-color:oklch(72% 0.18 75 / 0.5); }
|
|
415
|
+
#rp-progress { flex:1; height:3px; background:var(--surface-hi); border-radius:2px; overflow:hidden; }
|
|
416
|
+
#rp-fill { height:100%; background:var(--accent); border-radius:2px; transition:width 0.15s; }
|
|
417
|
+
#rp-counter { font-size:11px; color:var(--text-lo); font-family:var(--mono); white-space:nowrap; }
|
|
418
|
+
|
|
419
|
+
/* ── global feed (multi-project) ─────────────────────────── */
|
|
420
|
+
.gf-proj-tag { font-size:10px; padding:1px 6px; border-radius:6px; background:var(--surface-hi); color:var(--text-lo); white-space:nowrap; flex-shrink:0; margin-top:3px; }
|
|
421
|
+
|
|
422
|
+
/* ── project health score ────────────────────────────────── */
|
|
423
|
+
.proj-health { position:absolute; bottom:12px; right:12px; font-size:11px; font-weight:700; width:28px; height:28px; border-radius:50%; display:flex; align-items:center; justify-content:center; }
|
|
424
|
+
.ph-hi { background:oklch(65% 0.15 150 / 0.15); color:oklch(65% 0.15 150); }
|
|
425
|
+
.ph-mid { background:oklch(72% 0.18 75 / 0.15); color:oklch(78% 0.18 75); }
|
|
426
|
+
.ph-lo { background:oklch(60% 0.18 25 / 0.12); color:oklch(70% 0.18 25); }
|
|
427
|
+
|
|
428
|
+
/* ── budget cap ──────────────────────────────────────────── */
|
|
429
|
+
#budget-modal { display:none; position:fixed; inset:0; z-index:200; background:oklch(5% 0 0 / 0.6); align-items:center; justify-content:center; }
|
|
430
|
+
#budget-modal.open { display:flex; }
|
|
431
|
+
#budget-box { background:oklch(16% 0.009 55); border:1px solid oklch(32% 0.008 55); border-radius:10px; padding:22px 24px; width:320px; box-shadow:0 24px 60px oklch(5% 0 0 / 0.7); }
|
|
432
|
+
.bm-title { font-size:14px; font-weight:600; color:var(--text-hi); margin-bottom:14px; }
|
|
433
|
+
.bm-row { display:flex; flex-direction:column; gap:4px; margin-bottom:12px; }
|
|
434
|
+
.bm-lbl { font-size:11px; color:var(--text-lo); }
|
|
435
|
+
.bm-input { background:var(--surface); border:1px solid var(--border); border-radius:var(--r); padding:6px 10px; font-size:13px; color:var(--text-hi); font-family:var(--mono); outline:none; transition:border-color 0.1s; }
|
|
436
|
+
.bm-input:focus { border-color:var(--accent); }
|
|
437
|
+
.bm-btns { display:flex; gap:8px; margin-top:16px; }
|
|
438
|
+
.bm-save { flex:1; background:var(--accent); border:none; border-radius:var(--r); padding:7px 0; font-size:13px; color:oklch(11% 0.009 55); font-weight:600; cursor:pointer; }
|
|
439
|
+
.bm-save:hover { background:oklch(68% 0.18 75); }
|
|
440
|
+
.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; }
|
|
441
|
+
.bm-cancel:hover { color:var(--text-hi); }
|
|
362
442
|
</style>
|
|
363
443
|
</head>
|
|
364
444
|
<body>
|
|
@@ -399,6 +479,9 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
399
479
|
<div class="nav-item" data-view="orgs">
|
|
400
480
|
<span class="ico">⬡</span><span class="lbl">Orgs</span>
|
|
401
481
|
</div>
|
|
482
|
+
<div class="nav-item" data-view="global">
|
|
483
|
+
<span class="ico">⊕</span><span class="lbl">Global Feed</span>
|
|
484
|
+
</div>
|
|
402
485
|
</div>
|
|
403
486
|
</div>
|
|
404
487
|
<div id="sb-footer">
|
|
@@ -414,6 +497,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
414
497
|
<span class="pill"><span class="live-dot"></span> live</span>
|
|
415
498
|
<span id="topbar-cost"></span>
|
|
416
499
|
<div id="tb-right">
|
|
500
|
+
<button class="btn" id="btn-budget" onclick="openBudgetModal()" title="Set daily/monthly cost budget">⚑ Budget</button>
|
|
417
501
|
<button class="btn" onclick="openCmdPalette()">⌕ Search <kbd style="font-size:10px;opacity:0.6;margin-left:3px">⌘K</kbd></button>
|
|
418
502
|
<button class="btn" onclick="refreshCurrent()">↺ Refresh</button>
|
|
419
503
|
</div>
|
|
@@ -432,6 +516,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
432
516
|
<div class="cmd-footer">
|
|
433
517
|
<span class="cmd-key"><kbd>↑↓</kbd> navigate</span>
|
|
434
518
|
<span class="cmd-key"><kbd>↵</kbd> select</span>
|
|
519
|
+
<span class="cmd-key"><kbd>></kbd> search all sessions</span>
|
|
435
520
|
<span class="cmd-key"><kbd>esc</kbd> close</span>
|
|
436
521
|
</div>
|
|
437
522
|
</div>
|
|
@@ -445,6 +530,8 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
445
530
|
<h2>Live Feed</h2>
|
|
446
531
|
<span id="feed-sess">—</span>
|
|
447
532
|
<div id="feed-sess-nav">
|
|
533
|
+
<button class="live-tail-btn" id="btn-live-tail" onclick="toggleLiveTail()" title="Toggle live tail (auto-scroll + 5s refresh)">⬤ Tail</button>
|
|
534
|
+
<button class="sess-copy-btn" id="btn-replay" onclick="replayToggle()" title="Replay session event-by-event">⏵ Replay</button>
|
|
448
535
|
<button class="sess-copy-btn" id="btn-copy-sess" onclick="copySession()" title="Copy session as markdown">⎘ Copy</button>
|
|
449
536
|
<button class="density-btn" id="btn-density" onclick="toggleDensity()" title="Toggle compact view">⊟</button>
|
|
450
537
|
<button class="sess-btn" onclick="toggleFeedSearch()" title="Search in feed (/)">⌕</button>
|
|
@@ -463,6 +550,15 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
463
550
|
<span class="sctx-label" id="sctx-label"></span>
|
|
464
551
|
<button class="sctx-live" onclick="goLive()">⬤ Go live</button>
|
|
465
552
|
</div>
|
|
553
|
+
<div id="feed-recap"></div>
|
|
554
|
+
<div id="replay-bar">
|
|
555
|
+
<button class="rp-btn" id="rp-play" onclick="replayToggle()" title="Play/pause replay">▶</button>
|
|
556
|
+
<button class="rp-btn" onclick="replayStep(-1)" title="Step back">‹</button>
|
|
557
|
+
<button class="rp-btn" onclick="replayStep(1)" title="Step forward">›</button>
|
|
558
|
+
<div id="rp-progress"><div id="rp-fill" style="width:0%"></div></div>
|
|
559
|
+
<span id="rp-counter">0 / 0</span>
|
|
560
|
+
<button class="rp-btn" onclick="stopReplay()" title="Exit replay">✕</button>
|
|
561
|
+
</div>
|
|
466
562
|
<div id="feed-timeline" title="Session tool activity timeline"></div>
|
|
467
563
|
<div id="feed-time-filter">
|
|
468
564
|
<span class="tf-lbl">Range</span>
|
|
@@ -521,7 +617,10 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
521
617
|
<!-- SESSIONS -->
|
|
522
618
|
<div class="view" id="view-sessions">
|
|
523
619
|
<div class="vscroll">
|
|
524
|
-
<div
|
|
620
|
+
<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
|
|
621
|
+
<div class="pg-title" style="margin-bottom:0">Sessions</div>
|
|
622
|
+
<button id="sess-star-filter" onclick="toggleSessStarFilter()" title="Show only bookmarked sessions">☆ Starred</button>
|
|
623
|
+
</div>
|
|
525
624
|
<div class="pg-sub" id="sess-pg-sub">Recent Claude Code sessions for this project</div>
|
|
526
625
|
<div id="sess-content" class="sess-list"><div class="loading-txt">Loading…</div></div>
|
|
527
626
|
</div>
|
|
@@ -558,10 +657,40 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
558
657
|
</div>
|
|
559
658
|
</div>
|
|
560
659
|
|
|
660
|
+
<!-- GLOBAL FEED -->
|
|
661
|
+
<div class="view" id="view-global">
|
|
662
|
+
<div class="vscroll">
|
|
663
|
+
<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
|
|
664
|
+
<div class="pg-title" style="margin-bottom:0">Global Feed</div>
|
|
665
|
+
<span class="pg-sub" id="gf-sub" style="margin-bottom:0">Activity across all projects</span>
|
|
666
|
+
</div>
|
|
667
|
+
<div id="gf-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
|
|
561
671
|
</div><!-- /view-wrap -->
|
|
562
672
|
</div><!-- /main -->
|
|
563
673
|
</div><!-- /app -->
|
|
564
674
|
|
|
675
|
+
<!-- budget modal (fixed overlay, outside app) -->
|
|
676
|
+
<div id="budget-modal" onclick="if(event.target===this)closeBudgetModal()">
|
|
677
|
+
<div id="budget-box">
|
|
678
|
+
<div class="bm-title">Set Cost Budget</div>
|
|
679
|
+
<div class="bm-row">
|
|
680
|
+
<div class="bm-lbl">Daily limit ($)</div>
|
|
681
|
+
<input class="bm-input" id="bm-daily" type="number" min="0" step="1" placeholder="e.g. 20">
|
|
682
|
+
</div>
|
|
683
|
+
<div class="bm-row">
|
|
684
|
+
<div class="bm-lbl">Monthly limit ($)</div>
|
|
685
|
+
<input class="bm-input" id="bm-monthly" type="number" min="0" step="10" placeholder="e.g. 200">
|
|
686
|
+
</div>
|
|
687
|
+
<div class="bm-btns">
|
|
688
|
+
<button class="bm-cancel" onclick="closeBudgetModal()">Cancel</button>
|
|
689
|
+
<button class="bm-save" onclick="saveBudget()">Save</button>
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
|
|
565
694
|
<script>
|
|
566
695
|
// ── state ──────────────────────────────────────────────────
|
|
567
696
|
let DIR = '';
|
|
@@ -577,10 +706,14 @@ let userScrolled = false;
|
|
|
577
706
|
let selectedEntryId = null;
|
|
578
707
|
let allDrawers = [];
|
|
579
708
|
let dismissedAlerts = new Set();
|
|
580
|
-
let alertState = { todayCost: 0, errorCount: 0, longLoops: [] };
|
|
709
|
+
let alertState = { todayCost: 0, errorCount: 0, longLoops: [], anomaly: null, budgetAlert: null, budgetCls: 'alert-warn' };
|
|
581
710
|
let feedTimeFilter = 'all';
|
|
582
711
|
let cmdFocusIdx = 0;
|
|
583
712
|
let cmdItems = [];
|
|
713
|
+
let liveTailMode = false;
|
|
714
|
+
let liveTailTimer = null;
|
|
715
|
+
let bookmarks = new Set(JSON.parse(localStorage.getItem('mm-bookmarks') || '[]'));
|
|
716
|
+
let showStarredOnly = false;
|
|
584
717
|
|
|
585
718
|
// ── nav ────────────────────────────────────────────────────
|
|
586
719
|
document.querySelectorAll('.nav-item[data-view]').forEach(el => {
|
|
@@ -597,7 +730,7 @@ function switchView(v) {
|
|
|
597
730
|
el.classList.toggle('active', el.dataset.view === v));
|
|
598
731
|
document.querySelectorAll('.view').forEach(el =>
|
|
599
732
|
el.classList.toggle('active', el.id === 'view-' + v));
|
|
600
|
-
const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', memory:'Memory', orgs:'Orgs' };
|
|
733
|
+
const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', memory:'Memory', orgs:'Orgs', global:'Global Feed' };
|
|
601
734
|
document.getElementById('view-title').textContent = titles[v] || v;
|
|
602
735
|
if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
|
|
603
736
|
}
|
|
@@ -609,6 +742,7 @@ function renderView(v) {
|
|
|
609
742
|
if (v === 'loops') renderLoops();
|
|
610
743
|
if (v === 'memory') renderMemory();
|
|
611
744
|
if (v === 'orgs') renderOrgs();
|
|
745
|
+
if (v === 'global') renderGlobalFeed();
|
|
612
746
|
}
|
|
613
747
|
|
|
614
748
|
function refreshCurrent() {
|
|
@@ -628,6 +762,7 @@ async function init() {
|
|
|
628
762
|
document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
|
|
629
763
|
} catch (_) {}
|
|
630
764
|
viewRendered['now'] = true; // prevents switchView from re-rendering NOW on jumpToSession
|
|
765
|
+
updateBudgetBtnStyle();
|
|
631
766
|
await refreshNow();
|
|
632
767
|
startPolling();
|
|
633
768
|
}
|
|
@@ -662,9 +797,26 @@ async function refreshNow() {
|
|
|
662
797
|
}
|
|
663
798
|
|
|
664
799
|
async function refreshNowSilent() {
|
|
800
|
+
if (liveTailMode) { userScrolled = false; sessionIdx = 0; }
|
|
665
801
|
await Promise.allSettled([loadFeedSilent(), loadMetrics()]);
|
|
666
802
|
}
|
|
667
803
|
|
|
804
|
+
// ── live tail ──────────────────────────────────────────────
|
|
805
|
+
function toggleLiveTail() {
|
|
806
|
+
liveTailMode = !liveTailMode;
|
|
807
|
+
const btn = document.getElementById('btn-live-tail');
|
|
808
|
+
btn.classList.toggle('on', liveTailMode);
|
|
809
|
+
btn.title = liveTailMode ? 'Live tail ON — click to disable' : 'Toggle live tail (5s refresh + auto-scroll)';
|
|
810
|
+
clearInterval(liveTailTimer);
|
|
811
|
+
if (liveTailMode) {
|
|
812
|
+
// jump to newest session and start fast polling
|
|
813
|
+
if (sessionIdx !== 0 && allSessions.length) { sessionIdx = 0; userScrolled = false; loadFeedForSession(allSessions[0]); }
|
|
814
|
+
liveTailTimer = setInterval(() => { if (currentView === 'now') refreshNowSilent(); }, 5000);
|
|
815
|
+
} else {
|
|
816
|
+
startPolling();
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
668
820
|
// session nav
|
|
669
821
|
function prevSession() {
|
|
670
822
|
if (sessionIdx < allSessions.length - 1) { sessionIdx++; userScrolled = false; loadFeedForSession(allSessions[sessionIdx]); }
|
|
@@ -690,6 +842,7 @@ async function loadFeed() {
|
|
|
690
842
|
const todayEl = document.getElementById('m-today');
|
|
691
843
|
const sparkWrap = todayEl?.querySelector('.spark-wrap');
|
|
692
844
|
if (sparkWrap) sparkWrap.outerHTML = buildSparkline();
|
|
845
|
+
detectAnomalies();
|
|
693
846
|
} catch (err) {
|
|
694
847
|
setFeedContent('<div class="feed-empty">Could not load feed: ' + esc(err.message) + '</div>');
|
|
695
848
|
}
|
|
@@ -826,6 +979,8 @@ function renderFeedEvents(events, silent) {
|
|
|
826
979
|
// update timeline + breakdown with all original events (before time-filter)
|
|
827
980
|
buildTimeline(filtered);
|
|
828
981
|
buildBreakdown(filtered);
|
|
982
|
+
// session recap card
|
|
983
|
+
buildRecap(filtered, allSessions[sessionIdx]);
|
|
829
984
|
}
|
|
830
985
|
|
|
831
986
|
function renderGroupRow(g) {
|
|
@@ -934,28 +1089,34 @@ function closeDetail() {
|
|
|
934
1089
|
selectedEntryId = null;
|
|
935
1090
|
}
|
|
936
1091
|
|
|
937
|
-
// ──
|
|
1092
|
+
// ── 12-week calendar heatmap ───────────────────────────────
|
|
938
1093
|
function buildSparkline() {
|
|
939
|
-
// bucket allSessions into 7 days (0 = 6 days ago, 6 = today)
|
|
940
1094
|
const DAY = 86400000;
|
|
941
1095
|
const now = Date.now();
|
|
942
|
-
const
|
|
1096
|
+
const WEEKS = 12;
|
|
1097
|
+
const DAYS = WEEKS * 7; // 84 days
|
|
1098
|
+
const buckets = new Array(DAYS).fill(0);
|
|
943
1099
|
for (const s of allSessions) {
|
|
944
1100
|
const ts = s.lastTs || s.mtime;
|
|
945
1101
|
if (!ts) continue;
|
|
946
1102
|
const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
|
|
947
|
-
const
|
|
948
|
-
if (
|
|
1103
|
+
const idx = DAYS - 1 - Math.floor(age / DAY);
|
|
1104
|
+
if (idx >= 0 && idx < DAYS) buckets[idx]++;
|
|
949
1105
|
}
|
|
950
1106
|
const max = Math.max(...buckets, 1);
|
|
951
|
-
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1107
|
+
// offset so first cell starts on Monday of the week 12 weeks ago
|
|
1108
|
+
const todayDow = new Date().getDay(); // 0=Sun
|
|
1109
|
+
// pad start so column 0 begins on Monday
|
|
1110
|
+
const startOffset = todayDow === 0 ? 6 : todayDow - 1;
|
|
1111
|
+
const cells = buckets.map((v, i) => {
|
|
1112
|
+
const isToday = i === DAYS - 1;
|
|
1113
|
+
const level = v === 0 ? 0 : Math.min(4, Math.ceil(v / max * 4));
|
|
1114
|
+
const d = new Date(now - (DAYS - 1 - i) * DAY);
|
|
1115
|
+
const label = d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
1116
|
+
const title = `${label}: ${v} session${v !== 1 ? 's' : ''}`;
|
|
1117
|
+
return `<div class="cal-cell cal-${level}${isToday ? ' cal-today' : ''}" title="${title}"></div>`;
|
|
1118
|
+
});
|
|
1119
|
+
return `<div class="spark-wrap"><div class="spark-lbl">12-week activity ${buildWowDelta()}</div><div class="cal-grid">${cells.join('')}</div></div>`;
|
|
959
1120
|
}
|
|
960
1121
|
|
|
961
1122
|
// ── alerts rail ────────────────────────────────────────────
|
|
@@ -973,6 +1134,14 @@ function updateAlerts() {
|
|
|
973
1134
|
all.push({ id: 'feed-errors', cls: 'alert-warn', ico: '⚠', msg: `${alertState.errorCount} errors in current session`, action: 'jumpToErrors()' });
|
|
974
1135
|
}
|
|
975
1136
|
|
|
1137
|
+
if (alertState.anomaly) {
|
|
1138
|
+
all.push({ id: 'anomaly-sess', cls: 'alert-warn', ico: '◎', msg: alertState.anomaly });
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (alertState.budgetAlert) {
|
|
1142
|
+
all.push({ id: 'budget-alert', cls: alertState.budgetCls || 'alert-warn', ico: '⚑', msg: alertState.budgetAlert });
|
|
1143
|
+
}
|
|
1144
|
+
|
|
976
1145
|
for (const l of alertState.longLoops) {
|
|
977
1146
|
all.push({ id: 'loop-' + l, cls: 'alert-warn', ico: '↺', msg: `Long-running loop: ${l}` });
|
|
978
1147
|
}
|
|
@@ -995,6 +1164,21 @@ function dismissAlert(id) {
|
|
|
995
1164
|
updateAlerts();
|
|
996
1165
|
}
|
|
997
1166
|
|
|
1167
|
+
// ── anomaly detection ──────────────────────────────────────
|
|
1168
|
+
function detectAnomalies() {
|
|
1169
|
+
const withCost = allSessions.filter(s => typeof (s.totalCost ?? s.cost) === 'number');
|
|
1170
|
+
if (withCost.length < 3) { alertState.anomaly = null; updateAlerts(); return; }
|
|
1171
|
+
const avg = withCost.reduce((sum, s) => sum + (s.totalCost ?? s.cost ?? 0), 0) / withCost.length;
|
|
1172
|
+
const curr = allSessions[sessionIdx];
|
|
1173
|
+
const currCost = typeof curr?.totalCost === 'number' ? curr.totalCost : (typeof curr?.cost === 'number' ? curr.cost : null);
|
|
1174
|
+
if (currCost !== null && avg > 0.05 && currCost > avg * 2.5 && currCost > 0.5) {
|
|
1175
|
+
alertState.anomaly = `Session unusually costly: $${currCost.toFixed(2)} vs $${avg.toFixed(2)} avg`;
|
|
1176
|
+
} else {
|
|
1177
|
+
alertState.anomaly = null;
|
|
1178
|
+
}
|
|
1179
|
+
updateAlerts();
|
|
1180
|
+
}
|
|
1181
|
+
|
|
998
1182
|
// ── session context bar ────────────────────────────────────
|
|
999
1183
|
function showSessCtx(sess) {
|
|
1000
1184
|
const bar = document.getElementById('sess-ctx');
|
|
@@ -1026,6 +1210,7 @@ async function loadTodayMetrics() {
|
|
|
1026
1210
|
const s = data?.tokens?.summary || {};
|
|
1027
1211
|
alertState.todayCost = typeof s.todayCost === 'number' ? s.todayCost : 0;
|
|
1028
1212
|
updateAlerts();
|
|
1213
|
+
checkBudget();
|
|
1029
1214
|
// topbar cost badge
|
|
1030
1215
|
const badge = document.getElementById('topbar-cost');
|
|
1031
1216
|
if (badge && typeof s.todayCost === 'number') {
|
|
@@ -1035,11 +1220,20 @@ async function loadTodayMetrics() {
|
|
|
1035
1220
|
const cost = typeof s.todayCost === 'number' ? '$' + s.todayCost.toFixed(2) : '—';
|
|
1036
1221
|
const calls = s.todayCalls != null ? s.todayCalls : '—';
|
|
1037
1222
|
const moCost = typeof s.monthCost === 'number' ? '$' + s.monthCost.toFixed(2) : '—';
|
|
1223
|
+
// cost forecast: project monthly spend from daily average
|
|
1224
|
+
let forecast = '';
|
|
1225
|
+
if (typeof s.monthCost === 'number' && s.monthCost > 0) {
|
|
1226
|
+
const day = new Date().getDate();
|
|
1227
|
+
const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
|
|
1228
|
+
const projected = (s.monthCost / day) * daysInMonth;
|
|
1229
|
+
forecast = `<div class="m-row"><span class="m-name">Month forecast</span><span class="m-val forecast">~$${projected.toFixed(2)}</span></div>`;
|
|
1230
|
+
}
|
|
1038
1231
|
document.getElementById('m-today').innerHTML = `
|
|
1039
1232
|
<div class="m-group-title">Today</div>
|
|
1040
1233
|
<div class="m-row"><span class="m-name">API cost</span><span class="m-val gold">${cost}</span></div>
|
|
1041
1234
|
<div class="m-row"><span class="m-name">API calls</span><span class="m-val">${calls}</span></div>
|
|
1042
1235
|
<div class="m-row"><span class="m-name">Month total</span><span class="m-val">${moCost}</span></div>
|
|
1236
|
+
${forecast}
|
|
1043
1237
|
${buildSparkline()}
|
|
1044
1238
|
`;
|
|
1045
1239
|
} catch (_) {
|
|
@@ -1116,8 +1310,11 @@ function renderProjectGrid(projects, query) {
|
|
|
1116
1310
|
el.className = 'proj-grid';
|
|
1117
1311
|
el.innerHTML = filtered.map(p => {
|
|
1118
1312
|
const isCurrent = p.path === DIR;
|
|
1313
|
+
const score = computeHealthScore(p);
|
|
1314
|
+
const hCls = healthClass(score);
|
|
1119
1315
|
return `<div class="proj-card${isCurrent ? ' current' : ''}" onclick="switchProject('${esc(p.path || '')}')">
|
|
1120
1316
|
${isCurrent ? '<div class="proj-card-badge">active</div>' : ''}
|
|
1317
|
+
<div class="proj-health ${hCls}" title="Health score: ${score}">${score}</div>
|
|
1121
1318
|
<div class="proj-card-name">${esc(p.name || p.slug)}</div>
|
|
1122
1319
|
<div class="proj-card-path">${esc(p.path || '')}</div>
|
|
1123
1320
|
<div class="proj-card-stats">
|
|
@@ -1140,6 +1337,7 @@ async function renderSessions() {
|
|
|
1140
1337
|
try {
|
|
1141
1338
|
const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
|
|
1142
1339
|
allSessions = sessions; // always sync — stale ordering breaks jumpToSession
|
|
1340
|
+
initTags();
|
|
1143
1341
|
document.getElementById('bdg-sessions').textContent = sessions.length || '—';
|
|
1144
1342
|
document.getElementById('sess-pg-sub').textContent =
|
|
1145
1343
|
sessions.length + ' session' + (sessions.length !== 1 ? 's' : '') + ' · ' + (DIR.split('/').pop() || DIR);
|
|
@@ -1147,23 +1345,36 @@ async function renderSessions() {
|
|
|
1147
1345
|
el.innerHTML = '<div class="empty"><div class="empty-ico">◫</div><div>No sessions yet</div></div>';
|
|
1148
1346
|
return;
|
|
1149
1347
|
}
|
|
1150
|
-
|
|
1348
|
+
let toShow = showStarredOnly ? sessions.filter(s => bookmarks.has(s.id)) : sessions;
|
|
1349
|
+
if (activeTagFilter) toShow = toShow.filter(s => (allTags.sessionTags.get(s.id) || []).includes(activeTagFilter));
|
|
1350
|
+
if (!toShow.length) {
|
|
1351
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
el.innerHTML = toShow.map(s => {
|
|
1151
1355
|
const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
|
|
1152
1356
|
const msgs = s.totalMessages ? s.totalMessages + ' msg' : '';
|
|
1153
1357
|
const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2)
|
|
1154
1358
|
: typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '';
|
|
1155
1359
|
const meta = [dur, msgs, cost].filter(Boolean).join(' · ') || s.id.slice(0, 16);
|
|
1156
1360
|
const summaries = (s.summaries || []).slice(0, 2).map(sm => { const t = typeof sm === 'string' ? sm : (sm.summary || sm.text || String(sm)); return `<span class="sr-tag">${esc(t.slice(0, 40))}</span>`; }).join('');
|
|
1157
|
-
|
|
1361
|
+
const autoTags = (allTags.sessionTags.get(s.id) || []).map(t => `<span class="sr-autotag">${esc(t)}</span>`).join('');
|
|
1362
|
+
const isStarred = bookmarks.has(s.id);
|
|
1363
|
+
return `<div class="sess-row" onclick="jumpToSession('${esc(s.id)}')">
|
|
1158
1364
|
<div class="sr-top">
|
|
1159
1365
|
<div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
|
|
1160
1366
|
<div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>
|
|
1367
|
+
<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>
|
|
1161
1368
|
<span class="sr-view">→ view</span>
|
|
1162
1369
|
</div>
|
|
1163
1370
|
<div class="sr-meta">${esc(meta)}</div>
|
|
1164
|
-
${summaries ? `<div class="sr-tags">${summaries}</div>` : ''}
|
|
1371
|
+
${(summaries || autoTags) ? `<div class="sr-tags">${summaries}${autoTags}</div>` : ''}
|
|
1165
1372
|
</div>`;
|
|
1166
1373
|
}).join('');
|
|
1374
|
+
// prepend tag filter bar if there are common tags
|
|
1375
|
+
if (allTags.common.size > 1) {
|
|
1376
|
+
el.innerHTML = buildTagFilterBar(toShow) + el.innerHTML;
|
|
1377
|
+
}
|
|
1167
1378
|
} catch (err) {
|
|
1168
1379
|
el.innerHTML = '<div class="empty">Could not load sessions: ' + esc(err.message) + '</div>';
|
|
1169
1380
|
}
|
|
@@ -1177,6 +1388,313 @@ function jumpToSession(id) {
|
|
|
1177
1388
|
}, 80);
|
|
1178
1389
|
}
|
|
1179
1390
|
|
|
1391
|
+
// ── session bookmarks ──────────────────────────────────────
|
|
1392
|
+
function toggleBookmark(id, e) {
|
|
1393
|
+
e.stopPropagation();
|
|
1394
|
+
if (bookmarks.has(id)) bookmarks.delete(id);
|
|
1395
|
+
else bookmarks.add(id);
|
|
1396
|
+
localStorage.setItem('mm-bookmarks', JSON.stringify([...bookmarks]));
|
|
1397
|
+
document.querySelectorAll('.sess-star[data-sid="' + id + '"]').forEach(el => {
|
|
1398
|
+
const on = bookmarks.has(id);
|
|
1399
|
+
el.classList.toggle('on', on);
|
|
1400
|
+
el.textContent = on ? '★' : '☆';
|
|
1401
|
+
el.title = on ? 'Remove bookmark' : 'Bookmark session';
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function toggleSessStarFilter() {
|
|
1406
|
+
showStarredOnly = !showStarredOnly;
|
|
1407
|
+
const btn = document.getElementById('sess-star-filter');
|
|
1408
|
+
btn.classList.toggle('on', showStarredOnly);
|
|
1409
|
+
// re-render with filter
|
|
1410
|
+
viewRendered['sessions'] = false;
|
|
1411
|
+
renderSessions();
|
|
1412
|
+
viewRendered['sessions'] = true;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// ── feature 1: auto-tags ───────────────────────────────────
|
|
1416
|
+
const STOP_WORDS = new Set('the a an and or but in on at to for of is are was were be been being have has had do does did will would could should may might shall can i you he she it we they this that these those with from by about as into through during before after above below up down out off over under again further then once here there when where why how all any both each few more most other some such no nor not only own same so than too very just because if although when while'.split(' '));
|
|
1417
|
+
|
|
1418
|
+
function extractTags(sessions) {
|
|
1419
|
+
// compute per-session tags from lastPrompt text
|
|
1420
|
+
const sessionTags = new Map();
|
|
1421
|
+
const globalFreq = {};
|
|
1422
|
+
for (const s of sessions) {
|
|
1423
|
+
const text = (s.lastPrompt || '').toLowerCase();
|
|
1424
|
+
const words = text.match(/\b[a-z][a-z0-9_-]{2,}\b/g) || [];
|
|
1425
|
+
const freq = {};
|
|
1426
|
+
for (const w of words) {
|
|
1427
|
+
if (!STOP_WORDS.has(w)) freq[w] = (freq[w] || 0) + 1;
|
|
1428
|
+
}
|
|
1429
|
+
// top 3 words for this session
|
|
1430
|
+
const top = Object.entries(freq).sort((a, b) => b[1] - a[1]).slice(0, 3).map(e => e[0]);
|
|
1431
|
+
sessionTags.set(s.id, top);
|
|
1432
|
+
for (const t of top) globalFreq[t] = (globalFreq[t] || 0) + 1;
|
|
1433
|
+
}
|
|
1434
|
+
// only keep tags that appear in 2+ sessions OR are in the current session
|
|
1435
|
+
const common = new Set(Object.entries(globalFreq).filter(([, v]) => v >= 2).map(([k]) => k));
|
|
1436
|
+
return { sessionTags, common };
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
let allTags = { sessionTags: new Map(), common: new Set() };
|
|
1440
|
+
let activeTagFilter = null;
|
|
1441
|
+
|
|
1442
|
+
function initTags() {
|
|
1443
|
+
allTags = extractTags(allSessions);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function buildTagFilterBar(sessions) {
|
|
1447
|
+
if (!allTags.common.size) return '';
|
|
1448
|
+
const sorted = [...allTags.common].sort();
|
|
1449
|
+
const chips = sorted.map(t =>
|
|
1450
|
+
`<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" onclick="setTagFilter('${esc(t)}')">${esc(t)}</button>`
|
|
1451
|
+
).join('');
|
|
1452
|
+
return `<div class="tag-filter-bar">${chips}</div>`;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function setTagFilter(tag) {
|
|
1456
|
+
activeTagFilter = activeTagFilter === tag ? null : tag;
|
|
1457
|
+
viewRendered['sessions'] = false;
|
|
1458
|
+
renderSessions();
|
|
1459
|
+
viewRendered['sessions'] = true;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// ── feature 2: session recap ───────────────────────────────
|
|
1463
|
+
function buildRecap(events, sess) {
|
|
1464
|
+
const recap = document.getElementById('feed-recap');
|
|
1465
|
+
if (!recap) return;
|
|
1466
|
+
const tools = events.filter(e => e.kind === 'tool');
|
|
1467
|
+
const users = events.filter(e => e.kind === 'user');
|
|
1468
|
+
const errors = events.filter(e => e._errored);
|
|
1469
|
+
if (!tools.length && !users.length) { recap.className = ''; return; }
|
|
1470
|
+
|
|
1471
|
+
// dominant tool category
|
|
1472
|
+
const cats = {};
|
|
1473
|
+
for (const e of tools) cats[e.cat || 'other'] = (cats[e.cat || 'other'] || 0) + 1;
|
|
1474
|
+
const topCat = Object.entries(cats).sort((a, b) => b[1] - a[1])[0];
|
|
1475
|
+
const topPct = topCat ? Math.round(topCat[1] / tools.length * 100) : 0;
|
|
1476
|
+
|
|
1477
|
+
const costStr = sess?.totalCost != null ? '$' + sess.totalCost.toFixed(2) : (sess?.cost != null ? '$' + sess.cost.toFixed(2) : null);
|
|
1478
|
+
|
|
1479
|
+
const stats = [
|
|
1480
|
+
tools.length ? `<span class="recap-stat rs-tool">${tools.length} tool calls${topCat ? ' · ' + topPct + '% ' + topCat[0] : ''}</span>` : '',
|
|
1481
|
+
users.length ? `<span class="recap-stat rs-user">${users.length} message${users.length !== 1 ? 's' : ''}</span>` : '',
|
|
1482
|
+
costStr ? `<span class="recap-stat rs-cost">${costStr}</span>` : '',
|
|
1483
|
+
errors.length ? `<span class="recap-stat rs-err">${errors.length} error${errors.length !== 1 ? 's' : ''}</span>` : '',
|
|
1484
|
+
].filter(Boolean).join('');
|
|
1485
|
+
|
|
1486
|
+
recap.innerHTML = `<div class="recap-text">${stats}</div>`;
|
|
1487
|
+
recap.className = 'show';
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// ── feature 3: global feed ─────────────────────────────────
|
|
1491
|
+
async function renderGlobalFeed() {
|
|
1492
|
+
const el = document.getElementById('gf-content');
|
|
1493
|
+
el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
|
|
1494
|
+
try {
|
|
1495
|
+
// fetch project list using ORIGINAL_DIR
|
|
1496
|
+
const data = await apiFetch('/api/data?dir=' + enc(ORIGINAL_DIR));
|
|
1497
|
+
const projects = (data?.allProjects || []).slice(0, 8);
|
|
1498
|
+
if (!projects.length) {
|
|
1499
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No projects found</div></div>';
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
document.getElementById('gf-sub').textContent = `Last activity across ${projects.length} projects`;
|
|
1503
|
+
|
|
1504
|
+
// fetch sessions for each project in parallel
|
|
1505
|
+
const results = await Promise.allSettled(
|
|
1506
|
+
projects.map(p => apiFetch('/api/session-journal?dir=' + enc(p.path)).then(d => ({ project: p, sessions: d.sessions || [] })))
|
|
1507
|
+
);
|
|
1508
|
+
|
|
1509
|
+
// flatten + sort by recency
|
|
1510
|
+
const entries = [];
|
|
1511
|
+
for (const r of results) {
|
|
1512
|
+
if (r.status !== 'fulfilled') continue;
|
|
1513
|
+
const { project, sessions } = r.value;
|
|
1514
|
+
for (const s of sessions.slice(0, 3)) {
|
|
1515
|
+
entries.push({ project, session: s });
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
entries.sort((a, b) => {
|
|
1519
|
+
const ta = a.session.lastTs || a.session.mtime || 0;
|
|
1520
|
+
const tb = b.session.lastTs || b.session.mtime || 0;
|
|
1521
|
+
return (typeof tb === 'number' ? tb : new Date(tb).getTime()) - (typeof ta === 'number' ? ta : new Date(ta).getTime());
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
if (!entries.length) {
|
|
1525
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No sessions found</div></div>';
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
el.innerHTML = '<div class="sess-list">' + entries.map(({ project, session: s }) => {
|
|
1530
|
+
const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
|
|
1531
|
+
const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
|
|
1532
|
+
const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2) : '';
|
|
1533
|
+
const meta = [dur, cost].filter(Boolean).join(' · ') || s.id.slice(0, 12);
|
|
1534
|
+
return `<div class="sess-row" onclick="switchProject('${esc(project.path)}');setTimeout(()=>jumpToSession('${esc(s.id)}'),150)">
|
|
1535
|
+
<div class="sr-top">
|
|
1536
|
+
<div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
|
|
1537
|
+
<div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>
|
|
1538
|
+
<span class="gf-proj-tag">${esc(projName)}</span>
|
|
1539
|
+
</div>
|
|
1540
|
+
<div class="sr-meta">${esc(meta)}</div>
|
|
1541
|
+
</div>`;
|
|
1542
|
+
}).join('') + '</div>';
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// ── feature 4: budget cap + desktop notification ───────────
|
|
1549
|
+
let budget = JSON.parse(localStorage.getItem('mm-budget') || '{}');
|
|
1550
|
+
|
|
1551
|
+
function openBudgetModal() {
|
|
1552
|
+
const b = budget;
|
|
1553
|
+
document.getElementById('bm-daily').value = b.daily || '';
|
|
1554
|
+
document.getElementById('bm-monthly').value = b.monthly || '';
|
|
1555
|
+
document.getElementById('budget-modal').classList.add('open');
|
|
1556
|
+
document.getElementById('bm-daily').focus();
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function closeBudgetModal() {
|
|
1560
|
+
document.getElementById('budget-modal').classList.remove('open');
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function saveBudget() {
|
|
1564
|
+
budget = {
|
|
1565
|
+
daily: parseFloat(document.getElementById('bm-daily').value) || null,
|
|
1566
|
+
monthly: parseFloat(document.getElementById('bm-monthly').value) || null,
|
|
1567
|
+
};
|
|
1568
|
+
localStorage.setItem('mm-budget', JSON.stringify(budget));
|
|
1569
|
+
closeBudgetModal();
|
|
1570
|
+
checkBudget(); // check immediately
|
|
1571
|
+
updateBudgetBtnStyle();
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function updateBudgetBtnStyle() {
|
|
1575
|
+
const btn = document.getElementById('btn-budget');
|
|
1576
|
+
if (!btn) return;
|
|
1577
|
+
const hasBudget = budget.daily || budget.monthly;
|
|
1578
|
+
btn.style.color = hasBudget ? 'var(--accent)' : '';
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function checkBudget() {
|
|
1582
|
+
const cost = alertState.todayCost;
|
|
1583
|
+
if (!cost) return;
|
|
1584
|
+
if (budget.daily) {
|
|
1585
|
+
const pct = cost / budget.daily;
|
|
1586
|
+
if (pct >= 1 && !dismissedAlerts.has('budget-daily-over')) {
|
|
1587
|
+
alertState.budgetAlert = `Daily budget exceeded: $${cost.toFixed(2)} / $${budget.daily}`;
|
|
1588
|
+
alertState.budgetCls = 'alert-crit';
|
|
1589
|
+
} else if (pct >= 0.8 && !dismissedAlerts.has('budget-daily-warn')) {
|
|
1590
|
+
alertState.budgetAlert = `Approaching daily budget: $${cost.toFixed(2)} / $${budget.daily}`;
|
|
1591
|
+
alertState.budgetCls = 'alert-warn';
|
|
1592
|
+
maybeNotify('monomind budget', `$${cost.toFixed(2)} of $${budget.daily} daily budget used`);
|
|
1593
|
+
} else {
|
|
1594
|
+
alertState.budgetAlert = null;
|
|
1595
|
+
}
|
|
1596
|
+
updateAlerts();
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function maybeNotify(title, body) {
|
|
1601
|
+
if (!('Notification' in window)) return;
|
|
1602
|
+
if (Notification.permission === 'granted') {
|
|
1603
|
+
new Notification(title, { body, icon: '' });
|
|
1604
|
+
} else if (Notification.permission !== 'denied') {
|
|
1605
|
+
Notification.requestPermission().then(p => { if (p === 'granted') new Notification(title, { body }); });
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// ── feature 5: session replay ──────────────────────────────
|
|
1610
|
+
let replayEvents = [];
|
|
1611
|
+
let replayIdx = 0;
|
|
1612
|
+
let replayActive = false;
|
|
1613
|
+
let replayTimer = null;
|
|
1614
|
+
|
|
1615
|
+
function startReplay() {
|
|
1616
|
+
// collect visible feed entries as ordered list
|
|
1617
|
+
const entries = [...document.querySelectorAll('#feed-content .feed-entry')];
|
|
1618
|
+
if (!entries.length) return;
|
|
1619
|
+
replayEvents = entries;
|
|
1620
|
+
replayIdx = 0;
|
|
1621
|
+
replayActive = false;
|
|
1622
|
+
document.getElementById('replay-bar').classList.add('show');
|
|
1623
|
+
// dim all entries
|
|
1624
|
+
entries.forEach(el => { el.style.opacity = '0.2'; el.style.transition = 'opacity 0.15s'; });
|
|
1625
|
+
highlightReplayEntry();
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function stopReplay() {
|
|
1629
|
+
clearInterval(replayTimer);
|
|
1630
|
+
replayActive = false;
|
|
1631
|
+
replayEvents.forEach(el => { el.style.opacity = ''; el.style.transition = ''; });
|
|
1632
|
+
replayEvents = [];
|
|
1633
|
+
document.getElementById('replay-bar').classList.remove('show');
|
|
1634
|
+
document.getElementById('rp-play').textContent = '▶';
|
|
1635
|
+
document.getElementById('rp-play').classList.remove('active');
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function highlightReplayEntry() {
|
|
1639
|
+
replayEvents.forEach((el, i) => {
|
|
1640
|
+
el.style.opacity = i === replayIdx ? '1' : (i < replayIdx ? '0.5' : '0.2');
|
|
1641
|
+
});
|
|
1642
|
+
const total = replayEvents.length;
|
|
1643
|
+
const pct = total > 1 ? Math.round(replayIdx / (total - 1) * 100) : 100;
|
|
1644
|
+
document.getElementById('rp-fill').style.width = pct + '%';
|
|
1645
|
+
document.getElementById('rp-counter').textContent = `${replayIdx + 1} / ${total}`;
|
|
1646
|
+
// scroll into view
|
|
1647
|
+
replayEvents[replayIdx]?.scrollIntoView({ block: 'nearest' });
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function replayStep(dir) {
|
|
1651
|
+
replayIdx = Math.max(0, Math.min(replayEvents.length - 1, replayIdx + dir));
|
|
1652
|
+
highlightReplayEntry();
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function replayToggle() {
|
|
1656
|
+
if (!replayEvents.length) { startReplay(); return; }
|
|
1657
|
+
replayActive = !replayActive;
|
|
1658
|
+
const btn = document.getElementById('rp-play');
|
|
1659
|
+
btn.textContent = replayActive ? '⏸' : '▶';
|
|
1660
|
+
btn.classList.toggle('active', replayActive);
|
|
1661
|
+
if (replayActive) {
|
|
1662
|
+
replayTimer = setInterval(() => {
|
|
1663
|
+
if (replayIdx >= replayEvents.length - 1) { replayToggle(); return; }
|
|
1664
|
+
replayStep(1);
|
|
1665
|
+
}, 600);
|
|
1666
|
+
} else {
|
|
1667
|
+
clearInterval(replayTimer);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// ── feature 6: project health score ───────────────────────
|
|
1672
|
+
function computeHealthScore(p) {
|
|
1673
|
+
let score = 50; // base
|
|
1674
|
+
const now = Date.now();
|
|
1675
|
+
const DAY = 86400000;
|
|
1676
|
+
// recency: up to +30 points for activity in last 7 days
|
|
1677
|
+
if (p.lastActivity) {
|
|
1678
|
+
const age = now - (typeof p.lastActivity === 'number' ? p.lastActivity : new Date(p.lastActivity).getTime());
|
|
1679
|
+
if (age < DAY) score += 30;
|
|
1680
|
+
else if (age < 3*DAY) score += 20;
|
|
1681
|
+
else if (age < 7*DAY) score += 10;
|
|
1682
|
+
else if (age > 30*DAY) score -= 15;
|
|
1683
|
+
}
|
|
1684
|
+
// session count: up to +15
|
|
1685
|
+
const sc = p.sessionCount || 0;
|
|
1686
|
+
score += Math.min(15, sc * 2);
|
|
1687
|
+
// memory: up to +5
|
|
1688
|
+
score += Math.min(5, (p.memoryCount || 0));
|
|
1689
|
+
return Math.max(0, Math.min(99, Math.round(score)));
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function healthClass(score) {
|
|
1693
|
+
if (score >= 70) return 'ph-hi';
|
|
1694
|
+
if (score >= 40) return 'ph-mid';
|
|
1695
|
+
return 'ph-lo';
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1180
1698
|
// ── loops ──────────────────────────────────────────────────
|
|
1181
1699
|
async function renderLoops() {
|
|
1182
1700
|
const el = document.getElementById('loops-content');
|
|
@@ -1516,6 +2034,16 @@ function cmdSearch(q) {
|
|
|
1516
2034
|
const lq = q.toLowerCase().trim();
|
|
1517
2035
|
const results = document.getElementById('cmd-results');
|
|
1518
2036
|
|
|
2037
|
+
// ">" prefix = cross-session full-text search
|
|
2038
|
+
if (q.startsWith('>')) {
|
|
2039
|
+
const sq = q.slice(1).trim();
|
|
2040
|
+
results.innerHTML = sq.length >= 2
|
|
2041
|
+
? '<div class="cmd-empty">Searching sessions…</div>'
|
|
2042
|
+
: '<div class="cmd-empty">Type at least 2 chars after > to search all sessions</div>';
|
|
2043
|
+
if (sq.length >= 2) searchSessions(sq);
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1519
2047
|
const sessMatches = allSessions
|
|
1520
2048
|
.filter(s => !lq || (s.lastPrompt || s.id).toLowerCase().includes(lq))
|
|
1521
2049
|
.slice(0, 5);
|
|
@@ -1617,6 +2145,40 @@ function executeCmdItem() {
|
|
|
1617
2145
|
else if (item.type === 'project') switchProject(item.data.path);
|
|
1618
2146
|
}
|
|
1619
2147
|
|
|
2148
|
+
// ── cross-session search ───────────────────────────────────
|
|
2149
|
+
async function searchSessions(q) {
|
|
2150
|
+
const resultsEl = document.getElementById('cmd-results');
|
|
2151
|
+
try {
|
|
2152
|
+
const data = await apiFetch('/api/search-sessions?dir=' + enc(DIR) + '&q=' + enc(q));
|
|
2153
|
+
if (!data.results?.length) {
|
|
2154
|
+
resultsEl.innerHTML = '<div class="cmd-empty">No matches found across sessions</div>';
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
cmdItems = [];
|
|
2158
|
+
let html = '<div class="cmd-group-lbl">Matches across sessions</div>';
|
|
2159
|
+
data.results.forEach(r => {
|
|
2160
|
+
const idx = cmdItems.length;
|
|
2161
|
+
cmdItems.push({ type: 'session', data: { id: r.id, lastPrompt: r.lastPrompt, lastTs: r.mtime } });
|
|
2162
|
+
const snippet = r.matches[0]?.text?.replace(/\s+/g, ' ').trim() || '';
|
|
2163
|
+
html += `<div class="cmd-item" data-ci="${idx}">
|
|
2164
|
+
<span class="ci-ico">◫</span>
|
|
2165
|
+
<div class="cmd-item-body">
|
|
2166
|
+
<div class="ci-title">${esc(r.lastPrompt || r.id.slice(0, 32))}</div>
|
|
2167
|
+
<div class="ci-sub">${esc(snippet.length > 70 ? snippet.slice(0, 70) + '…' : snippet)}</div>
|
|
2168
|
+
</div>
|
|
2169
|
+
</div>`;
|
|
2170
|
+
});
|
|
2171
|
+
resultsEl.innerHTML = html;
|
|
2172
|
+
cmdFocusIdx = 0;
|
|
2173
|
+
updateCmdFocus();
|
|
2174
|
+
resultsEl.querySelectorAll('.cmd-item').forEach(el => {
|
|
2175
|
+
el.addEventListener('click', () => { cmdFocusIdx = parseInt(el.dataset.ci); executeCmdItem(); });
|
|
2176
|
+
});
|
|
2177
|
+
} catch (_) {
|
|
2178
|
+
resultsEl.innerHTML = '<div class="cmd-empty">Search error — is the server running?</div>';
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
1620
2182
|
// ── keyboard shortcuts ─────────────────────────────────────
|
|
1621
2183
|
document.addEventListener('keydown', e => {
|
|
1622
2184
|
// ⌘K / Ctrl+K — command palette
|