pi-open-tui 0.2.7 → 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 excludes time-to-first-token, detected stalls, and tool execution between messages. In a multi-turn agent run, it is weighted from the turns with measurable streaming spans; an unmeasurable tool turn no longer hides a measurable final response. A rate that cannot be identified reliably 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.
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
 
@@ -95,3 +95,5 @@ This project builds on the work of several Pi community packages:
95
95
  - **[pi-tps](https://github.com/monotykamary/pi-tps)** — the turn timing, stall detection, and conservative TPS measurement approach
96
96
 
97
97
  The animated logo frames are derived from `pi-claude-code-tui`, which in turn derive from Pi's official install script (`pi.dev/install.sh`). The runtime detection list and git porcelain parsing borrow structure from `pi-zentui`.
98
+
99
+ Special thanks to the **[LINUX DO](https://linux.do)** community for their support.
@@ -144,16 +144,22 @@ class SettingsUi implements SettingsUiHandle {
144
144
  private selectList: SelectList;
145
145
  private readonly selectedItemByTab: Partial<Record<Tab, string>> = {};
146
146
  private readonly container: Box;
147
+ private readonly theme: Theme;
148
+ private readonly onChange: (config: OpenTuiConfig) => void;
149
+ private readonly onClose: () => void;
147
150
  private cachedWidth: number | undefined;
148
151
  private cachedLines: string[] | undefined;
149
152
 
150
153
  constructor(
151
- private readonly theme: Theme,
154
+ theme: Theme,
152
155
  config: OpenTuiConfig,
153
- private readonly onChange: (config: OpenTuiConfig) => void,
154
- private readonly onClose: () => void,
156
+ onChange: (config: OpenTuiConfig) => void,
157
+ onClose: () => void,
155
158
  ) {
159
+ this.theme = theme;
156
160
  this.config = config;
161
+ this.onChange = onChange;
162
+ this.onClose = onClose;
157
163
  this.container = new Box(1, 1, (s: string) => theme.bg("customMessageBg", s));
158
164
  this.selectList = new SelectList([], 12, {
159
165
  selectedPrefix: (t) => theme.fg("accent", t),
@@ -13,13 +13,7 @@ import type { IconMode, TelemetryConfig } from "./config.ts";
13
13
  import { resolveGlyphs } from "./icons.ts";
14
14
  import { fmtTokens, formatDuration } from "./utils.ts";
15
15
 
16
- const STALL_THRESHOLD_MS = 500;
17
- const MIN_STREAM_MS = 1;
18
- const MIN_STREAM_UPDATES = 5;
19
- const MIN_INTER_CHUNK_MS = 1;
20
- const MIN_GENERATION_MS = 200;
21
- const STALL_DOMINANCE_RATIO = 0.85;
22
- const MAX_PLAUSIBLE_TPS = 10_000;
16
+ const STALL_THRESHOLD_MS = 1000;
23
17
 
24
18
  type TelemetryEvent =
25
19
  | AgentStartEvent
@@ -34,12 +28,8 @@ type AgentMessage = MessageStartEvent["message"];
34
28
  type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
35
29
 
36
30
  interface MessageTiming {
37
- startMs: number;
38
31
  lastUpdateMs: number;
39
- firstUpdateSeen: boolean;
40
- streamUpdateCount: number;
41
- firstStreamUpdateMs: number | null;
42
- lastStreamUpdateMs: number | null;
32
+ firstOutputMs: number | null;
43
33
  inStall: boolean;
44
34
  }
45
35
 
@@ -49,13 +39,8 @@ interface TurnTiming {
49
39
  currentMessage: MessageTiming | null;
50
40
  messages: AssistantMessage[];
51
41
  generationMs: number;
52
- streamMs: number;
53
- streamIntervals: number;
54
- streamUpdateCount: number;
55
42
  stallMs: number;
56
43
  stallCount: number;
57
- isToolCall: boolean;
58
- modelKey: string | null;
59
44
  }
60
45
 
61
46
  export interface TurnTelemetry {
@@ -84,7 +69,6 @@ function round(value: number, decimals: number): number {
84
69
 
85
70
  export class TurnTelemetryTracker {
86
71
  private readonly now: () => number;
87
- private readonly tpsCaps = new Map<string, number>();
88
72
  private turn: TurnTiming | undefined;
89
73
  private agentStartMs: number | null = null;
90
74
  private agentTurns: TurnTelemetry[] = [];
@@ -116,7 +100,6 @@ export class TurnTelemetryTracker {
116
100
  this.endMessage(event.message);
117
101
  return;
118
102
  case "tool_execution_start":
119
- if (this.turn) this.turn.isToolCall = true;
120
103
  return;
121
104
  case "turn_end":
122
105
  return this.endTurnAndCollect();
@@ -130,13 +113,8 @@ export class TurnTelemetryTracker {
130
113
  currentMessage: null,
131
114
  messages: [],
132
115
  generationMs: 0,
133
- streamMs: 0,
134
- streamIntervals: 0,
135
- streamUpdateCount: 0,
136
116
  stallMs: 0,
137
117
  stallCount: 0,
138
- isToolCall: false,
139
- modelKey: null,
140
118
  };
141
119
  }
142
120
 
@@ -144,15 +122,10 @@ export class TurnTelemetryTracker {
144
122
  if (!this.turn || !isAssistantMessage(message)) return;
145
123
  const now = this.now();
146
124
  this.turn.currentMessage = {
147
- startMs: now,
148
125
  lastUpdateMs: now,
149
- firstUpdateSeen: false,
150
- streamUpdateCount: 0,
151
- firstStreamUpdateMs: null,
152
- lastStreamUpdateMs: null,
126
+ firstOutputMs: null,
153
127
  inStall: false,
154
128
  };
155
- this.turn.modelKey ??= `${message.provider}:${message.model}`;
156
129
  }
157
130
 
158
131
  private updateMessage(event: MessageUpdateEvent): void {
@@ -169,17 +142,13 @@ export class TurnTelemetryTracker {
169
142
  if (!turn || !current || !isAssistantMessage(message)) return;
170
143
 
171
144
  const now = this.now();
172
- if (!current.firstUpdateSeen) {
173
- current.firstUpdateSeen = true;
145
+ if (current.firstOutputMs === null) {
146
+ current.firstOutputMs = now;
174
147
  turn.firstTokenMs ??= now;
175
148
  current.lastUpdateMs = now;
176
149
  return;
177
150
  }
178
151
 
179
- current.streamUpdateCount++;
180
- current.firstStreamUpdateMs ??= now;
181
- current.lastStreamUpdateMs = now;
182
-
183
152
  const gap = now - current.lastUpdateMs;
184
153
  if (gap >= STALL_THRESHOLD_MS) {
185
154
  if (!current.inStall) turn.stallCount++;
@@ -197,17 +166,14 @@ export class TurnTelemetryTracker {
197
166
 
198
167
  const current = turn.currentMessage;
199
168
  if (current) {
200
- turn.generationMs += this.now() - current.startMs;
201
- turn.streamUpdateCount += current.streamUpdateCount;
202
- turn.streamIntervals += Math.max(0, current.streamUpdateCount - 1);
203
- // Sum per-message spans so tool execution between messages never enters TPS.
204
- if (current.firstStreamUpdateMs !== null && current.lastStreamUpdateMs !== null) {
205
- turn.streamMs += current.lastStreamUpdateMs - current.firstStreamUpdateMs;
169
+ const endMs = this.now();
170
+ turn.generationMs = endMs - turn.startMs;
171
+ if (current.firstOutputMs === null && message.usage.output > 0) {
172
+ turn.firstTokenMs ??= endMs;
206
173
  }
207
174
  turn.currentMessage = null;
208
175
  }
209
176
  turn.messages.push(message);
210
- turn.modelKey ??= `${message.provider}:${message.model}`;
211
177
  }
212
178
 
213
179
  private endTurnAndCollect(): TurnTelemetry | undefined {
@@ -235,49 +201,11 @@ export class TurnTelemetryTracker {
235
201
  if (![inputTokens, outputTokens, totalTokens, costUsd].every(Number.isFinite)) {
236
202
  throw new Error("Invalid assistant usage in turn telemetry");
237
203
  }
238
- if (outputTokens <= 0 || !turn.modelKey) return;
239
-
240
- const streamMs = turn.streamUpdateCount > 0 ? turn.streamMs : null;
241
- const averageGapMs = turn.streamIntervals > 0 ? turn.streamMs / turn.streamIntervals : 0;
242
- let tps: number | null = null;
243
- let measurementMs: number | null = null;
244
- let primary = false;
245
- if (
246
- streamMs !== null &&
247
- streamMs >= MIN_STREAM_MS &&
248
- turn.streamUpdateCount >= MIN_STREAM_UPDATES &&
249
- averageGapMs >= MIN_INTER_CHUNK_MS &&
250
- turn.stallMs < streamMs &&
251
- streamMs - turn.stallMs >= MIN_GENERATION_MS &&
252
- turn.stallMs < streamMs - turn.stallMs
253
- ) {
254
- measurementMs = streamMs - turn.stallMs;
255
- tps = round(outputTokens / (measurementMs / 1000), 1);
256
- primary = true;
257
- } else if (turn.streamUpdateCount >= 2 && turn.generationMs >= MIN_GENERATION_MS) {
258
- let activeMs = turn.generationMs - turn.stallMs;
259
- if (activeMs < MIN_GENERATION_MS || turn.stallMs > turn.generationMs * STALL_DOMINANCE_RATIO) {
260
- activeMs = turn.generationMs - turn.stallMs / 2;
261
- }
262
- measurementMs = Math.max(activeMs, MIN_GENERATION_MS);
263
- tps = round(outputTokens / (measurementMs / 1000), 1);
264
- }
265
-
266
- if (tps !== null && tps > MAX_PLAUSIBLE_TPS) {
267
- tps = null;
268
- measurementMs = null;
269
- primary = false;
270
- }
271
- if (primary && tps !== null) {
272
- this.tpsCaps.set(turn.modelKey, Math.max(tps, this.tpsCaps.get(turn.modelKey) ?? 0));
273
- }
274
- if (turn.isToolCall && tps !== null) {
275
- tps = this.tpsCaps.has(turn.modelKey)
276
- ? Math.min(tps, this.tpsCaps.get(turn.modelKey)!)
277
- : null;
278
- if (tps === null) measurementMs = null;
279
- }
280
204
 
205
+ const measurementMs = outputTokens > 0 && turn.generationMs > 0 ? turn.generationMs : null;
206
+ const tps = measurementMs === null
207
+ ? null
208
+ : round(outputTokens / (measurementMs / 1000), 1);
281
209
  const validCost = Number.isFinite(costUsd) && costUsd > 0;
282
210
  const validTokens = Number.isFinite(totalTokens) && totalTokens > 0;
283
211
  return {
@@ -311,19 +239,14 @@ export class TurnTelemetryTracker {
311
239
  const costUsd = turns.reduce((sum, turn) => sum + turn.costUsd, 0);
312
240
  const stallMs = turns.reduce((sum, turn) => sum + turn.stallMs, 0);
313
241
  const stallCount = turns.reduce((sum, turn) => sum + turn.stallCount, 0);
314
- const measuredTurns = turns.filter((turn) => turn.measurementMs !== null);
315
- const measuredOutputTokens = measuredTurns.reduce((sum, turn) => sum + turn.outputTokens, 0);
316
- const measurementMs = measuredTurns.length > 0
317
- ? measuredTurns.reduce((sum, turn) => sum + turn.measurementMs!, 0)
318
- : null;
319
- const tps =
320
- measurementMs !== null && measurementMs >= MIN_GENERATION_MS
321
- ? round(measuredOutputTokens / (measurementMs / 1000), 1)
322
- : null;
323
- const boundedTps = tps !== null && tps <= MAX_PLAUSIBLE_TPS ? tps : null;
242
+ const generationMs = turns.reduce((sum, turn) => sum + turn.generationMs, 0);
243
+ const measurementMs = outputTokens > 0 && generationMs > 0 ? generationMs : null;
244
+ const tps = measurementMs === null
245
+ ? null
246
+ : round(outputTokens / (measurementMs / 1000), 1);
324
247
  const validRate = costUsd > 0 && totalTokens > 0;
325
248
  return {
326
- tps: boundedTps,
249
+ tps,
327
250
  ttftMs: turns[0]!.ttftMs,
328
251
  totalMs: this.now() - startMs,
329
252
  inputTokens,
@@ -331,7 +254,7 @@ export class TurnTelemetryTracker {
331
254
  stallMs,
332
255
  stallCount,
333
256
  rateUsdPerMTokens: validRate ? round(costUsd / (totalTokens / 1_000_000), 2) : null,
334
- generationMs: turns.reduce((sum, turn) => sum + turn.generationMs, 0),
257
+ generationMs,
335
258
  totalTokens,
336
259
  costUsd,
337
260
  measurementMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-open-tui",
3
- "version": "0.2.7",
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": {
@@ -24,7 +24,7 @@
24
24
  "LICENSE"
25
25
  ],
26
26
  "scripts": {
27
- "test": "node --test --experimental-transform-types tests/settings-command.test.ts tests/footer.test.ts tests/editor.test.ts tests/telemetry.test.ts",
27
+ "test": "node --test tests/settings-command.test.ts tests/footer.test.ts tests/editor.test.ts tests/telemetry.test.ts",
28
28
  "typecheck": "tsc --noEmit"
29
29
  },
30
30
  "pi": {