pi-cc-extensions 0.9.11 → 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 +3 -2
- package/README.md +3 -2
- package/extensions/config/config.ts +23 -10
- package/extensions/config/panel.ts +39 -14
- package/extensions/feature/shell/footer.ts +35 -49
- package/extensions/feature/shell/git-stats.ts +162 -0
- package/extensions/renderer/compact-mode.ts +13 -3
- package/extensions/renderer/tool/diff/diff-edit-render.ts +64 -11
- package/extensions/renderer/tool/names.ts +2 -2
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -61,14 +61,14 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
61
61
|
"expandedOutputMaxLines": 10, // expanded tool Output lines; overflow shows a footer hint
|
|
62
62
|
"expandedPreviewMaxLines": 40, // max lines for expanded TaskList bodies (expanded diffs always show all)
|
|
63
63
|
"expandedCardBackground": "userMessageBg", // expanded card background slot: userMessageBg (same as user prompts) / toolPendingBg (neutral) etc.
|
|
64
|
-
"
|
|
64
|
+
"toolLabelClip": 0, // tool summary path/command clip length; 0 = fit width
|
|
65
65
|
"showStartupHeader": true, // startup header (logo + tips) toggle
|
|
66
66
|
|
|
67
67
|
// diff
|
|
68
68
|
"diffViewMode": "auto", // layout: auto / split / unified
|
|
69
69
|
"diffIndicatorMode": "bars", // change indicators: bars / classic / none
|
|
70
70
|
"diffSplitMinWidth": 120, // min terminal width for side-by-side columns
|
|
71
|
-
"editDiffCollapsedLines": 24, //
|
|
71
|
+
"editDiffCollapsedLines": 24, // edit collapse lines; 0 = change-count summary only
|
|
72
72
|
"writeDiffCollapsedLines": 0, // write collapse lines; 0 = creation summary only
|
|
73
73
|
"diffWordWrap": true, // wrap long diff lines
|
|
74
74
|
|
|
@@ -83,6 +83,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
83
83
|
"footerCurrency": "USD", // display currency for the footer cost
|
|
84
84
|
"footerCurrencyRate": null, // display units per 1 cost unit; null changes the symbol only
|
|
85
85
|
"footerNerdIcons": true, // Nerd Font glyphs for git/cache; false = plain text
|
|
86
|
+
"footerGitStatsMode": "working", // working (default): uncommitted; branch: cumulative net changes
|
|
86
87
|
"footerHiddenKeys": [], // hidden plugin chip keys
|
|
87
88
|
"footerLine1Keys": ["pi-usage"], // line1 plugin chip order; pi-usage is shown by default, data from @narumitw/pi-usage
|
|
88
89
|
"footerLine2Keys": [], // line2 plugin chip order (after cwd/git)
|
package/README.md
CHANGED
|
@@ -61,14 +61,14 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
61
61
|
"expandedOutputMaxLines": 10, // 展开工具卡 Output 可见行数,超出在末行显示展开提示
|
|
62
62
|
"expandedPreviewMaxLines": 40, // 展开 TaskList 正文最大行数(展开 diff 始终全量)
|
|
63
63
|
"expandedCardBackground": "userMessageBg", // 展开卡背景槽位;userMessageBg 与用户消息同色,toolPendingBg 为中性灰
|
|
64
|
-
"
|
|
64
|
+
"toolLabelClip": 0, // 工具摘要 path/command 折叠字符数;0 = 按可用宽度
|
|
65
65
|
"showStartupHeader": true, // 启动头(logo + tips)开关
|
|
66
66
|
|
|
67
67
|
// diff
|
|
68
68
|
"diffViewMode": "auto", // 布局:auto / split / unified
|
|
69
69
|
"diffIndicatorMode": "bars", // 变更指示:bars / classic / none
|
|
70
70
|
"diffSplitMinWidth": 120, // 左右分栏的最小终端宽度
|
|
71
|
-
"editDiffCollapsedLines": 24, //
|
|
71
|
+
"editDiffCollapsedLines": 24, // edit 折叠行数;0 仅显示变更统计摘要
|
|
72
72
|
"writeDiffCollapsedLines": 0, // write 折叠行数,0 仅显示创建摘要
|
|
73
73
|
"diffWordWrap": true, // 长 diff 行换行
|
|
74
74
|
|
|
@@ -83,6 +83,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
83
83
|
"footerCurrency": "USD", // 底栏费用的显示币种
|
|
84
84
|
"footerCurrencyRate": null, // 1 单位费用换成多少显示币种;null 只换符号、不乘倍率
|
|
85
85
|
"footerNerdIcons": true, // git/缓存用 Nerd Font 图标;false 为纯文本
|
|
86
|
+
"footerGitStatsMode": "working", // working(默认):未提交改动;branch:分支累计净改动
|
|
86
87
|
"footerHiddenKeys": [], // 隐藏的插件芯片 key
|
|
87
88
|
"footerLine1Keys": ["pi-usage"], // line1 插件芯片顺序;pi-usage 默认显示,数据来自 @narumitw/pi-usage
|
|
88
89
|
"footerLine2Keys": [], // line2 插件芯片顺序(接在 cwd/git 后)
|
|
@@ -13,11 +13,13 @@ export type CompactStyleMode = "on" | "compact" | "off";
|
|
|
13
13
|
|
|
14
14
|
export type DiffViewMode = "auto" | "split" | "unified";
|
|
15
15
|
export type DiffIndicatorMode = "bars" | "classic" | "none";
|
|
16
|
+
export type FooterGitStatsMode = "working" | "branch";
|
|
16
17
|
|
|
17
18
|
export interface ToolDisplayConfig {
|
|
18
19
|
diffViewMode: DiffViewMode;
|
|
19
20
|
diffIndicatorMode: DiffIndicatorMode;
|
|
20
21
|
diffSplitMinWidth: number;
|
|
22
|
+
/** Edit-only collapsed body lines. 0 = change counts and an expand hint. */
|
|
21
23
|
editDiffCollapsedLines: number;
|
|
22
24
|
/** Write-only collapsed body lines. 0 = `↳ created · click to show more`. */
|
|
23
25
|
writeDiffCollapsedLines: number;
|
|
@@ -57,7 +59,7 @@ export type Config = {
|
|
|
57
59
|
expandedCardBackground: ThemeBg;
|
|
58
60
|
expandedInputMaxLines: number;
|
|
59
61
|
expandedOutputMaxLines: number;
|
|
60
|
-
|
|
62
|
+
toolLabelClip: number;
|
|
61
63
|
useSummaryTitlesAsThinkingTitle: boolean;
|
|
62
64
|
previewLines: number;
|
|
63
65
|
animationIntervalMs: number;
|
|
@@ -75,6 +77,7 @@ export type Config = {
|
|
|
75
77
|
/** 1 单位费用对应的显示币种数量。null 只换符号、不乘倍率。 */
|
|
76
78
|
footerCurrencyRate: number | null;
|
|
77
79
|
footerNerdIcons: boolean;
|
|
80
|
+
footerGitStatsMode: FooterGitStatsMode;
|
|
78
81
|
footerHiddenKeys: string[];
|
|
79
82
|
footerLine1Keys: string[];
|
|
80
83
|
footerLine2Keys: string[];
|
|
@@ -87,8 +90,9 @@ const LEGACY_CONFIG_PATH = join(AGENT_DIR, "claude-code-style.json");
|
|
|
87
90
|
|
|
88
91
|
export const DIFF_VIEW_MODES: DiffViewMode[] = ["auto", "split", "unified"];
|
|
89
92
|
export const DIFF_INDICATOR_MODES: DiffIndicatorMode[] = ["bars", "classic", "none"];
|
|
93
|
+
export const FOOTER_GIT_STATS_MODES: FooterGitStatsMode[] = ["working", "branch"];
|
|
90
94
|
export const DIFF_SPLIT_MIN_WIDTH_VALUES = ["80", "100", "120", "140", "160", "180"];
|
|
91
|
-
export const DIFF_COLLAPSED_LINES_VALUES = ["12", "24", "36", "48", "80", "120"];
|
|
95
|
+
export const DIFF_COLLAPSED_LINES_VALUES = ["0", "12", "24", "36", "48", "80", "120"];
|
|
92
96
|
/** Write collapsed presets. 0 = stats only (`+N -0` + expand hint). */
|
|
93
97
|
export const WRITE_DIFF_COLLAPSED_LINES_VALUES = ["0", "4", "8", "12", "24", "36"];
|
|
94
98
|
/** Presets for expanded body height — keep low options first so cycling stays TUI-friendly. */
|
|
@@ -112,7 +116,7 @@ export const EXPANDED_INPUT_MAX_LINES_VALUES = ["5", "10", "20", "40", "80"];
|
|
|
112
116
|
/** 展开工具卡 Output 可见行数预设。 */
|
|
113
117
|
export const EXPANDED_OUTPUT_MAX_LINES_VALUES = ["10", "20", "40", "80", "120"];
|
|
114
118
|
/** 工具摘要里 path/command 等输入的折叠字符数;0 = 只按可用宽度截断。 */
|
|
115
|
-
export const
|
|
119
|
+
export const TOOL_LABEL_CLIP_VALUES = ["0", "40", "60", "80", "100", "120", "160"];
|
|
116
120
|
export const THINKING_PREVIEW_LINES_VALUES = ["0", "1", "3", "5", "10"];
|
|
117
121
|
export const THINKING_ANIMATION_INTERVAL_VALUES = ["40", "60", "90", "120", "180"];
|
|
118
122
|
/** Tools commonly toggled in excludeRenderers via the settings panel. */
|
|
@@ -143,7 +147,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
143
147
|
expandedCardBackground: "userMessageBg",
|
|
144
148
|
expandedInputMaxLines: 5,
|
|
145
149
|
expandedOutputMaxLines: 10,
|
|
146
|
-
|
|
150
|
+
toolLabelClip: 0,
|
|
147
151
|
useSummaryTitlesAsThinkingTitle: true,
|
|
148
152
|
previewLines: 3,
|
|
149
153
|
animationIntervalMs: 90,
|
|
@@ -159,6 +163,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
159
163
|
footerCurrency: "USD",
|
|
160
164
|
footerCurrencyRate: null,
|
|
161
165
|
footerNerdIcons: true,
|
|
166
|
+
footerGitStatsMode: "working",
|
|
162
167
|
...DEFAULT_FOOTER_CHIP_LAYOUT,
|
|
163
168
|
};
|
|
164
169
|
|
|
@@ -174,10 +179,10 @@ export function pickPositiveInt(value: unknown, fallback: number, min = 1, max =
|
|
|
174
179
|
return Math.min(max, Math.max(min, Math.floor(n)));
|
|
175
180
|
}
|
|
176
181
|
|
|
177
|
-
/**
|
|
178
|
-
export function
|
|
182
|
+
/** toolLabelClip:0 表示按宽度自适应,其余夹到 8..500。 */
|
|
183
|
+
export function pickToolLabelClip(value: unknown): number {
|
|
179
184
|
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
180
|
-
return n === 0 ? 0 : pickPositiveInt(value, DEFAULT_CONFIG.
|
|
185
|
+
return n === 0 ? 0 : pickPositiveInt(value, DEFAULT_CONFIG.toolLabelClip, 8, 500);
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
export function pickPositiveNumber(value: unknown, fallback: number, min = 1): number {
|
|
@@ -228,7 +233,7 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
228
233
|
editDiffCollapsedLines: pickPositiveInt(
|
|
229
234
|
source.editDiffCollapsedLines,
|
|
230
235
|
DEFAULT_CONFIG.editDiffCollapsedLines,
|
|
231
|
-
|
|
236
|
+
0,
|
|
232
237
|
500,
|
|
233
238
|
),
|
|
234
239
|
writeDiffCollapsedLines: pickPositiveInt(
|
|
@@ -261,7 +266,9 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
261
266
|
1,
|
|
262
267
|
5_000,
|
|
263
268
|
),
|
|
264
|
-
|
|
269
|
+
toolLabelClip: pickToolLabelClip(
|
|
270
|
+
source.toolLabelClip ?? source.inputClip, // 兼容旧配置键
|
|
271
|
+
),
|
|
265
272
|
useSummaryTitlesAsThinkingTitle: source.useSummaryTitlesAsThinkingTitle !== false,
|
|
266
273
|
previewLines: pickPositiveInt(
|
|
267
274
|
source.previewLines,
|
|
@@ -285,6 +292,11 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
285
292
|
footerCurrency: normalizeCurrency(source.footerCurrency),
|
|
286
293
|
footerCurrencyRate: normalizeFooterCurrencyRate(source.footerCurrencyRate),
|
|
287
294
|
footerNerdIcons: source.footerNerdIcons !== false,
|
|
295
|
+
footerGitStatsMode: pickEnum(
|
|
296
|
+
source.footerGitStatsMode,
|
|
297
|
+
FOOTER_GIT_STATS_MODES,
|
|
298
|
+
DEFAULT_CONFIG.footerGitStatsMode,
|
|
299
|
+
),
|
|
288
300
|
...normalizeFooterChipLayout(source),
|
|
289
301
|
};
|
|
290
302
|
}
|
|
@@ -326,7 +338,7 @@ export function formatConfigStatus(source: Config = config): string {
|
|
|
326
338
|
`expandedMax=${source.expandedPreviewMaxLines}`,
|
|
327
339
|
`expandedInput=${source.expandedInputMaxLines}`,
|
|
328
340
|
`expandedOutput=${source.expandedOutputMaxLines}`,
|
|
329
|
-
`
|
|
341
|
+
`toolLabelClip=${source.toolLabelClip}`,
|
|
330
342
|
`thinkingTitle=${source.useSummaryTitlesAsThinkingTitle ? "summary" : "default"}`,
|
|
331
343
|
`thinkingPreview=${source.previewLines}`,
|
|
332
344
|
`thinkingAnimation=${source.animationIntervalMs}ms`,
|
|
@@ -342,6 +354,7 @@ export function formatConfigStatus(source: Config = config): string {
|
|
|
342
354
|
`footerCurrency=${source.footerCurrency}`,
|
|
343
355
|
`footerRate=${source.footerCurrencyRate ?? "-"}`,
|
|
344
356
|
`footerIcons=${source.footerNerdIcons ? "nerd" : "plain"}`,
|
|
357
|
+
`footerGitStats=${source.footerGitStatsMode}`,
|
|
345
358
|
formatFooterChipSummary(source),
|
|
346
359
|
].join(" · ");
|
|
347
360
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
applyCustomFooter,
|
|
12
12
|
clearCustomFooter,
|
|
13
13
|
getFooterStatusSnapshot,
|
|
14
|
+
refreshFooterGitStats,
|
|
14
15
|
} from "../feature/shell/footer.ts";
|
|
15
16
|
import {
|
|
16
17
|
FOOTER_CURRENCY_PRESETS,
|
|
@@ -41,20 +42,22 @@ import {
|
|
|
41
42
|
EXPANDED_INPUT_MAX_LINES_VALUES,
|
|
42
43
|
EXPANDED_OUTPUT_MAX_LINES_VALUES,
|
|
43
44
|
EXPANDED_PREVIEW_MAX_LINES_VALUES,
|
|
45
|
+
FOOTER_GIT_STATS_MODES,
|
|
44
46
|
formatExcludeRenderers,
|
|
45
47
|
getCompactThinkingConfig,
|
|
46
|
-
pickInputClip,
|
|
47
48
|
pickPositiveInt,
|
|
48
49
|
pickPositiveNumber,
|
|
50
|
+
pickToolLabelClip,
|
|
49
51
|
THINKING_ANIMATION_INTERVAL_VALUES,
|
|
50
52
|
THINKING_PREVIEW_LINES_VALUES,
|
|
51
|
-
|
|
53
|
+
TOOL_LABEL_CLIP_VALUES,
|
|
52
54
|
WRITE_DIFF_COLLAPSED_LINES_VALUES,
|
|
53
55
|
updateConfig,
|
|
54
56
|
type CompactStyleMode,
|
|
55
57
|
type Config,
|
|
56
58
|
type DiffIndicatorMode,
|
|
57
59
|
type DiffViewMode,
|
|
60
|
+
type FooterGitStatsMode,
|
|
58
61
|
} from "./config.ts";
|
|
59
62
|
|
|
60
63
|
/** renderer 注入的渲染副作用,面板自身不触碰渲染状态。 */
|
|
@@ -84,6 +87,12 @@ function customFooterDescription(enabled: boolean): string {
|
|
|
84
87
|
return "Custom status bar with model, context, cache, cost, git, and plugin chips.";
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
function footerGitStatsDescription(mode: FooterGitStatsMode): string {
|
|
91
|
+
return mode === "branch"
|
|
92
|
+
? "Net changes since the merge base, including uncommitted edits. Base: origin/HEAD, then local main/master. Untracked files excluded; unavailable stats hidden."
|
|
93
|
+
: "Uncommitted net changes relative to HEAD (default). Includes staged and unstaged edits; excludes untracked files.";
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
function pluginChipsDescription(): string {
|
|
88
97
|
return `Arrange plugin chips: ${formatFooterChipSummary(config)}. Enter to edit.`;
|
|
89
98
|
}
|
|
@@ -444,15 +453,15 @@ export async function showCcstylePanel(
|
|
|
444
453
|
);
|
|
445
454
|
},
|
|
446
455
|
};
|
|
447
|
-
const
|
|
448
|
-
id: "
|
|
449
|
-
label: "
|
|
456
|
+
const toolLabelClipSetting = {
|
|
457
|
+
id: "toolLabelClip",
|
|
458
|
+
label: "Tool label clip",
|
|
450
459
|
description:
|
|
451
460
|
"Max characters for path/command/name in single and grouped tool summaries; 0 fits available width. Enter to type a custom value.",
|
|
452
|
-
currentValue: String(config.
|
|
453
|
-
values: [...
|
|
461
|
+
currentValue: String(config.toolLabelClip),
|
|
462
|
+
values: [...TOOL_LABEL_CLIP_VALUES],
|
|
454
463
|
submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
|
|
455
|
-
buildNumberInputSubmenu(theme,
|
|
464
|
+
buildNumberInputSubmenu(theme, toolLabelClipSetting, closeSubmenu),
|
|
456
465
|
};
|
|
457
466
|
const diffViewSetting = {
|
|
458
467
|
id: "diffViewMode",
|
|
@@ -482,7 +491,7 @@ export async function showCcstylePanel(
|
|
|
482
491
|
id: "editDiffCollapsedLines",
|
|
483
492
|
label: "Edit collapsed lines",
|
|
484
493
|
description:
|
|
485
|
-
"
|
|
494
|
+
"Edit diff body lines before the expand hint (Ctrl+O / click). 0 shows only change counts and the hint.",
|
|
486
495
|
currentValue: String(config.editDiffCollapsedLines),
|
|
487
496
|
values: [...DIFF_COLLAPSED_LINES_VALUES],
|
|
488
497
|
submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
|
|
@@ -705,6 +714,13 @@ export async function showCcstylePanel(
|
|
|
705
714
|
currentValue: config.enableCustomFooter ? "on" : "off",
|
|
706
715
|
values: ["on", "off"],
|
|
707
716
|
};
|
|
717
|
+
const footerGitStatsSetting = {
|
|
718
|
+
id: "footerGitStatsMode",
|
|
719
|
+
label: "Git changes",
|
|
720
|
+
description: footerGitStatsDescription(config.footerGitStatsMode),
|
|
721
|
+
currentValue: config.footerGitStatsMode,
|
|
722
|
+
values: FOOTER_GIT_STATS_MODES,
|
|
723
|
+
};
|
|
708
724
|
const pluginChipsSetting = {
|
|
709
725
|
id: "footerPluginChips",
|
|
710
726
|
label: "Plugin chips",
|
|
@@ -769,6 +785,14 @@ export async function showCcstylePanel(
|
|
|
769
785
|
ctx.ui.notify(`Updated ${id}: ${footerCurrencyRateSetting.currentValue}`, "info");
|
|
770
786
|
return;
|
|
771
787
|
}
|
|
788
|
+
if (id === "footerGitStatsMode") {
|
|
789
|
+
updateConfig({ footerGitStatsMode: value as FooterGitStatsMode });
|
|
790
|
+
footerGitStatsSetting.currentValue = config.footerGitStatsMode;
|
|
791
|
+
footerGitStatsSetting.description = footerGitStatsDescription(config.footerGitStatsMode);
|
|
792
|
+
refreshFooterGitStats();
|
|
793
|
+
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
772
796
|
if (id === "footerNerdIcons") {
|
|
773
797
|
const enabled = value === "on";
|
|
774
798
|
updateConfig({ footerNerdIcons: enabled });
|
|
@@ -788,11 +812,11 @@ export async function showCcstylePanel(
|
|
|
788
812
|
return;
|
|
789
813
|
}
|
|
790
814
|
switch (id) {
|
|
791
|
-
case "
|
|
815
|
+
case "toolLabelClip":
|
|
792
816
|
updateConfig({
|
|
793
|
-
|
|
817
|
+
toolLabelClip: pickToolLabelClip(value),
|
|
794
818
|
});
|
|
795
|
-
|
|
819
|
+
toolLabelClipSetting.currentValue = String(config.toolLabelClip);
|
|
796
820
|
break;
|
|
797
821
|
case "mode": {
|
|
798
822
|
// 选项值带 Experimental 标记,选择后还原为真实 mode 值。
|
|
@@ -825,7 +849,7 @@ export async function showCcstylePanel(
|
|
|
825
849
|
editDiffCollapsedLines: pickPositiveInt(
|
|
826
850
|
value,
|
|
827
851
|
DEFAULT_CONFIG.editDiffCollapsedLines,
|
|
828
|
-
|
|
852
|
+
0,
|
|
829
853
|
500,
|
|
830
854
|
),
|
|
831
855
|
});
|
|
@@ -950,7 +974,7 @@ export async function showCcstylePanel(
|
|
|
950
974
|
expandedOutputSetting,
|
|
951
975
|
expandedMaxSetting,
|
|
952
976
|
expandedCardBgSetting,
|
|
953
|
-
|
|
977
|
+
toolLabelClipSetting,
|
|
954
978
|
startupHeaderSetting,
|
|
955
979
|
],
|
|
956
980
|
},
|
|
@@ -984,6 +1008,7 @@ export async function showCcstylePanel(
|
|
|
984
1008
|
footerCurrencySetting,
|
|
985
1009
|
footerCurrencyRateSetting,
|
|
986
1010
|
footerNerdIconsSetting,
|
|
1011
|
+
footerGitStatsSetting,
|
|
987
1012
|
pluginChipsSetting,
|
|
988
1013
|
],
|
|
989
1014
|
},
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
|
|
15
15
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
17
|
-
import { execFile } from "node:child_process";
|
|
18
17
|
import { existsSync } from "node:fs";
|
|
19
18
|
import { homedir } from "node:os";
|
|
20
19
|
import { join } from "node:path";
|
|
@@ -29,8 +28,11 @@ import {
|
|
|
29
28
|
visibleFooterPluginTexts,
|
|
30
29
|
type FooterChipLayout,
|
|
31
30
|
} from "./footer-layout.ts";
|
|
31
|
+
import { createGitStatsRefresher, readGitStats, type GitStats } from "./git-stats.ts";
|
|
32
32
|
import { MCP_STATUS_KEY, buildMcpChip } from "./mcp-chip.ts";
|
|
33
33
|
|
|
34
|
+
export { parseGitStats } from "./git-stats.ts";
|
|
35
|
+
|
|
34
36
|
const GIT_REFRESH_INTERVAL_MS = 10_000;
|
|
35
37
|
const USAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
36
38
|
// 后台连接 MCP 服务器不会触发 footer 重绘,靠这个兜底轮询把芯片刷出来
|
|
@@ -49,8 +51,6 @@ export function footerGlyphs(nerdIcons: boolean): { git: string; cache: string;
|
|
|
49
51
|
: { git: "", cache: "", mcp: "" };
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
type GitStats = { add: number; del: number };
|
|
53
|
-
|
|
54
54
|
type XaiFooterReport = {
|
|
55
55
|
buckets?: Array<{
|
|
56
56
|
id?: string;
|
|
@@ -62,19 +62,6 @@ type XaiFooterReport = {
|
|
|
62
62
|
metrics?: Array<{ id?: string; value?: unknown }>;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
export function parseGitStats(stdout: string): GitStats {
|
|
66
|
-
let add = 0;
|
|
67
|
-
let del = 0;
|
|
68
|
-
for (const line of stdout.split("\n")) {
|
|
69
|
-
const match = line.match(/^(\d+)\s+(\d+)/);
|
|
70
|
-
if (match) {
|
|
71
|
-
add += Number(match[1]);
|
|
72
|
-
del += Number(match[2]);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return { add, del };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
65
|
export function formatXaiFooterChip(report: XaiFooterReport): string | undefined {
|
|
79
66
|
const included = report.buckets?.find((b) => b.id === "included-allowance");
|
|
80
67
|
if (included?.unit === "percent" && typeof included.used === "number") {
|
|
@@ -181,7 +168,12 @@ function colorUsageChip(theme: any, text: string): string {
|
|
|
181
168
|
}
|
|
182
169
|
|
|
183
170
|
let currentTui: any = undefined;
|
|
184
|
-
let refreshCurrentGitStats: (() => void) | undefined;
|
|
171
|
+
let refreshCurrentGitStats: ((reset?: boolean) => void) | undefined;
|
|
172
|
+
|
|
173
|
+
/** Apply a /ccstyle Git mode change immediately, without recreating the footer. */
|
|
174
|
+
export function refreshFooterGitStats(): void {
|
|
175
|
+
refreshCurrentGitStats?.(true);
|
|
176
|
+
}
|
|
185
177
|
let refreshCurrentUsage: (() => void) | undefined;
|
|
186
178
|
let refreshCurrentMcpChip: (() => void) | undefined;
|
|
187
179
|
/** 工具/命令列表在 pi API 上,而 footer 工厂只拿得到 ctx,加载时记下来。 */
|
|
@@ -193,33 +185,23 @@ const createCustomFooterFactory =
|
|
|
193
185
|
const sep = theme.fg("muted", " · ");
|
|
194
186
|
const joinChips = (parts: string[]) => parts.filter(Boolean).join(sep);
|
|
195
187
|
let gitStats: GitStats | undefined;
|
|
196
|
-
let gitRefreshRunning = false;
|
|
197
188
|
let localUsageChip = "";
|
|
198
189
|
let usageGeneration = 0;
|
|
199
190
|
let usageAbort: AbortController | undefined;
|
|
200
191
|
let disposed = false;
|
|
201
192
|
|
|
202
|
-
// 查询在 render
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (gitStats?.add === next.add && gitStats.del === next.del) return;
|
|
215
|
-
gitStats = next;
|
|
216
|
-
tui.requestRender();
|
|
217
|
-
},
|
|
218
|
-
);
|
|
219
|
-
};
|
|
220
|
-
refreshCurrentGitStats = refreshGitStats;
|
|
221
|
-
refreshGitStats();
|
|
222
|
-
const gitRefreshTimer = setInterval(refreshGitStats, GIT_REFRESH_INTERVAL_MS);
|
|
193
|
+
// 查询在 render 外执行;模式/分支变化清旧值,普通轮询只在统计变化时重绘。
|
|
194
|
+
const gitRefresher = createGitStatsRefresher({
|
|
195
|
+
getMode: () => config.footerGitStatsMode,
|
|
196
|
+
query: (mode, signal) => readGitStats(ctx.cwd, mode, signal),
|
|
197
|
+
onChange: (next) => {
|
|
198
|
+
gitStats = next;
|
|
199
|
+
tui.requestRender();
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
refreshCurrentGitStats = gitRefresher.refresh;
|
|
203
|
+
gitRefresher.refresh();
|
|
204
|
+
const gitRefreshTimer = setInterval(() => gitRefresher.refresh(), GIT_REFRESH_INTERVAL_MS);
|
|
223
205
|
gitRefreshTimer.unref?.();
|
|
224
206
|
|
|
225
207
|
const refreshUsage = () => {
|
|
@@ -315,9 +297,9 @@ const createCustomFooterFactory =
|
|
|
315
297
|
const mcpRefreshTimer = setInterval(refreshMcp, MCP_REFRESH_INTERVAL_MS);
|
|
316
298
|
mcpRefreshTimer.unref?.();
|
|
317
299
|
|
|
318
|
-
//
|
|
300
|
+
// 分支变化时重新选择基准,并拒绝旧分支的异步查询结果。
|
|
319
301
|
const unsubBranch = footerData.onBranchChange(() => {
|
|
320
|
-
|
|
302
|
+
gitRefresher.refresh(true);
|
|
321
303
|
tui.requestRender();
|
|
322
304
|
});
|
|
323
305
|
|
|
@@ -442,14 +424,17 @@ const createCustomFooterFactory =
|
|
|
442
424
|
}
|
|
443
425
|
const branch = footerData.getGitBranch();
|
|
444
426
|
if (branch) {
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
427
|
+
const s = gitStats;
|
|
428
|
+
let stats = "";
|
|
429
|
+
if (s && s.status)
|
|
430
|
+
stats = theme.fg("dim", " [") + gitColor(s.status) + theme.fg("dim", "]");
|
|
431
|
+
if (s && (s.add || s.del))
|
|
432
|
+
stats +=
|
|
433
|
+
theme.fg("dim", "(") +
|
|
434
|
+
theme.fg("success", `+${s.add}`) +
|
|
435
|
+
" " +
|
|
436
|
+
theme.fg("error", `−${s.del}`) +
|
|
437
|
+
theme.fg("dim", ")");
|
|
453
438
|
const gitLabel = glyphs.git ? `${glyphs.git} ${branch}` : branch;
|
|
454
439
|
place += theme.fg("muted", " on ") + gitColor(gitLabel) + stats;
|
|
455
440
|
}
|
|
@@ -470,6 +455,7 @@ const createCustomFooterFactory =
|
|
|
470
455
|
},
|
|
471
456
|
dispose(): void {
|
|
472
457
|
disposed = true;
|
|
458
|
+
gitRefresher.dispose();
|
|
473
459
|
usageAbort?.abort();
|
|
474
460
|
usageAbort = undefined;
|
|
475
461
|
clearInterval(gitRefreshTimer);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import type { FooterGitStatsMode } from "../../config/config.ts";
|
|
4
|
+
|
|
5
|
+
export type GitStats = { add: number; del: number; status?: string };
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const BRANCH_BASE_REFS = ["refs/remotes/origin/HEAD", "refs/heads/main", "refs/heads/master"];
|
|
9
|
+
|
|
10
|
+
export function parseGitStats(stdout: string): GitStats {
|
|
11
|
+
let add = 0;
|
|
12
|
+
let del = 0;
|
|
13
|
+
for (const line of stdout.split("\n")) {
|
|
14
|
+
const match = line.match(/^(\d+)\s+(\d+)/);
|
|
15
|
+
if (match) {
|
|
16
|
+
add += Number(match[1]);
|
|
17
|
+
del += Number(match[2]);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { add, del };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CONFLICT_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* git status --porcelain --branch 的第一行带分支头(## branch...origin/branch [ahead N, behind M]),
|
|
27
|
+
* 其余行是 XY 文件状态。返回 Starship 风格的状态符号串(= ! ? ⇡⇣),无变化时 undefined。
|
|
28
|
+
*/
|
|
29
|
+
export function parseGitStatus(stdout: string): string | undefined {
|
|
30
|
+
let conflicted = false;
|
|
31
|
+
let modified = false;
|
|
32
|
+
let untracked = false;
|
|
33
|
+
let ahead: number | undefined;
|
|
34
|
+
let behind: number | undefined;
|
|
35
|
+
for (const line of stdout.split("\n")) {
|
|
36
|
+
if (line.startsWith("## ")) {
|
|
37
|
+
ahead = Number(line.match(/ahead (\d+)/)?.[1]) || undefined;
|
|
38
|
+
behind = Number(line.match(/behind (\d+)/)?.[1]) || undefined;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const code = line.slice(0, 2);
|
|
42
|
+
if (code === "??") untracked = true;
|
|
43
|
+
else if (CONFLICT_CODES.has(code)) {
|
|
44
|
+
// 冲突文件必然也有未提交内容,! 与 = 同时出现
|
|
45
|
+
conflicted = true;
|
|
46
|
+
modified = true;
|
|
47
|
+
} else if (line.trim()) modified = true;
|
|
48
|
+
}
|
|
49
|
+
let status = "";
|
|
50
|
+
if (conflicted) status += "=";
|
|
51
|
+
if (modified) status += "!";
|
|
52
|
+
if (untracked) status += "?";
|
|
53
|
+
if (ahead) status += `⇡${ahead}`;
|
|
54
|
+
if (behind) status += `⇣${behind}`;
|
|
55
|
+
return status || undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Read only local refs and tracked files; never fetch or change the index. */
|
|
59
|
+
export async function readGitStats(
|
|
60
|
+
cwd: string,
|
|
61
|
+
mode: FooterGitStatsMode,
|
|
62
|
+
signal?: AbortSignal,
|
|
63
|
+
): Promise<GitStats | undefined> {
|
|
64
|
+
const git = async (args: string[]) => {
|
|
65
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
66
|
+
cwd,
|
|
67
|
+
timeout: 2000,
|
|
68
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
69
|
+
signal,
|
|
70
|
+
});
|
|
71
|
+
return stdout.trim();
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
let base = "HEAD";
|
|
75
|
+
if (mode === "branch") {
|
|
76
|
+
let baseCommit: string | undefined;
|
|
77
|
+
for (const ref of BRANCH_BASE_REFS) {
|
|
78
|
+
try {
|
|
79
|
+
baseCommit = await git(["rev-parse", "--verify", `${ref}^{commit}`]);
|
|
80
|
+
break;
|
|
81
|
+
} catch {
|
|
82
|
+
if (signal?.aborted) return undefined;
|
|
83
|
+
// Missing/dangling remote HEAD: try the local default branches.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!baseCommit) return undefined;
|
|
87
|
+
base = await git(["merge-base", "HEAD", baseCommit]);
|
|
88
|
+
}
|
|
89
|
+
// One comparison includes committed + staged + unstaged net changes,
|
|
90
|
+
// without double-counting edits that are later reverted. Untracked files are excluded.
|
|
91
|
+
const [numstat, statusOut] = await Promise.all([
|
|
92
|
+
git(["diff", "--numstat", base, "--"]),
|
|
93
|
+
git(["status", "--porcelain", "--branch"]).catch(() => ""),
|
|
94
|
+
]);
|
|
95
|
+
if (signal?.aborted) return undefined;
|
|
96
|
+
const stats = parseGitStats(numstat);
|
|
97
|
+
const status = parseGitStatus(statusOut);
|
|
98
|
+
return status ? { ...stats, status } : stats;
|
|
99
|
+
} catch {
|
|
100
|
+
// Non-repository, unborn HEAD, unrelated histories, timeout, etc.
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Coalesce overlapping refreshes and reject stale results after a mode/branch change. */
|
|
106
|
+
export function createGitStatsRefresher(options: {
|
|
107
|
+
getMode: () => FooterGitStatsMode;
|
|
108
|
+
query: (mode: FooterGitStatsMode, signal: AbortSignal) => Promise<GitStats | undefined>;
|
|
109
|
+
onChange: (stats: GitStats | undefined) => void;
|
|
110
|
+
}): { refresh: (reset?: boolean) => void; dispose: () => void } {
|
|
111
|
+
let stats: GitStats | undefined;
|
|
112
|
+
let mode = options.getMode();
|
|
113
|
+
let generation = 0;
|
|
114
|
+
let running = false;
|
|
115
|
+
let pending = false;
|
|
116
|
+
let disposed = false;
|
|
117
|
+
let controller: AbortController | undefined;
|
|
118
|
+
const publish = (next: GitStats | undefined) => {
|
|
119
|
+
if (stats?.add === next?.add && stats?.del === next?.del && stats?.status === next?.status)
|
|
120
|
+
return;
|
|
121
|
+
stats = next;
|
|
122
|
+
options.onChange(next);
|
|
123
|
+
};
|
|
124
|
+
const refresh = (reset = false) => {
|
|
125
|
+
if (disposed) return;
|
|
126
|
+
const nextMode = options.getMode();
|
|
127
|
+
if (reset || mode !== nextMode) publish(undefined);
|
|
128
|
+
mode = nextMode;
|
|
129
|
+
const request = ++generation;
|
|
130
|
+
if (running) {
|
|
131
|
+
pending = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
running = true;
|
|
135
|
+
const queryController = new AbortController();
|
|
136
|
+
controller = queryController;
|
|
137
|
+
void (async () => {
|
|
138
|
+
let next: GitStats | undefined;
|
|
139
|
+
try {
|
|
140
|
+
next = await options.query(nextMode, queryController.signal);
|
|
141
|
+
} catch {
|
|
142
|
+
// Failed queries clear the old numbers rather than leaving a stale chip.
|
|
143
|
+
} finally {
|
|
144
|
+
if (!disposed && request === generation && nextMode === options.getMode()) publish(next);
|
|
145
|
+
running = false;
|
|
146
|
+
controller = undefined;
|
|
147
|
+
if (pending && !disposed) {
|
|
148
|
+
pending = false;
|
|
149
|
+
refresh();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})();
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
refresh,
|
|
156
|
+
dispose: () => {
|
|
157
|
+
disposed = true;
|
|
158
|
+
generation++;
|
|
159
|
+
controller?.abort();
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
isRichDiffComponent,
|
|
43
43
|
} from "./tool/diff/diff-renderer.ts";
|
|
44
44
|
import { renderRichToolResult } from "./tool/diff/index.ts";
|
|
45
|
+
import { displayConfigCacheKey } from "./tool/diff/diff-component.ts";
|
|
45
46
|
import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
|
|
46
47
|
import { isToolCallHovered } from "./mouse/hover.ts";
|
|
47
48
|
import { getToolMouseTui } from "./mouse/scroll.ts";
|
|
@@ -710,8 +711,9 @@ function compactEditWriteLine(
|
|
|
710
711
|
// 展开卡 Box(1,1) 已 pad;折叠行自己留 1 格前导空格
|
|
711
712
|
const iconPart = `${options.flushLeft ? "" : " "}${theme.fg(iconColor, icon)} `;
|
|
712
713
|
const namePart = theme.fg("toolTitle", name);
|
|
713
|
-
const
|
|
714
|
-
|
|
714
|
+
const clickable = options.hint !== false && component.expanded !== true;
|
|
715
|
+
const clickText = clickable ? showMoreHintText() : "";
|
|
716
|
+
const hintText = clickable ? ` · ${clickText}` : "";
|
|
715
717
|
const fixedWidth =
|
|
716
718
|
visibleWidth(iconPart) +
|
|
717
719
|
visibleWidth(namePart) +
|
|
@@ -720,7 +722,11 @@ function compactEditWriteLine(
|
|
|
720
722
|
const pathWidth = Math.max(0, width - fixedWidth - (path ? 1 : 0));
|
|
721
723
|
const pathPart =
|
|
722
724
|
pathWidth > 0 && path ? ` ${formatDisplayPath(path, component.cwd, pathWidth)}` : "";
|
|
723
|
-
|
|
725
|
+
// 圆点保持 dim;hover 只高亮可点击文字,与 compact 摘要行一致
|
|
726
|
+
const hintPart = clickable
|
|
727
|
+
? `${theme.fg("dim", " · ")}${theme.fg(isToolCallHovered(component.toolCallId) ? "text" : "dim", clickText)}`
|
|
728
|
+
: "";
|
|
729
|
+
const line = `${iconPart}${namePart}${theme.fg("toolTitle", pathPart)}${statsStyled}${hintPart}`;
|
|
724
730
|
return ["", truncateToWidth(line, width, "")];
|
|
725
731
|
}
|
|
726
732
|
|
|
@@ -735,6 +741,7 @@ type CompactEditPaintHit = {
|
|
|
735
741
|
theme: unknown;
|
|
736
742
|
/** 展开卡的背景槽位同样参与命中判断。 */
|
|
737
743
|
bgSlot: string;
|
|
744
|
+
diffConfigKey: string;
|
|
738
745
|
expanded: boolean;
|
|
739
746
|
result: unknown;
|
|
740
747
|
isPartial: boolean;
|
|
@@ -766,6 +773,7 @@ function compactEditWriteLines(
|
|
|
766
773
|
const hover = isToolCallHovered(component.toolCallId);
|
|
767
774
|
const ioHover = compactEditIoHover(component);
|
|
768
775
|
const theme = themeOf();
|
|
776
|
+
const diffConfigKey = displayConfigCacheKey(getToolDisplayConfig());
|
|
769
777
|
if (!pending) {
|
|
770
778
|
const hit = compactEditPaintCache.get(component);
|
|
771
779
|
if (
|
|
@@ -773,6 +781,7 @@ function compactEditWriteLines(
|
|
|
773
781
|
hit.width === width &&
|
|
774
782
|
hit.theme === theme &&
|
|
775
783
|
hit.bgSlot === config.expandedCardBackground &&
|
|
784
|
+
hit.diffConfigKey === diffConfigKey &&
|
|
776
785
|
hit.expanded === expanded &&
|
|
777
786
|
hit.result === component.result &&
|
|
778
787
|
hit.isPartial === isPartial &&
|
|
@@ -789,6 +798,7 @@ function compactEditWriteLines(
|
|
|
789
798
|
width,
|
|
790
799
|
theme,
|
|
791
800
|
bgSlot: config.expandedCardBackground,
|
|
801
|
+
diffConfigKey,
|
|
792
802
|
expanded,
|
|
793
803
|
result: component.result,
|
|
794
804
|
isPartial,
|
|
@@ -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 {
|