@armadra/agent 0.6.1 → 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 (152) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/CHANGELOG.zh-CN.md +56 -0
  3. package/README.md +168 -575
  4. package/README.zh-CN.md +165 -596
  5. package/dist/agent/session-settings.d.ts +4 -1
  6. package/dist/agent/session-settings.js +7 -2
  7. package/dist/agent/session.d.ts +6 -0
  8. package/dist/agent/session.js +8 -0
  9. package/dist/agent/subagent-background.d.ts +75 -0
  10. package/dist/agent/subagent-background.js +209 -0
  11. package/dist/agent/subagent-registry.d.ts +24 -5
  12. package/dist/agent/subagent-registry.js +64 -92
  13. package/dist/agent/types-w5.d.ts +11 -1
  14. package/dist/agent/types.d.ts +5 -0
  15. package/dist/agents/builtin.js +0 -1
  16. package/dist/agents/external.js +0 -1
  17. package/dist/agents/parse.js +4 -3
  18. package/dist/agents/result.d.ts +7 -1
  19. package/dist/agents/result.js +20 -1
  20. package/dist/agents/task-control.d.ts +13 -2
  21. package/dist/agents/task-record.d.ts +13 -1
  22. package/dist/agents/task-record.js +32 -0
  23. package/dist/agents/types.d.ts +5 -1
  24. package/dist/ai/apis/chatgpt-backend.d.ts +7 -2
  25. package/dist/ai/apis/chatgpt-backend.js +12 -4
  26. package/dist/ai/providers/discovered-cache.d.ts +29 -12
  27. package/dist/ai/providers/discovered-cache.js +72 -12
  28. package/dist/ai/providers/registry.js +7 -6
  29. package/dist/ai/types.d.ts +5 -0
  30. package/dist/auth/chatgpt/backend-client.d.ts +14 -4
  31. package/dist/auth/chatgpt/backend-client.js +41 -8
  32. package/dist/auth/chatgpt/cli.d.ts +2 -1
  33. package/dist/auth/chatgpt/cli.js +14 -7
  34. package/dist/auth/chatgpt/follow.d.ts +16 -0
  35. package/dist/auth/chatgpt/follow.js +51 -0
  36. package/dist/auth/chatgpt/presets.d.ts +8 -0
  37. package/dist/auth/chatgpt/presets.js +12 -0
  38. package/dist/auth/testing/fake-oauth.d.ts +4 -1
  39. package/dist/auth/testing/fake-oauth.js +24 -1
  40. package/dist/bundle/ama.cjs +2016 -664
  41. package/dist/cli/compose-agents.d.ts +2 -1
  42. package/dist/cli/compose-agents.js +11 -2
  43. package/dist/cli/compose-providers.d.ts +2 -1
  44. package/dist/cli/compose-providers.js +6 -3
  45. package/dist/cli/startup-steps.js +4 -1
  46. package/dist/cli/subcommands/models-discover.d.ts +5 -2
  47. package/dist/cli/subcommands/models-discover.js +52 -10
  48. package/dist/config/json-schema.js +10 -2
  49. package/dist/config/key-docs.js +9 -3
  50. package/dist/config/merge.d.ts +1 -1
  51. package/dist/config/merge.js +20 -3
  52. package/dist/config/schema-w5.js +4 -2
  53. package/dist/config/schema-w6.d.ts +1 -1
  54. package/dist/config/schema-w6.js +2 -1
  55. package/dist/config/schema.js +2 -0
  56. package/dist/config/settings-registry.js +4 -0
  57. package/dist/config/types-w5.d.ts +10 -0
  58. package/dist/config/types-w5.js +2 -0
  59. package/dist/config/types-w6.d.ts +5 -0
  60. package/dist/config/types.d.ts +3 -1
  61. package/dist/git/info.d.ts +19 -0
  62. package/dist/git/info.js +64 -8
  63. package/dist/i18n/catalog.d.ts +52 -8
  64. package/dist/i18n/messages/agents.d.ts +56 -0
  65. package/dist/i18n/messages/agents.js +58 -2
  66. package/dist/i18n/messages/auth.d.ts +12 -0
  67. package/dist/i18n/messages/auth.js +20 -0
  68. package/dist/i18n/messages/config-keys.d.ts +8 -0
  69. package/dist/i18n/messages/config-keys.js +18 -10
  70. package/dist/i18n/messages/config.d.ts +8 -0
  71. package/dist/i18n/messages/interactive-startup.d.ts +0 -16
  72. package/dist/i18n/messages/interactive-startup.js +0 -16
  73. package/dist/i18n/messages/interactive.d.ts +23 -16
  74. package/dist/i18n/messages/interactive.js +25 -0
  75. package/dist/i18n/messages/print.d.ts +4 -0
  76. package/dist/i18n/messages/print.js +4 -0
  77. package/dist/i18n/messages/report.js +4 -4
  78. package/dist/i18n/messages/settings.d.ts +6 -0
  79. package/dist/i18n/messages/settings.js +6 -0
  80. package/dist/i18n/messages/subcommands-config.d.ts +4 -0
  81. package/dist/i18n/messages/subcommands-config.js +4 -0
  82. package/dist/i18n/messages/subcommands.d.ts +4 -0
  83. package/dist/index.d.ts +1 -0
  84. package/dist/modes/commands-core.js +8 -4
  85. package/dist/modes/interactive/agent-bar.d.ts +3 -1
  86. package/dist/modes/interactive/agent-bar.js +12 -5
  87. package/dist/modes/interactive/agent-ui.d.ts +21 -3
  88. package/dist/modes/interactive/agent-ui.js +81 -12
  89. package/dist/modes/interactive/agent-view.d.ts +4 -0
  90. package/dist/modes/interactive/agent-view.js +14 -1
  91. package/dist/modes/interactive/approval-dock.d.ts +51 -0
  92. package/dist/modes/interactive/approval-dock.js +112 -0
  93. package/dist/modes/interactive/approval-ui.d.ts +43 -0
  94. package/dist/modes/interactive/approval-ui.js +64 -0
  95. package/dist/modes/interactive/commands.js +4 -1
  96. package/dist/modes/interactive/event-notices.d.ts +6 -1
  97. package/dist/modes/interactive/event-notices.js +7 -1
  98. package/dist/modes/interactive/interactive-mode.d.ts +4 -2
  99. package/dist/modes/interactive/interactive-mode.js +33 -38
  100. package/dist/modes/interactive/key-dispatch.d.ts +21 -4
  101. package/dist/modes/interactive/key-dispatch.js +60 -8
  102. package/dist/modes/interactive/line/line-mode.js +5 -0
  103. package/dist/modes/interactive/run-indicator.d.ts +15 -0
  104. package/dist/modes/interactive/run-indicator.js +37 -5
  105. package/dist/modes/interactive/session-events.js +5 -1
  106. package/dist/modes/interactive/startup-header.d.ts +29 -14
  107. package/dist/modes/interactive/startup-header.js +93 -59
  108. package/dist/modes/interactive/startup-logo.d.ts +83 -0
  109. package/dist/modes/interactive/startup-logo.js +183 -0
  110. package/dist/modes/interactive/status-area.d.ts +18 -0
  111. package/dist/modes/interactive/status-area.js +67 -1
  112. package/dist/modes/interactive/status-bar.d.ts +9 -1
  113. package/dist/modes/interactive/status-bar.js +43 -11
  114. package/dist/modes/interactive/status-line.d.ts +2 -0
  115. package/dist/modes/interactive/status-line.js +11 -6
  116. package/dist/modes/interactive/status-quota.d.ts +44 -0
  117. package/dist/modes/interactive/status-quota.js +135 -0
  118. package/dist/modes/interactive/subagent-view.d.ts +1 -0
  119. package/dist/modes/interactive/subagent-view.js +8 -0
  120. package/dist/modes/interactive/task-background.d.ts +31 -0
  121. package/dist/modes/interactive/task-background.js +68 -0
  122. package/dist/modes/interactive/tool-view.d.ts +7 -1
  123. package/dist/modes/interactive/tool-view.js +24 -1
  124. package/dist/modes/print/print-mode.d.ts +11 -0
  125. package/dist/modes/print/print-mode.js +36 -1
  126. package/dist/modes/rpc/commands.d.ts +2 -1
  127. package/dist/modes/rpc/commands.js +9 -1
  128. package/dist/rpc.d.ts +13 -0
  129. package/dist/rpc.js +3 -0
  130. package/dist/tools/task-ctl.d.ts +2 -0
  131. package/dist/tools/task-ctl.js +7 -2
  132. package/dist/tools/task.d.ts +11 -0
  133. package/dist/tools/task.js +20 -2
  134. package/dist/tui/components/editor.d.ts +2 -0
  135. package/dist/tui/components/editor.js +4 -0
  136. package/dist/tui/components/loader.d.ts +5 -1
  137. package/dist/tui/components/loader.js +18 -5
  138. package/dist/tui/keybindings.d.ts +8 -3
  139. package/dist/tui/keybindings.js +8 -3
  140. package/docs/agents.md +52 -28
  141. package/docs/en/host-api.md +5 -1
  142. package/docs/en/providers.md +8 -4
  143. package/docs/en/rpc.md +19 -7
  144. package/docs/en/sessions.md +3 -1
  145. package/docs/en/tui.md +84 -66
  146. package/docs/host-api.md +5 -1
  147. package/docs/providers.md +29 -12
  148. package/docs/rpc.md +19 -7
  149. package/docs/sessions.md +2 -1
  150. package/docs/tui-design.md +42 -29
  151. package/docs/tui.md +67 -49
  152. package/package.json +1 -1
