pi-cc-extensions 0.9.11 → 1.0.0

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 CHANGED
@@ -61,14 +61,15 @@ 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
- "inputClip": 0, // tool summary path/command clip length; 0 = fit width
64
+ "toolLabelClip": 0, // tool summary path/command clip length; 0 = fit width
65
65
  "showStartupHeader": true, // startup header (logo + tips) toggle
66
+ "stickyUserMessage": true, // fullscreen: pin a user message's first line once it scrolls off the top edge, until the next one takes over
66
67
 
67
68
  // diff
68
69
  "diffViewMode": "auto", // layout: auto / split / unified
69
70
  "diffIndicatorMode": "bars", // change indicators: bars / classic / none
70
71
  "diffSplitMinWidth": 120, // min terminal width for side-by-side columns
71
- "editDiffCollapsedLines": 24, // Edit collapse lines; beyond that shows the expand hint
72
+ "editDiffCollapsedLines": 24, // edit collapse lines; 0 = change-count summary only
72
73
  "writeDiffCollapsedLines": 0, // write collapse lines; 0 = creation summary only
73
74
  "diffWordWrap": true, // wrap long diff lines
74
75
 
@@ -83,6 +84,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
83
84
  "footerCurrency": "USD", // display currency for the footer cost
84
85
  "footerCurrencyRate": null, // display units per 1 cost unit; null changes the symbol only
85
86
  "footerNerdIcons": true, // Nerd Font glyphs for git/cache; false = plain text
87
+ "footerGitStatsMode": "working", // working (default): uncommitted; branch: cumulative net changes
86
88
  "footerHiddenKeys": [], // hidden plugin chip keys
87
89
  "footerLine1Keys": ["pi-usage"], // line1 plugin chip order; pi-usage is shown by default, data from @narumitw/pi-usage
88
90
  "footerLine2Keys": [], // line2 plugin chip order (after cwd/git)
@@ -91,7 +93,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
91
93
  ```
92
94
 
93
95
  > [!TIP]
94
- > **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. An expanded diff always shows every line; when expanded Input/Output exceeds the line cap, the footer `… +N more lines · click to show more` opens a full preview. Click to collapse (dragging inside the card selects text).
96
+ > **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. An expanded diff always shows every line; when expanded Input/Output exceeds the line cap, the footer `… +N more lines · click to show more` opens a full preview. Click to collapse.
95
97
 
96
98
  > [!NOTE]
97
99
  > **Mermaid rendering**: set `markdown.mermaid` to `final` via `~/.pi/agent/settings.json` or the Mermaid diagrams option in `/settings`. Default `streaming` redraws per frame; `final` renders once at completion.
package/README.md CHANGED
@@ -61,14 +61,15 @@ 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
- "inputClip": 0, // 工具摘要 path/command 折叠字符数;0 = 按可用宽度
64
+ "toolLabelClip": 0, // 工具摘要 path/command 折叠字符数;0 = 按可用宽度
65
65
  "showStartupHeader": true, // 启动头(logo + tips)开关
66
+ "stickyUserMessage": true, // fullscreen:userMessage 正文首行滚出视口上沿后钉住,直到下一条指令接手
66
67
 
67
68
  // diff
68
69
  "diffViewMode": "auto", // 布局:auto / split / unified
69
70
  "diffIndicatorMode": "bars", // 变更指示:bars / classic / none
70
71
  "diffSplitMinWidth": 120, // 左右分栏的最小终端宽度
71
- "editDiffCollapsedLines": 24, // Edit 折叠行数,超出显示展开提示
72
+ "editDiffCollapsedLines": 24, // edit 折叠行数;0 仅显示变更统计摘要
72
73
  "writeDiffCollapsedLines": 0, // write 折叠行数,0 仅显示创建摘要
73
74
  "diffWordWrap": true, // 长 diff 行换行
74
75
 
@@ -83,6 +84,7 @@ pi install git:github.com/minuque/pi-cc-extensions
83
84
  "footerCurrency": "USD", // 底栏费用的显示币种
84
85
  "footerCurrencyRate": null, // 1 单位费用换成多少显示币种;null 只换符号、不乘倍率
85
86
  "footerNerdIcons": true, // git/缓存用 Nerd Font 图标;false 为纯文本
87
+ "footerGitStatsMode": "working", // working(默认):未提交改动;branch:分支累计净改动
86
88
  "footerHiddenKeys": [], // 隐藏的插件芯片 key
87
89
  "footerLine1Keys": ["pi-usage"], // line1 插件芯片顺序;pi-usage 默认显示,数据来自 @narumitw/pi-usage
88
90
  "footerLine2Keys": [], // line2 插件芯片顺序(接在 cwd/git 后)
@@ -91,7 +93,7 @@ pi install git:github.com/minuque/pi-cc-extensions
91
93
  ```
