@selesai/code 0.9.15 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +2 -2
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +2 -4
  6. package/dist/extensions/pi-zentui/.editorconfig +12 -0
  7. package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
  8. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  9. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
  10. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
  11. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  12. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
  13. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
  14. package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
  15. package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
  16. package/dist/extensions/pi-zentui/.github/release.yml +18 -0
  17. package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
  18. package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
  19. package/dist/extensions/pi-zentui/AGENTS.md +107 -0
  20. package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
  21. package/dist/extensions/pi-zentui/LICENSE +21 -0
  22. package/dist/extensions/pi-zentui/README.md +188 -0
  23. package/dist/extensions/pi-zentui/SECURITY.md +18 -0
  24. package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
  25. package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
  26. package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
  27. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
  28. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
  29. package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
  30. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
  31. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
  32. package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
  33. package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
  34. package/dist/extensions/pi-zentui/biome.json +31 -0
  35. package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
  36. package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
  37. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
  38. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
  39. package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
  40. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
  41. package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
  42. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
  43. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
  44. package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
  45. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
  46. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
  47. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
  48. package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
  49. package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
  50. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
  51. package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
  52. package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
  53. package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
  54. package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
  55. package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
  56. package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
  57. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
  58. package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
  59. package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
  60. package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
  61. package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
  62. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
  63. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
  64. package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
  65. package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
  66. package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
  67. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
  68. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
  69. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
  70. package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
  71. package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
  72. package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
  73. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
  74. package/dist/extensions/pi-zentui/package-lock.json +5311 -0
  75. package/dist/extensions/pi-zentui/package.json +60 -0
  76. package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
  77. package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
  78. package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
  79. package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
  80. package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
  81. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
  82. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
  83. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
  84. package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
  85. package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
  86. package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
  87. package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
  88. package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
  89. package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
  90. package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
  91. package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
  92. package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
  93. package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
  94. package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
  95. package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
  96. package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
  97. package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
  98. package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
  99. package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
  100. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
  101. package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
  102. package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
  103. package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
  104. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
  105. package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
  106. package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
  107. package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
  108. package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
  109. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
  110. package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
  111. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
  112. package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
  113. package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
  114. package/dist/extensions/pi-zentui/tsconfig.json +16 -0
  115. package/dist/extensions/tps.test.ts +38 -0
  116. package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
  117. package/dist/modes/interactive/components/settings-selector.d.ts +1 -1
  118. package/dist/modes/interactive/interactive-mode.js +1 -1
  119. package/dist/skills/design-references/SKILL.md +31 -0
  120. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  121. package/dist/themes/adventure.json +84 -0
  122. package/dist/themes/adwaita-dark.json +84 -0
  123. package/dist/themes/arcoiris.json +84 -0
  124. package/dist/themes/arthur.json +84 -0
  125. package/dist/themes/atom.json +84 -0
  126. package/dist/themes/aura.json +84 -0
  127. package/dist/themes/black-metal-bathory.json +84 -0
  128. package/dist/themes/black-metal-burzum.json +84 -0
  129. package/dist/themes/black-metal-khold.json +84 -0
  130. package/dist/themes/box.json +84 -0
  131. package/dist/themes/brogrammer.json +84 -0
  132. package/dist/themes/carbonfox.json +84 -0
  133. package/dist/themes/catppuccin-mocha.json +84 -0
  134. package/dist/themes/citruszest.json +84 -0
  135. package/dist/themes/cursor-dark.json +84 -0
  136. package/dist/themes/cutie-pro.json +84 -0
  137. package/dist/themes/dark-modern.json +84 -0
  138. package/dist/themes/dark-pastel.json +84 -0
  139. package/dist/themes/dimmed-monokai.json +84 -0
  140. package/dist/themes/doom-peacock.json +84 -0
  141. package/dist/themes/dracula-plus.json +84 -0
  142. package/dist/themes/dracula.json +80 -0
  143. package/dist/themes/earthsong.json +84 -0
  144. package/dist/themes/everforest-dark-hard.json +84 -0
  145. package/dist/themes/everforest-dark.json +80 -0
  146. package/dist/themes/fahrenheit.json +84 -0
  147. package/dist/themes/flatland.json +84 -0
  148. package/dist/themes/flexoki-dark.json +84 -0
  149. package/dist/themes/front-end-delight.json +84 -0
  150. package/dist/themes/fun-forrest.json +84 -0
  151. package/dist/themes/galizur.json +84 -0
  152. package/dist/themes/github-dark-colorblind.json +84 -0
  153. package/dist/themes/github-dark-high-contrast.json +84 -0
  154. package/dist/themes/glacier.json +84 -0
  155. package/dist/themes/gruber-darker.json +84 -0
  156. package/dist/themes/gruvbox-dark-hard.json +84 -0
  157. package/dist/themes/gruvbox-dark.json +84 -0
  158. package/dist/themes/gruvbox-material.json +84 -0
  159. package/dist/themes/guezwhoz.json +84 -0
  160. package/dist/themes/hacktober.json +84 -0
  161. package/dist/themes/hardcore.json +84 -0
  162. package/dist/themes/havn-skumring.json +84 -0
  163. package/dist/themes/ic-orange-ppl.json +84 -0
  164. package/dist/themes/iterm2-smoooooth.json +84 -0
  165. package/dist/themes/iterm2-tango-dark.json +84 -0
  166. package/dist/themes/japanesque.json +84 -0
  167. package/dist/themes/jellybeans.json +84 -0
  168. package/dist/themes/kanagawa-wave.json +84 -0
  169. package/dist/themes/kurokula.json +84 -0
  170. package/dist/themes/later-this-evening.json +84 -0
  171. package/dist/themes/lovelace.json +84 -0
  172. package/dist/themes/material-darker.json +84 -0
  173. package/dist/themes/matte-black.json +84 -0
  174. package/dist/themes/mellow.json +84 -0
  175. package/dist/themes/miasma.json +84 -0
  176. package/dist/themes/nord.json +80 -0
  177. package/dist/themes/nvim-dark.json +84 -0
  178. package/dist/themes/one-dark.json +80 -0
  179. package/dist/themes/popping-and-locking.json +84 -0
  180. package/dist/themes/rose-pine.json +80 -0
  181. package/dist/themes/sea-shells.json +84 -0
  182. package/dist/themes/sleepy-hollow.json +84 -0
  183. package/dist/themes/smyck.json +84 -0
  184. package/dist/themes/solarized-dark.json +80 -0
  185. package/dist/themes/tokyo-night.json +80 -0
  186. package/dist/themes/tomorrow-night-bright.json +84 -0
  187. package/dist/themes/tomorrow-night-burns.json +84 -0
  188. package/dist/themes/tomorrow-night.json +84 -0
  189. package/dist/themes/twilight.json +84 -0
  190. package/dist/themes/vague.json +84 -0
  191. package/dist/themes/vesper.json +84 -0
  192. package/dist/themes/xcode-dark-hc.json +84 -0
  193. package/docs/custom-provider.md +0 -1
  194. package/package.json +3 -3
  195. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  196. package/dist/extensions/auto-model/classifier.ts +0 -23
  197. package/dist/extensions/auto-model/config.ts +0 -51
  198. package/dist/extensions/auto-model/index.ts +0 -22
  199. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  200. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  201. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  202. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  203. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  204. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  205. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  206. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  207. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  208. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  209. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  210. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  211. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  212. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  213. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  214. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  215. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  216. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  217. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  218. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  219. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  220. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  221. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  222. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  223. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  224. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  225. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  226. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  227. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  228. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  229. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  230. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  231. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  232. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  233. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  234. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  235. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  236. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  237. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  238. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  239. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  240. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  241. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  242. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  243. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  244. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  245. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  246. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  247. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  248. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  249. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  250. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  251. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  252. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  253. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  254. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  255. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  256. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  257. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  258. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  259. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  260. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  261. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  262. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  263. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  264. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  265. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  266. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  267. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  268. package/dist/extensions/preview-tools-disabled.ts +0 -11
  269. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,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
+ }