@herbertgao/pi-extensions 2026.9.19 → 2026.9.20

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.
Files changed (186) hide show
  1. package/README.md +1 -8
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +76 -59
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +76 -59
  4. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/preview.webp +0 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +93 -34
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +346 -117
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +6 -36
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/index.ts +2 -2
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -29
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +330 -360
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +56 -108
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/session.ts +97 -15
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +80 -45
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/flush-docked-bash.ts +36 -0
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-layout.ts +229 -0
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +479 -0
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/startup-header.ts +92 -61
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +58 -38
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +19 -3
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +1080 -229
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +232 -259
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +38 -48
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +8 -2
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +2 -7
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +277 -113
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +15 -6
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +18 -72
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +61 -26
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +25 -57
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-component.ts +125 -0
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +227 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +136 -0
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +115 -0
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-inline.ts +324 -0
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-layout.ts +762 -0
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +152 -0
  37. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-palette.ts +410 -0
  38. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-parse.ts +535 -0
  39. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +21 -3219
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +44 -0
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +568 -0
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +19 -18
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +2 -2
  45. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +1 -0
  46. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +142 -192
  47. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/mcp-title.ts +39 -0
  48. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{message-display.ts → tool/message-display.ts} +24 -90
  49. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +426 -0
  50. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +90 -132
  51. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{show-more-hint.ts → tool/show-more-hint.ts} +10 -0
  52. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +8 -26
  53. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-color.ts +44 -0
  54. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +76 -0
  55. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/component-tree.ts +34 -0
  56. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +28 -0
  57. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/fullscreen-detect.ts +51 -0
  58. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +124 -0
  59. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/sgr-mouse.ts +70 -0
  60. package/node_modules/@herbertgao/pi-cc-extensions/package.json +7 -7
  61. package/package.json +3 -7
  62. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.en.svg +0 -101
  63. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.svg +0 -101
  64. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/welcome.webp +0 -0
  65. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/render-utils.ts +0 -5
  66. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +0 -30
  67. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-display-utils.ts +0 -12
  68. package/node_modules/pi-footer/CHANGELOG.md +0 -145
  69. package/node_modules/pi-footer/LICENSE +0 -21
  70. package/node_modules/pi-footer/README.md +0 -354
  71. package/node_modules/pi-footer/package.json +0 -68
  72. package/node_modules/pi-footer/src/cache.ts +0 -106
  73. package/node_modules/pi-footer/src/colors.ts +0 -237
  74. package/node_modules/pi-footer/src/config.ts +0 -202
  75. package/node_modules/pi-footer/src/event-widgets.ts +0 -41
  76. package/node_modules/pi-footer/src/extension-statuses.ts +0 -102
  77. package/node_modules/pi-footer/src/git.ts +0 -135
  78. package/node_modules/pi-footer/src/index.ts +0 -268
  79. package/node_modules/pi-footer/src/metrics.ts +0 -141
  80. package/node_modules/pi-footer/src/presets.ts +0 -383
  81. package/node_modules/pi-footer/src/render.ts +0 -114
  82. package/node_modules/pi-footer/src/separators.ts +0 -52
  83. package/node_modules/pi-footer/src/types.ts +0 -122
  84. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +0 -22
  85. package/node_modules/pi-footer/src/ui/color-options.ts +0 -195
  86. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +0 -61
  87. package/node_modules/pi-footer/src/ui/edit-colors.ts +0 -13
  88. package/node_modules/pi-footer/src/ui/events.ts +0 -35
  89. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +0 -60
  90. package/node_modules/pi-footer/src/ui/extension-statuses.ts +0 -73
  91. package/node_modules/pi-footer/src/ui/fields.ts +0 -252
  92. package/node_modules/pi-footer/src/ui/global-menu.ts +0 -126
  93. package/node_modules/pi-footer/src/ui/helpers.ts +0 -44
  94. package/node_modules/pi-footer/src/ui/layout.ts +0 -17
  95. package/node_modules/pi-footer/src/ui/line-list.ts +0 -19
  96. package/node_modules/pi-footer/src/ui/model.ts +0 -64
  97. package/node_modules/pi-footer/src/ui/navigation.ts +0 -29
  98. package/node_modules/pi-footer/src/ui/option-edit.ts +0 -93
  99. package/node_modules/pi-footer/src/ui/overlay-render.ts +0 -103
  100. package/node_modules/pi-footer/src/ui/screen-context.ts +0 -18
  101. package/node_modules/pi-footer/src/ui/screen-controller.ts +0 -27
  102. package/node_modules/pi-footer/src/ui/screen-render.ts +0 -32
  103. package/node_modules/pi-footer/src/ui/screen-state.ts +0 -20
  104. package/node_modules/pi-footer/src/ui/screen.ts +0 -243
  105. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +0 -92
  106. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +0 -52
  107. package/node_modules/pi-footer/src/ui/screens/controller.ts +0 -12
  108. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +0 -68
  109. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +0 -115
  110. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +0 -61
  111. package/node_modules/pi-footer/src/ui/screens/global.ts +0 -75
  112. package/node_modules/pi-footer/src/ui/screens/line-list.ts +0 -108
  113. package/node_modules/pi-footer/src/ui/screens/main.ts +0 -99
  114. package/node_modules/pi-footer/src/ui/screens/terminal.ts +0 -121
  115. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +0 -154
  116. package/node_modules/pi-footer/src/ui/terminal-menu.ts +0 -32
  117. package/node_modules/pi-footer/src/ui/theme.ts +0 -48
  118. package/node_modules/pi-footer/src/ui/title-bar.ts +0 -100
  119. package/node_modules/pi-footer/src/ui/widget-actions.ts +0 -76
  120. package/node_modules/pi-footer/src/ui.ts +0 -66
  121. package/node_modules/pi-footer/src/widgets/context.ts +0 -28
  122. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +0 -17
  123. package/node_modules/pi-footer/src/widgets/core/context-window.ts +0 -24
  124. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +0 -19
  125. package/node_modules/pi-footer/src/widgets/core/cwd.ts +0 -108
  126. package/node_modules/pi-footer/src/widgets/core/event.ts +0 -34
  127. package/node_modules/pi-footer/src/widgets/core/external-status.ts +0 -102
  128. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +0 -20
  129. package/node_modules/pi-footer/src/widgets/core/model.ts +0 -35
  130. package/node_modules/pi-footer/src/widgets/core/provider.ts +0 -17
  131. package/node_modules/pi-footer/src/widgets/core/session-name.ts +0 -17
  132. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +0 -17
  133. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +0 -17
  134. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +0 -17
  135. package/node_modules/pi-footer/src/widgets/git/branch.ts +0 -81
  136. package/node_modules/pi-footer/src/widgets/git/clean.ts +0 -19
  137. package/node_modules/pi-footer/src/widgets/git/deletions.ts +0 -17
  138. package/node_modules/pi-footer/src/widgets/git/diff.ts +0 -38
  139. package/node_modules/pi-footer/src/widgets/git/insertions.ts +0 -17
  140. package/node_modules/pi-footer/src/widgets/git/remote.ts +0 -17
  141. package/node_modules/pi-footer/src/widgets/git/root.ts +0 -17
  142. package/node_modules/pi-footer/src/widgets/git/sha.ts +0 -17
  143. package/node_modules/pi-footer/src/widgets/git/staged.ts +0 -17
  144. package/node_modules/pi-footer/src/widgets/git/status.ts +0 -19
  145. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +0 -17
  146. package/node_modules/pi-footer/src/widgets/git/untracked.ts +0 -17
  147. package/node_modules/pi-footer/src/widgets/instance.ts +0 -118
  148. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +0 -18
  149. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +0 -32
  150. package/node_modules/pi-footer/src/widgets/layout/separator.ts +0 -57
  151. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +0 -31
  152. package/node_modules/pi-footer/src/widgets/options.ts +0 -122
  153. package/node_modules/pi-footer/src/widgets/project/runtime.ts +0 -361
  154. package/node_modules/pi-footer/src/widgets/registry.ts +0 -223
  155. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +0 -17
  156. package/node_modules/pi-footer/src/widgets/session/compactions.ts +0 -17
  157. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +0 -18
  158. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +0 -18
  159. package/node_modules/pi-footer/src/widgets/session/messages.ts +0 -19
  160. package/node_modules/pi-footer/src/widgets/session/session-id.ts +0 -17
  161. package/node_modules/pi-footer/src/widgets/session/session-start.ts +0 -18
  162. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +0 -17
  163. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +0 -19
  164. package/node_modules/pi-footer/src/widgets/session/total-time.ts +0 -18
  165. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +0 -17
  166. package/node_modules/pi-footer/src/widgets/store.ts +0 -26
  167. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +0 -55
  168. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +0 -18
  169. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +0 -18
  170. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +0 -108
  171. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +0 -26
  172. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +0 -29
  173. package/node_modules/pi-footer/src/widgets/tokens/context.ts +0 -29
  174. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +0 -51
  175. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +0 -25
  176. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +0 -18
  177. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +0 -25
  178. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +0 -18
  179. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +0 -20
  180. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +0 -25
  181. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +0 -18
  182. package/node_modules/pi-footer/src/widgets/types.ts +0 -181
  183. package/node_modules/pi-footer/src/widgets/utils/colors.ts +0 -11
  184. package/node_modules/pi-footer/src/widgets/utils/context.ts +0 -112
  185. package/node_modules/pi-footer/src/widgets/utils/session.ts +0 -19
  186. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +0 -78
