pi-thinking-header 0.3.3 → 0.4.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 +35 -16
- package/index.js +185 -28
- package/install-patch.mjs +30 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,12 +4,15 @@ dsh 风格的 pi thinking 显示插件:带 token 计数的单行标题 + 折
|
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
展开时: 折叠时(ctrl+t / 点击标题行):
|
|
7
|
-
Thinking
|
|
8
|
-
|
|
9
|
-
正式回复…
|
|
7
|
+
流式中:Thinking… 流式中:Thinking (~2.50k tokens) · …尾随最新内容保持可见
|
|
8
|
+
结束后:Thinking (~2.50k tokens) 结束后:Thinking (~2.50k tokens) · 第一行内容…
|
|
9
|
+
模型思考全文…(流式中仅最后一行刷新) 正式回复…
|
|
10
|
+
正式回复…
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
- token 数量为本地估算:`ceil(chars / 4)
|
|
13
|
+
- token 数量为本地估算:`ceil(chars / 4)`。折叠预览中流式实时增长;展开时标题行
|
|
14
|
+
在流式期间保持稳定(只显示 `Thinking…`),块结束后才显示最终计数——pi 按整行连续
|
|
15
|
+
区间重绘,标题行的计数若每个 chunk 都变化,会把前面的思考行与最新行一起擦除重绘
|
|
13
16
|
- 小数点位数可配置(默认 2 位):`/thinking-header decimals <0-6>`,或直接编辑
|
|
14
17
|
配置文件 `~/.pi/agent/thinking-header.json`(`{"decimals": 2}`)。例如默认效果
|
|
15
18
|
`2.50k`;`decimals: 0` → `3k`;`decimals: 4` → `2.5000k`。两种模式均实时读取
|
|
@@ -39,17 +42,26 @@ pi install ./pi-thinking-header
|
|
|
39
42
|
所有功能的统一入口:
|
|
40
43
|
|
|
41
44
|
```
|
|
42
|
-
/thinking-header # 状态总览(模式/补丁状态/decimals)+ 用法
|
|
45
|
+
/thinking-header # 状态总览(模式/补丁状态/autoPatch/decimals)+ 用法
|
|
43
46
|
/thinking-header patch # 应用/修复完整模式补丁(确认后执行)
|
|
47
|
+
/thinking-header autopatch # 查看启动自动补丁状态
|
|
48
|
+
/thinking-header autopatch on|off# 启动时自动检查并应用/迁移补丁(默认 on)
|
|
44
49
|
/thinking-header decimals # 查看当前小数位
|
|
45
50
|
/thinking-header decimals <0-6> # 设置小数位并持久化(默认 2)
|
|
46
51
|
```
|
|
47
52
|
|
|
53
|
+
启动自动补丁(`autopatch`,默认开启):每次会话启动(`session_start`)自动探测磁盘上的
|
|
54
|
+
bundle——未打补丁时自动应用、检测到旧版本 patch 时自动迁移(installer 自带备份恢复逻辑),
|
|
55
|
+
已是最新的则静默跳过。补丁写入磁盘后需重启 pi 才生效:当前会话仍用降级/旧渲染,notify
|
|
56
|
+
会提示。因此 `pi update` 之后补丁丢失、或插件包更新带来新 patch 版本时,都无需手动
|
|
57
|
+
处理,下次启动自动修复。关闭方式:`/thinking-header autopatch off`,或直接编辑配置文件
|
|
58
|
+
(`{"autoPatch": false}`)。
|
|
59
|
+
|
|
48
60
|
补丁安装(`patch` 子命令,内部运行 `node install-patch.mjs`):
|
|
49
61
|
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
- `pi update` 之后补丁丢失时,extension
|
|
62
|
+
- 已是当前版本 → 提示 already applied(含当前 decimals)
|
|
63
|
+
- 未打补丁(或补丁版本较旧 → 迁移)→ 确认后执行安装/迁移,输出结果并提醒重启 pi(当前会话仍是旧代码,重启后生效)
|
|
64
|
+
- `pi update` 之后补丁丢失时,extension 自动进入降级模式;启动自动补丁(默认开)会在下次会话开始时自动修复,也可用该命令立即修复
|
|
53
65
|
|
|
54
66
|
小数位配置(`decimals` 子命令):
|
|
55
67
|
|
|
@@ -64,7 +76,8 @@ pi install ./pi-thinking-header
|
|
|
64
76
|
|
|
65
77
|
pi 的 extension API 无法接管内置 thinking 块的折叠渲染,完整效果需要附带
|
|
66
78
|
的 bundle patch。推荐直接在 pi 会话内运行 `/thinking-header patch`
|
|
67
|
-
|
|
79
|
+
(自动定位安装位置);也可什么都不做——默认开启的启动自动补丁会在首次
|
|
80
|
+
会话开始时自动应用。手动执行:
|
|
68
81
|
|
|
69
82
|
```bash
|
|
70
83
|
# npm 安装:~/.pi/agent/npm/pi-thinking-header/install-patch.mjs
|
|
@@ -73,13 +86,17 @@ node <包目录>/install-patch.mjs
|
|
|
73
86
|
```
|
|
74
87
|
|
|
75
88
|
- 每条消息独立的 token 计数与预览(含历史消息)
|
|
89
|
+
- 展开态标题行流式期间字节级稳定(`Thinking…`):折叠更新只重绘最后一行,
|
|
90
|
+
之前的思考行不会被一起擦除重绘;块结束后标题行才出现最终计数(回复流式时
|
|
91
|
+
已结束的思考块直接显示最终计数)
|
|
76
92
|
- 折叠态严格单行、宽度感知截断、点击切换
|
|
77
93
|
- 流式期间折叠预览滚动跟随最新行行尾(dsh 同款),结束后回到第一行
|
|
78
94
|
- 小数点位数渲染时实时读取配置文件(mtime 缓存),与降级模式共享同一份配置
|
|
79
|
-
- 幂等;自动从 v1/v2/v3/v3.1 patch 迁移(恢复原始备份后重新应用;早期 v3
|
|
95
|
+
- 幂等;自动从 v1/v2/v3/v3.1/v3.2 patch 迁移(恢复原始备份后重新应用;早期 v3
|
|
80
96
|
折叠组件缺 `invalidate()`、退出/切换模式时触发 `this.child.invalidate is not
|
|
81
|
-
a function` 崩溃且无备份时,会先原地热修复);`pi update`
|
|
82
|
-
|
|
97
|
+
a function` 崩溃且无备份时,会先原地热修复);`pi update` 后无需手动处理——
|
|
98
|
+
启动自动补丁(默认开)会在下次会话开始时重新应用/迁移;插件包更新后同样如此
|
|
99
|
+
(patch 版本随插件更新时自动迁移到新版)
|
|
83
100
|
- 回滚:
|
|
84
101
|
```bash
|
|
85
102
|
cp <chunk路径>.pre-thinking-header.bak <chunk路径>
|
|
@@ -89,11 +106,13 @@ node <包目录>/install-patch.mjs
|
|
|
89
106
|
### 纯插件模式:extension 自动降级
|
|
90
107
|
|
|
91
108
|
extension 启动时探测运行中的 pi bundle:**检测到任意版本的 patch 时**(含旧版本)
|
|
92
|
-
extension
|
|
93
|
-
|
|
109
|
+
extension 保持惰性(避免双标题;旧版本会在下次会话启动时被自动迁移到当前版本,
|
|
110
|
+
或运行 `/thinking-header patch` 立即迁移);**未检测到 patch 时**自动进入降级模式,
|
|
111
|
+
并在会话启动时自动应用补丁(默认行为,可关闭):
|
|
94
112
|
|
|
95
113
|
- 展开的 thinking:通过 markdown transformer 前置 `Thinking (~N tokens)` 标题行
|
|
96
|
-
(按块正确计数,`pi update`
|
|
114
|
+
(按块正确计数,`pi update` 后依然有效,无需维护)。流式期间同样保持稳定
|
|
115
|
+
(`Thinking…`),块结束后显示最终计数,避免正文旧行被一起重绘
|
|
97
116
|
- 折叠的 thinking:pi 的 extension API 只暴露一个全局 label,插件会在每条
|
|
98
117
|
assistant 消息结束后将其更新为最新消息的 `计数 · 预览`。局限:转录里更早的
|
|
99
118
|
折叠块也会显示最新 label —— 需要按消息独立显示请打 bundle patch
|
|
@@ -104,7 +123,7 @@ extension 保持惰性(避免双标题),请运行 `/thinking-header patch`
|
|
|
104
123
|
| 文件 | 说明 |
|
|
105
124
|
|------|------|
|
|
106
125
|
| `index.js` | pi extension(模式检测 + 降级实现,含流式行尾跟随 label) |
|
|
107
|
-
| `install-patch.mjs` | 完整模式安装器(v3.
|
|
126
|
+
| `install-patch.mjs` | 完整模式安装器(v3.3 bundle patch,渲染时读取 decimals 配置) |
|
|
108
127
|
| `package.json` | pi 包清单(`pi.extensions`) |
|
|
109
128
|
|
|
110
129
|
## License
|
package/index.js
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Thinking (~2.50k tokens)
|
|
9
9
|
*
|
|
10
|
-
* - Expanded: header line + full thinking text
|
|
10
|
+
* - Expanded: header line + full thinking text; while the block streams the
|
|
11
|
+
* header line stays byte-stable ("Thinking…") — pi's TUI redraws whole
|
|
12
|
+
* lines over the contiguous changed range, so any header change would
|
|
13
|
+
* redraw every earlier thinking line together with the latest one. The
|
|
14
|
+
* token count appears once the block settles.
|
|
11
15
|
* - Collapsed: single line — header + a preview of the thinking content that
|
|
12
16
|
* follows dsh's ReasoningRow: while streaming it shows the LATEST line with
|
|
13
17
|
* the window pinned to its tail (leading ellipsis — the line "scrolls" as
|
|
@@ -23,13 +27,23 @@
|
|
|
23
27
|
* 2. FALLBACK MODE (pure extension API) — used when the bundle is unpatched:
|
|
24
28
|
* - Expanded thinking: prepends a `Thinking (~N tokens)` header via a
|
|
25
29
|
* markdown transformer (correct per-block counts, works everywhere,
|
|
26
|
-
* survives pi updates).
|
|
30
|
+
* survives pi updates). Same streaming-stable header as full mode: while
|
|
31
|
+
* `ctx.isStreaming` the header stays "Thinking…" and only the final
|
|
32
|
+
* count is shown once the block settles.
|
|
27
33
|
* - Collapsed thinking: updates the global hidden-thinking label on each
|
|
28
34
|
* assistant message with the latest count + preview. Limitation: pi's
|
|
29
35
|
* extension API only exposes ONE global label, so collapsed blocks in
|
|
30
36
|
* older transcript messages will show the most recent label. Run
|
|
31
37
|
* /thinking-header patch for per-message fidelity.
|
|
32
38
|
*
|
|
39
|
+
* Startup auto-patch (autoPatch, default on): every session_start re-probes
|
|
40
|
+
* the bundle — unpatched → apply the patch, older family version → migrate —
|
|
41
|
+
* so a lost patch after `pi update` (or a newer extension release) heals on
|
|
42
|
+
* the next launch with zero manual steps. The patched file matters for the
|
|
43
|
+
* NEXT restart; the running session keeps its fallback (or old-patch)
|
|
44
|
+
* renderer and a notify explains that. Disable: /thinking-header autopatch
|
|
45
|
+
* off, or {"autoPatch": false} in the config file.
|
|
46
|
+
*
|
|
33
47
|
* Token counts are local estimates: ceil(chars / 4). The k-token decimal places
|
|
34
48
|
* are configurable (default 2): /thinking-header decimals <0-6>, persisted to
|
|
35
49
|
* <agentDir>/thinking-header.json ({"decimals": 2}). Both modes re-read the
|
|
@@ -50,9 +64,16 @@ const INSTALLER_PATH = fileURLToPath(new URL("./install-patch.mjs", import.meta.
|
|
|
50
64
|
|
|
51
65
|
// Family-wide marker: ANY patch version owns header rendering end-to-end, so
|
|
52
66
|
// the extension must stay inert for all of them (avoids double headers). The
|
|
53
|
-
// /thinking-header patch
|
|
67
|
+
// /thinking-header patch (and the startup auto-patch) migrates older patch
|
|
68
|
+
// versions to the current one.
|
|
54
69
|
const MARKER = "pi-thinking-header";
|
|
55
70
|
|
|
71
|
+
// Current patch version — keep in sync with CURRENT_MARKER in
|
|
72
|
+
// install-patch.mjs. A disk bundle carrying only an older family version is
|
|
73
|
+
// "patched" (extension stays inert) but not "current" (auto-migrates).
|
|
74
|
+
const PATCH_VERSION = "v3.3";
|
|
75
|
+
const CURRENT_MARKER = MARKER + ":" + PATCH_VERSION;
|
|
76
|
+
|
|
56
77
|
// ---------------- decimals config (shared with the bundle patch) -------------
|
|
57
78
|
// <agentDir>/thinking-header.json: { "decimals": 2 } — k-token decimal places.
|
|
58
79
|
// The bundle patch reads the same file at render time (mtime-cached), so both
|
|
@@ -94,7 +115,10 @@ const getDecimals = () => {
|
|
|
94
115
|
return typeof d === "number" && Number.isInteger(d) && d >= 0 && d <= 6 ? d : DEFAULT_DECIMALS;
|
|
95
116
|
};
|
|
96
117
|
|
|
97
|
-
|
|
118
|
+
// Startup auto-patch: only an explicit `false` disables it (absent key = on).
|
|
119
|
+
const getAutoPatch = () => loadConfig().autoPatch !== false;
|
|
120
|
+
|
|
121
|
+
function saveConfigKey(key, value) {
|
|
98
122
|
let cfg = {};
|
|
99
123
|
try {
|
|
100
124
|
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
@@ -102,7 +126,7 @@ function saveDecimals(n) {
|
|
|
102
126
|
} catch {
|
|
103
127
|
// no/invalid config → start fresh, keep nothing
|
|
104
128
|
}
|
|
105
|
-
cfg
|
|
129
|
+
cfg[key] = value;
|
|
106
130
|
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
107
131
|
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
|
|
108
132
|
configCache.mtime = -1n; // force re-read on next loadConfig()
|
|
@@ -113,7 +137,9 @@ const COMPONENT_ANCHOR =
|
|
|
113
137
|
|
|
114
138
|
/**
|
|
115
139
|
* Locate the running pi's bundle chunk that contains AssistantMessageComponent.
|
|
116
|
-
* Returns { found: true, patched
|
|
140
|
+
* Returns { found: true, patched, current } or { found: false }:
|
|
141
|
+
* - patched: ANY pi-thinking-header family version is on disk
|
|
142
|
+
* - current: exactly the version this extension ships (PATCH_VERSION)
|
|
117
143
|
*/
|
|
118
144
|
function probeBundle() {
|
|
119
145
|
try {
|
|
@@ -128,7 +154,11 @@ function probeBundle() {
|
|
|
128
154
|
const path = join(chunksDir, file);
|
|
129
155
|
const src = readFileSync(path, "utf8");
|
|
130
156
|
if (!src.includes(COMPONENT_ANCHOR)) continue;
|
|
131
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
found: true,
|
|
159
|
+
patched: src.includes(MARKER),
|
|
160
|
+
current: src.includes(CURRENT_MARKER),
|
|
161
|
+
};
|
|
132
162
|
}
|
|
133
163
|
} catch {
|
|
134
164
|
// pi binary not resolvable — fall through
|
|
@@ -204,10 +234,58 @@ const thinkingLabel = (blocks, running) => {
|
|
|
204
234
|
return label;
|
|
205
235
|
};
|
|
206
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Startup auto-patch: on session_start, re-probe the bundle and run the
|
|
239
|
+
* installer when the disk is unpatched (apply) or carries an older family
|
|
240
|
+
* version (migrate). The patched file only matters for the NEXT pi start —
|
|
241
|
+
* the running session keeps whichever renderer it booted with — so the
|
|
242
|
+
* notify says so. Best-effort by design: failures degrade to the manual
|
|
243
|
+
* /thinking-header patch command, never to a broken startup.
|
|
244
|
+
*/
|
|
245
|
+
function registerAutoPatch(pi) {
|
|
246
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
247
|
+
try {
|
|
248
|
+
if (!getAutoPatch()) return;
|
|
249
|
+
const before = probeBundle();
|
|
250
|
+
if (!before.found || before.current) return; // up to date / not resolvable
|
|
251
|
+
const migrating = before.patched; // older family version on disk
|
|
252
|
+
const { stdout, stderr } = await execFileAsync(
|
|
253
|
+
process.execPath,
|
|
254
|
+
[INSTALLER_PATH],
|
|
255
|
+
{ timeout: 30_000, windowsHide: true, maxBuffer: 1024 * 1024 },
|
|
256
|
+
);
|
|
257
|
+
if (probeBundle().current) {
|
|
258
|
+
ctx.ui.notify(
|
|
259
|
+
`pi-thinking-header: bundle patch ${migrating ? "migrated to" : "applied"} (${PATCH_VERSION}) at startup — ` +
|
|
260
|
+
"effective after the next restart; this session keeps its current renderer.",
|
|
261
|
+
"info",
|
|
262
|
+
);
|
|
263
|
+
} else {
|
|
264
|
+
const out = (stdout + "\n" + stderr).trim();
|
|
265
|
+
ctx.ui.notify(
|
|
266
|
+
"pi-thinking-header: startup auto-patch had no effect.\n" +
|
|
267
|
+
(out || "(no installer output)") +
|
|
268
|
+
"\nRun /thinking-header patch for details.",
|
|
269
|
+
"warning",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
} catch (err) {
|
|
273
|
+
const detail = [err?.message, err?.stdout, err?.stderr].filter(Boolean).join("\n");
|
|
274
|
+
ctx.ui.notify(
|
|
275
|
+
"pi-thinking-header: startup auto-patch failed.\n" +
|
|
276
|
+
detail +
|
|
277
|
+
"\nRun /thinking-header patch to apply manually.",
|
|
278
|
+
"warning",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
207
284
|
export default function (pi) {
|
|
208
285
|
// /thinking-header — single entry point for everything this package does:
|
|
209
286
|
// /thinking-header status overview + usage
|
|
210
287
|
// /thinking-header patch apply/repair the full-mode bundle patch
|
|
288
|
+
// /thinking-header autopatch on|off startup auto-check+apply (default on)
|
|
211
289
|
// /thinking-header decimals show the k-token decimal places
|
|
212
290
|
// /thinking-header decimals <0-6> set + persist them (0-6, default 2)
|
|
213
291
|
// Registered in BOTH modes. Fallback mode re-reads the config per label; the
|
|
@@ -217,21 +295,38 @@ export default function (pi) {
|
|
|
217
295
|
"usage:\n" +
|
|
218
296
|
" /thinking-header status overview\n" +
|
|
219
297
|
" /thinking-header patch apply/repair the full-mode patch\n" +
|
|
298
|
+
" /thinking-header autopatch on|off startup auto-check+apply (default on)\n" +
|
|
220
299
|
" /thinking-header decimals show k-token decimal places\n" +
|
|
221
300
|
" /thinking-header decimals <0-6> set them (persisted, applies live)";
|
|
222
301
|
|
|
223
302
|
pi.registerCommand("thinking-header", {
|
|
224
|
-
description:
|
|
225
|
-
"pi-thinking-header: status / `patch` apply-repair full-mode patch / `decimals <0-6>` token-count decimals",
|
|
303
|
+
description: "Thinking display settings: status, patch, autopatch, decimals",
|
|
226
304
|
getArgumentCompletions: (prefix) => {
|
|
227
305
|
const p = (prefix ?? "").trimStart();
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
306
|
+
const tokens = p.split(/\s+/);
|
|
307
|
+
// Fully typed token → close the menu so Enter SUBMITS the command:
|
|
308
|
+
// pi's editor consumes Enter to accept an open completion (argument
|
|
309
|
+
// prefixes don't start with "/", so accept-without-submit applies), and
|
|
310
|
+
// an exact-match item would re-open the menu on every re-query — Enter
|
|
311
|
+
// would be swallowed repeatedly and the command would never run.
|
|
312
|
+
if (tokens.length === 1) {
|
|
313
|
+
const items = [
|
|
314
|
+
{ value: "patch", label: "apply/repair the full-mode bundle patch" },
|
|
315
|
+
{ value: "autopatch", label: "startup auto-apply/migrate the patch (on/off)" },
|
|
316
|
+
{ value: "decimals", label: "k-token decimal places (0-6, default 2)" },
|
|
317
|
+
];
|
|
318
|
+
const filtered = items.filter((i) => i.value.startsWith(tokens[0]));
|
|
319
|
+
if (filtered.length === 0) return null;
|
|
320
|
+
if (filtered.length === 1 && filtered[0].value === tokens[0]) return null;
|
|
321
|
+
return filtered;
|
|
322
|
+
}
|
|
323
|
+
if (tokens.length === 2 && tokens[0] === "autopatch") {
|
|
324
|
+
const opts = ["on", "off"].filter((o) => o.startsWith(tokens[1]));
|
|
325
|
+
if (opts.length === 0) return null;
|
|
326
|
+
if (opts.length === 1 && opts[0] === tokens[1]) return null;
|
|
327
|
+
return opts.map((o) => ({ value: o, label: `startup auto-patch: ${o}` }));
|
|
328
|
+
}
|
|
329
|
+
return null; // decimals <0-6>: single-digit values never need completion
|
|
235
330
|
},
|
|
236
331
|
handler: async (args, ctx) => {
|
|
237
332
|
const argv = (args ?? "").trim().split(/\s+/).filter(Boolean);
|
|
@@ -254,7 +349,7 @@ export default function (pi) {
|
|
|
254
349
|
return;
|
|
255
350
|
}
|
|
256
351
|
try {
|
|
257
|
-
|
|
352
|
+
saveConfigKey("decimals", n);
|
|
258
353
|
} catch (err) {
|
|
259
354
|
ctx.ui.notify(`pi-thinking-header: failed to save ${CONFIG_PATH}: ${err?.message ?? err}`, "error");
|
|
260
355
|
return;
|
|
@@ -267,6 +362,42 @@ export default function (pi) {
|
|
|
267
362
|
return;
|
|
268
363
|
}
|
|
269
364
|
|
|
365
|
+
// ---- autopatch subcommand: startup auto-check+apply toggle --------
|
|
366
|
+
if (argv[0] === "autopatch") {
|
|
367
|
+
const raw = argv[1];
|
|
368
|
+
if (raw === undefined) {
|
|
369
|
+
ctx.ui.notify(
|
|
370
|
+
`pi-thinking-header: autoPatch = ${getAutoPatch() ? "on" : "off"} (default on)\n` +
|
|
371
|
+
"session-start check: applies the patch when missing, migrates old versions\n" +
|
|
372
|
+
`config: ${CONFIG_PATH}\n` +
|
|
373
|
+
"usage: /thinking-header autopatch on|off",
|
|
374
|
+
"info",
|
|
375
|
+
);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (raw !== "on" && raw !== "off") {
|
|
379
|
+
ctx.ui.notify('pi-thinking-header: autopatch expects "on" or "off"', "error");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
saveConfigKey("autoPatch", raw === "on");
|
|
384
|
+
} catch (err) {
|
|
385
|
+
ctx.ui.notify(
|
|
386
|
+
`pi-thinking-header: failed to save ${CONFIG_PATH}: ${err?.message ?? err}`,
|
|
387
|
+
"error",
|
|
388
|
+
);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
ctx.ui.notify(
|
|
392
|
+
`pi-thinking-header: autoPatch = ${raw} → saved to ${CONFIG_PATH}\n` +
|
|
393
|
+
(raw === "on"
|
|
394
|
+
? "the next session start applies/migrates the patch automatically"
|
|
395
|
+
: "startup auto-patch disabled — use /thinking-header patch to apply manually"),
|
|
396
|
+
"info",
|
|
397
|
+
);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
270
401
|
// ---- patch subcommand: apply/repair the full-mode bundle patch ----
|
|
271
402
|
if (argv[0] === "patch") {
|
|
272
403
|
try {
|
|
@@ -278,9 +409,9 @@ export default function (pi) {
|
|
|
278
409
|
);
|
|
279
410
|
return;
|
|
280
411
|
}
|
|
281
|
-
if (state.
|
|
412
|
+
if (state.current) {
|
|
282
413
|
ctx.ui.notify(
|
|
283
|
-
`pi-thinking-header: full-mode patch already applied (
|
|
414
|
+
`pi-thinking-header: full-mode patch already applied (${PATCH_VERSION}) — decimals = ${getDecimals()}`,
|
|
284
415
|
"info",
|
|
285
416
|
);
|
|
286
417
|
return;
|
|
@@ -316,12 +447,15 @@ export default function (pi) {
|
|
|
316
447
|
const state = probeBundle();
|
|
317
448
|
const mode = !state.found
|
|
318
449
|
? "fallback (pi installation not detected)"
|
|
319
|
-
: state.
|
|
320
|
-
?
|
|
321
|
-
:
|
|
450
|
+
: state.current
|
|
451
|
+
? `full (bundle patch ${PATCH_VERSION} applied)`
|
|
452
|
+
: state.patched
|
|
453
|
+
? "full (older patch on disk — auto-migrates at next session start, or /thinking-header patch now)"
|
|
454
|
+
: "fallback (bundle unpatched — auto-patches at next session start, or /thinking-header patch now)";
|
|
322
455
|
ctx.ui.notify(
|
|
323
456
|
"pi-thinking-header\n" +
|
|
324
457
|
`mode: ${mode}\n` +
|
|
458
|
+
`autoPatch: ${getAutoPatch() ? "on" : "off"} (default on)\n` +
|
|
325
459
|
`decimals: ${getDecimals()} (default ${DEFAULT_DECIMALS})\n` +
|
|
326
460
|
`config: ${CONFIG_PATH}\n` +
|
|
327
461
|
usage,
|
|
@@ -331,20 +465,37 @@ export default function (pi) {
|
|
|
331
465
|
});
|
|
332
466
|
|
|
333
467
|
const bundle = probeBundle();
|
|
468
|
+
if (bundle.found && bundle.current) {
|
|
469
|
+
// FULL MODE, up to date: the bundle patch owns rendering end-to-end
|
|
470
|
+
// (per-message labels, width-aware single-line previews). Stay inert — a
|
|
471
|
+
// global label update here would corrupt the patch's per-block counts.
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Not current on disk (unpatched, or an older family version): register the
|
|
476
|
+
// startup auto-patch. It lands on disk for the NEXT restart; this session
|
|
477
|
+
// keeps whichever renderer it booted with.
|
|
478
|
+
registerAutoPatch(pi);
|
|
479
|
+
|
|
334
480
|
if (bundle.found && bundle.patched) {
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
// would corrupt the patch's per-block counts.
|
|
481
|
+
// An older family patch still owns rendering for THIS session → stay
|
|
482
|
+
// inert to avoid double headers; the auto-patch migrates it (above).
|
|
338
483
|
return;
|
|
339
484
|
}
|
|
340
485
|
|
|
341
486
|
// ---------------- FALLBACK MODE (pure extension API) ----------------
|
|
342
487
|
|
|
343
|
-
// Expanded thinking: per-block header with token count.
|
|
488
|
+
// Expanded thinking: per-block header with token count. While the block is
|
|
489
|
+
// still streaming the header MUST stay byte-stable ("Thinking…"): pi's TUI
|
|
490
|
+
// diffs whole lines and rewrites the contiguous [firstChanged..lastChanged]
|
|
491
|
+
// range, so any header change on every chunk would erase+redraw every
|
|
492
|
+
// earlier thinking line together with the latest one. The token count
|
|
493
|
+
// appears once the block settles (isStreaming flips to false).
|
|
344
494
|
pi.registerMarkdownTransformer((markdown, ctx) => {
|
|
345
495
|
if (ctx?.messageType !== "assistant-thinking") return markdown;
|
|
346
496
|
const text = markdown.trim();
|
|
347
497
|
if (!text) return markdown;
|
|
498
|
+
if (ctx.isStreaming) return `*Thinking…*\n\n${markdown}`;
|
|
348
499
|
return `*Thinking (~${formatTokens(Math.ceil(text.length / 4))} tokens)*\n\n${markdown}`;
|
|
349
500
|
});
|
|
350
501
|
|
|
@@ -354,7 +505,10 @@ export default function (pi) {
|
|
|
354
505
|
// once the message settles (message_end) it returns to the first line.
|
|
355
506
|
// Limitation: pi's extension API only exposes ONE global label, so collapsed
|
|
356
507
|
// blocks in older transcript messages will show the most recent label. Run
|
|
357
|
-
// /thinking-header patch for per-message fidelity.
|
|
508
|
+
// /thinking-header patch for per-message fidelity. Re-setting the SAME label
|
|
509
|
+
// is skipped: pi pushes the global label into every message component on
|
|
510
|
+
// each set, so a no-op set would still rebuild all of them.
|
|
511
|
+
let lastLabel = null;
|
|
358
512
|
const updateLabel = async (message, ctx, running) => {
|
|
359
513
|
try {
|
|
360
514
|
if (!message || message.role !== "assistant") return;
|
|
@@ -362,7 +516,10 @@ export default function (pi) {
|
|
|
362
516
|
(c) => c?.type === "thinking" && typeof c.thinking === "string" && c.thinking.trim(),
|
|
363
517
|
);
|
|
364
518
|
if (blocks.length === 0) return;
|
|
365
|
-
|
|
519
|
+
const label = thinkingLabel(blocks, running);
|
|
520
|
+
if (label === lastLabel) return;
|
|
521
|
+
lastLabel = label;
|
|
522
|
+
ctx.ui.setHiddenThinkingLabel(label);
|
|
366
523
|
} catch {
|
|
367
524
|
// display-only; never break the session over a label
|
|
368
525
|
}
|
package/install-patch.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* pi-thinking-header — FULL MODE installer (bundle patch, v3.
|
|
3
|
+
* pi-thinking-header — FULL MODE installer (bundle patch, v3.3).
|
|
4
4
|
*
|
|
5
5
|
* Patches pi's AssistantMessageComponent for dsh-style thinking rendering:
|
|
6
6
|
*
|
|
@@ -46,8 +46,12 @@ const MD_TAIL =
|
|
|
46
46
|
",this.outputPad,0,this.markdownTheme,{color:text=>theme.fg(\"thinkingText\",text),italic:!0}," +
|
|
47
47
|
'{transform:createMarkdownTransform("assistant-thinking",this.isStreaming,this.markdownTransformers)})';
|
|
48
48
|
|
|
49
|
+
// hasVisibleContentAfter is in scope at the patch site (updateContent): a
|
|
50
|
+
// thinking block is "still streaming" iff the message is streaming AND nothing
|
|
51
|
+
// visible follows it — after the reply starts, earlier blocks show their final
|
|
52
|
+
// count instead of staying on "Thinking…".
|
|
49
53
|
const HEADER_TEXT =
|
|
50
|
-
'new Text(theme.italic(theme.fg("thinkingText",this.
|
|
54
|
+
'new Text(theme.italic(theme.fg("thinkingText",this.thinkingHeaderLabel(thinkingBlocks,this.isStreaming&&!hasVisibleContentAfter))),this.outputPad,0)';
|
|
51
55
|
|
|
52
56
|
const ANCHOR2 =
|
|
53
57
|
'thinkingComponent=hidden?new Text(theme.italic(theme.fg("thinkingText",this.hiddenThinkingLabel)),this.outputPad,0):new Markdown(thinkingBlocks.' +
|
|
@@ -58,11 +62,11 @@ const ANCHOR2 =
|
|
|
58
62
|
// ---------- injected code ----------
|
|
59
63
|
// Current patch version; also the key for mode detection (family-wide) and
|
|
60
64
|
// migration ("any older family member → restore pristine backup, re-apply").
|
|
61
|
-
const CURRENT_MARKER = "pi-thinking-header:v3.
|
|
65
|
+
const CURRENT_MARKER = "pi-thinking-header:v3.3";
|
|
62
66
|
|
|
63
67
|
// Marker comment doubles as the extension's mode-detection key.
|
|
64
68
|
const HELPER =
|
|
65
|
-
"/*pi-thinking-header:v3.
|
|
69
|
+
"/*pi-thinking-header:v3.3*/" +
|
|
66
70
|
// decimals config: per-instance cache keyed on the config file's mtime
|
|
67
71
|
// (mtimeNs BigInt — ns precision beats same-ms collisions). Lazy ??= init
|
|
68
72
|
// (no class fields needed) + a reader that resolves
|
|
@@ -73,14 +77,26 @@ const HELPER =
|
|
|
73
77
|
'p=(process.env.PI_CODING_AGENT_DIR||os.homedir()+"/.pi/agent")+"/thinking-header.json",st=fs.statSync(p,{bigint:!0});' +
|
|
74
78
|
'if(st.mtimeNs!==this._pthT){let c={};try{c=JSON.parse(fs.readFileSync(p,"utf8"))}catch{}' +
|
|
75
79
|
"this._pthT=st.mtimeNs,this._pthC=c}}catch{this._pthC=null}return this._pthC||{}}" +
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
'
|
|
80
|
+
// Token count: local estimate ceil(chars / 4). Decimals come from _piCfg():
|
|
81
|
+
// integer 0–6 wins, anything else → 2. No trailing-zero stripping: the
|
|
82
|
+
// setting is authoritative (2.50k stays 2.50k).
|
|
83
|
+
'_tokensLabel(blocks){let chars=0;for(let b of blocks)chars+=b.length;' +
|
|
80
84
|
"let tokens=Math.ceil(chars/4),c=this._piCfg()," +
|
|
81
|
-
'd=typeof c.decimals=="number"&&Number.isInteger(c.decimals)&&c.decimals>=0&&c.decimals<=6?c.decimals:2
|
|
82
|
-
'
|
|
83
|
-
|
|
85
|
+
'd=typeof c.decimals=="number"&&Number.isInteger(c.decimals)&&c.decimals>=0&&c.decimals<=6?c.decimals:2;' +
|
|
86
|
+
'return(tokens>=1000?(tokens/1000).toFixed(d)+"k":""+tokens)+" tokens"}' +
|
|
87
|
+
// Live one-line label: "Thinking (~2.50k tokens)" (prefix honours a custom
|
|
88
|
+
// label, minus trailing dots). Used by the collapsed row ONLY — that row is
|
|
89
|
+
// a single screen line, so a live count redraws just that one line.
|
|
90
|
+
'thinkingLabelText(blocks){return this.hiddenThinkingLabel.replace(/\\.{3}$/,"")+" (~"+this._tokensLabel(blocks)+")"}' +
|
|
91
|
+
// Expanded header. While the block is ACTIVELY streaming (it is the message's
|
|
92
|
+
// last content and chunks still arrive) it renders the bare prefix + "…" and
|
|
93
|
+
// MUST stay byte-stable: pi's TUI diffs whole lines and rewrites the
|
|
94
|
+
// contiguous [firstChanged..lastChanged] range, so changing the header on
|
|
95
|
+
// every chunk would erase+redraw every earlier thinking line together with
|
|
96
|
+
// the latest one. The final count appears once the block settles (stream
|
|
97
|
+
// ends or later content starts), when the block no longer re-renders per
|
|
98
|
+
// chunk and no redraw cascade is possible.
|
|
99
|
+
'thinkingHeaderLabel(blocks,streaming){let p=this.hiddenThinkingLabel.replace(/\\.{3}$/,"");if(streaming)return p+"…";return p+" (~"+this._tokensLabel(blocks)+")"}' +
|
|
84
100
|
// Collapsed state: plain-object component rendering ONE line:
|
|
85
101
|
// label + " · " + preview, truncated to the render width (CJK-aware).
|
|
86
102
|
// dsh ReasoningRow parity: running → latest non-empty line with the window
|
|
@@ -163,7 +179,7 @@ const TAIL_BROKEN = "padTotal)]}}}";
|
|
|
163
179
|
const TAIL_FIXED = "padTotal)]},invalidate(){}}}";
|
|
164
180
|
|
|
165
181
|
if (src.includes(CURRENT_MARKER) && src.includes(TAIL_FIXED)) {
|
|
166
|
-
console.log(`Already patched (v3.
|
|
182
|
+
console.log(`Already patched (v3.3): ${chunkPath}`);
|
|
167
183
|
process.exit(0);
|
|
168
184
|
}
|
|
169
185
|
|
|
@@ -181,7 +197,7 @@ if (src.includes("pi-thinking-header") || src.includes("thinkingLabelText(")) {
|
|
|
181
197
|
writeFileSync(chunkPath, src);
|
|
182
198
|
console.log("Hotfixed (early v3 → v3.1): added missing invalidate() to the collapsed-thinking component");
|
|
183
199
|
console.log(`File: ${chunkPath}`);
|
|
184
|
-
console.log("Re-run the installer to update the patch to
|
|
200
|
+
console.log("Re-run the installer to update the patch to the latest version.");
|
|
185
201
|
process.exit(0);
|
|
186
202
|
}
|
|
187
203
|
if (!existsSync(backup)) {
|
|
@@ -205,6 +221,6 @@ if (!src.includes(CURRENT_MARKER) || !src.includes("piThinkingCollapsed(") || !s
|
|
|
205
221
|
|
|
206
222
|
verifyESM(src);
|
|
207
223
|
writeFileSync(chunkPath, src);
|
|
208
|
-
console.log(`Patched (v3.
|
|
224
|
+
console.log(`Patched (v3.3): ${chunkPath}`);
|
|
209
225
|
console.log(`Backup: ${backup}`);
|
|
210
226
|
console.log("Revert with: cp '" + backup + "' '" + chunkPath + "'");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-thinking-header",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "dsh-style thinking display for pi: one-line header with token count (configurable decimals, default 2); collapsed thinking shows a single-line content preview",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|