pi-cc-extensions 0.8.67 → 0.8.69

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.
@@ -41,11 +41,8 @@ import { paddedBackgroundRow } from "./tool/grouping.ts";
41
41
  import { hasVisibleText, stripBackgroundAnsi } from "../utils/ansi-text.ts";
42
42
  import { walkComponentTree } from "../utils/component-tree.ts";
43
43
  import {
44
- ASSISTANT_REENTRY_DESCRIPTION,
45
44
  ASSISTANT_REENTRY_KEY,
46
- ASSISTANT_SET_EXPANDED_DESCRIPTION,
47
45
  ASSISTANT_SET_EXPANDED_KEY,
48
- ASSISTANT_TOGGLE_ROUND_DESCRIPTION,
49
46
  ASSISTANT_TOGGLE_ROUND_KEY,
50
47
  COMPACT_MODE_PATCH_KEY,
51
48
  COMPACT_THINKING_PATCH_KEY,
@@ -433,28 +430,16 @@ function isToolComponent(value: any): boolean {
433
430
  }
434
431
 
435
432
  function detachAssistantExpansion(component: any): void {
436
- let owned =
437
- typeof component?.[ASSISTANT_SET_EXPANDED_KEY] === "function" &&
438
- component.setExpanded === component[ASSISTANT_SET_EXPANDED_KEY];
439
- if (!owned && component && typeof component === "object") {
440
- // 首次升级时清理旧模块使用 Symbol() 留下的实例方法。
441
- owned = Object.getOwnPropertySymbols(component).some(
442
- (symbol) =>
443
- symbol.description === ASSISTANT_SET_EXPANDED_DESCRIPTION && component[symbol] === true,
444
- );
433
+ if (
434
+ typeof component?.[ASSISTANT_SET_EXPANDED_KEY] !== "function" ||
435
+ component.setExpanded !== component[ASSISTANT_SET_EXPANDED_KEY]
436
+ ) {
437
+ return;
445
438
  }
446
- if (!owned) return;
447
-
448
439
  delete component.setExpanded;
449
- for (const symbol of Object.getOwnPropertySymbols(component)) {
450
- if (
451
- symbol.description === ASSISTANT_SET_EXPANDED_DESCRIPTION ||
452
- symbol.description === ASSISTANT_TOGGLE_ROUND_DESCRIPTION ||
453
- symbol.description === ASSISTANT_REENTRY_DESCRIPTION
454
- ) {
455
- delete component[symbol];
456
- }
457
- }
440
+ delete component[ASSISTANT_SET_EXPANDED_KEY];
441
+ delete component[ASSISTANT_TOGGLE_ROUND_KEY];
442
+ delete component[ASSISTANT_REENTRY_KEY];
458
443
  }
459
444
 
460
445
  /** 供 mouse-interaction 识别可点击的 compact assistant 行(仅 compact 模式下生效)。 */
@@ -62,20 +62,13 @@ type ToolRenderMethods = {
62
62
  type GlobalToolRenderPatch = {
63
63
  version: 2;
64
64
  prototype: any;
65
- owner: object;
66
65
  active: boolean;
67
- enabled: () => boolean;
68
66
  mode: () => CompactStyleMode;
69
67
  wrap: (tool: any) => any;
70
68
  byDefinition: WeakMap<object, any>;
71
69
  byName: Map<string, any>;
72
70
  downstream: ToolRenderMethods;
73
71
  installed: ToolRenderMethods;
74
- // 兼容旧扩展读取 Symbol 上的 legacy 字段。
75
- originalHasRendererDefinition: ToolRenderMethods["hasRendererDefinition"];
76
- originalGetRenderShell: ToolRenderMethods["getRenderShell"];
77
- originalGetCallRenderer: ToolRenderMethods["getCallRenderer"];
78
- originalGetResultRenderer: ToolRenderMethods["getResultRenderer"];
79
72
  };
80
73
 
81
74
  type ToolExpandedBackgroundPatch = {
@@ -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,6 @@ 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(.*)$/;
44
43
  const HUNK_HEADER_PATTERN = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/;
45
44
  const MIN_LINE_NUMBER_WIDTH = 2;
46
45
 
@@ -81,14 +80,8 @@ function parseCanonicalDiffLine(line: string): {
81
80
  };
82
81
  }
83
82
 
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] ?? "");
83
+ const match = line.match(CANONICAL_LINE_PATTERN);
84
+ return match ? toParsedDiffLine(match[1] ?? " ", match[2] ?? "", match[3] ?? "") : null;
92
85
  }
93
86
 
94
87
  function toNumber(value: string | undefined): number | null {
@@ -2,7 +2,7 @@ import { config } from "../../config/config.ts";
2
2
  import { oneLine } from "../../utils/format.ts";
3
3
 
4
4
  function clip(value: unknown): string {
5
- return oneLine(value, config.toolInputNameLength);
5
+ return oneLine(value, config.inputClip);
6
6
  }
7
7
 
8
8
  /**
@@ -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
  }