pi-firecode 0.8.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +58 -24
  2. package/README.zh-CN.md +74 -0
  3. package/config.example.jsonc +47 -57
  4. package/dist/config.example.jsonc +90 -0
  5. package/dist/index.js +11464 -0
  6. package/dist/master/prompts/master.en.md +22 -0
  7. package/dist/master/prompts/master.zh.md +22 -0
  8. package/dist/master/prompts/worker.en.md +1 -0
  9. package/dist/master/prompts/worker.zh.md +1 -0
  10. package/{review → dist/review}/prompts/review.en.md +7 -6
  11. package/{review → dist/review}/prompts/review.zh.md +7 -6
  12. package/dist/watcher/prompts/watch.en.md +43 -0
  13. package/{watcher → dist/watcher}/prompts/watch.zh.md +1 -1
  14. package/package.json +10 -27
  15. package/config.ts +0 -474
  16. package/deliver.ts +0 -32
  17. package/flame-frames.ts +0 -460
  18. package/format.ts +0 -80
  19. package/header.ts +0 -92
  20. package/herdr-client.ts +0 -60
  21. package/index.ts +0 -62
  22. package/jsonc.ts +0 -34
  23. package/master/event-card.ts +0 -106
  24. package/master/event-format.ts +0 -52
  25. package/master/index.ts +0 -1200
  26. package/master/prompt.ts +0 -26
  27. package/master/prompts/master.zh.md +0 -17
  28. package/master/prompts/worker.zh.md +0 -1
  29. package/master/role.ts +0 -13
  30. package/master/spawn.ts +0 -193
  31. package/master/state.ts +0 -201
  32. package/provider/claude-sub.ts +0 -129
  33. package/provider/openai-native/index.ts +0 -8
  34. package/provider/openai-native/src/compact-client.ts +0 -362
  35. package/provider/openai-native/src/config.ts +0 -297
  36. package/provider/openai-native/src/extension.ts +0 -93
  37. package/provider/openai-native/src/native-compaction.ts +0 -151
  38. package/provider/openai-native/src/native-details.ts +0 -157
  39. package/provider/openai-native/src/native-replay.ts +0 -253
  40. package/provider/openai-native/src/native-runtime.ts +0 -144
  41. package/provider/openai-native/src/options.ts +0 -85
  42. package/provider/openai-native/src/request-pipeline.ts +0 -21
  43. package/provider/openai-native/src/responses-input.ts +0 -433
  44. package/review/advisor.ts +0 -112
  45. package/review/card.ts +0 -385
  46. package/review/checkpoint.ts +0 -384
  47. package/review/evidence.ts +0 -232
  48. package/review/index.ts +0 -1246
  49. package/review/outcome.ts +0 -74
  50. package/review/progress.ts +0 -324
  51. package/review/prompt.ts +0 -210
  52. package/review/reviewer.ts +0 -356
  53. package/review/session.ts +0 -129
  54. package/review/state.ts +0 -905
  55. package/review/ui.ts +0 -528
  56. package/session/bark.ts +0 -157
  57. package/session/herdr-display.ts +0 -87
  58. package/session/presets.ts +0 -257
  59. package/session/rename.ts +0 -46
  60. package/session/stats.ts +0 -310
  61. package/session/working-flame.ts +0 -116
  62. package/statusbar/index.ts +0 -118
  63. package/statusbar/quota-cache.ts +0 -60
  64. package/statusbar/quota-parse.ts +0 -85
  65. package/statusbar/quota.ts +0 -203
  66. package/statusbar/render.ts +0 -181
  67. package/statusbar/tps.ts +0 -87
  68. package/theme.ts +0 -84
  69. package/tools/grouping.ts +0 -233
  70. package/tools/index.ts +0 -183
  71. package/tools/line.ts +0 -194
  72. package/tools/parts.ts +0 -143
  73. package/tools/timing.ts +0 -22
  74. package/watcher/card.ts +0 -85
  75. package/watcher/index.ts +0 -204
  76. package/watcher/observer.ts +0 -75
  77. package/watcher/transcript.ts +0 -64
  78. /package/{review → dist/review}/prompts/advisor.en.md +0 -0
  79. /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
