pi-open-tui 0.2.8 → 0.2.9

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
@@ -73,7 +73,7 @@ After each complete agent run, open-tui shows one transient notification. Tool-c
73
73
 
74
74
  The notification uses the footer's icon mode and semantic theme colors. Configure its master switch and individual TPS, TTFT, duration, token, stall, and cost segments from the **Telemetry** tab in `/open-tui`.
75
75
 
76
- TPS is the actual output delivery rate: provider-reported output tokens divided by the time from the first meaningful output delta through `message_end`. It excludes time-to-first-token and tool execution between assistant messages, but includes stalls during streaming. Multi-message and multi-turn runs sum only measurable token-duration pairs before calculating the rate, so an unmeasurable short tool response does not pollute or hide a measurable final response. Messages with fewer than two meaningful deltas or less than 200 ms of observed delivery time are shown as `TPS —` unless another measurable message exists in the run. The `stall` segment shows occurrence count followed by accumulated duration. The optional `$ / M` value uses the model's list-price `usage.cost.total`; it is not the session's cumulative cost shown in the footer.
76
+ TPS is the complete generation throughput for the agent run: all provider-reported assistant output tokens divided by the summed generation time of every LLM turn, measured from `turn_start` through the assistant `message_end`. This includes time-to-first-token, hidden reasoning, buffering, and stalls so the token count and timing cover the same interval. Tool execution between turns is excluded. A run with no output tokens or no measurable generation time is shown as `TPS —`. The `stall` segment shows occurrence count followed by accumulated duration. The optional `$ / M` value uses the model's list-price `usage.cost.total`; it is not the session's cumulative cost shown in the footer.
77
77
 
78
78
  ## Local development
79
79
 
@@ -14,8 +14,6 @@ import { resolveGlyphs } from "./icons.ts";
14
14
  import { fmtTokens, formatDuration } from "./utils.ts";
15
15
 
16
16
  const STALL_THRESHOLD_MS = 1000;
17
- const MIN_STREAM_UPDATES = 2;
18
- const MIN_MEASUREMENT_MS = 200;
19
17
 
20
18
  type TelemetryEvent =
21
19
  | AgentStartEvent
@@ -30,24 +28,16 @@ type AgentMessage = MessageStartEvent["message"];
30
28
  type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
31
29
 
32
30
  interface MessageTiming {
33
- startMs: number;
34
31
  lastUpdateMs: number;
35
32
  firstOutputMs: number | null;
36
- streamUpdateCount: number;
37
33
  inStall: boolean;
38
34
  }
39
35
 