@@ -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
+ }
@@ -8,6 +8,9 @@
8
8
  * session_rewound、/tree 之后;结果变化时重画。
9
9
  * - [W5-U] `model_fallback` 之后状态栏模型项显示 `主模型 → 回退模型`,切回主模型(`model_changed`)后消失。
10
10
  * - `telemetry_tick`(流式中 ≤ 2 Hz)只重取统计并重画(速率行),状态栏的内容这时不变。
11
+ * - [W6] 订阅配额(status-quota.ts):记住最近一次 `quota_update`(账户级,换会话不清;换到别的供应商不显示),
12
+ * full 时在状态栏下方占第三行、compact 时在行尾追加短项;第三行显示中每分钟重画一次(重置倒计时),
13
+ * 不显示时不挂计时器。Ctrl+G 折叠时第三行与速率行一起收起。
11
14
  */
12
15
  import type { AgentSession, SessionEvent } from "../../agent/types.js";
13
16
  import type { Runtime } from "../../cli/runtime.js";
@@ -15,6 +18,9 @@ import type { StatusLineMode } from "../../config/types.js";
15
18
  import { type Component, type Theme } from "../../tui.js";
16
19
  import { StatusBar } from "./status-bar.js";
17
20
  import { StatusLine } from "./status-line.js";
