@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,431 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { FOOTER_FORMAT_ALIASES } from "../extensions/zentui/config";
3
+ import {
4
+ collectFooterFormatReferences,
5
+ compileCompactFormat,
6
+ joinNonEmpty,
7
+ parseFooterFormat,
8
+ renderFormatSplit,
9
+ renderFormatTokens,
10
+ stripOrphanSeparators,
11
+ } from "../extensions/zentui/footer-format";
12
+
13
+ describe("parseFooterFormat", () => {
14
+ it("returns empty array for empty string", () => {
15
+ expect(parseFooterFormat("")).toEqual([]);
16
+ });
17
+
18
+ it("parses a single variable", () => {
19
+ expect(parseFooterFormat("$cwd")).toEqual([{ kind: "var", name: "cwd" }]);
20
+ });
21
+
22
+ it("parses model-info variables independently", () => {
23
+ expect(parseFooterFormat("$model $provider")).toEqual([
24
+ { kind: "var", name: "model" },
25
+ { kind: "text", value: " " },
26
+ { kind: "var", name: "provider" },
27
+ ]);
28
+ });
29
+
30
+ it("parses $package and $package_version as package-version vars", () => {
31
+ expect(parseFooterFormat("$package")).toEqual([{ kind: "var", name: "package" }]);
32
+ expect(parseFooterFormat("$package_version")).toEqual([
33
+ { kind: "var", name: "package_version" },
34
+ ]);
35
+ });
36
+
37
+ it("parses $git_commit, $commit, $git_tag, and $tag as git-commit vars", () => {
38
+ expect(parseFooterFormat("$git_commit")).toEqual([{ kind: "var", name: "git_commit" }]);
39
+ expect(parseFooterFormat("$commit")).toEqual([{ kind: "var", name: "commit" }]);
40
+ expect(parseFooterFormat("$git_tag")).toEqual([{ kind: "var", name: "git_tag" }]);
41
+ expect(parseFooterFormat("$tag")).toEqual([{ kind: "var", name: "tag" }]);
42
+ });
43
+
44
+ it("parses git-metrics vars", () => {
45
+ expect(parseFooterFormat("$git_metrics")).toEqual([{ kind: "var", name: "git_metrics" }]);
46
+ expect(parseFooterFormat("$git_added")).toEqual([{ kind: "var", name: "git_added" }]);
47
+ expect(parseFooterFormat("$git_deleted")).toEqual([{ kind: "var", name: "git_deleted" }]);
48
+ });
49
+
50
+ it("parses braced variables", () => {
51
+ const braced = "${" + "git_branch}";
52
+ expect(parseFooterFormat(braced)).toEqual([{ kind: "var", name: "git_branch" }]);
53
+ });
54
+
55
+ it("parses text and variables mixed", () => {
56
+ expect(parseFooterFormat("$cwd on $git_branch")).toEqual([
57
+ { kind: "var", name: "cwd" },
58
+ { kind: "text", value: " on " },
59
+ { kind: "var", name: "git_branch" },
60
+ ]);
61
+ });
62
+
63
+ it("parses the fill token", () => {
64
+ expect(parseFooterFormat("$fill")).toEqual([{ kind: "fill" }]);
65
+ });
66
+
67
+ it("preserves literal text exactly including multiple spaces", () => {
68
+ const tokens = parseFooterFormat("$cwd on $git_branch");
69
+ expect(tokens).toEqual([
70
+ { kind: "var", name: "cwd" },
71
+ { kind: "text", value: " on " },
72
+ { kind: "var", name: "git_branch" },
73
+ ]);
74
+ });
75
+
76
+ it("preserves leading and trailing text", () => {
77
+ const tokens = parseFooterFormat("prefix $cwd suffix");
78
+ expect(tokens).toEqual([
79
+ { kind: "text", value: "prefix " },
80
+ { kind: "var", name: "cwd" },
81
+ { kind: "text", value: " suffix" },
82
+ ]);
83
+ });
84
+
85
+ it("treats unknown variable names as var tokens", () => {
86
+ expect(parseFooterFormat("$nope")).toEqual([{ kind: "var", name: "nope" }]);
87
+ });
88
+
89
+ it("handles mixed fill, vars, and text", () => {
90
+ expect(parseFooterFormat("$cwd on $git_branch $fill $cost $time")).toEqual([
91
+ { kind: "var", name: "cwd" },
92
+ { kind: "text", value: " on " },
93
+ { kind: "var", name: "git_branch" },
94
+ { kind: "text", value: " " },
95
+ { kind: "fill" },
96
+ { kind: "text", value: " " },
97
+ { kind: "var", name: "cost" },
98
+ { kind: "text", value: " " },
99
+ { kind: "var", name: "time" },
100
+ ]);
101
+ });
102
+ });
103
+
104
+ describe("renderFormatSplit", () => {
105
+ const renderVar = (name: string): string => {
106
+ const map: Record<string, string> = {
107
+ cwd: "DIR",
108
+ session_name: "Session name",
109
+ git_branch: "BRANCH",
110
+ cost: "$0",
111
+ time: "12:00",
112
+ unknown: "",
113
+ };
114
+ return map[name] ?? "";
115
+ };
116
+
117
+ it("splits at the first fill token into left and right", () => {
118
+ const tokens = parseFooterFormat("$cwd$fill$cost $time");
119
+ const { left, middle, right } = renderFormatSplit(tokens, renderVar);
120
+ expect(left).toBe("DIR");
121
+ expect(middle).toBe("");
122
+ expect(right).toBe("$0 12:00");
123
+ });
124
+
125
+ it("puts everything in left when there is no fill", () => {
126
+ const tokens = parseFooterFormat("$cwd on $git_branch");
127
+ const { left, middle, right } = renderFormatSplit(tokens, renderVar);
128
+ expect(left).toBe("DIR on BRANCH");
129
+ expect(middle).toBe("");
130
+ expect(right).toBe("");
131
+ });
132
+
133
+ it("centers content between two fills in the middle zone", () => {
134
+ const tokens = parseFooterFormat("$cwd$fill$cost$fill$time");
135
+ const { left, middle, right } = renderFormatSplit(tokens, renderVar);
136
+ expect(left).toBe("DIR");
137
+ expect(middle).toBe("$0");
138
+ expect(right).toBe("12:00");
139
+ });
140
+
141
+ it("ignores fill tokens beyond the first two", () => {
142
+ const tokens = parseFooterFormat("$cwd$fill$cost$fill$time$fill");
143
+ const { left, middle, right } = renderFormatSplit(tokens, renderVar);
144
+ expect(left).toBe("DIR");
145
+ expect(middle).toBe("$0");
146
+ expect(right).toBe("12:00");
147
+ });
148
+
149
+ it("renders session-name variables in plain and braced forms", () => {
150
+ expect(renderFormatSplit(parseFooterFormat("$session_name"), renderVar).left).toBe(
151
+ "Session name",
152
+ );
153
+ const bracedSessionName = "${" + "session_name}";
154
+ expect(renderFormatSplit(parseFooterFormat(bracedSessionName), renderVar).left).toBe(
155
+ "Session name",
156
+ );
157
+ });
158
+
159
+ it("renders unknown variables as empty string", () => {
160
+ const tokens = parseFooterFormat("$nope");
161
+ const { left } = renderFormatSplit(tokens, renderVar);
162
+ expect(left).toBe("");
163
+ });
164
+
165
+ it("renders literal text verbatim", () => {
166
+ const tokens = parseFooterFormat("hello $cwd world");
167
+ const { left } = renderFormatSplit(tokens, renderVar);
168
+ expect(left).toBe("hello DIR world");
169
+ });
170
+
171
+ it("handles empty token list", () => {
172
+ const { left, middle, right } = renderFormatSplit([], renderVar);
173
+ expect(left).toBe("");
174
+ expect(middle).toBe("");
175
+ expect(right).toBe("");
176
+ });
177
+
178
+ it("handles fill at the start", () => {
179
+ const tokens = parseFooterFormat("$fill$cost");
180
+ const { left, middle, right } = renderFormatSplit(tokens, renderVar);
181
+ expect(left).toBe("");
182
+ expect(middle).toBe("");
183
+ expect(right).toBe("$0");
184
+ });
185
+
186
+ it("handles fill at the end", () => {
187
+ const tokens = parseFooterFormat("$cwd$fill");
188
+ const { left, middle, right } = renderFormatSplit(tokens, renderVar);
189
+ expect(left).toBe("DIR");
190
+ expect(middle).toBe("");
191
+ expect(right).toBe("");
192
+ });
193
+ });
194
+
195
+ describe("conditional groups", () => {
196
+ const renderVar = (name: string): string => {
197
+ const map: Record<string, string> = {
198
+ cwd: "DIR",
199
+ git_branch: "",
200
+ git_status: "[!]",
201
+ cost: "$0",
202
+ runtime: "",
203
+ };
204
+ return map[name] ?? "";
205
+ };
206
+
207
+ it("parses group tokens", () => {
208
+ expect(parseFooterFormat("($git_branch)")).toEqual([
209
+ { kind: "group", tokens: [{ kind: "var", name: "git_branch" }] },
210
+ ]);
211
+ });
212
+
213
+ it("drops a group when all vars are empty", () => {
214
+ const tokens = parseFooterFormat("($git_branch)");
215
+ expect(renderFormatSplit(tokens, renderVar).left).toBe("");
216
+ });
217
+
218
+ it("drops surrounding literals inside an empty group", () => {
219
+ const tokens = parseFooterFormat("($git_branch on )$cwd");
220
+ expect(renderFormatSplit(tokens, renderVar).left).toBe("DIR");
221
+ });
222
+
223
+ it("keeps a group when any var is non-empty", () => {
224
+ const tokens = parseFooterFormat("($git_status)");
225
+ expect(renderFormatSplit(tokens, renderVar).left).toBe("[!]");
226
+ });
227
+
228
+ it("supports nested groups", () => {
229
+ const tokens = parseFooterFormat("(($git_branch) via $runtime)$cwd");
230
+ expect(renderFormatSplit(tokens, renderVar).left).toBe("DIR");
231
+ });
232
+
233
+ it("still splits on fill outside groups", () => {
234
+ const tokens = parseFooterFormat("$cwd$fill($cost)");
235
+ const { left, right } = renderFormatSplit(tokens, renderVar);
236
+ expect(left).toBe("DIR");
237
+ expect(right).toBe("$0");
238
+ });
239
+
240
+ it("shows text-only groups", () => {
241
+ const tokens = parseFooterFormat("(literal)$cwd");
242
+ expect(renderFormatSplit(tokens, renderVar).left).toBe("literalDIR");
243
+ });
244
+
245
+ it("treats unmatched top-level ) as literal and keeps trailing tokens", () => {
246
+ const tokens = parseFooterFormat("$cwd) $cost");
247
+ expect(tokens).toEqual([
248
+ { kind: "var", name: "cwd" },
249
+ { kind: "text", value: ") " },
250
+ { kind: "var", name: "cost" },
251
+ ]);
252
+ expect(renderFormatSplit(tokens, renderVar).left).toBe("DIR) $0");
253
+ });
254
+
255
+ it("keeps nested groups working when trailing tokens follow a closed group", () => {
256
+ const tokens = parseFooterFormat("($git_status)$cwd");
257
+ expect(renderFormatSplit(tokens, renderVar).left).toBe("[!]DIR");
258
+ });
259
+ it("drops a conditional session-name group when the name is empty", () => {
260
+ const noSessionName = (name: string) => (name === "sep" ? " | " : "");
261
+ const raw = renderFormatSplit(parseFooterFormat("($sep$session_name)"), noSessionName).left;
262
+ expect(stripOrphanSeparators(raw)).toBe("");
263
+ });
264
+ });
265
+
266
+ describe("joinNonEmpty", () => {
267
+ it("joins only non-empty parts", () => {
268
+ expect(joinNonEmpty(["a", "", "b", ""], " | ")).toBe("a | b");
269
+ expect(joinNonEmpty(["", ""], " | ")).toBe("");
270
+ expect(joinNonEmpty(["only"], " | ")).toBe("only");
271
+ });
272
+ });
273
+
274
+ describe("compact footer format", () => {
275
+ it("splits top-level wrap variables and records incoming boundary kinds", () => {
276
+ const expected = [
277
+ { kind: "tokens", tokens: [{ kind: "var", name: "cwd" }], boundary: "space" },
278
+ { kind: "tokens", tokens: [{ kind: "var", name: "context" }], boundary: "space" },
279
+ ];
280
+ expect(compileCompactFormat(parseFooterFormat("$cwd$wrap$context"))).toEqual(expected);
281
+ expect(compileCompactFormat(parseFooterFormat("$cwd$" + "{wrap}$context"))).toEqual(expected);
282
+ expect(
283
+ compileCompactFormat(parseFooterFormat("$context$wrap_sep$tokens")).map(
284
+ ({ kind, boundary }) => ({ kind, boundary }),
285
+ ),
286
+ ).toEqual([
287
+ { kind: "tokens", boundary: "space" },
288
+ { kind: "tokens", boundary: "separator" },
289
+ ]);
290
+ });
291
+
292
+ it("keeps nested boundaries empty without splitting and leaves wide rendering unchanged", () => {
293
+ const [nested] = compileCompactFormat(
294
+ parseFooterFormat("$cwd(foo $wrap $context bar $wrap_sep $tokens)"),
295
+ );
296
+ expect(nested?.kind).toBe("tokens");
297
+ if (nested?.kind !== "tokens") return;
298
+ expect(
299
+ renderFormatTokens(
300
+ nested.tokens,
301
+ (name) => ({ cwd: "DIR", context: "CTX", tokens: "TOK" })[name] ?? "",
302
+ ),
303
+ ).toBe("DIRfoo CTX bar TOK");
304
+ expect(
305
+ renderFormatSplit(
306
+ parseFooterFormat("$cwd$wrap$context$wrap_sep$tokens"),
307
+ (name) => ({ cwd: "DIR", context: "CTX", tokens: "TOK" })[name] ?? "",
308
+ ).left,
309
+ ).toBe("DIRCTXTOK");
310
+ });
311
+
312
+ it("ignores fill and recognizes only a standalone extensions chunk", () => {
313
+ const chunks = compileCompactFormat(
314
+ parseFooterFormat("$cwd$fill$wrap $extensions $wrap prefix $extensions$wrap($extensions)"),
315
+ );
316
+ expect(chunks.map((chunk) => chunk.kind)).toEqual(["tokens", "extensions", "tokens", "tokens"]);
317
+ });
318
+
319
+ it("renders conditional chunks and preserves incoming boundaries around empty content", () => {
320
+ const chunks = compileCompactFormat(
321
+ parseFooterFormat("$cwd$wrap_sep$missing$wrap(on $git_branch) $git_status$wrap_sep$context"),
322
+ );
323
+ expect(chunks.map((chunk) => chunk.boundary)).toEqual([
324
+ "space",
325
+ "separator",
326
+ "space",
327
+ "separator",
328
+ ]);
329
+ const rendered = chunks.flatMap((chunk) =>
330
+ chunk.kind === "tokens"
331
+ ? [
332
+ {
333
+ text: renderFormatTokens(chunk.tokens, (name) =>
334
+ name === "git_status" ? "[!]" : "",
335
+ ).trim(),
336
+ boundary: chunk.boundary,
337
+ },
338
+ ]
339
+ : [],
340
+ );
341
+ expect(rendered).toEqual([
342
+ { text: "", boundary: "space" },
343
+ { text: "", boundary: "separator" },
344
+ { text: "[!]", boundary: "space" },
345
+ { text: "", boundary: "separator" },
346
+ ]);
347
+ });
348
+
349
+ it("collects canonical data references and excludes layout variables", () => {
350
+ expect(
351
+ collectFooterFormatReferences(
352
+ parseFooterFormat(
353
+ "$directory $branch $model $provider $wrap $wrap_sep $extensions $fill $duration",
354
+ ),
355
+ FOOTER_FORMAT_ALIASES,
356
+ ),
357
+ ).toEqual(new Set(["cwd", "git_branch", "model", "provider", "session_duration"]));
358
+ });
359
+ });
360
+
361
+ describe("stripOrphanSeparators", () => {
362
+ it("strips leading and trailing plain pipe separators", () => {
363
+ expect(stripOrphanSeparators(" | tokens")).toBe("tokens");
364
+ expect(stripOrphanSeparators("tokens | ")).toBe("tokens");
365
+ expect(stripOrphanSeparators(" | tokens | ")).toBe("tokens");
366
+ });
367
+
368
+ it("collapses repeated plain pipe separators", () => {
369
+ // Two themed/plain seps in a row produce " | | " (space on both sides of each |).
370
+ expect(stripOrphanSeparators("ctx | | tokens")).toBe("ctx | tokens");
371
+ expect(stripOrphanSeparators(" | | $0")).toBe("$0");
372
+ });
373
+
374
+ it("strips leading/trailing whitespace after empty groups drop", () => {
375
+ expect(stripOrphanSeparators(" DIR")).toBe("DIR");
376
+ expect(stripOrphanSeparators("DIR ")).toBe("DIR");
377
+ expect(stripOrphanSeparators(" ")).toBe("");
378
+ });
379
+
380
+ it("handles ANSI-wrapped themed separators", () => {
381
+ const sep = "\x1b[90m | \x1b[0m";
382
+ expect(stripOrphanSeparators(`${sep}tokens`)).toBe("tokens");
383
+ expect(stripOrphanSeparators(`ctx${sep}${sep}tokens`)).toBe(`ctx${sep}tokens`);
384
+ expect(stripOrphanSeparators(`ctx${sep}tokens${sep}`)).toBe(`ctx${sep}tokens`);
385
+ expect(stripOrphanSeparators(`${sep}tokens${sep}cost`)).toBe(`tokens${sep}cost`);
386
+ });
387
+
388
+ it("does not destroy intentional pipes without surrounding spaces", () => {
389
+ expect(stripOrphanSeparators("branch|feature")).toBe("branch|feature");
390
+ });
391
+
392
+ it("tidies right-side groups with empty middle metrics", () => {
393
+ const renderVar = (name: string): string => {
394
+ const map: Record<string, string> = {
395
+ context: "",
396
+ tokens: "",
397
+ cost: "$0",
398
+ sep: " | ",
399
+ };
400
+ return map[name] ?? "";
401
+ };
402
+ const tokens = parseFooterFormat("($context)($sep$tokens)($sep$cost)");
403
+
404
+ // Empty content vars drop their groups even when $sep is present.
405
+ const raw = renderFormatSplit(tokens, renderVar).left;
406
+ expect(raw).toBe(" | $0");
407
+ expect(stripOrphanSeparators(raw)).toBe("$0");
408
+ });
409
+
410
+ it("drops groups whose only non-empty var is $sep", () => {
411
+ const renderVar = (name: string): string => (name === "sep" ? " | " : "");
412
+ const tokens = parseFooterFormat("($sep$tokens)cwd");
413
+ expect(renderFormatSplit(tokens, renderVar).left).toBe("cwd");
414
+ });
415
+
416
+ it("preserves themed separators between non-empty parts", () => {
417
+ const sep = "\x1b[90m | \x1b[0m";
418
+ const renderVar = (name: string): string => {
419
+ const map: Record<string, string> = {
420
+ context: "72%",
421
+ tokens: "↑1",
422
+ cost: "$0",
423
+ sep,
424
+ };
425
+ return map[name] ?? "";
426
+ };
427
+ const tokens = parseFooterFormat("($context)($sep$tokens)($sep$cost)");
428
+ const raw = renderFormatSplit(tokens, renderVar).left;
429
+ expect(stripOrphanSeparators(raw)).toBe(`72%${sep}↑1${sep}$0`);
430
+ });
431
+ });
@@ -0,0 +1,113 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import { visibleWidth } from "@earendil-works/pi-tui";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ compactChunkBudget,
6
+ fullFooterFitsAligned,
7
+ packCompactChunks,
8
+ reflowFullFooter,
9
+ } from "../extensions/zentui/footer-layout";
10
+
11
+ const chunk = (text: string, boundary: "space" | "separator" = "space") => ({
12
+ text,
13
+ boundary,
14
+ });
15
+
16
+ describe("responsive footer layout", () => {
17
+ it("models centered-middle placement and its floor bias", () => {
18
+ expect(fullFooterFitsAligned({ left: "LLLLL", middle: "MMMM", right: "" }, 10)).toBe(false);
19
+ expect(fullFooterFitsAligned({ left: "LLLLL", middle: "MMMM", right: "" }, 11)).toBe(true);
20
+ expect(fullFooterFitsAligned({ left: "", middle: "MMMM", right: "RRRRR" }, 10)).toBe(true);
21
+ expect(fullFooterFitsAligned({ left: "LLLL", middle: "MMM", right: "RRRR" }, 13)).toBe(true);
22
+ expect(fullFooterFitsAligned({ left: "LLLL", middle: "MMM", right: "RRRR" }, 12)).toBe(false);
23
+ });
24
+
25
+ it("measures ANSI and Nerd Font content by visible terminal cells", () => {
26
+ const zones = { left: "\u001b[31mLL\u001b[0m", middle: "\ue0a0", right: "RR" };
27
+ expect(fullFooterFitsAligned(zones, 7)).toBe(true);
28
+ expect(fullFooterFitsAligned(zones, 6)).toBe(false);
29
+ });
30
+
31
+ it("prefers left / middle+right and falls back to left+middle / right", () => {
32
+ expect(reflowFullFooter({ left: "LLLL", middle: "MM", right: "RR" }, 5)).toEqual([
33
+ "LLLL",
34
+ "MM RR",
35
+ ]);
36
+ expect(reflowFullFooter({ left: "L", middle: "MMMM", right: "RRRR" }, 6)).toEqual([
37
+ "L MMMM",
38
+ "RRRR",
39
+ ]);
40
+ expect(reflowFullFooter({ left: "LLLLLL", middle: "MMMM", right: "RRRR" }, 6)).toBeUndefined();
41
+ });
42
+
43
+ it("collapses empty reflow zones without blank rows or stray spaces", () => {
44
+ expect(reflowFullFooter({ left: "L", middle: "", right: "R" }, 2)).toEqual(["L", "R"]);
45
+ expect(reflowFullFooter({ left: "", middle: "M", right: "R" }, 3)).toEqual(["M R"]);
46
+ expect(reflowFullFooter({ left: "L", middle: "M", right: "" }, 3)).toEqual(["L", "M"]);
47
+ });
48
+
49
+ it("uses the exact half-row compact budget", () => {
50
+ expect(compactChunkBudget(47)).toBe(23);
51
+ expect(compactChunkBudget(19)).toBe(9);
52
+ expect(compactChunkBudget(17)).toBe(8);
53
+ expect(compactChunkBudget(8)).toBe(8);
54
+ expect(compactChunkBudget(5)).toBe(8);
55
+ });
56
+
57
+ it("packs chunks with boundary-aware same-row joins and no divider at breaks", () => {
58
+ expect(packCompactChunks([chunk("one"), chunk("two")], 7, 2, " | ")).toEqual(["one two"]);
59
+ expect(packCompactChunks([chunk("one"), chunk("two", "separator")], 9, 2, " | ")).toEqual([
60
+ "one | two",
61
+ ]);
62
+ expect(packCompactChunks([chunk("one"), chunk("two", "separator")], 8, 2, " | ")).toEqual([
63
+ "one",
64
+ "two",
65
+ ]);
66
+ expect(
67
+ packCompactChunks([chunk("one"), chunk(""), chunk(" "), chunk("two")], 7, 2, " | "),
68
+ ).toEqual(["one two"]);
69
+ });
70
+
71
+ it("retains the next surviving chunk's incoming boundary", () => {
72
+ expect(
73
+ packCompactChunks([chunk("A"), chunk("", "separator"), chunk("B", "space")], 5, 2, " | "),
74
+ ).toEqual(["A B"]);
75
+ expect(
76
+ packCompactChunks([chunk("A"), chunk("", "space"), chunk("B", "separator")], 5, 2, " | "),
77
+ ).toEqual(["A | B"]);
78
+ });
79
+
80
+ it("supports every finite line limit and unlimited rows", () => {
81
+ const chunks = [chunk("one"), chunk("two"), chunk("three")];
82
+ expect(packCompactChunks(chunks, 5, 1, " | ")).toEqual(["one…"]);
83
+ expect(packCompactChunks(chunks, 7, 2, " | ")).toEqual(["one two", "three"]);
84
+ expect(packCompactChunks(chunks, 5, 3, " | ")).toEqual(["one", "two", "three"]);
85
+ expect(packCompactChunks(chunks, 5, "unlimited", " | ")).toEqual(["one", "two", "three"]);
86
+ });
87
+
88
+ it("marks finite-cap omissions with exactly one ellipsis", () => {
89
+ expect(
90
+ packCompactChunks([chunk("12345"), chunk("later")], 5, 1, " | ").map(
91
+ stripVTControlCharacters,
92
+ ),
93
+ ).toEqual(["1234…"]);
94
+ expect(
95
+ packCompactChunks([chunk("123456789"), chunk("later")], 5, 1, " | ").map(
96
+ stripVTControlCharacters,
97
+ ),
98
+ ).toEqual(["1234…"]);
99
+ });
100
+
101
+ it("keeps every ANSI and wide-glyph row within width, even below the compact budget", () => {
102
+ const rows = packCompactChunks(
103
+ [chunk("\u001b[31mabcdef\u001b[0m"), chunk("界界界"), chunk("\ue0a0")],
104
+ 5,
105
+ 3,
106
+ " | ",
107
+ );
108
+ expect(rows.every((row) => visibleWidth(row) <= 5)).toBe(true);
109
+ expect(
110
+ packCompactChunks([chunk("abcdefgh")], 5, 1, " | ").every((row) => visibleWidth(row) <= 5),
111
+ ).toBe(true);
112
+ });
113
+ });
@@ -0,0 +1,123 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { defaultConfig } from "../extensions/zentui/config";
4
+ import { installFooter } from "../extensions/zentui/footer";
5
+ import { emptyGitStatus } from "../extensions/zentui/git";
6
+ import { createInitialState } from "../extensions/zentui/state";
7
+
8
+ type FooterFactory = (
9
+ tui: { requestRender(): void },
10
+ theme: Theme,
11
+ data: {
12
+ onBranchChange(callback: () => void): () => void;
13
+ getExtensionStatuses(): Map<string, string>;
14
+ },
15
+ ) => { render(width: number): string[] };
16
+
17
+ function theme(): Theme {
18
+ return {
19
+ fg: (_color: string, text: string) => text,
20
+ bold: (text: string) => text,
21
+ italic: (text: string) => text,
22
+ underline: (text: string) => text,
23
+ strikethrough: (text: string) => text,
24
+ getThinkingBorderColor: () => (text: string) => text,
25
+ } as unknown as Theme;
26
+ }
27
+
28
+ describe("Footer repository path rendering", () => {
29
+ it("uses the same repository cwd in built-in, custom, and final responsive fallback output", () => {
30
+ let footerFactory: FooterFactory | undefined;
31
+ const cwd = "/repo/extensions/zentui";
32
+ const context = {
33
+ cwd,
34
+ model: { contextWindow: 100_000 },
35
+ getContextUsage: () => ({ percent: 10, tokens: 10_000, contextWindow: 100_000 }),
36
+ sessionManager: { getSessionName: () => undefined },
37
+ ui: {
38
+ setFooter(factory: FooterFactory | undefined) {
39
+ footerFactory = factory;
40
+ },
41
+ },
42
+ };
43
+ const config = structuredClone(defaultConfig);
44
+ config.icons.cwd = "";
45
+ config.components.footer.styles.starship.pathDisplay = {
46
+ mode: "repository",
47
+ depth: 0,
48
+ };
49
+ const render = (width: number) => {
50
+ installFooter(context as never, createInitialState(emptyGitStatus()), () => config, {
51
+ setRequestRender() {},
52
+ scheduleProjectRefresh() {},
53
+ getRepositoryRoot: () => "/repo",
54
+ });
55
+ const footer = footerFactory?.({ requestRender() {} }, theme(), {
56
+ onBranchChange: () => () => {},
57
+ getExtensionStatuses: () => new Map(),
58
+ });
59
+ return footer?.render(width).join("\n") ?? "";
60
+ };
61
+
62
+ const starship = config.components.footer.styles.starship;
63
+ starship.responsive = false;
64
+ starship.format = "";
65
+ const builtIn = render(160);
66
+ expect(builtIn).toMatch(/(?:^|\s)extensions\/zentui(?:\s|$)/);
67
+ expect(builtIn).not.toContain("/repo/extensions/zentui");
68
+
69
+ starship.format = "$cwd";
70
+ const custom = render(160);
71
+ expect(custom.trim()).toBe("extensions/zentui");
72
+ expect(custom).not.toContain("/repo/extensions/zentui");
73
+
74
+ starship.responsive = true;
75
+ starship.format = "X".repeat(100);
76
+ starship.compactFormat = "$cwd";
77
+ const compact = render(50);
78
+ expect(compact.trim()).toBe("extensions/zentui");
79
+ expect(compact).not.toContain("/repo/extensions/zentui");
80
+ });
81
+
82
+ it("only asks for a repository root in repository mode", () => {
83
+ let footerFactory: FooterFactory | undefined;
84
+ const config = structuredClone(defaultConfig);
85
+ config.icons.cwd = "";
86
+ const getRepositoryRoot = vi.fn(() => "/repo");
87
+ installFooter(
88
+ {
89
+ cwd: "/repo/extensions/zentui",
90
+ model: { contextWindow: 100_000 },
91
+ getContextUsage: () => ({ percent: 10, tokens: 10_000, contextWindow: 100_000 }),
92
+ sessionManager: { getSessionName: () => undefined },
93
+ ui: {
94
+ setFooter(factory: FooterFactory | undefined) {
95
+ footerFactory = factory;
96
+ },
97
+ },
98
+ } as never,
99
+ createInitialState(emptyGitStatus()),
100
+ () => config,
101
+ {
102
+ setRequestRender() {},
103
+ scheduleProjectRefresh() {},
104
+ getRepositoryRoot,
105
+ },
106
+ );
107
+ const footer = footerFactory?.({ requestRender() {} }, theme(), {
108
+ onBranchChange: () => () => {},
109
+ getExtensionStatuses: () => new Map(),
110
+ });
111
+
112
+ config.components.footer.styles.starship.pathDisplay.mode = "basename";
113
+ footer?.render(160);
114
+ config.components.footer.styles.starship.pathDisplay.mode = "full";
115
+ footer?.render(160);
116
+ expect(getRepositoryRoot).not.toHaveBeenCalled();
117
+
118
+ config.components.footer.styles.starship.pathDisplay.mode = "repository";
119
+ footer?.render(160);
120
+ expect(getRepositoryRoot).toHaveBeenCalledTimes(1);
121
+ expect(getRepositoryRoot).toHaveBeenCalledWith("/repo/extensions/zentui");
122
+ });
123
+ });