@armadra/agent 0.6.3 → 0.6.5
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 +29 -0
- package/CHANGELOG.zh-CN.md +21 -0
- package/dist/agent/queue.d.ts +9 -0
- package/dist/agent/queue.js +27 -0
- package/dist/agent/session-subagent.d.ts +1 -0
- package/dist/agent/session-subagent.js +9 -0
- package/dist/agent/session.d.ts +5 -0
- package/dist/agent/session.js +21 -2
- package/dist/agent/subagent-direct.d.ts +5 -2
- package/dist/agent/subagent-direct.js +34 -1
- package/dist/agent/subagent-registry.d.ts +2 -1
- package/dist/agent/subagent-registry.js +2 -2
- package/dist/agent/types.d.ts +7 -0
- package/dist/agents/task-record.d.ts +3 -3
- package/dist/ai/apis/chatgpt-rate-limits.js +7 -2
- package/dist/ai/types.d.ts +2 -1
- package/dist/bundle/ama.cjs +417 -106
- package/dist/config/json-schema.js +1 -0
- package/dist/config/key-docs.js +8 -1
- 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/acp/client.d.ts +13 -4
- package/dist/drivers/acp/client.js +17 -6
- package/dist/drivers/acp/types.d.ts +22 -2
- package/dist/drivers/runner.js +29 -2
- package/dist/i18n/catalog.d.ts +13 -0
- package/dist/i18n/messages/agents.d.ts +4 -0
- package/dist/i18n/messages/agents.js +4 -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-view.d.ts +8 -0
- package/dist/i18n/messages/interactive-view.js +10 -0
- package/dist/i18n/messages/interactive.d.ts +10 -0
- package/dist/i18n/messages/interactive.js +6 -0
- package/dist/i18n/messages/report.d.ts +8 -0
- package/dist/i18n/messages/report.js +8 -0
- package/dist/i18n/messages/settings.d.ts +2 -0
- package/dist/i18n/messages/settings.js +2 -0
- package/dist/modes/commands-core.js +11 -0
- package/dist/modes/interactive/agent-ui.js +1 -0
- package/dist/modes/interactive/agent-view.d.ts +5 -0
- package/dist/modes/interactive/agent-view.js +14 -3
- package/dist/modes/interactive/interactive-mode.js +13 -1
- package/dist/modes/interactive/interrupt-send.d.ts +25 -0
- package/dist/modes/interactive/interrupt-send.js +32 -0
- package/dist/modes/interactive/key-dispatch.d.ts +8 -1
- package/dist/modes/interactive/key-dispatch.js +37 -0
- package/dist/modes/interactive/line/line-mode.d.ts +1 -0
- package/dist/modes/interactive/line/line-mode.js +29 -7
- package/dist/modes/interactive/run-indicator.d.ts +19 -2
- package/dist/modes/interactive/run-indicator.js +29 -7
- package/dist/modes/interactive/status-bar.d.ts +4 -1
- package/dist/modes/interactive/status-bar.js +17 -6
- package/dist/modes/interactive/status-line.d.ts +10 -9
- package/dist/modes/interactive/status-line.js +46 -38
- package/dist/modes/interactive/status-quota.d.ts +16 -2
- package/dist/modes/interactive/status-quota.js +54 -34
- package/dist/modes/interactive/task-background.d.ts +2 -2
- package/dist/modes/rpc/commands.d.ts +2 -0
- package/dist/modes/rpc/commands.js +15 -1
- package/dist/rpc.d.ts +7 -0
- package/dist/tools/types.d.ts +6 -0
- package/dist/tui/keybindings.d.ts +7 -0
- package/dist/tui/keybindings.js +7 -0
- package/docs/acp.md +3 -0
- package/docs/en/rpc.md +9 -7
- package/docs/en/tui.md +49 -31
- package/docs/rpc.md +9 -7
- package/docs/session-format.md +1 -1
- package/docs/tui.md +49 -31
- package/package.json +1 -1
|
@@ -67,6 +67,7 @@ export const BUILTIN_COMMANDS = [
|
|
|
67
67
|
command("tasks", "tasks", "[id] | stop <id> | bg [id]"),
|
|
68
68
|
command("agents", "agents"),
|
|
69
69
|
command("paste", "paste"),
|
|
70
|
+
command("interrupt", "interrupt", { key: "interruptArgs" }),
|
|
70
71
|
command("exit", "exit"),
|
|
71
72
|
// [W6-C0] 第六波(W6-S / W6-T1 / W6-M 实现)
|
|
72
73
|
command("config", "config", "[key=value]"),
|
|
@@ -246,6 +247,16 @@ export async function runSlashCommand(line, ctx) {
|
|
|
246
247
|
return planCommand(session, args, ctx);
|
|
247
248
|
case "tasks":
|
|
248
249
|
return { kind: "handled", message: await tasksCommand(session, args) };
|
|
250
|
+
case "interrupt": {
|
|
251
|
+
// 打断并立即发送:运行中中止当前回合、以「排队的插话 + 本条」开新回合;空闲时就是普通提示
|
|
252
|
+
if (args === "")
|
|
253
|
+
throw new AmaError("invalid_arguments", msg().report.command.interruptUsage);
|
|
254
|
+
const { isStreaming, isCompacting } = session.state;
|
|
255
|
+
if (!isStreaming && !isCompacting)
|
|
256
|
+
return { kind: "prompt", text: args };
|
|
257
|
+
void session.prompt(args, { interrupt: true }).catch(() => undefined);
|
|
258
|
+
return { kind: "handled", message: msg().report.command.interruptSent(args) };
|
|
259
|
+
}
|
|
249
260
|
case "agents":
|
|
250
261
|
return { kind: "handled", message: describeAgents(session.state.sessionId) };
|
|
251
262
|
case "paste": {
|
|
@@ -376,6 +376,7 @@ export class AgentUi {
|
|
|
376
376
|
close: () => this.closeView(),
|
|
377
377
|
stop: (id) => stopTask(this.deps.session().state.sessionId, id),
|
|
378
378
|
background: (id) => this.deps.session().backgroundTask(id, "user"),
|
|
379
|
+
enterMode: () => this.deps.area?.ui().enterWhileRunning === "interrupt" ? "interrupt" : "queue",
|
|
379
380
|
messages: {
|
|
380
381
|
...(ui.showThinking !== undefined ? { showThinking: ui.showThinking } : {}),
|
|
381
382
|
...(ui.markdown !== undefined ? { markdown: ui.markdown } : {}),
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* 所查看的任务在等审批时标题显示「等待审批」,审批框照常以覆盖层弹在视图上面。
|
|
19
19
|
* [W7-C] `Ctrl+B`(`app.tasks.background`)转后台正在看的任务(只对前台任务有效,否则落回编辑器);
|
|
20
20
|
* 视图里另认 `/tasks bg [id]`。
|
|
21
|
+
* [打断并发送] `Ctrl+X`(`app.message.interrupt`)= 打断该子 Agent 当前回合并立即发送(`registry.message` 的
|
|
22
|
+
* interrupt);外部 Agent 的驱动不能中断回合时退回排队并提示。`ui.enterWhileRunning: "interrupt"` 时与 Enter 互换。
|
|
21
23
|
*/
|
|
22
24
|
import { type Component, type Focusable, type Keybindings, type Theme } from "../../tui.js";
|
|
23
25
|
import { type TaskSource } from "./agent-bar.js";
|
|
@@ -43,6 +45,8 @@ export interface AgentViewDeps {
|
|
|
43
45
|
background?(taskId: string): string[];
|
|
44
46
|
/** 消息区的显示选项(思考块、Markdown、紧凑)。 */
|
|
45
47
|
messages?: Omit<MessageViewOptions, "theme">;
|
|
48
|
+
/** `ui.enterWhileRunning`:`interrupt` 时 Enter 打断并发送、专用键排队。 */
|
|
49
|
+
enterMode?(): "queue" | "interrupt";
|
|
46
50
|
}
|
|
47
51
|
export declare class AgentView implements Component, Focusable {
|
|
48
52
|
private taskId;
|
|
@@ -67,6 +71,7 @@ export declare class AgentView implements Component, Focusable {
|
|
|
67
71
|
/** 每秒:运行中的工具重画耗时。 */
|
|
68
72
|
tick(): void;
|
|
69
73
|
private switchTo;
|
|
74
|
+
private enterInterrupts;
|
|
70
75
|
private submit;
|
|
71
76
|
handleInput(data: string): void;
|
|
72
77
|
private scroll;
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* 所查看的任务在等审批时标题显示「等待审批」,审批框照常以覆盖层弹在视图上面。
|
|
19
19
|
* [W7-C] `Ctrl+B`(`app.tasks.background`)转后台正在看的任务(只对前台任务有效,否则落回编辑器);
|
|
20
20
|
* 视图里另认 `/tasks bg [id]`。
|
|
21
|
+
* [打断并发送] `Ctrl+X`(`app.message.interrupt`)= 打断该子 Agent 当前回合并立即发送(`registry.message` 的
|
|
22
|
+
* interrupt);外部 Agent 的驱动不能中断回合时退回排队并提示。`ui.enterWhileRunning: "interrupt"` 时与 Enter 互换。
|
|
21
23
|
*/
|
|
22
24
|
import { msg } from "../../i18n/index.js";
|
|
23
25
|
import { Editor, isPrintableText, parseKey, truncateToWidth, visibleWidth, } from "../../tui.js";
|
|
@@ -47,7 +49,7 @@ export class AgentView {
|
|
|
47
49
|
maxVisibleLines: 4,
|
|
48
50
|
placeholder: msg().agents.view.placeholder(taskId),
|
|
49
51
|
requestRender: () => deps.render(),
|
|
50
|
-
onSubmit: (text) => void this.submit(text),
|
|
52
|
+
onSubmit: (text) => void this.submit(text, this.enterInterrupts()),
|
|
51
53
|
});
|
|
52
54
|
this.attach();
|
|
53
55
|
}
|
|
@@ -137,7 +139,10 @@ export class AgentView {
|
|
|
137
139
|
this.attach();
|
|
138
140
|
this.deps.onSwitch?.(next);
|
|
139
141
|
}
|
|
140
|
-
|
|
142
|
+
enterInterrupts() {
|
|
143
|
+
return this.deps.enterMode?.() === "interrupt";
|
|
144
|
+
}
|
|
145
|
+
async submit(text, interrupt = false) {
|
|
141
146
|
const m = msg().agents.view;
|
|
142
147
|
const trimmed = text.trim();
|
|
143
148
|
if (trimmed === "")
|
|
@@ -167,7 +172,7 @@ export class AgentView {
|
|
|
167
172
|
try {
|
|
168
173
|
if (registry === undefined)
|
|
169
174
|
throw new Error(m.unknown(taskId));
|
|
170
|
-
const reply = await registry.message(taskId, text);
|
|
175
|
+
const reply = await registry.message(taskId, text, interrupt);
|
|
171
176
|
this.flash = m[reply](taskId);
|
|
172
177
|
}
|
|
173
178
|
catch (error) {
|
|
@@ -188,6 +193,12 @@ export class AgentView {
|
|
|
188
193
|
return;
|
|
189
194
|
}
|
|
190
195
|
}
|
|
196
|
+
if (keys.matches(data, "app.message.interrupt") && !this.editor.isCompletionOpen) {
|
|
197
|
+
const text = this.editor.takeSubmission();
|
|
198
|
+
if (text !== null)
|
|
199
|
+
void this.submit(text, !this.enterInterrupts());
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
191
202
|
if (keys.matches(data, "tui.select.cancel") && !this.editor.isCompletionOpen) {
|
|
192
203
|
if (!empty)
|
|
193
204
|
this.editor.clear();
|
|
@@ -39,6 +39,7 @@ import { InteractiveCompletion } from "./completion.js";
|
|
|
39
39
|
import { confirmBypass } from "./confirm-dialog.js";
|
|
40
40
|
import { configUiFor } from "./config-ui.js";
|
|
41
41
|
import { createKeyDispatch } from "./key-dispatch.js";
|
|
42
|
+
import { enterModeOf, interruptKeys, interruptSender } from "./interrupt-send.js";
|
|
42
43
|
import { MessageView, exitSummaryLines } from "./message-view.js";
|
|
43
44
|
import { openPicker } from "./pickers.js";
|
|
44
45
|
import { modelPickerFor } from "./model-picker.js";
|
|
@@ -84,7 +85,9 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
84
85
|
spinner: () => loader.frame,
|
|
85
86
|
subagent: (toolCallId) => subagents.forToolCall(toolCallId),
|
|
86
87
|
});
|
|
87
|
-
const
|
|
88
|
+
const enterMode = () => enterModeOf(runtime);
|
|
89
|
+
const sendKeys = interruptKeys(keys, enterMode);
|
|
90
|
+
const queueView = new QueueView(theme, sendKeys.now);
|
|
88
91
|
const loaderSlot = new Container();
|
|
89
92
|
const loader = new Loader(() => tui.requestRender(), {
|
|
90
93
|
theme,
|
|
@@ -124,6 +127,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
124
127
|
requestRender: () => tui.requestRender(),
|
|
125
128
|
...(historyFile !== undefined ? { historyFile } : {}),
|
|
126
129
|
onSubmit: (text) => submit(text, "enter"),
|
|
130
|
+
onChange: () => indicator.refreshHints(),
|
|
127
131
|
});
|
|
128
132
|
tui.addChild(view);
|
|
129
133
|
tui.addChild(new Spacer());
|
|
@@ -161,7 +165,9 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
161
165
|
const indicator = new RunIndicator({
|
|
162
166
|
...{ theme, loader, slot: loaderSlot, tools, render },
|
|
163
167
|
...(backgroundKey !== undefined ? { backgroundKey } : {}),
|
|
168
|
+
...(sendKeys.key !== undefined ? { interruptKey: sendKeys.key } : {}),
|
|
164
169
|
});
|
|
170
|
+
Object.assign(indicator, { draft: () => !editor.isEmpty(), enterMode });
|
|
165
171
|
const setQueue = (steering, followUp) => {
|
|
166
172
|
status.setQueue(steering.length, followUp.length);
|
|
167
173
|
queueView.setQueue(steering, followUp);
|
|
@@ -361,6 +367,12 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
361
367
|
onPasteImage: () => void agentUi.paste(),
|
|
362
368
|
confirmMode,
|
|
363
369
|
submit: (text, via) => submit(text, via),
|
|
370
|
+
enterWhileRunning: enterMode,
|
|
371
|
+
interruptSend: interruptSender({
|
|
372
|
+
session: () => session,
|
|
373
|
+
takeImages: () => draftImages.splice(0),
|
|
374
|
+
showHint,
|
|
375
|
+
}),
|
|
364
376
|
runCommand: (line) => void runCommand(line),
|
|
365
377
|
exit: (code) => exit(code),
|
|
366
378
|
onInterrupted: (empty) => void rewind.afterInterrupt(empty),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 打断并立即发送(交互模式):运行中按 `app.message.interrupt`(缺省 `Ctrl+X`;`ui.enterWhileRunning:
|
|
3
|
+
* "interrupt"` 时是 Enter)→ `prompt(text, { interrupt: true })`:取走排队的插话、中止当前回合(工具按
|
|
4
|
+
* abort 收尾),以「插话… + 本条」开新回合(会话里 origin `interrupt`)。不走「中断即撤回」。
|
|
5
|
+
*/
|
|
6
|
+
import type { AgentSession } from "../../agent/types.js";
|
|
7
|
+
import type { ImageBlock } from "../../ai/types.js";
|
|
8
|
+
import type { Runtime } from "../../cli/runtime.js";
|
|
9
|
+
import type { Keybindings } from "../../tui.js";
|
|
10
|
+
import type { EnterMode } from "./run-indicator.js";
|
|
11
|
+
export interface InterruptSendDeps {
|
|
12
|
+
session(): AgentSession;
|
|
13
|
+
/** 取走输入框附带的图片(粘贴的剪贴板图片)。 */
|
|
14
|
+
takeImages(): ImageBlock[];
|
|
15
|
+
showHint(text: string): void;
|
|
16
|
+
}
|
|
17
|
+
/** 返回 `interruptSend(text)`:文字可为空(只把排队的插话立即送出);没有可发的内容给一行提示。 */
|
|
18
|
+
export declare function interruptSender(deps: InterruptSendDeps): (text: string) => void;
|
|
19
|
+
/** `ui.enterWhileRunning`(每次按键时读,`/config` 改了立即生效)。 */
|
|
20
|
+
export declare function enterModeOf(runtime: Pick<Runtime, "config">): EnterMode;
|
|
21
|
+
/** 专用键的标签与「立即送出排队插话」的键(interrupt 模式下是 Enter)。 */
|
|
22
|
+
export declare function interruptKeys(keys: Keybindings, mode: () => EnterMode): {
|
|
23
|
+
key: string | undefined;
|
|
24
|
+
now(): string | undefined;
|
|
25
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 打断并立即发送(交互模式):运行中按 `app.message.interrupt`(缺省 `Ctrl+X`;`ui.enterWhileRunning:
|
|
3
|
+
* "interrupt"` 时是 Enter)→ `prompt(text, { interrupt: true })`:取走排队的插话、中止当前回合(工具按
|
|
4
|
+
* abort 收尾),以「插话… + 本条」开新回合(会话里 origin `interrupt`)。不走「中断即撤回」。
|
|
5
|
+
*/
|
|
6
|
+
import { isAmaError } from "../../errors.js";
|
|
7
|
+
import { msg } from "../../i18n/index.js";
|
|
8
|
+
import { keyLabel } from "./task-background.js";
|
|
9
|
+
/** 返回 `interruptSend(text)`:文字可为空(只把排队的插话立即送出);没有可发的内容给一行提示。 */
|
|
10
|
+
export function interruptSender(deps) {
|
|
11
|
+
return (text) => {
|
|
12
|
+
const images = deps.takeImages();
|
|
13
|
+
deps.showHint(msg().interactive.keys.interruptSent);
|
|
14
|
+
const options = images.length > 0 ? { interrupt: true, images } : { interrupt: true };
|
|
15
|
+
void deps
|
|
16
|
+
.session()
|
|
17
|
+
.prompt(text, options)
|
|
18
|
+
.catch((error) => {
|
|
19
|
+
if (isAmaError(error) && error.code === "invalid_arguments")
|
|
20
|
+
deps.showHint(msg().interactive.keys.nothingToSend);
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** `ui.enterWhileRunning`(每次按键时读,`/config` 改了立即生效)。 */
|
|
25
|
+
export function enterModeOf(runtime) {
|
|
26
|
+
return runtime.config.ui?.enterWhileRunning === "interrupt" ? "interrupt" : "queue";
|
|
27
|
+
}
|
|
28
|
+
/** 专用键的标签与「立即送出排队插话」的键(interrupt 模式下是 Enter)。 */
|
|
29
|
+
export function interruptKeys(keys, mode) {
|
|
30
|
+
const key = keyLabel(keys, "app.message.interrupt");
|
|
31
|
+
return { key, now: () => (mode() === "interrupt" ? "Enter" : key) };
|
|
32
|
+
}
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* [W7-C] `app.tasks.background`(缺省 `Ctrl+B`):有阻塞中的前台任务时全部转后台(不看输入框),底部提示
|
|
17
17
|
* 「已转后台:t2 …」;没有时落回编辑器(光标左移),不吞键。Esc 中断只连带前台任务,仍在运行的后台任务
|
|
18
18
|
* 写进中断提示。
|
|
19
|
+
* [打断并发送] 运行中 Enter 缺省排队(steer);`app.message.interrupt`(缺省 `Ctrl+X`)= 打断当前回合并立即
|
|
20
|
+
* 以输入框的文字(连同排队的插话)开新回合(`prompt{interrupt:true}`)。`ui.enterWhileRunning: "interrupt"`
|
|
21
|
+
* 时两者互换。补全打开、有覆盖层(审批 / 选择器)时不触发;斜杠命令照常当命令;空闲时专用键等同 Enter。
|
|
19
22
|
*/
|
|
20
23
|
import type { AgentSession } from "../../agent/types.js";
|
|
21
24
|
import type { PermissionMode } from "../../permissions/types.js";
|
|
@@ -60,7 +63,11 @@ export interface KeyDispatchDeps {
|
|
|
60
63
|
confirmMode?(mode: PermissionMode): boolean | Promise<boolean>;
|
|
61
64
|
/** Ctrl+V:粘贴剪贴板图片(W5-U)。 */
|
|
62
65
|
onPasteImage?(): void;
|
|
63
|
-
submit(text: string, via: "followUp"): void;
|
|
66
|
+
submit(text: string, via: "enter" | "followUp"): void;
|
|
67
|
+
/** 运行中 Enter 的语义(`ui.enterWhileRunning`,缺省 `queue`)。 */
|
|
68
|
+
enterWhileRunning?(): "queue" | "interrupt";
|
|
69
|
+
/** 打断并立即发送(文字可为空:只把排队的插话立即送出)。没有时专用键不处理。 */
|
|
70
|
+
interruptSend?(text: string): void;
|
|
64
71
|
runCommand(line: string): void;
|
|
65
72
|
exit(code: number): void;
|
|
66
73
|
/** Esc 中断之后(参数:中断后输入框是否为空)。 */
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* [W7-C] `app.tasks.background`(缺省 `Ctrl+B`):有阻塞中的前台任务时全部转后台(不看输入框),底部提示
|
|
17
17
|
* 「已转后台:t2 …」;没有时落回编辑器(光标左移),不吞键。Esc 中断只连带前台任务,仍在运行的后台任务
|
|
18
18
|
* 写进中断提示。
|
|
19
|
+
* [打断并发送] 运行中 Enter 缺省排队(steer);`app.message.interrupt`(缺省 `Ctrl+X`)= 打断当前回合并立即
|
|
20
|
+
* 以输入框的文字(连同排队的插话)开新回合(`prompt{interrupt:true}`)。`ui.enterWhileRunning: "interrupt"`
|
|
21
|
+
* 时两者互换。补全打开、有覆盖层(审批 / 选择器)时不触发;斜杠命令照常当命令;空闲时专用键等同 Enter。
|
|
19
22
|
*/
|
|
20
23
|
import { msg } from "../../i18n/index.js";
|
|
21
24
|
import { ExitCode } from "../../cli/exit-codes.js";
|
|
@@ -99,6 +102,38 @@ export function createKeyDispatch(deps) {
|
|
|
99
102
|
else
|
|
100
103
|
void answer.then(settle);
|
|
101
104
|
};
|
|
105
|
+
/**
|
|
106
|
+
* 运行中的「排队 / 打断并发送」:专用键与(`interrupt` 模式下的)Enter。返回 false 交给编辑器
|
|
107
|
+
* (Enter 照常提交 = 排队;斜杠命令照常执行)。
|
|
108
|
+
*/
|
|
109
|
+
const sendKeys = (is) => {
|
|
110
|
+
if (deps.interruptSend === undefined || editor.isCompletionOpen)
|
|
111
|
+
return false;
|
|
112
|
+
const viaKey = is("app.message.interrupt");
|
|
113
|
+
if (!deps.busy()) {
|
|
114
|
+
if (!viaKey || editor.isEmpty())
|
|
115
|
+
return false;
|
|
116
|
+
const text = editor.takeSubmission();
|
|
117
|
+
if (text !== null)
|
|
118
|
+
deps.submit(text, "enter");
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
const interruptMode = deps.enterWhileRunning?.() === "interrupt";
|
|
122
|
+
if (!viaKey && !(interruptMode && is("tui.editor.submit")))
|
|
123
|
+
return false;
|
|
124
|
+
const command = editor.getExpandedText().trimStart().startsWith("/");
|
|
125
|
+
if (command || viaKey === interruptMode) {
|
|
126
|
+
// 排队(interrupt 模式下的专用键)或斜杠命令:同 Enter 提交
|
|
127
|
+
if (!viaKey)
|
|
128
|
+
return false;
|
|
129
|
+
const text = editor.takeSubmission();
|
|
130
|
+
if (text !== null)
|
|
131
|
+
deps.submit(text, "enter");
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
deps.interruptSend(editor.takeSubmission() ?? "");
|
|
135
|
+
return true;
|
|
136
|
+
};
|
|
102
137
|
/** 光标在最后一行(`↓` 不再下移,只会到行尾)。 */
|
|
103
138
|
const atLastLine = () => editor.cursor.line >= editor.getText().split("\n").length - 1;
|
|
104
139
|
/** `app.agents.focus`:进栏返回 true;落空给提示后返回 false(按键照常交给编辑器)。 */
|
|
@@ -170,6 +205,8 @@ export function createKeyDispatch(deps) {
|
|
|
170
205
|
return true;
|
|
171
206
|
}
|
|
172
207
|
ctrlCArmedAt = Number.NEGATIVE_INFINITY;
|
|
208
|
+
if (sendKeys(is))
|
|
209
|
+
return true;
|
|
173
210
|
if (is("app.interrupt") && !editor.isCompletionOpen && deps.busy()) {
|
|
174
211
|
esc.reset();
|
|
175
212
|
interrupt();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - stdin 是 TTY 且可开 raw:自带单行编辑器(line-editor.ts:历史、括号粘贴折叠),运行中回车
|
|
5
5
|
* = steer;Ctrl+C 运行中中断、空闲时连按两次退出(130);空行 Ctrl+D 退出;审批 y / a / N 单键问答。
|
|
6
|
+
* 运行中 `/interrupt <文本>` = 打断当前回合并立即以它开新回合(排队的插话在前)。
|
|
6
7
|
* 问句之前逐行打印执行前预览(`request.preview`,W3-B9a-2)。
|
|
7
8
|
* - 否则(管道):逐行读 stdin,每行依次执行(等上一条运行结束);没有审批 UI,ask → deny。
|
|
8
9
|
* 模型错误在运行结束时只打印一次(重试中只显示 ↻);有运行最终失败时退出码 1。
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - stdin 是 TTY 且可开 raw:自带单行编辑器(line-editor.ts:历史、括号粘贴折叠),运行中回车
|
|
5
5
|
* = steer;Ctrl+C 运行中中断、空闲时连按两次退出(130);空行 Ctrl+D 退出;审批 y / a / N 单键问答。
|
|
6
|
+
* 运行中 `/interrupt <文本>` = 打断当前回合并立即以它开新回合(排队的插话在前)。
|
|
6
7
|
* 问句之前逐行打印执行前预览(`request.preview`,W3-B9a-2)。
|
|
7
8
|
* - 否则(管道):逐行读 stdin,每行依次执行(等上一条运行结束);没有审批 UI,ask → deny。
|
|
8
9
|
* 模型错误在运行结束时只打印一次(重试中只显示 ↻);有运行最终失败时退出码 1。
|
|
@@ -134,13 +135,14 @@ export async function runLineMode(runtime, context, options = {}) {
|
|
|
134
135
|
const question = approvalQuestion(request, (taskId) => taskRegistryView(session.state.sessionId)?.get(taskId)?.agent);
|
|
135
136
|
void ed.ask(`\n${preview.join("")}${question}`, "n").then((answer) => {
|
|
136
137
|
resolve(answer === "y" ? "allow" : answer === "a" ? "allow_session" : "deny");
|
|
137
|
-
if (
|
|
138
|
+
if (running === 0)
|
|
138
139
|
ed.render();
|
|
139
140
|
});
|
|
140
141
|
}),
|
|
141
142
|
});
|
|
142
143
|
runtime.notifier.set((message, level) => err(`ama: [${level}] ${message}\n`));
|
|
143
|
-
|
|
144
|
+
/** 进行中的提示(`/interrupt` 开的新回合与被它中止的那一轮可能短暂重叠);> 0 = 运行中。 */
|
|
145
|
+
let running = 0;
|
|
144
146
|
let lastInterrupt = 0;
|
|
145
147
|
return new Promise((resolve) => {
|
|
146
148
|
let finished = false;
|
|
@@ -164,12 +166,27 @@ export async function runLineMode(runtime, context, options = {}) {
|
|
|
164
166
|
});
|
|
165
167
|
};
|
|
166
168
|
const run = (text) => {
|
|
167
|
-
|
|
169
|
+
running++;
|
|
168
170
|
void handle(text).then((outcome) => {
|
|
169
|
-
|
|
171
|
+
running--;
|
|
170
172
|
if (outcome === "exit")
|
|
171
173
|
finish(ExitCode.Ok);
|
|
172
|
-
else
|
|
174
|
+
else if (running === 0)
|
|
175
|
+
ed.render();
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
/** 运行中 `/interrupt <文本>`:打断当前回合并立即以它(排队的插话在前)开新回合。 */
|
|
179
|
+
const interruptWith = (text) => {
|
|
180
|
+
if (text === "")
|
|
181
|
+
return err(`ama: ${msg().report.command.interruptUsage}\n`);
|
|
182
|
+
out(`${msg().report.command.interruptSent(text)}\n`);
|
|
183
|
+
running++;
|
|
184
|
+
void session
|
|
185
|
+
.prompt(text, { interrupt: true })
|
|
186
|
+
.catch((error) => err(`ama: ${errorText(error)}\n`))
|
|
187
|
+
.finally(() => {
|
|
188
|
+
printer.endLine();
|
|
189
|
+
if (--running === 0)
|
|
173
190
|
ed.render();
|
|
174
191
|
});
|
|
175
192
|
};
|
|
@@ -179,7 +196,7 @@ export async function runLineMode(runtime, context, options = {}) {
|
|
|
179
196
|
if (action.kind === "eof")
|
|
180
197
|
return finish(ExitCode.Ok);
|
|
181
198
|
if (action.kind === "interrupt") {
|
|
182
|
-
if (
|
|
199
|
+
if (running > 0) {
|
|
183
200
|
void session.abort();
|
|
184
201
|
err(`${msg().interactive.line.interrupted}\n`);
|
|
185
202
|
}
|
|
@@ -193,7 +210,12 @@ export async function runLineMode(runtime, context, options = {}) {
|
|
|
193
210
|
}
|
|
194
211
|
continue;
|
|
195
212
|
}
|
|
196
|
-
if (
|
|
213
|
+
if (running > 0) {
|
|
214
|
+
const forced = /^\/interrupt(?:\s+([\s\S]*))?$/.exec(action.text.trim());
|
|
215
|
+
if (forced !== null) {
|
|
216
|
+
interruptWith((forced[1] ?? "").trim());
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
197
219
|
// [W7-C] 运行中 `/tasks bg [id]` 照样是命令(转后台阻塞中的前台任务),不当插话
|
|
198
220
|
if (/^\/tasks\s+bg(\s+\S+)?\s*$/.test(action.text.trim())) {
|
|
199
221
|
void handle(action.text.trim());
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
* [W7-C] 有阻塞中的前台任务时可丢弃项前面加 `Ctrl+B 转后台`;有停靠的审批时 `↓ Agent 栏` 换成
|
|
15
15
|
* `↓ 处理审批`。顺序 `Esc 中断 · Ctrl+B 转后台 · ↓ Agent 栏`,窄屏从后往前丢。
|
|
16
16
|
*
|
|
17
|
+
* [打断并发送] 输入框有字时可丢弃项最前面加 `Enter 排队 · Ctrl+X 打断并发送`(`ui.enterWhileRunning:
|
|
18
|
+
* "interrupt"` 时语义互换),顺序 `Esc 中断 · Enter 排队 · Ctrl+X 打断并发送 · Ctrl+B 转后台 · ↓ Agent 栏`。
|
|
19
|
+
*
|
|
17
20
|
* 排队消息:缩进 2 列(挂在当前回合下),`↳ 插话 / 之后 文本` 整体 muted、标签 dim;超过 3 条首行
|
|
18
|
-
* `… 另 N 条`;末行 `Alt+↑ 取回 · Esc
|
|
21
|
+
* `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`,有插话时中间加 `Ctrl+X 立即发送`(interrupt 模式是 Enter)。
|
|
19
22
|
*/
|
|
20
23
|
import type { SessionEvent } from "../../agent/types.js";
|
|
21
24
|
import { Container, Text, type Loader, type Theme } from "../../tui.js";
|
|
@@ -30,7 +33,11 @@ export interface RunIndicatorDeps {
|
|
|
30
33
|
render(): void;
|
|
31
34
|
/** [W7-C] `app.tasks.background` 的按键标签(`Ctrl+B`);没有绑定时不提示。 */
|
|
32
35
|
backgroundKey?: string;
|
|
36
|
+
/** `app.message.interrupt` 的按键标签(`Ctrl+X`);没有绑定时不提示。 */
|
|
37
|
+
interruptKey?: string;
|
|
33
38
|
}
|
|
39
|
+
/** 运行中 Enter 的语义(`ui.enterWhileRunning`)。 */
|
|
40
|
+
export type EnterMode = "queue" | "interrupt";
|
|
34
41
|
export declare class RunIndicator {
|
|
35
42
|
private readonly deps;
|
|
36
43
|
running: boolean;
|
|
@@ -44,8 +51,14 @@ export declare class RunIndicator {
|
|
|
44
51
|
background: () => boolean;
|
|
45
52
|
/** [W7-C] 有停靠在 Agent 栏的审批。 */
|
|
46
53
|
docked: () => boolean;
|
|
54
|
+
/** 输入框有字(显示 Enter / 专用键的提示)。 */
|
|
55
|
+
draft: () => boolean;
|
|
56
|
+
/** 运行中 Enter 的语义。 */
|
|
57
|
+
enterMode: () => EnterMode;
|
|
47
58
|
private hintKey;
|
|
48
59
|
constructor(deps: RunIndicatorDeps);
|
|
60
|
+
/** 可丢弃提示变了才换动词(每帧、输入框内容变化时调)。 */
|
|
61
|
+
refreshHints(): void;
|
|
49
62
|
/** 可丢弃的按键提示(按丢弃的逆序排列)。 */
|
|
50
63
|
private optionalHints;
|
|
51
64
|
get busy(): boolean;
|
|
@@ -61,6 +74,10 @@ export declare class RunIndicator {
|
|
|
61
74
|
/** 排队消息(插话 / 之后),挂在 Loader 之上。 */
|
|
62
75
|
export declare class QueueView extends Text {
|
|
63
76
|
private readonly theme;
|
|
64
|
-
|
|
77
|
+
private readonly nowKey;
|
|
78
|
+
/**
|
|
79
|
+
* @param nowKey 立即送出排队插话的键(专用键;interrupt 模式下是 Enter);undefined 不提示。
|
|
80
|
+
*/
|
|
81
|
+
constructor(theme: Theme, nowKey?: () => string | undefined);
|
|
65
82
|
setQueue(steering: readonly string[], followUp: readonly string[]): void;
|
|
66
83
|
}
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
* [W7-C] 有阻塞中的前台任务时可丢弃项前面加 `Ctrl+B 转后台`;有停靠的审批时 `↓ Agent 栏` 换成
|
|
15
15
|
* `↓ 处理审批`。顺序 `Esc 中断 · Ctrl+B 转后台 · ↓ Agent 栏`,窄屏从后往前丢。
|
|
16
16
|
*
|
|
17
|
+
* [打断并发送] 输入框有字时可丢弃项最前面加 `Enter 排队 · Ctrl+X 打断并发送`(`ui.enterWhileRunning:
|
|
18
|
+
* "interrupt"` 时语义互换),顺序 `Esc 中断 · Enter 排队 · Ctrl+X 打断并发送 · Ctrl+B 转后台 · ↓ Agent 栏`。
|
|
19
|
+
*
|
|
17
20
|
* 排队消息:缩进 2 列(挂在当前回合下),`↳ 插话 / 之后 文本` 整体 muted、标签 dim;超过 3 条首行
|
|
18
|
-
* `… 另 N 条`;末行 `Alt+↑ 取回 · Esc
|
|
21
|
+
* `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`,有插话时中间加 `Ctrl+X 立即发送`(interrupt 模式是 Enter)。
|
|
19
22
|
*/
|
|
20
23
|
import { msg } from "../../i18n/index.js";
|
|
21
24
|
import { Container, Text } from "../../tui.js";
|
|
@@ -58,19 +61,28 @@ export class RunIndicator {
|
|
|
58
61
|
background = () => false;
|
|
59
62
|
/** [W7-C] 有停靠在 Agent 栏的审批。 */
|
|
60
63
|
docked = () => false;
|
|
64
|
+
/** 输入框有字(显示 Enter / 专用键的提示)。 */
|
|
65
|
+
draft = () => false;
|
|
66
|
+
/** 运行中 Enter 的语义。 */
|
|
67
|
+
enterMode = () => "queue";
|
|
61
68
|
hintKey = "";
|
|
62
69
|
constructor(deps) {
|
|
63
70
|
this.deps = deps;
|
|
64
|
-
deps.loader.onFrame(() =>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
deps.loader.onFrame(() => this.refreshHints());
|
|
72
|
+
}
|
|
73
|
+
/** 可丢弃提示变了才换动词(每帧、输入框内容变化时调)。 */
|
|
74
|
+
refreshHints() {
|
|
75
|
+
if (this.busy && !this.approval && this.optionalHints().join("\n") !== this.hintKey)
|
|
76
|
+
this.applyVerb();
|
|
68
77
|
}
|
|
69
78
|
/** 可丢弃的按键提示(按丢弃的逆序排列)。 */
|
|
70
79
|
optionalHints() {
|
|
71
80
|
const down = this.deps.theme.glyphs.arrowDown;
|
|
72
81
|
const key = this.deps.backgroundKey;
|
|
73
82
|
const out = [];
|
|
83
|
+
const send = this.deps.interruptKey;
|
|
84
|
+
if (send !== undefined && this.draft())
|
|
85
|
+
out.push(msg().interactive.view.run.sendHint(this.enterMode(), send));
|
|
74
86
|
if (key !== undefined && this.background())
|
|
75
87
|
out.push(msg().agents.background.runHint(key));
|
|
76
88
|
if (this.docked())
|
|
@@ -200,9 +212,14 @@ export class RunIndicator {
|
|
|
200
212
|
/** 排队消息(插话 / 之后),挂在 Loader 之上。 */
|
|
201
213
|
export class QueueView extends Text {
|
|
202
214
|
theme;
|
|
203
|
-
|
|
215
|
+
nowKey;
|
|
216
|
+
/**
|
|
217
|
+
* @param nowKey 立即送出排队插话的键(专用键;interrupt 模式下是 Enter);undefined 不提示。
|
|
218
|
+
*/
|
|
219
|
+
constructor(theme, nowKey = () => undefined) {
|
|
204
220
|
super("");
|
|
205
221
|
this.theme = theme;
|
|
222
|
+
this.nowKey = nowKey;
|
|
206
223
|
}
|
|
207
224
|
setQueue(steering, followUp) {
|
|
208
225
|
const t = this.theme;
|
|
@@ -217,7 +234,12 @@ export class QueueView extends Text {
|
|
|
217
234
|
t.fg("dim", msg().interactive.view.run.queueMore(t.glyphs.ellipsis, rows.length - shown.length)));
|
|
218
235
|
}
|
|
219
236
|
if (rows.length > 0) {
|
|
220
|
-
|
|
237
|
+
const run = msg().interactive.view.run;
|
|
238
|
+
const now = steering.length > 0 ? this.nowKey() : undefined;
|
|
239
|
+
const hint = now === undefined
|
|
240
|
+
? run.queueHint(t.glyphs.arrowUp)
|
|
241
|
+
: run.queueHintNow(t.glyphs.arrowUp, now);
|
|
242
|
+
shown.push(" " + t.fg("dim", hint));
|
|
221
243
|
}
|
|
222
244
|
this.setText(shown.join("\n"));
|
|
223
245
|
}
|
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
* - [W6] compact 在行尾(宿主状态之后)追加订阅配额短项 `5h 10% wk 31%`(status-quota.ts),最先丢;
|
|
26
26
|
* 既有记号顺序不变。full 的配额在状态栏下方单独一行。
|
|
27
27
|
* - [W5-U] bash 在操作系统沙箱里跑时(S2)用量类项多一个 `沙箱`(与 codemode 同一丢弃优先级)。
|
|
28
|
-
* - [W5-U] 模型回退中(`model_fallback
|
|
28
|
+
* - [W5-U] 模型回退中(`model_fallback`)compact 模型项显示 `主模型 → 回退模型`,切回主模型后恢复;[W7] full
|
|
29
|
+
* 本行只显示主模型,`→ 回退模型` 作为开关类项在速率行左区。
|
|
30
|
+
* - [W7] full 左右分区:左 = 状态(权限模式、`shift+tab` 提示),右 = 模型与度量;开关类项在速率行左区,
|
|
31
|
+
* 配额在第三行右区(status-line.ts / status-quota.ts)。
|
|
29
32
|
* - 模型名缩写:宽 < 100 去掉供应商前缀,< 60 再去掉 `@渠道`,< 48 去掉版本后缀(第一个 `-数字` 起)。
|
|
30
33
|
* - ASCII:`⎇` → `git`(字形表 `branch`)、`−` → `-`、`♨` → `~`。
|
|
31
34
|
* - 宽度不够时按优先级丢弃(数字大的先丢,表见 §1.2);会变的数字按最宽形状占位(`reserve`),
|
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
* - [W6] compact 在行尾(宿主状态之后)追加订阅配额短项 `5h 10% wk 31%`(status-quota.ts),最先丢;
|
|
26
26
|
* 既有记号顺序不变。full 的配额在状态栏下方单独一行。
|
|
27
27
|
* - [W5-U] bash 在操作系统沙箱里跑时(S2)用量类项多一个 `沙箱`(与 codemode 同一丢弃优先级)。
|
|
28
|
-
* - [W5-U] 模型回退中(`model_fallback
|
|
28
|
+
* - [W5-U] 模型回退中(`model_fallback`)compact 模型项显示 `主模型 → 回退模型`,切回主模型后恢复;[W7] full
|
|
29
|
+
* 本行只显示主模型,`→ 回退模型` 作为开关类项在速率行左区。
|
|
30
|
+
* - [W7] full 左右分区:左 = 状态(权限模式、`shift+tab` 提示),右 = 模型与度量;开关类项在速率行左区,
|
|
31
|
+
* 配额在第三行右区(status-line.ts / status-quota.ts)。
|
|
29
32
|
* - 模型名缩写:宽 < 100 去掉供应商前缀,< 60 再去掉 `@渠道`,< 48 去掉版本后缀(第一个 `-数字` 起)。
|
|
30
33
|
* - ASCII:`⎇` → `git`(字形表 `branch`)、`−` → `-`、`♨` → `~`。
|
|
31
34
|
* - 宽度不够时按优先级丢弃(数字大的先丢,表见 §1.2);会变的数字按最宽形状占位(`reserve`),
|
|
@@ -121,9 +124,10 @@ export function layoutRow(parts, width, theme, style = {}) {
|
|
|
121
124
|
let kept = [...parts];
|
|
122
125
|
const zone = (z) => kept.filter((p) => p.zone === z);
|
|
123
126
|
const fits = () => {
|
|
124
|
-
const
|
|
127
|
+
const left = zone("left");
|
|
125
128
|
const right = zone("right");
|
|
126
|
-
|
|
129
|
+
const between = left.length === 0 || right.length === 0 ? 0 : sep.length;
|
|
130
|
+
return join(left)[1] + between + join(right)[1] <= width;
|
|
127
131
|
};
|
|
128
132
|
while (!fits()) {
|
|
129
133
|
const droppable = kept.filter((p) => p.priority !== undefined);
|
|
@@ -135,7 +139,14 @@ export function layoutRow(parts, width, theme, style = {}) {
|
|
|
135
139
|
const l = join(zone("left"))[0];
|
|
136
140
|
const r = join(zone("right"))[0];
|
|
137
141
|
const gap = width - visibleWidth(l) - visibleWidth(r);
|
|
138
|
-
|
|
142
|
+
// 左区为空时右区右对齐([W7] full 的速率行没有开关、配额行)
|
|
143
|
+
const line = r === ""
|
|
144
|
+
? l
|
|
145
|
+
: l === ""
|
|
146
|
+
? " ".repeat(Math.max(0, gap)) + r
|
|
147
|
+
: gap >= MIN_GAP
|
|
148
|
+
? l + " ".repeat(gap) + r
|
|
149
|
+
: l + dim(sep) + r;
|
|
139
150
|
return truncateToWidth(line, width);
|
|
140
151
|
}
|
|
141
152
|
/** 按终端宽度缩写模型名(§3.10)。 */
|
|
@@ -277,8 +288,8 @@ export class StatusBar {
|
|
|
277
288
|
}
|
|
278
289
|
const model = state.model === undefined ? "?" : abbreviateModel(formatModelRef(state.model), width);
|
|
279
290
|
const p = full ? FULL : COMPACT;
|
|
280
|
-
|
|
281
|
-
|
|
291
|
+
// 回退中:compact 显示 `主模型 → 回退模型`(回退模型 warning);full 本行只留主模型,`→ 回退模型` 在速率行左区
|
|
292
|
+
const fallback = full ? undefined : this.source.fallback?.();
|
|
282
293
|
const modelText = fallback === undefined
|
|
283
294
|
? theme.fg("accent", model)
|
|
284
295
|
: theme.fg("accent", abbreviateModel(fallback.from, width)) +
|
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
* 速率行(第五波 §1.1,D1、D2):`ui.statusLine: "full"` 时在状态栏上方多一行。[W5-A]
|
|
3
3
|
*
|
|
4
4
|
* ```text
|
|
5
|
-
* tps: 100 tok/s • 546 tok / 5.5s (avg 100 · ttft 1.4s)
|
|
5
|
+
* codemode on · preset x tps: 100 tok/s • 546 tok / 5.5s (avg 100 · ttft 1.4s) · ↑12.3k ↓1.2k · cache 83% ♨ · [-]
|
|
6
6
|
* ```
|
|
7
7
|
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* - [W7] 左右分区:左 = 状态 / 开关(`codemode on|only`〔网络未隔离追加 `net!`〕· `沙箱` · `preset 名` ·
|
|
9
|
+
* `→ 回退模型` · `queue N` · [宿主]),没有开关时左区为空、右区右对齐;右 = 度量:
|
|
10
|
+
* `tps: <速率>`(流式中是最近 2 s 窗口的瞬时值、前缀 accent;结束后是该请求的平均值;还没有请求时 `tps: —`)
|
|
11
|
+
* • `<输出 token> tok / <耗时>`(首 token 起算)`(avg <会话均速> · ttft <首 token 延迟>)` · `↑ ↓` · cache ·
|
|
12
|
+
* rebill,行尾 `[-]`(折叠提示:Ctrl+G 或 `/statusline compact`);`•` 走字形表(ASCII `*`);
|
|
13
13
|
* - 数据:`StatusBar.current()` 的 `telemetry` 与用量(同一次 getStats);流式中 `telemetry_tick` 时刷新;
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
14
|
+
* - 丢弃顺序(数字大先丢;`tps` 与 `[-]` 永不丢):右区度量先丢——tok/耗时 12 → token 11 → cache 10 →
|
|
15
|
+
* rebill 9 → avg 8 → ttft 7,左区开关最后丢——宿主 6 → queue 5 → 预设 4 → 回退 3 → 沙箱 2 → codemode 1;
|
|
16
|
+
* 数字按最宽形状占位,不随数值抖动;
|
|
16
17
|
* - `compact` 时不占行(render 返回空)。
|
|
17
18
|
* - [W6] 配色(用户参考):标签、单位、`•`、括号 dim;速率数字 `tool`(紫);输出量与耗时、avg 数字 accent(蓝);
|
|
18
|
-
* ttft 数字 `tool
|
|
19
|
+
* ttft 数字 `tool`;用量项 dim;回退模型 warning。
|
|
19
20
|
*/
|
|
20
21
|
import { type Component, type Theme } from "../../tui.js";
|
|
21
22
|
import { type StatusBar, type StatusBarSource } from "./status-bar.js";
|