21
+ import { QuotaLine, type QuotaView } from "./status-quota.js";
22
+ /** 配额行重画间隔(倒计时只到分钟)。 */
23
+ export declare const QUOTA_TICK_MS = 60000;
18
24
  /** 一行提示;空时不占行。 */
19
25
  export declare class HintLine implements Component {
20
26
  private text;
@@ -42,6 +48,10 @@ export declare class StatusArea {
42
48
  readonly hint: HintLine;
43
49
  readonly bar: StatusBar;
44
50
  readonly rate: StatusLine;
51
+ /** [W6] 订阅配额行(full 第三行)。 */
52
+ readonly quota: QuotaLine;
53
+ private lastQuota;
54
+ private ticker;
45
55
  private mode;
46
56
  private startedAt;
47
57
  private git;
@@ -52,6 +62,13 @@ export declare class StatusArea {
52
62
  private fallback;
53
63
  private bashSandbox;
54
64
  constructor(deps: StatusAreaDeps);
65
+ /** [W6] 当前模型走 ChatGPT 订阅时的配额;codex 还没数据为 pending,SIWC 没数据或非订阅模型不显示。 */
66
+ quotaView(): QuotaView;
67
+ private backendMemo;
68
+ /** 模型的 ChatGPT 订阅后端(渠道 compat `chatgptBackend`);按引用记住上次结果。 */
69
+ private backendOf;
70
+ /** 第三行显示且有重置时间时每分钟重画;否则不挂计时器。 */
71
+ private syncTicker;
55
72
  layout(): StatusLineMode;
56
73
  /** [W6-A] 界面配置(Agent 栏开关、子 Agent 视图的消息显示选项)。 */
57
74
  ui(): NonNullable<Runtime["config"]["ui"]>;
@@ -64,6 +81,7 @@ export declare class StatusArea {
64
81
  /** 返回 true:事件已处理完(只需重画),调用方不再分派。 */
65
82
  onEvent(event: SessionEvent): boolean;
66
83
  dispose(): void;
84
+ private disposeGit;
67
85
  private gitView;
68
86
  private watchGit;
69
87
  }
@@ -8,6 +8,9 @@
8
8
  * session_rewound、/tree 之后;结果变化时重画。
9
9
  * - [W5-U] `model_fallback` 之后状态栏模型项显示 `主模型 → 回退模型`,切回主模型(`model_changed`)后消失。
10
10
  * - `telemetry_tick`(流式中 ≤ 2 Hz)只重取统计并重画(速率行),状态栏的内容这时不变。
11
+ * - [W6] 订阅配额(status-quota.ts):记住最近一次 `quota_update`(账户级,换会话不清;换到别的供应商不显示),
12
+ * full 时在状态栏下方占第三行、compact 时在行尾追加短项;第三行显示中每分钟重画一次(重置倒计时),
13
+ * 不显示时不挂计时器。Ctrl+G 折叠时第三行与速率行一起收起。
11
14
  */
12
15
  import { msg } from "../../i18n/index.js";
13
16
  import { basename } from "node:path";
@@ -20,8 +23,11 @@ import { effectiveCodemodeMode } from "../../tools/presets.js";
20
23
  import { truncateToWidth } from "../../tui.js";
21
24
  import { StatusBar } from "./status-bar.js";
22
25
  import { StatusLine } from "./status-line.js";
26
+ import { QuotaLine } from "./status-quota.js";
23
27
  /** 结束后可能改了工作区的工具。 */
24
28
  const WRITE_TOOLS = new Set(["write", "edit", "bash", "task"]);
29
+ /** 配额行重画间隔(倒计时只到分钟)。 */
30
+ export const QUOTA_TICK_MS = 60_000;
25
31
  /** 一行提示;空时不占行。 */
26
32
  export class HintLine {
27
33
  text = "";
@@ -38,6 +44,10 @@ export class StatusArea {
38
44
  hint = new HintLine();
39
45
  bar;
40
46
  rate;
47
+ /** [W6] 订阅配额行(full 第三行)。 */
48
+ quota;
49
+ lastQuota;
50
+ ticker;
41
51
  mode;
42
52
  startedAt;
43
53
  git;
@@ -79,10 +89,53 @@ export class StatusArea {
79
89
  .session()
80
90
  .getTools()
81
91
  .some((tool) => tool.name === "bash"),
92
+ quota: () => this.quotaView(),
82
93
  };
83
94
  this.bar = new StatusBar(source, deps.theme);
84
95
  this.rate = new StatusLine(this.bar, source, deps.theme);
96
+ this.quota = new QuotaLine({ layout: () => this.mode, quota: () => this.quotaView(), now: () => deps.now() }, deps.theme);
85
97
  this.watchGit();
98
+ this.syncTicker();
99
+ }
100
+ /** [W6] 当前模型走 ChatGPT 订阅时的配额;codex 还没数据为 pending,SIWC 没数据或非订阅模型不显示。 */
101
+ quotaView() {
102
+ const model = this.deps.session().state.model;
103
+ if (model === undefined)
104
+ return undefined;
105
+ const backend = this.backendOf(formatModelRef(model));
106
+ if (backend === undefined)
107
+ return undefined;
108
+ const quota = this.lastQuota ?? this.deps.session().getStats().subscription?.quota;
109
+ if (quota !== undefined && quota.provider === model.provider)
110
+ return { quota };
111
+ return backend === "codex" ? "pending" : undefined;
112
+ }
113
+ backendMemo;
114
+ /** 模型的 ChatGPT 订阅后端(渠道 compat `chatgptBackend`);按引用记住上次结果。 */
115
+ backendOf(ref) {
116
+ if (this.backendMemo?.ref === ref)
117
+ return this.backendMemo.backend;
118
+ const found = this.deps.runtime.providers.findModel(ref);
119
+ const value = found.ok
120
+ ? found.model.compat?.chatgptBackend
121
+ : undefined;
122
+ const backend = value === "codex" || value === "siwc" ? value : undefined;
123
+ this.backendMemo = { ref, backend };
124
+ return backend;
125
+ }
126
+ /** 第三行显示且有重置时间时每分钟重画;否则不挂计时器。 */
127
+ syncTicker() {
128
+ const view = this.mode === "full" ? this.quotaView() : undefined;
129
+ const ticking = typeof view === "object" &&
130
+ (view.quota.primary?.resetsAt !== undefined || view.quota.secondary?.resetsAt !== undefined);
131
+ if (ticking && this.ticker === undefined) {
132
+ this.ticker = setInterval(() => this.deps.render(), QUOTA_TICK_MS);
133
+ this.ticker.unref?.();
134
+ }
135
+ else if (!ticking && this.ticker !== undefined) {
136
+ clearInterval(this.ticker);
137
+ this.ticker = undefined;
138
+ }
86
139
  }
87
140
  layout() {
88
141
  return this.mode;
@@ -95,6 +148,7 @@ export class StatusArea {
95
148
  if (mode === this.mode)
96
149
  return;
97
150
  this.mode = mode;
151
+ this.syncTicker();
98
152
  (this.deps.redraw ?? this.deps.render)();
99
153
  }
100
154
  toggle() {
@@ -110,6 +164,7 @@ export class StatusArea {
110
164
  this.fallback = undefined;
111
165
  this.watchGit();
112
166
  this.bar.refresh();
167
+ this.syncTicker();
113
168
  }
114
169
  refreshGit() {
115
170
  void this.git?.refresh();
@@ -120,6 +175,10 @@ export class StatusArea {
120
175
  case "telemetry_tick":
121
176
  this.bar.refresh();
122
177
  return true;
178
+ case "quota_update":
179
+ this.lastQuota = event;
180
+ this.syncTicker();
181
+ return true;
123
182
  case "agent_settled":
124
183
  case "session_rewound":
125
184
  this.refreshGit();
@@ -141,6 +200,7 @@ export class StatusArea {
141
200
  if (formatModelRef(model) === this.fallback.from)
142
201
  this.fallback = undefined;
143
202
  }
203
+ this.syncTicker();
144
204
  return false;
145
205
  }
146
206
  default:
@@ -148,6 +208,12 @@ export class StatusArea {
148
208
  }
149
209
  }
150
210
  dispose() {
211
+ this.disposeGit();
212
+ if (this.ticker !== undefined)
213
+ clearInterval(this.ticker);
214
+ this.ticker = undefined;
215
+ }
216
+ disposeGit() {
151
217
  this.offGit();
152
218
  this.git?.dispose();
153
219
  this.git = undefined;
@@ -162,7 +228,7 @@ export class StatusArea {
162
228
  const cwd = this.deps.session().state.cwd;
163
229
  if (cwd === this.gitCwd && this.git !== undefined)
164
230
  return;
165
- this.dispose();
231
+ this.disposeGit();
166
232
  this.gitCwd = cwd;
167
233
  const git = new GitInfoWatcher(cwd, this.deps.env !== undefined ? { env: this.deps.env } : {});
168
234
  this.git = git;