@armadra/agent 0.2.1 → 0.4.0

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 (232) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +142 -49
  3. package/dist/agent/retry.d.ts +1 -1
  4. package/dist/agent/retry.js +2 -1
  5. package/dist/agent/session-cache.js +6 -3
  6. package/dist/agent/session-classifier.d.ts +18 -0
  7. package/dist/agent/session-classifier.js +103 -0
  8. package/dist/agent/session-core.d.ts +13 -0
  9. package/dist/agent/session-state.js +2 -1
  10. package/dist/agent/session-sync.js +9 -2
  11. package/dist/agent/session-tools.js +24 -3
  12. package/dist/agent/session.d.ts +3 -0
  13. package/dist/agent/session.js +20 -10
  14. package/dist/agent/tool-runner.js +15 -2
  15. package/dist/agent/types.d.ts +9 -1
  16. package/dist/ai/apis/anthropic-messages.js +3 -2
  17. package/dist/ai/apis/google-generative-ai.js +3 -2
  18. package/dist/ai/apis/openai-completions.js +3 -2
  19. package/dist/ai/apis/openai-responses.js +5 -3
  20. package/dist/ai/cache/fingerprint.d.ts +1 -1
  21. package/dist/ai/cache/fingerprint.js +1 -1
  22. package/dist/ai/cache/reporting.js +2 -1
  23. package/dist/ai/http.d.ts +28 -6
  24. package/dist/ai/http.js +41 -8
  25. package/dist/ai/providers/channels.d.ts +51 -0
  26. package/dist/ai/providers/channels.js +96 -0
  27. package/dist/ai/providers/enrich.d.ts +34 -0
  28. package/dist/ai/providers/enrich.js +86 -0
  29. package/dist/ai/providers/models-dev-cache.d.ts +53 -0
  30. package/dist/ai/providers/models-dev-cache.js +147 -0
  31. package/dist/ai/providers/models-dev.d.ts +99 -0
  32. package/dist/ai/providers/models-dev.js +315 -0
  33. package/dist/ai/providers/registry.d.ts +42 -4
  34. package/dist/ai/providers/registry.js +195 -34
  35. package/dist/ai/providers/suggest.d.ts +18 -0
  36. package/dist/ai/providers/suggest.js +72 -0
  37. package/dist/ai/sse.d.ts +6 -2
  38. package/dist/ai/sse.js +22 -2
  39. package/dist/ai/types.d.ts +30 -3
  40. package/dist/bundle/ama.cjs +17893 -10698
  41. package/dist/cli/args.d.ts +23 -5
  42. package/dist/cli/args.js +113 -13
  43. package/dist/cli/bootstrap.js +28 -3
  44. package/dist/cli/codemode-notice.d.ts +20 -0
  45. package/dist/cli/codemode-notice.js +55 -0
  46. package/dist/cli/compose-providers.js +3 -0
  47. package/dist/cli/compose-session.d.ts +8 -0
  48. package/dist/cli/compose-session.js +32 -1
  49. package/dist/cli/compose-store.d.ts +1 -1
  50. package/dist/cli/compose-store.js +3 -1
  51. package/dist/cli/compose.d.ts +17 -5
  52. package/dist/cli/compose.js +53 -19
  53. package/dist/cli/default-model.d.ts +38 -1
  54. package/dist/cli/default-model.js +96 -9
  55. package/dist/cli/deps.d.ts +38 -0
  56. package/dist/cli/exit-codes.d.ts +2 -0
  57. package/dist/cli/exit-codes.js +3 -0
  58. package/dist/cli/fake-visibility.d.ts +13 -0
  59. package/dist/cli/fake-visibility.js +26 -0
  60. package/dist/cli/from-prompt.d.ts +18 -0
  61. package/dist/cli/from-prompt.js +49 -0
  62. package/dist/cli/main.d.ts +10 -2
  63. package/dist/cli/main.js +88 -2
  64. package/dist/cli/proxy.d.ts +51 -0
  65. package/dist/cli/proxy.js +135 -0
  66. package/dist/cli/startup-screen.d.ts +29 -0
  67. package/dist/cli/startup-screen.js +47 -0
  68. package/dist/cli/startup-steps.d.ts +1 -1
  69. package/dist/cli/startup-steps.js +20 -13
  70. package/dist/cli/subcommands/config.d.ts +29 -4
  71. package/dist/cli/subcommands/config.js +188 -25
  72. package/dist/cli/subcommands/context.js +5 -1
  73. package/dist/cli/subcommands/doctor.js +12 -1
  74. package/dist/cli/subcommands/init.d.ts +6 -0
  75. package/dist/cli/subcommands/init.js +23 -0
  76. package/dist/cli/subcommands/model-meta.d.ts +16 -0
  77. package/dist/cli/subcommands/model-meta.js +54 -0
  78. package/dist/cli/subcommands/models-cache-probe.js +1 -1
  79. package/dist/cli/subcommands/models-discover.d.ts +18 -6
  80. package/dist/cli/subcommands/models-discover.js +87 -52
  81. package/dist/cli/subcommands/models.js +26 -18
  82. package/dist/cli/subcommands/probe-runner.d.ts +96 -0
  83. package/dist/cli/subcommands/probe-runner.js +264 -0
  84. package/dist/cli/subcommands/providers-list.d.ts +8 -0
  85. package/dist/cli/subcommands/providers-list.js +97 -0
  86. package/dist/cli/subcommands/providers-plan.d.ts +79 -0
  87. package/dist/cli/subcommands/providers-plan.js +215 -0
  88. package/dist/cli/subcommands/providers-probe.d.ts +34 -0
  89. package/dist/cli/subcommands/providers-probe.js +87 -0
  90. package/dist/cli/subcommands/providers.d.ts +28 -0
  91. package/dist/cli/subcommands/providers.js +436 -0
  92. package/dist/cli/subcommands/sessions-export.d.ts +10 -0
  93. package/dist/cli/subcommands/sessions-export.js +59 -0
  94. package/dist/cli/subcommands/sessions-search.d.ts +13 -0
  95. package/dist/cli/subcommands/sessions-search.js +103 -0
  96. package/dist/cli/subcommands/sessions.d.ts +3 -2
  97. package/dist/cli/subcommands/sessions.js +22 -1
  98. package/dist/cli/subcommands/stats.d.ts +17 -0
  99. package/dist/cli/subcommands/stats.js +198 -0
  100. package/dist/cli/system-prompt-arg.d.ts +11 -0
  101. package/dist/cli/system-prompt-arg.js +34 -0
  102. package/dist/codemode/modes.d.ts +4 -11
  103. package/dist/codemode/modes.js +5 -27
  104. package/dist/codemode/tool.d.ts +18 -14
  105. package/dist/codemode/tool.js +71 -28
  106. package/dist/config/init.d.ts +34 -0
  107. package/dist/config/init.js +99 -0
  108. package/dist/config/json-schema.d.ts +15 -0
  109. package/dist/config/json-schema.js +215 -0
  110. package/dist/config/key-docs.d.ts +22 -0
  111. package/dist/config/key-docs.js +105 -0
  112. package/dist/config/merge.d.ts +9 -8
  113. package/dist/config/merge.js +22 -7
  114. package/dist/config/schema.d.ts +1 -1
  115. package/dist/config/schema.js +97 -15
  116. package/dist/config/types.d.ts +57 -11
  117. package/dist/config/types.js +19 -1
  118. package/dist/modes/commands-core.js +6 -5
  119. package/dist/modes/image-input.d.ts +27 -0
  120. package/dist/modes/image-input.js +78 -0
  121. package/dist/modes/interactive/approval-dialog.d.ts +21 -5
  122. package/dist/modes/interactive/approval-dialog.js +106 -27
  123. package/dist/modes/interactive/commands.d.ts +11 -2
  124. package/dist/modes/interactive/commands.js +58 -19
  125. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  126. package/dist/modes/interactive/interactive-mode.js +70 -82
  127. package/dist/modes/interactive/key-dispatch.d.ts +3 -1
  128. package/dist/modes/interactive/key-dispatch.js +5 -6
  129. package/dist/modes/interactive/line/line-mode.d.ts +1 -0
  130. package/dist/modes/interactive/line/line-mode.js +9 -5
  131. package/dist/modes/interactive/line/line-render.d.ts +4 -0
  132. package/dist/modes/interactive/line/line-render.js +28 -6
  133. package/dist/modes/interactive/message-view.d.ts +48 -9
  134. package/dist/modes/interactive/message-view.js +238 -44
  135. package/dist/modes/interactive/panels.d.ts +18 -0
  136. package/dist/modes/interactive/panels.js +143 -0
  137. package/dist/modes/interactive/pickers.d.ts +23 -2
  138. package/dist/modes/interactive/pickers.js +48 -15
  139. package/dist/modes/interactive/run-indicator.d.ts +51 -0
  140. package/dist/modes/interactive/run-indicator.js +189 -0
  141. package/dist/modes/interactive/startup-header.d.ts +40 -0
  142. package/dist/modes/interactive/startup-header.js +169 -0
  143. package/dist/modes/interactive/startup-ui.d.ts +7 -2
  144. package/dist/modes/interactive/startup-ui.js +41 -11
  145. package/dist/modes/interactive/status-bar.d.ts +18 -15
  146. package/dist/modes/interactive/status-bar.js +98 -56
  147. package/dist/modes/interactive/tool-summary.d.ts +46 -0
  148. package/dist/modes/interactive/tool-summary.js +218 -0
  149. package/dist/modes/interactive/tool-view.d.ts +48 -15
  150. package/dist/modes/interactive/tool-view.js +203 -145
  151. package/dist/modes/print/print-mode.d.ts +33 -4
  152. package/dist/modes/print/print-mode.js +128 -8
  153. package/dist/modes/rpc/commands.js +12 -2
  154. package/dist/permissions/auto-safe.d.ts +60 -0
  155. package/dist/permissions/auto-safe.js +529 -0
  156. package/dist/permissions/classifier.d.ts +64 -0
  157. package/dist/permissions/classifier.js +184 -0
  158. package/dist/permissions/dangerous.d.ts +5 -0
  159. package/dist/permissions/dangerous.js +1 -1
  160. package/dist/permissions/modes.d.ts +30 -0
  161. package/dist/permissions/modes.js +78 -0
  162. package/dist/permissions/pipeline.d.ts +31 -4
  163. package/dist/permissions/pipeline.js +196 -6
  164. package/dist/permissions/protected.d.ts +19 -0
  165. package/dist/permissions/protected.js +74 -0
  166. package/dist/permissions/rules.js +3 -0
  167. package/dist/permissions/types.d.ts +50 -3
  168. package/dist/permissions/types.js +3 -0
  169. package/dist/rpc.d.ts +2 -0
  170. package/dist/sdk.d.ts +9 -3
  171. package/dist/sdk.js +10 -2
  172. package/dist/session/export.d.ts +32 -0
  173. package/dist/session/export.js +187 -0
  174. package/dist/session/projection.js +5 -1
  175. package/dist/session/redact.d.ts +15 -0
  176. package/dist/session/redact.js +55 -0
  177. package/dist/session/reuse.d.ts +33 -0
  178. package/dist/session/reuse.js +86 -0
  179. package/dist/session/scan.d.ts +34 -0
  180. package/dist/session/scan.js +140 -0
  181. package/dist/session/search.d.ts +52 -0
  182. package/dist/session/search.js +211 -0
  183. package/dist/session/stats-aggregate.d.ts +63 -0
  184. package/dist/session/stats-aggregate.js +163 -0
  185. package/dist/session/stats-index.d.ts +26 -0
  186. package/dist/session/stats-index.js +91 -0
  187. package/dist/session/stats-scan.d.ts +54 -0
  188. package/dist/session/stats-scan.js +236 -0
  189. package/dist/session/types.d.ts +2 -0
  190. package/dist/tools/image-file.d.ts +31 -0
  191. package/dist/tools/image-file.js +114 -0
  192. package/dist/tools/presets.d.ts +35 -8
  193. package/dist/tools/presets.js +56 -17
  194. package/dist/tools/read.d.ts +4 -8
  195. package/dist/tools/read.js +15 -64
  196. package/dist/tui/component.d.ts +8 -2
  197. package/dist/tui/component.js +3 -1
  198. package/dist/tui/components/box.d.ts +6 -1
  199. package/dist/tui/components/box.js +16 -6
  200. package/dist/tui/components/card.d.ts +23 -0
  201. package/dist/tui/components/card.js +37 -0
  202. package/dist/tui/components/editor-history.d.ts +6 -0
  203. package/dist/tui/components/editor-history.js +45 -0
  204. package/dist/tui/components/editor-paste.d.ts +1 -1
  205. package/dist/tui/components/editor-paste.js +4 -4
  206. package/dist/tui/components/editor.d.ts +11 -5
  207. package/dist/tui/components/editor.js +52 -58
  208. package/dist/tui/components/key-value.d.ts +3 -0
  209. package/dist/tui/components/key-value.js +16 -6
  210. package/dist/tui/components/loader.d.ts +37 -7
  211. package/dist/tui/components/loader.js +84 -21
  212. package/dist/tui/components/markdown.d.ts +5 -1
  213. package/dist/tui/components/markdown.js +45 -21
  214. package/dist/tui/components/meter.d.ts +3 -3
  215. package/dist/tui/components/meter.js +13 -11
  216. package/dist/tui/components/select-list.d.ts +23 -1
  217. package/dist/tui/components/select-list.js +76 -13
  218. package/dist/tui/glyphs.d.ts +68 -0
  219. package/dist/tui/glyphs.js +114 -0
  220. package/dist/tui/theme.d.ts +30 -6
  221. package/dist/tui/theme.js +103 -17
  222. package/dist/tui.d.ts +4 -2
  223. package/dist/tui.js +3 -1
  224. package/docs/codemode.md +23 -9
  225. package/docs/hooks.md +10 -10
  226. package/docs/permissions.md +148 -0
  227. package/docs/providers.md +241 -3
  228. package/docs/rpc.md +38 -38
  229. package/docs/session-format.md +3 -2
  230. package/docs/sessions.md +134 -0
  231. package/docs/tui.md +140 -61
  232. package/package.json +3 -1
