@krtclcdy/tersio 2.10.0 → 2.12.0

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/dashboard/app.js CHANGED
@@ -44,14 +44,22 @@
44
44
  if (c) c.textContent = new Date().toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
45
45
  }
46
46
  tick(); setInterval(tick, 1000);
47
- function syncThemeIcon() {
48
- var dark = document.documentElement.getAttribute('data-theme') === 'dark' ||
49
- (!document.documentElement.getAttribute('data-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
50
- var moon = document.getElementById('iconMoon'), sun = document.getElementById('iconSun');
51
- if (moon) moon.classList.toggle('hidden', dark);
52
- if (sun) sun.classList.toggle('hidden', !dark);
53
- }
54
- syncThemeIcon();
47
+ function currentTheme() {
48
+ try { return localStorage.getItem('tersio-theme') || 'system'; } catch (e) { return 'system'; }
49
+ }
50
+ function paintThemeTabs() {
51
+ var v = currentTheme();
52
+ Array.prototype.forEach.call(document.querySelectorAll('[data-theme-val]'), function(b) {
53
+ b.classList.toggle('on', b.getAttribute('data-theme-val') === v);
54
+ });
55
+ }
56
+ function applyTheme(v) {
57
+ if (v === 'system') document.documentElement.removeAttribute('data-theme');
58
+ else document.documentElement.setAttribute('data-theme', v);
59
+ try { localStorage.setItem('tersio-theme', v); } catch (e) { }
60
+ paintThemeTabs();
61
+ }
62
+ applyTheme(currentTheme());
55
63
  (function initRail() {
56
64
  var rail = document.getElementById('rail');
57
65
  if (!rail || initRail.done) return;
@@ -146,14 +154,15 @@
146
154
  if (DATA) render(DATA);
147
155
  }).catch(function() { clearTimeout(to); });
148
156
  })();
