@selesai/code 0.9.15 → 0.10.0

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