pi-cc-extensions 0.8.68 → 0.8.70

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.
@@ -1,3 +1,5 @@
1
+ import { posix, win32 } from "node:path";
2
+ import { visibleWidth } from "@earendil-works/pi-tui";
1
3
  import { config } from "../../config/config.ts";
2
4
  import { oneLine } from "../../utils/format.ts";
3
5
 
@@ -31,8 +33,107 @@ export type ToolCallSummaryOptions = {
31
33
  title?: string;
32
34
  /** 文案变体;缺省 "default"。 */
33
35
  variant?: ToolCallSummaryVariant;
36
+ /** 工具执行目录;仅用于生成展示路径,不修改调用参数。 */
37
+ cwd?: string;
34
38
  };
35
39
 
40
+ export type ToolCallSummary = {
41
+ main: string;
42
+ detail: string;
43
+ /** 路径摘要保留结构,供最终渲染按实际宽度优先保留文件名。 */
44
+ path?: { prefix: string; value: string };
45
+ };
46
+
47
+ function pathApi(value: string) {
48
+ if (win32.isAbsolute(value)) return win32;
49
+ if (posix.isAbsolute(value)) return posix;
50
+ return undefined;
51
+ }
52
+
53
+ /** cwd 内绝对路径转相对路径;cwd 外路径保持不变。 */
54
+ export function displayPath(value: unknown, cwd?: string): string {
55
+ const text = oneLine(value, 4096);
56
+ const base = cwd ? oneLine(cwd, 4096) : "";
57
+ const api = pathApi(text);
58
+ if (!api || !base || !api.isAbsolute(base)) return text;
59
+ const relative = api.relative(base, text);
60
+ if (!relative) return api.basename(text) || ".";
61
+ if (relative === ".." || relative.startsWith(`..${api.sep}`) || api.isAbsolute(relative)) {
62
+ return text;
63
+ }
64
+ return relative;
65
+ }
66
+
67
+ function headToWidth(text: string, width: number, ellipsis = ""): string {
68
+ if (visibleWidth(text) <= width) return text;
69
+ if (width <= 0) return "";
70
+ const suffix = visibleWidth(ellipsis) <= width ? ellipsis : "";
71
+ const contentWidth = width - visibleWidth(suffix);
72
+ let head = "";
73
+ for (const char of Array.from(text)) {
74
+ if (visibleWidth(head + char) > contentWidth) break;
75
+ head += char;
76
+ }
77
+ return head + suffix;
78
+ }
79
+
80
+ function tailToWidth(text: string, width: number): string {
81
+ if (width <= 0) return "";
82
+ let tail = "";
83
+ for (const char of Array.from(text).reverse()) {
84
+ if (visibleWidth(char + tail) > width) break;
85
+ tail = char + tail;
86
+ }
87
+ return tail;
88
+ }
89
+
90
+ /** 中间截断路径:目录保留开头,末尾优先完整保留文件名。 */
91
+ export function truncatePathToWidth(path: string, width: number): string {
92
+ if (visibleWidth(path) <= width) return path;
93
+ if (width <= 1) return width === 1 ? "…" : "";
94
+ const separatorIndex = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
95
+ const filename = separatorIndex >= 0 ? path.slice(separatorIndex + 1) : path;
96
+ const filenameWidth = visibleWidth(filename);
97
+ if (separatorIndex >= 0 && filenameWidth + 1 <= width) {
98
+ const separator = path[separatorIndex]!;
99
+ const suffix = `${separator}${filename}`;
100
+ const prefixWidth = width - visibleWidth(suffix) - 1;
101
+ const prefix = headToWidth(path.slice(0, separatorIndex), Math.max(0, prefixWidth));
102
+ return `${prefix}…${prefix ? suffix : filename}`;
103
+ }
104
+ const leftWidth = Math.max(1, Math.floor((width - 1) * 0.45));
105
+ const rightWidth = Math.max(0, width - leftWidth - 1);
106
+ return `${headToWidth(filename, leftWidth)}…${tailToWidth(filename, rightWidth)}`;
107
+ }
108
+
109
+ /** 路径展示的统一入口:相对化后按配置和当前可用宽度截断。 */
110
+ export function formatDisplayPath(value: unknown, cwd: string | undefined, width: number): string {
111
+ return truncatePathToWidth(displayPath(value, cwd), Math.min(width, config.inputClip));
112
+ }
113
+
114
+ /** 按最终终端宽度渲染摘要;路径摘要不会再被整行头部截断。 */
115
+ export function fitToolCallSummary(summary: ToolCallSummary, width: number): string {
116
+ if (!summary.path) return headToWidth(summary.main, width, "…");
117
+ const prefix = summary.path.prefix;
118
+ const pathWidth = Math.max(0, width - visibleWidth(prefix) - 1);
119
+ if (pathWidth <= 0) return headToWidth(prefix, width, "…");
120
+ return `${prefix} ${truncatePathToWidth(summary.path.value, Math.min(pathWidth, config.inputClip))}`;
121
+ }
122
+
123
+ function pathSummary(
124
+ prefix: string,
125
+ value: unknown,
126
+ cwd: string | undefined,
127
+ detail = "",
128
+ ): ToolCallSummary {
129
+ const path = displayPath(value, cwd);
130
+ return {
131
+ main: `${prefix} ${truncatePathToWidth(path, config.inputClip)}`,
132
+ detail,
133
+ path: { prefix, value: path },
134
+ };
135
+ }
136
+
36
137
  /**
37
138
  * 单工具调用摘要({ main, detail })。
38
139
  *
@@ -43,7 +144,7 @@ export function toolCallSummary(
43
144
  toolName: string,
44
145
  args: any,
45
146
  opts: ToolCallSummaryOptions = {},
46
- ): { main: string; detail: string } {
147
+ ): ToolCallSummary {
47
148
  const title = opts.title ?? humanizeToolLabel(toolName);
48
149
  const variant = opts.variant ?? "default";
49
150
  if (!args || typeof args !== "object") return { main: title, detail: "" };
@@ -106,15 +207,13 @@ export function toolCallSummary(
106
207
  args.offset !== undefined ? `offset=${args.offset}` : "",
107
208
  args.limit !== undefined ? `limit=${args.limit}` : "",
108
209
  ].filter(Boolean);
109
- if (variant === "grouping") {
110
- return {
111
- main: `Read ${clip(args.path || "...")}`,
112
- detail: details.length ? ` (${details.join(", ")})` : "",
113
- };
210
+ const detail = details.length ? ` (${details.join(", ")})` : "";
211
+ if (typeof args.path === "string" && args.path) {
212
+ return pathSummary(variant === "grouping" ? "Read" : title, args.path, opts.cwd, detail);
114
213
  }
115
214
  return {
116
- main: `${title}${args.path ? ` ${clip(args.path)}` : ""}`,
117
- detail: details.length ? ` (${details.join(", ")})` : "",
215
+ main: variant === "grouping" ? "Read ..." : title,
216
+ detail,
118
217
  };
119
218
  }
120
219
  if (variant === "grouping") {
@@ -135,9 +234,11 @@ export function toolCallSummary(
135
234
  }
136
235
  }
137
236
  if (variant === "default") {
237
+ const preferredPath = args.path ?? args.file_path;
238
+ if (typeof preferredPath === "string" && preferredPath) {
239
+ return pathSummary(title, preferredPath, opts.cwd);
240
+ }
138
241
  const preferred =
139
- args.path ??
140
- args.file_path ??
141
242
  args.command ??
142
243
  args.query ??
143
244
  args.question ??
@@ -204,7 +204,7 @@ export class ExpandedToolResultText {
204
204
  }
205
205
  }
206
206
 
207
- /** Affordance next to truncated Input/Output headers — click opens full preview. */
207
+ /** 截断体末行 `… +N more lines` 旁的展开提示,点击打开全量预览。 */
208
208
  export const SHOW_MORE_LABEL = "• click to show more";
