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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.3.0
4
+
5
+ 状态从三种扩到四种,加入上下文压缩的显示,并给抽屉补上悬浮提示。
6
+
7
+ - **压缩中**:状态栏显示图标、开始时刻、「压缩上下文中」与实时秒数;抽屉第 0 行同步计时。
8
+ 图标闪动,速度列写「压缩上下文」,首字列固定为破折号。
9
+ - **压缩完成**:图标停止闪动,文案改为「压缩完成」,保持到下一条请求发出。该行进入
10
+ 抽屉 #1,补上输出 Token 与折算速度(output ÷ wall),但**不计入底部平均值**。
11
+ - 压缩没有首字与生成速度,这两项数据源里本就不存在;总用时取 wall,是日志事实,
12
+ 刷新后不变。
13
+ - 压缩用哪个模型:优先显式配置,否则跟随最后一次请求(复用 KV 缓存)。压缩结束后
14
+ compaction/summary 会记录真实模型并显示;压缩期间尚无此记录,显示对话模型名。
15
+ - **悬浮提示**:时间列显示完整时间戳,速度列显示模型,输出 Token 列显示输入 token
16
+ 与缓存命中率;七个表头各自说明该列含义,# 列的说明直接画出四种状态的图标。
17
+ - 提示用自建浮层而非 title,因为 title 只能显示纯文本,而 # 列的说明需要图标。
18
+ - 压缩阶段的状态栏预留空闲态宽度,切换时整体尺寸与图标位置都不变。
19
+
20
+ ## 0.2.3
21
+
22
+ - 中断的行现在显示它跑了多久:被手动停止的请求没有 model,总用时回退到 wall
23
+ (endTime - startTime),刷新后仍是同一个数。
24
+ - 「中断」标记移到速度列。首字若已返回就照常显示实测值,总用时也照常显示——
25
+ 缺的只是生成速度,由速度列说明原因。
26
+ - 中断行不计入底部平均值,但标题仍数满十行。
27
+
3
28
  ## 0.2.2
4
29
 
5
30
  - 修复:切换模型后抽屉卡在旧的一轮。数据源的 target 曾被缓存,而会话重建
package/README.en.md CHANGED
@@ -95,75 +95,67 @@ Two further details make the edges actually coincide:
95
95
  Click the pill to open the drawer; click it again, or press Escape, or click outside, to
96
96
  close it. There is no hover-open and no tooltip.
97
97
 
98
- ## Three states
98
+ ## Four states
99
99
 
100
- This plugin has two kinds of busy and one kind of idle, and all three look different:
100
+ The plugin sorts "what is happening right now" into four phases, each with its own glyph and
101
+ wording:
101
102
 
102
- | State | Decided by | Pill icon | Drawer header | Live row |
103
- | --- | --- | --- | --- | --- |
104
- | **Model writing** | `model` is `null` | ⚡ pulsing bolt | `Streaming` | yes (11 rows) |
105
- | **Tools running** | `model` set, `endTime` `null` | **pulsing T** | **`Calling tools`** | yes (11 rows) |
106
- | **Idle** | `endTime` set, or no running step | still | `last N` | no (10 rows) |
103
+ | State | Decided by | Pill icon | Drawer header |
104
+ | --- | --- | --- | --- |
105
+ | **Model writing** | `model` is `null` | ⚡ pulsing | `Streaming` |
106
+ | **Tools running** | `model` set, `endTime` `null` | T pulsing | `Calling tools` |
107
+ | **Compacting** | newest compaction `running` | ≡ pulsing | `Compacting context` |
108
+ | **Compacted** | compaction finished, no request since | ≡ still | `Context compacted` |
109
+ | **Idle** | otherwise | ⚡ still | `Last N` |
107
110
 
108
- ### Why the tool phase is its own state
111
+ When a compaction finishes the pill says "Context compacted" and stops pulsing, and it stays
112
+ that way until the next request starts — at which point the writing phase takes over again.
109
113
 
110
- This plugin measures **model output**, and `step/end` only arrives after the step's tool calls
111
- have all returned. Keying off `running` (which `step/end` clears) therefore kept the plugin in
112
- "streaming" for the entire tool phase after the reply had already finished: the bolt kept
113
- pulsing and the live row kept occupying an eleventh row.
114
+ ### Compaction is its own activity
114
115
 
