@armadra/agent 0.7.1 → 0.7.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 (97) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/CHANGELOG.zh-CN.md +38 -0
  3. package/dist/agent/session-cache.d.ts +4 -2
  4. package/dist/agent/session-cache.js +5 -3
  5. package/dist/agent/session-compaction.d.ts +19 -1
  6. package/dist/agent/session-compaction.js +42 -0
  7. package/dist/agent/session-core.d.ts +2 -0
  8. package/dist/agent/session-state.d.ts +9 -3
  9. package/dist/agent/session-state.js +8 -6
  10. package/dist/agent/session.js +3 -3
  11. package/dist/agent/types-w5.d.ts +3 -0
  12. package/dist/agent/types.d.ts +18 -0
  13. package/dist/agents/task-record.d.ts +4 -0
  14. package/dist/agents/task-record.js +18 -0
  15. package/dist/ai/context.d.ts +7 -2
  16. package/dist/ai/context.js +45 -7
  17. package/dist/ai/providers/catalog-data.js +1 -1
  18. package/dist/bundle/ama.cjs +1663 -486
  19. package/dist/compaction/breakdown.d.ts +51 -0
  20. package/dist/compaction/breakdown.js +159 -0
  21. package/dist/compaction/estimate.d.ts +6 -0
  22. package/dist/compaction/estimate.js +1 -1
  23. package/dist/compaction/prefix-estimate.d.ts +25 -0
  24. package/dist/compaction/prefix-estimate.js +38 -0
  25. package/dist/config/json-schema.js +1 -0
  26. package/dist/config/key-docs.js +1 -0
  27. package/dist/config/schema.js +2 -0
  28. package/dist/config/settings-registry.js +1 -0
  29. package/dist/config/types.d.ts +2 -0
  30. package/dist/drivers/native/codex-app-server.js +6 -0
  31. package/dist/drivers/runner.js +17 -0
  32. package/dist/drivers/store.d.ts +3 -0
  33. package/dist/drivers/store.js +4 -0
  34. package/dist/i18n/catalog.d.ts +55 -0
  35. package/dist/i18n/messages/agents.d.ts +3 -0
  36. package/dist/i18n/messages/agents.js +3 -0
  37. package/dist/i18n/messages/config-keys.d.ts +2 -0
  38. package/dist/i18n/messages/config-keys.js +2 -0
  39. package/dist/i18n/messages/config.d.ts +2 -0
  40. package/dist/i18n/messages/interactive.d.ts +6 -0
  41. package/dist/i18n/messages/interactive.js +6 -0
  42. package/dist/i18n/messages/panels.d.ts +3 -0
  43. package/dist/i18n/messages/panels.js +5 -2
  44. package/dist/i18n/messages/report-context.d.ts +98 -0
  45. package/dist/i18n/messages/report-context.js +99 -0
  46. package/dist/i18n/messages/report.d.ts +100 -0
  47. package/dist/i18n/messages/report.js +13 -2
  48. package/dist/i18n/messages/settings.d.ts +2 -0
  49. package/dist/i18n/messages/settings.js +2 -0
  50. package/dist/i18n/messages/subcommands.js +2 -2
  51. package/dist/modes/acp/acp-events.d.ts +17 -4
  52. package/dist/modes/acp/acp-events.js +32 -6
  53. package/dist/modes/commands-core.js +4 -0
  54. package/dist/modes/context-report.d.ts +37 -0
  55. package/dist/modes/context-report.js +188 -0
  56. package/dist/modes/interactive/agent-bar.d.ts +2 -0
  57. package/dist/modes/interactive/agent-bar.js +6 -0
  58. package/dist/modes/interactive/commands.d.ts +2 -1
  59. package/dist/modes/interactive/commands.js +10 -1
  60. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  61. package/dist/modes/interactive/interactive-mode.js +6 -1
  62. package/dist/modes/interactive/panels.d.ts +2 -1
  63. package/dist/modes/interactive/panels.js +13 -5
  64. package/dist/modes/interactive/program-status-ui.d.ts +72 -0
  65. package/dist/modes/interactive/program-status-ui.js +229 -0
  66. package/dist/modes/interactive/session-events.d.ts +4 -0
  67. package/dist/modes/interactive/session-events.js +24 -2
  68. package/dist/modes/interactive/status-bar.d.ts +21 -3
  69. package/dist/modes/interactive/status-bar.js +64 -25
  70. package/dist/modes/interactive/status-ctx.d.ts +55 -0
  71. package/dist/modes/interactive/status-ctx.js +125 -0
  72. package/dist/modes/interactive/status-line.d.ts +2 -1
  73. package/dist/modes/interactive/status-line.js +3 -2
  74. package/dist/modes/interactive/tasks-report.js +4 -0
  75. package/dist/modes/print/print-mode.js +12 -0
  76. package/dist/modes/session-report.d.ts +10 -2
  77. package/dist/modes/session-report.js +26 -2
  78. package/dist/sdk.d.ts +1 -1
  79. package/dist/tools/types.d.ts +6 -0
  80. package/dist/tui/components/meter.d.ts +2 -0
  81. package/dist/tui/components/meter.js +1 -1
  82. package/dist/tui/program-status.d.ts +90 -0
  83. package/dist/tui/program-status.js +259 -0
  84. package/docs/acp.md +4 -4
  85. package/docs/agents.md +1 -1
  86. package/docs/en/acp.md +4 -4
  87. package/docs/en/providers.md +5 -3
  88. package/docs/en/rpc.md +3 -0
  89. package/docs/en/sessions.md +23 -0
  90. package/docs/en/tui.md +79 -22
  91. package/docs/providers.md +8 -5
  92. package/docs/rpc.md +1 -0
  93. package/docs/session-format.md +3 -3
  94. package/docs/sessions.md +22 -0
  95. package/docs/tui-design.md +2 -2
  96. package/docs/tui.md +79 -22
  97. package/package.json +1 -1
