pi-thinking-header 0.3.4 → 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.
Files changed (3) hide show
  1. package/README.md +21 -9
  2. package/index.js +162 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -42,17 +42,26 @@ pi install ./pi-thinking-header
42
42
  所有功能的统一入口:
43
43
 
44
44
  ```
45
- /thinking-header # 状态总览(模式/补丁状态/decimals)+ 用法
45
+ /thinking-header # 状态总览(模式/补丁状态/autoPatch/decimals)+ 用法
46
46
  /thinking-header patch # 应用/修复完整模式补丁(确认后执行)
47
+ /thinking-header autopatch # 查看启动自动补丁状态
48
+ /thinking-header autopatch on|off# 启动时自动检查并应用/迁移补丁(默认 on)
47
49
  /thinking-header decimals # 查看当前小数位
48
50
  /thinking-header decimals <0-6> # 设置小数位并持久化(默认 2)
49
51
  ```
50
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
+
51
60
  补丁安装(`patch` 子命令,内部运行 `node install-patch.mjs`):
52
61
 
53
- - 已打补丁 → 提示 already applied(含当前 decimals)
54
- - 未打补丁(或补丁版本较旧)→ 确认后执行安装/迁移,输出结果并提醒重启 pi(当前会话仍是旧代码,重启后生效)
55
- - `pi update` 之后补丁丢失时,extension 自动进入降级模式,此时用该命令可一键修复
62
+ - 已是当前版本 → 提示 already applied(含当前 decimals)
63
+ - 未打补丁(或补丁版本较旧 → 迁移)→ 确认后执行安装/迁移,输出结果并提醒重启 pi(当前会话仍是旧代码,重启后生效)
64
+ - `pi update` 之后补丁丢失时,extension 自动进入降级模式;启动自动补丁(默认开)会在下次会话开始时自动修复,也可用该命令立即修复
56
65
 
57
66
  小数位配置(`decimals` 子命令):
58
67
 
@@ -67,7 +76,8 @@ pi install ./pi-thinking-header
67
76
 
68
77
  pi 的 extension API 无法接管内置 thinking 块的折叠渲染,完整效果需要附带
69
78
  的 bundle patch。推荐直接在 pi 会话内运行 `/thinking-header patch`
70
- (自动定位安装位置);也可手动执行:
79
+ (自动定位安装位置);也可什么都不做——默认开启的启动自动补丁会在首次
80
+ 会话开始时自动应用。手动执行:
71
81
 
72
82
  ```bash
73
83
  # npm 安装:~/.pi/agent/npm/pi-thinking-header/install-patch.mjs
@@ -84,8 +94,9 @@ node <包目录>/install-patch.mjs
84
94
  - 小数点位数渲染时实时读取配置文件(mtime 缓存),与降级模式共享同一份配置
85
95
  - 幂等;自动从 v1/v2/v3/v3.1/v3.2 patch 迁移(恢复原始备份后重新应用;早期 v3
86
96
  折叠组件缺 `invalidate()`、退出/切换模式时触发 `this.child.invalidate is not
87
- a function` 崩溃且无备份时,会先原地热修复);`pi update` 后重跑一次即可;
88
- 插件包更新后也建议重跑 `/thinking-header patch` 使 patch 与 extension 版本一致
97
+ a function` 崩溃且无备份时,会先原地热修复);`pi update` 后无需手动处理——
98
+ 启动自动补丁(默认开)会在下次会话开始时重新应用/迁移;插件包更新后同样如此
99
+ (patch 版本随插件更新时自动迁移到新版)
89
100
  - 回滚:
