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.
- 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 +18 -13
- 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/tools/host.ts
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主适配:读宿主组件私有字段、改宿主原型、找聊天容器——全插件只在这一个文件做。
|
|
3
|
+
* 宿主没有聊天投影接缝,这些都是对实现细节的依赖;升级 pi 时只审这一个文件。
|
|
4
|
+
* 每个读取点都校验形状:字段改名或换型时抛 HostShapeError,分组安装与渲染据此整体退回原生显示并明确提示,
|
|
5
|
+
* 不悄悄画错。形状只对首批真实实例自检:构造假实例探测要 TUI 引用且有副作用。向上游要投影钩子是长期方向。
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
8
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
9
|
+
import {
|
|
10
|
+
AssistantMessageComponent,
|
|
11
|
+
CustomMessageComponent,
|
|
12
|
+
getMarkdownTheme,
|
|
13
|
+
type CustomEntry,
|
|
14
|
+
type EntryRenderer,
|
|
15
|
+
type Theme,
|
|
16
|
+
ToolExecutionComponent,
|
|
17
|
+
UserMessageComponent,
|
|
18
|
+
type AgentSessionEvent,
|
|
19
|
+
type ExtensionUIContext,
|
|
20
|
+
type MessageRenderer,
|
|
21
|
+
type ToolDefinition,
|
|
22
|
+
} from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { stripVTControlCharacters } from "node:util";
|
|
24
|
+
import { Container, ScrollView, Spacer, Text, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
25
|
+
import type { RowState, ToolResult } from "./line.js";
|
|
26
|
+
|
|
27
|
+
export class HostShapeError extends Error {
|
|
28
|
+
constructor(where: string) {
|
|
29
|
+
super(`过程分组已停用:宿主组件形状变了(${where}),已保持原生显示;请升级 FireCode`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 宿主工具行:结果与单工具展开仍归宿主所有,这里只读。 */
|
|
34
|
+
export type ToolRow = ToolExecutionComponent;
|
|
35
|
+
export interface ToolFacts {
|
|
36
|
+
toolName: string;
|
|
37
|
+
toolCallId: string;
|
|
38
|
+
args: unknown;
|
|
39
|
+
cwd: string;
|
|
40
|
+
expanded: boolean;
|
|
41
|
+
isPartial: boolean;
|
|
42
|
+
rendererState: RowState;
|
|
43
|
+
toolDefinition?: { label?: string };
|
|
44
|
+
callRendererComponent?: Component;
|
|
45
|
+
result?: ToolResult & { isError: boolean };
|
|
46
|
+
ui: TUI;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type Shape = Record<string, "string" | "boolean" | "object">;
|
|
50
|
+
const TOOL_SHAPE: Shape = { toolName: "string", toolCallId: "string", cwd: "string", expanded: "boolean", isPartial: "boolean", rendererState: "object", ui: "object" };
|
|
51
|
+
const ASSISTANT_SHAPE: Shape = { contentContainer: "object", isStreaming: "boolean" };
|
|
52
|
+
|
|
53
|
+
function checked<T>(value: object, shape: Shape, where: string): T {
|
|
54
|
+
for (const [field, type] of Object.entries(shape)) {
|
|
55
|
+
const actual = (value as Record<string, unknown>)[field];
|
|
56
|
+
if (typeof actual !== type || actual === null) throw new HostShapeError(`${where}.${field}`);
|
|
57
|
+
}
|
|
58
|
+
return value as T;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 发现钩子只比对工具行归属哪个 TUI,不做形状校验(形状交给发现后的统一自检)。 */
|
|
62
|
+
export function rowUiOf(row: ToolRow): unknown {
|
|
63
|
+
return (row as unknown as { ui?: unknown }).ui;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function toolFacts(row: ToolRow): ToolFacts {
|
|
67
|
+
return checked<ToolFacts>(row, TOOL_SHAPE, "ToolExecutionComponent");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface AssistantFacts {
|
|
71
|
+
contentContainer: Container;
|
|
72
|
+
lastMessage?: AssistantMessage;
|
|
73
|
+
isStreaming: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function assistantFacts(source: AssistantMessageComponent): AssistantFacts {
|
|
77
|
+
const facts = checked<AssistantFacts>(source, ASSISTANT_SHAPE, "AssistantMessageComponent");
|
|
78
|
+
if (!(facts.contentContainer instanceof Container)) throw new HostShapeError("AssistantMessageComponent.contentContainer");
|
|
79
|
+
return facts;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** CustomMessage 的原始消息(content 与扩展给的 details)。 */
|
|
83
|
+
export function customMessageOf(component: CustomMessageComponent): { content: unknown; details?: unknown } {
|
|
84
|
+
const message = (component as unknown as { message?: unknown }).message;
|
|
85
|
+
if (typeof message !== "object" || message === null) throw new HostShapeError("CustomMessageComponent.message");
|
|
86
|
+
return message as { content: unknown; details?: unknown };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function userTextOf(component: UserMessageComponent): string {
|
|
90
|
+
return checked<{ text: string }>(component, { text: "string" }, "UserMessageComponent").text;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 宿主整行单色提示的原文(带颜色转义)。宿主的 ThemedText 首次渲染前 text 还是空串,原文由私有的 build 现算。 */
|
|
94
|
+
export function textComponentText(component: Text): string {
|
|
95
|
+
const build = (component as unknown as { build?: unknown }).build;
|
|
96
|
+
if (typeof build === "function") return String(build.call(component));
|
|
97
|
+
return checked<{ text: string }>(component, { text: "string" }, "Text").text;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 宿主对 ctrl+o 的状态回显;在过程分组里 ctrl+o 就是“全部展开/全部折叠”,这句回显只是噪声。 */
|
|
101
|
+
const TOOL_OUTPUT_ECHO = /^Tool output: (?:expanded|collapsed)$/;
|
|
102
|
+
|
|
103
|
+
export function isToolOutputEcho(component: Component | undefined): boolean {
|
|
104
|
+
return component instanceof Text && TOOL_OUTPUT_ECHO.test(stripVTControlCharacters(textComponentText(component)).trim());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 宿主 CustomEntry(轮记录等):类不对扩展导出,按它独有的 hasContent 能力识别。 */
|
|
108
|
+
export function isEntry(component: Component): boolean {
|
|
109
|
+
return component instanceof Container && typeof (component as unknown as { hasContent?: unknown }).hasContent === "function";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 宿主不给 TUI 句柄:挂一个空 widget 拿到引用后立即撤掉。 */
|
|
113
|
+
export function captureTui(ui: ExtensionUIContext, use: (tui: TUI) => void): void {
|
|
114
|
+
ui.setWidget("firecode-tui-capture", (tui) => {
|
|
115
|
+
use(tui);
|
|
116
|
+
return { render: () => [], invalidate() {} };
|
|
117
|
+
});
|
|
118
|
+
ui.setWidget("firecode-tui-capture", undefined);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 聊天容器 = 直接装着用户消息、助手消息或工具行的容器(宿主只在聊天里构造用户消息组件)。 */
|
|
122
|
+
export function findChat(value: Component): Container | undefined {
|
|
123
|
+
if (!(value instanceof Container)) return undefined;
|
|
124
|
+
if (value.children.some(isChatChild)) return value;
|
|
125
|
+
for (const child of value.children) {
|
|
126
|
+
const found = findChat(child);
|
|
127
|
+
if (found) return found;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function isChatChild(child: Component): boolean {
|
|
133
|
+
return child instanceof ToolExecutionComponent || child instanceof AssistantMessageComponent || child instanceof UserMessageComponent;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 装着聊天容器的滚动视图(全屏模式下跟随末尾的那个);主屏模式没有,返回 undefined。 */
|
|
137
|
+
export function scrollViewOf(root: Component, target: Component): ScrollView | undefined {
|
|
138
|
+
const contains = (node: Component): boolean => node === target || (node instanceof Container && node.children.some(contains));
|
|
139
|
+
const visit = (node: Component): ScrollView | undefined => {
|
|
140
|
+
if (!(node instanceof Container)) return undefined;
|
|
141
|
+
for (const child of node.children) {
|
|
142
|
+
if (!contains(child)) continue;
|
|
143
|
+
return visit(child) ?? (child instanceof ScrollView ? child : undefined);
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
147
|
+
return visit(root);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 滚动视图上次布局的内容总高(宿主私有字段):点击锚定按它把组件内行号换成滚动内容行号。 */
|
|
151
|
+
export function scrollContentHeight(view: ScrollView): number {
|
|
152
|
+
const height = (view as unknown as { contentHeight?: unknown }).contentHeight;
|
|
153
|
+
if (typeof height !== "number") throw new HostShapeError("ScrollView.contentHeight");
|
|
154
|
+
return height;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 原型补丁的安装与精确还原:只还原仍是自己装上的那一层。 */
|
|
158
|
+
export function patchMethod<T extends object, K extends keyof T>(target: T, key: K, replacement: T[K]): () => void {
|
|
159
|
+
const original = target[key];
|
|
160
|
+
if (typeof original !== "function") throw new HostShapeError(`${String(key)} 不是方法`);
|
|
161
|
+
target[key] = replacement;
|
|
162
|
+
return () => {
|
|
163
|
+
if (target[key] === replacement) target[key] = original;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 机器消息卡片(指挥官事件、审查结果卡)的原生展开只由投影决定:主会话的分组补丁压住宿主对它们的全局展开,
|
|
169
|
+
* 只放行投影点开登记过的卡片。主会话与子代理视图同一机制。
|
|
170
|
+
*/
|
|
171
|
+
const openedCards = new WeakSet<Component>();
|
|
172
|
+
|
|
173
|
+
export function openCard(card: CustomMessageComponent): void {
|
|
174
|
+
openedCards.add(card);
|
|
175
|
+
card.setExpanded(true);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function isCardOpened(card: Component): boolean {
|
|
179
|
+
return openedCards.has(card);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 独立的工具行 TUI 句柄:除 requestRender 外全部委托给真实 TUI。主会话的分组补丁按“工具行属于主 TUI”识别自己的行,
|
|
184
|
+
* 不在主聊天里的投影(子代理全过程视图)用它建工具行,单工具展开与重绘都归调用方。
|
|
185
|
+
*/
|
|
186
|
+
export function detachedTui(tui: TUI, requestRender: () => void): TUI {
|
|
187
|
+
return Object.create(tui, { requestRender: { value: requestRender } }) as TUI;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 镜像建组件要的外部来源:工具行的 TUI 句柄与工作目录、工具渲染定义、自定义消息与自定义记录的渲染器、主题。 */
|
|
191
|
+
export interface MirrorSources {
|
|
192
|
+
ui: TUI;
|
|
193
|
+
cwd: string;
|
|
194
|
+
theme: Theme;
|
|
195
|
+
toolDefinition(name: string): ToolDefinition | undefined;
|
|
196
|
+
messageRenderer(customType: string): MessageRenderer | undefined;
|
|
197
|
+
entryRenderer(customType: string): EntryRenderer | undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 会话分支里镜像认得的条目(宿主 SessionEntry 的结构子集)。 */
|
|
201
|
+
export type MirrorEntry =
|
|
202
|
+
| { type: "message"; message: AgentMessage }
|
|
203
|
+
| { type: "custom_message"; customType: string; content: unknown; display: boolean; details?: unknown; timestamp: string }
|
|
204
|
+
| { type: "custom"; customType: string; data?: unknown; timestamp: string }
|
|
205
|
+
| { type: string };
|
|
206
|
+
|
|
207
|
+
function customMessage(entry: { customType: string; content: unknown; details?: unknown; timestamp: string }) {
|
|
208
|
+
return {
|
|
209
|
+
role: "custom", customType: entry.customType, content: entry.content, display: true,
|
|
210
|
+
details: entry.details, timestamp: Date.parse(entry.timestamp),
|
|
211
|
+
} as AgentMessage;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 宿主 CustomEntryComponent 的壳(类不导出):Container 带 hasContent 能力,里面放一个零行标记;isEntry 按同一能力识别。 */
|
|
215
|
+
function entryShell(marker: Component): Container {
|
|
216
|
+
const shell = new Container();
|
|
217
|
+
shell.addChild(new Spacer(1));
|
|
218
|
+
shell.addChild(marker);
|
|
219
|
+
(shell as unknown as { hasContent: () => boolean }).hasContent = () => true;
|
|
220
|
+
return shell;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const ABORTED_TEXT = "Operation aborted";
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 宿主把会话消息与运行事件变成聊天组件的逻辑(interactive-mode 的 addMessageToChat、renderSessionItems 与事件分支)是私有的;
|
|
227
|
+
* 子代理全过程视图要同一套组件喂给过程组投影,这里是它的最小镜像:user / assistant(含 toolCall 工具行)/ toolResult / custom,
|
|
228
|
+
* 历史回放与运行中事件两条入口。每建一个助手或工具组件都过形状自检,宿主改了就抛 HostShapeError,由调用方整体提示,不画错。
|
|
229
|
+
* 不镜像的(bashExecution、压缩与分支摘要、技能块、缓存提示)在子代理会话里不出现或无关。
|
|
230
|
+
*/
|
|
231
|
+
export class ChatMirror {
|
|
232
|
+
readonly chat = new Container();
|
|
233
|
+
private readonly pending = new Map<string, ToolExecutionComponent>();
|
|
234
|
+
private streaming: AssistantMessageComponent | undefined;
|
|
235
|
+
|
|
236
|
+
constructor(private readonly sources: MirrorSources) {}
|
|
237
|
+
|
|
238
|
+
/** 会话分支里的一个条目(历史回放):消息、显示的自定义消息与自定义记录。 */
|
|
239
|
+
replay(entry: MirrorEntry): void {
|
|
240
|
+
if (entry.type === "message" && "message" in entry) this.replayMessage(entry.message);
|
|
241
|
+
else if (entry.type === "custom_message" && "display" in entry) {
|
|
242
|
+
if (entry.display) this.add(customMessage(entry));
|
|
243
|
+
} else if (entry.type === "custom" && "customType" in entry) this.addEntry(entry);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private replayMessage(message: AgentMessage): void {
|
|
247
|
+
if (message.role === "assistant") {
|
|
248
|
+
this.chat.addChild(this.assistant(message));
|
|
249
|
+
for (const call of message.content) {
|
|
250
|
+
if (call.type !== "toolCall") continue;
|
|
251
|
+
const row = this.toolRow(call.name, call.id, call.arguments);
|
|
252
|
+
if (message.stopReason === "aborted" || message.stopReason === "error")
|
|
253
|
+
row.updateResult({ content: [{ type: "text", text: message.stopReason === "aborted" ? ABORTED_TEXT : message.errorMessage || "Error" }], isError: true });
|
|
254
|
+
else this.pending.set(call.id, row);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (message.role === "toolResult") {
|
|
259
|
+
this.pending.get(message.toolCallId)?.updateResult(message);
|
|
260
|
+
this.pending.delete(message.toolCallId);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
this.add(message);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** 运行中的会话事件;返回是否改了聊天树或组件(调用方据此重绘)。 */
|
|
267
|
+
handle(event: AgentSessionEvent): boolean {
|
|
268
|
+
switch (event.type) {
|
|
269
|
+
case "message_start":
|
|
270
|
+
if (event.message.role === "assistant") {
|
|
271
|
+
this.streaming = this.assistant(event.message, true);
|
|
272
|
+
this.chat.addChild(this.streaming);
|
|
273
|
+
} else this.add(event.message);
|
|
274
|
+
return true;
|
|
275
|
+
case "message_update":
|
|
276
|
+
if (event.message.role !== "assistant") return false;
|
|
277
|
+
// 回合中途打开视图时错过了这条消息的 message_start:在第一次更新时补建。
|
|
278
|
+
if (!this.streaming) {
|
|
279
|
+
this.streaming = this.assistant(event.message, true);
|
|
280
|
+
this.chat.addChild(this.streaming);
|
|
281
|
+
}
|
|
282
|
+
this.streaming.updateContent(event.message, true);
|
|
283
|
+
for (const call of event.message.content) {
|
|
284
|
+
if (call.type !== "toolCall") continue;
|
|
285
|
+
const row = this.pending.get(call.id);
|
|
286
|
+
if (row) row.updateArgs(call.arguments);
|
|
287
|
+
else this.pending.set(call.id, this.toolRow(call.name, call.id, call.arguments));
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
case "message_end":
|
|
291
|
+
if (!this.streaming || event.message.role !== "assistant") return false;
|
|
292
|
+
this.streaming.updateContent(event.message, false);
|
|
293
|
+
if (event.message.stopReason === "aborted" || event.message.stopReason === "error") {
|
|
294
|
+
const text = event.message.stopReason === "aborted" ? ABORTED_TEXT : event.message.errorMessage || "Error";
|
|
295
|
+
for (const row of this.pending.values()) row.updateResult({ content: [{ type: "text", text }], isError: true });
|
|
296
|
+
this.pending.clear();
|
|
297
|
+
} else for (const row of this.pending.values()) row.setArgsComplete();
|
|
298
|
+
this.streaming = undefined;
|
|
299
|
+
return true;
|
|
300
|
+
case "tool_execution_start": {
|
|
301
|
+
if (event.parentToolCallId) return false;
|
|
302
|
+
const row = this.pending.get(event.toolCallId) ?? this.toolRow(event.toolName, event.toolCallId, event.args);
|
|
303
|
+
this.pending.set(event.toolCallId, row);
|
|
304
|
+
row.markExecutionStarted();
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
case "tool_execution_update":
|
|
308
|
+
this.pending.get(event.toolCallId)?.updateResult({ ...event.partialResult, isError: false }, true);
|
|
309
|
+
return this.pending.has(event.toolCallId);
|
|
310
|
+
case "tool_execution_end": {
|
|
311
|
+
const row = this.pending.get(event.toolCallId);
|
|
312
|
+
row?.updateResult({ ...event.result, isError: event.isError });
|
|
313
|
+
this.pending.delete(event.toolCallId);
|
|
314
|
+
return row !== undefined;
|
|
315
|
+
}
|
|
316
|
+
case "agent_end":
|
|
317
|
+
if (this.streaming) this.chat.removeChild(this.streaming);
|
|
318
|
+
this.streaming = undefined;
|
|
319
|
+
this.pending.clear();
|
|
320
|
+
return true;
|
|
321
|
+
case "entry_appended":
|
|
322
|
+
if (event.entry.type === "custom") return this.addEntry(event.entry);
|
|
323
|
+
if (event.entry.type !== "custom_message" || !event.entry.display) return false;
|
|
324
|
+
this.add(customMessage(event.entry));
|
|
325
|
+
return true;
|
|
326
|
+
default:
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** 自定义记录(如轮记录):有渲染器的以宿主 CustomEntry 的同一形态放进聊天树,没有的不显示(与宿主一致)。 */
|
|
332
|
+
private addEntry(entry: { customType: string; data?: unknown; timestamp: string }): boolean {
|
|
333
|
+
const renderer = this.sources.entryRenderer(entry.customType);
|
|
334
|
+
const component = renderer?.(entry as CustomEntry, { expanded: false }, this.sources.theme);
|
|
335
|
+
if (!component) return false;
|
|
336
|
+
this.chat.addChild(entryShell(component));
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private add(message: AgentMessage): void {
|
|
341
|
+
if (message.role === "custom") {
|
|
342
|
+
const custom = message as ConstructorParameters<typeof CustomMessageComponent>[0];
|
|
343
|
+
if (!custom.display) return;
|
|
344
|
+
this.chat.addChild(new CustomMessageComponent(custom, this.sources.messageRenderer(custom.customType), getMarkdownTheme()));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (message.role !== "user") return;
|
|
348
|
+
const text = typeof message.content === "string"
|
|
349
|
+
? message.content
|
|
350
|
+
: message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("");
|
|
351
|
+
if (!text) return;
|
|
352
|
+
if (this.chat.children.length) this.chat.addChild(new Spacer(1));
|
|
353
|
+
this.chat.addChild(new UserMessageComponent(text, getMarkdownTheme()));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private assistant(message: AssistantMessage, streaming = false): AssistantMessageComponent {
|
|
357
|
+
const component = new AssistantMessageComponent(message, true, getMarkdownTheme());
|
|
358
|
+
if (streaming) component.updateContent(message, true);
|
|
359
|
+
assistantFacts(component);
|
|
360
|
+
return component;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private toolRow(name: string, id: string, args: unknown): ToolExecutionComponent {
|
|
364
|
+
const row = new ToolExecutionComponent(name, id, args, {}, this.sources.toolDefinition(name), this.sources.ui, this.sources.cwd);
|
|
365
|
+
toolFacts(row);
|
|
366
|
+
this.chat.addChild(row);
|
|
367
|
+
return row;
|
|
368
|
+
}
|
|
369
|
+
}
|
package/tools/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 接管默认 4 工具(read/bash/edit/write
|
|
2
|
+
* 接管默认 4 工具(read/bash/edit/write)的展示:组摘要/紧凑列表 + 单工具正文,保留耗时/大小列。
|
|
3
3
|
*
|
|
4
4
|
* 只包装默认激活的工具:原版 pi 的 registerTool 是注册即激活(会话构建与 reload
|
|
5
5
|
* 固定 includeAllExtensionTools),给 grep/find/ls 挂渲染包装会把它们在所有会话
|
|
@@ -10,21 +10,17 @@ import {
|
|
|
10
10
|
createEditTool,
|
|
11
11
|
createReadTool,
|
|
12
12
|
createWriteTool,
|
|
13
|
+
defineTool,
|
|
13
14
|
type ExtensionAPI,
|
|
14
15
|
} from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import {
|
|
16
|
+
import { watchBusy } from "../busy.js";
|
|
17
|
+
import { installGroupPatch } from "./grouping.js";
|
|
16
18
|
import { ToolLine, makeResultRenderer } from "./line.js";
|
|
17
|
-
import {
|
|
19
|
+
import { LABEL, toolTarget } from "./actions.js";
|
|
20
|
+
import { diffMeta } from "./parts.js";
|
|
21
|
+
import { ROUND_ENTRY, renderRound } from "./round.js";
|
|
18
22
|
import { clearDurations, executeTimed } from "./timing.js";
|
|
19
|
-
|
|
20
|
-
const LABEL = { read: "读取", bash: "操作", edit: "修改", write: "写入" } as const;
|
|
21
|
-
|
|
22
|
-
type ToolArgs = {
|
|
23
|
-
path?: string;
|
|
24
|
-
file_path?: string;
|
|
25
|
-
offset?: number;
|
|
26
|
-
limit?: number;
|
|
27
|
-
};
|
|
23
|
+
import { TurnClock } from "./turn-clock.js";
|
|
28
24
|
|
|
29
25
|
type ToolMap = {
|
|
30
26
|
read: ReturnType<typeof createReadTool>;
|
|
@@ -54,16 +50,6 @@ function tools(cwd: string): ToolMap {
|
|
|
54
50
|
return value;
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
const argPath = (args: ToolArgs): string => args?.file_path ?? args?.path ?? "";
|
|
58
|
-
|
|
59
|
-
/** read 的 offset/limit → `:12-40`、`:12+`。 */
|
|
60
|
-
function rangeSuffix(args: ToolArgs): string {
|
|
61
|
-
if (args.offset === undefined && args.limit === undefined) return "";
|
|
62
|
-
const start = args.offset ?? 1;
|
|
63
|
-
if (args.limit === undefined) return `:${start}+`;
|
|
64
|
-
return `:${start}-${start + args.limit - 1}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
53
|
function lineCount(text: string): number {
|
|
68
54
|
if (text === "") return 0;
|
|
69
55
|
const lines = text.split("\n").length;
|
|
@@ -77,96 +63,113 @@ function invoke<T extends (...args: never[]) => unknown>(
|
|
|
77
63
|
return Reflect.apply(execute, undefined, args) as ReturnType<T>;
|
|
78
64
|
}
|
|
79
65
|
|
|
80
|
-
|
|
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() {
|
|
81
77
|
const initial = tools(process.cwd());
|
|
82
|
-
|
|
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
|
+
}
|
|
83
150
|
|
|
84
|
-
|
|
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?.();
|
|
85
161
|
clearDurations();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
installGroupPatch(ctx.ui, decorated);
|
|
162
|
+
dispose = installGroupPatch(ctx.ui, { clock });
|
|
163
|
+
ctx.ui.setToolsExpanded(false);
|
|
90
164
|
});
|
|
91
|
-
pi.on("session_shutdown",
|
|
165
|
+
pi.on("session_shutdown", () => {
|
|
166
|
+
if (!dispose) return;
|
|
167
|
+
dispose();
|
|
168
|
+
dispose = undefined;
|
|
92
169
|
clearDurations();
|
|
93
|
-
uninstallGroupPatch();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
pi.registerTool({
|
|
97
|
-
...initial.read,
|
|
98
|
-
label: LABEL.read,
|
|
99
|
-
renderShell: "self",
|
|
100
|
-
execute: (id, params, signal, update, ctx) =>
|
|
101
|
-
executeTimed(id, () => invoke(tools(ctx.cwd).read.execute, [id, params, signal, update, ctx])),
|
|
102
|
-
renderCall: (args, theme, ctx) =>
|
|
103
|
-
new ToolLine({
|
|
104
|
-
label: LABEL.read,
|
|
105
|
-
value: pathValue(argPath(args), ctx.cwd, rangeSuffix(args)),
|
|
106
|
-
clip: "start",
|
|
107
|
-
theme,
|
|
108
|
-
ctx,
|
|
109
|
-
}),
|
|
110
|
-
renderResult: makeResultRenderer(true),
|
|
111
170
|
});
|
|
112
171
|
|
|
113
|
-
pi.registerTool(
|
|
114
|
-
...initial.bash,
|
|
115
|
-
label: LABEL.bash,
|
|
116
|
-
renderShell: "self",
|
|
117
|
-
execute: (id, params, signal, update, ctx) =>
|
|
118
|
-
executeTimed(id, () => invoke(tools(ctx.cwd).bash.execute, [id, params, signal, update, ctx])),
|
|
119
|
-
renderCall: (args, theme, ctx) =>
|
|
120
|
-
new ToolLine({
|
|
121
|
-
label: LABEL.bash,
|
|
122
|
-
value: commandParts(args.command),
|
|
123
|
-
clip: "end",
|
|
124
|
-
theme,
|
|
125
|
-
ctx,
|
|
126
|
-
}),
|
|
127
|
-
renderResult: makeResultRenderer(true),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const editResult = makeResultRenderer(false);
|
|
131
|
-
pi.registerTool({
|
|
132
|
-
...initial.edit,
|
|
133
|
-
label: LABEL.edit,
|
|
134
|
-
renderShell: "self",
|
|
135
|
-
execute: (id, params, signal, update, ctx) =>
|
|
136
|
-
executeTimed(id, () => invoke(tools(ctx.cwd).edit.execute, [id, params, signal, update, ctx])),
|
|
137
|
-
renderCall: (args, theme, ctx) =>
|
|
138
|
-
new ToolLine({
|
|
139
|
-
label: LABEL.edit,
|
|
140
|
-
value: pathValue(argPath(args), ctx.cwd),
|
|
141
|
-
clip: "start",
|
|
142
|
-
theme,
|
|
143
|
-
ctx,
|
|
144
|
-
}),
|
|
145
|
-
renderResult(result, options, theme, ctx) {
|
|
146
|
-
const details = result.details as { diff?: unknown } | undefined;
|
|
147
|
-
const diff = !ctx.isError && typeof details?.diff === "string" ? details.diff : undefined;
|
|
148
|
-
ctx.state.meta = diff ? diffMeta(diff) : undefined;
|
|
149
|
-
return editResult(result, options, theme, ctx);
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
pi.registerTool({
|
|
154
|
-
...initial.write,
|
|
155
|
-
label: LABEL.write,
|
|
156
|
-
renderShell: "self",
|
|
157
|
-
execute: (id, params, signal, update, ctx) =>
|
|
158
|
-
executeTimed(id, () => invoke(tools(ctx.cwd).write.execute, [id, params, signal, update, ctx])),
|
|
159
|
-
renderCall: (args, theme, ctx) =>
|
|
160
|
-
new ToolLine({
|
|
161
|
-
label: LABEL.write,
|
|
162
|
-
value: pathValue(argPath(args), ctx.cwd),
|
|
163
|
-
clip: "start",
|
|
164
|
-
meta: [{ text: ` +${lineCount(args.content ?? "")}`, color: "toolDiffAdded" }],
|
|
165
|
-
theme,
|
|
166
|
-
ctx,
|
|
167
|
-
}),
|
|
168
|
-
renderResult: makeResultRenderer(false),
|
|
169
|
-
});
|
|
172
|
+
for (const definition of Object.values(toolDefinitions())) pi.registerTool(definition);
|
|
170
173
|
|
|
171
174
|
pi.registerCommand("tool-status", {
|
|
172
175
|
description: "显示当前已加载/启用工具",
|