pi-metrics 0.7.1 → 0.8.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 +2 -2
- package/README.zh-CN.md +2 -2
- package/SKILL.md +1 -1
- package/locales/index.json +15 -15
- package/package.json +2 -2
- package/src/config-panel.ts +252 -0
- package/src/index.ts +49 -55
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Session metrics for the [Pi coding agent](https://github.com/earendil-works/pi):
|
|
|
11
11
|
- `on-stop` (default): the transcript stays quiet during the run. When the agent fully settles (`agent_settled` — including auto-retries, compaction continuations, or Esc interruption), one summary line reports the total elapsed time, blended TPS, TTFT, summed in/out tokens, stalls, and blended cost.
|
|
12
12
|
- `live`: one line per turn, right when the turn ends (the line already carries the turn duration, so no separate elapsed notice is emitted). A multi-turn run also gets a final `⏱ <duration>` line.
|
|
13
13
|
- Telemetry for every turn is persisted as `tps` custom session entries in both modes, and restored after session resume or `/tree` navigation.
|
|
14
|
-
- Metrics are exposed through session entries and notifications. Use `/config:metrics` to open the TUI settings
|
|
14
|
+
- Metrics are exposed through session entries and notifications. Use `/config:metrics` to open the TUI settings panel, or `/config:metrics enable|disable|live|on-stop|reset` to change a setting directly.
|
|
15
15
|
|
|
16
16
|
## Configuration
|
|
17
17
|
|
|
@@ -29,7 +29,7 @@ The configuration file is `<pi-agent-dir>/extensions/pi-metrics/config.json`:
|
|
|
29
29
|
| `enabled` | `true` / `false` | `true` | Master switch. `false` registers no metric handlers at all. |
|
|
30
30
|
| `display` | `"on-stop"` / `"live"` | `"on-stop"` | When metric lines appear: one summary line after the run stops, or one line per turn. |
|
|
31
31
|
|
|
32
|
-
Use `/config:metrics` for the
|
|
32
|
+
Use `/config:metrics` for the settings panel (Enter toggles the master switch in place and opens a picker for the display timing; every change is saved and applied immediately, so no `/reload` is needed), or the direct form to change one field. Run `/reload` after editing the file by hand.
|
|
33
33
|
|
|
34
34
|
## How the summary line is computed
|
|
35
35
|
|
package/README.zh-CN.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- `on-stop`(默认):运行过程中对话区保持干净;AI 完全停止(`agent_settled`,覆盖自动重试、compaction 续跑以及 Esc 中断)后只出一行汇总:总耗时、混合 TPS、TTFT、in/out 合计、stall 和综合费率。
|
|
12
12
|
- `live`:每轮结束时立刻出一行(该行已包含本轮耗时,不再单独发耗时提示);整段超过一轮时再补一行 `⏱ <耗时>`。
|
|
13
13
|
- 两种模式都会把每轮遥测写进 `tps` custom session entry,恢复 session 或 `/tree` 后照常恢复显示。
|
|
14
|
-
- Metrics 通过 session entry 和通知提供。使用 `/config:metrics` 打开 TUI
|
|
14
|
+
- Metrics 通过 session entry 和通知提供。使用 `/config:metrics` 打开 TUI 配置面板,或用 `/config:metrics enable|disable|live|on-stop|reset` 直接改一项。
|
|
15
15
|
|
|
16
16
|
## 配置
|
|
17
17
|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
| `enabled` | `true` / `false` | `true` | 总开关;`false` 时完全不注册指标处理器。 |
|
|
30
30
|
| `display` | `"on-stop"` / `"live"` | `"on-stop"` | 指标行何时出现:整段停下后汇总一行,还是每轮一行。 |
|
|
31
31
|
|
|
32
|
-
用 `/config:metrics`
|
|
32
|
+
用 `/config:metrics` 打开配置面板(总开关回车切换,显示时机回车选;每改一项立即写盘并生效,不需要 `/reload`),或用带参数的形式改单个字段;手动修改文件后执行 `/reload`。可参考 [`config.example.json`](./config.example.json)。
|
|
33
33
|
|
|
34
34
|
## 汇总行是怎么算的
|
|
35
35
|
|
package/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: "启用与排查 pi-metrics 的耗时、TPS、TTFT、token 和成
|
|
|
5
5
|
|
|
6
6
|
# 配置 pi-metrics
|
|
7
7
|
|
|
8
|
-
配置文件为 `<Pi agent 目录>/extensions/pi-metrics/config.json`,字段有 `enabled`(默认 `true`)和 `display`(`"on-stop"` 默认 / `"live"
|
|
8
|
+
配置文件为 `<Pi agent 目录>/extensions/pi-metrics/config.json`,字段有 `enabled`(默认 `true`)和 `display`(`"on-stop"` 默认 / `"live"`)。使用 `/config:metrics` 打开 TUI 配置面板:总开关回车原地切换,显示时机回车打开二级列表;每改一项立即写盘并生效,不需要 `/reload`。也可用 `/config:metrics enable|disable|live|on-stop|reset` 直接改一项。手动修改配置文件后执行 `/reload`。
|
|
9
9
|
|
|
10
10
|
`display` 决定指标行何时出现:`on-stop` 只在整段运行停下后出一行汇总(总耗时 · 混合 TPS · TTFT · in/out · stall · 费率),`live` 每轮结束就出一行。spinner 上的实时耗时两种模式都有。
|
|
11
11
|
|
package/locales/index.json
CHANGED
|
@@ -47,17 +47,21 @@
|
|
|
47
47
|
"zh-CN": "指标配置",
|
|
48
48
|
"en-US": "Metrics settings"
|
|
49
49
|
},
|
|
50
|
-
"
|
|
51
|
-
"zh-CN": "
|
|
52
|
-
"en-US": "
|
|
50
|
+
"configLabelEnabled": {
|
|
51
|
+
"zh-CN": "启用指标",
|
|
52
|
+
"en-US": "Enable metrics"
|
|
53
53
|
},
|
|
54
|
-
"
|
|
55
|
-
"zh-CN": "
|
|
56
|
-
"en-US": "
|
|
54
|
+
"configLabelDisplay": {
|
|
55
|
+
"zh-CN": "显示时机",
|
|
56
|
+
"en-US": "Display timing"
|
|
57
57
|
},
|
|
58
|
-
"
|
|
59
|
-
"zh-CN": "
|
|
60
|
-
"en-US": "
|
|
58
|
+
"configDescEnabled": {
|
|
59
|
+
"zh-CN": "关闭后不注册任何指标处理器",
|
|
60
|
+
"en-US": "Turn off to register no metric handlers"
|
|
61
|
+
},
|
|
62
|
+
"configDescDisplay": {
|
|
63
|
+
"zh-CN": "每轮实时一行,或停下后只汇总一行",
|
|
64
|
+
"en-US": "One line per turn, or a single summary line when stopped"
|
|
61
65
|
},
|
|
62
66
|
"configDisplayLive": {
|
|
63
67
|
"zh-CN": "每轮实时一行",
|
|
@@ -75,17 +79,13 @@
|
|
|
75
79
|
"zh-CN": "关",
|
|
76
80
|
"en-US": "off"
|
|
77
81
|
},
|
|
78
|
-
"configDone": {
|
|
79
|
-
"zh-CN": "完成",
|
|
80
|
-
"en-US": "Done"
|
|
81
|
-
},
|
|
82
82
|
"configCommandInteractiveOnly": {
|
|
83
83
|
"zh-CN": "指标配置需要在 TUI 中修改;请在交互式 Pi 会话中执行命令。",
|
|
84
84
|
"en-US": "Metrics configuration requires the TUI; run this command in an interactive Pi session."
|
|
85
85
|
},
|
|
86
86
|
"configCommandSaved": {
|
|
87
|
-
"zh-CN": "指标配置已保存:{path}
|
|
88
|
-
"en-US": "Metrics configuration saved to {path}.
|
|
87
|
+
"zh-CN": "指标配置已保存:{path}。",
|
|
88
|
+
"en-US": "Metrics configuration saved to {path}."
|
|
89
89
|
},
|
|
90
90
|
"configCommandInvalid": {
|
|
91
91
|
"zh-CN": "指标配置无效:{error}",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-metrics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Pi metrics extension: elapsed-time HUD and resilient TPS telemetry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"@earendil-works/pi-tui": ">=0.80.0"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"pi-extensions-i18n": "^0.
|
|
65
|
+
"pi-extensions-i18n": "^0.9.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@earendil-works/pi-ai": "0.85.1",
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-metrics 的 TUI 配置面板。
|
|
3
|
+
*
|
|
4
|
+
* 原来用 `ctx.ui.select` 链式菜单:每选一项都要重新弹一轮菜单,屏幕上既看不出哪个是
|
|
5
|
+
* 状态、哪个是按钮。这里改用 Pi 自带 `SettingsList`:左边字段名、右边当前值,选中项
|
|
6
|
+
* 下方给一行说明。开关回车原地切换,枚举回车打开二级列表。
|
|
7
|
+
*
|
|
8
|
+
* 改动即时生效:面板每次读配置都走 `getConfig()`(入口注入的读取函数),写完由入口
|
|
9
|
+
* 保存并立刻按新配置重装指标处理器,关掉面板不用 /reload。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getSelectListTheme,
|
|
14
|
+
getSettingsListTheme,
|
|
15
|
+
type ExtensionCommandContext,
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import {
|
|
18
|
+
Container,
|
|
19
|
+
SelectList,
|
|
20
|
+
SettingsList,
|
|
21
|
+
Text,
|
|
22
|
+
type Component,
|
|
23
|
+
type SelectItem,
|
|
24
|
+
type SettingItem,
|
|
25
|
+
} from "@earendil-works/pi-tui";
|
|
26
|
+
import { parseConfig, type MetricsConfig, type MetricsDisplay } from "./config.ts";
|
|
27
|
+
import { i18n } from "./i18n.ts";
|
|
28
|
+
|
|
29
|
+
/** 面板上的一项取值方式:开关原地切换,枚举回车开二级列表。 */
|
|
30
|
+
const PANEL_KIND = {
|
|
31
|
+
/** 开关:Enter/空格在开与关之间切换。 */
|
|
32
|
+
toggle: "toggle",
|
|
33
|
+
/** 枚举:Enter 打开二级选择列表。 */
|
|
34
|
+
choice: "choice",
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
/** 面板项定义。 */
|
|
38
|
+
interface PanelItemSpec {
|
|
39
|
+
/** 配置字段名。 */
|
|
40
|
+
id: PanelFieldId;
|
|
41
|
+
/** 取值方式。 */
|
|
42
|
+
kind: (typeof PANEL_KIND)[keyof typeof PANEL_KIND];
|
|
43
|
+
/** 行标题的文案 key。 */
|
|
44
|
+
labelKey: string;
|
|
45
|
+
/** 说明行的文案 key。 */
|
|
46
|
+
descriptionKey: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 面板覆盖的配置字段名。 */
|
|
50
|
+
export type PanelFieldId = "enabled" | "display";
|
|
51
|
+
|
|
52
|
+
/** 显示时机的候选值。 */
|
|
53
|
+
const DISPLAY_VALUES: readonly MetricsDisplay[] = ["on-stop", "live"];
|
|
54
|
+
|
|
55
|
+
/** 显示时机取值到文案 key 的映射。 */
|
|
56
|
+
const DISPLAY_LABEL_KEYS: Record<MetricsDisplay, string> = {
|
|
57
|
+
"on-stop": "configDisplayOnStop",
|
|
58
|
+
live: "configDisplayLive",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** 二级列表最多同时显示几行。 */
|
|
62
|
+
const CHOICE_MENU_MAX_VISIBLE = 8;
|
|
63
|
+
|
|
64
|
+
/** 配置里存的开关值,避免把本地化文案写回配置。 */
|
|
65
|
+
const TOGGLE_ON = "on";
|
|
66
|
+
const TOGGLE_OFF = "off";
|
|
67
|
+
|
|
68
|
+
/** 面板项顺序:先总开关,再显示时机。 */
|
|
69
|
+
const PANEL_ITEMS: readonly PanelItemSpec[] = [
|
|
70
|
+
{
|
|
71
|
+
id: "enabled",
|
|
72
|
+
kind: PANEL_KIND.toggle,
|
|
73
|
+
labelKey: "configLabelEnabled",
|
|
74
|
+
descriptionKey: "configDescEnabled",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "display",
|
|
78
|
+
kind: PANEL_KIND.choice,
|
|
79
|
+
labelKey: "configLabelDisplay",
|
|
80
|
+
descriptionKey: "configDescDisplay",
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
/** 面板覆盖的配置字段名,测试用它核对没有字段漏在面板外。 */
|
|
85
|
+
export const CONFIG_PANEL_IDS: readonly string[] = PANEL_ITEMS.map((item) => item.id);
|
|
86
|
+
|
|
87
|
+
/** 二级列表的一项:label 是展示文本,value 是写回配置的值。 */
|
|
88
|
+
export interface PanelOption {
|
|
89
|
+
/** 展示文本。 */
|
|
90
|
+
label: string;
|
|
91
|
+
/** 写回配置的值。 */
|
|
92
|
+
value: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 取本地化后的开关文案。 */
|
|
96
|
+
function toggleLabels(): { on: string; off: string } {
|
|
97
|
+
return { on: i18n.t("configOn"), off: i18n.t("configOff") };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 开关的候选项。 */
|
|
101
|
+
function toggleOptions(): PanelOption[] {
|
|
102
|
+
const labels = toggleLabels();
|
|
103
|
+
return [
|
|
104
|
+
{ label: labels.on, value: TOGGLE_ON },
|
|
105
|
+
{ label: labels.off, value: TOGGLE_OFF },
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 显示时机的候选项。 */
|
|
110
|
+
function displayOptions(): PanelOption[] {
|
|
111
|
+
return DISPLAY_VALUES.map((value) => ({ label: i18n.t(DISPLAY_LABEL_KEYS[value]), value }));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 按字段取候选项。 */
|
|
115
|
+
function fieldOptions(id: PanelFieldId): PanelOption[] {
|
|
116
|
+
return id === "enabled" ? toggleOptions() : displayOptions();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 按配置值取展示文本;不在候选表里就直接显示原值。 */
|
|
120
|
+
export function optionLabelForValue(options: readonly PanelOption[], value: string): string {
|
|
121
|
+
return options.find((option) => option.value === value)?.label ?? value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 按展示文本反查写回值;找不到说明候选表变了,返回 undefined。 */
|
|
125
|
+
export function optionValueFromLabel(
|
|
126
|
+
options: readonly PanelOption[],
|
|
127
|
+
label: string,
|
|
128
|
+
): string | undefined {
|
|
129
|
+
return options.find((option) => option.label === label)?.value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 二级选择列表:Enter 选定并回传展示文本,Esc 不改动直接返回。
|
|
134
|
+
*
|
|
135
|
+
* 列表项的 value 与 label 取同一段文本:SettingsList 会把回传值直接显示在右侧,
|
|
136
|
+
* 回传展示文本才能让「每轮实时一行」这类文案在选中后仍然可读。
|
|
137
|
+
*/
|
|
138
|
+
function createChoiceSubmenu(
|
|
139
|
+
options: readonly PanelOption[],
|
|
140
|
+
currentLabel: string,
|
|
141
|
+
done: (selectedLabel?: string) => void,
|
|
142
|
+
): Component {
|
|
143
|
+
/** 建一份列表快照,能对上当前值就预选中它。 */
|
|
144
|
+
const buildList = (list: readonly PanelOption[]): SelectList => {
|
|
145
|
+
const items: SelectItem[] = list.map((option) => ({ value: option.label, label: option.label }));
|
|
146
|
+
const selectList = new SelectList(
|
|
147
|
+
items,
|
|
148
|
+
Math.max(1, Math.min(items.length, CHOICE_MENU_MAX_VISIBLE)),
|
|
149
|
+
getSelectListTheme(),
|
|
150
|
+
);
|
|
151
|
+
const currentIndex = list.findIndex((option) => option.label === currentLabel);
|
|
152
|
+
if (currentIndex >= 0) selectList.setSelectedIndex(currentIndex);
|
|
153
|
+
selectList.onSelect = (item) => done(item.value);
|
|
154
|
+
selectList.onCancel = () => done(undefined);
|
|
155
|
+
return selectList;
|
|
156
|
+
};
|
|
157
|
+
return buildList(options);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 把当前配置转成 Pi 设置列表的条目。 */
|
|
161
|
+
export function toSettingItems(config: MetricsConfig): SettingItem[] {
|
|
162
|
+
const labels = toggleLabels();
|
|
163
|
+
return PANEL_ITEMS.map((item) => {
|
|
164
|
+
const label = i18n.t(item.labelKey);
|
|
165
|
+
const description = i18n.t(item.descriptionKey);
|
|
166
|
+
const options = fieldOptions(item.id);
|
|
167
|
+
if (item.kind === PANEL_KIND.toggle) {
|
|
168
|
+
return {
|
|
169
|
+
id: item.id,
|
|
170
|
+
label,
|
|
171
|
+
description,
|
|
172
|
+
currentValue: config.enabled ? labels.on : labels.off,
|
|
173
|
+
values: [labels.on, labels.off],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
id: item.id,
|
|
178
|
+
label,
|
|
179
|
+
description,
|
|
180
|
+
currentValue: optionLabelForValue(options, config.display),
|
|
181
|
+
// 二级列表接管 Enter,避免在枚举值之间反复循环而不显示候选。
|
|
182
|
+
submenu: (currentLabel: string, done) => createChoiceSubmenu(options, currentLabel, done),
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 把面板返回的展示文本写回配置;无法识别时返回 undefined,由调用方保持不变。
|
|
189
|
+
*
|
|
190
|
+
* 结果统一过一遍 parseConfig,保证写盘的值仍在校验规则允许的范围内。
|
|
191
|
+
*/
|
|
192
|
+
export function applyPanelChange(
|
|
193
|
+
config: MetricsConfig,
|
|
194
|
+
id: string,
|
|
195
|
+
value: string,
|
|
196
|
+
): MetricsConfig | undefined {
|
|
197
|
+
if (id === "enabled") {
|
|
198
|
+
const labels = toggleLabels();
|
|
199
|
+
if (value !== labels.on && value !== labels.off) return undefined;
|
|
200
|
+
return parseConfig({ ...config, enabled: value === labels.on });
|
|
201
|
+
}
|
|
202
|
+
if (id === "display") {
|
|
203
|
+
const stored = optionValueFromLabel(displayOptions(), value);
|
|
204
|
+
return stored === undefined ? undefined : parseConfig({ ...config, display: stored });
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 面板与宿主之间的接口;配置读写与实时生效都由入口注入。 */
|
|
210
|
+
export interface ConfigPanelHandlers {
|
|
211
|
+
/** 读取当前配置。 */
|
|
212
|
+
getConfig(): MetricsConfig;
|
|
213
|
+
/** 某一项被改动后调用,由入口负责保存并按新配置重装指标处理器。 */
|
|
214
|
+
onChange(config: MetricsConfig): void;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 打开配置面板;改动即时生效,Esc 关闭。 */
|
|
218
|
+
export async function openConfigPanel(
|
|
219
|
+
ctx: ExtensionCommandContext,
|
|
220
|
+
handlers: ConfigPanelHandlers,
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
223
|
+
const container = new Container();
|
|
224
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(i18n.t("configMenuTitle"))), 1, 1));
|
|
225
|
+
|
|
226
|
+
const settingsList = new SettingsList(
|
|
227
|
+
toSettingItems(handlers.getConfig()),
|
|
228
|
+
PANEL_ITEMS.length,
|
|
229
|
+
getSettingsListTheme(),
|
|
230
|
+
(id, newValue) => {
|
|
231
|
+
const next = applyPanelChange(handlers.getConfig(), id, newValue);
|
|
232
|
+
if (next) handlers.onChange(next);
|
|
233
|
+
},
|
|
234
|
+
() => done(undefined),
|
|
235
|
+
);
|
|
236
|
+
container.addChild(settingsList);
|
|
237
|
+
|
|
238
|
+
// ctx.ui.custom 要求返回 Component:布局交给 Container,键盘转给设置列表,
|
|
239
|
+
// 列表改了数据后必须 requestRender,否则画面停在上一帧。
|
|
240
|
+
return {
|
|
241
|
+
/** 面板整体按 Container 布局渲染。 */
|
|
242
|
+
render: (width) => container.render(width),
|
|
243
|
+
/** 无本地缓存,交给 Container 清理。 */
|
|
244
|
+
invalidate: () => container.invalidate(),
|
|
245
|
+
/** 键盘输入交给设置列表,并触发一次重绘。 */
|
|
246
|
+
handleInput: (data) => {
|
|
247
|
+
settingsList.handleInput(data);
|
|
248
|
+
tui.requestRender();
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ExtensionContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { createTranslator, installNoticeRenderer, loadCatalog, notifyWithSource } from "pi-extensions-i18n";
|
|
7
|
+
import { openConfigPanel } from "./config-panel.ts";
|
|
7
8
|
import { NOTICE_SOURCE } from "./notice.ts";
|
|
8
9
|
import turnElapsed, { createElapsedTracker } from "./turn-elapsed.ts";
|
|
9
10
|
import tps from "./tps.ts";
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
loadConfig,
|
|
14
15
|
saveConfig,
|
|
15
16
|
type MetricsConfig,
|
|
16
|
-
type MetricsDisplay,
|
|
17
17
|
} from "./config.ts";
|
|
18
18
|
|
|
19
19
|
const messages = loadCatalog(new URL("../locales/index.json", import.meta.url));
|
|
@@ -27,10 +27,6 @@ const DISPLAY_ON_STOP_COMMAND = "on-stop";
|
|
|
27
27
|
const NOTICE_WARNING = "warning" as const;
|
|
28
28
|
const NOTICE_INFO = "info" as const;
|
|
29
29
|
const NOTICE_ERROR = "error" as const;
|
|
30
|
-
const DISPLAY_FLIP: Record<MetricsDisplay, MetricsDisplay> = {
|
|
31
|
-
[DISPLAY_LIVE_COMMAND]: DISPLAY_ON_STOP_COMMAND,
|
|
32
|
-
[DISPLAY_ON_STOP_COMMAND]: DISPLAY_LIVE_COMMAND,
|
|
33
|
-
};
|
|
34
30
|
|
|
35
31
|
/** 命令参数到配置片段的映射:直接改配置时要覆盖的字段。 */
|
|
36
32
|
const COMMAND_CONFIG_PATCH: ReadonlyMap<string, Partial<MetricsConfig>> = new Map([
|
|
@@ -40,11 +36,6 @@ const COMMAND_CONFIG_PATCH: ReadonlyMap<string, Partial<MetricsConfig>> = new Ma
|
|
|
40
36
|
[DISPLAY_ON_STOP_COMMAND, { display: "on-stop" }],
|
|
41
37
|
]);
|
|
42
38
|
|
|
43
|
-
/** 配置值对应的菜单文案。 */
|
|
44
|
-
function displayLabel(display: MetricsDisplay): string {
|
|
45
|
-
return i18n.t(display === "on-stop" ? "configDisplayOnStop" : "configDisplayLive");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
39
|
/** 保存配置并提示结果;写盘失败时只提示不抛出,返回是否成功。 */
|
|
49
40
|
function persistConfig(ctx: ExtensionCommandContext, config: MetricsConfig): boolean {
|
|
50
41
|
try {
|
|
@@ -77,25 +68,6 @@ function reportConfigError(ctx: ExtensionCommandContext, error: unknown): false
|
|
|
77
68
|
return false;
|
|
78
69
|
}
|
|
79
70
|
|
|
80
|
-
/**
|
|
81
|
-
* 弹出配置菜单,返回用户改动后的配置。
|
|
82
|
-
* 只调用 TUI 选择,不写盘:选择「完成」或按 Esc 时返回 null,由调用方结束编辑;
|
|
83
|
-
* 返回的配置还需要调用方自己落盘。
|
|
84
|
-
*/
|
|
85
|
-
async function promptConfigChange(
|
|
86
|
-
ctx: ExtensionCommandContext,
|
|
87
|
-
current: MetricsConfig,
|
|
88
|
-
): Promise<MetricsConfig | null> {
|
|
89
|
-
const enabledChoice = i18n.t("configEnabled", { value: i18n.t(current.enabled ? "configOn" : "configOff") });
|
|
90
|
-
const displayChoice = i18n.t("configDisplay", { value: displayLabel(current.display) });
|
|
91
|
-
const doneChoice = i18n.t("configDone");
|
|
92
|
-
const selected = await ctx.ui.select(i18n.t("configMenuTitle"), [enabledChoice, displayChoice, doneChoice]);
|
|
93
|
-
if (selected === undefined || selected === doneChoice) return null;
|
|
94
|
-
return selected === enabledChoice
|
|
95
|
-
? { ...current, enabled: !current.enabled }
|
|
96
|
-
: { ...current, display: DISPLAY_FLIP[current.display] };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
71
|
/** 取命令参数对应的配置补丁;`reset` 用整份默认配置覆盖当前值,未知参数返回 null。 */
|
|
100
72
|
function configPatchFor(value: string): Partial<MetricsConfig> | null {
|
|
101
73
|
if (value === CONFIG_RESET_COMMAND) return { ...DEFAULT_METRICS_CONFIG };
|
|
@@ -105,25 +77,30 @@ function configPatchFor(value: string): Partial<MetricsConfig> | null {
|
|
|
105
77
|
/**
|
|
106
78
|
* 应用直接参数形式的配置命令:读当前配置、叠加一个补丁后写回。
|
|
107
79
|
* 未知参数只提示用法;写盘失败由 persistConfig 自己提示,本函数不抛出。
|
|
80
|
+
* 返回生效后的配置,参数无效或写盘失败时返回 null,调用方据此决定是否重装处理器。
|
|
108
81
|
*/
|
|
109
|
-
function applyCommandSetting(value: string, ctx: ExtensionCommandContext):
|
|
82
|
+
function applyCommandSetting(value: string, ctx: ExtensionCommandContext): MetricsConfig | null {
|
|
110
83
|
const patch = configPatchFor(value);
|
|
111
84
|
if (patch === null) {
|
|
112
85
|
notifyWithSource({ ctx, source: NOTICE_SOURCE, level: NOTICE_WARNING, message: i18n.t("configCommandUsage") });
|
|
113
|
-
return;
|
|
86
|
+
return null;
|
|
114
87
|
}
|
|
115
88
|
let current: MetricsConfig;
|
|
116
89
|
try {
|
|
117
90
|
current = loadConfig();
|
|
118
91
|
} catch (error) {
|
|
119
92
|
reportConfigError(ctx, error);
|
|
120
|
-
return;
|
|
93
|
+
return null;
|
|
121
94
|
}
|
|
122
|
-
|
|
95
|
+
const next = { ...current, ...patch };
|
|
96
|
+
return persistConfig(ctx, next) ? next : null;
|
|
123
97
|
}
|
|
124
98
|
|
|
125
|
-
/**
|
|
126
|
-
function registerConfigCommand(
|
|
99
|
+
/** 注册配置命令:不带参数打开面板,带参数时直接改一项。 */
|
|
100
|
+
function registerConfigCommand(
|
|
101
|
+
pi: ExtensionAPI,
|
|
102
|
+
applyConfig: (config: MetricsConfig) => void,
|
|
103
|
+
): void {
|
|
127
104
|
const command = {
|
|
128
105
|
description: i18n.t("configCommandDescription"),
|
|
129
106
|
/** Completes the supported direct settings. */
|
|
@@ -135,32 +112,33 @@ function registerConfigCommand(pi: ExtensionAPI): void {
|
|
|
135
112
|
{ value: DISPLAY_ON_STOP_COMMAND, label: DISPLAY_ON_STOP_COMMAND },
|
|
136
113
|
],
|
|
137
114
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
115
|
+
* 处理命令:带参数时直改一项,不带参数时打开配置面板。
|
|
116
|
+
* 两种路径都在写盘后立刻把新配置应用到当前会话,不需要 /reload。
|
|
140
117
|
*/
|
|
141
118
|
handler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
142
119
|
const value = args.trim();
|
|
143
120
|
if (value) {
|
|
144
|
-
applyCommandSetting(value, ctx);
|
|
121
|
+
const next = applyCommandSetting(value, ctx);
|
|
122
|
+
if (next) applyConfig(next);
|
|
145
123
|
return;
|
|
146
124
|
}
|
|
147
125
|
if (!ctx.hasUI) {
|
|
148
126
|
notifyWithSource({ ctx, source: NOTICE_SOURCE, level: NOTICE_WARNING, message: i18n.t("configCommandInteractiveOnly") });
|
|
149
127
|
return;
|
|
150
128
|
}
|
|
151
|
-
let current: MetricsConfig;
|
|
152
129
|
try {
|
|
153
|
-
|
|
130
|
+
loadConfig();
|
|
154
131
|
} catch (error) {
|
|
155
132
|
reportConfigError(ctx, error);
|
|
156
133
|
return;
|
|
157
134
|
}
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
135
|
+
// 面板每改一项就落盘并立刻生效;Esc 关闭面板后结束。
|
|
136
|
+
await openConfigPanel(ctx, {
|
|
137
|
+
getConfig: loadConfig,
|
|
138
|
+
onChange: (config) => {
|
|
139
|
+
if (persistConfig(ctx, config)) applyConfig(config);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
164
142
|
},
|
|
165
143
|
};
|
|
166
144
|
for (const name of CONFIG_COMMAND_ALIASES) pi.registerCommand(name, command);
|
|
@@ -170,14 +148,34 @@ function registerConfigCommand(pi: ExtensionAPI): void {
|
|
|
170
148
|
export default function piHud(pi: ExtensionAPI): void {
|
|
171
149
|
// 提示画成会话区里的带底色消息块;渲染器在本包这个模块实例里注册一次。
|
|
172
150
|
installNoticeRenderer(pi);
|
|
173
|
-
|
|
174
|
-
|
|
151
|
+
|
|
152
|
+
/** 已停用的指标处理器;重装前先解引用,让旧闭包可回收。 */
|
|
153
|
+
let disposeHandlers: (() => void) | undefined;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 按一份配置重装指标处理器。
|
|
157
|
+
* 关闭时只解掉旧闭包,因此面板里关掉开关立即不再注册新处理器,打开开关立即恢复,
|
|
158
|
+
* 不需要 /reload(原来只在扩展加载时读一次配置,改了要重启会话)。
|
|
159
|
+
*/
|
|
160
|
+
const applyConfig = (next: MetricsConfig): void => {
|
|
161
|
+
disposeHandlers?.();
|
|
162
|
+
disposeHandlers = undefined;
|
|
163
|
+
if (!next.enabled) return;
|
|
164
|
+
// 两个模块共用同一个运行时钟:on-stop 汇总行里的整段耗时和 spinner 显示的是同一段。
|
|
165
|
+
const tracker = createElapsedTracker();
|
|
166
|
+
// 两个模块的注册函数都返回 void:Pi 在 /reload 时按模块清掉监听,这里只解引用。
|
|
167
|
+
turnElapsed(pi, { tracker, display: next.display });
|
|
168
|
+
tps(pi, { tracker, display: next.display });
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
registerConfigCommand(pi, applyConfig);
|
|
172
|
+
|
|
175
173
|
let configError: unknown;
|
|
176
174
|
try {
|
|
177
|
-
|
|
175
|
+
applyConfig(loadConfig());
|
|
178
176
|
} catch (error) {
|
|
179
|
-
config = { ...DEFAULT_METRICS_CONFIG };
|
|
180
177
|
configError = error;
|
|
178
|
+
applyConfig({ ...DEFAULT_METRICS_CONFIG });
|
|
181
179
|
}
|
|
182
180
|
if (configError !== undefined) {
|
|
183
181
|
pi.on("session_start", (_event, ctx: ExtensionContext) => {
|
|
@@ -192,17 +190,13 @@ export default function piHud(pi: ExtensionAPI): void {
|
|
|
192
190
|
});
|
|
193
191
|
});
|
|
194
192
|
}
|
|
195
|
-
if (!config.enabled) return;
|
|
196
|
-
// 两个模块共用同一个运行时钟:on-stop 汇总行里的整段耗时和 spinner 显示的是同一段。
|
|
197
|
-
const tracker = createElapsedTracker();
|
|
198
|
-
turnElapsed(pi, { tracker, display: config.display });
|
|
199
|
-
tps(pi, { tracker, display: config.display });
|
|
200
193
|
}
|
|
201
194
|
|
|
202
195
|
export { configPath, loadConfig, parseConfig, saveConfig } from "./config.ts";
|
|
203
196
|
export type { MetricsConfig } from "./config.ts";
|
|
204
197
|
export { default as turnElapsed } from "./turn-elapsed.ts";
|
|
205
198
|
export { default as tps } from "./tps.ts";
|
|
199
|
+
export * from "./config-panel.ts";
|
|
206
200
|
export * from "./format-utils.ts";
|
|
207
201
|
export * from "./run-summary.ts";
|
|
208
202
|
export * from "./tps.ts";
|