149
- document.getElementById('theme').addEventListener('click', function() {
150
- var cur = document.documentElement.getAttribute('data-theme');
151
- var sysDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
152
- var next = (cur || (sysDark ? 'dark' : 'light')) === 'dark' ? 'light' : 'dark';
153
- document.documentElement.setAttribute('data-theme', next);
154
- try { localStorage.setItem('tersio-theme', next); } catch (e) { }
155
- syncThemeIcon();
157
+ Array.prototype.forEach.call(document.querySelectorAll('[data-theme-val]'), function(b) {
158
+ b.addEventListener('click', function() { applyTheme(b.getAttribute('data-theme-val')); });
156
159
  });
160
+ (function() {
161
+ var sysMedia = window.matchMedia('(prefers-color-scheme: dark)');
162
+ if (sysMedia && typeof sysMedia.addEventListener === 'function') sysMedia.addEventListener('change', function() {
163
+ if (currentTheme() === 'system') applyTheme('system');
164
+ });
165
+ })();
157
166
 
158
167
  var PROVIDERS = [
159
168
  [/muse/i, 'Meta', 'meta', '#0082fb'],
@@ -188,6 +197,13 @@
188
197
  function dayKey(d) {
189
198
  return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
190
199
  }
200
+ function relTime(ts) {
201
+ var s = Math.max(0, Math.floor((Date.now() - ts) / 1000));
202
+ if (s < 60) return 'just now';
203
+ if (s < 3600) return Math.floor(s / 60) + 'm ago';
204
+ if (s < 86400) return Math.floor(s / 3600) + 'h ago';
205
+ return Math.floor(s / 86400) + 'd ago';
206
+ }
191
207
  function modelTotal(byModel, m) {
192
208
  var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
193
209
  return b.input + b.output + b.cacheRead + b.cacheWrite;
@@ -302,6 +318,13 @@
302
318
  if (other) rows.push(['Other', other, 'var(--dim)']);
303
319
  return tipDayHTML(head, weekTotal, rows.length ? rows : [['no activity', 0, 'var(--dim)']]);
304
320
  }
321
+ // Shared empty state (shadcn pattern): icon chip + title + hint. Callers
322
+ // inject the HTML; render()'s trailing lucide.createIcons() paints icons.
323
+ function emptyState(icon, title, desc) {
324
+ return '<span class="empty-icon"><i data-lucide="' + icon + '" class="size-5"></i></span>' +
325
+ '<p class="empty-title">' + title + '</p>' +
326
+ '<p class="empty-desc">' + desc + '</p>';
327
+ }
305
328
  function renderGraph(byDay) {
306
329
  var g = document.getElementById('graph');
307
330
  g.innerHTML = '';
@@ -312,6 +335,15 @@
312
335
  var b = byDay[day];
313
336
  sums[day] = b.input + b.output + b.cacheRead + b.cacheWrite;
314
337
  });
338
+ var hasData = Object.keys(sums).some(function(k) { return sums[k] > 0; });
339
+ document.getElementById('emptyGraph').classList.toggle('hidden', hasData);
340
+ document.getElementById('graph').style.display = hasData ? '' : 'none';
341
+ document.getElementById('graphMonths').style.display = hasData ? '' : 'none';
342
+ if (!hasData) {
343
+ document.getElementById('graphRange').textContent = '';
344
+ document.getElementById('graphCap').textContent = 'no data in trailing 12 months';
345
+ return;
346
+ }
315
347
  // per-day values under the active mode
316
348
  var dayKeys = [], wk;
317
349
  for (wk = 0; wk < 53; wk++) for (var di = 0; di < 7; di++) {
@@ -428,6 +460,7 @@
428
460
  var tops = topModels(byModel, 8).filter(function(m) { return modelTotal(byModel, m) > 0; });
429
461
  var cards = document.getElementById('modelCards');
430
462
  cards.innerHTML = '';
463
+ if (!tops.length) cards.innerHTML = '<div class="empty md:col-span-3">' + emptyState('boxes', 'No models yet', 'Model token totals will appear here once sessions report tokens.') + '</div>';
431
464
  tops.slice(0, 3).forEach(function(m, i) {
432
465
  var v = vendorOf(m), d = wow(m);
433
466
  var card = document.createElement('div');
@@ -493,8 +526,10 @@
493
526
  hoverModel(li, m);
494
527
  ol.appendChild(li);
495
528
  });
496
- if (!tops.length) ol.innerHTML = '<li class="mono text-sm px-4 py-6" style="color: var(--dim)">no session tokens yet</li>';
497
- else if ('IntersectionObserver' in window && !reduce) {
529
+ document.getElementById('modelsCount').textContent = tops.length ? tops.length + ' models' : '';
530
+ ol.style.display = tops.length ? '' : 'none';
531
+ document.getElementById('emptyModels').classList.toggle('hidden', tops.length > 0);
532
+ if (tops.length && 'IntersectionObserver' in window && !reduce) {
498
533
  var fio = new IntersectionObserver(function(entries) {
499
534
  entries.forEach(function(e) {
500
535
  if (e.isIntersecting) {
@@ -504,7 +539,7 @@
504
539
  });
505
540
  }, { threshold: 0.2 });
506
541
  fio.observe(ol);
507
- } else {
542
+ } else if (tops.length) {
508
543
  ol.querySelectorAll('.bar-fill').forEach(function(f) { f.style.width = f.dataset.w || '0'; });
509
544
  }
510
545
  if (hasGsap && hasST && !cardsShown && tops.length) {
@@ -669,6 +704,29 @@
669
704
  var el = document.getElementById('usdSpark');
670
705
  el.innerHTML = '';
671
706
  var keys = Object.keys(byDay).sort().slice(-30);
707
+ // Zero state: the spark strip is a fixed h-16, so a placeholder inside it
708
+ // overflows onto the big number. Hide the number, strip, and stats, and
709
+ // mount one placeholder in the card body instead. Restored below on data.
710
+ var mid = el.parentElement || null, card = mid && mid.parentElement ? mid.parentElement : null;
711
+ var usdEl = document.getElementById('usd'), noteEl = document.getElementById('usdNote');
712
+ var ex = document.getElementById('emptyUsd');
713
+ if (!keys.length && card && mid) {
714
+ if (!ex) {
715
+ ex = document.createElement('div');
716
+ ex.id = 'emptyUsd'; ex.className = 'empty'; ex.style.marginTop = '16px';
717
+ ex.innerHTML = emptyState('trending-up', 'No cost data yet', 'Daily spend will spark here once sessions report tokens.');
718
+ card.insertBefore(ex, mid);
719
+ }
720
+ el.style.display = 'none'; mid.style.display = 'none';
721
+ if (usdEl) usdEl.style.display = 'none';
722
+ if (noteEl) noteEl.style.display = 'none';
723
+ } else {
724
+ if (ex && ex.parentElement) ex.parentElement.removeChild(ex);
725
+ el.style.display = '';
726
+ if (mid) mid.style.display = '';
727
+ if (usdEl) usdEl.style.display = '';
728
+ if (noteEl) noteEl.style.display = '';
729
+ }
672
730
  var rate = total ? usd / total : 0;
673
731
  var max = 1, vols = [];
674
732
  keys.forEach(function(k) {
@@ -719,6 +777,11 @@
719
777
  var byDay = d.byDay || {};
720
778
  renderSpark(byDay, d.usd || 0, d.savedUsd || 0, total);
721
779
  document.getElementById('verChip').textContent = 'tersio v' + (d.version || '?');
780
+ var dp = d.paths || {};
781
+ [['pathLedger', dp.ledger], ['pathSessions', dp.sessions], ['pathRtk', dp.rtk]].forEach(function(pair) {
782
+ var pel = document.getElementById(pair[0]);
783
+ if (pel && pair[1]) { pel.textContent = pair[1]; pel.title = pair[1]; }
784
+ });
722
785
  countUp(document.getElementById('saved'), d.savedUsd || 0, true);
723
786
  document.getElementById('co2').textContent = '~' + (d.co2g || 0).toFixed(1) + 'g';
724
787
  (function() {
@@ -760,6 +823,7 @@
760
823
  bindCmdTable();
761
824
  renderGraph(d.byDay || {});
762
825
  renderModels();
826
+ renderRecent();
763
827
  renderCmd();
764
828
 
765
829
  if (window.lucide) lucide.createIcons();
@@ -770,6 +834,34 @@
770
834
  observe();
771
835
  }
772
836
 
837
+ function renderRecent() {
838
+ var body = document.getElementById('recent');
839
+ body.innerHTML = '';
840
+ var rows = (DATA.recent || []).slice(0, 13);
841
+ rows.forEach(function(r, i) {
842
+ var v = vendorOf(r.m);
843
+ var tr = document.createElement('tr');
844
+ tr.className = 'rrow' + (i < rows.length - 1 ? ' rowline' : '');
845
+ var dot = '<span style="display:inline-block;width:8px;height:8px;border-radius:99px;background:' + v.color + ';margin-right:8px"></span>';
846
+ tr.innerHTML = '<td class="py-2.5 pr-3 truncate" style="max-width: 180px"></td>' +
847
+ '<td class="text-right py-2.5 pr-3 whitespace-nowrap"></td>' +
848
+ '<td class="text-right py-2.5 whitespace-nowrap" style="color: var(--dim)"></td>';
849
+ var tds = tr.children;
850
+ tds[0].innerHTML = dot + '<span></span>';
851
+ tds[0].querySelector('span:last-child').textContent = r.m;
852
+ tds[0].title = r.m;
853
+ tds[1].innerHTML = '<span style="color:#fb923c"></span> <span style="color:var(--accent)"></span>';
854
+ tds[1].children[0].textContent = fmt(r.i) + '\u2191';
855
+ tds[1].children[1].textContent = fmt(r.o) + '\u2193';
856
+ tds[1].title = fmt(r.i) + ' in / ' + fmt(r.o) + ' out';
857
+ tds[2].textContent = relTime(r.t);
858
+ body.appendChild(tr);
859
+ });
860
+ document.getElementById('recentCount').textContent = rows.length ? rows.length + ' requests' : '';
861
+ document.getElementById('recentTable').style.display = rows.length ? '' : 'none';
862
+ document.getElementById('emptyRecent').classList.toggle('hidden', rows.length > 0);
863
+ }
864
+
773
865
  var seen = new WeakSet();
774
866
  function observe() {
775
867
  var els = document.querySelectorAll('.rise');
@@ -789,13 +881,42 @@
789
881
 
790
882
  function load() {
791
883
  fetch('data.json').then(function(r) { return r.json(); }).then(render).catch(function() {
792
- document.getElementById('topTable').style.display = 'none';
793
- document.getElementById('emptyTop').classList.remove('hidden');
884
+ var cards = document.getElementById('modelCards');
885
+ if (cards && !cards.children.length) cards.innerHTML = '<div class="empty md:col-span-3">' + emptyState('cloud-off', 'Could not load data', 'Serve with tersio gain instead of opening this file directly.') + '</div>';
794
886
  if (window.lucide) lucide.createIcons();
795
887
  observe();
796
888
  });
797
889
  }
798
890
  document.getElementById('reload').addEventListener('click', load);
891
+ (function() {
892
+ var dlg = document.getElementById('settings');
893
+ if (!dlg || typeof dlg.showModal !== 'function') return;
894
+ document.getElementById('settingsBtn').addEventListener('click', function() {
895
+ if (typeof dlg.showModal === 'function') dlg.showModal();
896
+ });
897
+ document.getElementById('settingsClose').addEventListener('click', function() { dlg.close(); });
898
+ dlg.addEventListener('click', function(ev) { if (ev.target === dlg) dlg.close(); });
899
+ })();
900
+ (function() {
901
+ var resetBtn = document.getElementById('reset');
902
+ if (!resetBtn) return;
903
+ if (window.location.protocol === 'file:') { resetBtn.style.display = 'none'; return; }
904
+ var label = document.getElementById('resetLabel');
905
+ resetBtn.addEventListener('click', function() {
906
+ if (resetBtn.dataset.armed) {
907
+ delete resetBtn.dataset.armed;
908
+ label.textContent = 'Clearing…';
909
+ fetch('reset', { method: 'POST' }).then(function(r) { return r.json(); }).then(function() {
910
+ label.textContent = 'Reset';
911
+ load();
912
+ }).catch(function() { label.textContent = 'Reset'; });
913
+ } else {
914
+ resetBtn.dataset.armed = '1';
915
+ label.textContent = 'Sure?';
916
+ setTimeout(function() { delete resetBtn.dataset.armed; if (label.textContent === 'Sure?') label.textContent = 'Reset'; }, 3000);
917
+ }
918
+ });
919
+ })();
799
920
  load();
800
921
  observe();
801
922
  })();
@@ -5,6 +5,7 @@
5
5
  --cell-0: rgba(24, 24, 27, .07);
6
6
  --cell-1: rgba(4, 120, 87, .3); --cell-2: rgba(4, 120, 87, .55);
7
7
  --cell-3: rgba(4, 120, 87, .8); --cell-4: #047857;
8
+ --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.07);
8
9
  }
9
10
  html[data-theme="light"] { color-scheme: light; }
10
11
  html[data-theme="dark"] { color-scheme: dark; }
@@ -16,6 +17,7 @@
16
17
  --cell-0: rgba(255, 255, 255, .07);
17
18
  --cell-1: rgba(52, 211, 153, .3); --cell-2: rgba(52, 211, 153, .55);
18
19
  --cell-3: rgba(52, 211, 153, .8); --cell-4: #34d399;
20
+ --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.55);
19
21
  }
20
22
  }
21
23
  body { background: var(--bg); color: var(--ink); }
@@ -34,6 +36,7 @@
34
36
  --cell-0: rgba(255, 255, 255, .07);
35
37
  --cell-1: rgba(52, 211, 153, .3); --cell-2: rgba(52, 211, 153, .55);
36
38
  --cell-3: rgba(52, 211, 153, .8); --cell-4: #34d399;
39
+ --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.55);
37
40
  }
38
41
  .ticker { animation: tick 36s linear infinite; will-change: transform; }
39
42
  .ticker:hover { animation-play-state: paused; }
@@ -47,6 +50,8 @@
47
50
  }
