pi-firecode 1.0.0 → 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.
- package/README.md +55 -41
- package/README.zh-CN.md +74 -0
- package/config.example.jsonc +21 -22
- package/dist/config.example.jsonc +90 -0
- package/dist/index.js +11464 -0
- package/dist/master/prompts/master.en.md +22 -0
- package/{master → dist/master}/prompts/master.zh.md +3 -3
- package/dist/master/prompts/worker.en.md +1 -0
- package/dist/watcher/prompts/watch.en.md +43 -0
- package/package.json +10 -27
- package/activity.ts +0 -92
- package/busy.ts +0 -188
- package/config.ts +0 -500
- package/deliver.ts +0 -119
- package/flame.ts +0 -149
- package/format.ts +0 -133
- package/header.ts +0 -218
- package/herdr-client.ts +0 -60
- package/index.ts +0 -68
- package/jsonc.ts +0 -34
- package/master/actions.ts +0 -293
- package/master/activity-list.ts +0 -281
- package/master/event-card.ts +0 -87
- package/master/event-format.ts +0 -83
- package/master/guard.ts +0 -46
- package/master/index.ts +0 -252
- package/master/list-view.ts +0 -129
- package/master/outbox.ts +0 -188
- package/master/prompt.ts +0 -26
- package/master/role.ts +0 -18
- package/master/run.ts +0 -315
- package/master/runtime.ts +0 -259
- package/master/spawn.ts +0 -234
- package/master/state.ts +0 -225
- package/master/worker-view.ts +0 -517
- package/provider/claude-sub.ts +0 -151
- package/provider/openai-native/index.ts +0 -8
- package/provider/openai-native/src/compact-client.ts +0 -350
- package/provider/openai-native/src/config.ts +0 -297
- package/provider/openai-native/src/extension.ts +0 -93
- package/provider/openai-native/src/native-compaction.ts +0 -151
- package/provider/openai-native/src/native-details.ts +0 -157
- package/provider/openai-native/src/native-replay.ts +0 -253
- package/provider/openai-native/src/native-runtime.ts +0 -144
- package/provider/openai-native/src/options.ts +0 -87
- package/provider/openai-native/src/request-pipeline.ts +0 -21
- package/provider/openai-native/src/responses-input.ts +0 -433
- package/review/advisor.ts +0 -110
- package/review/card.ts +0 -398
- package/review/checkpoint.ts +0 -378
- package/review/evidence.ts +0 -230
- package/review/index.ts +0 -992
- package/review/occupancy.ts +0 -25
- package/review/outcome.ts +0 -98
- package/review/prompt.ts +0 -209
- package/review/reviewer.ts +0 -353
- package/review/session.ts +0 -110
- package/review/state.ts +0 -877
- package/review/ui.ts +0 -89
- package/round-recorder.ts +0 -18
- package/session/herdr-display.ts +0 -87
- package/session/presets.ts +0 -300
- package/session/quota.ts +0 -123
- package/session/rename.ts +0 -46
- package/session/stats.ts +0 -313
- package/statusbar/index.ts +0 -256
- package/statusbar/render.ts +0 -113
- package/theme.ts +0 -45
- package/tools/actions.ts +0 -53
- package/tools/assistant-view.ts +0 -64
- package/tools/click-anchor.ts +0 -56
- package/tools/group-view.ts +0 -420
- package/tools/grouping.ts +0 -158
- package/tools/host.ts +0 -369
- package/tools/index.ts +0 -186
- package/tools/line.ts +0 -231
- package/tools/machine.ts +0 -78
- package/tools/parts.ts +0 -143
- package/tools/round.ts +0 -95
- package/tools/timing.ts +0 -22
- package/tools/turn-clock.ts +0 -41
- package/tools/turn-summary.ts +0 -109
- package/truncated-write.ts +0 -18
- package/watcher/card.ts +0 -91
- package/watcher/index.ts +0 -176
- package/watcher/observer.ts +0 -75
- package/watcher/transcript.ts +0 -56
- /package/{master → dist/master}/prompts/worker.zh.md +0 -0
- /package/{review → dist/review}/prompts/advisor.en.md +0 -0
- /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
- /package/{review → dist/review}/prompts/review.en.md +0 -0
- /package/{review → dist/review}/prompts/review.zh.md +0 -0
- /package/{watcher → dist/watcher}/prompts/watch.zh.md +0 -0
package/tools/index.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 接管默认 4 工具(read/bash/edit/write)的展示:组摘要/紧凑列表 + 单工具正文,保留耗时/大小列。
|
|
3
|
-
*
|
|
4
|
-
* 只包装默认激活的工具:原版 pi 的 registerTool 是注册即激活(会话构建与 reload
|
|
5
|
-
* 固定 includeAllExtensionTools),给 grep/find/ls 挂渲染包装会把它们在所有会话
|
|
6
|
-
* 强制打开——渲染接管不得改变工具集,因此那三个用宿主默认渲染。
|
|
7
|
-
*/
|
|
8
|
-
import {
|
|
9
|
-
createBashTool,
|
|
10
|
-
createEditTool,
|
|
11
|
-
createReadTool,
|
|
12
|
-
createWriteTool,
|
|
13
|
-
defineTool,
|
|
14
|
-
type ExtensionAPI,
|
|
15
|
-
} from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { watchBusy } from "../busy.js";
|
|
17
|
-
import { installGroupPatch } from "./grouping.js";
|
|
18
|
-
import { ToolLine, makeResultRenderer } from "./line.js";
|
|
19
|
-
import { LABEL, toolTarget } from "./actions.js";
|
|
20
|
-
import { diffMeta } from "./parts.js";
|
|
21
|
-
import { ROUND_ENTRY, renderRound } from "./round.js";
|
|
22
|
-
import { clearDurations, executeTimed } from "./timing.js";
|
|
23
|
-
import { TurnClock } from "./turn-clock.js";
|
|
24
|
-
|
|
25
|
-
type ToolMap = {
|
|
26
|
-
read: ReturnType<typeof createReadTool>;
|
|
27
|
-
bash: ReturnType<typeof createBashTool>;
|
|
28
|
-
edit: ReturnType<typeof createEditTool>;
|
|
29
|
-
write: ReturnType<typeof createWriteTool>;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const cache = new Map<string, ToolMap>();
|
|
33
|
-
|
|
34
|
-
function createTools(cwd: string): ToolMap {
|
|
35
|
-
return {
|
|
36
|
-
read: createReadTool(cwd),
|
|
37
|
-
bash: createBashTool(cwd),
|
|
38
|
-
edit: createEditTool(cwd),
|
|
39
|
-
write: createWriteTool(cwd),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** 工具实例按 cwd 复用:同一会话内 cwd 不变,切目录也不必重建全部工具。 */
|
|
44
|
-
function tools(cwd: string): ToolMap {
|
|
45
|
-
let value = cache.get(cwd);
|
|
46
|
-
if (!value) {
|
|
47
|
-
value = createTools(cwd);
|
|
48
|
-
cache.set(cwd, value);
|
|
49
|
-
}
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function lineCount(text: string): number {
|
|
54
|
-
if (text === "") return 0;
|
|
55
|
-
const lines = text.split("\n").length;
|
|
56
|
-
return text.endsWith("\n") ? lines - 1 : lines;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function invoke<T extends (...args: never[]) => unknown>(
|
|
60
|
-
execute: T,
|
|
61
|
-
args: unknown[],
|
|
62
|
-
): ReturnType<T> {
|
|
63
|
-
return Reflect.apply(execute, undefined, args) as ReturnType<T>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const EDIT_RESULT = makeResultRenderer(false);
|
|
67
|
-
let definitions: ReturnType<typeof buildDefinitions> | undefined;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* 默认四工具的展示包装(执行仍是宿主工具,按调用 cwd 取实例):主会话注册它们,子代理全过程视图拿同一份定义渲染子会话的工具行。
|
|
71
|
-
*/
|
|
72
|
-
export function toolDefinitions() {
|
|
73
|
-
return definitions ??= buildDefinitions();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function buildDefinitions() {
|
|
77
|
-
const initial = tools(process.cwd());
|
|
78
|
-
return {
|
|
79
|
-
read: defineTool({
|
|
80
|
-
...initial.read,
|
|
81
|
-
label: LABEL.read,
|
|
82
|
-
renderShell: "self",
|
|
83
|
-
execute: (id, params, signal, update, ctx) =>
|
|
84
|
-
executeTimed(id, () => invoke(tools(ctx.cwd).read.execute, [id, params, signal, update, ctx])),
|
|
85
|
-
renderCall: (args, theme, ctx) =>
|
|
86
|
-
new ToolLine({
|
|
87
|
-
label: LABEL.read,
|
|
88
|
-
...toolTarget("read", args, ctx.cwd),
|
|
89
|
-
theme,
|
|
90
|
-
ctx,
|
|
91
|
-
}),
|
|
92
|
-
renderResult: makeResultRenderer(true),
|
|
93
|
-
}),
|
|
94
|
-
bash: defineTool({
|
|
95
|
-
...initial.bash,
|
|
96
|
-
label: LABEL.bash,
|
|
97
|
-
renderShell: "self",
|
|
98
|
-
execute: (id, params, signal, update, ctx) =>
|
|
99
|
-
executeTimed(id, () => invoke(tools(ctx.cwd).bash.execute, [id, params, signal, update, ctx])),
|
|
100
|
-
renderCall: (args, theme, ctx) =>
|
|
101
|
-
new ToolLine({
|
|
102
|
-
label: LABEL.bash,
|
|
103
|
-
...toolTarget("bash", args, ctx.cwd),
|
|
104
|
-
theme,
|
|
105
|
-
ctx,
|
|
106
|
-
}),
|
|
107
|
-
renderResult: makeResultRenderer(true),
|
|
108
|
-
}),
|
|
109
|
-
edit: defineTool({
|
|
110
|
-
...initial.edit,
|
|
111
|
-
label: LABEL.edit,
|
|
112
|
-
renderShell: "self",
|
|
113
|
-
execute: (id, params, signal, update, ctx) =>
|
|
114
|
-
executeTimed(id, () => invoke(tools(ctx.cwd).edit.execute, [id, params, signal, update, ctx])),
|
|
115
|
-
renderCall: (args, theme, ctx) =>
|
|
116
|
-
new ToolLine({
|
|
117
|
-
label: LABEL.edit,
|
|
118
|
-
...toolTarget("edit", args, ctx.cwd),
|
|
119
|
-
theme,
|
|
120
|
-
ctx,
|
|
121
|
-
}),
|
|
122
|
-
renderResult(result, options, theme, ctx) {
|
|
123
|
-
const details = result.details as { diff?: unknown } | undefined;
|
|
124
|
-
const diff = !ctx.isError && typeof details?.diff === "string" ? details.diff : undefined;
|
|
125
|
-
ctx.state.meta = diff ? diffMeta(diff) : undefined;
|
|
126
|
-
const display = options.expanded && diff
|
|
127
|
-
? { ...result, content: [...result.content, { type: "text" as const, text: diff }] }
|
|
128
|
-
: result;
|
|
129
|
-
return EDIT_RESULT(display, options, theme, ctx);
|
|
130
|
-
},
|
|
131
|
-
}),
|
|
132
|
-
write: defineTool({
|
|
133
|
-
...initial.write,
|
|
134
|
-
label: LABEL.write,
|
|
135
|
-
renderShell: "self",
|
|
136
|
-
execute: (id, params, signal, update, ctx) =>
|
|
137
|
-
executeTimed(id, () => invoke(tools(ctx.cwd).write.execute, [id, params, signal, update, ctx])),
|
|
138
|
-
renderCall: (args, theme, ctx) =>
|
|
139
|
-
new ToolLine({
|
|
140
|
-
label: LABEL.write,
|
|
141
|
-
...toolTarget("write", args, ctx.cwd),
|
|
142
|
-
meta: [{ text: ` +${lineCount(args.content ?? "")}`, color: "toolDiffAdded" }],
|
|
143
|
-
theme,
|
|
144
|
-
ctx,
|
|
145
|
-
}),
|
|
146
|
-
renderResult: makeResultRenderer(false),
|
|
147
|
-
}),
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function registerToolRendering(pi: ExtensionAPI): void {
|
|
152
|
-
let dispose: (() => void) | undefined;
|
|
153
|
-
// 时钟只投影 busy.ts 的唯一状态机;拆会话会重载扩展,不跨会话复用。
|
|
154
|
-
const clock = new TurnClock();
|
|
155
|
-
// 轮记录由根级轮记录器写(每个会话都有);这里只把它渲染成零行标记,供摘要行读。
|
|
156
|
-
pi.registerEntryRenderer(ROUND_ENTRY, renderRound);
|
|
157
|
-
watchBusy(pi, { onChange: (view) => clock.sync(view) });
|
|
158
|
-
pi.on("session_start", (_event, ctx) => {
|
|
159
|
-
if (ctx.mode !== "tui") return;
|
|
160
|
-
dispose?.();
|
|
161
|
-
clearDurations();
|
|
162
|
-
dispose = installGroupPatch(ctx.ui, { clock });
|
|
163
|
-
ctx.ui.setToolsExpanded(false);
|
|
164
|
-
});
|
|
165
|
-
pi.on("session_shutdown", () => {
|
|
166
|
-
if (!dispose) return;
|
|
167
|
-
dispose();
|
|
168
|
-
dispose = undefined;
|
|
169
|
-
clearDurations();
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
for (const definition of Object.values(toolDefinitions())) pi.registerTool(definition);
|
|
173
|
-
|
|
174
|
-
pi.registerCommand("tool-status", {
|
|
175
|
-
description: "显示当前已加载/启用工具",
|
|
176
|
-
handler: async (_args, ctx) => {
|
|
177
|
-
ctx.ui.notify(
|
|
178
|
-
`active: ${pi.getActiveTools().join(", ")}\nall: ${pi
|
|
179
|
-
.getAllTools()
|
|
180
|
-
.map((tool) => tool.name)
|
|
181
|
-
.join(", ")}`,
|
|
182
|
-
"info",
|
|
183
|
-
);
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
}
|
package/tools/line.ts
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/** 单条工具行的渲染:状态字形 + 标签 + 主体 + 右侧耗时/大小,展开时附完整结果。 */
|
|
2
|
-
import { stripVTControlCharacters } from "node:util";
|
|
3
|
-
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { type Component, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
5
|
-
import { type ClipSide, clip, oneLine } from "../format.js";
|
|
6
|
-
import {
|
|
7
|
-
type Part,
|
|
8
|
-
clipParts,
|
|
9
|
-
durationPart,
|
|
10
|
-
paint,
|
|
11
|
-
partsWidth,
|
|
12
|
-
sizePart,
|
|
13
|
-
} from "./parts.js";
|
|
14
|
-
import { takeDuration } from "./timing.js";
|
|
15
|
-
|
|
16
|
-
export const RAIL = "▏ ";
|
|
17
|
-
/**
|
|
18
|
-
* 聊天行右侧留出的列:宿主全屏模式把滚动条画在最后一列,按列切掉它时会丢掉行尾的颜色复位,
|
|
19
|
-
* 所以行宽到倒数第二列为止,末尾再空一格与滚动条隔开。
|
|
20
|
-
*/
|
|
21
|
-
export const CHAT_GUTTER = 2;
|
|
22
|
-
/** 右侧列与主体之间至少留出的空隙,不够就先丢弃右侧列 */
|
|
23
|
-
const RIGHT_GAP = 2;
|
|
24
|
-
const MIN_VALUE_WIDTH = 8;
|
|
25
|
-
/** 有错误摘要时,主体最多占可用宽度的比例 */
|
|
26
|
-
const ERROR_VALUE_RATIO = 0.4;
|
|
27
|
-
|
|
28
|
-
const STATUS = {
|
|
29
|
-
run: { glyph: "●", color: "accent", bg: "toolPendingBg" },
|
|
30
|
-
ok: { glyph: "✓", color: "success", bg: "toolSuccessBg" },
|
|
31
|
-
err: { glyph: "✗", color: "error", bg: "toolErrorBg" },
|
|
32
|
-
} as const satisfies Record<string, Status>;
|
|
33
|
-
|
|
34
|
-
type Status = { glyph: string; color: ThemeColor; bg?: ThemeBg };
|
|
35
|
-
type ThemeBg = Parameters<Theme["bg"]>[0];
|
|
36
|
-
|
|
37
|
-
/** renderCall / renderResult 之间共享的行状态。 */
|
|
38
|
-
export type RowState = {
|
|
39
|
-
/** 结果字符数(read/bash),驱动右对齐大小列 */
|
|
40
|
-
chars?: number;
|
|
41
|
-
durationMs?: number;
|
|
42
|
-
/** 结果产生的动态后缀(edit 的 ±diff) */
|
|
43
|
-
meta?: Part[];
|
|
44
|
-
/** 失败时的一行摘要 */
|
|
45
|
-
errorText?: string;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export type RenderContext = {
|
|
49
|
-
state: RowState;
|
|
50
|
-
cwd: string;
|
|
51
|
-
toolCallId: string;
|
|
52
|
-
isPartial: boolean;
|
|
53
|
-
isError: boolean;
|
|
54
|
-
expanded: boolean;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
type ResultContent = { type: string; text?: string };
|
|
58
|
-
export type ToolResult = { content?: ResultContent[]; details?: unknown };
|
|
59
|
-
|
|
60
|
-
export type ToolLineOptions = {
|
|
61
|
-
label: string;
|
|
62
|
-
value: Part[];
|
|
63
|
-
/** 溢出时从哪端截断:路径保尾部,命令保头部 */
|
|
64
|
-
clip: ClipSide;
|
|
65
|
-
/** 紧跟 value 的左侧后缀(edit ±diff、write +N) */
|
|
66
|
-
meta?: Part[];
|
|
67
|
-
theme: Theme;
|
|
68
|
-
ctx: RenderContext;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
function sanitizeDisplayText(text: string): string {
|
|
72
|
-
let output = "";
|
|
73
|
-
for (const char of stripVTControlCharacters(text)) {
|
|
74
|
-
const code = char.codePointAt(0);
|
|
75
|
-
if (code === undefined || code === 0x0d) continue;
|
|
76
|
-
if (code === 0x09 || code === 0x0a) output += char;
|
|
77
|
-
else if (code > 0x1f && (code < 0xfff9 || code > 0xfffb)) output += char;
|
|
78
|
-
}
|
|
79
|
-
return output;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function resultText(result: ToolResult, includeText: boolean): { displayText: string; chars: number } {
|
|
83
|
-
const blocks: string[] = [];
|
|
84
|
-
let chars = 0;
|
|
85
|
-
for (const item of result.content ?? []) {
|
|
86
|
-
if (item.type !== "text" || typeof item.text !== "string") continue;
|
|
87
|
-
if (includeText) blocks.push(sanitizeDisplayText(item.text));
|
|
88
|
-
chars += item.text.length;
|
|
89
|
-
}
|
|
90
|
-
return { displayText: blocks.join("\n"), chars };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** 右侧列之间用 " · " 分隔。 */
|
|
94
|
-
function rightContentWidth(parts: Part[]): number {
|
|
95
|
-
return partsWidth(parts) + Math.max(0, parts.length - 1) * 3;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const EMPTY_RESULT: Component = { render: () => [], invalidate() {} };
|
|
99
|
-
|
|
100
|
-
class ExpandedResult implements Component {
|
|
101
|
-
constructor(
|
|
102
|
-
private readonly text: string,
|
|
103
|
-
private readonly theme: Theme,
|
|
104
|
-
) {}
|
|
105
|
-
|
|
106
|
-
invalidate(): void {}
|
|
107
|
-
|
|
108
|
-
render(width: number): string[] {
|
|
109
|
-
const lines = wrapTextWithAnsi(this.text, Math.max(1, width));
|
|
110
|
-
return ["", ...lines.map((line) => this.theme.fg("toolOutput", line))];
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function paintBgLine(line: string, width: number, bgFn?: (text: string) => string): string {
|
|
115
|
-
const visLen = visibleWidth(line);
|
|
116
|
-
const padNeeded = Math.max(0, width - visLen);
|
|
117
|
-
const padded = line + " ".repeat(padNeeded);
|
|
118
|
-
return bgFn ? bgFn(padded) : padded;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** 折叠摘要从工具行取当前动作词;未提供该能力的静态工具由通用参数摘要承担。 */
|
|
122
|
-
export interface ActionLine extends Component {
|
|
123
|
-
readonly actionWord: string;
|
|
124
|
-
/** 当前动作的简短目标(纯文本),摘要行跟在动作词后面。 */
|
|
125
|
-
readonly actionTarget?: string;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export class ToolLine implements ActionLine {
|
|
129
|
-
constructor(private readonly options: ToolLineOptions) {}
|
|
130
|
-
invalidate(): void {}
|
|
131
|
-
|
|
132
|
-
get actionWord(): string {
|
|
133
|
-
return this.options.label;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
get actionTarget(): string {
|
|
137
|
-
return this.options.value.map((part) => part.text).join("").trim();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
render(width: number): string[] {
|
|
141
|
-
const { theme, ctx } = this.options;
|
|
142
|
-
const state = ctx.state;
|
|
143
|
-
const status = ctx.isError ? STATUS.err : ctx.isPartial ? STATUS.run : STATUS.ok;
|
|
144
|
-
const value = this.valueWithMeta();
|
|
145
|
-
return renderLine(theme, width, {
|
|
146
|
-
status,
|
|
147
|
-
label: { text: this.options.label, color: ctx.isError ? "error" : "toolTitle", bold: true },
|
|
148
|
-
value: ctx.isError ? value.map((part) => ({ ...part, color: "error" as const })) : value,
|
|
149
|
-
clip: this.options.clip,
|
|
150
|
-
error: ctx.isError && !ctx.expanded ? oneLine(state.errorText ?? "") : "",
|
|
151
|
-
right: [durationPart(state.durationMs), sizePart(state.chars)].filter((part): part is Part => !!part),
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private valueWithMeta(): Part[] {
|
|
156
|
-
const { value, meta, ctx } = this.options;
|
|
157
|
-
return [...value, ...(meta ?? []), ...(ctx.state.meta ?? [])];
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** 一行的布局:头与尾固定,值可裁,错误摘要与值分摊,右列空间不足时先整列丢弃。 */
|
|
162
|
-
type LineSpec = {
|
|
163
|
-
status: Status;
|
|
164
|
-
label: Part;
|
|
165
|
-
value: Part[];
|
|
166
|
-
clip: ClipSide;
|
|
167
|
-
error?: string;
|
|
168
|
-
right: Part[];
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
function renderLine(theme: Theme, width: number, spec: LineSpec): string[] {
|
|
172
|
-
const { status } = spec;
|
|
173
|
-
const safeWidth = Math.max(1, width - CHAT_GUTTER);
|
|
174
|
-
const head: Part[] = [
|
|
175
|
-
{ text: RAIL, color: "dim" },
|
|
176
|
-
{ text: `${status.glyph} `, color: status.color, bold: true },
|
|
177
|
-
spec.label,
|
|
178
|
-
...(partsWidth(spec.value) ? [{ text: " " }] : []),
|
|
179
|
-
];
|
|
180
|
-
const fixedWidth = partsWidth(head);
|
|
181
|
-
const bg = status.bg;
|
|
182
|
-
const bgFn = bg ? (text: string) => theme.bg(bg, text) : undefined;
|
|
183
|
-
if (safeWidth <= fixedWidth) {
|
|
184
|
-
const clipped = paint(theme, clipParts(head, safeWidth, "end"));
|
|
185
|
-
return [paintBgLine(clipped, width, bgFn)];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const right = [...spec.right];
|
|
189
|
-
while (right.length && safeWidth - fixedWidth - rightContentWidth(right) - RIGHT_GAP < MIN_VALUE_WIDTH)
|
|
190
|
-
right.pop();
|
|
191
|
-
const rightVisible = rightContentWidth(right);
|
|
192
|
-
const freeWidth = safeWidth - fixedWidth - (right.length ? rightVisible + RIGHT_GAP : 0);
|
|
193
|
-
let value = spec.value;
|
|
194
|
-
let errorPart: Part | undefined;
|
|
195
|
-
if (spec.error) {
|
|
196
|
-
value = clipParts(value, Math.max(1, Math.floor(freeWidth * ERROR_VALUE_RATIO)), spec.clip);
|
|
197
|
-
const errorWidth = freeWidth - partsWidth(value) - 3;
|
|
198
|
-
if (errorWidth >= 1)
|
|
199
|
-
errorPart = { text: ` · ${clip(spec.error, errorWidth, "end")}`, color: "error" };
|
|
200
|
-
} else {
|
|
201
|
-
value = clipParts(value, freeWidth, spec.clip);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const body = [...head, ...value, ...(errorPart ? [errorPart] : [])];
|
|
205
|
-
let line = paint(theme, body);
|
|
206
|
-
if (right.length) {
|
|
207
|
-
const pad = Math.max(RIGHT_GAP, safeWidth - partsWidth(body) - rightVisible);
|
|
208
|
-
line += " ".repeat(pad) + right.map((part) => paint(theme, [part])).join(theme.fg("dim", " · "));
|
|
209
|
-
}
|
|
210
|
-
return [paintBgLine(line, width, bgFn)];
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** 折叠时只回写行状态;展开时输出完整结果。 */
|
|
214
|
-
export function makeResultRenderer(sized: boolean) {
|
|
215
|
-
return (
|
|
216
|
-
result: ToolResult,
|
|
217
|
-
options: { expanded: boolean },
|
|
218
|
-
theme: Theme,
|
|
219
|
-
context: RenderContext,
|
|
220
|
-
): Component => {
|
|
221
|
-
const state = context.state;
|
|
222
|
-
const { displayText, chars } = resultText(result, options.expanded || context.isError);
|
|
223
|
-
if (sized) state.chars = chars;
|
|
224
|
-
const durationMs = takeDuration(context.toolCallId);
|
|
225
|
-
if (durationMs !== undefined) state.durationMs = durationMs;
|
|
226
|
-
state.errorText = context.isError ? displayText : "";
|
|
227
|
-
return options.expanded && displayText !== ""
|
|
228
|
-
? new ExpandedResult(displayText, theme)
|
|
229
|
-
: EMPTY_RESULT;
|
|
230
|
-
};
|
|
231
|
-
}
|
package/tools/machine.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/** 机器消息(指挥官事件、观察员发言、审查卡)的一行投影:卡片与折叠展开态共用,数据只来自信封正文。 */
|
|
2
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { type EnvelopeTag, parseEnvelopes } from "../deliver.js";
|
|
4
|
-
import { clip, firstSentence } from "../format.js";
|
|
5
|
-
import { CHAT_GUTTER } from "./line.js";
|
|
6
|
-
|
|
7
|
-
export interface MachineEntry {
|
|
8
|
-
/** 一行标题:Master 事件与审查卡是信封正文第一行原样,观察员固定为“观察员”。 */
|
|
9
|
-
title: string;
|
|
10
|
-
/** 正文首句,没有则为空。 */
|
|
11
|
-
preview: string;
|
|
12
|
-
/** 画失败色:只认生产端声明的事实——Master 事件带“错误:”分节,审查卡的 details.tone 为 warning/error。 */
|
|
13
|
-
alarm: boolean;
|
|
14
|
-
/** Master 事件才有:标题第一个空格前的子代理名。 */
|
|
15
|
-
worker?: string;
|
|
16
|
-
/** Master 事件正文带独占一行的“错误:”分节:子代理失败。 */
|
|
17
|
-
failed?: boolean;
|
|
18
|
-
/** 落定类 Master 事件的子代理本次运行时长;有它即触发摘要行的到达高亮。 */
|
|
19
|
-
duration?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** 正文分节标记独占一行(如“回复:”“错误:”);失败时预览取错误分节之后的正文,否则取首个分节之后的。 */
|
|
23
|
-
const SECTION = /^[^\s:]{1,8}:$/u;
|
|
24
|
-
const ERROR_SECTION = "错误:";
|
|
25
|
-
const RUN_TIME = /^耗时:本次运行 (\S+)/mu;
|
|
26
|
-
/** 审查卡生产端(review/card.ts)在 details.tone 里声明的失败色。 */
|
|
27
|
-
const ALARM_TONES = new Set(["warning", "error"]);
|
|
28
|
-
/** 审查卡正文里的发现标题(“## 发现 1:…”)与原因行(“原因:…”)。 */
|
|
29
|
-
const FINDING = /^#{1,6}\s*(?:发现|Finding)\s*[^::]*[::]\s*(.+)$/mu;
|
|
30
|
-
const REASON = /^(?:原因|Reason)[::]\s*(.+)$/mu;
|
|
31
|
-
/** 审查卡里不是结论的行:模型分节、模型清单、卡点、分隔线与用时脚注。 */
|
|
32
|
-
const REVIEW_NOISE = /^(?:\*\*(?:模型|Model)[ ·].*\*\*|(?:模型|Models)[::].*|(?:卡点|Blocker)[::].*|---|(?:用时|Elapsed)[::].*)$/u;
|
|
33
|
-
|
|
34
|
-
/** review/state.ts 多审查者通过汇总的一行:“• 模型短名:结论”。 */
|
|
35
|
-
const MODEL_BULLET = /^•\s*[^\s::]+[::]\s*/u;
|
|
36
|
-
|
|
37
|
-
/** details 是承载信封的 CustomMessage 的 details(审查卡带 tone);用户消息形态没有。 */
|
|
38
|
-
export function machineEntries(text: string, details?: unknown): MachineEntry[] | undefined {
|
|
39
|
-
return parseEnvelopes(text)?.map(({ tag, body }) => entryOf(tag, body, details));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function entryOf(tag: EnvelopeTag, body: string, details: unknown): MachineEntry {
|
|
43
|
-
const [heading = "", ...rest] = body.split("\n");
|
|
44
|
-
if (tag === "firecode_watcher") return { title: "观察员", preview: firstSentence(rest.join("\n")), alarm: false };
|
|
45
|
-
if (tag === "firecode_review")
|
|
46
|
-
return { title: heading, preview: reviewPreview(rest), alarm: ALARM_TONES.has((details as { tone?: string } | undefined)?.tone ?? "") };
|
|
47
|
-
// 失败只看是否有独占一行的“错误:”,不论它是第几个分节;失败时预览从错误分节取。
|
|
48
|
-
// 预览只取分节里给人看的正文:没有分节的事件(通知、被中断)正文是写给模型的指令,↳ 只显示标题。
|
|
49
|
-
const errorAt = rest.findIndex((line) => line.trim() === ERROR_SECTION);
|
|
50
|
-
const failed = errorAt >= 0;
|
|
51
|
-
const marker = failed ? errorAt : rest.findIndex((line) => SECTION.test(line.trim()));
|
|
52
|
-
const content = marker < 0 ? "" : rest.slice(marker + 1).filter((line) => !RUN_TIME.test(line)).join("\n");
|
|
53
|
-
const duration = RUN_TIME.exec(body)?.[1];
|
|
54
|
-
return {
|
|
55
|
-
title: heading,
|
|
56
|
-
preview: firstSentence(content),
|
|
57
|
-
alarm: failed,
|
|
58
|
-
worker: heading.split(" ", 1)[0],
|
|
59
|
-
failed,
|
|
60
|
-
...(duration ? { duration } : {}),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** 审查卡的预览是结论:首条发现标题,否则原因,否则第一句非模型名的正文(多审查者汇总行去掉“• 模型:”前缀)。 */
|
|
65
|
-
function reviewPreview(lines: string[]): string {
|
|
66
|
-
const text = lines.join("\n");
|
|
67
|
-
const pick = FINDING.exec(text)?.[1] ?? REASON.exec(text)?.[1];
|
|
68
|
-
const body = lines.filter((line) => !REVIEW_NOISE.test(line.trim())).map((line) => line.replace(MODEL_BULLET, ""));
|
|
69
|
-
return firstSentence(pick ?? body.join("\n"));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** “↳ 标题 · 时长 首句”;不铺背景,可直接用 clip 截断。宿主在聊天区右缘画滚动条,留一列给它,截断才由 clip 加省略号。 */
|
|
73
|
-
export function machineLine(entry: MachineEntry, theme: Theme, width: number): string {
|
|
74
|
-
const head = `${theme.fg(entry.alarm ? "error" : "success", "↳")} ${theme.fg(entry.alarm ? "error" : "text", entry.title)}`;
|
|
75
|
-
const duration = entry.duration ? `${theme.fg("dim", " · ")}${theme.fg("muted", entry.duration)}` : "";
|
|
76
|
-
const preview = entry.preview ? ` ${theme.fg("muted", entry.preview)}` : "";
|
|
77
|
-
return clip(`${head}${duration}${preview}`, Math.max(1, width - CHAT_GUTTER));
|
|
78
|
-
}
|
package/tools/parts.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/** 工具行的着色片段:路径、命令、大小、耗时、diff 统计。 */
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
-
import { type ClipSide, clip, formatDuration, oneLine } from "../format.js";
|
|
6
|
-
import { sizeColor } from "../theme.js";
|
|
7
|
-
|
|
8
|
-
/** pi 公共出口未导出 ThemeBg:从 Theme.bg 的参数派生,避免依赖内部路径。 */
|
|
9
|
-
type ThemeBg = Parameters<Theme["bg"]>[0];
|
|
10
|
-
|
|
11
|
-
const HOME = homedir();
|
|
12
|
-
const ELLIPSIS = "…";
|
|
13
|
-
/** 结果小于此字符数不显示大小列(噪音阈值) */
|
|
14
|
-
const SIZE_MIN_CHARS = 100;
|
|
15
|
-
/** 快过此耗时不值得占一列 */
|
|
16
|
-
const DURATION_MIN_MS = 1_000;
|
|
17
|
-
|
|
18
|
-
/** 一段带颜色的文本片段;不给 color 则用终端默认前景色。 */
|
|
19
|
-
export type Part = { text: string; color?: ThemeColor; bg?: ThemeBg; bold?: boolean };
|
|
20
|
-
|
|
21
|
-
export function partsWidth(parts: Part[]): number {
|
|
22
|
-
return parts.reduce((total, part) => total + visibleWidth(part.text), 0);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function paint(theme: Theme, parts: Part[]): string {
|
|
26
|
-
return parts
|
|
27
|
-
.map((part) => {
|
|
28
|
-
let text = part.bold ? theme.bold(part.text) : part.text;
|
|
29
|
-
if (part.color) text = theme.fg(part.color, text);
|
|
30
|
-
if (part.bg) text = theme.bg(part.bg, text);
|
|
31
|
-
return text;
|
|
32
|
-
})
|
|
33
|
-
.join("");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** 按方向截断片段序列:start 保留尾部片段(basename 优先),end 保留头部。 */
|
|
37
|
-
export function clipParts(parts: Part[], width: number, from: ClipSide): Part[] {
|
|
38
|
-
if (partsWidth(parts) <= width) return parts;
|
|
39
|
-
const ordered = from === "start" ? [...parts].reverse() : parts;
|
|
40
|
-
const kept: Part[] = [];
|
|
41
|
-
let used = 0;
|
|
42
|
-
for (const part of ordered) {
|
|
43
|
-
const partWidth = visibleWidth(part.text);
|
|
44
|
-
if (used + partWidth <= width) {
|
|
45
|
-
kept.push(part);
|
|
46
|
-
used += partWidth;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
const room = width - used;
|
|
50
|
-
if (room >= 1) kept.push({ ...part, text: clip(part.text, room, from) });
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
return from === "start" ? kept.reverse() : kept;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** 路径显示:cwd 内 → ./,家目录 → ~,目录前缀暗色、basename 亮色。 */
|
|
57
|
-
export function pathValue(path: string, cwd: string, suffix = ""): Part[] {
|
|
58
|
-
let display = path || ELLIPSIS;
|
|
59
|
-
if (cwd && display.startsWith(`${cwd}/`)) display = `./${display.slice(cwd.length + 1)}`;
|
|
60
|
-
else if (HOME && display.startsWith(HOME)) display = `~${display.slice(HOME.length)}`;
|
|
61
|
-
const cut = display.lastIndexOf("/") + 1;
|
|
62
|
-
const parts: Part[] = [];
|
|
63
|
-
if (cut > 0) parts.push({ text: display.slice(0, cut), color: "muted" });
|
|
64
|
-
parts.push({ text: display.slice(cut), color: "accent" });
|
|
65
|
-
if (suffix) parts.push({ text: suffix, color: "muted" });
|
|
66
|
-
return parts;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const CONNECTOR = /^(&&|\|\||\||;|↵)$/;
|
|
70
|
-
const REDIRECT = /^\d*(>>?|<)/;
|
|
71
|
-
const ENV_ASSIGN = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
72
|
-
|
|
73
|
-
function appendPart(parts: Part[], text: string, color?: ThemeColor): void {
|
|
74
|
-
const last = parts[parts.length - 1];
|
|
75
|
-
if (last && last.color === color) last.text += text;
|
|
76
|
-
else parts.push({ text, color });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** 命令分层着色:命令词 accent,参数默认色,管道/重定向/连接符/环境赋值暗色。 */
|
|
80
|
-
export function commandParts(command: string): Part[] {
|
|
81
|
-
const display = oneLine((command || "").replace(/\r?\n/g, " ↵ ")).replaceAll(HOME, "~");
|
|
82
|
-
if (!display) return [{ text: "$ ", color: "muted" }, { text: ELLIPSIS, color: "accent" }];
|
|
83
|
-
const parts: Part[] = [{ text: "$ ", color: "muted" }];
|
|
84
|
-
let expectCommand = true;
|
|
85
|
-
for (const token of display.split(" ")) {
|
|
86
|
-
let color: ThemeColor | undefined;
|
|
87
|
-
if (CONNECTOR.test(token)) {
|
|
88
|
-
color = "muted";
|
|
89
|
-
expectCommand = true;
|
|
90
|
-
} else if (REDIRECT.test(token)) {
|
|
91
|
-
color = "muted";
|
|
92
|
-
} else if (expectCommand && ENV_ASSIGN.test(token)) {
|
|
93
|
-
color = "muted";
|
|
94
|
-
} else if (expectCommand) {
|
|
95
|
-
color = "accent";
|
|
96
|
-
expectCommand = false;
|
|
97
|
-
}
|
|
98
|
-
if (token.endsWith(";")) expectCommand = true;
|
|
99
|
-
appendPart(parts, `${token} `, color);
|
|
100
|
-
}
|
|
101
|
-
const last = parts[parts.length - 1];
|
|
102
|
-
last.text = last.text.trimEnd();
|
|
103
|
-
return parts;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function sizePart(chars: number | undefined): Part | undefined {
|
|
107
|
-
if (chars === undefined || chars < SIZE_MIN_CHARS) return undefined;
|
|
108
|
-
return { text: `${(chars / 1000).toFixed(1)}k`, color: sizeColor(chars) };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function durationPart(durationMs: number | undefined): Part | undefined {
|
|
112
|
-
if (durationMs === undefined || durationMs < DURATION_MIN_MS) return undefined;
|
|
113
|
-
return { text: formatDuration(durationMs), color: "dim" };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function diffMeta(diff: string): Part[] {
|
|
117
|
-
let added = 0;
|
|
118
|
-
let removed = 0;
|
|
119
|
-
for (const line of diff.split("\n")) {
|
|
120
|
-
if (line.startsWith("+")) added++;
|
|
121
|
-
else if (line.startsWith("-")) removed++;
|
|
122
|
-
}
|
|
123
|
-
const parts: Part[] = [];
|
|
124
|
-
if (added) parts.push({ text: ` +${added}`, color: "toolDiffAdded" });
|
|
125
|
-
if (removed) parts.push({ text: ` -${removed}`, color: "toolDiffRemoved" });
|
|
126
|
-
return parts;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function genericArgsParts(args: unknown): Part[] {
|
|
130
|
-
if (!args || typeof args !== "object") {
|
|
131
|
-
return [{ text: String(args ?? ""), color: "accent" }];
|
|
132
|
-
}
|
|
133
|
-
const entries = Object.entries(args as Record<string, unknown>);
|
|
134
|
-
if (entries.length === 0) return [];
|
|
135
|
-
const preferredKey = ["prompt", "task", "query", "path", "command", "name", "message", "url"]
|
|
136
|
-
.find((k) => k in (args as object));
|
|
137
|
-
const [key, value] = preferredKey
|
|
138
|
-
? [preferredKey, (args as Record<string, unknown>)[preferredKey]]
|
|
139
|
-
: entries[0];
|
|
140
|
-
const valStr = typeof value === "string" ? value : JSON.stringify(value);
|
|
141
|
-
const text = oneLine(valStr.length > 60 ? `${valStr.slice(0, 57)}…` : valStr);
|
|
142
|
-
return [{ text: `${key}: `, color: "muted" }, { text, color: "accent" }];
|
|
143
|
-
}
|