@ai-zen/cli 1.0.0-alpha.31 → 1.0.0-alpha.32

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/CHANGELOG.md CHANGED
@@ -1,1349 +1,1502 @@
1
+ ## [1.0.0-alpha.32] - 2026-11-03
2
+
3
+ > This release does one thing: it removes the startup splash animation. Previously every `ai`
4
+ > startup waited about 1.8s for the reveal + sweep before entering the chat, and that pure
5
+ > blocking sat right in front of "read config → resume session → create session". Now it goes
6
+ > straight into the chat, and the branding moved into the chat area itself, landing in the
7
+ > vertical middle when the conversation is empty.
8
+
9
+ ### 💥 Behavior changes
10
+
11
+ - **No splash animation on startup** — removed `splash.tsx` and the `await mount(<Splash />)`
12
+ in `runTui()`. The old implementation revealed line by line over 330ms + a 700ms diagonal
13
+ gradient sweep + a 1100ms hold and auto fade-out, about 1.8s in total; after removal the
14
+ remaining blank window measured about 66ms (`readConfig` three times + `AgentRepository.list`,
15
+ all local file reads), so initialization was not moved into the chat screen.
16
+ - **An empty conversation shows the brand block in the vertical middle of the chat area** — logo
17
+ (6 lines of ASCII on wide terminals, degrading to `◢ AI · ZEN ◣` on narrow ones) + tagline +
18
+ the three version numbers `CLI / sdk / core`, each line centered on its own. It is now
19
+ ordinary transcript content: it scrolls up naturally with messages and can be scrolled back
20
+ into view; it yields as soon as the first message is typed (including the optimistic echo).
21
+ The gradient is fixed at phase 0 and runs no timers.
22
+ - **The version numbers moved** — previously they appeared once on the splash, now they hang in
23
+ the brand block; the bottom status bar (model · Agent · reasoning effort | per-turn /
24
+ cumulative usage · rate | keybindings) is unchanged, character for character.
25
+ - **`computeChatLayout` gains `anchor: "bottom" | "center"`** (defaults to `bottom`, existing
26
+ behavior unchanged): an empty conversation passes `center`, splitting the remainder below one
27
+ screenful evenly between top and bottom. Pinning to the top leaves a big empty band in the
28
+ middle, pinning to the bottom presses right onto the input box, and neither looks good.
29
+ - Bottom-pinning semantics are unchanged: once there are messages, the whole remainder is still
30
+ added at the head, so the bottom edge of the latest line sits right against the divider.
31
+
32
+ ### 🧪 Tests
33
+
34
+ - 806 cases all green, `tsc` (production / test configs) and `npm run build` clean.
35
+ - Added: 6 cases in `welcome.test.ts` (wide / narrow logo selection, tagline, three version
36
+ spots, per-character coloring, per-line centering), 2 chat screen cases (appears on an empty
37
+ conversation / disappears after the first message), 1 case for `anchor: center`; removed the
38
+ 2 Splash cases.
39
+ - Known leftover: the two `↑` history cases in `chat.test.tsx` were already flaky in the
40
+ previous release (`flush(20)` colliding with the 60ms frame batching), unrelated to this
41
+ release, not yet addressed.
42
+
1
43
  ## [1.0.0-alpha.31] - 2026-11-03
2
44
 
3
- > 本版把 CLI 接到重审后的 SDK 上:`@ai-zen/agents-sdk` alpha.26 + `@ai-zen/agents-core`
4
- > 4.5.4。CLI 自己的改动很小(交接期间让护栏自己让行、交接文件名去掉会话 id 后缀),
5
- > 但 SDK 那 24 条修复全部随本版到达用户 —— 其中几条是「一条普通命令就能让进程卡死」
6
- > 或「工具报成功其实啥也没干」级别的。
45
+ > This release attaches the CLI to the re-reviewed SDK: `@ai-zen/agents-sdk` alpha.26 +
46
+ > `@ai-zen/agents-core` 4.5.4. The CLI's own changes are small (guardrails stand down by
47
+ > themselves during handoff, handoff document filenames drop the session id suffix), but all
48
+ > 24 SDK fixes reach users with this release — several of them are of the "one ordinary command
49
+ > is enough to wedge the process" or "the tool reported success but did nothing at all" kind.
7
50
  >
8
- > 口径没变:工具层不承担安全边界(模型不是攻击者),只修正常用法会踩到的东西。
9
-
10
- ### 💥 行为变更(来自 SDK alpha.26)
11
-
12
- - **`exec` 的退出码不再是 `null`** —— 命令正常结束报 `0`,非零如实返回;模型不再把
13
- 成功的命令误判成「没有正常结束」。
14
- - **超时会回收整个进程组** —— 以前只杀 shell,`cmd | cat`、`sh -c '… &'` 的孙进程留成孤儿
15
- (`sleep 45 &` 还能当常驻进程用,绕过 `spawn`)。现在 `detached` + `kill(-pid)` 整组回收。
16
- - **`exec` 输出截断如实标注** —— `maxBuffer` 显式设到 16MB,真截断时报 `truncated`,
17
- 不再把 `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` 当退出码、同时谎称「完整内容已落盘」。
18
- - **`download_file` 新增必填 `timeout`(毫秒)**,并改流式写盘(200MB 文件不再吃 650MB 堆);
19
- 中断时不再删除同路径下原本就存在的文件。
20
- - **`spawn` 不再假报成功** —— 返回字段 `success` 改为 `started`,并写明「不跟踪退出状态」;
21
- 命令不存在 / 语法错误 / `exit 3` 不会再显示成成功。
22
- - **`glob` / `find_text` 对不存在的目录报错**,不再静默返回 `[]` 让你以为文件不存在。
23
- - **`generate_image` 支持只返回 `b64_json` 的端点** —— 落盘后给本地路径,不再
24
- `success: true` 却一张图都没有。
25
- - **`edit` / `batch_edit` 修掉两个会毁文件的坑** —— 空 `oldText` 会让进程同步死循环(连
26
- `Ctrl+C` 都没用);替换文本里的 `$&`、`$$` 会被当成替换模式,静默写坏文件。
27
- - **`permissions` 真正生效** —— 被 deny 的 skill 不能再靠点名加载;`mcps` 里没允许的 server
28
- 即使别的 Agent 连上了也调不到。
29
- - **MCP 连接更稳** —— 建连中退出不会「复活」连接(stdio 子进程不再留孤儿);运行期断线
30
- 会把状态标回 `disconnected`(`load_mcp` 不再返回过期清单);建连有 60s 超时。
31
-
32
- ### 🛠️ CLI 自身
33
-
34
- - **交接/迁移期间的护栏让行由护栏自己判断**(`isSuspended: () => handoffService.busy`)。
35
- 以前是趁交接前把 `lastUsage` 清零 —— 那是同一事实的两份拷贝,改一份另一份会过期。
36
- 现在 `lastUsage` 是从 `messages` 派生的只读值,换掉 `messages` 就自动跟随。
37
- - **交接文档文件名去掉会话 id 后缀**:`<YYYYMMDD-HHmmss>.md`。会话名往往是手改的,
38
- 带上只会更难认;同一秒并发交接本就该避免,不为它加随机后缀。
39
-
40
- ### 🧪 测试
41
-
42
- - 799 用例全绿(含构建与 e2e 门禁),`tsc` 双配置(生产 / 测试)干净。
43
- - 新增:护栏接线用例(交接中让行、结束后恢复硬线);用量构造改为「带 usage 的 assistant
44
- 消息」(`lastUsage` 已只读)。
51
+ > The stance is unchanged: the tool layer does not carry the safety boundary (the model is not
52
+ > an attacker); it only fixes things normal usage runs into.
53
+
54
+ ### 💥 Behavior changes (from SDK alpha.26)
55
+
56
+ - **The `exec` exit code is no longer `null`** — a command that ends normally reports `0`, and
57
+ non-zero codes are returned truthfully; the model no longer misreads a successful command as
58
+ "did not end normally".
59
+ - **A timeout reclaims the whole process group** — previously only the shell was killed, so the
60
+ grandchildren of `cmd | cat`, `sh -c '… &'` were left as orphans (`sleep 45 &` could even
61
+ serve as a resident process, bypassing `spawn`). Now `detached` + `kill(-pid)` reclaims the
62
+ entire group.
63
+ - **`exec` output truncation is labeled truthfully** — `maxBuffer` is explicitly set to 16MB, a
64
+ real truncation reports `truncated`, and `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` is no longer
65
+ taken as an exit code while falsely claiming "the full content was written to disk".
66
+ - **`download_file` gains a required `timeout` (milliseconds)** and now writes to disk as a
67
+ stream (a 200MB file no longer eats 650MB of heap); on interruption it no longer deletes a
68
+ file that already existed at the same path.
69
+ - **`spawn` no longer reports false success** — the returned field `success` becomes `started`,
70
+ and it states that "exit status is not tracked"; a missing command / syntax error / `exit 3`
71
+ no longer displays as success.
72
+ - **`glob` / `find_text` report an error for nonexistent directories**, no longer silently
73
+ returning `[]` and letting you believe the file does not exist.
74
+ - **`generate_image` supports endpoints that only return `b64_json`** — after writing to disk
75
+ it gives a local path, no more `success: true` with not a single image.
76
+ - **`edit` / `batch_edit` fix two file-destroying pitfalls** — an empty `oldText` puts the
77
+ process into a synchronous infinite loop (not even `Ctrl+C` helps); `$&` and `$$` in the
78
+ replacement text are taken as replacement patterns and silently corrupt the file.
79
+ - **`permissions` actually take effect** — a denied skill can no longer be loaded by naming it;
80
+ a server not allowed in `mcps` cannot be called even if another Agent has connected to it.
81
+ - **MCP connections are more stable** — quitting mid-connect no longer "resurrects" the
82
+ connection (stdio child processes are no longer left orphaned); a runtime disconnect marks the
83
+ status back to `disconnected` (`load_mcp` no longer returns a stale manifest); connecting has
84
+ a 60s timeout.
85
+
86
+ ### 🛠️ CLI itself
87
+
88
+ - **Stand-down of the guardrails during handoff/migration is decided by the guardrails themselves**
89
+ (`isSuspended: () => handoffService.busy`). Previously `lastUsage` was zeroed right before
90
+ handoff — that was two copies of the same fact, and changing one left the other stale. Now
91
+ `lastUsage` is a read-only value derived from `messages`; replacing `messages` follows
92
+ automatically.
93
+ - **Handoff document filenames drop the session id suffix**: `<YYYYMMDD-HHmmss>.md`. Session
94
+ names are usually edited by hand, so carrying them only makes files harder to recognize;
95
+ concurrent handoffs within the same second should be avoided anyway, and no random suffix is
96
+ added for it.
97
+
98
+ ### 🧪 Tests
99
+
100
+ - 799 cases all green (including the build and e2e gates), `tsc` clean on both configs
101
+ (production / test).
102
+ - Added: guardrail wiring cases (stand down during handoff, restore the hard line afterwards);
103
+ usage construction switched to an "assistant message with usage" (`lastUsage` is read-only
104
+ now).
45
105
 
46
106
  ## [1.0.0-alpha.30] - 2026-11-02
47
107
 
48
- > 本版把 22 条审查问题一次收口。有三处直接改变手感:补全菜单改为底部锚定、多行输入里
49
- > `↑`/`↓` 先移光标、输入区限高 8 行;另外详情屏不再暴露 `custom`,换成了「恢复出厂设置」。
108
+ > This release closes out 22 review issues in one pass. Three of them change the feel directly:
109
+ > the completion menu is anchored to the bottom, `↑`/`↓` move the cursor first in multi-line
110
+ > input, and the input area is capped at 8 lines; the detail screen also no longer exposes
111
+ > `custom`, replaced by "restore factory defaults".
50
112
  >
51
- > 注:`1.0.0-alpha.28` / `.29` 的条目此前也只在本地(registry 上的 `alpha` 停在 alpha.27),
52
- > 本版一并带出去。
53
-
54
- ### 💥 行为变更
55
-
56
- - **补全菜单改为底部锚定** —— `/` 命令与 `@` 路径共用一套 `BottomMenu`:`items[0]` 紧贴输入框,
57
- 其余向上生长、装不下从顶端丢弃;行数上限由布局按剩余空间放行(不再固定 8 行),
58
- 渲染与 IME 光标读同一个 `layout.hintRows`。说明文字不再把命令名挤成一截。
59
- - **`↑` / `↓` 先移动光标** —— 多行草稿里到首行 / 末行才翻历史(原来一按就跳历史)。
60
- - **输入区限高 8 行**,窗口跟随光标(原来钉住末尾,光标退到第一行时会消失在框外)。
61
- - **`↑` 翻历史带图片** —— 历史数据源统一到 `messages`,回到带图消息时图片一起恢复;
62
- `file_id` 类型的历史图片只能恢复文本(本地没有副本)。
63
- - **`/load` 列表只显示会话文件名** —— 不再逐个读会话文件解析摘要。
64
- - **`/save <名字>`** —— 另存为指定文件名(旧文件名消失、last-session 指针跟随);
65
- 重名且不是当前会话时先二次确认。
66
- - **详情屏不再有 `custom` 开关,改为「恢复出厂设置」** —— 出厂模型 / 图片模型用出厂定义
67
- **整条替换**(`custom` 随之消失 = 重新归 SDK 托管);默认 Agent 只把提示词换回出厂,
68
- 名称 / 描述 / 模型 / 权限全部保留;出厂清单里没有同 id 的(自建)不显示该动作。
69
- - **粘贴文本 256KB 上限** —— 超了报错且**不截断**(半截代码 / 半截 JSON 比不粘更坑)。
70
- - **写句子不再乱弹 `@` 菜单** —— token 边界补上 `。 、 ! ? ( ) 《 》 【 】 〔 〕 「 」 『 』 … — ~`。
71
- - **没有端点时禁止新建模型 / 图片模型** —— 以前能创建出 `endpointId: ""` 的空壳。
72
- - **坏掉的 `mcp.json` 不再冒充空配置** —— MCP 屏写明「无法解析」、照样能编辑,
73
- 保存产出的正是一份格式正确的新文件(那是修复,不是覆盖丢数据)。
74
-
75
- ### 🐛 修复
76
-
77
- - KV 编辑器删掉末项后回车崩溃(光标钳到新的末项)。
78
- - `SelectList` 列表变短后回车崩溃(渲染与确认都走 `safeIndex`)。
79
- - 自动落盘失败混在 info 语气里上报。
80
- - `/back` 撤回后不落盘(存档里还留着被撤回的消息)。
81
- - 删掉默认 Agent 时没清 `config.defaultAgent`,下次启动死在「Agent 未绑定模型」。
82
- - stdout 被提前关闭时打 EPIPE 堆栈(按正常结束处理)。
83
- - `/load` `/back` 选择器条数预算少扣 2 行,最后两条被吸底布局吃掉。
84
- - `truncate` 按字素簇截断,不再把 emoji 劈成孤立代理项。
85
- - e2e 断言与事实不符(`ai hook` 写 stdout、退出码 0),且被 `vitest.config.ts` 排除、
86
- 永远不会红 —— 断言修正后从 5 红变全绿。
87
-
88
- ### 🔒 安全
89
-
90
- - **`@ai-zen/agents-sdk` 升到 `1.0.0-alpha.25`**:`EntityRepository.path()` 校验实体 id 不得逃出
91
- 仓储目录(`resolve` + `root + sep`,裸 `startsWith` 拦不住 `<root>-evil`)。`/save ../x`
92
- 现在明确报错,不再可能写到会话目录之外。这一版同时带上 SDK 的端点 `capabilities`
93
- 迁移回填(老的 `deepseek` 端点会补上 `filesApi`,图片改走 Files API)。
94
-
95
- ### ✅ 测试
96
-
97
- - 单测 **57 文件 / 798 例通过**,e2e **9 passed / 3 skipped**(3 条需真 API key),
98
- `tsc --noEmit` 干净。
99
- - 发布门禁 `prepublishOnly` 改为 `npm run test:all`(单测 + 构建 + e2e);
100
- `npm test` 语义不变,仍只跑类型 + 单测。
101
- - 测试耗时 43.6s → ~8s:去掉 `isolate`、单 fork、依赖注入替换全部模块级 `vi.mock`。
113
+ > Note: the `1.0.0-alpha.28` / `.29` entries had so far only existed locally (the registry's
114
+ > `alpha` tag stopped at alpha.27); this release carries them out as well.
115
+
116
+ ### 💥 Behavior changes
117
+
118
+ - **The completion menu is anchored to the bottom** — `/` commands and `@` paths share one
119
+ `BottomMenu`: `items[0]` sits right above the input box, the rest grows upward and is dropped
120
+ from the top when it does not fit; the row cap is granted by the layout according to the
121
+ remaining space (no longer fixed at 8 rows), and rendering and the IME cursor read the same
122
+ `layout.hintRows`. Descriptions no longer squeeze the command name into a stub.
123
+ - **`↑` / `↓` move the cursor first** — in a multi-line draft they page through history only at
124
+ the first / last line (previously one press jumped to history).
125
+ - **The input area is capped at 8 lines**, and the window follows the cursor (previously it was
126
+ pinned to the end, so the cursor disappeared outside the box when it went back to the first
127
+ line).
128
+ - **`↑` history paging carries images** — the history data source is unified on `messages`, so
129
+ going back to a message with images restores the images as well; history images of type
130
+ `file_id` can only restore text (there is no local copy).
131
+ - **The `/load` list shows session filenames only** — no longer reading each session file to
132
+ parse a summary.
133
+ - **`/save <name>`** — save as a specified filename (the old filename disappears, the
134
+ last-session pointer follows); if the name is taken and is not the current session, a second
135
+ confirmation is asked first.
136
+ - **The detail screen no longer has a `custom` toggle, replaced by "restore factory defaults"**
137
+ — the factory model / image model are **replaced wholesale** with the factory definition
138
+ (`custom` disappears with it = managed by the SDK again); the default Agent only swaps the
139
+ prompt back to the factory one, keeping the name / description / model / permissions all
140
+ intact; entries with no matching id in the factory list (self-built) do not show the action.
141
+ - **256KB cap on pasted text** — exceeding it reports an error and does **not truncate** (half
142
+ a piece of code / half a JSON is worse than not pasting).
143
+ - **Writing sentences no longer pops up the `@` menu at random** — the token boundary adds
144
+ `。 、 ! ? ( ) 《 》 【 】 〔 〕 「 」 『 』 … — ~`.
145
+ - **No endpoint, no new model / image model** — previously an empty shell with
146
+ `endpointId: ""` could be created.
147
+ - **A broken `mcp.json` no longer passes itself off as empty config** — the MCP screen states
148
+ "cannot be parsed" and stays editable, and saving produces exactly a new, correctly formatted
149
+ file (that is a repair, not a data-losing overwrite).
150
+
151
+ ### 🐛 Fixes
152
+
153
+ - The KV editor crashed on Enter after deleting the last item (the cursor is clamped to the new
154
+ last item).
155
+ - `SelectList` crashed on Enter after the list got shorter (both rendering and confirmation go
156
+ through `safeIndex`).
157
+ - Automatic save failures were reported in an info tone.
158
+ - `/back` did not save after reverting (the reverted messages stayed in the archive).
159
+ - Deleting the default Agent did not clear `config.defaultAgent`, and the next startup died on
160
+ "Agent has no model bound".
161
+ - An EPIPE stack trace was printed when stdout was closed early (now handled as a normal exit).
162
+ - The `/load` `/back` selector row budget under-counted by 2 rows, so the bottom-pinned layout
163
+ ate the last two entries.
164
+ - `truncate` truncates by grapheme cluster and no longer splits emoji into lone surrogates.
165
+ - The e2e assertion did not match reality (`ai hook` writes to stdout, exit code 0) and was
166
+ excluded by `vitest.config.ts`, so it could never go red — after correcting the assertion it
167
+ went from 5 red to all green.
168
+
169
+ ### 🔒 Security
170
+
171
+ - **`@ai-zen/agents-sdk` bumped to `1.0.0-alpha.25`**: `EntityRepository.path()` now validates
172
+ that an entity id cannot escape the repository directory (`resolve` + `root + sep`; a bare
173
+ `startsWith` cannot stop `<root>-evil`). `/save ../x` now reports a clear error and can no
174
+ longer write outside the session directory. This release also carries the SDK's endpoint
175
+ `capabilities` migration backfill (old `deepseek` endpoints get `filesApi`, and images move
176
+ to the Files API).
177
+
178
+ ### ✅ Tests
179
+
180
+ - Unit tests **57 files / 798 cases passing**, e2e **9 passed / 3 skipped** (3 need a real API
181
+ key), `tsc --noEmit` clean.
182
+ - The release gate `prepublishOnly` changed to `npm run test:all` (unit tests + build + e2e);
183
+ `npm test` keeps its semantics and still runs only types + unit tests.
184
+ - Test time 43.6s → ~8s: removed `isolate`, single fork, dependency injection replaced all
185
+ module-level `vi.mock`.
102
186
 
103
187
  ## [1.0.0-alpha.29] - 2026-10-31
104
188
 
105
- > `Ctrl+V` 从「弹一句未支持」变成**真的粘贴**:剪贴板里有图片就附在消息上,
106
- > 没有就退回粘贴文本。
189
+ > `Ctrl+V` goes from "pops up an unsupported notice" to **actually pasting**: if the clipboard
190
+ > holds an image it is attached to the message, otherwise it falls back to pasting text.
107
191
 
108
- ### ✨ 新功能
192
+ ### ✨ New features
109
193
 
110
- - **粘贴图片** —— `Ctrl+V`(kitty 协议下 `⌘V`)先问系统剪贴板要图片,有图就附到待发送
111
- 消息上,输入框上方出现 `📎 已附加 N 张图片 · Ctrl+V 再加 · Esc 清空`;没有图片则
112
- 当作普通粘贴,把剪贴板文本插到光标处。按平台探测:
194
+ - **Pasting images** — `Ctrl+V` (or `⌘V` under the kitty protocol) first asks the system
195
+ clipboard for an image; if there is one it is attached to the message being composed, and
196
+ `📎 N images attached · Ctrl+V to add more · Esc to clear` appears above the input box; with
197
+ no image it counts as an ordinary paste, inserting the clipboard text at the cursor. Probed
198
+ per platform:
113
199
 
114
- | 平台 | 图片 | 文本 |
200
+ | Platform | Image | Text |
115
201
  |---|---|---|
116
- | macOS | `osascript -e "get the clipboard as «class PNGf»"`(stdout 是 `«data PNGf<HEX>»`) | `pbpaste` |
117
- | Linux | `wl-paste --type image/png`(Wayland)→ `xclip -t image/png -o`(X11) | `wl-paste --no-newline` → `xclip -o` |
202
+ | macOS | `osascript -e "get the clipboard as «class PNGf»"` (stdout is `«data PNGf<HEX>»`) | `pbpaste` |
203
+ | Linux | `wl-paste --type image/png` (Wayland) → `xclip -t image/png -o` (X11) | `wl-paste --no-newline` → `xclip -o` |
118
204
  | Windows | PowerShell `Get-Clipboard -Format Image` → PNG → base64 | `Get-Clipboard -Raw` |
119
205
 
120
- - **全程不落盘** —— 图片以 base64 直接交给 SDK 的 `fileToContentSection()`(它的
121
- `FileSource` 本来就有 `{ base64, mime }` 变体):上传 Files API 还是内联 base64、
122
- 模型不支持视觉时怎么退化,全部由 SDK 按当前端点决定,CLI 不重复实现、也不硬编码端点。
123
- - **一条消息最多 4 张**(`MAX_PASTED_IMAGES`):一张截图内联后常 1~3MB,再多一次请求
124
- 就是几十 MB。超限、图片过大(>20MB)、剪贴板为空、没有剪贴板工具,各有独立提示。
125
-
126
- ### 🎨 交互
127
-
128
- - **图片优先、文本兑底**,顺序不能反:文本先粘进去就来不及了(草稿里已经多了一坨东西)。
129
- - `Esc` 有附件时**先清附件、草稿保留** —— 误粘一张总有退路,又不必让 `ESC` 变成
130
- 「清空一切」的随机键。
131
- - 附件提示行恒为一行,行数同时计入 `computeChatLayout`(占高)与
132
- `computeInputCursorPosition`(IME 光标 y)—— 差一行输入框就整体位移,这条有前科。
133
- - 乐观回显按「文本段 + 图片占位块」构造,与实际发送的结构一致(否则图片那几条会在
134
- 真实消息落地前闪一下不见);转录里内联图片只显示 `[图片]`,不把几十万字符的
135
- data URL 铺到屏幕上(远程 URL 才带地址)。
136
-
137
- ### 🐛 修复
138
-
139
- - **带图片的消息会丢掉 cwd 注入** —— `CwdTrackerPlugin` 原来只找 `content` 是字符串的
140
- 用户消息,换成内容块数组后会被直接跳过,那一轮模型看到的还是上一轮的工作目录。
141
- 现在追加到**最后一个 text 段**上;数组里没有 text 段时补一段。
142
- - README / getting-started 里「`PgUp`/`PgDn`(或 `Alt+V`/`Ctrl+V`)翻页」是错的:
143
- `Ctrl+V` 从未兼任翻页(`common-keys.ts` 只认 `Alt+V`),已修正。
144
-
145
- ### ✅ 测试
146
-
147
- - 新增 33 例:`clipboard.test.ts` 15(三平台命令选择 / osascript 十六进制解析 / 缺工具 /
148
- 空输出 / 超限 / CRLF 归一)、`user-content.test.ts` 6(文本段在首位 / 部分失败 /
149
- 全失败退回纯文本 / warning 透传)、`cwd-tracker-plugin.test.ts` 4(字符串 / 内容块 /
150
- 只有图片 / 不重复注入)、`pasted-images.test.tsx` 3(恒一行 / 布局多占一行 / 光标 y 同步)、
151
- `chat.test.tsx` +5(无图粘文本 / 有图出附件行 / Esc 清附件保草稿 / 发送带图 / 空剪贴板提示)。
152
- - 全量 **57 文件 / 741 例通过**,`tsc --noEmit` 0 错误。
153
- - 真产物验证(本机 Linux、无剪贴板工具):`readClipboardImage() → { kind: "none" }`、
154
- `readClipboardText() → null`,11ms 不抛错;不打桩地跑真实 SDK 转换,`vision: true` 得到
155
- `image_url` 内联块,`vision` 缺省得到文本提示 + warning。
206
+ - **Nothing ever touches the disk** — images are handed to the SDK's `fileToContentSection()`
207
+ as raw base64 (its `FileSource` already has a `{ base64, mime }` variant): whether to upload
208
+ to the Files API or inline base64, and how to degrade when the model does not support vision,
209
+ is entirely decided by the SDK per current endpoint — the CLI neither reimplements it nor
210
+ hardcodes endpoints.
211
+ - **At most 4 images per message** (`MAX_PASTED_IMAGES`): one screenshot is often 1–3MB once
212
+ inlined, and a few more in a single request easily reach tens of MB. Over the cap, an
213
+ oversized image (>20MB), an empty clipboard and a missing clipboard tool each have their own
214
+ message.
215
+
216
+ ### 🎨 Interaction
217
+
218
+ - **Images first, text as the fallback**, and the order cannot be reversed: pasting the text
219
+ first leaves no room (there is already a chunk in the draft by then).
220
+ - **`Esc` with attachments clears the attachments first and keeps the draft** — a mistaken paste
221
+ always has a way back, and `Esc` does not have to become a random key that "clears everything".
222
+ - The attachment hint row is always one line, and its row count is counted both in
223
+ `computeChatLayout` (height) and in `computeInputCursorPosition` (IME cursor y) — one row off
224
+ and the whole input box shifts; this has happened before.
225
+ - The optimistic echo is built from "text segments + image placeholder blocks", matching the
226
+ structure actually sent (otherwise the image entries would flash and vanish before the real
227
+ message lands); inline images in the transcript show only `[image]`, so a data URL of hundreds
228
+ of thousands of characters is not painted on screen (only remote URLs carry an address).
229
+
230
+ ### 🐛 Fixes
231
+
232
+ - **Messages with images lost the cwd injection** — `CwdTrackerPlugin` used to look only for
233
+ user messages whose `content` was a string, and skipped them outright once content became a
234
+ block array, so the model saw the working directory of the previous turn for that round. Now
235
+ it appends to the **last text segment**; when the array has no text segment, one is added.
236
+ - "`PgUp`/`PgDn` (or `Alt+V`/`Ctrl+V`) page through" in README / getting-started was wrong:
237
+ `Ctrl+V` never doubled as paging (`common-keys.ts` only accepts `Alt+V`), now corrected.
238
+
239
+ ### ✅ Tests
240
+
241
+ - 33 cases added: `clipboard.test.ts` 15 (three-platform command selection / osascript hex
242
+ parsing / missing tool / empty output / over the limit / CRLF normalization),
243
+ `user-content.test.ts` 6 (text segment first / partial failure / total failure falls back to
244
+ plain text / warning passthrough), `cwd-tracker-plugin.test.ts` 4 (string / content blocks /
245
+ images only / no duplicate injection), `pasted-images.test.tsx` 3 (always one line / layout
246
+ takes one extra row / cursor y stays in sync), `chat.test.tsx` +5 (paste text without images /
247
+ attachment row with images / Esc clears attachments and keeps the draft / send with images /
248
+ empty clipboard notice).
249
+ - Full run **57 files / 741 cases passing**, `tsc --noEmit` 0 errors.
250
+ - Real-artifact verification (local Linux, no clipboard tool): `readClipboardImage() → { kind: "none" }`,
251
+ `readClipboardText() → null`, 11ms without throwing; running the real SDK conversion without
252
+ stubs, `vision: true` yields an `image_url` inline block and a missing `vision` yields a text
253
+ hint + warning.
156
254
 
157
255
  ## [1.0.0-alpha.28] - 2026-10-31
158
256
 
159
- > 输入框里敲 `@` 唤起**文件路径补全** —— 补进去的是相对路径文本,模型用 `read` 工具自己去读。
160
-
161
- ### ✨ 新功能
162
-
163
- - **`@` 文件路径补全** —— 敲 `@` 弹出项目内候选,`↑`/`↓` 选择、`Tab` 或 `Enter` 补进草稿、
164
- `Esc` 只关菜单(不动草稿)。三层拆分,与仓库既有风格一致:
165
- - `tui/mention.ts`(纯逻辑):`extractMention()` 判「光标在不在一个 `@token` 里」、
166
- `rankPaths()` 子序列模糊排序、`menuWindow()` 视窗、`mentionMenuRows()` 行数;
167
- - `tui/file-index.ts`(IO):`git ls-files -co --exclude-standard` 优先(尊重 `.gitignore`、
168
- 含未跟踪文件),非 git 目录回落 `fs.glob` 并排除 `node_modules` / `.git` / `dist`;
169
- 15s 缓存、20000 条封顶、5s 超时;
170
- - `screens/chat.tsx`:按键 → 候选 → `replaceRange()` 落字的接线。
171
- - **判据落在字符事实上**:从光标往回扫到空白 / 行首,途中遇到 `@` 且其前一个是行首、空白或
172
- 分隔符才算 mention。于是 `a@b.com` 不会弹菜单;`@src/tui`、`(@src/x)`、`"@src/x"` 都会;
173
- 光标停在 token 中间(`@src|/foo.ts`)时 query 截止于光标、`end` 向后吃掉整个 token ——
174
- 补全替换的是整段,不会留下半截残字。
175
- - **排序**:贪婪子序列 + 跳跃惩罚 + 连续命中奖励;**整体前缀 > 文件名前缀**,同分时浅的 / 短的
176
- 优先。实测本仓:`@` → 182 条索引里取 50 条(1.1ms)、`@chat` 命中 19 条(0.2ms)、
177
- `@file-index` 精确命中 2 条(0.1ms)。
178
- - **目录可逐层往下钻**:选中目录补尾斜杠(`@src/tui/`),补完菜单重新过滤出该目录下内容,
179
- 于是能一路 `@src/` → `@src/tui/` → 目标文件。
180
-
181
- ### 🎨 交互
182
-
183
- - `Tab` / `Enter` = **补全不发送**(选完继续接着写,避免误发半截消息);`Esc` 只关菜单,
184
- 不清草稿(原 `Esc` 语义是清空草稿,`@` 打一半按 Esc 会连正文一起没)。
185
- - 候选可能几十条,菜单**最多占 8 行**并随高亮滚动;索引未就绪时先占一行「正在扫描项目文件…」。
186
- - 行数有**唯一真源** `mentionMenuRows()`:`computeChatLayout` 的 `hintCount`、IME 光标的 y 坐标、
187
- `MentionMenu` 实际渲染三处同源,并有用例断言 `mentionMenuRows(n) === menuWindow(n)` 的行数。
188
- - 占位提示改为「输入消息,/ 命令,@ 文件…」,让这个入口能被看见。
189
-
190
- ### 🔧 重构
191
-
192
- - `useInputEditing` 新增 `replaceRange(start, end, text)`:整段替换**并记撤销快照**,
193
- 补错了 `Ctrl+_` 能退回(用 `setInputText` 会绕过撤销栈)。
194
-
195
- ### ✅ 测试
196
-
197
- - 新增 39 例:`mention.test.ts` 22(提取 / 插入 / 排序 / 视窗 / 行数一致)、
198
- `file-index.test.ts` 9(git 清单归一 / 缓存 TTL / cwd 切换 / 兜底 / 异常收敛 / 真磁盘)、
199
- `components-mention.test.tsx` 5(8 行封顶 / 滚动跟随 / 目录尾斜杠 / 扫描占位 / 无匹配不占行)、
200
- `chat.test.tsx` +6(弹菜单 / Tab 补全不发送 / Enter 补全 / ↓ 换高亮 / Esc 只关菜单 / 无匹配照常发)、
201
- `use-input-editing.test.tsx` +3(区间替换 / 可撤销 / 越界钳制)。
202
- - 全量 **53 文件 / 708 例通过**,`tsc --noEmit` 0 错误。
257
+ > Typing `@` in the input box brings up **file path completion** — what gets inserted is the
258
+ > relative path text, and the model reads the file itself with the `read` tool.
259
+
260
+ ### ✨ New features
261
+
262
+ - **`@` file path completion** — typing `@` pops up in-project candidates, `↑`/`↓` to select,
263
+ `Tab` or `Enter` to insert into the draft, `Esc` to close the menu only (without touching the
264
+ draft). Three layers, consistent with the existing style of the repo:
265
+ - `tui/mention.ts` (pure logic): `extractMention()` decides whether the cursor is inside an
266
+ `@token`, `rankPaths()` fuzzy subsequence ranking, `menuWindow()` the viewport,
267
+ `mentionMenuRows()` the row count;
268
+ - `tui/file-index.ts` (IO): `git ls-files -co --exclude-standard` preferred (honors
269
+ `.gitignore`, includes untracked files), falling back to `fs.glob` outside a git directory
270
+ and excluding `node_modules` / `.git` / `dist`; 15s cache, 20000 entries capped, 5s timeout;
271
+ - `screens/chat.tsx`: the wiring from keybinding → candidates → `replaceRange()` insertion.
272
+ - **The criterion rests on character facts**: scan back from the cursor to whitespace / line
273
+ start, and an `@` encountered on the way counts as a mention only if it is preceded by line
274
+ start, whitespace or a separator. So `a@b.com` does not pop up the menu, while `@src/tui`,
275
+ `(@src/x)` and `"@src/x"` all do; when the cursor sits in the middle of a token
276
+ (`@src|/foo.ts`), the query ends at the cursor and `end` eats the whole token forward —
277
+ completion replaces the whole span and leaves no partial leftovers.
278
+ - **Ranking**: greedy subsequence + gap penalty + consecutive-hit bonus; **whole-span prefix >
279
+ filename prefix**, and on a tie the shallower / shorter one wins. Measured on this repo: `@`
280
+ takes 50 out of 182 indexed entries (1.1ms), `@chat` hits 19 (0.2ms), `@file-index` matches 2
281
+ exactly (0.1ms).
282
+ - **Directories can be drilled into level by level**: selecting a directory appends a trailing
283
+ slash (`@src/tui/`), and after insertion the menu refilters to that directory's contents, so
284
+ you can walk `@src/` → `@src/tui/` → the target file.
285
+
286
+ ### 🎨 Interaction
287
+
288
+ - `Tab` / `Enter` = **complete without sending** (keep writing after picking, to avoid sending
289
+ half a message by mistake); `Esc` only closes the menu and does not clear the draft (the old
290
+ `Esc` semantic was to clear the draft, so pressing Esc halfway through an `@` took the body
291
+ with it).
292
+ - Candidates can number in the dozens; the menu **takes at most 8 rows** and scrolls with the
293
+ highlight; while the index is not ready, one row of "Scanning project files…" is shown first.
294
+ - The row count has a **single source of truth** `mentionMenuRows()`: `computeChatLayout`'s
295
+ `hintCount`, the IME cursor y coordinate and `MentionMenu`'s actual rendering draw from the
296
+ same source, with a case asserting `mentionMenuRows(n) === menuWindow(n)`.
297
+ - The placeholder changed to "Type a message, / for commands, @ for files…" so this entry point
298
+ can be seen.
299
+
300
+ ### 🔧 Refactors
301
+
302
+ - `useInputEditing` gains `replaceRange(start, end, text)`: it replaces the span **and records
303
+ an undo snapshot**, so a wrong completion can be taken back with `Ctrl+_` (using
304
+ `setInputText` would bypass the undo stack).
305
+
306
+ ### ✅ Tests
307
+
308
+ - 39 cases added: `mention.test.ts` 22 (extraction / insertion / ranking / viewport / row count
309
+ consistency), `file-index.test.ts` 9 (git listing normalization / cache TTL / cwd switch /
310
+ fallback / error convergence / real disk), `components-mention.test.tsx` 5 (8-row cap /
311
+ scrolling follows / directory trailing slash / scanning placeholder / no match takes no row),
312
+ `chat.test.tsx` +6 (menu pops up / Tab completes without sending / Enter completes / ↓ changes
313
+ the highlight / Esc only closes the menu / no match still sends), `use-input-editing.test.tsx`
314
+ +3 (range replacement / undoable / out-of-range clamping).
315
+ - Full run **53 files / 708 cases passing**, `tsc --noEmit` 0 errors.
203
316
 
204
317
  ## [1.0.0-alpha.27] - 2026-10-29
205
318
 
206
- > SDK 的警告与错误现在会进通知栏——**通用通道**,不为某个功能单独接线。
207
-
208
- ### ✨ 新功能
209
-
210
- - **SDK 日志 → 通知栏**(`sdk-log.ts` 从「只落盘」改成 tee)—— `installSdkLogger()`
211
- 过去把 SDK 日志**整体接管到文件**(不然 `console.log` 会撕坏 Ink 的帧),代价是
212
- 「SDK 说了什么」在界面上一个字都看不见,只能事后翻 `~/.ai-zen/logs/sdk.log`。
213
- 现在 `warn` / `error` 除了落盘,还会转给通知栏:
214
- - **晚绑定**:`installSdkLogger()` 跑在 Ink 渲染之前,而通知栏活在 `Chat` 的 React
215
- state 里。`Chat` 挂载时注册 sink;注册前攒最近 5 条、注册时补推 —— 启动期的
216
- 问题恰恰最该看见。
217
- - **去重**:同一句话反复刷(例如每轮都失败一次)只上栏一次。
218
- - **封顶 20 条**:到顶后补一句 `More SDK warnings suppressed — see <日志路径>`,
219
- 之后不再刷。SDK 的日志量按「每次请求 / 每次刷新」计,不封顶会把栏位和 React
220
- 状态一起冲垮。
221
- - `info` 只进文件 —— SDK 打 info 很密(mcp manifest、工具发现),上栏等于刷屏。
222
- - 以后 SDK 任何新特性只要打 `warn` / `error`,界面上自动就有,**不必再为它传回调**。
223
- 这条通道只用既有的 `setLogger`,与 SDK 版本解耦。
224
- - **通知栏新增 `warn` 档** —— `NoticeTone` 由 `info | error` 扩为 `info | warn | error`,
225
- `NoticeBar` 用 `theme.warn` 着色。警告既不该降级成普通提示(会漏看),也不该冒充
226
- 错误(会误导)。
227
-
228
- ### 🔧 依赖
229
-
230
- - `@ai-zen/agents-sdk` `1.0.0-alpha.23` → `1.0.0-alpha.24`:`tools/` 目录的加载改为
231
- fail-safe —— 无 `export` 的文件不再被求值、求值期抑制 `process.exit` / `abort`
232
- 并把 `console` 输出改指 stderr、单文件 import 加超时。
233
-
234
- 这修的是一次真实事故:一个无参数时 `process.exit(2)` 的开发脚本放进
235
- `.ai-zen/tools/` 后,SDK 在建会话途中 `import()` 了它,于是**每次启动都在初始化
236
- 途中把 CLI 杀掉**,屏幕上只剩最后一帧,看着像「卡住不启动」;同目录另几个脚本还在
237
- 每次启动时静默重写项目里的 PNG 资产。现在这类文件会被跳过(或被抑制),并通过上面
238
- 那条通道在通知栏报出来。
239
-
240
- ### ✅ 测试
241
-
242
- - 新增 `src/sdk-log-notice.test.ts` 6 例:`warn`/`error` 上栏、`info` 不上栏、
243
- 启动期缓冲补推、去重、封顶收尾、静默模式不上栏。
244
- - 全量 **50 文件 / 663 例通过**(alpha.26 时 49 文件 / 657 例),`tsc --noEmit` 0 错误。
319
+ > SDK warnings and errors now go to the notice bar — a **generic channel**, not wired up
320
+ > separately for each feature.
321
+
322
+ ### ✨ New features
323
+
324
+ - **SDK logs → notice bar** (`sdk-log.ts` goes from "write to disk only" to a tee) —
325
+ `installSdkLogger()` used to **take over SDK logs wholesale into a file** (otherwise
326
+ `console.log` would tear up Ink's frames), at the cost that a single word of "what the SDK
327
+ said" was invisible in the UI and could only be found afterwards in `~/.ai-zen/logs/sdk.log`.
328
+ Now `warn` / `error` are also forwarded to the notice bar besides being written to disk:
329
+ - **Late binding**: `installSdkLogger()` runs before Ink renders, while the notice bar lives
330
+ in `Chat`'s React state. `Chat` registers a sink on mount; the latest 5 messages are
331
+ buffered before registration and pushed on registration — startup problems are exactly what
332
+ most needs to be seen.
333
+ - **Dedup**: the same sentence repeating (for example failing once per turn) reaches the bar
334
+ only once.
335
+ - **Capped at 20 rows**: after the cap, one extra line `More SDK warnings suppressed — see <log path>`
336
+ is appended and nothing more is pushed. SDK log volume is counted per "request / refresh",
337
+ and without a cap the bar and the React state would be flooded together.
338
+ - `info` goes to the file only — the SDK logs info densely (mcp manifest, tool discovery), and
339
+ putting it on the bar is spam.
340
+ - From now on, any new SDK feature that logs `warn` / `error` is automatically visible in the
341
+ UI, **no callback needs to be passed for it**. This channel only uses the existing
342
+ `setLogger` and is decoupled from the SDK version.
343
+ - **The notice bar gains a `warn` tone** — `NoticeTone` expands from `info | error` to
344
+ `info | warn | error`, and `NoticeBar` colors it with `theme.warn`. A warning should neither be
345
+ downgraded to an ordinary notice (it would be missed) nor pass itself off as an error (it
346
+ would mislead).
347
+
348
+ ### 🔧 Dependencies
349
+
350
+ - `@ai-zen/agents-sdk` `1.0.0-alpha.23` → `1.0.0-alpha.24`: loading of the `tools/` directory
351
+ became fail-safe — files without `export` are no longer evaluated, `process.exit` / `abort`
352
+ are suppressed during evaluation while `console` output is redirected to stderr, and importing
353
+ a single file now has a timeout.
354
+
355
+ This fixes a real incident: a dev script that called `process.exit(2)` when run without
356
+ arguments was placed in `.ai-zen/tools/`, the SDK `import()`-ed it midway through creating a
357
+ session, and so **every startup killed the CLI during initialization**, leaving only the last
358
+ frame on screen and looking like "stuck on startup"; several other scripts in the same
359
+ directory silently rewrote PNG assets in the project on every startup. Such files are now
360
+ skipped (or suppressed) and reported in the notice bar through the channel above.
361
+
362
+ ### ✅ Tests
363
+
364
+ - 6 cases added in `src/sdk-log-notice.test.ts`: `warn`/`error` reach the bar, `info` does not,
365
+ buffered startup messages are pushed later, dedup, cap wrap-up, silent mode does not touch the
366
+ bar.
367
+ - Full run **50 files / 663 cases passing** (49 files / 657 cases at alpha.26), `tsc --noEmit`
368
+ 0 errors.
245
369
 
246
370
  ## [1.0.0-alpha.26] - 2026-10-29
247
371
 
248
- > `/update` 从「一个转圈 + 一句结果」改成**每一步都说清在干什么**,
249
- > 并补上了此前名不副实的「重启生效」——现在是真的重启进程。
250
-
251
- ### ✨ 新功能
252
-
253
- - **装完可以一键重启** —— 装完弹确认框「已装好 vX,重启后才会生效…现在重启?」,
254
- 默认高亮「是」(`/migrate` 用的同一套 `Confirm` 浮层,回车即走);确认后先落盘当前
255
- 对话再重启,新进程按常规启动即**续接上一轮会话**。拒绝也不拦人,照旧给「重启 ai 后
256
- 生效」的提示。
257
- - **过程信息不再缺席** —— 依次报出「正在查询最新版本…」→「发现 vX(当前 vY),
258
- 开始安装…」→「✅ 已安装 …」;移交终端给 npm 之前,先在真实 TTY 上打一段头部:
259
- 装什么包、当前版本、`npm root -g` 得到的安装位置、以及实际执行的命令。
260
-
261
- ### 🐛 修复
262
-
263
- - **「重启生效」原本是空头支票** —— 旧实现只 `push` 一句「重启 ai 后生效」,
264
- `/update` 自己也从不重启。而 `exit({ restart: true })` 那条路(`/model`、`/lang` 在用)
265
- 只是同进程重挂载 `<Chat/>`,内存里还是旧模块图,对「磁盘上的代码变了」这种升级
266
- 等于没装。现在改为**真·重新 exec 自己**:`spawn` 新进程跑新 dist,父进程在挂起态
267
- 等它退出后跟着退。
268
- - **`/update` 期间的转圈是「冻住」的** —— `readDistTags()` 用的是 `spawnSync`,
269
- 同步阻塞事件循环,Ink 的动画定时器跟着停摆 1–3 秒。改为异步 `spawn`,并加 20 秒
270
- 超时兜底(断网时不会再无限期挂着)。
271
- - **转圈上的文案是「处理中…」** —— 这个文案来自生成阶段 `phaseLabel(session.phase)`,
272
- 而 `/update` 根本没有生成轮次,`phase` 为 `null` 就兜底成了通用的「处理中…」,
273
- 与正在发生的事毫无关系。新增 `busySpinnerLabel()`:有生成阶段仍由 SDK 阶段驱动,
274
- 阻塞型命令期间改显 `/update 执行中…`。
275
-
276
- ### 🔧 改进
277
-
278
- - **查询与安装都加 `--prefer-online`** —— npm 会缓存 packument,刚发版时
279
- `npm view` 可能仍拿到旧 dist-tags,把「有新版」误判成「已是最新」(发布当天踩过);
280
- 安装侧同理。另加 `--no-fund` 减噪。
281
-
282
- ### ⚠️ 两个容易踩的终端细节(写在代码注释里)
283
-
284
- - **重启必须在挂起态里退出**:`suspendTerminal` 的 `beginSuspend()` 关 raw 模式,
285
- `endSuspend()` 会把它重新打开;若等 `suspendTerminal` 返回后再 `process.exit`,
286
- 终端就停在 raw 模式,回到 shell 后按键全是乱码。
287
- - **重启前要先 `disableMouseTracking()`**:鼠标上报由 `mount()` 按 Ink 实例开关,
288
- 而挂起期间实例并未退出 —— 不关的话,子进程若异常死亡,终端会一直把滚轮写成
289
- 转义序列。
290
-
291
- ### 📖 文档
292
-
293
- - `README.md` / `README.zh.md` / `docs/{zh,en}/getting-started.md` —— `/update` 的描述
294
- 订正:原文写「全局安装 alpha 通道」,实际是在**全部** dist-tags 里挑 semver 最大者
295
- (所以 alpha 比 `latest` 新时会装 alpha),并补上「逐步骤显示进度、装完可一键重启」。
296
-
297
- ### ✅ 测试
298
-
299
- - 全量 **49 文件 / 657 例通过**(alpha.25 时 649 例),`tsc --noEmit` 0 错误。
300
- - `/update` 用例 6 → 11 例:新增「确认后重启 + 重启前落盘」「拒绝重启则不落盘也不重启」
301
- 「新会话无用户消息不落盘」「三条过程通知按序上报」「终端头部含版本/位置/命令」
302
- 「查询带 `--prefer-online`」。
303
- - 新增 `busySpinnerLabel` 3 例,含回归守卫:命令执行期间**不得**回落成「处理中…」。
372
+ > `/update` changes from "one spinner + one result line" to **saying what it is doing at every
373
+ > step**, and fills in the previously nominal "takes effect after restart" — it now really
374
+ > restarts the process.
375
+
376
+ ### ✨ New features
377
+
378
+ - **One key to restart after install** — a confirmation box pops up after install: "vX is
379
+ installed, it takes effect after a restart… restart now?", with "Yes" highlighted by default
380
+ (the same `Confirm` overlay used by `/migrate`, Enter goes ahead); after confirming, the
381
+ current conversation is saved first and then the process restarts, and the new process
382
+ **resumes the previous session** on a normal start. Declining blocks nothing and still gives
383
+ the "takes effect after restarting ai" hint.
384
+ - **Progress information is no longer missing** — it reports in order "Querying the latest
385
+ version…" → "Found vX (current vY), installing…" → "✅ Installed …"; before handing the
386
+ terminal over to npm, it prints a header on the real TTY: which package is being installed,
387
+ the current version, the install location from `npm root -g`, and the command actually
388
+ executed.
389
+
390
+ ### 🐛 Fixes
391
+
392
+ - **"Takes effect after restart" was an empty promise** — the old implementation only `push`-ed
393
+ one "takes effect after restarting ai" line, and `/update` itself never restarted. And the
394
+ `exit({ restart: true })` path (used by `/model`, `/lang`) merely remounted `<Chat/>` in the
395
+ same process, so the in-memory module graph stayed old, which for an upgrade like "the code on
396
+ disk changed" amounts to not installing at all. It now **really re-execs itself**: `spawn` a
397
+ new process running the new dist, and the parent waits in a suspended state and exits after
398
+ the child exits.
399
+ - **The spinner during `/update` was "frozen"** — `readDistTags()` used `spawnSync`, blocking
400
+ the event loop synchronously, so Ink's animation timer stalled for 1–3 seconds. Changed to an
401
+ async `spawn` with a 20 second timeout (no more hanging indefinitely when offline).
402
+ - **The spinner label was "Processing…"** — that label comes from `phaseLabel(session.phase)` in
403
+ the generation phase, but `/update` has no generation turn at all, so `phase` was `null` and
404
+ the fallback became the generic "Processing…", having nothing to do with what was happening.
405
+ Added `busySpinnerLabel()`: with a generation phase it is still driven by the SDK phase, and
406
+ during blocking commands it shows `/update running…` instead.
407
+
408
+ ### 🔧 Improvements
409
+
410
+ - **`--prefer-online` on both the query and the install** — npm caches the packument, so right
411
+ after a release `npm view` may still return old dist-tags and mistake "there is a new version"
412
+ for "already the latest" (this happened on release day); the install side likewise. Also added
413
+ `--no-fund` to cut noise.
414
+
415
+ ### ⚠️ Two terminal gotchas (written in code comments)
416
+
417
+ - **The restart must exit inside the suspended state**: `beginSuspend()` in `suspendTerminal`
418
+ turns raw mode off, and `endSuspend()` turns it back on; if `process.exit` is called after
419
+ `suspendTerminal` returns, the terminal stays in raw mode and every key is garbage after
420
+ returning to the shell.
421
+ - **`disableMouseTracking()` must be called before the restart**: mouse reporting is toggled by
422
+ `mount()` per Ink instance, and the instance has not exited during suspension — without
423
+ turning it off, if the child process dies abnormally, the terminal keeps writing wheel events
424
+ as escape sequences.
425
+
426
+ ### 📖 Docs
427
+
428
+ - `README.md` / `README.zh.md` / `docs/{zh,en}/getting-started.md` — the `/update` description is
429
+ corrected: the original said "installs the alpha channel globally", while it actually picks
430
+ the highest semver among **all** dist-tags (so alpha is installed when it is newer than
431
+ `latest`), and "shows progress step by step, one-key restart after install" was added.
432
+
433
+ ### ✅ Tests
434
+
435
+ - Full run **49 files / 657 cases passing** (649 cases at alpha.25), `tsc --noEmit` 0 errors.
436
+ - `/update` cases went from 6 to 11: added "restart after confirmation + save before restart",
437
+ "declining the restart neither saves nor restarts", "a new session with no user messages is
438
+ not saved", "three progress notices reported in order", "the terminal header contains
439
+ version/location/command", "the query carries `--prefer-online`".
440
+ - 3 cases added for `busySpinnerLabel`, including a regression guard: the label **must not** fall
441
+ back to "Processing…" while a command is running.
304
442
 
305
443
  ## [1.0.0-alpha.25] - 2026-10-29
306
444
 
307
- > 升级到 SDK `1.0.0-alpha.23`(面向模型与用户的文案全面英文化),
308
- > 并补上缺失的 `LICENSE` 文件。
309
-
310
- ### 🔧 依赖
445
+ > Upgraded to SDK `1.0.0-alpha.23` (model-facing and user-facing copy is now fully in English),
446
+ > and added the missing `LICENSE` file.
311
447
 
312
- - `@ai-zen/agents-sdk` `1.0.0-alpha.22` → `1.0.0-alpha.23`。
448
+ ### 🔧 Dependencies
313
449
 
314
- ### 💬 行为变化(来自 SDK)
450
+ - `@ai-zen/agents-sdk` `1.0.0-alpha.22` → `1.0.0-alpha.23`.
315
451
 
316
- - **SDK 内置文案全部改为英文** —— 工具 schema 描述、出厂系统提示词、注入语、
317
- 工具返回值、错误信息、出厂 Skill 与 catalog 数据一律固定英文,SDK 内不再有
318
- 任何中文文案,也**不引入 i18n**(理由:`tokenThresholds` / `maxToolOutput`
319
- 都是绝对量,工具 schema 换语言就是另一组 token 数,性能基准会失去单一变量)。
320
- - **回复语言改为「跟随用户」** —— 出厂系统提示词里「请用中文回复」被替换为
321
- `Always reply in the same language the user writes in.`,按每条消息自适应。
322
- - **对 CLI 无破坏性影响** —— CLI 自带 `i18n` 字典(`src/i18n`),界面文案与
323
- 通知全部由 CLI 自己渲染,不解析 SDK 的文案字符串;对 SDK 的用法仅限于类型导入
324
- 与 `AgentRepository` / `ConfigManager` / `setLogger` / `HandoffService` 等结构性
325
- API。49 文件 / 649 例测试在升级前后逐项一致。
452
+ ### 💬 Behavior changes (from SDK)
326
453
 
327
- ### 📄 许可证
454
+ - **All built-in SDK copy is now English** — tool schema descriptions, the factory system prompt,
455
+ injected wording, tool return values, error messages, factory Skills and catalog data are all
456
+ fixed English; the SDK no longer contains any Chinese copy, and **no i18n is introduced**
457
+ (reason: `tokenThresholds` / `maxToolOutput` are absolute quantities, and changing the language
458
+ of tool schemas means a different set of token counts, which would remove the single variable
459
+ from performance benchmarks).
460
+ - **Reply language becomes "follow the user"** — "please reply in Chinese" in the factory system
461
+ prompt is replaced by `Always reply in the same language the user writes in.`, adapting per
462
+ message.
463
+ - **No breaking impact on the CLI** — the CLI ships its own `i18n` dictionary (`src/i18n`), and
464
+ all UI copy and notices are rendered by the CLI itself without parsing SDK copy strings; SDK
465
+ usage is limited to type imports and structural APIs such as `AgentRepository` /
466
+ `ConfigManager` / `setLogger` / `HandoffService`. The 49 files / 649 test cases are identical
467
+ item by item before and after the upgrade.
328
468
 
329
- - **新增 `LICENSE`** —— 此前 `package.json` 声明了 `"license": "MIT"`,但仓库里
330
- 没有许可证正文,npm 包内自然也没有。现补上 MIT 全文(与 `agents` 仓库根
331
- `LICENSE` 一致,`Copyright (c) 2024 LZQCN`)。
332
- `files` 字段未改动:npm 对 `LICENSE` 强制包含,`npm pack` 已确认 tarball 内含该文件。
469
+ ### 📄 License
333
470
 
334
- ### 📖 文档
471
+ - **Added `LICENSE`** — `package.json` declared `"license": "MIT"` before, but the repository
472
+ had no license text, and naturally the npm package did not either. The full MIT text is added
473
+ now (identical to `LICENSE` at the root of the `agents` repository,
474
+ `Copyright (c) 2024 LZQCN`). The `files` field is unchanged: npm includes `LICENSE` forcibly,
475
+ and `npm pack` confirmed the tarball contains the file.
335
476
 
336
- - `docs/{zh,en}/getting-started.md` —— 运行时依赖清单的 SDK 版本从 `1.0.0-alpha.18`
337
- 订正为 `1.0.0-alpha.23`,`@ai-zen/agents-core` 从 `^4.5.2` 订正为 `^4.5.3`
338
- (与实际 `package.json` 对齐)。
339
- - `docs/{zh,en}/index.md` —— 许可证一节改为指向仓库根的 `LICENSE` 文件,并说明全文
340
- 随包一同发布。
341
- - `README.md` / `README.zh.md` —— 启动界面示例中的版本号同步为 `1.0.0-alpha.25`。
477
+ ### 📖 Docs
342
478
 
343
- ### ✅ 测试
479
+ - `docs/{zh,en}/getting-started.md` — the SDK version in the runtime dependency list is corrected
480
+ from `1.0.0-alpha.18` to `1.0.0-alpha.23`, and `@ai-zen/agents-core` from `^4.5.2` to `^4.5.3`
481
+ (aligned with the actual `package.json`).
482
+ - `docs/{zh,en}/index.md` — the license section now points to the `LICENSE` file at the
483
+ repository root and states that the full text ships with the package.
484
+ - `README.md` / `README.zh.md` — the version number in the startup screen example is synced to
485
+ `1.0.0-alpha.25`.
344
486
 
345
- - 全量 **49 文件 / 649 例通过**,`tsc --noEmit` 0 错误,与 alpha.24 基线一致
346
- (本次仅升依赖,无源码改动)。
487
+ ### ✅ Tests
347
488
 
489
+ - Full run **49 files / 649 cases passing**, `tsc --noEmit` 0 errors, consistent with the alpha.24
490
+ baseline (this release only bumps dependencies, no source changes).
348
491
  ## [1.0.0-alpha.24] - 2026-10-28
349
492
 
350
- > 三件事:**看不见的上下文**现在看得见了(不进上下文的消息划删除线),
351
- > 升级有了 `/update`,中断策略从写死的常量变成可配置项。
352
-
353
- ### ✨ 新功能
354
-
355
- - **不进上下文的消息划删除线** —— 判据与 core 的 `formatHistory()` 同源
356
- (`status == undefined || Completed` 且非 `omit`),而策略早已在 `Agent.run()` 收尾时
357
- 结算进字段(`keep`→`Completed`、`drop`→`Aborted`,工具轮次 `fill`→`Completed`、
358
- `drop`→`omit`),渲染层不重复推导策略。
359
- - 流式中那条(`Pending` / `Writing`)不划线:它在 `formatHistory` 里同样不在,但那是
360
- 「尚未」而不是「被丢弃」。
361
- - `hidden` 仍彻底不渲染;`omit` 不再让消息凭空消失(原先 `hidden || omit` 一起
362
- `return []`,用户中断一轮工具调用后那几条会无声蒸发)。
363
- - **`/update`** —— 装 registry 上最新的发布版:读 `npm view @ai-zen/cli dist-tags --json`,
364
- 取**所有 tag 里 semver 最大**的那个。只认 `latest` 会把跑 alpha 的用户降级(实际发布
365
- 状态就是 `latest 0.9.0 < alpha 1.0.0-alpha.23`);目标不比当前进程新则不装(本地开发版
366
- 不会被「升级」降级)。安装走 `suspendTerminal` + `stdio: inherit`,npm 的进度与报错直接
367
- 打在真实终端上;装完读 `npm root -g` 下的 `package.json` 判定版本(`npm view` 查 registry
368
- 有几分钟延迟),并提示重启(内存里还是旧代码,不自动重启以免丢会话)。
369
- - **中断策略可配置** —— SDK 升级到 `1.0.0-alpha.22`:`AppConfig` 新增
370
- `abortedMessageStrategy`(`keep` / `drop`,出厂 `keep`)与 `abortedToolRoundStrategy`
371
- (`fill` / `drop`,出厂 `fill`),`createAgent()` 不传参数时直接读 `scope.config`。
372
- 配置中心「默认项」新增两项即可切换;它们只在**建 Agent** 时读取,所以关掉配置中心时
373
- 宿主会自动重建会话(`WizardCloseResult.sessionOptionsChanged`),不必再手敲 `/reload`。
374
-
375
- ### ♻️ 重构
376
-
377
- - **CLI 不再写死 `abortedMessageStrategy: "keep"`** —— 写死会盖掉用户的选择。现在配置即
378
- 事实:`config.json` 里写什么就用什么,`agent-creator` 只负责建 Agent。
379
- - **配置迁移补齐策略字段**(SDK 侧)—— 缺失或非法值一律写回出厂默认并落盘一次,各端不必
380
- 各自 `?? "keep"` 兜底。
381
- - **新增 `src/version-compare.ts`** —— semver 比较(核心段 / 预发布标识 / 忽略 build),
382
- 供 `/update` 从多个 dist-tag 里挑最新;不为此引 `semver` 依赖。
383
-
384
- ### ✅ 测试
385
-
386
- - 全量 **49 文件 / 649 例通过**(alpha.23 时 626 例)。
387
- - 新增:`version-compare.test.ts` 10 例、`chat-commands-update.test.ts` 6 例(tag 选择、
388
- 已是最新不安装、安装失败、npm 缺失、dist-tags 查不到),`messageToLines` 删除线 3 例,
389
- 策略 setter / 向导两屏 / 宿主重载 4 例。
390
- - 端到端实测(临时 `AI_ZEN_DIR`):出厂默认 `config: keep fill → agent: keep fill`,
391
- 改成 drop 并重建后 `config: drop drop → agent: drop drop`。
392
-
393
- ### 🔧 依赖
394
-
395
- - `@ai-zen/agents-sdk` `1.0.0-alpha.21` → `1.0.0-alpha.22`。
493
+ > Three things: **invisible context** is now visible (messages that never enter the context are struck through),
494
+ > upgrades have `/update`, and the abort strategy went from a hardcoded constant to a configurable option.
495
+
496
+ ### ✨ New features
497
+
498
+ - **Messages that never enter the context are struck through** — the predicate shares its source with core's
499
+ `formatHistory()` (`status == undefined || Completed` and not `omit`), while the strategy was already settled into the
500
+ field when `Agent.run()` wraps up (`keep`→`Completed`, `drop`→`Aborted`; tool rounds `fill`→`Completed`,
501
+ `drop`→`omit`), so the render layer does not re-derive the strategy.
502
+ - The streaming one (`Pending` / `Writing`) is not struck through: it is absent from `formatHistory` as well, but that
503
+ means "not yet" rather than "dropped".
504
+ - `hidden` still does not render at all; `omit` no longer makes messages vanish into thin air (previously `hidden || omit`
505
+ both `return []`, so after a user aborted a round of tool calls those messages evaporated silently).
506
+ - **`/update`** — installs the newest published version from the registry: reads
507
+ `npm view @ai-zen/cli dist-tags --json` and takes the one with **the highest semver across all tags**. Honoring only
508
+ `latest` would downgrade users running alpha builds (the actual published state is
509
+ `latest 0.9.0 < alpha 1.0.0-alpha.23`); if the target is not newer than the current process, nothing is installed
510
+ (a local dev build is not "upgraded" into a downgrade). Installation goes through `suspendTerminal` +
511
+ `stdio: inherit`, so npm progress and errors land directly on the real terminal; afterwards the version is read from
512
+ the `package.json` under `npm root -g` (`npm view` against the registry lags by a few minutes), and a restart is
513
+ suggested (old code is still in memory; no automatic restart, to avoid losing the session).
514
+ - **Configurable abort strategy** — SDK upgraded to `1.0.0-alpha.22`: `AppConfig` gains
515
+ `abortedMessageStrategy` (`keep` / `drop`, factory `keep`) and `abortedToolRoundStrategy`
516
+ (`fill` / `drop`, factory `fill`); `createAgent()` reads `scope.config` directly when no arguments are passed.
517
+ Two new items under the configuration center's "Defaults" are enough to switch them; they are read only **when an
518
+ Agent is created**, so closing the configuration center makes the host rebuild the session automatically
519
+ (`WizardCloseResult.sessionOptionsChanged`), with no need to type `/reload` by hand.
520
+
521
+ ### ♻️ Refactors
522
+
523
+ - **CLI no longer hardcodes `abortedMessageStrategy: "keep"`** — hardcoding overrides the user's choice. Configuration
524
+ is now the source of truth: whatever `config.json` says is used, and `agent-creator` only creates the Agent.
525
+ - **Config migration backfills the strategy fields** (SDK side) — missing or invalid values are always written back as
526
+ the factory default and persisted once, so no client needs its own `?? "keep"` fallback.
527
+ - **New `src/version-compare.ts`** — semver comparison (core segments / prerelease identifiers / build ignored),
528
+ used by `/update` to pick the newest among multiple dist-tags; no `semver` dependency is pulled in for it.
529
+
530
+ ### ✅ Tests
531
+
532
+ - Full run: **49 files / 649 cases passed** (626 cases at alpha.23).
533
+ - New: `version-compare.test.ts` 10 cases, `chat-commands-update.test.ts` 6 cases (tag selection, already newest so
534
+ no install, install failure, npm missing, dist-tags unavailable), `messageToLines` strikethrough 3 cases,
535
+ strategy setters / two wizard screens / host reload 4 cases.
536
+ - End-to-end check (temporary `AI_ZEN_DIR`): factory default `config: keep fill → agent: keep fill`; after switching
537
+ to drop and rebuilding, `config: drop drop → agent: drop drop`.
538
+
539
+ ### 🔧 Dependencies
540
+
541
+ - `@ai-zen/agents-sdk` `1.0.0-alpha.21` → `1.0.0-alpha.22`.
396
542
 
397
543
  ## [1.0.0-alpha.23] - 2026-10-28
398
544
 
399
- > 子 Agent 从「通知栏里报一句运行中」升级为一条**可切换的栏目**:主 / 子 Agent
400
- > 各自的消息分栏展示,执行状态内联在标签上。
401
-
402
- ### ✨ 新功能
403
-
404
- - **主子 Agent 切换栏目** —— 对话中出现子 Agent 时,消息列表下方多出一条栏目
405
- `主AGENT ◈子AGENT1 ◈子AGENT2`,当前项背景色高亮;`←` / `→` 切换后,消息列表
406
- 即换成该 Agent 自己的对话。
407
- - **数据源**:core 的 `onSubAgentStart` 钩子给出子 Agent 实例;`AgentTool` /
408
- `AgentToolLazy` 在委派结束后**不销毁**实例,`messages` 始终可读——所以只要存下
409
- 引用,就能回看子 Agent 的完整过程(思考、工具调用、多轮推理),而不只是最终结果。
410
- - **生命周期与内循环对齐**:子 Agent 是内循环里工具调用的产物,本轮内循环全部结束时
411
- (`send` 收尾;异常 / 中止走 `finally`,同样清)整条栏目清空,展示序号归零。
412
- - **执行状态内联**:`◈` + 颜色(运行中蓝、完成绿、失败红),运行中的蓝点每 500ms
413
- 在亮 / 暗之间闪烁。**不再**往通知栏推「子任务运行中 / 已完成」——那会让通知栏
414
- 凭空多出一行,整帧高度跟着跳。
415
- - **`←` / `→` 按状态分流**:输入框为空(光标停在 0,左右键本无事可做)或生成中
416
- (输入框已被 Spinner 顶替)时切换栏目;输入框有内容时仍是移光标,不牺牲任何编辑能力。
417
- - 发送消息时自动切回主 Agent 视图(否则用户看不到自己的提问)。
418
-
419
- ### 🐛 修复
420
-
421
- - **`/new`、`/reload` 后子 Agent 残留** —— 这两个操作只换 `agent`、不重建 `ChatSession`
422
- 实例,子 Agent 视图不会自然消失;现随 `reset` / `reload` / `migrate` 显式清空。
423
- - **视图 id 与展示序号混用** —— 序号每轮归零,若 id 也跟着归零,下一轮的 `sub-1` 会与
424
- 上一轮同名,而选中态按 id 匹配,残留的选中 id 会「接上」新出现的子 Agent。现拆成
425
- 两个计数器:id 永久递增、只让展示序号归零。
426
-
427
- ### ♻️ 重构
428
-
429
- - **修正 `sub-agent-guard-plugin` 的过时注释** —— 原注释称 `call_skill_sub_agent`
430
- 「不分发 onSubAgentStart、暂不覆盖」,但 skill 子 Agent 统一到 `AgentToolLazy` 之后
431
- 它其实是被覆盖的(core 只有两条委派路径,均分发该钩子)。这条错注释在本次实现
432
- 前期直接误导了对覆盖面的判断。
433
-
434
- ### ✅ 测试
435
-
436
- - 全量 **47 文件 / 626 例通过**(发布 alpha.22 时 604 例)。
437
- - 新增护栏:栏目条目顺序与 1 空格 padding、符号 `◈` 与三色映射、超宽仍单行、
438
- 按序编号、结束转 `done`、出错转 `failed` 且不被 `end` 覆盖、进度不进通知栏、
439
- 执行期间有内容而收尾后清空、`reset` / `reload` 也清空、`←`/`→` 切换数据源与
440
- 输入非空时不切换、布局高度与 IME 偏移。
441
- - 破坏性验证:去掉 `computeChatLayout` 里的 `agentTabLines` → 布局用例红
442
- (`expected 4 to be 5`);命令说明写长 → 宽度护栏红(`expected 62 to be <= 36`)。
545
+ > Sub-agents went from "one line in the notice bar saying it is running" to a **switchable tab row**: main and
546
+ > sub-agent messages are shown in separate columns, with execution status inlined on the labels.
547
+
548
+ ### ✨ New features
549
+
550
+ - **Main/sub-agent tab row** — when a sub-agent appears in the conversation, a tab row appears below the message
551
+ list, `MAIN AGENT ◈SUB AGENT1 ◈SUB AGENT2`, with the current item highlighted by background color; after
552
+ switching with `←` / `→`, the message list is replaced by that Agent's own conversation.
553
+ - **Data source**: core's `onSubAgentStart` hook hands over the sub-agent instance; `AgentTool` /
554
+ `AgentToolLazy` **do not destroy** the instance after delegation ends, so `messages` is always readable — keeping
555
+ a reference is enough to review the sub-agent's whole process (reasoning, tool calls, multi-round inference),
556
+ not just the final result.
557
+ - **Lifecycle aligned with the inner loop**: a sub-agent is a product of a tool call inside the inner loop; when
558
+ this round's inner loop ends entirely (`send` wraps up; exceptions / aborts go through `finally` and clear as
559
+ well) the whole tab row is emptied and the display index resets to zero.
560
+ - **Inline execution status**: `◈` + color (running blue, done green, failed red); the running blue dot blinks
561
+ between bright and dim every 500ms. It **no longer** pushes "sub-task running / done" into the notice bar —
562
+ that would add a row out of nowhere and make the whole frame height jump.
563
+ - **`←` / `→` routed by state**: switches tabs when the input line is empty (cursor at 0, where left/right have
564
+ nothing to do) or while generating (the input line has been replaced by the spinner); when the input line has
565
+ content the keys still move the cursor, sacrificing no editing capability.
566
+ - Sending a message switches back to the main Agent view automatically (otherwise the user cannot see their own
567
+ question).
568
+
569
+ ### 🐛 Fixes
570
+
571
+ - **Sub-agents left over after `/new` and `/reload`** — those two operations only swap `agent` and do not rebuild
572
+ the `ChatSession` instance, so the sub-agent view does not disappear on its own; it is now cleared explicitly on
573
+ `reset` / `reload` / `migrate`.
574
+ - **View id and display index mixed up** — the index resets every round; if the id reset too, next round's `sub-1`
575
+ would share a name with the previous round's, and since selection is matched by id, a leftover selected id would
576
+ "latch onto" a newly appeared sub-agent. They are now two counters: the id increments forever, and only the
577
+ display index resets.
578
+
579
+ ### ♻️ Refactors
580
+
581
+ - **Fixed the outdated comment in `sub-agent-guard-plugin`** — the original comment claimed `call_skill_sub_agent`
582
+ "does not dispatch onSubAgentStart, not covered yet", but after skill sub-agents were unified onto `AgentToolLazy`
583
+ it in fact is covered (core has only two delegation paths, both dispatch the hook). That wrong comment directly
584
+ misled the assessment of the covered surface early in this implementation.
585
+
586
+ ### ✅ Tests
587
+
588
+ - Full run: **47 files / 626 cases passed** (604 cases when alpha.22 was released).
589
+ - New guards: tab entry order and 1-space padding, the `◈` symbol and the three-color mapping, still a single line
590
+ when over-wide, sequential numbering, switching to `done` at the end, switching to `failed` on error without being
591
+ overwritten by `end`, progress kept out of the notice bar, content present during execution but cleared after
592
+ wrapping up, cleared on `reset` / `reload` too, `←`/`→` switching the data source and not switching when the input
593
+ is non-empty, layout height and IME offset.
594
+ - Break checks: removing `agentTabLines` from `computeChatLayout` → layout case red
595
+ (`expected 4 to be 5`); making a command description longer → width guard red (`expected 62 to be <= 36`).
443
596
 
444
597
  ## [1.0.0-alpha.22] - 2026-10-28
445
598
 
446
- > 这一版是**整体代码评审 + 治本修复**的成果:先通读 `src/` 全部非测试代码做全量评审
447
- > (P0~P3 共 30 项,其中 3 项经实测查证为非问题、未改代码),再按「能查根因就查、
448
- > 不加兜底/不吞异常/不做多余校验」的原则逐项落地,最后补上 Ctrl+C 中断交接。
599
+ > This release is the result of a **full code review plus root-cause fixes**: first a complete review of all
600
+ > non-test code in `src/` (30 items P0~P3, 3 of them verified by experiment to be non-issues and left unchanged),
601
+ > then each item landed on the principle "trace the root cause if you can, no defensive fallbacks / no swallowed exceptions / no redundant validation", and finally Ctrl+C abort for handoff was added.
449
602
 
450
- ### 🐛 修复
603
+ ### 🐛 Fixes
451
604
 
452
- - **折行按字素簇计宽,修掉 emoji 撑破整帧布局(P0)** —— `toCells` / `wrapSpans` / `clipSpans` / `toChars` / `segmentsWidth` 原先按 **code point** 遍历文本:VS16 序列(`✌️` = `U+270C U+FE0F`)被算成 1 列而实际占 2 列 → 行超宽 → Ink 在渲染期二次折行 → 实际帧高比 `computeChatLayout` 预算多一行 → **输入框被顶出屏幕**;ZWJ 家族(`👨‍👩‍👧`)被算成 6 列而实际占 2 列 → 每两个簇就折一次。现在全部改走 `graphemes()` + `clusterWidth()`,与 Ink 排版同一把尺子。
453
- - **阈值写入不再整对象替换** —— `updateModel` 是浅合并,此前调用方直接构造 `{ handoff, guard }`,会把 `tokenThresholds` 里其它字段(SDK 后续新增的压缩阈值等)一并清空。新增 `mergeTokenThresholds()`,所有阈值写入走它。
454
- - **`/migrate` 之后屏幕一片空白** —— 迁移用 `prune` 策略把 `messages` 砍到 1~2 条,但 `transcriptFrom` 仍停在旧值,`messages.slice(transcriptFrom)` 恒为空:迁移成功、上下文有内容,屏幕上却什么都没有。现在迁移成功后重置转录显示起点(同 `/new` 的做法)。
455
- - **通知栏只剩最后一条** —— 订阅端是单值 state,同一批摘要 / 分阶段进度会被逐条覆盖。改为保留最近 4 条的队列(`MAX_NOTICE_HISTORY`),并让同一批摘要合成**一条多行**,既全部显示又只占一个槽位;排版决策(折行 / 多条让位 / 条内截断)收进 `notice.ts` 纯逻辑,新增可独立渲染的 `NoticeBar`。
456
- - **`ai hook --help` 以非零码退出** —— 「想看用法」与「写错了子动作」此前混为一谈。现在 `ai hook` / `ai hook --help` 正常输出、退出码 0;`ai hook boom` 报错并 exit 1。`ai help` / `ai version` 单词形式等价于对应选项,但**只在它是唯一参数时**判定(`ai help me refactor this` 仍是在提问)。
457
- - **`ai hook install` 漏清重复块** —— rc 里可能残留多个钩子块(老版本 + 新版本各写一次),旧实现只看第一块,于是「第一块恰好是最新版 → 判定已安装 → 后面的旧块永远留着」。改为取全部块,只有「恰好一块且内容一致」才算已安装。
458
- - **会话落盘不是原子写** —— SDK 的 `EntityRepository.write()` 是直接覆盖,进程在写一半时被杀(Ctrl+C / OOM / 断电)就留下半截 JSON,`read()` 解析失败返回 null,**整段历史就此丢失**(会话文件可达数 MB,写入窗口并不小)。改为「写 `<id>.json.tmp` → `rename`」,任何时刻读到的要么是旧文件、要么是完整新文件。
459
- - **Ctrl+C 无法中断会话交接** —— 两处根因:`summary` 模式的独立模型请求没有任何取消点;`self` 模式虽能被 `agent.abort()` 打断,但 abort 不抛错,中止后被误判成「模型没写出文档」,于是**重试一轮、再降级 summary 再跑一轮**——用户以为取消了,实际多烧两轮全量输入。现在交接接受 `AbortSignal`:取消时抛 `HandoffAbortedError`、**不降级不重试**、清掉写了一半的文档、`migrate` 也不改动会话历史;TUI 里 Ctrl+C 会先走交接的取消通道,提示「已取消」而不是报错。(依赖 `@ai-zen/agents-sdk` 同名改动)
460
- - **通知栏文案整体右移 2 列** —— 缩进被加了两遍:`NoticeBar` 的 `paddingX={1}` 一层,`notice.ts` 又硬拼 ` ` 一层,于是文案比同屏的分隔线与 `❯` 输入行多缩 2 列;迁移一次推 4 条通知时最刺眼。现在横向定位只归容器管,逻辑层只管折行。顺带去掉 `· ` / `✖ ` 前缀——语气已由 `tone` 着色表达(error 走红色),符号纯属占位;多行通知每行都从行首开始。配置更新的子项摘要改为「一个汉字宽度」(2 列)缩进,不再带项目符号。
605
+ - **Line wrapping measures width by grapheme cluster, fixing emoji blowing up the whole frame layout (P0)** — `toCells` / `wrapSpans` / `clipSpans` / `toChars` / `segmentsWidth` used to walk the text by **code point**: a VS16 sequence (`✌️` = `U+270C U+FE0F`) was counted as 1 column although it occupies 2 → the line got too wide → Ink wrapped a second time while rendering → the actual frame was one row taller than `computeChatLayout` had budgeted → **the input line was pushed off screen**; ZWJ families (`👨‍👩‍👧`) were counted as 6 columns although they occupy 2 → a wrap every two clusters. All of it now goes through `graphemes()` + `clusterWidth()`, the same ruler Ink lays out with.
606
+ - **Threshold writes no longer replace the whole object** — `updateModel` is a shallow merge, and callers used to construct `{ handoff, guard }` directly, which wiped every other field in `tokenThresholds` (compression thresholds the SDK added later, and so on). New `mergeTokenThresholds()`; all threshold writes go through it.
607
+ - **Blank screen after `/migrate`** — migration uses the `prune` strategy to cut `messages` down to 1~2, but `transcriptFrom` stayed at its old value, so `messages.slice(transcriptFrom)` was always empty: the migration succeeded and the context had content, yet the screen showed nothing. The transcript display origin is now reset after a successful migration (the same approach as `/new`).
608
+ - **Notice bar kept only the last entry** — the subscriber side is a single-value state, so a batch of summaries / staged progress overwrote each other one by one. It is now a queue keeping the most recent 4 (`MAX_NOTICE_HISTORY`), and a batch of summaries is merged into **one multi-line** entry, so everything is shown while occupying a single slot; layout decisions (wrapping / yielding when there are several / truncation within an entry) moved into `notice.ts` as pure logic, plus a new independently renderable `NoticeBar`.
609
+ - **`ai hook --help` exits with a non-zero code** — "wanted to see usage" and "wrote the wrong sub-action" used to be conflated. Now `ai hook` / `ai hook --help` print normally and exit 0; `ai hook boom` reports an error and exits 1. The bare-word forms `ai help` / `ai version` are equivalent to the corresponding options, but **only when the word is the sole argument** (`ai help me refactor this` is still a question).
610
+ - **`ai hook install` failed to clean up duplicate blocks** — an rc file may hold several hook blocks (an old version and a new one each wrote once), and the old implementation looked only at the first, so "the first block happens to be the newest → judged installed → the old blocks behind it stay forever". It now takes all blocks and counts as installed only when there is exactly one block and its content matches.
611
+ - **Conversation persistence was not an atomic write** — the SDK's `EntityRepository.write()` overwrites directly, so a process killed mid-write (Ctrl+C / OOM / power loss) leaves half a JSON, `read()` fails to parse and returns null, and **the whole history is lost** (conversation files can reach several MB, so the write window is not small). It now writes `<id>.json.tmp` → `rename`, so any read at any moment sees either the old file or a complete new one.
612
+ - **Ctrl+C could not abort a session handoff** — two root causes: the standalone model request in `summary` mode had no cancellation point at all; `self` mode could be interrupted by `agent.abort()`, but abort does not throw, so after the abort it was misjudged as "the model did not write a document", and the code **retried one round, then downgraded to summary and ran another round** — the user thought they had cancelled while two more full-input rounds burned. Handoff now accepts an `AbortSignal`: cancellation throws `HandoffAbortedError`, **no downgrade and no retry**, the half-written document is cleaned up, and `migrate` leaves the conversation history untouched; in the TUI, Ctrl+C first goes through the handoff's cancellation path and reports "cancelled" instead of an error. (Depends on the same-named change in `@ai-zen/agents-sdk`.)
613
+ - **Notice bar text shifted right by 2 columns** — the indent was applied twice: one layer from `NoticeBar`'s `paddingX={1}`, another layer from `notice.ts` hard-concatenating two spaces, so the text sat 2 columns further right than the separator and the `❯` input row on the same screen; it was most glaring when a migration pushed 4 notices at once. Horizontal positioning is now owned solely by the container and the logic layer only wraps. The `· ` / `✖ ` prefixes were dropped along the way — tone is already expressed by `tone` coloring (error goes red), so the symbols were pure placeholders; every line of a multi-line notice now starts at column 0. The per-item summary of configuration updates is now indented by one CJK character width (2 columns) instead of carrying a bullet.
461
614
 
462
- ### ✨ 新功能
615
+ ### ✨ New features
463
616
 
464
- - **`/load` 列表显示会话摘要** —— 会话 id 是时间戳(`chat_2026_10_28_00_56_34`),直接当标签列出来完全分不清哪条是哪条。现在取首条用户消息的第一行作为标签。实现上先只 `stat`、排序后**只为将要显示的那几条**解析内容,会话攒到上百条时 `/load` 也不会被拖慢。
617
+ - **`/load` list shows conversation summaries** — a conversation id is a timestamp (`chat_2026_10_28_00_56_34`), so listing them directly as labels made it impossible to tell which was which. The first line of the first user message is now taken as the label. Implementation-wise it only `stat`s first, and after sorting parses content **only for the few entries about to be displayed**, so `/load` is not slowed down even when conversations pile up into the hundreds.
465
618
 
466
- ### ♻️ 重构
619
+ ### ♻️ Refactors
467
620
 
468
- - **`chat.tsx` 五步拆 hook:1085 → 830 行** —— 只留会话生命周期与渲染组装,其余拆成 5 个可单测的 hook:`use-chat-viewport`(视口与滚动换算,所有动作经 `clampViewTop()`)、`use-overlays`(确认框 / 选择器的 resolve 生命周期,关状态与结算在同一次函数式更新里完成)、`use-wizard-host`(向导宿主,依赖注入)、`use-input-editing`(文本 / 光标 / 历史 / kill ring / undo / stash)、`common-keys`(两个 `useInput` 共用的键位 + Ctrl+D 双击退出)。
469
- - **斜杠命令改为表驱动** —— 557 行的 `switch` 换成 `HANDLERS: Record<CommandName, CommandHandler>` + 18 个处理器:「新增命令忘了写处理器」从运行时静默落到「未知命令」变成**编译错误**;命令名解析只剩 registry 一处(`parseCommandInput` / `parseCommandArgs` 共用同一套切分),`blocksInput` 成为命令元数据而不再是宿主白名单。
470
- - **向导字段工厂去噪** —— `commit` / `toggle` 签名放宽为 `string | null | Promise<string | null>`,删掉 23 处 `Promise.resolve(...)` 噪音;`nameField()` / `endpointIdField()` / `customPatch()` 等收掉 8 处重复段;`DetailFieldKind` 的 `"map"` 改名 `"open"`(语义本就是「按 Enter 进子屏」)。
471
- - **会话落盘收敛为唯一入口** —— 「读旧记录拿 `createdAt` → 写 → 更新指针」原先在 5 个地方各写一遍并且已经漂移(`saveConversation()` 漏写 `cwd`、stdio 的 `--save` 不更新指针、还写了一个已废弃的 `modelId`)。收敛到 `persistConversation()`,各处差异只剩「是否更新指针」一个显式开关。
472
- - **初始化与单例改为缓存 Promise** —— `getScope()` / `ensureConfigDir()` 缓存实例时,`if (x) return x` 之后到赋值之间有一段 `await`,并发调用会一起穿透(建出两个 Scope → MCP 双连接、双份工具发现)。改缓存 Promise 让并发调用共享同一次初始化,失败不缓存。
473
- - **删死代码与不可达兜底** —— 死字段 `ConversationContext.running|input|shouldSend`、从未启用的 i18n 复数机制(`pluralRules` + `key_one/_other`)、`noticeLines()` 末尾不可达的截断兜底(穷举 84 组证明从不执行)、`pick(direct, …)` 从没人传过的参数、`useChatViewport` 零引用的 `scrollTo` 导出、所有命令都空转一遍的 `parseHandoffMode`;`cwd` / `previewLimit` 从「可选 + 内部兜底」收成必填,兜底只保留在 SDK 可选字段的真实边界上。
474
- - **命令说明文案精简** —— 斜杠菜单与 `/help` 都是一行一条,说明过长会把整行塞满。按「只写做什么、不写实现细节」重写:砍掉内部概念(`scope`、写入配置、重建会话)与冗余限定词,括号只留用户必须预期的后果(替换会话 / 重建 / 不切换会话)。中文最长 59 → **34**、英文 73 → **49**(平均 19.3 / 33.8),`/mcp` 从全场最长退到中游。
475
- - **权限四维上屏走词条** —— `tools` / `skills` / `mcps` / `subagents` 是配置里的键,直接上屏等于把内部标识暴露给用户,映射到 `perm.*` 词条。
621
+ - **`chat.tsx` split into five hooks: 1085 → 830 lines** — only conversation lifecycle and render assembly are left; the rest became 5 unit-testable hooks: `use-chat-viewport` (viewport and scroll arithmetic, every action going through `clampViewTop()`), `use-overlays` (resolve lifecycle of confirm boxes / selectors, with closing and settling done in the same functional update), `use-wizard-host` (wizard host, dependency injection), `use-input-editing` (text / cursor / history / kill ring / undo / stash), `common-keys` (keybindings shared by the two `useInput` handlers + double Ctrl+D to quit).
622
+ - **Slash commands are table-driven** — the 557-line `switch` became `HANDLERS: Record<CommandName, CommandHandler>` + 18 handlers: "added a command but forgot the handler" changed from silently falling through to "unknown command" at runtime into a **compile error**; command-name parsing lives only in the registry (`parseCommandInput` / `parseCommandArgs` share one tokenizer), and `blocksInput` became command metadata instead of a host whitelist.
623
+ - **Wizard field factory de-noised** — the `commit` / `toggle` signatures were widened to `string | null | Promise<string | null>`, removing 23 `Promise.resolve(...)` noise sites; `nameField()` / `endpointIdField()` / `customPatch()` and friends absorbed 8 duplicated chunks; `DetailFieldKind`'s `"map"` was renamed to `"open"` (the semantics were always "Enter opens a sub-screen").
624
+ - **Conversation persistence converged into a single entry point** — "read the old record for `createdAt` → write → update the pointer" used to be written out in 5 places and had already drifted (`saveConversation()` missing `cwd`, stdio's `--save` not updating the pointer, and a deprecated `modelId` still being written). Everything converges on `persistConversation()`, and the only remaining difference is one explicit switch: whether to update the pointer.
625
+ - **Initialization and singletons cache promises** — when `getScope()` / `ensureConfigDir()` cached instances, there was an `await` between `if (x) return x` and the assignment, so concurrent calls fell through together (two Scopes built → MCP connected twice, tool discovery done twice). Caching the promise lets concurrent calls share one initialization; failures are not cached.
626
+ - **Dead code and unreachable fallbacks removed** — the dead fields `ConversationContext.running|input|shouldSend`, the never-enabled i18n plural machinery (`pluralRules` + `key_one/_other`), the unreachable truncation fallback at the end of `noticeLines()` (84 combinations enumerated to prove it never runs), the `pick(direct, …)` parameter nobody ever passed, the zero-reference `scrollTo` export of `useChatViewport`, and `parseHandoffMode`, which every command ran to no effect; `cwd` / `previewLimit` went from "optional + internal fallback" to required, with fallbacks kept only at the real boundary of the SDK's optional fields.
627
+ - **Command descriptions trimmed** — the slash menu and `/help` are one line per command, and an over-long description fills the whole line. Rewritten on the principle "say what it does, not how it is implemented": internal concepts dropped (`scope`, writing config, rebuilding the session) along with redundant qualifiers, and parentheses keep only the consequences the user must expect (replaces the conversation / rebuilds / does not switch conversation). Longest Chinese 59 → **34**, English 73 → **49** (mean 19.3 / 33.8), and `/mcp` fell from the longest to mid-pack.
628
+ - **The four permission dimensions go on screen through dictionary entries** — `tools` / `skills` / `mcps` / `subagents` are keys in the configuration, and showing them directly would expose internal identifiers to the user, so they map to `perm.*` entries.
476
629
 
477
- ### ⚡ 性能
630
+ ### ⚡ Performance
478
631
 
479
- - **`graphemes()` 加快速路径** —— 折行是每帧热路径,但绝大多数文本(ASCII / CJK / 代码)每一簇就是一个 code point,不需要 `Intl.Segmenter`。先用一次正则判定「是否可能出现多 code point 合为一簇」(astral / CRLF / ZWNJ·ZWJ / 韩文组合字母 / `\p{M}` 组合附加符),命中才走分词器。一帧 60 行中文:**655 µs → 52 µs**(省 604 µs,帧预算 1%)。判定条件宁可误报不可漏报,新增 6 例护栏覆盖各类形态。
632
+ - **Fast path for `graphemes()`** — wrapping is a per-frame hot path, but for the vast majority of text (ASCII / CJK / code) every cluster is a single code point and `Intl.Segmenter` is unnecessary. One regex first decides "could multiple code points form one cluster" (astral / CRLF / ZWNJ·ZWJ / Hangul composing jamo / `\p{M}` combining marks), and only on a hit does it go to the segmenter. One frame of 60 lines of Chinese: **655 µs → 52 µs** (604 µs saved, 1% of the frame budget). The predicate prefers false positives over false negatives, and 6 new guard cases cover the various forms.
480
633
 
481
- ### 🔧 依赖
634
+ ### 🔧 Dependencies
482
635
 
483
- - 升级 `@ai-zen/agents-sdk` 至 `1.0.0-alpha.21`(交接取消能力)。
636
+ - Upgraded `@ai-zen/agents-sdk` to `1.0.0-alpha.21` (handoff cancellation).
484
637
 
485
- ### ✅ 测试
638
+ ### ✅ Tests
486
639
 
487
- - 全量 **46 文件 / 604 例通过**(评审前 36 文件 / 487 例,净增 10 文件 / 117 例护栏)。
488
- - 重点新增:`use-chat-viewport` 7 例、`use-overlays` 6 例、`use-wizard-host` 9 例、`use-input-editing` 13 例、`common-keys` 10 例、`chat.test.tsx` 7 例集成护栏、`lines.test.ts` 12 例(字素簇折行)、`notice-bar` 4 例、`conversation-persist` 10 例(含 spy `fs.rename` 钉住原子写)。
489
- - 破坏性验证(每项都确认护栏会红):折行改回 code point、`clampCursor` 改 `Array.from`、翻页步长改 `budget`、删掉 PgUp 分支、去掉 `\p{M}`、原子写改回直写、`clearDraft` 去掉历史游标复位。
640
+ - Full run: **46 files / 604 cases passed** (36 files / 487 cases before the review; a net gain of 10 files / 117 guard cases).
641
+ - Notable additions: `use-chat-viewport` 7 cases, `use-overlays` 6 cases, `use-wizard-host` 9 cases, `use-input-editing` 13 cases, `common-keys` 10 cases, `chat.test.tsx` 7 integration guard cases, `lines.test.ts` 12 cases (grapheme-cluster wrapping), `notice-bar` 4 cases, `conversation-persist` 10 cases (including a spy on `fs.rename` pinning the atomic write).
642
+ - Break checks (each confirmed to turn the guard red): wrapping back to code points, `clampCursor` switched to `Array.from`, paging step switched to `budget`, the PgUp branch removed, `\p{M}` dropped, atomic write back to direct write, the history cursor reset removed from `clearDraft`.
490
643
 
491
644
  ## [1.0.0-alpha.21] - 2026-10-27
492
645
 
493
- ### ✨ 新功能
494
-
495
- - **状态栏改为三个信息块,并统计生成速率** —— 此前状态栏是一条按优先级排列的文本流,窄屏折行时会把 `本轮 1284 tok` 这类单元从中间劈开,也让「哪段信息属于一类」看不出来。
496
- - 现在分成三块:① 模型 · Agent · 思考强度 ② 本轮 / 累计 token 用量 · 生成速率 ③ 快捷键提示(垫底)。
497
- - **换行以块为单位**:新增 `packSegments()`(`src/tui/status-segments.ts`)——放得下就并排(` · ` 连接),放不下就把**整块**挪到下一行;只有单块自身超过一整行时才退化成块内折行(优先空格断、CJK 硬断、行首孤儿 `·` 丢弃、行尾空格裁掉)。行边界只会落在块边界上。
498
- - **行数确定**:`statusBarRows()` 在纯逻辑里量出实际行数并交给 `computeChatLayout({ statusBarLines })`,测量与渲染是同一个函数——窄屏下状态栏变高不会把输入框顶出屏幕。
499
- - 数字与单位用 NBSP 绑定(`本轮␣1284␣tok`),块内也不会被拆成两行。
500
- - **生成速率两口径,不混成一个数**:生成中 `⚡ 47 tok/s` 是估算值(按输出增量折算,CJK 0.7 / 字、其它非空白 0.25 / 字符、空白 0.1,取最近 2 秒滑动窗口);一轮结束后改用 `⚡ 均 38 tok/s`,分母是「首字 → 末字」跨度,分子是 API 返回的真实 `usage.completion_tokens`(多轮工具调用逐轮累加)。跨度 < 400ms 不显示,避免首包爆发把速率放成几百。
501
- - **新增 `/agent`:会话内快速切换主 Agent** —— 选择器列出 `~/.ai-zen/agents/*.json` 的全部主 Agent,标注当前项与其绑定的模型名(换 Agent 等于连模型一起换)。
502
- - 支持 `/agent <id>` 直切;无参弹选择器。
503
- - 切换前做完整链路预检:Agent 存在 → 绑定了模型 → 模型还在配置里 → 端点在 → 端点有 Key。跑不起来的标「不可用」并给出原因,而不是切过去之后卡在启动错误屏。
504
- - 写入 `config.defaultAgent`(与 `/config` →「默认 Agent」同一字段),随后重建会话;**消息与上下文保留**。不改 Agent 定义本身(提示词 / 权限 / `modelParams` 一概不碰)。
505
- - **新增 `/mcp`:直达 MCP 管理界面** —— 不另建一套编辑器,直接把配置中心里已有的 MCP 屏打开(增 / 删 / 改 / 启停 / 全局⇄项目作用域切换都在那一屏)。关闭向导后沿用既有 `mcpChanged` 路径自动 `session.reload()`,新服务器当场生效。
506
- - 默认落点:项目级有服务器就进 `project`,否则进 `global`(项目级为空时进去只会看到空列表)。`/mcp global`、`/mcp project`(可缩写 `g` / `p`)可指定。
507
- - **新增 `/reload`:刷新运行环境** —— `session.reload()` 内含 `resetScope()`(断开旧 MCP 连接、丢掉配置快照)→ 重建 Agent → 重新发现工具 / Skill / MCP。手改 `mcp.json` / `skills/` / `tools/` 后一条命令生效,**消息与上下文保留**,无需重启 CLI。
508
- - **中断时保留已输出的思考与内容** —— 按 `Ctrl+C` / `Esc` 停下后,模型**已经吐出来的那半句话不再凭空消失**。此前中断只是把在途消息标成 `Aborted`,而历史过滤器只放行 `completed`,于是屏幕上还留着的思考与回答在下一轮请求里整条不见,模型下一轮像失忆一样重来。
509
- - 根因在上游:core 的 `abort()` 只管打标记、`formatHistory()` 只按状态过滤,两者都不关心「已经输出了什么」。core `4.5.3` 新增 `abortedMessageStrategy`(`drop` / `keep`,默认 `drop`),并导出 `reconcileAbortedMessages()`,在工具轮次归一**之后**收尾纯内容消息。
510
- - CLI 显式设成 `keep`:确有输出的被中断 assistant 会被恢复为 `completed` 进入上下文。**空输出不保留**(避免空消息污染上下文)、**带工具调用的不归它管**(仍走「整轮进出」,否则会出现「有调用无结果」的非法历史)。
511
- - SDK 依赖随之升级到 `1.0.0-alpha.20`(`createAgent` 新增第三参 `CreateAgentOptions`,顺带把此前完全没暴露的 `abortedToolRoundStrategy` / `abortedToolRoundReason` 一起接出来),core 升到 `4.5.3`。
512
- - **界面国际化(中文 / English)** —— 首次启动按系统语言识别(`LC_ALL` → `LC_MESSAGES` → `LANG` → Node 的 `Intl` locale,**兜底英文**),识别结果写入 `~/.ai-zen/cli/preferences.json`,之后以偏好为准(改了系统语言也不会让界面语言悄悄漂移)。`/lang` 可随时切换,`/lang zh` / `/lang en` 直接指定。
513
- - 词典编译进产物(`src/i18n/{zh,en}.ts` + 配置中心专区 `{zh,en}-wizard.ts`):key 扁平化,`{n}` 插值;切换语言只换一个引用,不重读磁盘、不重启。缺词条时返回 key 本身(不做跨语言静默回退,否则英文界面里会突然冒出中文)。`t()` 支持 `_one` / `_other` 复数变体(由 `Intl.PluralRules` 选),但 `i18n/index.test.ts` 要求**中英 key 集合完全一致**,故现有词条统一用 `(s)` 写法,不开变体。
514
- - 语言偏好放 CLI 独立的 `preferences.json`,**不塞进共享的 `config.json`** —— 那是 CLI 与 Desktop 共用的 `AppConfig` 契约,塞进去会被桌面端当未知字段丢掉。
515
- - 覆盖范围是**整套 TUI 界面**:主对话框(状态栏三块、输入占位与提示、通知、思考档位标签、生成中提示)、命令层(`/help`、斜杠菜单、各命令的选择器与反馈)、配置中心全部屏幕(端点 / 模型 / 图片模型 / MCP / Agent 定义 / 权限 / 默认项 / 工具输出上限 / 新建引导 / KV 与多行文本编辑器)、交接方式选择器与确认框、启动屏与启动错误屏,以及业务模块返回的 summary(`/model` `/agent` `/think` 的切换结果、交接进度与降级提示)。
516
- - `COMMAND_REGISTRY` 里存的是 **i18n key 而不是文案** —— 它是纯数据模块、加载时机早于语言初始化,存文案会把「加载那一刻的语言」定死;渲染时由 `getCommandHints()` 现取。启动屏的 `TAGLINE` 常量同理改成了 `tagline()` 函数。
517
- - **管道模式不做 i18n**:`ai <提示词>` / 管道输入的所有输出与报错**一律英文**(它是给脚本用的,不应随人的界面语言变),故 stdio 路径**不初始化语言**,行为与 `preferences.json` 彻底解耦。
518
- - **`ai --help` 与 `ai hook install / uninstall` 走 i18n**:两者都在 TUI 之外打印,因此在各自分支里先 `initLocale()` 再取词条;`HELP_TEXT` 那个模块级常量相应改成 `helpText()` 现拼(常量会把加载那一刻的语言定死)。
519
- - **会话 id 改名**:`对话_2025年07月01日14时30分00秒` → `chat_2026_10_28_00_56_34`(英文前缀 + 全下划线时间戳,中英一致、文件名安全)。它同时是文件名,跟着语言变会让历史文件与新文件混排,故时间戳由新增的 `formatFileStamp()` 生成;而**显示用**的 `formatShortTime()` 反而改为随语言切换。管道模式 `--save` 不再自己拼名字,改为调用 `generateConversationId()`,两处重复拼接一并消掉。
520
- - **注入历史的「工作目录」提示改为英文并显式标记**:`[system-injected: working directory changed — not written by the user]` —— 翻存档或交接文档时一眼能分辨哪些内容不是用户敲的;`stripCwdNote()` 同时认新旧两代格式,避免旧存档把这段笔记渲染到屏幕上。
521
- - 新建 Sub-agent 时写进 tool schema 的 `task` 参数描述也走词典(中 `完整的任务描述` / 英 `Full task description`)—— 这是唯一一处「协议内容也翻译」的例外,代价是同一份定义在不同界面语言下创建会得到不同的 schema 文本。
522
- - 唯一不翻的注入内容就是上述工作目录提示;`hook.ts` 写进 rc 文件的三个代码块标记串(`# === ai hook (开始) ===` 等)也保持原样 —— 翻了就再也找不到旧块。
523
- - 时间格式随语言切换 dayjs locale 与 pattern:中文 `7月1日 14:30` / `昨天 14:30`,英文 `Jul 1, 14:30` / `Yesterday 14:30`。
524
- - 顺带排掉两个隐藏地雷:状态栏曾用**文案**判断思考强度是否置灰(`effort === "关"`),英文界面下永远不成立,改为档位语义的 `effortDimmed()`;输入区占位提示曾硬编码在 `inputLines()` 里(渲染层却走 `t()`),英文界面下量输入行高度会短一行、连带硬件光标 y 偏移,现在两处同源。
525
-
526
- ### 🔧 行为变更
527
-
528
- - **消息栏的「思考中…」拆成六态:连接中 / 排队中 / 思考中 / 调用工具 / 等待工具返回 / 输出中** —— 原先那行只有「思考中…」一个词,但一轮对话里模型其实走过几个阶段,用同一个词概括在多数时候是**说错话**:等首 token 时并没有在思考,工具执行时模型根本没在跑。
529
- - 判据只用**事件与 delta 的形状**(`src/tui/phase.ts`,纯逻辑可单测):`connecting` = 请求发出到响应头到达;`queued` = `open` 之后、首个内容 chunk 之前;`thinking` = 收到 `reasoning_content`;`tool-calling` = 收到 `tool_calls` / `function_call`(带工具名,后续只带参数分片时沿用首个 delta 里的名字);`tool-waiting` = `parsed` 且本轮带工具调用;`streaming` = 收到 `content`。一个 chunk 里同时出现时优先级是 tool_calls > content > reasoning。
530
- - 「连接中」这一段靠新增的 `PhasePlugin`(`chat-session.ts`)提供:SDK 的 `open` 事件是在 `chat.completions.create()` **await 返回之后**才发的,只代表响应头到了,之前的 DNS / TCP / TLS / 服务端受理全静默。
531
- - **子 Agent 的输出不推进主阶段**:委派期间主 Agent 正在等工具返回,若把子 Agent 的 delta 也算进主阶段,界面会跳成「输出中」,正好盖掉用户最需要知道的那句话。子 Agent 的 chunk 仍计入速率、仍触发重绘。
532
- - spinner 那一行加了 `truncate`:busy 时输入区高度被 `computeChatLayout` 固定为 1 行(`inputHeight = 1`),标签一旦折行就会把输入框顶出屏幕。
533
- - **状态栏去掉「⏳ 生成中」** —— 本轮到底在干什么已由消息栏 spinner 逐阶段说明,状态栏再挂一个笼统的「生成中」只是重复占宽度(窄屏下还把快捷键提示挤到下一行)。
534
-
535
- - **shell 钩子改为进入 TUI(此前的管道模式是实现错误)** —— 钩子装在 `command_not_found_handle` / `command_not_found_handler` 上,触发场景是「人坐在交互终端前把命令敲错了」,但函数体写的是 `ai "$@"`,而 `decideMode()` 的规则是「有位置参数 → stdio」,于是钩子会话必然落到管道模式:吐一段文字到 stdout 就退出,**没有历史、不能追问**。`mode.ts` 头顶那张分类表把「脚本调用」与「人手动触发」混成了一个判据,所以这不是补丁能修的,是分类本身错了。
536
- - 函数体现在是 `ai --tui --new "$@"`:新增 `--tui` 标志显式覆盖「有参数 → stdio」的判定,进 TUI 并把原命令**作为首条消息自动发送**(不预填到输入框——钩子的语义就是「这条命令跑不了,你帮我处理」,再让人敲一次回车没有意义)。
537
- - 首条消息**强制当普通用户消息**发送,不经斜杠命令解析:敲错的可能是 `/opt/bin/foo` 这种以 `/` 开头的命令,否则会被当成 CLI 命令而报「未知命令」。`runSubmit()` 为此多了 `asMessage` 选项。
538
- - `--tui` 在非 TTY 环境(脚本 / 管道)报错退出(退出码 2、英文提示),**不**降级成管道模式 —— 否则整屏界面内容会被写进管道。
539
- - 首条消息只在首次挂载时发送:切模型 / `/lang` 等引起的重挂载不会重发。
540
- - **钩子会话总是新开会话**:函数体多一个 `--new`(`ai --tui --new "$@"`)。否则敲错一次命令,那条命令就被追加进该目录原有的长对话里,上下文与会话名一起被搅乱。`--new` 是通用标志,手动用同样有效:`ai --new` 跳过 `last-session.json` 指针直接新建;它**只作用于 TUI 路径**,出现在 stdio 路径上时静默忽略(一次 stdio 调用本就是一次性会话,不存在续接)。
541
- - **`ai hook install` 现在会自我升级**:此前只要 rc 文件里存在 `MARKER_START` 就打印「已安装」直接返回,于是函数体改过之后(比如本次加了 `--new`)老用户永远升不上去。现在会把已安装块与当前版本归一化比对(去空行、逐行 trim),不一致就原地覆盖并提示「已更新到最新版本」(新增词条 `hook.updated`)。
542
-
543
- - **`/migration` 不再是 `/migrate` 的别名** —— 只保留 `/migrate`,`/migration` 现在报「未知命令」。registry、命令执行器、`blocksInput` 判定与四份文档同步。
544
- - **SDK 全局日志默认写入 `~/.ai-zen/logs/sdk.log`** —— 以前默认**丢弃**,只有 `AIZ_DEBUG_SDK=1` 才落盘到 `/tmp`。但 SDK 的 `warn` / `error` 承载「MCP 连不上」「Skill 解析失败」这类关键诊断,默认丢弃等于故障无痕。现在默认落盘(单文件 2MB 封顶后轮转为 `sdk.log.1`),`AIZ_DEBUG_SDK=0` 可彻底关掉,`AIZ_SDK_LOG_FILE` 可改路径。界面上依旧一个字都不显示。
545
-
546
- ### 🐛 修复
547
-
548
- - **修复 IME(输入法)候选框「有时候定位不准」** —— 硬件光标位置以前靠 `displayWidth()` 自己算。但宽度是个陷阱:Ink 排版时用 `string-width`,我们手写的表在若干字符上与它不一致(`⚡` `✅` `✨` `⭐` 我们算 1 列、Ink 算 2 列;ZWJ 组合 emoji `👨‍👩‍👧` 我们算 8 列、Ink 算 2 列;keycap `1️⃣` 我们算 3、Ink 算 2),输入里一出现这类字符,x 就会偏。现在改为**渲染后实测**:
549
- - 光标所在字符包进独立 `<Box ref>`(行内嵌套的 `<Text>` 片段在 yoga 里没有节点,`measureElement()` 对它返回 `null`,实测),用 Ink 7 的 `measureElement()` 读它拓印到帧内的绝对坐标 —— 宽度由渲染器自己算,不再经过我们的模型。
550
- - 拆行后原本整行 `truncate` 的兜底会丢:一旦内容超宽(如宽度模型把 `⚡` 算成 1 列)这行会被顶成两行,整帧高一行、底部被顶出屏幕。解法是把溢出交给**可截断的邻段**吸收——`before` / `after` 段 `flexShrink={1} minWidth={0}` + `wrap="truncate"`,光标块 `flexShrink={0}`;实测容器 40 列、`before` 60 列时仍是**一行**。
551
- - 测量只能在渲染后(`useEffect`)做,而 Ink 的 `setCursorPosition` 只在 commit 阶段(`useInsertionEffect`)推送,所以存在一次 commit 的延迟:首次渲染先用旧手算值顶一帧,下一帧被实测值纠正(多一趟渲染,Ink 自带节流)。
552
- - 占位提示行(未输入任何内容)不走实测,仍用前缀后的固定位置。
553
- - **占位提示态不再自绘「假光标」** —— 输入框为空时会显示灰字提示「输入消息,/ 查看命令…」,末尾原本还画了一个 `▏`。但硬件光标这时就停在提示语开头(`x = 前缀宽`),终端自己会把它显示出来,再画一个方块等于屏幕上两个光标,而且位置还在提示语末尾,反而误导。现在占位态只留提示文字。
554
- - **修复「输入 emoji 后反显光标消失 / 真实光标错位」** —— 根因有两个,都是把「字符」当成了 code point:
555
- - **光标停在字素簇中间**:`✌️` 是 `U+270C U+FE0F` **两个 code point、一个字素簇**(`👨‍👩‍👧` 五个、`🇨🇳` 两个区域指示符)。按 code point 切分时,光标能停在 `✌` 与 `U+FE0F` 之间,于是反显的是一串**变体选择符** —— Ink 的输出里反显段直接消失(实测:`at=U+FE0F` 时帧里既无反显转义也没字符)。同时退格会只删掉半个 emoji、留下孤立的 `U+FE0F`。现在全部光标运算(折行、移动、删除、插入、撤销)改用 `Intl.Segmenter` 的**字素簇**为单位(`graphemes()`,Node 内置无需依赖)。
556
- - **宽度表换成 Ink 自己的尺子**:`displayWidth()` 不再用手写的 `isWide` 区间表,直接调用 **`string-width`**(Ink 排版用的就是它,见 `ink/build/output.js`);单簇宽度带缓存,折行热路径几乎零成本。从此折行、状态栏装行、光标列号与渲染器同源 —— 状态栏那条「预留 1 列余量」也降级为纯安全边际。
557
- - **光标单位改成字素簇**:`✌️` 是 `U+270C U+FE0F` **两个 code point、一个字素簇**(`👨‍👩‍👧` 五个、`🇨🇳` 两个区域指示符)。按 code point 切分时,光标能停在 `✌` 与 `U+FE0F` 之间,于是反显的是一串**变体选择符** —— Ink 的输出里反显段直接消失(实测:`at=U+FE0F` 时帧里既无反显转义也没字符)。同时退格会只删掉半个 emoji、留下孤立的 `U+FE0F`。现在全部光标运算(折行、移动、删除、插入、撤销)改用 `Intl.Segmenter` 的**字素簇**为单位(`graphemes()`,Node 内置无需依赖)。
558
- - **新增直接依赖** `string-width@^8.3.0`(纯 ESM,本就在 Ink 的依赖树里,体积无变化)。
559
- - **通知栏行数未计入光标 y** —— 渲染顺序是「对话区 → 通知栏 → 斜杠菜单 → 分隔线 → 输入行」,而 y 公式漏了通知栏一段。于是任何通知出现时硬件光标都会整体上浮那么多行;`/help` 一次塞最多 8 行通知,候选框能浮进对话区。现已补上 `noticeCount`(现作为实测未就绪时的兜底算法保留)。
560
- - **修复 SDK 日志「接管了却照样糊界面」** —— 加载 MCP 时 `[load_mcp]` 连同整份 tools / resources 清单直接砸在渲染区外面。原因是 SDK 有 4 个模块在**模块顶层**执行 `const log = getLogger()`:模块求值那一刻就把 `console.log` 的函数引用冻进了常量,`setLogger()` 事后只替换内部变量,改不动已经抓走的引用;ESM 的静态 import 又保证这些模块在 `runTui()` 之前就已求值 —— 于是 CLI 无论怎么接管都会漏。
561
- - 修在 SDK 侧(不改 CLI 侧骗 console 的 workaround):4 处全部改为**运行时取 logger**(`getLogger().info(...)` 等);`skillTools` 那处是从未被调用的死代码,连同 import 一并删除。至此 SDK 内 10 处日志调用全部经由 `getLogger()` 单一出口,不存在 console 旁路。
562
-
563
- ### 📝 文档
564
-
565
- - README(中 / 英)与 `docs/{zh,en}/getting-started.md` 的命令表新增 `/agent`、`/reload`、`/mcp` 三行。
566
- - README(中 / 英)与 `docs/{zh,en}/index.md` 的状态栏说明从「一条按优先级的文本流」改为「三个信息块 + 按块换行」。
567
- - `docs/{zh,en}/mcp.md` 的「管理」章节补充 `/mcp` 直达入口与 `/reload` 刷新;`docs/{zh,en}/migration.md` 去掉「别名 `/migration`」。
568
- - README(中 / 英)新增「界面语言」一节:识别优先级、`preferences.json` 与 `/lang`、覆盖范围,以及刻意不翻的三类内容。
569
- - `docs/manifest.json` 与 README 中的版本号更新为 `1.0.0-alpha.21`。
570
- - README(中 / 英)的「环境变量」补 `AIZ_DEBUG_SDK=0` / `AIZ_SDK_LOG_FILE`,「文件系统布局」补 `~/.ai-zen/logs/`,键位表下补一段「中断后已输出的思考与内容会保留」的说明。
571
- - README(中 / 英)与 `docs/{zh,en}/getting-started.md` 的命令表补 `/lang`;「文件系统布局」与 `docs/{zh,en}/configuration.md` 的目录树补 `~/.ai-zen/cli/preferences.json`(CLI 本地偏好)。
572
-
573
- ### ⚠️ 已知取舍
574
-
575
- - **emoji 的渲染宽度可能仍与终端对不上,这是终端层的事**:代码侧已经做到与渲染器同源(`displayWidth` = Ink 用的 `string-width`,光标位置用 `measureElement` 实测),但**终端自己对 emoji / East Asian Ambiguous 字符的列宽判定**仍可能与 Unicode 表不同(部分终端把 `✌️` 渲染成 1 列而不是 2 列,且没有查询接口)。那种情况下整行文本的相对位置就已经由终端决定了,光标只能跟着 Ink 的模型走。输入 / 删除 / 光标单位层面的 emoji 问题已在本次修完。
576
- - **光标是「终端光标 + 字符反显」两套并存,这是故意的**:光标处的字符仍用 `<Text inverse>` 反显成一个方块,位置的真相源是实测出来的硬件光标。两者视觉上会叠成一个略重的方块——接受这个重影,因为任一层失效时另一层还能报位置(某些终端 / 多路复用器里硬件光标不显示,或 Ink 卸载时反显又没了)。以后别把反显当重复渲染删掉。
577
- - **块不被劈开 ⇒ 行数随宽度变化**:80 列下三块各占一行(3 行),120 列可把身份与用量并成 1 行(共 2 行)。旧的「一条流」能压到 2 行,代价是把 `本轮 / 累计` 拆到两行——信息完整性优先。
578
- - **生成中的速率是估算值**:SDK 只在每个请求的**最后一个 chunk** 才给 `usage`(`stream_options.include_usage`),拿不到逐 token 用量;把估算与精确值混成一个数字才是误导,因此分成「实时估算」与「本轮均速」两种写法。
579
- - **`/mcp` 只覆盖配置层**:服务器当前是否连通、暴露了多少工具,在 SDK 里是插件私有状态(`McpScopePlugin` 的 `mcpManager`),没有公开 API,界面不展示连接状态。
580
- - **`/reload` 有一两秒空档**:重建 Agent 且重连 MCP,服务器多的场景会有可感知延迟;期间消息与上下文不受影响。
581
-
582
- ### ✅ 测试
583
-
584
- - 全量 **33 文件 / 440 例通过**,`npx tsc --noEmit -p tsconfig.test.json` 干净。
585
- - 新增 `src/i18n/index.test.ts`(14 例:语言识别优先级 / `C`·`POSIX` 跳过 / 兜底英文 / 插值 / 缺词条回退 key / 中英词条一一对应)与 `src/i18n/preferences.test.ts`(9 例:文件缺失·损坏·非法值的容错、原子写、`initLocale` 只识别一次并落盘);`reasoning-effort.test.ts` 补 5 例「置灰不靠文案判断」。
586
- - pty 真机验收:`LANG=en_US.UTF-8` 首次启动→英文并落盘 `en`;`/lang` 切中文→状态栏/输入框/通知全部跟随,且**重启后保持中文**(偏好先于系统语言);`/lang en` 切回英文正常。
587
- - pty 真机验收(阶段 2):英文下斜杠菜单显示 `❯ /help Show this help`、`/help` 输出全英文列表;`/lang zh` 后斜杠菜单与 `/help` 整体切回中文。
588
- - pty 真机验收(阶段 3 / 4,隔离的 `AI_ZEN_DIR` + `LANG=en_US.UTF-8`):首次启动识别为英文并落盘;启动屏 `The AI collaboration desk in your terminal`;主屏 `Type a message, / for commands…`、`thinking default · total 0 tok`;配置中心 `◆ Configuration` 与 `+ New model — marked as custom automatically` / `openai · factory-managed · vision`;模型详情 `◆ Model · GPT-6 Astra` / `factory-managed (editing switches it to custom)` / `Delete model`;新建模型与新建端点引导屏全英文。把 `preferences.json` 改回 `zh` 后**在 `LANG=en_US.UTF-8` 下重启仍为中文界面**(偏好优先于系统语言),配置中心与模型列表中文文案与改造前一致。
589
- - **断言中文的测试一律显式锁语言**:`setLocale("zh")` 加在 `screens.test.tsx`、`config-wizard.test.tsx`、`reasoning-effort.test.ts`、`registry.test.ts`、`agent-prompt.test.tsx`、`handoff-choice.test.ts`、`config-editor.test.ts`、`agent-switch.test.ts`、`model-switch.test.ts`、`handoff-service.test.ts`、`auto-migrate-confirm.test.ts` —— 默认语言是 `en`,不锁就会在 CI 上莫名其妙地红。
590
- - `mode.test.ts` 新增 6 例 `--tui`:有参数进 TUI 且参数作为首条消息、无参数时同不带 `--tui`、以 `/` 开头的命令不被当选项 / 子命令、`--tui --help` 时 help 优先、非 TTY 拒绝而非降级、`--` 之后的 `--tui` 是普通参数。
591
- - `mode.test.ts` 新增 8 例 `--new`:单独用时进 TUI 且 `newSession: true`、`--tui --new <命令>` 同时带首条消息与新会话、与 `--tui` 顺序无关、有参数时落到 stdio 并被忽略、非 TTY 时落到 stdio 并被忽略、`--tui --new` 非 TTY 仍拒绝而非降级、`--new --help` 时 help 优先、`--` 之后的 `--new` 是普通参数。
592
- - 新增 `src/hook.test.ts`(9 例,全部在临时 HOME 下跑,**不触碰真实 `~/.bashrc`**):写入的函数体含 `ai --tui --new "$@"` 且只有一个块、保留 rc 原有内容、重复安装幂等、**已装旧函数体(`ai --tui "$@"`)时原地升级**、历史 aiz 块迁移、zsh 走 `command_not_found_handler` 且写入 `.zshrc`;卸载移除块并保留其余内容、未安装时不动文件、rc 文件不存在时不报错。
593
- - pty 真机验收(钩子路径):`ai --tui gti status` 进 TUI 并自动把 `gti status` 当用户消息发出(界面显示 `❯ 你 / gti status`),模型报错(故意的本地假端点)后界面仍存活可继续追问;`ai --tui /opt/bin/nonexistent --flag` 同样当**普通消息**发出,不被斜杠命令解析吃掉;`ai --tui` 在非 TTY 下退出码 2;写进 `~/.bashrc` 的函数体为 `ai --tui --new "$@"` 且 `bash -n` 语法通过。
594
- - pty 真机验收(`--new`,隔离 `AI_ZEN_DIR` + 预置 `last-session.json` 指向旧会话):`ai` 续接并显示旧消息 `MARKER-OLD-QUESTION`;`ai --new` 停在**空对话**;`ai --tui --new gti status` 空对话且首条消息自动发出;`--help` 中英两个界面都列出 `--new`。
595
- - 本次改动后全量 **36 文件 / 487 例通过**,`npx tsc --noEmit` 与 `-p tsconfig.test.json` 均干净。
596
- - 新增 `src/tui/phase.test.ts`(15 例:六态各自的推进、工具名沿用、无 delta 的收尾 chunk 不改阶段、content 压过 reasoning、`reset` 归零、中英文案六者互不相同);`chat-session.test.ts` 补 3 例(按 SDK 事件时序推进、子 Agent 输出不覆盖主阶段、`send` 结束阶段归零)。
597
- - pty 真机验收(本地 mock SSE 服务端按阶段分段吐流):依次抓到「连接中…」→「排队中…(等待首 token)」→「思考中…」→「输出中…」→「调用工具 no_such_tool…」→「等待 sub_agent_default 返回…」;状态栏同期只有 `⚡ 4.2 tok/s` 与本轮 / 累计 token,无「生成中」。
598
- - 新增 `src/agent-switch.test.ts`(10 例)、`src/mcp-command.test.ts`(6 例)、`src/tui/token-rate.test.ts`(20 例)、`src/tui/status-segments.test.ts`(17 例)、`src/tui/screens/input-row.test.tsx`(7 例)、`src/tui/text.test.ts`(18 例);`input-editing.test.ts` 补 3 例「emoji 不被拆半个」的用例。
599
- - `src/tui/screens.test.tsx` 增加状态栏三块顺序、块内不被拆、宽屏并排、以及「Ink 实际渲染行数 == `statusBarRows` 量出行数」的断言;`computeInputCursorPosition` 补「通知栏行数参与 y」的用例。
600
- - `input-row.test.tsx` 里那条 `⚡` 用例同时断言「手算给 8、实测给 9」——本次改动的理由就在这两行里。
646
+ ### ✨ New features
647
+
648
+ - **Status bar split into three information blocks, plus a generation rate** — the status bar used to be one priority-ordered stream of text, and when it wrapped on a narrow screen it cut units like `this round 1284 tok` in half while also making it impossible to tell which pieces of information belonged together.
649
+ - It is now three blocks: ① model · Agent · reasoning effort ② this round / cumulative token usage · generation rate ③ keybinding hints (at the bottom).
650
+ - **Wrapping is per block**: new `packSegments()` (`src/tui/status-segments.ts`) — if it fits, blocks sit side by side (joined by ` · `); if not, the **whole block** moves to the next line; only a single block longer than a whole line degrades to wrapping inside the block (prefer breaking at spaces, hard break for CJK, drop an orphan `·` at line start, trim trailing spaces). Line boundaries only ever fall on block boundaries.
651
+ - **Row count is deterministic**: `statusBarRows()` measures the actual row count in pure logic and hands it to `computeChatLayout({ statusBarLines })`; measurement and rendering are the same function, so a taller status bar on a narrow screen never pushes the input line off screen.
652
+ - Numbers and units are bound with NBSP (`this round␣1284␣tok`), so they are not split across two lines inside a block either.
653
+ - **Two measures for the generation rate, never mixed into one number**: while generating, `⚡ 47 tok/s` is an estimate (derived from output deltas, CJK 0.7 / character, other non-whitespace 0.25 / character, whitespace 0.1, over a sliding window of the last 2 seconds); after a round ends it switches to `⚡ avg 38 tok/s`, where the denominator is the first-char → last-char span and the numerator is the real `usage.completion_tokens` returned by the API (accumulated round by round for multi-round tool calls). A span < 400ms is not shown, so a burst on the first packet cannot inflate the rate into the hundreds.
654
+ - **New `/agent`: switch the main Agent quickly inside a conversation** — the selector lists every main Agent in `~/.ai-zen/agents/*.json`, marking the current one and the model it is bound to (switching Agent means switching the model with it).
655
+ - Supports `/agent <id>` for a direct switch; no argument opens the selector.
656
+ - A full-chain precheck runs before switching: Agent exists → bound to a model → the model is still in the configuration → the endpoint exists → the endpoint has a key. Ones that cannot run are marked "unavailable" with a reason, instead of switching and then being stuck on the startup error screen.
657
+ - Writes `config.defaultAgent` (the same field as `/config` → "Default Agent") and then rebuilds the session; **messages and context are kept**. The Agent definition itself is not modified (prompt / permissions / `modelParams` all untouched).
658
+ - **New `/mcp`: jump straight to the MCP management screen** — no separate editor is built; the existing MCP screen in the configuration center is opened directly (add / delete / edit / enable-disable / global⇄project scope switching all live on that screen). After the wizard closes it reuses the existing `mcpChanged` path to call `session.reload()` automatically, so new servers take effect on the spot.
659
+ - Default landing: if the project scope has servers, go to `project`, otherwise `global` (entering the project scope when it is empty shows only an empty list). `/mcp global`, `/mcp project` (abbreviable to `g` / `p`) can specify it.
660
+ - **New `/reload`: refresh the runtime environment** — `session.reload()` contains `resetScope()` (disconnect the old MCP connections, drop the config snapshot) → rebuild the Agent → rediscover tools / Skills / MCP. After hand-editing `mcp.json` / `skills/` / `tools/`, one command makes it take effect, **messages and context are kept**, and the CLI need not be restarted.
661
+ - **Keep the reasoning and content already output when aborting** — after stopping with `Ctrl+C` / `Esc`, the half sentence the model **already emitted no longer vanishes into thin air**. Previously abort only marked the in-flight message `Aborted`, while the history filter let only `completed` through, so the reasoning and answer still visible on screen disappeared entirely from the next request, and the model started over the next round as if it had amnesia.
662
+ - The root cause is upstream: core's `abort()` only sets a flag and `formatHistory()` only filters by status, and neither cares about "what has already been output". core `4.5.3` adds `abortedMessageStrategy` (`drop` / `keep`, default `drop`) and exports `reconcileAbortedMessages()`, which settles pure content messages **after** tool rounds are normalized.
663
+ - The CLI sets it explicitly to `keep`: an aborted assistant message that really produced output is restored to `completed` and enters the context. **Empty output is not kept** (to avoid empty messages polluting the context) and **messages with tool calls are not its business** (they still follow "whole round in, whole round out", otherwise the history would be illegal with "a call but no result").
664
+ - The SDK dependency is upgraded to `1.0.0-alpha.20` accordingly (`createAgent` gains a third parameter `CreateAgentOptions`, which also surfaces the previously unexposed `abortedToolRoundStrategy` / `abortedToolRoundReason`), and core goes to `4.5.3`.
665
+ - **UI internationalization (Chinese / English)** — the first launch detects the system language (`LC_ALL` → `LC_MESSAGES` → `LANG` → Node's `Intl` locale, **English as the fallback**), the result is written to `~/.ai-zen/cli/preferences.json`, and the preference wins from then on (changing the system language later does not let the UI language drift quietly). `/lang` switches at any time; `/lang zh` / `/lang en` set it directly.
666
+ - Dictionaries are compiled into the bundle (`src/i18n/{zh,en}.ts` plus the configuration center section `{zh,en}-wizard.ts`): flattened keys, `{n}` interpolation; switching language swaps one reference, with no disk re-read and no restart. A missing entry returns the key itself (no silent cross-language fallback, which would make Chinese pop up in the English UI). `t()` supports `_one` / `_other` plural variants (chosen by `Intl.PluralRules`), but `i18n/index.test.ts` requires the **Chinese and English key sets to be identical**, so existing entries use the `(s)` form uniformly and no variants are enabled.
667
+ - The language preference lives in the CLI's own `preferences.json` and is **not stuffed into the shared `config.json`** — that is the `AppConfig` contract shared by the CLI and Desktop, and anything stuffed in there would be dropped by the desktop app as an unknown field.
668
+ - Coverage is **the whole TUI**: the main conversation screen (three status bar blocks, input placeholder and hints, notices, reasoning effort labels, generating hints), the command layer (`/help`, the slash menu, selectors and feedback of every command), every screen of the configuration center (endpoints / models / image models / MCP / Agent definitions / permissions / defaults / tool output limits / creation wizard / KV and multi-line text editors), the handoff mode selector and confirm boxes, the startup screen and the startup error screen, plus the summaries returned by business modules (the switch results of `/model` `/agent` `/think`, handoff progress and downgrade hints).
669
+ - `COMMAND_REGISTRY` stores **i18n keys rather than display text** — it is a pure data module loaded before language initialization, so storing text would freeze "the language at load time"; rendering calls `getCommandHints()` to fetch it on the spot. The startup screen's `TAGLINE` constant became a `tagline()` function for the same reason.
670
+ - **Pipeline mode does not do i18n**: all output and errors of `ai <prompt>` / piped input are **English only** (it is for scripts and should not follow a person's UI language), so the stdio path **does not initialize the language** and its behavior is fully decoupled from `preferences.json`.
671
+ - **`ai --help` and `ai hook install / uninstall` go through i18n**: both print outside the TUI, so each branch calls `initLocale()` before fetching entries; the module-level `HELP_TEXT` constant became `helpText()` assembled on demand (a constant would freeze the language at load time).
672
+ - **Conversation id renamed**: the old localized `dialogue_<date and time with Chinese units>` form → `chat_2026_10_28_00_56_34` (English prefix + all-underscore timestamp, identical in Chinese and English, filename-safe). It is also the file name, and letting it follow the language would mix old and new history files, so the timestamp is produced by the new `formatFileStamp()`; **display** times, by contrast, now follow the language via `formatShortTime()`. Pipeline mode's `--save` no longer assembles a name itself and calls `generateConversationId()` instead, removing the duplicated assembly in both places.
673
+ - **The "working directory" note injected into history switched to English with an explicit marker**: `[system-injected: working directory changed — not written by the user]` — flipping through an archive or a handoff document makes it obvious at a glance which content the user did not type; `stripCwdNote()` recognizes both generations of the format, so old archives do not render this note on screen.
674
+ - The `task` parameter description written into the tool schema when creating a sub-agent also comes from the dictionary (Chinese `Complete task description` / English `Full task description`) — the only exception where "protocol content is translated too"; the cost is that the same definition created under different UI languages yields different schema text.
675
+ - The only injected content left untranslated is the working-directory note above; the three code-block marker strings `hook.ts` writes into the rc file (such as `# === ai hook (开始) ===`) also stay as they are — translated, the old block could never be found again.
676
+ - Time formats switch dayjs locale and pattern along with the language: Chinese renders a localized month-day form plus time with a "yesterday" prefix, English `Jul 1, 14:30` / `Yesterday 14:30`.
677
+ - Two hidden mines were cleared along the way: the status bar used to decide whether reasoning effort is dimmed by **display text** (`effort === "off"`), which never held in the English UI, and now uses `effortDimmed()` based on the level's semantics; the input area placeholder used to be hardcoded inside `inputLines()` (while the render layer went through `t()`), so in the English UI the measured input row height came out one line short and dragged the hardware cursor's y offset along — both now share one source.
678
+
679
+ ### 🔧 Behavior changes
680
+
681
+ - **The message row's "thinking…" split into six states: connecting / queued / thinking / calling a tool / waiting for a tool result / streaming** — that row used to have only the single word "thinking…", but a model actually goes through several stages within one round, and summing them up with one word is **plain wrong** most of the time: it is not thinking while waiting for the first token, and while a tool runs the model is not running at all.
682
+ - The predicate uses only **the shape of events and deltas** (`src/tui/phase.ts`, pure logic and unit-testable): `connecting` = from request dispatch to response headers; `queued` = after `open` and before the first content chunk; `thinking` = `reasoning_content` received; `tool-calling` = `tool_calls` / `function_call` received (with a tool name, reusing the name from the first delta when later chunks carry only argument fragments); `tool-waiting` = `parsed` and this round has tool calls; `streaming` = `content` received. When several appear in one chunk the priority is tool_calls > content > reasoning.
683
+ - The "connecting" stage is provided by the new `PhasePlugin` (`chat-session.ts`): the SDK's `open` event is emitted **after the `await` of `chat.completions.create()` returns** and only means the response headers arrived; the DNS / TCP / TLS / server acceptance before that is entirely silent.
684
+ - **Sub-agent output does not advance the main stage**: during delegation the main Agent is waiting for a tool result, and counting the sub-agent's deltas into the main stage would make the UI jump to "streaming", covering exactly the sentence the user most needs to know. Sub-agent chunks still count toward the rate and still trigger a redraw.
685
+ - The spinner row got `truncate`: while busy the input area height is fixed at 1 row by `computeChatLayout` (`inputHeight = 1`), and a wrapped label would push the input line off screen.
686
+ - **"⏳ Generating" removed from the status bar** — what this round is doing is already explained stage by stage by the message row spinner, and another generic "generating" in the status bar only duplicates it and eats width (on a narrow screen it also squeezes the keybinding hints onto the next line).
687
+
688
+ - **Shell hook switched to entering the TUI (the earlier pipeline mode was an implementation error)** — the hook is installed on `command_not_found_handle` / `command_not_found_handler`, and the triggering situation is "a person sitting at an interactive terminal typed a command wrong", yet the function body was `ai "$@"`, while the rule in `decideMode()` is "positional arguments → stdio", so the hook session inevitably fell into pipeline mode: it spat some text to stdout and exited, **with no history and no follow-up questions**. The classification table at the top of `mode.ts` merged "script invocation" and "manually triggered by a person" into one predicate, so this was not something a patch could fix — the classification itself was wrong.
689
+ - The function body is now `ai --tui --new "$@"`: the new `--tui` flag explicitly overrides the "has arguments → stdio" decision, enters the TUI and **sends the original command automatically as the first message** (not pre-filled into the input line — the semantics of the hook are "this command cannot run, handle it for me", and making the user press Enter again is pointless).
690
+ - The first message is **forced to be sent as an ordinary user message** and skips slash-command parsing: the mistyped command could be something like `/opt/bin/foo` starting with `/`, which would otherwise be treated as a CLI command and reported as "unknown command". `runSubmit()` gained an `asMessage` option for this.
691
+ - In non-TTY environments (scripts / pipes) `--tui` reports an error and exits (exit code 2, English message) and does **not** degrade into pipeline mode — otherwise a whole screen of UI content would be written into the pipe.
692
+ - The first message is sent only on the first mount: remounts caused by switching models / `/lang` do not resend it.
693
+ - **Hook sessions always start a new conversation**: the function body has an extra `--new` (`ai --tui --new "$@"`). Otherwise one mistyped command would be appended to the long conversation that already existed in that directory, scrambling both the context and the conversation name. `--new` is a general flag and works the same when used manually: `ai --new` skips the `last-session.json` pointer and starts a new conversation; it **only affects the TUI path** and is silently ignored on the stdio path (a stdio invocation is a one-shot conversation anyway, so there is nothing to continue).
694
+ - **`ai hook install` now upgrades itself**: previously it printed "installed" and returned as soon as `MARKER_START` existed in the rc file, so after the function body changed (say, `--new` added this time) existing users could never upgrade. It now normalizes the installed blocks and the current version and compares them (blank lines removed, every line trimmed), overwrites in place when they differ, and reports "updated to the latest version" (new entry `hook.updated`).
695
+
696
+ - **`/migration` is no longer an alias of `/migrate`** — only `/migrate` remains, and `/migration` now reports "unknown command". The registry, the command executor, the `blocksInput` decision and the four documents were updated together.
697
+ - **SDK global logs go to `~/.ai-zen/logs/sdk.log` by default** — previously they were **discarded** by default, and only `AIZ_DEBUG_SDK=1` wrote them to `/tmp`. But the SDK's `warn` / `error` carry critical diagnostics such as "MCP cannot connect" and "Skill failed to parse", and discarding them by default meant failures left no trace. They are now written by default (rotated to `sdk.log.1` after a 2MB cap per file), `AIZ_DEBUG_SDK=0` turns it off completely, and `AIZ_SDK_LOG_FILE` changes the path. The UI still shows not a single character of it.
698
+
699
+ ### 🐛 Fixes
700
+
701
+ - **Fixed the IME candidate window being "sometimes mispositioned"** — the hardware cursor position used to be computed by `displayWidth()`. But width is a trap: Ink lays out with `string-width`, and the hand-written table disagreed with it on several characters (`⚡` `✅` `✨` `⭐` were counted as 1 column against Ink's 2; the ZWJ emoji `👨‍👩‍👧` as 8 columns against 2; the keycap `1️⃣` as 3 against 2), so as soon as such a character appeared in the input, x drifted. It is now **measured after rendering**:
702
+ - The character at the cursor is wrapped in its own `<Box ref>` (inline nested `<Text>` fragments have no node in yoga and `measureElement()` returns `null` for them, verified by experiment), and Ink 7's `measureElement()` reads the absolute coordinates it is imprinted at within the frame — width is computed by the renderer itself and no longer passes through the hand-written model.
703
+ - After splitting rows, the previous whole-row `truncate` fallback would be lost: once the content is too wide (e.g. the width model counts `⚡` as 1 column) the row is pushed into two rows, the whole frame grows a row and the bottom is pushed off screen. The solution hands the overflow to the **truncatable adjacent segments** — `before` / `after` use `flexShrink={1} minWidth={0}` + `wrap="truncate"` and the cursor block uses `flexShrink={0}`; measured with a 40-column container and a 60-column `before`, it was still **one row**.
704
+ - Measurement can only happen after rendering (`useEffect`), while Ink pushes `setCursorPosition` only in the commit phase (`useInsertionEffect`), so there is a one-commit delay: the first render puts up one frame with the old hand-computed value, and the next frame is corrected by the measured value (one extra render pass, which Ink throttles itself).
705
+ - The placeholder row (nothing typed yet) does not go through measurement and still uses the fixed position after the prefix.
706
+ - **The placeholder state no longer draws a "fake cursor"** — when the input line is empty it shows the gray hint "Type a message, / for commands…", and a `▏` used to be drawn at the end as well. But the hardware cursor already sits at the start of the hint (`x = prefix width`), the terminal displays it on its own, and drawing another block means two cursors on screen — and positioned at the end of the hint, which is misleading. The placeholder state now keeps only the hint text.
707
+ - **Fixed "the inverse cursor disappears / the real cursor is misaligned after typing emoji"** — two root causes, both treating a "character" as a code point:
708
+ - **The cursor could stop in the middle of a grapheme cluster**: `✌️` is `U+270C U+FE0F`, **two code points, one grapheme cluster** (`👨‍👩‍👧` five, `🇨🇳` two regional indicators). When splitting by code point the cursor could stop between `✌` and `U+FE0F`, so the inverse display became a string of **variation selectors** — in Ink's output the inverse segment disappears entirely (measured: with `at=U+FE0F` the frame carries neither the inverse escape nor any character). Backspace also deleted only half an emoji and left an orphan `U+FE0F`. All cursor arithmetic (wrapping, moving, deleting, inserting, undoing) now uses `Intl.Segmenter`'s **grapheme clusters** as the unit (`graphemes()`, built into Node with no dependency).
709
+ - **The width table switched to Ink's own ruler**: `displayWidth()` no longer uses the hand-written `isWide` range table but calls **`string-width`** directly (that is what Ink lays out with, see `ink/build/output.js`); per-cluster widths are cached, so the wrapping hot path costs almost nothing. From now on wrapping, status bar packing, cursor column numbers and the renderer share one source — and the status bar's "reserve 1 extra column" degraded to a pure safety margin.
710
+ - **Cursor unit changed to grapheme clusters**: `✌️` is `U+270C U+FE0F`, **two code points, one grapheme cluster** (`👨‍👩‍👧` five, `🇨🇳` two regional indicators). When splitting by code point the cursor could stop between `✌` and `U+FE0F`, so the inverse display became a string of **variation selectors** — in Ink's output the inverse segment disappears entirely (measured: with `at=U+FE0F` the frame carries neither the inverse escape nor any character). Backspace also deleted only half an emoji and left an orphan `U+FE0F`. All cursor arithmetic (wrapping, moving, deleting, inserting, undoing) now uses `Intl.Segmenter`'s **grapheme clusters** as the unit (`graphemes()`, built into Node with no dependency).
711
+ - **New direct dependency** `string-width@^8.3.0` (pure ESM, already in Ink's dependency tree, no size change).
712
+ - **Notice bar rows were not counted in the cursor y** — the render order is "conversation area → notice bar → slash menu → separator → input row", and the y formula was missing the notice bar. So whenever a notice appeared the hardware cursor floated up by that many rows; `/help` pushes up to 8 rows of notices at once, and the candidate window could float into the conversation area. `noticeCount` is now included (kept as the fallback algorithm while the measurement is not ready).
713
+ - **Fixed SDK logs "taken over and still smearing the UI"** — when loading MCP, `[load_mcp]` along with the whole tools / resources listing smashed straight outside the render area. The reason was that 4 SDK modules run `const log = getLogger()` **at module top level**: the moment the module is evaluated the `console.log` function reference is frozen into a constant, and `setLogger()` only swaps an internal variable afterwards, unable to change the reference already captured; ESM static imports also guarantee those modules are evaluated before `runTui()` — so no matter how the CLI took over, it leaked.
714
+ - Fixed on the SDK side (not with a CLI-side workaround that lies to console): all 4 sites now **take the logger at runtime** (`getLogger().info(...)` and so on); the `skillTools` site was dead code that was never called and was removed along with its import. All 10 log calls inside the SDK now go through the single `getLogger()` exit; no console bypass remains.
715
+
716
+ ### 📝 Docs
717
+
718
+ - README (zh / en) and `docs/{zh,en}/getting-started.md`: the command table gained three rows, `/agent`, `/reload`, `/mcp`.
719
+ - README (zh / en) and `docs/{zh,en}/index.md`: the status bar description changed from "one priority-ordered stream of text" to "three information blocks + per-block wrapping".
720
+ - `docs/{zh,en}/mcp.md`: the "Management" section gained the `/mcp` direct entry and the `/reload` refresh; `docs/{zh,en}/migration.md` dropped the `/migration` alias.
721
+ - README (zh / en) gained a "UI language" section: detection priority, `preferences.json` and `/lang`, coverage, and the three kinds of content deliberately left untranslated.
722
+ - Version numbers in `docs/manifest.json` and the READMEs updated to `1.0.0-alpha.21`.
723
+ - README (zh / en): "Environment variables" gained `AIZ_DEBUG_SDK=0` / `AIZ_SDK_LOG_FILE`, "Filesystem layout" gained `~/.ai-zen/logs/`, and a note was added under the keybinding table saying reasoning and content already output are kept after an abort.
724
+ - README (zh / en) and `docs/{zh,en}/getting-started.md`: the command table gained `/lang`; the directory trees in "Filesystem layout" and `docs/{zh,en}/configuration.md` gained `~/.ai-zen/cli/preferences.json` (CLI-local preferences).
725
+
726
+ ### ⚠️ Known trade-offs
727
+
728
+ - **Emoji render width may still disagree with the terminal, and that is the terminal layer's business**: the code side now shares one source with the renderer (`displayWidth` = the `string-width` Ink uses, cursor position measured with `measureElement`), but **the terminal's own column-width judgment for emoji / East Asian Ambiguous characters** may still differ from the Unicode tables (some terminals render `✌️` as 1 column instead of 2, and offer no query interface). In that case the relative position of the whole line is already decided by the terminal, and the cursor can only follow Ink's model. The emoji problems at the input / delete / cursor-unit level are all fixed in this release.
729
+ - **The cursor is "terminal cursor + inverse character" side by side, deliberately**: the character at the cursor is still inverted into a block with `<Text inverse>`, and the source of truth for position is the measured hardware cursor. Visually the two stack into a slightly heavy block — accept the ghosting, because when either layer fails the other can still report the position (some terminals / multiplexers do not show the hardware cursor, or the inverse display is gone when Ink unmounts). Do not delete the inverse as duplicate rendering later.
730
+ - **Blocks are not split ⇒ row count varies with width**: at 80 columns the three blocks each take a row (3 rows), at 120 columns identity and usage can sit on one row (2 rows in total). The old "one stream" could be squeezed into 2 rows, at the cost of splitting `this round / cumulative` across two rows — information integrity wins.
731
+ - **The rate while generating is an estimate**: the SDK only provides `usage` in the **last chunk** of each request (`stream_options.include_usage`) and offers no per-token usage; mixing estimates and exact values into one number is the misleading option, hence the two forms "real-time estimate" and "round average".
732
+ - **`/mcp` only covers the configuration layer**: whether a server is currently reachable and how many tools it exposes is plugin-private state in the SDK (`McpScopePlugin`'s `mcpManager`) with no public API, so the UI does not show connection status.
733
+ - **`/reload` has a gap of a second or two**: it rebuilds the Agent and reconnects MCP, so with many servers there is a perceptible delay; messages and context are unaffected during it.
734
+
735
+ ### ✅ Tests
736
+
737
+ - Full run: **33 files / 440 cases passed**, `npx tsc --noEmit -p tsconfig.test.json` clean.
738
+ - New `src/i18n/index.test.ts` (14 cases: language detection priority / `C`·`POSIX` skipped / English fallback / interpolation / missing entry falls back to the key / Chinese and English entries correspond one to one) and `src/i18n/preferences.test.ts` (9 cases: tolerance of a missing, corrupt or invalid file, atomic write, `initLocale` detecting only once and persisting); `reasoning-effort.test.ts` gained 5 cases for "dimming does not rely on display text".
739
+ - pty acceptance on a real terminal: `LANG=en_US.UTF-8` first launch → English, with `en` persisted; `/lang` to Chinese → status bar / input line / notices all follow, and **Chinese is kept after a restart** (the preference beats the system language); `/lang en` back to English works.
740
+ - pty acceptance (stage 2): in English the slash menu shows `❯ /help Show this help` and `/help` outputs an all-English list; after `/lang zh` the slash menu and `/help` switch back to Chinese as a whole.
741
+ - pty acceptance (stages 3 / 4, isolated `AI_ZEN_DIR` + `LANG=en_US.UTF-8`): first launch detected as English and persisted; the startup screen shows `The AI collaboration desk in your terminal`; the main screen shows `Type a message, / for commands…` and `thinking default · total 0 tok`; the configuration center shows `◆ Configuration` with `+ New model — marked as custom automatically` / `openai · factory-managed · vision`; the model detail screen shows `◆ Model · GPT-6 Astra` / `factory-managed (editing switches it to custom)` / `Delete model`; the new-model and new-endpoint wizard screens are entirely English. After setting `preferences.json` back to `zh`, **a restart under `LANG=en_US.UTF-8` still shows the Chinese UI** (the preference beats the system language), and the Chinese text in the configuration center and model list matches the pre-rework state.
742
+ - **Tests asserting Chinese always pin the language explicitly**: `setLocale("zh")` added in `screens.test.tsx`, `config-wizard.test.tsx`, `reasoning-effort.test.ts`, `registry.test.ts`, `agent-prompt.test.tsx`, `handoff-choice.test.ts`, `config-editor.test.ts`, `agent-switch.test.ts`, `model-switch.test.ts`, `handoff-service.test.ts`, `auto-migrate-confirm.test.ts` — the default language is `en`, and without pinning they would go red on CI for no apparent reason.
743
+ - `mode.test.ts` gained 6 cases for `--tui`: with arguments it enters the TUI and the arguments become the first message; without arguments it behaves the same as without `--tui`; a command starting with `/` is not taken as an option / sub-command; `--tui --help` gives help priority; non-TTY is rejected rather than degraded; a `--tui` after `--` is an ordinary argument.
744
+ - `mode.test.ts` gained 8 cases for `--new`: used alone it enters the TUI with `newSession: true`; `--tui --new <command>` carries both the first message and the new session; the order relative to `--tui` does not matter; with arguments it falls to stdio and is ignored; in non-TTY it falls to stdio and is ignored; `--tui --new` in non-TTY is still rejected rather than degraded; `--new --help` gives help priority; a `--new` after `--` is an ordinary argument.
745
+ - New `src/hook.test.ts` (9 cases, all run under a temporary HOME, **never touching the real `~/.bashrc`**): the written function body contains `ai --tui --new "$@"` and exactly one block, existing rc content is preserved, repeated installation is idempotent, **an old function body already installed (`ai --tui "$@"`) is upgraded in place**, historical aiz blocks are migrated, zsh uses `command_not_found_handler` and writes to `.zshrc`; uninstalling removes the block and keeps the rest, leaves the file alone when nothing is installed, and does not error when the rc file does not exist.
746
+ - pty acceptance (hook path): `ai --tui gti status` enters the TUI and automatically sends `gti status` as a user message (the UI shows `❯ you / gti status`), and after the model errors out (a deliberate local fake endpoint) the UI survives and further questions are possible; `ai --tui /opt/bin/nonexistent --flag` is likewise sent as an **ordinary message** and is not eaten by slash-command parsing; `ai --tui` in non-TTY exits with code 2; the function body written to `~/.bashrc` is `ai --tui --new "$@"` and passes `bash -n` syntax checking.
747
+ - pty acceptance (`--new`, isolated `AI_ZEN_DIR` + a preset `last-session.json` pointing at an old conversation): `ai` continues it and shows the old message `MARKER-OLD-QUESTION`; `ai --new` lands in an **empty conversation**; `ai --tui --new gti status` starts empty and sends the first message automatically; `--help` lists `--new` in both the Chinese and English UI.
748
+ - Full run after this change: **36 files / 487 cases passed**, `npx tsc --noEmit` and `-p tsconfig.test.json` both clean.
749
+ - New `src/tui/phase.test.ts` (15 cases: the progression of each of the six states, tool name reuse, a trailing chunk without deltas not changing the stage, content beating reasoning, `reset` returning to zero, the six Chinese and English strings all differing from each other); `chat-session.test.ts` gained 3 cases (progression following the SDK event order, sub-agent output not overriding the main stage, the stage returning to zero when `send` ends).
750
+ - pty acceptance (a local mock SSE server emitting the stream in stages): "connecting…" → "queued… (waiting for the first token)" → "thinking…" → "streaming…" → "calling tool no_such_tool…" → "waiting for sub_agent_default…" were all observed in order; the status bar carried only `⚡ 4.2 tok/s` and this round / cumulative tokens over the same period, with no "generating".
751
+ - New `src/agent-switch.test.ts` (10 cases), `src/mcp-command.test.ts` (6 cases), `src/tui/token-rate.test.ts` (20 cases), `src/tui/status-segments.test.ts` (17 cases), `src/tui/screens/input-row.test.tsx` (7 cases), `src/tui/text.test.ts` (18 cases); `input-editing.test.ts` gained 3 cases for "an emoji is never cut in half".
752
+ - `src/tui/screens.test.tsx` gained assertions for the order of the three status bar blocks, blocks not being split, side-by-side layout on wide screens, and "the row count Ink actually renders == the row count `statusBarRows` measures"; `computeInputCursorPosition` gained a case for "notice bar rows participate in y".
753
+ - The `⚡` case in `input-row.test.tsx` asserts both "hand-computation gives 8, measurement gives 9" — the reason for this change is in those two lines.
601
754
 
602
755
  ## [1.0.0-alpha.20] - 2026-10-27
603
756
 
604
- ### ✨ 新功能
757
+ ### ✨ New features
605
758
 
606
- - **对话屏支持鼠标滚轮滚动** —— 此前滚轮只能滚终端自己的回滚缓冲(`alternateScreen: false` 走内联渲染),滚上去看到的是历史帧快照,状态栏与输入框会直接滚出视野。
607
- - 新增 `src/tui/mouse.ts`:挂载时下发 `DECSET 1000`(按钮事件,含滚轮)+ `1006`(SGR 扩展坐标);`mount()` 的退出、`farewell()`、信号与未捕获异常兜底**三处**都下发关闭序列,进程崩了也不会把 `^[[<64;…M` 喷进 shell。
608
- - `parseWheel()` 剥掉 Shift(4) / Meta(8) / Ctrl(16) 修饰位后取滚轮码(`64` 上、`65` 下),横向滚轮 `66` / `67` 直接忽略;一格滚 3 行。
609
- - 滚轮与 `PgUp`/`PgDn`、按行滚动共用同一套视口钳制 —— 抽出纯函数 `clampViewTop()`(`screens/layout.ts`),越界方向自然吃掉,返回 `null` 即贴底跟随最新内容。生成过程中滚动不会被拉回底部。
610
- - **`isTerminalReply()` 纳入鼠标序列**:点击 / 拖拽 / 滚轮一律在源头丢弃。这一步是必须的 —— Ink 会把整条 SGR 序列当 `char` 交给 `useInput`(实测为 `"[<64;10;5M"`,所有键标志位 false),不拦就会落进输入框变成正文。
611
- - 必须用 `1006`(SGR 坐标)而不是老的 X10 编码:后者被 Ink 的解析器拆成 `[M` + 三个普通字符两条事件,三个字符会直接被当正文插入(已实测)。
612
- - 启动屏「任意键跳过」不再把鼠标事件当成按键。
759
+ - **The chat screen supports mouse wheel scrolling** — previously the wheel could only scroll the terminal's own scrollback buffer (`alternateScreen: false` uses inline rendering), and scrolling up showed snapshots of historical frames while the status bar and input line scrolled out of view entirely.
760
+ - New `src/tui/mouse.ts`: on mount it emits `DECSET 1000` (button events, wheel included) + `1006` (SGR extended coordinates); the close sequence is emitted in **three** places — `mount()`'s exit, `farewell()`, and the signal / uncaught exception fallback — so a crashing process does not spray `^[[<64;…M` into the shell.
761
+ - `parseWheel()` strips the Shift(4) / Meta(8) / Ctrl(16) modifier bits and takes the wheel code (`64` up, `65` down); horizontal wheel `66` / `67` is ignored outright; one notch scrolls 3 rows.
762
+ - The wheel shares one viewport clamp with `PgUp`/`PgDn` and line-wise scrolling — extracted into the pure function `clampViewTop()` (`screens/layout.ts`); scrolling against the boundary is absorbed naturally, and returning `null` means "glued to the bottom, following the newest content". Scrolling during generation is not snapped back to the bottom.
763
+ - **`isTerminalReply()` now covers mouse sequences**: clicks / drags / wheel events are dropped at the source. This step is mandatory — Ink hands the whole SGR sequence to `useInput` as `char` (measured as `"[<64;10;5M"`, all key flags false), and without filtering it lands in the input line as text.
764
+ - `1006` (SGR coordinates) must be used rather than the old X10 encoding: Ink's parser splits the latter into two events, `[M` plus three ordinary characters, and those three characters get inserted as text directly (verified by experiment).
765
+ - The startup screen's "press any key to skip" no longer treats mouse events as keys.
613
766
 
614
- ### 📝 文档
767
+ ### 📝 Docs
615
768
 
616
- - README(中 / 英)键位表新增「鼠标滚轮」一行,并写明两个坑:开启上报后**拖选复制需按住 `Shift`**(回退到终端原生选择);**`tmux` 用户需 `set -g mouse off`**,否则滚轮会先被 copy-mode 拦下。
617
- - `docs/manifest.json`、`docs/{zh,en}/getting-started.md`、README 中的版本号与运行时依赖版本对齐(`agents-core ^4.5.2`、`agents-sdk 1.0.0-alpha.18`)。
769
+ - README (zh / en): the keybinding table gained a "mouse wheel" row, documenting two gotchas: with reporting enabled **drag-select copy requires holding `Shift`** (falling back to the terminal's native selection); **`tmux` users need `set -g mouse off`**, otherwise the wheel is intercepted by copy-mode first.
770
+ - Version numbers and runtime dependency versions in `docs/manifest.json`, `docs/{zh,en}/getting-started.md` and the READMEs aligned (`agents-core ^4.5.2`, `agents-sdk 1.0.0-alpha.18`).
618
771
 
619
- ### ⚠️ 已知取舍(终端机制使然,非缺陷)
772
+ ### ⚠️ Known trade-offs (inherent to the terminal, not defects)
620
773
 
621
- - 鼠标上报是终端级开关:点击 / 拖拽不再交给终端做原生选择,复制需按住 `Shift`(iTerm2 / Windows Terminal / kitty 等均如此)。
622
- - 应用内滚动只动视口,不产生终端 scrollback —— 想回看历史用滚轮 / `PgUp`,而不是终端的回滚条。
623
- - 老版 Windows `conhost` 不支持 `1006`,滚轮在那种终端上表现为无反应。
774
+ - Mouse reporting is a terminal-level switch: clicks / drags no longer go to the terminal for native selection, and copying requires holding `Shift` (true for iTerm2 / Windows Terminal / kitty and others).
775
+ - In-app scrolling only moves the viewport and produces no terminal scrollback — to review history use the wheel / `PgUp`, not the terminal scrollbar.
776
+ - Old Windows `conhost` does not support `1006`, so the wheel appears to do nothing in that terminal.
624
777
 
625
- ### ✅ 测试
778
+ ### ✅ Tests
626
779
 
627
- - 全量 **25 文件 / 316 例通过**(新增 `src/tui/mouse.test.ts` 1 文件 11 例;`screens.test.tsx` 新增滚轮解析、`clampViewTop` 钳制、启动屏鼠标守卫三组用例)。
628
- - 真机(pty)端到端验证:滚轮方向解析、修饰位剥离、点击丢弃、普通打字不受影响、退出时下发关闭序列。
780
+ - Full run: **25 files / 316 cases passed** (new `src/tui/mouse.test.ts`, 1 file, 11 cases; `screens.test.tsx` gained three groups: wheel parsing, `clampViewTop` clamping, startup screen mouse guard).
781
+ - End-to-end check on a real terminal (pty): wheel direction parsing, modifier stripping, clicks dropped, ordinary typing unaffected, close sequence emitted on exit.
629
782
 
630
783
  ## [1.0.0-alpha.19] - 2026-10-22
631
784
 
632
- ### 🐛 修复
785
+ ### 🐛 Fixes
633
786
 
634
- - **修复升级后首次启动报「Agent「default」未绑定模型」** —— alpha.18 把模型真相源改成 `AgentDefinition.modelId`,并把「为历史 Agent 补齐 modelId」放在 `bootstrap()` 里。但 TUI 的**续接会话**路径(`resumeLastSession()` 命中上一轮存档)根本不会经过 `resolveTarget()`,也就不会读取配置,于是 `bootstrap()` 从未执行:Agent 还是没绑模型、`config.defaultModel` 也还在,启动直接失败。
635
- - `runTui()` 开头显式 `await ensureConfigDir()`,配置初始化早于任何 Agent 读取;
636
- - `modelOf()` 自身也先读一次配置(双重保险),即使调用方漏了也不会再撞到这个错误。
637
- - 新增回归测试:老配置(有 `defaultModel`、Agent 无 `modelId`)读一次配置后,模型被搬到 Agent 上、`config.json` 里的旧字段被清除。
787
+ - **Fixed the "Agent 'default' is not bound to a model" error on the first launch after upgrading** — alpha.18 moved the model source of truth to `AgentDefinition.modelId` and put "backfill modelId for historical Agents" inside `bootstrap()`. But the TUI's **session continuation** path (`resumeLastSession()` hitting the previous archive) never goes through `resolveTarget()` and therefore never reads the configuration, so `bootstrap()` never ran: the Agent still had no model, `config.defaultModel` was still there, and startup failed outright.
788
+ - `runTui()` now explicitly `await ensureConfigDir()` at the start, so configuration initialization precedes any Agent read;
789
+ - `modelOf()` also reads the configuration once itself (double insurance), so a missed caller can no longer hit this error.
790
+ - New regression test: with an old configuration (has `defaultModel`, Agent has no `modelId`), after one configuration read the model is moved onto the Agent and the old field in `config.json` is cleared.
638
791
 
639
- ### ✅ 测试
792
+ ### ✅ Tests
640
793
 
641
- - 全量 **24 文件 / 299 例通过**。
794
+ - Full run: **24 files / 299 cases passed**.
642
795
 
643
796
  ## [1.0.0-alpha.18] - 2026-10-22
644
797
 
645
- ### 🐛 修复
798
+ ### 🐛 Fixes
646
799
 
647
- - **修复「模型显示与实际请求不是同一个」导致的 401 死锁** —— 此前模型有三处来源:状态栏与会话存档里的 `modelId`、Agent 定义里的 `modelId`、以及配置里的 `defaultModel`。三者一旦分叉,就会:
648
- - 状态栏写着 `deepseek-flash`,请求实际发给 GLM,而 bigmodel 的 key 无效 → 看到 401;
649
- - 想切回 DeepSeek 时,`/model` 拿**显示层**的值判断,发现「已经是当前模型」直接返回,实际模型一个字都没改——于是怎么切都还是 401,只能删配置。
800
+ - **Fixed the 401 deadlock caused by "the displayed model is not the model actually requested"** — the model used to come from three places: the `modelId` in the status bar and the session archive, the `modelId` in the Agent definition, and the `defaultModel` in config. Once the three diverged:
801
+ - the status bar said `deepseek-flash`, the request actually went to GLM, and bigmodel's key was invalid → 401;
802
+ - when switching back to DeepSeek, `/model` judged by the **display layer** value, found that it was "already the current model" and returned immediately, without changing the actual model at all — so no amount of switching fixed the 401, and the only way out was deleting the config.
650
803
 
651
- 现在模型**只存在 `AgentDefinition.modelId` 一处**,状态栏、凭据预检、上下文阈值与 SDK 实发请求全部读它。
804
+ The model now **exists in exactly one place, `AgentDefinition.modelId`**, and the status bar, the credential pre-check, the context thresholds and the request the SDK actually sends all read it.
652
805
 
653
- ### 🔧 行为变更(不兼容)
806
+ ### 🔧 Behavior changes (incompatible)
654
807
 
655
- - **升级 `@ai-zen/agents-sdk` 至 `1.0.0-alpha.17`**:配置层不再有 `defaultModel`,缺失 `modelId` 的 Agent 会直接报错(默认 Agent 出厂自带 `deepseek-flash`);历史配置升级时会把老的 `defaultModel` 搬进默认 Agent,不改变你正在用的模型。
656
- - **会话存档不再记 `modelId`** —— 续接对话时模型完全由 Agent 定义决定;旧存档里的该字段读取时忽略。
657
- - **`/model` 只写 Agent 定义** —— 不再有「Agent 未绑定就改全局默认模型」的分支。
658
- - **切模型即重置思考强度** —— `AgentDefinition.modelParams` 在切模型时**直接清空**(不保留、不迁移、不逐键剔除),让目标模型走自己的 `defaultParams`。原因:思考参数是模型相关的,DeepSeek 关思考的 `thinking: {type:"disabled"}` 带到 GLM-5.3 上会被直接拒绝——此前会出现「一个不肯关思考的模型 + 一个说要关的参数」同时发出去。想调再用 `/think` 选一次。
659
- - **配置中心移除「默认模型」项** —— 模型改在 Agent 详情页的「模型」字段里选(影响范围从「全局默认」变成「该 Agent」)。
808
+ - **Upgraded `@ai-zen/agents-sdk` to `1.0.0-alpha.17`**: the config layer no longer has `defaultModel`, and an Agent missing `modelId` errors out immediately (the default Agent ships with `deepseek-flash`); when upgrading historical configs, the old `defaultModel` is moved into the default Agent, so the model in use does not change.
809
+ - **Session archives no longer record `modelId`** — when a conversation is resumed, the model is determined entirely by the Agent definition; the field in old archives is ignored on read.
810
+ - **`/model` only writes the Agent definition** — the branch that changed the global default model when the Agent had no binding no longer exists.
811
+ - **Switching models resets reasoning effort** — `AgentDefinition.modelParams` is **cleared outright** when the model is switched (no retention, no migration, no per-key removal), so the target model uses its own `defaultParams`. Rationale: thinking parameters are model-specific, and DeepSeek's `thinking: {type:"disabled"}` for turning thinking off is rejected outright by GLM-5.3 — previously "a model unwilling to turn thinking off + a parameter saying to turn it off" could be sent together. To tune it again, select once more with `/think`.
812
+ - **Removed the "default model" entry from the configuration center** — the model is now chosen in the "model" field of the Agent detail screen (scope changes from "global default" to "that Agent").
660
813
 
661
- ### ✅ 测试
814
+ ### ✅ Tests
662
815
 
663
- - 全量 **24 文件 / 298 例通过**。
664
- - 新增 `model-switch` 用例:切换即清空 `modelParams`(不是只剔除思考键)、原本没有也不多写空对象。
816
+ - Full run of **24 files / 298 cases passed**.
817
+ - New `model-switch` cases: switching clears `modelParams` (not merely removing the thinking keys), and no extra empty object is written when there was none.
665
818
 
666
819
  ## [1.0.0-alpha.17] - 2026-10-22
667
820
 
668
- ### 🔧 行为变更
821
+ ### 🔧 Behavior changes
669
822
 
670
- - **升级 `@ai-zen/agents-sdk` 至 `1.0.0-alpha.16`(内含 core `4.5.2`):模型参数不再能覆盖请求体的固定字段** —— core 之前把模型参数 spread 在请求体最外层,于是 `Model.defaultParams` / `AgentDefinition.modelParams` 里若出现 `model` / `messages` / `tools` / `stream` / `stream_options`,会静默覆盖框架自己填的值。最惨的是 `stream: false`:流式被关掉而调用方仍按异步流解析,整轮直接失败。现在固定字段永远胜出。
671
- - 对 `/think` 与思考档位**没有影响**:写入的都是 `thinking` / `reasoning_effort` 这类非固定键,照常透传。
672
- - 唯一需要注意的是别在 `modelParams` 里手写上面那五个键——写了也不会生效。
823
+ - **Upgraded `@ai-zen/agents-sdk` to `1.0.0-alpha.16` (bundling core `4.5.2`): model parameters can no longer override the fixed fields of the request body** — core previously spread the model parameters at the outermost level of the request body, so if `model` / `messages` / `tools` / `stream` / `stream_options` appeared in `Model.defaultParams` / `AgentDefinition.modelParams`, they silently overrode the values the framework itself filled in. Worst of all was `stream: false`: streaming was turned off while the caller still parsed an async stream, failing the whole turn. Fixed fields now always win.
824
+ - This has **no effect** on `/think` and the effort levels: what gets written are non-fixed keys such as `thinking` / `reasoning_effort`, which pass through as before.
825
+ - The only thing to watch is not to hand-write those five keys in `modelParams` — writing them has no effect.
673
826
 
674
827
  ## [1.0.0-alpha.16] - 2026-10-22
675
828
 
676
- ### 🔧 行为变更(不兼容)
829
+ ### 🔧 Behavior changes (incompatible)
677
830
 
678
- - **升级 `@ai-zen/agents-sdk` 至 `1.0.0-alpha.15`:思考档位不再「存档位名」,改存展开后的参数** —— 上一版把档位名写在 Agent 定义的 `reasoningEffort` 上,运行期由 SDK 查表翻译成请求参数。那等于把「什么叫 `max`」这件产品语义塞进了运行时。现在改成**配置即事实**:
679
- - `/think` 把选中档位的参数补丁直接写进 `AgentDefinition.modelParams`(如 `{"thinking":{"type":"enabled"},"reasoning_effort":"high"}`),SDK 只做一次 `{...defaultParams, ...modelParams}` 浅合并;
680
- - **旧 `reasoningEffort` 自动迁移**:读 Agent 定义时展开成 `modelParams` 并写回;模型没关联策略的只删旧字段(不会瞎猜参数)。
681
- - **思考策略表的归属调整为「SDK 提供、CLI 使用」** —— 表的类型、出厂内容与读写放在 SDK(CLI 与桌面端共享同一份「智谱 5.3 只认 low/high/max」的知识),CLI 只负责**利用**它:限制 `/think` 的可选档位、把档位展开成参数写进 Agent。`Scope` 不再需要注入策略表。
831
+ - **Upgraded `@ai-zen/agents-sdk` to `1.0.0-alpha.15`: effort levels no longer store the level name but the expanded parameters** — the previous version wrote the level name into the Agent definition's `reasoningEffort`, and at runtime the SDK looked it up in a table and translated it into request parameters. That pushed the product semantics of "what `max` means" into the runtime. It is now **configuration is the fact**:
832
+ - `/think` writes the selected level's parameter patch directly into `AgentDefinition.modelParams` (e.g. `{"thinking":{"type":"enabled"},"reasoning_effort":"high"}`), and the SDK only performs a single shallow merge of `{...defaultParams, ...modelParams}`;
833
+ - **the old `reasoningEffort` is migrated automatically**: when the Agent definition is read it is expanded into `modelParams` and written back; when the model has no associated preset, only the old field is deleted (no guessing at parameters).
834
+ - **Ownership of the reasoning preset table adjusted to "provided by the SDK, used by the CLI"** — the table's types, factory content and read/write live in the SDK (the CLI and the desktop share the same knowledge that "Zhipu 5.3 only accepts low/high/max"), and the CLI only **consumes** it: limiting the levels `/think` offers and expanding a level into parameters written into the Agent. `Scope` no longer needs the preset table injected.
682
835
 
683
- ### 🚀 新功能
836
+ ### 🚀 New features
684
837
 
685
- - **状态栏档位改为「从生效参数反查」** —— 显示的就是真正会发给 API 的那几个参数匹配到的档位(模型默认参数也算),不再依赖 Agent 上的档位声明。三种状态:命中某档 → 「低/中/高…」;有 Agent 覆盖但匹配不上任何档位(手改过参数)→「自定义」;完全匹配不上 →「默认」。
686
- - **`/config` →「模型」详情页的「思考策略」字段**继续可用,显示该策略及其档位(如 `zhipu-5.3(low / high / max)`),写错名字当场拦下。
838
+ - **The status bar level is now looked up from the effective parameters** — it shows the level matched by the parameters that will actually be sent to the API (the model defaults included), no longer relying on the level declaration on the Agent. Three states: matches a level → "low / medium / high…"; there is an Agent override but it matches no level (parameters were edited by hand) → "custom"; no match at all → "default".
839
+ - **The "reasoning preset" field in `/config` → "model" detail screen** remains available, showing the preset and its levels (e.g. `zhipu-5.3(low / high / max)`), and a wrong name is rejected on the spot.
687
840
 
688
- ### ✅ 测试
841
+ ### ✅ Tests
689
842
 
690
- - `src/reasoning-effort.test.ts` 重写(19 例):写入的是展开参数而非档位名、`max` → `off` 不残留 `reasoning_effort`、保留 `temperature` 等无关参数、`null` 清空后连 `modelParams` 一起删、往返一致(写进去能反查回原档位)。
691
- - 新增 `src/agents-store.test.ts`(4 例):旧 `reasoningEffort` 展开并写回磁盘、无策略时只删字段、无旧字段时不写盘、迁移后能反查回原档位。
692
- - 全量 **24 文件 / 299 例通过**。
843
+ - `src/reasoning-effort.test.ts` rewritten (19 cases): what is written is the expanded parameters rather than the level name; `max` → `off` leaves no residual `reasoning_effort`; unrelated parameters such as `temperature` are preserved; `null` clears and removes `modelParams` together; round trips are consistent (what was written can be looked up back to the original level).
844
+ - New `src/agents-store.test.ts` (4 cases): the old `reasoningEffort` is expanded and written back to disk; only the field is deleted when there is no preset; nothing is written to disk when the old field is absent; the original level can be looked up again after migration.
845
+ - Full run of **24 files / 299 cases passed**.
693
846
 
694
847
  ## [1.0.0-alpha.15] - 2026-10-22
695
848
 
696
- ### 🔧 行为变更(不兼容)
849
+ ### 🔧 Behavior changes (incompatible)
697
850
 
698
- - **升级 `@ai-zen/agents-sdk` 至 `1.0.0-alpha.14`:思考能力改为声明式策略表** —— 上一版用 `Model.reasoning: { efforts, disable? }` 表达厂商差异,`disable` 一词双义、`off` 靠拼接、还用全局枚举假装各家档位等价。现在协议差异全部搬进一张表:
699
- - 新增 `~/.ai-zen/reasoning-presets.json`(首次启动自动释放,出厂三张:`openai` / `zhipu-5.3` / `deepseek`);模型只记 `reasoningPreset` 这个名字;
700
- - `/think` 的可选档位改为读策略表 —— 表里有 `off` 才能关思考,没有就不给这个选项;
701
- - 模型没关联策略、或策略名在表里不存在 → 一律视为不可调档(状态栏 `—`),绝不把来路不明的参数发出去;
702
- - 档位中文标签改为「已知档位查表 + 未知档位名原样显示」,用户自建的档位名(如 `turbo`)不会显示成空白。
703
- - `src/stdio-runner.ts` / `src/tui/chat-session.ts` 的 `TokenThresholds` 改为直接从 SDK 导入(此前是绕开 alpha.13 缺导出的派生类型)。
851
+ - **Upgraded `@ai-zen/agents-sdk` to `1.0.0-alpha.14`: reasoning capability changed to a declarative preset table** — the previous version expressed vendor differences with `Model.reasoning: { efforts, disable? }`, where `disable` carried two meanings, `off` relied on concatenation, and a global enum pretended that every vendor's levels were equivalent. All protocol differences are now moved into one table:
852
+ - new `~/.ai-zen/reasoning-presets.json` (released automatically on first start, three factory presets: `openai` / `zhipu-5.3` / `deepseek`); a model only records the name `reasoningPreset`;
853
+ - the levels `/think` offers now come from the preset table — thinking can only be turned off if the table has `off`, and that option is not offered otherwise;
854
+ - a model with no preset associated, or with a preset name absent from the table → always treated as not adjustable (`—` in the status bar), and parameters of unknown origin are never sent;
855
+ - level labels became "look up known levels in the table + display unknown level names as-is", so level names created by users (e.g. `turbo`) are no longer shown as blank.
856
+ - `TokenThresholds` in `src/stdio-runner.ts` / `src/tui/chat-session.ts` now imported directly from the SDK (previously a derived type that worked around the missing export in alpha.13).
704
857
 
705
- ### 🚀 新功能
858
+ ### 🚀 New features
706
859
 
707
- - **`/config` →「模型」详情页新增「思考策略」字段** —— 显示该模型当前引用的策略及其档位(如 `zhipu-5.3(low / high / max)`),可直接改成表里另一个策略名;写错名字当场拦下并列出可选策略,留空 = 不调档。自建模型从此能指向内置策略,不必手改 JSON。
708
- - **策略表可自行编辑** —— 加一张表、调档位补丁都行;给某张表标 `"custom": true` 后 SDK 升级不再覆盖它,其余策略仍随 SDK 同步。
860
+ - **New "reasoning preset" field in `/config` → "model" detail screen** — shows the preset the model currently references and its levels (e.g. `zhipu-5.3(low / high / max)`), and can be changed directly to another preset name from the table; a wrong name is rejected on the spot with the available presets listed, and leaving it empty = no level adjustment. Custom models can now point at a built-in preset without hand-editing JSON.
861
+ - **The preset table can be edited** — adding a table or adjusting level patches is fine; once a table is marked `"custom": true`, SDK upgrades no longer overwrite it, while the remaining presets still sync with the SDK.
709
862
 
710
- ### ✅ 测试
863
+ ### ✅ Tests
711
864
 
712
- - `src/reasoning-effort.test.ts` 重写(14 例):可选档位 = 策略表的键、未知档位名原样显示、未关联策略/策略名不存在时抛错、状态栏在非法声明时回退「默认」。
713
- - 配置向导新增 2 例:写入合法策略名、非法策略名被拦下且不写盘。
714
- - 全量 **23 文件 / 290 例通过**。
865
+ - `src/reasoning-effort.test.ts` rewritten (14 cases): available levels = the keys of the preset table; unknown level names displayed as-is; an error is thrown when no preset is associated / the preset name does not exist; the status bar falls back to "default" on an illegal declaration.
866
+ - Configuration wizard: 2 new cases: writing a valid preset name; an invalid preset name is rejected and nothing is written to disk.
867
+ - Full run of **23 files / 290 cases passed**.
715
868
 
716
869
  ## [1.0.0-alpha.14] - 2026-10-22
717
870
 
718
- ### 🚀 新功能
871
+ ### 🚀 New features
719
872
 
720
- - **输入框键位对齐 Claude Code / readline** —— 编辑快捷键统一为业界通行的一套:
721
- - 按词操作:`Alt+B` / `Alt+F` 跨词移动(macOS 上 `Option+←/→` 发的就是 `ESC b` / `ESC f`,此前完全不响应);`Alt+Backspace`、`Alt+D`、`Ctrl+Delete` 按词删除
722
- - 整段操作:`Ctrl+K` 删到行尾、`Ctrl+U` 删到行首、`Ctrl+W` **删到前一空白**(标点不切分,`src/utils/foo.ts` 一次删完)
723
- - kill ring:`Ctrl+Y` 取回刚删的文本,`Alt+Y` 循环取回更早的;`Ctrl+_` 撤销上一次输入编辑;`Ctrl+S` 暂存 / 恢复草稿
724
- - 移动与历史:`Ctrl+A` / `Ctrl+E` 行首 / 行尾,`Ctrl+P` / `Ctrl+N` 历史上下
725
- - 新增纯函数层 `src/tui/input-editing.ts`(`deleteToPrevWhitespace` / `killedText` / kill ring / 撤销栈),12 例单测
726
- - **macOS 按键诊断** —— `AIZ_DEBUG_KEYS=1` 时把终端环境(`TERM` / `TERM_PROGRAM`)与每次按键(`input` + 修饰位 + `raw` 状态)写入 `/tmp/aiz-key-debug.log`(`AIZ_KEYS_FILE` 可改路径),用于定位「某个键在我机器上没反应」:没新行=键被系统截走,`flags` 不符=匹配逻辑问题,`raw=false`=按键被终端自己解释。
727
- - **`/model` 切换当前会话的模型并写入配置** —— 无参弹模型选择器(标注当前),`/model <id>` 直切。写入位置按「会话模型真正的来源」决定:Agent 定义显式绑定了模型 → 改该 Agent 的 `modelId`;没绑定(走默认模型)→ 改 `config.json` 的 `defaultModel`。切换后重建 Scope 与会话(消息与上下文保留),状态栏模型名随之更新。判定逻辑抽为 `src/model-switch.ts`(`resolveModelWriteTarget` / `switchModel`,7 例单测)。
728
- - **思考强度展示与切换(`/think`,别名 `/effort`)** —— 状态栏在模型名后展示当前强度(`关` / `极低` / `低` / `中` / `高` / `极高` / `最高`;模型可调但 Agent 未声明时显示`默认`,模型不可调时显示 `—`)。`/think` 无参弹选择器(标注当前)、`/think high` 直设、`/think default` 清除声明恢复跟随模型默认。
729
- - **档位存在 Agent 定义上**(`AgentDefinition.reasoningEffort`),不在模型上:同一个模型可以给编程 / 小说 / 闲聊三个 Agent 配不同强度,而模型的 `defaultParams` 不动。
730
- - **可选档位由模型能力决定**(`Model.reasoning.efforts`,SDK 按厂商官方文档声明):智谱 GLM-5.3 系只有 `low/high/max` 且不能关(传 `medium` 或 `disabled` 会 400),DeepSeek 是 `off/low/high/max`,OpenAI 是 `off/minimal/low/medium/high`;模型没声明就一个档位都不给,绝不推测。
731
- - 读写与校验抽为 `src/reasoning-effort.ts`(`effortChoices` / `describeEffort` / `setAgentEffort`,9 例单测)。
732
- - **SDK 日志接管** —— TUI 启动即 `setLogger()`,关掉 SDK 的 `console.log` 兜底(否则 `load_mcp` 的 manifest 会直写 stdout 撕裂界面);`AIZ_DEBUG_SDK=1` 时改为落盘 `/tmp/aiz-sdk.log`(`AIZ_SDK_LOG_FILE` 可改路径),6 例单测。
873
+ - **Input line keys aligned with Claude Code / readline** — editing shortcuts unified to the industry-standard set:
874
+ - word operations: `Alt+B` / `Alt+F` move by word (on macOS `Option+←/→` sends `ESC b` / `ESC f`, which previously did not respond at all); `Alt+Backspace`, `Alt+D`, `Ctrl+Delete` delete by word
875
+ - whole-segment operations: `Ctrl+K` deletes to end of line, `Ctrl+U` deletes to start of line, `Ctrl+W` **deletes to the previous whitespace** (punctuation does not split, so `src/utils/foo.ts` goes in one go)
876
+ - kill ring: `Ctrl+Y` retrieves the text just deleted, `Alt+Y` cycles through earlier ones; `Ctrl+_` undoes the last input edit; `Ctrl+S` stashes / restores the draft
877
+ - movement and history: `Ctrl+A` / `Ctrl+E` start / end of line, `Ctrl+P` / `Ctrl+N` previous / next history
878
+ - new pure-function layer `src/tui/input-editing.ts` (`deleteToPrevWhitespace` / `killedText` / kill ring / undo stack), 12 unit tests
879
+ - **macOS key diagnostics** — with `AIZ_DEBUG_KEYS=1` the terminal environment (`TERM` / `TERM_PROGRAM`) and every key press (`input` + modifier bits + `raw` state) are written to `/tmp/aiz-key-debug.log` (path configurable via `AIZ_KEYS_FILE`), for tracking down "a key does nothing on my machine": no new line = the key was intercepted by the system, `flags` mismatch = a matching logic problem, `raw=false` = the key was interpreted by the terminal itself.
880
+ - **`/model` switches the current session's model and writes the config** — with no argument it opens a model picker (annotating the current one), and `/model <id>` switches directly. The write target depends on "where the session model really comes from": model bound explicitly in the Agent definition → that Agent's `modelId` is changed; not bound (using the default model) → `defaultModel` in `config.json` is changed. After switching, the Scope and session are rebuilt (messages and context retained) and the model name in the status bar updates. The decision logic is extracted into `src/model-switch.ts` (`resolveModelWriteTarget` / `switchModel`, 7 unit tests).
881
+ - **Reasoning effort display and switching (`/think`, alias `/effort`)** — the status bar shows the current effort after the model name (`off` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`; `default` when the model is adjustable but the Agent has no declaration, `—` when the model is not adjustable). `/think` with no argument opens a picker (annotating the current one), `/think high` sets directly, `/think default` clears the declaration and follows the model default again.
882
+ - **The level lives on the Agent definition** (`AgentDefinition.reasoningEffort`), not on the model: the same model can have different efforts for the coding / novel / chat Agents, while the model's `defaultParams` stay untouched.
883
+ - **Available levels are decided by model capability** (`Model.reasoning.efforts`, declared by the SDK per the vendor's official docs): Zhipu GLM-5.3 only has `low/high/max` and cannot be turned off (passing `medium` or `disabled` returns 400), DeepSeek is `off/low/high/max`, OpenAI is `off/minimal/low/medium/high`; a model with no declaration offers no level at all — never guessed.
884
+ - Read/write and validation are extracted into `src/reasoning-effort.ts` (`effortChoices` / `describeEffort` / `setAgentEffort`, 9 unit tests).
885
+ - **SDK logging takeover** — the TUI calls `setLogger()` on startup, disabling the SDK's `console.log` fallback (otherwise the `load_mcp` manifest writes straight to stdout and tears the UI apart); with `AIZ_DEBUG_SDK=1` it writes to `/tmp/aiz-sdk.log` instead (path configurable via `AIZ_SDK_LOG_FILE`), 6 unit tests.
733
886
 
734
- ### 🔧 行为变更(不兼容)
887
+ ### 🔧 Behavior changes (incompatible)
735
888
 
736
- - **升级 `@ai-zen/agents-sdk` 至 `1.0.0-alpha.13`,上下文阈值改为结构体** —— `Model.maxContextTokens` 被 `Model.tokenThresholds: { handoff, guard }` 取代(`maxContextChars` 淘汰):
737
- - `handoff`:交接软线,`prompt_tokens` 超过它触发任务交接;
738
- - `guard`:强行阻断硬线,每次请求**前**超过它即中断本轮并报错。
739
- - 两条线都是**绝对值**,`ContextGuardPlugin` 的 `ratio` / `suspendedRatio` / `isSuspended` 已从 SDK 删除——想调松就改数字。代价是交接回合不再有 2.0 倍额外放宽,出厂把 `guard` 配成 `handoff × 1.5`(375K)以留出缓冲。
740
- - 旧配置**自动迁移**:SDK 的 `ConfigManager.read()` 读到即迁(`maxContextTokens` → `handoff`,`maxContextChars` 按 `/4` 折算,缺 `guard` 按 1.5 倍补一次,旧键删除),检测到差异才写回磁盘。用户无需手改 `config.json`。
741
- - 配置中心里原来的「迁移阈值」一项拆成「交接阈值」与「阻断阈值」两行,并校验软线必须小于硬线。
889
+ - **Upgraded `@ai-zen/agents-sdk` to `1.0.0-alpha.13`, context thresholds changed to a struct** — `Model.maxContextTokens` is replaced by `Model.tokenThresholds: { handoff, guard }` (`maxContextChars` retired):
890
+ - `handoff`: the soft handoff line; `prompt_tokens` exceeding it triggers a task handoff;
891
+ - `guard`: the hard cutoff line; exceeding it **before** each request aborts the turn with an error.
892
+ - both lines are **absolute values**, and `ratio` / `suspendedRatio` / `isSuspended` of `ContextGuardPlugin` are removed from the SDK — loosen things by changing the numbers. The cost is that the handoff turn no longer gets the 2.0× extra allowance; by default `guard` is configured as `handoff × 1.5` (375K) to leave headroom.
893
+ - old configs are **migrated automatically**: the SDK's `ConfigManager.read()` migrates on read (`maxContextTokens` → `handoff`, `maxContextChars` converted by `/4`, a missing `guard` filled in once at 1.5×, old keys deleted), writing back to disk only when a difference is detected. Users do not need to hand-edit `config.json`.
894
+ - the former "migration threshold" entry in the configuration center is split into "handoff threshold" and "guard threshold" rows, and the soft line is validated to be less than the hard line.
742
895
 
743
- - **`Ctrl+N` 由「换行」改为「历史下一条」** —— 换行改用 `Ctrl+J` / `Ctrl+Enter` / `Alt+Enter` / `Shift+Enter` / `\` + `Enter`
744
- - **`Ctrl+V` 不再兼任「翻页」** —— 预留给 Claude Code 的「粘贴图片」;当前按下会提示暂未实现。翻页用 `PgUp` / `PgDn`,按行滚动用 `Shift+↑ ↓` / `Alt+↑ ↓`
745
- - **`Ctrl+U` 由「清空整个输入框」改为「删到行首」** —— macOS 的 iTerm2 与 Terminal.app 会把 `⌘⌫` 映射成 `Ctrl+U`,原先的语义会让 Mac 用户一按 ⌘⌫ 就丢掉整段草稿;清空全部改由 `Esc` 负责
746
- - **`Ctrl+C` 空闲时不再直接退出** —— 有草稿时先清空并提示,再按一次才退出
747
- - **`Ctrl+D` 改为双击确认退出** —— 有草稿时改为 readline 语义(删光标后一个字符)
748
- - **忙碌期(生成 / 迁移中)`Esc` 现在等于中断本轮**
896
+ - **`Ctrl+N` changed from "newline" to "next history entry"** — newline is now `Ctrl+J` / `Ctrl+Enter` / `Alt+Enter` / `Shift+Enter` / `\` + `Enter`
897
+ - **`Ctrl+V` no longer doubles as "page down"** — reserved for Claude Code's "paste image"; pressing it now shows a not-implemented-yet notice. Paging uses `PgUp` / `PgDn`, scrolling by line uses `Shift+↑ ↓` / `Alt+↑ ↓`
898
+ - **`Ctrl+U` changed from "clear the whole input box" to "delete to start of line"** — iTerm2 and Terminal.app on macOS map `⌘⌫` to `Ctrl+U`, and the old semantics made Mac users lose an entire draft with a single ⌘⌫; clearing everything is now handled by `Esc`
899
+ - **`Ctrl+C` no longer exits directly when idle** — with a draft, it clears the draft first with a notice, and a second press exits
900
+ - **`Ctrl+D` changed to double-press to confirm exit** — with a draft, readline semantics apply (delete one character after the cursor)
901
+ - **`Esc` during a busy period (generating / migrating) now aborts the current turn**
749
902
 
750
- ### ⏳ 暂未实现
903
+ ### ⏳ Not implemented yet
751
904
 
752
- - **粘贴图片(`Ctrl+V`)** —— 需要先解决「文件 → 内容块」的端点能力探测(Files API 上传 vs base64 内联),不能把上传端点硬编码成某个 id;随后的 SDK 改造再做,本轮已把该功能代码移除,避免半成品上线。
905
+ - **Pasting images (`Ctrl+V`)** — the endpoint capability probe for "file → content block" has to be solved first (Files API upload vs base64 inline), and the upload endpoint must not be hardcoded to a particular id; the subsequent SDK rework comes after, and the code for this feature was removed in this round to avoid shipping a half-finished feature.
753
906
 
754
- ### ✅ 测试
907
+ ### ✅ Tests
755
908
 
756
- - 新增 `src/tui/input-editing.test.ts`(12 例)+ `src/tui/key-debug.test.ts`(4 例)+ `src/sdk-log.test.ts`(6 例)+ `src/model-switch.test.ts`(7 例)+ `src/reasoning-effort.test.ts`(9 例)+ 输入工具函数 8 例;全量 **23 文件 / 286 例** 通过,`tsc` 与 `build` 零错误。
909
+ - New `src/tui/input-editing.test.ts` (12 cases) + `src/tui/key-debug.test.ts` (4 cases) + `src/sdk-log.test.ts` (6 cases) + `src/model-switch.test.ts` (7 cases) + `src/reasoning-effort.test.ts` (9 cases) + 8 cases for the input utility functions; full run of **23 files / 286 cases** passed, `tsc` and `build` with zero errors.
757
910
 
758
- ### 📄 文档
911
+ ### 📄 Docs
759
912
 
760
- - `README{,.zh}.md` 重写「键盘与终端兼容」小节:改为业界通行路线「Option 当 Meta + readline 系 Ctrl 键」,附 Apple Terminal / iTerm2 / VS Code / Ghostty 的具体设置路径;说明 `⌘` 需 kitty 键盘协议才会上报、`⌘⌫` 被终端映射为 `Ctrl+U` 的原因;补充按键诊断用法。`README{,.zh}.md` 与 `docs/{zh,en}/getting-started.md` 的命令表新增 `/model`、`/think`(含「配置文件来源」对照),`docs/manifest.json` 版本号同步。
913
+ - Rewrote the "keyboard and terminal compatibility" section of `README{,.zh}.md`: switched to the industry-standard route "Option as Meta + readline-style Ctrl keys", with the concrete settings paths for Apple Terminal / iTerm2 / VS Code / Ghostty; explained that `⌘` only reports with the kitty keyboard protocol, and why `⌘⌫` is mapped to `Ctrl+U` by the terminal; added the usage of key diagnostics. The command tables in `README{,.zh}.md` and `docs/{zh,en}/getting-started.md` gained `/model` and `/think` (with a "config file source" comparison), and the version in `docs/manifest.json` was synced.
761
914
 
762
915
  ## [1.0.0-alpha.13] - 2026-10-21
763
916
 
764
- ### 🚀 新功能
917
+ ### 🚀 New features
765
918
 
766
- - **交接二次确认统一为「三选一」** —— 自动迁移、手动 `/migrate`、手动 `/handoff` 三条链路此前交互不一致(自动是 2 项选择器 + Esc 跳过;`/migrate` 是 yes/no 确认;`/handoff` 不询问)。现统一为同一个「交接方式」选择器:
767
- - **当前会话自己写交接文档**(`self`,保真 · 可核实真实状态)
768
- - **单独 Agent 写交接文档**(`summary`,独立摘要 · 快且省,不核实文件)
769
- - **不交接**(跳过本次;`Esc` 同效)
919
+ - **Handoff confirmation unified to a three-way choice** — the three paths of automatic migration, manual `/migrate` and manual `/handoff` previously behaved inconsistently (automatic: a 2-item picker + Esc to skip; `/migrate`: a yes/no confirmation; `/handoff`: no question). They are now unified into the same "handoff mode" picker:
920
+ - **the current session writes the handoff doc itself** (`self`, faithful · the real state can be verified)
921
+ - **a separate Agent writes the handoff doc** (`summary`, independent summary · fast and cheap, files are not verified)
922
+ - **no handoff** (skip this time; `Esc` does the same)
770
923
 
771
- 命令后显式带 `self` / `summary`(如 `/migrate self`)时直接执行、不再询问。选择器抽为共用模块 `src/tui/screens/handoff-choice.ts`,三处复用同一份选项与文案。
924
+ When `self` / `summary` is given explicitly after the command (e.g. `/migrate self`), it executes directly without asking. The picker is extracted into the shared module `src/tui/screens/handoff-choice.ts`, with the same options and copy reused in all three places.
772
925
 
773
- ### 🔧 修复
926
+ ### 🔧 Fixes
774
927
 
775
- - **自动「不交接」的确认提示被覆盖** —— `notice` 通道只保留最新一条,原先「已跳过本次任务迁移」与「可随时 /migrate…」分两条推送,后者覆盖前者;现合并为单条,跳过确认可见。
928
+ - **The confirmation notice for an automatic "no handoff" was overwritten** — the `notice` channel keeps only the latest entry, and the previous "this task migration has been skipped" and "you can /migrate at any time…" were pushed as two entries, the latter overwriting the former; they are now merged into a single entry, so the skip confirmation is visible.
776
929
 
777
- ### ✅ 测试
930
+ ### ✅ Tests
778
931
 
779
- - 新增 `src/tui/screens/handoff-choice.test.ts`(5 例):三选一取值顺序、`self`/`summary` 原样返回、「不交接」/`Esc` → `null`、标题透传。全量 **18 文件 / 237 用例**通过,`tsc` 与 `build` 零错误。
932
+ - New `src/tui/screens/handoff-choice.test.ts` (5 cases): the value order of the three-way choice, `self`/`summary` returned as-is, "no handoff"/`Esc` → `null`, title passed through. Full run of **18 files / 237 cases** passed, `tsc` and `build` with zero errors.
780
933
 
781
- ### 📄 文档
934
+ ### 📄 Docs
782
935
 
783
- - `docs/{zh,en}/migration.md`「触发方式」同步为统一的三选一交互(手动命令均会先询问,除非显式指定执行者);`docs/manifest.json`、`README{,.zh}.md` 版本号同步。
936
+ - The "trigger methods" section of `docs/{zh,en}/migration.md` synced to the unified three-way choice (manual commands always ask first unless the executor is specified explicitly); the versions in `docs/manifest.json` and `README{,.zh}.md` synced.
784
937
 
785
938
  ## [1.0.0-alpha.12] - 2026-10-20
786
939
 
787
- ### 🔧 修复
940
+ ### 🔧 Fixes
788
941
 
789
- - **自动迁移后旧会话被覆盖、历史丢失** —— `ChatSession` 曾把会话 id 存两份(`this.currentId` 与 `ctx.currentId`)。手动 `/migrate` 与 `/new` 会同步两份,但**自动迁移**由 SDK 的 `AutoMigratePlugin` 直接驱动 `HandoffService.migrate()`,**不经过 `ChatSession` 的任何方法**,新 id 只写进了 `ctx.currentId`。于是 `ConversationPersistPlugin.getId()` 仍返回旧 id,迁移后第一次落盘就把 `prune` 产生的「系统提示 + 交接断点」写进**旧会话文件**,把已保存的完整历史覆盖掉(用户表现为「交接之后历史全没了,只剩交接文档」)。
790
- - 修法:`currentId` / `currentName` 改为**代理 `ctx` 的 getter/setter**,消灭双真相源——谁更新 `ctx` 都立刻生效。
791
- - `ConversationContext.currentId` 由 `string | undefined` 收紧为 `string`;`reset()` 中两行冗余双写随之删除。
792
- - **测试污染**:`chat-session.test.ts` 的端到端用例会把交接文档写进 `<cwd>/.ai-zen/handoffs`,现改指临时目录并自动清理。
942
+ - **After automatic migration the old session was overwritten and history was lost** — `ChatSession` used to store the session id in two places (`this.currentId` and `ctx.currentId`). Manual `/migrate` and `/new` synced both, but **automatic migration** is driven by the SDK's `AutoMigratePlugin` calling `HandoffService.migrate()` directly and **goes through no method of `ChatSession`**, so the new id was written only into `ctx.currentId`. `ConversationPersistPlugin.getId()` therefore still returned the old id, and the first write after migration stored the "system prompt + handoff breakpoint" produced by `prune` into the **old session file**, overwriting the complete saved history (users saw "all history gone after the handoff, only the handoff doc left").
943
+ - Fix: `currentId` / `currentName` became **getters/setters proxying `ctx`**, eliminating the second source of truth — whoever updates `ctx` takes effect immediately.
944
+ - `ConversationContext.currentId` narrowed from `string | undefined` to `string`; the two redundant double writes in `reset()` were removed accordingly.
945
+ - **Test pollution**: the end-to-end case in `chat-session.test.ts` wrote handoff docs into `<cwd>/.ai-zen/handoffs`; it now points at a temp directory with automatic cleanup.
793
946
 
794
- ### ✅ 测试
947
+ ### ✅ Tests
795
948
 
796
- - 新增 3 例:`ctx` 变更即时生效、赋值回写 `ctx`、端到端(真实 `handoffService.migrate()` 后 id 必须跟随;该用例在修复前会失败,已反向验证)。全量 **17 文件 / 232 用例**通过,`tsc`(含测试工程)与 `build` 零错误。
949
+ - 3 new cases: `ctx` changes take effect immediately; assignment writes back to `ctx`; end to end (the id must follow after a real `handoffService.migrate()`; this case does fail before the fix, verified in reverse). Full run of **17 files / 232 cases** passed, `tsc` (including the test project) and `build` with zero errors.
797
950
 
798
- ### 📄 文档
951
+ ### 📄 Docs
799
952
 
800
- - `docs/{zh,en}/migration.md`「迁移链路」补充「会话 id 只有一个真相源」的约束与失效表现;`docs/manifest.json`、`README{,.zh}.md` 版本号同步。
801
- - 依赖仍为 `@ai-zen/agents-sdk` `1.0.0-alpha.11`(本次无 SDK 改动)。
953
+ - The "migration chain" section of `docs/{zh,en}/migration.md` gained the constraint "the session id has a single source of truth" and its failure symptoms; the versions in `docs/manifest.json` and `README{,.zh}.md` synced.
954
+ - The dependency is still `@ai-zen/agents-sdk` `1.0.0-alpha.11` (no SDK changes this time).
802
955
 
803
956
  ## [1.0.0-alpha.11] - 2026-10-20
804
957
 
805
- ### ⚠️ 依赖
958
+ ### ⚠️ Dependencies
806
959
 
807
- - `@ai-zen/agents-sdk` `1.0.0-alpha.10 → 1.0.0-alpha.11`(`createCallMcpTool` 新增 `env` 参数为破坏性变更;CLI 不直接调用该工厂,无代码改动)。
960
+ - `@ai-zen/agents-sdk` `1.0.0-alpha.10 → 1.0.0-alpha.11` (the new `env` parameter of `createCallMcpTool` is a breaking change; the CLI does not call that factory directly, so there are no code changes).
808
961
 
809
- ### 🔧 变更(随 SDK)
962
+ ### 🔧 Changes (with SDK)
810
963
 
811
- - **`call_mcp_tool` 输出保护**:调用 MCP 工具时,文本输出超过 `maxToolOutput`(默认 32768 字符)会完整落盘为 `result.txt`,只返回警告 + 头尾预览,可用 `read` 分批读取——不再一次把上万字符灌进上下文。
812
- - **图片 / 音频 / 二进制资源原样返回**:MCP 返回的 `image` / `audio` / `resource.blob` 转成 `image_url` / `file` 内容块透传(`resource.text` 并入正文、`resource_link` 转为可读线索),不再静默丢弃;媒体不受 `maxToolOutput` 约束。
964
+ - **`call_mcp_tool` output protection**: when an MCP tool is called, text output exceeding `maxToolOutput` (32768 characters by default) is written in full to `result.txt`, and only a warning plus a head/tail preview is returned, readable in batches with `read` — no more pouring tens of thousands of characters into the context at once.
965
+ - **Images / audio / binary resources returned as-is**: the `image` / `audio` / `resource.blob` returned by MCP are converted into `image_url` / `file` content blocks and passed through (`resource.text` merged into the body, `resource_link` turned into readable clues), no longer dropped silently; media is not subject to `maxToolOutput`.
813
966
 
814
- ### 📄 文档
967
+ ### 📄 Docs
815
968
 
816
- - `docs/{zh,en}/mcp.md` 说明「仅文本受输出保护、媒体不受上限约束」;`docs/{zh,en}/getting-started.md`、`docs/manifest.json`、`README{,.zh}.md` 版本同步。
969
+ - `docs/{zh,en}/mcp.md` explains "only text is subject to output protection; media is not limited by the cap"; the versions in `docs/{zh,en}/getting-started.md`, `docs/manifest.json` and `README{,.zh}.md` synced.
817
970
 
818
971
  ## [1.0.0-alpha.10] - 2026-10-20
819
972
 
820
- ### 💥 变更
973
+ ### 💥 Changes
821
974
 
822
- - **交接与迁移拆成两条命令**(语义唯一,命名与实现对齐):
823
- - **`/handoff`** —— 原 agent 停手、核实状态、把交接文档写到 `<项目>/.ai-zen/handoffs/`,**会话照旧**(不弹确认,可反复执行;交接回合留在 `agent.messages` 里,可继续要求细化同一份文档);
824
- - **`/migrate`**(别名 `/migration`)—— 同一次交接 + 清历史 + 开新会话(需确认)。
825
- - 两条都支持 `self` / `summary` 参数(如 `/handoff summary`、`/migrate self`)跳过默认选择。
826
- - **自动迁移改为「执行者三选一」选择器**:上下文超阈值时不再只问「是否迁移」,而是选择 **原 agent 自交接(默认高亮,保真)/ 独立摘要(便宜)/ 跳过本次**。选择器复用既有 picker 浮层(`setPickerState`)。非交互环境(管道、重定向、e2e)无选择通道,按默认执行者迁移,不阻塞、不额外输出。
827
- - **交接文档目录**:新增 `PROJECT_HANDOFFS_DIR = <cwd>/.ai-zen/handoffs`,交接文档落在项目内,可随仓库共享/评审。
975
+ - **Handoff and migration split into two commands** (single semantics, naming aligned with the implementation):
976
+ - **`/handoff`** — the original agent stops, verifies the state and writes the handoff doc to `<project>/.ai-zen/handoffs/`, with the **session unchanged** (no confirmation, repeatable; the handoff turn stays in `agent.messages`, so you can keep asking to refine the same doc);
977
+ - **`/migrate`** (alias `/migration`) — the same handoff + clear history + start a new session (confirmation required).
978
+ - both support the `self` / `summary` argument (e.g. `/handoff summary`, `/migrate self`) to skip the default choice.
979
+ - **Automatic migration changed to an "executor three-way choice" picker**: when the context exceeds the threshold it no longer only asks "whether to migrate", but offers **self-handoff by the original agent (highlighted by default, faithful) / independent summary (cheap) / skip this time**. The picker reuses the existing picker overlay (`setPickerState`). Non-interactive environments (pipes, redirection, e2e) have no choice channel and migrate with the default executor, without blocking or extra output.
980
+ - **Handoff doc directory**: new `PROJECT_HANDOFFS_DIR = <cwd>/.ai-zen/handoffs`, so handoff docs live inside the project and can be shared / reviewed with the repository.
828
981
 
829
- ### ♻️ 重构
982
+ ### ♻️ Refactors
830
983
 
831
- - **`src/migration-service.ts` → `src/handoff-service.ts`**:`createMigrationService` → `createHandoffService`,钩子从 `onBeforeMigrate` / `onMigrated` 改为 `onBeforeHandoff` / `onHandoff` / `onMigrated`。存档旧会话移到 `onHandoff`(此时已包含交接回合,归档可审计);`/handoff` 不切换会话也因此不需要额外存档(`ConversationPersistPlugin` 每轮已落盘)。
832
- - **`src/auto-migrate-confirm-plugin.ts` → `src/auto-migrate-confirm.ts`**:`AutoMigrateConfirmPlugin`(继承 + 重写 `onAfterSend`)退化为 `createAutoMigratePlugin()` 工厂——「何时迁移」仍在 SDK 插件里,「用谁交接」通过 SDK 新增的 `resolveMode` 回调注入,不再重复实现阈值判断。
833
- - `src/agent-creator.ts` 抽出并导出 `listSkillDirs()`:Scope 装配与交接服务(读取 `handoff` Skill 正文)共用同一份目录优先级,避免两处各算一遍。
834
- - `ChatSessionOptions.requestConfirm` → `requestMode`(返回 `HandoffMode | null`);`ChatSession.migrate()` / `handoff()` 返回文档路径;`ConversationContext.migrationService` → `handoffService`。
835
- - `ContextGuardPlugin` 接线 `isSuspended: () => handoffService.busy`:交接回合护栏从 `× 1.5` 放宽到 `× 2`。
836
- - 斜杠命令解析支持参数:命令名取首个 token(`/migrate summary` 不再被当成未知命令)。
984
+ - **`src/migration-service.ts` → `src/handoff-service.ts`**: `createMigrationService` → `createHandoffService`, and the hooks change from `onBeforeMigrate` / `onMigrated` to `onBeforeHandoff` / `onHandoff` / `onMigrated`. Archiving the old session moved to `onHandoff` (which now already contains the handoff turn, making the archive auditable); `/handoff` does not switch sessions and therefore needs no extra archiving (`ConversationPersistPlugin` already writes to disk every turn).
985
+ - **`src/auto-migrate-confirm-plugin.ts` → `src/auto-migrate-confirm.ts`**: `AutoMigrateConfirmPlugin` (inheriting and overriding `onAfterSend`) degenerated into the `createAutoMigratePlugin()` factory — "when to migrate" stays in the SDK plugin, and "who does the handoff" is injected through the SDK's new `resolveMode` callback, with the threshold logic no longer reimplemented.
986
+ - `src/agent-creator.ts` extracted and exported `listSkillDirs()`: Scope assembly and the handoff service (which reads the body of the `handoff` Skill) share the same directory priority, so it is not computed twice.
987
+ - `ChatSessionOptions.requestConfirm` → `requestMode` (returning `HandoffMode | null`); `ChatSession.migrate()` / `handoff()` return the doc path; `ConversationContext.migrationService` → `handoffService`.
988
+ - `ContextGuardPlugin` wired with `isSuspended: () => handoffService.busy`: the handoff-turn guard is relaxed from `× 1.5` to `× 2`.
989
+ - Slash command parsing supports arguments: the command name is the first token (`/migrate summary` is no longer treated as an unknown command).
837
990
 
838
- ### ⚠️ 依赖
991
+ ### ⚠️ Dependencies
839
992
 
840
- - `@ai-zen/agents-sdk` `1.0.0-alpha.9 → 1.0.0-alpha.10`(`TaskMigrationService` → `HandoffService` 为破坏性变更)。
993
+ - `@ai-zen/agents-sdk` `1.0.0-alpha.9 → 1.0.0-alpha.10` (`TaskMigrationService` → `HandoffService` is a breaking change).
841
994
 
842
- ### ✅ 测试
995
+ ### ✅ Tests
843
996
 
844
- - 新增 `src/handoff-service.test.ts`(迁移清 `lastUsage` / 新会话落盘 / `/handoff` 不切会话)、`src/auto-migrate-confirm.test.ts`(非交互默认值、选择透传、取消跳过、busy 让行)。全量 **17 文件 / 229 用例**通过,`tsc --noEmit`(含测试工程)与 `build` 零错误。
997
+ - New `src/handoff-service.test.ts` (migration clears `lastUsage` / the new session is written to disk / `/handoff` does not switch sessions), `src/auto-migrate-confirm.test.ts` (non-interactive default, choice passed through, cancel skips, busy yields). Full run of **17 files / 229 cases** passed, `tsc --noEmit` (including the test project) and `build` with zero errors.
845
998
 
846
- ### 📄 文档
999
+ ### 📄 Docs
847
1000
 
848
- - `docs/{en,zh}/migration.md` 重写为「任务交接与迁移」:两条命令的差异、两种执行者的取舍、自交接的代码级保障(守卫/放宽/防递归)、文档位置与命名、模板由 `handoff` Skill 承载(`metadata.custom: "true"` 退出同步、可项目级覆盖)、成本说明。网站镜像同步。
849
- - `README.md` / `README.zh.md` 命令表与「任务交接与迁移」章节、`docs/{en,zh}/index.md` 特性与导航同步。
1001
+ - `docs/{en,zh}/migration.md` rewritten as "task handoff and migration": the difference between the two commands, the trade-offs of the two executors, the code-level guarantees of self-handoff (guard / relaxation / recursion prevention), doc location and naming, the template carried by the `handoff` Skill (`metadata.custom: "true"` opts out of sync and can be overridden per project), and cost notes. The website mirror synced.
1002
+ - The command table and the "task handoff and migration" chapter of `README.md` / `README.zh.md`, and the features and navigation of `docs/{en,zh}/index.md`, synced.
850
1003
 
851
1004
  ## [1.0.0-alpha.9] - 2026-10-14
852
1005
 
853
- ### 🔧 修复
1006
+ ### 🔧 Fixes
854
1007
 
855
- - **自动迁移与 CLI 的交互适配(两处)**:
856
- - **迁移期间输入框未真正拦住** —— 主输入 `useInput` 的 `isActive` 此前只挡「确认框 / 选择器 / 配置向导」,未挡「生成中」态。自动迁移在 `send()` 内 `await onAfterSend` 执行(`busy` 为真、输入框已被 Spinner 顶替),但按键仍会被静默吞进隐藏的输入缓冲、待本轮结束才浮现。现令主输入在 `busy` 时整体失活,另设「忙碌期」处理器仅放行 `Ctrl+C` 中止 / `Ctrl+D` 退出 / `PgUp`·`Shift+↑↓` 回看;`/migrate` 手动迁移同样纳入 `busy`。
857
- - **迁移完成后仍被总上下文护栏拦截** —— 迁移(prune)会物理重置上下文,却遗留 `agent.lastUsage`(迁移前那一次请求的 `prompt_tokens`)。`ContextGuardPlugin`(`onInnerLoopStart`,硬上限 `maxTokens × 1.5`)据此误判,迁移后第一条消息即被 `ContextOverflowError` 中断。现于迁移后清空 `agent.lastUsage`,护栏首轮无据可查而放行,待下一次响应以真实用量回填。
1008
+ - **Interaction adaptations between automatic migration and the CLI (two places)**:
1009
+ - **The input line did not really block during migration** — the `isActive` of the main `useInput` only blocked the "confirmation box / picker / configuration wizard", not the "generating" state. Automatic migration runs inside `send()` via `await onAfterSend` (`busy` is true and the input line has been replaced by the Spinner), yet keystrokes were still swallowed silently into the hidden input buffer and only surfaced once the turn ended. The main input is now fully deactivated while `busy`, with a separate "busy period" handler that only passes `Ctrl+C` to abort / `Ctrl+D` to exit / `PgUp`·`Shift+↑↓` to look back; manual `/migrate` is included in `busy` as well.
1010
+ - **Still blocked by the overall context guard after migration** — migration (prune) physically resets the context but left `agent.lastUsage` behind (the `prompt_tokens` of the request before migration). `ContextGuardPlugin` (`onInnerLoopStart`, hard cap `maxTokens × 1.5`) misjudged from it, and the first message after migration was aborted with `ContextOverflowError`. `agent.lastUsage` is now cleared after migration, so the guard has nothing to go on in the first round and lets it pass, until the next response fills in the real usage.
858
1011
 
859
- ### ✅ 测试
1012
+ ### ✅ Tests
860
1013
 
861
- - 新增 `src/migration-service.test.ts`:断言迁移后 `agent.lastUsage` 被清空(移除修复后该用例确会失败)。全量单测 **17 文件 / 227 用例**通过,`tsc --noEmit` 与 `pnpm build` 零错误。
1014
+ - New `src/migration-service.test.ts`: asserts that `agent.lastUsage` is cleared after migration (this case does indeed fail when the fix is removed). Full run of **17 files / 227 cases** passed, `tsc --noEmit` and `pnpm build` with zero errors.
862
1015
 
863
1016
  ## [1.0.0-alpha.8] - 2026-10-14
864
1017
 
865
- ### 🔧 修复
1018
+ ### 🔧 Fixes
866
1019
 
867
- - **修复长时间 TUI 会话内存无界增长导致的 OOM(崩溃)** —— CLI 入口此前从未设定 `NODE_ENV`,React 因而按 `process.env.NODE_ENV !== "production"` 载入 **development** 版 `react-reconciler`;该开发构建每次渲染都会调用 `performance.measure()`(User Timing API),而 Node 的 performance 条目缓冲**无上限、从不逐出**,长时间流式会话下条目持续累积,约 55 分钟即将堆顶到 `heap_size_limit`(本机 ~1GB)触发 OOM。现于 `src/index.ts` 顶部设 `process.env.NODE_ENV ||= "production"`;因 ESM 静态 `import` 会被提升到模块体之前执行,对 TUI 入口 `./tui/index.js` 改用动态 `import()`,确保 React/Ink 在 `NODE_ENV` 设定**之后**才被求值,从而加载 production 版 reconciler(该版不调用 `performance.measure`)。显式 `NODE_ENV=development` 仍保留开发构建,便于排查。
1020
+ - **Fixed the OOM (crash) caused by unbounded memory growth in long TUI sessions** — the CLI entry never set `NODE_ENV`, so React loaded the **development** build of `react-reconciler` per `process.env.NODE_ENV !== "production"`; that dev build calls `performance.measure()` (User Timing API) on every render, and Node's performance entry buffer is **unbounded and never evicted**, so entries kept accumulating during long streaming sessions and the heap approached `heap_size_limit` (about 1GB on this machine) after roughly 55 minutes, triggering OOM. `process.env.NODE_ENV ||= "production"` is now set at the top of `src/index.ts`; because ESM static `import`s are hoisted and evaluated before the module body, the TUI entry `./tui/index.js` now uses a dynamic `import()`, ensuring React/Ink is evaluated **after** `NODE_ENV` is set and thus loads the production reconciler (which does not call `performance.measure`). An explicit `NODE_ENV=development` still keeps the dev build for debugging.
868
1021
 
869
- ### ✅ 测试
1022
+ ### ✅ Tests
870
1023
 
871
- - **端到端实证** —— 真实 ESM 下经入口加载 Ink 并连续重渲染 300 次:修复后 `performance.measure` 条目 **0 → 0**(production reconciler),显式 `NODE_ENV=development` 对照组 **0 → 905**(dev 埋点仍在)。全量单测 **16 文件 / 226 用例**通过,`tsc --noEmit` 与 `pnpm build` 零错误。
1024
+ - **End-to-end proof** — loading Ink through the entry point under real ESM and re-rendering 300 times in a row: after the fix, `performance.measure` entries went **0 → 0** (production reconciler), while the control group with an explicit `NODE_ENV=development` went **0 → 905** (dev instrumentation still in place). Full run of **16 files / 226 cases** passed, `tsc --noEmit` and `pnpm build` with zero errors.
872
1025
 
873
1026
  ## [1.0.0-alpha.7] - 2026-10-14
874
1027
 
875
- ### 📄 文档
1028
+ ### 📄 Docs
876
1029
 
877
- - **状态栏 token 用量纳入文档** —— `docs/zh|en/index.md` 的「主要特性」补上「状态栏实时展示 模型 · Agent · 本轮/累计 token 用量 · 生成状态」(此前仅在 README 提及),累计口径为 `sumUsage(agent.messages)`(含所有层级的子 Agent)。
878
- - 修正 `TODO.md` 中已过时的路径引用(`src/tui/config-wizard.tsx` → 目录 `src/tui/config-wizard/`)。
1030
+ - **Status bar token usage added to the docs** — the "main features" section of `docs/zh|en/index.md` gained "the status bar shows in real time the model · Agent · current-turn/cumulative token usage · generation state" (previously mentioned only in the README), with cumulative usage defined as `sumUsage(agent.messages)` (including sub-agents at all levels).
1031
+ - Fixed the outdated path reference in `TODO.md` (`src/tui/config-wizard.tsx` → directory `src/tui/config-wizard/`).
879
1032
 
880
- ### ⚠️ 依赖
1033
+ ### ⚠️ Dependencies
881
1034
 
882
- - `@ai-zen/agents-core` `^4.5.0 → ^4.5.1`、`@ai-zen/agents-sdk` `1.0.0-alpha.8 → 1.0.0-alpha.9`
1035
+ - `@ai-zen/agents-core` `^4.5.0 → ^4.5.1`, `@ai-zen/agents-sdk` `1.0.0-alpha.8 → 1.0.0-alpha.9`
883
1036
 
884
- > 纯文档修正,无代码变更。
1037
+ > Docs-only correction, no code changes.
885
1038
 
886
1039
  ## [1.0.0-alpha.6] - 2026-10-14
887
1040
 
888
- ### ♻️ 重构
1041
+ ### ♻️ Refactors
889
1042
 
890
- - **累计用量改用 SDK 的 `sumUsage`,移除 CLI 自建的内存累加器** —— 依赖 `@ai-zen/agents-sdk` `1.0.0-alpha.8` / `@ai-zen/agents-core` `4.5.0`:除助手消息的 `usage` 外,委派产生的用量现由 core 在委派边界钉到工具消息的 `subAgentUsage` 上。故 `ChatSession.totalUsage` 直接 `sumUsage(agent.messages)` 即可(含所有层级的子 Agent),删掉 `src/usage.ts` 与 `onSubAgentEnd` 内存回灌——子 Agent 开销随消息持久化,续接 / `/load` 亦不丢,且不再有双重计数风险。
1043
+ - **Cumulative usage now uses the SDK's `sumUsage`, and the CLI's own in-memory accumulator is removed** — depends on `@ai-zen/agents-sdk` `1.0.0-alpha.8` / `@ai-zen/agents-core` `4.5.0`: besides the `usage` of assistant messages, usage produced by delegation is now pinned by core onto the tool message's `subAgentUsage` at the delegation boundary. So `ChatSession.totalUsage` can simply be `sumUsage(agent.messages)` (covering sub-agents at every level), `src/usage.ts` and the `onSubAgentEnd` in-memory back-feed are deleted — sub-agent cost is persisted along with messages and is not lost on resume / `/load`, and the double-counting risk is gone.
891
1044
 
892
- ### ⚠️ 依赖
1045
+ ### ⚠️ Dependencies
893
1046
 
894
- - `@ai-zen/agents-sdk` `1.0.0-alpha.7 → 1.0.0-alpha.8`、`@ai-zen/agents-core` `^4.4.0 → ^4.5.0`
1047
+ - `@ai-zen/agents-sdk` `1.0.0-alpha.7 → 1.0.0-alpha.8`, `@ai-zen/agents-core` `^4.4.0 → ^4.5.0`
895
1048
 
896
- ### ✅ 测试
1049
+ ### ✅ Tests
897
1050
 
898
- - 移除 `src/usage.test.ts`(逻辑下沉 core,已由 core 覆盖);`chat-session.test.ts` 的 `totalUsage` 用例改为覆盖「assistant 的 usage + 工具消息的 subAgentUsage」求和。全量单测 **16 文件 / 226 用例**通过,`tsc --noEmit` 与 `npm run build` 零错误。
1051
+ - Removed `src/usage.test.ts` (the logic moved down into core and is already covered there); the `totalUsage` case in `chat-session.test.ts` now covers summing "the assistant's usage + the tool message's subAgentUsage". Full unit suite **16 files / 226 cases** pass, `tsc --noEmit` and `npm run build` report zero errors.
899
1052
 
900
1053
  ## [1.0.0-alpha.5] - 2026-10-14
901
1054
 
902
- ### 🚀 新功能
1055
+ ### 🚀 New features
903
1056
 
904
- - **状态栏展示 token 累计消耗** —— 依托 `@ai-zen/agents-sdk` `1.0.0-alpha.7` / `@ai-zen/agents-core` `4.4.0` 为每条 assistant 消息钉上的 `usage`,新增 `src/usage.ts`(`addUsage` / `sumMessagesUsage` 纯函数)与 `ChatSession.totalUsage`:对本会话全部 assistant 消息的 `usage` 求和即计费口径的累计消耗;子 Agent 的 messages 不在父 Agent 中,故在 `onSubAgentEnd` 单独累加并入、`/new` 时归零。状态栏由「`{tokens} tok`」升级为「`本轮 x · 累计 y tok`」(本轮 = `lastUsage.total_tokens`,累计 = `totalUsage.total_tokens`)。
905
- - **依赖升级** —— `@ai-zen/agents-sdk` `1.0.0-alpha.6 → 1.0.0-alpha.7`、`@ai-zen/agents-core` `^4.3.2 → ^4.4.0`(后者新增 `AgentNS.Message.usage` 逐条 token 用量)。
1057
+ - **Status bar shows cumulative token consumption** — built on the `usage` that `@ai-zen/agents-sdk` `1.0.0-alpha.7` / `@ai-zen/agents-core` `4.4.0` pin onto each assistant message, added `src/usage.ts` (`addUsage` / `sumMessagesUsage` pure functions) and `ChatSession.totalUsage`: summing the `usage` of all assistant messages in the session gives the cumulative consumption on the billing basis; a sub-agent's messages are not in the parent agent, so they are accumulated separately in `onSubAgentEnd` and folded in, and reset to zero on `/new`. The status bar is upgraded from "`{tokens} tok`" to "`this round x · cumulative y tok`" (this round = `lastUsage.total_tokens`, cumulative = `totalUsage.total_tokens`).
1058
+ - **Dependency upgrade** — `@ai-zen/agents-sdk` `1.0.0-alpha.6 → 1.0.0-alpha.7`, `@ai-zen/agents-core` `^4.3.2 → ^4.4.0` (the latter adds `AgentNS.Message.usage`, per-message token usage).
906
1059
 
907
- ### ✅ 测试
1060
+ ### ✅ Tests
908
1061
 
909
- - 新增 `src/usage.test.ts`(4 例);`chat-session.test.ts` 增补 `totalUsage` 求和用例;`screens.test.tsx` 的 `StatusBar` 用例更新。全量单测 **17 文件 / 230 用例**通过,`tsc --noEmit` 与 `npm run build` 零错误。
1062
+ - Added `src/usage.test.ts` (4 cases); `chat-session.test.ts` gained a summation case for `totalUsage`; the `StatusBar` cases in `screens.test.tsx` were updated. Full unit suite **17 files / 230 cases** pass, `tsc --noEmit` and `npm run build` report zero errors.
910
1063
 
911
1064
  ## [1.0.0-alpha.4] - 2026-10-13
912
1065
 
913
- ### ♻️ 重构
1066
+ ### ♻️ Refactors
914
1067
 
915
- - **TUI 渲染直接以 `agent.messages` 为唯一数据源** —— 移除 `ChatEvent` / `chatReducer` / `LiveAssistant` / `Block` / `messagesToBlocks` 等自定义中间模型。SDK 在流式期间会**实时更新 `agent.messages`**(`Agent.parseStreamData` 把每个 delta 增量写进当前那条 message),因此跨消息顺序(数组序)与单条 assistant 内部顺序(`思考 → 工具 → 正文`)天然保持,直接渲染即可,杜绝「第二套真相」的失真与维护负担。已完成消息的折行 / 高亮按「消息 + 列宽 + 内容签名」缓存(`cachedMessageToLines`),流式中那条靠内容签名自动失效。
916
- - **notice / error 改为独立栏位** —— 系统级提示(命令反馈 / 迁移进度 / 报错)不再混入对话转录,也**绝不写进 `agent.messages`**(那会被 `formatHistory()` 原样发给模型、污染上下文);改在输入框上方的独立一行渲染(`src/tui/screens/notice.ts`)。
1068
+ - **TUI rendering uses `agent.messages` directly as the single source of truth** — removed custom intermediate models such as `ChatEvent` / `chatReducer` / `LiveAssistant` / `Block` / `messagesToBlocks`. During streaming the SDK **updates `agent.messages` in real time** (`Agent.parseStreamData` writes each delta incrementally into the current message), so the cross-message order (array order) and the order inside a single assistant message (`thinking → tool → text`) are preserved naturally and rendering directly is enough, which rules out the distortion and maintenance burden of a "second source of truth". Wrapping / highlighting of completed messages is cached by "message + column width + content signature" (`cachedMessageToLines`), and the in-flight message is invalidated automatically through its content signature.
1069
+ - **notice / error moved to a dedicated slot** — system-level notices (command feedback / migration progress / errors) no longer mix into the chat transcript, and are **never written into `agent.messages`** (that would be sent to the model verbatim by `formatHistory()` and pollute the context); they now render on a dedicated row above the input line (`src/tui/screens/notice.ts`).
917
1070
 
918
- ### 🎯 优化
1071
+ ### 🎯 Optimizations
919
1072
 
920
- - **流式增量渲染**:Ink 开启 `incrementalRendering` —— 从「每帧 erase 整屏 + 重写整屏」(`createStandard`)切到「只重绘变化的行」(`createIncremental`),每帧输出量由 O(屏幕大小) 降到 O(变化量),SSH / 弱网下流式更稳。
921
- - **流式合帧**(`src/tui/screens/frame-buffer.ts`):内容变更先攒入缓冲,按 60ms(约 16fps,低于 Ink 默认 30fps 输出节流)批量触发重渲染,把上屏频率从「每个 delta 一次」降到固定帧率。
922
- - **渲染缓存**:长对话下历史消息的折行 / 高亮免于每帧重算,成本不再随对话增长而恶化。
923
- - **思考与工具调用之间补一个空行**,排版更清晰。
1073
+ - **Incremental streaming rendering**: Ink enables `incrementalRendering` — switching from "erase the whole screen and rewrite it every frame" (`createStandard`) to "redraw only the changed lines" (`createIncremental`), bringing per-frame output from O(screen size) down to O(change size), so streaming is steadier over SSH / weak networks.
1074
+ - **Streaming frame coalescing** (`src/tui/screens/frame-buffer.ts`): content changes are buffered first and re-rendering is flushed in batches every 60ms (about 16fps, below Ink's default 30fps output throttle), taking the paint rate from "once per delta" down to a fixed frame rate.
1075
+ - **Render cache**: in long conversations the wrapping / highlighting of historical messages no longer needs to be recomputed every frame, so the cost no longer degrades as the conversation grows.
1076
+ - **A blank line is added between thinking and tool calls**, for a clearer layout.
924
1077
 
925
- ### ✅ 测试
1078
+ ### ✅ Tests
926
1079
 
927
- - 新增 `frame-buffer.test.ts`、`notice.test.ts`;`screens.test.tsx` 重写为基于 `messageToLines` / `transcriptLines` / `computeChatLayout(transcript)`。
928
- - `tsc` 零错误;全量单测 **16 文件 / 225 用例**通过;`npm run build` 通过。
929
- - 真机(pty)验证:SSH / 弱网下流式稳定,长对话滚动流畅。
1080
+ - Added `frame-buffer.test.ts`, `notice.test.ts`; `screens.test.tsx` rewritten on top of `messageToLines` / `transcriptLines` / `computeChatLayout(transcript)`.
1081
+ - `tsc` reports zero errors; full unit suite **16 files / 225 cases** pass; `npm run build` passes.
1082
+ - Verified on a real terminal (pty): streaming is stable over SSH / weak networks, and scrolling in long conversations is smooth.
930
1083
 
931
1084
  ## [1.0.0-alpha.3] - 2026-10-13
932
1085
 
933
- ### 💥 破坏性变更
1086
+ ### 💥 Breaking changes
934
1087
 
935
- - **内置工具改名(统一 `snake_case`:多词收敛、单词简化)** —— 随 `@ai-zen/agents-sdk` `1.0.0-alpha.6` 落地:SDK 将内置工具名统一为 `snake_case`,并把单词工具简化为单词 —— `readFile` → `read`、`writeFile` → `write`、`exec_async` → `spawn`、`findText` → `find_text`、`batchEdit` → `batch_edit`、`inspectFile` → `inspect_file`、`downloadFile` → `download_file`、`generateImage` → `generate_image`、`viewImage` → `view_image`、`indexedSearch` → `indexed_search`;工具类名同步为 `ReadTool` / `WriteTool` / `SpawnTool`。**若你在 Agent 定义、权限白名单(`tools.allow` / `tools.deny`)或自定义提示词里引用了旧工具名,请同步更新为 `snake_case` 新名**(旧名仅保留在 CHANGELOG 历史条目中)。
1088
+ - **Built-in tools renamed (unified on `snake_case`: multi-word names collapsed, single-word names simplified)** — landed with `@ai-zen/agents-sdk` `1.0.0-alpha.6`: the SDK unifies built-in tool names on `snake_case` and simplifies single-word tools to one word — `readFile` → `read`, `writeFile` → `write`, `exec_async` → `spawn`, `findText` → `find_text`, `batchEdit` → `batch_edit`, `inspectFile` → `inspect_file`, `downloadFile` → `download_file`, `generateImage` → `generate_image`, `viewImage` → `view_image`, `indexedSearch` → `indexed_search`; tool class names are likewise now `ReadTool` / `WriteTool` / `SpawnTool`. **If your Agent definitions, permission allow lists (`tools.allow` / `tools.deny`) or custom prompts reference the old tool names, update them to the new `snake_case` names** (old names survive only in historical CHANGELOG entries).
936
1089
 
937
- ### 🚀 新功能
1090
+ ### 🚀 New features
938
1091
 
939
- - **正文 / 思考按 Markdown 源码着色** —— AI 回答本身即 Markdown,现交给 `highlight.js` 的 `markdown` 词法着色:标题(`#`)、粗体、斜体、行内代码、列表、引用、链接各有配色;**围栏代码块保留围栏行**(开 / 闭围栏以低调暗灰渲染)并继续按标注语言做语法高亮。标记符(`#` / `**` / 反引号 等)**原样保留、只染色**(源码着色,非语义渲染)。正文用**空格优先折行**,避免把英文单词从中间切断。
1092
+ - **Text / thinking colored by Markdown source** — AI answers are Markdown already, so they are now colored by `highlight.js`'s `markdown` lexer: headings (`#`), bold, italic, inline code, lists, quotes and links each get their own color; **fenced code blocks keep their fence lines** (opening / closing fences rendered in subdued dark gray) and continue to be syntax-highlighted by the annotated language. Markers (`#` / `**` / backticks, etc.) **are kept as-is and only colored** (source coloring, not semantic rendering). Text wraps **preferring spaces**, so English words are not cut in the middle.
940
1093
 
941
- ### 🎯 优化
1094
+ ### 🎯 Optimizations
942
1095
 
943
- - **正文默认文本色改用终端默认前景色** —— 此前正文基础色写死近白 `#e2e8f0`,在**亮色主题终端**里会「白底白字」看不见。现正文(含未指定颜色的默认分段)一律回落**终端默认前景色**,绝对色只留给标题 / 代码等强调项;同步移除语法色板里两处近白 token 色(`variable` / `params`)。
1096
+ - **The default text color falls back to the terminal default foreground** — the base text color used to be hardcoded near-white `#e2e8f0`, which was invisible "white on white" in **light-theme terminals**. Text (including default segments with no color assigned) now always falls back to the **terminal default foreground**, with absolute colors kept only for emphasis such as headings / code; the two near-white token colors in the syntax palette (`variable` / `params`) are removed at the same time.
944
1097
 
945
- ### ⚠️ 依赖升级
1098
+ ### ⚠️ Dependency upgrades
946
1099
 
947
- - **`@ai-zen/agents-core` 升至 `^4.3.2`、`@ai-zen/agents-sdk` 升至 `1.0.0-alpha.6`** ——
948
- - core `4.3.1`:**中断的并行工具轮次归一化** —— 一轮回复里并行调用的多个工具若被中断(`Ctrl+C`),未完成的工具结果会在历史里被补齐 / 归一,避免下一轮因 `tool_calls` 数量与实际工具结果数量不匹配而被服务端拒绝(默认 `fill`:保留 assistant 正文并补齐未完成结果)。
949
- - core `4.3.2` / sdk `1.0.0-alpha.4…6`:内置工具统一 `snake_case` + 单词简化、`exec_async` → `spawn`、类名对齐(见「破坏性变更」)。
950
- - CLI 侧仅同步工具名(文档 / 测试 fixture / 提示语),运行时行为全量委托 SDK,无需其它适配。
1100
+ - **`@ai-zen/agents-core` raised to `^4.3.2`, `@ai-zen/agents-sdk` raised to `1.0.0-alpha.6`** —
1101
+ - core `4.3.1`: **normalization of interrupted parallel tool rounds** — when several tools called in parallel within one reply are interrupted (`Ctrl+C`), unfinished tool results are filled in / normalized in the history, so the next round is not rejected by the server because the number of `tool_calls` does not match the number of actual tool results (default `fill`: keep the assistant text and fill in unfinished results).
1102
+ - core `4.3.2` / sdk `1.0.0-alpha.4…6`: built-in tools unified on `snake_case` + single-word simplification, `exec_async` → `spawn`, class names aligned (see "Breaking changes").
1103
+ - On the CLI side only tool names were synced (docs / test fixtures / prompts); runtime behavior is fully delegated to the SDK, with no other adaptation needed.
951
1104
 
952
- ### ✅ 测试
1105
+ ### ✅ Tests
953
1106
 
954
- - `tsc --noEmit`(`tsconfig.test.json`)零错误;全量单测 **15 文件 / 235 用例**通过
955
- - 修复 `config-wizard.test.tsx` 两个预存失败:为向导注入终端尺寸(新增可选 `terminalSize` prop),消除 `ink-testing-library` 假 stdout 缺 `rows` 时 `terminal-size` 探测本机行数、把吸底帧撑到 38 行的干扰
956
- - 渲染核验:标题 / 粗体 / 行内代码 / 列表 / 引用 / 链接与围栏代码块均按预期着色,普通正文无颜色(回落终端默认)
1107
+ - `tsc --noEmit` (`tsconfig.test.json`) reports zero errors; full unit suite **15 files / 235 cases** pass
1108
+ - Fixed two pre-existing failures in `config-wizard.test.tsx`: terminal size is now injected into the wizard (new optional `terminalSize` prop), removing the interference where `ink-testing-library`'s fake stdout lacked `rows`, so `terminal-size` probed the host machine's row count and stretched the bottom-anchored frame to 38 rows
1109
+ - Render verification: headings / bold / inline code / lists / quotes / links and fenced code blocks are colored as expected, and plain text has no color (terminal default)
957
1110
 
958
1111
  ## [1.0.0-alpha.2] - 2026-10-03
959
1112
 
960
- ### ⚠️ 依赖升级
1113
+ ### ⚠️ Dependency upgrades
961
1114
 
962
- - **`@ai-zen/agents-sdk` 升至 `1.0.0-alpha.3`** — 拉取 SDK 对内置工具 `generateImage` 参数描述的修正。原描述写着「配置中的图片模型可通过 `aiz config show` 查看」,等于**指路让模型去执行宿主 CLI 命令**;而**工具描述会随每次请求的 `tools` 字段进入每一层 agent 的上下文**——模型据此 `exec("aiz config show")`,CLI 又把这条命令解析为**纯 stdio 模式**、以 `config show` 为提示词**再拉起一个 agent**,其工具描述里又是同一句指引,遂形成**自指型递归**(每层都真连 API,越套越深)。SDK 现已把该描述改为「若指定的模型不存在,本工具会返回当前可用的模型列表」,与 `call()` 的实际行为一致,且不再引导模型去执行宿主 CLI。该版本**无破坏性变更**,CLI 源码无需适配(类型 / 默认配置全量委托 SDK,版本横幅由实际安装版本读取,工具清单由 SDK 静态注册表自动发现)。
1115
+ - **`@ai-zen/agents-sdk` raised to `1.0.0-alpha.3`** — picks up the SDK's fix to the parameter description of the built-in tool `generateImage`. The old description said "the image model in the configuration can be viewed via `aiz config show`", which effectively **pointed the model at running a host CLI command**; and **tool descriptions enter every layer of agent context with the `tools` field of each request** — the model therefore ran `exec("aiz config show")`, the CLI parsed that command as **pure stdio mode** and launched **another agent** with `config show` as the prompt, whose tool description carried the same instruction again, forming a **self-referential recursion** (every layer really hit the API, nesting deeper and deeper). The SDK now rewrites the description to "if the specified model does not exist, this tool returns the list of currently available models", which matches `call()`'s actual behavior and no longer steers the model toward running the host CLI. This version has **no breaking changes**, and the CLI source needs no adaptation (types / default config are fully delegated to the SDK, the version banner reads the actually installed version, and the tool list is auto-discovered from the SDK's static registry).
963
1116
 
964
- ### 📝 文档
1117
+ ### 📝 Docs
965
1118
 
966
- - `docs/zh|en/getting-started.md`:运行时依赖版本同步为 `@ai-zen/agents-sdk` `1.0.0-alpha.3`
967
- - `docs/manifest.json`:版本号同步为 `1.0.0-alpha.2`
968
- - 中英 README 启动横幅示例版本同步为 `v1.0.0-alpha.2`
1119
+ - `docs/zh|en/getting-started.md`: runtime dependency version synced to `@ai-zen/agents-sdk` `1.0.0-alpha.3`
1120
+ - `docs/manifest.json`: version number synced to `1.0.0-alpha.2`
1121
+ - The example startup banner version in the Chinese and English READMEs synced to `v1.0.0-alpha.2`
969
1122
 
970
- ### ✅ 测试
1123
+ ### ✅ Tests
971
1124
 
972
- - `tsc --noEmit`(`tsconfig.test.json`)与 `npm run build` 零错误
973
- - 全量单测 13 文件 / 224 用例通过
1125
+ - `tsc --noEmit` (`tsconfig.test.json`) and `npm run build` report zero errors
1126
+ - Full unit suite: 13 files / 224 cases pass
974
1127
 
975
1128
  ## [1.0.0-alpha.1] - 2026-09-30
976
1129
 
977
- > 本版本汇总**相对上一发布版本 `0.9.0`** 的全部变化。
1130
+ > This release summarizes all changes **relative to the previous release `0.9.0`**.
978
1131
 
979
- ### 💥 破坏性变更
1132
+ ### 💥 Breaking changes
980
1133
 
981
- - **启动命令由 `aiz` / `zen` 改为 `ai`** —— `package.json` 的 `bin` 改为 `{ "ai", "aiz", "zen" }`;`aiz` / `zen` 作为**过渡别名**保留,未来版本移除。
982
- - **引入双模式运行,两种模式互不混合** —— 带位置参数、或 stdin/stdout 非 TTY(管道 / 重定向 / CI)时启用**纯 stdio 模式**:无任何额外交互,最终 assistant 文本以纯文本写入 `stdout`(零 ANSI / emoji 前缀 / spinner),日志、进度、工具过程与错误摘要一律走 `stderr`,成功退出码 `0`、失败非 `0`、默认不落盘;仅在**无参数且 stdin/stdout 均为 TTY** 时进入 **TUI 模式**。
983
- - **交互界面整体重写为 Ink(React)全屏 TUI** —— 取代原 inquirer 行式对话;随之**移除主菜单**(`/menu` 与 `src/menus/*`)以及 `inquirer` / `@types/inquirer` / `chalk` 依赖。
1134
+ - **The startup command changes from `aiz` / `zen` to `ai`** — `bin` in `package.json` becomes `{ "ai", "aiz", "zen" }`; `aiz` / `zen` are kept as **transitional aliases** and will be removed in a future version.
1135
+ - **Dual-mode operation introduced, the two modes never mix** — when positional arguments are present, or stdin/stdout are not a TTY (pipe / redirection / CI), **pure stdio mode** is used: no extra interaction at all, the final assistant text is written to `stdout` as plain text (zero ANSI / emoji prefixes / spinner), while logs, progress, tool activity and error summaries all go to `stderr`, with exit code `0` on success and non-`0` on failure, and nothing written to disk by default; **TUI mode** is entered only when there are **no arguments and both stdin/stdout are TTYs**.
1136
+ - **The whole interactive UI was rewritten as an Ink (React) full-screen TUI** — replacing the former inquirer line-based conversation; the **main menu is removed** (`/menu` and `src/menus/*`) along with the `inquirer` / `@types/inquirer` / `chalk` dependencies.
984
1137
 
985
- ### ✨ 新增
1138
+ ### ✨ Added
986
1139
 
987
- - **TUI 全屏交互界面(Ink + React)**:
988
- - 动画渐变 ASCII 启动界面,结束后直接进入对话(有上一轮会话则自动续接);
989
- - 对话区**吸底**(分隔线 / 输入行 / 状态栏固定底部,整帧高度恒为「终端行数 − 1」以避开 Ink 全屏分支);
990
- - 流式输出(思考 / 正文 / 工具调用分行)、历史按行裁剪自底滚动;
991
- - **多行输入**(`Enter` 发送、`Ctrl+N` / `Ctrl+J` 换行)与**光标编辑**(`←`/`→`/`Home`/`End`/`Backspace`/`Delete`,`Ctrl`/`Meta` + `←`/`→` 跨词,反显光标 + 硬件光标同步);
992
- - **应用内翻页**(`PgUp`/`PgDn` 翻页、`Shift`/`Alt` + `↑`/`↓` 单行滚动、发送自动回底);
993
- - 输入历史、内联 `/` 命令菜单、状态栏、确认框。
994
- - **语法高亮**:工具调用参数按 **JSON** 高亮、正文与思考里的**围栏代码块**按语言高亮。高亮**数据**委托 `highlight.js`(经 `lowlight` 产出 hast),CLI 仅把 `hljs-*` scope 映射为 Ink `<Text>` 颜色 —— 与同为 Ink/React 的 Google Gemini CLI 做法一致。
995
- - **对话内命令**:`/help`、`/load`、`/clear`、`/new`、`/save`、`/config`、`/key`、`/migrate`、`/back`、`/editor`、`/exit`(`registry.ts` 为命令清单唯一来源,`/help` 与输入补全自动同步)。
996
- - **首启凭据引导 + 配置中心(Ink 原生)**:端点缺 API Key 时先弹凭据设置屏;`/config` 配置中心管理端点、模型、图片模型、默认项(默认模型 / 图片模型 / Agent / 迁移模型)、工具输出上限、**MCP 服务器**(全局 / 项目作用域)与 **Agent 定义**(顶层 Agent + Sub-agent,含四维权限);编辑「出厂托管」条目自动转 `custom: true`,危险操作二次确认并做引用守卫。
997
- - **多行文本走系统编辑器**:Agent 提示词、Sub-agent 函数说明 / 参数 schema 等多行字段按 `Enter` 调起 `$EDITOR`(Windows 默认 `notepad`);参数 schema 以 `.json` 打开并即时校验。
998
- - **会话持久化改革:会话即文件 + 单指针** —— 每轮结束落盘、`cli/last-session.json` 记录上一轮会话 id、首条消息后才落盘、`/save` 幂等;继续 / 加载时按消息重建历史。
999
- - **纯 stdio 模式配套**:`ai --help` / `-h`、`ai --version` / `-v`、`ai --save`;SDK 日志与 `console.log` 统一改道 `stderr` 以保证 stdout 纯净。
1000
- - **IME 候选框跟随光标**:用 Ink `useCursor()` 把硬件光标同步到编辑光标,中文输入法候选框正确定位。
1140
+ - **Full-screen TUI (Ink + React)**:
1141
+ - animated gradient ASCII splash screen, going straight into the conversation when it ends (auto-resuming the previous session if there is one);
1142
+ - the chat area is **bottom-anchored** (separator / input line / status bar fixed at the bottom, with the whole frame always "terminal rows − 1" tall to stay out of Ink's full-screen branch);
1143
+ - streaming output (thinking / text / tool calls on separate rows), with history trimmed by line and scrolling from the bottom;
1144
+ - **multi-line input** (`Enter` sends, `Ctrl+N` / `Ctrl+J` insert a newline) and **cursor editing** (`←`/`→`/`Home`/`End`/`Backspace`/`Delete`, `Ctrl`/`Meta` + `←`/`→` by word, inverse-video cursor + hardware cursor sync);
1145
+ - **in-app paging** (`PgUp`/`PgDn` page, `Shift`/`Alt` + `↑`/`↓` scroll one line, sending jumps back to the bottom);
1146
+ - input history, inline `/` command menu, status bar, confirmation dialogs.
1147
+ - **Syntax highlighting**: tool call arguments highlighted as **JSON**, and **fenced code blocks** in text and thinking highlighted by language. The highlighting **data** is delegated to `highlight.js` (via `lowlight` producing hast), and the CLI only maps `hljs-*` scopes to Ink `<Text>` colors — the same approach as the Ink/React-based Google Gemini CLI.
1148
+ - **In-conversation commands**: `/help`, `/load`, `/clear`, `/new`, `/save`, `/config`, `/key`, `/migrate`, `/back`, `/editor`, `/exit` (`registry.ts` is the single source of the command list, and `/help` and input autocomplete stay in sync automatically).
1149
+ - **First-run credential onboarding + configuration center (Ink native)**: when the endpoint has no API key, a credential setup screen is shown first; the `/config` configuration center manages endpoints, models, image models, defaults (default model / image model / Agent / migration model), tool output limits, **MCP servers** (global / project scope) and **Agent definitions** (top-level Agents + sub-agents, with four dimensions of permissions); editing a "factory-managed" entry automatically switches it to `custom: true`, and dangerous operations require a second confirmation and are guarded by reference checks.
1150
+ - **Multi-line text goes through the system editor**: multi-line fields such as agent prompts and sub-agent function descriptions / parameter schemas open `$EDITOR` on `Enter` (`notepad` by default on Windows); parameter schemas open as `.json` and are validated immediately.
1151
+ - **Session persistence overhaul: a session is a file + a single pointer** — written to disk at the end of every round, `cli/last-session.json` records the previous round's session id, nothing is written to disk until the first message, and `/save` is idempotent; resume / load rebuilds the history from messages.
1152
+ - **Extras for pure stdio mode**: `ai --help` / `-h`, `ai --version` / `-v`, `ai --save`; SDK logs and `console.log` are uniformly redirected to `stderr` to keep stdout clean.
1153
+ - **IME candidate window follows the cursor**: Ink's `useCursor()` syncs the hardware cursor to the editing cursor, so the Chinese IME candidate window is positioned correctly.
1001
1154
 
1002
- ### 🔧 变更
1155
+ ### 🔧 Changes
1003
1156
 
1004
- - **依赖**:新增 `ink` / `react`(运行时)、`ink-testing-library` / `@types/react`(开发);新增语法高亮 `highlight.js` / `lowlight`(运行时)与 `@types/hast`(开发);**移除** `inquirer` / `@types/inquirer` / `chalk`。
1005
- - **升级 `@ai-zen/agents-sdk`**:`1.0.0-alpha.0` → `1.0.0-alpha.1` → `1.0.0-alpha.2`(出厂模型目录刷新、模型清单托管、默认 `mcp.json` 增补 `chrome-devtools` 等;CLI 侧代码零改动)。
1006
- - **TUI 输出统一走 Ink**:迁移进度、会话落盘失败等提示不再 `console.log` / `console.warn` 直写终端(会撕裂 Ink 帧),改为经回调渲染为 notice 块;启动 Logo 渐变也改为 Ink 逐字符 `<Text color>`。
1007
- - **配置向导 / 覆盖层统一「吸底」布局**,列表项与键位提示裁成**严格单行**,修复 VS Code 终端里内容被裁切的问题。
1008
- - **终端状态安全与干净退出**:`SIGINT` / `SIGTERM` / 未捕获异常均卸载 Ink、恢复 raw mode 与光标;`Ctrl+C` 语义为「忙时取消本轮、空闲时退出」;退出显式暂停 stdin 并终止进程(`farewell()`)。
1009
- - **移除旧行式链路(死代码)**:`src/conversation-runner.ts`、`src/slash-hint-prompt.ts`、`src/delta-renderer.ts`、inquirer 版 `src/config-wizard.ts`、`src/draft-*.ts` 等;新增 `src/mode.ts` / `src/stdio-runner.ts` / `src/session-pointer.ts` / `src/conversation-persist-plugin.ts` / `src/config-editor.ts` 与 `src/tui/*` 新实现。
1010
- - 文档:中英 README 与 `docs/zh|en/*` 同步(运行模式、`ai` 命令名、配置中心、多行编辑器等),`docs/manifest.json` 同步。
1157
+ - **Dependencies**: added `ink` / `react` (runtime), `ink-testing-library` / `@types/react` (dev); added syntax highlighting `highlight.js` / `lowlight` (runtime) and `@types/hast` (dev); **removed** `inquirer` / `@types/inquirer` / `chalk`.
1158
+ - **Upgraded `@ai-zen/agents-sdk`**: `1.0.0-alpha.0` → `1.0.0-alpha.1` → `1.0.0-alpha.2` (factory model catalog refresh, model list hosting, `chrome-devtools` and others added to the default `mcp.json`; zero code changes on the CLI side).
1159
+ - **TUI output uniformly goes through Ink**: notices such as migration progress and session write failures no longer use `console.log` / `console.warn` to write directly to the terminal (which tears Ink frames) and are rendered as notice blocks via callbacks; the startup logo gradient also uses Ink per-character `<Text color>`.
1160
+ - **The config wizard / overlays are unified on a "bottom-anchored" layout**, with list items and key hints trimmed to a **strict single line**, fixing content clipping in the VS Code terminal.
1161
+ - **Terminal state safety and clean exit**: `SIGINT` / `SIGTERM` / uncaught exceptions all uninstall Ink and restore raw mode and the cursor; `Ctrl+C` means "cancel the current round when busy, exit when idle"; exiting explicitly pauses stdin and terminates the process (`farewell()`).
1162
+ - **Removed the old line-based pipeline (dead code)**: `src/conversation-runner.ts`, `src/slash-hint-prompt.ts`, `src/delta-renderer.ts`, the inquirer-based `src/config-wizard.ts`, `src/draft-*.ts` and so on; added the new implementations `src/mode.ts` / `src/stdio-runner.ts` / `src/session-pointer.ts` / `src/conversation-persist-plugin.ts` / `src/config-editor.ts` and `src/tui/*`.
1163
+ - Docs: the Chinese and English READMEs and `docs/zh|en/*` synced (run modes, the `ai` command name, the configuration center, the multi-line editor, etc.), and `docs/manifest.json` synced.
1011
1164
 
1012
- ### ✅ 测试
1165
+ ### ✅ Tests
1013
1166
 
1014
- - 全量单测 **13 文件 / 224 例**,`tsc --noEmit` 与 `npm run build` 零错误;e2e **12 例**真连 DeepSeek API(覆盖双模式、stdio 纯净性、退出码、`--save` 落盘)。
1015
- - 新增测试文件:`mode.test.ts`、`stdio-runner.test.ts`、`theme.test.ts`、`screens.test.tsx`、`tui/config-wizard.test.tsx`、`tui/agent-prompt.test.tsx`、`tui/highlight.test.ts`、`sub-agent-guard-plugin.test.ts`、`auto-migrate-confirm-plugin.test.ts`、`conversation-commands/registry.test.ts`。
1016
- - 真实 PTY 实测:启动 → 对话 → 流式 → `/load` → `/clear` → 退出交还 shell;首启凭据链路;配置中心各子屏。
1167
+ - Full unit suite **13 files / 224 cases**, `tsc --noEmit` and `npm run build` report zero errors; e2e **12 cases** really hitting the DeepSeek API (covering both modes, stdio purity, exit codes, `--save` persistence).
1168
+ - New test files: `mode.test.ts`, `stdio-runner.test.ts`, `theme.test.ts`, `screens.test.tsx`, `tui/config-wizard.test.tsx`, `tui/agent-prompt.test.tsx`, `tui/highlight.test.ts`, `sub-agent-guard-plugin.test.ts`, `auto-migrate-confirm-plugin.test.ts`, `conversation-commands/registry.test.ts`.
1169
+ - Verified on a real PTY: startup → conversation → streaming → `/load` → `/clear` → exit and hand the shell back; the first-run credential flow; each sub-screen of the configuration center.
1017
1170
 
1018
1171
  ## [0.9.0] - 2026-09-30
1019
1172
 
1020
- ### 💥 破坏性变更
1173
+ ### 💥 Breaking changes
1021
1174
 
1022
- - **升级 `@ai-zen/agents-sdk` 到 `1.0.0-alpha.0`(Provider → Scope 架构重构)** — SDK 1.0.0-alpha.0 将全局入口对象 `Provider` 改名为 `Scope`,把四类能力来源疏散为 5 个可组合的 `ScopePlugin`,并移除 `Provider.create()`(改为显式装配)。CLI 侧同步适配 `src/agent-creator.ts`:
1023
- - 导入 `Scope` / `allInOne` 取代 `Provider`
1175
+ - **Upgraded `@ai-zen/agents-sdk` to `1.0.0-alpha.0` (Provider → Scope architecture refactor)** — SDK 1.0.0-alpha.0 renames the global entry object `Provider` to `Scope`, spreads the four capability sources into 5 composable `ScopePlugin`s, and removes `Provider.create()` (replaced by explicit assembly). The CLI side adapted `src/agent-creator.ts` accordingly:
1176
+ - import `Scope` / `allInOne` instead of `Provider`
1024
1177
  - `Provider.create({ config, agentsDir, subAgentsPaths, skillsPaths, toolsPaths, mcpPaths })` → `new Scope({ config, agentsDir }).use(...allInOne({ subAgentsPaths, skillsPaths, toolsPaths, mcpPaths }))` + `await scope.init()`
1025
- - 单例/工厂改名:`getProvider` → `getScope`、`resetProvider` → `resetScope`;`createAgent` 内部改调 `sdkCreateAgent(scope, agentId)`
1026
- - 依赖声明由 `^0.12.0` 改为精确 `1.0.0-alpha.0`(alpha 阶段锁定)
1178
+ - singleton / factory renames: `getProvider` → `getScope`, `resetProvider` → `resetScope`; `createAgent` now calls `sdkCreateAgent(scope, agentId)` internally
1179
+ - the dependency declaration changed from `^0.12.0` to the exact `1.0.0-alpha.0` (pinned during the alpha phase)
1027
1180
 
1028
- ### 📝 文档
1181
+ ### 📝 Docs
1029
1182
 
1030
- - 中英 README 与 `docs/zh|en/{tools,mcp,skills}.md`:`Provider` / `getProvider()` 同步为 `Scope` / `getScope()`
1031
- - `docs/zh|en/getting-started.md`:运行时依赖版本同步为 `@ai-zen/agents-sdk` `1.0.0-alpha.0`
1032
- - `docs/manifest.json`:版本号同步为 `0.9.0`
1183
+ - Chinese and English READMEs and `docs/zh|en/{tools,mcp,skills}.md`: `Provider` / `getProvider()` synced to `Scope` / `getScope()`
1184
+ - `docs/zh|en/getting-started.md`: runtime dependency version synced to `@ai-zen/agents-sdk` `1.0.0-alpha.0`
1185
+ - `docs/manifest.json`: version number synced to `0.9.0`
1033
1186
 
1034
- ### ✅ 测试
1187
+ ### ✅ Tests
1035
1188
 
1036
- - `tsc --noEmit`(`tsconfig.test.json`)与 `npm run build` 零错误
1037
- - 全量单测 9 文件 / 74 用例通过
1038
- - e2e 6 用例通过(含真实 DeepSeek API 对话,验证 CLI → SDK `1.0.0-alpha.0` → API 全链路)
1189
+ - `tsc --noEmit` (`tsconfig.test.json`) and `npm run build` report zero errors
1190
+ - Full unit suite: 9 files / 74 cases pass
1191
+ - 6 e2e cases pass (including a real DeepSeek API conversation, verifying the whole CLI → SDK `1.0.0-alpha.0` → API chain)
1039
1192
 
1040
1193
  ## [0.8.2] - 2026-09-29
1041
1194
 
1042
- ### ⚠️ 依赖升级
1195
+ ### ⚠️ Dependency upgrades
1043
1196
 
1044
- - **`@ai-zen/agents-sdk` 升至 `^0.12.0`** — SDK 0.12.0 新增内置工具 `inspectFile`(内置工具类 19 → 20)、`readFile` 的 `range` 分批读取参数,新增配置项 `maxToolOutput`(缺省 32768 字符)与工具输出保护行为,并精简了 `batchEdit` 的输出。该版本**无破坏性变更**,CLI 源码无需适配:类型与默认配置全量委托 SDK,`src/version.ts` 由实际安装版本读取,工具清单由 SDK 静态注册表自动发现,故版本横幅随之显示 sdk `0.12.0`
1197
+ - **`@ai-zen/agents-sdk` raised to `^0.12.0`** — SDK 0.12.0 adds the built-in tool `inspectFile` (built-in tool classes 19 → 20) and the `range` batched-read parameter of `readFile`, adds the config option `maxToolOutput` (32768 characters by default) with tool output protection behavior, and trims the output of `batchEdit`. This version has **no breaking changes**, and the CLI source needs no adaptation: types and default config are fully delegated to the SDK, `src/version.ts` reads the actually installed version, and the tool list is auto-discovered from the SDK's static registry, so the version banner now shows sdk `0.12.0`
1045
1198
 
1046
- ### 📝 文档
1199
+ ### 📝 Docs
1047
1200
 
1048
- - 同步内置工具计数 19 → 20:中英 README、`docs/zh|en/tools.md`、`docs/zh|en/index.md` 与 `docs/manifest.json`
1049
- - 工具表补充 `inspectFile`(勘察文件结构概况),并为 `readFile` 补注 `range` 参数
1050
- - `docs/zh|en/tools.md` 新增「输出保护」说明:超限时 `exec` / `findText` / `glob` / `ls` 落盘并返回警告,`readFile` / `inspectFile` 仅警告不落盘
1051
- - `docs/zh|en/getting-started.md` 的运行时依赖版本同步为 `@ai-zen/agents-sdk` `^0.12.0`
1201
+ - Synced the built-in tool count 19 → 20: the Chinese and English READMEs, `docs/zh|en/tools.md`, `docs/zh|en/index.md` and `docs/manifest.json`
1202
+ - Added `inspectFile` (surveying a file's structural overview) to the tool table, and documented the `range` parameter for `readFile`
1203
+ - `docs/zh|en/tools.md` gained an "output protection" section: when a limit is exceeded, `exec` / `findText` / `glob` / `ls` write to disk and return a warning, while `readFile` / `inspectFile` only warn without writing to disk
1204
+ - The runtime dependency version in `docs/zh|en/getting-started.md` synced to `@ai-zen/agents-sdk` `^0.12.0`
1052
1205
 
1053
- ### ✅ 测试
1206
+ ### ✅ Tests
1054
1207
 
1055
- - 沿用既有 9 文件 / 74 用例,全部通过;`tsc --noEmit` 与 `npm run build`(`0.8.2`)均通过
1208
+ - Reuses the existing 9 files / 74 cases, all passing; `tsc --noEmit` and `npm run build` (`0.8.2`) both pass
1056
1209
 
1057
1210
  ## [0.8.1] - 2026-09-28
1058
1211
 
1059
- ### 🚀 新功能
1212
+ ### 🚀 New features
1060
1213
 
1061
- - **Skill 子 Agent 纳入子 Agent 上下文护栏与流式渲染** — 依托 SDK 0.11.0 将 `call_skill_sub_agent` 统一到 `AgentToolLazy`,技能子 Agent 自此与常规 SubAgent 共享同一套委派边界(`onSubAgentStart` / `onSubAgentEnd` 钩子与 `sub-agent-start` / `sub-agent-end` 事件)。故 0.8.0 引入的 `SubAgentGuardInstallerPlugin` 与子 Agent 流式渲染**自动覆盖**该路径,本次 CLI 无源码改动(0.8.0 CHANGELOG「覆盖范围」中「技能子 Agent 不在覆盖范围」的说明随之失效)
1214
+ - **Skill sub-agents are brought under the sub-agent context guard and streaming rendering** — relying on SDK 0.11.0 unifying `call_skill_sub_agent` under `AgentToolLazy`, skill sub-agents now share the same delegation boundary with regular SubAgents (`onSubAgentStart` / `onSubAgentEnd` hooks and `sub-agent-start` / `sub-agent-end` events). So the `SubAgentGuardInstallerPlugin` and the sub-agent streaming rendering introduced in 0.8.0 **automatically cover** that path, and this time the CLI has no source changes (the note in the 0.8.0 CHANGELOG's "coverage" saying "skill sub-agents are not covered" no longer applies)
1062
1215
 
1063
- ### ⚠️ 依赖升级
1216
+ ### ⚠️ Dependency upgrades
1064
1217
 
1065
- - **`@ai-zen/agents-core` 升至 `^4.3.0`、`@ai-zen/agents-sdk` 升至 `^0.11.0`** — `src/version.ts` 由实际安装版本读取,版本横幅随之显示 core `4.3.0` / sdk `0.11.0`。SDK 0.11.0 的破坏性变更(`createCallSkillSubAgentTool` 移除第三参 `provider`)仅影响直接调用该工厂函数的代码;CLI 经 `Provider.instantiate` 间接使用,无需适配
1218
+ - **`@ai-zen/agents-core` raised to `^4.3.0`, `@ai-zen/agents-sdk` raised to `^0.11.0`** — `src/version.ts` reads the actually installed version, so the version banner now shows core `4.3.0` / sdk `0.11.0`. SDK 0.11.0's breaking change (removal of `createCallSkillSubAgentTool`'s third parameter `provider`) only affects code that calls that factory function directly; the CLI uses it indirectly through `Provider.instantiate` and needs no adaptation
1066
1219
 
1067
- ### ✅ 测试
1220
+ ### ✅ Tests
1068
1221
 
1069
- - 沿用既有 9 文件 / 74 用例,全部通过;`tsc --noEmit` 与 `npm run build`(`0.8.1`)均通过
1222
+ - Reuses the existing 9 files / 74 cases, all passing; `tsc --noEmit` and `npm run build` (`0.8.1`) both pass
1070
1223
 
1071
1224
  ## [0.8.0] - 2026-09-28
1072
1225
 
1073
- ### 🚀 新功能
1226
+ ### 🚀 New features
1074
1227
 
1075
- - **子 Agent 上下文护栏(`SubAgentGuardInstallerPlugin`)** — 补上「主 Agent 有护栏、子 Agent 无护栏」的缺口(新增 `src/sub-agent-guard-plugin.ts`):
1076
- - **背景**:SDK 的子 Agent 由 `AgentTool` / `AgentToolLazy` 在委派时 `new Agent({...})` 构建,新建实例不携带任何插件(`Agent._plugins` 每实例私有,不向子 Agent 传播),且子 Agent 走 `run()` 而非 `send()`(`onBeforeSend` / `onAfterSend` 不会触发)——因此主 Agent 上的 `ContextGuardPlugin` 与自动迁移插件对子 Agent 完全无效,子 Agent 的内循环没有约束
1077
- - **介入点**:core 4.2.0 的 `onSubAgentStart` 钩子在「子 Agent 已构建、尚未 `run()`」时由宿主 Agent 分发,载荷携带子 Agent 实例;`use()` 只是把插件推入 `_plugins`,而钩子分发每次都实时遍历该列表,故在钩子内安插对**本次 `run()` 立即生效**(无需 `init()`,`AgentTool` 也不调用子 Agent 的 `init()`)
1078
- - **约束口径**:把主 Agent 同款的 `ContextGuardPlugin` 装到子 Agent 上——同一 `maxTokens`(当前模型 `maxContextTokens`)、同样不传 ratio(沿用 SDK 默认 1.5),使「上下文 token 超过 `maxTokens` × 1.5 即中断」对子 Agent 同样成立;**不设轮次上限、不设工具调用次数上限**(仅此一项限制)
1079
- - **中断语义为「软中断」**:子 Agent 的 run 立即结束,异常经 `AgentTool` 的 `finally`(分发 `onSubAgentEnd`)上抛至父 Agent,而父 Agent 默认 `allowJsonParseError = true`,故被降级为一条工具结果文本(`执行工具 X 时出错: …`),父 Agent 继续下一轮——停掉烧钱的子 Agent,但不终止用户对话
1080
- - **递归覆盖**:安装器在委派时把自身一并装给子 Agent,故子 Agent 再委派(孙级)同样受约束(默认 SubAgent 定义为 `subagents: deny`,该路径默认不触发,仅在放开递归时生效)
1081
- - **覆盖范围**:仅 core 的 `AgentTool` / `AgentToolLazy` 两条委派路径(均分发 `onSubAgentStart`);SDK 的 `call_skill_sub_agent`(技能子 Agent)不分发该钩子,本次不覆盖
1228
+ - **Sub-agent context guard (`SubAgentGuardInstallerPlugin`)** — closes the gap where "the main Agent has a guard while sub-agents have none" (added `src/sub-agent-guard-plugin.ts`):
1229
+ - **Background**: SDK sub-agents are built by `AgentTool` / `AgentToolLazy` via `new Agent({...})` at delegation time; a newly built instance carries no plugins (`Agent._plugins` is per-instance and does not propagate to sub-agents), and sub-agents go through `run()` rather than `send()` (`onBeforeSend` / `onAfterSend` never fire) — therefore the `ContextGuardPlugin` and the auto-migration plugin on the main Agent have no effect at all on sub-agents, and a sub-agent's inner loop is unconstrained
1230
+ - **Intervention point**: core 4.2.0's `onSubAgentStart` hook is dispatched by the host Agent at the moment "the sub-agent has been built and `run()` has not been called yet", with a payload carrying the sub-agent instance; `use()` merely pushes a plugin into `_plugins`, and the hook dispatch iterates that list live every time, so installing into the sub-agent inside the hook takes effect **for the current `run()` immediately** (no `init()` needed, and `AgentTool` does not call a sub-agent's `init()` either)
1231
+ - **Constraint policy**: install the same `ContextGuardPlugin` the main Agent uses onto the sub-agent — the same `maxTokens` (the current model's `maxContextTokens`), likewise without a ratio (keeping the SDK default 1.5), so that "context tokens exceeding `maxTokens` × 1.5 aborts" holds for sub-agents as well; **no round limit and no tool-call-count limit are set** (this is the only constraint)
1232
+ - **Interruption semantics are a "soft abort"**: the sub-agent's run ends immediately, the exception is thrown up to the parent Agent through `AgentTool`'s `finally` (which dispatches `onSubAgentEnd`), and since the parent Agent has `allowJsonParseError = true` by default, it is downgraded to a single tool result text (`error while executing tool X: …`) and the parent Agent proceeds to the next round — the money-burning sub-agent is stopped without terminating the user's conversation
1233
+ - **Recursive coverage**: the installer installs itself into the sub-agent at delegation time, so further delegation by the sub-agent (grandchild level) is constrained as well (the default SubAgent definition is `subagents: deny`, so that path does not trigger by default and only takes effect when recursion is opened up)
1234
+ - **Coverage**: only core's two delegation paths `AgentTool` / `AgentToolLazy` (both dispatch `onSubAgentStart`); the SDK's `call_skill_sub_agent` (skill sub-agents) does not dispatch that hook and is not covered here
1082
1235
 
1083
- ### ⚠️ 依赖升级
1236
+ ### ⚠️ Dependency upgrades
1084
1237
 
1085
- - **`@ai-zen/agents-core` 升至 `^4.2.0`、`@ai-zen/agents-sdk` 升至 `^0.10.0`** — `src/version.ts` 由实际安装版本读取,版本横幅随之显示 core `4.2.0` / sdk `0.10.0`;SDK 0.10.0 新增的 `load_mcp` `include_manifest` 开关为向后兼容的可选参数(默认 `true`,行为与升级前逐字一致),CLI 无需适配
1238
+ - **`@ai-zen/agents-core` raised to `^4.2.0`, `@ai-zen/agents-sdk` raised to `^0.10.0`** — `src/version.ts` reads the actually installed version, so the version banner now shows core `4.2.0` / sdk `0.10.0`; the `include_manifest` switch of `load_mcp` added in SDK 0.10.0 is a backward-compatible optional parameter (default `true`, behavior byte-for-byte identical to before the upgrade), so the CLI needs no adaptation
1086
1239
 
1087
- ### 🔧 修复
1240
+ ### 🔧 Fixes
1088
1241
 
1089
- - **子 Agent 事件对齐 core 4.2.0 的改名与载荷结构** — core 4.2.0 将事件 `sub-agent` 更名为 `sub-agent-start`,载荷由 `{ agent: 子 Agent, ctx }` 改为 `SubAgentContext = { agent: 主 Agent, subAgent: 子 Agent, toolCallContext }`。`src/conversation-runner.ts` 同步适配:事件监听与注销改用 `sub-agent-start`(此前监听旧名将不再触发,子 Agent 流式渲染会静默失效);子 Agent 实例改取自 `subAgent` 字段,工具名改取自 `toolCallContext.tool_call.function.name`(此前读取已不存在的 `ctx` 字段会在 `sub-agent-end` 回调中抛错)
1242
+ - **Sub-agent events aligned with core 4.2.0's renames and payload structure** — core 4.2.0 renames the event `sub-agent` to `sub-agent-start` and changes the payload from `{ agent: sub-agent, ctx }` to `SubAgentContext = { agent: main agent, subAgent: sub-agent, toolCallContext }`. `src/conversation-runner.ts` adapted accordingly: event listening and unsubscription now use `sub-agent-start` (listening to the old name no longer fires, which would silently disable sub-agent streaming rendering); the sub-agent instance is now read from the `subAgent` field, and the tool name from `toolCallContext.tool_call.function.name` (previously reading the now-nonexistent `ctx` field threw inside the `sub-agent-end` callback)
1090
1243
 
1091
- ### ✅ 测试
1244
+ ### ✅ Tests
1092
1245
 
1093
- - 新增 `src/sub-agent-guard-plugin.test.ts`(6 个用例):安装器的装载清单与「不拒绝委派」、安插后对本次 `run()` 立即生效(无需 `init()`)、用量恰好等于硬上限时放行、首轮无 usage 数据时放行、阈值随传入 `maxTokens` 变化、孙级传染
1094
- - `tsc --noEmit` 类型检查、全量单测与 `npm run build` 通过(74 passed / 9 files)
1246
+ - Added `src/sub-agent-guard-plugin.test.ts` (6 cases): the installer's plugin list and "does not reject delegation", immediate effect on the current `run()` after installation (no `init()` needed), pass-through when usage exactly equals the hard limit, pass-through on the first round with no usage data, the threshold following the passed `maxTokens`, and grandchild-level propagation
1247
+ - `tsc --noEmit` type checking, the full unit suite and `npm run build` pass (74 passed / 9 files)
1095
1248
 
1096
1249
  ## [0.7.0] - 2026-09-20
1097
1250
 
1098
- ### 🚀 新功能
1251
+ ### 🚀 New features
1099
1252
 
1100
- - **自动迁移二次确认** — 上下文 token 超限触发的自动迁移不再静默执行,迁移前先向用户确认(新增 `src/auto-migrate-confirm-plugin.ts`):
1101
- - `AutoMigrateConfirmPlugin` 继承 SDK 的 `AutoMigratePlugin`,**仅在「超阈值」与「调用迁移」之间插入一次确认**:阈值判断、迁移调用与错误处理仍复用基类,实际迁移仍委托共享的 `migrationService`(与 `/migrate` 同一条链路)——即 SDK 侧「只负责触发」的语义不变,确认是 CLI 层的增量
1102
- - 确认框展示当前用量与阈值(如 `260000/250000 tokens`)并说明影响(生成交接文档 → 保存当前对话 → 开启新会话继续),**默认「是」**(回车即迁移,与 `/migrate` 默认值一致)
1103
- - 选择「否」仅跳过**本次**迁移,不记录隐式状态:当前对话不受影响(历史未被剔除),可继续提问或随时 `/migrate`;仍处超限状态时下次发送后再次询问,并提示继续增长至严重超限会被 `ContextGuardPlugin` 中断
1104
- - **非交互环境不询问**:仅在 stdin 与 stdout 均为 TTY 时弹出确认框;管道、重定向、e2e 脚本无确认通道,保持既有静默自动迁移行为(不阻塞等待输入、不额外输出,管道语义不变)
1105
- - 对话装配处(`src/conversation-runner.ts`)改注册 `AutoMigrateConfirmPlugin`,其余插件(`ContextGuardPlugin` 护栏位置)不变
1106
- - **对话内命令提示** — 对话输入行键入 `/` 即实时列出可用命令及说明,继续输入按前缀收敛候选,无匹配时提示「无匹配命令(输入 /help 查看全部)」:
1107
- - 新增 `src/slash-hint-prompt.ts`:继承 inquirer 的 input 提示并覆写 `render`,将候选命令作为底栏(`bottomContent`)交由 inquirer 的 `ScreenManager` 渲染,复用其折行、终端缩放、擦除与光标归位处理,渲染表现与其它提示一致
1108
- - **仅提示,不改变提交语义**:回车始终提交输入原文,命令仍由 `dispatchCommand` 按完整名称精确匹配分发(不做前缀补全、不做高亮选择)
1109
- - 非 TTY 环境(stdin/stdout 重定向、e2e 脚本)自动不渲染提示,退化为普通输入行,既有输出行为不变
1110
- - 普通聊天内容(非 `/` 开头)不产生任何提示
1111
- - 对话主循环输入改由 `src/slash-hint-prompt.ts` 的 `askInput()` 提供(`src/conversation-runner.ts` 不再直接构造输入提示)
1253
+ - **Second confirmation for auto-migration** — auto-migration triggered by exceeding the context token limit no longer runs silently; the user is asked to confirm before migrating (added `src/auto-migrate-confirm-plugin.ts`):
1254
+ - `AutoMigrateConfirmPlugin` extends the SDK's `AutoMigratePlugin` and **only inserts one confirmation between "threshold exceeded" and "migration invoked"**: threshold checking, migration invocation and error handling still reuse the base class, and the actual migration is still delegated to the shared `migrationService` (the same chain as `/migrate`) — i.e. the SDK-side semantics of "only responsible for triggering" are unchanged, and the confirmation is an increment on the CLI layer
1255
+ - The confirmation dialog shows the current usage and the threshold (e.g. `260000/250000 tokens`) and explains the impact (generate a handoff document → save the current conversation → start a new session and continue), and **defaults to "yes"** (Enter migrates, consistent with `/migrate`'s default)
1256
+ - Choosing "no" only skips **this** migration and records no implicit state: the current conversation is unaffected (the history is not trimmed), and the user can keep asking or run `/migrate` at any time; when still over the limit, the prompt appears again after the next send, noting that continued growth into severe overflow will be aborted by `ContextGuardPlugin`
1257
+ - **No prompt in non-interactive environments**: the confirmation dialog is only shown when both stdin and stdout are TTYs; pipes, redirection and e2e scripts have no confirmation channel, so the existing silent auto-migration behavior is kept (no blocking on input, no extra output, pipe semantics unchanged)
1258
+ - The conversation wiring (`src/conversation-runner.ts`) now registers `AutoMigrateConfirmPlugin`, while the other plugins (including the `ContextGuardPlugin` guard position) are unchanged
1259
+ - **In-conversation command hints** — typing `/` on the chat input line immediately lists the available commands and their descriptions; further input narrows the candidates by prefix, and when nothing matches it shows "no matching command (type /help to see all)":
1260
+ - Added `src/slash-hint-prompt.ts`: it extends inquirer's input prompt and overrides `render`, handing the candidate commands to inquirer's `ScreenManager` as the bottom bar (`bottomContent`), so its wrapping, terminal resize, erasing and cursor restoration handling is reused and rendering matches the other prompts
1261
+ - **Hints only, submission semantics unchanged**: Enter always submits the input verbatim, and commands are still dispatched by `dispatchCommand` using exact matching on the full name (no prefix completion, no highlighted selection)
1262
+ - In non-TTY environments (stdin/stdout redirected, e2e scripts) the hint is simply not rendered and it degrades to a plain input line, with the existing output behavior unchanged
1263
+ - Ordinary chat content (not starting with `/`) produces no hint at all
1264
+ - The main conversation loop now gets its input from `askInput()` in `src/slash-hint-prompt.ts` (`src/conversation-runner.ts` no longer constructs the input prompt directly)
1112
1265
 
1113
- ### 🎯 优化
1266
+ ### 🎯 Optimizations
1114
1267
 
1115
- - **对话命令清单收敛为唯一来源** — 新增 `src/conversation-commands/registry.ts` 集中声明命令名、别名与说明,由分发(`index.ts`)、帮助(`help.ts`)与输入提示三处共用:
1116
- - **别名不再重复登记**:registry 声明的别名由 `index.ts` 自动展开为处理函数表的键;registry 声明了命令却未注册处理函数时于启动阶段即报错(fail-fast)
1117
- - `/help` 输出改由 registry 渲染,命令列按最长命令串对齐,与输入提示列宽一致(命令与说明文案保持不变)
1118
- - `getCommandNames()` 迁至 registry,`conversation-commands/index.ts` 原样再导出,调用方无需改动
1268
+ - **The conversation command list is collapsed into a single source** — added `src/conversation-commands/registry.ts`, which declares command names, aliases and descriptions in one place, shared by dispatch (`index.ts`), help (`help.ts`) and input hints:
1269
+ - **Aliases are no longer registered twice**: aliases declared by the registry are automatically expanded by `index.ts` into keys of the handler table; if the registry declares a command with no registered handler, an error is raised at startup (fail-fast)
1270
+ - `/help` output is now rendered by the registry, with the command column aligned to the longest command string and consistent with the input hint's column width (the command and description texts are unchanged)
1271
+ - `getCommandNames()` moved to the registry, and `conversation-commands/index.ts` re-exports it as before, so callers need no changes
1119
1272
 
1120
- ### ⚠️ 依赖约束
1273
+ ### ⚠️ Dependency constraints
1121
1274
 
1122
- - **引用了 inquirer 内部模块** `inquirer/lib/prompts/input.js` — inquirer 9 未导出 BasePrompt/InputPrompt,实现命令提示底栏只能深层导入(inquirer@9 的 `package.json` 无 `exports` 字段,ESM 深层导入已在 Node 26 + inquirer 9.3.8 下实测可用)。类型声明见 `src/typings/inquirer-internals.d.ts`;**升级 inquirer 主版本时须回归验证该模块路径与 `render` 行为**
1123
- - **未新增运行时依赖**,`pnpm-lock.yaml` 不变
1275
+ - **References the inquirer internal module** `inquirer/lib/prompts/input.js` — inquirer 9 does not export BasePrompt/InputPrompt, so implementing the command-hint bottom bar requires a deep import (inquirer@9's `package.json` has no `exports` field, and ESM deep imports were verified working on Node 26 + inquirer 9.3.8). Type declarations live in `src/typings/inquirer-internals.d.ts`; **when upgrading inquirer's major version, that module path and the `render` behavior must be re-verified**
1276
+ - **No new runtime dependency**, `pnpm-lock.yaml` unchanged
1124
1277
 
1125
- ### ✅ 测试
1278
+ ### ✅ Tests
1126
1279
 
1127
- - 新增 `src/auto-migrate-confirm-plugin.test.ts`(阈值判断、确认与拒绝分支、非交互降级、TTY 判定与默认确认文案)
1128
- - 新增 `src/slash-hint-prompt.test.ts`(提示文本生成 + 底栏渲染,覆盖 TTY / 非 TTY / 已提交 / 错误行分支)
1129
- - 新增 `src/conversation-commands/registry.test.ts`(元数据、别名折叠、前缀匹配、命令分发与别名等价)
1130
- - `tsc` 类型检查与全量单测通过(68 passed / 8 files),`npm run build` 成功
1131
- - 构建产物在真实 TTY 下实测:输入 `/` 列出全部命令、`/b` 收敛为 `/back`、回车提交原文且提示行正确擦除;`/help` 输出格式与列宽正常
1280
+ - Added `src/auto-migrate-confirm-plugin.test.ts` (threshold checks, the confirm and decline branches, non-interactive degradation, TTY detection and the default confirmation text)
1281
+ - Added `src/slash-hint-prompt.test.ts` (hint text generation + bottom bar rendering, covering the TTY / non-TTY / submitted / error-line branches)
1282
+ - Added `src/conversation-commands/registry.test.ts` (metadata, alias folding, prefix matching, command dispatch and alias equivalence)
1283
+ - `tsc` type checking and the full unit suite pass (68 passed / 8 files), and `npm run build` succeeds
1284
+ - The build output was verified on a real TTY: typing `/` lists all commands, `/b` narrows to `/back`, Enter submits the raw text and the hint line is erased correctly; `/help` output format and column widths are normal
1132
1285
 
1133
1286
  ## [0.6.3] - 2026-09-01
1134
1287
 
1135
- ### 🔧 修复
1288
+ ### 🔧 Fixes
1136
1289
 
1137
- - **修复迁移后旧消息既未标记 omit 也未物理删除的问题(依赖升级)** — 上游 `@ai-zen/agents-sdk` 0.9.3 修复 `createAgent` 直接引用 `definition.messages` 导致对话 `append` 反向污染 Agent 定义模板的缺陷;此前该缺陷使任务迁移 `prune`/`omit` 策略失效(旧消息不被清理、却能注入断点消息)
1138
- - **恢复草稿/已保存对话时拷贝消息数组** — `src/agent-creator.ts` 由直接引用改为 `agent.messages = [...messages]`,避免后续 `append` 改写草稿/对话存档的内存数组
1139
- - **依赖跟随升级** — `@ai-zen/agents-sdk` 至 `0.9.3`(借助 caret 范围自动解析)
1290
+ - **Fixed old messages being neither marked omit nor physically deleted after migration (dependency upgrade)** — upstream `@ai-zen/agents-sdk` 0.9.3 fixes the defect where `createAgent` referenced `definition.messages` directly, causing conversation `append` to pollute the Agent definition template in reverse; that defect had made task migration's `prune`/`omit` strategies ineffective (old messages were not cleaned up, yet breakpoint messages could be injected)
1291
+ - **Copy the message array when restoring a draft / saved conversation** — `src/agent-creator.ts` changed from a direct reference to `agent.messages = [...messages]`, so later `append` calls do not rewrite the in-memory array of a draft / conversation archive
1292
+ - **Dependencies upgraded along** — `@ai-zen/agents-sdk` to `0.9.3` (resolved automatically through the caret range)
1140
1293
 
1141
- ### ✅ 测试
1294
+ ### ✅ Tests
1142
1295
 
1143
- - `tsc` 类型检查与全量单测通过(37 passed / 5 files)
1296
+ - `tsc` type checking and the full unit suite pass (37 passed / 5 files)
1144
1297
 
1145
1298
  ## [0.6.2] - 2026-08-31
1146
1299
 
1147
- ### 🎯 优化
1300
+ ### 🎯 Optimizations
1148
1301
 
1149
- - **启动版本 banner 增强** — 新增 `src/version.ts`,统一从实际安装包读取版本号(而非依赖声明中的版本范围);主菜单版本行由仅打印 CLI 版本扩展为同时展示 CLI / SDK / Core 三版本,例如 `AI-Zen CLI v0.6.2 · SDK 0.9.2 · Core 4.1.0`
1150
- - **适配 `@ai-zen/agents-core` 4.1.0 类型变更** — 上游将 `AgentNS.Message.id` 由可选改为**强制必填**(构造时自动生成)。`src/menus/agents.ts` 与 `src/conversation-commands/back.test.ts` 中直接构造的裸消息对象改用 `Message` 内置工厂方法创建(`Message.System` / `Message.User` / `Message.Assistant`,以及 `new Message(...)`),由 core 统一生成消息 `id`
1151
- - **依赖跟随升级** — `@ai-zen/agents-sdk` 至 `0.9.2`、`@ai-zen/agents-core` 至 `4.1.0`(借助 caret 范围自动解析)
1302
+ - **Startup version banner enhanced** — added `src/version.ts`, which uniformly reads version numbers from the actually installed packages (rather than the version ranges in the dependency declarations); the main menu version row is expanded from printing only the CLI version to showing the CLI / SDK / Core versions together, e.g. `AI-Zen CLI v0.6.2 · SDK 0.9.2 · Core 4.1.0`
1303
+ - **Adapted to `@ai-zen/agents-core` 4.1.0 type changes** — upstream changed `AgentNS.Message.id` from optional to **mandatory** (auto-generated on construction). Bare message objects constructed directly in `src/menus/agents.ts` and `src/conversation-commands/back.test.ts` now use `Message`'s built-in factory methods (`Message.System` / `Message.User` / `Message.Assistant`, and `new Message(...)`), letting core generate the message `id` uniformly
1304
+ - **Dependencies upgraded along** — `@ai-zen/agents-sdk` to `0.9.2`, `@ai-zen/agents-core` to `4.1.0` (resolved automatically through the caret ranges)
1152
1305
 
1153
- ### ✅ 测试
1306
+ ### ✅ Tests
1154
1307
 
1155
- - 依赖升级后 `tsc` 类型检查与全量单测通过(37 passed / 5 files),`npm run build` 成功
1308
+ - After the dependency upgrade, `tsc` type checking and the full unit suite pass (37 passed / 5 files), and `npm run build` succeeds
1156
1309
 
1157
1310
  ## [0.6.1] - 2026-08-28
1158
1311
 
1159
- ### 🎯 优化
1312
+ ### 🎯 Optimizations
1160
1313
 
1161
- - **升级 `@ai-zen/agents-sdk` 到 0.9.0** — 迁移服务新增 `strategy` 开关:
1162
- - **`omit`(默认)**:历史消息标记 `omit: true` 保留可审计,追加「对话断点」消息为新上下文起点
1163
- - **`prune`(CLI 采用)**:物理剔除历史,仅保留系统提示 + 交接断点(即先前替换行为)
1164
- - CLI `migration-service.ts` 显式启用 `strategy: "prune"`,与端侧「迁移后开启新会话」的产品语义一致,**对外使用无差异**
1314
+ - **Upgraded `@ai-zen/agents-sdk` to 0.9.0** — the migration service gains a `strategy` switch:
1315
+ - **`omit` (default)**: historical messages are marked `omit: true` and kept for auditability, and a "conversation breakpoint" message is appended as the starting point of the new context
1316
+ - **`prune` (used by the CLI)**: history is physically removed, keeping only the system prompt + handoff breakpoint (i.e. the previous replacement behavior)
1317
+ - CLI `migration-service.ts` explicitly enables `strategy: "prune"`, consistent with the client-side product semantics of "start a new session after migration", with **no difference in external usage**
1165
1318
 
1166
- ### ✅ 测试
1319
+ ### ✅ Tests
1167
1320
 
1168
- - 依赖升级后单测全量通过(37 passed),`tsc --noEmit` 零错误
1321
+ - After the dependency upgrade the full unit suite passes (37 passed), and `tsc --noEmit` reports zero errors
1169
1322
 
1170
1323
  ## [0.6.0] - 2026-08-24
1171
1324
 
1172
- ### 💥 破坏性变更
1325
+ ### 💥 Breaking changes
1173
1326
 
1174
- - **升级 `@ai-zen/agents-sdk` 到 0.8.0** — 任务迁移能力在 SDK 内完成重构(详见 SDK CHANGELOG):
1175
- - **`TaskMigrationService` 变为实例化、自包含迁移服务** —— 构造仅接收 `{ onBeforeMigrate?, onMigrated?, logger? }`(钩子平铺,无 `hooks` 包装),`migrate({ agent })` 直接复用传入 `agent` 的 `client`/`model`/`modelConfig` 生成交接文档,无需 Provider、无需独立迁移 Agent;暴露结构化 `MigrationContext`
1176
- - **`AutoMigratePlugin` 收敛为仅触发角色** —— 构造注入 `{ service, maxTokens }`,只检测 token 超限并委托 `service.migrate`
1177
- - **CLI 移除 `createMigrationAgent`** —— 删除 `src/agent-creator.ts` 中依赖已删除 `TaskMigrationService.createAgentDefinition` 的迁移 Agent 构建;手动迁移命令与自动迁移回调统一改为使用 SDK 迁移服务实例,钩子平铺在服务构造上
1327
+ - **Upgraded `@ai-zen/agents-sdk` to 0.8.0** — the task migration capability was refactored inside the SDK (see the SDK CHANGELOG for details):
1328
+ - **`TaskMigrationService` becomes an instantiable, self-contained migration service** — the constructor takes only `{ onBeforeMigrate?, onMigrated?, logger? }` (hooks flat, no `hooks` wrapper), and `migrate({ agent })` directly reuses the passed `agent`'s `client`/`model`/`modelConfig` to generate the handoff document, with no Provider and no dedicated migration Agent needed; it exposes a structured `MigrationContext`
1329
+ - **`AutoMigratePlugin` narrows to a trigger-only role** — the constructor injects `{ service, maxTokens }`, and it only detects token overflow and delegates to `service.migrate`
1330
+ - **The CLI removes `createMigrationAgent`** — the migration Agent construction in `src/agent-creator.ts` that depended on the deleted `TaskMigrationService.createAgentDefinition` is removed; the manual migration command and the auto-migration callback both now use the SDK migration service instance, with hooks flat on the service constructor
1178
1331
 
1179
- ### 🚀 新功能
1332
+ ### 🚀 New features
1180
1333
 
1181
- - **手动任务迁移命令 `/migrate`** — 对话中随时输入 `/migrate` 即可主动触发任务迁移,无需等待 token 超限:
1182
- - 迁移动作:保存当前对话 → 生成交接文档 → 开启新会话(注入交接文档为上下文)→ 新对话落盘为草稿(`_current.json`)
1183
- - 与自动迁移联动复用 SDK 的 `TaskMigrationService.migrate`,通过 `onMigrated` 钩子处理后保存
1184
- - 非破坏性:旧对话已保存到 `conversations/`,生成失败时可重试或继续当前对话
1334
+ - **Manual task migration command `/migrate`** — typing `/migrate` in a conversation triggers task migration at any time, without waiting for the token limit to be exceeded:
1335
+ - Migration steps: save the current conversation → generate a handoff document → start a new session (injecting the handoff document as context) → write the new conversation to disk as a draft (`_current.json`)
1336
+ - Reuses the SDK's `TaskMigrationService.migrate` together with auto-migration, handling the post-migration save through the `onMigrated` hook
1337
+ - Non-destructive: the old conversation is already saved under `conversations/`, so on failure the user can retry or continue the current conversation
1185
1338
 
1186
- ### 🎯 优化
1339
+ ### 🎯 Optimizations
1187
1340
 
1188
- - **迁移服务单实例收敛** — 将 `TaskMigrationService` 实例挂载到 `ConversationContext.migrationService`,自动迁移(`AutoMigratePlugin` 触发)与手动迁移(`/migrate` 命令)共用**同一个实例**;迁移前后处理(保存旧对话 / 开启新会话 / 落盘草稿)统一收敛到该服务钩子,消除自动与手动两侧各自 `new TaskMigrationService` 及重复的后处理实现
1189
- - **迁移服务抽离为独立模块** — 新增 `src/migration-service.ts`,由 `createMigrationService(ctx)` 统一构建迁移服务实例(含迁移前后钩子);`saveCurrentConversation` 提升为 `conversation-repository.ts` 的共享 `saveConversation`,供迁移钩子与错误保存共用
1341
+ - **The migration service is collapsed to a single instance** — the `TaskMigrationService` instance is mounted on `ConversationContext.migrationService`, shared by auto-migration (triggered by `AutoMigratePlugin`) and manual migration (the `/migrate` command) as **the same instance**; pre/post-migration handling (saving the old conversation / starting a new session / writing the draft to disk) is unified into that service's hooks, eliminating the separate `new TaskMigrationService` on both the automatic and the manual side and the duplicated post-processing implementations
1342
+ - **The migration service is extracted into a standalone module** — added `src/migration-service.ts`, where `createMigrationService(ctx)` builds the migration service instance uniformly (including the pre/post-migration hooks); `saveCurrentConversation` is promoted to the shared `saveConversation` in `conversation-repository.ts`, used by both the migration hooks and error saving
1190
1343
 
1191
- ### ✅ 测试
1344
+ ### ✅ Tests
1192
1345
 
1193
- - SDK 侧:`TaskMigrationService`(含 `migrate` / 序列化 / 钩子上下文)、`AutoMigratePlugin`(触发委托断言)全量通过(447 passed / 2 skipped)
1194
- - CLI 侧:`pretest` 类型检查与全量单测通过(37 passed),`npm run build` 成功
1346
+ - SDK side: `TaskMigrationService` (including `migrate` / serialization / hook context) and `AutoMigratePlugin` (trigger delegation assertions) all pass (447 passed / 2 skipped)
1347
+ - CLI side: `pretest` type checking and the full unit suite pass (37 passed), and `npm run build` succeeds
1195
1348
 
1196
1349
  ## [0.5.0] - 2026-08-23
1197
1350
 
1198
- ### 💥 破坏性变更
1351
+ ### 💥 Breaking changes
1199
1352
 
1200
- - **升级 `@ai-zen/agents-core` 到 4.0.0、`@ai-zen/agents-sdk` 到 0.7.0** — 随底层核心重构(官方 OpenAI SDK + 插件化 Agent 驱动层):
1201
- - **`createModel()` 返回结构变化** — 从返回旧 `ChatCompletionModel` 实例改为 `{ client, model, modelConfig }`(openai SDK client + 模型名 + 模型参数);`createMigrationAgent` 相应解构后传入 `SdkAgent` 新构造签名
1202
- - **`SdkAgent` 构造签名变化** — 由 `{ ..., model }`(模型对象)改为 `{ client, model, modelConfig }`
1203
- - **CLI 其余部分完全兼容** — 流式事件(`open`/`chunk`/`error`/`sub-agent`/`sub-agent-end`)、插件(`AutoMigratePlugin`/`AutoRefreshToolsPlugin`/`ContextGuardPlugin`)、`AgentNS` 类型(`Delta`/`StreamResponseData`/`Message`)在 core 4.0.0 / sdk 0.7.0 中原样保留,`AgentPlugin`/`SendContext` 由 sdk 重新导出,无破坏
1353
+ - **Upgraded `@ai-zen/agents-core` to 4.0.0 and `@ai-zen/agents-sdk` to 0.7.0** — along with the underlying core refactor (official OpenAI SDK + plugin-based Agent driver layer):
1354
+ - **`createModel()` return structure changed** — instead of returning an old `ChatCompletionModel` instance it now returns `{ client, model, modelConfig }` (openai SDK client + model name + model parameters); `createMigrationAgent` destructures it accordingly and passes it to `SdkAgent`'s new constructor signature
1355
+ - **`SdkAgent` constructor signature changed** — from `{ ..., model }` (a model object) to `{ client, model, modelConfig }`
1356
+ - **The rest of the CLI is fully compatible** — stream events (`open`/`chunk`/`error`/`sub-agent`/`sub-agent-end`), plugins (`AutoMigratePlugin`/`AutoRefreshToolsPlugin`/`ContextGuardPlugin`) and `AgentNS` types (`Delta`/`StreamResponseData`/`Message`) are kept as-is in core 4.0.0 / sdk 0.7.0, and `AgentPlugin`/`SendContext` are re-exported by the sdk, with no breakage
1204
1357
 
1205
- ### ✅ 测试
1358
+ ### ✅ Tests
1206
1359
 
1207
- - 依赖升级后单测全量通过(37 passed / 0 failed),`tsc --noEmit` 零错误
1360
+ - After the dependency upgrade the full unit suite passes (37 passed / 0 failed), and `tsc --noEmit` reports zero errors
1208
1361
 
1209
- ### 🎯 优化
1362
+ ### 🎯 Optimizations
1210
1363
 
1211
- - **`test:all` / `test:e2e` 前置构建** — e2e 测试运行 `dist/index.js`,此前若 `dist` 缺失或为旧构建会导致 e2e 失败(升级后曾因旧 `dist` 报 `AgentContext must have a client`)。现于 e2e 前自动 `npm run build`,干净环境(CI clone 后)可开箱运行
1364
+ - **`test:all` / `test:e2e` pre-build** — e2e tests run `dist/index.js`, and previously a missing or stale `dist` caused e2e failures (after an upgrade it once reported `AgentContext must have a client` because of a stale `dist`). `npm run build` now runs automatically before e2e, so a clean environment (a fresh CI clone) works out of the box
1212
1365
 
1213
1366
  ## [0.4.0] - 2026-08-05
1214
1367
 
1215
- ### 🚀 新功能
1368
+ ### 🚀 New features
1216
1369
 
1217
- - **升级 `@ai-zen/agents-core` 到 3.4.0、`@ai-zen/agents-sdk` 到 0.6.0** — 随底层升级带来开箱即用的增强能力:
1218
- - **内置工具中断信号(abort)支持** — `sleep`/`exec`/`downloadFile`/`generateImage`/`glob`/`findText` 及 subAgent 体系监听 `abort` 信号,CLI 终止对话时超长耗时操作可及时中断,避免资源滞留(`exec` 可区分 `aborted` 与 `timeout` 终止原因)
1219
- - **工具定义内聚重构** — `Tool` 基类瘦身(不再强制传 `type`/`function`),定义与实现同处一类,回调签名由 `this` 注入改为显式 `(parsed_args, ctx)` 传参,`SdkCallbackTool` 构造改为 `{env}` 容器并透传完整 `ToolCallContext`
1220
- - **MCP 工具 signal 透传** — `call_mcp_tool` / `read_mcp_resource` 支持中断
1221
- - cli 层未直接使用上述受影响 API,升级为无破坏性平滑升级,功能不受影响
1370
+ - **Upgraded `@ai-zen/agents-core` to 3.4.0 and `@ai-zen/agents-sdk` to 0.6.0** — the underlying upgrade brings out-of-the-box enhancements:
1371
+ - **Abort signal support for built-in tools** — `sleep`/`exec`/`downloadFile`/`generateImage`/`glob`/`findText` and the subAgent system listen to the `abort` signal, so long-running operations can be interrupted promptly when the CLI terminates a conversation, avoiding stranded resources (`exec` can distinguish `aborted` from `timeout` termination reasons)
1372
+ - **Tool definition cohesion refactor** — the `Tool` base class is slimmed down (no longer requires passing `type`/`function`), definition and implementation live in the same class, the callback signature changes from `this` injection to explicit `(parsed_args, ctx)` parameters, and the `SdkCallbackTool` constructor becomes an `{env}` container passing through the full `ToolCallContext`
1373
+ - **MCP tool signal pass-through** — `call_mcp_tool` / `read_mcp_resource` support interruption
1374
+ - The CLI layer does not use the affected APIs directly, so this is a smooth non-breaking upgrade with no functional impact
1222
1375
 
1223
- ### ✅ 测试
1376
+ ### ✅ Tests
1224
1377
 
1225
- - 依赖升级后单测全量通过(37 passed / 0 failed)
1378
+ - After the dependency upgrade the full unit suite passes (37 passed / 0 failed)
1226
1379
 
1227
1380
  ## [0.3.5] - 2026-08-13
1228
1381
 
1229
- ### 🎯 优化
1382
+ ### 🎯 Optimizations
1230
1383
 
1231
- - **升级 `@ai-zen/agents-core` 到 3.3.1、`@ai-zen/agents-sdk` 到 0.5.7** — 跟随上游内部重构,开箱即用,CLI 无代码改动:
1232
- - **`innerLoopTasks` 拆分为双集合语义** — Core 3.3.1 新增 `innerLoopsTasks`(整组任务记录)与 `innerLoopTasks`(当前轮活跃任务);`abort()` 只中止当前轮活跃任务,不再误标已完成的轮次
1233
- - **内循环开头统一追加 Assistant 占位** — `send()` / `AgentTool` 不再手动追加 Assistant,由 `run()` 每次内循环开头统一处理,多轮工具调用同样收敛到该处
1234
- - **SDK 0.5.7 同步升级** — 依赖 core 3.3.1,全部 426 个测试(含真实 DeepSeek e2e)通过,完全兼容
1384
+ - **Upgraded `@ai-zen/agents-core` to 3.3.1 and `@ai-zen/agents-sdk` to 0.5.7** — following upstream internal refactors, out of the box, with no CLI code changes:
1385
+ - **`innerLoopTasks` split into two set semantics** — Core 3.3.1 adds `innerLoopsTasks` (whole-group task records) and `innerLoopTasks` (tasks active in the current round); `abort()` now only aborts tasks active in the current round and no longer mislabels completed rounds
1386
+ - **An Assistant placeholder is appended uniformly at the start of the inner loop** — `send()` / `AgentTool` no longer append the Assistant manually; `run()` handles it uniformly at the start of every inner loop, and multi-round tool calls converge there as well
1387
+ - **SDK 0.5.7 upgraded in sync** — depends on core 3.3.1, all 426 tests (including real DeepSeek e2e) pass, fully compatible
1235
1388
 
1236
1389
  ## [0.3.4] - 2026-08-14
1237
1390
 
1238
- ### 🎯 优化
1391
+ ### 🎯 Optimizations
1239
1392
 
1240
- - **升级 `@ai-zen/agents-core` 到 3.3.0、`@ai-zen/agents-sdk` 到 0.5.6** — 跟随上游统一重构,开箱即用,CLI 无代码改动:
1241
- - **`ToolCallContext` 统一贯穿「拦截决策 → 执行」** — Core 3.3.0 将 `FunctionCallContext` 统一为 `ToolCallContext`:`onToolCall` 钩子与 `Tool.exec(ctx)` 收同一个实例;新增 `tool_call`(统一形状)、`tool`(匹配到的工具)、`signal`(中止信号)字段,`toolCall` 改名为 `tool_call`。保留 `@deprecated FunctionCallContext` 兼容别名,旧代码无需改动
1242
- - **新增 `onToolCall` 拦截钩子** — Core 3.3.0 与 SDK 0.5.6 同时提供:每个工具调用执行前可**拒绝**(返回字符串 = 拒绝原因作为工具结果回给 LLM、工具不执行、继续下一轮);SDK 侧 `AgentPlugin` 支持该钩子(多插件按注册顺序调用,任一返回字符串即拒绝)
1393
+ - **Upgraded `@ai-zen/agents-core` to 3.3.0 and `@ai-zen/agents-sdk` to 0.5.6** — following the upstream unified refactor, out of the box, with no CLI code changes:
1394
+ - **`ToolCallContext` runs through "interception decision → execution"** — Core 3.3.0 unifies `FunctionCallContext` into `ToolCallContext`: the `onToolCall` hook and `Tool.exec(ctx)` receive the same instance; the fields `tool_call` (unified shape), `tool` (the matched tool) and `signal` (abort signal) are added, and `toolCall` is renamed to `tool_call`. A `@deprecated FunctionCallContext` compatibility alias is kept, so old code needs no changes
1395
+ - **New `onToolCall` interception hook** — provided by both Core 3.3.0 and SDK 0.5.6: before each tool call executes it can **reject** it (returning a string = the rejection reason returned to the LLM as the tool result, the tool does not execute, and the next round continues); on the SDK side `AgentPlugin` supports the hook (multiple plugins are called in registration order, and the first string returned rejects)
1243
1396
 
1244
1397
  ## [0.3.3] - 2026-08-05
1245
1398
 
1246
- ### 🎯 优化
1399
+ ### 🎯 Optimizations
1247
1400
 
1248
- - **依赖声明统一为版本范围** — `@ai-zen/agents-core` 与 `@ai-zen/agents-sdk` 由精确版本(`3.2.0` / `0.5.3`)改为 caret 范围(`^3.2.0` / `^0.5.3`),与 SDK 侧对 core 的 `workspace:^`(发布为 `^3.x`)语义对齐。允许各自主版本范围内的兼容升级,避免升级版本时反复改动依赖声明;实际解析版本保持 `3.2.0` / `0.5.3`(当前 latest),功能不受影响
1401
+ - **Dependency declarations unified as version ranges** — `@ai-zen/agents-core` and `@ai-zen/agents-sdk` changed from exact versions (`3.2.0` / `0.5.3`) to caret ranges (`^3.2.0` / `^0.5.3`), aligned with the SDK side's `workspace:^` for core (published as `^3.x`). This allows compatible upgrades within each major version and avoids repeatedly editing dependency declarations when upgrading; the resolved versions remain `3.2.0` / `0.5.3` (current latest), with no functional impact
1249
1402
 
1250
1403
  ## [0.3.2] - 2026-08-05
1251
1404
 
1252
- ### 🚀 新功能
1405
+ ### 🚀 New features
1253
1406
 
1254
- - **接入 `ContextGuardPlugin` 上下文安全护栏** — 在 `conversation-runner.ts` 的插件装配中注册 SDK 0.5.3 提供的 `ContextGuardPlugin`(置于 `AutoMigratePlugin` 之前,复用同一 `maxTokens`)。每次内循环发请求前检测用量,超过 `maxTokens×1.2`(超阈 20%)即抛出 `ContextOverflowError` 中断对话,防止读入超大文件等突发超限撑爆上下文;与迁移插件区间互补:正常超限走交接迁移,严重超限由护栏直接中断报错
1407
+ - **Wired in the `ContextGuardPlugin` context guard** — registers the `ContextGuardPlugin` provided by SDK 0.5.3 in `conversation-runner.ts`'s plugin assembly (placed before `AutoMigratePlugin`, reusing the same `maxTokens`). Before each inner-loop request it checks usage and, once `maxTokens×1.2` (20% over the threshold) is exceeded, throws `ContextOverflowError` to abort the conversation, preventing sudden overflow (such as reading a huge file) from blowing up the context; it complements the migration plugin's range: normal overflow goes through handoff migration, while severe overflow is aborted directly by the guard with an error
1255
1408
 
1256
- ### ✨ 优化
1409
+ ### ✨ Optimizations
1257
1410
 
1258
- - **修正 `conversation-runner.ts` 插件注册注释编号** — 统一连续编号(1 cwdTracker / 2 autoRefreshTools / 3 draftPlugin / 4 contextGuard / 5 autoMigrate),消除此前重复编号
1411
+ - **Fixed the plugin registration comment numbering in `conversation-runner.ts`** — unified consecutive numbering (1 cwdTracker / 2 autoRefreshTools / 3 draftPlugin / 4 contextGuard / 5 autoMigrate), removing the previous duplicate numbering
1259
1412
 
1260
1413
  ## [0.3.1] - 2026-08-05
1261
1414
 
1262
- ### 🚀 新功能
1415
+ ### 🚀 New features
1263
1416
 
1264
- - **升级 `@ai-zen/agents-sdk` 到 0.5.3** — 随 SDK 新能力开箱即用:
1265
- - **初始化默认释放 socket-pty MCP 配置** — SDK `bootstrap()` 新增 `ensureDefaultMcpConfig()`,CLI 首启在 `~/.ai-zen/mcp.json` 自动写入含 `socket-pty` 终端的默认 MCP 配置(`npx -y @ai-zen/socket-pty mcp`),文件已存在则幂等不覆盖
1266
- - **`load_mcp` 透传 server description** — 枚举各 MCP 服务器描述供 LLM 参考(对齐 `load_skill`)
1267
- - **新增 `ContextGuardPlugin` 上下文安全护栏** — 在发请求前检测用量,超过 `maxTokens×1.2`(超阈 20%)时抛出 `ContextOverflowError` 中断对话,防止读入超大文件导致上下文失控;与 `AutoMigratePlugin` 职责分离、区间互补
1417
+ - **Upgraded `@ai-zen/agents-sdk` to 0.5.3** — new SDK capabilities available out of the box:
1418
+ - **A default socket-pty MCP config is released on initialization** — SDK `bootstrap()` adds `ensureDefaultMcpConfig()`; on first start the CLI automatically writes a default MCP config containing the `socket-pty` terminal (`npx -y @ai-zen/socket-pty mcp`) to `~/.ai-zen/mcp.json`, idempotently and without overwriting an existing file
1419
+ - **`load_mcp` passes through the server description** — enumerates each MCP server's description for the LLM to reference (aligned with `load_skill`)
1420
+ - **New `ContextGuardPlugin` context guard** — checks usage before sending a request and, once `maxTokens×1.2` (20% over the threshold) is exceeded, throws `ContextOverflowError` to abort the conversation, preventing a huge file read from letting the context run away; responsibilities are separated from `AutoMigratePlugin` and the ranges complement each other
1268
1421
 
1269
- ### 🔧 修复
1422
+ ### 🔧 Fixes
1270
1423
 
1271
- - **自动迁移后未立即保存新对话为草稿** — `AutoMigratePlugin` 的迁移发生在 `onAfterSend`(所有内循环已结束、`DraftPlugin` 的 `onInnerLoopEnd` 不再触发),迁移替换 `agent.messages` 后的新开场白未被及时落盘,用户在中途退出会丢失迁移后的开场白。现于 `onMigrated` 回调中迁移完成后立即将新消息写入草稿(`_current.json`),与「原对话先保存为 `conversations/`」形成完整闭环
1424
+ - **The new conversation was not saved as a draft immediately after auto-migration** — `AutoMigratePlugin` migrates in `onAfterSend` (all inner loops have ended, so `DraftPlugin`'s `onInnerLoopEnd` no longer fires), so the new opening message produced after migration replaced `agent.messages` was not written to disk in time, and a user exiting midway lost the post-migration opening. The new messages are now written to the draft (`_current.json`) immediately after migration completes in the `onMigrated` callback, closing the loop together with "the original conversation is saved to `conversations/` first"
1272
1425
 
1273
- ### 🎯 优化
1426
+ ### 🎯 Optimizations
1274
1427
 
1275
- - **MCP 配置结构统一为业界标准 `mcpServers`** — 以 SDK(`discoverMcpServers`)为准,CLI 的 `McpConfig` 顶层字段由 `servers` 改为 `mcpServers`、server 传输字段由 `transport` 改为 `type`。消除格式漂移,CLI 管理界面(`zen config`)与 SDK 默认释放的 mcp.json 完全对齐,socket-pty 默认配置可被正确显示与管理
1428
+ - **The MCP config structure is unified on the industry-standard `mcpServers`** — following the SDK (`discoverMcpServers`), the CLI's `McpConfig` top-level field changes from `servers` to `mcpServers`, and the server transport field from `transport` to `type`. This eliminates format drift, fully aligning the CLI management UI (`zen config`) with the mcp.json released by the SDK by default, so the default socket-pty config can be displayed and managed correctly
1276
1429
 
1277
- ### ✅ 测试
1430
+ ### ✅ Tests
1278
1431
 
1279
- - 更新 `config.test.ts` 中 MCP 读写相关断言以匹配新的 `mcpServers` 结构与 `type` 字段
1432
+ - Updated the MCP read/write assertions in `config.test.ts` to match the new `mcpServers` structure and `type` field
1280
1433
 
1281
1434
  ## [0.3.0] - 2026-08-01
1282
1435
 
1283
- ### 💥 破坏性变更
1436
+ ### 💥 Breaking changes
1284
1437
 
1285
- - **会话/草稿持久化下放 CLI** — SDK 0.5.0 移除了会话/草稿产品层(`ConversationRepository` / `DraftRepository` / `AutoDraftPlugin`)。CLI 自建本地存储:
1286
- - 新增 `src/conversation-repository.ts` — `conversationRepository`(复用 SDK `EntityRepository`)
1287
- - 新增 `src/draft-repository.ts` — `DraftRepository` + `draftRepository` 单例(草稿无 id 字段,单独实现)
1288
- - 新增 `src/draft-plugin.ts` — CLI 草稿插件 `DraftPlugin`(替代被移除的 `AutoDraftPlugin`,`onInnerLoopEnd` 自动保存)
1289
- - **更新 `@ai-zen/agents-sdk` 到 0.5.0** — 内置工具类化(`SdkCallbackTool` + `ToolEnv` 注入)、`Provider` 支持 `cwd`/`env`、`generateImage` 类化为 `GenerateImageTool`
1438
+ - **Session/draft persistence moved down into the CLI** — SDK 0.5.0 removed the session/draft product layer (`ConversationRepository` / `DraftRepository` / `AutoDraftPlugin`). The CLI builds its own local storage:
1439
+ - Added `src/conversation-repository.ts` — `conversationRepository` (reusing the SDK's `EntityRepository`)
1440
+ - Added `src/draft-repository.ts` — `DraftRepository` + the `draftRepository` singleton (drafts have no id field, so it is implemented separately)
1441
+ - Added `src/draft-plugin.ts` — the CLI draft plugin `DraftPlugin` (replacing the removed `AutoDraftPlugin`, auto-saving in `onInnerLoopEnd`)
1442
+ - **Updated `@ai-zen/agents-sdk` to 0.5.0** — built-in tools turned into classes (`SdkCallbackTool` + `ToolEnv` injection), `Provider` supports `cwd`/`env`, `generateImage` turned into the `GenerateImageTool` class
1290
1443
 
1291
- ### 🎯 优化
1444
+ ### 🎯 Optimizations
1292
1445
 
1293
- - **依赖同步线上版本** — `@ai-zen/agents-core` `3.0.1`、`@ai-zen/agents-sdk` `0.5.0`(开发期曾使用本地 `link:` 调试,已还原为 npm 版本号)
1446
+ - **Dependencies synced to the published versions** — `@ai-zen/agents-core` `3.0.1`, `@ai-zen/agents-sdk` `0.5.0` (a local `link:` was used for debugging during development and has been restored to npm version numbers)
1294
1447
 
1295
1448
  ## [0.2.3] - 2026-07-29
1296
1449
 
1297
- ### 🎯 优化
1450
+ ### 🎯 Optimizations
1298
1451
 
1299
- - **更新 `@ai-zen/agents-sdk` 到 0.4.0** — load_mcp 返回值改为结构化 JSON(含完整 inputSchema),日志 API 改为全局单例 `getLogger()`/`setLogger()`
1452
+ - **Updated `@ai-zen/agents-sdk` to 0.4.0** — the load_mcp return value changed to structured JSON (including the full inputSchema), and the logging API changed to the global singleton `getLogger()`/`setLogger()`
1300
1453
 
1301
1454
  ## [0.2.2] - 2026-07-27
1302
1455
 
1303
- ### 🎯 优化
1456
+ ### 🎯 Optimizations
1304
1457
 
1305
- - **更新 `@ai-zen/agents-sdk` 到 0.3.4** — 包含 AutoMigratePlugin 钩子重命名(`onHandoff` → `onMigrated`)
1306
- - **保存旧对话的时机修正** — 从 `onMigrated`(原 `onHandoff`)移至 `onBeforeMigrate`,确保保存的是完整的旧对话历史
1458
+ - **Updated `@ai-zen/agents-sdk` to 0.3.4** — includes the AutoMigratePlugin hook rename (`onHandoff` → `onMigrated`)
1459
+ - **Fixed the timing of saving the old conversation** — moved from `onMigrated` (formerly `onHandoff`) to `onBeforeMigrate`, ensuring that what is saved is the complete old conversation history
1307
1460
 
1308
1461
  ## [0.2.1] - 2026-07-27
1309
1462
 
1310
- ### 🎯 优化
1463
+ ### 🎯 Optimizations
1311
1464
 
1312
- - **更新 `@ai-zen/agents-sdk` 到 0.3.3** — 包含默认 Agent/SubAgent 提示词优化(信息明确性要求、矛盾检测)
1465
+ - **Updated `@ai-zen/agents-sdk` to 0.3.3** — includes default Agent/SubAgent prompt improvements (information explicitness requirements, contradiction detection)
1313
1466
 
1314
1467
  ## [0.2.0] - 2026-07-26
1315
1468
 
1316
- ### 💥 破坏性变更
1469
+ ### 💥 Breaking changes
1317
1470
 
1318
- - **同步 SDK 0.3.2 异步 API** — `@ai-zen/agents-sdk` 从 `0.3.1` → `0.3.2`
1319
- - **`readConfig()` / `saveConfig()` 改为 async** — 因 SDK `ConfigManager` 全面异步化
1320
- - **`createAgent()` 恢复为 async** — 因 SDK `createAgent()` 改回异步
1321
- - **`installHook()` / `uninstallHook()` 改为 async** — Shell hook 安装/卸载使用 `fs.promises`
1471
+ - **Synced to SDK 0.3.2's async API** — `@ai-zen/agents-sdk` from `0.3.1` → `0.3.2`
1472
+ - **`readConfig()` / `saveConfig()` changed to async** — because the SDK's `ConfigManager` is fully async
1473
+ - **`createAgent()` restored to async** — because the SDK's `createAgent()` is async again
1474
+ - **`installHook()` / `uninstallHook()` changed to async** — shell hook install/uninstall uses `fs.promises`
1322
1475
 
1323
- ### 🎯 优化
1476
+ ### 🎯 Optimizations
1324
1477
 
1325
- - **全面消除同步文件 IO** — `config.ts`、`hook.ts`、`conversations.ts`、`config-display.ts` 中所有 `existsSync`、`readFileSync`、`writeFileSync`、`readdirSync`、`statSync`、`renameSync`、`appendFileSync`、`mkdirSync`、`unlinkSync` 替换为 `fs.promises` 异步 API
1326
- - **`getConversationsList()` 改为 async** — 对话列表读取使用异步文件 API
1327
- - **MCP 配置读写异步化** — `readMcpConfig()`、`writeMcpConfig()`、`readProjectMcpConfig()` 均改为 async
1478
+ - **Synchronous file IO fully eliminated** — every `existsSync`, `readFileSync`, `writeFileSync`, `readdirSync`, `statSync`, `renameSync`, `appendFileSync`, `mkdirSync` and `unlinkSync` in `config.ts`, `hook.ts`, `conversations.ts` and `config-display.ts` is replaced with the async `fs.promises` API
1479
+ - **`getConversationsList()` changed to async** — the conversation list is read through async file APIs
1480
+ - **MCP config read/write made async** — `readMcpConfig()`, `writeMcpConfig()` and `readProjectMcpConfig()` are all async now
1328
1481
 
1329
- ### ✅ 测试
1482
+ ### ✅ Tests
1330
1483
 
1331
- - 同步更新 `config.test.ts` 为 `async`/`await`,全部 43 个测试通过(含 6 个 E2E)
1484
+ - `config.test.ts` updated in sync to `async`/`await`, all 43 tests pass (including 6 E2E)
1332
1485
 
1333
1486
  ## [0.1.7] - 2026-07-21
1334
1487
 
1335
- ### 🔧 修复
1488
+ ### 🔧 Fixes
1336
1489
 
1337
- - **更新依赖**: `@ai-zen/agents-core` → `3.0.0-alpha.4`, `@ai-zen/agents-sdk` → `0.2.7`
1490
+ - **Updated dependencies**: `@ai-zen/agents-core` → `3.0.0-alpha.4`, `@ai-zen/agents-sdk` → `0.2.7`
1338
1491
 
1339
1492
  ## [0.1.6] - 2026-07-20
1340
1493
 
1341
- ### 🔧 修复
1494
+ ### 🔧 Fixes
1342
1495
 
1343
- - **更新 `@ai-zen/agents-sdk` 到 0.2.6** — 包含默认 SubAgent 函数名和描述修复,LLM 现在能正确识别子 Agent
1496
+ - **Updated `@ai-zen/agents-sdk` to 0.2.6** — includes fixes to the default SubAgent function name and description, so the LLM can now identify sub-agents correctly
1344
1497
 
1345
1498
  ## [0.1.5] - 2026-07-20
1346
1499
 
1347
- ### 🔧 修复
1500
+ ### 🔧 Fixes
1348
1501
 
1349
- - **更新 `@ai-zen/agents-sdk` 到 0.2.5** — 包含默认 SubAgent 权限修复,`general-assistant` 现在能正常使用工具
1502
+ - **Updated `@ai-zen/agents-sdk` to 0.2.5** — includes a fix to default SubAgent permissions, so `general-assistant` can now use tools normally