pi-firecode 0.8.1 → 1.0.0
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/README.md +35 -15
- package/activity.ts +92 -0
- package/busy.ts +188 -0
- package/config.example.jsonc +38 -47
- package/config.ts +198 -172
- package/deliver.ts +100 -13
- package/flame.ts +149 -0
- package/format.ts +84 -31
- package/header.ts +208 -82
- package/index.ts +17 -11
- package/master/actions.ts +293 -0
- package/master/activity-list.ts +281 -0
- package/master/event-card.ts +18 -37
- package/master/event-format.ts +71 -40
- package/master/guard.ts +46 -0
- package/master/index.ts +88 -1036
- package/master/list-view.ts +129 -0
- package/master/outbox.ts +188 -0
- package/master/prompts/master.zh.md +17 -12
- package/master/prompts/worker.zh.md +1 -1
- package/master/role.ts +6 -1
- package/master/run.ts +315 -0
- package/master/runtime.ts +259 -0
- package/master/spawn.ts +79 -38
- package/master/state.ts +30 -6
- package/master/worker-view.ts +517 -0
- package/package.json +1 -1
- package/provider/claude-sub.ts +35 -13
- package/provider/openai-native/src/compact-client.ts +2 -14
- package/provider/openai-native/src/options.ts +4 -2
- package/review/advisor.ts +0 -2
- package/review/card.ts +36 -23
- package/review/checkpoint.ts +2 -8
- package/review/evidence.ts +42 -44
- package/review/index.ts +271 -525
- package/review/occupancy.ts +25 -0
- package/review/outcome.ts +31 -7
- package/review/prompt.ts +7 -8
- package/review/prompts/review.en.md +7 -6
- package/review/prompts/review.zh.md +7 -6
- package/review/reviewer.ts +2 -5
- package/review/session.ts +11 -30
- package/review/state.ts +32 -60
- package/review/ui.ts +30 -469
- package/round-recorder.ts +18 -0
- package/session/herdr-display.ts +1 -1
- package/session/presets.ts +130 -87
- package/session/quota.ts +123 -0
- package/session/stats.ts +4 -1
- package/statusbar/index.ts +242 -104
- package/statusbar/render.ts +95 -163
- package/theme.ts +2 -41
- package/tools/actions.ts +53 -0
- package/tools/assistant-view.ts +64 -0
- package/tools/click-anchor.ts +56 -0
- package/tools/group-view.ts +420 -0
- package/tools/grouping.ts +146 -221
- package/tools/host.ts +369 -0
- package/tools/index.ts +108 -105
- package/tools/line.ts +95 -58
- package/tools/machine.ts +78 -0
- package/tools/parts.ts +2 -2
- package/tools/round.ts +95 -0
- package/tools/turn-clock.ts +41 -0
- package/tools/turn-summary.ts +109 -0
- package/truncated-write.ts +18 -0
- package/watcher/card.ts +28 -22
- package/watcher/index.ts +16 -44
- package/watcher/observer.ts +3 -3
- package/watcher/prompts/watch.zh.md +1 -1
- package/watcher/transcript.ts +4 -12
- package/flame-frames.ts +0 -460
- package/review/progress.ts +0 -324
- package/session/bark.ts +0 -157
- package/session/working-flame.ts +0 -116
- package/statusbar/quota-cache.ts +0 -60
- package/statusbar/quota-parse.ts +0 -85
- package/statusbar/quota.ts +0 -203
- package/statusbar/tps.ts +0 -87
package/review/ui.ts
CHANGED
|
@@ -1,116 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* /fire-review
|
|
2
|
+
* /fire-review 的界面接管:esc 取消的只读编辑器与终端标题。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 审查进度不在这里画:它经占用频道(occupancy.ts)发布,由输入框外壳嵌进上边框,界面只此一处。
|
|
5
|
+
* Working 指示的可见性归输入框外壳(statusbar)统一管理,这里不碰。
|
|
6
6
|
*/
|
|
7
7
|
import { basename } from "node:path";
|
|
8
8
|
import {
|
|
9
9
|
CustomEditor,
|
|
10
|
-
DynamicBorder,
|
|
11
10
|
type ExtensionContext,
|
|
12
11
|
type KeybindingsManager,
|
|
13
|
-
type Theme,
|
|
14
12
|
} from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import {
|
|
16
|
-
type Component,
|
|
17
|
-
type EditorTheme,
|
|
18
|
-
type TUI,
|
|
19
|
-
truncateToWidth,
|
|
20
|
-
visibleWidth,
|
|
21
|
-
} from "@earendil-works/pi-tui";
|
|
22
|
-
import { clip, formatDuration } from "../format.js";
|
|
13
|
+
import type { EditorComponent, EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
23
14
|
import type { Language } from "../config.js";
|
|
24
|
-
import {
|
|
25
|
-
FLAME_FRAME_COUNT,
|
|
26
|
-
flameFrameLines,
|
|
27
|
-
flameFrameWidth,
|
|
28
|
-
} from "../flame-frames.js";
|
|
29
|
-
import type { ProgressTool, ReviewerProgress } from "./progress.js";
|
|
30
|
-
import type { Phase } from "./state.js";
|
|
15
|
+
import { clip } from "../format.js";
|
|
31
16
|
|
|
32
|
-
const WIDGET_KEY = "fire-review";
|
|
33
|
-
const FRAME_MS = 100;
|
|
34
|
-
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
35
|
-
const REVIEW_COLOR: readonly [number, number, number] = [255, 153, 102];
|
|
36
|
-
/** 火焰排版阈值:窄屏(48–59 列)收紧边距也要有火焰,<48 才退化为居中文本。 */
|
|
37
|
-
const FLAME_MIN_WIDTH = 48;
|
|
38
|
-
const FLAME_WIDE_WIDTH = 60;
|
|
39
|
-
const FLAME_MARGIN_MIN = 6;
|
|
40
|
-
const FLAME_MARGIN_NARROW = 2;
|
|
41
|
-
const FLAME_GAP_MIN = 8;
|
|
42
|
-
const FLAME_GAP_NARROW = 4;
|
|
43
|
-
const FLAME_GAP_IDEAL = 16;
|
|
44
17
|
let reviewTitleActive = false;
|
|
45
18
|
|
|
46
|
-
/** 活动条渲染所需的一切;executor 每次状态变化后重新提供。 */
|
|
47
|
-
export interface ActivityView {
|
|
48
|
-
phase: Phase;
|
|
49
|
-
round: number;
|
|
50
|
-
focus: string;
|
|
51
|
-
roundStartedAt: number;
|
|
52
|
-
progressStartedAt?: number;
|
|
53
|
-
reviewers: readonly ReviewerProgress[];
|
|
54
|
-
/** 当前 progress 属于谁:修复相据此判断能否把残留摘要当顾问裁决展示。 */
|
|
55
|
-
progressKind?: "reviewers" | "advisor";
|
|
56
|
-
advisorRunning: boolean;
|
|
57
|
-
consecutiveFailures?: number;
|
|
58
|
-
cwd?: string;
|
|
59
|
-
language: Language;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
type ViewSource = () => ActivityView | undefined;
|
|
63
|
-
|
|
64
|
-
export function showActivity(ctx: ExtensionContext, view: ViewSource): void {
|
|
65
|
-
if (ctx.hasUI === false) return;
|
|
66
|
-
if (typeof ctx.ui.setWorkingVisible === "function") ctx.ui.setWorkingVisible(false);
|
|
67
|
-
setReviewTitle(ctx, view());
|
|
68
|
-
if (typeof ctx.ui.setWidget !== "function") return;
|
|
69
|
-
ctx.ui.setWidget(
|
|
70
|
-
WIDGET_KEY,
|
|
71
|
-
(tui: TUI, theme: Theme) =>
|
|
72
|
-
new ActivityBar(view, theme, tui, () => tui.requestRender()),
|
|
73
|
-
{ placement: "aboveEditor" },
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function hideActivity(ctx: ExtensionContext): void {
|
|
78
|
-
if (ctx.hasUI === false) return;
|
|
79
|
-
if (typeof ctx.ui.setWorkingVisible === "function") ctx.ui.setWorkingVisible(true);
|
|
80
|
-
restoreReviewTitle(ctx);
|
|
81
|
-
if (typeof ctx.ui.setWidget === "function")
|
|
82
|
-
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
19
|
/**
|
|
86
20
|
* 审查等模型结论时(排队/审查中/顾问仲裁)接管编辑器:禁止输入,esc/Ctrl+C 随时取消。
|
|
21
|
+
* 返回解锁函数,还原成锁定前的编辑器工厂(可能是别的扩展设置的自定义编辑器)。
|
|
87
22
|
*
|
|
88
23
|
* 不能用全局输入钩子比对裸 \x1b:终端开启增强键盘协议后 esc 是带修饰的序列,
|
|
89
24
|
* 字面量比较会漏。这里统一走 keybindings 匹配。
|
|
90
25
|
* awaiting_fix 相不接管——那时是执行模型在改代码,用户应能正常输入与中断。
|
|
91
26
|
*/
|
|
92
|
-
export function lockEditor(
|
|
93
|
-
ctx
|
|
94
|
-
|
|
95
|
-
cancel: () => void,
|
|
96
|
-
): void {
|
|
97
|
-
if (ctx.hasUI === false || typeof ctx.ui.setEditorComponent !== "function") return;
|
|
27
|
+
export function lockEditor(ctx: ExtensionContext, cancel: () => void): () => void {
|
|
28
|
+
if (ctx.hasUI === false || typeof ctx.ui.setEditorComponent !== "function") return () => {};
|
|
29
|
+
const previous = ctx.ui.getEditorComponent();
|
|
98
30
|
ctx.ui.setEditorComponent((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
|
|
99
|
-
const
|
|
100
|
-
return
|
|
31
|
+
const frame = previous?.(tui, theme, keybindings) ?? new CustomEditor(tui, theme, keybindings);
|
|
32
|
+
return new ReviewEditor(tui, theme, keybindings, cancel, frame);
|
|
101
33
|
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
export function unlockEditor(ctx: ExtensionContext): void {
|
|
105
|
-
if (ctx.hasUI === false) return;
|
|
106
|
-
if (typeof ctx.ui.setEditorComponent === "function")
|
|
107
|
-
ctx.ui.setEditorComponent(undefined);
|
|
34
|
+
return () => ctx.ui.setEditorComponent(previous);
|
|
108
35
|
}
|
|
109
36
|
|
|
110
37
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* 动效由上方活动条负责,这里静态提示即可。
|
|
38
|
+
* 审查期间的只读编辑器:输入区收起成一行暗色提示,上下边框取自 frame(锁定前的编辑器),
|
|
39
|
+
* 外壳状态(审查进度、会话标题、模型)因此保持可见。
|
|
114
40
|
*/
|
|
115
41
|
class ReviewEditor extends CustomEditor {
|
|
116
42
|
constructor(
|
|
@@ -118,408 +44,43 @@ class ReviewEditor extends CustomEditor {
|
|
|
118
44
|
theme: EditorTheme,
|
|
119
45
|
private readonly keys: KeybindingsManager,
|
|
120
46
|
private readonly cancel: () => void,
|
|
47
|
+
private readonly frame: EditorComponent,
|
|
121
48
|
) {
|
|
122
49
|
super(tui, theme, keys);
|
|
123
50
|
}
|
|
124
51
|
|
|
125
52
|
override handleInput(data: string): void {
|
|
126
|
-
if (
|
|
127
|
-
this.keys.matches(data, "app.interrupt") ||
|
|
128
|
-
this.keys.matches(data, "app.clear")
|
|
129
|
-
) {
|
|
130
|
-
this.cancel();
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
53
|
+
if (this.keys.matches(data, "app.interrupt") || this.keys.matches(data, "app.clear")) this.cancel();
|
|
133
54
|
// 审查期间不接受任何其他输入:插话会污染本轮审查的会话证据。
|
|
134
55
|
}
|
|
135
56
|
|
|
136
|
-
override render(
|
|
137
|
-
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ---- 组件 ----
|
|
143
|
-
|
|
144
|
-
abstract class Animated implements Component {
|
|
145
|
-
protected frame = 0;
|
|
146
|
-
private readonly timer: ReturnType<typeof setInterval>;
|
|
147
|
-
|
|
148
|
-
constructor(requestRender: () => void) {
|
|
149
|
-
this.timer = setInterval(() => {
|
|
150
|
-
this.frame += 1;
|
|
151
|
-
requestRender();
|
|
152
|
-
}, FRAME_MS);
|
|
153
|
-
this.timer.unref?.();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
abstract render(width: number): string[];
|
|
157
|
-
|
|
158
|
-
invalidate(): void {}
|
|
159
|
-
|
|
160
|
-
dispose(): void {
|
|
161
|
-
clearInterval(this.timer);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
protected spinner() {
|
|
165
|
-
return SPINNER[this.frame % SPINNER.length];
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
class ActivityBar extends Animated {
|
|
170
|
-
/** 官方边框组件;颜色仍用品牌橙,边框字符与宽度处理交给宿主。 */
|
|
171
|
-
private readonly border = new DynamicBorder(reviewColor);
|
|
172
|
-
|
|
173
|
-
constructor(
|
|
174
|
-
private readonly view: ViewSource,
|
|
175
|
-
private readonly theme: Theme,
|
|
176
|
-
private readonly tui: TUI,
|
|
177
|
-
requestRender: () => void,
|
|
178
|
-
) {
|
|
179
|
-
super(requestRender);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
render(width: number): string[] {
|
|
183
|
-
const view = this.view();
|
|
184
|
-
if (!view) return [];
|
|
185
|
-
const safeWidth = Math.max(1, width);
|
|
186
|
-
const terminalRows = (this.tui as TUI & { terminal?: { rows?: number } }).terminal?.rows;
|
|
187
|
-
const maxTotalLines = terminalRows ? Math.floor(terminalRows * 0.7) : 16;
|
|
188
|
-
if (maxTotalLines <= 0) return [];
|
|
189
|
-
if (maxTotalLines === 1) {
|
|
190
|
-
return [centerLine(boldText(this.theme.fg("muted", activityTitle(view))), safeWidth)];
|
|
191
|
-
}
|
|
192
|
-
if (maxTotalLines === 2) {
|
|
193
|
-
const line1 = boldText(this.theme.fg("muted", activityTitle(view)));
|
|
194
|
-
let line2 = view.reviewers.map((r) => `${statusIcon(r.status)} ${shortModelName(r.label)}`).join(" ");
|
|
195
|
-
if (visibleWidth(line2) > safeWidth) {
|
|
196
|
-
line2 = view.reviewers.map((r) => shortModelName(r.label)).join(" ");
|
|
197
|
-
}
|
|
198
|
-
return [
|
|
199
|
-
centerLine(line1, safeWidth),
|
|
200
|
-
centerLine(this.theme.fg("muted", truncateToWidth(line2, safeWidth, "…")), safeWidth),
|
|
201
|
-
];
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const n = view.reviewers.length;
|
|
205
|
-
// 极矮预算模式(总行数放不下边框+标题+全员):去掉边框,将总预算行数全部分配给审查者
|
|
206
|
-
if (maxTotalLines <= n + 1) {
|
|
207
|
-
// 每人一行也装不下时按行分组压缩:丢行动作可以,静默丢审查者不行。
|
|
208
|
-
if (maxTotalLines <= n) {
|
|
209
|
-
const perLine = Math.ceil(n / maxTotalLines);
|
|
210
|
-
const lines: string[] = [];
|
|
211
|
-
for (let i = 0; i < n; i += perLine) {
|
|
212
|
-
const group = view.reviewers
|
|
213
|
-
.slice(i, i + perLine)
|
|
214
|
-
.map((r) => `${statusIcon(r.status)} ${shortModelName(r.label)}`)
|
|
215
|
-
.join(" ");
|
|
216
|
-
lines.push(centerLine(group, safeWidth));
|
|
217
|
-
}
|
|
218
|
-
return lines;
|
|
219
|
-
}
|
|
220
|
-
return view.reviewers.map((r) => {
|
|
221
|
-
const raw = r.details?.[0] || r.summary || (r.status === "running" ? r.action : "通过");
|
|
222
|
-
const act = raw.replace(/^[↳\s]+/u, "");
|
|
223
|
-
return centerLine(`${statusIcon(r.status)} ${shortModelName(r.label)}: ${act}${elapsedSuffix(r)}`, safeWidth);
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const border = this.border.render(safeWidth)[0] ?? "";
|
|
228
|
-
const maxBodyLines = Math.max(1, maxTotalLines - 2);
|
|
229
|
-
const content = this.contentRows(view, maxBodyLines, safeWidth);
|
|
230
|
-
const body = safeWidth >= FLAME_MIN_WIDTH
|
|
231
|
-
? this.renderFlameBody(content, safeWidth, maxBodyLines)
|
|
232
|
-
: content.map((line) => centerLine(line, safeWidth));
|
|
233
|
-
const finalLines = [border, ...body.slice(0, maxBodyLines), border];
|
|
234
|
-
return finalLines.slice(0, maxTotalLines);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private contentRows(view: ActivityView, maxBodyLines: number, width: number): string[] {
|
|
238
|
-
const title = boldText(this.theme.fg("muted", activityTitle(view)));
|
|
239
|
-
const hintText = activityHint(view);
|
|
240
|
-
const hint = hintText ? this.theme.fg("dim", hintText) : "";
|
|
241
|
-
const reviewers = view.reviewers;
|
|
242
|
-
const n = reviewers.length;
|
|
243
|
-
|
|
244
|
-
// 紧凑纵向模式(放不下多行展开但每人一行能装下):单行纵向用满整宽输出
|
|
245
|
-
if (maxBodyLines < n + 1) {
|
|
246
|
-
return reviewers.map((r) => {
|
|
247
|
-
const raw = r.details?.[0] || r.summary || (r.status === "running" ? r.action : "通过");
|
|
248
|
-
const act = raw.replace(/^[↳\s]+/u, "");
|
|
249
|
-
return `${statusIcon(r.status)} ${shortModelName(r.label)}: ${act}${elapsedSuffix(r)}`;
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// 中等/多行展开模式:只要行数足够容纳审查者多行展开
|
|
254
|
-
if (maxBodyLines >= n * 2 + 1) {
|
|
255
|
-
// 预留标题/提示/呼吸空行的开销后再分每人行预算。
|
|
256
|
-
const budgetPer = Math.max(2, Math.floor((maxBodyLines - (hint ? 6 : 4)) / n));
|
|
257
|
-
const details = activityRows(view, this.spinner(), maxBodyLines, budgetPer);
|
|
258
|
-
return breathe(title, details, hint, maxBodyLines);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// 紧凑单行模式:标题 + 单行审查者;有余量时同样补呼吸空行。
|
|
262
|
-
const details = activityRows(view, this.spinner(), maxBodyLines - 1, 1);
|
|
263
|
-
return breathe(title, details, hint, maxBodyLines);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private renderFlameBody(contentRows: string[], width: number, maxBodyLines: number) {
|
|
267
|
-
const wide = width >= FLAME_WIDE_WIDTH;
|
|
268
|
-
const margin = wide ? FLAME_MARGIN_MIN : FLAME_MARGIN_NARROW;
|
|
269
|
-
const gapMin = wide ? FLAME_GAP_MIN : FLAME_GAP_NARROW;
|
|
270
|
-
const flameHeight = Math.min(maxBodyLines, Math.max(1, contentRows.length));
|
|
271
|
-
const rawFlame = flameFrameLines(flameHeight, this.frame % FLAME_FRAME_COUNT);
|
|
272
|
-
const flameWidth = flameFrameWidth(flameHeight);
|
|
273
|
-
const contentWidth = Math.min(
|
|
274
|
-
Math.max(1, ...contentRows.map((row) => visibleWidth(row))),
|
|
275
|
-
Math.max(1, width - margin * 2 - gapMin - flameWidth),
|
|
276
|
-
);
|
|
277
|
-
const gap = Math.max(
|
|
278
|
-
gapMin,
|
|
279
|
-
Math.min(
|
|
280
|
-
FLAME_GAP_IDEAL,
|
|
281
|
-
width - margin * 2 - contentWidth - flameWidth,
|
|
282
|
-
),
|
|
283
|
-
);
|
|
284
|
-
const group = contentWidth + gap + flameWidth;
|
|
285
|
-
const indent = " ".repeat(
|
|
286
|
-
Math.max(margin, Math.floor((width - group) / 2)),
|
|
287
|
-
);
|
|
288
|
-
const paddedRows = [...contentRows];
|
|
289
|
-
while (paddedRows.length < flameHeight) paddedRows.push("");
|
|
290
|
-
return rawFlame.map((line, index) => {
|
|
291
|
-
const row = truncateToWidth(paddedRows[index] ?? "", contentWidth, "…");
|
|
292
|
-
const padding = " ".repeat(
|
|
293
|
-
Math.max(0, contentWidth - visibleWidth(row) + gap),
|
|
294
|
-
);
|
|
295
|
-
const flame = `${truncateToWidth(line, flameWidth, "")}\x1b[0m`;
|
|
296
|
-
return `${indent}${row}${padding}${flame}`;
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// ---- 文案与样式 ----
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* 呼吸空行:预算内按优先级补回排版留白(标题后 > 提示前 > 顶部 > 底部),
|
|
305
|
-
* 预算不够时逐个放弃、退回紧凑排列;超出部分由调用方的 slice 兑底。
|
|
306
|
-
*/
|
|
307
|
-
function breathe(title: string, details: string[], hint: string, maxBodyLines: number): string[] {
|
|
308
|
-
const mandatory = 1 + details.length + (hint ? 1 : 0);
|
|
309
|
-
let spare = maxBodyLines - mandatory;
|
|
310
|
-
const afterTitle = spare > 0 && (spare -= 1) >= 0;
|
|
311
|
-
const beforeHint = hint !== "" && spare > 0 && (spare -= 1) >= 0;
|
|
312
|
-
const top = spare > 0 && (spare -= 1) >= 0;
|
|
313
|
-
const bottom = spare > 0 && (spare -= 1) >= 0;
|
|
314
|
-
return [
|
|
315
|
-
...(top ? [""] : []),
|
|
316
|
-
title,
|
|
317
|
-
...(afterTitle ? [""] : []),
|
|
318
|
-
...details,
|
|
319
|
-
...(hint ? [...(beforeHint ? [""] : []), hint] : []),
|
|
320
|
-
...(bottom ? [""] : []),
|
|
321
|
-
].slice(0, maxBodyLines);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function activityTitle(view: ActivityView) {
|
|
325
|
-
const { language } = view;
|
|
326
|
-
if (view.phase === "queued")
|
|
327
|
-
return language === "en" ? "🔥 Running" : "🔥 执行中";
|
|
328
|
-
if (view.phase === "needs_fix") {
|
|
329
|
-
// 连败轮数并进标题:正文首行留给顾问模型行,与审查相的布局对齐。
|
|
330
|
-
const failures = view.consecutiveFailures ?? 0;
|
|
331
|
-
const base = language === "en" ? "🔥 Advisor consulting" : "🔥 顾问介入中";
|
|
332
|
-
if (failures <= 0) return base;
|
|
333
|
-
return language === "en"
|
|
334
|
-
? `${base} · ${failures} straight fails`
|
|
335
|
-
: `${base} · 连续 ${failures} 轮未过`;
|
|
57
|
+
override render(width: number): string[] {
|
|
58
|
+
const lines = this.frame.render(width);
|
|
59
|
+
if (lines.length < 2) return lines;
|
|
60
|
+
const keys = this.keys.getKeys("app.interrupt").join("/").replaceAll("escape", "esc") || "esc";
|
|
61
|
+
const hint = clip(` 审查进行中 · ${keys} 取消`, width, "end", "");
|
|
62
|
+
return [lines[0], `\x1b[2m${hint}\x1b[22m`, lines[lines.length - 1]];
|
|
336
63
|
}
|
|
337
|
-
const phase = view.phase === "awaiting_fix"
|
|
338
|
-
? language === "en" ? "Repair in progress" : "修复中"
|
|
339
|
-
: language === "en" ? "Review in progress" : "审查中";
|
|
340
|
-
return `🔥 ${roundTitle(view.round, phase, language)}`;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function activityRows(view: ActivityView, spinner: string, budget = 16, budgetPer = 2): string[] {
|
|
344
|
-
if (view.phase === "queued")
|
|
345
|
-
return [view.language === "en" ? "Runs a review automatically when done" : "完成后自动审查"];
|
|
346
|
-
if (view.phase === "awaiting_fix") {
|
|
347
|
-
// 顾问落定与进入修复相在同一微任务链内,needs_fix 相没有渲染机会:
|
|
348
|
-
// 裁决摘要在迁入相展示,才有可见时机。
|
|
349
|
-
const advisorLine = view.progressKind === "advisor"
|
|
350
|
-
? view.reviewers.find((reviewer) => reviewer.summary)?.summary
|
|
351
|
-
: undefined;
|
|
352
|
-
return [
|
|
353
|
-
view.language === "en"
|
|
354
|
-
? `Repairing Round ${view.round} review feedback`
|
|
355
|
-
: `正在修复第 ${view.round} 轮审查反馈`,
|
|
356
|
-
...(advisorLine ? [`💡 ${clip(advisorLine, 64)}`] : []),
|
|
357
|
-
];
|
|
358
|
-
}
|
|
359
|
-
// 连败轮数已并入标题;正文首行即顾问模型行,与审查者行同格式。
|
|
360
|
-
if (view.phase === "needs_fix")
|
|
361
|
-
return view.reviewers.flatMap((reviewer) =>
|
|
362
|
-
reviewerActivityRows(reviewer, spinner, view.language, 1, true, budget, budgetPer),
|
|
363
|
-
);
|
|
364
|
-
const blocks = view.reviewers.map((reviewer) =>
|
|
365
|
-
reviewerActivityRows(reviewer, spinner, view.language, view.reviewers.length, false, budget, budgetPer),
|
|
366
|
-
);
|
|
367
|
-
// 模型之间空一行:只在预算容得下内容+分隔+标题提示时才加,紧屏不挤掉正文。
|
|
368
|
-
const total = blocks.reduce((sum, block) => sum + block.length, 0);
|
|
369
|
-
const spaced = blocks.length > 1 && budget >= total + blocks.length - 1 + 2;
|
|
370
|
-
return blocks.flatMap((block, index) => (spaced && index > 0 ? ["", ...block] : block));
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function reviewerActivityRows(
|
|
374
|
-
reviewer: ReviewerProgress,
|
|
375
|
-
spinner: string,
|
|
376
|
-
language: Language,
|
|
377
|
-
totalReviewers: number,
|
|
378
|
-
isAdvisor = false,
|
|
379
|
-
budget = 16,
|
|
380
|
-
budgetPer = 2,
|
|
381
|
-
): string[] {
|
|
382
|
-
const settled = reviewer.status !== "running";
|
|
383
|
-
const multiline = totalReviewers <= 3 && budgetPer >= 2;
|
|
384
|
-
const label = isAdvisor
|
|
385
|
-
? `${language === "en" ? "Advisor" : "顾问"} ${reviewer.label}`
|
|
386
|
-
: reviewer.label;
|
|
387
|
-
|
|
388
|
-
const elapsed = elapsedSuffix(reviewer);
|
|
389
|
-
if (settled) {
|
|
390
|
-
const icon = statusIcon(reviewer.status);
|
|
391
|
-
const details = reviewer.details && reviewer.details.length > 0
|
|
392
|
-
? reviewer.details
|
|
393
|
-
: [reviewer.summary || (reviewer.status === "passed" ? "审查通过" : "发现问题")];
|
|
394
|
-
|
|
395
|
-
if (!multiline) {
|
|
396
|
-
const text = details[0]?.replace(/^[↳\s]+/u, "") || "";
|
|
397
|
-
return [`${icon} ${label} · ${clip(text, 48)}${elapsed}`];
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const statusText = reviewer.status === "passed"
|
|
401
|
-
? (language === "en" ? "Passed" : "通过")
|
|
402
|
-
: reviewer.status === "failed"
|
|
403
|
-
? (language === "en" ? "Failed" : "未通过")
|
|
404
|
-
: (language === "en" ? "Error" : "异常");
|
|
405
|
-
const firstDetail = details[0] ? ` · ${details[0]}` : ` · ${statusText}`;
|
|
406
|
-
const lines = [`${icon} ${label}${firstDetail}${elapsed}`];
|
|
407
|
-
for (const d of details.slice(1, budgetPer)) {
|
|
408
|
-
const clean = d.replace(/^[↳\s]+/u, "").trim();
|
|
409
|
-
if (clean) lines.push(` ↳ ${clip(clean, 54)}`);
|
|
410
|
-
}
|
|
411
|
-
return lines;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (reviewer.toolCalls === 0) {
|
|
415
|
-
return [`${spinner} ${label} · ${language === "en" ? "Thinking" : "思考中"}${elapsed}`];
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (!multiline) {
|
|
419
|
-
return [`${spinner} ${label} · ${reviewer.action} · ${callsText(reviewer.toolCalls, language)}${elapsed}`];
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// 动作按时间顺序滚动(旧在上新在下),不标「当前/历史」。
|
|
423
|
-
const lines = [`${spinner} ${label} · ${callsText(reviewer.toolCalls, language)}${elapsed}`];
|
|
424
|
-
const prev = reviewer.recentTools?.at(-1);
|
|
425
|
-
if (prev) lines.push(` ↳ ${clip(formatTool(prev, language), 50)}`);
|
|
426
|
-
lines.push(` ↳ ${clip(reviewer.action, 50)}`);
|
|
427
|
-
return lines;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function formatTool(tool: ProgressTool, language: Language): string {
|
|
431
|
-
const verb = language === "en" ? tool.tool : tool.tool === "read" ? "读" : tool.tool === "bash" ? "跑" : tool.tool === "edit" ? "改" : tool.tool === "write" ? "写" : tool.tool;
|
|
432
|
-
const target = tool.args.split("/").pop() || tool.args;
|
|
433
|
-
return `${verb} ${target}`;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/** 每模型耗时后缀的唯一出口:运行中走表、落定冻结;所有携带每模型内容的行都拼它。 */
|
|
437
|
-
function elapsedSuffix(reviewer: ReviewerProgress): string {
|
|
438
|
-
if (!reviewer.startedAt) return "";
|
|
439
|
-
const elapsedMs = Math.max(0, (reviewer.settledAt ?? Date.now()) - reviewer.startedAt);
|
|
440
|
-
return elapsedMs > 0 ? ` · ${formatDuration(elapsedMs)}` : "";
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/** 审查者状态图标的唯一映射;运行中的动态 spinner 由调用方另行处理。 */
|
|
444
|
-
function statusIcon(status: ReviewerProgress["status"]): string {
|
|
445
|
-
if (status === "passed") return "✅";
|
|
446
|
-
if (status === "failed") return "❌";
|
|
447
|
-
if (status === "error") return "⚠️";
|
|
448
|
-
return "⠋";
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function shortModelName(label: string): string {
|
|
452
|
-
let s = label.replace(/^gpt-5\.6-/u, "").replace(/-\d+k$/u, "");
|
|
453
|
-
s = s.replace(/^(?:claude-3-[57]-|claude-)/u, "c37-").replace(/^(?:gemini-2\.[05]-|gemini-)/u, "g25-");
|
|
454
|
-
if (s.startsWith("c37-sonnet") || s === "c37-3-7-sonnet") return "c37";
|
|
455
|
-
if (s.startsWith("g25-pro") || s === "g25-2.5-pro") return "g25";
|
|
456
|
-
return s.length > 7 ? `${s.slice(0, 6)}…` : s;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function activityHint(view: ActivityView) {
|
|
460
|
-
if (view.phase === "awaiting_fix") return undefined;
|
|
461
|
-
if (view.phase === "queued")
|
|
462
|
-
return view.language === "en"
|
|
463
|
-
? "Esc/Ctrl+C cancel automatic review"
|
|
464
|
-
: "Esc/Ctrl+C 取消自动审查";
|
|
465
|
-
if (view.phase === "needs_fix")
|
|
466
|
-
return view.language === "en"
|
|
467
|
-
? "Esc/Ctrl+C skip consult"
|
|
468
|
-
: "Esc/Ctrl+C 跳过咨询";
|
|
469
|
-
return view.language === "en"
|
|
470
|
-
? "Esc/Ctrl+C cancel"
|
|
471
|
-
: "Esc/Ctrl+C 取消";
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function roundTitle(round: number, title: string, language: Language) {
|
|
475
|
-
if (round <= 1) return title;
|
|
476
|
-
return language === "en" ? `Round ${round} ${title}` : `第 ${round} 轮${title}`;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function callsText(calls: number, language: Language) {
|
|
480
|
-
return language === "en" ? `${calls} calls` : `${calls} 次调用`;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function centerLine(line: string, width: number) {
|
|
484
|
-
if (width <= 0) return "";
|
|
485
|
-
const text = truncateToWidth(line, width, "…");
|
|
486
|
-
const padding = Math.max(0, width - visibleWidth(text));
|
|
487
|
-
const left = Math.floor(padding / 2);
|
|
488
|
-
return `${" ".repeat(left)}${text}${" ".repeat(padding - left)}`;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function boldText(text: string) {
|
|
492
|
-
return `\x1b[1m${text}\x1b[22m`;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function reviewColor(text: string) {
|
|
496
|
-
const [red, green, blue] = REVIEW_COLOR;
|
|
497
|
-
return `\x1b[38;2;${red};${green};${blue}m${text}\x1b[39m`;
|
|
498
64
|
}
|
|
499
65
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
getCwd?: () => unknown;
|
|
505
|
-
};
|
|
66
|
+
/** 审查期间终端标题写明“审查中 R轮次 · 会话名”,结束后还原。 */
|
|
67
|
+
export function showReviewTitle(ctx: ExtensionContext, round: number, language: Language) {
|
|
68
|
+
if (!ctx.hasUI || typeof ctx.ui.setTitle !== "function") return;
|
|
69
|
+
const manager = ctx.sessionManager as { getSessionName?: () => unknown; getCwd?: () => unknown };
|
|
506
70
|
const rawName = manager.getSessionName?.();
|
|
507
71
|
const rawCwd = manager.getCwd?.();
|
|
508
72
|
const who = typeof rawName === "string" && rawName
|
|
509
73
|
? rawName
|
|
510
74
|
: typeof rawCwd === "string" ? basename(rawCwd) : "";
|
|
511
|
-
const label =
|
|
75
|
+
const label = language === "en" ? "Reviewing" : "审查中";
|
|
512
76
|
reviewTitleActive = true;
|
|
513
|
-
ctx.ui.setTitle(`${label}${
|
|
77
|
+
ctx.ui.setTitle(`${label}${round > 0 ? ` R${round}` : ""}${who ? ` · ${who}` : ""}`);
|
|
514
78
|
}
|
|
515
79
|
|
|
516
|
-
function
|
|
80
|
+
export function hideReviewTitle(ctx: ExtensionContext) {
|
|
517
81
|
if (!reviewTitleActive || !ctx.hasUI || typeof ctx.ui.setTitle !== "function") return;
|
|
518
82
|
reviewTitleActive = false;
|
|
519
|
-
const manager = ctx.sessionManager as {
|
|
520
|
-
getSessionName?: () => unknown;
|
|
521
|
-
getCwd?: () => unknown;
|
|
522
|
-
};
|
|
83
|
+
const manager = ctx.sessionManager as { getSessionName?: () => unknown; getCwd?: () => unknown };
|
|
523
84
|
const rawName = manager.getSessionName?.();
|
|
524
85
|
const rawCwd = manager.getCwd?.();
|
|
525
86
|
const name = typeof rawName === "string" && rawName ? rawName : undefined;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轮记录器:会话歇下时把这一段的整段时长、终态与均速写成轮记录(CustomEntry),写入后在进程内总线上通知。
|
|
3
|
+
* 每个会话都注册——主会话与每个子代理会话跑的是同一段代码、同一个歇下判定(busy.ts),与界面无关:
|
|
4
|
+
* 主会话的摘要行、输入框外壳、子代理全过程视图都只读记录;Master 结果事件的“本次运行”耗时也读子代理会话里的
|
|
5
|
+
* 这份记录,不再在指挥官这边另外计时——同一段时长只有一个事实源,视图与事件不会对不上。
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { watchBusy } from "./busy.js";
|
|
9
|
+
import { ROUND_ENTRY, ROUND_RECORDED_CHANNEL } from "./tools/round.js";
|
|
10
|
+
|
|
11
|
+
export function registerRoundRecorder(pi: ExtensionAPI): void {
|
|
12
|
+
watchBusy(pi, {
|
|
13
|
+
onSettled: (_ctx, round) => {
|
|
14
|
+
pi.appendEntry(ROUND_ENTRY, round);
|
|
15
|
+
pi.events.emit(ROUND_RECORDED_CHANNEL, undefined);
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
package/session/herdr-display.ts
CHANGED