40
- interface MessageMeasurement {
41
- outputTokens: number;
42
- durationMs: number;
43
- }
44
-
45
36
  interface TurnTiming {
46
37
  startMs: number;
47
38
  firstTokenMs: number | null;
48
39
  currentMessage: MessageTiming | null;
49
40
  messages: AssistantMessage[];
50
- measurements: MessageMeasurement[];
51
41
  generationMs: number;
52
42
  stallMs: number;
53
43
  stallCount: number;
@@ -68,11 +58,6 @@ export interface TurnTelemetry {
68
58
  measurementMs: number | null;
69
59
  }
70
60
 
71
- interface MeasuredTurn {
72
- telemetry: TurnTelemetry;
73
- measuredOutputTokens: number;
74
- }
75
-
76
61
  function isAssistantMessage(message: AgentMessage): message is AssistantMessage {
77
62
  return message.role === "assistant";
78
63
  }
@@ -86,7 +71,7 @@ export class TurnTelemetryTracker {
86
71
  private readonly now: () => number;
87
72
  private turn: TurnTiming | undefined;
88
73
  private agentStartMs: number | null = null;
89
- private agentTurns: MeasuredTurn[] = [];
74
+ private agentTurns: TurnTelemetry[] = [];
90
75
 
91
76
  constructor(now: () => number = () => performance.now()) {
92
77
  this.now = now;
@@ -127,7 +112,6 @@ export class TurnTelemetryTracker {
127
112
  firstTokenMs: null,
128
113
  currentMessage: null,
129
114
  messages: [],
130
- measurements: [],
131
115
  generationMs: 0,
132
116
  stallMs: 0,
133
117
  stallCount: 0,
@@ -138,10 +122,8 @@ export class TurnTelemetryTracker {
138
122
  if (!this.turn || !isAssistantMessage(message)) return;
139
123
  const now = this.now();
140
124
  this.turn.currentMessage = {
141
- startMs: now,
142
125
  lastUpdateMs: now,
143
126
  firstOutputMs: null,
144
- streamUpdateCount: 0,
145
127
  inStall: false,
146
128
  };
147
129
  }
@@ -163,13 +145,10 @@ export class TurnTelemetryTracker {
163
145
  if (current.firstOutputMs === null) {
164
146
  current.firstOutputMs = now;
165
147
  turn.firstTokenMs ??= now;
166
- current.streamUpdateCount = 1;
167
148
  current.lastUpdateMs = now;
168
149
  return;
169
150
  }
170
151
 
171
- current.streamUpdateCount++;
172
-
173
152
  const gap = now - current.lastUpdateMs;
174
153
  if (gap >= STALL_THRESHOLD_MS) {
175
154
  if (!current.inStall) turn.stallCount++;
@@ -188,30 +167,22 @@ export class TurnTelemetryTracker {
188
167
  const current = turn.currentMessage;
189
168
  if (current) {
190
169
  const endMs = this.now();
191
- turn.generationMs += endMs - current.startMs;
170
+ turn.generationMs = endMs - turn.startMs;
192
171
  if (current.firstOutputMs === null && message.usage.output > 0) {
193
172
  turn.firstTokenMs ??= endMs;
194
173
  }
195
- const measurementMs = current.firstOutputMs === null ? 0 : endMs - current.firstOutputMs;
196
- if (
197
- message.usage.output > 0 &&
198
- current.streamUpdateCount >= MIN_STREAM_UPDATES &&
199
- measurementMs >= MIN_MEASUREMENT_MS
200
- ) {
201
- turn.measurements.push({ outputTokens: message.usage.output, durationMs: measurementMs });
202
- }
203
174
  turn.currentMessage = null;
204
175
  }
205
176
  turn.messages.push(message);
206
177
  }
207
178
 
208
179
  private endTurnAndCollect(): TurnTelemetry | undefined {
209
- const result = this.endTurn();
210
- if (result && this.agentStartMs !== null) this.agentTurns.push(result);
211
- return result?.telemetry;
180
+ const telemetry = this.endTurn();
181
+ if (telemetry && this.agentStartMs !== null) this.agentTurns.push(telemetry);
182
+ return telemetry;
212
183
  }
213
184
 
214
- private endTurn(): MeasuredTurn | undefined {
185
+ private endTurn(): TurnTelemetry | undefined {
215
186
  const turn = this.turn;
216
187
  this.turn = undefined;
217
188
  if (!turn || turn.firstTokenMs === null || turn.messages.length === 0) return;
@@ -231,32 +202,27 @@ export class TurnTelemetryTracker {
231
202
  throw new Error("Invalid assistant usage in turn telemetry");
232
203
  }
233
204
 
234
- const measuredOutputTokens = turn.measurements.reduce((sum, measurement) => sum + measurement.outputTokens, 0);
235
- const measuredMs = turn.measurements.reduce((sum, measurement) => sum + measurement.durationMs, 0);
236
- const measurementMs = measuredMs > 0 ? measuredMs : null;
205
+ const measurementMs = outputTokens > 0 && turn.generationMs > 0 ? turn.generationMs : null;
237
206
  const tps = measurementMs === null
238
207
  ? null
239
- : round(measuredOutputTokens / (measurementMs / 1000), 1);
208
+ : round(outputTokens / (measurementMs / 1000), 1);
240
209
  const validCost = Number.isFinite(costUsd) && costUsd > 0;
241
210
  const validTokens = Number.isFinite(totalTokens) && totalTokens > 0;
242
211
  return {
243
- telemetry: {
244
- tps,
245
- ttftMs: turn.firstTokenMs - turn.startMs,
246
- totalMs: endMs - turn.startMs,
247
- inputTokens,
248
- outputTokens,
249
- stallMs: turn.stallMs,
250
- stallCount: turn.stallCount,
251
- rateUsdPerMTokens: validCost && validTokens
252
- ? round(costUsd / (totalTokens / 1_000_000), 2)
253
- : null,
254
- generationMs: turn.generationMs,
255
- totalTokens,
256
- costUsd: validCost ? costUsd : 0,
257
- measurementMs,
258
- },
259
- measuredOutputTokens,
212
+ tps,
213
+ ttftMs: turn.firstTokenMs - turn.startMs,
214
+ totalMs: endMs - turn.startMs,
215
+ inputTokens,
216
+ outputTokens,
217
+ stallMs: turn.stallMs,
218
+ stallCount: turn.stallCount,
219
+ rateUsdPerMTokens: validCost && validTokens
220
+ ? round(costUsd / (totalTokens / 1_000_000), 2)
221
+ : null,
222
+ generationMs: turn.generationMs,
223
+ totalTokens,
224
+ costUsd: validCost ? costUsd : 0,
225
+ measurementMs,
260
226
  };
261
227
  }
262
228
 
@@ -267,29 +233,28 @@ export class TurnTelemetryTracker {
267
233
  this.agentTurns = [];
268
234
  if (startMs === null || turns.length === 0) return;
269
235
 
270
- const outputTokens = turns.reduce((sum, turn) => sum + turn.telemetry.outputTokens, 0);
271
- const inputTokens = turns.reduce((sum, turn) => sum + turn.telemetry.inputTokens, 0);
272
- const totalTokens = turns.reduce((sum, turn) => sum + turn.telemetry.totalTokens, 0);
273
- const costUsd = turns.reduce((sum, turn) => sum + turn.telemetry.costUsd, 0);
274
- const stallMs = turns.reduce((sum, turn) => sum + turn.telemetry.stallMs, 0);
275
- const stallCount = turns.reduce((sum, turn) => sum + turn.telemetry.stallCount, 0);
276
- const measuredOutputTokens = turns.reduce((sum, turn) => sum + turn.measuredOutputTokens, 0);
277
- const measuredMs = turns.reduce((sum, turn) => sum + (turn.telemetry.measurementMs ?? 0), 0);
278
- const measurementMs = measuredMs > 0 ? measuredMs : null;
236
+ const outputTokens = turns.reduce((sum, turn) => sum + turn.outputTokens, 0);
237
+ const inputTokens = turns.reduce((sum, turn) => sum + turn.inputTokens, 0);
238
+ const totalTokens = turns.reduce((sum, turn) => sum + turn.totalTokens, 0);
239
+ const costUsd = turns.reduce((sum, turn) => sum + turn.costUsd, 0);
240
+ const stallMs = turns.reduce((sum, turn) => sum + turn.stallMs, 0);
241
+ const stallCount = turns.reduce((sum, turn) => sum + turn.stallCount, 0);
242
+ const generationMs = turns.reduce((sum, turn) => sum + turn.generationMs, 0);
243
+ const measurementMs = outputTokens > 0 && generationMs > 0 ? generationMs : null;
279
244
  const tps = measurementMs === null
280
245
  ? null
281
- : round(measuredOutputTokens / (measurementMs / 1000), 1);
246
+ : round(outputTokens / (measurementMs / 1000), 1);
282
247
  const validRate = costUsd > 0 && totalTokens > 0;
283
248
  return {
284
249
  tps,
285
- ttftMs: turns[0]!.telemetry.ttftMs,
250
+ ttftMs: turns[0]!.ttftMs,
286
251
  totalMs: this.now() - startMs,
287
252
  inputTokens,
288
253
  outputTokens,
289
254
  stallMs,
290
255
  stallCount,
291
256
  rateUsdPerMTokens: validRate ? round(costUsd / (totalTokens / 1_000_000), 2) : null,
292
- generationMs: turns.reduce((sum, turn) => sum + turn.telemetry.generationMs, 0),
257
+ generationMs,
293
258
  totalTokens,
294
259
  costUsd,
295
260
  measurementMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-open-tui",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "A polished TUI for Pi coding agent: animated logo header, Starship-style footer, rounded editor with model metadata, and prompt-box user messages. Combines the best of pi-haiku, pi-claude-code-tui, and pi-zentui.",
5
5
  "type": "module",
6
6
  "repository": {