@selesai/code 0.9.15 → 0.10.0

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