@inkobytes/nexus 1.0.0 → 1.0.2

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.
@@ -133,7 +133,9 @@
133
133
  </section>
134
134
  <section class="wide report-section" id="nexus-report">
135
135
  <h2><span data-icon="file-text"></span>Nexus Report</h2>
136
- <pre id="report"></pre>
136
+ <div id="report-intro" class="report-intro"></div>
137
+ <div id="report-tabs" class="report-tabs"></div>
138
+ <div id="report-panel" class="report-panel"></div>
137
139
  </section>
138
140
  </div>
139
141
  </main>
@@ -141,9 +143,11 @@
141
143
  <script>
142
144
  let currentSnapshot = null;
143
145
  let queueView = { mode: 'queue', agent: null };
146
+ let reportView = '';
144
147
  let repoRoot = '';
145
148
  let lastQueueSig = '';
146
149
  let lastLocksSig = '';
150
+ let sectionObserver = null;
147
151
 
148
152
  function queueViewFromHash(hash) {
149
153
  const value = String(hash || '').replace(/^#/, '').toLowerCase();
@@ -199,6 +203,8 @@
199
203
  const subagentsTag = group.subagents > 0 ? '<span class="lock-group-subagents">+subagents (' + group.subagents + ')</span>' : '';
200
204
  const modelText = Array.from(group.modelLabels).join(', ');
201
205
  const modelTag = modelText ? '<span class="lock-group-model">' + escapeHtml(modelText) + '</span>' : '';
206
+ const agentName = escapeHtml(formatAgentName(agent));
207
+ const agentBadge = '<span class="lock-group-agent"><span class="lock-group-agent-icon"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg></span><span>' + agentName + '</span></span>';
202
208
  const files = group.locks.map(lock => {
203
209
  const href = 'vscode://file' + escapeHtml(repoRoot + '/' + lock.target);
204
210
  const intent = lock.intent ? '<div class="lock-meta"><span class="lock-intent">' + escapeHtml(lock.intent) + '</span></div>' : '';
@@ -215,7 +221,7 @@
215
221
  return '<details class="lock-group" open>'
216
222
  + '<summary class="lock-group-header">'
217
223
  + trustDot
218
- + '<span class="lock-group-agent">' + escapeHtml(agent) + '</span>'
224
+ + agentBadge
219
225
  + modelTag
220
226
  + subagentsTag
221
227
  + '<span class="lock-group-count">' + group.locks.length + ' file' + (group.locks.length !== 1 ? 's' : '') + '</span>'
@@ -239,9 +245,7 @@
239
245
  renderAgentBars(chartTasks);
240
246
  fillFeed('standup', data.standup);
241
247
  fillFeed('releases', data.releases);
242
- document.getElementById('report').textContent = data.report && data.report.trim()
243
- ? data.report
244
- : 'No Nexus report entries yet.';
248
+ renderReportSection(data);
245
249
  document.getElementById('git').innerHTML = data.dirtyFiles.length
246
250
  ? '<ul>' + data.dirtyFiles.map(line => {
247
251
  const xy = line.slice(0, 2);
@@ -254,6 +258,7 @@
254
258
  : label) + '</li>';
255
259
  }).join('') + '</ul>'
256
260
  : '<p class="muted">Working tree clean.</p>';
261
+ syncSidebarActive();
257
262
  }
258
263
 
259
264
  document.querySelectorAll('[data-queue-mode], [data-next-agent]').forEach((button) => {
@@ -268,6 +273,8 @@
268
273
  });
269
274
 
270
275
  window.addEventListener('hashchange', syncQueueViewFromHash);
276
+ window.addEventListener('hashchange', syncSidebarActive);
277
+ document.addEventListener('scroll', syncSidebarActive, { passive: true });
271
278
 
272
279
  function renderQueuePanel(data) {
273
280
  if (!data) return;
@@ -419,6 +426,8 @@
419
426
  const dateText = completedAt && !Number.isNaN(completedAt.getTime())
420
427
  ? completedAt.toLocaleString()
421
428
  : 'unknown time';
429
+ const agentName = formatAgentName(entry.agent || 'unknown');
430
+ const agentBadge = '<span class="ledger-agent"><span class="ledger-agent-icon"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg></span><span>' + escapeHtml(agentName) + '</span></span>';
422
431
  const files = Array.isArray(entry.files) ? entry.files : [];
423
432
  const fileText = files.length ? files.join(', ') : 'no files';
424
433
  const sha = entry.sha && entry.sha !== 'unknown' ? entry.sha.slice(0, 7) : 'unknown';
@@ -434,7 +443,7 @@
434
443
  + '</div>'
435
444
  + '<div class="ledger-meta">'
436
445
  + '<span>' + escapeHtml(dateText) + '</span>'
437
- + '<span>' + escapeHtml(entry.agent || 'unknown') + '</span>'
446
+ + agentBadge
438
447
  + '<span>' + escapeHtml(entry.epic || 'unknown epic') + '</span>'
439
448
  + '<span>' + escapeHtml(entry.cost || 'unknown cost') + '</span>'
440
449
  + '<span>' + escapeHtml(sha) + '</span>'
@@ -445,6 +454,128 @@
445
454
  + '</article>';
446
455
  }
447
456
 
457
+ function renderReportSection(data) {
458
+ const intro = formatReportIntro(data?.reportIntro);
459
+ const blocks = Array.isArray(data?.reportBlocks) ? data.reportBlocks : [];
460
+ const introEl = document.getElementById('report-intro');
461
+ const tabsEl = document.getElementById('report-tabs');
462
+ const panelEl = document.getElementById('report-panel');
463
+
464
+ introEl.textContent = intro;
465
+ introEl.hidden = !intro;
466
+
467
+ if (!blocks.length) {
468
+ tabsEl.innerHTML = '';
469
+ panelEl.innerHTML = '<p class="muted">No Nexus report entries yet.</p>';
470
+ return;
471
+ }
472
+
473
+ const monthGroups = getRecentReportMonthGroups(blocks, data?.generatedAt);
474
+ if (!monthGroups.length) {
475
+ tabsEl.innerHTML = '';
476
+ panelEl.innerHTML = '<p class="muted">No dated Nexus report entries yet.</p>';
477
+ return;
478
+ }
479
+
480
+ if (!monthGroups.some((group) => group.key === reportView)) {
481
+ reportView = monthGroups[0].key;
482
+ }
483
+
484
+ tabsEl.innerHTML = monthGroups.map((group) => renderReportTab(group)).join('');
485
+ tabsEl.querySelectorAll('[data-report-view]').forEach((button) => {
486
+ button.addEventListener('click', () => {
487
+ reportView = button.dataset.reportView || '';
488
+ renderReportSection(currentSnapshot);
489
+ });
490
+ });
491
+
492
+ const activeGroup = monthGroups.find((group) => group.key === reportView) || monthGroups[0];
493
+ panelEl.innerHTML = activeGroup ? renderReportMonthGroup(activeGroup) : '<p class="muted">No Nexus report entries in this month.</p>';
494
+ }
495
+
496
+ function formatReportIntro(value) {
497
+ return String(value || '')
498
+ .replace(/^#\s+[^\n]+\n*/m, '')
499
+ .trim();
500
+ }
501
+
502
+ function getRecentReportMonthGroups(blocks, generatedAt) {
503
+ const reference = new Date(generatedAt || Date.now());
504
+ const refMonthIndex = reference.getFullYear() * 12 + reference.getMonth();
505
+ const filtered = blocks.filter((block) => {
506
+ if (!block.monthKey) return false;
507
+ const parts = String(block.monthKey).split('-');
508
+ if (parts.length !== 2) return false;
509
+ const year = Number.parseInt(parts[0], 10);
510
+ const month = Number.parseInt(parts[1], 10) - 1;
511
+ if (!Number.isInteger(year) || !Number.isInteger(month)) return false;
512
+ const diff = refMonthIndex - (year * 12 + month);
513
+ return diff >= 0 && diff < 12;
514
+ });
515
+ const groups = [];
516
+ const seen = new Map();
517
+
518
+ for (const block of filtered) {
519
+ const key = block.monthKey || 'undated';
520
+ if (!seen.has(key)) {
521
+ const group = {
522
+ key,
523
+ label: block.monthLabel || 'Undated',
524
+ tabLabel: formatReportTabLabel(block.monthKey, generatedAt),
525
+ items: [],
526
+ };
527
+ seen.set(key, group);
528
+ groups.push(group);
529
+ }
530
+ seen.get(key).items.push(block);
531
+ }
532
+
533
+ return groups;
534
+ }
535
+
536
+ function formatReportTabLabel(monthKey, generatedAt) {
537
+ const parts = String(monthKey || '').split('-');
538
+ if (parts.length !== 2) return 'Undated';
539
+ const year = Number.parseInt(parts[0], 10);
540
+ const month = Number.parseInt(parts[1], 10) - 1;
541
+ if (!Number.isInteger(year) || !Number.isInteger(month)) return 'Undated';
542
+
543
+ const date = new Date(year, month, 1);
544
+ const reference = new Date(generatedAt || Date.now());
545
+ const sameYear = date.getFullYear() === reference.getFullYear();
546
+ return date.toLocaleString('en-US', sameYear
547
+ ? { month: 'short' }
548
+ : { month: 'short', year: 'numeric' });
549
+ }
550
+
551
+ function renderReportTab(group) {
552
+ const active = group.key === reportView ? ' active' : '';
553
+ return '<button type="button" class="tab-button' + active + '" data-report-view="' + escapeHtml(group.key) + '">'
554
+ + escapeHtml(group.tabLabel || group.label)
555
+ + '</button>';
556
+ }
557
+
558
+ function renderReportMonthGroup(group) {
559
+ return '<section class="report-month-group">'
560
+ + '<div class="report-month-heading">' + escapeHtml(group.label) + '</div>'
561
+ + '<div class="report-month-list">' + group.items.map(renderReportBlock).join('') + '</div>'
562
+ + '</section>';
563
+ }
564
+
565
+ function renderReportBlock(block) {
566
+ const details = block.details
567
+ ? '<pre class="report-entry-body">' + escapeHtml(block.details) + '</pre>'
568
+ : '';
569
+
570
+ return '<article class="report-entry">'
571
+ + '<div class="report-entry-head">'
572
+ + '<div class="report-entry-target">' + escapeHtml(block.target || 'Untitled report entry') + '</div>'
573
+ + '<div class="report-entry-time">' + escapeHtml(block.timestamp || 'Undated') + '</div>'
574
+ + '</div>'
575
+ + details
576
+ + '</article>';
577
+ }
578
+
448
579
  function getChartTasks(data) {
449
580
  const seen = new Set();
450
581
  const tasks = [];
@@ -475,6 +606,51 @@
475
606
  return String(agent || '').replace(/^@/, '').toLowerCase();
476
607
  }
477
608
 
609
+ function syncSidebarActive(preferredId) {
610
+ const links = Array.from(document.querySelectorAll('.sidebar .nav-link[href^="#"]'));
611
+ if (!links.length) return;
612
+
613
+ let activeId = preferredId || '';
614
+ if (!activeId) {
615
+ const hash = String(window.location.hash || '').replace(/^#/, '');
616
+ if (hash) activeId = hash;
617
+ }
618
+
619
+ if (!activeId) {
620
+ const sections = links
621
+ .map((link) => document.querySelector(link.getAttribute('href')))
622
+ .filter(Boolean);
623
+ const threshold = 160;
624
+ const current = sections.find((section) => {
625
+ const rect = section.getBoundingClientRect();
626
+ return rect.top <= threshold && rect.bottom > threshold;
627
+ });
628
+ activeId = current?.id || sections[0]?.id || '';
629
+ }
630
+
631
+ links.forEach((link) => {
632
+ const href = String(link.getAttribute('href') || '');
633
+ const id = href.replace(/^#/, '');
634
+ link.classList.toggle('active', id === activeId);
635
+ });
636
+ }
637
+
638
+ function initSectionObserver() {
639
+ const sections = Array.from(document.querySelectorAll('main section[id]'));
640
+ if (!sections.length || typeof IntersectionObserver === 'undefined') {
641
+ syncSidebarActive();
642
+ return;
643
+ }
644
+ if (sectionObserver) sectionObserver.disconnect();
645
+ sectionObserver = new IntersectionObserver((entries) => {
646
+ const visible = entries
647
+ .filter((entry) => entry.isIntersecting)
648
+ .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
649
+ if (visible?.target?.id) syncSidebarActive(visible.target.id);
650
+ }, { rootMargin: '-18% 0px -58% 0px', threshold: [0.2, 0.35, 0.55] });
651
+ sections.forEach((section) => sectionObserver.observe(section));
652
+ }
653
+
478
654
  function updateAgentStatus(data) {
479
655
  const now = Math.floor(Date.now() / 1000);
480
656
  const activeLockAgents = new Set((data.locks || [])
@@ -537,12 +713,14 @@
537
713
  epics[epic].total++;
538
714
  if (task.checked) epics[epic].done++;
539
715
  }
540
- el.innerHTML = '<div class="chart-list">' + Object.entries(epics).map(([epic, d]) => {
716
+ el.innerHTML = '<div class="chart-list">' + Object.entries(epics).map(([epic, d], index) => {
541
717
  const pct = d.total > 0 ? (d.done / d.total) * 100 : 0;
718
+ const share = queue.length > 0 ? (d.total / queue.length) * 100 : 0;
542
719
  return '<div class="chart-row">'
543
- + '<div class="chart-row-header"><span class="chart-row-label">' + escapeHtml(epic) + '</span>'
720
+ + '<div class="chart-row-header"><span class="chart-row-label"><span class="chart-row-rank">0' + (index + 1) + '</span>' + escapeHtml(epic) + '</span>'
544
721
  + '<span class="chart-row-val">' + d.done + '/' + d.total + '</span></div>'
545
722
  + '<div class="chart-bar-track"><div class="chart-bar-fill clr-done" style="width:' + pct.toFixed(1) + '%"></div></div>'
723
+ + '<div class="chart-row-note">' + share.toFixed(0) + '% of tracked work</div>'
546
724
  + '</div>';
547
725
  }).join('') + '</div>';
548
726
  }
@@ -577,41 +755,52 @@
577
755
  }
578
756
  const total = Object.values(agents).reduce((a, b) => a + b, 0) || 1;
579
757
  const entries = Object.entries(agents).sort((a, b) => b[1] - a[1]);
758
+ const [leadAgent, leadCount] = entries[0];
759
+ const leadShare = ((leadCount / total) * 100).toFixed(0);
580
760
 
581
761
  // SVG pie chart
582
- const cx = 50, cy = 50, r = 40;
762
+ const cx = 50, cy = 50, outerR = 46, innerR = 22;
583
763
  let angle = -Math.PI / 2;
584
764
  const slices = entries.length === 1
585
765
  ? (() => {
586
766
  const [a, n] = entries[0];
587
767
  const color = AGENT_COLORS[a] || '#6b7f74';
588
- return '<circle cx="' + cx + '" cy="' + cy + '" r="' + r + '" fill="' + color + '"><title>@' + escapeHtml(a) + ': ' + n + ' tasks</title></circle>';
768
+ return '<circle cx="' + cx + '" cy="' + cy + '" r="' + outerR + '" fill="none" stroke="' + color + '" stroke-width="' + (outerR - innerR) + '" stroke-linecap="round"><title>@' + escapeHtml(a) + ': ' + n + ' tasks</title></circle>';
589
769
  })()
590
770
  : entries.map(([a, n]) => {
591
771
  const sweep = (n / total) * 2 * Math.PI;
592
- const x1 = cx + r * Math.cos(angle);
593
- const y1 = cy + r * Math.sin(angle);
772
+ const x1 = cx + outerR * Math.cos(angle);
773
+ const y1 = cy + outerR * Math.sin(angle);
594
774
  angle += sweep;
595
- const x2 = cx + r * Math.cos(angle);
596
- const y2 = cy + r * Math.sin(angle);
775
+ const x2 = cx + outerR * Math.cos(angle);
776
+ const y2 = cy + outerR * Math.sin(angle);
597
777
  const large = sweep > Math.PI ? 1 : 0;
598
778
  const color = AGENT_COLORS[a] || '#6b7f74';
599
779
  return '<path d="M' + cx + ',' + cy + ' L' + x1.toFixed(2) + ',' + y1.toFixed(2)
600
- + ' A' + r + ',' + r + ' 0 ' + large + ',1 ' + x2.toFixed(2) + ',' + y2.toFixed(2)
601
- + ' Z" fill="' + color + '" stroke="var(--panel)" stroke-width="1.5"><title>@' + escapeHtml(a) + ': ' + n + ' tasks</title></path>';
780
+ + ' A' + outerR + ',' + outerR + ' 0 ' + large + ',1 ' + x2.toFixed(2) + ',' + y2.toFixed(2)
781
+ + ' Z" fill="' + color + '" opacity="0.96"><title>@' + escapeHtml(a) + ': ' + n + ' tasks</title></path>';
602
782
  }).join('');
603
783
 
604
784
  const legend = entries.map(([a, n]) => {
605
785
  const color = AGENT_COLORS[a] || '#6b7f74';
606
- return '<div class="agent-legend-item"><div class="agent-legend-dot" style="background:' + color + '"></div>@' + escapeHtml(a) + '<span class="chart-row-val">' + n + '</span></div>';
786
+ const pct = ((n / total) * 100).toFixed(0);
787
+ return '<div class="agent-legend-item"><div class="agent-legend-dot" style="background:' + color + '"></div><span class="agent-legend-name">@' + escapeHtml(a) + '</span><span class="agent-legend-share">' + pct + '%</span><span class="chart-row-val">' + n + '</span></div>';
607
788
  }).join('');
608
789
 
609
790
  const agentCount = entries.length;
610
791
  el.innerHTML = '<div class="pie-wrap">'
611
- + '<svg class="pie-svg" viewBox="0 0 100 100"><g>' + slices + '</g></svg>'
792
+ + '<div class="pie-shell"><svg class="pie-svg" viewBox="0 0 100 100">'
793
+ + '<defs><filter id="pieGlow" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="1.5" result="blur"></feGaussianBlur><feMerge><feMergeNode in="blur"></feMergeNode><feMergeNode in="SourceGraphic"></feMergeNode></feMerge></filter></defs>'
794
+ + '<circle cx="' + cx + '" cy="' + cy + '" r="' + outerR + '" class="pie-bg"></circle>'
795
+ + '<g filter="url(#pieGlow)">' + slices + '</g>'
796
+ + '<circle cx="' + cx + '" cy="' + cy + '" r="' + innerR + '" class="pie-core"></circle>'
797
+ + '<text class="pie-total" x="' + cx + '" y="47">' + total + '</text>'
798
+ + '<text class="pie-sub" x="' + cx + '" y="60">tasks</text>'
799
+ + '</svg></div>'
612
800
  + '<div class="agent-legend">' + legend + '</div>'
613
801
  + '</div>'
614
- + '<div class="pie-caption">' + total + ' task' + (total !== 1 ? 's' : '') + ' across ' + agentCount + ' agent' + (agentCount !== 1 ? 's' : '') + '</div>';
802
+ + '<div class="pie-caption"><strong>@' + escapeHtml(leadAgent) + '</strong> is carrying ' + leadShare + '% of tracked work. '
803
+ + total + ' task' + (total !== 1 ? 's' : '') + ' across ' + agentCount + ' agent' + (agentCount !== 1 ? 's' : '') + '.</div>';
615
804
  }
616
805
 
617
806
  function renderIcons() {
@@ -669,7 +858,9 @@
669
858
  return String(value).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
670
859
  }
671
860
  renderIcons();
861
+ initSectionObserver();
672
862
  syncQueueViewFromHash();
863
+ syncSidebarActive();
673
864
  load();
674
865
  new EventSource('/events').addEventListener('update', load);
675
866
  </script>