@@ -11,29 +11,33 @@
11
11
  * - 晚绑定:`runtime.approvals.setUiBroker`(审批对话框)与 `runtime.notifier.set`(宿主通知进消息区),
12
12
  * 退出时撤下。
13
13
  * - 键位:Enter 发送(运行中 = steer),其余应用级键位见 `key-dispatch.ts`。
14
- * - 启动画面按 `ui.quietStartup`:normal 标题 + 模型 / 信任 / 资源清单,header 只有标题,silent 不输出。
14
+ * - 启动头按 `ui.quietStartup`(startup-header.ts):normal 框 + 模型 / 目录 / 模式 / 资源清单(窄屏或
15
+ * `ui.compact` 去框),header 一行,silent 不输出。
15
16
  */
17
+ import { promptImages, sessionModel } from "../image-input.js";
16
18
  import { join } from "node:path";
17
19
  import { AgentSessionImpl } from "../../agent/session.js";
18
20
  import { currentSession, switchSession } from "../../cli/compose-session.js";
19
- import { buildStartupScreen } from "../../cli/startup-screen.js";
21
+ import { startupInfo, startupScreenLevel } from "../../cli/startup-screen.js";
20
22
  import { KEYBINDINGS_FILE } from "../../config/paths.js";
21
23
  import { AmaError, isAmaError } from "../../errors.js";
