@andares/pi-toolkits 0.9.7 → 0.9.8
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 +7 -7
- package/README.zh-CN.md +7 -7
- package/package.json +5 -5
- package/src/features/ask/reasons.ts +1 -1
- package/src/features/autostart/autostart.ts +1 -1
- package/src/features/autostart/index.ts +1 -1
- package/src/features/compact/compact.test.ts +33 -10
- package/src/features/compact/index.ts +27 -34
- package/src/features/compact/prompt.ts +23 -12
- package/src/features/favorites/constants.ts +6 -5
- package/src/features/favorites/patch-cycle.ts +4 -3
- package/src/features/favorites/patch-selector.ts +2 -2
- package/src/features/stash/stash.ts +1 -1
- package/src/lib/gates.ts +4 -0
- package/src/lib/model-config.ts +2 -2
- package/src/lib/model-picker.ts +1 -1
package/README.md
CHANGED
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
[](https://pi.dev/packages/@andares/pi-toolkits)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://pnpm.io)
|
|
13
|
-
[](https://github.com/earendil-works/pi)
|
|
14
14
|
[](https://github.com/andares/pi-toolkits)
|
|
15
15
|
|
|
16
16
|
In a nutshell: seven ready-to-use capabilities — **read-only consult mode** `ask`, **session-scoped boundary permission** `ask-perm`, **model favorites** `favorites`, **prompt stash** `stash`, **third-party code review** `third-review`, **dedicated compaction model** `compact-model` and **session startup automation** `autostart` — with more to come.
|
|
17
17
|
|
|
18
|
-
**Requirements**: pi `>=
|
|
18
|
+
**Requirements**: pi `>=0.99.0`. Every published pi release from 0.99.0 through 1.4.0 has been verified with the full gate (install → `tsc --noEmit` → full test suite → keybinding audit → hash compare of the host files this package patches), each in a sandbox with the whole `@earendil-works/*` family pinned to the version under test. Newer releases are verified as they ship and the floor moves only when the measurement says so; `pnpm check:compat` reproduces the result. Note: pi ≥ 0.99 hosts require host-provided packages to be declared in `peerDependencies` with a `"*"` range — `@earendil-works/pi-tui` and `typebox` (the `scratchpad` tool builds a TypeBox schema) are declared that way; an installed copy in `dependencies` would make pi warn.
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
@@ -255,11 +255,11 @@ pi's `/compact` and auto-compact summarize the context **with the current sessio
|
|
|
255
255
|
| auto-compact threshold reached | summarized by the compaction model |
|
|
256
256
|
| context overflow recovery | summarized by the compaction model, retry behavior unchanged |
|
|
257
257
|
|
|
258
|
-
**Parts that are identical to pi's built-in behavior after takeover** (the built-in implementation is mirrored verbatim — copied from 0.87.1, byte-identical across
|
|
258
|
+
**Parts that are identical to pi's built-in behavior after takeover** (the built-in implementation is mirrored verbatim — copied from 0.87.1, byte-identical across 1.1.0 → 1.4.0 apart from the two 1.4.0 fixes adopted here: summaries request the model's output limit instead of a fixed fraction of `reserveTokens`, and `/compact <instructions>` also reaches the split-turn prefix — and only the executing model changes; the conversation serializer itself is imported from the host, so it follows whatever the running host does, e.g. 1.2.0 stops feeding assistant thinking back into the summarization request):
|
|
259
259
|
|
|
260
260
|
- **Summary format** — the structured Goal / Progress / Key Decisions checkpoint; when a summary already exists, the iterative-update prompt is used so information carries over instead of being lost
|
|
261
261
|
- **Split behavior** — the `keepRecentTokens` retention policy, and `firstKeptEntryId` / `tokensBefore` passed back unchanged
|
|
262
|
-
- **Output budget** — the
|
|
262
|
+
- **Output budget** — summaries ask for the model's own output limit, exactly like the built-in since 1.4.0 (#9512: a fixed 0.8×/0.5× `reserveTokens` cap made long reasoning fail the summary with "generation hit the token cap"); `reserveTokens` only decides when compaction starts
|
|
263
263
|
- **File-list tail** — `<read-files>` / `<modified-files>` tags are appended to the summary as usual, with details maintained in the same shape
|
|
264
264
|
- **Usage accounting** — the compaction model's tokens/cost are counted in the session statistics
|
|
265
265
|
- **Cancellable** — `Esc` during compaction cancels cleanly (the signal is passed through to the underlying call)
|
|
@@ -336,7 +336,7 @@ An **identical command line** declared by both sources runs once: a duplicate is
|
|
|
336
336
|
|
|
337
337
|
**Key constraint: extension commands only**. Commands registered via `registerCommand` such as `/add-dir` (pi-add-dir) and `/cd` (pi-cd) work; built-in commands (`/compact`, `/reload`, `/resume`, …) are dispatched in the TUI layer and cannot be triggered from the extension layer; skill commands (`/skill:name`) are an expansion mechanism rather than an execution mechanism. The target extension you depend on (e.g. pi-add-dir) must be installed first.
|
|
338
338
|
|
|
339
|
-
> **Implementation notes**: execution uses `sendUserMessage(cmd, { expandPromptTemplates: true })` — that option routes `/cmd args` into command dispatch instead of the model prompt path (the wrapper defaults to `false`; `sendUserMessage` and `_tryExecuteExtensionCommand` are byte-identical across 1.
|
|
339
|
+
> **Implementation notes**: execution uses `sendUserMessage(cmd, { expandPromptTemplates: true })` — that option routes `/cmd args` into command dispatch instead of the model prompt path (the wrapper defaults to `false`; `sendUserMessage` and `_tryExecuteExtensionCommand` are byte-identical across 1.1.0–1.4.0, see the host-compatibility discipline in AGENTS.md). Pre-validation uses `pi.getCommands()` (accepting only `source: "extension"`). Depends on `smol-toml` (zero-dependency, ~10KB, TOML 1.0).
|
|
340
340
|
|
|
341
341
|
---
|
|
342
342
|
|
|
@@ -352,7 +352,7 @@ pnpm typecheck # tsc --noEmit
|
|
|
352
352
|
pnpm test # vitest (all feature unit tests)
|
|
353
353
|
|
|
354
354
|
pnpm check:keybindings # audit this plugin's keys against the host's own dispatch table
|
|
355
|
-
pnpm check:compat #
|
|
355
|
+
pnpm check:compat # mandatory 4-line window + backward probe (4 more lines) → public floor
|
|
356
356
|
```
|
|
357
357
|
|
|
358
358
|
### Project structure
|
|
@@ -414,7 +414,7 @@ PRs and issues welcome. A few conventions:
|
|
|
414
414
|
- **Built-in patches** (favorites) must stay **version-guarded and idempotent** — verify the target prototype members exist before patching, guard against double-apply, degrade with a `console.warn` instead of breaking the extension
|
|
415
415
|
- **Built-in prompt sync** (compact) — the summarization prompts in `src/features/compact/prompt.ts` are copied verbatim from pi's built-in compaction; when upgrading the pi dependency, diff `dist/core/compaction/compaction.js` (and `utils.js`) and update the strings + assembly helpers to match, keeping takeover output byte-compatible with stock
|
|
416
416
|
- **Keybindings** — before picking a new shortcut, check pi defaults + reserved keys, terminal control chars, and OS/terminal-emulator grabs; prefer `ctrl+alt+<letter>` for cross-platform safety. Do not stop at "it works on my machine in the main screen": in `fullscreen` mode (pi's default since 1.0) the alt-screen layer consumes `tui.altScreen.*` **before** dialogs see the key, and on Windows/WSL several of those bindings differ from the Linux defaults. Run `pnpm check:keybindings [candidate...]` (and `pnpm check:keybindings --all` after a pi upgrade) — it probes the installed host's resolved keybinding table with the host's own `matchesKey`. A key free in the host table can still be taken at the machine layer: Windows Terminal binds `ctrl+v` (paste) and `ctrl+shift+f` (find), and third-party Windows tools commonly grab `ctrl+alt+<letter>` (a screenshot tool once took `ctrl+alt+f`) — so try the key for real on the target machine before shipping it.
|
|
417
|
-
- **Host compatibility** — the peer range is a measured statement, not a wish: the four most recently released pi minor lines are
|
|
417
|
+
- **Host compatibility** — the peer range is a measured statement, not a wish, and it only points backwards: the four most recently released pi minor lines are a mandatory window (fix our code, never raise the floor), below it we probe up to four older lines to find where compatibility actually ends, and the single number users see is the resulting floor (`pi >= <floor>`, no upper bound). Re-run `pnpm check:compat` whenever the host dependency moves and read its verdict block (it fails if the declared floor drifts from the measured one). See the pi 宿主兼容 section of AGENTS.md; keybinding collisions found during an upgrade are reported to the maintainer for a decision, never re-keyed silently.
|
|
418
418
|
- **Quality gate** — `pnpm typecheck && pnpm test` must pass before submitting
|
|
419
419
|
|
|
420
420
|
---
|
package/README.zh-CN.md
CHANGED
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
[](https://pi.dev/packages/@andares/pi-toolkits)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://pnpm.io)
|
|
13
|
-
[](https://github.com/earendil-works/pi)
|
|
14
14
|
[](https://github.com/andares/pi-toolkits)
|
|
15
15
|
|
|
16
16
|
一句话介绍:目前包含七个开箱即用的功能——**只读咨询模式** `ask`、**会话级边界权限** `ask-perm`、**模型收藏** `favorites`、**提示词暂存** `stash`、**第三方代码评审** `third-review`、**独立压缩模型** `compact-model`、**会话启动自动化** `autostart`,后续持续追加。
|
|
17
17
|
|
|
18
|
-
**版本要求**:pi `>=
|
|
18
|
+
**版本要求**:pi `>=0.99.0`。0.99.0 到 1.4.0 的每一个已发布版本都用完整门禁实测过(install → `tsc --noEmit` → 全量测试 → 键位审计 → 对 patch 目标宿主文件做哈希比对),每个版本都在沙箱中把整个 `@earendil-works/*` 家族钉在同一版本。更新的版本随发布逐个验证,下限只在实测结果允许时移动;`pnpm check:compat` 可复现该结论。注:pi ≥ 0.99 的宿主要求「宿主提供的包」一律声明为 peerDependencies `"*"` —— `@earendil-works/pi-tui` 与 `typebox`(ask 的 `scratchpad` 用它构造参数 schema)都按此声明;若放进 `dependencies`,pi 会告警。
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
@@ -255,11 +255,11 @@ pi 的 `/compact` 与 auto-compact 默认**用当前会话模型**做上下文
|
|
|
255
255
|
| auto-compact 阈值触发 | 压缩模型总结 |
|
|
256
256
|
| context overflow 恢复 | 压缩模型总结,重试行为不变 |
|
|
257
257
|
|
|
258
|
-
**接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,复制自 0.87.1
|
|
258
|
+
**接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,复制自 0.87.1、在 1.1.0 → 1.4.0 内字节级不变 —— 除了此处采纳的两项 1.4.0 修正:摘要请求使用模型自身的输出上限而非 `reserveTokens` 的固定比例,以及 `/compact <instructions>` 也会进入 split-turn 前缀;只换执行模型;对话序列化函数(`serializeConversation`)是直接从宿主导入的,因此跟随宿主行为 —— 例如 1.2.0 起不再把 assistant thinking 回填进摘要请求):
|
|
259
259
|
|
|
260
260
|
- **摘要格式** —— Goal / Progress / Key Decisions 结构化 checkpoint;已有摘要时用迭代更新版 prompt,信息延续不丢
|
|
261
261
|
- **切分行为** —— 保留最近 `keepRecentTokens` 的策略、`firstKeptEntryId` / `tokensBefore` 原样回传,不动
|
|
262
|
-
- **输出预算** ——
|
|
262
|
+
- **输出预算** —— 摘要请求使用模型自身的输出上限,与 1.4.0 起的内建一致(#9512:过去按 0.8×/0.5× `reserveTokens` 设上限,长思考会把摘要顶到 "generation hit the token cap" 而失败);`reserveTokens` 只决定何时开始压缩
|
|
263
263
|
- **文件清单尾部** —— `<read-files>` / `<modified-files>` 标签照常拼在摘要尾部,details 同形状维护
|
|
264
264
|
- **用量统计** —— 压缩模型的 token/cost 计入 session 统计
|
|
265
265
|
- **可取消** —— 压缩进行中 `Esc` 干净取消(信号透传到底层调用)
|
|
@@ -336,7 +336,7 @@ run = [
|
|
|
336
336
|
|
|
337
337
|
**关键约束:只支持扩展命令**。`/add-dir`(pi-add-dir)、`/cd`(pi-cd)等 `registerCommand` 注册的命令可以;内置命令(`/compact`、`/reload`、`/resume`…)在 TUI 层分发,扩展层触发不了;skill 命令(`/skill:name`)是展开机制而非执行机制。依赖的目标扩展(如 pi-add-dir)需要先安装。
|
|
338
338
|
|
|
339
|
-
> **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,`sendUserMessage` 与 `_tryExecuteExtensionCommand` 在 1.
|
|
339
|
+
> **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,`sendUserMessage` 与 `_tryExecuteExtensionCommand` 在 1.1.0–1.4.0 全区间字节级一致,见 AGENTS.md 宿主兼容纪律)。预校验用 `pi.getCommands()`(只认 `source: "extension"`)。依赖 `smol-toml`(零依赖、~10KB、TOML 1.0)。
|
|
340
340
|
|
|
341
341
|
---
|
|
342
342
|
|
|
@@ -352,7 +352,7 @@ pnpm typecheck # tsc --noEmit
|
|
|
352
352
|
pnpm test # vitest(全部功能单元测试)
|
|
353
353
|
|
|
354
354
|
pnpm check:keybindings # 用宿主自己的分发表审计本插件的按键
|
|
355
|
-
pnpm check:compat # 校验声明的 pi
|
|
355
|
+
pnpm check:compat # 校验声明的 pi 下限:强制 4 条线窗口 + 向后 4 条线探针 → public floor
|
|
356
356
|
```
|
|
357
357
|
|
|
358
358
|
### 项目结构
|
|
@@ -412,7 +412,7 @@ pnpm release:github --audit # 列出 npm 版本 vs GitHub Release vs 远端 t
|
|
|
412
412
|
- **内建 patch**(favorites)必须保持**版本守卫 + 幂等** —— 打 patch 前先确认目标原型成员存在,防止重复应用,失败时用 `console.warn` 降级而不是把扩展搞挂
|
|
413
413
|
- **内建提示词同步**(compact) —— `src/features/compact/prompt.ts` 里的总结 prompt 逐字抄自 pi 内建压缩;升级 pi 依赖时 diff `dist/core/compaction/compaction.js`(及 `utils.js`),同步字符串与组装辅助函数,保持接管输出与内建字节级一致
|
|
414
414
|
- **键位** —— 定新快捷键前先查 pi 默认键位 + 保留键、终端控制字符、OS/终端模拟器占用;跨平台安全优先选 `ctrl+alt+<字母>`。**不得只验证「主屏下我这台机器能用」**:自 pi 1.0 起默认 `fullscreen`,alt-screen 层在对话框看到按键**之前**就消费 `tui.altScreen.*`,且 Windows/WSL 上多个绑定与 Linux 默认值不同。用 `pnpm check:keybindings [候选键...]` 核查(pi 升级后跑 `pnpm check:keybindings --all`),它用宿主自己的 `matchesKey` 探测已安装宿主的**已解析**键位表。**宿主表空闲 ≠ 机器层空闲**:Windows Terminal 默认占 `ctrl+v`(粘贴)与 `ctrl+shift+f`(查找),第三方 Windows 工具常绑 `ctrl+alt+<字母>`(截图工具占过 `ctrl+alt+f`)——定键后必须在真机试按,确认按键真的到得了应用。
|
|
415
|
-
- **宿主兼容** —— peer
|
|
415
|
+
- **宿主兼容** —— peer 区间是实测结论而非愿望,而且只往回指:最近 4 个已发布中版本线是**强制窗口**(不兼容就改我们的代码,绝不抬下限),窗口之下再向后探最多 4 条线找出兼容实际停在哪里,对使用者只展示一个数:由此得到的**下限**(`pi >= <floor>`,无上界)。宿主依赖一动就重跑 `pnpm check:compat`,读它的 verdict 段(声明下限与实测不一致会直接判失败)。详见 AGENTS.md「pi 宿主兼容」;升级中发现的键位冲突**上报崔总决策**,不得顺手换键。
|
|
416
416
|
- **质量门禁** —— 提交前必须通过 `pnpm typecheck && pnpm test`
|
|
417
417
|
|
|
418
418
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andares/pi-toolkits",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), session-scoped permission checks, model favorites, prompt stash, third-party code review, session autostart, and a dedicated compaction model",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"boundary"
|
|
34
34
|
],
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@earendil-works/pi-coding-agent": ">=
|
|
36
|
+
"@earendil-works/pi-coding-agent": ">=0.99.0",
|
|
37
37
|
"@earendil-works/pi-tui": "*",
|
|
38
38
|
"typebox": "*"
|
|
39
39
|
},
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"smol-toml": "^1.9.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@earendil-works/pi-ai": "^1.
|
|
47
|
-
"@earendil-works/pi-coding-agent": "^1.
|
|
48
|
-
"@earendil-works/pi-tui": "^1.
|
|
46
|
+
"@earendil-works/pi-ai": "^1.4.0",
|
|
47
|
+
"@earendil-works/pi-coding-agent": "^1.4.0",
|
|
48
|
+
"@earendil-works/pi-tui": "^1.4.0",
|
|
49
49
|
"@types/node": "^26.6.2",
|
|
50
50
|
"typebox": "^1.3.27",
|
|
51
51
|
"typescript": "^5.6.0",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ask feature — actionable rejection messages.
|
|
3
3
|
*
|
|
4
4
|
* The `tool_call` hook's `reason` is delivered to the model as the tool result
|
|
5
|
-
* (verified against pi 1.
|
|
5
|
+
* (verified against pi 1.4.0: `extensions/runner.js` returns the `{block,
|
|
6
6
|
* reason}` result unchanged and pi-agent-core's `agent-loop.js` turns it into
|
|
7
7
|
* `createErrorToolResult(reason)`; both paths are byte-identical to 1.2.0), so
|
|
8
8
|
* it is the best place to name the nearest allowed alternative and stop the model
|
|
@@ -48,7 +48,7 @@ export const RUNNABLE_REASONS: ReadonlySet<string> = new Set([
|
|
|
48
48
|
* `-r` (`--resume`), `--session <path>` and `--fork <path>` all open an
|
|
49
49
|
* existing session *before* the first runtime exists, and that runtime is
|
|
50
50
|
* created without an explicit session_start event, so extensions see pi's
|
|
51
|
-
* default reason — `startup` — not `resume` (verified on pi 1.
|
|
51
|
+
* default reason — `startup` — not `resume` (verified on pi 1.1.0–1.4.0:
|
|
52
52
|
* `main.js` builds the initial session manager from those flags and calls
|
|
53
53
|
* `createAgentSessionRuntime` with no `sessionStartEvent`;
|
|
54
54
|
* `agent-session.js` defaults it to `{ reason: "startup" }`). `resume` is only
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* that flag is what routes "/cmd args" through _tryExecuteExtensionCommand
|
|
28
28
|
* instead of the model prompt path. The wrapper defaults it to false in this
|
|
29
29
|
* package (each byte-identical across every published version of the supported
|
|
30
|
-
* window, 1.
|
|
30
|
+
* window, 1.1.0 → 1.4.0; see AGENTS.md host-compat discipline for the version
|
|
31
31
|
* matrix).
|
|
32
32
|
*
|
|
33
33
|
* Only extension commands can run this way. Built-in commands (/compact,
|
|
@@ -418,27 +418,25 @@ describe("session_before_compact handler", () => {
|
|
|
418
418
|
.calls[0] as [
|
|
419
419
|
AvailableModel,
|
|
420
420
|
unknown,
|
|
421
|
-
{ maxTokens
|
|
421
|
+
{ maxTokens?: number; signal: AbortSignal },
|
|
422
422
|
];
|
|
423
423
|
expect(modelKey(model)).toBe("google/gemini-2.5-flash");
|
|
424
|
-
// 0
|
|
425
|
-
|
|
424
|
+
// pi 1.4.0 (#9512): no fixed output cap — the request omits maxTokens
|
|
425
|
+
// so the provider uses the model's own output limit.
|
|
426
|
+
expect(options).not.toHaveProperty("maxTokens");
|
|
426
427
|
expect(options.signal).toBe(event.signal);
|
|
427
428
|
});
|
|
428
429
|
|
|
429
|
-
it("
|
|
430
|
+
it("does not derive an output budget from reserveTokens", async () => {
|
|
430
431
|
getCompactStore().setModel("google/gemini-2.5-flash");
|
|
431
432
|
const { handler } = registeredHandler();
|
|
432
433
|
const bigCapModel = { ...MODELS[0], maxTokens: 65536 };
|
|
433
434
|
const { ctx, complete } = fakeCtx({ models: [bigCapModel] });
|
|
434
435
|
await handler(fakeEvent() as never, ctx);
|
|
435
436
|
const [, , options] = (complete as ReturnType<typeof vi.fn>).mock
|
|
436
|
-
.calls[0] as [
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
{ maxTokens: number },
|
|
440
|
-
];
|
|
441
|
-
expect(options.maxTokens).toBe(12800);
|
|
437
|
+
.calls[0] as [AvailableModel, unknown, { maxTokens?: number }];
|
|
438
|
+
// reserveTokens (16000) used to cap this at 12800; the cap is gone.
|
|
439
|
+
expect(options).not.toHaveProperty("maxTokens");
|
|
442
440
|
});
|
|
443
441
|
|
|
444
442
|
it("falls back with a warning on an empty summary", async () => {
|
|
@@ -568,6 +566,31 @@ describe("session_before_compact handler", () => {
|
|
|
568
566
|
expect(prompts[0]).toContain("Additional focus: 保留命令输出细节");
|
|
569
567
|
});
|
|
570
568
|
|
|
569
|
+
it("passes customInstructions to the split-turn prefix summary too", async () => {
|
|
570
|
+
getCompactStore().setModel("google/gemini-2.5-flash");
|
|
571
|
+
const { handler } = registeredHandler();
|
|
572
|
+
const prompts: string[] = [];
|
|
573
|
+
const { ctx } = fakeCtx({
|
|
574
|
+
complete: vi.fn(async (_model, context) => {
|
|
575
|
+
prompts.push(
|
|
576
|
+
(context.messages[0]?.content as Array<{ text?: string }>)[0]?.text ??
|
|
577
|
+
"",
|
|
578
|
+
);
|
|
579
|
+
return fakeResponse("## Goal\n- x");
|
|
580
|
+
}),
|
|
581
|
+
});
|
|
582
|
+
const event = fakeEvent({
|
|
583
|
+
messagesToSummarize: [],
|
|
584
|
+
turnPrefixMessages: [userMessage("prefix msg")],
|
|
585
|
+
isSplitTurn: true,
|
|
586
|
+
});
|
|
587
|
+
Object.assign(event, { customInstructions: "保留命令输出细节" });
|
|
588
|
+
await handler(event as never, ctx);
|
|
589
|
+
expect(prompts).toHaveLength(1);
|
|
590
|
+
expect(prompts[0]).toContain("# Instructions");
|
|
591
|
+
expect(prompts[0]).toContain("Additional focus: 保留命令输出细节");
|
|
592
|
+
});
|
|
593
|
+
|
|
571
594
|
it("switches to the update prompt when a previous summary exists", async () => {
|
|
572
595
|
getCompactStore().setModel("google/gemini-2.5-flash");
|
|
573
596
|
const { handler } = registeredHandler();
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
* - takeover point: the `session_before_compact` extension event, which pi
|
|
10
10
|
* emits for ALL three trigger reasons (manual / threshold / overflow)
|
|
11
11
|
* before running its own compaction
|
|
12
|
-
* - prompts, message assembly, split-turn handling,
|
|
13
|
-
* summary file-list tail are verbatim pi built-in behavior (copied
|
|
14
|
-
* 0.87.1
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* - prompts, message assembly, split-turn handling, summary budgets, and
|
|
13
|
+
* the summary file-list tail are verbatim pi built-in behavior (copied
|
|
14
|
+
* from 0.87.1; `core/compaction/compaction.js` stayed byte-identical
|
|
15
|
+
* 1.0.0 → 1.3.0, and 1.4.0 changed only the summary output budget and
|
|
16
|
+
* the split-turn focus handling mirrored below) — only the executing
|
|
17
|
+
* model differs. The one exception is `serializeConversation`, which is
|
|
18
|
+
* imported from the host instead of mirrored, so it tracks whatever the
|
|
19
|
+
* running host does (1.2.0 stopped feeding assistant thinking into the
|
|
20
|
+
* request).
|
|
19
21
|
* - lossless fallback: unset config, unavailable model, empty/failed
|
|
20
22
|
* summary, or user abort → return undefined and pi's default compaction
|
|
21
23
|
* (session model) runs untouched. The extension can never make
|
|
@@ -75,7 +77,6 @@ async function completeSummary(
|
|
|
75
77
|
ctx: ExtensionContext,
|
|
76
78
|
model: AvailableModel,
|
|
77
79
|
promptText: string,
|
|
78
|
-
maxTokens: number,
|
|
79
80
|
signal: AbortSignal,
|
|
80
81
|
): Promise<{ text: string; usage: CompleteUsage }> {
|
|
81
82
|
const response = await ctx.modelRegistry.complete(
|
|
@@ -91,7 +92,10 @@ async function completeSummary(
|
|
|
91
92
|
],
|
|
92
93
|
},
|
|
93
94
|
{
|
|
94
|
-
maxTokens
|
|
95
|
+
// No `maxTokens`: since pi 1.4.0 (#9512) the built-in compaction asks
|
|
96
|
+
// for the model's own output limit instead of a fixed fraction of
|
|
97
|
+
// reserveTokens, so a long reasoning block can no longer eat the cap
|
|
98
|
+
// and fail the summary with "generation hit the token cap".
|
|
95
99
|
signal,
|
|
96
100
|
cacheRetention: "none",
|
|
97
101
|
sessionId: uuidv7(),
|
|
@@ -118,30 +122,26 @@ async function completeSummary(
|
|
|
118
122
|
return { text, usage: response.usage };
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
/**
|
|
122
|
-
* pi's output-budget formula (compaction.js, 0.87.1 → 1.3.0 unchanged): a
|
|
123
|
-
* fraction of reserveTokens clamped by the model's own output cap.
|
|
124
|
-
*/
|
|
125
|
-
function summarizationBudget(
|
|
126
|
-
fraction: number,
|
|
127
|
-
reserveTokens: number,
|
|
128
|
-
modelMaxTokens: number,
|
|
129
|
-
): number {
|
|
130
|
-
return Math.min(
|
|
131
|
-
Math.floor(fraction * reserveTokens),
|
|
132
|
-
modelMaxTokens > 0 ? modelMaxTokens : Number.POSITIVE_INFINITY,
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
125
|
/**
|
|
137
126
|
* Run the full compaction summarization with the configured model,
|
|
138
|
-
* replicating pi's compact()
|
|
139
|
-
*
|
|
127
|
+
* replicating pi's compact() as of 1.4.0 (byte-identical to 0.87.1's shape
|
|
128
|
+
* apart from the two 1.4.0 fixes adopted for the whole window, see below):
|
|
140
129
|
* history summary (first-connection or iterative-update prompt, custom
|
|
141
130
|
* focus appended), plus a separate turn-prefix checkpoint when the cut point
|
|
142
131
|
* splits a turn, merged into one summary with the file-list tail. Returns
|
|
143
132
|
* undefined on any failure after notifying (pi default compaction then takes
|
|
144
133
|
* over).
|
|
134
|
+
*
|
|
135
|
+
* 1.4.0 changed two things this mirror covers, and we follow 1.4.0 for every
|
|
136
|
+
* host in the supported window (1.1.0 → 1.4.0) because both are bug fixes
|
|
137
|
+
* rather than behavior we could sensibly keep per version:
|
|
138
|
+
* - summaries no longer cap maxTokens at 0.8/0.5 × reserveTokens; the
|
|
139
|
+
* request omits maxTokens so the provider uses the model's output limit
|
|
140
|
+
* (#9512 — long reasoning made summaries fail with "generation hit the
|
|
141
|
+
* token cap"). `reserveTokens` now only decides when compaction starts.
|
|
142
|
+
* - `/compact <instructions>` also reaches the split-turn prefix summary
|
|
143
|
+
* (#11058; before 1.4.0 the focus was dropped when the split turn was the
|
|
144
|
+
* only new content).
|
|
145
145
|
*/
|
|
146
146
|
async function runCompactModelCompaction(
|
|
147
147
|
ctx: ExtensionContext,
|
|
@@ -157,7 +157,6 @@ async function runCompactModelCompaction(
|
|
|
157
157
|
firstKeptEntryId,
|
|
158
158
|
previousSummary,
|
|
159
159
|
fileOps,
|
|
160
|
-
settings,
|
|
161
160
|
} = preparation;
|
|
162
161
|
|
|
163
162
|
try {
|
|
@@ -177,11 +176,6 @@ async function runCompactModelCompaction(
|
|
|
177
176
|
previousSummary,
|
|
178
177
|
customInstructions,
|
|
179
178
|
),
|
|
180
|
-
summarizationBudget(
|
|
181
|
-
0.8,
|
|
182
|
-
settings.reserveTokens,
|
|
183
|
-
model.maxTokens,
|
|
184
|
-
),
|
|
185
179
|
signal,
|
|
186
180
|
);
|
|
187
181
|
historyText = history.text;
|
|
@@ -192,8 +186,8 @@ async function runCompactModelCompaction(
|
|
|
192
186
|
model,
|
|
193
187
|
buildTurnPrefixPromptText(
|
|
194
188
|
serializeConversation(convertToLlm(turnPrefixMessages)),
|
|
189
|
+
customInstructions,
|
|
195
190
|
),
|
|
196
|
-
summarizationBudget(0.5, settings.reserveTokens, model.maxTokens),
|
|
197
191
|
signal,
|
|
198
192
|
);
|
|
199
193
|
summaryText = `${historyText}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefix.text}`;
|
|
@@ -212,7 +206,6 @@ async function runCompactModelCompaction(
|
|
|
212
206
|
previousSummary,
|
|
213
207
|
customInstructions,
|
|
214
208
|
),
|
|
215
|
-
summarizationBudget(0.8, settings.reserveTokens, model.maxTokens),
|
|
216
209
|
signal,
|
|
217
210
|
);
|
|
218
211
|
summaryText = history.text;
|
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
* summaries produced by the configured compact model are formatted exactly
|
|
7
7
|
* like pi's own compaction output. Only the executing model differs.
|
|
8
8
|
* Verified byte-identical against every published version of the supported
|
|
9
|
-
* window (1.
|
|
10
|
-
*
|
|
11
|
-
* utils.js change (1.2.0 dropping assistant thinking from
|
|
9
|
+
* window (1.1.0 → 1.4.0): core/usage-totals.js is unchanged across that
|
|
10
|
+
* span, the only utils.js change (1.2.0 dropping assistant thinking from
|
|
12
11
|
* serializeConversation) does not touch the strings mirrored here — that
|
|
13
|
-
* serializer is imported from the host, not mirrored
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* serializer is imported from the host, not mirrored — and compaction.js
|
|
13
|
+
* changed once, in 1.4.0, in the output budget and the split-turn focus
|
|
14
|
+
* handling (both adopted here, see buildTurnPrefixPromptText). 0.87.0
|
|
15
|
+
* shipped a different split-turn prefix prompt; the text mirrored here is
|
|
16
|
+
* 0.87.1's fix, and the mirror is the only variant any supported host gets
|
|
17
|
+
* — intentional.
|
|
17
18
|
*
|
|
18
19
|
* Update these strings whenever pi changes its built-in prompts (diff
|
|
19
20
|
* dist/core/compaction/compaction.js between versions).
|
|
@@ -146,11 +147,21 @@ export function buildHistoryPromptText(
|
|
|
146
147
|
|
|
147
148
|
/**
|
|
148
149
|
* Build the split-turn prefix prompt text (pi 0.87.1
|
|
149
|
-
* generateTurnPrefixSummary): `# Conversation` block
|
|
150
|
-
* `# Instructions
|
|
150
|
+
* generateTurnPrefixSummary, extended in 1.4.0): `# Conversation` block
|
|
151
|
+
* followed by `# Instructions`, with the `/compact <instructions>` focus
|
|
152
|
+
* appended as `Additional focus: ...`. 1.4.0 added that last line (#11058:
|
|
153
|
+
* the focus used to be dropped when the split turn was the only new
|
|
154
|
+
* content since the last compaction); we use the fixed form on every host.
|
|
151
155
|
*/
|
|
152
|
-
export function buildTurnPrefixPromptText(
|
|
153
|
-
|
|
156
|
+
export function buildTurnPrefixPromptText(
|
|
157
|
+
conversationText: string,
|
|
158
|
+
customInstructions?: string,
|
|
159
|
+
): string {
|
|
160
|
+
let promptText = `# Conversation\n${conversationText}\n\n# Instructions\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
161
|
+
if (customInstructions) {
|
|
162
|
+
promptText = `${promptText}\n\nAdditional focus: ${customInstructions}`;
|
|
163
|
+
}
|
|
164
|
+
return promptText;
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
/** File-operation sets extracted by pi's prepareCompaction. */
|
|
@@ -203,7 +214,7 @@ export function formatFileOperations(
|
|
|
203
214
|
/**
|
|
204
215
|
* Combine two provider usage records field-wise (pi 0.87.1 compaction.js
|
|
205
216
|
* combineUsage, moved verbatim to core/usage-totals.js in 0.99.0 and
|
|
206
|
-
* unchanged through 1.
|
|
217
|
+
* unchanged through 1.4.0): used when
|
|
207
218
|
* a split turn produces two summarization calls.
|
|
208
219
|
*/
|
|
209
220
|
export function combineUsage<T extends UsageLike>(first: T, second: T): T {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* bindings — only the cycle semantics change (patch-cycle.ts), so no
|
|
9
9
|
* keybinding conflicts arise.
|
|
10
10
|
*
|
|
11
|
-
* Verified against every published version of the supported
|
|
12
|
-
* (
|
|
11
|
+
* Verified against every published version of the supported range
|
|
12
|
+
* (0.99.0 → 1.4.0) keybinding
|
|
13
13
|
* tables + pi-tui input dispatch on WSL2 + Windows Terminal; re-run
|
|
14
14
|
* `pnpm check:keybindings` after every host upgrade before trusting this.
|
|
15
15
|
*/
|
|
@@ -30,11 +30,12 @@ export const FAVORITES_FILE = "pi-toolkits-favorites.json";
|
|
|
30
30
|
* Windows/WSL `tui.altScreen.search` resolves to ctrl+f, and
|
|
31
31
|
* `TuiAltScreen.handleViewportInput()` checks it before
|
|
32
32
|
* `shouldDeferViewportInputToOverlay()`, so the keystroke opens transcript
|
|
33
|
-
* search and the selector never sees it (verified in pi-tui 1.
|
|
33
|
+
* search and the selector never sees it (verified in pi-tui 1.4.0
|
|
34
34
|
* `dist/tui-alt-screen.js`: the search check is the first key branch and
|
|
35
|
-
* returns `{ consume: true }` unconditionally
|
|
35
|
+
* returns `{ consume: true }` unconditionally; the 1.4.0 changes to that
|
|
36
|
+
* file are exit/teardown only).
|
|
36
37
|
*
|
|
37
|
-
* Why ctrl+alt+f was discarded: clean at the pi layer across 1.
|
|
38
|
+
* Why ctrl+alt+f was discarded: clean at the pi layer across 1.1–1.4 (only
|
|
38
39
|
* ctrl+alt+] is taken from that family) and byte-distinguishable without
|
|
39
40
|
* kitty — but grabbed at the OS/app layer on the author's machine by a
|
|
40
41
|
* Windows screenshot tool, so it never reached the terminal. Lesson: a key
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* model available" feedback); otherwise jump to it
|
|
16
16
|
* - the apply logic mirrors pi's _cycleScopedModel/_cycleAvailableModel
|
|
17
17
|
* (byte-identical across every published version in the supported window
|
|
18
|
-
* 1.
|
|
18
|
+
* 1.1.0 → 1.4.0 — `cycleModel` itself is untouched by 1.4.0's recovery and
|
|
19
|
+
* tool-restore rewrites:
|
|
19
20
|
* state swap, session persistence, default-model persistence only when
|
|
20
21
|
* `options.persist` is set, thinking-level re-clamp, model_select emit
|
|
21
22
|
* with source "cycle")
|
|
@@ -31,9 +32,9 @@ import { getFavoritesStore, modelKey } from "./store.js";
|
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* pi's `Model<any>` — the exact type of `ModelCycleResult.model`
|
|
34
|
-
* (pi-coding-agent 1.
|
|
35
|
+
* (pi-coding-agent 1.1–1.4; the field is unchanged across that span).
|
|
35
36
|
* Derived here instead of importing `Model` from @earendil-works/pi-ai,
|
|
36
|
-
* whose index.d.ts (1.
|
|
37
|
+
* whose index.d.ts (1.1 through 1.4.0 alike) re-exports it via an
|
|
37
38
|
* extensioned `"./types.ts"` specifier that not every TS resolver substitutes
|
|
38
39
|
* to `types.d.ts` (tsc 5.9 bundler does; other tsserver builds don't).
|
|
39
40
|
*/
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
* upgraded internals), patches are skipped with a warning — the extension
|
|
22
22
|
* keeps loading and ctrl+p falls back to stock behavior. The prototype
|
|
23
23
|
* shape touched here (handleInput/updateList/filterModels and the instance
|
|
24
|
-
* fields below) is byte-identical across
|
|
25
|
-
* pi 1.
|
|
24
|
+
* fields below) is byte-identical across the mandatory window
|
|
25
|
+
* pi 1.1.0 → 1.4.0 (and exercised by the 0.99.0 → 1.0.4 backward probes).
|
|
26
26
|
*/
|
|
27
27
|
import {
|
|
28
28
|
ModelSelectorComponent,
|
|
@@ -21,7 +21,7 @@ import type { KeyId } from "@earendil-works/pi-tui";
|
|
|
21
21
|
/**
|
|
22
22
|
* Stash hotkey.
|
|
23
23
|
*
|
|
24
|
-
* Compatibility rationale (verified against pi
|
|
24
|
+
* Compatibility rationale (verified against pi 0.99.0–1.4.0 keybindings, live
|
|
25
25
|
* matchesKey probes and terminal/OS behavior):
|
|
26
26
|
* - pi: no default binding for ctrl+alt+<letter> (only ctrl+alt+] is taken);
|
|
27
27
|
* extension shortcuts dispatch first while the editor is focused
|
package/src/lib/gates.ts
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
* slot; a second presentation would mount over the first and strand its
|
|
18
18
|
* promise. The queue serializes our own presentations; the host slot is
|
|
19
19
|
* still shared with other extensions (pi does not arbitrate globally).
|
|
20
|
+
* pi 1.4.0 stacks extension dialogs so a second one no longer strands the
|
|
21
|
+
* first (#11166), but the 1.1–1.3 hosts in the supported window still
|
|
22
|
+
* replace it — the queue stays, and it also keeps one confirmation visible
|
|
23
|
+
* at a time instead of a pile of stacked dialogs on 1.4.0.
|
|
20
24
|
* - A gate that throws blocks the call (fail closed), matching pi's own
|
|
21
25
|
* `tool_call` handler contract.
|
|
22
26
|
*/
|
package/src/lib/model-config.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* (review, compact-model — favorites keeps its own store-local key logic).
|
|
5
5
|
*
|
|
6
6
|
* Types are derived from pi-coding-agent's own exports (ModelRegistry)
|
|
7
|
-
* rather than imported from @earendil-works/pi-ai: pi-ai's index.d.ts (1.
|
|
8
|
-
* through 1.
|
|
7
|
+
* rather than imported from @earendil-works/pi-ai: pi-ai's index.d.ts (1.1.0
|
|
8
|
+
* through 1.4.0 alike) re-exports `Model`/`Api` via extensioned
|
|
9
9
|
* `./types.ts` specifiers, which tsc 5.9 bundler resolution substitutes to
|
|
10
10
|
* `.d.ts` but some tsserver builds do not. Deriving from ModelRegistry keeps
|
|
11
11
|
* every resolver in agreement with the exact type
|
package/src/lib/model-picker.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The component is driven through a small ModelRuntime facade backed by
|
|
11
11
|
* ctx.modelRegistry (pi's extension-facing wrapper). SAFETY: verified
|
|
12
|
-
* against pi 1.
|
|
12
|
+
* against pi 1.1.0–1.4.0 dist (modes/interactive/components/model-selector.js +
|
|
13
13
|
* model-catalog-refresh.js — byte-identical across that span) — the
|
|
14
14
|
* component only calls getAvailableSnapshot, getModel, getError and refresh
|
|
15
15
|
* on modelRuntime, and ModelRegistry provides exactly those
|