@selesai/code 0.9.15 → 0.10.1

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