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.
@@ -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";
@@ -62,20 +61,13 @@ type ToolRenderMethods = {
62
61
  type GlobalToolRenderPatch = {
63
62
  version: 2;
64
63
  prototype: any;
65
- owner: object;
66
64
  active: boolean;
67
- enabled: () => boolean;
68
65
  mode: () => CompactStyleMode;
69
66
  wrap: (tool: any) => any;
70
67
  byDefinition: WeakMap<object, any>;
71
68
  byName: Map<string, any>;
72
69
  downstream: ToolRenderMethods;
73
70
  installed: ToolRenderMethods;
74
- // 兼容旧扩展读取 Symbol 上的 legacy 字段。
75
- originalHasRendererDefinition: ToolRenderMethods["hasRendererDefinition"];
76
- originalGetRenderShell: ToolRenderMethods["getRenderShell"];
77
- originalGetCallRenderer: ToolRenderMethods["getCallRenderer"];
78
- originalGetResultRenderer: ToolRenderMethods["getResultRenderer"];
79
71
  };
80
72
 
81
73
  type ToolExpandedBackgroundPatch = {
@@ -242,6 +234,7 @@ function createCcstyleTool(
242
234
  const summary = toolCallSummary(toolName, args, {
243
235
  title: label === toolName ? humanizeToolLabel(label) : label,
244
236
  variant: "default",
237
+ cwd: context?.cwd,
245
238
  });
246
239
  let writeStatsText = "";
247
240
  let writeStatsStyled = "";
@@ -274,8 +267,8 @@ function createCcstyleTool(
274
267
  );
275
268
  const mainWidth = Math.max(0, callWidth - visibleWidth(extraText));
276
269
  cachedWidth = width;
277
- // 纯文本先截断再着色(省略号不带 ANSI);从头截断,与多 tool 一致
278
- cachedLine = `${lead}${icon} ${theme.fg("toolTitle", headTruncateToWidth(summary.main, mainWidth))}${extraStyled}`;
270
+ // 路径按最终可用宽度中间截断,避免整行二次截断隐藏文件名。
271
+ cachedLine = `${lead}${icon} ${theme.fg("toolTitle", fitToolCallSummary(summary, mainWidth))}${extraStyled}`;
279
272
  return [truncateToWidth(cachedLine, viewportWidth, "")];
280
273
  },
281
274
  invalidate() {},
@@ -450,63 +443,39 @@ function isOwnershipAwarePatch(value: any): value is GlobalToolRenderPatch {
450
443
  );
451
444
  }
452
445
 
453
- function downstreamForGlobalToolInstall(prototype: any, previous: any): ToolRenderMethods {
446
+ function downstreamForGlobalToolInstall(
447
+ prototype: any,
448
+ previous: GlobalToolRenderPatch | undefined,
449
+ ): ToolRenderMethods {
454
450
  const current = prototypeToolRenderMethods(prototype);
455
- if (!previous || previous.prototype !== prototype) return current;
456
- if (isOwnershipAwarePatch(previous)) {
457
- return {
458
- hasRendererDefinition:
459
- current.hasRendererDefinition === previous.installed.hasRendererDefinition
460
- ? previous.downstream.hasRendererDefinition
461
- : current.hasRendererDefinition,
462
- getRenderShell:
463
- current.getRenderShell === previous.installed.getRenderShell
464
- ? previous.downstream.getRenderShell
465
- : current.getRenderShell,
466
- getCallRenderer:
467
- current.getCallRenderer === previous.installed.getCallRenderer
468
- ? previous.downstream.getCallRenderer
469
- : current.getCallRenderer,
470
- getResultRenderer:
471
- current.getResultRenderer === previous.installed.getResultRenderer
472
- ? previous.downstream.getResultRenderer
473
- : current.getResultRenderer,
474
- };
451
+ if (!previous || previous.prototype !== prototype || !isOwnershipAwarePatch(previous)) {
452
+ return current;
475
453
  }
476
-
477
- // pre-v2 Symbol:originalX 显式记录真实下游(native 原型方法),无需源码嗅探。
478
- // 缺失字段回退到当前原型方法(视作 external)。
479
- const originalOrCurrent = (field: string, method: Function): Function =>
480
- typeof previous[field] === "function" ? previous[field] : method;
481
454
  return {
482
- hasRendererDefinition: originalOrCurrent(
483
- "originalHasRendererDefinition",
484
- current.hasRendererDefinition,
485
- ) as ToolRenderMethods["hasRendererDefinition"],
486
- getRenderShell: originalOrCurrent(
487
- "originalGetRenderShell",
488
- current.getRenderShell,
489
- ) as ToolRenderMethods["getRenderShell"],
490
- getCallRenderer: originalOrCurrent(
491
- "originalGetCallRenderer",
492
- current.getCallRenderer,
493
- ) as ToolRenderMethods["getCallRenderer"],
494
- getResultRenderer: originalOrCurrent(
495
- "originalGetResultRenderer",
496
- current.getResultRenderer,
497
- ) as ToolRenderMethods["getResultRenderer"],
455
+ hasRendererDefinition:
456
+ current.hasRendererDefinition === previous.installed.hasRendererDefinition
457
+ ? previous.downstream.hasRendererDefinition
458
+ : current.hasRendererDefinition,
459
+ getRenderShell:
460
+ current.getRenderShell === previous.installed.getRenderShell
461
+ ? previous.downstream.getRenderShell
462
+ : current.getRenderShell,
463
+ getCallRenderer:
464
+ current.getCallRenderer === previous.installed.getCallRenderer
465
+ ? previous.downstream.getCallRenderer
466
+ : current.getCallRenderer,
467
+ getResultRenderer:
468
+ current.getResultRenderer === previous.installed.getResultRenderer
469
+ ? previous.downstream.getResultRenderer
470
+ : current.getResultRenderer,
498
471
  };
499
472
  }
500
473
 
501
- function disconnectGlobalToolRenderPatch(patch: any): void {
502
- if (!patch || typeof patch !== "object") return;
474
+ function disconnectGlobalToolRenderPatch(patch: GlobalToolRenderPatch | undefined): void {
475
+ if (!patch) return;
503
476
  patch.active = false;
504
- patch.enabled = () => false;
505
- patch.mode = () => "off";
506
- patch.wrap = (tool: any) => tool;
507
477
  patch.byDefinition = new WeakMap();
508
- if (patch.byName && typeof patch.byName.clear === "function") patch.byName.clear();
509
- else patch.byName = new Map();
478
+ patch.byName.clear();
510
479
  }
511
480
 
512
481
  function installGlobalToolRendering(
@@ -515,25 +484,18 @@ function installGlobalToolRendering(
515
484
  const prototype = (ToolExecutionComponent as any).prototype;
516
485
  const previous = patchRegistry.get<GlobalToolRenderPatch>(GLOBAL_TOOL_RENDER_PATCH);
517
486
  const downstream = downstreamForGlobalToolInstall(prototype, previous);
518
- // 外部仍持有的旧 wrapper 先变为无回调 pass-through,再挂新安装。
519
- disconnectGlobalToolRenderPatch(previous);
487
+ if (isOwnershipAwarePatch(previous)) disconnectGlobalToolRenderPatch(previous);
520
488
 
521
489
  const patch: GlobalToolRenderPatch = {
522
490
  version: 2,
523
491
  prototype,
524
- owner: {},
525
492
  active: true,
526
- enabled: () => config.mode === "on",
527
493
  mode: () => config.mode,
528
494
  wrap: (tool: any) => createCcstyleTool(tool, writeExecutionMetadata),
529
495
  byDefinition: new WeakMap(),
530
496
  byName: new Map(),
531
497
  downstream,
532
498
  installed: undefined as any,
533
- originalHasRendererDefinition: downstream.hasRendererDefinition,
534
- originalGetRenderShell: downstream.getRenderShell,
535
- originalGetCallRenderer: downstream.getCallRenderer,
536
- originalGetResultRenderer: downstream.getResultRenderer,
537
499
  };
538
500
 
539
501
  patch.installed = {
@@ -14,7 +14,6 @@ import {
14
14
  } from "./default-mode.ts";
15
15
  import { isLazyProxyTui } from "../utils/fullscreen-detect.ts";
16
16
  import { showCcstylePanel } from "../config/panel.ts";
17
- import { GLOBAL_COMPACTION_RENDER_PATCH, patchRegistry } from "../utils/patch-keys.ts";
18
17
  import {
19
18
  config,
20
19
  formatConfigStatus,
@@ -93,16 +92,6 @@ function applyStyleMode(mode: CompactStyleMode, ctx: any, toolGrouping?: ToolGro
93
92
  ctx.ui.notify(`Claude Code style: ${mode}`, "info");
94
93
  }
95
94
 
96
- type LegacyCompactionRenderPatch = {
97
- enabled?: () => boolean;
98
- };
99
-
100
- /** Disable the pre-native compaction monkey patch left alive by /reload. */
101
- function deactivateLegacyCompactionRendering() {
102
- const patch = patchRegistry.get<LegacyCompactionRenderPatch>(GLOBAL_COMPACTION_RENDER_PATCH);
103
- if (patch) patch.enabled = () => false;
104
- }
105
-
106
95
  export default function (
107
96
  pi: ExtensionAPI,
108
97
  configOverride?: Partial<Config>,
@@ -137,7 +126,6 @@ export default function (
137
126
  const disposeMessageDisplay = installMessageDisplayRendering();
138
127
  // 展开背景必须在 compact-mode 之后装,shutdown 时先于 compact 释放。
139
128
  const disposeToolExpandedBackground = installToolExpandedBackground();
140
- deactivateLegacyCompactionRendering();
141
129
  installation = {
142
130
  defaultMode,
143
131
  toolGrouping,
@@ -270,7 +258,6 @@ export default function (
270
258
  current.compactMode.shutdown();
271
259
  compactModeHooks = undefined;
272
260
  current.disposeMessageDisplay();
273
- deactivateLegacyCompactionRendering();
274
261
  clearAllAnimations();
275
262
  installation = undefined;
276
263
  });
@@ -21,13 +21,8 @@ export function sharedToolHoverState(): SharedToolHoverState {
21
21
  return patchRegistry.ensure(TOOL_HOVER_STATE_KEY, () => ({ toolCallId: null }));
22
22
  }
23
23
 
24
- // 状态单源在 globalThis 槽;hoveredToolCallId 变量仅为兼容旧 deep import 保留
25
- // (外部扩展可能直接从 interaction.ts re-export 读取),内部读取一律走槽。
26
- export let hoveredToolCallId: string | null = sharedToolHoverState().toolCallId;
27
-
28
24
  export function setHoveredToolCallId(toolCallId: string | null): void {
29
25
  sharedToolHoverState().toolCallId = toolCallId;
30
- hoveredToolCallId = toolCallId;
31
26
  }
32
27
 
33
28
  export function isToolCallHovered(toolCallId: string | null | undefined): boolean {
@@ -19,6 +19,7 @@ import {
19
19
  collectToolComponents,
20
20
  extractToolFramePlacements,
21
21
  isSgrLeftPress,
22
+ isSgrLeftRelease,
22
23
  isToolExecutionComponent,
23
24
  parseSgrMousePackets,
24
25
  stripTerminalSequences,
@@ -36,6 +37,7 @@ import {
36
37
  isScrollbarColumnAt,
37
38
  } from "./layout.ts";
38
39
  import {
40
+ disableOfficialScrollToEnd,
39
41
  fullscreenLazyTui,
40
42
  hideScrollButton,
41
43
  isScrollBottomInput,
@@ -156,7 +158,11 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
156
158
  }
157
159
 
158
160
  const EXPAND_PANEL_DOUBLE_CLICK_MS = 400;
159
- let lastExpandPanelClick: { id: unknown; at: number } | null = null;
161
+ /** 松开后短时间内的重复按下视为同一次单击(终端会在 mouseup 后再打一次 press)。 */
162
+ const EXPAND_PANEL_CLICK_ARM_MS = 50;
163
+ let pendingExpandPress: { id: unknown } | null = null;
164
+ let lastExpandClick: { id: unknown; at: number } | null = null;
165
+ let armExpandClickTimer: ReturnType<typeof setTimeout> | null = null;
160
166
 
161
167
  function expandPanelIdentity(card: any): unknown {
162
168
  return card instanceof ThinkingPreviewBlock
@@ -167,17 +173,48 @@ function expandPanelIdentity(card: any): unknown {
167
173
  function isExpandPanelDoubleClick(card: any): boolean {
168
174
  const now = Date.now();
169
175
  const id = expandPanelIdentity(card);
170
- const prev = lastExpandPanelClick;
171
- lastExpandPanelClick = { id, at: now };
172
- return Boolean(prev && prev.id === id && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS);
176
+ const prev = lastExpandClick;
177
+ pendingExpandPress = { id };
178
+ if (prev && prev.id === id && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS) {
179
+ pendingExpandPress = null;
180
+ lastExpandClick = null;
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+
186
+ function completeExpandPanelClick(): void {
187
+ if (!pendingExpandPress) return;
188
+ const click = { id: pendingExpandPress.id, at: Date.now() };
189
+ pendingExpandPress = null;
190
+ if (armExpandClickTimer) clearTimeout(armExpandClickTimer);
191
+ armExpandClickTimer = setTimeout(() => {
192
+ armExpandClickTimer = null;
193
+ // 50ms 内又按下:仍是同一次单击,不能武装成双击。
194
+ if (pendingExpandPress) return;
195
+ lastExpandClick = click;
196
+ }, EXPAND_PANEL_CLICK_ARM_MS);
197
+ if (
198
+ typeof armExpandClickTimer === "object" &&
199
+ armExpandClickTimer &&
200
+ "unref" in armExpandClickTimer
201
+ ) {
202
+ armExpandClickTimer.unref();
203
+ }
173
204
  }
174
205
 
175
206
  function clearExpandPanelDoubleClick(): void {
176
- lastExpandPanelClick = null;
207
+ pendingExpandPress = null;
208
+ lastExpandClick = null;
209
+ if (armExpandClickTimer) {
210
+ clearTimeout(armExpandClickTimer);
211
+ armExpandClickTimer = null;
212
+ }
177
213
  }
178
214
 
179
215
  function collapseExpandedCard(tui: any, card: any): boolean {
180
- if (!isExpandPanelDoubleClick(card)) return false;
216
+ // 单击也 consume,避免官方链再合成一次 press 把单击当成双击。
217
+ if (!isExpandPanelDoubleClick(card)) return true;
181
218
  card.setExpanded(false);
182
219
  setHoveredToolCallId(null);
183
220
  setHoveredToolGroup(null);
@@ -333,18 +370,17 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
333
370
  const plain = stripTerminalSequencesPreservingLayout(line);
334
371
  const section = view.matchShowMoreLine(plain);
335
372
  if (section) {
373
+ clearExpandPanelDoubleClick();
336
374
  const box = view.showMoreHitbox(plain);
337
- if (box && x + 1 >= box.startCol && x + 1 <= box.endCol) {
338
- return tryOpenToolIoShowMore({
339
- kind: "show-more",
340
- row: 0,
341
- startCol: box.startCol,
342
- endCol: box.endCol,
343
- component,
344
- view,
345
- section,
346
- });
347
- }
375
+ return tryOpenToolIoShowMore({
376
+ kind: "show-more",
377
+ row: 0,
378
+ startCol: box?.startCol ?? 1,
379
+ endCol: box?.endCol ?? 1,
380
+ component,
381
+ view,
382
+ section,
383
+ });
348
384
  }
349
385
  }
350
386
  // 双击收起:内部工具仍归所属 group。单击放行官方(选择/链接)。
@@ -461,6 +497,7 @@ function patchFullscreenViewportInput(tui: any): void {
461
497
  if (packets && tui.hasOverlay?.() && hasActiveTextPreview()) return undefined;
462
498
  if (packets && !tui.hasOverlay?.()) {
463
499
  for (const packet of packets) {
500
+ if (isSgrLeftRelease(packet)) completeExpandPanelClick();
464
501
  if (isSgrLeftPress(packet) && handleFullscreenToolClick(tui, packet)) {
465
502
  return { consume: true };
466
503
  }
@@ -797,6 +834,7 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
797
834
  let consumed = false;
798
835
  for (const packet of packets) {
799
836
  updateToolSummaryHover(getToolMouseTui(), packet);
837
+ if (isSgrLeftRelease(packet)) completeExpandPanelClick();
800
838
  if (!isSgrLeftPress(packet)) continue;
801
839
  if (toggleToolAtMouseClick(getToolMouseTui(), packet)) {
802
840
  consumed = true;
@@ -879,10 +917,12 @@ export function installToolMouseInteraction(
879
917
  if (isLazyProxyTui(tui)) {
880
918
  patchFullscreenViewportInput(tui);
881
919
  ensureFullscreenToolMouseMotion(tui);
920
+ disableOfficialScrollToEnd(tui);
882
921
  setScrollButtonWidget({
883
922
  render: (width: number) => {
884
923
  patchFullscreenViewportInput(tui);
885
924
  ensureFullscreenToolMouseMotion(tui);
925
+ disableOfficialScrollToEnd(tui);
886
926
  return renderScrollButton(width, theme);
887
927
  },
888
928
  invalidate() {},
@@ -924,13 +964,3 @@ export function scheduleSessionRender(refresh?: () => void): void {
924
964
  tui.requestRender(true);
925
965
  }, 0);
926
966
  }
927
-
928
- // toolMouseTui 跨模块读取一律走 getToolMouseTui()(jiti 转译下 let 绑定可能是快照);
929
- // 此 re-export 仅保留兼容旧导入,新代码请用 getToolMouseTui。
930
- export { toolMouseTui } from "./scroll.ts";
931
- export {
932
- hoveredToolCallId,
933
- isToolCallHovered,
934
- setHoveredToolGroup,
935
- setHoveredToolIo,
936
- } from "./hover.ts";
@@ -7,6 +7,7 @@ export {
7
7
  export {
8
8
  parseSgrMousePackets,
9
9
  isSgrLeftPress,
10
+ isSgrLeftRelease,
10
11
  type SgrMousePacket,
11
12
  } from "../../utils/sgr-mouse.ts";
12
13
 
@@ -18,26 +18,14 @@ const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|
18
18
  const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
19
19
  const SCROLL_BOTTOM_SHORTCUT = "ctrl+end";
20
20
 
21
- /**
22
- * 当前安装的 tui 宿主。宿主放本模块(滚动按钮/调度依赖它),
23
- * 由 mouse-interaction 经 setToolMouseTui 维护;跨模块一律经绑定/setter 访问。
24
- *
25
- * 状态镜像到 globalThis(Symbol 槽):jiti 转译下经 re-export 链读取的
26
- * 模块级 let 绑定是初始值快照(死绑定,实测恒 null),函数调用才是活引用。
27
- * 跨模块读取一律用 getToolMouseTui(),避免拿到加载时的快照。
28
- */
29
21
  patchRegistry.ensure(TOOL_MOUSE_TUI_SLOT, () => null);
30
- export let toolMouseTui: any = null;
31
22
  export function getToolMouseTui(): any {
32
23
  return patchRegistry.get(TOOL_MOUSE_TUI_SLOT);
33
24
  }
34
25
  export function setToolMouseTui(tui: any): void {
35
- toolMouseTui = tui;
36
26
  patchRegistry.install(TOOL_MOUSE_TUI_SLOT, tui);
37
27
  }
38
28
 
39
- // 滚动按钮状态同 toolMouseTui:镜像到 globalThis(Symbol 槽),跨模块读取
40
- // 一律用 getter(jiti 转译下模块级 let 绑定是初始值快照)。
41
29
  type ScrollButtonState = { visible: boolean; hovered: boolean; widget: any };
42
30
  function scrollButtonState(): ScrollButtonState {
43
31
  return patchRegistry.ensure(SCROLL_BUTTON_STATE_SLOT, () => ({
@@ -91,6 +79,11 @@ export function fullscreenLazyTui(tui: any): boolean {
91
79
  return isLazyProxyTui(tui) && tui.mode === "fullscreen";
92
80
  }
93
81
 
82
+ /** 关掉 pi 0.85 Jump to latest overlay,避免和本仓库 dock 按钮叠两层。 */
83
+ export function disableOfficialScrollToEnd(tui: any): void {
84
+ if (typeof tui?.scrollToEndIndicator === "function") tui.scrollToEndIndicator = undefined;
85
+ }
86
+
94
87
  /** 官方 fullscreen:是否已跟随 transcript 底部(按钮隐藏条件)。 */
95
88
  export function isFullscreenAtBottom(tui: any): boolean {
96
89
  const following = tui.isFollowingOutput ?? tui.getPrimaryScrollView?.()?.isFollowingEnd ?? true;
@@ -164,7 +157,7 @@ export function scheduleScrollButtonSync(tui: any, data: string): void {
164
157
  const previousLines = tui.previousLines;
165
158
  const check = (attempt: number) => {
166
159
  scrollButtonSyncScheduled = false;
167
- if (toolMouseTui !== tui) return;
160
+ if (getToolMouseTui() !== tui) return;
168
161
  // Pi renders on its own frame timer. Inspect the resulting viewport before
169
162
  // showing the button so empty or non-scrollable transcripts never flash it.
170
163
  const rendered = tui.previousLines !== previousLines;
@@ -192,7 +185,7 @@ export function updateScrollButtonFromInput(tui: any, data: string): void {
192
185
  }
193
186
 
194
187
  export function renderScrollButton(width: number, theme: any): string[] {
195
- if (!getScrollButtonVisible() || !fullscreenLazyTui(toolMouseTui)) return [];
188
+ if (!getScrollButtonVisible() || !fullscreenLazyTui(getToolMouseTui())) return [];
196
189
  const shortcut = formatShortcut(SCROLL_BOTTOM_SHORTCUT);
197
190
  const label = theme.fg(
198
191
  getScrollButtonHovered() ? "text" : "accent",
@@ -45,18 +45,10 @@ export function resolveDiffDisplayLimit(
45
45
  return expanded ? Math.max(0, expandedLimit) : Math.max(1, collapsedLimit);
46
46
  }
47
47
 
48
- /** Write collapsed body limit. Missing/NaN falls back to edit's `editDiffCollapsedLines`. */
49
48
  export function resolveWriteCollapsedLimit(
50
- config: Pick<ToolDisplayConfig, "writeDiffCollapsedLines" | "editDiffCollapsedLines">,
49
+ config: Pick<ToolDisplayConfig, "writeDiffCollapsedLines">,
51
50
  ): number {
52
- const value = config.writeDiffCollapsedLines;
53
- if (Number.isFinite(value)) {
54
- return Math.max(0, value);
55
- }
56
- const fallback = Number.isFinite(config.editDiffCollapsedLines)
57
- ? config.editDiffCollapsedLines
58
- : DEFAULT_TOOL_DISPLAY_CONFIG.writeDiffCollapsedLines;
59
- return Math.max(0, fallback);
51
+ return Math.max(0, config.writeDiffCollapsedLines);
60
52
  }
61
53
 
62
54
  /**
@@ -40,7 +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
- const LEGACY_LINE_PATTERN = /^([+\- ])(\s*\d+)\s(.*)$/;
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,}\.\.\.$/;
44
46
  const HUNK_HEADER_PATTERN = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/;
45
47
  const MIN_LINE_NUMBER_WIDTH = 2;
46
48
 
@@ -63,7 +65,10 @@ function toParsedDiffLine(
63
65
  return { lineKind: "context", lineNumber: normalizedLineNumber, content };
64
66
  }
65
67
 
66
- function parseCanonicalDiffLine(line: string): {
68
+ function parseCanonicalDiffLine(
69
+ line: string,
70
+ allowPiFormat: boolean,
71
+ ): {
67
72
  lineKind: DiffLineKind;
68
73
  lineNumber: string;
69
74
  content: string;
@@ -81,14 +86,9 @@ function parseCanonicalDiffLine(line: string): {
81
86
  };
82
87
  }
83
88
 
84
- const canonicalMatch = line.match(CANONICAL_LINE_PATTERN);
85
- const legacyMatch = canonicalMatch ? null : line.match(LEGACY_LINE_PATTERN);
86
- const matched = canonicalMatch ?? legacyMatch;
87
- if (!matched) {
88
- return null;
89
- }
90
-
91
- return toParsedDiffLine(matched[1] ?? " ", matched[2] ?? "", matched[3] ?? "");
89
+ const match =
90
+ line.match(CANONICAL_LINE_PATTERN) ?? (allowPiFormat ? line.match(PI_LINE_PATTERN) : null);
91
+ return match ? toParsedDiffLine(match[1] ?? " ", match[2] ?? "", match[3] ?? "") : null;
92
92
  }
93
93
 
94
94
  function toNumber(value: string | undefined): number | null {
@@ -194,12 +194,14 @@ export function parseDiff(diffText: string): ParsedDiff {
194
194
  let oldLineCursor: number | null = null;
195
195
  let newLineCursor: number | null = null;
196
196
  let lineNumberDelta = 0;
197
+ let hasHunkHeader = false;
197
198
 
198
199
  for (const rawLine of diffText.replace(/\r/g, "").split("\n")) {
199
200
  stats.lines++;
200
201
 
201
202
  const hunkMatch = rawLine.match(HUNK_HEADER_PATTERN);
202
203
  if (hunkMatch) {
204
+ hasHunkHeader = true;
203
205
  hunkIndex++;
204
206
  stats.hunks = Math.max(stats.hunks, hunkIndex);
205
207
  oldLineCursor = toNumber(hunkMatch[1]);
@@ -224,7 +226,14 @@ export function parseDiff(diffText: string): ParsedDiff {
224
226
  lineNumberDelta = 0;
225
227
  }
226
228
 
227
- 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);
228
237
  if (canonical) {
229
238
  hunkIndex = ensureImplicitHunk(hunkIndex);
230
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;