@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,825 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type { PolishedTuiConfig } from "../extensions/zentui/config";
4
+ import { emptyGitStatus, type GitReadResult } from "../extensions/zentui/git";
5
+
6
+ const mocks = vi.hoisted(() => ({
7
+ config: undefined as unknown as PolishedTuiConfig,
8
+ readGitStatus: vi.fn(),
9
+ readRuntimeInfo: vi.fn(),
10
+ readPackageVersionResult: vi.fn(),
11
+ syncState: vi.fn(),
12
+ }));
13
+
14
+ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
15
+ const actual = await importOriginal<typeof import("@earendil-works/pi-coding-agent")>();
16
+ return {
17
+ ...actual,
18
+ getSettingsListTheme: () => ({
19
+ label: (text: string) => text,
20
+ value: (text: string) => text,
21
+ description: (text: string) => text,
22
+ cursor: "> ",
23
+ hint: (text: string) => text,
24
+ }),
25
+ };
26
+ });
27
+
28
+ vi.mock("../extensions/zentui/config", async (importOriginal) => {
29
+ const actual = await importOriginal<typeof import("../extensions/zentui/config")>();
30
+ return {
31
+ ...actual,
32
+ ensureConfigExists: () => {},
33
+ loadConfig: () => mocks.config,
34
+ saveEditorComponentPatch(patch: Record<string, unknown>) {
35
+ mocks.config = {
36
+ ...mocks.config,
37
+ components: {
38
+ ...mocks.config.components,
39
+ editor: { ...mocks.config.components.editor, ...patch },
40
+ },
41
+ ...(patch.modelLabel === undefined
42
+ ? {}
43
+ : { editorModelLabel: patch.modelLabel as "id" | "name" }),
44
+ };
45
+ return mocks.config;
46
+ },
47
+ saveFooterComponentPatch(patch: Record<string, unknown>) {
48
+ const footer = mocks.config.components.footer;
49
+ mocks.config = {
50
+ ...mocks.config,
51
+ components: {
52
+ ...mocks.config.components,
53
+ footer: { ...footer, ...patch },
54
+ },
55
+ };
56
+ return mocks.config;
57
+ },
58
+ saveStarshipFooterStylePatch(patch: Record<string, unknown>) {
59
+ const footer = mocks.config.components.footer;
60
+ const starship = footer.styles.starship;
61
+ const nextStarship = {
62
+ ...starship,
63
+ ...patch,
64
+ ...(patch.segments
65
+ ? { segments: { ...starship.segments, ...(patch.segments as object) } }
66
+ : {}),
67
+ ...(patch.gitCommit
68
+ ? { gitCommit: { ...starship.gitCommit, ...(patch.gitCommit as object) } }
69
+ : {}),
70
+ ...(patch.gitMetrics
71
+ ? { gitMetrics: { ...starship.gitMetrics, ...(patch.gitMetrics as object) } }
72
+ : {}),
73
+ };
74
+ mocks.config = {
75
+ ...mocks.config,
76
+ ...(patch.format === undefined ? {} : { footerFormat: patch.format as string }),
77
+ ...(patch.responsive === undefined
78
+ ? {}
79
+ : { responsiveFooter: patch.responsive as boolean }),
80
+ ...(patch.compactMaxLines === undefined
81
+ ? {}
82
+ : { compactFooterMaxLines: patch.compactMaxLines as 1 | 2 | 3 | "unlimited" }),
83
+ ...(patch.segments === undefined ? {} : { footerSegments: nextStarship.segments }),
84
+ ...(patch.gitCommit === undefined ? {} : { gitCommit: nextStarship.gitCommit }),
85
+ ...(patch.gitMetrics === undefined ? {} : { gitMetrics: nextStarship.gitMetrics }),
86
+ components: {
87
+ ...mocks.config.components,
88
+ footer: { ...footer, styles: { starship: nextStarship } },
89
+ },
90
+ };
91
+ return mocks.config;
92
+ },
93
+ saveEditorModelLabel(value: "id" | "name") {
94
+ mocks.config = {
95
+ ...mocks.config,
96
+ editorModelLabel: value,
97
+ components: {
98
+ ...mocks.config.components,
99
+ editor: { ...mocks.config.components.editor, modelLabel: value },
100
+ footer: { ...mocks.config.components.footer, modelLabel: value },
101
+ },
102
+ };
103
+ return mocks.config;
104
+ },
105
+ saveFooterFormatPatch(value: string) {
106
+ const footer = mocks.config.components.footer;
107
+ mocks.config = {
108
+ ...mocks.config,
109
+ footerFormat: value,
110
+ components: {
111
+ ...mocks.config.components,
112
+ footer: {
113
+ ...footer,
114
+ styles: { starship: { ...footer.styles.starship, format: value } },
115
+ },
116
+ },
117
+ };
118
+ return mocks.config;
119
+ },
120
+ saveFooterSegmentsPatch(patch: Record<string, boolean>) {
121
+ const footer = mocks.config.components.footer;
122
+ mocks.config = {
123
+ ...mocks.config,
124
+ footerSegments: { ...mocks.config.footerSegments, ...patch },
125
+ components: {
126
+ ...mocks.config.components,
127
+ footer: {
128
+ ...footer,
129
+ styles: {
130
+ starship: {
131
+ ...footer.styles.starship,
132
+ segments: { ...footer.styles.starship.segments, ...patch },
133
+ },
134
+ },
135
+ },
136
+ },
137
+ };
138
+ return mocks.config;
139
+ },
140
+ saveResponsiveFooterPatch(patch: Record<string, unknown>) {
141
+ const footer = mocks.config.components.footer;
142
+ mocks.config = {
143
+ ...mocks.config,
144
+ ...patch,
145
+ components: {
146
+ ...mocks.config.components,
147
+ footer: {
148
+ ...footer,
149
+ styles: {
150
+ starship: {
151
+ ...footer.styles.starship,
152
+ ...(patch.responsiveFooter === undefined
153
+ ? {}
154
+ : { responsive: patch.responsiveFooter as boolean }),
155
+ ...(patch.compactFooterMaxLines === undefined
156
+ ? {}
157
+ : {
158
+ compactMaxLines: patch.compactFooterMaxLines as 1 | 2 | 3 | "unlimited",
159
+ }),
160
+ },
161
+ },
162
+ },
163
+ },
164
+ };
165
+ return mocks.config;
166
+ },
167
+ saveGitCommitPatch(patch: Record<string, boolean>) {
168
+ const footer = mocks.config.components.footer;
169
+ mocks.config = {
170
+ ...mocks.config,
171
+ gitCommit: { ...mocks.config.gitCommit, ...patch },
172
+ components: {
173
+ ...mocks.config.components,
174
+ footer: {
175
+ ...footer,
176
+ styles: {
177
+ starship: {
178
+ ...footer.styles.starship,
179
+ gitCommit: { ...footer.styles.starship.gitCommit, ...patch },
180
+ },
181
+ },
182
+ },
183
+ },
184
+ };
185
+ return mocks.config;
186
+ },
187
+ saveGitMetricsPatch(patch: Record<string, boolean>) {
188
+ const footer = mocks.config.components.footer;
189
+ mocks.config = {
190
+ ...mocks.config,
191
+ gitMetrics: { ...mocks.config.gitMetrics, ...patch },
192
+ components: {
193
+ ...mocks.config.components,
194
+ footer: {
195
+ ...footer,
196
+ styles: {
197
+ starship: {
198
+ ...footer.styles.starship,
199
+ gitMetrics: { ...footer.styles.starship.gitMetrics, ...patch },
200
+ },
201
+ },
202
+ },
203
+ },
204
+ };
205
+ return mocks.config;
206
+ },
207
+ };
208
+ });
209
+
210
+ vi.mock("../extensions/zentui/git", async (importOriginal) => {
211
+ const actual = await importOriginal<typeof import("../extensions/zentui/git")>();
212
+ mocks.readGitStatus.mockImplementation(async () => ({
213
+ kind: "ok" as const,
214
+ status: actual.emptyGitStatus(),
215
+ }));
216
+ return { ...actual, readGitStatus: mocks.readGitStatus };
217
+ });
218
+ vi.mock("../extensions/zentui/runtime", () => ({ readRuntimeInfo: mocks.readRuntimeInfo }));
219
+ vi.mock("../extensions/zentui/package-version", () => ({
220
+ readPackageVersionResult: mocks.readPackageVersionResult,
221
+ }));
222
+ vi.mock("../extensions/zentui/state", async (importOriginal) => {
223
+ const actual = await importOriginal<typeof import("../extensions/zentui/state")>();
224
+ return {
225
+ ...actual,
226
+ syncState(...args: Parameters<typeof actual.syncState>) {
227
+ mocks.syncState(...args);
228
+ return actual.syncState(...args);
229
+ },
230
+ };
231
+ });
232
+
233
+ import { defaultConfig } from "../extensions/zentui/config";
234
+ import zentui from "../extensions/zentui/index";
235
+
236
+ type Handler = (event: unknown, ctx: unknown) => unknown | Promise<unknown>;
237
+ type Command = { handler: (args: string, ctx: unknown) => Promise<void> };
238
+
239
+ function makeTheme(): Theme {
240
+ return {
241
+ fg(_color: string, text: string) {
242
+ return text;
243
+ },
244
+ bold: (text: string) => text,
245
+ italic: (text: string) => text,
246
+ underline: (text: string) => text,
247
+ strikethrough: (text: string) => text,
248
+ getThinkingBorderColor: () => (text: string) => text,
249
+ } as unknown as Theme;
250
+ }
251
+
252
+ function loadExtension() {
253
+ const handlers = new Map<string, Handler[]>();
254
+ let command: Command | undefined;
255
+ zentui({
256
+ on(name: string, handler: Handler) {
257
+ handlers.set(name, [...(handlers.get(name) ?? []), handler]);
258
+ },
259
+ registerCommand(name: string, options: Command) {
260
+ if (name === "zentui") command = options;
261
+ },
262
+ getThinkingLevel: () => "off",
263
+ } as never);
264
+ if (!command) throw new Error("zentui command was not registered");
265
+ return { handlers, command };
266
+ }
267
+
268
+ async function emit(handlers: Map<string, Handler[]>, name: string, ctx: unknown) {
269
+ for (const handler of handlers.get(name) ?? []) await handler({}, ctx);
270
+ }
271
+
272
+ async function settleProjectRefresh() {
273
+ for (let index = 0; index < 8; index++) await Promise.resolve();
274
+ }
275
+
276
+ function updateStarship(
277
+ patch: Partial<PolishedTuiConfig["components"]["footer"]["styles"]["starship"]>,
278
+ ) {
279
+ const footer = mocks.config.components.footer;
280
+ mocks.config = {
281
+ ...mocks.config,
282
+ components: {
283
+ ...mocks.config.components,
284
+ footer: {
285
+ ...footer,
286
+ styles: { starship: { ...footer.styles.starship, ...patch } },
287
+ },
288
+ },
289
+ };
290
+ }
291
+
292
+ function createContext(custom?: (factory: (...args: unknown[]) => unknown) => Promise<void>) {
293
+ let footerFactory: unknown;
294
+ let editorFactory: unknown;
295
+ const theme = makeTheme();
296
+ return {
297
+ hasUI: true,
298
+ mode: "tui",
299
+ cwd: "/tmp/responsive-dependencies",
300
+ model: { id: "test", provider: "test", contextWindow: 100_000 },
301
+ getContextUsage: () => ({ tokens: 1, contextWindow: 100_000, percent: 1 }),
302
+ sessionManager: {
303
+ getBranch: () => [],
304
+ getEntries: () => [],
305
+ getSessionName: () => undefined,
306
+ },
307
+ ui: {
308
+ theme,
309
+ getEditorText: () => "",
310
+ setEditorText() {},
311
+ setFooter(factory: unknown) {
312
+ footerFactory = factory;
313
+ },
314
+ setEditorComponent(factory: unknown) {
315
+ editorFactory = factory;
316
+ },
317
+ getEditorComponent: () => editorFactory,
318
+ notify() {},
319
+ custom,
320
+ },
321
+ get footerFactory() {
322
+ return footerFactory;
323
+ },
324
+ };
325
+ }
326
+
327
+ beforeEach(() => {
328
+ vi.useFakeTimers();
329
+ const footer = defaultConfig.components.footer;
330
+ mocks.config = {
331
+ ...defaultConfig,
332
+ features: { ...defaultConfig.features, editor: false, statusLine: true },
333
+ components: {
334
+ ...defaultConfig.components,
335
+ editor: {
336
+ ...defaultConfig.components.editor,
337
+ enabled: false,
338
+ modelLabel: "id",
339
+ },
340
+ footer: {
341
+ ...footer,
342
+ style: "starship",
343
+ modelLabel: "id",
344
+ styles: {
345
+ starship: {
346
+ ...footer.styles.starship,
347
+ format: "$cwd",
348
+ responsive: false,
349
+ compactFormat: "$package",
350
+ compactMaxLines: 2,
351
+ },
352
+ },
353
+ },
354
+ },
355
+ projectRefreshIntervalMs: 0,
356
+ footerFormat: "$cwd",
357
+ responsiveFooter: false,
358
+ compactFooterFormat: "$package",
359
+ };
360
+ mocks.readGitStatus.mockClear();
361
+ mocks.readRuntimeInfo.mockReset().mockResolvedValue({ kind: "ok", runtime: undefined });
362
+ mocks.readPackageVersionResult.mockReset().mockResolvedValue({ kind: "ok", result: null });
363
+ mocks.syncState.mockClear();
364
+ });
365
+
366
+ afterEach(() => {
367
+ vi.useRealTimers();
368
+ vi.restoreAllMocks();
369
+ });
370
+
371
+ describe("responsive footer dependency reconciliation", () => {
372
+ it("refreshes newly active format probes with polling disabled and skips unchanged unions", async () => {
373
+ const { handlers, command } = loadExtension();
374
+ const ctx = createContext();
375
+ await emit(handlers, "session_start", ctx);
376
+ await settleProjectRefresh();
377
+ expect(mocks.readPackageVersionResult).not.toHaveBeenCalled();
378
+ expect(mocks.readRuntimeInfo).not.toHaveBeenCalled();
379
+
380
+ const gitReads = mocks.readGitStatus.mock.calls.length;
381
+ await command.handler('format "$directory"', ctx);
382
+ await settleProjectRefresh();
383
+ expect(mocks.readGitStatus).toHaveBeenCalledTimes(gitReads);
384
+
385
+ await command.handler('format "$package"', ctx);
386
+ await settleProjectRefresh();
387
+ expect(mocks.readPackageVersionResult).toHaveBeenCalledOnce();
388
+ });
389
+
390
+ it("clears package state when its reference deactivates before a failed re-enable", async () => {
391
+ updateStarship({ format: "$package", responsive: false });
392
+ mocks.readPackageVersionResult.mockResolvedValueOnce({
393
+ kind: "ok",
394
+ result: { ecosystem: "nodejs", version: "1.2.3" },
395
+ });
396
+ const { handlers, command } = loadExtension();
397
+ const ctx = createContext();
398
+ await emit(handlers, "session_start", ctx);
399
+ await settleProjectRefresh();
400
+ const state = mocks.syncState.mock.calls[0]?.[0] as {
401
+ packageVersion?: { ecosystem: string; version: string };
402
+ };
403
+ expect(state.packageVersion).toEqual({ ecosystem: "nodejs", version: "1.2.3" });
404
+
405
+ await command.handler('format "$cwd"', ctx);
406
+ await settleProjectRefresh();
407
+ expect(state.packageVersion).toBeUndefined();
408
+
409
+ mocks.readPackageVersionResult.mockResolvedValue({ kind: "error" });
410
+ await command.handler('format "$package"', ctx);
411
+ await settleProjectRefresh();
412
+ expect(mocks.readPackageVersionResult).toHaveBeenCalledTimes(2);
413
+ expect(state.packageVersion).toBeUndefined();
414
+ });
415
+
416
+ it("invalidates an in-flight package read across remove and failed re-enable", async () => {
417
+ updateStarship({ format: "$package", responsive: false });
418
+ let resolveObsoleteRead:
419
+ | ((value: { kind: "ok"; result: { ecosystem: string; version: string } }) => void)
420
+ | undefined;
421
+ mocks.readPackageVersionResult
422
+ .mockImplementationOnce(
423
+ () =>
424
+ new Promise((resolve) => {
425
+ resolveObsoleteRead = resolve;
426
+ }),
427
+ )
428
+ .mockResolvedValueOnce({ kind: "error" });
429
+ const { handlers, command } = loadExtension();
430
+ const ctx = createContext();
431
+ await emit(handlers, "session_start", ctx);
432
+ await settleProjectRefresh();
433
+ expect(mocks.readPackageVersionResult).toHaveBeenCalledOnce();
434
+
435
+ const state = mocks.syncState.mock.calls[0]?.[0] as {
436
+ packageVersion?: { ecosystem: string; version: string };
437
+ };
438
+ await command.handler('format "$cwd"', ctx);
439
+ await settleProjectRefresh();
440
+ expect(state.packageVersion).toBeUndefined();
441
+
442
+ await command.handler('format "$package"', ctx);
443
+ await settleProjectRefresh();
444
+ expect(mocks.readPackageVersionResult).toHaveBeenCalledTimes(2);
445
+ expect(state.packageVersion).toBeUndefined();
446
+
447
+ resolveObsoleteRead?.({
448
+ kind: "ok",
449
+ result: { ecosystem: "nodejs", version: "9.9.9-obsolete" },
450
+ });
451
+ await settleProjectRefresh();
452
+ expect(state.packageVersion).toBeUndefined();
453
+ });
454
+
455
+ it("replaces in-flight Footer probes across live styles while Minimalist keeps refresh active", async () => {
456
+ const editor = mocks.config.components.editor;
457
+ updateStarship({ format: "$package $runtime", responsive: false });
458
+ mocks.config = {
459
+ ...mocks.config,
460
+ components: {
461
+ ...mocks.config.components,
462
+ editor: {
463
+ ...editor,
464
+ enabled: true,
465
+ style: "minimalist",
466
+ styles: {
467
+ ...editor.styles,
468
+ minimalist: { ...editor.styles.minimalist, showGit: true },
469
+ },
470
+ },
471
+ },
472
+ };
473
+ let resolveObsoletePackage:
474
+ | ((value: { kind: "ok"; result: { ecosystem: string; version: string } }) => void)
475
+ | undefined;
476
+ let resolveObsoleteRuntime:
477
+ | ((value: {
478
+ kind: "ok";
479
+ runtime: { name: string; symbol: string; style: string; version: string };
480
+ }) => void)
481
+ | undefined;
482
+ mocks.readPackageVersionResult
483
+ .mockImplementationOnce(
484
+ () =>
485
+ new Promise((resolve) => {
486
+ resolveObsoletePackage = resolve;
487
+ }),
488
+ )
489
+ .mockResolvedValueOnce({ kind: "error" });
490
+ mocks.readRuntimeInfo
491
+ .mockImplementationOnce(
492
+ () =>
493
+ new Promise((resolve) => {
494
+ resolveObsoleteRuntime = resolve;
495
+ }),
496
+ )
497
+ .mockResolvedValueOnce({ kind: "error" });
498
+ const { handlers, command } = loadExtension();
499
+ const ctx = createContext();
500
+ await emit(handlers, "session_start", ctx);
501
+ await settleProjectRefresh();
502
+ expect(mocks.readPackageVersionResult).toHaveBeenCalledOnce();
503
+ expect(mocks.readRuntimeInfo).toHaveBeenCalledOnce();
504
+
505
+ const state = mocks.syncState.mock.calls[0]?.[0] as {
506
+ packageVersion?: { ecosystem: string; version: string };
507
+ runtime?: { name: string; version?: string };
508
+ };
509
+ await command.handler("footer off", ctx);
510
+ await settleProjectRefresh();
511
+ expect(state.packageVersion).toBeUndefined();
512
+ expect(state.runtime).toBeUndefined();
513
+
514
+ await command.handler("footer on", ctx);
515
+ await settleProjectRefresh();
516
+ expect(mocks.readPackageVersionResult).toHaveBeenCalledTimes(2);
517
+ expect(mocks.readRuntimeInfo).toHaveBeenCalledTimes(2);
518
+ expect(state.packageVersion).toBeUndefined();
519
+ expect(state.runtime).toBeUndefined();
520
+
521
+ resolveObsoletePackage?.({
522
+ kind: "ok",
523
+ result: { ecosystem: "nodejs", version: "9.9.9-obsolete" },
524
+ });
525
+ resolveObsoleteRuntime?.({
526
+ kind: "ok",
527
+ runtime: {
528
+ name: "Obsolete",
529
+ symbol: "O",
530
+ style: "bold red",
531
+ version: "9.9.9-obsolete",
532
+ },
533
+ });
534
+ await settleProjectRefresh();
535
+ expect(state.packageVersion).toBeUndefined();
536
+ expect(state.runtime).toBeUndefined();
537
+
538
+ const footerFactory = ctx.footerFactory as
539
+ | ((
540
+ tui: { requestRender(): void },
541
+ theme: Theme,
542
+ data: {
543
+ onBranchChange(callback: () => void): () => void;
544
+ getExtensionStatuses(): Map<string, string>;
545
+ },
546
+ ) => { render(width: number): string[] })
547
+ | undefined;
548
+ const rendered =
549
+ footerFactory?.({ requestRender() {} }, makeTheme(), {
550
+ onBranchChange: () => () => {},
551
+ getExtensionStatuses: () => new Map(),
552
+ })
553
+ .render(120)
554
+ .join("\n") ?? "";
555
+ expect(rendered).not.toContain("9.9.9-obsolete");
556
+ expect(rendered).not.toContain("Obsolete");
557
+ });
558
+
559
+ it("probes runtime only while a custom Footer format references it", async () => {
560
+ const { handlers, command } = loadExtension();
561
+ const ctx = createContext();
562
+ await emit(handlers, "session_start", ctx);
563
+ await settleProjectRefresh();
564
+ expect(mocks.readRuntimeInfo).not.toHaveBeenCalled();
565
+
566
+ await command.handler('format "$runtime"', ctx);
567
+ await settleProjectRefresh();
568
+ expect(mocks.readRuntimeInfo).toHaveBeenCalledOnce();
569
+
570
+ await command.handler('format "$cwd"', ctx);
571
+ await settleProjectRefresh();
572
+ expect(mocks.readRuntimeInfo).toHaveBeenCalledOnce();
573
+ });
574
+
575
+ it("counts the enabled built-in runtime segment as an active reference", async () => {
576
+ updateStarship({
577
+ format: "",
578
+ responsive: false,
579
+ segments: { ...mocks.config.components.footer.styles.starship.segments, runtime: true },
580
+ });
581
+ const { handlers } = loadExtension();
582
+ await emit(handlers, "session_start", createContext());
583
+ await settleProjectRefresh();
584
+ expect(mocks.readRuntimeInfo).toHaveBeenCalledOnce();
585
+ });
586
+
587
+ it("skips runtime and package probes for Minimalist-only project refreshes", async () => {
588
+ const editor = mocks.config.components.editor;
589
+ const footer = mocks.config.components.footer;
590
+ mocks.config = {
591
+ ...mocks.config,
592
+ components: {
593
+ ...mocks.config.components,
594
+ editor: {
595
+ ...editor,
596
+ enabled: true,
597
+ style: "minimalist",
598
+ styles: {
599
+ ...editor.styles,
600
+ minimalist: { ...editor.styles.minimalist, showGit: true },
601
+ },
602
+ },
603
+ footer: { ...footer, style: "native" },
604
+ },
605
+ };
606
+ const { handlers } = loadExtension();
607
+ await emit(handlers, "session_start", createContext());
608
+ await settleProjectRefresh();
609
+ expect(mocks.readGitStatus).toHaveBeenCalledOnce();
610
+ expect(mocks.readRuntimeInfo).not.toHaveBeenCalled();
611
+ expect(mocks.readPackageVersionResult).not.toHaveBeenCalled();
612
+ });
613
+
614
+ it("keeps Git state while an obsolete cwd refresh finishes before the queued live cwd", async () => {
615
+ const editor = mocks.config.components.editor;
616
+ const footer = mocks.config.components.footer;
617
+ mocks.config = {
618
+ ...mocks.config,
619
+ components: {
620
+ ...mocks.config.components,
621
+ editor: {
622
+ ...editor,
623
+ enabled: true,
624
+ style: "minimalist",
625
+ styles: {
626
+ ...editor.styles,
627
+ minimalist: { ...editor.styles.minimalist, showGit: true },
628
+ },
629
+ },
630
+ footer: { ...footer, style: "native" },
631
+ },
632
+ };
633
+
634
+ let resolveObsoleteRead: ((result: GitReadResult) => void) | undefined;
635
+ let resolveLiveRead: ((result: GitReadResult) => void) | undefined;
636
+ mocks.readGitStatus
637
+ .mockResolvedValueOnce({
638
+ kind: "ok",
639
+ status: { ...emptyGitStatus(), branch: "repo-a" },
640
+ })
641
+ .mockImplementationOnce(
642
+ () =>
643
+ new Promise<GitReadResult>((resolve) => {
644
+ resolveObsoleteRead = resolve;
645
+ }),
646
+ )
647
+ .mockImplementationOnce(
648
+ () =>
649
+ new Promise<GitReadResult>((resolve) => {
650
+ resolveLiveRead = resolve;
651
+ }),
652
+ );
653
+
654
+ const { handlers } = loadExtension();
655
+ const ctx = createContext();
656
+ const repoCwd = "/worktrees/repo-a/nested";
657
+ ctx.cwd = repoCwd;
658
+ await emit(handlers, "session_start", ctx);
659
+ await settleProjectRefresh();
660
+
661
+ const state = mocks.syncState.mock.calls[0]?.[0] as { branch?: string };
662
+ expect(state.branch).toBe("repo-a");
663
+
664
+ ctx.cwd = "/tmp/not-a-repo";
665
+ await emit(handlers, "tool_execution_end", ctx);
666
+ vi.advanceTimersByTime(5_000);
667
+ await settleProjectRefresh();
668
+ expect(mocks.readGitStatus).toHaveBeenCalledTimes(2);
669
+
670
+ ctx.cwd = repoCwd;
671
+ await emit(handlers, "tool_execution_end", ctx);
672
+ resolveObsoleteRead?.({ kind: "not_a_repo" });
673
+ await settleProjectRefresh();
674
+
675
+ expect(state.branch).toBe("repo-a");
676
+ expect(mocks.readGitStatus).toHaveBeenCalledTimes(2);
677
+
678
+ vi.advanceTimersByTime(5_000);
679
+ await settleProjectRefresh();
680
+ expect(mocks.readGitStatus).toHaveBeenCalledTimes(3);
681
+ expect(state.branch).toBe("repo-a");
682
+
683
+ resolveLiveRead?.({
684
+ kind: "ok",
685
+ status: { ...emptyGitStatus(), branch: "repo-a-updated" },
686
+ });
687
+ await settleProjectRefresh();
688
+ expect(state.branch).toBe("repo-a-updated");
689
+ });
690
+
691
+ it("refreshes compact-only probes immediately when responsiveness is enabled", async () => {
692
+ const ctx = createContext(async (factory) => {
693
+ const component = factory({ requestRender() {} }, makeTheme(), {}, () => {}) as {
694
+ handleInput?: (data: string) => void;
695
+ };
696
+ for (let index = 0; index < 4; index++) component.handleInput?.("\t");
697
+ for (let index = 0; index < 3; index++) component.handleInput?.("\x1b[B");
698
+ component.handleInput?.(" ");
699
+ });
700
+ const { handlers, command } = loadExtension();
701
+ await emit(handlers, "session_start", ctx);
702
+ await settleProjectRefresh();
703
+ expect(mocks.readPackageVersionResult).not.toHaveBeenCalled();
704
+ await command.handler("", ctx);
705
+ await settleProjectRefresh();
706
+ expect(mocks.config.responsiveFooter).toBe(true);
707
+ expect(mocks.readPackageVersionResult).toHaveBeenCalledOnce();
708
+ });
709
+
710
+ it("immediately resyncs editor state when the model label source changes", async () => {
711
+ const ctx = createContext(async (factory) => {
712
+ const component = factory({ requestRender() {} }, makeTheme(), {}, () => {}) as {
713
+ render(width: number): string[];
714
+ handleInput(data: string): void;
715
+ };
716
+ component.handleInput("\t");
717
+ for (let attempts = 0; attempts < 8; attempts++) {
718
+ if (component.render(120).some((line) => line.includes("> Editor model label"))) break;
719
+ component.handleInput("\x1b[B");
720
+ }
721
+ component.handleInput(" ");
722
+ });
723
+ const { handlers, command } = loadExtension();
724
+ await emit(handlers, "session_start", ctx);
725
+ const before = mocks.syncState.mock.calls.length;
726
+ await command.handler("", ctx);
727
+ expect(mocks.config.components.editor.modelLabel).toBe("name");
728
+ expect(mocks.config.components.footer.modelLabel).toBe("id");
729
+ expect(mocks.syncState).toHaveBeenCalledTimes(before + 1);
730
+ });
731
+
732
+ it("forces Git refreshes for exact-tag and submodule probe changes", async () => {
733
+ updateStarship({ format: "$git_commit $git_metrics", responsive: false });
734
+ let target: "showTag" | "ignoreSubmodules" = "showTag";
735
+ const ctx = createContext(async (factory) => {
736
+ const component = factory({ requestRender() {} }, makeTheme(), {}, () => {}) as {
737
+ render(width: number): string[];
738
+ handleInput(data: string): void;
739
+ };
740
+ for (let index = 0; index < 6; index++) component.handleInput("\t");
741
+ const label = target === "showTag" ? "Show exact-match tag" : "Ignore submodules";
742
+ for (let attempts = 0; attempts < 12; attempts++) {
743
+ if (component.render(140).some((line) => line.includes(`> ${label}`))) break;
744
+ component.handleInput("\x1b[B");
745
+ }
746
+ component.handleInput(" ");
747
+ });
748
+ const { handlers, command } = loadExtension();
749
+ await emit(handlers, "session_start", ctx);
750
+ await settleProjectRefresh();
751
+
752
+ const initialReads = mocks.readGitStatus.mock.calls.length;
753
+ await command.handler("", ctx);
754
+ await settleProjectRefresh();
755
+ expect(mocks.readGitStatus).toHaveBeenCalledTimes(initialReads + 1);
756
+ expect(mocks.readGitStatus.mock.calls.at(-1)?.[1]).toMatchObject({ readExactTag: false });
757
+
758
+ target = "ignoreSubmodules";
759
+ await command.handler("", ctx);
760
+ await settleProjectRefresh();
761
+ expect(mocks.readGitStatus).toHaveBeenCalledTimes(initialReads + 2);
762
+ expect(mocks.readGitStatus.mock.calls.at(-1)?.[1]).toMatchObject({ ignoreSubmodules: true });
763
+ });
764
+
765
+ it("refreshes probes activated by built-in segment settings", async () => {
766
+ updateStarship({ format: "", compactFormat: "$cwd" });
767
+ const ctx = createContext(async (factory) => {
768
+ const component = factory({ requestRender() {} }, makeTheme(), {}, () => {}) as {
769
+ handleInput?: (data: string) => void;
770
+ };
771
+ for (let index = 0; index < 5; index++) component.handleInput?.("\t");
772
+ for (let index = 0; index < 11; index++) component.handleInput?.("\x1b[B");
773
+ component.handleInput?.(" ");
774
+ });
775
+ const { handlers, command } = loadExtension();
776
+ await emit(handlers, "session_start", ctx);
777
+ await settleProjectRefresh();
778
+ expect(mocks.readPackageVersionResult).not.toHaveBeenCalled();
779
+ await command.handler("", ctx);
780
+ await settleProjectRefresh();
781
+ expect(mocks.config.footerSegments.packageVersion).toBe(true);
782
+ expect(mocks.readPackageVersionResult).toHaveBeenCalledOnce();
783
+ });
784
+
785
+ it("does not refresh when only the compact row limit changes", async () => {
786
+ updateStarship({ responsive: true });
787
+ const ctx = createContext(async (factory) => {
788
+ const component = factory({ requestRender() {} }, makeTheme(), {}, () => {}) as {
789
+ handleInput?: (data: string) => void;
790
+ };
791
+ for (let index = 0; index < 4; index++) component.handleInput?.("\t");
792
+ for (let index = 0; index < 4; index++) component.handleInput?.("\x1b[B");
793
+ component.handleInput?.(" ");
794
+ });
795
+ const { handlers, command } = loadExtension();
796
+ await emit(handlers, "session_start", ctx);
797
+ await settleProjectRefresh();
798
+ const gitReads = mocks.readGitStatus.mock.calls.length;
799
+ await command.handler("", ctx);
800
+ await settleProjectRefresh();
801
+ expect(mocks.config.compactFooterMaxLines).toBe(3);
802
+ expect(mocks.readGitStatus).toHaveBeenCalledTimes(gitReads);
803
+ });
804
+
805
+ it("installs exactly one timer only while active candidates require time or duration", async () => {
806
+ const setIntervalSpy = vi.spyOn(globalThis, "setInterval");
807
+ const clearIntervalSpy = vi.spyOn(globalThis, "clearInterval");
808
+ const { handlers, command } = loadExtension();
809
+ const ctx = createContext();
810
+ await emit(handlers, "session_start", ctx);
811
+ await settleProjectRefresh();
812
+ expect(setIntervalSpy).not.toHaveBeenCalled();
813
+
814
+ await command.handler('format "$time"', ctx);
815
+ expect(setIntervalSpy).toHaveBeenCalledTimes(1);
816
+ await command.handler('format "$duration"', ctx);
817
+ expect(setIntervalSpy).toHaveBeenCalledTimes(2);
818
+ expect(clearIntervalSpy).toHaveBeenCalledTimes(1);
819
+ await command.handler('format "$cwd"', ctx);
820
+ expect(clearIntervalSpy).toHaveBeenCalledTimes(2);
821
+
822
+ await emit(handlers, "session_shutdown", ctx);
823
+ expect(clearIntervalSpy).toHaveBeenCalledTimes(2);
824
+ });
825
+ });