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/master/event-card.ts
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Master 事件卡:默认紧凑(每事件一行标题行),ctrl+o 展开完整内容。
|
|
3
|
-
*
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
}
|
package/master/event-format.ts
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
/**
|
|
22
|
+
* 产出的事件:正文与是否落定类。落定类(已返回、失败、被中断、审查通过/停止/未完成)才带“本次运行”耗时,
|
|
23
|
+
* 界面据此触发到达高亮;其余事件(待续跑、已切换模型、补充说明未送达)只带当前任务耗时。
|
|
24
|
+
*/
|
|
25
|
+
export interface MasterEvent {
|
|
26
|
+
body: string;
|
|
27
|
+
settled: boolean;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
}
|
package/master/guard.ts
ADDED
|
@@ -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
|
+
}
|