@selesai/code 0.9.15 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/CHANGELOG.md +22 -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/skills/design-references/SKILL.md +31 -0
  118. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  119. package/dist/themes/adventure.json +84 -0
  120. package/dist/themes/adwaita-dark.json +84 -0
  121. package/dist/themes/arcoiris.json +84 -0
  122. package/dist/themes/arthur.json +84 -0
  123. package/dist/themes/atom.json +84 -0
  124. package/dist/themes/aura.json +84 -0
  125. package/dist/themes/black-metal-bathory.json +84 -0
  126. package/dist/themes/black-metal-burzum.json +84 -0
  127. package/dist/themes/black-metal-khold.json +84 -0
  128. package/dist/themes/box.json +84 -0
  129. package/dist/themes/brogrammer.json +84 -0
  130. package/dist/themes/carbonfox.json +84 -0
  131. package/dist/themes/catppuccin-mocha.json +84 -0
  132. package/dist/themes/citruszest.json +84 -0
  133. package/dist/themes/cursor-dark.json +84 -0
  134. package/dist/themes/cutie-pro.json +84 -0
  135. package/dist/themes/dark-modern.json +84 -0
  136. package/dist/themes/dark-pastel.json +84 -0
  137. package/dist/themes/dimmed-monokai.json +84 -0
  138. package/dist/themes/doom-peacock.json +84 -0
  139. package/dist/themes/dracula-plus.json +84 -0
  140. package/dist/themes/dracula.json +80 -0
  141. package/dist/themes/earthsong.json +84 -0
  142. package/dist/themes/everforest-dark-hard.json +84 -0
  143. package/dist/themes/everforest-dark.json +80 -0
  144. package/dist/themes/fahrenheit.json +84 -0
  145. package/dist/themes/flatland.json +84 -0
  146. package/dist/themes/flexoki-dark.json +84 -0
  147. package/dist/themes/front-end-delight.json +84 -0
  148. package/dist/themes/fun-forrest.json +84 -0
  149. package/dist/themes/galizur.json +84 -0
  150. package/dist/themes/github-dark-colorblind.json +84 -0
  151. package/dist/themes/github-dark-high-contrast.json +84 -0
  152. package/dist/themes/glacier.json +84 -0
  153. package/dist/themes/gruber-darker.json +84 -0
  154. package/dist/themes/gruvbox-dark-hard.json +84 -0
  155. package/dist/themes/gruvbox-dark.json +84 -0
  156. package/dist/themes/gruvbox-material.json +84 -0
  157. package/dist/themes/guezwhoz.json +84 -0
  158. package/dist/themes/hacktober.json +84 -0
  159. package/dist/themes/hardcore.json +84 -0
  160. package/dist/themes/havn-skumring.json +84 -0
  161. package/dist/themes/ic-orange-ppl.json +84 -0
  162. package/dist/themes/iterm2-smoooooth.json +84 -0
  163. package/dist/themes/iterm2-tango-dark.json +84 -0
  164. package/dist/themes/japanesque.json +84 -0
  165. package/dist/themes/jellybeans.json +84 -0
  166. package/dist/themes/kanagawa-wave.json +84 -0
  167. package/dist/themes/kurokula.json +84 -0
  168. package/dist/themes/later-this-evening.json +84 -0
  169. package/dist/themes/lovelace.json +84 -0
  170. package/dist/themes/material-darker.json +84 -0
  171. package/dist/themes/matte-black.json +84 -0
  172. package/dist/themes/mellow.json +84 -0
  173. package/dist/themes/miasma.json +84 -0
  174. package/dist/themes/nord.json +80 -0
  175. package/dist/themes/nvim-dark.json +84 -0
  176. package/dist/themes/one-dark.json +80 -0
  177. package/dist/themes/popping-and-locking.json +84 -0
  178. package/dist/themes/rose-pine.json +80 -0
  179. package/dist/themes/sea-shells.json +84 -0
  180. package/dist/themes/sleepy-hollow.json +84 -0
  181. package/dist/themes/smyck.json +84 -0
  182. package/dist/themes/solarized-dark.json +80 -0
  183. package/dist/themes/tokyo-night.json +80 -0
  184. package/dist/themes/tomorrow-night-bright.json +84 -0
  185. package/dist/themes/tomorrow-night-burns.json +84 -0
  186. package/dist/themes/tomorrow-night.json +84 -0
  187. package/dist/themes/twilight.json +84 -0
  188. package/dist/themes/vague.json +84 -0
  189. package/dist/themes/vesper.json +84 -0
  190. package/dist/themes/xcode-dark-hc.json +84 -0
  191. package/docs/custom-provider.md +0 -1
  192. package/package.json +3 -3
  193. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  194. package/dist/extensions/auto-model/classifier.ts +0 -23
  195. package/dist/extensions/auto-model/config.ts +0 -51
  196. package/dist/extensions/auto-model/index.ts +0 -22
  197. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  198. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  199. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  200. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  201. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  202. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  203. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  204. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  205. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  206. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  207. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  208. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  209. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  210. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  211. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  212. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  213. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  214. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  215. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  216. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  217. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  218. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  219. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  220. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  221. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  222. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  223. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  224. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  225. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  226. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  227. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  228. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  229. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  230. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  231. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  232. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  233. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  234. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  235. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  236. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  237. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  238. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  239. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  240. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  241. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  242. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  243. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  244. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  245. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  246. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  247. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  248. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  249. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  250. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  251. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  252. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  253. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  254. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  255. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  256. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  257. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  258. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  259. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  260. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  261. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  262. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  263. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  264. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  265. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  266. package/dist/extensions/preview-tools-disabled.ts +0 -11
  267. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,812 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import type { SeparatorStyle, ZentuiConfig } from "./config";
