@armadra/agent 0.6.2 → 0.6.3

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 (130) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/CHANGELOG.zh-CN.md +44 -0
  3. package/README.md +168 -575
  4. package/README.zh-CN.md +165 -596
  5. package/dist/agent/session.d.ts +6 -0
  6. package/dist/agent/session.js +8 -0
  7. package/dist/agent/subagent-background.d.ts +75 -0
  8. package/dist/agent/subagent-background.js +209 -0
  9. package/dist/agent/subagent-registry.d.ts +24 -5
  10. package/dist/agent/subagent-registry.js +64 -92
  11. package/dist/agent/types-w5.d.ts +11 -1
  12. package/dist/agent/types.d.ts +5 -0
  13. package/dist/agents/builtin.js +0 -1
  14. package/dist/agents/external.js +0 -1
  15. package/dist/agents/parse.js +4 -3
  16. package/dist/agents/result.d.ts +7 -1
  17. package/dist/agents/result.js +20 -1
  18. package/dist/agents/task-control.d.ts +13 -2
  19. package/dist/agents/task-record.d.ts +13 -1
  20. package/dist/agents/task-record.js +32 -0
  21. package/dist/agents/types.d.ts +5 -1
  22. package/dist/ai/providers/discovered-cache.d.ts +10 -4
  23. package/dist/ai/providers/discovered-cache.js +30 -14
  24. package/dist/auth/chatgpt/backend-client.d.ts +6 -5
  25. package/dist/auth/chatgpt/backend-client.js +8 -7
  26. package/dist/bundle/ama.cjs +1691 -500
  27. package/dist/cli/compose-agents.d.ts +2 -1
  28. package/dist/cli/compose-agents.js +11 -2
  29. package/dist/cli/subcommands/models-discover.d.ts +1 -0
  30. package/dist/cli/subcommands/models-discover.js +12 -2
  31. package/dist/config/json-schema.js +9 -2
  32. package/dist/config/key-docs.js +2 -2
  33. package/dist/config/merge.d.ts +1 -1
  34. package/dist/config/merge.js +20 -3
  35. package/dist/config/schema-w5.js +4 -2
  36. package/dist/config/schema.js +2 -0
  37. package/dist/config/settings-registry.js +4 -0
  38. package/dist/config/types-w5.d.ts +10 -0
  39. package/dist/config/types-w5.js +2 -0
  40. package/dist/config/types.d.ts +3 -1
  41. package/dist/git/info.d.ts +19 -0
  42. package/dist/git/info.js +64 -8
  43. package/dist/i18n/catalog.d.ts +45 -8
  44. package/dist/i18n/messages/agents.d.ts +56 -0
  45. package/dist/i18n/messages/agents.js +58 -2
  46. package/dist/i18n/messages/config-keys.d.ts +6 -0
  47. package/dist/i18n/messages/config-keys.js +16 -10
  48. package/dist/i18n/messages/config.d.ts +6 -0
  49. package/dist/i18n/messages/interactive-startup.d.ts +0 -16
  50. package/dist/i18n/messages/interactive-startup.js +0 -16
  51. package/dist/i18n/messages/interactive.d.ts +23 -16
  52. package/dist/i18n/messages/interactive.js +25 -0
  53. package/dist/i18n/messages/print.d.ts +4 -0
  54. package/dist/i18n/messages/print.js +4 -0
  55. package/dist/i18n/messages/report.js +4 -4
  56. package/dist/i18n/messages/settings.d.ts +6 -0
  57. package/dist/i18n/messages/settings.js +6 -0
  58. package/dist/i18n/messages/subcommands-config.d.ts +4 -0
  59. package/dist/i18n/messages/subcommands-config.js +4 -0
  60. package/dist/i18n/messages/subcommands.d.ts +4 -0
  61. package/dist/index.d.ts +1 -0
  62. package/dist/modes/commands-core.js +8 -4
  63. package/dist/modes/interactive/agent-bar.d.ts +3 -1
  64. package/dist/modes/interactive/agent-bar.js +12 -5
  65. package/dist/modes/interactive/agent-ui.d.ts +21 -3
  66. package/dist/modes/interactive/agent-ui.js +81 -12
  67. package/dist/modes/interactive/agent-view.d.ts +4 -0
  68. package/dist/modes/interactive/agent-view.js +14 -1
  69. package/dist/modes/interactive/approval-dock.d.ts +51 -0
  70. package/dist/modes/interactive/approval-dock.js +112 -0
  71. package/dist/modes/interactive/approval-ui.d.ts +43 -0
  72. package/dist/modes/interactive/approval-ui.js +64 -0
  73. package/dist/modes/interactive/commands.js +4 -1
  74. package/dist/modes/interactive/event-notices.d.ts +6 -1
  75. package/dist/modes/interactive/event-notices.js +7 -1
  76. package/dist/modes/interactive/interactive-mode.d.ts +4 -2
  77. package/dist/modes/interactive/interactive-mode.js +33 -38
  78. package/dist/modes/interactive/key-dispatch.d.ts +21 -4
  79. package/dist/modes/interactive/key-dispatch.js +60 -8
  80. package/dist/modes/interactive/line/line-mode.js +5 -0
  81. package/dist/modes/interactive/run-indicator.d.ts +15 -0
  82. package/dist/modes/interactive/run-indicator.js +37 -5
  83. package/dist/modes/interactive/session-events.js +5 -1
  84. package/dist/modes/interactive/startup-header.d.ts +29 -14
  85. package/dist/modes/interactive/startup-header.js +93 -59
  86. package/dist/modes/interactive/startup-logo.d.ts +83 -0
  87. package/dist/modes/interactive/startup-logo.js +183 -0
  88. package/dist/modes/interactive/status-area.d.ts +18 -0
  89. package/dist/modes/interactive/status-area.js +67 -1
  90. package/dist/modes/interactive/status-bar.d.ts +9 -1
  91. package/dist/modes/interactive/status-bar.js +43 -11
  92. package/dist/modes/interactive/status-line.d.ts +2 -0
  93. package/dist/modes/interactive/status-line.js +11 -6
  94. package/dist/modes/interactive/status-quota.d.ts +44 -0
  95. package/dist/modes/interactive/status-quota.js +135 -0
  96. package/dist/modes/interactive/subagent-view.d.ts +1 -0
  97. package/dist/modes/interactive/subagent-view.js +8 -0
  98. package/dist/modes/interactive/task-background.d.ts +31 -0
  99. package/dist/modes/interactive/task-background.js +68 -0
  100. package/dist/modes/interactive/tool-view.d.ts +7 -1
  101. package/dist/modes/interactive/tool-view.js +24 -1
  102. package/dist/modes/print/print-mode.d.ts +11 -0
  103. package/dist/modes/print/print-mode.js +36 -1
  104. package/dist/modes/rpc/commands.d.ts +2 -1
  105. package/dist/modes/rpc/commands.js +9 -1
  106. package/dist/rpc.d.ts +13 -0
  107. package/dist/rpc.js +3 -0
  108. package/dist/tools/task-ctl.d.ts +2 -0
  109. package/dist/tools/task-ctl.js +7 -2
  110. package/dist/tools/task.d.ts +11 -0
  111. package/dist/tools/task.js +20 -2
  112. package/dist/tui/components/editor.d.ts +2 -0
  113. package/dist/tui/components/editor.js +4 -0
  114. package/dist/tui/components/loader.d.ts +5 -1
  115. package/dist/tui/components/loader.js +18 -5
  116. package/dist/tui/keybindings.d.ts +8 -3
  117. package/dist/tui/keybindings.js +8 -3
  118. package/docs/agents.md +52 -28
  119. package/docs/en/host-api.md +5 -1
  120. package/docs/en/providers.md +1 -1
  121. package/docs/en/rpc.md +19 -7
  122. package/docs/en/sessions.md +3 -1
  123. package/docs/en/tui.md +84 -66
  124. package/docs/host-api.md +5 -1
  125. package/docs/providers.md +4 -2
  126. package/docs/rpc.md +19 -7
  127. package/docs/sessions.md +2 -1
  128. package/docs/tui-design.md +42 -29
  129. package/docs/tui.md +67 -49
  130. package/package.json +1 -1