115
- The field that separates the first two is `model`, which is `messageTime − startTime`:
116
+ It belongs to no step: it is opened by `compaction/start` and closed by `compaction/end`, it
117
+ lives on the turn's `compactions[]`, it runs between two steps, and it can take tens of
118
+ seconds. No step is running during that time, so the pill used to keep showing the previous
119
+ step's figures and the pause looked like a hang.
116
120
 
117
- - the model is still writing → `null`
118
- - `assistant/message` has landed → a fixed value, **even while tools run**
121
+ It has no first token, no generation rate and no streamed token count — all three belong to a
122
+ settled reply. So the drawer's columns are filled like this:
119
123
 
120
- `endTime` then separates the tool phase from idle. Both fields come from the Host's
121
- `sessionStats` fold — the same data the Statistics page renders.
122
-
123
- ### The live row stays through the tool phase
124
-
125
- The request is genuinely still in flight, so it keeps its row; only the marker changes from the
126
- bolt to a T, and the drawer header switches from "Streaming" to "Calling tools". That makes all
127
- three states distinguishable at a glance.
124
+ | Column | While compacting | Once compacted |
125
+ | --- | --- | --- |
126
+ | First token | `—` | `—` |
127
+ | Speed | `Compaction` | `Compaction` |
128
+ | Tokens | `—` | the real figure (e.g. `4.9k tok`) |
129
+ | Time | counts up live | frozen at `wall` |
130
+ | Amortised | `—` | `output ÷ wall` |
128
131
 
129
- Both glyphs share **the same 14×14 svg box** (`.dsh-rum-trigger svg`) and the same `pulse`
130
- animation. Switching changes the shape and colour inside the box, never the box, so the status
131
- row does not shift. The T is drawn narrower on purpose so it does not read heavier than the bolt.
132
+ The total is `wall` (`endTime − startTime`): a log fact, so a reload shows the same span.
133
+ A compaction row is excluded from the footer average — its total is wall-clock time, and
134
+ folding it into the mean would pull it toward a number describing neither case.
132
135
 
133
- ### Fallback
136
+ ### Which model compacts
134
137
 
135
- A composition without the statistics fold has no `model` to read. There, `partial` and a
136
- Trajectory request can only mean the model is writing, so the state is always "model writing" —
137
- it never degrades to idle just because the fold is missing.
138
+ The summarizer picks, in order: an explicitly configured `summarizationProvider/Model` → **the
139
+ model the last request used** → the Agent's own model. The default is the second, because
140
+ replaying the whole conversation is only cheap when it hits the same provider's KV cache.
138
141
 
139
- **Drawer** — slides up from the status row, right-aligned to that pill, `position: fixed`
140
- on `document.body` so no scroll container can clip it. Its material is the shipped
141
- statistics dialog's own, copied token for token: the `--dsw-specific-menu` fill
142
- (`#303136f0` in dark mode, i.e. nearly opaque, the same surface behind the native
143
- numbers), the `--dsw-menu-backdrop-filter` 40px/150% blur, `--dsw-elevation-prominent`
144
- stroke and shadow, `--dsw-radius-lg`, and 16px padding. It lists the last ten requests,
145
- newest first, with no explanatory footer:
142
+ `compaction/summary` records the model actually used, so **after** a compaction the real name
143
+ is shown; **while** one runs the log has no such record yet, so the chat model is named, and
144
+ the generic wording is the last resort.
146
145
 
147
- | # | At | TTFT | Speed | Tokens | Time | Amortised |
148
- | --- | --- | --- | --- | --- | --- | --- |
149
- | 1 | 18:42:07 | 2.3 s | 445 tok/s | 1024 tok | 4.6 s | 223 tok/s |
146
+ ### Hover hints
150
147
 
151
- The `At` column is when the request was **issued** (`step/start`), matching the Statistics
152
- view. It is not the end time: a row stamped 18:42:07 with a 4.6 s total actually finished
153
- at 18:42:12, so an end stamp would read as "it finished then" and would also order the list
154
- by completion rather than by issue.
148
+ Every drawer column and every header carries a hint:
155
149
 
