@armadra/agent 0.6.2 → 0.6.4

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 (147) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/CHANGELOG.zh-CN.md +59 -0
  3. package/README.md +168 -575
  4. package/README.zh-CN.md +165 -596
  5. package/dist/agent/queue.d.ts +9 -0
  6. package/dist/agent/queue.js +27 -0
  7. package/dist/agent/session-subagent.d.ts +1 -0
  8. package/dist/agent/session-subagent.js +9 -0
  9. package/dist/agent/session.d.ts +11 -0
  10. package/dist/agent/session.js +29 -2
  11. package/dist/agent/subagent-background.d.ts +75 -0
  12. package/dist/agent/subagent-background.js +209 -0
  13. package/dist/agent/subagent-direct.d.ts +5 -2
  14. package/dist/agent/subagent-direct.js +34 -1
  15. package/dist/agent/subagent-registry.d.ts +26 -6
  16. package/dist/agent/subagent-registry.js +66 -94
  17. package/dist/agent/types-w5.d.ts +11 -1
  18. package/dist/agent/types.d.ts +12 -0
  19. package/dist/agents/builtin.js +0 -1
  20. package/dist/agents/external.js +0 -1
  21. package/dist/agents/parse.js +4 -3
  22. package/dist/agents/result.d.ts +7 -1
  23. package/dist/agents/result.js +20 -1
  24. package/dist/agents/task-control.d.ts +13 -2
  25. package/dist/agents/task-record.d.ts +16 -4
  26. package/dist/agents/task-record.js +32 -0
  27. package/dist/agents/types.d.ts +5 -1
  28. package/dist/ai/apis/chatgpt-rate-limits.js +7 -2
  29. package/dist/ai/providers/discovered-cache.d.ts +10 -4
  30. package/dist/ai/providers/discovered-cache.js +30 -14
  31. package/dist/ai/types.d.ts +2 -1
  32. package/dist/auth/chatgpt/backend-client.d.ts +6 -5
  33. package/dist/auth/chatgpt/backend-client.js +8 -7
  34. package/dist/bundle/ama.cjs +2034 -552
  35. package/dist/cli/compose-agents.d.ts +2 -1
  36. package/dist/cli/compose-agents.js +11 -2
  37. package/dist/cli/subcommands/models-discover.d.ts +1 -0
  38. package/dist/cli/subcommands/models-discover.js +12 -2
  39. package/dist/config/json-schema.js +10 -2
  40. package/dist/config/key-docs.js +9 -2
  41. package/dist/config/merge.d.ts +1 -1
  42. package/dist/config/merge.js +20 -3
  43. package/dist/config/schema-w5.js +4 -2
  44. package/dist/config/schema.js +4 -0
  45. package/dist/config/settings-registry.js +5 -0
  46. package/dist/config/types-w5.d.ts +10 -0
  47. package/dist/config/types-w5.js +2 -0
  48. package/dist/config/types.d.ts +5 -1
  49. package/dist/drivers/runner.js +29 -2
  50. package/dist/git/info.d.ts +19 -0
  51. package/dist/git/info.js +64 -8
  52. package/dist/i18n/catalog.d.ts +58 -8
  53. package/dist/i18n/messages/agents.d.ts +60 -0
  54. package/dist/i18n/messages/agents.js +62 -2
  55. package/dist/i18n/messages/config-keys.d.ts +8 -0
  56. package/dist/i18n/messages/config-keys.js +18 -10
  57. package/dist/i18n/messages/config.d.ts +8 -0
  58. package/dist/i18n/messages/interactive-startup.d.ts +0 -16
  59. package/dist/i18n/messages/interactive-startup.js +0 -16
  60. package/dist/i18n/messages/interactive-view.d.ts +8 -0
  61. package/dist/i18n/messages/interactive-view.js +10 -0
  62. package/dist/i18n/messages/interactive.d.ts +33 -16
  63. package/dist/i18n/messages/interactive.js +31 -0
  64. package/dist/i18n/messages/print.d.ts +4 -0
  65. package/dist/i18n/messages/print.js +4 -0
  66. package/dist/i18n/messages/report.d.ts +8 -0
  67. package/dist/i18n/messages/report.js +12 -4
  68. package/dist/i18n/messages/settings.d.ts +8 -0
  69. package/dist/i18n/messages/settings.js +8 -0
  70. package/dist/i18n/messages/subcommands-config.d.ts +4 -0
  71. package/dist/i18n/messages/subcommands-config.js +4 -0
  72. package/dist/i18n/messages/subcommands.d.ts +4 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/modes/commands-core.js +19 -4
  75. package/dist/modes/interactive/agent-bar.d.ts +3 -1
  76. package/dist/modes/interactive/agent-bar.js +12 -5
  77. package/dist/modes/interactive/agent-ui.d.ts +21 -3
  78. package/dist/modes/interactive/agent-ui.js +82 -12
  79. package/dist/modes/interactive/agent-view.d.ts +9 -0
  80. package/dist/modes/interactive/agent-view.js +28 -4
  81. package/dist/modes/interactive/approval-dock.d.ts +51 -0
  82. package/dist/modes/interactive/approval-dock.js +112 -0
  83. package/dist/modes/interactive/approval-ui.d.ts +43 -0
  84. package/dist/modes/interactive/approval-ui.js +64 -0
  85. package/dist/modes/interactive/commands.js +4 -1
  86. package/dist/modes/interactive/event-notices.d.ts +6 -1
  87. package/dist/modes/interactive/event-notices.js +7 -1
  88. package/dist/modes/interactive/interactive-mode.d.ts +4 -2
  89. package/dist/modes/interactive/interactive-mode.js +46 -39
  90. package/dist/modes/interactive/interrupt-send.d.ts +25 -0
  91. package/dist/modes/interactive/interrupt-send.js +32 -0
  92. package/dist/modes/interactive/key-dispatch.d.ts +29 -5
  93. package/dist/modes/interactive/key-dispatch.js +97 -8
  94. package/dist/modes/interactive/line/line-mode.d.ts +1 -0
  95. package/dist/modes/interactive/line/line-mode.js +34 -7
  96. package/dist/modes/interactive/run-indicator.d.ts +34 -2
  97. package/dist/modes/interactive/run-indicator.js +62 -8
  98. package/dist/modes/interactive/session-events.js +5 -1
  99. package/dist/modes/interactive/startup-header.d.ts +29 -14
  100. package/dist/modes/interactive/startup-header.js +93 -59
  101. package/dist/modes/interactive/startup-logo.d.ts +83 -0
  102. package/dist/modes/interactive/startup-logo.js +183 -0
  103. package/dist/modes/interactive/status-area.d.ts +18 -0
  104. package/dist/modes/interactive/status-area.js +67 -1
  105. package/dist/modes/interactive/status-bar.d.ts +13 -2
  106. package/dist/modes/interactive/status-bar.js +60 -17
  107. package/dist/modes/interactive/status-line.d.ts +11 -8
  108. package/dist/modes/interactive/status-line.js +52 -39
  109. package/dist/modes/interactive/status-quota.d.ts +58 -0
  110. package/dist/modes/interactive/status-quota.js +155 -0
  111. package/dist/modes/interactive/subagent-view.d.ts +1 -0
  112. package/dist/modes/interactive/subagent-view.js +8 -0
  113. package/dist/modes/interactive/task-background.d.ts +31 -0
  114. package/dist/modes/interactive/task-background.js +68 -0
  115. package/dist/modes/interactive/tool-view.d.ts +7 -1
  116. package/dist/modes/interactive/tool-view.js +24 -1
  117. package/dist/modes/print/print-mode.d.ts +11 -0
  118. package/dist/modes/print/print-mode.js +36 -1
  119. package/dist/modes/rpc/commands.d.ts +4 -1
  120. package/dist/modes/rpc/commands.js +24 -2
  121. package/dist/rpc.d.ts +20 -0
  122. package/dist/rpc.js +3 -0
  123. package/dist/tools/task-ctl.d.ts +2 -0
  124. package/dist/tools/task-ctl.js +7 -2
  125. package/dist/tools/task.d.ts +11 -0
  126. package/dist/tools/task.js +20 -2
  127. package/dist/tools/types.d.ts +6 -0
  128. package/dist/tui/components/editor.d.ts +2 -0
  129. package/dist/tui/components/editor.js +4 -0
  130. package/dist/tui/components/loader.d.ts +5 -1
  131. package/dist/tui/components/loader.js +18 -5
  132. package/dist/tui/keybindings.d.ts +15 -3
  133. package/dist/tui/keybindings.js +15 -3
  134. package/docs/agents.md +52 -28
  135. package/docs/en/host-api.md +5 -1
  136. package/docs/en/providers.md +1 -1
  137. package/docs/en/rpc.md +28 -14
  138. package/docs/en/sessions.md +3 -1
  139. package/docs/en/tui.md +106 -70
  140. package/docs/host-api.md +5 -1
  141. package/docs/providers.md +4 -2
  142. package/docs/rpc.md +28 -14
  143. package/docs/session-format.md +1 -1
  144. package/docs/sessions.md +2 -1
  145. package/docs/tui-design.md +42 -29
  146. package/docs/tui.md +106 -70
  147. package/package.json +1 -1
