@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,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
+ });