@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
@@ -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;
@@ -19,8 +19,16 @@
19
19
  * - 费用 = `stats.cost` + 外部 Agent 以美元计的用量(`stats.external`,W5-E;其它单位只在 /session 显示)。
20
20
  * - 着色:整行 dim;模式名正文色(Bypass permissions warning、Plan accent);模型 accent;ctx 按阈值
21
21
  * success / warning / error;rebill、queue warning;`net!` error。宽 ≥ 110 时 ctx 用余量表。
22
+ * [W6] full 本行按用户参考配色:思考级别 `user`(浅蓝)、目录与分支 success、短提交 dim、`↑N` 领先 `code`(橙)/
23
+ * `↓N` 落后 error、`(+a,-d)` 的 +a success / -d error、费用 warning、时长 `tool`(紫);compact 着色不变。
24
+ * - [W6] git 段在短提交后显示领先 / 落后上游 `↑N ↓N`(只 full;为 0 不显示,没有上游不显示)。
25
+ * - [W6] compact 在行尾(宿主状态之后)追加订阅配额短项 `5h 10% wk 31%`(status-quota.ts),最先丢;
26
+ * 既有记号顺序不变。full 的配额在状态栏下方单独一行。
22
27
  * - [W5-U] bash 在操作系统沙箱里跑时(S2)用量类项多一个 `沙箱`(与 codemode 同一丢弃优先级)。
23
- * - [W5-U] 模型回退中(`model_fallback`)模型项显示 `主模型 → 回退模型`,切回主模型后恢复。
28
+ * - [W5-U] 模型回退中(`model_fallback`)compact 模型项显示 `主模型 → 回退模型`,切回主模型后恢复;[W7] full
29
+ * 本行只显示主模型,`→ 回退模型` 作为开关类项在速率行左区。
30
+ * - [W7] full 左右分区:左 = 状态(权限模式、`shift+tab` 提示),右 = 模型与度量;开关类项在速率行左区,
31
+ * 配额在第三行右区(status-line.ts / status-quota.ts)。
24
32
  * - 模型名缩写:宽 < 100 去掉供应商前缀,< 60 再去掉 `@渠道`,< 48 去掉版本后缀(第一个 `-数字` 起)。
25
33
  * - ASCII:`⎇` → `git`(字形表 `branch`)、`−` → `-`、`♨` → `~`。
26
34
  * - 宽度不够时按优先级丢弃(数字大的先丢,表见 §1.2);会变的数字按最宽形状占位(`reserve`),
@@ -29,6 +37,7 @@
29
37
  import type { AgentSession, SessionCacheStats, SessionStats } from "../../agent/types.js";
30
38
  import type { StatusLineMode } from "../../config/types.js";
31
39
  import type { GitInfo } from "../../git/info.js";
40
+ import { type QuotaView } from "./status-quota.js";
32
41
  import { type Component, type Theme } from "../../tui.js";
33
42
  export interface StatusBarSource {
34
43
  session(): AgentSession;
@@ -58,6 +67,8 @@ export interface StatusBarSource {
58
67
  from: string;
59
68
  to: string;
60
69
  } | undefined;
70
+ /** [W6] 订阅配额(当前模型走 ChatGPT 订阅时);compact 行尾短项用。 */
71
+ quota?(): QuotaView;
61
72
  }
62
73
  export declare function formatTokens(count: number): string;
63
74
  export declare function formatCost(cost: number): string;