@@ -0,0 +1,479 @@
1
+ /**
2
+ * 自定义底栏:chips + zentui 图标/句子
3
+ *
4
+ * 第一行:model thinking · ██████░░░░ 45%/200k · 󰆼 42% · $0.01 · 用户排到 line1 的插件芯片
5
+ * 第二行:cwd in session on  branch (+16 −1) · 用户排到 line2 的插件芯片
6
+ * 第三行:仅当 line3 有可见插件芯片时出现
7
+ *
8
+ * - line1 短芯片,· 分隔;缓存用 zentui 󰆼,费用 success
9
+ * - line2 用 zentui 句式 in / on + 
10
+ * - 当前模型用量:优先 pi-usage 的 usage 状态;xAI 等不写 statusline 的供应商由本扩展补拉
11
+ *
12
+ * 模型/计费/思考级别变化时自动更新(pi.on 全局事件 + render 实时计算)
13
+ */
14
+
15
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
+ import { truncateToWidth } from "@earendil-works/pi-tui";
17
+ import { execFile } from "node:child_process";
18
+ import { existsSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { join } from "node:path";
21
+ import { pathToFileURL } from "node:url";
22
+ import { config } from "../../config/config.ts";
23
+ import { stripAnsi } from "../../utils/ansi-text.ts";
24
+ import {
25
+ PI_USAGE_KEY,
26
+ isSkippedFooterStatusKey,
27
+ resolveFooterChipLayout,
28
+ visibleFooterPluginTexts,
29
+ type FooterChipLayout,
30
+ } from "./footer-layout.ts";
31
+
32
+ const GIT_REFRESH_INTERVAL_MS = 10_000;
33
+ const USAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
34
+ const USAGE_TIMEOUT_MS = 15_000;
35
+ // zentui NERD_DEFAULT_ICONS:git / cacheHit。无 Nerd Font 时留空,只保留文字。
36
+ export const FOOTER_NERD_ICON_GIT = "";
37
+ export const FOOTER_NERD_ICON_CACHE = "󰆼";
38
+
39
+ export function footerGlyphs(nerdIcons: boolean): { git: string; cache: string } {
40
+ return nerdIcons
41
+ ? { git: FOOTER_NERD_ICON_GIT, cache: FOOTER_NERD_ICON_CACHE }
42
+ : { git: "", cache: "" };
43
+ }
44
+
45
+ type GitStats = { add: number; del: number };
46
+
47
+ type XaiFooterReport = {
48
+ buckets?: Array<{
49
+ id?: string;
50
+ unit?: string;
51
+ used?: number;
52
+ limit?: number;
53
+ remaining?: number;
54
+ }>;
55
+ metrics?: Array<{ id?: string; value?: unknown }>;
56
+ };
57
+
58
+ export function parseGitStats(stdout: string): GitStats {
59
+ let add = 0;
60
+ let del = 0;
61
+ for (const line of stdout.split("\n")) {
62
+ const match = line.match(/^(\d+)\s+(\d+)/);
63
+ if (match) {
64
+ add += Number(match[1]);
65
+ del += Number(match[2]);
66
+ }
67
+ }
68
+ return { add, del };
69
+ }
70
+
71
+ export function formatXaiFooterChip(report: XaiFooterReport): string | undefined {
72
+ const included = report.buckets?.find((b) => b.id === "included-allowance");
73
+ if (included?.unit === "percent" && typeof included.used === "number") {
74
+ return `xAI ${Math.round(included.used)}%`;
75
+ }
76
+ if (included && included.unit !== "percent") {
77
+ if (
78
+ typeof included.used === "number" &&
79
+ typeof included.limit === "number" &&
80
+ included.limit > 0
81
+ ) {
82
+ return `xAI ${Math.round((included.used / included.limit) * 100)}%`;
83
+ }
84
+ if (typeof included.remaining === "number") {
85
+ return `xAI $${included.remaining.toFixed(2)}`;
86
+ }
87
+ if (typeof included.used === "number") {
88
+ return `xAI $${included.used.toFixed(2)}`;
89
+ }
90
+ }
91
+ const prepaid = report.metrics?.find((m) => m.id === "prepaid-balance");
92
+ if (typeof prepaid?.value === "number") {
93
+ return `xAI $${prepaid.value.toFixed(2)}`;
94
+ }
95
+ return undefined;
96
+ }
97
+
98
+ const cachedExtensionStatuses = new Map<string, string>();
99
+ let cachedLocalUsageText = "";
100
+
101
+ /** 面板用:当前 setStatus 文案 + 本包 pi-usage(无文案时为空字符串)。 */
102
+ export function getFooterStatusSnapshot(): Map<string, string> {
103
+ const out = new Map<string, string>();
104
+ for (const [key, text] of cachedExtensionStatuses) {
105
+ if (isSkippedFooterStatusKey(key)) continue;
106
+ out.set(key, text);
107
+ }
108
+ out.set(PI_USAGE_KEY, cachedLocalUsageText);
109
+ return out;
110
+ }
111
+
112
+ function rememberExtensionStatuses(entries: Iterable<[string, string]>): void {
113
+ cachedExtensionStatuses.clear();
114
+ for (const [key, raw] of entries) {
115
+ if (isSkippedFooterStatusKey(key)) continue;
116
+ const text = stripAnsi(raw.replace(/[\r\n\t]+/g, " ").trim());
117
+ if (text) cachedExtensionStatuses.set(key, text);
118
+ }
119
+ }
120
+
121
+ function pluginTextsForRender(localUsageChip: string): Map<string, string> {
122
+ cachedLocalUsageText = stripAnsi(localUsageChip.replace(/[\r\n\t]+/g, " ").trim());
123
+ const texts = new Map<string, string>(cachedExtensionStatuses);
124
+ if (cachedLocalUsageText) texts.set(PI_USAGE_KEY, cachedLocalUsageText);
125
+ else texts.delete(PI_USAGE_KEY);
126
+ return texts;
127
+ }
128
+
129
+ function layoutFromConfig(): FooterChipLayout {
130
+ return {
131
+ footerHiddenKeys: config.footerHiddenKeys,
132
+ footerLine1Keys: config.footerLine1Keys,
133
+ footerLine2Keys: config.footerLine2Keys,
134
+ footerLine3Keys: config.footerLine3Keys,
135
+ };
136
+ }
137
+
138
+ let piUsageMod: any | null | undefined;
139
+
140
+ function piUsageImportSpecs(): string[] {
141
+ const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
142
+ const dist = join(agentDir, "npm", "node_modules", "@narumitw", "pi-usage", "dist", "index.ts");
143
+ const specs: string[] = [];
144
+ if (existsSync(dist)) specs.push(pathToFileURL(dist).href);
145
+ specs.push("@narumitw/pi-usage");
146
+ return specs;
147
+ }
148
+
149
+ async function getPiUsage(): Promise<any | null> {
150
+ if (piUsageMod !== undefined) return piUsageMod;
151
+ for (const spec of piUsageImportSpecs()) {
152
+ try {
153
+ piUsageMod = await import(spec);
154
+ return piUsageMod;
155
+ } catch {
156
+ // 下一候选:本机 Pi npm 目录,或包名(与本包同 node_modules 时)
157
+ }
158
+ }
159
+ piUsageMod = null;
160
+ return piUsageMod;
161
+ }
162
+
163
+ function colorUsageChip(theme: any, text: string): string {
164
+ return theme.fg("dim", stripAnsi(text));
165
+ }
166
+
167
+ let currentTui: any = undefined;
168
+ let refreshCurrentGitStats: (() => void) | undefined;
169
+ let refreshCurrentUsage: (() => void) | undefined;
170
+
171
+ const createCustomFooterFactory =
172
+ (ctx: ExtensionContext) => (tui: any, theme: any, footerData: any) => {
173
+ currentTui = tui;
174
+ const sep = theme.fg("muted", " · ");
175
+ const joinChips = (parts: string[]) => parts.filter(Boolean).join(sep);
176
+ let gitStats: GitStats | undefined;
177
+ let gitRefreshRunning = false;
178
+ let localUsageChip = "";
179
+ let usageGeneration = 0;
180
+ let usageAbort: AbortController | undefined;
181
+ let disposed = false;
182
+
183
+ // 查询在 render 外执行,保留旧值;仅统计结果真正变化时重绘,避免周期性清零抖动。
184
+ const refreshGitStats = () => {
185
+ if (gitRefreshRunning || disposed) return;
186
+ gitRefreshRunning = true;
187
+ execFile(
188
+ "git",
189
+ ["diff", "--numstat", "HEAD"],
190
+ { cwd: ctx.cwd, timeout: 2000 },
191
+ (err, stdout) => {
192
+ gitRefreshRunning = false;
193
+ if (err || disposed) return;
194
+ const next = parseGitStats(stdout);
195
+ if (gitStats?.add === next.add && gitStats.del === next.del) return;
196
+ gitStats = next;
197
+ tui.requestRender();
198
+ },
199
+ );
200
+ };
201
+ refreshCurrentGitStats = refreshGitStats;
202
+ refreshGitStats();
203
+ const gitRefreshTimer = setInterval(refreshGitStats, GIT_REFRESH_INTERVAL_MS);
204
+ gitRefreshTimer.unref?.();
205
+
206
+ const refreshUsage = () => {
207
+ if (disposed) return;
208
+ const generation = ++usageGeneration;
209
+ usageAbort?.abort();
210
+ const controller = new AbortController();
211
+ usageAbort = controller;
212
+ const stale = () => disposed || generation !== usageGeneration || controller.signal.aborted;
213
+ void (async () => {
214
+ try {
215
+ const model = ctx.model;
216
+ const api = await getPiUsage();
217
+ if (stale()) return;
218
+ if (!api || !model) {
219
+ if (localUsageChip) {
220
+ localUsageChip = "";
221
+ tui.requestRender();
222
+ }
223
+ return;
224
+ }
225
+ const adapter = api.adapterForProvider?.(model.provider);
226
+ if (!adapter || adapter.publishesStatusline !== false) {
227
+ if (localUsageChip) {
228
+ localUsageChip = "";
229
+ tui.requestRender();
230
+ }
231
+ return;
232
+ }
233
+ const auth = await api.resolveUsageAuth(ctx, adapter);
234
+ if (stale()) return;
235
+ if (!auth) {
236
+ const next = `${adapter.displayName} ✗`;
237
+ if (localUsageChip !== next) {
238
+ localUsageChip = next;
239
+ tui.requestRender();
240
+ }
241
+ return;
242
+ }
243
+ const guard = async () => {
244
+ if (stale()) throw new Error("aborted");
245
+ const again = await api.resolveUsageAuth(ctx, adapter);
246
+ if (again?.fingerprint !== auth.fingerprint) {
247
+ throw new Error("aborted");
248
+ }
249
+ };
250
+ const report = await api.queryProviderUsage(
251
+ adapter,
252
+ auth,
253
+ controller.signal,
254
+ USAGE_TIMEOUT_MS,
255
+ guard,
256
+ );
257
+ if (stale()) return;
258
+ const next =
259
+ (adapter.id === "xai" ? formatXaiFooterChip(report) : undefined) ||
260
+ api.formatUsageStatusline?.(report, model) ||
261
+ `${adapter.displayName} ✗`;
262
+ if (localUsageChip !== next) {
263
+ localUsageChip = next;
264
+ tui.requestRender();
265
+ }
266
+ } catch {
267
+ if (stale()) return;
268
+ const name = ctx.model?.provider === "xai" ? "xAI" : "usage";
269
+ const next = `${name} ✗`;
270
+ if (localUsageChip !== next) {
271
+ localUsageChip = next;
272
+ tui.requestRender();
273
+ }
274
+ } finally {
275
+ if (usageAbort === controller) usageAbort = undefined;
276
+ }
277
+ })();
278
+ };
279
+ refreshCurrentUsage = refreshUsage;
280
+ refreshUsage();
281
+ const usageRefreshTimer = setInterval(refreshUsage, USAGE_REFRESH_INTERVAL_MS);
282
+ usageRefreshTimer.unref?.();
283
+
284
+ // 分支变化时同步更新分支名和相对 HEAD 的统计。
285
+ const unsubBranch = footerData.onBranchChange(() => {
286
+ refreshGitStats();
287
+ tui.requestRender();
288
+ });
289
+
290
+ const fmt = (n: number): string => {
291
+ if (n < 1000) return `${n}`;
292
+ if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
293
+ if (n < 1000000) return `${Math.round(n / 1000)}k`;
294
+ if (n < 10000000) return `${(n / 1000000).toFixed(1)}M`;
295
+ return `${Math.round(n / 1000000)}M`;
296
+ };
297
+
298
+ // ---- pi-cc-status 风格:上下文条形图 ----
299
+ const BAR_WIDTH = 10;
300
+ const BAR_WARNING = 80; // >= 80% 转 warning 色
301
+ const BAR_ERROR = 95; // >= 95% 转 error 色
302
+ const barGauge = (pct: number): string => {
303
+ const clamped = Math.max(0, Math.min(100, pct));
304
+ const filled = Math.round((clamped / 100) * BAR_WIDTH);
305
+ const color = clamped >= BAR_ERROR ? "error" : clamped >= BAR_WARNING ? "warning" : "accent";
306
+ return theme.fg(color, "█".repeat(filled)) + theme.fg("dim", "░".repeat(BAR_WIDTH - filled));
307
+ };
308
+
309
+ // pi-cc-status showCachePercent:最近一条助手消息的 cacheRead 占比
310
+ const getCachePct = (): number => {
311
+ const branch = ctx.sessionManager.getBranch();
312
+ for (let i = branch.length - 1; i >= 0; i--) {
313
+ const e = branch[i];
314
+ if (e.type !== "message" || e.message.role !== "assistant") continue;
315
+ const u = e.message.usage;
316
+ if (!u) return 0;
317
+ const total = u.input + u.cacheWrite + u.cacheRead;
318
+ return total > 0 ? (u.cacheRead * 100) / total : 0;
319
+ }
320
+ return 0;
321
+ };
322
+
323
+ const render = (width: number): string[] => {
324
+ const model = ctx.model;
325
+ const contextUsage = ctx.getContextUsage();
326
+ const contextWindow = contextUsage?.contextWindow ?? model?.contextWindow ?? 0;
327
+ const percent = contextUsage?.percent;
328
+
329
+ let cost = 0;
330
+ for (const entry of ctx.sessionManager.getEntries()) {
331
+ if (entry.type === "message") {
332
+ const m = entry.message;
333
+ if (m.role === "assistant") cost += m.usage?.cost?.total || 0;
334
+ else if (m.role === "toolResult" && m.usage) cost += m.usage.cost?.total || 0;
335
+ } else if (
336
+ (entry.type === "branch_summary" || entry.type === "compaction") &&
337
+ entry.usage
338
+ ) {
339
+ cost += entry.usage.cost?.total || 0;
340
+ }
341
+ }
342
+
343
+ // cwd 默认只显示最底层路径(根目录时回退完整路径)
344
+ const cwdLabel = ctx.cwd.split(/[\\/]/).filter(Boolean).pop() || ctx.cwd;
345
+
346
+ const usingSubscription =
347
+ !!model &&
348
+ ["kimi-coding", "xai", "github-copilot", "openai"].includes(model.provider || "");
349
+ const modelLabel = model
350
+ ? model.id.includes("/")
351
+ ? model.id
352
+ : `${model.provider}/${model.id}`
353
+ : "no-model";
354
+
355
+ const thinkingLevelStr = ctx.thinkingLevel || "off";
356
+ const thinkingColor = theme.getThinkingBorderColor(thinkingLevelStr);
357
+
358
+ rememberExtensionStatuses(
359
+ footerData.getExtensionStatuses().entries() as Iterable<[string, string]>,
360
+ );
361
+ const pluginTexts = pluginTextsForRender(localUsageChip);
362
+ const layout = resolveFooterChipLayout(layoutFromConfig(), [...pluginTexts.keys()]);
363
+ const dimPlugin = (text: string) => colorUsageChip(theme, text);
364
+ const line1Plugins = visibleFooterPluginTexts(
365
+ layout.footerLine1Keys,
366
+ layout.footerHiddenKeys,
367
+ pluginTexts,
368
+ ).map(dimPlugin);
369
+ const line2Plugins = visibleFooterPluginTexts(
370
+ layout.footerLine2Keys,
371
+ layout.footerHiddenKeys,
372
+ pluginTexts,
373
+ ).map(dimPlugin);
374
+ const line3Plugins = visibleFooterPluginTexts(
375
+ layout.footerLine3Keys,
376
+ layout.footerHiddenKeys,
377
+ pluginTexts,
378
+ ).map(dimPlugin);
379
+
380
+ const pctLabel = percent === null || percent === undefined ? "?" : `${Math.floor(percent)}%`;
381
+ const cachePct = getCachePct();
382
+ const glyphs = footerGlyphs(config.footerNerdIcons);
383
+ const cacheLabel =
384
+ cachePct > 0 ? `${glyphs.cache ? `${glyphs.cache} ` : ""}${Math.floor(cachePct)}%` : "";
385
+ const costChip =
386
+ cost || usingSubscription
387
+ ? theme.fg("dim", `$${cost.toFixed(2)}`) +
388
+ (usingSubscription ? theme.fg("warning", " sub") : "")
389
+ : "";
390
+ const line1 = joinChips([
391
+ theme.fg("accent", modelLabel),
392
+ model?.reasoning ? thinkingColor(thinkingLevelStr) : "",
393
+ barGauge(percent ?? 0) + theme.fg("dim", ` ${pctLabel}/${fmt(contextWindow)}`),
394
+ cacheLabel ? theme.fg("dim", cacheLabel) : "",
395
+ costChip,
396
+ ...line1Plugins,
397
+ ]);
398
+
399
+ const gitColor = theme.getThinkingBorderColor("medium");
400
+ let place = theme.fg("accent", cwdLabel);
401
+ const sessionName = ctx.sessionManager.getSessionName();
402
+ if (sessionName) {
403
+ place += theme.fg("muted", " in ") + theme.fg("success", sessionName);
404
+ }
405
+ const branch = footerData.getGitBranch();
406
+ if (branch) {
407
+ const stats =
408
+ gitStats && (gitStats.add || gitStats.del)
409
+ ? theme.fg("dim", " (") +
410
+ theme.fg("success", `+${gitStats.add}`) +
411
+ " " +
412
+ theme.fg("error", `−${gitStats.del}`) +
413
+ theme.fg("dim", ")")
414
+ : "";
415
+ const gitLabel = glyphs.git ? `${glyphs.git} ${branch}` : branch;
416
+ place += theme.fg("muted", " on ") + gitColor(gitLabel) + stats;
417
+ }
418
+ const line2 = joinChips([place, ...line2Plugins]);
419
+ const line3 = joinChips(line3Plugins);
420
+
421
+ return [
422
+ truncateToWidth(line1, width),
423
+ ...(line2 ? [truncateToWidth(line2, width)] : []),
424
+ ...(line3 ? [truncateToWidth(line3, width)] : []),
425
+ ];
426
+ };
427
+
428
+ return {
429
+ render,
430
+ invalidate(): void {
431
+ tui.requestRender();
432
+ },
433
+ dispose(): void {
434
+ disposed = true;
435
+ usageAbort?.abort();
436
+ usageAbort = undefined;
437
+ clearInterval(gitRefreshTimer);
438
+ clearInterval(usageRefreshTimer);
439
+ unsubBranch();
440
+ if (currentTui === tui) {
441
+ currentTui = undefined;
442
+ refreshCurrentGitStats = undefined;
443
+ refreshCurrentUsage = undefined;
444
+ }
445
+ },
446
+ };
447
+ };
448
+
449
+ /** Apply the configured custom footer without disturbing another extension's footer when disabled. */
450
+ export function applyCustomFooter(ctx: ExtensionContext): void {
451
+ if (!ctx?.hasUI || typeof ctx.ui?.setFooter !== "function" || !config.enableCustomFooter) return;
452
+ try {
453
+ ctx.ui.setFooter(createCustomFooterFactory(ctx));
454
+ } catch (err) {
455
+ ctx.ui.notify(`footer error: ${err instanceof Error ? err.message : String(err)}`, "error");
456
+ }
457
+ }
458
+
459
+ /** Restore Pi's native footer when the user explicitly disables this extension's active footer. */
460
+ export function clearCustomFooter(ctx: ExtensionContext): void {
461
+ if (!ctx?.hasUI || typeof ctx.ui?.setFooter !== "function") return;
462
+ ctx.ui.setFooter(undefined);
463
+ }
464
+
465
+ export default function (pi: ExtensionAPI) {
466
+ // 模型/思考级别变化时强制重渲染(自定义 footer 不会被内置 invalidate() 触达)
467
+ pi.on("model_select", () => {
468
+ currentTui?.requestRender();
469
+ refreshCurrentUsage?.();
470
+ });
471
+ pi.on("thinking_level_select", () => currentTui?.requestRender());
472
+ // 工具执行完成后立即刷新;定时器只负责兜底捕获外部文件变化。
473
+ pi.on("tool_execution_end", () => refreshCurrentGitStats?.());
474
+
475
+ // 启动 / /reload / 新建会话 时按配置恢复
476
+ pi.on("session_start", (_event, ctx) => {
477
+ applyCustomFooter(ctx);
478
+ });
479
+ }
@@ -1,8 +1,14 @@
1
- import { VERSION, type AppKeybinding } from "@earendil-works/pi-coding-agent";
1
+ import { InteractiveMode, VERSION, type AppKeybinding } from "@earendil-works/pi-coding-agent";
2
2
  import { getKeybindings } from "@earendil-works/pi-tui";
3
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { config } from "../../config/config.ts";
5
-
5
+ import { ansi16ToRgb, ansi256ToRgb } from "../../utils/ansi-color.ts";
6
+ import { stripAnsi } from "../../utils/ansi-text.ts";
7
+ import {
8
+ EARLY_STARTUP_HEADER_PATCH,
9
+ EARLY_STARTUP_HEADER_SWAP_KEY,
10
+ patchRegistry,
11
+ } from "../../utils/patch-keys.ts";
6
12
  type Rgb = [number, number, number];
7
13
  type StyledPart = {
8
14
  raw: string;
@@ -10,8 +16,6 @@ type StyledPart = {
10
16
  };
11
17
 
12
18
  const ANSI_RESET = "\x1b[0m";
13
- const ANSI_PATTERN =
14
- /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
15
19
 
16
20
  // 官方 install.sh 静态 logo(4 行原样,短行补尾随空格统一到 8 列)+ 底部空行补到 5 行,
17
21
  // 与右侧 tips 行数等高;着色保持现状(accent 渐变)
@@ -40,29 +44,6 @@ const PALETTE_SPAN = 0.25;
40
44
  // 行间相位偏移:小步累加 → 整体左上暗→右下亮的对角渐变
41
45
  const LOGO_ROW_PHASE_STEP = 0.08;
42
46
 
43
- const ANSI_16_RGB_TABLE: Rgb[] = [
44
- [0, 0, 0],
45
- [128, 0, 0],
46
- [0, 128, 0],
47
- [128, 128, 0],
48
- [0, 0, 128],
49
- [128, 0, 128],
50
- [0, 128, 128],
51
- [192, 192, 192],
52
- [128, 128, 128],
53
- [255, 0, 0],
54
- [0, 255, 0],
55
- [255, 255, 0],
56
- [0, 0, 255],
57
- [255, 0, 255],
58
- [0, 255, 255],
59
- [255, 255, 255],
60
- ];
61
-
62
- function stripAnsi(text: string): string {
63
- return text.replace(ANSI_PATTERN, "");
64
- }
65
-
66
47
  function getVisibleLength(text: string): number {
67
48
  return [...stripAnsi(text)].length;
68
49
  }
@@ -104,27 +85,6 @@ function applyTruecolor(rgb: Rgb, text: string): string {
104
85
  return `\x1b[38;2;${red};${green};${blue}m${text}${ANSI_RESET}`;
105
86
  }
106
87
 
107
- function ansi16ToRgb(index: number): Rgb {
108
- return ANSI_16_RGB_TABLE[index] ?? [255, 255, 255];
109
- }
110
-
111
- function ansi256ToRgb(index: number): Rgb {
112
- if (index < 16) return ansi16ToRgb(index);
113
-
114
- if (index >= 232) {
115
- const gray = 8 + (index - 232) * 10;
116
- return [gray, gray, gray];
117
- }
118
-
119
- const cubeIndex = index - 16;
120
- const redIndex = Math.floor(cubeIndex / 36);
121
- const greenIndex = Math.floor((cubeIndex % 36) / 6);
122
- const blueIndex = cubeIndex % 6;
123
- const values = [0, 95, 135, 175, 215, 255];
124
-
125
- return [values[redIndex]!, values[greenIndex]!, values[blueIndex]!];
126
- }
127
-
128
88
  function parseTruecolorAnsi(ansi: string): Rgb | undefined {
129
89
  const match = ansi.match(/38;2;(\d+);(\d+);(\d+)/);
130
90
  if (!match) return undefined;
@@ -324,23 +284,94 @@ export function renderHeaderLines(
324
284
  );
325
285
  }
326
286
 
327
- /**
328
- * 按配置应用启动头:on → 自定义 header;off → 恢复官方默认 header。
329
- * 导出供 /ccstyle 面板在切换开关时实时重应用。
330
- */
331
- export function applyStartupHeader(ctx: any): void {
332
- if (!ctx?.hasUI || typeof ctx.ui?.setHeader !== "function") return;
333
- if (!config.showStartupHeader) {
334
- // 恢复官方内置 header(logo + 快捷键提示 + onboarding)。
335
- ctx.ui.setHeader(undefined);
336
- return;
337
- }
338
- ctx.ui.setHeader((_tui, theme) => ({
287
+ /** 官方 setHeader 的组件工厂:theme 由宿主注入。 */
288
+ function headerFactory(): (
289
+ tui: unknown,
290
+ theme: any,
291
+ ) => {
292
+ render(width: number): string[];
293
+ invalidate(): void;
294
+ } {
295
+ return (_tui: unknown, theme: any) => ({
339
296
  render(width: number): string[] {
340
297
  return renderHeaderLines(width, theme);
341
298
  },
342
299
  invalidate() {},
343
- }));
300
+ });
301
+ }
302
+
303
+ /** 按配置安装启动头。禁用时不碰槽位,避免清掉其他扩展的 header。 */
304
+ export function applyStartupHeader(ctx: any): void {
305
+ if (!ctx?.hasUI || typeof ctx.ui?.setHeader !== "function" || !config.showStartupHeader) {
306
+ return;
307
+ }
308
+ ctx.ui.setHeader(headerFactory());
309
+ }
310
+
311
+ type EarlyHeaderPatch = {
312
+ active: boolean;
313
+ original: (...args: any[]) => unknown;
314
+ installed: (...args: any[]) => unknown;
315
+ };
316
+
317
+ type HeaderContainerSwap = {
318
+ original: (child: any) => unknown;
319
+ installed: (child: any) => unknown;
320
+ };
321
+
322
+ /**
323
+ * Pi 在 session_start 之前就把原生启动头画上去了:init() 先建 header 再 requestRender,
324
+ * 之后才 await 工具检查、加载扩展,所以扩展最早只能到 session_start 换头,启动时会闪一下原生头。
325
+ * 这里补 init,在原生 header 刚进容器的瞬间换成 ccstyle 的,首次绘制就已经是我们的。
326
+ * 判定用 child === mode.builtInHeader,不依赖类名;配置关掉时不碰槽位,保留原生头。
327
+ */
328
+ export function installEarlyStartupHeader(): void {
329
+ const prototype = (InteractiveMode as any)?.prototype;
330
+ if (!prototype || typeof prototype.init !== "function") return;
331
+ const previous = patchRegistry.get<EarlyHeaderPatch>(EARLY_STARTUP_HEADER_PATCH);
332
+ if (previous) previous.active = false;
333
+ const original =
334
+ previous && prototype.init === previous.installed ? previous.original : prototype.init;
335
+ const patch: EarlyHeaderPatch = {
336
+ active: true,
337
+ original,
338
+ installed: async function (this: any, ...args: any[]) {
339
+ installHeaderContainerSwap(this, patch);
340
+ return original.apply(this, args);
341
+ },
342
+ };
343
+ prototype.init = patch.installed;
344
+ patchRegistry.install(EARLY_STARTUP_HEADER_PATCH, patch);
345
+ }
346
+
347
+ /** 只包一次 headerContainer.addChild;/reload 后按所有权接管旧包装。 */
348
+ function installHeaderContainerSwap(mode: any, patch: EarlyHeaderPatch): void {
349
+ const container = mode?.headerContainer;
350
+ if (!container || typeof container.addChild !== "function") return;
351
+ const previous = container[EARLY_STARTUP_HEADER_SWAP_KEY] as HeaderContainerSwap | undefined;
352
+ const original =
353
+ previous && container.addChild === previous.installed
354
+ ? previous.original
355
+ : container.addChild.bind(container);
356
+ const installed = function (child: any) {
357
+ const result = original(child);
358
+ if (!patch.active || !config.showStartupHeader || child !== mode.builtInHeader) return result;
359
+ if (typeof mode.setExtensionHeader !== "function") return result;
360
+ try {
361
+ mode.setExtensionHeader(headerFactory());
362
+ } catch {
363
+ // 换头失败就退回原生 header,不影响启动
364
+ }
365
+ return result;
366
+ };
367
+ container.addChild = installed;
368
+ container[EARLY_STARTUP_HEADER_SWAP_KEY] = { original, installed };
369
+ }
370
+
371
+ /** 用户从 /ccstyle 关掉本扩展启动头时,恢复官方 header。 */
372
+ export function clearStartupHeader(ctx: any): void {
373
+ if (!ctx?.hasUI || typeof ctx.ui?.setHeader !== "function") return;
374
+ ctx.ui.setHeader(undefined);
344
375
  }
345
376
 
346
377
  export default function piStartupHeader(pi: ExtensionAPI) {