pi-cc-extensions 0.9.9 → 0.9.11
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 +49 -50
- package/README.md +7 -8
- package/extensions/config/config.ts +48 -8
- package/extensions/config/panel.ts +158 -30
- package/extensions/feature/compact-thinking.ts +19 -16
- package/extensions/feature/shell/footer-currency.ts +29 -0
- package/extensions/feature/shell/footer.ts +52 -7
- package/extensions/feature/shell/mcp-chip.ts +136 -0
- package/extensions/renderer/compact-mode.ts +78 -14
- package/extensions/renderer/default-mode.ts +51 -13
- package/extensions/renderer/mouse/interaction.ts +32 -21
- package/extensions/renderer/tool/codemode.ts +306 -0
- package/extensions/renderer/tool/diff/diff-header.ts +1 -1
- package/extensions/renderer/tool/diff/diff-highlight.ts +36 -0
- package/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
- package/extensions/renderer/tool/diff/diff-write-render.ts +2 -2
- package/extensions/renderer/tool/diff/line-width-safety.ts +6 -5
- package/extensions/renderer/tool/grouping.ts +31 -6
- package/extensions/renderer/tool/message-display.ts +3 -3
- package/extensions/renderer/tool/names.ts +3 -0
- package/extensions/renderer/tool/result.ts +173 -36
- package/extensions/utils/ansi-text.ts +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
2
|
import { inspect } from "node:util";
|
|
3
3
|
import { config } from "../../config/config.ts";
|
|
4
|
+
import { createCodeBlockHighlighter } from "./diff/diff-highlight.ts";
|
|
4
5
|
import { showMoreHintText } from "./show-more-hint.ts";
|
|
5
6
|
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
6
7
|
import { getToolMouseTui } from "../mouse/scroll.ts";
|
|
@@ -231,7 +232,7 @@ export class ExpandedToolResultText {
|
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
/** 截断体末行 `… +N more lines` 旁的展开提示,点击打开全量预览。 */
|
|
234
|
-
export const SHOW_MORE_LABEL = "
|
|
235
|
+
export const SHOW_MORE_LABEL = "· click to show more";
|
|
235
236
|
|
|
236
237
|
export type ToolIoSection = "input" | "output";
|
|
237
238
|
|
|
@@ -268,6 +269,17 @@ export class ExpandedToolIoView {
|
|
|
268
269
|
private truncated: { input: boolean; output: boolean } = { input: false, output: false };
|
|
269
270
|
/** 0-based header line indexes that carry show-more after last render. */
|
|
270
271
|
private showMoreHeaderRows: { input?: number; output?: number } = {};
|
|
272
|
+
/** Input 里的代码块(codemode 的 `code`):着色行覆盖默认 dim/muted 样式。 */
|
|
273
|
+
private inputCode?:
|
|
274
|
+
| {
|
|
275
|
+
startLine: number;
|
|
276
|
+
language: string;
|
|
277
|
+
standalone: boolean;
|
|
278
|
+
code: string;
|
|
279
|
+
theme: unknown;
|
|
280
|
+
rows: () => string[];
|
|
281
|
+
}
|
|
282
|
+
| undefined;
|
|
271
283
|
|
|
272
284
|
/** flushLeft:贴左渲染(mode=on 展开卡);默认 false 保留前导空格(compact 等共用路径)。 */
|
|
273
285
|
private flushLeft: boolean;
|
|
@@ -325,6 +337,57 @@ export class ExpandedToolIoView {
|
|
|
325
337
|
return this.inputBody;
|
|
326
338
|
}
|
|
327
339
|
|
|
340
|
+
/**
|
|
341
|
+
* 登记 Input 里的代码块:同步 cli-highlight 打底,shiki 异步升级,落地后
|
|
342
|
+
* 经 invalidate 重画。同一份代码与主题只建一次(视图按宽度缓存行)。
|
|
343
|
+
*/
|
|
344
|
+
setInputCode(block: InputCodeBlock | undefined): void {
|
|
345
|
+
if (!block) {
|
|
346
|
+
if (this.inputCode) {
|
|
347
|
+
this.inputCode = undefined;
|
|
348
|
+
this.invalidate();
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (this.inputCode?.code === block.code && this.inputCode.theme === this.theme) {
|
|
353
|
+
this.inputCode.startLine = block.startLine;
|
|
354
|
+
this.inputCode.standalone = block.standalone;
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.inputCode = {
|
|
358
|
+
startLine: block.startLine,
|
|
359
|
+
language: block.language,
|
|
360
|
+
standalone: block.standalone,
|
|
361
|
+
code: block.code,
|
|
362
|
+
theme: this.theme,
|
|
363
|
+
rows: createCodeBlockHighlighter(block.code, block.language, this.theme, () => {
|
|
364
|
+
// 视图自己的行缓存 + 外层工具卡(清 paint 缓存并 requestRender)
|
|
365
|
+
this.invalidate();
|
|
366
|
+
invalidateIoView(this);
|
|
367
|
+
}),
|
|
368
|
+
};
|
|
369
|
+
this.invalidate();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 整段 Input 就是代码块时给出语言与代码,供全量预览包一层围栏(弹框按 Markdown
|
|
374
|
+
* 渲染,围栏代码块由 pi 的 highlightCode 着色)。混了别的字段时返回 undefined。
|
|
375
|
+
*/
|
|
376
|
+
getInputCodeFence(): { language: string; code: string } | undefined {
|
|
377
|
+
const block = this.inputCode;
|
|
378
|
+
return block?.standalone ? { language: block.language, code: block.code } : undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** 代码块里某一源行的着色内容(已剥掉行首缩进);不在块内时为 undefined。 */
|
|
382
|
+
private inputCodeLine(sourceIndex: number): string | undefined {
|
|
383
|
+
const block = this.inputCode;
|
|
384
|
+
if (!block) return undefined;
|
|
385
|
+
const offset = sourceIndex - block.startLine;
|
|
386
|
+
if (offset < 0) return undefined;
|
|
387
|
+
const rows = block.rows();
|
|
388
|
+
return offset < rows.length ? rows[offset] : undefined;
|
|
389
|
+
}
|
|
390
|
+
|
|
328
391
|
getOutputBody(): string {
|
|
329
392
|
return this.outputBody.trim() ? this.outputBody : "Done";
|
|
330
393
|
}
|
|
@@ -342,7 +405,7 @@ export class ExpandedToolIoView {
|
|
|
342
405
|
/** True when the plain truncation footer carries show-more. Input 续行带 │,Output 不带。 */
|
|
343
406
|
matchShowMoreLine(plainLine: string): ToolIoSection | null {
|
|
344
407
|
const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
|
|
345
|
-
if (!line.includes(`
|
|
408
|
+
if (!line.includes(` · ${showMoreHintText()}`) || !/\+\d+ more lines/.test(line)) return null;
|
|
346
409
|
if (this.truncated.input && line.includes("│")) return "input";
|
|
347
410
|
if (this.truncated.output) return "output";
|
|
348
411
|
return null;
|
|
@@ -363,7 +426,7 @@ export class ExpandedToolIoView {
|
|
|
363
426
|
/** Column range (1-based, visible cells) of show-more on a rendered header, if present. */
|
|
364
427
|
showMoreHitbox(plainLine: string): { startCol: number; endCol: number } | null {
|
|
365
428
|
const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
|
|
366
|
-
const label = `
|
|
429
|
+
const label = ` · ${showMoreHintText()}`;
|
|
367
430
|
const idx = line.lastIndexOf(label);
|
|
368
431
|
if (idx < 0) return null;
|
|
369
432
|
const before = line.slice(0, idx);
|
|
@@ -409,14 +472,6 @@ export class ExpandedToolIoView {
|
|
|
409
472
|
lines.push(truncateToWidth(theme.fg("dim", `${lead}│`), safeWidth, ""));
|
|
410
473
|
};
|
|
411
474
|
|
|
412
|
-
/** Style `key: value` input rows — dim keys, readable values. */
|
|
413
|
-
const styleInputLine = (rawLine: string): string => {
|
|
414
|
-
const match = rawLine.match(/^([A-Za-z_][\w.-]*)(:\s*)(.*)$/);
|
|
415
|
-
if (!match) return theme.fg("muted", rawLine);
|
|
416
|
-
const [, key, sep, rest] = match;
|
|
417
|
-
return theme.fg("dim", key + sep) + theme.fg("muted", rest ?? "");
|
|
418
|
-
};
|
|
419
|
-
|
|
420
475
|
const pushBody = (
|
|
421
476
|
body: string,
|
|
422
477
|
opts: { input?: boolean; limit: number; continued?: boolean; section: ToolIoSection },
|
|
@@ -428,8 +483,13 @@ export class ExpandedToolIoView {
|
|
|
428
483
|
}
|
|
429
484
|
const sourceLines = raw.split("\n");
|
|
430
485
|
const wrapped: string[] = [];
|
|
431
|
-
for (const source of sourceLines) {
|
|
432
|
-
|
|
486
|
+
for (const [index, source] of sourceLines.entries()) {
|
|
487
|
+
if (opts.input) {
|
|
488
|
+
// 代码块的行用着色版本(已剥缩进),其余走默认 dim/muted
|
|
489
|
+
wrapped.push(...wrapInputLine(source, contentWidth, theme, this.inputCodeLine(index)));
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const styled = theme.fg(bodyColor, source);
|
|
433
493
|
const parts = wrapTextWithAnsi(styled, contentWidth);
|
|
434
494
|
if (parts.length === 0) wrapped.push(styled);
|
|
435
495
|
else wrapped.push(...parts);
|
|
@@ -444,7 +504,7 @@ export class ExpandedToolIoView {
|
|
|
444
504
|
if (hidden > 0) {
|
|
445
505
|
// hover 只高亮文字,圆点保持 dim(与 group hint 一致)。
|
|
446
506
|
const more =
|
|
447
|
-
theme.fg("dim", "
|
|
507
|
+
theme.fg("dim", " ·") +
|
|
448
508
|
theme.fg(
|
|
449
509
|
this.hoveredSection === opts.section ? "text" : "dim",
|
|
450
510
|
` ${showMoreHintText()}`,
|
|
@@ -462,7 +522,15 @@ export class ExpandedToolIoView {
|
|
|
462
522
|
// Decide show-more from the same truncation rules as pushBody.
|
|
463
523
|
const inputWouldTruncate =
|
|
464
524
|
hasInput &&
|
|
465
|
-
bodyExceedsLineLimit(
|
|
525
|
+
bodyExceedsLineLimit(
|
|
526
|
+
this.inputBody,
|
|
527
|
+
this.maxInputLines,
|
|
528
|
+
contentWidth,
|
|
529
|
+
true,
|
|
530
|
+
theme,
|
|
531
|
+
bodyColor,
|
|
532
|
+
(inputIndex: number) => this.inputCodeLine(inputIndex),
|
|
533
|
+
);
|
|
466
534
|
const outputWouldTruncate = bodyExceedsLineLimit(
|
|
467
535
|
outputText,
|
|
468
536
|
this.maxOutputLines,
|
|
@@ -522,6 +590,40 @@ export function isExpandedToolIoView(value: unknown): value is ExpandedToolIoVie
|
|
|
522
590
|
);
|
|
523
591
|
}
|
|
524
592
|
|
|
593
|
+
/** Input 行着色:`key: value` 拆成 dim 键 + muted 值,其余整行 muted。 */
|
|
594
|
+
function styleInputSourceLine(line: string, theme: any): string {
|
|
595
|
+
const match = line.match(/^([A-Za-z_][\w.-]*)(:\s*)(.*)$/);
|
|
596
|
+
if (!match) return theme.fg("muted", line);
|
|
597
|
+
const [, key, sep, rest] = match;
|
|
598
|
+
return theme.fg("dim", key + sep) + theme.fg("muted", rest ?? "");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* 折一行 Input 源码。不带 styledBody 时与改动前完全一致(其他工具不受影响);
|
|
603
|
+
* 代码块(styledBody = 已着色且已剥缩进的行)续行跟着源码前导缩进:顶到 rail
|
|
604
|
+
* 会被读成新的一行,所以缩进先剥掉、折行宽度相应扣减,再给每一行加回去。
|
|
605
|
+
* 行数判断(bodyExceedsLineLimit)与实际渲染共用这一套,展开提示才对得上。
|
|
606
|
+
*/
|
|
607
|
+
function wrapInputLine(
|
|
608
|
+
source: string,
|
|
609
|
+
contentWidth: number,
|
|
610
|
+
theme: any,
|
|
611
|
+
styledBody?: string,
|
|
612
|
+
): string[] {
|
|
613
|
+
if (styledBody === undefined) {
|
|
614
|
+
const styled = styleInputSourceLine(source, theme);
|
|
615
|
+
const parts = wrapTextWithAnsi(styled, contentWidth);
|
|
616
|
+
return parts.length ? parts : [styled];
|
|
617
|
+
}
|
|
618
|
+
const lead = /^[ \t]*/.exec(source)?.[0] ?? "";
|
|
619
|
+
const leadWidth = visibleWidth(lead);
|
|
620
|
+
const parts = wrapTextWithAnsi(styledBody, Math.max(1, contentWidth - leadWidth));
|
|
621
|
+
const rows = parts.length ? parts : [styledBody];
|
|
622
|
+
if (!leadWidth) return rows;
|
|
623
|
+
const prefix = theme.fg("muted", lead);
|
|
624
|
+
return rows.map((row) => prefix + row);
|
|
625
|
+
}
|
|
626
|
+
|
|
525
627
|
/** True when body needs truncation at the given line limit (source lines or wrapped rows). */
|
|
526
628
|
function bodyExceedsLineLimit(
|
|
527
629
|
body: string,
|
|
@@ -530,23 +632,18 @@ function bodyExceedsLineLimit(
|
|
|
530
632
|
asInput: boolean,
|
|
531
633
|
theme: any,
|
|
532
634
|
bodyColor = "toolOutput",
|
|
635
|
+
/** 代码块的源行取着色内容,行数判断与实际渲染保持同一套规则。 */
|
|
636
|
+
codeLine?: (sourceIndex: number) => string | undefined,
|
|
533
637
|
): boolean {
|
|
534
638
|
const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
535
639
|
if (!raw.trim()) return false;
|
|
536
640
|
const sourceLines = raw.split("\n");
|
|
537
641
|
if (sourceLines.length > limit) return true;
|
|
538
642
|
let total = 0;
|
|
539
|
-
for (const source of sourceLines) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
styled = match
|
|
544
|
-
? theme.fg("dim", match[1] + match[2]) + theme.fg("muted", match[3] ?? "")
|
|
545
|
-
: theme.fg("muted", source);
|
|
546
|
-
} else {
|
|
547
|
-
styled = theme.fg(bodyColor, source);
|
|
548
|
-
}
|
|
549
|
-
const parts = wrapTextWithAnsi(styled, contentWidth);
|
|
643
|
+
for (const [index, source] of sourceLines.entries()) {
|
|
644
|
+
const parts = asInput
|
|
645
|
+
? wrapInputLine(source, contentWidth, theme, codeLine?.(index))
|
|
646
|
+
: wrapTextWithAnsi(theme.fg(bodyColor, source), contentWidth);
|
|
550
647
|
total += Math.max(1, parts.length);
|
|
551
648
|
if (total > limit) return true;
|
|
552
649
|
}
|
|
@@ -603,26 +700,42 @@ export function middleTruncateToWidth(text: string, width: number): string {
|
|
|
603
700
|
return `${left}…${right}`;
|
|
604
701
|
}
|
|
605
702
|
|
|
703
|
+
/**
|
|
704
|
+
* 展开卡 Input 的代码块:源行起始下标(相对 text)+ 语言 + 剥掉行首缩进的代码。
|
|
705
|
+
* standalone = 整段 Input 就是这块代码(没有 `code:` 标签),全量预览可以整段包围栏。
|
|
706
|
+
*/
|
|
707
|
+
export type InputCodeBlock = {
|
|
708
|
+
startLine: number;
|
|
709
|
+
language: string;
|
|
710
|
+
code: string;
|
|
711
|
+
standalone: boolean;
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
export type ToolInputBody = { text: string; codeBlock?: InputCodeBlock };
|
|
715
|
+
|
|
716
|
+
/** 按字段名判定可着色的入参:codemode / mcpscript 的 `code` 都是 JS 脚本。 */
|
|
717
|
+
const INPUT_CODE_LANGUAGES: Record<string, string> = { code: "javascript" };
|
|
718
|
+
|
|
606
719
|
/** Pretty-print tool call args for the expanded Input section. */
|
|
607
|
-
export function
|
|
608
|
-
if (args === undefined || args === null) return "";
|
|
720
|
+
export function formatToolInputBody(args: unknown, maxChars = 8_000): ToolInputBody {
|
|
721
|
+
if (args === undefined || args === null) return { text: "" };
|
|
609
722
|
if (typeof args !== "object") {
|
|
610
723
|
const text = sanitizeToolResultText(String(args));
|
|
611
|
-
return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
|
|
724
|
+
return { text: text.length > maxChars ? `${text.slice(0, maxChars)}…` : text };
|
|
612
725
|
}
|
|
613
726
|
if (Array.isArray(args)) {
|
|
614
727
|
try {
|
|
615
728
|
const json = JSON.stringify(args, null, 2);
|
|
616
|
-
return json.length > maxChars ? `${json.slice(0, maxChars)}…` : json;
|
|
729
|
+
return { text: json.length > maxChars ? `${json.slice(0, maxChars)}…` : json };
|
|
617
730
|
} catch {
|
|
618
|
-
return sanitizeToolResultText(String(args));
|
|
731
|
+
return { text: sanitizeToolResultText(String(args)) };
|
|
619
732
|
}
|
|
620
733
|
}
|
|
621
734
|
|
|
622
735
|
const entries = Object.entries(args as Record<string, unknown>).filter(
|
|
623
736
|
([, value]) => value !== undefined,
|
|
624
737
|
);
|
|
625
|
-
if (entries.length === 0) return "";
|
|
738
|
+
if (entries.length === 0) return { text: "" };
|
|
626
739
|
|
|
627
740
|
// Stable, human-first field order for common tools.
|
|
628
741
|
const preferred = [
|
|
@@ -648,13 +761,30 @@ export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
|
|
|
648
761
|
});
|
|
649
762
|
|
|
650
763
|
const lines: string[] = [];
|
|
764
|
+
let codeBlock: InputCodeBlock | undefined;
|
|
651
765
|
for (const [rawKey, value] of entries) {
|
|
652
766
|
const key = sanitizeToolResultText(rawKey);
|
|
653
767
|
if (typeof value === "string") {
|
|
654
768
|
const safeValue = sanitizeToolResultText(value);
|
|
655
769
|
if (safeValue.includes("\n")) {
|
|
656
|
-
|
|
657
|
-
|
|
770
|
+
const expanded = safeValue.replace(/\t/g, " ");
|
|
771
|
+
const language = INPUT_CODE_LANGUAGES[key];
|
|
772
|
+
// 只有一个 code 字段时(codemode 等)整块 Input 就是脚本,`code:` 标签是噪音
|
|
773
|
+
const standaloneCode = Boolean(language) && entries.length === 1;
|
|
774
|
+
if (!standaloneCode) lines.push(`${key}:`);
|
|
775
|
+
if (language) {
|
|
776
|
+
// 缩进由 wrapInputLine 负责,着色只喂剥掉行首缩进的正文
|
|
777
|
+
codeBlock = {
|
|
778
|
+
startLine: lines.length,
|
|
779
|
+
language,
|
|
780
|
+
code: expanded
|
|
781
|
+
.split("\n")
|
|
782
|
+
.map((line) => line.replace(/^[ \t]*/, ""))
|
|
783
|
+
.join("\n"),
|
|
784
|
+
standalone: standaloneCode,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
for (const line of expanded.split("\n")) {
|
|
658
788
|
lines.push(` ${line}`);
|
|
659
789
|
}
|
|
660
790
|
} else {
|
|
@@ -679,7 +809,13 @@ export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
|
|
|
679
809
|
}
|
|
680
810
|
}
|
|
681
811
|
const text = lines.join("\n");
|
|
682
|
-
|
|
812
|
+
// 截断后的行号与着色行不再对齐,这时只给纯文本
|
|
813
|
+
if (text.length > maxChars) return { text: `${text.slice(0, maxChars)}…` };
|
|
814
|
+
return codeBlock ? { text, codeBlock } : { text };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
|
|
818
|
+
return formatToolInputBody(args, maxChars).text;
|
|
683
819
|
}
|
|
684
820
|
|
|
685
821
|
export function hasExpandableDetail(outputText: string, args: unknown): boolean {
|
|
@@ -697,7 +833,7 @@ export function renderExpandedToolResult(
|
|
|
697
833
|
/** mode=on 展开卡贴左;compact 等保持默认前导空格 */
|
|
698
834
|
flushLeft = false,
|
|
699
835
|
): ExpandedToolIoView | ExpandedToolResultText | Text {
|
|
700
|
-
const inputBody =
|
|
836
|
+
const { text: inputBody, codeBlock } = formatToolInputBody(args);
|
|
701
837
|
const outputBody = body;
|
|
702
838
|
const maxOutputLines = config.expandedOutputMaxLines;
|
|
703
839
|
const maxInputLines = config.expandedInputMaxLines;
|
|
@@ -726,6 +862,7 @@ export function renderExpandedToolResult(
|
|
|
726
862
|
flushLeft,
|
|
727
863
|
);
|
|
728
864
|
}
|
|
865
|
+
view.setInputCode(codeBlock);
|
|
729
866
|
if (context) rememberIoView(context, view);
|
|
730
867
|
return view;
|
|
731
868
|
}
|
|
@@ -41,6 +41,7 @@ export function stripBackgroundAnsi(line: string): string {
|
|
|
41
41
|
/** 剥离行首状态图标(展开组内工具首行复用)。 */
|
|
42
42
|
export function stripLeadingStatusIcon(line: string): string {
|
|
43
43
|
return line.replace(
|
|
44
|
+
// 行首图标:ccstyle 用 `·`,pi 原生输出里还有 `•`,两种都要剥
|
|
44
45
|
/^((?:\x1b\[[0-9;]*m|[ \t]|[├└│─])*)(?:\x1b\[[0-9;]*m)*(?:[✓✗●○■⬤•·])(?:\x1b\[[0-9;]*m)*\s+/,
|
|
45
46
|
"$1",
|
|
46
47
|
);
|