@selesai/code 0.9.15 → 0.10.1

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 (269) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +2 -2
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +2 -4
  6. package/dist/extensions/pi-zentui/.editorconfig +12 -0
  7. package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
  8. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  9. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
  10. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
  11. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  12. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
  13. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
  14. package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
  15. package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
  16. package/dist/extensions/pi-zentui/.github/release.yml +18 -0
  17. package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
  18. package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
  19. package/dist/extensions/pi-zentui/AGENTS.md +107 -0
  20. package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
  21. package/dist/extensions/pi-zentui/LICENSE +21 -0
  22. package/dist/extensions/pi-zentui/README.md +188 -0
  23. package/dist/extensions/pi-zentui/SECURITY.md +18 -0
  24. package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
  25. package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
  26. package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
  27. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
  28. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
  29. package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
  30. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
  31. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
  32. package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
  33. package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
  34. package/dist/extensions/pi-zentui/biome.json +31 -0
  35. package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
  36. package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
  37. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
  38. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
  39. package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
  40. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
  41. package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
  42. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
  43. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
  44. package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
  45. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
  46. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
  47. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
  48. package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
  49. package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
  50. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
  51. package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
  52. package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
  53. package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
  54. package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
  55. package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
  56. package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
  57. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
  58. package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
  59. package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
  60. package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
  61. package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
  62. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
  63. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
  64. package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
  65. package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
  66. package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
  67. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
  68. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
  69. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
  70. package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
  71. package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
  72. package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
  73. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
  74. package/dist/extensions/pi-zentui/package-lock.json +5311 -0
  75. package/dist/extensions/pi-zentui/package.json +60 -0
  76. package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
  77. package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
  78. package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
  79. package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
  80. package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
  81. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
  82. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
  83. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
  84. package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
  85. package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
  86. package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
  87. package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
  88. package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
  89. package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
  90. package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
  91. package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
  92. package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
  93. package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
  94. package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
  95. package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
  96. package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
  97. package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
  98. package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
  99. package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
  100. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
  101. package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
  102. package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
  103. package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
  104. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
  105. package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
  106. package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
  107. package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
  108. package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
  109. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
  110. package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
  111. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
  112. package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
  113. package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
  114. package/dist/extensions/pi-zentui/tsconfig.json +16 -0
  115. package/dist/extensions/tps.test.ts +38 -0
  116. package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
  117. package/dist/modes/interactive/components/settings-selector.d.ts +1 -1
  118. package/dist/modes/interactive/interactive-mode.js +1 -1
  119. package/dist/skills/design-references/SKILL.md +31 -0
  120. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  121. package/dist/themes/adventure.json +84 -0
  122. package/dist/themes/adwaita-dark.json +84 -0
  123. package/dist/themes/arcoiris.json +84 -0
  124. package/dist/themes/arthur.json +84 -0
  125. package/dist/themes/atom.json +84 -0
  126. package/dist/themes/aura.json +84 -0
  127. package/dist/themes/black-metal-bathory.json +84 -0
  128. package/dist/themes/black-metal-burzum.json +84 -0
  129. package/dist/themes/black-metal-khold.json +84 -0
  130. package/dist/themes/box.json +84 -0
  131. package/dist/themes/brogrammer.json +84 -0
  132. package/dist/themes/carbonfox.json +84 -0
  133. package/dist/themes/catppuccin-mocha.json +84 -0
  134. package/dist/themes/citruszest.json +84 -0
  135. package/dist/themes/cursor-dark.json +84 -0
  136. package/dist/themes/cutie-pro.json +84 -0
  137. package/dist/themes/dark-modern.json +84 -0
  138. package/dist/themes/dark-pastel.json +84 -0
  139. package/dist/themes/dimmed-monokai.json +84 -0
  140. package/dist/themes/doom-peacock.json +84 -0
  141. package/dist/themes/dracula-plus.json +84 -0
  142. package/dist/themes/dracula.json +80 -0
  143. package/dist/themes/earthsong.json +84 -0
  144. package/dist/themes/everforest-dark-hard.json +84 -0
  145. package/dist/themes/everforest-dark.json +80 -0
  146. package/dist/themes/fahrenheit.json +84 -0
  147. package/dist/themes/flatland.json +84 -0
  148. package/dist/themes/flexoki-dark.json +84 -0
  149. package/dist/themes/front-end-delight.json +84 -0
  150. package/dist/themes/fun-forrest.json +84 -0
  151. package/dist/themes/galizur.json +84 -0
  152. package/dist/themes/github-dark-colorblind.json +84 -0
  153. package/dist/themes/github-dark-high-contrast.json +84 -0
  154. package/dist/themes/glacier.json +84 -0
  155. package/dist/themes/gruber-darker.json +84 -0
  156. package/dist/themes/gruvbox-dark-hard.json +84 -0
  157. package/dist/themes/gruvbox-dark.json +84 -0
  158. package/dist/themes/gruvbox-material.json +84 -0
  159. package/dist/themes/guezwhoz.json +84 -0
  160. package/dist/themes/hacktober.json +84 -0
  161. package/dist/themes/hardcore.json +84 -0
  162. package/dist/themes/havn-skumring.json +84 -0
  163. package/dist/themes/ic-orange-ppl.json +84 -0
  164. package/dist/themes/iterm2-smoooooth.json +84 -0
  165. package/dist/themes/iterm2-tango-dark.json +84 -0
  166. package/dist/themes/japanesque.json +84 -0
  167. package/dist/themes/jellybeans.json +84 -0
  168. package/dist/themes/kanagawa-wave.json +84 -0
  169. package/dist/themes/kurokula.json +84 -0
  170. package/dist/themes/later-this-evening.json +84 -0
  171. package/dist/themes/lovelace.json +84 -0
  172. package/dist/themes/material-darker.json +84 -0
  173. package/dist/themes/matte-black.json +84 -0
  174. package/dist/themes/mellow.json +84 -0
  175. package/dist/themes/miasma.json +84 -0
  176. package/dist/themes/nord.json +80 -0
  177. package/dist/themes/nvim-dark.json +84 -0
  178. package/dist/themes/one-dark.json +80 -0
  179. package/dist/themes/popping-and-locking.json +84 -0
  180. package/dist/themes/rose-pine.json +80 -0
  181. package/dist/themes/sea-shells.json +84 -0
  182. package/dist/themes/sleepy-hollow.json +84 -0
  183. package/dist/themes/smyck.json +84 -0
  184. package/dist/themes/solarized-dark.json +80 -0
  185. package/dist/themes/tokyo-night.json +80 -0
  186. package/dist/themes/tomorrow-night-bright.json +84 -0
  187. package/dist/themes/tomorrow-night-burns.json +84 -0
  188. package/dist/themes/tomorrow-night.json +84 -0
  189. package/dist/themes/twilight.json +84 -0
  190. package/dist/themes/vague.json +84 -0
  191. package/dist/themes/vesper.json +84 -0
  192. package/dist/themes/xcode-dark-hc.json +84 -0
  193. package/docs/custom-provider.md +0 -1
  194. package/package.json +3 -3
  195. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  196. package/dist/extensions/auto-model/classifier.ts +0 -23
  197. package/dist/extensions/auto-model/config.ts +0 -51
  198. package/dist/extensions/auto-model/index.ts +0 -22
  199. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  200. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  201. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  202. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  203. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  204. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  205. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  206. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  207. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  208. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  209. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  210. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  211. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  212. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  213. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  214. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  215. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  216. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  217. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  218. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  219. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  220. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  221. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  222. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  223. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  224. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  225. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  226. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  227. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  228. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  229. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  230. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  231. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  232. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  233. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  234. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  235. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  236. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  237. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  238. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  239. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  240. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  241. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  242. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  243. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  244. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  245. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  246. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  247. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  248. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  249. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  250. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  251. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  252. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  253. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  254. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  255. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  256. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  257. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  258. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  259. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  260. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  261. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  262. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  263. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  264. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  265. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  266. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  267. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  268. package/dist/extensions/preview-tools-disabled.ts +0 -11
  269. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,865 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import {
4
+ __resetUsageTotalsCacheForTests,
5
+ __usageTotalsAggregationPassCount,
6
+ buildCacheReadLabel,
7
+ buildCacheWriteLabel,
8
+ buildContextDisplayLabel,
9
+ buildContextGauge,
10
+ buildCostLabel,
11
+ buildSessionDurationLabel,
12
+ buildTokenLabel,
13
+ contextColorTier,
14
+ formatCount,
15
+ formatCwdLabel,
16
+ formatGitBranchText,
17
+ formatGitCommitSegment,
18
+ formatGitMetricsSegment,
19
+ formatOsLabel,
20
+ formatPackageVersionSegment,
21
+ getUsageTotals,
22
+ invalidateUsageTotalsCache,
23
+ } from "../extensions/zentui/format";
24
+ import {
25
+ ASCII_DEFAULT_ICONS,
26
+ NERD_DEFAULT_ICONS,
27
+ OS_PLATFORM_ICONS_ASCII,
28
+ OS_PLATFORM_ICONS_NERD,
29
+ } from "../extensions/zentui/icons";
30
+
31
+ const cacheHitIcon = "󰆼";
32
+
33
+ type TestUsage = {
34
+ input?: unknown;
35
+ output?: unknown;
36
+ cacheRead?: unknown;
37
+ cacheWrite?: unknown;
38
+ cost?: { total?: unknown } | null;
39
+ };
40
+
41
+ function makeUsage(
42
+ input: unknown,
43
+ output: unknown,
44
+ cost: unknown,
45
+ cacheRead: unknown = 0,
46
+ cacheWrite: unknown = 0,
47
+ ): TestUsage {
48
+ return { input, output, cacheRead, cacheWrite, cost: { total: cost } };
49
+ }
50
+
51
+ function makeAssistantEntry(
52
+ input: number,
53
+ output: number,
54
+ cost: number,
55
+ cacheRead = 0,
56
+ cacheWrite = 0,
57
+ ) {
58
+ return makeMessageUsageEntry("assistant", makeUsage(input, output, cost, cacheRead, cacheWrite));
59
+ }
60
+
61
+ function makeMessageUsageEntry(role: string, usage: TestUsage | undefined) {
62
+ return { type: "message", message: { role, usage } };
63
+ }
64
+
65
+ function makeSummaryUsageEntry(
66
+ type: "compaction" | "branch_summary",
67
+ usage: TestUsage | undefined,
68
+ ) {
69
+ return { type, usage };
70
+ }
71
+
72
+ function makeSessionContext(entries: unknown[], branch = entries) {
73
+ return {
74
+ sessionManager: {
75
+ getBranch: () => branch,
76
+ getEntries: () => entries,
77
+ },
78
+ };
79
+ }
80
+
81
+ describe("usage formatting", () => {
82
+ it("formats counts at compact decimal boundaries", () => {
83
+ expect(formatCount(0)).toBe("0");
84
+ expect(formatCount(999)).toBe("999");
85
+ expect(formatCount(1_000)).toBe("1.0k");
86
+ expect(formatCount(1_100)).toBe("1.1k");
87
+ expect(formatCount(9_999)).toBe("10.0k");
88
+ expect(formatCount(10_000)).toBe("10k");
89
+ expect(formatCount(27_000)).toBe("27k");
90
+ expect(formatCount(123_456)).toBe("123k");
91
+ expect(formatCount(999_999)).toBe("1000k");
92
+ expect(formatCount(1_000_000)).toBe("1.0M");
93
+ expect(formatCount(3_100_000)).toBe("3.1M");
94
+ expect(formatCount(44_000_000)).toBe("44M");
95
+ expect(formatCount(Number.MAX_SAFE_INTEGER)).toBe("9007199255M");
96
+ });
97
+
98
+ it("uses all session entries for totals instead of only the active branch", () => {
99
+ const branchEntry = makeAssistantEntry(100, 10, 1);
100
+ const freshTreeEntry = makeAssistantEntry(2_000_000, 100_000, 25);
101
+ const ctx = makeSessionContext([branchEntry, freshTreeEntry], [branchEntry]);
102
+
103
+ const totals = getUsageTotals(ctx as never);
104
+
105
+ expect(totals).toEqual({
106
+ input: 2_000_100,
107
+ output: 100_010,
108
+ cacheRead: 0,
109
+ cacheWrite: 0,
110
+ latestCacheHitRate: 0,
111
+ cost: 26,
112
+ });
113
+ expect(buildTokenLabel(totals, cacheHitIcon)).toBe("↑2.0M ↓100k");
114
+ expect(buildCostLabel(totals)).toBe("$26.000");
115
+ });
116
+
117
+ it("formats cache atoms independently and omits zero totals", () => {
118
+ expect(buildCacheReadLabel(1_200)).toBe("R1.2k");
119
+ expect(buildCacheWriteLabel(300)).toBe("W300");
120
+ expect(buildCacheReadLabel(0)).toBe("");
121
+ expect(buildCacheWriteLabel(0)).toBe("");
122
+ });
123
+
124
+ it("keeps token and cost labels compact", () => {
125
+ const totals = { input: 3_100_000, output: 197_000, cacheRead: 0, cacheWrite: 0, cost: 41.957 };
126
+ const tokenLabel = buildTokenLabel(totals, cacheHitIcon);
127
+
128
+ expect(tokenLabel).toBe("↑3.1M ↓197k");
129
+ expect(tokenLabel).not.toContain("R");
130
+ expect(tokenLabel).not.toContain("W");
131
+ expect(buildCostLabel(totals)).toBe("$41.957");
132
+ });
133
+
134
+ it("shows latest prompt cache hit rate icon without R/W totals", () => {
135
+ const totals = {
136
+ input: 100,
137
+ output: 10,
138
+ cacheRead: 800,
139
+ cacheWrite: 100,
140
+ latestCacheHitRate: 80,
141
+ cost: 1,
142
+ };
143
+ const tokenLabel = buildTokenLabel(totals, cacheHitIcon);
144
+
145
+ expect(tokenLabel).toBe("↑100 ↓10 󰆼 80.0%");
146
+ expect(tokenLabel).not.toContain("CH");
147
+ expect(tokenLabel).not.toContain("R");
148
+ expect(tokenLabel).not.toContain("W");
149
+ });
150
+
151
+ it("uses custom and empty cache hit icons", () => {
152
+ const totals = {
153
+ input: 100,
154
+ output: 10,
155
+ cacheRead: 800,
156
+ cacheWrite: 100,
157
+ latestCacheHitRate: 80,
158
+ cost: 1,
159
+ };
160
+
161
+ expect(buildTokenLabel(totals, "CH")).toBe("↑100 ↓10 CH 80.0%");
162
+ expect(buildTokenLabel(totals, "")).toBe("↑100 ↓10 80.0%");
163
+ });
164
+
165
+ it("uses the latest assistant message for prompt cache hit rate", () => {
166
+ const firstEntry = makeAssistantEntry(100, 10, 1, 900, 0);
167
+ const latestEntry = makeAssistantEntry(200, 20, 2, 300, 500);
168
+ const ctx = makeSessionContext([firstEntry, latestEntry], [firstEntry]);
169
+
170
+ const totals = getUsageTotals(ctx as never);
171
+
172
+ expect(totals.cacheRead).toBe(1200);
173
+ expect(totals.cacheWrite).toBe(500);
174
+ expect(totals.latestCacheHitRate).toBe(30);
175
+ expect(buildTokenLabel(totals, cacheHitIcon)).toBe("↑300 ↓30 󰆼 30.0%");
176
+ });
177
+
178
+ it("aggregates assistant, tool-result LLM, compaction, and branch-summary usage", () => {
179
+ const entries = [
180
+ makeMessageUsageEntry("assistant", makeUsage(10, 2, 0.1, 30, 8)),
181
+ makeMessageUsageEntry("toolResult", makeUsage(20, 4, 0.2, 40, 9)),
182
+ makeSummaryUsageEntry("compaction", makeUsage(30, 6, 0.3, 50, 10)),
183
+ makeSummaryUsageEntry("branch_summary", makeUsage(40, 8, 0.4, 60, 11)),
184
+ ];
185
+
186
+ const totals = getUsageTotals(makeSessionContext(entries) as never);
187
+
188
+ expect(totals).toEqual({
189
+ input: 100,
190
+ output: 20,
191
+ cacheRead: 180,
192
+ cacheWrite: 38,
193
+ latestCacheHitRate: 62.5,
194
+ cost: 1,
195
+ });
196
+ expect(buildTokenLabel(totals, cacheHitIcon)).toBe("↑100 ↓20 󰆼 62.5%");
197
+ expect(buildCostLabel(totals)).toBe("$1.000");
198
+ });
199
+
200
+ it("selects only one usage location and ignores unsupported entry shapes", () => {
201
+ const hybridMessage = {
202
+ ...makeMessageUsageEntry("assistant", makeUsage(1, 2, 0.1)),
203
+ usage: makeUsage(100, 200, 10),
204
+ };
205
+ const hybridCompaction = {
206
+ ...makeSummaryUsageEntry("compaction", makeUsage(3, 4, 0.2)),
207
+ message: { role: "assistant", usage: makeUsage(300, 400, 20) },
208
+ };
209
+ const entries = [
210
+ hybridMessage,
211
+ hybridCompaction,
212
+ makeMessageUsageEntry("user", makeUsage(1_000, 1_000, 30)),
213
+ { type: "custom", usage: makeUsage(2_000, 2_000, 40) },
214
+ ];
215
+
216
+ expect(getUsageTotals(makeSessionContext(entries) as never)).toEqual({
217
+ input: 4,
218
+ output: 6,
219
+ cacheRead: 0,
220
+ cacheWrite: 0,
221
+ latestCacheHitRate: 0,
222
+ cost: 0.30000000000000004,
223
+ });
224
+ });
225
+
226
+ it("normalizes missing, zero, and malformed optional numbers independently", () => {
227
+ const entries = [
228
+ makeMessageUsageEntry("assistant", {
229
+ input: "10",
230
+ output: -1,
231
+ cacheRead: Number.NaN,
232
+ cacheWrite: Number.POSITIVE_INFINITY,
233
+ cost: { total: Number.NEGATIVE_INFINITY },
234
+ }),
235
+ makeMessageUsageEntry("toolResult", {
236
+ input: 5,
237
+ output: 0,
238
+ cacheRead: 7,
239
+ cacheWrite: -2,
240
+ cost: null,
241
+ }),
242
+ makeSummaryUsageEntry("compaction", {}),
243
+ makeSummaryUsageEntry("branch_summary", undefined),
244
+ ];
245
+
246
+ const totals = getUsageTotals(makeSessionContext(entries) as never);
247
+
248
+ expect(totals).toEqual({
249
+ input: 5,
250
+ output: 0,
251
+ cacheRead: 7,
252
+ cacheWrite: 0,
253
+ latestCacheHitRate: undefined,
254
+ cost: 0,
255
+ });
256
+ for (const value of [
257
+ totals.input,
258
+ totals.output,
259
+ totals.cacheRead,
260
+ totals.cacheWrite,
261
+ totals.cost,
262
+ ]) {
263
+ expect(Number.isFinite(value)).toBe(true);
264
+ expect(value).toBeGreaterThanOrEqual(0);
265
+ }
266
+ });
267
+
268
+ it("adds distinct duplicate-looking persisted entries without deduplication", () => {
269
+ const entries = [
270
+ makeAssistantEntry(10, 2, 0.1),
271
+ makeAssistantEntry(10, 2, 0.1),
272
+ makeMessageUsageEntry("toolResult", makeUsage(10, 2, 0.1)),
273
+ ];
274
+
275
+ expect(getUsageTotals(makeSessionContext(entries) as never)).toMatchObject({
276
+ input: 30,
277
+ output: 6,
278
+ cost: 0.30000000000000004,
279
+ });
280
+ });
281
+
282
+ it("saturates huge aggregates and calculates huge prompt rates without overflow", () => {
283
+ const entries = [
284
+ makeAssistantEntry(
285
+ Number.MAX_VALUE,
286
+ Number.MAX_VALUE,
287
+ Number.MAX_VALUE,
288
+ Number.MAX_VALUE,
289
+ Number.MAX_VALUE,
290
+ ),
291
+ makeAssistantEntry(
292
+ Number.MAX_VALUE,
293
+ Number.MAX_VALUE,
294
+ Number.MAX_VALUE,
295
+ Number.MAX_VALUE,
296
+ Number.MAX_VALUE,
297
+ ),
298
+ ];
299
+
300
+ const totals = getUsageTotals(makeSessionContext(entries) as never);
301
+
302
+ expect(totals).toMatchObject({
303
+ input: Number.MAX_VALUE,
304
+ output: Number.MAX_VALUE,
305
+ cacheRead: Number.MAX_VALUE,
306
+ cacheWrite: Number.MAX_VALUE,
307
+ cost: Number.MAX_VALUE,
308
+ });
309
+ expect(totals.latestCacheHitRate).toBeCloseTo(100 / 3);
310
+ expect(Number.isFinite(totals.latestCacheHitRate ?? Number.NaN)).toBe(true);
311
+ expect(buildTokenLabel(totals, cacheHitIcon)).toBe(
312
+ `↑1.797693134862316e+302M ↓1.797693134862316e+302M ${cacheHitIcon} 33.3%`,
313
+ );
314
+ expect(buildCostLabel(totals)).toBe(`$${Number.MAX_VALUE.toFixed(3)}`);
315
+ expect(buildTokenLabel(totals, cacheHitIcon)).not.toMatch(/Infinity|NaN/);
316
+ expect(buildCostLabel(totals)).not.toMatch(/Infinity|NaN/);
317
+ });
318
+
319
+ it("falls back to the active branch when getEntries is unavailable", () => {
320
+ const ctx = { sessionManager: { getBranch: () => [makeAssistantEntry(12, 3, 0.4)] } };
321
+
322
+ expect(getUsageTotals(ctx as never)).toMatchObject({ input: 12, output: 3, cost: 0.4 });
323
+ });
324
+ });
325
+
326
+ describe("buildSessionDurationLabel", () => {
327
+ const FIXED_NOW = 1_700_000_000_000;
328
+ beforeEach(() => {
329
+ vi.useFakeTimers();
330
+ vi.setSystemTime(FIXED_NOW);
331
+ });
332
+ afterEach(() => {
333
+ vi.useRealTimers();
334
+ });
335
+
336
+ it("formats seconds only", () => {
337
+ expect(buildSessionDurationLabel(FIXED_NOW - 45_000)).toBe("45s");
338
+ });
339
+
340
+ it("formats minutes and seconds", () => {
341
+ expect(buildSessionDurationLabel(FIXED_NOW - (2 * 60 + 13) * 1000)).toBe("2m 13s");
342
+ });
343
+
344
+ it("formats hours and minutes", () => {
345
+ expect(buildSessionDurationLabel(FIXED_NOW - (1 * 3600 + 5 * 60) * 1000)).toBe("1h 5m");
346
+ });
347
+
348
+ it("clamps zero and negative elapsed to 0s", () => {
349
+ expect(buildSessionDurationLabel(FIXED_NOW)).toBe("0s");
350
+ expect(buildSessionDurationLabel(FIXED_NOW + 10_000)).toBe("0s");
351
+ });
352
+ });
353
+
354
+ describe("formatOsLabel", () => {
355
+ it("honors a custom icons.os over platform defaults", () => {
356
+ expect(formatOsLabel("X", "auto", "darwin")).toBe("X");
357
+ expect(formatOsLabel("X", "ascii", "linux")).toBe("X");
358
+ });
359
+
360
+ it("maps platform icons when using the mode default os glyph", () => {
361
+ expect(formatOsLabel(NERD_DEFAULT_ICONS.os, "auto", "linux")).toBe(
362
+ OS_PLATFORM_ICONS_NERD.linux,
363
+ );
364
+ expect(formatOsLabel(ASCII_DEFAULT_ICONS.os, "ascii", "darwin")).toBe(
365
+ OS_PLATFORM_ICONS_ASCII.darwin,
366
+ );
367
+ });
368
+ });
369
+
370
+ describe("formatCwdLabel", () => {
371
+ const home = "/Users/me";
372
+
373
+ it("defaults to basename and preserves current behavior", () => {
374
+ expect(formatCwdLabel("/Users/me/Projects/zentui", "")).toBe("zentui");
375
+ expect(formatCwdLabel("/Users/me/Projects/zentui/", "")).toBe("zentui");
376
+ expect(formatCwdLabel("/", "")).toBe("/");
377
+ expect(formatCwdLabel("C:\\Users\\me\\zentui", "")).toBe("zentui");
378
+ expect(formatCwdLabel("/tmp/project", "󰝰")).toBe("󰝰 project");
379
+ });
380
+
381
+ it("renders full paths with home contracted to ~", () => {
382
+ expect(formatCwdLabel("/Users/me/Projects/zentui", "", { mode: "full", home })).toBe(
383
+ "~/Projects/zentui",
384
+ );
385
+ expect(formatCwdLabel("/Users/me", "", { mode: "full", home })).toBe("~");
386
+ expect(formatCwdLabel("/tmp/project", "", { mode: "full", home })).toBe("/tmp/project");
387
+ expect(formatCwdLabel("/", "", { mode: "full", home })).toBe("/");
388
+ expect(
389
+ formatCwdLabel("C:\\Users\\me\\Projects\\zentui", "", {
390
+ mode: "full",
391
+ home: "C:\\Users\\me",
392
+ }),
393
+ ).toBe("~/Projects/zentui");
394
+ // Prefix-safe: /Users/me2 must not match home /Users/me
395
+ expect(formatCwdLabel("/Users/me2/Projects", "", { mode: "full", home })).toBe(
396
+ "/Users/me2/Projects",
397
+ );
398
+ });
399
+
400
+ it("truncates full paths to trailing directory depth (Starship-style)", () => {
401
+ expect(
402
+ formatCwdLabel("/Users/me/Projects/foo/bar", "", {
403
+ mode: "full",
404
+ home,
405
+ depth: 2,
406
+ }),
407
+ ).toBe("…/foo/bar");
408
+ expect(
409
+ formatCwdLabel("/var/log/nginx/access", "", {
410
+ mode: "full",
411
+ home,
412
+ depth: 2,
413
+ }),
414
+ ).toBe("…/nginx/access");
415
+ expect(
416
+ formatCwdLabel("C:\\a\\b\\c\\d", "", {
417
+ mode: "full",
418
+ home,
419
+ depth: 2,
420
+ }),
421
+ ).toBe("…/c/d");
422
+ expect(
423
+ formatCwdLabel("/Users/me/Projects/zentui", "", {
424
+ mode: "full",
425
+ home,
426
+ depth: 5,
427
+ }),
428
+ ).toBe("~/Projects/zentui");
429
+ expect(
430
+ formatCwdLabel("/Users/me/Projects/zentui", "", {
431
+ mode: "full",
432
+ home,
433
+ depth: 1,
434
+ }),
435
+ ).toBe("…/zentui");
436
+ expect(formatCwdLabel("/Users/me", "", { mode: "full", home, depth: 2 })).toBe("~");
437
+ expect(formatCwdLabel("/", "", { mode: "full", home, depth: 2 })).toBe("/");
438
+ expect(formatCwdLabel("//", "", { mode: "full", home, depth: 2 })).toBe("/");
439
+ expect(formatCwdLabel("//", "")).toBe("/");
440
+ expect(
441
+ formatCwdLabel("/Users/me/Projects/zentui", "", {
442
+ mode: "full",
443
+ home,
444
+ depth: 0,
445
+ }),
446
+ ).toBe("~/Projects/zentui");
447
+ // depth is ignored for basename
448
+ expect(
449
+ formatCwdLabel("/Users/me/Projects/zentui", "", {
450
+ mode: "basename",
451
+ depth: 2,
452
+ }),
453
+ ).toBe("zentui");
454
+ expect(
455
+ formatCwdLabel("/Users/me/Projects/zentui", "󰝰", {
456
+ mode: "full",
457
+ home,
458
+ depth: 1,
459
+ }),
460
+ ).toBe("󰝰 …/zentui");
461
+ });
462
+
463
+ it("renders safe repository-relative paths and falls back to unlimited full paths", () => {
464
+ const repositoryRoot = "/Users/me/Projects/zentui";
465
+ expect(
466
+ formatCwdLabel(`${repositoryRoot}/extensions/zentui`, "", {
467
+ mode: "repository",
468
+ repositoryRoot,
469
+ home,
470
+ depth: 0,
471
+ }),
472
+ ).toBe("extensions/zentui");
473
+ expect(
474
+ formatCwdLabel(repositoryRoot, "", { mode: "repository", repositoryRoot, home, depth: 2 }),
475
+ ).toBe(".");
476
+ expect(
477
+ formatCwdLabel(`${repositoryRoot}/packages/core/src`, "", {
478
+ mode: "repository",
479
+ repositoryRoot,
480
+ home,
481
+ depth: 2,
482
+ }),
483
+ ).toBe("…/core/src");
484
+ expect(
485
+ formatCwdLabel("C:\\repo\\extensions\\zentui\\", "", {
486
+ mode: "repository",
487
+ repositoryRoot: "C:\\repo\\",
488
+ depth: 0,
489
+ }),
490
+ ).toBe("extensions/zentui");
491
+ expect(
492
+ formatCwdLabel("/Users/me/Projects/other/src", "", {
493
+ mode: "repository",
494
+ repositoryRoot,
495
+ home,
496
+ depth: 1,
497
+ }),
498
+ ).toBe("~/Projects/other/src");
499
+ expect(
500
+ formatCwdLabel(`${repositoryRoot}/extensions`, "", {
501
+ mode: "repository",
502
+ home,
503
+ depth: 1,
504
+ }),
505
+ ).toBe("~/Projects/zentui/extensions");
506
+ });
507
+ });
508
+
509
+ describe("formatGitBranchText", () => {
510
+ it("preserves full, under-limit, and exactly-at-limit branch names", () => {
511
+ expect(formatGitBranchText("feature/long-name", "full")).toBe("feature/long-name");
512
+ expect(formatGitBranchText("main", 10)).toBe("main");
513
+ expect(formatGitBranchText("1234567890", 10)).toBe("1234567890");
514
+ });
515
+
516
+ it("includes the ellipsis inside the configured visible width", () => {
517
+ expect(formatGitBranchText("feature", 4)).toBe("fea…");
518
+ expect(formatGitBranchText("feature", 1)).toBe("…");
519
+ const unicode = formatGitBranchText("你好世界", 5);
520
+ expect(unicode).toBe("你好…");
521
+ expect(visibleWidth(unicode)).toBe(5);
522
+ });
523
+ });
524
+
525
+ describe("context helpers", () => {
526
+ it("classifies context color tiers from thresholds", () => {
527
+ expect(contextColorTier(10, { warning: 50, error: 80 })).toBe("normal");
528
+ expect(contextColorTier(50, { warning: 50, error: 80 })).toBe("warning");
529
+ expect(contextColorTier(80, { warning: 50, error: 80 })).toBe("error");
530
+ expect(contextColorTier(null)).toBe("normal");
531
+ });
532
+
533
+ it("builds stable-width gauges and style labels", () => {
534
+ expect(buildContextGauge(0, 10)).toHaveLength(10);
535
+ expect(buildContextGauge(100, 10)).toHaveLength(10);
536
+ expect(buildContextGauge(50, 10, true)).toBe("#####-----");
537
+ expect(buildContextDisplayLabel({ percent: 42, contextWindow: 128_000, style: "text" })).toBe(
538
+ "42.0%/128k",
539
+ );
540
+ expect(
541
+ buildContextDisplayLabel({ percent: 42, contextWindow: 128_000, style: "gauge" }),
542
+ ).toMatch(/^\[.{10}\]$/);
543
+ expect(
544
+ buildContextDisplayLabel({
545
+ percent: 42,
546
+ contextWindow: 128_000,
547
+ style: "text+gauge",
548
+ }),
549
+ ).toMatch(/^\[.{10}\] 42\.0%\/128k$/);
550
+ expect(buildContextDisplayLabel({ percent: null, contextWindow: 128_000 })).toBe("?/128k");
551
+ expect(buildContextDisplayLabel({ percent: Number.NaN, contextWindow: 128_000 })).toBe(
552
+ "?/128k",
553
+ );
554
+ expect(buildContextDisplayLabel({ percent: null, contextWindow: undefined })).toBe("--");
555
+ });
556
+ });
557
+
558
+ describe("getUsageTotals caching", () => {
559
+ it("reuses totals without another aggregation pass and recomputes after invalidate", () => {
560
+ __resetUsageTotalsCacheForTests();
561
+
562
+ const entries = [makeAssistantEntry(10, 1, 0.1)];
563
+ const ctx = makeSessionContext(entries) as never;
564
+
565
+ const first = getUsageTotals(ctx);
566
+ const second = getUsageTotals(ctx);
567
+ expect(second).toBe(first);
568
+ expect(__usageTotalsAggregationPassCount()).toBe(1);
569
+
570
+ entries.push(makeAssistantEntry(20, 2, 0.2));
571
+ const third = getUsageTotals(ctx);
572
+ expect(third.input).toBe(30);
573
+ expect(__usageTotalsAggregationPassCount()).toBe(2);
574
+
575
+ invalidateUsageTotalsCache();
576
+ const fourth = getUsageTotals(ctx);
577
+ expect(fourth).toEqual(third);
578
+ expect(fourth).not.toBe(third);
579
+ expect(__usageTotalsAggregationPassCount()).toBe(3);
580
+ });
581
+
582
+ it("recomputes when relevant middle usage changes with unchanged endpoints", () => {
583
+ __resetUsageTotalsCacheForTests();
584
+ const entries = [
585
+ makeAssistantEntry(1, 1, 0.1),
586
+ makeMessageUsageEntry("toolResult", makeUsage(2, 2, 0.2)),
587
+ makeSummaryUsageEntry("compaction", makeUsage(3, 3, 0.3)),
588
+ ];
589
+ const ctx = makeSessionContext(entries) as never;
590
+ expect(getUsageTotals(ctx).input).toBe(6);
591
+
592
+ entries[1] = makeMessageUsageEntry("toolResult", makeUsage(20, 2, 0.2));
593
+
594
+ expect(getUsageTotals(ctx).input).toBe(24);
595
+ expect(__usageTotalsAggregationPassCount()).toBe(2);
596
+ });
597
+
598
+ it.each([
599
+ ["assistant", () => makeMessageUsageEntry("assistant", makeUsage(1, 1, 0.1))],
600
+ ["tool result", () => makeMessageUsageEntry("toolResult", makeUsage(1, 1, 0.1))],
601
+ ["compaction", () => makeSummaryUsageEntry("compaction", makeUsage(1, 1, 0.1))],
602
+ ["branch summary", () => makeSummaryUsageEntry("branch_summary", makeUsage(1, 1, 0.1))],
603
+ ])("invalidates for appended and changed %s usage", (_label, makeEntry) => {
604
+ __resetUsageTotalsCacheForTests();
605
+ const entries: unknown[] = [makeAssistantEntry(10, 1, 1)];
606
+ const ctx = makeSessionContext(entries) as never;
607
+ const initial = getUsageTotals(ctx);
608
+
609
+ entries.push(makeEntry());
610
+ const appended = getUsageTotals(ctx);
611
+ expect(appended).not.toBe(initial);
612
+ expect(appended.input).toBe(11);
613
+
614
+ const entry = entries[1] as { message?: { usage?: TestUsage }; usage?: TestUsage };
615
+ const usage = entry.message?.usage ?? entry.usage;
616
+ if (usage) usage.input = 5;
617
+ const changed = getUsageTotals(ctx);
618
+ expect(changed).not.toBe(appended);
619
+ expect(changed.input).toBe(15);
620
+ expect(__usageTotalsAggregationPassCount()).toBe(3);
621
+ });
622
+ });
623
+
624
+ describe("formatGitMetricsSegment", () => {
625
+ const makeTheme = (): { fg: (color: string, text: string) => string } => ({
626
+ fg: (_color, text) => text,
627
+ });
628
+
629
+ it("returns empty when metrics are missing", () => {
630
+ expect(
631
+ formatGitMetricsSegment(
632
+ makeTheme(),
633
+ undefined,
634
+ { onlyNonzero: true },
635
+ "terminal",
636
+ "bold green",
637
+ "bold red",
638
+ ),
639
+ ).toBe("");
640
+ expect(
641
+ formatGitMetricsSegment(
642
+ makeTheme(),
643
+ null,
644
+ { onlyNonzero: true },
645
+ "terminal",
646
+ "bold green",
647
+ "bold red",
648
+ ),
649
+ ).toBe("");
650
+ });
651
+
652
+ it("renders both added and deleted", () => {
653
+ const out = formatGitMetricsSegment(
654
+ makeTheme(),
655
+ { added: 12, deleted: 3 },
656
+ { onlyNonzero: false },
657
+ "terminal",
658
+ "bold green",
659
+ "bold red",
660
+ );
661
+ expect(out).toContain("+12");
662
+ expect(out).toContain("−3");
663
+ });
664
+
665
+ it("omits each zero component independently when onlyNonzero", () => {
666
+ // 0 added → only show deleted.
667
+ expect(
668
+ formatGitMetricsSegment(
669
+ makeTheme(),
670
+ { added: 0, deleted: 5 },
671
+ { onlyNonzero: true },
672
+ "terminal",
673
+ "bold green",
674
+ "bold red",
675
+ ),
676
+ ).not.toContain("+0");
677
+ expect(
678
+ formatGitMetricsSegment(
679
+ makeTheme(),
680
+ { added: 0, deleted: 5 },
681
+ { onlyNonzero: true },
682
+ "terminal",
683
+ "bold green",
684
+ "bold red",
685
+ ),
686
+ ).toContain("−5");
687
+ // 0 deleted → only show added.
688
+ expect(
689
+ formatGitMetricsSegment(
690
+ makeTheme(),
691
+ { added: 7, deleted: 0 },
692
+ { onlyNonzero: true },
693
+ "terminal",
694
+ "bold green",
695
+ "bold red",
696
+ ),
697
+ ).toContain("+7");
698
+ });
699
+
700
+ it("hides entirely at 0/0 when onlyNonzero", () => {
701
+ expect(
702
+ formatGitMetricsSegment(
703
+ makeTheme(),
704
+ { added: 0, deleted: 0 },
705
+ { onlyNonzero: true },
706
+ "terminal",
707
+ "bold green",
708
+ "bold red",
709
+ ),
710
+ ).toBe("");
711
+ });
712
+ });
713
+
714
+ describe("formatGitCommitSegment", () => {
715
+ const makeTheme = (): { fg: (color: string, text: string) => string } => ({
716
+ fg: (_color, text) => text,
717
+ });
718
+ const FULL = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
719
+
720
+ it("returns empty when commit info or oid is missing", () => {
721
+ expect(
722
+ formatGitCommitSegment(
723
+ makeTheme(),
724
+ undefined,
725
+ { hashLength: 7, onlyDetached: true, showTag: true },
726
+ "terminal",
727
+ "bold green",
728
+ ),
729
+ ).toBe("");
730
+ expect(
731
+ formatGitCommitSegment(
732
+ makeTheme(),
733
+ { oid: null, detached: false, tag: null },
734
+ { hashLength: 7, onlyDetached: true, showTag: true },
735
+ "terminal",
736
+ "bold green",
737
+ ),
738
+ ).toBe("");
739
+ });
740
+
741
+ it("shows short hash on detached HEAD", () => {
742
+ const out = formatGitCommitSegment(
743
+ makeTheme(),
744
+ { oid: FULL, detached: true, tag: null },
745
+ { hashLength: 7, onlyDetached: true, showTag: true },
746
+ "terminal",
747
+ "bold green",
748
+ );
749
+ expect(out).toContain("a1b2c3d");
750
+ });
751
+
752
+ it("hides hash on a normal branch when onlyDetached is true", () => {
753
+ const out = formatGitCommitSegment(
754
+ makeTheme(),
755
+ { oid: FULL, detached: false, tag: null },
756
+ { hashLength: 7, onlyDetached: true, showTag: true },
757
+ "terminal",
758
+ "bold green",
759
+ );
760
+ expect(out).toBe("");
761
+ });
762
+
763
+ it("hides the whole segment (including tag) on a branch when onlyDetached is true", () => {
764
+ const out = formatGitCommitSegment(
765
+ makeTheme(),
766
+ { oid: FULL, detached: false, tag: "v1.0.0" },
767
+ { hashLength: 7, onlyDetached: true, showTag: true },
768
+ "terminal",
769
+ "bold green",
770
+ );
771
+ expect(out).toBe("");
772
+ expect(out).not.toContain("v1.0.0");
773
+ });
774
+
775
+ it("shows hash on a normal branch when onlyDetached is false", () => {
776
+ const out = formatGitCommitSegment(
777
+ makeTheme(),
778
+ { oid: FULL, detached: false, tag: null },
779
+ { hashLength: 7, onlyDetached: false, showTag: false },
780
+ "terminal",
781
+ "bold green",
782
+ );
783
+ expect(out).toContain("a1b2c3d");
784
+ });
785
+
786
+ it("appends exact-match tag when present", () => {
787
+ const out = formatGitCommitSegment(
788
+ makeTheme(),
789
+ { oid: FULL, detached: true, tag: "v1.2.3" },
790
+ { hashLength: 7, onlyDetached: true, showTag: true },
791
+ "terminal",
792
+ "bold green",
793
+ );
794
+ expect(out).toContain("a1b2c3d");
795
+ expect(out).toContain("v1.2.3");
796
+ });
797
+
798
+ it("hides tag when showTag is false", () => {
799
+ const out = formatGitCommitSegment(
800
+ makeTheme(),
801
+ { oid: FULL, detached: true, tag: "v1.2.3" },
802
+ { hashLength: 7, onlyDetached: true, showTag: false },
803
+ "terminal",
804
+ "bold green",
805
+ );
806
+ expect(out).not.toContain("v1.2.3");
807
+ });
808
+ });
809
+
810
+ describe("formatPackageVersionSegment", () => {
811
+ const makeTheme = (): { fg: (color: string, text: string) => string } => ({
812
+ // Identity wrapper: prefix text with the requested color token so we can
813
+ // assert Starship style strings are routed correctly.
814
+ fg: (color, text) => `[${color}]${text}[/${color}]`,
815
+ });
816
+
817
+ it("returns empty when no package is present", () => {
818
+ expect(formatPackageVersionSegment(makeTheme(), undefined, "terminal", "nerd", "", "208")).toBe(
819
+ "",
820
+ );
821
+ });
822
+
823
+ it("renders the Starship `is <glyph> <version>` shape", () => {
824
+ const out = formatPackageVersionSegment(
825
+ makeTheme(),
826
+ { ecosystem: "nodejs", version: "1.2.3" },
827
+ "terminal",
828
+ "nerd",
829
+ "",
830
+ "208",
831
+ );
832
+ expect(out).toContain("is");
833
+ expect(out).toContain("\u{f487}");
834
+ expect(out).toContain("1.2.3");
835
+ // Starship `package` default color 208 → ANSI 256-color code 38;5;208.
836
+ expect(out).toContain("38;5;208");
837
+ });
838
+
839
+ it("falls back to the ASCII package label in ASCII mode", () => {
840
+ const out = formatPackageVersionSegment(
841
+ makeTheme(),
842
+ { ecosystem: "nodejs", version: "1.2.3" },
843
+ "terminal",
844
+ "ascii",
845
+ "",
846
+ "208",
847
+ );
848
+ expect(out).toContain("is");
849
+ expect(out).toContain("pkg");
850
+ expect(out).not.toContain("\u{f487}");
851
+ });
852
+
853
+ it("honors a configured package icon override", () => {
854
+ const out = formatPackageVersionSegment(
855
+ makeTheme(),
856
+ { ecosystem: "nodejs", version: "1.2.3" },
857
+ "terminal",
858
+ "nerd",
859
+ "#", // custom override wins over mode default
860
+ "208",
861
+ );
862
+ expect(out).toContain("#");
863
+ expect(out).not.toContain("\u{f487}");
864
+ });
865
+ });