48
51
  .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; }
49
52
  .rowline { border-bottom: 1px solid var(--line); }
53
+ #recentTable, #cmdTable { table-layout: fixed; }
54
+ #recentTable td:first-child, #cmdTable td:nth-child(2) { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
50
55
  .pill { display: inline-flex; align-items: center; gap: 4px; border-radius: 999px; padding: 3px 10px; font-size: 11px; white-space: nowrap; }
51
56
  .pill.good { background: var(--accent-soft); color: var(--accent); }
52
57
  .pill.bad { background: rgba(248, 113, 113, .12); color: #f87171; }
@@ -84,21 +89,25 @@
84
89
  .rise { opacity: 0; transform: translateY(26px); }
85
90
  .rise.in { opacity: 1; transform: none; transition: opacity .7s cubic-bezier(.16, 1, .3, 1), transform .7s cubic-bezier(.16, 1, .3, 1); }
86
91
  .tilt { transition: transform .25s cubic-bezier(.16, 1, .3, 1), background .25s; }
87
- .tilt:hover { transform: translateX(6px); background: var(--accent-soft); }
92
+ .tilt:hover { transform: translateX(6px); box-shadow: var(--shadow-xs); }
88
93
  .seg button { color: var(--dim); border-radius: 8px; transition: background .2s, color .2s; cursor: pointer; }
89
94
  .seg button.on { background: var(--accent-soft); color: var(--ink); }
90
95
  .seg button:not(.on):hover { color: var(--ink); }
91
96
  .mrow { transition: background .25s; }
92
- .mrow:hover { background: var(--accent-soft); }
97
+ .mrow:hover { box-shadow: var(--shadow-xs); }
93
98
  .stripcell { transition: background .25s; }
94
- .stripcell:hover { background: var(--accent-soft); }
99
+ .stripcell:hover { box-shadow: var(--shadow-xs); }
95
100
  button, .tilt, .cell, .stripcell { cursor: pointer; }
96
101
  .btn-push { transition: transform .12s, background .2s; }
97
102
  .btn-push:hover { background: var(--accent-soft); }
98
103
  .btn-push:active { transform: scale(.96); }
99
104
  a.social { color: var(--ink); }
100
105
  a.social:hover { color: var(--accent); }
101
- a.social svg { width: 16px; height: 16px; display: block; }
106
+ a.social img, a.social svg { width: 16px; height: 16px; display: block; }
107
+ html[data-theme="dark"] a.social img { filter: invert(1); }
108
+ @media (prefers-color-scheme: dark) {
109
+ :root:not([data-theme="light"]) a.social img { filter: invert(1); }
110
+ }
102
111
  a.social:hover { outline: 1px solid var(--accent); outline-offset: 2px; }
103
112
  .cell { width: 100%; aspect-ratio: 1 / 1; border-radius: 3px; background: var(--cell-0); min-width: 0; }
104
113
  .cell.key { width: 11px; height: 11px; aspect-ratio: auto; flex-shrink: 0; }
@@ -117,19 +126,25 @@
117
126
  }