156
- A new request slides in at the top; the row that falls off the ten-row cap is removed in
157
- the **same frame** rather than fading out on its own. An exit phase would keep an
158
- eleventh row laid out for the animation's duration, growing the drawer by a row and
159
- shrinking it back — a visible height jump on every insertion. Opening the drawer also
160
- establishes a baseline instead of animating, so the whole list never slides in at once.
150
+ | Where | Content |
151
+ | --- | --- |
152
+ | Clock column | the full timestamp `11-06 10:47:41.828` |
153
+ | Speed column | the model that served that row |
154
+ | Tokens column | input tokens and cache hit rate |
155
+ | Headers | what the column measures (the `#` header draws the four state glyphs) |
161
156
 
162
- While a step is streaming, the drawer shows it as a live row above the history, and the
163
- status reading switches to a live style. Under a rule below the history sits a summary
164
- row — the average of the visible requests, in the same grid so each mean lands under its
165
- column. A column with no samples (say no TTFT on an interrupted request) prints a dash
166
- rather than a misleading zero.
157
+ The hints are a real overlay rather than `title`: `title` can only show plain text, and the
158
+ `#` header has to draw glyphs.
167
159
 
168
160
  ## How the numbers are measured
169
161
 
@@ -412,6 +404,25 @@ carries `ttft`, `model`, `decode`, `tps`, `output` and `wall`.
412
404
  | **Time** | **`model`** | **model time, not the Statistics view's total** |
413
405
  | Amortised | `output ÷ model` | this plugin's own figure |
414
406
 
407
+ **Interrupted rows:**
408
+
409
+ A request that was stopped never produced an `assistant/message`, so `model`, `decode` and
410
+ `tps` are all null. That row's **total falls back to `wall`** (`endTime − startTime`) — how
411
+ long the request actually ran before it was stopped. It is a log fact, so it survives a reload
412
+ unchanged, which is exactly what that column is for here.
413
+
414
+ **The first-token column falls back to `wall` too**, in two shapes: if the upstream never returned a
415
+ token, the first-token figure is the wait itself and equals the total; if a token had arrived,
416
+ `ttft` is already fixed and the row keeps that measurement, so the first-token figure is smaller
417
+ than the total — which is how a reader tells "it started answering and was cut off" from "it never
418
+ got to answer".
419
+
420
+ The word **"interrupted" sits in the speed column**, not the first-token column: both the
421
+ first-token figure and the total are real readings, and the only missing figure is the generation
422
+ rate, so that is the column that says why. Interrupted rows are
423
+ excluded from the footer average — their total is wall-clock time, and folding it into the mean
424
+ would pull it toward a number describing neither case — but the caption still counts all ten.
425
+
415
426
  **Why the total is `model` and not `wall`:**
416
427
 
417
428
  The Statistics view's total column is `wall = endTime − startTime`, which **includes the step's
package/README.md CHANGED
@@ -85,68 +85,60 @@ DSH 的极客级请求指标。它在 **设置 → 通用 → 性能与用量**
85
85
 
86
86
  点击气泡打开抽屉;再次点击、按 Esc、或点击外部即可关闭。没有悬停打开,也没有悬停提示。
87
87
 
88
- ## 三种状态
88
+ ## 四种状态
89
89
 
90
- 插件只该有两种「忙」和一种「闲」,三者的判定与呈现都不同:
90
+ 插件把「现在在干什么」分成四段,每段有自己的图标与文案:
91
91
 
92
- | 状态 | 判定 | 状态栏图标 | 抽屉右上角 | 实时行 |
93
- | --- | --- | --- | --- | --- |
94
- | **模型输出中** | `model` 为 `null` | ⚡ 闪电闪动 | `生成中` | 有(共 11 行) |
95
- | **工具调用中** | `model` 有值、`endTime` 为 `null` | **T 闪动** | **`调用工具中`** | 有(共 11 行) |
96
- | **空闲** | `endTime` 有值,或没有运行中的步骤 | 静止 | `最近 N 条` | 无(共 10 行) |
92
+ | 状态 | 判定 | 状态栏图标 | 抽屉右上角 |
93
+ | --- | --- | --- | --- |
94
+ | **模型输出中** | `model` 为 `null` | ⚡ 脉冲 | `生成中` |
95
+ | **工具调用中** | `model` 有值、`endTime` 为 `null` | T 脉冲 | `调用工具中` |
96
+ | **上下文压缩中** | 最近一次压缩 `running` | ≡ 脉冲 | `上下文压缩中` |
97
+ | **上下文压缩完成** | 压缩已结束、之后没有新请求 | ≡ 静止 | `上下文压缩完成` |
98
+ | **空闲** | 其余 | ⚡ 静止 | `最近 N 条` |
97
99
 