92
94
 
93
95
  > [!TIP]
94
- > **全屏模式**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要;工具卡展开态 diff 全量显示,Input/Output 超行时末行 `… +N more lines · click to show more` 打开全量预览,单击收起(卡内拖动为选中文本)。
96
+ > **全屏模式**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要;工具卡展开态 diff 全量显示,Input/Output 超行时末行 `… +N more lines · click to show more` 打开全量预览,单击收起。
95
97
 
96
98
  > [!NOTE]
97
99
  > **Mermaid 渲染**:建议把 `markdown.mermaid` 设为 `final`(`~/.pi/agent/settings.json` 或 `/settings` 面板的 Mermaid diagrams 选项)。默认 `streaming` 逐帧重绘,`final` 渲染最终版更稳定。
@@ -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,12 +59,14 @@ export type Config = {
57
59
  expandedCardBackground: ThemeBg;
58
60
  expandedInputMaxLines: number;
59
61
  expandedOutputMaxLines: number;
60
- inputClip: number;
62
+ toolLabelClip: number;
61
63
  useSummaryTitlesAsThinkingTitle: boolean;
62
64
  previewLines: number;
63
65
  animationIntervalMs: number;
64
66
  dimThinkingText: boolean;
65
67
  showStartupHeader: boolean;
68
+ /** fullscreen:userMessage 正文首行滚出视口上沿后钉住,直到下一条指令接手。 */
69
+ stickyUserMessage: boolean;
66
70
  enableSessionReference: boolean;
67
71
  enableSubagentAutocomplete: boolean;
68
72
  enableContextCommand: boolean;
@@ -75,6 +79,7 @@ export type Config = {
75
79
  /** 1 单位费用对应的显示币种数量。null 只换符号、不乘倍率。 */
76
80
  footerCurrencyRate: number | null;
77
81
  footerNerdIcons: boolean;
82
+ footerGitStatsMode: FooterGitStatsMode;
78
83
  footerHiddenKeys: string[];
79
84
  footerLine1Keys: string[];
80
85
  footerLine2Keys: string[];
@@ -87,8 +92,9 @@ const LEGACY_CONFIG_PATH = join(AGENT_DIR, "claude-code-style.json");
87
92
 
88
93
  export const DIFF_VIEW_MODES: DiffViewMode[] = ["auto", "split", "unified"];
89
94
  export const DIFF_INDICATOR_MODES: DiffIndicatorMode[] = ["bars", "classic", "none"];
95
+ export const FOOTER_GIT_STATS_MODES: FooterGitStatsMode[] = ["working", "branch"];
90
96
  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"];
97
+ export const DIFF_COLLAPSED_LINES_VALUES = ["0", "12", "24", "36", "48", "80", "120"];
92
98
  /** Write collapsed presets. 0 = stats only (`+N -0` + expand hint). */
93
99
  export const WRITE_DIFF_COLLAPSED_LINES_VALUES = ["0", "4", "8", "12", "24", "36"];
94
100
  /** Presets for expanded body height — keep low options first so cycling stays TUI-friendly. */
@@ -112,7 +118,7 @@ export const EXPANDED_INPUT_MAX_LINES_VALUES = ["5", "10", "20", "40", "80"];
112
118
  /** 展开工具卡 Output 可见行数预设。 */
113
119
  export const EXPANDED_OUTPUT_MAX_LINES_VALUES = ["10", "20", "40", "80", "120"];
114
120
  /** 工具摘要里 path/command 等输入的折叠字符数;0 = 只按可用宽度截断。 */
115
- export const INPUT_CLIP_VALUES = ["0", "40", "60", "80", "100", "120", "160"];
121
+ export const TOOL_LABEL_CLIP_VALUES = ["0", "40", "60", "80", "100", "120", "160"];
116
122
  export const THINKING_PREVIEW_LINES_VALUES = ["0", "1", "3", "5", "10"];
117
123
  export const THINKING_ANIMATION_INTERVAL_VALUES = ["40", "60", "90", "120", "180"];
118
124
  /** Tools commonly toggled in excludeRenderers via the settings panel. */
@@ -143,12 +149,13 @@ export const DEFAULT_CONFIG: Config = {
143
149
  expandedCardBackground: "userMessageBg",
144
150
  expandedInputMaxLines: 5,
145
151
  expandedOutputMaxLines: 10,
146
- inputClip: 0,
152
+ toolLabelClip: 0,
147
153
  useSummaryTitlesAsThinkingTitle: true,
148
154
  previewLines: 3,
149
155
  animationIntervalMs: 90,
150
156
  dimThinkingText: false,
151
157
  showStartupHeader: true,
158
+ stickyUserMessage: true,
152
159
  enableSessionReference: true,
153
160
  enableSubagentAutocomplete: true,
154
161
  enableContextCommand: true,
@@ -159,6 +166,7 @@ export const DEFAULT_CONFIG: Config = {
159
166
  footerCurrency: "USD",
160
167
  footerCurrencyRate: null,
161
168
  footerNerdIcons: true,
169
+ footerGitStatsMode: "working",
162
170
  ...DEFAULT_FOOTER_CHIP_LAYOUT,
163
171
  };
164
172
 
@@ -174,10 +182,10 @@ export function pickPositiveInt(value: unknown, fallback: number, min = 1, max =
174
182
  return Math.min(max, Math.max(min, Math.floor(n)));
175
183
  }
176
184
 
177
- /** inputClip:0 表示按宽度自适应,其余夹到 8..500。 */
178
- export function pickInputClip(value: unknown): number {
185
+ /** toolLabelClip:0 表示按宽度自适应,其余夹到 8..500。 */
186
+ export function pickToolLabelClip(value: unknown): number {
179
187
  const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
180
- return n === 0 ? 0 : pickPositiveInt(value, DEFAULT_CONFIG.inputClip, 8, 500);
188
+ return n === 0 ? 0 : pickPositiveInt(value, DEFAULT_CONFIG.toolLabelClip, 8, 500);
181
189
  }
182
190
 
183
191
  export function pickPositiveNumber(value: unknown, fallback: number, min = 1): number {
@@ -228,7 +236,7 @@ export function normalizeConfig(input: unknown): Config {
228
236
  editDiffCollapsedLines: pickPositiveInt(
229
237
  source.editDiffCollapsedLines,
230
238
  DEFAULT_CONFIG.editDiffCollapsedLines,
231
- 1,
239
+ 0,
232
240
  500,
233
241
  ),
234
242
  writeDiffCollapsedLines: pickPositiveInt(
@@ -261,7 +269,9 @@ export function normalizeConfig(input: unknown): Config {
261
269
  1,
262
270
  5_000,
263
271
  ),
264
- inputClip: pickInputClip(source.inputClip),
272
+ toolLabelClip: pickToolLabelClip(
273
+ source.toolLabelClip ?? source.inputClip, // 兼容旧配置键
274
+ ),
265
275
  useSummaryTitlesAsThinkingTitle: source.useSummaryTitlesAsThinkingTitle !== false,
266
276
  previewLines: pickPositiveInt(
267
277
  source.previewLines,
@@ -275,6 +285,7 @@ export function normalizeConfig(input: unknown): Config {
275
285
  ),
276
286
  dimThinkingText: source.dimThinkingText === true,
277
287
  showStartupHeader: source.showStartupHeader !== false,
288
+ stickyUserMessage: source.stickyUserMessage !== false,
278
289
  enableSessionReference: source.enableSessionReference !== false,
279
290
  enableSubagentAutocomplete: source.enableSubagentAutocomplete !== false,
280
291
  enableContextCommand: source.enableContextCommand !== false,
@@ -285,6 +296,11 @@ export function normalizeConfig(input: unknown): Config {
285
296
  footerCurrency: normalizeCurrency(source.footerCurrency),
286
297
  footerCurrencyRate: normalizeFooterCurrencyRate(source.footerCurrencyRate),
287
298
  footerNerdIcons: source.footerNerdIcons !== false,
299
+ footerGitStatsMode: pickEnum(
300
+ source.footerGitStatsMode,
301
+ FOOTER_GIT_STATS_MODES,
302
+ DEFAULT_CONFIG.footerGitStatsMode,
303
+ ),
288
304
  ...normalizeFooterChipLayout(source),
289
305
  };
290
306
  }
@@ -326,12 +342,13 @@ export function formatConfigStatus(source: Config = config): string {
326
342
  `expandedMax=${source.expandedPreviewMaxLines}`,
327
343
  `expandedInput=${source.expandedInputMaxLines}`,
328
344
  `expandedOutput=${source.expandedOutputMaxLines}`,
329
- `inputClip=${source.inputClip}`,
345
+ `toolLabelClip=${source.toolLabelClip}`,
330
346
  `thinkingTitle=${source.useSummaryTitlesAsThinkingTitle ? "summary" : "default"}`,
331
347
  `thinkingPreview=${source.previewLines}`,
332
348
  `thinkingAnimation=${source.animationIntervalMs}ms`,
333
349
  `thinkingDim=${source.dimThinkingText ? "on" : "off"}`,
334
350
  `startupHeader=${source.showStartupHeader ? "on" : "off"}`,
351
+ `stickyUserMessage=${source.stickyUserMessage ? "on" : "off"}`,
335
352
  `sessionRef=${source.enableSessionReference ? "on" : "off"}`,
336
353
  `subagentAuto=${source.enableSubagentAutocomplete ? "on" : "off"}`,
337
354
  `context=${source.enableContextCommand ? "on" : "off"}`,
@@ -342,6 +359,7 @@ export function formatConfigStatus(source: Config = config): string {
342
359
  `footerCurrency=${source.footerCurrency}`,
343
360
  `footerRate=${source.footerCurrencyRate ?? "-"}`,
344
361
  `footerIcons=${source.footerNerdIcons ? "nerd" : "plain"}`,
362
+ `footerGitStats=${source.footerGitStatsMode}`,
345
363
  formatFooterChipSummary(source),
346
364
  ].join(" · ");
347
365
  }
@@ -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
- INPUT_CLIP_VALUES,
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 注入的渲染副作用,面板自身不触碰渲染状态。 */
@@ -65,7 +68,7 @@ export type CcstylePanelHooks = {
65
68
 
66
69
  function modeSettingDescription(mode: CompactStyleMode): string {
67
70
  if (mode === "compact") {
68
- return "(Experimental) One summary line per assistant round; edit/write reuse the same Diff preview settings as on.";
71
+ return "One summary line per assistant round; edit/write reuse the same Diff preview settings as on.";
69
72
  }
70
73
  if (mode === "off") {
71
74
  return "Pi native tool rendering. Diff options below still apply independently.";
@@ -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
  }
@@ -413,8 +422,8 @@ export async function showCcstylePanel(
413
422
  id: "mode",
414
423
  label: "Mode",
415
424
  description: modeSettingDescription(config.mode),
416
- currentValue: config.mode === "compact" ? "compact (Experimental)" : config.mode,
417
- values: ["on", "compact (Experimental)", "off"],
425
+ currentValue: config.mode,
426
+ values: ["on", "compact", "off"],
418
427
  };
419
428
  // Tracks whether a nested submenu is open so Tab switches sections
420
429
  // only at the top level (mirrors Zentui settings: Tab = switch sections).
@@ -444,15 +453,15 @@ export async function showCcstylePanel(
444
453
  );
445
454
  },
446
455
  };
447
- const inputClipSetting = {
448
- id: "inputClip",
449
- label: "Input clip",
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.inputClip),
453
- values: [...INPUT_CLIP_VALUES],
461
+ currentValue: String(config.toolLabelClip),
462
+ values: [...TOOL_LABEL_CLIP_VALUES],
454
463
  submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
455
- buildNumberInputSubmenu(theme, inputClipSetting, closeSubmenu),
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
- "How many edit/diff body lines to show before the expand hint (Ctrl+O / click). Write uses its own setting below.",
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) =>
@@ -586,6 +595,15 @@ export async function showCcstylePanel(
586
595
  currentValue: config.showStartupHeader ? "on" : "off",
587
596
  values: ["on", "off"],
588
597
  };
598
+ const stickyUserMessageSetting = {
599
+ id: "stickyUserMessage",
600
+ label: "Sticky user message",
601
+ description: config.stickyUserMessage
602
+ ? "Fullscreen: pin a user message's first line once it scrolls off the top edge, until the next one takes over."
603
+ : "User messages scroll normally.",
604
+ currentValue: config.stickyUserMessage ? "on" : "off",
605
+ values: ["on", "off"],
606
+ };
589
607
 
590
608
  // 额外功能开关:注册于扩展加载期,切换后需重启(/reload)生效。
591
609
  const sessionReferenceToggle = featureToggleSetting(
@@ -705,6 +723,13 @@ export async function showCcstylePanel(
705
723
  currentValue: config.enableCustomFooter ? "on" : "off",
706
724
  values: ["on", "off"],
707
725
  };
726
+ const footerGitStatsSetting = {
727
+ id: "footerGitStatsMode",
728
+ label: "Git changes",
729
+ description: footerGitStatsDescription(config.footerGitStatsMode),
730
+ currentValue: config.footerGitStatsMode,
731
+ values: FOOTER_GIT_STATS_MODES,
732
+ };
708
733
  const pluginChipsSetting = {
709
734
  id: "footerPluginChips",
710
735
  label: "Plugin chips",
@@ -769,6 +794,14 @@ export async function showCcstylePanel(
769
794
  ctx.ui.notify(`Updated ${id}: ${footerCurrencyRateSetting.currentValue}`, "info");
770
795
  return;
771
796
  }
797
+ if (id === "footerGitStatsMode") {
798
+ updateConfig({ footerGitStatsMode: value as FooterGitStatsMode });
799
+ footerGitStatsSetting.currentValue = config.footerGitStatsMode;
800
+ footerGitStatsSetting.description = footerGitStatsDescription(config.footerGitStatsMode);
801
+ refreshFooterGitStats();
802
+ ctx.ui.notify(`Updated ${id}: ${value}`, "info");
803
+ return;
804
+ }
772
805
  if (id === "footerNerdIcons") {
773
806
  const enabled = value === "on";
774
807
  updateConfig({ footerNerdIcons: enabled });
@@ -788,16 +821,14 @@ export async function showCcstylePanel(
788
821
  return;
789
822
  }
790
823
  switch (id) {
791
- case "inputClip":
824
+ case "toolLabelClip":
792
825
  updateConfig({
793
- inputClip: pickInputClip(value),
826
+ toolLabelClip: pickToolLabelClip(value),
794
827
  });
795
- inputClipSetting.currentValue = String(config.inputClip);
828
+ toolLabelClipSetting.currentValue = String(config.toolLabelClip);
796
829
  break;
797
830
  case "mode": {
798
- // 选项值带 Experimental 标记,选择后还原为真实 mode 值。
799
- const mode: CompactStyleMode =
800
- value === "compact (Experimental)" ? "compact" : (value as CompactStyleMode);
831
+ const mode = value as CompactStyleMode;
801
832
  modeSetting.description = modeSettingDescription(mode);
802
833
  hooks.applyStyleMode(mode, ctx, toolGrouping);
803
834
  return;
@@ -825,7 +856,7 @@ export async function showCcstylePanel(
825
856
  editDiffCollapsedLines: pickPositiveInt(
826
857
  value,
827
858
  DEFAULT_CONFIG.editDiffCollapsedLines,
828
- 1,
859
+ 0,
829
860
  500,
830
861
  ),
831
862
  });
@@ -906,6 +937,13 @@ export async function showCcstylePanel(
906
937
  ? "Thinking text uses the theme's dim color."
907
938
  : "Keep the default thinking text color.";
908
939
  break;
940
+ case "stickyUserMessage": {
941
+ updateConfig({ stickyUserMessage: value === "on" });
942
+ stickyUserMessageSetting.description = config.stickyUserMessage
943
+ ? "Fullscreen: pin a user message's first line once it scrolls off the top edge, until the next one takes over."
944
+ : "User messages scroll normally.";
945
+ break;
946
+ }
909
947
  case "showStartupHeader": {
910
948
  const enabled = value === "on";
911
949
  updateConfig({ showStartupHeader: enabled });
@@ -950,7 +988,8 @@ export async function showCcstylePanel(
950
988
  expandedOutputSetting,
951
989
  expandedMaxSetting,
952
990
  expandedCardBgSetting,
953
- inputClipSetting,
991
+ toolLabelClipSetting,
992
+ stickyUserMessageSetting,
954
993
  startupHeaderSetting,
955
994
  ],
956
995
  },
@@ -984,6 +1023,7 @@ export async function showCcstylePanel(
984
1023
  footerCurrencySetting,
985
1024
  footerCurrencyRateSetting,
986
1025
  footerNerdIconsSetting,
1026
+ footerGitStatsSetting,
987
1027
  pluginChipsSetting,
988
1028
  ],
989
1029
  },
@@ -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 refreshGitStats = () => {
204
- if (gitRefreshRunning || disposed) return;
205
- gitRefreshRunning = true;
206
- execFile(
207
- "git",
208
- ["diff", "--numstat", "HEAD"],
209
- { cwd: ctx.cwd, timeout: 2000 },
210
- (err, stdout) => {
211
- gitRefreshRunning = false;
212
- if (err || disposed) return;
213
- const next = parseGitStats(stdout);
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
- // 分支变化时同步更新分支名和相对 HEAD 的统计。
300
+ // 分支变化时重新选择基准,并拒绝旧分支的异步查询结果。
319
301
  const unsubBranch = footerData.onBranchChange(() => {
320
- refreshGitStats();
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 stats =
446
- gitStats && (gitStats.add || gitStats.del)
447
- ? theme.fg("dim", " (") +
448
- theme.fg("success", `+${gitStats.add}`) +
449
- " " +
450
- theme.fg("error", `−${gitStats.del}`) +
451
- theme.fg("dim", ")")
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);