@herbertgao/pi-extensions 2026.10.4 → 2026.10.6

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 (52) hide show
  1. package/README.md +2 -2
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +13 -9
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +13 -9
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +72 -16
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +262 -48
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +2 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +1 -1
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +131 -28
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +2 -0
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +3 -0
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-currency.ts +29 -0
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +96 -59
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/git-stats.ts +162 -0
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/token-rate.ts +204 -0
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +51 -86
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +212 -74
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +50 -362
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +35 -3
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +16 -83
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +1 -1
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +28 -26
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +1 -1
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts +450 -0
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts +173 -0
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ccstyle-tool.ts +467 -0
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +5 -9
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +1 -24
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +64 -11
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +1 -1
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +41 -0
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +5 -1
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +2 -1
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +36 -10
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +1 -1
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +140 -51
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts +1 -1
  37. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +44 -4
  38. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +34 -803
  39. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-io.ts +735 -0
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-state.ts +138 -0
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +9 -0
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +5 -0
  43. package/node_modules/@herbertgao/pi-cc-extensions/package.json +5 -4
  44. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +2 -2
  45. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +3 -3
  46. package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
  47. package/node_modules/@herbertgao/sol-pi/README.md +3 -3
  48. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
  49. package/node_modules/@herbertgao/sol-pi/agents-install.md +3 -3
  50. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  51. package/package.json +2 -2
  52. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/show-more-hint.ts +0 -48
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * default mode(config.mode === "on"):Claude Code 风格工具卡渲染。
3
3
  *
4
- * 经 ToolExecutionComponent 原型补丁接管 call/result;edit/write 走 rich diff。
4
+ * 经 ToolExecutionComponent 原型补丁接管 call/result。
5
+ * 卡片怎么画在 tool/ccstyle-tool.ts,本文件只负责安装与所有权。
5
6
  * 生命周期对齐 compact-mode:installDefaultMode → hooks.shutdown。
6
7
  */
7
8
  import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