98
- ### 为什么要分「工具调用中」
100
+ 压缩完成后状态栏显示「压缩完成」并停止闪动,直到下一条请求发出——那时自动回到模型输出状态。
99
101
 
100
- 本插件统计的是**模型输出性能**,而 `step/end` 要等这一步的工具调用全部跑完才到。
101
- 若只看 `running`(它由 `step/end` 清零),模型回复早已结束、工具还在跑的整段时间里,
102
- 插件都会停在「生成中」:闪电一直闪、实时行一直占着第 11 行。
102
+ ### 压缩是一种独立的活动
103
103
 
104
- 分隔前两者的字段是 `model`,也就是 `messageTime − startTime`:
104
+ 压缩不属于任何一步:它由 `compaction/start` 与 `compaction/end` 界定,挂在回合的
105
+ `compactions[]` 上,跑在两步之间,可以持续几十秒。在这段时间里没有步骤在跑,
106
+ 所以之前只能沿用上一步的数字,看起来像是卡住了。
105
107
 
106
- - 模型还在写 → `null`
107
- - `assistant/message` 落地 → 定值,**即使工具还在跑**
108
+ 它没有首字、没有生成速度,也没有流式 token —— 那三项都依附于一条已结算的回复。
109
+ 因此抽屉里那几列,压缩行是这样处理的:
108
110
 
109
- 再用 `endTime` 把工具阶段与空闲分开。这两个字段都来自宿主的 `sessionStats` 折叠,
110
- 与统计页显示的是同一份数据。
111
-
112
- ### 工具阶段仍保留实时行
113
-
114
- 这一阶段请求确实还在飞行,所以它继续占一行;只是标记从闪电换成 T,
115
- 抽屉标题也从「生成中」改成「调用工具中」。三个状态因此一眼可分。
111
+ | 列 | 压缩期间 | 压缩完成后 |
112
+ | --- | --- | --- |
113
+ | 首字 | `—` | `—` |
114
+ | 速度 | `压缩上下文` | `压缩上下文` |
115
+ | 输出 Token | `—` | 实际值(如 `4.9k tok`) |
116
+ | 总用时 | 实时递增 | 冻结为 `wall` |
117
+ | 折算速度 | `—` | `output ÷ wall` |
116
118
 
117
- 两种图标共用**同一个 14×14 的 svg 盒**(`.dsh-rum-trigger svg`)与同一段 `pulse` 动画。
118
- 切换时改变的是盒内的字形与颜色,盒子本身不动,所以状态栏不会位移。
119
- T 的字形特意收窄,避免比闪电看起来更重。
119
+ 总用时取 `wall`(`endTime − startTime`):这是日志事实,刷新后仍是同一个数。
120
+ 压缩行不计入底部平均值——它的总用时是墙钟时间,混进均值会拉偏。
120
121
 
121
- ### 降级
122
+ ### 压缩用哪个模型
122
123
 
123
- 拿不到统计折叠时的组成(`uiConversation` 缺失)没有 `model` 可读,此时 `partial`
124
- 与轨迹域的请求都只可能表示模型正在输出,因此一律按「模型输出中」处理——
125
- 不会因为缺少折叠而误判为空闲。
124
+ 压缩的模型按顺序取:显式配置的 `summarizationProvider/Model` → **最后一次请求实际用的模型**
125
+ → Agent 自身的模型。默认走第二条,因为重放整段对话时复用同一个模型才能命中 KV 缓存。
126
126
 
