@ai-zen/cli 1.0.0-alpha.20 → 1.0.0-alpha.22

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.
Files changed (249) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.md +43 -17
  3. package/README.zh.md +43 -17
  4. package/dist/agent-creator.d.ts.map +1 -1
  5. package/dist/agent-creator.js +32 -10
  6. package/dist/agent-creator.js.map +1 -1
  7. package/dist/agent-switch.d.ts +61 -0
  8. package/dist/agent-switch.d.ts.map +1 -0
  9. package/dist/agent-switch.js +83 -0
  10. package/dist/agent-switch.js.map +1 -0
  11. package/dist/agents-store.d.ts +6 -1
  12. package/dist/agents-store.d.ts.map +1 -1
  13. package/dist/agents-store.js +6 -1
  14. package/dist/agents-store.js.map +1 -1
  15. package/dist/auto-migrate-confirm.d.ts.map +1 -1
  16. package/dist/auto-migrate-confirm.js +2 -1
  17. package/dist/auto-migrate-confirm.js.map +1 -1
  18. package/dist/config-editor.d.ts +9 -1
  19. package/dist/config-editor.d.ts.map +1 -1
  20. package/dist/config-editor.js +30 -19
  21. package/dist/config-editor.js.map +1 -1
  22. package/dist/config.d.ts +1 -1
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +29 -13
  25. package/dist/config.js.map +1 -1
  26. package/dist/conversation-commands/registry.d.ts +123 -4
  27. package/dist/conversation-commands/registry.d.ts.map +1 -1
  28. package/dist/conversation-commands/registry.js +92 -17
  29. package/dist/conversation-commands/registry.js.map +1 -1
  30. package/dist/conversation-persist-plugin.d.ts.map +1 -1
  31. package/dist/conversation-persist-plugin.js +6 -14
  32. package/dist/conversation-persist-plugin.js.map +1 -1
  33. package/dist/conversation-repository.d.ts +68 -14
  34. package/dist/conversation-repository.d.ts.map +1 -1
  35. package/dist/conversation-repository.js +81 -23
  36. package/dist/conversation-repository.js.map +1 -1
  37. package/dist/cwd-tracker-plugin.d.ts.map +1 -1
  38. package/dist/cwd-tracker-plugin.js +6 -2
  39. package/dist/cwd-tracker-plugin.js.map +1 -1
  40. package/dist/format-time.d.ts +17 -6
  41. package/dist/format-time.d.ts.map +1 -1
  42. package/dist/format-time.js +32 -14
  43. package/dist/format-time.js.map +1 -1
  44. package/dist/handoff-service.d.ts.map +1 -1
  45. package/dist/handoff-service.js +22 -19
  46. package/dist/handoff-service.js.map +1 -1
  47. package/dist/hook.d.ts.map +1 -1
  48. package/dist/hook.js +69 -15
  49. package/dist/hook.js.map +1 -1
  50. package/dist/i18n/en-wizard.d.ts +8 -0
  51. package/dist/i18n/en-wizard.d.ts.map +1 -0
  52. package/dist/i18n/en-wizard.js +290 -0
  53. package/dist/i18n/en-wizard.js.map +1 -0
  54. package/dist/i18n/en.d.ts +7 -0
  55. package/dist/i18n/en.d.ts.map +1 -0
  56. package/dist/i18n/en.js +224 -0
  57. package/dist/i18n/en.js.map +1 -0
  58. package/dist/i18n/index.d.ts +57 -0
  59. package/dist/i18n/index.d.ts.map +1 -0
  60. package/dist/i18n/index.js +109 -0
  61. package/dist/i18n/index.js.map +1 -0
  62. package/dist/i18n/preferences.d.ts +34 -0
  63. package/dist/i18n/preferences.d.ts.map +1 -0
  64. package/dist/i18n/preferences.js +69 -0
  65. package/dist/i18n/preferences.js.map +1 -0
  66. package/dist/i18n/zh-wizard.d.ts +11 -0
  67. package/dist/i18n/zh-wizard.d.ts.map +1 -0
  68. package/dist/i18n/zh-wizard.js +293 -0
  69. package/dist/i18n/zh-wizard.js.map +1 -0
  70. package/dist/i18n/zh.d.ts +10 -0
  71. package/dist/i18n/zh.d.ts.map +1 -0
  72. package/dist/i18n/zh.js +228 -0
  73. package/dist/i18n/zh.js.map +1 -0
  74. package/dist/index.js +41 -23
  75. package/dist/index.js.map +1 -1
  76. package/dist/mcp-command.d.ts +32 -0
  77. package/dist/mcp-command.d.ts.map +1 -0
  78. package/dist/mcp-command.js +37 -0
  79. package/dist/mcp-command.js.map +1 -0
  80. package/dist/mode.d.ts +31 -4
  81. package/dist/mode.d.ts.map +1 -1
  82. package/dist/mode.js +42 -7
  83. package/dist/mode.js.map +1 -1
  84. package/dist/model-switch.d.ts.map +1 -1
  85. package/dist/model-switch.js +5 -4
  86. package/dist/model-switch.js.map +1 -1
  87. package/dist/reasoning-effort.d.ts +23 -4
  88. package/dist/reasoning-effort.d.ts.map +1 -1
  89. package/dist/reasoning-effort.js +51 -24
  90. package/dist/reasoning-effort.js.map +1 -1
  91. package/dist/sdk-log.d.ts +23 -16
  92. package/dist/sdk-log.d.ts.map +1 -1
  93. package/dist/sdk-log.js +43 -19
  94. package/dist/sdk-log.js.map +1 -1
  95. package/dist/stdio-runner.d.ts.map +1 -1
  96. package/dist/stdio-runner.js +19 -19
  97. package/dist/stdio-runner.js.map +1 -1
  98. package/dist/tui/chat-session.d.ts +27 -0
  99. package/dist/tui/chat-session.d.ts.map +1 -1
  100. package/dist/tui/chat-session.js +120 -28
  101. package/dist/tui/chat-session.js.map +1 -1
  102. package/dist/tui/components.d.ts +43 -2
  103. package/dist/tui/components.d.ts.map +1 -1
  104. package/dist/tui/components.js +97 -4
  105. package/dist/tui/components.js.map +1 -1
  106. package/dist/tui/config-wizard/detail.d.ts +12 -6
  107. package/dist/tui/config-wizard/detail.d.ts.map +1 -1
  108. package/dist/tui/config-wizard/detail.js +33 -32
  109. package/dist/tui/config-wizard/detail.js.map +1 -1
  110. package/dist/tui/config-wizard/engine.d.ts.map +1 -1
  111. package/dist/tui/config-wizard/engine.js +26 -20
  112. package/dist/tui/config-wizard/engine.js.map +1 -1
  113. package/dist/tui/config-wizard/fields.d.ts.map +1 -1
  114. package/dist/tui/config-wizard/fields.js +176 -164
  115. package/dist/tui/config-wizard/fields.js.map +1 -1
  116. package/dist/tui/config-wizard/kv-editor.d.ts.map +1 -1
  117. package/dist/tui/config-wizard/kv-editor.js +9 -5
  118. package/dist/tui/config-wizard/kv-editor.js.map +1 -1
  119. package/dist/tui/config-wizard/list.d.ts.map +1 -1
  120. package/dist/tui/config-wizard/list.js +2 -1
  121. package/dist/tui/config-wizard/list.js.map +1 -1
  122. package/dist/tui/config-wizard/mcp-helpers.d.ts.map +1 -1
  123. package/dist/tui/config-wizard/mcp-helpers.js +4 -3
  124. package/dist/tui/config-wizard/mcp-helpers.js.map +1 -1
  125. package/dist/tui/config-wizard/menu.d.ts.map +1 -1
  126. package/dist/tui/config-wizard/menu.js +32 -19
  127. package/dist/tui/config-wizard/menu.js.map +1 -1
  128. package/dist/tui/config-wizard/prompt-editor.d.ts.map +1 -1
  129. package/dist/tui/config-wizard/prompt-editor.js +4 -3
  130. package/dist/tui/config-wizard/prompt-editor.js.map +1 -1
  131. package/dist/tui/config-wizard/prompt.d.ts.map +1 -1
  132. package/dist/tui/config-wizard/prompt.js +8 -7
  133. package/dist/tui/config-wizard/prompt.js.map +1 -1
  134. package/dist/tui/config-wizard/step-agents.d.ts.map +1 -1
  135. package/dist/tui/config-wizard/step-agents.js +29 -20
  136. package/dist/tui/config-wizard/step-agents.js.map +1 -1
  137. package/dist/tui/config-wizard/step-defaults.d.ts.map +1 -1
  138. package/dist/tui/config-wizard/step-defaults.js +21 -17
  139. package/dist/tui/config-wizard/step-defaults.js.map +1 -1
  140. package/dist/tui/config-wizard/step-endpoints.d.ts.map +1 -1
  141. package/dist/tui/config-wizard/step-endpoints.js +16 -6
  142. package/dist/tui/config-wizard/step-endpoints.js.map +1 -1
  143. package/dist/tui/config-wizard/step-mcp.d.ts.map +1 -1
  144. package/dist/tui/config-wizard/step-mcp.js +17 -19
  145. package/dist/tui/config-wizard/step-mcp.js.map +1 -1
  146. package/dist/tui/config-wizard/step-models.d.ts.map +1 -1
  147. package/dist/tui/config-wizard/step-models.js +41 -26
  148. package/dist/tui/config-wizard/step-models.js.map +1 -1
  149. package/dist/tui/config-wizard/step-new-endpoint.d.ts.map +1 -1
  150. package/dist/tui/config-wizard/step-new-endpoint.js +16 -12
  151. package/dist/tui/config-wizard/step-new-endpoint.js.map +1 -1
  152. package/dist/tui/config-wizard/step-simple.d.ts.map +1 -1
  153. package/dist/tui/config-wizard/step-simple.js +18 -17
  154. package/dist/tui/config-wizard/step-simple.js.map +1 -1
  155. package/dist/tui/index.d.ts +17 -1
  156. package/dist/tui/index.d.ts.map +1 -1
  157. package/dist/tui/index.js +25 -14
  158. package/dist/tui/index.js.map +1 -1
  159. package/dist/tui/input-editing.d.ts +4 -1
  160. package/dist/tui/input-editing.d.ts.map +1 -1
  161. package/dist/tui/input-editing.js +7 -3
  162. package/dist/tui/input-editing.js.map +1 -1
  163. package/dist/tui/phase.d.ts +64 -0
  164. package/dist/tui/phase.d.ts.map +1 -0
  165. package/dist/tui/phase.js +112 -0
  166. package/dist/tui/phase.js.map +1 -0
  167. package/dist/tui/screens/chat-commands.d.ts +54 -3
  168. package/dist/tui/screens/chat-commands.d.ts.map +1 -1
  169. package/dist/tui/screens/chat-commands.js +469 -330
  170. package/dist/tui/screens/chat-commands.js.map +1 -1
  171. package/dist/tui/screens/chat.d.ts +8 -0
  172. package/dist/tui/screens/chat.d.ts.map +1 -1
  173. package/dist/tui/screens/chat.js +206 -405
  174. package/dist/tui/screens/chat.js.map +1 -1
  175. package/dist/tui/screens/common-keys.d.ts +36 -0
  176. package/dist/tui/screens/common-keys.d.ts.map +1 -0
  177. package/dist/tui/screens/common-keys.js +67 -0
  178. package/dist/tui/screens/common-keys.js.map +1 -0
  179. package/dist/tui/screens/handoff-choice.d.ts.map +1 -1
  180. package/dist/tui/screens/handoff-choice.js +4 -3
  181. package/dist/tui/screens/handoff-choice.js.map +1 -1
  182. package/dist/tui/screens/index.d.ts +1 -1
  183. package/dist/tui/screens/index.d.ts.map +1 -1
  184. package/dist/tui/screens/index.js +1 -1
  185. package/dist/tui/screens/index.js.map +1 -1
  186. package/dist/tui/screens/input-row.d.ts +64 -0
  187. package/dist/tui/screens/input-row.d.ts.map +1 -0
  188. package/dist/tui/screens/input-row.js +67 -0
  189. package/dist/tui/screens/input-row.js.map +1 -0
  190. package/dist/tui/screens/input.d.ts +11 -1
  191. package/dist/tui/screens/input.d.ts.map +1 -1
  192. package/dist/tui/screens/input.js +21 -12
  193. package/dist/tui/screens/input.js.map +1 -1
  194. package/dist/tui/screens/layout.d.ts +8 -0
  195. package/dist/tui/screens/layout.d.ts.map +1 -1
  196. package/dist/tui/screens/layout.js +6 -2
  197. package/dist/tui/screens/layout.js.map +1 -1
  198. package/dist/tui/screens/lines.d.ts.map +1 -1
  199. package/dist/tui/screens/lines.js +6 -6
  200. package/dist/tui/screens/lines.js.map +1 -1
  201. package/dist/tui/screens/messages-view.d.ts.map +1 -1
  202. package/dist/tui/screens/messages-view.js +5 -4
  203. package/dist/tui/screens/messages-view.js.map +1 -1
  204. package/dist/tui/screens/notice-bar.d.ts +5 -0
  205. package/dist/tui/screens/notice-bar.d.ts.map +1 -0
  206. package/dist/tui/screens/notice-bar.js +15 -0
  207. package/dist/tui/screens/notice-bar.js.map +1 -0
  208. package/dist/tui/screens/notice.d.ts +40 -8
  209. package/dist/tui/screens/notice.d.ts.map +1 -1
  210. package/dist/tui/screens/notice.js +75 -15
  211. package/dist/tui/screens/notice.js.map +1 -1
  212. package/dist/tui/screens/overlays.d.ts.map +1 -1
  213. package/dist/tui/screens/overlays.js +3 -2
  214. package/dist/tui/screens/overlays.js.map +1 -1
  215. package/dist/tui/screens/splash.d.ts +1 -1
  216. package/dist/tui/screens/splash.d.ts.map +1 -1
  217. package/dist/tui/screens/splash.js +5 -2
  218. package/dist/tui/screens/splash.js.map +1 -1
  219. package/dist/tui/screens/use-chat-viewport.d.ts +22 -0
  220. package/dist/tui/screens/use-chat-viewport.d.ts.map +1 -0
  221. package/dist/tui/screens/use-chat-viewport.js +50 -0
  222. package/dist/tui/screens/use-chat-viewport.js.map +1 -0
  223. package/dist/tui/screens/use-input-editing.d.ts +44 -0
  224. package/dist/tui/screens/use-input-editing.d.ts.map +1 -0
  225. package/dist/tui/screens/use-input-editing.js +192 -0
  226. package/dist/tui/screens/use-input-editing.js.map +1 -0
  227. package/dist/tui/screens/use-overlays.d.ts +25 -0
  228. package/dist/tui/screens/use-overlays.d.ts.map +1 -0
  229. package/dist/tui/screens/use-overlays.js +32 -0
  230. package/dist/tui/screens/use-overlays.js.map +1 -0
  231. package/dist/tui/screens/use-wizard-host.d.ts +35 -0
  232. package/dist/tui/screens/use-wizard-host.d.ts.map +1 -0
  233. package/dist/tui/screens/use-wizard-host.js +103 -0
  234. package/dist/tui/screens/use-wizard-host.js.map +1 -0
  235. package/dist/tui/status-segments.d.ts +37 -0
  236. package/dist/tui/status-segments.d.ts.map +1 -0
  237. package/dist/tui/status-segments.js +166 -0
  238. package/dist/tui/status-segments.js.map +1 -0
  239. package/dist/tui/text.d.ts +8 -5
  240. package/dist/tui/text.d.ts.map +1 -1
  241. package/dist/tui/text.js +95 -35
  242. package/dist/tui/text.js.map +1 -1
  243. package/dist/tui/token-rate.d.ts +83 -0
  244. package/dist/tui/token-rate.d.ts.map +1 -0
  245. package/dist/tui/token-rate.js +195 -0
  246. package/dist/tui/token-rate.js.map +1 -0
  247. package/dist/types.d.ts +0 -3
  248. package/dist/types.d.ts.map +1 -1
  249. package/package.json +4 -3