118
127
  html[data-theme="light"] .ghostimg { opacity: .06; filter: brightness(0); }
119
128
  #tip { position: fixed; z-index: 100; pointer-events: none; min-width: 230px; max-width: 300px;
120
- background: color-mix(in srgb, var(--panel) 94%, transparent);
121
- -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
129
+ background: var(--panel);
122
130
  border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px;
123
- box-shadow: 0 16px 48px rgba(0, 0, 0, .35); opacity: 0; transition: opacity .15s; }
131
+ box-shadow: var(--shadow-xs); opacity: 0; transition: opacity .15s; }
124
132
  #tip.show { opacity: 1; }
125
133
  #tip .tt { font-size: 11px; text-transform: uppercase; letter-spacing: .14em; font-weight: 700; }
126
134
  #tip .tv { font-size: 13px; margin: 2px 0 8px; }
127
135
  #tip .tr { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 2.5px 0; }
128
136
  #tip .sw { width: 9px; height: 9px; border-radius: 2.5px; flex-shrink: 0; }
129
137
  #tip .tn { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
138
+ .scrollarea { overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
139
+ .scrollarea::-webkit-scrollbar { width: 8px; }
140
+ .scrollarea::-webkit-scrollbar-thumb { background: var(--line); border-radius: 99px; }
141
+ .scrollarea::-webkit-scrollbar-track { background: transparent; }
142
+ .rrow { transition: background .2s; cursor: pointer; }
143
+ .rrow:hover { box-shadow: var(--shadow-xs); }
144
+ #recentTable thead th { position: sticky; top: 0; background: var(--panel); z-index: 1; }
130
145
  #tip .tvr { flex-shrink: 0; }
131
146
  .modelcard { transition: background .25s; }
132
- .modelcard:hover { background: var(--accent-soft); }
147
+ .modelcard:hover { box-shadow: var(--shadow-xs); }
133
148
 
134
149
  .heatmonth { font-size: 10px; }
135
150
  #graphMonths span { overflow: visible; white-space: nowrap; }
@@ -155,3 +170,26 @@
155
170
  .tilt, .btn-push, .mrow, .bar-fill { transition: none; }
156
171
  .hero-glow, .ticker { animation: none; }
157
172
  }
