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.
Files changed (79) hide show
  1. package/README.md +35 -15
  2. package/activity.ts +92 -0
  3. package/busy.ts +188 -0
  4. package/config.example.jsonc +38 -47
  5. package/config.ts +198 -172
  6. package/deliver.ts +100 -13
  7. package/flame.ts +149 -0
  8. package/format.ts +84 -31
  9. package/header.ts +208 -82
  10. package/index.ts +17 -11
  11. package/master/actions.ts +293 -0
  12. package/master/activity-list.ts +281 -0
  13. package/master/event-card.ts +18 -37
  14. package/master/event-format.ts +71 -40
  15. package/master/guard.ts +46 -0
  16. package/master/index.ts +88 -1036
  17. package/master/list-view.ts +129 -0
  18. package/master/outbox.ts +188 -0
  19. package/master/prompts/master.zh.md +17 -12
  20. package/master/prompts/worker.zh.md +1 -1
  21. package/master/role.ts +6 -1
  22. package/master/run.ts +315 -0
  23. package/master/runtime.ts +259 -0
  24. package/master/spawn.ts +79 -38
  25. package/master/state.ts +30 -6
  26. package/master/worker-view.ts +517 -0
  27. package/package.json +1 -1
  28. package/provider/claude-sub.ts +35 -13
  29. package/provider/openai-native/src/compact-client.ts +2 -14
  30. package/provider/openai-native/src/options.ts +4 -2
  31. package/review/advisor.ts +0 -2
  32. package/review/card.ts +36 -23
  33. package/review/checkpoint.ts +2 -8
  34. package/review/evidence.ts +42 -44
  35. package/review/index.ts +271 -525
  36. package/review/occupancy.ts +25 -0
  37. package/review/outcome.ts +31 -7
  38. package/review/prompt.ts +7 -8
  39. package/review/prompts/review.en.md +7 -6
  40. package/review/prompts/review.zh.md +7 -6
  41. package/review/reviewer.ts +2 -5
  42. package/review/session.ts +11 -30
  43. package/review/state.ts +32 -60
  44. package/review/ui.ts +30 -469
  45. package/round-recorder.ts +18 -0
  46. package/session/herdr-display.ts +1 -1
  47. package/session/presets.ts +130 -87
  48. package/session/quota.ts +123 -0
  49. package/session/stats.ts +4 -1
  50. package/statusbar/index.ts +242 -104
  51. package/statusbar/render.ts +95 -163
  52. package/theme.ts +2 -41
  53. package/tools/actions.ts +53 -0
  54. package/tools/assistant-view.ts +64 -0
  55. package/tools/click-anchor.ts +56 -0
  56. package/tools/group-view.ts +420 -0
  57. package/tools/grouping.ts +146 -221
  58. package/tools/host.ts +369 -0
  59. package/tools/index.ts +108 -105
  60. package/tools/line.ts +95 -58
  61. package/tools/machine.ts +78 -0
  62. package/tools/parts.ts +2 -2
  63. package/tools/round.ts +95 -0
  64. package/tools/turn-clock.ts +41 -0
  65. package/tools/turn-summary.ts +109 -0
  66. package/truncated-write.ts +18 -0
  67. package/watcher/card.ts +28 -22
  68. package/watcher/index.ts +16 -44
  69. package/watcher/observer.ts +3 -3
  70. package/watcher/prompts/watch.zh.md +1 -1
  71. package/watcher/transcript.ts +4 -12
  72. package/flame-frames.ts +0 -460
  73. package/review/progress.ts +0 -324
  74. package/session/bark.ts +0 -157
  75. package/session/working-flame.ts +0 -116
  76. package/statusbar/quota-cache.ts +0 -60
  77. package/statusbar/quota-parse.ts +0 -85
  78. package/statusbar/quota.ts +0 -203
  79. package/statusbar/tps.ts +0 -87