@@ -9,9 +9,16 @@
9
9
  * 助手在流式输出 → 末块是思考 `思考中`,否则 `回复中 · ↓≈1.2k`(本条输出的估算 token,4 字符 ≈ 1)
10
10
  * 其余(等首个 token)→ `思考中`
11
11
  * 运行或压缩时 Loader 才挂进槽位;附加项 `Esc 中断`。
12
+ * [W7-A] 有该显示的子 Agent 任务时再带可丢弃的 `↓ Agent 栏`(窄屏先丢它);任务随时出现 / 结束,
13
+ * 每帧对一次,变了才换动词。
14
+ * [W7-C] 有阻塞中的前台任务时可丢弃项前面加 `Ctrl+B 转后台`;有停靠的审批时 `↓ Agent 栏` 换成
15
+ * `↓ 处理审批`。顺序 `Esc 中断 · Ctrl+B 转后台 · ↓ Agent 栏`,窄屏从后往前丢。
16
+ *
17
+ * [打断并发送] 输入框有字时可丢弃项最前面加 `Enter 排队 · Ctrl+X 打断并发送`(`ui.enterWhileRunning:
18
+ * "interrupt"` 时语义互换),顺序 `Esc 中断 · Enter 排队 · Ctrl+X 打断并发送 · Ctrl+B 转后台 · ↓ Agent 栏`。
12
19
  *
13
20
  * 排队消息:缩进 2 列(挂在当前回合下),`↳ 插话 / 之后 文本` 整体 muted、标签 dim;超过 3 条首行
14
- * `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`。
21
+ * `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`,有插话时中间加 `Ctrl+X 立即发送`(interrupt 模式是 Enter)。
15
22
  */
16
23
  import type { SessionEvent } from "../../agent/types.js";
17
24
  import { Container, Text, type Loader, type Theme } from "../../tui.js";
