@selesai/code 0.9.14 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +79 -29
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +3 -4
  6. package/dist/extensions/pi-zentui/.editorconfig +12 -0
  7. package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
  8. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  9. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
  10. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
  11. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  12. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
  13. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
  14. package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
  15. package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
  16. package/dist/extensions/pi-zentui/.github/release.yml +18 -0
  17. package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
  18. package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
  19. package/dist/extensions/pi-zentui/AGENTS.md +107 -0
  20. package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
  21. package/dist/extensions/pi-zentui/LICENSE +21 -0
  22. package/dist/extensions/pi-zentui/README.md +188 -0
  23. package/dist/extensions/pi-zentui/SECURITY.md +18 -0
  24. package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
  25. package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
  26. package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
  27. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
  28. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
  29. package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
  30. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
  31. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
  32. package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
  33. package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
  34. package/dist/extensions/pi-zentui/biome.json +31 -0
  35. package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
  36. package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
  37. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
  38. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
  39. package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
  40. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
  41. package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
  42. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
  43. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
  44. package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
  45. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
  46. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
  47. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
  48. package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
  49. package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
  50. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
  51. package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
  52. package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
  53. package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
  54. package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
  55. package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
  56. package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
  57. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
  58. package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
  59. package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
  60. package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
  61. package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
  62. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
  63. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
  64. package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
  65. package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
  66. package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
  67. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
  68. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
  69. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
  70. package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
  71. package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
  72. package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
  73. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
  74. package/dist/extensions/pi-zentui/package-lock.json +5311 -0
  75. package/dist/extensions/pi-zentui/package.json +60 -0
  76. package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
  77. package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
  78. package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
  79. package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
  80. package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
  81. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
  82. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
  83. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
  84. package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
  85. package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
  86. package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
  87. package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
  88. package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
  89. package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
  90. package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
  91. package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
  92. package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
  93. package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
  94. package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
  95. package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
  96. package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
  97. package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
  98. package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
  99. package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
  100. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
  101. package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
  102. package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
  103. package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
  104. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
  105. package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
  106. package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
  107. package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
  108. package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
  109. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
  110. package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
  111. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
  112. package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
  113. package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
  114. package/dist/extensions/pi-zentui/tsconfig.json +16 -0
  115. package/dist/extensions/tps.test.ts +38 -0
  116. package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
  117. package/dist/skills/design-references/SKILL.md +31 -0
  118. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  119. package/dist/themes/adventure.json +84 -0
  120. package/dist/themes/adwaita-dark.json +84 -0
  121. package/dist/themes/arcoiris.json +84 -0
  122. package/dist/themes/arthur.json +84 -0
  123. package/dist/themes/atom.json +84 -0
  124. package/dist/themes/aura.json +84 -0
  125. package/dist/themes/black-metal-bathory.json +84 -0
  126. package/dist/themes/black-metal-burzum.json +84 -0
  127. package/dist/themes/black-metal-khold.json +84 -0
  128. package/dist/themes/box.json +84 -0
  129. package/dist/themes/brogrammer.json +84 -0
  130. package/dist/themes/carbonfox.json +84 -0
  131. package/dist/themes/catppuccin-mocha.json +84 -0
  132. package/dist/themes/citruszest.json +84 -0
  133. package/dist/themes/cursor-dark.json +84 -0
  134. package/dist/themes/cutie-pro.json +84 -0
  135. package/dist/themes/dark-modern.json +84 -0
  136. package/dist/themes/dark-pastel.json +84 -0
  137. package/dist/themes/dimmed-monokai.json +84 -0
  138. package/dist/themes/doom-peacock.json +84 -0
  139. package/dist/themes/dracula-plus.json +84 -0
  140. package/dist/themes/dracula.json +80 -0
  141. package/dist/themes/earthsong.json +84 -0
  142. package/dist/themes/everforest-dark-hard.json +84 -0
  143. package/dist/themes/everforest-dark.json +80 -0
  144. package/dist/themes/fahrenheit.json +84 -0
  145. package/dist/themes/flatland.json +84 -0
  146. package/dist/themes/flexoki-dark.json +84 -0
  147. package/dist/themes/front-end-delight.json +84 -0
  148. package/dist/themes/fun-forrest.json +84 -0
  149. package/dist/themes/galizur.json +84 -0
  150. package/dist/themes/github-dark-colorblind.json +84 -0
  151. package/dist/themes/github-dark-high-contrast.json +84 -0
  152. package/dist/themes/glacier.json +84 -0
  153. package/dist/themes/gruber-darker.json +84 -0
  154. package/dist/themes/gruvbox-dark-hard.json +84 -0
  155. package/dist/themes/gruvbox-dark.json +84 -0
  156. package/dist/themes/gruvbox-material.json +84 -0
  157. package/dist/themes/guezwhoz.json +84 -0
  158. package/dist/themes/hacktober.json +84 -0
  159. package/dist/themes/hardcore.json +84 -0
  160. package/dist/themes/havn-skumring.json +84 -0
  161. package/dist/themes/ic-orange-ppl.json +84 -0
  162. package/dist/themes/iterm2-smoooooth.json +84 -0
  163. package/dist/themes/iterm2-tango-dark.json +84 -0
  164. package/dist/themes/japanesque.json +84 -0
  165. package/dist/themes/jellybeans.json +84 -0
  166. package/dist/themes/kanagawa-wave.json +84 -0
  167. package/dist/themes/kurokula.json +84 -0
  168. package/dist/themes/later-this-evening.json +84 -0
  169. package/dist/themes/lovelace.json +84 -0
  170. package/dist/themes/material-darker.json +84 -0
  171. package/dist/themes/matte-black.json +84 -0
  172. package/dist/themes/mellow.json +84 -0
  173. package/dist/themes/miasma.json +84 -0
  174. package/dist/themes/nord.json +80 -0
  175. package/dist/themes/nvim-dark.json +84 -0
  176. package/dist/themes/one-dark.json +80 -0
  177. package/dist/themes/popping-and-locking.json +84 -0
  178. package/dist/themes/rose-pine.json +80 -0
  179. package/dist/themes/sea-shells.json +84 -0
  180. package/dist/themes/sleepy-hollow.json +84 -0
  181. package/dist/themes/smyck.json +84 -0
  182. package/dist/themes/solarized-dark.json +80 -0
  183. package/dist/themes/tokyo-night.json +80 -0
  184. package/dist/themes/tomorrow-night-bright.json +84 -0
  185. package/dist/themes/tomorrow-night-burns.json +84 -0
  186. package/dist/themes/tomorrow-night.json +84 -0
  187. package/dist/themes/twilight.json +84 -0
  188. package/dist/themes/vague.json +84 -0
  189. package/dist/themes/vesper.json +84 -0
  190. package/dist/themes/xcode-dark-hc.json +84 -0
  191. package/docs/custom-provider.md +0 -1
  192. package/docs/plans/auto-model-routing-extension.md +490 -0
  193. package/package.json +3 -3
  194. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  195. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  196. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  197. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  198. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  199. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  200. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  201. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  202. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  203. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  204. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  205. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  206. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  207. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  208. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  209. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  210. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  211. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  212. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  213. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  214. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  215. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  216. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  217. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  218. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  219. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  220. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  221. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  222. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  223. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  224. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  225. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  226. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  227. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  228. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  229. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  230. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  231. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  232. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  233. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  234. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  235. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  236. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  237. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  238. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  239. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  240. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  241. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  242. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  243. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  244. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  245. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  246. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  247. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  248. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  249. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  250. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  251. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  252. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  253. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  254. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  255. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  256. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  257. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  258. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  259. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  260. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  261. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  262. package/dist/extensions/preview-tools-disabled.ts +0 -11
  263. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,378 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ vi.mock("../extensions/zentui/config", async (importOriginal) => {
5
+ const actual = await importOriginal<typeof import("../extensions/zentui/config")>();
6
+ return {
7
+ ...actual,
8
+ ensureConfigExists: () => {},
9
+ loadConfig: () => ({
10
+ ...actual.defaultConfig,
11
+ projectRefreshIntervalMs: 0,
12
+ components: {
13
+ ...actual.defaultConfig.components,
14
+ editor: {
15
+ ...actual.defaultConfig.components.editor,
16
+ enabled: true,
17
+ styles: {
18
+ ...actual.defaultConfig.components.editor.styles,
19
+ opencode: {
20
+ ...actual.defaultConfig.components.editor.styles.opencode,
21
+ metadataFormat: "$context $tokens $cache_hit",
22
+ },
23
+ },
24
+ },
25
+ footer: {
26
+ ...actual.defaultConfig.components.footer,
27
+ style: "starship",
28
+ },
29
+ },
30
+ features: { ...actual.defaultConfig.features, editor: false, statusLine: true },
31
+ }),
32
+ };
33
+ });
34
+
35
+ vi.mock("../extensions/zentui/git", async (importOriginal) => {
36
+ const actual = await importOriginal<typeof import("../extensions/zentui/git")>();
37
+ return {
38
+ ...actual,
39
+ readGitStatus: async () => ({ kind: "ok" as const, status: actual.emptyGitStatus() }),
40
+ };
41
+ });
42
+
43
+ vi.mock("../extensions/zentui/runtime", async (importOriginal) => {
44
+ const actual = await importOriginal<typeof import("../extensions/zentui/runtime")>();
45
+ return { ...actual, readRuntimeInfo: async () => ({ kind: "ok" as const, runtime: undefined }) };
46
+ });
47
+
48
+ vi.mock("../extensions/zentui/package-version", async (importOriginal) => {
49
+ const actual = await importOriginal<typeof import("../extensions/zentui/package-version")>();
50
+ return {
51
+ ...actual,
52
+ readPackageVersionResult: async () => ({ kind: "ok" as const, result: null }),
53
+ };
54
+ });
55
+
56
+ import zentui from "../extensions/zentui/index";
57
+
58
+ type Handler = (event: unknown, ctx: unknown) => unknown | Promise<unknown>;
59
+ type Footer = { render(width: number): string[]; dispose?: () => void };
60
+ type FooterFactory = (...args: unknown[]) => Footer;
61
+ type Editor = { render(width: number): string[]; setText(text: string): void };
62
+ type EditorFactory = (...args: unknown[]) => Editor;
63
+
64
+ function makeTheme(): Theme {
65
+ return {
66
+ fg(_color: string, text: string) {
67
+ return text;
68
+ },
69
+ bold(text: string) {
70
+ return text;
71
+ },
72
+ italic(text: string) {
73
+ return text;
74
+ },
75
+ underline(text: string) {
76
+ return text;
77
+ },
78
+ strikethrough(text: string) {
79
+ return text;
80
+ },
81
+ getThinkingBorderColor() {
82
+ return (text: string) => text;
83
+ },
84
+ } as unknown as Theme;
85
+ }
86
+
87
+ function assistant(totalTokens: number, stopReason = "stop") {
88
+ return {
89
+ role: "assistant",
90
+ content: [],
91
+ api: "anthropic-messages",
92
+ provider: "anthropic",
93
+ model: "test",
94
+ usage: {
95
+ input: totalTokens === 0 ? 0 : 101,
96
+ output: totalTokens === 0 ? 0 : 202,
97
+ cacheRead: totalTokens === 0 ? 0 : 303,
98
+ cacheWrite: totalTokens === 0 ? 0 : 404,
99
+ totalTokens,
100
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
101
+ },
102
+ stopReason,
103
+ timestamp: 1,
104
+ };
105
+ }
106
+
107
+ function persistedEntry(
108
+ id: string,
109
+ input: number,
110
+ output: number,
111
+ cost: number,
112
+ cacheRead = 0,
113
+ cacheWrite = 0,
114
+ ) {
115
+ return {
116
+ type: "message",
117
+ id,
118
+ message: {
119
+ role: "assistant",
120
+ usage: {
121
+ input,
122
+ output,
123
+ cacheRead,
124
+ cacheWrite,
125
+ totalTokens: input + output,
126
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: cost },
127
+ },
128
+ },
129
+ };
130
+ }
131
+
132
+ function loadExtension() {
133
+ const handlers = new Map<string, Handler[]>();
134
+ zentui({
135
+ on(name: string, handler: Handler) {
136
+ handlers.set(name, [...(handlers.get(name) ?? []), handler]);
137
+ },
138
+ registerCommand() {},
139
+ getThinkingLevel() {
140
+ return "off";
141
+ },
142
+ } as never);
143
+ return handlers;
144
+ }
145
+
146
+ async function emit(
147
+ handlers: Map<string, Handler[]>,
148
+ name: string,
149
+ ctx: unknown,
150
+ event: unknown = {},
151
+ ) {
152
+ for (const handler of handlers.get(name) ?? []) await handler(event, ctx);
153
+ }
154
+
155
+ function createHarness(
156
+ options: {
157
+ model?: { id: string; provider: string; contextWindow: number };
158
+ contextUsage?: { tokens: number; contextWindow: number; percent: number } | null;
159
+ } = {},
160
+ ) {
161
+ let footerFactory: FooterFactory | undefined;
162
+ let editorFactory: EditorFactory | undefined;
163
+ let editorText = "";
164
+ const requestRender = vi.fn();
165
+ const editorRequestRender = vi.fn();
166
+ const entries = [persistedEntry("old", 5, 6, 0.123)];
167
+ const state = {
168
+ model:
169
+ "model" in options
170
+ ? options.model
171
+ : { id: "test", provider: "anthropic", contextWindow: 10_000 },
172
+ contextUsage:
173
+ "contextUsage" in options
174
+ ? options.contextUsage
175
+ : { tokens: 1_000, contextWindow: 10_000, percent: 10 },
176
+ };
177
+ const theme = makeTheme();
178
+ const ctx = {
179
+ hasUI: true,
180
+ mode: "tui",
181
+ cwd: process.cwd(),
182
+ get model() {
183
+ return state.model;
184
+ },
185
+ sessionManager: {
186
+ getBranch: () => entries,
187
+ getEntries: () => entries,
188
+ getSessionName: () => undefined,
189
+ },
190
+ getContextUsage: () => state.contextUsage,
191
+ ui: {
192
+ theme,
193
+ getEditorText() {
194
+ return editorText;
195
+ },
196
+ setEditorText(text: string) {
197
+ editorText = text;
198
+ },
199
+ setFooter(factory: FooterFactory | undefined) {
200
+ footerFactory = factory;
201
+ },
202
+ setEditorComponent(factory: EditorFactory | undefined) {
203
+ editorFactory = factory;
204
+ },
205
+ getEditorComponent() {
206
+ return editorFactory;
207
+ },
208
+ },
209
+ };
210
+ return {
211
+ ctx,
212
+ entries,
213
+ state,
214
+ requestRender,
215
+ editorRequestRender,
216
+ createFooter() {
217
+ if (!footerFactory) throw new Error("footer was not installed");
218
+ return footerFactory({ requestRender }, theme, {
219
+ onBranchChange: () => () => {},
220
+ getExtensionStatuses: () => new Map<string, string>(),
221
+ });
222
+ },
223
+ createEditor() {
224
+ if (!editorFactory) throw new Error("editor was not installed");
225
+ const editor = editorFactory(
226
+ { requestRender: editorRequestRender, terminal: { rows: 24, cols: 160 } },
227
+ { borderColor: (text: string) => text, selectList: {} },
228
+ {},
229
+ );
230
+ editor.setText("typed text");
231
+ return editor;
232
+ },
233
+ };
234
+ }
235
+
236
+ function rendered(footer: Footer): string {
237
+ return footer.render(160).join("\n");
238
+ }
239
+
240
+ function renderedEditor(editor: Editor): string {
241
+ return editor.render(160).join("\n");
242
+ }
243
+
244
+ async function settleProjectRefresh(): Promise<void> {
245
+ await Promise.resolve();
246
+ await Promise.resolve();
247
+ await Promise.resolve();
248
+ }
249
+
250
+ afterEach(() => {
251
+ vi.useRealTimers();
252
+ vi.restoreAllMocks();
253
+ });
254
+
255
+ describe("live streaming context event integration", () => {
256
+ it("coalesces registered updates, uses the newest cumulative total, and finalizes canonically", async () => {
257
+ vi.useFakeTimers();
258
+ const handlers = loadExtension();
259
+ const harness = createHarness();
260
+ await emit(handlers, "session_start", harness.ctx);
261
+ await settleProjectRefresh();
262
+ const footer = harness.createFooter();
263
+ const editor = harness.createEditor();
264
+ expect(renderedEditor(editor)).toContain("10.0%/10k ↑5 ↓6 0.0%");
265
+ harness.requestRender.mockClear();
266
+ harness.editorRequestRender.mockClear();
267
+
268
+ await emit(handlers, "message_update", harness.ctx, { message: assistant(1_000) });
269
+ await emit(handlers, "message_update", harness.ctx, { message: assistant(1_100) });
270
+ vi.advanceTimersByTime(249);
271
+ expect(harness.requestRender).not.toHaveBeenCalled();
272
+ vi.advanceTimersByTime(1);
273
+ expect(harness.requestRender).toHaveBeenCalledTimes(1);
274
+ expect(harness.editorRequestRender).toHaveBeenCalledTimes(1);
275
+ expect(rendered(footer)).toContain("11.0%/10k");
276
+ expect(rendered(footer)).toContain("↑5 ↓6");
277
+ expect(rendered(footer)).toContain("$0.123");
278
+ expect(renderedEditor(editor)).toContain("11.0%/10k ↑5 ↓6 0.0%");
279
+
280
+ await emit(handlers, "message_end", harness.ctx, { message: assistant(1_100) });
281
+ expect(rendered(footer)).toContain("11.0%/10k");
282
+ expect(rendered(footer)).toContain("↑5 ↓6");
283
+ expect(rendered(footer)).toContain("$0.123");
284
+ expect(renderedEditor(editor)).toContain("11.0%/10k ↑5 ↓6 0.0%");
285
+
286
+ harness.state.contextUsage = { tokens: 1_200, contextWindow: 10_000, percent: 12 };
287
+ harness.entries.push(persistedEntry("new", 7, 8, 0.2, 21, 2));
288
+ await emit(handlers, "agent_end", harness.ctx);
289
+ const finalized = rendered(footer);
290
+ expect(finalized).toContain("12.0%/10k");
291
+ expect(finalized).toContain("↑12 ↓14");
292
+ expect(finalized).toContain("$0.323");
293
+ const finalizedEditor = renderedEditor(editor);
294
+ expect(finalizedEditor).toContain("12.0%/10k ↑12 ↓14 70.0%");
295
+ expect(finalizedEditor.match(/70\.0%/g)).toHaveLength(1);
296
+
297
+ footer.dispose?.();
298
+ await emit(handlers, "session_shutdown", harness.ctx);
299
+ });
300
+
301
+ it("uses the official context window when the current model is absent and ignores zero usage", async () => {
302
+ vi.useFakeTimers();
303
+ const handlers = loadExtension();
304
+ const harness = createHarness({ model: undefined });
305
+ await emit(handlers, "session_start", harness.ctx);
306
+ await settleProjectRefresh();
307
+ const footer = harness.createFooter();
308
+
309
+ await emit(handlers, "message_update", harness.ctx, { message: assistant(1_100) });
310
+ vi.advanceTimersByTime(250);
311
+ expect(rendered(footer)).toContain("11.0%/10k");
312
+
313
+ await emit(handlers, "agent_start", harness.ctx);
314
+ harness.requestRender.mockClear();
315
+ await emit(handlers, "message_update", harness.ctx, { message: assistant(0) });
316
+ vi.advanceTimersByTime(250);
317
+ expect(harness.requestRender).not.toHaveBeenCalled();
318
+ expect(rendered(footer)).toContain("10.0%/10k");
319
+
320
+ footer.dispose?.();
321
+ await emit(handlers, "session_shutdown", harness.ctx);
322
+ });
323
+
324
+ it("clears live usage at wired boundaries and recovers after null compaction", async () => {
325
+ vi.useFakeTimers();
326
+ const handlers = loadExtension();
327
+ const harness = createHarness();
328
+ await emit(handlers, "session_start", harness.ctx);
329
+ await settleProjectRefresh();
330
+ const footer = harness.createFooter();
331
+ const seedLive = async (tokens = 1_100) => {
332
+ await emit(handlers, "message_update", harness.ctx, { message: assistant(tokens) });
333
+ vi.advanceTimersByTime(250);
334
+ expect(rendered(footer)).toContain(`${(tokens / 100).toFixed(1)}%/10k`);
335
+ };
336
+
337
+ await seedLive();
338
+ await emit(handlers, "agent_start", harness.ctx);
339
+ expect(rendered(footer)).toContain("10.0%/10k");
340
+
341
+ await seedLive();
342
+ await emit(handlers, "model_select", harness.ctx);
343
+ expect(rendered(footer)).toContain("10.0%/10k");
344
+
345
+ await seedLive();
346
+ await emit(handlers, "tool_execution_start", harness.ctx);
347
+ expect(rendered(footer)).toContain("10.0%/10k");
348
+
349
+ await seedLive();
350
+ await emit(handlers, "session_tree", harness.ctx);
351
+ expect(rendered(footer)).toContain("10.0%/10k");
352
+
353
+ await emit(handlers, "turn_start", harness.ctx);
354
+ await seedLive();
355
+ await emit(handlers, "message_end", harness.ctx, { message: assistant(1_100, "error") });
356
+ expect(rendered(footer)).toContain("10.0%/10k");
357
+
358
+ await emit(handlers, "turn_start", harness.ctx);
359
+ await seedLive();
360
+ await emit(handlers, "message_end", harness.ctx, { message: assistant(1_100, "aborted") });
361
+ expect(rendered(footer)).toContain("10.0%/10k");
362
+
363
+ await seedLive();
364
+ harness.state.contextUsage = null;
365
+ await emit(handlers, "session_compact", harness.ctx);
366
+ expect(rendered(footer)).toContain("?/10k");
367
+ await seedLive(1_500);
368
+
369
+ harness.state.contextUsage = { tokens: 1_000, contextWindow: 10_000, percent: 10 };
370
+ await emit(handlers, "agent_start", harness.ctx);
371
+ await emit(handlers, "message_update", harness.ctx, { message: assistant(1_200) });
372
+ harness.requestRender.mockClear();
373
+ await emit(handlers, "session_shutdown", harness.ctx);
374
+ vi.advanceTimersByTime(250);
375
+ expect(harness.requestRender).not.toHaveBeenCalled();
376
+ expect(rendered(footer)).toContain("10.0%/10k");
377
+ });
378
+ });
@@ -0,0 +1,139 @@
1
+ import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import {
4
+ calculateLiveContextTokens,
5
+ LiveContextController,
6
+ liveContextFromMessage,
7
+ } from "../extensions/zentui/live-context";
8
+ import { SessionLifecycle } from "../extensions/zentui/session-lifecycle";
9
+
10
+ function usage(patch: Partial<Usage> = {}): Usage {
11
+ return {
12
+ input: 0,
13
+ output: 0,
14
+ cacheRead: 0,
15
+ cacheWrite: 0,
16
+ totalTokens: 0,
17
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
18
+ ...patch,
19
+ };
20
+ }
21
+
22
+ function assistant(messageUsage: Usage, stopReason: AssistantMessage["stopReason"] = "stop") {
23
+ return {
24
+ role: "assistant" as const,
25
+ content: [],
26
+ api: "anthropic-messages" as const,
27
+ provider: "anthropic",
28
+ model: "test",
29
+ usage: messageUsage,
30
+ stopReason,
31
+ timestamp: 1,
32
+ };
33
+ }
34
+
35
+ afterEach(() => {
36
+ vi.useRealTimers();
37
+ });
38
+
39
+ describe("live context usage", () => {
40
+ it("prefers totalTokens and otherwise includes cache components", () => {
41
+ expect(
42
+ calculateLiveContextTokens(
43
+ usage({ input: 400, output: 300, cacheRead: 200, cacheWrite: 100, totalTokens: 777 }),
44
+ ),
45
+ ).toBe(777);
46
+ expect(
47
+ calculateLiveContextTokens(
48
+ usage({ input: 400, output: 300, cacheRead: 200, cacheWrite: 100 }),
49
+ ),
50
+ ).toBe(1_000);
51
+ });
52
+
53
+ it("ignores zero usage, non-assistant messages, and failed assistant snapshots", () => {
54
+ expect(calculateLiveContextTokens(usage())).toBeUndefined();
55
+ expect(liveContextFromMessage({ role: "user" })).toBeUndefined();
56
+ expect(liveContextFromMessage(assistant(usage({ totalTokens: 100 }), "error"))).toBeUndefined();
57
+ expect(
58
+ liveContextFromMessage(assistant(usage({ totalTokens: 100 }), "aborted")),
59
+ ).toBeUndefined();
60
+ });
61
+
62
+ it("keeps the newest cumulative snapshot instead of summing deltas", () => {
63
+ vi.useFakeTimers();
64
+ const lifecycle = new SessionLifecycle();
65
+ lifecycle.start();
66
+ const render = vi.fn();
67
+ const controller = new LiveContextController(lifecycle, render);
68
+
69
+ controller.update(assistant(usage({ totalTokens: 1_000 })));
70
+ controller.update(assistant(usage({ totalTokens: 1_100 })));
71
+ expect(controller.get()).toEqual({ tokens: 1_100 });
72
+ vi.advanceTimersByTime(250);
73
+
74
+ expect(render).toHaveBeenCalledTimes(1);
75
+ expect(controller.get()).toEqual({ tokens: 1_100 });
76
+ });
77
+
78
+ it("coalesces bursts to at most one render per 250ms with the newest usage", () => {
79
+ vi.useFakeTimers();
80
+ const lifecycle = new SessionLifecycle();
81
+ lifecycle.start();
82
+ const renderedTokens: number[] = [];
83
+ let controller: LiveContextController;
84
+ controller = new LiveContextController(lifecycle, () => {
85
+ renderedTokens.push(controller.get()?.tokens ?? 0);
86
+ });
87
+
88
+ for (const totalTokens of [100, 200, 300, 400]) {
89
+ controller.update(assistant(usage({ totalTokens })));
90
+ }
91
+ expect(vi.getTimerCount()).toBe(1);
92
+ vi.advanceTimersByTime(249);
93
+ expect(renderedTokens).toEqual([]);
94
+ vi.advanceTimersByTime(1);
95
+ expect(renderedTokens).toEqual([400]);
96
+
97
+ controller.update(assistant(usage({ totalTokens: 500 })));
98
+ vi.advanceTimersByTime(250);
99
+ expect(renderedTokens).toEqual([400, 500]);
100
+ });
101
+
102
+ it("cancels old generations without suppressing scheduling after direct lifecycle restarts", () => {
103
+ vi.useFakeTimers();
104
+ const lifecycle = new SessionLifecycle();
105
+ lifecycle.start();
106
+ const renderedTokens: number[] = [];
107
+ let controller: LiveContextController;
108
+ controller = new LiveContextController(lifecycle, () => {
109
+ renderedTokens.push(controller.get()?.tokens ?? 0);
110
+ });
111
+
112
+ controller.update(assistant(usage({ totalTokens: 100 })));
113
+ lifecycle.shutdown();
114
+ vi.advanceTimersByTime(250);
115
+ expect(renderedTokens).toEqual([]);
116
+
117
+ lifecycle.start();
118
+ controller.update(assistant(usage({ totalTokens: 200 })));
119
+ vi.advanceTimersByTime(250);
120
+ expect(renderedTokens).toEqual([200]);
121
+
122
+ controller.update(assistant(usage({ totalTokens: 300 })));
123
+ lifecycle.start();
124
+ controller.update(assistant(usage({ totalTokens: 400 })));
125
+ vi.advanceTimersByTime(250);
126
+ expect(renderedTokens).toEqual([200, 400]);
127
+ });
128
+
129
+ it("recovers from a cleared compaction boundary when live usage resumes", () => {
130
+ const lifecycle = new SessionLifecycle();
131
+ lifecycle.start();
132
+ const controller = new LiveContextController(lifecycle, () => {});
133
+ controller.update(assistant(usage({ totalTokens: 100 })));
134
+ controller.clear();
135
+ expect(controller.get()).toBeUndefined();
136
+ controller.update(assistant(usage({ totalTokens: 150 })));
137
+ expect(controller.get()).toEqual({ tokens: 150 });
138
+ });
139
+ });