@andares/pi-toolkits 0.6.0 → 0.7.1

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 CHANGED
@@ -1,8 +1,9 @@
1
1
  <!-- markdownlint-disable MD033 -->
2
- <!-- Inline <a name> anchors are the only reliable TOC targets for CJK headers on GitHub. -->
2
+ <!-- Inline <a name> anchors keep the TOC targets stable and identical across README.md and README.zh-CN.md. -->
3
3
  # ⚙️ @andares/pi-toolkits
4
4
 
5
- > pi 编码代理的集成扩展工具包 — 一个包,持续生长新能力。
5
+ **English** · [中文](README.zh-CN.md)
6
+
6
7
  > Integrated toolkit extension for [pi](https://github.com/earendil-works/pi-coding-agent) — one package, growing new capabilities over time.
7
8
 
8
9
  [![npm version](https://img.shields.io/npm/v/@andares/pi-toolkits?label=npm&logo=npm)](https://www.npmjs.com/package/@andares/pi-toolkits)
@@ -11,101 +12,100 @@
11
12
  [![pi >= 0.86](https://img.shields.io/badge/pi-%3E%3D0.86.0%20%3C2.0.0-blueviolet)](https://github.com/earendil-works/pi)
12
13
  [![GitHub](https://img.shields.io/badge/github-andares%2Fpi--toolkits-181717?logo=github)](https://github.com/andares/pi-toolkits)
13
14
 
14
- 一句话介绍:目前包含五个开箱即用的功能——**只读咨询模式** `ask`、**模型收藏** `favorites`、**提示词暂存** `stash`、**第三方代码评审** `third-review`、**独立压缩模型** `compact-model`,后续持续追加。
15
- Currently ships five ready-to-use capabilities — **ask mode**, **model favorites**, **prompt stash**, **third-party code review** and a **dedicated compaction model** — with more to come.
15
+ In a nutshell: six ready-to-use capabilities — **read-only consult mode** `ask`, **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.
16
16
 
17
- **版本要求 · Requirements**:pi `>=0.86.0 <2.0.0`(覆盖最近 4 个中版本线 0.86.x / 0.87.x / 0.99.x / 1.0.x —— 0.88–0.98 从未发布。区间内全部 10 个已发布版本逐版本比对宿主 dist:用到的扩展 API 无差异,favorites 两处运行时 patch 目标 `ModelSelectorComponent` / `AgentSession.cycleModel` 字节级一致;并在 0.86.0、0.86.1、0.87.1、0.99.2、1.0.2 上逐一通过 typecheck + 全量测试,在真实 pi 1.0.2 上冒烟加载无警告)。注:pi ≥ 0.99 的宿主要求 `@earendil-works/pi-tui` 声明为 peerDependencies `"*"`(由宿主提供),本包已按要求声明。
17
+ **Requirements**: pi `>=0.86.0 <2.0.0` (covering the four most recently released minor lines — 0.86.x / 0.87.x / 0.99.x / 1.0.x; 0.88–0.98 were never released). Every one of the 10 published versions inside that range was compared against the host `dist`: no differences in the extension APIs we use, and the two runtime patch targets used by favorites — `ModelSelectorComponent` / `AgentSession.cycleModel` — are byte-identical. `typecheck` plus the full test suite pass on each of 0.86.0, 0.86.1, 0.87.1, 0.99.2 and 1.0.2, and smoke-loading on real pi 1.0.2 produces no warnings. Note: pi ≥ 0.99 hosts require `@earendil-works/pi-tui` to be declared in `peerDependencies` as `"*"` (provided by the host); this package declares it as required.
18
18
 
19
19
  ---
20
20
 
21
- ## 📦 安装 Installation
21
+ ## 📦 Installation
22
22
 
23
23
  <a name="install"></a>
24
24
 
25
25
  ```bash
26
- # 从 npm 安装(推荐)· from npm
26
+ # from npm (recommended)
27
27
  pi install npm:@andares/pi-toolkits
28
28
 
29
- # 本地开发安装 · local development
29
+ # local development install
30
30
  pi install .
31
31
 
32
- # 单次会话加载,不安装 · one-off session, no install
32
+ # one-off session load, no install
33
33
  pi -e ./src/index.ts
34
34
  ```
35
35
 
36
- 安装后在任意 pi 会话中验证:
36
+ After installing, verify in any pi session:
37
37
 
38
- - `/ask` → footer 出现灰色 `ask` 状态,`write`/`edit` 被硬禁用
39
- - `/model`(或 `ctrl+l`)→ 选择器顶部出现收藏提示行
40
- - 输入框内按 `ctrl+alt+y` → 当前提示词被暂存并清空输入框
41
- - 完成一个开发任务后输入 `/third-review` → 切换到评审模型执行查+修
42
- - `/compact-model` 选一个便宜快模型 → 之后 `/compact` 与自动压缩的总结都由它执行(footer 出现灰色 `compact` 状态)
38
+ - `/ask` → a grey `ask` status appears in the footer, `write`/`edit` are hard-disabled
39
+ - `/model` (or `ctrl+l`) → a favorites hint line appears at the top of the selector
40
+ - press `ctrl+alt+y` in the input box → the current prompt is stashed and the input box is cleared
41
+ - after finishing a development task, type `/third-review` → switches to the review model and runs check + fix
42
+ - `/compact-model`, pick a cheap fast model → from then on both `/compact` and auto-compaction summarize with it (a grey `compact` status appears in the footer)
43
43
 
44
44
  ---
45
45
 
46
- ## 📚 目录 Contents
46
+ ## 📚 Contents
47
47
 
48
- - [安装 Installation](#install)
49
- - [功能总览 Features at a glance](#features)
50
- - [🛡️ ask — 只读咨询模式](#ask)
51
- - [⭐ favorites — 模型收藏](#favorites)
52
- - [📥 stash — 提示词暂存](#stash)
53
- - [🔎 review — 第三方代码评审](#review)
54
- - [🗜️ compact — 独立压缩模型](#compact)
55
- - [🚀 autostart — 会话启动自动化](#autostart)
56
- - [开发 Development](#development)
57
- - [贡献 Contributing](#contributing)
58
- - [许可 License](#license)
48
+ - [Installation](#install)
49
+ - [Features at a glance](#features)
50
+ - [🛡️ ask — read-only consult mode](#ask)
51
+ - [⭐ favorites — model favorites](#favorites)
52
+ - [📥 stash — prompt stash](#stash)
53
+ - [🔎 review — third-party code review](#review)
54
+ - [🗜️ compact — dedicated compaction model](#compact)
55
+ - [🚀 autostart — session startup automation](#autostart)
56
+ - [Development](#development)
57
+ - [Contributing](#contributing)
58
+ - [License](#license)
59
59
 
60
60
  ---
61
61
 
62
- ## ✨ 功能总览 Features at a glance
62
+ ## ✨ Features at a glance
63
63
 
64
64
  <a name="features"></a>
65
65
 
66
- | 功能 | 入口 | 一句话说明 |
66
+ | Feature | Entry point | One-line description |
67
67
  | --- | --- | --- |
68
- | 🛡️ **ask** 只读咨询模式 | `/ask` | 进入只读问答模式,`write`/`edit` 硬禁用,`bash` 只读沙箱 |
69
- | ⭐ **favorites** 模型收藏 | `/model` 内 `ctrl+F` / `ctrl+J` | 收藏常用模型(加粗亮黄标记),`ctrl+p` 仅在收藏间循环 |
70
- | 📥 **stash** 提示词暂存 | `ctrl+alt+y` | 一段提示词的暂存槽,按键交换 / 连按两次暂存并清空输入框 |
71
- | 🔎 **review** 第三方代码评审 | `/third-review` | 召唤配置的评审模型,对刚完成的任务做查+修,重点检查「本轮需求实现 / 是否有遗漏 / 是否有错误」 |
72
- | 🗜️ **compact** 独立压缩模型 | `/compact-model` | 给 `/compact` 与 auto-compact 配一个便宜快模型做总结,主模型专注对话;未配置零开销、任何异常回落 pi 默认 |
73
- | 🚀 **autostart** 会话启动自动化 | `.pi/autostart.toml` | 项目内声明启动时自动执行的斜杠命令(首批用例:`/add-dir` 多仓库挂载);未信任不执行、未注册命令跳过不发给模型 |
68
+ | 🛡️ **ask** read-only consult mode | `/ask` | enter read-only Q&A mode: `write`/`edit` hard-disabled, `bash` runs in a read-only sandbox |
69
+ | ⭐ **favorites** model favorites | `ctrl+A` / `ctrl+J` inside `/model` | favorite the models you use most (marked in bold bright yellow), `ctrl+p` cycles only among favorites |
70
+ | 📥 **stash** prompt stash | `ctrl+alt+y` | a stash slot for one prompt: press to swap, press twice to stash and clear the input box |
71
+ | 🔎 **review** third-party code review | `/third-review` | summon the configured review model to check + fix the task just finished, focusing on "is this round's requirement implemented / is anything missing / is anything wrong" |
72
+ | 🗜️ **compact** dedicated compaction model | `/compact-model` | give `/compact` and auto-compact a cheap fast model to summarize with, leaving the main model free to focus on the conversation; zero overhead when unconfigured, falls back to pi's default on any failure |
73
+ | 🚀 **autostart** session startup automation | `.pi/autostart.toml` | declare slash commands to run automatically at session start, per project (first use case: mounting multi-repo directories with `/add-dir`); never runs in untrusted directories, unregistered commands are skipped instead of being sent to the model |
74
74
 
75
75
  ---
76
76
 
77
- ## 🛡️ ask — 只读咨询模式
77
+ ## 🛡️ ask — read-only consult mode
78
78
 
79
79
  <a name="ask"></a>
80
80
 
81
- 用 `/ask` 进入(再按一次退出)。适合只想要"问问题、读代码",绝不希望模型改动任何文件的场景。
81
+ Enter with `/ask` (press it again to exit). Meant for the case where you only want to "ask questions and read code", and absolutely never want the model to modify any file.
82
82
 
83
- **进入后**:
83
+ **While active**:
84
84
 
85
- - **`write` / `edit` 硬禁用** —— 从活跃工具集中移除,pi 运行时对未激活工具的调用直接拒绝("Tool not found"),文件修改在机制上不可能发生
86
- - **`bash` 保留但只读沙箱** —— 基于 shell 分词 + 写盘意图检测(`curl -o/-O`、`wget -O`、`dd of=`、`tee`、重定向等一律拦截)。启发式,非安全边界
87
- - **系统提示词追加** —— 追加一段模式说明,模型会遵守只读约束行事
88
- - **灰色 `ask` footer 状态** —— 常驻提示当前处于只读模式
85
+ - **`write` / `edit` hard-disabled** — removed from the active tool set; pi rejects calls to inactive tools outright ("Tool not found"), so file modification becomes mechanically impossible
86
+ - **`bash` kept, but sandboxed to read-only** — based on shell tokenization plus write-intent detection (blocks `curl -o/-O`, `wget -O`, `dd of=`, `tee`, redirections, and the like). Heuristic, not a security boundary
87
+ - **System prompt appendix** — a mode description is appended so the model behaves along the read-only constraint
88
+ - **Grey `ask` footer status** — a permanent reminder that read-only mode is on
89
89
 
90
- **退出**:再按 `/ask`,完整恢复进入前的工具集快照。
90
+ **Exit**: press `/ask` again; the tool-set snapshot taken on entry is fully restored.
91
91
 
92
92
  ---
93
93
 
94
- ## ⭐ favorites — 模型收藏
94
+ ## ⭐ favorites — model favorites
95
95
 
96
96
  <a name="favorites"></a>
97
97
 
98
- 标记常用模型,让模型切换「粘」在收藏上。收藏**跨会话、跨项目全局持久化**。
98
+ Mark the models you use often so that model switching "sticks" to them. Favorites are **persisted globally, across sessions and projects**.
99
99
 
100
- **在 `/model` 选择器内**(`/model` 或 `ctrl+l` 打开):
100
+ **Inside the `/model` selector** (open it with `/model` or `ctrl+l`):
101
101
 
102
- - 顶部提示行显示按键与当前过滤状态
103
- - `ctrl+F` —— 对当前选中模型开/关收藏;收藏的模型文字显示为**加粗亮黄**
104
- - `ctrl+J` —— 开/关「仅显示收藏模型」过滤(可与内置搜索叠加使用)
102
+ - The top hint line shows the keys and the current filter state
103
+ - `ctrl+A` — toggle the favorite flag on the currently selected model; favorite models are rendered in **bold bright yellow**
104
+ - `ctrl+J` — toggle the "show only favorite models" filter (composable with the built-in search)
105
105
 
106
- **模型循环**:只要存在 ≥1 个收藏,`ctrl+p` / `ctrl+shift+p` 就**只在收藏模型间循环**。当前模型不是收藏时,`ctrl+p` 跳到第一个收藏、`ctrl+shift+p` 跳到最后一个;收藏仅 1 个时停在原地(`Only one model available`)。`ctrl+l` / `/model` 始终可自由选择任意模型(含非收藏)。在状态文件中设 `"cycleOnlyFavorites": false` 可恢复全量循环。
106
+ **Model cycling**: as long as ≥1 favorite exists, `ctrl+p` / `ctrl+shift+p` cycle **only among favorite models**. When the current model is not a favorite, `ctrl+p` jumps to the first favorite and `ctrl+shift+p` to the last; with exactly one favorite it stays put (`Only one model available`). `ctrl+l` / `/model` always allow picking any model freely, favorites or not. Set `"cycleOnlyFavorites": false` in the state file to restore full cycling.
107
107
 
108
- **状态文件**:`<agent-dir>/pi-toolkits-favorites.json`(默认 `~/.pi/agent/`,与 auto-naming-session 配置同目录):
108
+ **State file**: `<agent-dir>/pi-toolkits-favorites.json` (defaults to `~/.pi/agent/`, the same directory as the auto-naming-session config):
109
109
 
110
110
  ```json
111
111
  {
@@ -114,123 +114,125 @@ pi -e ./src/index.ts
114
114
  }
115
115
  ```
116
116
 
117
- `/favorites` 命令列出当前收藏与循环模式。
117
+ The `/favorites` command lists the current favorites and the cycling mode.
118
118
 
119
- > **实现说明**:pi 的 `app.model.cycleForward`(`ctrl+p`)与 `cycleBackward` 是**保留键位**,扩展快捷键无法覆盖;且扩展快捷键只在编辑器聚焦时生效。因此本功能不重绑任何按键,而是在运行时 patch 两个内置原型(带版本守卫、`/reload` 幂等):`ModelSelectorComponent`(提示行、`ctrl+F`/`ctrl+J`、亮黄渲染、仅收藏过滤——按键仅在选择器内消费,编辑器里 `ctrl+f` 光标右移、`ctrl+j` 换行不受影响)与 `AgentSession.cycleModel`(收藏循环)。若 pi 升级导致 patch 无法应用,会 warn 并优雅降级,不影响其它功能。
119
+ > **Implementation notes**: pi's `app.model.cycleForward` (`ctrl+p`) and `cycleBackward` are **reserved keybindings** that extension shortcuts cannot override; extension shortcuts also only fire while the editor is focused. This feature therefore rebinds nothing; instead it patches two built-in prototypes at runtime (version-guarded, idempotent under `/reload`): `ModelSelectorComponent` (hint line, `ctrl+A`/`ctrl+J`, bright-yellow rendering, favorites-only filter — the keys are consumed only inside the selector, so in the editor `ctrl+j` still inserts a newline) and `AgentSession.cycleModel` (favorites-only cycling). If a pi upgrade makes the patch inapplicable, it warns and degrades gracefully without affecting the other features.
120
+ >
121
+ > **Why `ctrl+A` and not `ctrl+F`**: since pi 1.0 the default `tuiMode` is `fullscreen` (0.86–0.99 defaulted to `regular`), and on Windows/WSL the host resolves `tui.altScreen.search` to `ctrl+f`; `TuiAltScreen.handleViewportInput()` checks that binding **before** handing input to the focused overlay, so `ctrl+f` opened the transcript search instead of reaching the selector. An interim pick, `ctrl+alt+f`, was clean at the pi layer but turned out to be grabbed by a Windows screenshot tool — a key free in the host table is not necessarily free on the machine. The final pick takes a letter from "favorite" and audits each: `v` is Windows Terminal's paste, `o`/`r`/`t` are host app actions, `i` is byte-identical to Tab (the selector's scope toggle), `e` works but carries no mnemonic; `a` only shadows the search box's jump-to-line-start, reads as *add favorite*, and nothing upstream of the selector claims it. Full audit: `src/features/favorites/constants.ts` and `pnpm check:keybindings`.
120
122
 
121
123
  ---
122
124
 
123
- ## 📥 stash — 提示词暂存
125
+ ## 📥 stash — prompt stash
124
126
 
125
127
  <a name="stash"></a>
126
128
 
127
- 一个热键 + 一个暂存槽,在「当前提示词」与「暂存提示词」之间腾挪,方便把一段提示词先放一边、空出输入框写新的,需要时再取回。
129
+ One hotkey plus one stash slot, shuffling between "the prompt I am writing" and "the prompt I stashed": park a prompt to one side, free the input box for a new one, and take the old one back when you need it.
128
130
 
129
- | 操作 | 效果 |
131
+ | Action | Effect |
130
132
  | --- | --- |
131
- | 按一次 `ctrl+alt+y` | 当前提示词 → 入缓存;缓存内容 → 填入输入框(**交换**) |
132
- | 间隔再按 | 在两段提示词之间**来回切换** |
133
- | **400ms 内连按两次** | 当前提示词入缓存 + **清空输入框**(旧暂存被丢弃),直接开写新提示词;之后按一次取回 |
133
+ | Press `ctrl+alt+y` once | current prompt → stash; stashed content → input box (**swap**) |
134
+ | Press again after a while | **toggle back and forth** between the two prompts |
135
+ | Press twice **within 400ms** | current prompt goes to the stash + **input box is cleared** (the old stash is discarded), so you can start writing the new prompt right away; later press once to take it back |
134
136
 
135
- 缓存为空时,单按一次即「暂存 + 清空」。缓存为**进程内内存、不落盘**(提示词可能含敏感内容);`/new` 切会话不清缓存,`/reload` 清空。选择器(`/model`、`/tree` 等)打开时按键不生效(焦点在组件,不在编辑器)。
137
+ When the stash is empty, a single press means "stash + clear". The stash is **in-process memory and never written to disk** (prompts may contain sensitive content); `/new` switches sessions without clearing the stash, `/reload` clears it. While a selector (`/model`, `/tree`, …) is open the key does nothing (focus is on the component, not the editor).
136
138
 
137
- **为什么是 `ctrl+alt+y`**(四级兼容性调研结论):
139
+ **Why `ctrl+alt+y`** (the conclusion of a four-layer compatibility survey):
138
140
 
139
- | 层面 | 状态 | 说明 |
141
+ | Layer | Status | Notes |
140
142
  | --- | --- | --- |
141
- | pi 键位 | ✅ | 无默认 `ctrl+alt+字母` 绑定;扩展快捷键在编辑器内最先分发 |
142
- | Linux 终端 | ✅ | `alt` 以 ESC 前缀编码(`ctrl+alt+y` = `ESC + ctrl+y`),不依赖 kitty 协议即可区分 |
143
- | Windows Terminal | ✅ | 无默认 `ctrl+alt` 绑定 |
144
- | Windows 操作系统 | ✅ | 系统级仅保留 `ctrl+alt+del` |
143
+ | pi keybindings | ✅ | no default `ctrl+alt+<letter>` binding; extension shortcuts are dispatched first inside the editor |
144
+ | Linux terminals | ✅ | `alt` is encoded as an ESC prefix (`ctrl+alt+y` = `ESC + ctrl+y`), distinguishable without the kitty protocol |
145
+ | Windows Terminal | ✅ | no default `ctrl+alt` binding |
146
+ | Windows OS | ✅ | only `ctrl+alt+del` is reserved system-wide |
145
147
 
146
- **被排除的候选**:`ctrl+shift+字母` —— Linux 与 Windows 的终端 emulator 都占用复制/粘贴/标签页;且无 kitty 协议的终端上会坍缩成 `ctrl+字母`(`ctrl+shift+m` 即回车,会直接提交提示词,危险)。纯 `ctrl+字母` —— pi 几乎全部占用,唯一空闲的 `ctrl+q` 是 XON 流控字符,有历史包袱。
148
+ **Rejected candidates**: `ctrl+shift+<letter>` — terminal emulators on both Linux and Windows grab copy/paste/tabs; and on terminals without the kitty protocol it collapses into `ctrl+<letter>` (`ctrl+shift+m` is Enter, which would submit the prompt immediately — dangerous). Plain `ctrl+<letter>` — pi occupies almost all of them, and the only free one, `ctrl+q`, is the XON flow-control character, with historical baggage.
147
149
 
148
150
  ---
149
151
 
150
- ## 🔎 review — 第三方代码评审
152
+ ## 🔎 review — third-party code review
151
153
 
152
154
  <a name="review"></a>
153
155
 
154
- 开发任务完成后,输入 `/third-review` 指令召唤**另一个模型**以第三方身份,对刚完成的代码做「查 + 修」:评审模型拥有**完整工具权限**,确认的问题直接改,不是只读审计。评审回合留在当前会话记录里,评审完成后**会话保持评审模型**(不自动切回)。
156
+ After a development task is done, type the `/third-review` command to summon **a different model** as a third party to "check + fix" the code just written: the review model has **full tool permissions**, and confirmed problems are edited in place — this is not a read-only audit. The review turn stays in the current session transcript, and after the review **the session keeps the review model** (it does not switch back automatically).
155
157
 
156
- > **仅指令触发**:只有 `/third-review` 能发起评审。聊天中直接输入 `third-review` 不会触发(作为普通消息发给模型),避免误发。
158
+ > **Command-triggered only**: only `/third-review` starts a review. Typing `third-review` as chat text does not trigger it (that is sent to the model as an ordinary message), which avoids accidental launches.
157
159
 
158
- **评审提示词重点检查三点**(逐项给出结论):
160
+ **The review prompt focuses on three checks** (each gets an explicit verdict):
159
161
 
160
- 1. **本轮需求实现** —— 需求是否已完整、正确地实现,与需求描述是否一致
161
- 2. **是否有遗漏** —— 功能点、边界情况、异常处理、兼容性、测试等
162
- 3. **是否有错误** —— 逻辑错误、潜在 bug、类型/编译错误、安全与性能隐患等
162
+ 1. **Is this round's requirement implemented** — is the requirement fully and correctly implemented, consistent with the requirement description
163
+ 2. **Is anything missing** — feature points, edge cases, error handling, compatibility, tests, etc.
164
+ 3. **Is anything wrong** — logic errors, potential bugs, type/compile errors, security and performance hazards, etc.
163
165
 
164
- **范围强约束** —— 评审仅限**当前本轮修改内容**,避免扩大范围;本轮改动范围内确认的问题才直接修复,范围外的历史遗留/无关问题只记录提示、不动手改。
166
+ **Strict scope constraint** — the review is limited to **the changes made in the current round**, to avoid scope creep; only problems confirmed inside this round's changes are fixed directly, while pre-existing or unrelated issues outside that range are only recorded as notes and left untouched.
165
167
 
166
- **评审模型选择**(每次 `/third-review` 都会弹出选择界面):
168
+ **Choosing the review model** (every `/third-review` opens a picker):
167
169
 
168
- - `/third-review` → 弹出 **`/model` 同款选择器**(限高滚动 + 模糊搜索):**默认选中当前配置的模型**(✓ 标记),直接**回车**即用它开始评审
169
- - **换选其他模型** → 立即写回配置(**落盘 + 内存**),**下次默认使用该模型**
170
- - **取消**(Esc)→ 不执行评审,配置不变
171
- - 配置持久化于 `<agent-dir>/pi-toolkits-review.json`(默认 `~/.pi/agent/`):
170
+ - `/third-review` → opens the **same selector as `/model`** (height-limited scrolling + fuzzy search): **the currently configured model is selected by default** (✓ marker), so pressing **Enter** starts the review with it
171
+ - **Pick another model** → written back to the config immediately (**both on disk and in memory**), and **that model becomes the default next time**
172
+ - **Cancel** (Esc) → no review is run, the config is unchanged
173
+ - The config is persisted in `<agent-dir>/pi-toolkits-review.json` (defaults to `~/.pi/agent/`):
172
174
 
173
175
  ```json
174
176
  { "model": "anthropic/claude-sonnet-4" }
175
177
  ```
176
178
 
177
- - `/review-model` —— 单独的配置入口:弹出同一列表(不改会话模型,只更新默认配置)
178
- - **未配置 / 配置的模型已不可用**(如在 pi 的 models.json 里删掉了该模型)→ 提示后照常弹出列表(此时无默认项)
179
- - 任务仍在进行中(agent 未空闲)时触发 → 提示等待,不执行;同一时间只允许一个评审
180
- - footer 出现灰色 `review` 状态 = 已配置评审模型
179
+ - `/review-model` — a separate config entry point: opens the same list (without changing the session model, only updating the default config)
180
+ - **Unconfigured / the configured model is no longer available** (e.g. it was deleted from pi's models.json) → a notice is shown and the list opens as usual (with no default entry in that case)
181
+ - Triggered while a task is still in progress (the agent is not idle) → you are told to wait and nothing runs; only one review may be active at a time
182
+ - A grey `review` status in the footer means a review model is configured
181
183
 
182
184
  ---
183
185
 
184
- ## 🗜️ compact — 独立压缩模型
186
+ ## 🗜️ compact — dedicated compaction model
185
187
 
186
188
  <a name="compact"></a>
187
189
 
188
- pi 的 `/compact` 与 auto-compact 默认**用当前会话模型**做上下文总结——主模型往往贵且慢,而压缩总结是典型的「便宜快模型就能干好」的任务。本功能配置一个独立模型接管**全部三种压缩触发**的总结:
190
+ pi's `/compact` and auto-compact summarize the context **with the current session model** by default — the main model is usually expensive and slow, while compaction summarization is a textbook "a cheap fast model does this well" job. This feature configures a dedicated model to take over summarization for **all three compaction triggers**:
189
191
 
190
- | 触发 | 接管后 |
192
+ | Trigger | After takeover |
191
193
  | --- | --- |
192
- | 手动 `/compact [instructions]` | 压缩模型总结,自定义 focus 照常注入 |
193
- | auto-compact 阈值触发 | 压缩模型总结 |
194
- | context overflow 恢复 | 压缩模型总结,重试行为不变 |
194
+ | Manual `/compact [instructions]` | summarized by the compaction model, custom focus is injected as usual |
195
+ | auto-compact threshold reached | summarized by the compaction model |
196
+ | context overflow recovery | summarized by the compaction model, retry behavior unchanged |
195
197
 
196
- **接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,0.87.1 起字节级不变、已核对至 1.0.2,只换执行模型):
198
+ **Parts that are identical to pi's built-in behavior after takeover** (the built-in implementation is mirrored verbatim — byte-identical since 0.87.1, verified up to 1.0.2 — and only the executing model changes):
197
199
 
198
- - **摘要格式** —— Goal / Progress / Key Decisions 结构化 checkpoint;已有摘要时用迭代更新版 prompt,信息延续不丢
199
- - **切分行为** —— 保留最近 `keepRecentTokens` 的策略、`firstKeptEntryId` / `tokensBefore` 原样回传,不动
200
- - **输出预算** —— 与内建同公式(历史段 0.8×、切分前缀段 0.5× `reserveTokens`,再按模型输出上限收敛)
201
- - **文件清单尾部** —— `<read-files>` / `<modified-files>` 标签照常拼在摘要尾部,details 同形状维护
202
- - **用量统计** —— 压缩模型的 token/cost 计入 session 统计
203
- - **可取消** —— 压缩进行中 `Esc` 干净取消(信号透传到底层调用)
200
+ - **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
201
+ - **Split behavior** — the `keepRecentTokens` retention policy, and `firstKeptEntryId` / `tokensBefore` passed back unchanged
202
+ - **Output budget** — the same formula as the built-in (history segment 0.8×, split-prefix segment 0.5× of `reserveTokens`, then capped by the model's max output)
203
+ - **File-list tail** — `<read-files>` / `<modified-files>` tags are appended to the summary as usual, with details maintained in the same shape
204
+ - **Usage accounting** — the compaction model's tokens/cost are counted in the session statistics
205
+ - **Cancellable** — `Esc` during compaction cancels cleanly (the signal is passed through to the underlying call)
204
206
 
205
- **配置**(`<agent-dir>/pi-toolkits-compact.json`,默认 `~/.pi/agent/`):
207
+ **Configuration** (`<agent-dir>/pi-toolkits-compact.json`, defaults to `~/.pi/agent/`):
206
208
 
207
209
  ```json
208
210
  { "model": "google/gemini-2.5-flash" }
209
211
  ```
210
212
 
211
- - `/compact-model` —— 查看/更换:`/model` 同款选择器(限高滚动 + 模糊搜索),**默认选中当前配置的模型**,回车即确认;换选即写入;取消则不变(清除配置:编辑 `pi-toolkits-compact.json` 把 `model` 置为 `null`)
212
- - 未配置 = 功能未启用,全部走 pi 默认,零开销;footer 出现灰色 `compact` 状态 = 已配置
213
+ - `/compact-model` — view/replace: the **same selector as `/model`** (height-limited scrolling + fuzzy search), with **the currently configured model selected by default**; Enter confirms, picking another model writes it, cancelling leaves it unchanged (to clear the config: edit `pi-toolkits-compact.json` and set `model` to `null`)
214
+ - Unconfigured = feature disabled, everything goes through pi's default at zero overhead; a grey `compact` status in the footer means it is configured
213
215
 
214
- **无损回落**(本功能永远不会把压缩「做挂」):
216
+ **Lossless fallback** (this feature can never "break" compaction):
215
217
 
216
- | 情形 | 行为 |
218
+ | Situation | Behavior |
217
219
  | --- | --- |
218
- | 未配置 | 直接回落 pi 默认压缩 |
219
- | 配置的模型不可用(如从 models.json 删除) | warning 提示 + 回落 |
220
- | 总结失败 / 摘要为空 / 命中 token 上限 | 提示 + 回落 pi 默认压缩 |
221
- | 用户取消(`Esc`) | 干净取消,无错误刷屏 |
220
+ | Not configured | falls straight back to pi's default compaction |
221
+ | Configured model unavailable (e.g. deleted from models.json) | warning + fallback |
222
+ | Summarization failed / empty summary / token limit hit | notice + fallback to pi's default compaction |
223
+ | User cancelled (`Esc`) | clean cancel, no error spam |
222
224
 
223
- > **实现说明**:接管点是官方的 `session_before_compact` 扩展事件(三种触发都会先经过);返回压缩结果即完全接管,返回 `undefined` 即回落内建。auto-compact 路径绝不弹任何对话框(handler 内只 notify)。`/tree` 分支总结(`session_before_tree`)是另一个事件,本期不接管,留作后续迭代。
225
+ > **Implementation notes**: the takeover point is the official `session_before_compact` extension event (all three triggers pass through it); returning a compaction result takes over completely, returning `undefined` falls back to the built-in. The auto-compact path never shows any dialog (the handler only calls `notify`). `/tree` branch summarization (`session_before_tree`) is a different event and is not taken over in this iteration — it is left for later.
224
226
 
225
227
  ---
226
228
 
227
- ## 🚀 autostart — 会话启动自动化
229
+ ## 🚀 autostart — session startup automation
228
230
 
229
231
  <a name="autostart"></a>
230
232
 
231
- 多仓库项目的第一步:在项目内声明「开新会话时要自动执行的斜杠命令」,首批用例是 `/add-dir` 挂载兄弟仓库——新会话无需手动重放。
233
+ A first step for multi-repo projects: declare inside the project which slash commands should run automatically "when a new session starts" — the first use case being `/add-dir` mounting sibling repositories, so a new session does not have to replay them by hand.
232
234
 
233
- **配置**(`<项目根>/.pi/autostart.toml`,随项目进 git;`.pi/` 是 pi 的项目级配置惯例,先例 `.pi/mcp.json`):
235
+ **Configuration** (`<project root>/.pi/autostart.toml`, committed with the project; `.pi/` is pi's project-level config convention, following `.pi/mcp.json`):
234
236
 
235
237
  ```toml
236
238
  version = 1
@@ -242,29 +244,29 @@ run = [
242
244
  ]
243
245
  ```
244
246
 
245
- 无文件 = 功能未启用,零开销、静默跳过。
247
+ No file = feature disabled, zero overhead, silently skipped.
246
248
 
247
- **执行时机**:
249
+ **When it runs**:
248
250
 
249
- | session_start reason | 是否重放 | 原因 |
251
+ | session_start reason | Replayed | Why |
250
252
  | --- | --- | --- |
251
- | `startup` / `new` | ✅ | 新会话,按声明重放 |
252
- | `fork` | ✅ | 分支点可能早于配置出现 |
253
- | `resume` | ❌ | 恢复的分支已携带当时的挂载状态,重放反而会冲突 |
254
- | `reload` | ❌ | 扩展重建,重放会重复副作用 |
253
+ | `startup` / `new` | ✅ | a new session, replay as declared |
254
+ | `fork` | ✅ | the branch point may predate the config's appearance |
255
+ | `resume` | ❌ | the restored branch already carries the mount state from its time, replaying would conflict |
256
+ | `reload` | ❌ | extensions are rebuilt, replaying would duplicate side effects |
255
257
 
256
- **安全门**(按序):
258
+ **Safety gates** (in order):
257
259
 
258
- 1. **project trust** —— 未信任目录不自动执行任何命令(warning 提示);配置解析失败同样不执行(error 提示)
259
- 2. **命令预校验** —— 只执行「扩展注册的命令」;未注册的命令名**跳过并警告**,绝不发给模型(sendUserMessage 对未处理的 `/x` 会当作普通消息发给 LLM,这是必须预校验的原因)
260
+ 1. **project trust** — no command runs automatically in an untrusted directory (a warning is shown); a config parse failure likewise prevents execution (an error is shown)
261
+ 2. **Command pre-validation** — only commands **registered by extensions** are executed; an unregistered command name is **skipped with a warning** and never sent to the model (for an unhandled `/x`, `sendUserMessage` treats the text as an ordinary message and sends it to the LLM — this is exactly why pre-validation is mandatory)
260
262
 
261
- **关键约束:只支持扩展命令**。`/add-dir`(pi-add-dir)、`/cd`(pi-cd)等 `registerCommand` 注册的命令可以;内置命令(`/compact`、`/reload`、`/resume`…)在 TUI 层分发,扩展层触发不了;skill 命令(`/skill:name`)是展开机制而非执行机制。依赖的目标扩展(如 pi-add-dir)需要先安装。
263
+ **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.
262
264
 
263
- > **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,已对照 pi 0.86–1.0.4 源码核实,见 AGENTS.md 宿主兼容纪律)。预校验用 `pi.getCommands()`(只认 `source: "extension"`)。依赖 `smol-toml`(零依赖、~10KB、TOML 1.0)。
265
+ > **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`; verified against pi 0.86–1.0.4 sources, 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).
264
266
 
265
267
  ---
266
268
 
267
- ## 🛠️ 开发 Development
269
+ ## 🛠️ Development
268
270
 
269
271
  <a name="development"></a>
270
272
 
@@ -276,7 +278,7 @@ pnpm typecheck # tsc --noEmit
276
278
  pnpm test # vitest (all feature unit tests)
277
279
  ```
278
280
 
279
- ### 项目结构 Project structure
281
+ ### Project structure
280
282
 
281
283
  ```text
282
284
  src/
@@ -286,6 +288,8 @@ src/
286
288
  └── features/ # feature modules — one directory per feature
287
289
  ├── ask/ # ask mode: command + state machine, bash sandbox,
288
290
  │ # system prompt banner, tests
291
+ ├── autostart/ # session startup automation: autostart.toml parsing,
292
+ │ # command pre-validation + replay, tests
289
293
  ├── compact/ # dedicated compaction model: session_before_compact
290
294
  │ # takeover + /compact-model, store, prompts,
291
295
  │ # tests
@@ -296,9 +300,9 @@ src/
296
300
  └── stash/ # prompt stash: hotkey state machine + tests
297
301
  ```
298
302
 
299
- **新增功能 · adding a feature**:create `src/features/<name>/` exporting `registerXxx(pi)` and call it from `src/index.ts`. Existing modules stay untouched. Each feature ships its own `*.test.ts` (vitest) — keep them deterministic (inject clocks/state, no real agent-dir writes).
303
+ **Adding a feature**: create `src/features/<name>/` exporting `registerXxx(pi)` and call it from `src/index.ts`. Existing modules stay untouched. Each feature ships its own `*.test.ts` (vitest) — keep them deterministic (inject clocks/state, no real agent-dir writes).
300
304
 
301
- **发布 · publishing**(pnpm-only,one command):
305
+ **Publishing** (pnpm-only, one command):
302
306
 
303
307
  ```bash
304
308
  pnpm release patch # 0.1.2 → 0.1.3
@@ -307,11 +311,11 @@ pnpm release major # 0.1.2 → 1.0.0 (minor + patch zeroed)
307
311
  pnpm release patch --dry-run # preview without changing anything
308
312
  ```
309
313
 
310
- `release` 要求且仅要求 `major | minor | patch` 之一;上级递增清零下级。执行链:**工作区必须干净(有未提交变更直接中止,保证 tag 内容 == 发布内容)** → `typecheck + test` 门禁 → 改版本 → git commit + `vX.Y.Z` tag(自检 tag==HEAD 且工作区干净) → `pnpm publish`(`prepublishOnly` 二次门禁) → 只推当前分支 + 当前 tag 并校验远端哈希一致(远端 tag 不一致则跳过 Release 创建)。
314
+ `release` requires exactly one of `major | minor | patch`; incrementing a higher level zeroes the lower ones. The chain is: **the working tree must be clean (any uncommitted change aborts before anything happens, guaranteeing tag content == published content)** → `typecheck + test` gate → version bump → git commit + `vX.Y.Z` tag (self-check that tag == HEAD and the tree is clean) → `pnpm publish` (with `prepublishOnly` as a second gate) → push only the current branch + the current tag and verify the remote hashes match (if the remote tag differs, GitHub Release creation is skipped).
311
315
 
312
316
  ---
313
317
 
314
- ## 🤝 贡献 Contributing
318
+ ## 🤝 Contributing
315
319
 
316
320
  <a name="contributing"></a>
317
321
 
@@ -322,7 +326,7 @@ PRs and issues welcome. A few conventions:
322
326
  - **Tests** — every feature ships vitest cases; keep them deterministic (injectable clocks/state), no writes to the real agent dir
323
327
  - **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
324
328
  - **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
325
- - **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
329
+ - **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.
326
330
  - **Quality gate** — `pnpm typecheck && pnpm test` must pass before submitting
327
331
 
328
332
  ---
@@ -0,0 +1,337 @@
1
+ <!-- markdownlint-disable MD033 -->
2
+ <!-- Inline <a name> anchors are the only reliable TOC targets for CJK headers on GitHub. -->
3
+ # ⚙️ @andares/pi-toolkits
4
+
5
+ [English](README.md) · **中文**
6
+
7
+ > pi 编码代理的集成扩展工具包 — 一个包,持续生长新能力。
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@andares/pi-toolkits?label=npm&logo=npm)](https://www.npmjs.com/package/@andares/pi-toolkits)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
+ [![Package Manager](https://img.shields.io/badge/package%20manager-pnpm-orange?logo=pnpm)](https://pnpm.io)
12
+ [![pi >= 0.86](https://img.shields.io/badge/pi-%3E%3D0.86.0%20%3C2.0.0-blueviolet)](https://github.com/earendil-works/pi)
13
+ [![GitHub](https://img.shields.io/badge/github-andares%2Fpi--toolkits-181717?logo=github)](https://github.com/andares/pi-toolkits)
14
+
15
+ 一句话介绍:目前包含六个开箱即用的功能——**只读咨询模式** `ask`、**模型收藏** `favorites`、**提示词暂存** `stash`、**第三方代码评审** `third-review`、**独立压缩模型** `compact-model`、**会话启动自动化** `autostart`,后续持续追加。
16
+
17
+ **版本要求**:pi `>=0.86.0 <2.0.0`(覆盖最近 4 个中版本线 0.86.x / 0.87.x / 0.99.x / 1.0.x —— 0.88–0.98 从未发布。区间内全部 10 个已发布版本逐版本比对宿主 dist:用到的扩展 API 无差异,favorites 两处运行时 patch 目标 `ModelSelectorComponent` / `AgentSession.cycleModel` 字节级一致;并在 0.86.0、0.86.1、0.87.1、0.99.2、1.0.2 上逐一通过 typecheck + 全量测试,在真实 pi 1.0.2 上冒烟加载无警告)。注:pi ≥ 0.99 的宿主要求 `@earendil-works/pi-tui` 声明为 peerDependencies `"*"`(由宿主提供),本包已按要求声明。
18
+
19
+ ---
20
+
21
+ ## 📦 安装
22
+
23
+ <a name="install"></a>
24
+
25
+ ```bash
26
+ # 从 npm 安装(推荐)
27
+ pi install npm:@andares/pi-toolkits
28
+
29
+ # 本地开发安装
30
+ pi install .
31
+
32
+ # 单次会话加载,不安装
33
+ pi -e ./src/index.ts
34
+ ```
35
+
36
+ 安装后在任意 pi 会话中验证:
37
+
38
+ - `/ask` → footer 出现灰色 `ask` 状态,`write`/`edit` 被硬禁用
39
+ - `/model`(或 `ctrl+l`)→ 选择器顶部出现收藏提示行
40
+ - 输入框内按 `ctrl+alt+y` → 当前提示词被暂存并清空输入框
41
+ - 完成一个开发任务后输入 `/third-review` → 切换到评审模型执行查+修
42
+ - `/compact-model` 选一个便宜快模型 → 之后 `/compact` 与自动压缩的总结都由它执行(footer 出现灰色 `compact` 状态)
43
+
44
+ ---
45
+
46
+ ## 📚 目录
47
+
48
+ - [安装](#install)
49
+ - [功能总览](#features)
50
+ - [🛡️ ask — 只读咨询模式](#ask)
51
+ - [⭐ favorites — 模型收藏](#favorites)
52
+ - [📥 stash — 提示词暂存](#stash)
53
+ - [🔎 review — 第三方代码评审](#review)
54
+ - [🗜️ compact — 独立压缩模型](#compact)
55
+ - [🚀 autostart — 会话启动自动化](#autostart)
56
+ - [开发](#development)
57
+ - [贡献](#contributing)
58
+ - [许可](#license)
59
+
60
+ ---
61
+
62
+ ## ✨ 功能总览
63
+
64
+ <a name="features"></a>
65
+
66
+ | 功能 | 入口 | 一句话说明 |
67
+ | --- | --- | --- |
68
+ | 🛡️ **ask** 只读咨询模式 | `/ask` | 进入只读问答模式,`write`/`edit` 硬禁用,`bash` 只读沙箱 |
69
+ | ⭐ **favorites** 模型收藏 | `/model` 内 `ctrl+A` / `ctrl+J` | 收藏常用模型(加粗亮黄标记),`ctrl+p` 仅在收藏间循环 |
70
+ | 📥 **stash** 提示词暂存 | `ctrl+alt+y` | 一段提示词的暂存槽,按键交换 / 连按两次暂存并清空输入框 |
71
+ | 🔎 **review** 第三方代码评审 | `/third-review` | 召唤配置的评审模型,对刚完成的任务做查+修,重点检查「本轮需求实现 / 是否有遗漏 / 是否有错误」 |
72
+ | 🗜️ **compact** 独立压缩模型 | `/compact-model` | 给 `/compact` 与 auto-compact 配一个便宜快模型做总结,主模型专注对话;未配置零开销、任何异常回落 pi 默认 |
73
+ | 🚀 **autostart** 会话启动自动化 | `.pi/autostart.toml` | 项目内声明启动时自动执行的斜杠命令(首批用例:`/add-dir` 多仓库挂载);未信任不执行、未注册命令跳过不发给模型 |
74
+
75
+ ---
76
+
77
+ ## 🛡️ ask — 只读咨询模式
78
+
79
+ <a name="ask"></a>
80
+
81
+ 用 `/ask` 进入(再按一次退出)。适合只想要"问问题、读代码",绝不希望模型改动任何文件的场景。
82
+
83
+ **进入后**:
84
+
85
+ - **`write` / `edit` 硬禁用** —— 从活跃工具集中移除,pi 运行时对未激活工具的调用直接拒绝("Tool not found"),文件修改在机制上不可能发生
86
+ - **`bash` 保留但只读沙箱** —— 基于 shell 分词 + 写盘意图检测(`curl -o/-O`、`wget -O`、`dd of=`、`tee`、重定向等一律拦截)。启发式,非安全边界
87
+ - **系统提示词追加** —— 追加一段模式说明,模型会遵守只读约束行事
88
+ - **灰色 `ask` footer 状态** —— 常驻提示当前处于只读模式
89
+
90
+ **退出**:再按 `/ask`,完整恢复进入前的工具集快照。
91
+
92
+ ---
93
+
94
+ ## ⭐ favorites — 模型收藏
95
+
96
+ <a name="favorites"></a>
97
+
98
+ 标记常用模型,让模型切换「粘」在收藏上。收藏**跨会话、跨项目全局持久化**。
99
+
100
+ **在 `/model` 选择器内**(`/model` 或 `ctrl+l` 打开):
101
+
102
+ - 顶部提示行显示按键与当前过滤状态
103
+ - `ctrl+A` —— 对当前选中模型开/关收藏;收藏的模型文字显示为**加粗亮黄**
104
+ - `ctrl+J` —— 开/关「仅显示收藏模型」过滤(可与内置搜索叠加使用)
105
+
106
+ **模型循环**:只要存在 ≥1 个收藏,`ctrl+p` / `ctrl+shift+p` 就**只在收藏模型间循环**。当前模型不是收藏时,`ctrl+p` 跳到第一个收藏、`ctrl+shift+p` 跳到最后一个;收藏仅 1 个时停在原地(`Only one model available`)。`ctrl+l` / `/model` 始终可自由选择任意模型(含非收藏)。在状态文件中设 `"cycleOnlyFavorites": false` 可恢复全量循环。
107
+
108
+ **状态文件**:`<agent-dir>/pi-toolkits-favorites.json`(默认 `~/.pi/agent/`,与 auto-naming-session 配置同目录):
109
+
110
+ ```json
111
+ {
112
+ "favorites": ["anthropic/claude-sonnet-4"],
113
+ "config": { "cycleOnlyFavorites": true }
114
+ }
115
+ ```
116
+
117
+ `/favorites` 命令列出当前收藏与循环模式。
118
+
119
+ > **实现说明**:pi 的 `app.model.cycleForward`(`ctrl+p`)与 `cycleBackward` 是**保留键位**,扩展快捷键无法覆盖;且扩展快捷键只在编辑器聚焦时生效。因此本功能不重绑任何按键,而是在运行时 patch 两个内置原型(带版本守卫、`/reload` 幂等):`ModelSelectorComponent`(提示行、`ctrl+A`/`ctrl+J`、亮黄渲染、仅收藏过滤——按键仅在选择器内消费,编辑器里 `ctrl+j` 换行不受影响)与 `AgentSession.cycleModel`(收藏循环)。若 pi 升级导致 patch 无法应用,会 warn 并优雅降级,不影响其它功能。
120
+ >
121
+ > **为何是 `ctrl+A` 而不是 `ctrl+F`**:pi 自 1.0 起 `tuiMode` 默认 `fullscreen`(0.86–0.99 默认 `regular`),而在 Windows/WSL 上宿主把 `tui.altScreen.search` 解析为 `ctrl+f`;`TuiAltScreen.handleViewportInput()` 在**把输入交给聚焦覆盖层之前**先检查该绑定,于是 `ctrl+f` 打开了会话记录搜索、根本到不了选择器。曾过渡性地选过 `ctrl+alt+f`——pi 层完全空闲,却被 Windows 侧截图工具作为全局热键截走:**宿主表空闲 ≠ 机器层空闲**。最终从 "favorite" 一词里逐字母审计:`v` 是 Windows Terminal 的粘贴键,`o`/`r`/`t` 是宿主 app 层动作,`i` 与 Tab 字节恒等(选择器的 scope 切换),`e` 可用但无记忆点;`a` 仅遮蔽搜索框内的行首跳转、读作 *add favorite*,且选择器上游无任何消费者。完整依据见 `src/features/favorites/constants.ts` 与 `pnpm check:keybindings`。
122
+
123
+ ---
124
+
125
+ ## 📥 stash — 提示词暂存
126
+
127
+ <a name="stash"></a>
128
+
129
+ 一个热键 + 一个暂存槽,在「当前提示词」与「暂存提示词」之间腾挪,方便把一段提示词先放一边、空出输入框写新的,需要时再取回。
130
+
131
+ | 操作 | 效果 |
132
+ | --- | --- |
133
+ | 按一次 `ctrl+alt+y` | 当前提示词 → 入缓存;缓存内容 → 填入输入框(**交换**) |
134
+ | 间隔再按 | 在两段提示词之间**来回切换** |
135
+ | **400ms 内连按两次** | 当前提示词入缓存 + **清空输入框**(旧暂存被丢弃),直接开写新提示词;之后按一次取回 |
136
+
137
+ 缓存为空时,单按一次即「暂存 + 清空」。缓存为**进程内内存、不落盘**(提示词可能含敏感内容);`/new` 切会话不清缓存,`/reload` 清空。选择器(`/model`、`/tree` 等)打开时按键不生效(焦点在组件,不在编辑器)。
138
+
139
+ **为什么是 `ctrl+alt+y`**(四级兼容性调研结论):
140
+
141
+ | 层面 | 状态 | 说明 |
142
+ | --- | --- | --- |
143
+ | pi 键位 | ✅ | 无默认 `ctrl+alt+字母` 绑定;扩展快捷键在编辑器内最先分发 |
144
+ | Linux 终端 | ✅ | `alt` 以 ESC 前缀编码(`ctrl+alt+y` = `ESC + ctrl+y`),不依赖 kitty 协议即可区分 |
145
+ | Windows Terminal | ✅ | 无默认 `ctrl+alt` 绑定 |
146
+ | Windows 操作系统 | ✅ | 系统级仅保留 `ctrl+alt+del` |
147
+
148
+ **被排除的候选**:`ctrl+shift+字母` —— Linux 与 Windows 的终端 emulator 都占用复制/粘贴/标签页;且无 kitty 协议的终端上会坍缩成 `ctrl+字母`(`ctrl+shift+m` 即回车,会直接提交提示词,危险)。纯 `ctrl+字母` —— pi 几乎全部占用,唯一空闲的 `ctrl+q` 是 XON 流控字符,有历史包袱。
149
+
150
+ ---
151
+
152
+ ## 🔎 review — 第三方代码评审
153
+
154
+ <a name="review"></a>
155
+
156
+ 开发任务完成后,输入 `/third-review` 指令召唤**另一个模型**以第三方身份,对刚完成的代码做「查 + 修」:评审模型拥有**完整工具权限**,确认的问题直接改,不是只读审计。评审回合留在当前会话记录里,评审完成后**会话保持评审模型**(不自动切回)。
157
+
158
+ > **仅指令触发**:只有 `/third-review` 能发起评审。聊天中直接输入 `third-review` 不会触发(作为普通消息发给模型),避免误发。
159
+
160
+ **评审提示词重点检查三点**(逐项给出结论):
161
+
162
+ 1. **本轮需求实现** —— 需求是否已完整、正确地实现,与需求描述是否一致
163
+ 2. **是否有遗漏** —— 功能点、边界情况、异常处理、兼容性、测试等
164
+ 3. **是否有错误** —— 逻辑错误、潜在 bug、类型/编译错误、安全与性能隐患等
165
+
166
+ **范围强约束** —— 评审仅限**当前本轮修改内容**,避免扩大范围;本轮改动范围内确认的问题才直接修复,范围外的历史遗留/无关问题只记录提示、不动手改。
167
+
168
+ **评审模型选择**(每次 `/third-review` 都会弹出选择界面):
169
+
170
+ - `/third-review` → 弹出 **`/model` 同款选择器**(限高滚动 + 模糊搜索):**默认选中当前配置的模型**(✓ 标记),直接**回车**即用它开始评审
171
+ - **换选其他模型** → 立即写回配置(**落盘 + 内存**),**下次默认使用该模型**
172
+ - **取消**(Esc)→ 不执行评审,配置不变
173
+ - 配置持久化于 `<agent-dir>/pi-toolkits-review.json`(默认 `~/.pi/agent/`):
174
+
175
+ ```json
176
+ { "model": "anthropic/claude-sonnet-4" }
177
+ ```
178
+
179
+ - `/review-model` —— 单独的配置入口:弹出同一列表(不改会话模型,只更新默认配置)
180
+ - **未配置 / 配置的模型已不可用**(如在 pi 的 models.json 里删掉了该模型)→ 提示后照常弹出列表(此时无默认项)
181
+ - 任务仍在进行中(agent 未空闲)时触发 → 提示等待,不执行;同一时间只允许一个评审
182
+ - footer 出现灰色 `review` 状态 = 已配置评审模型
183
+
184
+ ---
185
+
186
+ ## 🗜️ compact — 独立压缩模型
187
+
188
+ <a name="compact"></a>
189
+
190
+ pi 的 `/compact` 与 auto-compact 默认**用当前会话模型**做上下文总结——主模型往往贵且慢,而压缩总结是典型的「便宜快模型就能干好」的任务。本功能配置一个独立模型接管**全部三种压缩触发**的总结:
191
+
192
+ | 触发 | 接管后 |
193
+ | --- | --- |
194
+ | 手动 `/compact [instructions]` | 压缩模型总结,自定义 focus 照常注入 |
195
+ | auto-compact 阈值触发 | 压缩模型总结 |
196
+ | context overflow 恢复 | 压缩模型总结,重试行为不变 |
197
+
198
+ **接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,0.87.1 起字节级不变、已核对至 1.0.2,只换执行模型):
199
+
200
+ - **摘要格式** —— Goal / Progress / Key Decisions 结构化 checkpoint;已有摘要时用迭代更新版 prompt,信息延续不丢
201
+ - **切分行为** —— 保留最近 `keepRecentTokens` 的策略、`firstKeptEntryId` / `tokensBefore` 原样回传,不动
202
+ - **输出预算** —— 与内建同公式(历史段 0.8×、切分前缀段 0.5× `reserveTokens`,再按模型输出上限收敛)
203
+ - **文件清单尾部** —— `<read-files>` / `<modified-files>` 标签照常拼在摘要尾部,details 同形状维护
204
+ - **用量统计** —— 压缩模型的 token/cost 计入 session 统计
205
+ - **可取消** —— 压缩进行中 `Esc` 干净取消(信号透传到底层调用)
206
+
207
+ **配置**(`<agent-dir>/pi-toolkits-compact.json`,默认 `~/.pi/agent/`):
208
+
209
+ ```json
210
+ { "model": "google/gemini-2.5-flash" }
211
+ ```
212
+
213
+ - `/compact-model` —— 查看/更换:`/model` 同款选择器(限高滚动 + 模糊搜索),**默认选中当前配置的模型**,回车即确认;换选即写入;取消则不变(清除配置:编辑 `pi-toolkits-compact.json` 把 `model` 置为 `null`)
214
+ - 未配置 = 功能未启用,全部走 pi 默认,零开销;footer 出现灰色 `compact` 状态 = 已配置
215
+
216
+ **无损回落**(本功能永远不会把压缩「做挂」):
217
+
218
+ | 情形 | 行为 |
219
+ | --- | --- |
220
+ | 未配置 | 直接回落 pi 默认压缩 |
221
+ | 配置的模型不可用(如从 models.json 删除) | warning 提示 + 回落 |
222
+ | 总结失败 / 摘要为空 / 命中 token 上限 | 提示 + 回落 pi 默认压缩 |
223
+ | 用户取消(`Esc`) | 干净取消,无错误刷屏 |
224
+
225
+ > **实现说明**:接管点是官方的 `session_before_compact` 扩展事件(三种触发都会先经过);返回压缩结果即完全接管,返回 `undefined` 即回落内建。auto-compact 路径绝不弹任何对话框(handler 内只 notify)。`/tree` 分支总结(`session_before_tree`)是另一个事件,本期不接管,留作后续迭代。
226
+
227
+ ---
228
+
229
+ ## 🚀 autostart — 会话启动自动化
230
+
231
+ <a name="autostart"></a>
232
+
233
+ 多仓库项目的第一步:在项目内声明「开新会话时要自动执行的斜杠命令」,首批用例是 `/add-dir` 挂载兄弟仓库——新会话无需手动重放。
234
+
235
+ **配置**(`<项目根>/.pi/autostart.toml`,随项目进 git;`.pi/` 是 pi 的项目级配置惯例,先例 `.pi/mcp.json`):
236
+
237
+ ```toml
238
+ version = 1
239
+
240
+ [command]
241
+ run = [
242
+ "/add-dir ../shop-frontend",
243
+ "/add-dir ../shop-api",
244
+ ]
245
+ ```
246
+
247
+ 无文件 = 功能未启用,零开销、静默跳过。
248
+
249
+ **执行时机**:
250
+
251
+ | session_start reason | 是否重放 | 原因 |
252
+ | --- | --- | --- |
253
+ | `startup` / `new` | ✅ | 新会话,按声明重放 |
254
+ | `fork` | ✅ | 分支点可能早于配置出现 |
255
+ | `resume` | ❌ | 恢复的分支已携带当时的挂载状态,重放反而会冲突 |
256
+ | `reload` | ❌ | 扩展重建,重放会重复副作用 |
257
+
258
+ **安全门**(按序):
259
+
260
+ 1. **project trust** —— 未信任目录不自动执行任何命令(warning 提示);配置解析失败同样不执行(error 提示)
261
+ 2. **命令预校验** —— 只执行「扩展注册的命令」;未注册的命令名**跳过并警告**,绝不发给模型(sendUserMessage 对未处理的 `/x` 会当作普通消息发给 LLM,这是必须预校验的原因)
262
+
263
+ **关键约束:只支持扩展命令**。`/add-dir`(pi-add-dir)、`/cd`(pi-cd)等 `registerCommand` 注册的命令可以;内置命令(`/compact`、`/reload`、`/resume`…)在 TUI 层分发,扩展层触发不了;skill 命令(`/skill:name`)是展开机制而非执行机制。依赖的目标扩展(如 pi-add-dir)需要先安装。
264
+
265
+ > **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,已对照 pi 0.86–1.0.4 源码核实,见 AGENTS.md 宿主兼容纪律)。预校验用 `pi.getCommands()`(只认 `source: "extension"`)。依赖 `smol-toml`(零依赖、~10KB、TOML 1.0)。
266
+
267
+ ---
268
+
269
+ ## 🛠️ 开发
270
+
271
+ <a name="development"></a>
272
+
273
+ **pnpm 是本仓库唯一受支持的包管理器** —— 本仓库内不得使用 `npm install` / `npm publish`。
274
+
275
+ ```bash
276
+ pnpm install # 安装依赖
277
+ pnpm typecheck # tsc --noEmit
278
+ pnpm test # vitest(全部功能单元测试)
279
+ ```
280
+
281
+ ### 项目结构
282
+
283
+ ```text
284
+ src/
285
+ ├── index.ts # 入口:聚合各功能模块
286
+ ├── lib/ # 共享基础设施(工具集快照/恢复、
287
+ │ # review 与 compact 共用的模型 key 解析)
288
+ └── features/ # 功能模块 —— 一个功能一个目录
289
+ ├── ask/ # ask 模式:命令 + 状态机、bash 沙箱、
290
+ │ # 系统提示词横幅、测试
291
+ ├── autostart/ # 会话启动自动化:autostart.toml 解析、
292
+ │ # 命令预校验 + 重放、测试
293
+ ├── compact/ # 独立压缩模型:session_before_compact 接管 +
294
+ │ # /compact-model、配置存储、prompt、测试
295
+ ├── favorites/ # 模型收藏:选择器与循环 patch、
296
+ │ # 持久化存储、测试
297
+ ├── review/ # 第三方评审:输入/指令触发、
298
+ │ # 评审模型配置存储、提示词、测试
299
+ └── stash/ # 提示词暂存:热键状态机 + 测试
300
+ ```
301
+
302
+ **新增功能**:新建 `src/features/<name>/`,导出 `registerXxx(pi)`,在 `src/index.ts` 中调用;已有模块保持不变。每个功能自带 `*.test.ts`(vitest)——保持确定性(时钟/状态可注入,不写真实 agent-dir)。
303
+
304
+ **发布**(仅 pnpm 一条命令):
305
+
306
+ ```bash
307
+ pnpm release patch # 0.1.2 → 0.1.3
308
+ pnpm release minor # 0.1.2 → 0.2.0 (patch 清零)
309
+ pnpm release major # 0.1.2 → 1.0.0 (minor + patch 清零)
310
+ pnpm release patch --dry-run # 预览,不做任何改动
311
+ ```
312
+
313
+ `release` 要求且仅要求 `major | minor | patch` 之一;上级递增清零下级。执行链:**工作区必须干净(有未提交变更直接中止,保证 tag 内容 == 发布内容)** → `typecheck + test` 门禁 → 改版本 → git commit + `vX.Y.Z` tag(自检 tag==HEAD 且工作区干净) → `pnpm publish`(`prepublishOnly` 二次门禁) → 只推当前分支 + 当前 tag 并校验远端哈希一致(远端 tag 不一致则跳过 Release 创建)。
314
+
315
+ ---
316
+
317
+ ## 🤝 贡献
318
+
319
+ <a name="contributing"></a>
320
+
321
+ 欢迎提交 PR 与 issue。几条约定:
322
+
323
+ - **只用 pnpm** —— 不得 `npm install` / `npm publish`;用 `pnpm add` / `pnpm publish`
324
+ - **功能模块模式** —— 一个功能一个目录(`src/features/<name>/`),导出 `registerXxx(pi)`,在 `src/index.ts` 注册;共享基础设施放 `src/lib/`
325
+ - **测试** —— 每个功能自带 vitest 用例;保持确定性(时钟/状态可注入),不写真实 agent dir
326
+ - **内建 patch**(favorites)必须保持**版本守卫 + 幂等** —— 打 patch 前先确认目标原型成员存在,防止重复应用,失败时用 `console.warn` 降级而不是把扩展搞挂
327
+ - **内建提示词同步**(compact) —— `src/features/compact/prompt.ts` 里的总结 prompt 逐字抄自 pi 内建压缩;升级 pi 依赖时 diff `dist/core/compaction/compaction.js`(及 `utils.js`),同步字符串与组装辅助函数,保持接管输出与内建字节级一致
328
+ - **键位** —— 定新快捷键前先查 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`)——定键后必须在真机试按,确认按键真的到得了应用。
329
+ - **质量门禁** —— 提交前必须通过 `pnpm typecheck && pnpm test`
330
+
331
+ ---
332
+
333
+ ## 📄 许可
334
+
335
+ <a name="license"></a>
336
+
337
+ MIT © 2026 Andares Cui. 见 [LICENSE](LICENSE)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andares/pi-toolkits",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), model favorites, prompt stash, third-party code review, and a dedicated compaction model",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -51,6 +51,7 @@
51
51
  "scripts": {
52
52
  "typecheck": "tsc --noEmit",
53
53
  "test": "vitest run",
54
+ "check:keybindings": "node ./scripts/check-keybindings.mjs",
54
55
  "release": "node ./scripts/publish.mjs",
55
56
  "tag-current": "node ./scripts/tag-current.mjs"
56
57
  }
@@ -4,17 +4,67 @@
4
4
  * Keybindings are only active INSIDE the /model model selector (the
5
5
  * ModelSelectorComponent consumes input before the search box, see
6
6
  * patch-selector.ts). Outside the selector the default pi bindings are
7
- * untouched: ctrl+f stays "cursor right" in the editor, ctrl+j stays
8
- * "new line". ctrl+p/ctrl+shift+p keep their bindings — only the cycle
9
- * semantics change (patch-cycle.ts), so no keybinding conflicts arise.
7
+ * untouched: ctrl+j stays "new line". ctrl+p/ctrl+shift+p keep their
8
+ * bindings — only the cycle semantics change (patch-cycle.ts), so no
9
+ * keybinding conflicts arise.
10
+ *
11
+ * Verified against pi 0.86.0 / 0.87.0 / 0.99.0 / 1.0.2 / 1.0.4 keybinding
12
+ * tables + pi-tui input dispatch on WSL2 + Windows Terminal; re-run
13
+ * `pnpm check:keybindings` after every host upgrade before trusting this.
10
14
  */
11
15
  export const FAVORITES_STATUS_KEY = "favorites";
12
16
 
13
17
  /** State file, written next to the auto-naming-session config convention. */
14
18
  export const FAVORITES_FILE = "pi-toolkits-favorites.json";
15
19
 
16
- /** Selector-scoped keybindings (only honored while the model selector is open). */
17
- export const FAVORITE_TOGGLE_KEY = "ctrl+f";
20
+ /**
21
+ * Selector-scoped keybindings (only honored while the model selector is open).
22
+ *
23
+ * Toggle key history: ctrl+f (through v0.6.0) → ctrl+alt+f (discarded before
24
+ * release) → ctrl+a (current). Each step is a real layer the previous pick
25
+ * missed — audit trail below; re-run `pnpm check:keybindings` on upgrades.
26
+ *
27
+ * Why ctrl+f is dead: fullscreen became pi's default `tuiMode` in 1.0
28
+ * (0.86–0.99 defaulted to `regular`, where the binding is inert). On
29
+ * Windows/WSL `tui.altScreen.search` resolves to ctrl+f, and
30
+ * `TuiAltScreen.handleViewportInput()` checks it before
31
+ * `shouldDeferViewportInputToOverlay()`, so the keystroke opens transcript
32
+ * search and the selector never sees it (verified in pi-tui 1.0.4
33
+ * `dist/tui-alt-screen.js`: the search check is the first key branch and
34
+ * returns `{ consume: true }` unconditionally).
35
+ *
36
+ * Why ctrl+alt+f was discarded: clean at the pi layer across 0.86–1.0 (only
37
+ * ctrl+alt+] is taken from that family) and byte-distinguishable without
38
+ * kitty — but grabbed at the OS/app layer on the author's machine by a
39
+ * Windows screenshot tool, so it never reached the terminal. Lesson: a key
40
+ * that is free in the host table is not necessarily free on the machine.
41
+ *
42
+ * Why ctrl+a (letter taken from "favorite", audited with the host's own
43
+ * matchesKey on WSL2 + Windows Terminal):
44
+ * - f: consumed upstream by `tui.altScreen.search` in fullscreen (above).
45
+ * - v: free at the pi layer on WSL (`app.clipboard.pasteImage` is alt+v
46
+ * there) but Windows Terminal binds ctrl+v to paste — and pasting a model
47
+ * name into the search box is a feature, not a slot to steal.
48
+ * - o / r / t: host-owned at the app layer (`app.tools.expand`,
49
+ * `app.session.rename`, `app.thinking.toggle`).
50
+ * - i: byte-identical to Tab on non-kitty terminals ("\t"), and Tab is the
51
+ * selector's built-in scope toggle — a guaranteed fight.
52
+ * - e: viable (shadows only `tui.editor.cursorLineEnd` inside the selector)
53
+ * but carries no mnemonic; a was preferred.
54
+ * - a: shadows only `tui.editor.cursorLineStart` (readline home) inside the
55
+ * selector — our patch consumes it before the search box, exactly like
56
+ * ctrl+j shadows newLine. No upstream consumer, nothing bound at the
57
+ * Windows Terminal / Windows OS layer, no other loaded extension claims
58
+ * it, and "a" reads as *add favorite*.
59
+ * (Rejected elsewhere: alt+f = cursorWordRight + tree unfold; shift+f is
60
+ * the literal character "F" without kitty, stealing typed search text;
61
+ * ctrl+shift+f collapses to ctrl+f on Windows/WSL and is Windows Terminal's
62
+ * own Find.)
63
+ *
64
+ * ctrl+j is kept: no host layer consumes it before the selector, so it only
65
+ * shadows `tui.input.newLine` inside the search box.
66
+ */
67
+ export const FAVORITE_TOGGLE_KEY = "ctrl+a";
18
68
  export const FAVORITES_ONLY_KEY = "ctrl+j";
19
69
 
20
70
  /**
@@ -6,8 +6,8 @@
6
6
  * - cycle patch: favorites-only cycling (fallback to stock when disabled or
7
7
  * no favorites; favorites-only pick; single-favorite jump; not-a-favorite
8
8
  * → forward first / backward last)
9
- * - selector patches: ctrl+j favorites-only filter, ctrl+f toggle, hint
10
- * injection, idempotent apply
9
+ * - selector patches: ctrl+j favorites-only filter, ctrl+a toggle, hint
10
+ * injection, idempotent apply, plain ctrl+f left to the host
11
11
  *
12
12
  * The patches read the shared store via getFavoritesStore(); each test gets an
13
13
  * isolated store backed by a temp file via setSharedFavoritesStore(). The
@@ -408,18 +408,37 @@ describe("selector patches", () => {
408
408
  expect(selector.filteredModels).toHaveLength(3);
409
409
  });
410
410
 
411
- it("ctrl+f toggles favorite for the selected model", () => {
411
+ it("ctrl+a toggles favorite for the selected model", () => {
412
412
  const selector = fakeSelector({
413
413
  filteredModels: models.map(item),
414
414
  selectedIndex: 1, // opus-4
415
415
  activeModels: models.map(item),
416
416
  });
417
- selector.handleInput("\x06"); // ctrl+f
417
+ selector.handleInput("\x01"); // ctrl+a
418
418
  expect(getFavoritesStore().has(models[1])).toBe(true);
419
- selector.handleInput("\x06");
419
+ selector.handleInput("\x01");
420
420
  expect(getFavoritesStore().has(models[1])).toBe(false);
421
421
  });
422
422
 
423
+ it("does not intercept the retired keys ctrl+f / ctrl+alt+f (host/machine-owned)", () => {
424
+ const forwarded: string[] = [];
425
+ getFavoritesStore().toggle(models[1]);
426
+ const selector = fakeSelector({
427
+ filteredModels: models.map(item),
428
+ selectedIndex: 1,
429
+ activeModels: models.map(item),
430
+ searchInput: {
431
+ getValue: () => "",
432
+ handleInput: (d: string) => forwarded.push(d),
433
+ },
434
+ });
435
+ selector.handleInput("\x06"); // stock ctrl+f — upstream transcript search
436
+ selector.handleInput("\x1b\x06"); // retired ctrl+alt+f — machine-level hotkey
437
+ // Both reached the search box instead of our toggle, so the store is unchanged.
438
+ expect(forwarded).toEqual(["\x06", "\x1b\x06"]);
439
+ expect(getFavoritesStore().has(models[1])).toBe(true);
440
+ });
441
+
423
442
  it("injects the hint row once (children.length grows by exactly one)", () => {
424
443
  const selector = fakeSelector({
425
444
  children: [1, 2, 3, 4] as unknown[],
@@ -4,11 +4,15 @@
4
4
  * What it does:
5
5
  * - Inside the /model selector (opened via /model or ctrl+l):
6
6
  * * a hint row at the top shows the keys and the favorites-only state
7
- * * ctrl+f toggles favorite on the currently selected model
7
+ * * ctrl+a toggles favorite on the currently selected model
8
8
  * * ctrl+j toggles the "only favorites" filter
9
9
  * * favorited model ids render in bold bright yellow
10
10
  * The keys are consumed by the selector component only, so pi's editor
11
- * bindings (ctrl+f cursor-right, ctrl+j newline) are untouched elsewhere.
11
+ * bindings (ctrl+j newline) are untouched elsewhere. (The toggle moved off
12
+ * ctrl+f after v0.6.0 — pi >= 1.0 defaults to fullscreen, where ctrl+f is
13
+ * consumed by the alt-screen transcript search before the selector sees
14
+ * it — and an interim ctrl+alt+f pick turned out to be grabbed by a
15
+ * Windows screenshot tool; see constants.ts for the full key audit.)
12
16
  * - ctrl+p / ctrl+shift+p keep their bindings but, once favorites exist
13
17
  * (and cycleOnlyFavorites is not false), cycle only among favorited
14
18
  * models instead of all models. ctrl+l / /model remain the escape hatch
@@ -19,7 +23,7 @@
19
23
  * Implementation: pi's extension API has no hook to render inside the
20
24
  * built-in model selector and cannot rebind the reserved ctrl+p actions, so
21
25
  * the feature patches two built-in prototypes at runtime:
22
- * - ModelSelectorComponent (hint row, ctrl+f/ctrl+j, styling, filter)
26
+ * - ModelSelectorComponent (hint row, favorites keys, styling, filter)
23
27
  * - AgentSession.cycleModel (favorites-only cycling)
24
28
  * Both patches are version-guarded (skip + warn if the API changed) and
25
29
  * idempotent across /reload.
@@ -91,13 +95,14 @@ export function registerFavorites(pi: ExtensionAPI): void {
91
95
  applyCyclePatch();
92
96
 
93
97
  pi.registerCommand("favorites", {
94
- description: "List favorited models (add/remove with ctrl+F in /model)",
98
+ description:
99
+ "List favorited models (add/remove with ctrl+A in /model)",
95
100
  handler: async (_args: string, ctx: ExtensionContext) => {
96
101
  captureTheme(ctx);
97
102
  const list = getFavoritesStore().list();
98
103
  if (list.length === 0) {
99
104
  ctx.ui.notify(
100
- "No favorited models. Open /model and press ctrl+F on a model to favorite it.",
105
+ "No favorited models. Open /model and press ctrl+A on a model to favorite it.",
101
106
  "info",
102
107
  );
103
108
  return;
@@ -4,11 +4,11 @@
4
4
  * pi's extension API has no hook to render inside the built-in model
5
5
  * selector, so we patch the component prototype at runtime. Three hooks:
6
6
  *
7
- * 1. handleInput — intercept ctrl+f (toggle favorite for the selected
8
- * model) and ctrl+j (toggle "only favorites" filter)
9
- * BEFORE the original forwards them to the search box.
10
- * Scoped to the selector: outside it, ctrl+f/ctrl+j keep
11
- * pi's default editor bindings untouched.
7
+ * 1. handleInput — intercept the favorites keys (see constants.ts for why
8
+ * they are ctrl+a / ctrl+j) BEFORE the original
9
+ * forwards them to the search box. Scoped to the
10
+ * selector: outside it, ctrl+j keeps pi's default editor
11
+ * binding untouched.
12
12
  * 2. updateList — inject a hint row at the top of the selector and render
13
13
  * favorited model ids in bold bright yellow.
14
14
  * 3. filterModels — when "only favorites" is on, temporarily swap
@@ -22,7 +22,7 @@
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
24
  * fields below) is byte-identical across every published version in
25
- * pi 0.86.0 → 1.0.2.
25
+ * pi 0.86.0 → 1.0.4.
26
26
  */
27
27
  import {
28
28
  ModelSelectorComponent,
@@ -138,7 +138,7 @@ export function applyModelSelectorPatches(): boolean {
138
138
  const originalUpdateList = proto.updateList;
139
139
  const originalFilterModels = proto.filterModels;
140
140
 
141
- // ── 1. Key interception (ctrl+f / ctrl+j) ─────────────────────────────
141
+ // ── 1. Key interception (favorites keys) ────────────────────────────
142
142
  proto.handleInput = function (
143
143
  this: SelectorPatchTarget,
144
144
  keyData: string,
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Current features:
8
8
  * - ask — read-only Q&A mode (/ask)
9
- * - favorites — model favorites (ctrl+F/ctrl+J in /model, favorites-only cycling)
9
+ * - favorites — model favorites (ctrl+A/ctrl+J in /model, favorites-only cycling)
10
10
  * - stash — prompt stash (ctrl+alt+y swap / double-press stash-and-clear)
11
11
  * - review — third-party code review with a configured review model (third-review)
12
12
  * - compact — dedicated compact model for /compact + auto-compact summaries