@andares/pi-toolkits 0.11.1 → 0.11.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/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/package.json +4 -4
- 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 +19 -0
- package/src/features/compact/index.ts +5 -3
- package/src/features/compact/prompt.ts +22 -11
- package/src/features/favorites/constants.ts +13 -8
- package/src/features/favorites/favorites.test.ts +42 -0
- package/src/features/favorites/patch-cycle.ts +5 -4
- package/src/features/favorites/patch-selector.ts +44 -8
- package/src/features/init/detect.test.ts +5 -2
- package/src/features/stash/stash.ts +1 -1
- package/src/lib/gates.ts +2 -2
- package/src/lib/model-config.ts +2 -2
- package/src/lib/model-picker.ts +6 -5
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
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 `>=0.99.0`. Every published pi release from 0.99.0 through 1.
|
|
18
|
+
**Requirements**: pi `>=0.99.0`. Every published pi release from 0.99.0 through 1.5.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
|
|
|
@@ -287,7 +287,7 @@ pi's `/compact` and auto-compact summarize the context **with the current sessio
|
|
|
287
287
|
| auto-compact threshold reached | summarized by the compaction model |
|
|
288
288
|
| context overflow recovery | summarized by the compaction model, retry behavior unchanged |
|
|
289
289
|
|
|
290
|
-
**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.
|
|
290
|
+
**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.2.0 → 1.5.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, plus the 1.5.0 `cost.totalFromProvider` merge rule in the usage accounting — 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):
|
|
291
291
|
|
|
292
292
|
- **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
|
|
293
293
|
- **Split behavior** — the `keepRecentTokens` retention policy, and `firstKeptEntryId` / `tokensBefore` passed back unchanged
|
|
@@ -368,7 +368,7 @@ An **identical command line** declared by both sources runs once: a duplicate is
|
|
|
368
368
|
|
|
369
369
|
**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.
|
|
370
370
|
|
|
371
|
-
> **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.
|
|
371
|
+
> **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.2.0–1.5.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).
|
|
372
372
|
|
|
373
373
|
---
|
|
374
374
|
|
package/README.zh-CN.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
一句话介绍:目前包含七个开箱即用的功能——**只读咨询模式** `ask`、**会话级边界权限** `ask-perm`、**模型收藏** `favorites`、**提示词暂存** `stash`、**第三方代码评审** `third-review`、**独立压缩模型** `compact-model`、**会话启动自动化** `autostart`,后续持续追加。
|
|
17
17
|
|
|
18
|
-
**版本要求**:pi `>=0.99.0`。0.99.0 到 1.
|
|
18
|
+
**版本要求**:pi `>=0.99.0`。0.99.0 到 1.5.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
|
|
|
@@ -287,7 +287,7 @@ pi 的 `/compact` 与 auto-compact 默认**用当前会话模型**做上下文
|
|
|
287
287
|
| auto-compact 阈值触发 | 压缩模型总结 |
|
|
288
288
|
| context overflow 恢复 | 压缩模型总结,重试行为不变 |
|
|
289
289
|
|
|
290
|
-
**接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,复制自 0.87.1、在 1.
|
|
290
|
+
**接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,复制自 0.87.1、在 1.2.0 → 1.5.0 内字节级不变 —— 除了此处采纳的两项 1.4.0 修正:摘要请求使用模型自身的输出上限而非 `reserveTokens` 的固定比例,以及 `/compact <instructions>` 也会进入 split-turn 前缀,外加 1.5.0 的 `cost.totalFromProvider` 合并规则(用量统计);只换执行模型;对话序列化函数(`serializeConversation`)是直接从宿主导入的,因此跟随宿主行为 —— 例如 1.2.0 起不再把 assistant thinking 回填进摘要请求):
|
|
291
291
|
|
|
292
292
|
- **摘要格式** —— Goal / Progress / Key Decisions 结构化 checkpoint;已有摘要时用迭代更新版 prompt,信息延续不丢
|
|
293
293
|
- **切分行为** —— 保留最近 `keepRecentTokens` 的策略、`firstKeptEntryId` / `tokensBefore` 原样回传,不动
|
|
@@ -368,7 +368,7 @@ run = [
|
|
|
368
368
|
|
|
369
369
|
**关键约束:只支持扩展命令**。`/add-dir`(pi-add-dir)、`/cd`(pi-cd)等 `registerCommand` 注册的命令可以;内置命令(`/compact`、`/reload`、`/resume`…)在 TUI 层分发,扩展层触发不了;skill 命令(`/skill:name`)是展开机制而非执行机制。依赖的目标扩展(如 pi-add-dir)需要先安装。
|
|
370
370
|
|
|
371
|
-
> **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,`sendUserMessage` 与 `_tryExecuteExtensionCommand` 在 1.
|
|
371
|
+
> **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,`sendUserMessage` 与 `_tryExecuteExtensionCommand` 在 1.2.0–1.5.0 全区间字节级一致,见 AGENTS.md 宿主兼容纪律)。预校验用 `pi.getCommands()`(只认 `source: "extension"`)。依赖 `smol-toml`(零依赖、~10KB、TOML 1.0)。
|
|
372
372
|
|
|
373
373
|
---
|
|
374
374
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andares/pi-toolkits",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
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, a dedicated compaction model, and an agent memory framework (/ask-init)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"smol-toml": "^1.9.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@earendil-works/pi-ai": "^1.
|
|
51
|
-
"@earendil-works/pi-coding-agent": "^1.
|
|
52
|
-
"@earendil-works/pi-tui": "^1.
|
|
50
|
+
"@earendil-works/pi-ai": "^1.5.0",
|
|
51
|
+
"@earendil-works/pi-coding-agent": "^1.5.0",
|
|
52
|
+
"@earendil-works/pi-tui": "^1.5.0",
|
|
53
53
|
"@types/node": "^26.6.2",
|
|
54
54
|
"typebox": "^1.3.27",
|
|
55
55
|
"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.5.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.2.0–1.5.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.2.0 → 1.5.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,
|
|
@@ -704,4 +704,23 @@ describe("combineUsage", () => {
|
|
|
704
704
|
const none = combineUsage(fakeUsage(), fakeUsage());
|
|
705
705
|
expect(none.cacheWrite1h).toBeUndefined();
|
|
706
706
|
});
|
|
707
|
+
|
|
708
|
+
it("marks the cost provider-reported only when every part is (pi 1.5.0)", () => {
|
|
709
|
+
const withFlag = (flag?: boolean): UsageLike => {
|
|
710
|
+
const usage = fakeUsage();
|
|
711
|
+
if (flag !== undefined) usage.cost.totalFromProvider = flag;
|
|
712
|
+
return usage;
|
|
713
|
+
};
|
|
714
|
+
expect(
|
|
715
|
+
combineUsage(withFlag(true), withFlag(true)).cost.totalFromProvider,
|
|
716
|
+
).toBe(true);
|
|
717
|
+
expect(
|
|
718
|
+
combineUsage(withFlag(true), withFlag(false)).cost.totalFromProvider,
|
|
719
|
+
).toBe(false);
|
|
720
|
+
// Absent on either side is not "provider-reported"; the field is always
|
|
721
|
+
// written as an explicit boolean, exactly like the built-in.
|
|
722
|
+
expect(
|
|
723
|
+
combineUsage(withFlag(), withFlag(true)).cost.totalFromProvider,
|
|
724
|
+
).toBe(false);
|
|
725
|
+
});
|
|
707
726
|
});
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
* - prompts, message assembly, split-turn handling, summary budgets, and
|
|
13
13
|
* the summary file-list tail are verbatim pi built-in behavior (copied
|
|
14
14
|
* from 0.87.1; `core/compaction/compaction.js` stayed byte-identical
|
|
15
|
-
* 1.0.0 → 1.3.0,
|
|
16
|
-
* the split-turn focus handling mirrored below
|
|
15
|
+
* 1.0.0 → 1.3.0, 1.4.0 changed only the summary output budget and
|
|
16
|
+
* the split-turn focus handling mirrored below, and 1.5.0 left it
|
|
17
|
+
* untouched; the only mirrored helper that moved after that span is
|
|
18
|
+
* `combineUsage`, see prompt.ts) — only the executing
|
|
17
19
|
* model differs. The one exception is `serializeConversation`, which is
|
|
18
20
|
* imported from the host instead of mirrored, so it tracks whatever the
|
|
19
21
|
* running host does (1.2.0 stopped feeding assistant thinking into the
|
|
@@ -133,7 +135,7 @@ async function completeSummary(
|
|
|
133
135
|
* over).
|
|
134
136
|
*
|
|
135
137
|
* 1.4.0 changed two things this mirror covers, and we follow 1.4.0 for every
|
|
136
|
-
* host in the supported window (1.
|
|
138
|
+
* host in the supported window (1.2.0 → 1.5.0) because both are bug fixes
|
|
137
139
|
* rather than behavior we could sensibly keep per version:
|
|
138
140
|
* - summaries no longer cap maxTokens at 0.8/0.5 × reserveTokens; the
|
|
139
141
|
* request omits maxTokens so the provider uses the model's output limit
|
|
@@ -6,14 +6,16 @@
|
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* window (1.2.0 → 1.5.0): the only utils.js change (1.2.0 dropping assistant
|
|
10
|
+
* thinking from serializeConversation) does not touch the strings mirrored
|
|
11
|
+
* here — that serializer is imported from the host, not mirrored —
|
|
12
|
+
* compaction.js last changed in 1.4.0, in the output budget and the
|
|
13
|
+
* split-turn focus handling (both adopted here, see
|
|
14
|
+
* buildTurnPrefixPromptText), and core/usage-totals.js changed once past
|
|
15
|
+
* that point, in 1.5.0, which only added the `cost.totalFromProvider` merge
|
|
16
|
+
* rule mirrored below (the numeric fields are untouched). 0.87.0 shipped a
|
|
17
|
+
* different split-turn prefix prompt; the text mirrored here is 0.87.1's
|
|
18
|
+
* fix, and the mirror is the only variant any supported host gets
|
|
17
19
|
* — intentional.
|
|
18
20
|
*
|
|
19
21
|
* Update these strings whenever pi changes its built-in prompts (diff
|
|
@@ -213,9 +215,12 @@ export function formatFileOperations(
|
|
|
213
215
|
|
|
214
216
|
/**
|
|
215
217
|
* Combine two provider usage records field-wise (pi 0.87.1 compaction.js
|
|
216
|
-
* combineUsage, moved verbatim to core/usage-totals.js in 0.99.0
|
|
217
|
-
* unchanged through 1.
|
|
218
|
-
*
|
|
218
|
+
* combineUsage, moved verbatim to core/usage-totals.js in 0.99.0; the
|
|
219
|
+
* numeric fields are unchanged through 1.5.0, which added the
|
|
220
|
+
* `cost.totalFromProvider` rule at the end of the cost block — a sum counts
|
|
221
|
+
* as provider-reported only when every part does, and the field is written
|
|
222
|
+
* as an explicit boolean either way): used when a split turn produces two
|
|
223
|
+
* summarization calls.
|
|
219
224
|
*/
|
|
220
225
|
export function combineUsage<T extends UsageLike>(first: T, second: T): T {
|
|
221
226
|
return {
|
|
@@ -239,6 +244,10 @@ export function combineUsage<T extends UsageLike>(first: T, second: T): T {
|
|
|
239
244
|
cacheRead: first.cost.cacheRead + second.cost.cacheRead,
|
|
240
245
|
cacheWrite: first.cost.cacheWrite + second.cost.cacheWrite,
|
|
241
246
|
total: first.cost.total + second.cost.total,
|
|
247
|
+
// A sum is provider-reported only when every part is (pi 1.5.0).
|
|
248
|
+
totalFromProvider:
|
|
249
|
+
first.cost.totalFromProvider === true &&
|
|
250
|
+
second.cost.totalFromProvider === true,
|
|
242
251
|
},
|
|
243
252
|
} as T;
|
|
244
253
|
}
|
|
@@ -258,5 +267,7 @@ export interface UsageLike {
|
|
|
258
267
|
cacheRead: number;
|
|
259
268
|
cacheWrite: number;
|
|
260
269
|
total: number;
|
|
270
|
+
/** pi 1.5.0: true only when the provider reported the cost itself. */
|
|
271
|
+
totalFromProvider?: boolean;
|
|
261
272
|
};
|
|
262
273
|
}
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* keybinding conflicts arise.
|
|
10
10
|
*
|
|
11
11
|
* Verified against every published version of the supported range
|
|
12
|
-
* (0.99.0 → 1.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* (0.99.0 → 1.5.0) keybinding tables + pi-tui input dispatch on WSL2 +
|
|
13
|
+
* Windows Terminal; re-run `pnpm check:keybindings` after every host upgrade
|
|
14
|
+
* before trusting this.
|
|
15
15
|
*/
|
|
16
16
|
export const FAVORITES_STATUS_KEY = "favorites";
|
|
17
17
|
|
|
@@ -30,12 +30,17 @@ 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.
|
|
34
|
-
* `dist/tui-alt-screen.js`: the search check is the first key branch
|
|
35
|
-
* returns `{ consume: true }` unconditionally; the 1.
|
|
36
|
-
* file are exit/teardown
|
|
33
|
+
* search and the selector never sees it (verified in pi-tui 1.5.0
|
|
34
|
+
* `dist/tui-alt-screen.js`: the search check is still the first key branch
|
|
35
|
+
* and still returns `{ consume: true }` unconditionally; the 1.5.0 changes to
|
|
36
|
+
* that file are exit/teardown, word-selection joiners, and a rename of
|
|
37
|
+
* `shouldDeferViewportInputToOverlay()` to `shouldDeferViewportInput()` —
|
|
38
|
+
* which now also defers viewport keys to a focused component that declares
|
|
39
|
+
* `capturesViewportInput`, but that guard sits BELOW the search branch, so
|
|
40
|
+
* it does not rescue ctrl+f for the selector even though 1.5.0 gives the
|
|
41
|
+
* selector `capturesViewportInput = true`).
|
|
37
42
|
*
|
|
38
|
-
* Why ctrl+alt+f was discarded: clean at the pi layer across 1.1–1.
|
|
43
|
+
* Why ctrl+alt+f was discarded: clean at the pi layer across 1.1–1.5 (only
|
|
39
44
|
* ctrl+alt+] is taken from that family) and byte-distinguishable without
|
|
40
45
|
* kitty — but grabbed at the OS/app layer on the author's machine by a
|
|
41
46
|
* Windows screenshot tool, so it never reached the terminal. Lesson: a key
|
|
@@ -464,4 +464,46 @@ describe("selector patches", () => {
|
|
|
464
464
|
"claude-opus-4",
|
|
465
465
|
]);
|
|
466
466
|
});
|
|
467
|
+
|
|
468
|
+
it("mirrors pi 1.5.0's disambiguating model name after the id", () => {
|
|
469
|
+
// The name is shown only when it adds words the id does not convey
|
|
470
|
+
// (pi's getDistinctModelName rule, copied — it is not exported).
|
|
471
|
+
type LineItem = {
|
|
472
|
+
provider: string;
|
|
473
|
+
id: string;
|
|
474
|
+
model: { provider: string; id: string; name?: string };
|
|
475
|
+
};
|
|
476
|
+
const redundant: LineItem = {
|
|
477
|
+
provider: "anthropic",
|
|
478
|
+
id: "claude-sonnet-4",
|
|
479
|
+
model: { provider: "anthropic", id: "claude-sonnet-4", name: "Claude Sonnet 4" },
|
|
480
|
+
};
|
|
481
|
+
const renamed: LineItem = {
|
|
482
|
+
provider: "openrouter",
|
|
483
|
+
id: "minimax/minimax-m3",
|
|
484
|
+
model: {
|
|
485
|
+
provider: "openrouter",
|
|
486
|
+
id: "minimax/minimax-m3",
|
|
487
|
+
name: "MiniMax M3 [rasmus@openrouter, parasail]",
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
const lines: string[] = [];
|
|
491
|
+
const selector = fakeSelector({
|
|
492
|
+
activeModels: [],
|
|
493
|
+
filteredModels: [redundant, renamed] as unknown as FakeSelector["filteredModels"],
|
|
494
|
+
listContainer: {
|
|
495
|
+
clear() {},
|
|
496
|
+
addChild: (child: unknown) =>
|
|
497
|
+
lines.push((child as { text?: string }).text ?? ""),
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
selector.updateList();
|
|
501
|
+
const redundantLine = lines.find((l) => l.includes("claude-sonnet-4"));
|
|
502
|
+
const renamedLine = lines.find((l) => l.includes("minimax/minimax-m3"));
|
|
503
|
+
expect(redundantLine).toBeDefined();
|
|
504
|
+
expect(redundantLine).not.toContain("(Claude Sonnet 4)");
|
|
505
|
+
expect(renamedLine).toContain(
|
|
506
|
+
"(MiniMax M3 [rasmus@openrouter, parasail])",
|
|
507
|
+
);
|
|
508
|
+
});
|
|
467
509
|
});
|
|
@@ -15,8 +15,9 @@
|
|
|
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.
|
|
19
|
-
* tool-restore rewrites
|
|
18
|
+
* 1.2.0 → 1.5.0 — `cycleModel` itself is untouched by 1.4.0's recovery and
|
|
19
|
+
* tool-restore rewrites and by 1.5.0's auth-routing/loadout-ordering
|
|
20
|
+
* changes:
|
|
20
21
|
* state swap, session persistence, default-model persistence only when
|
|
21
22
|
* `options.persist` is set, thinking-level re-clamp, model_select emit
|
|
22
23
|
* with source "cycle")
|
|
@@ -32,9 +33,9 @@ import { getFavoritesStore, modelKey } from "./store.js";
|
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* pi's `Model<any>` — the exact type of `ModelCycleResult.model`
|
|
35
|
-
* (pi-coding-agent 1.
|
|
36
|
+
* (pi-coding-agent 1.2–1.5; the field is unchanged across that span).
|
|
36
37
|
* Derived here instead of importing `Model` from @earendil-works/pi-ai,
|
|
37
|
-
* whose index.d.ts (1.
|
|
38
|
+
* whose index.d.ts (1.2 through 1.5.0 alike) re-exports it via an
|
|
38
39
|
* extensioned `"./types.ts"` specifier that not every TS resolver substitutes
|
|
39
40
|
* to `types.d.ts` (tsc 5.9 bundler does; other tsserver builds don't).
|
|
40
41
|
*/
|
|
@@ -21,8 +21,10 @@
|
|
|
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
|
|
25
|
-
* pi 1.
|
|
24
|
+
* fields below) is unchanged across the mandatory window
|
|
25
|
+
* pi 1.2.0 → 1.5.0 (and exercised by the 0.99.0 → 1.1.0 backward probes).
|
|
26
|
+
* updateList gained one layout change in 1.5.0 — the disambiguating model
|
|
27
|
+
* name after the id — mirrored here; see distinctModelName below.
|
|
26
28
|
*/
|
|
27
29
|
import {
|
|
28
30
|
ModelSelectorComponent,
|
|
@@ -61,7 +63,7 @@ interface SelectorPatchTarget {
|
|
|
61
63
|
handleInput(keyData: string): void;
|
|
62
64
|
updateList(): void;
|
|
63
65
|
filterModels(query: string): void;
|
|
64
|
-
/** pi 1.0–1.
|
|
66
|
+
/** pi 1.0–1.5: default-model badge source. */
|
|
65
67
|
isDefaultModel(model: ModelRef): boolean;
|
|
66
68
|
listContainer: ListContainerLike;
|
|
67
69
|
searchInput: SearchInputLike;
|
|
@@ -95,6 +97,37 @@ function sameModel(a: ModelRef, b: ModelRef): boolean {
|
|
|
95
97
|
return a.provider === b.provider && a.id === b.id;
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
/** Lowercase a label and split it into word tokens (pi's splitModelLabel). */
|
|
101
|
+
function splitModelLabel(value: string): string[] {
|
|
102
|
+
return value
|
|
103
|
+
.toLowerCase()
|
|
104
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Replica of pi 1.5.0's internal `getDistinctModelName`
|
|
110
|
+
* (`dist/modes/interactive/model-search.js`). It is not reachable from the
|
|
111
|
+
* package's `.` entry, so the rule is copied instead of imported: show the
|
|
112
|
+
* model name only when it has words the id does not already convey, which
|
|
113
|
+
* keeps renamed overrides visible without repeating `claude-sonnet-4` after
|
|
114
|
+
* `claude-sonnet-4-20250514`. Mirrored for every host in the window — the
|
|
115
|
+
* model `name` field exists across it, and one rendering rule is better than
|
|
116
|
+
* a version branch.
|
|
117
|
+
*/
|
|
118
|
+
function distinctModelName(id: string, name?: string): string | undefined {
|
|
119
|
+
const trimmed = name?.trim();
|
|
120
|
+
if (!trimmed) return undefined;
|
|
121
|
+
const idWords = splitModelLabel(id);
|
|
122
|
+
const idTokens = new Set(idWords);
|
|
123
|
+
const compactId = idWords.join("");
|
|
124
|
+
// Single-character words must match a whole id token: "4" is not covered
|
|
125
|
+
// by "mistral-small-2603".
|
|
126
|
+
const isCovered = (word: string): boolean =>
|
|
127
|
+
idTokens.has(word) || (word.length > 1 && compactId.includes(word));
|
|
128
|
+
return splitModelLabel(trimmed).every(isCovered) ? undefined : trimmed;
|
|
129
|
+
}
|
|
130
|
+
|
|
98
131
|
function buildHintText(state: SelectorPatchTarget): string {
|
|
99
132
|
const theme = themeProvider();
|
|
100
133
|
if (!theme) return "";
|
|
@@ -155,10 +188,11 @@ export function applyModelSelectorPatches(): boolean {
|
|
|
155
188
|
};
|
|
156
189
|
|
|
157
190
|
// ── 2. List rendering: hint row + bold bright yellow favorites ────────
|
|
158
|
-
// Replicates the built-in updateList layout (
|
|
159
|
-
//
|
|
160
|
-
// indicator, empty/error states, refresh
|
|
161
|
-
// one change: favorited model ids render
|
|
191
|
+
// Replicates the built-in updateList layout (fixed-width cursor and ✓
|
|
192
|
+
// current-marker columns, the 1.5.0 disambiguating model name, provider
|
|
193
|
+
// badge, `· default` badge, scroll indicator, empty/error states, refresh
|
|
194
|
+
// status, Model Name footer) with one change: favorited model ids render
|
|
195
|
+
// in the favorite style.
|
|
162
196
|
proto.updateList = function (this: SelectorPatchTarget): void {
|
|
163
197
|
const theme = themeProvider();
|
|
164
198
|
if (!theme) {
|
|
@@ -197,8 +231,10 @@ export function applyModelSelectorPatches(): boolean {
|
|
|
197
231
|
const currentMarker = isCurrent ? theme.fg("accent", "✓ ") : " ";
|
|
198
232
|
const baseText = isSelected ? theme.fg("accent", item.id) : item.id;
|
|
199
233
|
const modelText = isFavorite ? styleFavorite(item.id) : baseText;
|
|
234
|
+
const name = distinctModelName(item.id, item.model.name);
|
|
235
|
+
const nameText = name ? theme.fg("muted", ` (${name})`) : "";
|
|
200
236
|
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
|
201
|
-
const line = `${cursor}${currentMarker}${modelText} ${providerBadge}${defaultBadge}`;
|
|
237
|
+
const line = `${cursor}${currentMarker}${modelText}${nameText} ${providerBadge}${defaultBadge}`;
|
|
202
238
|
list.addChild(new Text(line, 0, 0));
|
|
203
239
|
}
|
|
204
240
|
if (startIndex > 0 || endIndex < this.filteredModels.length) {
|
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
scanRefs,
|
|
12
12
|
} from "./detect.js";
|
|
13
13
|
import { project, remove } from "./test-helpers.js";
|
|
14
|
+
// Static import (not `await import` inside the test): the per-test timeout
|
|
15
|
+
// must not have to cover transforming/loading the deterministic half of the
|
|
16
|
+
// feature — that is what made this test flake under matrix load.
|
|
17
|
+
import { stateToToml } from "./apply.js";
|
|
14
18
|
|
|
15
19
|
describe("isBareRef", () => {
|
|
16
20
|
const text = "see memory/DESIGN.md, .agents/memory/X.md, my-memory/y, ~/.pi/agent/memory/z";
|
|
@@ -207,8 +211,7 @@ describe("review regressions", () => {
|
|
|
207
211
|
expect(countBareRefs(text, "implementations/")).toBe(1);
|
|
208
212
|
});
|
|
209
213
|
|
|
210
|
-
it("round-trips custom budgets through the state TOML keys",
|
|
211
|
-
const { stateToToml } = await import("./apply.js");
|
|
214
|
+
it("round-trips custom budgets through the state TOML keys", () => {
|
|
212
215
|
const parsed = parseState(
|
|
213
216
|
stateToToml({
|
|
214
217
|
version: 1,
|
|
@@ -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 0.99.0–1.
|
|
24
|
+
* Compatibility rationale (verified against pi 0.99.0–1.5.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
|
@@ -18,9 +18,9 @@
|
|
|
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
20
|
* pi 1.4.0 stacks extension dialogs so a second one no longer strands the
|
|
21
|
-
* first (#11166), but the 1.
|
|
21
|
+
* first (#11166), but the 1.2–1.3 hosts in the supported window still
|
|
22
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
|
|
23
|
+
* at a time instead of a pile of stacked dialogs on later hosts.
|
|
24
24
|
* - A gate that throws blocks the call (fail closed), matching pi's own
|
|
25
25
|
* `tool_call` handler contract.
|
|
26
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.2.0
|
|
8
|
+
* through 1.5.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,11 +9,12 @@
|
|
|
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.
|
|
13
|
-
* model-catalog-refresh.js —
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* (
|
|
12
|
+
* against pi 1.2.0–1.5.0 dist (modes/interactive/components/model-selector.js +
|
|
13
|
+
* model-catalog-refresh.js) — the component only calls getAvailableSnapshot,
|
|
14
|
+
* getModel and getError on modelRuntime (plus refresh, which ModelRegistry
|
|
15
|
+
* provides) across that whole span. 1.5.0 changed the selector's rendering
|
|
16
|
+
* and key handling (page keys, disambiguating model names, viewport-input
|
|
17
|
+
* capture) but not the runtime surface this facade implements.
|
|
17
18
|
*
|
|
18
19
|
* `current` is passed as the component's currentModel: the built-in selector
|
|
19
20
|
* preselects, ✓-marks and sorts-first its currentModel, so one Enter accepts
|