@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
@@ -1,101 +1,36 @@
1
1
  /**
2
- * 工具调用显示(设计 §12.5)。[B7]
2
+ * 工具调用显示(设计 §12.5;终端界面视觉设计 v1 §3.5)。[B7]
3
3
  *
4
- * - 一行标题 `● bash git status`:圆点运行中为强调色,成功绿、失败红;摘要取最能代表这次调用的参数
5
- * (bash 命令、文件路径、搜索模式、task 描述……),路径相对会话 cwd。
6
- * - 折叠(缺省):结果前 3 行 + 剩余行数;`edit` 显示 `details.diff`(+/− 着色,前 12 行);
7
- * `bash` 运行中流式显示尾部 8 行;错误红色。`Ctrl+O` 展开全部(上限 400 行)。
8
- * - 嵌套:带 `parentToolCallId` 的调用(codemode 脚本里的 `tools.*`,B10)挂在外层调用下;折叠时只列
9
- * 内层调用的标题(最近 5 个),展开时完整显示。
4
+ * 三级层级,用缩进表达:
5
+ * ```
6
+ * ⏺ read src/tui/tui.ts:1+120 第 0 列:⏺(运行中 accent、成功 success、失败 error)+ 工具名 + 摘要
7
+ * ⎿ 读取 120 行 第 2 列:⎿ + 一行结果摘要(tool-summary.ts)
8
+ * 1 /** 第 4 列:输出正文
9
+ * … 另 117 行(Ctrl+O 展开)
10
+ * ```
11
+ * - 运行中摘要行 `⎿ ⠋ 运行中 · 4s`:spinner 与底部 Loader 同帧(ToolTracker.tick 由 Loader.onFrame 驱动),
12
+ * 标题行不变;bash 流式显示尾部 8 行(muted)。task 显示 `子 Agent · 运行中 1m05s`。
13
+ * - 折叠(缺省):结果前 3 行;edit 显示 `details.diff`(`@@` dim、`+` success、`-` error、上下文 muted,
14
+ * 宽 ≥ 60 时带行号列),前 12 行;`Ctrl+O` 展开全部(上限 400 行,长行折行)。
15
+ * - 嵌套:带 `parentToolCallId` 的调用(codemode 脚本里的 `tools.*`)挂在外层调用下、右移 4 列;折叠时只列
16
+ * 最近 5 个内层调用的标题与摘要行,展开时完整显示。
10
17
  * - 工具输出先去掉 ANSI 与控制字符、Tab 换成空格,避免打乱布局。
11
- * - 自定义渲染(W3-B9a-2,`ToolDefinition.renderCall / renderResult`):`getTool` 能取到定义时,标题摘要
12
- * 优先取 `renderCall(input, width)` 的第一行(codemode 显示脚本首行);成功的结果优先用
13
- * `renderResult(result, width, expanded)`(去控制字符、按宽截断、上限 400 行);自定义渲染抛错或返回
14
- * 空时回到缺省显示;错误结果始终用缺省的红色显示。
18
+ * - 自定义渲染(`ToolDefinition.renderCall / renderResult`):标题摘要优先取 `renderCall` 第一行;成功的结果
19
+ * 正文优先用 `renderResult`;抛错或返回空时回到缺省显示;错误结果始终用缺省显示。
15
20
  */
16
- import { isAbsolute, relative } from "node:path";
17
- import { stripAnsi, truncateToWidth, wrapTextWithAnsi, } from "../../tui.js";
21
+ import { formatElapsed, truncateToWidth, wrapTextWithAnsi, } from "../../tui.js";
18
22
  import { contentText } from "./message-view.js";
23
+ import { bashOutput, cleanLines, diffOf, flat, parseDiff, resultSummary, toolSummary, } from "./tool-summary.js";
24
+ export { cleanLines, toolSummary } from "./tool-summary.js";
19
25
  export const COLLAPSED_RESULT_LINES = 3;
20
26
  export const COLLAPSED_DIFF_LINES = 12;
21
27
  export const BASH_TAIL_LINES = 8;
