@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
|
@@ -0,0 +1,229 @@
|
|
|
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 { msg } from "../../i18n/index.js";
|
|
18
|
+
import { ProgramStatusEmitter, idSegment, } from "../../tui/program-status.js";
|
|
19
|
+
import { flat, toolSummary } from "./tool-summary.js";
|
|
20
|
+
const AUTH_EXPIRED = /\bauth_expired\b/;
|
|
21
|
+
/** 会话事件 → 状态记录(只读事件,不改会话)。 */
|
|
22
|
+
export class ProgramStatusTracker {
|
|
23
|
+
emitter;
|
|
24
|
+
cwd;
|
|
25
|
+
running = false;
|
|
26
|
+
activity;
|
|
27
|
+
approvals = new Map();
|
|
28
|
+
plan;
|
|
29
|
+
auth;
|
|
30
|
+
outcome;
|
|
31
|
+
lastError;
|
|
32
|
+
tasks = new Map();
|
|
33
|
+
constructor(emitter, cwd = () => undefined) {
|
|
34
|
+
this.emitter = emitter;
|
|
35
|
+
this.cwd = cwd;
|
|
36
|
+
}
|
|
37
|
+
onEvent(event) {
|
|
38
|
+
const run = msg().interactive.view.run;
|
|
39
|
+
switch (event.type) {
|
|
40
|
+
case "session_start":
|
|
41
|
+
if (event.reason !== "startup")
|
|
42
|
+
this.reset();
|
|
43
|
+
break;
|
|
44
|
+
case "agent_start":
|
|
45
|
+
this.running = true;
|
|
46
|
+
this.activity = run.thinking;
|
|
47
|
+
this.outcome = this.auth = this.lastError = undefined;
|
|
48
|
+
for (const [key, task] of this.tasks)
|
|
49
|
+
if (task.state !== "working" && task.state !== "blocked")
|
|
50
|
+
this.dropTask(key);
|
|
51
|
+
break;
|
|
52
|
+
case "message_update": {
|
|
53
|
+
const last = event.message.content.at(-1);
|
|
54
|
+
this.activity = last?.type === "text" ? run.replying : run.thinking;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case "tool_execution_start":
|
|
58
|
+
if (event.parentToolCallId === undefined)
|
|
59
|
+
this.activity = run.runningTool(event.toolName);
|
|
60
|
+
break;
|
|
61
|
+
case "compaction_start":
|
|
62
|
+
this.activity = run.compacting;
|
|
63
|
+
break;
|
|
64
|
+
case "message_end":
|
|
65
|
+
if (event.message.role === "assistant" && event.message.stopReason === "error")
|
|
66
|
+
this.lastError = event.message.errorMessage ?? "error";
|
|
67
|
+
break;
|
|
68
|
+
case "agent_end":
|
|
69
|
+
if (!event.willRetry)
|
|
70
|
+
this.outcome = this.outcomeOf(event.stopReason);
|
|
71
|
+
break;
|
|
72
|
+
case "agent_settled":
|
|
73
|
+
this.running = false;
|
|
74
|
+
this.outcome ??= { state: "done" };
|
|
75
|
+
break;
|
|
76
|
+
case "permission_request": {
|
|
77
|
+
const summary = flat(toolSummary(event.toolName, event.input, this.cwd()), 80);
|
|
78
|
+
const text = summary === "" ? event.toolName : `${event.toolName} · ${summary}`;
|
|
79
|
+
const taskId = event.context?.taskId;
|
|
80
|
+
this.approvals.set(event.requestId, { msg: text, ...(taskId ? { taskId } : {}) });
|
|
81
|
+
const task = taskId !== undefined ? this.tasks.get(taskId) : undefined;
|
|
82
|
+
if (task !== undefined)
|
|
83
|
+
Object.assign(task, { state: "blocked", msg: text });
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "permission_resolved": {
|
|
87
|
+
const taskId = this.approvals.get(event.requestId)?.taskId;
|
|
88
|
+
this.approvals.delete(event.requestId);
|
|
89
|
+
const task = taskId !== undefined ? this.tasks.get(taskId) : undefined;
|
|
90
|
+
const still = [...this.approvals.values()].some((p) => p.taskId === taskId);
|
|
91
|
+
if (task?.state === "blocked" && !still)
|
|
92
|
+
Object.assign(task, { state: "working" });
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "plan_proposed":
|
|
96
|
+
this.plan = msg().plan.dialog.title;
|
|
97
|
+
break;
|
|
98
|
+
case "plan_resolved":
|
|
99
|
+
this.plan = undefined;
|
|
100
|
+
break;
|
|
101
|
+
case "model_changed":
|
|
102
|
+
this.auth = undefined;
|
|
103
|
+
break;
|
|
104
|
+
case "subagent_start":
|
|
105
|
+
this.tasks.set(event.taskId, {
|
|
106
|
+
id: `task/${idSegment(event.taskId)}`,
|
|
107
|
+
title: event.description,
|
|
108
|
+
state: "working",
|
|
109
|
+
msg: event.agent,
|
|
110
|
+
});
|
|
111
|
+
break;
|
|
112
|
+
case "subagent_update": {
|
|
113
|
+
const task = this.tasks.get(event.taskId);
|
|
114
|
+
if (task?.state === "working" && event.toolName !== undefined)
|
|
115
|
+
task.msg = run.runningTool(event.toolName);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "subagent_end": {
|
|
119
|
+
const task = this.tasks.get(event.taskId);
|
|
120
|
+
if (task === undefined)
|
|
121
|
+
break;
|
|
122
|
+
const s = event.status;
|
|
123
|
+
task.state =
|
|
124
|
+
s === "completed" ? "done" : s === "aborted" || s === "interrupted" ? "idle" : "error";
|
|
125
|
+
delete task.msg;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.sync();
|
|
132
|
+
}
|
|
133
|
+
/** 用户按键:已看到的 done / error 回到 idle。 */
|
|
134
|
+
acknowledge() {
|
|
135
|
+
if (this.running || this.outcome === undefined || this.outcome.state === "idle")
|
|
136
|
+
return;
|
|
137
|
+
this.outcome = { state: "idle" };
|
|
138
|
+
this.sync();
|
|
139
|
+
}
|
|
140
|
+
/** 当前根记录(测试也用)。 */
|
|
141
|
+
root() {
|
|
142
|
+
const pending = [...this.approvals.values()].at(-1);
|
|
143
|
+
if (pending !== undefined)
|
|
144
|
+
return blocked("permission", pending.msg);
|
|
145
|
+
if (this.plan !== undefined)
|
|
146
|
+
return blocked("question", this.plan);
|
|
147
|
+
if (this.auth !== undefined)
|
|
148
|
+
return blocked("auth", this.auth);
|
|
149
|
+
if (this.running)
|
|
150
|
+
return { state: "working", ...(this.activity !== undefined ? { msg: this.activity } : {}) };
|
|
151
|
+
return this.outcome ?? { state: "idle" };
|
|
152
|
+
}
|
|
153
|
+
/** 写出全部记录(发射器同值去重)。 */
|
|
154
|
+
sync() {
|
|
155
|
+
this.emitter.report(this.root());
|
|
156
|
+
for (const task of this.tasks.values())
|
|
157
|
+
this.emitter.report({
|
|
158
|
+
id: task.id,
|
|
159
|
+
state: task.state,
|
|
160
|
+
title: task.title,
|
|
161
|
+
...(task.state === "blocked" ? { kind: "permission" } : {}),
|
|
162
|
+
...(task.msg !== undefined ? { msg: task.msg } : {}),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
outcomeOf(stopReason) {
|
|
166
|
+
if (stopReason === "aborted")
|
|
167
|
+
return { state: "idle" };
|
|
168
|
+
if (stopReason !== "error")
|
|
169
|
+
return { state: "done" };
|
|
170
|
+
const text = flat(this.lastError ?? "error", 200);
|
|
171
|
+
if (AUTH_EXPIRED.test(text))
|
|
172
|
+
this.auth = text;
|
|
173
|
+
return { state: "error", msg: text };
|
|
174
|
+
}
|
|
175
|
+
dropTask(key) {
|
|
176
|
+
const task = this.tasks.get(key);
|
|
177
|
+
this.tasks.delete(key);
|
|
178
|
+
if (task !== undefined)
|
|
179
|
+
this.emitter.report({ id: task.id, state: "clear" });
|
|
180
|
+
}
|
|
181
|
+
reset() {
|
|
182
|
+
this.running = false;
|
|
183
|
+
this.approvals.clear();
|
|
184
|
+
this.plan = this.auth = this.outcome = this.lastError = this.activity = undefined;
|
|
185
|
+
for (const key of [...this.tasks.keys()])
|
|
186
|
+
this.dropTask(key);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function blocked(kind, text) {
|
|
190
|
+
return { state: "blocked", kind, msg: text };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 交互模式装配。缺省只在真实终端(没有注入 `terminal`)启用;测试注入终端时给了 `programStatus` 才启用,
|
|
194
|
+
* 其余情况与 `ui.programStatus: "off"` 一样全部空操作。
|
|
195
|
+
*/
|
|
196
|
+
export function programStatusUi(deps) {
|
|
197
|
+
const { terminal, programStatus } = deps.options;
|
|
198
|
+
const mode = deps.runtime.config.ui?.programStatus ?? "auto";
|
|
199
|
+
if ((terminal !== undefined && programStatus === undefined) || mode === "off")
|
|
200
|
+
return { wrap: (handler) => handler, start: () => undefined, exit: () => undefined };
|
|
201
|
+
const emitter = new ProgramStatusEmitter({
|
|
202
|
+
write: (data) => deps.tui.terminal.write(data),
|
|
203
|
+
mode,
|
|
204
|
+
env: deps.env,
|
|
205
|
+
app: "ama",
|
|
206
|
+
...programStatus,
|
|
207
|
+
});
|
|
208
|
+
const tracker = new ProgramStatusTracker(emitter, deps.cwd);
|
|
209
|
+
deps.tui.addInputListener((data) => {
|
|
210
|
+
if (emitter.handleInput(data))
|
|
211
|
+
return true;
|
|
212
|
+
tracker.acknowledge();
|
|
213
|
+
return false;
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
wrap: (handler) => (event) => {
|
|
217
|
+
tracker.onEvent(event);
|
|
218
|
+
handler(event);
|
|
219
|
+
},
|
|
220
|
+
start: () => {
|
|
221
|
+
emitter.start();
|
|
222
|
+
tracker.sync();
|
|
223
|
+
},
|
|
224
|
+
exit: () => {
|
|
225
|
+
emitter.report({ state: "clear" });
|
|
226
|
+
emitter.dispose();
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 交互界面的会话事件分派:`SessionEvent` → 消息区、工具视图、状态栏、运行指示。[B7]
|
|
3
3
|
* 从 interactive-mode.ts 拆出(W5-U:第五波事件先交给 agent-ui.ts,预算到限的 agent_settled warning 不重复显示)。
|
|
4
|
+
*
|
|
5
|
+
* 状态栏刷新时机:用户消息落盘(message_end,紧跟 message_start)后立刻刷新,尾部估算马上反映进 Ctx;
|
|
6
|
+
* 流式中助手消息带 usage 时按 ≤ 2 Hz 采样在途请求的上下文量(StatusBar.noteStreaming),消息结束后回到统计值。
|
|
7
|
+
* 模型没有上下文窗口(`ctx ?`)时每个模型提示一次怎么补。
|
|
4
8
|
*/
|
|
5
9
|
import type { AgentSession, SessionEvent } from "../../agent/types.js";
|
|
6
10
|
import type { AgentUi } from "./agent-ui.js";
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 交互界面的会话事件分派:`SessionEvent` → 消息区、工具视图、状态栏、运行指示。[B7]
|
|
3
3
|
* 从 interactive-mode.ts 拆出(W5-U:第五波事件先交给 agent-ui.ts,预算到限的 agent_settled warning 不重复显示)。
|
|
4
|
+
*
|
|
5
|
+
* 状态栏刷新时机:用户消息落盘(message_end,紧跟 message_start)后立刻刷新,尾部估算马上反映进 Ctx;
|
|
6
|
+
* 流式中助手消息带 usage 时按 ≤ 2 Hz 采样在途请求的上下文量(StatusBar.noteStreaming),消息结束后回到统计值。
|
|
7
|
+
* 模型没有上下文窗口(`ctx ?`)时每个模型提示一次怎么补。
|
|
4
8
|
*/
|
|
5
9
|
import { msg } from "../../i18n/index.js";
|
|
10
|
+
import { formatModelRef } from "../../ai/providers/channels.js";
|
|
6
11
|
import { cacheEventNotice, cacheNoticesEnabled } from "../session-report.js";
|
|
7
12
|
import { compactionErrorText, ImageBudgetNotices, LIMIT_WARNING } from "./event-notices.js";
|
|
8
13
|
export function createSessionEventHandler(deps) {
|
|
9
14
|
const { view, tools, status, area, agentUi, indicator, session, setQueue, notice, render } = deps;
|
|
10
15
|
const images = new ImageBudgetNotices((text) => notice("info", text));
|
|
16
|
+
const windowHinted = new Set();
|
|
17
|
+
/** 刷新状态栏;模型没有上下文窗口时每个模型提示一次。 */
|
|
18
|
+
const refresh = () => {
|
|
19
|
+
status.refresh();
|
|
20
|
+
if (status.current().contextWindow !== undefined)
|
|
21
|
+
return;
|
|
22
|
+
const model = session().state.model;
|
|
23
|
+
const ref = model === undefined ? undefined : formatModelRef(model);
|
|
24
|
+
if (ref === undefined || windowHinted.has(ref))
|
|
25
|
+
return;
|
|
26
|
+
windowHinted.add(ref);
|
|
27
|
+
view.addNotice("info", msg().interactive.statusLine.noWindow(ref));
|
|
28
|
+
};
|
|
11
29
|
return (event) => {
|
|
12
30
|
if (area.onEvent(event) || agentUi().onEvent(event)) {
|
|
13
31
|
// [W7-A] 子任务出现 / 结束:运行提示行的「↓ Agent 栏」跟着变
|
|
@@ -21,7 +39,7 @@ export function createSessionEventHandler(deps) {
|
|
|
21
39
|
// 预算到限已由 limit_reached 给出友好提示
|
|
22
40
|
if (event.warning !== undefined && event.warning !== LIMIT_WARNING)
|
|
23
41
|
view.addNotice("warn", event.warning);
|
|
24
|
-
|
|
42
|
+
refresh();
|
|
25
43
|
break;
|
|
26
44
|
case "message_start": {
|
|
27
45
|
const message = event.message;
|
|
@@ -36,12 +54,16 @@ export function createSessionEventHandler(deps) {
|
|
|
36
54
|
}
|
|
37
55
|
case "message_update":
|
|
38
56
|
view.updateAssistant(event.message);
|
|
57
|
+
status.noteStreaming(event.message.usage);
|
|
39
58
|
break;
|
|
40
59
|
case "message_end":
|
|
41
60
|
if (event.message.role === "assistant") {
|
|
42
61
|
view.endAssistant(event.message);
|
|
62
|
+
status.clearStreaming();
|
|
43
63
|
status.refresh();
|
|
44
64
|
}
|
|
65
|
+
else if (event.message.role === "user")
|
|
66
|
+
refresh();
|
|
45
67
|
break;
|
|
46
68
|
case "tool_execution_start": {
|
|
47
69
|
const started = tools.start(event);
|
|
@@ -89,7 +111,7 @@ export function createSessionEventHandler(deps) {
|
|
|
89
111
|
case "model_changed":
|
|
90
112
|
case "thinking_level_changed":
|
|
91
113
|
case "session_changed":
|
|
92
|
-
|
|
114
|
+
refresh();
|
|
93
115
|
render();
|
|
94
116
|
return;
|
|
95
117
|
default:
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* `Manual · shift+tab 切换 sonnet-4-5 · medium · ↑12.3k ↓1.2k · cache 80% ♨ · $0.12 · ctx 34% · proj ⎇ main 5ae9e54 +12 −3 · 2h24m`
|
|
11
11
|
* 右区顺序:模型 · 思考 · `↑ ↓` · cache · 费用 · rebill · ctx · git · 时长 · queue · codemode · 预设 · [宿主]。
|
|
12
12
|
* - `full`(独立终端缺省,用户样例):用量类项移到上方速率行(status-line.ts),本行为
|
|
13
|
-
* `Manual | 模型 思考 | Ctx 3.0% | 目录 ⎇ 分支 短提交 (+a,-d) | $费用 | 会话时长`;Ctx
|
|
13
|
+
* `Manual | 模型 思考 | Ctx 3.0% 8.2k/272k auto | 目录 ⎇ 分支 短提交 (+a,-d) | $费用 | 会话时长`;Ctx 一位小数、
|
|
14
|
+
* 不换余量表,带已用量 / 窗口与自动压缩标记(status-ctx.ts)。
|
|
15
|
+
* - 上下文:估算值带 `≈`;着色阈值与档一裁剪对齐;流式中有 usage 时显示在途请求的值(`noteStreaming`,≤ 2 Hz)。
|
|
14
16
|
*
|
|
15
17
|
* - 用量来自 `session.getStats()`,只在 `refresh()` 时读取(事件驱动),渲染只拼字符串;时长按渲染时刻算。
|
|
16
18
|
* - [W3-C2] `cache` 是**最近一次**请求的命中率;端点三态 `cache —` / `cache 未报告`;保温中追加 `♨`;
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
* 数字变化不会让某项时有时无(40 列不抖动)。
|
|
36
38
|
*/
|
|
37
39
|
import type { AgentSession, SessionCacheStats, SessionStats } from "../../agent/types.js";
|
|
40
|
+
import type { Usage } from "../../ai/types.js";
|
|
38
41
|
import type { StatusLineMode } from "../../config/types.js";
|
|
39
42
|
import type { GitInfo } from "../../git/info.js";
|
|
40
43
|
import { type QuotaView } from "./status-quota.js";
|
|
@@ -76,6 +79,11 @@ export declare function formatCost(cost: number): string;
|
|
|
76
79
|
export declare function formatDuration(ms: number): string;
|
|
77
80
|
/** `cache 83%` / `cache —` / `cache 未报告`,保温计时中追加 ` ♨`。 */
|
|
78
81
|
export declare function cacheText(cache: SessionCacheStats, warm?: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* 会话累计计费量(不是上下文):`Σ↑<输入含缓存读写> ↓<输出> R<缓存读> W<缓存写>`,R / W 为 0 不显示;
|
|
84
|
+
* ASCII 下 `Σ` → `sum `。
|
|
85
|
+
*/
|
|
86
|
+
export declare function tokensText(t: SessionStats["tokens"], g: Pick<Theme["glyphs"], "ascii" | "arrowUp" | "arrowDown">): string;
|
|
79
87
|
/** 状态费用:会话费用 + 外部 Agent 以美元计的用量;会话费用未知为 undefined。 */
|
|
80
88
|
export declare function statusCost(stats: SessionStats): number | undefined;
|
|
81
89
|
export interface Part {
|
|
@@ -127,15 +135,26 @@ export interface UsageItems {
|
|
|
127
135
|
host?: string;
|
|
128
136
|
}
|
|
129
137
|
/** 用量类项的文本(已着色;普通项 dim)。 */
|
|
130
|
-
export declare function usageItems(stats: SessionStats, queue: number, source: StatusBarSource, theme: Theme
|
|
138
|
+
export declare function usageItems(stats: SessionStats, queue: number, source: StatusBarSource, theme: Theme,
|
|
139
|
+
/** full 速率行:`Σ` 标明会话累计,缓存读写分列 `R / W`;compact 保持 `↑ ↓`(宿主按此解析、窄屏省宽)。 */
|
|
140
|
+
cumulative?: boolean): UsageItems;
|
|
131
141
|
export declare class StatusBar implements Component {
|
|
132
142
|
private readonly source;
|
|
133
143
|
private readonly theme;
|
|
134
144
|
private stats;
|
|
135
145
|
private queue;
|
|
146
|
+
/** 在途请求的上下文量(message_update 的 usage,≤ 2 Hz 采样);该消息结束后清掉。 */
|
|
147
|
+
private readonly live;
|
|
136
148
|
constructor(source: StatusBarSource, theme: Theme);
|
|
137
149
|
/** 用量、模型等变化后调用(事件驱动;流式期间不必每帧算)。 */
|
|
138
150
|
refresh(): void;
|
|
151
|
+
/**
|
|
152
|
+
* 流式中的助手消息带了 usage(有的协议在开头就给输入量):记下在途请求的上下文量,距上次采样
|
|
153
|
+
* ≥ 500 ms 才更新(≤ 2 Hz)。返回是否更新了。
|
|
154
|
+
*/
|
|
155
|
+
noteStreaming(usage: Usage | undefined): boolean;
|
|
156
|
+
/** 助手消息结束:丢掉在途值,回到统计值。 */
|
|
157
|
+
clearStreaming(): void;
|
|
139
158
|
setQueue(steering: number, followUp: number): void;
|
|
140
159
|
/** 当前统计(速率行共用,避免重复 getStats)。 */
|
|
141
160
|
current(): SessionStats;
|
|
@@ -144,7 +163,6 @@ export declare class StatusBar implements Component {
|
|
|
144
163
|
private parts;
|
|
145
164
|
/** `目录 ⎇ 分支 短提交 +a −b`:三项同组、各自按优先级丢弃;full 另带 `↑N ↓N` 并按参考配色。 */
|
|
146
165
|
private gitParts;
|
|
147
|
-
private ctxText;
|
|
148
166
|
render(width: number): string[];
|
|
149
167
|
invalidate(): void;
|
|
150
168
|
}
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* `Manual · shift+tab 切换 sonnet-4-5 · medium · ↑12.3k ↓1.2k · cache 80% ♨ · $0.12 · ctx 34% · proj ⎇ main 5ae9e54 +12 −3 · 2h24m`
|
|
11
11
|
* 右区顺序:模型 · 思考 · `↑ ↓` · cache · 费用 · rebill · ctx · git · 时长 · queue · codemode · 预设 · [宿主]。
|
|
12
12
|
* - `full`(独立终端缺省,用户样例):用量类项移到上方速率行(status-line.ts),本行为
|
|
13
|
-
* `Manual | 模型 思考 | Ctx 3.0% | 目录 ⎇ 分支 短提交 (+a,-d) | $费用 | 会话时长`;Ctx
|
|
13
|
+
* `Manual | 模型 思考 | Ctx 3.0% 8.2k/272k auto | 目录 ⎇ 分支 短提交 (+a,-d) | $费用 | 会话时长`;Ctx 一位小数、
|
|
14
|
+
* 不换余量表,带已用量 / 窗口与自动压缩标记(status-ctx.ts)。
|
|
15
|
+
* - 上下文:估算值带 `≈`;着色阈值与档一裁剪对齐;流式中有 usage 时显示在途请求的值(`noteStreaming`,≤ 2 Hz)。
|
|
14
16
|
*
|
|
15
17
|
* - 用量来自 `session.getStats()`,只在 `refresh()` 时读取(事件驱动),渲染只拼字符串;时长按渲染时刻算。
|
|
16
18
|
* - [W3-C2] `cache` 是**最近一次**请求的命中率;端点三态 `cache —` / `cache 未报告`;保温中追加 `♨`;
|
|
@@ -38,7 +40,8 @@ import { msg } from "../../i18n/index.js";
|
|
|
38
40
|
import { permissionModeLabel } from "../../permissions/modes.js";
|
|
39
41
|
import { formatModelRef } from "../../ai/providers/channels.js";
|
|
40
42
|
import { compactQuotaItem } from "./status-quota.js";
|
|
41
|
-
import {
|
|
43
|
+
import { LiveContext, compactCtxText, fullCtxParts } from "./status-ctx.js";
|
|
44
|
+
import { truncateToWidth, visibleWidth } from "../../tui.js";
|
|
42
45
|
export function formatTokens(count) {
|
|
43
46
|
if (count < 1000)
|
|
44
47
|
return String(count);
|
|
@@ -73,6 +76,22 @@ export function cacheText(cache, warm = "♨") {
|
|
|
73
76
|
: "—";
|
|
74
77
|
return `cache ${value}${cache.warming.state === "scheduled" ? ` ${warm}` : ""}`;
|
|
75
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* 会话累计计费量(不是上下文):`Σ↑<输入含缓存读写> ↓<输出> R<缓存读> W<缓存写>`,R / W 为 0 不显示;
|
|
81
|
+
* ASCII 下 `Σ` → `sum `。
|
|
82
|
+
*/
|
|
83
|
+
export function tokensText(t, g) {
|
|
84
|
+
const prompt = t.input + t.cacheRead + t.cacheWrite;
|
|
85
|
+
const parts = [
|
|
86
|
+
`${g.ascii ? "sum " : "Σ"}${g.arrowUp}${formatTokens(prompt)}`,
|
|
87
|
+
`${g.arrowDown}${formatTokens(t.output)}`,
|
|
88
|
+
];
|
|
89
|
+
if (t.cacheRead > 0)
|
|
90
|
+
parts.push(`R${formatTokens(t.cacheRead)}`);
|
|
91
|
+
if (t.cacheWrite > 0)
|
|
92
|
+
parts.push(`W${formatTokens(t.cacheWrite)}`);
|
|
93
|
+
return parts.join(" ");
|
|
94
|
+
}
|
|
76
95
|
/** 状态费用:会话费用 + 外部 Agent 以美元计的用量;会话费用未知为 undefined。 */
|
|
77
96
|
export function statusCost(stats) {
|
|
78
97
|
if (stats.cost === undefined)
|
|
@@ -86,8 +105,6 @@ export function statusCost(stats) {
|
|
|
86
105
|
}
|
|
87
106
|
/** 两区之间至少留的空格;不足时退回单区。 */
|
|
88
107
|
const MIN_GAP = 4;
|
|
89
|
-
/** ctx 改用余量表的最小宽度。 */
|
|
90
|
-
const METER_WIDTH = 110;
|
|
91
108
|
/** 按优先级丢弃后排成一行(两区 / 单区);连接符都是 dim。 */
|
|
92
109
|
export function layoutRow(parts, width, theme, style = {}) {
|
|
93
110
|
const sep = style.sep ?? " · ";
|
|
@@ -161,7 +178,9 @@ export function abbreviateModel(ref, width) {
|
|
|
161
178
|
return out;
|
|
162
179
|
}
|
|
163
180
|
/** 用量类项的文本(已着色;普通项 dim)。 */
|
|
164
|
-
export function usageItems(stats, queue, source, theme
|
|
181
|
+
export function usageItems(stats, queue, source, theme,
|
|
182
|
+
/** full 速率行:`Σ` 标明会话累计,缓存读写分列 `R / W`;compact 保持 `↑ ↓`(宿主按此解析、窄屏省宽)。 */
|
|
183
|
+
cumulative = false) {
|
|
165
184
|
const g = theme.glyphs;
|
|
166
185
|
const dim = (text) => theme.fg("dim", text);
|
|
167
186
|
const out = {};
|
|
@@ -169,7 +188,9 @@ export function usageItems(stats, queue, source, theme) {
|
|
|
169
188
|
const prompt = t.input + t.cacheRead + t.cacheWrite;
|
|
170
189
|
const cache = stats.cache;
|
|
171
190
|
if (prompt + t.output > 0) {
|
|
172
|
-
out.tokens = dim(
|
|
191
|
+
out.tokens = dim(cumulative
|
|
192
|
+
? tokensText(t, g)
|
|
193
|
+
: `${g.arrowUp}${formatTokens(prompt)} ${g.arrowDown}${formatTokens(t.output)}`);
|
|
173
194
|
if (cache !== undefined)
|
|
174
195
|
out.cache = dim(cacheText(cache, g.warm));
|
|
175
196
|
else if (stats.cacheHitRate !== undefined) {
|
|
@@ -228,16 +249,33 @@ const COMPACT = {
|
|
|
228
249
|
hint: 16,
|
|
229
250
|
quota: 17,
|
|
230
251
|
};
|
|
231
|
-
/**
|
|
232
|
-
|
|
252
|
+
/**
|
|
253
|
+
* full 布局本行的优先级(§1.2 下行表)。Ctx 段由百分比、已用量(ctxUsed)、`/窗口`(ctxWindow)与
|
|
254
|
+
* `auto` 标记组成,窄时先丢 auto、`/窗口`,再丢已用量,百分比最后丢。
|
|
255
|
+
*/
|
|
256
|
+
const FULL = {
|
|
257
|
+
model: 0,
|
|
258
|
+
ctx: 1,
|
|
259
|
+
cost: 2,
|
|
260
|
+
duration: 3,
|
|
261
|
+
ctxUsed: 4,
|
|
262
|
+
branch: 5,
|
|
263
|
+
dir: 6,
|
|
264
|
+
diff: 7,
|
|
265
|
+
ctxWindow: 8,
|
|
266
|
+
thinking: 9,
|
|
267
|
+
ctxAuto: 13,
|
|
268
|
+
};
|
|
233
269
|
const FULL_HINT = 12;
|
|
234
|
-
/** full 状态栏(用户样例):段间 ` |
|
|
235
|
-
const FULL_STYLE = { sep: " | " };
|
|
270
|
+
/** full 状态栏(用户样例):段间 ` | `;Ctx 段(status-ctx.ts)的成员自带前导空格 / 斜杠,组内不加连接符。 */
|
|
271
|
+
const FULL_STYLE = { sep: " | ", groups: { ctx: { glue: "" } } };
|
|
236
272
|
export class StatusBar {
|
|
237
273
|
source;
|
|
238
274
|
theme;
|
|
239
275
|
stats;
|
|
240
276
|
queue = 0;
|
|
277
|
+
/** 在途请求的上下文量(message_update 的 usage,≤ 2 Hz 采样);该消息结束后清掉。 */
|
|
278
|
+
live = new LiveContext(() => this.source.now?.() ?? Date.now());
|
|
241
279
|
constructor(source, theme) {
|
|
242
280
|
this.source = source;
|
|
243
281
|
this.theme = theme;
|
|
@@ -246,6 +284,17 @@ export class StatusBar {
|
|
|
246
284
|
refresh() {
|
|
247
285
|
this.stats = this.source.session().getStats();
|
|
248
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* 流式中的助手消息带了 usage(有的协议在开头就给输入量):记下在途请求的上下文量,距上次采样
|
|
289
|
+
* ≥ 500 ms 才更新(≤ 2 Hz)。返回是否更新了。
|
|
290
|
+
*/
|
|
291
|
+
noteStreaming(usage) {
|
|
292
|
+
return this.live.note(usage);
|
|
293
|
+
}
|
|
294
|
+
/** 助手消息结束:丢掉在途值,回到统计值。 */
|
|
295
|
+
clearStreaming() {
|
|
296
|
+
this.live.clear();
|
|
297
|
+
}
|
|
249
298
|
setQueue(steering, followUp) {
|
|
250
299
|
this.queue = steering + followUp;
|
|
251
300
|
}
|
|
@@ -309,7 +358,11 @@ export class StatusBar {
|
|
|
309
358
|
right(usage.cost, COMPACT.cost);
|
|
310
359
|
if (usage.rebill !== undefined)
|
|
311
360
|
right(usage.rebill, COMPACT.rebill);
|
|
312
|
-
|
|
361
|
+
const ctx = this.live.view(stats, g.ascii);
|
|
362
|
+
if (full)
|
|
363
|
+
fullCtxParts(ctx, theme, FULL).forEach((part) => parts.push(part));
|
|
364
|
+
else
|
|
365
|
+
right(compactCtxText(ctx, width, theme), p.ctx);
|
|
313
366
|
this.gitParts(p, full).forEach((part) => parts.push(part));
|
|
314
367
|
if (full) {
|
|
315
368
|
const cost = statusCost(stats);
|
|
@@ -384,20 +437,6 @@ export class StatusBar {
|
|
|
384
437
|
}
|
|
385
438
|
return out;
|
|
386
439
|
}
|
|
387
|
-
ctxText(percent, width, decimal) {
|
|
388
|
-
const theme = this.theme;
|
|
389
|
-
if (percent === undefined)
|
|
390
|
-
return theme.fg("dim", decimal ? "Ctx ?" : "ctx ?");
|
|
391
|
-
const color = levelColor(percent / 100);
|
|
392
|
-
// full 按用户样例总是 `Ctx 3.0%`;compact 宽屏换余量表(现状)
|
|
393
|
-
if (!decimal && width >= METER_WIDTH) {
|
|
394
|
-
const meter = new Meter(percent / 100, { label: theme.fg("dim", "ctx"), theme });
|
|
395
|
-
return meter.render(80)[0] ?? "";
|
|
396
|
-
}
|
|
397
|
-
if (decimal)
|
|
398
|
-
return theme.fg("dim", "Ctx ") + theme.fg(color, `${percent.toFixed(1)}%`);
|
|
399
|
-
return theme.fg("dim", "ctx ") + theme.fg(color, `${Math.round(percent)}%`);
|
|
400
|
-
}
|
|
401
440
|
render(width) {
|
|
402
441
|
const style = this.layout() === "full" ? FULL_STYLE : {};
|
|
403
442
|
return [layoutRow(this.parts(width), width, this.theme, style)];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 状态栏的上下文项(status-bar.ts 的一部分,单独成文件控制行数)。
|
|
3
|
+
*
|
|
4
|
+
* - full:`Ctx 3.0% 8.2k/272k auto`——百分比一位小数;已用量 / 窗口是绝对量;`auto` = 自动压缩开着
|
|
5
|
+
* (关闭、熔断或窗口未知时不显示)。成员各自按优先级丢弃:先丢 `auto`、`/窗口`,再丢已用量,百分比最后丢;
|
|
6
|
+
* 会变的数字按最宽形状占位(不抖动)。
|
|
7
|
+
* - compact:`ctx 34%`,≥ 110 列换余量表;0 < 占用 < 1% 时保留一位小数(不显示成 0%)。
|
|
8
|
+
* - 上下文量不是来自 usage(压缩后新 usage 之前、启动前缀基线)时数字前加 `≈`(ASCII `~`)。
|
|
9
|
+
* - 着色:warning 与档一裁剪阈值对齐(`pruneAt / 窗口`,缺省 0.7),≥ 0.9 error。
|
|
10
|
+
* - 流式中:助手消息的 usage 已经给出在途请求的上下文量时,按 ≤ 2 Hz 采样显示它,消息结束后回到统计值。
|
|
11
|
+
*/
|
|
12
|
+
import type { SessionStats } from "../../agent/types.js";
|
|
13
|
+
import type { Usage } from "../../ai/types.js";
|
|
14
|
+
import { type Theme } from "../../tui.js";
|
|
15
|
+
import { type Part } from "./status-bar.js";
|
|
16
|
+
/** 流式 usage 的采样间隔(与 telemetry_tick 同节奏,≤ 2 Hz)。 */
|
|
17
|
+
export declare const LIVE_CTX_INTERVAL_MS = 500;
|
|
18
|
+
/** 上下文百分比:full 一位小数;compact 取整,但 0 < p < 1 时保留一位。 */
|
|
19
|
+
export declare function ctxPercentText(percent: number, decimal: boolean): string;
|
|
20
|
+
/** 上下文着色的 warning 阈值:与档一裁剪阈值对齐(`pruneAt / 窗口`),缺省 0.7。 */
|
|
21
|
+
export declare function ctxWarnAt(stats: Pick<SessionStats, "context" | "contextWindow">): number;
|
|
22
|
+
/** 上下文量不是来自 usage(全量估算 / 启动前缀基线):`≈`(ASCII `~`),否则空串。 */
|
|
23
|
+
export declare function ctxApprox(stats: Pick<SessionStats, "context">, ascii?: boolean): string;
|
|
24
|
+
/** 状态栏上显示的上下文。 */
|
|
25
|
+
export interface CtxView {
|
|
26
|
+
tokens: number | undefined;
|
|
27
|
+
window: number | undefined;
|
|
28
|
+
percent: number | undefined;
|
|
29
|
+
approx: string;
|
|
30
|
+
warnAt: number;
|
|
31
|
+
auto: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** full 本行 Ctx 段各成员的优先级。 */
|
|
34
|
+
export interface CtxPriorities {
|
|
35
|
+
ctx: number;
|
|
36
|
+
ctxUsed: number;
|
|
37
|
+
ctxWindow: number;
|
|
38
|
+
ctxAuto: number;
|
|
39
|
+
}
|
|
40
|
+
/** 在途请求的上下文量(message_update 的 usage)。 */
|
|
41
|
+
export declare class LiveContext {
|
|
42
|
+
private readonly now;
|
|
43
|
+
private tokens;
|
|
44
|
+
private at;
|
|
45
|
+
constructor(now: () => number);
|
|
46
|
+
/** 记下 usage 给出的上下文量;距上次采样 < 500 ms 时不更新。返回是否更新了。 */
|
|
47
|
+
note(usage: Usage | undefined): boolean;
|
|
48
|
+
clear(): void;
|
|
49
|
+
/** 统计值 → 显示值(有在途值时以它为准,不再是估算)。 */
|
|
50
|
+
view(stats: SessionStats, ascii: boolean): CtxView;
|
|
51
|
+
}
|
|
52
|
+
/** compact:`ctx 34%`,宽屏换余量表。 */
|
|
53
|
+
export declare function compactCtxText(ctx: CtxView, width: number, theme: Theme): string;
|
|
54
|
+
/** full:`Ctx 3.0%` + ` 8.2k` + `/272k` + ` auto`(同组 `ctx`,组内不加连接符)。 */
|
|
55
|
+
export declare function fullCtxParts(ctx: CtxView, theme: Theme, p: CtxPriorities): Part[];
|