22
24
  import { detectSandboxCapability } from "../../codemode/capability.js";
23
25
  import { effectiveCodemodeMode } from "../../tools/presets.js";
24
- import { Container, Editor, Keybindings, Loader, ProcessTerminal, Spacer, TUI, Text, createTheme, detectCapabilities, loadKeybindingsFile, truncateToWidth, } from "../../tui.js";
26
+ import { Container, Editor, Keybindings, Loader, ProcessTerminal, Spacer, TUI, Text, createTheme, detectCapabilities, resolveAscii, resolveThemeName, loadKeybindingsFile, truncateToWidth, } from "../../tui.js";
25
27
  import { cacheEventNotice, cacheNoticesEnabled } from "../session-report.js";
26
28
  import { onTerminationSignals } from "../shared.js";
27
29
  import { ApprovalDialogBroker, approvalOutcomeText } from "./approval-dialog.js";
28
30
  import { ALL_COMMANDS, runInteractiveCommand } from "./commands.js";
29
31
  import { InteractiveCompletion } from "./completion.js";
30
32
  import { createKeyDispatch } from "./key-dispatch.js";
31
- import { MessageView } from "./message-view.js";
33
+ import { MessageView, exitSummaryLines } from "./message-view.js";
32
34
  import { openPicker } from "./pickers.js";
