pi-cc-extensions 0.9.8 → 0.9.10
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 +48 -48
- package/README.md +6 -6
- package/extensions/config/config.ts +35 -6
- package/extensions/config/panel.ts +27 -3
- package/extensions/feature/compact-thinking.ts +7 -4
- package/extensions/feature/shell/footer.ts +47 -5
- package/extensions/feature/shell/mcp-chip.ts +136 -0
- package/extensions/feature/shell/startup-header.ts +82 -6
- package/extensions/feature/shell/working-message.ts +21 -6
- package/extensions/index.ts +2 -1
- package/extensions/renderer/compact-mode.ts +105 -16
- package/extensions/renderer/default-mode.ts +36 -4
- package/extensions/renderer/mouse/interaction.ts +11 -2
- package/extensions/renderer/tool/codemode.ts +303 -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 +37 -9
- package/extensions/renderer/tool/message-display.ts +3 -3
- package/extensions/renderer/tool/names.ts +12 -4
- package/extensions/renderer/tool/result.ts +180 -37
- package/extensions/utils/ansi-text.ts +1 -0
- package/extensions/utils/patch-keys.ts +2 -0
- package/package.json +7 -4
|
@@ -5,8 +5,13 @@ import { config } from "../../config/config.ts";
|
|
|
5
5
|
import { oneLine } from "../../utils/format.ts";
|
|
6
6
|
import { headTruncateToWidth } from "./result.ts";
|
|
7
7
|
|
|
8
|
+
/** inputClip=0 时不设字符上限,交给渲染时的实际宽度截断。 */
|
|
9
|
+
function clipLimit(): number {
|
|
10
|
+
return config.inputClip > 0 ? config.inputClip : Number.POSITIVE_INFINITY;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
function clip(value: unknown): string {
|
|
9
|
-
return oneLine(value,
|
|
14
|
+
return oneLine(value, clipLimit());
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
/** 载荷(入参 JSON / 脚本代码)至少留出这么多宽度才显示,否则整段省略(避免只剩一个孤零零的 " {…")。 */
|
|
@@ -125,7 +130,7 @@ export function truncatePathToWidth(path: string, width: number): string {
|
|
|
125
130
|
|
|
126
131
|
/** 路径展示的统一入口:相对化后按配置和当前可用宽度截断。 */
|
|
127
132
|
export function formatDisplayPath(value: unknown, cwd: string | undefined, width: number): string {
|
|
128
|
-
return truncatePathToWidth(displayPath(value, cwd), Math.min(width,
|
|
133
|
+
return truncatePathToWidth(displayPath(value, cwd), Math.min(width, clipLimit()));
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
/** 按最终终端宽度渲染摘要;路径摘要不会再被整行头部截断。 */
|
|
@@ -134,7 +139,7 @@ export function fitToolCallSummary(summary: ToolCallSummary, width: number): str
|
|
|
134
139
|
const prefix = summary.path.prefix;
|
|
135
140
|
const pathWidth = Math.max(0, width - visibleWidth(prefix) - 1);
|
|
136
141
|
if (pathWidth <= 0) return headToWidth(prefix, width, "…");
|
|
137
|
-
return `${prefix} ${truncatePathToWidth(summary.path.value, Math.min(pathWidth,
|
|
142
|
+
return `${prefix} ${truncatePathToWidth(summary.path.value, Math.min(pathWidth, clipLimit()))}`;
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
function pathSummary(
|
|
@@ -145,7 +150,7 @@ function pathSummary(
|
|
|
145
150
|
): ToolCallSummary {
|
|
146
151
|
const path = displayPath(value, cwd);
|
|
147
152
|
return {
|
|
148
|
-
main: `${prefix} ${truncatePathToWidth(path,
|
|
153
|
+
main: `${prefix} ${truncatePathToWidth(path, clipLimit())}`,
|
|
149
154
|
detail,
|
|
150
155
|
path: { prefix, value: path },
|
|
151
156
|
};
|
|
@@ -250,6 +255,9 @@ export function toolCallSummary(
|
|
|
250
255
|
const code = typeof args.code === "string" && args.code ? clip(args.code) : "";
|
|
251
256
|
return code ? { main: title, detail: "", payload: code } : { main: title, detail: "" };
|
|
252
257
|
}
|
|
258
|
+
// 内置 codemode:标题只报工具名。脚本很长,塞进行首既挤又读不出重点;
|
|
259
|
+
// 要看代码展开 Input,要看跑了什么看子调用行。
|
|
260
|
+
if (name === "codemode") return { main: title, detail: "" };
|
|
253
261
|
|
|
254
262
|
const value = (fallback: string, ...keys: string[]) => {
|
|
255
263
|
const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item);
|
|
@@ -1,15 +1,22 @@
|
|
|
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";
|
|
7
8
|
import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
|
|
8
9
|
|
|
9
10
|
const TOOL_VIEWPORT_WIDTH_RATIO = 0.8;
|
|
11
|
+
/** 宽屏右侧留白上限:比例留白超过这么多列时改用固定留白。 */
|
|
12
|
+
const TOOL_VIEWPORT_MAX_GUTTER = 24;
|
|
10
13
|
|
|
11
14
|
export function toolViewportWidth(width: number): number {
|
|
12
|
-
return Math.max(
|
|
15
|
+
return Math.max(
|
|
16
|
+
1,
|
|
17
|
+
Math.floor(width * TOOL_VIEWPORT_WIDTH_RATIO),
|
|
18
|
+
Math.floor(width) - TOOL_VIEWPORT_MAX_GUTTER,
|
|
19
|
+
);
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
/** 与默认工具结果相同的一级缩进包装。子组件只扣始终加上的 1 列;↳ 行多出的 2 列由 truncateToWidth 吃掉。 */
|
|
@@ -225,7 +232,7 @@ export class ExpandedToolResultText {
|
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
/** 截断体末行 `… +N more lines` 旁的展开提示,点击打开全量预览。 */
|
|
228
|
-
export const SHOW_MORE_LABEL = "
|
|
235
|
+
export const SHOW_MORE_LABEL = "· click to show more";
|
|
229
236
|
|
|
230
237
|
export type ToolIoSection = "input" | "output";
|
|
231
238
|
|
|
@@ -262,6 +269,17 @@ export class ExpandedToolIoView {
|
|
|
262
269
|
private truncated: { input: boolean; output: boolean } = { input: false, output: false };
|
|
263
270
|
/** 0-based header line indexes that carry show-more after last render. */
|
|
264
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;
|
|
265
283
|
|
|
266
284
|
/** flushLeft:贴左渲染(mode=on 展开卡);默认 false 保留前导空格(compact 等共用路径)。 */
|
|
267
285
|
private flushLeft: boolean;
|
|
@@ -319,6 +337,57 @@ export class ExpandedToolIoView {
|
|
|
319
337
|
return this.inputBody;
|
|
320
338
|
}
|
|
321
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
|
+
|
|
322
391
|
getOutputBody(): string {
|
|
323
392
|
return this.outputBody.trim() ? this.outputBody : "Done";
|
|
324
393
|
}
|
|
@@ -336,7 +405,7 @@ export class ExpandedToolIoView {
|
|
|
336
405
|
/** True when the plain truncation footer carries show-more. Input 续行带 │,Output 不带。 */
|
|
337
406
|
matchShowMoreLine(plainLine: string): ToolIoSection | null {
|
|
338
407
|
const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
|
|
339
|
-
if (!line.includes(`
|
|
408
|
+
if (!line.includes(` · ${showMoreHintText()}`) || !/\+\d+ more lines/.test(line)) return null;
|
|
340
409
|
if (this.truncated.input && line.includes("│")) return "input";
|
|
341
410
|
if (this.truncated.output) return "output";
|
|
342
411
|
return null;
|
|
@@ -357,7 +426,7 @@ export class ExpandedToolIoView {
|
|
|
357
426
|
/** Column range (1-based, visible cells) of show-more on a rendered header, if present. */
|
|
358
427
|
showMoreHitbox(plainLine: string): { startCol: number; endCol: number } | null {
|
|
359
428
|
const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
|
|
360
|
-
const label = `
|
|
429
|
+
const label = ` · ${showMoreHintText()}`;
|
|
361
430
|
const idx = line.lastIndexOf(label);
|
|
362
431
|
if (idx < 0) return null;
|
|
363
432
|
const before = line.slice(0, idx);
|
|
@@ -403,14 +472,6 @@ export class ExpandedToolIoView {
|
|
|
403
472
|
lines.push(truncateToWidth(theme.fg("dim", `${lead}│`), safeWidth, ""));
|
|
404
473
|
};
|
|
405
474
|
|
|
406
|
-
/** Style `key: value` input rows — dim keys, readable values. */
|
|
407
|
-
const styleInputLine = (rawLine: string): string => {
|
|
408
|
-
const match = rawLine.match(/^([A-Za-z_][\w.-]*)(:\s*)(.*)$/);
|
|
409
|
-
if (!match) return theme.fg("muted", rawLine);
|
|
410
|
-
const [, key, sep, rest] = match;
|
|
411
|
-
return theme.fg("dim", key + sep) + theme.fg("muted", rest ?? "");
|
|
412
|
-
};
|
|
413
|
-
|
|
414
475
|
const pushBody = (
|
|
415
476
|
body: string,
|
|
416
477
|
opts: { input?: boolean; limit: number; continued?: boolean; section: ToolIoSection },
|
|
@@ -422,8 +483,13 @@ export class ExpandedToolIoView {
|
|
|
422
483
|
}
|
|
423
484
|
const sourceLines = raw.split("\n");
|
|
424
485
|
const wrapped: string[] = [];
|
|
425
|
-
for (const source of sourceLines) {
|
|
426
|
-
|
|
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);
|
|
427
493
|
const parts = wrapTextWithAnsi(styled, contentWidth);
|
|
428
494
|
if (parts.length === 0) wrapped.push(styled);
|
|
429
495
|
else wrapped.push(...parts);
|
|
@@ -438,7 +504,7 @@ export class ExpandedToolIoView {
|
|
|
438
504
|
if (hidden > 0) {
|
|
439
505
|
// hover 只高亮文字,圆点保持 dim(与 group hint 一致)。
|
|
440
506
|
const more =
|
|
441
|
-
theme.fg("dim", "
|
|
507
|
+
theme.fg("dim", " ·") +
|
|
442
508
|
theme.fg(
|
|
443
509
|
this.hoveredSection === opts.section ? "text" : "dim",
|
|
444
510
|
` ${showMoreHintText()}`,
|
|
@@ -456,7 +522,15 @@ export class ExpandedToolIoView {
|
|
|
456
522
|
// Decide show-more from the same truncation rules as pushBody.
|
|
457
523
|
const inputWouldTruncate =
|
|
458
524
|
hasInput &&
|
|
459
|
-
bodyExceedsLineLimit(
|
|
525
|
+
bodyExceedsLineLimit(
|
|
526
|
+
this.inputBody,
|
|
527
|
+
this.maxInputLines,
|
|
528
|
+
contentWidth,
|
|
529
|
+
true,
|
|
530
|
+
theme,
|
|
531
|
+
bodyColor,
|
|
532
|
+
(inputIndex: number) => this.inputCodeLine(inputIndex),
|
|
533
|
+
);
|
|
460
534
|
const outputWouldTruncate = bodyExceedsLineLimit(
|
|
461
535
|
outputText,
|
|
462
536
|
this.maxOutputLines,
|
|
@@ -516,6 +590,40 @@ export function isExpandedToolIoView(value: unknown): value is ExpandedToolIoVie
|
|
|
516
590
|
);
|
|
517
591
|
}
|
|
518
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
|
+
|
|
519
627
|
/** True when body needs truncation at the given line limit (source lines or wrapped rows). */
|
|
520
628
|
function bodyExceedsLineLimit(
|
|
521
629
|
body: string,
|
|
@@ -524,23 +632,18 @@ function bodyExceedsLineLimit(
|
|
|
524
632
|
asInput: boolean,
|
|
525
633
|
theme: any,
|
|
526
634
|
bodyColor = "toolOutput",
|
|
635
|
+
/** 代码块的源行取着色内容,行数判断与实际渲染保持同一套规则。 */
|
|
636
|
+
codeLine?: (sourceIndex: number) => string | undefined,
|
|
527
637
|
): boolean {
|
|
528
638
|
const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
529
639
|
if (!raw.trim()) return false;
|
|
530
640
|
const sourceLines = raw.split("\n");
|
|
531
641
|
if (sourceLines.length > limit) return true;
|
|
532
642
|
let total = 0;
|
|
533
|
-
for (const source of sourceLines) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
styled = match
|
|
538
|
-
? theme.fg("dim", match[1] + match[2]) + theme.fg("muted", match[3] ?? "")
|
|
539
|
-
: theme.fg("muted", source);
|
|
540
|
-
} else {
|
|
541
|
-
styled = theme.fg(bodyColor, source);
|
|
542
|
-
}
|
|
543
|
-
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);
|
|
544
647
|
total += Math.max(1, parts.length);
|
|
545
648
|
if (total > limit) return true;
|
|
546
649
|
}
|
|
@@ -597,26 +700,42 @@ export function middleTruncateToWidth(text: string, width: number): string {
|
|
|
597
700
|
return `${left}…${right}`;
|
|
598
701
|
}
|
|
599
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
|
+
|
|
600
719
|
/** Pretty-print tool call args for the expanded Input section. */
|
|
601
|
-
export function
|
|
602
|
-
if (args === undefined || args === null) return "";
|
|
720
|
+
export function formatToolInputBody(args: unknown, maxChars = 8_000): ToolInputBody {
|
|
721
|
+
if (args === undefined || args === null) return { text: "" };
|
|
603
722
|
if (typeof args !== "object") {
|
|
604
723
|
const text = sanitizeToolResultText(String(args));
|
|
605
|
-
return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
|
|
724
|
+
return { text: text.length > maxChars ? `${text.slice(0, maxChars)}…` : text };
|
|
606
725
|
}
|
|
607
726
|
if (Array.isArray(args)) {
|
|
608
727
|
try {
|
|
609
728
|
const json = JSON.stringify(args, null, 2);
|
|
610
|
-
return json.length > maxChars ? `${json.slice(0, maxChars)}…` : json;
|
|
729
|
+
return { text: json.length > maxChars ? `${json.slice(0, maxChars)}…` : json };
|
|
611
730
|
} catch {
|
|
612
|
-
return sanitizeToolResultText(String(args));
|
|
731
|
+
return { text: sanitizeToolResultText(String(args)) };
|
|
613
732
|
}
|
|
614
733
|
}
|
|
615
734
|
|
|
616
735
|
const entries = Object.entries(args as Record<string, unknown>).filter(
|
|
617
736
|
([, value]) => value !== undefined,
|
|
618
737
|
);
|
|
619
|
-
if (entries.length === 0) return "";
|
|
738
|
+
if (entries.length === 0) return { text: "" };
|
|
620
739
|
|
|
621
740
|
// Stable, human-first field order for common tools.
|
|
622
741
|
const preferred = [
|
|
@@ -642,13 +761,30 @@ export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
|
|
|
642
761
|
});
|
|
643
762
|
|
|
644
763
|
const lines: string[] = [];
|
|
764
|
+
let codeBlock: InputCodeBlock | undefined;
|
|
645
765
|
for (const [rawKey, value] of entries) {
|
|
646
766
|
const key = sanitizeToolResultText(rawKey);
|
|
647
767
|
if (typeof value === "string") {
|
|
648
768
|
const safeValue = sanitizeToolResultText(value);
|
|
649
769
|
if (safeValue.includes("\n")) {
|
|
650
|
-
|
|
651
|
-
|
|
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")) {
|
|
652
788
|
lines.push(` ${line}`);
|
|
653
789
|
}
|
|
654
790
|
} else {
|
|
@@ -673,7 +809,13 @@ export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
|
|
|
673
809
|
}
|
|
674
810
|
}
|
|
675
811
|
const text = lines.join("\n");
|
|
676
|
-
|
|
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;
|
|
677
819
|
}
|
|
678
820
|
|
|
679
821
|
export function hasExpandableDetail(outputText: string, args: unknown): boolean {
|
|
@@ -691,7 +833,7 @@ export function renderExpandedToolResult(
|
|
|
691
833
|
/** mode=on 展开卡贴左;compact 等保持默认前导空格 */
|
|
692
834
|
flushLeft = false,
|
|
693
835
|
): ExpandedToolIoView | ExpandedToolResultText | Text {
|
|
694
|
-
const inputBody =
|
|
836
|
+
const { text: inputBody, codeBlock } = formatToolInputBody(args);
|
|
695
837
|
const outputBody = body;
|
|
696
838
|
const maxOutputLines = config.expandedOutputMaxLines;
|
|
697
839
|
const maxInputLines = config.expandedInputMaxLines;
|
|
@@ -720,6 +862,7 @@ export function renderExpandedToolResult(
|
|
|
720
862
|
flushLeft,
|
|
721
863
|
);
|
|
722
864
|
}
|
|
865
|
+
view.setInputCode(codeBlock);
|
|
723
866
|
if (context) rememberIoView(context, view);
|
|
724
867
|
return view;
|
|
725
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
|
);
|
|
@@ -49,6 +49,8 @@ export const TOOL_MOUSE_TUI_SLOT = Symbol.for("pi.ccstyle.tool-mouse-tui");
|
|
|
49
49
|
export const SCROLL_BUTTON_STATE_SLOT = Symbol.for("pi.ccstyle.scroll-button-state");
|
|
50
50
|
export const OFFICIAL_SCROLL_TO_END_KEY = Symbol.for("pi.ccstyle.official-scroll-to-end");
|
|
51
51
|
export const FLUSH_DOCKED_BASH_PATCH = Symbol.for("pi.ccstyle.flush-docked-bash-patch");
|
|
52
|
+
export const EARLY_STARTUP_HEADER_PATCH = Symbol.for("pi.ccstyle.early-startup-header-patch");
|
|
53
|
+
export const EARLY_STARTUP_HEADER_SWAP_KEY = Symbol.for("pi.ccstyle.early-startup-header-swap");
|
|
52
54
|
export const TOOL_HOVER_STATE_KEY = Symbol.for("pi.ccstyle.tool-hover-state");
|
|
53
55
|
|
|
54
56
|
// ── rich diff 组件标记 ──
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Claude Code-style TUI output with some personal touches and handy utilities.",
|
|
6
6
|
"type": "module",
|
|
@@ -56,15 +56,18 @@
|
|
|
56
56
|
},
|
|
57
57
|
"bundledDependencies": [],
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@earendil-works/pi-ai": "
|
|
60
|
-
"@earendil-works/pi-coding-agent": "
|
|
61
|
-
"@earendil-works/pi-tui": "
|
|
59
|
+
"@earendil-works/pi-ai": "*",
|
|
60
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
61
|
+
"@earendil-works/pi-tui": "*"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|
|
64
64
|
"node": ">=22.19.0"
|
|
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",
|
|
68
71
|
"@types/node": "^26.1.1",
|
|
69
72
|
"typescript": "^5.9.3"
|
|
70
73
|
}
|