@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,276 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ const capabilities = vi.hoisted(() => ({
5
+ subscription: undefined as boolean | undefined,
6
+ autoCompaction: undefined as boolean | undefined,
7
+ throwSubscription: false,
8
+ throwAutoCompaction: false,
9
+ settingsCreate: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
13
+ const actual = await importOriginal<typeof import("@earendil-works/pi-coding-agent")>();
14
+ return {
15
+ ...actual,
16
+ SettingsManager: {
17
+ create(...args: unknown[]) {
18
+ capabilities.settingsCreate(...args);
19
+ if (capabilities.throwAutoCompaction) throw new Error("unsupported settings");
20
+ return {
21
+ drainErrors: () => [],
22
+ getCompactionEnabled: () => capabilities.autoCompaction,
23
+ };
24
+ },
25
+ },
26
+ };
27
+ });
28
+
29
+ vi.mock("../extensions/zentui/config", async (importOriginal) => {
30
+ const actual = await importOriginal<typeof import("../extensions/zentui/config")>();
31
+ return {
32
+ ...actual,
33
+ ensureConfigExists: () => {},
34
+ loadConfig: () => {
35
+ const footer = actual.defaultConfig.components.footer;
36
+ return {
37
+ ...actual.defaultConfig,
38
+ projectRefreshIntervalMs: 0,
39
+ components: {
40
+ ...actual.defaultConfig.components,
41
+ editor: { ...actual.defaultConfig.components.editor, enabled: false },
42
+ footer: {
43
+ ...footer,
44
+ enabled: true,
45
+ style: "starship",
46
+ styles: {
47
+ starship: {
48
+ ...footer.styles.starship,
49
+ segments: { ...footer.styles.starship.segments, modelInfo: true },
50
+ },
51
+ },
52
+ },
53
+ },
54
+ };
55
+ },
56
+ };
57
+ });
58
+
59
+ vi.mock("../extensions/zentui/git", async (importOriginal) => {
60
+ const actual = await importOriginal<typeof import("../extensions/zentui/git")>();
61
+ return {
62
+ ...actual,
63
+ readGitStatus: async () => ({ kind: "ok" as const, status: actual.emptyGitStatus() }),
64
+ };
65
+ });
66
+ vi.mock("../extensions/zentui/runtime", () => ({
67
+ readRuntimeInfo: async () => ({ kind: "ok" as const, runtime: undefined }),
68
+ }));
69
+ vi.mock("../extensions/zentui/package-version", () => ({
70
+ readPackageVersionResult: async () => ({ kind: "ok" as const, result: null }),
71
+ }));
72
+
73
+ import zentui from "../extensions/zentui/index";
74
+
75
+ type Handler = (event: unknown, ctx: unknown) => unknown | Promise<unknown>;
76
+ type Footer = { render(width: number): string[]; dispose?: () => void };
77
+ type FooterFactory = (...args: unknown[]) => Footer;
78
+
79
+ function makeTheme(): Theme {
80
+ return {
81
+ fg: (_color: string, text: string) => text,
82
+ bold: (text: string) => text,
83
+ italic: (text: string) => text,
84
+ underline: (text: string) => text,
85
+ strikethrough: (text: string) => text,
86
+ getThinkingBorderColor: () => (text: string) => text,
87
+ } as unknown as Theme;
88
+ }
89
+
90
+ function usageEntry(
91
+ id: string,
92
+ options: { input: number; output: number; read?: number; write?: number },
93
+ ) {
94
+ return {
95
+ type: "message",
96
+ id,
97
+ message: {
98
+ role: "assistant",
99
+ usage: {
100
+ input: options.input,
101
+ output: options.output,
102
+ cacheRead: options.read ?? 0,
103
+ cacheWrite: options.write ?? 0,
104
+ cost: { total: 0 },
105
+ },
106
+ },
107
+ };
108
+ }
109
+
110
+ function createHarness(name: string) {
111
+ let footerFactory: FooterFactory | undefined;
112
+ const entries = [usageEntry(`${name}-initial`, { input: 10, output: 2 })];
113
+ const state = { model: { id: `${name}-model`, provider: "test", contextWindow: 10_000 } };
114
+ const theme = makeTheme();
115
+ const ctx = {
116
+ hasUI: true,
117
+ mode: "tui",
118
+ cwd: `/tmp/${name}`,
119
+ get model() {
120
+ return state.model;
121
+ },
122
+ modelRegistry: {
123
+ isUsingOAuth() {
124
+ if (capabilities.throwSubscription) throw new Error("unsupported OAuth lookup");
125
+ return capabilities.subscription;
126
+ },
127
+ },
128
+ isProjectTrusted: () => true,
129
+ getContextUsage: () => ({ tokens: 1_000, contextWindow: 10_000, percent: 10 }),
130
+ sessionManager: {
131
+ getBranch: () => entries,
132
+ getEntries: () => entries,
133
+ getSessionName: () => undefined,
134
+ },
135
+ ui: {
136
+ theme,
137
+ setFooter(factory: FooterFactory | undefined) {
138
+ footerFactory = factory;
139
+ },
140
+ setEditorComponent() {},
141
+ getEditorComponent: () => undefined,
142
+ },
143
+ };
144
+ return {
145
+ ctx,
146
+ entries,
147
+ state,
148
+ createFooter() {
149
+ if (!footerFactory) throw new Error("footer was not installed");
150
+ return footerFactory({ requestRender() {} }, theme, {
151
+ onBranchChange: () => () => {},
152
+ getExtensionStatuses: () => new Map<string, string>(),
153
+ });
154
+ },
155
+ };
156
+ }
157
+
158
+ function loadExtension() {
159
+ const handlers = new Map<string, Handler[]>();
160
+ zentui({
161
+ on(name: string, handler: Handler) {
162
+ handlers.set(name, [...(handlers.get(name) ?? []), handler]);
163
+ },
164
+ registerCommand() {},
165
+ getThinkingLevel: () => "off",
166
+ } as never);
167
+ return handlers;
168
+ }
169
+
170
+ async function emit(handlers: Map<string, Handler[]>, name: string, ctx: unknown, event = {}) {
171
+ for (const handler of handlers.get(name) ?? []) await handler(event, ctx);
172
+ }
173
+
174
+ function rendered(footer: Footer): string {
175
+ return footer.render(160).join("\n");
176
+ }
177
+
178
+ beforeEach(() => {
179
+ capabilities.subscription = undefined;
180
+ capabilities.autoCompaction = undefined;
181
+ capabilities.throwSubscription = false;
182
+ capabilities.throwAutoCompaction = false;
183
+ capabilities.settingsCreate.mockClear();
184
+ });
185
+
186
+ afterEach(() => vi.restoreAllMocks());
187
+
188
+ describe("telemetry lifecycle integration", () => {
189
+ it("initializes, refreshes, clears unsupported values, and reconciles at agent_end", async () => {
190
+ const handlers = loadExtension();
191
+ const harness = createHarness("telemetry-events");
192
+ capabilities.subscription = true;
193
+ capabilities.autoCompaction = false;
194
+
195
+ await emit(handlers, "session_start", harness.ctx);
196
+ const footer = harness.createFooter();
197
+ expect(rendered(footer)).toContain("telemetry-events-model Test");
198
+ expect(rendered(footer)).toContain("$0.000 (sub)");
199
+ expect(rendered(footer)).not.toContain("(auto)");
200
+
201
+ harness.state.model = {
202
+ ...harness.state.model,
203
+ id: "selected-model",
204
+ provider: "changed-provider",
205
+ };
206
+ capabilities.subscription = false;
207
+ capabilities.autoCompaction = false;
208
+ await emit(handlers, "model_select", harness.ctx);
209
+ expect(rendered(footer)).toContain("selected-model Changed Provider");
210
+ expect(rendered(footer)).not.toContain("telemetry-events-model Test");
211
+ expect(rendered(footer)).not.toContain("(sub)");
212
+
213
+ capabilities.subscription = false;
214
+ capabilities.autoCompaction = true;
215
+ await emit(handlers, "tool_execution_end", harness.ctx);
216
+ expect(rendered(footer)).toContain("10.0%/10k (auto)");
217
+
218
+ capabilities.subscription = true;
219
+ capabilities.autoCompaction = true;
220
+ await emit(handlers, "session_info_changed", harness.ctx);
221
+ expect(rendered(footer)).toContain("$0.000 (sub)");
222
+ expect(rendered(footer)).toContain("10.0%/10k (auto)");
223
+
224
+ capabilities.throwSubscription = true;
225
+ capabilities.throwAutoCompaction = true;
226
+ await expect(emit(handlers, "session_info_changed", harness.ctx)).resolves.toBeUndefined();
227
+ expect(rendered(footer)).not.toMatch(/\(sub\)|\(auto\)/);
228
+
229
+ capabilities.throwSubscription = false;
230
+ capabilities.throwAutoCompaction = false;
231
+ capabilities.subscription = true;
232
+ capabilities.autoCompaction = true;
233
+ await emit(handlers, "message_end", harness.ctx, {
234
+ message: { role: "assistant", usage: { input: 5, output: 3 } },
235
+ });
236
+ harness.entries.push(
237
+ usageEntry("persisted-final", { input: 5, output: 3, read: 1_200, write: 300 }),
238
+ );
239
+ await emit(handlers, "agent_end", harness.ctx);
240
+ const final = rendered(footer);
241
+ expect(final).toContain("↑15 ↓5");
242
+ expect(final).toContain("R1.2k W300");
243
+ expect(final).toContain("$0.000 (sub)");
244
+ expect(final).toContain("10.0%/10k (auto)");
245
+
246
+ footer.dispose?.();
247
+ await emit(handlers, "session_shutdown", harness.ctx);
248
+ });
249
+
250
+ it("does not leak telemetry across shutdown and replacement session start", async () => {
251
+ const handlers = loadExtension();
252
+ const first = createHarness("telemetry-first");
253
+ capabilities.subscription = true;
254
+ capabilities.autoCompaction = true;
255
+ await emit(handlers, "session_start", first.ctx);
256
+ const firstFooter = first.createFooter();
257
+ expect(rendered(firstFooter)).toMatch(/\(sub\).*\(auto\)|\(auto\).*\(sub\)/);
258
+ await emit(handlers, "session_shutdown", first.ctx);
259
+ expect(() => first.createFooter()).toThrow("footer was not installed");
260
+
261
+ const replacement = createHarness("telemetry-replacement");
262
+ capabilities.subscription = undefined;
263
+ capabilities.autoCompaction = undefined;
264
+ await emit(handlers, "session_start", replacement.ctx);
265
+ const replacementFooter = replacement.createFooter();
266
+ expect(rendered(replacementFooter)).not.toMatch(/\(sub\)|\(auto\)|R1\.2k|W300/);
267
+ expect(capabilities.settingsCreate).toHaveBeenLastCalledWith(
268
+ "/tmp/telemetry-replacement",
269
+ undefined,
270
+ { projectTrusted: true },
271
+ );
272
+
273
+ replacementFooter.dispose?.();
274
+ await emit(handlers, "session_shutdown", replacement.ctx);
275
+ });
276
+ });
@@ -0,0 +1,105 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { resolveFooterTelemetry } from "../extensions/zentui/telemetry";
4
+
5
+ function makeContext(overrides: Record<string, unknown> = {}) {
6
+ return {
7
+ cwd: "/tmp/project",
8
+ model: { id: "model", provider: "anthropic" },
9
+ modelRegistry: { isUsingOAuth: () => false },
10
+ isProjectTrusted: () => true,
11
+ ...overrides,
12
+ } as unknown as ExtensionContext;
13
+ }
14
+
15
+ function settingsCapability(options: {
16
+ enabled?: unknown;
17
+ errors?: unknown[];
18
+ throwCreate?: boolean;
19
+ }) {
20
+ const create = vi.fn(() => {
21
+ if (options.throwCreate) throw new Error("unsupported");
22
+ return {
23
+ drainErrors: () => options.errors ?? [],
24
+ getCompactionEnabled: () => options.enabled,
25
+ };
26
+ });
27
+ return { create };
28
+ }
29
+
30
+ afterEach(() => vi.restoreAllMocks());
31
+
32
+ describe("resolveFooterTelemetry", () => {
33
+ it("uses the public OAuth and settings capabilities", () => {
34
+ const settingsManager = settingsCapability({ enabled: true });
35
+ const modelRegistry = { isUsingOAuth: vi.fn(() => true) };
36
+ const ctx = makeContext({ modelRegistry, isProjectTrusted: () => false });
37
+
38
+ expect(resolveFooterTelemetry(ctx, { settingsManager })).toEqual({
39
+ subscription: true,
40
+ autoCompaction: true,
41
+ });
42
+ expect(modelRegistry.isUsingOAuth).toHaveBeenCalledWith(ctx.model);
43
+ expect(settingsManager.create).toHaveBeenCalledWith("/tmp/project", undefined, {
44
+ projectTrusted: false,
45
+ });
46
+ });
47
+
48
+ it("recognizes Kimi subscription mode without an OAuth lookup", () => {
49
+ const isUsingOAuth = vi.fn(() => false);
50
+ const ctx = makeContext({
51
+ model: { id: "kimi", provider: "kimi-coding" },
52
+ modelRegistry: { isUsingOAuth },
53
+ });
54
+
55
+ expect(
56
+ resolveFooterTelemetry(ctx, { settingsManager: settingsCapability({ enabled: false }) }),
57
+ ).toMatchObject({ subscription: true, autoCompaction: false });
58
+ expect(isUsingOAuth).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it("returns explicit false values when supported telemetry is disabled", () => {
62
+ expect(
63
+ resolveFooterTelemetry(makeContext(), {
64
+ settingsManager: settingsCapability({ enabled: false }),
65
+ }),
66
+ ).toEqual({ subscription: false, autoCompaction: false });
67
+ });
68
+
69
+ it.each([
70
+ [
71
+ "absent capabilities",
72
+ makeContext({ modelRegistry: {}, isProjectTrusted: undefined }),
73
+ { settingsManager: {} },
74
+ { subscription: undefined, autoCompaction: undefined },
75
+ ],
76
+ [
77
+ "throwing capabilities",
78
+ makeContext({
79
+ modelRegistry: {
80
+ isUsingOAuth: () => {
81
+ throw new Error("nope");
82
+ },
83
+ },
84
+ }),
85
+ { settingsManager: settingsCapability({ throwCreate: true }) },
86
+ { subscription: undefined, autoCompaction: undefined },
87
+ ],
88
+ [
89
+ "settings load errors",
90
+ makeContext(),
91
+ { settingsManager: settingsCapability({ enabled: true, errors: [new Error("bad json")] }) },
92
+ { subscription: false, autoCompaction: undefined },
93
+ ],
94
+ ] as const)("omits unsupported telemetry for %s", (_name, ctx, capabilities, expected) => {
95
+ expect(resolveFooterTelemetry(ctx, capabilities)).toEqual(expected);
96
+ });
97
+
98
+ it("omits subscription without a model", () => {
99
+ expect(
100
+ resolveFooterTelemetry(makeContext({ model: undefined }), {
101
+ settingsManager: settingsCapability({ enabled: false }),
102
+ }),
103
+ ).toEqual({ subscription: undefined, autoCompaction: false });
104
+ });
105
+ });
@@ -0,0 +1,327 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import type { Text } from "@earendil-works/pi-tui";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { TURN_SUMMARY_ENTRY_TYPE } from "../extensions/zentui/interaction-summary";
6
+
7
+ const stripTerminalSequences = stripVTControlCharacters;
8
+
9
+ const runtime = vi.hoisted(() => ({
10
+ enabled: true,
11
+ turnSummary: true,
12
+ tokens: true,
13
+ thought: true,
14
+ }));
15
+
16
+ vi.mock("../extensions/zentui/config", async (importOriginal) => {
17
+ const actual = await importOriginal<typeof import("../extensions/zentui/config")>();
18
+ return {
19
+ ...actual,
20
+ ensureConfigExists() {},
21
+ loadConfig: () => {
22
+ const config = structuredClone(actual.defaultConfig);
23
+ config.projectRefreshIntervalMs = 0;
24
+ config.components.editor.enabled = false;
25
+ config.components.userMessages.enabled = false;
26
+ config.components.selectorBorders.enabled = false;
27
+ config.components.footer.style = "native";
28
+ config.components.workingLine.enabled = runtime.enabled;
29
+ config.components.workingLine.turnSummary = runtime.turnSummary;
30
+ config.components.workingLine.segments.tokens = runtime.tokens;
31
+ config.components.workingLine.segments.thought = runtime.thought;
32
+ config.components.workingLine.messages = { custom: true, values: ["Stable"] };
33
+ return config;
34
+ },
35
+ };
36
+ });
37
+
38
+ import zentui from "../extensions/zentui/index";
39
+
40
+ type Handler = (event: unknown, ctx: unknown) => unknown | Promise<unknown>;
41
+
42
+ function assistant(input: number, output: number, timestamp: number) {
43
+ return {
44
+ role: "assistant",
45
+ usage: { input, output },
46
+ content: [],
47
+ api: "test",
48
+ provider: "provider",
49
+ model: "model",
50
+ stopReason: "stop",
51
+ timestamp,
52
+ };
53
+ }
54
+
55
+ function harness(options: { renderer?: boolean; appendError?: boolean } = {}) {
56
+ const handlers = new Map<string, Handler[]>();
57
+ const appended: Array<[string, unknown]> = [];
58
+ const appendEntry = vi.fn((type: string, data: unknown) => {
59
+ if (options.appendError) throw new Error("append failed");
60
+ appended.push([type, data]);
61
+ });
62
+ const renderers = new Map<string, unknown>();
63
+ const workingIndicators: Array<{ frames?: string[] }> = [];
64
+ const sendMessage = vi.fn();
65
+ zentui({
66
+ on(name: string, handler: Handler) {
67
+ handlers.set(name, [...(handlers.get(name) ?? []), handler]);
68
+ },
69
+ registerCommand() {},
70
+ ...(options.renderer === false
71
+ ? {}
72
+ : {
73
+ registerEntryRenderer(type: string, renderer: unknown) {
74
+ renderers.set(type, renderer);
75
+ },
76
+ }),
77
+ appendEntry,
78
+ sendMessage,
79
+ getThinkingLevel: () => "off",
80
+ } as never);
81
+ let idle = true;
82
+ const ctx = {
83
+ hasUI: true,
84
+ mode: "tui",
85
+ cwd: process.cwd(),
86
+ model: undefined,
87
+ isIdle: () => idle,
88
+ getContextUsage: () => null,
89
+ sessionManager: { getBranch: () => [], getSessionName: () => undefined },
90
+ ui: {
91
+ theme: {
92
+ fg: (_color: string, text: string) => text,
93
+ bold: (text: string) => text,
94
+ } as Theme,
95
+ getEditorComponent: () => undefined,
96
+ setWorkingMessage() {},
97
+ setWorkingIndicator(indicator?: { frames?: string[] }) {
98
+ if (indicator) workingIndicators.push(indicator);
99
+ },
100
+ },
101
+ };
102
+ const emit = async (name: string, event: unknown = {}) => {
103
+ for (const handler of handlers.get(name) ?? []) await handler(event, ctx);
104
+ };
105
+ return {
106
+ appended,
107
+ appendEntry,
108
+ renderers,
109
+ workingIndicators,
110
+ sendMessage,
111
+ ctx,
112
+ emit,
113
+ setIdle: (value: boolean) => (idle = value),
114
+ };
115
+ }
116
+
117
+ beforeEach(() => {
118
+ runtime.enabled = true;
119
+ runtime.turnSummary = true;
120
+ runtime.tokens = true;
121
+ runtime.thought = true;
122
+ vi.useRealTimers();
123
+ });
124
+
125
+ describe("turn summary lifecycle integration", () => {
126
+ it("appends exactly once at full settlement, never at agent_end, using only appendEntry", async () => {
127
+ vi.useFakeTimers();
128
+ vi.setSystemTime(1_000);
129
+ const current = harness();
130
+ await current.emit("session_start");
131
+ await current.emit("agent_start");
132
+ await current.emit("turn_start");
133
+ await current.emit("message_end", { message: assistant(1000, 10, 1) });
134
+ await current.emit("agent_end");
135
+ expect(current.appended).toEqual([]);
136
+ vi.setSystemTime(43_000);
137
+ await current.emit("agent_settled");
138
+ expect(current.appended).toEqual([
139
+ [
140
+ TURN_SUMMARY_ENTRY_TYPE,
141
+ {
142
+ version: 3,
143
+ durationMs: 42_000,
144
+ thoughtDurationMs: 0,
145
+ input: 1000,
146
+ output: 10,
147
+ stylePrefix: "\x1b[1;36m",
148
+ },
149
+ ],
150
+ ]);
151
+ await current.emit("agent_settled");
152
+ expect(current.appended).toHaveLength(1);
153
+ expect(current.sendMessage).not.toHaveBeenCalled();
154
+ expect(current.renderers.has(TURN_SUMMARY_ENTRY_TYPE)).toBe(true);
155
+ });
156
+
157
+ it("persists thought in v3 even when the live Thinking segment is disabled", async () => {
158
+ vi.useFakeTimers();
159
+ vi.setSystemTime(1_000);
160
+ runtime.thought = false;
161
+ const current = harness();
162
+ await current.emit("session_start");
163
+ await current.emit("agent_start");
164
+ await current.emit("turn_start");
165
+ const message = assistant(7_100, 779, 1);
166
+ await current.emit("message_update", {
167
+ message,
168
+ assistantMessageEvent: { type: "thinking_start", contentIndex: 0, partial: message },
169
+ });
170
+ const activeThoughtFrames = current.workingIndicators.at(-1)?.frames ?? [];
171
+ expect(activeThoughtFrames.length).toBeGreaterThan(0);
172
+ for (const frame of activeThoughtFrames) {
173
+ const liveText = stripTerminalSequences(frame);
174
+ expect(liveText).not.toMatch(/(?:^| · )thinking \d+s(?: · |$)/);
175
+ expect(liveText).not.toMatch(/(?:^| · )thought for \d+s(?: · |$)/);
176
+ }
177
+ vi.setSystemTime(11_999);
178
+ await current.emit("message_update", {
179
+ message,
180
+ assistantMessageEvent: {
181
+ type: "thinking_end",
182
+ contentIndex: 0,
183
+ content: "done",
184
+ partial: message,
185
+ },
186
+ });
187
+ await current.emit("message_end", { message });
188
+ await current.emit("agent_end");
189
+ vi.setSystemTime(57_999);
190
+ await current.emit("agent_settled");
191
+ expect(current.appended[0]?.[1]).toEqual(
192
+ expect.objectContaining({ version: 3, thoughtDurationMs: 10_999, input: 7_100, output: 779 }),
193
+ );
194
+ const renderer = current.renderers.get(TURN_SUMMARY_ENTRY_TYPE) as
195
+ | ((entry: { data: unknown }, options: unknown, theme: Theme) => Text | undefined)
196
+ | undefined;
197
+ const rendered = renderer?.({ data: current.appended[0]?.[1] }, {}, current.ctx.ui.theme)
198
+ ?.render(100)
199
+ .join("\n");
200
+ expect(stripTerminalSequences(rendered ?? "")).toContain("thought for 10s");
201
+ });
202
+
203
+ it.each([
204
+ [false, true],
205
+ [true, false],
206
+ ] as const)(
207
+ "gates append by Working-line enabled=%s and summary=%s",
208
+ async (enabled, summary) => {
209
+ runtime.enabled = enabled;
210
+ runtime.turnSummary = summary;
211
+ const current = harness();
212
+ await current.emit("session_start");
213
+ await current.emit("agent_start");
214
+ await current.emit("agent_end");
215
+ await current.emit("agent_settled");
216
+ expect(current.appended).toEqual([]);
217
+ },
218
+ );
219
+
220
+ it("initializes and settles without registerEntryRenderer", async () => {
221
+ const current = harness({ renderer: false });
222
+ await expect(current.emit("session_start")).resolves.toBeUndefined();
223
+ await current.emit("agent_start");
224
+ await current.emit("agent_end");
225
+ await expect(current.emit("agent_settled")).resolves.toBeUndefined();
226
+ expect(current.appended).toHaveLength(1);
227
+ });
228
+
229
+ it("contains append failures after atomic settlement and handles the next interaction", async () => {
230
+ const current = harness({ appendError: true });
231
+ await current.emit("session_start");
232
+ await current.emit("agent_start");
233
+ await current.emit("agent_end");
234
+ await expect(current.emit("agent_settled")).resolves.toBeUndefined();
235
+ expect(current.appendEntry).toHaveBeenCalledTimes(1);
236
+ await current.emit("agent_settled");
237
+ expect(current.appendEntry).toHaveBeenCalledTimes(1);
238
+
239
+ await current.emit("agent_start");
240
+ await current.emit("agent_end");
241
+ await current.emit("agent_settled");
242
+ expect(current.appendEntry).toHaveBeenCalledTimes(2);
243
+ });
244
+
245
+ it("partitions a completed run from concurrent live usage on non-idle settlement", async () => {
246
+ vi.useFakeTimers();
247
+ vi.setSystemTime(1_000);
248
+ const current = harness();
249
+ await current.emit("session_start");
250
+ await current.emit("agent_start");
251
+ await current.emit("turn_start");
252
+ await current.emit("message_end", { message: assistant(10, 1, 1) });
253
+ await current.emit("agent_end");
254
+
255
+ vi.setSystemTime(5_000);
256
+ await current.emit("agent_start");
257
+ await current.emit("turn_start");
258
+ await current.emit("message_update", { message: assistant(7, 2, 2) });
259
+ current.setIdle(false);
260
+ vi.setSystemTime(6_000);
261
+ await current.emit("agent_settled");
262
+ expect(current.appended).toEqual([
263
+ [
264
+ TURN_SUMMARY_ENTRY_TYPE,
265
+ expect.objectContaining({
266
+ version: 3,
267
+ durationMs: 5_000,
268
+ thoughtDurationMs: 0,
269
+ input: 10,
270
+ output: 1,
271
+ }),
272
+ ],
273
+ ]);
274
+
275
+ await current.emit("message_end", { message: assistant(9, 3, 2) });
276
+ await current.emit("agent_end");
277
+ current.setIdle(true);
278
+ vi.setSystemTime(9_000);
279
+ await current.emit("agent_settled");
280
+ expect(current.appended[1]).toEqual([
281
+ TURN_SUMMARY_ENTRY_TYPE,
282
+ expect.objectContaining({
283
+ version: 3,
284
+ durationMs: 4_000,
285
+ thoughtDurationMs: 0,
286
+ input: 9,
287
+ output: 3,
288
+ }),
289
+ ]);
290
+ await current.emit("agent_settled");
291
+ expect(current.appended).toHaveLength(2);
292
+ });
293
+
294
+ it("commits and settles zero-only final usage exactly once", async () => {
295
+ const current = harness();
296
+ await current.emit("session_start");
297
+ await current.emit("agent_start");
298
+ await current.emit("turn_start");
299
+ await current.emit("message_update", { message: assistant(0, 0, 1) });
300
+ await current.emit("message_update", { message: assistant(0, 0, 1) });
301
+ await current.emit("message_end", { message: assistant(0, 0, 1) });
302
+ await current.emit("agent_end");
303
+ await current.emit("agent_settled");
304
+ await current.emit("agent_settled");
305
+ expect(current.appended).toEqual([
306
+ [TURN_SUMMARY_ENTRY_TYPE, expect.objectContaining({ input: 0, output: 0 })],
307
+ ]);
308
+ });
309
+
310
+ it("includes zero totals when live Tokens is disabled", async () => {
311
+ runtime.tokens = false;
312
+ const current = harness();
313
+ await current.emit("session_start");
314
+ await current.emit("agent_start");
315
+ await current.emit("agent_end");
316
+ await current.emit("agent_settled");
317
+ expect(current.appended[0]?.[1]).toMatchObject({ input: 0, output: 0 });
318
+ });
319
+
320
+ it("does not synthesize a summary on shutdown", async () => {
321
+ const current = harness();
322
+ await current.emit("session_start");
323
+ await current.emit("agent_start");
324
+ await current.emit("session_shutdown");
325
+ expect(current.appended).toEqual([]);
326
+ });
327
+ });