@selesai/code 0.9.14 → 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 (263) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +79 -29
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +3 -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/docs/plans/auto-model-routing-extension.md +490 -0
  193. package/package.json +3 -3
  194. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  195. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  196. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  197. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  198. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  199. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  200. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  201. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  202. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  203. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  204. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  205. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  206. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  207. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  208. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  209. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  210. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  211. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  212. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  213. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  214. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  215. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  216. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  217. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  218. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  219. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  220. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  221. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  222. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  223. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  224. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  225. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  226. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  227. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  228. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  229. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  230. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  231. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  232. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  233. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  234. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  235. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  236. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  237. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  238. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  239. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  240. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  241. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  242. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  243. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  244. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  245. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  246. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  247. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  248. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  249. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  250. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  251. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  252. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  253. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  254. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  255. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  256. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  257. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  258. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  259. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  260. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  261. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  262. package/dist/extensions/preview-tools-disabled.ts +0 -11
  263. 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
+ }