8
- import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
- import { config, getToolDisplayConfig, type CompactStyleMode } from "../config/config.ts";
9
+ import { config, type CompactStyleMode } from "../config/config.ts";
10
10
  import {
11
11
  COMPONENT_TOOL_RENDER_MODE,
12
12
  GLOBAL_TOOL_RENDER_PATCH,
@@ -14,48 +14,31 @@ import {
14
14
  TOOL_EXPANDED_BACKGROUND_PATCH,
15
15
  } from "../utils/patch-keys.ts";
16
16
  import { isToolCallHovered } from "./mouse/hover.ts";
17
- import {
18
- countLines,
19
- hasExpandableDetail,
20
- insetComponent,
21
- isToolExpanded,
22
- outputLineCount,
23
- pendingIcon,
24
- renderCollapsedToolResultToWidth,
25
- renderExpandedToolResult,
26
- resolveToolVisualState,
27
- scheduleAnimation,
28
- settledIcon,
29
- setToolVisualState,
30
- textFromResult,
31
- toolIconColor,
32
- toolViewportWidth,
33
- } from "./tool/result.ts";
34
- import { oneLine } from "../utils/format.ts";
35
- import { showMoreHintText } from "./tool/show-more-hint.ts";
36
- import {
37
- CODEMODE_TOOL_NAME,
38
- codemodeExpandedBody,
39
- createCodemodeResultComponent,
40
- } from "./tool/codemode.ts";
41
- import { countWriteDiffStats } from "./tool/diff/diff-renderer.ts";
42
- import { renderRichToolResult, type WriteExecutionMetadataStore } from "./tool/diff/index.ts";
17
+ import { isLiveToolCall } from "./tool/tool-state.ts";
43
18
  import { getMessageDisplayTheme } from "./tool/message-display.ts";
44
- import { mcpToolTitle } from "./tool/mcp-title.ts";
45
- import {
46
- fitToolCallSummary,
47
- humanizeToolLabel,
48
- renderToolSummary,
49
- toolCallSummary,
50
- } from "./tool/names.ts";
51
-
52
- // 成功勾:亮绿 truecolor(与 message-display 一致)
53
- const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
54
- const ANSI_FG_RESET = "\x1b[39m";
19
+ import { createCcstyleTool } from "./tool/ccstyle-tool.ts";
20
+ import type { WriteExecutionMetadataStore } from "./tool/diff/index.ts";
21
+ export { shouldRenderRichDiff } from "./tool/ccstyle-tool.ts";
55
22
 
56
23
  // pi-subagents 等扩展为 Agent 提供专用渲染器,ccstyle 必须保留。
57
24
  const DEDICATED_RENDERER_TOOLS = new Set(["Agent"]);
58
25
 
26
+ /** 排除名单内且自带 renderer 的工具保留原渲染。 */
27
+ export function preservesOriginalRenderer(
28
+ extensionDefinition: any,
29
+ toolName: string,
30
+ builtInToolDefinition?: any,
31
+ excludeRenderers: readonly string[] = config.excludeRenderers,
32
+ ): boolean {
33
+ if (!excludeRenderers.includes(toolName)) return false;
34
+ return [extensionDefinition, builtInToolDefinition].some(
35
+ (definition) =>
36
+ definition?.renderShell === "self" ||
37
+ typeof definition?.renderCall === "function" ||
38
+ typeof definition?.renderResult === "function",
39
+ );
40
+ }
41
+
59
42
  export type DefaultModeHooks = {
60
43
  /** 本安装是否仍持有全局工具渲染补丁。 */
61
44
  isOwner(): boolean;
@@ -95,6 +78,9 @@ type GlobalToolRenderPatch = {
95
78
  renderPaint?: (width: number) => string[];
96
79
  invalidatePaint?: () => void;
97
80
  paintCache?: WeakMap<object, ToolPaintHit>;
81
+ /** 给渲染上下文补 result;卸下时还原。 */
82
+ originalGetRenderContext?: (...args: any[]) => any;
83
+ getRenderContext?: (this: any, ...args: any[]) => any;
98
84
  };
99
85
 
100
86
  type ToolExpandedBackgroundPatch = {
@@ -105,326 +91,6 @@ type ToolExpandedBackgroundPatch = {
105
91
  dispose: () => void;
106
92
  };
107
93
 
108
- /** rich diff 的两个入口(本处与 compact 的 paintCompactEditWrite)都靠 renderRichToolResult
109
- * 返回 undefined 让位,这里只判断模式与工具名,避免多一个可能与让位不一致的 gate。 */
110
- export function shouldRenderRichDiff(
111
- mode: CompactStyleMode,
112
- toolName: string,
113
- isError: boolean,
114
- ): boolean {
115
- return mode === "on" && !isError && (toolName === "edit" || toolName === "write");
116
- }
117
-
118
- /** 排除名单内且自带 renderer 的工具保留原渲染。 */
119
- export function preservesOriginalRenderer(
120
- extensionDefinition: any,
121
- toolName: string,
122
- builtInToolDefinition?: any,
123
- excludeRenderers: readonly string[] = config.excludeRenderers,
124
- ): boolean {
125
- if (!excludeRenderers.includes(toolName)) return false;
126
- return [extensionDefinition, builtInToolDefinition].some(
127
- (definition) =>
128
- definition?.renderShell === "self" ||
129
- typeof definition?.renderCall === "function" ||
130
- typeof definition?.renderResult === "function",
131
- );
132
- }
133
-
134
- function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any[], fallback = "") {
135
- try {
136
- if (typeof tool?.[slot] === "function") return tool[slot](...args);
137
- } catch {
138
- // Fall through to raw fallback.
139
- }
140
- return new Text(fallback, 0, 0);
141
- }
142
-
143
- type ParsedTask = { id: string; status: string; subject: string };
144
-
145
- function parseTaskList(text: string): ParsedTask[] {
146
- return text
147
- .split("\n")
148
- .map((line) => line.match(/^#(\d+) \[([^\]]+)] (.+)$/))
149
- .filter((match): match is RegExpMatchArray => Boolean(match))
150
- .map((match) => ({ id: match[1]!, status: match[2]!, subject: match[3]! }));
151
- }
152
-
153
- function taskListSummary(tasks: ParsedTask[]): string {
154
- const counts = { pending: 0, in_progress: 0, completed: 0 };
155
- for (const task of tasks) {
156
- if (task.status in counts) counts[task.status as keyof typeof counts]++;
157
- }
158
- return [
159
- `${tasks.length} tasks`,
160
- counts.in_progress ? `${counts.in_progress} in progress` : "",
161
- counts.pending ? `${counts.pending} pending` : "",
162
- counts.completed ? `${counts.completed} completed` : "",
163
- ]
164
- .filter(Boolean)
165
- .join(" · ");
166
- }
167
-
168
- function renderExpandedTaskResult(
169
- toolName: string,
170
- text: string,
171
- theme: any,
172
- isError: boolean,
173
- ): any | undefined {
174
- if (isError) return undefined;
175
- if (toolName === "TaskList") {
176
- const tasks = parseTaskList(text);
177
- if (!tasks.length) return undefined;
178
- const limit = Math.max(1, config.expandedPreviewMaxLines);
179
- const rows = tasks.slice(0, limit).map((task) => {
180
- const color =
181
- task.status === "completed"
182
- ? "success"
183
- : task.status === "in_progress"
184
- ? "warning"
185
- : "muted";
186
- return `${theme.fg("accent", `#${task.id}`)} ${theme.fg(color, task.status)} ${theme.fg("dim", task.subject)}`;
187
- });
188
- if (tasks.length > rows.length)
189
- rows.push(theme.fg("muted", `… ${tasks.length - rows.length} more tasks`));
190
- // 贴左:外层展开卡片 Box(1,1) 提供 1 格 padding
191
- return new Text(
192
- `↳ ${theme.fg("muted", taskListSummary(tasks))}\n${rows.map((r) => ` ${r}`).join("\n")}`,
193
- 0,
194
- 0,
195
- );
196
- }
197
- const line = text.trim();
198
- if (!line || line.includes("\n")) return undefined;
199
- let formatted: string | undefined;
200
- let match: RegExpMatchArray | null;
201
- if (
202
- toolName === "TaskCreate" &&
203
- (match = line.match(/^Task #(\d+) created successfully: (.+)$/))
204
- ) {
205
- formatted = `${theme.fg("success", "Created task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`;
206
- } else if (toolName === "TaskUpdate" && (match = line.match(/^Updated task #(\d+) (.+)$/))) {
207
- formatted = `${theme.fg("success", "Updated task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`;
208
- } else if (toolName === "TaskExecute") {
209
- formatted = `${theme.fg("success", "Started")} ${theme.fg("muted", line)}`;
210
- } else if (toolName === "TaskStop") {
211
- formatted = `${theme.fg("success", "Stopped")} ${theme.fg("muted", line)}`;
212
- }
213
- return formatted ? new Text(`↳ ${formatted}`, 0, 0) : undefined;
214
- }
215
-
216
- /** 用 ccstyle call/result 包装任意工具定义。 */
217
- function createCcstyleTool(
218
- originalTool: any,
219
- writeExecutionMetadata: WriteExecutionMetadataStore,
220
- toolName: string,
221
- ): any {
222
- const label = originalTool.label || toolName;
223
- const defaultTitle = label === toolName ? humanizeToolLabel(label) : label;
224
- // MCP 工具直接用 adapter 暴露的真实工具名,不做人性化
225
- const title = mcpToolTitle({ toolName, definition: originalTool }) ?? defaultTitle;
226
-
227
- return {
228
- ...originalTool,
229
- renderShell: "self",
230
- renderCall(args: any, theme: any, context: any) {
231
- if (config.mode === "off") {
232
- return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName));
233
- }
234
-
235
- const visualState = resolveToolVisualState(context);
236
- const isPending =
237
- visualState === "pending" ||
238
- (!visualState && (context?.isPartial || context?.executionStarted));
239
- const animating = isPending && Boolean(context?.executionStarted);
240
- if (animating) scheduleAnimation(context, { light: true });
241
- const settledRawIcon = isPending ? "" : settledIcon(toolName, visualState);
242
- const settledIconStyled =
243
- visualState === "success"
244
- ? `${BRIGHT_GREEN}${settledRawIcon}${ANSI_FG_RESET}`
245
- : theme.fg(toolIconColor(context), settledRawIcon);
246
- // 逐帧 spinner:帧在 render() 内取,动画定时器只需 requestRender,
247
- // 不再每次 tick 走 updateDisplay 把整张卡重建一遍。
248
- const pendingIconStyled = () => theme.fg(toolIconColor(context), pendingIcon(toolName));
249
- const summary = toolCallSummary(toolName, args, {
250
- title,
251
- variant: "default",
252
- cwd: context?.cwd,
253
- });
254
- let writeStatsText = "";
255
- let writeStatsStyled = "";
256
- if (toolName === "write" && visualState === "success") {
257
- const meta = writeExecutionMetadata.get(context?.toolCallId);
258
- const stats = countWriteDiffStats(
259
- typeof args?.content === "string" ? args.content : undefined,
260
- meta?.previousContent,
261
- meta?.fileExistedBeforeWrite,
262
- );
263
- if (stats) {
264
- writeStatsText = ` (+${stats.added} -${stats.removed})`;
265
- writeStatsStyled = ` ${theme.fg("dim", "(")}${theme.fg("success", `+${stats.added}`)} ${theme.fg("error", `-${stats.removed}`)}${theme.fg("dim", ")")}`;
266
- }
267
- }
268
- const extraText = writeStatsText || summary.detail;
269
- const extraStyled = writeStatsStyled || theme.fg("dim", summary.detail);
270
- let cachedWidth: number | undefined;
271
- let cachedLine: string | undefined;
272
- let cachedIcon: string | undefined;
273
- const expanded = Boolean(context?.expanded);
274
- return {
275
- render(width: number) {
276
- // 轻量 tick 只会 requestRender,靠这里续期,动画才能自维持。
277
- if (animating) scheduleAnimation(context, { light: true });
278
- const icon = isPending ? pendingIconStyled() : settledIconStyled;
279
- if (cachedLine !== undefined && cachedWidth === width && cachedIcon === icon) {
280
- return [cachedLine];
281
- }
282
- const viewportWidth = toolViewportWidth(width);
283
- // 展开态贴左(外层 Box 已 pad 1);折叠 self-shell 保留 1 格前导空格
284
- const lead = expanded ? "" : " ";
285
- const callWidth = Math.max(
286
- 0,
287
- viewportWidth - visibleWidth(icon) - 1 - (expanded ? 0 : 1),
288
- );
289
- const mainWidth = Math.max(0, callWidth - visibleWidth(extraText));
290
- cachedWidth = width;
291
- cachedIcon = icon;
292
- // 路径按最终可用宽度中间截断,避免整行二次截断隐藏文件名。
293
- cachedLine = `${lead}${icon} ${renderToolSummary(summary, mainWidth, theme.fg.bind(theme))}${extraStyled}`;
294
- return [truncateToWidth(cachedLine, viewportWidth, "")];
295
- },
296
- invalidate() {
297
- cachedLine = undefined;
298
- cachedIcon = undefined;
299
- },
300
- };
301
- },
302
- renderResult(result: any, options: any, theme: any, context: any) {
303
- if (config.mode === "off") {
304
- return renderDefault(
305
- originalTool,
306
- "renderResult",
307
- [result, options, theme, context],
308
- textFromResult(result),
309
- );
310
- }
311
-
312
- const expanded = isToolExpanded(options, context);
313
- const isError = Boolean(options?.isError || context?.isError);
314
- // codemode:折叠态自己画子调用树,展开态给全部子调用与去掉脚本头的输出
315
- if (toolName === CODEMODE_TOOL_NAME) {
316
- if (!options?.isPartial) setToolVisualState(context, isError ? "error" : "success");
317
- if (expanded) {
318
- return renderExpandedToolResult(
319
- codemodeExpandedBody(result),
320
- theme,
321
- isError,
322
- context?.lastComponent,
323
- context?.args,
324
- context,
325
- true,
326
- );
327
- }
328
- // 折叠态不再复用展开视图,免得 hover/鼠标命中指向已隐藏的组件
329
- if (context?.state) context.state.ccstyleIoView = undefined;
330
- const toolCallId = context?.toolCallId;
331
- return createCodemodeResultComponent({
332
- result,
333
- theme,
334
- running: Boolean(options?.isPartial),
335
- isError,
336
- isHovered: () => isToolCallHovered(toolCallId),
337
- });
338
- }
339
- if (options?.isPartial) {
340
- // 展开态贴左(Box 提供 1 格 pad);折叠态保持 3 格对齐标题
341
- const pending = expanded ? "↳ Pending…" : " ↳ Pending…";
342
- return new Text(theme.fg("muted", pending), 0, 0);
343
- }
344
-
345
- setToolVisualState(context, isError ? "error" : "success");
346
- const toolCallId = context?.toolCallId;
347
- if (shouldRenderRichDiff(config.mode, toolName, Boolean(isError))) {
348
- // getter 保证 Diff indicator / wrap / limits 下次绘制即更新
349
- const richResult = renderRichToolResult(
350
- toolName,
351
- result,
352
- {
353
- ...options,
354
- expanded,
355
- // 全局共享状态避免 /reload 后旧 result renderer 闭包失联
356
- isHovered: () => isToolCallHovered(toolCallId),
357
- },
358
- theme,
359
- context,
360
- writeExecutionMetadata,
361
- getToolDisplayConfig,
362
- );
363
- // 展开态由 Box(1,1) 提供内边距;折叠态 inset 一级缩进
364
- if (richResult) return expanded ? richResult : insetComponent(richResult);
365
- }
366
-
367
- const text = textFromResult(result, expanded);
368
- const args = context?.args;
369
- if (expanded) {
370
- const taskResult = renderExpandedTaskResult(toolName, text, theme, Boolean(isError));
371
- if (taskResult) return taskResult;
372
- }
373
- const tasks = !isError && toolName === "TaskList" ? parseTaskList(text) : [];
374
- const outputLines = outputLineCount(result) || countLines(text);
375
- const lineWord = outputLines === 1 ? "line" : "lines";
376
- const action = toolName === "read" ? "loaded" : "returned";
377
- const rendered = tasks.length
378
- ? taskListSummary(tasks)
379
- : isError
380
- ? text
381
- ? // 72 与原 result.ts 默认一致,保持错误摘要截断宽度。
382
- oneLine(text, 72)
383
- : "Failed"
384
- : outputLines
385
- ? `${outputLines} ${lineWord} ${action}`
386
- : "Done";
387
- const expandable = !expanded && (tasks.length > 0 || hasExpandableDetail(text, args));
388
- const hintText = showMoreHintText();
389
- const hintPrefix = expandable ? theme.fg("dim", " · ") : "";
390
- const hint = expandable ? hintPrefix + theme.fg("dim", hintText) : "";
391
- const hoveredHint = expandable ? hintPrefix + theme.fg("text", hintText) : "";
392
- if (expanded) {
393
- return renderExpandedToolResult(
394
- text || "",
395
- theme,
396
- Boolean(isError),
397
- context?.lastComponent,
398
- args,
399
- context,
400
- true, // mode=on:贴左,由外层 Box(1,1) 提供 1 格 padding
401
- );
402
- }
403
- if (context?.state) context.state.ccstyleIoView = undefined;
404
- let cachedWidth: number | undefined;
405
- let cachedLine: string | undefined;
406
- let cachedHoveredLine: string | undefined;
407
- return {
408
- render(width: number) {
409
- if (cachedLine === undefined || cachedWidth !== width) {
410
- cachedWidth = width;
411
- cachedLine = theme.fg(
412
- isError ? "error" : "muted",
413
- renderCollapsedToolResultToWidth(rendered, hint, width),
414
- );
415
- cachedHoveredLine = theme.fg(
416
- isError ? "error" : "muted",
417
- renderCollapsedToolResultToWidth(rendered, hoveredHint, width),
418
- );
419
- }
420
- return [isToolCallHovered(toolCallId) ? cachedHoveredLine! : cachedLine];
421
- },
422
- invalidate() {},
423
- };
424
- },
425
- };
426
- }
427
-
428
94
  function syncToolShell(component: any, shell: "default" | "self"): void {
429
95
  const target = shell === "self" ? component.selfRenderContainer : component.contentBox;
430
96
  if (!target || !Array.isArray(component.children)) return;
@@ -566,6 +232,10 @@ function installGlobalToolRendering(
566
232
  previous?.originalInvalidate && prototype.invalidate === previous.invalidatePaint
567
233
  ? previous.originalInvalidate
568
234
  : prototype.invalidate;
235
+ const originalGetRenderContext =
236
+ previous?.originalGetRenderContext && prototype.getRenderContext === previous.getRenderContext
237
+ ? previous.originalGetRenderContext
238
+ : prototype.getRenderContext;
569
239
  if (isOwnershipAwarePatch(previous)) disconnectGlobalToolRenderPatch(previous);
570
240
 
571
241
  const patch: GlobalToolRenderPatch = {
@@ -581,6 +251,7 @@ function installGlobalToolRendering(
581
251
  installed: undefined as any,
582
252
  originalRender,
583
253
  originalInvalidate,
254
+ originalGetRenderContext,
584
255
  paintCache: new WeakMap(),
585
256
  };
586
257
 
@@ -618,6 +289,13 @@ function installGlobalToolRendering(
618
289
  prototype.getRenderShell = patch.installed.getRenderShell;
619
290
  prototype.getCallRenderer = patch.installed.getCallRenderer;
620
291
  prototype.getResultRenderer = patch.installed.getResultRenderer;
292
+ // 渲染上下文没有 result。ask_user_question 的标题要报问题数,只能从这里补。
293
+ patch.getRenderContext = function (this: any, ...args: any[]) {
294
+ const context = originalGetRenderContext.apply(this, args);
295
+ if (context && typeof context === "object") context.result = this.result;
296
+ return context;
297
+ };
298
+ prototype.getRenderContext = patch.getRenderContext;
621
299
  // fullscreen 滚动/选区每帧整树 render;内容未变时复用上一帧行,避免重做 diff/Box。
622
300
  patch.renderPaint = function (this: any, width: number): string[] {
623
301
  if (!patch.active || patch.mode() === "off") {
@@ -626,8 +304,11 @@ function installGlobalToolRendering(
626
304
  const cache = patch.paintCache;
627
305
  // A running partial tool owns a time-varying spinner. Light animation ticks
628
306
  // request a repaint without invalidating the component, so settled-content
629
- // caching must not intercept those paints.
630
- const cacheable = !(this.executionStarted && this.isPartial);
307
+ // caching must not intercept those paints. The waiting window before the
308
+ // tool is dispatched animates too, so it must bypass the cache as well.
309
+ const animatingRow =
310
+ this.isPartial && (this.executionStarted || isLiveToolCall(this.toolCallId));
311
+ const cacheable = !animatingRow;
631
312
  const hit = cacheable ? cache?.get(this) : undefined;
632
313
  if (hit && toolPaintMatches(hit, this, width)) return hit.lines;
633
314
  const lines = originalRender.call(this, width);
@@ -683,6 +364,13 @@ function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void {
683
364
  ) {
684
365
  prototype.invalidate = patch.originalInvalidate;
685
366
  }
367
+ if (
368
+ patch.getRenderContext &&
369
+ prototype.getRenderContext === patch.getRenderContext &&
370
+ patch.originalGetRenderContext
371
+ ) {
372
+ prototype.getRenderContext = patch.originalGetRenderContext;
373
+ }
686
374
  }
687
375
 
688
376
  /** 展开面板背景统一为 user message 背景色;折叠行保持原生状态色。
@@ -31,8 +31,18 @@ import {
31
31
  TOOL_MOUSE_DISABLE,
32
32
  } from "./mouse/interaction.ts";
33
33
  import { getToolMouseTui, noteNewTranscriptItem } from "./mouse/scroll.ts";
34
+ import {
35
+ disposeStickyPromptPatches,
36
+ installStickyPromptPatches,
37
+ setStickyPromptTheme,
38
+ } from "./mouse/sticky-prompt.ts";
34
39
  import { setHoveredToolGroup, setHoveredToolIo } from "./mouse/hover.ts";
35
- import { clearAllAnimations } from "./tool/result.ts";
40
+ import {
41
+ clearAllAnimations,
42
+ clearLiveToolCalls,
43
+ noteStreamingToolCalls,
44
+ retireLiveToolCall,
45
+ } from "./tool/tool-state.ts";
36
46
  import { installWriteOverride, WriteExecutionMetadataStore } from "./tool/diff/index.ts";
37
47
  import {
38
48
  installMessageDisplayRendering,
@@ -123,6 +133,8 @@ export default function (
123
133
  if (staleGlobalPatch) staleGlobalPatch.mode = () => "off";
124
134
  // regular 主屏的差分渲染补丁:视口上方变化不再触发清回滚的 fullRender。
125
135
  installMainScreenDoRenderPatch();
136
+ // 粘性用户消息头:标记注入 + fullscreen 帧合成。
137
+ installStickyPromptPatches();
126
138
  const defaultMode = installDefaultMode(writeExecutionMetadata);
127
139
  const toolGrouping = installToolGrouping(() => config.mode === "on");
128
140
  const compactMode = installCompactMode({
@@ -189,6 +201,9 @@ export default function (
189
201
  });
190
202
 
191
203
  pi.on("message_update", async (event) => {
204
+ // 声明期(assistant 消息结束前)就记住工具 id:这段工具行已在等启动,
205
+ // spinner 该转,但 Pi 还没跑 markExecutionStarted。
206
+ if (event.message?.role === "assistant") noteStreamingToolCalls(event.message);
192
207
  // compact-thinking 在 session_start 时于 compact 补丁之上再装一层;
193
208
  // 扩展事件先于 interactive-mode 的 updateContent 派发,此处先重新认领。
194
209
  if (config.mode === "compact" && event.message?.role === "assistant") {
@@ -204,6 +219,7 @@ export default function (
204
219
  });
205
220
 
206
221
  pi.on("tool_execution_end", async (event) => {
222
+ retireLiveToolCall(event?.toolCallId);
207
223
  if (config.mode !== "compact") return;
208
224
  // Agent 等工具收尾后延迟刷新,让 compact-thinking 先落最终态。
209
225
  const toolCallId: string | undefined = event?.toolCallId;
@@ -232,12 +248,16 @@ export default function (
232
248
  if (!hooks) return;
233
249
  hooks.toolGrouping.setTheme(ctx.ui.theme);
234
250
  setMessageDisplayTheme(ctx.ui.theme);
251
+ setStickyPromptTheme(ctx.ui.theme);
235
252
  ctx.ui.setStatus("ccstyle", undefined);
236
253
  // 先收集 resume transcript,再同步 compact 补丁与全局展开状态。
237
254
  syncCompactMode(ctx);
238
255
  // compact-thinking 的 session_start 处理在本 handler 之后执行(在其之上再装
239
256
  // 一层 updateContent);延迟再同步一次,保证 compact 补丁最终位于外层。
240
257
  setTimeout(() => syncCompactMode(ctx), 0);
258
+ // 历史回放发生在 session_start 之后,延迟到回放结束再收尾最后一个回合,
259
+ // 否则它没有后续消息,摘要行会停在 Running。
260
+ setTimeout(() => compactModeHooks?.settleReplayedRound(), 0);
241
261
  scheduleSessionRender(() => hooks.toolGrouping.refresh(getToolMouseTui()));
242
262
  });
243
263
 
@@ -249,6 +269,7 @@ export default function (
249
269
  if (!hooks) return;
250
270
  hooks.toolGrouping.setTheme(ctx.ui.theme);
251
271
  setMessageDisplayTheme(ctx.ui.theme);
272
+ setStickyPromptTheme(ctx.ui.theme);
252
273
  syncCompactMode(ctx);
253
274
  scheduleSessionRender(() => {
254
275
  syncCompactMode(ctx);
@@ -268,8 +289,18 @@ export default function (
268
289
  noteNewTranscriptItem();
269
290
  });
270
291
 
292
+ pi.on("turn_end", async () => {
293
+ clearLiveToolCalls();
294
+ });
295
+
296
+ pi.on("agent_end", async () => {
297
+ clearLiveToolCalls();
298
+ });
299
+
271
300
  pi.on("session_shutdown", async () => {
272
301
  writeExecutionMetadata.clear();
302
+ // 会话结束就清空,不能等 installation 早退判断;残留的 id 会让下一会话误转。
303
+ clearLiveToolCalls();
273
304
  // 鼠标交互独立于渲染层:fullscreen 下 installation 为 undefined,
274
305
  // 但 onTerminalInput 监听与 handleViewportInput 包装仍需释放。
275
306
  teardownToolMouseInteraction(mouseOwner);
@@ -281,6 +312,7 @@ export default function (
281
312
  current.compactMode.shutdown();
282
313
  compactModeHooks = undefined;
283
314
  current.disposeMessageDisplay();
315
+ disposeStickyPromptPatches();
284
316
  clearAllAnimations();
285
317
  installation = undefined;
286
318
  });
@@ -300,6 +332,6 @@ export {
300
332
  ExpandedToolIoView,
301
333
  ExpandedToolResultText,
302
334
  formatToolInputArgs,
303
- SHOW_MORE_LABEL,
304
- } from "./tool/result.ts";
335
+ } from "./tool/tool-io.ts";
336
+ export { SHOW_MORE_LABEL } from "./tool/result.ts";
305
337
  export { installToolMouseInteraction } from "./mouse/interaction.ts";