22
28
  export const EXPANDED_MAX_LINES = 400;
23
29
  export const COLLAPSED_CHILDREN = 5;
24
- /** 工具输出 → 可安全显示的行。 */
25
- export function cleanLines(text) {
26
- const clean = stripAnsi(text)
27
- .replace(/\r\n?/g, "\n")
28
- .replace(/\t/g, " ")
29
- .replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
30
- const lines = clean.split("\n");
31
- while (lines.length > 0 && lines[lines.length - 1].trim() === "")
32
- lines.pop();
33
- return lines;
34
- }
35
- function flat(text, max = 120) {
36
- const one = text.replace(/\s*\n\s*/g, " ⏎ ").trim();
37
- return one.length > max ? `${one.slice(0, max - 1)}…` : one;
38
- }
39
- function displayPath(path, cwd) {
40
- if (cwd === undefined || !isAbsolute(path))
41
- return path;
42
- const rel = relative(cwd, path);
43
- return rel === "" ? "." : rel.startsWith("..") || isAbsolute(rel) ? path : rel;
44
- }
45
- /** 标题行里的参数摘要。 */
46
- export function toolSummary(name, args, cwd) {
47
- if (typeof args !== "object" || args === null)
48
- return "";
49
- const a = args;
50
- const str = (key) => typeof a[key] === "string" && a[key] !== "" ? a[key] : undefined;
51
- const path = str("path") ?? str("file_path");
52
- switch (name) {
53
- case "bash":
54
- return flat(str("command") ?? "");
55
- case "read": {
56
- if (path === undefined)
57
- return "";
58
- const offset = typeof a["offset"] === "number" ? a["offset"] : undefined;
59
- const limit = typeof a["limit"] === "number" ? a["limit"] : undefined;
60
- const range = offset !== undefined || limit !== undefined
61
- ? `:${offset ?? 1}${limit !== undefined ? `+${limit}` : ""}`
62
- : "";
63
- return displayPath(path, cwd) + range;
64
- }
65
- case "grep":
66
- case "glob": {
67
- const pattern = str("pattern") ?? "";
68
- return path !== undefined ? `${flat(pattern)} in ${displayPath(path, cwd)}` : flat(pattern);
69
- }
70
- case "task":
71
- return flat(str("description") ?? str("prompt") ?? "");
72
- default:
73
- if (path !== undefined)
74
- return displayPath(path, cwd);
75
- for (const key of [
76
- "command",
77
- "pattern",
78
- "description",
79
- "query",
80
- "url",
81
- "name",
82
- "code",
83
- "script",
84
- ]) {
85
- const value = str(key);
86
- if (value !== undefined)
87
- return flat(value);
88
- }
89
- return "";
90
- }
91
- }
92
- function diffOf(result) {
93
- const details = result?.details;
94
- if (typeof details !== "object" || details === null)
95
- return undefined;
96
- const diff = details.diff;
97
- return typeof diff === "string" && diff !== "" ? diff : undefined;
98
- }
30
+ /** 正文缩进(第 4 列)。 */
31
+ const BODY = " ";
32
+ /** diff 行号列的最小显示宽度。 */
33
+ const DIFF_NUMBERS_MIN_WIDTH = 60;
99
34
  export class ToolView {
100
35
  toolCallId;
101
36
  toolName;
@@ -108,6 +43,12 @@ export class ToolView {
108
43
  nested = [];
109
44
  cache = null;
110
45
  version = 0;
46
+ startedAt;
47
+ /** 审批对话框打开时还没开始执行(摘要行显示「等待确认」,关闭后重新计时)。 */
48
+ awaiting = false;
49
+ elapsedMs = 0;
50
+ /** 重放的历史调用(没有真实耗时)。 */
51
+ replayed = false;
111
52
  /** 外层调用(嵌套时):子视图变化要让外层缓存失效。 */
112
53
  parent;
113
54
  constructor(toolCallId, toolName, args, options) {
@@ -115,6 +56,10 @@ export class ToolView {
115
56
  this.toolName = toolName;
116
57
  this.args = args;
117
58
  this.options = options;
59
+ this.startedAt = this.now();
60
+ }
61
+ now() {
62
+ return this.options.now?.() ?? Date.now();
118
63
  }
119
64
  get isRunning() {
120
65
  return this.state === "running";
@@ -141,6 +86,7 @@ export class ToolView {
141
86
  finish(result, isError) {
142
87
  this.result = result;
143
88
  this.state = isError || result.isError === true ? "error" : "done";
89
+ this.elapsedMs = this.now() - this.startedAt;
144
90
  this.touch();
145
91
  }
146
92
  addNested(child) {
@@ -149,6 +95,24 @@ export class ToolView {
149
95
  this.nested.push(child);
150
96
  this.touch();
151
97
  }
98
+ /** 审批打开 / 关闭:还没有输出的运行中调用显示「等待确认」,关闭后从此刻重新计时。 */
99
+ setAwaiting(awaiting) {
100
+ if (this.state !== "running")
101
+ return;
102
+ if (awaiting && this.partial !== "")
103
+ return;
104
+ if (!awaiting && this.awaiting)
105
+ this.startedAt = this.now();
106
+ if (this.awaiting === awaiting)
107
+ return;
108
+ this.awaiting = awaiting;
109
+ this.touch();
110
+ }
111
+ /** spinner 换帧 / 秒数变化:运行中的视图重画摘要行。 */
112
+ tick() {
113
+ if (this.state === "running")
114
+ this.touch();
115
+ }
152
116
  touch() {
153
117
  this.version++;
154
118
  this.parent?.touch();
@@ -162,20 +126,43 @@ export class ToolView {
162
126
  if (this.cache && this.cache.width === width && this.cache.version === this.version) {
163
127
  return this.cache.lines;
164
128
  }
165
- const lines = [this.header(width), ...this.body(width)];
129
+ const lines = [this.header(width), this.summaryLine(width), ...this.body(width)].filter((line) => line !== undefined);
166
130
  this.cache = { width, version: this.version, lines };
167
131
  return lines;
168
132
  }
169
133
  header(width) {
170
134
  const { theme } = this.options;
171
- const dot = this.state === "running"
172
- ? theme.fg("accent", "●")
173
- : this.state === "error"
174
- ? theme.fg("error", "●")
175
- : theme.fg("success", "●");
135
+ const color = this.state === "running" ? "accent" : this.state === "error" ? "error" : "success";
136
+ const dot = theme.fg(color, theme.glyphs.tool);
176
137
  const summary = this.customCall(width) ?? toolSummary(this.toolName, this.args, this.options.cwd);
177
138
  const title = `${dot} ${theme.bold(theme.fg("tool", this.toolName))}`;
178
- return truncateToWidth(summary === "" ? title : `${title} ${summary}`, width);
139
+ return truncateToWidth(summary === "" ? title : `${title} ${summary}`, width);
140
+ }
141
+ /** ` ⎿ 摘要`:运行中带 spinner 与秒数,完成后是结果摘要。 */
142
+ summaryLine(width) {
143
+ const { theme } = this.options;
144
+ const g = theme.glyphs;
145
+ const lead = " " + theme.fg("border", g.result) + " ";
146
+ if (this.state === "running" && this.awaiting) {
147
+ return truncateToWidth(lead + theme.fg("dim", `${g.spinnerStatic} 等待确认`), width);
148
+ }
149
+ if (this.state === "running") {
150
+ const frame = this.options.spinner?.() ?? g.spinnerStatic;
151
+ const elapsed = formatElapsed(this.now() - this.startedAt);
152
+ const what = this.toolName === "task" ? `子 Agent · 运行中 ${elapsed}` : `运行中 · ${elapsed}`;
153
+ return truncateToWidth(lead + theme.fg("accent", frame) + " " + theme.fg("muted", what), width);
154
+ }
155
+ const result = this.result;
156
+ const text = resultSummary({
157
+ name: this.toolName,
158
+ args: this.args,
159
+ result,
160
+ isError: this.state === "error",
161
+ lines: this.resultLines(),
162
+ elapsedMs: this.replayed ? undefined : this.elapsedMs,
163
+ nestedCount: this.nested.length,
164
+ }, theme);
165
+ return truncateToWidth(lead + text, width);
179
166
  }
180
167
  custom(render) {
181
168
  const tool = this.options.getTool?.(this.toolName);
@@ -201,93 +188,148 @@ export class ToolView {
201
188
  const result = this.result;
202
189
  if (result === undefined || this.state !== "done")
203
190
  return undefined;
204
- const inner = Math.max(1, width - 2);
191
+ const inner = Math.max(1, width - BODY.length);
205
192
  const rendered = this.custom((tool) => tool.renderResult?.(result, inner, this.expanded));
206
193
  if (!Array.isArray(rendered) || rendered.length === 0)
207
194
  return undefined;
208
195
  return rendered
209
196
  .slice(0, EXPANDED_MAX_LINES)
210
- .map((line) => " " + truncateToWidth(cleanLines(String(line)).join(" "), inner));
197
+ .map((line) => BODY + truncateToWidth(cleanLines(String(line)).join(" "), inner));
198
+ }
199
+ /** 完成后的结果正文(已清洗):bash 取 `details.output`。 */
200
+ resultLines() {
201
+ const result = this.result;
202
+ if (result === undefined)
203
+ return [];
204
+ if (this.toolName === "bash") {
205
+ const output = bashOutput(result);
206
+ if (output !== undefined)
207
+ return cleanLines(output);
208
+ }
209
+ return cleanLines(contentText(result.content));
211
210
  }
212
211
  indent(lines, width, color) {
213
212
  const { theme } = this.options;
214
- const inner = Math.max(1, width - 2);
213
+ const inner = Math.max(1, width - BODY.length);
215
214
  const out = [];
216
215
  for (const line of lines) {
217
216
  const pieces = this.expanded ? wrapTextWithAnsi(line, inner) : [truncateToWidth(line, inner)];
218
217
  for (const piece of pieces)
219
- out.push(" " + (color ? theme.fg(color, piece) : piece));
218
+ out.push(BODY + (color ? theme.fg(color, piece) : piece));
220
219
  }
221
220
  return out;
222
221
  }
223
222
  more(hidden) {
224
- return " " + this.options.theme.fg("dim", `… 另 ${hidden} 行(Ctrl+O 展开)`);
223
+ const { theme } = this.options;
224
+ return BODY + theme.fg("dim", `${theme.glyphs.ellipsis} 另 ${hidden} 行(Ctrl+O 展开)`);
225
225
  }
226
- body(width) {
226
+ nestedLines(width) {
227
227
  const { theme } = this.options;
228
+ if (this.nested.length === 0)
229
+ return [];
230
+ const inner = Math.max(1, width - BODY.length);
228
231
  const out = [];
229
- // 嵌套调用
230
- if (this.nested.length > 0) {
231
- if (this.expanded) {
232
- for (const child of this.nested) {
233
- for (const line of child.render(Math.max(1, width - 2)))
234
- out.push(" " + line);
235
- }
236
- }
237
- else {
238
- const shown = this.nested.slice(-COLLAPSED_CHILDREN);
239
- if (this.nested.length > shown.length) {
240
- out.push(" " + theme.fg("dim", `… 前 ${this.nested.length - shown.length} 个调用`));
241
- }
242
- for (const child of shown)
243
- out.push(" " + child.header(Math.max(1, width - 2)));
232
+ if (this.expanded) {
233
+ for (const child of this.nested) {
234
+ for (const line of child.render(inner))
235
+ out.push(BODY + line);
244
236
  }
237
+ return out;
238
+ }
239
+ const shown = this.nested.slice(-COLLAPSED_CHILDREN);
240
+ if (this.nested.length > shown.length) {
241
+ const hidden = this.nested.length - shown.length;
242
+ out.push(BODY + theme.fg("dim", `${theme.glyphs.ellipsis} 前 ${hidden} 个调用`));
245
243
  }
244
+ for (const child of shown) {
245
+ out.push(BODY + child.header(inner));
246
+ const summary = child.summaryLine(inner);
247
+ if (summary !== undefined)
248
+ out.push(BODY + summary);
249
+ }
250
+ return out;
251
+ }
252
+ body(width) {
253
+ const out = this.nestedLines(width);
246
254
  if (this.state === "running") {
247
255
  if (this.partial !== "") {
248
256
  const all = cleanLines(this.partial);
249
257
  const tail = this.expanded ? all.slice(-EXPANDED_MAX_LINES) : all.slice(-BASH_TAIL_LINES);
250
- out.push(...this.indent(tail, width, "dim"));
258
+ out.push(...this.indent(tail, width, "muted"));
251
259
  }
252
260
  return out;
253
261
  }
254
262
  const custom = this.customResult(width);
255
- if (custom !== undefined) {
256
- out.push(...custom);
257
- return out;
258
- }
263
+ if (custom !== undefined)
264
+ return [...out, ...custom];
259
265
  const diff = this.state === "done" ? diffOf(this.result) : undefined;
260
- if (diff !== undefined) {
261
- const all = cleanLines(diff).filter((l) => !l.startsWith("---") && !l.startsWith("+++"));
262
- const limit = this.expanded ? EXPANDED_MAX_LINES : COLLAPSED_DIFF_LINES;
263
- const shown = all.slice(0, limit);
264
- const inner = Math.max(1, width - 2);
265
- for (const line of shown) {
266
- const text = truncateToWidth(line, inner);
267
- const colored = line.startsWith("+")
268
- ? theme.fg("success", text)
269
- : line.startsWith("-")
270
- ? theme.fg("error", text)
271
- : line.startsWith("@@")
272
- ? theme.fg("accent", text)
273
- : theme.fg("dim", text);
274
- out.push(" " + colored);
275
- }
276
- if (all.length > shown.length)
277
- out.push(this.more(all.length - shown.length));
278
- return out;
279
- }
280
- const text = this.result === undefined ? "" : contentText(this.result.content);
281
- const all = cleanLines(text);
266
+ if (diff !== undefined)
267
+ return [...out, ...this.diffLines(parseDiff(diff), width)];
268
+ let all = this.resultLines();
269
+ // 非 bash 的失败:首行已在摘要里
270
+ if (this.state === "error" && this.toolName !== "bash")
271
+ all = all.slice(1);
282
272
  if (all.length === 0)
283
273
  return out;
284
274
  const limit = this.expanded ? EXPANDED_MAX_LINES : COLLAPSED_RESULT_LINES;
285
275
  const shown = all.slice(0, limit);
286
- out.push(...this.indent(shown, width, this.state === "error" ? "error" : "dim"));
276
+ if (this.state === "error")
277
+ out.push(...this.indent(shown, width, "error"));
278
+ else if (this.toolName === "read")
279
+ out.push(...this.readLines(shown, width));
280
+ else if (this.toolName === "grep")
281
+ out.push(...this.grepLines(shown, width));
282
+ else
283
+ out.push(...this.indent(shown, width, "muted"));
287
284
  if (all.length > shown.length)
288
285
  out.push(this.more(all.length - shown.length));
289
286
  return out;
290
287
  }
288
+ /** read:行号右对齐(按最长行号,至少 3 位)dim,与正文隔两格。 */
289
+ readLines(lines, width) {
290
+ const { theme } = this.options;
291
+ const parsed = lines.map((line) => /^\s*(\d+) {4}(.*)$/.exec(line));
292
+ const numWidth = Math.max(3, ...parsed.map((m) => (m ? m[1].length : 0)));
293
+ const styled = lines.map((line, i) => {
294
+ const m = parsed[i];
295
+ return m ? theme.fg("dim", m[1].padStart(numWidth)) + " " + m[2] : line;
296
+ });
297
+ return this.indent(styled, width);
298
+ }
299
+ /** grep:路径正文色,`:行号:` dim,命中片段 muted。 */
300
+ grepLines(lines, width) {
301
+ const { theme } = this.options;
302
+ const styled = lines.map((line) => {
303
+ const m = /^(.+?)(:\d+:)(.*)$/.exec(line);
304
+ return m
305
+ ? m[1] + theme.fg("dim", m[2]) + theme.fg("muted", m[3])
306
+ : theme.fg("muted", line);
307
+ });
308
+ return this.indent(styled, width);
309
+ }
310
+ diffLines(rows, width) {
311
+ const { theme } = this.options;
312
+ const limit = this.expanded ? EXPANDED_MAX_LINES : COLLAPSED_DIFF_LINES;
313
+ const shown = rows.slice(0, limit);
314
+ const numbers = width >= DIFF_NUMBERS_MIN_WIDTH;
315
+ const numWidth = Math.max(3, ...shown.map((r) => String(r.num ?? "").length));
316
+ const color = {
317
+ hunk: "dim",
318
+ add: "success",
319
+ del: "error",
320
+ ctx: "muted",
321
+ };
322
+ const styled = shown.map((row) => {
323
+ if (!numbers || row.kind === "hunk")
324
+ return theme.fg(color[row.kind], row.text);
325
+ const no = theme.fg("dim", String(row.num ?? "").padStart(numWidth));
326
+ return `${no} ${theme.fg(color[row.kind], row.text)}`;
327
+ });
328
+ const out = this.indent(styled, width);
329
+ if (rows.length > shown.length)
330
+ out.push(this.more(rows.length - shown.length));
331
+ return out;
332
+ }
291
333
  }
292
334
  /** 按 toolCallId 管理视图与嵌套;`Ctrl+O` 的全局展开状态也在这里。 */
293
335
  export class ToolTracker {
@@ -303,10 +345,15 @@ export class ToolTracker {
303
345
  get(toolCallId) {
304
346
  return this.views.get(toolCallId);
305
347
  }
348
+ /** 运行中的调用数(Loader 动词用)。 */
349
+ running() {
350
+ return [...this.views.values()].filter((view) => view.isRunning);
351
+ }
306
352
  /** 新调用;返回视图与它是否是顶层(顶层由调用方加进消息区)。 */
307
353
  start(event) {
308
354
  const existing = this.views.get(event.toolCallId);
309
- if (existing !== undefined)
355
+ // 同一调用的重复 start 复用;已结束的同名 id(有的供应商跨回合复用 id)另起一个视图
356
+ if (existing !== undefined && existing.isRunning)
310
357
  return { view: existing, topLevel: false };
311
358
  const view = new ToolView(event.toolCallId, event.toolName, event.args, this.options);
312
359
  view.setExpanded(this.expanded);
@@ -324,9 +371,20 @@ export class ToolTracker {
324
371
  end(toolCallId, result, isError) {
325
372
  this.views.get(toolCallId)?.finish(result, isError);
326
373
  }
374
+ /** 审批对话框打开 / 关闭(见 ToolView.setAwaiting)。 */
375
+ setAwaiting(awaiting) {
376
+ for (const view of this.views.values())
377
+ view.setAwaiting(awaiting);
378
+ }
379
+ /** Loader 换帧:运行中的视图重画摘要行。 */
380
+ tick() {
381
+ for (const view of this.views.values())
382
+ view.tick();
383
+ }
327
384
  /** 已完成的调用(重放历史)。 */
328
385
  completed(toolCallId, toolName, args, result, isError) {
329
386
  const { view } = this.start({ toolCallId, toolName, args });
387
+ view.replayed = true;
330
388
  if (result !== undefined)
331
389
  view.finish(result, isError);
332
390
  return view;
@@ -1,15 +1,44 @@
1
1
  /**
2
2
  * print 模式(`ama -p`,设计 §11.1 第 15–16 步)。[B6]
3
3
  *
4
- * - 提示 = 位置参数 + stdin 管道(stdin 非 TTY 时读到 EOF),两者都有时空一行拼接;都没有 → 用法错误 2。
4
+ * - 提示 = 位置参数 + stdin,两者都有时空一行拼接;都没有 → 用法错误 2。stdin 的读法:
5
+ * - 没有提示参数,或显式写了位置参数 `-`:读到 EOF(超过 3 s 时 stderr 提示一次正在等待);
6
+ * - 有提示参数且 stdin 是管道(shell 管道或父进程留的管道):等首字节,`AMA_STDIN_WAIT_MS`
7
+ * (缺省 2000,0 = 不等)内一个字节都没有 → 忽略 stdin 并在 stderr 提示;收到首字节后读到 EOF;
8
+ * - stdin 是普通文件 / 空设备:直接读(不会卡);TTY 或 `--no-stdin`:不读。
9
+ * - 图片:`--image`(可重复)与提示里的 `@图片路径` / 图片文件路径作为附件(modes/image-input.ts);
10
+ * 显式附件遇到当前模型不收图片、文件不存在或超限 → 用法错误 2,不发请求。
5
11
  * - `--output-format text`(缺省):运行结束后输出最后一条助手文本;`json`:一个结果对象(文本、
6
12
  * 停止原因、用量、缓存命中率、[W3-C2] `cache` 统计(同 `get_session_stats.cache`)、全部条目);
7
13
  * `stream-json`:每个会话事件一行(线上形状同 RPC,含 `cache_miss` / `cache_warm` /
8
14
  * `context_pressure`)。
9
- * - 无人值守:ask → deny(bootstrap 已按 print 设 unattended)。
10
- * - 退出码:最终助手消息 `error / aborted` 或提示被拒 → 1;SIGINT 130、SIGTERM 143(先 abort)。
15
+ * - 重试:text / json 格式在 stderr 打一行 `↻ 重试 n/m`(stream-json 里本来就有事件),等待期间不再无声。
16
+ * - 无人值守:ask → deny(bootstrap 已按 print 设 unattended)。被拒的调用(`tool_execution_end`
17
+ * 带 `denied`)在 stderr 汇总一行(工具 ×次数、首个原因、放行办法),json 结果带 `deniedTools`。
18
+ * - `--max-turns N`:到达上限时若最后一条助手消息停在 toolUse(还有活没做完)→ stderr 一行、
19
+ * json 带 `maxTurnsReached`、退出码 1。
20
+ * - 退出码:最终助手消息 `error / aborted` 或提示被拒 → 1;有工具调用被拒 → 7;SIGINT 130、
21
+ * SIGTERM 143(先 abort)。
11
22
  */
12
- import type { ModeContext } from "../../cli/deps.js";
23
+ import type { CliIo, ModeContext } from "../../cli/deps.js";
13
24
  import type { Runtime } from "../../cli/runtime.js";
14
25
  export declare function joinPrompt(argument: string | undefined, piped: string): string;
26
+ /** 等 stdin 多久后提示一次「正在等待」。 */
27
+ export declare const STDIN_WAIT_HINT_MS = 3000;
28
+ /** 有提示参数时等管道首字节的缺省上限;`AMA_STDIN_WAIT_MS` 覆盖,0 = 不等待。 */
29
+ export declare const STDIN_FIRST_BYTE_MS = 2000;
30
+ export declare function stdinWaitMs(env: Readonly<Record<string, string | undefined>>): number;
31
+ /**
32
+ * 决定读不读 stdin(规则见文件头)。`mode`:`auto` 按提示参数与 stdin 类型决定,`explicit`
33
+ * (位置参数 `-`)一直等到 EOF,`off`(`--no-stdin`)不读。
34
+ */
35
+ export declare function readPromptStdin(io: CliIo, prompt: string | undefined, mode: "auto" | "explicit" | "off", hintMs?: number): Promise<string>;
15
36
  export declare function runPrintMode(runtime: Runtime, context: ModeContext): Promise<number>;
37
+ /** 被拒的一次工具调用(json 结果的 `deniedTools` 元素)。 */
38
+ export interface DeniedTool {
39
+ toolCallId: string;
40
+ toolName: string;
41
+ reason: string;
42
+ }
43
+ /** stderr 一行:被拒的工具 ×次数、首个原因、放行办法。 */
44
+ export declare function describeDenied(denied: readonly DeniedTool[]): string;