209
209
 
210
210
  export type ToolIoSection = "input" | "output";
@@ -251,8 +251,8 @@ export class ExpandedToolIoView {
251
251
  inputBody: string,
252
252
  outputBody: string,
253
253
  isError: boolean,
254
- maxOutputLines = config.expandedPreviewMaxLines,
255
- maxInputLines = config.expandedPreviewMaxLines,
254
+ maxOutputLines = config.expandedOutputMaxLines,
255
+ maxInputLines = config.expandedInputMaxLines,
256
256
  flushLeft = false,
257
257
  ) {
258
258
  this.theme = theme;
@@ -309,12 +309,12 @@ export class ExpandedToolIoView {
309
309
  this.invalidate();
310
310
  }
311
311
 
312
- /** True when the plain header line is a truncated section with show-more. */
312
+ /** True when the plain truncation footer carries show-more. Input 续行带 │,Output 不带。 */
313
313
  matchShowMoreLine(plainLine: string): ToolIoSection | null {
314
314
  const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
315
- if (!line.includes(` • ${showMoreHintText()}`)) return null;
316
- if (/\bInput\b/.test(line) && this.truncated.input) return "input";
317
- if (/\bOutput\b/.test(line) && this.truncated.output) return "output";
315
+ if (!line.includes(` • ${showMoreHintText()}`) || !/\+\d+ more lines/.test(line)) return null;
316
+ if (this.truncated.input && line.includes("│")) return "input";
317
+ if (this.truncated.output) return "output";
318
318
  return null;
319
319
  }
320
320
 
@@ -360,24 +360,13 @@ export class ExpandedToolIoView {
360
360
  this.truncated = { input: false, output: false };
361
361
  this.showMoreHeaderRows = {};
362
362
 
363
- const pushHeader = (
364
- corner: "├" | "└",
365
- label: string,
366
- section: ToolIoSection,
367
- showMore: boolean,
368
- ) => {
363
+ const pushHeader = (corner: "├" | "└", label: string) => {
369
364
  const mark = theme.fg("dim", `${lead}${corner} `);
370
365
  const title = theme.fg(
371
366
  "accent",
372
367
  typeof theme.bold === "function" ? theme.bold(label) : label,
373
368
  );
374
- // hover 只高亮文字,圆点保持 dim(与 group hint 一致)。
375
- const more = showMore
376
- ? theme.fg("dim", " •") +
377
- theme.fg(this.hoveredSection === section ? "text" : "dim", ` ${showMoreHintText()}`)
378
- : "";
379
- if (showMore) this.showMoreHeaderRows[section] = lines.length;
380
- lines.push(truncateToWidth(mark + title + more, safeWidth, ""));
369
+ lines.push(truncateToWidth(mark + title, safeWidth, ""));
381
370
  };
382
371
 
383
372
  const pushRailLine = (styledContent: string, continued = true) => {
@@ -400,7 +389,7 @@ export class ExpandedToolIoView {
400
389
 
401
390
  const pushBody = (
402
391
  body: string,
403
- opts: { input?: boolean; limit: number; continued?: boolean },
392
+ opts: { input?: boolean; limit: number; continued?: boolean; section: ToolIoSection },
404
393
  ): boolean /* truncated */ => {
405
394
  const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
406
395
  if (!raw.trim()) {
@@ -423,7 +412,15 @@ export class ExpandedToolIoView {
423
412
  if (truncated) {
424
413
  const hidden = Math.max(0, wrapped.length - visible.length);
425
414
  if (hidden > 0) {
426
- pushRailLine(theme.fg("dim", `… +${hidden} more lines`), opts.continued);
415
+ // hover 只高亮文字,圆点保持 dim(与 group hint 一致)。
416
+ const more =
417
+ theme.fg("dim", " •") +
418
+ theme.fg(
419
+ this.hoveredSection === opts.section ? "text" : "dim",
420
+ ` ${showMoreHintText()}`,
421
+ );
422
+ pushRailLine(theme.fg("dim", `… +${hidden} more lines`) + more, opts.continued);
423
+ this.showMoreHeaderRows[opts.section] = lines.length - 1;
427
424
  }
428
425
  }
429
426
  return truncated;
@@ -447,20 +444,29 @@ export class ExpandedToolIoView {
447
444
 
448
445
  if (hasInput) {
449
446
  this.truncated.input = inputWouldTruncate;
450
- pushHeader("├", "Input", "input", inputWouldTruncate);
447
+ pushHeader("├", "Input");
451
448
  pushBody(this.inputBody, {
452
449
  input: true,
453
450
  limit: this.maxInputLines,
454
451
  continued: true,
452
+ section: "input",
455
453
  });
456
454
  pushBlankRail();
457
455
  this.truncated.output = outputWouldTruncate;
458
- pushHeader("└", "Output", "output", outputWouldTruncate);
459
- pushBody(outputText, { limit: this.maxOutputLines, continued: false });
456
+ pushHeader("└", "Output");
457
+ pushBody(outputText, {
458
+ limit: this.maxOutputLines,
459
+ continued: false,
460
+ section: "output",
461
+ });
460
462
  } else {
461
463
  this.truncated.output = outputWouldTruncate;
462
- pushHeader("└", "Output", "output", outputWouldTruncate);
463
- pushBody(outputText, { limit: this.maxOutputLines, continued: false });
464
+ pushHeader("└", "Output");
465
+ pushBody(outputText, {
466
+ limit: this.maxOutputLines,
467
+ continued: false,
468
+ section: "output",
469
+ });
464
470
  }
465
471
 
466
472
  this.cachedWidth = width;
@@ -663,13 +669,21 @@ export function renderExpandedToolResult(
663
669
  ): ExpandedToolIoView | ExpandedToolResultText | Text {
664
670
  const inputBody = formatToolInputArgs(args);
665
671
  const outputBody = body;
666
- const maxLines = config.expandedPreviewMaxLines;
672
+ const maxOutputLines = config.expandedOutputMaxLines;
673
+ const maxInputLines = config.expandedInputMaxLines;
667
674
 
668
675
  // Prefer structured Input/Output when we have args or non-empty output.
669
676
  if (inputBody.trim() || outputBody.trim()) {
670
677
  let view: ExpandedToolIoView;
671
678
  if (isExpandedToolIoView(lastComponent)) {
672
- lastComponent.setContent(inputBody, outputBody, isError, maxLines, maxLines, flushLeft);
679
+ lastComponent.setContent(
680
+ inputBody,
681
+ outputBody,
682
+ isError,
683
+ maxOutputLines,
684
+ maxInputLines,
685
+ flushLeft,
686
+ );
673
687
  view = lastComponent;
674
688
  } else {
675
689
  view = new ExpandedToolIoView(
@@ -677,8 +691,8 @@ export function renderExpandedToolResult(
677
691
  inputBody,
678
692
  outputBody,
679
693
  isError,
680
- maxLines,
681
- maxLines,
694
+ maxOutputLines,
695
+ maxInputLines,
682
696
  flushLeft,
683
697
  );
684
698
  }
@@ -18,13 +18,10 @@ export const COMPACT_THINKING_PATCH_KEY = Symbol.for("pi.ccstyle.compact-thinkin
18
18
  export const PROTOTYPE_ORIGINAL_KEY = Symbol.for("pi.ccstyle.prototype-original");
19
19
  export const COMPACT_THINKING_OWNER = Symbol.for("pi.ccstyle.compact-thinking-owner");
20
20
 
21
- // ── assistant 展开状态(实例标记;description 供旧模块按 symbol.description 清理) ──
22
- export const ASSISTANT_SET_EXPANDED_DESCRIPTION = "pi.ccstyle.compact-assistant-set-expanded";
23
- export const ASSISTANT_TOGGLE_ROUND_DESCRIPTION = "pi.ccstyle.compact-assistant-toggle-round";
24
- export const ASSISTANT_REENTRY_DESCRIPTION = "pi.ccstyle.compact-assistant-reentry";
25
- export const ASSISTANT_SET_EXPANDED_KEY = Symbol.for(ASSISTANT_SET_EXPANDED_DESCRIPTION);
26
- export const ASSISTANT_TOGGLE_ROUND_KEY = Symbol.for(ASSISTANT_TOGGLE_ROUND_DESCRIPTION);
27
- export const ASSISTANT_REENTRY_KEY = Symbol.for(ASSISTANT_REENTRY_DESCRIPTION);
21
+ // ── assistant 展开状态(实例标记) ──
22
+ export const ASSISTANT_SET_EXPANDED_KEY = Symbol.for("pi.ccstyle.compact-assistant-set-expanded");
23
+ export const ASSISTANT_TOGGLE_ROUND_KEY = Symbol.for("pi.ccstyle.compact-assistant-toggle-round");
24
+ export const ASSISTANT_REENTRY_KEY = Symbol.for("pi.ccstyle.compact-assistant-reentry");
28
25
 
29
26
  // ── 工具渲染补丁 ──
30
27
  export const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch");
@@ -36,9 +33,6 @@ export const TOOL_EXPANDED_BACKGROUND_PATCH = Symbol.for(
36
33
  // ── 消息组件补丁 ──
37
34
  export const MESSAGE_DISPLAY_PATCH = Symbol.for("pi.ccstyle.message-display-patch");
38
35
 
39
- // ── 压缩渲染补丁(/reload 残留 deactivate 用) ──
40
- export const GLOBAL_COMPACTION_RENDER_PATCH = Symbol.for("pi.ccstyle.compaction-render-patch");
41
-
42
36
  // ── 工具分组 ──
43
37
  export const TOOL_GROUPING_PATCH_KEY = Symbol.for("pi.ccstyle.tool-grouping-patch");
44
38
  export const TOOL_GROUPING_PARENT_KEY = Symbol.for("pi.ccstyle.tool-grouping-parent");
@@ -53,6 +53,12 @@ export function isSgrLeftPress(packet: SgrMousePacket): boolean {
53
53
  return packet.final === "M" && baseButton === 0 && (packet.code & 32) === 0;
54
54
  }
55
55
 
56
+ /** 是否为左键松开。 */
57
+ export function isSgrLeftRelease(packet: SgrMousePacket): boolean {
58
+ const baseButton = mouseBaseButton(packet.code);
59
+ return packet.final === "m" && baseButton === 0 && (packet.code & 32) === 0;
60
+ }
61
+
56
62
  /** 剥离修饰键位(4/8/16/32:shift/meta/ctrl/motion),得到基础按键码。 */
57
63
  export function mouseBaseButton(code: number): number {
58
64
  return code & ~(4 | 8 | 16 | 32);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.68",
3
+ "version": "0.8.70",
4
4
  "license": "MIT",
5
5
  "description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
6
6
  "type": "module",
@@ -39,10 +39,10 @@
39
39
  },
40
40
  "scripts": {
41
41
  "test": "node --test tests/**/*.test.ts",
42
- "typecheck": "node scripts/typecheck.mjs",
42
+ "typecheck": "tsc --noEmit",
43
43
  "format": "biome format --write .",
44
44
  "format:check": "biome format .",
45
- "docs:tool-render": "node scripts/generate-tool-render-examples.mjs",
45
+ "docs:tool-render": "node --experimental-strip-types scripts/generate-tool-render-examples.ts",
46
46
  "lint": "biome check .",
47
47
  "preversion": "npm run format && npm run lint && npm run typecheck && npm test && npm run docs:tool-render",
48
48
  "release": "npm version patch && npm publish",