pi-thinking-header 0.3.2
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/LICENSE +21 -0
- package/README.md +112 -0
- package/index.js +373 -0
- package/install-patch.mjs +210 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 focksor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# pi-thinking-header
|
|
2
|
+
|
|
3
|
+
dsh 风格的 pi thinking 显示插件:带 token 计数的单行标题 + 折叠时单行内容预览(参考 dsh `ReasoningRow` 的滚动跟随)。
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
展开时: 折叠时(ctrl+t / 点击标题行):
|
|
7
|
+
Thinking (~2.50k tokens) 流式中:Thinking (~2.50k tokens) · …尾随最新内容保持可见
|
|
8
|
+
模型思考全文… 结束后:Thinking (~2.50k tokens) · 第一行内容…
|
|
9
|
+
正式回复… 正式回复…
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- token 数量为本地估算:`ceil(chars / 4)`,流式输出时实时增长
|
|
13
|
+
- 小数点位数可配置(默认 2 位):`/thinking-header decimals <0-6>`,或直接编辑
|
|
14
|
+
配置文件 `~/.pi/agent/thinking-header.json`(`{"decimals": 2}`)。例如默认效果
|
|
15
|
+
`2.50k`;`decimals: 0` → `3k`;`decimals: 4` → `2.5000k`。两种模式均实时读取
|
|
16
|
+
该文件(mtime 缓存),修改后无需重启 pi
|
|
17
|
+
- 折叠预览与 dsh `ReasoningRow` 对齐:**流式期间**显示最新一行的**行尾**(前导省略号,
|
|
18
|
+
相当于 `scrollLeft = scrollWidth - clientWidth`),多行内容在单行内持续滚动跟随;
|
|
19
|
+
**结束后**回到第一行开头(尾部省略号,相当于 `scrollLeft = 0`)
|
|
20
|
+
- 截断按终端宽度计算(CJK 全角字符按 2 列计宽),保证严格单行
|
|
21
|
+
- 点击标题行或 `Ctrl+T` 切换展开/折叠
|
|
22
|
+
|
|
23
|
+
## 安装
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 从 npm 安装(写入 ~/.pi/agent/settings.json)
|
|
27
|
+
pi install npm:pi-thinking-header
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
本地开发调试:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/focksor/pi-thinking-header
|
|
34
|
+
pi install ./pi-thinking-header
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 会话内命令:`/thinking-header`
|
|
38
|
+
|
|
39
|
+
所有功能的统一入口:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
/thinking-header # 状态总览(模式/补丁状态/decimals)+ 用法
|
|
43
|
+
/thinking-header patch # 应用/修复完整模式补丁(确认后执行)
|
|
44
|
+
/thinking-header decimals # 查看当前小数位
|
|
45
|
+
/thinking-header decimals <0-6> # 设置小数位并持久化(默认 2)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
补丁安装(`patch` 子命令,内部运行 `node install-patch.mjs`):
|
|
49
|
+
|
|
50
|
+
- 已打补丁 → 提示 already applied(含当前 decimals)
|
|
51
|
+
- 未打补丁(或补丁版本较旧)→ 确认后执行安装/迁移,输出结果并提醒重启 pi(当前会话仍是旧代码,重启后生效)
|
|
52
|
+
- `pi update` 之后补丁丢失时,extension 自动进入降级模式,此时用该命令可一键修复
|
|
53
|
+
|
|
54
|
+
小数位配置(`decimals` 子命令):
|
|
55
|
+
|
|
56
|
+
- 有效范围 0–6 的整数,其它输入会报错且不修改配置
|
|
57
|
+
- 写入 `~/.pi/agent/thinking-header.json`(也可直接编辑该文件);降级模式立即生效,
|
|
58
|
+
完整模式下 patch 在每次渲染时读取该文件(mtime 缓存),同样无需重启
|
|
59
|
+
- 尊重 `PI_CODING_AGENT_DIR` 环境变量(支持 `~/...` 形式)
|
|
60
|
+
|
|
61
|
+
## 两种模式(自动检测)
|
|
62
|
+
|
|
63
|
+
### 完整模式(推荐):bundle patch
|
|
64
|
+
|
|
65
|
+
pi 的 extension API 无法接管内置 thinking 块的折叠渲染,完整效果需要附带
|
|
66
|
+
的 bundle patch。推荐直接在 pi 会话内运行 `/thinking-header patch`
|
|
67
|
+
(自动定位安装位置);也可手动执行:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# npm 安装:~/.pi/agent/npm/pi-thinking-header/install-patch.mjs
|
|
71
|
+
# 本地安装:<仓库路径>/install-patch.mjs
|
|
72
|
+
node <包目录>/install-patch.mjs
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- 每条消息独立的 token 计数与预览(含历史消息)
|
|
76
|
+
- 折叠态严格单行、宽度感知截断、点击切换
|
|
77
|
+
- 流式期间折叠预览滚动跟随最新行行尾(dsh 同款),结束后回到第一行
|
|
78
|
+
- 小数点位数渲染时实时读取配置文件(mtime 缓存),与降级模式共享同一份配置
|
|
79
|
+
- 幂等;自动从 v1/v2/v3/v3.1 patch 迁移(恢复原始备份后重新应用;早期 v3
|
|
80
|
+
折叠组件缺 `invalidate()`、退出/切换模式时触发 `this.child.invalidate is not
|
|
81
|
+
a function` 崩溃且无备份时,会先原地热修复);`pi update` 后重跑一次即可;
|
|
82
|
+
插件包更新后也建议重跑 `/thinking-header patch` 使 patch 与 extension 版本一致
|
|
83
|
+
- 回滚:
|
|
84
|
+
```bash
|
|
85
|
+
cp <chunk路径>.pre-thinking-header.bak <chunk路径>
|
|
86
|
+
# 路径见 install-patch.mjs 的输出
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 纯插件模式:extension 自动降级
|
|
90
|
+
|
|
91
|
+
extension 启动时探测运行中的 pi bundle:**检测到任意版本的 patch 时**(含旧版本)
|
|
92
|
+
extension 保持惰性(避免双标题),请运行 `/thinking-header patch` 迁移到当前 patch 版本;
|
|
93
|
+
**未检测到 patch 时**自动进入降级模式:
|
|
94
|
+
|
|
95
|
+
- 展开的 thinking:通过 markdown transformer 前置 `Thinking (~N tokens)` 标题行
|
|
96
|
+
(按块正确计数,`pi update` 后依然有效,无需维护)
|
|
97
|
+
- 折叠的 thinking:pi 的 extension API 只暴露一个全局 label,插件会在每条
|
|
98
|
+
assistant 消息结束后将其更新为最新消息的 `计数 · 预览`。局限:转录里更早的
|
|
99
|
+
折叠块也会显示最新 label —— 需要按消息独立显示请打 bundle patch
|
|
100
|
+
- bundle 已打 patch 时 extension 完全惰性,不会产生双标题
|
|
101
|
+
|
|
102
|
+
## 文件
|
|
103
|
+
|
|
104
|
+
| 文件 | 说明 |
|
|
105
|
+
|------|------|
|
|
106
|
+
| `index.js` | pi extension(模式检测 + 降级实现,含流式行尾跟随 label) |
|
|
107
|
+
| `install-patch.mjs` | 完整模式安装器(v3.2 bundle patch,渲染时读取 decimals 配置) |
|
|
108
|
+
| `package.json` | pi 包清单(`pi.extensions`) |
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT © focksor
|
package/index.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-thinking-header — dsh-style thinking display for pi.
|
|
3
|
+
*
|
|
4
|
+
* What it does
|
|
5
|
+
* ------------
|
|
6
|
+
* Thinking blocks get a one-line header with an estimated token count:
|
|
7
|
+
*
|
|
8
|
+
* Thinking (~2.50k tokens)
|
|
9
|
+
*
|
|
10
|
+
* - Expanded: header line + full thinking text
|
|
11
|
+
* - Collapsed: single line — header + a preview of the thinking content that
|
|
12
|
+
* follows dsh's ReasoningRow: while streaming it shows the LATEST line with
|
|
13
|
+
* the window pinned to its tail (leading ellipsis — the line "scrolls" as
|
|
14
|
+
* content arrives); when done it shows the FIRST line from its start
|
|
15
|
+
*
|
|
16
|
+
* Two modes (auto-detected at load time)
|
|
17
|
+
* --------------------------------------
|
|
18
|
+
* 1. FULL MODE — the companion bundle patch (install-patch.mjs) has patched the
|
|
19
|
+
* running pi installation. The patch implements everything per-message with
|
|
20
|
+
* width-aware truncation and click-to-toggle. This extension then stays
|
|
21
|
+
* completely inert to avoid double headers.
|
|
22
|
+
*
|
|
23
|
+
* 2. FALLBACK MODE (pure extension API) — used when the bundle is unpatched:
|
|
24
|
+
* - Expanded thinking: prepends a `Thinking (~N tokens)` header via a
|
|
25
|
+
* markdown transformer (correct per-block counts, works everywhere,
|
|
26
|
+
* survives pi updates).
|
|
27
|
+
* - Collapsed thinking: updates the global hidden-thinking label on each
|
|
28
|
+
* assistant message with the latest count + preview. Limitation: pi's
|
|
29
|
+
* extension API only exposes ONE global label, so collapsed blocks in
|
|
30
|
+
* older transcript messages will show the most recent label. Run
|
|
31
|
+
* /thinking-header patch for per-message fidelity.
|
|
32
|
+
*
|
|
33
|
+
* Token counts are local estimates: ceil(chars / 4). The k-token decimal places
|
|
34
|
+
* are configurable (default 2): /thinking-header decimals <0-6>, persisted to
|
|
35
|
+
* <agentDir>/thinking-header.json ({"decimals": 2}). Both modes re-read the
|
|
36
|
+
* file, so changes apply without restarting pi.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { execFile, execSync } from "node:child_process";
|
|
40
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
41
|
+
import { homedir } from "node:os";
|
|
42
|
+
import { dirname, join } from "node:path";
|
|
43
|
+
import { fileURLToPath } from "node:url";
|
|
44
|
+
import { promisify } from "node:util";
|
|
45
|
+
|
|
46
|
+
const execFileAsync = promisify(execFile);
|
|
47
|
+
|
|
48
|
+
// Companion installer (same package directory), executed by /thinking-header patch.
|
|
49
|
+
const INSTALLER_PATH = fileURLToPath(new URL("./install-patch.mjs", import.meta.url));
|
|
50
|
+
|
|
51
|
+
// Family-wide marker: ANY patch version owns header rendering end-to-end, so
|
|
52
|
+
// the extension must stay inert for all of them (avoids double headers). The
|
|
53
|
+
// /thinking-header patch migrates older patch versions to the current one.
|
|
54
|
+
const MARKER = "pi-thinking-header";
|
|
55
|
+
|
|
56
|
+
// ---------------- decimals config (shared with the bundle patch) -------------
|
|
57
|
+
// <agentDir>/thinking-header.json: { "decimals": 2 } — k-token decimal places.
|
|
58
|
+
// The bundle patch reads the same file at render time (mtime-cached), so both
|
|
59
|
+
// modes react to changes without restarting pi.
|
|
60
|
+
const DEFAULT_DECIMALS = 2;
|
|
61
|
+
const AGENT_DIR = process.env.PI_CODING_AGENT_DIR
|
|
62
|
+
? process.env.PI_CODING_AGENT_DIR.replace(/^~(?=\/|$)/, () => homedir())
|
|
63
|
+
: join(homedir(), ".pi", "agent");
|
|
64
|
+
const CONFIG_PATH = join(AGENT_DIR, "thinking-header.json");
|
|
65
|
+
|
|
66
|
+
const configCache = { mtime: -1n, value: {} };
|
|
67
|
+
|
|
68
|
+
function loadConfig() {
|
|
69
|
+
try {
|
|
70
|
+
// mtimeNs (BigInt) as cache key: ms-precision mtime can collide when a
|
|
71
|
+
// config write lands in the same millisecond as the previous read.
|
|
72
|
+
const st = statSync(CONFIG_PATH, { bigint: true });
|
|
73
|
+
if (st.mtimeNs !== configCache.mtime) {
|
|
74
|
+
let value = {};
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
77
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) value = parsed;
|
|
78
|
+
} catch {
|
|
79
|
+
// unparsable config → treat as empty
|
|
80
|
+
}
|
|
81
|
+
configCache.mtime = st.mtimeNs;
|
|
82
|
+
configCache.value = value;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// file missing → defaults (also drops cache entries for deleted files)
|
|
86
|
+
configCache.mtime = -1n;
|
|
87
|
+
configCache.value = {};
|
|
88
|
+
}
|
|
89
|
+
return configCache.value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const getDecimals = () => {
|
|
93
|
+
const d = loadConfig().decimals;
|
|
94
|
+
return typeof d === "number" && Number.isInteger(d) && d >= 0 && d <= 6 ? d : DEFAULT_DECIMALS;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function saveDecimals(n) {
|
|
98
|
+
let cfg = {};
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
101
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) cfg = parsed;
|
|
102
|
+
} catch {
|
|
103
|
+
// no/invalid config → start fresh, keep nothing
|
|
104
|
+
}
|
|
105
|
+
cfg.decimals = n;
|
|
106
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
107
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
|
|
108
|
+
configCache.mtime = -1n; // force re-read on next loadConfig()
|
|
109
|
+
}
|
|
110
|
+
// Unique fragment of AssistantMessageComponent#setHiddenThinkingLabel in the bundle
|
|
111
|
+
const COMPONENT_ANCHOR =
|
|
112
|
+
"setHiddenThinkingLabel(label){this.hiddenThinkingLabel=label,this.lastMessage&&this.updateContent(this.lastMessage)}";
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Locate the running pi's bundle chunk that contains AssistantMessageComponent.
|
|
116
|
+
* Returns { found: true, patched: boolean } or { found: false }.
|
|
117
|
+
*/
|
|
118
|
+
function probeBundle() {
|
|
119
|
+
try {
|
|
120
|
+
const bin = execSync("readlink -f $(which pi)", { stdio: ["ignore", "pipe", "ignore"] })
|
|
121
|
+
.toString()
|
|
122
|
+
.trim();
|
|
123
|
+
if (!bin.endsWith(join("dist", "bundle", "cli.js"))) return { found: false };
|
|
124
|
+
const chunksDir = join(dirname(bin), "chunks");
|
|
125
|
+
if (!existsSync(chunksDir)) return { found: false };
|
|
126
|
+
for (const file of readdirSync(chunksDir)) {
|
|
127
|
+
if (!file.endsWith(".js")) continue;
|
|
128
|
+
const path = join(chunksDir, file);
|
|
129
|
+
const src = readFileSync(path, "utf8");
|
|
130
|
+
if (!src.includes(COMPONENT_ANCHOR)) continue;
|
|
131
|
+
return { found: true, patched: src.includes(MARKER) };
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// pi binary not resolvable — fall through
|
|
135
|
+
}
|
|
136
|
+
return { found: false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// decimals: 0–6 (invalid values fall back to DEFAULT_DECIMALS); the setting is
|
|
140
|
+
// authoritative — no trailing-zero stripping, so decimals=2 renders 2.50k.
|
|
141
|
+
const formatTokens = (n, decimals = getDecimals()) =>
|
|
142
|
+
n >= 1000 ? (n / 1000).toFixed(decimals) + "k" : String(n);
|
|
143
|
+
|
|
144
|
+
const firstLine = (text) => {
|
|
145
|
+
for (const line of text.split("\n")) {
|
|
146
|
+
if (line.trim()) return line.trim();
|
|
147
|
+
}
|
|
148
|
+
return "";
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const lastLine = (text) => {
|
|
152
|
+
const lines = text.trimEnd().split("\n");
|
|
153
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
154
|
+
if (lines[i].trim()) return lines[i].trim();
|
|
155
|
+
}
|
|
156
|
+
return "";
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** Visible-length estimate that counts CJK-wide chars as 2 columns. */
|
|
160
|
+
const visibleLength = (s) => {
|
|
161
|
+
let n = 0;
|
|
162
|
+
for (const ch of s) n += ch.codePointAt(0) >= 0x2e80 ? 2 : 1;
|
|
163
|
+
return n;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** Truncate a preview so it fits `max` columns, appending an ellipsis when cut. */
|
|
167
|
+
const truncatePreview = (text, max) => {
|
|
168
|
+
if (visibleLength(text) <= max) return text;
|
|
169
|
+
let out = "";
|
|
170
|
+
let width = 0;
|
|
171
|
+
for (const ch of text) {
|
|
172
|
+
const w = ch.codePointAt(0) >= 0x2e80 ? 2 : 1;
|
|
173
|
+
if (width + w > max - 1) break;
|
|
174
|
+
out += ch;
|
|
175
|
+
width += w;
|
|
176
|
+
}
|
|
177
|
+
return out + "…";
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** Tail variant: keep the END visible, prepending an ellipsis when cut
|
|
181
|
+
* (terminal equivalent of dsh's scrollLeft = scrollWidth - clientWidth). */
|
|
182
|
+
const tailPreview = (text, max) => {
|
|
183
|
+
if (visibleLength(text) <= max) return text;
|
|
184
|
+
const out = [];
|
|
185
|
+
let width = 0;
|
|
186
|
+
const chars = [...text];
|
|
187
|
+
for (let i = chars.length - 1; i >= 0; i--) {
|
|
188
|
+
const w = chars[i].codePointAt(0) >= 0x2e80 ? 2 : 1;
|
|
189
|
+
if (width + w > max - 1) break;
|
|
190
|
+
out.push(chars[i]);
|
|
191
|
+
width += w;
|
|
192
|
+
}
|
|
193
|
+
return "…" + out.reverse().join("");
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** Hidden-thinking label: count + preview (tail-following while streaming). */
|
|
197
|
+
const thinkingLabel = (blocks, running) => {
|
|
198
|
+
const chars = blocks.reduce((n, c) => n + c.thinking.length, 0);
|
|
199
|
+
let label = `Thinking (~${formatTokens(Math.ceil(chars / 4))} tokens)`;
|
|
200
|
+
const text = running
|
|
201
|
+
? lastLine(blocks[blocks.length - 1].thinking)
|
|
202
|
+
: firstLine(blocks[blocks.length - 1].thinking);
|
|
203
|
+
if (text) label += " · " + (running ? tailPreview(text, 80) : truncatePreview(text, 80));
|
|
204
|
+
return label;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export default function (pi) {
|
|
208
|
+
// /thinking-header — single entry point for everything this package does:
|
|
209
|
+
// /thinking-header status overview + usage
|
|
210
|
+
// /thinking-header patch apply/repair the full-mode bundle patch
|
|
211
|
+
// /thinking-header decimals show the k-token decimal places
|
|
212
|
+
// /thinking-header decimals <0-6> set + persist them (0-6, default 2)
|
|
213
|
+
// Registered in BOTH modes. Fallback mode re-reads the config per label; the
|
|
214
|
+
// bundle patch re-reads it per render (mtime-cached), so decimal changes
|
|
215
|
+
// apply without restarting pi.
|
|
216
|
+
const usage =
|
|
217
|
+
"usage:\n" +
|
|
218
|
+
" /thinking-header status overview\n" +
|
|
219
|
+
" /thinking-header patch apply/repair the full-mode patch\n" +
|
|
220
|
+
" /thinking-header decimals show k-token decimal places\n" +
|
|
221
|
+
" /thinking-header decimals <0-6> set them (persisted, applies live)";
|
|
222
|
+
|
|
223
|
+
pi.registerCommand("thinking-header", {
|
|
224
|
+
description:
|
|
225
|
+
"pi-thinking-header: status / `patch` apply-repair full-mode patch / `decimals <0-6>` token-count decimals",
|
|
226
|
+
getArgumentCompletions: (prefix) => {
|
|
227
|
+
const p = (prefix ?? "").trimStart();
|
|
228
|
+
if (p.includes(" ")) return null;
|
|
229
|
+
const items = [
|
|
230
|
+
{ value: "patch", label: "apply/repair the full-mode bundle patch" },
|
|
231
|
+
{ value: "decimals", label: "k-token decimal places (0-6, default 2)" },
|
|
232
|
+
];
|
|
233
|
+
const filtered = items.filter((i) => i.value.startsWith(p));
|
|
234
|
+
return filtered.length > 0 ? filtered : null;
|
|
235
|
+
},
|
|
236
|
+
handler: async (args, ctx) => {
|
|
237
|
+
const argv = (args ?? "").trim().split(/\s+/).filter(Boolean);
|
|
238
|
+
|
|
239
|
+
// ---- decimals subcommand -------------------------------------
|
|
240
|
+
if (argv[0] === "decimals") {
|
|
241
|
+
const raw = argv[1];
|
|
242
|
+
if (raw === undefined) {
|
|
243
|
+
ctx.ui.notify(
|
|
244
|
+
`pi-thinking-header: decimals = ${getDecimals()} (default ${DEFAULT_DECIMALS})\n` +
|
|
245
|
+
`config: ${CONFIG_PATH}\n` +
|
|
246
|
+
"usage: /thinking-header decimals <0-6>",
|
|
247
|
+
"info",
|
|
248
|
+
);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const n = Number(raw);
|
|
252
|
+
if (!Number.isInteger(n) || n < 0 || n > 6) {
|
|
253
|
+
ctx.ui.notify("pi-thinking-header: decimals must be an integer between 0 and 6", "error");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
saveDecimals(n);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
ctx.ui.notify(`pi-thinking-header: failed to save ${CONFIG_PATH}: ${err?.message ?? err}`, "error");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
ctx.ui.notify(
|
|
263
|
+
`pi-thinking-header: decimals = ${n} → saved to ${CONFIG_PATH}\n` +
|
|
264
|
+
`e.g. Thinking (~${formatTokens(2500)} tokens)`,
|
|
265
|
+
"info",
|
|
266
|
+
);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---- patch subcommand: apply/repair the full-mode bundle patch ----
|
|
271
|
+
if (argv[0] === "patch") {
|
|
272
|
+
try {
|
|
273
|
+
const state = probeBundle();
|
|
274
|
+
if (!state.found) {
|
|
275
|
+
ctx.ui.notify(
|
|
276
|
+
"pi-thinking-header: could not locate the running pi installation (readlink -f $(which pi))",
|
|
277
|
+
"error",
|
|
278
|
+
);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (state.patched) {
|
|
282
|
+
ctx.ui.notify(
|
|
283
|
+
`pi-thinking-header: full-mode patch already applied (v3.2) — decimals = ${getDecimals()}`,
|
|
284
|
+
"info",
|
|
285
|
+
);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const ok = await ctx.ui.confirm(
|
|
289
|
+
"pi-thinking-header",
|
|
290
|
+
"Apply the full-mode bundle patch to the running pi installation?\n" +
|
|
291
|
+
"Modifies the installed bundle (a .bak backup is kept). Restart pi afterwards.",
|
|
292
|
+
);
|
|
293
|
+
if (!ok) return;
|
|
294
|
+
const { stdout, stderr } = await execFileAsync(
|
|
295
|
+
process.execPath,
|
|
296
|
+
[INSTALLER_PATH],
|
|
297
|
+
{ timeout: 30_000, windowsHide: true, maxBuffer: 1024 * 1024 },
|
|
298
|
+
);
|
|
299
|
+
const out = (stdout + "\n" + stderr).trim();
|
|
300
|
+
ctx.ui.notify("pi-thinking-header: patch applied.\n" + out, "info");
|
|
301
|
+
ctx.ui.notify("pi-thinking-header: restart pi to load the patched renderer.", "warning");
|
|
302
|
+
} catch (err) {
|
|
303
|
+
const detail = [err?.message, err?.stdout, err?.stderr].filter(Boolean).join("\n");
|
|
304
|
+
ctx.ui.notify("pi-thinking-header: patch failed.\n" + detail, "error");
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---- unknown arg → usage -------------------------------------
|
|
310
|
+
if (argv.length > 0) {
|
|
311
|
+
ctx.ui.notify("pi-thinking-header: unknown argument.\n" + usage, "error");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---- no args → status overview + usage ----
|
|
316
|
+
const state = probeBundle();
|
|
317
|
+
const mode = !state.found
|
|
318
|
+
? "fallback (pi installation not detected)"
|
|
319
|
+
: state.patched
|
|
320
|
+
? "full (bundle patch v3.2 applied)"
|
|
321
|
+
: "fallback (bundle unpatched — /thinking-header patch enables full mode)";
|
|
322
|
+
ctx.ui.notify(
|
|
323
|
+
"pi-thinking-header\n" +
|
|
324
|
+
`mode: ${mode}\n` +
|
|
325
|
+
`decimals: ${getDecimals()} (default ${DEFAULT_DECIMALS})\n` +
|
|
326
|
+
`config: ${CONFIG_PATH}\n` +
|
|
327
|
+
usage,
|
|
328
|
+
"info",
|
|
329
|
+
);
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const bundle = probeBundle();
|
|
334
|
+
if (bundle.found && bundle.patched) {
|
|
335
|
+
// FULL MODE: the bundle patch owns rendering end-to-end (per-message labels,
|
|
336
|
+
// width-aware single-line previews). Stay inert — a global label update here
|
|
337
|
+
// would corrupt the patch's per-block counts.
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------- FALLBACK MODE (pure extension API) ----------------
|
|
342
|
+
|
|
343
|
+
// Expanded thinking: per-block header with token count.
|
|
344
|
+
pi.registerMarkdownTransformer((markdown, ctx) => {
|
|
345
|
+
if (ctx?.messageType !== "assistant-thinking") return markdown;
|
|
346
|
+
const text = markdown.trim();
|
|
347
|
+
if (!text) return markdown;
|
|
348
|
+
return `*Thinking (~${formatTokens(Math.ceil(text.length / 4))} tokens)*\n\n${markdown}`;
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Collapsed thinking: pi exposes a single global label; keep it in sync with
|
|
352
|
+
// the latest assistant message (count + one-line preview). While streaming
|
|
353
|
+
// (message_update) the preview is the latest line tail-following dsh-style;
|
|
354
|
+
// once the message settles (message_end) it returns to the first line.
|
|
355
|
+
// Limitation: pi's extension API only exposes ONE global label, so collapsed
|
|
356
|
+
// blocks in older transcript messages will show the most recent label. Run
|
|
357
|
+
// /thinking-header patch for per-message fidelity.
|
|
358
|
+
const updateLabel = async (message, ctx, running) => {
|
|
359
|
+
try {
|
|
360
|
+
if (!message || message.role !== "assistant") return;
|
|
361
|
+
const blocks = (message.content ?? []).filter(
|
|
362
|
+
(c) => c?.type === "thinking" && typeof c.thinking === "string" && c.thinking.trim(),
|
|
363
|
+
);
|
|
364
|
+
if (blocks.length === 0) return;
|
|
365
|
+
ctx.ui.setHiddenThinkingLabel(thinkingLabel(blocks, running));
|
|
366
|
+
} catch {
|
|
367
|
+
// display-only; never break the session over a label
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
pi.on("message_update", (event, ctx) => updateLabel(event?.message, ctx, true));
|
|
372
|
+
pi.on("message_end", (event, ctx) => updateLabel(event?.message, ctx, false));
|
|
373
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pi-thinking-header — FULL MODE installer (bundle patch, v3.2).
|
|
4
|
+
*
|
|
5
|
+
* Patches pi's AssistantMessageComponent for dsh-style thinking rendering:
|
|
6
|
+
*
|
|
7
|
+
* Thinking (~2.50k tokens) · preview of the thinking content…
|
|
8
|
+
*
|
|
9
|
+
* - Expanded: one-line header (token count) + full thinking text
|
|
10
|
+
* - Collapsed: single line — header + content preview, truncated to the
|
|
11
|
+
* terminal width (CJK-aware); click or ctrl+t toggles
|
|
12
|
+
* - Collapsed preview follows dsh's ReasoningRow: while streaming it shows
|
|
13
|
+
* the LATEST line with the window pinned to its tail (leading ellipsis —
|
|
14
|
+
* the single line "scrolls" as content arrives); when done it shows the
|
|
15
|
+
* FIRST line from its start (trailing ellipsis)
|
|
16
|
+
* - Token count: local estimate ceil(chars / 4); live during streaming
|
|
17
|
+
* - Decimals: k-token decimal places configurable (0–6, default 2) via
|
|
18
|
+
* <agentDir>/thinking-header.json {"decimals": 2}; read per render with an
|
|
19
|
+
* mtime cache, so changes apply without restarting pi. Same file the
|
|
20
|
+
* extension uses in fallback mode.
|
|
21
|
+
*
|
|
22
|
+
* The extension (index.js) detects this patch and stays inert while it is
|
|
23
|
+
* applied; if pi is updated and the patch is gone, the extension's pure-API
|
|
24
|
+
* fallback takes over automatically.
|
|
25
|
+
*
|
|
26
|
+
* Idempotent. Migrates a v1 patch automatically (restores the pristine
|
|
27
|
+
* backup first). Re-run after `pi update` / `npm update -g`.
|
|
28
|
+
*
|
|
29
|
+
* Usage: node install-patch.mjs [path/to/chunk-*.js]
|
|
30
|
+
*/
|
|
31
|
+
import { readdirSync, readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs";
|
|
32
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
33
|
+
import { dirname, join } from "node:path";
|
|
34
|
+
|
|
35
|
+
// ---------- anchors (verified against pi 0.85.1 minified bundle) ----------
|
|
36
|
+
// AssistantMessageComponent#setHiddenThinkingLabel — unique: InteractiveMode's
|
|
37
|
+
// version references defaultHiddenThinkingLabel; RPC's takes _label.
|
|
38
|
+
const ANCHOR1 =
|
|
39
|
+
"setHiddenThinkingLabel(label){this.hiddenThinkingLabel=label,this.lastMessage&&this.updateContent(this.lastMessage)}";
|
|
40
|
+
|
|
41
|
+
// The minified bundle contains REAL newline bytes inside this template literal.
|
|
42
|
+
const NL = "\n";
|
|
43
|
+
const JOIN_TPL = "join(`" + NL + NL + "`)";
|
|
44
|
+
|
|
45
|
+
const MD_TAIL =
|
|
46
|
+
",this.outputPad,0,this.markdownTheme,{color:text=>theme.fg(\"thinkingText\",text),italic:!0}," +
|
|
47
|
+
'{transform:createMarkdownTransform("assistant-thinking",this.isStreaming,this.markdownTransformers)})';
|
|
48
|
+
|
|
49
|
+
const HEADER_TEXT =
|
|
50
|
+
'new Text(theme.italic(theme.fg("thinkingText",this.thinkingLabelText(thinkingBlocks))),this.outputPad,0)';
|
|
51
|
+
|
|
52
|
+
const ANCHOR2 =
|
|
53
|
+
'thinkingComponent=hidden?new Text(theme.italic(theme.fg("thinkingText",this.hiddenThinkingLabel)),this.outputPad,0):new Markdown(thinkingBlocks.' +
|
|
54
|
+
JOIN_TPL +
|
|
55
|
+
MD_TAIL +
|
|
56
|
+
";";
|
|
57
|
+
|
|
58
|
+
// ---------- injected code ----------
|
|
59
|
+
// Current patch version; also the key for mode detection (family-wide) and
|
|
60
|
+
// migration ("any older family member → restore pristine backup, re-apply").
|
|
61
|
+
const CURRENT_MARKER = "pi-thinking-header:v3.2";
|
|
62
|
+
|
|
63
|
+
// Marker comment doubles as the extension's mode-detection key.
|
|
64
|
+
const HELPER =
|
|
65
|
+
"/*pi-thinking-header:v3.2*/" +
|
|
66
|
+
// decimals config: per-instance cache keyed on the config file's mtime
|
|
67
|
+
// (mtimeNs BigInt — ns precision beats same-ms collisions). Lazy ??= init
|
|
68
|
+
// (no class fields needed) + a reader that resolves
|
|
69
|
+
// <agentDir>/thinking-header.json without any imports:
|
|
70
|
+
// process.getBuiltinModule works inside ESM (Node >= 22.3; on older runtimes
|
|
71
|
+
// the try/catch degrades to {} → default decimals).
|
|
72
|
+
'_piCfg(){this._pthT??=0n,this._pthC??=null;try{let fs=process.getBuiltinModule("node:fs"),os=process.getBuiltinModule("node:os"),' +
|
|
73
|
+
'p=(process.env.PI_CODING_AGENT_DIR||os.homedir()+"/.pi/agent")+"/thinking-header.json",st=fs.statSync(p,{bigint:!0});' +
|
|
74
|
+
'if(st.mtimeNs!==this._pthT){let c={};try{c=JSON.parse(fs.readFileSync(p,"utf8"))}catch{}' +
|
|
75
|
+
"this._pthT=st.mtimeNs,this._pthC=c}}catch{this._pthC=null}return this._pthC||{}}" +
|
|
76
|
+
// One-line label: "Thinking (~2.50k tokens)" (prefix honours a custom label, minus trailing dots)
|
|
77
|
+
// Decimals come from _piCfg(): integer 0–6 wins, anything else → 2. No
|
|
78
|
+
// trailing-zero stripping: the setting is authoritative (2.50k stays 2.50k).
|
|
79
|
+
'thinkingLabelText(blocks){let chars=0;for(let b of blocks)chars+=b.length;' +
|
|
80
|
+
"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
|
+
'count=tokens>=1000?(tokens/1000).toFixed(d)+"k":""+tokens;' +
|
|
83
|
+
'return this.hiddenThinkingLabel.replace(/\\.{3}$/,"")+" (~"+count+" tokens)"}' +
|
|
84
|
+
// Collapsed state: plain-object component rendering ONE line:
|
|
85
|
+
// label + " · " + preview, truncated to the render width (CJK-aware).
|
|
86
|
+
// dsh ReasoningRow parity: running → latest non-empty line with the window
|
|
87
|
+
// pinned to its tail (leading "…", i.e. scrollLeft=scrollWidth-clientWidth);
|
|
88
|
+
// done → first non-empty line from its start (trailing "…", scrollLeft=0).
|
|
89
|
+
"piThinkingCollapsed(blocks,running){let label=this.thinkingLabelText(blocks),self=this,text=\"\";" +
|
|
90
|
+
'if(running){for(let i=blocks.length-1;i>=0&&!text;i--){let ls=blocks[i].split("\\n");for(let j=ls.length-1;j>=0;j--){let l=ls[j].trim();if(l){text=l;break}}}}' +
|
|
91
|
+
'else{for(let b of blocks){let line=b.split("\\n").find(l=>l.trim());if(line){text=line.trim();break}}}' +
|
|
92
|
+
"let vlen=s=>{let n=0;for(let ch of s)n+=ch.codePointAt(0)>=0x2e80?2:1;return n};" +
|
|
93
|
+
"return {render(w){let inner=Math.max(8,w-self.outputPad*2),max=inner-vlen(label)-3;" +
|
|
94
|
+
"if(max<1){let padTotal=inner-vlen(label);if(padTotal<0)padTotal=0;" +
|
|
95
|
+
'return [" ".repeat(self.outputPad)+theme.italic(theme.fg("thinkingText",label))+" ".repeat(self.outputPad+padTotal)]}' +
|
|
96
|
+
"let prev=text;" +
|
|
97
|
+
"if(vlen(prev)>max){if(running){let out=[],n=0,cs=[...prev];for(let i=cs.length-1;i>=0;i--){let cw=cs[i].codePointAt(0)>=0x2e80?2:1;if(n+cw>max-1)break;out.push(cs[i]);n+=cw}prev=\"…\"+out.reverse().join(\"\")}" +
|
|
98
|
+
"else{let out=\"\",n=0;for(let ch of prev){let cw=ch.codePointAt(0)>=0x2e80?2:1;if(n+cw>max-1)break;out+=ch;n+=cw}prev=out+\"…\"}}" +
|
|
99
|
+
'let line=label+" · "+prev;let padTotal=inner-vlen(line);if(padTotal<0)padTotal=0;' +
|
|
100
|
+
// v3.1: the returned plain object MUST implement invalidate() — MouseRegion
|
|
101
|
+
// forwards lifecycle unconditionally (invalidate(){this.child.invalidate()}),
|
|
102
|
+
// so a render-only object crashed pi on exit/mode-switch with
|
|
103
|
+
// "this.child.invalidate is not a function". The object is stateless (render
|
|
104
|
+
// output is computed fresh on every call), so a no-op invalidate is correct.
|
|
105
|
+
'return [" ".repeat(self.outputPad)+theme.italic(theme.fg("thinkingText",line))+" ".repeat(self.outputPad+padTotal)]},invalidate(){}}}';
|
|
106
|
+
|
|
107
|
+
const PATCH2 =
|
|
108
|
+
"thinkingComponent=hidden?this.piThinkingCollapsed(thinkingBlocks,this.isStreaming):" +
|
|
109
|
+
"(()=>{let body=new Markdown(thinkingBlocks." +
|
|
110
|
+
JOIN_TPL +
|
|
111
|
+
MD_TAIL +
|
|
112
|
+
",box=new Container;" +
|
|
113
|
+
"return box.addChild(" +
|
|
114
|
+
HEADER_TEXT +
|
|
115
|
+
"),box.addChild(body),box})();";
|
|
116
|
+
|
|
117
|
+
// ---------- safety net: parse-check before writing ----------
|
|
118
|
+
// Same ESM parser pi's loader uses (vm.SourceTextModule), never executes the
|
|
119
|
+
// code. A brace-counting mistake must abort the install, not brick pi.
|
|
120
|
+
function verifyESM(src) {
|
|
121
|
+
const probe =
|
|
122
|
+
'const vm=require("node:vm");let d="";' +
|
|
123
|
+
'process.stdin.on("data",(c)=>d+=c).on("end",()=>{' +
|
|
124
|
+
'try{new vm.SourceTextModule(d,{identifier:"candidate"});process.exit(0)}' +
|
|
125
|
+
'catch(e){console.error(e.message);process.exit(1)}});';
|
|
126
|
+
const r = spawnSync(process.execPath, ["--experimental-vm-modules", "-e", probe], {
|
|
127
|
+
input: src,
|
|
128
|
+
encoding: "utf8",
|
|
129
|
+
timeout: 30_000,
|
|
130
|
+
});
|
|
131
|
+
if (r.error) throw r.error;
|
|
132
|
+
if (r.status !== 0) {
|
|
133
|
+
throw new Error("Candidate bundle failed ESM parse check: " + (r.stderr || "unknown").trim());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------- locate target chunk ----------
|
|
138
|
+
function locateChunk() {
|
|
139
|
+
if (process.argv[2]) return process.argv[2];
|
|
140
|
+
const bin = execSync("readlink -f $(which pi)").toString().trim(); // <pkg>/dist/bundle/cli.js
|
|
141
|
+
const chunksDir = join(dirname(bin), "chunks");
|
|
142
|
+
for (const f of readdirSync(chunksDir)) {
|
|
143
|
+
if (!f.endsWith(".js")) continue;
|
|
144
|
+
const p = join(chunksDir, f);
|
|
145
|
+
if (readFileSync(p, "utf8").includes(ANCHOR1)) return p;
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Could not find the AssistantMessageComponent chunk under ${chunksDir}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------- apply (with v1 migration) ----------
|
|
151
|
+
const chunkPath = locateChunk();
|
|
152
|
+
const backup = chunkPath + ".pre-thinking-header.bak";
|
|
153
|
+
let src = readFileSync(chunkPath, "utf8");
|
|
154
|
+
|
|
155
|
+
// Distinctive tail of piThinkingCollapsed's returned object, with and without
|
|
156
|
+
// the v3.1 invalidate() fix. `padTotal)]}}}` can only match the final return —
|
|
157
|
+
// the max<1 branch ends with a single `}` before `let prev=text;` — and it
|
|
158
|
+
// does NOT occur inside TAIL_FIXED (there `)]` is followed by a single `}`
|
|
159
|
+
// then `,`). TAIL_FIXED closes, in order: render fn, the invalidate method
|
|
160
|
+
// body (inside its own `(){}`), the object literal, and finally the
|
|
161
|
+
// piThinkingCollapsed class method itself — four `}` after `]` in total.
|
|
162
|
+
const TAIL_BROKEN = "padTotal)]}}}";
|
|
163
|
+
const TAIL_FIXED = "padTotal)]},invalidate(){}}}";
|
|
164
|
+
|
|
165
|
+
if (src.includes(CURRENT_MARKER) && src.includes(TAIL_FIXED)) {
|
|
166
|
+
console.log(`Already patched (v3.2): ${chunkPath}`);
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Any older member of the patch family (v1/v2/v3/v3.1, detected via the family
|
|
171
|
+
// string or the v1/v2 helper name) → restore pristine, then re-apply fresh.
|
|
172
|
+
if (src.includes("pi-thinking-header") || src.includes("thinkingLabelText(")) {
|
|
173
|
+
// Early v3 (pre-3.1) shipped a render-only collapsed component: MouseRegion
|
|
174
|
+
// .invalidate crashed pi on exit/mode-switch ("this.child.invalidate is not
|
|
175
|
+
// a function"). Without a pristine backup we cannot re-apply, so hotfix that
|
|
176
|
+
// one shape in place; a later installer run (with a backup) migrates fully.
|
|
177
|
+
if (src.includes(TAIL_BROKEN) && !existsSync(backup)) {
|
|
178
|
+
src = src.replace(TAIL_BROKEN, TAIL_FIXED);
|
|
179
|
+
if (!src.includes(TAIL_FIXED)) throw new Error("v3 → v3.1 hotfix failed");
|
|
180
|
+
verifyESM(src);
|
|
181
|
+
writeFileSync(chunkPath, src);
|
|
182
|
+
console.log("Hotfixed (early v3 → v3.1): added missing invalidate() to the collapsed-thinking component");
|
|
183
|
+
console.log(`File: ${chunkPath}`);
|
|
184
|
+
console.log("Re-run the installer to update the patch to v3.2 (configurable decimals).");
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
if (!existsSync(backup)) {
|
|
188
|
+
throw new Error("Older pi-thinking-header patch found but no pristine backup; reinstall pi, then re-run.");
|
|
189
|
+
}
|
|
190
|
+
src = readFileSync(backup, "utf8");
|
|
191
|
+
console.log("Migrating: older pi-thinking-header patch detected, restored pristine bundle from backup.");
|
|
192
|
+
} else if (!existsSync(backup)) {
|
|
193
|
+
copyFileSync(chunkPath, backup);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!src.includes(ANCHOR1)) throw new Error("ANCHOR1 not found (pi version changed?)");
|
|
197
|
+
if (!src.includes(ANCHOR2)) throw new Error("ANCHOR2 not found (pi version changed?)");
|
|
198
|
+
|
|
199
|
+
src = src.replace(ANCHOR1, ANCHOR1 + HELPER);
|
|
200
|
+
src = src.replace(ANCHOR2, PATCH2);
|
|
201
|
+
|
|
202
|
+
if (!src.includes(CURRENT_MARKER) || !src.includes("piThinkingCollapsed(") || !src.includes(TAIL_FIXED)) {
|
|
203
|
+
throw new Error("Patch did not apply");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
verifyESM(src);
|
|
207
|
+
writeFileSync(chunkPath, src);
|
|
208
|
+
console.log(`Patched (v3.2): ${chunkPath}`);
|
|
209
|
+
console.log(`Backup: ${backup}`);
|
|
210
|
+
console.log("Revert with: cp '" + backup + "' '" + chunkPath + "'");
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-thinking-header",
|
|
3
|
+
"version": "0.3.2",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"thinking",
|
|
9
|
+
"tui"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "focksor <focksor@outlook.com>",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/focksor/pi-thinking-header.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/focksor/pi-thinking-header/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/focksor/pi-thinking-header#readme",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22.19.0"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"index.js",
|
|
27
|
+
"install-patch.mjs"
|
|
28
|
+
],
|
|
29
|
+
"pi": {
|
|
30
|
+
"extensions": ["./index.js"]
|
|
31
|
+
}
|
|
32
|
+
}
|