@ai-zen/cli 1.0.0-alpha.20 → 1.0.0-alpha.23
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 +205 -0
- package/README.md +45 -17
- package/README.zh.md +45 -17
- package/dist/agent-creator.d.ts.map +1 -1
- package/dist/agent-creator.js +32 -10
- package/dist/agent-creator.js.map +1 -1
- package/dist/agent-switch.d.ts +61 -0
- package/dist/agent-switch.d.ts.map +1 -0
- package/dist/agent-switch.js +83 -0
- package/dist/agent-switch.js.map +1 -0
- package/dist/agents-store.d.ts +6 -1
- package/dist/agents-store.d.ts.map +1 -1
- package/dist/agents-store.js +6 -1
- package/dist/agents-store.js.map +1 -1
- package/dist/auto-migrate-confirm.d.ts.map +1 -1
- package/dist/auto-migrate-confirm.js +2 -1
- package/dist/auto-migrate-confirm.js.map +1 -1
- package/dist/config-editor.d.ts +9 -1
- package/dist/config-editor.d.ts.map +1 -1
- package/dist/config-editor.js +30 -19
- package/dist/config-editor.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +29 -13
- package/dist/config.js.map +1 -1
- package/dist/conversation-commands/registry.d.ts +123 -4
- package/dist/conversation-commands/registry.d.ts.map +1 -1
- package/dist/conversation-commands/registry.js +92 -17
- package/dist/conversation-commands/registry.js.map +1 -1
- package/dist/conversation-persist-plugin.d.ts.map +1 -1
- package/dist/conversation-persist-plugin.js +6 -14
- package/dist/conversation-persist-plugin.js.map +1 -1
- package/dist/conversation-repository.d.ts +68 -14
- package/dist/conversation-repository.d.ts.map +1 -1
- package/dist/conversation-repository.js +81 -23
- package/dist/conversation-repository.js.map +1 -1
- package/dist/cwd-tracker-plugin.d.ts.map +1 -1
- package/dist/cwd-tracker-plugin.js +6 -2
- package/dist/cwd-tracker-plugin.js.map +1 -1
- package/dist/format-time.d.ts +17 -6
- package/dist/format-time.d.ts.map +1 -1
- package/dist/format-time.js +32 -14
- package/dist/format-time.js.map +1 -1
- package/dist/handoff-service.d.ts.map +1 -1
- package/dist/handoff-service.js +22 -19
- package/dist/handoff-service.js.map +1 -1
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +69 -15
- package/dist/hook.js.map +1 -1
- package/dist/i18n/en-wizard.d.ts +8 -0
- package/dist/i18n/en-wizard.d.ts.map +1 -0
- package/dist/i18n/en-wizard.js +290 -0
- package/dist/i18n/en-wizard.js.map +1 -0
- package/dist/i18n/en.d.ts +7 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +223 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/index.d.ts +57 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +109 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/preferences.d.ts +34 -0
- package/dist/i18n/preferences.d.ts.map +1 -0
- package/dist/i18n/preferences.js +69 -0
- package/dist/i18n/preferences.js.map +1 -0
- package/dist/i18n/zh-wizard.d.ts +11 -0
- package/dist/i18n/zh-wizard.d.ts.map +1 -0
- package/dist/i18n/zh-wizard.js +293 -0
- package/dist/i18n/zh-wizard.js.map +1 -0
- package/dist/i18n/zh.d.ts +10 -0
- package/dist/i18n/zh.d.ts.map +1 -0
- package/dist/i18n/zh.js +227 -0
- package/dist/i18n/zh.js.map +1 -0
- package/dist/index.js +41 -23
- package/dist/index.js.map +1 -1
- package/dist/mcp-command.d.ts +32 -0
- package/dist/mcp-command.d.ts.map +1 -0
- package/dist/mcp-command.js +37 -0
- package/dist/mcp-command.js.map +1 -0
- package/dist/mode.d.ts +31 -4
- package/dist/mode.d.ts.map +1 -1
- package/dist/mode.js +42 -7
- package/dist/mode.js.map +1 -1
- package/dist/model-switch.d.ts.map +1 -1
- package/dist/model-switch.js +5 -4
- package/dist/model-switch.js.map +1 -1
- package/dist/reasoning-effort.d.ts +23 -4
- package/dist/reasoning-effort.d.ts.map +1 -1
- package/dist/reasoning-effort.js +51 -24
- package/dist/reasoning-effort.js.map +1 -1
- package/dist/sdk-log.d.ts +23 -16
- package/dist/sdk-log.d.ts.map +1 -1
- package/dist/sdk-log.js +43 -19
- package/dist/sdk-log.js.map +1 -1
- package/dist/stdio-runner.d.ts.map +1 -1
- package/dist/stdio-runner.js +19 -19
- package/dist/stdio-runner.js.map +1 -1
- package/dist/sub-agent-guard-plugin.d.ts +5 -2
- package/dist/sub-agent-guard-plugin.d.ts.map +1 -1
- package/dist/sub-agent-guard-plugin.js +5 -2
- package/dist/sub-agent-guard-plugin.js.map +1 -1
- package/dist/tui/chat-session.d.ts +73 -0
- package/dist/tui/chat-session.d.ts.map +1 -1
- package/dist/tui/chat-session.js +167 -33
- package/dist/tui/chat-session.js.map +1 -1
- package/dist/tui/components.d.ts +43 -2
- package/dist/tui/components.d.ts.map +1 -1
- package/dist/tui/components.js +97 -4
- package/dist/tui/components.js.map +1 -1
- package/dist/tui/config-wizard/detail.d.ts +12 -6
- package/dist/tui/config-wizard/detail.d.ts.map +1 -1
- package/dist/tui/config-wizard/detail.js +33 -32
- package/dist/tui/config-wizard/detail.js.map +1 -1
- package/dist/tui/config-wizard/engine.d.ts.map +1 -1
- package/dist/tui/config-wizard/engine.js +26 -20
- package/dist/tui/config-wizard/engine.js.map +1 -1
- package/dist/tui/config-wizard/fields.d.ts.map +1 -1
- package/dist/tui/config-wizard/fields.js +176 -164
- package/dist/tui/config-wizard/fields.js.map +1 -1
- package/dist/tui/config-wizard/kv-editor.d.ts.map +1 -1
- package/dist/tui/config-wizard/kv-editor.js +9 -5
- package/dist/tui/config-wizard/kv-editor.js.map +1 -1
- package/dist/tui/config-wizard/list.d.ts.map +1 -1
- package/dist/tui/config-wizard/list.js +2 -1
- package/dist/tui/config-wizard/list.js.map +1 -1
- package/dist/tui/config-wizard/mcp-helpers.d.ts.map +1 -1
- package/dist/tui/config-wizard/mcp-helpers.js +4 -3
- package/dist/tui/config-wizard/mcp-helpers.js.map +1 -1
- package/dist/tui/config-wizard/menu.d.ts.map +1 -1
- package/dist/tui/config-wizard/menu.js +32 -19
- package/dist/tui/config-wizard/menu.js.map +1 -1
- package/dist/tui/config-wizard/prompt-editor.d.ts.map +1 -1
- package/dist/tui/config-wizard/prompt-editor.js +4 -3
- package/dist/tui/config-wizard/prompt-editor.js.map +1 -1
- package/dist/tui/config-wizard/prompt.d.ts.map +1 -1
- package/dist/tui/config-wizard/prompt.js +8 -7
- package/dist/tui/config-wizard/prompt.js.map +1 -1
- package/dist/tui/config-wizard/step-agents.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-agents.js +29 -20
- package/dist/tui/config-wizard/step-agents.js.map +1 -1
- package/dist/tui/config-wizard/step-defaults.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-defaults.js +21 -17
- package/dist/tui/config-wizard/step-defaults.js.map +1 -1
- package/dist/tui/config-wizard/step-endpoints.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-endpoints.js +16 -6
- package/dist/tui/config-wizard/step-endpoints.js.map +1 -1
- package/dist/tui/config-wizard/step-mcp.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-mcp.js +17 -19
- package/dist/tui/config-wizard/step-mcp.js.map +1 -1
- package/dist/tui/config-wizard/step-models.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-models.js +41 -26
- package/dist/tui/config-wizard/step-models.js.map +1 -1
- package/dist/tui/config-wizard/step-new-endpoint.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-new-endpoint.js +16 -12
- package/dist/tui/config-wizard/step-new-endpoint.js.map +1 -1
- package/dist/tui/config-wizard/step-simple.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-simple.js +18 -17
- package/dist/tui/config-wizard/step-simple.js.map +1 -1
- package/dist/tui/index.d.ts +17 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +25 -14
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/input-editing.d.ts +4 -1
- package/dist/tui/input-editing.d.ts.map +1 -1
- package/dist/tui/input-editing.js +7 -3
- package/dist/tui/input-editing.js.map +1 -1
- package/dist/tui/phase.d.ts +64 -0
- package/dist/tui/phase.d.ts.map +1 -0
- package/dist/tui/phase.js +112 -0
- package/dist/tui/phase.js.map +1 -0
- package/dist/tui/screens/agent-tabs.d.ts +53 -0
- package/dist/tui/screens/agent-tabs.d.ts.map +1 -0
- package/dist/tui/screens/agent-tabs.js +81 -0
- package/dist/tui/screens/agent-tabs.js.map +1 -0
- package/dist/tui/screens/chat-commands.d.ts +54 -3
- package/dist/tui/screens/chat-commands.d.ts.map +1 -1
- package/dist/tui/screens/chat-commands.js +469 -330
- package/dist/tui/screens/chat-commands.js.map +1 -1
- package/dist/tui/screens/chat.d.ts +8 -0
- package/dist/tui/screens/chat.d.ts.map +1 -1
- package/dist/tui/screens/chat.js +263 -406
- package/dist/tui/screens/chat.js.map +1 -1
- package/dist/tui/screens/common-keys.d.ts +36 -0
- package/dist/tui/screens/common-keys.d.ts.map +1 -0
- package/dist/tui/screens/common-keys.js +67 -0
- package/dist/tui/screens/common-keys.js.map +1 -0
- package/dist/tui/screens/handoff-choice.d.ts.map +1 -1
- package/dist/tui/screens/handoff-choice.js +4 -3
- package/dist/tui/screens/handoff-choice.js.map +1 -1
- package/dist/tui/screens/index.d.ts +3 -1
- package/dist/tui/screens/index.d.ts.map +1 -1
- package/dist/tui/screens/index.js +2 -1
- package/dist/tui/screens/index.js.map +1 -1
- package/dist/tui/screens/input-row.d.ts +64 -0
- package/dist/tui/screens/input-row.d.ts.map +1 -0
- package/dist/tui/screens/input-row.js +67 -0
- package/dist/tui/screens/input-row.js.map +1 -0
- package/dist/tui/screens/input.d.ts +13 -1
- package/dist/tui/screens/input.d.ts.map +1 -1
- package/dist/tui/screens/input.js +26 -12
- package/dist/tui/screens/input.js.map +1 -1
- package/dist/tui/screens/layout.d.ts +15 -0
- package/dist/tui/screens/layout.d.ts.map +1 -1
- package/dist/tui/screens/layout.js +14 -2
- package/dist/tui/screens/layout.js.map +1 -1
- package/dist/tui/screens/lines.d.ts.map +1 -1
- package/dist/tui/screens/lines.js +6 -6
- package/dist/tui/screens/lines.js.map +1 -1
- package/dist/tui/screens/messages-view.d.ts.map +1 -1
- package/dist/tui/screens/messages-view.js +5 -4
- package/dist/tui/screens/messages-view.js.map +1 -1
- package/dist/tui/screens/notice-bar.d.ts +5 -0
- package/dist/tui/screens/notice-bar.d.ts.map +1 -0
- package/dist/tui/screens/notice-bar.js +15 -0
- package/dist/tui/screens/notice-bar.js.map +1 -0
- package/dist/tui/screens/notice.d.ts +40 -8
- package/dist/tui/screens/notice.d.ts.map +1 -1
- package/dist/tui/screens/notice.js +75 -15
- package/dist/tui/screens/notice.js.map +1 -1
- package/dist/tui/screens/overlays.d.ts.map +1 -1
- package/dist/tui/screens/overlays.js +3 -2
- package/dist/tui/screens/overlays.js.map +1 -1
- package/dist/tui/screens/splash.d.ts +1 -1
- package/dist/tui/screens/splash.d.ts.map +1 -1
- package/dist/tui/screens/splash.js +5 -2
- package/dist/tui/screens/splash.js.map +1 -1
- package/dist/tui/screens/use-chat-viewport.d.ts +22 -0
- package/dist/tui/screens/use-chat-viewport.d.ts.map +1 -0
- package/dist/tui/screens/use-chat-viewport.js +50 -0
- package/dist/tui/screens/use-chat-viewport.js.map +1 -0
- package/dist/tui/screens/use-input-editing.d.ts +44 -0
- package/dist/tui/screens/use-input-editing.d.ts.map +1 -0
- package/dist/tui/screens/use-input-editing.js +192 -0
- package/dist/tui/screens/use-input-editing.js.map +1 -0
- package/dist/tui/screens/use-overlays.d.ts +25 -0
- package/dist/tui/screens/use-overlays.d.ts.map +1 -0
- package/dist/tui/screens/use-overlays.js +32 -0
- package/dist/tui/screens/use-overlays.js.map +1 -0
- package/dist/tui/screens/use-wizard-host.d.ts +35 -0
- package/dist/tui/screens/use-wizard-host.d.ts.map +1 -0
- package/dist/tui/screens/use-wizard-host.js +103 -0
- package/dist/tui/screens/use-wizard-host.js.map +1 -0
- package/dist/tui/status-segments.d.ts +37 -0
- package/dist/tui/status-segments.d.ts.map +1 -0
- package/dist/tui/status-segments.js +166 -0
- package/dist/tui/status-segments.js.map +1 -0
- package/dist/tui/text.d.ts +8 -5
- package/dist/tui/text.d.ts.map +1 -1
- package/dist/tui/text.js +95 -35
- package/dist/tui/text.js.map +1 -1
- package/dist/tui/theme.d.ts +2 -0
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +2 -0
- package/dist/tui/theme.js.map +1 -1
- package/dist/tui/token-rate.d.ts +83 -0
- package/dist/tui/token-rate.d.ts.map +1 -0
- package/dist/tui/token-rate.js +195 -0
- package/dist/tui/token-rate.js.map +1 -0
- package/dist/types.d.ts +0 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,208 @@
|
|
|
1
|
+
## [1.0.0-alpha.23] - 2026-10-28
|
|
2
|
+
|
|
3
|
+
> 子 Agent 从「通知栏里报一句运行中」升级为一条**可切换的栏目**:主 / 子 Agent
|
|
4
|
+
> 各自的消息分栏展示,执行状态内联在标签上。
|
|
5
|
+
|
|
6
|
+
### ✨ 新功能
|
|
7
|
+
|
|
8
|
+
- **主子 Agent 切换栏目** —— 对话中出现子 Agent 时,消息列表下方多出一条栏目
|
|
9
|
+
`主AGENT ◈子AGENT1 ◈子AGENT2`,当前项背景色高亮;`←` / `→` 切换后,消息列表
|
|
10
|
+
即换成该 Agent 自己的对话。
|
|
11
|
+
- **数据源**:core 的 `onSubAgentStart` 钩子给出子 Agent 实例;`AgentTool` /
|
|
12
|
+
`AgentToolLazy` 在委派结束后**不销毁**实例,`messages` 始终可读——所以只要存下
|
|
13
|
+
引用,就能回看子 Agent 的完整过程(思考、工具调用、多轮推理),而不只是最终结果。
|
|
14
|
+
- **生命周期与内循环对齐**:子 Agent 是内循环里工具调用的产物,本轮内循环全部结束时
|
|
15
|
+
(`send` 收尾;异常 / 中止走 `finally`,同样清)整条栏目清空,展示序号归零。
|
|
16
|
+
- **执行状态内联**:`◈` + 颜色(运行中蓝、完成绿、失败红),运行中的蓝点每 500ms
|
|
17
|
+
在亮 / 暗之间闪烁。**不再**往通知栏推「子任务运行中 / 已完成」——那会让通知栏
|
|
18
|
+
凭空多出一行,整帧高度跟着跳。
|
|
19
|
+
- **`←` / `→` 按状态分流**:输入框为空(光标停在 0,左右键本无事可做)或生成中
|
|
20
|
+
(输入框已被 Spinner 顶替)时切换栏目;输入框有内容时仍是移光标,不牺牲任何编辑能力。
|
|
21
|
+
- 发送消息时自动切回主 Agent 视图(否则用户看不到自己的提问)。
|
|
22
|
+
|
|
23
|
+
### 🐛 修复
|
|
24
|
+
|
|
25
|
+
- **`/new`、`/reload` 后子 Agent 残留** —— 这两个操作只换 `agent`、不重建 `ChatSession`
|
|
26
|
+
实例,子 Agent 视图不会自然消失;现随 `reset` / `reload` / `migrate` 显式清空。
|
|
27
|
+
- **视图 id 与展示序号混用** —— 序号每轮归零,若 id 也跟着归零,下一轮的 `sub-1` 会与
|
|
28
|
+
上一轮同名,而选中态按 id 匹配,残留的选中 id 会「接上」新出现的子 Agent。现拆成
|
|
29
|
+
两个计数器:id 永久递增、只让展示序号归零。
|
|
30
|
+
|
|
31
|
+
### ♻️ 重构
|
|
32
|
+
|
|
33
|
+
- **修正 `sub-agent-guard-plugin` 的过时注释** —— 原注释称 `call_skill_sub_agent`
|
|
34
|
+
「不分发 onSubAgentStart、暂不覆盖」,但 skill 子 Agent 统一到 `AgentToolLazy` 之后
|
|
35
|
+
它其实是被覆盖的(core 只有两条委派路径,均分发该钩子)。这条错注释在本次实现
|
|
36
|
+
前期直接误导了对覆盖面的判断。
|
|
37
|
+
|
|
38
|
+
### ✅ 测试
|
|
39
|
+
|
|
40
|
+
- 全量 **47 文件 / 626 例通过**(发布 alpha.22 时 604 例)。
|
|
41
|
+
- 新增护栏:栏目条目顺序与 1 空格 padding、符号 `◈` 与三色映射、超宽仍单行、
|
|
42
|
+
按序编号、结束转 `done`、出错转 `failed` 且不被 `end` 覆盖、进度不进通知栏、
|
|
43
|
+
执行期间有内容而收尾后清空、`reset` / `reload` 也清空、`←`/`→` 切换数据源与
|
|
44
|
+
输入非空时不切换、布局高度与 IME 偏移。
|
|
45
|
+
- 破坏性验证:去掉 `computeChatLayout` 里的 `agentTabLines` → 布局用例红
|
|
46
|
+
(`expected 4 to be 5`);命令说明写长 → 宽度护栏红(`expected 62 to be <= 36`)。
|
|
47
|
+
|
|
48
|
+
## [1.0.0-alpha.22] - 2026-10-28
|
|
49
|
+
|
|
50
|
+
> 这一版是**整体代码评审 + 治本修复**的成果:先通读 `src/` 全部非测试代码做全量评审
|
|
51
|
+
> (P0~P3 共 30 项,其中 3 项经实测查证为非问题、未改代码),再按「能查根因就查、
|
|
52
|
+
> 不加兜底/不吞异常/不做多余校验」的原则逐项落地,最后补上 Ctrl+C 中断交接。
|
|
53
|
+
|
|
54
|
+
### 🐛 修复
|
|
55
|
+
|
|
56
|
+
- **折行按字素簇计宽,修掉 emoji 撑破整帧布局(P0)** —— `toCells` / `wrapSpans` / `clipSpans` / `toChars` / `segmentsWidth` 原先按 **code point** 遍历文本:VS16 序列(`✌️` = `U+270C U+FE0F`)被算成 1 列而实际占 2 列 → 行超宽 → Ink 在渲染期二次折行 → 实际帧高比 `computeChatLayout` 预算多一行 → **输入框被顶出屏幕**;ZWJ 家族(`👨👩👧`)被算成 6 列而实际占 2 列 → 每两个簇就折一次。现在全部改走 `graphemes()` + `clusterWidth()`,与 Ink 排版同一把尺子。
|
|
57
|
+
- **阈值写入不再整对象替换** —— `updateModel` 是浅合并,此前调用方直接构造 `{ handoff, guard }`,会把 `tokenThresholds` 里其它字段(SDK 后续新增的压缩阈值等)一并清空。新增 `mergeTokenThresholds()`,所有阈值写入走它。
|
|
58
|
+
- **`/migrate` 之后屏幕一片空白** —— 迁移用 `prune` 策略把 `messages` 砍到 1~2 条,但 `transcriptFrom` 仍停在旧值,`messages.slice(transcriptFrom)` 恒为空:迁移成功、上下文有内容,屏幕上却什么都没有。现在迁移成功后重置转录显示起点(同 `/new` 的做法)。
|
|
59
|
+
- **通知栏只剩最后一条** —— 订阅端是单值 state,同一批摘要 / 分阶段进度会被逐条覆盖。改为保留最近 4 条的队列(`MAX_NOTICE_HISTORY`),并让同一批摘要合成**一条多行**,既全部显示又只占一个槽位;排版决策(折行 / 多条让位 / 条内截断)收进 `notice.ts` 纯逻辑,新增可独立渲染的 `NoticeBar`。
|
|
60
|
+
- **`ai hook --help` 以非零码退出** —— 「想看用法」与「写错了子动作」此前混为一谈。现在 `ai hook` / `ai hook --help` 正常输出、退出码 0;`ai hook boom` 报错并 exit 1。`ai help` / `ai version` 单词形式等价于对应选项,但**只在它是唯一参数时**判定(`ai help me refactor this` 仍是在提问)。
|
|
61
|
+
- **`ai hook install` 漏清重复块** —— rc 里可能残留多个钩子块(老版本 + 新版本各写一次),旧实现只看第一块,于是「第一块恰好是最新版 → 判定已安装 → 后面的旧块永远留着」。改为取全部块,只有「恰好一块且内容一致」才算已安装。
|
|
62
|
+
- **会话落盘不是原子写** —— SDK 的 `EntityRepository.write()` 是直接覆盖,进程在写一半时被杀(Ctrl+C / OOM / 断电)就留下半截 JSON,`read()` 解析失败返回 null,**整段历史就此丢失**(会话文件可达数 MB,写入窗口并不小)。改为「写 `<id>.json.tmp` → `rename`」,任何时刻读到的要么是旧文件、要么是完整新文件。
|
|
63
|
+
- **Ctrl+C 无法中断会话交接** —— 两处根因:`summary` 模式的独立模型请求没有任何取消点;`self` 模式虽能被 `agent.abort()` 打断,但 abort 不抛错,中止后被误判成「模型没写出文档」,于是**重试一轮、再降级 summary 再跑一轮**——用户以为取消了,实际多烧两轮全量输入。现在交接接受 `AbortSignal`:取消时抛 `HandoffAbortedError`、**不降级不重试**、清掉写了一半的文档、`migrate` 也不改动会话历史;TUI 里 Ctrl+C 会先走交接的取消通道,提示「已取消」而不是报错。(依赖 `@ai-zen/agents-sdk` 同名改动)
|
|
64
|
+
- **通知栏文案整体右移 2 列** —— 缩进被加了两遍:`NoticeBar` 的 `paddingX={1}` 一层,`notice.ts` 又硬拼 ` ` 一层,于是文案比同屏的分隔线与 `❯` 输入行多缩 2 列;迁移一次推 4 条通知时最刺眼。现在横向定位只归容器管,逻辑层只管折行。顺带去掉 `· ` / `✖ ` 前缀——语气已由 `tone` 着色表达(error 走红色),符号纯属占位;多行通知每行都从行首开始。配置更新的子项摘要改为「一个汉字宽度」(2 列)缩进,不再带项目符号。
|
|
65
|
+
|
|
66
|
+
### ✨ 新功能
|
|
67
|
+
|
|
68
|
+
- **`/load` 列表显示会话摘要** —— 会话 id 是时间戳(`chat_2026_10_28_00_56_34`),直接当标签列出来完全分不清哪条是哪条。现在取首条用户消息的第一行作为标签。实现上先只 `stat`、排序后**只为将要显示的那几条**解析内容,会话攒到上百条时 `/load` 也不会被拖慢。
|
|
69
|
+
|
|
70
|
+
### ♻️ 重构
|
|
71
|
+
|
|
72
|
+
- **`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 双击退出)。
|
|
73
|
+
- **斜杠命令改为表驱动** —— 557 行的 `switch` 换成 `HANDLERS: Record<CommandName, CommandHandler>` + 18 个处理器:「新增命令忘了写处理器」从运行时静默落到「未知命令」变成**编译错误**;命令名解析只剩 registry 一处(`parseCommandInput` / `parseCommandArgs` 共用同一套切分),`blocksInput` 成为命令元数据而不再是宿主白名单。
|
|
74
|
+
- **向导字段工厂去噪** —— `commit` / `toggle` 签名放宽为 `string | null | Promise<string | null>`,删掉 23 处 `Promise.resolve(...)` 噪音;`nameField()` / `endpointIdField()` / `customPatch()` 等收掉 8 处重复段;`DetailFieldKind` 的 `"map"` 改名 `"open"`(语义本就是「按 Enter 进子屏」)。
|
|
75
|
+
- **会话落盘收敛为唯一入口** —— 「读旧记录拿 `createdAt` → 写 → 更新指针」原先在 5 个地方各写一遍并且已经漂移(`saveConversation()` 漏写 `cwd`、stdio 的 `--save` 不更新指针、还写了一个已废弃的 `modelId`)。收敛到 `persistConversation()`,各处差异只剩「是否更新指针」一个显式开关。
|
|
76
|
+
- **初始化与单例改为缓存 Promise** —— `getScope()` / `ensureConfigDir()` 缓存实例时,`if (x) return x` 之后到赋值之间有一段 `await`,并发调用会一起穿透(建出两个 Scope → MCP 双连接、双份工具发现)。改缓存 Promise 让并发调用共享同一次初始化,失败不缓存。
|
|
77
|
+
- **删死代码与不可达兜底** —— 死字段 `ConversationContext.running|input|shouldSend`、从未启用的 i18n 复数机制(`pluralRules` + `key_one/_other`)、`noticeLines()` 末尾不可达的截断兜底(穷举 84 组证明从不执行)、`pick(direct, …)` 从没人传过的参数、`useChatViewport` 零引用的 `scrollTo` 导出、所有命令都空转一遍的 `parseHandoffMode`;`cwd` / `previewLimit` 从「可选 + 内部兜底」收成必填,兜底只保留在 SDK 可选字段的真实边界上。
|
|
78
|
+
- **命令说明文案精简** —— 斜杠菜单与 `/help` 都是一行一条,说明过长会把整行塞满。按「只写做什么、不写实现细节」重写:砍掉内部概念(`scope`、写入配置、重建会话)与冗余限定词,括号只留用户必须预期的后果(替换会话 / 重建 / 不切换会话)。中文最长 59 → **34**、英文 73 → **49**(平均 19.3 / 33.8),`/mcp` 从全场最长退到中游。
|
|
79
|
+
- **权限四维上屏走词条** —— `tools` / `skills` / `mcps` / `subagents` 是配置里的键,直接上屏等于把内部标识暴露给用户,映射到 `perm.*` 词条。
|
|
80
|
+
|
|
81
|
+
### ⚡ 性能
|
|
82
|
+
|
|
83
|
+
- **`graphemes()` 加快速路径** —— 折行是每帧热路径,但绝大多数文本(ASCII / CJK / 代码)每一簇就是一个 code point,不需要 `Intl.Segmenter`。先用一次正则判定「是否可能出现多 code point 合为一簇」(astral / CRLF / ZWNJ·ZWJ / 韩文组合字母 / `\p{M}` 组合附加符),命中才走分词器。一帧 60 行中文:**655 µs → 52 µs**(省 604 µs,帧预算 1%)。判定条件宁可误报不可漏报,新增 6 例护栏覆盖各类形态。
|
|
84
|
+
|
|
85
|
+
### 🔧 依赖
|
|
86
|
+
|
|
87
|
+
- 升级 `@ai-zen/agents-sdk` 至 `1.0.0-alpha.21`(交接取消能力)。
|
|
88
|
+
|
|
89
|
+
### ✅ 测试
|
|
90
|
+
|
|
91
|
+
- 全量 **46 文件 / 604 例通过**(评审前 36 文件 / 487 例,净增 10 文件 / 117 例护栏)。
|
|
92
|
+
- 重点新增:`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` 钉住原子写)。
|
|
93
|
+
- 破坏性验证(每项都确认护栏会红):折行改回 code point、`clampCursor` 改 `Array.from`、翻页步长改 `budget`、删掉 PgUp 分支、去掉 `\p{M}`、原子写改回直写、`clearDraft` 去掉历史游标复位。
|
|
94
|
+
|
|
95
|
+
## [1.0.0-alpha.21] - 2026-10-27
|
|
96
|
+
|
|
97
|
+
### ✨ 新功能
|
|
98
|
+
|
|
99
|
+
- **状态栏改为三个信息块,并统计生成速率** —— 此前状态栏是一条按优先级排列的文本流,窄屏折行时会把 `本轮 1284 tok` 这类单元从中间劈开,也让「哪段信息属于一类」看不出来。
|
|
100
|
+
- 现在分成三块:① 模型 · Agent · 思考强度 ② 本轮 / 累计 token 用量 · 生成速率 ③ 快捷键提示(垫底)。
|
|
101
|
+
- **换行以块为单位**:新增 `packSegments()`(`src/tui/status-segments.ts`)——放得下就并排(` · ` 连接),放不下就把**整块**挪到下一行;只有单块自身超过一整行时才退化成块内折行(优先空格断、CJK 硬断、行首孤儿 `·` 丢弃、行尾空格裁掉)。行边界只会落在块边界上。
|
|
102
|
+
- **行数确定**:`statusBarRows()` 在纯逻辑里量出实际行数并交给 `computeChatLayout({ statusBarLines })`,测量与渲染是同一个函数——窄屏下状态栏变高不会把输入框顶出屏幕。
|
|
103
|
+
- 数字与单位用 NBSP 绑定(`本轮␣1284␣tok`),块内也不会被拆成两行。
|
|
104
|
+
- **生成速率两口径,不混成一个数**:生成中 `⚡ 47 tok/s` 是估算值(按输出增量折算,CJK 0.7 / 字、其它非空白 0.25 / 字符、空白 0.1,取最近 2 秒滑动窗口);一轮结束后改用 `⚡ 均 38 tok/s`,分母是「首字 → 末字」跨度,分子是 API 返回的真实 `usage.completion_tokens`(多轮工具调用逐轮累加)。跨度 < 400ms 不显示,避免首包爆发把速率放成几百。
|
|
105
|
+
- **新增 `/agent`:会话内快速切换主 Agent** —— 选择器列出 `~/.ai-zen/agents/*.json` 的全部主 Agent,标注当前项与其绑定的模型名(换 Agent 等于连模型一起换)。
|
|
106
|
+
- 支持 `/agent <id>` 直切;无参弹选择器。
|
|
107
|
+
- 切换前做完整链路预检:Agent 存在 → 绑定了模型 → 模型还在配置里 → 端点在 → 端点有 Key。跑不起来的标「不可用」并给出原因,而不是切过去之后卡在启动错误屏。
|
|
108
|
+
- 写入 `config.defaultAgent`(与 `/config` →「默认 Agent」同一字段),随后重建会话;**消息与上下文保留**。不改 Agent 定义本身(提示词 / 权限 / `modelParams` 一概不碰)。
|
|
109
|
+
- **新增 `/mcp`:直达 MCP 管理界面** —— 不另建一套编辑器,直接把配置中心里已有的 MCP 屏打开(增 / 删 / 改 / 启停 / 全局⇄项目作用域切换都在那一屏)。关闭向导后沿用既有 `mcpChanged` 路径自动 `session.reload()`,新服务器当场生效。
|
|
110
|
+
- 默认落点:项目级有服务器就进 `project`,否则进 `global`(项目级为空时进去只会看到空列表)。`/mcp global`、`/mcp project`(可缩写 `g` / `p`)可指定。
|
|
111
|
+
- **新增 `/reload`:刷新运行环境** —— `session.reload()` 内含 `resetScope()`(断开旧 MCP 连接、丢掉配置快照)→ 重建 Agent → 重新发现工具 / Skill / MCP。手改 `mcp.json` / `skills/` / `tools/` 后一条命令生效,**消息与上下文保留**,无需重启 CLI。
|
|
112
|
+
- **中断时保留已输出的思考与内容** —— 按 `Ctrl+C` / `Esc` 停下后,模型**已经吐出来的那半句话不再凭空消失**。此前中断只是把在途消息标成 `Aborted`,而历史过滤器只放行 `completed`,于是屏幕上还留着的思考与回答在下一轮请求里整条不见,模型下一轮像失忆一样重来。
|
|
113
|
+
- 根因在上游:core 的 `abort()` 只管打标记、`formatHistory()` 只按状态过滤,两者都不关心「已经输出了什么」。core `4.5.3` 新增 `abortedMessageStrategy`(`drop` / `keep`,默认 `drop`),并导出 `reconcileAbortedMessages()`,在工具轮次归一**之后**收尾纯内容消息。
|
|
114
|
+
- CLI 显式设成 `keep`:确有输出的被中断 assistant 会被恢复为 `completed` 进入上下文。**空输出不保留**(避免空消息污染上下文)、**带工具调用的不归它管**(仍走「整轮进出」,否则会出现「有调用无结果」的非法历史)。
|
|
115
|
+
- SDK 依赖随之升级到 `1.0.0-alpha.20`(`createAgent` 新增第三参 `CreateAgentOptions`,顺带把此前完全没暴露的 `abortedToolRoundStrategy` / `abortedToolRoundReason` 一起接出来),core 升到 `4.5.3`。
|
|
116
|
+
- **界面国际化(中文 / English)** —— 首次启动按系统语言识别(`LC_ALL` → `LC_MESSAGES` → `LANG` → Node 的 `Intl` locale,**兜底英文**),识别结果写入 `~/.ai-zen/cli/preferences.json`,之后以偏好为准(改了系统语言也不会让界面语言悄悄漂移)。`/lang` 可随时切换,`/lang zh` / `/lang en` 直接指定。
|
|
117
|
+
- 词典编译进产物(`src/i18n/{zh,en}.ts` + 配置中心专区 `{zh,en}-wizard.ts`):key 扁平化,`{n}` 插值;切换语言只换一个引用,不重读磁盘、不重启。缺词条时返回 key 本身(不做跨语言静默回退,否则英文界面里会突然冒出中文)。`t()` 支持 `_one` / `_other` 复数变体(由 `Intl.PluralRules` 选),但 `i18n/index.test.ts` 要求**中英 key 集合完全一致**,故现有词条统一用 `(s)` 写法,不开变体。
|
|
118
|
+
- 语言偏好放 CLI 独立的 `preferences.json`,**不塞进共享的 `config.json`** —— 那是 CLI 与 Desktop 共用的 `AppConfig` 契约,塞进去会被桌面端当未知字段丢掉。
|
|
119
|
+
- 覆盖范围是**整套 TUI 界面**:主对话框(状态栏三块、输入占位与提示、通知、思考档位标签、生成中提示)、命令层(`/help`、斜杠菜单、各命令的选择器与反馈)、配置中心全部屏幕(端点 / 模型 / 图片模型 / MCP / Agent 定义 / 权限 / 默认项 / 工具输出上限 / 新建引导 / KV 与多行文本编辑器)、交接方式选择器与确认框、启动屏与启动错误屏,以及业务模块返回的 summary(`/model` `/agent` `/think` 的切换结果、交接进度与降级提示)。
|
|
120
|
+
- `COMMAND_REGISTRY` 里存的是 **i18n key 而不是文案** —— 它是纯数据模块、加载时机早于语言初始化,存文案会把「加载那一刻的语言」定死;渲染时由 `getCommandHints()` 现取。启动屏的 `TAGLINE` 常量同理改成了 `tagline()` 函数。
|
|
121
|
+
- **管道模式不做 i18n**:`ai <提示词>` / 管道输入的所有输出与报错**一律英文**(它是给脚本用的,不应随人的界面语言变),故 stdio 路径**不初始化语言**,行为与 `preferences.json` 彻底解耦。
|
|
122
|
+
- **`ai --help` 与 `ai hook install / uninstall` 走 i18n**:两者都在 TUI 之外打印,因此在各自分支里先 `initLocale()` 再取词条;`HELP_TEXT` 那个模块级常量相应改成 `helpText()` 现拼(常量会把加载那一刻的语言定死)。
|
|
123
|
+
- **会话 id 改名**:`对话_2025年07月01日14时30分00秒` → `chat_2026_10_28_00_56_34`(英文前缀 + 全下划线时间戳,中英一致、文件名安全)。它同时是文件名,跟着语言变会让历史文件与新文件混排,故时间戳由新增的 `formatFileStamp()` 生成;而**显示用**的 `formatShortTime()` 反而改为随语言切换。管道模式 `--save` 不再自己拼名字,改为调用 `generateConversationId()`,两处重复拼接一并消掉。
|
|
124
|
+
- **注入历史的「工作目录」提示改为英文并显式标记**:`[system-injected: working directory changed — not written by the user]` —— 翻存档或交接文档时一眼能分辨哪些内容不是用户敲的;`stripCwdNote()` 同时认新旧两代格式,避免旧存档把这段笔记渲染到屏幕上。
|
|
125
|
+
- 新建 Sub-agent 时写进 tool schema 的 `task` 参数描述也走词典(中 `完整的任务描述` / 英 `Full task description`)—— 这是唯一一处「协议内容也翻译」的例外,代价是同一份定义在不同界面语言下创建会得到不同的 schema 文本。
|
|
126
|
+
- 唯一不翻的注入内容就是上述工作目录提示;`hook.ts` 写进 rc 文件的三个代码块标记串(`# === ai hook (开始) ===` 等)也保持原样 —— 翻了就再也找不到旧块。
|
|
127
|
+
- 时间格式随语言切换 dayjs locale 与 pattern:中文 `7月1日 14:30` / `昨天 14:30`,英文 `Jul 1, 14:30` / `Yesterday 14:30`。
|
|
128
|
+
- 顺带排掉两个隐藏地雷:状态栏曾用**文案**判断思考强度是否置灰(`effort === "关"`),英文界面下永远不成立,改为档位语义的 `effortDimmed()`;输入区占位提示曾硬编码在 `inputLines()` 里(渲染层却走 `t()`),英文界面下量输入行高度会短一行、连带硬件光标 y 偏移,现在两处同源。
|
|
129
|
+
|
|
130
|
+
### 🔧 行为变更
|
|
131
|
+
|
|
132
|
+
- **消息栏的「思考中…」拆成六态:连接中 / 排队中 / 思考中 / 调用工具 / 等待工具返回 / 输出中** —— 原先那行只有「思考中…」一个词,但一轮对话里模型其实走过几个阶段,用同一个词概括在多数时候是**说错话**:等首 token 时并没有在思考,工具执行时模型根本没在跑。
|
|
133
|
+
- 判据只用**事件与 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。
|
|
134
|
+
- 「连接中」这一段靠新增的 `PhasePlugin`(`chat-session.ts`)提供:SDK 的 `open` 事件是在 `chat.completions.create()` **await 返回之后**才发的,只代表响应头到了,之前的 DNS / TCP / TLS / 服务端受理全静默。
|
|
135
|
+
- **子 Agent 的输出不推进主阶段**:委派期间主 Agent 正在等工具返回,若把子 Agent 的 delta 也算进主阶段,界面会跳成「输出中」,正好盖掉用户最需要知道的那句话。子 Agent 的 chunk 仍计入速率、仍触发重绘。
|
|
136
|
+
- spinner 那一行加了 `truncate`:busy 时输入区高度被 `computeChatLayout` 固定为 1 行(`inputHeight = 1`),标签一旦折行就会把输入框顶出屏幕。
|
|
137
|
+
- **状态栏去掉「⏳ 生成中」** —— 本轮到底在干什么已由消息栏 spinner 逐阶段说明,状态栏再挂一个笼统的「生成中」只是重复占宽度(窄屏下还把快捷键提示挤到下一行)。
|
|
138
|
+
|
|
139
|
+
- **shell 钩子改为进入 TUI(此前的管道模式是实现错误)** —— 钩子装在 `command_not_found_handle` / `command_not_found_handler` 上,触发场景是「人坐在交互终端前把命令敲错了」,但函数体写的是 `ai "$@"`,而 `decideMode()` 的规则是「有位置参数 → stdio」,于是钩子会话必然落到管道模式:吐一段文字到 stdout 就退出,**没有历史、不能追问**。`mode.ts` 头顶那张分类表把「脚本调用」与「人手动触发」混成了一个判据,所以这不是补丁能修的,是分类本身错了。
|
|
140
|
+
- 函数体现在是 `ai --tui --new "$@"`:新增 `--tui` 标志显式覆盖「有参数 → stdio」的判定,进 TUI 并把原命令**作为首条消息自动发送**(不预填到输入框——钩子的语义就是「这条命令跑不了,你帮我处理」,再让人敲一次回车没有意义)。
|
|
141
|
+
- 首条消息**强制当普通用户消息**发送,不经斜杠命令解析:敲错的可能是 `/opt/bin/foo` 这种以 `/` 开头的命令,否则会被当成 CLI 命令而报「未知命令」。`runSubmit()` 为此多了 `asMessage` 选项。
|
|
142
|
+
- `--tui` 在非 TTY 环境(脚本 / 管道)报错退出(退出码 2、英文提示),**不**降级成管道模式 —— 否则整屏界面内容会被写进管道。
|
|
143
|
+
- 首条消息只在首次挂载时发送:切模型 / `/lang` 等引起的重挂载不会重发。
|
|
144
|
+
- **钩子会话总是新开会话**:函数体多一个 `--new`(`ai --tui --new "$@"`)。否则敲错一次命令,那条命令就被追加进该目录原有的长对话里,上下文与会话名一起被搅乱。`--new` 是通用标志,手动用同样有效:`ai --new` 跳过 `last-session.json` 指针直接新建;它**只作用于 TUI 路径**,出现在 stdio 路径上时静默忽略(一次 stdio 调用本就是一次性会话,不存在续接)。
|
|
145
|
+
- **`ai hook install` 现在会自我升级**:此前只要 rc 文件里存在 `MARKER_START` 就打印「已安装」直接返回,于是函数体改过之后(比如本次加了 `--new`)老用户永远升不上去。现在会把已安装块与当前版本归一化比对(去空行、逐行 trim),不一致就原地覆盖并提示「已更新到最新版本」(新增词条 `hook.updated`)。
|
|
146
|
+
|
|
147
|
+
- **`/migration` 不再是 `/migrate` 的别名** —— 只保留 `/migrate`,`/migration` 现在报「未知命令」。registry、命令执行器、`blocksInput` 判定与四份文档同步。
|
|
148
|
+
- **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` 可改路径。界面上依旧一个字都不显示。
|
|
149
|
+
|
|
150
|
+
### 🐛 修复
|
|
151
|
+
|
|
152
|
+
- **修复 IME(输入法)候选框「有时候定位不准」** —— 硬件光标位置以前靠 `displayWidth()` 自己算。但宽度是个陷阱:Ink 排版时用 `string-width`,我们手写的表在若干字符上与它不一致(`⚡` `✅` `✨` `⭐` 我们算 1 列、Ink 算 2 列;ZWJ 组合 emoji `👨👩👧` 我们算 8 列、Ink 算 2 列;keycap `1️⃣` 我们算 3、Ink 算 2),输入里一出现这类字符,x 就会偏。现在改为**渲染后实测**:
|
|
153
|
+
- 光标所在字符包进独立 `<Box ref>`(行内嵌套的 `<Text>` 片段在 yoga 里没有节点,`measureElement()` 对它返回 `null`,实测),用 Ink 7 的 `measureElement()` 读它拓印到帧内的绝对坐标 —— 宽度由渲染器自己算,不再经过我们的模型。
|
|
154
|
+
- 拆行后原本整行 `truncate` 的兜底会丢:一旦内容超宽(如宽度模型把 `⚡` 算成 1 列)这行会被顶成两行,整帧高一行、底部被顶出屏幕。解法是把溢出交给**可截断的邻段**吸收——`before` / `after` 段 `flexShrink={1} minWidth={0}` + `wrap="truncate"`,光标块 `flexShrink={0}`;实测容器 40 列、`before` 60 列时仍是**一行**。
|
|
155
|
+
- 测量只能在渲染后(`useEffect`)做,而 Ink 的 `setCursorPosition` 只在 commit 阶段(`useInsertionEffect`)推送,所以存在一次 commit 的延迟:首次渲染先用旧手算值顶一帧,下一帧被实测值纠正(多一趟渲染,Ink 自带节流)。
|
|
156
|
+
- 占位提示行(未输入任何内容)不走实测,仍用前缀后的固定位置。
|
|
157
|
+
- **占位提示态不再自绘「假光标」** —— 输入框为空时会显示灰字提示「输入消息,/ 查看命令…」,末尾原本还画了一个 `▏`。但硬件光标这时就停在提示语开头(`x = 前缀宽`),终端自己会把它显示出来,再画一个方块等于屏幕上两个光标,而且位置还在提示语末尾,反而误导。现在占位态只留提示文字。
|
|
158
|
+
- **修复「输入 emoji 后反显光标消失 / 真实光标错位」** —— 根因有两个,都是把「字符」当成了 code point:
|
|
159
|
+
- **光标停在字素簇中间**:`✌️` 是 `U+270C U+FE0F` **两个 code point、一个字素簇**(`👨👩👧` 五个、`🇨🇳` 两个区域指示符)。按 code point 切分时,光标能停在 `✌` 与 `U+FE0F` 之间,于是反显的是一串**变体选择符** —— Ink 的输出里反显段直接消失(实测:`at=U+FE0F` 时帧里既无反显转义也没字符)。同时退格会只删掉半个 emoji、留下孤立的 `U+FE0F`。现在全部光标运算(折行、移动、删除、插入、撤销)改用 `Intl.Segmenter` 的**字素簇**为单位(`graphemes()`,Node 内置无需依赖)。
|
|
160
|
+
- **宽度表换成 Ink 自己的尺子**:`displayWidth()` 不再用手写的 `isWide` 区间表,直接调用 **`string-width`**(Ink 排版用的就是它,见 `ink/build/output.js`);单簇宽度带缓存,折行热路径几乎零成本。从此折行、状态栏装行、光标列号与渲染器同源 —— 状态栏那条「预留 1 列余量」也降级为纯安全边际。
|
|
161
|
+
- **光标单位改成字素簇**:`✌️` 是 `U+270C U+FE0F` **两个 code point、一个字素簇**(`👨👩👧` 五个、`🇨🇳` 两个区域指示符)。按 code point 切分时,光标能停在 `✌` 与 `U+FE0F` 之间,于是反显的是一串**变体选择符** —— Ink 的输出里反显段直接消失(实测:`at=U+FE0F` 时帧里既无反显转义也没字符)。同时退格会只删掉半个 emoji、留下孤立的 `U+FE0F`。现在全部光标运算(折行、移动、删除、插入、撤销)改用 `Intl.Segmenter` 的**字素簇**为单位(`graphemes()`,Node 内置无需依赖)。
|
|
162
|
+
- **新增直接依赖** `string-width@^8.3.0`(纯 ESM,本就在 Ink 的依赖树里,体积无变化)。
|
|
163
|
+
- **通知栏行数未计入光标 y** —— 渲染顺序是「对话区 → 通知栏 → 斜杠菜单 → 分隔线 → 输入行」,而 y 公式漏了通知栏一段。于是任何通知出现时硬件光标都会整体上浮那么多行;`/help` 一次塞最多 8 行通知,候选框能浮进对话区。现已补上 `noticeCount`(现作为实测未就绪时的兜底算法保留)。
|
|
164
|
+
- **修复 SDK 日志「接管了却照样糊界面」** —— 加载 MCP 时 `[load_mcp]` 连同整份 tools / resources 清单直接砸在渲染区外面。原因是 SDK 有 4 个模块在**模块顶层**执行 `const log = getLogger()`:模块求值那一刻就把 `console.log` 的函数引用冻进了常量,`setLogger()` 事后只替换内部变量,改不动已经抓走的引用;ESM 的静态 import 又保证这些模块在 `runTui()` 之前就已求值 —— 于是 CLI 无论怎么接管都会漏。
|
|
165
|
+
- 修在 SDK 侧(不改 CLI 侧骗 console 的 workaround):4 处全部改为**运行时取 logger**(`getLogger().info(...)` 等);`skillTools` 那处是从未被调用的死代码,连同 import 一并删除。至此 SDK 内 10 处日志调用全部经由 `getLogger()` 单一出口,不存在 console 旁路。
|
|
166
|
+
|
|
167
|
+
### 📝 文档
|
|
168
|
+
|
|
169
|
+
- README(中 / 英)与 `docs/{zh,en}/getting-started.md` 的命令表新增 `/agent`、`/reload`、`/mcp` 三行。
|
|
170
|
+
- README(中 / 英)与 `docs/{zh,en}/index.md` 的状态栏说明从「一条按优先级的文本流」改为「三个信息块 + 按块换行」。
|
|
171
|
+
- `docs/{zh,en}/mcp.md` 的「管理」章节补充 `/mcp` 直达入口与 `/reload` 刷新;`docs/{zh,en}/migration.md` 去掉「别名 `/migration`」。
|
|
172
|
+
- README(中 / 英)新增「界面语言」一节:识别优先级、`preferences.json` 与 `/lang`、覆盖范围,以及刻意不翻的三类内容。
|
|
173
|
+
- `docs/manifest.json` 与 README 中的版本号更新为 `1.0.0-alpha.21`。
|
|
174
|
+
- README(中 / 英)的「环境变量」补 `AIZ_DEBUG_SDK=0` / `AIZ_SDK_LOG_FILE`,「文件系统布局」补 `~/.ai-zen/logs/`,键位表下补一段「中断后已输出的思考与内容会保留」的说明。
|
|
175
|
+
- README(中 / 英)与 `docs/{zh,en}/getting-started.md` 的命令表补 `/lang`;「文件系统布局」与 `docs/{zh,en}/configuration.md` 的目录树补 `~/.ai-zen/cli/preferences.json`(CLI 本地偏好)。
|
|
176
|
+
|
|
177
|
+
### ⚠️ 已知取舍
|
|
178
|
+
|
|
179
|
+
- **emoji 的渲染宽度可能仍与终端对不上,这是终端层的事**:代码侧已经做到与渲染器同源(`displayWidth` = Ink 用的 `string-width`,光标位置用 `measureElement` 实测),但**终端自己对 emoji / East Asian Ambiguous 字符的列宽判定**仍可能与 Unicode 表不同(部分终端把 `✌️` 渲染成 1 列而不是 2 列,且没有查询接口)。那种情况下整行文本的相对位置就已经由终端决定了,光标只能跟着 Ink 的模型走。输入 / 删除 / 光标单位层面的 emoji 问题已在本次修完。
|
|
180
|
+
- **光标是「终端光标 + 字符反显」两套并存,这是故意的**:光标处的字符仍用 `<Text inverse>` 反显成一个方块,位置的真相源是实测出来的硬件光标。两者视觉上会叠成一个略重的方块——接受这个重影,因为任一层失效时另一层还能报位置(某些终端 / 多路复用器里硬件光标不显示,或 Ink 卸载时反显又没了)。以后别把反显当重复渲染删掉。
|
|
181
|
+
- **块不被劈开 ⇒ 行数随宽度变化**:80 列下三块各占一行(3 行),120 列可把身份与用量并成 1 行(共 2 行)。旧的「一条流」能压到 2 行,代价是把 `本轮 / 累计` 拆到两行——信息完整性优先。
|
|
182
|
+
- **生成中的速率是估算值**:SDK 只在每个请求的**最后一个 chunk** 才给 `usage`(`stream_options.include_usage`),拿不到逐 token 用量;把估算与精确值混成一个数字才是误导,因此分成「实时估算」与「本轮均速」两种写法。
|
|
183
|
+
- **`/mcp` 只覆盖配置层**:服务器当前是否连通、暴露了多少工具,在 SDK 里是插件私有状态(`McpScopePlugin` 的 `mcpManager`),没有公开 API,界面不展示连接状态。
|
|
184
|
+
- **`/reload` 有一两秒空档**:重建 Agent 且重连 MCP,服务器多的场景会有可感知延迟;期间消息与上下文不受影响。
|
|
185
|
+
|
|
186
|
+
### ✅ 测试
|
|
187
|
+
|
|
188
|
+
- 全量 **33 文件 / 440 例通过**,`npx tsc --noEmit -p tsconfig.test.json` 干净。
|
|
189
|
+
- 新增 `src/i18n/index.test.ts`(14 例:语言识别优先级 / `C`·`POSIX` 跳过 / 兜底英文 / 插值 / 缺词条回退 key / 中英词条一一对应)与 `src/i18n/preferences.test.ts`(9 例:文件缺失·损坏·非法值的容错、原子写、`initLocale` 只识别一次并落盘);`reasoning-effort.test.ts` 补 5 例「置灰不靠文案判断」。
|
|
190
|
+
- pty 真机验收:`LANG=en_US.UTF-8` 首次启动→英文并落盘 `en`;`/lang` 切中文→状态栏/输入框/通知全部跟随,且**重启后保持中文**(偏好先于系统语言);`/lang en` 切回英文正常。
|
|
191
|
+
- pty 真机验收(阶段 2):英文下斜杠菜单显示 `❯ /help Show this help`、`/help` 输出全英文列表;`/lang zh` 后斜杠菜单与 `/help` 整体切回中文。
|
|
192
|
+
- 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` 下重启仍为中文界面**(偏好优先于系统语言),配置中心与模型列表中文文案与改造前一致。
|
|
193
|
+
- **断言中文的测试一律显式锁语言**:`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 上莫名其妙地红。
|
|
194
|
+
- `mode.test.ts` 新增 6 例 `--tui`:有参数进 TUI 且参数作为首条消息、无参数时同不带 `--tui`、以 `/` 开头的命令不被当选项 / 子命令、`--tui --help` 时 help 优先、非 TTY 拒绝而非降级、`--` 之后的 `--tui` 是普通参数。
|
|
195
|
+
- `mode.test.ts` 新增 8 例 `--new`:单独用时进 TUI 且 `newSession: true`、`--tui --new <命令>` 同时带首条消息与新会话、与 `--tui` 顺序无关、有参数时落到 stdio 并被忽略、非 TTY 时落到 stdio 并被忽略、`--tui --new` 非 TTY 仍拒绝而非降级、`--new --help` 时 help 优先、`--` 之后的 `--new` 是普通参数。
|
|
196
|
+
- 新增 `src/hook.test.ts`(9 例,全部在临时 HOME 下跑,**不触碰真实 `~/.bashrc`**):写入的函数体含 `ai --tui --new "$@"` 且只有一个块、保留 rc 原有内容、重复安装幂等、**已装旧函数体(`ai --tui "$@"`)时原地升级**、历史 aiz 块迁移、zsh 走 `command_not_found_handler` 且写入 `.zshrc`;卸载移除块并保留其余内容、未安装时不动文件、rc 文件不存在时不报错。
|
|
197
|
+
- 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` 语法通过。
|
|
198
|
+
- pty 真机验收(`--new`,隔离 `AI_ZEN_DIR` + 预置 `last-session.json` 指向旧会话):`ai` 续接并显示旧消息 `MARKER-OLD-QUESTION`;`ai --new` 停在**空对话**;`ai --tui --new gti status` 空对话且首条消息自动发出;`--help` 中英两个界面都列出 `--new`。
|
|
199
|
+
- 本次改动后全量 **36 文件 / 487 例通过**,`npx tsc --noEmit` 与 `-p tsconfig.test.json` 均干净。
|
|
200
|
+
- 新增 `src/tui/phase.test.ts`(15 例:六态各自的推进、工具名沿用、无 delta 的收尾 chunk 不改阶段、content 压过 reasoning、`reset` 归零、中英文案六者互不相同);`chat-session.test.ts` 补 3 例(按 SDK 事件时序推进、子 Agent 输出不覆盖主阶段、`send` 结束阶段归零)。
|
|
201
|
+
- pty 真机验收(本地 mock SSE 服务端按阶段分段吐流):依次抓到「连接中…」→「排队中…(等待首 token)」→「思考中…」→「输出中…」→「调用工具 no_such_tool…」→「等待 sub_agent_default 返回…」;状态栏同期只有 `⚡ 4.2 tok/s` 与本轮 / 累计 token,无「生成中」。
|
|
202
|
+
- 新增 `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 不被拆半个」的用例。
|
|
203
|
+
- `src/tui/screens.test.tsx` 增加状态栏三块顺序、块内不被拆、宽屏并排、以及「Ink 实际渲染行数 == `statusBarRows` 量出行数」的断言;`computeInputCursorPosition` 补「通知栏行数参与 y」的用例。
|
|
204
|
+
- `input-row.test.tsx` 里那条 `⚡` 用例同时断言「手算给 8、实测给 9」——本次改动的理由就在这两行里。
|
|
205
|
+
|
|
1
206
|
## [1.0.0-alpha.20] - 2026-10-27
|
|
2
207
|
|
|
3
208
|
### ✨ 新功能
|
package/README.md
CHANGED
|
@@ -66,10 +66,12 @@ When run with no arguments on an interactive terminal, the CLI opens a full-scre
|
|
|
66
66
|
╚═╝ ╚═╝╚═╝ ╚══════╝╚══════╝╚═╝ ╚═══╝
|
|
67
67
|
|
|
68
68
|
AI workbench in your terminal
|
|
69
|
-
v1.0.0-alpha.
|
|
69
|
+
v1.0.0-alpha.21 · sdk x · core y
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
After the splash, you land **directly in the chat screen** (resuming the last session if there is one, otherwise starting a new conversation). The chat screen is **bottom-pinned**: a divider, the input line, and
|
|
72
|
+
After the splash, you land **directly in the chat screen** (resuming the last session if there is one, otherwise starting a new conversation). The chat screen is **bottom-pinned**: a divider, the input line, and a **status bar** (three priority-ordered blocks: model · agent · reasoning effort | this-round / cumulative tokens · generation rate | key hints; a block that does not fit moves to the next line whole, so nothing is truncated and no block gets split) always sit at the bottom of the terminal, while the conversation scrolls above them. While generating, the spinner replaces the input line and its label shows **which phase** the round is in: connecting → queued → thinking → calling a tool → waiting for the tool to return → streaming; the two tool phases include the tool name. The generation rate shows a live value while streaming (2-second sliding window, estimated from output deltas) and switches to this round's average once it finishes (preferring the real `completion_tokens` from the API).
|
|
73
|
+
|
|
74
|
+
While sub-agents are running, a **main / sub-agent bar** appears below the transcript (`主AGENT ◈子AGENT1 ◈子AGENT2`): `←` / `→` switches which agent's conversation the transcript shows, the status diamond tells you where each one stands (blue blinking while running / green when done / red when failed), and the bar disappears once this round's inner loop finishes.
|
|
73
75
|
|
|
74
76
|
The chat screen provides a streaming output area (reasoning / answer / tool calls on separate lines), a multiline input (`Enter` sends; `Ctrl+N` inserts a newline; `← →` move the cursor, `Ctrl+← →` by word; `Ctrl+W` / `Alt+Backspace` delete the previous word, `Alt+D` / `Ctrl+Delete` the next word; `Ctrl+K` kills to end of line; `Ctrl+U` kills to line start; `PgUp`/`PgDn` (or `Alt+V`/`Ctrl+V`) scroll the transcript, the mouse wheel scrolls by line; on macOS `⌘← →` moves by word, `⌘⌫`/`⌘⌦` deletes by word, `⌘[`/`⌘]` pages and `⌘;`/`⌘'` scrolls by line (depending on whether the terminal reports Cmd — see below)., and an inline `/` command menu (↑↓ to choose, `Tab` to complete). Tool calls render as `⚙ name(args)` with **JSON syntax highlighting**; the answer / reasoning is **Markdown-source-colored** (headings, bold, italic, inline code, lists, quotes, links), and fenced code blocks are **syntax-highlighted by language with the fence markers kept** (highlight data from `highlight.js` via `lowlight`). `Ctrl+C` cancels the current request while generating, and exits when idle.
|
|
75
77
|
|
|
@@ -111,6 +113,8 @@ Key map (aligned with [Claude Code](https://docs.claude.com/en/docs/claude-code/
|
|
|
111
113
|
| `Shift+↑ ↓`, `Alt+↑ ↓`, `⌘;` / `⌘'`* | Scroll by line |
|
|
112
114
|
| `\` + `Enter` | Newline (works in any terminal) |
|
|
113
115
|
|
|
116
|
+
> After an interrupt (`Ctrl+C` / `Esc`), **the reasoning and content the model already produced stay in the context** — on the next turn it picks up that half-finished sentence instead of pretending it never said it. Use `/new` if you want a clean slate. Tool rounds are still reconciled all-in or all-out: unfinished tools get a reason backfilled, so no illegal "call without result" history is left behind.
|
|
117
|
+
|
|
114
118
|
Not implemented (no counterpart here): `Ctrl+O` transcript viewer, `Ctrl+R` reverse search, `Ctrl+B` background tasks, `Ctrl+T` task list, `Shift+Tab` permission modes.
|
|
115
119
|
|
|
116
120
|
`Ctrl+V` (Claude Code's "paste image") is **not implemented yet** — pressing it shows a notice. It first needs a proper "file → content block" capability probe (Files API upload vs base64 inline) instead of hard-coding a single endpoint. Note that macOS maps `⌘⌫` to `Ctrl+U`, which is why `Ctrl+U` kills to line start here; use `Esc` to clear the draft.
|
|
@@ -159,6 +163,19 @@ On first launch, if the endpoint bound to the selected model has no API Key yet,
|
|
|
159
163
|
|
|
160
164
|
Every conversation is persisted directly as its own file (`cli/conversations/<id>.json`) while you chat — there is no separate draft store. A single pointer (`cli/last-session.json`) remembers the **last session id**; next time you start `ai`, that session is **resumed automatically** so you pick up where you left off.
|
|
161
165
|
|
|
166
|
+
### UI language (Chinese / English)
|
|
167
|
+
|
|
168
|
+
On first launch the language is detected from the system: `LC_ALL` → `LC_MESSAGES` → `LANG` → the system `Intl` locale, **falling back to English**. The result is stored in `~/.ai-zen/cli/preferences.json`; from then on the stored preference wins, so changing the system language will not make the UI drift. `/lang` toggles at any time (no argument flips between the two), and `/lang zh` / `/lang en` select one directly.
|
|
169
|
+
|
|
170
|
+
The preference lives in the CLI's own `preferences.json`, **not in the shared `config.json`** — the latter is the config contract shared by the CLI and the Desktop app.
|
|
171
|
+
|
|
172
|
+
The whole TUI is covered (chat screen, commands and `/help`, every configuration-center screen, the handoff picker, the splash / boot-error screens), plus the terminal output of `ai --help` and `ai hook install / uninstall`. **Deliberately not translated:**
|
|
173
|
+
|
|
174
|
+
- **pipe mode (`ai "prompt"` / piped input) prints and errors in English only** — it is meant for scripts and should not follow a human user's UI language;
|
|
175
|
+
- the working-directory note injected into the model's history: English plus a `[system-injected: …]` marker saying it was not typed by the user. This is the only piece of protocol content left untranslated (the `task` parameter description of a new Sub-agent does follow the UI language);
|
|
176
|
+
- the three code-block markers `ai hook` writes into `~/.bashrc` (`# === ai hook … ===`) — translating them would lose track of the old block;
|
|
177
|
+
- the conversation-id stamp `chat_YYYY_MM_DD_HH_mm_ss` — it doubles as the file name, so following the UI language would interleave old and new files.
|
|
178
|
+
|
|
162
179
|
### Conversation Commands
|
|
163
180
|
|
|
164
181
|
While in a conversation, all commands start with `/`. Typing `/` lists the available commands, with descriptions, right below the input line; typing more characters narrows the candidates by prefix:
|
|
@@ -172,20 +189,24 @@ While in a conversation, all commands start with `/`. Typing `/` lists the avail
|
|
|
172
189
|
|
|
173
190
|
| Command | Description |
|
|
174
191
|
|---------|-------------|
|
|
175
|
-
| `/exit` `/quit` | Exit the conversation |
|
|
176
|
-
| `/save` | Save the
|
|
177
|
-
| `/load` | Load a
|
|
192
|
+
| `/exit` `/quit` | Exit the conversation (offers to save) |
|
|
193
|
+
| `/save` | Save the conversation (already flushed every turn) |
|
|
194
|
+
| `/load` | Load a conversation (replaces this one) |
|
|
178
195
|
| `/new` | Reset the conversation (clear history) |
|
|
179
|
-
| `/back` |
|
|
180
|
-
| `/editor` |
|
|
196
|
+
| `/back` | Retract a message (roll back to a point and resend) |
|
|
197
|
+
| `/editor` | Compose a long message in your editor (vim/nano) |
|
|
181
198
|
| `/clear` | Clear the screen |
|
|
182
|
-
| `/config` | Open the configuration center (endpoints / models / image models / MCP
|
|
183
|
-
| `/model` | Switch
|
|
184
|
-
| `/
|
|
185
|
-
| `/
|
|
186
|
-
| `/
|
|
187
|
-
| `/
|
|
188
|
-
| `/
|
|
199
|
+
| `/config` | Open the configuration center (endpoints / models / image models / MCP / agent definitions / defaults / tool-output cap) |
|
|
200
|
+
| `/model` | Switch model (writes the Agent's `modelId`; thinking level resets to the model default) |
|
|
201
|
+
| `/agent` | Switch the main Agent (writes `defaultAgent` and rebuilds the session; unusable Agents are marked with the reason) |
|
|
202
|
+
| `/reload` | Refresh the runtime (rediscovers tools / MCP; messages and context are kept) |
|
|
203
|
+
| `/mcp` | Manage MCP servers (add / remove / edit / toggle; `/mcp global` and `/mcp project` pick a scope) |
|
|
204
|
+
| `/lang` | Switch the interface language (`/lang zh`, `/lang en` set it explicitly; no argument toggles) |
|
|
205
|
+
| `/think` `/effort` | Switch reasoning effort (`/think default` follows the model default) |
|
|
206
|
+
| `/key` | Set the endpoint API key (rebuilds the session on save) |
|
|
207
|
+
| `/handoff` | Write a handoff document (keeps the session) |
|
|
208
|
+
| `/migrate` | Hand off and start a new session |
|
|
209
|
+
| `/help` | Show help |
|
|
189
210
|
|
|
190
211
|
### Task Handoff & Migration
|
|
191
212
|
|
|
@@ -217,7 +238,11 @@ ai hook install
|
|
|
217
238
|
ai hook uninstall
|
|
218
239
|
```
|
|
219
240
|
|
|
220
|
-
The hook calls
|
|
241
|
+
The hook calls **`ai --tui --new "$@"`**: it opens the interactive UI and **auto-sends the mistyped command as the first message**. A hook fires when a human at an interactive prompt typed something wrong, so it should open the TUI (you can follow up, and there is history) rather than dump a paragraph to stdout and exit. `--new` additionally makes each hook session a **fresh conversation**, instead of appending the mistyped command to whatever you had been doing in that directory. If you installed an older hook, re-running `ai hook install` upgrades it in place — the installed function body is compared with the current one and overwritten when it differs; you can also `ai hook uninstall` first.
|
|
242
|
+
|
|
243
|
+
`--tui` is usable by hand too: `ai --tui explain this error` opens the TUI and sends it. In a non-TTY environment (a script or a pipe) it fails with an error instead of degrading to pipe mode — otherwise the whole full-screen UI would be written into the pipe.
|
|
244
|
+
|
|
245
|
+
`--new` works on its own as well: `ai --new` starts a fresh conversation instead of resuming the last one in this directory. It only affects the TUI path; in stdio mode it is ignored (a stdio run is always a one-off session).
|
|
221
246
|
|
|
222
247
|
## Configuration
|
|
223
248
|
|
|
@@ -276,6 +301,7 @@ The `tokenThresholds` field on each model sets two usage lines: `handoff` (soft
|
|
|
276
301
|
|
|
277
302
|
- `AI_ZEN_DIR` — Override the shared root directory (default: `~/.ai-zen`). CLI runtime data goes to `$AI_ZEN_DIR/cli/`, and shared resources (agents, skills, tools, MCP, etc.) go to `$AI_ZEN_DIR/`.
|
|
278
303
|
- `AIZ_DEBUG_KEYS=1` — Enables key diagnostics logging (off by default, zero overhead); `AIZ_KEYS_FILE=<path>` overrides the target file (default `/tmp/aiz-key-debug.log`).
|
|
304
|
+
- `AIZ_DEBUG_SDK=0` — Disables SDK global logging. **On by default**: everything is written to `$AI_ZEN_DIR/logs/sdk.log` (no library logs ever reach the screen), rotated to `sdk.log.1` once the file passes 2MB; `AIZ_SDK_LOG_FILE=<path>` overrides the location. Check this file when debugging "MCP won't connect" or "Skill failed to parse".
|
|
279
305
|
|
|
280
306
|
## Filesystem Layout
|
|
281
307
|
|
|
@@ -284,7 +310,8 @@ The `tokenThresholds` field on each model sets two usage lines: `handoff` (soft
|
|
|
284
310
|
├── config.json ← Endpoints & models (shared with other AI-Zen clients)
|
|
285
311
|
├── cli/ ← CLI runtime data
|
|
286
312
|
│ ├── conversations/ ← CLI conversations
|
|
287
|
-
│
|
|
313
|
+
│ ├── last-session.json ← Pointer to the last session id
|
|
314
|
+
│ └── preferences.json ← CLI-local preferences (interface language, etc.)
|
|
288
315
|
├── agents/ ← Agent definitions (shared)
|
|
289
316
|
│ ├── default.json ← Default agent (created on first run)
|
|
290
317
|
│ └── my-custom-agent.json
|
|
@@ -297,7 +324,8 @@ The `tokenThresholds` field on each model sets two usage lines: `handoff` (soft
|
|
|
297
324
|
├── tools/ ← User-defined tools (shared)
|
|
298
325
|
│ └── my-tool.js
|
|
299
326
|
├── mcp.json ← MCP config (shared)
|
|
300
|
-
|
|
327
|
+
├── mcp-oauth/ ← MCP OAuth tokens (shared)
|
|
328
|
+
└── logs/ ← Diagnostics (sdk.log: SDK global log, 2MB rotation)
|
|
301
329
|
|
|
302
330
|
/path/to/project/
|
|
303
331
|
├── .mcp.json ← Project-shared MCP config (committable)
|
package/README.zh.md
CHANGED
|
@@ -66,10 +66,12 @@ ai --version
|
|
|
66
66
|
╚═╝ ╚═╝╚═╝ ╚══════╝╚══════╝╚═╝ ╚═══╝
|
|
67
67
|
|
|
68
68
|
终端里的 AI 协作台
|
|
69
|
-
v1.0.0-alpha.
|
|
69
|
+
v1.0.0-alpha.21 · sdk x · core y
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
启动界面结束后**直接进入对话界面**(若存在「上一轮会话」则自动续接,否则新建对话)。对话界面采用**底部固定**布局:分隔线、输入行与**状态栏**(按优先级分三块:模型 · Agent · 思考强度 | 本轮 / 累计 token 用量 · 生成速率 | 快捷键提示;窄屏时放不下的块整块顺延到下一行,既不截断内容也不劈开单块)始终贴在终端底部,对话内容在其上方滚动;生成中时输入行位置显示**分阶段进度**(连接中 → 排队中 → 思考中 → 调用工具 → 等待工具返回 → 输出中,工具阶段带上工具名)。生成速率在生成中显示实时值(最近 2 秒滑动窗口,按输出增量估算),一轮结束后换成这一轮的平均值(优先用 API 返回的真实 `completion_tokens`)。
|
|
73
|
+
|
|
74
|
+
子 Agent 运行期间,消息列表下方会出现一条**主子 Agent 栏目**(`主AGENT ◈子AGENT1 ◈子AGENT2`):`←` / `→` 切换即换消息列表的数据源,圆点表示执行状态(运行中蓝点闪烁 / 完成绿 / 失败红),本轮内循环结束时栏目自动收起。
|
|
73
75
|
|
|
74
76
|
对话界面包含:流式输出区(思考过程 / 回答正文 / 工具调用分行展示)、多行输入(`Enter` 发送、`Ctrl+N` 换行、`← →` 移动光标、`Ctrl+← →` 跨词、`Ctrl+W` / `Alt+Backspace` 删前一个词、`Alt+D` / `Ctrl+Delete` 删后一个词、`Ctrl+K` 删到行尾、`Ctrl+U` 删到行首、`PgUp`/`PgDn`(或 `Alt+V`/`Ctrl+V`)翻页、鼠标滚轮按行滚动;macOS 上另支持 `⌘← →` 跨词、`⌘⌫`/`⌘⌦` 按词删除、`⌘[`/`⌘]` 翻页、`⌘;`/`⌘'` 按行滚动(取决于终端是否上报 Cmd,见下))与内联 `/` 命令菜单(↑↓ 选择、`Tab` 补全)。工具调用以 `⚙ 名称(参数)` 展示并带 **JSON 语法高亮**,正文与思考按 **Markdown 源码着色**(标题 / 粗体 / 斜体 / 行内代码 / 列表 / 引用 / 链接),**围栏代码块按语言高亮并保留围栏标记**(高亮数据来自 `highlight.js` / `lowlight`)。`Ctrl+C` 在生成中取消本轮请求、空闲时退出。
|
|
75
77
|
|
|
@@ -111,6 +113,8 @@ macOS 上终端 TUI 的通行做法是 **Option 当 Meta + readline 系 Ctrl 键
|
|
|
111
113
|
| `Shift+↑ ↓`、`Alt+↑ ↓`、`⌘;` / `⌘'`* | 按行滚动 |
|
|
112
114
|
| `\` + `Enter` | 换行(任何终端可用) |
|
|
113
115
|
|
|
116
|
+
> 中断(`Ctrl+C` / `Esc`)后,模型**已经输出的思考与内容会保留在上下文里** —— 下一轮它会接着这半句话往下说,而不是当作自己没说过。想要彻底重置,用 `/new` 开新会话。工具调用轮次仍按「整轮进出」归一:没跑完的工具会补上中断原因,不会留下「有调用无结果」的非法历史。
|
|
117
|
+
|
|
114
118
|
未实现(我们的场景没有对应物):`Ctrl+O` 转录查看器、`Ctrl+R` 反向历史搜索、`Ctrl+B` 后台任务、`Ctrl+T` 任务清单、`Shift+Tab` 权限模式。
|
|
115
119
|
|
|
116
120
|
`Ctrl+V`(Claude Code 的「粘贴图片」)**暂未实现**:按下会提示未支持。原因是它需要先解决「文件 → 内容块」的端点能力探测(Files API 上传 vs base64内联),不能简单硬编码某个端点。⌘⌫ 已被终端映射为 `Ctrl+U`,所以这里删到行首;要清空草稿用 `Esc`。
|
|
@@ -159,6 +163,19 @@ key input="" flags=[pageUp] raw=false
|
|
|
159
163
|
|
|
160
164
|
每个会话都会在对话过程中**直接落盘为独立文件**(`cli/conversations/<id>.json`),不再有单独的草稿存储。一个指针文件(`cli/last-session.json`)记录**上一轮会话 id**;下次启动 `ai` 会**自动续接该会话**,接着上次继续。
|
|
161
165
|
|
|
166
|
+
### 界面语言(中文 / English)
|
|
167
|
+
|
|
168
|
+
首次启动按系统语言自动识别:`LC_ALL` → `LC_MESSAGES` → `LANG` → 系统 `Intl` locale,**识别不出来兑底英文**。识别结果写进 `~/.ai-zen/cli/preferences.json`,之后以偏好为准 —— 改了系统语言不会让界面语言漂移。`/lang` 可随时切换(无参在两者间切换),`/lang zh` / `/lang en` 直接指定。
|
|
169
|
+
|
|
170
|
+
偏好存在 CLI 独立的 `preferences.json` 里,**不写共享的 `config.json`** —— 后者是 CLI 与 Desktop 共用的配置契约。
|
|
171
|
+
|
|
172
|
+
覆盖范围:整套 TUI(主对话框、命令与 `/help`、配置中心全部屏幕、交接方式选择器、启动屏与启动错误屏),以及 `ai --help` 与 `ai hook install / uninstall` 的终端输出。**以下刻意不翻**:
|
|
173
|
+
|
|
174
|
+
- **管道模式(`ai "提示词"` / 管道输入)的输出与报错一律英文** —— 它是给脚本用的,不应随人的界面语言变;
|
|
175
|
+
- 注入给模型的工作目录提示:英文 + `[system-injected: …]` 标记(标明不是用户敲的)。这是唯一不翻的协议内容(新建 Sub-agent 的 `task` 参数描述仍随界面语言);
|
|
176
|
+
- `ai hook` 写进 `~/.bashrc` 的三个代码块标记串(`# === ai hook … ===`) —— 翻了就找不到旧块;
|
|
177
|
+
- 会话 id 时间戳 `chat_YYYY_MM_DD_HH_mm_ss` —— 它同时是文件名,跟着语言变会让历史文件与新文件混排。
|
|
178
|
+
|
|
162
179
|
### 对话命令
|
|
163
180
|
|
|
164
181
|
对话中输入的所有命令以 `/` 开头。输入 `/` 会在输入行下方实时列出可用命令及说明,继续输入即按前缀收敛候选:
|
|
@@ -172,20 +189,24 @@ key input="" flags=[pageUp] raw=false
|
|
|
172
189
|
|
|
173
190
|
| 命令 | 说明 |
|
|
174
191
|
|------|------|
|
|
175
|
-
| `/exit` `/quit` |
|
|
176
|
-
| `/save` |
|
|
177
|
-
| `/load` |
|
|
192
|
+
| `/exit` `/quit` | 退出对话(提示保存) |
|
|
193
|
+
| `/save` | 保存对话(本就每轮实时落盘) |
|
|
194
|
+
| `/load` | 加载对话(替换当前会话) |
|
|
178
195
|
| `/new` | 重置会话(清空历史) |
|
|
179
|
-
| `/back` |
|
|
180
|
-
| `/editor` |
|
|
196
|
+
| `/back` | 撤回上一条(回退到指定位置并可编辑重发) |
|
|
197
|
+
| `/editor` | 用外部编辑器(vim/nano)写长消息 |
|
|
181
198
|
| `/clear` | 清屏 |
|
|
182
|
-
| `/config` | 打开配置中心(端点 / 模型 / 图片模型 / MCP
|
|
183
|
-
| `/model` |
|
|
184
|
-
| `/
|
|
185
|
-
| `/
|
|
186
|
-
| `/
|
|
187
|
-
| `/
|
|
188
|
-
| `/
|
|
199
|
+
| `/config` | 打开配置中心(端点 / 模型 / 图片模型 / MCP / Agent 定义 / 默认项 / 工具上限) |
|
|
200
|
+
| `/model` | 切换模型(写入 Agent 的 `modelId`,思考强度重置为模型默认) |
|
|
201
|
+
| `/agent` | 切换主 Agent(写入 `defaultAgent` 并重建会话;跑不起来的标为不可用并给出原因) |
|
|
202
|
+
| `/reload` | 刷新运行环境(重新发现工具 / MCP;消息与上下文保留) |
|
|
203
|
+
| `/mcp` | 管理 MCP 服务器(增删改 / 启停;`/mcp global`、`/mcp project` 指定作用域) |
|
|
204
|
+
| `/lang` | 切换界面语言(`/lang zh`、`/lang en` 直指定,不带参数则切换) |
|
|
205
|
+
| `/think` `/effort` | 切换思考强度(`/think default` 跟随模型默认) |
|
|
206
|
+
| `/key` | 设置端点 API Key(保存后重建会话) |
|
|
207
|
+
| `/handoff` | 生成交接文档(不切换会话) |
|
|
208
|
+
| `/migrate` | 交接并开启新会话 |
|
|
209
|
+
| `/help` | 显示帮助 |
|
|
189
210
|
|
|
190
211
|
### 任务交接与迁移
|
|
191
212
|
|
|
@@ -217,7 +238,11 @@ ai hook install
|
|
|
217
238
|
ai hook uninstall
|
|
218
239
|
```
|
|
219
240
|
|
|
220
|
-
钩子函数体调用的是
|
|
241
|
+
钩子函数体调用的是 **`ai --tui --new "$@"`**:进入交互界面,并把摸错的命令**作为首条消息自动发给模型**。钩子的发起场景是「人坐在交互终端前敲错了命令」,所以它应该进 TUI(可以追问、有历史),而不是落到管道模式吐一段字就退出;`--new` 进一步保证钩子会话**总是新开一条对话**,不会把敲错的命令追加进该目录原有的长对话里。已装过旧版钩子的话,重新执行 `ai hook install` 会原地升级(会比较已安装的函数体与当前版本,不一致就覆盖);也可先 `ai hook uninstall` 再安装。
|
|
242
|
+
|
|
243
|
+
`--tui` 也可以手动用:`ai --tui 帮我看看这个报错` 会直接进 TUI 并发送。它在非 TTY 环境(如写进脚本或管道里)会报错退出而不是降级成管道模式 —— 否则整屏界面内容会被写进管道。
|
|
244
|
+
|
|
245
|
+
`--new` 也可以单独用:`ai --new` 直接开新一条对话,不续接该目录的上一轮会话。它只对 TUI 生效,stdio 模式下会被忽略(一次 stdio 调用本就是一次性会话)。
|
|
221
246
|
|
|
222
247
|
## 配置管理
|
|
223
248
|
|
|
@@ -276,6 +301,7 @@ ai hook uninstall
|
|
|
276
301
|
|
|
277
302
|
- `AI_ZEN_DIR` — 覆盖共享根目录(默认 `~/.ai-zen`)。CLI 运行时数据在 `$AI_ZEN_DIR/cli/`,共享资源(agents、skills、tools、MCP 等)在 `$AI_ZEN_DIR/`。
|
|
278
303
|
- `AIZ_DEBUG_KEYS=1` — 开启按键诊断日志(默认关闭,零开销);`AIZ_KEYS_FILE=<路径>` 可改落盘位置(默认 `/tmp/aiz-key-debug.log`)。
|
|
304
|
+
- `AIZ_DEBUG_SDK=0` — 关闭 SDK 全局日志。**默认开启**,一律写入 `$AI_ZEN_DIR/logs/sdk.log`(界面上不显示任何库日志),单文件 2MB 封顶后轮转为 `sdk.log.1`;`AIZ_SDK_LOG_FILE=<路径>` 可改落盘位置。排查「MCP 连不上」「Skill 解析失败」时看这个文件。
|
|
279
305
|
|
|
280
306
|
## 文件系统布局
|
|
281
307
|
|
|
@@ -284,7 +310,8 @@ ai hook uninstall
|
|
|
284
310
|
├── config.json ← 端点与模型配置(与其他 AI-Zen 客户端共享)
|
|
285
311
|
├── cli/ ← CLI 运行时数据
|
|
286
312
|
│ ├── conversations/ ← CLI 对话记录
|
|
287
|
-
│
|
|
313
|
+
│ ├── last-session.json ← 「上一轮会话 id」指针
|
|
314
|
+
│ └── preferences.json ← CLI 本地偏好(界面语言等)
|
|
288
315
|
├── agents/ ← Agent 定义(共享)
|
|
289
316
|
│ ├── default.json ← 默认 Agent(首次运行自动创建)
|
|
290
317
|
│ └── my-custom-agent.json
|
|
@@ -297,7 +324,8 @@ ai hook uninstall
|
|
|
297
324
|
├── tools/ ← 用户自定义工具(共享)
|
|
298
325
|
│ └── my-tool.js
|
|
299
326
|
├── mcp.json ← MCP 配置(共享)
|
|
300
|
-
|
|
327
|
+
├── mcp-oauth/ ← MCP OAuth 令牌(共享)
|
|
328
|
+
└── logs/ ← 诊断日志(sdk.log:SDK 全局日志,2MB 轮转)
|
|
301
329
|
|
|
302
330
|
/path/to/project/
|
|
303
331
|
├── .mcp.json ← 项目共享 MCP 配置(可提交 git)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-creator.d.ts","sourceRoot":"","sources":["../src/agent-creator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EACL,KAAK,EAGL,QAAQ,EACT,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-creator.d.ts","sourceRoot":"","sources":["../src/agent-creator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EACL,KAAK,EAGL,QAAQ,EACT,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAwBnD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAOxC;AAED,wBAAsB,QAAQ,IAAI,OAAO,CAAC,KAAK,CAAC,CAS/C;AAuCD;;;;;GAKG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAWhD;AAID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAmBhF"}
|
package/dist/agent-creator.js
CHANGED
|
@@ -10,7 +10,14 @@ import { Scope, allInOne, createAgent as sdkCreateAgent, } from "@ai-zen/agents-
|
|
|
10
10
|
import { AGENTS_DIR, SUB_AGENTS_DIR, SKILLS_DIR, TOOLS_DIR, AI_ZEN_DIR, PROJECT_SUB_AGENTS_DIR, PROJECT_SKILLS_DIR, PROJECT_TOOLS_DIR, USER_AGENTS_SKILLS_DIR, USER_AGENTS_MCP_CONFIG_FILE, PROJECT_AGENTS_SKILLS_DIR, PROJECT_AGENTS_MCP_CONFIG_FILE, } from "./config.js";
|
|
11
11
|
import { readConfig } from "./config.js";
|
|
12
12
|
// ==================== Scope 创建(单例)====================
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Scope 单例缓存的是 **Promise** 而非实例。
|
|
15
|
+
*
|
|
16
|
+
* 缓存实例时,`if (_scope) return _scope` 之后到赋值之间有一段 `await`(读配置、初始化),
|
|
17
|
+
* 并发调用会一起穿透 → 建出两个 Scope(MCP 双连接、双份工具发现、双份子进程)。
|
|
18
|
+
* 缓存 Promise 让并发调用共享同一次初始化。
|
|
19
|
+
*/
|
|
20
|
+
let _scopePromise = null;
|
|
14
21
|
/** existsSync 的别名,用于数组 .filter() 场景 */
|
|
15
22
|
const exists = existsSync;
|
|
16
23
|
/**
|
|
@@ -32,8 +39,16 @@ export function listSkillDirs() {
|
|
|
32
39
|
].filter(exists);
|
|
33
40
|
}
|
|
34
41
|
export async function getScope() {
|
|
35
|
-
if (
|
|
36
|
-
|
|
42
|
+
if (!_scopePromise) {
|
|
43
|
+
_scopePromise = createScopeOnce().catch((error) => {
|
|
44
|
+
// 初始化失败不缓存:否则一次瞬时故障会让后续所有发送都拿到同一个已 reject 的 Promise
|
|
45
|
+
_scopePromise = null;
|
|
46
|
+
throw error;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return _scopePromise;
|
|
50
|
+
}
|
|
51
|
+
async function createScopeOnce() {
|
|
37
52
|
const config = await readConfig();
|
|
38
53
|
// MCP 配置合并优先级(从高到低):
|
|
39
54
|
// 1. 项目/.mcp.json
|
|
@@ -50,7 +65,7 @@ export async function getScope() {
|
|
|
50
65
|
].filter(exists);
|
|
51
66
|
// Skills 目录优先级(从高到低):见 listSkillDirs()
|
|
52
67
|
const skillsPaths = listSkillDirs();
|
|
53
|
-
|
|
68
|
+
const scope = new Scope({
|
|
54
69
|
config,
|
|
55
70
|
agentsDir: AGENTS_DIR,
|
|
56
71
|
}).use(...allInOne({
|
|
@@ -59,8 +74,8 @@ export async function getScope() {
|
|
|
59
74
|
toolsPaths: [PROJECT_TOOLS_DIR, TOOLS_DIR].filter(exists),
|
|
60
75
|
mcpPaths,
|
|
61
76
|
}));
|
|
62
|
-
await
|
|
63
|
-
return
|
|
77
|
+
await scope.init();
|
|
78
|
+
return scope;
|
|
64
79
|
}
|
|
65
80
|
/**
|
|
66
81
|
* 重置 Scope 单例:释放当前 Scope(断开 MCP 连接等),下次 `getScope()` 按磁盘配置重建。
|
|
@@ -69,11 +84,13 @@ export async function getScope() {
|
|
|
69
84
|
* 读取一次,不重建则新配置不生效。`_scope` 是同步置空的,便于并发调用方立刻拿到新实例。
|
|
70
85
|
*/
|
|
71
86
|
export async function resetScope() {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
// 同步置空,便于并发调用方立刻拿到新实例;若初始化仍在进行,等它落地后再释放
|
|
88
|
+
const pending = _scopePromise;
|
|
89
|
+
_scopePromise = null;
|
|
90
|
+
if (!pending)
|
|
75
91
|
return;
|
|
76
92
|
try {
|
|
93
|
+
const scope = await pending;
|
|
77
94
|
await scope.dispose();
|
|
78
95
|
}
|
|
79
96
|
catch {
|
|
@@ -84,7 +101,12 @@ export async function createAgent(options) {
|
|
|
84
101
|
const { messages, agentId } = options;
|
|
85
102
|
const scope = await getScope();
|
|
86
103
|
// 始终从磁盘读取 Agent 定义(含 permissions、工具配置等)
|
|
87
|
-
const agent = await sdkCreateAgent(scope, agentId || "default"
|
|
104
|
+
const agent = await sdkCreateAgent(scope, agentId || "default", {
|
|
105
|
+
// 中断时保留模型已经输出的思考与内容:用户按下中断键后,屏幕上那半句话仍然
|
|
106
|
+
// 留在上下文里,模型下一轮能接着往下说,而不是「失忆重来」。
|
|
107
|
+
// (core 默认是 drop;工具轮次不受影响,仍按 abortedToolRoundStrategy 归一)
|
|
108
|
+
abortedMessageStrategy: "keep",
|
|
109
|
+
});
|
|
88
110
|
// 有历史消息时替换(恢复草稿/已保存对话)。
|
|
89
111
|
// 拷贝而非直接引用:恢复的数组来自草稿/对话存档,直接引用会让后续 append 改写传入数组。
|
|
90
112
|
if (messages && messages.length > 0) {
|