173
+ /* Empty states (shadcn pattern): dashed well, icon chip, title + hint. */
174
+ .empty { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2px; border: 1px dashed var(--line); border-radius: 12px; padding: 32px 16px; }
175
+ .empty.hidden { display: none; } /* beats .empty flex: same-file order, higher specificity than tailwind .hidden */
176
+ .empty-icon { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 999px; background: var(--track); color: var(--dim); margin-bottom: 10px; }
177
+ .empty-title { font-weight: 600; font-size: 14px; color: var(--ink); margin: 0; }
178
+ .empty-desc { font-size: 12px; color: var(--dim); margin: 0; max-width: 340px; }
179
+ #reset:hover { box-shadow: 0 0 14px var(--accent-soft); border-color: var(--accent); }
180
+ /* Settings dialog (shadcn pattern over the native modal). */
181
+ .dlg { margin: auto; border: 1px solid var(--line); background: var(--panel); color: var(--ink); border-radius: 16px; padding: 20px; width: min(460px, calc(100vw - 32px)); box-shadow: 0 16px 48px rgba(0, 0, 0, .35); }
182
+ .dlg::backdrop { background: rgba(0, 0, 0, .8); }
183
+ .dlg-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
184
+ .dlg-title { font-weight: 700; font-size: 16px; letter-spacing: -0.01em; margin: 0; }
185
+ .dlg-desc { font-size: 12px; color: var(--dim); margin: 2px 0 0; }
186
+ .dlg-label { font-size: 11px; text-transform: uppercase; letter-spacing: .14em; color: var(--dim); margin: 18px 0 8px; }
187
+ .dlg-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
188
+ .dlg-name { font-size: 13px; font-weight: 600; margin: 0; }
189
+ .dlg-hint { font-size: 12px; color: var(--dim); margin: 2px 0 0; }
190
+ .dlg-own { font-weight: 400; font-size: 11px; color: var(--dim); }
191
+ .dlg-paths { margin-top: 12px; display: grid; gap: 8px; }
192
+ .dlg-path { border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; min-width: 0; overflow: hidden; }
193
+ .dlg-file { font-size: 11px; color: var(--dim); margin: 4px 0 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
194
+ #settingsBtn:hover { box-shadow: 0 0 14px var(--accent-soft); border-color: var(--accent); }
195
+ .dlg .seg button { display: grid; place-items: center; }
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <meta name="color-scheme" content="dark light">
7
- <script>(function(){try{var t=localStorage.getItem('tersio-theme');if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
7
+ <script>(function(){try{var t=localStorage.getItem('tersio-theme');if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t);else document.documentElement.removeAttribute('data-theme');}catch(e){}})();</script>
8
8
  <title>Tersio Dashboard</title>
9
9
  <link rel="icon" type="image/png" href="brand.webp">
10
10
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
@@ -36,15 +36,63 @@
36
36
  <span id="clock">--:--</span>
37
37
  </span>
38
38
  <span class="hidden sm:block w-px h-5 shrink-0" style="background: var(--line)" aria-hidden="true"></span>
39
- <button id="theme" class="btn-push flex shrink-0 items-center p-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Toggle dark mode">
40
- <span id="iconMoon"><i data-lucide="moon" class="size-4"></i></span>
41
- <span id="iconSun" class="hidden"><i data-lucide="sun" class="size-4"></i></span>
39
+ <button id="reset" class="btn-push mono flex shrink-0 items-center gap-2 text-xs pl-2.5 pr-3 py-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Reset tersio statistics">
40
+ <i data-lucide="rotate-ccw" class="size-3.5"></i><span class="hidden md:inline" id="resetLabel">Reset</span>
42
41
  </button>
43
- <button id="reload" class="btn-push mono flex shrink-0 items-center gap-2 text-xs pl-2.5 pr-3 py-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Reload data">
44
- <i data-lucide="refresh-cw" class="size-3.5"></i><span class="hidden md:inline">Reload</span>
42
+ <button id="settingsBtn" class="btn-push flex shrink-0 items-center p-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Open settings">
43
+ <i data-lucide="settings" class="size-4"></i>
45
44
  </button>
46
45
  </header>
47
46
 
47
+ <!-- Settings dialog (shadcn pattern, native modal) -->
48
+ <dialog id="settings" class="dlg" aria-labelledby="settingsTitle">
49
+ <div class="dlg-head">
50
+ <div class="min-w-0">
51
+ <p class="dlg-title" id="settingsTitle">Settings</p>
52
+ <p class="dlg-desc">Appearance, data refresh, and where statistics live.</p>
53
+ </div>
54
+ <button id="settingsClose" class="btn-push flex shrink-0 items-center p-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Close settings">
55
+ <i data-lucide="x" class="size-4"></i>
56
+ </button>
57
+ </div>
58
+ <p class="dlg-label">Appearance</p>
59
+ <div class="dlg-row">
60
+ <div class="min-w-0">
61
+ <p class="dlg-name">Theme</p>
62
+ <p class="dlg-hint">Light, dark, or follow the system.</p>
63
+ </div>
64
+ <div class="seg mono text-xs flex items-center gap-1 p-1 rounded-[10px]" style="border: 1px solid var(--line); background: var(--panel)" role="tablist" aria-label="Theme">
65
+ <button data-theme-val="light" class="px-2.5 py-1.5" role="tab" aria-label="Light" title="Light"><i data-lucide="sun" class="size-4"></i></button>
66
+ <button data-theme-val="dark" class="px-2.5 py-1.5" role="tab" aria-label="Dark" title="Dark"><i data-lucide="moon" class="size-4"></i></button>
67
+ <button data-theme-val="system" class="px-2.5 py-1.5" role="tab" aria-label="System" title="System"><i data-lucide="monitor" class="size-4"></i></button>
68
+ </div>
69
+ </div>
70
+ <p class="dlg-label">Data</p>
71
+ <div class="dlg-row">
72
+ <div class="min-w-0">
73
+ <p class="dlg-name">Reload</p>
74
+ <p class="dlg-hint">Re-read statistics from disk.</p>
75
+ </div>
76
+ <button id="reload" class="btn-push mono flex shrink-0 items-center gap-2 text-xs pl-2.5 pr-3 py-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Reload data">
77
+ <i data-lucide="refresh-cw" class="size-3.5"></i><span>Reload</span>
78
+ </button>
79
+ </div>
80
+ <div class="dlg-paths">
81
+ <div class="dlg-path">
82
+ <p class="dlg-name">Usage ledger <span class="dlg-own">tersio-owned · reset clears</span></p>
83
+ <p class="mono dlg-file" id="pathLedger" title="">–</p>
84
+ </div>
85
+ <div class="dlg-path">
86
+ <p class="dlg-name">Session transcripts <span class="dlg-own">host-owned · never touched</span></p>
87
+ <p class="mono dlg-file" id="pathSessions" title="">–</p>
88
+ </div>
89
+ <div class="dlg-path">
90
+ <p class="dlg-name">RTK history <span class="dlg-own">rtk-owned · never touched</span></p>
91
+ <p class="mono dlg-file" id="pathRtk" title="">–</p>
92
+ </div>
93
+ </div>
94
+ </dialog>
95
+
48
96
  <!-- Hero: centered total -->
49
97
  <section class="mx-auto max-w-6xl text-center pt-14 pb-10 relative">
50
98
  <div class="hero-glow" aria-hidden="true"></div>
@@ -110,17 +158,18 @@
110
158
  <section class="rise mt-3 flex rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)" id="tokens" aria-label="Tokens by bucket"></section>
111
159
 
112
160
  <!-- Activity: full-width year heatmap -->
113
- <section id="activity" class="rise mt-8 rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Activity graph">
161
+ <section id="activity" class="rise mt-8" style="scroll-margin-top: 90px" aria-label="Activity graph">
114
162
  <div class="flex items-center gap-2 mb-1">
115
163
  <i data-lucide="calendar-days" class="size-4" style="color: var(--accent)"></i>
116
- <h2 class="display font-bold tracking-tight text-xl">Activity</h2>
117
- <div class="seg mono text-xs flex items-center gap-1 ml-auto p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="tablist" aria-label="Graph range">
164
+ <h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Activity</h2>
165
+ <div class="seg mono text-xs flex items-center gap-1 ml-auto p-1 rounded-[10px]" style="border: 1px solid var(--line); background: var(--panel)" role="tablist" aria-label="Graph range">
118
166
  <button data-gmode="daily" class="on px-3 py-1.5" role="tab">Daily</button>
119
167
  <button data-gmode="weekly" class="px-3 py-1.5" role="tab">Weekly</button>
120
168
  <button data-gmode="cumulative" class="px-3 py-1.5" role="tab">Cumulative</button>
121
169
  </div>
122
170
  </div>
123
- <p class="mono text-xs mb-4" style="color: var(--dim)"><span id="graphRange"></span> · <span id="graphCap">daily values / trailing 12 months</span></p>
171
+ <p class="mono text-xs mb-4 truncate" style="color: var(--dim)"><span id="graphRange"></span> · <span id="graphCap">daily values / trailing 12 months</span></p>
172
+ <div class="rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)">
124
173
  <div class="pb-1">
125
174
  <div class="grid w-full" style="grid-template-columns: repeat(53, minmax(0, 1fr)); gap: 3px;" id="graph"></div>
126
175
  </div>
@@ -130,13 +179,19 @@
130
179
  <span>more</span>
131
180
  </div>
132
181
  <div class="heatmonth mono mt-2 grid w-full" style="color: var(--dim); grid-template-columns: repeat(53, minmax(0, 1fr)); gap: 3px;" id="graphMonths"></div>
182
+ <div id="emptyGraph" class="empty hidden">
183
+ <span class="empty-icon"><i data-lucide="calendar-x" class="size-5"></i></span>
184
+ <p class="empty-title">No activity yet</p>
185
+ <p class="empty-desc">Daily token activity will chart here once sessions report tokens.</p>
186
+ </div>
187
+ </div>
133
188
  </section>
134
189
 
135
190
  <!-- Top models: 3 columns -->
136
191
  <section class="mt-8" aria-label="Top models">
137
192
  <div class="flex items-center gap-2 mb-1">
138
193
  <i data-lucide="cpu" class="size-4" style="color: var(--accent)"></i>
139
- <h2 class="display font-bold tracking-tight text-xl">Top models</h2>
194
+ <h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Top models</h2>
140
195
  <span class="mono text-xs ml-auto" style="color: var(--dim)">usage across sessions</span>
141
196
  </div>
142
197
  <p class="mono text-xs mb-4" style="color: var(--dim)">week-over-week vs prior 7 days</p>
@@ -144,26 +199,61 @@
144
199
  </section>
145
200
 
146
201
 
147
- <!-- Models list: full width -->
148
- <section class="rise mt-8" aria-label="All models">
202
+ <!-- Models + recent: twin cards -->
203
+ <div class="rise grid grid-cols-12 grid-flow-dense gap-3 mt-8">
204
+ <section class="col-span-12 lg:col-span-7 rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)" aria-label="All models">
149
205
  <div class="flex items-center gap-2 mb-1">
150
206
  <i data-lucide="layers" class="size-4" style="color: var(--accent)"></i>
151
- <h2 class="display font-bold tracking-tight text-xl">Models</h2>
207
+ <h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Models</h2>
208
+ <span class="mono text-xs ml-auto truncate shrink-0" style="color: var(--dim)" id="modelsCount"></span>
152
209
  </div>
153
210
  <p class="mono text-xs mb-4" style="color: var(--dim)">ranked by tokens / top 8</p>
154
- <ol id="models" class="rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)"></ol>
211
+ <div class="scrollarea" style="max-height:410px"><ol id="models" class="overflow-hidden"></ol></div>
212
+ <div id="emptyModels" class="empty hidden">
213
+ <span class="empty-icon"><i data-lucide="boxes" class="size-5"></i></span>
214
+ <p class="empty-title">No models yet</p>
215
+ <p class="empty-desc">Model token totals will appear here once sessions report tokens.</p>
216
+ </div>
217
+ </section>
218
+ <section class="col-span-12 lg:col-span-5 rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)" aria-label="Recent requests">
219
+ <div class="flex items-center gap-2 mb-1">
220
+ <i data-lucide="history" class="size-4" style="color: var(--accent)"></i>
221
+ <h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Recent requests</h2>
222
+ <span class="mono text-xs ml-auto truncate shrink-0" style="color: var(--dim)" id="recentCount"></span>
223
+ </div>
224
+ <p class="mono text-xs mb-4 truncate" style="color: var(--dim)">latest assistant messages</p>
225
+ <div class="scrollarea overflow-x-auto" style="max-height:410px">
226
+ <table class="w-full mono text-[13px]" id="recentTable">
227
+ <colgroup><col><col style="width:150px"><col style="width:90px"></colgroup>
228
+ <thead>
229
+ <tr class="text-left text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">
230
+ <th class="font-normal py-2 pr-3">Model</th>
231
+ <th class="font-normal text-right py-2 pr-3">In / Out</th>
232
+ <th class="font-normal text-right py-2">When</th>
233
+ </tr>
234
+ </thead>
235
+ <tbody id="recent"></tbody>
236
+ </table>
237
+ </div>
238
+ <div id="emptyRecent" class="empty hidden">
239
+ <span class="empty-icon"><i data-lucide="inbox" class="size-5"></i></span>
240
+ <p class="empty-title">No requests yet</p>
241
+ <p class="empty-desc">Recent assistant messages will show here once sessions report tokens.</p>
242
+ </div>
155
243
  </section>
244
+ </div>
156
245
 
157
- <!-- Command tools: session tool calls + measured RTK savings, one table -->
246
+ <!-- Command tools: own card, header outside like Models -->
158
247
  <section id="tools" class="rise mt-8 rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Command tools">
159
248
  <div class="flex items-center gap-2 mb-1">
160
249
  <i data-lucide="terminal" class="size-4" style="color: var(--accent)"></i>
161
- <h2 class="display font-bold tracking-tight text-xl">Command tools</h2>
162
- <span class="mono text-xs ml-auto" style="color: var(--dim)" id="toolsScope">tool calls in sessions</span>
250
+ <h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Command tools</h2>
251
+ <span class="mono text-xs ml-auto truncate min-w-0" style="color: var(--dim)" id="toolsScope">tool calls in sessions</span>
163
252
  </div>
164
- <p class="mono text-xs mb-4" style="color: var(--dim)">count · tokens saved · avg rate · avg time · share of executions</p>
253
+ <p class="mono text-xs mb-4 truncate" style="color: var(--dim)">count · tokens saved · avg rate · avg time · share of executions</p>
165
254
  <div class="overflow-x-auto">
166
255
  <table class="w-full mono text-[13px]" id="cmdTable">
256
+ <colgroup><col style="width:40px"><col><col style="width:90px"><col style="width:90px"><col style="width:90px"><col style="width:90px"><col style="width:140px"></colgroup>
167
257
  <thead>
168
258
  <tr class="text-left text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">
169
259
  <th class="font-normal text-right pr-3 py-2 w-10">#</th>
@@ -178,7 +268,11 @@
178
268
  <tbody id="cmdBody"></tbody>
179
269
  </table>
180
270
  </div>
181
- <p id="emptyCmd" class="mono text-sm py-6 text-center hidden" style="color: var(--dim)">No tool calls or RTK measurements yet.</p>
271
+ <div id="emptyCmd" class="empty hidden">
272
+ <span class="empty-icon"><i data-lucide="wrench" class="size-5"></i></span>
273
+ <p class="empty-title">No tool data yet</p>
274
+ <p class="empty-desc">Session tool calls and RTK measurements will appear here once recorded.</p>
275
+ </div>
182
276
  <div id="cmdPager" class="mono text-xs mt-4 flex flex-wrap items-center gap-x-4 gap-y-3" style="color: var(--dim)">
183
277
  <span id="cmdRange">Showing 0 of 0</span>
184
278
  <span class="seg flex items-center gap-1 p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="group" aria-label="Rows per page">
@@ -192,16 +286,14 @@
192
286
  <p class="mono text-[11px] mt-3" style="color: var(--dim)">saved / avg / time are RTK-metered per command — session tools without metering show –. caveman + ponytail gains are instruction-following, bench-measured in BENCHMARK.md.</p>
193
287
  </section>
194
288
 
195
- <footer class="mono text-xs mt-12 pt-4" style="color: var(--dim); border-top: 1px solid var(--line)">
196
-
197
289
  <footer class="mono text-xs mt-12 pt-4" style="color: var(--dim); border-top: 1px solid var(--line)">
198
290
  <div class="flex flex-wrap items-center gap-x-4 gap-y-3">
199
291
  <span>© 2026 Tersio. KurutoDenzeru. All rights reserved.</span>
200
292
  <span class="pill flat" id="verChip">tersio</span>
201
293
  <span class="ml-auto flex items-center gap-1" style="color: var(--ink)">
202
- <a href="https://github.com/KurutoDenzeru/tersio" target="_blank" rel="noopener" aria-label="GitHub" class="btn-push social grid size-8 place-items-center rounded-lg"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></a>
294
+ <a href="https://github.com/KurutoDenzeru/tersio" target="_blank" rel="noopener" aria-label="GitHub" class="btn-push social grid size-8 place-items-center rounded-lg"><img height="16" width="16" src="https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/github.svg" alt="GitHub"></a>
203
295
  <a href="https://linkedin.com/in/kurtcalacday/" target="_blank" rel="noopener" aria-label="LinkedIn" class="btn-push social grid size-8 place-items-center rounded-lg"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg></a>
204
- <a href="https://instagram.com/krtclcdy/" target="_blank" rel="noopener" aria-label="Instagram" class="btn-push social grid size-8 place-items-center rounded-lg"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z"/></svg></a>
296
+ <a href="https://instagram.com/krtclcdy/" target="_blank" rel="noopener" aria-label="Instagram" class="btn-push social grid size-8 place-items-center rounded-lg"><img height="16" width="16" src="https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/instagram.svg" alt="Instagram"></a>
205
297
  </span>
206
298
  </div>
207
299
  </footer>
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
+ import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.js';
3
4
  import { dirname, join } from 'node:path';
4
- import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
5
5
  import { readCavemanDefault } from '../shared/plugin-settings.js';
6
6
  const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
7
7
  const RULE_PATH = join(CAVERN_DIR, 'rule.md');
@@ -74,6 +74,17 @@ export default function cavemanSessionExtension(pi) {
74
74
  return true;
75
75
  }
76
76
  pi.setLabel?.('Caveman session toggle');
77
+ // Live mirror: a /tersio or /combo switch publishes shared state — adopt
78
+ // it at once so the next turn injects the new mode with no session reload.
79
+ // Stable identity, so the bridge set dedupes across re-inits.
80
+ function syncFromShared(state) {
81
+ const mode = normalizeMode('caveman', state.caveman);
82
+ if (mode) {
83
+ currentMode = mode;
84
+ syncStatus();
85
+ }
86
+ }
87
+ setSharedComboListener(syncFromShared);
77
88
  pi.registerCommand?.('caveman', {
78
89
  description: 'Toggle terse caveman replies for this session',
79
90
  handler: async (args, ctx) => {