127
- **抽屉**——从状态行向上滑出,右边缘与该气泡对齐,`position: fixed` 挂在
128
- `document.body` 上,因此不会被任何滚动容器裁剪。它的材质逐字复刻了原生统计弹窗:
129
- `--dsw-specific-menu` 底色(深色模式下是 `#303136f0`,即近乎不透明,正是原生数字背后的那层)、
130
- `--dsw-menu-backdrop-filter` 的 40px/150% 模糊、`--dsw-elevation-prominent` 描边与阴影、
131
- `--dsw-radius-lg`、以及 16px 内边距。它按最新在上的顺序列出最近十次请求,底部不加说明文字:
127
+ `compaction/summary` 会记录实际用的模型,所以**压缩结束后**显示的是真实模型名;
128
+ **压缩期间**日志里还没有这个信息,此时显示对话模型名,拿不到则写「使用对话模型」。
132
129
 
133
- | # | 时间 | 首字 | 速度 | 输出 Token | 总用时 | 折算速度 |
134
- | --- | --- | --- | --- | --- | --- | --- |
135
- | 1 | 18:42:07 | 2.3 s | 445 tok/s | 1024 tok | 4.6 s | 223 tok/s |
130
+ ### 悬浮提示
136
131
 
137
- 「时间」列取请求**发起**的时刻(`step/start`),与统计页一致。它不取结束时刻:
138
- 否则一行写着 18:42:07、总用时 4.6 秒,实际却是 18:42:12 才完成,读起来会误解成「那一刻就结束了」;
139
- 而且列表会变成按完成时间排序,而不是按发起时间。
132
+ 抽屉的每一列与表头都有提示:
140
133
 
141
- 新请求从顶部滑入;被十行上限挤掉的那一行在**同一帧**内移除,而不是自己淡出。
142
- 离场动画会让第 11 行在整个动画期间参与布局,使抽屉先长高一行再缩回——
143
- 每次插入都会看到一次高度跳动。打开抽屉时也只是建立基线,不播放动画,
144
- 所以整个列表不会在打开的瞬间一起滑入。
134
+ | 位置 | 内容 |
135
+ | --- | --- |
136
+ | 时间列 | 完整时间戳 `11-06 10:47:41.828` |
137
+ | 速度列 | 该行使用的模型 |
138
+ | 输出 Token 列 | 输入 token 与缓存命中率 |
139
+ | 表头 | 该列的含义(`#` 一列用图标展示四种状态) |
145
140
 
146
- 某个步骤正在流式生成时,抽屉把它显示为历史上方的一条 live 行,状态读数也切换为 live 样式。
147
- 历史下方有一条分隔线,线下面是汇总行——对当前可见请求取平均值,复用同一套 grid,
148
- 因此每个均值都落在它所汇总的那一列下面。完全没有样本的列(比如中断请求没有首字时间)
149
- 打印破折号,而不是容易误解的 0。
141
+ 提示是自建的浮层而不是 `title`:`title` 只能显示纯文本,而 `#` 列的说明要画出图标。
150
142
 
151
143
  ## 数字是怎么算出来的
152
144
 
@@ -370,6 +362,16 @@ const target = uiConversation.binding(sessionId).target('stats')
370
362
  | **总用时** | **`model`** | **模型用时,不是统计页的「总用时」列** |
371
363
  | 折算速度 | `output ÷ model` | 本插件独有 |
372
364
 
365
+ **中断的行:**
366
+
367
+ 被手动停止的请求没有 `assistant/message`,因此 `model`、`decode`、`tps` 全为 null。
368
+ 那一行的**总用时回退到 `wall`**(`endTime − startTime`),也就是这条请求从发出到被
369
+ 停止实际过了多久——它是日志里的事实,刷新多少次都不变,正是这个场合要的数字。
370
+
371
+ **首字也回退到 `wall`**,分两种情况:上游一直没返回 token,那么首字就是它等待的时长,
372
+ 缺的只是生成速度,所以由速度列说明原因。中断行不计入底部的平均值——它的总用时是墙钟
373
+ 时间,混进均值会拉向一个既不代表完成、也不代表中断的数;但平均行的标题仍数满十行。
374
+
373
375
  **总用时为何取 `model` 而不是 `wall`:**
374
376
 
375
377
  统计页的「总用时」列是 `wall = endTime − startTime`,它**把工具调用也算了进去**。