pi-firecode 1.0.0 → 1.0.2

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 (93) hide show
  1. package/README.md +59 -41
  2. package/README.zh-CN.md +78 -0
  3. package/config.example.jsonc +21 -22
  4. package/dist/config.example.jsonc +90 -0
  5. package/dist/index.js +11443 -0
  6. package/dist/master/prompts/master.en.md +22 -0
  7. package/{master → dist/master}/prompts/master.zh.md +3 -3
  8. package/dist/master/prompts/worker.en.md +1 -0
  9. package/dist/watcher/prompts/watch.en.md +43 -0
  10. package/package.json +10 -27
  11. package/activity.ts +0 -92
  12. package/busy.ts +0 -188
  13. package/config.ts +0 -500
  14. package/deliver.ts +0 -119
  15. package/flame.ts +0 -149
  16. package/format.ts +0 -133
  17. package/header.ts +0 -218
  18. package/herdr-client.ts +0 -60
  19. package/index.ts +0 -68
  20. package/jsonc.ts +0 -34
  21. package/master/actions.ts +0 -293
  22. package/master/activity-list.ts +0 -281
  23. package/master/event-card.ts +0 -87
  24. package/master/event-format.ts +0 -83
  25. package/master/guard.ts +0 -46
  26. package/master/index.ts +0 -252
  27. package/master/list-view.ts +0 -129
  28. package/master/outbox.ts +0 -188
  29. package/master/prompt.ts +0 -26
  30. package/master/role.ts +0 -18
  31. package/master/run.ts +0 -315
  32. package/master/runtime.ts +0 -259
  33. package/master/spawn.ts +0 -234
  34. package/master/state.ts +0 -225
  35. package/master/worker-view.ts +0 -517
  36. package/provider/claude-sub.ts +0 -151
  37. package/provider/openai-native/index.ts +0 -8
  38. package/provider/openai-native/src/compact-client.ts +0 -350
  39. package/provider/openai-native/src/config.ts +0 -297
  40. package/provider/openai-native/src/extension.ts +0 -93
  41. package/provider/openai-native/src/native-compaction.ts +0 -151
  42. package/provider/openai-native/src/native-details.ts +0 -157
  43. package/provider/openai-native/src/native-replay.ts +0 -253
  44. package/provider/openai-native/src/native-runtime.ts +0 -144
  45. package/provider/openai-native/src/options.ts +0 -87
  46. package/provider/openai-native/src/request-pipeline.ts +0 -21
  47. package/provider/openai-native/src/responses-input.ts +0 -433
  48. package/review/advisor.ts +0 -110
  49. package/review/card.ts +0 -398
  50. package/review/checkpoint.ts +0 -378
  51. package/review/evidence.ts +0 -230
  52. package/review/index.ts +0 -992
  53. package/review/occupancy.ts +0 -25
  54. package/review/outcome.ts +0 -98
  55. package/review/prompt.ts +0 -209
  56. package/review/reviewer.ts +0 -353
  57. package/review/session.ts +0 -110
  58. package/review/state.ts +0 -877
  59. package/review/ui.ts +0 -89
  60. package/round-recorder.ts +0 -18
  61. package/session/herdr-display.ts +0 -87
  62. package/session/presets.ts +0 -300
  63. package/session/quota.ts +0 -123
  64. package/session/rename.ts +0 -46
  65. package/session/stats.ts +0 -313
  66. package/statusbar/index.ts +0 -256
  67. package/statusbar/render.ts +0 -113
  68. package/theme.ts +0 -45
  69. package/tools/actions.ts +0 -53
  70. package/tools/assistant-view.ts +0 -64
  71. package/tools/click-anchor.ts +0 -56
  72. package/tools/group-view.ts +0 -420
  73. package/tools/grouping.ts +0 -158
  74. package/tools/host.ts +0 -369
  75. package/tools/index.ts +0 -186
  76. package/tools/line.ts +0 -231
  77. package/tools/machine.ts +0 -78
  78. package/tools/parts.ts +0 -143
  79. package/tools/round.ts +0 -95
  80. package/tools/timing.ts +0 -22
  81. package/tools/turn-clock.ts +0 -41
  82. package/tools/turn-summary.ts +0 -109
  83. package/truncated-write.ts +0 -18
  84. package/watcher/card.ts +0 -91
  85. package/watcher/index.ts +0 -176
  86. package/watcher/observer.ts +0 -75
  87. package/watcher/transcript.ts +0 -56
  88. /package/{master → dist/master}/prompts/worker.zh.md +0 -0
  89. /package/{review → dist/review}/prompts/advisor.en.md +0 -0
  90. /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
  91. /package/{review → dist/review}/prompts/review.en.md +0 -0
  92. /package/{review → dist/review}/prompts/review.zh.md +0 -0
  93. /package/{watcher → dist/watcher}/prompts/watch.zh.md +0 -0