package/review/card.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  import { getMarkdownTheme, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
12
12
  import { Box, type Component, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
13
13
  import type { Language } from "../config.js";
14
+ import { HEAT_COLORS, paint } from "../flame.js";
14
15
  import { formatDuration } from "../format.js";
15
16
  import type { CardData, StopReason } from "./state.js";
16
17
 
@@ -30,6 +31,28 @@ const CARD_KINDS = new Set([
30
31
  ]);
31
32
  const CARD_TONES = new Set(["success", "warning", "error", "neutral", "accent"]);
32
33
 
34
+ /**
35
+ * 卡标题前的单色字形与它的语义色,按卡种类一处定义:构建时写进 details.icon,渲染时按种类上色。
36
+ * 字形与全局体系一致(✓ ✗ ◌ ‖ 与审查的盲文点阵),颜色只表达结论:通过绿、失败与未完成红、取消灰,
37
+ * 审查过程中的中性卡(开始、顾问指引)用审查的金色——品牌火焰不用于中性状态。
38
+ */
39
+ const MARKS = {
40
+ start: { glyph: "⠿", color: "review" },
41
+ advisor: { glyph: "⠿", color: "review" },
42
+ pass: { glyph: "✓", color: "success" },
43
+ fail: { glyph: "✗", color: "error" },
44
+ stop: { glyph: "✗", color: "error" },
45
+ timeout: { glyph: "◌", color: "error" },
46
+ error: { glyph: "◌", color: "error" },
47
+ cancel: { glyph: "‖", color: "muted" },
48
+ } as const satisfies Record<CardData["kind"], { glyph: string; color: "review" | "success" | "error" | "muted" }>;
49
+
50
+ function paintMark(details: CardDetails, theme: Theme): string {
51
+ const mark = MARKS[details.kind as CardData["kind"]];
52
+ if (!mark) return details.icon;
53
+ return mark.color === "review" ? paint(HEAT_COLORS.gold, details.icon) : theme.fg(mark.color, details.icon);
54
+ }
55
+
33
56
  export type CardDetails = {
34
57
  version: typeof VERSION;
35
58
  kind: CardData["kind"];
@@ -101,7 +124,7 @@ class ReviewCard implements Component {
101
124
  function nativeCard(details: CardDetails, theme: Theme): Component {
102
125
  // 全家卡统一:无垂直内边距;消息间距由宿主 CustomMessageComponent 提供,不再叠加。
103
126
  const box = new Box(1, 0, (text) => theme.bg(backgroundFor(details.tone), text));
104
- box.addChild(new Text(`${details.icon} ${details.title}`, 0, 0));
127
+ box.addChild(new Text(`${paintMark(details, theme)} ${details.title}`, 0, 0));
105
128
  box.addChild(new Spacer(1));
106
129
  box.addChild(new Markdown(details.lines.join("\n"), 0, 0, getMarkdownTheme()));
107
130
  return box;
@@ -157,7 +180,7 @@ function started(card: Extract<CardData, { kind: "start" }>, language: Language)
157
180
  ? `Models: ${card.models.map(shortModel).join(", ")}`
158
181
  : `模型:${card.models.map(shortModel).join("、")}`,
159
182
  ];
160
- return spec(language, "start", title, lines, "neutral", "🔥");
183
+ return spec("start", title, lines, "neutral");
161
184
  }
162
185
 
163
186
  function shortModel(model: string) {
@@ -169,7 +192,7 @@ function passed(card: Extract<CardData, { kind: "pass" }>, language: Language):
169
192
  const lines = withFooter(formatReviewResultLines(card.summary), [
170
193
  elapsedLine(card.elapsedMs, card.totalElapsedMs, card.round > 1, language),
171
194
  ]);
172
- return spec(language, "pass", title, lines, "success", "✅");
195
+ return spec("pass", title, lines, "success");
173
196
  }
174
197
 
175
198
  function failed(card: Extract<CardData, { kind: "fail" }>, language: Language): BuiltCard {
@@ -183,12 +206,10 @@ function failed(card: Extract<CardData, { kind: "fail" }>, language: Language):
183
206
  : [elapsedLine(card.elapsedMs, card.totalElapsedMs, false, language)]),
184
207
  ];
185
208
  return spec(
186
- language,
187
209
  "fail",
188
210
  title,
189
211
  withFooter(formatReviewResultLines(card.details), footer),
190
212
  "warning",
191
- "❌",
192
213
  );
193
214
  }
194
215
 
@@ -203,16 +224,16 @@ function stopped(card: Extract<CardData, { kind: "stop" }>, language: Language):
203
224
  language,
204
225
  );
