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.
Files changed (79) hide show
  1. package/README.md +35 -15
  2. package/activity.ts +92 -0
  3. package/busy.ts +188 -0
  4. package/config.example.jsonc +38 -47
  5. package/config.ts +198 -172
  6. package/deliver.ts +100 -13
  7. package/flame.ts +149 -0
  8. package/format.ts +84 -31
  9. package/header.ts +208 -82
  10. package/index.ts +17 -11
  11. package/master/actions.ts +293 -0
  12. package/master/activity-list.ts +281 -0
  13. package/master/event-card.ts +18 -37
  14. package/master/event-format.ts +71 -40
  15. package/master/guard.ts +46 -0
  16. package/master/index.ts +88 -1036
  17. package/master/list-view.ts +129 -0
  18. package/master/outbox.ts +188 -0
  19. package/master/prompts/master.zh.md +17 -12
  20. package/master/prompts/worker.zh.md +1 -1
  21. package/master/role.ts +6 -1
  22. package/master/run.ts +315 -0
  23. package/master/runtime.ts +259 -0
  24. package/master/spawn.ts +79 -38
  25. package/master/state.ts +30 -6
  26. package/master/worker-view.ts +517 -0
  27. package/package.json +1 -1
  28. package/provider/claude-sub.ts +35 -13
  29. package/provider/openai-native/src/compact-client.ts +2 -14
  30. package/provider/openai-native/src/options.ts +4 -2
  31. package/review/advisor.ts +0 -2
  32. package/review/card.ts +36 -23
  33. package/review/checkpoint.ts +2 -8
  34. package/review/evidence.ts +42 -44
  35. package/review/index.ts +271 -525
  36. package/review/occupancy.ts +25 -0
  37. package/review/outcome.ts +31 -7
  38. package/review/prompt.ts +7 -8
  39. package/review/prompts/review.en.md +7 -6
  40. package/review/prompts/review.zh.md +7 -6
  41. package/review/reviewer.ts +2 -5
  42. package/review/session.ts +11 -30
  43. package/review/state.ts +32 -60
  44. package/review/ui.ts +30 -469
  45. package/round-recorder.ts +18 -0
  46. package/session/herdr-display.ts +1 -1
  47. package/session/presets.ts +130 -87
  48. package/session/quota.ts +123 -0
  49. package/session/stats.ts +4 -1
  50. package/statusbar/index.ts +242 -104
  51. package/statusbar/render.ts +95 -163
  52. package/theme.ts +2 -41
  53. package/tools/actions.ts +53 -0
  54. package/tools/assistant-view.ts +64 -0
  55. package/tools/click-anchor.ts +56 -0
  56. package/tools/group-view.ts +420 -0
  57. package/tools/grouping.ts +146 -221
  58. package/tools/host.ts +369 -0
  59. package/tools/index.ts +108 -105
  60. package/tools/line.ts +95 -58
  61. package/tools/machine.ts +78 -0
  62. package/tools/parts.ts +2 -2
  63. package/tools/round.ts +95 -0
  64. package/tools/turn-clock.ts +41 -0
  65. package/tools/turn-summary.ts +109 -0
  66. package/truncated-write.ts +18 -0
  67. package/watcher/card.ts +28 -22
  68. package/watcher/index.ts +16 -44
  69. package/watcher/observer.ts +3 -3
  70. package/watcher/prompts/watch.zh.md +1 -1
  71. package/watcher/transcript.ts +4 -12
  72. package/flame-frames.ts +0 -460
  73. package/review/progress.ts +0 -324
  74. package/session/bark.ts +0 -157
  75. package/session/working-flame.ts +0 -116
  76. package/statusbar/quota-cache.ts +0 -60
  77. package/statusbar/quota-parse.ts +0 -85
  78. package/statusbar/quota.ts +0 -203
  79. package/statusbar/tps.ts +0 -87