@@ -12,9 +12,13 @@
12
12
  * | permission_resolved(allow / allow_session)| tool_call_update(in_progress) |
13
13
  * | tool_execution_end | tool_call_update(completed / failed,`[diff?, text]`,locations[].line) |
14
14
  * | todo_updated | plan |
15
- * | turn_end | usage_update(上下文用量、窗口、会话累计美元) |
15
+ * | message_end(助手)/ turn_end | usage_update(上下文用量、窗口、会话累计美元;与上次同值不发) |
16
16
  * | permission_mode_changed | current_mode_update + config_option_update |
17
- * | model_changed / thinking_level_changed | config_option_update |
17
+ * | model_changed | config_option_update + usage_update(窗口可能变了,不去重) |
18
+ * | thinking_level_changed | config_option_update |
19
+ *
20
+ * new / load(回放之后)/ resume 的答复发出后,{@link AcpEventMapper.announce} 另发一条 `usage_update`,
21
+ * 客户端不用等第一轮结束就能显示用量。模型窗口未知时不发:schema 里 `size` 必填,不编造窗口。
18
22
  *
19
23
  * 工具结果只回前 4 KB 文本(完整结果在 ama 会话里);diff 只在实时事件里有(`fileChange` 不落盘)。
20
24
  * `session/load` 回放同一套映射(用户消息 → user_message_chunk,工具结果带文本、无 diff)。
@@ -54,6 +58,8 @@ export class AcpEventMapper {
54
58
  usedWireIds = new Set();
55
59
  /** 上次 `session_info_update` 带出的标题。 */
56
60
  lastTitle = null;
61
+ /** 上次 `usage_update` 的内容(同值不重发)。 */
62
+ lastUsage;
57
63
  constructor(cwd, emit, session,
58
64
  /** 会话的配置项与命令表({@link announce} 与配置变化时取;[ACP-D] 填实际内容)。 */
59
65
  extras = () => ({ configOptions: [], commands: [] })) {
@@ -62,11 +68,15 @@ export class AcpEventMapper {
62
68
  this.session = session;
63
69
  this.extras = extras;
64
70
  }
65
- /** 会话 new / load / resume 的响应发出后由服务端调:`available_commands_update` 与 `config_option_update`。 */
71
+ /**
72
+ * 会话 new / load / resume 的响应发出后由服务端调:`available_commands_update`、`config_option_update`
73
+ * 与 `usage_update`(客户端可能刚重建线程,用量不去重,照发一次)。
74
+ */
66
75
  announce() {
67
76
  const { configOptions, commands } = this.extras();
68
77
  this.emit({ sessionUpdate: "available_commands_update", availableCommands: commands });
69
78
  this.emit({ sessionUpdate: "config_option_update", configOptions });
79
+ this.emitUsage(true);
70
80
  }
71
81
  /** 回合结束后由服务端调:`session_info_update`(标题与上次不同才带 title)。 */
72
82
  emitSessionInfo(title, updatedAt) {
@@ -188,6 +198,11 @@ export class AcpEventMapper {
188
198
  })),
189
199
  });
190
200
  return;
201
+ case "message_end":
202
+ // 多工具轮里每条助手消息都更新一次用量,不等整轮结束
203
+ if ("role" in event.message && event.message.role === "assistant")
204
+ this.emitUsage();
205
+ return;
191
206
  case "turn_end":
192
207
  this.emitUsage();
193
208
  return;
@@ -205,21 +220,32 @@ export class AcpEventMapper {
205
220
  sessionUpdate: "config_option_update",
206
221
  configOptions: this.extras().configOptions,
207
222
  });
223
+ if (event.type === "model_changed")
224
+ this.emitUsage(true); // 换模型照发(窗口可能变了)
208
225
  return;
209
226
  default:
210
227
  return;
211
228
  }
212
229
  }