90
101
  ```bash
91
102
  cp <chunk路径>.pre-thinking-header.bak <chunk路径>
@@ -95,8 +106,9 @@ node <包目录>/install-patch.mjs
95
106
  ### 纯插件模式:extension 自动降级
96
107
 
97
108
  extension 启动时探测运行中的 pi bundle:**检测到任意版本的 patch 时**(含旧版本)
98
- extension 保持惰性(避免双标题),请运行 `/thinking-header patch` 迁移到当前 patch 版本;
99
- **未检测到 patch 时**自动进入降级模式:
109
+ extension 保持惰性(避免双标题;旧版本会在下次会话启动时被自动迁移到当前版本,
110
+ 或运行 `/thinking-header patch` 立即迁移);**未检测到 patch 时**自动进入降级模式,
111
+ 并在会话启动时自动应用补丁(默认行为,可关闭):
100
112
 
101
113
  - 展开的 thinking:通过 markdown transformer 前置 `Thinking (~N tokens)` 标题行
102
114
  (按块正确计数,`pi update` 后依然有效,无需维护)。流式期间同样保持稳定
package/index.js CHANGED
@@ -36,6 +36,14 @@
36
36
  * older transcript messages will show the most recent label. Run
37
37
  * /thinking-header patch for per-message fidelity.
38
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
+ *
39
47
  * Token counts are local estimates: ceil(chars / 4). The k-token decimal places
40
48
  * are configurable (default 2): /thinking-header decimals <0-6>, persisted to
41
49
  * <agentDir>/thinking-header.json ({"decimals": 2}). Both modes re-read the
@@ -56,9 +64,16 @@ const INSTALLER_PATH = fileURLToPath(new URL("./install-patch.mjs", import.meta.
56
64
 
57
65
  // Family-wide marker: ANY patch version owns header rendering end-to-end, so
58
66
  // the extension must stay inert for all of them (avoids double headers). The
59
- // /thinking-header patch migrates older patch versions to the current one.
67
+ // /thinking-header patch (and the startup auto-patch) migrates older patch
68
+ // versions to the current one.
60
69
  const MARKER = "pi-thinking-header";
61
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
+
62
77
  // ---------------- decimals config (shared with the bundle patch) -------------
63
78
  // <agentDir>/thinking-header.json: { "decimals": 2 } — k-token decimal places.
64
79
  // The bundle patch reads the same file at render time (mtime-cached), so both
@@ -100,7 +115,10 @@ const getDecimals = () => {
100
115
  return typeof d === "number" && Number.isInteger(d) && d >= 0 && d <= 6 ? d : DEFAULT_DECIMALS;
101
116
  };
102
117
 
103
- function saveDecimals(n) {
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) {
104
122
  let cfg = {};
105
123
  try {
106
124
  const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
@@ -108,7 +126,7 @@ function saveDecimals(n) {
108
126
  } catch {
109
127
  // no/invalid config → start fresh, keep nothing
110
128
  }
111
- cfg.decimals = n;
129
+ cfg[key] = value;
112
130
  mkdirSync(dirname(CONFIG_PATH), { recursive: true });
113
131
  writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
114
132
  configCache.mtime = -1n; // force re-read on next loadConfig()
@@ -119,7 +137,9 @@ const COMPONENT_ANCHOR =
119
137
 
120
138
  /**
121
139
  * Locate the running pi's bundle chunk that contains AssistantMessageComponent.
122
- * Returns { found: true, patched: boolean } or { found: false }.
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)
123
143
  */
124
144
  function probeBundle() {
125
145
  try {
@@ -134,7 +154,11 @@ function probeBundle() {
134
154
  const path = join(chunksDir, file);
135
155
  const src = readFileSync(path, "utf8");
136
156
  if (!src.includes(COMPONENT_ANCHOR)) continue;
137
- return { found: true, patched: src.includes(MARKER) };
157
+ return {
158
+ found: true,
159
+ patched: src.includes(MARKER),
160
+ current: src.includes(CURRENT_MARKER),
161
+ };
138
162
  }
139
163
  } catch {
140
164
  // pi binary not resolvable — fall through
@@ -210,10 +234,58 @@ const thinkingLabel = (blocks, running) => {
210
234
  return label;
211
235
  };
212
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
+
213
284
  export default function (pi) {
214
285
  // /thinking-header — single entry point for everything this package does:
215
286
  // /thinking-header status overview + usage
216
287
  // /thinking-header patch apply/repair the full-mode bundle patch
288
+ // /thinking-header autopatch on|off startup auto-check+apply (default on)
217
289
  // /thinking-header decimals show the k-token decimal places
218
290
  // /thinking-header decimals <0-6> set + persist them (0-6, default 2)
219
291
  // Registered in BOTH modes. Fallback mode re-reads the config per label; the
@@ -223,21 +295,38 @@ export default function (pi) {
223
295
  "usage:\n" +
224
296
  " /thinking-header status overview\n" +
225
297
  " /thinking-header patch apply/repair the full-mode patch\n" +
298
+ " /thinking-header autopatch on|off startup auto-check+apply (default on)\n" +
226
299
  " /thinking-header decimals show k-token decimal places\n" +
227
300
  " /thinking-header decimals <0-6> set them (persisted, applies live)";
228
301
 
229
302
  pi.registerCommand("thinking-header", {
230
- description:
231
- "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",
232
304
  getArgumentCompletions: (prefix) => {
233
305
  const p = (prefix ?? "").trimStart();
234
- if (p.includes(" ")) return null;
235
- const items = [
236
- { value: "patch", label: "apply/repair the full-mode bundle patch" },
237
- { value: "decimals", label: "k-token decimal places (0-6, default 2)" },
238
- ];
239
- const filtered = items.filter((i) => i.value.startsWith(p));
240
- return filtered.length > 0 ? filtered : null;
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
241
330
  },
242
331
  handler: async (args, ctx) => {
243
332
  const argv = (args ?? "").trim().split(/\s+/).filter(Boolean);
@@ -260,7 +349,7 @@ export default function (pi) {
260
349
  return;
261
350
  }
262
351
  try {
263
- saveDecimals(n);
352
+ saveConfigKey("decimals", n);
264
353
  } catch (err) {
265
354
  ctx.ui.notify(`pi-thinking-header: failed to save ${CONFIG_PATH}: ${err?.message ?? err}`, "error");
266
355
  return;
@@ -273,6 +362,42 @@ export default function (pi) {
273
362
  return;
274
363
  }
275
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
+
276
401
  // ---- patch subcommand: apply/repair the full-mode bundle patch ----
277
402
  if (argv[0] === "patch") {
278
403
  try {
@@ -284,9 +409,9 @@ export default function (pi) {
284
409
  );
285
410
  return;
286
411
  }
287
- if (state.patched) {
412
+ if (state.current) {
288
413
  ctx.ui.notify(
289
- `pi-thinking-header: full-mode patch already applied (v3.3) — decimals = ${getDecimals()}`,
414
+ `pi-thinking-header: full-mode patch already applied (${PATCH_VERSION}) — decimals = ${getDecimals()}`,
290
415
  "info",
291
416
  );
292
417
  return;
@@ -322,12 +447,15 @@ export default function (pi) {
322
447
  const state = probeBundle();
323
448
  const mode = !state.found
324
449
  ? "fallback (pi installation not detected)"
325
- : state.patched
326
- ? "full (bundle patch v3.3 applied)"
327
- : "fallback (bundle unpatched — /thinking-header patch enables full mode)";
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)";
328
455
  ctx.ui.notify(
329
456
  "pi-thinking-header\n" +
330
457
  `mode: ${mode}\n` +
458
+ `autoPatch: ${getAutoPatch() ? "on" : "off"} (default on)\n` +
331
459
  `decimals: ${getDecimals()} (default ${DEFAULT_DECIMALS})\n` +
332
460
  `config: ${CONFIG_PATH}\n` +
333
461
  usage,
@@ -337,10 +465,21 @@ export default function (pi) {
337
465
  });
338
466
 
339
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
+
340
480
  if (bundle.found && bundle.patched) {
341
- // FULL MODE: the bundle patch owns rendering end-to-end (per-message labels,
342
- // width-aware single-line previews). Stay inert — a global label update here
343
- // 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).
344
483
  return;
345
484
  }
346
485
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-thinking-header",
3
- "version": "0.3.4",
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",