package/tools/grouping.ts CHANGED
@@ -1,233 +1,158 @@
1
- /**
2
- * 连续工具行的视觉合并与第三方工具通用单行兜底:
3
- * 1. 原生内置或声明了 self 的工具:保留自渲染,去除相邻空行形成轨道;
4
- * 2. 未知第三方或默认宽框工具:自动收口为通用 ToolLine 单行紧凑轨道与全宽背景。
5
- */
6
- import type { ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
7
- import { ToolLine } from "./line.js";
8
- import { genericArgsParts } from "./parts.js";
9
-
10
- const GROUP_PATCH = Symbol.for("pi.firecode.group-patch");
11
- const REQUEST_RENDER_PATCH = Symbol.for("pi.firecode.request-render-patch");
12
- const GLOBAL_STATE = Symbol.for("pi.firecode.tools-state");
13
- const THEME_KEY = Symbol.for("@earendil-works/pi-coding-agent:theme");
14
-
15
- function getTheme(): Theme {
16
- const globalTheme = (globalThis as any)[THEME_KEY];
17
- if (globalTheme && typeof globalTheme.fg === "function") return globalTheme;
18
- return {
19
- fg: (_color: string, text: string) => text,
20
- bg: (_color: string, text: string) => text,
21
- bold: (text: string) => text,
22
- } as unknown as Theme;
23
- }
24
-
25
- type RenderFunction = (width: number) => string[];
26
- type RequestRenderFunction = (force?: boolean) => unknown;
27
- type ToolRow = { toolName: unknown; setExpanded: unknown; render: RenderFunction };
28
- type ToolRowPatch = { original: RenderFunction; patched: RenderFunction };
29
- type ToolRowPrototype = ToolRow & { [GROUP_PATCH]?: ToolRowPatch };
30
- type RequestRenderPatch = { original: RequestRenderFunction; patched: RequestRenderFunction };
31
- type ContainerLike = { children: unknown[] };
32
- type TuiLike = {
33
- requestRender: RequestRenderFunction;
34
- children?: unknown[];
35
- [REQUEST_RENDER_PATCH]?: RequestRenderPatch;
36
- };
37
- type GlobalState = {
38
- tui?: TuiLike;
39
- chat?: ContainerLike;
40
- indexedChildren?: unknown[];
41
- indexedLength?: number;
42
- joinedRows?: WeakSet<object>;
43
- patchedPrototype?: ToolRowPrototype;
44
- patchErrorReported?: boolean;
45
- decorated: Set<string>;
46
- };
47
-
48
- // 扩展热重载会重新执行模块,patch 状态必须挂在全局才能正确卸载。
49
- const runtimeGlobal = globalThis as typeof globalThis & { [GLOBAL_STATE]?: GlobalState };
50
- const globalState = (runtimeGlobal[GLOBAL_STATE] ??= { decorated: new Set<string>() });
51
-
52
- function isToolRow(value: unknown): value is ToolRow {
53
- if (!value || typeof value !== "object") return false;
54
- const row = value as ToolRow;
55
- return typeof row.render === "function" && typeof row.setExpanded === "function" && "toolName" in row;
56
- }
57
-
58
- function isDecoratedToolRow(value: unknown): value is ToolRow {
59
- return isToolRow(value);
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;
60
15
  }
61
16
 
62
- export function renderFallbackToolRow(row: any, width: number, customTheme?: Theme): string[] {
63
- const isError = row.result?.isError ?? false;
64
- const isPartial = row.isPartial ?? false;
65
- const toolName = typeof row.toolName === "string" ? row.toolName : "tool";
66
- const activeTheme = customTheme ?? getTheme();
67
- const toolLine = new ToolLine({
68
- label: toolName,
69
- value: genericArgsParts(row.args),
70
- clip: "end",
71
- theme: activeTheme,
72
- ctx: {
73
- state: row.rendererState ?? {},
74
- cwd: row.cwd ?? process.cwd(),
75
- toolCallId: row.toolCallId ?? "",
76
- isPartial,
77
- isError,
78
- expanded: row.expanded ?? false,
79
- },
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);
80
22
  });
81
- const lines = ["", ...toolLine.render(width)];
82
- if (row.expanded && row.result) {
83
- const textContent = Array.isArray(row.result.content)
84
- ? row.result.content
85
- .map((c: any) => (c?.type === "text" && typeof c.text === "string" ? c.text : ""))
86
- .filter(Boolean)
87
- .join("\n")
88
- : "";
89
- if (textContent) {
90
- const outputLines = textContent.split("\n").map((l: string) => activeTheme.fg("toolOutput", l));
91
- lines.push("", ...outputLines);
92
- }
93
- }
94
- return lines;
95
- }
96
-
97
- function findChatContainer(value: unknown, seen = new Set<unknown>()): ContainerLike | undefined {
98
- if (!value || typeof value !== "object" || seen.has(value)) return undefined;
99
- seen.add(value);
100
- const children = (value as ContainerLike).children;
101
- if (!Array.isArray(children)) return undefined;
102
- if (children.some(isToolRow)) return value as ContainerLike;
103
- for (const child of children) {
104
- const found = findChatContainer(child, seen);
105
- if (found) return found;
106
- }
107
- return undefined;
108
- }
109
-
110
- /** 增量标记"紧跟另一条装饰行"的行,聊天记录只增不改,可以从上次位置续算。 */
111
- function indexJoinedRows(children: unknown[]): void {
112
- const canAppend =
113
- globalState.indexedChildren === children &&
114
- globalState.indexedLength !== undefined &&
115
- globalState.indexedLength <= children.length &&
116
- globalState.joinedRows !== undefined;
117
- const joinedRows =
118
- canAppend && globalState.joinedRows ? globalState.joinedRows : new WeakSet<object>();
119
- const start = canAppend ? Math.max(1, globalState.indexedLength ?? 1) : 1;
120
- for (let index = start; index < children.length; index++) {
121
- const current = children[index];
122
- if (isDecoratedToolRow(current) && isDecoratedToolRow(children[index - 1]))
123
- joinedRows.add(current);
124
- }
125
- globalState.indexedChildren = children;
126
- globalState.indexedLength = children.length;
127
- globalState.joinedRows = joinedRows;
23
+ const dispose = () => {
24
+ if (runtime[OWNER] !== dispose) return;
25
+ detach();
26
+ delete runtime[OWNER];
27
+ };
28
+ runtime[OWNER] = dispose;
29
+ return dispose;
128
30
  }
