pi-firecode 0.8.0 → 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 +18 -13
  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
@@ -1,24 +1,19 @@
1
1
  /**
2
2
  * Master 事件卡:默认紧凑(每事件一行标题行),ctrl+o 展开完整内容。
3
- * content 给模型(完整事实),details 给渲染——两者刻意不对齐(先例:review/card.ts)。
4
- * 提取与校验的格式契约在 event-format.ts;旧会话无 details 的消息与校验失败一律降级
5
- * 完整内容;渲染器永不抛异常。
3
+ * 数据只来自消息正文里的信封(deliver.ts 是格式的唯一事实源);渲染器永不抛异常。
6
4
  */
7
5
  import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
8
6
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
9
7
  import { Box, type Component, Markdown, Text } from "@earendil-works/pi-tui";
10
- import { clip, oneLine } from "../format.js";
11
- import {
12
- MASTER_EVENT_TYPE,
13
- isValidMasterEventDetails,
14
- type MasterEventDetails,
15
- } from "./event-format.js";
8
+ import { clip, firstSentence, oneLine, textOf } from "../format.js";
9
+ import { parseEnvelopes } from "../deliver.js";
10
+ import { machineEntries } from "../tools/machine.js";
11
+ import { MASTER_EVENT_TYPE } from "./event-format.js";
16
12
 