213
- emitUsage() {
230
+ /**
231
+ * 发一条 `usage_update`:上下文用量、窗口与会话累计美元。窗口未知时不发(`size` 必填);
232
+ * 与上次内容相同时不发,`force` 时照发。
233
+ */
234
+ emitUsage(force = false) {
214
235
  const stats = this.session().getStats();
215
236
  if (stats.contextWindow === undefined)
216
237
  return;
217
- this.emit({
238
+ const update = {
218
239
  sessionUpdate: "usage_update",
219
240
  used: stats.contextTokens ?? 0,
220
241
  size: stats.contextWindow,
221
242
  ...(stats.cost !== undefined ? { cost: { amount: stats.cost, currency: "USD" } } : {}),
222
- });
243
+ };
244
+ const key = JSON.stringify(update);
245
+ if (!force && key === this.lastUsage)
246
+ return;
247
+ this.lastUsage = key;
248
+ this.emit(update);
223
249
  }
224
250
  /** `session/load`:按消息回放历史(工具结果带前 4 KB 文本;diff 不落盘,回放没有)。 */
225
251
  replay(messages) {
@@ -24,6 +24,7 @@ import { rewindCommand } from "./interactive/rewind-command.js";
24
24
  import { describeAgents, describeTaskOutput, describeTasks, stopTask, } from "./interactive/tasks-report.js";
25
25
  import { backgroundTasks, backgroundedText } from "./interactive/task-background.js";
26
26
  import { describeCache, describeFingerprint, describeSession } from "./session-report.js";
27
+ import { describeContext } from "./context-report.js";
27
28
  import { msg } from "../i18n/index.js";
28
29
  export { describeSession } from "./session-report.js";
29
30
  /** [W6-C0] 各功能批次实现的第六波命令(没有处理器时回「尚未提供」)。 */
@@ -61,6 +62,7 @@ export const BUILTIN_COMMANDS = [
61
62
  command("tools", "tools", { key: "toolsArgs" }),
62
63
  command("hooks", "hooks"),
63
64
  command("session", "session"),
65
+ command("context", "context"),
64
66
  command("cache", "cache", "[warm off|streaming|idle | fingerprint]"),
65
67
  command("statusline", "statusline", "[full|compact]"),
66
68
  command("plan", "plan", { key: "planArgs" }),
@@ -239,6 +241,8 @@ export async function runSlashCommand(line, ctx) {
239
241
  }
240
242
  case "session":
241
243
  return { kind: "handled", message: describeSession(session) };
244
+ case "context":
245
+ return { kind: "handled", message: describeContext(session) };
242
246
  case "cache":
243
247
  return { kind: "handled", message: cacheCommand(session, args) };
244
248
  case "rewind":
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `/context`:上下文分类明细的报告(line 模式文本与交互模式面板共用)。
3
+ *
4
+ * - 顶部:已用 / 窗口(百分比)、剩余、来源(「usage 实测 X + 估算 Y」或「全量估算」)、自动压缩阈值
5
+ * (窗口 − 预留,读会话的压缩设置与 `state.autoCompaction`)与档一裁剪起点;
6
+ * - 按类别:系统提示(按节)、工具声明(按工具)、用户消息、助手文本、推理、工具调用参数、工具结果
7
+ * (按工具名)、附件图片、摘要(压缩 / 分支)、自定义消息;每类 token 估算、占比与条形;
8
+ * - 底部:最大的 N 个工具结果,只有序号、工具名与大小,**绝不显示正文**。
9
+ *
10
+ * 统计只读:取活动分支的投影(与 `getStats().contextTokens` 同一口径),不改请求体、不写条目。
11
+ */
12
+ import type { AgentSession } from "../agent/types.js";
13
+ import { type ContextBreakdown } from "../compaction/breakdown.js";
14
+ import { type ContextEstimate } from "../compaction/estimate.js";
15
+ import { type Component, type Theme } from "../tui.js";
16
+ /** 底部列出的工具结果个数。 */
17
+ export declare const CONTEXT_TOP_RESULTS = 5;
18
+ export interface ContextSnapshot {
19
+ breakdown: ContextBreakdown;
20
+ estimate: ContextEstimate;
21
+ /** 首次请求前:系统提示与工具声明按将要发送的那一份估算(与状态栏的启动基线同一口径)。 */
22
+ prefixPending: boolean;
23
+ window: number | undefined;
24
+ /** 0–100(一位小数,封顶 100);无窗口时 undefined。 */
25
+ percent: number | undefined;
26
+ autoCompaction: boolean;
27
+ /** 档二自动压缩的触发点(窗口 − 预留);无窗口时 undefined。 */
28
+ compactAt: number | undefined;
29
+ /** 档一裁剪的触发点;无窗口时 undefined。 */
30
+ pruneAt: number | undefined;
31
+ }
32
+ /** 读会话的当前上下文(只读)。 */
33
+ export declare function contextSnapshot(session: AgentSession): ContextSnapshot;
34
+ /** 纯文本(line 模式、无面板时的通知)。 */
35
+ export declare function describeContext(session: AgentSession): string;
36
+ /** 交互模式的消息区面板(左竖条卡片)。 */
37
+ export declare function contextPanel(session: AgentSession, theme: Theme): Component;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * `/context`:上下文分类明细的报告(line 模式文本与交互模式面板共用)。
3
+ *
4
+ * - 顶部:已用 / 窗口(百分比)、剩余、来源(「usage 实测 X + 估算 Y」或「全量估算」)、自动压缩阈值
5
+ * (窗口 − 预留,读会话的压缩设置与 `state.autoCompaction`)与档一裁剪起点;
6
+ * - 按类别:系统提示(按节)、工具声明(按工具)、用户消息、助手文本、推理、工具调用参数、工具结果
7
+ * (按工具名)、附件图片、摘要(压缩 / 分支)、自定义消息;每类 token 估算、占比与条形;
8
+ * - 底部:最大的 N 个工具结果,只有序号、工具名与大小,**绝不显示正文**。
9
+ *
10
+ * 统计只读:取活动分支的投影(与 `getStats().contextTokens` 同一口径),不改请求体、不写条目。
11
+ */
12
+ import { AgentSessionImpl } from "../agent/session.js";
13
+ import { DEFAULT_COMPACTION_SETTINGS, pendingPrefixMessage } from "../agent/session-compaction.js";
14
+ import { breakdownMessages, largestToolResults, } from "../compaction/breakdown.js";
15
+ import { estimateContextTokens, estimateProjectedTokens, } from "../compaction/estimate.js";
16
+ import { prunePolicy } from "../compaction/prune-tier.js";
17
+ import { msg } from "../i18n/index.js";
18
+ import { buildProjection } from "../session/projection.js";
19
+ import { Card, KeyValue, Meter, Text, } from "../tui.js";
20
+ import { Indent, Stack } from "./interactive/panels.js";
21
+ import { formatTokenCount, renderRows } from "./session-report.js";
22
+ /** 底部列出的工具结果个数。 */
23
+ export const CONTEXT_TOP_RESULTS = 5;
24
+ /** 每类最多列出的名字(节、工具…)。 */
25
+ const MAX_PARTS = 6;
26
+ /** 读会话的当前上下文(只读)。 */
27
+ export function contextSnapshot(session) {
28
+ let messages = session.messages;
29
+ let estimate;
30
+ let compaction = {};
31
+ let prefixPending = false;
32
+ if (session instanceof AgentSessionImpl) {
33
+ const branch = session.manager.branch();
34
+ const items = buildProjection(branch).items;
35
+ messages = items.map((item) => item.message);
36
+ estimate = estimateProjectedTokens(items, branch);
37
+ compaction = (session.options.compaction ?? {});
38
+ if (!messages.some((message) => "role" in message && message.role === "system")) {
39
+ prefixPending = true;
40
+ messages = [pendingPrefixMessage(session), ...messages];
41
+ }
42
+ }
43
+ else
44
+ estimate = estimateContextTokens(messages);
45
+ const stats = session.getStats();
46
+ // 「已用」与状态栏、getStats 同一口径(含启动基线)
47
+ if (stats.contextTokens !== undefined)
48
+ estimate = { ...estimate, tokens: stats.contextTokens };
49
+ const window = stats.contextWindow;
50
+ const reserve = compaction.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens;
51
+ const budget = window === undefined ? undefined : Math.max(0, window - reserve);
52
+ const percent = window === undefined || window <= 0
53
+ ? undefined
54
+ : Math.min(100, Math.round((estimate.tokens / window) * 1000) / 10);
55
+ return {
56
+ breakdown: breakdownMessages(messages),
57
+ estimate,
58
+ prefixPending,
59
+ window,
60
+ percent,
61
+ autoCompaction: session.state.autoCompaction,
62
+ compactAt: budget,
63
+ pruneAt: budget === undefined ? undefined : prunePolicy(budget, compaction.prune).triggerTokens,
64
+ };
65
+ }
66
+ function headerRows(snap) {
67
+ const m = msg().report.contextReport;
68
+ const { estimate, window } = snap;
69
+ const rows = [
70
+ {
71
+ key: m.keyUsed,
72
+ value: m.used(formatTokenCount(estimate.tokens), window === undefined ? "?" : formatTokenCount(window), snap.percent === undefined ? undefined : `${snap.percent.toFixed(1)}%`),
73
+ },
74
+ ];
75
+ if (window !== undefined)
76
+ rows.push({
77
+ key: m.keyLeft,
78
+ value: m.left(formatTokenCount(Math.max(0, window - estimate.tokens))),
79
+ });
80
+ rows.push({
81
+ key: m.keySource,
82
+ value: snap.prefixPending
83
+ ? m.sourcePrefix
84
+ : estimate.lastUsageIndex === null
85
+ ? m.sourceEstimate
86
+ : m.sourceUsage(formatTokenCount(estimate.usageTokens), formatTokenCount(estimate.trailingTokens)),
87
+ });
88
+ let auto;
89
+ if (!snap.autoCompaction)
90
+ auto = m.autoCompactOff;
91
+ else if (snap.compactAt === undefined)
92
+ auto = m.windowUnknown;
93
+ else if (estimate.tokens >= snap.compactAt)
94
+ auto = m.autoCompactReached(formatTokenCount(snap.compactAt));
95
+ else
96
+ auto = m.autoCompact(formatTokenCount(snap.compactAt), formatTokenCount(snap.compactAt - estimate.tokens));
97
+ rows.push({ key: m.keyAutoCompact, value: auto });
98
+ if (snap.autoCompaction && snap.pruneAt !== undefined)
99
+ rows.push({ key: m.keyPrune, value: m.prune(formatTokenCount(Math.floor(snap.pruneAt))) });
100
+ return rows;
101
+ }
102
+ function partName(entry, name) {
103
+ const names = msg().report.contextReport.partNames;
104
+ if (entry.category === "summaries" || entry.category === "images")
105
+ return names[name] ?? name;
106
+ return name;
107
+ }
108
+ function partsText(entry) {
109
+ const m = msg().report.contextReport;
110
+ const shown = entry.parts
111
+ .slice(0, MAX_PARTS)
112
+ .map((part) => `${partName(entry, part.name)} ${formatTokenCount(part.tokens)}`);
113
+ if (entry.parts.length > MAX_PARTS)
114
+ shown.push(m.more(entry.parts.length - MAX_PARTS));
115
+ if (entry.category === "images" && entry.count > 0)
116
+ shown.unshift(m.images(entry.count));
117
+ return shown.join(" · ");
118
+ }
119
+ function categoryRows(breakdown, theme) {
120
+ const m = msg().report.contextReport;
121
+ const dim = (text) => (theme === undefined ? text : theme.fg("dim", text));
122
+ return breakdown.entries.map((entry) => {
123
+ const ratio = breakdown.total > 0 ? entry.tokens / breakdown.total : undefined;
124
+ // 占比条只表示份额,不按阈值变色
125
+ const meter = new Meter(ratio, { warnAt: 2, dangerAt: 2, ...(theme ? { theme } : {}) });
126
+ const parts = partsText(entry);
127
+ return {
128
+ key: m.categories[entry.category],
129
+ value: `${formatTokenCount(entry.tokens).padStart(6)} ${meter.render(40)[0] ?? ""}` +
130
+ (parts === "" ? "" : ` ${dim(parts)}`),
131
+ };
132
+ });
133
+ }
134
+ function largestRows(breakdown) {
135
+ return largestToolResults(breakdown, CONTEXT_TOP_RESULTS).map((result) => ({
136
+ key: `#${result.ordinal}`,
137
+ value: `${formatTokenCount(result.tokens).padStart(6)} ${result.toolName}`,
138
+ }));
139
+ }
140
+ /** 纯文本(line 模式、无面板时的通知)。 */
141
+ export function describeContext(session) {
142
+ const m = msg().report.contextReport;
143
+ const snap = contextSnapshot(session);
144
+ const { breakdown } = snap;
145
+ const lines = [m.title, ...renderRows(headerRows(snap), " "), ""];
146
+ lines.push(m.breakdown(formatTokenCount(breakdown.total)));
147
+ lines.push(` ${snap.prefixPending ? m.prefixEstimated : breakdown.hasSystem ? m.prefixCounted : m.prefixPending}`);
148
+ if (breakdown.total === 0)
149
+ lines.push(` ${m.empty}`);
150
+ else
151
+ lines.push(...renderRows(categoryRows(breakdown, undefined), " "));
152
+ lines.push("", m.largest(CONTEXT_TOP_RESULTS));
153
+ const largest = largestRows(breakdown);
154
+ if (largest.length === 0)
155
+ lines.push(` ${m.noToolResults}`);
156
+ else
157
+ lines.push(...renderRows(largest, " "));
158
+ return lines.join("\n");
159
+ }
160
+ /** 交互模式的消息区面板(左竖条卡片)。 */
161
+ export function contextPanel(session, theme) {
162
+ const m = msg().report.contextReport;
163
+ const snap = contextSnapshot(session);
164
+ const { breakdown } = snap;
165
+ const table = (rows, wrap = false) => new KeyValue(rows, { theme, maxKeyRatio: 0.3, ...(wrap ? { wrap: true } : {}) });
166
+ const largest = largestRows(breakdown);
167
+ const parts = [
168
+ table(headerRows(snap)),
169
+ "",
170
+ theme.bold(m.breakdown(formatTokenCount(breakdown.total))),
171
+ new Indent(new Stack([
172
+ new Text(theme.fg("dim", snap.prefixPending
173
+ ? m.prefixEstimated
174
+ : breakdown.hasSystem
175
+ ? m.prefixCounted
176
+ : m.prefixPending)),
177
+ breakdown.total === 0
178
+ ? theme.fg("dim", m.empty)
179
+ : table(categoryRows(breakdown, theme), true),
180
+ ]), 2),
181
+ "",
182
+ theme.bold(m.largest(CONTEXT_TOP_RESULTS)),
183
+ largest.length === 0
184
+ ? new Indent(new Stack([theme.fg("dim", m.noToolResults)]), 2)
185
+ : new Indent(table(largest), 2),
186
+ ];
187
+ return new Card(new Stack(parts), { theme, title: m.title });
188
+ }
@@ -37,6 +37,8 @@ export interface BarRow {
37
37
  turns: number;
38
38
  tool?: string;
39
39
  elapsedMs?: number;
40
+ /** 外部 Agent 报告的上下文占用(已格式化,如 `34%`)。 */
41
+ context?: string;
40
42
  }
41
43
  /** 栏与视图读的任务注册表(当前会话的 `registryOf(sessionId)`;测试给假的)。 */
42
44
  export type TaskSource = Pick<SubagentRegistry, "list" | "get" | "live" | "message">;
@@ -19,6 +19,7 @@
19
19
  * - 状态来自任务注册表(`TaskInfo`、`live().queued`)、子 Agent 折叠视图的跟踪(轮数、最近工具、用时,界面时钟)
20
20
  * 与 `permission_request.context.taskId`(等审批)。
21
21
  */
22
+ import { formatTaskContext } from "../../agents/task-record.js";
22
23
  import { msg } from "../../i18n/index.js";
23
24
  import { formatElapsed, truncateToWidth, visibleWidth, } from "../../tui.js";
24
25
  export const BAR_ROWS = 3;
@@ -71,6 +72,9 @@ export function taskRow(info, ctx) {
71
72
  status,
72
73
  turns: Math.max(state?.turns ?? 0, info.turns ?? 0),
73
74
  };
75
+ const context = formatTaskContext(info);
76
+ if (context !== undefined)
77
+ row.context = context;
74
78
  const tool = state?.tools.at(-1);
75
79
  if (tool !== undefined && status !== "queued")
76
80
  row.tool = tool;
@@ -95,6 +99,8 @@ export function rowFacts(row, withTool = true) {
95
99
  facts[0] += ` ${formatElapsed(row.elapsedMs)}`;
96
100
  if (row.turns > 0)
97
101
  facts.push(msg().agents.turns(row.turns));
102
+ if (row.context !== undefined)
103
+ facts.push(msg().agents.context(row.context));
98
104
  if (withTool && row.tool !== undefined && row.status === "running")
99
105
  facts.push(row.tool);
100
106
  return facts.join(" · ");
@@ -6,7 +6,8 @@
6
6
  * 之前分叉,消息文本回填编辑器)、权限模式、思考级别;
7
7
  * - 交互模式自有命令:`/tree`(同一文件内换叶子到选中消息之前,文本回填编辑器,可改后重发形成新分支)、
8
8
  * `/permissions`(当前模式、判定顺序与已加载规则);`/help` 追加这两条与按键说明。
9
- * - `/session`、`/cache`(无参数)与 `/permissions` 在消息区画左竖条面板(panels.ts),不再拍成文本。
9
+ * - `/session`、`/cache`(无参数)与 `/permissions` 在消息区画左竖条面板(panels.ts),不再拍成文本;
10
+ * `/context` 同样画面板(context-report.ts,`PANEL_COMMANDS`),没有面板时回落文本。
10
11
  * - `/rewind`(无参数)打开回滚列表与确认面板(rewind-flow.ts);带参数走 commands-core,对话变了时
11
12
  * 重画消息区并回填原消息。
12
13
  * - 不是命令(含模板与 `/skill:`)返回 false,调用方把整行当提示发出。
@@ -6,7 +6,8 @@
6
6
  * 之前分叉,消息文本回填编辑器)、权限模式、思考级别;
7
7
  * - 交互模式自有命令:`/tree`(同一文件内换叶子到选中消息之前,文本回填编辑器,可改后重发形成新分支)、
8
8
  * `/permissions`(当前模式、判定顺序与已加载规则);`/help` 追加这两条与按键说明。
9
- * - `/session`、`/cache`(无参数)与 `/permissions` 在消息区画左竖条面板(panels.ts),不再拍成文本。
9
+ * - `/session`、`/cache`(无参数)与 `/permissions` 在消息区画左竖条面板(panels.ts),不再拍成文本;
10
+ * `/context` 同样画面板(context-report.ts,`PANEL_COMMANDS`),没有面板时回落文本。
10
11
  * - `/rewind`(无参数)打开回滚列表与确认面板(rewind-flow.ts);带参数走 commands-core,对话变了时
11
12
  * 重画消息区并回填原消息。
12
13
  * - 不是命令(含模板与 `/skill:`)返回 false,调用方把整行当提示发出。
@@ -22,6 +23,7 @@ import { listSessions } from "../../cli/compose-store.js";
22
23
  import { hideFakeProvider } from "../../cli/fake-visibility.js";
23
24
  import { autoLayerText, permissionModeLabel } from "../../permissions/modes.js";
24
25
  import { BUILTIN_COMMANDS, parseSlash, runSlashCommand, } from "../commands-core.js";
26
+ import { contextPanel } from "../context-report.js";
25
27
  import { contentText } from "./message-view.js";
26
28
  import { cachePanel, permissionsPanel, sessionPanel } from "./panels.js";
27
29
  import { modelItems, permissionPickerSpec, sessionItems, thinkingItems, treeItems, } from "./pickers.js";
@@ -60,6 +62,13 @@ export const PANEL_COMMANDS = {
60
62
  await ui.traceView(args === "" ? undefined : args);
61
63
  return true;
62
64
  },
65
+ context: async (ui) => {
66
+ const theme = ui.theme?.();
67
+ if (ui.panel === undefined || theme === undefined)
68
+ return false;
69
+ ui.panel(contextPanel(ui.session(), theme));
70
+ return true;
71
+ },
63
72
  memory: async (ui, args) => {
64
73
  if (ui.memoryPanel === undefined)
65
74
  return false;
@@ -30,6 +30,7 @@ import { Editor, TUI, type Keybindings, type Terminal, type Theme } from "../../
30
30
  import { MessageView } from "./message-view.js";
31
31
  import { StatusArea } from "./status-area.js";
32
32
  import type { StatusBar } from "./status-bar.js";
33
+ import { type ProgramStatusOptions } from "./program-status-ui.js";
33
34
  import { ToolTracker } from "./tool-view.js";
34
35
  export interface InteractiveModeOptions {
35
36
  /** 缺省 `ProcessTerminal`(测试注入 MemoryTerminal)。 */
@@ -48,6 +49,8 @@ export interface InteractiveModeOptions {
48
49
  onReady?(handle: InteractiveHandle): void;
49
50
  /** 剪贴板读取的注入(测试;缺省调系统命令)。 */
50
51
  clipboard?: ClipboardDeps;
52
+ /** 终端程序状态 OSC 7501(缺省只在真实终端启用;测试注入终端时给了才启用)。 */
53
+ programStatus?: ProgramStatusOptions;
51
54
  }
52
55
  export interface InteractiveHandle {
53
56
  readonly tui: TUI;
@@ -50,6 +50,7 @@ import { playStartupAnimation } from "./startup-logo.js";
50
50
  import { QueueView, RunIndicator } from "./run-indicator.js";
51
51
  import { StatusArea, statusLineSlash } from "./status-area.js";
52
52
  import { createSessionEventHandler } from "./session-events.js";
53
+ import { programStatusUi } from "./program-status-ui.js";
53
54
  import { SubagentTracker } from "./subagent-view.js";
54
55
  import { loadKeys, processTerminal } from "./terminal-setup.js";
55
56
  import { ToolTracker } from "./tool-view.js";
@@ -70,6 +71,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
70
71
  const keys = options.keybindings ?? loadKeys(runtime, (m) => startupWarnings.push(m));
71
72
  let session = currentSession(runtime);
72
73
  const tui = new TUI(terminal);
74
+ const osc7501 = programStatusUi({ tui, runtime, env, options, cwd: () => session.state.cwd });
73
75
  const view = new MessageView({
74
76
  theme,
75
77
  ...(ui.showThinking !== undefined ? { showThinking: ui.showThinking } : {}),
@@ -174,7 +176,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
174
176
  render();
175
177
  };
176
178
  // ---- 会话事件 -------------------------------------------------------------
177
- const onEvent = createSessionEventHandler({
179
+ const handleEvent = createSessionEventHandler({
178
180
  view,
179
181
  tools,
180
182
  status,
@@ -186,6 +188,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
186
188
  notice,
187
189
  render,
188
190
  });
191
+ const onEvent = osc7501.wrap(handleEvent);
189
192
  let unsubscribe = session.subscribe(onEvent);
190
193
  const startupLevel = startupScreenLevel(runtime);
191
194
  const home = env["HOME"] ?? env["USERPROFILE"];
@@ -423,6 +426,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
423
426
  clearTimeout(hintTimer);
424
427
  loader.stop();
425
428
  area.dispose();
429
+ osc7501.exit();
426
430
  // 退出摘要留在回滚里;输入框、状态栏等底部区域撤掉,屏幕停在摘要下面
427
431
  view.addExitSummary(exitSummaryLines(session.getStats(), now() - startedAt, theme));
428
432
  tui.clear();
@@ -460,6 +464,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
460
464
  reject(new AmaError("terminal_init_failed", msg().interactive.app.terminalInitFailed(error instanceof Error ? error.message : String(error)), { cause: error }));
461
465
  return;
462
466
  }
467
+ osc7501.start();
463
468
  runtime.approvals.setUiBroker(broker);
464
469
  runtime.notifier.set((message, level) => notice(level, message));
465
470
  agentUi.attach(session);
@@ -2,7 +2,8 @@
2
2
  * 消息区面板(终端界面视觉设计 v1 §3.13):`/session`、`/cache`、`/permissions` 用左竖条卡片 + 键值表
3
3
  * 渲染,而不是把纯文本拍进通知。line 模式与 RPC 仍用 session-report 的文本版。
4
4
  *
5
- * - `/session`:标题 `会话 <id 前 8 位> · <文件>`;模型 / 消息 / 用量 / 上下文(余量表,阈值着色),
5
+ * - `/session`:标题 `会话 <id 前 8 位> · <文件>`;模型 / 消息 / 累计用量 / 上下文(余量表,阈值着色与档一
6
+ * 裁剪阈值对齐;估算值带 `≈`;末尾是距自动压缩的余量或「自动压缩关闭」),
6
7
  * 空一行后「缓存」段(与 `/cache` 共用 `cacheRows`);
7
8
  * - [W5-U] `/session` 多「子 Agent」行(任务汇总)与「外部 Agent」段(按 Agent 的运行次数与用量,单位不换算);
8
9
  * - `/permissions`:权限模式、判定顺序(折行对齐值列)、规则(allow success、deny error、来源 dim)、
@@ -2,7 +2,8 @@
2
2
  * 消息区面板(终端界面视觉设计 v1 §3.13):`/session`、`/cache`、`/permissions` 用左竖条卡片 + 键值表
3
3
  * 渲染,而不是把纯文本拍进通知。line 模式与 RPC 仍用 session-report 的文本版。
4
4
  *
5
- * - `/session`:标题 `会话 <id 前 8 位> · <文件>`;模型 / 消息 / 用量 / 上下文(余量表,阈值着色),
5
+ * - `/session`:标题 `会话 <id 前 8 位> · <文件>`;模型 / 消息 / 累计用量 / 上下文(余量表,阈值着色与档一
6
+ * 裁剪阈值对齐;估算值带 `≈`;末尾是距自动压缩的余量或「自动压缩关闭」),
6
7
  * 空一行后「缓存」段(与 `/cache` 共用 `cacheRows`);
7
8
  * - [W5-U] `/session` 多「子 Agent」行(任务汇总)与「外部 Agent」段(按 Agent 的运行次数与用量,单位不换算);
8
9
  * - `/permissions`:权限模式、判定顺序(折行对齐值列)、规则(allow success、deny error、来源 dim)、
@@ -11,7 +12,8 @@
11
12
  import { msg } from "../../i18n/index.js";
12
13
  import { autoLayerText, permissionModeLabel } from "../../permissions/modes.js";
13
14
  import { Card, KeyValue, Meter, } from "../../tui.js";
14
- import { cacheRows, externalRows, formatTokenCount, formatUsd, taskStatsText, } from "../session-report.js";
15
+ import { ctxPercentText, ctxWarnAt } from "./status-ctx.js";
16
+ import { autoCompactText, cacheRows, contextEstimated, externalRows, formatTokenCount, formatUsd, taskStatsText, } from "../session-report.js";
15
17
  import { tildePath } from "../../cli/startup-screen.js";
16
18
  /** 多段内容按顺序拼起来(段之间不加空行,由调用方放空串)。 */
17
19
  export class Stack {
@@ -64,12 +66,18 @@ export function sessionPanel(session, theme, options = {}) {
64
66
  const sep = theme.fg("dim", " · ");
65
67
  const model = state.model === undefined ? "?" : `${state.model.provider}/${state.model.id}`;
66
68
  const ratio = stats.contextPercent === undefined ? undefined : stats.contextPercent / 100;
67
- const meter = new Meter(ratio, { theme }).render(40)[0] ?? "";
69
+ const approx = contextEstimated(stats) ? "≈" : "";
70
+ const meter = new Meter(ratio, {
71
+ theme,
72
+ warnAt: ctxWarnAt(stats),
73
+ percent: (r) => approx + ctxPercentText(r * 100, false),
74
+ }).render(40)[0] ?? "";
68
75
  const window = stats.contextTokens === undefined && stats.contextWindow === undefined
69
76
  ? ""
70
77
  : sep +
71
- `${stats.contextTokens === undefined ? "?" : formatTokenCount(stats.contextTokens)} / ` +
78
+ `${stats.contextTokens === undefined ? "?" : approx + formatTokenCount(stats.contextTokens)} / ` +
72
79
  `${stats.contextWindow === undefined ? "?" : formatTokenCount(stats.contextWindow)}`;
80
+ const auto = autoCompactText(stats);
73
81
  const rows = [
74
82
  {
75
83
  key: m.model,
@@ -97,7 +105,7 @@ export function sessionPanel(session, theme, options = {}) {
97
105
  formatUsd(stats.cost),
98
106
  ].join(sep),
99
107
  },
100
- { key: m.context, value: meter + window },
108
+ { key: m.context, value: meter + window + (auto === undefined ? "" : sep + auto) },
101
109
  ];
102
110
  const tasks = taskStatsText(session);
103
111
  if (tasks !== undefined)
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 交互界面的终端程序状态(OSC 7501,src/tui/program-status.ts):会话事件 → 根记录与子任务记录。
3
+ *
4
+ * 根记录(优先级自上而下):
5
+ * 有待人处理的审批 → blocked:permission(msg = 工具 · 目标)
6
+ * 有待审批的计划 → blocked:question
7
+ * 上一回合因登录失效失败(`auth_expired`)→ blocked:auth(下一回合开始或换模型时解除)
8
+ * 回合进行中 → working(msg = 思考中 / 回复中 / 运行 bash / 压缩上下文,节流 + 同值去重)
9
+ * 回合结束 → done;出错 → error(msg = 错误首行);用户中断 → idle;之后用户再按键 → idle
10
+ * 启动 / 切换会话 → idle
11
+ * 子记录:子 Agent 任务 `id=task/<短 id>`、title = 任务描述,working / blocked / done / error(中断为 idle);
12
+ * 下一个主回合开始时清掉已结束的任务记录。退出交互模式前发不带 id 的 `state=clear`(用户主动退出,
13
+ * 不留记录)。
14
+ *
15
+ * 只在交互 TUI 里启用;print / rpc / acp 的 stdout 是数据或协议,不发。
16
+ */
17
+ import type { SessionEvent } from "../../agent/types.js";
18
+ import type { Runtime } from "../../cli/runtime.js";
19
+ import { ProgramStatusEmitter, type ProgramStatusReport } from "../../tui/program-status.js";
20
+ import type { TUI } from "../../tui.js";
21
+ export interface ProgramStatusOptions {
22
+ /** 检测超时(ms),缺省 300。 */
23
+ timeoutMs?: number;
24
+ /** 同一状态下 msg 更新的最小间隔(ms),缺省 500。 */
25
+ throttleMs?: number;
26
+ }
27
+ /** 会话事件 → 状态记录(只读事件,不改会话)。 */
28
+ export declare class ProgramStatusTracker {
29
+ private readonly emitter;
30
+ private readonly cwd;
31
+ private running;
32
+ private activity;
33
+ private readonly approvals;
34
+ private plan;
35
+ private auth;
36
+ private outcome;
37
+ private lastError;
38
+ private readonly tasks;
39
+ constructor(emitter: Pick<ProgramStatusEmitter, "report">, cwd?: () => string | undefined);
40
+ onEvent(event: SessionEvent): void;
41
+ /** 用户按键:已看到的 done / error 回到 idle。 */
42
+ acknowledge(): void;
43
+ /** 当前根记录(测试也用)。 */
44
+ root(): ProgramStatusReport;
45
+ /** 写出全部记录(发射器同值去重)。 */
46
+ sync(): void;
47
+ private outcomeOf;
48
+ private dropTask;
49
+ private reset;
50
+ }
51
+ export interface ProgramStatusUi {
52
+ /** 包一层会话事件处理:先更新状态记录,再交给原处理。 */
53
+ wrap(handler: (event: SessionEvent) => void): (event: SessionEvent) => void;
54
+ /** 终端进入 raw 模式之后:检测(auto)并写初始 idle。 */
55
+ start(): void;
56
+ /** 退出交互模式前:清掉全部记录、停计时器。 */
57
+ exit(): void;
58
+ }
59
+ /**
60
+ * 交互模式装配。缺省只在真实终端(没有注入 `terminal`)启用;测试注入终端时给了 `programStatus` 才启用,
61
+ * 其余情况与 `ui.programStatus: "off"` 一样全部空操作。
62
+ */
63
+ export declare function programStatusUi(deps: {
64
+ tui: TUI;
65
+ runtime: Runtime;
66
+ env: NodeJS.ProcessEnv;
67
+ options: {
68
+ terminal?: unknown;
69
+ programStatus?: ProgramStatusOptions;
70
+ };
71
+ cwd(): string | undefined;
72
+ }): ProgramStatusUi;