@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
@@ -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,6 +19,11 @@
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
28
  * - [W5-U] 模型回退中(`model_fallback`)模型项显示 `主模型 → 回退模型`,切回主模型后恢复。
24
29
  * - 模型名缩写:宽 < 100 去掉供应商前缀,< 60 再去掉 `@渠道`,< 48 去掉版本后缀(第一个 `-数字` 起)。
@@ -29,6 +34,7 @@
29
34
  import type { AgentSession, SessionCacheStats, SessionStats } from "../../agent/types.js";
30
35
  import type { StatusLineMode } from "../../config/types.js";
31
36
  import type { GitInfo } from "../../git/info.js";
37
+ import { type QuotaView } from "./status-quota.js";
32
38
  import { type Component, type Theme } from "../../tui.js";
33
39
  export interface StatusBarSource {
34
40
  session(): AgentSession;
@@ -58,6 +64,8 @@ export interface StatusBarSource {
58
64
  from: string;
59
65
  to: string;
60
66
  } | undefined;
67
+ /** [W6] 订阅配额(当前模型走 ChatGPT 订阅时);compact 行尾短项用。 */
68
+ quota?(): QuotaView;
61
69
  }
62
70
  export declare function formatTokens(count: number): string;
63
71
  export declare function formatCost(cost: number): string;
