@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,1406 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ KeybindingsManager,
5
+ Theme,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
8
+ import {
9
+ type AccentRailLayoutPatchDiagnostic,
10
+ installHostAccentRailLayoutPatch,
11
+ markAccentRailLayoutEditor,
12
+ retainAccentRailLayoutPatchInstallation,
13
+ } from "./accent-rail-layout-patch";
14
+ import {
15
+ type AccentRailEditorStyleConfig,
16
+ type ContextStyle,
17
+ type EditorComponentConfig,
18
+ type ExtensionStatusColorMode,
19
+ type ExtensionStatusPlacement,
20
+ ensureConfigExists,
21
+ FOOTER_FORMAT_ALIASES,
22
+ type FooterComponentConfig,
23
+ type FooterSegmentsConfig,
24
+ type GitBranchConfig,
25
+ type GitCommitConfig,
26
+ type GitMetricsConfig,
27
+ hasUnsupportedComponentStyle,
28
+ type IconMode,
29
+ loadConfig,
30
+ type MinimalistConfig,
31
+ type PathDisplayConfig,
32
+ type PolishedCopyFriendlyEditorStyleConfig,
33
+ type PolishedEditorStyleConfig,
34
+ type PolishedTuiConfig,
35
+ type SelectorBordersComponentConfig,
36
+ type SeparatorStyle,
37
+ saveAccentRailEditorStylePatch,
38
+ saveEditorComponentPatch,
39
+ saveExtensionStatusColorMode,
40
+ saveExtensionStatusDefaultPlacement,
41
+ saveExtensionStatusPlacement,
42
+ saveFooterComponentPatch,
43
+ saveIconsModePatch,
44
+ saveMinimalistEditorStylePatch,
45
+ savePolishedCopyFriendlyEditorStylePatch,
46
+ savePolishedEditorStylePatch,
47
+ saveSelectorBordersComponentPatch,
48
+ saveStarshipFooterStylePatch,
49
+ saveUserMessagesComponentPatch,
50
+ saveWorkingLineComponentPatch,
51
+ type UserMessagesComponentConfig,
52
+ type WorkingLineComponentPatch,
53
+ type ZentuiConfig,
54
+ } from "./config";
55
+ import {
56
+ type EditorTransferFailureReason,
57
+ replaceEditorComponentWithExpandedText,
58
+ } from "./editor-transfer";
59
+ import { installFooter, installHiddenFooter } from "./footer";
60
+ import { collectFooterFormatReferences, parseFooterFormat } from "./footer-format";
61
+ import {
62
+ buildSessionDurationLabel,
63
+ invalidateUsageTotalsCache,
64
+ resolveContextUsage,
65
+ } from "./format";
66
+ import { emptyGitStatus, readGitStatus } from "./git";
67
+ import {
68
+ InteractionMetricsTracker,
69
+ renderTurnSummaryEntry,
70
+ TURN_SUMMARY_ENTRY_TYPE,
71
+ } from "./interaction-summary";
72
+ import { LiveContextController } from "./live-context";
73
+ import { readPackageVersionResult } from "./package-version";
74
+ import {
75
+ createProjectRefreshScheduler,
76
+ type ProjectRefreshRun,
77
+ type ScheduleProjectRefreshOptions,
78
+ type StopProjectRefreshInterval,
79
+ startProjectRefreshInterval,
80
+ } from "./project-refresh";
81
+ import { applyProjectRefreshToState } from "./project-state";
82
+ import { RepositoryRootController, type RepositoryRootRequest } from "./repository-root";
83
+ import { readRuntimeInfo } from "./runtime";
84
+ import { installSelectorBorderStyle, removeSelectorBorderStyle } from "./selector-border";
85
+ import { SessionLifecycle } from "./session-lifecycle";
86
+ import { registerZentuiSettingsCommand } from "./settings-command";
87
+ import { createInitialState, type FooterState, modelLabelFor, syncState } from "./state";
88
+ import { resolveFooterTelemetry } from "./telemetry";
89
+ import { PolishedEditor, WrappedPolishedEditor } from "./ui";
90
+ import { installUserMessageStyle, removeUserMessageStyle } from "./user-message";
91
+ import {
92
+ AgentDurationClock,
93
+ snapshotWorkingLineHighStyle,
94
+ WorkingLineController,
95
+ } from "./working-line";
96
+
97
+ const ZENTUI_EDITOR_FACTORY = Symbol.for("pi-zentui.editor-factory");
98
+ const ZENTUI_EDITOR_BASE_FACTORY = Symbol.for("pi-zentui.editor-base-factory");
99
+ const ZENTUI_EDITOR_OWNER = Symbol.for("pi-zentui.editor-owner");
100
+ const ZENTUI_FOOTER_OWNER = Symbol.for("pi-zentui.footer-owner");
101
+
102
+ type EditorFactory = NonNullable<Parameters<ExtensionContext["ui"]["setEditorComponent"]>[0]>;
103
+
104
+ type ZentuiEditorFactory = EditorFactory & {
105
+ [ZENTUI_EDITOR_FACTORY]?: true;
106
+ [ZENTUI_EDITOR_BASE_FACTORY]?: EditorFactory;
107
+ [ZENTUI_EDITOR_OWNER]?: symbol;
108
+ };
109
+
110
+ type ApplyUiResult = {
111
+ editorBlocked: boolean;
112
+ editorReason?: string;
113
+ };
114
+
115
+ type EditorChangeResult = { ok: true } | { ok: false; reason: string };
116
+
117
+ type EditorInstallMode = "none" | "standalone" | "wrapper";
118
+ type InstalledFooterKind = "starship" | "hidden";
119
+
120
+ function editorTransferFailureMessage(reason: EditorTransferFailureReason): string {
121
+ switch (reason) {
122
+ case "unsupported-transfer-api":
123
+ return "this Pi version cannot safely transfer expanded editor text; reload Pi to apply this change";
124
+ case "editor-factory-snapshot-failed":
125
+ return "the current editor factory could not be read safely; reload Pi to apply this change";
126
+ case "editor-text-snapshot-failed":
127
+ return "expanded editor text could not be read safely; reload Pi to apply this change";
128
+ case "editor-text-preparation-failed":
129
+ return "expanded editor text could not be prepared safely; reload Pi to apply this change";
130
+ case "editor-replacement-failed-with-rollback":
131
+ return "the editor replacement failed; the previous factory was reapplied, but editor instance identity is not guaranteed";
132
+ case "editor-replacement-rollback-failed":
133
+ return "the editor replacement and previous-factory rollback both failed; reload Pi before editing";
134
+ }
135
+ }
136
+
137
+ function isZentuiEditorFactory(factory: EditorFactory | undefined): boolean {
138
+ return Boolean((factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_FACTORY]);
139
+ }
140
+
141
+ function getZentuiEditorBaseFactory(factory: EditorFactory | undefined): EditorFactory | undefined {
142
+ return (factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_BASE_FACTORY];
143
+ }
144
+
145
+ export function activeFooterReferences(config: ZentuiConfig): Set<string> {
146
+ const starship = config.components.footer.styles.starship;
147
+ const references = starship.format
148
+ ? collectFooterFormatReferences(parseFooterFormat(starship.format), FOOTER_FORMAT_ALIASES)
149
+ : new Set<string>([
150
+ ...(starship.segments.sessionName ? ["session_name"] : []),
151
+ ...(starship.segments.runtime ? ["runtime"] : []),
152
+ ...(starship.segments.gitCommit ? ["git_commit"] : []),
153
+ ...(starship.segments.gitMetrics ? ["git_metrics"] : []),
154
+ ...(starship.segments.packageVersion ? ["package"] : []),
155
+ ...(starship.segments.sessionDuration ? ["session_duration"] : []),
156
+ ...(starship.segments.time ? ["time"] : []),
157
+ ]);
158
+ if (starship.responsive) {
159
+ for (const name of collectFooterFormatReferences(
160
+ parseFooterFormat(starship.compactFormat),
161
+ FOOTER_FORMAT_ALIASES,
162
+ )) {
163
+ references.add(name);
164
+ }
165
+ }
166
+ return references;
167
+ }
168
+
169
+ function isTuiContext(ctx: ExtensionContext): boolean {
170
+ try {
171
+ const mode = (ctx as ExtensionContext & { mode?: string }).mode;
172
+ return ctx.hasUI && (mode === undefined || mode === "tui");
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+
178
+ export default function (pi: ExtensionAPI) {
179
+ const state: FooterState = createInitialState(emptyGitStatus());
180
+ const sessionLifecycle = new SessionLifecycle();
181
+ const editorOwnerToken = Symbol("zentui-editor-owner");
182
+
183
+ let currentConfig: PolishedTuiConfig = loadConfig();
184
+ // Keep the capability guard defensive for hosts with incomplete extension APIs.
185
+ if (typeof pi.registerEntryRenderer === "function") {
186
+ pi.registerEntryRenderer(TURN_SUMMARY_ENTRY_TYPE, (entry, options, theme) =>
187
+ renderTurnSummaryEntry(
188
+ entry,
189
+ {
190
+ ...options,
191
+ colorSource: currentConfig.components.workingLine.colorSource,
192
+ workingLineHigh: currentConfig.colors.workingLineHigh,
193
+ },
194
+ theme,
195
+ ),
196
+ );
197
+ }
198
+ let activeTheme: Theme | undefined;
199
+ let requestFooterRender: (() => void) | undefined;
200
+ let requestEditorRender: (() => void) | undefined;
201
+ let getActiveExtensionStatuses: () => ReadonlyMap<string, string> = () => new Map();
202
+ let stopRefreshInterval: StopProjectRefreshInterval = () => {};
203
+ let cleanupUserMessageStyle: () => void = () => {};
204
+ let userMessageStyleInstalled = false;
205
+ let cleanupSelectorBorderStyle: () => void = () => {};
206
+ let selectorBorderStyleInstalled = false;
207
+ let installedFooterKind: InstalledFooterKind | undefined;
208
+ let installedFooterToken: symbol | undefined;
209
+ let editorInstalled = false;
210
+ let editorInstallMode: EditorInstallMode = "none";
211
+ let installedEditorFactory: EditorFactory | undefined;
212
+ let wrappedEditorFactory: EditorFactory | undefined;
213
+ let stopSessionTimer: () => void = () => {};
214
+ let stopMinimalistDurationUpdates: () => void = () => {};
215
+ let minimalistDurationUpdatesActive = false;
216
+ let minimalistDecorationActive = false;
217
+ let sessionTimerRequirements = "";
218
+ let lastDurationLabel = "";
219
+ let lastProjectCwd: string | undefined;
220
+ const agentDurationClock = new AgentDurationClock();
221
+ const interactionMetrics = new InteractionMetricsTracker();
222
+ let agentRunActive = false;
223
+ let minimalistProjectRoot: string | undefined;
224
+ const repositoryRoots = new RepositoryRootController();
225
+ let projectRefreshActive = false;
226
+ let activeTuiContext: ExtensionContext | undefined;
227
+ let cleanupAccentRailLayoutPatch: () => void = () => {};
228
+ let accentRailLayoutPatchInstallSerial = 0;
229
+
230
+ const recordAccentRailLayoutPatchDiagnostic = (
231
+ diagnostic: AccentRailLayoutPatchDiagnostic,
232
+ version?: string,
233
+ ) => {
234
+ if (process.env.ZENTUI_DEBUG === "1") {
235
+ console.error(
236
+ `[zentui] Accent Rail fullscreen layout patch: ${diagnostic}${version ? ` (Pi TUI ${version})` : ""}`,
237
+ );
238
+ }
239
+ };
240
+
241
+ const isOwnedEditorFactory = (factory: EditorFactory | undefined) =>
242
+ (factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_OWNER] === editorOwnerToken;
243
+ const effectiveEditorEnabled = () =>
244
+ currentConfig.components.editor.enabled &&
245
+ !hasUnsupportedComponentStyle(currentConfig, "editor");
246
+ const effectiveUserMessagesEnabled = () =>
247
+ currentConfig.components.userMessages.enabled &&
248
+ !hasUnsupportedComponentStyle(currentConfig, "userMessages");
249
+ const effectiveSelectorBordersEnabled = () =>
250
+ currentConfig.components.selectorBorders.enabled &&
251
+ !hasUnsupportedComponentStyle(currentConfig, "selectorBorders");
252
+ const effectiveFooterStyle = () =>
253
+ hasUnsupportedComponentStyle(currentConfig, "footer")
254
+ ? ("native" as const)
255
+ : currentConfig.components.footer.style;
256
+
257
+ const ownsInstalledEditorFactory = () => {
258
+ if (
259
+ !sessionLifecycle.isCurrent() ||
260
+ !editorInstalled ||
261
+ !installedEditorFactory ||
262
+ !activeTuiContext
263
+ ) {
264
+ return false;
265
+ }
266
+ try {
267
+ return (
268
+ isOwnedEditorFactory(installedEditorFactory) &&
269
+ activeTuiContext.ui.getEditorComponent() === installedEditorFactory
270
+ );
271
+ } catch {
272
+ return false;
273
+ }
274
+ };
275
+
276
+ const refresh = () => {
277
+ if (!sessionLifecycle.isCurrent()) return;
278
+ requestFooterRender?.();
279
+ requestEditorRender?.();
280
+ };
281
+ const liveContext = new LiveContextController(sessionLifecycle, refresh);
282
+ const getActiveTheme = () => activeTheme;
283
+ const getCurrentConfig = () => currentConfig;
284
+ const workingLine = new WorkingLineController(
285
+ getCurrentConfig,
286
+ () => activeTheme as Theme,
287
+ agentDurationClock,
288
+ Math.random,
289
+ Date.now,
290
+ () => interactionMetrics.currentThought(),
291
+ );
292
+ const getContextSnapshot = (ctx: ExtensionContext) => resolveContextUsage(ctx, liveContext.get());
293
+ const getContextWindow = (ctx: ExtensionContext): number | undefined =>
294
+ getContextSnapshot(ctx).contextWindow;
295
+ const getContextPercent = (ctx: ExtensionContext): number | undefined =>
296
+ getContextSnapshot(ctx).percent;
297
+ const getEditorMeta = (ctx: ExtensionContext) => {
298
+ const context = getContextSnapshot(ctx);
299
+ return {
300
+ modelLabel: modelLabelFor(state, currentConfig.components.editor.modelLabel),
301
+ modelId: state.modelId,
302
+ modelName: state.modelName,
303
+ providerLabel: state.providerLabel,
304
+ sessionName: ctx.sessionManager.getSessionName() ?? "",
305
+ contextPercent: context.percent,
306
+ contextWindow: context.contextWindow,
307
+ inputTokens: state.usageTotals.input,
308
+ outputTokens: state.usageTotals.output,
309
+ cacheHitRate: state.usageTotals.latestCacheHitRate,
310
+ };
311
+ };
312
+ const getAgentDurationMs = () => agentDurationClock.elapsedMs();
313
+ const getThinkingLevel = () =>
314
+ sessionLifecycle.isCurrent() ? pi.getThinkingLevel() : ("off" as const);
315
+ const syncFooterState = (ctx: ExtensionContext) =>
316
+ syncState(state, ctx, currentConfig.icons.cacheHit, resolveFooterTelemetry(ctx));
317
+ const ownsInstalledFooter = () =>
318
+ Boolean(
319
+ activeTuiContext &&
320
+ installedFooterToken &&
321
+ ctxFooterOwner(activeTuiContext) === installedFooterToken,
322
+ );
323
+ const installedFooterReferences = () =>
324
+ installedFooterKind === "starship" && ownsInstalledFooter()
325
+ ? activeFooterReferences(currentConfig)
326
+ : new Set<string>();
327
+
328
+ type ProjectRefreshTarget = {
329
+ repository: RepositoryRootRequest;
330
+ sessionGeneration: number;
331
+ };
332
+ const refreshProjectState = async (
333
+ { repository, sessionGeneration }: ProjectRefreshTarget,
334
+ run: ProjectRefreshRun,
335
+ ) => {
336
+ const { cwd } = repository;
337
+ if (
338
+ !run.isCurrent() ||
339
+ !sessionLifecycle.isCurrent(sessionGeneration) ||
340
+ !repositoryRoots.isCurrent(repository)
341
+ ) {
342
+ return;
343
+ }
344
+ const starship = currentConfig.components.footer.styles.starship;
345
+ const gitCommitConfig = starship.gitCommit;
346
+ const gitMetricsConfig = starship.gitMetrics;
347
+ const references = installedFooterReferences();
348
+ const wantExactTag =
349
+ (references.has("git_commit") && gitCommitConfig.showTag) || references.has("git_tag");
350
+ const wantMetrics =
351
+ references.has("git_metrics") || references.has("git_added") || references.has("git_deleted");
352
+ const wantPackage = references.has("package") || references.has("package_version");
353
+ const wantRuntime = references.has("runtime");
354
+ const [git, runtime, packageVersion] = await Promise.all([
355
+ readGitStatus(cwd, {
356
+ readExactTag: wantExactTag,
357
+ readMetrics: wantMetrics,
358
+ ignoreSubmodules: gitMetricsConfig.ignoreSubmodules,
359
+ }),
360
+ wantRuntime
361
+ ? readRuntimeInfo(cwd)
362
+ : Promise.resolve({ kind: "ok" as const, runtime: undefined }),
363
+ wantPackage
364
+ ? readPackageVersionResult(cwd)
365
+ : Promise.resolve({ kind: "ok" as const, result: null }),
366
+ ]);
367
+ if (
368
+ !run.isCurrent() ||
369
+ !sessionLifecycle.isCurrent(sessionGeneration) ||
370
+ !repositoryRoots.isCurrent(repository)
371
+ ) {
372
+ return;
373
+ }
374
+ minimalistProjectRoot = repositoryRoots.update(repository, git.kind === "ok");
375
+ lastProjectCwd = applyProjectRefreshToState(state, {
376
+ cwd,
377
+ previousCwd: lastProjectCwd,
378
+ git,
379
+ runtime,
380
+ packageVersion,
381
+ });
382
+ };
383
+
384
+ const projectRefreshScheduler = createProjectRefreshScheduler(refreshProjectState, refresh);
385
+ const scheduleProjectRefresh = (
386
+ ctx: ExtensionContext,
387
+ options?: ScheduleProjectRefreshOptions,
388
+ ) => {
389
+ const sessionGeneration = sessionLifecycle.currentGeneration();
390
+ if (!sessionLifecycle.isCurrent(sessionGeneration)) return;
391
+ const repository = repositoryRoots.request(ctx.cwd);
392
+ minimalistProjectRoot = repositoryRoots.cachedRootForCwd(ctx.cwd);
393
+ projectRefreshScheduler.schedule({ repository, sessionGeneration }, options);
394
+ };
395
+
396
+ const minimalistProjectRequired = () => {
397
+ const editor = currentConfig.components.editor;
398
+ const minimalist = editor.styles.minimalist;
399
+ return (
400
+ effectiveEditorEnabled() &&
401
+ ownsInstalledEditorFactory() &&
402
+ editor.style === "minimalist" &&
403
+ (minimalist.showGit || minimalist.pathDisplay === "project")
404
+ );
405
+ };
406
+
407
+ const needsProjectRefresh = () =>
408
+ (installedFooterKind === "starship" && ownsInstalledFooter()) || minimalistProjectRequired();
409
+
410
+ const stopProjectRefresh = () => {
411
+ stopRefreshInterval();
412
+ stopRefreshInterval = () => {};
413
+ projectRefreshScheduler.stop();
414
+ projectRefreshActive = false;
415
+ };
416
+
417
+ const reconcileProjectRefresh = (ctx: ExtensionContext, force = false) => {
418
+ if (!sessionLifecycle.isCurrent() || !needsProjectRefresh()) {
419
+ stopProjectRefresh();
420
+ return;
421
+ }
422
+ const activated = !projectRefreshActive;
423
+ if (activated) {
424
+ stopRefreshInterval = startProjectRefreshInterval(
425
+ currentConfig.projectRefreshIntervalMs,
426
+ () => {
427
+ if (editorInstalled && !ownsInstalledEditorFactory()) {
428
+ reconcileObservedEditorOwnership(ctx);
429
+ }
430
+ if (!needsProjectRefresh()) {
431
+ stopProjectRefresh();
432
+ return;
433
+ }
434
+ scheduleProjectRefresh(ctx);
435
+ },
436
+ );
437
+ projectRefreshActive = true;
438
+ }
439
+ if (force && !activated) projectRefreshScheduler.invalidate();
440
+ if (force || activated) scheduleProjectRefresh(ctx, { force: true });
441
+ };
442
+
443
+ const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
444
+ if (!sessionLifecycle.isCurrent() || !ctx.hasUI) return;
445
+ if (editorInstalled && !ownsInstalledEditorFactory()) reconcileObservedEditorOwnership(ctx);
446
+ syncFooterState(ctx);
447
+ if (project && needsProjectRefresh()) scheduleProjectRefresh(ctx);
448
+ refresh();
449
+ };
450
+
451
+ const reconcileSessionTimer = () => {
452
+ const references = installedFooterReferences();
453
+ const needsTime = references.has("time");
454
+ const needsDuration = references.has("session_duration");
455
+ const nextRequirements = needsTime || needsDuration ? `${needsTime}:${needsDuration}` : "";
456
+ if (
457
+ !sessionLifecycle.isCurrent() ||
458
+ installedFooterKind !== "starship" ||
459
+ !ownsInstalledFooter() ||
460
+ !nextRequirements
461
+ ) {
462
+ stopSessionTimer();
463
+ sessionTimerRequirements = "";
464
+ lastDurationLabel = "";
465
+ return;
466
+ }
467
+ if (sessionTimerRequirements === nextRequirements) return;
468
+
469
+ stopSessionTimer();
470
+ sessionTimerRequirements = nextRequirements;
471
+ lastDurationLabel = "";
472
+ const timer = setInterval(() => {
473
+ if (!sessionLifecycle.isCurrent()) return;
474
+ if (needsTime) {
475
+ refresh();
476
+ return;
477
+ }
478
+ const label = state.sessionStartEpoch
479
+ ? buildSessionDurationLabel(state.sessionStartEpoch)
480
+ : "";
481
+ if (label === lastDurationLabel) return;
482
+ lastDurationLabel = label;
483
+ refresh();
484
+ }, 1000);
485
+ stopSessionTimer = () => {
486
+ clearInterval(timer);
487
+ sessionTimerRequirements = "";
488
+ stopSessionTimer = () => {};
489
+ };
490
+ };
491
+
492
+ const reconcileAgentTimer = () => {
493
+ const needed =
494
+ sessionLifecycle.isCurrent() &&
495
+ agentRunActive &&
496
+ agentDurationClock.isActive() &&
497
+ minimalistDecorationActive &&
498
+ effectiveEditorEnabled() &&
499
+ ownsInstalledEditorFactory() &&
500
+ currentConfig.components.editor.style === "minimalist" &&
501
+ currentConfig.components.editor.styles.minimalist.showTimer;
502
+ if (!needed) {
503
+ stopMinimalistDurationUpdates();
504
+ stopMinimalistDurationUpdates = () => {};
505
+ minimalistDurationUpdatesActive = false;
506
+ return;
507
+ }
508
+ if (minimalistDurationUpdatesActive) return;
509
+ minimalistDurationUpdatesActive = true;
510
+ stopMinimalistDurationUpdates = agentDurationClock.subscribe(() => {
511
+ const ctx = activeTuiContext;
512
+ if (ctx && editorInstalled && !ownsInstalledEditorFactory()) {
513
+ reconcileObservedEditorOwnership(ctx);
514
+ }
515
+ reconcileAgentTimer();
516
+ if (minimalistDurationUpdatesActive) refresh();
517
+ });
518
+ };
519
+
520
+ const setMinimalistDecorationActive = (active: boolean) => {
521
+ const next = sessionLifecycle.isCurrent() && active && ownsInstalledEditorFactory();
522
+ if (minimalistDecorationActive === next) return;
523
+ minimalistDecorationActive = next;
524
+ reconcileAgentTimer();
525
+ };
526
+
527
+ const startAgentTurn = (interactionStarted: boolean) => {
528
+ agentRunActive = true;
529
+ if (interactionStarted) agentDurationClock.start();
530
+ reconcileAgentTimer();
531
+ refresh();
532
+ };
533
+
534
+ const pauseAgentRun = () => {
535
+ agentRunActive = false;
536
+ reconcileAgentTimer();
537
+ refresh();
538
+ };
539
+
540
+ const settleAgentTurn = (nextStartedAt?: number) => {
541
+ agentRunActive = nextStartedAt !== undefined;
542
+ if (nextStartedAt === undefined) agentDurationClock.finish();
543
+ else agentDurationClock.start(nextStartedAt);
544
+ reconcileAgentTimer();
545
+ refresh();
546
+ };
547
+
548
+ const resetAgentTimer = () => {
549
+ stopMinimalistDurationUpdates();
550
+ stopMinimalistDurationUpdates = () => {};
551
+ minimalistDurationUpdatesActive = false;
552
+ agentRunActive = false;
553
+ agentDurationClock.reset();
554
+ };
555
+
556
+ const sameReferences = (left: Set<string>, right: Set<string>) =>
557
+ left.size === right.size && [...left].every((name) => right.has(name));
558
+
559
+ const applyFooterDependencyConfigChange = (
560
+ ctx: ExtensionContext,
561
+ save: () => PolishedTuiConfig,
562
+ ) => {
563
+ const before = activeFooterReferences(currentConfig);
564
+ const nextConfig = save();
565
+ const after = activeFooterReferences(nextConfig);
566
+ currentConfig = nextConfig;
567
+ if (sameReferences(before, after)) return;
568
+ reconcileSessionTimer();
569
+ reconcileProjectRefresh(ctx, true);
570
+ };
571
+
572
+ const installUserMessages = () => {
573
+ if (userMessageStyleInstalled) return;
574
+ let cleanup: (() => void) | undefined;
575
+ try {
576
+ cleanup = installUserMessageStyle(getActiveTheme, getCurrentConfig);
577
+ cleanupUserMessageStyle = cleanup;
578
+ userMessageStyleInstalled = true;
579
+ } catch {
580
+ try {
581
+ cleanup?.();
582
+ } catch {
583
+ // Best effort: the installer is locally transactional.
584
+ }
585
+ cleanupUserMessageStyle = () => {};
586
+ userMessageStyleInstalled = false;
587
+ }
588
+ };
589
+
590
+ const uninstallUserMessages = () => {
591
+ try {
592
+ cleanupUserMessageStyle();
593
+ } catch {
594
+ // Best effort cleanup.
595
+ } finally {
596
+ cleanupUserMessageStyle = () => {};
597
+ userMessageStyleInstalled = false;
598
+ }
599
+ };
600
+
601
+ const reconcileUserMessages = () => {
602
+ if (effectiveUserMessagesEnabled()) installUserMessages();
603
+ else uninstallUserMessages();
604
+ };
605
+
606
+ const installSelectorBorders = () => {
607
+ if (selectorBorderStyleInstalled) return;
608
+ let cleanup: (() => void) | undefined;
609
+ try {
610
+ cleanup = installSelectorBorderStyle(getActiveTheme, getCurrentConfig);
611
+ cleanupSelectorBorderStyle = cleanup;
612
+ selectorBorderStyleInstalled = true;
613
+ } catch {
614
+ try {
615
+ cleanup?.();
616
+ } catch {
617
+ // Best effort: the installer is locally transactional.
618
+ }
619
+ cleanupSelectorBorderStyle = () => {};
620
+ selectorBorderStyleInstalled = false;
621
+ }
622
+ };
623
+
624
+ const uninstallSelectorBorders = () => {
625
+ try {
626
+ cleanupSelectorBorderStyle();
627
+ } catch {
628
+ // Best effort cleanup.
629
+ } finally {
630
+ cleanupSelectorBorderStyle = () => {};
631
+ selectorBorderStyleInstalled = false;
632
+ }
633
+ };
634
+
635
+ const reconcileSelectorBorders = () => {
636
+ const selectors = currentConfig.components.selectorBorders;
637
+ if (effectiveSelectorBordersEnabled() && selectors.style === "zentui") {
638
+ installSelectorBorders();
639
+ } else uninstallSelectorBorders();
640
+ };
641
+
642
+ const clearEditorOwnership = () => {
643
+ setMinimalistDecorationActive(false);
644
+ requestEditorRender = undefined;
645
+ wrappedEditorFactory = undefined;
646
+ installedEditorFactory = undefined;
647
+ editorInstallMode = "none";
648
+ editorInstalled = false;
649
+ };
650
+
651
+ const trackZentuiEditorFactory = (factory: EditorFactory): boolean => {
652
+ if (!isOwnedEditorFactory(factory)) return false;
653
+ const baseFactory = getZentuiEditorBaseFactory(factory);
654
+ wrappedEditorFactory = baseFactory;
655
+ installedEditorFactory = factory;
656
+ editorInstallMode = baseFactory ? "wrapper" : "standalone";
657
+ editorInstalled = true;
658
+ return true;
659
+ };
660
+
661
+ const observeEditorFactory = (
662
+ ctx: ExtensionContext,
663
+ ): { known: true; factory: EditorFactory | undefined } | { known: false } => {
664
+ try {
665
+ return { known: true, factory: ctx.ui.getEditorComponent() };
666
+ } catch {
667
+ return { known: false };
668
+ }
669
+ };
670
+
671
+ const reconcileObservedEditorOwnership = (ctx: ExtensionContext) => {
672
+ const observed = observeEditorFactory(ctx);
673
+ if (!observed.known) return observed;
674
+ if (observed.factory && isOwnedEditorFactory(observed.factory)) {
675
+ trackZentuiEditorFactory(observed.factory);
676
+ } else {
677
+ clearEditorOwnership();
678
+ reconcileProjectRefresh(ctx);
679
+ }
680
+ return observed;
681
+ };
682
+
683
+ const accentRailLayoutActive = () =>
684
+ sessionLifecycle.isCurrent() &&
685
+ ownsInstalledEditorFactory() &&
686
+ effectiveEditorEnabled() &&
687
+ currentConfig.components.editor.style === "accent-rail";
688
+
689
+ const markOwnedAccentRailEditor = <T extends object>(editor: T): T => {
690
+ markAccentRailLayoutEditor(editor, editorOwnerToken, accentRailLayoutActive);
691
+ return editor;
692
+ };
693
+
694
+ const makeEditorFactory = (ctx: ExtensionContext): ZentuiEditorFactory => {
695
+ const sessionTheme = ctx.ui.theme;
696
+ const factory = ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
697
+ requestEditorRender = () => tui.requestRender();
698
+ return markOwnedAccentRailEditor(
699
+ new PolishedEditor(
700
+ tui,
701
+ theme,
702
+ keybindings,
703
+ sessionTheme,
704
+ getCurrentConfig,
705
+ () => getEditorMeta(ctx),
706
+ getThinkingLevel,
707
+ () => ({
708
+ cwd: ctx.cwd,
709
+ projectRoot: minimalistProjectRoot,
710
+ branch: state.branch,
711
+ dirty: state.dirty,
712
+ ahead: state.ahead,
713
+ behind: state.behind,
714
+ costLabel: state.costLabel,
715
+ modelLabel: modelLabelFor(state, currentConfig.components.editor.modelLabel),
716
+ thinkingLevel: getThinkingLevel(),
717
+ contextPercent: getContextPercent(ctx),
718
+ contextWindow: getContextWindow(ctx),
719
+ sessionName: ctx.sessionManager.getSessionName() ?? "",
720
+ agentDurationMs: getAgentDurationMs(),
721
+ agentActive: agentRunActive,
722
+ }),
723
+ setMinimalistDecorationActive,
724
+ ),
725
+ );
726
+ }) as ZentuiEditorFactory;
727
+ factory[ZENTUI_EDITOR_FACTORY] = true;
728
+ factory[ZENTUI_EDITOR_OWNER] = editorOwnerToken;
729
+ return factory;
730
+ };
731
+
732
+ const makeWrappedEditorFactory = (
733
+ ctx: ExtensionContext,
734
+ baseFactory: EditorFactory,
735
+ ): ZentuiEditorFactory => {
736
+ const sessionTheme = ctx.ui.theme;
737
+ const factory = ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
738
+ requestEditorRender = () => tui.requestRender();
739
+ return markOwnedAccentRailEditor(
740
+ new WrappedPolishedEditor(
741
+ baseFactory(tui, theme, keybindings),
742
+ sessionTheme,
743
+ getCurrentConfig,
744
+ () => getEditorMeta(ctx),
745
+ getThinkingLevel,
746
+ () => ({
747
+ cwd: ctx.cwd,
748
+ projectRoot: minimalistProjectRoot,
749
+ branch: state.branch,
750
+ dirty: state.dirty,
751
+ ahead: state.ahead,
752
+ behind: state.behind,
753
+ costLabel: state.costLabel,
754
+ modelLabel: modelLabelFor(state, currentConfig.components.editor.modelLabel),
755
+ thinkingLevel: getThinkingLevel(),
756
+ contextPercent: getContextPercent(ctx),
757
+ contextWindow: getContextWindow(ctx),
758
+ sessionName: ctx.sessionManager.getSessionName() ?? "",
759
+ agentDurationMs: getAgentDurationMs(),
760
+ agentActive: agentRunActive,
761
+ }),
762
+ setMinimalistDecorationActive,
763
+ ),
764
+ );
765
+ }) as ZentuiEditorFactory;
766
+ factory[ZENTUI_EDITOR_FACTORY] = true;
767
+ factory[ZENTUI_EDITOR_OWNER] = editorOwnerToken;
768
+ factory[ZENTUI_EDITOR_BASE_FACTORY] = baseFactory;
769
+ return factory;
770
+ };
771
+
772
+ const replaceEditor = (
773
+ ctx: ExtensionContext,
774
+ factory: EditorFactory | undefined,
775
+ ): EditorChangeResult => {
776
+ const result = replaceEditorComponentWithExpandedText(ctx.ui, factory);
777
+ return result.ok ? result : { ok: false, reason: editorTransferFailureMessage(result.reason) };
778
+ };
779
+
780
+ const installEditor = (ctx: ExtensionContext): EditorChangeResult => {
781
+ const currentFactory = ctx.ui.getEditorComponent();
782
+ if (currentFactory && currentFactory === installedEditorFactory) {
783
+ editorInstalled = true;
784
+ return { ok: true };
785
+ }
786
+
787
+ const currentZentuiBaseFactory = getZentuiEditorBaseFactory(currentFactory);
788
+ const baseFactory =
789
+ currentZentuiBaseFactory ??
790
+ (currentFactory && !isZentuiEditorFactory(currentFactory) ? currentFactory : undefined);
791
+ const nextFactory = baseFactory
792
+ ? makeWrappedEditorFactory(ctx, baseFactory)
793
+ : makeEditorFactory(ctx);
794
+ const replacement = replaceEditor(ctx, nextFactory);
795
+ if (!replacement.ok) return replacement;
796
+
797
+ trackZentuiEditorFactory(nextFactory);
798
+ return { ok: true };
799
+ };
800
+
801
+ const uninstallEditor = (
802
+ ctx: ExtensionContext,
803
+ options: { allowStaleZentui?: boolean } = {},
804
+ ): EditorChangeResult => {
805
+ const observed = observeEditorFactory(ctx);
806
+ if (!observed.known) {
807
+ return {
808
+ ok: false,
809
+ reason:
810
+ "the current editor factory could not be observed safely; reload Pi to apply this change",
811
+ };
812
+ }
813
+ const currentFactory = observed.factory;
814
+ if (!currentFactory || !isZentuiEditorFactory(currentFactory)) {
815
+ clearEditorOwnership();
816
+ return { ok: true };
817
+ }
818
+ if (!isOwnedEditorFactory(currentFactory) && !options.allowStaleZentui) {
819
+ clearEditorOwnership();
820
+ return { ok: true };
821
+ }
822
+
823
+ const replacement = replaceEditor(
824
+ ctx,
825
+ getZentuiEditorBaseFactory(currentFactory) ??
826
+ (editorInstallMode === "wrapper" && wrappedEditorFactory
827
+ ? wrappedEditorFactory
828
+ : undefined),
829
+ );
830
+ if (!replacement.ok) return replacement;
831
+
832
+ clearEditorOwnership();
833
+ return { ok: true };
834
+ };
835
+
836
+ const ctxFooterOwner = (ctx: ExtensionContext): unknown =>
837
+ (ctx.ui as unknown as Record<PropertyKey, unknown>)[ZENTUI_FOOTER_OWNER];
838
+
839
+ const setStatusLineOwnership = (ctx: ExtensionContext, token: symbol | undefined) => {
840
+ const ui = ctx.ui as unknown as Record<PropertyKey, unknown>;
841
+ try {
842
+ if (token) ui[ZENTUI_FOOTER_OWNER] = token;
843
+ else delete ui[ZENTUI_FOOTER_OWNER];
844
+ } catch {
845
+ // Failure to mark ownership intentionally prevents Native from restoring it.
846
+ }
847
+ };
848
+
849
+ const ownsStatusLine = (ctx: ExtensionContext) =>
850
+ installedFooterToken !== undefined && ctxFooterOwner(ctx) === installedFooterToken;
851
+
852
+ const clearFooterOwnership = (ctx: ExtensionContext, token: symbol) => {
853
+ if (installedFooterToken !== token) return;
854
+ installedFooterKind = undefined;
855
+ installedFooterToken = undefined;
856
+ if (ctxFooterOwner(ctx) === token) setStatusLineOwnership(ctx, undefined);
857
+ requestFooterRender = undefined;
858
+ getActiveExtensionStatuses = () => new Map();
859
+ stopSessionTimer();
860
+ if (sessionLifecycle.isCurrent()) reconcileProjectRefresh(ctx, true);
861
+ };
862
+
863
+ type FooterBookkeepingSnapshot = {
864
+ token: symbol | undefined;
865
+ requestRender: (() => void) | undefined;
866
+ getExtensionStatuses: () => ReadonlyMap<string, string>;
867
+ };
868
+
869
+ const snapshotFooterBookkeeping = (ctx: ExtensionContext): FooterBookkeepingSnapshot => ({
870
+ token: ownsStatusLine(ctx) ? installedFooterToken : undefined,
871
+ requestRender: requestFooterRender,
872
+ getExtensionStatuses: getActiveExtensionStatuses,
873
+ });
874
+
875
+ const resetFailedFooterInstallation = (
876
+ ctx: ExtensionContext,
877
+ token: symbol,
878
+ previous: FooterBookkeepingSnapshot,
879
+ ) => {
880
+ // Pi has no transactional Footer replacement API. If a live replacement
881
+ // fails while retaining our predecessor, preserve that Footer's callbacks
882
+ // and timer. Otherwise clear only local bookkeeping; never issue a
883
+ // destructive setFooter(undefined) rollback.
884
+ if (
885
+ previous.token !== undefined &&
886
+ installedFooterToken === previous.token &&
887
+ ctxFooterOwner(ctx) === previous.token
888
+ ) {
889
+ requestFooterRender = previous.requestRender;
890
+ getActiveExtensionStatuses = previous.getExtensionStatuses;
891
+ return;
892
+ }
893
+ clearFooterOwnership(ctx, token);
894
+ requestFooterRender = undefined;
895
+ getActiveExtensionStatuses = () => new Map();
896
+ stopSessionTimer();
897
+ };
898
+
899
+ const installStatusLine = (ctx: ExtensionContext) => {
900
+ if (installedFooterKind === "starship" && ownsStatusLine(ctx)) return;
901
+ const token = Symbol("zentui-starship-footer");
902
+ const previous = snapshotFooterBookkeeping(ctx);
903
+ try {
904
+ installFooter(ctx, state, getCurrentConfig, {
905
+ setRequestRender: (fn) => {
906
+ requestFooterRender = fn;
907
+ },
908
+ scheduleProjectRefresh,
909
+ setExtensionStatusesGetter(fn) {
910
+ getActiveExtensionStatuses = fn ?? (() => new Map());
911
+ },
912
+ getLiveContext: () => liveContext.get(),
913
+ getRepositoryRoot: (cwd) => repositoryRoots.rootForCwd(cwd),
914
+ onDispose: () => clearFooterOwnership(ctx, token),
915
+ });
916
+ installedFooterKind = "starship";
917
+ installedFooterToken = token;
918
+ setStatusLineOwnership(ctx, token);
919
+ refresh();
920
+ reconcileSessionTimer();
921
+ } catch {
922
+ resetFailedFooterInstallation(ctx, token, previous);
923
+ }
924
+ };
925
+
926
+ const installHiddenStatusLine = (ctx: ExtensionContext) => {
927
+ if (installedFooterKind === "hidden" && ownsStatusLine(ctx)) return;
928
+ const token = Symbol("zentui-hidden-footer");
929
+ const previous = snapshotFooterBookkeeping(ctx);
930
+ try {
931
+ installHiddenFooter(ctx, () => clearFooterOwnership(ctx, token));
932
+ installedFooterKind = "hidden";
933
+ installedFooterToken = token;
934
+ setStatusLineOwnership(ctx, token);
935
+ requestFooterRender = undefined;
936
+ getActiveExtensionStatuses = () => new Map();
937
+ stopSessionTimer();
938
+ } catch {
939
+ resetFailedFooterInstallation(ctx, token, previous);
940
+ }
941
+ };
942
+
943
+ const uninstallStatusLine = (
944
+ ctx: ExtensionContext,
945
+ options: { forceLocalCleanup?: boolean } = {},
946
+ ) => {
947
+ const ownedToken = ownsStatusLine(ctx) ? installedFooterToken : undefined;
948
+ if (!ownedToken) return;
949
+ try {
950
+ ctx.ui.setFooter(undefined);
951
+ } catch {
952
+ // A live transition must preserve an owned Footer that Pi retained.
953
+ // Shutdown cannot keep local callbacks alive, so it clears bookkeeping.
954
+ if (!options.forceLocalCleanup) return;
955
+ }
956
+ clearFooterOwnership(ctx, ownedToken);
957
+ };
958
+
959
+ const reconcileFooter = (ctx: ExtensionContext) => {
960
+ switch (effectiveFooterStyle()) {
961
+ case "starship":
962
+ installStatusLine(ctx);
963
+ break;
964
+ case "hidden":
965
+ installHiddenStatusLine(ctx);
966
+ break;
967
+ case "native":
968
+ uninstallStatusLine(ctx);
969
+ break;
970
+ }
971
+ };
972
+
973
+ const reconcileEditor = (
974
+ ctx: ExtensionContext,
975
+ options: { allowStaleZentui?: boolean } = {},
976
+ ): EditorChangeResult | undefined => {
977
+ try {
978
+ if (effectiveEditorEnabled()) {
979
+ const currentFactory = ctx.ui.getEditorComponent();
980
+ if (
981
+ isZentuiEditorFactory(currentFactory) &&
982
+ !isOwnedEditorFactory(currentFactory) &&
983
+ !options.allowStaleZentui
984
+ ) {
985
+ clearEditorOwnership();
986
+ return;
987
+ }
988
+ const editorMissingOrReplaced = !editorInstalled || !isOwnedEditorFactory(currentFactory);
989
+ if (editorMissingOrReplaced) return installEditor(ctx);
990
+ } else {
991
+ const currentFactory = ctx.ui.getEditorComponent();
992
+ if (editorInstalled || isOwnedEditorFactory(currentFactory)) return uninstallEditor(ctx);
993
+ }
994
+ } catch {
995
+ return {
996
+ ok: false,
997
+ reason: "the editor could not be reconciled safely; reload Pi to apply this change",
998
+ };
999
+ }
1000
+ };
1001
+
1002
+ const applyConfiguredUi = (
1003
+ ctx: ExtensionContext,
1004
+ options: { allowStaleZentui?: boolean } = {},
1005
+ ): ApplyUiResult => {
1006
+ const result: ApplyUiResult = { editorBlocked: false };
1007
+ if (!isTuiContext(ctx)) return result;
1008
+ activeTheme = ctx.ui.theme;
1009
+
1010
+ const editorChange = reconcileEditor(ctx, options);
1011
+ if (editorChange && !editorChange.ok) {
1012
+ result.editorBlocked = true;
1013
+ result.editorReason = editorChange.reason;
1014
+ }
1015
+ reconcileUserMessages();
1016
+ reconcileSelectorBorders();
1017
+ reconcileFooter(ctx);
1018
+ reconcileProjectRefresh(ctx);
1019
+ reconcileSessionTimer();
1020
+ reconcileAgentTimer();
1021
+ return result;
1022
+ };
1023
+
1024
+ const installUi = (ctx: ExtensionContext) => {
1025
+ if (!isTuiContext(ctx)) return;
1026
+ activeTuiContext = ctx;
1027
+ activeTheme = ctx.ui.theme;
1028
+ const staleFooterOwner = ctxFooterOwner(ctx);
1029
+ if (typeof staleFooterOwner === "symbol") installedFooterToken = staleFooterOwner;
1030
+ ensureConfigExists();
1031
+ currentConfig = loadConfig();
1032
+ syncFooterState(ctx);
1033
+ stopProjectRefresh();
1034
+
1035
+ uninstallUserMessages();
1036
+ uninstallSelectorBorders();
1037
+ try {
1038
+ removeUserMessageStyle();
1039
+ } catch {
1040
+ // Startup alone may supersede a stale registration from an earlier reload.
1041
+ }
1042
+ try {
1043
+ removeSelectorBorderStyle();
1044
+ } catch {
1045
+ // Startup alone may supersede a stale registration from an earlier reload.
1046
+ }
1047
+ uninstallStatusLine(ctx);
1048
+ if (effectiveEditorEnabled()) clearEditorOwnership();
1049
+ else {
1050
+ try {
1051
+ uninstallEditor(ctx, { allowStaleZentui: true });
1052
+ } catch {
1053
+ // Reconciliation below retries observable stale ownership.
1054
+ }
1055
+ }
1056
+
1057
+ applyConfiguredUi(ctx, { allowStaleZentui: true });
1058
+ refresh();
1059
+ };
1060
+
1061
+ const scheduleEditorReconciliation = (ctx: ExtensionContext) => {
1062
+ sessionLifecycle.defer(() => {
1063
+ if (!isTuiContext(ctx) || !effectiveEditorEnabled()) return;
1064
+ const observed = observeEditorFactory(ctx);
1065
+ if (!observed.known) return;
1066
+ if (observed.factory === installedEditorFactory) {
1067
+ reconcileProjectRefresh(ctx);
1068
+ return;
1069
+ }
1070
+ if (!observed.factory || !isOwnedEditorFactory(observed.factory)) {
1071
+ clearEditorOwnership();
1072
+ reconcileProjectRefresh(ctx);
1073
+ refresh();
1074
+ return;
1075
+ }
1076
+ trackZentuiEditorFactory(observed.factory);
1077
+ reconcileProjectRefresh(ctx);
1078
+ refresh();
1079
+ });
1080
+ };
1081
+
1082
+ const cleanupUi = (ctx?: ExtensionContext) => {
1083
+ if (!ctx || !sessionLifecycle.isCurrent()) return;
1084
+ sessionLifecycle.shutdown();
1085
+ stopSessionTimer();
1086
+ resetAgentTimer();
1087
+ stopProjectRefresh();
1088
+
1089
+ let retainedEditorOwnership = false;
1090
+ if (isTuiContext(ctx)) {
1091
+ uninstallStatusLine(ctx, { forceLocalCleanup: true });
1092
+ try {
1093
+ const before = observeEditorFactory(ctx);
1094
+ if (before.known) {
1095
+ const currentFactory = before.factory;
1096
+ if (currentFactory && isOwnedEditorFactory(currentFactory)) {
1097
+ replaceEditor(
1098
+ ctx,
1099
+ getZentuiEditorBaseFactory(currentFactory) ??
1100
+ (editorInstallMode === "wrapper" && wrappedEditorFactory
1101
+ ? wrappedEditorFactory
1102
+ : undefined),
1103
+ );
1104
+ }
1105
+ }
1106
+ const after = observeEditorFactory(ctx);
1107
+ if (after.known && after.factory && isOwnedEditorFactory(after.factory)) {
1108
+ trackZentuiEditorFactory(after.factory);
1109
+ retainedEditorOwnership = true;
1110
+ }
1111
+ } catch {
1112
+ // Continue cleaning independent surfaces.
1113
+ }
1114
+ }
1115
+ if (!retainedEditorOwnership) clearEditorOwnership();
1116
+ accentRailLayoutPatchInstallSerial += 1;
1117
+ cleanupAccentRailLayoutPatch();
1118
+ cleanupAccentRailLayoutPatch = () => {};
1119
+ uninstallUserMessages();
1120
+ uninstallSelectorBorders();
1121
+ installedFooterKind = undefined;
1122
+ installedFooterToken = undefined;
1123
+ requestFooterRender = undefined;
1124
+ requestEditorRender = undefined;
1125
+ getActiveExtensionStatuses = () => new Map();
1126
+ activeTheme = undefined;
1127
+ activeTuiContext = undefined;
1128
+ };
1129
+
1130
+ const syncInteractiveState = (_event: unknown, ctx: ExtensionContext) => {
1131
+ refreshInteractiveState(ctx);
1132
+ };
1133
+ const syncInteractiveAndProjectState = (_event: unknown, ctx: ExtensionContext) => {
1134
+ refreshInteractiveState(ctx, true);
1135
+ };
1136
+
1137
+ pi.on("session_start", async (_event, ctx) => {
1138
+ const lifecycleGeneration = sessionLifecycle.start();
1139
+ const layoutInstallSerial = ++accentRailLayoutPatchInstallSerial;
1140
+ cleanupAccentRailLayoutPatch();
1141
+ cleanupAccentRailLayoutPatch = () => {};
1142
+ if (isTuiContext(ctx)) {
1143
+ const layoutPatchRetention = await retainAccentRailLayoutPatchInstallation(
1144
+ () => installHostAccentRailLayoutPatch(editorOwnerToken),
1145
+ () =>
1146
+ sessionLifecycle.isCurrent(lifecycleGeneration) &&
1147
+ layoutInstallSerial === accentRailLayoutPatchInstallSerial,
1148
+ (layoutPatch) => {
1149
+ cleanupAccentRailLayoutPatch = layoutPatch.cleanup;
1150
+ recordAccentRailLayoutPatchDiagnostic(layoutPatch.diagnostic, layoutPatch.version);
1151
+ },
1152
+ );
1153
+ if (layoutPatchRetention === "stale") return;
1154
+ if (layoutPatchRetention === "failed") {
1155
+ recordAccentRailLayoutPatchDiagnostic("host-module-unavailable");
1156
+ }
1157
+ }
1158
+ if (!sessionLifecycle.isCurrent(lifecycleGeneration)) return;
1159
+ liveContext.clear();
1160
+ interactionMetrics.shutdown();
1161
+ state.sessionStartEpoch = Date.now();
1162
+ invalidateUsageTotalsCache();
1163
+ resetAgentTimer();
1164
+ lastProjectCwd = undefined;
1165
+ minimalistProjectRoot = undefined;
1166
+ repositoryRoots.reset();
1167
+ installUi(ctx);
1168
+ workingLine.startSession(ctx);
1169
+ scheduleEditorReconciliation(ctx);
1170
+ });
1171
+
1172
+ registerZentuiSettingsCommand(pi, {
1173
+ sessionLifecycle,
1174
+ getConfig: getCurrentConfig,
1175
+ setEditorComponent(patch: Partial<EditorComponentConfig>, ctx: ExtensionContext) {
1176
+ currentConfig = saveEditorComponentPatch(patch);
1177
+ let result: EditorChangeResult | undefined;
1178
+ if (patch.enabled !== undefined && isTuiContext(ctx)) {
1179
+ result = reconcileEditor(ctx);
1180
+ }
1181
+ if (patch.style !== undefined && patch.style !== "minimalist") {
1182
+ setMinimalistDecorationActive(false);
1183
+ }
1184
+ if (patch.modelLabel !== undefined) syncFooterState(ctx);
1185
+ reconcileProjectRefresh(ctx);
1186
+ reconcileAgentTimer();
1187
+ refresh();
1188
+ return {
1189
+ applied: !result || result.ok,
1190
+ reason: result && !result.ok ? result.reason : undefined,
1191
+ };
1192
+ },
1193
+ setPolished(patch: Partial<PolishedEditorStyleConfig>, _ctx: ExtensionContext) {
1194
+ currentConfig = savePolishedEditorStylePatch(patch);
1195
+ refresh();
1196
+ },
1197
+ setPolishedCopyFriendly(
1198
+ patch: Partial<PolishedCopyFriendlyEditorStyleConfig>,
1199
+ _ctx: ExtensionContext,
1200
+ ) {
1201
+ currentConfig = savePolishedCopyFriendlyEditorStylePatch(patch);
1202
+ refresh();
1203
+ },
1204
+ setAccentRail(patch: Partial<AccentRailEditorStyleConfig>, _ctx: ExtensionContext) {
1205
+ currentConfig = saveAccentRailEditorStylePatch(patch);
1206
+ refresh();
1207
+ },
1208
+ setMinimalist(patch: Partial<MinimalistConfig>, ctx: ExtensionContext) {
1209
+ currentConfig = saveMinimalistEditorStylePatch(patch);
1210
+ reconcileAgentTimer();
1211
+ reconcileProjectRefresh(ctx, patch.pathDisplay !== undefined || patch.showGit !== undefined);
1212
+ refresh();
1213
+ },
1214
+ setUserMessagesComponent(patch: Partial<UserMessagesComponentConfig>, _ctx: ExtensionContext) {
1215
+ currentConfig = saveUserMessagesComponentPatch(patch);
1216
+ if (patch.enabled !== undefined || patch.style !== undefined) reconcileUserMessages();
1217
+ refresh();
1218
+ },
1219
+ setWorkingLineComponent(patch: WorkingLineComponentPatch, ctx: ExtensionContext) {
1220
+ currentConfig = saveWorkingLineComponentPatch(patch);
1221
+ return workingLine.reconcile(ctx);
1222
+ },
1223
+ setSelectorBordersComponent(
1224
+ patch: Partial<SelectorBordersComponentConfig>,
1225
+ _ctx: ExtensionContext,
1226
+ ) {
1227
+ currentConfig = saveSelectorBordersComponentPatch(patch);
1228
+ if (patch.enabled !== undefined || patch.style !== undefined) reconcileSelectorBorders();
1229
+ refresh();
1230
+ },
1231
+ setFooterComponent(patch: Partial<FooterComponentConfig>, ctx: ExtensionContext) {
1232
+ const previousStyle = effectiveFooterStyle();
1233
+ currentConfig = saveFooterComponentPatch(patch);
1234
+ const styleChanged = effectiveFooterStyle() !== previousStyle;
1235
+ if (patch.style !== undefined) reconcileFooter(ctx);
1236
+ if (patch.modelLabel !== undefined) syncFooterState(ctx);
1237
+ reconcileProjectRefresh(ctx, styleChanged);
1238
+ reconcileSessionTimer();
1239
+ refresh();
1240
+ },
1241
+ setFooterSegments(patch: Partial<FooterSegmentsConfig>, ctx: ExtensionContext) {
1242
+ applyFooterDependencyConfigChange(ctx, () =>
1243
+ saveStarshipFooterStylePatch({ segments: patch as FooterSegmentsConfig }),
1244
+ );
1245
+ },
1246
+ setFooterFormat(value: string, ctx: ExtensionContext) {
1247
+ applyFooterDependencyConfigChange(ctx, () => saveStarshipFooterStylePatch({ format: value }));
1248
+ },
1249
+ setResponsiveFooter(
1250
+ patch: Partial<Pick<PolishedTuiConfig, "responsiveFooter" | "compactFooterMaxLines">>,
1251
+ ctx: ExtensionContext,
1252
+ ) {
1253
+ applyFooterDependencyConfigChange(ctx, () =>
1254
+ saveStarshipFooterStylePatch({
1255
+ ...(patch.responsiveFooter === undefined ? {} : { responsive: patch.responsiveFooter }),
1256
+ ...(patch.compactFooterMaxLines === undefined
1257
+ ? {}
1258
+ : { compactMaxLines: patch.compactFooterMaxLines }),
1259
+ }),
1260
+ );
1261
+ },
1262
+ setIconMode(mode: IconMode) {
1263
+ currentConfig = saveIconsModePatch(mode);
1264
+ },
1265
+ setContextStyle(style: ContextStyle) {
1266
+ currentConfig = saveStarshipFooterStylePatch({ contextStyle: style });
1267
+ },
1268
+ setSeparator(separator: SeparatorStyle) {
1269
+ currentConfig = saveStarshipFooterStylePatch({ separator });
1270
+ },
1271
+ setPathDisplay(patch: Partial<PathDisplayConfig>) {
1272
+ currentConfig = saveStarshipFooterStylePatch({ pathDisplay: patch as PathDisplayConfig });
1273
+ },
1274
+ setGitBranch(patch: Partial<GitBranchConfig>) {
1275
+ currentConfig = saveStarshipFooterStylePatch({ gitBranch: patch as GitBranchConfig });
1276
+ },
1277
+ setGitCommit(
1278
+ patch: Partial<Pick<GitCommitConfig, "onlyDetached" | "showTag">>,
1279
+ ctx: ExtensionContext,
1280
+ ) {
1281
+ currentConfig = saveStarshipFooterStylePatch({ gitCommit: patch as GitCommitConfig });
1282
+ if (patch.showTag !== undefined) reconcileProjectRefresh(ctx, true);
1283
+ },
1284
+ setGitMetrics(patch: Partial<GitMetricsConfig>, ctx: ExtensionContext) {
1285
+ currentConfig = saveStarshipFooterStylePatch({ gitMetrics: patch as GitMetricsConfig });
1286
+ if (patch.ignoreSubmodules !== undefined) reconcileProjectRefresh(ctx, true);
1287
+ },
1288
+ getActiveExtensionStatuses() {
1289
+ return getActiveExtensionStatuses();
1290
+ },
1291
+ setExtensionStatusDefaultPlacement(placement: ExtensionStatusPlacement) {
1292
+ currentConfig = saveExtensionStatusDefaultPlacement(placement);
1293
+ },
1294
+ setExtensionStatusPlacement(key: string, placement: ExtensionStatusPlacement) {
1295
+ currentConfig = saveExtensionStatusPlacement(key, placement);
1296
+ },
1297
+ setExtensionStatusColorMode(key: string, colorMode: ExtensionStatusColorMode) {
1298
+ currentConfig = saveExtensionStatusColorMode(key, colorMode);
1299
+ },
1300
+ requestRender() {
1301
+ refresh();
1302
+ },
1303
+ });
1304
+
1305
+ pi.on("session_shutdown", async (_event, ctx) => {
1306
+ liveContext.clear();
1307
+ interactionMetrics.shutdown();
1308
+ workingLine.dispose(ctx);
1309
+ cleanupUi(ctx);
1310
+ });
1311
+
1312
+ const syncInteractiveAndProjectStateWithUsage = (_event: unknown, ctx: ExtensionContext) => {
1313
+ invalidateUsageTotalsCache();
1314
+ refreshInteractiveState(ctx, true);
1315
+ };
1316
+
1317
+ pi.on("agent_start", (event, ctx) => {
1318
+ liveContext.clear();
1319
+ const { interactionStarted } = interactionMetrics.agentStart();
1320
+ startAgentTurn(interactionStarted);
1321
+ workingLine.startAgent(ctx);
1322
+ syncInteractiveState(event, ctx);
1323
+ });
1324
+ pi.on("turn_start", (_event, ctx) => {
1325
+ interactionMetrics.turnStart();
1326
+ workingLine.startTurn(ctx);
1327
+ });
1328
+ pi.on("agent_end", (event, ctx) => {
1329
+ liveContext.clear();
1330
+ const displayTokens = interactionMetrics.currentDisplayTokens();
1331
+ interactionMetrics.agentEnd();
1332
+ pauseAgentRun();
1333
+ workingLine.finishAgent(ctx);
1334
+ workingLine.updateMetrics(displayTokens, interactionMetrics.currentThought(), ctx);
1335
+ // Reconcile once more after Pi has persisted the assistant message.
1336
+ syncInteractiveAndProjectStateWithUsage(event, ctx);
1337
+ });
1338
+ pi.on("model_select", (event, ctx) => {
1339
+ liveContext.clear();
1340
+ syncInteractiveState(event, ctx);
1341
+ });
1342
+ pi.on("thinking_level_select", syncInteractiveState);
1343
+ pi.on("session_info_changed", syncInteractiveState);
1344
+ pi.on("message_update", (event, ctx) => {
1345
+ liveContext.update(event.message);
1346
+ const metrics = interactionMetrics.messageUpdate(
1347
+ event.message,
1348
+ "assistantMessageEvent" in event ? event.assistantMessageEvent : undefined,
1349
+ );
1350
+ if (metrics.usageChanged || metrics.thoughtChanged) {
1351
+ workingLine.updateMetrics(metrics.displayTokens, interactionMetrics.currentThought(), ctx);
1352
+ }
1353
+ });
1354
+ pi.on("message_end", (event, ctx) => {
1355
+ const result = interactionMetrics.messageEnd(event.message);
1356
+ if (result.status === "accepted") {
1357
+ workingLine.updateMetrics(result.displayTokens, interactionMetrics.currentThought(), ctx);
1358
+ }
1359
+ // Pi notifies extensions before persisting a successful message, so retain its live
1360
+ // context until agent_end; accepted failed messages clear immediately instead of showing
1361
+ // stale usage. Rejected and duplicate finals are not authoritative.
1362
+ if (
1363
+ result.status === "accepted" &&
1364
+ event.message.role === "assistant" &&
1365
+ (event.message.stopReason === "error" || event.message.stopReason === "aborted")
1366
+ ) {
1367
+ liveContext.clear();
1368
+ }
1369
+ syncInteractiveAndProjectStateWithUsage(event, ctx);
1370
+ });
1371
+ pi.on("agent_settled", (_event, ctx) => {
1372
+ const settled = interactionMetrics.settle(ctx.isIdle());
1373
+ if (!settled) return;
1374
+ settleAgentTurn(settled.nextStartedAt);
1375
+ workingLine.settle(settled.nextTokens, settled.nextThought, ctx);
1376
+ const config = currentConfig.components.workingLine;
1377
+ if (config.enabled && config.turnSummary) {
1378
+ try {
1379
+ pi.appendEntry(TURN_SUMMARY_ENTRY_TYPE, {
1380
+ version: 3,
1381
+ ...settled.summary,
1382
+ stylePrefix: snapshotWorkingLineHighStyle(ctx.ui.theme, config, currentConfig.colors),
1383
+ });
1384
+ } catch {
1385
+ // A transcript persistence failure must not break settlement cleanup.
1386
+ }
1387
+ }
1388
+ });
1389
+ pi.on("tool_execution_start", (event, ctx) => {
1390
+ liveContext.clear();
1391
+ workingLine.startTool(event.toolCallId, event.toolName, ctx);
1392
+ syncInteractiveState(event, ctx);
1393
+ });
1394
+ pi.on("tool_execution_end", (event, ctx) => {
1395
+ workingLine.finishTool(event.toolCallId, ctx);
1396
+ syncInteractiveAndProjectState(event, ctx);
1397
+ });
1398
+ pi.on("session_compact", (event, ctx) => {
1399
+ liveContext.clear();
1400
+ syncInteractiveAndProjectStateWithUsage(event, ctx);
1401
+ });
1402
+ pi.on("session_tree", (event, ctx) => {
1403
+ liveContext.clear();
1404
+ syncInteractiveAndProjectStateWithUsage(event, ctx);
1405
+ });
1406
+ }