35
+ import { StartupHeader } from "./startup-header.js";
36
+ import { QueueView, RunIndicator } from "./run-indicator.js";
33
37
  import { StatusBar } from "./status-bar.js";
34
38
  import { ToolTracker } from "./tool-view.js";
35
39
  const HINT_MS = 2500;
36
- const QUEUE_PREVIEW = 3;
40
+ const EDITOR_PLACEHOLDER = "输入消息,/ 命令,@ 文件,Shift+Enter 换行";
37
41
  function processTerminal() {
38
42
  const stdin = process.stdin;
39
43
  if (stdin.isTTY !== true || typeof stdin.setRawMode !== "function" || !process.stdout.isTTY) {
@@ -58,17 +62,17 @@ class HintLine {
58
62
  }
59
63
  invalidate() { }
60
64
  }
61
- function oneLine(text) {
62
- return text.replace(/\s+/g, " ").trim();
63
- }
64
65
  export function runInteractiveMode(runtime, context, options = {}) {
65
66
  const terminal = options.terminal ?? processTerminal();
66
67
  const ui = runtime.config.ui ?? {};
68
+ const env = context.io.env;
67
69
  const theme = options.theme ??
68
- createTheme(ui.theme ?? "dark", {
69
- caps: detectCapabilities(context.io.env, context.io.stdoutIsTTY),
70
+ createTheme(resolveThemeName(ui.theme, env), {
71
+ caps: detectCapabilities(env, context.io.stdoutIsTTY),
72
+ ascii: resolveAscii(ui.ascii, env),
70
73
  });
71
74
  const now = options.now ?? Date.now;
75
+ const startedAt = now();
72
76
  const startupWarnings = [];
73
77
  const keys = options.keybindings ?? loadKeys(runtime, (m) => startupWarnings.push(m));
74
78
  let session = currentSession(runtime);
@@ -77,20 +81,25 @@ export function runInteractiveMode(runtime, context, options = {}) {
77
81
  theme,
78
82
  ...(ui.showThinking !== undefined ? { showThinking: ui.showThinking } : {}),
79
83
  ...(ui.markdown !== undefined ? { markdown: ui.markdown } : {}),
84
+ ...(ui.compact === true ? { compact: true } : {}),
80
85
  });
81
86
  const tools = new ToolTracker({
82
87
  theme,
83
88
  cwd: session.state.cwd,
84
89
  getTool: (name) => session.getTools().find((tool) => tool.name === name),
90
+ now,
91
+ spinner: () => loader.frame,
85
92
  });
86
- const queueText = new Text("");
93
+ const queueView = new QueueView(theme);
87
94
  const loaderSlot = new Container();
88
95
  const loader = new Loader(() => tui.requestRender(), {
89
96
  theme,
90
- message: "工作中" + theme.fg("dim", " · Esc 中断"),
97
+ message: "思考中",
91
98
  now,
99
+ ...(ui.animation === false ? { animation: false } : {}),
92
100
  ...(options.spinnerIntervalMs !== undefined ? { intervalMs: options.spinnerIntervalMs } : {}),
93
101
  });
102
+ loader.onFrame(() => tools.tick());
94
103
  const hint = new HintLine();
95
104
  let sandboxStrict;
96
105
  const status = new StatusBar({
@@ -118,6 +127,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
118
127
  theme,
119
128
  keybindings: keys,
120
129
  maxVisibleLines: 8,
130
+ placeholder: EDITOR_PLACEHOLDER,
121
131
  autocomplete: completion,
122
132
  requestRender: () => tui.requestRender(),
123
133
  ...(historyFile !== undefined ? { historyFile } : {}),
@@ -125,15 +135,13 @@ export function runInteractiveMode(runtime, context, options = {}) {
125
135
  });
126
136
  tui.addChild(view);
127
137
  tui.addChild(new Spacer());
128
- tui.addChild(queueText);
138
+ tui.addChild(queueView);
129
139
  tui.addChild(loaderSlot);
130
140
  tui.addChild(editor);
131
141
  tui.addChild(hint);
132
142
  tui.addChild(status);
133
143
  // ---- 小工具 ---------------------------------------------------------------
134
144
  let finished = false;
135
- let running = false;
136
- let compacting = false;
137
145
  let hintTimer;
138
146
  let resolveExit = () => undefined;
139
147
  const render = () => tui.requestRender();
@@ -155,47 +163,20 @@ export function runInteractiveMode(runtime, context, options = {}) {
155
163
  }
156
164
  render();
157
165
  };
158
- const syncLoader = () => {
159
- const active = running || compacting;
160
- if (active && loaderSlot.children.length === 0) {
161
- loader.setMessage((compacting && !running ? "压缩上下文" : "工作中") + theme.fg("dim", " · Esc 中断"));
162
- loaderSlot.addChild(loader);
163
- loader.start();
164
- }
165
- else if (!active && loaderSlot.children.length > 0) {
166
- loader.stop();
167
- loaderSlot.clear();
168
- }
169
- render();
170
- };
166
+ const indicator = new RunIndicator({ theme, loader, slot: loaderSlot, tools, render });
171
167
  const setQueue = (steering, followUp) => {
172
168
  status.setQueue(steering.length, followUp.length);
173
- const rows = [
174
- ...steering.map((t) => `↳ steer ${oneLine(t)}`),
175
- ...followUp.map((t) => `↳ followUp ${oneLine(t)}`),
176
- ];
177
- const shown = rows.slice(-QUEUE_PREVIEW);
178
- if (rows.length > shown.length)
179
- shown.unshift(`… 另 ${rows.length - shown.length} 条`);
180
- if (rows.length > 0)
181
- shown.push("Alt+↑ 取回 · Esc 全部回填并中断");
182
- queueText.setText(shown.map((l) => theme.fg("dim", l)).join("\n"));
169
+ queueView.setQueue(steering, followUp);
183
170
  render();
184
171
  };
185
172
  // ---- 会话事件 -------------------------------------------------------------
186
173
  const onEvent = (event) => {
187
174
  switch (event.type) {
188
- case "agent_start":
189
- running = true;
190
- syncLoader();
191
- return;
192
175
  case "agent_settled":
193
- running = false;
194
176
  if (event.warning !== undefined)
195
177
  view.addNotice("warn", event.warning);
196
178
  status.refresh();
197
- syncLoader();
198
- return;
179
+ break;
199
180
  case "message_start": {
200
181
  const message = event.message;
201
182
  if (message.role === "user")
@@ -205,26 +186,22 @@ export function runInteractiveMode(runtime, context, options = {}) {
205
186
  else if (message.role === "custom" && message.display) {
206
187
  view.addNotice("info", typeof message.content === "string" ? message.content : "");
207
188
  }
208
- render();
209
- return;
189
+ break;
210
190
  }
211
191
  case "message_update":
212
192
  view.updateAssistant(event.message);
213
- render();
214
- return;
193
+ break;
215
194
  case "message_end":
216
195
  if (event.message.role === "assistant") {
217
196
  view.endAssistant(event.message);
218
197
  status.refresh();
219
198
  }
220
- render();
221
- return;
199
+ break;
222
200
  case "tool_execution_start": {
223
201
  const started = tools.start(event);
224
202
  if (started.topLevel)
225
- view.add(started.view);
226
- render();
227
- return;
203
+ view.addTool(started.view);
204
+ break;
228
205
  }
229
206
  case "tool_execution_update":
230
207
  tools.update(event.toolCallId, event.partial);
@@ -232,17 +209,11 @@ export function runInteractiveMode(runtime, context, options = {}) {
232
209
  return;
233
210
  case "tool_execution_end":
234
211
  tools.end(event.toolCallId, event.result, event.isError);
235
- render();
236
- return;
212
+ break;
237
213
  case "queue_update":
238
214
  setQueue(event.steering, event.followUp);
239
215
  return;
240
- case "compaction_start":
241
- compacting = true;
242
- syncLoader();
243
- return;
244
216
  case "compaction_end":
245
- compacting = false;
246
217
  if (event.result !== undefined)
247
218
  view.addCompaction(event.result);
248
219
  else if (event.error !== undefined)
@@ -250,17 +221,14 @@ export function runInteractiveMode(runtime, context, options = {}) {
250
221
  else if (event.aborted)
251
222
  view.addNotice("info", "压缩已取消");
252
223
  status.refresh();
253
- syncLoader();
254
- return;
224
+ break;
255
225
  case "auto_retry_start":
256
226
  view.addRetry(event.attempt, event.maxAttempts, event.delayMs, event.errorMessage);
257
- render();
258
- return;
227
+ break;
259
228
  case "auto_retry_end":
260
229
  if (!event.success)
261
230
  view.addRetryFailed(event.finalError);
262
- render();
263
- return;
231
+ break;
264
232
  case "cache_miss":
265
233
  case "context_pressure": {
266
234
  const shown = cacheEventNotice(event, cacheNoticesEnabled(session));
@@ -279,15 +247,21 @@ export function runInteractiveMode(runtime, context, options = {}) {
279
247
  render();
280
248
  return;
281
249
  default:
282
- return;
250
+ break;
283
251
  }
252
+ indicator.onEvent(event);
253
+ render();
284
254
  };
285
255
  let unsubscribe = session.subscribe(onEvent);
286
- const startup = buildStartupScreen(runtime);
287
- /** 清空消息区(切换会话、/tree):只留标题行。 */
256
+ const startupLevel = startupScreenLevel(runtime);
257
+ const home = env["HOME"] ?? env["USERPROFILE"];
258
+ const info = startupInfo(runtime, home);
259
+ const header = (level) => new StartupHeader(info, { theme, level, ...(ui.compact === true ? { compact: true } : {}) });
260
+ /** 清空消息区(切换会话、/tree):只留一行头。 */
288
261
  const resetView = () => {
289
262
  view.reset();
290
- view.addHeader(startup.slice(0, 1));
263
+ if (startupLevel !== "silent")
264
+ view.add(header("header"));
291
265
  };
292
266
  const replay = () => {
293
267
  view.replay(session.messages, {
@@ -307,11 +281,9 @@ export function runInteractiveMode(runtime, context, options = {}) {
307
281
  resetView();
308
282
  tools.clear();
309
283
  replay();
310
- running = false;
311
- compacting = false;
312
284
  setQueue([], []);
313
285
  status.refresh();
314
- syncLoader();
286
+ indicator.reset();
315
287
  if (next instanceof AgentSessionImpl)
316
288
  next.announceStart(reason);
317
289
  };
@@ -324,7 +296,10 @@ export function runInteractiveMode(runtime, context, options = {}) {
324
296
  const target = session;
325
297
  void target
326
298
  .waitForIdle()
327
- .then(() => target.prompt(text))
299
+ .then(async () => {
300
+ const images = await promptImages(text, [], target.state.cwd, sessionModel(runtime.providers, target));
301
+ return target.prompt(text, images.length > 0 ? { images } : {});
302
+ })
328
303
  .catch((error) => {
329
304
  if (isAmaError(error) && error.code === "busy") {
330
305
  void target.followUp(text).catch(() => undefined);
@@ -352,6 +327,13 @@ export function runInteractiveMode(runtime, context, options = {}) {
352
327
  columns: () => terminal.columns,
353
328
  }, spec),
354
329
  notice,
330
+ panel: (component) => {
331
+ view.add(component);
332
+ render();
333
+ },
334
+ theme: () => theme,
335
+ env,
336
+ ...(home !== undefined ? { home } : {}),
355
337
  setEditorText: (text) => {
356
338
  editor.setText(text);
357
339
  render();
@@ -384,7 +366,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
384
366
  send(text, via);
385
367
  }
386
368
  function send(text, via) {
387
- if (!running)
369
+ if (!indicator.running)
388
370
  startPrompt(text);
389
371
  else if (via === "followUp")
390
372
  void session.followUp(text).catch(() => undefined);
@@ -399,9 +381,10 @@ export function runInteractiveMode(runtime, context, options = {}) {
399
381
  status,
400
382
  session: () => session,
401
383
  inactive: () => finished || tui.hasOverlay,
402
- busy: () => running || compacting,
384
+ busy: () => indicator.busy,
403
385
  now,
404
386
  showHint,
387
+ onExpandToggle: (expanded) => view.setThinkingExpanded(expanded),
405
388
  submit: (text, via) => submit(text, via),
406
389
  runCommand: (line) => void runCommand(line),
407
390
  exit: (code) => exit(code),
@@ -415,11 +398,13 @@ export function runInteractiveMode(runtime, context, options = {}) {
415
398
  showOverlay: (component) => tui.showOverlay(component, { anchor: "bottom" }),
416
399
  onOpen: () => {
417
400
  editor.disableSubmit = true;
418
- render();
401
+ tools.setAwaiting(true);
402
+ indicator.setApproval(true);
419
403
  },
420
404
  onClose: () => {
421
405
  editor.disableSubmit = false;
422
- render();
406
+ tools.setAwaiting(false);
407
+ indicator.setApproval(false);
423
408
  },
424
409
  report: (request, outcome) => {
425
410
  if (outcome === "deny" || outcome === "cancelled") {
@@ -435,9 +420,11 @@ export function runInteractiveMode(runtime, context, options = {}) {
435
420
  finished = true;
436
421
  if (hintTimer !== undefined)
437
422
  clearTimeout(hintTimer);
438
- hint.setText("");
439
423
  loader.stop();
440
- loaderSlot.clear();
424
+ // 退出摘要留在回滚里;输入框、状态栏等底部区域撤掉,屏幕停在摘要下面
425
+ view.addExitSummary(exitSummaryLines(session.getStats(), now() - startedAt, theme));
426
+ tui.clear();
427
+ tui.addChild(view);
441
428
  offSignals();
442
429
  runtime.approvals.setUiBroker(undefined);
443
430
  runtime.notifier.set(undefined);
@@ -451,7 +438,8 @@ export function runInteractiveMode(runtime, context, options = {}) {
451
438
  resolveExit(code);
452
439
  });
453
440
  }
454
- view.addHeader(startup);
441
+ if (startupLevel !== "silent")
442
+ view.add(header(startupLevel));
455
443
  for (const warning of startupWarnings)
456
444
  view.addNotice("warn", warning);
457
445
  replay();
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 编辑器之前的输入监听:Ctrl+C 清空输入 / 再按退出、Esc 中断(clearQueue 回填编辑器后 abort)、
5
5
  * Ctrl+D 空输入退出、Alt+Enter followUp、Alt+↑ 取回最后一条排队消息、Shift+Tab 循环权限模式、
6
- * Ctrl+O 展开工具输出、Ctrl+L 模型、Ctrl+T 思考级别。键位由 `keybindings.json` 覆盖。
6
+ * Ctrl+O 展开工具输出与思考块、Ctrl+L 模型、Ctrl+T 思考级别。键位由 `keybindings.json` 覆盖。
7
7
  */
8
8
  import type { AgentSession } from "../../agent/types.js";
9
9
  import type { Editor, Keybindings } from "../../tui.js";
@@ -21,6 +21,8 @@ export interface KeyDispatchDeps {
21
21
  busy(): boolean;
22
22
  now(): number;
23
23
  showHint(text: string): void;
24
+ /** Ctrl+O 之后(消息区思考块跟着展开 / 折叠)。 */
25
+ onExpandToggle?(expanded: boolean): void;
24
26
  submit(text: string, via: "followUp"): void;
25
27
  runCommand(line: string): void;
26
28
  exit(code: number): void;
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * 编辑器之前的输入监听:Ctrl+C 清空输入 / 再按退出、Esc 中断(clearQueue 回填编辑器后 abort)、
5
5
  * Ctrl+D 空输入退出、Alt+Enter followUp、Alt+↑ 取回最后一条排队消息、Shift+Tab 循环权限模式、
6
- * Ctrl+O 展开工具输出、Ctrl+L 模型、Ctrl+T 思考级别。键位由 `keybindings.json` 覆盖。
6
+ * Ctrl+O 展开工具输出与思考块、Ctrl+L 模型、Ctrl+T 思考级别。键位由 `keybindings.json` 覆盖。
7
7
  */
8
8
  import { ExitCode } from "../../cli/exit-codes.js";
9
- import { PERMISSION_MODES_STRICT_FIRST } from "../../permissions/types.js";
9
+ import { nextCycleMode, permissionModeLabel } from "../../permissions/modes.js";
10
10
  const DOUBLE_CTRL_C_MS = 1500;
11
11
  /** 返回输入监听器:已处理返回 true,交给编辑器返回 false。 */
12
12
  export function createKeyDispatch(deps) {
@@ -39,12 +39,10 @@ export function createKeyDispatch(deps) {
39
39
  };
40
40
  const cyclePermission = () => {
41
41
  const session = deps.session();
42
- const modes = PERMISSION_MODES_STRICT_FIRST;
43
- const index = modes.indexOf(session.state.permissionMode);
44
- const next = modes[(index + 1) % modes.length];
42
+ const next = nextCycleMode(session.state.permissionMode);
45
43
  session.setPermissionMode(next);
46
44
  status.refresh();
47
- deps.showHint(`权限模式:${next}`);
45
+ deps.showHint(`权限模式:${permissionModeLabel(next)}`);
48
46
  };
49
47
  return (data) => {
50
48
  if (deps.inactive())
@@ -91,6 +89,7 @@ export function createKeyDispatch(deps) {
91
89
  }
92
90
  if (is("app.tools.expand")) {
93
91
  const expanded = tools.toggleExpanded();
92
+ deps.onExpandToggle?.(expanded);
94
93
  deps.showHint(expanded ? "工具输出:展开" : "工具输出:折叠");
95
94
  return true;
96
95
  }
@@ -5,6 +5,7 @@
5
5
  * = steer;Ctrl+C 运行中中断、空闲时连按两次退出(130);空行 Ctrl+D 退出;审批 y / a / N 单键问答。
6
6
  * 问句之前逐行打印执行前预览(`request.preview`,W3-B9a-2)。
7
7
  * - 否则(管道):逐行读 stdin,每行依次执行(等上一条运行结束);没有审批 UI,ask → deny。
8
+ * 模型错误在运行结束时只打印一次(重试中只显示 ↻);有运行最终失败时退出码 1。
8
9
  * - 斜杠命令走 commands-core(与 B7 同一语义),`pick` 退化为列出候选。
9
10
  */
10
11
  import type { ModeContext } from "../../../cli/deps.js";
@@ -5,8 +5,10 @@
5
5
  * = steer;Ctrl+C 运行中中断、空闲时连按两次退出(130);空行 Ctrl+D 退出;审批 y / a / N 单键问答。
6
6
  * 问句之前逐行打印执行前预览(`request.preview`,W3-B9a-2)。
7
7
  * - 否则(管道):逐行读 stdin,每行依次执行(等上一条运行结束);没有审批 UI,ask → deny。
8
+ * 模型错误在运行结束时只打印一次(重试中只显示 ↻);有运行最终失败时退出码 1。
8
9
  * - 斜杠命令走 commands-core(与 B7 同一语义),`pick` 退化为列出候选。
9
10
  */
11
+ import { promptImages, sessionModel } from "../../image-input.js";
10
12
  import { currentSession, switchSession } from "../../../cli/compose-session.js";
11
13
  import { ExitCode } from "../../../cli/exit-codes.js";
12
14
  import { previewDisplayLines } from "../../../permissions/preview.js";
@@ -67,16 +69,18 @@ export async function runLineMode(runtime, context, options = {}) {
67
69
  }
68
70
  catch (error) {
69
71
  printer.endLine();
72
+ printer.failures++;
70
73
  err(`ama: ${errorText(error)}\n`);
71
74
  }
72
75
  return undefined;
73
76
  };
74
77
  const prompt = async (target, text) => {
75
- await target.prompt(text);
78
+ const images = await promptImages(text, [], target.state.cwd, sessionModel(runtime.providers, target));
79
+ await target.prompt(text, images.length > 0 ? { images } : {});
76
80
  printer.endLine();
77
81
  };
78
82
  if (!raw)
79
- return runPiped(stdin, context, handle, () => session, () => unsubscribe());
83
+ return runPiped(stdin, context, handle, () => session, () => unsubscribe(), () => printer.failures > 0);
80
84
  // ---- raw 终端 ----
81
85
  const tty = stdin;
82
86
  editor = new LineEditor({ prompt: "› ", write: (t) => io.stdout(t) });
@@ -180,8 +184,8 @@ export async function runLineMode(runtime, context, options = {}) {
180
184
  ed.render();
181
185
  });
182
186
  }
183
- /** 管道:逐行依次执行;stdin 结束后等最后一条跑完退出 0。 */
184
- function runPiped(stdin, context, handle, session, unsubscribe) {
187
+ /** 管道:逐行依次执行;stdin 结束后等最后一条跑完退出——有失败的运行时 1,否则 0。 */
188
+ function runPiped(stdin, context, handle, session, unsubscribe, failed) {
185
189
  return new Promise((resolve) => {
186
190
  let chain = Promise.resolve(undefined);
187
191
  let finished = false;
@@ -203,7 +207,7 @@ function runPiped(stdin, context, handle, session, unsubscribe) {
203
207
  });
204
208
  };
205
209
  const reader = createLineReader(stdin, enqueue, () => {
206
- void chain.then(() => finish(ExitCode.Ok));
210
+ void chain.then(() => finish(failed() ? ExitCode.RuntimeError : ExitCode.Ok));
207
211
  });
208
212
  const offSignals = onTerminationSignals((code) => {
209
213
  void session()
@@ -23,6 +23,10 @@ export declare class EventPrinter {
23
23
  private readonly err;
24
24
  private readonly options;
25
25
  private midLine;
26
+ /** 本次运行的模型错误:等 agent_settled 再打(重试中的失败尝试只显示 ↻ 那一行)。 */
27
+ private pendingError;
28
+ /** 最终失败的运行次数(管道模式据此返回退出码 1)。 */
29
+ failures: number;
26
30
  constructor(out: (text: string) => void, err: (text: string) => void, options?: EventPrinterOptions);
27
31
  private line;
28
32
  /** 收尾:保证光标在行首。 */
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { THINKING_LEVELS } from "../../../ai/thinking.js";
6
6
  import { listSessions } from "../../../cli/compose-store.js";
7
- import { PERMISSION_MODES_STRICT_FIRST } from "../../../permissions/types.js";
7
+ import { AUTO_LAYER_TEXT, permissionModeLines } from "../../../permissions/modes.js";
8
8
  import { cacheEventNotice, warmSentNotice } from "../../session-report.js";
9
9
  const SUMMARY_KEYS = ["command", "path", "pattern", "file_path", "url", "description", "name"];
10
10
  /** 工具参数的一行摘要(第一个常见字段,折叠换行、截到 80 字符)。 */
@@ -24,11 +24,14 @@ export function argsSummary(args) {
24
24
  export function approvalQuestion(request) {
25
25
  const task = (request.context?.depth ?? 0) > 0 ? "[task] " : "";
26
26
  const summary = argsSummary(request.input);
27
+ const auto = request.autoDecision;
27
28
  const why = request.reason === "dangerous"
28
29
  ? "(危险命令)"
29
30
  : request.hookReason !== undefined
30
31
  ? `(${request.hookReason})`
31
- : "";
32
+ : auto !== undefined
33
+ ? `(Auto ${AUTO_LAYER_TEXT[auto.layer]}:${auto.reason})`
34
+ : "";
32
35
  return `${task}允许 ${request.toolName}${summary !== "" ? ` ${summary}` : ""}${why}?[y 允许 / a 本会话都允许 / N 拒绝] `;
33
36
  }
34
37
  /** `AMA_LOG=info|debug`。 */
@@ -40,6 +43,10 @@ export class EventPrinter {
40
43
  err;
41
44
  options;
42
45
  midLine = false;
46
+ /** 本次运行的模型错误:等 agent_settled 再打(重试中的失败尝试只显示 ↻ 那一行)。 */
47
+ pendingError;
48
+ /** 最终失败的运行次数(管道模式据此返回退出码 1)。 */
49
+ failures = 0;
43
50
  constructor(out, err, options = {}) {
44
51
  this.out = out;
45
52
  this.err = err;
@@ -72,9 +79,13 @@ export class EventPrinter {
72
79
  if (event.message.role === "assistant") {
73
80
  this.endLine();
74
81
  if (event.message.stopReason === "error")
75
- this.line(`ama: 错误:${event.message.errorMessage ?? "模型调用失败"}`, true);
82
+ this.pendingError = event.message.errorMessage ?? "模型调用失败";
76
83
  }
77
84
  return;
85
+ case "agent_end":
86
+ if (event.willRetry)
87
+ this.pendingError = undefined;
88
+ return;
78
89
  case "tool_execution_start": {
79
90
  const summary = argsSummary(event.args);
80
91
  this.line(`● ${event.toolName}${summary !== "" ? ` ${summary}` : ""}`);
@@ -112,11 +123,19 @@ export class EventPrinter {
112
123
  this.line(`ama: ${text}`, true);
113
124
  return;
114
125
  }
115
- case "agent_settled":
126
+ case "agent_settled": {
116
127
  this.endLine();
117
- if (event.warning !== undefined)
128
+ const error = this.pendingError;
129
+ this.pendingError = undefined;
130
+ if (error !== undefined) {
131
+ this.failures++;
132
+ this.line(`ama: 错误:${error}`, true);
133
+ }
134
+ // 失败时 warning 就是同一条错误文本,不再重复打印
135
+ if (event.warning !== undefined && event.warning !== error)
118
136
  this.line(`ama: ${event.warning}`, true);
119
137
  return;
138
+ }
120
139
  default:
121
140
  return;
122
141
  }
@@ -164,7 +183,10 @@ export async function pickHint(what, runtime, session) {
164
183
  ].join("\n");
165
184
  }
166
185
  case "permission":
167
- return `权限模式(/permission <模式>):${PERMISSION_MODES_STRICT_FIRST.join(" | ")};当前 ${session.state.permissionMode}`;
186
+ return [
187
+ "Mode(/permission <模式>):",
188
+ ...permissionModeLines(session.state.permissionMode, runtime.config.permission?.mode ?? "default").map((line) => ` ${line}`),
189
+ ].join("\n");
168
190
  case "thinking":
169
191
  return `思考级别(/thinking <级别>):${THINKING_LEVELS.join(" | ")};当前 ${session.state.thinkingLevel}`;
170
192
  }