@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,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
+ });