17
13
  export function registerMasterEventRenderer(pi: ExtensionAPI): void {
18
- pi.registerMessageRenderer<MasterEventDetails>(
14
+ pi.registerMessageRenderer(
19
15
  MASTER_EVENT_TYPE,
20
- (message, options, theme) =>
21
- new MasterEventCard(message.details, message.content, options.expanded, theme),
16
+ (message, options, theme) => new MasterEventCard(message.content, options.expanded, theme),
22
17
  );
23
18
  }
24
19
 
@@ -26,19 +21,12 @@ class MasterEventCard implements Component {
26
21
  private readonly card: Component;
27
22
  private readonly fallback: Component;
28
23
 
29
- constructor(
30
- details: MasterEventDetails | undefined,
31
- content: string | (string | unknown)[],
32
- expanded: boolean,
33
- theme: Theme,
34
- ) {
35
- const text = plainContent(content);
24
+ constructor(content: string | (string | unknown)[], expanded: boolean, theme: Theme) {
25
+ const text = textOf(content);
36
26
  this.fallback = new Text(text, 0, 0);
37
27
  let card: Component | undefined;
38
28
  try {
39
- card = !expanded && isValidMasterEventDetails(details)
40
- ? compactCard(details, theme)
41
- : fullCard(text, theme);
29
+ card = !expanded ? compactCard(text, theme) : fullCard(text, theme);
42
30
  } catch {
43
31
  card = undefined;
44
32
  }
@@ -63,8 +51,12 @@ class MasterEventCard implements Component {
63
51
  }
64
52
  }
65
53
 
66
- function compactCard(details: MasterEventDetails, theme: Theme): Component {
67
- return card(theme, details.titles.map((title) => new CompactTitle(`◆ ${title}`)));
54
+ /** 每个事件一行:标题 + 正文首句;不是信封的旧消息退化为完整内容。 */
55
+ function compactCard(text: string, theme: Theme): Component {
56
+ const entries = machineEntries(text);
57
+ if (!entries) return fullCard(text, theme);
58
+ return card(theme, entries.map((entry) =>
59
+ new CompactTitle(`◆ ${entry.title}${entry.preview ? ` — ${firstSentence(entry.preview)}` : ""}`)));
68
60
  }
69
61
 
70
62
  /**
@@ -83,7 +75,8 @@ class CompactTitle implements Component {
83
75
  }
84
76
 
85
77
  function fullCard(text: string, theme: Theme): Component {
86
- return card(theme, [new Markdown(text, 0, 0, getMarkdownTheme())]);
78
+ const bodies = parseEnvelopes(text)?.map((envelope) => envelope.body).join("\n\n") ?? text;
79
+ return card(theme, [new Markdown(bodies, 0, 0, getMarkdownTheme())]);
87
80
  }
88
81
 
89
82
  function card(theme: Theme, children: Component[]): Component {
@@ -92,15 +85,3 @@ function card(theme: Theme, children: Component[]): Component {
92
85
  for (const child of children) box.addChild(child);
93
86
  return box;
94
87
  }
95
-
96
- function plainContent(content: string | (string | unknown)[]): string {
97
- if (typeof content === "string") return content;
98
- if (!Array.isArray(content)) return "";
99
- return content
100
- .map((part) =>
101
- typeof part === "object" && part !== null && "text" in part
102
- ? String((part as { text: unknown }).text)
103
- : "")
104
- .filter(Boolean)
105
- .join("\n");
106
- }
@@ -1,52 +1,83 @@
1
1
  /**
2
- * Master 事件文本与紧凑行的共享格式契约(纯函数、零宿主依赖,可被测试直接导入)。
3
- * 分节标记是事件产文与紧凑卡提取的单一事实源:改词两侧编译期同步,杜绝静默退化
4
- * (曾经的隐式跨文件耦合让"改文案→预览悄悄消失"且测试全绿)。
2
+ * Master 事件正文的唯一产文处:第一行是给人看的标题“<名字> <结果词>”(名字是第一个空格前的词),
3
+ * 给模型的指令放在后续正文。分节标记独占一行“<标记>:”;失败只由“错误:”分节表示——
4
+ * 红色、失败计数与活动列表失败组都从这一个事实来(失败、审查停止、审查未完成;被中断不是失败)。
5
+ * 折叠界面(tools/machine.ts)按标题、“错误:”分节与“耗时:本次运行 …”行投影,改格式两侧都要同步。
5
6
  */
7
+ import { formatDuration } from "../format.js";
8
+ import type { ReviewOutcome } from "../review/outcome.js";
9
+
6
10
  export const MASTER_EVENT_TYPE = "firecode-master-event";
7
11
 
8
- /** 正文分节标记:事件产文用它拼标记行,紧凑行提取用它识别正文首句。 */
9
- export const BODY_SECTIONS = {
10
- reply: "回复",
11
- error: "错误",
12
- question: "问题",
13
- finalReply: "最终回复",
14
- lastOutput: "中断前最后输出",
12
+ const SECTIONS = {
13
+ reply: "回复:",
14
+ error: "错误:",
15
+ finalReply: "最终回复:",
16
+ advice: "顾问意见:",
15
17
  } as const;
16
18
 
17
- /** 产文侧唯一入口:正文段一律以 `<标记>:` 独占一行开头。 */
18
- export function sectionLine(section: keyof typeof BODY_SECTIONS): string {
19
- return `${BODY_SECTIONS[section]}:`;
20
- }
21
-
22
- const BODY_MARKER = new RegExp(`^(?:${Object.values(BODY_SECTIONS).join("|")}):$`, "u");
19
+ const OBLIGATION = "此票有审查义务,请显式 review。";
23
20
 
24
- const VERSION = 1;
25
-
26
- export interface MasterEventDetails {
27
- version: typeof VERSION;
28
- titles: string[];
21
+ /**
22
+ * 产出的事件:正文与是否落定类。落定类(已返回、失败、被中断、审查通过/停止/未完成)才带“本次运行”耗时,
23
+ * 界面据此触发到达高亮;其余事件(待续跑、已切换模型、补充说明未送达)只带当前任务耗时。
24
+ */
25
+ export interface MasterEvent {
26
+ body: string;
27
+ settled: boolean;
29
28
  }
30
29
 
31
- /** 每个事件一行:首行标题句 + 正文首句预览(渲染时按宽度截断)。 */
32
- export function masterEventDetails(contents: string[]): MasterEventDetails {
33
- return { version: VERSION, titles: contents.map(compactLine) };
34
- }
30
+ const lines = (...parts: (string | undefined)[]) => parts.filter((part) => part !== undefined).join("\n");
31
+ const settled = (...parts: (string | undefined)[]): MasterEvent => ({ body: lines(...parts), settled: true });
32
+ const notice = (...parts: (string | undefined)[]): MasterEvent => ({ body: lines(...parts), settled: false });
35
33
 
36
- function compactLine(content: string): string {
37
- const lines = content.split("\n");
38
- const title = lines[0] ?? "";
39
- const marker = lines.findIndex((line) => BODY_MARKER.test(line.trim()));
40
- if (marker < 0) return title;
41
- const preview = lines.slice(marker + 1).find((line) => line.trim());
42
- return preview ? `${title} — ${preview.trim()}` : title;
43
- }
34
+ /**
35
+ * 一次运行里用户在子代理全过程视图直接说的话(按顺序)。有就在标题结果词后注明来源、正文先列原话:
36
+ * 指挥官据此知道这次运行不是它派的,只记下不向用户复述;成败、发落、在飞数与普通 send 完全相同。
37
+ */
38
+ export type ViewPrompts = readonly string[];
39
+ const VIEW_MARK = "(你在子代理视图里直接派的)";
40
+ const runTitle = (name: string, word: string, view: ViewPrompts) => `${name} ${word}${view.length ? VIEW_MARK : ""}`;
41
+ const youSaid = (view: ViewPrompts) => view.map((prompt) => `你说:${prompt}`);
42
+
43
+ export const masterEvent = {
44
+ returned: (name: string, reply: string, obligation = false, view: ViewPrompts = []) =>
45
+ settled(runTitle(name, "已返回", view), ...youSaid(view), SECTIONS.reply, reply, obligation ? OBLIGATION : undefined),
46
+ failed: (name: string, error: string, obligation = false, view: ViewPrompts = []) =>
47
+ settled(runTitle(name, "失败", view), ...youSaid(view), SECTIONS.error, error, obligation ? OBLIGATION : undefined),
48
+ interrupted: (name: string, obligation: boolean, view: ViewPrompts = []) =>
49
+ settled(runTitle(name, "被中断", view), ...youSaid(view), obligation ? "会话与审查义务均已保留,可 send 续派" : "会话已保留,可 send 续派"),
50
+ reviewIncomplete: (name: string, reason: string, reply?: string) =>
51
+ settled(`${name} 审查未完成`, SECTIONS.error, reason, ...(reply === undefined ? [] : [SECTIONS.finalReply, reply || "(无回复)"])),
52
+ /** 审查终态;reply 是 Worker 最后一条回复。停止与未完成是失败:原因进“错误:”分节,顾问意见在其后。 */
53
+ review(name: string, outcome: ReviewOutcome, reply: string): MasterEvent {
54
+ const final = [SECTIONS.finalReply, reply || "(无回复)"];
55
+ if (outcome.status === "passed") return settled(`${name} 审查通过(${outcome.rounds} 轮)`, ...final);
56
+ if (outcome.status === "stopped")
57
+ return settled(
58
+ `${name} 审查停止(${outcome.rounds} 轮)`,
59
+ SECTIONS.error,
60
+ outcome.advisorAdvice ? `审查 ${outcome.rounds} 轮未通过,顾问叫停` : `审查 ${outcome.rounds} 轮用尽仍未通过`,
61
+ ...(outcome.advisorAdvice ? [SECTIONS.advice, outcome.advisorAdvice] : []),
62
+ ...final,
63
+ );
64
+ if (outcome.status === "failed") return masterEvent.reviewIncomplete(name, outcome.reason, reply);
65
+ if (outcome.status === "error") return masterEvent.reviewIncomplete(name, `审查读取失败:${outcome.message}`);
66
+ return masterEvent.reviewIncomplete(name, `审查结束时没有终态:${outcome.status}`);
67
+ },
68
+ /** 只给会话重载打断的回合:指挥官自己发起的 interrupt 它知道现场,不提醒。 */
69
+ resumeReminder: (name: string) => notice(`${name} 待续跑`, "上次回合被会话重载打断,恢复后一直没有续派;请 send 续派或 kill 收口"),
70
+ stranded: (name: string, texts: string[]) =>
71
+ notice(`${name} 补充说明未送达`, `回合结束时有 ${texts.length} 条补充说明未送达,请重发:`, texts.join("\n---\n")),
72
+ modelSwitched: (name: string, from: string, to: string, reason: string) =>
73
+ notice(`${name} 已切换模型`, `已切换 ${from}→${to}(${reason}),正在同一会话自动续跑`),
74
+ };
44
75
 
45
- export function isValidMasterEventDetails(value: unknown): value is MasterEventDetails {
46
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
47
- const record = value as Record<string, unknown>;
48
- if (Object.keys(record).length !== 2 || record.version !== VERSION) return false;
49
- return Array.isArray(record.titles) &&
50
- record.titles.length > 0 &&
51
- record.titles.every((title) => typeof title === "string");
76
+ /** 落定类正文末尾追加 Worker 本次运行耗时(子代理会话写下的轮记录给出);没有记录或非落定事件不追加,不用别处的计时冒充。 */
77
+ export function withElapsed(event: MasterEvent, { run, task }: { run?: number; task?: number }): string {
78
+ const parts = [
79
+ ...(run === undefined || !event.settled ? [] : [`本次运行 ${formatDuration(run)}`]),
80
+ ...(task === undefined ? [] : [`当前任务 ${formatDuration(task)}`]),
81
+ ];
82
+ return parts.length ? `${event.body}\n耗时:${parts.join(" · ")}` : event.body;
52
83
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Worker 会话里 Master 只注册这一件事:edit/write 只能落在当前 checkout 或系统临时目录内(按真实路径判断)。
3
+ * 临时目录放行是因为调研报告、评测产物这类交付物本来就写在那里;拦掉只会逼 Worker 改用 bash 绕过守卫。
4
+ */
5
+ import { realpath } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
8
+ import { isToolCallEventType, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
+
10
+ export function registerWorkerGuard(pi: ExtensionAPI): void {
11
+ pi.on("tool_call", async (event, ctx) => {
12
+ if (!isToolCallEventType("edit", event) && !isToolCallEventType("write", event)) return;
13
+ const reason = await outsideCheckoutReason(event.input.path, ctx.cwd);
14
+ if (reason) return { block: true, reason };
15
+ });
16
+ }
17
+
18
+ /** os.tmpdir() 在 macOS 是每用户的 /var/folders/…,/tmp 是另一处,两处都是临时目录。 */
19
+ const TEMP_ROOTS = [tmpdir(), "/tmp"];
20
+
21
+ async function outsideCheckoutReason(path: string, cwd: string): Promise<string | undefined> {
22
+ const target = await canonicalWritePath(resolve(cwd, path));
23
+ const roots = await Promise.all([cwd, ...TEMP_ROOTS].map(canonicalWritePath));
24
+ return roots.some((root) => within(root, target)) ? undefined : `子代理只能修改当前 checkout 或系统临时目录:${path}`;
25
+ }
26
+
27
+ function within(root: string, target: string): boolean {
28
+ const local = relative(root, target);
29
+ return local !== ".." && !local.startsWith(`..${sep}`) && !isAbsolute(local);
30
+ }
31
+
32
+ /** 目标可能还不存在:沿祖先找到第一个真实存在的目录取真实路径,再拼回缺失的部分。 */
33
+ async function canonicalWritePath(path: string): Promise<string> {
34
+ let ancestor = path;
35
+ const missing: string[] = [];
36
+ while (true) {
37
+ try {
38
+ return resolve(await realpath(ancestor), ...missing.reverse());
39
+ } catch {
40
+ const parent = dirname(ancestor);
41
+ if (parent === ancestor) return path;
42
+ missing.push(basename(ancestor));
43
+ ancestor = parent;
44
+ }
45
+ }
46
+ }