@cnife/pi-footer 0.1.0 → 0.2.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 CHANGED
@@ -8,18 +8,20 @@
8
8
 
9
9
  ```text
10
10
  <目录> · <分支> ↑<ahead> ↓<behind> *<未提交数> · <会话名>
11
- <provider>/<model_id> · <思考强度> · <已用>/<总量> (<百分比>) · $<花费>
11
+ <provider>/<model_id> · <思考强度> · <已用>/<总量> (<百分比>) · $<花费> · <tps>t/s
12
12
  ```
13
13
 
14
- - 各项 ` · ` 分隔,空值自动省略(无 git 段、无会话名、cost=0 等均不显示)
14
+ - 各项 ` · ` 分隔,空值自动省略(无 git 段、无会话名、cost=0、无 tps 等均不显示)
15
15
  - git:`main ↑1 ↓2 *3`,ahead/behind/未提交数为 0 时隐藏
16
16
  - cost:`<0.01` 用 3 位小数,否则 2 位;为 0 时整项隐藏
17
17
  - token:`<1000` 原值,`<1M` 用 `X.XK`,`≥1M` 用 `X.XM`
18
+ - tps:单条 assistant message 输出速率 `NN.Nt/s`,无数据时整项隐藏;口径 `usage.output / (message_end − before_provider_request)`,elapsed 含网络 + 排队 + 生成(对齐 pi 官方 `tps.ts`)
18
19
 
19
20
  ## 刷新
20
21
 
21
22
  - git 状态:10s 定时器 + `tool_result`(agent 工具执行后即时)+ `user_bash`(`!`/`!!` 命令后 1.5s debounce)+ `onBranchChange`
22
23
  - 其余字段随 pi 内部状态自动重渲染
24
+ - tps:`message_end` 后刷新并保持到下一条;切模型 / compact / 树导航 / 新会话时重置;`stopReason` 为 `aborted`/`error` 时丢弃
23
25
 
24
26
  ## 字符约束
25
27
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * cnife-footer - 个人专属 pi footer。
3
3
  * 两行:第一行工作区(目录 · git分支 ↑ahead ↓behind *未提交 · 会话名),
4
- * 第二行模型(provider/id · thinking · ctx · $cost)。
4
+ * 第二行模型(provider/id · thinking · ctx · $cost · tps)。
5
5
  * 全 dim,仅 ASCII + Unicode,无 nerd font,无配置。
6
6
  */
7
7
 
@@ -31,6 +31,12 @@ function fmtCost(c: number): string {
31
31
  return c < 0.01 ? `$${c.toFixed(3)}` : `$${c.toFixed(2)}`;
32
32
  }
33
33
 
34
+ /** Type guard: assistant message 才有 usage/stopReason,用于 message_end。 */
35
+ function isAssistantMessage(message: unknown): message is AssistantMessage {
36
+ if (!message || typeof message !== "object") return false;
37
+ return (message as { role?: unknown }).role === "assistant";
38
+ }
39
+
34
40
  /** Parse `git status -b --porcelain` output into branch/ahead/behind/dirty. */
35
41
  function parseGitStatus(stdout: string): GitState {
36
42
  const lines = stdout.split("\n");
@@ -78,6 +84,11 @@ export default function (pi: ExtensionAPI) {
78
84
  let refreshing = false;
79
85
  let cwd = "";
80
86
 
87
+ // TPS(输出速率):requestAt 配对 before_provider_request/message_end,
88
+ // lastTps 保存最近一次有效速率,render 时拼到第 2 行末尾。
89
+ let requestAt: number | null = null;
90
+ let lastTps: number | null = null;
91
+
81
92
  const refreshGit = async () => {
82
93
  if (refreshing) return;
83
94
  refreshing = true;
@@ -96,9 +107,18 @@ export default function (pi: ExtensionAPI) {
96
107
  }
97
108
  };
98
109
 
110
+ /** 清空 TPS 锚点与最近速率并重渲染(切模型/compact/树导航/无效结束时调用)。 */
111
+ const resetTps = () => {
112
+ requestAt = null;
113
+ lastTps = null;
114
+ activeTui?.requestRender();
115
+ };
116
+
99
117
  pi.on("session_start", async (_event, ctx) => {
100
118
  cwd = ctx.cwd;
101
119
  gitState = { branch: null, ahead: 0, behind: 0, dirty: 0 };
120
+ requestAt = null;
121
+ lastTps = null;
102
122
 
103
123
  ctx.ui.setFooter((tui, theme, footerData) => {
104
124
  activeTui = tui;
@@ -135,7 +155,7 @@ export default function (pi: ExtensionAPI) {
135
155
  const session = pi.getSessionName() ?? "";
136
156
  const line1 = renderLine([dir, gitStr, session], width);
137
157
 
138
- // Line 2: provider/id · thinking · ctx · $cost (cost=0 隐藏)
158
+ // Line 2: provider/id · thinking · ctx · $cost · tps (cost=0/无 tps 隐藏)
139
159
  const model = ctx.model
140
160
  ? `${ctx.model.provider}/${ctx.model.id}`
141
161
  : "no-model";
@@ -162,7 +182,11 @@ export default function (pi: ExtensionAPI) {
162
182
  }
163
183
 
164
184
  const costStr = cost > 0 ? fmtCost(cost) : "";
165
- const line2 = renderLine([model, thinking, ctxStr, costStr], width);
185
+ const tpsStr = lastTps != null ? `${lastTps.toFixed(1)}t/s` : "";
186
+ const line2 = renderLine(
187
+ [model, thinking, ctxStr, costStr, tpsStr],
188
+ width,
189
+ );
166
190
 
167
191
  return [line1, line2];
168
192
  } catch {
@@ -178,6 +202,41 @@ export default function (pi: ExtensionAPI) {
178
202
  }, GIT_INTERVAL_MS);
179
203
  });
180
204
 
205
+ // TPS(输出速率):before_provider_request 记请求时刻,message_end 算 output/elapsed。
206
+ // 口径对齐 pi 官方 tps.ts:elapsed 含网络 + 排队 + 生成,不用 message.timestamp。
207
+ pi.on("before_provider_request", () => {
208
+ requestAt = Date.now();
209
+ });
210
+
211
+ pi.on("message_end", (event) => {
212
+ if (!isAssistantMessage(event.message)) return;
213
+ const message = event.message as AssistantMessage;
214
+ // aborted/error 或无效数据时丢弃,不留旧值
215
+ if (message.stopReason === "aborted" || message.stopReason === "error") {
216
+ resetTps();
217
+ return;
218
+ }
219
+ const output = message.usage.output;
220
+ if (requestAt === null || output <= 0) {
221
+ resetTps();
222
+ return;
223
+ }
224
+ const elapsedSeconds = (Date.now() - requestAt) / 1000;
225
+ if (elapsedSeconds <= 0) {
226
+ resetTps();
227
+ return;
228
+ }
229
+ lastTps = output / elapsedSeconds;
230
+ activeTui?.requestRender();
231
+ // 消费完毕,等下一条 message 的 before_provider_request
232
+ requestAt = null;
233
+ });
234
+
235
+ // 切模型 / compact / 树导航后重置 TPS
236
+ pi.on("session_tree", () => resetTps());
237
+ pi.on("session_compact", () => resetTps());
238
+ pi.on("model_select", () => resetTps());
239
+
181
240
  // agent 工具执行后立即刷新 git
182
241
  pi.on("tool_result", async () => {
183
242
  void refreshGit();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cnife/pi-footer",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "pi-package"