@monoes/monomindcli 1.10.31 → 1.10.33
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/budget-status-handler.cjs +14 -0
- package/.claude/helpers/handlers/compact-handler.cjs +33 -0
- package/.claude/helpers/handlers/loops-status-handler.cjs +45 -0
- package/.claude/helpers/handlers/session-restore-handler.cjs +21 -15
- package/.claude/helpers/handlers/stats-handler.cjs +14 -0
- package/.claude/helpers/hook-handler.cjs +10 -70
- package/.claude/helpers/statusline.cjs +16 -5
- 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/ui/.monomind/sessions/current.json +1 -1
- package/dist/src/ui/dashboard-v2.html +770 -15
- package/dist/src/ui/server.mjs +34 -0
- package/package.json +1 -1
|
@@ -74,7 +74,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
74
74
|
#feed-sess-nav { margin-left: auto; display: flex; gap: 6px; align-items: center; }
|
|
75
75
|
.sess-btn { font-size: 11px; color: var(--text-lo); background: var(--surface); border: 1px solid var(--border); border-radius: 4px; padding: 2px 8px; cursor: pointer; transition: color 0.1s; line-height: 1.4; }
|
|
76
76
|
.sess-btn:hover { color: var(--text-hi); }
|
|
77
|
-
#feed-scroll { flex: 1; overflow-y: auto; }
|
|
77
|
+
#feed-scroll { flex: 1; overflow-y: auto; min-width: 0; }
|
|
78
78
|
#feed-scroll::-webkit-scrollbar { width: 3px; }
|
|
79
79
|
#feed-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
80
80
|
|
|
@@ -388,6 +388,136 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
388
388
|
|
|
389
389
|
/* ── forecast row ────────────────────────────────────────── */
|
|
390
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
|
+
/* ── ambient mode ────────────────────────────────────────── */
|
|
429
|
+
#app.ambient #sidebar,
|
|
430
|
+
#app.ambient #topbar,
|
|
431
|
+
#app.ambient #alerts-rail,
|
|
432
|
+
#app.ambient #feed-head,
|
|
433
|
+
#app.ambient #feed-time-filter,
|
|
434
|
+
#app.ambient #metrics-pane,
|
|
435
|
+
#app.ambient #replay-bar,
|
|
436
|
+
#app.ambient #feed-recap,
|
|
437
|
+
#app.ambient #feed-timeline { display:none !important; }
|
|
438
|
+
#app.ambient #main { background:var(--bg); }
|
|
439
|
+
#app.ambient #view-now { height:100vh; }
|
|
440
|
+
#app.ambient #feed-pane { border:none; }
|
|
441
|
+
#app.ambient .feed-entry { padding: 6px 22px; }
|
|
442
|
+
#app.ambient .feed-lbl { font-size:14px; }
|
|
443
|
+
#app-ambient-hint { display:none; position:fixed; bottom:16px; right:16px; font-size:11px; color:var(--text-xs); z-index:300; pointer-events:none; }
|
|
444
|
+
#app.ambient #app-ambient-hint { display:block; }
|
|
445
|
+
|
|
446
|
+
/* ── minimap scrubber ─────────────────────────────────────── */
|
|
447
|
+
#feed-minimap { position:absolute; top:0; right:0; width:8px; height:100%; z-index:10; cursor:pointer; }
|
|
448
|
+
#feed-minimap-track { position:absolute; inset:0; }
|
|
449
|
+
.mm-pip { position:absolute; right:1px; width:6px; border-radius:3px; min-height:3px; opacity:0.55; transition:opacity 0.1s; }
|
|
450
|
+
.mm-pip:hover { opacity:1; }
|
|
451
|
+
.mm-pip.mp-file { background:oklch(60% 0.12 220); }
|
|
452
|
+
.mm-pip.mp-bash { background:oklch(72% 0.18 75); }
|
|
453
|
+
.mm-pip.mp-agent { background:oklch(70% 0.15 300); }
|
|
454
|
+
.mm-pip.mp-mcp { background:oklch(65% 0.15 200); }
|
|
455
|
+
.mm-pip.mp-err { background:var(--red); opacity:0.8; }
|
|
456
|
+
.mm-pip.mp-user { background:var(--text-xs); }
|
|
457
|
+
.mm-pip.mp-other { background:var(--surface-hi); }
|
|
458
|
+
#mm-thumb { position:absolute; right:0; width:8px; background:oklch(100% 0 0 / 0.08); border-radius:4px; pointer-events:none; transition:top 0.05s; }
|
|
459
|
+
#feed-scroll-wrap { position:relative; flex:1; overflow:hidden; display:flex; }
|
|
460
|
+
#feed-scroll { flex:1; }
|
|
461
|
+
|
|
462
|
+
/* ── daily digest ─────────────────────────────────────────── */
|
|
463
|
+
#digest-card { display:none; flex-shrink:0; border-bottom:1px solid var(--border); background:oklch(13.5% 0.009 55); padding:10px 18px; }
|
|
464
|
+
#digest-card.show { display:block; }
|
|
465
|
+
.digest-row { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
|
|
466
|
+
.digest-title { font-size:11px; font-weight:600; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-lo); margin-bottom:6px; display:flex; align-items:center; gap:8px; }
|
|
467
|
+
.digest-close { margin-left:auto; background:none; border:none; color:var(--text-xs); cursor:pointer; font-size:13px; line-height:1; padding:0; }
|
|
468
|
+
.digest-close:hover { color:var(--text-lo); }
|
|
469
|
+
.digest-stat { display:inline-flex; align-items:center; gap:4px; font-size:11px; padding:2px 8px; border-radius:8px; background:var(--surface-hi); color:var(--text-mid); white-space:nowrap; }
|
|
470
|
+
|
|
471
|
+
/* ── cost leaderboard ────────────────────────────────────── */
|
|
472
|
+
.lb-toggle { 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); }
|
|
473
|
+
.lb-toggle:hover { color:var(--text-hi); }
|
|
474
|
+
.lb-toggle.on { background:oklch(72% 0.18 75 / 0.1); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
|
|
475
|
+
.lb-table { width:100%; border-collapse:collapse; margin-top:4px; }
|
|
476
|
+
.lb-table th { font-size:10px; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-xs); padding:4px 6px; text-align:left; border-bottom:1px solid var(--border); }
|
|
477
|
+
.lb-table td { font-size:12px; padding:7px 6px; border-bottom:1px solid oklch(25% 0.008 55 / 0.5); color:var(--text-mid); vertical-align:top; cursor:pointer; }
|
|
478
|
+
.lb-table tr:hover td { background:var(--surface-hi); color:var(--text-hi); }
|
|
479
|
+
.lb-rank { font-family:var(--mono); color:var(--text-xs); width:22px; }
|
|
480
|
+
.lb-cost { font-family:var(--mono); color:oklch(78% 0.18 75); white-space:nowrap; }
|
|
481
|
+
.lb-dur { font-family:var(--mono); color:var(--text-lo); white-space:nowrap; }
|
|
482
|
+
.lb-prompt { max-width:260px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
483
|
+
|
|
484
|
+
/* ── session diff ─────────────────────────────────────────── */
|
|
485
|
+
.diff-toggle { 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); }
|
|
486
|
+
.diff-toggle:hover { color:var(--text-hi); }
|
|
487
|
+
.diff-toggle.on { background:oklch(60% 0.12 220 / 0.1); color:oklch(65% 0.12 220); border-color:oklch(60% 0.12 220 / 0.3); }
|
|
488
|
+
#diff-panel { display:none; border:1px solid var(--border); border-radius:8px; margin-bottom:16px; overflow:hidden; }
|
|
489
|
+
#diff-panel.show { display:block; }
|
|
490
|
+
.diff-header { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--border); background:oklch(14% 0.009 55); }
|
|
491
|
+
.diff-title { font-size:11px; font-weight:600; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-lo); flex:1; }
|
|
492
|
+
.diff-clear { background:none; border:none; color:var(--text-xs); cursor:pointer; font-size:12px; }
|
|
493
|
+
.diff-cols { display:grid; grid-template-columns:1fr 1fr; }
|
|
494
|
+
.diff-col { padding:10px 14px; }
|
|
495
|
+
.diff-col + .diff-col { border-left:1px solid var(--border); }
|
|
496
|
+
.diff-col-title { font-size:11px; font-weight:600; color:var(--text-mid); margin-bottom:8px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
497
|
+
.diff-row { display:flex; justify-content:space-between; gap:8px; padding:3px 0; border-bottom:1px solid oklch(25% 0.008 55 / 0.4); }
|
|
498
|
+
.diff-row:last-child { border-bottom:none; }
|
|
499
|
+
.diff-k { font-size:11px; color:var(--text-xs); }
|
|
500
|
+
.diff-v { font-size:11px; font-family:var(--mono); color:var(--text-mid); }
|
|
501
|
+
.diff-v.diff-hi { color:oklch(78% 0.18 75); }
|
|
502
|
+
.diff-v.diff-lo { color:var(--red); }
|
|
503
|
+
.diff-hint { font-size:11px; color:var(--text-xs); padding:8px 12px; text-align:center; }
|
|
504
|
+
.sess-row.diff-sel-a { background:oklch(60% 0.12 220 / 0.08); outline:1px solid oklch(60% 0.12 220 / 0.3); }
|
|
505
|
+
.sess-row.diff-sel-b { background:oklch(72% 0.18 75 / 0.06); outline:1px solid oklch(72% 0.18 75 / 0.3); }
|
|
506
|
+
|
|
507
|
+
/* ── budget cap ──────────────────────────────────────────── */
|
|
508
|
+
#budget-modal { display:none; position:fixed; inset:0; z-index:200; background:oklch(5% 0 0 / 0.6); align-items:center; justify-content:center; }
|
|
509
|
+
#budget-modal.open { display:flex; }
|
|
510
|
+
#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); }
|
|
511
|
+
.bm-title { font-size:14px; font-weight:600; color:var(--text-hi); margin-bottom:14px; }
|
|
512
|
+
.bm-row { display:flex; flex-direction:column; gap:4px; margin-bottom:12px; }
|
|
513
|
+
.bm-lbl { font-size:11px; color:var(--text-lo); }
|
|
514
|
+
.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; }
|
|
515
|
+
.bm-input:focus { border-color:var(--accent); }
|
|
516
|
+
.bm-btns { display:flex; gap:8px; margin-top:16px; }
|
|
517
|
+
.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; }
|
|
518
|
+
.bm-save:hover { background:oklch(68% 0.18 75); }
|
|
519
|
+
.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; }
|
|
520
|
+
.bm-cancel:hover { color:var(--text-hi); }
|
|
391
521
|
</style>
|
|
392
522
|
</head>
|
|
393
523
|
<body>
|
|
@@ -428,6 +558,9 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
428
558
|
<div class="nav-item" data-view="orgs">
|
|
429
559
|
<span class="ico">⬡</span><span class="lbl">Orgs</span>
|
|
430
560
|
</div>
|
|
561
|
+
<div class="nav-item" data-view="global">
|
|
562
|
+
<span class="ico">⊕</span><span class="lbl">Global Feed</span>
|
|
563
|
+
</div>
|
|
431
564
|
</div>
|
|
432
565
|
</div>
|
|
433
566
|
<div id="sb-footer">
|
|
@@ -443,6 +576,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
443
576
|
<span class="pill"><span class="live-dot"></span> live</span>
|
|
444
577
|
<span id="topbar-cost"></span>
|
|
445
578
|
<div id="tb-right">
|
|
579
|
+
<button class="btn" id="btn-budget" onclick="openBudgetModal()" title="Set daily/monthly cost budget">⚑ Budget</button>
|
|
446
580
|
<button class="btn" onclick="openCmdPalette()">⌕ Search <kbd style="font-size:10px;opacity:0.6;margin-left:3px">⌘K</kbd></button>
|
|
447
581
|
<button class="btn" onclick="refreshCurrent()">↺ Refresh</button>
|
|
448
582
|
</div>
|
|
@@ -476,6 +610,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
476
610
|
<span id="feed-sess">—</span>
|
|
477
611
|
<div id="feed-sess-nav">
|
|
478
612
|
<button class="live-tail-btn" id="btn-live-tail" onclick="toggleLiveTail()" title="Toggle live tail (auto-scroll + 5s refresh)">⬤ Tail</button>
|
|
613
|
+
<button class="sess-copy-btn" id="btn-replay" onclick="replayToggle()" title="Replay session event-by-event">⏵ Replay</button>
|
|
479
614
|
<button class="sess-copy-btn" id="btn-copy-sess" onclick="copySession()" title="Copy session as markdown">⎘ Copy</button>
|
|
480
615
|
<button class="density-btn" id="btn-density" onclick="toggleDensity()" title="Toggle compact view">⊟</button>
|
|
481
616
|
<button class="sess-btn" onclick="toggleFeedSearch()" title="Search in feed (/)">⌕</button>
|
|
@@ -494,6 +629,15 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
494
629
|
<span class="sctx-label" id="sctx-label"></span>
|
|
495
630
|
<button class="sctx-live" onclick="goLive()">⬤ Go live</button>
|
|
496
631
|
</div>
|
|
632
|
+
<div id="feed-recap"></div>
|
|
633
|
+
<div id="replay-bar">
|
|
634
|
+
<button class="rp-btn" id="rp-play" onclick="replayToggle()" title="Play/pause replay">▶</button>
|
|
635
|
+
<button class="rp-btn" onclick="replayStep(-1)" title="Step back">‹</button>
|
|
636
|
+
<button class="rp-btn" onclick="replayStep(1)" title="Step forward">›</button>
|
|
637
|
+
<div id="rp-progress"><div id="rp-fill" style="width:0%"></div></div>
|
|
638
|
+
<span id="rp-counter">0 / 0</span>
|
|
639
|
+
<button class="rp-btn" onclick="stopReplay()" title="Exit replay">✕</button>
|
|
640
|
+
</div>
|
|
497
641
|
<div id="feed-timeline" title="Session tool activity timeline"></div>
|
|
498
642
|
<div id="feed-time-filter">
|
|
499
643
|
<span class="tf-lbl">Range</span>
|
|
@@ -501,10 +645,20 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
501
645
|
<button class="tf-btn" data-tf="1h" onclick="setFeedTimeFilter('1h')">1h</button>
|
|
502
646
|
<button class="tf-btn" data-tf="6h" onclick="setFeedTimeFilter('6h')">6h</button>
|
|
503
647
|
<button class="tf-btn" data-tf="24h" onclick="setFeedTimeFilter('24h')">24h</button>
|
|
504
|
-
<span class="kb-hint"><kbd>J</kbd><kbd>K</kbd> navigate <kbd>↵</kbd> detail <kbd>/</kbd> find <kbd>G</kbd> live <kbd>⌘K</kbd> search</span>
|
|
648
|
+
<span class="kb-hint"><kbd>J</kbd><kbd>K</kbd> navigate <kbd>↵</kbd> detail <kbd>/</kbd> find <kbd>G</kbd> live <kbd>A</kbd> ambient <kbd>⌘K</kbd> search</span>
|
|
505
649
|
</div>
|
|
506
|
-
<div id="
|
|
507
|
-
<div
|
|
650
|
+
<div id="digest-card">
|
|
651
|
+
<div class="digest-title">Today's Digest <button class="digest-close" onclick="dismissDigest()" title="Dismiss">✕</button></div>
|
|
652
|
+
<div class="digest-row" id="digest-stats"></div>
|
|
653
|
+
</div>
|
|
654
|
+
<div id="feed-scroll-wrap">
|
|
655
|
+
<div id="feed-scroll">
|
|
656
|
+
<div id="feed-content"><div class="loading-txt">Loading activity…</div></div>
|
|
657
|
+
</div>
|
|
658
|
+
<div id="feed-minimap" title="Click to jump · events map">
|
|
659
|
+
<div id="feed-minimap-track"></div>
|
|
660
|
+
<div id="mm-thumb"></div>
|
|
661
|
+
</div>
|
|
508
662
|
</div>
|
|
509
663
|
</div>
|
|
510
664
|
|
|
@@ -552,11 +706,23 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
552
706
|
<!-- SESSIONS -->
|
|
553
707
|
<div class="view" id="view-sessions">
|
|
554
708
|
<div class="vscroll">
|
|
555
|
-
<div style="display:flex;align-items:
|
|
709
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;flex-wrap:wrap">
|
|
556
710
|
<div class="pg-title" style="margin-bottom:0">Sessions</div>
|
|
557
711
|
<button id="sess-star-filter" onclick="toggleSessStarFilter()" title="Show only bookmarked sessions">☆ Starred</button>
|
|
712
|
+
<button class="lb-toggle" id="btn-leaderboard" onclick="toggleLeaderboard()" title="Cost leaderboard">⬆ Leaderboard</button>
|
|
713
|
+
<button class="diff-toggle" id="btn-diff" onclick="toggleDiffMode()" title="Compare two sessions">⇄ Compare</button>
|
|
558
714
|
</div>
|
|
559
715
|
<div class="pg-sub" id="sess-pg-sub">Recent Claude Code sessions for this project</div>
|
|
716
|
+
<div id="diff-panel">
|
|
717
|
+
<div class="diff-header"><span class="diff-title">Session Comparison</span><button class="diff-clear" onclick="clearDiff()" title="Clear">✕</button></div>
|
|
718
|
+
<div class="diff-hint" id="diff-hint">Click two sessions below to compare them</div>
|
|
719
|
+
<div class="diff-cols" id="diff-cols" style="display:none"></div>
|
|
720
|
+
</div>
|
|
721
|
+
<div id="lb-panel" style="display:none;margin-bottom:16px">
|
|
722
|
+
<table class="lb-table"><thead><tr>
|
|
723
|
+
<th class="lb-rank">#</th><th>Session</th><th class="lb-cost">Cost</th><th class="lb-dur">Duration</th>
|
|
724
|
+
</tr></thead><tbody id="lb-body"></tbody></table>
|
|
725
|
+
</div>
|
|
560
726
|
<div id="sess-content" class="sess-list"><div class="loading-txt">Loading…</div></div>
|
|
561
727
|
</div>
|
|
562
728
|
</div>
|
|
@@ -592,10 +758,41 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
592
758
|
</div>
|
|
593
759
|
</div>
|
|
594
760
|
|
|
761
|
+
<!-- GLOBAL FEED -->
|
|
762
|
+
<div class="view" id="view-global">
|
|
763
|
+
<div class="vscroll">
|
|
764
|
+
<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
|
|
765
|
+
<div class="pg-title" style="margin-bottom:0">Global Feed</div>
|
|
766
|
+
<span class="pg-sub" id="gf-sub" style="margin-bottom:0">Activity across all projects</span>
|
|
767
|
+
</div>
|
|
768
|
+
<div id="gf-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
|
|
769
|
+
</div>
|
|
770
|
+
</div>
|
|
771
|
+
|
|
595
772
|
</div><!-- /view-wrap -->
|
|
596
773
|
</div><!-- /main -->
|
|
774
|
+
<div id="app-ambient-hint">Press A to exit ambient mode</div>
|
|
597
775
|
</div><!-- /app -->
|
|
598
776
|
|
|
777
|
+
<!-- budget modal (fixed overlay, outside app) -->
|
|
778
|
+
<div id="budget-modal" onclick="if(event.target===this)closeBudgetModal()">
|
|
779
|
+
<div id="budget-box">
|
|
780
|
+
<div class="bm-title">Set Cost Budget</div>
|
|
781
|
+
<div class="bm-row">
|
|
782
|
+
<div class="bm-lbl">Daily limit ($)</div>
|
|
783
|
+
<input class="bm-input" id="bm-daily" type="number" min="0" step="1" placeholder="e.g. 20">
|
|
784
|
+
</div>
|
|
785
|
+
<div class="bm-row">
|
|
786
|
+
<div class="bm-lbl">Monthly limit ($)</div>
|
|
787
|
+
<input class="bm-input" id="bm-monthly" type="number" min="0" step="10" placeholder="e.g. 200">
|
|
788
|
+
</div>
|
|
789
|
+
<div class="bm-btns">
|
|
790
|
+
<button class="bm-cancel" onclick="closeBudgetModal()">Cancel</button>
|
|
791
|
+
<button class="bm-save" onclick="saveBudget()">Save</button>
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
</div>
|
|
795
|
+
|
|
599
796
|
<script>
|
|
600
797
|
// ── state ──────────────────────────────────────────────────
|
|
601
798
|
let DIR = '';
|
|
@@ -611,7 +808,7 @@ let userScrolled = false;
|
|
|
611
808
|
let selectedEntryId = null;
|
|
612
809
|
let allDrawers = [];
|
|
613
810
|
let dismissedAlerts = new Set();
|
|
614
|
-
let alertState = { todayCost: 0, errorCount: 0, longLoops: [], anomaly: null };
|
|
811
|
+
let alertState = { todayCost: 0, errorCount: 0, longLoops: [], anomaly: null, budgetAlert: null, budgetCls: 'alert-warn' };
|
|
615
812
|
let feedTimeFilter = 'all';
|
|
616
813
|
let cmdFocusIdx = 0;
|
|
617
814
|
let cmdItems = [];
|
|
@@ -635,7 +832,7 @@ function switchView(v) {
|
|
|
635
832
|
el.classList.toggle('active', el.dataset.view === v));
|
|
636
833
|
document.querySelectorAll('.view').forEach(el =>
|
|
637
834
|
el.classList.toggle('active', el.id === 'view-' + v));
|
|
638
|
-
const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', memory:'Memory', orgs:'Orgs' };
|
|
835
|
+
const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', memory:'Memory', orgs:'Orgs', global:'Global Feed' };
|
|
639
836
|
document.getElementById('view-title').textContent = titles[v] || v;
|
|
640
837
|
if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
|
|
641
838
|
}
|
|
@@ -647,6 +844,7 @@ function renderView(v) {
|
|
|
647
844
|
if (v === 'loops') renderLoops();
|
|
648
845
|
if (v === 'memory') renderMemory();
|
|
649
846
|
if (v === 'orgs') renderOrgs();
|
|
847
|
+
if (v === 'global') renderGlobalFeed();
|
|
650
848
|
}
|
|
651
849
|
|
|
652
850
|
function refreshCurrent() {
|
|
@@ -666,6 +864,7 @@ async function init() {
|
|
|
666
864
|
document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
|
|
667
865
|
} catch (_) {}
|
|
668
866
|
viewRendered['now'] = true; // prevents switchView from re-rendering NOW on jumpToSession
|
|
867
|
+
updateBudgetBtnStyle();
|
|
669
868
|
await refreshNow();
|
|
670
869
|
startPolling();
|
|
671
870
|
}
|
|
@@ -881,7 +1080,10 @@ function renderFeedEvents(events, silent) {
|
|
|
881
1080
|
|
|
882
1081
|
// update timeline + breakdown with all original events (before time-filter)
|
|
883
1082
|
buildTimeline(filtered);
|
|
884
|
-
|
|
1083
|
+
buildBreakdownByName(filtered);
|
|
1084
|
+
buildMinimap(filtered);
|
|
1085
|
+
// session recap card
|
|
1086
|
+
buildRecap(filtered, allSessions[sessionIdx]);
|
|
885
1087
|
}
|
|
886
1088
|
|
|
887
1089
|
function renderGroupRow(g) {
|
|
@@ -1039,6 +1241,10 @@ function updateAlerts() {
|
|
|
1039
1241
|
all.push({ id: 'anomaly-sess', cls: 'alert-warn', ico: '◎', msg: alertState.anomaly });
|
|
1040
1242
|
}
|
|
1041
1243
|
|
|
1244
|
+
if (alertState.budgetAlert) {
|
|
1245
|
+
all.push({ id: 'budget-alert', cls: alertState.budgetCls || 'alert-warn', ico: '⚑', msg: alertState.budgetAlert });
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1042
1248
|
for (const l of alertState.longLoops) {
|
|
1043
1249
|
all.push({ id: 'loop-' + l, cls: 'alert-warn', ico: '↺', msg: `Long-running loop: ${l}` });
|
|
1044
1250
|
}
|
|
@@ -1107,6 +1313,7 @@ async function loadTodayMetrics() {
|
|
|
1107
1313
|
const s = data?.tokens?.summary || {};
|
|
1108
1314
|
alertState.todayCost = typeof s.todayCost === 'number' ? s.todayCost : 0;
|
|
1109
1315
|
updateAlerts();
|
|
1316
|
+
checkBudget();
|
|
1110
1317
|
// topbar cost badge
|
|
1111
1318
|
const badge = document.getElementById('topbar-cost');
|
|
1112
1319
|
if (badge && typeof s.todayCost === 'number') {
|
|
@@ -1183,8 +1390,8 @@ async function renderProjects() {
|
|
|
1183
1390
|
const el = document.getElementById('proj-content');
|
|
1184
1391
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
1185
1392
|
try {
|
|
1186
|
-
const data = await apiFetch('/api/
|
|
1187
|
-
allProjects = data?.
|
|
1393
|
+
const data = await apiFetch('/api/projects');
|
|
1394
|
+
allProjects = data?.projects || [];
|
|
1188
1395
|
document.getElementById('bdg-projects').textContent = allProjects.length || '—';
|
|
1189
1396
|
document.getElementById('proj-pg-sub').textContent =
|
|
1190
1397
|
allProjects.length + ' project' + (allProjects.length !== 1 ? 's' : '') + ' found';
|
|
@@ -1206,8 +1413,11 @@ function renderProjectGrid(projects, query) {
|
|
|
1206
1413
|
el.className = 'proj-grid';
|
|
1207
1414
|
el.innerHTML = filtered.map(p => {
|
|
1208
1415
|
const isCurrent = p.path === DIR;
|
|
1416
|
+
const score = computeHealthScore(p);
|
|
1417
|
+
const hCls = healthClass(score);
|
|
1209
1418
|
return `<div class="proj-card${isCurrent ? ' current' : ''}" onclick="switchProject('${esc(p.path || '')}')">
|
|
1210
1419
|
${isCurrent ? '<div class="proj-card-badge">active</div>' : ''}
|
|
1420
|
+
<div class="proj-health ${hCls}" title="Health score: ${score}">${score}</div>
|
|
1211
1421
|
<div class="proj-card-name">${esc(p.name || p.slug)}</div>
|
|
1212
1422
|
<div class="proj-card-path">${esc(p.path || '')}</div>
|
|
1213
1423
|
<div class="proj-card-stats">
|
|
@@ -1230,6 +1440,7 @@ async function renderSessions() {
|
|
|
1230
1440
|
try {
|
|
1231
1441
|
const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
|
|
1232
1442
|
allSessions = sessions; // always sync — stale ordering breaks jumpToSession
|
|
1443
|
+
initTags();
|
|
1233
1444
|
document.getElementById('bdg-sessions').textContent = sessions.length || '—';
|
|
1234
1445
|
document.getElementById('sess-pg-sub').textContent =
|
|
1235
1446
|
sessions.length + ' session' + (sessions.length !== 1 ? 's' : '') + ' · ' + (DIR.split('/').pop() || DIR);
|
|
@@ -1237,20 +1448,24 @@ async function renderSessions() {
|
|
|
1237
1448
|
el.innerHTML = '<div class="empty"><div class="empty-ico">◫</div><div>No sessions yet</div></div>';
|
|
1238
1449
|
return;
|
|
1239
1450
|
}
|
|
1240
|
-
|
|
1451
|
+
let toShow = showStarredOnly ? sessions.filter(s => bookmarks.has(s.id)) : sessions;
|
|
1452
|
+
if (activeTagFilter) toShow = toShow.filter(s => (allTags.sessionTags.get(s.id) || []).includes(activeTagFilter));
|
|
1241
1453
|
if (!toShow.length) {
|
|
1242
1454
|
el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
|
|
1243
1455
|
return;
|
|
1244
1456
|
}
|
|
1245
|
-
|
|
1457
|
+
const sessData = JSON.stringify(toShow).replace(/'/g, ''');
|
|
1458
|
+
el.innerHTML = toShow.map((s, idx) => {
|
|
1246
1459
|
const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
|
|
1247
1460
|
const msgs = s.totalMessages ? s.totalMessages + ' msg' : '';
|
|
1248
1461
|
const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2)
|
|
1249
1462
|
: typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '';
|
|
1250
1463
|
const meta = [dur, msgs, cost].filter(Boolean).join(' · ') || s.id.slice(0, 16);
|
|
1251
1464
|
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('');
|
|
1465
|
+
const autoTags = (allTags.sessionTags.get(s.id) || []).map(t => `<span class="sr-autotag">${esc(t)}</span>`).join('');
|
|
1252
1466
|
const isStarred = bookmarks.has(s.id);
|
|
1253
|
-
|
|
1467
|
+
const sData = JSON.stringify(s).replace(/'/g, ''');
|
|
1468
|
+
return `<div class="sess-row" data-sess-idx="${idx}" onclick="diffMode ? diffSelectSession(JSON.parse(this.dataset.sessData || '{}'), this) : jumpToSession('${esc(s.id)}')" data-sess-data='${sData}'>
|
|
1254
1469
|
<div class="sr-top">
|
|
1255
1470
|
<div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
|
|
1256
1471
|
<div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>
|
|
@@ -1258,9 +1473,15 @@ async function renderSessions() {
|
|
|
1258
1473
|
<span class="sr-view">→ view</span>
|
|
1259
1474
|
</div>
|
|
1260
1475
|
<div class="sr-meta">${esc(meta)}</div>
|
|
1261
|
-
${summaries ? `<div class="sr-tags">${summaries}</div>` : ''}
|
|
1476
|
+
${(summaries || autoTags) ? `<div class="sr-tags">${summaries}${autoTags}</div>` : ''}
|
|
1262
1477
|
</div>`;
|
|
1263
1478
|
}).join('');
|
|
1479
|
+
// prepend tag filter bar if there are common tags
|
|
1480
|
+
if (allTags.common.size > 1) {
|
|
1481
|
+
el.innerHTML = buildTagFilterBar(toShow) + el.innerHTML;
|
|
1482
|
+
}
|
|
1483
|
+
buildDigest();
|
|
1484
|
+
if (leaderboardOpen) renderLeaderboard();
|
|
1264
1485
|
} catch (err) {
|
|
1265
1486
|
el.innerHTML = '<div class="empty">Could not load sessions: ' + esc(err.message) + '</div>';
|
|
1266
1487
|
}
|
|
@@ -1298,6 +1519,539 @@ function toggleSessStarFilter() {
|
|
|
1298
1519
|
viewRendered['sessions'] = true;
|
|
1299
1520
|
}
|
|
1300
1521
|
|
|
1522
|
+
// ── feature 1: auto-tags ───────────────────────────────────
|
|
1523
|
+
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(' '));
|
|
1524
|
+
|
|
1525
|
+
function extractTags(sessions) {
|
|
1526
|
+
// compute per-session tags from lastPrompt text
|
|
1527
|
+
const sessionTags = new Map();
|
|
1528
|
+
const globalFreq = {};
|
|
1529
|
+
for (const s of sessions) {
|
|
1530
|
+
const text = (s.lastPrompt || '').toLowerCase();
|
|
1531
|
+
const words = text.match(/\b[a-z][a-z0-9_-]{2,}\b/g) || [];
|
|
1532
|
+
const freq = {};
|
|
1533
|
+
for (const w of words) {
|
|
1534
|
+
if (!STOP_WORDS.has(w)) freq[w] = (freq[w] || 0) + 1;
|
|
1535
|
+
}
|
|
1536
|
+
// top 3 words for this session
|
|
1537
|
+
const top = Object.entries(freq).sort((a, b) => b[1] - a[1]).slice(0, 3).map(e => e[0]);
|
|
1538
|
+
sessionTags.set(s.id, top);
|
|
1539
|
+
for (const t of top) globalFreq[t] = (globalFreq[t] || 0) + 1;
|
|
1540
|
+
}
|
|
1541
|
+
// only keep tags that appear in 2+ sessions OR are in the current session
|
|
1542
|
+
const common = new Set(Object.entries(globalFreq).filter(([, v]) => v >= 2).map(([k]) => k));
|
|
1543
|
+
return { sessionTags, common };
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
let allTags = { sessionTags: new Map(), common: new Set() };
|
|
1547
|
+
let activeTagFilter = null;
|
|
1548
|
+
|
|
1549
|
+
function initTags() {
|
|
1550
|
+
allTags = extractTags(allSessions);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function buildTagFilterBar(sessions) {
|
|
1554
|
+
if (!allTags.common.size) return '';
|
|
1555
|
+
const sorted = [...allTags.common].sort();
|
|
1556
|
+
const chips = sorted.map(t =>
|
|
1557
|
+
`<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" onclick="setTagFilter('${esc(t)}')">${esc(t)}</button>`
|
|
1558
|
+
).join('');
|
|
1559
|
+
return `<div class="tag-filter-bar">${chips}</div>`;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function setTagFilter(tag) {
|
|
1563
|
+
activeTagFilter = activeTagFilter === tag ? null : tag;
|
|
1564
|
+
viewRendered['sessions'] = false;
|
|
1565
|
+
renderSessions();
|
|
1566
|
+
viewRendered['sessions'] = true;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// ── feature 2: session recap ───────────────────────────────
|
|
1570
|
+
function buildRecap(events, sess) {
|
|
1571
|
+
const recap = document.getElementById('feed-recap');
|
|
1572
|
+
if (!recap) return;
|
|
1573
|
+
const tools = events.filter(e => e.kind === 'tool');
|
|
1574
|
+
const users = events.filter(e => e.kind === 'user');
|
|
1575
|
+
const errors = events.filter(e => e._errored);
|
|
1576
|
+
if (!tools.length && !users.length) { recap.className = ''; return; }
|
|
1577
|
+
|
|
1578
|
+
// dominant tool category
|
|
1579
|
+
const cats = {};
|
|
1580
|
+
for (const e of tools) cats[e.cat || 'other'] = (cats[e.cat || 'other'] || 0) + 1;
|
|
1581
|
+
const topCat = Object.entries(cats).sort((a, b) => b[1] - a[1])[0];
|
|
1582
|
+
const topPct = topCat ? Math.round(topCat[1] / tools.length * 100) : 0;
|
|
1583
|
+
|
|
1584
|
+
const costStr = sess?.totalCost != null ? '$' + sess.totalCost.toFixed(2) : (sess?.cost != null ? '$' + sess.cost.toFixed(2) : null);
|
|
1585
|
+
|
|
1586
|
+
const stats = [
|
|
1587
|
+
tools.length ? `<span class="recap-stat rs-tool">${tools.length} tool calls${topCat ? ' · ' + topPct + '% ' + topCat[0] : ''}</span>` : '',
|
|
1588
|
+
users.length ? `<span class="recap-stat rs-user">${users.length} message${users.length !== 1 ? 's' : ''}</span>` : '',
|
|
1589
|
+
costStr ? `<span class="recap-stat rs-cost">${costStr}</span>` : '',
|
|
1590
|
+
errors.length ? `<span class="recap-stat rs-err">${errors.length} error${errors.length !== 1 ? 's' : ''}</span>` : '',
|
|
1591
|
+
].filter(Boolean).join('');
|
|
1592
|
+
|
|
1593
|
+
recap.innerHTML = `<div class="recap-text">${stats}</div>`;
|
|
1594
|
+
recap.className = 'show';
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// ── feature 3: global feed ─────────────────────────────────
|
|
1598
|
+
async function renderGlobalFeed() {
|
|
1599
|
+
const el = document.getElementById('gf-content');
|
|
1600
|
+
el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
|
|
1601
|
+
try {
|
|
1602
|
+
// fetch project list
|
|
1603
|
+
const data = await apiFetch('/api/projects');
|
|
1604
|
+
const projects = (data?.projects || []).slice(0, 8);
|
|
1605
|
+
if (!projects.length) {
|
|
1606
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No projects found</div></div>';
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
document.getElementById('gf-sub').textContent = `Last activity across ${projects.length} projects`;
|
|
1610
|
+
|
|
1611
|
+
// fetch sessions for each project in parallel
|
|
1612
|
+
const results = await Promise.allSettled(
|
|
1613
|
+
projects.map(p => apiFetch('/api/session-journal?dir=' + enc(p.path)).then(d => ({ project: p, sessions: d.sessions || [] })))
|
|
1614
|
+
);
|
|
1615
|
+
|
|
1616
|
+
// flatten + sort by recency
|
|
1617
|
+
const entries = [];
|
|
1618
|
+
for (const r of results) {
|
|
1619
|
+
if (r.status !== 'fulfilled') continue;
|
|
1620
|
+
const { project, sessions } = r.value;
|
|
1621
|
+
for (const s of sessions.slice(0, 3)) {
|
|
1622
|
+
entries.push({ project, session: s });
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
entries.sort((a, b) => {
|
|
1626
|
+
const ta = a.session.lastTs || a.session.mtime || 0;
|
|
1627
|
+
const tb = b.session.lastTs || b.session.mtime || 0;
|
|
1628
|
+
return (typeof tb === 'number' ? tb : new Date(tb).getTime()) - (typeof ta === 'number' ? ta : new Date(ta).getTime());
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
if (!entries.length) {
|
|
1632
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No sessions found</div></div>';
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
el.innerHTML = '<div class="sess-list">' + entries.map(({ project, session: s }) => {
|
|
1637
|
+
const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
|
|
1638
|
+
const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
|
|
1639
|
+
const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2) : '';
|
|
1640
|
+
const meta = [dur, cost].filter(Boolean).join(' · ') || s.id.slice(0, 12);
|
|
1641
|
+
return `<div class="sess-row" onclick="switchProject('${esc(project.path)}');setTimeout(()=>jumpToSession('${esc(s.id)}'),150)">
|
|
1642
|
+
<div class="sr-top">
|
|
1643
|
+
<div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
|
|
1644
|
+
<div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>
|
|
1645
|
+
<span class="gf-proj-tag">${esc(projName)}</span>
|
|
1646
|
+
</div>
|
|
1647
|
+
<div class="sr-meta">${esc(meta)}</div>
|
|
1648
|
+
</div>`;
|
|
1649
|
+
}).join('') + '</div>';
|
|
1650
|
+
} catch (err) {
|
|
1651
|
+
el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// ── feature 4: budget cap + desktop notification ───────────
|
|
1656
|
+
let budget = JSON.parse(localStorage.getItem('mm-budget') || '{}');
|
|
1657
|
+
|
|
1658
|
+
function openBudgetModal() {
|
|
1659
|
+
const b = budget;
|
|
1660
|
+
document.getElementById('bm-daily').value = b.daily || '';
|
|
1661
|
+
document.getElementById('bm-monthly').value = b.monthly || '';
|
|
1662
|
+
document.getElementById('budget-modal').classList.add('open');
|
|
1663
|
+
document.getElementById('bm-daily').focus();
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function closeBudgetModal() {
|
|
1667
|
+
document.getElementById('budget-modal').classList.remove('open');
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function saveBudget() {
|
|
1671
|
+
budget = {
|
|
1672
|
+
daily: parseFloat(document.getElementById('bm-daily').value) || null,
|
|
1673
|
+
monthly: parseFloat(document.getElementById('bm-monthly').value) || null,
|
|
1674
|
+
};
|
|
1675
|
+
localStorage.setItem('mm-budget', JSON.stringify(budget));
|
|
1676
|
+
closeBudgetModal();
|
|
1677
|
+
checkBudget(); // check immediately
|
|
1678
|
+
updateBudgetBtnStyle();
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function updateBudgetBtnStyle() {
|
|
1682
|
+
const btn = document.getElementById('btn-budget');
|
|
1683
|
+
if (!btn) return;
|
|
1684
|
+
const hasBudget = budget.daily || budget.monthly;
|
|
1685
|
+
btn.style.color = hasBudget ? 'var(--accent)' : '';
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function checkBudget() {
|
|
1689
|
+
const cost = alertState.todayCost;
|
|
1690
|
+
if (!cost) return;
|
|
1691
|
+
if (budget.daily) {
|
|
1692
|
+
const pct = cost / budget.daily;
|
|
1693
|
+
if (pct >= 1 && !dismissedAlerts.has('budget-daily-over')) {
|
|
1694
|
+
alertState.budgetAlert = `Daily budget exceeded: $${cost.toFixed(2)} / $${budget.daily}`;
|
|
1695
|
+
alertState.budgetCls = 'alert-crit';
|
|
1696
|
+
} else if (pct >= 0.8 && !dismissedAlerts.has('budget-daily-warn')) {
|
|
1697
|
+
alertState.budgetAlert = `Approaching daily budget: $${cost.toFixed(2)} / $${budget.daily}`;
|
|
1698
|
+
alertState.budgetCls = 'alert-warn';
|
|
1699
|
+
maybeNotify('monomind budget', `$${cost.toFixed(2)} of $${budget.daily} daily budget used`);
|
|
1700
|
+
} else {
|
|
1701
|
+
alertState.budgetAlert = null;
|
|
1702
|
+
}
|
|
1703
|
+
updateAlerts();
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function maybeNotify(title, body) {
|
|
1708
|
+
if (!('Notification' in window)) return;
|
|
1709
|
+
if (Notification.permission === 'granted') {
|
|
1710
|
+
new Notification(title, { body, icon: '' });
|
|
1711
|
+
} else if (Notification.permission !== 'denied') {
|
|
1712
|
+
Notification.requestPermission().then(p => { if (p === 'granted') new Notification(title, { body }); });
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// ── feature 5: session replay ──────────────────────────────
|
|
1717
|
+
let replayEvents = [];
|
|
1718
|
+
let replayIdx = 0;
|
|
1719
|
+
let replayActive = false;
|
|
1720
|
+
let replayTimer = null;
|
|
1721
|
+
|
|
1722
|
+
function startReplay() {
|
|
1723
|
+
// collect visible feed entries as ordered list
|
|
1724
|
+
const entries = [...document.querySelectorAll('#feed-content .feed-entry')];
|
|
1725
|
+
if (!entries.length) return;
|
|
1726
|
+
replayEvents = entries;
|
|
1727
|
+
replayIdx = 0;
|
|
1728
|
+
replayActive = false;
|
|
1729
|
+
document.getElementById('replay-bar').classList.add('show');
|
|
1730
|
+
// dim all entries
|
|
1731
|
+
entries.forEach(el => { el.style.opacity = '0.2'; el.style.transition = 'opacity 0.15s'; });
|
|
1732
|
+
highlightReplayEntry();
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function stopReplay() {
|
|
1736
|
+
clearInterval(replayTimer);
|
|
1737
|
+
replayActive = false;
|
|
1738
|
+
replayEvents.forEach(el => { el.style.opacity = ''; el.style.transition = ''; });
|
|
1739
|
+
replayEvents = [];
|
|
1740
|
+
document.getElementById('replay-bar').classList.remove('show');
|
|
1741
|
+
document.getElementById('rp-play').textContent = '▶';
|
|
1742
|
+
document.getElementById('rp-play').classList.remove('active');
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function highlightReplayEntry() {
|
|
1746
|
+
replayEvents.forEach((el, i) => {
|
|
1747
|
+
el.style.opacity = i === replayIdx ? '1' : (i < replayIdx ? '0.5' : '0.2');
|
|
1748
|
+
});
|
|
1749
|
+
const total = replayEvents.length;
|
|
1750
|
+
const pct = total > 1 ? Math.round(replayIdx / (total - 1) * 100) : 100;
|
|
1751
|
+
document.getElementById('rp-fill').style.width = pct + '%';
|
|
1752
|
+
document.getElementById('rp-counter').textContent = `${replayIdx + 1} / ${total}`;
|
|
1753
|
+
// scroll into view
|
|
1754
|
+
replayEvents[replayIdx]?.scrollIntoView({ block: 'nearest' });
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function replayStep(dir) {
|
|
1758
|
+
replayIdx = Math.max(0, Math.min(replayEvents.length - 1, replayIdx + dir));
|
|
1759
|
+
highlightReplayEntry();
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function replayToggle() {
|
|
1763
|
+
if (!replayEvents.length) { startReplay(); return; }
|
|
1764
|
+
replayActive = !replayActive;
|
|
1765
|
+
const btn = document.getElementById('rp-play');
|
|
1766
|
+
btn.textContent = replayActive ? '⏸' : '▶';
|
|
1767
|
+
btn.classList.toggle('active', replayActive);
|
|
1768
|
+
if (replayActive) {
|
|
1769
|
+
replayTimer = setInterval(() => {
|
|
1770
|
+
if (replayIdx >= replayEvents.length - 1) { replayToggle(); return; }
|
|
1771
|
+
replayStep(1);
|
|
1772
|
+
}, 600);
|
|
1773
|
+
} else {
|
|
1774
|
+
clearInterval(replayTimer);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// ── feature 6: project health score ───────────────────────
|
|
1779
|
+
function computeHealthScore(p) {
|
|
1780
|
+
let score = 50; // base
|
|
1781
|
+
const now = Date.now();
|
|
1782
|
+
const DAY = 86400000;
|
|
1783
|
+
// recency: up to +30 points for activity in last 7 days
|
|
1784
|
+
if (p.lastActivity) {
|
|
1785
|
+
const age = now - (typeof p.lastActivity === 'number' ? p.lastActivity : new Date(p.lastActivity).getTime());
|
|
1786
|
+
if (age < DAY) score += 30;
|
|
1787
|
+
else if (age < 3*DAY) score += 20;
|
|
1788
|
+
else if (age < 7*DAY) score += 10;
|
|
1789
|
+
else if (age > 30*DAY) score -= 15;
|
|
1790
|
+
}
|
|
1791
|
+
// session count: up to +15
|
|
1792
|
+
const sc = p.sessionCount || 0;
|
|
1793
|
+
score += Math.min(15, sc * 2);
|
|
1794
|
+
// memory: up to +5
|
|
1795
|
+
score += Math.min(5, (p.memoryCount || 0));
|
|
1796
|
+
return Math.max(0, Math.min(99, Math.round(score)));
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function healthClass(score) {
|
|
1800
|
+
if (score >= 70) return 'ph-hi';
|
|
1801
|
+
if (score >= 40) return 'ph-mid';
|
|
1802
|
+
return 'ph-lo';
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// ── feature 7: tool call frequency chart (by name) ─────────
|
|
1806
|
+
function buildBreakdownByName(events) {
|
|
1807
|
+
const counts = {};
|
|
1808
|
+
for (const ev of events) {
|
|
1809
|
+
if (ev.kind !== 'tool') continue;
|
|
1810
|
+
const name = (ev.name || ev.cat || 'other').replace(/^mcp__.*$/, 'MCP').replace(/^m__.*$/, 'MCP');
|
|
1811
|
+
counts[name] = (counts[name] || 0) + 1;
|
|
1812
|
+
}
|
|
1813
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
1814
|
+
if (!total) {
|
|
1815
|
+
document.getElementById('m-breakdown').innerHTML =
|
|
1816
|
+
'<div class="m-group-title">Tool Usage</div><div class="loading-txt" style="padding:6px 0">—</div>';
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
const CAT_COLOR = { Bash:'oklch(72% 0.18 75)', Read:'oklch(60% 0.12 220)', Edit:'oklch(65% 0.15 160)', Write:'oklch(65% 0.15 160)', Agent:'oklch(70% 0.15 300)', Task:'oklch(65% 0.15 280)', MCP:'oklch(65% 0.15 200)', WebFetch:'oklch(60% 0.12 195)', WebSearch:'oklch(60% 0.12 195)' };
|
|
1820
|
+
const getColor = n => CAT_COLOR[n] || 'var(--text-xs)';
|
|
1821
|
+
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 8);
|
|
1822
|
+
const rows = sorted.map(([name, cnt]) => {
|
|
1823
|
+
const pct = Math.round(cnt / total * 100);
|
|
1824
|
+
return `<div class="tb-row">
|
|
1825
|
+
<div class="tb-lbl" style="width:54px" title="${esc(name)}">${esc(name.length > 8 ? name.slice(0,7)+'…' : name)}</div>
|
|
1826
|
+
<div class="tb-bar-wrap"><div class="tb-bar" style="width:${pct}%;background:${getColor(name)}"></div></div>
|
|
1827
|
+
<div class="tb-count">${cnt}</div>
|
|
1828
|
+
</div>`;
|
|
1829
|
+
}).join('');
|
|
1830
|
+
document.getElementById('m-breakdown').innerHTML =
|
|
1831
|
+
`<div class="m-group-title">Tool Usage <span style="font-size:10px;color:var(--text-xs);font-weight:400">${total} calls</span></div><div class="m-breakdown">${rows}</div>`;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// ── feature 8: ambient mode ────────────────────────────────
|
|
1835
|
+
function toggleAmbient() {
|
|
1836
|
+
document.getElementById('app').classList.toggle('ambient');
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// ── feature 9: daily digest ────────────────────────────────
|
|
1840
|
+
const DIGEST_DISMISSED_KEY = 'mm-digest-dismissed';
|
|
1841
|
+
|
|
1842
|
+
function buildDigest() {
|
|
1843
|
+
const todayKey = new Date().toISOString().slice(0, 10);
|
|
1844
|
+
if (localStorage.getItem(DIGEST_DISMISSED_KEY) === todayKey) return;
|
|
1845
|
+
if (!allSessions.length) return;
|
|
1846
|
+
|
|
1847
|
+
const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0);
|
|
1848
|
+
const todaySessions = allSessions.filter(s => {
|
|
1849
|
+
const t = s.lastTs || s.mtime;
|
|
1850
|
+
return t && new Date(typeof t === 'number' ? t : t).getTime() >= todayStart.getTime();
|
|
1851
|
+
});
|
|
1852
|
+
if (!todaySessions.length) return;
|
|
1853
|
+
|
|
1854
|
+
const totalCost = todaySessions.reduce((a, s) => a + (s.totalCost || 0), 0);
|
|
1855
|
+
const totalTools = todaySessions.reduce((a, s) => a + (s.toolCalls || 0), 0);
|
|
1856
|
+
const totalMsgs = todaySessions.reduce((a, s) => a + (s.userMessages || 0), 0);
|
|
1857
|
+
const longestMs = Math.max(...todaySessions.map(s => s.totalDurationMs || 0));
|
|
1858
|
+
|
|
1859
|
+
// gather top tools from today's sessions' tag keywords as themes
|
|
1860
|
+
const themes = [...new Set(todaySessions.flatMap(s => s._tags || []))].slice(0, 3);
|
|
1861
|
+
|
|
1862
|
+
const stats = [
|
|
1863
|
+
`${todaySessions.length} session${todaySessions.length > 1 ? 's' : ''}`,
|
|
1864
|
+
totalCost > 0 ? `$${totalCost.toFixed(2)} spent` : null,
|
|
1865
|
+
totalTools > 0 ? `${totalTools} tool calls` : null,
|
|
1866
|
+
totalMsgs > 0 ? `${totalMsgs} messages` : null,
|
|
1867
|
+
longestMs > 0 ? `${fmtDur(longestMs)} longest` : null,
|
|
1868
|
+
...themes.map(t => `#${t}`),
|
|
1869
|
+
].filter(Boolean);
|
|
1870
|
+
|
|
1871
|
+
document.getElementById('digest-stats').innerHTML =
|
|
1872
|
+
stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('');
|
|
1873
|
+
document.getElementById('digest-card').classList.add('show');
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function dismissDigest() {
|
|
1877
|
+
const todayKey = new Date().toISOString().slice(0, 10);
|
|
1878
|
+
localStorage.setItem(DIGEST_DISMISSED_KEY, todayKey);
|
|
1879
|
+
document.getElementById('digest-card').classList.remove('show');
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// ── feature 10: minimap scrubber ──────────────────────────
|
|
1883
|
+
function buildMinimap(events) {
|
|
1884
|
+
const track = document.getElementById('feed-minimap-track');
|
|
1885
|
+
const thumb = document.getElementById('mm-thumb');
|
|
1886
|
+
const scroll = document.getElementById('feed-scroll');
|
|
1887
|
+
if (!track || !thumb || !scroll) return;
|
|
1888
|
+
|
|
1889
|
+
const tools = events.filter(ev => ev.kind === 'tool' || ev.kind === 'user');
|
|
1890
|
+
if (!tools.length) { track.innerHTML = ''; return; }
|
|
1891
|
+
|
|
1892
|
+
const CAT_CLS = { file:'mp-file', bash:'mp-bash', agent:'mp-agent', mcp:'mp-mcp', user:'mp-user' };
|
|
1893
|
+
const H = scroll.clientHeight || 400;
|
|
1894
|
+
const N = tools.length;
|
|
1895
|
+
track.innerHTML = tools.map((ev, i) => {
|
|
1896
|
+
const top = Math.round((i / N) * H);
|
|
1897
|
+
const h = Math.max(3, Math.round((1 / N) * H * 0.7));
|
|
1898
|
+
const cls = ev._errored ? 'mp-err' : (ev.kind === 'user' ? 'mp-user' : (CAT_CLS[ev.cat] || 'mp-other'));
|
|
1899
|
+
return `<div class="mm-pip ${cls}" style="top:${top}px;height:${h}px" onclick="minimapJump(${i},${N})"></div>`;
|
|
1900
|
+
}).join('');
|
|
1901
|
+
|
|
1902
|
+
// sync thumb
|
|
1903
|
+
const updateThumb = () => {
|
|
1904
|
+
const ratio = scroll.scrollHeight > H ? scroll.scrollTop / (scroll.scrollHeight - H) : 0;
|
|
1905
|
+
const thH = Math.max(20, Math.round(H * (H / Math.max(scroll.scrollHeight, H + 1))));
|
|
1906
|
+
thumb.style.height = thH + 'px';
|
|
1907
|
+
thumb.style.top = Math.round(ratio * (H - thH)) + 'px';
|
|
1908
|
+
};
|
|
1909
|
+
scroll.removeEventListener('scroll', scroll._mmListener || (() => {}));
|
|
1910
|
+
scroll._mmListener = updateThumb;
|
|
1911
|
+
scroll.addEventListener('scroll', scroll._mmListener);
|
|
1912
|
+
updateThumb();
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
function minimapJump(idx, total) {
|
|
1916
|
+
const scroll = document.getElementById('feed-scroll');
|
|
1917
|
+
if (!scroll) return;
|
|
1918
|
+
scroll.scrollTop = Math.round((idx / total) * scroll.scrollHeight);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// ── feature 11: cost leaderboard ──────────────────────────
|
|
1922
|
+
let leaderboardOpen = false;
|
|
1923
|
+
|
|
1924
|
+
function toggleLeaderboard() {
|
|
1925
|
+
leaderboardOpen = !leaderboardOpen;
|
|
1926
|
+
document.getElementById('btn-leaderboard').classList.toggle('on', leaderboardOpen);
|
|
1927
|
+
const panel = document.getElementById('lb-panel');
|
|
1928
|
+
panel.style.display = leaderboardOpen ? 'block' : 'none';
|
|
1929
|
+
if (leaderboardOpen) renderLeaderboard();
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function renderLeaderboard() {
|
|
1933
|
+
const sorted = [...allSessions]
|
|
1934
|
+
.filter(s => typeof s.totalCost === 'number' && s.totalCost > 0)
|
|
1935
|
+
.sort((a, b) => b.totalCost - a.totalCost)
|
|
1936
|
+
.slice(0, 15);
|
|
1937
|
+
const body = document.getElementById('lb-body');
|
|
1938
|
+
if (!sorted.length) { body.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--text-xs);padding:12px">No cost data yet</td></tr>'; return; }
|
|
1939
|
+
body.innerHTML = sorted.map((s, i) => {
|
|
1940
|
+
const cost = '$' + s.totalCost.toFixed(2);
|
|
1941
|
+
const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '—';
|
|
1942
|
+
const prompt = s.lastPrompt || s.id;
|
|
1943
|
+
return `<tr onclick="jumpToSession('${esc(s.id)}')" title="${esc(prompt)}">
|
|
1944
|
+
<td class="lb-rank">${i + 1}</td>
|
|
1945
|
+
<td class="lb-prompt">${esc(prompt.slice(0, 60))}</td>
|
|
1946
|
+
<td class="lb-cost">${cost}</td>
|
|
1947
|
+
<td class="lb-dur">${dur}</td>
|
|
1948
|
+
</tr>`;
|
|
1949
|
+
}).join('');
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// ── feature 12: session diff ──────────────────────────────
|
|
1953
|
+
let diffMode = false;
|
|
1954
|
+
let diffSelA = null; let diffSelB = null;
|
|
1955
|
+
|
|
1956
|
+
function toggleDiffMode() {
|
|
1957
|
+
diffMode = !diffMode;
|
|
1958
|
+
document.getElementById('btn-diff').classList.toggle('on', diffMode);
|
|
1959
|
+
const panel = document.getElementById('diff-panel');
|
|
1960
|
+
panel.classList.toggle('show', diffMode);
|
|
1961
|
+
if (!diffMode) clearDiff();
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function clearDiff() {
|
|
1965
|
+
diffSelA = null; diffSelB = null;
|
|
1966
|
+
document.getElementById('diff-hint').style.display = '';
|
|
1967
|
+
document.getElementById('diff-cols').style.display = 'none';
|
|
1968
|
+
document.getElementById('diff-cols').innerHTML = '';
|
|
1969
|
+
document.querySelectorAll('.sess-row.diff-sel-a, .sess-row.diff-sel-b').forEach(el => {
|
|
1970
|
+
el.classList.remove('diff-sel-a', 'diff-sel-b');
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
function diffSelectSession(sess, el) {
|
|
1975
|
+
if (!diffMode) return;
|
|
1976
|
+
if (diffSelA && diffSelA.id === sess.id) {
|
|
1977
|
+
diffSelA = null;
|
|
1978
|
+
el.classList.remove('diff-sel-a');
|
|
1979
|
+
document.getElementById('diff-hint').style.display = '';
|
|
1980
|
+
document.getElementById('diff-cols').style.display = 'none';
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
if (diffSelB && diffSelB.id === sess.id) {
|
|
1984
|
+
diffSelB = null;
|
|
1985
|
+
el.classList.remove('diff-sel-b');
|
|
1986
|
+
renderDiff();
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
if (!diffSelA) {
|
|
1990
|
+
diffSelA = sess;
|
|
1991
|
+
el.classList.add('diff-sel-a');
|
|
1992
|
+
} else if (!diffSelB) {
|
|
1993
|
+
diffSelB = sess;
|
|
1994
|
+
el.classList.add('diff-sel-b');
|
|
1995
|
+
} else {
|
|
1996
|
+
// replace B with new selection
|
|
1997
|
+
document.querySelectorAll('.sess-row.diff-sel-b').forEach(e => e.classList.remove('diff-sel-b'));
|
|
1998
|
+
diffSelB = sess;
|
|
1999
|
+
el.classList.add('diff-sel-b');
|
|
2000
|
+
}
|
|
2001
|
+
if (diffSelA && diffSelB) {
|
|
2002
|
+
renderDiff();
|
|
2003
|
+
} else {
|
|
2004
|
+
document.getElementById('diff-hint').textContent = diffSelA ? 'Now click a second session to compare' : 'Click two sessions below to compare them';
|
|
2005
|
+
document.getElementById('diff-hint').style.display = '';
|
|
2006
|
+
document.getElementById('diff-cols').style.display = 'none';
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function renderDiff() {
|
|
2011
|
+
if (!diffSelA || !diffSelB) return;
|
|
2012
|
+
document.getElementById('diff-hint').style.display = 'none';
|
|
2013
|
+
const cols = document.getElementById('diff-cols');
|
|
2014
|
+
cols.style.display = '';
|
|
2015
|
+
const fmt = (a, b, key, prefix = '') => {
|
|
2016
|
+
const va = a[key], vb = b[key];
|
|
2017
|
+
if (va == null && vb == null) return '';
|
|
2018
|
+
const fa = prefix + (va != null ? va : '—');
|
|
2019
|
+
const fb = prefix + (vb != null ? vb : '—');
|
|
2020
|
+
const hiA = va != null && vb != null && va > vb ? ' diff-hi' : (va != null && vb != null && va < vb ? ' diff-lo' : '');
|
|
2021
|
+
const hiB = va != null && vb != null && vb > va ? ' diff-hi' : (va != null && vb != null && vb < va ? ' diff-lo' : '');
|
|
2022
|
+
return [fa, hiA, fb, hiB];
|
|
2023
|
+
};
|
|
2024
|
+
|
|
2025
|
+
const diffCol = (s) => {
|
|
2026
|
+
const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2) : '—';
|
|
2027
|
+
const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '—';
|
|
2028
|
+
const tools = s.toolCalls != null ? s.toolCalls : '—';
|
|
2029
|
+
const msgs = s.userMessages != null ? s.userMessages : '—';
|
|
2030
|
+
const errs = s.errors != null ? s.errors : '—';
|
|
2031
|
+
const prompt = (s.lastPrompt || s.id || '').slice(0, 50);
|
|
2032
|
+
return { cost, dur, tools, msgs, errs, prompt, s };
|
|
2033
|
+
};
|
|
2034
|
+
|
|
2035
|
+
const a = diffCol(diffSelA), b = diffCol(diffSelB);
|
|
2036
|
+
const rows = [
|
|
2037
|
+
['Cost', a.cost, b.cost, typeof diffSelA.totalCost === 'number' && typeof diffSelB.totalCost === 'number' ? (diffSelA.totalCost > diffSelB.totalCost ? ['diff-hi','diff-lo'] : diffSelA.totalCost < diffSelB.totalCost ? ['diff-lo','diff-hi'] : ['','']) : ['','']],
|
|
2038
|
+
['Duration', a.dur, b.dur, ['','']],
|
|
2039
|
+
['Tool calls', a.tools, b.tools, typeof a.tools === 'number' && typeof b.tools === 'number' ? (a.tools > b.tools ? ['diff-hi','diff-lo'] : a.tools < b.tools ? ['diff-lo','diff-hi'] : ['','']) : ['','']],
|
|
2040
|
+
['Messages', a.msgs, b.msgs, ['','']],
|
|
2041
|
+
['Errors', a.errs, b.errs, ['','']],
|
|
2042
|
+
];
|
|
2043
|
+
|
|
2044
|
+
const colHtml = (idx) => `<div class="diff-col">
|
|
2045
|
+
<div class="diff-col-title">${esc(idx === 0 ? a.prompt : b.prompt)}</div>
|
|
2046
|
+
${rows.map(([k, va, vb, cls]) => `<div class="diff-row">
|
|
2047
|
+
<span class="diff-k">${k}</span>
|
|
2048
|
+
<span class="diff-v ${cls[idx]}">${esc(idx === 0 ? va : vb)}</span>
|
|
2049
|
+
</div>`).join('')}
|
|
2050
|
+
</div>`;
|
|
2051
|
+
|
|
2052
|
+
cols.innerHTML = colHtml(0) + colHtml(1);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1301
2055
|
// ── loops ──────────────────────────────────────────────────
|
|
1302
2056
|
async function renderLoops() {
|
|
1303
2057
|
const el = document.getElementById('loops-content');
|
|
@@ -1796,7 +2550,8 @@ document.addEventListener('keydown', e => {
|
|
|
1796
2550
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
1797
2551
|
if (document.getElementById('cmd-palette').classList.contains('open')) return;
|
|
1798
2552
|
|
|
1799
|
-
if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); }
|
|
2553
|
+
if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); }
|
|
2554
|
+
if (e.key === 'a' || e.key === 'A') { if (currentView === 'now') { e.preventDefault(); toggleAmbient(); } }
|
|
1800
2555
|
|
|
1801
2556
|
if (currentView === 'now') {
|
|
1802
2557
|
if (e.key === '/') { e.preventDefault(); toggleFeedSearch(); }
|