@armadra/agent 0.7.1 → 0.7.2
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.
- package/CHANGELOG.md +57 -0
- package/CHANGELOG.zh-CN.md +34 -0
- package/dist/agent/session-cache.d.ts +4 -2
- package/dist/agent/session-cache.js +5 -3
- package/dist/agent/session-compaction.d.ts +13 -1
- package/dist/agent/session-compaction.js +36 -0
- package/dist/agent/session-core.d.ts +2 -0
- package/dist/agent/session-state.d.ts +9 -3
- package/dist/agent/session-state.js +8 -6
- package/dist/agent/session.js +3 -3
- package/dist/agent/types-w5.d.ts +3 -0
- package/dist/agent/types.d.ts +18 -0
- package/dist/agents/task-record.d.ts +4 -0
- package/dist/agents/task-record.js +18 -0
- package/dist/ai/context.d.ts +7 -2
- package/dist/ai/context.js +45 -7
- package/dist/ai/providers/catalog-data.js +1 -1
- package/dist/bundle/ama.cjs +1643 -486
- package/dist/compaction/breakdown.d.ts +51 -0
- package/dist/compaction/breakdown.js +159 -0
- package/dist/compaction/estimate.d.ts +6 -0
- package/dist/compaction/estimate.js +1 -1
- package/dist/compaction/prefix-estimate.d.ts +23 -0
- package/dist/compaction/prefix-estimate.js +34 -0
- package/dist/config/json-schema.js +1 -0
- package/dist/config/key-docs.js +1 -0
- package/dist/config/schema.js +2 -0
- package/dist/config/settings-registry.js +1 -0
- package/dist/config/types.d.ts +2 -0
- package/dist/drivers/native/codex-app-server.js +6 -0
- package/dist/drivers/runner.js +17 -0
- package/dist/drivers/store.d.ts +3 -0
- package/dist/drivers/store.js +4 -0
- package/dist/i18n/catalog.d.ts +53 -0
- package/dist/i18n/messages/agents.d.ts +3 -0
- package/dist/i18n/messages/agents.js +3 -0
- package/dist/i18n/messages/config-keys.d.ts +2 -0
- package/dist/i18n/messages/config-keys.js +2 -0
- package/dist/i18n/messages/config.d.ts +2 -0
- package/dist/i18n/messages/interactive.d.ts +6 -0
- package/dist/i18n/messages/interactive.js +6 -0
- package/dist/i18n/messages/panels.d.ts +3 -0
- package/dist/i18n/messages/panels.js +5 -2
- package/dist/i18n/messages/report-context.d.ts +94 -0
- package/dist/i18n/messages/report-context.js +95 -0
- package/dist/i18n/messages/report.d.ts +96 -0
- package/dist/i18n/messages/report.js +13 -2
- package/dist/i18n/messages/settings.d.ts +2 -0
- package/dist/i18n/messages/settings.js +2 -0
- package/dist/i18n/messages/subcommands.js +2 -2
- package/dist/modes/acp/acp-events.d.ts +17 -4
- package/dist/modes/acp/acp-events.js +32 -6
- package/dist/modes/commands-core.js +4 -0
- package/dist/modes/context-report.d.ts +35 -0
- package/dist/modes/context-report.js +172 -0
- package/dist/modes/interactive/agent-bar.d.ts +2 -0
- package/dist/modes/interactive/agent-bar.js +6 -0
- package/dist/modes/interactive/commands.d.ts +2 -1
- package/dist/modes/interactive/commands.js +10 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.js +6 -1
- package/dist/modes/interactive/panels.d.ts +2 -1
- package/dist/modes/interactive/panels.js +13 -5
- package/dist/modes/interactive/program-status-ui.d.ts +72 -0
- package/dist/modes/interactive/program-status-ui.js +229 -0
- package/dist/modes/interactive/session-events.d.ts +4 -0
- package/dist/modes/interactive/session-events.js +24 -2
- package/dist/modes/interactive/status-bar.d.ts +21 -3
- package/dist/modes/interactive/status-bar.js +64 -25
- package/dist/modes/interactive/status-ctx.d.ts +55 -0
- package/dist/modes/interactive/status-ctx.js +125 -0
- package/dist/modes/interactive/status-line.d.ts +2 -1
- package/dist/modes/interactive/status-line.js +3 -2
- package/dist/modes/interactive/tasks-report.js +4 -0
- package/dist/modes/print/print-mode.js +12 -0
- package/dist/modes/session-report.d.ts +10 -2
- package/dist/modes/session-report.js +26 -2
- package/dist/sdk.d.ts +1 -1
- package/dist/tools/types.d.ts +6 -0
- package/dist/tui/components/meter.d.ts +2 -0
- package/dist/tui/components/meter.js +1 -1
- package/dist/tui/program-status.d.ts +90 -0
- package/dist/tui/program-status.js +259 -0
- package/docs/acp.md +4 -4
- package/docs/agents.md +1 -1
- package/docs/en/acp.md +4 -4
- package/docs/en/providers.md +5 -3
- package/docs/en/rpc.md +3 -0
- package/docs/en/sessions.md +23 -0
- package/docs/en/tui.md +79 -22
- package/docs/providers.md +8 -5
- package/docs/rpc.md +1 -0
- package/docs/session-format.md +3 -3
- package/docs/sessions.md +22 -0
- package/docs/tui-design.md +2 -2
- package/docs/tui.md +79 -22
- 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
|
|
15
|
+
* | message_end(助手)/ turn_end | usage_update(上下文用量、窗口、会话累计美元;与上次同值不发) |
|
|
16
16
|
* | permission_mode_changed | current_mode_update + config_option_update |
|
|
17
|
-
* | model_changed
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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,35 @@
|
|
|
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
|
+
window: number | undefined;
|
|
22
|
+
/** 0–100(一位小数,封顶 100);无窗口时 undefined。 */
|
|
23
|
+
percent: number | undefined;
|
|
24
|
+
autoCompaction: boolean;
|
|
25
|
+
/** 档二自动压缩的触发点(窗口 − 预留);无窗口时 undefined。 */
|
|
26
|
+
compactAt: number | undefined;
|
|
27
|
+
/** 档一裁剪的触发点;无窗口时 undefined。 */
|
|
28
|
+
pruneAt: number | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** 读会话的当前上下文(只读)。 */
|
|
31
|
+
export declare function contextSnapshot(session: AgentSession): ContextSnapshot;
|
|
32
|
+
/** 纯文本(line 模式、无面板时的通知)。 */
|
|
33
|
+
export declare function describeContext(session: AgentSession): string;
|
|
34
|
+
/** 交互模式的消息区面板(左竖条卡片)。 */
|
|
35
|
+
export declare function contextPanel(session: AgentSession, theme: Theme): Component;
|
|
@@ -0,0 +1,172 @@
|
|
|
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 } 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
|
+
if (session instanceof AgentSessionImpl) {
|
|
32
|
+
const branch = session.manager.branch();
|
|
33
|
+
const items = buildProjection(branch).items;
|
|
34
|
+
messages = items.map((item) => item.message);
|
|
35
|
+
estimate = estimateProjectedTokens(items, branch);
|
|
36
|
+
compaction = (session.options.compaction ?? {});
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
estimate = estimateContextTokens(messages);
|
|
40
|
+
const window = session.getStats().contextWindow;
|
|
41
|
+
const reserve = compaction.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens;
|
|
42
|
+
const budget = window === undefined ? undefined : Math.max(0, window - reserve);
|
|
43
|
+
const percent = window === undefined || window <= 0
|
|
44
|
+
? undefined
|
|
45
|
+
: Math.min(100, Math.round((estimate.tokens / window) * 1000) / 10);
|
|
46
|
+
return {
|
|
47
|
+
breakdown: breakdownMessages(messages),
|
|
48
|
+
estimate,
|
|
49
|
+
window,
|
|
50
|
+
percent,
|
|
51
|
+
autoCompaction: session.state.autoCompaction,
|
|
52
|
+
compactAt: budget,
|
|
53
|
+
pruneAt: budget === undefined ? undefined : prunePolicy(budget, compaction.prune).triggerTokens,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function headerRows(snap) {
|
|
57
|
+
const m = msg().report.contextReport;
|
|
58
|
+
const { estimate, window } = snap;
|
|
59
|
+
const rows = [
|
|
60
|
+
{
|
|
61
|
+
key: m.keyUsed,
|
|
62
|
+
value: m.used(formatTokenCount(estimate.tokens), window === undefined ? "?" : formatTokenCount(window), snap.percent === undefined ? undefined : `${snap.percent.toFixed(1)}%`),
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
if (window !== undefined)
|
|
66
|
+
rows.push({
|
|
67
|
+
key: m.keyLeft,
|
|
68
|
+
value: m.left(formatTokenCount(Math.max(0, window - estimate.tokens))),
|
|
69
|
+
});
|
|
70
|
+
rows.push({
|
|
71
|
+
key: m.keySource,
|
|
72
|
+
value: estimate.lastUsageIndex === null
|
|
73
|
+
? m.sourceEstimate
|
|
74
|
+
: m.sourceUsage(formatTokenCount(estimate.usageTokens), formatTokenCount(estimate.trailingTokens)),
|
|
75
|
+
});
|
|
76
|
+
let auto;
|
|
77
|
+
if (!snap.autoCompaction)
|
|
78
|
+
auto = m.autoCompactOff;
|
|
79
|
+
else if (snap.compactAt === undefined)
|
|
80
|
+
auto = m.windowUnknown;
|
|
81
|
+
else if (estimate.tokens >= snap.compactAt)
|
|
82
|
+
auto = m.autoCompactReached(formatTokenCount(snap.compactAt));
|
|
83
|
+
else
|
|
84
|
+
auto = m.autoCompact(formatTokenCount(snap.compactAt), formatTokenCount(snap.compactAt - estimate.tokens));
|
|
85
|
+
rows.push({ key: m.keyAutoCompact, value: auto });
|
|
86
|
+
if (snap.autoCompaction && snap.pruneAt !== undefined)
|
|
87
|
+
rows.push({ key: m.keyPrune, value: m.prune(formatTokenCount(Math.floor(snap.pruneAt))) });
|
|
88
|
+
return rows;
|
|
89
|
+
}
|
|
90
|
+
function partName(entry, name) {
|
|
91
|
+
const names = msg().report.contextReport.partNames;
|
|
92
|
+
if (entry.category === "summaries" || entry.category === "images")
|
|
93
|
+
return names[name] ?? name;
|
|
94
|
+
return name;
|
|
95
|
+
}
|
|
96
|
+
function partsText(entry) {
|
|
97
|
+
const m = msg().report.contextReport;
|
|
98
|
+
const shown = entry.parts
|
|
99
|
+
.slice(0, MAX_PARTS)
|
|
100
|
+
.map((part) => `${partName(entry, part.name)} ${formatTokenCount(part.tokens)}`);
|
|
101
|
+
if (entry.parts.length > MAX_PARTS)
|
|
102
|
+
shown.push(m.more(entry.parts.length - MAX_PARTS));
|
|
103
|
+
if (entry.category === "images" && entry.count > 0)
|
|
104
|
+
shown.unshift(m.images(entry.count));
|
|
105
|
+
return shown.join(" · ");
|
|
106
|
+
}
|
|
107
|
+
function categoryRows(breakdown, theme) {
|
|
108
|
+
const m = msg().report.contextReport;
|
|
109
|
+
const dim = (text) => (theme === undefined ? text : theme.fg("dim", text));
|
|
110
|
+
return breakdown.entries.map((entry) => {
|
|
111
|
+
const ratio = breakdown.total > 0 ? entry.tokens / breakdown.total : undefined;
|
|
112
|
+
// 占比条只表示份额,不按阈值变色
|
|
113
|
+
const meter = new Meter(ratio, { warnAt: 2, dangerAt: 2, ...(theme ? { theme } : {}) });
|
|
114
|
+
const parts = partsText(entry);
|
|
115
|
+
return {
|
|
116
|
+
key: m.categories[entry.category],
|
|
117
|
+
value: `${formatTokenCount(entry.tokens).padStart(6)} ${meter.render(40)[0] ?? ""}` +
|
|
118
|
+
(parts === "" ? "" : ` ${dim(parts)}`),
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function largestRows(breakdown) {
|
|
123
|
+
return largestToolResults(breakdown, CONTEXT_TOP_RESULTS).map((result) => ({
|
|
124
|
+
key: `#${result.ordinal}`,
|
|
125
|
+
value: `${formatTokenCount(result.tokens).padStart(6)} ${result.toolName}`,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
/** 纯文本(line 模式、无面板时的通知)。 */
|
|
129
|
+
export function describeContext(session) {
|
|
130
|
+
const m = msg().report.contextReport;
|
|
131
|
+
const snap = contextSnapshot(session);
|
|
132
|
+
const { breakdown } = snap;
|
|
133
|
+
const lines = [m.title, ...renderRows(headerRows(snap), " "), ""];
|
|
134
|
+
lines.push(m.breakdown(formatTokenCount(breakdown.total)));
|
|
135
|
+
lines.push(` ${breakdown.hasSystem ? m.prefixCounted : m.prefixPending}`);
|
|
136
|
+
if (breakdown.total === 0)
|
|
137
|
+
lines.push(` ${m.empty}`);
|
|
138
|
+
else
|
|
139
|
+
lines.push(...renderRows(categoryRows(breakdown, undefined), " "));
|
|
140
|
+
lines.push("", m.largest(CONTEXT_TOP_RESULTS));
|
|
141
|
+
const largest = largestRows(breakdown);
|
|
142
|
+
if (largest.length === 0)
|
|
143
|
+
lines.push(` ${m.noToolResults}`);
|
|
144
|
+
else
|
|
145
|
+
lines.push(...renderRows(largest, " "));
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
/** 交互模式的消息区面板(左竖条卡片)。 */
|
|
149
|
+
export function contextPanel(session, theme) {
|
|
150
|
+
const m = msg().report.contextReport;
|
|
151
|
+
const snap = contextSnapshot(session);
|
|
152
|
+
const { breakdown } = snap;
|
|
153
|
+
const table = (rows, wrap = false) => new KeyValue(rows, { theme, maxKeyRatio: 0.3, ...(wrap ? { wrap: true } : {}) });
|
|
154
|
+
const largest = largestRows(breakdown);
|
|
155
|
+
const parts = [
|
|
156
|
+
table(headerRows(snap)),
|
|
157
|
+
"",
|
|
158
|
+
theme.bold(m.breakdown(formatTokenCount(breakdown.total))),
|
|
159
|
+
new Indent(new Stack([
|
|
160
|
+
new Text(theme.fg("dim", breakdown.hasSystem ? m.prefixCounted : m.prefixPending)),
|
|
161
|
+
breakdown.total === 0
|
|
162
|
+
? theme.fg("dim", m.empty)
|
|
163
|
+
: table(categoryRows(breakdown, theme), true),
|
|
164
|
+
]), 2),
|
|
165
|
+
"",
|
|
166
|
+
theme.bold(m.largest(CONTEXT_TOP_RESULTS)),
|
|
167
|
+
largest.length === 0
|
|
168
|
+
? new Indent(new Stack([theme.fg("dim", m.noToolResults)]), 2)
|
|
169
|
+
: new Indent(table(largest), 2),
|
|
170
|
+
];
|
|
171
|
+
return new Card(new Stack(parts), { theme, title: m.title });
|
|
172
|
+
}
|
|
@@ -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
|
|
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 {
|
|
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
|
|
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;
|