@@ -1,17 +1,22 @@
1
1
  import { AgentNS } from "@ai-zen/agents-core";
2
+ import { HandoffAbortedError } from "@ai-zen/agents-sdk";
2
3
  import { spawnSync } from "node:child_process";
3
4
  import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
5
  import { tmpdir } from "node:os";
5
6
  import { join } from "node:path";
6
7
  import { stripCwdNote } from "../chat-session.js";
7
- import { matchCommandHints } from "../../conversation-commands/registry.js";
8
+ import { canonicalCommandName, matchCommandHints, parseCommandArgs, } from "../../conversation-commands/registry.js";
8
9
  import { conversationRepository, listConversations } from "../../conversation-repository.js";
9
10
  import { formatShortTime } from "../../format-time.js";
10
- import { readConfig, readReasoningPresets } from "../../config.js";
11
+ import { readConfig, readMcpConfig, readReasoningPresets, saveConfig } from "../../config.js";
11
12
  import { resetScope } from "../../agent-creator.js";
12
13
  import { switchModel } from "../../model-switch.js";
13
- import { effortChoices, effortLabel, describeEffort, setAgentEffort, } from "../../reasoning-effort.js";
14
+ import { listAgentChoices, switchAgent } from "../../agent-switch.js";
15
+ import { parseMcpScope, pickStartScope } from "../../mcp-command.js";
16
+ import { effortChoices, effortLabel, describeEffort, describeEffortView, setAgentEffort, } from "../../reasoning-effort.js";
14
17
  import { matchLevel, presetFor } from "../../reasoning-presets.js";
