@krtclcdy/tersio 2.17.0 → 2.19.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/README.md +20 -4
- package/cli/common.js +8 -2
- package/cli/currency.js +102 -0
- package/cli/dashboard.js +54 -2
- package/cli/doctor.js +2 -0
- package/cli/install.js +7 -73
- package/cli/profile.js +92 -0
- package/cli/reset.js +4 -2
- package/cli/settings.js +122 -0
- package/cli/usage.js +42 -16
- package/dashboard/app.js +435 -67
- package/dashboard/styles.css +83 -3
- package/dashboard/template.html +97 -17
- package/extensions/caveman-session/index.js +3 -4
- package/extensions/combo-toggle/index.js +3 -4
- package/extensions/rtk-session/index.js +3 -4
- package/extensions/shared/rtk-gain.js +13 -1
- package/extensions/shared/session-state.js +15 -0
- package/extensions/shared/usage-ledger.js +155 -83
- package/extensions/shared/usage-store.js +294 -0
- package/package.json +18 -1
- package/tersio.js +8 -1
package/dashboard/styles.css
CHANGED
|
@@ -41,6 +41,18 @@
|
|
|
41
41
|
--danger: #f87171; --danger-border: rgba(248, 113, 113, .3); --danger-soft: rgba(248, 113, 113, .07);
|
|
42
42
|
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.55);
|
|
43
43
|
}
|
|
44
|
+
html[data-theme="dark"] .modelsSection {
|
|
45
|
+
--panel: #ffffff;
|
|
46
|
+
--ink: #18181b;
|
|
47
|
+
--dim: #52525b;
|
|
48
|
+
--line: rgba(24, 24, 27, .12);
|
|
49
|
+
--track: rgba(24, 24, 27, .08);
|
|
50
|
+
--cell-0: rgba(24, 24, 27, .07);
|
|
51
|
+
--accent-soft: rgba(4, 120, 87, .12);
|
|
52
|
+
--accent: #047857;
|
|
53
|
+
color-scheme: light;
|
|
54
|
+
}
|
|
55
|
+
html[data-theme="dark"] .modelsSection .brandmark.placeholder { background: #fff !important; color: #000 !important; }
|
|
44
56
|
.ticker { animation: tick 36s linear infinite; will-change: transform; }
|
|
45
57
|
.ticker:hover { animation-play-state: paused; }
|
|
46
58
|
@keyframes tick { from { transform: translateX(0); } to { transform: translateX(-50%); } }
|
|
@@ -58,6 +70,16 @@
|
|
|
58
70
|
.pill { display: inline-flex; align-items: center; gap: 4px; border-radius: 999px; padding: 3px 10px; font-size: 11px; white-space: nowrap; }
|
|
59
71
|
.pill.good { background: var(--accent-soft); color: var(--accent); }
|
|
60
72
|
.pill.bad { background: rgba(248, 113, 113, .12); color: #f87171; }
|
|
73
|
+
/* .pill.flat was referenced by the est. / served chips but never defined, so
|
|
74
|
+
they rendered as bare text. Muted fill matches the badge family. */
|
|
75
|
+
.pill.flat { background: var(--track); color: var(--dim); }
|
|
76
|
+
/* shadcn/ui badge variants, used for per-request run status. Neutral by
|
|
77
|
+
default so the destructive (error) badge is the only thing that shouts. */
|
|
78
|
+
.badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 2px 9px; font-size: 11px; line-height: 1.55; white-space: nowrap; border: 1px solid transparent; }
|
|
79
|
+
.badge.secondary { background: var(--track); color: var(--dim); }
|
|
80
|
+
.badge.outline { border-color: var(--line); color: var(--ink); background: transparent; }
|
|
81
|
+
.badge.destructive { background: var(--danger-soft); color: var(--danger); border-color: var(--danger-border); }
|
|
82
|
+
#tip .tnote { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); font-size: 11px; line-height: 1.5; color: var(--dim); white-space: normal; word-break: break-word; }
|
|
61
83
|
.zface { display: inline-block; vertical-align: -2px; width: 14px; height: 14px; }
|
|
62
84
|
.zface.good { color: var(--accent); }
|
|
63
85
|
.zface.warn { color: #fbbf24; }
|
|
@@ -101,10 +123,10 @@
|
|
|
101
123
|
.seg button.on { background: var(--accent-soft); color: var(--ink); }
|
|
102
124
|
.seg button:not(.on):hover { color: var(--ink); }
|
|
103
125
|
.mrow { transition: background .25s; }
|
|
104
|
-
.mrow:hover { box-shadow: var(--shadow-xs); }
|
|
126
|
+
.mrow:hover { background: var(--track); box-shadow: var(--shadow-xs); }
|
|
105
127
|
.stripcell { transition: background .25s; }
|
|
106
128
|
.stripcell:hover { box-shadow: var(--shadow-xs); }
|
|
107
|
-
button, .tilt, .cell, .stripcell { cursor: pointer; }
|
|
129
|
+
button, .tilt, .cell, .stripcell, .modelcard, .mrow { cursor: pointer; }
|
|
108
130
|
.btn-push { transition: transform .12s, background .2s; }
|
|
109
131
|
.btn-push:hover { background: var(--accent-soft); }
|
|
110
132
|
.btn-push:active { transform: scale(.96); }
|
|
@@ -122,6 +144,7 @@
|
|
|
122
144
|
.cell.l3 { background: var(--cell-3); } .cell.l4 { background: var(--cell-4); }
|
|
123
145
|
.brandmark { width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; flex-shrink: 0; color: #fff; }
|
|
124
146
|
.brandmark.placeholder { background: transparent !important; border: 1px solid var(--line); color: var(--dim); }
|
|
147
|
+
html[data-theme="dark"] .brandmark.placeholder { background: #fff !important; color: #000 !important; }
|
|
125
148
|
.brandmark img { width: 18px; height: 18px; }
|
|
126
149
|
.brandmark svg { width: 18px; height: 18px; display: block; }
|
|
127
150
|
.brandmark.sm { width: 28px; height: 28px; border-radius: 8px; }
|
|
@@ -154,7 +177,7 @@
|
|
|
154
177
|
.scrollarea::-webkit-scrollbar-thumb { background: var(--line); border-radius: 99px; }
|
|
155
178
|
.scrollarea::-webkit-scrollbar-track { background: transparent; }
|
|
156
179
|
.rrow { transition: background .2s; cursor: pointer; }
|
|
157
|
-
.rrow:hover { box-shadow: var(--shadow-xs); }
|
|
180
|
+
.rrow:hover { background: var(--track); box-shadow: var(--shadow-xs); }
|
|
158
181
|
#recentTable thead th { position: sticky; top: 0; background: var(--panel); z-index: 1; }
|
|
159
182
|
#tip .tvr { flex-shrink: 0; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
160
183
|
.modelcard { transition: background .25s; }
|
|
@@ -222,3 +245,60 @@
|
|
|
222
245
|
.toast-desc { font-size: 12px; color: var(--dim); margin: 1px 0 0; }
|
|
223
246
|
@keyframes toast-in { from { opacity: 0; transform: translateY(8px) scale(.98); } }
|
|
224
247
|
@keyframes toast-out { to { opacity: 0; transform: translateY(6px) scale(.98); } }
|
|
248
|
+
/* Model detail dialog (shadcn chart language, native modal). KPI strip,
|
|
249
|
+
gradient area chart with range tabs, rounded bars, token-mix donut. */
|
|
250
|
+
.dlg.mdlg { width: min(880px, calc(100vw - 32px)); max-height: min(88vh, 960px); padding: 0; overflow: hidden; display: none; flex-direction: column; }
|
|
251
|
+
.dlg.mdlg[open] { display: flex; }
|
|
252
|
+
.dlg.mdlg .dlg-head { padding: 20px 20px 0; }
|
|
253
|
+
.md-body { overflow-y: auto; padding: 0 20px 20px; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--line) transparent; display: grid; gap: 12px; margin-top: 16px; }
|
|
254
|
+
.md-rankpill { border: 1px solid var(--line); background: var(--accent-soft); color: var(--accent); border-radius: 999px; padding: 4px 12px; font-size: 12px; font-weight: 700; white-space: nowrap; }
|
|
255
|
+
.md-kpis { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-flow: dense; gap: 8px; }
|
|
256
|
+
@media (min-width: 640px) { .md-kpis { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
|
257
|
+
.md-kpi { border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; background: var(--panel); }
|
|
258
|
+
.md-kpi p { margin: 0; }
|
|
259
|
+
.md-kpi .k { font-size: 10px; text-transform: uppercase; letter-spacing: .14em; color: var(--dim); }
|
|
260
|
+
.md-kpi .v { font-size: 22px; font-weight: 800; letter-spacing: -0.02em; margin-top: 4px; font-variant-numeric: tabular-nums; }
|
|
261
|
+
.md-kpi .d { font-size: 11px; margin-top: 2px; font-variant-numeric: tabular-nums; }
|
|
262
|
+
.md-kpi .d.up { color: var(--accent); }
|
|
263
|
+
.md-kpi .d.flat { color: var(--dim); }
|
|
264
|
+
.md-chart { position: relative; border: 1px solid var(--line); border-radius: 12px; padding: 14px; background: var(--panel); }
|
|
265
|
+
.md-chart-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
|
|
266
|
+
.md-chart-title { font-size: 13px; font-weight: 700; letter-spacing: -0.01em; margin: 0; }
|
|
267
|
+
.md-chart-sub { font-size: 11px; color: var(--dim); margin: 2px 0 0; text-transform: uppercase; letter-spacing: .12em; }
|
|
268
|
+
.md-trend { font-size: 11px; font-weight: 700; white-space: nowrap; border-radius: 999px; padding: 3px 10px; background: var(--track); color: var(--dim); }
|
|
269
|
+
.md-trend.up { background: var(--accent-soft); color: var(--accent); }
|
|
270
|
+
#mdSpark { width: 100%; height: 190px; display: block; }
|
|
271
|
+
#mdSpark .grid { stroke: var(--line); stroke-width: 1; }
|
|
272
|
+
#mdSpark .area { fill: url(#mdGrad); stroke: none; }
|
|
273
|
+
#mdSpark .line { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
|
|
274
|
+
#mdSpark .dot { fill: var(--accent); stroke: var(--panel); stroke-width: 2; }
|
|
275
|
+
#mdSpark .hoverdot { fill: var(--accent); stroke: var(--panel); stroke-width: 2; }
|
|
276
|
+
.md-tip { position: absolute; pointer-events: none; background: var(--panel); color: var(--ink); border: 1px solid var(--line); box-shadow: 0 8px 24px rgba(0, 0, 0, .28); border-radius: 8px; padding: 6px 10px; font-size: 11px; line-height: 1.5; white-space: nowrap; max-width: calc(100% - 28px); overflow: hidden; text-overflow: ellipsis; transform: translate(-50%, -110%); display: none; z-index: 5; font-variant-numeric: tabular-nums; }
|
|
277
|
+
.md-cols { display: grid; grid-template-columns: minmax(0, 1fr); grid-auto-flow: dense; gap: 12px; }
|
|
278
|
+
@media (min-width: 720px) { .md-cols { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); } }
|
|
279
|
+
.md-months { display: flex; justify-content: space-between; margin-top: 6px; font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: .12em; }
|
|
280
|
+
.md-bars { display: flex; align-items: flex-end; gap: 3px; height: 132px; background-image: linear-gradient(var(--line) 1px, transparent 1px); background-size: 100% 33px; background-position: 0 bottom; }
|
|
281
|
+
.md-bars > span { flex: 1; min-width: 3px; border-radius: 4px 4px 2px 2px; background: var(--accent); opacity: .4; cursor: default; }
|
|
282
|
+
.md-bars > span:hover { opacity: 1; }
|
|
283
|
+
.md-bars > span.top { opacity: 1; box-shadow: 0 0 8px var(--accent-soft); }
|
|
284
|
+
.md-bars > span.zero { background: var(--track); opacity: 1; box-shadow: none; }
|
|
285
|
+
.md-note { font-size: 11px; color: var(--dim); margin: 8px 0 0; font-variant-numeric: tabular-nums; }
|
|
286
|
+
.md-donut-wrap { position: relative; width: 150px; height: 150px; margin: 4px auto 8px; }
|
|
287
|
+
#mdMix { width: 150px; height: 150px; display: block; transform: rotate(-90deg); }
|
|
288
|
+
#mdMix circle { fill: none; stroke-width: 16; }
|
|
289
|
+
#mdMix .tk { stroke: var(--track); }
|
|
290
|
+
.md-donut-center { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; }
|
|
291
|
+
.md-donut-center p { margin: 0; font-size: 17px; font-weight: 800; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
|
|
292
|
+
.md-legend { display: grid; gap: 6px; font-size: 11px; }
|
|
293
|
+
.md-legend .row { display: flex; align-items: center; gap: 8px; }
|
|
294
|
+
.md-legend .dot { width: 8px; height: 8px; border-radius: 999px; flex-shrink: 0; }
|
|
295
|
+
.md-legend .pct { margin-left: auto; color: var(--dim); font-variant-numeric: tabular-nums; }
|
|
296
|
+
.md-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-flow: dense; gap: 8px; }
|
|
297
|
+
@media (min-width: 640px) { .md-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
|
298
|
+
.md-stat { border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; background: var(--panel); }
|
|
299
|
+
.md-stat p { margin: 0; }
|
|
300
|
+
.md-stat .k { font-size: 10px; text-transform: uppercase; letter-spacing: .14em; color: var(--dim); }
|
|
301
|
+
.md-stat .v { font-size: 20px; font-weight: 800; letter-spacing: -0.02em; margin-top: 4px; font-variant-numeric: tabular-nums; }
|
|
302
|
+
.md-stat .s { font-size: 11px; color: var(--dim); margin-top: 2px; }
|
|
303
|
+
#mdBadge .brandmark { width: 44px; height: 44px; border-radius: 12px; }
|
|
304
|
+
@media (prefers-reduced-motion: reduce) { .md-bars > span { transition: none; } }
|
package/dashboard/template.html
CHANGED
|
@@ -83,6 +83,10 @@
|
|
|
83
83
|
<p class="dlg-name">Session transcripts <span class="dlg-own">host-owned · never touched</span></p>
|
|
84
84
|
<p class="mono dlg-file" id="pathSessions" title="">–</p>
|
|
85
85
|
</div>
|
|
86
|
+
<div class="dlg-path">
|
|
87
|
+
<p class="dlg-name">Usage DB <span class="dlg-own">tersio-owned · reset clears</span></p>
|
|
88
|
+
<p class="mono dlg-file" id="pathUsageDb" title="">–</p>
|
|
89
|
+
</div>
|
|
86
90
|
</div>
|
|
87
91
|
<div class="dlg-danger">
|
|
88
92
|
<p class="dlg-label">Danger zone</p>
|
|
@@ -98,6 +102,76 @@
|
|
|
98
102
|
</div>
|
|
99
103
|
</dialog>
|
|
100
104
|
|
|
105
|
+
<!-- Model detail dialog (shadcn chart language, native modal): kpi strip,
|
|
106
|
+
gradient area chart with range tabs, rounded bars, token-mix donut. -->
|
|
107
|
+
<dialog id="modelDialog" class="dlg mdlg" aria-labelledby="mdTitle">
|
|
108
|
+
<div class="dlg-head">
|
|
109
|
+
<div class="min-w-0 flex items-center gap-3">
|
|
110
|
+
<span id="mdBadge"></span>
|
|
111
|
+
<div class="min-w-0">
|
|
112
|
+
<p class="dlg-title truncate" id="mdTitle">Model</p>
|
|
113
|
+
<p class="dlg-desc mono" id="mdSub">–</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
117
|
+
<span class="md-rankpill mono" id="mdRank">#–</span>
|
|
118
|
+
<button id="mdClose" class="btn-push flex shrink-0 items-center p-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Close model details">
|
|
119
|
+
<i data-lucide="x" class="size-4"></i>
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="md-body">
|
|
124
|
+
<div class="md-kpis" id="mdKpis"></div>
|
|
125
|
+
<div class="md-chart">
|
|
126
|
+
<div class="md-chart-head">
|
|
127
|
+
<div class="min-w-0">
|
|
128
|
+
<p class="md-chart-title">Token volume</p>
|
|
129
|
+
<p class="md-chart-sub mono" id="mdMomRange">–</p>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
132
|
+
<span class="md-trend mono" id="mdTrend"></span>
|
|
133
|
+
<span class="seg mono text-xs flex items-center gap-1 p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="group" aria-label="Chart range">
|
|
134
|
+
<button data-mdspan="30" class="px-2.5 py-1" role="button">30D</button>
|
|
135
|
+
<button data-mdspan="90" class="px-2.5 py-1" role="button">90D</button>
|
|
136
|
+
<button data-mdspan="all" class="on px-2.5 py-1" role="button">ALL</button>
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<svg id="mdSpark" viewBox="0 0 560 190" preserveAspectRatio="none" aria-hidden="true"></svg>
|
|
141
|
+
<div class="md-tip mono" id="mdTip"></div>
|
|
142
|
+
<div class="md-months mono" id="mdMonths" aria-hidden="true"></div>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="md-cols">
|
|
145
|
+
<div class="md-chart">
|
|
146
|
+
<div class="md-chart-head">
|
|
147
|
+
<div class="min-w-0">
|
|
148
|
+
<p class="md-chart-title">Daily activity</p>
|
|
149
|
+
<p class="md-chart-sub mono" id="mdBarsSub">tokens per day</p>
|
|
150
|
+
</div>
|
|
151
|
+
<span class="md-trend mono" id="mdActive"></span>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="md-bars" id="mdBars" aria-hidden="true"></div>
|
|
154
|
+
<div class="md-months mono" id="mdBarsMonths" aria-hidden="true"></div>
|
|
155
|
+
<div class="md-note mono" id="mdBarsNote"></div>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="md-chart">
|
|
158
|
+
<div class="md-chart-head">
|
|
159
|
+
<div class="min-w-0">
|
|
160
|
+
<p class="md-chart-title">Token mix</p>
|
|
161
|
+
<p class="md-chart-sub mono">input vs output vs cache</p>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="md-donut-wrap">
|
|
165
|
+
<svg id="mdMix" viewBox="0 0 140 140" aria-hidden="true"></svg>
|
|
166
|
+
<div class="md-donut-center"><p class="mono" id="mdMixTotal">–</p></div>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="md-legend mono" id="mdMixLegend"></div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="md-grid" id="mdStats"></div>
|
|
172
|
+
</div>
|
|
173
|
+
</dialog>
|
|
174
|
+
|
|
101
175
|
<!-- Hero: centered total -->
|
|
102
176
|
<section class="mx-auto max-w-6xl text-center pt-14 pb-10 relative">
|
|
103
177
|
<div class="hero-glow" aria-hidden="true"></div>
|
|
@@ -193,20 +267,18 @@
|
|
|
193
267
|
</section>
|
|
194
268
|
|
|
195
269
|
<!-- Top models: 3 columns -->
|
|
196
|
-
<section class="mt-8" aria-label="Top models">
|
|
270
|
+
<section class="mt-8 modelsSection" aria-label="Top models">
|
|
197
271
|
<div class="flex items-center gap-2 mb-1">
|
|
198
272
|
<i data-lucide="cpu" class="size-4" style="color: var(--accent)"></i>
|
|
199
273
|
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Top models</h2>
|
|
200
274
|
<span class="mono text-xs ml-auto" style="color: var(--dim)">usage across sessions</span>
|
|
201
275
|
</div>
|
|
202
|
-
<p class="mono text-xs mb-4" style="color: var(--dim)">week-over-week vs prior 7 days</p>
|
|
203
276
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3" id="modelCards"></div>
|
|
204
277
|
</section>
|
|
205
278
|
|
|
206
279
|
|
|
207
|
-
<!-- Models
|
|
208
|
-
|
|
209
|
-
<section class="col-span-12 lg:col-span-6 rounded-xl p-5 overflow-hidden flex flex-col" style="border: 1px solid var(--line); background: var(--panel)" aria-label="All models">
|
|
280
|
+
<!-- Models: own full-width section (split out of the old 50/50 twin card) -->
|
|
281
|
+
<section class="rise mt-8 rounded-xl p-5 overflow-hidden flex flex-col modelsSection" style="border: 1px solid var(--line); background: var(--panel)" aria-label="All models">
|
|
210
282
|
<div class="flex items-center gap-2 mb-1">
|
|
211
283
|
<i data-lucide="layers" class="size-4" style="color: var(--accent)"></i>
|
|
212
284
|
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Models</h2>
|
|
@@ -230,22 +302,29 @@
|
|
|
230
302
|
<p class="empty-desc">Model token totals will appear here once sessions report tokens.</p>
|
|
231
303
|
</div>
|
|
232
304
|
</section>
|
|
233
|
-
|
|
305
|
+
|
|
306
|
+
<!-- Recent requests: full-width, sitting with the other per-request views
|
|
307
|
+
right after Models. When is absolutely stamped, so the column reads as
|
|
308
|
+
a real timeline and sorts honestly against Time. -->
|
|
309
|
+
<section class="rise mt-8 rounded-xl p-5 overflow-hidden flex flex-col" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Recent requests">
|
|
234
310
|
<div class="flex items-center gap-2 mb-1">
|
|
235
311
|
<i data-lucide="history" class="size-4" style="color: var(--accent)"></i>
|
|
236
312
|
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Recent requests</h2>
|
|
237
313
|
<span class="mono text-xs ml-auto truncate shrink-0" style="color: var(--dim)" id="recentCount"></span>
|
|
238
314
|
</div>
|
|
239
|
-
<p class="mono text-xs mb-4 truncate" style="color: var(--dim)" title="Time = elapsed time with the model; Speed = output tokens per second">latest assistant messages ·
|
|
240
|
-
<div class="scrollarea overflow-x-auto">
|
|
315
|
+
<p class="mono text-xs mb-4 truncate" style="color: var(--dim)" title="Time = elapsed time with the model; Speed = output tokens per second">latest assistant messages · local time · sort any column · cost is measured when the host recorded it</p>
|
|
316
|
+
<div class="scrollarea overflow-x-auto" style="max-height: 560px">
|
|
241
317
|
<table class="w-full mono text-[13px]" id="recentTable">
|
|
242
|
-
<colgroup><col><col style="width:120px"><col style="width:
|
|
318
|
+
<colgroup><col><col style="width:96px"><col style="width:96px"><col style="width:120px"><col style="width:116px"><col style="width:120px"><col style="width:158px"></colgroup>
|
|
243
319
|
<thead>
|
|
244
320
|
<tr class="text-left text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">
|
|
245
|
-
<th class="font-normal py-2 pr-3">Model
|
|
246
|
-
<th class="font-normal text-right py-2 pr-3">
|
|
247
|
-
<th class="font-normal text-right py-2 pr-3"
|
|
248
|
-
<th class="font-normal text-right py-2">
|
|
321
|
+
<th class="font-normal py-2 pr-3" aria-sort="none"><button class="thsort" data-sort="model">Model <span class="arr">⇅</span></button></th>
|
|
322
|
+
<th class="font-normal text-right py-2 pr-3" aria-sort="none"><button class="thsort num" data-sort="input">Input <span class="arr">⇅</span></button></th>
|
|
323
|
+
<th class="font-normal text-right py-2 pr-3" aria-sort="none"><button class="thsort num" data-sort="output">Output <span class="arr">⇅</span></button></th>
|
|
324
|
+
<th class="font-normal text-right py-2 pr-3" aria-sort="none"><button class="thsort num" data-sort="time" title="Elapsed time with the model / output token speed">Time <span class="arr">⇅</span></button></th>
|
|
325
|
+
<th class="font-normal py-2 pr-3" aria-sort="none"><button class="thsort" data-sort="status">Status <span class="arr">⇅</span></button></th>
|
|
326
|
+
<th class="font-normal text-right py-2 pr-3" aria-sort="none"><button class="thsort num" data-sort="cost">Cost <span class="arr">⇅</span></button></th>
|
|
327
|
+
<th class="font-normal text-right py-2" aria-sort="none"><button class="thsort num" data-sort="when" title="Local timestamp: month day, hour:minute:second">When <span class="arr">⇅</span></button></th>
|
|
249
328
|
</tr>
|
|
250
329
|
</thead>
|
|
251
330
|
<tbody id="recent"></tbody>
|
|
@@ -260,21 +339,22 @@
|
|
|
260
339
|
<span id="recentRange">Showing 0 of 0</span>
|
|
261
340
|
<span class="seg flex items-center gap-1 p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="group" aria-label="Requests per page">
|
|
262
341
|
<button data-pp="10" class="px-2.5 py-1" role="button">10</button>
|
|
263
|
-
<button data-pp="15" class="
|
|
264
|
-
<button data-pp="25" class="px-2.5 py-1" role="button">25</button>
|
|
342
|
+
<button data-pp="15" class="px-2.5 py-1" role="button">15</button>
|
|
343
|
+
<button data-pp="25" class="on px-2.5 py-1" role="button">25</button>
|
|
265
344
|
<button data-pp="50" class="px-2.5 py-1" role="button">50</button>
|
|
266
345
|
</span>
|
|
267
346
|
<span class="ml-auto flex items-center gap-1" id="recentPages" role="navigation" aria-label="Recent pages"></span>
|
|
268
347
|
</div>
|
|
269
348
|
</section>
|
|
270
|
-
</div>
|
|
271
349
|
|
|
272
350
|
<!-- Command tools: own card, header outside like Models -->
|
|
273
351
|
<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">
|
|
274
352
|
<div class="flex items-center gap-2 mb-1">
|
|
275
353
|
<i data-lucide="terminal" class="size-4" style="color: var(--accent)"></i>
|
|
276
354
|
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Command tools</h2>
|
|
277
|
-
<
|
|
355
|
+
<div class="flex items-center gap-2 ml-auto min-w-0">
|
|
356
|
+
<span class="mono text-xs truncate min-w-0" style="color: var(--dim)" id="toolsScope">tool calls in sessions</span>
|
|
357
|
+
</div>
|
|
278
358
|
</div>
|
|
279
359
|
<p class="mono text-xs mb-4 truncate" style="color: var(--dim)">count · tokens saved · avg rate · avg time · share of executions</p>
|
|
280
360
|
<div class="overflow-x-auto">
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
+
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.js';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { readCavemanDefault } from '../shared/plugin-settings.js';
|
|
6
6
|
const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
@@ -46,9 +46,8 @@ export default function cavemanSessionExtension(pi) {
|
|
|
46
46
|
let isActive = false;
|
|
47
47
|
let lastCtx = undefined;
|
|
48
48
|
function syncStatus(ctx) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const c = ctx || lastCtx;
|
|
49
|
+
lastCtx = paintableCtx(lastCtx, ctx);
|
|
50
|
+
const c = lastCtx;
|
|
52
51
|
if (!c?.ui?.setStatus)
|
|
53
52
|
return;
|
|
54
53
|
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
-
import { activeModesSummary, asPromptArray, COMBO_LEVELS, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, normalizeComboLevel, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, setSharedComboMode, systemPromptIncludes, } from '../shared/session-state.js';
|
|
6
|
+
import { activeModesSummary, asPromptArray, COMBO_LEVELS, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, normalizeComboLevel, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, setSharedComboMode, systemPromptIncludes, } from '../shared/session-state.js';
|
|
7
7
|
import { readComboDefault, readPonytailDefault } from '../shared/plugin-settings.js';
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
const PONYTAIL_FALLBACK_INTENSITY = {
|
|
@@ -31,9 +31,8 @@ export default function comboToggleExtension(pi) {
|
|
|
31
31
|
pi.setLabel?.('Combo session toggle (all 3 add-ons)');
|
|
32
32
|
let lastCtx = undefined;
|
|
33
33
|
function syncStatus(ctx) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const c = ctx || lastCtx;
|
|
34
|
+
lastCtx = paintableCtx(lastCtx, ctx);
|
|
35
|
+
const c = lastCtx;
|
|
37
36
|
if (!c?.ui?.setStatus)
|
|
38
37
|
return;
|
|
39
38
|
// Single unified bar replaces the three per-extension bars while a preset is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.js';
|
|
3
|
+
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.js';
|
|
4
4
|
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
5
5
|
const DEFAULT_ENABLED = false;
|
|
6
6
|
function asBoolean(value) {
|
|
@@ -27,9 +27,8 @@ export default function rtkSessionExtension(pi) {
|
|
|
27
27
|
let isActive = false;
|
|
28
28
|
let lastCtx = undefined;
|
|
29
29
|
function syncStatus(ctx) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const c = ctx || lastCtx;
|
|
30
|
+
lastCtx = paintableCtx(lastCtx, ctx);
|
|
31
|
+
const c = lastCtx;
|
|
33
32
|
if (!c?.ui?.setStatus)
|
|
34
33
|
return;
|
|
35
34
|
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
@@ -7,11 +7,23 @@
|
|
|
7
7
|
// Caveman and Ponytail have no per-command counters (instruction-following
|
|
8
8
|
// isn't metered); their gains are bench-measured in BENCHMARK.md and cited
|
|
9
9
|
// as static figures wherever RTK rows appear.
|
|
10
|
+
//
|
|
11
|
+
// Rows group by command alone, machine-wide: one row per command no matter
|
|
12
|
+
// which repo ran it. Project filtering stays out — wasted column, same logic.
|
|
10
13
|
import { execFileSync } from 'node:child_process';
|
|
11
14
|
import fs from 'node:fs';
|
|
12
15
|
import os from 'node:os';
|
|
13
16
|
import path from 'node:path';
|
|
14
17
|
const EMPTY = { commands: 0, saved: 0, input: 0, avgPct: 0, totalMs: 0, byCommand: [] };
|
|
18
|
+
// RTK stores the whole command line, and `rtk ls -d` with a long path list
|
|
19
|
+
// runs to tens of KB. The table shows one clipped cell, so bound the string
|
|
20
|
+
// before it crosses the sqlite3 CLI and lands in every /data.json poll.
|
|
21
|
+
const MAX_CMD_CHARS = 200;
|
|
22
|
+
// The query() reader is line- and tab-delimited, and RTK stores heredocs and
|
|
23
|
+
// `node -e` scripts verbatim (~67 rows here), so one logical row can arrive as
|
|
24
|
+
// several lines and derail every row after it. Fold whitespace into spaces
|
|
25
|
+
// first: the stored command keeps its newlines, only the read view flattens.
|
|
26
|
+
const CMD_ONE_LINE = `replace(replace(replace(rtk_cmd, char(10), ' '), char(13), ' '), char(9), ' ')`;
|
|
15
27
|
export function rtkDbPath() {
|
|
16
28
|
const override = process.env.TERSIO_RTK_DB;
|
|
17
29
|
if (override)
|
|
@@ -44,7 +56,7 @@ export function readRtkGain(limit = 10, cutoffMs) {
|
|
|
44
56
|
? `WHERE CAST(strftime('%s', timestamp) AS INTEGER) >= ${Math.floor(cutoffMs / 1000)}`
|
|
45
57
|
: '';
|
|
46
58
|
const [[commands = '0', saved = '0', input = '0', avgPct = '0', totalMs = '0'] = []] = query(rtkDbPath(), `SELECT COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(SUM(input_tokens),0), COALESCE(AVG(savings_pct),0), COALESCE(SUM(exec_time_ms),0) FROM commands ${where};`);
|
|
47
|
-
const byCommand = query(rtkDbPath(), `SELECT
|
|
59
|
+
const byCommand = query(rtkDbPath(), `SELECT substr(${CMD_ONE_LINE}, 1, ${MAX_CMD_CHARS}), COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(AVG(savings_pct),0), COALESCE(AVG(exec_time_ms),0) FROM commands ${where} GROUP BY rtk_cmd ORDER BY SUM(saved_tokens) DESC LIMIT ${Math.max(1, Math.floor(limit))};`).map(([command, count, savedRow, pct, ms]) => ({
|
|
48
60
|
command,
|
|
49
61
|
count: Number(count) || 0,
|
|
50
62
|
saved: Number(savedRow) || 0,
|
|
@@ -79,6 +79,21 @@ export function paintStatusBar(ui, key, emoji, label, isActive) {
|
|
|
79
79
|
const indicator = isActive && theme?.fg ? theme.fg('accent', emoji) : emoji;
|
|
80
80
|
ui?.setStatus?.(key, theme?.fg ? `${indicator} ${theme.fg('muted', label)}` : `${indicator} ${label}`);
|
|
81
81
|
}
|
|
82
|
+
// Which context a status bar should remember to paint through.
|
|
83
|
+
//
|
|
84
|
+
// A UI-less event — a headless child session_start, or a top-level
|
|
85
|
+
// session_start that fires before the TUI attaches — must never become the
|
|
86
|
+
// remembered context. Two things went wrong when it did: the old
|
|
87
|
+
// `ctx || lastCtx` fallback only triggered for a *falsy* ctx, so a truthy
|
|
88
|
+
// ctx without `ui` returned early anyway, and remembering it destroyed the one
|
|
89
|
+
// paintable context we had. Every later bare syncStatus() then went mute —
|
|
90
|
+
// and the bare call is the bridge-listener path, which is how a mode change
|
|
91
|
+
// made by a sibling extension reaches this bar. The symptom is a bar frozen on
|
|
92
|
+
// a preset the session no longer has: it shows "combo BALANCED" while caveman
|
|
93
|
+
// and rtk are already off.
|
|
94
|
+
export function paintableCtx(remembered, next) {
|
|
95
|
+
return next?.ui?.setStatus ? next : remembered;
|
|
96
|
+
}
|
|
82
97
|
// Last-wins scan for a custom session entry; skips entries whose value fails
|
|
83
98
|
// to parse so a corrupt write never shadows an older valid one.
|
|
84
99
|
export function lastCustomValue(entries, customType, pick) {
|