@herbertgao/pi-extensions 2026.9.18 → 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 (226) hide show
  1. package/README.md +20 -28
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +76 -59
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.md +76 -59
  5. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/preview.webp +0 -0
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +93 -34
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +346 -117
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +6 -36
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/index.ts +2 -2
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -29
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +330 -360
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +56 -108
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/session.ts +97 -15
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +80 -45
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/flush-docked-bash.ts +36 -0
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-layout.ts +229 -0
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +479 -0
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/startup-header.ts +92 -61
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +75 -40
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +19 -3
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +1080 -229
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +232 -259
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +42 -41
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +8 -2
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +2 -7
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +277 -113
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +15 -6
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +18 -72
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +61 -26
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +25 -57
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-component.ts +125 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +227 -0
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +136 -0
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +115 -0
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-inline.ts +324 -0
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-layout.ts +762 -0
  37. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +152 -0
  38. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-palette.ts +410 -0
  39. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-parse.ts +535 -0
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +21 -3219
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +44 -0
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +568 -0
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +47 -10
  45. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +2 -2
  46. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +1 -0
  47. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +145 -190
  48. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/mcp-title.ts +39 -0
  49. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{message-display.ts → tool/message-display.ts} +24 -90
  50. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +426 -0
  51. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +90 -132
  52. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{show-more-hint.ts → tool/show-more-hint.ts} +10 -0
  53. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +8 -26
  54. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-color.ts +44 -0
  55. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +76 -0
  56. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/component-tree.ts +34 -0
  57. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +28 -0
  58. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/fullscreen-detect.ts +51 -0
  59. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +124 -0
  60. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/sgr-mouse.ts +70 -0
  61. package/node_modules/@herbertgao/pi-cc-extensions/package.json +8 -8
  62. package/node_modules/pi-antigravity/README.md +29 -6
  63. package/node_modules/pi-antigravity/package.json +12 -7
  64. package/node_modules/pi-antigravity/src/auth/accounts.ts +327 -0
  65. package/node_modules/pi-antigravity/src/auth/index.ts +9 -0
  66. package/node_modules/pi-antigravity/src/client/client.ts +1 -1
  67. package/node_modules/pi-antigravity/src/image/image.ts +4 -4
  68. package/node_modules/pi-antigravity/src/index.ts +203 -13
  69. package/node_modules/pi-antigravity/src/search/index.ts +1 -0
  70. package/node_modules/pi-antigravity/src/search/search.ts +325 -0
  71. package/node_modules/pi-antigravity/src/stream/stream.ts +191 -14
  72. package/node_modules/pi-antigravity/src/types/enums.ts +45 -36
  73. package/node_modules/pi-antigravity/src/types/types.ts +5 -12
  74. package/node_modules/pi-antigravity/src/utils/http.ts +7 -2
  75. package/node_modules/pi-antigravity/src/utils/util.ts +31 -3
  76. package/node_modules/pi-typesafe/README.md +2 -0
  77. package/node_modules/pi-typesafe/package.json +2 -1
  78. package/node_modules/pi-web-access/CHANGELOG.md +72 -0
  79. package/node_modules/pi-web-access/README.md +34 -13
  80. package/node_modules/pi-web-access/chrome-cookies.ts +79 -25
  81. package/node_modules/pi-web-access/curator-page.ts +5 -1
  82. package/node_modules/pi-web-access/curator-server.ts +2 -0
  83. package/node_modules/pi-web-access/dist/index.js +1337 -1009
  84. package/node_modules/pi-web-access/exa.ts +20 -52
  85. package/node_modules/pi-web-access/extract.ts +41 -4
  86. package/node_modules/pi-web-access/gemini-search.ts +12 -4
  87. package/node_modules/pi-web-access/index.ts +29 -37
  88. package/node_modules/pi-web-access/openai-search.ts +35 -7
  89. package/node_modules/pi-web-access/package.json +6 -5
  90. package/node_modules/pi-web-access/page-query.ts +2 -2
  91. package/node_modules/pi-web-access/query-rewrite.ts +2 -3
  92. package/node_modules/pi-web-access/ssrf-protection.ts +27 -10
  93. package/node_modules/pi-web-access/storage.ts +48 -77
  94. package/node_modules/pi-web-access/summary-model-scope.ts +6 -77
  95. package/node_modules/pi-web-access/summary-review.ts +3 -4
  96. package/node_modules/pi-web-access/tavily.ts +67 -20
  97. package/node_modules/pi-web-access/tool-activation.ts +53 -33
  98. package/node_modules/pi-web-access/utils.ts +1 -1
  99. package/node_modules/pi-web-access/you.ts +164 -0
  100. package/node_modules/pi-web-access/zai.ts +161 -0
  101. package/package.json +7 -11
  102. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.en.svg +0 -101
  103. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.svg +0 -101
  104. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/welcome.webp +0 -0
  105. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/render-utils.ts +0 -5
  106. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +0 -30
  107. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-display-utils.ts +0 -12
  108. package/node_modules/pi-footer/CHANGELOG.md +0 -145
  109. package/node_modules/pi-footer/LICENSE +0 -21
  110. package/node_modules/pi-footer/README.md +0 -354
  111. package/node_modules/pi-footer/package.json +0 -68
  112. package/node_modules/pi-footer/src/cache.ts +0 -106
  113. package/node_modules/pi-footer/src/colors.ts +0 -237
  114. package/node_modules/pi-footer/src/config.ts +0 -202
  115. package/node_modules/pi-footer/src/event-widgets.ts +0 -41
  116. package/node_modules/pi-footer/src/extension-statuses.ts +0 -102
  117. package/node_modules/pi-footer/src/git.ts +0 -135
  118. package/node_modules/pi-footer/src/index.ts +0 -268
  119. package/node_modules/pi-footer/src/metrics.ts +0 -141
  120. package/node_modules/pi-footer/src/presets.ts +0 -383
  121. package/node_modules/pi-footer/src/render.ts +0 -114
  122. package/node_modules/pi-footer/src/separators.ts +0 -52
  123. package/node_modules/pi-footer/src/types.ts +0 -122
  124. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +0 -22
  125. package/node_modules/pi-footer/src/ui/color-options.ts +0 -195
  126. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +0 -61
  127. package/node_modules/pi-footer/src/ui/edit-colors.ts +0 -13
  128. package/node_modules/pi-footer/src/ui/events.ts +0 -35
  129. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +0 -60
  130. package/node_modules/pi-footer/src/ui/extension-statuses.ts +0 -73
  131. package/node_modules/pi-footer/src/ui/fields.ts +0 -252
  132. package/node_modules/pi-footer/src/ui/global-menu.ts +0 -126
  133. package/node_modules/pi-footer/src/ui/helpers.ts +0 -44
  134. package/node_modules/pi-footer/src/ui/layout.ts +0 -17
  135. package/node_modules/pi-footer/src/ui/line-list.ts +0 -19
  136. package/node_modules/pi-footer/src/ui/model.ts +0 -64
  137. package/node_modules/pi-footer/src/ui/navigation.ts +0 -29
  138. package/node_modules/pi-footer/src/ui/option-edit.ts +0 -93
  139. package/node_modules/pi-footer/src/ui/overlay-render.ts +0 -103
  140. package/node_modules/pi-footer/src/ui/screen-context.ts +0 -18
  141. package/node_modules/pi-footer/src/ui/screen-controller.ts +0 -27
  142. package/node_modules/pi-footer/src/ui/screen-render.ts +0 -32
  143. package/node_modules/pi-footer/src/ui/screen-state.ts +0 -20
  144. package/node_modules/pi-footer/src/ui/screen.ts +0 -243
  145. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +0 -92
  146. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +0 -52
  147. package/node_modules/pi-footer/src/ui/screens/controller.ts +0 -12
  148. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +0 -68
  149. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +0 -115
  150. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +0 -61
  151. package/node_modules/pi-footer/src/ui/screens/global.ts +0 -75
  152. package/node_modules/pi-footer/src/ui/screens/line-list.ts +0 -108
  153. package/node_modules/pi-footer/src/ui/screens/main.ts +0 -99
  154. package/node_modules/pi-footer/src/ui/screens/terminal.ts +0 -121
  155. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +0 -154
  156. package/node_modules/pi-footer/src/ui/terminal-menu.ts +0 -32
  157. package/node_modules/pi-footer/src/ui/theme.ts +0 -48
  158. package/node_modules/pi-footer/src/ui/title-bar.ts +0 -100
  159. package/node_modules/pi-footer/src/ui/widget-actions.ts +0 -76
  160. package/node_modules/pi-footer/src/ui.ts +0 -66
  161. package/node_modules/pi-footer/src/widgets/context.ts +0 -28
  162. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +0 -17
  163. package/node_modules/pi-footer/src/widgets/core/context-window.ts +0 -24
  164. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +0 -19
  165. package/node_modules/pi-footer/src/widgets/core/cwd.ts +0 -108
  166. package/node_modules/pi-footer/src/widgets/core/event.ts +0 -34
  167. package/node_modules/pi-footer/src/widgets/core/external-status.ts +0 -102
  168. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +0 -20
  169. package/node_modules/pi-footer/src/widgets/core/model.ts +0 -35
  170. package/node_modules/pi-footer/src/widgets/core/provider.ts +0 -17
  171. package/node_modules/pi-footer/src/widgets/core/session-name.ts +0 -17
  172. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +0 -17
  173. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +0 -17
  174. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +0 -17
  175. package/node_modules/pi-footer/src/widgets/git/branch.ts +0 -81
  176. package/node_modules/pi-footer/src/widgets/git/clean.ts +0 -19
  177. package/node_modules/pi-footer/src/widgets/git/deletions.ts +0 -17
  178. package/node_modules/pi-footer/src/widgets/git/diff.ts +0 -38
  179. package/node_modules/pi-footer/src/widgets/git/insertions.ts +0 -17
  180. package/node_modules/pi-footer/src/widgets/git/remote.ts +0 -17
  181. package/node_modules/pi-footer/src/widgets/git/root.ts +0 -17
  182. package/node_modules/pi-footer/src/widgets/git/sha.ts +0 -17
  183. package/node_modules/pi-footer/src/widgets/git/staged.ts +0 -17
  184. package/node_modules/pi-footer/src/widgets/git/status.ts +0 -19
  185. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +0 -17
  186. package/node_modules/pi-footer/src/widgets/git/untracked.ts +0 -17
  187. package/node_modules/pi-footer/src/widgets/instance.ts +0 -118
  188. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +0 -18
  189. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +0 -32
  190. package/node_modules/pi-footer/src/widgets/layout/separator.ts +0 -57
  191. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +0 -31
  192. package/node_modules/pi-footer/src/widgets/options.ts +0 -122
  193. package/node_modules/pi-footer/src/widgets/project/runtime.ts +0 -361
  194. package/node_modules/pi-footer/src/widgets/registry.ts +0 -223
  195. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +0 -17
  196. package/node_modules/pi-footer/src/widgets/session/compactions.ts +0 -17
  197. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +0 -18
  198. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +0 -18
  199. package/node_modules/pi-footer/src/widgets/session/messages.ts +0 -19
  200. package/node_modules/pi-footer/src/widgets/session/session-id.ts +0 -17
  201. package/node_modules/pi-footer/src/widgets/session/session-start.ts +0 -18
  202. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +0 -17
  203. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +0 -19
  204. package/node_modules/pi-footer/src/widgets/session/total-time.ts +0 -18
  205. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +0 -17
  206. package/node_modules/pi-footer/src/widgets/store.ts +0 -26
  207. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +0 -55
  208. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +0 -18
  209. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +0 -18
  210. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +0 -108
  211. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +0 -26
  212. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +0 -29
  213. package/node_modules/pi-footer/src/widgets/tokens/context.ts +0 -29
  214. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +0 -51
  215. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +0 -25
  216. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +0 -18
  217. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +0 -25
  218. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +0 -18
  219. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +0 -20
  220. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +0 -25
  221. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +0 -18
  222. package/node_modules/pi-footer/src/widgets/types.ts +0 -181
  223. package/node_modules/pi-footer/src/widgets/utils/colors.ts +0 -11
  224. package/node_modules/pi-footer/src/widgets/utils/context.ts +0 -112
  225. package/node_modules/pi-footer/src/widgets/utils/session.ts +0 -19
  226. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +0 -78
