dsh-recent-usage-metrics 0.2.2 → 0.3.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/CHANGELOG.md +25 -0
- package/README.en.md +65 -54
- package/README.md +49 -47
- package/client/client.js +907 -54
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -89,7 +89,17 @@ window.__ModuleLoader__.load({
|
|
|
89
89
|
drawerEmpty: '还没有完成的请求。发一条消息后,它的指标会出现在这里。',
|
|
90
90
|
drawerLive: '生成中',
|
|
91
91
|
drawerTools: '调用工具中',
|
|
92
|
+
drawerCompacting: '上下文压缩中',
|
|
93
|
+
drawerCompacted: '上下文压缩完成',
|
|
92
94
|
interrupted: '中断',
|
|
95
|
+
/*
|
|
96
|
+
* The compaction phase has no model of its own until it finishes: `compaction/start`
|
|
97
|
+
* records only an id and a turn, and `compaction/summary` is what reports the model. While
|
|
98
|
+
* it runs, the summarizer reuses the routed model of the last request, so that is what the
|
|
99
|
+
* row can honestly say — falling back to this general wording when even that is unknown.
|
|
100
|
+
*/
|
|
101
|
+
compactionSpeed: '压缩上下文',
|
|
102
|
+
usingChatModel: '使用对话模型',
|
|
93
103
|
colIndex: '#',
|
|
94
104
|
colTime: '时间',
|
|
95
105
|
colTtft: '首字',
|
|
@@ -102,6 +112,16 @@ window.__ModuleLoader__.load({
|
|
|
102
112
|
secondsAgo: '{count} 秒前',
|
|
103
113
|
minutesAgo: '{count} 分钟前',
|
|
104
114
|
hoursAgo: '{count} 小时前',
|
|
115
|
+
// Column-header hints. `{bolt}` / `{tools}` / `{compact}` are replaced by the glyphs
|
|
116
|
+
// themselves, which is why these are rendered by the tooltip component and not by title.
|
|
117
|
+
hintIndex: '行号:1–10 为已完成的最近记录,{bolt} 为模型输出中,{tools} 为工具调用中,{compact} 为压缩上下文中',
|
|
118
|
+
hintTime: '请求发出的时刻(本地时间)',
|
|
119
|
+
hintTtft: '从发出到收到第一个 token 的耗时',
|
|
120
|
+
hintSpeed: '模型生成速度 = 输出 token ÷ 解码时长',
|
|
121
|
+
hintTokens: '模型本次生成的 token 数',
|
|
122
|
+
hintTotal: '请求耗时:从发出请求一直到模型输出结束',
|
|
123
|
+
hintAmortized: '输出 token ÷ 总用时,包含请求时间和响应时间,更加能表明本地到模型输出完整链路的速度',
|
|
124
|
+
tokensHint: '输入 {input} · 缓存命中 {hit}',
|
|
105
125
|
}
|
|
106
126
|
|
|
107
127
|
const en = {
|
|
@@ -125,7 +145,11 @@ window.__ModuleLoader__.load({
|
|
|
125
145
|
drawerEmpty: 'No completed requests yet. Send a message and its metrics appear here.',
|
|
126
146
|
drawerLive: 'Streaming',
|
|
127
147
|
drawerTools: 'Calling tools',
|
|
148
|
+
drawerCompacting: 'Compacting context',
|
|
149
|
+
drawerCompacted: 'Context compacted',
|
|
128
150
|
interrupted: 'Interrupted',
|
|
151
|
+
compactionSpeed: 'Compaction',
|
|
152
|
+
usingChatModel: 'Chat model',
|
|
129
153
|
colIndex: '#',
|
|
130
154
|
colTime: 'At',
|
|
131
155
|
/*
|
|
@@ -148,6 +172,14 @@ window.__ModuleLoader__.load({
|
|
|
148
172
|
secondsAgo: '{count}s ago',
|
|
149
173
|
minutesAgo: '{count}m ago',
|
|
150
174
|
hoursAgo: '{count}h ago',
|
|
175
|
+
hintIndex: 'Row number: 1–10 are the most recent finished records; {bolt} is a model reply, {tools} is a tool call, {compact} is a context compaction',
|
|
176
|
+
hintTime: 'When the request was issued (local time)',
|
|
177
|
+
hintTtft: 'Time from issuing the request to receiving its first token',
|
|
178
|
+
hintSpeed: 'Generation speed = output tokens ÷ decode time',
|
|
179
|
+
hintTokens: 'Tokens this reply generated',
|
|
180
|
+
hintTotal: 'Request time: from issuing the request until the model finished replying',
|
|
181
|
+
hintAmortized: 'Output tokens ÷ total time, including both queueing and response — the throughput of the whole local-to-model path',
|
|
182
|
+
tokensHint: 'Input {input} · cache hit {hit}',
|
|
151
183
|
}
|
|
152
184
|
|
|
153
185
|
const STYLE_TEXT = `
|
|
@@ -472,6 +504,89 @@ window.__ModuleLoader__.load({
|
|
|
472
504
|
.dsh-rum-tool-icon {
|
|
473
505
|
color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary));
|
|
474
506
|
}
|
|
507
|
+
/*
|
|
508
|
+
* The compaction marker.
|
|
509
|
+
*
|
|
510
|
+
* Shares the bolt's box and pulse so switching phases never moves the pill; only the glyph
|
|
511
|
+
* inside changes. The colour is deliberately its own — the three phases must stay apart for a
|
|
512
|
+
* reader who cannot tell the shapes apart.
|
|
513
|
+
*/
|
|
514
|
+
.dsh-rum-compaction-icon {
|
|
515
|
+
color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-brand-primary));
|
|
516
|
+
}
|
|
517
|
+
/* A finished compaction keeps its glyph but stops breathing, so "done" looks settled. */
|
|
518
|
+
.dsh-rum-still {
|
|
519
|
+
animation: none;
|
|
520
|
+
}
|
|
521
|
+
/*
|
|
522
|
+
* The compaction reading replaces the five metric slots with a clock, a model, the wording and
|
|
523
|
+
* a running span.
|
|
524
|
+
*
|
|
525
|
+
* The button reserves the width the idle layout needs, measured once and refreshed on resize.
|
|
526
|
+
* Every segment inside is either fixed-width or clipped, so no value can grow the pill while
|
|
527
|
+
* it is showing — the composer below would shift on each tick otherwise.
|
|
528
|
+
*/
|
|
529
|
+
.dsh-rum-compact-anchor {
|
|
530
|
+
min-width: var(--dsh-rum-pill-width, auto);
|
|
531
|
+
}
|
|
532
|
+
.dsh-rum-compact-parts {
|
|
533
|
+
align-items: baseline;
|
|
534
|
+
gap: 8px;
|
|
535
|
+
}
|
|
536
|
+
.dsh-rum-compact-clock {
|
|
537
|
+
flex: none;
|
|
538
|
+
font-variant-numeric: tabular-nums;
|
|
539
|
+
}
|
|
540
|
+
.dsh-rum-compact-model {
|
|
541
|
+
/*
|
|
542
|
+
* The model name is the only segment whose length is unknown, so it absorbs the slack and
|
|
543
|
+
* truncates. Without the cap a long provider/model pair would push the wording out of the
|
|
544
|
+
* pill and the drawer's own edge would no longer line up with it.
|
|
545
|
+
*/
|
|
546
|
+
flex: 0 1 auto;
|
|
547
|
+
max-width: 18ch;
|
|
548
|
+
overflow: hidden;
|
|
549
|
+
text-overflow: ellipsis;
|
|
550
|
+
white-space: nowrap;
|
|
551
|
+
}
|
|
552
|
+
.dsh-rum-compact-span {
|
|
553
|
+
/* "12.3" up to "999.9": five characters plus the decimal point, reserved. */
|
|
554
|
+
width: 5.5ch;
|
|
555
|
+
}
|
|
556
|
+
/*
|
|
557
|
+
* The hover hint.
|
|
558
|
+
*
|
|
559
|
+
* Fixed on the body so no scroll container can clip it, above the anchor by default. The
|
|
560
|
+
* surface is the same menu material the drawer uses, so the hint cannot look foreign beside it.
|
|
561
|
+
*/
|
|
562
|
+
.dsh-rum-tip {
|
|
563
|
+
position: fixed;
|
|
564
|
+
z-index: 2147483100;
|
|
565
|
+
max-width: 320px;
|
|
566
|
+
padding: 7px 9px;
|
|
567
|
+
border-radius: var(--dsw-radius-sm);
|
|
568
|
+
border: 1px solid var(--dsw-alias-border-l1, transparent);
|
|
569
|
+
background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-2));
|
|
570
|
+
color: var(--dsw-alias-label-secondary);
|
|
571
|
+
font-size: 11px;
|
|
572
|
+
line-height: 1.5;
|
|
573
|
+
font-variant-numeric: tabular-nums;
|
|
574
|
+
box-shadow: var(--dsw-elevation-prominent, 0 6px 20px rgba(0, 0, 0, .28));
|
|
575
|
+
transform: translateY(-100%);
|
|
576
|
+
margin-top: -8px;
|
|
577
|
+
pointer-events: none;
|
|
578
|
+
white-space: normal;
|
|
579
|
+
}
|
|
580
|
+
/* The glyphs the index hint names, drawn inline so the hint can show them rather than spell them. */
|
|
581
|
+
.dsh-rum-tip svg {
|
|
582
|
+
width: 10px;
|
|
583
|
+
height: 10px;
|
|
584
|
+
vertical-align: -1px;
|
|
585
|
+
margin: 0 1px;
|
|
586
|
+
}
|
|
587
|
+
.dsh-rum-tip .dsh-rum-tip-icon {
|
|
588
|
+
color: var(--dsw-alias-brand-primary);
|
|
589
|
+
}
|
|
475
590
|
.dsh-rum-empty {
|
|
476
591
|
flex: 1 1 auto;
|
|
477
592
|
padding: 14px 6px 16px;
|
|
@@ -879,7 +994,27 @@ window.__ModuleLoader__.load({
|
|
|
879
994
|
*/
|
|
880
995
|
function readingOfStatsStep(entry) {
|
|
881
996
|
const stats = entry.metrics
|
|
882
|
-
|
|
997
|
+
/*
|
|
998
|
+
* A step the fold marks aborted ended without a settled reply: the user stopped it, or the
|
|
999
|
+
* upstream failed mid-request. Either way there is no `messageTime`, so `model`, `decode`
|
|
1000
|
+
* and `tps` are all null and the row would be entirely blank.
|
|
1001
|
+
*/
|
|
1002
|
+
const interrupted = stats.aborted === true || stats.interrupted === true
|
|
1003
|
+
const wallMs = numberOrNull(stats.wall)
|
|
1004
|
+
/*
|
|
1005
|
+
* The first-token column of an interrupted request.
|
|
1006
|
+
*
|
|
1007
|
+
* The live reading counts up from the moment the request was issued, so a stopped request
|
|
1008
|
+
* did have a first-token figure on screen — it simply never landed in the log as `ttft`,
|
|
1009
|
+
* because the fold only writes that once a first token actually arrived. Two cases:
|
|
1010
|
+
*
|
|
1011
|
+
* - No first token ever came: the counter was still running when the request stopped, so
|
|
1012
|
+
* the figure it showed was the elapsed span. `wall` is that span, and reporting it keeps
|
|
1013
|
+
* the column honest instead of printing a dash for a request that demonstrably waited.
|
|
1014
|
+
* - A first token did arrive: `ttft` is set and frozen, so the row keeps the real value,
|
|
1015
|
+
* which is smaller than the total — exactly the ordering the user expects to see.
|
|
1016
|
+
*/
|
|
1017
|
+
const ttftMs = numberOrNull(stats.ttft) ?? (interrupted ? wallMs : null)
|
|
883
1018
|
/*
|
|
884
1019
|
* The drawer's "total" is the Statistics view's MODEL time, not its wall time.
|
|
885
1020
|
*
|
|
@@ -888,17 +1023,15 @@ window.__ModuleLoader__.load({
|
|
|
888
1023
|
* The drawer answers "how long did this request take to answer", so it reports `model` —
|
|
889
1024
|
* and that figure is final the moment the reply lands, which is why it must not climb
|
|
890
1025
|
* while tools run.
|
|
1026
|
+
*
|
|
1027
|
+
* An interrupted step has no `model` at all, so it falls back to `wall`. That is the
|
|
1028
|
+
* figure the user actually wants there: how long the request ran before it was stopped.
|
|
1029
|
+
* It is a log fact — `endTime - startTime` — so it survives a reload unchanged, unlike
|
|
1030
|
+
* the live reading, which is recomputed and lost.
|
|
891
1031
|
*/
|
|
892
|
-
const totalMs = numberOrNull(stats.model)
|
|
1032
|
+
const totalMs = numberOrNull(stats.model) ?? (interrupted ? wallMs : null)
|
|
893
1033
|
const tokens = numberOrNull(stats.output)
|
|
894
1034
|
const speed = numberOrNull(stats.tps)
|
|
895
|
-
/*
|
|
896
|
-
* A step the fold marks interrupted or aborted never produced a settled reply: a
|
|
897
|
-
* cancelled request has no first token to subtract from, so its ttft is null and the
|
|
898
|
-
* drawer used to print the same dash a not-yet-started step would. The flag lets the
|
|
899
|
-
* cell say what actually happened instead.
|
|
900
|
-
*/
|
|
901
|
-
const interrupted = stats.interrupted === true || stats.aborted === true
|
|
902
1035
|
return {
|
|
903
1036
|
key: `${entry.turn}:${entry.step}`,
|
|
904
1037
|
at: numberOrNull(stats.startTime) ?? numberOrNull(stats.endTime),
|
|
@@ -918,6 +1051,16 @@ window.__ModuleLoader__.load({
|
|
|
918
1051
|
tokens !== null && tokens > 0 && totalMs !== null && totalMs > 0
|
|
919
1052
|
? tokens / (totalMs / 1000)
|
|
920
1053
|
: null,
|
|
1054
|
+
/*
|
|
1055
|
+
* The model that answered, and the input-side accounting, all for tooltips. The fold
|
|
1056
|
+
* does not put a provider/model on a step — it only carries them for a compaction — so
|
|
1057
|
+
* the caller fills these from the chat snapshot's own assistant nodes.
|
|
1058
|
+
*/
|
|
1059
|
+
model: null,
|
|
1060
|
+
provider: null,
|
|
1061
|
+
context: numberOrNull(stats.context),
|
|
1062
|
+
cacheRead: numberOrNull(stats.cacheRead),
|
|
1063
|
+
cacheHit: numberOrNull(stats.cacheHit),
|
|
921
1064
|
}
|
|
922
1065
|
}
|
|
923
1066
|
|
|
@@ -951,6 +1094,160 @@ window.__ModuleLoader__.load({
|
|
|
951
1094
|
)
|
|
952
1095
|
}
|
|
953
1096
|
|
|
1097
|
+
/**
|
|
1098
|
+
* A compaction transaction as a drawer row.
|
|
1099
|
+
*
|
|
1100
|
+
* Compaction is not a step: it lives on `turn.compactions[]`, opened by `compaction/start`
|
|
1101
|
+
* and closed by `compaction/end`, and it never produces an `assistant/message`. So it has
|
|
1102
|
+
* no first token, no decode span and no generation rate — the three columns that depend on
|
|
1103
|
+
* a settled reply stay empty, and the speed column says what is happening instead.
|
|
1104
|
+
*
|
|
1105
|
+
* What it does have is a wall-clock span, which is the figure the user wants: how long the
|
|
1106
|
+
* summarization ran. While it runs, `endTime` is null and the drawer counts up from
|
|
1107
|
+
* `startTime`; once it lands, `wall` freezes the same span.
|
|
1108
|
+
*/
|
|
1109
|
+
function readingOfCompaction(compaction) {
|
|
1110
|
+
const startMs = numberOrNull(compaction.startTime)
|
|
1111
|
+
const wallMs = numberOrNull(compaction.wall)
|
|
1112
|
+
const running = compaction.running === true
|
|
1113
|
+
const tokens = numberOrNull(compaction.output)
|
|
1114
|
+
return {
|
|
1115
|
+
key: `compaction:${compaction.compactionId}`,
|
|
1116
|
+
compaction: true,
|
|
1117
|
+
compactionId: compaction.compactionId,
|
|
1118
|
+
running,
|
|
1119
|
+
at: startMs,
|
|
1120
|
+
startMs,
|
|
1121
|
+
// A compaction has no first token, so the column is always the dash.
|
|
1122
|
+
ttftMs: null,
|
|
1123
|
+
interrupted: false,
|
|
1124
|
+
totalMs: running ? null : wallMs,
|
|
1125
|
+
tokens,
|
|
1126
|
+
estimated: false,
|
|
1127
|
+
speed: null,
|
|
1128
|
+
/*
|
|
1129
|
+
* The amortised figure a compaction can report is `output ÷ wall` — the throughput of
|
|
1130
|
+
* the whole summarization call, which includes replaying the conversation prefix. It
|
|
1131
|
+
* is not the decode rate an ordinary row reports, but it is a real measurement of this
|
|
1132
|
+
* transaction, so it is shown as-is rather than hidden behind the estimate marker.
|
|
1133
|
+
*/
|
|
1134
|
+
amortized:
|
|
1135
|
+
!running && tokens !== null && tokens > 0 && wallMs !== null && wallMs > 0
|
|
1136
|
+
? tokens / (wallMs / 1000)
|
|
1137
|
+
: null,
|
|
1138
|
+
/** Which model summarised. Only `compaction/summary` reports it, so it lands at the end. */
|
|
1139
|
+
model: typeof compaction.model === 'string' && compaction.model !== '' ? compaction.model : null,
|
|
1140
|
+
provider:
|
|
1141
|
+
typeof compaction.provider === 'string' && compaction.provider !== '' ? compaction.provider : null,
|
|
1142
|
+
/** Input-side accounting, for the tokens column's tooltip. */
|
|
1143
|
+
context: numberOrNull(compaction.context),
|
|
1144
|
+
cacheRead: numberOrNull(compaction.cacheRead),
|
|
1145
|
+
cacheHit: numberOrNull(compaction.cacheHit),
|
|
1146
|
+
shadowedTokens: numberOrNull(compaction.shadowedTokens),
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Every compaction the session fold knows about, oldest first.
|
|
1152
|
+
*
|
|
1153
|
+
* They are read separately from the steps because they are not steps: `historyFromStats`
|
|
1154
|
+
* merges the two streams and orders them by start time, so a compaction lands in the
|
|
1155
|
+
* drawer exactly where it happened in the transcript.
|
|
1156
|
+
*/
|
|
1157
|
+
function compactionsFromStats(statsSnapshot) {
|
|
1158
|
+
const turns = statsSnapshot?.turns
|
|
1159
|
+
if (!Array.isArray(turns)) return []
|
|
1160
|
+
const rows = []
|
|
1161
|
+
for (const turn of turns) {
|
|
1162
|
+
if (!Array.isArray(turn.compactions)) continue
|
|
1163
|
+
for (const compaction of turn.compactions) {
|
|
1164
|
+
if (typeof compaction?.compactionId !== 'string') continue
|
|
1165
|
+
if (numberOrNull(compaction.startTime) === null) continue
|
|
1166
|
+
rows.push(readingOfCompaction(compaction))
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return rows
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Which model answered each step, keyed `turn:step`.
|
|
1174
|
+
*
|
|
1175
|
+
* The fold does not put a provider/model on a step — `stepMetrics` carries only derived
|
|
1176
|
+
* figures, and an assistant chat node carries only its usage and timings. The model is
|
|
1177
|
+
* recorded once per routed request as a `request/header` event row on the turn, so a step
|
|
1178
|
+
* is attributed to the newest header that precedes it.
|
|
1179
|
+
*
|
|
1180
|
+
* Verified against the log: on a real session the newest preceding header matched the
|
|
1181
|
+
* `assistant/message.source` of every one of 40 sampled steps. Headers are per-routed-
|
|
1182
|
+
* request, so several consecutive steps share one, and a model switch starts a new one.
|
|
1183
|
+
*/
|
|
1184
|
+
function modelIndexFromStats(statsSnapshot) {
|
|
1185
|
+
const turns = statsSnapshot?.turns
|
|
1186
|
+
const index = new Map()
|
|
1187
|
+
if (!Array.isArray(turns)) return index
|
|
1188
|
+
for (const turn of turns) {
|
|
1189
|
+
if (!Array.isArray(turn.steps)) continue
|
|
1190
|
+
const headers = []
|
|
1191
|
+
if (Array.isArray(turn.events)) {
|
|
1192
|
+
for (const event of turn.events) {
|
|
1193
|
+
if (event?.kind !== 'request-header') continue
|
|
1194
|
+
if (typeof event.model !== 'string' && typeof event.provider !== 'string') continue
|
|
1195
|
+
headers.push({ time: numberOrNull(event.time) ?? 0, provider: event.provider ?? null, model: event.model ?? null })
|
|
1196
|
+
}
|
|
1197
|
+
headers.sort((left, right) => left.time - right.time)
|
|
1198
|
+
}
|
|
1199
|
+
if (headers.length === 0) continue
|
|
1200
|
+
for (const step of turn.steps) {
|
|
1201
|
+
const at = numberOrNull(step.startTime)
|
|
1202
|
+
if (at === null || typeof step.step !== 'number') continue
|
|
1203
|
+
let chosen = null
|
|
1204
|
+
for (const header of headers) {
|
|
1205
|
+
if (header.time <= at) chosen = header
|
|
1206
|
+
else break
|
|
1207
|
+
}
|
|
1208
|
+
if (chosen === null) continue
|
|
1209
|
+
index.set(`${turn.turn}:${step.step}`, chosen)
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return index
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Whether any step started after this compaction finished.
|
|
1217
|
+
*
|
|
1218
|
+
* That is what ends the "just compacted" wording: the fold keeps a compaction record for
|
|
1219
|
+
* the life of the session, so its presence alone can never mean "this just happened".
|
|
1220
|
+
* A step whose `startTime` is later than the compaction's end is the next request.
|
|
1221
|
+
*/
|
|
1222
|
+
function compactionHadSuccessor(compaction, statsSnapshot) {
|
|
1223
|
+
const turns = statsSnapshot?.turns
|
|
1224
|
+
if (!Array.isArray(turns)) return false
|
|
1225
|
+
const endedAt = numberOrNull(compaction.totalMs) === null ? null : compaction.at + compaction.totalMs
|
|
1226
|
+
if (endedAt === null) return false
|
|
1227
|
+
for (const turn of turns) {
|
|
1228
|
+
if (!Array.isArray(turn.steps)) continue
|
|
1229
|
+
for (const step of turn.steps) {
|
|
1230
|
+
const start = numberOrNull(step.startTime)
|
|
1231
|
+
if (start !== null && start > endedAt) return true
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return false
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* The newest compaction in a snapshot, or null.
|
|
1239
|
+
*
|
|
1240
|
+
* The pill and the live row both need one record, not the list, so this picks by start
|
|
1241
|
+
* time: a session can compact several times, and only the latest is "what is happening".
|
|
1242
|
+
*/
|
|
1243
|
+
function compactionsOf(statsSnapshot) {
|
|
1244
|
+
const all = compactionsFromStats(statsSnapshot)
|
|
1245
|
+
if (all.length === 0) return null
|
|
1246
|
+
let newest = all[0]
|
|
1247
|
+
for (const row of all) if ((row.at ?? 0) > (newest.at ?? 0)) newest = row
|
|
1248
|
+
return newest
|
|
1249
|
+
}
|
|
1250
|
+
|
|
954
1251
|
/**
|
|
955
1252
|
* The newest readings for a session, newest first.
|
|
956
1253
|
*
|
|
@@ -970,14 +1267,30 @@ window.__ModuleLoader__.load({
|
|
|
970
1267
|
*/
|
|
971
1268
|
function historyFromStats(statsSnapshot) {
|
|
972
1269
|
const steps = statsSteps(statsSnapshot)
|
|
1270
|
+
const models = modelIndexFromStats(statsSnapshot)
|
|
973
1271
|
const rows = []
|
|
974
1272
|
for (const entry of steps) {
|
|
975
1273
|
// A step only has a row once it has started; `step/start` is what supplies this.
|
|
976
1274
|
if (numberOrNull(entry.metrics.startTime) === null) continue
|
|
977
1275
|
if (!statsStepHasFigures(entry.metrics)) continue
|
|
978
|
-
|
|
1276
|
+
const row = readingOfStatsStep(entry)
|
|
1277
|
+
// Attribute the request to the model that served it, for the speed column's tooltip.
|
|
1278
|
+
const owner = models.get(row.key)
|
|
1279
|
+
if (owner !== undefined) {
|
|
1280
|
+
row.model = owner.model
|
|
1281
|
+
row.provider = owner.provider
|
|
1282
|
+
}
|
|
1283
|
+
rows.push(row)
|
|
979
1284
|
}
|
|
980
|
-
|
|
1285
|
+
// Compactions are not steps, so they are collected separately and then ordered together.
|
|
1286
|
+
for (const row of compactionsFromStats(statsSnapshot)) rows.push(row)
|
|
1287
|
+
/*
|
|
1288
|
+
* Newest first, by start time. The two streams interleave in real time — a turn can run
|
|
1289
|
+
* several steps, compact, then run more — so ordering them by the moment each began is
|
|
1290
|
+
* what puts the compaction where it actually happened rather than at either end.
|
|
1291
|
+
*/
|
|
1292
|
+
rows.sort((left, right) => (right.at ?? 0) - (left.at ?? 0))
|
|
1293
|
+
return rows
|
|
981
1294
|
}
|
|
982
1295
|
|
|
983
1296
|
function readJson(key) {
|
|
@@ -1299,6 +1612,230 @@ window.__ModuleLoader__.load({
|
|
|
1299
1612
|
)
|
|
1300
1613
|
}
|
|
1301
1614
|
|
|
1615
|
+
/**
|
|
1616
|
+
* The compaction marker: three stacked bars, each shorter than the one above.
|
|
1617
|
+
*
|
|
1618
|
+
* Chosen over the converging-chevron and arrow-pair candidates because at 14px the three
|
|
1619
|
+
* bars keep visible gaps between them, while the pointed shapes blurred into a single
|
|
1620
|
+
* blob. It also carries the meaning directly — a long history reduced to a short one.
|
|
1621
|
+
*
|
|
1622
|
+
* Bar widths and gaps mirror the bolt's vertical extent so the three glyphs (bolt, T,
|
|
1623
|
+
* compactor) share one optical size inside the same 14x14 box.
|
|
1624
|
+
*/
|
|
1625
|
+
function CompactionIcon(props) {
|
|
1626
|
+
return h(
|
|
1627
|
+
'svg',
|
|
1628
|
+
{ viewBox: '0 0 16 16', className: props.className, 'aria-hidden': true, fill: 'currentColor' },
|
|
1629
|
+
h('rect', { x: 2.8, y: 3.1, width: 10.4, height: 2, rx: 1 }),
|
|
1630
|
+
h('rect', { x: 4.6, y: 7, width: 6.8, height: 2, rx: 1 }),
|
|
1631
|
+
h('rect', { x: 6.5, y: 10.9, width: 3, height: 2, rx: 1 }),
|
|
1632
|
+
)
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* The full timestamp, for the clock column's hint.
|
|
1637
|
+
*
|
|
1638
|
+
* `MM-DD HH:mm:ss.SSS` — the clock cell itself only has room for the time of day, and the
|
|
1639
|
+
* relative wording it replaced ("3 minutes ago") cannot answer which request a row is.
|
|
1640
|
+
*/
|
|
1641
|
+
function absoluteStamp(at) {
|
|
1642
|
+
const ms = numberOrNull(at)
|
|
1643
|
+
if (ms === null) return ''
|
|
1644
|
+
const date = new Date(ms)
|
|
1645
|
+
const pad = (value, width) => String(value).padStart(width, '0')
|
|
1646
|
+
return (
|
|
1647
|
+
`${pad(date.getMonth() + 1, 2)}-${pad(date.getDate(), 2)} ` +
|
|
1648
|
+
`${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}:${pad(date.getSeconds(), 2)}.` +
|
|
1649
|
+
`${pad(date.getMilliseconds(), 3)}`
|
|
1650
|
+
)
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/** The speed column's hint: which model served this row. */
|
|
1654
|
+
function modelHintText(row, t) {
|
|
1655
|
+
if (row.compaction === true) return compactionModelName(row, t)
|
|
1656
|
+
const model = typeof row.model === 'string' && row.model !== '' ? row.model : null
|
|
1657
|
+
const provider = typeof row.provider === 'string' && row.provider !== '' ? row.provider : null
|
|
1658
|
+
if (model === null) return t('usingChatModel')
|
|
1659
|
+
return provider === null ? model : `${provider} / ${model}`
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/** The tokens column's hint: the input side, which the cell itself does not show. */
|
|
1663
|
+
function tokensHintText(row, t) {
|
|
1664
|
+
const none = t('none')
|
|
1665
|
+
const input = row.context === null || row.context === undefined ? none : tokensText(row.context, false, t)
|
|
1666
|
+
const hit =
|
|
1667
|
+
row.cacheHit === null || row.cacheHit === undefined
|
|
1668
|
+
? none
|
|
1669
|
+
: `${(row.cacheHit * 100).toFixed(1)}%`
|
|
1670
|
+
return t('tokensHint', { input, hit })
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/**
|
|
1674
|
+
* The index header's hint, with the three phase glyphs drawn in place.
|
|
1675
|
+
*
|
|
1676
|
+
* This is the reason the drawer owns a tooltip component at all: `title` can only carry
|
|
1677
|
+
* plain text, and the hint has to name the bolt, the T and the compaction bars. The glyphs
|
|
1678
|
+
* are rendered as elements and substituted into the sentence, so the wording and the
|
|
1679
|
+
* drawer's own markers can never drift apart.
|
|
1680
|
+
*/
|
|
1681
|
+
function indexHint(t) {
|
|
1682
|
+
const glyph = (Icon) => h(Icon, { className: 'dsh-rum-tip-icon' })
|
|
1683
|
+
/*
|
|
1684
|
+
* Split on every placeholder first, then walk the pieces: a string piece is literal text,
|
|
1685
|
+
* a placeholder becomes its glyph. Walking once over a token list is simpler to follow
|
|
1686
|
+
* than nesting one split per placeholder.
|
|
1687
|
+
*/
|
|
1688
|
+
const tokens = t('hintIndex').split(/(\{bolt\}|\{tools\}|\{compact\})/)
|
|
1689
|
+
const nodes = []
|
|
1690
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
1691
|
+
const token = tokens[i]
|
|
1692
|
+
if (token === '') continue
|
|
1693
|
+
if (token === '{bolt}') nodes.push(glyph(BoltIcon))
|
|
1694
|
+
else if (token === '{tools}') nodes.push(glyph(ToolIcon))
|
|
1695
|
+
else if (token === '{compact}') nodes.push(glyph(CompactionIcon))
|
|
1696
|
+
else nodes.push(token)
|
|
1697
|
+
}
|
|
1698
|
+
return h('span', null, ...nodes)
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* A hover hint, rendered as real DOM rather than through `title`.
|
|
1703
|
+
*
|
|
1704
|
+
* `title` can only show plain text, and the `#` header's hint names the three state glyphs
|
|
1705
|
+
* — it has to draw them. Owning the tooltip also keeps one visual language across the
|
|
1706
|
+
* drawer, lets it follow the theme's own surfaces, and avoids the browser's one-second
|
|
1707
|
+
* delay.
|
|
1708
|
+
*
|
|
1709
|
+
* It is positioned `fixed` on the body so no scroll container can clip it, and flipped
|
|
1710
|
+
* below the anchor when there is no room above.
|
|
1711
|
+
*/
|
|
1712
|
+
function Tooltip(props) {
|
|
1713
|
+
const { open, x, y, children } = props
|
|
1714
|
+
if (!open) return null
|
|
1715
|
+
return portal(
|
|
1716
|
+
h(
|
|
1717
|
+
'div',
|
|
1718
|
+
{
|
|
1719
|
+
className: 'dsh-rum-tip',
|
|
1720
|
+
role: 'tooltip',
|
|
1721
|
+
style: { left: `${x}px`, top: `${y}px` },
|
|
1722
|
+
},
|
|
1723
|
+
children,
|
|
1724
|
+
),
|
|
1725
|
+
)
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* Hover state for one row or one header.
|
|
1730
|
+
*
|
|
1731
|
+
* A short delay keeps a hint from flashing while the pointer sweeps across the drawer,
|
|
1732
|
+
* and a single shared timer is enough because only one hint can be open at a time.
|
|
1733
|
+
*/
|
|
1734
|
+
function useHint(delay) {
|
|
1735
|
+
const [hint, setHint] = useState(null)
|
|
1736
|
+
const timer = useRef(null)
|
|
1737
|
+
const cancel = useCallback(() => {
|
|
1738
|
+
if (timer.current !== null) {
|
|
1739
|
+
clearTimeout(timer.current)
|
|
1740
|
+
timer.current = null
|
|
1741
|
+
}
|
|
1742
|
+
}, [])
|
|
1743
|
+
const show = useCallback(
|
|
1744
|
+
(content, event) => {
|
|
1745
|
+
cancel()
|
|
1746
|
+
const rect = event?.currentTarget?.getBoundingClientRect?.()
|
|
1747
|
+
if (rect === undefined) return
|
|
1748
|
+
timer.current = setTimeout(() => {
|
|
1749
|
+
timer.current = null
|
|
1750
|
+
setHint({ content, x: rect.left, y: rect.top })
|
|
1751
|
+
}, delay)
|
|
1752
|
+
},
|
|
1753
|
+
[cancel, delay],
|
|
1754
|
+
)
|
|
1755
|
+
const hide = useCallback(() => {
|
|
1756
|
+
cancel()
|
|
1757
|
+
setHint(null)
|
|
1758
|
+
}, [cancel])
|
|
1759
|
+
useEffect(() => cancel, [cancel])
|
|
1760
|
+
return { hint, show, hide }
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* The compaction wording as one sentence, for the button's accessible name.
|
|
1765
|
+
*/
|
|
1766
|
+
function compactionSentence(compaction, phase, now, t) {
|
|
1767
|
+
const elapsed = compactionSpan(compaction, now)
|
|
1768
|
+
const parts = [
|
|
1769
|
+
clockText(compaction.at),
|
|
1770
|
+
compactionModelName(compaction, t),
|
|
1771
|
+
phase === 'compacting' ? t('drawerCompacting') : t('drawerCompacted'),
|
|
1772
|
+
secondsText(elapsed, t),
|
|
1773
|
+
]
|
|
1774
|
+
return parts.join(' · ')
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
/**
|
|
1778
|
+
* How long a compaction has run: live while it runs, frozen once it lands.
|
|
1779
|
+
*
|
|
1780
|
+
* The live figure counts from `startTime`, which is the same origin the drawer row uses,
|
|
1781
|
+
* so the pill and the row can never disagree. Once the fold reports `wall`, that log value
|
|
1782
|
+
* takes over so a reload shows the same span it showed live.
|
|
1783
|
+
*/
|
|
1784
|
+
function compactionSpan(compaction, now) {
|
|
1785
|
+
if (compaction.running === true) {
|
|
1786
|
+
if (compaction.startMs === null) return null
|
|
1787
|
+
return Math.max(0, now - compaction.startMs)
|
|
1788
|
+
}
|
|
1789
|
+
return compaction.totalMs
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/**
|
|
1793
|
+
* The model a compaction used, or the wording that stands in for it.
|
|
1794
|
+
*
|
|
1795
|
+
* `compaction/summary` reports the real model, so a finished compaction always knows it.
|
|
1796
|
+
* While one runs the fold has only an id and a turn — the summarizer's own request is not
|
|
1797
|
+
* logged as a header — so the routed model of the last request is the honest reading, and
|
|
1798
|
+
* the general wording is the last resort.
|
|
1799
|
+
*/
|
|
1800
|
+
function compactionModelName(compaction, t) {
|
|
1801
|
+
if (typeof compaction.model === 'string' && compaction.model !== '') return compaction.model
|
|
1802
|
+
return t('usingChatModel')
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* The compaction variant of the status reading.
|
|
1807
|
+
*
|
|
1808
|
+
* Three segments replace the five metric slots: the clock the compaction began at, the
|
|
1809
|
+
* model doing it, and how long it has been running. The pill's own width is left to
|
|
1810
|
+
* `--dsh-rum-pill-width`, measured from the idle layout, so switching into this phase
|
|
1811
|
+
* cannot move anything beside it.
|
|
1812
|
+
*/
|
|
1813
|
+
function CompactionReading(props) {
|
|
1814
|
+
const { compaction, phase, now, t } = props
|
|
1815
|
+
const span = compactionSpan(compaction, now)
|
|
1816
|
+
return h(
|
|
1817
|
+
'span',
|
|
1818
|
+
{ className: 'dsh-rum-parts dsh-rum-compact-parts' },
|
|
1819
|
+
h('span', { className: 'dsh-rum-part dsh-rum-compact-clock' }, clockText(compaction.at)),
|
|
1820
|
+
h(
|
|
1821
|
+
'span',
|
|
1822
|
+
{ className: 'dsh-rum-part dsh-rum-compact-model', title: compactionModelName(compaction, t) },
|
|
1823
|
+
compactionModelName(compaction, t),
|
|
1824
|
+
),
|
|
1825
|
+
h(
|
|
1826
|
+
'span',
|
|
1827
|
+
{ className: 'dsh-rum-part' },
|
|
1828
|
+
phase === 'compacting' ? t('drawerCompacting') : t('drawerCompacted'),
|
|
1829
|
+
),
|
|
1830
|
+
h(
|
|
1831
|
+
'span',
|
|
1832
|
+
{ className: 'dsh-rum-part' },
|
|
1833
|
+
h('span', { className: 'dsh-rum-num dsh-rum-compact-span' }, durationSlot(span)),
|
|
1834
|
+
h('span', { className: 'dsh-rum-unit' }, DURATION_UNIT),
|
|
1835
|
+
),
|
|
1836
|
+
)
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1302
1839
|
function ChevronIcon(props) {
|
|
1303
1840
|
return h(
|
|
1304
1841
|
'svg',
|
|
@@ -1598,7 +2135,15 @@ window.__ModuleLoader__.load({
|
|
|
1598
2135
|
return null
|
|
1599
2136
|
}
|
|
1600
2137
|
|
|
1601
|
-
function useLiveReading(
|
|
2138
|
+
function useLiveReading(
|
|
2139
|
+
useChat,
|
|
2140
|
+
enabled,
|
|
2141
|
+
trajectory,
|
|
2142
|
+
runningStatsStep,
|
|
2143
|
+
sessionTurnOpen,
|
|
2144
|
+
sessionRunning,
|
|
2145
|
+
sessionStats,
|
|
2146
|
+
) {
|
|
1602
2147
|
const partial = useChat((snapshot) => snapshot.legacy.partial)
|
|
1603
2148
|
const nodes = useChat((snapshot) => snapshot.legacy.nodes)
|
|
1604
2149
|
const turnTimings = useChat((snapshot) => snapshot.legacy.turnTimings)
|
|
@@ -1664,7 +2209,38 @@ window.__ModuleLoader__.load({
|
|
|
1664
2209
|
* A closed turn admits no live step at all, so a step still flagged running in a stale
|
|
1665
2210
|
* snapshot cannot revive either marker.
|
|
1666
2211
|
*/
|
|
2212
|
+
/*
|
|
2213
|
+
* The newest compaction in the session, running or just finished.
|
|
2214
|
+
*
|
|
2215
|
+
* Compaction is a fourth kind of activity that overlaps the others: it runs between
|
|
2216
|
+
* steps, belongs to no step, and can take tens of seconds. Without it the pill simply
|
|
2217
|
+
* froze on the previous figures for the whole summarization, which is exactly the
|
|
2218
|
+
* "I cannot tell what it is doing" complaint.
|
|
2219
|
+
*/
|
|
2220
|
+
const compactionNow = compactionsOf(sessionStats)
|
|
2221
|
+
const liveCompaction = compactionNow?.running === true ? compactionNow : null
|
|
2222
|
+
/*
|
|
2223
|
+
* A compaction that finished and has not yet been followed by a new request.
|
|
2224
|
+
*
|
|
2225
|
+
* The pill keeps saying "compacted" until the next request starts, so the user can see
|
|
2226
|
+
* that the long pause was the summarization finishing rather than the app stalling. A
|
|
2227
|
+
* step that began after the compaction ended is what ends it — the fold keeps the
|
|
2228
|
+
* compaction record forever, so its mere presence cannot mean "just happened".
|
|
2229
|
+
*/
|
|
2230
|
+
const recentCompaction =
|
|
2231
|
+
liveCompaction !== null
|
|
2232
|
+
? null
|
|
2233
|
+
: compactionNow !== null && !compactionHadSuccessor(compactionNow, sessionStats)
|
|
2234
|
+
? compactionNow
|
|
2235
|
+
: null
|
|
1667
2236
|
const livePhase = (() => {
|
|
2237
|
+
/*
|
|
2238
|
+
* Compaction wins over the step phases: it happens between steps, so a running
|
|
2239
|
+
* compaction and a running step cannot both be true, and checking it first keeps the
|
|
2240
|
+
* "compacted, waiting for the next request" state from being mistaken for idle.
|
|
2241
|
+
*/
|
|
2242
|
+
if (liveCompaction !== null) return 'compacting'
|
|
2243
|
+
if (recentCompaction !== null) return 'compacted'
|
|
1668
2244
|
if (!turnIsOpen) return null
|
|
1669
2245
|
if (runningStatsStep !== null && runningStatsStep !== undefined) {
|
|
1670
2246
|
const metrics = runningStatsStep.metrics
|
|
@@ -1677,7 +2253,9 @@ window.__ModuleLoader__.load({
|
|
|
1677
2253
|
if (partial !== null && partial !== undefined) return 'writing'
|
|
1678
2254
|
return null
|
|
1679
2255
|
})()
|
|
1680
|
-
|
|
2256
|
+
/** The compaction a render should show, whichever of the two phases is active. */
|
|
2257
|
+
const activeCompaction = liveCompaction ?? recentCompaction
|
|
2258
|
+
const liveStep = livePhase === null || activeCompaction !== null
|
|
1681
2259
|
? null
|
|
1682
2260
|
: runningStatsStep !== null && runningStatsStep !== undefined
|
|
1683
2261
|
? { turn: runningStatsStep.turn, step: runningStatsStep.step }
|
|
@@ -1686,7 +2264,11 @@ window.__ModuleLoader__.load({
|
|
|
1686
2264
|
? null
|
|
1687
2265
|
: { turn: partial.turn, step: partial.step }
|
|
1688
2266
|
: { turn: request.turn, step: request.step }
|
|
1689
|
-
|
|
2267
|
+
/*
|
|
2268
|
+
* A compaction is live too, even though it has no step: the clock has to tick for its
|
|
2269
|
+
* running span, and the early returns below gate on this flag.
|
|
2270
|
+
*/
|
|
2271
|
+
const running = enabled && (liveStep !== null || activeCompaction !== null)
|
|
1690
2272
|
const [now, setNow] = useState(() => Date.now())
|
|
1691
2273
|
const observation = useRef({ key: null, firstTokenAt: null })
|
|
1692
2274
|
|
|
@@ -1703,7 +2285,11 @@ window.__ModuleLoader__.load({
|
|
|
1703
2285
|
* content, so that appearance IS the first delta.
|
|
1704
2286
|
*/
|
|
1705
2287
|
useEffect(() => {
|
|
1706
|
-
|
|
2288
|
+
/*
|
|
2289
|
+
* A compaction has no step, so there is no first-token mark to find: the guard covers
|
|
2290
|
+
* the compacting phase, where `running` is true but `liveStep` is null by design.
|
|
2291
|
+
*/
|
|
2292
|
+
if (!running || liveStep === null) {
|
|
1707
2293
|
observation.current = { key: null, firstTokenAt: null }
|
|
1708
2294
|
return
|
|
1709
2295
|
}
|
|
@@ -1740,6 +2326,27 @@ window.__ModuleLoader__.load({
|
|
|
1740
2326
|
* lag the fold.
|
|
1741
2327
|
* 3. Reconstruction from the previous step's last tool result, then the turn's start.
|
|
1742
2328
|
*/
|
|
2329
|
+
/*
|
|
2330
|
+
* A compaction has no step to measure, so the reading is built from the compaction alone:
|
|
2331
|
+
* its own clock, its own span and its own model. Returning here keeps every step-shaped
|
|
2332
|
+
* figure below out of the picture — there is no step to attribute them to.
|
|
2333
|
+
*/
|
|
2334
|
+
if (activeCompaction !== null) {
|
|
2335
|
+
return {
|
|
2336
|
+
key: null,
|
|
2337
|
+
at: activeCompaction.at,
|
|
2338
|
+
startMs: activeCompaction.startMs,
|
|
2339
|
+
ttftMs: null,
|
|
2340
|
+
totalMs: compactionSpan(activeCompaction, now),
|
|
2341
|
+
tokens: activeCompaction.tokens,
|
|
2342
|
+
estimated: false,
|
|
2343
|
+
speed: null,
|
|
2344
|
+
amortized: activeCompaction.amortized,
|
|
2345
|
+
replyLanded: false,
|
|
2346
|
+
phase: livePhase,
|
|
2347
|
+
compaction: activeCompaction,
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
1743
2350
|
const statsStart =
|
|
1744
2351
|
runningStatsStep !== null &&
|
|
1745
2352
|
runningStatsStep !== undefined &&
|
|
@@ -1824,6 +2431,7 @@ window.__ModuleLoader__.load({
|
|
|
1824
2431
|
: null,
|
|
1825
2432
|
replyLanded: true,
|
|
1826
2433
|
phase: livePhase,
|
|
2434
|
+
compaction: activeCompaction,
|
|
1827
2435
|
}
|
|
1828
2436
|
}
|
|
1829
2437
|
|
|
@@ -1849,6 +2457,7 @@ window.__ModuleLoader__.load({
|
|
|
1849
2457
|
amortized: tokens > 0 && elapsedMs !== null && elapsedMs > 0 ? tokens / (elapsedMs / 1000) : null,
|
|
1850
2458
|
replyLanded: false,
|
|
1851
2459
|
phase: livePhase,
|
|
2460
|
+
compaction: activeCompaction,
|
|
1852
2461
|
}
|
|
1853
2462
|
}
|
|
1854
2463
|
|
|
@@ -2020,13 +2629,19 @@ window.__ModuleLoader__.load({
|
|
|
2020
2629
|
* Average of the rows on screen, computed over the ones that actually carry each
|
|
2021
2630
|
* figure (an interrupted request may have no TTFT, a placeholder no tokens). Returns
|
|
2022
2631
|
* null for a column with no samples so the row can print a dash instead of a fake 0.
|
|
2632
|
+
*
|
|
2633
|
+
* Interrupted rows are excluded from every mean. Their total is wall-clock time — it
|
|
2634
|
+
* includes however long the request ran before being stopped — so averaging it against
|
|
2635
|
+
* model times would pull the figures toward a number that describes neither case. The
|
|
2636
|
+
* row still counts toward `count`, because the caption names the list, not the sample.
|
|
2023
2637
|
*/
|
|
2024
2638
|
function averageOf(rows) {
|
|
2025
2639
|
if (rows.length === 0) return null
|
|
2640
|
+
const measurable = rows.filter((row) => row.interrupted !== true && row.compaction !== true)
|
|
2026
2641
|
const mean = (pick) => {
|
|
2027
2642
|
let sum = 0
|
|
2028
2643
|
let count = 0
|
|
2029
|
-
for (const row of
|
|
2644
|
+
for (const row of measurable) {
|
|
2030
2645
|
const value = pick(row)
|
|
2031
2646
|
if (value === null || value === undefined || !Number.isFinite(value)) continue
|
|
2032
2647
|
sum += value
|
|
@@ -2036,6 +2651,7 @@ window.__ModuleLoader__.load({
|
|
|
2036
2651
|
}
|
|
2037
2652
|
return {
|
|
2038
2653
|
count: rows.length,
|
|
2654
|
+
measured: measurable.length,
|
|
2039
2655
|
ttftMs: mean((row) => row.ttftMs),
|
|
2040
2656
|
speed: mean((row) => row.speed),
|
|
2041
2657
|
tokens: mean((row) => row.tokens),
|
|
@@ -2043,8 +2659,8 @@ window.__ModuleLoader__.load({
|
|
|
2043
2659
|
amortized: mean((row) => row.amortized),
|
|
2044
2660
|
// A column is "estimated" when any sample in it was; the tilde then still means
|
|
2045
2661
|
// the same thing it does on a single row.
|
|
2046
|
-
estimated:
|
|
2047
|
-
anyEstimated:
|
|
2662
|
+
estimated: measurable.some((row) => row.estimated === true),
|
|
2663
|
+
anyEstimated: measurable.some((row) => row.estimated === true),
|
|
2048
2664
|
}
|
|
2049
2665
|
}
|
|
2050
2666
|
|
|
@@ -2305,7 +2921,15 @@ window.__ModuleLoader__.load({
|
|
|
2305
2921
|
return last.running !== false
|
|
2306
2922
|
}, [sessionStats])
|
|
2307
2923
|
const chatNodes = useChat((snapshot) => snapshot.legacy.nodes)
|
|
2308
|
-
const live = useLiveReading(
|
|
2924
|
+
const live = useLiveReading(
|
|
2925
|
+
useChat,
|
|
2926
|
+
geek,
|
|
2927
|
+
trajectory,
|
|
2928
|
+
runningStatsStep,
|
|
2929
|
+
sessionTurnOpen,
|
|
2930
|
+
sessionRunning,
|
|
2931
|
+
sessionStats,
|
|
2932
|
+
)
|
|
2309
2933
|
|
|
2310
2934
|
const triggerRef = useRef(null)
|
|
2311
2935
|
const drawerRef = useRef(null)
|
|
@@ -2403,7 +3027,18 @@ window.__ModuleLoader__.load({
|
|
|
2403
3027
|
*/
|
|
2404
3028
|
const listedRows = useMemo(() => {
|
|
2405
3029
|
const base = statsRows ?? fallbackRows ?? []
|
|
2406
|
-
|
|
3030
|
+
/*
|
|
3031
|
+
* The running compaction is rendered as its own row above the list, so the numbered
|
|
3032
|
+
* list must not repeat it. A finished compaction stays in the list — it is history
|
|
3033
|
+
* then, and it is what the drawer is for.
|
|
3034
|
+
*/
|
|
3035
|
+
const runningCompactionKey =
|
|
3036
|
+
live !== null && live.compaction !== null && live.compaction.running === true
|
|
3037
|
+
? live.compaction.key
|
|
3038
|
+
: null
|
|
3039
|
+
let visible = base
|
|
3040
|
+
if (live !== null && live.key !== null) visible = visible.filter((row) => row.key !== live.key)
|
|
3041
|
+
if (runningCompactionKey !== null) visible = visible.filter((row) => row.key !== runningCompactionKey)
|
|
2407
3042
|
return visible.slice(0, MAX_ROWS)
|
|
2408
3043
|
}, [statsRows, fallbackRows, live])
|
|
2409
3044
|
|
|
@@ -2461,8 +3096,30 @@ window.__ModuleLoader__.load({
|
|
|
2461
3096
|
return widths
|
|
2462
3097
|
})
|
|
2463
3098
|
}
|
|
3099
|
+
/*
|
|
3100
|
+
* Publish the idle layout's width so the compaction phase can reserve it.
|
|
3101
|
+
*
|
|
3102
|
+
* The compaction reading is much shorter than five metric slots, and without a floor
|
|
3103
|
+
* the pill would shrink while it runs — moving the composer's neighbours and then
|
|
3104
|
+
* moving them back when the phase ends. Measuring the settled layout once and pinning
|
|
3105
|
+
* it as a CSS custom property keeps the pill the same size through every phase.
|
|
3106
|
+
*
|
|
3107
|
+
* The measurement is skipped in the compaction phases themselves: that layout is the
|
|
3108
|
+
* one being constrained, so measuring it would ratchet the width down to itself.
|
|
3109
|
+
*/
|
|
3110
|
+
const measurePillWidth = () => {
|
|
3111
|
+
const anchor = triggerRef.current
|
|
3112
|
+
if (anchor === null) return
|
|
3113
|
+
const phase = anchor.getAttribute('data-live-phase')
|
|
3114
|
+
if (phase === 'compacting' || phase === 'compacted') return
|
|
3115
|
+
const width = anchor.getBoundingClientRect().width
|
|
3116
|
+
if (!Number.isFinite(width) || width <= 0) return
|
|
3117
|
+
anchor.style.setProperty('--dsh-rum-pill-width', `${Math.round(width)}px`)
|
|
3118
|
+
}
|
|
2464
3119
|
measure()
|
|
3120
|
+
measurePillWidth()
|
|
2465
3121
|
window.addEventListener('resize', measure)
|
|
3122
|
+
window.addEventListener('resize', measurePillWidth)
|
|
2466
3123
|
/*
|
|
2467
3124
|
* A font-size change (the Settings font scale) or a metric swap moves the pill's
|
|
2468
3125
|
* cells without a window resize; observe the pill itself so the drawer columns
|
|
@@ -2476,6 +3133,7 @@ window.__ModuleLoader__.load({
|
|
|
2476
3133
|
}
|
|
2477
3134
|
return () => {
|
|
2478
3135
|
window.removeEventListener('resize', measure)
|
|
3136
|
+
window.removeEventListener('resize', measurePillWidth)
|
|
2479
3137
|
observer?.disconnect()
|
|
2480
3138
|
}
|
|
2481
3139
|
}, [mounted])
|
|
@@ -2589,6 +3247,22 @@ window.__ModuleLoader__.load({
|
|
|
2589
3247
|
total: secondsText(current.totalMs, t),
|
|
2590
3248
|
amortized: speedText(current.amortized, approximate, t),
|
|
2591
3249
|
})}`
|
|
3250
|
+
/*
|
|
3251
|
+
* The compaction phases replace the whole set of figures with one sentence: a clock, the
|
|
3252
|
+
* model, the wording and a running span. That layout is built as its own branch because
|
|
3253
|
+
* none of the five metric slots apply — a compaction has no first token and no rate.
|
|
3254
|
+
*/
|
|
3255
|
+
const compaction = live?.compaction ?? null
|
|
3256
|
+
const compactingPhase = live?.phase === 'compacting' || live?.phase === 'compacted'
|
|
3257
|
+
/*
|
|
3258
|
+
* One hover hint serves the whole drawer: only one can be open at a time, and sharing the
|
|
3259
|
+
* timer is what stops a hint from a previous row lingering while the pointer moves on.
|
|
3260
|
+
*/
|
|
3261
|
+
const hint = useHint(300)
|
|
3262
|
+
const compactionLabel =
|
|
3263
|
+
compaction === null
|
|
3264
|
+
? statusLabel
|
|
3265
|
+
: `${t('statusAria')} · ${compactionSentence(compaction, live.phase, now, t)}`
|
|
2592
3266
|
|
|
2593
3267
|
return h(
|
|
2594
3268
|
'div',
|
|
@@ -2598,18 +3272,52 @@ window.__ModuleLoader__.load({
|
|
|
2598
3272
|
{
|
|
2599
3273
|
ref: triggerRef,
|
|
2600
3274
|
type: 'button',
|
|
2601
|
-
className:
|
|
3275
|
+
className:
|
|
3276
|
+
'dsh-rum-trigger' +
|
|
3277
|
+
(live !== null ? ' dsh-rum-live' : '') +
|
|
3278
|
+
(compactingPhase ? ' dsh-rum-compact-anchor' : ''),
|
|
2602
3279
|
'aria-haspopup': 'dialog',
|
|
2603
3280
|
'aria-expanded': open,
|
|
2604
|
-
'aria-label':
|
|
3281
|
+
'aria-label': compactionLabel,
|
|
3282
|
+
/* The active phase, so a test can observe it without reaching into the hooks. */
|
|
3283
|
+
'data-live-phase': live === null ? undefined : live.phase,
|
|
3284
|
+
/*
|
|
3285
|
+
* The compaction reading, serialised for tests. The phase attribute says which of
|
|
3286
|
+
* the four states is active; this says what the row actually carries — no first
|
|
3287
|
+
* token, no rate, a span that freezes once the fold reports `wall`.
|
|
3288
|
+
*/
|
|
3289
|
+
'data-compaction-reading':
|
|
3290
|
+
compaction === null || live === null
|
|
3291
|
+
? undefined
|
|
3292
|
+
: JSON.stringify({
|
|
3293
|
+
compactionId: compaction.compactionId,
|
|
3294
|
+
running: compaction.running,
|
|
3295
|
+
ttftMs: live.ttftMs,
|
|
3296
|
+
speed: live.speed,
|
|
3297
|
+
totalMs: compaction.running === true ? live.totalMs : compaction.totalMs,
|
|
3298
|
+
startMs: compaction.startMs,
|
|
3299
|
+
tokens: compaction.running === true ? null : compaction.tokens,
|
|
3300
|
+
model: compaction.model,
|
|
3301
|
+
}),
|
|
3302
|
+
/* The four phases above are all a reader needs, but a spec also asserts the shape
|
|
3303
|
+
* of the compaction reading, so its identity is published too. */
|
|
3304
|
+
'data-compaction': compaction === null ? undefined : compaction.compactionId,
|
|
2605
3305
|
onClick: () => {
|
|
2606
3306
|
setPinned((current) => !current)
|
|
2607
3307
|
},
|
|
2608
3308
|
},
|
|
2609
3309
|
live?.phase === 'tools'
|
|
2610
3310
|
? h(ToolIcon, { className: 'dsh-rum-bolt dsh-rum-tool-icon' })
|
|
2611
|
-
:
|
|
2612
|
-
|
|
3311
|
+
: compactingPhase
|
|
3312
|
+
? h(CompactionIcon, {
|
|
3313
|
+
className:
|
|
3314
|
+
'dsh-rum-bolt dsh-rum-compaction-icon' +
|
|
3315
|
+
(live.phase === 'compacting' ? '' : ' dsh-rum-still'),
|
|
3316
|
+
})
|
|
3317
|
+
: h(BoltIcon, { className: 'dsh-rum-bolt' }),
|
|
3318
|
+
compactingPhase && compaction !== null
|
|
3319
|
+
? h(CompactionReading, { compaction, phase: live.phase, now, t })
|
|
3320
|
+
: h(
|
|
2613
3321
|
'span',
|
|
2614
3322
|
{ className: 'dsh-rum-parts', ref: partsRef },
|
|
2615
3323
|
h(
|
|
@@ -2647,6 +3355,12 @@ window.__ModuleLoader__.load({
|
|
|
2647
3355
|
),
|
|
2648
3356
|
),
|
|
2649
3357
|
),
|
|
3358
|
+
h(Tooltip, {
|
|
3359
|
+
open: hint.hint !== null,
|
|
3360
|
+
x: hint.hint?.x ?? 0,
|
|
3361
|
+
y: hint.hint?.y ?? 0,
|
|
3362
|
+
children: hint.hint?.content ?? null,
|
|
3363
|
+
}),
|
|
2650
3364
|
mounted
|
|
2651
3365
|
? portal(
|
|
2652
3366
|
h(
|
|
@@ -2706,19 +3420,76 @@ window.__ModuleLoader__.load({
|
|
|
2706
3420
|
{ className: 'dsh-rum-row', 'data-head': true, role: 'row' },
|
|
2707
3421
|
h(
|
|
2708
3422
|
'span',
|
|
2709
|
-
{
|
|
3423
|
+
{
|
|
3424
|
+
className: 'dsh-rum-col',
|
|
3425
|
+
role: 'columnheader',
|
|
3426
|
+
'data-align': 'start',
|
|
3427
|
+
onMouseEnter: (event) => hint.show(indexHint(t), event),
|
|
3428
|
+
onMouseLeave: hint.hide,
|
|
3429
|
+
},
|
|
2710
3430
|
t('colIndex'),
|
|
2711
3431
|
),
|
|
2712
3432
|
h(
|
|
2713
3433
|
'span',
|
|
2714
|
-
{
|
|
3434
|
+
{
|
|
3435
|
+
className: 'dsh-rum-col',
|
|
3436
|
+
role: 'columnheader',
|
|
3437
|
+
'data-align': 'start',
|
|
3438
|
+
onMouseEnter: (event) => hint.show(t('hintTime'), event),
|
|
3439
|
+
onMouseLeave: hint.hide,
|
|
3440
|
+
},
|
|
2715
3441
|
t('colTime'),
|
|
2716
3442
|
),
|
|
2717
|
-
h(
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
3443
|
+
h(
|
|
3444
|
+
'span',
|
|
3445
|
+
{
|
|
3446
|
+
className: 'dsh-rum-col',
|
|
3447
|
+
role: 'columnheader',
|
|
3448
|
+
onMouseEnter: (event) => hint.show(t('hintTtft'), event),
|
|
3449
|
+
onMouseLeave: hint.hide,
|
|
3450
|
+
},
|
|
3451
|
+
t('colTtft'),
|
|
3452
|
+
),
|
|
3453
|
+
h(
|
|
3454
|
+
'span',
|
|
3455
|
+
{
|
|
3456
|
+
className: 'dsh-rum-col',
|
|
3457
|
+
role: 'columnheader',
|
|
3458
|
+
onMouseEnter: (event) => hint.show(t('hintSpeed'), event),
|
|
3459
|
+
onMouseLeave: hint.hide,
|
|
3460
|
+
},
|
|
3461
|
+
t('colSpeed'),
|
|
3462
|
+
),
|
|
3463
|
+
h(
|
|
3464
|
+
'span',
|
|
3465
|
+
{
|
|
3466
|
+
className: 'dsh-rum-col',
|
|
3467
|
+
role: 'columnheader',
|
|
3468
|
+
onMouseEnter: (event) => hint.show(t('hintTokens'), event),
|
|
3469
|
+
onMouseLeave: hint.hide,
|
|
3470
|
+
},
|
|
3471
|
+
t('colTokens'),
|
|
3472
|
+
),
|
|
3473
|
+
h(
|
|
3474
|
+
'span',
|
|
3475
|
+
{
|
|
3476
|
+
className: 'dsh-rum-col',
|
|
3477
|
+
role: 'columnheader',
|
|
3478
|
+
onMouseEnter: (event) => hint.show(t('hintTotal'), event),
|
|
3479
|
+
onMouseLeave: hint.hide,
|
|
3480
|
+
},
|
|
3481
|
+
t('colTotal'),
|
|
3482
|
+
),
|
|
3483
|
+
h(
|
|
3484
|
+
'span',
|
|
3485
|
+
{
|
|
3486
|
+
className: 'dsh-rum-col',
|
|
3487
|
+
role: 'columnheader',
|
|
3488
|
+
onMouseEnter: (event) => hint.show(t('hintAmortized'), event),
|
|
3489
|
+
onMouseLeave: hint.hide,
|
|
3490
|
+
},
|
|
3491
|
+
t('colAmortized'),
|
|
3492
|
+
),
|
|
2722
3493
|
),
|
|
2723
3494
|
live !== null
|
|
2724
3495
|
? h(
|
|
@@ -2746,17 +3517,21 @@ window.__ModuleLoader__.load({
|
|
|
2746
3517
|
*/
|
|
2747
3518
|
className: 'dsh-rum-col dsh-rum-index',
|
|
2748
3519
|
role: 'cell',
|
|
2749
|
-
'aria-label':
|
|
2750
|
-
|
|
3520
|
+
'aria-label':
|
|
3521
|
+
live.compaction !== null && live.compaction !== undefined
|
|
3522
|
+
? t('drawerCompacting')
|
|
3523
|
+
: t('drawerLive'),
|
|
2751
3524
|
},
|
|
2752
3525
|
/*
|
|
2753
|
-
* The same
|
|
2754
|
-
*
|
|
2755
|
-
* a
|
|
3526
|
+
* The same glyphs the status pill uses, so a marker means one thing
|
|
3527
|
+
* wherever it appears: the bolt for a reply, the T for its tools,
|
|
3528
|
+
* the three bars for a compaction.
|
|
2756
3529
|
*/
|
|
2757
|
-
live.
|
|
2758
|
-
? h(
|
|
2759
|
-
:
|
|
3530
|
+
live.compaction !== null && live.compaction !== undefined
|
|
3531
|
+
? h(CompactionIcon, { className: 'dsh-rum-live-bolt dsh-rum-compaction-icon' })
|
|
3532
|
+
: live.phase === 'tools'
|
|
3533
|
+
? h(ToolIcon, { className: 'dsh-rum-live-bolt dsh-rum-tool-icon' })
|
|
3534
|
+
: h(BoltIcon, { className: 'dsh-rum-live-bolt' }),
|
|
2760
3535
|
),
|
|
2761
3536
|
/*
|
|
2762
3537
|
* The clock, not the word "streaming".
|
|
@@ -2782,10 +3557,21 @@ window.__ModuleLoader__.load({
|
|
|
2782
3557
|
h(
|
|
2783
3558
|
'span',
|
|
2784
3559
|
{ className: 'dsh-rum-col dsh-rum-primary', role: 'cell' },
|
|
2785
|
-
|
|
3560
|
+
live.compaction !== null && live.compaction !== undefined
|
|
3561
|
+
? t('compactionSpeed')
|
|
3562
|
+
: speedText(live.speed, live.estimated === true, t),
|
|
2786
3563
|
),
|
|
2787
3564
|
h('span', { className: 'dsh-rum-col', role: 'cell' }, tokensText(live.tokens, live.estimated === true, t)),
|
|
2788
|
-
h(
|
|
3565
|
+
h(
|
|
3566
|
+
'span',
|
|
3567
|
+
{ className: 'dsh-rum-col', role: 'cell' },
|
|
3568
|
+
secondsText(
|
|
3569
|
+
live.compaction !== null && live.compaction !== undefined
|
|
3570
|
+
? compactionSpan(live.compaction, now)
|
|
3571
|
+
: live.totalMs,
|
|
3572
|
+
t,
|
|
3573
|
+
),
|
|
3574
|
+
),
|
|
2789
3575
|
h(
|
|
2790
3576
|
'span',
|
|
2791
3577
|
{ className: 'dsh-rum-col', role: 'cell' },
|
|
@@ -2798,39 +3584,106 @@ window.__ModuleLoader__.load({
|
|
|
2798
3584
|
'div',
|
|
2799
3585
|
{
|
|
2800
3586
|
key: entry.key,
|
|
2801
|
-
className:
|
|
3587
|
+
className:
|
|
3588
|
+
'dsh-rum-row dsh-rum-item' +
|
|
3589
|
+
(entry.row.compaction === true ? ' dsh-rum-compaction-row' : ''),
|
|
2802
3590
|
'data-phase': entry.phase,
|
|
2803
3591
|
role: 'row',
|
|
2804
3592
|
},
|
|
2805
|
-
|
|
3593
|
+
/*
|
|
3594
|
+
* The index column shows the phase glyph instead of a number while a
|
|
3595
|
+
* request or a compaction is still running: the row is not a finished
|
|
3596
|
+
* record yet, so numbering it would imply it belongs with the ten.
|
|
3597
|
+
*/
|
|
3598
|
+
h(
|
|
3599
|
+
'span',
|
|
3600
|
+
{ className: 'dsh-rum-index', role: 'cell' },
|
|
3601
|
+
entry.row.compaction === true
|
|
3602
|
+
? h(CompactionIcon, {
|
|
3603
|
+
className:
|
|
3604
|
+
'dsh-rum-live-bolt dsh-rum-compaction-icon' +
|
|
3605
|
+
(entry.row.running === true ? '' : ' dsh-rum-still'),
|
|
3606
|
+
})
|
|
3607
|
+
: entry.phase === 'tools'
|
|
3608
|
+
? h(ToolIcon, { className: 'dsh-rum-live-bolt dsh-rum-tool-icon' })
|
|
3609
|
+
: entry.phase === 'writing'
|
|
3610
|
+
? h(BoltIcon, { className: 'dsh-rum-live-bolt' })
|
|
3611
|
+
: String(entry.index + 1),
|
|
3612
|
+
),
|
|
3613
|
+
/*
|
|
3614
|
+
* The clock's hint is the absolute timestamp: the relative wording the
|
|
3615
|
+
* cell used to carry in `title` moved into the tooltip once the drawer
|
|
3616
|
+
* gained one, and "3 minutes ago" cannot answer "which one was that".
|
|
3617
|
+
*/
|
|
2806
3618
|
h(
|
|
2807
3619
|
'span',
|
|
2808
|
-
{
|
|
3620
|
+
{
|
|
3621
|
+
className: 'dsh-rum-clock',
|
|
3622
|
+
role: 'cell',
|
|
3623
|
+
onMouseEnter: (event) => hint.show(absoluteStamp(entry.row.at), event),
|
|
3624
|
+
onMouseLeave: hint.hide,
|
|
3625
|
+
},
|
|
2809
3626
|
clockText(entry.row.at),
|
|
2810
3627
|
),
|
|
2811
3628
|
h(
|
|
2812
3629
|
'span',
|
|
2813
3630
|
{ className: 'dsh-rum-col', role: 'cell' },
|
|
2814
3631
|
/*
|
|
2815
|
-
* A
|
|
2816
|
-
*
|
|
2817
|
-
*
|
|
3632
|
+
* A compaction has no first token, so the column is always the dash.
|
|
3633
|
+
* An interrupted request keeps whatever was measured: one stopped
|
|
3634
|
+
* after its first token shows that real figure.
|
|
2818
3635
|
*/
|
|
2819
|
-
entry.row.
|
|
2820
|
-
? t('interrupted')
|
|
2821
|
-
: secondsText(entry.row.ttftMs, t),
|
|
3636
|
+
secondsText(entry.row.compaction === true ? null : entry.row.ttftMs, t),
|
|
2822
3637
|
),
|
|
2823
3638
|
h(
|
|
2824
3639
|
'span',
|
|
2825
|
-
{
|
|
2826
|
-
|
|
3640
|
+
{
|
|
3641
|
+
className: 'dsh-rum-col dsh-rum-primary',
|
|
3642
|
+
role: 'cell',
|
|
3643
|
+
/*
|
|
3644
|
+
* The hint names the model that served this row. A compaction's
|
|
3645
|
+
* summarizer reuses the routed model, and `compaction/summary`
|
|
3646
|
+
* records it, so both kinds of row can answer.
|
|
3647
|
+
*/
|
|
3648
|
+
onMouseEnter: (event) => hint.show(modelHintText(entry.row, t), event),
|
|
3649
|
+
onMouseLeave: hint.hide,
|
|
3650
|
+
},
|
|
3651
|
+
/*
|
|
3652
|
+
* A cancelled step never settled and a compaction never generates, so
|
|
3653
|
+
* neither has a rate. The column says what happened instead of the same
|
|
3654
|
+
* dash a request that has not started would show.
|
|
3655
|
+
*/
|
|
3656
|
+
entry.row.compaction === true
|
|
3657
|
+
? t('compactionSpeed')
|
|
3658
|
+
: entry.row.interrupted === true
|
|
3659
|
+
? t('interrupted')
|
|
3660
|
+
: speedText(entry.row.speed, false, t),
|
|
2827
3661
|
),
|
|
2828
3662
|
h(
|
|
2829
3663
|
'span',
|
|
2830
|
-
{
|
|
3664
|
+
{
|
|
3665
|
+
className: 'dsh-rum-col',
|
|
3666
|
+
role: 'cell',
|
|
3667
|
+
onMouseEnter: (event) => hint.show(tokensHintText(entry.row, t), event),
|
|
3668
|
+
onMouseLeave: hint.hide,
|
|
3669
|
+
},
|
|
2831
3670
|
tokensText(entry.row.tokens, entry.row.estimated, t),
|
|
2832
3671
|
),
|
|
2833
|
-
h(
|
|
3672
|
+
h(
|
|
3673
|
+
'span',
|
|
3674
|
+
{ className: 'dsh-rum-col', role: 'cell' },
|
|
3675
|
+
secondsText(
|
|
3676
|
+
/*
|
|
3677
|
+
* A running compaction counts up from its own start, exactly as the
|
|
3678
|
+
* pill does, so the two figures cannot diverge; the frozen `wall`
|
|
3679
|
+
* takes over the moment the fold reports it.
|
|
3680
|
+
*/
|
|
3681
|
+
entry.row.compaction === true && entry.row.running === true
|
|
3682
|
+
? compactionSpan(entry.row, now)
|
|
3683
|
+
: entry.row.totalMs,
|
|
3684
|
+
t,
|
|
3685
|
+
),
|
|
3686
|
+
),
|
|
2834
3687
|
h(
|
|
2835
3688
|
'span',
|
|
2836
3689
|
{ className: 'dsh-rum-col', role: 'cell' },
|