@@ -9,6 +9,10 @@
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 栏`,窄屏从后往前丢。
12
16
  *
13
17
  * 排队消息:缩进 2 列(挂在当前回合下),`↳ 插话 / 之后 文本` 整体 muted、标签 dim;超过 3 条首行
14
18
  * `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`。
@@ -48,8 +52,32 @@ export class RunIndicator {
48
52
  approval = false;
49
53
  retry;
50
54
  stream;
55
+ /** [W7-A] 有该显示的子 Agent 任务(interactive-mode 在 AgentUi 建好后接上)。 */
56
+ agents = () => false;
57
+ /** [W7-C] 有阻塞中的前台任务(Ctrl+B 可转后台)。 */
58
+ background = () => false;
59
+ /** [W7-C] 有停靠在 Agent 栏的审批。 */
60
+ docked = () => false;
61
+ hintKey = "";
51
62
  constructor(deps) {
52
63
  this.deps = deps;
64
+ deps.loader.onFrame(() => {
65
+ if (this.busy && !this.approval && this.optionalHints().join("\n") !== this.hintKey)
66
+ this.applyVerb();
67
+ });
68
+ }
69
+ /** 可丢弃的按键提示(按丢弃的逆序排列)。 */
70
+ optionalHints() {
71
+ const down = this.deps.theme.glyphs.arrowDown;
72
+ const key = this.deps.backgroundKey;
73
+ const out = [];
74
+ if (key !== undefined && this.background())
75
+ out.push(msg().agents.background.runHint(key));
76
+ if (this.docked())
77
+ out.push(msg().agents.approval.runHint(down));
78
+ else if (this.agents())
79
+ out.push(msg().agents.bar.runHint(down));
80
+ return out;
53
81
  }
54
82
  get busy() {
55
83
  return this.running || this.compacting;
@@ -133,17 +161,21 @@ export class RunIndicator {
133
161
  const m = msg().interactive.view.run;
134
162
  const esc = [m.esc];
135
163
  if (this.approval) {
164
+ this.hintKey = "";
136
165
  loader.setVerb(m.awaitingApproval);
137
166
  return;
138
167
  }
168
+ const hints = this.optionalHints();
169
+ this.hintKey = hints.join("\n");
170
+ const optional = hints.length > 0 ? { optional: hints } : {};
139
171
  const running = tools.running();
140
172
  const top = running.filter((view) => !running.some((p) => p.children.includes(view)));
141
173
  if (top.length === 1) {
142
- loader.setVerb(m.runningTool(top[0].toolName), esc);
174
+ loader.setVerb(m.runningTool(top[0].toolName), esc, optional);
143
175
  return;
144
176
  }
145
177
  if (top.length > 1) {
146
- loader.setVerb(m.runningTools(top.length), esc);
178
+ loader.setVerb(m.runningTools(top.length), esc, optional);
147
179
  return;
148
180
  }
149
181
  if (this.retry !== undefined) {
@@ -154,15 +186,15 @@ export class RunIndicator {
154
186
  return;
155
187
  }
156
188
  if (this.compacting) {
157
- loader.setVerb(m.compacting, esc);
189
+ loader.setVerb(m.compacting, esc, optional);
158
190
  return;
159
191
  }
160
192
  const stream = this.stream;
161
193
  if (stream !== undefined && !stream.thinking && stream.tokens > 0) {
162
- loader.setVerb(m.replying, [`↓≈${formatTokens(stream.tokens)}`, ...esc]);
194
+ loader.setVerb(m.replying, [`↓≈${formatTokens(stream.tokens)}`, ...esc], optional);
163
195
  return;
164
196
  }
165
- loader.setVerb(m.thinking, esc);
197
+ loader.setVerb(m.thinking, esc, optional);
166
198
  }
167
199
  }
168
200
  /** 排队消息(插话 / 之后),挂在 Loader 之上。 */
@@ -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;
@@ -0,0 +1,183 @@
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
+ /** 缺省字形(实心块字)。 */
12
+ export const LOGO_BLOCK = [
13
+ " ▄███▄ ██▄ ▄██ ▄███▄",
14
+ "██▀ ▀██ ███▄ ▄███ ██▀ ▀██",
15
+ "███████ ██ ▀█▀ ██ ███████",
16
+ "██ ██ ██ ██ ██ ██",
17
+ "▀▀ ▀▀ ▀▀ ▀▀ ▀▀ ▀▀",
18
+ ];
19
+ /** 纯 ASCII 字形(只用 `_ / \ |`)。 */
20
+ export const LOGO_ASCII = [
21
+ " _ __ __ _",
22
+ " / \\ | \\/ | / \\",
23
+ " / _ \\ | |\\/| | / _ \\",
24
+ " / ___ \\| | | |/ ___ \\",
25
+ "/_/ \\_\\_| |_/_/ \\_\\",
26
+ ];
27
+ /** 扫描头的宽度(列)。 */
28
+ export const SWEEP_BAND = 3;
29
+ /** 帧间隔(ms);帧数 = 字形宽 + 扫描头宽,总时长约 1 s(上限 1.2 s)。 */
30
+ export const FRAME_MS = 32;
31
+ const GRADIENT = ["accent", "user", "tool"];
32
+ /** 每个字母起始列(渐变按字母分段,不把一个字母劈成两色)。 */
33
+ const LETTER_STARTS = { block: [0, 8, 18], ascii: [0, 8, 16] };
34
+ export function logoRows(theme) {
35
+ return theme.glyphs.ascii ? LOGO_ASCII : LOGO_BLOCK;
36
+ }
37
+ function letterColor(col, ascii) {
38
+ const starts = LETTER_STARTS[ascii ? "ascii" : "block"];
39
+ let index = 0;
40
+ for (let i = 0; i < starts.length; i++)
41
+ if (col >= starts[i])
42
+ index = i;
43
+ return GRADIENT[index];
44
+ }
45
+ /** 字形的列宽(逐字符都是 1 列)。 */
46
+ export function logoWidth(rows) {
47
+ return Math.max(...rows.map((row) => [...row].length));
48
+ }
49
+ /** 动画总帧数(不含定格帧)。 */
50
+ export function sweepFrames(theme) {
51
+ return logoWidth(logoRows(theme)) + SWEEP_BAND;
52
+ }
53
+ /** 第 `col` 列的样式;`sweep` 为扫描头所在列,undefined 为定格。 */
54
+ function styleAt(col, ascii, sweep) {
55
+ const settled = { color: letterColor(col, ascii), bold: true };
56
+ if (sweep === undefined || col <= sweep - SWEEP_BAND)
57
+ return settled;
58
+ if (col <= sweep)
59
+ return { color: "text", bold: true };
60
+ return { color: "dim", bold: false };
61
+ }
62
+ /** 画字形:同样式的连续列合成一段,空格不着色;返回的每行都补足到字形宽。 */
63
+ export function paintLogo(theme, sweep) {
64
+ const rows = logoRows(theme);
65
+ const width = logoWidth(rows);
66
+ return rows.map((row) => {
67
+ const chars = [...row.padEnd(width)];
68
+ let out = "";
69
+ let run = "";
70
+ let runStyle;
71
+ const flush = () => {
72
+ if (run === "")
73
+ return;
74
+ if (runStyle === undefined)
75
+ out += run;
76
+ else {
77
+ const colored = theme.fg(runStyle.color, run);
78
+ out += runStyle.bold ? theme.bold(colored) : colored;
79
+ }
80
+ run = "";
81
+ };
82
+ chars.forEach((ch, col) => {
83
+ const style = ch === " " ? undefined : styleAt(col, theme.glyphs.ascii, sweep);
84
+ if (style?.color !== runStyle?.color || style?.bold !== runStyle?.bold) {
85
+ flush();
86
+ runStyle = style;
87
+ }
88
+ run += ch;
89
+ });
90
+ flush();
91
+ return out;
92
+ });
93
+ }
94
+ /** 一次性的点亮扫描:`start()` 从第 0 帧开始,走完或 `finish()` 后定格,不会再动。 */
95
+ export class LogoAnimation {
96
+ target;
97
+ options;
98
+ frame = 0;
99
+ timer;
100
+ done = false;
101
+ constructor(target, options) {
102
+ this.target = target;
103
+ this.options = options;
104
+ }
105
+ get running() {
106
+ return !this.done && this.timer !== undefined;
107
+ }
108
+ start() {
109
+ if (this.done || this.timer !== undefined)
110
+ return;
111
+ this.step();
112
+ }
113
+ step() {
114
+ this.timer = undefined;
115
+ if (this.frame >= this.options.frames || this.options.canContinue?.() === false) {
116
+ this.finish();
117
+ return;
118
+ }
119
+ this.target.setSweep(this.frame++);
120
+ this.options.render();
121
+ this.timer = setTimeout(() => this.step(), this.options.intervalMs ?? FRAME_MS);
122
+ this.timer.unref?.();
123
+ }
124
+ /** 立即定格(按键、退出、走完);重复调用无副作用。 */
125
+ finish() {
126
+ if (this.done)
127
+ return;
128
+ this.done = true;
129
+ if (this.timer !== undefined)
130
+ clearTimeout(this.timer);
131
+ this.timer = undefined;
132
+ this.target.setSweep(undefined);
133
+ this.options.render();
134
+ this.options.onDone?.();
135
+ }
136
+ }
137
+ /** 终端低于这个行数不播放(启动头 + 输入框 + 状态栏放不下时差分渲染会把动画帧滚进回滚区)。 */
138
+ export const MIN_ANIMATION_ROWS = 16;
139
+ export function shouldAnimate(gate) {
140
+ const ci = gate.env["CI"];
141
+ return (gate.animation !== false &&
142
+ gate.hasLogo &&
143
+ gate.colors > 0 &&
144
+ gate.io.stdoutIsTTY &&
145
+ !gate.host &&
146
+ (gate.prompt ?? "").trim() === "" &&
147
+ (ci === undefined || ci === "" || ci === "0" || ci === "false") &&
148
+ gate.rows >= MIN_ANIMATION_ROWS);
149
+ }
150
+ /**
151
+ * 界面启动后调用:条件满足就播放点亮扫描并返回动画(退出时 `finish()`),否则返回 undefined、启动头
152
+ * 直接是定格帧。按任意键立即定格——监听器不吞键,按键照常进输入框。
153
+ */
154
+ export function playStartupAnimation(tui, header, theme, conditions) {
155
+ const term = tui.terminal;
156
+ const columns = term.columns;
157
+ if (header === undefined)
158
+ return undefined;
159
+ const gate = {
160
+ ...conditions,
161
+ hasLogo: header.hasLogo(term.columns),
162
+ colors: theme.caps.colors,
163
+ rows: term.rows,
164
+ };
165
+ // 整屏内容装得下视口,动画帧才只在原地改写、不进回滚区
166
+ const fits = () => tui.render(term.columns).length <= term.rows;
167
+ if (!shouldAnimate(gate) || !fits())
168
+ return undefined;
169
+ let offInput = () => undefined;
170
+ const animation = new LogoAnimation(header, {
171
+ frames: sweepFrames(theme),
172
+ render: () => tui.requestRender(),
173
+ // 改了尺寸(差分渲染要整屏重画)也立即定格
174
+ canContinue: () => term.columns === columns && fits(),
175
+ onDone: () => offInput(),
176
+ });
177
+ offInput = tui.addInputListener(() => {
178
+ animation.finish();
179
+ return false;
180
+ });
181
+ animation.start();
182
+ return animation;
183
+ }