205
226
  const body = [advisorModelLine(card.advisorModel, language), "", ...adviceLines(card.advisor.advice)];
206
- return spec(language, "stop", title, withFooter(body, footer), "warning", "❌");
227
+ return spec("stop", title, withFooter(body, footer), "warning");
207
228
  }
208
229
  const title = qualityTitle(card.round, language === "en" ? "Review failed" : "审查未通过", language);
209
230
  const body = formatReviewResultLines(card.details || stopReason(card.reason, language));
210
- return spec(language, "stop", title, withFooter(body, footer), "warning", "❌");
231
+ return spec("stop", title, withFooter(body, footer), "warning");
211
232
  }
212
233
 
213
234
  function cancelled(card: Extract<CardData, { kind: "cancel" }>, language: Language): BuiltCard {
214
235
  const title = language === "en" ? "Review cancelled" : "审查已取消";
215
- return spec(language, "cancel", title, [reasonText(card.reason, language)], "neutral", "⏸");
236
+ return spec("cancel", title, [reasonText(card.reason, language)], "neutral");
216
237
  }
217
238
 
218
239
  function timedOut(_card: Extract<CardData, { kind: "timeout" }>, language: Language): BuiltCard {
@@ -221,7 +242,7 @@ function timedOut(_card: Extract<CardData, { kind: "timeout" }>, language: Langu
221
242
  language === "en" ? "Blocker: review timed out" : "卡点:审查超时",
222
243
  language === "en" ? "Reason: overall time limit exceeded" : "原因:超过总体时限",
223
244
  ];
224
- return spec(language, "timeout", title, lines, "warning", "🛑");
245
+ return spec("timeout", title, lines, "warning");
225
246
  }
226
247
 
227
248
  /** 终止原因的展示文案(reducer 只出枚举,这里本地化)。 */
@@ -250,7 +271,7 @@ function errored(card: Extract<CardData, { kind: "error" }>, language: Language)
250
271
  ? []
251
272
  : ["", elapsedLine(card.elapsedMs, card.totalElapsedMs, false, language)]),
252
273
  ];
253
- return spec(language, "error", title, lines, "warning", "🛑");
274
+ return spec("error", title, lines, "warning");
254
275
  }
255
276
 
256
277
  /** 顾问卡与审查结果卡同构:裁决进标题,正文用粗体模型分节行开头。 */
@@ -259,10 +280,10 @@ function advisorCard(card: Extract<CardData, { kind: "advisor" }>, language: Lan
259
280
  const title = language === "en" ? `Advisor guidance · ${decision}` : `顾问指引 · ${decision}`;
260
281
  const body = [advisorModelLine(card.advisorModel, language), "", ...adviceLines(card.advisor.advice)];
261
282
  const footer = card.elapsedMs === undefined ? [] : [elapsedLine(card.elapsedMs, undefined, false, language)];
262
- return spec(language, "advisor", title, withFooter(body, footer), "neutral", "🧭");
283
+ return spec("advisor", title, withFooter(body, footer), "neutral");
263
284
  }
264
285
 