129
31
 
130
- function followsToolRow(row: ToolRow): boolean {
131
- if (!globalState.chat) globalState.chat = findChatContainer(globalState.tui);
132
- const children = globalState.chat?.children;
133
- if (!children) return false;
134
- if (globalState.indexedChildren !== children || globalState.indexedLength !== children.length)
135
- indexJoinedRows(children);
136
- return globalState.joinedRows?.has(row) ?? false;
32
+ /** 首个真实实例的形状自检:私有字段改名或换型就不安装。 */
33
+ function checkShape(child: Component): void {
34
+ if (child instanceof ToolExecutionComponent) toolFacts(child);
35
+ if (child instanceof AssistantMessageComponent) assistantFacts(child);
137
36
  }
138
37
 
139
- /** 走 pi 原生渲染(会消费工具自带 renderCall)的工具;其余走通用兜底行。
140
- * subagents 自带中文 ToolLine 渲染(master/index.ts),不列入会被兜底行遮掉。 */
141
- export const FIRECODE_TOOLS = new Set(["read", "bash", "edit", "write", "find", "grep", "ls", "subagents"]);
142
-
143
- function patchToolRows(): void {
144
- if (globalState.patchedPrototype) return;
145
- const chat = globalState.chat ?? findChatContainer(globalState.tui);
146
- if (!chat) return;
147
- globalState.chat = chat;
148
- const row = chat.children.find(isToolRow);
149
- if (!row) return;
150
- const prototype = Object.getPrototypeOf(row) as ToolRowPrototype;
151
- if (prototype[GROUP_PATCH]) {
152
- globalState.patchedPrototype = prototype;
153
- return;
154
- }
155
- const original = prototype.render;
156
- const patched: RenderFunction = function (this: ToolRow, width: number): string[] {
157
- const toolName = typeof this.toolName === "string" ? this.toolName : "";
158
- const isFirecodeTool = FIRECODE_TOOLS.has(toolName);
159
- const isExpanded = Boolean((this as any).expanded);
160
- const lines = isFirecodeTool || isExpanded
161
- ? original.call(this, width)
162
- : renderFallbackToolRow(this, width);
163
- return followsToolRow(this) && lines[0] === "" ? lines.slice(1) : lines;
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();
164
49
  };
165
- Object.defineProperty(prototype, GROUP_PATCH, {
166
- configurable: true,
167
- value: { original, patched } satisfies ToolRowPatch,
168
- });
169
- prototype.render = patched;
170
- globalState.patchedPrototype = prototype;
171
- }
172
-
173
- export function installGroupPatch(ui: ExtensionUIContext, decorated: Set<string>): void {
174
- globalState.decorated = decorated;
175
- globalState.patchErrorReported = false;
176
- // 借一次 widget 生命周期拿到 tui 实例,拿到即注销。
177
- ui.setWidget("firecode-tui-capture", (tui) => {
178
- const patchableTui = tui as TuiLike;
179
- globalState.tui = patchableTui;
180
- globalState.chat = undefined;
181
- globalState.indexedChildren = undefined;
182
- globalState.joinedRows = undefined;
183
- if (!patchableTui[REQUEST_RENDER_PATCH]) {
184
- const original = patchableTui.requestRender;
185
- const patched: RequestRenderFunction = (force) => {
186
- try {
187
- patchToolRows();
188
- } catch (error) {
189
- if (!globalState.patchErrorReported) {
190
- globalState.patchErrorReported = true;
191
- ui.notify(
192
- `工具轨道连接失败:${error instanceof Error ? error.message : String(error)}`,
193
- "warning",
194
- );
195
- }
196
- }
197
- return original.call(patchableTui, force);
198
- };
199
- Object.defineProperty(patchableTui, REQUEST_RENDER_PATCH, {
200
- configurable: true,
201
- value: { original, patched } satisfies RequestRenderPatch,
202
- });
203
- patchableTui.requestRender = patched;
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();
204
66
  }
205
- patchToolRows();
206
- return { render: () => [], invalidate: () => {} };
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);
207
153
  });
208
- ui.setWidget("firecode-tui-capture", undefined);
154
+ restores.push(removeHook);
155
+ discover();
156
+ return detach;
209
157
  }
210
158
 
211
- export function uninstallGroupPatch(): void {
212
- const tui = globalState.tui;
213
- const requestPatch = tui?.[REQUEST_RENDER_PATCH];
214
- if (tui && requestPatch && tui.requestRender === requestPatch.patched) {
215
- tui.requestRender = requestPatch.original;
216
- delete tui[REQUEST_RENDER_PATCH];
217
- }
218
-
219
- const prototype = globalState.patchedPrototype;
220
- const rowPatch = prototype?.[GROUP_PATCH];
221
- if (prototype && rowPatch && prototype.render === rowPatch.patched) {
222
- prototype.render = rowPatch.original;
223
- delete prototype[GROUP_PATCH];
224
- }
225
-
226
- globalState.tui = undefined;
227
- globalState.chat = undefined;
228
- globalState.indexedChildren = undefined;
229
- globalState.indexedLength = undefined;
230
- globalState.joinedRows = undefined;
231
- globalState.patchedPrototype = undefined;
232
- globalState.patchErrorReported = false;
233
- }