@selesai/code 0.9.15 → 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 (267) hide show
  1. package/CHANGELOG.md +22 -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/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/package.json +3 -3
  193. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  194. package/dist/extensions/auto-model/classifier.ts +0 -23
  195. package/dist/extensions/auto-model/config.ts +0 -51
  196. package/dist/extensions/auto-model/index.ts +0 -22
  197. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  198. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  199. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  200. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  201. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  202. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  203. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  204. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  205. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  206. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  207. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  208. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  209. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  210. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  211. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  212. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  213. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  214. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  215. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  216. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  217. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  218. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  219. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  220. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  221. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  222. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  223. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  224. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  225. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  226. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  227. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  228. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  229. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  230. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  231. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  232. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  233. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  234. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  235. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  236. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  237. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  238. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  239. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  240. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  241. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  242. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  243. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  244. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  245. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  246. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  247. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  248. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  249. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  250. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  251. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  252. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  253. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  254. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  255. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  256. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  257. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  258. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  259. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  260. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  261. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  262. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  263. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  264. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  265. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  266. package/dist/extensions/preview-tools-disabled.ts +0 -11
  267. 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
+ });