@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,568 @@
1
+ import { homedir, hostname, userInfo } from "node:os";
2
+ import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
3
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
+ import type {
5
+ ColorSource,
6
+ ColorSpec,
7
+ ContextStyle,
8
+ ContextThresholds,
9
+ GitBranchMaxLength,
10
+ PathDisplayMode,
11
+ } from "./config";
12
+ import type { GitCommitInfo, GitMetricsInfo } from "./git";
13
+ import type { IconMode } from "./icons";
14
+ import { resolveOsIcon, resolvePackageIcon, resolveRuntimeSymbol } from "./icons";
15
+ import type { PackageVersionResult } from "./package-version";
16
+ import type { RuntimeInfo } from "./runtime";
17
+ import { renderStyleForSource } from "./style";
18
+
19
+ /**
20
+ * Starship `git_commit` style — render a short hash, optionally with an
21
+ * exact-match tag. See https://starship.rs/config/#git-commit
22
+ *
23
+ * Visibility is decided by the caller; this helper only formats the data.
24
+ * `hashLength` is clamped to [4, 40] upstream.
25
+ */
26
+ export function formatGitCommitSegment(
27
+ theme: Pick<Theme, "fg">,
28
+ commit: GitCommitInfo | undefined,
29
+ config: { hashLength: number; onlyDetached: boolean; showTag: boolean },
30
+ colorSource: ColorSource,
31
+ style: ColorSpec,
32
+ ): string {
33
+ if (!commit?.oid) return "";
34
+ // Starship's only_detached hides the whole module when attached.
35
+ if (config.onlyDetached && !commit.detached) return "";
36
+ const hash = commit.oid.slice(0, config.hashLength);
37
+ const tag = config.showTag && commit.tag ? commit.tag : "";
38
+ if (!hash && !tag) return "";
39
+ const label = [hash, tag].filter(Boolean).join(" ");
40
+ return renderStyleForSource(theme, colorSource, style, label);
41
+ }
42
+
43
+ /**
44
+ * Starship `git_metrics` style — render `+added −deleted` line counts.
45
+ * See https://starship.rs/config/#git-metrics
46
+ *
47
+ * When `onlyNonzero` is true, each zero component is omitted independently
48
+ * and the whole segment hides at 0/0.
49
+ */
50
+ export function formatGitMetricsSegment(
51
+ theme: Pick<Theme, "fg">,
52
+ metrics: GitMetricsInfo | null | undefined,
53
+ config: { onlyNonzero: boolean },
54
+ colorSource: ColorSource,
55
+ addedStyle: ColorSpec,
56
+ deletedStyle: ColorSpec,
57
+ ): string {
58
+ if (!metrics) return "";
59
+ const showAdded = !config.onlyNonzero || metrics.added > 0;
60
+ const showDeleted = !config.onlyNonzero || metrics.deleted > 0;
61
+ if (!showAdded && !showDeleted) return "";
62
+ const parts: string[] = [];
63
+ if (showAdded) {
64
+ parts.push(renderStyleForSource(theme, colorSource, addedStyle, `+${metrics.added}`));
65
+ }
66
+ if (showDeleted) {
67
+ parts.push(renderStyleForSource(theme, colorSource, deletedStyle, `−${metrics.deleted}`));
68
+ }
69
+ return parts.join(" ");
70
+ }
71
+
72
+ export type UsageTotals = {
73
+ input: number;
74
+ output: number;
75
+ cacheRead: number;
76
+ cacheWrite: number;
77
+ latestCacheHitRate?: number;
78
+ cost: number;
79
+ };
80
+
81
+ export type ContextColorTier = "normal" | "warning" | "error";
82
+
83
+ type SessionUsage = {
84
+ input?: unknown;
85
+ output?: unknown;
86
+ cacheRead?: unknown;
87
+ cacheWrite?: unknown;
88
+ cost?: unknown;
89
+ };
90
+
91
+ type SessionEntry = {
92
+ type?: string;
93
+ id?: string | number;
94
+ timestamp?: string | number;
95
+ usage?: SessionUsage;
96
+ message?: {
97
+ role?: string;
98
+ usage?: SessionUsage;
99
+ };
100
+ };
101
+
102
+ type SelectedUsage = {
103
+ usage: SessionUsage | undefined;
104
+ location: "message" | "entry";
105
+ isAssistant: boolean;
106
+ };
107
+
108
+ type UsageCacheEntry = {
109
+ key: string;
110
+ totals: UsageTotals;
111
+ };
112
+
113
+ const MAX_USAGE_TOTAL = Number.MAX_VALUE;
114
+
115
+ let usageTotalsCache: UsageCacheEntry | undefined;
116
+ let usageTotalsAggregationPassCount = 0;
117
+
118
+ export function formatCount(value: number): string {
119
+ if (value < 1000) return value.toString();
120
+ if (value < 10_000) return `${(value / 1000).toFixed(1)}k`;
121
+ if (value < 1_000_000) return `${Math.round(value / 1000)}k`;
122
+ if (value < 10_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
123
+ return `${Math.round(value / 1_000_000)}M`;
124
+ }
125
+
126
+ /** Human-readable whole-second duration used by settled and minimalist turn UI. */
127
+ export function formatElapsedDuration(durationMs: number): string {
128
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
129
+ const hours = Math.floor(totalSeconds / 3600);
130
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
131
+ const seconds = totalSeconds % 60;
132
+ if (hours > 0) return `${hours}h ${minutes}m`;
133
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
134
+ return `${seconds}s`;
135
+ }
136
+
137
+ export function formatProviderLabel(provider: string | undefined): string {
138
+ if (!provider) return "Unknown";
139
+
140
+ const known: Record<string, string> = {
141
+ anthropic: "Anthropic",
142
+ gemini: "Google",
143
+ google: "Google",
144
+ ollama: "Ollama",
145
+ openai: "OpenAI",
146
+ "openai-codex": "OpenAI",
147
+ };
148
+
149
+ return (
150
+ known[provider] ?? provider.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
151
+ );
152
+ }
153
+
154
+ function calculateCacheHitRate(
155
+ input: number,
156
+ cacheRead: number,
157
+ cacheWrite: number,
158
+ ): number | undefined {
159
+ const promptTokens = input + cacheRead + cacheWrite;
160
+ if (promptTokens === 0) return undefined;
161
+ if (Number.isFinite(promptTokens)) return (cacheRead / promptTokens) * 100;
162
+
163
+ const scale = Math.max(input, cacheRead, cacheWrite);
164
+ const scaledPromptTokens = input / scale + cacheRead / scale + cacheWrite / scale;
165
+ return (cacheRead / scale / scaledPromptTokens) * 100;
166
+ }
167
+
168
+ function normalizeUsageNumber(value: unknown): number {
169
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
170
+ }
171
+
172
+ function usageCostTotal(usage: SessionUsage | undefined): number {
173
+ if (typeof usage?.cost !== "object" || usage.cost === null) return 0;
174
+ return normalizeUsageNumber((usage.cost as { total?: unknown }).total);
175
+ }
176
+
177
+ function addUsageTotal(total: number, value: number): number {
178
+ const sum = total + value;
179
+ return Number.isFinite(sum) ? sum : MAX_USAGE_TOTAL;
180
+ }
181
+
182
+ function usageForEntry(entry: SessionEntry): SelectedUsage | undefined {
183
+ if (entry.type === "message") {
184
+ const role = entry.message?.role;
185
+ if (role !== "assistant" && role !== "toolResult") return undefined;
186
+ return {
187
+ usage: entry.message?.usage,
188
+ location: "message",
189
+ isAssistant: role === "assistant",
190
+ };
191
+ }
192
+ if (entry.type === "compaction" || entry.type === "branch_summary") {
193
+ return { usage: entry.usage, location: "entry", isAssistant: false };
194
+ }
195
+ return undefined;
196
+ }
197
+
198
+ function normalizedUsage(usage: SessionUsage | undefined) {
199
+ return {
200
+ input: normalizeUsageNumber(usage?.input),
201
+ output: normalizeUsageNumber(usage?.output),
202
+ cacheRead: normalizeUsageNumber(usage?.cacheRead),
203
+ cacheWrite: normalizeUsageNumber(usage?.cacheWrite),
204
+ cost: usageCostTotal(usage),
205
+ };
206
+ }
207
+
208
+ function entryIdentity(entry: SessionEntry): string {
209
+ const selected = usageForEntry(entry);
210
+ if (!selected) return "unsupported";
211
+ const usage = normalizedUsage(selected.usage);
212
+ return JSON.stringify([
213
+ entry.id ?? null,
214
+ entry.timestamp ?? null,
215
+ entry.type ?? null,
216
+ entry.message?.role ?? null,
217
+ selected.location,
218
+ usage.input,
219
+ usage.output,
220
+ usage.cacheRead,
221
+ usage.cacheWrite,
222
+ usage.cost,
223
+ ]);
224
+ }
225
+
226
+ function buildUsageFingerprint(entries: readonly SessionEntry[]): string {
227
+ return entries.map(entryIdentity).join("\0");
228
+ }
229
+
230
+ function computeUsageTotals(entries: readonly SessionEntry[]): UsageTotals {
231
+ usageTotalsAggregationPassCount += 1;
232
+ let input = 0;
233
+ let output = 0;
234
+ let cacheRead = 0;
235
+ let cacheWrite = 0;
236
+ let latestCacheHitRate: number | undefined;
237
+ let cost = 0;
238
+
239
+ for (const entry of entries) {
240
+ const selected = usageForEntry(entry);
241
+ if (!selected) continue;
242
+ const usage = normalizedUsage(selected.usage);
243
+
244
+ input = addUsageTotal(input, usage.input);
245
+ output = addUsageTotal(output, usage.output);
246
+ cacheRead = addUsageTotal(cacheRead, usage.cacheRead);
247
+ cacheWrite = addUsageTotal(cacheWrite, usage.cacheWrite);
248
+ cost = addUsageTotal(cost, usage.cost);
249
+ if (selected.isAssistant) {
250
+ latestCacheHitRate = calculateCacheHitRate(usage.input, usage.cacheRead, usage.cacheWrite);
251
+ }
252
+ }
253
+
254
+ return Object.freeze({ input, output, cacheRead, cacheWrite, latestCacheHitRate, cost });
255
+ }
256
+
257
+ export function invalidateUsageTotalsCache(): void {
258
+ usageTotalsCache = undefined;
259
+ }
260
+
261
+ /** Test helper: counts aggregation passes only; every cache lookup still fingerprints all entries. */
262
+ export function __usageTotalsAggregationPassCount(): number {
263
+ return usageTotalsAggregationPassCount;
264
+ }
265
+
266
+ /** Test helper: reset the aggregation-pass counter and cached totals. */
267
+ export function __resetUsageTotalsCacheForTests(): void {
268
+ usageTotalsCache = undefined;
269
+ usageTotalsAggregationPassCount = 0;
270
+ }
271
+
272
+ export function getUsageTotals(ctx: ExtensionContext): UsageTotals {
273
+ const sessionManager = ctx.sessionManager as {
274
+ getEntries?: () => readonly SessionEntry[];
275
+ getBranch: () => readonly SessionEntry[];
276
+ };
277
+ const entries =
278
+ typeof sessionManager.getEntries === "function"
279
+ ? sessionManager.getEntries()
280
+ : sessionManager.getBranch();
281
+ const key = buildUsageFingerprint(entries);
282
+ if (usageTotalsCache?.key === key) return usageTotalsCache.totals;
283
+
284
+ const totals = computeUsageTotals(entries);
285
+ usageTotalsCache = { key, totals };
286
+ return totals;
287
+ }
288
+
289
+ export function buildCacheReadLabel(cacheRead: number): string {
290
+ return cacheRead > 0 ? `R${formatCount(cacheRead)}` : "";
291
+ }
292
+
293
+ export function buildCacheWriteLabel(cacheWrite: number): string {
294
+ return cacheWrite > 0 ? `W${formatCount(cacheWrite)}` : "";
295
+ }
296
+
297
+ export function buildSessionTokenLabel(totals: Pick<UsageTotals, "input" | "output">): string {
298
+ const parts: string[] = [];
299
+ if (totals.input) parts.push(`↑${formatCount(totals.input)}`);
300
+ if (totals.output) parts.push(`↓${formatCount(totals.output)}`);
301
+ return parts.length > 0 ? parts.join(" ") : "↑0 ↓0";
302
+ }
303
+
304
+ export function formatCacheHitRate(rate: number | undefined): string {
305
+ return `${rate !== undefined && Number.isFinite(rate) ? rate.toFixed(1) : "0.0"}%`;
306
+ }
307
+
308
+ export function buildTokenLabel(totals: UsageTotals, cacheHitIcon = "󰆼"): string {
309
+ const tokenLabel = buildSessionTokenLabel(totals);
310
+ const hasCacheTokens = totals.cacheRead > 0 || totals.cacheWrite > 0;
311
+ if (!hasCacheTokens || totals.latestCacheHitRate === undefined) return tokenLabel;
312
+
313
+ const cacheHitRate = formatCacheHitRate(totals.latestCacheHitRate);
314
+ return `${tokenLabel} ${cacheHitIcon ? `${cacheHitIcon} ` : ""}${cacheHitRate}`;
315
+ }
316
+
317
+ export function buildCostLabel(totals: UsageTotals): string {
318
+ return `$${totals.cost.toFixed(3)}`;
319
+ }
320
+
321
+ export function buildSessionDurationLabel(startEpoch: number): string {
322
+ const totalSeconds = Math.max(0, Math.floor((Date.now() - startEpoch) / 1000));
323
+ const hours = Math.floor(totalSeconds / 3600);
324
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
325
+ const seconds = totalSeconds % 60;
326
+ if (hours > 0) return `${hours}h ${minutes}m`;
327
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
328
+ return `${seconds}s`;
329
+ }
330
+
331
+ export function contextColorTier(
332
+ percent: number | null | undefined,
333
+ thresholds: ContextThresholds = { warning: 70, error: 90 },
334
+ ): ContextColorTier {
335
+ if (percent === null || percent === undefined || !Number.isFinite(percent)) return "normal";
336
+ if (percent >= thresholds.error) return "error";
337
+ if (percent >= thresholds.warning) return "warning";
338
+ return "normal";
339
+ }
340
+
341
+ export function buildContextGauge(percent: number, width = 10, ascii = false): string {
342
+ const clamped = Math.max(0, Math.min(100, percent));
343
+ const filled = Math.round((clamped / 100) * width);
344
+ const on = ascii ? "#" : "█";
345
+ const off = ascii ? "-" : "░";
346
+ return `${on.repeat(filled)}${off.repeat(Math.max(0, width - filled))}`;
347
+ }
348
+
349
+ export function formatContextPercentLabel(
350
+ percent: number | null | undefined,
351
+ contextWindow: number | undefined,
352
+ ): string {
353
+ if (!contextWindow || contextWindow <= 0) return "--";
354
+ const percentLabel =
355
+ percent === null || percent === undefined || !Number.isFinite(percent)
356
+ ? "?"
357
+ : `${Math.max(0, Math.min(999, percent)).toFixed(1)}%`;
358
+ return `${percentLabel}/${formatCount(contextWindow)}`;
359
+ }
360
+
361
+ export function buildContextDisplayLabel(options: {
362
+ percent: number | null | undefined;
363
+ contextWindow: number | undefined;
364
+ style?: ContextStyle;
365
+ asciiGauge?: boolean;
366
+ }): string {
367
+ const { percent, contextWindow, style = "text", asciiGauge = false } = options;
368
+ if (!contextWindow || contextWindow <= 0) return "--";
369
+
370
+ const text = formatContextPercentLabel(percent, contextWindow);
371
+ const numericPercent =
372
+ percent === null || percent === undefined || !Number.isFinite(percent)
373
+ ? 0
374
+ : Math.max(0, Math.min(100, percent));
375
+ const gauge = buildContextGauge(numericPercent, 10, asciiGauge);
376
+
377
+ if (style === "gauge") return `[${gauge}]`;
378
+ if (style === "text+gauge") return `[${gauge}] ${text}`;
379
+ return text;
380
+ }
381
+
382
+ export type ContextUsageSnapshot = {
383
+ percent: number | undefined;
384
+ contextWindow: number | undefined;
385
+ };
386
+
387
+ export function resolveContextUsage(
388
+ ctx: Pick<ExtensionContext, "model" | "getContextUsage">,
389
+ live?: { tokens: number },
390
+ ): ContextUsageSnapshot {
391
+ const usage = ctx.getContextUsage();
392
+ const contextWindow = ctx.model?.contextWindow ?? usage?.contextWindow;
393
+ return {
394
+ percent:
395
+ live && contextWindow && contextWindow > 0
396
+ ? (live.tokens / contextWindow) * 100
397
+ : (usage?.percent ?? undefined),
398
+ contextWindow,
399
+ };
400
+ }
401
+
402
+ export function buildContextLabel(ctx: ExtensionContext): string {
403
+ const { percent, contextWindow } = resolveContextUsage(ctx);
404
+ return formatContextPercentLabel(percent, contextWindow);
405
+ }
406
+
407
+ export function formatRuntimeSegment(
408
+ theme: Pick<Theme, "fg">,
409
+ runtime: RuntimeInfo | undefined,
410
+ prefixStyle: ColorSpec,
411
+ colorSource: ColorSource,
412
+ mode: IconMode = "auto",
413
+ ): string {
414
+ if (!runtime) return "";
415
+ const symbol = resolveRuntimeSymbol(runtime.name, runtime.symbol, mode);
416
+ const label = runtime.version ? `${symbol} ${runtime.version}` : symbol;
417
+ return `${renderStyleForSource(theme, colorSource, prefixStyle, "via")} ${renderStyleForSource(theme, colorSource, runtime.style, label)}`;
418
+ }
419
+
420
+ /**
421
+ * Render the package-version segment in Starship `is <glyph> <version>` shape.
422
+ *
423
+ * Distinct from the runtime segment: this surfaces the project's own
424
+ * manifest version (e.g. `package.json#version`), not the installed
425
+ * toolchain version. Glyph comes from the Starship Nerd Font preset
426
+ * (https://starship.rs/presets/nerd-font); default color `208` matches
427
+ * the Starship `package` module default
428
+ * (https://starship.rs/config/#package-version).
429
+ */
430
+ export function formatPackageVersionSegment(
431
+ theme: Pick<Theme, "fg">,
432
+ pkg: PackageVersionResult | undefined,
433
+ colorSource: ColorSource,
434
+ mode: IconMode = "auto",
435
+ configuredIcon: string = "",
436
+ versionStyle: ColorSpec = "208",
437
+ ): string {
438
+ if (!pkg) return "";
439
+ const icon = resolvePackageIcon(configuredIcon, mode);
440
+ const label = `${icon} ${pkg.version}`;
441
+ return `${renderStyleForSource(theme, colorSource, "", "is")} ${renderStyleForSource(theme, colorSource, versionStyle, label)}`;
442
+ }
443
+
444
+ export type FormatCwdOptions = {
445
+ mode?: PathDisplayMode;
446
+ /** Final path components to keep in full/repository mode. 0 = unlimited. */
447
+ depth?: number;
448
+ home?: string;
449
+ repositoryRoot?: string;
450
+ };
451
+
452
+ function normalizeDisplayPath(cwd: string): string {
453
+ const withSlashes = cwd.replace(/\\/g, "/");
454
+ if (withSlashes === "/" || /^\/+$/.test(withSlashes)) return "/";
455
+ const stripped = withSlashes.replace(/\/+$/, "");
456
+ return stripped === "" ? withSlashes : stripped;
457
+ }
458
+
459
+ function toHomePath(path: string, home: string): string {
460
+ if (!home) return path;
461
+ const homeNorm = home.replace(/\\/g, "/").replace(/\/+$/, "");
462
+ if (!homeNorm) return path;
463
+ if (path === homeNorm) return "~";
464
+ if (path.startsWith(`${homeNorm}/`)) return `~${path.slice(homeNorm.length)}`;
465
+ return path;
466
+ }
467
+
468
+ /** Starship-style: keep last `depth` components; prefix with `…/` when parents were dropped. */
469
+ function applyPathDepth(path: string, depth: number): string {
470
+ if (!Number.isFinite(depth) || depth <= 0) return path;
471
+ const limit = Math.floor(depth);
472
+ if (path === "~" || path === "/") return path;
473
+
474
+ let components: string[];
475
+ if (path.startsWith("~/")) {
476
+ components = path.slice(2).split("/").filter(Boolean);
477
+ } else if (/^[A-Za-z]:\//.test(path)) {
478
+ components = path.slice(3).split("/").filter(Boolean);
479
+ } else if (path.startsWith("/")) {
480
+ components = path.slice(1).split("/").filter(Boolean);
481
+ } else {
482
+ components = path.split("/").filter(Boolean);
483
+ }
484
+
485
+ if (components.length <= limit) return path;
486
+ return `…/${components.slice(-limit).join("/")}`;
487
+ }
488
+
489
+ export function formatCwdLabel(cwd: string, cwdIcon: string, options?: FormatCwdOptions): string {
490
+ const mode = options?.mode ?? "basename";
491
+ const normalized = normalizeDisplayPath(cwd);
492
+ const fullPath = (depth = options?.depth ?? 0) => {
493
+ const home =
494
+ options?.home ??
495
+ (() => {
496
+ try {
497
+ return homedir();
498
+ } catch {
499
+ return "";
500
+ }
501
+ })();
502
+ return applyPathDepth(toHomePath(normalized, home), depth);
503
+ };
504
+ let pathText: string;
505
+ if (mode === "full") {
506
+ pathText = fullPath();
507
+ } else if (mode === "repository") {
508
+ const root = options?.repositoryRoot ? normalizeDisplayPath(options.repositoryRoot) : undefined;
509
+ if (!root) {
510
+ pathText = fullPath(0);
511
+ } else {
512
+ const rootPrefix = root === "/" ? "/" : `${root}/`;
513
+ if (normalized !== root && !normalized.startsWith(rootPrefix)) {
514
+ // Unsafe roots fail open to the existing unlimited full path.
515
+ pathText = fullPath(0);
516
+ } else {
517
+ const relative = normalized === root ? "." : normalized.slice(rootPrefix.length);
518
+ pathText = relative === "." ? relative : applyPathDepth(relative, options?.depth ?? 0);
519
+ }
520
+ }
521
+ } else if (normalized === "/") {
522
+ pathText = "/";
523
+ } else {
524
+ const parts = normalized.split("/").filter(Boolean);
525
+ pathText = parts[parts.length - 1] ?? cwd;
526
+ }
527
+ return cwdIcon ? `${cwdIcon} ${pathText}` : pathText;
528
+ }
529
+
530
+ function stripAnsi(text: string): string {
531
+ return text.replace(/\u001B\[[0-9;]*m/g, "");
532
+ }
533
+
534
+ export function formatGitBranchText(
535
+ branch: string,
536
+ maxLength: GitBranchMaxLength = "full",
537
+ ): string {
538
+ if (maxLength === "full" || visibleWidth(branch) <= maxLength) return branch;
539
+ return stripAnsi(truncateToWidth(branch, maxLength, "…"));
540
+ }
541
+
542
+ export function formatUsernameHostLabel(icon: string): string {
543
+ try {
544
+ const user = userInfo().username;
545
+ const host = hostname();
546
+ if (!user || !host) return "";
547
+ const label = `${user}@${host}`;
548
+ return icon ? `${icon} ${label}` : label;
549
+ } catch {
550
+ return "";
551
+ }
552
+ }
553
+
554
+ export function formatTimeLabel(icon: string): string {
555
+ const now = new Date();
556
+ const hours = String(now.getHours()).padStart(2, "0");
557
+ const minutes = String(now.getMinutes()).padStart(2, "0");
558
+ const label = `${hours}:${minutes}`;
559
+ return icon ? `${icon} ${label}` : label;
560
+ }
561
+
562
+ export function formatOsLabel(
563
+ configuredIcon: string,
564
+ mode: IconMode = "auto",
565
+ platform: string = process.platform,
566
+ ): string {
567
+ return resolveOsIcon(configuredIcon, mode, platform);
568
+ }