@herbertgao/pi-extensions 2026.10.5 → 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.
- package/README.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +22 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +2 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +131 -28
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +2 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +3 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +10 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/token-rate.ts +15 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +2 -23
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +105 -55
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +23 -471
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +6 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +16 -83
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts +53 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ccstyle-tool.ts +467 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +1 -24
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +2 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +53 -34
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +41 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -862
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-io.ts +735 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-state.ts +138 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +5 -4
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +2 -2
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/sol-pi/README.md +3 -3
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
- package/node_modules/@herbertgao/sol-pi/agents-install.md +3 -3
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
- package/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/show-more-hint.ts +0 -48
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "../compact-mode.ts";
|
|
15
15
|
import { isMessageDisplayComponent } from "../tool/message-display.ts";
|
|
16
16
|
import { isLazyProxyTui } from "../../utils/fullscreen-detect.ts";
|
|
17
|
-
import { setToolTuiFullscreen, isCollapseHintLine } from "../tool/
|
|
17
|
+
import { setToolTuiFullscreen, isCollapseHintLine } from "../tool/names.ts";
|
|
18
18
|
import {
|
|
19
19
|
type ExpandedToolIoView,
|
|
20
20
|
getActiveIoViewFrame,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type IoViewFrameState,
|
|
23
23
|
setActiveIoViewFrame,
|
|
24
24
|
type ToolIoSection,
|
|
25
|
-
} from "../tool/
|
|
25
|
+
} from "../tool/tool-io.ts";
|
|
26
26
|
import {
|
|
27
27
|
collectToolComponents,
|
|
28
28
|
extractToolFramePlacements,
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
STICKY_PROMPT_MARKER_PATCH,
|
|
26
26
|
STICKY_PROMPT_STATE_SLOT,
|
|
27
27
|
} from "../../utils/patch-keys.ts";
|
|
28
|
-
import { isToolTuiFullscreen } from "../tool/
|
|
28
|
+
import { isToolTuiFullscreen } from "../tool/names.ts";
|
|
29
29
|
import type { SgrMousePacket } from "./packets.ts";
|
|
30
30
|
import { setScrollButtonVisible } from "./scroll.ts";
|
|
31
31
|
|
|
@@ -172,6 +172,36 @@ export type StickyPromptInfo = {
|
|
|
172
172
|
text: string;
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* 滚动条轨道顶端那格所在的屏幕行和列;那一格是滑块、轨道在最顶端不可点、或
|
|
177
|
+
* 没有滚动条时返回 null。点在轨道顶端会被 Pi 当成「跳到会话开头」。
|
|
178
|
+
* 与 pi-tui getScrollbarGeometry 同一公式(它不在包入口导出):滑块至少 2 行,
|
|
179
|
+
* 位置按 scrollTop 占可滚范围的比例映射到轨道。
|
|
180
|
+
*/
|
|
181
|
+
export function scrollbarTopTrackCell(box: any): { row: number; col: number } | null {
|
|
182
|
+
const view = box?.scrollView;
|
|
183
|
+
const rect = box?.rect;
|
|
184
|
+
const clip = box?.clip;
|
|
185
|
+
if (!view?.isScrollbarVisible || !rect || !clip || rect.width <= 0 || rect.height <= 0)
|
|
186
|
+
return null;
|
|
187
|
+
const column = rect.x + rect.width - 1;
|
|
188
|
+
if (column < clip.x || column >= clip.x + clip.width) return null;
|
|
189
|
+
const contentHeight = box.children?.[0]?.rect?.height ?? box.scrollContentLines?.length ?? 0;
|
|
190
|
+
const trackHeight = rect.height;
|
|
191
|
+
if (contentHeight <= 0) return null;
|
|
192
|
+
const thumbHeight = Math.max(
|
|
193
|
+
Math.min(2, trackHeight),
|
|
194
|
+
Math.min(trackHeight, Math.round((trackHeight * trackHeight) / contentHeight)),
|
|
195
|
+
);
|
|
196
|
+
const maxScrollTop = Math.max(0, contentHeight - trackHeight);
|
|
197
|
+
const maxThumbTop = trackHeight - thumbHeight;
|
|
198
|
+
const scrollTop = Number(view.scrollTop ?? 0);
|
|
199
|
+
const thumbTop =
|
|
200
|
+
rect.y + (maxScrollTop === 0 ? 0 : Math.round((scrollTop / maxScrollTop) * maxThumbTop));
|
|
201
|
+
// 顶端这一格是轨道而非滑块时才返回它:滑块一上来,整格就归拖动。
|
|
202
|
+
return thumbTop > clip.y ? { row: clip.y, col: column } : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
175
205
|
/** 视口上沿落在 userMessage 之下时,返回粘性行的位置与文本。 */
|
|
176
206
|
export function computeStickyPrompt(tui: any, layout: any): StickyPromptInfo | null {
|
|
177
207
|
if (!layout?.root) return null;
|
|
@@ -265,10 +295,7 @@ export function renderStickyPromptScreen(
|
|
|
265
295
|
return next;
|
|
266
296
|
}
|
|
267
297
|
|
|
268
|
-
/**
|
|
269
|
-
* 指针是否落在粘性行上。返回 true 时调用方不再做工具 hover,避免同一行双重高亮;
|
|
270
|
-
* hover 状态变化自行请求重绘(它不在 hover.ts 的状态里)。
|
|
271
|
-
*/
|
|
298
|
+
/** 行与列都落在本帧画出的粘性条内。滚动条那一列在条外。 */
|
|
272
299
|
function stickyBarHit(tui: any, packet: SgrMousePacket): boolean {
|
|
273
300
|
const state = stickyState();
|
|
274
301
|
if (!stickyPromptActive() || !state.visible || state.layout !== tui?.currentLayout) return false;
|
|
@@ -277,15 +304,36 @@ function stickyBarHit(tui: any, packet: SgrMousePacket): boolean {
|
|
|
277
304
|
return col >= state.x && col < state.x + state.width;
|
|
278
305
|
}
|
|
279
306
|
|
|
307
|
+
/**
|
|
308
|
+
* 指针是否落在粘性条上。返回 true 时调用方不再做工具 hover,避免同一行双重高亮;
|
|
309
|
+
* hover 状态变化自行请求重绘(它不在 hover.ts 的状态里)。
|
|
310
|
+
*/
|
|
280
311
|
export function updateStickyPromptHover(tui: any, packet: SgrMousePacket): boolean {
|
|
281
312
|
const onBar = stickyBarHit(tui, packet);
|
|
282
313
|
if (setStickyHovered(onBar)) tui.requestRender?.();
|
|
283
314
|
return onBar;
|
|
284
315
|
}
|
|
285
316
|
|
|
317
|
+
/**
|
|
318
|
+
* 滚动条轨道顶端那一格的按下:吞掉不处理。
|
|
319
|
+
* 放行的话 Pi 会把滑块跳到轨道顶端,阅读位置回到会话开头。粘性条在时整行最容易误点。
|
|
320
|
+
* 那格是滑块时不拦,照常拖动;没有滚动条、滚动条不可见时也不拦。
|
|
321
|
+
*/
|
|
322
|
+
export function scrollbarTopTrackHit(tui: any, packet: SgrMousePacket): boolean {
|
|
323
|
+
if (!stickyPromptActive()) return false;
|
|
324
|
+
const state = stickyState();
|
|
325
|
+
if (!state.visible || state.layout !== tui?.currentLayout) return false;
|
|
326
|
+
const box = findScrollBox(tui?.currentLayout?.root, tui?.getPrimaryScrollView?.());
|
|
327
|
+
const cell = scrollbarTopTrackCell(box);
|
|
328
|
+
if (!cell) return false;
|
|
329
|
+
// 只在粘性条那一行拦:那一行被遮住,不知道点的是轨道还是滑块。
|
|
330
|
+
return state.row === cell.row && packet.row - 1 === cell.row && packet.col - 1 === cell.col;
|
|
331
|
+
}
|
|
332
|
+
|
|
286
333
|
/** 单击粘性行:视口滚到该消息首行(整块可见),并亮出回到底部按钮。 */
|
|
287
334
|
export function handleStickyPromptClick(tui: any, packet: SgrMousePacket): boolean {
|
|
288
335
|
const state = stickyState();
|
|
336
|
+
if (scrollbarTopTrackHit(tui, packet)) return true;
|
|
289
337
|
if (!stickyBarHit(tui, packet)) return false;
|
|
290
338
|
const view = tui.getPrimaryScrollView?.();
|
|
291
339
|
if (typeof view?.scrollTo === "function") {
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
14
14
|
import { stripAnsi } from "../../utils/ansi-text.ts";
|
|
15
15
|
import { oneLine } from "../../utils/format.ts";
|
|
16
|
-
import { showMoreHintText } from "./
|
|
16
|
+
import { showMoreHintText } from "./names.ts";
|
|
17
17
|
import { headTruncateToWidth, toolViewportWidth } from "./result.ts";
|
|
18
18
|
|
|
19
19
|
export const ASK_USER_QUESTION_TOOL_NAME = "ask_user_question";
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ccstyle 工具卡本体:call/result 渲染,不碰原型补丁。
|
|
3
|
+
* default 用它接管 ToolExecutionComponent;compact 展开面板复用同一份 render。
|
|
4
|
+
*/
|
|
5
|
+
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
|
+
import { config, getToolDisplayConfig, type CompactStyleMode } from "../../config/config.ts";
|
|
7
|
+
import { isToolCallHovered } from "../mouse/hover.ts";
|
|
8
|
+
import {
|
|
9
|
+
ASK_USER_QUESTION_TOOL_NAME,
|
|
10
|
+
askUserExpandedBody,
|
|
11
|
+
askUserQuestionCount,
|
|
12
|
+
createAskUserResultComponent,
|
|
13
|
+
} from "./ask-user-question.ts";
|
|
14
|
+
import {
|
|
15
|
+
CODEMODE_TOOL_NAME,
|
|
16
|
+
codemodeExpandedBody,
|
|
17
|
+
createCodemodeResultComponent,
|
|
18
|
+
} from "./codemode.ts";
|
|
19
|
+
import { countWriteDiffStats, writeStatsInCollapsedBody } from "./diff/diff-renderer.ts";
|
|
20
|
+
import { renderRichToolResult, type WriteExecutionMetadataStore } from "./diff/index.ts";
|
|
21
|
+
import { mcpToolTitle } from "./mcp-title.ts";
|
|
22
|
+
import {
|
|
23
|
+
humanizeToolLabel,
|
|
24
|
+
renderToolSummary,
|
|
25
|
+
SHELL_TOOL_NAMES,
|
|
26
|
+
toolCallSummary,
|
|
27
|
+
} from "./names.ts";
|
|
28
|
+
import {
|
|
29
|
+
collapsedResultSummary,
|
|
30
|
+
formatToolDuration,
|
|
31
|
+
insetComponent,
|
|
32
|
+
renderCollapsedToolResultToWidth,
|
|
33
|
+
textFromResult,
|
|
34
|
+
toolViewportWidth,
|
|
35
|
+
} from "./result.ts";
|
|
36
|
+
import { hasExpandableDetail, isExpandedToolIoView, renderExpandedToolResult } from "./tool-io.ts";
|
|
37
|
+
import {
|
|
38
|
+
isLiveToolCall,
|
|
39
|
+
isToolExpanded,
|
|
40
|
+
pendingIcon,
|
|
41
|
+
resolveToolVisualState,
|
|
42
|
+
scheduleAnimation,
|
|
43
|
+
settledIcon,
|
|
44
|
+
setToolVisualState,
|
|
45
|
+
toolIconColor,
|
|
46
|
+
} from "./tool-state.ts";
|
|
47
|
+
import { showMoreHintText } from "./names.ts";
|
|
48
|
+
|
|
49
|
+
/** 成功勾:亮绿 truecolor(与 message-display 一致)。 */
|
|
50
|
+
const BRIGHT_GREEN = "[38;2;80;220;100m";
|
|
51
|
+
const ANSI_FG_RESET = "[39m";
|
|
52
|
+
|
|
53
|
+
/** `(+A -D)`:plain 用于占宽,styled 用于上屏。default 写标题与 compact edit/write 标题共用。 */
|
|
54
|
+
export function styleChangeCounts(
|
|
55
|
+
fg: (color: string, text: string) => string,
|
|
56
|
+
added: number,
|
|
57
|
+
removed: number,
|
|
58
|
+
): { text: string; styled: string } {
|
|
59
|
+
return {
|
|
60
|
+
text: ` (+${added} -${removed})`,
|
|
61
|
+
styled: ` ${fg("dim", "(")}${fg("success", `+${added}`)} ${fg("error", `-${removed}`)}${fg("dim", ")")}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** rich diff 的两个入口(本处与 compact 的 paintCompactEditWrite)都靠 renderRichToolResult
|
|
66
|
+
* 返回 undefined 让位,这里只判断模式与工具名,避免多一个可能与让位不一致的 gate。 */
|
|
67
|
+
export function shouldRenderRichDiff(
|
|
68
|
+
mode: CompactStyleMode,
|
|
69
|
+
toolName: string,
|
|
70
|
+
isError: boolean,
|
|
71
|
+
): boolean {
|
|
72
|
+
return mode === "on" && !isError && (toolName === "edit" || toolName === "write");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any[], fallback = "") {
|
|
76
|
+
try {
|
|
77
|
+
if (typeof tool?.[slot] === "function") return tool[slot](...args);
|
|
78
|
+
} catch {
|
|
79
|
+
// Fall through to raw fallback.
|
|
80
|
+
}
|
|
81
|
+
return new Text(fallback, 0, 0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type ParsedTask = { id: string; status: string; subject: string };
|
|
85
|
+
|
|
86
|
+
function parseTaskList(text: string): ParsedTask[] {
|
|
87
|
+
return text
|
|
88
|
+
.split("\n")
|
|
89
|
+
.map((line) => line.match(/^#(\d+) \[([^\]]+)] (.+)$/))
|
|
90
|
+
.filter((match): match is RegExpMatchArray => Boolean(match))
|
|
91
|
+
.map((match) => ({ id: match[1]!, status: match[2]!, subject: match[3]! }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function taskListSummary(tasks: ParsedTask[]): string {
|
|
95
|
+
const counts = { pending: 0, in_progress: 0, completed: 0 };
|
|
96
|
+
for (const task of tasks) {
|
|
97
|
+
if (task.status in counts) counts[task.status as keyof typeof counts]++;
|
|
98
|
+
}
|
|
99
|
+
return [
|
|
100
|
+
`${tasks.length} tasks`,
|
|
101
|
+
counts.in_progress ? `${counts.in_progress} in progress` : "",
|
|
102
|
+
counts.pending ? `${counts.pending} pending` : "",
|
|
103
|
+
counts.completed ? `${counts.completed} completed` : "",
|
|
104
|
+
]
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.join(" · ");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function renderExpandedTaskResult(
|
|
110
|
+
toolName: string,
|
|
111
|
+
text: string,
|
|
112
|
+
theme: any,
|
|
113
|
+
isError: boolean,
|
|
114
|
+
): any | undefined {
|
|
115
|
+
if (isError) return undefined;
|
|
116
|
+
if (toolName === "TaskList") {
|
|
117
|
+
const tasks = parseTaskList(text);
|
|
118
|
+
if (!tasks.length) return undefined;
|
|
119
|
+
const limit = Math.max(1, config.expandedPreviewMaxLines);
|
|
120
|
+
const rows = tasks.slice(0, limit).map((task) => {
|
|
121
|
+
const color =
|
|
122
|
+
task.status === "completed"
|
|
123
|
+
? "success"
|
|
124
|
+
: task.status === "in_progress"
|
|
125
|
+
? "warning"
|
|
126
|
+
: "muted";
|
|
127
|
+
return `${theme.fg("accent", `#${task.id}`)} ${theme.fg(color, task.status)} ${theme.fg("dim", task.subject)}`;
|
|
128
|
+
});
|
|
129
|
+
if (tasks.length > rows.length)
|
|
130
|
+
rows.push(theme.fg("muted", `… ${tasks.length - rows.length} more tasks`));
|
|
131
|
+
// 贴左:外层展开卡片 Box(1,1) 提供 1 格 padding
|
|
132
|
+
return new Text(
|
|
133
|
+
`↳ ${theme.fg("muted", taskListSummary(tasks))}\n${rows.map((r) => ` ${r}`).join("\n")}`,
|
|
134
|
+
0,
|
|
135
|
+
0,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const line = text.trim();
|
|
139
|
+
if (!line || line.includes("\n")) return undefined;
|
|
140
|
+
let formatted: string | undefined;
|
|
141
|
+
let match: RegExpMatchArray | null;
|
|
142
|
+
if (
|
|
143
|
+
toolName === "TaskCreate" &&
|
|
144
|
+
(match = line.match(/^Task #(\d+) created successfully: (.+)$/))
|
|
145
|
+
) {
|
|
146
|
+
formatted = `${theme.fg("success", "Created task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`;
|
|
147
|
+
} else if (toolName === "TaskUpdate" && (match = line.match(/^Updated task #(\d+) (.+)$/))) {
|
|
148
|
+
formatted = `${theme.fg("success", "Updated task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`;
|
|
149
|
+
} else if (toolName === "TaskExecute") {
|
|
150
|
+
formatted = `${theme.fg("success", "Started")} ${theme.fg("muted", line)}`;
|
|
151
|
+
} else if (toolName === "TaskStop") {
|
|
152
|
+
formatted = `${theme.fg("success", "Stopped")} ${theme.fg("muted", line)}`;
|
|
153
|
+
}
|
|
154
|
+
return formatted ? new Text(`↳ ${formatted}`, 0, 0) : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* shell 的耗时文本。Pi 原生 shell 渲染器会在结果底部报一行,ccstyle 的通用折叠行把它盖掉了。
|
|
159
|
+
*
|
|
160
|
+
* 两条路都要:新 Pi 在渲染上下文里给 `durationMs`(单调,restore 出来的行也带得到),优先用它;
|
|
161
|
+
* 拿不到(旧 Pi、无耗时的结果)再退回自己的时钟——开始时间在 renderCall 记,结束时间在结果定型时定一次。
|
|
162
|
+
* 运行中的实时计时也只能走时钟。
|
|
163
|
+
*/
|
|
164
|
+
function shellDurationText(toolName: string, context: any, isPartial: boolean): string {
|
|
165
|
+
if (!SHELL_TOOL_NAMES.has(toolName)) return "";
|
|
166
|
+
const recorded = context?.durationMs;
|
|
167
|
+
if (!isPartial && typeof recorded === "number" && Number.isFinite(recorded) && recorded >= 0) {
|
|
168
|
+
return formatToolDuration(recorded);
|
|
169
|
+
}
|
|
170
|
+
const startedAt = context?.state?.ccstyleShellStartedAt;
|
|
171
|
+
if (typeof startedAt !== "number") return "";
|
|
172
|
+
const endedAt = context?.state?.ccstyleShellEndedAt;
|
|
173
|
+
const end = typeof endedAt === "number" ? endedAt : Date.now();
|
|
174
|
+
// 时钟回拨时别吐负数;快照固定时钟也会走到这。
|
|
175
|
+
return formatToolDuration(Math.max(0, end - startedAt));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 用 ccstyle call/result 包装任意工具定义。 */
|
|
179
|
+
export function createCcstyleTool(
|
|
180
|
+
originalTool: any,
|
|
181
|
+
writeExecutionMetadata: WriteExecutionMetadataStore,
|
|
182
|
+
toolName: string,
|
|
183
|
+
): any {
|
|
184
|
+
const label = originalTool.label || toolName;
|
|
185
|
+
const defaultTitle = label === toolName ? humanizeToolLabel(label) : label;
|
|
186
|
+
// MCP 工具直接用 adapter 暴露的真实工具名,不做人性化
|
|
187
|
+
const title = mcpToolTitle({ toolName, definition: originalTool }) ?? defaultTitle;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
...originalTool,
|
|
191
|
+
renderShell: "self",
|
|
192
|
+
renderCall(args: any, theme: any, context: any) {
|
|
193
|
+
if (config.mode === "off") {
|
|
194
|
+
return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// shell 的耗时从执行开始算;restore 出来的行没有 executionStarted,不参与。
|
|
198
|
+
if (
|
|
199
|
+
SHELL_TOOL_NAMES.has(toolName) &&
|
|
200
|
+
context?.executionStarted &&
|
|
201
|
+
context.state &&
|
|
202
|
+
context.state.ccstyleShellStartedAt === undefined
|
|
203
|
+
) {
|
|
204
|
+
context.state.ccstyleShellStartedAt = Date.now();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const visualState = resolveToolVisualState(context);
|
|
208
|
+
const isPending =
|
|
209
|
+
visualState === "pending" ||
|
|
210
|
+
(!visualState && (context?.isPartial || context?.executionStarted));
|
|
211
|
+
// 等待窗口(声明流式中或等待派发)也要转:Pi 要到 assistant 消息结束
|
|
212
|
+
// 才派发执行,这段只靠 executionStarted 会一直冻在第一帧。
|
|
213
|
+
const animating =
|
|
214
|
+
isPending && (Boolean(context?.executionStarted) || isLiveToolCall(context?.toolCallId));
|
|
215
|
+
if (animating) scheduleAnimation(context, { light: true });
|
|
216
|
+
const settledRawIcon = isPending ? "" : settledIcon(toolName, visualState);
|
|
217
|
+
const settledIconStyled =
|
|
218
|
+
visualState === "success"
|
|
219
|
+
? `${BRIGHT_GREEN}${settledRawIcon}${ANSI_FG_RESET}`
|
|
220
|
+
: theme.fg(toolIconColor(context), settledRawIcon);
|
|
221
|
+
// 逐帧 spinner:帧在 render() 内取,动画定时器只需 requestRender,
|
|
222
|
+
// 不再每次 tick 走 updateDisplay 把整张卡重建一遍。
|
|
223
|
+
const pendingIconStyled = () => theme.fg(toolIconColor(context), pendingIcon(toolName));
|
|
224
|
+
const summary = toolCallSummary(toolName, args, {
|
|
225
|
+
title,
|
|
226
|
+
variant: "default",
|
|
227
|
+
cwd: context?.cwd,
|
|
228
|
+
});
|
|
229
|
+
// ask_user_question:问题原文在折叠结果树里逐条列出,标题只留工具名和问题数,
|
|
230
|
+
// 否则第一个问题会在标题和树里各出现一次。
|
|
231
|
+
if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
232
|
+
// Pi 的渲染上下文不带 result,问题数从这里取。
|
|
233
|
+
const questionCount = askUserQuestionCount(context?.result, args);
|
|
234
|
+
summary.main = title;
|
|
235
|
+
summary.detail = questionCount === undefined ? "" : ` (+${questionCount})`;
|
|
236
|
+
summary.payload = undefined;
|
|
237
|
+
}
|
|
238
|
+
let writeStatsText = "";
|
|
239
|
+
let writeStatsStyled = "";
|
|
240
|
+
// 折叠 0 行预览时计数由正文那行(↳ diff +A -D · created …)显示,标题不重复。
|
|
241
|
+
const statsInBody = writeStatsInCollapsedBody(
|
|
242
|
+
Boolean(context?.expanded),
|
|
243
|
+
getToolDisplayConfig(),
|
|
244
|
+
);
|
|
245
|
+
if (toolName === "write" && visualState === "success" && !statsInBody) {
|
|
246
|
+
const meta = writeExecutionMetadata.get(context?.toolCallId);
|
|
247
|
+
const stats = countWriteDiffStats(
|
|
248
|
+
typeof args?.content === "string" ? args.content : undefined,
|
|
249
|
+
meta?.previousContent,
|
|
250
|
+
meta?.fileExistedBeforeWrite,
|
|
251
|
+
);
|
|
252
|
+
if (stats) {
|
|
253
|
+
const counts = styleChangeCounts(
|
|
254
|
+
(color, text) => theme.fg(color, text),
|
|
255
|
+
stats.added,
|
|
256
|
+
stats.removed,
|
|
257
|
+
);
|
|
258
|
+
writeStatsText = counts.text;
|
|
259
|
+
writeStatsStyled = counts.styled;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const extraText = writeStatsText || summary.detail;
|
|
263
|
+
const extraStyled = writeStatsStyled || theme.fg("dim", summary.detail);
|
|
264
|
+
let cachedWidth: number | undefined;
|
|
265
|
+
let cachedLine: string | undefined;
|
|
266
|
+
let cachedIcon: string | undefined;
|
|
267
|
+
const expanded = Boolean(context?.expanded);
|
|
268
|
+
return {
|
|
269
|
+
render(width: number) {
|
|
270
|
+
// 轻量 tick 只会 requestRender,靠这里续期,动画才能自维持。
|
|
271
|
+
if (animating) scheduleAnimation(context, { light: true });
|
|
272
|
+
const icon = isPending ? pendingIconStyled() : settledIconStyled;
|
|
273
|
+
if (cachedLine !== undefined && cachedWidth === width && cachedIcon === icon) {
|
|
274
|
+
return [cachedLine];
|
|
275
|
+
}
|
|
276
|
+
const viewportWidth = toolViewportWidth(width);
|
|
277
|
+
// 展开态贴左(外层 Box 已 pad 1);折叠 self-shell 保留 1 格前导空格
|
|
278
|
+
const lead = expanded ? "" : " ";
|
|
279
|
+
const callWidth = Math.max(
|
|
280
|
+
0,
|
|
281
|
+
viewportWidth - visibleWidth(icon) - 1 - (expanded ? 0 : 1),
|
|
282
|
+
);
|
|
283
|
+
const mainWidth = Math.max(0, callWidth - visibleWidth(extraText));
|
|
284
|
+
cachedWidth = width;
|
|
285
|
+
cachedIcon = icon;
|
|
286
|
+
// 路径按最终可用宽度中间截断,避免整行二次截断隐藏文件名。
|
|
287
|
+
cachedLine = `${lead}${icon} ${renderToolSummary(summary, mainWidth, theme.fg.bind(theme))}${extraStyled}`;
|
|
288
|
+
return [truncateToWidth(cachedLine, viewportWidth, "")];
|
|
289
|
+
},
|
|
290
|
+
invalidate() {
|
|
291
|
+
cachedLine = undefined;
|
|
292
|
+
cachedIcon = undefined;
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
renderResult(result: any, options: any, theme: any, context: any) {
|
|
297
|
+
if (config.mode === "off") {
|
|
298
|
+
return renderDefault(
|
|
299
|
+
originalTool,
|
|
300
|
+
"renderResult",
|
|
301
|
+
[result, options, theme, context],
|
|
302
|
+
textFromResult(result),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const expanded = isToolExpanded(options, context);
|
|
307
|
+
const isError = Boolean(options?.isError || context?.isError);
|
|
308
|
+
// codemode:折叠态自己画子调用树,展开态给全部子调用与去掉脚本头的输出
|
|
309
|
+
if (toolName === CODEMODE_TOOL_NAME) {
|
|
310
|
+
if (!options?.isPartial) setToolVisualState(context, isError ? "error" : "success");
|
|
311
|
+
if (expanded) {
|
|
312
|
+
return renderExpandedToolResult(
|
|
313
|
+
codemodeExpandedBody(result),
|
|
314
|
+
theme,
|
|
315
|
+
isError,
|
|
316
|
+
context?.lastComponent,
|
|
317
|
+
context?.args,
|
|
318
|
+
context,
|
|
319
|
+
true,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
// 折叠态不再复用展开视图,免得 hover/鼠标命中指向已隐藏的组件
|
|
323
|
+
if (context?.state) context.state.ccstyleIoView = undefined;
|
|
324
|
+
const toolCallId = context?.toolCallId;
|
|
325
|
+
return createCodemodeResultComponent({
|
|
326
|
+
result,
|
|
327
|
+
theme,
|
|
328
|
+
running: Boolean(options?.isPartial),
|
|
329
|
+
isError,
|
|
330
|
+
isHovered: () => isToolCallHovered(toolCallId),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
// ask_user_question:折叠态摊成问答树,展开态按问题逐条列出选择。
|
|
334
|
+
if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
335
|
+
if (!options?.isPartial) setToolVisualState(context, isError ? "error" : "success");
|
|
336
|
+
if (options?.isPartial) {
|
|
337
|
+
// 折叠态 1 格缩进:与完成后的 ├/└ 对齐标题行的勾。
|
|
338
|
+
const pending = expanded ? "↳ Pending…" : " ↳ Pending…";
|
|
339
|
+
return new Text(theme.fg("muted", pending), 0, 0);
|
|
340
|
+
}
|
|
341
|
+
if (expanded) {
|
|
342
|
+
return renderExpandedToolResult(
|
|
343
|
+
askUserExpandedBody(result),
|
|
344
|
+
theme,
|
|
345
|
+
isError,
|
|
346
|
+
context?.lastComponent,
|
|
347
|
+
context?.args,
|
|
348
|
+
context,
|
|
349
|
+
true,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (context?.state) context.state.ccstyleIoView = undefined;
|
|
353
|
+
const toolCallId = context?.toolCallId;
|
|
354
|
+
return createAskUserResultComponent({
|
|
355
|
+
result,
|
|
356
|
+
theme,
|
|
357
|
+
isError,
|
|
358
|
+
isHovered: () => isToolCallHovered(toolCallId),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (options?.isPartial) {
|
|
362
|
+
// 折叠态保持 3 格:与完成后的 ` ↳ …` 同列,工具跑完那行不横跳。
|
|
363
|
+
// 展开态贴左(Box 提供 1 格 pad)。
|
|
364
|
+
const pending = expanded ? "↳ Pending…" : " ↳ Pending…";
|
|
365
|
+
if (!SHELL_TOOL_NAMES.has(toolName)) {
|
|
366
|
+
return new Text(theme.fg("muted", pending), 0, 0);
|
|
367
|
+
}
|
|
368
|
+
// shell 运行中报已用时间:数字在 render() 内取,动画 tick 才能带上新值。
|
|
369
|
+
return {
|
|
370
|
+
render(width: number) {
|
|
371
|
+
const elapsed = shellDurationText(toolName, context, true);
|
|
372
|
+
const suffix = elapsed ? theme.fg("dim", ` · ${elapsed}`) : "";
|
|
373
|
+
return [truncateToWidth(theme.fg("muted", pending) + suffix, width, "")];
|
|
374
|
+
},
|
|
375
|
+
invalidate(): void {},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// shell:结果定型时定一次结束时间,这一行之后不再变。
|
|
380
|
+
if (
|
|
381
|
+
SHELL_TOOL_NAMES.has(toolName) &&
|
|
382
|
+
context?.state &&
|
|
383
|
+
context.state.ccstyleShellEndedAt === undefined
|
|
384
|
+
) {
|
|
385
|
+
context.state.ccstyleShellEndedAt = Date.now();
|
|
386
|
+
}
|
|
387
|
+
// 折叠行接在摘要后,展开态接在 Output 正文后(原生也是在结果底部报一行)。
|
|
388
|
+
const duration = shellDurationText(toolName, context, false);
|
|
389
|
+
|
|
390
|
+
setToolVisualState(context, isError ? "error" : "success");
|
|
391
|
+
const toolCallId = context?.toolCallId;
|
|
392
|
+
if (shouldRenderRichDiff(config.mode, toolName, Boolean(isError))) {
|
|
393
|
+
// getter 保证 Diff indicator / wrap / limits 下次绘制即更新
|
|
394
|
+
const richResult = renderRichToolResult(
|
|
395
|
+
toolName,
|
|
396
|
+
result,
|
|
397
|
+
{
|
|
398
|
+
...options,
|
|
399
|
+
expanded,
|
|
400
|
+
// 全局共享状态避免 /reload 后旧 result renderer 闭包失联
|
|
401
|
+
isHovered: () => isToolCallHovered(toolCallId),
|
|
402
|
+
},
|
|
403
|
+
theme,
|
|
404
|
+
context,
|
|
405
|
+
writeExecutionMetadata,
|
|
406
|
+
getToolDisplayConfig,
|
|
407
|
+
);
|
|
408
|
+
// 展开态由 Box(1,1) 提供内边距;折叠态 inset 一级缩进
|
|
409
|
+
if (richResult) return expanded ? richResult : insetComponent(richResult);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const text = textFromResult(result, expanded);
|
|
413
|
+
const args = context?.args;
|
|
414
|
+
if (expanded) {
|
|
415
|
+
const taskResult = renderExpandedTaskResult(toolName, text, theme, Boolean(isError));
|
|
416
|
+
if (taskResult) return taskResult;
|
|
417
|
+
}
|
|
418
|
+
const tasks = !isError && toolName === "TaskList" ? parseTaskList(text) : [];
|
|
419
|
+
const rendered = tasks.length
|
|
420
|
+
? taskListSummary(tasks)
|
|
421
|
+
: collapsedResultSummary({ result, toolName, text, isError });
|
|
422
|
+
const expandable = !expanded && (tasks.length > 0 || hasExpandableDetail(text, args));
|
|
423
|
+
const hintText = showMoreHintText();
|
|
424
|
+
const hintPrefix = expandable ? theme.fg("dim", " · ") : "";
|
|
425
|
+
const hint = expandable ? hintPrefix + theme.fg("dim", hintText) : "";
|
|
426
|
+
const hoveredHint = expandable ? hintPrefix + theme.fg("text", hintText) : "";
|
|
427
|
+
if (expanded) {
|
|
428
|
+
const view = renderExpandedToolResult(
|
|
429
|
+
text || "",
|
|
430
|
+
theme,
|
|
431
|
+
Boolean(isError),
|
|
432
|
+
context?.lastComponent,
|
|
433
|
+
args,
|
|
434
|
+
context,
|
|
435
|
+
true, // mode=on:贴左,由外层 Box(1,1) 提供 1 格 padding
|
|
436
|
+
);
|
|
437
|
+
// 尾部附注只能挂在 IoView 上:鼠标层按对象识别它,包一层会断掉命中。
|
|
438
|
+
// 与折叠态保持同一写法:裸值,不写 Took。
|
|
439
|
+
if (isExpandedToolIoView(view)) view.setFooter(duration || undefined);
|
|
440
|
+
return view;
|
|
441
|
+
}
|
|
442
|
+
if (context?.state) context.state.ccstyleIoView = undefined;
|
|
443
|
+
// 折叠态的耗时接在摘要后、展开提示前(提示必须留在行尾供鼠标层识别)。
|
|
444
|
+
const body = duration ? `${rendered}${theme.fg("dim", ` · ${duration}`)}` : rendered;
|
|
445
|
+
let cachedWidth: number | undefined;
|
|
446
|
+
let cachedLine: string | undefined;
|
|
447
|
+
let cachedHoveredLine: string | undefined;
|
|
448
|
+
return {
|
|
449
|
+
render(width: number) {
|
|
450
|
+
if (cachedLine === undefined || cachedWidth !== width) {
|
|
451
|
+
cachedWidth = width;
|
|
452
|
+
cachedLine = theme.fg(
|
|
453
|
+
isError ? "error" : "muted",
|
|
454
|
+
renderCollapsedToolResultToWidth(body, hint, width),
|
|
455
|
+
);
|
|
456
|
+
cachedHoveredLine = theme.fg(
|
|
457
|
+
isError ? "error" : "muted",
|
|
458
|
+
renderCollapsedToolResultToWidth(body, hoveredHint, width),
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return [isToolCallHovered(toolCallId) ? cachedHoveredLine! : cachedLine];
|
|
462
|
+
},
|
|
463
|
+
invalidate() {},
|
|
464
|
+
};
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
renderToolSummary,
|
|
20
20
|
toolCallSummary,
|
|
21
21
|
} from "./names.ts";
|
|
22
|
-
import { showMoreHintText } from "./
|
|
22
|
+
import { showMoreHintText } from "./names.ts";
|
|
23
23
|
import { formatToolDuration, headTruncateToWidth, toolViewportWidth } from "./result.ts";
|
|
24
24
|
|
|
25
25
|
export const CODEMODE_TOOL_NAME = "codemode";
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const ANSI_SGR_PATTERN = /\x1b\[([0-9
|
|
1
|
+
const ANSI_SGR_PATTERN = /\x1b\[([0-9;]*)m/g;
|
|
2
2
|
const STYLE_RESET_PARAMS = [39, 22, 23, 24, 25, 27, 28, 29, 59] as const;
|
|
3
3
|
|
|
4
4
|
export { ANSI_SGR_PATTERN, STYLE_RESET_PARAMS };
|
|
@@ -88,35 +88,12 @@ export function stripBackgroundSgrParams(params: readonly number[]): number[] {
|
|
|
88
88
|
return sanitized;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
function isKeptColonSgr(token: string): boolean {
|
|
92
|
-
const parts = token.split(":");
|
|
93
|
-
if (parts[0] === "48") return false;
|
|
94
|
-
if (parts[0] !== "38") return true;
|
|
95
|
-
const channels =
|
|
96
|
-
parts[1] === "5" && parts.length === 3
|
|
97
|
-
? parts.slice(2)
|
|
98
|
-
: parts[1] === "2" && (parts.length === 5 || parts.length === 6)
|
|
99
|
-
? parts.slice(-3)
|
|
100
|
-
: undefined;
|
|
101
|
-
return !!channels?.every((c) => /^\d+$/.test(c) && Number(c) <= 255);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
91
|
export function filterSgrSequences(text: string, filter: (params: number[]) => number[]): string {
|
|
105
92
|
if (!text || !text.includes("\x1b[")) {
|
|
106
93
|
return text;
|
|
107
94
|
}
|
|
108
95
|
|
|
109
96
|
return text.replace(ANSI_SGR_PATTERN, (_sequence, rawParams: string) => {
|
|
110
|
-
if (rawParams.includes(":")) {
|
|
111
|
-
// 冒号形式 (ISO 8613-6):逐段处理;去掉 48:*,校验 38:*,其余原样保留
|
|
112
|
-
const kept = rawParams.split(";").flatMap((token) => {
|
|
113
|
-
if (!token.includes(":")) {
|
|
114
|
-
return token ? filter(toSgrParams(token)).map(String) : [];
|
|
115
|
-
}
|
|
116
|
-
return isKeptColonSgr(token) ? [token] : [];
|
|
117
|
-
});
|
|
118
|
-
return kept.length > 0 ? `\x1b[${kept.join(";")}m` : "";
|
|
119
|
-
}
|
|
120
97
|
const parsed = toSgrParams(rawParams);
|
|
121
98
|
if (parsed.length === 0) {
|
|
122
99
|
return "";
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Text, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { EditToolDetails } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { sanitizeToolResultText } from "../../../utils/tool-result-sanitize.ts";
|
|
4
|
-
import { showMoreHintText } from "../
|
|
4
|
+
import { showMoreHintText } from "../names.ts";
|
|
5
5
|
import {
|
|
6
6
|
getLineNumberWidth,
|
|
7
7
|
parseDiff,
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { DEFAULT_TOOL_DISPLAY_CONFIG, type ToolDisplayConfig } from "../../../config/config.ts";
|
|
3
|
-
import { showMoreHintText } from "../
|
|
3
|
+
import { showMoreHintText } from "../names.ts";
|
|
4
4
|
import {
|
|
5
5
|
buildCollapsedDiffHintText,
|
|
6
6
|
clampRenderedLineToWidth,
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts
CHANGED
|
@@ -9,7 +9,8 @@ export function splitWriteContentLines(content: string): string[] {
|
|
|
9
9
|
return [];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const normalized = content.replace(/\r/g, "");
|
|
13
|
+
const lines = normalized.split("\n");
|
|
13
14
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
14
15
|
lines.pop();
|
|
15
16
|
}
|