@@ -131,7 +139,7 @@ export declare class StatusBar implements Component {
131
139
  queued(): number;
132
140
  layout(): StatusLineMode;
133
141
  private parts;
134
- /** `目录 ⎇ 分支 短提交 +a −b`:三项同组、各自按优先级丢弃。 */
142
+ /** `目录 ⎇ 分支 短提交 +a −b`:三项同组、各自按优先级丢弃;full 另带 `↑N ↓N` 并按参考配色。 */
135
143
  private gitParts;
136
144
  private ctxText;
137
145
  render(width: number): string[];
@@ -19,6 +19,11 @@
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
28
  * - [W5-U] 模型回退中(`model_fallback`)模型项显示 `主模型 → 回退模型`,切回主模型后恢复。
24
29
  * - 模型名缩写:宽 < 100 去掉供应商前缀,< 60 再去掉 `@渠道`,< 48 去掉版本后缀(第一个 `-数字` 起)。
@@ -29,6 +34,7 @@
29
34
  import { msg } from "../../i18n/index.js";
30
35
  import { permissionModeLabel } from "../../permissions/modes.js";
31
36
  import { formatModelRef } from "../../ai/providers/channels.js";
37
+ import { compactQuotaItem } from "./status-quota.js";
32
38
  import { Meter, levelColor, truncateToWidth, visibleWidth, } from "../../tui.js";
33
39
  export function formatTokens(count) {
34
40
  if (count < 1000)
@@ -209,6 +215,7 @@ const COMPACT = {
209
215
  preset: 14,
210
216
  host: 15,
211
217
  hint: 16,
218
+ quota: 17,
212
219
  };
213
220
  /** full 布局本行的优先级(§1.2 下行表)。 */
214
221
  const FULL = { model: 0, ctx: 1, cost: 2, duration: 3, branch: 4, dir: 5, diff: 6, thinking: 7 };
@@ -279,7 +286,8 @@ export class StatusBar {
279
286
  theme.fg("warning", abbreviateModel(fallback.to, width));
280
287
  right(modelText, p.model, full ? { group: "model" } : {});
281
288
  if (state.thinkingLevel !== "off") {
282
- right(dim(state.thinkingLevel), p.thinking, full ? { group: "model" } : {});
289
+ const level = full ? theme.fg("user", state.thinkingLevel) : dim(state.thinkingLevel);
290
+ right(level, p.thinking, full ? { group: "model" } : {});
283
291
  }
284
292
  const usage = full ? {} : usageItems(stats, this.queue, this.source, theme);
285
293
  if (usage.tokens !== undefined)
@@ -297,13 +305,15 @@ export class StatusBar {
297
305
  const used = stats.tokens.input + stats.tokens.output + stats.tokens.cacheRead > 0;
298
306
  if (cost === undefined && used)
299
307
  right(dim("$?"), FULL.cost);
300
- else if (cost !== undefined && cost > 0)
301
- right(dim(formatCost(cost)), FULL.cost);
308
+ else if (cost !== undefined && cost > 0) {
309
+ right(theme.fg("warning", formatCost(cost)), FULL.cost);
310
+ }
302
311
  }
303
312
  const started = this.source.sessionStartedAt?.() ?? stats.telemetry?.sessionStartedAt;
304
313
  if (started !== undefined) {
305
314
  const now = this.source.now?.() ?? Date.now();
306
- right(dim(formatDuration(now - started)), p.duration, { reserve: 6 });
315
+ const text = formatDuration(now - started);
316
+ right(full ? theme.fg("tool", text) : dim(text), p.duration, { reserve: 6 });
307
317
  }
308
318
  if (usage.queue !== undefined)
309
319
  right(usage.queue, COMPACT.queue);
@@ -315,9 +325,13 @@ export class StatusBar {
315
325
  right(usage.preset, COMPACT.preset);
316
326
  if (usage.host !== undefined)
317
327
  right(usage.host, COMPACT.host);
328
+ const quota = full ? undefined : this.source.quota?.();
329
+ const quotaItem = typeof quota === "object" ? compactQuotaItem(quota.quota, theme) : undefined;
330
+ if (quotaItem !== undefined)
331
+ right(quotaItem, COMPACT.quota);
318
332
  return parts;
319
333
  }
320
- /** `目录 ⎇ 分支 短提交 +a −b`:三项同组、各自按优先级丢弃。 */
334
+ /** `目录 ⎇ 分支 短提交 +a −b`:三项同组、各自按优先级丢弃;full 另带 `↑N ↓N` 并按参考配色。 */
321
335
  gitParts(p, full) {
322
336
  const git = this.source.git?.();
323
337
  if (git === undefined)
@@ -325,19 +339,37 @@ export class StatusBar {
325
339
  const theme = this.theme;
326
340
  const g = theme.glyphs;
327
341
  const dim = (text) => theme.fg("dim", text);
342
+ const ok = (text) => (full ? theme.fg("success", text) : dim(text));
328
343
  const out = [];
329
- const add = (text, priority) => void out.push({ text: dim(text), priority, zone: "right", group: "git" });
344
+ const add = (text, priority) => void out.push({ text, priority, zone: "right", group: "git" });
330
345
  if (git.dir !== "")
331
- add(git.dir, p.dir);
346
+ add(ok(git.dir), p.dir);
332
347
  const info = git.info;
333
348
  if (info === undefined)
334
349
  return out;
335
- const head = [info.branch, info.shortHead].filter((s) => s !== undefined).join(" ");
336
- if (head !== "")
337
- add(`${g.branch} ${head}`, p.branch);
350
+ const head = [];
351
+ if (info.branch !== undefined)
352
+ head.push(ok(info.branch));
353
+ if (info.shortHead !== undefined)
354
+ head.push(dim(info.shortHead));
355
+ if (full && info.ahead !== undefined && info.ahead > 0) {
356
+ head.push(theme.fg("code", `${g.arrowUp}${info.ahead}`));
357
+ }
358
+ if (full && info.behind !== undefined && info.behind > 0) {
359
+ head.push(theme.fg("error", `${g.arrowDown}${info.behind}`));
360
+ }
361
+ if (!full) {
362
+ const text = [info.branch, info.shortHead].filter((s) => s !== undefined).join(" ");
363
+ if (text !== "")
364
+ add(dim(`${g.branch} ${text}`), p.branch);
365
+ }
366
+ else if (head.length > 0)
367
+ add(`${dim(g.branch)} ${head.join(" ")}`, p.branch);
338
368
  if (info.insertions !== undefined && info.deletions !== undefined) {
339
369
  const { insertions: a, deletions: d } = info;
340
- add(full ? `(+${a},-${d})` : `+${a} ${g.ascii ? "-" : "−"}${d}`, p.diff);
370
+ add(full
371
+ ? `${dim("(")}${theme.fg("success", `+${a}`)}${dim(",")}${theme.fg("error", `-${d}`)}${dim(")")}`
372
+ : dim(`+${a} ${g.ascii ? "-" : "−"}${d}`), p.diff);
341
373
  }
342
374
  return out;
343
375
  }
@@ -14,6 +14,8 @@
14
14
  * - 丢弃顺序(§1.2,数字大先丢;`tps` 与 `[-]` 永不丢):tok/耗时 10 → codemode 9 → queue 8 → token 7 →
15
15
  * cache 6 → rebill 5 → 预设 4 → 宿主 3 → avg 2 → ttft 1;数字按最宽形状占位,不随数值抖动;
16
16
  * - `compact` 时不占行(render 返回空)。
17
+ * - [W6] 配色(用户参考):标签、单位、`•`、括号 dim;速率数字 `tool`(紫);输出量与耗时、avg 数字 accent(蓝);
18
+ * ttft 数字 `tool`;右区用量项不变。
17
19
  */
18
20
  import { type Component, type Theme } from "../../tui.js";
19
21
  import { type StatusBar, type StatusBarSource } from "./status-bar.js";
@@ -14,6 +14,8 @@
14
14
  * - 丢弃顺序(§1.2,数字大先丢;`tps` 与 `[-]` 永不丢):tok/耗时 10 → codemode 9 → queue 8 → token 7 →
15
15
  * cache 6 → rebill 5 → 预设 4 → 宿主 3 → avg 2 → ttft 1;数字按最宽形状占位,不随数值抖动;
16
16
  * - `compact` 时不占行(render 返回空)。
17
+ * - [W6] 配色(用户参考):标签、单位、`•`、括号 dim;速率数字 `tool`(紫);输出量与耗时、avg 数字 accent(蓝);
18
+ * ttft 数字 `tool`;右区用量项不变。
17
19
  */
18
20
  import { truncateToWidth } from "../../tui.js";
19
21
  import { formatTokens, layoutRow, usageItems, } from "./status-bar.js";
@@ -68,29 +70,32 @@ export class StatusLine {
68
70
  if (text !== undefined)
69
71
  parts.push({ text, priority, zone: "right" });
70
72
  };
73
+ const blue = (text) => theme.fg("accent", text);
74
+ const purple = (text) => theme.fg("tool", text);
75
+ const amountText = (tokens, ms) => `${blue(formatTokens(tokens))}${dim(" tok / ")}${blue(formatSeconds(ms))}`;
71
76
  let tps;
72
77
  let amount;
73
78
  if (live !== undefined) {
74
79
  tps = live.tps;
75
- amount = `${formatTokens(live.outputTokens)} tok / ${formatSeconds(live.elapsedMs)}`;
80
+ amount = amountText(live.outputTokens, live.elapsedMs);
76
81
  }
77
82
  else if (last?.doneAt !== undefined) {
78
83
  tps = last.tps;
79
84
  if (last.outputTokens !== undefined && last.firstTokenAt !== undefined) {
80
- amount = `${formatTokens(last.outputTokens)} tok / ${formatSeconds(last.doneAt - last.firstTokenAt)}`;
85
+ amount = amountText(last.outputTokens, last.doneAt - last.firstTokenAt);
81
86
  }
82
87
  }
83
88
  const label = live !== undefined ? theme.fg("accent", "tps:") : dim("tps:");
84
- left(`${label} ${dim(tps === undefined ? "—" : `${formatRate(tps)} tok/s`)}`, undefined, RESERVE.tps);
89
+ left(`${label} ${tps === undefined ? dim("—") : `${purple(formatRate(tps))}${dim(" tok/s")}`}`, undefined, RESERVE.tps);
85
90
  if (amount !== undefined) {
86
- left(dim(amount), PRIORITY.amount, RESERVE.amount, { lead: ` ${theme.glyphs.dot} ` });
91
+ left(amount, PRIORITY.amount, RESERVE.amount, { lead: ` ${theme.glyphs.dot} ` });
87
92
  }
88
93
  const stat = { group: "stats", lead: " " };
89
94
  if (telemetry?.avgTps !== undefined) {
90
- left(dim(`avg ${formatRate(telemetry.avgTps)}`), PRIORITY.avg, RESERVE.avg, stat);
95
+ left(`${dim("avg ")}${blue(formatRate(telemetry.avgTps))}`, PRIORITY.avg, RESERVE.avg, stat);
91
96
  }
92
97
  if (last?.ttftMs !== undefined) {
93
- left(dim(`ttft ${formatSeconds(last.ttftMs)}`), PRIORITY.ttft, RESERVE.ttft, stat);
98
+ left(`${dim("ttft ")}${purple(formatSeconds(last.ttftMs))}`, PRIORITY.ttft, RESERVE.ttft, stat);
94
99
  }
95
100
  const usage = usageItems(stats, this.bar.queued(), this.source, theme);
96
101
  right(usage.tokens, PRIORITY.tokens);
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 订阅配额行(full 布局第三行,状态栏下方;compact 只在行尾追加一个短项)。[W6]
3
+ *
4
+ * ```text
5
+ * Session: 10.0% | Reset: 2h 18m | Weekly: 31.0% | Weekly Reset: 6d 5h ≥ 80 列
6
+ * 5h 10% ↻2h18m · wk 31% ↻6d5h < 80 列
7
+ * ```
8
+ *
9
+ * - 数据:会话的 `quota_update`(codex flavor 的 `x-codex-primary/secondary-*` 头、`codex.rate_limits` 事件;
10
+ * SIWC 只有 429)。primary = 5 小时窗口、secondary = 周窗口;窗口长度不是 5 h / 7 d 时标签换成实际时长。
11
+ * - 只在当前模型走 ChatGPT 订阅(compat `chatgptBackend`)时出现;codex flavor 还没有数据时显示
12
+ * 「配额:首次请求后显示」(第一次请求就会带回配额,先占住行免得行数跳动),SIWC 没有数据时不占行
13
+ * (只有超限才有配额,占位会一直挂着误导)。
14
+ * - 着色:标签与分隔 dim;百分比按阈值(≥ 70% warning、≥ 90% error,否则 success);重置时间 `tool`(紫)。
15
+ * - 重置时间是相对时长(`2h 18m`、`6d 5h`),按渲染时刻算;StatusArea 每分钟重画一次。
16
+ */
17
+ import type { QuotaUpdateEvent } from "../../agent/types-w6.js";
18
+ import type { StatusLineMode } from "../../config/types.js";
19
+ import { type Component, type Theme } from "../../tui.js";
20
+ import { type Part } from "./status-bar.js";
21
+ /** 配额行的数据:有数据 / 等首次请求 / 不显示(undefined)。 */
22
+ export type QuotaView = {
23
+ quota: QuotaUpdateEvent;
24
+ } | "pending" | undefined;
25
+ /** 第三行改用短格式的宽度。 */
26
+ export declare const QUOTA_NARROW_WIDTH = 80;
27
+ /** 距重置:`6d 5h` / `2h 18m` / `18m`(已过或不足一分钟 `0m`);`tight` 去掉空格。 */
28
+ export declare function formatRemaining(ms: number, tight?: boolean): string;
29
+ /** compact 行尾的短项 `5h 10% wk 31%`(项内只用空格,不含 ` · `,宿主解析不受影响)。 */
30
+ export declare function compactQuotaItem(quota: QuotaUpdateEvent, theme: Theme): string | undefined;
31
+ /** 配额行各项(宽 / 窄两种格式);重置时间先丢,主窗口百分比永不丢。 */
32
+ export declare function quotaParts(quota: QuotaUpdateEvent, width: number, now: number, theme: Theme): Part[];
33
+ export interface QuotaLineSource {
34
+ layout(): StatusLineMode;
35
+ quota(): QuotaView;
36
+ now(): number;
37
+ }
38
+ export declare class QuotaLine implements Component {
39
+ private readonly source;
40
+ private readonly theme;
41
+ constructor(source: QuotaLineSource, theme: Theme);
42
+ render(width: number): string[];
43
+ invalidate(): void;
44
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * 订阅配额行(full 布局第三行,状态栏下方;compact 只在行尾追加一个短项)。[W6]
3
+ *
4
+ * ```text
5
+ * Session: 10.0% | Reset: 2h 18m | Weekly: 31.0% | Weekly Reset: 6d 5h ≥ 80 列
6
+ * 5h 10% ↻2h18m · wk 31% ↻6d5h < 80 列
7
+ * ```
8
+ *
9
+ * - 数据:会话的 `quota_update`(codex flavor 的 `x-codex-primary/secondary-*` 头、`codex.rate_limits` 事件;
10
+ * SIWC 只有 429)。primary = 5 小时窗口、secondary = 周窗口;窗口长度不是 5 h / 7 d 时标签换成实际时长。
11
+ * - 只在当前模型走 ChatGPT 订阅(compat `chatgptBackend`)时出现;codex flavor 还没有数据时显示
12
+ * 「配额:首次请求后显示」(第一次请求就会带回配额,先占住行免得行数跳动),SIWC 没有数据时不占行
13
+ * (只有超限才有配额,占位会一直挂着误导)。
14
+ * - 着色:标签与分隔 dim;百分比按阈值(≥ 70% warning、≥ 90% error,否则 success);重置时间 `tool`(紫)。
15
+ * - 重置时间是相对时长(`2h 18m`、`6d 5h`),按渲染时刻算;StatusArea 每分钟重画一次。
16
+ */
17
+ import { msg } from "../../i18n/index.js";
18
+ import { levelColor, truncateToWidth, visibleWidth, } from "../../tui.js";
19
+ import { layoutRow } from "./status-bar.js";
20
+ /** 第三行改用短格式的宽度。 */
21
+ export const QUOTA_NARROW_WIDTH = 80;
22
+ const FIVE_HOURS = 300;
23
+ const WEEK = 10_080;
24
+ /** 距重置:`6d 5h` / `2h 18m` / `18m`(已过或不足一分钟 `0m`);`tight` 去掉空格。 */
25
+ export function formatRemaining(ms, tight = false) {
26
+ const minutes = Math.max(0, Math.floor(ms / 60_000));
27
+ const days = Math.floor(minutes / 1440);
28
+ const hours = Math.floor((minutes % 1440) / 60);
29
+ const sep = tight ? "" : " ";
30
+ if (days > 0)
31
+ return `${days}d${sep}${hours}h`;
32
+ if (hours > 0)
33
+ return `${hours}h${sep}${minutes % 60}m`;
34
+ return `${minutes}m`;
35
+ }
36
+ /** 窗口长度的紧凑写法(标签用):`5h`、`7d`、`90m`。 */
37
+ function windowSpan(minutes) {
38
+ if (minutes % 1440 === 0)
39
+ return `${minutes / 1440}d`;
40
+ if (minutes % 60 === 0)
41
+ return `${minutes / 60}h`;
42
+ return `${Math.round(minutes)}m`;
43
+ }
44
+ function labels(w, which) {
45
+ const m = msg().interactive.statusLine.quota;
46
+ const minutes = w.windowMinutes;
47
+ const standard = minutes === undefined || minutes === (which === "primary" ? FIVE_HOURS : WEEK);
48
+ if (!standard)
49
+ return { pct: m.window(windowSpan(minutes)), reset: m.windowReset(windowSpan(minutes)) };
50
+ return which === "primary"
51
+ ? { pct: m.session, reset: m.reset }
52
+ : { pct: m.weekly, reset: m.weeklyReset };
53
+ }
54
+ function shortLabel(w, which) {
55
+ const m = msg().interactive.statusLine.quota;
56
+ const minutes = w.windowMinutes;
57
+ if (minutes === FIVE_HOURS || (minutes === undefined && which === "primary"))
58
+ return m.short5h;
59
+ if (minutes === WEEK || (minutes === undefined && which === "secondary"))
60
+ return m.shortWeek;
61
+ return windowSpan(minutes);
62
+ }
63
+ function percent(theme, used, decimal) {
64
+ return theme.fg(levelColor(used / 100), decimal ? `${used.toFixed(1)}%` : `${Math.round(used)}%`);
65
+ }
66
+ function windows(quota) {
67
+ const out = [];
68
+ if (quota.primary !== undefined)
69
+ out.push(["primary", quota.primary]);
70
+ if (quota.secondary !== undefined)
71
+ out.push(["secondary", quota.secondary]);
72
+ return out;
73
+ }
74
+ /** compact 行尾的短项 `5h 10% wk 31%`(项内只用空格,不含 ` · `,宿主解析不受影响)。 */
75
+ export function compactQuotaItem(quota, theme) {
76
+ const dim = (text) => theme.fg("dim", text);
77
+ const items = windows(quota).map(([which, w]) => `${dim(shortLabel(w, which))} ${percent(theme, w.usedPercent, false)}`);
78
+ return items.length === 0 ? undefined : items.join(" ");
79
+ }
80
+ /** 配额行各项(宽 / 窄两种格式);重置时间先丢,主窗口百分比永不丢。 */
81
+ export function quotaParts(quota, width, now, theme) {
82
+ const dim = (text) => theme.fg("dim", text);
83
+ const narrow = width < QUOTA_NARROW_WIDTH;
84
+ const parts = [];
85
+ windows(quota).forEach(([which, w], i) => {
86
+ const first = i === 0;
87
+ const group = narrow ? which : undefined;
88
+ const name = narrow ? `${shortLabel(w, which)} ` : labels(w, which).pct;
89
+ parts.push({
90
+ text: dim(name) + percent(theme, w.usedPercent, !narrow),
91
+ priority: first ? undefined : 2,
92
+ zone: "left",
93
+ ...(group !== undefined ? { group } : {}),
94
+ });
95
+ if (w.resetsAt === undefined)
96
+ return;
97
+ const left = formatRemaining(w.resetsAt - now, narrow);
98
+ parts.push({
99
+ text: narrow
100
+ ? theme.fg("tool", `${theme.glyphs.retry}${left}`)
101
+ : dim(labels(w, which).reset) + theme.fg("tool", left),
102
+ priority: first ? 3 : 1,
103
+ zone: "left",
104
+ // 宽格式按最宽形状占位(`23h 59m`),数值变化不让某项时有时无
105
+ reserve: narrow ? 0 : visibleWidth(labels(w, which).reset) + 7,
106
+ ...(group !== undefined ? { group } : {}),
107
+ });
108
+ });
109
+ return parts;
110
+ }
111
+ export class QuotaLine {
112
+ source;
113
+ theme;
114
+ constructor(source, theme) {
115
+ this.source = source;
116
+ this.theme = theme;
117
+ }
118
+ render(width) {
119
+ if (this.source.layout() !== "full")
120
+ return [];
121
+ const view = this.source.quota();
122
+ if (view === undefined)
123
+ return [];
124
+ const theme = this.theme;
125
+ if (view === "pending") {
126
+ return [truncateToWidth(theme.fg("dim", msg().interactive.statusLine.quota.pending), width)];
127
+ }
128
+ const parts = quotaParts(view.quota, width, this.source.now(), theme);
129
+ if (parts.length === 0)
130
+ return [];
131
+ const sep = width < QUOTA_NARROW_WIDTH ? " · " : " | ";
132
+ return [layoutRow(parts, width, theme, { sep })];
133
+ }
134
+ invalidate() { }
135
+ }
@@ -12,6 +12,7 @@
12
12
  * 后台任务的工具调用立即返回,工具行下面多一行跟随状态(`↳ t2 explore · 完成 · 7 轮 …`,运行中每秒刷新);
13
13
  * 完成后父会话收到的 `<task-notification>` 只显示一行(`notificationSummary`),失败 / 停止另有一行提示。
14
14
  * 外部 runner(claude / codex)同一组事件,用量可能缺。
15
+ * [W7-C] `subagent_background`(前台转后台):置 `background`,工具行出跟随行。
15
16
  */
16
17
  import type { SessionEvent, SubagentStatus } from "../../agent/types.js";
17
18
  import type { Usage } from "../../ai/types.js";
@@ -12,6 +12,7 @@
12
12
  * 后台任务的工具调用立即返回,工具行下面多一行跟随状态(`↳ t2 explore · 完成 · 7 轮 …`,运行中每秒刷新);
13
13
  * 完成后父会话收到的 `<task-notification>` 只显示一行(`notificationSummary`),失败 / 停止另有一行提示。
14
14
  * 外部 runner(claude / codex)同一组事件,用量可能缺。
15
+ * [W7-C] `subagent_background`(前台转后台):置 `background`,工具行出跟随行。
15
16
  */
16
17
  import { msg } from "../../i18n/index.js";
17
18
  import { formatElapsed } from "../../tui.js";
@@ -61,6 +62,13 @@ export class SubagentTracker {
61
62
  }
62
63
  return state;
63
64
  }
65
+ case "subagent_background": {
66
+ const state = this.tasks.get(event.taskId);
67
+ if (state === undefined)
68
+ return undefined;
69
+ state.background = true;
70
+ return state;
71
+ }
64
72
  case "subagent_end": {
65
73
  const state = this.tasks.get(event.taskId);
66
74
  if (state === undefined)
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 前台子 Agent 任务转后台与栏内停止的界面接线(docs/agents-concurrency-plan.md §2.4、§2.8)。[W7-C]
3
+ *
4
+ * - `Ctrl+B`(`app.tasks.background`):有阻塞中的前台任务(`registry.blocking()`,含 `task_ctl wait`)时
5
+ * 全部转后台(`session.backgroundTask(undefined, "user")`),不看输入框;没有时落回编辑器。
6
+ * - 栏内 `b` / `Ctrl+B`:转后台选中的任务;`x`:1.5 s 内连按两次停止(`StopArm`)。
7
+ * - `/tasks bg [id]`(commands-core.ts,line 模式同):同 Ctrl+B / 指定任务。
8
+ * - Esc 中断只连带前台任务:仍在运行的后台任务写进中断提示(`runningBackground`)。
9
+ */
10
+ import type { AgentSession } from "../../agent/types.js";
11
+ import type { Keybindings } from "../../tui.js";
12
+ /** 栏内 `x` 两次之间的最长间隔。 */
13
+ export declare const STOP_CONFIRM_MS = 1500;
14
+ /** 正在阻塞父回合的任务(前台运行中、或有 `task_ctl wait` 在等)。 */
15
+ export declare function blockingTasks(session: AgentSession): string[];
16
+ /** 人工转后台:不给 taskId = 全部阻塞中的任务(没有时不调用,返回空)。 */
17
+ export declare function backgroundTasks(session: AgentSession, taskId?: string): string[];
18
+ /** 仍在运行的后台任务(Esc 中断不影响它们)。 */
19
+ export declare function runningBackground(session: AgentSession): string[];
20
+ /** 转后台后的一行提示;没有转任何任务时给「没有可转的」。 */
21
+ export declare function backgroundedText(ids: readonly string[]): string;
22
+ /** `ctrl+b` → `Ctrl+B`(提示行用);动作没有绑定时 undefined。 */
23
+ export declare function keyLabel(keys: Keybindings, action: "app.tasks.background"): string | undefined;
24
+ /** 栏内 `x` 的双击确认:第一次 `arm`(提示「再按 x 停止」),间隔内对同一任务再按为 `stop`。 */
25
+ export declare class StopArm {
26
+ private readonly windowMs;
27
+ private armed;
28
+ constructor(windowMs?: number);
29
+ press(taskId: string, now: number): "arm" | "stop";
30
+ reset(): void;
31
+ }