package/review/ui.ts DELETED
@@ -1,528 +0,0 @@
1
- /**
2
- * /fire-review 的活动 UI:编辑器上方的固定活动条与 esc 取消接管。
3
- *
4
- * 只读 executor 传入的快照函数,自身不持状态;动画由组件内部计时器驱动,
5
- * dispose 时清理。审查看不见就等于坏了,这一层是可用性的主体。
6
- */
7
- import { basename } from "node:path";
8
- import {
9
- CustomEditor,
10
- DynamicBorder,
11
- type ExtensionContext,
12
- type KeybindingsManager,
13
- type Theme,
14
- } 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";
23
- 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";
31
-
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
- let reviewTitleActive = false;
45
-
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
- /**
86
- * 审查等模型结论时(排队/审查中/顾问仲裁)接管编辑器:禁止输入,esc/Ctrl+C 随时取消。
87
- *
88
- * 不能用全局输入钩子比对裸 \x1b:终端开启增强键盘协议后 esc 是带修饰的序列,
89
- * 字面量比较会漏。这里统一走 keybindings 匹配。
90
- * awaiting_fix 相不接管——那时是执行模型在改代码,用户应能正常输入与中断。
91
- */
92
- export function lockEditor(
93
- ctx: ExtensionContext,
94
- _view: ViewSource,
95
- cancel: () => void,
96
- ): void {
97
- if (ctx.hasUI === false || typeof ctx.ui.setEditorComponent !== "function") return;
98
- ctx.ui.setEditorComponent((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
99
- const editor = new ReviewEditor(tui, theme, keybindings, cancel);
100
- return editor;
101
- });
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);
108
- }
109
-
110
- /**
111
- * 审查期间的只读编辑器。
112
- *
113
- * 动效由上方活动条负责,这里静态提示即可。
114
- */
115
- class ReviewEditor extends CustomEditor {
116
- constructor(
117
- tui: TUI,
118
- theme: EditorTheme,
119
- private readonly keys: KeybindingsManager,
120
- private readonly cancel: () => void,
121
- ) {
122
- super(tui, theme, keys);
123
- }
124
-
125
- 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
- }
133
- // 审查期间不接受任何其他输入:插话会污染本轮审查的会话证据。
134
- }
135
-
136
- override render(_width: number): string[] {
137
- // pi-flow 交互:审查期间输入区完全隐藏,不额外插入一行提示。
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} 轮未过`;
336
- }
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
- }
499
-
500
- function setReviewTitle(ctx: ExtensionContext, view: ActivityView | undefined) {
501
- if (!view || !ctx.hasUI || typeof ctx.ui.setTitle !== "function") return;
502
- const manager = ctx.sessionManager as {
503
- getSessionName?: () => unknown;
504
- getCwd?: () => unknown;
505
- };
506
- const rawName = manager.getSessionName?.();
507
- const rawCwd = manager.getCwd?.();
508
- const who = typeof rawName === "string" && rawName
509
- ? rawName
510
- : typeof rawCwd === "string" ? basename(rawCwd) : "";
511
- const label = view.language === "en" ? "Reviewing" : "审查中";
512
- reviewTitleActive = true;
513
- ctx.ui.setTitle(`${label}${view.round > 0 ? ` R${view.round}` : ""}${who ? ` · ${who}` : ""}`);
514
- }
515
-
516
- function restoreReviewTitle(ctx: ExtensionContext) {
517
- if (!reviewTitleActive || !ctx.hasUI || typeof ctx.ui.setTitle !== "function") return;
518
- reviewTitleActive = false;
519
- const manager = ctx.sessionManager as {
520
- getSessionName?: () => unknown;
521
- getCwd?: () => unknown;
522
- };
523
- const rawName = manager.getSessionName?.();
524
- const rawCwd = manager.getCwd?.();
525
- const name = typeof rawName === "string" && rawName ? rawName : undefined;
526
- const dir = typeof rawCwd === "string" ? basename(rawCwd) : "";
527
- ctx.ui.setTitle(name ? `π - ${name} - ${dir}` : `π - ${dir}`);
528
- }
package/session/bark.ts DELETED
@@ -1,157 +0,0 @@
1
- /**
2
- * Bark 通知:每轮任务彻底落定(agent_settled 且不再自动续跑)时,
3
- * 把最后一条回复推送到 iPhone 的 Bark App。
4
- *
5
- * - 子代理会话不发通知,通知统一由指挥官会话发出。
6
- * - 子代理池里有待发落消息时升 timeSensitive 并带副标题,可穿透专注模式;平时为默认 active。
7
- * - 同会话固定 id:新通知经 APNs CollapseID 顶掉旧通知,通知栏每会话只留最新一条。
8
- * - 推送地址在 ~/.pi/agent/bark-key(整行即 https://api.day.app/<key>/),
9
- * 缺失时静默停用;~/.pi/agent/bark-crypto.json 存在时走 AES-256-GCM 端到端加密。
10
- */
11
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
- import crypto from "node:crypto";
13
- import fs from "node:fs";
14
- import os from "node:os";
15
- import path from "node:path";
16
- import { loadMasterState, masterStatePath } from "../master/state.js";
17
-
18
- const KEY_FILE = path.join(os.homedir(), ".pi", "agent", "bark-key");
19
- const CRYPTO_FILE = path.join(os.homedir(), ".pi", "agent", "bark-crypto.json");
20
- const MAX_BODY_LENGTH = 200;
21
- // pi.dev 的 logo 是白色透明背景 SVG,通知图标不支持矢量图;经 wsrv.nl 转黑底 PNG。
22
- const ICON_URL = "https://wsrv.nl/?url=pi.dev/logo.svg&w=256&h=256&output=png&bg=black";
23
-
24
- export interface BarkPayload {
25
- title: string;
26
- subtitle?: string;
27
- body: string;
28
- group: string;
29
- id: string;
30
- level: "active" | "timeSensitive";
31
- icon: string;
32
- }
33
-
34
- export function buildBarkPayload(input: {
35
- title: string;
36
- body: string;
37
- group: string;
38
- sessionId: string;
39
- awaitingDecision: boolean;
40
- }): BarkPayload {
41
- return {
42
- title: input.title,
43
- ...(input.awaitingDecision ? { subtitle: "待拍板" } : {}),
44
- body: input.body || "任务已完成",
45
- group: input.group,
46
- id: input.sessionId,
47
- level: input.awaitingDecision ? "timeSensitive" : "active",
48
- icon: ICON_URL,
49
- };
50
- }
51
-
52
- /** 只读旁路判定:状态文件损坏由 Master 自己报告与恢复,通知不放大故障,一律按无待拍板降级。 */
53
- export function hasPendingDisposition(statePath: string): boolean {
54
- try {
55
- return loadMasterState(statePath)?.workers.some((worker) => worker.disposition !== undefined) ?? false;
56
- } catch {
57
- return false;
58
- }
59
- }
60
-
61
- export function registerBark(pi: ExtensionAPI, subsession = false): void {
62
- // 通知只归指挥官会话。
63
- if (subsession) return;
64
-
65
- let lastAssistantText = "";
66
-
67
- pi.on("message_end", (event) => {
68
- if (event.message.role !== "assistant") return;
69
- const text = extractText(event.message);
70
- if (text) lastAssistantText = text;
71
- });
72
-
73
- pi.on("agent_settled", (_event, ctx) => {
74
- if (ctx.isIdle?.() !== true) return;
75
- const sessionId = ctx.sessionManager.getSessionId();
76
- const dirName = ctx.cwd ? path.basename(ctx.cwd) : "pi";
77
- void sendBark(
78
- buildBarkPayload({
79
- // 优先会话名;家目录直接跑 pi 时目录名恰好是用户名,不适合当标题。
80
- title: pi.getSessionName() || dirName,
81
- body: cleanMarkdown(lastAssistantText).slice(0, MAX_BODY_LENGTH),
82
- group: dirName,
83
- sessionId,
84
- awaitingDecision: hasPendingDisposition(masterStatePath(sessionId)),
85
- }),
86
- );
87
- });
88
- }
89
-
90
- async function sendBark(payload: BarkPayload): Promise<void> {
91
- const barkUrl = readBarkUrl();
92
- if (!barkUrl) return;
93
- const cryptoConfig = readCryptoConfig();
94
- // 加密时 id 必须提到顶层明文:折叠(CollapseID)由服务端写 APNs 头,密文内的 id 它读不到;
95
- // id 只是本地会话 uuid,无内容敏感性。level/subtitle 等内容字段由设备端解密后应用,留在密文内。
96
- const body = cryptoConfig
97
- ? { ciphertext: encryptPayload(payload, cryptoConfig.key, cryptoConfig.iv), iv: cryptoConfig.iv, id: payload.id }
98
- : payload;
99
- try {
100
- await fetch(barkUrl.endsWith("/") ? barkUrl : `${barkUrl}/`, {
101
- method: "POST",
102
- headers: { "Content-Type": "application/json; charset=utf-8" },
103
- body: JSON.stringify(body),
104
- signal: AbortSignal.timeout(5000),
105
- });
106
- } catch {
107
- // 推送是旁路能力,失败不打扰正常会话。
108
- }
109
- }
110
-
111
- function readBarkUrl(): string | undefined {
112
- try {
113
- const url = fs.readFileSync(KEY_FILE, "utf8").trim();
114
- return url.length > 0 ? url : undefined;
115
- } catch {
116
- return undefined;
117
- }
118
- }
119
-
120
- function readCryptoConfig(): { key: string; iv: string } | undefined {
121
- try {
122
- const parsed = JSON.parse(fs.readFileSync(CRYPTO_FILE, "utf8"));
123
- if (typeof parsed.key === "string" && typeof parsed.iv === "string") return parsed;
124
- } catch {
125
- // 未配置加密时走明文。
126
- }
127
- return undefined;
128
- }
129
-
130
- /** AES-256-GCM,密文+authTag 拼接后 base64,与 Bark 官方客户端解密逻辑一致。 */
131
- function encryptPayload(payload: unknown, key: string, iv: string): string {
132
- const cipher = crypto.createCipheriv("aes-256-gcm", Buffer.from(key, "utf8"), Buffer.from(iv, "utf8"));
133
- const encrypted = Buffer.concat([cipher.update(JSON.stringify(payload), "utf8"), cipher.final()]);
134
- return Buffer.concat([encrypted, cipher.getAuthTag()]).toString("base64");
135
- }
136
-
137
- function extractText(message: { content?: unknown }): string {
138
- const blocks = Array.isArray(message?.content) ? message.content : [];
139
- return blocks
140
- .filter((block: any) => block?.type === "text" && typeof block.text === "string")
141
- .map((block: any) => block.text as string)
142
- .join("\n")
143
- .trim();
144
- }
145
-
146
- /** 去掉 markdown 标记,通知栏显示纯文字。 */
147
- function cleanMarkdown(text: string): string {
148
- return text
149
- .replace(/```\w*\n?([\s\S]*?)```/g, "$1")
150
- .replace(/^#{1,6}\s+/gm, "")
151
- .replace(/^[-*+>]\s+/gm, "")
152
- .replace(/\*\*(.+?)\*\*/g, "$1")
153
- .replace(/__(.+?)__/g, "$1")
154
- .replace(/`([^`]+)`/g, "$1")
155
- .replace(/\n{2,}/g, "\n")
156
- .trim();
157
- }