@@ -24,7 +31,13 @@ export interface RunIndicatorDeps {
24
31
  slot: Container;
25
32
  tools: ToolTracker;
26
33
  render(): void;
34
+ /** [W7-C] `app.tasks.background` 的按键标签(`Ctrl+B`);没有绑定时不提示。 */
35
+ backgroundKey?: string;
36
+ /** `app.message.interrupt` 的按键标签(`Ctrl+X`);没有绑定时不提示。 */
37
+ interruptKey?: string;
27
38
  }
39
+ /** 运行中 Enter 的语义(`ui.enterWhileRunning`)。 */
40
+ export type EnterMode = "queue" | "interrupt";
28
41
  export declare class RunIndicator {
29
42
  private readonly deps;
30
43
  running: boolean;
@@ -32,7 +45,22 @@ export declare class RunIndicator {
32
45
  private approval;
33
46
  private retry;
34
47
  private stream;
48
+ /** [W7-A] 有该显示的子 Agent 任务(interactive-mode 在 AgentUi 建好后接上)。 */
49
+ agents: () => boolean;
50
+ /** [W7-C] 有阻塞中的前台任务(Ctrl+B 可转后台)。 */
51
+ background: () => boolean;
52
+ /** [W7-C] 有停靠在 Agent 栏的审批。 */
53
+ docked: () => boolean;
54
+ /** 输入框有字(显示 Enter / 专用键的提示)。 */
55
+ draft: () => boolean;
56
+ /** 运行中 Enter 的语义。 */
57
+ enterMode: () => EnterMode;
58
+ private hintKey;
35
59
  constructor(deps: RunIndicatorDeps);
60
+ /** 可丢弃提示变了才换动词(每帧、输入框内容变化时调)。 */
61
+ refreshHints(): void;
62
+ /** 可丢弃的按键提示(按丢弃的逆序排列)。 */
63
+ private optionalHints;
36
64
  get busy(): boolean;
37
65
  setApproval(open: boolean): void;
38
66
  /** 会话切换后清零。 */
@@ -46,6 +74,10 @@ export declare class RunIndicator {
46
74
  /** 排队消息(插话 / 之后),挂在 Loader 之上。 */
47
75
  export declare class QueueView extends Text {
48
76
  private readonly theme;
49
- constructor(theme: Theme);
77
+ private readonly nowKey;
78
+ /**
79
+ * @param nowKey 立即送出排队插话的键(专用键;interrupt 模式下是 Enter);undefined 不提示。
80
+ */
81
+ constructor(theme: Theme, nowKey?: () => string | undefined);
50
82
  setQueue(steering: readonly string[], followUp: readonly string[]): void;
51
83
  }
@@ -9,9 +9,16 @@
9
9
  * 助手在流式输出 → 末块是思考 `思考中`,否则 `回复中 · ↓≈1.2k`(本条输出的估算 token,4 字符 ≈ 1)
10
10
  * 其余(等首个 token)→ `思考中`
11
11
  * 运行或压缩时 Loader 才挂进槽位;附加项 `Esc 中断`。
12
+ * [W7-A] 有该显示的子 Agent 任务时再带可丢弃的 `↓ Agent 栏`(窄屏先丢它);任务随时出现 / 结束,
13
+ * 每帧对一次,变了才换动词。
14
+ * [W7-C] 有阻塞中的前台任务时可丢弃项前面加 `Ctrl+B 转后台`;有停靠的审批时 `↓ Agent 栏` 换成
15
+ * `↓ 处理审批`。顺序 `Esc 中断 · Ctrl+B 转后台 · ↓ Agent 栏`,窄屏从后往前丢。
16
+ *
17
+ * [打断并发送] 输入框有字时可丢弃项最前面加 `Enter 排队 · Ctrl+X 打断并发送`(`ui.enterWhileRunning:
18
+ * "interrupt"` 时语义互换),顺序 `Esc 中断 · Enter 排队 · Ctrl+X 打断并发送 · Ctrl+B 转后台 · ↓ Agent 栏`。
12
19
  *
13
20
  * 排队消息:缩进 2 列(挂在当前回合下),`↳ 插话 / 之后 文本` 整体 muted、标签 dim;超过 3 条首行
14
- * `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`。
21
+ * `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`,有插话时中间加 `Ctrl+X 立即发送`(interrupt 模式是 Enter)。
15
22
  */
16
23
  import { msg } from "../../i18n/index.js";
17
24
  import { Container, Text } from "../../tui.js";
@@ -48,8 +55,41 @@ export class RunIndicator {
48
55
  approval = false;
49
56
  retry;
50
57
  stream;
58
+ /** [W7-A] 有该显示的子 Agent 任务(interactive-mode 在 AgentUi 建好后接上)。 */
59
+ agents = () => false;
60
+ /** [W7-C] 有阻塞中的前台任务(Ctrl+B 可转后台)。 */
61
+ background = () => false;
62
+ /** [W7-C] 有停靠在 Agent 栏的审批。 */
63
+ docked = () => false;
64
+ /** 输入框有字(显示 Enter / 专用键的提示)。 */
65
+ draft = () => false;
66
+ /** 运行中 Enter 的语义。 */
67
+ enterMode = () => "queue";
68
+ hintKey = "";
51
69
  constructor(deps) {
52
70
  this.deps = deps;
71
+ deps.loader.onFrame(() => this.refreshHints());
72
+ }
73
+ /** 可丢弃提示变了才换动词(每帧、输入框内容变化时调)。 */
74
+ refreshHints() {
75
+ if (this.busy && !this.approval && this.optionalHints().join("\n") !== this.hintKey)
76
+ this.applyVerb();
77
+ }
78
+ /** 可丢弃的按键提示(按丢弃的逆序排列)。 */
79
+ optionalHints() {
80
+ const down = this.deps.theme.glyphs.arrowDown;
81
+ const key = this.deps.backgroundKey;
82
+ const out = [];
83
+ const send = this.deps.interruptKey;
84
+ if (send !== undefined && this.draft())
85
+ out.push(msg().interactive.view.run.sendHint(this.enterMode(), send));
86
+ if (key !== undefined && this.background())
87
+ out.push(msg().agents.background.runHint(key));
88
+ if (this.docked())
89
+ out.push(msg().agents.approval.runHint(down));
90
+ else if (this.agents())
91
+ out.push(msg().agents.bar.runHint(down));
92
+ return out;
53
93
  }
54
94
  get busy() {
55
95
  return this.running || this.compacting;
@@ -133,17 +173,21 @@ export class RunIndicator {
133
173
  const m = msg().interactive.view.run;
134
174
  const esc = [m.esc];
135
175
  if (this.approval) {
176
+ this.hintKey = "";
136
177
  loader.setVerb(m.awaitingApproval);
137
178
  return;
138
179
  }
180
+ const hints = this.optionalHints();
181
+ this.hintKey = hints.join("\n");
182
+ const optional = hints.length > 0 ? { optional: hints } : {};
139
183
  const running = tools.running();
140
184
  const top = running.filter((view) => !running.some((p) => p.children.includes(view)));
141
185
  if (top.length === 1) {
142
- loader.setVerb(m.runningTool(top[0].toolName), esc);
186
+ loader.setVerb(m.runningTool(top[0].toolName), esc, optional);
143
187
  return;
144
188
  }
145
189
  if (top.length > 1) {
146
- loader.setVerb(m.runningTools(top.length), esc);
190
+ loader.setVerb(m.runningTools(top.length), esc, optional);
147
191
  return;
148
192
  }
149
193
  if (this.retry !== undefined) {
@@ -154,23 +198,28 @@ export class RunIndicator {
154
198
  return;
155
199
  }
156
200
  if (this.compacting) {
157
- loader.setVerb(m.compacting, esc);
201
+ loader.setVerb(m.compacting, esc, optional);
158
202
  return;
159
203
  }
160
204
  const stream = this.stream;
161
205
  if (stream !== undefined && !stream.thinking && stream.tokens > 0) {
162
- loader.setVerb(m.replying, [`↓≈${formatTokens(stream.tokens)}`, ...esc]);
206
+ loader.setVerb(m.replying, [`↓≈${formatTokens(stream.tokens)}`, ...esc], optional);
163
207
  return;
164
208
  }
165
- loader.setVerb(m.thinking, esc);
209
+ loader.setVerb(m.thinking, esc, optional);
166
210
  }
167
211
  }
168
212
  /** 排队消息(插话 / 之后),挂在 Loader 之上。 */
169
213
  export class QueueView extends Text {
170
214
  theme;
171
- constructor(theme) {
215
+ nowKey;
216
+ /**
217
+ * @param nowKey 立即送出排队插话的键(专用键;interrupt 模式下是 Enter);undefined 不提示。
218
+ */
219
+ constructor(theme, nowKey = () => undefined) {
172
220
  super("");
173
221
  this.theme = theme;
222
+ this.nowKey = nowKey;
174
223
  }
175
224
  setQueue(steering, followUp) {
176
225
  const t = this.theme;
@@ -185,7 +234,12 @@ export class QueueView extends Text {
185
234
  t.fg("dim", msg().interactive.view.run.queueMore(t.glyphs.ellipsis, rows.length - shown.length)));
186
235
  }
187
236
  if (rows.length > 0) {
188
- shown.push(" " + t.fg("dim", msg().interactive.view.run.queueHint(t.glyphs.arrowUp)));
237
+ const run = msg().interactive.view.run;
238
+ const now = steering.length > 0 ? this.nowKey() : undefined;
239
+ const hint = now === undefined
240
+ ? run.queueHint(t.glyphs.arrowUp)
241
+ : run.queueHintNow(t.glyphs.arrowUp, now);
242
+ shown.push(" " + t.fg("dim", hint));
189
243
  }
190
244
  this.setText(shown.join("\n"));
191
245
  }
@@ -9,8 +9,12 @@ export function createSessionEventHandler(deps) {
9
9
  const { view, tools, status, area, agentUi, indicator, session, setQueue, notice, render } = deps;
10
10
  const images = new ImageBudgetNotices((text) => notice("info", text));
11
11
  return (event) => {
12
- if (area.onEvent(event) || agentUi().onEvent(event))
12
+ if (area.onEvent(event) || agentUi().onEvent(event)) {
13
+ // [W7-A] 子任务出现 / 结束:运行提示行的「↓ Agent 栏」跟着变
14
+ if (event.type === "subagent_start" || event.type === "subagent_end")
15
+ indicator.sync();
13
16
  return render();
17
+ }
14
18
  images.onEvent(event);
15
19
  switch (event.type) {
16
20
  case "agent_settled":
@@ -1,32 +1,43 @@
1
1
  /**
2
2
  * 交互模式启动头(终端界面视觉设计 v1 §3.1)。
3
3
  *
4
- * - `normal`:宽度 ≥ 56 且非 `ui.compact` 时画框(宽 `min(width, 64)`,左对齐)——标题 `✻ ama x.y.z`、
5
- * 键值行(模型 / 目录 / 模式 / 已加载 / 宿主 / 警告)、按键提示;否则去框去键列,每项一行。
6
- * - `header`:一行 `✻ ama x.y.z · 模型 · 模式 · /help`,无框。`silent` 不输出(调用方不加本组件)。
7
- * - 着色:`✻` accent、标题粗体;键列 dim;模型 accent;已信任 success / 未信任 warning;Bypass 模式 warning;
8
- * 按键提示 dim。路径过长时从左截断(`…/armadra-agent`)。宽度变化时按新宽重算是否去框。
4
+ * - `normal`:无框。宽度 ≥ 72 时「AMA」字符画(startup-logo.ts)在左、信息列在右;48–71 列字符画在上、
5
+ * 空一行、信息列在下;< 48 列退回两行简洁头(不画字符画)。`ui.logo: "off"` 或 `ui.compact` 时只画信息列,
6
+ * 首行 `✻ ama x.y.z`。
7
+ * - 信息列:标题(版本)、模型 · 思考、目录 · 信任(来源)、模式 · 预设 · codemode、已加载 / 宿主 / 警告
8
+ * (有才画)、按键提示。
9
+ * - `header`:一行 `✻ ama x.y.z · 模型 · 模式 · /help`。`silent` 不输出(调用方不加本组件)。
10
+ * - 着色:字符画按列 accent → user → tool 渐变;`✻` accent、标题粗体;模型 accent;已信任 success / 未信任
11
+ * warning;Bypass 模式 warning;按键提示与分隔点 dim。路径过长时从左截断(`…/armadra-agent`)。
12
+ * - 启动动画通过 `setSweep` 改字符画的扫描位置(undefined = 定格),信息列不变。
9
13
  */
10
14
  import type { StartupInfo } from "../../cli/startup-screen.js";
11
15
  import { type Component, type Theme } from "../../tui.js";
12
- /** 画框的最小宽度;更窄时去框去键列。 */
13
- export declare const HEADER_BOX_MIN_WIDTH = 56;
14
- /** 设计稿写 60;常见的「供应商/模型@渠道 · 思考 级别」要 62 列左右才不截断,取 64。 */
15
- export declare const HEADER_BOX_MAX_WIDTH = 64;
16
+ import { type SweepTarget } from "./startup-logo.js";
17
+ /** 字符画与信息列并排的最小宽度。 */
18
+ export declare const HEADER_SIDE_MIN_WIDTH = 72;
19
+ /** 画字符画的最小宽度;更窄时两行简洁头。 */
20
+ export declare const HEADER_LOGO_MIN_WIDTH = 48;
16
21
  export interface StartupHeaderOptions {
17
22
  theme: Theme;
18
23
  level: "normal" | "header";
19
- /** `ui.compact`:normal 也不画框。 */
20
- compact?: boolean;
24
+ /** `ui.compact`:不画字符画。 */
25
+ compact?: boolean | undefined;
26
+ /** `ui.logo`,缺省 auto;off 不画字符画。 */
27
+ logo?: "auto" | "off" | undefined;
21
28
  }
22
29
  /** 从左截断到 `width` 列(保留末尾,前面加省略号)。 */
23
30
  export declare function truncateLeft(text: string, width: number, ellipsis?: string): string;
24
- export declare class StartupHeader implements Component {
31
+ export declare class StartupHeader implements Component, SweepTarget {
25
32
  private readonly info;
26
33
  private readonly options;
27
34
  private cache;
35
+ private sweep;
28
36
  constructor(info: StartupInfo, options: StartupHeaderOptions);
29
37
  private get theme();
38
+ /** 这个宽度下是否画字符画(动画只在画了时播放)。 */
39
+ hasLogo(width: number): boolean;
40
+ setSweep(sweep: number | undefined): void;
30
41
  private title;
31
42
  private modeText;
32
43
  private trustText;
@@ -34,7 +45,11 @@ export declare class StartupHeader implements Component {
34
45
  private codemodeText;
35
46
  render(width: number): string[];
36
47
  private headerLine;
37
- private plainLines;
38
- private boxLines;
48
+ /** < 48 列:两行(版本 · 模型 · 思考 / 模式 · 目录 · 信任),有警告再加一行。 */
49
+ private narrowLines;
50
+ /** 信息列:首行 `title`,其后模型、目录、模式、可选行、按键提示;每行截到 `width`。 */
51
+ private infoLines;
52
+ /** 字符画 + 信息列:够宽并排(顶端对齐),否则上下叠放。 */
53
+ private logoLines;
39
54
  invalidate(): void;
40
55
  }
@@ -1,19 +1,26 @@
1
1
  /**
2
2
  * 交互模式启动头(终端界面视觉设计 v1 §3.1)。
3
3
  *
4
- * - `normal`:宽度 ≥ 56 且非 `ui.compact` 时画框(宽 `min(width, 64)`,左对齐)——标题 `✻ ama x.y.z`、
5
- * 键值行(模型 / 目录 / 模式 / 已加载 / 宿主 / 警告)、按键提示;否则去框去键列,每项一行。
6
- * - `header`:一行 `✻ ama x.y.z · 模型 · 模式 · /help`,无框。`silent` 不输出(调用方不加本组件)。
7
- * - 着色:`✻` accent、标题粗体;键列 dim;模型 accent;已信任 success / 未信任 warning;Bypass 模式 warning;
8
- * 按键提示 dim。路径过长时从左截断(`…/armadra-agent`)。宽度变化时按新宽重算是否去框。
4
+ * - `normal`:无框。宽度 ≥ 72 时「AMA」字符画(startup-logo.ts)在左、信息列在右;48–71 列字符画在上、
5
+ * 空一行、信息列在下;< 48 列退回两行简洁头(不画字符画)。`ui.logo: "off"` 或 `ui.compact` 时只画信息列,
6
+ * 首行 `✻ ama x.y.z`。
7
+ * - 信息列:标题(版本)、模型 · 思考、目录 · 信任(来源)、模式 · 预设 · codemode、已加载 / 宿主 / 警告
8
+ * (有才画)、按键提示。
9
+ * - `header`:一行 `✻ ama x.y.z · 模型 · 模式 · /help`。`silent` 不输出(调用方不加本组件)。
10
+ * - 着色:字符画按列 accent → user → tool 渐变;`✻` accent、标题粗体;模型 accent;已信任 success / 未信任
11
+ * warning;Bypass 模式 warning;按键提示与分隔点 dim。路径过长时从左截断(`…/armadra-agent`)。
12
+ * - 启动动画通过 `setSweep` 改字符画的扫描位置(undefined = 定格),信息列不变。
9
13
  */
10
14
  import { msg } from "../../i18n/index.js";
11
15
  import { permissionModeLabel } from "../../permissions/modes.js";
12
- import { Box, KeyValue, padToWidth, truncateToWidth, visibleWidth, } from "../../tui.js";
13
- /** 画框的最小宽度;更窄时去框去键列。 */
14
- export const HEADER_BOX_MIN_WIDTH = 56;
15
- /** 设计稿写 60;常见的「供应商/模型@渠道 · 思考 级别」要 62 列左右才不截断,取 64。 */
16
- export const HEADER_BOX_MAX_WIDTH = 64;
16
+ import { truncateToWidth, visibleWidth } from "../../tui.js";
17
+ import { logoRows, logoWidth, paintLogo } from "./startup-logo.js";
18
+ /** 字符画与信息列并排的最小宽度。 */
19
+ export const HEADER_SIDE_MIN_WIDTH = 72;
20
+ /** 画字符画的最小宽度;更窄时两行简洁头。 */
21
+ export const HEADER_LOGO_MIN_WIDTH = 48;
22
+ /** 字符画与信息列之间的空列。 */
23
+ const GAP = 3;
17
24
  /** 从左截断到 `width` 列(保留末尾,前面加省略号)。 */
18
25
  export function truncateLeft(text, width, ellipsis = "…") {
19
26
  if (visibleWidth(text) <= width)
@@ -33,6 +40,13 @@ export function truncateLeft(text, width, ellipsis = "…") {
33
40
  }
34
41
  return ellipsis + out;
35
42
  }
43
+ /** 按 ` · ` 分段的提示放不下时从末尾整段去掉(至少留第一段,再不够由调用方截断)。 */
44
+ function fitSegments(text, width) {
45
+ const parts = text.split(" · ");
46
+ while (parts.length > 1 && visibleWidth(parts.join(" · ")) > width)
47
+ parts.pop();
48
+ return parts.join(" · ");
49
+ }
36
50
  /** 去掉模型引用里的渠道(`@messages`)。 */
37
51
  function withoutChannel(model) {
38
52
  const at = model.indexOf("@");
@@ -42,6 +56,7 @@ export class StartupHeader {
42
56
  info;
43
57
  options;
44
58
  cache = null;
59
+ sweep;
45
60
  constructor(info, options) {
46
61
  this.info = info;
47
62
  this.options = options;
@@ -49,6 +64,19 @@ export class StartupHeader {
49
64
  get theme() {
50
65
  return this.options.theme;
51
66
  }
67
+ /** 这个宽度下是否画字符画(动画只在画了时播放)。 */
68
+ hasLogo(width) {
69
+ return (this.options.level === "normal" &&
70
+ this.options.logo !== "off" &&
71
+ this.options.compact !== true &&
72
+ width >= HEADER_LOGO_MIN_WIDTH);
73
+ }
74
+ setSweep(sweep) {
75
+ if (sweep === this.sweep)
76
+ return;
77
+ this.sweep = sweep;
78
+ this.cache = null;
79
+ }
52
80
  title() {
53
81
  const t = this.theme;
54
82
  return `${t.fg("accent", t.glyphs.thinking)} ${t.bold(`ama ${this.info.version}`)}`;
@@ -82,11 +110,15 @@ export class StartupHeader {
82
110
  render(width) {
83
111
  if (this.cache?.width === width)
84
112
  return this.cache.lines;
85
- const lines = this.options.level === "header"
86
- ? [this.headerLine(width)]
87
- : this.options.compact === true || width < HEADER_BOX_MIN_WIDTH
88
- ? this.plainLines(width)
89
- : this.boxLines(width);
113
+ let lines;
114
+ if (this.options.level === "header")
115
+ lines = [this.headerLine(width)];
116
+ else if (width < HEADER_LOGO_MIN_WIDTH)
117
+ lines = this.narrowLines(width);
118
+ else if (!this.hasLogo(width))
119
+ lines = this.infoLines(width, this.title());
120
+ else
121
+ lines = this.logoLines(width);
90
122
  this.cache = { width, lines };
91
123
  return lines;
92
124
  }
@@ -101,66 +133,68 @@ export class StartupHeader {
101
133
  ];
102
134
  return truncateToWidth(parts.join(sep), width);
103
135
  }
104
- plainLines(width) {
136
+ /** < 48 列:两行(版本 · 模型 · 思考 / 模式 · 目录 · 信任),有警告再加一行。 */
137
+ narrowLines(width) {
105
138
  const t = this.theme;
106
139
  const sep = t.fg("dim", " · ");
107
- const ellipsis = t.glyphs.ellipsis;
108
140
  const { info } = this;
109
- const lines = [this.title()];
110
- lines.push(t.fg("accent", withoutChannel(info.model)) + sep + info.thinking);
141
+ const lines = [
142
+ [this.title(), t.fg("accent", withoutChannel(info.model)), info.thinking].join(sep),
143
+ ];
144
+ const mode = this.modeText();
111
145
  const trust = this.trustText(false);
112
- const room = Math.max(4, width - visibleWidth(trust) - 3);
113
- lines.push(truncateLeft(info.cwd, room, ellipsis) + sep + trust);
114
- const mode = [this.modeText(), info.preset, this.codemodeText()].filter((p) => p !== undefined);
115
- lines.push(mode.join(sep));
116
- const loaded = this.loadedText();
117
- if (loaded !== undefined)
118
- lines.push(loaded);
119
- const m = msg().interactive.startup.header;
120
- if (info.host !== undefined)
121
- lines.push(m.host(info.host));
122
- if (info.warnings > 0)
123
- lines.push(t.fg("warning", m.warnings(info.warnings)));
124
- lines.push(t.fg("dim", m.hintCompact));
146
+ const room = Math.max(4, width - visibleWidth(mode) - visibleWidth(trust) - 6);
147
+ lines.push([mode, truncateLeft(info.cwd, room, t.glyphs.ellipsis), trust].join(sep));
148
+ if (info.warnings > 0) {
149
+ lines.push(t.fg("warning", msg().interactive.startup.header.warnings(info.warnings)));
150
+ }
125
151
  return lines.map((line) => truncateToWidth(line, width));
126
152
  }
127
- boxLines(width) {
153
+ /** 信息列:首行 `title`,其后模型、目录、模式、可选行、按键提示;每行截到 `width`。 */
154
+ infoLines(width, title) {
128
155
  const t = this.theme;
129
156
  const sep = t.fg("dim", " · ");
130
157
  const { info } = this;
131
158
  const m = msg().interactive.startup.header;
132
- const boxWidth = Math.min(width, HEADER_BOX_MAX_WIDTH);
133
- // 键列至少 6 列(「已加载」的宽度),没有这一行时也对齐;英文键更宽时按最宽的键
134
- const keyWidth = Math.max(6, ...[m.keyModel, m.keyDir, m.keyMode, m.keyLoaded, m.keyHost, m.keyWarnings].map((k) => visibleWidth(k)));
135
- /** 框内文本宽:边框 2 + 内边距 2;键列 + 间隔 1。 */
136
- const valueWidth = boxWidth - 4 - keyWidth - 1;
137
159
  const trust = this.trustText(true);
138
- const cwdRoom = Math.max(4, valueWidth - visibleWidth(trust) - 3);
139
- const rows = [
140
- { key: m.keyModel, value: t.fg("accent", info.model) + sep + m.thinking(info.thinking) },
141
- { key: m.keyDir, value: truncateLeft(info.cwd, cwdRoom, t.glyphs.ellipsis) + sep + trust },
142
- {
143
- key: m.keyMode,
144
- value: [this.modeText(), m.preset(info.preset), this.codemodeText()]
145
- .filter((p) => p !== undefined)
146
- .join(sep),
147
- },
160
+ const cwdRoom = Math.max(4, width - visibleWidth(trust) - 3);
161
+ const lines = [
162
+ title,
163
+ t.fg("accent", info.model) + sep + m.thinking(info.thinking),
164
+ truncateLeft(info.cwd, cwdRoom, t.glyphs.ellipsis) + sep + trust,
165
+ [this.modeText(), m.preset(info.preset), this.codemodeText()]
166
+ .filter((p) => p !== undefined)
167
+ .join(sep),
148
168
  ];
149
169
  const loaded = this.loadedText();
150
170
  if (loaded !== undefined)
151
- rows.push({ key: m.keyLoaded, value: loaded });
171
+ lines.push(loaded);
152
172
  if (info.host !== undefined)
153
- rows.push({ key: m.keyHost, value: info.host });
154
- if (info.warnings > 0) {
155
- rows.push({ key: m.keyWarnings, value: t.fg("warning", m.warningsValue(info.warnings)) });
173
+ lines.push(m.host(info.host));
174
+ if (info.warnings > 0)
175
+ lines.push(t.fg("warning", m.warnings(info.warnings)));
176
+ lines.push(t.fg("dim", fitSegments(m.hint, width)));
177
+ return lines.map((line) => truncateToWidth(line, width));
178
+ }
179
+ /** 字符画 + 信息列:够宽并排(顶端对齐),否则上下叠放。 */
180
+ logoLines(width) {
181
+ const t = this.theme;
182
+ const logo = paintLogo(t, this.sweep);
183
+ const logoCols = logoWidth(logoRows(t));
184
+ const title = `${t.bold("ama")} ${t.fg("dim", this.info.version)}`;
185
+ if (width < HEADER_SIDE_MIN_WIDTH) {
186
+ return [...logo, "", ...this.infoLines(width, title)];
187
+ }
188
+ const info = this.infoLines(width - logoCols - GAP, title);
189
+ const rows = Math.max(logo.length, info.length);
190
+ const blank = " ".repeat(logoCols);
191
+ const gap = " ".repeat(GAP);
192
+ const lines = [];
193
+ for (let i = 0; i < rows; i++) {
194
+ const right = info[i];
195
+ lines.push(right === undefined ? (logo[i] ?? "") : (logo[i] ?? blank) + gap + right);
156
196
  }
157
- const padded = rows.map((row) => ({ ...row, key: padToWidth(row.key, keyWidth) }));
158
- const kv = new KeyValue(padded, { theme: t, gap: 1, maxKeyRatio: 1 });
159
- const body = {
160
- render: (inner) => [this.title(), "", ...kv.render(inner), "", t.fg("dim", m.hint)],
161
- invalidate: () => kv.invalidate(),
162
- };
163
- return new Box(body, { theme: t }).render(boxWidth);
197
+ return lines;
164
198
  }
165
199
  invalidate() {
166
200
  this.cache = null;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * 启动头的「AMA」字符画与启动动画(终端界面视觉设计 v1 §3.1.1)。
3
+ *
4
+ * - 字形:缺省 5 行实心块字(`█▀▄`,宽 25);ASCII 回退(`ui.ascii` / `AMA_ASCII=1`)用 figlet 风格的
5
+ * `_ / \ |`(同为 5 行、宽 25)。
6
+ * - 着色:按列三段渐变 accent → user → tool(主题已有颜色,深浅主题 / 16 色都有对应值);无色主题原样输出。
7
+ * - 动画(点亮扫描):未扫到的列 dim,扫描头 3 列 text 粗体高亮,扫过的列定格为渐变色;每帧前进 1 列,
8
+ * 约 30 帧 × 32 ms ≈ 1 s,结束停在定格帧。动画只重画启动头所在行(差分渲染器原地改写),所以只在
9
+ * 整屏内容装得下视口时播放;任何时候 `finish()` 立即定格。
10
+ */
11
+ import type { Theme, TUI } from "../../tui.js";
12
+ /** 缺省字形(实心块字)。 */
13
+ export declare const LOGO_BLOCK: readonly string[];
14
+ /** 纯 ASCII 字形(只用 `_ / \ |`)。 */
15
+ export declare const LOGO_ASCII: readonly string[];
16
+ /** 扫描头的宽度(列)。 */
17
+ export declare const SWEEP_BAND = 3;
18
+ /** 帧间隔(ms);帧数 = 字形宽 + 扫描头宽,总时长约 1 s(上限 1.2 s)。 */
19
+ export declare const FRAME_MS = 32;
20
+ export declare function logoRows(theme: Theme): readonly string[];
21
+ /** 字形的列宽(逐字符都是 1 列)。 */
22
+ export declare function logoWidth(rows: readonly string[]): number;
23
+ /** 动画总帧数(不含定格帧)。 */
24
+ export declare function sweepFrames(theme: Theme): number;
25
+ /** 画字形:同样式的连续列合成一段,空格不着色;返回的每行都补足到字形宽。 */
26
+ export declare function paintLogo(theme: Theme, sweep?: number): string[];
27
+ /** 动画画到的目标(启动头)。 */
28
+ export interface SweepTarget {
29
+ setSweep(sweep: number | undefined): void;
30
+ }
31
+ export interface LogoAnimationOptions {
32
+ frames: number;
33
+ intervalMs?: number;
34
+ /** 每帧之后请求重绘。 */
35
+ render(): void;
36
+ /** 每帧之前检查:返回 false(内容已超出视口等)立即定格。 */
37
+ canContinue?(): boolean;
38
+ /** 定格之后(走完或提前结束)调用一次。 */
39
+ onDone?(): void;
40
+ }
41
+ /** 一次性的点亮扫描:`start()` 从第 0 帧开始,走完或 `finish()` 后定格,不会再动。 */
42
+ export declare class LogoAnimation {
43
+ private readonly target;
44
+ private readonly options;
45
+ private frame;
46
+ private timer;
47
+ private done;
48
+ constructor(target: SweepTarget, options: LogoAnimationOptions);
49
+ get running(): boolean;
50
+ start(): void;
51
+ private step;
52
+ /** 立即定格(按键、退出、走完);重复调用无副作用。 */
53
+ finish(): void;
54
+ }
55
+ /** 是否播放启动动画的输入(interactive-mode 汇总)。 */
56
+ export interface AnimationGate {
57
+ /** `ui.animation`。 */
58
+ animation: boolean | undefined;
59
+ /** 启动头确实画了 logo(normal 档、`ui.logo` 非 off、非 compact、宽度够)。 */
60
+ hasLogo: boolean;
61
+ colors: number;
62
+ io: {
63
+ stdoutIsTTY: boolean;
64
+ };
65
+ /** 嵌入宿主(profile.host)。 */
66
+ host: boolean;
67
+ /** 启动即提交的提示(`ama "..."`);非空不播放。 */
68
+ prompt: string | undefined;
69
+ env: NodeJS.ProcessEnv;
70
+ rows: number;
71
+ }
72
+ /** 终端低于这个行数不播放(启动头 + 输入框 + 状态栏放不下时差分渲染会把动画帧滚进回滚区)。 */
73
+ export declare const MIN_ANIMATION_ROWS = 16;
74
+ export declare function shouldAnimate(gate: AnimationGate): boolean;
75
+ /** 能播放动画的启动头。 */
76
+ export interface AnimatedHeader extends SweepTarget {
77
+ hasLogo(width: number): boolean;
78
+ }
79
+ /**
80
+ * 界面启动后调用:条件满足就播放点亮扫描并返回动画(退出时 `finish()`),否则返回 undefined、启动头
81
+ * 直接是定格帧。按任意键立即定格——监听器不吞键,按键照常进输入框。
82
+ */
83
+ export declare function playStartupAnimation(tui: TUI, header: AnimatedHeader | undefined, theme: Theme, conditions: Omit<AnimationGate, "hasLogo" | "colors" | "rows">): LogoAnimation | undefined;