@@ -1,135 +0,0 @@
1
- import { basename } from "node:path";
2
-
3
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
-
5
- import { asyncCache, CACHE_NAMESPACES } from "./cache.js";
6
- import type { GitInfo, StatuslineConfig } from "./types.js";
7
-
8
- const CACHE_TTL_MS = 2000;
9
-
10
- export const EMPTY_GIT_INFO: GitInfo = {
11
- branch: null,
12
- sha: null,
13
- root: null,
14
- staged: 0,
15
- unstaged: 0,
16
- untracked: 0,
17
- insertions: 0,
18
- deletions: 0,
19
- ahead: 0,
20
- behind: 0,
21
- remote: null,
22
- isRepo: false,
23
- };
24
-
25
- interface GitFetch {
26
- pi: ExtensionAPI;
27
- cwd: string;
28
- branchHint: string | null;
29
- }
30
-
31
- // SWR read for the live footer: returns the cached (possibly stale) value immediately, kicks an
32
- // async refresh through asyncCache, and repaints via requestRender when fresh data lands. Stays
33
- // off the render path's critical section — the git subprocesses run async via pi.exec.
34
- export function getGitInfo(
35
- pi: ExtensionAPI,
36
- cwd: string,
37
- branchHint: string | null,
38
- requestRender: () => void,
39
- ): GitInfo {
40
- return (
41
- asyncCache.get(
42
- CACHE_NAMESPACES.git,
43
- `${cwd}:${branchHint ?? ""}`,
44
- CACHE_TTL_MS,
45
- { pi, cwd, branchHint },
46
- fetchGitInfo,
47
- requestRender,
48
- ) ?? EMPTY_GIT_INFO
49
- );
50
- }
51
-
52
- // Awaitable git fetch for callers that can block (e.g. the config-UI preview command). Same work
53
- // the SWR fetcher does, exposed directly so a one-shot snapshot can await real data.
54
- export function loadGitInfo(
55
- pi: ExtensionAPI,
56
- cwd: string,
57
- branchHint: string | null,
58
- ): Promise<GitInfo> {
59
- return fetchGitInfo({ pi, cwd, branchHint }).then((info) => info ?? EMPTY_GIT_INFO);
60
- }
61
-
62
- async function fetchGitInfo({ pi, cwd, branchHint }: GitFetch): Promise<GitInfo | null> {
63
- const rootPath = await git(pi, cwd, ["rev-parse", "--show-toplevel"]);
64
- if (!rootPath) return EMPTY_GIT_INFO;
65
-
66
- const [branch, sha, porcelain, shortstat, aheadBehind, remote] = await Promise.all([
67
- branchHint ? Promise.resolve(branchHint) : git(pi, cwd, ["rev-parse", "--abbrev-ref", "HEAD"]),
68
- git(pi, cwd, ["rev-parse", "--short", "HEAD"]),
69
- git(pi, cwd, ["status", "--porcelain=v1"]),
70
- git(pi, cwd, ["diff", "--shortstat", "HEAD"]),
71
- git(pi, cwd, ["rev-list", "--left-right", "--count", "@{upstream}...HEAD"]),
72
- git(pi, cwd, ["remote", "get-url", "origin"]),
73
- ]);
74
-
75
- return {
76
- branch,
77
- sha,
78
- root: basename(rootPath),
79
- ...parsePorcelain(porcelain),
80
- ...parseShortstat(shortstat),
81
- ...parseAheadBehind(aheadBehind),
82
- remote,
83
- isRepo: true,
84
- };
85
- }
86
-
87
- async function git(pi: ExtensionAPI, cwd: string, args: string[]): Promise<string | null> {
88
- const { stdout, code, killed } = await pi.exec("git", args, { cwd, timeout: 500 });
89
- // trimEnd, not trim: `status --porcelain=v1` encodes staged vs unstaged in columns 1/2, so the
90
- // leading space of the first line is significant. trim() would strip it and miscount that file.
91
- return code !== 0 || killed ? null : stdout.trimEnd() || null;
92
- }
93
-
94
- function parsePorcelain(output: string | null): Pick<GitInfo, "staged" | "unstaged" | "untracked"> {
95
- let staged = 0;
96
- let unstaged = 0;
97
- let untracked = 0;
98
-
99
- for (const line of output?.split("\n") ?? []) {
100
- if (line.length < 2) continue;
101
- const x = line[0];
102
- const y = line[1];
103
- if (x === "?" && y === "?") {
104
- untracked += 1;
105
- continue;
106
- }
107
- if (x !== " " && x !== undefined) staged += 1;
108
- if (y !== " " && y !== undefined) unstaged += 1;
109
- }
110
-
111
- return { staged, unstaged, untracked };
112
- }
113
-
114
- function parseShortstat(output: string | null): Pick<GitInfo, "insertions" | "deletions"> {
115
- const insertions = /([0-9]+) insertion/.exec(output ?? "")?.[1];
116
- const deletions = /([0-9]+) deletion/.exec(output ?? "")?.[1];
117
- return {
118
- insertions: insertions ? Number(insertions) : 0,
119
- deletions: deletions ? Number(deletions) : 0,
120
- };
121
- }
122
-
123
- function parseAheadBehind(output: string | null): Pick<GitInfo, "ahead" | "behind"> {
124
- const [behind, ahead] = output?.split(/\s+/).map(Number) ?? [];
125
- return {
126
- ahead: Number.isFinite(ahead) ? (ahead ?? 0) : 0,
127
- behind: Number.isFinite(behind) ? (behind ?? 0) : 0,
128
- };
129
- }
130
-
131
- export function hasEnabledGitWidgets(config: Pick<StatuslineConfig, "lines">): boolean {
132
- return config.lines.some((line) =>
133
- line.some((widget) => widget.enabled && widget.type.startsWith("git-")),
134
- );
135
- }
@@ -1,268 +0,0 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionCommandContext,
4
- ExtensionContext,
5
- } from "@earendil-works/pi-coding-agent";
6
- import { truncateToWidth } from "@earendil-works/pi-tui";
7
-
8
- import {
9
- cloneConfig,
10
- configWithPreset,
11
- DEFAULT_CONFIG,
12
- isPreset,
13
- loadConfig,
14
- saveConfig,
15
- STATUS_KEY,
16
- } from "./config.js";
17
- import { EventWidgetValues, UPDATE_EVENT_WIDGET_EVENT } from "./event-widgets.js";
18
- import {
19
- EMPTY_EXTENSION_STATUSES,
20
- visibleExtensionStatusRowEntries,
21
- } from "./extension-statuses.js";
22
- import { EMPTY_GIT_INFO, getGitInfo, hasEnabledGitWidgets, loadGitInfo } from "./git.js";
23
- import { collectSessionMetrics, collectTurnMetrics } from "./metrics.js";
24
- import { renderStatuslines } from "./render.js";
25
- import { isRecord, type GitInfo, type StatuslineConfig, type StatuslineData } from "./types.js";
26
- import { openStatuslineConfigUi } from "./ui.js";
27
- import { WidgetStore } from "./widgets/store.js";
28
-
29
- // Structural mirror of pi's footerData — collectStatuslineData only needs the branch getter.
30
- interface FooterDataLike {
31
- getGitBranch(): string | null;
32
- }
33
-
34
- export default async function statuslineExtension(pi: ExtensionAPI): Promise<void> {
35
- let config = await loadConfig();
36
- let widgetStore = WidgetStore.fromConfig(config);
37
- const eventWidgets = new EventWidgetValues();
38
- let liveTextVerbosity: string | undefined;
39
- let renderCurrentFooter: (() => void) | undefined;
40
- let getExtensionStatuses = (): ReadonlyMap<string, string> => EMPTY_EXTENSION_STATUSES;
41
-
42
- function apply(ctx: ExtensionContext | ExtensionCommandContext): void {
43
- if (!ctx.hasUI || !config.enabled) {
44
- ctx.ui.setFooter(undefined);
45
- ctx.ui.setStatus(STATUS_KEY, undefined);
46
- return;
47
- }
48
-
49
- ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("accent", "pi-footer"));
50
- ctx.ui.setFooter((tui, theme, footerData) => {
51
- getExtensionStatuses = () => footerData.getExtensionStatuses();
52
- renderCurrentFooter = () => tui.requestRender();
53
- const unsubscribeBranch = footerData.onBranchChange(() => tui.requestRender());
54
- return {
55
- dispose(): void {
56
- unsubscribeBranch();
57
- if (renderCurrentFooter) renderCurrentFooter = undefined;
58
- getExtensionStatuses = () => EMPTY_EXTENSION_STATUSES;
59
- },
60
- invalidate(): void {},
61
- render(width: number): string[] {
62
- const data = collectStatuslineData(ctx, pi, footerData, eventWidgets.values, {
63
- config,
64
- requestRender: () => tui.requestRender(),
65
- textVerbosity: liveTextVerbosity,
66
- });
67
- const lines = renderStatuslines(widgetStore, data, width, {
68
- getExtensionStatuses,
69
- theme,
70
- requestRender: () => tui.requestRender(),
71
- });
72
- if (lines.length === 0) return [];
73
-
74
- const statuses = visibleExtensionStatusRowEntries(
75
- footerData.getExtensionStatuses(),
76
- config.extensionStatusRow.hiddenKeys,
77
- STATUS_KEY,
78
- ).map((entry) => entry.value);
79
- const renderedLines = lines.map((line) => truncateToWidth(line, width, "…"));
80
- if (statuses.length === 0) return renderedLines;
81
- return [
82
- ...renderedLines,
83
- truncateToWidth(theme.fg("dim", statuses.join(" ")), width, "…"),
84
- ];
85
- },
86
- };
87
- });
88
- }
89
-
90
- function replaceConfig(next: StatuslineConfig): void {
91
- config = next;
92
- widgetStore = WidgetStore.fromConfig(config);
93
- }
94
-
95
- async function setConfig(
96
- next: StatuslineConfig,
97
- ctx: ExtensionContext | ExtensionCommandContext,
98
- ): Promise<void> {
99
- replaceConfig(next);
100
- await saveConfig(config);
101
- apply(ctx);
102
- }
103
-
104
- pi.events.on(UPDATE_EVENT_WIDGET_EVENT, (payload: unknown) => {
105
- const changed = eventWidgets.update(payload);
106
- if (changed) renderCurrentFooter?.();
107
- });
108
-
109
- // pi has no verbosity accessor; capture the real value from the actual provider
110
- // request payload. Passive — the footer picks it up on its next render rather than
111
- // forcing one. Only the openai-codex-responses payload carries text.verbosity.
112
- //
113
- // Handlers run in extension load order and the payload is chained forward, so we
114
- // observe it as of our own position in that chain (we return undefined and never
115
- // mutate it). A custom provider that bakes verbosity into the body is always seen;
116
- // a later-loaded extension that rewrites text.verbosity in its own
117
- // before_provider_request handler is the one case we'd miss.
118
- pi.on("before_provider_request", (event) => {
119
- const verbosity = readTextVerbosity(event.payload);
120
- if (verbosity !== undefined) liveTextVerbosity = verbosity;
121
- });
122
-
123
- pi.registerCommand("footer", {
124
- description: "Configure the pi statusline/footer",
125
- handler: async (args, ctx) => {
126
- const handled = await handleArgs(args, ctx, config, async (next) => setConfig(next, ctx));
127
- if (handled) return;
128
-
129
- const previewData = collectStatuslineData(
130
- ctx,
131
- pi,
132
- {
133
- getGitBranch: () => "main",
134
- },
135
- eventWidgets.values,
136
- {
137
- collectGit: true,
138
- git: await loadGitInfo(pi, ctx.cwd, "main"),
139
- textVerbosity: liveTextVerbosity,
140
- },
141
- );
142
- const result = await openStatuslineConfigUi(
143
- ctx,
144
- config,
145
- previewData,
146
- (updated) => {
147
- replaceConfig(updated);
148
- apply(ctx);
149
- },
150
- async (updated) => setConfig(updated, ctx),
151
- getExtensionStatuses,
152
- );
153
- replaceConfig(result.config);
154
- apply(ctx);
155
- },
156
- });
157
-
158
- pi.on("session_start", async (_event, ctx) => {
159
- replaceConfig(await loadConfig());
160
- apply(ctx);
161
- });
162
-
163
- pi.on("model_select", async (_event, ctx) => {
164
- // A new model selection invalidates the captured verbosity; fall back to the
165
- // provider default until the next request reveals the real value.
166
- liveTextVerbosity = undefined;
167
- apply(ctx);
168
- });
169
-
170
- pi.on("session_shutdown", (_event, ctx) => {
171
- ctx.ui.setFooter(undefined);
172
- ctx.ui.setStatus(STATUS_KEY, undefined);
173
- });
174
- }
175
-
176
- function collectStatuslineData(
177
- ctx: ExtensionContext | ExtensionCommandContext,
178
- pi: ExtensionAPI,
179
- footerData: FooterDataLike,
180
- eventWidgets: ReadonlyMap<string, string>,
181
- options: {
182
- config?: StatuslineConfig;
183
- collectGit?: boolean;
184
- requestRender?: () => void;
185
- git?: GitInfo;
186
- textVerbosity?: string | undefined;
187
- } = {},
188
- ): StatuslineData {
189
- const contextUsage = ctx.getContextUsage();
190
- const collectGit =
191
- options.collectGit ?? (options.config ? hasEnabledGitWidgets(options.config) : true);
192
- return {
193
- model: ctx.model?.id,
194
- provider: ctx.model?.provider,
195
- sessionName: pi.getSessionName(),
196
- sessionId: ctx.sessionManager.getSessionId(),
197
- thinkingLevel: ctx.model?.reasoning ? pi.getThinkingLevel() : undefined,
198
- textVerbosity: getTextVerbosity(ctx.model, options.textVerbosity),
199
- git:
200
- options.git ??
201
- (collectGit
202
- ? getGitInfo(pi, ctx.cwd, footerData.getGitBranch(), options.requestRender ?? (() => {}))
203
- : EMPTY_GIT_INFO),
204
- cwd: ctx.cwd,
205
- activeToolCount: pi.getActiveTools().length,
206
- usingSubscription: ctx.model ? ctx.modelRegistry.isUsingOAuth(ctx.model) : false,
207
- contextTokens: contextUsage?.tokens ?? undefined,
208
- contextMaxTokens: contextUsage?.contextWindow,
209
- metrics: collectSessionMetrics(ctx.sessionManager.getBranch()),
210
- turnMetrics: collectTurnMetrics(ctx.sessionManager.getEntries()),
211
- eventWidgets,
212
- };
213
- }
214
-
215
- async function handleArgs(
216
- args: string,
217
- ctx: ExtensionCommandContext,
218
- currentConfig: StatuslineConfig,
219
- setConfig: (config: StatuslineConfig) => Promise<void>,
220
- ): Promise<boolean> {
221
- const [command, value] = args.trim().split(/\s+/, 2);
222
- if (!command) return false;
223
-
224
- if (command === "on" || command === "enable") {
225
- await setConfig({ ...currentConfig, enabled: true });
226
- ctx.ui.notify("pi-footer enabled", "info");
227
- return true;
228
- }
229
- if (command === "off" || command === "disable") {
230
- await setConfig({ ...currentConfig, enabled: false });
231
- ctx.ui.notify("pi-footer disabled", "info");
232
- return true;
233
- }
234
- if (command === "reset") {
235
- await setConfig(cloneConfig(DEFAULT_CONFIG));
236
- ctx.ui.notify("pi-footer reset to defaults", "info");
237
- return true;
238
- }
239
- if (command === "preset" && isPreset(value)) {
240
- await setConfig(configWithPreset(currentConfig, value));
241
- ctx.ui.notify(`pi-footer preset: ${value}`, "info");
242
- return true;
243
- }
244
-
245
- ctx.ui.notify(
246
- "Usage: /footer [on|off|reset|preset compact|default|powerline|powerline-bright|powerline-blocks|powerline-mono|git-heavy|pi-footer|demo|demo-standard]",
247
- "warning",
248
- );
249
- return true;
250
- }
251
-
252
- function getTextVerbosity(
253
- model: ExtensionContext["model"],
254
- liveVerbosity: string | undefined,
255
- ): string | undefined {
256
- // pi exposes text verbosity only through the OpenAI Codex Responses provider.
257
- if (!model || model.api !== "openai-codex-responses") return undefined;
258
- // Prefer the value captured from the real request payload; before the first
259
- // request (or just after a model switch) fall back to the provider default.
260
- return liveVerbosity ?? "low";
261
- }
262
-
263
- function readTextVerbosity(payload: unknown): string | undefined {
264
- if (!isRecord(payload)) return undefined;
265
- const text = payload.text;
266
- if (!isRecord(text)) return undefined;
267
- return typeof text.verbosity === "string" ? text.verbosity : undefined;
268
- }
@@ -1,141 +0,0 @@
1
- import { isRecord, type SessionMetrics, type TurnMetrics } from "./types.js";
2
-
3
- // Intentionally loose structural projection of the `usage` field on a pi session message entry.
4
- // Fields stay `unknown` and are validated at runtime (see isRecord usage below) rather than being
5
- // derived from pi's SDK types — keeps the defensive parsing robust to upstream shape changes.
6
- interface UsageLike {
7
- input?: unknown;
8
- output?: unknown;
9
- cacheRead?: unknown;
10
- cacheWrite?: unknown;
11
- totalTokens?: unknown;
12
- cost?: {
13
- total?: unknown;
14
- };
15
- }
16
-
17
- // Intentionally loose structural projection of a pi session message entry (from
18
- // sessionManager.getBranch()). Same rationale as UsageLike: `unknown` fields, runtime-validated.
19
- interface MessageLike {
20
- role?: unknown;
21
- timestamp?: unknown;
22
- usage?: unknown;
23
- }
24
-
25
- export function collectSessionMetrics(entries: readonly unknown[]): SessionMetrics {
26
- const metrics: SessionMetrics = {
27
- inputTokens: 0,
28
- outputTokens: 0,
29
- cacheReadTokens: 0,
30
- cacheWriteTokens: 0,
31
- totalTokens: 0,
32
- costUsd: 0,
33
- userMessages: 0,
34
- assistantMessages: 0,
35
- toolResults: 0,
36
- firstTimestampMs: undefined,
37
- lastTimestampMs: undefined,
38
- compactions: 0,
39
- };
40
-
41
- for (const entry of entries) {
42
- const message = getMessage(entry);
43
- if (!message) continue;
44
-
45
- const timestampMs = normalizeTimestamp(message.timestamp ?? getEntryTimestamp(entry));
46
- if (timestampMs !== undefined) {
47
- metrics.firstTimestampMs =
48
- metrics.firstTimestampMs === undefined
49
- ? timestampMs
50
- : Math.min(metrics.firstTimestampMs, timestampMs);
51
- metrics.lastTimestampMs =
52
- metrics.lastTimestampMs === undefined
53
- ? timestampMs
54
- : Math.max(metrics.lastTimestampMs, timestampMs);
55
- }
56
-
57
- if (message.role === "user") metrics.userMessages += 1;
58
- if (message.role === "toolResult") metrics.toolResults += 1;
59
- if (message.role === "compactionSummary") metrics.compactions += 1;
60
- if (message.role !== "assistant") continue;
61
-
62
- metrics.assistantMessages += 1;
63
- const usage = getUsage(message.usage);
64
- if (!usage) continue;
65
-
66
- const input = numberOrZero(usage.input);
67
- const output = numberOrZero(usage.output);
68
- const cacheRead = numberOrZero(usage.cacheRead);
69
- const cacheWrite = numberOrZero(usage.cacheWrite);
70
-
71
- metrics.inputTokens += input;
72
- metrics.outputTokens += output;
73
- metrics.cacheReadTokens += cacheRead;
74
- metrics.cacheWriteTokens += cacheWrite;
75
- metrics.totalTokens +=
76
- numberOrZero(usage.totalTokens) || input + output + cacheRead + cacheWrite;
77
- metrics.costUsd += numberOrZero(usage.cost?.total);
78
- }
79
-
80
- return metrics;
81
- }
82
-
83
- export function collectTurnMetrics(entries: readonly unknown[]): TurnMetrics {
84
- const metrics: TurnMetrics = {
85
- inputTokens: 0,
86
- outputTokens: 0,
87
- cacheReadTokens: 0,
88
- cacheWriteTokens: 0,
89
- totalTokens: 0,
90
- costUsd: 0,
91
- };
92
-
93
- for (let index = entries.length - 1; index >= 0; index -= 1) {
94
- const message = getMessage(entries[index]);
95
- if (message?.role !== "assistant") continue;
96
-
97
- const usage = getUsage(message.usage);
98
- if (!usage) continue;
99
-
100
- const input = numberOrZero(usage.input);
101
- const output = numberOrZero(usage.output);
102
- const cacheRead = numberOrZero(usage.cacheRead);
103
- const cacheWrite = numberOrZero(usage.cacheWrite);
104
-
105
- metrics.inputTokens = input;
106
- metrics.outputTokens = output;
107
- metrics.cacheReadTokens = cacheRead;
108
- metrics.cacheWriteTokens = cacheWrite;
109
- metrics.totalTokens =
110
- numberOrZero(usage.totalTokens) || input + output + cacheRead + cacheWrite;
111
- metrics.costUsd = numberOrZero(usage.cost?.total);
112
- return metrics;
113
- }
114
-
115
- return metrics;
116
- }
117
-
118
- function getMessage(entry: unknown): MessageLike | undefined {
119
- if (!isRecord(entry)) return undefined;
120
- const message = entry.message;
121
- return isRecord(message) ? message : undefined;
122
- }
123
-
124
- function getEntryTimestamp(entry: unknown): unknown {
125
- return isRecord(entry) ? entry.timestamp : undefined;
126
- }
127
-
128
- function getUsage(value: unknown): UsageLike | undefined {
129
- return isRecord(value) ? (value as UsageLike) : undefined;
130
- }
131
-
132
- function normalizeTimestamp(value: unknown): number | undefined {
133
- if (typeof value === "number" && Number.isFinite(value)) return value;
134
- if (typeof value !== "string") return undefined;
135
- const parsed = Date.parse(value);
136
- return Number.isFinite(parsed) ? parsed : undefined;
137
- }
138
-
139
- function numberOrZero(value: unknown): number {
140
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
141
- }