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.
- package/README.en.md +8 -8
- package/README.md +8 -8
- package/extensions/config/config.ts +46 -16
- package/extensions/config/panel.ts +173 -43
- package/extensions/feature/compact-thinking.ts +12 -12
- package/extensions/feature/shell/footer-currency.ts +29 -0
- package/extensions/feature/shell/footer.ts +40 -51
- package/extensions/feature/shell/git-stats.ts +162 -0
- package/extensions/renderer/compact-mode.ts +35 -6
- package/extensions/renderer/default-mode.ts +15 -9
- package/extensions/renderer/mouse/interaction.ts +27 -25
- package/extensions/renderer/tool/codemode.ts +4 -1
- package/extensions/renderer/tool/diff/diff-edit-render.ts +64 -11
- package/extensions/renderer/tool/names.ts +2 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
8
|
+
/** toolLabelClip=0 时不设字符上限,交给渲染时的实际宽度截断。 */
|
|
9
9
|
function clipLimit(): number {
|
|
10
|
-
return config.
|
|
10
|
+
return config.toolLabelClip > 0 ? config.toolLabelClip : Number.POSITIVE_INFINITY;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function clip(value: unknown): string {
|