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
|
|
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
|
-
|
|
154
|
+
theme: Theme,
|
|
152
155
|
config: OpenTuiConfig,
|
|
153
|
-
|
|
154
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
173
|
-
current.
|
|
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
|
-
|
|
201
|
-
turn.
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
?
|
|
318
|
-
:
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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": {
|