pi-open-tui 0.2.8 → 0.2.10
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
|
@@ -14,7 +14,7 @@ A polished TUI for [Pi](https://pi.dev) coding agent. Combines the best of pi-ha
|
|
|
14
14
|
- **Working timer** — live elapsed time while the agent is working, done duration when finished
|
|
15
15
|
- **Turn telemetry** — generation speed, TTFT, stalls, tokens, and list-price rate after each complete agent run
|
|
16
16
|
- **Zero prototype patches** — uses public Pi APIs (setHeader/setFooter/setEditorComponent), safe across Pi updates
|
|
17
|
-
- **Interactive settings UI** — `/open-tui` opens a tabbed settings dialog (
|
|
17
|
+
- **Interactive settings UI** — `/open-tui` opens a tabbed settings dialog (General / Icons / Footer / Telemetry)
|
|
18
18
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
@@ -35,6 +35,7 @@ Run `/open-tui` to open the interactive settings UI. Configuration is stored at
|
|
|
35
35
|
```json
|
|
36
36
|
{
|
|
37
37
|
"enabled": true,
|
|
38
|
+
"settingsLanguage": "en",
|
|
38
39
|
"icons": {
|
|
39
40
|
"mode": "auto"
|
|
40
41
|
},
|
|
@@ -46,7 +47,8 @@ Run `/open-tui` to open the interactive settings UI. Configuration is stored at
|
|
|
46
47
|
"runtime": true,
|
|
47
48
|
"context": true,
|
|
48
49
|
"tokens": true,
|
|
49
|
-
"cost": true
|
|
50
|
+
"cost": true,
|
|
51
|
+
"extensionStatuses": true
|
|
50
52
|
},
|
|
51
53
|
"telemetry": {
|
|
52
54
|
"enabled": true,
|
|
@@ -60,8 +62,10 @@ Run `/open-tui` to open the interactive settings UI. Configuration is stored at
|
|
|
60
62
|
}
|
|
61
63
|
```
|
|
62
64
|
|
|
65
|
+
- `settingsLanguage`: language for the `/open-tui` settings UI only; `en` or `zh`
|
|
63
66
|
- `icons.mode`: `auto` (detect Nerd Font), `nerd` (force Nerd Font glyphs), or `ascii` (plain fallbacks)
|
|
64
67
|
- `footerSegments.gitCommit`: shows short hash + tag on detached HEAD (off by default)
|
|
68
|
+
- `footerSegments.extensionStatuses`: shows statuses published by extensions through Pi's `setStatus()` API (on by default); turn it off to hide the whole status line, including MCP
|
|
65
69
|
|
|
66
70
|
## Turn telemetry
|
|
67
71
|
|
|
@@ -73,7 +77,7 @@ After each complete agent run, open-tui shows one transient notification. Tool-c
|
|
|
73
77
|
|
|
74
78
|
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
79
|
|
|
76
|
-
TPS is the
|
|
80
|
+
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
81
|
|
|
78
82
|
## Local development
|
|
79
83
|
|
|
@@ -3,6 +3,8 @@ import { join } from "node:path";
|
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { IconMode } from "./icons.ts";
|
|
5
5
|
|
|
6
|
+
export type SettingsLanguage = "en" | "zh";
|
|
7
|
+
|
|
6
8
|
export type { IconMode } from "./icons.ts";
|
|
7
9
|
|
|
8
10
|
export interface FooterSegments {
|
|
@@ -14,6 +16,7 @@ export interface FooterSegments {
|
|
|
14
16
|
context: boolean;
|
|
15
17
|
tokens: boolean;
|
|
16
18
|
cost: boolean;
|
|
19
|
+
extensionStatuses: boolean;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export interface TelemetryConfig {
|
|
@@ -28,6 +31,7 @@ export interface TelemetryConfig {
|
|
|
28
31
|
|
|
29
32
|
export interface OpenTuiConfig {
|
|
30
33
|
enabled: boolean;
|
|
34
|
+
settingsLanguage: SettingsLanguage;
|
|
31
35
|
icons: {
|
|
32
36
|
mode: IconMode;
|
|
33
37
|
};
|
|
@@ -37,6 +41,7 @@ export interface OpenTuiConfig {
|
|
|
37
41
|
|
|
38
42
|
export const DEFAULT_CONFIG: OpenTuiConfig = {
|
|
39
43
|
enabled: true,
|
|
44
|
+
settingsLanguage: "en",
|
|
40
45
|
icons: {
|
|
41
46
|
mode: "auto",
|
|
42
47
|
},
|
|
@@ -49,6 +54,7 @@ export const DEFAULT_CONFIG: OpenTuiConfig = {
|
|
|
49
54
|
context: true,
|
|
50
55
|
tokens: true,
|
|
51
56
|
cost: true,
|
|
57
|
+
extensionStatuses: true,
|
|
52
58
|
},
|
|
53
59
|
telemetry: {
|
|
54
60
|
enabled: true,
|
|
@@ -110,7 +116,11 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
|
|
|
110
116
|
try {
|
|
111
117
|
const raw = readFileSync(path, "utf8");
|
|
112
118
|
const parsed: unknown = JSON.parse(raw);
|
|
113
|
-
|
|
119
|
+
const config = deepMerge(DEFAULT_CONFIG, parsed);
|
|
120
|
+
if (config.settingsLanguage !== "en" && config.settingsLanguage !== "zh") {
|
|
121
|
+
config.settingsLanguage = DEFAULT_CONFIG.settingsLanguage;
|
|
122
|
+
}
|
|
123
|
+
return config;
|
|
114
124
|
} catch (err) {
|
|
115
125
|
notify?.(`open-tui config parse error: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
116
126
|
return structuredClone(DEFAULT_CONFIG);
|
|
@@ -262,15 +262,17 @@ export function installFooter(
|
|
|
262
262
|
|
|
263
263
|
const mainLines = [line1, line2]
|
|
264
264
|
.map((line) => truncateToWidth(line, width, theme.fg("dim", "...")));
|
|
265
|
-
return
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
265
|
+
return segments.extensionStatuses
|
|
266
|
+
? [
|
|
267
|
+
...mainLines,
|
|
268
|
+
...renderExtensionStatusLines(
|
|
269
|
+
theme,
|
|
270
|
+
footerData.getExtensionStatuses(),
|
|
271
|
+
glyphs,
|
|
272
|
+
width,
|
|
273
|
+
),
|
|
274
|
+
]
|
|
275
|
+
: mainLines;
|
|
274
276
|
},
|
|
275
277
|
};
|
|
276
278
|
});
|
|
@@ -8,23 +8,80 @@ import {
|
|
|
8
8
|
type TUI,
|
|
9
9
|
Text,
|
|
10
10
|
} from "@earendil-works/pi-tui";
|
|
11
|
-
import type { IconMode, OpenTuiConfig } from "./config.ts";
|
|
11
|
+
import type { IconMode, OpenTuiConfig, SettingsLanguage } from "./config.ts";
|
|
12
12
|
|
|
13
13
|
interface SettingItem {
|
|
14
14
|
id: string;
|
|
15
15
|
label: string;
|
|
16
16
|
currentValue: string;
|
|
17
|
-
values: string[];
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
type Tab = "features" | "icons" | "segments" | "telemetry";
|
|
21
20
|
|
|
22
|
-
const TABS:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
const TABS: Tab[] = ["features", "icons", "segments", "telemetry"];
|
|
22
|
+
|
|
23
|
+
const COPY = {
|
|
24
|
+
en: {
|
|
25
|
+
title: "Open TUI Settings",
|
|
26
|
+
tabs: { features: "General", icons: "Icons", segments: "Footer", telemetry: "Telemetry" },
|
|
27
|
+
hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change · Esc/q: close",
|
|
28
|
+
labels: {
|
|
29
|
+
enabled: "Enabled",
|
|
30
|
+
language: "Language",
|
|
31
|
+
iconMode: "Icon mode",
|
|
32
|
+
cwd: "CWD",
|
|
33
|
+
gitBranch: "Git branch",
|
|
34
|
+
gitStatus: "Git status",
|
|
35
|
+
gitCommit: "Git commit (detached)",
|
|
36
|
+
runtime: "Runtime",
|
|
37
|
+
context: "Context bar",
|
|
38
|
+
tokens: "Tokens",
|
|
39
|
+
cost: "Cost",
|
|
40
|
+
extensionStatuses: "Extension status line",
|
|
41
|
+
totalDuration: "Total duration",
|
|
42
|
+
tokenCounts: "Token counts",
|
|
43
|
+
stallDetails: "Stall details",
|
|
44
|
+
costRate: "Cost rate",
|
|
45
|
+
},
|
|
46
|
+
values: {
|
|
47
|
+
on: "On",
|
|
48
|
+
off: "Off",
|
|
49
|
+
languages: { en: "English", zh: "简体中文" },
|
|
50
|
+
icons: { auto: "Auto", nerd: "Nerd", ascii: "ASCII" },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
zh: {
|
|
54
|
+
title: "Open TUI 设置",
|
|
55
|
+
tabs: { features: "常规", icons: "图标", segments: "Footer", telemetry: "遥测" },
|
|
56
|
+
hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · Esc/q:关闭",
|
|
57
|
+
labels: {
|
|
58
|
+
enabled: "启用",
|
|
59
|
+
language: "语言",
|
|
60
|
+
iconMode: "图标模式",
|
|
61
|
+
cwd: "当前目录",
|
|
62
|
+
gitBranch: "Git 分支",
|
|
63
|
+
gitStatus: "Git 状态",
|
|
64
|
+
gitCommit: "Git 提交(分离 HEAD)",
|
|
65
|
+
runtime: "运行环境",
|
|
66
|
+
context: "上下文栏",
|
|
67
|
+
tokens: "Token",
|
|
68
|
+
cost: "费用",
|
|
69
|
+
extensionStatuses: "扩展状态行",
|
|
70
|
+
totalDuration: "总耗时",
|
|
71
|
+
tokenCounts: "Token 数量",
|
|
72
|
+
stallDetails: "停顿详情",
|
|
73
|
+
costRate: "费用速率",
|
|
74
|
+
},
|
|
75
|
+
values: {
|
|
76
|
+
on: "开启",
|
|
77
|
+
off: "关闭",
|
|
78
|
+
languages: { en: "English", zh: "简体中文" },
|
|
79
|
+
icons: { auto: "自动", nerd: "Nerd", ascii: "ASCII" },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
type SettingsCopy = (typeof COPY)[SettingsLanguage];
|
|
28
85
|
|
|
29
86
|
function toggleSetting(config: OpenTuiConfig, key: keyof OpenTuiConfig["footerSegments"]): OpenTuiConfig {
|
|
30
87
|
return {
|
|
@@ -47,6 +104,10 @@ function toggleEnabled(config: OpenTuiConfig): OpenTuiConfig {
|
|
|
47
104
|
return { ...config, enabled: !config.enabled };
|
|
48
105
|
}
|
|
49
106
|
|
|
107
|
+
function toggleLanguage(config: OpenTuiConfig): OpenTuiConfig {
|
|
108
|
+
return { ...config, settingsLanguage: config.settingsLanguage === "en" ? "zh" : "en" };
|
|
109
|
+
}
|
|
110
|
+
|
|
50
111
|
function toggleTelemetry(config: OpenTuiConfig, key: keyof OpenTuiConfig["telemetry"]): OpenTuiConfig {
|
|
51
112
|
return {
|
|
52
113
|
...config,
|
|
@@ -54,61 +115,54 @@ function toggleTelemetry(config: OpenTuiConfig, key: keyof OpenTuiConfig["teleme
|
|
|
54
115
|
};
|
|
55
116
|
}
|
|
56
117
|
|
|
57
|
-
function buildFeaturesItems(config: OpenTuiConfig): SettingItem[] {
|
|
118
|
+
function buildFeaturesItems(config: OpenTuiConfig, copy: SettingsCopy): SettingItem[] {
|
|
58
119
|
return [
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
label: "Enabled",
|
|
62
|
-
currentValue: config.enabled ? "on" : "off",
|
|
63
|
-
values: ["on", "off"],
|
|
64
|
-
},
|
|
120
|
+
{ id: "enabled", label: copy.labels.enabled, currentValue: config.enabled ? copy.values.on : copy.values.off },
|
|
121
|
+
{ id: "settingsLanguage", label: copy.labels.language, currentValue: copy.values.languages[config.settingsLanguage] },
|
|
65
122
|
];
|
|
66
123
|
}
|
|
67
124
|
|
|
68
|
-
function buildIconsItems(config: OpenTuiConfig): SettingItem[] {
|
|
69
|
-
return [
|
|
70
|
-
{
|
|
71
|
-
id: "mode",
|
|
72
|
-
label: "Icon mode",
|
|
73
|
-
currentValue: config.icons.mode,
|
|
74
|
-
values: ["auto", "nerd", "ascii"],
|
|
75
|
-
},
|
|
76
|
-
];
|
|
125
|
+
function buildIconsItems(config: OpenTuiConfig, copy: SettingsCopy): SettingItem[] {
|
|
126
|
+
return [{ id: "mode", label: copy.labels.iconMode, currentValue: copy.values.icons[config.icons.mode] }];
|
|
77
127
|
}
|
|
78
128
|
|
|
79
|
-
function buildSegmentsItems(config: OpenTuiConfig): SettingItem[] {
|
|
129
|
+
function buildSegmentsItems(config: OpenTuiConfig, copy: SettingsCopy): SettingItem[] {
|
|
80
130
|
const segs = config.footerSegments;
|
|
131
|
+
const flag = (value: boolean) => value ? copy.values.on : copy.values.off;
|
|
81
132
|
return [
|
|
82
|
-
{ id: "cwd", label:
|
|
83
|
-
{ id: "gitBranch", label:
|
|
84
|
-
{ id: "gitStatus", label:
|
|
85
|
-
{ id: "gitCommit", label:
|
|
86
|
-
{ id: "runtime", label:
|
|
87
|
-
{ id: "context", label:
|
|
88
|
-
{ id: "tokens", label:
|
|
89
|
-
{ id: "cost", label:
|
|
133
|
+
{ id: "cwd", label: copy.labels.cwd, currentValue: flag(segs.cwd) },
|
|
134
|
+
{ id: "gitBranch", label: copy.labels.gitBranch, currentValue: flag(segs.gitBranch) },
|
|
135
|
+
{ id: "gitStatus", label: copy.labels.gitStatus, currentValue: flag(segs.gitStatus) },
|
|
136
|
+
{ id: "gitCommit", label: copy.labels.gitCommit, currentValue: flag(segs.gitCommit) },
|
|
137
|
+
{ id: "runtime", label: copy.labels.runtime, currentValue: flag(segs.runtime) },
|
|
138
|
+
{ id: "context", label: copy.labels.context, currentValue: flag(segs.context) },
|
|
139
|
+
{ id: "tokens", label: copy.labels.tokens, currentValue: flag(segs.tokens) },
|
|
140
|
+
{ id: "cost", label: copy.labels.cost, currentValue: flag(segs.cost) },
|
|
141
|
+
{ id: "extensionStatuses", label: copy.labels.extensionStatuses, currentValue: flag(segs.extensionStatuses) },
|
|
90
142
|
];
|
|
91
143
|
}
|
|
92
144
|
|
|
93
|
-
function buildTelemetryItems(config: OpenTuiConfig): SettingItem[] {
|
|
145
|
+
function buildTelemetryItems(config: OpenTuiConfig, copy: SettingsCopy): SettingItem[] {
|
|
94
146
|
const telemetry = config.telemetry;
|
|
147
|
+
const flag = (value: boolean) => value ? copy.values.on : copy.values.off;
|
|
95
148
|
return [
|
|
96
|
-
{ id: "enabled", label:
|
|
97
|
-
{ id: "tps", label: "TPS", currentValue: telemetry.tps
|
|
98
|
-
{ id: "ttft", label: "TTFT", currentValue: telemetry.ttft
|
|
99
|
-
{ id: "duration", label:
|
|
100
|
-
{ id: "tokens", label:
|
|
101
|
-
{ id: "stalls", label:
|
|
102
|
-
{ id: "cost", label:
|
|
149
|
+
{ id: "enabled", label: copy.labels.enabled, currentValue: flag(telemetry.enabled) },
|
|
150
|
+
{ id: "tps", label: "TPS", currentValue: flag(telemetry.tps) },
|
|
151
|
+
{ id: "ttft", label: "TTFT", currentValue: flag(telemetry.ttft) },
|
|
152
|
+
{ id: "duration", label: copy.labels.totalDuration, currentValue: flag(telemetry.duration) },
|
|
153
|
+
{ id: "tokens", label: copy.labels.tokenCounts, currentValue: flag(telemetry.tokens) },
|
|
154
|
+
{ id: "stalls", label: copy.labels.stallDetails, currentValue: flag(telemetry.stalls) },
|
|
155
|
+
{ id: "cost", label: copy.labels.costRate, currentValue: flag(telemetry.cost) },
|
|
103
156
|
];
|
|
104
157
|
}
|
|
105
158
|
|
|
106
159
|
function buildItems(tab: Tab, config: OpenTuiConfig): SettingItem[] {
|
|
160
|
+
const copy = COPY[config.settingsLanguage];
|
|
107
161
|
switch (tab) {
|
|
108
|
-
case "features": return buildFeaturesItems(config);
|
|
109
|
-
case "icons": return buildIconsItems(config);
|
|
110
|
-
case "segments": return buildSegmentsItems(config);
|
|
111
|
-
case "telemetry": return buildTelemetryItems(config);
|
|
162
|
+
case "features": return buildFeaturesItems(config, copy);
|
|
163
|
+
case "icons": return buildIconsItems(config, copy);
|
|
164
|
+
case "segments": return buildSegmentsItems(config, copy);
|
|
165
|
+
case "telemetry": return buildTelemetryItems(config, copy);
|
|
112
166
|
}
|
|
113
167
|
}
|
|
114
168
|
|
|
@@ -117,12 +171,11 @@ function handleSettingChange(
|
|
|
117
171
|
itemId: string,
|
|
118
172
|
config: OpenTuiConfig,
|
|
119
173
|
): OpenTuiConfig {
|
|
120
|
-
if (tab === "features"
|
|
121
|
-
return toggleEnabled(config);
|
|
122
|
-
|
|
123
|
-
if (tab === "icons" && itemId === "mode") {
|
|
124
|
-
return cycleIconMode(config);
|
|
174
|
+
if (tab === "features") {
|
|
175
|
+
if (itemId === "enabled") return toggleEnabled(config);
|
|
176
|
+
if (itemId === "settingsLanguage") return toggleLanguage(config);
|
|
125
177
|
}
|
|
178
|
+
if (tab === "icons" && itemId === "mode") return cycleIconMode(config);
|
|
126
179
|
if (tab === "segments") {
|
|
127
180
|
return toggleSetting(config, itemId as keyof OpenTuiConfig["footerSegments"]);
|
|
128
181
|
}
|
|
@@ -149,6 +202,7 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
149
202
|
private readonly onClose: () => void;
|
|
150
203
|
private cachedWidth: number | undefined;
|
|
151
204
|
private cachedLines: string[] | undefined;
|
|
205
|
+
private compact = false;
|
|
152
206
|
|
|
153
207
|
constructor(
|
|
154
208
|
theme: Theme,
|
|
@@ -171,22 +225,36 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
171
225
|
this.rebuild();
|
|
172
226
|
}
|
|
173
227
|
|
|
228
|
+
private applySetting(itemId: string): void {
|
|
229
|
+
this.selectedItemByTab[this.tab] = itemId;
|
|
230
|
+
this.config = handleSettingChange(this.tab, itemId, this.config);
|
|
231
|
+
this.onChange(this.config);
|
|
232
|
+
this.rebuild(itemId);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private switchTab(offset: number): void {
|
|
236
|
+
const idx = TABS.indexOf(this.tab);
|
|
237
|
+
this.tab = TABS[(idx + offset + TABS.length) % TABS.length]!;
|
|
238
|
+
this.rebuild();
|
|
239
|
+
}
|
|
240
|
+
|
|
174
241
|
private rebuild(preferredItemId = this.selectedItemByTab[this.tab]): void {
|
|
242
|
+
const copy = COPY[this.config.settingsLanguage];
|
|
175
243
|
this.container.clear();
|
|
176
|
-
this.container.addChild(new Text(this.theme.bold(this.theme.fg("accent",
|
|
244
|
+
this.container.addChild(new Text(this.theme.bold(this.theme.fg("accent", copy.title)), 1, 0));
|
|
177
245
|
|
|
178
|
-
const tabBar = TABS.map((
|
|
179
|
-
const active =
|
|
180
|
-
const label = active ? `[${
|
|
246
|
+
const tabBar = TABS.map((tab) => {
|
|
247
|
+
const active = tab === this.tab;
|
|
248
|
+
const label = active ? `[${copy.tabs[tab]}]` : ` ${copy.tabs[tab]} `;
|
|
181
249
|
return active ? this.theme.fg("accent", label) : this.theme.fg("dim", label);
|
|
182
250
|
}).join(" ");
|
|
183
251
|
this.container.addChild(new Text(tabBar, 1, 0));
|
|
184
|
-
this.container.addChild(new Text(this.theme.fg("dim",
|
|
252
|
+
this.container.addChild(new Text(this.theme.fg("dim", copy.hint), 1, 0));
|
|
185
253
|
|
|
186
254
|
const items = buildItems(this.tab, this.config).map((item) => ({
|
|
187
255
|
value: item.id,
|
|
188
|
-
label: item.label,
|
|
189
|
-
description: item.currentValue,
|
|
256
|
+
label: this.compact ? `${item.label}: ${item.currentValue}` : item.label,
|
|
257
|
+
description: this.compact ? undefined : item.currentValue,
|
|
190
258
|
} as SelectItem));
|
|
191
259
|
this.selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
192
260
|
selectedPrefix: (t) => this.theme.fg("accent", t),
|
|
@@ -204,11 +272,7 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
204
272
|
this.selectedItemByTab[this.tab] = item.value;
|
|
205
273
|
};
|
|
206
274
|
this.selectList.onSelect = (item) => {
|
|
207
|
-
this.
|
|
208
|
-
this.config = handleSettingChange(this.tab, item.value, this.config);
|
|
209
|
-
this.onChange(this.config);
|
|
210
|
-
this.rebuild(item.value);
|
|
211
|
-
this.invalidate();
|
|
275
|
+
this.applySetting(item.value);
|
|
212
276
|
};
|
|
213
277
|
this.selectList.onCancel = () => {
|
|
214
278
|
this.onClose();
|
|
@@ -220,30 +284,35 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
220
284
|
}
|
|
221
285
|
|
|
222
286
|
handleInput(data: string): void {
|
|
223
|
-
if (matchesKey(data, Key.tab)) {
|
|
224
|
-
|
|
225
|
-
this.tab = TABS[(idx + 1) % TABS.length]!.id;
|
|
226
|
-
this.rebuild();
|
|
287
|
+
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
|
|
288
|
+
this.switchTab(1);
|
|
227
289
|
this.invalidate();
|
|
228
290
|
return;
|
|
229
291
|
}
|
|
230
|
-
if (matchesKey(data, Key.shift("tab"))) {
|
|
231
|
-
|
|
232
|
-
this.tab = TABS[(idx - 1 + TABS.length) % TABS.length]!.id;
|
|
233
|
-
this.rebuild();
|
|
292
|
+
if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) {
|
|
293
|
+
this.switchTab(-1);
|
|
234
294
|
this.invalidate();
|
|
235
295
|
return;
|
|
236
296
|
}
|
|
237
|
-
if (matchesKey(data, Key.escape)) {
|
|
297
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, "q")) {
|
|
238
298
|
this.onClose();
|
|
239
299
|
return;
|
|
240
300
|
}
|
|
241
|
-
|
|
242
|
-
|
|
301
|
+
if (matchesKey(data, Key.space) || data === " ") {
|
|
302
|
+
const selected = this.selectList.getSelectedItem();
|
|
303
|
+
if (selected) this.applySetting(selected.value);
|
|
304
|
+
} else {
|
|
305
|
+
this.selectList.handleInput?.(data);
|
|
306
|
+
}
|
|
243
307
|
this.invalidate();
|
|
244
308
|
}
|
|
245
309
|
|
|
246
310
|
render(width: number): string[] {
|
|
311
|
+
const compact = width <= 60;
|
|
312
|
+
if (compact !== this.compact) {
|
|
313
|
+
this.compact = compact;
|
|
314
|
+
this.rebuild();
|
|
315
|
+
}
|
|
247
316
|
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
248
317
|
this.cachedWidth = width;
|
|
249
318
|
this.cachedLines = this.container.render(width);
|
|
@@ -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:
|
|
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
|
|
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
|
|
210
|
-
if (
|
|
211
|
-
return
|
|
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():
|
|
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
|
|
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(
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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.
|
|
271
|
-
const inputTokens = turns.reduce((sum, turn) => sum + turn.
|
|
272
|
-
const totalTokens = turns.reduce((sum, turn) => sum + turn.
|
|
273
|
-
const costUsd = turns.reduce((sum, turn) => sum + turn.
|
|
274
|
-
const stallMs = turns.reduce((sum, turn) => sum + turn.
|
|
275
|
-
const stallCount = turns.reduce((sum, turn) => sum + turn.
|
|
276
|
-
const
|
|
277
|
-
const
|
|
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(
|
|
246
|
+
: round(outputTokens / (measurementMs / 1000), 1);
|
|
282
247
|
const validRate = costUsd > 0 && totalTokens > 0;
|
|
283
248
|
return {
|
|
284
249
|
tps,
|
|
285
|
-
ttftMs: turns[0]!.
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.2.10",
|
|
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": {
|