pi-cc-extensions 0.8.69 → 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.
@@ -36,8 +36,8 @@ import { renderRichToolResult } from "./tool/diff/index.ts";
36
36
  import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
37
37
  import { isToolCallHovered } from "./mouse/hover.ts";
38
38
  import { insetComponent, renderExpandedToolResult, scheduleAnimation } from "./tool/result.ts";
39
- import { oneLine } from "../utils/format.ts";
40
39
  import { paddedBackgroundRow } from "./tool/grouping.ts";
40
+ import { formatDisplayPath } from "./tool/names.ts";
41
41
  import { hasVisibleText, stripBackgroundAnsi } from "../utils/ansi-text.ts";
42
42
  import { walkComponentTree } from "../utils/component-tree.ts";
43
43
  import {
@@ -502,13 +502,12 @@ function compactEditWriteLine(
502
502
  const theme = themeOf();
503
503
  const name = String(component.toolName ?? "tool");
504
504
  const args = component.args ?? {};
505
- const path = sanitizeToolResultText(
505
+ const path =
506
506
  typeof args.path === "string" && args.path
507
507
  ? args.path
508
508
  : typeof args.file_path === "string" && args.file_path
509
509
  ? args.file_path
510
- : "",
511
- );
510
+ : "";
512
511
  const isError = component.result?.isError === true;
513
512
  const isPending = !component.result || component.isPartial === true;
514
513
  const icon = isError ? "✗" : isPending ? toolLoadingIcon() : "✓";
@@ -542,7 +541,8 @@ function compactEditWriteLine(
542
541
  visibleWidth(statsText) +
543
542
  visibleWidth(hintText);
544
543
  const pathWidth = Math.max(0, width - fixedWidth - (path ? 1 : 0));
545
- const pathPart = pathWidth > 0 && path ? ` ${oneLine(path, pathWidth)}` : "";
544
+ const pathPart =
545
+ pathWidth > 0 && path ? ` ${formatDisplayPath(path, component.cwd, pathWidth)}` : "";
546
546
  const line = `${iconPart}${namePart}${theme.fg("toolTitle", pathPart)}${statsStyled}${hintText ? theme.fg("dim", hintText) : ""}`;
547
547
  return ["", truncateToWidth(line, width, "")];
548
548
  }
@@ -17,7 +17,6 @@ import { isToolCallHovered } from "./mouse/hover.ts";
17
17
  import {
18
18
  countLines,
19
19
  hasExpandableDetail,
20
- headTruncateToWidth,
21
20
  insetComponent,
22
21
  isToolExpanded,
23
22
  outputLineCount,
@@ -37,7 +36,7 @@ import { showMoreHintText } from "./tool/show-more-hint.ts";
37
36
  import { countWriteDiffStats } from "./tool/diff/diff-renderer.ts";
38
37
  import { renderRichToolResult, type WriteExecutionMetadataStore } from "./tool/diff/index.ts";
39
38
  import { getMessageDisplayTheme } from "./tool/message-display.ts";
40
- import { humanizeToolLabel, toolCallSummary } from "./tool/names.ts";
39
+ import { fitToolCallSummary, humanizeToolLabel, toolCallSummary } from "./tool/names.ts";
41
40
 
42
41
  // 成功勾:亮绿 truecolor(与 message-display 一致)
43
42
  const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
@@ -235,6 +234,7 @@ function createCcstyleTool(
235
234
  const summary = toolCallSummary(toolName, args, {
236
235
  title: label === toolName ? humanizeToolLabel(label) : label,
237
236
  variant: "default",
237
+ cwd: context?.cwd,
238
238
  });
239
239
  let writeStatsText = "";
240
240
  let writeStatsStyled = "";
@@ -267,8 +267,8 @@ function createCcstyleTool(
267
267
  );
268
268
  const mainWidth = Math.max(0, callWidth - visibleWidth(extraText));
269
269
  cachedWidth = width;
270
- // 纯文本先截断再着色(省略号不带 ANSI);从头截断,与多 tool 一致
271
- cachedLine = `${lead}${icon} ${theme.fg("toolTitle", headTruncateToWidth(summary.main, mainWidth))}${extraStyled}`;
270
+ // 路径按最终可用宽度中间截断,避免整行二次截断隐藏文件名。
271
+ cachedLine = `${lead}${icon} ${theme.fg("toolTitle", fitToolCallSummary(summary, mainWidth))}${extraStyled}`;
272
272
  return [truncateToWidth(cachedLine, viewportWidth, "")];
273
273
  },
274
274
  invalidate() {},
@@ -40,6 +40,9 @@ export interface DiffStats {
40
40
 
41
41
  const CANONICAL_LINE_PATTERN = /^([+\- ])(\s*\d+)\|(.*)$/;
42
42
  const HASHLINE_ANCHOR_LINE_PATTERN = /^([+\- ])(\s*\d+)#([A-Za-z0-9]+| {2}):(.*)$/;
43
+ // Pi still emits space-separated numbered rows, unlike OMP's pipe-delimited format.
44
+ const PI_LINE_PATTERN = /^([+\- ])(\s*\d+)\s(.*)$/;
45
+ const PI_OMISSION_LINE_PATTERN = /^ {3,}\.\.\.$/;
43
46
  const HUNK_HEADER_PATTERN = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/;
44
47
  const MIN_LINE_NUMBER_WIDTH = 2;
45
48
 
@@ -62,7 +65,10 @@ function toParsedDiffLine(
62
65
  return { lineKind: "context", lineNumber: normalizedLineNumber, content };
63
66
  }
64
67
 
65
- function parseCanonicalDiffLine(line: string): {
68
+ function parseCanonicalDiffLine(
69
+ line: string,
70
+ allowPiFormat: boolean,
71
+ ): {
66
72
  lineKind: DiffLineKind;
67
73
  lineNumber: string;
68
74
  content: string;
@@ -80,7 +86,8 @@ function parseCanonicalDiffLine(line: string): {
80
86
  };
81
87
  }
82
88
 
83
- const match = line.match(CANONICAL_LINE_PATTERN);
89
+ const match =
90
+ line.match(CANONICAL_LINE_PATTERN) ?? (allowPiFormat ? line.match(PI_LINE_PATTERN) : null);
84
91
  return match ? toParsedDiffLine(match[1] ?? " ", match[2] ?? "", match[3] ?? "") : null;
85
92
  }
86
93
 
@@ -187,12 +194,14 @@ export function parseDiff(diffText: string): ParsedDiff {
187
194
  let oldLineCursor: number | null = null;
188
195
  let newLineCursor: number | null = null;
189
196
  let lineNumberDelta = 0;
197
+ let hasHunkHeader = false;
190
198
 
191
199
  for (const rawLine of diffText.replace(/\r/g, "").split("\n")) {
192
200
  stats.lines++;
193
201
 
194
202
  const hunkMatch = rawLine.match(HUNK_HEADER_PATTERN);
195
203
  if (hunkMatch) {
204
+ hasHunkHeader = true;
196
205
  hunkIndex++;
197
206
  stats.hunks = Math.max(stats.hunks, hunkIndex);
198
207
  oldLineCursor = toNumber(hunkMatch[1]);
@@ -217,7 +226,14 @@ export function parseDiff(diffText: string): ParsedDiff {
217
226
  lineNumberDelta = 0;
218
227
  }
219
228
 
220
- const canonical = parseCanonicalDiffLine(rawLine);
229
+ // Pi pads omitted context with a blank line number; it is not a source row.
230
+ if (!hasHunkHeader && PI_OMISSION_LINE_PATTERN.test(rawLine)) {
231
+ entries.push(createMetaEntry(rawLine, hunkIndex));
232
+ continue;
233
+ }
234
+
235
+ // Pi's headerless format is ambiguous with numeric source text in unified hunks.
236
+ const canonical = parseCanonicalDiffLine(rawLine, !hasHunkHeader);
221
237
  if (canonical) {
222
238
  hunkIndex = ensureImplicitHunk(hunkIndex);
223
239
  stats.hunks = Math.max(stats.hunks, hunkIndex);
@@ -10,7 +10,12 @@ import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-load
10
10
  import { isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
11
11
  import { stripAnsi, stripBackgroundAnsi, stripLeadingStatusIcon } from "../../utils/ansi-text.ts";
12
12
  import { walkComponentTree } from "../../utils/component-tree.ts";
13
- import { humanizeToolLabel, toolCallSummary } from "./names.ts";
13
+ import {
14
+ fitToolCallSummary,
15
+ humanizeToolLabel,
16
+ toolCallSummary,
17
+ type ToolCallSummary,
18
+ } from "./names.ts";
14
19
  import {
15
20
  patchRegistry,
16
21
  TOOL_GROUPING_GENERATION_KEY as GENERATION_KEY,
@@ -142,8 +147,11 @@ export function paddedBackgroundRow(
142
147
  return `${bgAnsi}${stable}\x1b[49m`;
143
148
  }
144
149
 
145
- function toolSummary(tool: any): { main: string; detail: string } {
146
- return toolCallSummary(toolName(tool), tool?.args ?? {}, { variant: "grouping" });
150
+ function toolSummary(tool: any): ToolCallSummary {
151
+ return toolCallSummary(toolName(tool), tool?.args ?? {}, {
152
+ variant: "grouping",
153
+ cwd: tool?.cwd,
154
+ });
147
155
  }
148
156
 
149
157
  function toolNameList(tools: any[]): string {
@@ -331,11 +339,14 @@ export class ToolGroupComponent extends Container {
331
339
  const continuation = index === total - 1 ? " " : "│ ";
332
340
  if (!this._expanded) {
333
341
  const summary = toolSummary(tool);
342
+ const prefix = ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `;
343
+ const detail = fg("dim", summary.detail);
344
+ const mainWidth = Math.max(0, width - visibleWidth(prefix) - visibleWidth(detail));
334
345
  lines.push(
335
346
  truncateToWidth(
336
- ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} ${fg("toolTitle", summary.main)}${fg("dim", summary.detail)}`,
347
+ `${prefix}${fg("toolTitle", fitToolCallSummary(summary, mainWidth))}${detail}`,
337
348
  width,
338
- "…",
349
+ "",
339
350
  ),
340
351
  );
341
352
  continue;
@@ -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 ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.69",
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",