@@ -131,7 +142,7 @@ export declare class StatusBar implements Component {
131
142
  queued(): number;
132
143
  layout(): StatusLineMode;
133
144
  private parts;
134
- /** `目录 ⎇ 分支 短提交 +a −b`:三项同组、各自按优先级丢弃。 */
145
+ /** `目录 ⎇ 分支 短提交 +a −b`:三项同组、各自按优先级丢弃;full 另带 `↑N ↓N` 并按参考配色。 */
135
146
  private gitParts;
136
147
  private ctxText;
137
148
  render(width: number): string[];
@@ -19,8 +19,16 @@
19
19
  * - 费用 = `stats.cost` + 外部 Agent 以美元计的用量(`stats.external`,W5-E;其它单位只在 /session 显示)。
20
20
  * - 着色:整行 dim;模式名正文色(Bypass permissions warning、Plan accent);模型 accent;ctx 按阈值
21
21
  * success / warning / error;rebill、queue warning;`net!` error。宽 ≥ 110 时 ctx 用余量表。
22
+ * [W6] full 本行按用户参考配色:思考级别 `user`(浅蓝)、目录与分支 success、短提交 dim、`↑N` 领先 `code`(橙)/
23
+ * `↓N` 落后 error、`(+a,-d)` 的 +a success / -d error、费用 warning、时长 `tool`(紫);compact 着色不变。
24
+ * - [W6] git 段在短提交后显示领先 / 落后上游 `↑N ↓N`(只 full;为 0 不显示,没有上游不显示)。
25
+ * - [W6] compact 在行尾(宿主状态之后)追加订阅配额短项 `5h 10% wk 31%`(status-quota.ts),最先丢;
26
+ * 既有记号顺序不变。full 的配额在状态栏下方单独一行。
22
27
  * - [W5-U] bash 在操作系统沙箱里跑时(S2)用量类项多一个 `沙箱`(与 codemode 同一丢弃优先级)。
23
- * - [W5-U] 模型回退中(`model_fallback`)模型项显示 `主模型 → 回退模型`,切回主模型后恢复。
28
+ * - [W5-U] 模型回退中(`model_fallback`)compact 模型项显示 `主模型 → 回退模型`,切回主模型后恢复;[W7] full
29
+ * 本行只显示主模型,`→ 回退模型` 作为开关类项在速率行左区。
30
+ * - [W7] full 左右分区:左 = 状态(权限模式、`shift+tab` 提示),右 = 模型与度量;开关类项在速率行左区,
31
+ * 配额在第三行右区(status-line.ts / status-quota.ts)。
24
32
  * - 模型名缩写:宽 < 100 去掉供应商前缀,< 60 再去掉 `@渠道`,< 48 去掉版本后缀(第一个 `-数字` 起)。
25
33
  * - ASCII:`⎇` → `git`(字形表 `branch`)、`−` → `-`、`♨` → `~`。
26
34
  * - 宽度不够时按优先级丢弃(数字大的先丢,表见 §1.2);会变的数字按最宽形状占位(`reserve`),
@@ -29,6 +37,7 @@
29
37
  import { msg } from "../../i18n/index.js";
30
38
  import { permissionModeLabel } from "../../permissions/modes.js";
31
39
  import { formatModelRef } from "../../ai/providers/channels.js";
40
+ import { compactQuotaItem } from "./status-quota.js";
32
41
  import { Meter, levelColor, truncateToWidth, visibleWidth, } from "../../tui.js";
33
42
  export function formatTokens(count) {
34
43
  if (count < 1000)
@@ -115,9 +124,10 @@ export function layoutRow(parts, width, theme, style = {}) {
115
124
  let kept = [...parts];
116
125
  const zone = (z) => kept.filter((p) => p.zone === z);
117
126
  const fits = () => {
118
- const l = join(zone("left"))[1];
127
+ const left = zone("left");
119
128
  const right = zone("right");
120
- return l + (right.length === 0 ? 0 : sep.length + join(right)[1]) <= width;
129
+ const between = left.length === 0 || right.length === 0 ? 0 : sep.length;
130
+ return join(left)[1] + between + join(right)[1] <= width;
121
131
  };
122
132
  while (!fits()) {
123
133
  const droppable = kept.filter((p) => p.priority !== undefined);
@@ -129,7 +139,14 @@ export function layoutRow(parts, width, theme, style = {}) {
129
139
  const l = join(zone("left"))[0];
130
140
  const r = join(zone("right"))[0];
131
141
  const gap = width - visibleWidth(l) - visibleWidth(r);
132
- const line = r === "" ? l : gap >= MIN_GAP ? l + " ".repeat(gap) + r : l + dim(sep) + r;
142
+ // 左区为空时右区右对齐([W7] full 的速率行没有开关、配额行)
143
+ const line = r === ""
144
+ ? l
145
+ : l === ""
146
+ ? " ".repeat(Math.max(0, gap)) + r
147
+ : gap >= MIN_GAP
148
+ ? l + " ".repeat(gap) + r
149
+ : l + dim(sep) + r;
133
150
  return truncateToWidth(line, width);
134
151
  }
135
152
  /** 按终端宽度缩写模型名(§3.10)。 */
@@ -209,6 +226,7 @@ const COMPACT = {
209
226
  preset: 14,
210
227
  host: 15,
211
228
  hint: 16,
229
+ quota: 17,
212
230
  };
213
231
  /** full 布局本行的优先级(§1.2 下行表)。 */
214
232
  const FULL = { model: 0, ctx: 1, cost: 2, duration: 3, branch: 4, dir: 5, diff: 6, thinking: 7 };
@@ -270,8 +288,8 @@ export class StatusBar {
270
288
  }
271
289
  const model = state.model === undefined ? "?" : abbreviateModel(formatModelRef(state.model), width);
272
290
  const p = full ? FULL : COMPACT;
273
- const fallback = this.source.fallback?.();
274
- // 回退中:`主模型 → 回退模型`(回退模型 warning),切回主模型后恢复原样
291
+ // 回退中:compact 显示 `主模型 → 回退模型`(回退模型 warning);full 本行只留主模型,`→ 回退模型` 在速率行左区
292
+ const fallback = full ? undefined : this.source.fallback?.();
275
293
  const modelText = fallback === undefined
276
294
  ? theme.fg("accent", model)
277
295
  : theme.fg("accent", abbreviateModel(fallback.from, width)) +
@@ -279,7 +297,8 @@ export class StatusBar {
279
297
  theme.fg("warning", abbreviateModel(fallback.to, width));
280
298
  right(modelText, p.model, full ? { group: "model" } : {});
281
299
  if (state.thinkingLevel !== "off") {
282
- right(dim(state.thinkingLevel), p.thinking, full ? { group: "model" } : {});
300
+ const level = full ? theme.fg("user", state.thinkingLevel) : dim(state.thinkingLevel);
301
+ right(level, p.thinking, full ? { group: "model" } : {});
283
302
  }
284
303
  const usage = full ? {} : usageItems(stats, this.queue, this.source, theme);
285
304
  if (usage.tokens !== undefined)
@@ -297,13 +316,15 @@ export class StatusBar {
297
316
  const used = stats.tokens.input + stats.tokens.output + stats.tokens.cacheRead > 0;
298
317
  if (cost === undefined && used)
299
318
  right(dim("$?"), FULL.cost);
300
- else if (cost !== undefined && cost > 0)
301
- right(dim(formatCost(cost)), FULL.cost);
319
+ else if (cost !== undefined && cost > 0) {
320
+ right(theme.fg("warning", formatCost(cost)), FULL.cost);
321
+ }
302
322
  }
303
323
  const started = this.source.sessionStartedAt?.() ?? stats.telemetry?.sessionStartedAt;
304
324
  if (started !== undefined) {
305
325
  const now = this.source.now?.() ?? Date.now();
306
- right(dim(formatDuration(now - started)), p.duration, { reserve: 6 });
326
+ const text = formatDuration(now - started);
327
+ right(full ? theme.fg("tool", text) : dim(text), p.duration, { reserve: 6 });
307
328
  }
308
329
  if (usage.queue !== undefined)
309
330
  right(usage.queue, COMPACT.queue);
@@ -315,9 +336,13 @@ export class StatusBar {
315
336
  right(usage.preset, COMPACT.preset);
316
337
  if (usage.host !== undefined)
317
338
  right(usage.host, COMPACT.host);
339
+ const quota = full ? undefined : this.source.quota?.();
340
+ const quotaItem = typeof quota === "object" ? compactQuotaItem(quota.quota, theme) : undefined;
341
+ if (quotaItem !== undefined)
342
+ right(quotaItem, COMPACT.quota);
318
343
  return parts;
319
344
  }
320
- /** `目录 ⎇ 分支 短提交 +a −b`:三项同组、各自按优先级丢弃。 */
345
+ /** `目录 ⎇ 分支 短提交 +a −b`:三项同组、各自按优先级丢弃;full 另带 `↑N ↓N` 并按参考配色。 */
321
346
  gitParts(p, full) {
322
347
  const git = this.source.git?.();
323
348
  if (git === undefined)
@@ -325,19 +350,37 @@ export class StatusBar {
325
350
  const theme = this.theme;
326
351
  const g = theme.glyphs;
327
352
  const dim = (text) => theme.fg("dim", text);
353
+ const ok = (text) => (full ? theme.fg("success", text) : dim(text));
328
354
  const out = [];
329
- const add = (text, priority) => void out.push({ text: dim(text), priority, zone: "right", group: "git" });
355
+ const add = (text, priority) => void out.push({ text, priority, zone: "right", group: "git" });
330
356
  if (git.dir !== "")
331
- add(git.dir, p.dir);
357
+ add(ok(git.dir), p.dir);
332
358
  const info = git.info;
333
359
  if (info === undefined)
334
360
  return out;
335
- const head = [info.branch, info.shortHead].filter((s) => s !== undefined).join(" ");
336
- if (head !== "")
337
- add(`${g.branch} ${head}`, p.branch);
361
+ const head = [];
362
+ if (info.branch !== undefined)
363
+ head.push(ok(info.branch));
364
+ if (info.shortHead !== undefined)
365
+ head.push(dim(info.shortHead));
366
+ if (full && info.ahead !== undefined && info.ahead > 0) {
367
+ head.push(theme.fg("code", `${g.arrowUp}${info.ahead}`));
368
+ }
369
+ if (full && info.behind !== undefined && info.behind > 0) {
370
+ head.push(theme.fg("error", `${g.arrowDown}${info.behind}`));
371
+ }
372
+ if (!full) {
373
+ const text = [info.branch, info.shortHead].filter((s) => s !== undefined).join(" ");
374
+ if (text !== "")
375
+ add(dim(`${g.branch} ${text}`), p.branch);
376
+ }
377
+ else if (head.length > 0)
378
+ add(`${dim(g.branch)} ${head.join(" ")}`, p.branch);
338
379
  if (info.insertions !== undefined && info.deletions !== undefined) {
339
380
  const { insertions: a, deletions: d } = info;
340
- add(full ? `(+${a},-${d})` : `+${a} ${g.ascii ? "-" : "−"}${d}`, p.diff);
381
+ add(full
382
+ ? `${dim("(")}${theme.fg("success", `+${a}`)}${dim(",")}${theme.fg("error", `-${d}`)}${dim(")")}`
383
+ : dim(`+${a} ${g.ascii ? "-" : "−"}${d}`), p.diff);
341
384
  }
342
385
  return out;
343
386
  }
@@ -2,18 +2,21 @@
2
2
  * 速率行(第五波 §1.1,D1、D2):`ui.statusLine: "full"` 时在状态栏上方多一行。[W5-A]
3
3
  *
4
4
  * ```text
5
- * tps: 100 tok/s • 546 tok / 5.5s (avg 100 · ttft 1.4s) ↑12.3k ↓1.2k · cache 83% ♨ · [-]
5
+ * codemode on · preset x tps: 100 tok/s • 546 tok / 5.5s (avg 100 · ttft 1.4s) · ↑12.3k ↓1.2k · cache 83% ♨ · [-]
6
6
  * ```
7
7
  *
8
- * - 左区(用户样例):`tps: <速率>`(流式中是最近 2 s 窗口的瞬时值、前缀 accent;结束后是该请求的平均值;
9
- * 还没有请求时 `tps: —`)• `<输出 token> tok / <耗时>`(首 token 起算)`(avg <会话均速> · ttft <首 token 延迟>)`;
10
- * `•` 走字形表(ASCII `*`);
11
- * - 右区:从状态栏迁来的用量类项 `↑ ↓` · cache · rebill · queue · codemode · 预设 · [宿主],行尾 `[-]`
12
- * (折叠提示:Ctrl+G 或 `/statusline compact`);
8
+ * - [W7] 左右分区:左 = 状态 / 开关(`codemode on|only`〔网络未隔离追加 `net!`〕· `沙箱` · `preset 名` ·
9
+ * `→ 回退模型` · `queue N` · [宿主]),没有开关时左区为空、右区右对齐;右 = 度量:
10
+ * `tps: <速率>`(流式中是最近 2 s 窗口的瞬时值、前缀 accent;结束后是该请求的平均值;还没有请求时 `tps: —`)
11
+ * • `<输出 token> tok / <耗时>`(首 token 起算)`(avg <会话均速> · ttft <首 token 延迟>)` · `↑ ↓` · cache ·
12
+ * rebill,行尾 `[-]`(折叠提示:Ctrl+G 或 `/statusline compact`);`•` 走字形表(ASCII `*`);
13
13
  * - 数据:`StatusBar.current()` 的 `telemetry` 与用量(同一次 getStats);流式中 `telemetry_tick` 时刷新;
14
- * - 丢弃顺序(§1.2,数字大先丢;`tps` 与 `[-]` 永不丢):tok/耗时 10 → codemode 9 → queue 8 → token 7 →
15
- * cache 6 → rebill 5 → 预设 4 → 宿主 3 → avg 2 → ttft 1;数字按最宽形状占位,不随数值抖动;
14
+ * - 丢弃顺序(数字大先丢;`tps` 与 `[-]` 永不丢):右区度量先丢——tok/耗时 12 → token 11 → cache 10 →
15
+ * rebill 9 → avg 8 → ttft 7,左区开关最后丢——宿主 6 → queue 5 → 预设 4 → 回退 3 → 沙箱 2 → codemode 1;
16
+ * 数字按最宽形状占位,不随数值抖动;
16
17
  * - `compact` 时不占行(render 返回空)。
18
+ * - [W6] 配色(用户参考):标签、单位、`•`、括号 dim;速率数字 `tool`(紫);输出量与耗时、avg 数字 accent(蓝);
19
+ * ttft 数字 `tool`;用量项 dim;回退模型 warning。
17
20
  */
18
21
  import { type Component, type Theme } from "../../tui.js";
19
22
  import { type StatusBar, type StatusBarSource } from "./status-bar.js";