18
+ import { getLocale, t } from "../../i18n/index.js";
19
+ import { persistLocale } from "../../i18n/preferences.js";
15
20
  import { readAgentStore } from "../../agents-store.js";
16
21
  import { truncate } from "./render.js";
17
22
  import { chooseHandoffMode } from "./handoff-choice.js";
@@ -23,341 +28,475 @@ export function parseHandoffMode(raw) {
23
28
  const value = raw?.trim().toLowerCase();
24
29
  return value === "self" || value === "summary" ? value : undefined;
25
30
  }
26
- export async function runCommand(name, raw, host) {
27
- const { session, notice, clearTranscript, rows, exit, suspendTerminal, openWizard, openKeyEditor, doExit, setPickerState, setInputText, setEffortText, } = host;
28
- const push = (text, tone = "info") => {
29
- notice(text, tone);
30
- };
31
- // 命令参数:首个 token 为命令名,其余为参数(如 `/migrate summary`)
32
- const args = raw.trim().split(/\s+/).slice(1);
33
- const modeArg = args[0]?.toLowerCase();
34
- const mode = parseHandoffMode(modeArg);
35
- const invalidMode = modeArg !== undefined && mode === undefined;
36
- switch (name) {
37
- case "config":
38
- await openWizard({ kind: "menu" });
39
- break;
40
- case "key":
41
- await openKeyEditor();
42
- break;
43
- case "model": {
44
- const config = await readConfig();
45
- const models = config.models ?? [];
46
- if (models.length === 0) {
47
- push("配置里还没有任何模型,请用 /config 添加端点与模型", "error");
48
- break;
49
- }
50
- // 支持 `/model <id>` 直切,无参时弹选择器
51
- let chosen = args[0];
52
- if (chosen && !models.some((model) => model.id === chosen)) {
53
- push(`未知模型:${chosen}(可选:${models.map((m) => m.id).join(" / ")})`, "error");
54
- break;
55
- }
56
- if (!chosen) {
57
- const items = models.map((model) => ({
58
- label: model.id === session.modelId ? `${model.name}(当前)` : model.name,
59
- value: model.id,
60
- hint: model.id,
61
- }));
62
- const picked = await new Promise((resolve) => setPickerState({ title: "切换模型(会写入配置)", items, resolve }));
63
- if (picked == null) {
64
- push("已取消切换模型");
65
- break;
66
- }
67
- chosen = picked;
68
- }
69
- if (chosen === session.modelId) {
70
- push(`已经是当前模型:${chosen}`);
71
- break;
72
- }
73
- // 先确认目标模型可用:重挂载走的是 ChatSession.create → createModel,
74
- // 端点缺 Key 会直接抛错落到启动错误屏,用户连 /key 都进不去
75
- const targetModel = models.find((model) => model.id === chosen);
76
- const targetEndpoint = (config.endpoints ?? []).find((endpoint) => endpoint.id === targetModel.endpointId);
77
- if (!targetEndpoint) {
78
- push(`模型 ${chosen} 引用的端点 ${targetModel.endpointId} 不存在,请用 /config 检查`, "error");
79
- break;
80
- }
81
- if (!targetEndpoint.apiKey) {
82
- push(`端点「${targetEndpoint.name}」还没有 API Key,请先用 /config 配置后再切换`, "error");
83
- break;
84
- }
85
- try {
86
- // 只写 Agent 定义(模型唯一真相源)
87
- const result = await switchModel({ config, agentId: session.agentId, modelId: chosen });
88
- // Scope 持有配置快照:不重建的话 getScope() 会把旧模型又交给新 Agent
89
- await resetScope();
90
- // 重挂载对话屏:模型变了要重算上下文阈值、重建 Agent;消息与上下文保留
91
- exit({
92
- restart: true,
93
- agentId: session.agentId,
94
- messages: session.messages,
95
- notice: result.summary,
96
- });
97
- }
98
- catch (error) {
99
- push(`切换模型失败:${error?.message ?? error}`, "error");
100
- }
101
- break;
102
- }
103
- case "think":
104
- case "effort": {
105
- const config = await readConfig();
106
- const presets = await readReasoningPresets();
107
- const store = await readAgentStore();
108
- const definition = store.agents.find((item) => item.id === session.agentId) ?? null;
109
- const modelId = definition?.modelId;
110
- if (!modelId) {
111
- push(`Agent「${session.agentId}」未绑定模型,请用 /config 为它选择模型`, "error");
112
- break;
113
- }
114
- const model = (config.models ?? []).find((item) => item.id === modelId);
115
- if (!model) {
116
- push(`模型 ${modelId} 不存在,请用 /model 或 /config 检查`, "error");
117
- break;
118
- }
119
- const choices = effortChoices(model, presets);
120
- if (choices.length === 0) {
121
- push(`模型 ${model.id} 未关联可用的思考策略(缺 reasoningPreset,或策略表里没这一档),无法调整`, "error");
122
- break;
123
- }
124
- // 支持 `/think high` 直设、`/think default` 恢复跟随模型默认,无参时弹选择器
125
- let picked = args[0];
126
- if (picked !== undefined && picked !== "default" && !choices.includes(picked)) {
127
- push(`未知强度:${picked}(可选 ${choices.join(" / ")} / default)`, "error");
128
- break;
129
- }
130
- if (!picked) {
131
- // 「当前」指 Agent 定义上真正写着的那一档(模型默认参数不算,那是跟随状态)
132
- const current = matchLevel(presetFor(model.id, model.reasoningPreset, presets), definition?.modelParams);
133
- const items = [
134
- {
135
- label: `跟随模型默认${current ? "" : "(当前)"}`,
136
- value: "default",
137
- hint: "删除 Agent 上的思考参数",
138
- },
139
- ...choices.map((value) => ({
140
- label: `${effortLabel(value)}${value === current ? "(当前)" : ""}`,
141
- value,
142
- hint: value,
143
- })),
144
- ];
145
- const choice = await new Promise((resolve) => setPickerState({
146
- title: `思考强度(当前:${describeEffort(model, presets, definition)})`,
147
- items,
148
- resolve,
149
- }));
150
- if (choice == null) {
151
- push("已取消切换思考强度");
152
- break;
153
- }
154
- picked = choice;
155
- }
156
- const effort = picked === "default" ? null : picked;
157
- const agentLevel = matchLevel(presetFor(model.id, model.reasoningPreset, presets), definition?.modelParams);
158
- const unchanged = effort === null ? agentLevel === undefined : agentLevel === effort;
159
- if (unchanged) {
160
- push("思考强度未变化");
161
- break;
162
- }
163
- try {
164
- const result = await setAgentEffort({
165
- config,
166
- presets,
167
- agentId: session.agentId,
168
- effort,
169
- // 已读过定义,避免命令层与写入层各读一次盘
170
- readAgent: async () => definition,
171
- });
172
- // 状态栏先跟上;档位要通过重建 Agent 才会进请求参数
173
- setEffortText(describeEffort(result.model, presets, result.definition));
174
- await session.reload();
175
- push(result.summary);
176
- }
177
- catch (error) {
178
- push(`切换思考强度失败:${error?.message ?? error}`, "error");
179
- }
180
- break;
181
- }
182
- case "help": {
183
- const lines = ["可用命令:"];
184
- for (const hint of matchCommandHints("")) {
185
- lines.push(` ${hint.label} ${hint.description}`);
186
- }
187
- push(lines.join("\n"));
188
- break;
189
- }
190
- case "clear":
191
- // 清屏但保留上下文:仅把转录显示起点推到最新
192
- clearTranscript();
193
- break;
194
- case "new": {
195
- // reset 会先归档当前对话(有内容才存),返回存档 id(无内容为 null)
196
- const archivedId = await session.reset();
197
- clearTranscript();
198
- push(archivedId ? `旧对话已存档:${archivedId}\n已开始新对话` : "已开始新对话");
199
- break;
31
+ /**
32
+ * 解析 `/handoff` `/migrate` 的执行者参数。
33
+ *
34
+ * 单独成函数而不是挂在 `CommandContext` 上:16 个命令里只有这两个用得到,
35
+ * 挂在上下文中会让另外 14 个命令(`/save` `/help` `/clear` …)每次也白算一遍。
36
+ */
37
+ export function parseHandoffSelection(args) {
38
+ const raw = args[0]?.toLowerCase();
39
+ const mode = parseHandoffMode(raw);
40
+ return { mode, invalid: raw !== undefined && mode === undefined ? raw : undefined };
41
+ }
42
+ // ==================== 各命令 ====================
43
+ const cmdSave = async ({ session, push }) => {
44
+ try {
45
+ const id = await session.save();
46
+ push(t("cmd.save.done", { id }));
47
+ }
48
+ catch (error) {
49
+ push(t("cmd.save.failed", { error: error?.message ?? error }), "error");
50
+ }
51
+ };
52
+ const cmdLoad = async ({ push, rows, exit, pick }) => {
53
+ // 先算可见条数:摘要只为将要显示的那几条解析(会话多时不拖慢 /load)
54
+ const maxItems = Math.max(1, (rows ?? 24) - 8);
55
+ const list = await listConversations({ previewLimit: maxItems });
56
+ if (list.length === 0) {
57
+ push(t("cmd.load.none"), "error");
58
+ return;
59
+ }
60
+ const allItems = list.map((c) => ({
61
+ // 有首条消息就用它当标签:纯时间戳 id 根本分不清哪条是哪条
62
+ label: c.preview || c.name,
63
+ value: c.id,
64
+ hint: formatShortTime(c.updatedAt),
65
+ }));
66
+ // 覆盖层不能高于终端:仅展示最近的有限条
67
+ const items = allItems.slice(0, maxItems);
68
+ const title = allItems.length > maxItems ? t("cmd.load.tooMany", { n: maxItems }) : t("cmd.load.titleReplacing");
69
+ const choice = await pick(title, items);
70
+ if (choice == null) {
71
+ push(t("cmd.load.cancelled"));
72
+ return;
73
+ }
74
+ const conversation = await conversationRepository.read(choice);
75
+ if (!conversation) {
76
+ push(t("cmd.load.readFailed", { id: choice }), "error");
77
+ return;
78
+ }
79
+ // 整体切换会话(含模型 / Agent)——交由上层重挂载对话屏
80
+ exit({
81
+ load: {
82
+ agentId: conversation.agentId,
83
+ messages: conversation.messages,
84
+ conversationId: conversation.id,
85
+ conversationName: conversation.id,
86
+ },
87
+ });
88
+ };
89
+ const cmdNew = async ({ session, clearTranscript, push }) => {
90
+ // reset 会先归档当前对话(有内容才存),返回存档 id(无内容为 null)
91
+ const archivedId = await session.reset();
92
+ clearTranscript();
93
+ push(archivedId ? t("cmd.new.archived", { id: archivedId }) : t("cmd.new.started"));
94
+ };
95
+ const cmdBack = async ({ session, push, rows, exit, pick }) => {
96
+ const allItems = [];
97
+ session.messages.forEach((message, index) => {
98
+ if (message.role !== AgentNS.Role.User)
99
+ return;
100
+ const content = typeof message.content === "string" ? message.content : "";
101
+ allItems.push({ label: truncate(stripCwdNote(content), 60), value: String(index) });
102
+ });
103
+ if (allItems.length === 0) {
104
+ push(t("cmd.back.empty"), "error");
105
+ return;
106
+ }
107
+ // 最近的在最前;仅展示最近的有限条,避免覆盖层高于终端
108
+ const maxItems = Math.max(1, (rows ?? 24) - 8);
109
+ const items = allItems.reverse().slice(0, maxItems);
110
+ const title = allItems.length > maxItems ? t("cmd.back.tooMany", { n: maxItems }) : t("cmd.back.title");
111
+ const choice = await pick(title, items);
112
+ if (choice == null) {
113
+ push(t("cmd.back.cancelled"));
114
+ return;
115
+ }
116
+ const text = session.rollbackTo(Number(choice));
117
+ exit({ restart: true, messages: session.messages, initialInput: text });
118
+ };
119
+ const cmdEditor = async ({ push, suspendTerminal, setInputText }) => {
120
+ const file = join(tmpdir(), `aizen-input-${Date.now()}.md`);
121
+ try {
122
+ writeFileSync(file, "", "utf-8");
123
+ await suspendTerminal(() => {
124
+ const editor = process.env.EDITOR || process.env.VISUAL || (process.platform === "win32" ? "notepad" : "vi");
125
+ const result = spawnSync(editor, [file], { stdio: "inherit" });
126
+ if (result.error)
127
+ throw result.error;
128
+ });
129
+ const text = readFileSync(file, "utf-8").trim();
130
+ if (text)
131
+ setInputText(text);
132
+ }
133
+ catch (error) {
134
+ push(t("cmd.editor.failed", { error: error?.message ?? error }), "error");
135
+ }
136
+ finally {
137
+ try {
138
+ unlinkSync(file);
200
139
  }
201
- case "save": {
202
- try {
203
- const id = await session.save();
204
- push(`对话已保存:${id}`);
205
- }
206
- catch (error) {
207
- push(`保存失败:${error?.message ?? error}`, "error");
208
- }
209
- break;
140
+ catch {
141
+ /* 忽略 */
210
142
  }
211
- case "load": {
212
- const list = await listConversations();
213
- if (list.length === 0) {
214
- push("还没有已保存的对话", "error");
215
- break;
216
- }
217
- const allItems = list.map((c) => ({
218
- label: c.name,
219
- value: c.id,
220
- hint: formatShortTime(c.updatedAt),
221
- }));
222
- // 覆盖层不能高于终端:仅展示最近的有限条
223
- const maxItems = Math.max(1, (rows ?? 24) - 8);
224
- const items = allItems.slice(0, maxItems);
225
- const title = allItems.length > maxItems
226
- ? `选择要加载的对话(仅显示最近 ${maxItems} 条)`
227
- : "选择要加载的对话(当前会话将被替换)";
228
- const choice = await new Promise((resolve) => setPickerState({ title, items, resolve }));
229
- if (choice == null) {
230
- push("已取消加载");
231
- break;
232
- }
233
- const conversation = await conversationRepository.read(choice);
234
- if (!conversation) {
235
- push(`对话不存在或读取失败:${choice}`, "error");
236
- break;
237
- }
238
- // 整体切换会话(含模型 / Agent)——交由上层重挂载对话屏
239
- exit({
240
- load: {
241
- agentId: conversation.agentId,
242
- messages: conversation.messages,
243
- conversationId: conversation.id,
244
- conversationName: conversation.id,
245
- },
246
- });
247
- break;
143
+ }
144
+ };
145
+ const cmdClear = async ({ clearTranscript }) => {
146
+ // 清屏但保留上下文:仅把转录显示起点推到最新
147
+ clearTranscript();
148
+ };
149
+ const cmdConfig = async ({ openWizard }) => {
150
+ await openWizard({ kind: "menu" });
151
+ };
152
+ const cmdModel = async ({ session, push, args, exit, pick }) => {
153
+ const config = await readConfig();
154
+ const models = config.models ?? [];
155
+ if (models.length === 0) {
156
+ push(t("cmd.model.noModels"), "error");
157
+ return;
158
+ }
159
+ // 支持 `/model <id>` 直切,无参时弹选择器
160
+ let chosen = args[0];
161
+ if (chosen && !models.some((model) => model.id === chosen)) {
162
+ push(t("cmd.model.unknown", { id: chosen, options: models.map((m) => m.id).join(" / ") }), "error");
163
+ return;
164
+ }
165
+ if (!chosen) {
166
+ const items = models.map((model) => ({
167
+ label: model.id === session.modelId ? `${model.name}${t("common.current")}` : model.name,
168
+ value: model.id,
169
+ hint: model.id,
170
+ }));
171
+ const picked = await pick(t("cmd.model.title"), items);
172
+ if (picked == null) {
173
+ push(t("cmd.model.cancelled"));
174
+ return;
248
175
  }
249
- case "handoff": {
250
- if (invalidMode) {
251
- push(`未知的执行者:${modeArg}(可选 self / summary)`, "error");
252
- break;
253
- }
254
- if (!session.messages.some((m) => m.role === AgentNS.Role.User)) {
255
- push("当前对话还没有可交接的内容", "error");
256
- break;
257
- }
258
- // 未显式指定执行者时三选一(与自动迁移统一);显式 `/handoff self|summary` 则直接采用
259
- const chosen = mode ?? (await chooseHandoffMode(setPickerState, "编写交接文档 · 选择交接方式"));
260
- if (chosen == null) {
261
- push("已取消交接");
262
- break;
263
- }
264
- try {
265
- const path = await session.handoff(chosen);
266
- push(`交接文档已生成:${path}`);
267
- }
268
- catch (error) {
269
- push(`编写交接文档失败:${error?.message ?? error}`, "error");
270
- }
271
- break;
176
+ chosen = picked;
177
+ }
178
+ if (chosen === session.modelId) {
179
+ push(t("cmd.model.already", { id: chosen }));
180
+ return;
181
+ }
182
+ // 先确认目标模型可用:重挂载走的是 ChatSession.create → createModel,
183
+ // 端点缺 Key 会直接抛错落到启动错误屏,用户连 /key 都进不去
184
+ const targetModel = models.find((model) => model.id === chosen);
185
+ const targetEndpoint = (config.endpoints ?? []).find((endpoint) => endpoint.id === targetModel.endpointId);
186
+ if (!targetEndpoint) {
187
+ push(t("cmd.model.endpointMissing", { id: chosen, endpoint: targetModel.endpointId }), "error");
188
+ return;
189
+ }
190
+ if (!targetEndpoint.apiKey) {
191
+ push(t("cmd.model.noApiKey", { name: targetEndpoint.name }), "error");
192
+ return;
193
+ }
194
+ try {
195
+ // 只写 Agent 定义(模型唯一真相源)
196
+ const result = await switchModel({ config, agentId: session.agentId, modelId: chosen });
197
+ // Scope 持有配置快照:不重建的话 getScope() 会把旧模型又交给新 Agent
198
+ await resetScope();
199
+ // 重挂载对话屏:模型变了要重算上下文阈值、重建 Agent;消息与上下文保留
200
+ exit({ restart: true, agentId: session.agentId, messages: session.messages, notice: result.summary });
201
+ }
202
+ catch (error) {
203
+ push(t("cmd.model.failed", { error: error?.message ?? error }), "error");
204
+ }
205
+ };
206
+ const cmdAgent = async ({ session, push, args, exit, pick }) => {
207
+ const config = await readConfig();
208
+ const store = await readAgentStore();
209
+ const choices = listAgentChoices(config, store.agents);
210
+ if (choices.length === 0) {
211
+ push(t("cmd.agent.noAgents"), "error");
212
+ return;
213
+ }
214
+ // 支持 `/agent <id>` 直切,无参时弹选择器(与 /model 同一套做法)
215
+ let chosen = args[0];
216
+ if (chosen && !choices.some((choice) => choice.id === chosen)) {
217
+ push(t("cmd.agent.unknown", { id: chosen, options: choices.map((choice) => choice.id).join(" / ") }), "error");
218
+ return;
219
+ }
220
+ if (!chosen) {
221
+ const items = choices.map((choice) => {
222
+ const current = choice.id === session.agentId;
223
+ return {
224
+ label: `${choice.name}${current ? t("common.current") : ""}${choice.selectable ? "" : t("cmd.agent.unavailable")}`,
225
+ value: choice.id,
226
+ // 可用时给模型名(切换等于连模型一起换,得让用户看见换成哪个);
227
+ // 不可用时给原因,否则用户只能干看着选不动
228
+ hint: choice.selectable ? (choice.modelName ?? choice.id) : (choice.reason ?? ""),
229
+ };
230
+ });
231
+ const picked = await pick(t("cmd.agent.title"), items);
232
+ if (picked == null) {
233
+ push(t("cmd.agent.cancelled"));
234
+ return;
272
235
  }
273
- case "migrate":
274
- case "migration": {
275
- if (invalidMode) {
276
- push(`未知的执行者:${modeArg}(可选 self / summary)`, "error");
277
- break;
278
- }
279
- if (!session.messages.some((m) => m.role === AgentNS.Role.User)) {
280
- push("当前对话还没有可迁移的内容", "error");
281
- break;
282
- }
283
- // 与原 yes/no 确认统一为三选一:选「不交接」即取消本次迁移
284
- const chosen = mode ?? (await chooseHandoffMode(setPickerState, "任务迁移 · 选择交接方式(完成后开启新会话)"));
285
- if (chosen == null) {
286
- push("已取消迁移");
287
- break;
288
- }
289
- try {
290
- const path = await session.migrate(chosen);
291
- push(`任务迁移完成,已开启新会话\n📄 交接文档:${path}`);
292
- }
293
- catch (error) {
294
- push(`任务迁移失败:${error?.message ?? error}`, "error");
295
- }
296
- break;
236
+ chosen = picked;
237
+ }
238
+ const target = choices.find((choice) => choice.id === chosen);
239
+ // 切过去跑不起来(未绑模型 / 端点缺 Key)就别切:重挂载后用户会卡在启动错误屏
240
+ if (!target.selectable) {
241
+ push(t("cmd.agent.unavailableReason", { name: target.name, reason: target.reason ?? "" }), "error");
242
+ return;
243
+ }
244
+ try {
245
+ const result = switchAgent({ config, agents: store.agents, targetAgentId: chosen });
246
+ await saveConfig(result.config);
247
+ if (chosen === session.agentId) {
248
+ // 会话已经在这个 Agent 上:只需把「默认 Agent」记号写对,不必重建
249
+ push(t("cmd.agent.alreadyOn", { summary: result.summary ?? "" }));
250
+ return;
297
251
  }
298
- case "back": {
299
- const allItems = [];
300
- session.messages.forEach((message, index) => {
301
- if (message.role !== AgentNS.Role.User)
302
- return;
303
- const content = typeof message.content === "string" ? message.content : "";
304
- allItems.push({ label: truncate(stripCwdNote(content), 60), value: String(index) });
305
- });
306
- if (allItems.length === 0) {
307
- push("还没有可撤回的消息", "error");
308
- break;
309
- }
310
- // 最近的在最前;仅展示最近的有限条,避免覆盖层高于终端
311
- const maxItems = Math.max(1, (rows ?? 24) - 8);
312
- const items = allItems.reverse().slice(0, maxItems);
313
- const title = allItems.length > maxItems
314
- ? `选择要撤回到的消息(仅显示最近 ${maxItems} 条)`
315
- : "选择要撤回到的消息(其后内容将被移除)";
316
- const choice = await new Promise((resolve) => setPickerState({ title, items, resolve }));
317
- if (choice == null) {
318
- push("已取消撤回");
319
- break;
320
- }
321
- const text = session.rollbackTo(Number(choice));
322
- exit({ restart: true, messages: session.messages, initialInput: text });
323
- break;
252
+ // Scope 持有 Agent / 配置快照:不重建的话下一轮请求还是旧 Agent
253
+ await resetScope();
254
+ // 重挂载对话屏:提示词、工具集、模型都跟着 Agent 变;消息与上下文保留
255
+ exit({ restart: true, agentId: chosen, messages: session.messages, notice: result.summary });
256
+ }
257
+ catch (error) {
258
+ push(t("cmd.agent.failed", { error: error?.message ?? error }), "error");
259
+ }
260
+ };
261
+ const cmdReload = async ({ session, push }) => {
262
+ // 手改配置文件(mcp.json / skills / tools / config.json)后不用重启 CLI:
263
+ // `session.reload()` 内含 resetScope()(断开旧 MCP 连接)→ 重建 Agent → 重新发现。
264
+ // 消息与上下文保留,所以「刷新一下就看到新东西」不需要新开对话。
265
+ try {
266
+ await session.reload();
267
+ push(t("cmd.reload.done"));
268
+ }
269
+ catch (error) {
270
+ push(t("cmd.reload.failed", { error: error?.message ?? error }), "error");
271
+ }
272
+ };
273
+ const cmdMcp = async ({ push, args, openWizard }) => {
274
+ // 不另建一套 MCP 编辑界面:直接打开配置中心里已有的 MCP 屏。
275
+ // 增 / 删 / 改 / 启停 / 切换作用域都在那一屏里;关闭后宿主会因 `mcpChanged`
276
+ // 自动 `session.reload()`,新服务器当场生效(与 /config 走同一条路径)。
277
+ const rawScope = args[0];
278
+ const explicit = parseMcpScope(rawScope);
279
+ if (rawScope && !explicit) {
280
+ push(t("cmd.mcp.unknownScope", { scope: rawScope }), "error");
281
+ return;
282
+ }
283
+ const scope = explicit ??
284
+ pickStartScope({
285
+ global: await readMcpConfig("global"),
286
+ project: await readMcpConfig("project"),
287
+ });
288
+ await openWizard({ kind: "mcp", scope });
289
+ };
290
+ const cmdLang = async ({ push, args, refreshLocale }) => {
291
+ // `/lang` 无参在中文 / 英文之间切;`/lang zh`、`/lang en` 直接指定。
292
+ // 状态栏、输入行、通知等界面文案都走 `t()`,但它是模块级函数 —— 语言变了
293
+ // 已渲染的内容不会自己更新,必须让 React 重跑一遍渲染函数(refreshLocale)。
294
+ const wanted = args[0]?.toLowerCase();
295
+ if (wanted && wanted !== "zh" && wanted !== "en") {
296
+ push(t("cmd.lang.unknown", { lang: wanted }), "error");
297
+ return;
298
+ }
299
+ const target = wanted ?? (getLocale() === "zh" ? "en" : "zh");
300
+ await persistLocale(target);
301
+ refreshLocale();
302
+ push(t("notice.languageChanged"));
303
+ };
304
+ const cmdThink = async ({ session, push, args, setEffortView, pick }) => {
305
+ const config = await readConfig();
306
+ const presets = await readReasoningPresets();
307
+ const store = await readAgentStore();
308
+ const definition = store.agents.find((item) => item.id === session.agentId) ?? null;
309
+ const modelId = definition?.modelId;
310
+ if (!modelId) {
311
+ push(t("cmd.think.noModel", { id: session.agentId }), "error");
312
+ return;
313
+ }
314
+ const model = (config.models ?? []).find((item) => item.id === modelId);
315
+ if (!model) {
316
+ push(t("cmd.think.modelMissing", { id: modelId }), "error");
317
+ return;
318
+ }
319
+ const choices = effortChoices(model, presets);
320
+ if (choices.length === 0) {
321
+ push(t("cmd.think.notAdjustable", { id: model.id }), "error");
322
+ return;
323
+ }
324
+ // 支持 `/think high` 直设、`/think default` 恢复跟随模型默认,无参时弹选择器
325
+ let picked = args[0];
326
+ if (picked !== undefined && picked !== "default" && !choices.includes(picked)) {
327
+ push(t("cmd.think.unknown", { level: picked, options: choices.join(" / ") }), "error");
328
+ return;
329
+ }
330
+ if (!picked) {
331
+ // 「当前」指 Agent 定义上真正写着的那一档(模型默认参数不算,那是跟随状态)
332
+ const current = matchLevel(presetFor(model.id, model.reasoningPreset, presets), definition?.modelParams);
333
+ const items = [
334
+ {
335
+ label: `${t("cmd.think.followDefault")}${current ? "" : t("common.current")}`,
336
+ value: "default",
337
+ hint: t("cmd.think.clearHint"),
338
+ },
339
+ ...choices.map((value) => ({
340
+ label: `${effortLabel(value)}${value === current ? t("common.current") : ""}`,
341
+ value,
342
+ hint: value,
343
+ })),
344
+ ];
345
+ const choice = await pick(t("cmd.think.title", { level: describeEffort(model, presets, definition) }), items);
346
+ if (choice == null) {
347
+ push(t("cmd.think.cancelled"));
348
+ return;
324
349
  }
325
- case "editor": {
326
- const file = join(tmpdir(), `aizen-input-${Date.now()}.md`);
327
- try {
328
- writeFileSync(file, "", "utf-8");
329
- await suspendTerminal(() => {
330
- const editor = process.env.EDITOR ||
331
- process.env.VISUAL ||
332
- (process.platform === "win32" ? "notepad" : "vi");
333
- const result = spawnSync(editor, [file], { stdio: "inherit" });
334
- if (result.error)
335
- throw result.error;
336
- });
337
- const text = readFileSync(file, "utf-8").trim();
338
- if (text)
339
- setInputText(text);
340
- }
341
- catch (error) {
342
- push(`编辑器打开失败:${error?.message ?? error}`, "error");
343
- }
344
- finally {
345
- try {
346
- unlinkSync(file);
347
- }
348
- catch {
349
- /* 忽略 */
350
- }
351
- }
352
- break;
350
+ picked = choice;
351
+ }
352
+ const effort = picked === "default" ? null : picked;
353
+ const agentLevel = matchLevel(presetFor(model.id, model.reasoningPreset, presets), definition?.modelParams);
354
+ const unchanged = effort === null ? agentLevel === undefined : agentLevel === effort;
355
+ if (unchanged) {
356
+ push(t("cmd.think.unchanged"));
357
+ return;
358
+ }
359
+ try {
360
+ const result = await setAgentEffort({
361
+ config,
362
+ presets,
363
+ agentId: session.agentId,
364
+ effort,
365
+ // 已读过定义,避免命令层与写入层各读一次盘
366
+ readAgent: async () => definition,
367
+ });
368
+ // 状态栏先跟上;档位要通过重建 Agent 才会进请求参数
369
+ setEffortView(describeEffortView(result.model, presets, result.definition));
370
+ await session.reload();
371
+ push(result.summary);
372
+ }
373
+ catch (error) {
374
+ push(t("cmd.think.failed", { error: error?.message ?? error }), "error");
375
+ }
376
+ };
377
+ const cmdKey = async ({ openKeyEditor }) => {
378
+ await openKeyEditor();
379
+ };
380
+ const cmdHandoff = async ({ session, push, args, setPickerState }) => {
381
+ const handoff = parseHandoffSelection(args);
382
+ if (handoff.invalid !== undefined) {
383
+ push(t("cmd.handoff.unknownMode", { who: handoff.invalid }), "error");
384
+ return;
385
+ }
386
+ if (!session.messages.some((m) => m.role === AgentNS.Role.User)) {
387
+ push(t("cmd.handoff.empty"), "error");
388
+ return;
389
+ }
390
+ // 未显式指定执行者时三选一(与自动迁移统一);显式 `/handoff self|summary` 则直接采用
391
+ const chosen = handoff.mode ?? (await chooseHandoffMode(setPickerState, t("cmd.handoff.title")));
392
+ if (chosen == null) {
393
+ push(t("cmd.handoff.cancelled"));
394
+ return;
395
+ }
396
+ try {
397
+ const path = await session.handoff(chosen);
398
+ push(t("cmd.handoff.done", { path }));
399
+ }
400
+ catch (error) {
401
+ // 用户按 Ctrl+C 取消:不是失败,也不该报错堆栈
402
+ if (error instanceof HandoffAbortedError) {
403
+ push(t("cmd.handoff.cancelled"));
404
+ return;
353
405
  }
354
- case "exit":
355
- case "quit": {
356
- await doExit();
357
- break;
406
+ push(t("cmd.handoff.failed", { error: error?.message ?? error }), "error");
407
+ }
408
+ };
409
+ const cmdMigrate = async ({ session, push, args, setPickerState, clearTranscript }) => {
410
+ const handoff = parseHandoffSelection(args);
411
+ if (handoff.invalid !== undefined) {
412
+ push(t("cmd.handoff.unknownMode", { who: handoff.invalid }), "error");
413
+ return;
414
+ }
415
+ if (!session.messages.some((m) => m.role === AgentNS.Role.User)) {
416
+ push(t("cmd.migrate.empty"), "error");
417
+ return;
418
+ }
419
+ // 与原 yes/no 确认统一为三选一:选「不交接」即取消本次迁移
420
+ const chosen = handoff.mode ?? (await chooseHandoffMode(setPickerState, t("cmd.migrate.title")));
421
+ if (chosen == null) {
422
+ push(t("cmd.migrate.cancelled"));
423
+ return;
424
+ }
425
+ try {
426
+ const path = await session.migrate(chosen);
427
+ // 迁移用 `prune` 策略把 messages 剔除到「系统提示 + 交接断点」(仅 1~2 条),
428
+ // 转录显示起点必须跟着归零,否则 `messages.slice(transcriptFrom)` 恒为空——
429
+ // 迁移成功、上下文有内容,但屏幕上什么都没有(同 `/new` 分支的做法)。
430
+ clearTranscript();
431
+ push(t("cmd.migrate.done", { path }));
432
+ }
433
+ catch (error) {
434
+ // 用户按 Ctrl+C 取消:交接已停下且未改动会话,不是失败
435
+ if (error instanceof HandoffAbortedError) {
436
+ push(t("cmd.migrate.cancelled"));
437
+ return;
358
438
  }
359
- default:
360
- push(`未知命令:${raw}(输入 /help 查看可用命令)`, "error");
439
+ push(t("cmd.migrate.failed", { error: error?.message ?? error }), "error");
440
+ }
441
+ };
442
+ const cmdHelp = async ({ push }) => {
443
+ const lines = [t("cmd.help.title")];
444
+ for (const hint of matchCommandHints("")) {
445
+ lines.push(` ${hint.label} ${hint.description}`);
446
+ }
447
+ push(lines.join("\n"));
448
+ };
449
+ const cmdExit = async ({ doExit }) => {
450
+ await doExit();
451
+ };
452
+ /**
453
+ * 命令处理器表 —— 键是**主名**,类型保证与 `COMMAND_REGISTRY` 一一对应。
454
+ * 别名(`/effort`、`/quit`)由 `canonicalCommandName()` 归一后落到同一处理器。
455
+ */
456
+ const HANDLERS = {
457
+ save: cmdSave,
458
+ load: cmdLoad,
459
+ new: cmdNew,
460
+ back: cmdBack,
461
+ editor: cmdEditor,
462
+ clear: cmdClear,
463
+ config: cmdConfig,
464
+ model: cmdModel,
465
+ agent: cmdAgent,
466
+ reload: cmdReload,
467
+ mcp: cmdMcp,
468
+ lang: cmdLang,
469
+ think: cmdThink,
470
+ key: cmdKey,
471
+ handoff: cmdHandoff,
472
+ migrate: cmdMigrate,
473
+ help: cmdHelp,
474
+ exit: cmdExit,
475
+ };
476
+ /** 组装执行上下文:参数解析、通知与选择器的统一入口 */
477
+ function buildContext(name, raw, host) {
478
+ return {
479
+ ...host,
480
+ name,
481
+ raw,
482
+ // 参数切分与命令名解析同源(registry 的 splitCommandInput),命令层不再自己切一遍
483
+ args: parseCommandArgs(raw),
484
+ push: (text, tone = "info") => host.notice(text, tone),
485
+ pick: (title, items) => new Promise((resolve) => host.setPickerState({ title, items, resolve })),
486
+ };
487
+ }
488
+ /**
489
+ * 执行一条斜杠命令。
490
+ *
491
+ * @param name 命令名(含别名,大小写不敏感;由 `parseCommandInput()` 从输入里取出)
492
+ * @param raw 原始输入(含 `/`),用于参数解析与错误回显
493
+ */
494
+ export async function runCommand(name, raw, host) {
495
+ const canonical = canonicalCommandName(name);
496
+ if (!canonical) {
497
+ host.notice(t("cmd.unknown", { cmd: raw }), "error");
498
+ return;
361
499
  }
500
+ await HANDLERS[canonical](buildContext(canonical, raw, host));
362
501
  }
363
502
  //# sourceMappingURL=chat-commands.js.map