265
- /** 裁决词→人话文案的唯一映射:卡标题与活动条摘要共用,防两处文案漂移。 */
286
+ /** 裁决词→人话文案的唯一映射:卡标题与审查活动行共用,防两处文案漂移。 */
266
287
  export function decisionText(verdict: "continue" | "narrow" | "stop", language: Language) {
267
288
  return language === "en"
268
289
  ? { continue: "Continue fixing", narrow: "Narrow scope", stop: "Stop fixing" }[verdict]
@@ -304,7 +325,7 @@ function elapsedLine(
304
325
  const elapsed = showTotal && totalMs !== undefined
305
326
  ? `${formatDuration(ms)} / ${language === "en" ? "total" : "总"} ${formatDuration(totalMs)}`
306
327
  : formatDuration(ms);
307
- return language === "en" ? `⏱ Elapsed: ${elapsed}` : `⏱ 用时:${elapsed}`;
328
+ return language === "en" ? `Elapsed: ${elapsed}` : `用时:${elapsed}`;
308
329
  }
309
330
 
310
331
  function formatReviewResultLines(review: string) {
@@ -365,21 +386,13 @@ const REDUNDANT_REVIEW_LINES = new Set([
365
386
  ]);
366
387
 
367
388
  function spec(
368
- language: Language,
369
- kind: CardDetails["kind"],
389
+ kind: CardData["kind"],
370
390
  title: string,
371
391
  lines: string[],
372
392
  tone: CardDetails["tone"],
373
- icon: string,
374
393
  ): BuiltCard {
375
- const localized = localize(lines, language);
376
394
  return {
377
395
  content: `${title}\n${lines.join("\n")}`,
378
- details: { version: VERSION, kind, title, lines: localized, tone, icon },
396
+ details: { version: VERSION, kind, title, lines, tone, icon: MARKS[kind].glyph },
379
397
  };
380
398
  }
381
-
382
- /** details 行已本地化成品;content 里除标题外都是事实,不做二次翻译。 */
383
- function localize(lines: string[], language: Language) {
384
- return lines;
385
- }
@@ -40,10 +40,7 @@ export class CheckpointConflictError extends Error {
40
40
 
41
41
  /** 只读会话访问(checkpoint 读/写前的 CAS 读取)。 */
42
42
  export interface CheckpointReadContext {
43
- sessionManager?: {
44
- getBranch?: () => unknown[];
45
- getEntries?: () => unknown[];
46
- };
43
+ sessionManager: { getBranch(): unknown[] };
47
44
  }
48
45
 
49
46
  const PHASES = new Set([
@@ -319,10 +316,7 @@ export function readCheckpoint(ctx: CheckpointReadContext): ReviewState | undefi
319
316
  }
320
317
 
321
318
  function latestEntry(ctx: CheckpointReadContext): Record<string, unknown> | undefined {
322
- const entries =
323
- ctx.sessionManager?.getBranch?.() ??
324
- ctx.sessionManager?.getEntries?.() ??
325
- [];
319
+ const entries = ctx.sessionManager.getBranch();
326
320
  for (let index = entries.length - 1; index >= 0; index -= 1) {
327
321
  const entry = entries[index];
328
322
  if (!isRecord(entry) || entry.type !== "custom") continue;
@@ -10,6 +10,7 @@
10
10
  * toolResult 正文仍跳过(输出体积大且非一手证据——审查者应自行重跑验证命令)。
11
11
  */
12
12
  import type { Language } from "../config.js";
13
+ import { textOf } from "../format.js";
13
14
 
14
15
  export const DEFAULT_EVIDENCE_TOKENS = 24_000;
15
16
  /** 单条消息渲染上限,防单条超长消息撑爆预算。 */
@@ -21,13 +22,20 @@ export interface Evidence {
21
22
  omitted: number;
22
23
  }
23
24
 
25
+ interface Render {
26
+ language: Language;
27
+ /** 会话文件:截断标记据此告诉审查者完整原文在哪里。内存会话没有。 */
28
+ sessionFile?: string;
29
+ failedCalls: ReadonlySet<string>;
30
+ }
31
+
24
32
  export function buildEvidence(
25
33
  entries: readonly unknown[],
26
34
  language: Language,
27
- budgetTokens = DEFAULT_EVIDENCE_TOKENS,
35
+ { budgetTokens = DEFAULT_EVIDENCE_TOKENS, sessionFile }: { budgetTokens?: number; sessionFile?: string } = {},
28
36
  ): Evidence {
29
- const failedCalls = collectFailedCalls(entries);
30
- const blocks = entries.flatMap((entry) => renderEntry(entry, language, failedCalls));
37
+ const render: Render = { language, ...(sessionFile ? { sessionFile } : {}), failedCalls: collectFailedCalls(entries) };
38
+ const blocks = entries.flatMap((entry) => renderEntry(entry, render));
31
39
  if (blocks.length === 0) return { text: "", omitted: 0 };
32
40
  // 锚点必须是首条用户消息(原始需求):它之前可能排着其他扩展的可显示消息,
33
41
  // 盲取第一块会把真正的需求锚点让进预算竞争、在长会话里被裁掉。
@@ -78,11 +86,8 @@ function collectFailedCalls(entries: readonly unknown[]): ReadonlySet<string> {
78
86
  return failed;
79
87
  }
80
88
 
81
- function renderEntry(
82
- entry: unknown,
83
- language: Language,
84
- failedCalls: ReadonlySet<string>,
85
- ): EvidenceBlock[] {
89
+ function renderEntry(entry: unknown, render: Render): EvidenceBlock[] {
90
+ const { language } = render;
86
91
  if (!isRecord(entry)) return [];
87
92
  switch (entry.type) {
88
93
  case "message": {
@@ -91,25 +96,25 @@ function renderEntry(
91
96
  if (message.role === "user")
92
97
  return [
93
98
  {
94
- text: `## ${userLabel(language)}\n${clip(messageText(message.content))}`,
99
+ text: `## ${userLabel(language)}\n${clip(textOf(message.content), render)}`,
95
100
  role: "user" as const,
96
101
  },
97
102
  ];
98
103
  if (message.role === "assistant")
99
- return [{ text: `## ${assistantLabel(language)}\n${assistantBody(message.content, language, failedCalls)}` }];
104
+ return [{ text: `## ${assistantLabel(language)}\n${assistantBody(message.content, render)}` }];
100
105
  return [];
101
106
  }
102
107
  case "custom_message": {
103
108
  if (entry.display !== true) return [];
104
- return [{ text: `## ${customLabel(language, String(entry.customType ?? ""))}\n${clip(messageText(entry.content))}` }];
109
+ return [{ text: `## ${customLabel(language, String(entry.customType ?? ""))}\n${clip(textOf(entry.content), render)}` }];
105
110
  }
106
111
  case "compaction":
107
112
  return typeof entry.summary === "string" && entry.summary
108
- ? [{ text: `## ${summaryLabel(language)}\n${clip(entry.summary)}` }]
113
+ ? [{ text: `## ${summaryLabel(language)}\n${clip(entry.summary, render)}` }]
109
114
  : [];
110
115
  case "branch_summary":
111
116
  return typeof entry.summary === "string" && entry.summary
112
- ? [{ text: `## ${branchSummaryLabel(language)}\n${clip(entry.summary)}` }]
117
+ ? [{ text: `## ${branchSummaryLabel(language)}\n${clip(entry.summary, render)}` }]
113
118
  : [];
114
119
  default:
115
120
  return [];
@@ -137,26 +142,19 @@ function branchSummaryLabel(language: Language) {
137
142
  }
138
143
 
139
144
  /** assistant 正文 = 文本段 + 工具调用轨迹;纯工具回合也因此留下编辑记录。 */
140
- function assistantBody(
141
- content: unknown,
142
- language: Language,
143
- failedCalls: ReadonlySet<string>,
144
- ): string {
145
- if (typeof content === "string") return clip(content);
145
+ function assistantBody(content: unknown, render: Render): string {
146
+ if (typeof content === "string") return clip(content, render);
146
147
  if (!Array.isArray(content)) return "";
147
148
  const trail = content
148
- .map((part) => toolCallLine(asRecord(part), language, failedCalls))
149
+ .map((part) => toolCallLine(asRecord(part), render))
149
150
  .filter(Boolean)
150
151
  .join("\n");
151
- const body = clip(messageText(content));
152
+ const body = clip(textOf(content), render);
152
153
  return [body, trail].filter(Boolean).join("\n");
153
154
  }
154
155
 
155
- function toolCallLine(
156
- part: Record<string, unknown> | undefined,
157
- language: Language,
158
- failedCalls: ReadonlySet<string>,
159
- ): string {
156
+ function toolCallLine(part: Record<string, unknown> | undefined, render: Render): string {
157
+ const { language, failedCalls } = render;
160
158
  if (part?.type !== "toolCall" || typeof part.name !== "string" || !part.name) return "";
161
159
  const args = asRecord(part.arguments);
162
160
  const target =
@@ -171,33 +169,33 @@ function toolCallLine(
171
169
  ? " (failed)"
172
170
  : "(失败)"
173
171
  : "";
174
- return `${`[${part.name}] ${clipLine(target)}`.trimEnd()}${failed}`;
172
+ return `${`[${part.name}] ${clipLine(target, render)}`.trimEnd()}${failed}`;
175
173
  }
176
174
 
177
175
  /** 单行轨迹上限:防超长 bash 命令撑大证据块;路径不受影响。 */
178
176
  const TOOL_LINE_MAX_CHARS = 200;
179
177
 
180
- function clipLine(text: string) {
178
+ function clipLine(text: string, { language, sessionFile }: Render) {
181
179
  const single = text.replace(/\s+/gu, " ").trim();
182
- return single.length <= TOOL_LINE_MAX_CHARS ? single : `${single.slice(0, TOOL_LINE_MAX_CHARS)}…`;
183
- }
184
-
185
- function messageText(content: unknown): string {
186
- if (typeof content === "string") return content;
187
- if (!Array.isArray(content)) return "";
188
- return content
189
- .map((part) => {
190
- const record = asRecord(part);
191
- return record?.type === "text" && typeof record.text === "string"
192
- ? record.text
193
- : "";
194
- })
195
- .join("\n");
180
+ if (single.length <= TOOL_LINE_MAX_CHARS) return single;
181
+ const where = sessionFile ?? (language === "en" ? "the session file" : "会话文件");
182
+ const note = language === "en"
183
+ ? `…[truncated, ${single.length} chars; full original in ${where}]`
184
+ : `…[截断,原文 ${single.length} 字,完整原文在 ${where}]`;
185
+ return `${single.slice(0, TOOL_LINE_MAX_CHARS)}${note}`;
196
186
  }
197
187
 
198
- function clip(text: string) {
188
+ /**
189
+ * 截断处必须写明是证据组装的截断:裸省略号会被审查者当成回复本身没写完(长交付物曾因此连判 FAIL)。
190
+ * 完整原文在会话文件里,审查者能用 read 自行核对。
191
+ */
192
+ function clip(text: string, { language, sessionFile }: Render) {
199
193
  if (text.length <= MESSAGE_MAX_CHARS) return text.trim();
200
- return `${text.slice(0, MESSAGE_MAX_CHARS).trim()}\n[…]`;
194
+ const where = sessionFile ?? (language === "en" ? "the session file" : "会话文件");
195
+ const note = language === "en"
196
+ ? `[evidence truncated: this message has ${text.length} characters, only the first ${MESSAGE_MAX_CHARS} are shown; the full original is in ${where} — read it if you need to verify]`
197
+ : `[证据截断:本条消息原文 ${text.length} 字,此处只给出前 ${MESSAGE_MAX_CHARS} 字;完整原文在 ${where},需要核对时用 read 查看]`;
198
+ return `${text.slice(0, MESSAGE_MAX_CHARS).trim()}\n${note}`;
201
199
  }
202
200
 
203
201
  /**