package/tools/grouping.ts DELETED
@@ -1,158 +0,0 @@
1
- /** 过程分组的安装:原始聊天树不变,渲染与鼠标命中共用同一份投影;宿主私有细节全部经 host.ts。 */
2
- import { AssistantMessageComponent, CustomMessageComponent, ToolExecutionComponent, UserMessageComponent, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
3
- import { Container, type Component, type TUI } from "@earendil-works/pi-tui";
4
- import { onFrame } from "../flame.js";
5
- import { ClickAnchor } from "./click-anchor.js";
6
- import { isMachineMessage, projectProcessGroups, toggleToolDetails, type ProjectionEnv } from "./group-view.js";
7
- import { assistantFacts, captureTui, findChat, HostShapeError, isCardOpened, patchMethod, rowUiOf, scrollContentHeight, scrollViewOf, toolFacts } from "./host.js";
8
- import type { TurnClock } from "./turn-clock.js";
9
-
10
- const OWNER = Symbol.for("pi.firecode.tool-groups");
11
- const runtime = globalThis as typeof globalThis & { [OWNER]?: () => void };
12
-
13
- export interface GroupOptions {
14
- clock: TurnClock;
15
- }
16
-
17
- export function installGroupPatch(ui: ExtensionUIContext, options: GroupOptions): () => void {
18
- runtime[OWNER]?.();
19
- let detach = () => {};
20
- captureTui(ui, (tui) => {
21
- detach = attach(tui, ui, options);
22
- });
23
- const dispose = () => {
24
- if (runtime[OWNER] !== dispose) return;
25
- detach();
26
- delete runtime[OWNER];
27
- };
28
- runtime[OWNER] = dispose;
29
- return dispose;
30
- }
31
-
32
- /** 首个真实实例的形状自检:私有字段改名或换型就不安装。 */
33
- function checkShape(child: Component): void {
34
- if (child instanceof ToolExecutionComponent) toolFacts(child);
35
- if (child instanceof AssistantMessageComponent) assistantFacts(child);
36
- }
37
-
38
- function attach(tui: TUI, ui: ExtensionUIContext, options: GroupOptions): () => void {
39
- const originalExpand = ToolExecutionComponent.prototype.setExpanded;
40
- const originalMessageExpand = CustomMessageComponent.prototype.setExpanded;
41
- const originalAdd = Container.prototype.addChild;
42
- const restores: (() => void)[] = [];
43
- let stopFrames: (() => void) | undefined;
44
- let attached = false;
45
- const detach = () => {
46
- stopFrames?.();
47
- stopFrames = undefined;
48
- for (const restore of restores.splice(0).reverse()) restore();
49
- };
50
- let abandoned = false;
51
- /** 自检、渲染、点击或全局展开时发现宿主形状不符:整体退回原生显示并明确提示一次,不悄悄画错。 */
52
- const abandon = (error: HostShapeError) => {
53
- detach();
54
- if (abandoned) return;
55
- abandoned = true;
56
- ui.notify(error.message, "warning");
57
- };
58
- /** 宿主调进来的每个入口都经这里:形状不符就退回原生,再按原生行为完成这次调用。 */
59
- const guarded = <T>(run: () => T, native: () => T): T => {
60
- try {
61
- return run();
62
- } catch (error) {
63
- if (!(error instanceof HostShapeError)) throw error;
64
- abandon(error);
65
- return native();
66
- }
67
- };
68
- // 归属只比对 TUI 引用,不校验别的 TUI 的工具行形状。
69
- const belongsHere = (row: ToolExecutionComponent) => rowUiOf(row) === tui;
70
- // 全局展开只控制组摘要/列表;单工具正文通过下方独立的鼠标入口调用原方法。
71
- restores.push(patchMethod(ToolExecutionComponent.prototype, "setExpanded", function (this: ToolExecutionComponent, value) {
72
- originalExpand.call(this, belongsHere(this) ? false : value);
73
- }));
74
- // 机器消息的原生卡片只由投影点开(host.ts 的 openCard);全局展开不平铺它(其余 CustomMessage 照旧跟随全局)。
75
- restores.push(patchMethod(CustomMessageComponent.prototype, "setExpanded", function (this: CustomMessageComponent, value) {
76
- const target = guarded(() => (isMachineMessage(this) && !isCardOpened(this) ? false : value), () => value);
77
- originalMessageExpand.call(this, target);
78
- }));
79
-
80
- const install = (chat: Container) => {
81
- const render = chat.render;
82
- const mouse = chat.handleMouse;
83
- const projection = new Container();
84
- const overrides = new Set<object>();
85
- let lastExpanded = ui.getToolsExpanded();
86
- const anchor = new ClickAnchor(() => {
87
- const view = scrollViewOf(tui, chat);
88
- return view && {
89
- top: view.scrollTop,
90
- following: view.isFollowingEnd,
91
- viewport: view.viewportHeight,
92
- contentHeight: scrollContentHeight(view),
93
- holdAt: (top: number) => view.scrollTo(top, { disableFollow: true }),
94
- follow: () => view.scrollToEnd(),
95
- };
96
- });
97
- const env: ProjectionEnv = {
98
- ui, clock: options.clock, headless: {},
99
- toggleRow: (row) => {
100
- toggleToolDetails(row, originalExpand);
101
- tui.requestRender();
102
- },
103
- isOpen: (key) => overrides.has(key),
104
- toggleOpen: (key) => {
105
- if (!overrides.delete(key)) overrides.add(key);
106
- tui.requestRender();
107
- },
108
- };
109
- chat.render = (width) => {
110
- // ctrl+o 永远是全部展开/全部折叠:全局档位一变,逐轮覆盖(含被点开的机器消息卡)一并复位。
111
- if (ui.getToolsExpanded() !== lastExpanded) {
112
- lastExpanded = ui.getToolsExpanded();
113
- overrides.clear();
114
- }
115
- return guarded(() => {
116
- const { nodes, animating } = projectProcessGroups(chat.children, env);
117
- projection.children = nodes;
118
- // 动效只经全局时钟:有活的摘要才订阅,静止即取消。
119
- if (animating && !stopFrames) stopFrames = onFrame(() => tui.requestRender());
120
- else if (!animating && stopFrames) { stopFrames(); stopFrames = undefined; }
121
- const lines = projection.render(width);
122
- anchor.layout(lines.length);
123
- return lines;
124
- }, () => render.call(chat, width));
125
- };
126
- chat.handleMouse = (event) => guarded(() => {
127
- const result = projection.handleMouse(event);
128
- if (result && event.type === "click") anchor.click(event.y);
129
- return result;
130
- }, () => mouse.call(chat, event));
131
- restores.push(() => {
132
- chat.render = render;
133
- chat.handleMouse = mouse;
134
- });
135
- };
136
- const discover = (trigger?: Component) => {
137
- if (attached) return;
138
- const chat = findChat(tui);
139
- if (!chat) return;
140
- attached = true;
141
- removeHook();
142
- const healthy = guarded(() => {
143
- for (const child of trigger ? [trigger, ...chat.children] : chat.children) checkShape(child);
144
- return true;
145
- }, () => false);
146
- if (healthy) install(chat);
147
- };
148
- // 宿主没有聊天容器句柄;首条用户消息、助手或工具插入时定位(首条用户消息一出现就带竖条),随后立即卸掉发现钩子。
149
- const removeHook = patchMethod(Container.prototype, "addChild", function (this: Container, child: Component) {
150
- originalAdd.call(this, child);
151
- if (child instanceof AssistantMessageComponent || child instanceof UserMessageComponent
152
- || (child instanceof ToolExecutionComponent && rowUiOf(child) === tui)) discover(child);
153
- });
154
- restores.push(removeHook);
155
- discover();
156
- return detach;
157
- }
158
-
package/tools/host.ts DELETED
@@ -1,369 +0,0 @@
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
- }