pi-cc-extensions 0.9.10 → 0.9.12

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.
@@ -13,7 +13,6 @@ import {
13
13
  setHoveredCompactAssistant,
14
14
  } from "../compact-mode.ts";
15
15
  import { isMessageDisplayComponent } from "../tool/message-display.ts";
16
- import { config } from "../../config/config.ts";
17
16
  import { isLazyProxyTui } from "../../utils/fullscreen-detect.ts";
18
17
  import { setToolTuiFullscreen, isCollapseHintLine } from "../tool/show-more-hint.ts";
19
18
  import {
@@ -308,10 +307,6 @@ function ensureFullscreenToolMouseMotion(tui: any): void {
308
307
  releaseFullscreenToolMouseMotion(tui);
309
308
  return;
310
309
  }
311
- // 面板改 scrollStepLines 后,下一帧渲染即同步(restore 仍按 original 恢复)。
312
- if (typeof tui.wheelScrollLines === "number" && tui.wheelScrollLines !== config.scrollStepLines) {
313
- tui.wheelScrollLines = config.scrollStepLines;
314
- }
315
310
  if (
316
311
  !toolMouseInteractionActive() ||
317
312
  tui.mouseEnabled === false ||
@@ -348,7 +343,6 @@ function releaseFullscreenToolMouseMotion(tui?: any): void {
348
343
  }
349
344
 
350
345
  const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
351
- const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol("ccstyle.fullscreen-wheel-scroll-original");
352
346
 
353
347
  /**
354
348
  * diff 结果组件自带 remainder 行:声明存在时只有那一行是展开入口(正文里的同名字样不算);
@@ -418,11 +412,11 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
418
412
  if (!isTool && !isGroup && !isAssistant && !isThinking && !isMessage) return false;
419
413
  if (!component.expanded) {
420
414
  // collapsed 仅按钮文本可展开,不能把同一行正文/留白变成点击区。
415
+ // thinking 例外:标题行(标题、隐藏行数与提示同一行)整行都是展开入口。
421
416
  const hint = collapsedHintHitbox(line);
422
417
  const onHint = Boolean(
423
418
  hint &&
424
- packet.col >= hint.startCol &&
425
- packet.col <= hint.endCol &&
419
+ (isThinking || (packet.col >= hint.startCol && packet.col <= hint.endCol)) &&
426
420
  isCollapsedHintRow(component, line),
427
421
  );
428
422
  if (!onHint) {
@@ -437,18 +431,16 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
437
431
  if (other !== component && other.expanded) {
438
432
  // 展开 round 卡内 thinking/工具时,外层 compact 卡是它的容器,不能收起。
439
433
  // 面板挂在摘要行时,anchor 与摘要行是同一张外层卡,点内部工具不能收起它。
440
- if (
441
- (isThinking || isTool) &&
442
- isCompactAssistantComponent(other) &&
443
- (other === target.owner || other === target.owner?.roundAnchor)
444
- )
445
- continue;
434
+ // 同一回合的摘要行、anchor 和其他消息都是外层面板,收起任一个都会关掉整块。
435
+ if ((isThinking || isTool) && isSameExpandedRound(other, target.owner)) continue;
446
436
  other.setExpanded(false);
447
437
  other.invalidate?.();
448
438
  }
449
439
  }
450
440
  // 展开 round 卡内工具时,让 compact 的强制折叠放行它(非 round 内工具为空操作)。
451
441
  if (isTool) markCompactRoundToolExpanded(component);
442
+ // compact 摘要行自己记展开前的视口(收起时还原跟随),这里不动它。
443
+ if (!isAssistant) keepViewportOnExpand(tui);
452
444
  component.setExpanded(true);
453
445
  clearPendingCollapsePress();
454
446
  } else {
@@ -489,6 +481,25 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
489
481
  return true;
490
482
  }
491
483
 
484
+ /** 点到的组件和外层卡是否属于同一张 compact 回合面板。 */
485
+ function isSameExpandedRound(other: any, owner: any): boolean {
486
+ if (!owner || !isCompactAssistantComponent(other)) return false;
487
+ const anchor = owner.roundAnchor ?? owner;
488
+ return other === owner || other === anchor || other.roundAnchor === anchor;
489
+ }
490
+
491
+ /**
492
+ * 原地展开时停在当前滚动位置:transcript 跟随底部时,内容变高会把视口拖到底,
493
+ * 点击的行和它上方的外层面板被顶出屏幕。展开前关掉跟随,点击行留在原位。
494
+ */
495
+ function keepViewportOnExpand(tui: any): void {
496
+ const view = tui.getPrimaryScrollView?.();
497
+ if (typeof view?.scrollTo !== "function" || typeof view.scrollTop !== "number") return;
498
+ view.scrollTo(view.scrollTop, { disableFollow: true });
499
+ // 已脱离底部跟随:亮出回到底部按钮,新输出到来时仍有入口。
500
+ if (view.isFollowingEnd === false) setScrollButtonVisible(true);
501
+ }
502
+
492
503
  /**
493
504
  * fullscreen 鼠标悬停:collapsed 卡 [click to show more] hint、
494
505
  * expanded 卡截断头 show-more、回到底部按钮。motion 不 consume,官方链照常。
@@ -528,7 +539,8 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
528
539
  if (component instanceof ToolGroupComponent) {
529
540
  if (overHint) target = { kind: "group", component };
530
541
  } else if (component instanceof ThinkingPreviewBlock) {
531
- if (component.expanded || overHint) target = { kind: "thinking", component };
542
+ // thinking 折叠标题行整行可点:整块 hover 都高亮动作词,不只高亮按钮文本。
543
+ if (component.expanded || Boolean(hintBox)) target = { kind: "thinking", component };
532
544
  } else if (isToolExecutionComponent(component)) {
533
545
  let view: ExpandedToolIoView | null = null;
534
546
  let section: ToolIoSection | null = null;
@@ -571,11 +583,6 @@ function patchFullscreenViewportInput(tui: any): void {
571
583
  const proto = Object.getPrototypeOf(tui);
572
584
  const original = proto?.handleViewportInput;
573
585
  if (typeof original !== "function") return;
574
- // 官方原生 routeWheel 已完整处理嵌套 ScrollView;只调整默认步进(config.scrollStepLines)。
575
- if (typeof tui.wheelScrollLines === "number") {
576
- tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = tui.wheelScrollLines;
577
- tui.wheelScrollLines = config.scrollStepLines;
578
- }
579
586
  tui[FULLSCREEN_VIEWPORT_PATCH] = true;
580
587
  tui.handleViewportInput = function (this: any, data: string) {
581
588
  if (toolMouseInteractionActive() && tui.mode === "fullscreen") {
@@ -624,11 +631,6 @@ function restoreFullscreenViewportInput(tui: any): void {
624
631
  if (typeof proto?.handleViewportInput === "function") {
625
632
  tui.handleViewportInput = proto.handleViewportInput;
626
633
  }
627
- const originalWheelScrollLines = tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL];
628
- if (typeof originalWheelScrollLines === "number") {
629
- tui.wheelScrollLines = originalWheelScrollLines;
630
- tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = undefined;
631
- }
632
634
  tui[FULLSCREEN_VIEWPORT_PATCH] = false;
633
635
  }
634
636
 
@@ -235,7 +235,10 @@ export function codemodeCollapsedLines(options: {
235
235
  const summary = codemodeSummaryParts(calls, codemodeOutputLineCount(result), running);
236
236
  // 没有子调用时不画衔接符,避免孤零零一个 └
237
237
  const prefix = shown.length > 0 ? `${INDENT}${fg("dim", "└")} ` : INDENT;
238
- const hint = running ? "" : ` ${fg(options.hovered ? "text" : "dim", `· ${showMoreHintText()}`)}`;
238
+ // 圆点保持 dim;hover 只高亮可点击文字。
239
+ const hint = running
240
+ ? ""
241
+ : ` ${fg("dim", "·")} ${fg(options.hovered ? "text" : "dim", showMoreHintText())}`;
239
242
  const bodyWidth = Math.max(1, rowWidth - visibleWidth(prefix) - visibleWidth(hint));
240
243
  const body = fg(isError ? "error" : "muted", fitSummaryParts(summary, bodyWidth));
241
244
  lines.push(truncateToWidth(`${prefix}${body}${hint}`, rowWidth, ""));
@@ -1,6 +1,7 @@
1
- import { Text, type Component } from "@earendil-works/pi-tui";
1
+ import { Text, visibleWidth, type Component } from "@earendil-works/pi-tui";
2
2
  import type { EditToolDetails } from "@earendil-works/pi-coding-agent";
3
3
  import { sanitizeToolResultText } from "../../../utils/tool-result-sanitize.ts";
4
+ import { showMoreHintText } from "../show-more-hint.ts";
4
5
  import {
5
6
  getLineNumberWidth,
6
7
  parseDiff,
@@ -20,6 +21,7 @@ import {
20
21
  import { renderDiffFrameLine, renderHeaderRows, renderSingleDiffRow } from "./diff-header.ts";
21
22
  import {
22
23
  applyLineLimit,
24
+ clampDiffLineToWidth,
23
25
  clampDiffLinesToWidth,
24
26
  resolveDiffDisplayLimit,
25
27
  resolveDiffProcessBudget,
@@ -81,6 +83,33 @@ export function countEditDiffStats(
81
83
  }
82
84
  }
83
85
 
86
+ /** Keep counts in the result row: the default-mode edit title does not include them. */
87
+ function renderEditCollapsedSummaryLine(
88
+ stats: ParsedDiff["stats"],
89
+ width: number,
90
+ theme: DiffTheme,
91
+ hovered: boolean,
92
+ ): string {
93
+ const counts = `+${stats.added} -${stats.removed}`;
94
+ const hint = showMoreHintText();
95
+ const candidates = [
96
+ `↳ diff ${counts} · ${hint}`,
97
+ `↳ ${counts} · ${hint}`,
98
+ `${counts} · ${hint}`,
99
+ `↳ diff ${counts}`,
100
+ counts,
101
+ "diff",
102
+ "…",
103
+ ];
104
+ const text = candidates.find((candidate) => visibleWidth(candidate) <= width) ?? "";
105
+ const hintIndex = text.lastIndexOf(hint);
106
+ const styled =
107
+ hovered && hintIndex >= 0
108
+ ? theme.fg("muted", text.slice(0, hintIndex)) + theme.fg("text", hint)
109
+ : theme.fg("muted", text);
110
+ return clampDiffLineToWidth(styled, width);
111
+ }
112
+
84
113
  export function renderEditDiffResult(
85
114
  details: unknown,
86
115
  options: DiffRenderOptions,
@@ -110,25 +139,33 @@ export function renderEditDiffResult(
110
139
  return new Text(theme.fg("muted", "↳ no diff data"), 0, 0);
111
140
  }
112
141
 
113
- const splitRows = buildSplitRows(renderEntries);
114
- const showHashlineAnchors =
115
- options.expanded === true &&
116
- renderEntries.some((entry) => entry.kind === "line" && !!entry.hashlineAnchorContent);
117
- const lineNumberWidth = getLineNumberWidth(renderEntries, showHashlineAnchors);
118
142
  const palette = resolveDiffPalette(theme);
119
143
  // Rich diffs use ccstyle's self shell. Keep the panel transparent so the
120
144
  // separator cannot leak toolSuccessBg across the entire new column.
121
145
  const containerBgAnsi = undefined;
122
146
  const language = resolveLanguageFromPath(options.filePath);
123
147
  const cache = createDiffRenderCache();
124
- const highlightLine = createCodeLineHighlighter(language, theme, renderEntries, () => {
125
- cache.invalidate();
126
- options.invalidate?.();
127
- });
148
+ // Summary-only previews need parsed counts, not split rows or syntax highlighting.
149
+ // Keep detailed data lazy so a live setting change can still reveal the same diff.
150
+ function buildDetailedData() {
151
+ const showHashlineAnchors =
152
+ options.expanded === true &&
153
+ renderEntries.some((entry) => entry.kind === "line" && !!entry.hashlineAnchorContent);
154
+ return {
155
+ splitRows: buildSplitRows(renderEntries),
156
+ showHashlineAnchors,
157
+ lineNumberWidth: getLineNumberWidth(renderEntries, showHashlineAnchors),
158
+ highlightLine: createCodeLineHighlighter(language, theme, renderEntries, () => {
159
+ cache.invalidate();
160
+ options.invalidate?.();
161
+ }),
162
+ };
163
+ }
164
+ let detailedData: ReturnType<typeof buildDetailedData> | undefined;
128
165
 
129
166
  return {
130
167
  [RICH_DIFF_COMPONENT]: true,
131
- /** 鼠标层只在真正的 remainder 行上提供展开入口。 */
168
+ /** Only the current summary/remainder row is an expand target, never diff body text. */
132
169
  isCollapsedHintLine(plainLine: string): boolean {
133
170
  const hint = cache.getHintLine();
134
171
  return hint !== undefined && normalizeCollapsedHintLine(plainLine) === hint;
@@ -147,6 +184,20 @@ export function renderEditDiffResult(
147
184
  return cached;
148
185
  }
149
186
 
187
+ // Like write's zero-line preview, handle this before the normal body limiter.
188
+ if (!options.expanded && live.editDiffCollapsedLines === 0) {
189
+ const summary = renderEditCollapsedSummaryLine(parsed.stats, safeWidth, theme, hovered);
190
+ return cache.set(
191
+ safeWidth,
192
+ options.expanded,
193
+ mode,
194
+ configKey,
195
+ hovered,
196
+ [summary],
197
+ normalizeCollapsedHintLine(summary),
198
+ );
199
+ }
200
+
150
201
  if (mode === "summary") {
151
202
  return cache.set(
152
203
  safeWidth,
@@ -166,6 +217,8 @@ export function renderEditDiffResult(
166
217
  );
167
218
  }
168
219
 
220
+ const { splitRows, showHashlineAnchors, lineNumberWidth, highlightLine } = (detailedData ??=
221
+ buildDetailedData());
169
222
  const headerRows = renderHeaderRows(parsed.stats, mode, safeWidth, theme);
170
223
  const displayLimit = resolveDiffDisplayLimit(options.expanded, live.editDiffCollapsedLines);
171
224
  const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
@@ -5,9 +5,9 @@ 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 时不设字符上限,交给渲染时的实际宽度截断。 */
8
+ /** toolLabelClip=0 时不设字符上限,交给渲染时的实际宽度截断。 */
9
9
  function clipLimit(): number {
10
- return config.inputClip > 0 ? config.inputClip : Number.POSITIVE_INFINITY;
10
+ return config.toolLabelClip > 0 ? config.toolLabelClip : Number.POSITIVE_INFINITY;
11
11
  }
12
12
 
13
13
  function clip(value: unknown): string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.9.10",
3
+ "version": "0.9.12",
4
4
  "license": "MIT",
5
5
  "description": "Claude Code-style TUI output with some personal touches and handy utilities.",
6
6
  "type": "module",