@krtclcdy/tersio 2.16.0 → 2.18.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 +18 -2
- package/cli/dashboard.js +4 -0
- package/cli/usage.js +21 -6
- package/dashboard/app.js +395 -89
- package/dashboard/styles.css +39 -5
- package/dashboard/template.html +47 -19
- 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 +16 -1
- package/extensions/shared/session-state.js +15 -0
- package/extensions/shared/usage-ledger.js +72 -9
- package/package.json +1 -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,7 +70,21 @@
|
|
|
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. */
|
|
61
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; }
|
|
83
|
+
.zface { display: inline-block; vertical-align: -2px; width: 14px; height: 14px; }
|
|
84
|
+
.zface.good { color: var(--accent); }
|
|
85
|
+
.zface.warn { color: #fbbf24; }
|
|
86
|
+
.zface.bad { color: #f87171; }
|
|
87
|
+
#tip .tvr .zface { vertical-align: -3px; }
|
|
62
88
|
.fxbtn { background: var(--track); color: var(--ink); border: 0; cursor: pointer; gap: 6px; }
|
|
63
89
|
.fxbtn:focus-visible { outline: 1px solid var(--accent); }
|
|
64
90
|
.fxbtn svg { transition: transform .15s; }
|
|
@@ -97,7 +123,7 @@
|
|
|
97
123
|
.seg button.on { background: var(--accent-soft); color: var(--ink); }
|
|
98
124
|
.seg button:not(.on):hover { color: var(--ink); }
|
|
99
125
|
.mrow { transition: background .25s; }
|
|
100
|
-
.mrow:hover { box-shadow: var(--shadow-xs); }
|
|
126
|
+
.mrow:hover { background: var(--track); box-shadow: var(--shadow-xs); }
|
|
101
127
|
.stripcell { transition: background .25s; }
|
|
102
128
|
.stripcell:hover { box-shadow: var(--shadow-xs); }
|
|
103
129
|
button, .tilt, .cell, .stripcell { cursor: pointer; }
|
|
@@ -117,25 +143,33 @@
|
|
|
117
143
|
.cell.l1 { background: var(--cell-1); } .cell.l2 { background: var(--cell-2); }
|
|
118
144
|
.cell.l3 { background: var(--cell-3); } .cell.l4 { background: var(--cell-4); }
|
|
119
145
|
.brandmark { width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; flex-shrink: 0; color: #fff; }
|
|
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; }
|
|
120
148
|
.brandmark img { width: 18px; height: 18px; }
|
|
149
|
+
.brandmark svg { width: 18px; height: 18px; display: block; }
|
|
121
150
|
.brandmark.sm { width: 28px; height: 28px; border-radius: 8px; }
|
|
122
151
|
.brandmark.sm img { width: 14px; height: 14px; }
|
|
152
|
+
.brandmark.sm svg { width: 14px; height: 14px; }
|
|
123
153
|
.brandmark .fb { display: none; width: 18px; height: 18px; }
|
|
124
154
|
.brandmark.sm .fb { width: 14px; height: 14px; }
|
|
125
155
|
.ghostimg { position: absolute; right: -6px; bottom: -10px; width: 88px; height: 88px;
|
|
126
156
|
opacity: .08; pointer-events: none; user-select: none; }
|
|
157
|
+
span.ghostimg { display: grid; place-items: center; }
|
|
158
|
+
span.ghostimg svg { width: 72px; height: 72px; display: block; }
|
|
159
|
+
.ghosticon { position: absolute; right: 4px; bottom: -6px; width: 72px; height: 72px;
|
|
160
|
+
opacity: .06; pointer-events: none; user-select: none; color: var(--dim); }
|
|
127
161
|
@media (prefers-color-scheme: light) {
|
|
128
162
|
:root:not([data-theme="dark"]) .ghostimg { opacity: .06; filter: brightness(0); }
|
|
129
163
|
}
|
|
130
164
|
html[data-theme="light"] .ghostimg { opacity: .06; filter: brightness(0); }
|
|
131
|
-
#tip { position: fixed; z-index: 100; pointer-events: none; min-width:
|
|
165
|
+
#tip { position: fixed; z-index: 100; pointer-events: none; min-width: 250px; max-width: 370px;
|
|
132
166
|
background: var(--panel);
|
|
133
167
|
border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px;
|
|
134
168
|
box-shadow: var(--shadow-xs); opacity: 0; transition: opacity .15s; }
|
|
135
169
|
#tip.show { opacity: 1; }
|
|
136
170
|
#tip .tt { font-size: 11px; text-transform: uppercase; letter-spacing: .14em; font-weight: 700; }
|
|
137
171
|
#tip .tv { font-size: 13px; margin: 2px 0 8px; }
|
|
138
|
-
#tip .tr { display: flex; align-items: center; gap:
|
|
172
|
+
#tip .tr { display: flex; align-items: center; gap: 10px; font-size: 12px; padding: 3px 0; }
|
|
139
173
|
#tip .sw { width: 9px; height: 9px; border-radius: 2.5px; flex-shrink: 0; }
|
|
140
174
|
#tip .tn { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
141
175
|
.scrollarea { overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
|
|
@@ -143,9 +177,9 @@
|
|
|
143
177
|
.scrollarea::-webkit-scrollbar-thumb { background: var(--line); border-radius: 99px; }
|
|
144
178
|
.scrollarea::-webkit-scrollbar-track { background: transparent; }
|
|
145
179
|
.rrow { transition: background .2s; cursor: pointer; }
|
|
146
|
-
.rrow:hover { box-shadow: var(--shadow-xs); }
|
|
180
|
+
.rrow:hover { background: var(--track); box-shadow: var(--shadow-xs); }
|
|
147
181
|
#recentTable thead th { position: sticky; top: 0; background: var(--panel); z-index: 1; }
|
|
148
|
-
#tip .tvr { flex-shrink: 0; }
|
|
182
|
+
#tip .tvr { flex-shrink: 0; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
149
183
|
.modelcard { transition: background .25s; }
|
|
150
184
|
.modelcard:hover { box-shadow: var(--shadow-xs); }
|
|
151
185
|
|
package/dashboard/template.html
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
<div class="dlg-head">
|
|
47
47
|
<div class="min-w-0">
|
|
48
48
|
<p class="dlg-title" id="settingsTitle">Settings</p>
|
|
49
|
-
<p class="dlg-desc">Appearance, data, and where statistics live
|
|
49
|
+
<p class="dlg-desc">Appearance, data, and where statistics live. <span id="verChip" class="mono">tersio</span></p>
|
|
50
50
|
</div>
|
|
51
51
|
<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">
|
|
52
52
|
<i data-lucide="x" class="size-4"></i>
|
|
@@ -193,48 +193,64 @@
|
|
|
193
193
|
</section>
|
|
194
194
|
|
|
195
195
|
<!-- Top models: 3 columns -->
|
|
196
|
-
<section class="mt-8" aria-label="Top models">
|
|
196
|
+
<section class="mt-8 modelsSection" aria-label="Top models">
|
|
197
197
|
<div class="flex items-center gap-2 mb-1">
|
|
198
198
|
<i data-lucide="cpu" class="size-4" style="color: var(--accent)"></i>
|
|
199
199
|
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Top models</h2>
|
|
200
200
|
<span class="mono text-xs ml-auto" style="color: var(--dim)">usage across sessions</span>
|
|
201
201
|
</div>
|
|
202
|
-
<p class="mono text-xs mb-4" style="color: var(--dim)">week-over-week vs prior 7 days</p>
|
|
203
202
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3" id="modelCards"></div>
|
|
204
203
|
</section>
|
|
205
204
|
|
|
206
205
|
|
|
207
|
-
<!-- Models
|
|
208
|
-
|
|
209
|
-
<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">
|
|
206
|
+
<!-- Models: own full-width section (split out of the old 50/50 twin card) -->
|
|
207
|
+
<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
208
|
<div class="flex items-center gap-2 mb-1">
|
|
211
209
|
<i data-lucide="layers" class="size-4" style="color: var(--accent)"></i>
|
|
212
210
|
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Models</h2>
|
|
213
211
|
<span class="mono text-xs ml-auto truncate shrink-0" style="color: var(--dim)" id="modelsCount"></span>
|
|
214
212
|
</div>
|
|
215
|
-
<p class="mono text-xs mb-4" style="color: var(--dim)">ranked by tokens / top
|
|
216
|
-
<div class="scrollarea"
|
|
213
|
+
<p class="mono text-xs mb-4" style="color: var(--dim)">ranked by tokens / top 10</p>
|
|
214
|
+
<div class="scrollarea"><ol id="models" class="overflow-hidden"></ol></div>
|
|
215
|
+
<div class="mono text-xs mt-4 pt-4 flex flex-wrap items-center gap-x-4 gap-y-3 mt-auto" style="color: var(--dim); border-top: 1px solid var(--line)">
|
|
216
|
+
<span id="modelRange">Showing 0 of 0</span>
|
|
217
|
+
<span class="seg flex items-center gap-1 p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="group" aria-label="Models per page">
|
|
218
|
+
<button data-pp="10" class="on px-2.5 py-1" role="button">10</button>
|
|
219
|
+
<button data-pp="15" class="px-2.5 py-1" role="button">15</button>
|
|
220
|
+
<button data-pp="25" class="px-2.5 py-1" role="button">25</button>
|
|
221
|
+
<button data-pp="50" class="px-2.5 py-1" role="button">50</button>
|
|
222
|
+
</span>
|
|
223
|
+
<span class="ml-auto flex items-center gap-1" id="modelPages" role="navigation" aria-label="Model pages"></span>
|
|
224
|
+
</div>
|
|
217
225
|
<div id="emptyModels" class="empty hidden">
|
|
218
226
|
<span class="empty-icon"><i data-lucide="boxes" class="size-5"></i></span>
|
|
219
227
|
<p class="empty-title">No models yet</p>
|
|
220
228
|
<p class="empty-desc">Model token totals will appear here once sessions report tokens.</p>
|
|
221
229
|
</div>
|
|
222
230
|
</section>
|
|
223
|
-
|
|
231
|
+
|
|
232
|
+
<!-- Recent requests: full-width, sitting with the other per-request views
|
|
233
|
+
right after Models. When is absolutely stamped, so the column reads as
|
|
234
|
+
a real timeline and sorts honestly against Time. -->
|
|
235
|
+
<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">
|
|
224
236
|
<div class="flex items-center gap-2 mb-1">
|
|
225
237
|
<i data-lucide="history" class="size-4" style="color: var(--accent)"></i>
|
|
226
238
|
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Recent requests</h2>
|
|
227
239
|
<span class="mono text-xs ml-auto truncate shrink-0" style="color: var(--dim)" id="recentCount"></span>
|
|
228
240
|
</div>
|
|
229
|
-
<p class="mono text-xs mb-4 truncate" style="color: var(--dim)">latest assistant messages</p>
|
|
230
|
-
<div class="scrollarea overflow-x-auto" style="max-height:
|
|
241
|
+
<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>
|
|
242
|
+
<div class="scrollarea overflow-x-auto" style="max-height: 560px">
|
|
231
243
|
<table class="w-full mono text-[13px]" id="recentTable">
|
|
232
|
-
<colgroup><col><col style="width:
|
|
244
|
+
<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>
|
|
233
245
|
<thead>
|
|
234
246
|
<tr class="text-left text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">
|
|
235
|
-
<th class="font-normal py-2 pr-3">Model
|
|
236
|
-
<th class="font-normal text-right py-2 pr-3">
|
|
237
|
-
<th class="font-normal text-right py-2">
|
|
247
|
+
<th class="font-normal py-2 pr-3" aria-sort="none"><button class="thsort" data-sort="model">Model <span class="arr">⇅</span></button></th>
|
|
248
|
+
<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>
|
|
249
|
+
<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>
|
|
250
|
+
<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>
|
|
251
|
+
<th class="font-normal py-2 pr-3" aria-sort="none"><button class="thsort" data-sort="status">Status <span class="arr">⇅</span></button></th>
|
|
252
|
+
<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>
|
|
253
|
+
<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>
|
|
238
254
|
</tr>
|
|
239
255
|
</thead>
|
|
240
256
|
<tbody id="recent"></tbody>
|
|
@@ -245,24 +261,37 @@
|
|
|
245
261
|
<p class="empty-title">No requests yet</p>
|
|
246
262
|
<p class="empty-desc">Recent assistant messages will show here once sessions report tokens.</p>
|
|
247
263
|
</div>
|
|
264
|
+
<div class="mono text-xs mt-4 pt-4 flex flex-wrap items-center gap-x-4 gap-y-3 mt-auto" style="color: var(--dim); border-top: 1px solid var(--line)">
|
|
265
|
+
<span id="recentRange">Showing 0 of 0</span>
|
|
266
|
+
<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">
|
|
267
|
+
<button data-pp="10" class="px-2.5 py-1" role="button">10</button>
|
|
268
|
+
<button data-pp="15" class="px-2.5 py-1" role="button">15</button>
|
|
269
|
+
<button data-pp="25" class="on px-2.5 py-1" role="button">25</button>
|
|
270
|
+
<button data-pp="50" class="px-2.5 py-1" role="button">50</button>
|
|
271
|
+
</span>
|
|
272
|
+
<span class="ml-auto flex items-center gap-1" id="recentPages" role="navigation" aria-label="Recent pages"></span>
|
|
273
|
+
</div>
|
|
248
274
|
</section>
|
|
249
|
-
</div>
|
|
250
275
|
|
|
251
276
|
<!-- Command tools: own card, header outside like Models -->
|
|
252
277
|
<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">
|
|
253
278
|
<div class="flex items-center gap-2 mb-1">
|
|
254
279
|
<i data-lucide="terminal" class="size-4" style="color: var(--accent)"></i>
|
|
255
280
|
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Command tools</h2>
|
|
256
|
-
<
|
|
281
|
+
<div class="flex items-center gap-2 ml-auto min-w-0">
|
|
282
|
+
<select id="cmdProject" class="mono text-xs px-2 py-1.5 rounded-[10px]" style="border: 1px solid var(--line); background: var(--panel); color: var(--ink); max-width: 190px" aria-label="Filter command tools by project"></select>
|
|
283
|
+
<span class="mono text-xs truncate min-w-0" style="color: var(--dim)" id="toolsScope">tool calls in sessions</span>
|
|
284
|
+
</div>
|
|
257
285
|
</div>
|
|
258
286
|
<p class="mono text-xs mb-4 truncate" style="color: var(--dim)">count · tokens saved · avg rate · avg time · share of executions</p>
|
|
259
287
|
<div class="overflow-x-auto">
|
|
260
288
|
<table class="w-full mono text-[13px]" id="cmdTable">
|
|
261
|
-
<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>
|
|
289
|
+
<colgroup><col style="width:40px"><col><col style="width:120px"><col style="width:90px"><col style="width:90px"><col style="width:90px"><col style="width:90px"><col style="width:140px"></colgroup>
|
|
262
290
|
<thead>
|
|
263
291
|
<tr class="text-left text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">
|
|
264
292
|
<th class="font-normal text-right pr-3 py-2 w-10">#</th>
|
|
265
293
|
<th class="font-normal py-2 pr-3" aria-sort="none"><button class="thsort" data-sort="name">Tool / Command <span class="arr">⇅</span></button></th>
|
|
294
|
+
<th class="font-normal py-2 pr-3" aria-sort="none"><button class="thsort" data-sort="project">Project <span class="arr">⇅</span></button></th>
|
|
266
295
|
<th class="font-normal text-right py-2 pr-3" aria-sort="descending"><button class="thsort num" data-sort="count">Count <span class="arr">↓</span></button></th>
|
|
267
296
|
<th class="font-normal text-right py-2 pr-3" aria-sort="none"><button class="thsort num" data-sort="saved">Saved <span class="arr">⇅</span></button></th>
|
|
268
297
|
<th class="font-normal text-right py-2 pr-3" aria-sort="none"><button class="thsort num" data-sort="avgPct">Avg% <span class="arr">⇅</span></button></th>
|
|
@@ -294,7 +323,6 @@
|
|
|
294
323
|
<footer class="mono text-xs mt-12 pt-4" style="color: var(--dim); border-top: 1px solid var(--line)">
|
|
295
324
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-3">
|
|
296
325
|
<span>© 2026 Tersio. KurutoDenzeru. All rights reserved.</span>
|
|
297
|
-
<span class="pill flat" id="verChip">tersio</span>
|
|
298
326
|
<span class="ml-auto flex items-center gap-1" style="color: var(--ink)">
|
|
299
327
|
<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>
|
|
300
328
|
<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>
|
|
@@ -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,25 @@
|
|
|
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 are grouped by (command, project) rather than command alone: RTK's
|
|
12
|
+
// history is machine-wide, so fusing projects made one busy repo's `rtk grep`
|
|
13
|
+
// count as everyone's and let high-saving commands from other projects crowd
|
|
14
|
+
// every other project out of the table entirely.
|
|
10
15
|
import { execFileSync } from 'node:child_process';
|
|
11
16
|
import fs from 'node:fs';
|
|
12
17
|
import os from 'node:os';
|
|
13
18
|
import path from 'node:path';
|
|
14
19
|
const EMPTY = { commands: 0, saved: 0, input: 0, avgPct: 0, totalMs: 0, byCommand: [] };
|
|
20
|
+
// RTK stores the whole command line, and `rtk ls -d` with a long path list
|
|
21
|
+
// runs to tens of KB. The table shows one clipped cell, so bound the string
|
|
22
|
+
// before it crosses the sqlite3 CLI and lands in every /data.json poll.
|
|
23
|
+
const MAX_CMD_CHARS = 200;
|
|
24
|
+
// The query() reader is line- and tab-delimited, and RTK stores heredocs and
|
|
25
|
+
// `node -e` scripts verbatim (~67 rows here), so one logical row can arrive as
|
|
26
|
+
// several lines and derail every row after it. Fold whitespace into spaces
|
|
27
|
+
// first: the stored command keeps its newlines, only the read view flattens.
|
|
28
|
+
const CMD_ONE_LINE = `replace(replace(replace(rtk_cmd, char(10), ' '), char(13), ' '), char(9), ' ')`;
|
|
15
29
|
export function rtkDbPath() {
|
|
16
30
|
const override = process.env.TERSIO_RTK_DB;
|
|
17
31
|
if (override)
|
|
@@ -44,8 +58,9 @@ export function readRtkGain(limit = 10, cutoffMs) {
|
|
|
44
58
|
? `WHERE CAST(strftime('%s', timestamp) AS INTEGER) >= ${Math.floor(cutoffMs / 1000)}`
|
|
45
59
|
: '';
|
|
46
60
|
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
|
|
61
|
+
const byCommand = query(rtkDbPath(), `SELECT substr(${CMD_ONE_LINE}, 1, ${MAX_CMD_CHARS}), COALESCE(project_path, ''), COUNT(*), COALESCE(SUM(saved_tokens),0), COALESCE(AVG(savings_pct),0), COALESCE(AVG(exec_time_ms),0) FROM commands ${where} GROUP BY rtk_cmd, project_path ORDER BY SUM(saved_tokens) DESC LIMIT ${Math.max(1, Math.floor(limit))};`).map(([command, project, count, savedRow, pct, ms]) => ({
|
|
48
62
|
command,
|
|
63
|
+
project,
|
|
49
64
|
count: Number(count) || 0,
|
|
50
65
|
saved: Number(savedRow) || 0,
|
|
51
66
|
avgPct: Number(pct) || 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) {
|
|
@@ -50,6 +50,42 @@ export function appendUsage(kind, detail) {
|
|
|
50
50
|
function zeroBreakdown() {
|
|
51
51
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
52
52
|
}
|
|
53
|
+
// Message-level generation time in ms. OMP writes `duration` (ms, float);
|
|
54
|
+
// codex token_count rows carry none. Non-positive/non-finite → undefined
|
|
55
|
+
// so renderers print – instead of a fake 0.0s.
|
|
56
|
+
function durOf(v) {
|
|
57
|
+
return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
58
|
+
}
|
|
59
|
+
// Measured cost for one message. OMP writes usage.cost as
|
|
60
|
+
// { input, output, cacheRead, cacheWrite, total }; a bare number shows up in
|
|
61
|
+
// older fixtures. Anything else means "not recorded" — deliberately not 0, so
|
|
62
|
+
// the dashboard can tell a genuinely free run from an unrecorded one.
|
|
63
|
+
function costOf(usage) {
|
|
64
|
+
const c = usage.cost;
|
|
65
|
+
if (typeof c === 'number')
|
|
66
|
+
return Number.isFinite(c) ? c : undefined;
|
|
67
|
+
if (c && typeof c === 'object') {
|
|
68
|
+
const total = c.total;
|
|
69
|
+
if (typeof total === 'number' && Number.isFinite(total))
|
|
70
|
+
return total;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
// First line only: real messages carry multi-line provider errors, and this
|
|
75
|
+
// ends up in a tooltip.
|
|
76
|
+
function statusOf(msg) {
|
|
77
|
+
const stop = typeof msg.stopReason === 'string' ? msg.stopReason : '';
|
|
78
|
+
const note = typeof msg.errorMessage === 'string' && msg.errorMessage.trim()
|
|
79
|
+
? msg.errorMessage.split('\n')[0].trim().slice(0, 180)
|
|
80
|
+
: undefined;
|
|
81
|
+
if (stop === 'error' || msg.isError === true) {
|
|
82
|
+
const code = typeof msg.errorStatus === 'number' && Number.isFinite(msg.errorStatus) ? msg.errorStatus : undefined;
|
|
83
|
+
return { st: 'error', code, note };
|
|
84
|
+
}
|
|
85
|
+
if (stop === 'aborted')
|
|
86
|
+
return { st: 'aborted', note };
|
|
87
|
+
return { st: 'completed', note };
|
|
88
|
+
}
|
|
53
89
|
function addInto(into, u) {
|
|
54
90
|
for (const k of ['input', 'output', 'cacheRead', 'cacheWrite']) {
|
|
55
91
|
const v = u[k];
|
|
@@ -98,6 +134,10 @@ function walkJsonl(dir, out, cap) {
|
|
|
98
134
|
out.push(full);
|
|
99
135
|
}
|
|
100
136
|
}
|
|
137
|
+
// Recent requests are their own full-width table in the dashboard, so this is
|
|
138
|
+
// a payload bound rather than a "few highlights" bound. Rows are small
|
|
139
|
+
// (model, tokens, timing, status), so a few hundred cost little to ship.
|
|
140
|
+
const RECENT_LIMIT = 200;
|
|
101
141
|
export function importSessionTokens() {
|
|
102
142
|
const watermark = readResetWatermark();
|
|
103
143
|
const byModel = {};
|
|
@@ -118,8 +158,7 @@ export function importSessionTokens() {
|
|
|
118
158
|
let codexProvider = null;
|
|
119
159
|
const recent = [];
|
|
120
160
|
// Rows from before the reset watermark (and rows with no usable timestamp,
|
|
121
|
-
|
|
122
|
-
function ingest(model, usage, ts) {
|
|
161
|
+
function ingest(model, usage, ts, durMs, run) {
|
|
123
162
|
const ms = ts === undefined ? NaN : typeof ts === 'number' ? ts : Date.parse(ts);
|
|
124
163
|
if (watermark > 0 && (!Number.isFinite(ms) || ms < watermark))
|
|
125
164
|
return false;
|
|
@@ -128,8 +167,22 @@ export function importSessionTokens() {
|
|
|
128
167
|
addInto(byModel[model], usage);
|
|
129
168
|
byModelMessages[model] = (byModelMessages[model] ?? 0) + 1;
|
|
130
169
|
const num = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
|
|
131
|
-
if (Number.isFinite(ms))
|
|
132
|
-
|
|
170
|
+
if (Number.isFinite(ms)) {
|
|
171
|
+
const outcome = run ?? { st: 'completed' };
|
|
172
|
+
recent.push({
|
|
173
|
+
m: model,
|
|
174
|
+
i: num(usage.input),
|
|
175
|
+
o: num(usage.output),
|
|
176
|
+
t: ms,
|
|
177
|
+
d: durOf(durMs),
|
|
178
|
+
cr: num(usage.cacheRead),
|
|
179
|
+
cw: num(usage.cacheWrite),
|
|
180
|
+
usd: costOf(usage),
|
|
181
|
+
st: outcome.st,
|
|
182
|
+
code: outcome.code,
|
|
183
|
+
note: outcome.note,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
133
186
|
const day = ts !== undefined ? dayKey(ts) : null;
|
|
134
187
|
if (day) {
|
|
135
188
|
byDay[day] ??= zeroBreakdown();
|
|
@@ -179,9 +232,12 @@ export function importSessionTokens() {
|
|
|
179
232
|
if (!msg || msg.role !== 'assistant' || !msg.usage)
|
|
180
233
|
continue;
|
|
181
234
|
const model = typeof msg.model === 'string' && msg.model ? msg.model : 'unknown';
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
235
|
+
const explicitDur = msg.duration;
|
|
236
|
+
const computedDur = (typeof msg.completedAt === 'number' && typeof msg.timestamp === 'number') ? msg.completedAt - msg.timestamp : undefined;
|
|
237
|
+
const counted = ingest(model, msg.usage, row.timestamp, typeof explicitDur === 'number' ? explicitDur : computedDur, statusOf(msg));
|
|
238
|
+
const measured = costOf(msg.usage);
|
|
239
|
+
if (counted && measured !== undefined)
|
|
240
|
+
costMeasured += measured;
|
|
185
241
|
if (!counted)
|
|
186
242
|
continue;
|
|
187
243
|
for (const part of msg.content ?? []) {
|
|
@@ -195,7 +251,7 @@ export function importSessionTokens() {
|
|
|
195
251
|
}
|
|
196
252
|
}
|
|
197
253
|
recent.sort((a, b) => b.t - a.t);
|
|
198
|
-
return { messages, totals, byModel, byDay, byDayModel, byTool, byModelMessages, costMeasured, recent: recent.slice(0,
|
|
254
|
+
return { messages, totals, byModel, byDay, byDayModel, byTool, byModelMessages, costMeasured, recent: recent.slice(0, RECENT_LIMIT) };
|
|
199
255
|
}
|
|
200
256
|
// Lead binary of a shell string: first segment head past `cd` chains and
|
|
201
257
|
// VAR=x assignments (`cd /x && git status` → `git`). Falls back to `bash`.
|
|
@@ -222,9 +278,16 @@ export { co2GramsFor, energyWhFor };
|
|
|
222
278
|
export function usdCost(t, model) {
|
|
223
279
|
const { price, known } = model ? priceFor(model) : { price: DEFAULT_PRICE, known: false };
|
|
224
280
|
const m = 1 / 1_000_000;
|
|
281
|
+
const buckets = {
|
|
282
|
+
input: t.input * price.input * m,
|
|
283
|
+
output: t.output * price.output * m,
|
|
284
|
+
cacheRead: t.cacheRead * price.cacheRead * m,
|
|
285
|
+
cacheWrite: t.cacheWrite * price.cacheWrite * m,
|
|
286
|
+
};
|
|
225
287
|
return {
|
|
226
|
-
usd:
|
|
288
|
+
usd: buckets.input + buckets.output + buckets.cacheRead + buckets.cacheWrite,
|
|
227
289
|
priced: model ? known : false,
|
|
290
|
+
buckets,
|
|
228
291
|
};
|
|
229
292
|
}
|
|
230
293
|
// CO2 now model-differentiated via ./carbon.ts (EcoLogits 0.8.2 port).
|
package/package.json
CHANGED