@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,730 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import { Loader, visibleWidth } from "@earendil-works/pi-tui";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const stripTerminalSequences = stripVTControlCharacters;
7
+
8
+ const runtime = vi.hoisted(() => ({
9
+ enabled: true,
10
+ custom: true,
11
+ message: "Stable",
12
+ spinner: "star-bloom" as "star-bloom" | "pulse",
13
+ spinnerIntervalMs: 100,
14
+ textIntervalMs: 60,
15
+ }));
16
+
17
+ vi.mock("../extensions/zentui/config", async (importOriginal) => {
18
+ const actual = await importOriginal<typeof import("../extensions/zentui/config")>();
19
+ return {
20
+ ...actual,
21
+ ensureConfigExists() {},
22
+ loadConfig: () => {
23
+ const config = structuredClone(actual.defaultConfig);
24
+ config.projectRefreshIntervalMs = 0;
25
+ config.components.editor.enabled = false;
26
+ config.components.userMessages.enabled = false;
27
+ config.components.selectorBorders.enabled = false;
28
+ config.components.footer.style = "native";
29
+ config.components.workingLine.enabled = runtime.enabled;
30
+ config.components.workingLine.spinner = runtime.spinner;
31
+ config.components.workingLine.spinnerIntervalMs = runtime.spinnerIntervalMs;
32
+ config.components.workingLine.textIntervalMs = runtime.textIntervalMs;
33
+ config.components.workingLine.messages = {
34
+ custom: runtime.custom,
35
+ values: [runtime.message],
36
+ };
37
+ return config;
38
+ },
39
+ };
40
+ });
41
+
42
+ import zentui from "../extensions/zentui/index";
43
+
44
+ type Handler = (event: unknown, ctx: unknown) => unknown | Promise<unknown>;
45
+
46
+ function theme(): Theme {
47
+ return {
48
+ fg(color: string, text: string) {
49
+ const codes: Record<string, number> = { dim: 90, muted: 36, accent: 96 };
50
+ return `\x1b[${codes[color] ?? 37}m${text}\x1b[0m`;
51
+ },
52
+ bold: (text: string) => `\x1b[1m${text}\x1b[0m`,
53
+ } as Theme;
54
+ }
55
+
56
+ function loaderPhase(rendered: string): [string, number | undefined] {
57
+ const stripped = stripTerminalSequences(rendered);
58
+ const plain = stripped.trimStart();
59
+ const separatorIndex = plain.indexOf(" ");
60
+ if (separatorIndex < 0) throw new Error("Expected a spinner separator in the working row");
61
+ const spinner = plain.slice(0, separatorIndex);
62
+ const textOffset =
63
+ visibleWidth(stripped) - visibleWidth(plain) + visibleWidth(plain.slice(0, separatorIndex + 1));
64
+ const marker = [...rendered.matchAll(/\x1b\[96m\x1b\[1m/g)].find((match) => {
65
+ if (match.index === undefined) return false;
66
+ return visibleWidth(stripTerminalSequences(rendered.slice(0, match.index))) >= textOffset;
67
+ });
68
+ return [
69
+ spinner,
70
+ marker?.index === undefined
71
+ ? undefined
72
+ : visibleWidth(stripTerminalSequences(rendered.slice(0, marker.index))) - textOffset,
73
+ ];
74
+ }
75
+
76
+ function loadExtension() {
77
+ const handlers = new Map<string, Handler[]>();
78
+ zentui({
79
+ registerEntryRenderer() {},
80
+ appendEntry() {},
81
+ on(name: string, handler: Handler) {
82
+ handlers.set(name, [...(handlers.get(name) ?? []), handler]);
83
+ },
84
+ registerCommand() {},
85
+ getThinkingLevel: () => "off",
86
+ } as never);
87
+ return handlers;
88
+ }
89
+
90
+ async function emit(
91
+ handlers: Map<string, Handler[]>,
92
+ name: string,
93
+ ctx: unknown,
94
+ event: unknown = {},
95
+ ) {
96
+ for (const handler of handlers.get(name) ?? []) await handler(event, ctx);
97
+ }
98
+
99
+ function harness() {
100
+ const calls: Array<[string, unknown?]> = [];
101
+ const forbidden = vi.fn();
102
+ let indicator: { frames?: string[]; intervalMs?: number } | undefined;
103
+ let message = "";
104
+ let loader: Loader | undefined;
105
+ const ui = {
106
+ theme: theme(),
107
+ getEditorComponent: () => undefined,
108
+ setEditorComponent: forbidden,
109
+ setFooter: forbidden,
110
+ setWidget: forbidden,
111
+ setWorkingVisible: forbidden,
112
+ setWorkingMessage(value?: string) {
113
+ calls.push(["message", value]);
114
+ message = value ?? "Working...";
115
+ loader?.setMessage(message);
116
+ },
117
+ setWorkingIndicator(value?: unknown) {
118
+ calls.push(["indicator", value]);
119
+ indicator = value as typeof indicator;
120
+ loader?.setIndicator(indicator);
121
+ },
122
+ };
123
+ return {
124
+ calls,
125
+ forbidden,
126
+ activateLoader() {
127
+ loader = new Loader(
128
+ { requestRender() {} } as never,
129
+ (text) => text,
130
+ (text) => text,
131
+ message,
132
+ indicator,
133
+ );
134
+ return loader;
135
+ },
136
+ ctx: {
137
+ hasUI: true,
138
+ mode: "tui",
139
+ cwd: process.cwd(),
140
+ model: undefined,
141
+ getContextUsage: () => null,
142
+ sessionManager: { getBranch: () => [], getSessionName: () => undefined },
143
+ ui,
144
+ },
145
+ };
146
+ }
147
+
148
+ beforeEach(() => {
149
+ runtime.enabled = true;
150
+ runtime.custom = true;
151
+ runtime.message = "Stable";
152
+ runtime.spinner = "star-bloom";
153
+ runtime.spinnerIntervalMs = 100;
154
+ runtime.textIntervalMs = 60;
155
+ });
156
+
157
+ describe("working-line extension lifecycle integration", () => {
158
+ it("wires full-row rebuilds, authoritative usage, parallel tools, and isolated cleanup", async () => {
159
+ const handlers = loadExtension();
160
+ const current = harness();
161
+ const row = () => {
162
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
163
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
164
+ };
165
+ await emit(handlers, "session_start", current.ctx);
166
+ expect(current.calls.slice(0, 2).map(([name, value]) => [name, value])).toEqual([
167
+ ["message", ""],
168
+ ["indicator", expect.any(Object)],
169
+ ]);
170
+ await emit(handlers, "agent_start", current.ctx);
171
+ await emit(handlers, "turn_start", current.ctx, { turnIndex: 0, timestamp: Date.now() });
172
+ const partialAssistant = {
173
+ role: "assistant",
174
+ usage: { input: 100, output: 4 },
175
+ content: [],
176
+ api: "google-generative-ai",
177
+ provider: "google",
178
+ model: "test",
179
+ stopReason: "toolUse",
180
+ timestamp: Date.now(),
181
+ };
182
+ const laterPartialAssistant = { ...partialAssistant, usage: { input: 120, output: 7 } };
183
+ const finalAssistant = { ...partialAssistant, usage: { input: 150, output: 9 } };
184
+ await emit(handlers, "message_update", current.ctx, { message: partialAssistant });
185
+ expect(row()).toMatch(/Stable · \d+s · ↑100 ↓4/);
186
+ const beforeDuplicate = current.calls.length;
187
+ await emit(handlers, "message_update", current.ctx, { message: partialAssistant });
188
+ expect(current.calls).toHaveLength(beforeDuplicate);
189
+ await emit(handlers, "message_update", current.ctx, { message: laterPartialAssistant });
190
+ expect(row()).toMatch(/Stable · \d+s · ↑120 ↓7/);
191
+ await emit(handlers, "message_end", current.ctx, { message: finalAssistant });
192
+ expect(row()).toMatch(/Stable · \d+s · ↑150 ↓9/);
193
+ await emit(handlers, "tool_execution_start", current.ctx, {
194
+ toolCallId: "one",
195
+ toolName: "read",
196
+ });
197
+ expect(row()).toMatch(/Stable · read · \d+s · ↑150 ↓9/);
198
+ await emit(handlers, "tool_execution_start", current.ctx, {
199
+ toolCallId: "two",
200
+ toolName: "bash",
201
+ });
202
+ expect(row()).toMatch(/Stable · bash · \d+s · ↑150 ↓9/);
203
+ await emit(handlers, "tool_execution_end", current.ctx, { toolCallId: "two" });
204
+ expect(row()).toMatch(/Stable · read · \d+s · ↑150 ↓9/);
205
+ await emit(handlers, "tool_execution_end", current.ctx, { toolCallId: "one" });
206
+ expect(row()).toMatch(/Stable · \d+s · ↑150 ↓9/);
207
+ await emit(handlers, "turn_start", current.ctx, {
208
+ turnIndex: 1,
209
+ timestamp: Date.now(),
210
+ });
211
+ expect(row()).toContain("↑150 ↓9");
212
+ await emit(handlers, "agent_end", current.ctx);
213
+ expect(current.calls.filter(([name]) => name === "message")).toEqual([["message", ""]]);
214
+ expect(current.forbidden).not.toHaveBeenCalled();
215
+ const beforeLateEnds = current.calls.length;
216
+ await emit(handlers, "message_end", current.ctx, {
217
+ message: { role: "user", usage: { input: 99, output: 99 } },
218
+ });
219
+ await emit(handlers, "message_end", current.ctx, {
220
+ message: { ...finalAssistant, responseId: "late-after-agent" },
221
+ });
222
+ expect(current.calls).toHaveLength(beforeLateEnds);
223
+ await emit(handlers, "session_shutdown", current.ctx);
224
+ expect(current.calls.slice(-2).map(([name, value]) => [name, value])).toEqual([
225
+ ["indicator", undefined],
226
+ ["message", undefined],
227
+ ]);
228
+ expect(current.forbidden).not.toHaveBeenCalled();
229
+ });
230
+
231
+ it("compacts live provider usage", async () => {
232
+ const handlers = loadExtension();
233
+ const current = harness();
234
+ const row = () => {
235
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
236
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
237
+ };
238
+ const message = (input: number, output: number) => ({
239
+ role: "assistant",
240
+ usage: { input, output },
241
+ content: [],
242
+ responseId: "reported",
243
+ });
244
+
245
+ await emit(handlers, "session_start", current.ctx);
246
+ await emit(handlers, "agent_start", current.ctx);
247
+ await emit(handlers, "turn_start", current.ctx);
248
+ await emit(handlers, "message_update", current.ctx, { message: message(27_000, 1_400) });
249
+ expect(row()).toContain("↑27k ↓1.4k");
250
+ await emit(handlers, "message_end", current.ctx, { message: message(27_000, 1_400) });
251
+ expect(row()).toContain("↑27k ↓1.4k");
252
+ });
253
+
254
+ it("visibly reconciles an estimated 1.0k output to exact 999", async () => {
255
+ const handlers = loadExtension();
256
+ const current = harness();
257
+ const row = () => {
258
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
259
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
260
+ };
261
+ const message = (output: number) => ({
262
+ role: "assistant",
263
+ usage: { input: 0, output },
264
+ content: [],
265
+ responseId: "estimated-boundary",
266
+ });
267
+
268
+ await emit(handlers, "session_start", current.ctx);
269
+ await emit(handlers, "agent_start", current.ctx);
270
+ await emit(handlers, "turn_start", current.ctx);
271
+ const partial = message(998);
272
+ await emit(handlers, "message_update", current.ctx, { message: partial });
273
+ await emit(handlers, "message_update", current.ctx, {
274
+ message: partial,
275
+ assistantMessageEvent: {
276
+ type: "text_delta",
277
+ contentIndex: 0,
278
+ delta: "abcdefgh",
279
+ partial: { content: [{ type: "text", text: "abcdefgh" }] },
280
+ },
281
+ });
282
+ expect(row()).toContain("↑0 ↓1.0k");
283
+ await emit(handlers, "message_end", current.ctx, { message: message(999) });
284
+ expect(row()).toContain("↑0 ↓999");
285
+ });
286
+
287
+ it("keeps cumulative billion-scale totals compact across continuations", async () => {
288
+ runtime.message = "Response usage stays visible here";
289
+ const handlers = loadExtension();
290
+ const current = harness();
291
+ const row = () => {
292
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
293
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
294
+ };
295
+ const assistant = (input: number, output: number, responseId: string) => ({
296
+ role: "assistant",
297
+ usage: { input, output },
298
+ content: [],
299
+ api: "openai-completions",
300
+ provider: "openai-compatible",
301
+ model: "test",
302
+ stopReason: "toolUse",
303
+ timestamp: Date.now(),
304
+ responseId,
305
+ });
306
+
307
+ await emit(handlers, "session_start", current.ctx);
308
+ await emit(handlers, "agent_start", current.ctx);
309
+ await emit(handlers, "turn_start", current.ctx);
310
+ await emit(handlers, "message_end", current.ctx, {
311
+ message: assistant(1_000_000_000, 1_000_000_000, "first"),
312
+ });
313
+ expect(row()).toContain("↑1000M ↓1000M");
314
+
315
+ await emit(handlers, "turn_start", current.ctx);
316
+ expect(row()).toContain("↑1000M ↓1000M");
317
+ await emit(handlers, "tool_execution_start", current.ctx, {
318
+ toolCallId: "wide",
319
+ toolName: "123456789012345678",
320
+ });
321
+ const writesBeforeLiveUsage = current.calls.length;
322
+ await emit(handlers, "message_update", current.ctx, {
323
+ message: assistant(12, 3, "second"),
324
+ });
325
+ expect(row()).toContain("↑1000M ↓1000M");
326
+ expect(current.calls).toHaveLength(writesBeforeLiveUsage);
327
+
328
+ await emit(handlers, "tool_execution_end", current.ctx, { toolCallId: "wide" });
329
+ await emit(handlers, "message_end", current.ctx, {
330
+ message: assistant(14, 5, "second"),
331
+ });
332
+ expect(row()).toContain("↑1000M ↓1000M");
333
+
334
+ await emit(handlers, "turn_start", current.ctx);
335
+ await emit(handlers, "message_end", current.ctx, {
336
+ message: assistant(499_986, 499_995, "third"),
337
+ });
338
+ expect(row()).toContain("↑1001M ↓1001M");
339
+ });
340
+
341
+ it("keeps committed tokens through retry lifecycle and initial zero usage, then accumulates final usage", async () => {
342
+ const handlers = loadExtension();
343
+ const current = harness();
344
+ const row = () => {
345
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
346
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
347
+ };
348
+ const assistant = (input: number, output: number, responseId: string) => ({
349
+ role: "assistant",
350
+ usage: { input, output },
351
+ provider: "mistral",
352
+ responseId,
353
+ });
354
+
355
+ await emit(handlers, "session_start", current.ctx);
356
+ await emit(handlers, "agent_start", current.ctx);
357
+ await emit(handlers, "turn_start", current.ctx);
358
+ await emit(handlers, "message_end", current.ctx, {
359
+ message: assistant(20, 4, "committed"),
360
+ });
361
+ expect(row()).toContain("↑20 ↓4");
362
+
363
+ await emit(handlers, "agent_end", current.ctx);
364
+ expect(row()).toContain("↑20 ↓4");
365
+ await emit(handlers, "agent_start", current.ctx);
366
+ expect(row()).toContain("↑20 ↓4");
367
+ await emit(handlers, "turn_start", current.ctx);
368
+ expect(row()).toContain("↑20 ↓4");
369
+ await emit(handlers, "message_update", current.ctx, {
370
+ message: assistant(0, 0, "retry"),
371
+ });
372
+ expect(row()).toContain("↑20 ↓4");
373
+ await emit(handlers, "message_end", current.ctx, {
374
+ message: assistant(5, 2, "retry"),
375
+ });
376
+ expect(row()).toContain("↑25 ↓6");
377
+ });
378
+
379
+ it("shows OpenAI Codex Responses placeholder usage as a zero estimate until exact final usage", async () => {
380
+ const handlers = loadExtension();
381
+ const current = harness();
382
+ const row = () => {
383
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
384
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
385
+ };
386
+ const partial = {
387
+ role: "assistant",
388
+ content: [{ type: "text", text: "streaming" }],
389
+ // Codex Responses reports exact usage only on terminal response.completed.
390
+ usage: { input: 0, output: 0 },
391
+ provider: "openai-codex",
392
+ model: "gpt-5.4",
393
+ responseId: "terminal-only",
394
+ };
395
+ await emit(handlers, "session_start", current.ctx);
396
+ await emit(handlers, "agent_start", current.ctx);
397
+ await emit(handlers, "turn_start", current.ctx);
398
+ const writesBeforeUpdates = current.calls.length;
399
+ await emit(handlers, "message_update", current.ctx, { message: partial });
400
+ await emit(handlers, "message_update", current.ctx, { message: partial });
401
+ await emit(handlers, "message_update", current.ctx, {
402
+ message: { role: "assistant", usage: { input: 2.5, output: 1 } },
403
+ });
404
+ await emit(handlers, "message_update", current.ctx, {
405
+ message: { role: "user", usage: { input: 99, output: 99 } },
406
+ });
407
+ expect(current.calls).toHaveLength(writesBeforeUpdates + 1);
408
+ expect(row()).toContain("↑0 ↓0");
409
+ await emit(handlers, "message_end", current.ctx, {
410
+ message: { ...partial, usage: { input: 42, output: 6 } },
411
+ });
412
+ expect(row()).toContain("↑42 ↓6");
413
+ });
414
+
415
+ it("preserves approximation across rejected finals and ignores late messages", async () => {
416
+ const handlers = loadExtension();
417
+ const current = harness();
418
+ const row = () => {
419
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
420
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
421
+ };
422
+ const assistantMessage = (input: number, output: number, responseId: string) => ({
423
+ role: "assistant",
424
+ usage: { input, output },
425
+ content: [],
426
+ responseId,
427
+ });
428
+ const partial = assistantMessage(0, 0, "active");
429
+ await emit(handlers, "session_start", current.ctx);
430
+ await emit(handlers, "agent_start", current.ctx);
431
+ await emit(handlers, "turn_start", current.ctx);
432
+ await emit(handlers, "message_end", current.ctx, {
433
+ message: assistantMessage(1, 1, "already-committed"),
434
+ });
435
+ await emit(handlers, "turn_start", current.ctx);
436
+ await emit(handlers, "message_update", current.ctx, { message: partial });
437
+ expect(row()).toContain("↑1 ↓1");
438
+ const beforeRejected = current.calls.length;
439
+ await emit(handlers, "message_end", current.ctx, {
440
+ message: assistantMessage(90, 9, "mismatched"),
441
+ });
442
+ expect(current.calls).toHaveLength(beforeRejected);
443
+ expect(row()).toContain("↑1 ↓1");
444
+ await emit(handlers, "message_end", current.ctx, {
445
+ message: assistantMessage(90, 9, "already-committed"),
446
+ });
447
+ expect(current.calls).toHaveLength(beforeRejected);
448
+ expect(row()).toContain("↑1 ↓1");
449
+ const beforeLateMessages = current.calls.length;
450
+ await emit(handlers, "message_end", current.ctx, {
451
+ message: { role: "user", usage: { input: 99, output: 99 } },
452
+ });
453
+ await emit(handlers, "message_end", current.ctx, {
454
+ message: assistantMessage(99, 99, "late-unmatched"),
455
+ });
456
+ expect(current.calls).toHaveLength(beforeLateMessages);
457
+ expect(row()).toContain("↑1 ↓1");
458
+ await emit(handlers, "message_end", current.ctx, {
459
+ message: assistantMessage(7, 2, "active"),
460
+ });
461
+ expect(row()).toContain("↑8 ↓3");
462
+ const beforeDuplicate = current.calls.length;
463
+ await emit(handlers, "message_end", current.ctx, {
464
+ message: assistantMessage(99, 99, "active"),
465
+ });
466
+ expect(current.calls).toHaveLength(beforeDuplicate);
467
+ expect(row()).toContain("↑8 ↓3");
468
+ });
469
+
470
+ it("streams OpenCode tool-call arguments immediately and reconciles whole-interaction totals", async () => {
471
+ const handlers = loadExtension();
472
+ const current = harness();
473
+ const rows = () =>
474
+ current.calls
475
+ .filter(([name, value]) => name === "indicator" && value !== undefined)
476
+ .map(([, value]) =>
477
+ stripTerminalSequences((value as { frames?: string[] }).frames?.[0] ?? ""),
478
+ );
479
+ const message = (
480
+ input: number,
481
+ output: number,
482
+ responseId: string,
483
+ content: unknown[] = [],
484
+ ) => ({
485
+ role: "assistant",
486
+ usage: { input, output },
487
+ content,
488
+ api: "openai-completions",
489
+ provider: "opencode",
490
+ model: "openai/gpt-5",
491
+ stopReason: "toolUse",
492
+ timestamp: Date.now(),
493
+ responseId,
494
+ });
495
+ const toolCall = { type: "toolCall", id: "call-1", name: "bash", arguments: {} };
496
+
497
+ await emit(handlers, "session_start", current.ctx);
498
+ await emit(handlers, "agent_start", current.ctx);
499
+ await emit(handlers, "turn_start", current.ctx);
500
+ await emit(handlers, "message_end", current.ctx, { message: message(10, 2, "first") });
501
+ await emit(handlers, "turn_start", current.ctx);
502
+ const partial = message(0, 0, "tool-response", [toolCall]);
503
+ await emit(handlers, "message_update", current.ctx, {
504
+ message: partial,
505
+ assistantMessageEvent: {
506
+ type: "toolcall_start",
507
+ contentIndex: 0,
508
+ partial,
509
+ },
510
+ });
511
+ const beforeDeltas = current.calls.length;
512
+ await emit(handlers, "message_update", current.ctx, {
513
+ message: partial,
514
+ assistantMessageEvent: {
515
+ type: "toolcall_delta",
516
+ contentIndex: 0,
517
+ delta: '{"command":',
518
+ partial,
519
+ },
520
+ });
521
+ await emit(handlers, "message_update", current.ctx, {
522
+ message: partial,
523
+ assistantMessageEvent: {
524
+ type: "toolcall_delta",
525
+ contentIndex: 0,
526
+ delta: '{"command":',
527
+ partial,
528
+ },
529
+ });
530
+ expect(current.calls).toHaveLength(beforeDeltas + 2);
531
+ expect(rows().some((row) => row.includes("↑10 ↓5"))).toBe(true);
532
+ expect(rows().at(-1)).toContain("↑10 ↓8");
533
+ expect(rows().at(-1)).not.toMatch(/thinking|thought for/);
534
+ const beforeEnd = current.calls.length;
535
+ await emit(handlers, "message_update", current.ctx, {
536
+ message: partial,
537
+ assistantMessageEvent: {
538
+ type: "toolcall_end",
539
+ contentIndex: 0,
540
+ toolCall,
541
+ partial,
542
+ },
543
+ });
544
+ expect(current.calls).toHaveLength(beforeEnd);
545
+ await emit(handlers, "message_end", current.ctx, {
546
+ message: message(15, 4, "tool-response", [toolCall]),
547
+ });
548
+ expect(rows().at(-1)).toContain("↑25 ↓6");
549
+ expect(rows().at(-1)).not.toMatch(/thinking|thought for/);
550
+ });
551
+
552
+ it("preserves estimated values on malformed finals and reconciles exact finals", async () => {
553
+ const handlers = loadExtension();
554
+ const current = harness();
555
+ const row = () => {
556
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
557
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
558
+ };
559
+ const message = (input: number, output: number, responseId: string) => ({
560
+ role: "assistant",
561
+ usage: { input, output },
562
+ content: [],
563
+ responseId,
564
+ });
565
+
566
+ await emit(handlers, "session_start", current.ctx);
567
+ await emit(handlers, "agent_start", current.ctx);
568
+ await emit(handlers, "turn_start", current.ctx);
569
+ await emit(handlers, "message_update", current.ctx, {
570
+ message: message(0, 0, "malformed"),
571
+ assistantMessageEvent: {
572
+ type: "text_delta",
573
+ contentIndex: 0,
574
+ delta: "abcdefghijkl",
575
+ partial: { content: [{ type: "text", text: "abcdefghijkl" }] },
576
+ },
577
+ });
578
+ expect(row()).toContain("↑0 ↓3");
579
+ await emit(handlers, "message_end", current.ctx, {
580
+ message: message(-1, 9, "malformed"),
581
+ });
582
+ expect(row()).toContain("↑0 ↓3");
583
+
584
+ await emit(handlers, "turn_start", current.ctx);
585
+ await emit(handlers, "message_update", current.ctx, {
586
+ message: message(0, 0, "exact"),
587
+ assistantMessageEvent: {
588
+ type: "text_delta",
589
+ contentIndex: 0,
590
+ delta: "abcdefgh",
591
+ partial: { content: [{ type: "text", text: "abcdefgh" }] },
592
+ },
593
+ });
594
+ expect(row()).toContain("↑0 ↓2");
595
+ await emit(handlers, "message_end", current.ctx, {
596
+ message: message(4, 1, "exact"),
597
+ });
598
+ expect(row()).toContain("↑4 ↓1");
599
+ });
600
+
601
+ it("ignores an all-zero placeholder after nonzero live usage", async () => {
602
+ const handlers = loadExtension();
603
+ const current = harness();
604
+ const row = () => {
605
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
606
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
607
+ };
608
+ const message = (input: number, output: number) => ({
609
+ role: "assistant",
610
+ usage: { input, output },
611
+ provider: "mistral",
612
+ responseId: "zero-snapshot",
613
+ });
614
+ await emit(handlers, "session_start", current.ctx);
615
+ await emit(handlers, "agent_start", current.ctx);
616
+ await emit(handlers, "turn_start", current.ctx);
617
+ await emit(handlers, "message_update", current.ctx, { message: message(20, 4) });
618
+ expect(row()).toContain("↑20 ↓4");
619
+ await emit(handlers, "message_update", current.ctx, { message: message(0, 0) });
620
+ expect(row()).toContain("↑20 ↓4");
621
+ await emit(handlers, "message_end", current.ctx, { message: message(25, 7) });
622
+ expect(row()).toContain("↑25 ↓7");
623
+ });
624
+
625
+ it.each(["star-bloom", "pulse"] as const)(
626
+ "rebases the %s spinner and animated text on delayed Loader activation and later rebuilds",
627
+ async (spinner) => {
628
+ runtime.spinner = spinner;
629
+ vi.useFakeTimers();
630
+ vi.setSystemTime(0);
631
+ const handlers = loadExtension();
632
+ const current = harness();
633
+ let loader: Loader | undefined;
634
+ try {
635
+ await emit(handlers, "session_start", current.ctx);
636
+ vi.advanceTimersByTime(5000);
637
+ loader = current.activateLoader();
638
+ const activatedAtFrameZero = loader.render(80)[1] ?? "";
639
+ await emit(handlers, "agent_start", current.ctx);
640
+ await emit(handlers, "turn_start", current.ctx, { turnIndex: 0, timestamp: Date.now() });
641
+ expect(current.calls.filter(([name]) => name === "indicator")).toHaveLength(2);
642
+ expect(loaderPhase(loader.render(80)[1] ?? "")).toEqual(loaderPhase(activatedAtFrameZero));
643
+
644
+ vi.advanceTimersByTime(900);
645
+ const beforeRebuild = loader.render(80)[1] ?? "";
646
+ const beforePhase = loaderPhase(beforeRebuild);
647
+ expect(beforePhase[1], "expected animated-text high tier before rebuild").toBeDefined();
648
+ const assistant = {
649
+ role: "assistant",
650
+ usage: { input: 12, output: 3 },
651
+ content: [],
652
+ api: "google-generative-ai",
653
+ provider: "google",
654
+ model: "test",
655
+ stopReason: "toolUse",
656
+ timestamp: Date.now(),
657
+ };
658
+ await emit(handlers, "message_update", current.ctx, { message: assistant });
659
+ const afterRebuild = loader.render(80)[1] ?? "";
660
+ expect(stripTerminalSequences(afterRebuild)).toContain("↑12 ↓3");
661
+ const afterPhase = loaderPhase(afterRebuild);
662
+ expect(afterPhase[1], "expected animated-text high tier after rebuild").toBeDefined();
663
+ expect(afterPhase).toEqual(beforePhase);
664
+ const writesAfterRebuild = current.calls.length;
665
+ await emit(handlers, "message_update", current.ctx, { message: assistant });
666
+ expect(current.calls).toHaveLength(writesAfterRebuild);
667
+ } finally {
668
+ loader?.stop();
669
+ await emit(handlers, "session_shutdown", current.ctx);
670
+ vi.useRealTimers();
671
+ }
672
+ },
673
+ );
674
+
675
+ it("regenerates repeated meaningful streaming rows within the worst bounded scheduler budget", async () => {
676
+ runtime.message = "m".repeat(43);
677
+ runtime.spinnerIntervalMs = 997;
678
+ runtime.textIntervalMs = 900;
679
+ const handlers = loadExtension();
680
+ const current = harness();
681
+ await emit(handlers, "session_start", current.ctx);
682
+ await emit(handlers, "agent_start", current.ctx);
683
+ await emit(handlers, "turn_start", current.ctx);
684
+ const callsBeforeStreaming = current.calls.length;
685
+ const started = performance.now();
686
+ for (let output = 1; output <= 20; output++) {
687
+ await emit(handlers, "message_update", current.ctx, {
688
+ message: {
689
+ role: "assistant",
690
+ usage: { input: 999_999_999, output },
691
+ responseId: "performance-stream",
692
+ },
693
+ });
694
+ }
695
+ const elapsedMs = performance.now() - started;
696
+ expect(current.calls).toHaveLength(callsBeforeStreaming + 20);
697
+ expect(elapsedMs).toBeLessThan(10_000);
698
+ await emit(handlers, "session_shutdown", current.ctx);
699
+ });
700
+
701
+ it("owns the full fallback row while custom messages are off and releases both APIs", async () => {
702
+ runtime.custom = false;
703
+ const handlers = loadExtension();
704
+ const current = harness();
705
+ await emit(handlers, "session_start", current.ctx);
706
+ await emit(handlers, "agent_start", current.ctx);
707
+ await emit(handlers, "turn_start", current.ctx, { turnIndex: 0, timestamp: Date.now() });
708
+ await emit(handlers, "agent_end", current.ctx);
709
+ expect(current.calls[0]).toEqual(["message", ""]);
710
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] };
711
+ expect(stripTerminalSequences(indicator.frames?.[0] ?? "")).toContain("Working…");
712
+ await emit(handlers, "session_shutdown", current.ctx);
713
+ expect(current.calls.slice(-2)).toEqual([
714
+ ["indicator", undefined],
715
+ ["message", undefined],
716
+ ]);
717
+ });
718
+
719
+ it("makes zero working-row calls when startup is disabled", async () => {
720
+ runtime.enabled = false;
721
+ const handlers = loadExtension();
722
+ const current = harness();
723
+ await emit(handlers, "session_start", current.ctx);
724
+ await emit(handlers, "agent_start", current.ctx);
725
+ await emit(handlers, "agent_end", current.ctx);
726
+ await emit(handlers, "session_shutdown", current.ctx);
727
+ expect(current.calls).toEqual([]);
728
+ expect(current.forbidden).not.toHaveBeenCalled();
729
+ });
730
+ });