4
+ import { FOOTER_FORMAT_ALIASES } from "./config";
5
+ import { sanitizeEditorMetadataText } from "./editor-metadata-format";
6
+ import {
7
+ collectExtensionStatusSegments,
8
+ type ExtensionStatusSegment,
9
+ sanitizeExtensionStatusText,
10
+ } from "./extension-status";
11
+ import {
12
+ collectFooterFormatReferences,
13
+ compileCompactFormat,
14
+ parseFooterFormat,
15
+ renderFormatSplit,
16
+ renderFormatTokens,
17
+ stripOrphanSeparators,
18
+ } from "./footer-format";
19
+ import {
20
+ compactChunkBudget,
21
+ fullFooterFitsAligned,
22
+ packCompactChunks,
23
+ reflowFullFooter,
24
+ } from "./footer-layout";
25
+ import {
26
+ buildContextDisplayLabel,
27
+ buildSessionDurationLabel,
28
+ contextColorTier,
29
+ formatCwdLabel,
30
+ formatGitBranchText,
31
+ formatGitCommitSegment,
32
+ formatGitMetricsSegment,
33
+ formatOsLabel,
34
+ formatPackageVersionSegment,
35
+ formatRuntimeSegment,
36
+ formatTimeLabel,
37
+ formatUsernameHostLabel,
38
+ resolveContextUsage,
39
+ } from "./format";
40
+ import { resolveRuntimeSymbol } from "./icons";
41
+ import type { LiveContextOverride } from "./live-context";
42
+ import { type FooterState, modelLabelFor } from "./state";
43
+ import { renderStyleForSource } from "./style";
44
+
45
+ const separatorText: Record<SeparatorStyle, string> = {
46
+ pipe: " | ",
47
+ dot: " · ",
48
+ chevron: " › ",
49
+ none: " ",
50
+ };
51
+
52
+ function joinStatusTexts(statusTexts: string[], separator: string): string {
53
+ return statusTexts.filter(Boolean).join(separator);
54
+ }
55
+
56
+ function normalizeModelInfoPart(value: string): string {
57
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
58
+ }
59
+
60
+ function composeModelInfoLabel(model: string, provider: string): string {
61
+ const normalizedModel = normalizeModelInfoPart(model);
62
+ const normalizedProvider = normalizeModelInfoPart(provider);
63
+ const providerIsDuplicated =
64
+ normalizedProvider.length > 0 && normalizedModel.includes(normalizedProvider);
65
+ return [model, providerIsDuplicated ? "" : provider].filter(Boolean).join(" ");
66
+ }
67
+
68
+ function fitStatusTexts(statusTexts: string[], maxWidth: number, separator: string): string {
69
+ if (maxWidth <= 0) return "";
70
+
71
+ const fitted: string[] = [];
72
+ for (const text of statusTexts) {
73
+ const candidate = joinStatusTexts([...fitted, text], separator);
74
+ if (visibleWidth(candidate) <= maxWidth) {
75
+ fitted.push(text);
76
+ continue;
77
+ }
78
+
79
+ if (fitted.length === 0) {
80
+ return maxWidth > 1 ? truncateToWidth(text, maxWidth, "…") : "";
81
+ }
82
+ break;
83
+ }
84
+
85
+ return joinStatusTexts(fitted, separator);
86
+ }
87
+
88
+ function appendStatusArea(base: string, statusText: string, separator: string): string {
89
+ if (!base) return statusText;
90
+ if (!statusText) return base;
91
+ return `${base}${separator}${statusText}`;
92
+ }
93
+
94
+ function prependStatusArea(base: string, statusText: string, separator: string): string {
95
+ if (!base) return statusText;
96
+ if (!statusText) return base;
97
+ return `${statusText}${separator}${base}`;
98
+ }
99
+
100
+ function composeBuiltInFooterContent(left: string, right: string, innerWidth: number): string {
101
+ const leftWidth = visibleWidth(left);
102
+ const rightWidth = visibleWidth(right);
103
+ return leftWidth >= innerWidth
104
+ ? truncateToWidth(left, innerWidth, "")
105
+ : leftWidth + 1 + rightWidth <= innerWidth
106
+ ? `${left}${" ".repeat(innerWidth - leftWidth - rightWidth)}${right}`
107
+ : truncateToWidth(left, innerWidth, "");
108
+ }
109
+
110
+ function composeFooterContent(
111
+ builtInLeft: string,
112
+ builtInRight: string,
113
+ extensionLeft: string[],
114
+ extensionMiddle: string[],
115
+ extensionRight: string[],
116
+ separator: string,
117
+ innerWidth: number,
118
+ ): string {
119
+ const builtInLeftWidth = visibleWidth(builtInLeft);
120
+ const builtInRightWidth = visibleWidth(builtInRight);
121
+ const minimumGap = builtInLeft && builtInRight ? 1 : 0;
122
+
123
+ if (builtInLeftWidth + minimumGap + builtInRightWidth > innerWidth) {
124
+ return composeBuiltInFooterContent(builtInLeft, builtInRight, innerWidth);
125
+ }
126
+
127
+ const available = Math.max(0, innerWidth - builtInLeftWidth - builtInRightWidth - minimumGap);
128
+ let remaining = available;
129
+ const leftConnectorWidth = builtInLeft && extensionLeft.length > 0 ? visibleWidth(separator) : 0;
130
+ const rightConnectorWidth =
131
+ builtInRight && extensionRight.length > 0 ? visibleWidth(separator) : 0;
132
+ let leftStatus = "";
133
+ let rightStatus = "";
134
+
135
+ if (extensionLeft.length > 0 && extensionRight.length > 0) {
136
+ const leftBudget = Math.max(0, Math.floor(available / 2) - leftConnectorWidth);
137
+ leftStatus = fitStatusTexts(extensionLeft, leftBudget, separator);
138
+ remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
139
+
140
+ const rightBudget = Math.max(0, remaining - rightConnectorWidth);
141
+ rightStatus = fitStatusTexts(extensionRight, rightBudget, separator);
142
+ remaining -= rightStatus ? rightConnectorWidth + visibleWidth(rightStatus) : 0;
143
+
144
+ const expandedLeftBudget = Math.max(0, remaining + visibleWidth(leftStatus));
145
+ const expandedLeftStatus = fitStatusTexts(extensionLeft, expandedLeftBudget, separator);
146
+ if (visibleWidth(expandedLeftStatus) > visibleWidth(leftStatus)) {
147
+ remaining += leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
148
+ leftStatus = expandedLeftStatus;
149
+ remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
150
+ }
151
+ } else if (extensionLeft.length > 0) {
152
+ leftStatus = fitStatusTexts(
153
+ extensionLeft,
154
+ Math.max(0, available - leftConnectorWidth),
155
+ separator,
156
+ );
157
+ remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
158
+ } else if (extensionRight.length > 0) {
159
+ rightStatus = fitStatusTexts(
160
+ extensionRight,
161
+ Math.max(0, available - rightConnectorWidth),
162
+ separator,
163
+ );
164
+ remaining -= rightStatus ? rightConnectorWidth + visibleWidth(rightStatus) : 0;
165
+ }
166
+
167
+ const left = appendStatusArea(builtInLeft, leftStatus, separator);
168
+ const right = prependStatusArea(builtInRight, rightStatus, separator);
169
+ const gapWidth = Math.max(0, innerWidth - visibleWidth(left) - visibleWidth(right));
170
+ const middle = fitStatusTexts(extensionMiddle, gapWidth, separator);
171
+ const middleWidth = visibleWidth(middle);
172
+
173
+ if (!middle || middleWidth <= 0) {
174
+ return `${left}${" ".repeat(gapWidth)}${right}`;
175
+ }
176
+
177
+ const leftPadding = Math.floor((gapWidth - middleWidth) / 2);
178
+ const rightPadding = gapWidth - middleWidth - leftPadding;
179
+ return `${left}${" ".repeat(leftPadding)}${middle}${" ".repeat(rightPadding)}${right}`;
180
+ }
181
+
182
+ export function installFooter(
183
+ ctx: ExtensionContext,
184
+ state: FooterState,
185
+ getConfig: () => ZentuiConfig,
186
+ hooks: {
187
+ setRequestRender: (fn: (() => void) | undefined) => void;
188
+ scheduleProjectRefresh: (ctx: ExtensionContext) => void;
189
+ setExtensionStatusesGetter?: (fn: (() => ReadonlyMap<string, string>) | undefined) => void;
190
+ getLiveContext?: () => LiveContextOverride | undefined;
191
+ getRepositoryRoot?: (cwd: string) => string | undefined;
192
+ onDispose?: () => void;
193
+ },
194
+ ): void {
195
+ ctx.ui.setFooter((tui, theme, footerData) => {
196
+ hooks.setRequestRender(() => tui.requestRender());
197
+ hooks.setExtensionStatusesGetter?.(() => footerData.getExtensionStatuses());
198
+ const unsubscribeBranch = footerData.onBranchChange(() => {
199
+ hooks.scheduleProjectRefresh(ctx);
200
+ tui.requestRender();
201
+ });
202
+
203
+ return {
204
+ dispose: () => {
205
+ unsubscribeBranch();
206
+ hooks.setRequestRender(undefined);
207
+ hooks.setExtensionStatusesGetter?.(undefined);
208
+ hooks.onDispose?.();
209
+ },
210
+ invalidate() {},
211
+ render(width: number): string[] {
212
+ if (width <= 0) return [""];
213
+ const config = getConfig();
214
+ const footer = config.components.footer;
215
+ const footerModelLabel = modelLabelFor(state, footer.modelLabel);
216
+ const wideFormatTokens = config.components.footer.styles.starship.format
217
+ ? parseFooterFormat(config.components.footer.styles.starship.format)
218
+ : [];
219
+ const compactFormatTokens = config.components.footer.styles.starship.responsive
220
+ ? parseFooterFormat(config.components.footer.styles.starship.compactFormat)
221
+ : [];
222
+ const wideReferences = collectFooterFormatReferences(
223
+ wideFormatTokens,
224
+ FOOTER_FORMAT_ALIASES,
225
+ );
226
+ const compactReferences = collectFooterFormatReferences(
227
+ compactFormatTokens,
228
+ FOOTER_FORMAT_ALIASES,
229
+ );
230
+ const colorSource = config.components.footer.colorSource;
231
+ const iconMode = config.icons.mode;
232
+ const pathDisplay = config.components.footer.styles.starship.pathDisplay;
233
+ const formattedCwd = sanitizeEditorMetadataText(
234
+ formatCwdLabel(ctx.cwd, config.icons.cwd, {
235
+ mode: pathDisplay.mode,
236
+ depth: pathDisplay.depth,
237
+ repositoryRoot:
238
+ pathDisplay.mode === "repository" ? hooks.getRepositoryRoot?.(ctx.cwd) : undefined,
239
+ }),
240
+ );
241
+ const branch = sanitizeEditorMetadataText(state.branch ?? "") || undefined;
242
+ const gitStateLabel = sanitizeEditorMetadataText(state.gitStateLabel ?? "");
243
+ const commit = state.commit
244
+ ? {
245
+ ...state.commit,
246
+ oid: state.commit.oid ? sanitizeEditorMetadataText(state.commit.oid) || null : null,
247
+ tag: state.commit.tag ? sanitizeEditorMetadataText(state.commit.tag) || null : null,
248
+ }
249
+ : undefined;
250
+ const runtime = state.runtime
251
+ ? {
252
+ ...state.runtime,
253
+ name: sanitizeEditorMetadataText(state.runtime.name),
254
+ symbol: sanitizeEditorMetadataText(state.runtime.symbol),
255
+ version: sanitizeEditorMetadataText(state.runtime.version ?? ""),
256
+ }
257
+ : undefined;
258
+ const packageVersion = state.packageVersion
259
+ ? {
260
+ ...state.packageVersion,
261
+ version: sanitizeEditorMetadataText(state.packageVersion.version),
262
+ }
263
+ : undefined;
264
+ const separator = renderStyleForSource(
265
+ theme,
266
+ colorSource,
267
+ config.colors.separator,
268
+ separatorText[config.components.footer.styles.starship.separator],
269
+ );
270
+ const innerWidth = Math.max(1, width - 2);
271
+ const cwdLabel = renderStyleForSource(theme, colorSource, config.colors.cwd, formattedCwd);
272
+ const needsSessionName =
273
+ (config.components.footer.styles.starship.format
274
+ ? wideReferences.has("session_name")
275
+ : config.components.footer.styles.starship.segments.sessionName) ||
276
+ compactReferences.has("session_name");
277
+ const sessionName = needsSessionName
278
+ ? sanitizeExtensionStatusText(ctx.sessionManager.getSessionName() ?? "")
279
+ : "";
280
+ const sessionNameLabel = sessionName
281
+ ? renderStyleForSource(theme, colorSource, config.colors.sessionName, sessionName)
282
+ : "";
283
+ const builtInSessionNameLabel = sessionNameLabel ? `in ${sessionNameLabel}` : "";
284
+ const branchText = branch
285
+ ? formatGitBranchText(
286
+ branch,
287
+ config.components.footer.styles.starship.gitBranch.maxLength,
288
+ )
289
+ : undefined;
290
+ const { percent: contextPercent, contextWindow } = resolveContextUsage(
291
+ ctx,
292
+ hooks.getLiveContext?.(),
293
+ );
294
+ const contextLabel = buildContextDisplayLabel({
295
+ percent: contextPercent,
296
+ contextWindow,
297
+ style: config.components.footer.styles.starship.contextStyle,
298
+ asciiGauge: iconMode === "ascii",
299
+ });
300
+ const tier = contextColorTier(
301
+ contextPercent,
302
+ config.components.footer.styles.starship.contextThresholds,
303
+ );
304
+ const contextColor =
305
+ tier === "error"
306
+ ? config.colors.contextError
307
+ : tier === "warning"
308
+ ? config.colors.contextWarning
309
+ : config.colors.contextNormal;
310
+ const cacheReadLabel = state.cacheReadLabel
311
+ ? renderStyleForSource(theme, colorSource, config.colors.tokens, state.cacheReadLabel)
312
+ : "";
313
+ const cacheWriteLabel = state.cacheWriteLabel
314
+ ? renderStyleForSource(theme, colorSource, config.colors.tokens, state.cacheWriteLabel)
315
+ : "";
316
+ const subscriptionLabel = state.subscription
317
+ ? renderStyleForSource(theme, colorSource, config.colors.cost, "(sub)")
318
+ : "";
319
+ const autoCompactionLabel = state.autoCompaction
320
+ ? renderStyleForSource(theme, colorSource, contextColor, "(auto)")
321
+ : "";
322
+ const gitColor = (text: string) =>
323
+ renderStyleForSource(theme, colorSource, config.colors.gitBranch, text);
324
+ const gitStatusColor = (text: string) =>
325
+ renderStyleForSource(theme, colorSource, config.colors.gitStatus, text);
326
+ const gitIcon = config.icons.git ? gitColor(config.icons.git) : "";
327
+ const gitCounts = config.components.footer.styles.starship.segments.gitCounts;
328
+ const stashLabel =
329
+ state.stashed > 0
330
+ ? gitCounts
331
+ ? `${config.icons.stashed}${state.stashed}`
332
+ : config.icons.stashed
333
+ : "";
334
+ const allStatus = [
335
+ state.conflicted > 0 ? config.icons.conflicted : "",
336
+ stashLabel,
337
+ state.deleted > 0 ? config.icons.deleted : "",
338
+ state.renamed > 0 ? config.icons.renamed : "",
339
+ state.modified > 0 ? config.icons.modified : "",
340
+ state.typechanged > 0 ? config.icons.typechanged : "",
341
+ state.staged > 0 ? config.icons.staged : "",
342
+ state.untracked > 0 ? config.icons.untracked : "",
343
+ ].join("");
344
+ const aheadBehind = (() => {
345
+ if (state.ahead > 0 && state.behind > 0) {
346
+ return gitCounts
347
+ ? `${config.icons.ahead}${state.ahead}${config.icons.behind}${state.behind}`
348
+ : config.icons.diverged;
349
+ }
350
+ if (state.ahead > 0)
351
+ return gitCounts ? `${config.icons.ahead}${state.ahead}` : config.icons.ahead;
352
+ if (state.behind > 0)
353
+ return gitCounts ? `${config.icons.behind}${state.behind}` : config.icons.behind;
354
+ return "";
355
+ })();
356
+ const statusBlock =
357
+ allStatus || aheadBehind ? gitStatusColor(`[${allStatus}${aheadBehind}]`) : "";
358
+ const gitStateBlock = gitStateLabel ? gitStatusColor(gitStateLabel) : "";
359
+ const renderVariable = (name: string): string => {
360
+ const canonical = FOOTER_FORMAT_ALIASES[name] ?? name;
361
+ switch (canonical) {
362
+ case "cwd":
363
+ return cwdLabel;
364
+ case "session_name":
365
+ return sessionNameLabel;
366
+ case "git_branch":
367
+ return branchText
368
+ ? gitIcon
369
+ ? `${gitIcon} ${gitColor(branchText)}`
370
+ : gitColor(branchText)
371
+ : "";
372
+ case "git_status":
373
+ return statusBlock;
374
+ case "git_state":
375
+ return gitStateBlock;
376
+ case "runtime": {
377
+ if (!runtime) return "";
378
+ const symbol = resolveRuntimeSymbol(runtime.name, runtime.symbol, iconMode);
379
+ const label = runtime.version ? `${symbol} ${runtime.version}` : symbol;
380
+ return renderStyleForSource(theme, colorSource, runtime.style, label);
381
+ }
382
+ case "model":
383
+ return sanitizeExtensionStatusText(footerModelLabel);
384
+ case "provider":
385
+ return sanitizeExtensionStatusText(state.providerLabel);
386
+ case "session_duration":
387
+ return state.sessionStartEpoch
388
+ ? renderStyleForSource(
389
+ theme,
390
+ colorSource,
391
+ config.colors.sessionDuration,
392
+ buildSessionDurationLabel(state.sessionStartEpoch),
393
+ )
394
+ : "";
395
+ case "username":
396
+ return renderStyleForSource(
397
+ theme,
398
+ colorSource,
399
+ config.colors.username,
400
+ formatUsernameHostLabel(config.icons.username),
401
+ );
402
+ case "os":
403
+ return renderStyleForSource(
404
+ theme,
405
+ colorSource,
406
+ config.colors.os,
407
+ formatOsLabel(config.icons.os, iconMode),
408
+ );
409
+ case "time":
410
+ return renderStyleForSource(
411
+ theme,
412
+ colorSource,
413
+ config.colors.time,
414
+ formatTimeLabel(config.icons.time),
415
+ );
416
+ case "context":
417
+ return renderStyleForSource(theme, colorSource, contextColor, contextLabel);
418
+ case "tokens":
419
+ return renderStyleForSource(
420
+ theme,
421
+ colorSource,
422
+ config.colors.tokens,
423
+ state.tokenLabel,
424
+ );
425
+ case "cache_read":
426
+ return cacheReadLabel;
427
+ case "cache_write":
428
+ return cacheWriteLabel;
429
+ case "cost":
430
+ return renderStyleForSource(theme, colorSource, config.colors.cost, state.costLabel);
431
+ case "subscription":
432
+ return subscriptionLabel;
433
+ case "auto_compaction":
434
+ return autoCompactionLabel;
435
+ case "package":
436
+ return formatPackageVersionSegment(
437
+ theme,
438
+ packageVersion,
439
+ colorSource,
440
+ iconMode,
441
+ config.icons.package,
442
+ config.colors.packageVersion,
443
+ );
444
+ case "package_version":
445
+ return packageVersion?.version
446
+ ? renderStyleForSource(
447
+ theme,
448
+ colorSource,
449
+ config.colors.packageVersion,
450
+ packageVersion.version,
451
+ )
452
+ : "";
453
+ case "sep":
454
+ return renderStyleForSource(theme, colorSource, config.colors.separator, " | ");
455
+ case "git_commit":
456
+ return formatGitCommitSegment(
457
+ theme,
458
+ commit,
459
+ config.components.footer.styles.starship.gitCommit,
460
+ colorSource,
461
+ config.colors.gitCommit,
462
+ );
463
+ case "git_tag":
464
+ return config.components.footer.styles.starship.gitCommit.showTag && commit?.tag
465
+ ? renderStyleForSource(theme, colorSource, config.colors.gitCommit, commit.tag)
466
+ : "";
467
+ case "git_metrics":
468
+ return formatGitMetricsSegment(
469
+ theme,
470
+ state.metrics,
471
+ config.components.footer.styles.starship.gitMetrics,
472
+ colorSource,
473
+ config.colors.gitMetricsAdded,
474
+ config.colors.gitMetricsDeleted,
475
+ );
476
+ case "git_added":
477
+ return state.metrics
478
+ ? renderStyleForSource(
479
+ theme,
480
+ colorSource,
481
+ config.colors.gitMetricsAdded,
482
+ `+${state.metrics.added}`,
483
+ )
484
+ : "";
485
+ case "git_deleted":
486
+ return state.metrics
487
+ ? renderStyleForSource(
488
+ theme,
489
+ colorSource,
490
+ config.colors.gitMetricsDeleted,
491
+ `−${state.metrics.deleted}`,
492
+ )
493
+ : "";
494
+ default:
495
+ return "";
496
+ }
497
+ };
498
+ const branchParts: string[] = [];
499
+ if (config.components.footer.styles.starship.segments.gitBranch) {
500
+ if (branchText) {
501
+ branchParts.push("on", gitIcon, gitColor(branchText));
502
+ } else if (commit?.detached) {
503
+ // `HEAD` uses git-branch style; `(hash)` uses git-commit style
504
+ // (bold green) per Starship `git_commit` format.
505
+ branchParts.push("on", gitIcon, gitColor("HEAD"));
506
+ if (config.components.footer.styles.starship.segments.gitCommit && commit.oid) {
507
+ const shortHash = commit.oid.slice(
508
+ 0,
509
+ config.components.footer.styles.starship.gitCommit.hashLength,
510
+ );
511
+ const tag =
512
+ config.components.footer.styles.starship.gitCommit.showTag && commit.tag
513
+ ? commit.tag
514
+ : "";
515
+ const inner = [shortHash, tag].filter(Boolean).join(" ");
516
+ branchParts.push(
517
+ renderStyleForSource(theme, colorSource, config.colors.gitCommit, `(${inner})`),
518
+ );
519
+ }
520
+ }
521
+ }
522
+ const gitStatusParts =
523
+ config.components.footer.styles.starship.segments.gitStatus && statusBlock
524
+ ? [statusBlock]
525
+ : [];
526
+ const showGitState =
527
+ config.components.footer.styles.starship.segments.gitBranch ||
528
+ config.components.footer.styles.starship.segments.gitStatus;
529
+ const gitStateParts = showGitState && gitStateBlock ? [gitStateBlock] : [];
530
+ const branchLabel = [...branchParts, ...gitStatusParts, ...gitStateParts]
531
+ .filter(Boolean)
532
+ .join(" ");
533
+ const runtimeLabel = config.components.footer.styles.starship.segments.runtime
534
+ ? formatRuntimeSegment(theme, runtime, config.colors.runtimePrefix, colorSource, iconMode)
535
+ : "";
536
+ const packageVersionLabel = config.components.footer.styles.starship.segments.packageVersion
537
+ ? formatPackageVersionSegment(
538
+ theme,
539
+ packageVersion,
540
+ colorSource,
541
+ iconMode,
542
+ config.icons.package,
543
+ config.colors.packageVersion,
544
+ )
545
+ : "";
546
+ // Skip standalone gitCommit when hash is already folded into the
547
+ // branch display on detached HEAD.
548
+ const hashFoldedIntoBranch =
549
+ commit?.detached && config.components.footer.styles.starship.segments.gitBranch;
550
+ const gitCommitLabel =
551
+ config.components.footer.styles.starship.segments.gitCommit && !hashFoldedIntoBranch
552
+ ? formatGitCommitSegment(
553
+ theme,
554
+ commit,
555
+ config.components.footer.styles.starship.gitCommit,
556
+ colorSource,
557
+ config.colors.gitCommit,
558
+ )
559
+ : "";
560
+ const gitMetricsLabel = config.components.footer.styles.starship.segments.gitMetrics
561
+ ? formatGitMetricsSegment(
562
+ theme,
563
+ state.metrics,
564
+ config.components.footer.styles.starship.gitMetrics,
565
+ colorSource,
566
+ config.colors.gitMetricsAdded,
567
+ config.colors.gitMetricsDeleted,
568
+ )
569
+ : "";
570
+
571
+ const sessionDurationSegment = (() => {
572
+ if (
573
+ !config.components.footer.styles.starship.segments.sessionDuration ||
574
+ !state.sessionStartEpoch
575
+ )
576
+ return "";
577
+ const timeLabel = buildSessionDurationLabel(state.sessionStartEpoch);
578
+ const prefix = renderStyleForSource(theme, colorSource, "", "up for");
579
+ const time = renderStyleForSource(
580
+ theme,
581
+ colorSource,
582
+ config.colors.sessionDuration,
583
+ timeLabel,
584
+ );
585
+ return `${prefix} ${time}`;
586
+ })();
587
+ const usernameSegment = config.components.footer.styles.starship.segments.username
588
+ ? renderStyleForSource(
589
+ theme,
590
+ colorSource,
591
+ config.colors.username,
592
+ formatUsernameHostLabel(config.icons.username),
593
+ )
594
+ : "";
595
+ const osSegment = config.components.footer.styles.starship.segments.os
596
+ ? renderStyleForSource(
597
+ theme,
598
+ colorSource,
599
+ config.colors.os,
600
+ formatOsLabel(config.icons.os, iconMode),
601
+ )
602
+ : "";
603
+ const left = [
604
+ osSegment,
605
+ usernameSegment,
606
+ config.components.footer.styles.starship.segments.cwd ? cwdLabel : "",
607
+ config.components.footer.styles.starship.segments.sessionName
608
+ ? builtInSessionNameLabel
609
+ : "",
610
+ branchLabel,
611
+ gitCommitLabel,
612
+ gitMetricsLabel,
613
+ packageVersionLabel,
614
+ runtimeLabel,
615
+ sessionDurationSegment,
616
+ ]
617
+ .filter(Boolean)
618
+ .join(" ");
619
+
620
+ const modelInfoSegment = config.components.footer.styles.starship.segments.modelInfo
621
+ ? composeModelInfoLabel(
622
+ sanitizeExtensionStatusText(footerModelLabel),
623
+ sanitizeExtensionStatusText(state.providerLabel),
624
+ )
625
+ : "";
626
+ const timeSegment = config.components.footer.styles.starship.segments.time
627
+ ? renderStyleForSource(
628
+ theme,
629
+ colorSource,
630
+ config.colors.time,
631
+ formatTimeLabel(config.icons.time),
632
+ )
633
+ : "";
634
+ const builtInContextLabel = [
635
+ renderStyleForSource(theme, colorSource, contextColor, contextLabel),
636
+ autoCompactionLabel,
637
+ ]
638
+ .filter(Boolean)
639
+ .join(" ");
640
+ const builtInTokenLabel = [
641
+ renderStyleForSource(theme, colorSource, config.colors.tokens, state.tokenLabel),
642
+ cacheReadLabel,
643
+ cacheWriteLabel,
644
+ ]
645
+ .filter(Boolean)
646
+ .join(" ");
647
+ const builtInCostLabel = [
648
+ renderStyleForSource(theme, colorSource, config.colors.cost, state.costLabel),
649
+ subscriptionLabel,
650
+ ]
651
+ .filter(Boolean)
652
+ .join(" ");
653
+ const right = [
654
+ modelInfoSegment,
655
+ config.components.footer.styles.starship.segments.context ? builtInContextLabel : "",
656
+ config.components.footer.styles.starship.segments.tokens ? builtInTokenLabel : "",
657
+ config.components.footer.styles.starship.segments.cost ? builtInCostLabel : "",
658
+ timeSegment,
659
+ ]
660
+ .filter(Boolean)
661
+ .join(separator);
662
+
663
+ let contentLeft = left;
664
+ let contentMiddle = "";
665
+ let contentRight = right;
666
+ if (config.components.footer.styles.starship.format) {
667
+ const {
668
+ left: fmtLeft,
669
+ middle: fmtMiddle,
670
+ right: fmtRight,
671
+ } = renderFormatSplit(wideFormatTokens, renderVariable);
672
+ contentLeft = stripOrphanSeparators(fmtLeft);
673
+ contentMiddle = stripOrphanSeparators(fmtMiddle);
674
+ contentRight = stripOrphanSeparators(fmtRight);
675
+ }
676
+
677
+ const extensionStatuses = collectExtensionStatusSegments(
678
+ footerData.getExtensionStatuses(),
679
+ config,
680
+ );
681
+ const renderExtensionStatus = (segment: ExtensionStatusSegment) =>
682
+ segment.colorMode === "original"
683
+ ? segment.text
684
+ : renderStyleForSource(theme, colorSource, config.colors.extensionStatus, segment.text);
685
+ const extensionLeftSegments = extensionStatuses.left.map(renderExtensionStatus);
686
+ const extensionMiddleSegments = extensionStatuses.middle.map(renderExtensionStatus);
687
+ const extensionRightSegments = extensionStatuses.right.map(renderExtensionStatus);
688
+ const middleSegments = contentMiddle
689
+ ? [contentMiddle, ...extensionMiddleSegments]
690
+ : extensionMiddleSegments;
691
+ const renderLegacyContent = () =>
692
+ composeFooterContent(
693
+ contentLeft,
694
+ contentRight,
695
+ extensionLeftSegments,
696
+ middleSegments,
697
+ extensionRightSegments,
698
+ separator,
699
+ innerWidth,
700
+ );
701
+ const frameRows = (rows: string[]) =>
702
+ rows.map((row) => {
703
+ const framed = width > 2 ? ` ${truncateToWidth(row, width - 2, "")} ` : row;
704
+ return truncateToWidth(framed, width, "");
705
+ });
706
+
707
+ if (!config.components.footer.styles.starship.responsive)
708
+ return frameRows([renderLegacyContent()]);
709
+
710
+ const fullZones = {
711
+ left: appendStatusArea(
712
+ contentLeft,
713
+ joinStatusTexts(extensionLeftSegments, separator),
714
+ separator,
715
+ ),
716
+ middle: appendStatusArea(
717
+ contentMiddle,
718
+ joinStatusTexts(extensionMiddleSegments, separator),
719
+ separator,
720
+ ),
721
+ right: prependStatusArea(
722
+ contentRight,
723
+ joinStatusTexts(extensionRightSegments, separator),
724
+ separator,
725
+ ),
726
+ };
727
+ if (fullFooterFitsAligned(fullZones, innerWidth)) {
728
+ return frameRows([renderLegacyContent()]);
729
+ }
730
+
731
+ const reflowed = reflowFullFooter(fullZones, innerWidth);
732
+ if (reflowed) return frameRows(reflowed);
733
+
734
+ const chunkBudget = compactChunkBudget(innerWidth);
735
+ const compactCwdLabel = truncateToWidth(cwdLabel, chunkBudget, "…");
736
+ const compactSessionNameLabel = truncateToWidth(
737
+ sessionNameLabel,
738
+ Math.max(1, chunkBudget - visibleWidth("in ")),
739
+ "…",
740
+ );
741
+ const compactBranchBudget = Math.max(
742
+ 1,
743
+ chunkBudget - visibleWidth("on ") - (statusBlock ? visibleWidth(statusBlock) + 1 : 0),
744
+ );
745
+ const compactBranchLabel = truncateToWidth(
746
+ renderVariable("git_branch"),
747
+ compactBranchBudget,
748
+ "…",
749
+ );
750
+ const renderCompactVariable = (name: string): string => {
751
+ const canonical = FOOTER_FORMAT_ALIASES[name] ?? name;
752
+ switch (canonical) {
753
+ case "cwd":
754
+ return compactCwdLabel;
755
+ case "session_name":
756
+ return compactSessionNameLabel;
757
+ case "git_branch":
758
+ return compactBranchLabel;
759
+ default:
760
+ return renderVariable(name);
761
+ }
762
+ };
763
+ const compactChunks: Array<{
764
+ text: string;
765
+ boundary: "space" | "separator";
766
+ }> = [];
767
+ for (const chunk of compileCompactFormat(compactFormatTokens)) {
768
+ if (chunk.kind === "extensions") {
769
+ const statuses = [
770
+ ...extensionLeftSegments,
771
+ ...extensionMiddleSegments,
772
+ ...extensionRightSegments,
773
+ ];
774
+ for (const [index, text] of statuses.entries()) {
775
+ compactChunks.push({
776
+ text,
777
+ boundary: index === 0 ? chunk.boundary : "space",
778
+ });
779
+ }
780
+ continue;
781
+ }
782
+ let rendered = stripOrphanSeparators(
783
+ renderFormatTokens(chunk.tokens, renderCompactVariable),
784
+ );
785
+ const references = collectFooterFormatReferences(chunk.tokens, FOOTER_FORMAT_ALIASES);
786
+ if (["cwd", "session_name", "git_branch"].some((name) => references.has(name))) {
787
+ rendered = truncateToWidth(rendered, chunkBudget, "…");
788
+ }
789
+ if (rendered) compactChunks.push({ text: rendered, boundary: chunk.boundary });
790
+ }
791
+ return frameRows(
792
+ packCompactChunks(
793
+ compactChunks,
794
+ innerWidth,
795
+ config.components.footer.styles.starship.compactMaxLines,
796
+ renderVariable("sep"),
797
+ ),
798
+ );
799
+ },
800
+ };
801
+ });
802
+ }
803
+
804
+ export function installHiddenFooter(ctx: ExtensionContext, onDispose?: () => void): void {
805
+ ctx.ui.setFooter(() => ({
806
+ dispose: onDispose,
807
+ invalidate() {},
808
+ render(): string[] {
809
+ return [];
810
+ },
811
+ }));
812
+ }