pi-cc-extensions 0.9.12 → 1.0.1
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.en.md +4 -1
- package/README.md +4 -1
- package/extensions/config/config.ts +21 -0
- package/extensions/config/panel.ts +68 -6
- package/extensions/feature/shell/footer.ts +54 -10
- package/extensions/feature/shell/token-rate.ts +192 -0
- package/extensions/feature/shell/working-message.ts +52 -65
- package/extensions/renderer/compact-mode.ts +108 -25
- package/extensions/renderer/default-mode.ts +152 -24
- package/extensions/renderer/index.ts +30 -1
- package/extensions/renderer/mouse/interaction.ts +22 -4
- package/extensions/renderer/mouse/sticky-prompt.ts +398 -0
- package/extensions/renderer/tool/ask-user-question.ts +173 -0
- package/extensions/renderer/tool/codemode.ts +4 -8
- package/extensions/renderer/tool/diff/diff-edit-render.ts +6 -6
- package/extensions/renderer/tool/diff/diff-presentation.ts +41 -0
- package/extensions/renderer/tool/diff/diff-renderer.ts +5 -1
- package/extensions/renderer/tool/diff/diff-write-render.ts +35 -9
- package/extensions/renderer/tool/grouping.ts +102 -24
- package/extensions/renderer/tool/names.ts +1 -1
- package/extensions/renderer/tool/result.ts +106 -15
- package/extensions/utils/format.ts +9 -0
- package/extensions/utils/patch-keys.ts +5 -0
- package/package.json +1 -4
- package/themes/cc-dark.json +1 -1
- package/themes/cc-light.json +1 -1
|
@@ -20,4 +20,8 @@ export { shouldHighlightCodeBlock } from "./diff-highlight.ts";
|
|
|
20
20
|
export type { DisplayConfigInput } from "./diff-component.ts";
|
|
21
21
|
export { isRichDiffComponent } from "./diff-component.ts";
|
|
22
22
|
export { countEditDiffStats, renderEditDiffResult } from "./diff-edit-render.ts";
|
|
23
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
countWriteDiffStats,
|
|
25
|
+
renderWriteDiffResult,
|
|
26
|
+
writeStatsInCollapsedBody,
|
|
27
|
+
} from "./diff-write-render.ts";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { ToolDisplayConfig } from "../../../config/config.ts";
|
|
2
3
|
import { sanitizeToolResultText } from "../../../utils/tool-result-sanitize.ts";
|
|
3
4
|
import { showMoreHintText } from "../show-more-hint.ts";
|
|
4
5
|
import { splitWriteContentLines } from "./diff-text.ts";
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
buildDiffSummaryText,
|
|
55
56
|
normalizeDiffRenderWidth,
|
|
56
57
|
resolveDiffPresentationMode,
|
|
58
|
+
styleDiffSummaryLine,
|
|
57
59
|
type DiffPresentationMode,
|
|
58
60
|
} from "./diff-presentation.ts";
|
|
59
61
|
|
|
@@ -290,13 +292,14 @@ function renderWriteOverwriteGuardRows(
|
|
|
290
292
|
return renderSingleDiffRow(buildWriteOverwriteGuardText(guard, width), "warning", width, theme);
|
|
291
293
|
}
|
|
292
294
|
|
|
293
|
-
/** Folded write with `writeDiffCollapsedLines: 0`: `↳ created · click to show more`. */
|
|
295
|
+
/** Folded write with `writeDiffCollapsedLines: 0`: `↳ diff +A -D · created · click to show more`. */
|
|
294
296
|
function renderWriteCollapsedHintLine(
|
|
295
297
|
wasOverwrite: boolean,
|
|
296
298
|
width: number,
|
|
297
299
|
theme: DiffTheme,
|
|
298
300
|
hovered: boolean,
|
|
299
301
|
headerLabel?: string,
|
|
302
|
+
stats?: { added: number; removed: number },
|
|
300
303
|
): string[] {
|
|
301
304
|
const safeWidth = normalizeDiffRenderWidth(width);
|
|
302
305
|
if (safeWidth === 0) {
|
|
@@ -304,7 +307,20 @@ function renderWriteCollapsedHintLine(
|
|
|
304
307
|
}
|
|
305
308
|
const actionLabel = headerLabel?.trim() || (wasOverwrite ? "overwritten" : "created");
|
|
306
309
|
const clickLabel = showMoreHintText();
|
|
307
|
-
const
|
|
310
|
+
const counts = stats ? `+${stats.added} -${stats.removed}` : "";
|
|
311
|
+
const candidates = stats
|
|
312
|
+
? [
|
|
313
|
+
`↳ diff ${counts} · ${actionLabel} · ${clickLabel}`,
|
|
314
|
+
`↳ diff ${counts} · ${clickLabel}`,
|
|
315
|
+
`↳ diff ${counts} · ${actionLabel}`,
|
|
316
|
+
`↳ diff ${counts}`,
|
|
317
|
+
`↳ ${counts} · ${clickLabel}`,
|
|
318
|
+
`↳ ${actionLabel} · ${clickLabel}`,
|
|
319
|
+
`↳ ${actionLabel}`,
|
|
320
|
+
actionLabel,
|
|
321
|
+
"…",
|
|
322
|
+
]
|
|
323
|
+
: [`↳ ${actionLabel} · ${clickLabel}`, `↳ ${actionLabel}`, actionLabel, "…"];
|
|
308
324
|
let text = candidates[candidates.length - 1] ?? "…";
|
|
309
325
|
for (const candidate of candidates) {
|
|
310
326
|
if (visibleWidth(candidate) <= safeWidth) {
|
|
@@ -315,16 +331,21 @@ function renderWriteCollapsedHintLine(
|
|
|
315
331
|
if (visibleWidth(text) > safeWidth) {
|
|
316
332
|
text = truncateToWidth(text, safeWidth, "");
|
|
317
333
|
}
|
|
318
|
-
const
|
|
319
|
-
const styled =
|
|
320
|
-
hovered && clickIndex >= 0
|
|
321
|
-
? theme.fg("muted", text.slice(0, clickIndex)) +
|
|
322
|
-
theme.fg("text", clickLabel) +
|
|
323
|
-
theme.fg("muted", text.slice(clickIndex + clickLabel.length))
|
|
324
|
-
: theme.fg("muted", text);
|
|
334
|
+
const styled = styleDiffSummaryLine(text, stats, clickLabel, theme, hovered);
|
|
325
335
|
return [clampDiffLineToWidth(styled, safeWidth)];
|
|
326
336
|
}
|
|
327
337
|
|
|
338
|
+
/**
|
|
339
|
+
* 折叠 0 行预览时计数由正文那行显示(`↳ diff +A -D · created · click to show more`),
|
|
340
|
+
* 标题不再重复。compact 与 default 两个模式的写标题共用这条口径。
|
|
341
|
+
*/
|
|
342
|
+
export function writeStatsInCollapsedBody(
|
|
343
|
+
expanded: boolean,
|
|
344
|
+
display: Pick<ToolDisplayConfig, "writeDiffCollapsedLines">,
|
|
345
|
+
): boolean {
|
|
346
|
+
return !expanded && resolveWriteCollapsedLimit(display) === 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
328
349
|
/**
|
|
329
350
|
* write 变更行统计(compact 单行用):复用当前 write diff 算法。
|
|
330
351
|
* 新文件 = 全部新内容为 added;覆盖已有文件用 LCS 精确统计。
|
|
@@ -413,6 +434,10 @@ export function renderWriteDiffResult(
|
|
|
413
434
|
const hint = cache.getHintLine();
|
|
414
435
|
return hint !== undefined && normalizeCollapsedHintLine(plainLine) === hint;
|
|
415
436
|
},
|
|
437
|
+
/** 折叠 0 行预览时正文那行自带 +A -D:标题不再重复计数。 */
|
|
438
|
+
bodyShowsStats(): boolean {
|
|
439
|
+
return writeStatsInCollapsedBody(options.expanded, resolveLiveDisplayConfig(config));
|
|
440
|
+
},
|
|
416
441
|
render(width: number): string[] {
|
|
417
442
|
// Live config: panel can change indicator/wrap/limits after this component is created.
|
|
418
443
|
const live = resolveLiveDisplayConfig(config);
|
|
@@ -445,6 +470,7 @@ export function renderWriteDiffResult(
|
|
|
445
470
|
theme,
|
|
446
471
|
hovered,
|
|
447
472
|
options.headerLabel,
|
|
473
|
+
countWriteDiffStats(content, options.previousContent, options.fileExistedBeforeWrite),
|
|
448
474
|
);
|
|
449
475
|
return cache.set(
|
|
450
476
|
safeWidth,
|
|
@@ -8,6 +8,12 @@ import {
|
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
9
|
import { config } from "../../config/config.ts";
|
|
10
10
|
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
11
|
+
import {
|
|
12
|
+
ASK_USER_QUESTION_TOOL_NAME,
|
|
13
|
+
askUserAnswerText,
|
|
14
|
+
askUserAnswers,
|
|
15
|
+
type AskUserAnswer,
|
|
16
|
+
} from "./ask-user-question.ts";
|
|
11
17
|
import {
|
|
12
18
|
CODEMODE_TOOL_NAME,
|
|
13
19
|
codemodeCalls,
|
|
@@ -23,10 +29,16 @@ import {
|
|
|
23
29
|
fitToolCallSummary,
|
|
24
30
|
humanizeToolLabel,
|
|
25
31
|
renderToolSummary,
|
|
32
|
+
SHELL_TOOL_NAMES,
|
|
26
33
|
toolCallSummary,
|
|
27
34
|
type ToolCallSummary,
|
|
28
35
|
} from "./names.ts";
|
|
29
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
formatToolDuration,
|
|
38
|
+
headTruncateToWidth,
|
|
39
|
+
isLiveToolCall,
|
|
40
|
+
toolViewportWidth,
|
|
41
|
+
} from "./result.ts";
|
|
30
42
|
import {
|
|
31
43
|
patchRegistry,
|
|
32
44
|
TOOL_GROUPING_GENERATION_KEY as GENERATION_KEY,
|
|
@@ -108,6 +120,12 @@ function statusIcon(value: ToolStatus): string {
|
|
|
108
120
|
return toolLoadingIcon();
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
/** 等待窗口或运行中的行:spinner 逐帧变化,分组卡要跟着跳帧。 */
|
|
124
|
+
function isAnimatingTool(tool: any): boolean {
|
|
125
|
+
if (status(tool) !== "pending") return false;
|
|
126
|
+
return Boolean(tool?.executionStarted) || isLiveToolCall(tool?.toolCallId);
|
|
127
|
+
}
|
|
128
|
+
|
|
111
129
|
function scheduleGroupAnimation(patch: Patch): void {
|
|
112
130
|
if (patch.animationTimer || !patch.active) return;
|
|
113
131
|
patch.animationTimer = setTimeout(() => {
|
|
@@ -115,11 +133,7 @@ function scheduleGroupAnimation(patch: Patch): void {
|
|
|
115
133
|
if (!patch.active) return;
|
|
116
134
|
let needsRender = false;
|
|
117
135
|
for (const group of patch.groups) {
|
|
118
|
-
if (
|
|
119
|
-
(group.children as any[]).some(
|
|
120
|
-
(tool) => tool?.executionStarted && status(tool) === "pending",
|
|
121
|
-
)
|
|
122
|
-
) {
|
|
136
|
+
if ((group.children as any[]).some(isAnimatingTool)) {
|
|
123
137
|
group.invalidate();
|
|
124
138
|
// 分组卡不是 ToolExecutionComponent:它的 invalidate() 不会请求渲染,
|
|
125
139
|
// 不补这一步 spinner 只在别人渲染时才跳帧(并行工具下表现为卡顿/冻结)。
|
|
@@ -203,6 +217,39 @@ function toolSummary(tool: any): ToolCallSummary {
|
|
|
203
217
|
* codemode 的折叠行:有结果就报该次调用的统计(与单卡汇总行同一套口径),
|
|
204
218
|
* 免得一张卡里几行长代码预览互相看不出区别。还没结果时退回代码预览。
|
|
205
219
|
*/
|
|
220
|
+
/**
|
|
221
|
+
* ask_user_question 的折叠行:有结果就报「问题 -> 回答」,
|
|
222
|
+
* 多题压成一行并标注剩余条数。还没结果时退回问题预览。
|
|
223
|
+
*/
|
|
224
|
+
function askUserResultSummary(tool: any): ToolCallSummary | undefined {
|
|
225
|
+
if (toolName(tool) !== ASK_USER_QUESTION_TOOL_NAME || !tool?.result) return undefined;
|
|
226
|
+
const answers = askUserAnswers(tool.result)
|
|
227
|
+
.map((answer: AskUserAnswer, index: number) => ({ answer, index }))
|
|
228
|
+
.sort((left, right) => {
|
|
229
|
+
const leftIndex = Number(left.answer.questionIndex);
|
|
230
|
+
const rightIndex = Number(right.answer.questionIndex);
|
|
231
|
+
const leftMissing = !Number.isFinite(leftIndex);
|
|
232
|
+
const rightMissing = !Number.isFinite(rightIndex);
|
|
233
|
+
if (leftMissing || rightMissing) {
|
|
234
|
+
if (leftMissing !== rightMissing) return leftMissing ? 1 : -1;
|
|
235
|
+
return left.index - right.index;
|
|
236
|
+
}
|
|
237
|
+
return leftIndex - rightIndex;
|
|
238
|
+
})
|
|
239
|
+
.map((entry) => entry.answer);
|
|
240
|
+
if (answers.length === 0) {
|
|
241
|
+
const label = tool.result?.details?.cancelled === true ? "Declined" : "Done";
|
|
242
|
+
return { main: `${toolTitle(tool)} ${label}`, detail: "" };
|
|
243
|
+
}
|
|
244
|
+
const first = answers[0]!;
|
|
245
|
+
const question = String(first.question ?? "").trim() || "Question";
|
|
246
|
+
const more = answers.length > 1 ? ` (+${answers.length - 1})` : "";
|
|
247
|
+
return {
|
|
248
|
+
main: `${toolTitle(tool)} ${question} -> ${askUserAnswerText(first)}${more}`,
|
|
249
|
+
detail: "",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
206
253
|
function codemodeResultSummary(tool: any): ToolCallSummary | undefined {
|
|
207
254
|
if (toolName(tool) !== CODEMODE_TOOL_NAME || !tool?.result) return undefined;
|
|
208
255
|
const parts = codemodeSummaryParts(
|
|
@@ -213,10 +260,36 @@ function codemodeResultSummary(tool: any): ToolCallSummary | undefined {
|
|
|
213
260
|
return { main: `${toolTitle(tool)} ${parts.join(" · ")}`, detail: "" };
|
|
214
261
|
}
|
|
215
262
|
|
|
263
|
+
/**
|
|
264
|
+
* 组内 shell 行的尾巴:只报耗时(`· 12.4s`)。输出和错误正文都在展开后看;
|
|
265
|
+
* 失败靠行首的 ✗,组头另有 failed 计数。
|
|
266
|
+
* 耗时取 pi 记在结果上的 durationMs(单卡渲染上下文里的同一个值),运行中还拿不到就不报。
|
|
267
|
+
*/
|
|
268
|
+
function shellResultTail(
|
|
269
|
+
tool: any,
|
|
270
|
+
fg: (color: string, text: string) => string,
|
|
271
|
+
toolStatus: ToolStatus,
|
|
272
|
+
): string {
|
|
273
|
+
if (!SHELL_TOOL_NAMES.has(toolName(tool)) || toolStatus === "pending" || !tool?.result) return "";
|
|
274
|
+
const durationMs = tool.result?.durationMs;
|
|
275
|
+
const duration =
|
|
276
|
+
typeof durationMs === "number" && Number.isFinite(durationMs) && durationMs >= 0
|
|
277
|
+
? formatToolDuration(durationMs)
|
|
278
|
+
: "";
|
|
279
|
+
return duration ? ` ${fg("dim", `· ${duration}`)}` : "";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 组头的工具列表:名称用与单组标题、子行同一套人性化写法,次数一律带上(各跑一次也报 ×1)。
|
|
284
|
+
* 同名(同显示标题)的合并成一项,避免 `MCP call×1, MCP call×1` 这种重复。
|
|
285
|
+
*/
|
|
216
286
|
function toolNameList(tools: any[]): string {
|
|
217
287
|
const counts = new Map<string, number>();
|
|
218
|
-
for (const tool of tools)
|
|
219
|
-
|
|
288
|
+
for (const tool of tools) {
|
|
289
|
+
const label = toolTitle(tool);
|
|
290
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
291
|
+
}
|
|
292
|
+
return [...counts].map(([label, count]) => `${label}×${count}`).join(", ");
|
|
220
293
|
}
|
|
221
294
|
|
|
222
295
|
let nextGroupId = 1;
|
|
@@ -389,23 +462,23 @@ export class ToolGroupComponent extends Container {
|
|
|
389
462
|
const names = new Set(this.children.map(toolName));
|
|
390
463
|
const label = names.size === 1 ? toolTitle(this.children[0]) : "Multiple Tools";
|
|
391
464
|
const overall: ToolStatus = counts.error ? "error" : counts.pending ? "pending" : "success";
|
|
392
|
-
if (
|
|
393
|
-
(this.children as any[]).some((tool) => tool?.executionStarted && status(tool) === "pending")
|
|
394
|
-
)
|
|
395
|
-
scheduleGroupAnimation(this.patch);
|
|
465
|
+
if ((this.children as any[]).some(isAnimatingTool)) scheduleGroupAnimation(this.patch);
|
|
396
466
|
const overallColor = overall === "pending" ? "accent" : overall;
|
|
397
|
-
const nameList = names.size > 1 ? ` ${fg("dim", `· ${toolNameList(this.children)}`)}` : "";
|
|
398
467
|
// 圆点保持 dim;hover 只高亮可点击文字。
|
|
399
468
|
const hintText = this._expanded ? collapseHintText() : showMoreHintText();
|
|
400
469
|
const hint = `${fg("dim", "·")} ${fg(this.hintHovered ? "text" : "dim", hintText)}`;
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
470
|
+
const rowWidth = toolViewportWidth(width);
|
|
471
|
+
// 工具列表让位给展开提示:提示被截断的话,鼠标层要在行尾找 `click to show more`
|
|
472
|
+
// 就找不到,整张卡在窄宽度下点不开。名字截断后子行里仍能看全。
|
|
473
|
+
const head = ` ${fg(overallColor, "●")} ${label}: ${countText}`;
|
|
474
|
+
const tail = ` ${hint}`;
|
|
475
|
+
const listRoom = rowWidth - visibleWidth(head) - visibleWidth(tail) - 3;
|
|
476
|
+
// 连一个工具名都放不下就不放列表:退化成 `· …` 只会把行撑爆。
|
|
477
|
+
const nameList =
|
|
478
|
+
names.size > 1 && listRoom >= 4
|
|
479
|
+
? ` ${fg("dim", `· ${headTruncateToWidth(toolNameList(this.children), listRoom)}`)}`
|
|
480
|
+
: "";
|
|
481
|
+
const lines = ["", truncateToWidth(`${head}${nameList}${tail}`, rowWidth, "")];
|
|
409
482
|
const total = this.children.length;
|
|
410
483
|
const childPaints = this._expanded
|
|
411
484
|
? (this.children as any[]).map((tool) => tool.render?.(Math.max(1, width - 2)))
|
|
@@ -433,15 +506,20 @@ export class ToolGroupComponent extends Container {
|
|
|
433
506
|
const branch = index === total - 1 ? "└" : "├";
|
|
434
507
|
const continuation = index === total - 1 ? " " : "│ ";
|
|
435
508
|
if (!this._expanded) {
|
|
436
|
-
const summary =
|
|
509
|
+
const summary =
|
|
510
|
+
askUserResultSummary(tool) ?? codemodeResultSummary(tool) ?? toolSummary(tool);
|
|
437
511
|
const prefix = ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `;
|
|
438
512
|
const detail = fg("dim", summary.detail);
|
|
513
|
+
const result = shellResultTail(tool, fg, toolStatus);
|
|
439
514
|
// 与单工具卡标题同宽,宽屏右侧留白一致
|
|
440
515
|
const rowWidth = toolViewportWidth(width);
|
|
441
|
-
const mainWidth = Math.max(
|
|
516
|
+
const mainWidth = Math.max(
|
|
517
|
+
0,
|
|
518
|
+
rowWidth - visibleWidth(prefix) - visibleWidth(detail) - visibleWidth(result),
|
|
519
|
+
);
|
|
442
520
|
lines.push(
|
|
443
521
|
truncateToWidth(
|
|
444
|
-
`${prefix}${renderToolSummary(summary, mainWidth, fg)}${detail}`,
|
|
522
|
+
`${prefix}${renderToolSummary(summary, mainWidth, fg)}${detail}${result}`,
|
|
445
523
|
rowWidth,
|
|
446
524
|
"",
|
|
447
525
|
),
|
|
@@ -157,7 +157,7 @@ function pathSummary(
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/** 入参是 shell 命令的工具。 */
|
|
160
|
-
const SHELL_TOOL_NAMES = new Set(["bash", "powershell", "pwsh", "sh", "zsh", "cmd"]);
|
|
160
|
+
export const SHELL_TOOL_NAMES = new Set(["bash", "powershell", "pwsh", "sh", "zsh", "cmd"]);
|
|
161
161
|
|
|
162
162
|
/** 内置检索工具:pattern 缺失(无效调用)时保留 "…" 占位。 */
|
|
163
163
|
const SEARCH_TOOL_NAMES = new Set(["grep", "find"]);
|
|
@@ -6,6 +6,7 @@ import { showMoreHintText } from "./show-more-hint.ts";
|
|
|
6
6
|
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
7
7
|
import { getToolMouseTui } from "../mouse/scroll.ts";
|
|
8
8
|
import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
|
|
9
|
+
import { oneLine } from "../../utils/format.ts";
|
|
9
10
|
|
|
10
11
|
const TOOL_VIEWPORT_WIDTH_RATIO = 0.8;
|
|
11
12
|
/** 宽屏右侧留白上限:比例留白超过这么多列时改用固定留白。 */
|
|
@@ -75,10 +76,46 @@ export function countLines(text: string): number {
|
|
|
75
76
|
.filter((line) => line.trim().length > 0).length;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* 折叠行的结果文案:`N lines returned`(read 说 loaded)、`Done`、失败时首行错误。
|
|
81
|
+
* 单卡折叠行与工具组的子行共用这一处口径;TaskList 的树形摘要不走这里,由调用方先判。
|
|
82
|
+
*/
|
|
83
|
+
export function collapsedResultSummary(options: {
|
|
84
|
+
result: any;
|
|
85
|
+
toolName: string;
|
|
86
|
+
text: string;
|
|
87
|
+
isError: boolean;
|
|
88
|
+
}): string {
|
|
89
|
+
const { result, toolName, text, isError } = options;
|
|
90
|
+
if (isError) {
|
|
91
|
+
// 72 与原 result.ts 默认一致,保持错误摘要截断宽度。
|
|
92
|
+
return text ? oneLine(text, 72) : "Failed";
|
|
93
|
+
}
|
|
94
|
+
const outputLines = outputLineCount(result) || countLines(text);
|
|
95
|
+
if (!outputLines) return "Done";
|
|
96
|
+
const action = toolName === "read" ? "loaded" : "returned";
|
|
97
|
+
return `${outputLines} ${outputLines === 1 ? "line" : "lines"} ${action}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
78
100
|
function hasExpandableResult(text: string): boolean {
|
|
79
101
|
return countLines(text) > 1;
|
|
80
102
|
}
|
|
81
103
|
|
|
104
|
+
/**
|
|
105
|
+
* 工具耗时:不到 1 秒报毫秒,分钟级起带上分/时。
|
|
106
|
+
* 只报到秒的话,几分钟的长命令会显示成 `300.0s`。
|
|
107
|
+
*/
|
|
108
|
+
export function formatToolDuration(ms: number): string {
|
|
109
|
+
const seconds = ms / 1000;
|
|
110
|
+
if (seconds < 1) return `${Math.round(ms)}ms`;
|
|
111
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
112
|
+
const totalSeconds = Math.floor(seconds);
|
|
113
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
114
|
+
const remainder = totalSeconds % 60;
|
|
115
|
+
if (minutes < 60) return `${minutes}m ${remainder}s`;
|
|
116
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m ${remainder}s`;
|
|
117
|
+
}
|
|
118
|
+
|
|
82
119
|
const activeAnimationContexts = new Set<any>();
|
|
83
120
|
let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
84
121
|
|
|
@@ -135,6 +172,36 @@ export function scheduleAnimation(
|
|
|
135
172
|
}
|
|
136
173
|
}
|
|
137
174
|
|
|
175
|
+
/**
|
|
176
|
+
* 本回合流式消息里出现过的工具调用。Pi 要等整条 assistant 消息结束才派发执行,
|
|
177
|
+
* 这段窗口工具行已经在等启动,spinner 该逐帧转。
|
|
178
|
+
* 恢复或翻历史重建的旧行不在这个集合里,保持静止。
|
|
179
|
+
*/
|
|
180
|
+
const liveToolCalls = new Set<string>();
|
|
181
|
+
|
|
182
|
+
/** 收下当前流式 assistant 消息里的工具调用 id。 */
|
|
183
|
+
export function noteStreamingToolCalls(message: any): void {
|
|
184
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
185
|
+
for (const block of content) {
|
|
186
|
+
const id = block?.type === "toolCall" ? block.id : undefined;
|
|
187
|
+
if (typeof id === "string" && id) liveToolCalls.add(id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 工具收尾后移出等待窗口。 */
|
|
192
|
+
export function retireLiveToolCall(toolCallId: unknown): void {
|
|
193
|
+
if (typeof toolCallId === "string") liveToolCalls.delete(toolCallId);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function clearLiveToolCalls(): void {
|
|
197
|
+
liveToolCalls.clear();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 工具行是否处于等待窗口(流式声明中或等待派发)。 */
|
|
201
|
+
export function isLiveToolCall(toolCallId: unknown): boolean {
|
|
202
|
+
return typeof toolCallId === "string" && liveToolCalls.has(toolCallId);
|
|
203
|
+
}
|
|
204
|
+
|
|
138
205
|
export function pendingIcon(_name: string): string {
|
|
139
206
|
return toolLoadingIcon();
|
|
140
207
|
}
|
|
@@ -231,7 +298,7 @@ export class ExpandedToolResultText {
|
|
|
231
298
|
}
|
|
232
299
|
}
|
|
233
300
|
|
|
234
|
-
/** 截断体末行 `…
|
|
301
|
+
/** 截断体末行 `… N more lines` 旁的展开提示,点击打开全量预览。 */
|
|
235
302
|
export const SHOW_MORE_LABEL = "· click to show more";
|
|
236
303
|
|
|
237
304
|
export type ToolIoSection = "input" | "output";
|
|
@@ -243,14 +310,12 @@ const EXPANDED_TOOL_IO_VIEW_GENERATION = Symbol("ccstyle-expanded-tool-io-view")
|
|
|
243
310
|
/**
|
|
244
311
|
* Expanded tool body with clear Input / Output sections (Grok Build–style).
|
|
245
312
|
*
|
|
246
|
-
* Visual frame
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* flushLeft=true(仅 mode=on 展开卡):去掉树线前导空格,由外层 Box(1,1) 提供 1 格 padding。
|
|
313
|
+
* Visual frame(flushLeft=true:树线的前导空格交给外层 Box(1,1),├/└ 与标题行的勾同列):
|
|
314
|
+
* ├ Input click to show more
|
|
315
|
+
* │ path: src/a.ts
|
|
316
|
+
* │
|
|
317
|
+
* └ Output click to show more
|
|
318
|
+
* result line…
|
|
254
319
|
*
|
|
255
320
|
* Reused across re-renders via context.lastComponent when possible.
|
|
256
321
|
*/
|
|
@@ -281,8 +346,10 @@ export class ExpandedToolIoView {
|
|
|
281
346
|
}
|
|
282
347
|
| undefined;
|
|
283
348
|
|
|
284
|
-
/** flushLeft
|
|
349
|
+
/** flushLeft:贴左渲染(外层 Box 负责 1 格 pad);默认 false 保留 1 格前导空格。 */
|
|
285
350
|
private flushLeft: boolean;
|
|
351
|
+
/** 尾部附注(shell 的耗时)。挂到 Output 的 show-more 行上,没有那一行时单独成行。 */
|
|
352
|
+
private footer: string | undefined;
|
|
286
353
|
|
|
287
354
|
constructor(
|
|
288
355
|
theme: any,
|
|
@@ -398,6 +465,13 @@ export class ExpandedToolIoView {
|
|
|
398
465
|
this.invalidate();
|
|
399
466
|
}
|
|
400
467
|
|
|
468
|
+
/** 尾部附注(耗时等)。挂在 show-more 行上;无 show-more 行时单独成行。 */
|
|
469
|
+
setFooter(text: string | undefined): void {
|
|
470
|
+
if (this.footer === text) return;
|
|
471
|
+
this.footer = text;
|
|
472
|
+
this.invalidate();
|
|
473
|
+
}
|
|
474
|
+
|
|
401
475
|
getHoveredSection(): ToolIoSection | null {
|
|
402
476
|
return this.hoveredSection;
|
|
403
477
|
}
|
|
@@ -405,7 +479,7 @@ export class ExpandedToolIoView {
|
|
|
405
479
|
/** True when the plain truncation footer carries show-more. Input 续行带 │,Output 不带。 */
|
|
406
480
|
matchShowMoreLine(plainLine: string): ToolIoSection | null {
|
|
407
481
|
const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
|
|
408
|
-
if (!line.includes(` · ${showMoreHintText()}`) ||
|
|
482
|
+
if (!line.includes(` · ${showMoreHintText()}`) || !/\d+ more lines/.test(line)) return null;
|
|
409
483
|
if (this.truncated.input && line.includes("│")) return "input";
|
|
410
484
|
if (this.truncated.output) return "output";
|
|
411
485
|
return null;
|
|
@@ -442,7 +516,7 @@ export class ExpandedToolIoView {
|
|
|
442
516
|
|
|
443
517
|
const theme = this.theme;
|
|
444
518
|
const safeWidth = Math.max(1, Math.floor(width));
|
|
445
|
-
// flushLeft:贴左(外层 Box 负责 1 格 pad);否则保留 1
|
|
519
|
+
// flushLeft:贴左(外层 Box 负责 1 格 pad);否则保留 1 格前导空格
|
|
446
520
|
const lead = this.flushLeft ? "" : " ";
|
|
447
521
|
const rail = `${lead}│ `;
|
|
448
522
|
const railWidth = visibleWidth(rail);
|
|
@@ -474,7 +548,14 @@ export class ExpandedToolIoView {
|
|
|
474
548
|
|
|
475
549
|
const pushBody = (
|
|
476
550
|
body: string,
|
|
477
|
-
opts: {
|
|
551
|
+
opts: {
|
|
552
|
+
input?: boolean;
|
|
553
|
+
limit: number;
|
|
554
|
+
continued?: boolean;
|
|
555
|
+
section: ToolIoSection;
|
|
556
|
+
/** 挂在 show-more 行上的附注(shell 耗时),插在展开提示前。 */
|
|
557
|
+
footerNote?: string;
|
|
558
|
+
},
|
|
478
559
|
): boolean /* truncated */ => {
|
|
479
560
|
const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
480
561
|
if (!raw.trim()) {
|
|
@@ -502,6 +583,8 @@ export class ExpandedToolIoView {
|
|
|
502
583
|
if (truncated) {
|
|
503
584
|
const hidden = Math.max(0, wrapped.length - visible.length);
|
|
504
585
|
if (hidden > 0) {
|
|
586
|
+
// 省略号已经表达了「还有」,计数前不再加 +。
|
|
587
|
+
const note = opts.footerNote ? theme.fg("dim", ` · ${opts.footerNote}`) : "";
|
|
505
588
|
// hover 只高亮文字,圆点保持 dim(与 group hint 一致)。
|
|
506
589
|
const more =
|
|
507
590
|
theme.fg("dim", " ·") +
|
|
@@ -509,7 +592,7 @@ export class ExpandedToolIoView {
|
|
|
509
592
|
this.hoveredSection === opts.section ? "text" : "dim",
|
|
510
593
|
` ${showMoreHintText()}`,
|
|
511
594
|
);
|
|
512
|
-
pushRailLine(theme.fg("dim", `…
|
|
595
|
+
pushRailLine(theme.fg("dim", `… ${hidden} more lines`) + note + more, opts.continued);
|
|
513
596
|
this.showMoreHeaderRows[opts.section] = lines.length - 1;
|
|
514
597
|
}
|
|
515
598
|
}
|
|
@@ -556,6 +639,7 @@ export class ExpandedToolIoView {
|
|
|
556
639
|
limit: this.maxOutputLines,
|
|
557
640
|
continued: false,
|
|
558
641
|
section: "output",
|
|
642
|
+
footerNote: this.footer,
|
|
559
643
|
});
|
|
560
644
|
} else {
|
|
561
645
|
this.truncated.output = outputWouldTruncate;
|
|
@@ -564,9 +648,16 @@ export class ExpandedToolIoView {
|
|
|
564
648
|
limit: this.maxOutputLines,
|
|
565
649
|
continued: false,
|
|
566
650
|
section: "output",
|
|
651
|
+
footerNote: this.footer,
|
|
567
652
|
});
|
|
568
653
|
}
|
|
569
654
|
|
|
655
|
+
// 耗时挂在 Output 的 show-more 行上(与折叠态一样,提示留在行尾);
|
|
656
|
+
// 没截断就没有那一行,才单独成行。放在最后追加,前面记下的行号不受影响。
|
|
657
|
+
if (this.footer && this.showMoreHeaderRows.output === undefined) {
|
|
658
|
+
pushRailLine(theme.fg("muted", this.footer), false);
|
|
659
|
+
}
|
|
660
|
+
|
|
570
661
|
this.cachedWidth = width;
|
|
571
662
|
this.cachedLines = lines;
|
|
572
663
|
return withIoViewMarkers(this, lines);
|
|
@@ -830,7 +921,7 @@ export function renderExpandedToolResult(
|
|
|
830
921
|
lastComponent?: unknown,
|
|
831
922
|
args?: unknown,
|
|
832
923
|
context?: any,
|
|
833
|
-
/**
|
|
924
|
+
/** 贴左渲染:树线的前导空格交给外层 Box(1,1) */
|
|
834
925
|
flushLeft = false,
|
|
835
926
|
): ExpandedToolIoView | ExpandedToolResultText | Text {
|
|
836
927
|
const { text: inputBody, codeBlock } = formatToolInputBody(args);
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
|
|
3
3
|
|
|
4
|
+
/** token/计数缩写:<1k 原数,<1M 用 k,≥1M 用 M。footer 与 working message 共用。 */
|
|
5
|
+
export function formatTokenCount(value: number): string {
|
|
6
|
+
if (value < 1000) return `${value}`;
|
|
7
|
+
if (value < 10_000) return `${(value / 1000).toFixed(1)}k`;
|
|
8
|
+
if (value < 1_000_000) return `${Math.round(value / 1000)}k`;
|
|
9
|
+
if (value < 10_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
|
|
10
|
+
return `${Math.round(value / 1_000_000)}M`;
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
/** 毫秒 → "1h 2m 3s"/"2m 3s"/"3s";低于 1 秒返回 ""(省略)。 */
|
|
5
14
|
export function formatDuration(ms: number): string {
|
|
6
15
|
const totalSec = Math.floor(ms / 1000);
|
|
@@ -43,6 +43,11 @@ export const TOOL_GROUPING_GENERATION_KEY = Symbol.for("pi.ccstyle.tool-grouping
|
|
|
43
43
|
// ── regular 主屏渲染补丁 ──
|
|
44
44
|
export const MAIN_SCREEN_DO_RENDER_PATCH = Symbol.for("pi.ccstyle.main-screen-do-render-patch");
|
|
45
45
|
|
|
46
|
+
// ── fullscreen 粘性用户消息头 ──
|
|
47
|
+
export const STICKY_PROMPT_MARKER_PATCH = Symbol.for("pi.ccstyle.sticky-prompt-marker-patch");
|
|
48
|
+
export const STICKY_PROMPT_FRAME_PATCH = Symbol.for("pi.ccstyle.sticky-prompt-frame-patch");
|
|
49
|
+
export const STICKY_PROMPT_STATE_SLOT = Symbol.for("pi.ccstyle.sticky-prompt-state");
|
|
50
|
+
|
|
46
51
|
// ── 鼠标交互 ──
|
|
47
52
|
export const TOOL_MOUSE_OWNER_KEY = Symbol.for("pi.ccstyle.tool-mouse-owner");
|
|
48
53
|
export const TOOL_MOUSE_TUI_SLOT = Symbol.for("pi.ccstyle.tool-mouse-tui");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Claude Code-style TUI output with some personal touches and handy utilities.",
|
|
6
6
|
"type": "module",
|
|
@@ -65,9 +65,6 @@
|
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@biomejs/biome": "2.5.5",
|
|
68
|
-
"@earendil-works/pi-ai": "0.99.1",
|
|
69
|
-
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
70
|
-
"@earendil-works/pi-tui": "0.99.1",
|
|
71
68
|
"@types/node": "^26.1.1",
|
|
72
69
|
"typescript": "^5.9.3"
|
|
73
70
|
}
|
package/themes/cc-dark.json
CHANGED